@tristan2828/ui-foundation 4.33.0 → 4.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/conventions/AGENTS.md +9 -3
  2. package/conventions/docs/add-an-entity.md +33 -3
  3. package/conventions/docs/cell-patterns.md +51 -9
  4. package/conventions/docs/design-language.md +65 -1
  5. package/conventions/docs/timed-screens.md +13 -0
  6. package/dist/components/app/app-shell-bottom-bar.d.ts +11 -0
  7. package/dist/components/app/app-shell-bottom-bar.js +49 -0
  8. package/dist/components/app/app-shell-nav.d.ts +41 -0
  9. package/dist/components/app/app-shell-nav.js +40 -8
  10. package/dist/components/app/app-shell.d.ts +15 -4
  11. package/dist/components/app/app-shell.js +21 -7
  12. package/dist/components/app/data-table.d.ts +1 -1
  13. package/dist/components/app/data-table.js +10 -4
  14. package/dist/components/app/line-chart.d.ts +26 -0
  15. package/dist/components/app/line-chart.js +59 -0
  16. package/dist/components/app/markdown.d.ts +9 -2
  17. package/dist/components/app/markdown.js +13 -9
  18. package/dist/components/app/milkdown-editor.d.ts +1 -1
  19. package/dist/components/app/milkdown-editor.js +7 -4
  20. package/dist/components/app/prose-look.d.ts +13 -4
  21. package/dist/components/app/prose-look.js +8 -5
  22. package/dist/components/app/rich-text-editor.d.ts +8 -1
  23. package/dist/components/app/widget-card.d.ts +11 -1
  24. package/dist/components/app/widget-card.js +19 -3
  25. package/dist/hooks/use-muted.d.ts +4 -10
  26. package/dist/hooks/use-muted.js +4 -67
  27. package/dist/hooks/use-remembered-flag.d.ts +18 -0
  28. package/dist/hooks/use-remembered-flag.js +78 -0
  29. package/dist/index.d.ts +3 -2
  30. package/dist/index.js +2 -1
  31. package/dist/lib/line-chart-layout.d.ts +23 -0
  32. package/dist/lib/line-chart-layout.js +44 -0
  33. package/dist/lib/markdown-top-heading.d.ts +2 -0
  34. package/dist/lib/markdown-top-heading.js +21 -0
  35. package/dist/testing/index.d.ts +16 -4
  36. package/dist/testing/index.js +5 -3
  37. package/package.json +3 -2
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { AppShell, type AppShellProps, type AppShellVersion, type NavEntry, type NavItem, type NavSection } from './components/app/app-shell.js';
1
+ export { AppShell, type AppShellProps, type AppShellVersion, type BottomBarItem, type NavEntry, type NavItem, type NavSection, } from './components/app/app-shell.js';
2
2
  export type { IconComponent } from './components/app/icon-component.js';
3
3
  export { CountLink, type CountLinkProps } from './components/app/count-link.js';
4
4
  export { DataTable, type DataTableProps, type DataTableTotals, type DataTableWidth, type SortingState, } from './components/app/data-table.js';
@@ -13,7 +13,7 @@ export { ErrorState } from './components/app/error-state.js';
13
13
  export { InlineCreate, type InlineCreateProps } from './components/app/inline-create.js';
14
14
  export { FoundationProviders } from './components/app/foundation-providers.js';
15
15
  export { Markdown, type MarkdownProps } from './components/app/markdown.js';
16
- export type { HeadingLevel } from './components/app/prose-look.js';
16
+ export type { HeadingFrom, HeadingLevel } from './components/app/prose-look.js';
17
17
  export { MockModeBanner } from './components/app/mock-mode-banner.js';
18
18
  export { MultiChoice } from './components/app/multi-choice.js';
19
19
  export { MultiPick, type MultiPickOption, type MultiPickProps } from './components/app/multi-pick.js';
