@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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/data-table.js +2 -2
  3. package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
  4. package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
  5. package/dist/form.js +2 -2
  6. package/dist/index.d.ts +2743 -93
  7. package/dist/index.js +2748 -111
  8. package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
  9. package/package.json +4 -2
  10. package/src/components/accordion/accordion.styles.ts +41 -0
  11. package/src/components/accordion/accordion.tsx +86 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/alert/alert-icons.tsx +66 -0
  14. package/src/components/alert/alert.styles.ts +57 -0
  15. package/src/components/alert/alert.tsx +86 -0
  16. package/src/components/alert/index.ts +2 -0
  17. package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
  18. package/src/components/alert-dialog/alert-dialog.tsx +118 -0
  19. package/src/components/alert-dialog/confirm.tsx +111 -0
  20. package/src/components/alert-dialog/index.ts +15 -0
  21. package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
  22. package/src/components/aspect-ratio/index.ts +1 -0
  23. package/src/components/badge/badge.styles.ts +1 -1
  24. package/src/components/banner/banner.styles.ts +40 -0
  25. package/src/components/banner/banner.tsx +101 -0
  26. package/src/components/banner/index.ts +2 -0
  27. package/src/components/blockquote/blockquote.tsx +59 -0
  28. package/src/components/blockquote/index.ts +1 -0
  29. package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
  30. package/src/components/breadcrumb/breadcrumb.tsx +135 -0
  31. package/src/components/breadcrumb/index.ts +12 -0
  32. package/src/components/button/button.tsx +11 -8
  33. package/src/components/code/code.styles.ts +33 -0
  34. package/src/components/code/code.tsx +96 -0
  35. package/src/components/code/index.ts +2 -0
  36. package/src/components/combobox/combobox.styles.ts +50 -0
  37. package/src/components/combobox/combobox.tsx +203 -0
  38. package/src/components/combobox/index.ts +2 -0
  39. package/src/components/command-menu/command-menu.styles.ts +41 -0
  40. package/src/components/command-menu/command-menu.tsx +217 -0
  41. package/src/components/command-menu/index.ts +14 -0
  42. package/src/components/description-list/description-list.styles.ts +31 -0
  43. package/src/components/description-list/description-list.tsx +56 -0
  44. package/src/components/description-list/index.ts +7 -0
  45. package/src/components/dialog/close-icon.tsx +22 -0
  46. package/src/components/dialog/dialog.styles.ts +41 -0
  47. package/src/components/dialog/dialog.tsx +111 -0
  48. package/src/components/dialog/index.ts +13 -0
  49. package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
  50. package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
  51. package/src/components/dropdown-menu/index.ts +19 -0
  52. package/src/components/grid/grid.tsx +144 -0
  53. package/src/components/grid/index.ts +1 -0
  54. package/src/components/link/index.ts +2 -0
  55. package/src/components/link/link.styles.ts +39 -0
  56. package/src/components/link/link.tsx +62 -0
  57. package/src/components/pagination/index.ts +3 -0
  58. package/src/components/pagination/pagination-range.ts +35 -0
  59. package/src/components/pagination/pagination.styles.ts +41 -0
  60. package/src/components/pagination/pagination.tsx +130 -0
  61. package/src/components/popover/index.ts +9 -0
  62. package/src/components/popover/popover.tsx +66 -0
  63. package/src/components/progress/index.ts +7 -0
  64. package/src/components/progress/progress.styles.ts +68 -0
  65. package/src/components/progress/progress.tsx +158 -0
  66. package/src/components/prose/index.ts +1 -0
  67. package/src/components/prose/prose.tsx +82 -0
  68. package/src/components/radio-group/index.ts +7 -0
  69. package/src/components/radio-group/radio-group.styles.ts +64 -0
  70. package/src/components/radio-group/radio-group.tsx +128 -0
  71. package/src/components/scroll-area/index.ts +1 -0
  72. package/src/components/scroll-area/scroll-area.tsx +68 -0
  73. package/src/components/sheet/index.ts +13 -0
  74. package/src/components/sheet/sheet.styles.ts +60 -0
  75. package/src/components/sheet/sheet.tsx +108 -0
  76. package/src/components/slider/index.ts +2 -0
  77. package/src/components/slider/slider.styles.ts +32 -0
  78. package/src/components/slider/slider.tsx +131 -0
  79. package/src/components/stat-card/index.ts +2 -0
  80. package/src/components/stat-card/stat-card.styles.ts +24 -0
  81. package/src/components/stat-card/stat-card.tsx +108 -0
  82. package/src/components/toast/index.ts +1 -0
  83. package/src/components/toast/toast.tsx +91 -0
  84. package/src/index.ts +25 -0
  85. package/src/provider/glyph-provider.tsx +16 -2
