@hideyukimori/nene2-ui 0.19.1 โ†’ 0.21.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
@@ -35,6 +35,16 @@ bound. Providing the parts scales with the number of screens, which is finite.**
35
35
  cannot express it, add a variant โ€” for everyone. _This is the principle that matters:_ the
36
36
  13,021 style violations the fleet is currently remediating exist because values could be
37
37
  written inline. Make them unwritable and there is nothing to inspect.
38
+
39
+ ๐Ÿ”ด **But "add a variant" is the wrong move when the axis is wrong** (#487). `Button` carried
40
+ one closed enum that mixed *shape* with *colour* โ€” `primary | secondary | danger | ghost` โ€”
41
+ so "the ghost shape in the danger colour" was not sayable. Five ships each invented a
42
+ private name for that one cell and no two agreed (`ghost-danger`, `danger-ghost`,
43
+ `link-danger`, `danger-outline`, and nene-deal reached it through slots). Enumerating four
44
+ more values would have left `4 shapes ร— 4 tones = 16` names to chase. 0.20.0 splits the
45
+ axis instead: `variant` is the shape, `tone` is the colour. **Before adding a value, check
46
+ whether what is missing is a value or an axis** โ€” a value that ships keep spelling with a
47
+ hyphen in the middle is usually two axes wearing one name.
38
48
  4. **No strings.** The kit ships no user-visible text. `Spinner` takes `label`, `ErrorState` takes
39
49
  `retryLabel`. Localisation stays with the product (`@hideyukimori/nene2-i18n`), so a wording
40
50
  change never becomes a kit release.
@@ -28,9 +28,9 @@ export function Pagination(props) {
28
28
  const atEnd = props.page === undefined ? !props.canNext : props.page >= props.pageCount;
29
29
  const goPrev = () => props.page === undefined ? props.onPrev() : props.onPageChange(props.page - 1);
30
30
  const goNext = () => props.page === undefined ? props.onNext() : props.onPageChange(props.page + 1);
31
- const prev = (_jsx(Button, { variant: "secondary", size: size, disabled: atStart, onClick: goPrev, "aria-label": previousLabel, children: previousLabel }, "prev"));
31
+ const prev = (_jsx(Button, { variant: "outline", tone: "neutral", size: size, disabled: atStart, onClick: goPrev, "aria-label": previousLabel, children: previousLabel }, "prev"));
32
32
  const current = (_jsx("span", { "aria-current": "page", className: "font-sans text-x-slot-pagination-fg", children: status }, "status"));
33
- const next = (_jsx(Button, { variant: "secondary", size: size, disabled: atEnd, onClick: goNext, "aria-label": nextLabel, children: nextLabel }, "next"));
33
+ const next = (_jsx(Button, { variant: "outline", tone: "neutral", size: size, disabled: atEnd, onClick: goNext, "aria-label": nextLabel, children: nextLabel }, "next"));
34
34
  const order = statusPlacement === 'start'
35
35
  ? [current, prev, next]
36
36
  : statusPlacement === 'end'
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","sourceRoot":"","sources":["../../src/data/Pagination.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAiElC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,MAAM,EACJ,KAAK,EACL,aAAa,EACb,SAAS,EACT,MAAM,EACN,SAAS,EACT,IAAI,GAAG,IAAI,EACX,aAAa,EACb,eAAe,GAAG,QAAQ,GAC3B,GAAG,KAAK,CAAC;IAEV,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC;IAExF,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAEjF,MAAM,IAAI,GAAG,CACX,KAAC,MAAM,IAEL,OAAO,EAAC,WAAW,EACnB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,MAAM,gBACH,aAAa,YAExB,aAAa,IAPV,MAAM,CAQH,CACV,CAAC;IACF,MAAM,OAAO,GAAG,CACd,+BAAgC,MAAM,EAAC,SAAS,EAAC,qCAAqC,YACnF,MAAM,IADC,QAAQ,CAEX,CACR,CAAC;IACF,MAAM,IAAI,GAAG,CACX,KAAC,MAAM,IAEL,OAAO,EAAC,WAAW,EACnB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,KAAK,EACf,OAAO,EAAE,MAAM,gBACH,SAAS,YAEpB,SAAS,IAPN,MAAM,CAQH,CACV,CAAC;IACF,MAAM,KAAK,GACT,eAAe,KAAK,OAAO;QACzB,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;QACvB,CAAC,CAAC,eAAe,KAAK,KAAK;YACzB,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC;YACvB,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAE9B,OAAO,CACL,4BAAiB,KAAK,EAAE,SAAS,EAAE,SAAS,YAC1C,KAAC,KAAK,IACJ,SAAS,EAAC,YAAY,EACtB,GAAG,EAAC,KAAK,EACT,KAAK,EAAC,QAAQ;YACd,6EAA6E;YAC7E,SAAS,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,IAAI,sCAAsC,CAAC,YAE9E,KAAK,GACA,GACJ,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"Pagination.js","sourceRoot":"","sources":["../../src/data/Pagination.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAiElC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,MAAM,EACJ,KAAK,EACL,aAAa,EACb,SAAS,EACT,MAAM,EACN,SAAS,EACT,IAAI,GAAG,IAAI,EACX,aAAa,EACb,eAAe,GAAG,QAAQ,GAC3B,GAAG,KAAK,CAAC;IAEV,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC;IAExF,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAEjF,MAAM,IAAI,GAAG,CACX,KAAC,MAAM,IAEL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,SAAS,EACd,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,MAAM,gBACH,aAAa,YAExB,aAAa,IARV,MAAM,CASH,CACV,CAAC;IACF,MAAM,OAAO,GAAG,CACd,+BAAgC,MAAM,EAAC,SAAS,EAAC,qCAAqC,YACnF,MAAM,IADC,QAAQ,CAEX,CACR,CAAC;IACF,MAAM,IAAI,GAAG,CACX,KAAC,MAAM,IAEL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,SAAS,EACd,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,KAAK,EACf,OAAO,EAAE,MAAM,gBACH,SAAS,YAEpB,SAAS,IARN,MAAM,CASH,CACV,CAAC;IACF,MAAM,KAAK,GACT,eAAe,KAAK,OAAO;QACzB,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;QACvB,CAAC,CAAC,eAAe,KAAK,KAAK;YACzB,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC;YACvB,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAE9B,OAAO,CACL,4BAAiB,KAAK,EAAE,SAAS,EAAE,SAAS,YAC1C,KAAC,KAAK,IACJ,SAAS,EAAC,YAAY,EACtB,GAAG,EAAC,KAAK,EACT,KAAK,EAAC,QAAQ;YACd,6EAA6E;YAC7E,SAAS,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,IAAI,sCAAsC,CAAC,YAE9E,KAAK,GACA,GACJ,CACP,CAAC;AACJ,CAAC"}
@@ -10,6 +10,22 @@ export interface BadgeProps {
10
10
  * badge as it does in an alert (0.17.0, #422).
11
11
  */
12
12
  tone?: 'neutral' | 'accent' | 'danger' | 'success' | 'warn' | 'info';
13
+ /**
14
+ * Draw a small filled circle before the label.
15
+ *
16
+ * ๐Ÿ”ด It takes its colour from `currentColor`, so there is no dot colour slot and there
17
+ * must not be one: the dot is the tone, shown small. A slot would let a product set a
18
+ * badge whose dot disagrees with its own text, and it would have to be redefined for
19
+ * every tone โ€” six values to keep in step with six that already exist (the
20
+ * redefine-the-default shape nene-vault retired in #481).
21
+ *
22
+ * Whether a badge has one is a per-call-site decision, not a theme value: nene-clear
23
+ * renders dotted and undotted badges **on the same screen** (`DunningPage` marks overdue
24
+ * with a dot and paused without one), so it cannot be a slot. Hence a prop.
25
+ *
26
+ * Omit it and the render is byte-for-byte what it was before this prop existed.
27
+ */
28
+ dot?: boolean;
13
29
  /** Composed after the kit's own classes (design principle 2). */
14
30
  className?: string;
15
31
  children: ReactNode;
@@ -36,4 +52,4 @@ export interface BadgeProps {
36
52
  *
37
53
  * A badge that already fits is unchanged (35.88 ร— 26.5 before and after, measured).
38
54
  */
39
- export declare function Badge({ tone, className, children }: BadgeProps): import("react").JSX.Element;
55
+ export declare function Badge({ tone, dot, className, children }: BadgeProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../lib/cx.js';
3
3
  const TONE_CLASS = {
4
4
  neutral: 'bg-x-slot-badge-neutral-bg text-x-slot-badge-neutral-fg border-x-slot-badge-neutral-border',
@@ -30,7 +30,7 @@ const TONE_CLASS = {
30
30
  *
31
31
  * A badge that already fits is unchanged (35.88 ร— 26.5 before and after, measured).
32
32
  */
33
- export function Badge({ tone = 'neutral', className, children }) {
34
- return (_jsx("span", { className: cx('inline-flex items-center whitespace-nowrap gap-x-slot-badge-gap rounded-x-slot-badge border px-x-slot-badge-pad-x py-x-slot-badge-pad-y font-sans text-x-slot-badge font-x-slot-badge', TONE_CLASS[tone], className), children: children }));
33
+ export function Badge({ tone = 'neutral', dot = false, className, children }) {
34
+ return (_jsxs("span", { className: cx('inline-flex items-center whitespace-nowrap gap-x-slot-badge-gap rounded-x-slot-badge border px-x-slot-badge-pad-x py-x-slot-badge-pad-y font-sans text-x-slot-badge font-x-slot-badge', TONE_CLASS[tone], className), children: [dot && (_jsx("span", { "aria-hidden": "true", className: "size-x-slot-badge-dot shrink-0 rounded-full bg-current" })), children] }));
35
35
  }
36
36
  //# sourceMappingURL=Badge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/feedback/Badge.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAkBlC,MAAM,UAAU,GAAoD;IAClE,OAAO,EACL,4FAA4F;IAC9F,MAAM,EAAE,yFAAyF;IACjG,MAAM,EAAE,yFAAyF;IACjG,OAAO,EACL,4FAA4F;IAC9F,IAAI,EAAE,mFAAmF;IACzF,IAAI,EAAE,mFAAmF;CAC1F,CAAC;AAEF;;;;;;GAMG;AACH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,KAAK,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAc;IACzE,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,uLAAuL,EACvL,UAAU,CAAC,IAAI,CAAC,EAChB,SAAS,CACV,YAEA,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/feedback/Badge.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAkClC,MAAM,UAAU,GAAoD;IAClE,OAAO,EACL,4FAA4F;IAC9F,MAAM,EAAE,yFAAyF;IACjG,MAAM,EAAE,yFAAyF;IACjG,OAAO,EACL,4FAA4F;IAC9F,IAAI,EAAE,mFAAmF;IACzF,IAAI,EAAE,mFAAmF;CAC1F,CAAC;AAEF;;;;;;GAMG;AACH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,KAAK,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,GAAG,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAc;IACtF,OAAO,CACL,gBACE,SAAS,EAAE,EAAE,CACX,uLAAuL,EACvL,UAAU,CAAC,IAAI,CAAC,EAChB,SAAS,CACV,aAQA,GAAG,IAAI,CACN,8BACc,MAAM,EAClB,SAAS,EAAC,wDAAwD,GAClE,CACH,EACA,QAAQ,IACJ,CACR,CAAC;AACJ,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  export interface InlineAlertProps {
3
- /** What the message means. `danger` is announced assertively; `info` politely. */
4
- tone?: 'info' | 'warn' | 'danger';
3
+ /** What the message means. `danger` is announced assertively; the rest politely. */
4
+ tone?: 'info' | 'warn' | 'danger' | 'success';
5
5
  /**
6
6
  * ๐Ÿ”ด Needed to point an `aria-describedby` at this message. Without it a product that
7
7
  * wants the alert read out with a control has nowhere to put an id, and nene-vault
@@ -22,8 +22,14 @@ export interface InlineAlertProps {
22
22
  * someone listening and no one looking. The palette now carries `warn` of its own.
23
23
  *
24
24
  * ๐Ÿ”ด The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
25
- * which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
26
- * Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
27
- * of role is precisely the part that is easy to get wrong and invisible when you do.
25
+ * which interrupts a screen reader; every other tone becomes `role="status"`, which waits
26
+ * its turn. Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and
27
+ * the choice of role is precisely the part that is easy to get wrong and invisible when you
28
+ * do.
29
+ *
30
+ * ๐Ÿ”ด `success` (#486) is deliberately **polite**, not assertive. Good news does not earn an
31
+ * interruption, and `ToastProvider` already settled the same question the same way
32
+ * (`POLITE_TONES = ['info', 'success']`). Announcing a success assertively would cut off
33
+ * whatever the person was reading to tell them nothing went wrong.
28
34
  */
29
35
  export declare function InlineAlert({ tone, id, className, children }: InlineAlertProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ const TONE_CLASS = {
4
4
  info: 'bg-x-slot-alert-info-bg text-x-slot-alert-info-fg border-x-slot-alert-info-border',
5
5
  warn: 'bg-x-slot-alert-warn-bg text-x-slot-alert-warn-fg border-x-slot-alert-warn-border',
6
6
  danger: 'bg-x-slot-alert-danger-bg text-x-slot-alert-danger-fg border-x-slot-alert-danger-border',
7
+ success: 'bg-x-slot-alert-success-bg text-x-slot-alert-success-fg border-x-slot-alert-success-border',
7
8
  };
8
9
  /**
9
10
  * A message attached to the thing it is about, rather than to the page.
@@ -14,9 +15,15 @@ const TONE_CLASS = {
14
15
  * someone listening and no one looking. The palette now carries `warn` of its own.
15
16
  *
16
17
  * ๐Ÿ”ด The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
17
- * which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
18
- * Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
19
- * of role is precisely the part that is easy to get wrong and invisible when you do.
18
+ * which interrupts a screen reader; every other tone becomes `role="status"`, which waits
19
+ * its turn. Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and
20
+ * the choice of role is precisely the part that is easy to get wrong and invisible when you
21
+ * do.
22
+ *
23
+ * ๐Ÿ”ด `success` (#486) is deliberately **polite**, not assertive. Good news does not earn an
24
+ * interruption, and `ToastProvider` already settled the same question the same way
25
+ * (`POLITE_TONES = ['info', 'success']`). Announcing a success assertively would cut off
26
+ * whatever the person was reading to tell them nothing went wrong.
20
27
  */
21
28
  export function InlineAlert({ tone = 'info', id, className, children }) {
22
29
  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 }));
@@ -1 +1 @@
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"}
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;IACjG,OAAO,EACL,4FAA4F;CAC/F,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;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"}
@@ -4,6 +4,24 @@ export interface PageHeaderProps {
4
4
  className?: string;
5
5
  /** Localized page title. */
6
6
  title: string;
7
+ /**
8
+ * Localized one-line description of the page, under the title.
9
+ *
10
+ * ๐Ÿ”ด Not decoration. nene-clear carries one on **every one of its ten pages** โ€” the prop
11
+ * is `sub` there and the value is always a catalog key (`t('dunning.subtitle')`,
12
+ * `t('audit.subtitle')`, โ€ฆ), so folding it into `title` is not available to it: the two
13
+ * halves are separate keys, and joining them would put the separator โ€” a string โ€” in the
14
+ * ship, against principle 4. `PageHeader` takes no `children` either, so without this prop
15
+ * the line has nowhere to go and ten pages lose their description (#492).
16
+ *
17
+ * Omit it and the render is byte-for-byte what it was before this prop existed.
18
+ *
19
+ * โš ๏ธ Its size is not a slot, on purpose. The title has no size slot either โ€” an `<h1>`
20
+ * reset by Preflight inherits the body size โ€” so adding one here alone would let a product
21
+ * scale the description while its title stayed put. Products that need a type scale of
22
+ * their own are blocked on the same missing thing, and it is one gap, not two.
23
+ */
24
+ description?: string;
7
25
  actions?: ReactNode;
8
26
  }
9
- export declare function PageHeader({ title, actions, className }: PageHeaderProps): import("react").JSX.Element;
27
+ export declare function PageHeader({ title, description, actions, className }: PageHeaderProps): import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../lib/cx.js';
3
- export function PageHeader({ title, actions, className }) {
4
- return (_jsxs("header", { className: cx('flex items-center justify-between py-x-slot-page-header-pad-y', className), children: [_jsx("h1", { className: "font-sans font-x-slot-page-header text-x-slot-page-header-fg", children: title }), actions] }));
3
+ export function PageHeader({ title, description, actions, className }) {
4
+ const heading = (_jsx("h1", { className: "font-sans font-x-slot-page-header text-x-slot-page-header-fg", children: title }));
5
+ return (_jsxs("header", { className: cx('flex items-center justify-between py-x-slot-page-header-pad-y', className), children: [description === undefined ? (heading) : (_jsxs("div", { children: [heading, _jsx("p", { className: "mt-x-slot-page-header-gap font-sans text-x-slot-page-header-description-fg", children: description })] })), actions] }));
5
6
  }
6
7
  //# sourceMappingURL=PageHeader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader.js","sourceRoot":"","sources":["../../src/layout/PageHeader.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAUlC,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAmB;IACvE,OAAO,CACL,kBACE,SAAS,EAAE,EAAE,CAAC,+DAA+D,EAAE,SAAS,CAAC,aAEzF,aAAI,SAAS,EAAC,8DAA8D,YAAE,KAAK,GAAM,EACxF,OAAO,IACD,CACV,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"PageHeader.js","sourceRoot":"","sources":["../../src/layout/PageHeader.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AA4BlC,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAmB;IACpF,MAAM,OAAO,GAAG,CACd,aAAI,SAAS,EAAC,8DAA8D,YAAE,KAAK,GAAM,CAC1F,CAAC;IAEF,OAAO,CACL,kBACE,SAAS,EAAE,EAAE,CAAC,+DAA+D,EAAE,SAAS,CAAC,aAMxF,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,CAC3B,OAAO,CACR,CAAC,CAAC,CAAC,CACF,0BACG,OAAO,EACR,YAAG,SAAS,EAAC,4EAA4E,YACtF,WAAW,GACV,IACA,CACP,EACA,OAAO,IACD,CACV,CAAC;AACJ,CAAC"}
@@ -12,6 +12,6 @@ import { Modal } from './Modal.js';
12
12
  * kind of detail that differs between five independent implementations.
13
13
  */
14
14
  export function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, tone = 'default', }) {
15
- return (_jsx(Modal, { open: open, onClose: onCancel, title: title, children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { children: message }), _jsxs(Stack, { direction: "horizontal", gap: "2xs", justify: "end", children: [_jsx(Button, { variant: "secondary", onClick: onCancel, children: cancelLabel }), _jsx(Button, { variant: tone === 'danger' ? 'danger' : 'primary', onClick: onConfirm, children: confirmLabel })] })] }) }));
15
+ return (_jsx(Modal, { open: open, onClose: onCancel, title: title, children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { children: message }), _jsxs(Stack, { direction: "horizontal", gap: "2xs", justify: "end", children: [_jsx(Button, { variant: "outline", tone: "neutral", onClick: onCancel, children: cancelLabel }), _jsx(Button, { tone: tone === 'danger' ? 'danger' : 'accent', onClick: onConfirm, children: confirmLabel })] })] }) }));
16
16
  }
