@glyphui/ui 0.1.1 → 0.2.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/data-table.js +2 -2
- package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
- package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
- package/dist/form.js +2 -2
- package/dist/index.d.ts +2743 -93
- package/dist/index.js +2748 -111
- package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
- package/package.json +4 -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/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/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/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/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -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/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/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/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/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/index.ts +25 -0
- package/src/provider/glyph-provider.tsx +16 -2
|
@@ -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 @@
|
|
|
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: 'fixed inset-0 z-50 animate-fade-in bg-overlay',
|
|
8
|
+
content: [
|
|
9
|
+
'fixed z-50 flex flex-col gap-5 border-border bg-surface p-6 text-foreground shadow-lg outline-none',
|
|
10
|
+
'transition-[translate] duration-(--glyph-duration-slow) ease-standard',
|
|
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
|
+
// Slide in from the edge using @starting-style (Tailwind's `starting:` variant).
|
|
25
|
+
side: {
|
|
26
|
+
right: {
|
|
27
|
+
content:
|
|
28
|
+
'inset-y-0 right-0 h-full w-full border-l-(length:--glyph-border-width) starting:translate-x-full',
|
|
29
|
+
},
|
|
30
|
+
left: {
|
|
31
|
+
content:
|
|
32
|
+
'inset-y-0 left-0 h-full w-full border-r-(length:--glyph-border-width) starting:-translate-x-full',
|
|
33
|
+
},
|
|
34
|
+
top: {
|
|
35
|
+
content:
|
|
36
|
+
'inset-x-0 top-0 max-h-[85dvh] border-b-(length:--glyph-border-width) starting:-translate-y-full',
|
|
37
|
+
},
|
|
38
|
+
bottom: {
|
|
39
|
+
content:
|
|
40
|
+
'inset-x-0 bottom-0 max-h-[85dvh] rounded-t-xl border-t-(length:--glyph-border-width) starting:translate-y-full',
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
|
|
4
|
+
export const statCardStyles = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
root: 'gap-2',
|
|
7
|
+
header: 'flex items-center justify-between gap-2',
|
|
8
|
+
label: 'text-small text-muted-foreground',
|
|
9
|
+
icon: 'flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&>svg]:size-4',
|
|
10
|
+
valueRow: 'flex flex-wrap items-baseline gap-x-2 gap-y-1',
|
|
11
|
+
value: 'font-display text-h2 tabular-nums text-foreground',
|
|
12
|
+
delta: 'gap-1 [&>svg]:size-3',
|
|
13
|
+
description: 'text-small text-faint-foreground',
|
|
14
|
+
chart: 'mt-1 min-w-0 text-primary',
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export type StatCardVariants = VariantProps<typeof statCardStyles>
|
|
19
|
+
|
|
20
|
+
declare module '@glyphui/tokens' {
|
|
21
|
+
interface GlyphComponentRegistry {
|
|
22
|
+
StatCard: StylesEntry<typeof statCardStyles>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { Badge, type BadgeProps } from '../badge'
|
|
4
|
+
import { Card, type CardProps } from '../card'
|
|
5
|
+
import { statCardStyles } from './stat-card.styles'
|
|
6
|
+
|
|
7
|
+
type Trend = 'up' | 'down' | 'neutral'
|
|
8
|
+
|
|
9
|
+
export interface StatCardProps extends Omit<CardProps, 'children'> {
|
|
10
|
+
label: ReactNode
|
|
11
|
+
value: ReactNode
|
|
12
|
+
/** Change versus the previous period, e.g. "+12.4%". */
|
|
13
|
+
delta?: ReactNode
|
|
14
|
+
/** Direction of the change. Colors the delta: up is good unless `invertTrend` is set. */
|
|
15
|
+
trend?: Trend
|
|
16
|
+
/** For metrics where going down is good (churn, latency, cost). */
|
|
17
|
+
invertTrend?: boolean
|
|
18
|
+
icon?: ReactNode
|
|
19
|
+
description?: ReactNode
|
|
20
|
+
/** A sparkline or small chart under the value. Colored with the primary color by default. */
|
|
21
|
+
chart?: ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ArrowUp = () => (
|
|
25
|
+
<svg
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="2.5"
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
<path d="M7 17 17 7M8 7h9v9" />
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
const ArrowDown = () => (
|
|
38
|
+
<svg
|
|
39
|
+
viewBox="0 0 24 24"
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
strokeWidth="2.5"
|
|
43
|
+
strokeLinecap="round"
|
|
44
|
+
strokeLinejoin="round"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
<path d="M7 7l10 10M17 8v9H8" />
|
|
48
|
+
</svg>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
function deltaTone(trend: Trend, invert: boolean): BadgeProps['tone'] {
|
|
52
|
+
if (trend === 'neutral') return 'neutral'
|
|
53
|
+
const good = (trend === 'up') !== invert
|
|
54
|
+
return good ? 'success' : 'danger'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A key metric with its label, change versus the last period, and an optional chart. */
|
|
58
|
+
export function StatCard(inProps: StatCardProps) {
|
|
59
|
+
const [props, slots] = useComponentConfig('StatCard', inProps)
|
|
60
|
+
const {
|
|
61
|
+
label,
|
|
62
|
+
value,
|
|
63
|
+
delta,
|
|
64
|
+
trend = 'neutral',
|
|
65
|
+
invertTrend = false,
|
|
66
|
+
icon,
|
|
67
|
+
description,
|
|
68
|
+
chart,
|
|
69
|
+
className,
|
|
70
|
+
...cardProps
|
|
71
|
+
} = props
|
|
72
|
+
const s = statCardStyles()
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Card
|
|
76
|
+
data-slot="stat-card"
|
|
77
|
+
className={s.root({ class: [slots.root, className] })}
|
|
78
|
+
{...cardProps}
|
|
79
|
+
>
|
|
80
|
+
<div className={s.header({ class: slots.header })}>
|
|
81
|
+
<p className={s.label({ class: slots.label })}>{label}</p>
|
|
82
|
+
{icon && (
|
|
83
|
+
<span className={s.icon({ class: slots.icon })} aria-hidden="true">
|
|
84
|
+
{icon}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
<div className={s.valueRow({ class: slots.valueRow })}>
|
|
89
|
+
<p data-slot="stat-card-value" className={s.value({ class: slots.value })}>
|
|
90
|
+
{value}
|
|
91
|
+
</p>
|
|
92
|
+
{delta !== undefined && (
|
|
93
|
+
<Badge
|
|
94
|
+
data-trend={trend}
|
|
95
|
+
tone={deltaTone(trend, invertTrend)}
|
|
96
|
+
className={s.delta({ class: slots.delta })}
|
|
97
|
+
>
|
|
98
|
+
{trend === 'up' && <ArrowUp />}
|
|
99
|
+
{trend === 'down' && <ArrowDown />}
|
|
100
|
+
{delta}
|
|
101
|
+
</Badge>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
{description && <p className={s.description({ class: slots.description })}>{description}</p>}
|
|
105
|
+
{chart && <div className={s.chart({ class: slots.chart })}>{chart}</div>}
|
|
106
|
+
</Card>
|
|
107
|
+
)
|
|
108
|
+
}
|