@glyphui/ui 0.1.0 → 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.
- package/README.md +1 -1
- package/dist/data-table.js +2 -2
- package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
- package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
- package/dist/form.js +2 -2
- package/dist/index.d.ts +2743 -93
- package/dist/index.js +2748 -111
- package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
- package/package.json +4 -2
- package/src/components/accordion/accordion.styles.ts +41 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/alert-icons.tsx +66 -0
- package/src/components/alert/alert.styles.ts +57 -0
- package/src/components/alert/alert.tsx +86 -0
- package/src/components/alert/index.ts +2 -0
- package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
- package/src/components/alert-dialog/alert-dialog.tsx +118 -0
- package/src/components/alert-dialog/confirm.tsx +111 -0
- package/src/components/alert-dialog/index.ts +15 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/badge/badge.styles.ts +1 -1
- package/src/components/banner/banner.styles.ts +40 -0
- package/src/components/banner/banner.tsx +101 -0
- package/src/components/banner/index.ts +2 -0
- package/src/components/blockquote/blockquote.tsx +59 -0
- package/src/components/blockquote/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
- package/src/components/breadcrumb/breadcrumb.tsx +135 -0
- package/src/components/breadcrumb/index.ts +12 -0
- package/src/components/button/button.tsx +11 -8
- package/src/components/code/code.styles.ts +33 -0
- package/src/components/code/code.tsx +96 -0
- package/src/components/code/index.ts +2 -0
- package/src/components/combobox/combobox.styles.ts +50 -0
- package/src/components/combobox/combobox.tsx +203 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command-menu/command-menu.styles.ts +41 -0
- package/src/components/command-menu/command-menu.tsx +217 -0
- package/src/components/command-menu/index.ts +14 -0
- package/src/components/description-list/description-list.styles.ts +31 -0
- package/src/components/description-list/description-list.tsx +56 -0
- package/src/components/description-list/index.ts +7 -0
- package/src/components/dialog/close-icon.tsx +22 -0
- package/src/components/dialog/dialog.styles.ts +41 -0
- package/src/components/dialog/dialog.tsx +111 -0
- package/src/components/dialog/index.ts +13 -0
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
- package/src/components/dropdown-menu/index.ts +19 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/link/link.styles.ts +39 -0
- package/src/components/link/link.tsx +62 -0
- package/src/components/pagination/index.ts +3 -0
- package/src/components/pagination/pagination-range.ts +35 -0
- package/src/components/pagination/pagination.styles.ts +41 -0
- package/src/components/pagination/pagination.tsx +130 -0
- package/src/components/popover/index.ts +9 -0
- package/src/components/popover/popover.tsx +66 -0
- package/src/components/progress/index.ts +7 -0
- package/src/components/progress/progress.styles.ts +68 -0
- package/src/components/progress/progress.tsx +158 -0
- package/src/components/prose/index.ts +1 -0
- package/src/components/prose/prose.tsx +82 -0
- package/src/components/radio-group/index.ts +7 -0
- package/src/components/radio-group/radio-group.styles.ts +64 -0
- package/src/components/radio-group/radio-group.tsx +128 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +68 -0
- package/src/components/sheet/index.ts +13 -0
- package/src/components/sheet/sheet.styles.ts +60 -0
- package/src/components/sheet/sheet.tsx +108 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider.styles.ts +32 -0
- package/src/components/slider/slider.tsx +131 -0
- package/src/components/stat-card/index.ts +2 -0
- package/src/components/stat-card/stat-card.styles.ts +24 -0
- package/src/components/stat-card/stat-card.tsx +108 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/index.ts +25 -0
- 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,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,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
|
+
}
|