@tristan2828/ui-foundation 4.55.0 → 4.57.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/docs/add-an-entity.md +2 -2
- package/conventions/docs/cell-patterns.md +14 -14
- package/conventions/docs/column-options.md +3 -4
- package/conventions/docs/design-language.md +8 -8
- package/conventions/docs/reminders.md +25 -1
- package/conventions/docs/timed-screens.md +1 -1
- package/dist/components/app/data-environment-banner.d.ts +1 -1
- package/dist/components/app/data-environment-banner.js +1 -1
- package/eslint/index.js +1 -2
- package/package.json +5 -8
|
@@ -214,14 +214,14 @@ is a filled-in example while the Widgets demo is still here).
|
|
|
214
214
|
`editInPlace({ kind: 'choice', ... })` with `DateField` as the
|
|
215
215
|
control, `defaultOpen`, `modal={false}`, `onChange={(day) =>
|
|
216
216
|
props.commit(day)}` and `onOpenChange` cancelling on close, as a
|
|
217
|
-
Select is (
|
|
217
|
+
Select is (the fixtures' `app/DateField` EditInPlace). The backend column
|
|
218
218
|
is a SQL `date` (`datetime.date` in the model).
|
|
219
219
|
|
|
220
220
|
**An image on a record** (a photo of it) is the stored file's id on the
|
|
221
221
|
wire (`string`, nullable), and on the form the foundation's
|
|
222
222
|
`ImageField` in a `Controller` (`z.string().nullable()`): `value` and
|
|
223
223
|
`onChange` are the id, `thumbUrl` the record's saved photo,
|
|
224
|
-
`onBusyChange` holds the submit while a file uploads (
|
|
224
|
+
`onBusyChange` holds the submit while a file uploads (the fixtures'
|
|
225
225
|
`app/ImageField`). Its `upload` is the app's own, one function in the
|
|
226
226
|
gateway that sends the file to the app's endpoint and resolves to
|
|
227
227
|
`{ id, thumbUrl, url }` (the `ImageUploadResult`), rejecting with the
|
|
@@ -52,7 +52,7 @@ view render them, and their specs pin them. Patterns 19 (stage circle),
|
|
|
52
52
|
20 (count linking to the related records) and 22 (a highlighted row),
|
|
53
53
|
the multi-value variant of 4, the step bar and star in 9, the dependency
|
|
54
54
|
list in 10, the ring in 13 and the pressed icon in 15 were each built
|
|
55
|
-
for a real app's screen first; the package's
|
|
55
|
+
for a real app's screen first; the package's fixtures renders
|
|
56
56
|
them (`app/StageCircle`, `app/CountLink`, `patterns/CellPatterns`,
|
|
57
57
|
`patterns/Scales`, `app/DataTable`), and
|
|
58
58
|
its checks pin their accessibility and contrast in both themes. The
|
|
@@ -151,7 +151,7 @@ narrow.
|
|
|
151
151
|
read-only cell isn't a button: `render={<span role="img" aria-label … tabIndex={0} />}`
|
|
152
152
|
keeps it focusable and named without faking one.
|
|
153
153
|
- Needs the `TooltipProvider` that `AppShell` mounts — free in any
|
|
154
|
-
cell, absent in an isolated
|
|
154
|
+
cell, absent in an isolated fixture.
|
|
155
155
|
- Encode the value in the glyph's **shape** (solid / half / slashed /
|
|
156
156
|
dashed), not its colour.
|
|
157
157
|
- **Give the trigger a focus ring**
|
|
@@ -162,7 +162,7 @@ narrow.
|
|
|
162
162
|
|
|
163
163
|
A multi value (several categories on one record) where each value has
|
|
164
164
|
its own icon and colour slot, and the column is narrow. The package's
|
|
165
|
-
|
|
165
|
+
fixtures render it (`patterns/CellPatterns`, "Icons with one tooltip").
|
|
166
166
|
|
|
167
167
|
```tsx
|
|
168
168
|
// values: the row's categories, `undefined` for one still loading.
|
|
@@ -323,7 +323,7 @@ own state.
|
|
|
323
323
|
|
|
324
324
|
### Two variants the package draws: a step bar and a star
|
|
325
325
|
|
|
326
|
-
Both are in the package's
|
|
326
|
+
Both are in the package's fixtures (`patterns/Scales`), every value on
|
|
327
327
|
the page and on a card, measured by axe and the glyph check in both
|
|
328
328
|
themes. Each is one image to a screen reader, named in words.
|
|
329
329
|
|
|
@@ -375,7 +375,7 @@ A one-to-many value (`★ 4.7 (3)` opening a per-person breakdown).
|
|
|
375
375
|
|
|
376
376
|
The records a row waits on (parts it needs, a date that has to pass,
|
|
377
377
|
other records that have to finish), summarised in the cell, listed in
|
|
378
|
-
the popover. The package's
|
|
378
|
+
the popover. The package's fixtures render it
|
|
379
379
|
(`patterns/CellPatterns`, "Dependency list").
|
|
380
380
|
|
|
381
381
|
- **The cell: a computed summary.** "Ready" when nothing holds the row,
|
|
@@ -567,7 +567,7 @@ now", "happened"). Widget's In Stock column is the reference
|
|
|
567
567
|
A yes/no that belongs to another value rather than having a column of
|
|
568
568
|
its own: a "focus" flag that only exists while the status is "doing", a
|
|
569
569
|
"pinned" mark on an open item. It's a small icon button inside the owning
|
|
570
|
-
value's cell, beside its word. The package's
|
|
570
|
+
value's cell, beside its word. The package's fixtures render it
|
|
571
571
|
(`patterns/CellPatterns`, "Pressed icon in a cell").
|
|
572
572
|
|
|
573
573
|
**Pick it over the Switch** when the yes/no only means something while
|
|
@@ -621,7 +621,7 @@ export function FocusToggle({ item }: { item: Item }) {
|
|
|
621
621
|
mode (3.2:1; dark is 5.2:1). `/70` measures 2.7:1, and a control a
|
|
622
622
|
reader can't see fails them. Hover lifts it to full
|
|
623
623
|
`muted-foreground`. axe doesn't measure icons, so the package's
|
|
624
|
-
|
|
624
|
+
fixtures glyph check does.
|
|
625
625
|
- **Tone: `destructive-text`, read as "flagged", not "error".** A flag
|
|
626
626
|
claims attention ahead of the rest of the row, and the destructive
|
|
627
627
|
tone is the one with that pull. The other tones are taken where it
|
|
@@ -1201,8 +1201,8 @@ const CATEGORY_COLUMNS: LegacyColumnDef<Category, unknown>[] = [
|
|
|
1201
1201
|
create={(name) => createCategory.mutateAsync({ name, colour: 'blue', icon: 'house' })} />} … />
|
|
1202
1202
|
```
|
|
1203
1203
|
|
|
1204
|
-
The worked example is the package's
|
|
1205
|
-
"Edit in the rows" (`data-table.
|
|
1204
|
+
The worked example is the package's fixture `app/DataTable`
|
|
1205
|
+
"Edit in the rows" (`data-table.fixtures.tsx`); the template has no small
|
|
1206
1206
|
entity yet.
|
|
1207
1207
|
|
|
1208
1208
|
### A table of a few short columns: `width="content"`
|
|
@@ -1223,7 +1223,7 @@ A status that's an ordered lifecycle (each record moves through the same
|
|
|
1223
1223
|
stages, in the same order, toward one that means *done*), with one or
|
|
1224
1224
|
more exit states off the side. A small circle beside the word fills as
|
|
1225
1225
|
the record moves on. The package ships it: `StageCircle`, rendered with
|
|
1226
|
-
every stage and tone in its
|
|
1226
|
+
every stage and tone in its fixtures (`app/StageCircle`).
|
|
1227
1227
|
|
|
1228
1228
|
| Stage (`stage`) | Drawn as | Say it for |
|
|
1229
1229
|
|---|---|---|
|
|
@@ -1290,7 +1290,7 @@ cell: ({ getValue }) => {
|
|
|
1290
1290
|
A count of the records in another table that point at this row (open
|
|
1291
1291
|
Tasks per Project, items per Tag), which opens that table already filtered
|
|
1292
1292
|
to them. The count is a `computed` integer (the entity plan's type).
|
|
1293
|
-
The package ships it: `CountLink`, rendered in its
|
|
1293
|
+
The package ships it: `CountLink`, rendered in its fixtures
|
|
1294
1294
|
(`app/CountLink`).
|
|
1295
1295
|
|
|
1296
1296
|
```tsx
|
|
@@ -1359,7 +1359,7 @@ months), each heading with a count and a summary ("4 · 1 h 12 m"). The
|
|
|
1359
1359
|
same screens usually want one quick filter as pills above the rows (All /
|
|
1360
1360
|
Video / Audio / Text, one pressed at a time) rather than a dropdown: there
|
|
1361
1361
|
are few values, and switching between them is the main thing people do.
|
|
1362
|
-
Raised for an app's feed (issue #135); the package's
|
|
1362
|
+
Raised for an app's feed (issue #135); the package's fixtures renders it
|
|
1363
1363
|
(`app/DataTable` "Grouped" and "Grouped no matches"), with axe in both
|
|
1364
1364
|
themes.
|
|
1365
1365
|
|
|
@@ -1446,7 +1446,7 @@ current one. `DataTable`'s `highlightRow` returns true for it.
|
|
|
1446
1446
|
`opacity-50` (or `text-foreground/60`) drops their text under 4.5:1 the
|
|
1447
1447
|
same way. Render their text in `text-muted-foreground`, which is
|
|
1448
1448
|
measured, and keep the word that says done.
|
|
1449
|
-
-
|
|
1449
|
+
- The fixtures' `app/DataTable` Highlighted story has two highlighted rows
|
|
1450
1450
|
(one striped), pinned columns and muted text inside them, under axe and
|
|
1451
1451
|
the token check in both themes; `e2e/data-table.spec.ts` checks every
|
|
1452
1452
|
cell wears the wash.
|
|
@@ -1459,7 +1459,7 @@ columns: **grouped by one of its choice fields**, picked by the reader
|
|
|
1459
1459
|
house), and **one record at a time without leaving the list**, its notes
|
|
1460
1460
|
and key facts in a panel beside the rows. `DataTable`'s `groupings` and
|
|
1461
1461
|
`detailPanel` (4.46) do both. Raised for an app's task list; the
|
|
1462
|
-
package's
|
|
1462
|
+
package's fixtures render it (`app/DataTable grouping`), with axe in
|
|
1463
1463
|
both themes and its specs in a real browser.
|
|
1464
1464
|
|
|
1465
1465
|
```tsx
|
|
@@ -108,10 +108,9 @@ copied tokens, and every one drifted:
|
|
|
108
108
|
An option using a fill the design system can't produce is being judged as
|
|
109
109
|
something the app can't render.
|
|
110
110
|
|
|
111
|
-
**Render the options from the app's real components
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
rather than hand-writing a standalone HTML page.
|
|
111
|
+
**Render the options from the app's real components** where the pattern
|
|
112
|
+
needs the real tokens and the real `Badge`, so the winner can be copied
|
|
113
|
+
straight out, rather than hand-writing a standalone HTML page.
|
|
115
114
|
|
|
116
115
|
## After the pick
|
|
117
116
|
|
|
@@ -57,7 +57,7 @@ shade in both themes, because it always carries white text and the page
|
|
|
57
57
|
behind it never enters the calculation.
|
|
58
58
|
|
|
59
59
|
Measured ratios (WCAG 2.x, sRGB) are recorded next to each primitive in
|
|
60
|
-
the package's `styles/theme.css`, and its `e2e/
|
|
60
|
+
the package's `styles/theme.css`, and its `e2e/fixtures-visual.spec.ts` re-checks all
|
|
61
61
|
twelve tone/style combinations with axe against the rendered DOM in both
|
|
62
62
|
themes. **A new tone or style is not done until it appears in that story.**
|
|
63
63
|
|
|
@@ -116,7 +116,7 @@ Two rules that keep this honest:
|
|
|
116
116
|
An icon that carries meaning on its own (a stage circle, a category's
|
|
117
117
|
icon, a pressed flag) must clear WCAG's **3:1 non-text minimum** against
|
|
118
118
|
the background behind it, in both themes. axe measures text only, so the
|
|
119
|
-
package's
|
|
119
|
+
package's fixtures have a glyph check of its own: every `<svg>` in its
|
|
120
120
|
design-language stories, its ink (with any opacity modifier painted over
|
|
121
121
|
its background) against what's behind it. Every `*-text` tone, every
|
|
122
122
|
category slot and `muted-foreground` clear it as they are. An opacity
|
|
@@ -235,7 +235,7 @@ Five things about them:
|
|
|
235
235
|
- **The glyph check re-measures them.** All twelve clear the 3:1
|
|
236
236
|
non-text minimum in both themes, on the page and on a card (recorded in
|
|
237
237
|
`theme.css`). axe checks text contrast, not icon contrast, so the
|
|
238
|
-
package's
|
|
238
|
+
package's fixtures glyph check draws an icon in each slot on both
|
|
239
239
|
surfaces (`patterns/CategoricalColour`) and fails a change that takes
|
|
240
240
|
one below 3:1. Its `SideBySide` story shows both themes at once, under
|
|
241
241
|
normal vision and simulated deuteranopia and protanopia.
|
|
@@ -389,8 +389,8 @@ set, not a `DataTable` prop:
|
|
|
389
389
|
doesn't change. The selectors are in `styles/theme.css`. A composite
|
|
390
390
|
reaches `compact` with the `density-compact:` variant (`styles/index.css`:
|
|
391
391
|
any ancestor with `data-density="compact"`), the way `WidgetCard` and
|
|
392
|
-
`ModuleCard` do; an app's own card can use it too.
|
|
393
|
-
|
|
392
|
+
`ModuleCard` do; an app's own card can use it too. the package's
|
|
393
|
+
fixtures page shows any fixture at a density, and `patterns/Density`
|
|
394
394
|
shows the primitives, `DataTable`, the sidebar and the hub cards at
|
|
395
395
|
each. Its checks
|
|
396
396
|
measure that every step changes what it should, and that the default
|
|
@@ -507,7 +507,7 @@ const layout = useWidgetLayout('my-app:hub', Object.keys(HUB_CARDS))
|
|
|
507
507
|
equal columns that wraps (the class above: one column on a phone, as
|
|
508
508
|
many 288px-or-wider columns as fit). Each card is as tall as its rows;
|
|
509
509
|
it doesn't stretch to its tallest neighbour.
|
|
510
|
-
-
|
|
510
|
+
- The fixtures' `app/WidgetCard` shows three widgets on a page and every
|
|
511
511
|
one in each state, a folded summary in each state and an accordion,
|
|
512
512
|
and `app/WidgetLayoutSheet` a hub with its Customize sheet; its checks run axe and the token and glyph checks in
|
|
513
513
|
both themes and at `large`.
|
|
@@ -577,7 +577,7 @@ const { data, isLoading, error, refetch } = useProjectsSummaryQuery()
|
|
|
577
577
|
own page. The row scrolls inside itself and the page never does. A
|
|
578
578
|
desktop shows the cards either way, so don't render your own row
|
|
579
579
|
beside a strip hidden on a phone.
|
|
580
|
-
-
|
|
580
|
+
- The fixtures' `app/ModuleCard` shows a hub with a strip and a widget,
|
|
581
581
|
every state, both at `compact`, and both again as a row of chips (Row,
|
|
582
582
|
Row states); its checks run axe and the token and glyph checks in both
|
|
583
583
|
themes and at `large`, measure the strip at a desktop and a phone
|
|
@@ -603,7 +603,7 @@ under its own header.
|
|
|
603
603
|
swaps the tone's own icon for a better one (a lock for read-only).
|
|
604
604
|
- **A wash, not a fill:** the tone at 10% (20% in dark) with `foreground`
|
|
605
605
|
text and the tone's `*-text` icon. The message reads as body text, so
|
|
606
|
-
nothing inside it is muted.
|
|
606
|
+
nothing inside it is muted. The fixtures' `app/Notice` measures every
|
|
607
607
|
tone with axe and the glyph check in both themes.
|
|
608
608
|
- **One or two sentences and at most one action**: what the state means
|
|
609
609
|
and what to do about it. More than that belongs in a section.
|
|
@@ -35,7 +35,7 @@ const push = usePushPermission()
|
|
|
35
35
|
- **Put it where reminders are set**: the top of the reminders list, or a
|
|
36
36
|
settings page. One per device: each phone or browser allows its own.
|
|
37
37
|
- **Every state says what to do next.** `AllowReminders` shows them all
|
|
38
|
-
(the package's
|
|
38
|
+
(the package's fixtures, `app/AllowReminders`):
|
|
39
39
|
|
|
40
40
|
| `status` | Means | Shows |
|
|
41
41
|
|---|---|---|
|
|
@@ -80,6 +80,30 @@ Scope it to the signed-in user, as Widgets are.
|
|
|
80
80
|
package's `reminders` only with its `Reminder` model (imported, or the
|
|
81
81
|
job run without a model). So an app without push, or with its own
|
|
82
82
|
table, gets neither from `create_all` or autogenerate.
|
|
83
|
+
- **Channels beyond push** (email, a phone-alert app): hand the job a
|
|
84
|
+
sender per channel, and it still decides what's due and stamps it once.
|
|
85
|
+
A sender is `async def send(session, reminder)`: it returns `None` when
|
|
86
|
+
sent, or a `ChannelResult` ("sent", "failed" with its `error`, or
|
|
87
|
+
"skipped"), and an exception it raises is a failed result. `channels`
|
|
88
|
+
picks a reminder's channels (every one when left out; push is
|
|
89
|
+
`"push"`), and `on_sent` sees each reminder's results before its stamp
|
|
90
|
+
is committed, so a last-sent time per channel lands with it:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
reminders.register(
|
|
94
|
+
model=MyReminder,
|
|
95
|
+
senders={"email": send_email},
|
|
96
|
+
channels=lambda r: r.channels.split(","),
|
|
97
|
+
on_sent=stamp_channels, # async (session, reminder, results)
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
One failed channel doesn't stop the others; it's sent to error
|
|
102
|
+
reporting, and the reminder is done for the day either way (a channel
|
|
103
|
+
that's down isn't tried again every minute). A sender reads the session
|
|
104
|
+
but doesn't write it. `run_due_reminders` (same arguments) returns
|
|
105
|
+
each reminder's results, for a script or a test; a sender named
|
|
106
|
+
`"push"` replaces the package's.
|
|
83
107
|
- **Something else to send?** Call `send_push` from the app's own code or
|
|
84
108
|
job: a message when a long task finishes, a daily summary. The `url` is
|
|
85
109
|
always the app's own path.
|
|
@@ -30,7 +30,7 @@ package has the parts, so a screen like this is assembled, not rebuilt:
|
|
|
30
30
|
| `usePrefersReducedMotion` | Whether to swap movement for a fade |
|
|
31
31
|
|
|
32
32
|
All of them are imported from `@tristan2828/ui-foundation`. The package's
|
|
33
|
-
|
|
33
|
+
The package's fixtures have the whole thing working together: `app/SessionView`, the
|
|
34
34
|
Timer story, and the same run on the dark stage, DarkStage.
|
|
35
35
|
|
|
36
36
|
## The view
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare function DataEnvironmentBanner(): import("react").JSX.Element;
|
|
2
|
-
/** The banner itself, for a label already known (
|
|
2
|
+
/** The banner itself, for a label already known (fixtures, tests). Renders nothing for null. */
|
|
3
3
|
export declare function DataEnvironmentNotice({ label }: {
|
|
4
4
|
label: string | null;
|
|
5
5
|
}): import("react").JSX.Element | null;
|
|
@@ -16,7 +16,7 @@ export function DataEnvironmentBanner() {
|
|
|
16
16
|
const label = useDataLabel();
|
|
17
17
|
return _jsx(DataEnvironmentNotice, { label: label });
|
|
18
18
|
}
|
|
19
|
-
/** The banner itself, for a label already known (
|
|
19
|
+
/** The banner itself, for a label already known (fixtures, tests). Renders nothing for null. */
|
|
20
20
|
export function DataEnvironmentNotice({ label }) {
|
|
21
21
|
if (label === null)
|
|
22
22
|
return null;
|
package/eslint/index.js
CHANGED
|
@@ -29,7 +29,7 @@ function shippedPrimitives() {
|
|
|
29
29
|
for (const dir of ['../dist/components/ui', '../src/components/ui']) {
|
|
30
30
|
try {
|
|
31
31
|
return readdirSync(new URL(dir, import.meta.url))
|
|
32
|
-
.filter((file) => /^[a-z0-9-]+\.(js|tsx)$/.test(file) && !file.includes('.
|
|
32
|
+
.filter((file) => /^[a-z0-9-]+\.(js|tsx)$/.test(file) && !file.includes('.fixtures.'))
|
|
33
33
|
.map((file) => file.replace(/\.(js|tsx)$/, ''))
|
|
34
34
|
} catch {
|
|
35
35
|
// Not there — try the next location.
|
|
@@ -130,7 +130,6 @@ export default function uiFoundation(options = {}) {
|
|
|
130
130
|
// session's in-progress files fail another session's verify.
|
|
131
131
|
globalIgnores([
|
|
132
132
|
'dist',
|
|
133
|
-
'storybook-static',
|
|
134
133
|
'test-results',
|
|
135
134
|
'playwright-report',
|
|
136
135
|
'public/mockServiceWorker.js',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.57.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": {
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"gen:api": "openapi-typescript ./openapi/foundation.yaml -o ./src/api/schema.ts",
|
|
67
67
|
"lint": "eslint .",
|
|
68
68
|
"docs:index": "node scripts/doc-index.mjs",
|
|
69
|
-
"
|
|
70
|
-
"build-
|
|
71
|
-
"preview-
|
|
69
|
+
"harness": "vite -c vite.harness.config.ts",
|
|
70
|
+
"build-harness": "vite build -c vite.harness.config.ts",
|
|
71
|
+
"preview-harness": "vite preview -c vite.harness.config.ts",
|
|
72
72
|
"verify:fast": "npm run gen:api && git diff --exit-code -- src/api/schema.ts && node scripts/doc-index.mjs --check && tsc -b && eslint . --max-warnings 0 && node bin/ui-foundation.mjs check-deps && npm run build && vitest run",
|
|
73
|
-
"verify": "npm run verify:fast && playwright test -c playwright.
|
|
73
|
+
"verify": "npm run verify:fast && playwright test -c playwright.harness.config.ts"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@axe-core/playwright": "^4.13.0",
|
|
@@ -157,8 +157,6 @@
|
|
|
157
157
|
"@base-ui/react": "^1.8.0",
|
|
158
158
|
"@eslint/js": "^10.0.1",
|
|
159
159
|
"@playwright/test": "^1.63.0",
|
|
160
|
-
"@storybook/addon-docs": "^10.6.0",
|
|
161
|
-
"@storybook/react-vite": "^10.6.0",
|
|
162
160
|
"@tailwindcss/vite": "^4.3.3",
|
|
163
161
|
"@tanstack/eslint-plugin-query": "^5.103.1",
|
|
164
162
|
"@tanstack/react-query": "^5.103.1",
|
|
@@ -180,7 +178,6 @@
|
|
|
180
178
|
"react-hook-form": "^7.88.0",
|
|
181
179
|
"react-router": "^7.18.4",
|
|
182
180
|
"sonner": "^2.0.8",
|
|
183
|
-
"storybook": "^10.6.3",
|
|
184
181
|
"tailwindcss": "^4.3.3",
|
|
185
182
|
"typescript": "~6.0.3",
|
|
186
183
|
"typescript-eslint": "^8.70.0",
|