@glyphui/ui 0.1.1 → 0.2.0

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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/data-table.js +2 -2
  3. package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
  4. package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
  5. package/dist/form.js +2 -2
  6. package/dist/index.d.ts +2743 -93
  7. package/dist/index.js +2748 -111
  8. package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
  9. package/package.json +4 -2
  10. package/src/components/accordion/accordion.styles.ts +41 -0
  11. package/src/components/accordion/accordion.tsx +86 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/alert/alert-icons.tsx +66 -0
  14. package/src/components/alert/alert.styles.ts +57 -0
  15. package/src/components/alert/alert.tsx +86 -0
  16. package/src/components/alert/index.ts +2 -0
  17. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  18. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  19. package/src/components/alert-dialog/confirm.tsx +111 -0
  20. package/src/components/alert-dialog/index.ts +15 -0
  21. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  22. package/src/components/aspect-ratio/index.ts +1 -0
  23. package/src/components/badge/badge.styles.ts +1 -1
  24. package/src/components/banner/banner.styles.ts +40 -0
  25. package/src/components/banner/banner.tsx +101 -0
  26. package/src/components/banner/index.ts +2 -0
  27. package/src/components/blockquote/blockquote.tsx +59 -0
  28. package/src/components/blockquote/index.ts +1 -0
  29. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  30. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  31. package/src/components/breadcrumb/index.ts +12 -0
  32. package/src/components/button/button.tsx +11 -8
  33. package/src/components/code/code.styles.ts +33 -0
  34. package/src/components/code/code.tsx +96 -0
  35. package/src/components/code/index.ts +2 -0
  36. package/src/components/combobox/combobox.styles.ts +50 -0
  37. package/src/components/combobox/combobox.tsx +203 -0
  38. package/src/components/combobox/index.ts +2 -0
  39. package/src/components/command-menu/command-menu.styles.ts +41 -0
  40. package/src/components/command-menu/command-menu.tsx +217 -0
  41. package/src/components/command-menu/index.ts +14 -0
  42. package/src/components/description-list/description-list.styles.ts +31 -0
  43. package/src/components/description-list/description-list.tsx +56 -0
  44. package/src/components/description-list/index.ts +7 -0
  45. package/src/components/dialog/close-icon.tsx +22 -0
  46. package/src/components/dialog/dialog.styles.ts +41 -0
  47. package/src/components/dialog/dialog.tsx +111 -0
  48. package/src/components/dialog/index.ts +13 -0
  49. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  50. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  51. package/src/components/dropdown-menu/index.ts +19 -0
  52. package/src/components/grid/grid.tsx +144 -0
  53. package/src/components/grid/index.ts +1 -0
  54. package/src/components/link/index.ts +2 -0
  55. package/src/components/link/link.styles.ts +39 -0
  56. package/src/components/link/link.tsx +62 -0
  57. package/src/components/pagination/index.ts +3 -0
  58. package/src/components/pagination/pagination-range.ts +35 -0
  59. package/src/components/pagination/pagination.styles.ts +41 -0
  60. package/src/components/pagination/pagination.tsx +130 -0
  61. package/src/components/popover/index.ts +9 -0
  62. package/src/components/popover/popover.tsx +66 -0
  63. package/src/components/progress/index.ts +7 -0
  64. package/src/components/progress/progress.styles.ts +68 -0
  65. package/src/components/progress/progress.tsx +158 -0
  66. package/src/components/prose/index.ts +1 -0
  67. package/src/components/prose/prose.tsx +82 -0
  68. package/src/components/radio-group/index.ts +7 -0
  69. package/src/components/radio-group/radio-group.styles.ts +64 -0
  70. package/src/components/radio-group/radio-group.tsx +128 -0
  71. package/src/components/scroll-area/index.ts +1 -0
  72. package/src/components/scroll-area/scroll-area.tsx +68 -0
  73. package/src/components/sheet/index.ts +13 -0
  74. package/src/components/sheet/sheet.styles.ts +60 -0
  75. package/src/components/sheet/sheet.tsx +108 -0
  76. package/src/components/slider/index.ts +2 -0
  77. package/src/components/slider/slider.styles.ts +32 -0
  78. package/src/components/slider/slider.tsx +131 -0
  79. package/src/components/stat-card/index.ts +2 -0
  80. package/src/components/stat-card/stat-card.styles.ts +24 -0
  81. package/src/components/stat-card/stat-card.tsx +108 -0
  82. package/src/components/toast/index.ts +1 -0
  83. package/src/components/toast/toast.tsx +91 -0
  84. package/src/index.ts +25 -0
  85. package/src/provider/glyph-provider.tsx +16 -2