17
17
  //# sourceMappingURL=ConfirmDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../src/overlay/ConfirmDialog.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAkBnC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,YAAY,EACZ,WAAW,EACX,SAAS,EACT,QAAQ,EACR,IAAI,GAAG,SAAS,GACG;IACnB,OAAO,CACL,KAAC,KAAK,IAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,YAChD,MAAC,KAAK,IAAC,GAAG,EAAC,IAAI,aACb,KAAC,IAAI,cAAE,OAAO,GAAQ,EACtB,MAAC,KAAK,IAAC,SAAS,EAAC,YAAY,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,EAAC,KAAK,aACnD,KAAC,MAAM,IAAC,OAAO,EAAC,WAAW,EAAC,OAAO,EAAE,QAAQ,YAC1C,WAAW,GACL,EACT,KAAC,MAAM,IAAC,OAAO,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,YAC1E,YAAY,GACN,IACH,IACF,GACF,CACT,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../src/overlay/ConfirmDialog.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAkBnC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,YAAY,EACZ,WAAW,EACX,SAAS,EACT,QAAQ,EACR,IAAI,GAAG,SAAS,GACG;IACnB,OAAO,CACL,KAAC,KAAK,IAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,YAChD,MAAC,KAAK,IAAC,GAAG,EAAC,IAAI,aACb,KAAC,IAAI,cAAE,OAAO,GAAQ,EACtB,MAAC,KAAK,IAAC,SAAS,EAAC,YAAY,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,EAAC,KAAK,aACnD,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,SAAS,EAAC,OAAO,EAAE,QAAQ,YACvD,WAAW,GACL,EAIT,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,YACtE,YAAY,GACN,IACH,IACF,GACF,CACT,CAAC;AACJ,CAAC"}
