sparkdesign 0.4.7 → 0.4.9

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 (96) hide show
  1. package/cli/registry/AGENTS.md +1 -1
  2. package/cli/registry/agent-manifest.json +3996 -67
  3. package/cli/registry/basic/accordion.tsx +79 -0
  4. package/cli/registry/basic/alert-dialog.tsx +3 -6
  5. package/cli/registry/basic/badge.tsx +49 -0
  6. package/cli/registry/basic/button.tsx +32 -14
  7. package/cli/registry/basic/card.tsx +20 -8
  8. package/cli/registry/basic/collapsible-card.tsx +12 -5
  9. package/cli/registry/basic/combobox.tsx +104 -46
  10. package/cli/registry/basic/context-menu.tsx +2 -3
  11. package/cli/registry/basic/date-picker.tsx +78 -7
  12. package/cli/registry/basic/dialog.tsx +3 -8
  13. package/cli/registry/basic/drawer.tsx +3 -5
  14. package/cli/registry/basic/dropdown-menu.tsx +2 -3
  15. package/cli/registry/basic/ellipsis-text.tsx +151 -0
  16. package/cli/registry/basic/form.tsx +186 -0
  17. package/cli/registry/basic/hover-card.tsx +2 -3
  18. package/cli/registry/basic/icon-button.tsx +29 -14
  19. package/cli/registry/basic/input-group.tsx +4 -4
  20. package/cli/registry/basic/input.tsx +29 -13
  21. package/cli/registry/basic/popover.tsx +2 -3
  22. package/cli/registry/basic/select.tsx +24 -4
  23. package/cli/registry/basic/sidebar.tsx +665 -0
  24. package/cli/registry/basic/sonner.tsx +10 -10
  25. package/cli/registry/basic/spinner.tsx +20 -5
  26. package/cli/registry/basic/textarea.tsx +30 -12
  27. package/cli/registry/basic/tooltip.tsx +2 -1
  28. package/cli/registry/chat/chat-input/compound.tsx +1 -0
  29. package/cli/registry/chat/user-question/compound.tsx +2 -0
  30. package/cli/registry/meta.json +250 -30
  31. package/dist/registry/basic/accordion.d.ts +15 -0
  32. package/dist/registry/basic/alert-dialog.d.ts +1 -1
  33. package/dist/registry/basic/avatar.d.ts +1 -1
  34. package/dist/registry/basic/badge.d.ts +23 -0
  35. package/dist/registry/basic/button.d.ts +3 -1
  36. package/dist/registry/basic/card.d.ts +9 -4
  37. package/dist/registry/basic/combobox.d.ts +20 -9
  38. package/dist/registry/basic/date-picker.d.ts +18 -9
  39. package/dist/registry/basic/dialog.d.ts +1 -1
  40. package/dist/registry/basic/ellipsis-text.d.ts +45 -0
  41. package/dist/registry/basic/form.d.ts +23 -0
  42. package/dist/registry/basic/icon-button.d.ts +17 -3
  43. package/dist/registry/basic/input-group.d.ts +5 -3
  44. package/dist/registry/basic/input.d.ts +8 -3
  45. package/dist/registry/basic/item.d.ts +3 -3
  46. package/dist/registry/basic/resizable.d.ts +48 -48
  47. package/dist/registry/basic/select.d.ts +7 -2
  48. package/dist/registry/basic/sidebar.d.ts +72 -0
  49. package/dist/registry/basic/spinner.d.ts +6 -2
  50. package/dist/registry/basic/tag.d.ts +1 -1
  51. package/dist/registry/basic/textarea.d.ts +9 -3
  52. package/dist/registry/basic/toggle.d.ts +1 -1
  53. package/dist/scale/computed.css +11 -0
  54. package/dist/scale/config.css +11 -0
  55. package/dist/scale/presets/compact.css +7 -0
  56. package/dist/scale/presets/dense.css +7 -0
  57. package/dist/scale/presets/sharp.css +7 -0
  58. package/dist/scale/presets/soft.css +7 -0
  59. package/dist/spark-design.cjs.js +34 -37
  60. package/dist/spark-design.es.js +7200 -4933
  61. package/dist/sparkdesign.css +1 -1
  62. package/dist/src/components/basic/Accordion/index.d.ts +13 -0
  63. package/dist/src/components/basic/Badge/index.d.ts +13 -0
  64. package/dist/src/components/basic/EllipsisText/index.d.ts +4 -36
  65. package/dist/src/components/basic/Form/index.d.ts +12 -0
  66. package/dist/src/components/basic/Sidebar/index.d.ts +13 -0
  67. package/dist/src/components/index.d.ts +7 -3
  68. package/dist/src/lib/index.d.ts +1 -1
  69. package/dist/src/lib/motion.d.ts +79 -0
  70. package/dist/theme-base.css +22 -0
  71. package/dist/themes/dark-mint.css +6 -0
  72. package/dist/themes/dark-parchment.css +6 -0
  73. package/dist/themes/light-parchment.css +6 -0
  74. package/dist/tokens/scale/computed.css +11 -0
  75. package/dist/tokens/scale/config.css +11 -0
  76. package/dist/tokens/scale/presets/compact.css +7 -0
  77. package/dist/tokens/scale/presets/dense.css +7 -0
  78. package/dist/tokens/scale/presets/sharp.css +7 -0
  79. package/dist/tokens/scale/presets/soft.css +7 -0
  80. package/dist/tokens/theme-base.css +22 -0
  81. package/dist/tokens/themes/dark-mint.css +6 -0
  82. package/dist/tokens/themes/dark-parchment.css +6 -0
  83. package/dist/tokens/themes/light-parchment.css +6 -0
  84. package/docs/agent/component-selection.md +106 -4
  85. package/package.json +8 -3
  86. package/registry/agent-manifest.json +3996 -67
  87. package/cli/registry/chat/user-question/UserQuestionCard.tsx +0 -198
  88. package/cli/registry/chat/user-question/UserQuestionFooter.tsx +0 -66
  89. package/cli/registry/chat/user-question/UserQuestionHeader.tsx +0 -64
  90. package/cli/registry/chat/user-question/useUserQuestionState.ts +0 -165
  91. package/dist/registry/chat/user-question/UserQuestionCard.d.ts +0 -36
  92. package/dist/registry/chat/user-question/UserQuestionFooter.d.ts +0 -24
  93. package/dist/registry/chat/user-question/UserQuestionHeader.d.ts +0 -26
  94. package/dist/registry/chat/user-question/useUserQuestionState.d.ts +0 -26
  95. package/dist/src/components/basic/CollapsibleSection/index.d.ts +0 -43
  96. package/dist/src/components/chat/Response/StreamingMarkdownBlock.d.ts +0 -12
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { ArrowDownSLine } from "./icons-inline"
8
+
9
+ const Accordion = AccordionPrimitive.Root
10
+
11
+ const AccordionItem = React.forwardRef<
12
+ React.ElementRef<typeof AccordionPrimitive.Item>,
13
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
+ >(({ className, ...props }, ref) => (
15
+ <AccordionPrimitive.Item
16
+ ref={ref}
17
+ data-slot="accordion-item"
18
+ className={cn("border-b border-border-tertiary last:border-b-0", className)}
19
+ {...props}
20
+ />
21
+ ))
22
+ AccordionItem.displayName = "AccordionItem"
23
+
24
+ const AccordionTrigger = React.forwardRef<
25
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
26
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
27
+ chevronIcon?: React.ReactNode
28
+ }
29
+ >(({ className, children, chevronIcon, ...props }, ref) => (
30
+ <AccordionPrimitive.Header className="flex">
31
+ <AccordionPrimitive.Trigger
32
+ ref={ref}
33
+ data-slot="accordion-trigger"
34
+ className={cn(
35
+ "group flex flex-1 items-center justify-between gap-2 py-4 text-left text-sm font-medium text-text transition-all outline-none hover:text-text-secondary",
36
+ "focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base",
37
+ "disabled:pointer-events-none disabled:opacity-50",
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <span
44
+ className="inline-flex shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180"
45
+ aria-hidden
46
+ >
47
+ {chevronIcon ?? <ArrowDownSLine className="size-4 text-text-secondary" />}
48
+ </span>
49
+ </AccordionPrimitive.Trigger>
50
+ </AccordionPrimitive.Header>
51
+ ))
52
+ AccordionTrigger.displayName = "AccordionTrigger"
53
+
54
+ const AccordionContent = React.forwardRef<
55
+ React.ElementRef<typeof AccordionPrimitive.Content>,
56
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
57
+ >(({ className, children, ...props }, ref) => (
58
+ <AccordionPrimitive.Content
59
+ ref={ref}
60
+ data-slot="accordion-content"
61
+ className={cn(
62
+ "overflow-hidden text-sm text-text-secondary data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
63
+ className
64
+ )}
65
+ {...props}
66
+ >
67
+ <div className="pb-4 pt-0">{children}</div>
68
+ </AccordionPrimitive.Content>
69
+ ))
70
+ AccordionContent.displayName = "AccordionContent"
71
+
72
+ export type AccordionProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>
73
+ export type AccordionItemProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
74
+ export type AccordionTriggerProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
75
+ chevronIcon?: React.ReactNode
76
+ }
77
+ export type AccordionContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
78
+
79
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -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>
@@ -0,0 +1,49 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base [&>svg]:size-3 [&>svg]:pointer-events-none",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "border-transparent bg-primary text-text-on-primary hover:bg-primary-hover",
13
+ secondary: "border-transparent bg-fill-secondary text-text hover:bg-fill",
14
+ destructive: "border-transparent bg-error text-text-on-primary hover:bg-error-hover",
15
+ outline: "border-border-tertiary bg-transparent text-text",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ variant: "default",
20
+ },
21
+ }
22
+ )
23
+
24
+ function Badge({
25
+ className,
26
+ variant,
27
+ asChild = false,
28
+ ...props
29
+ }: React.ComponentProps<"span"> &
30
+ VariantProps<typeof badgeVariants> & {
31
+ asChild?: boolean
32
+ }) {
33
+ const Comp = asChild ? Slot : "span"
34
+
35
+ return (
36
+ <Comp
37
+ data-slot="badge"
38
+ className={cn(badgeVariants({ variant }), className)}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ export type BadgeProps = React.ComponentProps<"span"> &
45
+ VariantProps<typeof badgeVariants> & {
46
+ asChild?: boolean
47
+ }
48
+
49
+ export { Badge, badgeVariants }
@@ -1,9 +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'
5
+ import { Spinner } from './spinner'
4
6
 
5
7
  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',
8
+ 'inline-flex items-center justify-center gap-1.5 font-medium transition-[color,background-color,border-color,box-shadow,transform] duration-200 active:scale-[0.98] focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
7
9
  {
8
10
  variants: {
9
11
  variant: {
@@ -13,6 +15,7 @@ const buttonVariants = cva(
13
15
  outline: 'border border-border-tertiary bg-bg-container text-text hover:bg-fill-secondary',
14
16
  ghost: 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text',
15
17
  text: 'bg-transparent text-text-secondary hover:text-text',
18
+ destructive: 'bg-error text-text-on-primary hover:bg-error-hover',
16
19
  },
17
20
  size: {
18
21
  sm: 'h-7 px-2 text-xs',
@@ -49,6 +52,8 @@ export interface ButtonProps
49
52
  textButton?: boolean
50
53
  prefixIcon?: ReactNode
51
54
  suffixIcon?: ReactNode
55
+ loading?: boolean
56
+ asChild?: boolean
52
57
  }
53
58
 
54
59
  const Button = forwardRef<HTMLButtonElement, ButtonProps>(
@@ -62,13 +67,17 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
62
67
  suffixIcon,
63
68
  rounded = 'square',
64
69
  disabled = false,
70
+ loading = false,
71
+ asChild = false,
65
72
  className,
66
73
  ...props
67
74
  },
68
75
  ref
69
76
  ) => {
77
+ const Comp = asChild ? Slot.Root : 'button'
78
+ const isDisabled = disabled || loading
70
79
  return (
71
- <button
80
+ <Comp
72
81
  ref={ref}
73
82
  className={buttonVariants({
74
83
  variant: textButton ? 'text' : variant,
@@ -77,21 +86,30 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
77
86
  textOnly: textButton,
78
87
  className,
79
88
  })}
80
- disabled={disabled}
89
+ disabled={isDisabled}
90
+ aria-busy={loading || undefined}
81
91
  {...props}
82
92
  >
83
- {prefixIcon && (
84
- <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
85
- {prefixIcon}
86
- </span>
93
+ {asChild ? (
94
+ children
95
+ ) : (
96
+ <>
97
+ {loading ? (
98
+ <Spinner className="shrink-0" />
99
+ ) : prefixIcon ? (
100
+ <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
101
+ {prefixIcon}
102
+ </span>
103
+ ) : null}
104
+ {children}
105
+ {suffixIcon && (
106
+ <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
107
+ {suffixIcon}
108
+ </span>
109
+ )}
110
+ </>
87
111
  )}
88
- {children}
89
- {suffixIcon && (
90
- <span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
91
- {suffixIcon}
92
- </span>
93
- )}
94
- </button>
112
+ </Comp>
95
113
  )
96
114
  }
97
115
  )
@@ -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
- import { useState, useRef, useLayoutEffect, type ReactNode } from 'react'
1
+ import { useState, useRef, useLayoutEffect, type KeyboardEvent, 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 {
@@ -88,6 +89,13 @@ export function CollapsibleCard({
88
89
  setTimeout(() => setIsAnimating(false), 300)
89
90
  }
90
91
 
92
+ const handleToggleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
93
+ if (!collapsible) return
94
+ if (event.key !== 'Enter' && event.key !== ' ') return
95
+ event.preventDefault()
96
+ handleToggle()
97
+ }
98
+
91
99
  const showArrow = collapsible && (isHovered || headerIcon == null)
92
100
  const headerIconNode = showArrow
93
101
  ? <ArrowDownSLine className={cn('w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 transition-transform duration-200 text-text-secondary', isExpanded ? 'rotate-0' : '-rotate-90')} />
@@ -122,7 +130,9 @@ export function CollapsibleCard({
122
130
  <div
123
131
  className={cn('flex flex-row items-center gap-2 flex-1 min-w-0', collapsible && 'cursor-pointer')}
124
132
  onClick={handleToggle}
133
+ onKeyDown={handleToggleKeyDown}
125
134
  role={collapsible ? 'button' : undefined}
135
+ tabIndex={collapsible ? 0 : undefined}
126
136
  aria-expanded={collapsible ? isExpanded : undefined}
127
137
  >
128
138
  <div className="w-[var(--font-size-sm)] h-[var(--font-size-sm)] flex items-center justify-center shrink-0 text-text-secondary [&>svg]:w-[var(--font-size-sm)] [&>svg]:h-[var(--font-size-sm)]">
@@ -150,10 +160,7 @@ export function CollapsibleCard({
150
160
  <AnimatePresence initial={false}>
151
161
  {isExpanded && (
152
162
  <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] }}
163
+ {...MOTION_EXPAND}
157
164
  className={cn('overflow-hidden', isAnimating && 'select-none')}
158
165
  >
159
166
  {/* 隐藏测量用,用于获取内容实际高度 */}
@@ -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 && (