@glyphui/ui 0.2.0 → 0.4.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 (116) hide show
  1. package/README.md +65 -12
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +466 -0
  4. package/dist/color-mode-script-Bf7LYkOj.js +13 -0
  5. package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
  6. package/dist/data-table.js +3 -2
  7. package/dist/{field-CCecIzdq.js → field-CVbywksE.js} +21 -5
  8. package/dist/{field-context-B6WOcK5m.js → field-context-7I-cWvvS.js} +3 -57
  9. package/dist/form.d.ts +11 -4
  10. package/dist/form.js +34 -8
  11. package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
  12. package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
  13. package/dist/index.d.ts +2334 -283
  14. package/dist/index.js +2952 -326
  15. package/dist/react-router.d.ts +18 -0
  16. package/dist/react-router.js +44 -0
  17. package/dist/{states-Rp8wZ_jH.js → states-IvhjBgna.js} +2 -1
  18. package/dist/tv-DLbT66dC.js +70 -0
  19. package/dist/vite.d.ts +27 -0
  20. package/dist/vite.js +29 -0
  21. package/fonts.css +10 -0
  22. package/package.json +47 -5
  23. package/src/components/accordion/accordion.styles.ts +4 -4
  24. package/src/components/badge/badge.styles.ts +11 -16
  25. package/src/components/banner/banner.styles.ts +4 -4
  26. package/src/components/button/button.styles.ts +3 -1
  27. package/src/components/calendar/calendar.styles.ts +51 -0
  28. package/src/components/calendar/calendar.tsx +84 -0
  29. package/src/components/calendar/index.ts +3 -0
  30. package/src/components/carousel/carousel.styles.ts +43 -0
  31. package/src/components/carousel/carousel.tsx +278 -0
  32. package/src/components/carousel/index.ts +13 -0
  33. package/src/components/collapsible/collapsible.tsx +66 -0
  34. package/src/components/collapsible/index.ts +6 -0
  35. package/src/components/context-menu/context-menu.styles.ts +19 -0
  36. package/src/components/context-menu/context-menu.tsx +226 -0
  37. package/src/components/context-menu/index.ts +19 -0
  38. package/src/components/copy-button/copy-button.tsx +117 -0
  39. package/src/components/copy-button/index.ts +1 -0
  40. package/src/components/date-picker/date-picker.styles.ts +42 -0
  41. package/src/components/date-picker/date-picker.tsx +322 -0
  42. package/src/components/date-picker/index.ts +8 -0
  43. package/src/components/dropdown-menu/dropdown-menu.styles.ts +2 -2
  44. package/src/components/eyebrow/eyebrow.styles.ts +1 -1
  45. package/src/components/field/field-context.ts +2 -0
  46. package/src/components/field/field.styles.ts +4 -2
  47. package/src/components/field/field.tsx +25 -4
  48. package/src/components/file-upload/file-upload.styles.ts +43 -0
  49. package/src/components/file-upload/file-upload.tsx +343 -0
  50. package/src/components/file-upload/index.ts +10 -0
  51. package/src/components/hover-card/hover-card.tsx +41 -0
  52. package/src/components/hover-card/index.ts +1 -0
  53. package/src/components/input-otp/index.ts +10 -0
  54. package/src/components/input-otp/input-otp.styles.ts +38 -0
  55. package/src/components/input-otp/input-otp.tsx +116 -0
  56. package/src/components/link/link.styles.ts +1 -1
  57. package/src/components/modal/index.ts +2 -0
  58. package/src/components/modal/modal.styles.ts +20 -0
  59. package/src/components/modal/modal.tsx +111 -0
  60. package/src/components/navigation-menu/index.ts +14 -0
  61. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  62. package/src/components/navigation-menu/navigation-menu.tsx +208 -0
  63. package/src/components/number-input/index.ts +2 -0
  64. package/src/components/number-input/number-input.styles.ts +41 -0
  65. package/src/components/number-input/number-input.tsx +211 -0
  66. package/src/components/password-input/index.ts +7 -0
  67. package/src/components/password-input/password-input.styles.ts +30 -0
  68. package/src/components/password-input/password-input.tsx +127 -0
  69. package/src/components/prose/prose.tsx +1 -1
  70. package/src/components/resizable/index.ts +9 -0
  71. package/src/components/resizable/resizable.styles.ts +28 -0
  72. package/src/components/resizable/resizable.tsx +69 -0
  73. package/src/components/scroll-area/scroll-area.tsx +23 -1
  74. package/src/components/segmented-control/segmented-control.tsx +17 -2
  75. package/src/components/sheet/sheet.styles.ts +7 -7
  76. package/src/components/slider/slider.tsx +9 -2
  77. package/src/components/stepper/index.ts +2 -0
  78. package/src/components/stepper/stepper.styles.ts +74 -0
  79. package/src/components/stepper/stepper.tsx +138 -0
  80. package/src/components/tag-input/index.ts +2 -0
  81. package/src/components/tag-input/tag-input.styles.ts +39 -0
  82. package/src/components/tag-input/tag-input.tsx +198 -0
  83. package/src/components/text/text.styles.ts +4 -4
  84. package/src/components/timeline/index.ts +8 -0
  85. package/src/components/timeline/timeline.styles.ts +56 -0
  86. package/src/components/timeline/timeline.tsx +91 -0
  87. package/src/components/toggle/index.ts +9 -0
  88. package/src/components/toggle/toggle.styles.ts +39 -0
  89. package/src/components/toggle/toggle.tsx +61 -0
  90. package/src/components/tree-view/index.ts +2 -0
  91. package/src/components/tree-view/tree-view.styles.ts +42 -0
  92. package/src/components/tree-view/tree-view.tsx +274 -0
  93. package/src/components/visually-hidden/index.ts +1 -0
  94. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  95. package/src/index.ts +20 -0
  96. package/src/integrations/react-router.tsx +39 -0
  97. package/src/integrations/vite.test-d.ts +6 -0
  98. package/src/integrations/vite.test.ts +28 -0
  99. package/src/integrations/vite.ts +33 -0
  100. package/src/internal/dev.ts +12 -0
  101. package/src/patterns/app-shell/app-shell.styles.ts +1 -1
  102. package/src/patterns/app-shell/navbar.tsx +34 -16
  103. package/src/patterns/app-shell/sidebar.tsx +7 -2
  104. package/src/patterns/chart/chart-parts.tsx +179 -0
  105. package/src/patterns/chart/chart.styles.ts +35 -0
  106. package/src/patterns/chart/chart.tsx +386 -0
  107. package/src/patterns/chart/index.ts +13 -0
  108. package/src/patterns/form/form.tsx +48 -6
  109. package/src/patterns/form/index.ts +1 -0
  110. package/src/provider/color-mode-script.ts +28 -0
  111. package/src/provider/color-mode.tsx +14 -20
  112. package/src/provider/glyph-context.ts +38 -0
  113. package/src/provider/glyph-provider.tsx +16 -4
  114. package/src/provider/index.ts +4 -0
  115. package/src/provider/theme-scope.tsx +7 -3
  116. package/styles.css +88 -0
