sparkdesign 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/cli/registry/basic/alert-dialog.tsx +3 -6
  2. package/cli/registry/basic/button.tsx +19 -6
  3. package/cli/registry/basic/card.tsx +20 -8
  4. package/cli/registry/basic/collapsible-card.tsx +2 -4
  5. package/cli/registry/basic/combobox.tsx +104 -46
  6. package/cli/registry/basic/context-menu.tsx +2 -3
  7. package/cli/registry/basic/date-picker.tsx +78 -7
  8. package/cli/registry/basic/dialog.tsx +3 -8
  9. package/cli/registry/basic/drawer.tsx +3 -5
  10. package/cli/registry/basic/dropdown-menu.tsx +2 -3
  11. package/cli/registry/basic/hover-card.tsx +2 -3
  12. package/cli/registry/basic/icon-button.tsx +18 -11
  13. package/cli/registry/basic/input-group.tsx +4 -4
  14. package/cli/registry/basic/input.tsx +29 -13
  15. package/cli/registry/basic/popover.tsx +2 -3
  16. package/cli/registry/basic/select.tsx +24 -4
  17. package/cli/registry/basic/spinner.tsx +20 -5
  18. package/cli/registry/basic/textarea.tsx +30 -12
  19. package/cli/registry/basic/tooltip.tsx +2 -1
  20. package/cli/registry/meta.json +97 -30
  21. package/dist/registry/basic/alert-dialog.d.ts +1 -1
  22. package/dist/registry/basic/avatar.d.ts +1 -1
  23. package/dist/registry/basic/button.d.ts +3 -1
  24. package/dist/registry/basic/card.d.ts +9 -4
  25. package/dist/registry/basic/combobox.d.ts +20 -9
  26. package/dist/registry/basic/date-picker.d.ts +18 -9
  27. package/dist/registry/basic/dialog.d.ts +1 -1
  28. package/dist/registry/basic/icon-button.d.ts +2 -1
  29. package/dist/registry/basic/input-group.d.ts +5 -3
  30. package/dist/registry/basic/input.d.ts +8 -3
  31. package/dist/registry/basic/item.d.ts +2 -2
  32. package/dist/registry/basic/resizable.d.ts +48 -48
  33. package/dist/registry/basic/select.d.ts +7 -2
  34. package/dist/registry/basic/spinner.d.ts +6 -2
  35. package/dist/registry/basic/tag.d.ts +1 -1
  36. package/dist/registry/basic/textarea.d.ts +9 -3
  37. package/dist/registry/basic/toggle.d.ts +1 -1
  38. package/dist/scale/computed.css +11 -0
  39. package/dist/scale/config.css +11 -0
  40. package/dist/scale/presets/compact.css +7 -0
  41. package/dist/scale/presets/dense.css +7 -0
  42. package/dist/scale/presets/sharp.css +7 -0
  43. package/dist/scale/presets/soft.css +7 -0
  44. package/dist/spark-design.cjs.js +35 -35
  45. package/dist/spark-design.es.js +5151 -3767
  46. package/dist/sparkdesign.css +1 -1
  47. package/dist/src/components/index.d.ts +1 -1
  48. package/dist/src/lib/index.d.ts +1 -1
  49. package/dist/src/lib/motion.d.ts +79 -0
  50. package/dist/theme-base.css +22 -0
  51. package/dist/themes/dark-mint.css +6 -0
  52. package/dist/themes/dark-parchment.css +6 -0
  53. package/dist/themes/light-parchment.css +6 -0
  54. package/dist/tokens/scale/computed.css +11 -0
  55. package/dist/tokens/scale/config.css +11 -0
  56. package/dist/tokens/scale/presets/compact.css +7 -0
  57. package/dist/tokens/scale/presets/dense.css +7 -0
  58. package/dist/tokens/scale/presets/sharp.css +7 -0
  59. package/dist/tokens/scale/presets/soft.css +7 -0
  60. package/dist/tokens/theme-base.css +22 -0
  61. package/dist/tokens/themes/dark-mint.css +6 -0
  62. package/dist/tokens/themes/dark-parchment.css +6 -0
  63. package/dist/tokens/themes/light-parchment.css +6 -0
  64. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
3
3
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
5
  import { cn } from '@/lib/utils'
6
+ import { MOTION_FADE, MOTION_SLIDE_UP } from '@/lib/motion'
6
7
  import { getThemeFromDocument } from './theme-from-document'
