@tristan2828/ui-foundation 3.8.0 → 3.9.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/conventions/AGENTS.md
CHANGED
|
@@ -132,7 +132,9 @@ net.
|
|
|
132
132
|
Follow `docs/foundation/add-an-entity.md` (in Claude Code, `/new-entity
|
|
133
133
|
<Name>` is a shortcut to it). It builds from the entity's plan,
|
|
134
134
|
`docs/entities/<entity>.md`; if there's no plan, work one out with the
|
|
135
|
-
developer first. Never guess the fields.
|
|
135
|
+
developer first. Never guess the fields. The files it copies are the
|
|
136
|
+
Widgets demo's until the app removes it; after that, the `### Reference
|
|
137
|
+
files` section below names the app's own.
|
|
136
138
|
|
|
137
139
|
## Upgrading the Foundation
|
|
138
140
|
```bash
|
|
@@ -12,6 +12,30 @@ Do not skip steps or reorder them. Replace `<Entity>` with the PascalCase
|
|
|
12
12
|
entity name (e.g. `Invoice`) and `<entity>` with its kebab-case form
|
|
13
13
|
(`invoice`) throughout.
|
|
14
14
|
|
|
15
|
+
## The reference files
|
|
16
|
+
|
|
17
|
+
The steps name the template's Widgets demo as the code to copy:
|
|
18
|
+
`src/routes/widgets/`, `src/api/gateway/widgets.ts` and
|
|
19
|
+
`widget-categories.ts`, their mocks and tests. An app deletes the demo
|
|
20
|
+
once its own entity works, and from then on those names point at nothing.
|
|
21
|
+
So the app's `AGENTS.md`, below the foundation block, has a
|
|
22
|
+
**`### Reference files`** section: for each pattern (a table, a form, a
|
|
23
|
+
reference field, a multi choice and the rest), the app's own file to copy.
|
|
24
|
+
|
|
25
|
+
- **That section exists:** it wins. Wherever a step below names a demo
|
|
26
|
+
file, copy the file the section names for that pattern instead. A
|
|
27
|
+
pattern it doesn't list, and the demo is gone: stop and ask which file
|
|
28
|
+
to copy. Never rebuild a pattern from this file's prose alone.
|
|
29
|
+
- **It doesn't, and the demo is still here:** copy the demo, as written.
|
|
30
|
+
- **When the app removes the demo:** write the section first, naming the
|
|
31
|
+
file that now holds each pattern the demo showed. The template's own
|
|
32
|
+
`AGENTS.md` has the section, filled in for the demo, as the shape.
|
|
33
|
+
|
|
34
|
+
The demo's referenced entity is `WidgetCategory` (`/widget-categories`,
|
|
35
|
+
the `widget_categories` table), named for its widget so it never collides
|
|
36
|
+
with an app's own entity. An app whose entity is a Category builds it
|
|
37
|
+
alongside the demo with no rename.
|
|
38
|
+
|
|
15
39
|
## Before anything: the entity plan
|
|
16
40
|
|
|
17
41
|
**Never guess what an entity is.** Every field, type, option list and rule
|
|
@@ -64,14 +88,14 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
64
88
|
a raw sentinel instead of a "not set" label, passes `verify` unnoticed.
|
|
65
89
|
One row costs nothing and step 8 asserts it. Extend `tests/mocks/conformance.test.ts` so the
|
|
66
90
|
new handlers are validated against `openapi.yaml`, the same way
|
|
67
|
-
`widgets`/`categories` already are.
|
|
91
|
+
`widgets`/`widget-categories` already are.
|
|
68
92
|
5. **Add a gateway module in `src/api/gateway/<entity>.ts`** until step 3's
|
|
69
93
|
tests pass, built on `safeFetch`/`toAppError` from
|
|
70
94
|
`@tristan2828/ui-foundation/gateway` (see `src/api/gateway/widgets.ts`).
|
|
71
95
|
Wire → `Page<T>` / `AppError` translation only; no hand-written types
|
|
72
96
|
(everything comes from `schema.d.ts`).
|
|
73
|
-
6. **Copy the
|
|
74
|
-
|
|
97
|
+
6. **Copy the reference files, one for one, not just the two screens**
|
|
98
|
+
(the Widgets demo's, or the ones the app's `### Reference files` names) — shaped by the plan: table columns and sortable columns from
|
|
75
99
|
its "List" column, toolbar filters from its "Filter" column, form fields
|
|
76
100
|
and labels from its field table, and field types by pattern (a
|
|
77
101
|
`reference` field copies Category's searchable combobox, a
|
|
@@ -85,12 +109,14 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
85
109
|
`<MultiChoice>` control from the foundation on the
|
|
86
110
|
form and as a toolbar filter via `useTableUrlState`'s multi filters,
|
|
87
111
|
badges in the table, and a `filters` array the gateway sends as a
|
|
88
|
-
repeated parameter
|
|
112
|
+
repeated parameter; each option shows its label, never its wire value
|
|
113
|
+
(`quick_win` reads "Quick win"): one `Record<value, label>`
|
|
114
|
+
(`WIDGET_TAG_LABELS`) feeds `MultiChoice`'s `getLabel` and the badges — and a `multi reference` copies Extra Categories:
|
|
89
115
|
the `<MultiReference>` control on the form and as a toolbar filter, an
|
|
90
116
|
array of ids on the wire, a repeated any-of list filter, and names from
|
|
91
117
|
a **lookup by id** on the referenced entity
|
|
92
|
-
(`GET /categories?ids=1&ids=3`, `
|
|
93
|
-
`
|
|
118
|
+
(`GET /widget-categories?ids=1&ids=3`, `getWidgetCategoriesByIds`,
|
|
119
|
+
`useWidgetCategoriesByIdsQuery`). Add that `ids` parameter to the referenced
|
|
94
120
|
entity's list endpoint if it lacks one. Never name a picked id from
|
|
95
121
|
search results alone: a saved pick the current search doesn't return
|
|
96
122
|
would show without a name. The table looks up every id on its page in
|
|
@@ -128,7 +154,7 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
128
154
|
once and is restored if the save fails, with a toast naming the row),
|
|
129
155
|
then refetches the lists. Its specs cover a flip that saves, the
|
|
130
156
|
keyboard, and a failed save putting the switch back.
|
|
131
|
-
- `src/routes/widgets/use-widgets.ts`, `use-categories.ts` →
|
|
157
|
+
- `src/routes/widgets/use-widgets.ts`, `use-widget-categories.ts` →
|
|
132
158
|
`src/routes/<entity>/use-<entity>.ts` (TanStack Query hooks over the
|
|
133
159
|
new gateway module)
|
|
134
160
|
- `src/routes/widgets/widget-schema.ts` →
|
|
@@ -28,7 +28,8 @@ One or two sentences: what one record is, and who uses the screens.
|
|
|
28
28
|
say its default and the filter's three labels, e.g. Any stock /
|
|
29
29
|
In stock / Out of stock), `single choice` (list the options, in display
|
|
30
30
|
order; see the note below if it is *optional*), `multi choice` (list
|
|
31
|
-
the options
|
|
31
|
+
the options, with a label for any whose wire value isn't fit to show,
|
|
32
|
+
e.g. `quick_win` (Quick win); any number can be picked —
|
|
32
33
|
chips on the form, badges in the table, an any-of filter), `reference`
|
|
33
34
|
(to another entity — name it; a searchable combobox), `multi reference`
|
|
34
35
|
(to any number of records of another entity — name it; chips with a
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
export declare function MultiChoice<T extends string>({ options, value, onValueChange, id, placeholder, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, className, }: {
|
|
2
|
+
export declare function MultiChoice<T extends string>({ options, value, onValueChange, getLabel, id, placeholder, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, className, }: {
|
|
3
3
|
/** Every allowed option, in display order. */
|
|
4
4
|
options: readonly T[];
|
|
5
5
|
value: T[];
|
|
6
6
|
onValueChange: (value: T[]) => void;
|
|
7
|
+
/**
|
|
8
|
+
* The text shown for an option: on its chip, in the dropdown, and what
|
|
9
|
+
* typing matches against. Defaults to the value itself.
|
|
10
|
+
*/
|
|
11
|
+
getLabel?: (value: T) => string;
|
|
7
12
|
/** Id for the text input, so a <FieldLabel htmlFor> labels it. */
|
|
8
13
|
id?: string;
|
|
9
14
|
placeholder?: string;
|
|
@@ -5,9 +5,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
// filter — so entities reuse it rather than re-assembling the combobox
|
|
6
6
|
// primitive each time. Built on shadcn's Combobox `multiple` mode, following
|
|
7
7
|
// its own ComboboxMultiple example.
|
|
8
|
+
//
|
|
9
|
+
// Values are the wire's enum strings; `getLabel` names them when those
|
|
10
|
+
// aren't fit to show (`quick_win` → "Quick win"). Chips, options and the
|
|
11
|
+
// typed-text match all use the label, and the value is what's emitted.
|
|
8
12
|
import * as React from 'react';
|
|
9
13
|
import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxValue, useComboboxAnchor, } from '../ui/combobox.js';
|
|
10
|
-
export function MultiChoice({ options, value, onValueChange, id, placeholder, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, className, }) {
|
|
14
|
+
export function MultiChoice({ options, value, onValueChange, getLabel = String, id, placeholder, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, className, }) {
|
|
11
15
|
const anchor = useComboboxAnchor();
|
|
12
|
-
return (_jsxs(Combobox, { multiple: true, autoHighlight: true, items: options, value: value, onValueChange: (next) => onValueChange(next), children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: option }, option))), _jsx(ComboboxChipsInput, { id: id, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, placeholder: selected.length === 0 ? placeholder : undefined })] })) }) }), _jsxs(ComboboxContent, { anchor: anchor, children: [_jsx(ComboboxEmpty, { children: "No options found." }), _jsx(ComboboxList, { children: (option) => (_jsx(ComboboxItem, { value: option, children: option }, option)) })] })] }));
|
|
16
|
+
return (_jsxs(Combobox, { multiple: true, autoHighlight: true, items: options, value: value, onValueChange: (next) => onValueChange(next), itemToStringLabel: getLabel, children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: getLabel(option) }, option))), _jsx(ComboboxChipsInput, { id: id, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, placeholder: selected.length === 0 ? placeholder : undefined })] })) }) }), _jsxs(ComboboxContent, { anchor: anchor, children: [_jsx(ComboboxEmpty, { children: "No options found." }), _jsx(ComboboxList, { children: (option) => (_jsx(ComboboxItem, { value: option, children: getLabel(option) }, option)) })] })] }));
|
|
13
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|