@@ -0,0 +1,35 @@
1
+ export type PaginationItem = number | 'ellipsis'
2
+
3
+ /**
4
+ * The page numbers to show, with `'ellipsis'` for gaps. Always includes the first and
5
+ * last page and `siblingCount` pages on each side of the current one, and keeps the
6
+ * number of items constant so the control doesn't jump in width.
7
+ *
8
+ * `paginationRange(6, 20)` → `[1, 'ellipsis', 5, 6, 7, 'ellipsis', 20]`
9
+ */
10
+ export function paginationRange(
11
+ page: number,
12
+ pageCount: number,
13
+ siblingCount = 1,
14
+ ): PaginationItem[] {
15
+ const count = Math.max(0, Math.floor(pageCount))
16
+ if (count === 0) return []
17
+ const current = Math.min(Math.max(1, Math.floor(page)), count)
18
+ // first + last + current + 2 ellipses + siblings on both sides
19
+ const slots = siblingCount * 2 + 5
20
+ if (count <= slots) return range(1, count)
21
+
22
+ const left = Math.max(current - siblingCount, 1)
23
+ const right = Math.min(current + siblingCount, count)
24
+ const showLeftGap = left > 2
25
+ const showRightGap = right < count - 1
26
+ const edge = 3 + siblingCount * 2
27
+
28
+ if (!showLeftGap && showRightGap) return [...range(1, edge), 'ellipsis', count]
29
+ if (showLeftGap && !showRightGap) return [1, 'ellipsis', ...range(count - edge + 1, count)]
30
+ return [1, 'ellipsis', ...range(left, right), 'ellipsis', count]
31
+ }
32
+
33
+ function range(from: number, to: number): number[] {
34
+ return Array.from({ length: to - from + 1 }, (_, i) => from + i)
35
+ }
@@ -0,0 +1,41 @@
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 paginationStyles = tv({
6
+ slots: {
7
+ root: 'flex',
8
+ list: 'flex flex-wrap items-center gap-1',
9
+ item: [
10
+ 'inline-flex shrink-0 cursor-pointer select-none items-center justify-center rounded-md font-medium tabular-nums text-muted-foreground',
11
+ transition,
12
+ focusRing,
13
+ 'hover:bg-muted hover:text-foreground',
14
+ 'aria-[current=page]:bg-primary aria-[current=page]:text-primary-foreground aria-[current=page]:shadow-xs',
15
+ 'disabled:pointer-events-none disabled:opacity-40 aria-disabled:pointer-events-none aria-disabled:opacity-40',
16
+ '[&>svg]:size-4',
17
+ ],
18
+ ellipsis: 'inline-flex items-center justify-center text-faint-foreground',
19
+ },
20
+ variants: {
21
+ size: {
22
+ sm: {
23
+ item: 'h-(--glyph-control-sm) min-w-(--glyph-control-sm) px-1.5 text-small',
24
+ ellipsis: 'h-(--glyph-control-sm) w-(--glyph-control-sm) text-small',
25
+ },
26
+ md: {
27
+ item: 'h-(--glyph-control-md) min-w-(--glyph-control-md) px-2 text-small',
28
+ ellipsis: 'h-(--glyph-control-md) w-(--glyph-control-md) text-small',
29
+ },
30
+ },
31
+ },
32
+ defaultVariants: { size: 'md' },
33
+ })
34
+
35
+ export type PaginationVariants = VariantProps<typeof paginationStyles>
36
+
37
+ declare module '@glyphui/tokens' {
38
+ interface GlyphComponentRegistry {
39
+ Pagination: StylesEntry<typeof paginationStyles>
40
+ }
41
+ }
@@ -0,0 +1,130 @@
1
+ import type { ComponentProps, ReactNode } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { ChevronLeftIcon, ChevronRightIcon } from '../../internal/icons'
4
+ import { useLinkComponent } from '../../provider/glyph-context'
5
+ import { type PaginationVariants, paginationStyles } from './pagination.styles'
6
+ import { paginationRange } from './pagination-range'
7
+
8
+ const ChevronsLeftIcon = () => (
9
+ <svg
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ >
18
+ <path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
19
+ </svg>
20
+ )
21
+ const ChevronsRightIcon = () => (
22
+ <svg
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ strokeWidth="2"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ aria-hidden="true"
30
+ >
31
+ <path d="m6 17 5-5-5-5M13 17l5-5-5-5" />
32
+ </svg>
33
+ )
34
+
35
+ export interface PaginationProps
36
+ extends Omit<ComponentProps<'nav'>, 'onChange'>,
37
+ PaginationVariants {
38
+ /** Current page, starting at 1. */
39
+ page: number
40
+ pageCount: number
41
+ onPageChange?: (page: number) => void
42
+ /** Render pages as links (through the provider's linkComponent) instead of buttons. */
43
+ getHref?: (page: number) => string
44
+ /** Pages shown on each side of the current page. Default 1. */
45
+ siblingCount?: number
46
+ /** Show first / last page buttons. Default true. */
47
+ showEdges?: boolean
48
+ }
49
+
50
+ /** Page navigation for long lists. Controlled: you own `page`. */
51
+ export function Pagination(inProps: PaginationProps) {
52
+ const [props, slots] = useComponentConfig('Pagination', inProps)
53
+ const {
54
+ page,
55
+ pageCount,
56
+ onPageChange,
57
+ getHref,
58
+ siblingCount = 1,
59
+ showEdges = true,
60
+ size,
61
+ className,
62
+ ...rest
63
+ } = props
64
+ const Link = useLinkComponent()
65
+ const s = paginationStyles({ size })
66
+ const itemClass = s.item({ class: slots.item })
67
+ const current = Math.min(Math.max(1, page), Math.max(1, pageCount))
68
+
69
+ const control = (
70
+ target: number,
71
+ label: string,
72
+ content: ReactNode,
73
+ options: { current?: boolean } = {},
74
+ ) => {
75
+ const disabled = target < 1 || target > pageCount || (target === current && !options.current)
76
+ const shared = {
77
+ 'aria-label': label,
78
+ 'aria-current': options.current ? ('page' as const) : undefined,
79
+ className: itemClass,
80
+ }
81
+ if (getHref) {
82
+ if (disabled) {
83
+ return (
84
+ <span aria-disabled="true" {...shared}>
85
+ {content}
86
+ </span>
87
+ )
88
+ }
89
+ return (
90
+ <Link href={getHref(target)} onClick={() => onPageChange?.(target)} {...shared}>
91
+ {content}
92
+ </Link>
93
+ )
94
+ }
95
+ return (
96
+ <button type="button" disabled={disabled} onClick={() => onPageChange?.(target)} {...shared}>
97
+ {content}
98
+ </button>
99
+ )
100
+ }
101
+
102
+ return (
103
+ <nav
104
+ aria-label="Pagination"
105
+ data-slot="pagination"
106
+ className={s.root({ class: [slots.root, className] })}
107
+ {...rest}
108
+ >
109
+ <ul className={s.list({ class: slots.list })}>
110
+ {showEdges && <li>{control(1, 'First page', <ChevronsLeftIcon />)}</li>}
111
+ <li>{control(current - 1, 'Previous page', <ChevronLeftIcon />)}</li>
112
+ {paginationRange(current, pageCount, siblingCount).map((item, index) =>
113
+ item === 'ellipsis' ? (
114
+ <li
115
+ key={`gap-${index}`}
116
+ className={s.ellipsis({ class: slots.ellipsis })}
117
+ aria-hidden="true"
118
+ >
119
+ …
120
+ </li>
121
+ ) : (
122
+ <li key={item}>{control(item, `Page ${item}`, item, { current: item === current })}</li>
123
+ ),
124
+ )}
125
+ <li>{control(current + 1, 'Next page', <ChevronRightIcon />)}</li>
126
+ {showEdges && <li>{control(pageCount, 'Last page', <ChevronsRightIcon />)}</li>}
127
+ </ul>
128
+ </nav>
129
+ )
130
+ }
@@ -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
+ }