@pantoia/ui 0.3.0 → 0.5.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 +46 -0
- package/accordion/index.module.css +4 -4
- package/accordion/index.tsx +21 -51
- package/alert-dialog/index.tsx +36 -42
- package/badge/index.tsx +1 -1
- package/button/index.tsx +31 -30
- package/callout/index.tsx +4 -4
- package/checkbox/index.tsx +20 -85
- package/combobox/index.tsx +122 -316
- package/command-palette/index.tsx +51 -32
- package/dialog/index.tsx +28 -24
- package/dist/index.js +5 -5
- package/drawer/index.module.css +2 -2
- package/drawer/index.tsx +52 -130
- package/dropdown-menu/index.tsx +87 -95
- package/field/index.tsx +27 -86
- package/input/index.tsx +7 -3
- package/kbd/index.tsx +2 -2
- package/label/index.tsx +6 -8
- package/number-field/index.tsx +122 -173
- package/package.json +19 -11
- package/popover/index.tsx +61 -32
- package/radio-group/index.tsx +35 -38
- package/registry.json +64 -154
- package/select/index.tsx +141 -70
- package/switch/index.tsx +13 -6
- package/table/index.tsx +1 -2
- package/tabs/index.module.css +3 -3
- package/tabs/index.tsx +22 -76
- package/textarea/index.tsx +19 -7
- package/toast/index.module.css +187 -0
- package/toast/index.tsx +141 -46
- package/toggle/index.tsx +4 -3
- package/tokens.css +7 -1
- package/tooltip/index.tsx +61 -30
- package/utilities.css +11 -6
- package/accordion/AGENTS.md +0 -60
- package/alert-dialog/AGENTS.md +0 -78
- package/badge/AGENTS.md +0 -35
- package/button/AGENTS.md +0 -39
- package/callout/AGENTS.md +0 -37
- package/checkbox/AGENTS.md +0 -54
- package/combobox/AGENTS.md +0 -150
- package/command-palette/AGENTS.md +0 -41
- package/dialog/AGENTS.md +0 -81
- package/drawer/AGENTS.md +0 -66
- package/dropdown-menu/AGENTS.md +0 -133
- package/field/AGENTS.md +0 -92
- package/input/AGENTS.md +0 -35
- package/kbd/AGENTS.md +0 -17
- package/label/AGENTS.md +0 -21
- package/number-field/AGENTS.md +0 -49
- package/popover/AGENTS.md +0 -64
- package/radio-group/AGENTS.md +0 -81
- package/select/AGENTS.md +0 -90
- package/switch/AGENTS.md +0 -37
- package/table/AGENTS.md +0 -82
- package/tabs/AGENTS.md +0 -61
- package/test-utils.ts +0 -10
- package/textarea/AGENTS.md +0 -35
- package/toast/AGENTS.md +0 -34
- package/toggle/AGENTS.md +0 -37
- package/tooltip/AGENTS.md +0 -50
package/number-field/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { NumberField as NumberFieldPrimitive } from '@base-ui/react/number-field'
|
|
4
|
+
import { useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
4
5
|
|
|
5
6
|
import { tv } from '../tv'
|
|
6
7
|
|
|
@@ -38,200 +39,148 @@ export const numberField = tv({
|
|
|
38
39
|
defaultVariants: { surface: 'bare', size: 'md' },
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n))
|
|
45
|
-
|
|
46
|
-
const GROUPED = new Intl.NumberFormat('en-US')
|
|
47
|
-
|
|
48
|
-
const TYPING = /[^0-9.-]/g
|
|
49
|
-
|
|
50
|
-
function snap(n: number, min: number, step: number) {
|
|
51
|
-
const from = Number.isFinite(min) ? min : 0
|
|
52
|
-
return from + Math.round((n - from) / step) * step
|
|
42
|
+
const PAGE_KEYS: Record<string, 'ArrowUp' | 'ArrowDown'> = {
|
|
43
|
+
PageUp: 'ArrowUp',
|
|
44
|
+
PageDown: 'ArrowDown',
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
useEffect(() => { latest.current = step })
|
|
62
|
-
|
|
63
|
-
const stop = () => {
|
|
64
|
-
clearTimeout(timer.current)
|
|
65
|
-
clearInterval(beat.current)
|
|
66
|
-
setPressed(false)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
useEffect(() => stop, [])
|
|
70
|
-
|
|
71
|
-
const onPointerDown = (e: ReactPointerEvent<HTMLButtonElement>) => {
|
|
72
|
-
if (e.button !== 0 || blocked) return
|
|
73
|
-
|
|
74
|
-
latest.current()
|
|
75
|
-
setPressed(true)
|
|
76
|
-
e.currentTarget.setPointerCapture(e.pointerId)
|
|
47
|
+
function snap(n: number, min: number | undefined, max: number | undefined, step: number) {
|
|
48
|
+
const from = min ?? 0
|
|
49
|
+
const snapped = from + Math.round((n - from) / step) * step
|
|
50
|
+
return Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? Number.NEGATIVE_INFINITY, snapped))
|
|
51
|
+
}
|
|
77
52
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
function blurDetails(event: Event): NumberFieldPrimitive.Root.ChangeEventDetails {
|
|
54
|
+
const details: NumberFieldPrimitive.Root.ChangeEventDetails = {
|
|
55
|
+
reason: 'input-blur',
|
|
56
|
+
event: event as FocusEvent,
|
|
57
|
+
trigger: event.target instanceof Element ? event.target : undefined,
|
|
58
|
+
isCanceled: false,
|
|
59
|
+
isPropagationAllowed: false,
|
|
60
|
+
cancel: () => { details.isCanceled = true },
|
|
61
|
+
allowPropagation: () => { details.isPropagationAllowed = true },
|
|
81
62
|
}
|
|
82
|
-
|
|
83
|
-
return { pressed, onPointerDown, onPointerUp: stop, onPointerCancel: stop, onLostPointerCapture: stop }
|
|
63
|
+
return details
|
|
84
64
|
}
|
|
85
65
|
|
|
86
|
-
export
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
surface?: 'bare' | 'well'
|
|
96
|
-
size?: 'sm' | 'md' | 'lg'
|
|
97
|
-
disabled?: boolean
|
|
98
|
-
readOnly?: boolean
|
|
99
|
-
className?: string
|
|
100
|
-
}
|
|
66
|
+
export type NumberFieldProps = Omit<NumberFieldPrimitive.Root.Props, 'className'>
|
|
67
|
+
& Pick<NumberFieldPrimitive.Input.Props, 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'aria-invalid' | 'placeholder' | 'autoFocus'>
|
|
68
|
+
& {
|
|
69
|
+
decrementLabel: string
|
|
70
|
+
incrementLabel: string
|
|
71
|
+
surface?: 'bare' | 'well'
|
|
72
|
+
size?: 'sm' | 'md' | 'lg'
|
|
73
|
+
className?: string
|
|
74
|
+
}
|
|
101
75
|
|
|
102
|
-
export function NumberField(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
76
|
+
export function NumberField({
|
|
77
|
+
value,
|
|
78
|
+
defaultValue,
|
|
79
|
+
onValueChange,
|
|
80
|
+
onValueCommitted,
|
|
81
|
+
decrementLabel,
|
|
82
|
+
incrementLabel,
|
|
83
|
+
step = 1,
|
|
84
|
+
largeStep,
|
|
85
|
+
min,
|
|
86
|
+
max,
|
|
87
|
+
surface,
|
|
88
|
+
size,
|
|
89
|
+
disabled = false,
|
|
90
|
+
readOnly = false,
|
|
91
|
+
locale = 'en-US',
|
|
92
|
+
className,
|
|
93
|
+
'aria-label': ariaLabel,
|
|
94
|
+
'aria-labelledby': ariaLabelledBy,
|
|
95
|
+
'aria-describedby': ariaDescribedBy,
|
|
96
|
+
'aria-invalid': ariaInvalid,
|
|
97
|
+
placeholder,
|
|
98
|
+
autoFocus,
|
|
99
|
+
...rest
|
|
100
|
+
}: NumberFieldProps) {
|
|
101
|
+
const [own, setOwn] = useState<number | null>(defaultValue ?? null)
|
|
102
|
+
const controlled = value !== undefined
|
|
123
103
|
const current = controlled ? value : own
|
|
104
|
+
const unit = step === 'any' ? 1 : step
|
|
124
105
|
|
|
125
106
|
const ui = numberField({ surface, size, readonly: readOnly })
|
|
107
|
+
const stepButton = <button type="button" disabled={disabled || readOnly} />
|
|
126
108
|
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (!controlled) setOwn(
|
|
130
|
-
if (after !== current) onValueChange?.(after)
|
|
109
|
+
const change: NumberFieldPrimitive.Root.Props['onValueChange'] = (next, details) => {
|
|
110
|
+
onValueChange?.(next, details)
|
|
111
|
+
if (!details.isCanceled && !controlled) setOwn(next)
|
|
131
112
|
}
|
|
132
113
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const down = useHold(() => { setTyping(null); set(from - step) }, blocked || atMin)
|
|
139
|
-
const up = useHold(() => { setTyping(null); set(from + step) }, blocked || atMax)
|
|
140
|
-
|
|
141
|
-
const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
142
|
-
if (blocked) return
|
|
143
|
-
|
|
144
|
-
const moves: Record<string, number> = {
|
|
145
|
-
ArrowUp: from + step,
|
|
146
|
-
ArrowDown: from - step,
|
|
147
|
-
PageUp: from + step * 10,
|
|
148
|
-
PageDown: from - step * 10,
|
|
149
|
-
Home: min,
|
|
150
|
-
End: max,
|
|
114
|
+
const commit: NumberFieldPrimitive.Root.Props['onValueCommitted'] = (committed, details) => {
|
|
115
|
+
if (details.reason !== 'input-blur' || committed === null || step === 'any') {
|
|
116
|
+
onValueCommitted?.(committed, details)
|
|
117
|
+
return
|
|
151
118
|
}
|
|
152
119
|
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
120
|
+
const snapped = snap(committed, min, max, unit)
|
|
121
|
+
if (snapped !== committed) {
|
|
122
|
+
const changeDetails = blurDetails(details.event)
|
|
123
|
+
change(snapped, changeDetails)
|
|
124
|
+
if (changeDetails.isCanceled) return
|
|
125
|
+
}
|
|
155
126
|
|
|
156
|
-
|
|
157
|
-
setTyping(null)
|
|
158
|
-
set(next)
|
|
127
|
+
onValueCommitted?.(snapped, details)
|
|
159
128
|
}
|
|
160
129
|
|
|
161
|
-
const
|
|
162
|
-
e.
|
|
163
|
-
|
|
130
|
+
const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
131
|
+
const arrow = PAGE_KEYS[e.key]
|
|
132
|
+
if (!arrow || e.altKey || e.ctrlKey || e.metaKey) return
|
|
164
133
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
setTyping(min >= 0 ? raw.replace(/-/g, '') : raw)
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
e.currentTarget.dispatchEvent(new KeyboardEvent('keydown', { key: arrow, shiftKey: true, bubbles: true, cancelable: true }))
|
|
168
136
|
}
|
|
169
137
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
138
|
+
const inputProps = Object.fromEntries(
|
|
139
|
+
Object.entries({
|
|
140
|
+
'aria-label': ariaLabel,
|
|
141
|
+
'aria-labelledby': ariaLabelledBy,
|
|
142
|
+
'aria-describedby': ariaDescribedBy,
|
|
143
|
+
'aria-invalid': ariaInvalid,
|
|
144
|
+
placeholder,
|
|
145
|
+
autoFocus,
|
|
146
|
+
}).filter(([, v]) => v !== undefined),
|
|
147
|
+
) as NumberFieldPrimitive.Input.Props
|
|
180
148
|
|
|
181
149
|
return (
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<button
|
|
219
|
-
type="button"
|
|
220
|
-
aria-label={incrementLabel}
|
|
221
|
-
disabled={blocked}
|
|
222
|
-
aria-disabled={atMax || undefined}
|
|
223
|
-
data-pressed={up.pressed ? '' : undefined}
|
|
224
|
-
className={ui.step()}
|
|
225
|
-
onPointerDown={up.onPointerDown}
|
|
226
|
-
onPointerUp={up.onPointerUp}
|
|
227
|
-
onPointerCancel={up.onPointerCancel}
|
|
228
|
-
onLostPointerCapture={up.onLostPointerCapture}
|
|
229
|
-
>
|
|
230
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
|
|
231
|
-
<path d="M5 12h14" />
|
|
232
|
-
<path d="M12 5v14" />
|
|
233
|
-
</svg>
|
|
234
|
-
</button>
|
|
235
|
-
</div>
|
|
150
|
+
<NumberFieldPrimitive.Root
|
|
151
|
+
{...rest}
|
|
152
|
+
value={current}
|
|
153
|
+
onValueChange={change}
|
|
154
|
+
onValueCommitted={commit}
|
|
155
|
+
step={step}
|
|
156
|
+
largeStep={largeStep ?? unit * 10}
|
|
157
|
+
min={min}
|
|
158
|
+
max={max}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
readOnly={readOnly}
|
|
161
|
+
locale={locale}
|
|
162
|
+
className={className}
|
|
163
|
+
>
|
|
164
|
+
<NumberFieldPrimitive.Group className={ui.root()}>
|
|
165
|
+
<NumberFieldPrimitive.Decrement render={stepButton} aria-label={decrementLabel} className={ui.step()}>
|
|
166
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
|
|
167
|
+
<path d="M5 12h14" />
|
|
168
|
+
</svg>
|
|
169
|
+
</NumberFieldPrimitive.Decrement>
|
|
170
|
+
|
|
171
|
+
<NumberFieldPrimitive.Input
|
|
172
|
+
{...inputProps}
|
|
173
|
+
onKeyDown={onKeyDown}
|
|
174
|
+
className={ui.input()}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
<NumberFieldPrimitive.Increment render={stepButton} aria-label={incrementLabel} className={ui.step()}>
|
|
178
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
|
|
179
|
+
<path d="M5 12h14" />
|
|
180
|
+
<path d="M12 5v14" />
|
|
181
|
+
</svg>
|
|
182
|
+
</NumberFieldPrimitive.Increment>
|
|
183
|
+
</NumberFieldPrimitive.Group>
|
|
184
|
+
</NumberFieldPrimitive.Root>
|
|
236
185
|
)
|
|
237
186
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantoia/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pantoia Design System",
|
|
5
5
|
"homepage": "https://design.pantoia.com",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/pantoia/pantoia-
|
|
9
|
-
"directory": "packages/registry"
|
|
8
|
+
"url": "git+https://github.com/pantoia/pantoia-ui.git"
|
|
10
9
|
},
|
|
11
10
|
"license": "MIT",
|
|
12
11
|
"type": "module",
|
|
@@ -16,10 +15,8 @@
|
|
|
16
15
|
"files": [
|
|
17
16
|
"*/index.tsx",
|
|
18
17
|
"*/index.module.css",
|
|
19
|
-
"*/AGENTS.md",
|
|
20
18
|
"dist",
|
|
21
19
|
"tv.ts",
|
|
22
|
-
"test-utils.ts",
|
|
23
20
|
"tailwind.css",
|
|
24
21
|
"palette.css",
|
|
25
22
|
"tokens.css",
|
|
@@ -33,7 +30,6 @@
|
|
|
33
30
|
"./tokens.css": "./tokens.css",
|
|
34
31
|
"./utilities.css": "./utilities.css",
|
|
35
32
|
"./registry.json": "./registry.json",
|
|
36
|
-
"./test-utils": "./test-utils.ts",
|
|
37
33
|
"./*": "./*/index.tsx"
|
|
38
34
|
},
|
|
39
35
|
"sideEffects": [
|
|
@@ -47,11 +43,16 @@
|
|
|
47
43
|
},
|
|
48
44
|
"scripts": {
|
|
49
45
|
"prepack": "tsc cli/index.ts --ignoreConfig --outDir dist --target es2022 --module esnext --moduleResolution bundler --skipLibCheck --types node",
|
|
50
|
-
"typecheck": "tsc --noEmit"
|
|
46
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
|
|
47
|
+
"check": "tsx scripts/check.ts",
|
|
48
|
+
"lint": "oxlint --deny-warnings",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"verify": "npm run typecheck && npm run check && npm run lint && npm run test",
|
|
51
|
+
"dev:up": "docker compose up -d dev",
|
|
52
|
+
"dev:down": "docker compose down"
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
53
|
-
"
|
|
54
|
-
"sonner": "^2.0.8",
|
|
55
|
+
"@base-ui/react": "^1.8.0",
|
|
55
56
|
"tailwind-variants": "^3.3.1"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
@@ -60,12 +61,19 @@
|
|
|
60
61
|
"tailwindcss": "^4.0.0"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
|
-
"@
|
|
64
|
+
"@testing-library/dom": "^10.4.1",
|
|
65
|
+
"@testing-library/react": "^16.3.3",
|
|
66
|
+
"@testing-library/user-event": "^14.6.6",
|
|
64
67
|
"@types/node": "26.4.0",
|
|
65
68
|
"@types/react": "19.2.18",
|
|
66
69
|
"@types/react-dom": "19.2.5",
|
|
70
|
+
"happy-dom": "^20.12.0",
|
|
71
|
+
"oxlint": "1.81.0",
|
|
67
72
|
"react": "19.2.8",
|
|
68
73
|
"react-dom": "19.2.8",
|
|
69
|
-
"
|
|
74
|
+
"tailwindcss": "4.3.3",
|
|
75
|
+
"tsx": "4.23.12",
|
|
76
|
+
"typescript": "7.0.2",
|
|
77
|
+
"vitest": "^4.1.11"
|
|
70
78
|
}
|
|
71
79
|
}
|
package/popover/index.tsx
CHANGED
|
@@ -1,41 +1,58 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Popover as PopoverPrimitive } from '
|
|
4
|
-
import
|
|
3
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
|
|
4
|
+
import { useRender } from '@base-ui/react/use-render'
|
|
5
|
+
import { createContext, useContext, useState } from 'react'
|
|
6
|
+
import type { ComponentProps, Dispatch, SetStateAction } from 'react'
|
|
5
7
|
|
|
6
8
|
import { tv } from '../tv'
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
type AnchorState = [Element | null, Dispatch<SetStateAction<Element | null>>]
|
|
11
|
+
|
|
12
|
+
const AnchorContext = createContext<AnchorState>([null, () => {}])
|
|
13
|
+
|
|
14
|
+
export type PopoverProps = ComponentProps<typeof PopoverPrimitive.Root>
|
|
15
|
+
|
|
16
|
+
export function Popover(props: PopoverProps) {
|
|
17
|
+
const anchor = useState<Element | null>(null)
|
|
14
18
|
|
|
15
|
-
export function Popover({ open, defaultOpen, onOpenChange, children }: PopoverProps) {
|
|
16
19
|
return (
|
|
17
|
-
<
|
|
18
|
-
{
|
|
19
|
-
</
|
|
20
|
+
<AnchorContext.Provider value={anchor}>
|
|
21
|
+
<PopoverPrimitive.Root {...props} />
|
|
22
|
+
</AnchorContext.Provider>
|
|
20
23
|
)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
export
|
|
24
|
-
|
|
26
|
+
export type PopoverTriggerProps = ComponentProps<typeof PopoverPrimitive.Trigger>
|
|
27
|
+
|
|
28
|
+
export function PopoverTrigger(props: PopoverTriggerProps) {
|
|
29
|
+
return <PopoverPrimitive.Trigger {...props} />
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
export
|
|
28
|
-
|
|
32
|
+
export type PopoverAnchorProps = useRender.ComponentProps<'div'>
|
|
33
|
+
|
|
34
|
+
export function PopoverAnchor({ render, ref, ...rest }: PopoverAnchorProps) {
|
|
35
|
+
const [, setAnchor] = useContext(AnchorContext)
|
|
36
|
+
|
|
37
|
+
return useRender({
|
|
38
|
+
defaultTagName: 'div',
|
|
39
|
+
render,
|
|
40
|
+
ref: ref ? [setAnchor, ref] : setAnchor,
|
|
41
|
+
props: rest,
|
|
42
|
+
})
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
export
|
|
32
|
-
|
|
45
|
+
export type PopoverCloseProps = ComponentProps<typeof PopoverPrimitive.Close>
|
|
46
|
+
|
|
47
|
+
export function PopoverClose(props: PopoverCloseProps) {
|
|
48
|
+
return <PopoverPrimitive.Close {...props} />
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
export
|
|
51
|
+
export type PopoverContentProps = Omit<ComponentProps<typeof PopoverPrimitive.Popup>, 'className'> & {
|
|
36
52
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
37
53
|
align?: 'start' | 'center' | 'end'
|
|
38
54
|
arrow?: boolean
|
|
55
|
+
className?: string
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
const popoverContent = tv({
|
|
@@ -44,6 +61,14 @@ const popoverContent = tv({
|
|
|
44
61
|
+ ' focus-visible:outline-none',
|
|
45
62
|
})
|
|
46
63
|
|
|
64
|
+
const popoverArrow = tv({
|
|
65
|
+
base: 'data-uncentered:invisible'
|
|
66
|
+
+ ' side-top:top-full'
|
|
67
|
+
+ ' side-bottom:bottom-full side-bottom:rotate-180'
|
|
68
|
+
+ ' side-right:left-0 side-right:origin-top-left side-right:rotate-90 side-right:-translate-y-1/2'
|
|
69
|
+
+ ' side-left:right-0 side-left:origin-top-right side-left:-rotate-90 side-left:-translate-y-1/2',
|
|
70
|
+
})
|
|
71
|
+
|
|
47
72
|
export function PopoverContent({
|
|
48
73
|
side = 'bottom',
|
|
49
74
|
align = 'center',
|
|
@@ -52,25 +77,29 @@ export function PopoverContent({
|
|
|
52
77
|
children,
|
|
53
78
|
...rest
|
|
54
79
|
}: PopoverContentProps) {
|
|
80
|
+
const [anchor] = useContext(AnchorContext)
|
|
81
|
+
|
|
55
82
|
return (
|
|
56
83
|
<PopoverPrimitive.Portal>
|
|
57
|
-
<PopoverPrimitive.
|
|
58
|
-
{
|
|
84
|
+
<PopoverPrimitive.Positioner
|
|
85
|
+
anchor={anchor ?? undefined}
|
|
59
86
|
side={side}
|
|
60
87
|
align={align}
|
|
61
|
-
sideOffset={8}
|
|
88
|
+
sideOffset={arrow ? 14 : 8}
|
|
62
89
|
arrowPadding={16}
|
|
63
|
-
className=
|
|
90
|
+
className="z-overlay min-w-max will-change-transform"
|
|
64
91
|
>
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
<PopoverPrimitive.Popup {...rest} className={popoverContent({ className })}>
|
|
93
|
+
{children}
|
|
94
|
+
{arrow && (
|
|
95
|
+
<PopoverPrimitive.Arrow className={popoverArrow()}>
|
|
96
|
+
<svg width={12} height={6} viewBox="0 0 12 6" preserveAspectRatio="none" className="block fill-bg-panel -translate-y-px">
|
|
97
|
+
<path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
|
|
98
|
+
</svg>
|
|
99
|
+
</PopoverPrimitive.Arrow>
|
|
100
|
+
)}
|
|
101
|
+
</PopoverPrimitive.Popup>
|
|
102
|
+
</PopoverPrimitive.Positioner>
|
|
74
103
|
</PopoverPrimitive.Portal>
|
|
75
104
|
)
|
|
76
105
|
}
|
package/radio-group/index.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { Radio } from '@base-ui/react/radio'
|
|
4
|
+
import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'
|
|
5
|
+
import { cloneElement, createContext, use, useEffect, useId, useState, type ComponentProps, type ReactElement, type ReactNode } from 'react'
|
|
5
6
|
|
|
6
7
|
import { Label } from '../label'
|
|
7
8
|
import { tv } from '../tv'
|
|
@@ -14,6 +15,7 @@ export interface RadioItemContext {
|
|
|
14
15
|
id: string
|
|
15
16
|
labelId: string
|
|
16
17
|
descriptionId: string
|
|
18
|
+
setDescribed: (described: boolean) => void
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const ItemContext = createContext<RadioItemContext | null>(null)
|
|
@@ -44,69 +46,57 @@ export const radioGroupItem = tv({
|
|
|
44
46
|
defaultVariants: { color: 'neutral' },
|
|
45
47
|
})
|
|
46
48
|
|
|
47
|
-
export
|
|
48
|
-
value?: string
|
|
49
|
-
defaultValue?: string
|
|
50
|
-
onValueChange?: (value: string) => void
|
|
49
|
+
export type RadioGroupProps = Omit<RadioGroupPrimitive.Props<string>, 'className'> & {
|
|
51
50
|
color?: RadioColor
|
|
52
|
-
disabled?: boolean
|
|
53
51
|
className?: string
|
|
54
|
-
children?: ReactNode
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
const radioGroup = tv({ base: 'flex flex-col gap-3' })
|
|
58
55
|
|
|
59
|
-
export function RadioGroup({
|
|
60
|
-
value,
|
|
61
|
-
defaultValue,
|
|
62
|
-
onValueChange,
|
|
63
|
-
color = 'neutral',
|
|
64
|
-
disabled = false,
|
|
65
|
-
className,
|
|
66
|
-
children,
|
|
67
|
-
...rest
|
|
68
|
-
}: RadioGroupProps) {
|
|
56
|
+
export function RadioGroup({ color = 'neutral', className, ...rest }: RadioGroupProps) {
|
|
69
57
|
return (
|
|
70
58
|
<ColorContext value={color}>
|
|
71
|
-
<RadioGroupPrimitive
|
|
72
|
-
{...rest}
|
|
73
|
-
value={value}
|
|
74
|
-
defaultValue={defaultValue}
|
|
75
|
-
onValueChange={onValueChange}
|
|
76
|
-
disabled={disabled}
|
|
77
|
-
className={radioGroup({ className })}
|
|
78
|
-
>
|
|
79
|
-
{children}
|
|
80
|
-
</RadioGroupPrimitive.Root>
|
|
59
|
+
<RadioGroupPrimitive<string> {...rest} className={radioGroup({ className })} />
|
|
81
60
|
</ColorContext>
|
|
82
61
|
)
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
export
|
|
64
|
+
export type RadioGroupItemProps = Omit<ComponentProps<typeof Radio.Root>, 'className' | 'value'> & {
|
|
86
65
|
value: string
|
|
87
|
-
disabled?: boolean
|
|
88
66
|
className?: string
|
|
89
|
-
children?: ReactNode
|
|
90
67
|
}
|
|
91
68
|
|
|
92
|
-
export function RadioGroupItem({
|
|
69
|
+
export function RadioGroupItem({
|
|
70
|
+
value,
|
|
71
|
+
className,
|
|
72
|
+
children,
|
|
73
|
+
nativeButton = true,
|
|
74
|
+
render = <button type="button" />,
|
|
75
|
+
...rest
|
|
76
|
+
}: RadioGroupItemProps) {
|
|
93
77
|
const id = useId()
|
|
78
|
+
const [described, setDescribed] = useState(false)
|
|
94
79
|
const color = use(ColorContext)
|
|
95
80
|
const ui = radioGroupItem({ color })
|
|
81
|
+
const renderWithId: RadioGroupItemProps['render'] = typeof render === 'function'
|
|
82
|
+
? (props, state) => render({ ...props, id }, state)
|
|
83
|
+
: cloneElement(render as ReactElement<{ id?: string }>, { id })
|
|
96
84
|
|
|
97
85
|
return (
|
|
98
|
-
<ItemContext value={{ id, labelId: `${id}-label`, descriptionId: `${id}-description
|
|
86
|
+
<ItemContext value={{ id, labelId: `${id}-label`, descriptionId: `${id}-description`, setDescribed }}>
|
|
99
87
|
<div className={ui.root({ className })}>
|
|
100
|
-
<
|
|
88
|
+
<Radio.Root<string>
|
|
89
|
+
{...rest}
|
|
90
|
+
nativeButton={nativeButton}
|
|
91
|
+
render={renderWithId}
|
|
101
92
|
id={id}
|
|
102
93
|
value={value}
|
|
103
|
-
disabled={disabled}
|
|
104
94
|
aria-labelledby={`${id}-label`}
|
|
105
|
-
aria-describedby={`${id}-description`}
|
|
95
|
+
aria-describedby={described ? `${id}-description` : undefined}
|
|
106
96
|
className={ui.control()}
|
|
107
97
|
>
|
|
108
|
-
<
|
|
109
|
-
</
|
|
98
|
+
<Radio.Indicator className="text-fg-neutral-solid-contrast size-2.5 rounded-full bg-current" />
|
|
99
|
+
</Radio.Root>
|
|
110
100
|
|
|
111
101
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
112
102
|
{children}
|
|
@@ -130,6 +120,13 @@ const radioGroupItemDescription = tv({ base: 'text-caption text-fg-subtle' })
|
|
|
130
120
|
|
|
131
121
|
export function RadioGroupItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
|
|
132
122
|
const ctx = use(ItemContext)
|
|
123
|
+
const setDescribed = ctx?.setDescribed
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!setDescribed) return
|
|
127
|
+
setDescribed(true)
|
|
128
|
+
return () => setDescribed(false)
|
|
129
|
+
}, [setDescribed])
|
|
133
130
|
|
|
134
131
|
return (
|
|
135
132
|
<p
|