@@ -28,6 +28,20 @@ interface ModalBase {
28
28
  * `showModal`, so it can only be verified in a real browser (see #392, live lane).
29
29
  */
30
30
  scrollable?: boolean;
31
+ /**
32
+ * The dialog's actions, laid out below the body and **outside its scroll area**.
33
+ *
34
+ * ๐Ÿ”ด Not a matter of layout โ€” a matter of structure. Pushed into `children`, the action row
35
+ * joins the body's scroll region, so with `scrollable` it slides off the bottom of a tall
36
+ * dialog and the person cannot reach "Confirm". nene-clear uses a footer in **11 of 11**
37
+ * dialogs and makes it a required prop (#493); eight ships wrote a modal and every one of
38
+ * them drew this row.
39
+ *
40
+ * The kit lays the row out end-aligned with a gap, which is what those eleven do. A product
41
+ * that wants another arrangement composes it inside โ€” `footer={<div className="flex w-full
42
+ * justify-between">โ€ฆ</div>}` โ€” rather than the kit growing an alignment prop.
43
+ */
44
+ footer?: ReactNode;
31
45
  children: ReactNode;
32
46
  }
33
47
  interface PlainModal extends ModalBase {
@@ -37,6 +51,19 @@ interface PlainModal extends ModalBase {
37
51
  interface HeaderModal extends ModalBase {
38
52
  /** Draw a header: the title, and a control that closes the dialog. */
39
53
  header: true;
54
+ /**
55
+ * Localized one-line description, under the title.
56
+ *
57
+ * ๐Ÿ”ด On `HeaderModal` only, and deliberately. Without a header the title is never drawn โ€”
58
+ * it becomes the dialog's `aria-label` โ€” so a description would have nothing to sit under
59
+ * and would be dropped in silence. The same reasoning `closeLabel` uses below: let the
60
+ * compiler ask for it exactly where it is going to be rendered.
61
+ *
62
+ * ๐Ÿ”ด Named `description`, matching `PageHeader` (#492/#497) and `EmptyState` (#456), not
63
+ * clear's local `sub`. Nine of clear's eleven dialogs carry this line (#493). One word for
64
+ * one thing across the kit โ€” the rule `Badge`'s tone vocabulary states, applied to a prop.
65
+ */
66
+ description?: string;
40
67
  /**
41
68
  * Localized name for the close control.
42
69
  *
@@ -33,7 +33,7 @@ const SHEET_CLASS = 'max-sm:mt-auto max-sm:mb-0 max-sm:w-full max-sm:max-w-none
33
33
  * up until a consumer overrode one side of a pair.
34
34
  */
35
35
  export function Modal(props) {
36
- const { open, onClose, title, size, sheetOnMobile, scrollable, children } = props;
36
+ const { open, onClose, title, size, sheetOnMobile, scrollable, footer, children } = props;
37
37
  const ref = useRef(null);
38
38
  const titleId = useId();
39
39
  useEffect(() => {
@@ -72,6 +72,13 @@ export function Modal(props) {
72
72
  // The sheet classes below (`max-sm:mt-auto max-sm:mb-0`) always assumed this margin was
73
73
  // there; saying it explicitly is the kit owning an assumption it had been borrowing.
74
74
  // jsdom does not implement `showModal`, so the position itself is a live-lane check.
75
- 'm-auto bg-x-slot-modal-bg text-x-slot-modal-fg border border-x-slot-modal-border rounded-x-slot-modal', 'p-x-slot-modal-pad font-sans backdrop:bg-x-slot-modal-scrim/50', size !== undefined && SIZE_CLASS[size], sheetOnMobile === true && SHEET_CLASS, scrollable === true && 'flex flex-col'), children: [props.header === true && (_jsxs("header", { className: "mb-x-slot-modal-header-gap flex items-start justify-between gap-x-slot-modal-header-gap", children: [_jsx("h2", { id: titleId, className: "font-x-slot-modal-title text-x-slot-modal-title-size", children: title }), _jsx("button", { type: "button", "aria-label": props.closeLabel, onClick: onClose, className: "rounded-x-slot-control text-x-slot-modal-close-fg leading-none hover:brightness-x-slot-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-x-slot-focus-ring", children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] })), scrollable === true ? _jsx("div", { className: "min-h-0 overflow-y-auto", children: children }) : children] }));
75
+ 'm-auto bg-x-slot-modal-bg text-x-slot-modal-fg border border-x-slot-modal-border rounded-x-slot-modal', 'p-x-slot-modal-pad font-sans backdrop:bg-x-slot-modal-scrim/50', size !== undefined && SIZE_CLASS[size], sheetOnMobile === true && SHEET_CLASS,
76
+ // The column is what keeps the footer out of the scroll region: the body flexes and
77
+ // scrolls, the footer keeps its height. Without `scrollable` there is nothing to
78
+ // scroll, so the dialog keeps the block layout every caller has had until now.
79
+ scrollable === true && 'flex flex-col'), children: [props.header === true && (_jsxs("header", { className: "mb-x-slot-modal-header-gap flex items-start justify-between gap-x-slot-modal-header-gap", children: [props.description === undefined ? (_jsx("h2", { id: titleId, className: "font-x-slot-modal-title text-x-slot-modal-title-size", children: title })) : (_jsxs("div", { children: [_jsx("h2", { id: titleId, className: "font-x-slot-modal-title text-x-slot-modal-title-size", children: title }), _jsx("p", { className: "mt-x-slot-modal-description-gap font-sans text-x-slot-modal-description-fg", children: props.description })] })), _jsx("button", { type: "button", "aria-label": props.closeLabel, onClick: onClose, className: "rounded-x-slot-control text-x-slot-modal-close-fg leading-none hover:brightness-x-slot-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-x-slot-focus-ring", children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] })), scrollable === true ? _jsx("div", { className: "min-h-0 overflow-y-auto", children: children }) : children, footer !== undefined && (
80
+ // `shrink-0` matters only in the scrollable column, where the body would otherwise
81
+ // take the footer's height with it; it is inert in the block layout.
82
+ _jsx("footer", { className: "mt-x-slot-modal-footer-gap flex shrink-0 justify-end gap-x-slot-modal-footer-gap", children: footer }))] }));
76
83
  }
77
84
  //# sourceMappingURL=Modal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAuDlC,MAAM,UAAU,GAA8B;IAC5C,EAAE,EAAE,uBAAuB;IAC3B,EAAE,EAAE,uBAAuB;IAC3B,EAAE,EAAE,uBAAuB;CAC5B,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,GACf,kFAAkF,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClF,MAAM,GAAG,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO;QAExB,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,OAAO,EAAE,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;gBACvC,IAAI,CAAC,EAAE,CAAC,IAAI;oBAAE,EAAE,CAAC,SAAS,EAAE,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,OAAO,EAAE,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACnC,IAAI,EAAE,CAAC,IAAI;gBAAE,EAAE,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,yFAAyF;IACzF,4EAA4E;IAC5E,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAEvF,OAAO,CACL,kBACE,GAAG,EAAE,GAAG,KACJ,MAAM;QACV,qFAAqF;QACrF,wEAAwE;QACxE,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,EAAE;QACX,qFAAqF;QACrF,yFAAyF;QACzF,0FAA0F;QAC1F,uFAAuF;QACvF,wFAAwF;QACxF,qFAAqF;QACrF,qFAAqF;QACrF,uGAAuG,EACvG,gEAAgE,EAChE,IAAI,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,CAAC,EACtC,aAAa,KAAK,IAAI,IAAI,WAAW,EACrC,UAAU,KAAK,IAAI,IAAI,eAAe,CACvC,aAEA,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,CACxB,kBAAQ,SAAS,EAAC,yFAAyF,aACzG,aAAI,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,sDAAsD,YAC9E,KAAK,GACH,EACL,iBACE,IAAI,EAAC,QAAQ,gBACD,KAAK,CAAC,UAAU,EAC5B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,6LAA6L,YAIvM,8BAAkB,MAAM,uBAAc,GAC/B,IACF,CACV,EACA,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,yBAAyB,YAAE,QAAQ,GAAO,CAAC,CAAC,CAAC,QAAQ,IACpF,CACV,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,37 @@
1
1
  import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
2
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
3
- variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
3
+ /**
4
+ * The shape: which properties get painted. Not which colour they get painted in.
5
+ *
6
+ * ๐Ÿ”ด This used to be one closed enum that mixed shape and colour
7
+ * (`primary | secondary | danger | ghost`), so "the ghost shape in the danger colour" was
8
+ * not expressible. Five ships each invented their own name for that one missing cell, and
9
+ * no two agreed: nene-clear `ghost-danger`, nene-field `danger-ghost` (the same thing,
10
+ * written backwards), nene-profile `link-danger`, nene-origin `danger-outline`, and
11
+ * nene-deal reached the same place through slots instead โ€” it set
12
+ * `--color-x-slot-button-danger-bg: transparent` and turned the filled danger into an
13
+ * outlined one (#455). One need, five workarounds, because the API could not say it.
14
+ *
15
+ * ๐Ÿ”ด `bare`, not `ghost`. The old name meant "no fill, no border" here, but nene-clear's
16
+ * `.btn-ghost` has both a background and a border โ€” it is this kit's `outline`. The word is
17
+ * split across the industry too (Bootstrap's ghost is an outline, Material's is a text
18
+ * button), so a ship reading `ghost` cannot know which one it got. Renaming costs nothing:
19
+ * neither consuming ship uses it (nene-vault 0, nene-deal 0, measured 2026-08-27).
20
+ */
21
+ variant?: 'solid' | 'outline' | 'bare' | 'link';
22
+ /**
23
+ * The colour: what the shape means. Not how loud it is.
24
+ *
25
+ * ๐Ÿ”ด The same six words `Badge` has carried since 0.17.0, in the same order, painted from
26
+ * the same shape of slot. A tone means one thing across the kit or it means nothing.
27
+ *
28
+ * ๐Ÿ”ด The default is `accent`, not `neutral`. What 0.19.x called `primary` was never a tone
29
+ * โ€” it was "the accent-coloured filled button", so `<Button>` keeps rendering exactly that.
30
+ * `neutral` is the quiet grey step, which is what `Badge` has always meant by the word.
31
+ * Reading `neutral` as "the ordinary action" is how this kit ended up with two vocabularies
32
+ * in the first place.
33
+ */
34
+ tone?: 'neutral' | 'accent' | 'danger' | 'success' | 'warn' | 'info';
4
35
  /**
5
36
  * Two sizes, not a length. `sm` for dense rows and toolbars.
6
37
  *
@@ -10,4 +41,4 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
10
41
  size?: 'md' | 'sm';
11
42
  children: ReactNode;
12
43
  }
13
- export declare function Button({ variant, size, children, type, className, ...rest }: ButtonProps): import("react").JSX.Element;
44
+ export declare function Button({ variant, tone, size, children, type, className, ...rest }: ButtonProps): import("react").JSX.Element;
@@ -1,18 +1,82 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../lib/cx.js';
3
3
  import { CLICKABLE_CLASS, TOUCH_CLASS } from '../lib/states.js';
4
- const VARIANT_CLASS = {
5
- primary: 'bg-x-slot-button-primary-bg text-x-slot-button-primary-fg shadow-x-slot-button-primary',
6
- secondary: 'bg-x-slot-button-secondary-bg text-x-slot-button-secondary-fg border-x-slot-button-secondary-border',
7
- danger: 'bg-x-slot-button-danger-bg text-x-slot-button-danger-fg border-x-slot-button-danger-border',
8
- // No fill and no border: for the third action in a row, where two framed buttons would
9
- // compete with each other. nene-vault ships this variant already.
10
- ghost: 'bg-transparent text-x-slot-button-ghost-fg',
4
+ /**
5
+ * Shape ร— tone, written out.
6
+ *
7
+ * ๐Ÿ”ด Sixteen literal strings and no interpolation, because Tailwind reads the source as text.
8
+ * `` `bg-x-slot-button-${tone}-bg` `` produces the right class at runtime and no CSS for it at
9
+ * build time โ€” the generator never sees the string, so the rule is never emitted. The kit has
10
+ * to spell every combination it supports.
11
+ *
12
+ * ๐Ÿ”ด `solid` paints `-bg` + `-fg`; every other shape paints `-ink`. They are separate slots
13
+ * because they are separate colours: on a filled danger button the text is the on-accent
14
+ * colour, and on an outlined one it is the danger colour itself. Deriving one from the other
15
+ * would have made the outlined button's label white on white.
16
+ *
17
+ * ๐Ÿ”ด `outline`'s background is a single slot that does not vary with tone. nene-clear's
18
+ * outlined danger sits on the plain surface (`background: var(--surface)`) and only reaches
19
+ * for a tinted one on hover; painting the rest a tone-tinted colour would be louder than what
20
+ * any ship draws today.
21
+ */
22
+ const SHAPE_TONE_CLASS = {
23
+ solid: {
24
+ neutral: 'bg-x-slot-button-neutral-bg text-x-slot-button-neutral-fg shadow-x-slot-button-solid',
25
+ accent: 'bg-x-slot-button-accent-bg text-x-slot-button-accent-fg shadow-x-slot-button-solid',
26
+ danger: 'bg-x-slot-button-danger-bg text-x-slot-button-danger-fg shadow-x-slot-button-solid',
27
+ success: 'bg-x-slot-button-success-bg text-x-slot-button-success-fg shadow-x-slot-button-solid',
28
+ warn: 'bg-x-slot-button-warn-bg text-x-slot-button-warn-fg shadow-x-slot-button-solid',
29
+ info: 'bg-x-slot-button-info-bg text-x-slot-button-info-fg shadow-x-slot-button-solid',
30
+ },
31
+ outline: {
32
+ neutral: 'bg-x-slot-button-outline-bg text-x-slot-button-neutral-ink border-x-slot-button-neutral-border',
33
+ accent: 'bg-x-slot-button-outline-bg text-x-slot-button-accent-ink border-x-slot-button-accent-border',
34
+ danger: 'bg-x-slot-button-outline-bg text-x-slot-button-danger-ink border-x-slot-button-danger-border',
35
+ success: 'bg-x-slot-button-outline-bg text-x-slot-button-success-ink border-x-slot-button-success-border',
36
+ warn: 'bg-x-slot-button-outline-bg text-x-slot-button-warn-ink border-x-slot-button-warn-border',
37
+ info: 'bg-x-slot-button-outline-bg text-x-slot-button-info-ink border-x-slot-button-info-border',
38
+ },
39
+ bare: {
40
+ neutral: 'bg-transparent text-x-slot-button-neutral-ink',
41
+ accent: 'bg-transparent text-x-slot-button-accent-ink',
42
+ danger: 'bg-transparent text-x-slot-button-danger-ink',
43
+ success: 'bg-transparent text-x-slot-button-success-ink',
44
+ warn: 'bg-transparent text-x-slot-button-warn-ink',
45
+ info: 'bg-transparent text-x-slot-button-info-ink',
46
+ },
47
+ link: {
48
+ neutral: 'bg-transparent text-x-slot-button-neutral-ink underline-offset-4 hover:underline',
49
+ accent: 'bg-transparent text-x-slot-button-accent-ink underline-offset-4 hover:underline',
50
+ danger: 'bg-transparent text-x-slot-button-danger-ink underline-offset-4 hover:underline',
51
+ success: 'bg-transparent text-x-slot-button-success-ink underline-offset-4 hover:underline',
52
+ warn: 'bg-transparent text-x-slot-button-warn-ink underline-offset-4 hover:underline',
53
+ info: 'bg-transparent text-x-slot-button-info-ink underline-offset-4 hover:underline',
54
+ },
11
55
  };
12
- const SIZE_CLASS = {
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',
56
+ /**
57
+ * ๐Ÿ”ด Padding and type size are separate because `link` takes one and not the other.
58
+ *
59
+ * A link-shaped button is a run of text inside a sentence or a table cell; padding would push
60
+ * it off the line it belongs to. nene-clear's `.btn-link` sets `padding: 0` and
61
+ * nene-profile's renders through a `linkbtn` class that does the same. The type size still
62
+ * applies โ€” it is the same two sizes as every other button.
63
+ */
64
+ const PAD_CLASS = {
65
+ md: 'px-x-slot-button-pad-x py-x-slot-button-pad-y',
66
+ sm: 'px-x-slot-button-sm-pad-x py-x-slot-button-sm-pad-y',
15
67
  };
68
+ const TEXT_CLASS = {
69
+ md: 'text-x-slot-button-size',
70
+ sm: 'text-x-slot-button-sm-size',
71
+ };
72
+ /**
73
+ * `link` drops the box the other three shapes share.
74
+ *
75
+ * ๐Ÿ”ด `border-0` rather than leaving the transparent border in place: the border is there so a
76
+ * bordered button matches an unbordered one in height, and a link has no height to match.
77
+ * `rounded-none` for the same reason โ€” there is no box to round.
78
+ */
79
+ const LINK_UNBOX = 'border-0 rounded-none';
16
80
  // ๐Ÿ”ด `inline-flex`, so the button places what is inside it. A `<button>` is `inline-block`
17
81
  // by default, which leaves an icon and its label on a shared baseline rather than centred โ€”
18
82
  // visible as a half-pixel of drift on every icon button, and invisible in a diff. The kit
@@ -42,8 +106,21 @@ const SIZE_CLASS = {
42
106
  // instead of its own. A maximum cannot collide with a height: different property, and it
43
107
  // leaves anything already smaller alone.
44
108
  const SVG_BOUND = '[&_svg]:max-h-x-slot-button-icon [&_svg]:max-w-x-slot-button-icon';
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}`;
46
- export function Button({ variant = 'primary', size = 'md', children, type = 'button', className, ...rest }) {
47
- return (_jsx("button", { type: type, className: cx(BASE_CLASS, SIZE_CLASS[size], VARIANT_CLASS[variant], className), ...rest, children: children }));
109
+ // ๐Ÿ”ด `whitespace-nowrap` is load-bearing (#501). A button's label is a command, not body
110
+ // copy: it names one action, and a wrapped command reads as two. nene-clear measured 15
111
+ // buttons wrapping on its SP layout (PC 1440x900: zero) โ€” "็ฃไฟƒใ‚’้€ไฟก" became four lines at
112
+ // 70px, and "ๆถˆ่พผใ‚’็ขบๅฎš" broke *inside* a word as "ๆถˆ่พผใ‚’ / ็ขบๅฎš".
113
+ //
114
+ // ๐Ÿ”ด CJK breaks at any character and Latin breaks at word boundaries, so fixing one is not
115
+ // evidence about the other (nene-vault, #477). `nowrap` stops both.
116
+ //
117
+ // This is the same property `Badge` got in #477. It was added there by naming the component
118
+ // rather than by deriving which components need it, so `Button` was simply not on the list โ€”
119
+ // the third instance of that shape (#486 / #501 / #503). The inventory that would have
120
+ // caught all of them at once is a separate, larger piece of work; this line is the stanch.
121
+ const BASE_CLASS = `whitespace-nowrap 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}`;
122
+ export function Button({ variant = 'solid', tone = 'accent', size = 'md', children, type = 'button', className, ...rest }) {
123
+ const isLink = variant === 'link';
124
+ return (_jsx("button", { type: type, className: cx(BASE_CLASS, isLink ? LINK_UNBOX : PAD_CLASS[size], TEXT_CLASS[size], SHAPE_TONE_CLASS[variant][tone], className), ...rest, children: children }));
48
125
  }
49
126
  //# sourceMappingURL=Button.js.map
@@ -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,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAchE,MAAM,aAAa,GAAwD;IACzE,OAAO,EAAE,wFAAwF;IACjG,SAAS,EACP,qGAAqG;IACvG,MAAM,EACJ,4FAA4F;IAC9F,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
+ {"version":3,"file":"Button.js","sourceRoot":"","sources":["../../src/primitives/Button.tsx"],"names":[],"mappings":"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"}
@@ -2,6 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button } from '../primitives/Button.js';
3
3
  import { cx } from '../lib/cx.js';
4
4
  export function ErrorState({ message, retryLabel, onRetry, className }) {
5
- return (_jsxs("div", { className: cx('py-x-slot-state-pad-y', className), role: "alert", children: [_jsx("p", { className: "font-sans text-x-slot-error-state-fg", children: message }), _jsx("div", { className: "py-x-slot-state-action-pad-y", children: _jsx(Button, { variant: "secondary", onClick: onRetry, children: retryLabel }) })] }));
5
+ return (_jsxs("div", { className: cx('py-x-slot-state-pad-y', className), role: "alert", children: [_jsx("p", { className: "font-sans text-x-slot-error-state-fg", children: message }), _jsx("div", { className: "py-x-slot-state-action-pad-y", children: _jsx(Button, { variant: "outline", tone: "neutral", onClick: onRetry, children: retryLabel }) })] }));
6
6
  }
7
7
  //# sourceMappingURL=ErrorState.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorState.js","sourceRoot":"","sources":["../../src/states/ErrorState.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAYlC,MAAM,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAmB;IACrF,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,EAAE,IAAI,EAAC,OAAO,aAClE,YAAG,SAAS,EAAC,sCAAsC,YAAE,OAAO,GAAK,EACjE,cAAK,SAAS,EAAC,8BAA8B,YAC3C,KAAC,MAAM,IAAC,OAAO,EAAC,WAAW,EAAC,OAAO,EAAE,OAAO,YACzC,UAAU,GACJ,GACL,IACF,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"ErrorState.js","sourceRoot":"","sources":["../../src/states/ErrorState.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAYlC,MAAM,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAmB;IACrF,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,SAAS,CAAC,EAAE,IAAI,EAAC,OAAO,aAClE,YAAG,SAAS,EAAC,sCAAsC,YAAE,OAAO,GAAK,EACjE,cAAK,SAAS,EAAC,8BAA8B,YAC3C,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,SAAS,EAAC,OAAO,EAAE,OAAO,YACtD,UAAU,GACJ,GACL,IACF,CACP,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hideyukimori/nene2-ui",
3
- "version": "0.19.1",
3
+ "version": "0.21.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",
@@ -42,6 +42,11 @@
42
42
  --color-warn-soft: oklch(95% 0.035 80);
43
43
  --color-on-warn: oklch(21% 0.04 75);
44
44
  --color-danger-soft: oklch(95% 0.03 30);
45
+ /* ๐Ÿ”ด ๅฅ‘็ด„ใ‚ญใƒผใ ใŒๅ€คใŒ็„กใ‹ใฃใŸ2ใค๏ผˆ#487 ใงๅฟ…่ฆใซใชใฃใŸใฎใงไธŽใˆใ‚‹๏ผ‰ใ€‚
46
+ * `on-danger` ใฏ 0.19.x ใŒ danger ใฎๅก—ใ‚Šใซไฝฟใฃใฆใ„ใŸ `on-accent` ใจๅŒๅ€ค๏ผๆ็”ปใฏไธๅค‰ใ€‚
47
+ * `accent-soft` ใฏไป–ใฎ *-soft๏ผˆ95% / chroma ~0.03 / ๅŒ hue๏ผ‰ใจๅŒใ˜ไฝœใ‚Šๆ–นใ€‚ */
48
+ --color-on-danger: oklch(99% 0 0);
49
+ --color-accent-soft: oklch(95% 0.03 250);
45
50
 
46
51
  /* ๐Ÿ”ด Six more contract colours, added in 0.17.0 for `Badge` (#422). Five ships carry a
47
52
  * success tone and a warning tone in their own badge (clear / field / invoice / origin /
@@ -170,6 +175,7 @@
170
175
  * nene-deal โ€” were already correcting that independently, so the painting this replaces
171
176
  * is one no consumer used. */
172
177
  --spacing-x-slot-badge-gap: var(--spacing-x-3xs);
178
+ --spacing-x-slot-badge-dot: var(--spacing-x-3xs);
173
179
  --spacing-x-slot-switch-pad-x: var(--spacing-x-2xs);
174
180
  --spacing-x-slot-switch-pad-y: var(--spacing-x-3xs);
175
181
  --spacing-x-slot-alert-pad: var(--spacing-x-2xs);
@@ -187,6 +193,7 @@
187
193
  /* Between an EmptyState's two lines (#456). Only read when `description` is passed. */
188
194
  --spacing-x-slot-empty-state-gap: var(--spacing-x-3xs);
189
195
  --spacing-x-slot-page-header-pad-y: var(--spacing-x-md);
196
+ --spacing-x-slot-page-header-gap: var(--spacing-x-3xs);
190
197
 
191
198
  --radius-x-slot-button: var(--radius-x-md);
192
199
  --radius-x-slot-control: var(--radius-x-md);
@@ -230,6 +237,11 @@
230
237
  --color-x-slot-alert-danger-bg: var(--color-surface);
231
238
  --color-x-slot-alert-danger-fg: var(--color-danger);
232
239
  --color-x-slot-alert-danger-border: var(--color-danger);
240
+ /* `success` (#486). Shaped like `danger`: the surface stays the page's, and the meaning is
241
+ * carried by the border and the text โ€” an alert is read, not filled. */
242
+ --color-x-slot-alert-success-bg: var(--color-surface);
243
+ --color-x-slot-alert-success-fg: var(--color-success);
244
+ --color-x-slot-alert-success-border: var(--color-success);
233
245
  --color-x-slot-field-label: var(--color-text-muted);
234
246
  --color-x-slot-field-hint: var(--color-text-muted);
235
247
  --text-x-slot-field-hint-size: var(--text-x-2xs);
@@ -283,22 +295,65 @@
283
295
  * there is now an opening. */
284
296
  --color-x-slot-focus-ring: var(--color-text-primary);
285
297
 
286
- --color-x-slot-button-primary-bg: var(--color-accent);
287
- --color-x-slot-button-primary-fg: var(--color-on-accent);
288
- --color-x-slot-button-secondary-bg: var(--color-surface-raised);
289
- --color-x-slot-button-secondary-fg: var(--color-text-primary);
290
- --color-x-slot-button-secondary-border: var(--color-border);
298
+ /* Button: shape ร— tone (#487).
299
+ *
300
+ * ๐Ÿ”ด The six tones are `Badge`'s six, in `Badge`'s order, read through the same shape of
301
+ * slot. That was the whole argument for splitting the axis โ€” if the words do not line up,
302
+ * the kit still has two vocabularies and has only moved where they disagree.
303
+ *
304
+ * ๐Ÿ”ด `-fg` reads the contract's own `on-<tone>`, not `on-accent` for everything. The
305
+ * contract has carried `on-warn` since it was frozen (2026-07-14) and it is a dark brown,
306
+ * because `--color-warn` is mid-luminance and white on it measures 3.48:1 โ€” below AA.
307
+ * With `on-warn` it is 4.97:1. The first draft of this PR painted every tone's label with
308
+ * `on-accent` and shipped that 3.48:1; the contract had already solved it.
309
+ *
310
+ * ๐Ÿ”ด The default tone is `accent`, so `<Button>` is still the accent-coloured filled
311
+ * button 0.19.x called `primary`. `neutral` is the quiet grey step.
312
+ *
313
+ * Four colours per tone, none derivable from the others:
314
+ * -bg the fill, painted by `solid` only
315
+ * -fg what sits on that fill (the contract's `on-<tone>`)
316
+ * -ink the tone itself, painted by `outline` / `bare` / `link`
317
+ * -border the outline's frame โ€” the *-soft step, not the tone
318
+ *
319
+ * ๐Ÿ”ด `-border` is the soft step. nene-clear draws its outlined danger with a frame far
320
+ * lighter than the label (`--bad-line` #e3bcbc against `--bad` #9c2c2c). Using the tone for
321
+ * both makes every outlined button louder than any ship draws today.
322
+ *
323
+ * ๐Ÿ”ด Renaming a slot fails SILENTLY. A ship that redefined `primary-* / secondary-* /
324
+ * danger-border / ghost-fg` sees no error โ€” the property stops being read and the kit
325
+ * default paints instead. Both consuming ships redefined some (nene-deal 4, nene-vault 3,
326
+ * measured 2026-08-27), so the migration note carries the mapping. */
327
+ --color-x-slot-button-outline-bg: var(--color-surface-raised);
328
+ --color-x-slot-button-neutral-bg: var(--color-border);
329
+ --color-x-slot-button-neutral-fg: var(--color-text-primary);
330
+ --color-x-slot-button-neutral-ink: var(--color-text-primary);
331
+ --color-x-slot-button-neutral-border: var(--color-border);
332
+ --color-x-slot-button-accent-bg: var(--color-accent);
333
+ --color-x-slot-button-accent-fg: var(--color-on-accent);
334
+ --color-x-slot-button-accent-ink: var(--color-accent);
335
+ --color-x-slot-button-accent-border: var(--color-accent-soft);
291
336
  --color-x-slot-button-danger-bg: var(--color-danger);
292
- --color-x-slot-button-danger-fg: var(--color-on-accent);
293
- /* ๐Ÿ”ด Defaults to the fill, so a filled danger button looks exactly as it did: the border
294
- * and the background are the same colour, which is what `border-transparent` over a filled
295
- * background already rendered. The slot exists for the ships whose danger button is an
296
- * outline rather than a fill โ€” nene-deal's is `background: transparent` with a tinted
297
- * border (#455), and it could not express that: the variant emitted no border class at
298
- * all, so `border-transparent` from BASE_CLASS won and no slot value could reach it. */
299
- --color-x-slot-button-danger-border: var(--color-danger);
300
- --color-x-slot-button-ghost-fg: var(--color-text-primary);
301
-
337
+ --color-x-slot-button-danger-fg: var(--color-on-danger);
338
+ --color-x-slot-button-danger-ink: var(--color-danger);
339
+ --color-x-slot-button-danger-border: var(--color-danger-soft);
340
+ --color-x-slot-button-success-bg: var(--color-success);
341
+ --color-x-slot-button-success-fg: var(--color-on-success);
342
+ --color-x-slot-button-success-ink: var(--color-success);
343
+ --color-x-slot-button-success-border: var(--color-success-soft);
344
+ --color-x-slot-button-warn-bg: var(--color-warn);
345
+ --color-x-slot-button-warn-fg: var(--color-on-warn);
346
+ /* ๐Ÿ”ด warn ใฎ ink ใ ใ‘ tone ๆœฌไฝ“่‰ฒใงใฏใชใ„ใ€‚`--color-warn` ใฏไธญ้–“่ผๅบฆใงใ€็™ฝๅœฐใซ็ฝฎใใจ
347
+ * 3.58:1 โ€”โ€”AA ใ‚’ๅ‰ฒใ‚‹๏ผˆไป–ใฎ tone ใฏๆœฌไฝ“่‰ฒใฎใพใพใง 4.7ใ€œ5.5 ้€šใ‚‹๏ผ‰ใ€‚ๅฅ‘็ด„ใฎ `--color-on-warn`
348
+ * ใฏ 16:1 ใงใ€**InlineAlert ใŒๆ—ขใซใ€Œ็™ฝๅœฐใซ่ผ‰ใ‚‹ warn ใฎๅญ—ใ€ใจใ—ใฆไฝฟใฃใฆใ„ใ‚‹**
349
+ * ๏ผˆ`--color-x-slot-alert-warn-fg`๏ผ‰ใ€‚ๅŒใ˜็”จ้€”ใซๅŒใ˜ใƒˆใƒผใ‚ฏใƒณใ‚’ๅ……ใฆใ‚‹ใฎใฏๆƒใˆใ‚‹ๆ–นๅ‘ใงใ‚ใฃใฆใ€
350
+ * ไพ‹ๅค–ใงใฏใชใ„ใ€‚โ‡’ ๅฅ‘็ด„ใฎๅ€คใฏ1ใคใ‚‚ๅค‰ใˆใšใซๆธˆใ‚€๏ผˆBadge ใ‚‚ InlineAlert ใ‚‚ๅ‹•ใ‹ใชใ„๏ผ‰ใ€‚ */
351
+ --color-x-slot-button-warn-ink: var(--color-on-warn);
352
+ --color-x-slot-button-warn-border: var(--color-warn-soft);
353
+ --color-x-slot-button-info-bg: var(--color-info);
354
+ --color-x-slot-button-info-fg: var(--color-on-info);
355
+ --color-x-slot-button-info-ink: var(--color-info);
356
+ --color-x-slot-button-info-border: var(--color-info-soft);
302
357
  /* Shared by Input, Select and Textarea โ€” they are one control with three shapes, and a
303
358
  * product that darkens one and not the others has a bug, not a design. */
304
359
  --color-x-slot-control-fg: var(--color-text-primary);
@@ -360,8 +415,10 @@
360
415
  --color-x-slot-toast-border: var(--color-border);
361
416
  --color-x-slot-toast-danger-fg: var(--color-danger);
362
417
  --color-x-slot-toast-danger-border: var(--color-danger);
363
- /* `success` tone (#457). Badge and InlineAlert already carry this meaning; the toast is
364
- * the one surface that had only two. */
418
+ /* `success` tone (#457). โš ๏ธ This comment claimed InlineAlert already carried the meaning.
419
+ * It did not, and would not until #486 โ€” the sentence was written from the enumeration in
420
+ * the issue rather than from the component. Corrected here rather than deleted: a note
421
+ * that was wrong about a neighbour is the exact shape #486 is about. */
365
422
  --color-x-slot-toast-success-fg: var(--color-success);
366
423
  --color-x-slot-toast-success-border: var(--color-success);
367
424
  /* The toast's second line (#457). Only read when `description` is passed. */
@@ -388,6 +445,7 @@
388
445
 
389
446
  --color-x-slot-pagination-fg: var(--color-text-muted);
390
447
  --color-x-slot-page-header-fg: var(--color-text-primary);
448
+ --color-x-slot-page-header-description-fg: var(--color-text-muted);
391
449
  --color-x-slot-text-fg: var(--color-text-primary);
392
450
  --color-x-slot-text-muted-fg: var(--color-text-muted);
393
451
  --color-x-slot-spinner-fg: var(--color-text-muted);
@@ -473,12 +531,19 @@
473
531
  --container-x-slot-modal-lg: var(--container-xl);
474
532
 
475
533
  --spacing-x-slot-modal-header-gap: var(--spacing-x-xs);
534
+ /* The action row (#493). One slot for both the space above it and the space between the
535
+ * buttons: they are the same rhythm, and a product that wants them apart can say so. */
536
+ --spacing-x-slot-modal-footer-gap: var(--spacing-x-xs);
537
+ /* The dialog's second line (#493). Only read when `description` is passed. Same value as
538
+ * PageHeader's and EmptyState's โ€” one word, one look. */
539
+ --spacing-x-slot-modal-description-gap: var(--spacing-x-3xs);
476
540
  --font-weight-x-slot-modal-title: var(--font-weight-medium);
477
541
  /* `inherit`, so the header title takes the dialog's own size unless a product says
478
542
  * otherwise. There is no previous rendering to copy โ€” the header is new โ€” and picking a
479
543
  * step here would decide type for every product that turns the header on. */
480
544
  --text-x-slot-modal-title-size: inherit;
481
545
  --color-x-slot-modal-close-fg: var(--color-text-muted);
546
+ --color-x-slot-modal-description-fg: var(--color-text-muted);
482
547
 
483
548
  /* โ”€โ”€ Shadow slots โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
484
549
  * ๐Ÿ”ด Shadow was the one dimension that never got slots (#386). 0.9.0 moved "colour, weight
@@ -492,7 +557,10 @@
492
557
  * `Card` and `Toast` wrote `shadow-sm`, so theirs point at the scale. A default that is
493
558
  * anything other than the current rendering changes how every product already looks โ€”
494
559
  * and "the default is harmless" only holds while the default is that copy. */
495
- --shadow-x-slot-button-primary: none;
560
+ /* ๐Ÿ”ด Renamed from `--shadow-x-slot-button-primary` (#487): the shadow belongs to the
561
+ * filled shape, not to one colour. nene-vault redefines this one (`var(--shadow-sm)`) and
562
+ * must rename it โ€” see the migration note. */
563
+ --shadow-x-slot-button-solid: none;
496
564
  --shadow-x-slot-card-raised: var(--shadow-sm);
497
565
  --shadow-x-slot-toast: var(--shadow-sm);
498
566