@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,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,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,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
|
+
}
|