@imfusion/web-ui 0.5.1-dev.6.gece7a7b5 → 0.6.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 (98) hide show
  1. package/README.md +90 -43
  2. package/bin/install.js +428 -0
  3. package/bin/install.test.ts +329 -0
  4. package/dist/build/vite-css-module-names/index.d.ts +20 -0
  5. package/dist/build/vite-css-module-names.js +17 -0
  6. package/dist/chunk-DmhlhrBa.js +11 -0
  7. package/dist/code-Blo48PGr.js +136 -0
  8. package/dist/codegen/gen-icons.d.ts +24 -0
  9. package/dist/components/callout/callout.d.ts +1 -1
  10. package/dist/components/chip-link/chip-link.d.ts +1 -1
  11. package/dist/components/code/code.d.ts +10 -18
  12. package/dist/components/field/field.d.ts +104 -0
  13. package/dist/components/field/field.meta.d.ts +2 -0
  14. package/dist/components/field/index.d.ts +2 -0
  15. package/dist/components/fieldset/fieldset.d.ts +29 -0
  16. package/dist/components/fieldset/fieldset.meta.d.ts +2 -0
  17. package/dist/components/fieldset/index.d.ts +2 -0
  18. package/dist/components/icon/icon.d.ts +16 -0
  19. package/dist/components/icon/icon.meta.d.ts +2 -0
  20. package/dist/components/icon/index.d.ts +4 -0
  21. package/dist/components/icon/types.d.ts +2 -0
  22. package/dist/components/input/input.d.ts +3 -1
  23. package/dist/components/typo/typo.d.ts +2 -2
  24. package/dist/docgen/component-sources.d.ts +7 -0
  25. package/dist/hooks/index.d.ts +1 -0
  26. package/dist/hooks/use-resize-observer.d.ts +2 -0
  27. package/dist/icons/catalog.gen.d.ts +8357 -0
  28. package/dist/icons/icon-config-provider.d.ts +8 -0
  29. package/dist/icons/icon-context.d.ts +4 -0
  30. package/dist/icons/icons.gen.d.ts +1672 -0
  31. package/dist/icons/index.d.ts +3 -0
  32. package/dist/icons-wBmF0U2x.js +78 -0
  33. package/dist/icons.js +2 -0
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.js +1793 -12210
  36. package/dist/integrations/code-highlight/code-highlight.d.ts +8 -6
  37. package/dist/integrations/code-highlight/highlighter.d.ts +32 -3
  38. package/dist/integrations/code-highlight/language-patterns.d.ts +7 -0
  39. package/dist/integrations/code-highlight/languages/cmake.d.ts +1 -0
  40. package/dist/integrations/code-highlight/languages/cpp.d.ts +1 -0
  41. package/dist/integrations/code-highlight/languages/python.d.ts +1 -0
  42. package/dist/integrations/code-highlight.js +198 -59
  43. package/dist/integrations/image-display-options.js +70 -69
  44. package/dist/llms/gen-tokens.d.ts +7 -0
  45. package/dist/meta-CySnRuVp.js +21 -0
  46. package/dist/provider/web-ui-provider.d.ts +4 -1
  47. package/dist/style.css +1 -1
  48. package/dist/tabs-CMKvMF4E.js +369 -0
  49. package/package.json +48 -26
  50. package/src/docgen/doc.gen.json +381 -38
  51. package/src/llms/icon-catalog.gen.json +11203 -0
  52. package/src/llms/install-templates/AGENTS.md +34 -0
  53. package/src/llms/install-templates/codex-hooks.json +44 -0
  54. package/src/llms/install-templates/hooks/baseline-staleness.sh +17 -0
  55. package/src/llms/install-templates/hooks/session-start.sh +5 -0
  56. package/src/llms/install-templates/hooks/stop.sh +18 -0
  57. package/src/llms/install-templates/hooks/subagent-start.sh +5 -0
  58. package/src/llms/install-templates/hooks/user-prompt-submit.sh +5 -0
  59. package/src/llms/install-templates/settings.json +45 -0
  60. package/src/llms/llms.gen.txt +17 -0
  61. package/src/llms/skills/imf-web-ui/SKILL.md +13 -12
  62. package/src/llms/skills/imf-web-ui-audit/SKILL.md +119 -0
  63. package/src/llms/skills/imf-web-ui-components/SKILL.md +56 -3
  64. package/src/llms/skills/imf-web-ui-conventions/SKILL.md +57 -0
  65. package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +141 -0
  66. package/src/llms/skills/imf-web-ui-conventions/templates/REPORT.md +45 -0
  67. package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +82 -0
  68. package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +27 -0
  69. package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +65 -0
  70. package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +50 -0
  71. package/src/llms/skills/imf-web-ui-conventions/topics/components.md +101 -0
  72. package/src/llms/skills/imf-web-ui-conventions/topics/data.md +221 -0
  73. package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +40 -0
  74. package/src/llms/skills/imf-web-ui-conventions/topics/git.md +34 -0
  75. package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +35 -0
  76. package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +26 -0
  77. package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +53 -0
  78. package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +44 -0
  79. package/src/llms/skills/imf-web-ui-conventions/topics/react.md +109 -0
  80. package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +88 -0
  81. package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +25 -0
  82. package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +7 -0
  83. package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +116 -0
  84. package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +73 -0
  85. package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +62 -0
  86. package/src/llms/skills/imf-web-ui-setup/SKILL.md +67 -37
  87. package/src/llms/skills/imf-web-ui-update/SKILL.md +157 -0
  88. package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
  89. package/src/llms/skills/imf-web-ui-ux/references/forms.md +2 -2
  90. package/src/llms/tokens.gen.json +887 -0
  91. package/bin/install-skill.js +0 -180
  92. package/dist/code-qBbqAHK-.js +0 -190
  93. package/dist/meta-B8C51eyL.js +0 -74
  94. package/dist/tabs-DqBFSqq6.js +0 -3789
  95. package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -93
  96. package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +0 -133
  97. package/src/llms/skills/imf-web-ui-frontend-patterns/references/react-patterns.md +0 -94
  98. package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +0 -201
