@pantoia/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/accordion/index.module.css +4 -4
- package/accordion/index.tsx +30 -58
- package/alert-dialog/index.tsx +38 -44
- package/badge/index.tsx +2 -2
- package/button/index.tsx +31 -30
- package/callout/index.tsx +29 -8
- package/checkbox/index.tsx +24 -88
- package/combobox/index.tsx +133 -323
- package/command-palette/index.tsx +55 -34
- package/dialog/index.tsx +30 -26
- package/dist/index.js +155 -27
- package/drawer/index.module.css +2 -2
- package/drawer/index.tsx +54 -132
- package/dropdown-menu/index.tsx +95 -100
- package/field/index.tsx +43 -94
- package/input/index.tsx +13 -9
- package/kbd/index.tsx +2 -2
- package/label/index.tsx +7 -9
- package/number-field/index.tsx +129 -176
- package/package.json +12 -15
- package/palette.css +114 -0
- package/popover/index.tsx +62 -33
- package/radio-group/index.tsx +36 -39
- package/registry.json +65 -169
- package/select/index.tsx +154 -76
- package/switch/index.tsx +13 -6
- package/table/index.tsx +5 -6
- package/tabs/index.module.css +5 -3
- package/tabs/index.tsx +26 -77
- package/tailwind.css +3 -0
- package/textarea/index.tsx +25 -13
- package/toast/index.module.css +187 -0
- package/toast/index.tsx +141 -46
- package/toggle/index.tsx +8 -7
- package/{styles.css → tokens.css} +82 -303
- package/tooltip/index.tsx +62 -31
- package/tv.ts +0 -1
- package/utilities.css +124 -0
- 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 -41
- package/checkbox/AGENTS.md +0 -54
- package/combobox/AGENTS.md +0 -150
- package/command-palette/AGENTS.md +0 -45
- package/dialog/AGENTS.md +0 -81
- package/drawer/AGENTS.md +0 -66
- package/dropdown-menu/AGENTS.md +0 -133
- package/field/AGENTS.md +0 -96
- package/input/AGENTS.md +0 -35
- package/kbd/AGENTS.md +0 -17
- package/label/AGENTS.md +0 -21
- package/number-field/AGENTS.md +0 -53
- package/popover/AGENTS.md +0 -64
- package/press.css +0 -37
- 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 -60
- 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/field/index.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, use, useId, type ReactNode } from 'react'
|
|
3
|
+
import { Field as FieldPrimitive } from '@base-ui/react/field'
|
|
5
4
|
|
|
6
5
|
import { Label } from '../label'
|
|
7
6
|
import { tv } from '../tv'
|
|
@@ -20,90 +19,41 @@ export const field = tv({
|
|
|
20
19
|
defaultVariants: { layout: 'stack' },
|
|
21
20
|
})
|
|
22
21
|
|
|
23
|
-
export
|
|
24
|
-
id: string
|
|
25
|
-
labelId: string
|
|
26
|
-
messageId: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface FieldControl {
|
|
30
|
-
id: string
|
|
31
|
-
'aria-labelledby': string
|
|
32
|
-
'aria-invalid': 'true' | undefined
|
|
33
|
-
'aria-describedby': string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const Context = createContext<FieldContext | null>(null)
|
|
37
|
-
|
|
38
|
-
export interface FieldProps {
|
|
39
|
-
invalid?: boolean
|
|
22
|
+
export type FieldProps = Omit<FieldPrimitive.Root.Props, 'className'> & {
|
|
40
23
|
layout?: 'stack' | 'inline' | 'split'
|
|
41
24
|
className?: string
|
|
42
|
-
children?: ReactNode | ((args: { control: FieldControl }) => ReactNode)
|
|
43
25
|
}
|
|
44
26
|
|
|
45
|
-
export function Field({
|
|
46
|
-
|
|
47
|
-
const ctx: FieldContext = {
|
|
48
|
-
id,
|
|
49
|
-
labelId: `${id}-label`,
|
|
50
|
-
messageId: `${id}-message`,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const control: FieldControl = {
|
|
54
|
-
'id': id,
|
|
55
|
-
'aria-labelledby': ctx.labelId,
|
|
56
|
-
'aria-invalid': invalid ? 'true' : undefined,
|
|
57
|
-
'aria-describedby': ctx.messageId,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<Context value={ctx}>
|
|
62
|
-
<div className={field({ layout, className })}>
|
|
63
|
-
{typeof children === 'function' ? children({ control }) : children}
|
|
64
|
-
</div>
|
|
65
|
-
</Context>
|
|
66
|
-
)
|
|
27
|
+
export function Field({ layout, className, ...rest }: FieldProps) {
|
|
28
|
+
return <FieldPrimitive.Root {...rest} className={field({ layout, className })} />
|
|
67
29
|
}
|
|
68
30
|
|
|
69
|
-
const fieldLabel = tv({ base: 'group-has-disabled:text-
|
|
31
|
+
const fieldLabel = tv({ base: 'group-has-disabled:text-fg-disabled' })
|
|
70
32
|
|
|
71
|
-
export
|
|
72
|
-
|
|
33
|
+
export type FieldLabelProps = Omit<FieldPrimitive.Label.Props, 'className'> & {
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
73
36
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
id={ctx?.labelId}
|
|
77
|
-
htmlFor={ctx?.id}
|
|
78
|
-
className={fieldLabel({ className })}
|
|
79
|
-
>
|
|
80
|
-
{children}
|
|
81
|
-
</Label>
|
|
82
|
-
)
|
|
37
|
+
export function FieldLabel({ render = <Label />, className, ...rest }: FieldLabelProps) {
|
|
38
|
+
return <FieldPrimitive.Label {...rest} render={render} className={fieldLabel({ className })} />
|
|
83
39
|
}
|
|
84
40
|
|
|
85
41
|
const fieldHelp = tv({
|
|
86
|
-
base: 'basis-full text-caption text-
|
|
87
|
-
+ ' group-has-disabled:text-
|
|
42
|
+
base: 'basis-full text-caption text-fg-subtle'
|
|
43
|
+
+ ' group-has-disabled:text-fg-disabled',
|
|
88
44
|
})
|
|
89
45
|
|
|
90
|
-
export
|
|
91
|
-
|
|
46
|
+
export type FieldHelpProps = Omit<FieldPrimitive.Description.Props, 'className'> & {
|
|
47
|
+
className?: string
|
|
48
|
+
}
|
|
92
49
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id={ctx?.messageId}
|
|
96
|
-
className={fieldHelp({ className })}
|
|
97
|
-
>
|
|
98
|
-
{children}
|
|
99
|
-
</p>
|
|
100
|
-
)
|
|
50
|
+
export function FieldHelp({ className, ...rest }: FieldHelpProps) {
|
|
51
|
+
return <FieldPrimitive.Description {...rest} className={fieldHelp({ className })} />
|
|
101
52
|
}
|
|
102
53
|
|
|
103
|
-
export
|
|
54
|
+
export type FieldErrorProps = Omit<FieldPrimitive.Error.Props, 'className'> & {
|
|
104
55
|
items?: string | string[]
|
|
105
56
|
className?: string
|
|
106
|
-
children?: ReactNode
|
|
107
57
|
}
|
|
108
58
|
|
|
109
59
|
const fieldError = tv({
|
|
@@ -111,39 +61,38 @@ const fieldError = tv({
|
|
|
111
61
|
+ ' flex items-start gap-tight',
|
|
112
62
|
})
|
|
113
63
|
|
|
114
|
-
export function FieldError({ items, className, children }: FieldErrorProps) {
|
|
115
|
-
const
|
|
116
|
-
const list = items == null ? [] : Array.isArray(items) ? items : [items]
|
|
64
|
+
export function FieldError({ items, match = true, render = <p />, className, children, ...rest }: FieldErrorProps) {
|
|
65
|
+
const list: string[] = items == null ? [] : Array.isArray(items) ? items : [items]
|
|
117
66
|
|
|
118
67
|
return (
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
68
|
+
<FieldPrimitive.Error {...rest} match={match} render={render} className={fieldError({ className })}>
|
|
69
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="mt-0.5 size-4 shrink-0">
|
|
70
|
+
<circle cx="12" cy="12" r="10" />
|
|
71
|
+
<line x1="12" x2="12" y1="8" y2="12" />
|
|
72
|
+
<line x1="12" x2="12.01" y1="16" y2="16" />
|
|
73
|
+
</svg>
|
|
124
74
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
125
75
|
{children ?? list.map(t => <span key={t}>{t}</span>)}
|
|
126
76
|
</span>
|
|
127
|
-
</
|
|
77
|
+
</FieldPrimitive.Error>
|
|
128
78
|
)
|
|
129
79
|
}
|
|
130
80
|
|
|
131
81
|
const fieldPending = tv({
|
|
132
|
-
base: 'basis-full text-caption text-
|
|
82
|
+
base: 'basis-full text-caption text-fg-subtle'
|
|
133
83
|
+ ' flex items-start gap-tight',
|
|
134
84
|
})
|
|
135
85
|
|
|
136
|
-
export
|
|
137
|
-
const ctx = use(Context)
|
|
86
|
+
export type FieldPendingProps = FieldHelpProps
|
|
138
87
|
|
|
88
|
+
export function FieldPending({ className, children, ...rest }: FieldPendingProps) {
|
|
139
89
|
return (
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<LoaderCircle className="mt-0.5 size-4 shrink-0 animate-spin motion-reduce:animate-none" />
|
|
90
|
+
<FieldPrimitive.Description {...rest} className={fieldPending({ className })}>
|
|
91
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="mt-0.5 size-4 shrink-0 animate-spin motion-reduce:animate-none">
|
|
92
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
93
|
+
</svg>
|
|
145
94
|
<span className="min-w-0">{children}</span>
|
|
146
|
-
</
|
|
95
|
+
</FieldPrimitive.Description>
|
|
147
96
|
)
|
|
148
97
|
}
|
|
149
98
|
|
|
@@ -152,16 +101,16 @@ const fieldSuccess = tv({
|
|
|
152
101
|
+ ' flex items-start gap-tight',
|
|
153
102
|
})
|
|
154
103
|
|
|
155
|
-
export
|
|
156
|
-
const ctx = use(Context)
|
|
104
|
+
export type FieldSuccessProps = FieldHelpProps
|
|
157
105
|
|
|
106
|
+
export function FieldSuccess({ className, children, ...rest }: FieldSuccessProps) {
|
|
158
107
|
return (
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
108
|
+
<FieldPrimitive.Description {...rest} className={fieldSuccess({ className })}>
|
|
109
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="mt-0.5 size-4 shrink-0">
|
|
110
|
+
<circle cx="12" cy="12" r="10" />
|
|
111
|
+
<path d="m16 9-5.5 5.5L8 12" />
|
|
112
|
+
</svg>
|
|
164
113
|
<span className="min-w-0">{children}</span>
|
|
165
|
-
</
|
|
114
|
+
</FieldPrimitive.Description>
|
|
166
115
|
)
|
|
167
116
|
}
|
package/input/index.tsx
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Input as InputPrimitive } from '@base-ui/react/input'
|
|
4
|
+
import type { ComponentProps } from 'react'
|
|
2
5
|
|
|
3
6
|
import { tv } from '../tv'
|
|
4
7
|
|
|
5
8
|
export const input = tv({
|
|
6
|
-
base: 'w-full rounded-r8 text-value text-
|
|
7
|
-
+ ' placeholder:text-
|
|
9
|
+
base: 'w-full rounded-r8 text-value text-fg'
|
|
10
|
+
+ ' placeholder:text-fg-placeholder'
|
|
8
11
|
+ ' invalid:ring-2 invalid:ring-line-danger'
|
|
9
|
-
+ ' read-only:text-
|
|
12
|
+
+ ' read-only:text-fg-muted'
|
|
10
13
|
+ ' transition-state'
|
|
11
14
|
+ ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
|
|
12
|
-
+ ' disabled:bg-bg-neutral-disabled disabled:text-
|
|
15
|
+
+ ' disabled:bg-bg-neutral-disabled disabled:text-fg-disabled disabled:cursor-not-allowed',
|
|
13
16
|
|
|
14
17
|
variants: {
|
|
15
18
|
surface: {
|
|
16
|
-
bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-
|
|
17
|
-
well: 'bg-
|
|
19
|
+
bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-bg-well',
|
|
20
|
+
well: 'bg-bg-well',
|
|
18
21
|
},
|
|
19
22
|
size: {
|
|
20
23
|
sm: 'h-8 px-3',
|
|
@@ -26,11 +29,12 @@ export const input = tv({
|
|
|
26
29
|
defaultVariants: { surface: 'bare', size: 'md' },
|
|
27
30
|
})
|
|
28
31
|
|
|
29
|
-
export
|
|
32
|
+
export type InputProps = Omit<ComponentProps<typeof InputPrimitive>, 'className' | 'size'> & {
|
|
30
33
|
surface?: 'bare' | 'well'
|
|
31
34
|
size?: 'sm' | 'md' | 'lg'
|
|
35
|
+
className?: string
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export function Input({ surface, size, className, ...rest }: InputProps) {
|
|
35
|
-
return <
|
|
39
|
+
return <InputPrimitive {...rest} className={input({ surface, size, className })} />
|
|
36
40
|
}
|
package/kbd/index.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import type { HTMLAttributes } from 'react'
|
|
|
3
3
|
import { tv } from '../tv'
|
|
4
4
|
|
|
5
5
|
const kbd = tv({
|
|
6
|
-
base: 'bg-
|
|
7
|
-
+ ' border border-
|
|
6
|
+
base: 'bg-bg-neutral-solid/10 text-fg text-micro rounded-r4 inline-flex items-center'
|
|
7
|
+
+ ' border border-transparent px-1.5 py-0.5 font-mono',
|
|
8
8
|
})
|
|
9
9
|
|
|
10
10
|
export function Kbd({ className, ...rest }: HTMLAttributes<HTMLElement>) {
|
package/label/index.tsx
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { Label as LabelPrimitive } from 'radix-ui'
|
|
2
1
|
import type { ComponentProps } from 'react'
|
|
3
2
|
|
|
4
3
|
import { tv } from '../tv'
|
|
5
4
|
|
|
6
|
-
const labelText = tv({ base: 'text-label text-
|
|
5
|
+
const labelText = tv({ base: 'text-label text-fg' })
|
|
7
6
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
7
|
+
export type LabelProps = Omit<ComponentProps<'label'>, 'className'> & {
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Label({ className, ...rest }: LabelProps) {
|
|
12
|
+
return <label {...rest} className={labelText({ className })} />
|
|
15
13
|
}
|
package/number-field/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { NumberField as NumberFieldPrimitive } from '@base-ui/react/number-field'
|
|
4
|
+
import { useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
5
5
|
|
|
6
6
|
import { tv } from '../tv'
|
|
7
7
|
|
|
@@ -13,18 +13,18 @@ export const numberField = tv({
|
|
|
13
13
|
+ ' has-disabled:bg-bg-neutral-disabled has-disabled:cursor-not-allowed'
|
|
14
14
|
+ ' has-focus-visible:outline-2 has-focus-visible:outline-offset-2'
|
|
15
15
|
+ ' has-focus-visible:outline-line-focus-ring',
|
|
16
|
-
step: 'text-
|
|
16
|
+
step: 'text-fg-muted grid shrink-0 place-items-center rounded-r8'
|
|
17
17
|
+ ' transition-state press press-shrink enabled:not-aria-disabled:hover:bg-bg-neutral-hover'
|
|
18
|
-
+ ' disabled:text-
|
|
19
|
-
+ ' aria-disabled:text-
|
|
20
|
-
input: 'text-value text-
|
|
21
|
-
+ ' disabled:text-
|
|
18
|
+
+ ' disabled:text-fg-disabled disabled:cursor-not-allowed'
|
|
19
|
+
+ ' aria-disabled:text-fg-disabled aria-disabled:cursor-not-allowed',
|
|
20
|
+
input: 'text-value text-fg w-full min-w-0 bg-transparent text-center outline-none'
|
|
21
|
+
+ ' disabled:text-fg-disabled disabled:cursor-not-allowed',
|
|
22
22
|
},
|
|
23
23
|
|
|
24
24
|
variants: {
|
|
25
25
|
surface: {
|
|
26
26
|
bare: { root: 'bg-transparent ring-1 ring-inset ring-line-strong' },
|
|
27
|
-
well: { root: 'bg-
|
|
27
|
+
well: { root: 'bg-bg-well' },
|
|
28
28
|
},
|
|
29
29
|
size: {
|
|
30
30
|
sm: { root: 'h-8', step: 'size-8' },
|
|
@@ -32,202 +32,155 @@ export const numberField = tv({
|
|
|
32
32
|
lg: { root: 'h-10', step: 'size-10', input: 'text-t16' },
|
|
33
33
|
},
|
|
34
34
|
readonly: {
|
|
35
|
-
true: { root: 'bg-
|
|
35
|
+
true: { root: 'bg-bg-well', input: 'text-fg-muted' },
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
defaultVariants: { surface: 'bare', size: 'md' },
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n))
|
|
46
|
-
|
|
47
|
-
const GROUPED = new Intl.NumberFormat('en-US')
|
|
48
|
-
|
|
49
|
-
const TYPING = /[^0-9.-]/g
|
|
50
|
-
|
|
51
|
-
function snap(n: number, min: number, step: number) {
|
|
52
|
-
const from = Number.isFinite(min) ? min : 0
|
|
53
|
-
return from + Math.round((n - from) / step) * step
|
|
42
|
+
const PAGE_KEYS: Record<string, 'ArrowUp' | 'ArrowDown'> = {
|
|
43
|
+
PageUp: 'ArrowUp',
|
|
44
|
+
PageDown: 'ArrowDown',
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
useEffect(() => { latest.current = step })
|
|
63
|
-
|
|
64
|
-
const stop = () => {
|
|
65
|
-
clearTimeout(timer.current)
|
|
66
|
-
clearInterval(beat.current)
|
|
67
|
-
setPressed(false)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
useEffect(() => stop, [])
|
|
71
|
-
|
|
72
|
-
const onPointerDown = (e: ReactPointerEvent<HTMLButtonElement>) => {
|
|
73
|
-
if (e.button !== 0 || blocked) return
|
|
74
|
-
|
|
75
|
-
latest.current()
|
|
76
|
-
setPressed(true)
|
|
77
|
-
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
|
+
}
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 },
|
|
82
62
|
}
|
|
83
|
-
|
|
84
|
-
return { pressed, onPointerDown, onPointerUp: stop, onPointerCancel: stop, onLostPointerCapture: stop }
|
|
63
|
+
return details
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
surface?: 'bare' | 'well'
|
|
97
|
-
size?: 'sm' | 'md' | 'lg'
|
|
98
|
-
disabled?: boolean
|
|
99
|
-
readOnly?: boolean
|
|
100
|
-
className?: string
|
|
101
|
-
}
|
|
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
|
+
}
|
|
102
75
|
|
|
103
|
-
export function NumberField(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
103
|
const current = controlled ? value : own
|
|
104
|
+
const unit = step === 'any' ? 1 : step
|
|
125
105
|
|
|
126
106
|
const ui = numberField({ surface, size, readonly: readOnly })
|
|
107
|
+
const stepButton = <button type="button" disabled={disabled || readOnly} />
|
|
127
108
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (!controlled) setOwn(
|
|
131
|
-
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)
|
|
132
112
|
}
|
|
133
113
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const down = useHold(() => { setTyping(null); set(from - step) }, blocked || atMin)
|
|
140
|
-
const up = useHold(() => { setTyping(null); set(from + step) }, blocked || atMax)
|
|
141
|
-
|
|
142
|
-
const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
143
|
-
if (blocked) return
|
|
144
|
-
|
|
145
|
-
const moves: Record<string, number> = {
|
|
146
|
-
ArrowUp: from + step,
|
|
147
|
-
ArrowDown: from - step,
|
|
148
|
-
PageUp: from + step * 10,
|
|
149
|
-
PageDown: from - step * 10,
|
|
150
|
-
Home: min,
|
|
151
|
-
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
|
|
152
118
|
}
|
|
153
119
|
|
|
154
|
-
const
|
|
155
|
-
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
|
+
}
|
|
156
126
|
|
|
157
|
-
|
|
158
|
-
setTyping(null)
|
|
159
|
-
set(next)
|
|
127
|
+
onValueCommitted?.(snapped, details)
|
|
160
128
|
}
|
|
161
129
|
|
|
162
|
-
const
|
|
163
|
-
e.
|
|
164
|
-
|
|
130
|
+
const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
131
|
+
const arrow = PAGE_KEYS[e.key]
|
|
132
|
+
if (!arrow || e.altKey || e.ctrlKey || e.metaKey) return
|
|
165
133
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
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 }))
|
|
169
136
|
}
|
|
170
137
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
181
148
|
|
|
182
149
|
return (
|
|
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
|
-
type="button"
|
|
219
|
-
aria-label={incrementLabel}
|
|
220
|
-
disabled={blocked}
|
|
221
|
-
aria-disabled={atMax || undefined}
|
|
222
|
-
data-pressed={up.pressed ? '' : undefined}
|
|
223
|
-
className={ui.step()}
|
|
224
|
-
onPointerDown={up.onPointerDown}
|
|
225
|
-
onPointerUp={up.onPointerUp}
|
|
226
|
-
onPointerCancel={up.onPointerCancel}
|
|
227
|
-
onLostPointerCapture={up.onLostPointerCapture}
|
|
228
|
-
>
|
|
229
|
-
<Plus className="size-4" />
|
|
230
|
-
</button>
|
|
231
|
-
</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>
|
|
232
185
|
)
|
|
233
186
|
}
|