7
8
 
8
9
  const AlertDialog = AlertDialogPrimitive.Root
@@ -16,9 +17,7 @@ const AlertDialogOverlay = React.forwardRef<
16
17
  <AlertDialogPrimitive.Overlay ref={ref} asChild {...props}>
17
18
  <motion.div
18
19
  className={cn('fixed inset-0 z-50 bg-bg-mask/60', className)}
19
- initial={{ opacity: 0 }}
20
- animate={{ opacity: 1 }}
21
- transition={{ duration: 0.15 }}
20
+ {...MOTION_FADE}
22
21
  />
23
22
  </AlertDialogPrimitive.Overlay>
24
23
  ))
@@ -59,9 +58,7 @@ const AlertDialogContent = React.forwardRef<
59
58
  <AlertDialogPrimitive.Content ref={ref} asChild {...props}>
60
59
  <motion.div
61
60
  className={cn(contentVariants({ size }), className)}
62
- initial={{ opacity: 0, y: 8 }}
63
- animate={{ opacity: 1, y: 0 }}
64
- transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
61
+ {...MOTION_SLIDE_UP}
65
62
  >
66
63
  {children}
67
64
  </motion.div>
@@ -1,9 +1,12 @@
1
1
  import { forwardRef } from 'react'
2
2
  import type { ButtonHTMLAttributes, ReactNode } from 'react'
3
+ import { Slot } from 'radix-ui'
3
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
+ import { cn } from '@/lib/utils'
6
+ import { Spinner } from './spinner'
4
7
 
