@hideyukimori/nene2-ui 0.2.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/data/DataTable.d.ts +32 -0
  4. package/dist/data/DataTable.js +17 -0
  5. package/dist/data/DataTable.js.map +1 -0
  6. package/dist/data/DetailList.d.ts +14 -0
  7. package/dist/data/DetailList.js +9 -0
  8. package/dist/data/DetailList.js.map +1 -0
  9. package/dist/data/Pagination.d.ts +26 -0
  10. package/dist/data/Pagination.js +20 -0
  11. package/dist/data/Pagination.js.map +1 -0
  12. package/dist/feedback/Badge.d.ts +14 -0
  13. package/dist/feedback/Badge.js +18 -0
  14. package/dist/feedback/Badge.js.map +1 -0
  15. package/dist/feedback/InlineAlert.d.ts +15 -0
  16. package/dist/feedback/InlineAlert.js +18 -0
  17. package/dist/feedback/InlineAlert.js.map +1 -0
  18. package/dist/feedback/ToastProvider.d.ts +31 -0
  19. package/dist/feedback/ToastProvider.js +56 -0
  20. package/dist/feedback/ToastProvider.js.map +1 -0
  21. package/dist/feedback/toast-context.d.ts +20 -0
  22. package/dist/feedback/toast-context.js +17 -0
  23. package/dist/feedback/toast-context.js.map +1 -0
  24. package/dist/forms/FormField.d.ts +47 -0
  25. package/dist/forms/FormField.js +25 -0
  26. package/dist/forms/FormField.js.map +1 -0
  27. package/dist/forms/field-context.d.ts +54 -0
  28. package/dist/forms/field-context.js +51 -0
  29. package/dist/forms/field-context.js.map +1 -0
  30. package/dist/index.d.ts +34 -0
  31. package/dist/index.js +47 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/layout/Box.d.ts +20 -0
  34. package/dist/layout/Box.js +17 -0
  35. package/dist/layout/Box.js.map +1 -0
  36. package/dist/layout/Card.d.ts +16 -0
  37. package/dist/layout/Card.js +13 -0
  38. package/dist/layout/Card.js.map +1 -0
  39. package/dist/layout/Grid.d.ts +15 -0
  40. package/dist/layout/Grid.js +7 -0
  41. package/dist/layout/Grid.js.map +1 -0
  42. package/dist/layout/PageHeader.d.ts +7 -0
  43. package/dist/layout/PageHeader.js +5 -0
  44. package/dist/layout/PageHeader.js.map +1 -0
  45. package/dist/layout/Section.d.ts +16 -0
  46. package/dist/layout/Section.js +14 -0
  47. package/dist/layout/Section.js.map +1 -0
  48. package/dist/layout/Stack.d.ts +20 -0
  49. package/dist/layout/Stack.js +24 -0
  50. package/dist/layout/Stack.js.map +1 -0
  51. package/dist/lib/cx.d.ts +12 -0
  52. package/dist/lib/cx.js +15 -0
  53. package/dist/lib/cx.js.map +1 -0
  54. package/dist/lib/source-probe.d.ts +28 -0
  55. package/dist/lib/source-probe.js +29 -0
  56. package/dist/lib/source-probe.js.map +1 -0
  57. package/dist/lib/spacing.d.ts +54 -0
  58. package/dist/lib/spacing.js +85 -0
  59. package/dist/lib/spacing.js.map +1 -0
  60. package/dist/lib/states.d.ts +44 -0
  61. package/dist/lib/states.js +45 -0
  62. package/dist/lib/states.js.map +1 -0
  63. package/dist/overlay/ConfirmDialog.d.ts +24 -0
  64. package/dist/overlay/ConfirmDialog.js +17 -0
  65. package/dist/overlay/ConfirmDialog.js.map +1 -0
  66. package/dist/overlay/Modal.d.ts +24 -0
  67. package/dist/overlay/Modal.js +47 -0
  68. package/dist/overlay/Modal.js.map +1 -0
  69. package/dist/primitives/Button.d.ts +6 -0
  70. package/dist/primitives/Button.js +13 -0
  71. package/dist/primitives/Button.js.map +1 -0
  72. package/dist/primitives/Checkbox.d.ts +14 -0
  73. package/dist/primitives/Checkbox.js +19 -0
  74. package/dist/primitives/Checkbox.js.map +1 -0
  75. package/dist/primitives/Icon.d.ts +38 -0
  76. package/dist/primitives/Icon.js +38 -0
  77. package/dist/primitives/Icon.js.map +1 -0
  78. package/dist/primitives/Input.d.ts +7 -0
  79. package/dist/primitives/Input.js +15 -0
  80. package/dist/primitives/Input.js.map +1 -0
  81. package/dist/primitives/Radio.d.ts +19 -0
  82. package/dist/primitives/Radio.js +20 -0
  83. package/dist/primitives/Radio.js.map +1 -0
  84. package/dist/primitives/Select.d.ts +9 -0
  85. package/dist/primitives/Select.js +15 -0
  86. package/dist/primitives/Select.js.map +1 -0
  87. package/dist/primitives/Spinner.d.ts +5 -0
  88. package/dist/primitives/Spinner.js +5 -0
  89. package/dist/primitives/Spinner.js.map +1 -0
  90. package/dist/primitives/Switch.d.ts +19 -0
  91. package/dist/primitives/Switch.js +19 -0
  92. package/dist/primitives/Switch.js.map +1 -0
  93. package/dist/primitives/Text.d.ts +7 -0
  94. package/dist/primitives/Text.js +6 -0
  95. package/dist/primitives/Text.js.map +1 -0
  96. package/dist/primitives/Textarea.d.ts +11 -0
  97. package/dist/primitives/Textarea.js +19 -0
  98. package/dist/primitives/Textarea.js.map +1 -0
  99. package/dist/states/EmptyState.d.ts +5 -0
  100. package/dist/states/EmptyState.js +5 -0
  101. package/dist/states/EmptyState.js.map +1 -0
  102. package/dist/states/ErrorState.d.ts +8 -0
  103. package/dist/states/ErrorState.js +6 -0
  104. package/dist/states/ErrorState.js.map +1 -0
  105. package/dist/states/LoadingState.d.ts +18 -0
  106. package/dist/states/LoadingState.js +19 -0
  107. package/dist/states/LoadingState.js.map +1 -0
  108. package/dist/theme/tokens.d.ts +19 -0
  109. package/dist/theme/tokens.js +20 -0
  110. package/dist/theme/tokens.js.map +1 -0
  111. package/package.json +41 -0
  112. package/themes/default.css +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HideyukiMORI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @hideyukimori/nene2-ui