@@ -30,6 +30,7 @@ export { StageCircle, type StageCircleProps, type StageCircleStage, type StageCi
30
30
  export { Notice, type NoticeProps, type NoticeTone } from './components/app/notice.js';
31
31
  export { SourceCredit, type Source, type SourceCreditProps } from './components/app/source-credit.js';
32
32
  export { ProgressRing, type ProgressRingProps } from './components/app/progress-ring.js';
33
+ export { LineChart, type LineChartMark, type LineChartPoint, type LineChartProps, } from './components/app/line-chart.js';
33
34
  export { StepBar, type StepBarProps } from './components/app/step-bar.js';
34
35
  export { RatingStar, type RatingStarProps, type RatingStarValue } from './components/app/rating-star.js';
35
36
  export { DateField, type DateFieldProps } from './components/app/date-field.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // the gateway seam, MSW support and Playwright suites have their own entry
4
4
  // points (`/gateway`, `/mocks`, `/testing`) so each stays behind its boundary.
5
5
  // Composites
6
- export { AppShell } from './components/app/app-shell.js';
6
+ export { AppShell, } from './components/app/app-shell.js';
7
7
  export { CountLink } from './components/app/count-link.js';
8
8
  export { DataTable, } from './components/app/data-table.js';
9
9
  export {} from './components/app/table-groups.js';
@@ -33,6 +33,7 @@ export { StageCircle, } from './components/app/stage-circle.js';
33
33
  export { Notice } from './components/app/notice.js';
34
34
  export { SourceCredit } from './components/app/source-credit.js';
35
35
  export { ProgressRing } from './components/app/progress-ring.js';
36
+ export { LineChart, } from './components/app/line-chart.js';
36
37
  export { StepBar } from './components/app/step-bar.js';
37
38
  export { RatingStar } from './components/app/rating-star.js';
38
39
  export { DateField } from './components/app/date-field.js';
@@ -0,0 +1,23 @@
1
+ export type LineChartPoint = {
2
+ /** A day (`'YYYY-MM-DD'`), a timestamp or any number. Points sit apart by it; if one isn't a time or a number, they're evenly spaced. */
3
+ x: string | number;
4
+ y: number;
5
+ };
6
+ export type LineChartMark = 'min' | 'max' | 'last';
7
+ export type ChartLayout = {
8
+ /** Each point's place, in the order given. */
9
+ places: {
10
+ x: number;
11
+ y: number;
12
+ }[];
13
+ /** The y range drawn, bottom to top: the values and the target, with room around them. */
14
+ domain: {
15
+ min: number;
16
+ max: number;
17
+ };
18
+ /** The target line's height, if there's a target. */
19
+ targetY?: number;
20
+ /** Which point the mark is on, if any. */
21
+ markIndex?: number;
22
+ };
23
+ export declare function chartLayout(points: readonly LineChartPoint[], target?: number, mark?: LineChartMark): ChartLayout;
@@ -0,0 +1,44 @@
1
+ // Where LineChart's points, target and mark go (#238), React-free so the
2
+ // rules are unit tested on their own (tests/line-chart-layout.test.ts).
3
+ // Positions are fractions of the plot, 0…1, x from the left and y from
4
+ // the top, so the chart fills whatever width it's given.
5
+ // A point's position along x: a number as it is, a day or a timestamp as
6
+ // its time.
7
+ function along(x) {
8
+ return typeof x === 'number' ? x : Date.parse(x);
9
+ }
10
+ // Room above and below the values, as a share of their range, so the line
11
+ // never runs along the plot's edge.
12
+ const PAD = 0.1;
13
+ export function chartLayout(points, target, mark) {
14
+ const values = points.map((point) => point.y);
15
+ const all = target === undefined ? values : [...values, target];
16
+ let min = Math.min(...all);
17
+ let max = Math.max(...all);
18
+ // One value (or all the same): a band around it, so the line sits mid-plot.
19
+ const spread = max - min || Math.abs(max) || 1;
20
+ min -= spread * PAD;
21
+ max += spread * PAD;
22
+ const height = (y) => (max - y) / (max - min);
23
+ const times = points.map((point) => along(point.x));
24
+ const timed = times.every(Number.isFinite) && times.length > 1 && times.at(-1) !== times[0];
25
+ const first = timed ? Math.min(...times) : 0;
26
+ const span = timed ? Math.max(...times) - first : points.length - 1;
27
+ const across = (index) => (span === 0 ? 0.5 : ((timed ? times[index] : index) - first) / span);
28
+ let markIndex;
29
+ if (points.length > 0 && mark) {
30
+ if (mark === 'last')
31
+ markIndex = points.length - 1;
32
+ else {
33
+ const pick = mark === 'min' ? Math.min(...values) : Math.max(...values);
34
+ // The latest of equal values: the one that's still true.
35
+ markIndex = values.lastIndexOf(pick);
36
+ }
37
+ }
38
+ return {
39
+ places: points.map((point, index) => ({ x: across(index), y: height(point.y) })),
40
+ domain: { min, max },
41
+ targetY: target === undefined ? undefined : height(target),
42
+ markIndex,
43
+ };
44
+ }
@@ -0,0 +1,2 @@
1
+ /** The lowest `depth` of any heading in the text, nested ones included; 1 when it has none. */
2
+ export declare function textTopHeading(markdown: string): number;
@@ -0,0 +1,21 @@
1
+ // The text's own highest heading level (1 for `#`), for `headingFrom="text"`
2
+ // on <Markdown> and the rich-text editor: notes pasted or imported from
3
+ // another tool often start at `##`, and that level is the one that takes
4
+ // the field's `headingLevel`. Parsed as both parse it (remark, GFM), so a
5
+ // `#` in a code block or a setext heading counts as they render it.
6
+ import remarkGfm from 'remark-gfm';
7
+ import remarkParse from 'remark-parse';
8
+ import { unified } from 'unified';
9
+ const parser = unified().use(remarkParse).use(remarkGfm);
10
+ /** The lowest `depth` of any heading in the text, nested ones included; 1 when it has none. */
11
+ export function textTopHeading(markdown) {
12
+ let top = 7;
13
+ const visit = (node) => {
14
+ if (node.type === 'heading' && node.depth)
15
+ top = Math.min(top, node.depth);
16
+ for (const child of node.children ?? [])
17
+ visit(child);
18
+ };
19
+ visit(parser.parse(markdown));
20
+ return top === 7 ? 1 : top;
21
+ }
@@ -43,8 +43,11 @@ export type A11ySuiteOptions = {
43
43
  * sparse one, so both filled values and "not set" labels are checked.
44
44
  */
45
45
  viewRoutes?: readonly string[];
46
- /** Screens outside the app shell, e.g. '/login' (and '/register' if the app has it). */
47
- loggedOutRoutes: readonly string[];
46
+ /**
47
+ * Screens outside the app shell, e.g. '/login' (and '/register' if the
48
+ * app has it). An app with several a11y spec files lists them in one.
49
+ */
50
+ loggedOutRoutes?: readonly string[];
48
51
  /**
49
52
  * The app's nav, the same `NavEntry[]` passed to `AppShell` (`src/nav.ts`).
50
53
  * Given, each page in it is a test of its own, so the suite's time per
@@ -54,6 +57,13 @@ export type A11ySuiteOptions = {
54
57
  * their number.
55
58
  */
56
59
  nav?: readonly NavEntry[];
60
+ /**
61
+ * Whether to check that the rendered sidebar links exactly `nav`'s pages
62
+ * (default true). An app with one a11y spec per module passes each
63
+ * module's own entries as `nav` with `checkSidebar: false`, and keeps
64
+ * the check in one file that passes the whole nav.
65
+ */
66
+ checkSidebar?: boolean;
57
67
  };
58
68
  /**
59
69
  * Every data view on the page has loaded: at least one shows its data (or,
@@ -71,12 +81,14 @@ export declare function expectDataViewsLoaded(page: Page, { success }?: {
71
81
  * Accessibility of the real screens in light and dark mode: every page in
72
82
  * the Primary nav (one test each from `nav`, or discovered in the sidebar
73
83
  * when `nav` is left out), the given form routes, and the logged-out
74
- * screens. axe's
84
+ * screens. A modular app can split it into a spec per module: each passes
85
+ * its module's `nav` entries and routes with `checkSidebar: false`, and
86
+ * one file keeps the whole-sidebar check. axe's
75
87
  * color-contrast rule against the rendered dark DOM is also what keeps the
76
88
  * dark-mode tokens readable. The theme follows the OS setting, so
77
89
  * emulateMedia switches it.
78
90
  */
79
- export declare function defineA11ySuite({ formRoutes, viewRoutes, loggedOutRoutes, nav }: A11ySuiteOptions): void;
91
+ export declare function defineA11ySuite({ formRoutes, viewRoutes, loggedOutRoutes, nav, checkSidebar, }: A11ySuiteOptions): void;
80
92
  export type MockModeBannerSuiteOptions = {
81
93
  /** Screens to check, logged-out ones included (e.g. '/login'). */
82
94
  routes: readonly string[];
@@ -140,16 +140,18 @@ async function expectNavPageAccessible(page, route, colorScheme) {
140
140
  * Accessibility of the real screens in light and dark mode: every page in
141
141
  * the Primary nav (one test each from `nav`, or discovered in the sidebar
142
142
  * when `nav` is left out), the given form routes, and the logged-out
143
- * screens. axe's
143
+ * screens. A modular app can split it into a spec per module: each passes
144
+ * its module's `nav` entries and routes with `checkSidebar: false`, and
145
+ * one file keeps the whole-sidebar check. axe's
144
146
  * color-contrast rule against the rendered dark DOM is also what keeps the
145
147
  * dark-mode tokens readable. The theme follows the OS setting, so
146
148
  * emulateMedia switches it.
147
149
  */
148
- export function defineA11ySuite({ formRoutes, viewRoutes = [], loggedOutRoutes, nav }) {
150
+ export function defineA11ySuite({ formRoutes, viewRoutes = [], loggedOutRoutes = [], nav, checkSidebar = true, }) {
149
151
  // Each `to` as given, query string and all, so a page that opens on a
150
152
  // saved view is checked on it.
151
153
  const listed = nav && navTargets(nav);
152
- if (listed) {
154
+ if (listed && checkSidebar) {
153
155
  test('the sidebar links exactly the pages in nav', async ({ page, isMobile }) => {
154
156
  const rendered = await navRoutes(page, isMobile);
155
157
  expect(pagePaths(rendered), 'The sidebar and the nav given to defineA11ySuite differ: pass the same NavEntry[] AppShell gets').toEqual(pagePaths(listed));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "4.33.0",
3
+ "version": "4.34.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": {
@@ -65,10 +65,11 @@
65
65
  "prepack": "npm run build",
66
66
  "gen:api": "openapi-typescript ./openapi/foundation.yaml -o ./src/api/schema.ts",
67
67
  "lint": "eslint .",
68
+ "docs:index": "node scripts/doc-index.mjs",
68
69
  "storybook": "storybook dev -p 6006",
69
70
  "build-storybook": "storybook build",
70
71
  "preview-storybook": "vite preview --outDir storybook-static --port 6006 --strictPort",
71
- "verify:fast": "npm run gen:api && git diff --exit-code -- src/api/schema.ts && tsc -b && eslint . --max-warnings 0 && node bin/ui-foundation.mjs check-deps && npm run build && vitest run",
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",
72
73
  "verify": "npm run verify:fast && playwright test -c playwright.storybook.config.ts"
73
74
  },
74
75
  "peerDependencies": {