@duro-app/ui 0.35.2 → 0.35.3
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/dist/components/ColorInput/ColorInput.d.ts +26 -0
- package/dist/components/ColorInput/ColorInput.d.ts.map +1 -0
- package/dist/components/ColorInput/ColorInput.meta.d.ts +3 -0
- package/dist/components/ColorInput/ColorInput.meta.d.ts.map +1 -0
- package/dist/components/ColorInput/styles.css.d.ts +26 -0
- package/dist/components/ColorInput/styles.css.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +39 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.meta.d.ts +3 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.meta.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +20 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +3 -0
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2655 -2530
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ColorInput/ColorInput.meta.ts +22 -0
- package/src/components/ColorInput/ColorInput.tsx +73 -0
- package/src/components/ColorInput/styles.css.ts +40 -0
- package/src/components/ConfirmDialog/ConfirmDialog.meta.ts +37 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +99 -0
- package/src/components/Input/Input.tsx +51 -5
- package/src/components/Input/styles.css.ts +5 -0
- package/src/components/Select/Select.tsx +11 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@duro-app/tokens": "^0.35.
|
|
57
|
+
"@duro-app/tokens": "^0.35.3"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'A styled native color swatch (<input type="color">) for picking a hex color. Field-context aware like Input.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Picking an accent/brand/category color in a form (e.g. a zone or tag color)',
|
|
8
|
+
'Inside a Field.Root, where it adopts the label, invalid state, and disabled',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Free-text hex entry with validation — use Input with a pattern instead',
|
|
12
|
+
'A full palette/gradient picker — out of scope; this is the native swatch',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Input', relationship: 'Sibling form control; same Field-context contract'},
|
|
16
|
+
{component: 'Field', relationship: 'Wrap in Field.Root for label + error wiring'},
|
|
17
|
+
],
|
|
18
|
+
example: `<Field.Root>
|
|
19
|
+
<Field.Label>Accent</Field.Label>
|
|
20
|
+
<ColorInput value={accent} onChange={(e) => setAccent(e.target.value)} />
|
|
21
|
+
</Field.Root>`,
|
|
22
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {html} from 'react-strict-dom'
|
|
2
|
+
import {useFieldContext} from '../Field/FieldContext'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
interface ColorInputProps {
|
|
6
|
+
/** Hex color, controlled (e.g. "#cfc8ba"). */
|
|
7
|
+
value?: string
|
|
8
|
+
defaultValue?: string
|
|
9
|
+
name?: string
|
|
10
|
+
/** Explicit id — overrides the Field-context controlId when set. */
|
|
11
|
+
id?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
required?: boolean
|
|
14
|
+
/** Accessible name when there is no associated <Field.Label>. */
|
|
15
|
+
'aria-label'?: string
|
|
16
|
+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
17
|
+
onBlur?: () => void
|
|
18
|
+
ref?: React.Ref<HTMLInputElement>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A styled native `<input type="color">` swatch. Mirrors <Input>'s native
|
|
23
|
+
* surface (value/defaultValue/name/onChange/ref/disabled/required) and adopts
|
|
24
|
+
* the parent <Field.Root> context (controlId for label association, value/
|
|
25
|
+
* onChange when bound to a Form, invalid border, disabled) — so it drops into a
|
|
26
|
+
* <Field> exactly like <Input>. The text <Input> can't render type=color, hence
|
|
27
|
+
* a dedicated leaf.
|
|
28
|
+
*/
|
|
29
|
+
export function ColorInput({
|
|
30
|
+
value,
|
|
31
|
+
defaultValue,
|
|
32
|
+
name,
|
|
33
|
+
id,
|
|
34
|
+
disabled,
|
|
35
|
+
required,
|
|
36
|
+
'aria-label': ariaLabel,
|
|
37
|
+
onChange,
|
|
38
|
+
onBlur,
|
|
39
|
+
ref,
|
|
40
|
+
}: ColorInputProps) {
|
|
41
|
+
const ctx = useFieldContext()
|
|
42
|
+
const fieldCtx = ctx?.field
|
|
43
|
+
|
|
44
|
+
const effectiveName = name ?? fieldCtx?.name
|
|
45
|
+
const effectiveValue = value ?? (fieldCtx ? String(fieldCtx.value ?? '') : undefined)
|
|
46
|
+
const effectiveOnChange = onChange ?? fieldCtx?.onChange
|
|
47
|
+
const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
|
|
48
|
+
const effectiveRef = ref ?? fieldCtx?.ref
|
|
49
|
+
const effectiveDisabled = disabled ?? ctx?.disabled
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<html.input
|
|
53
|
+
type="color"
|
|
54
|
+
id={id ?? ctx?.controlId}
|
|
55
|
+
name={effectiveName}
|
|
56
|
+
value={effectiveValue}
|
|
57
|
+
defaultValue={defaultValue}
|
|
58
|
+
disabled={effectiveDisabled}
|
|
59
|
+
required={required}
|
|
60
|
+
aria-label={ariaLabel}
|
|
61
|
+
aria-invalid={ctx?.invalid || undefined}
|
|
62
|
+
aria-describedby={
|
|
63
|
+
ctx
|
|
64
|
+
? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() || undefined
|
|
65
|
+
: undefined
|
|
66
|
+
}
|
|
67
|
+
onChange={effectiveOnChange as React.ComponentProps<typeof html.input>['onChange']}
|
|
68
|
+
onBlur={effectiveOnBlur as React.ComponentProps<typeof html.input>['onBlur']}
|
|
69
|
+
ref={effectiveRef as React.Ref<HTMLInputElement>}
|
|
70
|
+
style={[styles.base, ctx?.invalid && styles.invalid, effectiveDisabled && styles.disabled]}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
|
|
5
|
+
export const styles = css.create({
|
|
6
|
+
base: {
|
|
7
|
+
width: 44,
|
|
8
|
+
height: 34,
|
|
9
|
+
padding: 2,
|
|
10
|
+
backgroundColor: colors.bg,
|
|
11
|
+
borderWidth: 1,
|
|
12
|
+
borderStyle: 'solid',
|
|
13
|
+
borderColor: {
|
|
14
|
+
default: colors.border,
|
|
15
|
+
':hover': colors.textMuted,
|
|
16
|
+
},
|
|
17
|
+
borderRadius: radii.sm,
|
|
18
|
+
cursor: 'pointer',
|
|
19
|
+
outlineWidth: {
|
|
20
|
+
default: 0,
|
|
21
|
+
':focus-visible': 2,
|
|
22
|
+
},
|
|
23
|
+
outlineStyle: {
|
|
24
|
+
default: 'none',
|
|
25
|
+
':focus-visible': 'solid',
|
|
26
|
+
},
|
|
27
|
+
outlineColor: {
|
|
28
|
+
default: 'transparent',
|
|
29
|
+
':focus-visible': colors.accent,
|
|
30
|
+
},
|
|
31
|
+
outlineOffset: 1,
|
|
32
|
+
},
|
|
33
|
+
invalid: {
|
|
34
|
+
borderColor: colors.error,
|
|
35
|
+
},
|
|
36
|
+
disabled: {
|
|
37
|
+
cursor: 'not-allowed',
|
|
38
|
+
opacity: 0.5,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Destructive-confirmation dialog with an optional type-a-phrase gate. DS owns layout + gating; the app owns the submit (confirmSlot) so it composes with form libraries.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Confirming an irreversible action (delete org, delete account, drop a record with cascades)',
|
|
8
|
+
'High-impact actions that should require typing a phrase (the slug/name) before enabling confirm',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Routine confirmations — use a Dialog with a plain confirm button',
|
|
12
|
+
'Non-destructive flows — a Dialog or inline action is enough',
|
|
13
|
+
],
|
|
14
|
+
anatomy: {
|
|
15
|
+
required: ['title', 'children (warning body)'],
|
|
16
|
+
optional: ['confirmPhrase', 'promptLabel', 'confirmSlot', 'onConfirm', 'confirmLabel'],
|
|
17
|
+
},
|
|
18
|
+
relatedTo: [
|
|
19
|
+
{component: 'Dialog', relationship: 'Built on Dialog; this adds the phrase gate + footer'},
|
|
20
|
+
{component: 'Button', relationship: 'The confirm/cancel actions'},
|
|
21
|
+
],
|
|
22
|
+
example: `<ConfirmDialog
|
|
23
|
+
open={open}
|
|
24
|
+
onOpenChange={setOpen}
|
|
25
|
+
title="Delete organization?"
|
|
26
|
+
confirmPhrase={org.name}
|
|
27
|
+
promptLabel={<>Type <b>{org.name}</b> to confirm</>}
|
|
28
|
+
confirmSlot={({satisfied}) => (
|
|
29
|
+
<fetcher.Form method="post">
|
|
30
|
+
<input type="hidden" name="intent" value="delete-org" />
|
|
31
|
+
<Button type="submit" variant="danger" disabled={!satisfied}>Delete</Button>
|
|
32
|
+
</fetcher.Form>
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
This deletes the org and all its landscapes. This cannot be undone.
|
|
36
|
+
</ConfirmDialog>`,
|
|
37
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {type ReactNode, useState} from 'react'
|
|
2
|
+
import {Dialog} from '../Dialog/Dialog'
|
|
3
|
+
import {Button} from '../Button/Button'
|
|
4
|
+
import {Input} from '../Input/Input'
|
|
5
|
+
import {Field} from '../Field/Field'
|
|
6
|
+
|
|
7
|
+
type DialogSize = 'sm' | 'md' | 'lg'
|
|
8
|
+
|
|
9
|
+
interface ConfirmDialogProps {
|
|
10
|
+
open: boolean
|
|
11
|
+
onOpenChange: (open: boolean) => void
|
|
12
|
+
title: ReactNode
|
|
13
|
+
/** Warning / cascade body. */
|
|
14
|
+
children?: ReactNode
|
|
15
|
+
/** When set, the confirm action stays DISABLED until the user types this
|
|
16
|
+
* exact phrase (org name, slug…). The typed value is a client-side gate
|
|
17
|
+
* only — it is never submitted. */
|
|
18
|
+
confirmPhrase?: string
|
|
19
|
+
/** Prompt shown above the phrase input. */
|
|
20
|
+
promptLabel?: ReactNode
|
|
21
|
+
cancelLabel?: ReactNode
|
|
22
|
+
/**
|
|
23
|
+
* App-owned confirm control. Receives whether the phrase gate is satisfied,
|
|
24
|
+
* so the app can render its OWN <Button type="submit"> INSIDE its form
|
|
25
|
+
* (e.g. a react-router fetcher.Form with hidden intent fields) — the DS does
|
|
26
|
+
* NOT own the submit, avoiding a fight with the app's form flow. Takes
|
|
27
|
+
* precedence over onConfirm/confirmLabel.
|
|
28
|
+
*/
|
|
29
|
+
confirmSlot?: (gate: {satisfied: boolean}) => ReactNode
|
|
30
|
+
/** Simple case (no app form): the DS renders a danger Button calling this. */
|
|
31
|
+
onConfirm?: () => void
|
|
32
|
+
confirmLabel?: ReactNode
|
|
33
|
+
size?: DialogSize
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generic destructive-confirmation dialog: layout + an optional type-a-phrase
|
|
38
|
+
* gate. The DS owns the shell and the gate; the APP owns the actual submit
|
|
39
|
+
* (via `confirmSlot`) so it composes with form libraries instead of forcing a
|
|
40
|
+
* callback. Domain warning text goes in `children`.
|
|
41
|
+
*/
|
|
42
|
+
export function ConfirmDialog({
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
title,
|
|
46
|
+
children,
|
|
47
|
+
confirmPhrase,
|
|
48
|
+
promptLabel,
|
|
49
|
+
cancelLabel = 'Cancel',
|
|
50
|
+
confirmSlot,
|
|
51
|
+
onConfirm,
|
|
52
|
+
confirmLabel = 'Confirm',
|
|
53
|
+
size = 'sm',
|
|
54
|
+
}: ConfirmDialogProps) {
|
|
55
|
+
const [typed, setTyped] = useState('')
|
|
56
|
+
const satisfied = !confirmPhrase || typed.trim() === confirmPhrase
|
|
57
|
+
|
|
58
|
+
const handleOpenChange = (next: boolean) => {
|
|
59
|
+
if (!next) setTyped('') // reset the gate when the dialog closes
|
|
60
|
+
onOpenChange(next)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
|
|
65
|
+
<Dialog.Portal size={size}>
|
|
66
|
+
<Dialog.Header>
|
|
67
|
+
<Dialog.Title>{title}</Dialog.Title>
|
|
68
|
+
</Dialog.Header>
|
|
69
|
+
<Dialog.Body>
|
|
70
|
+
{children}
|
|
71
|
+
{confirmPhrase ? (
|
|
72
|
+
<Field.Root>
|
|
73
|
+
{promptLabel ? <Field.Label>{promptLabel}</Field.Label> : null}
|
|
74
|
+
<Input
|
|
75
|
+
font="mono"
|
|
76
|
+
value={typed}
|
|
77
|
+
onChange={(e) => setTyped(e.target.value)}
|
|
78
|
+
autoFocus
|
|
79
|
+
aria-label="confirmation phrase"
|
|
80
|
+
/>
|
|
81
|
+
</Field.Root>
|
|
82
|
+
) : null}
|
|
83
|
+
</Dialog.Body>
|
|
84
|
+
<Dialog.Footer>
|
|
85
|
+
<Button variant="secondary" onClick={() => handleOpenChange(false)}>
|
|
86
|
+
{cancelLabel}
|
|
87
|
+
</Button>
|
|
88
|
+
{confirmSlot ? (
|
|
89
|
+
confirmSlot({satisfied})
|
|
90
|
+
) : onConfirm ? (
|
|
91
|
+
<Button variant="danger" disabled={!satisfied} onClick={onConfirm}>
|
|
92
|
+
{confirmLabel}
|
|
93
|
+
</Button>
|
|
94
|
+
) : null}
|
|
95
|
+
</Dialog.Footer>
|
|
96
|
+
</Dialog.Portal>
|
|
97
|
+
</Dialog.Root>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -10,11 +10,26 @@ export type InputVariant = 'default' | 'error'
|
|
|
10
10
|
|
|
11
11
|
interface InputProps {
|
|
12
12
|
variant?: InputVariant
|
|
13
|
+
/** Orthogonal to variant — `mono` keeps the error border but uses the
|
|
14
|
+
* monospace family (slugs, identifiers, hashes). */
|
|
15
|
+
font?: 'mono'
|
|
13
16
|
type?: InputType
|
|
17
|
+
/** Explicit id — overrides the Field-context controlId when set (e.g. a
|
|
18
|
+
* control used outside a Field that still needs a stable id). */
|
|
19
|
+
id?: string
|
|
14
20
|
name?: string
|
|
15
21
|
placeholder?: string
|
|
16
22
|
required?: boolean
|
|
23
|
+
readOnly?: boolean
|
|
17
24
|
minLength?: number
|
|
25
|
+
maxLength?: number
|
|
26
|
+
/** Numeric/range hints — forwarded to the native input (date/number/etc). */
|
|
27
|
+
min?: string | number
|
|
28
|
+
max?: string | number
|
|
29
|
+
step?: string | number
|
|
30
|
+
inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
|
|
31
|
+
/** id of a <datalist> for native typeahead. */
|
|
32
|
+
list?: string
|
|
18
33
|
pattern?: string
|
|
19
34
|
autoComplete?:
|
|
20
35
|
| 'on'
|
|
@@ -30,6 +45,10 @@ interface InputProps {
|
|
|
30
45
|
defaultValue?: string
|
|
31
46
|
disabled?: boolean
|
|
32
47
|
autoFocus?: boolean
|
|
48
|
+
/** Accessible name when there's no associated <Field.Label>. */
|
|
49
|
+
'aria-label'?: string
|
|
50
|
+
/** Extra describedby ids (merged with the Field context's, if any). */
|
|
51
|
+
'aria-describedby'?: string
|
|
33
52
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
34
53
|
onBlur?: () => void
|
|
35
54
|
ref?: React.Ref<HTMLInputElement>
|
|
@@ -37,17 +56,28 @@ interface InputProps {
|
|
|
37
56
|
|
|
38
57
|
export function Input({
|
|
39
58
|
variant = 'default',
|
|
59
|
+
font,
|
|
40
60
|
type = 'text',
|
|
61
|
+
id,
|
|
41
62
|
name,
|
|
42
63
|
placeholder,
|
|
43
64
|
required,
|
|
65
|
+
readOnly,
|
|
44
66
|
minLength,
|
|
67
|
+
maxLength,
|
|
68
|
+
min,
|
|
69
|
+
max,
|
|
70
|
+
step,
|
|
71
|
+
inputMode,
|
|
72
|
+
list,
|
|
45
73
|
pattern,
|
|
46
74
|
autoComplete,
|
|
47
75
|
value,
|
|
48
76
|
defaultValue,
|
|
49
77
|
disabled,
|
|
50
78
|
autoFocus,
|
|
79
|
+
'aria-label': ariaLabel,
|
|
80
|
+
'aria-describedby': ariaDescribedby,
|
|
51
81
|
onChange,
|
|
52
82
|
onBlur,
|
|
53
83
|
ref,
|
|
@@ -63,16 +93,29 @@ export function Input({
|
|
|
63
93
|
const effectiveRef = ref ?? fieldCtx?.ref
|
|
64
94
|
const effectiveDisabled = disabled ?? ctx?.disabled
|
|
65
95
|
const effectiveVariant = ctx?.invalid ? 'error' : variant
|
|
96
|
+
// Field context owns describedby (description + error); fall back to the
|
|
97
|
+
// explicit prop only when standalone.
|
|
98
|
+
const describedBy = ctx
|
|
99
|
+
? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim()
|
|
100
|
+
: ariaDescribedby
|
|
66
101
|
|
|
67
102
|
// react-strict-dom omits some web-only props from its types, but the
|
|
68
103
|
// underlying DOM element supports them. Type-assert to pass them through.
|
|
69
104
|
const extraProps: Record<string, unknown> = {}
|
|
70
105
|
if (pattern !== undefined) extraProps.pattern = pattern
|
|
71
106
|
if (autoFocus) extraProps.autoFocus = true
|
|
107
|
+
if (readOnly) extraProps.readOnly = true
|
|
108
|
+
if (maxLength !== undefined) extraProps.maxLength = maxLength
|
|
109
|
+
if (min !== undefined) extraProps.min = min
|
|
110
|
+
if (max !== undefined) extraProps.max = max
|
|
111
|
+
if (step !== undefined) extraProps.step = step
|
|
112
|
+
if (inputMode !== undefined) extraProps.inputMode = inputMode
|
|
113
|
+
if (list !== undefined) extraProps.list = list
|
|
114
|
+
if (ariaLabel !== undefined) extraProps['aria-label'] = ariaLabel
|
|
72
115
|
|
|
73
116
|
return (
|
|
74
117
|
<html.input
|
|
75
|
-
id={ctx?.controlId}
|
|
118
|
+
id={id ?? ctx?.controlId}
|
|
76
119
|
type={type}
|
|
77
120
|
name={effectiveName}
|
|
78
121
|
placeholder={placeholder}
|
|
@@ -82,14 +125,17 @@ export function Input({
|
|
|
82
125
|
value={effectiveValue}
|
|
83
126
|
defaultValue={defaultValue}
|
|
84
127
|
disabled={effectiveDisabled}
|
|
85
|
-
aria-describedby={
|
|
86
|
-
ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
|
|
87
|
-
}
|
|
128
|
+
aria-describedby={describedBy || undefined}
|
|
88
129
|
aria-invalid={ctx?.invalid || variant === 'error' || undefined}
|
|
89
130
|
onChange={effectiveOnChange as StrictInputProps['onChange']}
|
|
90
131
|
onBlur={effectiveOnBlur as StrictInputProps['onBlur']}
|
|
91
132
|
ref={effectiveRef as React.Ref<HTMLInputElement>}
|
|
92
|
-
style={[
|
|
133
|
+
style={[
|
|
134
|
+
styles.base,
|
|
135
|
+
styles[effectiveVariant],
|
|
136
|
+
font === 'mono' && styles.mono,
|
|
137
|
+
groupCtx?.inGroup && styles.inGroup,
|
|
138
|
+
]}
|
|
93
139
|
{...extraProps}
|
|
94
140
|
/>
|
|
95
141
|
)
|
|
@@ -56,4 +56,9 @@ export const styles = css.create({
|
|
|
56
56
|
borderRadius: 0,
|
|
57
57
|
outlineWidth: 0,
|
|
58
58
|
},
|
|
59
|
+
// orthogonal to variant (default/error) — composes on top so a mono field
|
|
60
|
+
// still shows its error border. For slugs, identifiers, hashes.
|
|
61
|
+
mono: {
|
|
62
|
+
fontFamily: typography.fontFamilyMono,
|
|
63
|
+
},
|
|
59
64
|
})
|
|
@@ -3,6 +3,7 @@ import {html} from 'react-strict-dom'
|
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
4
|
import {SelectContext, useSelect} from './SelectContext'
|
|
5
5
|
import {useSelectRoot} from './useSelectRoot'
|
|
6
|
+
import {useFieldContext} from '../Field/FieldContext'
|
|
6
7
|
|
|
7
8
|
// --- Root ---
|
|
8
9
|
interface RootProps {
|
|
@@ -37,6 +38,13 @@ function Trigger({
|
|
|
37
38
|
}) {
|
|
38
39
|
const {open, toggle, listboxId, highlightedId, triggerRef} = useSelect()
|
|
39
40
|
const localRef = useRef<HTMLButtonElement>(null)
|
|
41
|
+
// Inside a <Field.Root>, adopt its controlId so <Field.Label for={controlId}>
|
|
42
|
+
// labels the trigger (a labelable <button>) — no manual aria-label needed; and
|
|
43
|
+
// surface the field's description/error + invalid state, mirroring <Input>.
|
|
44
|
+
const fieldCtx = useFieldContext()
|
|
45
|
+
const describedBy = fieldCtx
|
|
46
|
+
? `${fieldCtx.descriptionId} ${fieldCtx.invalid ? fieldCtx.errorId : ''}`.trim()
|
|
47
|
+
: undefined
|
|
40
48
|
|
|
41
49
|
// Sync local ref to context triggerRef
|
|
42
50
|
useEffect(() => {
|
|
@@ -46,6 +54,7 @@ function Trigger({
|
|
|
46
54
|
return (
|
|
47
55
|
<html.button
|
|
48
56
|
ref={localRef}
|
|
57
|
+
id={fieldCtx?.controlId}
|
|
49
58
|
type="button"
|
|
50
59
|
role={'combobox' as 'listbox'}
|
|
51
60
|
onClick={toggle}
|
|
@@ -54,6 +63,8 @@ function Trigger({
|
|
|
54
63
|
aria-haspopup="listbox"
|
|
55
64
|
aria-controls={open ? listboxId : undefined}
|
|
56
65
|
aria-activedescendant={highlightedId ?? undefined}
|
|
66
|
+
aria-describedby={describedBy || undefined}
|
|
67
|
+
aria-invalid={fieldCtx?.invalid || undefined}
|
|
57
68
|
style={styles.trigger}
|
|
58
69
|
>
|
|
59
70
|
{children}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
|
13
13
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
14
14
|
export {Checkbox} from './components/Checkbox/Checkbox'
|
|
15
15
|
export {CheckboxGroup} from './components/CheckboxGroup/CheckboxGroup'
|
|
16
|
+
export {ColorInput} from './components/ColorInput/ColorInput'
|
|
17
|
+
export {ConfirmDialog} from './components/ConfirmDialog/ConfirmDialog'
|
|
16
18
|
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
17
19
|
export {DetailPanel, type DetailPanelSize} from './components/DetailPanel/DetailPanel'
|
|
18
20
|
export {Drawer, type DrawerAnchor, type DrawerSize} from './components/Drawer/Drawer'
|