@hideyukimori/nene2-ui 0.11.0 → 0.14.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 +41 -0
- package/dist/feedback/InlineAlert.d.ts +13 -4
- package/dist/feedback/InlineAlert.js +5 -5
- package/dist/feedback/InlineAlert.js.map +1 -1
- package/dist/forms/FormField.d.ts +11 -1
- package/dist/forms/FormField.js +7 -3
- package/dist/forms/FormField.js.map +1 -1
- package/dist/forms/field-context.d.ts +13 -0
- package/dist/forms/field-context.js +6 -1
- package/dist/forms/field-context.js.map +1 -1
- package/dist/lib/states.d.ts +32 -2
- package/dist/lib/states.js +36 -1
- package/dist/lib/states.js.map +1 -1
- package/dist/primitives/Button.js +17 -4
- package/dist/primitives/Button.js.map +1 -1
- package/dist/primitives/Checkbox.js +2 -2
- package/dist/primitives/Checkbox.js.map +1 -1
- package/dist/primitives/Input.js +2 -2
- package/dist/primitives/Input.js.map +1 -1
- package/dist/primitives/Radio.js +2 -2
- package/dist/primitives/Radio.js.map +1 -1
- package/dist/primitives/Select.js +2 -2
- package/dist/primitives/Select.js.map +1 -1
- package/dist/primitives/Switch.js +2 -2
- package/dist/primitives/Switch.js.map +1 -1
- package/dist/primitives/Textarea.js +2 -2
- package/dist/primitives/Textarea.js.map +1 -1
- package/package.json +1 -1
- package/themes/default.css +87 -3
package/README.md
CHANGED
|
@@ -258,6 +258,47 @@ wrap every choice in a `<div>` to say them.
|
|
|
258
258
|
label, so the kit carries it). That was right and it left the reason in place. **Fixing what
|
|
259
259
|
a report names is not the same as fixing what it is about.**
|
|
260
260
|
|
|
261
|
+
### 🔴 A palette that is missing a meaning
|
|
262
|
+
|
|
263
|
+
The kit's theme defines **8 of the 28 colours** in the frozen Core Token Contract v1. That
|
|
264
|
+
is a legal theme — the contract says what a name means, not that every name must be used —
|
|
265
|
+
but it stops being harmless the moment a component needs one of the twenty.
|
|
266
|
+
|
|
267
|
+
`warn` was one. Six ships define `--color-warn`; nene-payout, the product this theme was
|
|
268
|
+
promoted from, does not. So `--color-x-slot-alert-warn-*` pointed at `--color-danger` and a
|
|
269
|
+
warning rendered as an error — as nene-vault put it, **the difference reached someone
|
|
270
|
+
listening and no one looking**.
|
|
271
|
+
|
|
272
|
+
🔑 Same root cause as the radius scale, and the same shape as it: **a set whose members all
|
|
273
|
+
resolve to one value cannot express the distinction it is named for.** Both are now checked
|
|
274
|
+
— radius by step count, colour by whether two meanings resolve to the same value.
|
|
275
|
+
|
|
276
|
+
Still undefined: `surface-overlay`, `surface-sunken`, `text-faint`, `text-inverse`,
|
|
277
|
+
`border-strong`, `accent-hover`, `accent-soft`, `on-danger`, `success`, `success-soft`,
|
|
278
|
+
`on-success`, `info`, `info-soft`, `on-info`, `focus-ring`, `scrim`. Two of those —
|
|
279
|
+
`focus-ring` and `scrim` — already have component slots pointing at substitutes.
|
|
280
|
+
|
|
281
|
+
### 🔴 Two validity states, and only one of them is invalid
|
|
282
|
+
|
|
283
|
+
A value can be worth flagging without being wrong. nene-vault marks a retention period under
|
|
284
|
+
ten years, which satisfies every rule its form has.
|
|
285
|
+
|
|
286
|
+
| | ARIA | paints |
|
|
287
|
+
| --------- | ----------------------------------------------------- | ----------------------------------------------- |
|
|
288
|
+
| `error` | `aria-invalid` | `--color-x-slot-control-invalid-*` |
|
|
289
|
+
| `warning` | 🔴 **nothing** — announced through `aria-describedby` | `--color-x-slot-control-warn-*` via `data-warn` |
|
|
290
|
+
|
|
291
|
+
Setting `aria-invalid` on a legal value announces it as an error; leaving it off left the
|
|
292
|
+
field with no signal at all, because **the kit painted neither state** until 0.12.0. Seven of
|
|
293
|
+
nine ships paint validity themselves, so migrating onto the kit removed a signal the product
|
|
294
|
+
already had.
|
|
295
|
+
|
|
296
|
+
🔑 The test guarding vault's amber border asserted `aria-invalid="true"` — the attribute, not
|
|
297
|
+
the paint — and stayed green through the loss. **A test can check a stand-in for the thing it
|
|
298
|
+
cares about; while both live in the same component that is the same test, and the moment the
|
|
299
|
+
real thing moves upstream the stand-in stays behind and keeps passing.** Moving an
|
|
300
|
+
implementation upstream produces this shape structurally.
|
|
301
|
+
|
|
261
302
|
### 🔴 What a slot check must cover
|
|
262
303
|
|
|
263
304
|
The rule above — _every design value a component uses comes from a slot_ — was false in 26 of
|
|
@@ -2,19 +2,28 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
export interface InlineAlertProps {
|
|
3
3
|
/** What the message means. `danger` is announced assertively; `info` politely. */
|
|
4
4
|
tone?: 'info' | 'warn' | 'danger';
|
|
5
|
+
/**
|
|
6
|
+
* 🔴 Needed to point an `aria-describedby` at this message. Without it a product that
|
|
7
|
+
* wants the alert read out with a control has nowhere to put an id, and nene-vault
|
|
8
|
+
* wrapped the alert in a `<div>` to get one (2026-08-23). `FormField`'s `hint` is static
|
|
9
|
+
* help text, so it does not cover "a warning about the value that is in there now".
|
|
10
|
+
*/
|
|
11
|
+
id?: string;
|
|
12
|
+
/** Composed after the kit's own classes (design principle 2). */
|
|
13
|
+
className?: string;
|
|
5
14
|
children: ReactNode;
|
|
6
15
|
}
|
|
7
16
|
/**
|
|
8
17
|
* A message attached to the thing it is about, rather than to the page.
|
|
9
18
|
*
|
|
10
19
|
* 🔴 Each tone reads its colours from the theme, so a product can tell "warning" from
|
|
11
|
-
* "failure" by sight. Until
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* "failure" by sight. Until 0.12.0 they could not: `warn` pointed at `--color-danger`, so
|
|
21
|
+
* only the announced urgency differed — and as nene-vault put it, that difference reaches
|
|
22
|
+
* someone listening and no one looking. The palette now carries `warn` of its own.
|
|
14
23
|
*
|
|
15
24
|
* 🔴 The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
|
|
16
25
|
* which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
|
|
17
26
|
* Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
|
|
18
27
|
* of role is precisely the part that is easy to get wrong and invisible when you do.
|
|
19
28
|
*/
|
|
20
|
-
export declare function InlineAlert({ tone, children }: InlineAlertProps): import("react").JSX.Element;
|
|
29
|
+
export declare function InlineAlert({ tone, id, className, children }: InlineAlertProps): import("react").JSX.Element;
|
|
@@ -9,16 +9,16 @@ const TONE_CLASS = {
|
|
|
9
9
|
* A message attached to the thing it is about, rather than to the page.
|
|
10
10
|
*
|
|
11
11
|
* 🔴 Each tone reads its colours from the theme, so a product can tell "warning" from
|
|
12
|
-
* "failure" by sight. Until
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* "failure" by sight. Until 0.12.0 they could not: `warn` pointed at `--color-danger`, so
|
|
13
|
+
* only the announced urgency differed — and as nene-vault put it, that difference reaches
|
|
14
|
+
* someone listening and no one looking. The palette now carries `warn` of its own.
|
|
15
15
|
*
|
|
16
16
|
* 🔴 The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
|
|
17
17
|
* which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
|
|
18
18
|
* Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
|
|
19
19
|
* of role is precisely the part that is easy to get wrong and invisible when you do.
|
|
20
20
|
*/
|
|
21
|
-
export function InlineAlert({ tone = 'info', children }) {
|
|
22
|
-
return (_jsx("div", { role: tone === 'danger' ? 'alert' : 'status', className: cx('rounded-x-slot-alert border p-x-slot-alert-pad font-sans', TONE_CLASS[tone]), children: children }));
|
|
21
|
+
export function InlineAlert({ tone = 'info', id, className, children }) {
|
|
22
|
+
return (_jsx("div", { id: id, role: tone === 'danger' ? 'alert' : 'status', className: cx('rounded-x-slot-alert border p-x-slot-alert-pad font-sans', TONE_CLASS[tone], className), children: children }));
|
|
23
23
|
}
|
|
24
24
|
//# sourceMappingURL=InlineAlert.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InlineAlert.js","sourceRoot":"","sources":["../../src/feedback/InlineAlert.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"InlineAlert.js","sourceRoot":"","sources":["../../src/feedback/InlineAlert.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAiBlC,MAAM,UAAU,GAA0D;IACxE,IAAI,EAAE,mFAAmF;IACzF,IAAI,EAAE,mFAAmF;IACzF,MAAM,EAAE,yFAAyF;CAClG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAoB;IACtF,OAAO,CACL,cACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAC5C,SAAS,EAAE,EAAE,CACX,0DAA0D,EAC1D,UAAU,CAAC,IAAI,CAAC,EAChB,SAAS,CACV,YAEA,QAAQ,GACL,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -6,6 +6,16 @@ export interface FormFieldProps {
|
|
|
6
6
|
label: string;
|
|
7
7
|
/** Localized error message, or null when valid. */
|
|
8
8
|
error?: string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Localized warning, or null when there is nothing to flag.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 Not a milder `error`. A warning marks a value the form accepts — nene-vault flags a
|
|
13
|
+
* retention period under ten years, which satisfies every rule the field has. So it does
|
|
14
|
+
* NOT set `aria-invalid`: announcing a legal value as invalid is worse than saying
|
|
15
|
+
* nothing. It is announced as a `role="status"` and linked through `aria-describedby`,
|
|
16
|
+
* and it paints through `data-warn`.
|
|
17
|
+
*/
|
|
18
|
+
warning?: string | null;
|
|
9
19
|
/**
|
|
10
20
|
* Localized help text, shown under the control and read out with it.
|
|
11
21
|
*
|
|
@@ -44,4 +54,4 @@ export interface FormFieldProps {
|
|
|
44
54
|
* 🔴 The hint stays visible when there is an error. nene-profile hides it in that case, and
|
|
45
55
|
* that is the moment the user most needs it; the error is read first instead.
|
|
46
56
|
*/
|
|
47
|
-
export declare function FormField({ id, label, error, hint, labelAdornment, required, requiredMarker, children, }: FormFieldProps): import("react").JSX.Element;
|
|
57
|
+
export declare function FormField({ id, label, error, warning, hint, labelAdornment, required, requiredMarker, children, }: FormFieldProps): import("react").JSX.Element;
|
package/dist/forms/FormField.js
CHANGED
|
@@ -16,10 +16,14 @@ import { FieldContext } from './field-context.js';
|
|
|
16
16
|
* 🔴 The hint stays visible when there is an error. nene-profile hides it in that case, and
|
|
17
17
|
* that is the moment the user most needs it; the error is read first instead.
|
|
18
18
|
*/
|
|
19
|
-
export function FormField({ id, label, error = null, hint, labelAdornment, required = false, requiredMarker, children, }) {
|
|
19
|
+
export function FormField({ id, label, error = null, warning = null, hint, labelAdornment, required = false, requiredMarker, children, }) {
|
|
20
20
|
const errorId = error === null ? null : `${id}-error`;
|
|
21
|
+
const warningId = warning === null ? null : `${id}-warning`;
|
|
21
22
|
const hintId = hint === undefined || hint === null ? null : `${id}-hint`;
|
|
22
|
-
const value = useMemo(() => ({ id, errorId, hintId, required }), [id, errorId, hintId, required]);
|
|
23
|
-
return (_jsx(FieldContext.Provider, { value: value, children: _jsxs("div", { className: "flex flex-col gap-x-slot-field-gap", children: [_jsxs("label", { htmlFor: id, className: "font-sans font-x-slot-field-label text-x-slot-field-label-size text-x-slot-field-label", children: [label, required && requiredMarker !== undefined && requiredMarker !== null ? (_jsx("span", { className: "text-x-slot-field-
|
|
23
|
+
const value = useMemo(() => ({ id, errorId, warningId, hintId, required }), [id, errorId, warningId, hintId, required]);
|
|
24
|
+
return (_jsx(FieldContext.Provider, { value: value, children: _jsxs("div", { className: "flex flex-col gap-x-slot-field-gap", children: [_jsxs("label", { htmlFor: id, className: "font-sans font-x-slot-field-label text-x-slot-field-label-size text-x-slot-field-label", children: [label, required && requiredMarker !== undefined && requiredMarker !== null ? (_jsx("span", { className: "text-x-slot-field-required-marker", children: requiredMarker })) : null, labelAdornment] }), children, hintId === null ? null : (_jsx("span", { id: hintId, className: "font-sans text-x-slot-field-hint-size text-x-slot-field-hint", children: hint })), warningId === null ? null : (
|
|
25
|
+
// role="status" ではなく role="alert" にすると、入力の途中で読み上げを割り込む。
|
|
26
|
+
// 警告は「見直す価値がある」であって「止まれ」ではないので、次の切れ目で読ませる。
|
|
27
|
+
_jsx("p", { id: warningId, role: "status", className: "font-sans text-x-slot-field-error-size text-x-slot-field-warning-fg", children: warning })), errorId === null ? null : (_jsx("p", { id: errorId, role: "alert", className: "font-sans text-x-slot-field-error-size text-x-slot-field-error", children: error }))] }) }));
|
|
24
28
|
}
|
|
25
29
|
//# sourceMappingURL=FormField.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormField.js","sourceRoot":"","sources":["../../src/forms/FormField.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"FormField.js","sourceRoot":"","sources":["../../src/forms/FormField.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA2ClD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,GAAG,IAAI,EACZ,OAAO,GAAG,IAAI,EACd,IAAI,EACJ,cAAc,EACd,QAAQ,GAAG,KAAK,EAChB,cAAc,EACd,QAAQ,GACO;IACf,MAAM,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC;IACtD,MAAM,SAAS,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,UAAU,CAAC;IAC5D,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC;IACzE,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EACpD,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAC3C,CAAC;IAEF,OAAO,CACL,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACjC,eAAK,SAAS,EAAC,oCAAoC,aACjD,iBACE,OAAO,EAAE,EAAE,EACX,SAAS,EAAC,wFAAwF,aAEjG,KAAK,EACL,QAAQ,IAAI,cAAc,KAAK,SAAS,IAAI,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,CACrE,eAAM,SAAS,EAAC,mCAAmC,YAAE,cAAc,GAAQ,CAC5E,CAAC,CAAC,CAAC,IAAI,EACP,cAAc,IACT,EACP,QAAQ,EACR,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACxB,eACE,EAAE,EAAE,MAAM,EACV,SAAS,EAAC,8DAA8D,YAEvE,IAAI,GACA,CACR,EACA,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC3B,wDAAwD;gBACxD,2CAA2C;gBAC3C,YACE,EAAE,EAAE,SAAS,EACb,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,qEAAqE,YAE9E,OAAO,GACN,CACL,EACA,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACzB,YACE,EAAE,EAAE,OAAO,EACX,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,gEAAgE,YAEzE,KAAK,GACJ,CACL,IACG,GACgB,CACzB,CAAC;AACJ,CAAC"}
|
|
@@ -6,6 +6,13 @@ export interface FieldContextValue {
|
|
|
6
6
|
errorId: string | null;
|
|
7
7
|
/** id of the rendered hint, or null when there is no hint. */
|
|
8
8
|
hintId: string | null;
|
|
9
|
+
/**
|
|
10
|
+
* id of the rendered warning, or null when there is none.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 Separate from `errorId`, and deliberately not `aria-invalid`. A warning marks a
|
|
13
|
+
* value worth a second look, not a value the form rejects.
|
|
14
|
+
*/
|
|
15
|
+
warningId: string | null;
|
|
9
16
|
/** Whether the field must be filled in. */
|
|
10
17
|
required: boolean;
|
|
11
18
|
}
|
|
@@ -15,6 +22,12 @@ export interface FieldWiring {
|
|
|
15
22
|
'aria-invalid'?: AriaAttributes['aria-invalid'] | undefined;
|
|
16
23
|
'aria-describedby'?: string | undefined;
|
|
17
24
|
'aria-required'?: AriaAttributes['aria-required'] | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Present when the field carries a warning. Not an ARIA attribute — it paints, and says
|
|
27
|
+
* nothing to assistive technology, because the warning text is already linked through
|
|
28
|
+
* `aria-describedby`.
|
|
29
|
+
*/
|
|
30
|
+
'data-warn'?: '' | undefined;
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
33
|
* Wire a control to the `FormField` around it.
|
|
@@ -38,14 +38,19 @@ export function useFieldWiring(explicit) {
|
|
|
38
38
|
'aria-invalid': explicit.ariaInvalid,
|
|
39
39
|
'aria-describedby': explicit.ariaDescribedBy,
|
|
40
40
|
'aria-required': explicit.ariaRequired,
|
|
41
|
+
'data-warn': undefined,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
// Error first, then warning, then hint — read in the order they matter.
|
|
45
|
+
const described = [field.errorId, field.warningId, field.hintId]
|
|
46
|
+
.filter((v) => v !== null)
|
|
47
|
+
.join(' ');
|
|
44
48
|
return {
|
|
45
49
|
id: explicit.id ?? field.id,
|
|
46
50
|
'aria-invalid': explicit.ariaInvalid ?? (field.errorId === null ? undefined : true),
|
|
47
51
|
'aria-describedby': explicit.ariaDescribedBy ?? (described === '' ? undefined : described),
|
|
48
52
|
'aria-required': explicit.ariaRequired ?? (field.required ? true : undefined),
|
|
53
|
+
'data-warn': field.warningId === null ? undefined : '',
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
//# sourceMappingURL=field-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-context.js","sourceRoot":"","sources":["../../src/forms/field-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAuB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"field-context.js","sourceRoot":"","sources":["../../src/forms/field-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAuB,MAAM,OAAO,CAAC;AAoBvE,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAC;AAiB1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,cAAc,CAAC,QAK9B;IACC,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IAEvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,cAAc,EAAE,QAAQ,CAAC,WAAW;YACpC,kBAAkB,EAAE,QAAQ,CAAC,eAAe;YAC5C,eAAe,EAAE,QAAQ,CAAC,YAAY;YACtC,WAAW,EAAE,SAAS;SACvB,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC;SAC7D,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;SACtC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEb,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE;QAC3B,cAAc,EAAE,QAAQ,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;QACnF,kBAAkB,EAAE,QAAQ,CAAC,eAAe,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1F,eAAe,EAAE,QAAQ,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7E,WAAW,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;KACvD,CAAC;AACJ,CAAC"}
|
package/dist/lib/states.d.ts
CHANGED
|
@@ -64,6 +64,36 @@ export declare const DISABLED_CLASS = "disabled:opacity-x-disabled disabled:curs
|
|
|
64
64
|
*/
|
|
65
65
|
export declare const HOVER_CLASS = "hover:brightness-x-slot-hover active:brightness-x-slot-press disabled:hover:brightness-100 disabled:active:brightness-100";
|
|
66
66
|
/** Controls that can be focused and disabled — every interactive primitive in the kit. */
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Validity, painted.
|
|
69
|
+
*
|
|
70
|
+
* 🔴 Two states, and only one of them is `aria-invalid`. A value can be worth flagging
|
|
71
|
+
* without being wrong: nene-vault marks a retention period under ten years, which satisfies
|
|
72
|
+
* every rule the form has. Setting `aria-invalid` there would announce a legal value as an
|
|
73
|
+
* error, and not setting it left the field with no signal at all — the kit painted neither.
|
|
74
|
+
* So the warning is `data-warn`, an attribute that carries no ARIA meaning; the message
|
|
75
|
+
* beside it is what a screen reader gets, through `aria-describedby`.
|
|
76
|
+
*
|
|
77
|
+
* The error keeps `aria-invalid`, which is what it is for.
|
|
78
|
+
*/
|
|
79
|
+
export declare const VALIDITY_CLASS: string;
|
|
80
|
+
/**
|
|
81
|
+
* The minimum a control may be on a touch screen.
|
|
82
|
+
*
|
|
83
|
+
* 🔴 Deliberately NOT part of `CONTROL_CLASS`, though everything else here is. This has to
|
|
84
|
+
* go on the element that *is* the target, and for a checkbox that is the `<label>`, not the
|
|
85
|
+
* `<input>` — the box is `size-x-slot-choice-box` (16px square), so a minimum height on it
|
|
86
|
+
* produces a 44×16 rectangle instead of a bigger target. Folding it into the shared bundle
|
|
87
|
+
* would have applied it there, and the result would have looked like a styling bug rather
|
|
88
|
+
* than a mis-placed constraint.
|
|
89
|
+
*
|
|
90
|
+
* 🔴 It only works at all because the elements that carry it are flex containers. A
|
|
91
|
+
* `min-height` on an `inline-block` leaves the label at the top of the taller box; with
|
|
92
|
+
* `items-center` it stays centred. `Button` became one in 0.13.0 for an unrelated reason
|
|
93
|
+
* (an icon and its label sat on a shared baseline), and that turned out to be a
|
|
94
|
+
* prerequisite for this.
|
|
95
|
+
*/
|
|
96
|
+
export declare const TOUCH_CLASS = "pointer-coarse:min-h-x-slot-control-touch-min";
|
|
97
|
+
export declare const CONTROL_CLASS: string;
|
|
68
98
|
/** Controls that are also clicked: buttons, switches, the toast dismiss. */
|
|
69
|
-
export declare const CLICKABLE_CLASS
|
|
99
|
+
export declare const CLICKABLE_CLASS: string;
|
package/dist/lib/states.js
CHANGED
|
@@ -64,7 +64,42 @@ export const DISABLED_CLASS = 'disabled:opacity-x-disabled disabled:cursor-not-a
|
|
|
64
64
|
*/
|
|
65
65
|
export const HOVER_CLASS = 'hover:brightness-x-slot-hover active:brightness-x-slot-press disabled:hover:brightness-100 disabled:active:brightness-100';
|
|
66
66
|
/** Controls that can be focused and disabled — every interactive primitive in the kit. */
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Validity, painted.
|
|
69
|
+
*
|
|
70
|
+
* 🔴 Two states, and only one of them is `aria-invalid`. A value can be worth flagging
|
|
71
|
+
* without being wrong: nene-vault marks a retention period under ten years, which satisfies
|
|
72
|
+
* every rule the form has. Setting `aria-invalid` there would announce a legal value as an
|
|
73
|
+
* error, and not setting it left the field with no signal at all — the kit painted neither.
|
|
74
|
+
* So the warning is `data-warn`, an attribute that carries no ARIA meaning; the message
|
|
75
|
+
* beside it is what a screen reader gets, through `aria-describedby`.
|
|
76
|
+
*
|
|
77
|
+
* The error keeps `aria-invalid`, which is what it is for.
|
|
78
|
+
*/
|
|
79
|
+
export const VALIDITY_CLASS = [
|
|
80
|
+
'aria-invalid:border-x-slot-control-invalid-border',
|
|
81
|
+
'aria-invalid:bg-x-slot-control-invalid-bg',
|
|
82
|
+
'data-[warn]:border-x-slot-control-warn-border',
|
|
83
|
+
'data-[warn]:bg-x-slot-control-warn-bg',
|
|
84
|
+
].join(' ');
|
|
85
|
+
/**
|
|
86
|
+
* The minimum a control may be on a touch screen.
|
|
87
|
+
*
|
|
88
|
+
* 🔴 Deliberately NOT part of `CONTROL_CLASS`, though everything else here is. This has to
|
|
89
|
+
* go on the element that *is* the target, and for a checkbox that is the `<label>`, not the
|
|
90
|
+
* `<input>` — the box is `size-x-slot-choice-box` (16px square), so a minimum height on it
|
|
91
|
+
* produces a 44×16 rectangle instead of a bigger target. Folding it into the shared bundle
|
|
92
|
+
* would have applied it there, and the result would have looked like a styling bug rather
|
|
93
|
+
* than a mis-placed constraint.
|
|
94
|
+
*
|
|
95
|
+
* 🔴 It only works at all because the elements that carry it are flex containers. A
|
|
96
|
+
* `min-height` on an `inline-block` leaves the label at the top of the taller box; with
|
|
97
|
+
* `items-center` it stays centred. `Button` became one in 0.13.0 for an unrelated reason
|
|
98
|
+
* (an icon and its label sat on a shared baseline), and that turned out to be a
|
|
99
|
+
* prerequisite for this.
|
|
100
|
+
*/
|
|
101
|
+
export const TOUCH_CLASS = 'pointer-coarse:min-h-x-slot-control-touch-min';
|
|
102
|
+
export const CONTROL_CLASS = `${FOCUS_CLASS} ${DISABLED_CLASS} ${VALIDITY_CLASS}`;
|
|
68
103
|
/** Controls that are also clicked: buttons, switches, the toast dismiss. */
|
|
69
104
|
export const CLICKABLE_CLASS = `${CONTROL_CLASS} ${HOVER_CLASS}`;
|
|
70
105
|
//# sourceMappingURL=states.js.map
|
package/dist/lib/states.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"states.js","sourceRoot":"","sources":["../../src/lib/states.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GACtB,gGAAgG,CAAC;AAEnG;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAExF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GACtB,2HAA2H,CAAC;AAE9H,0FAA0F;AAC1F,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,WAAW,IAAI,cAAc,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"states.js","sourceRoot":"","sources":["../../src/lib/states.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GACtB,gGAAgG,CAAC;AAEnG;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAExF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GACtB,2HAA2H,CAAC;AAE9H,0FAA0F;AAC1F;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,mDAAmD;IACnD,2CAA2C;IAC3C,+CAA+C;IAC/C,uCAAuC;CACxC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEZ;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,+CAA+C,CAAC;AAE3E,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,WAAW,IAAI,cAAc,IAAI,cAAc,EAAE,CAAC;AAElF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,aAAa,IAAI,WAAW,EAAE,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../lib/cx.js';
|
|
3
|
-
import { CLICKABLE_CLASS } from '../lib/states.js';
|
|
3
|
+
import { CLICKABLE_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
4
4
|
const VARIANT_CLASS = {
|
|
5
5
|
primary: 'bg-x-slot-button-primary-bg text-x-slot-button-primary-fg',
|
|
6
6
|
secondary: 'bg-x-slot-button-secondary-bg text-x-slot-button-secondary-fg border-x-slot-button-secondary-border',
|
|
@@ -10,9 +10,22 @@ const VARIANT_CLASS = {
|
|
|
10
10
|
ghost: 'bg-transparent text-x-slot-button-ghost-fg',
|
|
11
11
|
};
|
|
12
12
|
const SIZE_CLASS = {
|
|
13
|
-
md: 'px-x-slot-button-pad-x py-x-slot-button-pad-y',
|
|
14
|
-
sm: 'px-x-slot-button-sm-pad-x py-x-slot-button-sm-pad-y',
|
|
13
|
+
md: 'px-x-slot-button-pad-x py-x-slot-button-pad-y text-x-slot-button-size',
|
|
14
|
+
sm: 'px-x-slot-button-sm-pad-x py-x-slot-button-sm-pad-y text-x-slot-button-sm-size',
|
|
15
15
|
};
|
|
16
|
+
// 🔴 `inline-flex`, so the button places what is inside it. A `<button>` is `inline-block`
|
|
17
|
+
// by default, which leaves an icon and its label on a shared baseline rather than centred —
|
|
18
|
+
// visible as a half-pixel of drift on every icon button, and invisible in a diff. The kit
|
|
19
|
+
// already takes responsibility for the size of an `<svg>` in here (`SVG_BOUND`); taking
|
|
20
|
+
// responsibility for where it sits is the other half of the same job, and leaving one to the
|
|
21
|
+
// caller while doing the other is the asymmetry nene-vault had to work around by writing
|
|
22
|
+
// `inline-flex` at the call site.
|
|
23
|
+
//
|
|
24
|
+
// ⚠️ This changes rendering. A text-only button is laid out by flex rather than by inline
|
|
25
|
+
// flow, so it no longer sits on the surrounding text's baseline the way an inline-block does.
|
|
26
|
+
// Inside a paragraph that is visible; in the button rows and toolbars these are actually used
|
|
27
|
+
// in, it is not. `inline-flex` rather than `flex` keeps the element from claiming a line.
|
|
28
|
+
//
|
|
16
29
|
// 🔴 `border border-transparent` is load-bearing. Height comes from the padding, so the
|
|
17
30
|
// `secondary` variant — the only one with a visible border — would otherwise stand 2px
|
|
18
31
|
// taller than the others. nene-vault puts a primary and a secondary side by side in seven
|
|
@@ -29,7 +42,7 @@ const SIZE_CLASS = {
|
|
|
29
42
|
// instead of its own. A maximum cannot collide with a height: different property, and it
|
|
30
43
|
// leaves anything already smaller alone.
|
|
31
44
|
const SVG_BOUND = '[&_svg]:max-h-x-slot-button-icon [&_svg]:max-w-x-slot-button-icon';
|
|
32
|
-
const BASE_CLASS = `rounded-x-slot-button border border-transparent
|
|
45
|
+
const BASE_CLASS = `rounded-x-slot-button border border-transparent inline-flex items-center justify-center gap-x-slot-button-gap font-sans font-x-slot-button ${SVG_BOUND} ${TOUCH_CLASS} ${CLICKABLE_CLASS}`;
|
|
33
46
|
export function Button({ variant = 'primary', size = 'md', children, type = 'button', className, ...rest }) {
|
|
34
47
|
return (_jsx("button", { type: type, className: cx(BASE_CLASS, SIZE_CLASS[size], VARIANT_CLASS[variant], className), ...rest, children: children }));
|
|
35
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../src/primitives/Button.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../src/primitives/Button.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAchE,MAAM,aAAa,GAAwD;IACzE,OAAO,EAAE,2DAA2D;IACpE,SAAS,EACP,qGAAqG;IACvG,MAAM,EAAE,yDAAyD;IACjE,uFAAuF;IACvF,kEAAkE;IAClE,KAAK,EAAE,4CAA4C;CACpD,CAAC;AAEF,MAAM,UAAU,GAAqD;IACnE,EAAE,EAAE,uEAAuE;IAC3E,EAAE,EAAE,gFAAgF;CACrF,CAAC;AAEF,2FAA2F;AAC3F,4FAA4F;AAC5F,0FAA0F;AAC1F,wFAAwF;AACxF,6FAA6F;AAC7F,yFAAyF;AACzF,kCAAkC;AAClC,EAAE;AACF,0FAA0F;AAC1F,8FAA8F;AAC9F,8FAA8F;AAC9F,0FAA0F;AAC1F,EAAE;AACF,wFAAwF;AACxF,uFAAuF;AACvF,0FAA0F;AAC1F,6FAA6F;AAC7F,mDAAmD;AACnD,2FAA2F;AAC3F,6FAA6F;AAC7F,4FAA4F;AAC5F,yDAAyD;AACzD,EAAE;AACF,2FAA2F;AAC3F,0FAA0F;AAC1F,yFAAyF;AACzF,yFAAyF;AACzF,yCAAyC;AACzC,MAAM,SAAS,GAAG,mEAAmE,CAAC;AAEtF,MAAM,UAAU,GAAG,8IAA8I,SAAS,IAAI,WAAW,IAAI,eAAe,EAAE,CAAC;AAE/M,MAAM,UAAU,MAAM,CAAC,EACrB,OAAO,GAAG,SAAS,EACnB,IAAI,GAAG,IAAI,EACX,QAAQ,EACR,IAAI,GAAG,QAAQ,EACf,SAAS,EACT,GAAG,IAAI,EACK;IACZ,OAAO,CACL,iBACE,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,KAC1E,IAAI,YAEP,QAAQ,GACF,CACV,CAAC;AACJ,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CONTROL_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CONTROL_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
import { useFieldWiring } from '../forms/field-context.js';
|
|
6
6
|
const BOX_CLASS = `size-x-slot-choice-box shrink-0 rounded-x-slot-control border border-x-slot-choice-border accent-x-slot-choice-accent ${CONTROL_CLASS}`;
|
|
7
7
|
/**
|
|
@@ -31,6 +31,6 @@ const BOX_CLASS = `size-x-slot-choice-box shrink-0 rounded-x-slot-control border
|
|
|
31
31
|
*/
|
|
32
32
|
export const Checkbox = forwardRef(function Checkbox({ label, className, inputClassName, id, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, 'aria-required': ariaRequired, ...rest }, ref) {
|
|
33
33
|
const wiring = useFieldWiring({ id, ariaInvalid, ariaDescribedBy, ariaRequired });
|
|
34
|
-
return (_jsxs("label", { className: cx('inline-flex cursor-pointer items-center gap-x-slot-choice-gap font-sans text-x-slot-choice-size text-x-slot-choice-fg', className), children: [_jsx("input", { ref: ref, type: "checkbox", className: cx(BOX_CLASS, inputClassName), ...wiring, ...rest }), label] }));
|
|
34
|
+
return (_jsxs("label", { className: cx(TOUCH_CLASS, 'inline-flex cursor-pointer items-center gap-x-slot-choice-gap font-sans text-x-slot-choice-size text-x-slot-choice-fg', className), children: [_jsx("input", { ref: ref, type: "checkbox", className: cx(BOX_CLASS, inputClassName), ...wiring, ...rest }), label] }));
|
|
35
35
|
});
|
|
36
36
|
//# sourceMappingURL=Checkbox.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.js","sourceRoot":"","sources":["../../src/primitives/Checkbox.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,OAAO,CAAC;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Checkbox.js","sourceRoot":"","sources":["../../src/primitives/Checkbox.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,OAAO,CAAC;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAY3D,MAAM,SAAS,GAAG,yHAAyH,aAAa,EAAE,CAAC;AAE3J;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAkC,SAAS,QAAQ,CACnF,EACE,KAAK,EACL,SAAS,EACT,cAAc,EACd,EAAE,EACF,cAAc,EAAE,WAAW,EAC3B,kBAAkB,EAAE,eAAe,EACnC,eAAe,EAAE,YAAY,EAC7B,GAAG,IAAI,EACR,EACD,GAAG;IAEH,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,CAAC;IAElF,OAAO,CACL,iBACE,SAAS,EAAE,EAAE,CACX,WAAW,EACX,uHAAuH,EACvH,SAAS,CACV,aAED,gBACE,GAAG,EAAE,GAAG,EACR,IAAI,EAAC,UAAU,EACf,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,KACpC,MAAM,KACN,IAAI,GACR,EACD,KAAK,IACA,CACT,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
package/dist/primitives/Input.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CONTROL_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CONTROL_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
import { useFieldWiring } from '../forms/field-context.js';
|
|
6
|
-
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg placeholder:text-x-slot-control-placeholder ${CONTROL_CLASS}`;
|
|
6
|
+
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg placeholder:text-x-slot-control-placeholder ${CONTROL_CLASS} ${TOUCH_CLASS}`;
|
|
7
7
|
/**
|
|
8
8
|
* Text input primitive. forwardRef so it works directly with react-hook-form `register`.
|
|
9
9
|
* Visual values come from theme tokens only.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/primitives/Input.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4B,MAAM,OAAO,CAAC;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/primitives/Input.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4B,MAAM,OAAO,CAAC;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,MAAM,UAAU,GAAG,8RAA8R,aAAa,IAAI,WAAW,EAAE,CAAC;AAEhV;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAA+B,SAAS,KAAK,CAC1E,EACE,SAAS,EACT,EAAE,EACF,cAAc,EAAE,WAAW,EAC3B,kBAAkB,EAAE,eAAe,EACnC,eAAe,EAAE,YAAY,EAC7B,GAAG,IAAI,EACR,EACD,GAAG;IAEH,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,CAAC;IAElF,OAAO,gBAAO,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,KAAM,MAAM,KAAM,IAAI,GAAI,CAAC;AACzF,CAAC,CAAC,CAAC"}
|
package/dist/primitives/Radio.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CONTROL_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CONTROL_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
const DOT_CLASS = `size-x-slot-choice-box shrink-0 border border-x-slot-choice-border accent-x-slot-choice-accent ${CONTROL_CLASS}`;
|
|
6
6
|
/**
|
|
7
7
|
* One option in a radio group, with its label.
|
|
@@ -28,6 +28,6 @@ const DOT_CLASS = `size-x-slot-choice-box shrink-0 border border-x-slot-choice-b
|
|
|
28
28
|
* the pointer cursor on every choice — visible to a mouse user, invisible in a diff.
|
|
29
29
|
*/
|
|
30
30
|
export const Radio = forwardRef(function Radio({ label, name, className, inputClassName, ...rest }, ref) {
|
|
31
|
-
return (_jsxs("label", { className: cx('inline-flex cursor-pointer items-center gap-x-slot-choice-gap font-sans text-x-slot-choice-size text-x-slot-choice-fg', className), children: [_jsx("input", { ref: ref, type: "radio", name: name, className: cx(DOT_CLASS, inputClassName), ...rest }), label] }));
|
|
31
|
+
return (_jsxs("label", { className: cx(TOUCH_CLASS, 'inline-flex cursor-pointer items-center gap-x-slot-choice-gap font-sans text-x-slot-choice-size text-x-slot-choice-fg', className), children: [_jsx("input", { ref: ref, type: "radio", name: name, className: cx(DOT_CLASS, inputClassName), ...rest }), label] }));
|
|
32
32
|
});
|
|
33
33
|
//# sourceMappingURL=Radio.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Radio.js","sourceRoot":"","sources":["../../src/primitives/Radio.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,OAAO,CAAC;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Radio.js","sourceRoot":"","sources":["../../src/primitives/Radio.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,OAAO,CAAC;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAc9D,MAAM,SAAS,GAAG,kGAAkG,aAAa,EAAE,CAAC;AAEpI;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAA+B,SAAS,KAAK,CAC1E,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,EACnD,GAAG;IAEH,OAAO,CACL,iBACE,SAAS,EAAE,EAAE,CACX,WAAW,EACX,uHAAuH,EACvH,SAAS,CACV,aAED,gBACE,GAAG,EAAE,GAAG,EACR,IAAI,EAAC,OAAO,EACZ,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,KACpC,IAAI,GACR,EACD,KAAK,IACA,CACT,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CONTROL_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CONTROL_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
import { useFieldWiring } from '../forms/field-context.js';
|
|
6
|
-
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg ${CONTROL_CLASS}`;
|
|
6
|
+
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg ${CONTROL_CLASS} ${TOUCH_CLASS}`;
|
|
7
7
|
/**
|
|
8
8
|
* Select primitive. forwardRef so it works directly with react-hook-form `register`.
|
|
9
9
|
* Visual values come from theme tokens only.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.js","sourceRoot":"","sources":["../../src/primitives/Select.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA6C,MAAM,OAAO,CAAC;AAC9E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Select.js","sourceRoot":"","sources":["../../src/primitives/Select.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA6C,MAAM,OAAO,CAAC;AAC9E,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAM3D,MAAM,UAAU,GAAG,kPAAkP,aAAa,IAAI,WAAW,EAAE,CAAC;AAEpS;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAiC,SAAS,MAAM,CAC9E,EACE,QAAQ,EACR,SAAS,EACT,EAAE,EACF,cAAc,EAAE,WAAW,EAC3B,kBAAkB,EAAE,eAAe,EACnC,eAAe,EAAE,YAAY,EAC7B,GAAG,IAAI,EACR,EACD,GAAG;IAEH,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,CAAC;IAElF,OAAO,CACL,iBAAQ,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,KAAM,MAAM,KAAM,IAAI,YACzE,QAAQ,GACF,CACV,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CLICKABLE_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CLICKABLE_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
/**
|
|
6
6
|
* An on/off control that takes effect immediately.
|
|
7
7
|
*
|
|
@@ -14,7 +14,7 @@ import { CLICKABLE_CLASS } from '../lib/states.js';
|
|
|
14
14
|
* `aria-checked` carries the state; the visible track is decoration on top of it.
|
|
15
15
|
*/
|
|
16
16
|
export const Switch = forwardRef(function Switch({ checked, onCheckedChange, label, className, ...rest }, ref) {
|
|
17
|
-
return (_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": checked, "aria-label": label, onClick: () => onCheckedChange(!checked), className: cx('inline-flex items-center rounded-x-slot-switch border border-x-slot-switch-border px-x-slot-switch-pad-x py-x-slot-switch-pad-y font-sans', checked
|
|
17
|
+
return (_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": checked, "aria-label": label, onClick: () => onCheckedChange(!checked), className: cx(TOUCH_CLASS, 'inline-flex items-center rounded-x-slot-switch border border-x-slot-switch-border px-x-slot-switch-pad-x py-x-slot-switch-pad-y font-sans', checked
|
|
18
18
|
? 'bg-x-slot-switch-on-bg text-x-slot-switch-on-fg'
|
|
19
19
|
: 'bg-x-slot-switch-off-bg text-x-slot-switch-off-fg', CLICKABLE_CLASS, className), ...rest, children: label }));
|
|
20
20
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Switch.js","sourceRoot":"","sources":["../../src/primitives/Switch.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA6B,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Switch.js","sourceRoot":"","sources":["../../src/primitives/Switch.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA6B,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAUhE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAiC,SAAS,MAAM,CAC9E,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EACvD,GAAG;IAEH,OAAO,CACL,iBACE,GAAG,EAAE,GAAG,EACR,IAAI,EAAC,QAAQ,EACb,IAAI,EAAC,QAAQ,kBACC,OAAO,gBACT,KAAK,EACjB,OAAO,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,EACxC,SAAS,EAAE,EAAE,CACX,WAAW,EACX,2IAA2I,EAC3I,OAAO;YACL,CAAC,CAAC,iDAAiD;YACnD,CAAC,CAAC,mDAAmD,EACvD,eAAe,EACf,SAAS,CACV,KACG,IAAI,YAEP,KAAK,GACC,CACV,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
-
import { CONTROL_CLASS } from '../lib/states.js';
|
|
4
|
+
import { CONTROL_CLASS, TOUCH_CLASS } from '../lib/states.js';
|
|
5
5
|
import { useFieldWiring } from '../forms/field-context.js';
|
|
6
|
-
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg placeholder:text-x-slot-control-placeholder ${CONTROL_CLASS}`;
|
|
6
|
+
const BASE_CLASS = `w-full rounded-x-slot-control border border-x-slot-control-border bg-x-slot-control-bg px-x-slot-control-pad-x py-x-slot-control-pad-y font-sans text-x-slot-control-size pointer-coarse:text-x-slot-control-touch-size text-x-slot-control-fg placeholder:text-x-slot-control-placeholder ${CONTROL_CLASS} ${TOUCH_CLASS}`;
|
|
7
7
|
/**
|
|
8
8
|
* Multi-line text input. Deliberately identical to `Input` apart from the element, so the
|
|
9
9
|
* two never drift: five ships wrote their own `Textarea.tsx` and the kit shipped without
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Textarea.js","sourceRoot":"","sources":["../../src/primitives/Textarea.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA+B,MAAM,OAAO,CAAC;AAChE,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Textarea.js","sourceRoot":"","sources":["../../src/primitives/Textarea.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA+B,MAAM,OAAO,CAAC;AAChE,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,MAAM,UAAU,GAAG,8RAA8R,aAAa,IAAI,WAAW,EAAE,CAAC;AAEhV;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAqC,SAAS,QAAQ,CACtF,EACE,SAAS,EACT,EAAE,EACF,cAAc,EAAE,WAAW,EAC3B,kBAAkB,EAAE,eAAe,EACnC,eAAe,EAAE,YAAY,EAC7B,GAAG,IAAI,EACR,EACD,GAAG;IAEH,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,CAAC;IAElF,OAAO,mBAAU,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,KAAM,MAAM,KAAM,IAAI,GAAI,CAAC;AAC5F,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hideyukimori/nene2-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "NeNe fleet shared React UI kit — token-driven primitives on Tailwind v4 @theme. Components carry no design of their own; themes do.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/themes/default.css
CHANGED
|
@@ -19,6 +19,30 @@
|
|
|
19
19
|
--color-on-accent: oklch(99% 0 0);
|
|
20
20
|
--color-danger: oklch(55% 0.2 25);
|
|
21
21
|
|
|
22
|
+
/* 🔴 Four contract colours the theme did not define. They are not new: `warn`,
|
|
23
|
+
* `warn-soft`, `on-warn` and `danger-soft` are in the frozen Core Token Contract v1
|
|
24
|
+
* (28 colours), and this theme defined 8 of them. Twenty are still missing — see the
|
|
25
|
+
* README — but these four are what a validity state needs.
|
|
26
|
+
*
|
|
27
|
+
* Same root cause as the radius scale: the theme was promoted verbatim from nene-payout,
|
|
28
|
+
* and payout has no warn. Six other ships do (nene-vault, nene-invoice, nene-field,
|
|
29
|
+
* nene-records, nene-deal, nene-profile), so `--color-x-slot-alert-warn-*` pointed at
|
|
30
|
+
* `--color-danger` — a warning that renders as an error. Promoting from the product with
|
|
31
|
+
* the fewest violations exported its missing distinctions along with its consistency.
|
|
32
|
+
*
|
|
33
|
+
* Values are the fleet median of the eight `--color-warn` definitions in use
|
|
34
|
+
* (2026-08-23; nene-field's hex converted to OKLCH to be counted).
|
|
35
|
+
*
|
|
36
|
+
* 🔴 `on-warn` is measured, not chosen. Against this `warn` fill: white is 3.48:1 and
|
|
37
|
+
* `text-primary` is 4.47:1 — which misses the 4.5:1 body-text threshold by 0.03, and is
|
|
38
|
+
* exactly the kind of near-miss that survives review by eye. The dark amber below is
|
|
39
|
+
* 4.97:1. The `warn` fill itself is 3.58:1 against `surface-raised`, clearing the 3:1
|
|
40
|
+
* that WCAG 1.4.11 asks of a non-text boundary. */
|
|
41
|
+
--color-warn: oklch(63% 0.135 75);
|
|
42
|
+
--color-warn-soft: oklch(95% 0.035 80);
|
|
43
|
+
--color-on-warn: oklch(21% 0.04 75);
|
|
44
|
+
--color-danger-soft: oklch(95% 0.03 30);
|
|
45
|
+
|
|
22
46
|
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.06);
|
|
23
47
|
|
|
24
48
|
/* Typography. Until 0.3.0 the kit had none of these and the components carried
|
|
@@ -54,6 +78,21 @@
|
|
|
54
78
|
--text-x-xl: 1.5rem;
|
|
55
79
|
--text-x-ios-floor: 16px;
|
|
56
80
|
|
|
81
|
+
/* The smallest a control may be on a touch screen. Not a design step, for the same reason
|
|
82
|
+
* `--text-x-ios-floor` is not: it is what the platform requires, and a product lowering it
|
|
83
|
+
* is not expressing a preference, it is making targets people miss.
|
|
84
|
+
*
|
|
85
|
+
* 🔴 44px, which is what WCAG 2.5.5 (AAA) and Apple's HIG both ask for — the smallest
|
|
86
|
+
* value that satisfies the standards rather than the fleet's average. Two ships already
|
|
87
|
+
* set 48px (nene-profile, nene-deal), Material's number; a product that wants it raises
|
|
88
|
+
* the slot. Picking the higher one here would be the kit imposing a size, and picking a
|
|
89
|
+
* median would make a device constraint look like a preference.
|
|
90
|
+
*
|
|
91
|
+
* ⚠️ The kit's own `sm` button computes to about 31px against a 14px body — above the
|
|
92
|
+
* 24px WCAG 2.5.8 (AA) asks for, and well under this. The `md` button is already ~47px,
|
|
93
|
+
* so this changes nothing there. */
|
|
94
|
+
--spacing-x-touch-floor: 44px;
|
|
95
|
+
|
|
57
96
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
58
97
|
* ② SLOTS — a product may redefine these.
|
|
59
98
|
*
|
|
@@ -70,6 +109,12 @@
|
|
|
70
109
|
--spacing-x-slot-button-pad-y: var(--spacing-x-xs);
|
|
71
110
|
--spacing-x-slot-button-sm-pad-x: var(--spacing-x-xs);
|
|
72
111
|
--spacing-x-slot-button-sm-pad-y: var(--spacing-x-3xs);
|
|
112
|
+
/* 🔴 The gap between an icon and its label. It exists because the button became a flex
|
|
113
|
+
* container in 0.13.0, and a flex container drops the whitespace that used to separate
|
|
114
|
+
* them: `<Button><Icon /> Save</Button>` renders the text as an anonymous flex item, and
|
|
115
|
+
* leading whitespace in one is stripped. `3xs` is 0.25rem, which is about the width of
|
|
116
|
+
* the space that disappears — so the change of display does not also change the spacing. */
|
|
117
|
+
--spacing-x-slot-button-gap: var(--spacing-x-3xs);
|
|
73
118
|
--spacing-x-slot-control-pad-x: var(--spacing-x-2xs);
|
|
74
119
|
--spacing-x-slot-control-pad-y: var(--spacing-x-xs);
|
|
75
120
|
--spacing-x-slot-field-gap: var(--spacing-x-2xs);
|
|
@@ -126,9 +171,11 @@
|
|
|
126
171
|
--color-x-slot-alert-info-bg: var(--color-surface);
|
|
127
172
|
--color-x-slot-alert-info-fg: var(--color-text-primary);
|
|
128
173
|
--color-x-slot-alert-info-border: var(--color-border);
|
|
174
|
+
/* 🔴 Changed in 0.12.0: these pointed at `--color-danger`, so a warning rendered as an
|
|
175
|
+
* error. Not a shade — a different meaning wearing the wrong one. */
|
|
129
176
|
--color-x-slot-alert-warn-bg: var(--color-surface);
|
|
130
|
-
--color-x-slot-alert-warn-fg: var(--color-
|
|
131
|
-
--color-x-slot-alert-warn-border: var(--color-
|
|
177
|
+
--color-x-slot-alert-warn-fg: var(--color-on-warn);
|
|
178
|
+
--color-x-slot-alert-warn-border: var(--color-warn);
|
|
132
179
|
--color-x-slot-alert-danger-bg: var(--color-surface);
|
|
133
180
|
--color-x-slot-alert-danger-fg: var(--color-danger);
|
|
134
181
|
--color-x-slot-alert-danger-border: var(--color-danger);
|
|
@@ -137,6 +184,10 @@
|
|
|
137
184
|
--text-x-slot-field-hint-size: var(--text-x-2xs);
|
|
138
185
|
--color-x-slot-field-error: var(--color-danger);
|
|
139
186
|
--text-x-slot-field-error-size: var(--text-x-2xs);
|
|
187
|
+
/* 🔴 `on-warn`, not `warn`. This text sits on the page, not on a warn fill: `--color-warn`
|
|
188
|
+
* measures 3.58:1 against `surface-raised`, which clears the 3:1 a border needs and misses
|
|
189
|
+
* the 4.5:1 body text needs. The dark amber clears both. */
|
|
190
|
+
--color-x-slot-field-warning-fg: var(--color-on-warn);
|
|
140
191
|
--text-x-slot-field-label-size: var(--text-x-xs);
|
|
141
192
|
/* 🔴 `var(--font-weight-medium)`, not `500`. Identical output — Tailwind defines that
|
|
142
193
|
* step as 500 — but a literal here is a weight the kit invented, and the namespace has a
|
|
@@ -156,6 +207,11 @@
|
|
|
156
207
|
* only because the kit cannot know a product's rem base — not because it is yours to tune. */
|
|
157
208
|
--text-x-slot-control-size: var(--text-x-md);
|
|
158
209
|
--text-x-slot-control-touch-size: var(--text-x-ios-floor);
|
|
210
|
+
/* The other half of the same constraint: the first says a focused control must not be
|
|
211
|
+
* small enough to make iOS zoom, this says it must not be small enough to miss. Applied
|
|
212
|
+
* only under `@media (pointer: coarse)` — pointer, not width, for the reason recorded
|
|
213
|
+
* above (an iPad in landscape is wide and still has fingers). */
|
|
214
|
+
--spacing-x-slot-control-touch-min: var(--spacing-x-touch-floor);
|
|
159
215
|
|
|
160
216
|
/* ── Colour slots ─────────────────────────────────────────────────────────────
|
|
161
217
|
* Every colour a component paints comes from here.
|
|
@@ -192,6 +248,25 @@
|
|
|
192
248
|
--color-x-slot-control-border: var(--color-border);
|
|
193
249
|
--color-x-slot-control-placeholder: var(--color-text-muted);
|
|
194
250
|
|
|
251
|
+
/* 🔴 The kit painted nothing for validity until 0.12.0 — an invalid control looked exactly
|
|
252
|
+
* like a valid one, while `aria-invalid` was set and announced. Seven of nine ships paint
|
|
253
|
+
* it themselves (Tailwind `aria-invalid:` variants, CSS selectors, or a class name), so a
|
|
254
|
+
* migration onto the kit silently removed a signal the product already had. nene-vault
|
|
255
|
+
* lost an amber border on retention periods under ten years, and the test that guarded it
|
|
256
|
+
* asserted `aria-invalid="true"` — the attribute, not the paint — so it stayed green.
|
|
257
|
+
*
|
|
258
|
+
* 🔑 A test can check a stand-in for the thing it cares about. While both live in the
|
|
259
|
+
* same component that is the same test; the moment the real thing moves upstream, the
|
|
260
|
+
* stand-in stays behind and keeps passing.
|
|
261
|
+
*
|
|
262
|
+
* Border, not a ring: `ring-*` is a box-shadow and a parent with `overflow: hidden` clips
|
|
263
|
+
* it — the same reason the focus indicator here is an `outline`. The background carries
|
|
264
|
+
* the rest, and neither competes with the focus outline. */
|
|
265
|
+
--color-x-slot-control-invalid-border: var(--color-danger);
|
|
266
|
+
--color-x-slot-control-invalid-bg: var(--color-danger-soft);
|
|
267
|
+
--color-x-slot-control-warn-border: var(--color-warn);
|
|
268
|
+
--color-x-slot-control-warn-bg: var(--color-warn-soft);
|
|
269
|
+
|
|
195
270
|
--color-x-slot-choice-fg: var(--color-text-primary);
|
|
196
271
|
--color-x-slot-choice-border: var(--color-border);
|
|
197
272
|
|
|
@@ -242,7 +317,10 @@
|
|
|
242
317
|
--color-x-slot-spinner-fg: var(--color-text-muted);
|
|
243
318
|
--color-x-slot-empty-state-fg: var(--color-text-muted);
|
|
244
319
|
--color-x-slot-error-state-fg: var(--color-danger);
|
|
245
|
-
|
|
320
|
+
/* 🔴 The required marker, not the error message — that one is `--color-x-slot-field-error`.
|
|
321
|
+
* Named `-error-fg` when the colour slots were swept in (0.9.0, same day), which reads as
|
|
322
|
+
* "the error message's colour" and is not. Renamed before anything consumed it. */
|
|
323
|
+
--color-x-slot-field-required-marker: var(--color-danger);
|
|
246
324
|
|
|
247
325
|
/* An upper bound for an `<svg>` written directly inside a `Button`.
|
|
248
326
|
*
|
|
@@ -282,6 +360,12 @@
|
|
|
282
360
|
* rather than acted on: changing the type scale is its own decision, with its own
|
|
283
361
|
* measurement, and it is not this release's. */
|
|
284
362
|
--text-x-slot-button-size: inherit;
|
|
363
|
+
/* 🔴 `sm` gets its own, because padding already has one and the pair was asymmetric:
|
|
364
|
+
* `--spacing-x-slot-button-{,sm-}pad-{x,y}` is four slots, the font was one. nene-vault
|
|
365
|
+
* shipped md=13px and sm=12px, and on the kit both came out at the same size — its
|
|
366
|
+
* pagination "Previous" measured 12px in production against 13px on the migrated build.
|
|
367
|
+
* Defaults to `inherit` like its sibling, so nothing renders differently today. */
|
|
368
|
+
--text-x-slot-button-sm-size: inherit;
|
|
285
369
|
--text-x-slot-choice-size: inherit;
|
|
286
370
|
|
|
287
371
|
/* Sentinel for the consumer's build (#316). Zero-width, so applying it does nothing.
|