@imfusion/web-ui 0.5.1-dev.2.g0a349a2a → 0.5.1-dev.20.g9170acaf
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 +122 -43
- package/bin/install.js +343 -0
- package/bin/install.test.ts +166 -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/logo/logo.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/style.css +1 -1
- package/package.json +34 -24
- package/src/docgen/doc.gen.json +1 -1
- package/src/llms/skills/imf-web-ui/SKILL.md +15 -11
- package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +71 -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 +21 -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 +1 -1
- package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +45 -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 +196 -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} +26 -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 +16 -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 +45 -0
- package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +89 -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/README.md +27 -0
- package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +36 -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,196 @@
|
|
|
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 network is the only place untyped data enters the app, so it is the only place that validates. Everything a backend sends
|
|
24
|
+
is parsed against a Zod schema at that edge; the TypeScript types are derived from the schemas with `z.infer`, never
|
|
25
|
+
hand-written. A hand-written response type is a claim about the backend that nothing checks.
|
|
26
|
+
|
|
27
|
+
The transport in `http/` is deliberately untyped: it returns `unknown` and leaves the parse to the caller, so no call site
|
|
28
|
+
can accidentally skip validation.
|
|
29
|
+
|
|
30
|
+
## Errors are values
|
|
31
|
+
|
|
32
|
+
A failed request becomes an `ApiError` carrying a machine-readable code — it crosses the boundary the same way values do, not
|
|
33
|
+
as an exception caught ad hoc. The frontend owns the mapping from codes to what the user reads.
|
|
34
|
+
|
|
35
|
+
## The pattern, end to end
|
|
36
|
+
|
|
37
|
+
The key factory — topic as the first segment, one function per key shape:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// api/user/query-key.ts
|
|
41
|
+
const TOPIC = "user" as const;
|
|
42
|
+
|
|
43
|
+
export const userKeys = {
|
|
44
|
+
all: [TOPIC] as const,
|
|
45
|
+
me: () => [TOPIC, "me"] as const,
|
|
46
|
+
detail: (id: string) => [TOPIC, "detail", id] as const
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Query options — key from the factory, parse at the boundary:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// api/user/user.ts
|
|
54
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
55
|
+
|
|
56
|
+
import { bffClient } from "#/http/bff-client";
|
|
57
|
+
import { userKeys } from "./query-key";
|
|
58
|
+
import { userSchema } from "./types";
|
|
59
|
+
|
|
60
|
+
export const userQueryOptions = () =>
|
|
61
|
+
queryOptions({
|
|
62
|
+
queryKey: userKeys.me(),
|
|
63
|
+
queryFn: async () => userSchema.parse(await bffClient("/me"))
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Components consume query options directly with `useSuspenseQuery` (prefetched routes) or `useQuery` (secondary data). No
|
|
68
|
+
wrapper hooks — the options function is the reusable unit.
|
|
69
|
+
|
|
70
|
+
Mutations follow the same shape with `mutationOptions`, the dirtied topic first in the `mutationKey`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// api/user/user.ts
|
|
74
|
+
export const updateUserMutationOptions = () =>
|
|
75
|
+
mutationOptions({
|
|
76
|
+
mutationKey: [...userKeys.all, "update"],
|
|
77
|
+
mutationFn: async (patch: UserPatch) => userSchema.parse(await bffClient("/me", { method: "PATCH", body: patch }))
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The root route types the router context so every loader can reach the query client via DI:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// routes/__root.tsx
|
|
85
|
+
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
|
86
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
87
|
+
|
|
88
|
+
interface RouterContext {
|
|
89
|
+
queryClient: QueryClient;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
93
|
+
component: () => <Outlet />
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The router is created with that context filled in:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// router.ts
|
|
101
|
+
import { createRouter } from "@tanstack/react-router";
|
|
102
|
+
import { routeTree } from "./routeTree.gen";
|
|
103
|
+
import { createQueryClient } from "./query-client";
|
|
104
|
+
|
|
105
|
+
const queryClient = createQueryClient();
|
|
106
|
+
|
|
107
|
+
export const router = createRouter({
|
|
108
|
+
routeTree,
|
|
109
|
+
context: { queryClient }
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A route prefetches in the `loader`, reads with `useSuspenseQuery`, and provides a `pendingComponent`; errors bubble to the
|
|
114
|
+
nearest `errorComponent`:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// routes/me.tsx
|
|
118
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
119
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
120
|
+
|
|
121
|
+
import { userQueryOptions } from "#/api/user/user";
|
|
122
|
+
|
|
123
|
+
export const Route = createFileRoute("/me")({
|
|
124
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(userQueryOptions()),
|
|
125
|
+
pendingComponent: () => <p>Loading…</p>,
|
|
126
|
+
component: MePage
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
function MePage() {
|
|
130
|
+
const { data: user } = useSuspenseQuery(userQueryOptions());
|
|
131
|
+
return <h1>{user.name}</h1>;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Mutations use `useMutation` with the options factory. Manual invalidation is unnecessary when automatic invalidation is on
|
|
136
|
+
(see below):
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// components/user-settings.tsx
|
|
140
|
+
import { useMutation } from "@tanstack/react-query";
|
|
141
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
142
|
+
|
|
143
|
+
import { userQueryOptions, updateUserMutationOptions } from "#/api/user/user";
|
|
144
|
+
|
|
145
|
+
function UserSettings() {
|
|
146
|
+
const { data: user } = useSuspenseQuery(userQueryOptions());
|
|
147
|
+
const updateUser = useMutation(updateUserMutationOptions());
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<form
|
|
151
|
+
onSubmit={e => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
updateUser.mutate({ name: "New Name" });
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<input defaultValue={user.name} />
|
|
157
|
+
</form>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Automatic invalidation
|
|
163
|
+
|
|
164
|
+
Mutations don't invalidate by hand at every call site — the query client does it centrally, keyed off the mutation's topic. A
|
|
165
|
+
`MutationCache` `onSuccess` invalidates `mutationKey[0]`, on by default via `meta.autoInvalidate`; that's why every
|
|
166
|
+
mutation's key leads with the topic it dirties:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// query-client.ts
|
|
170
|
+
export function createQueryClient() {
|
|
171
|
+
const queryClient = new QueryClient({
|
|
172
|
+
defaultOptions: {
|
|
173
|
+
mutations: { meta: { autoInvalidate: true } }
|
|
174
|
+
},
|
|
175
|
+
mutationCache: new MutationCache({
|
|
176
|
+
onSuccess: async (_data, _variables, _context, mutation) => {
|
|
177
|
+
if (!mutation.meta?.autoInvalidate) return;
|
|
178
|
+
const topic = mutation.options.mutationKey?.[0];
|
|
179
|
+
if (topic !== undefined) await queryClient.invalidateQueries({ queryKey: [topic] });
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
return queryClient;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Opt out per mutation with `meta: { autoInvalidate: false }` when a coarse topic-wide refetch is wrong (a huge list, a
|
|
188
|
+
targeted optimistic update), and invalidate precisely through the key factory instead. Background reading:
|
|
189
|
+
[query invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation) and
|
|
190
|
+
[automatic invalidation after mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations).
|
|
191
|
+
|
|
192
|
+
## Testing the data layer
|
|
193
|
+
|
|
194
|
+
Stub the network at the `fetch` boundary and let the real query client run: the test then exercises the same parse and error
|
|
195
|
+
path production does. Schemas, clients, and query options are where behaviour worth asserting lives — the general philosophy
|
|
196
|
+
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# React
|
|
1
|
+
# React
|
|
2
2
|
|
|
3
3
|
The house defaults for the React code around `@imfusion/web-ui`, in full. The links throughout are for **you, the agent**:
|
|
4
4
|
consult them while building — they are the authoritative source when a case here is ambiguous. Hand them to the human only if
|
|
@@ -43,9 +43,8 @@ function UserCardContainer({ userId }: { userId: string }) {
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Why it matters here: dumb components are the layer where `@imfusion/web-ui` lives. Keeping them free of logic keeps every
|
|
46
|
-
screen restylable, testable with plain props, and resilient to library updates.
|
|
47
|
-
|
|
48
|
-
breaking upstream change then lands in one file instead of every call site.
|
|
46
|
+
screen restylable, testable with plain props, and resilient to library updates. When and how to wrap the library's primitives
|
|
47
|
+
in app-level dumb components is [library-boundary.md](library-boundary.md).
|
|
49
48
|
|
|
50
49
|
## Compose, don't configure
|
|
51
50
|
|
|
@@ -56,15 +55,18 @@ components. When state must be shared between siblings, lift it to the nearest c
|
|
|
56
55
|
|
|
57
56
|
## Put state where its truth lives
|
|
58
57
|
|
|
59
|
-
Work down this list and stop at the first match:
|
|
58
|
+
State comes in kinds, and each kind has an owner. Work down this list and stop at the first match:
|
|
60
59
|
|
|
61
|
-
1. **
|
|
62
|
-
features you get for free.
|
|
63
|
-
2. **
|
|
64
|
-
that's how stale-UI bugs are born.
|
|
65
|
-
3. **
|
|
66
|
-
4. **
|
|
67
|
-
5. **Local
|
|
60
|
+
1. **URL state** — shareable via the address bar (filters, sort, pagination, active tab) → TanStack Router search params.
|
|
61
|
+
Back button and copied links are UX features you get for free.
|
|
62
|
+
2. **Server state** — comes from an API → TanStack Query's cache ([data.md](data.md)). Never copy server data into `useState`
|
|
63
|
+
— that's how stale-UI bugs are born.
|
|
64
|
+
3. **Subtree state** — scoped to a subtree, resets on leave (wizard progress) → React context.
|
|
65
|
+
4. **Client state** — app-wide and persistent → TanStack Store, and only now.
|
|
66
|
+
5. **Local state** — one component's own (input value, open/closed) → `useState`.
|
|
67
|
+
|
|
68
|
+
`useState` is the right tool for local UI state, and most state is local: whether a panel is open, which tab is active, a
|
|
69
|
+
draft value being typed, a hover flag. Keep those in the component and don't reach for a library.
|
|
68
70
|
|
|
69
71
|
Most frontends need far less of tier 4 than they think; tiers 1–2 usually dissolve the "we need a store" instinct. For
|
|
70
72
|
structuring the state itself, [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure) is the
|
|
@@ -82,6 +84,18 @@ When an effect is genuinely needed, extract it into a custom hook named for its
|
|
|
82
84
|
hook isolates the dependency array, and the component body stays declarative. Pattern reference:
|
|
83
85
|
[Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks).
|
|
84
86
|
|
|
87
|
+
The naming rule holds even for the rare effect that stays inline: give the callback a name, so the intent survives without a
|
|
88
|
+
comment —
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
useEffect(
|
|
92
|
+
function syncDocumentTitle() {
|
|
93
|
+
document.title = title;
|
|
94
|
+
},
|
|
95
|
+
[title]
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
85
99
|
## Reading list
|
|
86
100
|
|
|
87
101
|
Consult while building; each is the authority for its topic:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Stack
|
|
2
|
+
|
|
3
|
+
The topic→tool map for an ImFusion frontend. Check this before adding a dependency; check the TanStack suite before adding a
|
|
4
|
+
non-TanStack one.
|
|
5
|
+
|
|
6
|
+
| Topic | Tool |
|
|
7
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| Build | [Vite](https://vite.dev) |
|
|
9
|
+
| Routing, URL state | [TanStack Router](https://tanstack.com/router) — file-based, type-safe search params |
|
|
10
|
+
| Server state | [TanStack Query](https://tanstack.com/query) — the pattern around it is [data.md](data.md) |
|
|
11
|
+
| Forms | [TanStack Form](https://tanstack.com/form) |
|
|
12
|
+
| Data grids | [TanStack Table](https://tanstack.com/table) + web-ui's styled `Table` parts |
|
|
13
|
+
| App-wide client state | [TanStack Store](https://tanstack.com/store) — last resort in the state ladder ([react.md](react.md)) |
|
|
14
|
+
| Schema validation | [Zod](https://zod.dev) — at the network boundary ([data.md](data.md)) |
|
|
15
|
+
| Styling | CSS Modules ([styling.md](styling.md)) |
|
|
16
|
+
| Test | [Vitest](https://vitest.dev) |
|
|
17
|
+
| Format | [Prettier](https://prettier.io) — values in [tooling.md](tooling.md) |
|
|
18
|
+
| Lint | [ESLint](https://eslint.org) flat config ([tooling.md](tooling.md)) |
|
|
19
|
+
| Types | `tsc --noEmit` — own script, own CI step |
|
|
20
|
+
| Staged files | [lint-staged](https://github.com/lint-staged/lint-staged) (or nano-staged, drop-in) |
|
|
21
|
+
| Dead code | [knip](https://knipjs.dev) — needs per-repo config |
|
|
22
|
+
|
|
23
|
+
## Devtools come with the library
|
|
24
|
+
|
|
25
|
+
Every TanStack library that ships a devtools package gets it as a dev dependency, mounted in development only. Router is a
|
|
26
|
+
given, so `@tanstack/react-router-devtools` is a given too; Query's goes in when Query does. Look for a `-devtools` sibling
|
|
27
|
+
whenever you add a TanStack dependency — the set grows, and not every library has one yet. Once a project has several,
|
|
28
|
+
`@tanstack/devtools` hosts them in one panel.
|
|
29
|
+
|
|
30
|
+
## When a library owns a layer
|
|
31
|
+
|
|
32
|
+
A library owns the layer once you find yourself rebuilding what it does: validation timing and cross-field rules (Form),
|
|
33
|
+
caching and refetching (Query), URL as the source of truth (Router), sorting and pagination over rows (Table). Adding the
|
|
34
|
+
library mid-project is normal and cheap; unpicking a hand-rolled version later is not.
|
|
35
|
+
|
|
36
|
+
## Docs over memory
|
|
37
|
+
|
|
38
|
+
`npx @tanstack/cli` for TanStack docs — never work from memory. Where a project has wired up `@tanstack/intent`, use it to
|
|
39
|
+
reach the Agent Skills its TanStack dependencies ship, and read those too.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Styling
|
|
2
|
+
|
|
3
|
+
**CSS Modules with native CSS by default** — nesting and custom properties are the DRY mechanism; no Sass, no CSS-in-JS, no
|
|
4
|
+
utility-class framework. Where the stylesheet lives and who owns one is [components.md](components.md).
|
|
5
|
+
|
|
6
|
+
- **Nest pseudo-elements, states, and child selectors under the root** so a selector prefix is written once:
|
|
7
|
+
|
|
8
|
+
```css
|
|
9
|
+
.root {
|
|
10
|
+
transition: clip-path var(--ease);
|
|
11
|
+
&:focus-visible {
|
|
12
|
+
outline: var(--imf-ui-border-size-2) solid var(--imf-ui-color-fg-support);
|
|
13
|
+
}
|
|
14
|
+
&[data-disabled] {
|
|
15
|
+
opacity: 0.5;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **Lift a repeated literal into a custom property** (an easing, a colour-math result) and reference it. Custom properties
|
|
21
|
+
also carry per-state/per-variant values down the tree — the parameterisation a mixin would provide, done natively.
|
|
22
|
+
- Don't "merge" rules that only look similar. Structurally different output (three distinct `clip-path` polygons) isn't
|
|
23
|
+
repetition a mixin can remove. Keep it explicit.
|
|
24
|
+
|
|
25
|
+
## Build custom UI from tokens
|
|
26
|
+
|
|
27
|
+
Anything you build that the library doesn't cover — a stat widget, a custom panel — uses `--imf-ui-*` variables for color,
|
|
28
|
+
spacing, radius, and type instead of hardcoded values. That's what makes custom UI look native next to library components,
|
|
29
|
+
and what keeps it correct when the theme changes. A hex code or a magic `px` next to a concept the tokens already name is a
|
|
30
|
+
defect.
|
|
31
|
+
|
|
32
|
+
Geometry that can't be a token (a clip-path percentage, a hairline `1px`) lives in a named custom property at the top of the
|
|
33
|
+
stylesheet, with the invariant written next to it. Don't silently approximate to the nearest token.
|
|
34
|
+
|
|
35
|
+
## Override through the sanctioned seams
|
|
36
|
+
|
|
37
|
+
All library styles live in the `imf-ui.components` CSS layer, so **any plain selector you write wins** — that's the whole
|
|
38
|
+
override contract:
|
|
39
|
+
|
|
40
|
+
- Target the stable hooks: `data-imf-ui-component` attributes and your own classes/wrappers.
|
|
41
|
+
- Never target the library's internal class names — they are generated and change without notice.
|
|
42
|
+
- Never `!important` — if you think you need it, you're targeting the wrong thing.
|
|
43
|
+
- **Style against state via data attributes** (`[data-checked]`, `[data-disabled]`, `[data-popup-open]`) — components expose
|
|
44
|
+
their state there, so you never maintain your own state classes. `className` is purely a styling surface:
|
|
45
|
+
|
|
46
|
+
```css
|
|
47
|
+
[data-imf-ui-component="Switch"][data-checked] {
|
|
48
|
+
outline: 2px solid var(--imf-ui-color-bg-positive);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## The color system
|
|
53
|
+
|
|
54
|
+
The color roles you can name: **surfaces** (`main` the canvas, `support` panels, `minor` popovers), **brand** (identity, full
|
|
55
|
+
saturation) vs **primary** (contrast-tuned, CTAs — distinct roles on purpose), **status** (`negative`, `warning`, `positive`,
|
|
56
|
+
`info`), and three **accent** slots.
|
|
57
|
+
|
|
58
|
+
Two layers:
|
|
59
|
+
|
|
60
|
+
- **Controls** are the 80/20 customization surface — override one and every derived token shifts:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
:root {
|
|
64
|
+
--imf-ui-color-primary-hue: 30; /* every primary semantic token follows */
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Semantic tokens** are what you consume in your own CSS: `--imf-ui-color-bg-{name}` per role, a flat
|
|
69
|
+
`--imf-ui-color-fg-{main|support|minor|oncolor|…}` ladder for text. Borders and rings draw from the `fg-*` space.
|
|
70
|
+
|
|
71
|
+
Rules of thumb: if you push a hue control into a pale corner, you own overriding the matching `fg-*` token; the color scheme
|
|
72
|
+
is `<html data-imf-ui-color-scheme="light|dark">` (the provider sets it) — scheme-specific styling selects via that
|
|
73
|
+
attribute.
|
|
74
|
+
|
|
75
|
+
## Responsive styling
|
|
76
|
+
|
|
77
|
+
- **Tune components per breakpoint by overriding their `--imf-ui-*` variables inside your own media queries** — custom
|
|
78
|
+
properties cross the `@media` boundary, props don't. That's why components expose sizing as variables instead of a `width`
|
|
79
|
+
prop:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
@media (min-width: 768px) {
|
|
83
|
+
.my-shell {
|
|
84
|
+
--imf-ui-appshell-navbar-width: 22rem;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Write literal breakpoint values.** The library's `@custom-media` aliases are build-internal and don't ship.
|
|
90
|
+
- **JS only for behaviour** (render a burger menu on mobile): `useMediaQuery(minWidth("md"))`. Never for styling CSS can do.
|
|
91
|
+
- **No responsive props.** `size={{ sm: … }}` objects are deliberately not offered — write the media query.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Test the **decisions**, not the rendering.
|
|
4
|
+
|
|
5
|
+
- **Pure logic gets unit tests** — pricing, permissions, date math, parsing, reducers. These are cheap, fast, and the place
|
|
6
|
+
bugs actually hide.
|
|
7
|
+
- **Presentational components generally don't.** A component that maps props onto web-ui primitives has no logic of its own;
|
|
8
|
+
asserting that it rendered a `<Button>` tests React, not your code.
|
|
9
|
+
- **Behaviour a user performs gets an interaction test** — a form that validates, a flow with steps. Test it through the
|
|
10
|
+
interface the user has (roles, labels, visible text), not through internals.
|
|
11
|
+
- **Extract the decision out of a hook and test that.** A hook whose interesting part is a plain function is easier to test
|
|
12
|
+
as a plain function than through a render harness. A hook that only wraps a browser API has no decision to extract.
|
|
13
|
+
|
|
14
|
+
The data layer has its own recipe — stub `fetch`, run the real query client — in [data.md](data.md).
|
|
15
|
+
|
|
16
|
+
The measure isn't coverage percentage. It's whether a test failing tells you something you didn't already know.
|