@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.
@@ -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 (Storybook `app/DateField` EditInPlace). The backend column
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 (Storybook
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 Storybook renders
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 Storybook story.
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
- Storybook renders it (`patterns/CellPatterns`, "Icons with one tooltip").
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 Storybook (`patterns/Scales`), every value on
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 Storybook renders it
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 Storybook renders it
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
- Storybook glyph check does.
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 Storybook story `app/DataTable`
1205
- "Edit in the rows" (`data-table.stories.tsx`); the template has no small
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 Storybook (`app/StageCircle`).
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 Storybook
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 Storybook renders it
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
- - Storybook's `app/DataTable` Highlighted story has two highlighted rows
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 Storybook renders it (`app/DataTable grouping`), with axe in
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.** Storybook is already
112
- in this repo and is the obvious host: the cards then use the real tokens
113
- and the real `Badge`, and the winner can be copied straight out. Do this
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/storybook-visual.spec.ts` re-checks all
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 Storybook has a glyph check of its own: every `<svg>` in its
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 Storybook glyph check draws an icon in each slot on both
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. Storybook's
393
- Density toolbar shows any story at a density, and `patterns/Density`
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
- - Storybook's `app/WidgetCard` shows three widgets on a page and every
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
- - Storybook's `app/ModuleCard` shows a hub with a strip and a widget,
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. Storybook's `app/Notice` measures every
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 Storybook, `app/AllowReminders`):
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
- Storybook has the whole thing working together: `app/SessionView`, the
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 (Storybook, tests). Renders nothing for null. */
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 (Storybook, tests). Renders nothing for null. */
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('.stories.'))
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.55.0",
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
- "storybook": "storybook dev -p 6006",
70
- "build-storybook": "storybook build",
71
- "preview-storybook": "vite preview --outDir storybook-static --port 6006 --strictPort",
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.storybook.config.ts"
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",