@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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/data/DataTable.d.ts +32 -0
- package/dist/data/DataTable.js +17 -0
- package/dist/data/DataTable.js.map +1 -0
- package/dist/data/DetailList.d.ts +14 -0
- package/dist/data/DetailList.js +9 -0
- package/dist/data/DetailList.js.map +1 -0
- package/dist/data/Pagination.d.ts +26 -0
- package/dist/data/Pagination.js +20 -0
- package/dist/data/Pagination.js.map +1 -0
- package/dist/feedback/Badge.d.ts +14 -0
- package/dist/feedback/Badge.js +18 -0
- package/dist/feedback/Badge.js.map +1 -0
- package/dist/feedback/InlineAlert.d.ts +15 -0
- package/dist/feedback/InlineAlert.js +18 -0
- package/dist/feedback/InlineAlert.js.map +1 -0
- package/dist/feedback/ToastProvider.d.ts +31 -0
- package/dist/feedback/ToastProvider.js +56 -0
- package/dist/feedback/ToastProvider.js.map +1 -0
- package/dist/feedback/toast-context.d.ts +20 -0
- package/dist/feedback/toast-context.js +17 -0
- package/dist/feedback/toast-context.js.map +1 -0
- package/dist/forms/FormField.d.ts +47 -0
- package/dist/forms/FormField.js +25 -0
- package/dist/forms/FormField.js.map +1 -0
- package/dist/forms/field-context.d.ts +54 -0
- package/dist/forms/field-context.js +51 -0
- package/dist/forms/field-context.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/Box.d.ts +20 -0
- package/dist/layout/Box.js +17 -0
- package/dist/layout/Box.js.map +1 -0
- package/dist/layout/Card.d.ts +16 -0
- package/dist/layout/Card.js +13 -0
- package/dist/layout/Card.js.map +1 -0
- package/dist/layout/Grid.d.ts +15 -0
- package/dist/layout/Grid.js +7 -0
- package/dist/layout/Grid.js.map +1 -0
- package/dist/layout/PageHeader.d.ts +7 -0
- package/dist/layout/PageHeader.js +5 -0
- package/dist/layout/PageHeader.js.map +1 -0
- package/dist/layout/Section.d.ts +16 -0
- package/dist/layout/Section.js +14 -0
- package/dist/layout/Section.js.map +1 -0
- package/dist/layout/Stack.d.ts +20 -0
- package/dist/layout/Stack.js +24 -0
- package/dist/layout/Stack.js.map +1 -0
- package/dist/lib/cx.d.ts +12 -0
- package/dist/lib/cx.js +15 -0
- package/dist/lib/cx.js.map +1 -0
- package/dist/lib/source-probe.d.ts +28 -0
- package/dist/lib/source-probe.js +29 -0
- package/dist/lib/source-probe.js.map +1 -0
- package/dist/lib/spacing.d.ts +54 -0
- package/dist/lib/spacing.js +85 -0
- package/dist/lib/spacing.js.map +1 -0
- package/dist/lib/states.d.ts +44 -0
- package/dist/lib/states.js +45 -0
- package/dist/lib/states.js.map +1 -0
- package/dist/overlay/ConfirmDialog.d.ts +24 -0
- package/dist/overlay/ConfirmDialog.js +17 -0
- package/dist/overlay/ConfirmDialog.js.map +1 -0
- package/dist/overlay/Modal.d.ts +24 -0
- package/dist/overlay/Modal.js +47 -0
- package/dist/overlay/Modal.js.map +1 -0
- package/dist/primitives/Button.d.ts +6 -0
- package/dist/primitives/Button.js +13 -0
- package/dist/primitives/Button.js.map +1 -0
- package/dist/primitives/Checkbox.d.ts +14 -0
- package/dist/primitives/Checkbox.js +19 -0
- package/dist/primitives/Checkbox.js.map +1 -0
- package/dist/primitives/Icon.d.ts +38 -0
- package/dist/primitives/Icon.js +38 -0
- package/dist/primitives/Icon.js.map +1 -0
- package/dist/primitives/Input.d.ts +7 -0
- package/dist/primitives/Input.js +15 -0
- package/dist/primitives/Input.js.map +1 -0
- package/dist/primitives/Radio.d.ts +19 -0
- package/dist/primitives/Radio.js +20 -0
- package/dist/primitives/Radio.js.map +1 -0
- package/dist/primitives/Select.d.ts +9 -0
- package/dist/primitives/Select.js +15 -0
- package/dist/primitives/Select.js.map +1 -0
- package/dist/primitives/Spinner.d.ts +5 -0
- package/dist/primitives/Spinner.js +5 -0
- package/dist/primitives/Spinner.js.map +1 -0
- package/dist/primitives/Switch.d.ts +19 -0
- package/dist/primitives/Switch.js +19 -0
- package/dist/primitives/Switch.js.map +1 -0
- package/dist/primitives/Text.d.ts +7 -0
- package/dist/primitives/Text.js +6 -0
- package/dist/primitives/Text.js.map +1 -0
- package/dist/primitives/Textarea.d.ts +11 -0
- package/dist/primitives/Textarea.js +19 -0
- package/dist/primitives/Textarea.js.map +1 -0
- package/dist/states/EmptyState.d.ts +5 -0
- package/dist/states/EmptyState.js +5 -0
- package/dist/states/EmptyState.js.map +1 -0
- package/dist/states/ErrorState.d.ts +8 -0
- package/dist/states/ErrorState.js +6 -0
- package/dist/states/ErrorState.js.map +1 -0
- package/dist/states/LoadingState.d.ts +18 -0
- package/dist/states/LoadingState.js +19 -0
- package/dist/states/LoadingState.js.map +1 -0
- package/dist/theme/tokens.d.ts +19 -0
- package/dist/theme/tokens.js +20 -0
- package/dist/theme/tokens.js.map +1 -0
- package/package.json +41 -0
- 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":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC1F,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAoD,MAAM,oBAAoB,CAAC;AAyBpG,MAAM,UAAU,GAA8B;IAC5C,IAAI,EAAE,mDAAmD;IACzD,MAAM,EAAE,6CAA6C;CACtD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,WAAW,EACX,YAAY,EACZ,iBAAiB,GAAG,IAAI,EACxB,QAAQ,GACW;IACnB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAU,EAAE,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,GAAG,EAAyC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAEzB,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5B,CAAC;QACD,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACrE,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,IAAI,GAAG,WAAW,CACtB,CAAC,OAAe,EAAE,OAAsB,EAAE,EAAE;QAC1C,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC;QAC5C,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC;QACrF,MAAM,CAAC,OAAO,CAAC,GAAG,CAChB,EAAE,EACF,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,iBAAiB,CAAC,CACxE,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC,EACD,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAC7B,CAAC;IAEF,kFAAkF;IAClF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,OAAO,GAAG,EAAE;YACV,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YAC1D,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,GAAG,GAAG,OAAO,CAAW,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAG,CAAC,IAAe,EAAE,IAA4B,EAAE,EAAE,CAAC,CAChE,2BACa,IAAI,gBACH,WAAW,EACvB,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,yBAAyB,YAElC,MAAM;aACJ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;aACtC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACd,eAEE,SAAS,EAAE,EAAE,CACX,4EAA4E,EAC5E,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CACvB,aAED,yBAAO,KAAK,CAAC,OAAO,GAAQ,EAC5B,iBACE,IAAI,EAAC,QAAQ,gBACD,YAAY,EACxB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,EAChC,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,aAAa,CAAC,YAEpD,YAAY,GACN,KAdJ,KAAK,CAAC,EAAE,CAeT,CACP,CAAC,GACA,CACP,CAAC;IAEF,OAAO,CACL,MAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,GAAG,aAC9B,QAAQ,EACR,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EACxB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,IACR,CACzB,CAAC;AACJ,CAAC"}
|
|
@@ -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;
|