@glyphui/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/data-table.js +2 -2
- package/dist/{field-DXZD0l8w.js → field-CCecIzdq.js} +1 -1
- package/dist/{field-context-B8cYheXz.js → field-context-B6WOcK5m.js} +99 -99
- package/dist/form.js +2 -2
- package/dist/index.d.ts +2743 -93
- package/dist/index.js +2748 -111
- package/dist/{states-BxPjCUxz.js → states-Rp8wZ_jH.js} +1 -1
- package/package.json +4 -2
- package/src/components/accordion/accordion.styles.ts +41 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/alert-icons.tsx +66 -0
- package/src/components/alert/alert.styles.ts +57 -0
- package/src/components/alert/alert.tsx +86 -0
- package/src/components/alert/index.ts +2 -0
- package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
- package/src/components/alert-dialog/alert-dialog.tsx +118 -0
- package/src/components/alert-dialog/confirm.tsx +111 -0
- package/src/components/alert-dialog/index.ts +15 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/badge/badge.styles.ts +1 -1
- package/src/components/banner/banner.styles.ts +40 -0
- package/src/components/banner/banner.tsx +101 -0
- package/src/components/banner/index.ts +2 -0
- package/src/components/blockquote/blockquote.tsx +59 -0
- package/src/components/blockquote/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
- package/src/components/breadcrumb/breadcrumb.tsx +135 -0
- package/src/components/breadcrumb/index.ts +12 -0
- package/src/components/button/button.tsx +11 -8
- package/src/components/code/code.styles.ts +33 -0
- package/src/components/code/code.tsx +96 -0
- package/src/components/code/index.ts +2 -0
- package/src/components/combobox/combobox.styles.ts +50 -0
- package/src/components/combobox/combobox.tsx +203 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command-menu/command-menu.styles.ts +41 -0
- package/src/components/command-menu/command-menu.tsx +217 -0
- package/src/components/command-menu/index.ts +14 -0
- package/src/components/description-list/description-list.styles.ts +31 -0
- package/src/components/description-list/description-list.tsx +56 -0
- package/src/components/description-list/index.ts +7 -0
- package/src/components/dialog/close-icon.tsx +22 -0
- package/src/components/dialog/dialog.styles.ts +41 -0
- package/src/components/dialog/dialog.tsx +111 -0
- package/src/components/dialog/index.ts +13 -0
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
- package/src/components/dropdown-menu/index.ts +19 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/link/link.styles.ts +39 -0
- package/src/components/link/link.tsx +62 -0
- package/src/components/pagination/index.ts +3 -0
- package/src/components/pagination/pagination-range.ts +35 -0
- package/src/components/pagination/pagination.styles.ts +41 -0
- package/src/components/pagination/pagination.tsx +130 -0
- package/src/components/popover/index.ts +9 -0
- package/src/components/popover/popover.tsx +66 -0
- package/src/components/progress/index.ts +7 -0
- package/src/components/progress/progress.styles.ts +68 -0
- package/src/components/progress/progress.tsx +158 -0
- package/src/components/prose/index.ts +1 -0
- package/src/components/prose/prose.tsx +82 -0
- package/src/components/radio-group/index.ts +7 -0
- package/src/components/radio-group/radio-group.styles.ts +64 -0
- package/src/components/radio-group/radio-group.tsx +128 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +68 -0
- package/src/components/sheet/index.ts +13 -0
- package/src/components/sheet/sheet.styles.ts +60 -0
- package/src/components/sheet/sheet.tsx +108 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider.styles.ts +32 -0
- package/src/components/slider/slider.tsx +131 -0
- package/src/components/stat-card/index.ts +2 -0
- package/src/components/stat-card/stat-card.styles.ts +24 -0
- package/src/components/stat-card/stat-card.tsx +108 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/index.ts +25 -0
- package/src/provider/glyph-provider.tsx +16 -2
|
@@ -0,0 +1,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,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
|
+
}
|