@glyphui/ui 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +457 -0
  4. package/dist/data-table.js +3 -2
  5. package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
  6. package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
  7. package/dist/form.js +3 -2
  8. package/dist/index.d.ts +4960 -266
  9. package/dist/index.js +5336 -146
  10. package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
  11. package/dist/tv-DBAGlgXJ.js +58 -0
  12. package/package.json +19 -2
  13. package/src/components/accordion/accordion.styles.ts +41 -0
  14. package/src/components/accordion/accordion.tsx +86 -0
  15. package/src/components/accordion/index.ts +8 -0
  16. package/src/components/alert/alert-icons.tsx +66 -0
  17. package/src/components/alert/alert.styles.ts +57 -0
  18. package/src/components/alert/alert.tsx +86 -0
  19. package/src/components/alert/index.ts +2 -0
  20. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  21. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  22. package/src/components/alert-dialog/confirm.tsx +111 -0
  23. package/src/components/alert-dialog/index.ts +15 -0
  24. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  25. package/src/components/aspect-ratio/index.ts +1 -0
  26. package/src/components/badge/badge.styles.ts +1 -1
  27. package/src/components/banner/banner.styles.ts +40 -0
  28. package/src/components/banner/banner.tsx +101 -0
  29. package/src/components/banner/index.ts +2 -0
  30. package/src/components/blockquote/blockquote.tsx +59 -0
  31. package/src/components/blockquote/index.ts +1 -0
  32. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  33. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  34. package/src/components/breadcrumb/index.ts +12 -0
  35. package/src/components/button/button.tsx +11 -8
  36. package/src/components/calendar/calendar.styles.ts +50 -0
  37. package/src/components/calendar/calendar.tsx +84 -0
  38. package/src/components/calendar/index.ts +3 -0
  39. package/src/components/carousel/carousel.styles.ts +43 -0
  40. package/src/components/carousel/carousel.tsx +278 -0
  41. package/src/components/carousel/index.ts +13 -0
  42. package/src/components/code/code.styles.ts +33 -0
  43. package/src/components/code/code.tsx +96 -0
  44. package/src/components/code/index.ts +2 -0
  45. package/src/components/collapsible/collapsible.tsx +66 -0
  46. package/src/components/collapsible/index.ts +6 -0
  47. package/src/components/combobox/combobox.styles.ts +50 -0
  48. package/src/components/combobox/combobox.tsx +203 -0
  49. package/src/components/combobox/index.ts +2 -0
  50. package/src/components/command-menu/command-menu.styles.ts +41 -0
  51. package/src/components/command-menu/command-menu.tsx +217 -0
  52. package/src/components/command-menu/index.ts +14 -0
  53. package/src/components/context-menu/context-menu.styles.ts +19 -0
  54. package/src/components/context-menu/context-menu.tsx +226 -0
  55. package/src/components/context-menu/index.ts +19 -0
  56. package/src/components/copy-button/copy-button.tsx +117 -0
  57. package/src/components/copy-button/index.ts +1 -0
  58. package/src/components/date-picker/date-picker.styles.ts +42 -0
  59. package/src/components/date-picker/date-picker.tsx +322 -0
  60. package/src/components/date-picker/index.ts +8 -0
  61. package/src/components/description-list/description-list.styles.ts +31 -0
  62. package/src/components/description-list/description-list.tsx +56 -0
  63. package/src/components/description-list/index.ts +7 -0
  64. package/src/components/dialog/close-icon.tsx +22 -0
  65. package/src/components/dialog/dialog.styles.ts +41 -0
  66. package/src/components/dialog/dialog.tsx +111 -0
  67. package/src/components/dialog/index.ts +13 -0
  68. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  69. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  70. package/src/components/dropdown-menu/index.ts +19 -0
  71. package/src/components/file-upload/file-upload.styles.ts +43 -0
  72. package/src/components/file-upload/file-upload.tsx +343 -0
  73. package/src/components/file-upload/index.ts +10 -0
  74. package/src/components/grid/grid.tsx +144 -0
  75. package/src/components/grid/index.ts +1 -0
  76. package/src/components/hover-card/hover-card.tsx +41 -0
  77. package/src/components/hover-card/index.ts +1 -0
  78. package/src/components/input-otp/index.ts +10 -0
  79. package/src/components/input-otp/input-otp.styles.ts +38 -0
  80. package/src/components/input-otp/input-otp.tsx +116 -0
  81. package/src/components/link/index.ts +2 -0
  82. package/src/components/link/link.styles.ts +39 -0
  83. package/src/components/link/link.tsx +62 -0
  84. package/src/components/modal/index.ts +2 -0
  85. package/src/components/modal/modal.styles.ts +18 -0
  86. package/src/components/modal/modal.tsx +86 -0
  87. package/src/components/navigation-menu/index.ts +14 -0
  88. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  89. package/src/components/navigation-menu/navigation-menu.tsx +198 -0
  90. package/src/components/number-input/index.ts +2 -0
  91. package/src/components/number-input/number-input.styles.ts +41 -0
  92. package/src/components/number-input/number-input.tsx +211 -0
  93. package/src/components/pagination/index.ts +3 -0
  94. package/src/components/pagination/pagination-range.ts +35 -0
  95. package/src/components/pagination/pagination.styles.ts +41 -0
  96. package/src/components/pagination/pagination.tsx +130 -0
  97. package/src/components/password-input/index.ts +7 -0
  98. package/src/components/password-input/password-input.styles.ts +30 -0
  99. package/src/components/password-input/password-input.tsx +127 -0
  100. package/src/components/popover/index.ts +9 -0
  101. package/src/components/popover/popover.tsx +66 -0
  102. package/src/components/progress/index.ts +7 -0
  103. package/src/components/progress/progress.styles.ts +68 -0
  104. package/src/components/progress/progress.tsx +158 -0
  105. package/src/components/prose/index.ts +1 -0
  106. package/src/components/prose/prose.tsx +82 -0
  107. package/src/components/radio-group/index.ts +7 -0
  108. package/src/components/radio-group/radio-group.styles.ts +64 -0
  109. package/src/components/radio-group/radio-group.tsx +128 -0
  110. package/src/components/resizable/index.ts +9 -0
  111. package/src/components/resizable/resizable.styles.ts +28 -0
  112. package/src/components/resizable/resizable.tsx +69 -0
  113. package/src/components/scroll-area/index.ts +1 -0
  114. package/src/components/scroll-area/scroll-area.tsx +68 -0
  115. package/src/components/sheet/index.ts +13 -0
  116. package/src/components/sheet/sheet.styles.ts +60 -0
  117. package/src/components/sheet/sheet.tsx +108 -0
  118. package/src/components/slider/index.ts +2 -0
  119. package/src/components/slider/slider.styles.ts +32 -0
  120. package/src/components/slider/slider.tsx +131 -0
  121. package/src/components/stat-card/index.ts +2 -0
  122. package/src/components/stat-card/stat-card.styles.ts +24 -0
  123. package/src/components/stat-card/stat-card.tsx +108 -0
  124. package/src/components/stepper/index.ts +2 -0
  125. package/src/components/stepper/stepper.styles.ts +74 -0
  126. package/src/components/stepper/stepper.tsx +138 -0
  127. package/src/components/tag-input/index.ts +2 -0
  128. package/src/components/tag-input/tag-input.styles.ts +39 -0
  129. package/src/components/tag-input/tag-input.tsx +198 -0
  130. package/src/components/timeline/index.ts +8 -0
  131. package/src/components/timeline/timeline.styles.ts +56 -0
  132. package/src/components/timeline/timeline.tsx +91 -0
  133. package/src/components/toast/index.ts +1 -0
  134. package/src/components/toast/toast.tsx +91 -0
  135. package/src/components/toggle/index.ts +9 -0
  136. package/src/components/toggle/toggle.styles.ts +39 -0
  137. package/src/components/toggle/toggle.tsx +61 -0
  138. package/src/components/tree-view/index.ts +2 -0
  139. package/src/components/tree-view/tree-view.styles.ts +42 -0
  140. package/src/components/tree-view/tree-view.tsx +274 -0
  141. package/src/components/visually-hidden/index.ts +1 -0
  142. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  143. package/src/index.ts +45 -0
  144. package/src/patterns/chart/chart-parts.tsx +171 -0
  145. package/src/patterns/chart/chart.styles.ts +35 -0
  146. package/src/patterns/chart/chart.tsx +369 -0
  147. package/src/patterns/chart/index.ts +13 -0
  148. package/src/provider/glyph-provider.tsx +16 -2
  149. package/styles.css +16 -0
@@ -0,0 +1,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,9 @@
1
+ export {
2
+ Popover,
3
+ PopoverAnchor,
4
+ PopoverClose,
5
+ PopoverContent,
6
+ type PopoverContentProps,
7
+ PopoverTrigger,
8
+ popoverStyles,
9
+ } from './popover'
@@ -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,7 @@
1
+ export { Progress, ProgressCircle, type ProgressCircleProps, type ProgressProps } from './progress'
2
+ export {
3
+ type ProgressCircleVariants,
4
+ type ProgressVariants,
5
+ progressCircleStyles,
6
+ progressStyles,
7
+ } from './progress.styles'
@@ -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,7 @@
1
+ export {
2
+ RadioGroup,
3
+ RadioGroupItem,
4
+ type RadioGroupItemProps,
5
+ type RadioGroupProps,
6
+ } from './radio-group'
7
+ export { type RadioGroupVariants, radioGroupStyles } from './radio-group.styles'
@@ -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
+ }