@glyphui/ui 0.2.0 → 0.4.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 +65 -12
- package/dist/chart.d.ts +202 -0
- package/dist/chart.js +466 -0
- package/dist/color-mode-script-Bf7LYkOj.js +13 -0
- package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
- package/dist/data-table.js +3 -2
- package/dist/{field-CCecIzdq.js → field-CVbywksE.js} +21 -5
- package/dist/{field-context-B6WOcK5m.js → field-context-7I-cWvvS.js} +3 -57
- package/dist/form.d.ts +11 -4
- package/dist/form.js +34 -8
- package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
- package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
- package/dist/index.d.ts +2334 -283
- package/dist/index.js +2952 -326
- package/dist/react-router.d.ts +18 -0
- package/dist/react-router.js +44 -0
- package/dist/{states-Rp8wZ_jH.js → states-IvhjBgna.js} +2 -1
- package/dist/tv-DLbT66dC.js +70 -0
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +29 -0
- package/fonts.css +10 -0
- package/package.json +47 -5
- package/src/components/accordion/accordion.styles.ts +4 -4
- package/src/components/badge/badge.styles.ts +11 -16
- package/src/components/banner/banner.styles.ts +4 -4
- package/src/components/button/button.styles.ts +3 -1
- package/src/components/calendar/calendar.styles.ts +51 -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/collapsible/collapsible.tsx +66 -0
- package/src/components/collapsible/index.ts +6 -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/dropdown-menu/dropdown-menu.styles.ts +2 -2
- package/src/components/eyebrow/eyebrow.styles.ts +1 -1
- package/src/components/field/field-context.ts +2 -0
- package/src/components/field/field.styles.ts +4 -2
- package/src/components/field/field.tsx +25 -4
- 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/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/link.styles.ts +1 -1
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/modal.styles.ts +20 -0
- package/src/components/modal/modal.tsx +111 -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 +208 -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/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/prose/prose.tsx +1 -1
- 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/scroll-area.tsx +23 -1
- package/src/components/segmented-control/segmented-control.tsx +17 -2
- package/src/components/sheet/sheet.styles.ts +7 -7
- package/src/components/slider/slider.tsx +9 -2
- 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/text/text.styles.ts +4 -4
- 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/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 +20 -0
- package/src/integrations/react-router.tsx +39 -0
- package/src/integrations/vite.test-d.ts +6 -0
- package/src/integrations/vite.test.ts +28 -0
- package/src/integrations/vite.ts +33 -0
- package/src/internal/dev.ts +12 -0
- package/src/patterns/app-shell/app-shell.styles.ts +1 -1
- package/src/patterns/app-shell/navbar.tsx +34 -16
- package/src/patterns/app-shell/sidebar.tsx +7 -2
- package/src/patterns/chart/chart-parts.tsx +179 -0
- package/src/patterns/chart/chart.styles.ts +35 -0
- package/src/patterns/chart/chart.tsx +386 -0
- package/src/patterns/chart/index.ts +13 -0
- package/src/patterns/form/form.tsx +48 -6
- package/src/patterns/form/index.ts +1 -0
- package/src/provider/color-mode-script.ts +28 -0
- package/src/provider/color-mode.tsx +14 -20
- package/src/provider/glyph-context.ts +38 -0
- package/src/provider/glyph-provider.tsx +16 -4
- package/src/provider/index.ts +4 -0
- package/src/provider/theme-scope.tsx +7 -3
- package/styles.css +88 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
3
|
+
import { fieldControl } from '../input/input.styles'
|
|
4
|
+
|
|
5
|
+
export const tagInputStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'grid gap-1.5',
|
|
8
|
+
frame: [
|
|
9
|
+
...fieldControl,
|
|
10
|
+
'flex cursor-text flex-wrap items-center gap-1.5 px-2 py-1',
|
|
11
|
+
'focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/20',
|
|
12
|
+
'data-invalid:focus-within:ring-danger/20',
|
|
13
|
+
'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50',
|
|
14
|
+
],
|
|
15
|
+
tag: 'inline-flex h-6 max-w-full items-center gap-1 rounded-pill bg-muted pr-1 pl-2.5 text-caption text-foreground',
|
|
16
|
+
tagLabel: 'truncate',
|
|
17
|
+
remove:
|
|
18
|
+
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-pill text-muted-foreground hover:bg-border-strong/40 hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring [&>svg]:size-3',
|
|
19
|
+
input:
|
|
20
|
+
'h-7 min-w-24 flex-1 bg-transparent px-1 text-foreground outline-none placeholder:text-faint-foreground disabled:cursor-not-allowed',
|
|
21
|
+
message: 'text-small font-medium text-danger-text',
|
|
22
|
+
},
|
|
23
|
+
variants: {
|
|
24
|
+
size: {
|
|
25
|
+
sm: { frame: 'min-h-(--glyph-control-sm) text-small' },
|
|
26
|
+
md: { frame: 'min-h-(--glyph-control-md) text-small' },
|
|
27
|
+
lg: { frame: 'min-h-(--glyph-control-lg) text-body' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { size: 'md' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type TagInputVariants = VariantProps<typeof tagInputStyles>
|
|
34
|
+
|
|
35
|
+
declare module '@glyphui/tokens' {
|
|
36
|
+
interface GlyphComponentRegistry {
|
|
37
|
+
TagInput: StylesEntry<typeof tagInputStyles>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { type ClipboardEvent, type KeyboardEvent, useId, useRef, useState } from 'react'
|
|
2
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { useControllableState } from '../../hooks/use-controllable-state'
|
|
4
|
+
import { useFieldControl } from '../field/field-context'
|
|
5
|
+
import { type TagInputVariants, tagInputStyles } from './tag-input.styles'
|
|
6
|
+
|
|
7
|
+
export interface TagInputProps extends TagInputVariants {
|
|
8
|
+
value?: string[]
|
|
9
|
+
defaultValue?: string[]
|
|
10
|
+
onValueChange?: (tags: string[]) => void
|
|
11
|
+
placeholder?: string
|
|
12
|
+
/** Stop accepting tags after this many. */
|
|
13
|
+
maxTags?: number
|
|
14
|
+
/** Return an error message to reject a tag, or `true` to accept it. */
|
|
15
|
+
validate?: (tag: string, tags: readonly string[]) => string | true
|
|
16
|
+
/** Allow the same tag more than once. Default false. */
|
|
17
|
+
allowDuplicates?: boolean
|
|
18
|
+
/** Keys (besides Enter) that turn the typed text into a tag. Default `[',']`. */
|
|
19
|
+
separators?: readonly string[]
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
/** Submits each tag as its own hidden input with this name. */
|
|
22
|
+
name?: string
|
|
23
|
+
id?: string
|
|
24
|
+
className?: string
|
|
25
|
+
'aria-label'?: string
|
|
26
|
+
'aria-describedby'?: string
|
|
27
|
+
'aria-invalid'?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function XIcon() {
|
|
31
|
+
return (
|
|
32
|
+
<svg
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
strokeWidth="2.5"
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Free-form multi-value input. Enter or a comma turns the text into a removable tag;
|
|
47
|
+
* Backspace on an empty input removes the last one. Pasting a comma-separated list adds them all.
|
|
48
|
+
*/
|
|
49
|
+
export function TagInput(inProps: TagInputProps) {
|
|
50
|
+
const [props, slots] = useComponentConfig('TagInput', useFieldControl(inProps))
|
|
51
|
+
const {
|
|
52
|
+
value: valueProp,
|
|
53
|
+
defaultValue = [],
|
|
54
|
+
onValueChange,
|
|
55
|
+
placeholder = 'Add a tag…',
|
|
56
|
+
maxTags,
|
|
57
|
+
validate,
|
|
58
|
+
allowDuplicates = false,
|
|
59
|
+
separators = [','],
|
|
60
|
+
disabled,
|
|
61
|
+
name,
|
|
62
|
+
id,
|
|
63
|
+
size,
|
|
64
|
+
className,
|
|
65
|
+
'aria-label': ariaLabel,
|
|
66
|
+
'aria-describedby': ariaDescribedBy,
|
|
67
|
+
'aria-invalid': ariaInvalid,
|
|
68
|
+
} = props
|
|
69
|
+
const [tags, setTags] = useControllableState<string[]>({
|
|
70
|
+
value: valueProp,
|
|
71
|
+
defaultValue,
|
|
72
|
+
onChange: onValueChange,
|
|
73
|
+
})
|
|
74
|
+
const [draft, setDraft] = useState('')
|
|
75
|
+
const [error, setError] = useState<string | null>(null)
|
|
76
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
77
|
+
const errorId = useId()
|
|
78
|
+
const s = tagInputStyles({ size })
|
|
79
|
+
const full = maxTags !== undefined && tags.length >= maxTags
|
|
80
|
+
const invalid = Boolean(error) || ariaInvalid === true
|
|
81
|
+
|
|
82
|
+
const add = (raw: readonly string[]) => {
|
|
83
|
+
let next = [...tags]
|
|
84
|
+
let problem: string | null = null
|
|
85
|
+
for (const candidate of raw.map((t) => t.trim()).filter(Boolean)) {
|
|
86
|
+
if (maxTags !== undefined && next.length >= maxTags) {
|
|
87
|
+
problem = `You can add up to ${maxTags} tags.`
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
if (!allowDuplicates && next.includes(candidate)) {
|
|
91
|
+
problem = `“${candidate}” is already added.`
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const verdict = validate?.(candidate, next) ?? true
|
|
95
|
+
if (verdict !== true) {
|
|
96
|
+
problem = verdict
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
next = [...next, candidate]
|
|
100
|
+
}
|
|
101
|
+
if (next.length !== tags.length) setTags(next)
|
|
102
|
+
setError(problem)
|
|
103
|
+
return problem === null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const commitDraft = () => {
|
|
107
|
+
if (!draft.trim()) return
|
|
108
|
+
if (add([draft])) setDraft('')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const remove = (index: number) => {
|
|
112
|
+
setTags(tags.filter((_, i) => i !== index))
|
|
113
|
+
setError(null)
|
|
114
|
+
inputRef.current?.focus()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
118
|
+
if (event.key === 'Enter' || separators.includes(event.key)) {
|
|
119
|
+
if (event.key === 'Enter' && !draft.trim()) return
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
commitDraft()
|
|
122
|
+
} else if (event.key === 'Backspace' && draft === '' && tags.length > 0) {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
remove(tags.length - 1)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
129
|
+
const text = event.clipboardData.getData('text')
|
|
130
|
+
const pattern = new RegExp(
|
|
131
|
+
`[${['\n', ...separators].map((s) => s.replace(/[-\\^\]]/g, '\\$&')).join('')}]`,
|
|
132
|
+
)
|
|
133
|
+
if (!pattern.test(text)) return
|
|
134
|
+
event.preventDefault()
|
|
135
|
+
add(text.split(pattern))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const describedBy =
|
|
139
|
+
[ariaDescribedBy, error ? errorId : undefined].filter(Boolean).join(' ') || undefined
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div data-slot="tag-input" className={s.root({ class: [slots.root, className] })}>
|
|
143
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions: clicking the frame focuses the input, a convenience; the input itself is focusable */}
|
|
144
|
+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard users reach the input directly */}
|
|
145
|
+
<div
|
|
146
|
+
data-invalid={invalid || undefined}
|
|
147
|
+
className={s.frame({ class: slots.frame })}
|
|
148
|
+
onClick={() => inputRef.current?.focus()}
|
|
149
|
+
>
|
|
150
|
+
{tags.map((tag, index) => (
|
|
151
|
+
<span key={`${tag}-${index}`} data-slot="tag" className={s.tag({ class: slots.tag })}>
|
|
152
|
+
<span className={s.tagLabel({ class: slots.tagLabel })}>{tag}</span>
|
|
153
|
+
{!disabled && (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
aria-label={`Remove ${tag}`}
|
|
157
|
+
className={s.remove({ class: slots.remove })}
|
|
158
|
+
onClick={(event) => {
|
|
159
|
+
event.stopPropagation()
|
|
160
|
+
remove(index)
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<XIcon />
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
</span>
|
|
167
|
+
))}
|
|
168
|
+
<input
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
id={id}
|
|
171
|
+
value={draft}
|
|
172
|
+
disabled={disabled || full}
|
|
173
|
+
placeholder={full ? undefined : tags.length === 0 ? placeholder : undefined}
|
|
174
|
+
aria-label={ariaLabel}
|
|
175
|
+
aria-describedby={describedBy}
|
|
176
|
+
aria-invalid={invalid || undefined}
|
|
177
|
+
className={s.input({ class: slots.input })}
|
|
178
|
+
onChange={(event) => {
|
|
179
|
+
setDraft(event.target.value)
|
|
180
|
+
if (error) setError(null)
|
|
181
|
+
}}
|
|
182
|
+
onKeyDown={handleKeyDown}
|
|
183
|
+
onPaste={handlePaste}
|
|
184
|
+
onBlur={commitDraft}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
{error && (
|
|
188
|
+
<p id={errorId} role="alert" className={s.message({ class: slots.message })}>
|
|
189
|
+
{error}
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
{name &&
|
|
193
|
+
tags.map((tag, index) => (
|
|
194
|
+
<input key={`${tag}-${index}`} type="hidden" name={name} value={tag} />
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -13,10 +13,10 @@ export const textStyles = tv({
|
|
|
13
13
|
default: { base: 'text-foreground' },
|
|
14
14
|
muted: { base: 'text-muted-foreground' },
|
|
15
15
|
faint: { base: 'text-faint-foreground' },
|
|
16
|
-
primary: { base: 'text-primary' },
|
|
17
|
-
success: { base: 'text-success' },
|
|
18
|
-
warning: { base: 'text-warning' },
|
|
19
|
-
danger: { base: 'text-danger' },
|
|
16
|
+
primary: { base: 'text-primary-text' },
|
|
17
|
+
success: { base: 'text-success-text' },
|
|
18
|
+
warning: { base: 'text-warning-text' },
|
|
19
|
+
danger: { base: 'text-danger-text' },
|
|
20
20
|
},
|
|
21
21
|
weight: {
|
|
22
22
|
regular: { base: 'font-normal' },
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { themedBorder } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const timelineStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'grid',
|
|
8
|
+
item: 'group/item relative grid grid-cols-[auto_1fr] gap-x-3',
|
|
9
|
+
rail: 'flex flex-col items-center',
|
|
10
|
+
marker: [
|
|
11
|
+
'z-10 flex size-6 shrink-0 items-center justify-center rounded-pill bg-surface [&>svg]:size-3.5',
|
|
12
|
+
themedBorder,
|
|
13
|
+
],
|
|
14
|
+
dot: 'size-2 rounded-pill bg-current',
|
|
15
|
+
connector: 'my-1 w-px flex-1 bg-border group-last/item:hidden',
|
|
16
|
+
content: 'grid min-w-0 gap-0.5 pt-0.5 pb-6 group-last/item:pb-0',
|
|
17
|
+
header: 'flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5',
|
|
18
|
+
title: 'text-small font-medium text-foreground',
|
|
19
|
+
time: 'font-mono text-caption text-faint-foreground tabular-nums',
|
|
20
|
+
description: 'text-small text-muted-foreground',
|
|
21
|
+
},
|
|
22
|
+
variants: {
|
|
23
|
+
tone: {
|
|
24
|
+
neutral: { marker: 'border-border text-muted-foreground' },
|
|
25
|
+
primary: {
|
|
26
|
+
marker:
|
|
27
|
+
'border-primary/40 text-[color-mix(in_oklab,var(--color-primary)_80%,var(--color-foreground))]',
|
|
28
|
+
},
|
|
29
|
+
info: {
|
|
30
|
+
marker:
|
|
31
|
+
'border-info/40 text-[color-mix(in_oklab,var(--color-info)_80%,var(--color-foreground))]',
|
|
32
|
+
},
|
|
33
|
+
success: {
|
|
34
|
+
marker:
|
|
35
|
+
'border-success/40 text-[color-mix(in_oklab,var(--color-success)_80%,var(--color-foreground))]',
|
|
36
|
+
},
|
|
37
|
+
warning: {
|
|
38
|
+
marker:
|
|
39
|
+
'border-warning/50 text-[color-mix(in_oklab,var(--color-warning)_80%,var(--color-foreground))]',
|
|
40
|
+
},
|
|
41
|
+
danger: {
|
|
42
|
+
marker:
|
|
43
|
+
'border-danger/40 text-[color-mix(in_oklab,var(--color-danger)_80%,var(--color-foreground))]',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
defaultVariants: { tone: 'neutral' },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export type TimelineVariants = VariantProps<typeof timelineStyles>
|
|
51
|
+
|
|
52
|
+
declare module '@glyphui/tokens' {
|
|
53
|
+
interface GlyphComponentRegistry {
|
|
54
|
+
Timeline: StylesEntry<typeof timelineStyles>
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ComponentProps, createContext, type ReactNode, useContext } from 'react'
|
|
2
|
+
import { type SlotClasses, useComponentConfig } from '../../hooks/use-component-config'
|
|
3
|
+
import { type TimelineVariants, timelineStyles } from './timeline.styles'
|
|
4
|
+
|
|
5
|
+
const TimelineContext = createContext<SlotClasses<'Timeline'>>({})
|
|
6
|
+
|
|
7
|
+
export interface TimelineItemData extends TimelineVariants {
|
|
8
|
+
/** Stable key. Falls back to the index. */
|
|
9
|
+
id?: string
|
|
10
|
+
title: ReactNode
|
|
11
|
+
/** Display text for when it happened, e.g. "2h ago" or "Oct 12". */
|
|
12
|
+
time?: ReactNode
|
|
13
|
+
/** Machine-readable date for the `<time>` element, e.g. "2026-10-12T09:41". */
|
|
14
|
+
dateTime?: string
|
|
15
|
+
description?: ReactNode
|
|
16
|
+
/** Replaces the dot in the marker. */
|
|
17
|
+
icon?: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TimelineProps extends Omit<ComponentProps<'ol'>, 'children'> {
|
|
21
|
+
/** Data-driven items. Use `children` with TimelineItem for custom content. */
|
|
22
|
+
items?: readonly TimelineItemData[]
|
|
23
|
+
children?: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A vertical list of events in order, for activity feeds, changelogs and order tracking. */
|
|
27
|
+
export function Timeline(inProps: TimelineProps) {
|
|
28
|
+
const [props, slots] = useComponentConfig('Timeline', inProps)
|
|
29
|
+
const { items, className, children, ...rest } = props
|
|
30
|
+
const s = timelineStyles()
|
|
31
|
+
return (
|
|
32
|
+
<TimelineContext.Provider value={slots}>
|
|
33
|
+
<ol data-slot="timeline" className={s.root({ class: [slots.root, className] })} {...rest}>
|
|
34
|
+
{items
|
|
35
|
+
? items.map(({ id, description, ...item }, index) => (
|
|
36
|
+
<TimelineItem key={id ?? index} {...item}>
|
|
37
|
+
{description}
|
|
38
|
+
</TimelineItem>
|
|
39
|
+
))
|
|
40
|
+
: children}
|
|
41
|
+
</ol>
|
|
42
|
+
</TimelineContext.Provider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TimelineItemProps
|
|
47
|
+
extends Omit<ComponentProps<'li'>, 'title'>,
|
|
48
|
+
Omit<TimelineItemData, 'id' | 'description'> {
|
|
49
|
+
/** Description below the title. */
|
|
50
|
+
children?: ReactNode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function TimelineItem({
|
|
54
|
+
title,
|
|
55
|
+
time,
|
|
56
|
+
dateTime,
|
|
57
|
+
icon,
|
|
58
|
+
tone,
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: TimelineItemProps) {
|
|
63
|
+
const slots = useContext(TimelineContext)
|
|
64
|
+
const s = timelineStyles({ tone })
|
|
65
|
+
return (
|
|
66
|
+
<li
|
|
67
|
+
data-slot="timeline-item"
|
|
68
|
+
data-tone={tone ?? 'neutral'}
|
|
69
|
+
className={s.item({ class: [slots.item, className] })}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
<div className={s.rail({ class: slots.rail })} aria-hidden="true">
|
|
73
|
+
<span className={s.marker({ class: slots.marker })}>
|
|
74
|
+
{icon ?? <span className={s.dot({ class: slots.dot })} />}
|
|
75
|
+
</span>
|
|
76
|
+
<span className={s.connector({ class: slots.connector })} />
|
|
77
|
+
</div>
|
|
78
|
+
<div className={s.content({ class: slots.content })}>
|
|
79
|
+
<div className={s.header({ class: slots.header })}>
|
|
80
|
+
<p className={s.title({ class: slots.title })}>{title}</p>
|
|
81
|
+
{time && (
|
|
82
|
+
<time dateTime={dateTime} className={s.time({ class: slots.time })}>
|
|
83
|
+
{time}
|
|
84
|
+
</time>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
{children && <div className={s.description({ class: slots.description })}>{children}</div>}
|
|
88
|
+
</div>
|
|
89
|
+
</li>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export const toggleStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
base: [
|
|
8
|
+
'inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-muted-foreground',
|
|
9
|
+
transition,
|
|
10
|
+
focusRing,
|
|
11
|
+
'hover:bg-muted hover:text-foreground',
|
|
12
|
+
'data-[state=on]:bg-muted data-[state=on]:text-foreground',
|
|
13
|
+
'disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
|
|
14
|
+
],
|
|
15
|
+
group: 'inline-flex items-center gap-1',
|
|
16
|
+
},
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
default: { base: 'bg-transparent' },
|
|
20
|
+
outline: {
|
|
21
|
+
base: ['border-border bg-transparent shadow-xs hover:border-border-strong', themedBorder],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
sm: { base: 'h-(--glyph-control-sm) min-w-(--glyph-control-sm) px-2 text-caption' },
|
|
26
|
+
md: { base: 'h-(--glyph-control-md) min-w-(--glyph-control-md) px-2.5 text-small' },
|
|
27
|
+
lg: { base: 'h-(--glyph-control-lg) min-w-(--glyph-control-lg) px-3 text-small' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { variant: 'default', size: 'md' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type ToggleVariants = VariantProps<typeof toggleStyles>
|
|
34
|
+
|
|
35
|
+
declare module '@glyphui/tokens' {
|
|
36
|
+
interface GlyphComponentRegistry {
|
|
37
|
+
Toggle: StylesEntry<typeof toggleStyles>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ToggleGroup as ToggleGroupPrimitive, Toggle as TogglePrimitive } from 'radix-ui'
|
|
2
|
+
import { type ComponentProps, createContext, useContext } from 'react'
|
|
3
|
+
import { useComponentConfig } from '../../hooks/use-component-config'
|
|
4
|
+
import { type ToggleVariants, toggleStyles } from './toggle.styles'
|
|
5
|
+
|
|
6
|
+
export interface ToggleProps extends ComponentProps<typeof TogglePrimitive.Root>, ToggleVariants {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A two-state button, e.g. Bold in a toolbar. Use Switch for settings that take effect
|
|
10
|
+
* immediately, and Checkbox inside forms.
|
|
11
|
+
*/
|
|
12
|
+
export function Toggle(inProps: ToggleProps) {
|
|
13
|
+
const [props, slots] = useComponentConfig('Toggle', inProps)
|
|
14
|
+
const { variant, size, className, ...rest } = props
|
|
15
|
+
return (
|
|
16
|
+
<TogglePrimitive.Root
|
|
17
|
+
data-slot="toggle"
|
|
18
|
+
className={toggleStyles({ variant, size }).base({ class: [slots.base, className] })}
|
|
19
|
+
{...rest}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ToggleGroupContext = createContext<ToggleVariants>({})
|
|
25
|
+
|
|
26
|
+
export type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive.Root> & ToggleVariants
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A set of toggles. `type="single"` behaves like a radio group (text alignment),
|
|
30
|
+
* `type="multiple"` like checkboxes (bold, italic, underline). Arrow keys move between items.
|
|
31
|
+
*/
|
|
32
|
+
export function ToggleGroup(inProps: ToggleGroupProps) {
|
|
33
|
+
const [props, slots] = useComponentConfig('Toggle', inProps)
|
|
34
|
+
const { variant, size, className, ...rest } = props
|
|
35
|
+
return (
|
|
36
|
+
<ToggleGroupContext.Provider value={{ variant, size }}>
|
|
37
|
+
<ToggleGroupPrimitive.Root
|
|
38
|
+
data-slot="toggle-group"
|
|
39
|
+
className={toggleStyles().group({ class: [slots.group, className] })}
|
|
40
|
+
{...rest}
|
|
41
|
+
/>
|
|
42
|
+
</ToggleGroupContext.Provider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ToggleGroupItemProps
|
|
47
|
+
extends ComponentProps<typeof ToggleGroupPrimitive.Item>,
|
|
48
|
+
ToggleVariants {}
|
|
49
|
+
|
|
50
|
+
export function ToggleGroupItem({ variant, size, className, ...props }: ToggleGroupItemProps) {
|
|
51
|
+
const group = useContext(ToggleGroupContext)
|
|
52
|
+
const [, slots] = useComponentConfig('Toggle', {})
|
|
53
|
+
const s = toggleStyles({ variant: variant ?? group.variant, size: size ?? group.size })
|
|
54
|
+
return (
|
|
55
|
+
<ToggleGroupPrimitive.Item
|
|
56
|
+
data-slot="toggle-group-item"
|
|
57
|
+
className={s.base({ class: [slots.base, className] })}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { StylesEntry } from '../../utils/registry'
|
|
2
|
+
import { transition } from '../../utils/styles'
|
|
3
|
+
import { tv, type VariantProps } from '../../utils/tv'
|
|
4
|
+
|
|
5
|
+
export const treeViewStyles = tv({
|
|
6
|
+
slots: {
|
|
7
|
+
root: 'grid gap-px text-small',
|
|
8
|
+
// Focus and selection are drawn on the item's own row only, never on nested rows.
|
|
9
|
+
item: [
|
|
10
|
+
'outline-none',
|
|
11
|
+
'focus-visible:[&>[data-slot=tree-row]]:ring-2 focus-visible:[&>[data-slot=tree-row]]:ring-ring',
|
|
12
|
+
'aria-selected:[&>[data-slot=tree-row]]:bg-muted aria-selected:[&>[data-slot=tree-row]]:text-foreground aria-selected:[&>[data-slot=tree-row]]:font-medium',
|
|
13
|
+
'aria-disabled:[&>[data-slot=tree-row]]:pointer-events-none aria-disabled:[&>[data-slot=tree-row]]:opacity-50',
|
|
14
|
+
],
|
|
15
|
+
row: [
|
|
16
|
+
'group/row flex h-8 cursor-pointer select-none items-center gap-1.5 rounded-sm pr-2 text-muted-foreground',
|
|
17
|
+
transition,
|
|
18
|
+
'hover:bg-muted hover:text-foreground',
|
|
19
|
+
],
|
|
20
|
+
chevron:
|
|
21
|
+
'size-4 shrink-0 text-faint-foreground transition-transform duration-(--glyph-duration-fast) ease-standard group-data-[state=open]/row:rotate-90',
|
|
22
|
+
spacer: 'size-4 shrink-0',
|
|
23
|
+
icon: 'flex size-4 shrink-0 items-center justify-center text-faint-foreground [&>svg]:size-4',
|
|
24
|
+
label: 'min-w-0 flex-1 truncate',
|
|
25
|
+
group: 'grid gap-px',
|
|
26
|
+
},
|
|
27
|
+
variants: {
|
|
28
|
+
size: {
|
|
29
|
+
sm: { row: 'h-7 text-caption' },
|
|
30
|
+
md: {},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: { size: 'md' },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export type TreeViewVariants = VariantProps<typeof treeViewStyles>
|
|
37
|
+
|
|
38
|
+
declare module '@glyphui/tokens' {
|
|
39
|
+
interface GlyphComponentRegistry {
|
|
40
|
+
TreeView: StylesEntry<typeof treeViewStyles>
|
|
41
|
+
}
|
|
42
|
+
}
|