@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 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 it sets them, `warn` and `danger` share this palette's single
12
- * alert hue and only the announced urgency differs — and as nene-vault put it, that
13
- * difference reaches someone listening and no one looking.
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 it sets them, `warn` and `danger` share this palette's single
13
- * alert hue and only the announced urgency differs — and as nene-vault put it, that
14
- * difference reaches someone listening and no one looking.
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;AAQlC,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,QAAQ,EAAoB;IACvE,OAAO,CACL,cACE,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAC5C,SAAS,EAAE,EAAE,CAAC,0DAA0D,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,YAE1F,QAAQ,GACL,CACP,CAAC;AACJ,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;
@@ -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-error-fg", 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 })), 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 }))] }) }));
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;AAiClD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,GAAG,IAAI,EACZ,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,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,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAElG,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,4BAA4B,YAAE,cAAc,GAAQ,CACrE,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,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"}
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
- const described = [field.errorId, field.hintId].filter((v) => v !== null).join(' ');
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;AAavE,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAC;AAW1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;SACvC,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEjG,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;KAC9E,CAAC;AACJ,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"}
@@ -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
- export declare const CONTROL_CLASS = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-x-slot-focus-ring disabled:opacity-x-disabled disabled:cursor-not-allowed";
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 = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-x-slot-focus-ring disabled:opacity-x-disabled disabled:cursor-not-allowed hover:brightness-x-slot-hover active:brightness-x-slot-press disabled:hover:brightness-100 disabled:active:brightness-100";
99
+ export declare const CLICKABLE_CLASS: string;
@@ -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
- export const CONTROL_CLASS = `${FOCUS_CLASS} ${DISABLED_CLASS}`;
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
@@ -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;AAEhE,4EAA4E;AAC5E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,aAAa,IAAI,WAAW,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 font-sans text-x-slot-button-size font-x-slot-button ${SVG_BOUND} ${CLICKABLE_CLASS}`;
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;AAcnD,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,+CAA+C;IACnD,EAAE,EAAE,qDAAqD;CAC1D,CAAC;AAEF,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,wGAAwG,SAAS,IAAI,eAAe,EAAE,CAAC;AAE1J,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
+ {"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;AACjD,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,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"}
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"}
@@ -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;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,MAAM,UAAU,GAAG,8RAA8R,aAAa,EAAE,CAAC;AAEjU;;;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"}
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"}
@@ -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;AAcjD,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,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
+ {"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;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAM3D,MAAM,UAAU,GAAG,kPAAkP,aAAa,EAAE,CAAC;AAErR;;;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
+ {"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;AAUnD;;;;;;;;;;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,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
+ {"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;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,MAAM,UAAU,GAAG,8RAA8R,aAAa,EAAE,CAAC;AAEjU;;;;;;;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"}
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.11.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",
@@ -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-danger);
131
- --color-x-slot-alert-warn-border: var(--color-danger);
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
- --color-x-slot-field-error-fg: var(--color-danger);
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.