@@ -0,0 +1,278 @@
1
+ import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
2
+ import {
3
+ Children,
4
+ type ComponentProps,
5
+ createContext,
6
+ type KeyboardEvent,
7
+ type ReactNode,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useState,
12
+ } from 'react'
13
+ import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
14
+ import { cn } from '../../utils/cn'
15
+ import { IconButton, type IconButtonProps } from '../button'
16
+ import { type CarouselVariants, carouselStyles } from './carousel.styles'
17
+
18
+ export type CarouselApi = UseEmblaCarouselType[1]
19
+ type EmblaArgs = Parameters<typeof useEmblaCarousel>
20
+ export type CarouselOptions = EmblaArgs[0]
21
+ export type CarouselPlugin = NonNullable<EmblaArgs[1]>[number]
22
+
23
+ type Orientation = NonNullable<CarouselVariants['orientation']>
24
+
25
+ interface CarouselContextValue {
26
+ viewportRef: UseEmblaCarouselType[0]
27
+ api: CarouselApi
28
+ orientation: Orientation
29
+ selectedIndex: number
30
+ snapCount: number
31
+ canScrollPrev: boolean
32
+ canScrollNext: boolean
33
+ scrollPrev: () => void
34
+ scrollNext: () => void
35
+ scrollTo: (index: number) => void
36
+ s: ReturnType<typeof carouselStyles>
37
+ slots: SlotClasses<'Carousel'>
38
+ }
39
+
40
+ const CarouselContext = createContext<CarouselContextValue | null>(null)
41
+
42
+ function useCarousel(): CarouselContextValue {
43
+ const value = useContext(CarouselContext)
44
+ if (!value) throw new Error('Carousel parts must be used inside <Carousel>.')
45
+ return value
46
+ }
47
+
48
+ export interface CarouselProps
49
+ extends Omit<ComponentProps<'section'>, 'aria-label'>,
50
+ CarouselVariants {
51
+ /** Embla options, e.g. `{ align: 'start', loop: true }`. */
52
+ opts?: CarouselOptions
53
+ plugins?: CarouselPlugin[]
54
+ /** Shortcut for `opts.loop`. */
55
+ loop?: boolean
56
+ /** Receives the Embla API for custom controls. */
57
+ setApi?: (api: CarouselApi) => void
58
+ /** Required: describes the content, e.g. "Customer stories". */
59
+ 'aria-label': string
60
+ }
61
+
62
+ /**
63
+ * Swipeable, keyboard-accessible slides. Compose with CarouselContent, CarouselItem,
64
+ * CarouselPrevious, CarouselNext and CarouselDots.
65
+ */
66
+ export function Carousel(inProps: CarouselProps) {
67
+ const [props, slots] = useComponentConfig('Carousel', inProps)
68
+ const {
69
+ opts,
70
+ plugins,
71
+ loop,
72
+ setApi,
73
+ orientation = 'horizontal',
74
+ className,
75
+ children,
76
+ ...rest
77
+ } = props
78
+ const [viewportRef, api] = useEmblaCarousel(
79
+ {
80
+ ...opts,
81
+ ...(loop !== undefined && { loop }),
82
+ axis: orientation === 'horizontal' ? 'x' : 'y',
83
+ },
84
+ plugins,
85
+ )
86
+ const [state, setState] = useState({
87
+ selectedIndex: 0,
88
+ snapCount: 0,
89
+ canScrollPrev: false,
90
+ canScrollNext: false,
91
+ })
92
+
93
+ const sync = useCallback((embla: NonNullable<CarouselApi>) => {
94
+ setState({
95
+ selectedIndex: embla.selectedScrollSnap(),
96
+ snapCount: embla.scrollSnapList().length,
97
+ canScrollPrev: embla.canScrollPrev(),
98
+ canScrollNext: embla.canScrollNext(),
99
+ })
100
+ }, [])
101
+
102
+ useEffect(() => {
103
+ if (!api) return
104
+ setApi?.(api)
105
+ sync(api)
106
+ api.on('select', sync).on('reInit', sync)
107
+ return () => {
108
+ api.off('select', sync).off('reInit', sync)
109
+ }
110
+ }, [api, setApi, sync])
111
+
112
+ const scrollPrev = useCallback(() => api?.scrollPrev(), [api])
113
+ const scrollNext = useCallback(() => api?.scrollNext(), [api])
114
+ const scrollTo = useCallback((index: number) => api?.scrollTo(index), [api])
115
+
116
+ const onKeyDownCapture = (event: KeyboardEvent<HTMLElement>) => {
117
+ const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
118
+ const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
119
+ if (event.key === prevKey) {
120
+ event.preventDefault()
121
+ scrollPrev()
122
+ } else if (event.key === nextKey) {
123
+ event.preventDefault()
124
+ scrollNext()
125
+ }
126
+ }
127
+
128
+ const s = carouselStyles({ orientation })
129
+ return (
130
+ <CarouselContext.Provider
131
+ value={{
132
+ viewportRef,
133
+ api,
134
+ orientation,
135
+ ...state,
136
+ scrollPrev,
137
+ scrollNext,
138
+ scrollTo,
139
+ s,
140
+ slots,
141
+ }}
142
+ >
143
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: a labelled section is a region, which supports aria-roledescription */}
144
+ <section
145
+ data-slot="carousel"
146
+ aria-roledescription="carousel"
147
+ onKeyDownCapture={onKeyDownCapture}
148
+ className={s.root({ class: [slots.root, className] })}
149
+ {...rest}
150
+ >
151
+ {children}
152
+ </section>
153
+ </CarouselContext.Provider>
154
+ )
155
+ }
156
+
157
+ const SlideContext = createContext<{ index: number; total: number } | null>(null)
158
+
159
+ export function CarouselContent({ className, children, ...props }: ComponentProps<'div'>) {
160
+ const { viewportRef, s, slots } = useCarousel()
161
+ const items = Children.toArray(children)
162
+ return (
163
+ <div
164
+ ref={viewportRef}
165
+ data-slot="carousel-viewport"
166
+ className={s.viewport({ class: slots.viewport })}
167
+ >
168
+ <div
169
+ data-slot="carousel-content"
170
+ className={s.container({ class: [slots.container, className] })}
171
+ {...props}
172
+ >
173
+ {items.map((child, index) => (
174
+ <SlideContext.Provider key={index} value={{ index, total: items.length }}>
175
+ {child}
176
+ </SlideContext.Provider>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ )
181
+ }
182
+
183
+ export function CarouselItem({ className, ...props }: ComponentProps<'div'>) {
184
+ const { s, slots } = useCarousel()
185
+ const slide = useContext(SlideContext)
186
+ return (
187
+ // biome-ignore lint/a11y/useSemanticElements: role="group" + aria-roledescription is the WAI-ARIA carousel slide pattern
188
+ <div
189
+ role="group"
190
+ aria-roledescription="slide"
191
+ aria-label={slide ? `${slide.index + 1} of ${slide.total}` : undefined}
192
+ data-slot="carousel-item"
193
+ className={s.item({ class: [slots.item, className] })}
194
+ {...props}
195
+ />
196
+ )
197
+ }
198
+
199
+ function Chevron({ direction }: { direction: 'left' | 'right' }) {
200
+ return (
201
+ <svg
202
+ viewBox="0 0 24 24"
203
+ fill="none"
204
+ stroke="currentColor"
205
+ strokeWidth="2"
206
+ strokeLinecap="round"
207
+ strokeLinejoin="round"
208
+ aria-hidden="true"
209
+ >
210
+ <path d={direction === 'left' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} />
211
+ </svg>
212
+ )
213
+ }
214
+
215
+ type ControlProps = Omit<IconButtonProps, 'icon' | 'aria-label'> & {
216
+ 'aria-label'?: string
217
+ icon?: ReactNode
218
+ }
219
+
220
+ export function CarouselPrevious({ className, icon, ...props }: ControlProps) {
221
+ const { scrollPrev, canScrollPrev, orientation, s, slots } = useCarousel()
222
+ return (
223
+ <IconButton
224
+ variant="outline"
225
+ size="sm"
226
+ aria-label="Previous slide"
227
+ icon={icon ?? <Chevron direction="left" />}
228
+ disabled={!canScrollPrev}
229
+ onClick={scrollPrev}
230
+ className={s.control({
231
+ class: [slots.control, orientation === 'horizontal' ? 'left-3' : 'top-3', className],
232
+ })}
233
+ {...props}
234
+ />
235
+ )
236
+ }
237
+
238
+ export function CarouselNext({ className, icon, ...props }: ControlProps) {
239
+ const { scrollNext, canScrollNext, orientation, s, slots } = useCarousel()
240
+ return (
241
+ <IconButton
242
+ variant="outline"
243
+ size="sm"
244
+ aria-label="Next slide"
245
+ icon={icon ?? <Chevron direction="right" />}
246
+ disabled={!canScrollNext}
247
+ onClick={scrollNext}
248
+ className={s.control({
249
+ class: [slots.control, orientation === 'horizontal' ? 'right-3' : 'bottom-3', className],
250
+ })}
251
+ {...props}
252
+ />
253
+ )
254
+ }
255
+
256
+ /** One button per snap point; the current one is highlighted. */
257
+ export function CarouselDots({ className, ...props }: ComponentProps<'div'>) {
258
+ const { snapCount, selectedIndex, scrollTo, s, slots } = useCarousel()
259
+ if (snapCount < 2) return null
260
+ return (
261
+ <div
262
+ data-slot="carousel-dots"
263
+ className={s.dots({ class: [slots.dots, className] })}
264
+ {...props}
265
+ >
266
+ {Array.from({ length: snapCount }, (_, index) => (
267
+ <button
268
+ key={index}
269
+ type="button"
270
+ aria-label={`Go to slide ${index + 1}`}
271
+ aria-current={index === selectedIndex}
272
+ onClick={() => scrollTo(index)}
273
+ className={cn(s.dot({ class: slots.dot }))}
274
+ />
275
+ ))}
276
+ </div>
277
+ )
278
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Carousel,
3
+ type CarouselApi,
4
+ CarouselContent,
5
+ CarouselDots,
6
+ CarouselItem,
7
+ CarouselNext,
8
+ type CarouselOptions,
9
+ type CarouselPlugin,
10
+ CarouselPrevious,
11
+ type CarouselProps,
12
+ } from './carousel'
13
+ export { type CarouselVariants, carouselStyles } from './carousel.styles'
@@ -0,0 +1,66 @@
1
+ import { Collapsible as CollapsiblePrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import type { StylesEntry } from '../../utils/registry'
5
+ import { tv } from '../../utils/tv'
6
+
7
+ export const collapsibleStyles = tv({
8
+ slots: {
9
+ root: '',
10
+ trigger: '',
11
+ content:
12
+ 'overflow-hidden data-[state=closed]:animate-collapse-up data-[state=open]:animate-collapse-down',
13
+ },
14
+ })
15
+
16
+ declare module '@glyphui/tokens' {
17
+ interface GlyphComponentRegistry {
18
+ Collapsible: StylesEntry<typeof collapsibleStyles>
19
+ }
20
+ }
21
+
22
+ const s = collapsibleStyles()
23
+
24
+ /**
25
+ * Shows and hides one region, e.g. "Show advanced options". For a list of expandable
26
+ * sections use Accordion.
27
+ */
28
+ export function Collapsible(inProps: ComponentProps<typeof CollapsiblePrimitive.Root>) {
29
+ const [{ className, ...rest }, slots] = useComponentConfig('Collapsible', inProps)
30
+ return (
31
+ <CollapsiblePrimitive.Root
32
+ data-slot="collapsible"
33
+ className={s.root({ class: [slots.root, className] })}
34
+ {...rest}
35
+ />
36
+ )
37
+ }
38
+
39
+ export function CollapsibleTrigger({
40
+ className,
41
+ ...props
42
+ }: ComponentProps<typeof CollapsiblePrimitive.Trigger>) {
43
+ const [, slots] = useComponentConfig('Collapsible', {})
44
+ return (
45
+ <CollapsiblePrimitive.Trigger
46
+ data-slot="collapsible-trigger"
47
+ className={s.trigger({ class: [slots.trigger, className] })}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ /** Animates its height open and closed; stays mounted until the closing animation ends. */
54
+ export function CollapsibleContent({
55
+ className,
56
+ ...props
57
+ }: ComponentProps<typeof CollapsiblePrimitive.Content>) {
58
+ const [, slots] = useComponentConfig('Collapsible', {})
59
+ return (
60
+ <CollapsiblePrimitive.Content
61
+ data-slot="collapsible-content"
62
+ className={s.content({ class: [slots.content, className] })}
63
+ {...props}
64
+ />
65
+ )
66
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ Collapsible,
3
+ CollapsibleContent,
4
+ CollapsibleTrigger,
5
+ collapsibleStyles,
6
+ } from './collapsible'
@@ -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
+ }
@@ -0,0 +1,226 @@
1
+ import { ContextMenu as MenuPrimitive } from 'radix-ui'
2
+ import type { ComponentProps, ReactNode, SVGProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { contextMenuStyles } from './context-menu.styles'
5
+
6
+ function useParts(tone?: 'default' | 'danger', inset?: boolean) {
7
+ const [, slots] = useComponentConfig('ContextMenu', {})
8
+ return { s: contextMenuStyles({ tone, inset }), slots }
9
+ }
10
+
11
+ const svg = {
12
+ xmlns: 'http://www.w3.org/2000/svg',
13
+ viewBox: '0 0 24 24',
14
+ fill: 'none',
15
+ stroke: 'currentColor',
16
+ strokeWidth: 2,
17
+ strokeLinecap: 'round',
18
+ strokeLinejoin: 'round',
19
+ } as const satisfies SVGProps<SVGSVGElement>
20
+
21
+ const CheckIcon = () => (
22
+ <svg aria-hidden="true" {...svg}>
23
+ <path d="M20 6 9 17l-5-5" />
24
+ </svg>
25
+ )
26
+ const DotIcon = () => (
27
+ <svg aria-hidden="true" {...svg}>
28
+ <circle cx="12" cy="12" r="4" fill="currentColor" />
29
+ </svg>
30
+ )
31
+ const ChevronRightIcon = () => (
32
+ <svg aria-hidden="true" {...svg}>
33
+ <path d="m9 18 6-6-6-6" />
34
+ </svg>
35
+ )
36
+
37
+ /** A menu opened by right-click or long-press on an area. Same items as DropdownMenu. */
38
+ export const ContextMenu = MenuPrimitive.Root
39
+ export const ContextMenuTrigger = MenuPrimitive.Trigger
40
+ export const ContextMenuGroup = MenuPrimitive.Group
41
+ export const ContextMenuRadioGroup = MenuPrimitive.RadioGroup
42
+ export const ContextMenuSub = MenuPrimitive.Sub
43
+
44
+ export function ContextMenuContent(inProps: ComponentProps<typeof MenuPrimitive.Content>) {
45
+ const [props, slots] = useComponentConfig('ContextMenu', inProps)
46
+ const { className, ...rest } = props
47
+ const s = contextMenuStyles()
48
+ return (
49
+ <MenuPrimitive.Portal>
50
+ <MenuPrimitive.Content
51
+ data-slot="context-menu-content"
52
+ className={s.content({ class: [slots.content, className] })}
53
+ {...rest}
54
+ />
55
+ </MenuPrimitive.Portal>
56
+ )
57
+ }
58
+
59
+ export interface ContextMenuItemProps extends ComponentProps<typeof MenuPrimitive.Item> {
60
+ icon?: ReactNode
61
+ /** Keyboard hint shown at the end, e.g. "⌘C". Display only. */
62
+ shortcut?: ReactNode
63
+ /** `danger` for destructive actions. */
64
+ tone?: 'default' | 'danger'
65
+ /** Indent to line up with checkbox and radio items. */
66
+ inset?: boolean
67
+ }
68
+
69
+ export function ContextMenuItem({
70
+ icon,
71
+ shortcut,
72
+ tone,
73
+ inset,
74
+ className,
75
+ children,
76
+ ...props
77
+ }: ContextMenuItemProps) {
78
+ const { s, slots } = useParts(tone, inset)
79
+ return (
80
+ <MenuPrimitive.Item
81
+ data-slot="context-menu-item"
82
+ data-tone={tone ?? 'default'}
83
+ className={s.item({ class: [slots.item, className] })}
84
+ {...props}
85
+ >
86
+ {icon && (
87
+ <span className={s.icon({ class: slots.icon })} aria-hidden="true">
88
+ {icon}
89
+ </span>
90
+ )}
91
+ {children}
92
+ {shortcut && <ContextMenuShortcut>{shortcut}</ContextMenuShortcut>}
93
+ </MenuPrimitive.Item>
94
+ )
95
+ }
96
+
97
+ export function ContextMenuCheckboxItem({
98
+ className,
99
+ children,
100
+ ...props
101
+ }: ComponentProps<typeof MenuPrimitive.CheckboxItem>) {
102
+ const { s, slots } = useParts(undefined, true)
103
+ return (
104
+ <MenuPrimitive.CheckboxItem
105
+ data-slot="context-menu-checkbox-item"
106
+ className={s.item({ class: [slots.item, className] })}
107
+ {...props}
108
+ >
109
+ <span className={s.indicator({ class: slots.indicator })}>
110
+ <MenuPrimitive.ItemIndicator>
111
+ <CheckIcon />
112
+ </MenuPrimitive.ItemIndicator>
113
+ </span>
114
+ {children}
115
+ </MenuPrimitive.CheckboxItem>
116
+ )
117
+ }
118
+
119
+ export function ContextMenuRadioItem({
120
+ className,
121
+ children,
122
+ ...props
123
+ }: ComponentProps<typeof MenuPrimitive.RadioItem>) {
124
+ const { s, slots } = useParts(undefined, true)
125
+ return (
126
+ <MenuPrimitive.RadioItem
127
+ data-slot="context-menu-radio-item"
128
+ className={s.item({ class: [slots.item, className] })}
129
+ {...props}
130
+ >
131
+ <span className={s.indicator({ class: slots.indicator })}>
132
+ <MenuPrimitive.ItemIndicator>
133
+ <DotIcon />
134
+ </MenuPrimitive.ItemIndicator>
135
+ </span>
136
+ {children}
137
+ </MenuPrimitive.RadioItem>
138
+ )
139
+ }
140
+
141
+ export function ContextMenuLabel({
142
+ inset,
143
+ className,
144
+ ...props
145
+ }: ComponentProps<typeof MenuPrimitive.Label> & { inset?: boolean }) {
146
+ const { s, slots } = useParts(undefined, inset)
147
+ return (
148
+ <MenuPrimitive.Label
149
+ data-slot="context-menu-label"
150
+ className={s.label({ class: [slots.label, className] })}
151
+ {...props}
152
+ />
153
+ )
154
+ }
155
+
156
+ export function ContextMenuSeparator({
157
+ className,
158
+ ...props
159
+ }: ComponentProps<typeof MenuPrimitive.Separator>) {
160
+ const { s, slots } = useParts()
161
+ return (
162
+ <MenuPrimitive.Separator
163
+ data-slot="context-menu-separator"
164
+ className={s.separator({ class: [slots.separator, className] })}
165
+ {...props}
166
+ />
167
+ )
168
+ }
169
+
170
+ export function ContextMenuShortcut({ className, ...props }: ComponentProps<'span'>) {
171
+ const { s, slots } = useParts()
172
+ return (
173
+ <span
174
+ data-slot="context-menu-shortcut"
175
+ className={s.shortcut({ class: [slots.shortcut, className] })}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+
181
+ export interface ContextMenuSubTriggerProps
182
+ extends ComponentProps<typeof MenuPrimitive.SubTrigger> {
183
+ icon?: ReactNode
184
+ }
185
+
186
+ export function ContextMenuSubTrigger({
187
+ icon,
188
+ className,
189
+ children,
190
+ ...props
191
+ }: ContextMenuSubTriggerProps) {
192
+ const { s, slots } = useParts()
193
+ return (
194
+ <MenuPrimitive.SubTrigger
195
+ data-slot="context-menu-sub-trigger"
196
+ className={s.subTrigger({ class: [slots.subTrigger, className] })}
197
+ {...props}
198
+ >
199
+ {icon && (
200
+ <span className={s.icon({ class: slots.icon })} aria-hidden="true">
201
+ {icon}
202
+ </span>
203
+ )}
204
+ {children}
205
+ <span className="ml-auto flex text-faint-foreground">
206
+ <ChevronRightIcon />
207
+ </span>
208
+ </MenuPrimitive.SubTrigger>
209
+ )
210
+ }
211
+
212
+ export function ContextMenuSubContent({
213
+ className,
214
+ ...props
215
+ }: ComponentProps<typeof MenuPrimitive.SubContent>) {
216
+ const { s, slots } = useParts()
217
+ return (
218
+ <MenuPrimitive.Portal>
219
+ <MenuPrimitive.SubContent
220
+ data-slot="context-menu-sub-content"
221
+ className={s.subContent({ class: [slots.subContent, className] })}
222
+ {...props}
223
+ />
224
+ </MenuPrimitive.Portal>
225
+ )
226
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ ContextMenu,
3
+ ContextMenuCheckboxItem,
4
+ ContextMenuContent,
5
+ ContextMenuGroup,
6
+ ContextMenuItem,
7
+ type ContextMenuItemProps,
8
+ ContextMenuLabel,
9
+ ContextMenuRadioGroup,
10
+ ContextMenuRadioItem,
11
+ ContextMenuSeparator,
12
+ ContextMenuShortcut,
13
+ ContextMenuSub,
14
+ ContextMenuSubContent,
15
+ ContextMenuSubTrigger,
16
+ type ContextMenuSubTriggerProps,
17
+ ContextMenuTrigger,
18
+ } from './context-menu'
19
+ export { type ContextMenuVariants, contextMenuStyles } from './context-menu.styles'