@@ -0,0 +1,35 @@
1
+ # Library boundary
2
+
3
+ The contract between an app and `@imfusion/web-ui`.
4
+
5
+ ## Stay behind the library
6
+
7
+ - Never import Base UI (or any other upstream the library wraps) directly — no upstream stylesheets, no upstream components,
8
+ even when upstream docs show it that way: everything a component needs ships in `@imfusion/web-ui`.
9
+ - Library missing something upstream has → report the gap (see `imf-web-ui-components`); don't reach around it.
10
+ - Import icons from `@imfusion/web-ui/icons` and render them through its `Icon` component. Direct imports from
11
+ `iconoir-react` are outside the library boundary.
12
+
13
+ ## Wrap primitives when the app has a reason to
14
+
15
+ - Repeated adaptation around a primitive (default props, a styling override, a composition, an accessibility refinement, a
16
+ restriction of the API) → wrap it once in an app-level dumb component in `components/` ([components.md](components.md)).
17
+ - The wrapper derives its props from the primitive (`React.ComponentProps<typeof Button>`, narrowed or extended) and styles
18
+ itself through the sanctioned seams — never the library's internals.
19
+ - Wrap for a reason — any repeated adaptation counts; a wrapper that only renames a primitive is indirection with no payoff.
20
+ - Components marked `experimental` in the identity index get wrapped **always**, even with nothing added yet — a breaking
21
+ upstream change then lands in one file instead of every call site.
22
+
23
+ ## Derive types, don't import them
24
+
25
+ - Prop types come from the components themselves: `React.ComponentProps<typeof Button>`.
26
+ - The library deliberately exports no `Props` types — don't look for them, don't re-declare prop shapes by hand.
27
+
28
+ ## Integrations own their peers
29
+
30
+ - `@imfusion/web-ui/integrations/*` components depend on optional peers (e.g. `@tanstack/highlight` for `CodeHighlight`). Add
31
+ the peer explicitly to the consumer's `package.json` — never rely on hoisting.
32
+
33
+ ## Styling crosses the boundary through seams
34
+
35
+ - Tokens in, sanctioned selectors at the edge, never the library's internals — the full contract is [styling.md](styling.md).
@@ -0,0 +1,26 @@
1
+ # Library setup
2
+
3
+ Library wiring (styles import + `WebUIProvider`) for anyone using `@imfusion/web-ui` — `imf-web-ui-setup library-setup`
4
+ proposes a bootstrap change, `imf-web-ui-audit library-setup` is the read-only health check.
5
+
6
+ ## Entry-point wiring
7
+
8
+ - Every consumer entry point has exactly two lines, in this order:
9
+
10
+ ```tsx
11
+ import "@imfusion/web-ui/styles.css";
12
+ import { WebUIProvider, Button } from "@imfusion/web-ui";
13
+ ```
14
+
15
+ - Wrap the app root in `<WebUIProvider>` once — components rendered outside it lack the theme and CSS-variable context they
16
+ expect.
17
+ - Never import a Base UI (or other upstream) stylesheet or component directly — everything a web-ui component needs is inside
18
+ `styles.css` and the package's own exports. What may be reached for when the library lacks a component:
19
+ [library boundary](library-boundary.md).
20
+
21
+ ## Symptoms of broken setup
22
+
23
+ - Components render but look unstyled → the `styles.css` import is missing from the entry point.
24
+ - Components render but ignore the theme (wrong colors, CSS variables not resolving) → mounted outside `<WebUIProvider>`.
25
+ - An integration component throws on import → its optional peer dependency is not installed; check the component's
26
+ description in the docgen index (`imf-web-ui-components`) for which peer to add to `package.json`.
@@ -0,0 +1,53 @@
1
+ # npm project
2
+
3
+ The npm side of an ImFusion frontend: `package.json`, scripts, dependencies. `imf-web-ui-setup` audits against this file.
4
+
5
+ ## package.json
6
+
7
+ ```jsonc
8
+ {
9
+ "name": "@imfusion/scan-review",
10
+ "type": "module",
11
+ "private": true, // only when the package is not meant to be published
12
+ "engines": { "node": ">=22" },
13
+ "imports": { "#/*": "./src/*" }
14
+ }
15
+ ```
16
+
17
+ - `"type": "module"` always.
18
+ - `"private": true` only for apps that never publish.
19
+ - Pin Node via `engines.node` or `.nvmrc` — never a personal version manager's config.
20
+ - Wire the `#/` alias through `imports` ([project-structure.md](project-structure.md)).
21
+
22
+ ## Scripts
23
+
24
+ Same name, same meaning, every repo — "what can I run to check this?" is tab-completing `verify:`.
25
+
26
+ | Script | Runs |
27
+ | ------------------ | --------------------------------------------------------------------- |
28
+ | `dev` | dev server |
29
+ | `build` | production build |
30
+ | `verify:deps` | exact-pin check over `dependencies` and `devDependencies` |
31
+ | `verify:format` | `prettier --check .` |
32
+ | `verify:lint` | `eslint . --cache --max-warnings=0` |
33
+ | `verify:typecheck` | `tsc --noEmit` (or `tsc -b --noEmit` in a project-references setup) |
34
+ | `verify:tests` | `vitest run` |
35
+ | `verify:knip` | `knip` — dead-code and unused-export check |
36
+ | `verify:staged` | staged-file subset, called by the pre-commit hook |
37
+ | `verify:full` | every `verify:*` check plus the build; what CI runs |
38
+ | `format` | `prettier --write .` |
39
+ | `lint` | `eslint . --cache --fix` |
40
+ | `git:config` | see [git.md](git.md); run by hand once per clone, named in the README |
41
+
42
+ - **Every check is `verify:*`** — one namespace for everything that reads and reports.
43
+ - **Write-mode scripts keep the tool name** (`format`, `lint`): changing files isn't verifying — no prefix, no `:fix` suffix.
44
+ - `verify:staged` is fast and partial: staged-file format/lint plus the relevant project-wide checks for staged source or
45
+ config changes, including `verify:knip` — a passing commit is not CI green.
46
+ - `verify:full` is the CI gate.
47
+
48
+ ## Dependencies
49
+
50
+ - **Pin exactly** — no `^`, `~`, or `latest`, in `dependencies` and `devDependencies` alike: `verify:deps` catches drift,
51
+ `save-exact=true` in `.npmrc` prevents it.
52
+ - **`ignore-scripts=true` in `.npmrc`** — blocks lifecycle scripts on install (the supply-chain vector); setup that matters
53
+ is a command someone runs, not a hook that fires on install.
@@ -0,0 +1,44 @@
1
+ # Project structure
2
+
3
+ Kebab-case throughout, folders and files alike; exported symbols stay PascalCase — only the filename is kebab.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ src/
9
+ main.tsx # providers and router bootstrap; one WebUIProvider at the app root
10
+ routes/ # TanStack Router file-based routes; routing only — they compose, they don't fetch inline
11
+ __root.tsx # document-wide error/not-found boundary and Outlet
12
+ _public/ # anonymous route group; resolves the current user when public pages need it
13
+ route.tsx
14
+ _app/ # pathless authenticated route group; guards the subtree before children render
15
+ route.tsx
16
+ api/ # one folder per API topic — layout and behaviour in data.md
17
+ auth/ # one current-user query plus server-owned login/logout URL helpers
18
+ components/ # grouped by kind — anatomy in components.md
19
+ app-shell/ # optional persistent authenticated chrome: brand, navigation, and session action
20
+ http/ # transport: client, error normalisation — the only transport-aware place (data.md)
21
+ lib/ # framework-free helpers, each with a colocated .test.ts when it has decisions to test
22
+ auth/
23
+ login-url.ts
24
+ login-url.test.ts
25
+ ```
26
+
27
+ - Each subtree's rules live with its topic: [data.md](data.md) for `api/` and `http/`, [components.md](components.md) for
28
+ `components/`.
29
+
30
+ ## Application boundary
31
+
32
+ - Start every frontend with the root providers, a current-user query, and pathless `_public/` and `_app/` route groups —
33
+ contract in [authentication.md](authentication.md); add feature routes only after that boundary exists.
34
+ - Greenfield: `_app/` starts with a minimal `AppShell` — ImFusion logo, route navigation, and a stable session-action area
35
+ around the route `Outlet`.
36
+ - Omit the shell only when the human explicitly says the product has no persistent authenticated navigation; the `_app/`
37
+ guard stays.
38
+ - An established project preserves its working choice; public pages can use a small branded header instead.
39
+
40
+ ## Imports
41
+
42
+ - `#/` alias for anything outside the current folder, plain `./` for siblings.
43
+ - The alias is always `#/` → `src/`: `#` is Node's own subpath-import prefix, so it can't collide with an npm scope.
44
+ - Wire it once, through `package.json`'s `imports` field (toolchain-native), with a matching tsconfig `paths` entry.
@@ -0,0 +1,109 @@
1
+ # React
2
+
3
+ House defaults for React around `@imfusion/web-ui`. Links are for you, the agent — authoritative when a case here is
4
+ ambiguous; hand them to the human only if asked.
5
+
6
+ ## Composition: pages (smart containers), partials, dumb components
7
+
8
+ The architecture principle, top-down — rooted in Dan Abramov's
9
+ [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) and
10
+ [Thinking in React](https://react.dev/learn/thinking-in-react). Styling lives exclusively in dumb and layout components,
11
+ nowhere else:
12
+
13
+ - **Pages are the smart containers.** The route component in `routes/` is the container — don't go looking for `*Container`
14
+ files. It owns how things _work_: loaders prefetch, the component fetches data, orchestrates, composes partials and passes
15
+ data down. Zero styling — a page wanting CSS (two panels side by side) means a layout component, not an inline style. A
16
+ standalone container earns its place only off-route (a modal fetching its own data).
17
+ - **Partials** are pure composition: reusable units of dumb and layout components with light logic and zero styling of their
18
+ own, extracted because they read as a unit. A partial with a CSS file is a red flag — the styled element wants to be its
19
+ own dumb component, or the wrapper wants to be a layout component.
20
+ - **Layout components** own _arrangement_ and nothing else: `Stack`-/`Row`-based wrappers with token gaps, a page grid, a
21
+ section frame.
22
+ - **Dumb components** own how things _look_: style and compose library primitives, take plain data and callbacks as props; no
23
+ fetching, routing, or business logic. All non-layout styling lives here and only here — logic-free dumb components (the
24
+ `@imfusion/web-ui` layer) keep screens restylable, testable with plain props, resilient to library updates.
25
+ - Wrapping library primitives in app-level dumb components: [library-boundary.md](library-boundary.md).
26
+
27
+ ```tsx
28
+ // Dumb — renders what it's given
29
+ function UserCard({ name, role, onEdit }: { name: string; role: string; onEdit: () => void }) {
30
+ return (
31
+ <Card.Root>
32
+ <Card.Content>
33
+ <Typo>{name}</Typo>
34
+ <Chip>{role}</Chip>
35
+ </Card.Content>
36
+ <Card.Footer>
37
+ <Button onClick={onEdit}>Edit</Button>
38
+ </Card.Footer>
39
+ </Card.Root>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ```tsx
45
+ // Smart — knows where data comes from, renders the dumb component
46
+ function UserCardContainer({ userId }: { userId: string }) {
47
+ const user = useUserQuery(userId);
48
+ const openEditor = useEditorNavigation(userId);
49
+ return <UserCard name={user.data.name} role={user.data.role} onEdit={openEditor} />;
50
+ }
51
+ ```
52
+
53
+ ## Compose, don't configure
54
+
55
+ - Build screen-level pieces by composing primitives (`Stack`, `Row`, `Card`, dumb components), not one component with a dozen
56
+ boolean props — a prop list that reads like a settings page wanted to be two or three components.
57
+ - Lift state shared between siblings to the nearest common parent
58
+ ([Sharing State Between Components](https://react.dev/learn/sharing-state-between-components)); never sync copies.
59
+
60
+ ## Put state where its truth lives
61
+
62
+ Work down this list; stop at the first match:
63
+
64
+ 1. **URL state** — shareable via the address bar (filters, sort, pagination, active tab) → TanStack Router search params;
65
+ back button and copied links come free. These cross an external boundary — parse per [validation.md](validation.md).
66
+ 2. **Server state** — from an API → TanStack Query's cache ([data.md](data.md)); never copy it into `useState` — that's how
67
+ stale-UI bugs are born.
68
+ 3. **Subtree state** — resets on leave (wizard progress) → React context.
69
+ 4. **Client state** — app-wide and persistent → TanStack Store, and only now: tiers 1–2 usually dissolve the "we need a
70
+ store" instinct.
71
+ 5. **Local state** — one component's own (input value, open/closed, a draft, a hover flag) → `useState`. Most state is local;
72
+ keep it in the component, no library.
73
+
74
+ - A form turning drafts into a submitted domain value validates that boundary per [validation.md](validation.md).
75
+ - Shape the state itself per [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure) —
76
+ especially its rules on redundant and duplicated state.
77
+
78
+ ## Effects: last resort, and named
79
+
80
+ - No `useEffect` for: derived values (render or `useMemo`), responses to user actions (the event handler), server sync (the
81
+ data-fetching layer). Effects only synchronize with systems _outside_ React.
82
+ - Read [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) — the definitive misuse catalog —
83
+ before every effect you're tempted to write.
84
+ - Extract a genuine effect into a custom hook named for its purpose (`useSyncedScroll`, `useDocumentTitle`, `useHotkey`),
85
+ never an anonymous inline `useEffect`: the name documents intent, the hook isolates the dependency array. Pattern:
86
+ [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks).
87
+ - The rare effect that stays inline still names its callback, so intent survives without a comment:
88
+
89
+ ```tsx
90
+ useEffect(
91
+ function syncDocumentTitle() {
92
+ document.title = title;
93
+ },
94
+ [title]
95
+ );
96
+ ```
97
+
98
+ - A dependency sourced from a hook or query return: destructure the primitive first — [typescript.md](typescript.md#naming).
99
+
100
+ ## Reading list
101
+
102
+ Consult while building; each is the authority for its topic:
103
+
104
+ - [Thinking in React](https://react.dev/learn/thinking-in-react) — decomposition and one-way data flow
105
+ - [Keeping Components Pure](https://react.dev/learn/keeping-components-pure) — why dumb components stay dumb
106
+ - [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure) — shaping state without duplication
107
+ - [Sharing State Between Components](https://react.dev/learn/sharing-state-between-components) — lifting state
108
+ - [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) — the effect misuse catalog
109
+ - [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) — named effects live here
@@ -0,0 +1,88 @@
1
+ # Styling
2
+
3
+ How app CSS is written and how it meets `@imfusion/web-ui`.
4
+
5
+ ## CSS authoring
6
+
7
+ - **CSS Modules with native CSS only** — no Sass, no CSS-in-JS, no utility-class framework: nesting and custom properties
8
+ already cover DRY. Stylesheet location and ownership: [components.md](components.md).
9
+ - Nest pseudo-elements, states, and child selectors under the root so the prefix is written once:
10
+
11
+ ```css
12
+ .root {
13
+ transition: clip-path var(--ease);
14
+
15
+ &:focus-visible {
16
+ outline: var(--imf-ui-border-size-2) solid var(--imf-ui-color-fg-support);
17
+ }
18
+
19
+ &[data-disabled] {
20
+ opacity: 0.5;
21
+ }
22
+ }
23
+ ```
24
+
25
+ - Lift a repeated literal (an easing, a colour-math result) into a custom property; custom properties also carry
26
+ per-state/per-variant values down the tree — native parameterisation, no mixin needed.
27
+ - Don't merge rules that only look similar — structurally different output (three distinct `clip-path` polygons) isn't
28
+ repetition; keep it explicit.
29
+
30
+ ## Build custom UI from tokens
31
+
32
+ - Custom UI the library doesn't cover (a stat widget, a custom panel) uses names from the shipped token index
33
+ ([tokens.md](tokens.md)) for color, size, radius, and type — that's what makes it look native and survive theme changes.
34
+ - A hex code or a magic `px` next to a concept the tokens already name is a defect.
35
+ - Geometry that can't be a token (a clip-path percentage, a hairline `1px`) → named custom property at the top of the
36
+ stylesheet, invariant written next to it. Don't silently approximate to the nearest token.
37
+
38
+ ## Override through the sanctioned seams
39
+
40
+ - All library styles live in the `imf-ui.components` CSS layer, so any plain selector you write wins — that's the whole
41
+ override contract.
42
+ - Target the stable hooks: `data-imf-ui-component` attributes and your own classes/wrappers.
43
+ - Never target the library's internal class names — they are generated and change without notice.
44
+ - Never `!important` — needing it means you're targeting the wrong thing.
45
+ - Style against state via data attributes (`[data-checked]`, `[data-disabled]`, `[data-popup-open]`) — components expose
46
+ state there, so never maintain your own state classes. `className` is purely a styling surface:
47
+
48
+ ```css
49
+ [data-imf-ui-component="Switch"][data-checked] {
50
+ outline: 2px solid var(--imf-ui-color-bg-positive);
51
+ }
52
+ ```
53
+
54
+ ## The color system
55
+
56
+ - Roles: **surfaces** (`main` canvas, `support` panels, `minor` popovers); **brand** (identity, full saturation) vs
57
+ **primary** (contrast-tuned, CTAs) — distinct roles on purpose; **status** (`negative`, `warning`, `positive`, `info`);
58
+ three **accent** slots.
59
+ - **Controls** are the 80/20 customization surface — override one and every derived token shifts:
60
+
61
+ ```css
62
+ :root {
63
+ --imf-ui-color-primary-hue: 30; /* every primary semantic token follows */
64
+ }
65
+ ```
66
+
67
+ - **Semantic tokens** are what you consume: `--imf-ui-color-bg-{name}` per role, a flat
68
+ `--imf-ui-color-fg-{main|support|minor|oncolor|…}` ladder for text. Borders and rings draw from `fg-*`.
69
+ - Push a hue control into a pale corner → you own overriding the matching `fg-*` token.
70
+ - Color scheme is `<html data-imf-ui-color-scheme="light|dark">`, set by the provider; scheme-specific styling selects via
71
+ that attribute.
72
+
73
+ ## Responsive styling
74
+
75
+ - Tune components per breakpoint by overriding their `--imf-ui-*` variables inside your own media queries — custom properties
76
+ cross the `@media` boundary, props don't (why sizing is variables, not a `width` prop):
77
+
78
+ ```css
79
+ @media (min-width: 768px) {
80
+ .my-shell {
81
+ --imf-ui-appshell-navbar-width: 22rem;
82
+ }
83
+ }
84
+ ```
85
+
86
+ - Write literal breakpoint values — the library's `@custom-media` aliases are build-internal and don't ship.
87
+ - JS only for behaviour (render a burger menu on mobile): `useMediaQuery(minWidth("md"))`. Never for styling CSS can do.
88
+ - No responsive props — `size={{ sm: … }}` objects are deliberately not offered; write the media query.
@@ -0,0 +1,25 @@
1
+ # Testing
2
+
3
+ Test the **decisions**, not the rendering.
4
+
5
+ ## Naming
6
+
7
+ - Colocated tests use `.test.ts` / `.test.tsx` (e.g. `login-url.test.ts`) — not `.spec.*` or plural `.tests.*`.
8
+
9
+ ## What gets a test
10
+
11
+ - **Pure logic gets unit tests** — pricing, permissions, date math, parsing, reducers: cheap, fast, and where bugs actually
12
+ hide.
13
+ - **Presentational components generally don't**: a component mapping props onto web-ui primitives has no logic of its own —
14
+ asserting a `<Button>` rendered tests React, not your code.
15
+ - **Behaviour a user performs gets an interaction test** — a validating form, a stepped flow — through the interface the user
16
+ has (roles, labels, visible text), not through internals.
17
+ - **Extract the decision out of a hook and test it as a plain function** — easier than a render harness; a hook that only
18
+ wraps a browser API has no decision to extract.
19
+ - The measure isn't coverage percentage — it's whether a failing test tells you something you didn't already know.
20
+
21
+ ## Layer-specific recipes
22
+
23
+ - Data layer: stub `fetch`, run the real query client — the recipe is in [data.md](data.md).
24
+ - A schema whose constraints or transforms encode product behavior gets focused accepted, rejected, and transformed cases:
25
+ [validation.md](validation.md).
@@ -0,0 +1,7 @@
1
+ # Tokens
2
+
3
+ ## Token index
4
+
5
+ - `@imfusion/web-ui` ships the generated token index at `node_modules/@imfusion/web-ui/src/llms/tokens.gen.json` — the only
6
+ source for consumer token names and authored default values.
7
+ - Look up the exact entry there; never invent, recall, or duplicate a plausible `--imf-ui-*` name.
@@ -0,0 +1,116 @@
1
+ # Tooling
2
+
3
+ Dependency and configuration baseline for an ImFusion frontend; check before adding a dependency and before writing its
4
+ config.
5
+
6
+ ## Topic-to-tool map
7
+
8
+ - Use the mapped tool for each topic; prefer typed config (`.ts` over `.json`) where the tool supports it.
9
+
10
+ | Topic | Tool |
11
+ | --------------------- | ----------------------------------------------------------------------------------------------------- |
12
+ | Build | [Vite](https://vite.dev) |
13
+ | Routing, URL state | [TanStack Router](https://tanstack.com/router) — file-based, type-safe search params |
14
+ | Server state | [TanStack Query](https://tanstack.com/query) — the pattern around it is [data.md](data.md) |
15
+ | Forms | [TanStack Form](https://tanstack.com/form) |
16
+ | Data grids | [TanStack Table](https://tanstack.com/table) + web-ui's styled `Table` parts |
17
+ | App-wide client state | [TanStack Store](https://tanstack.com/store) — last resort in the state ladder ([react.md](react.md)) |
18
+ | Schema validation | [Zod](https://zod.dev) — at the network boundary ([data.md](data.md)) |
19
+ | Styling | CSS Modules ([styling.md](styling.md)) |
20
+ | Test | [Vitest](https://vitest.dev) |
21
+ | Format | [Prettier](https://prettier.io) |
22
+ | Lint | [ESLint](https://eslint.org) flat config |
23
+ | Types | `tsc --noEmit` — own script, own CI step |
24
+ | Staged files | [lint-staged](https://github.com/lint-staged/lint-staged) (or nano-staged, drop-in) |
25
+ | Dead code | [Knip](https://knipjs.dev) via `verify:knip` — needs per-repo config |
26
+
27
+ ## When a library owns a layer
28
+
29
+ - Adopt the library once you're rebuilding what it does — validation timing and cross-field rules (Form), caching and
30
+ refetching (Query), URL as source of truth (Router), sorting/pagination over rows (Table): adding it mid-project is cheap,
31
+ unpicking a hand-rolled version later is not.
32
+
33
+ ## Devtools
34
+
35
+ - Every TanStack library with a devtools package gets it as a dev dependency, mounted in development only — Router's
36
+ `@tanstack/react-router-devtools` is a given; Query's goes in when Query does.
37
+ - Check for a `-devtools` sibling on every new TanStack dependency — not every library has one yet.
38
+ - Once several are in, host them in one panel via `@tanstack/devtools`.
39
+
40
+ ## Docs over memory
41
+
42
+ - `npx @tanstack/cli` for TanStack docs — never work from memory.
43
+ - Where `@tanstack/intent` is wired up, use it to reach and read the Agent Skills the TanStack dependencies ship.
44
+
45
+ ## Prettier
46
+
47
+ - Config file shape is free (`.prettierrc`, `prettier.config.ts`); the values are not:
48
+
49
+ ```ts
50
+ import type { Config } from "prettier";
51
+
52
+ const config: Config = {
53
+ printWidth: 125,
54
+ tabWidth: 2,
55
+ useTabs: false,
56
+ trailingComma: "none",
57
+ arrowParens: "avoid",
58
+ semi: true,
59
+ singleQuote: false,
60
+ proseWrap: "always"
61
+ };
62
+ ```
63
+
64
+ - Never omit the config file — no file means defaults, and the values silently differ.
65
+
66
+ ## ESLint
67
+
68
+ Flat config (`eslint.config.ts`):
69
+
70
+ - `strictTypeChecked` + `stylisticTypeChecked`, `projectService: true`
71
+ - `as` and `!` banned outside tests (`consistent-type-assertions`)
72
+ - `.gitignore` as the ignore source (`includeIgnoreFile` from `@eslint/compat`)
73
+ - `#/` alias enforced via `no-restricted-imports` banning `../*` — parent-relative paths error, siblings (`./`) stay relative
74
+ ([project-structure.md](project-structure.md))
75
+
76
+ ## tsconfig
77
+
78
+ ```jsonc
79
+ {
80
+ "compilerOptions": {
81
+ "strict": true,
82
+ "moduleResolution": "bundler",
83
+ "verbatimModuleSyntax": true, // import type stays import type
84
+ "noUnusedLocals": true,
85
+ "noUnusedParameters": true,
86
+ "noFallthroughCasesInSwitch": true,
87
+ "noUncheckedSideEffectImports": true,
88
+ "skipLibCheck": true,
89
+ "paths": { "#/*": ["./src/*"] }
90
+ }
91
+ }
92
+ ```
93
+
94
+ - `#/` → `src/` alias rationale: [project-structure.md](project-structure.md).
95
+
96
+ ## Staged files
97
+
98
+ - Runner config (lint-staged or nano-staged) applies eslint `--fix` and prettier `--write` to staged files only.
99
+ - The pre-commit runner also invokes `verify:knip` when staged source or project config can change the reachability graph.
100
+
101
+ ## CSS class names
102
+
103
+ - Generated CSS Module class names are readable in the DOM, `{prefix}-{file}-{local}`, never the default hash: a legible DOM
104
+ is what makes devtools and browser automation usable.
105
+ - The library ships the plugin; pass a short, app-scoped prefix:
106
+
107
+ ```ts
108
+ import { readableCssModuleNames } from "@imfusion/web-ui/build/vite-css-module-names";
109
+
110
+ export default defineConfig({
111
+ plugins: [react(), readableCssModuleNames({ prefix: "acme" })]
112
+ });
113
+ ```
114
+
115
+ - One pattern for dev, Storybook, and production: register the plugin in **every** tool that compiles the CSS — a compiler
116
+ left out generates different names for the same source, and its styles silently don't apply.
@@ -0,0 +1,73 @@
1
+ # TypeScript & code style
2
+
3
+ Functional by default: pure functions over stateful classes, immutable data over in-place mutation, expressions over
4
+ statements, side effects at the edges (network, DOM, store).
5
+
6
+ ## Types
7
+
8
+ - No `any`. `unknown` at a boundary you can't type, narrowed before use.
9
+ - Infer locals; annotate contracts. Exported functions get an explicit return type.
10
+ - No temporal coupling — model the states instead of initialising `null` and filling in later.
11
+ - Avoid `as`. Fix the type. The honest exception: an untyped third-party boundary, kept at the boundary.
12
+ - Derive, don't duplicate:
13
+
14
+ ```ts
15
+ const sizes = ["sm", "md", "lg"] as const;
16
+ type Size = (typeof sizes)[number];
17
+
18
+ const labels: Record<Size, string> = { sm: "S", md: "M", lg: "L" }; // compiler breaks if `sizes` changes
19
+ ```
20
+
21
+ - Same rule across boundaries: library props via `React.ComponentProps<typeof Button>`
22
+ ([library-boundary.md](library-boundary.md)), untrusted data via a runtime schema and `z.infer`
23
+ ([validation.md](validation.md)).
24
+ - Function signatures: 1–2 positional arguments; at 3+, one destructured object.
25
+
26
+ ## Immutability & expressions
27
+
28
+ - Array methods before loops — `map`, `filter`, `find`, `some`, `flatMap` name what they do:
29
+
30
+ ```ts
31
+ const activeNames = users.filter(u => u.isActive).map(u => u.name);
32
+ ```
33
+
34
+ - Produce new values: spreads and `structuredClone` over in-place edits, `toSorted`/`toReversed` over `sort`/`reverse` (which
35
+ mutate their receiver).
36
+ - Allowed exceptions: a genuine early exit (`for` + `break`), a measured hot loop.
37
+ - Keep chains flat: 3–4 steps read well; longer wants named intermediates, and a `reduce` doing four things wants to be a
38
+ loop.
39
+
40
+ ## Naming
41
+
42
+ - Say what it is, not what it's made of: `useUserQuery`, not `useUserHook`; `retryDelay`, not `num`.
43
+ - Booleans read as assertions: `isOpen`, `hasAccess`, `canSubmit`.
44
+ - Handlers: `onX` as props, `handleX` as implementations.
45
+ - Match the vocabulary the product and the API already use — no synonyms for terms the backend named.
46
+ - Keep the object a hook or query returns as a namespace instead of destructuring it, and drop the `Query`/`Mutation` suffix
47
+ from the local binding — the binding reads as the domain name, and every field access shows where it came from:
48
+
49
+ ```tsx
50
+ const search = Route.useSearch(); // not const { page, filter } = ...
51
+ const user = useSuspenseQuery(context.api.user.getDetails()); // not const { data } = ...
52
+ const updateUser = useMutation(context.api.user.update());
53
+
54
+ search.page;
55
+ user.data.name;
56
+ updateUser.mutate(values);
57
+ updateUser.isPending;
58
+ ```
59
+
60
+ - Exception: a `useEffect` dependency array needs a stable primitive, since deps compare by identity — destructure the
61
+ primitive out first rather than listing the object or a deep path into it:
62
+
63
+ ```tsx
64
+ const user = useSuspenseQuery(context.api.auth.getUser());
65
+ const { name } = user.data; // destructure for the dep array
66
+
67
+ useEffect(
68
+ function syncDocumentTitle() {
69
+ document.title = name;
70
+ },
71
+ [name]
72
+ );
73
+ ```
@@ -0,0 +1,62 @@
1
+ # Validation
2
+
3
+ Validate where data crosses from untrusted representation into frontend-owned values; the schema is the single source of
4
+ truth for runtime check and TypeScript type.
5
+
6
+ ## Boundaries
7
+
8
+ - Validate once, at the edge: network responses on entry; URL path and search params before business logic; persisted browser
9
+ data on read; user input becoming a submitted domain or request value.
10
+ - Inside the boundary: parsed values only, no repeated defensive shape checks.
11
+ - Outgoing requests built from already parsed domain values are serialized, not re-validated.
12
+ - A typed client's handwritten generic is not validation — it only asserts a type onto an untrusted response.
13
+
14
+ ## Schema first, type derived
15
+
16
+ - Zod schema, type via `z.infer`; never a handwritten type beside its schema.
17
+ - Same rule for request and response shapes the frontend owns or consumes at runtime.
18
+
19
+ ```ts
20
+ import { z } from "zod";
21
+
22
+ export const userSchema = z.object({
23
+ id: z.string(),
24
+ email: z.email()
25
+ });
26
+
27
+ export type User = z.infer<typeof userSchema>;
28
+ ```
29
+
30
+ ## TanStack Router search params
31
+
32
+ - `validateSearch` takes a Zod v4 schema directly (TanStack Router v1) — no adapter or parsing wrapper.
33
+ - `Route.useSearch()` infers its type from `validateSearch`.
34
+ - `.catch()`: malformed URL input falls back without interrupting navigation.
35
+ - `.default()`: only for missing values; malformed values still follow the route's validation error path.
36
+
37
+ ```tsx
38
+ import { createFileRoute } from "@tanstack/react-router";
39
+ import { z } from "zod";
40
+
41
+ const searchSchema = z.object({
42
+ page: z.number().int().positive().catch(1),
43
+ filter: z.string().catch("")
44
+ });
45
+
46
+ export const Route = createFileRoute("/users")({
47
+ validateSearch: searchSchema,
48
+ component: Users
49
+ });
50
+
51
+ function Users() {
52
+ const search = Route.useSearch(); // inferred: z.infer<typeof searchSchema>
53
+ return <UserList page={search.page} filter={search.filter} />;
54
+ }
55
+ ```
56
+
57
+ ## Failure handling
58
+
59
+ - `schema.parse`: invalid data is a contract failure on the normal error path (malformed backend response → route error
60
+ boundary).
61
+ - `schema.safeParse`: failure is expected and the caller handles the issues (submitted user input).
62
+ - Transforms and coercion live in the boundary schema — never scatter trimming, number conversion, or defaulting downstream.