@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,30 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing } from '../../utils/styles'
|
|
3
|
+
import { tv } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const passwordInputStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'grid gap-1.5',
|
|
8
|
+
toggle: [
|
|
9
|
+
'-mr-1 inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-faint-foreground hover:bg-muted hover:text-foreground [&>svg]:size-4',
|
|
10
|
+
focusRing,
|
|
11
|
+
],
|
|
12
|
+
meter: 'flex items-center gap-2',
|
|
13
|
+
bars: 'grid flex-1 grid-cols-3 gap-1',
|
|
14
|
+
bar: 'h-1 rounded-pill bg-muted transition-colors duration-(--glyph-duration-base)',
|
|
15
|
+
label: 'w-14 text-right text-caption text-muted-foreground',
|
|
16
|
+
},
|
|
17
|
+
variants: {
|
|
18
|
+
strength: {
|
|
19
|
+
weak: { label: 'text-danger' },
|
|
20
|
+
fair: { label: 'text-warning' },
|
|
21
|
+
strong: { label: 'text-success' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
declare module '@glyphui/tokens' {
|
|
27
|
+
interface GlyphComponentRegistry {
|
|
28
|
+
PasswordInput: StylesEntry<typeof passwordInputStyles>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { Input, type InputProps } from '../input/input'
|
|
4
|
+
import { passwordInputStyles } from './password-input.styles'
|
|
5
|
+
|
|
6
|
+
export type PasswordStrength = 'weak' | 'fair' | 'strong'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A quick strength estimate from length and character variety. A hint for users,
|
|
10
|
+
* not a security check: enforce real rules on the server.
|
|
11
|
+
*/
|
|
12
|
+
export function passwordStrength(password: string): { score: number; strength: PasswordStrength } {
|
|
13
|
+
let score = 0
|
|
14
|
+
if (password.length >= 8) score++
|
|
15
|
+
if (password.length >= 12) score++
|
|
16
|
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++
|
|
17
|
+
if (/\d/.test(password)) score++
|
|
18
|
+
if (/[^A-Za-z0-9]/.test(password)) score++
|
|
19
|
+
const strength: PasswordStrength = score >= 4 ? 'strong' : score >= 3 ? 'fair' : 'weak'
|
|
20
|
+
return { score, strength }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const filled: Record<PasswordStrength, number> = { weak: 1, fair: 2, strong: 3 }
|
|
24
|
+
const barColor: Record<PasswordStrength, string> = {
|
|
25
|
+
weak: 'bg-danger',
|
|
26
|
+
fair: 'bg-warning',
|
|
27
|
+
strong: 'bg-success',
|
|
28
|
+
}
|
|
29
|
+
const labels: Record<PasswordStrength, string> = { weak: 'Weak', fair: 'Fair', strong: 'Strong' }
|
|
30
|
+
|
|
31
|
+
export interface PasswordInputProps extends Omit<InputProps, 'type' | 'suffix'> {
|
|
32
|
+
/** Show a strength meter under the field once the user starts typing. */
|
|
33
|
+
showStrength?: boolean
|
|
34
|
+
/** Start with the password visible. */
|
|
35
|
+
defaultVisible?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function EyeIcon({ off }: { off: boolean }) {
|
|
39
|
+
return (
|
|
40
|
+
<svg
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
fill="none"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="2"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
>
|
|
49
|
+
{off ? (
|
|
50
|
+
<>
|
|
51
|
+
<path d="M10.7 5.1A10.4 10.4 0 0 1 12 5c7 0 10 7 10 7a13 13 0 0 1-1.7 2.7M6.6 6.6A13.5 13.5 0 0 0 2 12s3 7 10 7a9.7 9.7 0 0 0 5.4-1.6" />
|
|
52
|
+
<path d="M9.9 9.9a3 3 0 1 0 4.2 4.2M2 2l20 20" />
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
<>
|
|
56
|
+
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12Z" />
|
|
57
|
+
<circle cx="12" cy="12" r="3" />
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</svg>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A password field with a show/hide toggle and an optional strength meter. */
|
|
65
|
+
export function PasswordInput(inProps: PasswordInputProps) {
|
|
66
|
+
const [props, slots] = useComponentConfig('PasswordInput', inProps)
|
|
67
|
+
const {
|
|
68
|
+
showStrength = false,
|
|
69
|
+
defaultVisible = false,
|
|
70
|
+
value,
|
|
71
|
+
defaultValue,
|
|
72
|
+
onChange,
|
|
73
|
+
className,
|
|
74
|
+
...rest
|
|
75
|
+
} = props
|
|
76
|
+
const [visible, setVisible] = useState(defaultVisible)
|
|
77
|
+
const [typed, setTyped] = useState(String(defaultValue ?? ''))
|
|
78
|
+
const current = value !== undefined ? String(value) : typed
|
|
79
|
+
const { strength } = passwordStrength(current)
|
|
80
|
+
const s = passwordInputStyles({ strength })
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div data-slot="password-input" className={s.root({ class: [slots.root, className] })}>
|
|
84
|
+
<Input
|
|
85
|
+
{...rest}
|
|
86
|
+
{...(value !== undefined && { value })}
|
|
87
|
+
{...(defaultValue !== undefined && { defaultValue })}
|
|
88
|
+
type={visible ? 'text' : 'password'}
|
|
89
|
+
autoComplete={rest.autoComplete ?? 'current-password'}
|
|
90
|
+
spellCheck={false}
|
|
91
|
+
onChange={(event) => {
|
|
92
|
+
setTyped(event.target.value)
|
|
93
|
+
onChange?.(event)
|
|
94
|
+
}}
|
|
95
|
+
suffix={
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
aria-label={visible ? 'Hide password' : 'Show password'}
|
|
99
|
+
aria-pressed={visible}
|
|
100
|
+
disabled={rest.disabled}
|
|
101
|
+
className={s.toggle({ class: slots.toggle })}
|
|
102
|
+
onClick={() => setVisible((v) => !v)}
|
|
103
|
+
>
|
|
104
|
+
<EyeIcon off={visible} />
|
|
105
|
+
</button>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
{showStrength && current.length > 0 && (
|
|
109
|
+
<div className={s.meter({ class: slots.meter })}>
|
|
110
|
+
<div className={s.bars({ class: slots.bars })} aria-hidden="true">
|
|
111
|
+
{[1, 2, 3].map((n) => (
|
|
112
|
+
<span
|
|
113
|
+
key={n}
|
|
114
|
+
className={s.bar({
|
|
115
|
+
class: [slots.bar, n <= filled[strength] && barColor[strength]],
|
|
116
|
+
})}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
<span className={s.label({ class: slots.label })} aria-live="polite">
|
|
121
|
+
{labels[strength]}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } 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 { popover } from '../../utils/styles'
|
|
6
|
+
import { tv } from '../../utils/tv'
|
|
7
|
+
|
|
8
|
+
export const popoverStyles = tv({
|
|
9
|
+
slots: {
|
|
10
|
+
content: [
|
|
11
|
+
popover,
|
|
12
|
+
'w-72 max-w-[calc(100vw-2rem)] overflow-visible p-4 text-small outline-none',
|
|
13
|
+
],
|
|
14
|
+
arrow: 'fill-raised',
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
declare module '@glyphui/tokens' {
|
|
19
|
+
interface GlyphComponentRegistry {
|
|
20
|
+
Popover: StylesEntry<typeof popoverStyles>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Floating content anchored to a trigger: quick settings, details, small forms. */
|
|
25
|
+
export const Popover = PopoverPrimitive.Root
|
|
26
|
+
export const PopoverTrigger = PopoverPrimitive.Trigger
|
|
27
|
+
export const PopoverAnchor = PopoverPrimitive.Anchor
|
|
28
|
+
export const PopoverClose = PopoverPrimitive.Close
|
|
29
|
+
|
|
30
|
+
export interface PopoverContentProps extends ComponentProps<typeof PopoverPrimitive.Content> {
|
|
31
|
+
/** Render a small arrow pointing at the trigger. */
|
|
32
|
+
showArrow?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PopoverContent(inProps: PopoverContentProps) {
|
|
36
|
+
const [props, slots] = useComponentConfig('Popover', inProps)
|
|
37
|
+
const {
|
|
38
|
+
showArrow = false,
|
|
39
|
+
sideOffset = 6,
|
|
40
|
+
align = 'center',
|
|
41
|
+
className,
|
|
42
|
+
children,
|
|
43
|
+
...rest
|
|
44
|
+
} = props
|
|
45
|
+
const s = popoverStyles()
|
|
46
|
+
return (
|
|
47
|
+
<PopoverPrimitive.Portal>
|
|
48
|
+
<PopoverPrimitive.Content
|
|
49
|
+
data-slot="popover-content"
|
|
50
|
+
sideOffset={sideOffset}
|
|
51
|
+
align={align}
|
|
52
|
+
className={s.content({ class: [slots.content, className] })}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
{showArrow && (
|
|
57
|
+
<PopoverPrimitive.Arrow
|
|
58
|
+
width={12}
|
|
59
|
+
height={6}
|
|
60
|
+
className={s.arrow({ class: slots.arrow })}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</PopoverPrimitive.Content>
|
|
64
|
+
</PopoverPrimitive.Portal>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
|
|
4
|
+
export const progressStyles = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
root: 'grid w-full gap-1.5',
|
|
7
|
+
header: 'flex items-baseline justify-between gap-3 text-small',
|
|
8
|
+
label: 'font-medium text-foreground',
|
|
9
|
+
value: 'tabular-nums text-muted-foreground',
|
|
10
|
+
track: 'relative w-full overflow-hidden rounded-pill bg-muted',
|
|
11
|
+
indicator: [
|
|
12
|
+
'h-full w-full rounded-pill transition-transform duration-(--glyph-duration-slow) ease-standard',
|
|
13
|
+
'data-[state=indeterminate]:w-1/3 data-[state=indeterminate]:animate-pulse',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: { track: 'h-1' },
|
|
19
|
+
md: { track: 'h-2' },
|
|
20
|
+
lg: { track: 'h-3' },
|
|
21
|
+
},
|
|
22
|
+
tone: {
|
|
23
|
+
primary: { indicator: 'bg-primary' },
|
|
24
|
+
neutral: { indicator: 'bg-foreground' },
|
|
25
|
+
success: { indicator: 'bg-success' },
|
|
26
|
+
warning: { indicator: 'bg-warning' },
|
|
27
|
+
danger: { indicator: 'bg-danger' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { size: 'md', tone: 'primary' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type ProgressVariants = VariantProps<typeof progressStyles>
|
|
34
|
+
|
|
35
|
+
export const progressCircleStyles = tv({
|
|
36
|
+
slots: {
|
|
37
|
+
root: 'relative inline-flex shrink-0 items-center justify-center',
|
|
38
|
+
svg: 'size-full -rotate-90',
|
|
39
|
+
track: 'stroke-muted',
|
|
40
|
+
indicator:
|
|
41
|
+
'stroke-current transition-[stroke-dashoffset] duration-(--glyph-duration-slow) ease-standard',
|
|
42
|
+
value: 'absolute font-medium tabular-nums text-foreground',
|
|
43
|
+
},
|
|
44
|
+
variants: {
|
|
45
|
+
size: {
|
|
46
|
+
sm: { root: 'size-6', value: 'hidden' },
|
|
47
|
+
md: { root: 'size-10', value: 'text-[0.625rem]' },
|
|
48
|
+
lg: { root: 'size-16', value: 'text-small' },
|
|
49
|
+
},
|
|
50
|
+
tone: {
|
|
51
|
+
primary: { indicator: 'text-primary' },
|
|
52
|
+
neutral: { indicator: 'text-foreground' },
|
|
53
|
+
success: { indicator: 'text-success' },
|
|
54
|
+
warning: { indicator: 'text-warning' },
|
|
55
|
+
danger: { indicator: 'text-danger' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: { size: 'md', tone: 'primary' },
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export type ProgressCircleVariants = VariantProps<typeof progressCircleStyles>
|
|
62
|
+
|
|
63
|
+
declare module '@glyphui/tokens' {
|
|
64
|
+
interface GlyphComponentRegistry {
|
|
65
|
+
Progress: StylesEntry<typeof progressStyles>
|
|
66
|
+
ProgressCircle: StylesEntry<typeof progressCircleStyles>
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Progress as ProgressPrimitive } from 'radix-ui'
|
|
2
|
+
import { type ComponentProps, type ReactNode, useId } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import {
|
|
5
|
+
type ProgressCircleVariants,
|
|
6
|
+
type ProgressVariants,
|
|
7
|
+
progressCircleStyles,
|
|
8
|
+
progressStyles,
|
|
9
|
+
} from './progress.styles'
|
|
10
|
+
|
|
11
|
+
function clampPercent(value: number, max: number): number {
|
|
12
|
+
if (max <= 0) return 0
|
|
13
|
+
return Math.min(100, Math.max(0, (value / max) * 100))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const defaultFormat = (value: number, max: number) => `${Math.round(clampPercent(value, max))}%`
|
|
17
|
+
|
|
18
|
+
export interface ProgressProps extends Omit<ComponentProps<'div'>, 'children'>, ProgressVariants {
|
|
19
|
+
/** Current value. `null` or omitted shows an indeterminate bar. */
|
|
20
|
+
value?: number | null
|
|
21
|
+
max?: number
|
|
22
|
+
/** Visible label above the bar; also its accessible name. */
|
|
23
|
+
label?: ReactNode
|
|
24
|
+
/** Show the value next to the label. */
|
|
25
|
+
showValue?: boolean
|
|
26
|
+
formatValue?: (value: number, max: number) => string
|
|
27
|
+
/** Accessible name when there is no visible label. */
|
|
28
|
+
'aria-label'?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A horizontal bar showing how far a task has progressed. */
|
|
32
|
+
export function Progress(inProps: ProgressProps) {
|
|
33
|
+
const [props, slots] = useComponentConfig('Progress', inProps)
|
|
34
|
+
const {
|
|
35
|
+
value = null,
|
|
36
|
+
max = 100,
|
|
37
|
+
label,
|
|
38
|
+
showValue = false,
|
|
39
|
+
formatValue = defaultFormat,
|
|
40
|
+
size,
|
|
41
|
+
tone,
|
|
42
|
+
className,
|
|
43
|
+
'aria-label': ariaLabel,
|
|
44
|
+
...rest
|
|
45
|
+
} = props
|
|
46
|
+
const labelId = useId()
|
|
47
|
+
const s = progressStyles({ size, tone })
|
|
48
|
+
const determinate = typeof value === 'number'
|
|
49
|
+
const percent = determinate ? clampPercent(value, max) : 0
|
|
50
|
+
const valueText = determinate ? formatValue(value, max) : undefined
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div data-slot="progress" className={s.root({ class: [slots.root, className] })} {...rest}>
|
|
54
|
+
{(label || (showValue && valueText)) && (
|
|
55
|
+
<div data-slot="progress-header" className={s.header({ class: slots.header })}>
|
|
56
|
+
{label && (
|
|
57
|
+
<span id={labelId} className={s.label({ class: slots.label })}>
|
|
58
|
+
{label}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
{showValue && valueText && (
|
|
62
|
+
<span className={s.value({ class: slots.value })} aria-hidden="true">
|
|
63
|
+
{valueText}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
<ProgressPrimitive.Root
|
|
69
|
+
data-slot="progress-track"
|
|
70
|
+
value={determinate ? Math.min(Math.max(value, 0), max) : null}
|
|
71
|
+
max={max}
|
|
72
|
+
getValueLabel={(v, m) => formatValue(v, m)}
|
|
73
|
+
aria-labelledby={label ? labelId : undefined}
|
|
74
|
+
aria-label={label ? undefined : ariaLabel}
|
|
75
|
+
className={s.track({ class: slots.track })}
|
|
76
|
+
>
|
|
77
|
+
<ProgressPrimitive.Indicator
|
|
78
|
+
data-slot="progress-indicator"
|
|
79
|
+
className={s.indicator({ class: slots.indicator })}
|
|
80
|
+
style={determinate ? { transform: `translateX(-${100 - percent}%)` } : undefined}
|
|
81
|
+
/>
|
|
82
|
+
</ProgressPrimitive.Root>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ProgressCircleProps
|
|
88
|
+
extends Omit<ComponentProps<'div'>, 'children'>,
|
|
89
|
+
ProgressCircleVariants {
|
|
90
|
+
value: number
|
|
91
|
+
max?: number
|
|
92
|
+
/** Show the percentage in the center (md and lg sizes). */
|
|
93
|
+
showValue?: boolean
|
|
94
|
+
formatValue?: (value: number, max: number) => string
|
|
95
|
+
/** Accessible name, e.g. "Storage used". */
|
|
96
|
+
'aria-label'?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const RADIUS = 16
|
|
100
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
|
|
101
|
+
|
|
102
|
+
/** A compact ring for progress or a share of a total. */
|
|
103
|
+
export function ProgressCircle(inProps: ProgressCircleProps) {
|
|
104
|
+
const [props, slots] = useComponentConfig('ProgressCircle', inProps)
|
|
105
|
+
const {
|
|
106
|
+
value,
|
|
107
|
+
max = 100,
|
|
108
|
+
showValue = false,
|
|
109
|
+
formatValue = defaultFormat,
|
|
110
|
+
size,
|
|
111
|
+
tone,
|
|
112
|
+
className,
|
|
113
|
+
...rest
|
|
114
|
+
} = props
|
|
115
|
+
const s = progressCircleStyles({ size, tone })
|
|
116
|
+
const percent = clampPercent(value, max)
|
|
117
|
+
const valueText = formatValue(value, max)
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
data-slot="progress-circle"
|
|
122
|
+
role="progressbar"
|
|
123
|
+
aria-valuemin={0}
|
|
124
|
+
aria-valuemax={max}
|
|
125
|
+
aria-valuenow={Math.min(Math.max(value, 0), max)}
|
|
126
|
+
aria-valuetext={valueText}
|
|
127
|
+
className={s.root({ class: [slots.root, className] })}
|
|
128
|
+
{...rest}
|
|
129
|
+
>
|
|
130
|
+
<svg viewBox="0 0 40 40" className={s.svg({ class: slots.svg })} aria-hidden="true">
|
|
131
|
+
<circle
|
|
132
|
+
cx="20"
|
|
133
|
+
cy="20"
|
|
134
|
+
r={RADIUS}
|
|
135
|
+
fill="none"
|
|
136
|
+
strokeWidth="4"
|
|
137
|
+
className={s.track({ class: slots.track })}
|
|
138
|
+
/>
|
|
139
|
+
<circle
|
|
140
|
+
cx="20"
|
|
141
|
+
cy="20"
|
|
142
|
+
r={RADIUS}
|
|
143
|
+
fill="none"
|
|
144
|
+
strokeWidth="4"
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeDasharray={CIRCUMFERENCE}
|
|
147
|
+
strokeDashoffset={CIRCUMFERENCE * (1 - percent / 100)}
|
|
148
|
+
className={s.indicator({ class: slots.indicator })}
|
|
149
|
+
/>
|
|
150
|
+
</svg>
|
|
151
|
+
{showValue && (
|
|
152
|
+
<span className={s.value({ class: slots.value })} aria-hidden="true">
|
|
153
|
+
{valueText}
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Prose, type ProseProps, proseStyles } from './prose'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Slot } from 'radix-ui'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
5
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Element styles for long-form content. Every value comes from theme tokens, so articles
|
|
9
|
+
* pick up the theme's display font, type scale, colors and radius.
|
|
10
|
+
*/
|
|
11
|
+
export const proseStyles = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
base: [
|
|
14
|
+
'text-foreground [&>:first-child]:mt-0 [&>:last-child]:mb-0',
|
|
15
|
+
// Headings
|
|
16
|
+
'[&_h1]:mt-[1.4em] [&_h1]:mb-[0.5em] [&_h1]:font-display [&_h1]:text-h1 [&_h1]:text-balance',
|
|
17
|
+
'[&_h2]:mt-[1.6em] [&_h2]:mb-[0.5em] [&_h2]:font-display [&_h2]:text-h2 [&_h2]:text-balance',
|
|
18
|
+
'[&_h3]:mt-[1.5em] [&_h3]:mb-[0.4em] [&_h3]:font-display [&_h3]:text-h3 [&_h3]:text-balance',
|
|
19
|
+
'[&_h4]:mt-[1.4em] [&_h4]:mb-[0.4em] [&_h4]:font-display [&_h4]:text-h4',
|
|
20
|
+
// Text
|
|
21
|
+
'[&_p]:my-[1em] [&_p]:text-pretty',
|
|
22
|
+
'[&_strong]:font-semibold [&_strong]:text-foreground',
|
|
23
|
+
'[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:decoration-primary/40 [&_a]:underline-offset-4 [&_a:hover]:decoration-primary',
|
|
24
|
+
// Lists
|
|
25
|
+
'[&_ul]:my-[1em] [&_ul]:list-disc [&_ul]:pl-[1.5em] [&_ol]:my-[1em] [&_ol]:list-decimal [&_ol]:pl-[1.5em]',
|
|
26
|
+
'[&_li]:my-[0.35em] [&_li]:pl-[0.25em] [&_li::marker]:text-faint-foreground',
|
|
27
|
+
// Quotes
|
|
28
|
+
'[&_blockquote]:my-[1.5em] [&_blockquote]:border-l-2 [&_blockquote]:border-primary [&_blockquote]:pl-[1em] [&_blockquote]:font-display [&_blockquote]:text-muted-foreground',
|
|
29
|
+
// Code
|
|
30
|
+
'[&_:not(pre)>code]:rounded-xs [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-[0.3em] [&_:not(pre)>code]:py-[0.15em] [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]',
|
|
31
|
+
'[&_pre]:my-[1.5em] [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:border-border [&_pre]:bg-subtle [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-small [&_pre]:leading-relaxed',
|
|
32
|
+
// Media and rules
|
|
33
|
+
'[&_hr]:my-[2em] [&_hr]:border-border',
|
|
34
|
+
'[&_img]:my-[1.5em] [&_img]:max-w-full [&_img]:rounded-lg',
|
|
35
|
+
'[&_figure]:my-[1.75em] [&_figure_img]:my-0 [&_figcaption]:mt-2 [&_figcaption]:text-small [&_figcaption]:text-faint-foreground',
|
|
36
|
+
// Tables
|
|
37
|
+
'[&_table]:my-[1.5em] [&_table]:w-full [&_table]:border-collapse [&_table]:text-small [&_table]:tabular-nums',
|
|
38
|
+
'[&_th]:border-b [&_th]:border-border-strong [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium',
|
|
39
|
+
'[&_td]:border-b [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_td]:align-top',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
variants: {
|
|
43
|
+
size: {
|
|
44
|
+
sm: { base: 'text-small [&_blockquote]:text-body' },
|
|
45
|
+
md: { base: 'text-body [&_blockquote]:text-lead' },
|
|
46
|
+
lg: { base: 'text-lead [&_blockquote]:text-h4' },
|
|
47
|
+
},
|
|
48
|
+
fullWidth: {
|
|
49
|
+
true: { base: 'max-w-none' },
|
|
50
|
+
false: { base: 'max-w-prose' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: { size: 'md', fullWidth: false },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
declare module '@glyphui/tokens' {
|
|
57
|
+
interface GlyphComponentRegistry {
|
|
58
|
+
Prose: StylesEntry<typeof proseStyles>
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProseProps extends ComponentProps<'div'>, VariantProps<typeof proseStyles> {
|
|
63
|
+
/** Render the child element (e.g. `<article>`) instead of a `<div>`. */
|
|
64
|
+
asChild?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Styles raw HTML or MDX (headings, lists, links, quotes, code, tables, images) from theme
|
|
69
|
+
* tokens. Wrap rendered Markdown or CMS content in it.
|
|
70
|
+
*/
|
|
71
|
+
export function Prose(inProps: ProseProps) {
|
|
72
|
+
const [props, slots] = useComponentConfig('Prose', inProps)
|
|
73
|
+
const { size, fullWidth, asChild, className, ...rest } = props
|
|
74
|
+
const Comp = asChild ? Slot.Root : 'div'
|
|
75
|
+
return (
|
|
76
|
+
<Comp
|
|
77
|
+
data-slot="prose"
|
|
78
|
+
className={proseStyles({ size, fullWidth }).base({ class: [slots.base, className] })}
|
|
79
|
+
{...rest}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { focusRing, themedBorder, transition } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
const control = [
|
|
6
|
+
'mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-pill border-input bg-surface shadow-xs',
|
|
7
|
+
themedBorder,
|
|
8
|
+
transition,
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const radioGroupStyles = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
root: '',
|
|
14
|
+
option: 'flex items-start gap-2.5',
|
|
15
|
+
control: [
|
|
16
|
+
...control,
|
|
17
|
+
focusRing,
|
|
18
|
+
'peer cursor-pointer hover:border-border-strong aria-invalid:border-danger',
|
|
19
|
+
'data-[state=checked]:border-primary disabled:cursor-not-allowed disabled:opacity-50',
|
|
20
|
+
],
|
|
21
|
+
indicator:
|
|
22
|
+
'flex size-full items-center justify-center after:block after:size-2 after:rounded-pill after:bg-primary',
|
|
23
|
+
text: 'grid gap-0.5',
|
|
24
|
+
label:
|
|
25
|
+
'cursor-pointer text-small font-medium text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-60',
|
|
26
|
+
description: 'text-small text-faint-foreground',
|
|
27
|
+
card: [
|
|
28
|
+
'group relative flex w-full cursor-pointer items-start gap-3 rounded-lg border-border bg-surface p-4 text-left shadow-xs',
|
|
29
|
+
themedBorder,
|
|
30
|
+
transition,
|
|
31
|
+
focusRing,
|
|
32
|
+
'hover:border-border-strong',
|
|
33
|
+
'data-[state=checked]:border-primary data-[state=checked]:ring-1 data-[state=checked]:ring-primary',
|
|
34
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
35
|
+
],
|
|
36
|
+
cardControl: [...control, 'group-data-[state=checked]:border-primary'],
|
|
37
|
+
},
|
|
38
|
+
variants: {
|
|
39
|
+
orientation: {
|
|
40
|
+
vertical: { root: 'grid gap-3' },
|
|
41
|
+
horizontal: { root: 'flex flex-wrap gap-x-5 gap-y-3' },
|
|
42
|
+
},
|
|
43
|
+
variant: {
|
|
44
|
+
default: {},
|
|
45
|
+
card: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
compoundVariants: [
|
|
49
|
+
{
|
|
50
|
+
variant: 'card',
|
|
51
|
+
orientation: 'horizontal',
|
|
52
|
+
class: { root: 'grid grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] gap-3' },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
defaultVariants: { orientation: 'vertical', variant: 'default' },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export type RadioGroupVariants = VariantProps<typeof radioGroupStyles>
|
|
59
|
+
|
|
60
|
+
declare module '@glyphui/tokens' {
|
|
61
|
+
interface GlyphComponentRegistry {
|
|
62
|
+
RadioGroup: StylesEntry<typeof radioGroupStyles>
|
|
63
|
+
}
|
|
64
|
+
}
|