@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.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +457 -0
  4. package/dist/data-table.js +3 -2
  5. package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
  6. package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
  7. package/dist/form.js +3 -2
  8. package/dist/index.d.ts +4960 -266
  9. package/dist/index.js +5336 -146
  10. package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
  11. package/dist/tv-DBAGlgXJ.js +58 -0
  12. package/package.json +19 -2
  13. package/src/components/accordion/accordion.styles.ts +41 -0
  14. package/src/components/accordion/accordion.tsx +86 -0
  15. package/src/components/accordion/index.ts +8 -0
  16. package/src/components/alert/alert-icons.tsx +66 -0
  17. package/src/components/alert/alert.styles.ts +57 -0
  18. package/src/components/alert/alert.tsx +86 -0
  19. package/src/components/alert/index.ts +2 -0
  20. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  21. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  22. package/src/components/alert-dialog/confirm.tsx +111 -0
  23. package/src/components/alert-dialog/index.ts +15 -0
  24. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  25. package/src/components/aspect-ratio/index.ts +1 -0
  26. package/src/components/badge/badge.styles.ts +1 -1
  27. package/src/components/banner/banner.styles.ts +40 -0
  28. package/src/components/banner/banner.tsx +101 -0
  29. package/src/components/banner/index.ts +2 -0
  30. package/src/components/blockquote/blockquote.tsx +59 -0
  31. package/src/components/blockquote/index.ts +1 -0
  32. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  33. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  34. package/src/components/breadcrumb/index.ts +12 -0
  35. package/src/components/button/button.tsx +11 -8
  36. package/src/components/calendar/calendar.styles.ts +50 -0
  37. package/src/components/calendar/calendar.tsx +84 -0
  38. package/src/components/calendar/index.ts +3 -0
  39. package/src/components/carousel/carousel.styles.ts +43 -0
  40. package/src/components/carousel/carousel.tsx +278 -0
  41. package/src/components/carousel/index.ts +13 -0
  42. package/src/components/code/code.styles.ts +33 -0
  43. package/src/components/code/code.tsx +96 -0
  44. package/src/components/code/index.ts +2 -0
  45. package/src/components/collapsible/collapsible.tsx +66 -0
  46. package/src/components/collapsible/index.ts +6 -0
  47. package/src/components/combobox/combobox.styles.ts +50 -0
  48. package/src/components/combobox/combobox.tsx +203 -0
  49. package/src/components/combobox/index.ts +2 -0
  50. package/src/components/command-menu/command-menu.styles.ts +41 -0
  51. package/src/components/command-menu/command-menu.tsx +217 -0
  52. package/src/components/command-menu/index.ts +14 -0
  53. package/src/components/context-menu/context-menu.styles.ts +19 -0
  54. package/src/components/context-menu/context-menu.tsx +226 -0
  55. package/src/components/context-menu/index.ts +19 -0
  56. package/src/components/copy-button/copy-button.tsx +117 -0
  57. package/src/components/copy-button/index.ts +1 -0
  58. package/src/components/date-picker/date-picker.styles.ts +42 -0
  59. package/src/components/date-picker/date-picker.tsx +322 -0
  60. package/src/components/date-picker/index.ts +8 -0
  61. package/src/components/description-list/description-list.styles.ts +31 -0
  62. package/src/components/description-list/description-list.tsx +56 -0
  63. package/src/components/description-list/index.ts +7 -0
  64. package/src/components/dialog/close-icon.tsx +22 -0
  65. package/src/components/dialog/dialog.styles.ts +41 -0
  66. package/src/components/dialog/dialog.tsx +111 -0
  67. package/src/components/dialog/index.ts +13 -0
  68. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  69. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  70. package/src/components/dropdown-menu/index.ts +19 -0
  71. package/src/components/file-upload/file-upload.styles.ts +43 -0
  72. package/src/components/file-upload/file-upload.tsx +343 -0
  73. package/src/components/file-upload/index.ts +10 -0
  74. package/src/components/grid/grid.tsx +144 -0
  75. package/src/components/grid/index.ts +1 -0
  76. package/src/components/hover-card/hover-card.tsx +41 -0
  77. package/src/components/hover-card/index.ts +1 -0
  78. package/src/components/input-otp/index.ts +10 -0
  79. package/src/components/input-otp/input-otp.styles.ts +38 -0
  80. package/src/components/input-otp/input-otp.tsx +116 -0
  81. package/src/components/link/index.ts +2 -0
  82. package/src/components/link/link.styles.ts +39 -0
  83. package/src/components/link/link.tsx +62 -0
  84. package/src/components/modal/index.ts +2 -0
  85. package/src/components/modal/modal.styles.ts +18 -0
  86. package/src/components/modal/modal.tsx +86 -0
  87. package/src/components/navigation-menu/index.ts +14 -0
  88. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  89. package/src/components/navigation-menu/navigation-menu.tsx +198 -0
  90. package/src/components/number-input/index.ts +2 -0
  91. package/src/components/number-input/number-input.styles.ts +41 -0
  92. package/src/components/number-input/number-input.tsx +211 -0
  93. package/src/components/pagination/index.ts +3 -0
  94. package/src/components/pagination/pagination-range.ts +35 -0
  95. package/src/components/pagination/pagination.styles.ts +41 -0
  96. package/src/components/pagination/pagination.tsx +130 -0
  97. package/src/components/password-input/index.ts +7 -0
  98. package/src/components/password-input/password-input.styles.ts +30 -0
  99. package/src/components/password-input/password-input.tsx +127 -0
  100. package/src/components/popover/index.ts +9 -0
  101. package/src/components/popover/popover.tsx +66 -0
  102. package/src/components/progress/index.ts +7 -0
  103. package/src/components/progress/progress.styles.ts +68 -0
  104. package/src/components/progress/progress.tsx +158 -0
  105. package/src/components/prose/index.ts +1 -0
  106. package/src/components/prose/prose.tsx +82 -0
  107. package/src/components/radio-group/index.ts +7 -0
  108. package/src/components/radio-group/radio-group.styles.ts +64 -0
  109. package/src/components/radio-group/radio-group.tsx +128 -0
  110. package/src/components/resizable/index.ts +9 -0
  111. package/src/components/resizable/resizable.styles.ts +28 -0
  112. package/src/components/resizable/resizable.tsx +69 -0
  113. package/src/components/scroll-area/index.ts +1 -0
  114. package/src/components/scroll-area/scroll-area.tsx +68 -0
  115. package/src/components/sheet/index.ts +13 -0
  116. package/src/components/sheet/sheet.styles.ts +60 -0
  117. package/src/components/sheet/sheet.tsx +108 -0
  118. package/src/components/slider/index.ts +2 -0
  119. package/src/components/slider/slider.styles.ts +32 -0
  120. package/src/components/slider/slider.tsx +131 -0
  121. package/src/components/stat-card/index.ts +2 -0
  122. package/src/components/stat-card/stat-card.styles.ts +24 -0
  123. package/src/components/stat-card/stat-card.tsx +108 -0
  124. package/src/components/stepper/index.ts +2 -0
  125. package/src/components/stepper/stepper.styles.ts +74 -0
  126. package/src/components/stepper/stepper.tsx +138 -0
  127. package/src/components/tag-input/index.ts +2 -0
  128. package/src/components/tag-input/tag-input.styles.ts +39 -0
  129. package/src/components/tag-input/tag-input.tsx +198 -0
  130. package/src/components/timeline/index.ts +8 -0
  131. package/src/components/timeline/timeline.styles.ts +56 -0
  132. package/src/components/timeline/timeline.tsx +91 -0
  133. package/src/components/toast/index.ts +1 -0
  134. package/src/components/toast/toast.tsx +91 -0
  135. package/src/components/toggle/index.ts +9 -0
  136. package/src/components/toggle/toggle.styles.ts +39 -0
  137. package/src/components/toggle/toggle.tsx +61 -0
  138. package/src/components/tree-view/index.ts +2 -0
  139. package/src/components/tree-view/tree-view.styles.ts +42 -0
  140. package/src/components/tree-view/tree-view.tsx +274 -0
  141. package/src/components/visually-hidden/index.ts +1 -0
  142. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  143. package/src/index.ts +45 -0
  144. package/src/patterns/chart/chart-parts.tsx +171 -0
  145. package/src/patterns/chart/chart.styles.ts +35 -0
  146. package/src/patterns/chart/chart.tsx +369 -0
  147. package/src/patterns/chart/index.ts +13 -0
  148. package/src/provider/glyph-provider.tsx +16 -2
  149. package/styles.css +16 -0
