@imfusion/web-ui 0.5.1-dev.3.g8e0a047a → 0.5.1-dev.32.gddf4b438
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 +148 -55
- package/bin/install.js +374 -0
- package/bin/install.test.ts +243 -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/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/index.d.ts +2 -0
- package/dist/index.js +4956 -4317
- package/dist/integrations/image-display-options.js +1 -1
- package/dist/style.css +1 -1
- package/dist/{tabs-DqBFSqq6.js → tabs-CVp_SgBl.js} +1 -1
- package/package.json +35 -24
- package/src/docgen/doc.gen.json +327 -0
- package/src/llms/llms.gen.txt +12 -0
- package/src/llms/skills/imf-web-ui/SKILL.md +15 -11
- package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +82 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/baseline-staleness.sh +17 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/post-tool-use.sh +33 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/session-start.sh +4 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/user-prompt-submit.sh +4 -0
- package/src/llms/skills/imf-web-ui-agent-setup/templates/settings.json +37 -0
- package/src/llms/skills/imf-web-ui-components/SKILL.md +2 -1
- package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +46 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/assets.md +23 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/class-names.md +42 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/components.md +63 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/data.md +201 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/docs-structure.md +44 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/git.md +33 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/library-boundary.md +37 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/npm-project.md +57 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/project-structure.md +21 -0
- package/src/llms/skills/{imf-web-ui-frontend-patterns/references/react-patterns.md → imf-web-ui-frontend-conventions/references/react.md} +28 -12
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/stack.md +39 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/styling.md +91 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/testing.md +18 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/tooling.md +76 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/typescript.md +46 -0
- package/src/llms/skills/imf-web-ui-frontend-conventions/references/validation.md +88 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +66 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/templates/AGENTS.md +34 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/templates/FRONTEND_SETUP_REPORT.md +45 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/templates/README.md +27 -0
- package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +36 -0
- package/src/llms/skills/imf-web-ui-update/SKILL.md +153 -0
- package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
- package/src/llms/skills/imf-web-ui-ux/references/forms.md +1 -1
- package/bin/install-skill.js +0 -180
- 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-imfusion-frontend-setup/SKILL.md +0 -201
- package/src/llms/skills/imf-web-ui-setup/SKILL.md +0 -57
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# SessionStart: point the agent at the vendored conventions baseline once per session.
|
|
3
|
+
echo "The ImFusion frontend conventions baseline is vendored in .agents/skills/ (imf-web-ui-frontend-conventions and siblings). Load the relevant skill before writing code, styles, or docs; repo docs in docs/ carry only what is unique to this repo."
|
|
4
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/session-start.sh\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"UserPromptSubmit": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/user-prompt-submit.sh\""
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Edit|MultiEdit|Write",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/post-tool-use.sh\"",
|
|
31
|
+
"statusMessage": "Verifying file"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -3,7 +3,8 @@ name: imf-web-ui-components
|
|
|
3
3
|
description:
|
|
4
4
|
"Look up @imfusion/web-ui component APIs without reading their source: the two-hop lookup (identity index -> prop data),
|
|
5
5
|
compound components, and integrations. Load when you need the props, sub-components, or defaults of a specific component —
|
|
6
|
-
not for choosing between components (imf-web-ui-ux) or first-time setup (imf-web-ui-setup)."
|
|
6
|
+
not for choosing between components (imf-web-ui-ux) or first-time setup (imf-web-ui-library-setup)."
|
|
7
|
+
allowed-tools: Bash
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# imf-web-ui-components
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: imf-web-ui-frontend-conventions
|
|
3
|
+
description:
|
|
4
|
+
"The ImFusion frontend conventions baseline — in-house conventions, valid in every ImFusion frontend and usable by anyone
|
|
5
|
+
who likes them. A router over topic references: library boundary, React, components, TypeScript, styling, validation, data
|
|
6
|
+
layer, project structure, testing, stack, npm project, tooling config, git, assets, docs structure. Load when writing
|
|
7
|
+
wrapper components, custom UI, styling beyond the defaults, validating external data, adding new files to a consumer app,
|
|
8
|
+
writing repo docs, touching tool config, or choosing any dependency."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# imf-web-ui-frontend-conventions
|
|
12
|
+
|
|
13
|
+
The ImFusion frontend baseline. In-house conventions, not industry claims — they encode how ImFusion frontends are built, and
|
|
14
|
+
anyone else is welcome to them.
|
|
15
|
+
|
|
16
|
+
**The project wins.** These defaults fill vacuums: if the host project already has a convention — a styling system, a state
|
|
17
|
+
library, a folder shape — that stands. They are not a license to refactor a consumer codebase toward this document.
|
|
18
|
+
|
|
19
|
+
**Alignment mode.** The exception is intent: when the user asks to _align_ the repo with the baseline ("align", "alignment"
|
|
20
|
+
is the flag), the project-wins guard lifts. Deviations then become migration findings — proposed as a plan, still nothing
|
|
21
|
+
changed until the human approves.
|
|
22
|
+
|
|
23
|
+
## The references
|
|
24
|
+
|
|
25
|
+
Each topic lives in one reference. Read the one whose moment you're in; starting a new feature usually wants several.
|
|
26
|
+
|
|
27
|
+
| Reference | Covers | Read when |
|
|
28
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
29
|
+
| [library-boundary.md](references/library-boundary.md) | staying behind `@imfusion/web-ui`, wrappers, type derivation, peers | touching anything that renders library components |
|
|
30
|
+
| [react.md](references/react.md) | component roles, state kinds and owners, effects discipline | writing new screens, components, or wrappers |
|
|
31
|
+
| [components.md](references/components.md) | component anatomy on disk: grouping, folders, colocation | adding a component file |
|
|
32
|
+
| [typescript.md](references/typescript.md) | functional style, types, naming | writing any code |
|
|
33
|
+
| [styling.md](references/styling.md) | native CSS Modules, tokens, the override contract | writing CSS or styling beyond the defaults |
|
|
34
|
+
| [class-names.md](references/class-names.md) | CVA variants, `cx`, merging the incoming `className` | writing a component with variants or a `className` prop |
|
|
35
|
+
| [validation.md](references/validation.md) | runtime schemas, boundary parsing, schema-derived types | accepting data the frontend does not own |
|
|
36
|
+
| [data.md](references/data.md) | `api/`+`http/` shape, query/mutation patterns and invalidation | adding an API topic, a fetch, or a mutation |
|
|
37
|
+
| [project-structure.md](references/project-structure.md) | the `src/` tree, file naming, imports | adding files rather than editing existing ones |
|
|
38
|
+
| [testing.md](references/testing.md) | what's worth testing and what isn't | writing or reviewing tests |
|
|
39
|
+
| [stack.md](references/stack.md) | the topic→tool map, when a library owns a layer | choosing or adding any dependency |
|
|
40
|
+
| [npm-project.md](references/npm-project.md) | `package.json`, the `verify:*` script set, dependency pinning | running or adding a script, adding a dependency |
|
|
41
|
+
| [tooling.md](references/tooling.md) | Prettier, ESLint, tsconfig, staged files, CSS class names | touching tool config |
|
|
42
|
+
| [git.md](references/git.md) | `git:config`, verify scopes, staleness at commit time | wiring hooks or the commit path |
|
|
43
|
+
| [assets.md](references/assets.md) | image formats, the WebP recipe | adding images or other static assets |
|
|
44
|
+
| [docs-structure.md](references/docs-structure.md) | what a repo documents, where, how it's written | writing or restructuring repo docs |
|
|
45
|
+
|
|
46
|
+
`imf-web-ui-frontend-setup` bootstraps and audits a repo against this baseline.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Assets
|
|
2
|
+
|
|
3
|
+
Import everything from `src/assets/` so the bundler fingerprints and bundles it. Never reference an image by public-path
|
|
4
|
+
string.
|
|
5
|
+
|
|
6
|
+
## Photographs — WebP
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
magick source.png -resize 2000x -quality 80 -define webp:method=6 src/assets/name.webp
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- Quality 80 is visually lossless on photos and routinely cuts file size by an order of magnitude.
|
|
13
|
+
- `method=6` is the densest encoding. It's a one-off cost at conversion time, so take the smaller file.
|
|
14
|
+
- Cap the long edge at 2000px — nothing on the market resolves more in a content image.
|
|
15
|
+
- No `<picture>` fallback needed; WebP is supported everywhere since 2020.
|
|
16
|
+
|
|
17
|
+
## Other formats
|
|
18
|
+
|
|
19
|
+
- **Alpha, or pixels that must stay exact** → PNG, optimized with `oxipng` or `pngquant`.
|
|
20
|
+
- **Icons, logos, line art** → inline SVG component, so it inherits `currentColor` and follows the theme.
|
|
21
|
+
|
|
22
|
+
These rules apply to assets as they're added or touched. Existing assets in another format are not findings to sweep —
|
|
23
|
+
convert opportunistically, when you're working on them anyway.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Class names in components
|
|
2
|
+
|
|
3
|
+
How a component turns props into a `className` string. What the build does with the result is [tooling.md](tooling.md); what
|
|
4
|
+
goes in the stylesheet is [styling.md](styling.md).
|
|
5
|
+
|
|
6
|
+
**CVA is the only tool.** `class-variance-authority` maps variant props to CSS Module classes. No `clsx`, no
|
|
7
|
+
`tailwind-merge`, no local `cn()` helper. `cx` is CVA's own concatenator and `@imfusion/web-ui` re-exports it, so it comes
|
|
8
|
+
from the library alongside the components.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { cva } from "class-variance-authority";
|
|
12
|
+
import classes from "./chip.module.css";
|
|
13
|
+
|
|
14
|
+
const chip = cva(classes.root, {
|
|
15
|
+
variants: {
|
|
16
|
+
appearance: { outline: classes.appearanceOutline, solid: classes.appearanceSolid },
|
|
17
|
+
inline: { false: null, true: classes.inline }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function Chip({ appearance = "outline", inline = false, className, ...props }: Props) {
|
|
22
|
+
return <span {...props} className={chip({ appearance, inline, className })} />;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Base class is CVA's first argument**, variant values are CSS Module references, never string literals. A boolean axis
|
|
27
|
+
uses `null` for its off-state.
|
|
28
|
+
- **The incoming `className` goes into CVA's `className` slot**, which appends it last so a caller's class always wins. With
|
|
29
|
+
no variants to map, `cx(classes.inline, className)` does the same job.
|
|
30
|
+
- **Every component that accepts `className` merges it.** A component whose surface is deliberately closed omits the prop
|
|
31
|
+
entirely rather than accepting and ignoring it.
|
|
32
|
+
- **Defaults live in the props destructuring, not CVA's `defaultVariants`.** react-docgen-typescript reads the destructuring,
|
|
33
|
+
so defaults declared in CVA don't reach the generated docs.
|
|
34
|
+
- **Resolve a function-form `className` before merging.** Components built on a library that passes render state
|
|
35
|
+
(`className={state => …}`) receive either shape:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
className={state => cx(classes.root, typeof className === "function" ? className(state) : className)}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **Two components sharing one visual share one CVA module**, a `{name}.cva.ts` next to them, so their variant axes can't
|
|
42
|
+
drift apart.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
How a component lives on disk and how dumb it stays. Roles, state, and effects in full are [react.md](react.md); styling is
|
|
4
|
+
[styling.md](styling.md).
|
|
5
|
+
|
|
6
|
+
## As dumb as possible
|
|
7
|
+
|
|
8
|
+
Data comes from outside — props-fed, never self-fetching. Presentational logic (formatting a label, deriving a display state)
|
|
9
|
+
is fine; owning data is not:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// components/user-card/user-card.tsx
|
|
13
|
+
interface Props {
|
|
14
|
+
name: string;
|
|
15
|
+
role: string;
|
|
16
|
+
onEdit: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function UserCard({ name, role, onEdit }: Props) {
|
|
20
|
+
return (
|
|
21
|
+
<Card.Root>
|
|
22
|
+
<Card.Content>
|
|
23
|
+
<Typo>{name}</Typo>
|
|
24
|
+
<Chip>{role}</Chip>
|
|
25
|
+
</Card.Content>
|
|
26
|
+
<Card.Footer>
|
|
27
|
+
<Button onClick={onEdit}>Edit</Button>
|
|
28
|
+
</Card.Footer>
|
|
29
|
+
</Card.Root>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No query hook, no store access, no route awareness — the component renders what it's given and reports events upward. Local
|
|
35
|
+
UI state (`isOpen`, a draft value) is allowed; anything whose truth lives elsewhere is not ([react.md](react.md)).
|
|
36
|
+
|
|
37
|
+
## Grouping
|
|
38
|
+
|
|
39
|
+
`components/` groups by kind — a layout component under `layouts/`, not beside a domain widget. Flat is fine while there are
|
|
40
|
+
few; let the grouping follow what the project actually has rather than imposing it up front. The kinds worth separating are
|
|
41
|
+
the component roles in [react.md](react.md).
|
|
42
|
+
|
|
43
|
+
## One file or a folder
|
|
44
|
+
|
|
45
|
+
A component gets a folder once it has more than one file — sub-components, styles, tests — with an `index.ts` that only
|
|
46
|
+
re-exports:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
components/data-table/
|
|
50
|
+
data-table.tsx
|
|
51
|
+
data-table-row.tsx
|
|
52
|
+
data-table.module.css
|
|
53
|
+
index.ts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Imports then read `#/components/data-table` and the inside can be restructured without touching call sites. Single-file
|
|
57
|
+
components stay single files, directly in their group.
|
|
58
|
+
|
|
59
|
+
## Colocation
|
|
60
|
+
|
|
61
|
+
The stylesheet is colocated as `<component>.module.css` next to the component, and only dumb components have one — a
|
|
62
|
+
container that wants CSS is asking for a layout component instead ([react.md](react.md)). Tests and local types sit next to
|
|
63
|
+
their subject too. A file you have to hunt for in a parallel tree gets edited less carefully.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Data
|
|
2
|
+
|
|
3
|
+
How data enters, moves through, and leaves an ImFusion frontend. This is the in-house pattern **for TanStack Query + Router**
|
|
4
|
+
— the default stack ([stack.md](stack.md)). A project on a different data layer keeps the boundary principles (validate at
|
|
5
|
+
the edge, errors as values) but not necessarily this file layout.
|
|
6
|
+
|
|
7
|
+
## The shape
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
api/<topic>/ # one folder per API topic
|
|
12
|
+
<topic>.ts # queryOptions / mutationOptions
|
|
13
|
+
query-key.ts # key factory, topic as the first segment
|
|
14
|
+
types.ts # Zod schemas + z.infer types
|
|
15
|
+
http/ # transport: client, error normalisation — the only transport-aware place
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A query lives next to its key factory and its schemas, so a query key is never spelled out at a call site. Anything
|
|
19
|
+
transport-level (base client, error normalisation) lives in `http/` and nowhere else.
|
|
20
|
+
|
|
21
|
+
## The network boundary validates
|
|
22
|
+
|
|
23
|
+
The transport in `http/` returns `unknown`; the topic schema parses the response in its query or mutation function before the
|
|
24
|
+
value enters the app. The schema is also the source of its TypeScript type. The full boundary, type-derivation, and failure
|
|
25
|
+
handling rules are [validation.md](validation.md).
|
|
26
|
+
|
|
27
|
+
## Errors are values
|
|
28
|
+
|
|
29
|
+
A failed request becomes an `ApiError` carrying a machine-readable code — it crosses the boundary the same way values do, not
|
|
30
|
+
as an exception caught ad hoc. The frontend owns the mapping from codes to what the user reads.
|
|
31
|
+
|
|
32
|
+
## The pattern, end to end
|
|
33
|
+
|
|
34
|
+
The key factory — topic as the first segment, one function per key shape:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// api/user/query-key.ts
|
|
38
|
+
const TOPIC = "user" as const;
|
|
39
|
+
|
|
40
|
+
export const userKeys = {
|
|
41
|
+
all: [TOPIC] as const,
|
|
42
|
+
me: () => [TOPIC, "me"] as const,
|
|
43
|
+
detail: (id: string) => [TOPIC, "detail", id] as const
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Query options — key from the factory, parse at the boundary:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// api/user/user.ts
|
|
51
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
52
|
+
|
|
53
|
+
import { bffClient } from "#/http/bff-client";
|
|
54
|
+
import { userKeys } from "./query-key";
|
|
55
|
+
import { userSchema } from "./types";
|
|
56
|
+
|
|
57
|
+
export const userQueryOptions = () =>
|
|
58
|
+
queryOptions({
|
|
59
|
+
queryKey: userKeys.me(),
|
|
60
|
+
queryFn: async () => userSchema.parse(await bffClient("/me"))
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Components consume query options directly with `useSuspenseQuery` (prefetched routes) or `useQuery` (secondary data). No
|
|
65
|
+
wrapper hooks — the options function is the reusable unit. Call sites may spread the returned options only to add
|
|
66
|
+
component-specific callbacks or overrides.
|
|
67
|
+
|
|
68
|
+
Mutations follow the same shape with `mutationOptions`, the dirtied topic first in the `mutationKey`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// api/user/user.ts
|
|
72
|
+
export const updateUserMutationOptions = () =>
|
|
73
|
+
mutationOptions({
|
|
74
|
+
mutationKey: [...userKeys.all, "update"],
|
|
75
|
+
mutationFn: async (patch: UserPatch) => userSchema.parse(await bffClient("/me", { method: "PATCH", body: patch }))
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The root route types the router context so every loader can reach the query client via DI:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// routes/__root.tsx
|
|
83
|
+
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
|
84
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
85
|
+
|
|
86
|
+
interface RouterContext {
|
|
87
|
+
queryClient: QueryClient;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
91
|
+
component: () => <Outlet />
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The router is created with that context filled in:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// router.ts
|
|
99
|
+
import { createRouter } from "@tanstack/react-router";
|
|
100
|
+
import { routeTree } from "./routeTree.gen";
|
|
101
|
+
import { createQueryClient } from "./query-client";
|
|
102
|
+
|
|
103
|
+
const queryClient = createQueryClient();
|
|
104
|
+
|
|
105
|
+
export const router = createRouter({
|
|
106
|
+
routeTree,
|
|
107
|
+
context: { queryClient }
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A route prefetches in the `loader`, reads with `useSuspenseQuery`, and provides a `pendingComponent`; errors bubble to the
|
|
112
|
+
nearest `errorComponent`:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// routes/me.tsx
|
|
116
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
117
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
118
|
+
|
|
119
|
+
import { userQueryOptions } from "#/api/user/user";
|
|
120
|
+
|
|
121
|
+
export const Route = createFileRoute("/me")({
|
|
122
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(userQueryOptions()),
|
|
123
|
+
pendingComponent: () => <p>Loading…</p>,
|
|
124
|
+
component: MePage
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function MePage() {
|
|
128
|
+
const { data: user } = useSuspenseQuery(userQueryOptions());
|
|
129
|
+
return <h1>{user.name}</h1>;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Mutations use `useMutation` with the options factory instead of reconstructing `mutationKey` and `mutationFn` at the call
|
|
134
|
+
site. Manual invalidation is unnecessary when automatic invalidation is on (see below):
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// components/user-settings.tsx
|
|
138
|
+
import { useMutation } from "@tanstack/react-query";
|
|
139
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
140
|
+
|
|
141
|
+
import { userQueryOptions, updateUserMutationOptions } from "#/api/user/user";
|
|
142
|
+
|
|
143
|
+
function UserSettings() {
|
|
144
|
+
const { data: user } = useSuspenseQuery(userQueryOptions());
|
|
145
|
+
const updateUser = useMutation(updateUserMutationOptions());
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<form
|
|
149
|
+
onSubmit={e => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
updateUser.mutate({ name: "New Name" });
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<input defaultValue={user.name} />
|
|
155
|
+
</form>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Automatic invalidation
|
|
161
|
+
|
|
162
|
+
Mutations don't invalidate by hand at every call site — the query client does it centrally, keyed off the mutation's topic. A
|
|
163
|
+
`MutationCache` `onSuccess` invalidates `mutationKey[0]`, on by default via `meta.autoInvalidate`; that's why every
|
|
164
|
+
mutation's key leads with the topic it dirties:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// query-client.ts
|
|
168
|
+
export function createQueryClient() {
|
|
169
|
+
const queryClient = new QueryClient({
|
|
170
|
+
defaultOptions: {
|
|
171
|
+
mutations: { meta: { autoInvalidate: true } }
|
|
172
|
+
},
|
|
173
|
+
mutationCache: new MutationCache({
|
|
174
|
+
onSuccess: async (_data, _variables, _context, mutation) => {
|
|
175
|
+
if (!mutation.meta?.autoInvalidate) return;
|
|
176
|
+
const topic = mutation.options.mutationKey?.[0];
|
|
177
|
+
if (topic !== undefined) await queryClient.invalidateQueries({ queryKey: [topic] });
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
return queryClient;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Opt out per mutation with `meta: { autoInvalidate: false }` when a coarse topic-wide refetch is wrong (a huge list, a
|
|
186
|
+
targeted optimistic update), and invalidate precisely through the key factory instead. Background reading:
|
|
187
|
+
[query invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation) and
|
|
188
|
+
[automatic invalidation after mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations).
|
|
189
|
+
|
|
190
|
+
## Audit
|
|
191
|
+
|
|
192
|
+
Inspect the complete path, not the presence of TanStack Query alone: transport returns `unknown`; topic schemas parse
|
|
193
|
+
responses; API types derive from those schemas; keys come from topic factories; API topics expose reusable `queryOptions` and
|
|
194
|
+
`mutationOptions`; hooks consume those options with only local overrides; mutation keys match the configured invalidation
|
|
195
|
+
strategy.
|
|
196
|
+
|
|
197
|
+
## Testing the data layer
|
|
198
|
+
|
|
199
|
+
Stub the network at the `fetch` boundary and let the real query client run: the test then exercises the same parse and error
|
|
200
|
+
path production does. Schemas, clients, and query options are where behaviour worth asserting lives — schema cases are in
|
|
201
|
+
[validation.md](validation.md), and the general philosophy is [testing.md](testing.md).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Documentation structure
|
|
2
|
+
|
|
3
|
+
What a repo documents, where, and how it's written. `imf-web-ui-frontend-setup` scaffolds this shape from its `templates/`
|
|
4
|
+
and audits it.
|
|
5
|
+
|
|
6
|
+
## The shape
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
<app>/
|
|
10
|
+
README.md # humans: what the app is, setup, scripts
|
|
11
|
+
AGENTS.md # agents: stack, pointers into docs/ — restates nothing
|
|
12
|
+
docs/
|
|
13
|
+
index.md # registers every doc with a one-line "covers" summary
|
|
14
|
+
<topic>.md # repo-unique content only
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Repo docs hold only what is unique to the repo
|
|
18
|
+
|
|
19
|
+
The litmus, applied sentence by sentence: **would this be true in every ImFusion frontend? Then it's baseline — it lives in
|
|
20
|
+
the installed skill references, don't restate it.** The baseline is already in the repo, vendored under
|
|
21
|
+
`.agents/skills/imf-web-ui-frontend-conventions/`, human-readable and versioned.
|
|
22
|
+
|
|
23
|
+
Where the repo deviates from the baseline, the doc says so as a **named deviation** — what the baseline prescribes, what this
|
|
24
|
+
repo does instead, and why. A deviation written as freestanding convention gets copied into the next repo as if it were house
|
|
25
|
+
style.
|
|
26
|
+
|
|
27
|
+
## How docs are written
|
|
28
|
+
|
|
29
|
+
- **Docs explain concepts; code is the source of truth for facts.** A doc captures the why — invariants, rationale,
|
|
30
|
+
decisions. It never restates a fact that lives in code (a script definition, a type shape, a config value): point at the
|
|
31
|
+
file instead. A copied fact rots the moment the code changes.
|
|
32
|
+
- **State what is — no decision residue.** Positive, present-tense statements about the current state. Never narrate the
|
|
33
|
+
delta from a past decision or refute alternatives nobody raised ("there is no X mode", "Y was dropped") — that history
|
|
34
|
+
belongs in commits and tickets. A negation earns its place only as a guardrail or to preempt a wrong assumption a present
|
|
35
|
+
reader would actually arrive at.
|
|
36
|
+
- **Boy Scout rule.** Discovered rot — a stale pointer, a doc contradicting the code — is always your responsibility: fix it
|
|
37
|
+
in place if trivial, otherwise report it. The two failure modes are equal: stepping over rot, and cramming unrelated
|
|
38
|
+
cleanup into an unrelated change.
|
|
39
|
+
|
|
40
|
+
## Staleness at commit time
|
|
41
|
+
|
|
42
|
+
Docs are checked when they can go stale: at the commit. A staged change that invalidates a doc — a renamed script, a moved
|
|
43
|
+
folder, a changed flow — updates that doc **in the same commit**, not in a follow-up. Pre-commit carries the advisory
|
|
44
|
+
staleness checks (vendored baseline, docs) — the mechanics are in [git.md](git.md).
|
|
@@ -0,0 +1,33 @@
|
|
|
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. Two silent failure modes to check
|
|
12
|
+
for: `git:config` never run (hooks exist only on the machine that configured by hand), and `core.hooksPath` pointing at a
|
|
13
|
+
directory that doesn't exist. Config **and** directory.
|
|
14
|
+
|
|
15
|
+
## Verify scopes
|
|
16
|
+
|
|
17
|
+
Two blocking, one advisory:
|
|
18
|
+
|
|
19
|
+
- **staged** — `verify:staged`, called by the pre-commit hook: lint, format, restage. Fast; a passing commit is not CI green.
|
|
20
|
+
- **full** — `verify:full`: the build plus every `verify:*` check. What CI runs.
|
|
21
|
+
- **files** — post-edit agent hook. Advisory, never exits non-zero, so a mid-flight refactor can't trap the agent.
|
|
22
|
+
|
|
23
|
+
One script owns each scope's step list; npm scripts and hooks only launch them. Name by depth, not by occasion — a
|
|
24
|
+
`preflight` needs explaining and invites a near-identical sibling, and two of those drift into "passes locally, fails in CI".
|
|
25
|
+
|
|
26
|
+
## Staleness at commit time
|
|
27
|
+
|
|
28
|
+
Pre-commit also runs the advisory staleness checks — warn, never block:
|
|
29
|
+
|
|
30
|
+
- **Vendored baseline** — `.agents/hooks/imf-web-ui/baseline-staleness.sh` compares the installed `imf-web-ui-*` skill
|
|
31
|
+
markers against the installed package version; the fix it names is `npx web-ui-install`.
|
|
32
|
+
- **Docs** — a staged change that invalidates a doc updates that doc in the same commit
|
|
33
|
+
([docs-structure.md](docs-structure.md)). Where the repo has an agent commit workflow, a staged docs audit belongs in it.
|
|
@@ -0,0 +1,37 @@
|
|
|
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`. If the library is missing
|
|
9
|
+
something upstream has, report the gap (see `imf-web-ui-components`); don't reach around it.
|
|
10
|
+
|
|
11
|
+
## Wrap primitives when the app has a reason to
|
|
12
|
+
|
|
13
|
+
When the app keeps repeating something around a primitive — default props, a styling override, a composition, an
|
|
14
|
+
accessibility refinement, a restriction of the API — wrap it once in an app-level dumb component and use that. The wrapper
|
|
15
|
+
derives its props from the primitive (`React.ComponentProps<typeof Button>`, narrowed or extended), lives in `components/`
|
|
16
|
+
like any other dumb component ([components.md](components.md)), and styles itself through the sanctioned seams — it never
|
|
17
|
+
reaches into the library's internals.
|
|
18
|
+
|
|
19
|
+
Two rules keep wrappers honest:
|
|
20
|
+
|
|
21
|
+
- Wrap for a reason — any repeated adaptation counts. A wrapper that only renames a primitive is indirection with no payoff.
|
|
22
|
+
- Components marked `experimental` in the identity index get wrapped **always**, even with nothing added yet — a breaking
|
|
23
|
+
upstream change then lands in one file instead of every call site.
|
|
24
|
+
|
|
25
|
+
## Derive types, don't import them
|
|
26
|
+
|
|
27
|
+
Prop types come from the components themselves: `React.ComponentProps<typeof Button>`. The library deliberately exports no
|
|
28
|
+
`Props` types — don't look for them, and don't re-declare prop shapes by hand.
|
|
29
|
+
|
|
30
|
+
## Integrations own their peers
|
|
31
|
+
|
|
32
|
+
Components under `@imfusion/web-ui/integrations/*` depend on optional peers (e.g. `@tanstack/highlight` for `CodeHighlight`).
|
|
33
|
+
Add the peer explicitly to the consumer's `package.json` — never rely on hoisting.
|
|
34
|
+
|
|
35
|
+
## Styling crosses the boundary through seams
|
|
36
|
+
|
|
37
|
+
Tokens in, sanctioned selectors at the edge, never the library's internals — the full contract is [styling.md](styling.md).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# npm project
|
|
2
|
+
|
|
3
|
+
How the npm side of an ImFusion frontend is structured: `package.json`, scripts, dependencies. `imf-web-ui-frontend-setup`
|
|
4
|
+
audits against this file.
|
|
5
|
+
|
|
6
|
+
## package.json
|
|
7
|
+
|
|
8
|
+
The exemplary shape:
|
|
9
|
+
|
|
10
|
+
```jsonc
|
|
11
|
+
{
|
|
12
|
+
"name": "@imfusion/scan-review",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"private": true, // only when the package is not meant to be published
|
|
15
|
+
"engines": { "node": ">=22" },
|
|
16
|
+
"imports": { "#/*": "./src/*" }
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `"type": "module"` always.
|
|
21
|
+
- `"private": true` for apps that never publish; a publishable package drops it.
|
|
22
|
+
- Node pinned via `engines.node` or `.nvmrc` — not a personal version manager's config.
|
|
23
|
+
- The `#/` alias wired through `imports` ([project-structure.md](project-structure.md)).
|
|
24
|
+
|
|
25
|
+
## Scripts
|
|
26
|
+
|
|
27
|
+
Same name, same meaning, every repo — "what can I run to check this?" is answered by tab-completing `verify:`.
|
|
28
|
+
|
|
29
|
+
| Script | Runs |
|
|
30
|
+
| ------------------ | --------------------------------------------------------------------- |
|
|
31
|
+
| `dev` | dev server |
|
|
32
|
+
| `build` | production build |
|
|
33
|
+
| `verify:deps` | exact-pin check over `dependencies` and `devDependencies` |
|
|
34
|
+
| `verify:format` | `prettier --check .` |
|
|
35
|
+
| `verify:lint` | `eslint . --cache --max-warnings=0` |
|
|
36
|
+
| `verify:typecheck` | `tsc --noEmit` (or `tsc -b --noEmit` in a project-references setup) |
|
|
37
|
+
| `verify:tests` | `vitest run` |
|
|
38
|
+
| `verify:staged` | staged-file subset, called by the pre-commit hook |
|
|
39
|
+
| `verify:full` | every `verify:*` check plus the build; what CI runs |
|
|
40
|
+
| `format` | `prettier --write .` |
|
|
41
|
+
| `lint` | `eslint . --cache --fix` |
|
|
42
|
+
| `git:config` | see [git.md](git.md); run by hand once per clone, named in the README |
|
|
43
|
+
|
|
44
|
+
Two rules generate the names:
|
|
45
|
+
|
|
46
|
+
- **Every check is `verify:*`.** One namespace for everything that reads and reports.
|
|
47
|
+
- **Write-mode scripts keep the tool name.** `format` and `lint` change files, which isn't verifying — no prefix, no `:fix`
|
|
48
|
+
suffix.
|
|
49
|
+
|
|
50
|
+
`verify:staged` is fast and partial; a passing commit is not CI green. `verify:full` is the CI gate.
|
|
51
|
+
|
|
52
|
+
## Dependencies
|
|
53
|
+
|
|
54
|
+
- **Pinned exactly.** No `^`, `~`, or `latest`, in `dependencies` and `devDependencies` alike. `verify:deps` catches drift;
|
|
55
|
+
`save-exact=true` in `.npmrc` prevents it.
|
|
56
|
+
- **`ignore-scripts=true` in `.npmrc`.** Blocks lifecycle scripts on install (the supply-chain vector). Setup that matters is
|
|
57
|
+
a command someone runs, not a hook that fires on install.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Project structure
|
|
2
|
+
|
|
3
|
+
Kebab-case throughout, folders and files alike. Exported symbols stay PascalCase — only the filename is kebab.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
routes/ # TanStack Router file-based routes; routing only — they compose, they don't fetch inline
|
|
8
|
+
api/ # one folder per API topic — layout and behaviour in data.md
|
|
9
|
+
components/ # grouped by kind — anatomy in components.md
|
|
10
|
+
http/ # transport: client, error normalisation — the only transport-aware place (data.md)
|
|
11
|
+
lib/ # framework-free helpers
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each subtree's own rules live with its topic: [data.md](data.md) for `api/` and `http/`, [components.md](components.md) for
|
|
15
|
+
`components/`.
|
|
16
|
+
|
|
17
|
+
## Imports
|
|
18
|
+
|
|
19
|
+
The `#/` alias for anything outside the current folder, plain `./` for siblings. The alias is always `#/` → `src/` — `#` is
|
|
20
|
+
Node's own subpath-import prefix, so it can't collide with an npm scope. Wire it once, through `package.json`'s `imports`
|
|
21
|
+
field where the toolchain resolves it natively, with a matching tsconfig `paths` entry.
|