@imfusion/web-ui 0.5.1-dev.7.ga571658e → 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.
- package/README.md +55 -33
- package/bin/install.js +428 -0
- package/bin/install.test.ts +329 -0
- package/dist/build/vite-css-module-names/index.d.ts +20 -0
- package/dist/build/vite-css-module-names.js +17 -0
- package/dist/chunk-DmhlhrBa.js +11 -0
- package/dist/code-Blo48PGr.js +136 -0
- package/dist/codegen/gen-icons.d.ts +24 -0
- package/dist/components/callout/callout.d.ts +1 -1
- package/dist/components/chip-link/chip-link.d.ts +1 -1
- package/dist/components/code/code.d.ts +10 -18
- package/dist/components/field/field.d.ts +104 -0
- package/dist/components/field/field.meta.d.ts +2 -0
- package/dist/components/field/index.d.ts +2 -0
- package/dist/components/fieldset/fieldset.d.ts +29 -0
- package/dist/components/fieldset/fieldset.meta.d.ts +2 -0
- package/dist/components/fieldset/index.d.ts +2 -0
- package/dist/components/icon/icon.d.ts +16 -0
- package/dist/components/icon/icon.meta.d.ts +2 -0
- package/dist/components/icon/index.d.ts +4 -0
- package/dist/components/icon/types.d.ts +2 -0
- package/dist/components/input/input.d.ts +3 -1
- package/dist/components/typo/typo.d.ts +2 -2
- package/dist/docgen/component-sources.d.ts +7 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-resize-observer.d.ts +2 -0
- package/dist/icons/catalog.gen.d.ts +8357 -0
- package/dist/icons/icon-config-provider.d.ts +8 -0
- package/dist/icons/icon-context.d.ts +4 -0
- package/dist/icons/icons.gen.d.ts +1672 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons-wBmF0U2x.js +78 -0
- package/dist/icons.js +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1793 -12210
- package/dist/integrations/code-highlight/code-highlight.d.ts +8 -6
- package/dist/integrations/code-highlight/highlighter.d.ts +32 -3
- package/dist/integrations/code-highlight/language-patterns.d.ts +7 -0
- package/dist/integrations/code-highlight/languages/cmake.d.ts +1 -0
- package/dist/integrations/code-highlight/languages/cpp.d.ts +1 -0
- package/dist/integrations/code-highlight/languages/python.d.ts +1 -0
- package/dist/integrations/code-highlight.js +198 -59
- package/dist/integrations/image-display-options.js +70 -69
- package/dist/llms/gen-tokens.d.ts +7 -0
- package/dist/meta-CySnRuVp.js +21 -0
- package/dist/provider/web-ui-provider.d.ts +4 -1
- package/dist/style.css +1 -1
- package/dist/tabs-CMKvMF4E.js +369 -0
- package/package.json +48 -26
- package/src/docgen/doc.gen.json +381 -38
- package/src/llms/icon-catalog.gen.json +11203 -0
- package/src/llms/install-templates/AGENTS.md +34 -0
- package/src/llms/install-templates/codex-hooks.json +44 -0
- package/src/llms/install-templates/hooks/baseline-staleness.sh +17 -0
- package/src/llms/install-templates/hooks/session-start.sh +5 -0
- package/src/llms/install-templates/hooks/stop.sh +18 -0
- package/src/llms/install-templates/hooks/subagent-start.sh +5 -0
- package/src/llms/install-templates/hooks/user-prompt-submit.sh +5 -0
- package/src/llms/install-templates/settings.json +45 -0
- package/src/llms/llms.gen.txt +17 -0
- package/src/llms/skills/imf-web-ui/SKILL.md +13 -12
- package/src/llms/skills/imf-web-ui-audit/SKILL.md +119 -0
- package/src/llms/skills/imf-web-ui-components/SKILL.md +56 -3
- package/src/llms/skills/imf-web-ui-conventions/SKILL.md +57 -0
- package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +141 -0
- package/src/llms/skills/imf-web-ui-conventions/templates/REPORT.md +45 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +82 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +27 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +65 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +50 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/components.md +101 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/data.md +221 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +40 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/git.md +34 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +35 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +26 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +53 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +44 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/react.md +109 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +88 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +25 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +7 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +116 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +73 -0
- package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +62 -0
- package/src/llms/skills/imf-web-ui-setup/SKILL.md +67 -37
- package/src/llms/skills/imf-web-ui-update/SKILL.md +157 -0
- package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
- package/src/llms/skills/imf-web-ui-ux/references/forms.md +2 -2
- package/src/llms/tokens.gen.json +887 -0
- package/bin/install-skill.js +0 -203
- package/dist/code-qBbqAHK-.js +0 -190
- package/dist/meta-B8C51eyL.js +0 -74
- package/dist/tabs-DqBFSqq6.js +0 -3789
- package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -93
- package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +0 -133
- package/src/llms/skills/imf-web-ui-frontend-patterns/references/react-patterns.md +0 -94
- package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +0 -201
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Frontend Report
|
|
2
|
+
|
|
3
|
+
Mode: `<setup|audit>` Scope: `<all applicable topics or the resolved topic>`
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+
This template is the report contract. Keep every H2 below once and in this order.
|
|
7
|
+
Use `None.` for an empty section.
|
|
8
|
+
|
|
9
|
+
Broken, Missing, Deviations, and Unverified entries use:
|
|
10
|
+
- **[high|medium|low] reference-or-agent-tooling — Short title**
|
|
11
|
+
- Evidence: `path:line`
|
|
12
|
+
- Impact: concrete consequence
|
|
13
|
+
- Next action: smallest selectable follow-up
|
|
14
|
+
|
|
15
|
+
Present entries name the topic and evidence path. Deviations are selectable follow-up work, not defects. Optional tools are not
|
|
16
|
+
missing findings.
|
|
17
|
+
-->
|
|
18
|
+
|
|
19
|
+
## Verdict
|
|
20
|
+
|
|
21
|
+
<One short assessment of the current frontend setup.>
|
|
22
|
+
|
|
23
|
+
## Broken
|
|
24
|
+
|
|
25
|
+
None.
|
|
26
|
+
|
|
27
|
+
## Missing
|
|
28
|
+
|
|
29
|
+
None.
|
|
30
|
+
|
|
31
|
+
## Deviations
|
|
32
|
+
|
|
33
|
+
None.
|
|
34
|
+
|
|
35
|
+
## Present
|
|
36
|
+
|
|
37
|
+
None.
|
|
38
|
+
|
|
39
|
+
## Unverified
|
|
40
|
+
|
|
41
|
+
None.
|
|
42
|
+
|
|
43
|
+
## Reviewer notes
|
|
44
|
+
|
|
45
|
+
<!-- Human-owned. Preserve everything under this heading verbatim when refreshing the report. -->
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Agent tooling
|
|
2
|
+
|
|
3
|
+
How a consumer repo carries the `@imfusion/web-ui` agent tooling; `npx web-ui-install` does the mechanics, this topic carries
|
|
4
|
+
the judgment.
|
|
5
|
+
|
|
6
|
+
## The skill bundle
|
|
7
|
+
|
|
8
|
+
- `npx web-ui-install` installs or refreshes the vendored `imf-web-ui-*` skills, refreshes the
|
|
9
|
+
`<!-- imf-web-ui:begin/end -->` fence in an existing `AGENTS.md`, and removes skills dropped from the bundle.
|
|
10
|
+
- Target: remembered first-run choice — `.claude/skills/`, vendor-neutral `.agents/skills/`, or both (`.agents/` real copy,
|
|
11
|
+
`.claude/` symlink); `--reconfigure` re-opens it, `--target claude|agents` selects non-interactively.
|
|
12
|
+
- Staleness: per-skill `.imf-web-ui-skill-version.json`; a marker older than the installed package means re-run the binary —
|
|
13
|
+
never hand-diff or hand-edit vendored skill contents.
|
|
14
|
+
|
|
15
|
+
## The hooks
|
|
16
|
+
|
|
17
|
+
- `npx web-ui-install --hooks` adds three injection hooks and a turn-end gate.
|
|
18
|
+
- Scripts in `.agents/hooks/imf-web-ui/` are installer-owned: refreshed wholesale each run, retired scripts pruned with their
|
|
19
|
+
registrations.
|
|
20
|
+
- Registrations merge idempotently into `.claude/settings.json` (Claude Code) and `.codex/hooks.json` (Codex), never touching
|
|
21
|
+
entries the installer didn't write.
|
|
22
|
+
|
|
23
|
+
- **SessionStart** — once per session: points the agent at the `imf-web-ui` skills router.
|
|
24
|
+
- **SubagentStart** — the same line, byte for byte, for each spawned subagent; subagents don't reliably inherit the parent
|
|
25
|
+
session's context.
|
|
26
|
+
- **UserPromptSubmit** — one line per prompt naming the companion skills to consult.
|
|
27
|
+
- **Stop** — the verify gate: when the turn edited source files, it blocks the agent from finishing once, with the
|
|
28
|
+
instruction to run the project's verification and fix what it reports. Re-entry is detected from the payload, so the gate
|
|
29
|
+
can never loop; a turn that edited nothing passes untouched.
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
SS(["SessionStart<br/>once per session"]) --> ss["session-start.sh"]
|
|
34
|
+
SA(["SubagentStart<br/>per spawned subagent"]) --> sa["subagent-start.sh"]
|
|
35
|
+
UP(["UserPromptSubmit<br/>every prompt"]) --> up["user-prompt-submit.sh"]
|
|
36
|
+
ST(["Stop<br/>turn ends after edits"]) --> st["stop.sh"]
|
|
37
|
+
ss --> router["injects the router pointer:<br/>start at imf-web-ui"]
|
|
38
|
+
sa --> router
|
|
39
|
+
up --> hints["injects the per-task hints:<br/>components · conventions · ux"]
|
|
40
|
+
st --> gate["blocks once:<br/>run verification first"]
|
|
41
|
+
router --> agent["agent routes to the right<br/>companion skill"]
|
|
42
|
+
hints --> agent
|
|
43
|
+
gate --> agent2["agent verifies,<br/>then finishes"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Each injection hook echoes one fixed line and exits 0; the router does the task-sorting a shell script can't — why a hint
|
|
47
|
+
on every prompt isn't noise.
|
|
48
|
+
- `baseline-staleness.sh` ships alongside but is not an agent hook; the repo's pre-commit calls it ([git.md](./git.md),
|
|
49
|
+
staleness at commit time).
|
|
50
|
+
- Never edit an installed script — the next install overwrites it; adapt in the repo's own hooks.
|
|
51
|
+
- Registration entries are yours: the installer matches by script name and keeps edited commands across re-runs. Monorepo:
|
|
52
|
+
the Codex commands anchor at `$(git rev-parse --show-toplevel)` — adjust their paths once after installing when the
|
|
53
|
+
frontend isn't the git toplevel.
|
|
54
|
+
- **Read the registration files before installing**; per event: nothing registered → install as shipped; already covered by
|
|
55
|
+
the repo (its own session reminder, say) → don't stack a second hook — fold the missing line into the repo's script, or
|
|
56
|
+
adapt the shipped one and register that; surface it and let the human pick.
|
|
57
|
+
|
|
58
|
+
## Codex trust
|
|
59
|
+
|
|
60
|
+
- Codex runs a project's hooks only when two trust gates hold: the project itself is trusted, and each hook script has been
|
|
61
|
+
trusted via the `/hooks` review, which keys trust to the script's hash.
|
|
62
|
+
- Until then hooks are skipped, and skipped silently — from outside, a skipped hook and a hook that ran and said nothing look
|
|
63
|
+
identical.
|
|
64
|
+
- After installing or updating hooks, tell the user to trust the project and review `/hooks` in Codex, and again after any
|
|
65
|
+
hook script changes.
|
|
66
|
+
- When a hint doesn't show up, check trust before debugging the script.
|
|
67
|
+
- Claude Code has no trust gate for project hooks.
|
|
68
|
+
|
|
69
|
+
## Dependency-shipped skills
|
|
70
|
+
|
|
71
|
+
- npm packages can ship Agent Skills of their own; TanStack does — [tooling.md](./tooling.md) covers TanStack Intent and its
|
|
72
|
+
allowlist.
|
|
73
|
+
|
|
74
|
+
## Hook docs
|
|
75
|
+
|
|
76
|
+
Both hosts move fast; read the current references before adapting or adding a hook — payloads and stdout rules differ per
|
|
77
|
+
host and per event.
|
|
78
|
+
|
|
79
|
+
- [Claude Code: hooks reference](https://code.claude.com/docs/en/hooks) — event list, JSON input and output, exit codes,
|
|
80
|
+
`disableAllHooks`
|
|
81
|
+
- [Codex: hooks](https://learn.chatgpt.com/docs/hooks) — events, the `hooks.json` schema, trust, `/hooks`
|
|
82
|
+
- [Codex: config reference](https://learn.chatgpt.com/docs/config-file/config-reference) — `[features]` and `[hooks.state]`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Assets
|
|
2
|
+
|
|
3
|
+
## Importing
|
|
4
|
+
|
|
5
|
+
- Import everything from `src/assets/` so the bundler fingerprints and bundles it. Never reference an image by public-path
|
|
6
|
+
string.
|
|
7
|
+
|
|
8
|
+
## Photographs — WebP
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
magick source.png -resize 2000x -quality 80 -define webp:method=6 src/assets/name.webp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- Quality 80 — visually lossless on photos, routinely an order of magnitude smaller.
|
|
15
|
+
- `method=6` — densest encoding; a one-off cost at conversion time, so take the smaller file.
|
|
16
|
+
- Long edge ≤ 2000px — nothing on the market resolves more in a content image.
|
|
17
|
+
- No `<picture>` fallback — WebP is supported everywhere since 2020.
|
|
18
|
+
|
|
19
|
+
## Other formats
|
|
20
|
+
|
|
21
|
+
- Alpha, or pixels that must stay exact → PNG, optimized with `oxipng` or `pngquant`.
|
|
22
|
+
- Icons, logos, line art → inline SVG component, so it inherits `currentColor` and follows the theme.
|
|
23
|
+
|
|
24
|
+
## Scope
|
|
25
|
+
|
|
26
|
+
- Rules apply to assets as they're added or touched. Existing assets in another format are not findings to sweep — convert
|
|
27
|
+
opportunistically.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
One route-protection shape for every frontend; provider, session mechanism, endpoint paths, and login/logout transport stay
|
|
4
|
+
project-owned — no provider is prescribed or named.
|
|
5
|
+
|
|
6
|
+
## Starter shape
|
|
7
|
+
|
|
8
|
+
- Authentication sits at the route-group boundary, before child routes render.
|
|
9
|
+
- `_public/` and `_app/`: pathless TanStack Router groups — guards and layouts without `public`/`app` in the URL.
|
|
10
|
+
- The authenticated group keeps this shape even without an `AppShell`.
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
src/
|
|
14
|
+
api/auth/ # getUser query options, keys.ts, types.ts, index.ts barrel
|
|
15
|
+
lib/auth/
|
|
16
|
+
login-url.ts # pure login navigation helper; not an API topic file
|
|
17
|
+
login-url.test.ts # focused helper tests
|
|
18
|
+
routes/
|
|
19
|
+
__root.tsx # global Outlet and error/not-found boundaries; no auth guard
|
|
20
|
+
_public/route.tsx # anonymous route group
|
|
21
|
+
_app/route.tsx # authenticated route group
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## One current-user query
|
|
25
|
+
|
|
26
|
+
- One identity source of truth: a server-backed current-user query; never copy session credentials or access tokens into
|
|
27
|
+
React state.
|
|
28
|
+
- The auth topic follows the default [data.md](data.md) shape: `getUser` in `api/auth/queries.ts`, key leading with the
|
|
29
|
+
`auth` topic, schema in `types.ts`.
|
|
30
|
+
- `lib/auth/login-url.ts`: project-owned navigation helper, not an options factory; tests colocated.
|
|
31
|
+
- Reached as `context.api.auth.getUser()`; returns query options, never a hook or a user value — callers pick
|
|
32
|
+
`ensureQueryData` or a query hook.
|
|
33
|
+
- Never infer authentication from local storage, a decoded token, a route flag, or permission-gated chrome — stale or
|
|
34
|
+
forgeable; the server response and its schema define the current user.
|
|
35
|
+
|
|
36
|
+
## The two route groups
|
|
37
|
+
|
|
38
|
+
Both resolve the current-user query in `beforeLoad` when they need the answer; only the meaning of a 401 differs:
|
|
39
|
+
|
|
40
|
+
| Group | 401 means | Result |
|
|
41
|
+
| ---------- | ----------------- | ------------------------------------------- |
|
|
42
|
+
| `_app/` | not logged in | navigate to the server-owned login endpoint |
|
|
43
|
+
| `_public/` | anonymous visitor | continue with `user: null` |
|
|
44
|
+
|
|
45
|
+
- The `_app/route.tsx` guard awaits the query before children render, converts only an authentication 401 into login
|
|
46
|
+
navigation, rethrows router redirects, and lets 5xx, connection failures, and schema mismatches reach the error boundary.
|
|
47
|
+
- A public landing route may redirect an authenticated user into `_app/`.
|
|
48
|
+
- The root route stays neutral: global `Outlet` and error/not-found boundaries, no public/authenticated decision.
|
|
49
|
+
|
|
50
|
+
## Login and logout
|
|
51
|
+
|
|
52
|
+
- Follow the project's documented transport. Server-owned flow: login is a browser navigation, not a Query fetch; logout is
|
|
53
|
+
the server's documented state-changing action, not an ad-hoc client request.
|
|
54
|
+
- Preserve a validated same-origin return path when the server supports returning to the interrupted route.
|
|
55
|
+
- Never hard-code an endpoint shape or provider into shared frontend conventions.
|
|
56
|
+
- No intermediate login route when the server owns the flow.
|
|
57
|
+
- A failed API request is an auth failure only when its typed error is specifically a 401.
|
|
58
|
+
|
|
59
|
+
## App shell
|
|
60
|
+
|
|
61
|
+
- `AppShell` is authenticated chrome, not the authentication mechanism.
|
|
62
|
+
- Greenfield default: propose a minimal shell — ImFusion logo, route navigation, stable session action area around the
|
|
63
|
+
`_app/` outlet.
|
|
64
|
+
- An explicit no-persistent-navigation decision may omit the shell; the `_app/` guard stays.
|
|
65
|
+
- An established project keeps its working choice; public pages may use a small branded header.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Class names in components
|
|
2
|
+
|
|
3
|
+
How a component turns props into a `className` string. What the build does with the result: [tooling.md](tooling.md); what
|
|
4
|
+
goes in the stylesheet: [styling.md](styling.md).
|
|
5
|
+
|
|
6
|
+
## CVA is the only tool
|
|
7
|
+
|
|
8
|
+
- `class-variance-authority` maps variant props to CSS Module classes. No `clsx`, no `tailwind-merge`, no local `cn()`
|
|
9
|
+
helper.
|
|
10
|
+
- `cx` is CVA's own concatenator, re-exported by `@imfusion/web-ui` — import it from the library alongside the components.
|
|
11
|
+
- Base class is CVA's first argument; variant values are CSS Module references, never string literals. A boolean axis uses
|
|
12
|
+
`null` for its off-state.
|
|
13
|
+
- Defaults live in the props destructuring, not CVA's `defaultVariants`: react-docgen-typescript reads the destructuring, so
|
|
14
|
+
defaults declared in CVA don't reach the generated docs.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
18
|
+
appearance?: "outline" | "solid";
|
|
19
|
+
inline?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const chip = cva(classes.root, {
|
|
23
|
+
variants: {
|
|
24
|
+
appearance: { outline: classes.appearanceOutline, solid: classes.appearanceSolid },
|
|
25
|
+
inline: { false: null, true: classes.inline }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function Chip({ appearance = "outline", inline = false, className, ...props }: Props) {
|
|
30
|
+
return <span {...props} className={chip({ appearance, inline, className })} />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Merging `className`
|
|
35
|
+
|
|
36
|
+
- Every component that accepts `className` merges it; a deliberately closed surface omits the prop entirely rather than
|
|
37
|
+
accepting and ignoring it.
|
|
38
|
+
- The incoming `className` goes into CVA's `className` slot, which appends it last so a caller's class always wins. With no
|
|
39
|
+
variants to map, `cx(classes.inline, className)` does the same job.
|
|
40
|
+
- Resolve a function-form `className` before merging — components built on a library that passes render state
|
|
41
|
+
(`className={state => …}`) receive either shape:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
className={state => cx(classes.root, typeof className === "function" ? className(state) : className)}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Shared CVA modules
|
|
48
|
+
|
|
49
|
+
- Two components sharing one visual share one CVA module, a `{name}.cva.ts` next to them, so their variant axes can't drift
|
|
50
|
+
apart.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
How a component lives on disk and how dumb it stays. Roles, state, effects: [react.md](react.md); styling:
|
|
4
|
+
[styling.md](styling.md).
|
|
5
|
+
|
|
6
|
+
## As dumb as possible
|
|
7
|
+
|
|
8
|
+
- Props-fed, never self-fetching: no query hook, no store access, no route awareness — render what's given, report events
|
|
9
|
+
upward.
|
|
10
|
+
- Presentational logic (formatting a label, deriving a display state) is fine; owning data is not.
|
|
11
|
+
- Local UI state (`isOpen`, a draft value) is allowed; anything whose truth lives elsewhere is not ([react.md](react.md)).
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// components/user-card/user-card.tsx
|
|
15
|
+
interface Props {
|
|
16
|
+
name: string;
|
|
17
|
+
onEdit: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function UserCard({ name, onEdit }: Props) {
|
|
21
|
+
return (
|
|
22
|
+
<Card.Root>
|
|
23
|
+
<Typo>{name}</Typo>
|
|
24
|
+
<Button onClick={onEdit}>Edit</Button>
|
|
25
|
+
</Card.Root>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Grouping
|
|
31
|
+
|
|
32
|
+
- `components/` groups by kind (the component roles in [react.md](react.md)) — a layout component under `layouts/`, not
|
|
33
|
+
beside a domain widget.
|
|
34
|
+
- Flat is fine while there are few; let grouping follow what the project has, don't impose it up front.
|
|
35
|
+
|
|
36
|
+
## One file or a folder
|
|
37
|
+
|
|
38
|
+
- More than one file (sub-components, styles, tests) → a folder with an `index.ts` that only re-exports; single-file
|
|
39
|
+
components stay single files, directly in their group.
|
|
40
|
+
- Imports read `#/components/data-table`, so the inside can be restructured without touching call sites.
|
|
41
|
+
|
|
42
|
+
The full anatomy of a folder component:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
components/data-table/
|
|
46
|
+
data-table.tsx # the component: props type + export, CVA variants when it has axes
|
|
47
|
+
data-table-row.tsx # sub-component, private unless index.ts re-exports it
|
|
48
|
+
data-table.module.css # co-located stylesheet (styling.md)
|
|
49
|
+
data-table.test.tsx # only when the component carries decisions worth testing (testing.md)
|
|
50
|
+
index.ts # re-exports only
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Most components need only a subset — start with the `.tsx`, add files as they earn their place.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// data-table.tsx — CVA even with one variant axis, so future axes slot in (class-names.md)
|
|
57
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
58
|
+
import classes from "./data-table.module.css";
|
|
59
|
+
|
|
60
|
+
const dataTable = cva(classes.root, {
|
|
61
|
+
variants: { density: { comfortable: classes.densityComfortable, compact: classes.densityCompact } }
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Intersection, not `interface extends` — VariantProps is an object type. Defaults live in the destructuring.
|
|
65
|
+
type Props = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof dataTable> & { rows: TableRow[] };
|
|
66
|
+
|
|
67
|
+
export function DataTable({ className, density = "comfortable", rows, ...props }: Props) {
|
|
68
|
+
return (
|
|
69
|
+
<div {...props} className={dataTable({ density, className })}>
|
|
70
|
+
{/* … */}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
/* data-table.module.css — native nesting + library tokens, no preprocessor, no @layer (styling.md) */
|
|
78
|
+
.root {
|
|
79
|
+
border-radius: var(--imf-ui-border-radius-2);
|
|
80
|
+
font-size: var(--imf-ui-font-size-1);
|
|
81
|
+
|
|
82
|
+
&:focus-visible {
|
|
83
|
+
/* states nest under the root */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.densityCompact {
|
|
88
|
+
/* one class per CVA variant value */
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// index.ts — re-exports only; callers import #/components/data-table
|
|
94
|
+
export { DataTable } from "./data-table";
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Colocation
|
|
98
|
+
|
|
99
|
+
- Stylesheet: `<component>.module.css` next to the component; only dumb components have one — a container that wants CSS is
|
|
100
|
+
asking for a layout component instead ([react.md](react.md)).
|
|
101
|
+
- Tests and local types sit next to their subject: a file hunted for in a parallel tree gets edited less carefully.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Data
|
|
2
|
+
|
|
3
|
+
The in-house pattern **for TanStack Query + Router**, the default stack ([tooling.md](tooling.md)); other data layers keep
|
|
4
|
+
the boundary principles — validate at the edge, errors as values — not this file layout.
|
|
5
|
+
|
|
6
|
+
## The shape
|
|
7
|
+
|
|
8
|
+
- One folder per API topic: options factories, key factory, Zod schemas together, so a query key is never spelled out at a
|
|
9
|
+
call site.
|
|
10
|
+
- Name a file for what it holds, never for the topic it sits in — the folder already says that: `keys.ts`, not `<topic>.ts`
|
|
11
|
+
or `query-key.ts` (it holds mutation keys too).
|
|
12
|
+
- A file appears only when the topic needs it: a topic with only queries has no `mutations.ts`, and vice versa.
|
|
13
|
+
- A GraphQL (or similarly document-based) transport may split `queries.ts`/`mutations.ts` further into
|
|
14
|
+
`queryFns.ts`/`mutationFns.ts`; that's an elaboration of the same options-vs-mutations split, not its origin.
|
|
15
|
+
- Transport code (base client, error normalisation): `http/` and nowhere else.
|
|
16
|
+
- Topic helpers: `lib/`, framework-free, tested beside themselves (`build-query-string.ts` + its test) — never inside the
|
|
17
|
+
topic folder.
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
src/
|
|
21
|
+
api/
|
|
22
|
+
topics.ts # one topic registry, reused by keys and namespaces
|
|
23
|
+
index.ts # topic barrels composed as api.<topic>
|
|
24
|
+
<topic>/
|
|
25
|
+
index.ts # barrel: named re-exports of what other modules call
|
|
26
|
+
keys.ts # createApiKeys — query and mutation keys
|
|
27
|
+
queries.ts # query options factories — present when the topic has queries
|
|
28
|
+
mutations.ts # mutation options factories — present when the topic has mutations
|
|
29
|
+
types.ts # Zod schemas + z.infer types
|
|
30
|
+
types.test.ts # schema cases, when the schema encodes product behaviour
|
|
31
|
+
http/ # transport: client, error normalisation — the only transport-aware place
|
|
32
|
+
lib/ # framework-free helpers shared by API topics and other callers
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The network boundary validates
|
|
36
|
+
|
|
37
|
+
- `http/` returns `unknown`; the topic schema parses the response in its query or mutation function and sources the
|
|
38
|
+
TypeScript type. Full rules: [validation.md](validation.md).
|
|
39
|
+
|
|
40
|
+
## Errors are values
|
|
41
|
+
|
|
42
|
+
- A failed request becomes an `ApiError` with a machine-readable code — a value crossing the boundary, not an ad-hoc caught
|
|
43
|
+
exception; the frontend maps codes to what the user reads.
|
|
44
|
+
|
|
45
|
+
## Topic registry and key factory
|
|
46
|
+
|
|
47
|
+
- One registry of topic names, reused for the barrel's namespaces and the first segment of every query and mutation key.
|
|
48
|
+
- `createApiKeys(topic, { queries, mutations })` types every key `readonly [Topic, ...unknown[]]` and returns `keys.topic`:
|
|
49
|
+
cross-topic invalidation names the topic, never reconstructs a key prefix.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// api/topics.ts — one list of topic names, ApiTopic derived from it
|
|
53
|
+
export const API_TOPICS = { AUTH: "auth", USER: "user", ORDERS: "orders" } as const;
|
|
54
|
+
export type ApiTopic = (typeof API_TOPICS)[keyof typeof API_TOPICS];
|
|
55
|
+
export type QueryKey<Topic extends ApiTopic> = readonly [Topic, ...ReadonlyArray<unknown>] | readonly [Topic];
|
|
56
|
+
export type QueryKeyFactory<Topic extends ApiTopic> = (...args: never[]) => QueryKey<Topic>;
|
|
57
|
+
|
|
58
|
+
export function createApiKeys<
|
|
59
|
+
Topic extends ApiTopic,
|
|
60
|
+
QueryKeys extends Record<string, QueryKey<Topic> | QueryKeyFactory<Topic>>,
|
|
61
|
+
MutationKeys extends Record<string, QueryKey<Topic>>
|
|
62
|
+
>(topic: Topic, keys: { queries: QueryKeys; mutations: MutationKeys }) {
|
|
63
|
+
return { topic: [topic] as const, queries: keys.queries, mutations: keys.mutations };
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// api/user/keys.ts — createApiKeys enforces topic-first
|
|
69
|
+
import { API_TOPICS, createApiKeys } from "../topics";
|
|
70
|
+
|
|
71
|
+
export const keys = createApiKeys(API_TOPICS.USER, {
|
|
72
|
+
queries: { details: ["user", "details"], byId: (id: string) => ["user", "detail", id] },
|
|
73
|
+
mutations: { update: ["user", "update"] }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// api/user/index.ts — the topic barrel
|
|
77
|
+
export * from "./keys";
|
|
78
|
+
export * from "./queries";
|
|
79
|
+
export * from "./mutations";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// api/index.ts — namespace imports keyed by the topic registry
|
|
84
|
+
import { API_TOPICS } from "./topics";
|
|
85
|
+
import * as auth from "./auth";
|
|
86
|
+
import * as orders from "./orders";
|
|
87
|
+
import * as user from "./user";
|
|
88
|
+
|
|
89
|
+
export const api = {
|
|
90
|
+
[API_TOPICS.AUTH]: auth,
|
|
91
|
+
[API_TOPICS.USER]: user,
|
|
92
|
+
[API_TOPICS.ORDERS]: orders
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
export type API = typeof api;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Options factories and naming
|
|
99
|
+
|
|
100
|
+
- Topic functions return **options** — never a hook, never data; the caller picks `ensureQueryData` or `useSuspenseQuery`.
|
|
101
|
+
- No topic prefix, no `Options` suffix: `context.api.<topic>.<fn>()` carries both.
|
|
102
|
+
- Queries `get*` (`getAll`, `getById`, `getBySlug`, `getDetails`, `getActive`, `getFiltered`); mutations bare verbs
|
|
103
|
+
(`update`, `login`, `addItem`, `deleteAddress`, `setShippingMethod`).
|
|
104
|
+
- Same suffix-free naming applies to the local binding a query or mutation resolves to —
|
|
105
|
+
[typescript.md](typescript.md#naming).
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// api/user/queries.ts — returns options; keys come from the factory
|
|
109
|
+
import { queryOptions, type QueryOptions } from "@tanstack/react-query";
|
|
110
|
+
import { bffClient } from "../http/bff-client";
|
|
111
|
+
import { userSchema, type User } from "./types";
|
|
112
|
+
import { keys } from "./keys";
|
|
113
|
+
|
|
114
|
+
export function getDetails(args: { options?: Omit<QueryOptions<User>, "queryKey" | "queryFn"> } = {}) {
|
|
115
|
+
return queryOptions({
|
|
116
|
+
...args.options,
|
|
117
|
+
queryKey: keys.queries.details,
|
|
118
|
+
queryFn: async () => userSchema.parse(await bffClient("/me"))
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// api/user/mutations.ts — overrides merge over the base, mutationKey leads with the topic
|
|
125
|
+
import type { MutationOptions } from "@tanstack/react-query";
|
|
126
|
+
import { bffClient } from "../http/bff-client";
|
|
127
|
+
import { userSchema, type User, type UserPatch } from "./types";
|
|
128
|
+
import { keys } from "./keys";
|
|
129
|
+
import type { ApiError } from "../http/api-error";
|
|
130
|
+
|
|
131
|
+
export function update(options?: Omit<MutationOptions<User, ApiError, UserPatch>, "mutationFn">) {
|
|
132
|
+
return {
|
|
133
|
+
mutationKey: keys.mutations.update,
|
|
134
|
+
mutationFn: async (patch: UserPatch) => userSchema.parse(await bffClient("/me", { method: "PATCH", body: patch })),
|
|
135
|
+
...options
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Router context access
|
|
141
|
+
|
|
142
|
+
- Loaders call `context.api.<topic>.<fn>()`; components use `Route.useRouteContext()`.
|
|
143
|
+
- `createRouter` seeds a placeholder `queryClient`; `router.update()` fills it once the client exists — required because the
|
|
144
|
+
`MutationCache` closes over both `queryClient` and `router`.
|
|
145
|
+
- Direct imports stay for plain utilities and tests outside the router tree.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// routes/__root.tsx
|
|
149
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
150
|
+
import type { API } from "../api";
|
|
151
|
+
|
|
152
|
+
interface RouterContext {
|
|
153
|
+
api: API;
|
|
154
|
+
queryClient: QueryClient;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// router.tsx
|
|
160
|
+
import { createRouter } from "@tanstack/react-router";
|
|
161
|
+
import { api } from "./api";
|
|
162
|
+
import { createQueryClient } from "./query-client";
|
|
163
|
+
|
|
164
|
+
const queryClient = createQueryClient();
|
|
165
|
+
const router = createRouter({ routeTree, context: { api, queryClient } });
|
|
166
|
+
router.update({ context: { api, queryClient } });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// routes/user.tsx
|
|
171
|
+
export const Route = createFileRoute("/user")({
|
|
172
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(context.api.user.getDetails()),
|
|
173
|
+
component: UserPage
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function UserPage() {
|
|
177
|
+
const context = Route.useRouteContext();
|
|
178
|
+
const user = useSuspenseQuery(context.api.user.getDetails());
|
|
179
|
+
const updateUser = useMutation(
|
|
180
|
+
context.api.user.update({
|
|
181
|
+
onSuccess: () => context.queryClient.invalidateQueries({ queryKey: context.api.orders.keys.topic })
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
return <input defaultValue={user.data.name} onBlur={e => updateUser.mutate({ name: e.target.value })} />;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Automatic invalidation
|
|
189
|
+
|
|
190
|
+
- No hand invalidation at call sites: a `MutationCache` `onSuccess` invalidates `mutationKey[0]` centrally, on by default via
|
|
191
|
+
`meta.autoInvalidate` — hence every mutation key leads with the topic it dirties.
|
|
192
|
+
- Cross-topic invalidation: per-call-site `onSuccess` override naming `keys.topic` (see `routes/user.tsx`).
|
|
193
|
+
- Opt out with `meta: { autoInvalidate: false }` when a topic-wide refetch is wrong (huge list, targeted optimistic update);
|
|
194
|
+
invalidate precisely via the key factory.
|
|
195
|
+
- Background: [query invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation),
|
|
196
|
+
[automatic invalidation after mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations).
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// query-client.ts
|
|
200
|
+
export function createQueryClient() {
|
|
201
|
+
const queryClient = new QueryClient({
|
|
202
|
+
defaultOptions: {
|
|
203
|
+
mutations: { meta: { autoInvalidate: true } }
|
|
204
|
+
},
|
|
205
|
+
mutationCache: new MutationCache({
|
|
206
|
+
onSuccess: async (_data, _variables, _context, mutation) => {
|
|
207
|
+
if (!mutation.meta?.autoInvalidate) return;
|
|
208
|
+
const topic = mutation.options.mutationKey?.[0];
|
|
209
|
+
if (topic !== undefined) await queryClient.invalidateQueries({ queryKey: [topic] });
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
return queryClient;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Testing the data layer
|
|
218
|
+
|
|
219
|
+
- Stub the network at the `fetch` boundary and run the real query client: same parse and error path as production.
|
|
220
|
+
- Assert behaviour in schemas, clients, and query options — schema cases: [validation.md](validation.md), philosophy:
|
|
221
|
+
[testing.md](testing.md).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Documentation structure
|
|
2
|
+
|
|
3
|
+
What a repo documents, where, and how. `imf-web-ui-setup` scaffolds this shape from its `templates/` and audits it.
|
|
4
|
+
|
|
5
|
+
## The shape
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
<app>/
|
|
9
|
+
README.md # humans: what the app is, setup, scripts
|
|
10
|
+
AGENTS.md # agents: tooling and architecture pointers into docs/ — restates nothing
|
|
11
|
+
docs/
|
|
12
|
+
index.md # registers every doc with a one-line "covers" summary
|
|
13
|
+
<topic>.md # repo-unique content only
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Repo docs hold only what is unique to the repo
|
|
17
|
+
|
|
18
|
+
- Litmus, per sentence: would this be true in every ImFusion frontend? Then it's baseline — don't restate it; it's already
|
|
19
|
+
vendored in-repo under `.agents/skills/imf-web-ui-conventions/`, human-readable and versioned.
|
|
20
|
+
- Write deviations as **named deviations** — what the baseline prescribes, what this repo does instead, and why: a deviation
|
|
21
|
+
written as freestanding convention gets copied into the next repo as house style.
|
|
22
|
+
|
|
23
|
+
## How docs are written
|
|
24
|
+
|
|
25
|
+
- **Docs explain concepts; code is the source of truth for facts.** Capture the why — invariants, rationale, decisions. Never
|
|
26
|
+
restate a fact that lives in code (a script definition, a type shape, a config value); point at the file — a copied fact
|
|
27
|
+
rots the moment the code changes.
|
|
28
|
+
- **State what is — no decision residue.** Present-tense statements about the current state; never narrate the delta from a
|
|
29
|
+
past decision or refute alternatives nobody raised ("there is no X mode", "Y was dropped") — history belongs in commits and
|
|
30
|
+
tickets. A negation earns its place only as a guardrail or to preempt a wrong assumption a present reader would actually
|
|
31
|
+
arrive at.
|
|
32
|
+
- **Boy Scout rule.** Discovered rot (a stale pointer, a doc contradicting the code) is always your responsibility: fix it in
|
|
33
|
+
place if trivial, otherwise report it. Equal failure modes: stepping over rot, and cramming unrelated cleanup into an
|
|
34
|
+
unrelated change.
|
|
35
|
+
|
|
36
|
+
## Staleness at commit time
|
|
37
|
+
|
|
38
|
+
- Docs are checked when they can go stale: at the commit. A staged change that invalidates a doc (a renamed script, a moved
|
|
39
|
+
folder, a changed flow) updates it **in the same commit**, never a follow-up.
|
|
40
|
+
- Pre-commit carries the advisory staleness checks (vendored baseline, docs) — mechanics in [git.md](git.md).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Git
|
|
2
|
+
|
|
3
|
+
## git:config
|
|
4
|
+
|
|
5
|
+
- One script holds the repo's git configuration, run by hand once per clone (the README names it):
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
git config core.hooksPath .githooks && git config pull.rebase true && git config merge.ff only
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- Hooks live in a tracked directory; history strategy doesn't depend on personal git config.
|
|
12
|
+
- Check both silent failure modes — config **and** directory: `git:config` never run (hooks exist only on the machine that
|
|
13
|
+
configured by hand), and `core.hooksPath` pointing at a missing directory.
|
|
14
|
+
|
|
15
|
+
## Verify scopes
|
|
16
|
+
|
|
17
|
+
Two scopes, both blocking:
|
|
18
|
+
|
|
19
|
+
- **staged** — `verify:staged`, called by the pre-commit hook: lint, format, restage, plus the relevant project-wide checks,
|
|
20
|
+
including `verify:knip` when staged source or project config can change the reachability graph. Fast; a passing commit is
|
|
21
|
+
not CI green.
|
|
22
|
+
- **full** — `verify:full`: the build plus every `verify:*` check; what CI runs.
|
|
23
|
+
- One script owns each scope's step list; npm scripts and hooks only launch them.
|
|
24
|
+
- Name by depth, not by occasion: a `preflight` needs explaining and invites a near-identical sibling that drifts into
|
|
25
|
+
"passes locally, fails in CI".
|
|
26
|
+
|
|
27
|
+
## Staleness at commit time
|
|
28
|
+
|
|
29
|
+
Pre-commit also runs the advisory staleness checks — warn, never block:
|
|
30
|
+
|
|
31
|
+
- **Vendored baseline** — `.agents/hooks/imf-web-ui/baseline-staleness.sh` compares installed `imf-web-ui-*` skill markers
|
|
32
|
+
against the installed package version; the fix it names is `npx web-ui-install`.
|
|
33
|
+
- **Docs** — a staged change that invalidates a doc updates it in the same commit ([docs-structure.md](docs-structure.md)).
|
|
34
|
+
An agent commit workflow, where the repo has one, carries a staged docs audit.
|