@@ -0,0 +1,128 @@
1
+ import { RadioGroup as RadioGroupPrimitive } from 'radix-ui'
2
+ import { type ComponentProps, createContext, type ReactNode, useContext, useId } from 'react'
3
+ import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
4
+ import { useFieldControl } from '../field/field-context'
5
+ import { type RadioGroupVariants, radioGroupStyles } from './radio-group.styles'
6
+
7
+ type RootProps = Omit<
8
+ ComponentProps<typeof RadioGroupPrimitive.Root>,
9
+ 'value' | 'defaultValue' | 'onValueChange' | 'orientation'
10
+ >
11
+
12
+ export interface RadioGroupProps<T extends string> extends RootProps, RadioGroupVariants {
13
+ value?: T
14
+ defaultValue?: T
15
+ /** Receives your value type, not `string`. */
16
+ onValueChange?: (value: T) => void
17
+ }
18
+
19
+ interface RadioGroupContextValue {
20
+ s: ReturnType<typeof radioGroupStyles>
21
+ slots: SlotClasses<'RadioGroup'>
22
+ variant: NonNullable<RadioGroupVariants['variant']>
23
+ }
24
+
25
+ const RadioGroupContext = createContext<RadioGroupContextValue | null>(null)
26
+
27
+ function useRadioGroupContext(): RadioGroupContextValue {
28
+ return useContext(RadioGroupContext) ?? { s: radioGroupStyles(), slots: {}, variant: 'default' }
29
+ }
30
+
31
+ /**
32
+ * Pick exactly one option from a short visible list. `variant="card"` turns each option
33
+ * into a bordered card with a title and description.
34
+ */
35
+ export function RadioGroup<T extends string>(inProps: RadioGroupProps<T>) {
36
+ const [props, slots] = useComponentConfig('RadioGroup', useFieldControl(inProps))
37
+ const { variant = 'default', orientation = 'vertical', onValueChange, className, ...rest } = props
38
+ const s = radioGroupStyles({ variant, orientation })
39
+ return (
40
+ <RadioGroupContext.Provider value={{ s, slots, variant }}>
41
+ <RadioGroupPrimitive.Root
42
+ data-slot="radio-group"
43
+ data-variant={variant}
44
+ orientation={orientation}
45
+ {...(onValueChange && { onValueChange: (value: string) => onValueChange(value as T) })}
46
+ className={s.root({ class: [slots.root, className] })}
47
+ {...rest}
48
+ />
49
+ </RadioGroupContext.Provider>
50
+ )
51
+ }
52
+
53
+ export interface RadioGroupItemProps
54
+ extends Omit<ComponentProps<typeof RadioGroupPrimitive.Item>, 'children'> {
55
+ /** Visible label. For card items this is the card title. */
56
+ label?: ReactNode
57
+ description?: ReactNode
58
+ /** Extra content inside a card item, below the description. */
59
+ children?: ReactNode
60
+ }
61
+
62
+ export function RadioGroupItem({
63
+ label,
64
+ description,
65
+ children,
66
+ className,
67
+ id,
68
+ ...props
69
+ }: RadioGroupItemProps) {
70
+ const { s, slots, variant } = useRadioGroupContext()
71
+ const generated = useId()
72
+ const itemId = id ?? generated
73
+ const descriptionId = description ? `${itemId}-description` : undefined
74
+
75
+ if (variant === 'card') {
76
+ return (
77
+ <RadioGroupPrimitive.Item
78
+ id={itemId}
79
+ data-slot="radio-group-card"
80
+ aria-describedby={descriptionId}
81
+ className={s.card({ class: [slots.card, className] })}
82
+ {...props}
83
+ >
84
+ <span className={s.cardControl({ class: slots.cardControl })} aria-hidden="true">
85
+ <RadioGroupPrimitive.Indicator className={s.indicator({ class: slots.indicator })} />
86
+ </span>
87
+ <span className={s.text({ class: slots.text })}>
88
+ {label && <span className={s.label({ class: slots.label })}>{label}</span>}
89
+ {description && (
90
+ <span id={descriptionId} className={s.description({ class: slots.description })}>
91
+ {description}
92
+ </span>
93
+ )}
94
+ {children}
95
+ </span>
96
+ </RadioGroupPrimitive.Item>
97
+ )
98
+ }
99
+
100
+ const control = (
101
+ <RadioGroupPrimitive.Item
102
+ id={itemId}
103
+ data-slot="radio-group-item"
104
+ aria-describedby={descriptionId}
105
+ className={s.control({ class: [slots.control, !label && className] })}
106
+ {...props}
107
+ >
108
+ <RadioGroupPrimitive.Indicator className={s.indicator({ class: slots.indicator })} />
109
+ </RadioGroupPrimitive.Item>
110
+ )
111
+ if (!label) return control
112
+
113
+ return (
114
+ <div data-slot="radio-group-option" className={s.option({ class: [slots.option, className] })}>
115
+ {control}
116
+ <span className={s.text({ class: slots.text })}>
117
+ <label htmlFor={itemId} className={s.label({ class: slots.label })}>
118
+ {label}
119
+ </label>
120
+ {description && (
121
+ <span id={descriptionId} className={s.description({ class: slots.description })}>
122
+ {description}
123
+ </span>
124
+ )}
125
+ </span>
126
+ </div>
127
+ )
128
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ ResizableHandle,
3
+ type ResizableHandleProps,
4
+ ResizablePanel,
5
+ ResizablePanelGroup,
6
+ type ResizablePanelGroupProps,
7
+ type ResizablePanelProps,
8
+ } from './resizable'
9
+ export { type ResizableVariants, resizableStyles } from './resizable.styles'
@@ -0,0 +1,28 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { tv, type VariantProps } from '../../utils/tv'
3
+
4
+ export const resizableStyles = tv({
5
+ slots: {
6
+ group: 'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
7
+ panel: 'min-w-0',
8
+ // A 1px divider with a wider invisible hit area. Highlights while hovered, focused or dragged.
9
+ handle: [
10
+ 'relative flex w-px shrink-0 items-center justify-center bg-border outline-none',
11
+ 'transition-colors duration-(--glyph-duration-fast)',
12
+ 'after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2',
13
+ 'hover:bg-border-strong data-[resize-handle-state=drag]:bg-primary',
14
+ 'focus-visible:bg-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background',
15
+ 'data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full',
16
+ 'data-[panel-group-direction=vertical]:after:inset-x-0 data-[panel-group-direction=vertical]:after:inset-y-auto data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:top-1/2 data-[panel-group-direction=vertical]:after:h-2 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2',
17
+ ],
18
+ grip: 'z-10 flex h-5 w-3 items-center justify-center rounded-xs border border-border bg-surface text-faint-foreground shadow-xs in-data-[panel-group-direction=vertical]:h-3 in-data-[panel-group-direction=vertical]:w-5 [&>svg]:size-2.5 in-data-[panel-group-direction=vertical]:[&>svg]:rotate-90',
19
+ },
20
+ })
21
+
22
+ export type ResizableVariants = VariantProps<typeof resizableStyles>
23
+
24
+ declare module '@glyphui/tokens' {
25
+ interface GlyphComponentRegistry {
26
+ Resizable: StylesEntry<typeof resizableStyles>
27
+ }
28
+ }
@@ -0,0 +1,69 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { resizableStyles } from './resizable.styles'
5
+
6
+ const s = resizableStyles()
7
+
8
+ function useSlots() {
9
+ return useComponentConfig('Resizable', {})[1]
10
+ }
11
+
12
+ export type ResizablePanelGroupProps = ComponentProps<typeof PanelGroup>
13
+
14
+ /**
15
+ * Panels separated by draggable, keyboard-accessible dividers, for editors and split views.
16
+ * Set `direction` to "horizontal" or "vertical"; pass `autoSaveId` to remember sizes.
17
+ */
18
+ export function ResizablePanelGroup({ className, ...props }: ResizablePanelGroupProps) {
19
+ const slots = useSlots()
20
+ return (
21
+ <PanelGroup
22
+ data-slot="resizable-group"
23
+ className={s.group({ class: [slots.group, className] })}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ export type ResizablePanelProps = ComponentProps<typeof Panel>
30
+
31
+ export function ResizablePanel({ className, ...props }: ResizablePanelProps) {
32
+ const slots = useSlots()
33
+ return (
34
+ <Panel
35
+ data-slot="resizable-panel"
36
+ className={s.panel({ class: [slots.panel, className] })}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ export interface ResizableHandleProps extends ComponentProps<typeof PanelResizeHandle> {
43
+ /** Show a visible grip on the divider. */
44
+ withHandle?: boolean
45
+ }
46
+
47
+ export function ResizableHandle({ withHandle, className, ...props }: ResizableHandleProps) {
48
+ const slots = useSlots()
49
+ return (
50
+ <PanelResizeHandle
51
+ data-slot="resizable-handle"
52
+ className={s.handle({ class: [slots.handle, className] })}
53
+ {...props}
54
+ >
55
+ {withHandle && (
56
+ <div className={s.grip({ class: slots.grip })} aria-hidden="true">
57
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
58
+ <circle cx="9" cy="6" r="1.5" />
59
+ <circle cx="15" cy="6" r="1.5" />
60
+ <circle cx="9" cy="12" r="1.5" />
61
+ <circle cx="15" cy="12" r="1.5" />
62
+ <circle cx="9" cy="18" r="1.5" />
63
+ <circle cx="15" cy="18" r="1.5" />
64
+ </svg>
65
+ </div>
66
+ )}
67
+ </PanelResizeHandle>
68
+ )
69
+ }
@@ -0,0 +1 @@
1
+ export { ScrollArea, type ScrollAreaProps, scrollAreaStyles } from './scroll-area'
@@ -0,0 +1,68 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } 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 { focusRing } from '../../utils/styles'
6
+ import { tv } from '../../utils/tv'
7
+
8
+ export const scrollAreaStyles = tv({
9
+ slots: {
10
+ root: 'relative overflow-hidden',
11
+ viewport: ['size-full rounded-[inherit]', focusRing],
12
+ scrollbar: [
13
+ 'flex touch-none select-none p-px transition-colors duration-(--glyph-duration-fast)',
14
+ 'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:border-l data-[orientation=vertical]:border-l-transparent',
15
+ 'data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=horizontal]:border-t-transparent',
16
+ ],
17
+ thumb: 'relative flex-1 rounded-pill bg-border-strong hover:bg-faint-foreground',
18
+ corner: 'bg-transparent',
19
+ },
20
+ })
21
+
22
+ declare module '@glyphui/tokens' {
23
+ interface GlyphComponentRegistry {
24
+ ScrollArea: StylesEntry<typeof scrollAreaStyles>
25
+ }
26
+ }
27
+
28
+ export interface ScrollAreaProps extends ComponentProps<typeof ScrollAreaPrimitive.Root> {
29
+ /** Which scrollbars to show. Default vertical. */
30
+ orientation?: 'vertical' | 'horizontal' | 'both'
31
+ viewportClassName?: string
32
+ }
33
+
34
+ /**
35
+ * A scrollable region with slim, theme-colored scrollbars. Give it a height (or width)
36
+ * with `className`, e.g. `h-72`.
37
+ */
38
+ export function ScrollArea(inProps: ScrollAreaProps) {
39
+ const [props, slots] = useComponentConfig('ScrollArea', inProps)
40
+ const { orientation = 'vertical', viewportClassName, className, children, ...rest } = props
41
+ const s = scrollAreaStyles()
42
+ const bar = (o: 'vertical' | 'horizontal') => (
43
+ <ScrollAreaPrimitive.Scrollbar
44
+ data-slot="scroll-area-scrollbar"
45
+ orientation={o}
46
+ className={s.scrollbar({ class: slots.scrollbar })}
47
+ >
48
+ <ScrollAreaPrimitive.Thumb className={s.thumb({ class: slots.thumb })} />
49
+ </ScrollAreaPrimitive.Scrollbar>
50
+ )
51
+ return (
52
+ <ScrollAreaPrimitive.Root
53
+ data-slot="scroll-area"
54
+ className={s.root({ class: [slots.root, className] })}
55
+ {...rest}
56
+ >
57
+ <ScrollAreaPrimitive.Viewport
58
+ data-slot="scroll-area-viewport"
59
+ className={s.viewport({ class: [slots.viewport, viewportClassName] })}
60
+ >
61
+ {children}
62
+ </ScrollAreaPrimitive.Viewport>
63
+ {orientation !== 'horizontal' && bar('vertical')}
64
+ {orientation !== 'vertical' && bar('horizontal')}
65
+ <ScrollAreaPrimitive.Corner className={s.corner({ class: slots.corner })} />
66
+ </ScrollAreaPrimitive.Root>
67
+ )
68
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Sheet,
3
+ SheetBody,
4
+ SheetClose,
5
+ SheetContent,
6
+ type SheetContentProps,
7
+ SheetDescription,
8
+ SheetFooter,
9
+ SheetHeader,
10
+ SheetTitle,
11
+ SheetTrigger,
12
+ } from './sheet'
13
+ export { type SheetVariants, sheetStyles } from './sheet.styles'
@@ -0,0 +1,60 @@
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 sheetStyles = tv({
6
+ slots: {
7
+ overlay:
8
+ 'fixed inset-0 z-50 bg-overlay data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out',
9
+ content: [
10
+ 'fixed z-50 flex flex-col gap-5 border-border bg-surface p-6 text-foreground shadow-lg outline-none',
11
+ ],
12
+ close: [
13
+ 'absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground [&>svg]:size-4',
14
+ transition,
15
+ focusRing,
16
+ ],
17
+ header: 'flex flex-col gap-1.5 pr-8',
18
+ title: 'font-display text-h4 text-foreground',
19
+ description: 'text-small text-muted-foreground',
20
+ body: '-mx-6 min-h-0 flex-1 overflow-y-auto px-6 text-small',
21
+ footer: 'mt-auto flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
22
+ },
23
+ variants: {
24
+ // Slides in and out from its edge; Radix keeps it mounted until the exit animation ends.
25
+ side: {
26
+ right: {
27
+ content:
28
+ 'inset-y-0 right-0 h-full w-full border-l-(length:--glyph-border-width) data-[state=open]:animate-sheet-in-right data-[state=closed]:animate-sheet-out-right',
29
+ },
30
+ left: {
31
+ content:
32
+ 'inset-y-0 left-0 h-full w-full border-r-(length:--glyph-border-width) data-[state=open]:animate-sheet-in-left data-[state=closed]:animate-sheet-out-left',
33
+ },
34
+ top: {
35
+ content:
36
+ 'inset-x-0 top-0 max-h-[85dvh] border-b-(length:--glyph-border-width) data-[state=open]:animate-sheet-in-top data-[state=closed]:animate-sheet-out-top',
37
+ },
38
+ bottom: {
39
+ content:
40
+ 'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t-(length:--glyph-border-width) data-[state=open]:animate-sheet-in-bottom data-[state=closed]:animate-sheet-out-bottom',
41
+ },
42
+ },
43
+ size: { sm: {}, md: {}, lg: {}, xl: {} },
44
+ },
45
+ compoundVariants: [
46
+ { side: ['left', 'right'], size: 'sm', class: { content: 'sm:max-w-sm' } },
47
+ { side: ['left', 'right'], size: 'md', class: { content: 'sm:max-w-md' } },
48
+ { side: ['left', 'right'], size: 'lg', class: { content: 'sm:max-w-xl' } },
49
+ { side: ['left', 'right'], size: 'xl', class: { content: 'sm:max-w-3xl' } },
50
+ ],
51
+ defaultVariants: { side: 'right', size: 'md' },
52
+ })
53
+
54
+ export type SheetVariants = VariantProps<typeof sheetStyles>
55
+
56
+ declare module '@glyphui/tokens' {
57
+ interface GlyphComponentRegistry {
58
+ Sheet: StylesEntry<typeof sheetStyles>
59
+ }
60
+ }
@@ -0,0 +1,108 @@
1
+ import { Dialog as DialogPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { CloseIcon } from '../dialog/close-icon'
5
+ import { type SheetVariants, sheetStyles } from './sheet.styles'
6
+
7
+ function useParts() {
8
+ const [, slots] = useComponentConfig('Sheet', {})
9
+ return { s: sheetStyles(), slots }
10
+ }
11
+
12
+ /** A panel that slides in from an edge: filters, details, settings, mobile navigation. */
13
+ export const Sheet = DialogPrimitive.Root
14
+ export const SheetTrigger = DialogPrimitive.Trigger
15
+ export const SheetClose = DialogPrimitive.Close
16
+
17
+ export interface SheetContentProps
18
+ extends ComponentProps<typeof DialogPrimitive.Content>,
19
+ SheetVariants {
20
+ /** Show the close button in the corner. Default true. */
21
+ showClose?: boolean
22
+ overlayClassName?: string
23
+ }
24
+
25
+ export function SheetContent(inProps: SheetContentProps) {
26
+ const [props, slots] = useComponentConfig('Sheet', inProps)
27
+ const { side, size, showClose = true, overlayClassName, className, children, ...rest } = props
28
+ const s = sheetStyles({ side, size })
29
+ return (
30
+ <DialogPrimitive.Portal>
31
+ <DialogPrimitive.Overlay
32
+ data-slot="sheet-overlay"
33
+ className={s.overlay({ class: [slots.overlay, overlayClassName] })}
34
+ />
35
+ <DialogPrimitive.Content
36
+ data-slot="sheet-content"
37
+ data-side={side ?? 'right'}
38
+ className={s.content({ class: [slots.content, className] })}
39
+ {...rest}
40
+ >
41
+ {children}
42
+ {showClose && (
43
+ <DialogPrimitive.Close
44
+ data-slot="sheet-close"
45
+ aria-label="Close"
46
+ className={s.close({ class: slots.close })}
47
+ >
48
+ <CloseIcon />
49
+ </DialogPrimitive.Close>
50
+ )}
51
+ </DialogPrimitive.Content>
52
+ </DialogPrimitive.Portal>
53
+ )
54
+ }
55
+
56
+ export function SheetHeader({ className, ...props }: ComponentProps<'div'>) {
57
+ const { s, slots } = useParts()
58
+ return (
59
+ <div
60
+ data-slot="sheet-header"
61
+ className={s.header({ class: [slots.header, className] })}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export function SheetTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {
68
+ const { s, slots } = useParts()
69
+ return (
70
+ <DialogPrimitive.Title
71
+ data-slot="sheet-title"
72
+ className={s.title({ class: [slots.title, className] })}
73
+ {...props}
74
+ />
75
+ )
76
+ }
77
+
78
+ export function SheetDescription({
79
+ className,
80
+ ...props
81
+ }: ComponentProps<typeof DialogPrimitive.Description>) {
82
+ const { s, slots } = useParts()
83
+ return (
84
+ <DialogPrimitive.Description
85
+ data-slot="sheet-description"
86
+ className={s.description({ class: [slots.description, className] })}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+
92
+ export function SheetBody({ className, ...props }: ComponentProps<'div'>) {
93
+ const { s, slots } = useParts()
94
+ return (
95
+ <div data-slot="sheet-body" className={s.body({ class: [slots.body, className] })} {...props} />
96
+ )
97
+ }
98
+
99
+ export function SheetFooter({ className, ...props }: ComponentProps<'div'>) {
100
+ const { s, slots } = useParts()
101
+ return (
102
+ <div
103
+ data-slot="sheet-footer"
104
+ className={s.footer({ class: [slots.footer, className] })}
105
+ {...props}
106
+ />
107
+ )
108
+ }
@@ -0,0 +1,2 @@
1
+ export { Slider, type SliderMark, type SliderProps, type SliderValue } from './slider'
2
+ export { type SliderVariants, sliderStyles } from './slider.styles'
@@ -0,0 +1,32 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { tv, type VariantProps } from '../../utils/tv'
3
+
4
+ export const sliderStyles = tv({
5
+ slots: {
6
+ wrapper: 'grid w-full gap-1.5',
7
+ root: 'relative flex h-5 w-full touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-50',
8
+ track: 'relative h-1.5 w-full grow overflow-hidden rounded-pill bg-muted',
9
+ range: 'absolute h-full bg-primary',
10
+ thumb: [
11
+ 'relative block size-4 cursor-grab rounded-pill border-2 border-primary bg-background shadow-sm outline-none active:cursor-grabbing',
12
+ 'transition-[box-shadow] duration-(--glyph-duration-fast) ease-standard',
13
+ 'hover:ring-4 hover:ring-ring/15 focus-visible:ring-4 focus-visible:ring-ring/30',
14
+ 'data-disabled:cursor-not-allowed',
15
+ ],
16
+ value:
17
+ 'pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 whitespace-nowrap rounded-xs bg-secondary px-1.5 py-0.5 font-mono text-caption text-secondary-foreground tabular-nums',
18
+ marks: 'relative h-4 w-full',
19
+ mark: 'absolute top-0 -translate-x-1/2 text-caption text-faint-foreground tabular-nums',
20
+ },
21
+ variants: {
22
+ showValue: { true: { wrapper: 'pt-7' } },
23
+ },
24
+ })
25
+
26
+ export type SliderVariants = VariantProps<typeof sliderStyles>
27
+
28
+ declare module '@glyphui/tokens' {
29
+ interface GlyphComponentRegistry {
30
+ Slider: StylesEntry<typeof sliderStyles>
31
+ }
32
+ }
@@ -0,0 +1,131 @@
1
+ import { Slider as SliderPrimitive } from 'radix-ui'
2
+ import type { ReactNode } from 'react'
3
+ import { useComponentConfig } from '../../hooks/use-component-config'
4
+ import { useFieldControl } from '../field/field-context'
5
+ import { sliderStyles } from './slider.styles'
6
+
7
+ export type SliderValue = number | readonly [number, number]
8
+
9
+ export interface SliderMark {
10
+ value: number
11
+ label?: ReactNode
12
+ }
13
+
14
+ export interface SliderProps<V extends SliderValue> {
15
+ /** A number for one thumb, a `[min, max]` tuple for a range. */
16
+ value?: V
17
+ defaultValue?: V
18
+ onValueChange?: (value: V) => void
19
+ /** Fires once when the user releases the thumb. Use it for expensive updates. */
20
+ onValueCommit?: (value: V) => void
21
+ min?: number
22
+ max?: number
23
+ step?: number
24
+ /** Minimum distance between range thumbs, in steps. */
25
+ minStepsBetweenThumbs?: number
26
+ /** Labels shown under the track at given values. */
27
+ marks?: readonly (number | SliderMark)[]
28
+ /** Show the current value above each thumb. Pass a function to format it. */
29
+ showValue?: boolean | ((value: number) => ReactNode)
30
+ disabled?: boolean
31
+ name?: string
32
+ id?: string
33
+ className?: string
34
+ 'aria-label'?: string
35
+ 'aria-describedby'?: string
36
+ 'aria-invalid'?: boolean
37
+ /** Accessible names for range thumbs. Default: "Minimum", "Maximum". */
38
+ thumbLabels?: readonly [string, string]
39
+ }
40
+
41
+ const toArray = (value: SliderValue | undefined): number[] | undefined =>
42
+ value === undefined ? undefined : typeof value === 'number' ? [value] : [...value]
43
+
44
+ /** Choose a number, or a range with two thumbs, by dragging or with arrow keys. */
45
+ export function Slider<V extends SliderValue = number>(inProps: SliderProps<V>) {
46
+ const [props, slots] = useComponentConfig('Slider', useFieldControl(inProps))
47
+ const {
48
+ value,
49
+ defaultValue,
50
+ onValueChange,
51
+ onValueCommit,
52
+ min = 0,
53
+ max = 100,
54
+ step = 1,
55
+ minStepsBetweenThumbs,
56
+ marks,
57
+ showValue = false,
58
+ disabled,
59
+ name,
60
+ id,
61
+ className,
62
+ thumbLabels = ['Minimum', 'Maximum'],
63
+ 'aria-label': ariaLabel,
64
+ 'aria-describedby': ariaDescribedBy,
65
+ 'aria-invalid': ariaInvalid,
66
+ } = props
67
+ const isRange = Array.isArray(value ?? defaultValue)
68
+ const fromArray = (next: number[]) => (isRange ? [next[0], next[1]] : next[0]) as V
69
+ const current = toArray(value) ?? toArray(defaultValue) ?? [min]
70
+ const s = sliderStyles({ showValue: Boolean(showValue) })
71
+ const format = typeof showValue === 'function' ? showValue : (n: number) => n
72
+
73
+ return (
74
+ <div data-slot="slider" className={s.wrapper({ class: [slots.wrapper, className] })}>
75
+ <SliderPrimitive.Root
76
+ className={s.root({ class: slots.root })}
77
+ min={min}
78
+ max={max}
79
+ step={step}
80
+ disabled={disabled}
81
+ name={name}
82
+ {...(minStepsBetweenThumbs !== undefined && { minStepsBetweenThumbs })}
83
+ {...(value !== undefined && { value: toArray(value) })}
84
+ {...(defaultValue !== undefined && { defaultValue: toArray(defaultValue) })}
85
+ {...(onValueChange && {
86
+ onValueChange: (next: number[]) => onValueChange(fromArray(next)),
87
+ })}
88
+ {...(onValueCommit && {
89
+ onValueCommit: (next: number[]) => onValueCommit(fromArray(next)),
90
+ })}
91
+ >
92
+ <SliderPrimitive.Track className={s.track({ class: slots.track })}>
93
+ <SliderPrimitive.Range className={s.range({ class: slots.range })} />
94
+ </SliderPrimitive.Track>
95
+ {current.map((thumbValue, index) => (
96
+ <SliderPrimitive.Thumb
97
+ key={index}
98
+ id={index === 0 ? id : undefined}
99
+ aria-label={isRange ? thumbLabels[index] : ariaLabel}
100
+ aria-describedby={ariaDescribedBy}
101
+ aria-invalid={ariaInvalid}
102
+ className={s.thumb({ class: slots.thumb })}
103
+ >
104
+ {showValue && (
105
+ <span className={s.value({ class: slots.value })} aria-hidden="true">
106
+ {format(thumbValue)}
107
+ </span>
108
+ )}
109
+ </SliderPrimitive.Thumb>
110
+ ))}
111
+ </SliderPrimitive.Root>
112
+ {marks && marks.length > 0 && (
113
+ <div className={s.marks({ class: slots.marks })} aria-hidden="true">
114
+ {marks.map((mark) => {
115
+ const m = typeof mark === 'number' ? { value: mark } : mark
116
+ const percent = ((m.value - min) / (max - min || 1)) * 100
117
+ return (
118
+ <span
119
+ key={m.value}
120
+ className={s.mark({ class: slots.mark })}
121
+ style={{ left: `${percent}%` }}
122
+ >
123
+ {m.label ?? m.value}
124
+ </span>
125
+ )
126
+ })}
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }
@@ -0,0 +1,2 @@
1
+ export { StatCard, type StatCardProps } from './stat-card'
2
+ export { type StatCardVariants, statCardStyles } from './stat-card.styles'