2
+
3
+ Shared React UI kit for the NeNe fleet. **Components carry no design of their own — themes do.**
4
+
5
+ ## Why this exists
6
+
7
+ The fleet already had design **tokens** (`@hideyukimori/nene2-tokens`) and a **conformance
8
+ checker** (`@hideyukimori/nene2-standards`) — but no box to ship the actual parts in. So every
9
+ product wrote its own, and consistency was chased afterwards by counting violations.
10
+
11
+ Measured across the 13 product frontends on 2026-08-23:
12
+
13
+ | Component | Independent implementations |
14
+ | ------------ | --------------------------: |
15
+ | `Button` | **20** |
16
+ | `Input` | 17 |
17
+ | `Select` | 16 |
18
+ | `EmptyState` | 15 |
19
+ | `Text` | 14 |
20
+ | `Stack` | 12 |
21
+
22
+ Of the **266** files in the fleet's `shared/ui` directories, **74% share a name with at least one
23
+ other product**. The designs had already converged — only the execution was scattered N ways.
24
+
25
+ 🔑 **Enforcing consistency by inspection scales with the number of deviations, which has no upper
26
+ bound. Providing the parts scales with the number of screens, which is finite.**
27
+
28
+ ## Design principles
29
+
30
+ 1. **Pass native props through.** Every primitive extends its element's HTML attributes, so there
31
+ is nothing new to learn and nothing that quietly breaks accessibility.
32
+ 2. **`className` composes, never replaces.** Caller classes are appended last via `cx`, so
33
+ Tailwind's later-wins cascade makes them an escape hatch rather than a footgun.
34
+ 3. **No colour, spacing or radius props.** `<Button color="#f00">` does not exist. If a variant
35
+ cannot express it, add a variant — for everyone. _This is the principle that matters:_ the
36
+ 13,021 style violations the fleet is currently remediating exist because values could be
37
+ written inline. Make them unwritable and there is nothing to inspect.
38
+ 4. **No strings.** The kit ships no user-visible text. `Spinner` takes `label`, `ErrorState` takes
39
+ `retryLabel`. Localisation stays with the product (`@hideyukimori/nene2-i18n`), so a wording
40
+ change never becomes a kit release.
41
+ 5. **The three states ship together.** `LoadingState` / `EmptyState` / `ErrorState` exist as a set
42
+ so a screen that handles only the happy path is visibly incomplete.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ npm i @hideyukimori/nene2-ui
48
+ ```
49
+
50
+ Peer dependencies: `react >= 19`, `tailwindcss >= 4`.
51
+
52
+ ## Use
53
+
54
+ ```css
55
+ /* src/shared/ui/theme/index.css */
56
+ @import 'tailwindcss';
57
+ @import '@hideyukimori/nene2-ui/themes/default.css';
58
+
59
+ /* 🔴 Required. Adjust the relative path to reach your node_modules. */
60
+ @source '../../../../node_modules/@hideyukimori/nene2-ui/dist';
61
+ ```
62
+
63
+ ### 🔴 The `@source` line is not optional
64
+
65
+ Tailwind v4 discovers classes by scanning your source files, and **it does not walk
66
+ `node_modules`**. Every class this kit ships lives in its `dist/`, so without that line
67
+ Tailwind never sees them and **generates none of them**.
68
+
69
+ Nothing goes red when this happens. Measured by nene-vault on 2026-08-23 in a real
70
+ application: the build passed with no warning, the types passed, **all 275 tests passed**,
71
+ and the stylesheet came out 47.1 KB instead of 58.6 KB — with every `gap-*`, `rounded-*`,
72
+ focus ring and disabled treatment missing. The tell was that `p-x-lg`, the _same token_
73
+ written in the app's own `.tsx`, was generated. The only difference is which directory the
74
+ file sits in.
75
+
76
+ A test suite cannot catch it either: jsdom does not compute styles. On screen the symptom is
77
+ simply "the kit does not seem to do anything".
78
+
79
+ ### Proving it, so nobody has to remember
80
+
81
+ The kit exports a class that exists nowhere except its own `dist`. If your build generates
82
+ it, the kit is in your `@source`; if not, every class the kit ships was dropped. Check it
83
+ where you already check other things:
84
+
85
+ ```js
86
+ import { SOURCE_PROBE_CLASS } from '@hideyukimori/nene2-ui';
87
+ import { readFileSync } from 'node:fs';
88
+
89
+ const css = readFileSync('dist/assets/index.css', 'utf8');
90
+ if (!css.includes(`.${SOURCE_PROBE_CLASS}`)) {
91
+ throw new Error('nene2-ui is not in Tailwind @source — none of its classes were generated');
92
+ }
93
+ ```
94
+
95
+ The probe resolves to `padding: 0px`, so it changes nothing if it is ever applied.
96
+
97
+ ```tsx
98
+ import { PageHeader, Button, FormField, Input, EmptyState } from '@hideyukimori/nene2-ui';
99
+
100
+ export function InvoiceListPage({ t, invoices }) {
101
+ return (
102
+ <>
103
+ <PageHeader title={t('invoices.title')} actions={<Button>{t('invoices.new')}</Button>} />
104
+ {invoices.length === 0 ? (
105
+ <EmptyState message={t('invoices.empty')} />
106
+ ) : (
107
+ <InvoiceTable rows={invoices} />
108
+ )}
109
+ </>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ## Theming
115
+
116
+ Components reference Tailwind utilities derived from `@theme` custom properties. A theme is one
117
+ CSS file of ~20 lines, and it is the **only** place a design value may appear.
118
+
119
+ ```
120
+ tokens contract ──themegen──▶ themes/<name>.css (@theme block)
121
+
122
+ product's active.css imports exactly one
123
+ ```
124
+
125
+ To rebrand, replace the theme file. **Never edit a component.** This is what makes adopting the
126
+ kit reversible: the look is 20 lines away from being something else.
127
+
128
+ Regenerate a theme from the token contract with:
129
+
130
+ ```bash
131
+ npx @hideyukimori/nene2-tokens themegen
132
+ ```
133
+
134
+ ## What is in scope
135
+
136
+ **In:** admin console and business-screen UI, page layout scaffolding, form field structure, the
137
+ loading/empty/error state set, read-only detail displays.
138
+
139
+ **Out:** end-user-facing themes that are a _product feature_ (NeNe Records ships 30 of them —
140
+ `aurora`, `newsprint`, `noir`, `japandi`, …), public marketing and brand surfaces, domain-specific
141
+ screens (invoice previews, kanban boards, reports), and anything in the `model/` layer — data
142
+ fetching and state are governed by `@hideyukimori/nene2-standards`, not by this kit.
143
+
144
+ ## Components
145
+
146
+ 🔴 **This table is generated from `src/index.ts`.** Regenerate it when you add a component —
147
+ a component list that lags the code is how a product ends up writing a part that already exists.
148
+
149
+ | Group | Components |
150
+ | ------------ | ---------------------------------------------------------------------------------------- |
151
+ | `primitives` | `Button` `Input` `Select` `Spinner` `Text` `Textarea` `Icon` `Checkbox` `Radio` `Switch` |
152
+ | `layout` | `PageHeader` `Stack` `Grid` `Box` `Section` `Card` |
153
+ | `forms` | `FormField` |
154
+ | `states` | `LoadingState` `EmptyState` `ErrorState` |
155
+ | `overlay` | `Modal` `ConfirmDialog` |
156
+ | `feedback` | `Badge` `InlineAlert` `ToastProvider` |
157
+ | `data` | `DetailList` `DataTable` `Pagination` |
158
+ | `theme` | `tokens` (read-only `var()` accessors for canvas/chart use) |
159
+
160
+ `Input`, `Select` and `Textarea` use `forwardRef`, so they drop straight into
161
+ `react-hook-form`'s `register`, and pick up their `id` / `aria-describedby` from the
162
+ `FormField` around them.
163
+
164
+ ### Not yet here
165
+
166
+ `BrandMark` — measured as recurring
167
+ across ≥3 products. They land as migrating products contribute their implementation upward,
168
+ rather than being designed up front.
169
+
170
+ Until one lands, a product writing its own control can import `CONTROL_CLASS` so that at least
171
+ the focus ring and the disabled treatment match the rest of the kit. **What each product imports
172
+ it for is the measured list of what the kit is still missing.**
173
+
174
+ ## Contributing a component
175
+
176
+ The kit grows by **promotion, not invention**. If a screen needs something the kit lacks:
177
+
178
+ 1. Build it in that product's `shared/ui` first, against tokens only.
179
+ 2. When a second product needs the same thing, promote it here — the second use is the evidence.
180
+ 3. Open an issue if a variant is missing. **Do not special-case it in the screen**; one exception
181
+ granted is the moment the kit stops meaning anything.
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ npm ci
187
+ npm run check # type-check + format:check + test
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT © hideyuki MORI
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface DataColumn<Row> {
3
+ /** Stable key, also used as the React key. */
4
+ key: string;
5
+ /** Localized column heading. */
6
+ header: string;
7
+ /** Cell renderer. Kept explicit so a column can render a Badge, a link, anything. */
8
+ cell: (row: Row) => ReactNode;
9
+ /** Numeric columns read better right-aligned. Alignment, not styling. */
10
+ align?: 'start' | 'end';
11
+ }
12
+ export interface DataTableProps<Row> {
13
+ columns: DataColumn<Row>[];
14
+ rows: Row[];
15
+ /** Stable identity per row. Index-as-key breaks as soon as the list is sorted. */
16
+ rowKey: (row: Row) => string;
17
+ /** Localized description of what the table contains. Becomes the table's caption. */
18
+ caption: string;
19
+ }
20
+ /**
21
+ * A tabular list.
22
+ *
23
+ * 🔴 Every heading carries `scope="col"`. Without it a screen reader cannot pair a cell with
24
+ * its column, so each cell is read as a bare value — and nothing about the rendered table
25
+ * looks any different, which is why three ships each shipped a table and the `scope` is the
26
+ * detail most likely to differ between them.
27
+ *
28
+ * 🔴 The caption is required, not optional. A visually obvious table ("it's clearly the
29
+ * invoice list") is not obvious to someone arriving at it by keyboard from elsewhere on the
30
+ * page. It is visually hidden, not absent.
31
+ */
32
+ export declare function DataTable<Row>({ columns, rows, rowKey, caption }: DataTableProps<Row>): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A tabular list.
4
+ *
5
+ * 🔴 Every heading carries `scope="col"`. Without it a screen reader cannot pair a cell with
6
+ * its column, so each cell is read as a bare value — and nothing about the rendered table
7
+ * looks any different, which is why three ships each shipped a table and the `scope` is the
8
+ * detail most likely to differ between them.
9
+ *
10
+ * 🔴 The caption is required, not optional. A visually obvious table ("it's clearly the
11
+ * invoice list") is not obvious to someone arriving at it by keyboard from elsewhere on the
12
+ * page. It is visually hidden, not absent.
13
+ */
14
+ export function DataTable({ columns, rows, rowKey, caption }) {
15
+ return (_jsxs("table", { className: "w-full border-collapse font-sans text-text-primary", children: [_jsx("caption", { className: "sr-only", children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map((col) => (_jsx("th", { scope: "col", className: `border-b border-border px-x-2xs py-x-3xs font-medium text-text-muted ${col.align === 'end' ? 'text-right' : 'text-left'}`, children: col.header }, col.key))) }) }), _jsx("tbody", { children: rows.map((row) => (_jsx("tr", { children: columns.map((col) => (_jsx("td", { className: `border-b border-border px-x-2xs py-x-3xs ${col.align === 'end' ? 'text-right' : 'text-left'}`, children: col.cell(row) }, col.key))) }, rowKey(row)))) })] }));
16
+ }
17
+ //# sourceMappingURL=DataTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DataTable.js","sourceRoot":"","sources":["../../src/data/DataTable.tsx"],"names":[],"mappings":";AAsBA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAuB;IACpF,OAAO,CACL,iBAAO,SAAS,EAAC,oDAAoD,aACnE,kBAAS,SAAS,EAAC,SAAS,YAAE,OAAO,GAAW,EAChD,0BACE,uBACG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACpB,aAEE,KAAK,EAAC,KAAK,EACX,SAAS,EAAE,wEACT,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WACvC,EAAE,YAED,GAAG,CAAC,MAAM,IANN,GAAG,CAAC,GAAG,CAOT,CACN,CAAC,GACC,GACC,EACR,0BACG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,uBACG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACpB,aAEE,SAAS,EAAE,4CACT,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WACvC,EAAE,YAED,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IALT,GAAG,CAAC,GAAG,CAMT,CACN,CAAC,IAVK,MAAM,CAAC,GAAG,CAAC,CAWf,CACN,CAAC,GACI,IACF,CACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface DetailRow {
3
+ /** Localized row label. */
4
+ label: string;
5
+ value: ReactNode;
6
+ }
7
+ export interface DetailListProps {
8
+ rows: DetailRow[];
9
+ }
10
+ /**
11
+ * Read-only key/value display for detail screens. Uses a description list so the
12
+ * label/value relationship is conveyed to assistive technology.
13
+ */
14
+ export declare function DetailList({ rows }: DetailListProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Read-only key/value display for detail screens. Uses a description list so the
4
+ * label/value relationship is conveyed to assistive technology.
5
+ */
6
+ export function DetailList({ rows }) {
7
+ return (_jsx("dl", { className: "flex flex-col gap-x-stack-sm", children: rows.map((row) => (_jsxs("div", { className: "flex flex-col gap-x-inline-sm border-b border-border py-x-stack-sm", children: [_jsx("dt", { className: "font-sans font-medium text-text-muted", children: row.label }), _jsx("dd", { className: "font-sans text-text-primary", children: row.value })] }, row.label))) }));
8
+ }
9
+ //# sourceMappingURL=DetailList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DetailList.js","sourceRoot":"","sources":["../../src/data/DetailList.tsx"],"names":[],"mappings":";AAYA;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,IAAI,EAAmB;IAClD,OAAO,CACL,aAAI,SAAS,EAAC,8BAA8B,YACzC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,eAEE,SAAS,EAAC,oEAAoE,aAE9E,aAAI,SAAS,EAAC,uCAAuC,YAAE,GAAG,CAAC,KAAK,GAAM,EACtE,aAAI,SAAS,EAAC,6BAA6B,YAAE,GAAG,CAAC,KAAK,GAAM,KAJvD,GAAG,CAAC,KAAK,CAKV,CACP,CAAC,GACC,CACN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ export interface PaginationProps {
2
+ /** 1-based. */
3
+ page: number;
4
+ pageCount: number;
5
+ onPageChange: (page: number) => void;
6
+ /** Localized name for the navigation region, e.g. "Invoice pages". */
7
+ label: string;
8
+ previousLabel: string;
9
+ nextLabel: string;
10
+ /** Localized current position, e.g. "Page 2 of 9". The kit ships no strings. */
11
+ status: string;
12
+ }
13
+ /**
14
+ * Page-by-page navigation for a list.
15
+ *
16
+ * 🔴 The current position is text, and the region is a named `<nav>`. Four ships wrote this
17
+ * component and the recurring shape marks the current page by colour alone — which is
18
+ * invisible to a screen reader and to anyone who cannot distinguish the two shades. The
19
+ * `status` string is required for the same reason: "you are here" has to be readable, not
20
+ * merely visible.
21
+ *
22
+ * The ends are disabled rather than hidden. A control that disappears at the boundary makes
23
+ * the row of controls jump, and moves the next-page button under the cursor that just
24
+ * clicked it.
25
+ */
26
+ export declare function Pagination({ page, pageCount, onPageChange, label, previousLabel, nextLabel, status, }: PaginationProps): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from '../primitives/Button.js';
3
+ import { Stack } from '../layout/Stack.js';
4
+ /**
5
+ * Page-by-page navigation for a list.
6
+ *
7
+ * 🔴 The current position is text, and the region is a named `<nav>`. Four ships wrote this
8
+ * component and the recurring shape marks the current page by colour alone — which is
9
+ * invisible to a screen reader and to anyone who cannot distinguish the two shades. The
10
+ * `status` string is required for the same reason: "you are here" has to be readable, not
11
+ * merely visible.
12
+ *
13
+ * The ends are disabled rather than hidden. A control that disappears at the boundary makes
14
+ * the row of controls jump, and moves the next-page button under the cursor that just
15
+ * clicked it.
16
+ */
17
+ export function Pagination({ page, pageCount, onPageChange, label, previousLabel, nextLabel, status, }) {
18
+ return (_jsx("nav", { "aria-label": label, children: _jsxs(Stack, { direction: "horizontal", gap: "2xs", align: "center", children: [_jsx(Button, { variant: "secondary", disabled: page <= 1, onClick: () => onPageChange(page - 1), "aria-label": previousLabel, children: previousLabel }), _jsx("span", { "aria-current": "page", className: "font-sans text-text-muted", children: status }), _jsx(Button, { variant: "secondary", disabled: page >= pageCount, onClick: () => onPageChange(page + 1), "aria-label": nextLabel, children: nextLabel })] }) }));
19
+ }
20
+ //# sourceMappingURL=Pagination.js.map
@@ -0,0 +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;AAe3C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,KAAK,EACL,aAAa,EACb,SAAS,EACT,MAAM,GACU;IAChB,OAAO,CACL,4BAAiB,KAAK,YACpB,MAAC,KAAK,IAAC,SAAS,EAAC,YAAY,EAAC,GAAG,EAAC,KAAK,EAAC,KAAK,EAAC,QAAQ,aACpD,KAAC,MAAM,IACL,OAAO,EAAC,WAAW,EACnB,QAAQ,EAAE,IAAI,IAAI,CAAC,EACnB,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,CAAC,gBACzB,aAAa,YAExB,aAAa,GACP,EACT,+BAAmB,MAAM,EAAC,SAAS,EAAC,2BAA2B,YAC5D,MAAM,GACF,EACP,KAAC,MAAM,IACL,OAAO,EAAC,WAAW,EACnB,QAAQ,EAAE,IAAI,IAAI,SAAS,EAC3B,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,CAAC,gBACzB,SAAS,YAEpB,SAAS,GACH,IACH,GACJ,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface BadgeProps {
3
+ /** What the badge means, not what colour it is. */
4
+ tone?: 'neutral' | 'accent' | 'danger';
5
+ children: ReactNode;
6
+ }
7
+ /**
8
+ * A small status marker.
9
+ *
10
+ * 🔴 `tone` names a meaning, never a colour. `<Badge tone="danger">` survives a rebrand;
11
+ * `<Badge color="red">` becomes a lie the moment the theme changes, and there is no way to
12
+ * find every such lie afterwards except by reading every screen.
13
+ */
14
+ export declare function Badge({ tone, children }: BadgeProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../lib/cx.js';
3
+ const TONE_CLASS = {
4
+ neutral: 'bg-surface text-text-muted border-border',
5
+ accent: 'bg-accent text-on-accent border-accent',
6
+ danger: 'bg-danger text-on-accent border-danger',
7
+ };
8
+ /**
9
+ * A small status marker.
10
+ *
11
+ * 🔴 `tone` names a meaning, never a colour. `<Badge tone="danger">` survives a rebrand;
12
+ * `<Badge color="red">` becomes a lie the moment the theme changes, and there is no way to
13
+ * find every such lie afterwards except by reading every screen.
14
+ */
15
+ export function Badge({ tone = 'neutral', children }) {
16
+ return (_jsx("span", { className: cx('inline-flex items-center rounded-x-md border px-x-2xs py-x-3xs font-sans', TONE_CLASS[tone]), children: children }));
17
+ }
18
+ //# sourceMappingURL=Badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/feedback/Badge.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAQlC,MAAM,UAAU,GAAoD;IAClE,OAAO,EAAE,0CAA0C;IACnD,MAAM,EAAE,wCAAwC;IAChD,MAAM,EAAE,wCAAwC;CACjD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAc;IAC9D,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,UAAU,CAAC,IAAI,CAAC,CACjB,YAEA,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface InlineAlertProps {
3
+ /** What the message means. `danger` is announced assertively; `info` politely. */
4
+ tone?: 'info' | 'danger';
5
+ children: ReactNode;
6
+ }
7
+ /**
8
+ * A message attached to the thing it is about, rather than to the page.
9
+ *
10
+ * 🔴 The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
11
+ * which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
12
+ * Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
13
+ * of role is precisely the part that is easy to get wrong and invisible when you do.
14
+ */
15
+ export declare function InlineAlert({ tone, children }: InlineAlertProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../lib/cx.js';
3
+ const TONE_CLASS = {
4
+ info: 'bg-surface text-text-primary border-border',
5
+ danger: 'bg-surface text-danger border-danger',
6
+ };
7
+ /**
8
+ * A message attached to the thing it is about, rather than to the page.
9
+ *
10
+ * 🔴 The tone decides the ARIA role, not just the colour. `danger` becomes `role="alert"`,
11
+ * which interrupts a screen reader; `info` becomes `role="status"`, which waits its turn.
12
+ * Six ships wrote this component (three as `InlineAlert`, three as `Alert`) and the choice
13
+ * of role is precisely the part that is easy to get wrong and invisible when you do.
14
+ */
15
+ export function InlineAlert({ tone = 'info', children }) {
16
+ return (_jsx("div", { role: tone === 'danger' ? 'alert' : 'status', className: cx('rounded-x-md border p-x-2xs font-sans', TONE_CLASS[tone]), children: children }));
17
+ }
18
+ //# sourceMappingURL=InlineAlert.js.map
@@ -0,0 +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,4CAA4C;IAClD,MAAM,EAAE,sCAAsC;CAC/C,CAAC;AAEF;;;;;;;GAOG;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,uCAAuC,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,YAEvE,QAAQ,GACL,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface ToastProviderProps {
3
+ /** Localized name for the toast region, e.g. "Notifications". */
4
+ regionLabel: string;
5
+ /** Localized label for each toast's dismiss control. */
6
+ dismissLabel: string;
7
+ /**
8
+ * How long a toast stays, in milliseconds.
9
+ *
10
+ * 🔴 Five seconds, not the two-and-a-bit the fleet settled on independently (nene-field
11
+ * 2200ms, nene-deal 2600ms). A toast that vanishes before a screen reader has finished
12
+ * reading it was never delivered, and 2.2s is not enough for a sentence. Callers can
13
+ * shorten it per toast if they have a reason.
14
+ */
15
+ defaultDurationMs?: number;
16
+ children: ReactNode;
17
+ }
18
+ /**
19
+ * Hosts the toast queue and the live regions that announce it.
20
+ *
21
+ * 🔴 The live regions are always in the DOM, even with nothing to show. Four ships
22
+ * (nene-records, nene-field, nene-invoice, nene-deal) each create theirs at the moment the
23
+ * first toast appears — `nene-deal` even returns `null` when the queue is empty. A live
24
+ * region that arrives together with its content is frequently not announced at all: the
25
+ * assistive technology has nothing to have been watching. The toast is on screen, looks
26
+ * right, and is silent — which is why this survived four independent implementations.
27
+ *
28
+ * 🔴 Two regions, not one. `polite` waits for a pause; `danger` needs `assertive`, which
29
+ * interrupts. Both must pre-exist for the same reason, so both are rendered empty.
30
+ */
31
+ export declare function ToastProvider({ regionLabel, dismissLabel, defaultDurationMs, children, }: ToastProviderProps): import("react").JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { cx } from '../lib/cx.js';
4
+ import { CONTROL_CLASS } from '../lib/states.js';
5
+ import { ToastContext } from './toast-context.js';
6
+ const TONE_CLASS = {
7
+ info: 'bg-surface-raised text-text-primary border-border',
8
+ danger: 'bg-surface-raised text-danger border-danger',
9
+ };
10
+ /**
11
+ * Hosts the toast queue and the live regions that announce it.
12
+ *
13
+ * 🔴 The live regions are always in the DOM, even with nothing to show. Four ships
14
+ * (nene-records, nene-field, nene-invoice, nene-deal) each create theirs at the moment the
15
+ * first toast appears — `nene-deal` even returns `null` when the queue is empty. A live
16
+ * region that arrives together with its content is frequently not announced at all: the
17
+ * assistive technology has nothing to have been watching. The toast is on screen, looks
18
+ * right, and is silent — which is why this survived four independent implementations.
19
+ *
20
+ * 🔴 Two regions, not one. `polite` waits for a pause; `danger` needs `assertive`, which
21
+ * interrupts. Both must pre-exist for the same reason, so both are rendered empty.
22
+ */
23
+ export function ToastProvider({ regionLabel, dismissLabel, defaultDurationMs = 5000, children, }) {
24
+ const [toasts, setToasts] = useState([]);
25
+ const timers = useRef(new Map());
26
+ const nextId = useRef(0);
27
+ const dismiss = useCallback((id) => {
28
+ const timer = timers.current.get(id);
29
+ if (timer !== undefined) {
30
+ clearTimeout(timer);
31
+ timers.current.delete(id);
32
+ }
33
+ setToasts((current) => current.filter((toast) => toast.id !== id));
34
+ }, []);
35
+ const show = useCallback((message, options) => {
36
+ const id = `toast-${(nextId.current += 1)}`;
37
+ setToasts((current) => [...current, { id, message, tone: options?.tone ?? 'info' }]);
38
+ timers.current.set(id, setTimeout(() => dismiss(id), options?.durationMs ?? defaultDurationMs));
39
+ return id;
40
+ }, [defaultDurationMs, dismiss]);
41
+ // Timers outlive the component otherwise, and fire setState on an unmounted tree.
42
+ useEffect(() => {
43
+ const pending = timers.current;
44
+ return () => {
45
+ for (const timer of pending.values())
46
+ clearTimeout(timer);
47
+ pending.clear();
48
+ };
49
+ }, []);
50
+ const api = useMemo(() => ({ show, dismiss }), [show, dismiss]);
51
+ const region = (tone, live) => (_jsx("div", { "aria-live": live, "aria-label": regionLabel, role: "region", className: "flex flex-col gap-x-2xs", children: toasts
52
+ .filter((toast) => toast.tone === tone)
53
+ .map((toast) => (_jsxs("div", { className: cx('flex items-start gap-x-2xs rounded-x-md border p-x-2xs font-sans shadow-sm', TONE_CLASS[toast.tone]), children: [_jsx("span", { children: toast.message }), _jsx("button", { type: "button", "aria-label": dismissLabel, onClick: () => dismiss(toast.id), className: cx('rounded-x-md px-x-3xs', CONTROL_CLASS), children: dismissLabel })] }, toast.id))) }));
54
+ return (_jsxs(ToastContext.Provider, { value: api, children: [children, region('info', 'polite'), region('danger', 'assertive')] }));
55
+ }
56
+ //# sourceMappingURL=ToastProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastProvider.js","sourceRoot":"","sources":["../../src/feedback/ToastProvider.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export type ToastTone = 'info' | 'danger';
2
+ export interface ToastOptions {
3
+ tone?: ToastTone;
4
+ /** Overrides the provider's default. */
5
+ durationMs?: number;
6
+ }
7
+ export interface ToastApi {
8
+ /** Shows a toast and returns its id, so a caller can dismiss it early. */
9
+ show: (message: string, options?: ToastOptions) => string;
10
+ dismiss: (id: string) => void;
11
+ }
12
+ export declare const ToastContext: import("react").Context<ToastApi | null>;
13
+ /**
14
+ * Read the toast API.
15
+ *
16
+ * Throws outside a provider rather than returning a no-op: a `show()` that silently does
17
+ * nothing is a bug that surfaces only when somebody notices a confirmation that never
18
+ * appeared, which may be never.
19
+ */
20
+ export declare function useToast(): ToastApi;
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ToastContext = createContext(null);
3
+ /**
4
+ * Read the toast API.
5
+ *
6
+ * Throws outside a provider rather than returning a no-op: a `show()` that silently does
7
+ * nothing is a bug that surfaces only when somebody notices a confirmation that never
8
+ * appeared, which may be never.
9
+ */
10
+ export function useToast() {
11
+ const api = useContext(ToastContext);
12
+ if (api === null) {
13
+ throw new Error('useToast must be used inside a <ToastProvider>');
14
+ }
15
+ return api;
16
+ }
17
+ //# sourceMappingURL=toast-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast-context.js","sourceRoot":"","sources":["../../src/feedback/toast-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAgBlD,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAAkB,IAAI,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface FormFieldProps {
3
+ /** id of the control this field labels; the control picks it up automatically. */
4
+ id: string;
5
+ /** Localized label. */
6
+ label: string;
7
+ /** Localized error message, or null when valid. */
8
+ error?: string | null;
9
+ /**
10
+ * Localized help text, shown under the control and read out with it.
11
+ *
12
+ * 🔴 Under the control, not beside the label — see `labelAdornment` for that. Three ships
13
+ * ship a prop called `hint` and they do not agree on where it goes: nene-vault and
14
+ * nene-profile put it under the control, nene-invoice puts it inside the `<label>`. One
15
+ * name, two placements, so picking either one silently moves the other's text.
16
+ */
17
+ hint?: ReactNode;
18
+ /** Small node rendered beside the label — a keyboard hint, a unit, a link. */
19
+ labelAdornment?: ReactNode;
20
+ /** Marks the control `aria-required`. Carries no visible text of its own. */
21
+ required?: boolean;
22
+ /**
23
+ * Visible required marker, rendered after the label when `required` is set.
24
+ *
25
+ * 🔴 Separate from `required` on purpose. `required` is meaning and needs no words;
26
+ * a marker is presentation and does need them, and the kit ships no strings (principle 4).
27
+ * Folding the two together would mean the kit picking "*" for every locale.
28
+ */
29
+ requiredMarker?: ReactNode;
30
+ children: ReactNode;
31
+ }
32
+ /**
33
+ * Labelled form field wrapper: associates a `<label>` with its control, and links the
34
+ * control to its help text and its validation error.
35
+ *
36
+ * 🔴 The control no longer has to opt in. v0.1 documented that the control was "responsible
37
+ * for setting `aria-describedby`", and the fleet did not do it — nene-vault sets
38
+ * `aria-invalid` on 3 fields and links the reason zero times (measured 2026-08-23; see
39
+ * field-context.ts for how that number differs from a raw grep). A field's own error message
40
+ * is the field's job, so `FormField` publishes the ids through context and the kit's controls
41
+ * read them. A control from outside the kit still works; it simply gets the label association
42
+ * it always had.
43
+ *
44
+ * 🔴 The hint stays visible when there is an error. nene-profile hides it in that case, and
45
+ * that is the moment the user most needs it; the error is read first instead.
46
+ */
47
+ export declare function FormField({ id, label, error, hint, labelAdornment, required, requiredMarker, children, }: FormFieldProps): import("react").JSX.Element;