5
8
  const buttonVariants = cva(
6
- 'inline-flex items-center justify-center gap-1.5 font-medium transition-colors duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
9
+ 'inline-flex items-center justify-center gap-1.5 font-medium transition-[colors,transform] duration-200 active:scale-[0.98] focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
7
10
  {
8
11
  variants: {
9
12
  variant: {
@@ -13,6 +16,7 @@ const buttonVariants = cva(
13
16
  outline: 'border border-border-tertiary bg-bg-container text-text hover:bg-fill-secondary',
14
17
  ghost: 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text',
15
18
  text: 'bg-transparent text-text-secondary hover:text-text',
19
+ destructive: 'bg-error text-text-on-primary hover:bg-error-hover',
16
20
  },
17
21
  size: {
18
22
  sm: 'h-7 px-2 text-xs',
@@ -49,6 +53,8 @@ export interface ButtonProps
49
53
  textButton?: boolean
50
54
  prefixIcon?: ReactNode
51
55
  suffixIcon?: ReactNode
56
+ loading?: boolean
57
+ asChild?: boolean
52
58
  }
53
59
 
54
60
  const Button = forwardRef<HTMLButtonElement, ButtonProps>(
@@ -62,13 +68,17 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
62
68
  suffixIcon,
63
69
  rounded = 'square',
64
70
  disabled = false,
71
+ loading = false,
72
+ asChild = false,
65
73
  className,
66
74
  ...props
67
75
  },
68
76
  ref
69
77
  ) => {
78
+ const Comp = asChild ? Slot.Root : 'button'
79
+ const isDisabled = disabled || loading
70
80
  return (
71
- <button
81
+ <Comp
72
82
  ref={ref}
73
83
  className={buttonVariants({
74
84
  variant: textButton ? 'text' : variant,
@@ -77,21 +87,24 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
77
87
  textOnly: textButton,
78
88
  className,
79
89
  })}
80
- disabled={disabled}
90
+ disabled={isDisabled}
91
+ aria-busy={loading || undefined}
81
92
  {...props}
82
93
  >
83
- {prefixIcon && (
94
+ {loading ? (
95
+ <Spinner className="shrink-0" />
96
+ ) : prefixIcon ? (
84
97
  <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
85
98
  {prefixIcon}
86
99
  </span>
87
- )}
100
+ ) : null}
88
101
  {children}
89
102
  {suffixIcon && (
90
103
  <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
91
104
  {suffixIcon}
92
105
  </span>
93
106
  )}
94
- </button>
107
+ </Comp>
95
108
  )
96
109
  }
97
110
  )
@@ -1,15 +1,27 @@
1
1
  import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
2
3
  import { cn } from '@/lib/utils'
3
4
 
4
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
- ({ className, ...props }, ref) => (
5
+ const cardVariants = cva('flex flex-col gap-4 rounded-xl text-text py-6', {
6
+ variants: {
7
+ variant: {
8
+ outline: 'border border-border-tertiary bg-bg-container shadow-sm transition-shadow hover:shadow-md',
9
+ filled: 'bg-fill-tertiary',
10
+ ghost: 'bg-transparent',
11
+ },
12
+ },
13
+ defaultVariants: { variant: 'outline' },
14
+ })
15
+
16
+ export interface CardProps
17
+ extends React.HTMLAttributes<HTMLDivElement>,
18
+ VariantProps<typeof cardVariants> {}
19
+
20
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
21
+ ({ className, variant = 'outline', ...props }, ref) => (
6
22
  <div
7
23
  ref={ref}
8
- className={cn(
9
- 'flex flex-col gap-4 rounded-xl border border-border-tertiary bg-bg-container text-text shadow-sm',
10
- 'py-6',
11
- className,
12
- )}
24
+ className={cn(cardVariants({ variant }), className)}
13
25
  {...props}
14
26
  />
15
27
  ),
@@ -84,7 +96,6 @@ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
84
96
  )
85
97
  CardFooter.displayName = 'CardFooter'
86
98
 
87
- export type CardProps = React.HTMLAttributes<HTMLDivElement>
88
99
  export type CardHeaderProps = React.HTMLAttributes<HTMLDivElement>
89
100
  export type CardTitleProps = React.HTMLAttributes<HTMLDivElement>
90
101
  export type CardDescriptionProps = React.HTMLAttributes<HTMLDivElement>
@@ -100,4 +111,5 @@ export {
100
111
  CardAction,
101
112
  CardContent,
102
113
  CardFooter,
114
+ cardVariants,
103
115
  }
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useLayoutEffect, type ReactNode } from 'react'
2
2
  import { motion, AnimatePresence } from 'framer-motion'
3
3
  import { cn } from '@/lib/utils'
4
+ import { MOTION_EXPAND } from '@/lib/motion'
4
5
  import { ArrowDownSLine } from './icons-inline'
5
6
 
6
7
  export interface CollapsibleCardProps {
@@ -150,10 +151,7 @@ export function CollapsibleCard({
150
151
  <AnimatePresence initial={false}>
151
152
  {isExpanded && (
152
153
  <motion.div
153
- initial={{ height: 0, opacity: 0 }}
154
- animate={{ height: 'auto', opacity: 1 }}
155
- exit={{ height: 0, opacity: 0 }}
156
- transition={{ duration: 0.2, ease: [0.2, 0.8, 0.2, 1] }}
154
+ {...MOTION_EXPAND}
157
155
  className={cn('overflow-hidden', isAnimating && 'select-none')}
158
156
  >
159
157
  {/* 隐藏测量用,用于获取内容实际高度 */}
@@ -1,73 +1,131 @@
1
1
  import * as React from 'react'
2
+ import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
3
+
2
4
  import { cn } from '@/lib/utils'
3
- import { Input } from './input'
5
+ import { Button } from './button'
6
+ import {
7
+ Command,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandInput,
11
+ CommandItem,
12
+ CommandList,
13
+ } from './command'
14
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
4
15
 
5
16
  export interface ComboboxOption {
6
17
  value: string
7
18
  label: string
8
19
  disabled?: boolean
20
+ keywords?: string[]
9
21
  }
10
22
 
11
- export interface ComboboxProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
23
+ export interface ComboboxProps {
12
24
  options: ComboboxOption[]
13
25
  value?: string
26
+ defaultValue?: string
14
27
  onValueChange?: (value: string) => void
15
- placeholder?: string
16
- emptyText?: string
28
+ placeholder?: React.ReactNode
29
+ searchPlaceholder?: string
30
+ emptyText?: React.ReactNode
31
+ disabled?: boolean
32
+ className?: string
33
+ contentClassName?: string
34
+ align?: 'start' | 'center' | 'end'
35
+ triggerClassName?: string
36
+ id?: string
37
+ name?: string
38
+ 'aria-label'?: string
17
39
  }
18
40
 
19
41
  function Combobox({
20
42
  options,
21
43
  value,
44
+ defaultValue,
22
45
  onValueChange,
23
- placeholder = 'Search...',
46
+ placeholder = 'Select an option…',
47
+ searchPlaceholder = 'Search…',
24
48
  emptyText = 'No option found.',
49
+ disabled,
25
50
  className,
26
- ...props
51
+ contentClassName,
52
+ triggerClassName,
53
+ align = 'start',
54
+ id,
55
+ name,
56
+ 'aria-label': ariaLabel,
27
57
  }: ComboboxProps) {
28
- const [query, setQuery] = React.useState('')
29
- const selected = options.find((option) => option.value === value)
30
- const filtered = options.filter((option) =>
31
- option.label.toLowerCase().includes(query.toLowerCase()),
58
+ const isControlled = value !== undefined
59
+ const [internal, setInternal] = React.useState<string | undefined>(defaultValue)
60
+ const [open, setOpen] = React.useState(false)
61
+ const current = isControlled ? value : internal
62
+ const selected = options.find((option) => option.value === current)
63
+
64
+ const handleSelect = React.useCallback(
65
+ (next: string) => {
66
+ const resolved = next === current ? '' : next
67
+ if (!isControlled) setInternal(resolved)
68
+ onValueChange?.(resolved)
69
+ setOpen(false)
70
+ },
71
+ [current, isControlled, onValueChange],
32
72
  )
33
73
 
34
74
  return (
35
- <div className={cn('w-full rounded-lg border border-border-tertiary bg-bg-container p-1', className)} {...props}>
36
- <Input
37
- role="combobox"
38
- aria-expanded="true"
39
- value={query || selected?.label || ''}
40
- onChange={(event) => setQuery(event.target.value)}
41
- placeholder={placeholder}
42
- className="border-0 bg-transparent shadow-none"
43
- />
44
- <div role="listbox" className="mt-1 max-h-60 overflow-auto rounded-md">
45
- {filtered.length > 0 ? (
46
- filtered.map((option) => (
47
- <button
48
- key={option.value}
49
- type="button"
50
- role="option"
51
- aria-selected={option.value === value}
52
- disabled={option.disabled}
53
- className={cn(
54
- 'flex w-full items-center rounded-md px-3 py-2 text-left text-sm text-text transition-colors',
55
- 'hover:bg-fill-secondary focus:bg-fill-secondary focus:outline-none',
56
- 'disabled:pointer-events-none disabled:opacity-50',
57
- option.value === value && 'bg-fill-secondary',
58
- )}
59
- onClick={() => {
60
- onValueChange?.(option.value)
61
- setQuery('')
62
- }}
63
- >
64
- {option.label}
65
- </button>
66
- ))
67
- ) : (
68
- <div className="px-3 py-2 text-sm text-text-tertiary">{emptyText}</div>
69
- )}
70
- </div>
75
+ <div className={cn('w-60', className)}>
76
+ <Popover open={open} onOpenChange={setOpen}>
77
+ <PopoverTrigger asChild>
78
+ <Button
79
+ id={id}
80
+ variant="outline"
81
+ role="combobox"
82
+ aria-expanded={open}
83
+ aria-label={ariaLabel}
84
+ disabled={disabled}
85
+ className={cn(
86
+ 'w-full justify-between font-normal',
87
+ !selected && 'text-text-tertiary',
88
+ triggerClassName,
89
+ )}
90
+ suffixIcon={<ChevronsUpDownIcon className="size-4 shrink-0 opacity-60" />}
91
+ >
92
+ <span className="truncate text-left">
93
+ {selected ? selected.label : placeholder}
94
+ </span>
95
+ </Button>
96
+ </PopoverTrigger>
97
+ <PopoverContent
98
+ align={align}
99
+ className={cn('w-[var(--radix-popover-trigger-width)] min-w-56 p-0', contentClassName)}
100
+ >
101
+ <Command>
102
+ <CommandInput placeholder={searchPlaceholder} />
103
+ <CommandList>
104
+ <CommandEmpty>{emptyText}</CommandEmpty>
105
+ <CommandGroup>
106
+ {options.map((option) => (
107
+ <CommandItem
108
+ key={option.value}
109
+ value={option.value}
110
+ keywords={[option.label, ...(option.keywords ?? [])]}
111
+ disabled={option.disabled}
112
+ onSelect={handleSelect}
113
+ >
114
+ <CheckIcon
115
+ className={cn(
116
+ 'mr-2 size-4',
117
+ option.value === current ? 'opacity-100' : 'opacity-0',
118
+ )}
119
+ />
120
+ <span className="truncate">{option.label}</span>
121
+ </CommandItem>
122
+ ))}
123
+ </CommandGroup>
124
+ </CommandList>
125
+ </Command>
126
+ </PopoverContent>
127
+ </Popover>
128
+ {name ? <input type="hidden" name={name} value={current ?? ''} /> : null}
71
129
  </div>
72
130
  )
73
131
  }
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
3
3
  import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
5
  import { cn } from '@/lib/utils'
6
+ import { MOTION_SCALE_IN } from '@/lib/motion'
6
7
  import { ArrowRightLine, CheckLine } from './icons-inline'
7
8
  import { getThemeFromDocument } from './theme-from-document'
8
9
 
@@ -40,9 +41,7 @@ const ContextMenuContent = React.forwardRef<
40
41
  {...props}
41
42
  >
42
43
  <motion.div
43
- initial={{ opacity: 0 }}
44
- animate={{ opacity: 1 }}
45
- transition={{ duration: 0.12, ease: [0.4, 0, 0.2, 1] }}
44
+ {...MOTION_SCALE_IN}
46
45
  >
47
46
  {children}
48
47
  </motion.div>
@@ -1,13 +1,84 @@
1
1
  import * as React from 'react'
2
- import { Input, type InputProps } from './input'
2
+ import { format } from 'date-fns'
3
+ import { CalendarIcon } from 'lucide-react'
3
4
 
4
- export interface DatePickerProps extends Omit<InputProps, 'type'> {
5
- type?: 'date' | 'datetime-local' | 'month' | 'time' | 'week'
5
+ import { cn } from '@/lib/utils'
6
+ import { Button } from './button'
7
+ import { Calendar } from './calendar'
8
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
9
+
10
+ export interface DatePickerProps {
11
+ value?: Date
12
+ defaultValue?: Date
13
+ onChange?: (date: Date | undefined) => void
14
+ placeholder?: React.ReactNode
15
+ disabled?: boolean
16
+ className?: string
17
+ align?: 'start' | 'center' | 'end'
18
+ formatString?: string
19
+ id?: string
20
+ name?: string
21
+ 'aria-label'?: string
6
22
  }
7
23
 
8
- const DatePicker = React.forwardRef<HTMLInputElement, DatePickerProps>(
9
- ({ type = 'date', ...props }, ref) => <Input ref={ref} type={type} {...props} />,
10
- )
11
- DatePicker.displayName = 'DatePicker'
24
+ function DatePicker({
25
+ value,
26
+ defaultValue,
27
+ onChange,
28
+ placeholder = 'Pick a date',
29
+ disabled,
30
+ className,
31
+ align = 'start',
32
+ formatString = 'PPP',
33
+ id,
34
+ name,
35
+ 'aria-label': ariaLabel,
36
+ }: DatePickerProps) {
37
+ const isControlled = value !== undefined
38
+ const [internal, setInternal] = React.useState<Date | undefined>(defaultValue)
39
+ const [open, setOpen] = React.useState(false)
40
+ const selected = isControlled ? value : internal
41
+
42
+ const handleSelect = React.useCallback(
43
+ (next: Date | undefined) => {
44
+ if (!isControlled) setInternal(next)
45
+ onChange?.(next)
46
+ if (next) setOpen(false)
47
+ },
48
+ [isControlled, onChange],
49
+ )
50
+
51
+ return (
52
+ <Popover open={open} onOpenChange={setOpen}>
53
+ <PopoverTrigger asChild>
54
+ <Button
55
+ id={id}
56
+ variant="outline"
57
+ disabled={disabled}
58
+ aria-label={ariaLabel ?? (typeof placeholder === 'string' ? placeholder : 'Pick a date')}
59
+ className={cn(
60
+ 'w-60 justify-start text-left font-normal',
61
+ !selected && 'text-text-tertiary',
62
+ className,
63
+ )}
64
+ suffixIcon={<CalendarIcon className="ml-auto size-4 opacity-60" />}
65
+ >
66
+ {selected ? format(selected, formatString) : <span>{placeholder}</span>}
67
+ </Button>
68
+ </PopoverTrigger>
69
+ <PopoverContent className="w-auto p-0" align={align}>
70
+ <Calendar
71
+ mode="single"
72
+ selected={selected}
73
+ onSelect={handleSelect}
74
+ autoFocus
75
+ />
76
+ {name ? (
77
+ <input type="hidden" name={name} value={selected ? selected.toISOString() : ''} />
78
+ ) : null}
79
+ </PopoverContent>
80
+ </Popover>
81
+ )
82
+ }
12
83
 
13
84
  export { DatePicker }
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
3
3
  import * as DialogPrimitive from '@radix-ui/react-dialog'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
5
  import { cn } from '@/lib/utils'
6
+ import { MOTION_FADE, MOTION_SLIDE_UP } from '@/lib/motion'
6
7
  import { CloseLine } from './icons-inline'
7
8
  import { getThemeFromDocument } from './theme-from-document'
8
9
 
@@ -18,10 +19,7 @@ const DialogOverlay = React.forwardRef<
18
19
  <DialogPrimitive.Overlay ref={ref} asChild {...props}>
19
20
  <motion.div
20
21
  className={cn('fixed inset-0 z-50 bg-bg-mask/60', className)}
21
- initial={{ opacity: 0 }}
22
- animate={{ opacity: 1 }}
23
- exit={{ opacity: 0 }}
24
- transition={{ duration: 0.15 }}
22
+ {...MOTION_FADE}
25
23
  />
26
24
  </DialogPrimitive.Overlay>
27
25
  ))
@@ -81,10 +79,7 @@ const DialogContent = React.forwardRef<
81
79
  <DialogPrimitive.Content ref={ref} asChild {...props}>
82
80
  <motion.div
83
81
  className={cn(contentVariants({ size }), className)}
84
- initial={{ opacity: 0, y: 8 }}
85
- animate={{ opacity: 1, y: 0 }}
86
- exit={{ opacity: 0, y: 8 }}
87
- transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
82
+ {...MOTION_SLIDE_UP}
88
83
  >
89
84
  {children}
90
85
  {!hideCloseButton && (
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
3
3
  import * as DialogPrimitive from '@radix-ui/react-dialog'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
5
  import { cn } from '@/lib/utils'
6
+ import { MOTION_FADE, MOTION_DURATION, MOTION_EASE } from '@/lib/motion'
6
7
  import { CloseLine } from './icons-inline'
7
8
  import { getThemeFromDocument } from './theme-from-document'
8
9
 
@@ -18,10 +19,7 @@ const DrawerOverlay = React.forwardRef<
18
19
  <DialogPrimitive.Overlay ref={ref} asChild {...props}>
19
20
  <motion.div
20
21
  className={cn('fixed inset-0 z-50 bg-bg-mask/60', className)}
21
- initial={{ opacity: 0 }}
22
- animate={{ opacity: 1 }}
23
- exit={{ opacity: 0 }}
24
- transition={{ duration: 0.15 }}
22
+ {...MOTION_FADE}
25
23
  />
26
24
  </DialogPrimitive.Overlay>
27
25
  ))
@@ -83,7 +81,7 @@ const DrawerContent = React.forwardRef<
83
81
  initial={motionProps.initial}
84
82
  animate={motionProps.animate}
85
83
  exit={motionProps.exit}
86
- transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
84
+ transition={{ duration: MOTION_DURATION.slow, ease: MOTION_EASE.standard }}
87
85
  >
88
86
  {children}
89
87
  {showCloseButton && (
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
3
3
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
5
  import { cn } from '@/lib/utils'
6
+ import { MOTION_SCALE_IN } from '@/lib/motion'
6
7
  import { ArrowRightLine, CheckLine } from './icons-inline'
7
8
  import { getThemeFromDocument } from './theme-from-document'
8
9
 
@@ -42,9 +43,7 @@ const DropdownMenuContent = React.forwardRef<
42
43
  {...props}
43
44
  >
44
45
  <motion.div
45
- initial={{ opacity: 0 }}
46
- animate={{ opacity: 1 }}
47
- transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
46
+ {...MOTION_SCALE_IN}
48
47
  >
49
48
  {children}
50
49
  </motion.div>
@@ -2,6 +2,7 @@ import * as React from 'react'
2
2
  import { motion } from 'framer-motion'
3
3
  import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
4
4
  import { cn } from '@/lib/utils'
5
+ import { MOTION_SCALE_IN } from '@/lib/motion'
5
6
  import { getThemeFromDocument } from './theme-from-document'
6
7
 
7
8
  const HoverCard = HoverCardPrimitive.Root
@@ -41,9 +42,7 @@ const HoverCardContent = React.forwardRef<
41
42
  {...props}
42
43
  >
43
44
  <motion.div
44
- initial={{ opacity: 0, y: 4 }}
45
- animate={{ opacity: 1, y: 0 }}
46
- transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
45
+ {...MOTION_SCALE_IN}
47
46
  >
48
47
  {children}
49
48
  </motion.div>
@@ -1,10 +1,11 @@
1
1
  import { forwardRef } from 'react'
2
2
  import type { ButtonHTMLAttributes, ReactNode } from 'react'
3
+ import { Slot } from 'radix-ui'
3
4
  import { cva, type VariantProps } from 'class-variance-authority'
4
5
  import { cn } from '@/lib/utils'
5
6
 
6
7
  const iconButtonVariants = cva(
7
- 'flex-none shrink-0 inline-flex items-center justify-center box-border transition-colors duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer [&>*]:flex [&>*]:items-center [&>*]:justify-center [&>*]:size-full [&>*]:[&>svg]:block [&>*]:[&>svg]:leading-none',
8
+ 'flex-none shrink-0 inline-flex items-center justify-center box-border transition-[colors,transform] duration-200 active:scale-[0.98] focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer [&>*]:flex [&>*]:items-center [&>*]:justify-center [&>*]:size-full [&>*]:[&>svg]:block [&>*]:[&>svg]:leading-none',
8
9
  {
9
10
  variants: {
10
11
  variant: {
@@ -13,6 +14,7 @@ const iconButtonVariants = cva(
13
14
  tertiary: 'bg-fill-secondary text-text hover:bg-fill',
14
15
  ghost: 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text',
15
16
  iconOnly: 'bg-transparent text-text-secondary hover:text-text',
17
+ destructive: 'bg-error text-text-on-primary hover:bg-error-hover',
16
18
  },
17
19
  rounded: {
18
20
  square: 'rounded',
@@ -36,6 +38,7 @@ export interface IconButtonProps
36
38
  extends ButtonHTMLAttributes<HTMLButtonElement>,
37
39
  VariantProps<typeof iconButtonVariants> {
38
40
  icon: ReactNode
41
+ asChild?: boolean
39
42
  }
40
43
 
41
44
  export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
@@ -46,20 +49,24 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
46
49
  rounded = 'square',
47
50
  icon,
48
51
  disabled = false,
52
+ asChild = false,
49
53
  className,
50
54
  ...props
51
55
  },
52
56
  ref
53
- ) => (
54
- <button
55
- ref={ref}
56
- className={cn(iconButtonVariants({ variant, size, rounded }), className)}
57
- disabled={disabled}
58
- {...props}
59
- >
60
- <span className="inline-flex shrink-0 size-full items-center justify-center">{icon}</span>
61
- </button>
62
- )
57
+ ) => {
58
+ const Comp = asChild ? Slot.Root : 'button'
59
+ return (
60
+ <Comp
61
+ ref={ref}
62
+ className={cn(iconButtonVariants({ variant, size, rounded }), className)}
63
+ disabled={disabled}
64
+ {...props}
65
+ >
66
+ <span className="inline-flex shrink-0 size-full items-center justify-center">{icon}</span>
67
+ </Comp>
68
+ )
69
+ }
63
70
  )
64
71
  IconButton.displayName = 'IconButton'
65
72
 
@@ -3,8 +3,8 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
 
4
4
  import { cn } from "@/lib/utils"
5
5
  import { Button } from "./button"
6
- import { Input } from "./input"
7
- import { Textarea } from "./textarea"
6
+ import { Input, type InputProps } from "./input"
7
+ import { Textarea, type TextareaProps } from "./textarea"
8
8
 
9
9
  function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10
10
  return (
@@ -129,7 +129,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
129
129
  function InputGroupInput({
130
130
  className,
131
131
  ...props
132
- }: React.ComponentProps<"input">) {
132
+ }: InputProps) {
133
133
  return (
134
134
  <Input
135
135
  data-slot="input-group-control"
@@ -145,7 +145,7 @@ function InputGroupInput({
145
145
  function InputGroupTextarea({
146
146
  className,
147
147
  ...props
148
- }: React.ComponentProps<"textarea">) {
148
+ }: TextareaProps) {
149
149
  return (
150
150
  <Textarea
151
151
  data-slot="input-group-control"