@@ -0,0 +1,203 @@
1
+ import { Command as Cmdk } from 'cmdk'
2
+ import { Popover } from 'radix-ui'
3
+ import { type ReactNode, useMemo, useState } from 'react'
4
+ import { useComponentConfig } from '../../hooks/use-component-config'
5
+ import { useControllableState } from '../../hooks/use-controllable-state'
6
+ import { CheckIcon, ChevronsUpDownIcon, SearchIcon } from '../../internal/icons'
7
+ import { useFieldControl } from '../field/field-context'
8
+ import { comboboxStyles } from './combobox.styles'
9
+
10
+ export interface ComboboxOption<T extends string = string> {
11
+ value: T
12
+ /** Shown in the list and trigger, and matched by search. */
13
+ label: string
14
+ description?: ReactNode
15
+ /** Extra search terms, e.g. abbreviations. */
16
+ keywords?: readonly string[]
17
+ disabled?: boolean
18
+ }
19
+
20
+ interface SingleProps<T extends string> {
21
+ multiple?: false
22
+ value?: T | null
23
+ defaultValue?: T | null
24
+ onValueChange?: (value: T | null) => void
25
+ }
26
+
27
+ interface MultipleProps<T extends string> {
28
+ multiple: true
29
+ value?: readonly T[]
30
+ defaultValue?: readonly T[]
31
+ onValueChange?: (value: T[]) => void
32
+ }
33
+
34
+ interface SharedProps<T extends string> {
35
+ options: readonly ComboboxOption<T>[]
36
+ placeholder?: ReactNode
37
+ searchPlaceholder?: string
38
+ /** Shown when the search matches nothing. */
39
+ emptyText?: ReactNode
40
+ /** Chips shown in the trigger before collapsing to "+N". Multiple mode only. Default 3. */
41
+ maxChips?: number
42
+ size?: 'sm' | 'md' | 'lg'
43
+ disabled?: boolean
44
+ required?: boolean
45
+ /** Submits the value with a native form (comma-free: one hidden input per value). */
46
+ name?: string
47
+ id?: string
48
+ className?: string
49
+ contentClassName?: string
50
+ 'aria-label'?: string
51
+ 'aria-describedby'?: string
52
+ 'aria-invalid'?: boolean
53
+ }
54
+
55
+ export type ComboboxProps<T extends string = string> = SharedProps<T> &
56
+ (SingleProps<T> | MultipleProps<T>)
57
+
58
+ function toArray<T>(value: T | readonly T[] | null | undefined): T[] | undefined {
59
+ if (value === undefined) return undefined
60
+ if (value === null) return []
61
+ return Array.isArray(value) ? [...(value as readonly T[])] : [value as T]
62
+ }
63
+
64
+ /**
65
+ * A select with search, for long option lists. `multiple` allows several values, shown
66
+ * as chips. The value type is inferred from `options`.
67
+ */
68
+ export function Combobox<T extends string>(inProps: ComboboxProps<T>) {
69
+ const [props, slots] = useComponentConfig('Combobox', useFieldControl(inProps))
70
+ const {
71
+ options,
72
+ multiple = false,
73
+ placeholder = 'Select…',
74
+ searchPlaceholder = 'Search…',
75
+ emptyText = 'No results',
76
+ maxChips = 3,
77
+ size,
78
+ disabled,
79
+ required,
80
+ name,
81
+ id,
82
+ className,
83
+ contentClassName,
84
+ 'aria-label': ariaLabel,
85
+ 'aria-describedby': ariaDescribedBy,
86
+ 'aria-invalid': ariaInvalid,
87
+ } = props
88
+ const onValueChange = props.onValueChange as ((value: T | T[] | null) => void) | undefined
89
+
90
+ const [open, setOpen] = useState(false)
91
+ const [selected, setSelected] = useControllableState<T[]>({
92
+ value: toArray<T>(props.value),
93
+ defaultValue: toArray<T>(props.defaultValue) ?? [],
94
+ onChange: (next) => onValueChange?.(multiple ? next : (next[0] ?? null)),
95
+ })
96
+ const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options])
97
+ const s = comboboxStyles({ size, multiple })
98
+
99
+ const choose = (value: T) => {
100
+ if (multiple) {
101
+ setSelected(
102
+ selected.includes(value) ? selected.filter((v) => v !== value) : [...selected, value],
103
+ )
104
+ return
105
+ }
106
+ setSelected(selected[0] === value ? [] : [value])
107
+ setOpen(false)
108
+ }
109
+
110
+ const labels = selected.map((v) => byValue.get(v)?.label ?? v)
111
+ let display: ReactNode
112
+ if (labels.length === 0) {
113
+ display = <span className={s.placeholder({ class: slots.placeholder })}>{placeholder}</span>
114
+ } else if (multiple) {
115
+ const shown = labels.slice(0, maxChips)
116
+ display = (
117
+ <>
118
+ {shown.map((label, i) => (
119
+ <span key={selected[i]} className={s.chip({ class: slots.chip })}>
120
+ {label}
121
+ </span>
122
+ ))}
123
+ {labels.length > shown.length && (
124
+ <span className={s.chip({ class: slots.chip })}>+{labels.length - shown.length}</span>
125
+ )}
126
+ </>
127
+ )
128
+ } else {
129
+ display = <span className="truncate">{labels[0]}</span>
130
+ }
131
+
132
+ return (
133
+ <Popover.Root open={open} onOpenChange={setOpen}>
134
+ <Popover.Trigger asChild>
135
+ <button
136
+ type="button"
137
+ id={id}
138
+ role="combobox"
139
+ aria-haspopup="listbox"
140
+ aria-expanded={open}
141
+ aria-label={ariaLabel}
142
+ aria-describedby={ariaDescribedBy}
143
+ aria-invalid={ariaInvalid}
144
+ aria-required={required}
145
+ disabled={disabled}
146
+ data-slot="combobox-trigger"
147
+ className={s.trigger({ class: [slots.trigger, className] })}
148
+ >
149
+ <span className={s.value({ class: slots.value })}>{display}</span>
150
+ <ChevronsUpDownIcon className={s.icon({ class: slots.icon })} />
151
+ </button>
152
+ </Popover.Trigger>
153
+ {name &&
154
+ selected.map((value) => <input key={value} type="hidden" name={name} value={value} />)}
155
+ <Popover.Portal>
156
+ <Popover.Content
157
+ align="start"
158
+ sideOffset={4}
159
+ data-slot="combobox-content"
160
+ className={s.content({ class: [slots.content, contentClassName] })}
161
+ >
162
+ <Cmdk loop label={ariaLabel ?? 'Options'}>
163
+ <div className={s.search({ class: slots.search })}>
164
+ <SearchIcon />
165
+ <Cmdk.Input
166
+ placeholder={searchPlaceholder}
167
+ className={s.input({ class: slots.input })}
168
+ />
169
+ </div>
170
+ <Cmdk.List className={s.list({ class: slots.list })}>
171
+ <Cmdk.Empty className={s.empty({ class: slots.empty })}>{emptyText}</Cmdk.Empty>
172
+ {options.map((option) => {
173
+ const checked = selected.includes(option.value)
174
+ return (
175
+ <Cmdk.Item
176
+ key={option.value}
177
+ value={option.value}
178
+ keywords={[option.label, ...(option.keywords ?? [])]}
179
+ disabled={option.disabled}
180
+ onSelect={() => choose(option.value)}
181
+ data-checked={checked || undefined}
182
+ aria-checked={checked}
183
+ className={s.item({ class: slots.item })}
184
+ >
185
+ <span className={s.itemText({ class: slots.itemText })}>
186
+ <span className="truncate">{option.label}</span>
187
+ {option.description && (
188
+ <span className={s.itemDescription({ class: slots.itemDescription })}>
189
+ {option.description}
190
+ </span>
191
+ )}
192
+ </span>
193
+ {checked && <CheckIcon className={s.check({ class: slots.check })} />}
194
+ </Cmdk.Item>
195
+ )
196
+ })}
197
+ </Cmdk.List>
198
+ </Cmdk>
199
+ </Popover.Content>
200
+ </Popover.Portal>
201
+ </Popover.Root>
202
+ )
203
+ }
@@ -0,0 +1,2 @@
1
+ export { Combobox, type ComboboxOption, type ComboboxProps } from './combobox'
2
+ export { type ComboboxVariants, comboboxStyles } from './combobox.styles'
@@ -0,0 +1,41 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { themedBorder } from '../../utils/styles'
3
+ import { tv } from '../../utils/tv'
4
+
5
+ export const commandMenuStyles = tv({
6
+ slots: {
7
+ overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
8
+ content: [
9
+ 'fixed top-[12vh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-xl -translate-x-1/2 overflow-hidden rounded-xl border-border bg-raised text-foreground shadow-lg outline-none animate-pop-in',
10
+ themedBorder,
11
+ ],
12
+ root: 'flex w-full flex-col overflow-hidden bg-raised text-foreground',
13
+ search:
14
+ 'flex items-center gap-2.5 border-b border-border px-4 text-faint-foreground [&>svg]:size-4 [&>svg]:shrink-0',
15
+ input:
16
+ 'h-12 w-full min-w-0 bg-transparent text-body text-foreground outline-none placeholder:text-faint-foreground',
17
+ list: 'max-h-[min(24rem,60vh)] overflow-y-auto overscroll-contain p-2',
18
+ empty: 'py-10 text-center text-small text-muted-foreground',
19
+ group: [
20
+ 'overflow-hidden',
21
+ '[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pt-2 [&_[cmdk-group-heading]]:pb-1',
22
+ '[&_[cmdk-group-heading]]:font-mono [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:uppercase',
23
+ '[&_[cmdk-group-heading]]:tracking-[0.1em] [&_[cmdk-group-heading]]:text-faint-foreground',
24
+ ],
25
+ item: [
26
+ 'relative flex h-10 cursor-pointer select-none items-center gap-3 rounded-md px-2.5 text-small text-foreground outline-none',
27
+ 'data-[selected=true]:bg-muted data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
28
+ ],
29
+ itemIcon:
30
+ 'flex size-4 shrink-0 items-center justify-center text-muted-foreground [&>svg]:size-4',
31
+ itemLabel: 'min-w-0 flex-1 truncate',
32
+ shortcut: 'ml-auto flex shrink-0 items-center gap-1',
33
+ separator: '-mx-2 my-1.5 h-px bg-border',
34
+ },
35
+ })
36
+
37
+ declare module '@glyphui/tokens' {
38
+ interface GlyphComponentRegistry {
39
+ CommandMenu: StylesEntry<typeof commandMenuStyles>
40
+ }
41
+ }
@@ -0,0 +1,217 @@
1
+ import { Command as Cmdk } from 'cmdk'
2
+ import { Dialog } from 'radix-ui'
3
+ import {
4
+ type ComponentProps,
5
+ createContext,
6
+ type Dispatch,
7
+ type ReactNode,
8
+ type SetStateAction,
9
+ useContext,
10
+ useEffect,
11
+ } from 'react'
12
+ import { useComponentConfig } from '../../hooks/use-component-config'
13
+ import { useControllableState } from '../../hooks/use-controllable-state'
14
+ import { SearchIcon } from '../../internal/icons'
15
+ import { Kbd } from '../kbd'
16
+ import { commandMenuStyles } from './command-menu.styles'
17
+
18
+ const s = commandMenuStyles()
19
+
20
+ const CommandMenuContext = createContext<{ close: () => void } | null>(null)
21
+
22
+ function useSlots() {
23
+ return useComponentConfig('CommandMenu', {})[1]
24
+ }
25
+
26
+ export interface CommandMenuProps {
27
+ open?: boolean
28
+ defaultOpen?: boolean
29
+ onOpenChange?: (open: boolean) => void
30
+ /** Accessible name of the dialog and list. */
31
+ label?: string
32
+ /** Wrap around from the last item to the first with arrow keys. Default true. */
33
+ loop?: boolean
34
+ /** Replace cmdk's fuzzy ranking. Return 0 to hide an item, higher numbers to rank it first. */
35
+ filter?: (value: string, search: string, keywords?: string[]) => number
36
+ className?: string
37
+ children: ReactNode
38
+ }
39
+
40
+ /**
41
+ * A ⌘K command palette in a modal dialog: search, grouped actions, keyboard navigation.
42
+ * Pair with `useCommandMenuShortcut(setOpen)` to open it from the keyboard.
43
+ */
44
+ export function CommandMenu(inProps: CommandMenuProps) {
45
+ const [props, slots] = useComponentConfig('CommandMenu', inProps)
46
+ const {
47
+ open: openProp,
48
+ defaultOpen = false,
49
+ onOpenChange,
50
+ label = 'Command menu',
51
+ loop = true,
52
+ filter,
53
+ className,
54
+ children,
55
+ } = props
56
+ const [open, setOpen] = useControllableState({
57
+ value: openProp,
58
+ defaultValue: defaultOpen,
59
+ onChange: onOpenChange,
60
+ })
61
+
62
+ return (
63
+ <Dialog.Root open={open} onOpenChange={setOpen}>
64
+ <Dialog.Portal>
65
+ <Dialog.Overlay className={s.overlay({ class: slots.overlay })} />
66
+ <Dialog.Content
67
+ data-slot="command-menu"
68
+ aria-describedby={undefined}
69
+ className={s.content({ class: [slots.content, className] })}
70
+ >
71
+ <Dialog.Title className="sr-only">{label}</Dialog.Title>
72
+ <CommandMenuContext.Provider value={{ close: () => setOpen(false) }}>
73
+ <Command label={label} loop={loop} {...(filter && { filter })}>
74
+ {children}
75
+ </Command>
76
+ </CommandMenuContext.Provider>
77
+ </Dialog.Content>
78
+ </Dialog.Portal>
79
+ </Dialog.Root>
80
+ )
81
+ }
82
+
83
+ /** The palette without a dialog, for inline search panels. */
84
+ export function Command({ className, ...props }: ComponentProps<typeof Cmdk>) {
85
+ const slots = useSlots()
86
+ return (
87
+ <Cmdk data-slot="command" className={s.root({ class: [slots.root, className] })} {...props} />
88
+ )
89
+ }
90
+
91
+ export function CommandInput({ className, ...props }: ComponentProps<typeof Cmdk.Input>) {
92
+ const slots = useSlots()
93
+ return (
94
+ <div className={s.search({ class: slots.search })}>
95
+ <SearchIcon />
96
+ <Cmdk.Input
97
+ data-slot="command-input"
98
+ className={s.input({ class: [slots.input, className] })}
99
+ {...props}
100
+ />
101
+ </div>
102
+ )
103
+ }
104
+
105
+ export function CommandList({ className, ...props }: ComponentProps<typeof Cmdk.List>) {
106
+ const slots = useSlots()
107
+ return (
108
+ <Cmdk.List
109
+ data-slot="command-list"
110
+ className={s.list({ class: [slots.list, className] })}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ export function CommandEmpty({
117
+ className,
118
+ children = 'No results',
119
+ ...props
120
+ }: ComponentProps<typeof Cmdk.Empty>) {
121
+ const slots = useSlots()
122
+ return (
123
+ <Cmdk.Empty
124
+ data-slot="command-empty"
125
+ className={s.empty({ class: [slots.empty, className] })}
126
+ {...props}
127
+ >
128
+ {children}
129
+ </Cmdk.Empty>
130
+ )
131
+ }
132
+
133
+ export function CommandGroup({ className, ...props }: ComponentProps<typeof Cmdk.Group>) {
134
+ const slots = useSlots()
135
+ return (
136
+ <Cmdk.Group
137
+ data-slot="command-group"
138
+ className={s.group({ class: [slots.group, className] })}
139
+ {...props}
140
+ />
141
+ )
142
+ }
143
+
144
+ export interface CommandItemProps extends ComponentProps<typeof Cmdk.Item> {
145
+ icon?: ReactNode
146
+ /** Keyboard hint shown at the end, e.g. `"⌘N"` or `["⌘", "N"]`. Display only. */
147
+ shortcut?: string | readonly string[]
148
+ /** Close the surrounding CommandMenu after selecting. Default true. */
149
+ closeOnSelect?: boolean
150
+ }
151
+
152
+ export function CommandItem({
153
+ icon,
154
+ shortcut,
155
+ closeOnSelect = true,
156
+ onSelect,
157
+ className,
158
+ children,
159
+ ...props
160
+ }: CommandItemProps) {
161
+ const slots = useSlots()
162
+ const menu = useContext(CommandMenuContext)
163
+ const keys = shortcut === undefined ? [] : typeof shortcut === 'string' ? [shortcut] : shortcut
164
+ return (
165
+ <Cmdk.Item
166
+ data-slot="command-item"
167
+ onSelect={(value) => {
168
+ onSelect?.(value)
169
+ if (closeOnSelect) menu?.close()
170
+ }}
171
+ className={s.item({ class: [slots.item, className] })}
172
+ {...props}
173
+ >
174
+ {icon && (
175
+ <span className={s.itemIcon({ class: slots.itemIcon })} aria-hidden="true">
176
+ {icon}
177
+ </span>
178
+ )}
179
+ <span className={s.itemLabel({ class: slots.itemLabel })}>{children}</span>
180
+ {keys.length > 0 && (
181
+ <span className={s.shortcut({ class: slots.shortcut })} aria-hidden="true">
182
+ {keys.map((key) => (
183
+ <Kbd key={key}>{key}</Kbd>
184
+ ))}
185
+ </span>
186
+ )}
187
+ </Cmdk.Item>
188
+ )
189
+ }
190
+
191
+ export function CommandSeparator({ className, ...props }: ComponentProps<typeof Cmdk.Separator>) {
192
+ const slots = useSlots()
193
+ return (
194
+ <Cmdk.Separator
195
+ data-slot="command-separator"
196
+ className={s.separator({ class: [slots.separator, className] })}
197
+ {...props}
198
+ />
199
+ )
200
+ }
201
+
202
+ /** Toggles a command menu with ⌘K (macOS) or Ctrl+K. Pass another key to change it. */
203
+ export function useCommandMenuShortcut(
204
+ setOpen: Dispatch<SetStateAction<boolean>>,
205
+ key = 'k',
206
+ ): void {
207
+ useEffect(() => {
208
+ const onKeyDown = (event: KeyboardEvent) => {
209
+ if (event.key.toLowerCase() === key.toLowerCase() && (event.metaKey || event.ctrlKey)) {
210
+ event.preventDefault()
211
+ setOpen((open) => !open)
212
+ }
213
+ }
214
+ document.addEventListener('keydown', onKeyDown)
215
+ return () => document.removeEventListener('keydown', onKeyDown)
216
+ }, [setOpen, key])
217
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ Command,
3
+ CommandEmpty,
4
+ CommandGroup,
5
+ CommandInput,
6
+ CommandItem,
7
+ type CommandItemProps,
8
+ CommandList,
9
+ CommandMenu,
10
+ type CommandMenuProps,
11
+ CommandSeparator,
12
+ useCommandMenuShortcut,
13
+ } from './command-menu'
14
+ export { commandMenuStyles } from './command-menu.styles'
@@ -0,0 +1,31 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { tv, type VariantProps } from '../../utils/tv'
3
+
4
+ export const descriptionListStyles = tv({
5
+ slots: {
6
+ root: 'grid w-full',
7
+ item: 'grid min-w-0 gap-1',
8
+ term: 'text-small text-muted-foreground',
9
+ details: 'min-w-0 break-words text-small text-foreground',
10
+ },
11
+ variants: {
12
+ layout: {
13
+ stacked: { root: 'gap-4' },
14
+ inline: { item: 'sm:grid-cols-[minmax(0,12rem)_1fr] sm:gap-4' },
15
+ },
16
+ dividers: {
17
+ true: { root: 'gap-0 divide-y divide-border', item: 'py-3 first:pt-0 last:pb-0' },
18
+ false: {},
19
+ },
20
+ },
21
+ compoundVariants: [{ layout: 'inline', dividers: false, class: { root: 'gap-3' } }],
22
+ defaultVariants: { layout: 'stacked', dividers: false },
23
+ })
24
+
25
+ export type DescriptionListVariants = VariantProps<typeof descriptionListStyles>
26
+
27
+ declare module '@glyphui/tokens' {
28
+ interface GlyphComponentRegistry {
29
+ DescriptionList: StylesEntry<typeof descriptionListStyles>
30
+ }
31
+ }
@@ -0,0 +1,56 @@
1
+ import { type ComponentProps, createContext, type ReactNode, useContext } from 'react'
2
+ import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
3
+ import { type DescriptionListVariants, descriptionListStyles } from './description-list.styles'
4
+
5
+ type Styles = ReturnType<typeof descriptionListStyles>
6
+
7
+ const DescriptionListContext = createContext<{
8
+ s: Styles
9
+ slots: SlotClasses<'DescriptionList'>
10
+ } | null>(null)
11
+
12
+ export interface DescriptionListProps extends ComponentProps<'dl'>, DescriptionListVariants {}
13
+
14
+ /**
15
+ * Label–value pairs, such as account details or order metadata.
16
+ * `layout="inline"` puts terms and details side by side from the `sm` breakpoint.
17
+ */
18
+ export function DescriptionList(inProps: DescriptionListProps) {
19
+ const [props, slots] = useComponentConfig('DescriptionList', inProps)
20
+ const { layout, dividers, className, ...rest } = props
21
+ const s = descriptionListStyles({ layout, dividers })
22
+ return (
23
+ <DescriptionListContext.Provider value={{ s, slots }}>
24
+ <dl
25
+ data-slot="description-list"
26
+ className={s.root({ class: [slots.root, className] })}
27
+ {...rest}
28
+ />
29
+ </DescriptionListContext.Provider>
30
+ )
31
+ }
32
+
33
+ export interface DescriptionItemProps extends Omit<ComponentProps<'div'>, 'children'> {
34
+ term: ReactNode
35
+ children: ReactNode
36
+ }
37
+
38
+ export function DescriptionItem({ term, children, className, ...props }: DescriptionItemProps) {
39
+ const ctx = useContext(DescriptionListContext)
40
+ const s = ctx?.s ?? descriptionListStyles()
41
+ const slots = ctx?.slots ?? {}
42
+ return (
43
+ <div
44
+ data-slot="description-item"
45
+ className={s.item({ class: [slots.item, className] })}
46
+ {...props}
47
+ >
48
+ <dt data-slot="description-term" className={s.term({ class: slots.term })}>
49
+ {term}
50
+ </dt>
51
+ <dd data-slot="description-details" className={s.details({ class: slots.details })}>
52
+ {children}
53
+ </dd>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ DescriptionItem,
3
+ type DescriptionItemProps,
4
+ DescriptionList,
5
+ type DescriptionListProps,
6
+ } from './description-list'
7
+ export { type DescriptionListVariants, descriptionListStyles } from './description-list.styles'
@@ -0,0 +1,22 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ /** Local close icon so overlay components stay self-contained. */
4
+ export function CloseIcon(props: SVGProps<SVGSVGElement>) {
5
+ return (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ width="16"
9
+ height="16"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ {...props}
18
+ >
19
+ <path d="M18 6 6 18M6 6l12 12" />
20
+ </svg>
21
+ )
22
+ }
@@ -0,0 +1,41 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { focusRing, themedBorder, transition } from '../../utils/styles'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const dialogStyles = tv({
6
+ slots: {
7
+ overlay: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
8
+ content: [
9
+ 'fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 gap-5 overflow-y-auto',
10
+ 'rounded-xl border-border bg-surface p-6 text-foreground shadow-lg outline-none animate-pop-in',
11
+ themedBorder,
12
+ ],
13
+ close: [
14
+ 'absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground [&>svg]:size-4',
15
+ transition,
16
+ focusRing,
17
+ ],
18
+ header: 'flex flex-col gap-1.5 pr-8',
19
+ title: 'font-display text-h4 text-foreground',
20
+ description: 'text-small text-muted-foreground',
21
+ body: 'min-w-0 text-small text-foreground',
22
+ footer: 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
23
+ },
24
+ variants: {
25
+ size: {
26
+ sm: { content: 'max-w-sm' },
27
+ md: { content: 'max-w-lg' },
28
+ lg: { content: 'max-w-2xl' },
29
+ xl: { content: 'max-w-4xl' },
30
+ },
31
+ },
32
+ defaultVariants: { size: 'md' },
33
+ })
34
+
35
+ export type DialogVariants = VariantProps<typeof dialogStyles>
36
+
37
+ declare module '@glyphui/tokens' {
38
+ interface GlyphComponentRegistry {
39
+ Dialog: StylesEntry<typeof dialogStyles>
40
+ }
41
+ }