@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.
- package/README.md +65 -12
- package/dist/chart.d.ts +202 -0
- package/dist/chart.js +466 -0
- package/dist/color-mode-script-Bf7LYkOj.js +13 -0
- package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
- package/dist/data-table.js +3 -2
- package/dist/{field-CCecIzdq.js → field-CVbywksE.js} +21 -5
- package/dist/{field-context-B6WOcK5m.js → field-context-7I-cWvvS.js} +3 -57
- package/dist/form.d.ts +11 -4
- package/dist/form.js +34 -8
- package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
- package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
- package/dist/index.d.ts +2334 -283
- package/dist/index.js +2952 -326
- package/dist/react-router.d.ts +18 -0
- package/dist/react-router.js +44 -0
- package/dist/{states-Rp8wZ_jH.js → states-IvhjBgna.js} +2 -1
- package/dist/tv-DLbT66dC.js +70 -0
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +29 -0
- package/fonts.css +10 -0
- package/package.json +47 -5
- package/src/components/accordion/accordion.styles.ts +4 -4
- package/src/components/badge/badge.styles.ts +11 -16
- package/src/components/banner/banner.styles.ts +4 -4
- package/src/components/button/button.styles.ts +3 -1
- package/src/components/calendar/calendar.styles.ts +51 -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/collapsible/collapsible.tsx +66 -0
- package/src/components/collapsible/index.ts +6 -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/dropdown-menu/dropdown-menu.styles.ts +2 -2
- package/src/components/eyebrow/eyebrow.styles.ts +1 -1
- package/src/components/field/field-context.ts +2 -0
- package/src/components/field/field.styles.ts +4 -2
- package/src/components/field/field.tsx +25 -4
- 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/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/link.styles.ts +1 -1
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/modal.styles.ts +20 -0
- package/src/components/modal/modal.tsx +111 -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 +208 -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/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/prose/prose.tsx +1 -1
- 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/scroll-area.tsx +23 -1
- package/src/components/segmented-control/segmented-control.tsx +17 -2
- package/src/components/sheet/sheet.styles.ts +7 -7
- package/src/components/slider/slider.tsx +9 -2
- 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/text/text.styles.ts +4 -4
- 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/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 +20 -0
- package/src/integrations/react-router.tsx +39 -0
- package/src/integrations/vite.test-d.ts +6 -0
- package/src/integrations/vite.test.ts +28 -0
- package/src/integrations/vite.ts +33 -0
- package/src/internal/dev.ts +12 -0
- package/src/patterns/app-shell/app-shell.styles.ts +1 -1
- package/src/patterns/app-shell/navbar.tsx +34 -16
- package/src/patterns/app-shell/sidebar.tsx +7 -2
- package/src/patterns/chart/chart-parts.tsx +179 -0
- package/src/patterns/chart/chart.styles.ts +35 -0
- package/src/patterns/chart/chart.tsx +386 -0
- package/src/patterns/chart/index.ts +13 -0
- package/src/patterns/form/form.tsx +48 -6
- package/src/patterns/form/index.ts +1 -0
- package/src/provider/color-mode-script.ts +28 -0
- package/src/provider/color-mode.tsx +14 -20
- package/src/provider/glyph-context.ts +38 -0
- package/src/provider/glyph-provider.tsx +16 -4
- package/src/provider/index.ts +4 -0
- package/src/provider/theme-scope.tsx +7 -3
- 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,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'
|