@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,84 @@
|
|
|
1
|
+
import { DayPicker, type DayPickerProps } from 'react-day-picker'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { calendarStyles } from './calendar.styles'
|
|
4
|
+
|
|
5
|
+
export type CalendarProps = DayPickerProps
|
|
6
|
+
|
|
7
|
+
function ChevronIcon({
|
|
8
|
+
className,
|
|
9
|
+
orientation = 'left',
|
|
10
|
+
}: {
|
|
11
|
+
className?: string
|
|
12
|
+
orientation?: 'up' | 'down' | 'left' | 'right'
|
|
13
|
+
}) {
|
|
14
|
+
const d = {
|
|
15
|
+
left: 'm15 18-6-6 6-6',
|
|
16
|
+
right: 'm9 18 6-6-6-6',
|
|
17
|
+
up: 'm18 15-6-6-6 6',
|
|
18
|
+
down: 'm6 9 6 6 6-6',
|
|
19
|
+
}[orientation]
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
strokeWidth="2"
|
|
26
|
+
strokeLinecap="round"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
className={className}
|
|
30
|
+
>
|
|
31
|
+
<path d={d} />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A month grid for picking dates, built on react-day-picker and styled by the theme.
|
|
38
|
+
* Supports `mode="single" | "multiple" | "range"`, disabled dates and several months.
|
|
39
|
+
*/
|
|
40
|
+
export function Calendar(inProps: CalendarProps) {
|
|
41
|
+
const [props, slots] = useComponentConfig('Calendar', inProps)
|
|
42
|
+
const { className, classNames, components, showOutsideDays = true, ...rest } = props
|
|
43
|
+
const s = calendarStyles()
|
|
44
|
+
return (
|
|
45
|
+
<DayPicker
|
|
46
|
+
showOutsideDays={showOutsideDays}
|
|
47
|
+
className={s.root({ class: [slots.root, className] })}
|
|
48
|
+
classNames={{
|
|
49
|
+
months: s.months({ class: slots.months }),
|
|
50
|
+
month: s.month({ class: slots.month }),
|
|
51
|
+
month_caption: s.monthCaption({ class: slots.monthCaption }),
|
|
52
|
+
caption_label: s.captionLabel({ class: slots.captionLabel }),
|
|
53
|
+
nav: s.nav({ class: slots.nav }),
|
|
54
|
+
button_previous: s.navButton({ class: slots.navButton }),
|
|
55
|
+
button_next: s.navButton({ class: slots.navButton }),
|
|
56
|
+
chevron: s.chevron({ class: slots.chevron }),
|
|
57
|
+
month_grid: s.monthGrid({ class: slots.monthGrid }),
|
|
58
|
+
weekdays: s.weekdays({ class: slots.weekdays }),
|
|
59
|
+
weekday: s.weekday({ class: slots.weekday }),
|
|
60
|
+
weeks: s.weeks({ class: slots.weeks }),
|
|
61
|
+
week: s.week({ class: slots.week }),
|
|
62
|
+
day: s.day({ class: slots.day }),
|
|
63
|
+
day_button: s.dayButton({ class: slots.dayButton }),
|
|
64
|
+
today: s.today({ class: slots.today }),
|
|
65
|
+
selected: s.selected({ class: slots.selected }),
|
|
66
|
+
outside: s.outside({ class: slots.outside }),
|
|
67
|
+
disabled: s.disabled({ class: slots.disabled }),
|
|
68
|
+
range_start: s.rangeStart({ class: slots.rangeStart }),
|
|
69
|
+
range_middle: s.rangeMiddle({ class: slots.rangeMiddle }),
|
|
70
|
+
range_end: s.rangeEnd({ class: slots.rangeEnd }),
|
|
71
|
+
hidden: s.hidden({ class: slots.hidden }),
|
|
72
|
+
footer: s.footer({ class: slots.footer }),
|
|
73
|
+
...classNames,
|
|
74
|
+
}}
|
|
75
|
+
components={{
|
|
76
|
+
Chevron: ({ className: chevronClass, orientation }) => (
|
|
77
|
+
<ChevronIcon className={chevronClass} orientation={orientation} />
|
|
78
|
+
),
|
|
79
|
+
...components,
|
|
80
|
+
}}
|
|
81
|
+
{...(rest as DayPickerProps)}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing, transition } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const carouselStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'relative',
|
|
8
|
+
viewport: 'overflow-hidden rounded-lg',
|
|
9
|
+
container: 'flex',
|
|
10
|
+
item: 'min-w-0 shrink-0 grow-0 basis-full',
|
|
11
|
+
control: 'absolute z-10 border-border bg-surface/85 shadow-sm backdrop-blur disabled:opacity-0',
|
|
12
|
+
dots: 'mt-3 flex items-center justify-center gap-1.5',
|
|
13
|
+
dot: [
|
|
14
|
+
'h-2 w-2 cursor-pointer rounded-pill bg-border-strong',
|
|
15
|
+
transition,
|
|
16
|
+
focusRing,
|
|
17
|
+
'hover:bg-muted-foreground aria-[current=true]:w-5 aria-[current=true]:bg-primary',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
variants: {
|
|
21
|
+
orientation: {
|
|
22
|
+
horizontal: {
|
|
23
|
+
container: '-ml-4',
|
|
24
|
+
item: 'pl-4',
|
|
25
|
+
control: 'top-1/2 -translate-y-1/2',
|
|
26
|
+
},
|
|
27
|
+
vertical: {
|
|
28
|
+
container: '-mt-4 h-full flex-col',
|
|
29
|
+
item: 'pt-4',
|
|
30
|
+
control: 'left-1/2 -translate-x-1/2 [&_svg]:rotate-90',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: { orientation: 'horizontal' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export type CarouselVariants = VariantProps<typeof carouselStyles>
|
|
38
|
+
|
|
39
|
+
declare module '@glyphui/tokens' {
|
|
40
|
+
interface GlyphComponentRegistry {
|
|
41
|
+
Carousel: StylesEntry<typeof carouselStyles>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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,33 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { themedBorder } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const codeStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
base: 'rounded-xs bg-muted px-[0.3em] py-[0.15em] font-mono text-[0.875em] text-foreground',
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const codeBlockStyles = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
root: ['relative min-w-0 overflow-hidden rounded-lg border-border bg-subtle', themedBorder],
|
|
14
|
+
header:
|
|
15
|
+
'flex min-h-10 items-center justify-between gap-2 border-b border-border py-1 pr-1.5 pl-4',
|
|
16
|
+
title: 'truncate font-mono text-caption text-faint-foreground',
|
|
17
|
+
copy: 'shrink-0',
|
|
18
|
+
floatingCopy: 'absolute top-2 right-2',
|
|
19
|
+
pre: 'overflow-x-auto p-4 font-mono text-small leading-relaxed text-foreground',
|
|
20
|
+
},
|
|
21
|
+
variants: {
|
|
22
|
+
wrap: { true: { pre: 'whitespace-pre-wrap break-words' } },
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type CodeBlockVariants = VariantProps<typeof codeBlockStyles>
|
|
27
|
+
|
|
28
|
+
declare module '@glyphui/tokens' {
|
|
29
|
+
interface GlyphComponentRegistry {
|
|
30
|
+
Code: StylesEntry<typeof codeStyles>
|
|
31
|
+
CodeBlock: StylesEntry<typeof codeBlockStyles>
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { CheckIcon } from '../../internal/icons'
|
|
4
|
+
import { IconButton } from '../button'
|
|
5
|
+
import { type CodeBlockVariants, codeBlockStyles, codeStyles } from './code.styles'
|
|
6
|
+
|
|
7
|
+
/** Inline code within running text. */
|
|
8
|
+
export function Code(inProps: ComponentProps<'code'>) {
|
|
9
|
+
const [{ className, ...rest }, slots] = useComponentConfig('Code', inProps)
|
|
10
|
+
return (
|
|
11
|
+
<code
|
|
12
|
+
data-slot="code"
|
|
13
|
+
className={codeStyles().base({ class: [slots.base, className] })}
|
|
14
|
+
{...rest}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CopyIcon = () => (
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeWidth="2"
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
>
|
|
29
|
+
<rect width="14" height="14" x="8" y="8" rx="2" />
|
|
30
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
31
|
+
</svg>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export interface CodeBlockProps
|
|
35
|
+
extends Omit<ComponentProps<'figure'>, 'children' | 'title'>,
|
|
36
|
+
CodeBlockVariants {
|
|
37
|
+
/** The code to show and copy. */
|
|
38
|
+
code: string
|
|
39
|
+
/** File name or caption shown in the header. */
|
|
40
|
+
title?: ReactNode
|
|
41
|
+
/** Shown in the header when there is no title, e.g. "tsx". */
|
|
42
|
+
language?: string
|
|
43
|
+
/** Show a copy button. Default true. */
|
|
44
|
+
copyable?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A block of code with an optional file name and a copy button. No syntax highlighting is applied. */
|
|
48
|
+
export function CodeBlock(inProps: CodeBlockProps) {
|
|
49
|
+
const [props, slots] = useComponentConfig('CodeBlock', inProps)
|
|
50
|
+
const { code, title, language, copyable = true, wrap, className, ...rest } = props
|
|
51
|
+
const [copied, setCopied] = useState(false)
|
|
52
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
53
|
+
const s = codeBlockStyles({ wrap })
|
|
54
|
+
const heading = title ?? language
|
|
55
|
+
|
|
56
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
57
|
+
|
|
58
|
+
const copy = async () => {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(code)
|
|
61
|
+
setCopied(true)
|
|
62
|
+
clearTimeout(timer.current)
|
|
63
|
+
timer.current = setTimeout(() => setCopied(false), 1600)
|
|
64
|
+
} catch {
|
|
65
|
+
// Clipboard blocked (permissions, insecure context): the code stays selectable.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const copyButton = copyable ? (
|
|
70
|
+
<IconButton
|
|
71
|
+
size="sm"
|
|
72
|
+
icon={copied ? <CheckIcon /> : <CopyIcon />}
|
|
73
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
74
|
+
onClick={copy}
|
|
75
|
+
className={
|
|
76
|
+
heading ? s.copy({ class: slots.copy }) : s.floatingCopy({ class: slots.floatingCopy })
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
) : null
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<figure data-slot="code-block" className={s.root({ class: [slots.root, className] })} {...rest}>
|
|
83
|
+
{heading ? (
|
|
84
|
+
<figcaption className={s.header({ class: slots.header })}>
|
|
85
|
+
<span className={s.title({ class: slots.title })}>{heading}</span>
|
|
86
|
+
{copyButton}
|
|
87
|
+
</figcaption>
|
|
88
|
+
) : (
|
|
89
|
+
copyButton
|
|
90
|
+
)}
|
|
91
|
+
<pre className={s.pre({ class: slots.pre })}>
|
|
92
|
+
<code>{code}</code>
|
|
93
|
+
</pre>
|
|
94
|
+
</figure>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -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,50 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { popover } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
import { fieldControl } from '../input/input.styles'
|
|
5
|
+
|
|
6
|
+
export const comboboxStyles = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
trigger: [
|
|
9
|
+
...fieldControl,
|
|
10
|
+
'flex cursor-pointer items-center justify-between gap-2 px-3 text-left outline-none',
|
|
11
|
+
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20',
|
|
12
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
|
+
],
|
|
14
|
+
value: 'flex min-w-0 flex-1 items-center gap-1 truncate',
|
|
15
|
+
placeholder: 'truncate text-faint-foreground',
|
|
16
|
+
chip: 'inline-flex h-5.5 max-w-40 shrink-0 items-center truncate rounded-pill bg-muted px-2 text-caption text-foreground',
|
|
17
|
+
icon: 'size-4 shrink-0 text-faint-foreground',
|
|
18
|
+
content: [popover, 'w-(--radix-popover-trigger-width) min-w-56 p-0'],
|
|
19
|
+
search:
|
|
20
|
+
'flex items-center gap-2 border-b border-border px-3 text-faint-foreground [&>svg]:size-4 [&>svg]:shrink-0',
|
|
21
|
+
input:
|
|
22
|
+
'h-10 w-full min-w-0 bg-transparent text-small text-foreground outline-none placeholder:text-faint-foreground',
|
|
23
|
+
list: 'max-h-72 overflow-y-auto overscroll-contain p-1',
|
|
24
|
+
empty: 'px-3 py-6 text-center text-small text-muted-foreground',
|
|
25
|
+
item: [
|
|
26
|
+
'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.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
|
+
itemText: 'grid min-w-0 flex-1',
|
|
30
|
+
itemDescription: 'truncate text-caption text-faint-foreground',
|
|
31
|
+
check: 'size-4 shrink-0 text-primary',
|
|
32
|
+
},
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
sm: { trigger: 'min-h-(--glyph-control-sm) text-small' },
|
|
36
|
+
md: { trigger: 'min-h-(--glyph-control-md) text-small' },
|
|
37
|
+
lg: { trigger: 'min-h-(--glyph-control-lg) text-body' },
|
|
38
|
+
},
|
|
39
|
+
multiple: { true: { trigger: 'py-1', value: 'flex-wrap overflow-visible whitespace-normal' } },
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { size: 'md' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type ComboboxVariants = VariantProps<typeof comboboxStyles>
|
|
45
|
+
|
|
46
|
+
declare module '@glyphui/tokens' {
|
|
47
|
+
interface GlyphComponentRegistry {
|
|
48
|
+
Combobox: StylesEntry<typeof comboboxStyles>
|
|
49
|
+
}
|
|
50
|
+
}
|