@glyphui/ui 0.1.1 → 0.3.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/chart.d.ts +202 -0
- package/dist/chart.js +457 -0
- package/dist/data-table.js +3 -2
- package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
- package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
- package/dist/form.js +3 -2
- package/dist/index.d.ts +4960 -266
- package/dist/index.js +5336 -146
- package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
- package/dist/tv-DBAGlgXJ.js +58 -0
- package/package.json +19 -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/calendar/calendar.styles.ts +50 -0
- package/src/components/calendar/calendar.tsx +84 -0
- package/src/components/calendar/index.ts +3 -0
- package/src/components/carousel/carousel.styles.ts +43 -0
- package/src/components/carousel/carousel.tsx +278 -0
- package/src/components/carousel/index.ts +13 -0
- 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/collapsible/collapsible.tsx +66 -0
- package/src/components/collapsible/index.ts +6 -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/context-menu/context-menu.styles.ts +19 -0
- package/src/components/context-menu/context-menu.tsx +226 -0
- package/src/components/context-menu/index.ts +19 -0
- package/src/components/copy-button/copy-button.tsx +117 -0
- package/src/components/copy-button/index.ts +1 -0
- package/src/components/date-picker/date-picker.styles.ts +42 -0
- package/src/components/date-picker/date-picker.tsx +322 -0
- package/src/components/date-picker/index.ts +8 -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/file-upload/file-upload.styles.ts +43 -0
- package/src/components/file-upload/file-upload.tsx +343 -0
- package/src/components/file-upload/index.ts +10 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/hover-card/hover-card.tsx +41 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/input-otp/index.ts +10 -0
- package/src/components/input-otp/input-otp.styles.ts +38 -0
- package/src/components/input-otp/input-otp.tsx +116 -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/modal/index.ts +2 -0
- package/src/components/modal/modal.styles.ts +18 -0
- package/src/components/modal/modal.tsx +86 -0
- package/src/components/navigation-menu/index.ts +14 -0
- package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
- package/src/components/navigation-menu/navigation-menu.tsx +198 -0
- package/src/components/number-input/index.ts +2 -0
- package/src/components/number-input/number-input.styles.ts +41 -0
- package/src/components/number-input/number-input.tsx +211 -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/password-input/index.ts +7 -0
- package/src/components/password-input/password-input.styles.ts +30 -0
- package/src/components/password-input/password-input.tsx +127 -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/resizable/index.ts +9 -0
- package/src/components/resizable/resizable.styles.ts +28 -0
- package/src/components/resizable/resizable.tsx +69 -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/stepper/index.ts +2 -0
- package/src/components/stepper/stepper.styles.ts +74 -0
- package/src/components/stepper/stepper.tsx +138 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.styles.ts +39 -0
- package/src/components/tag-input/tag-input.tsx +198 -0
- package/src/components/timeline/index.ts +8 -0
- package/src/components/timeline/timeline.styles.ts +56 -0
- package/src/components/timeline/timeline.tsx +91 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/components/toggle/index.ts +9 -0
- package/src/components/toggle/toggle.styles.ts +39 -0
- package/src/components/toggle/toggle.tsx +61 -0
- package/src/components/tree-view/index.ts +2 -0
- package/src/components/tree-view/tree-view.styles.ts +42 -0
- package/src/components/tree-view/tree-view.tsx +274 -0
- package/src/components/visually-hidden/index.ts +1 -0
- package/src/components/visually-hidden/visually-hidden.tsx +10 -0
- package/src/index.ts +45 -0
- package/src/patterns/chart/chart-parts.tsx +171 -0
- package/src/patterns/chart/chart.styles.ts +35 -0
- package/src/patterns/chart/chart.tsx +369 -0
- package/src/patterns/chart/index.ts +13 -0
- package/src/provider/glyph-provider.tsx +16 -2
- package/styles.css +16 -0
|
@@ -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,19 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
import { dropdownMenuStyles } from '../dropdown-menu/dropdown-menu.styles'
|
|
4
|
+
|
|
5
|
+
/** Same look as DropdownMenu; only the available-height variable differs. */
|
|
6
|
+
export const contextMenuStyles = tv({
|
|
7
|
+
extend: dropdownMenuStyles,
|
|
8
|
+
slots: {
|
|
9
|
+
content: 'max-h-(--radix-context-menu-content-available-height)',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ContextMenuVariants = VariantProps<typeof contextMenuStyles>
|
|
14
|
+
|
|
15
|
+
declare module '@glyphui/tokens' {
|
|
16
|
+
interface GlyphComponentRegistry {
|
|
17
|
+
ContextMenu: StylesEntry<typeof contextMenuStyles>
|
|
18
|
+
}
|
|
19
|
+
}
|