@imfusion/web-ui 0.6.1-dev.9.g317bd6f2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE.txt +30 -0
  2. package/README.md +104 -172
  3. package/THIRD_PARTY_NOTICES.md +34 -0
  4. package/bin/install.js +28 -10
  5. package/dist/code-BFMQnmu9.js +147 -0
  6. package/dist/codegen/gen-code-highlight-theme.d.ts +1 -0
  7. package/dist/components/code/code.d.ts +5 -4
  8. package/dist/components/stack/stack.d.ts +1 -1
  9. package/dist/components/toast/index.d.ts +2 -0
  10. package/dist/components/toast/toast.d.ts +200 -0
  11. package/dist/components/toast/toast.meta.d.ts +2 -0
  12. package/dist/components/typo/typo.d.ts +23 -22
  13. package/dist/icons/icon-config.d.ts +12 -0
  14. package/dist/{icons-wBmF0U2x.js → icons-Cy1HAosO.js} +1 -1
  15. package/dist/icons.js +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1278 -1069
  18. package/dist/integrations/code-highlight/highlighter.d.ts +24 -0
  19. package/dist/integrations/code-highlight.js +80 -47
  20. package/dist/integrations/image-display-options.js +2 -2
  21. package/dist/provider/web-ui-provider.d.ts +3 -3
  22. package/dist/style.css +1 -1
  23. package/dist/{tabs-CMKvMF4E.js → tabs-DIe1Utiy.js} +2 -0
  24. package/docs/assets/imfusion-banner.svg +16 -0
  25. package/package.json +14 -7
  26. package/src/docgen/doc.gen.json +515 -1
  27. package/src/llms/install-templates/AGENTS.md +15 -18
  28. package/src/llms/llms.gen.txt +39 -33
  29. package/src/llms/skills/imf-web-ui/SKILL.md +30 -39
  30. package/src/llms/skills/imf-web-ui-audit/SKILL.md +50 -102
  31. package/src/llms/skills/imf-web-ui-components/SKILL.md +47 -104
  32. package/src/llms/skills/imf-web-ui-conventions/SKILL.md +44 -52
  33. package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +1 -0
  34. package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +40 -62
  35. package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +11 -12
  36. package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +31 -46
  37. package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +18 -23
  38. package/src/llms/skills/imf-web-ui-conventions/topics/components.md +20 -69
  39. package/src/llms/skills/imf-web-ui-conventions/topics/data.md +50 -146
  40. package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +17 -23
  41. package/src/llms/skills/imf-web-ui-conventions/topics/git.md +15 -20
  42. package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +28 -19
  43. package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +20 -16
  44. package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +28 -42
  45. package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +28 -30
  46. package/src/llms/skills/imf-web-ui-conventions/topics/react.md +28 -74
  47. package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +65 -62
  48. package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +12 -14
  49. package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +9 -4
  50. package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +40 -68
  51. package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +26 -50
  52. package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +19 -25
  53. package/src/llms/skills/imf-web-ui-setup/SKILL.md +45 -64
  54. package/src/llms/skills/imf-web-ui-update/SKILL.md +48 -114
  55. package/src/llms/skills/imf-web-ui-ux/SKILL.md +64 -92
  56. package/src/llms/skills/imf-web-ui-ux/references/forms.md +16 -36
  57. package/src/llms/skills/imf-web-ui-ux/references/usability-heuristics.md +14 -27
  58. package/src/llms/skills/imf-web-ui-ux/references/visual-design.md +22 -38
  59. package/src/llms/tokens.gen.json +5 -5
  60. package/bin/install.test.ts +0 -329
  61. package/dist/code-Blo48PGr.js +0 -136
  62. package/dist/icons/icon-config-provider.d.ts +0 -8
  63. package/dist/icons/icon-context.d.ts +0 -4
@@ -1,27 +1,22 @@
1
1
  # Class names in components
2
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).
3
+ Use CVA to turn design props into CSS Module classes, then merge the caller's `className`.
5
4
 
6
5
  ## CVA is the only tool
7
6
 
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.
7
+ - Use `class-variance-authority` for variant axes. Do not add `clsx`, `tailwind-merge`, or a local `cn` helper.
8
+ - Import `cx` from `@imfusion/web-ui` when a component only needs concatenation.
9
+ - Pass CSS Module references to CVA, not string class names.
10
+ - Represent a boolean axis with `false: null` and `true: classes.axisOn`.
11
+ - Put defaults in function destructuring. CVA `defaultVariants` does not reach the generated docs.
15
12
 
16
13
  ```tsx
17
- interface Props extends HTMLAttributes<HTMLSpanElement> {
18
- appearance?: "outline" | "solid";
19
- inline?: boolean;
20
- }
21
-
22
14
  const chip = cva(classes.root, {
23
15
  variants: {
24
- appearance: { outline: classes.appearanceOutline, solid: classes.appearanceSolid },
16
+ appearance: {
17
+ outline: classes.appearanceOutline,
18
+ solid: classes.appearanceSolid
19
+ },
25
20
  inline: { false: null, true: classes.inline }
26
21
  }
27
22
  });
@@ -33,18 +28,18 @@ export function Chip({ appearance = "outline", inline = false, className, ...pro
33
28
 
34
29
  ## Merging `className`
35
30
 
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:
31
+ A component that accepts `className` must merge it. Put the incoming value in CVA's `className` slot so it is appended after
32
+ the component classes. If there is no CVA configuration, use `cx`.
33
+
34
+ A library render prop may make `className` a function. Resolve it with the render state before merging:
42
35
 
43
36
  ```tsx
44
37
  className={state => cx(classes.root, typeof className === "function" ? className(state) : className)}
45
38
  ```
46
39
 
40
+ A closed component should omit `className`, not accept and ignore it.
41
+
47
42
  ## Shared CVA modules
48
43
 
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.
44
+ When two components share the same visual variant axes, put their CVA call in a `{name}.cva.ts` module next to them. This
45
+ keeps the visual contract in one place.
@@ -1,26 +1,25 @@
1
1
  # Components
2
2
 
3
- How a component lives on disk and how dumb it stays. Roles, state, effects: [react.md](react.md); styling:
3
+ Keep consumer components small and easy to replace. React roles are described in [react.md](react.md); CSS ownership is in
4
4
  [styling.md](styling.md).
5
5
 
6
6
  ## As dumb as possible
7
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)).
8
+ - Feed components data and callbacks through props.
9
+ - Keep fetching, routing, and business logic in the page or feature layer.
10
+ - Local UI state such as an open state or draft value is fine.
11
+ - Formatting and display-only decisions are fine; owning server state is not.
12
12
 
13
13
  ```tsx
14
- // components/user-card/user-card.tsx
15
- interface Props {
14
+ interface UserCardProps {
16
15
  name: string;
17
16
  onEdit: () => void;
18
17
  }
19
18
 
20
- export function UserCard({ name, onEdit }: Props) {
19
+ export function UserCard({ name, onEdit }: UserCardProps) {
21
20
  return (
22
21
  <Card.Root>
23
- <Typo>{name}</Typo>
22
+ <Typo.P>{name}</Typo.P>
24
23
  <Button onClick={onEdit}>Edit</Button>
25
24
  </Card.Root>
26
25
  );
@@ -29,73 +28,25 @@ export function UserCard({ name, onEdit }: Props) {
29
28
 
30
29
  ## Grouping
31
30
 
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.
31
+ Group by component role, such as layout or domain UI, according to the project structure. Do not create a hierarchy before
32
+ the project needs one.
35
33
 
36
34
  ## One file or a folder
37
35
 
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.
36
+ A one-file component can stay in its group. A component with parts, styles, or tests gets a folder with an `index.ts` barrel.
37
+ Call sites import the folder path:
41
38
 
42
- The full anatomy of a folder component:
43
-
44
- ```
39
+ ```text
45
40
  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
41
+ data-table.tsx
42
+ data-table.module.css
43
+ data-table.test.tsx
44
+ index.ts
51
45
  ```
52
46
 
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
- ```
47
+ The barrel only re-exports the public pieces.
96
48
 
97
49
  ## Colocation
98
50
 
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.
51
+ Keep a component's stylesheet, tests, and local types beside the component. A container that needs styling usually wants to
52
+ be a layout or dumb component instead.
@@ -1,207 +1,111 @@
1
1
  # Data
2
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.
3
+ This topic defines the ImFusion pattern for TanStack Query and TanStack Router. Other data libraries keep the same boundary
4
+ rules: validate external data at the edge and represent errors as values.
5
5
 
6
6
  ## The shape
7
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.
8
+ Use one folder per API topic:
18
9
 
19
10
  ```text
20
11
  src/
21
12
  api/
22
- topics.ts # one topic registry, reused by keys and namespaces
23
- index.ts # topic barrels composed as api.<topic>
13
+ topics.ts
14
+ index.ts
24
15
  <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
16
+ index.ts
17
+ keys.ts
18
+ queries.ts # when the topic has queries
19
+ mutations.ts # when it has mutations
20
+ types.ts # schemas and inferred types
21
+ types.test.ts # when schema rules encode behavior
22
+ http/ # transport and error normalization
23
+ lib/ # framework-free shared helpers
33
24
  ```
34
25
 
26
+ `keys.ts` owns query and mutation keys. Topic functions return options, not hooks or data. Keep transport code in `http/` and
27
+ keep topic helpers out of the API folder when they are shared and framework-free.
28
+
35
29
  ## The network boundary validates
36
30
 
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).
31
+ The transport returns `unknown`. The topic schema parses it in the query or mutation function, and `z.infer` supplies the
32
+ TypeScript type. A handwritten generic on `fetch` is not validation. See [validation.md](validation.md).
39
33
 
40
34
  ## Errors are values
41
35
 
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.
36
+ Normalize failed requests into an `ApiError` with a machine-readable code. The UI maps the code to user-facing text instead
37
+ of handling arbitrary thrown shapes at every call site.
44
38
 
45
39
  ## Topic registry and key factory
46
40
 
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.
41
+ Keep one registry of topic names. Use it for API namespaces and as the first segment of every query and mutation key:
50
42
 
51
43
  ```ts
52
- // api/topics.ts — one list of topic names, ApiTopic derived from it
53
44
  export const API_TOPICS = { AUTH: "auth", USER: "user", ORDERS: "orders" } as const;
54
45
  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>;
46
+ type QueryKey<Topic extends ApiTopic> = readonly [Topic, ...ReadonlyArray<unknown>] | readonly [Topic];
57
47
 
58
48
  export function createApiKeys<
59
49
  Topic extends ApiTopic,
60
- QueryKeys extends Record<string, QueryKey<Topic> | QueryKeyFactory<Topic>>,
50
+ QueryKeys extends Record<string, QueryKey<Topic> | ((...args: never[]) => QueryKey<Topic>)>,
61
51
  MutationKeys extends Record<string, QueryKey<Topic>>
62
52
  >(topic: Topic, keys: { queries: QueryKeys; mutations: MutationKeys }) {
63
53
  return { topic: [topic] as const, queries: keys.queries, mutations: keys.mutations };
64
54
  }
65
55
  ```
66
56
 
67
- ```ts
68
- // api/user/keys.ts — createApiKeys enforces topic-first
69
- import { API_TOPICS, createApiKeys } from "../topics";
57
+ Use the factory in each topic so every key starts with that topic:
70
58
 
59
+ ```ts
71
60
  export const keys = createApiKeys(API_TOPICS.USER, {
72
- queries: { details: ["user", "details"], byId: (id: string) => ["user", "detail", id] },
73
- mutations: { update: ["user", "update"] }
61
+ queries: {
62
+ details: [API_TOPICS.USER, "details"],
63
+ byId: (id: string) => [API_TOPICS.USER, "detail", id]
64
+ },
65
+ mutations: { update: [API_TOPICS.USER, "update"] }
74
66
  });
75
-
76
- // api/user/index.ts — the topic barrel
77
- export * from "./keys";
78
- export * from "./queries";
79
- export * from "./mutations";
80
67
  ```
81
68
 
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
- ```
69
+ Cross-topic invalidation can then use `keys.topic` instead of rebuilding a prefix by hand.
97
70
 
98
71
  ## Options factories and naming
99
72
 
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).
73
+ Queries use `get*` names such as `getAll`, `getById`, and `getDetails`. Mutations use bare verbs such as `update` or
74
+ `delete`. Do not add a topic prefix or `Options` suffix; `context.api.user.getDetails()` already supplies the context.
106
75
 
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";
76
+ A query factory owns its key and parsing:
113
77
 
114
- export function getDetails(args: { options?: Omit<QueryOptions<User>, "queryKey" | "queryFn"> } = {}) {
78
+ ```ts
79
+ export function getDetails(options?: QueryOptions<User>) {
115
80
  return queryOptions({
116
- ...args.options,
81
+ ...options,
117
82
  queryKey: keys.queries.details,
118
83
  queryFn: async () => userSchema.parse(await bffClient("/me"))
119
84
  });
120
85
  }
121
86
  ```
122
87
 
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
- ```
88
+ Callers choose `ensureQueryData`, `useSuspenseQuery`, or another Query API.
139
89
 
140
90
  ## Router context access
141
91
 
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";
92
+ Put the API object and `QueryClient` in router context. Loaders use `context.api.<topic>.<fn>()`; route components read the
93
+ same context with `Route.useRouteContext()`. Direct imports are for plain utilities and tests outside the route tree.
151
94
 
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
- });
95
+ ## Automatic invalidation
175
96
 
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
- ```
97
+ Mutation keys start with the topic they change. The shared `MutationCache` invalidates that topic after success by default.
98
+ Use `meta: { autoInvalidate: false }` for a mutation where a topic-wide refetch is wrong, then invalidate exact keys
99
+ yourself.
187
100
 
188
- ## Automatic invalidation
101
+ For a related topic, add an explicit success handler that invalidates that topic's `keys.topic`.
189
102
 
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).
103
+ Configure the shared cache once, so the default applies to every mutation:
197
104
 
198
105
  ```ts
199
- // query-client.ts
200
106
  export function createQueryClient() {
201
107
  const queryClient = new QueryClient({
202
- defaultOptions: {
203
- mutations: { meta: { autoInvalidate: true } }
204
- },
108
+ defaultOptions: { mutations: { meta: { autoInvalidate: true } } },
205
109
  mutationCache: new MutationCache({
206
110
  onSuccess: async (_data, _variables, _context, mutation) => {
207
111
  if (!mutation.meta?.autoInvalidate) return;
@@ -216,6 +120,6 @@ export function createQueryClient() {
216
120
 
217
121
  ## Testing the data layer
218
122
 
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).
123
+ Stub `fetch` at the transport boundary and run the real query client. Test schemas, transport normalization, and query
124
+ options where they encode decisions. Schema cases belong beside the schema; the general test boundary is in
125
+ [testing.md](testing.md).
@@ -1,40 +1,34 @@
1
1
  # Documentation structure
2
2
 
3
- What a repo documents, where, and how. `imf-web-ui-setup` scaffolds this shape from its `templates/` and audits it.
3
+ Keep repository docs useful to a human who has just joined the project. Invoke `/documentation-writer` for new or updated
4
+ documentation. The setup skill can propose the shape; the audit skill can check it.
4
5
 
5
6
  ## The shape
6
7
 
7
- ```
8
+ ```text
8
9
  <app>/
9
- README.md # humans: what the app is, setup, scripts
10
- AGENTS.md # agents: tooling and architecture pointers into docs/ — restates nothing
10
+ README.md # what the app is, setup, and scripts
11
+ AGENTS.md # short pointers for agents
11
12
  docs/
12
- index.md # registers every doc with a one-line "covers" summary
13
- <topic>.md # repo-unique content only
13
+ index.md # index of repo-specific docs
14
+ <topic>.md
14
15
  ```
15
16
 
16
17
  ## Repo docs hold only what is unique to the repo
17
18
 
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.
19
+ If a rule should be true in every ImFusion frontend, keep it in the shipped conventions skill. Repo docs should explain local
20
+ decisions and deviations. A deviation names the baseline, the local choice, and why it exists.
22
21
 
23
22
  ## How docs are written
24
23
 
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.
24
+ - Give each page one reader goal.
25
+ - Use plain language and active verbs.
26
+ - Explain a reason when it helps someone make the right choice.
27
+ - Let code own changing facts such as scripts, types, token names, and config. Link to the source instead of copying it.
28
+ - State the current situation. History belongs in commits and tickets.
29
+ - Use examples that a reader could adapt, not placeholder prose.
35
30
 
36
31
  ## Staleness at commit time
37
32
 
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).
33
+ When a change makes a doc inaccurate, update the doc in the same commit. The repository's commit workflow runs the staged
34
+ docs audit; the check is advisory, but the correction belongs with the change.
@@ -1,34 +1,29 @@
1
1
  # Git
2
2
 
3
+ Use the repository's tracked configuration and keep verification scopes clear.
4
+
3
5
  ## git:config
4
6
 
5
- - One script holds the repo's git configuration, run by hand once per clone (the README names it):
7
+ Run once in a clone:
6
8
 
7
- ```
8
- git config core.hooksPath .githooks && git config pull.rebase true && git config merge.ff only
9
+ ```sh
10
+ git config core.hooksPath .githooks
11
+ git config pull.rebase true
12
+ git config merge.ff only
9
13
  ```
10
14
 
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.
15
+ The hooks are tracked in `.githooks/`. Check both the Git setting and the directory; a missing hook path fails silently.
14
16
 
15
17
  ## Verify scopes
16
18
 
17
- Two scopes, both blocking:
19
+ There are two scopes:
18
20
 
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".
21
+ - **staged**: `verify:staged`, run by pre-commit. It is fast and checks the staged files plus relevant project-wide checks.
22
+ - **full**: `verify:full`, the build and complete verification used by CI.
26
23
 
27
- ## Staleness at commit time
24
+ The scripts under `scripts/verify/` own the step lists. Hooks and npm scripts only launch them.
28
25
 
29
- Pre-commit also runs the advisory staleness checks — warn, never block:
26
+ ## Staleness at commit time
30
27
 
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.
28
+ Pre-commit also runs advisory checks for vendored skill markers and documentation drift. If a staged change makes a doc
29
+ stale, update the doc in the same commit. For an outdated installed skill, run `npx web-ui-install` in the consumer project.
@@ -1,35 +1,44 @@
1
1
  # Library boundary
2
2
 
3
- The contract between an app and `@imfusion/web-ui`.
3
+ Keep the application behind `@imfusion/web-ui`. The library owns the public component API, implementation packages, and
4
+ integration seams.
4
5
 
5
6
  ## Stay behind the library
6
7
 
7
- - Never import Base UI (or any other upstream the library wraps) directly — no upstream stylesheets, no upstream components,
8
- even when upstream docs show it that way: everything a component needs ships in `@imfusion/web-ui`.
9
- - Library missing something upstream has → report the gap (see `imf-web-ui-components`); don't reach around it.
10
- - Import icons from `@imfusion/web-ui/icons` and render them through its `Icon` component. Direct imports from
11
- `iconoir-react` are outside the library boundary.
8
+ - Do not import Base UI or another implementation package directly, including its stylesheet.
9
+ - If Web UI is missing a component or part, report the gap instead of reaching around it.
10
+ - Import glyphs from `@imfusion/web-ui/icons` and render them with `Icon`.
12
11
 
13
12
  ## Wrap primitives when the app has a reason to
14
13
 
15
- - Repeated adaptation around a primitive (default props, a styling override, a composition, an accessibility refinement, a
16
- restriction of the API) → wrap it once in an app-level dumb component in `components/` ([components.md](components.md)).
17
- - The wrapper derives its props from the primitive (`React.ComponentProps<typeof Button>`, narrowed or extended) and styles
18
- itself through the sanctioned seams — never the library's internals.
19
- - Wrap for a reason — any repeated adaptation counts; a wrapper that only renames a primitive is indirection with no payoff.
20
- - Components marked `experimental` in the identity index get wrapped **always**, even with nothing added yet — a breaking
21
- upstream change then lands in one file instead of every call site.
14
+ Create an app-level dumb wrapper when you repeat a default, composition, styling override, accessibility improvement, or API
15
+ restriction. A wrapper that only renames a component adds indirection without helping.
22
16
 
23
- ## Derive types, don't import them
17
+ Derive its props from the primitive:
24
18
 
25
- - Prop types come from the components themselves: `React.ComponentProps<typeof Button>`.
26
- - The library deliberately exports no `Props` types — don't look for them, don't re-declare prop shapes by hand.
19
+ ```tsx
20
+ type SaveButtonProps = React.ComponentProps<typeof Button> & {
21
+ busy?: boolean;
22
+ };
23
+ ```
24
+
25
+ Style the wrapper through tokens, your own classes, and the library's public data attributes. Do not target library CSS
26
+ Module classes.
27
+
28
+ An `experimental` primitive should be wrapped once before it is used throughout an app. That gives an API change one place to
29
+ land.
30
+
31
+ ## Derive types, don't copy them
32
+
33
+ The library intentionally does not export component `Props` interfaces. Use `React.ComponentProps<typeof Component>` and
34
+ narrow or extend that type locally.
27
35
 
28
36
  ## Integrations own their peers
29
37
 
30
- - `@imfusion/web-ui/integrations/*` components depend on optional peers (e.g. `@tanstack/highlight` for `CodeHighlight`). Add
31
- the peer explicitly to the consumer's `package.json` — never rely on hoisting.
38
+ An integration under `@imfusion/web-ui/integrations/*` has an optional peer. Add that peer explicitly to the consumer's
39
+ `package.json`; npm hoisting is not a dependency contract.
32
40
 
33
41
  ## Styling crosses the boundary through seams
34
42
 
35
- - Tokens in, sanctioned selectors at the edge, never the library's internals — the full contract is [styling.md](styling.md).
43
+ Use token overrides, your own classes, and `data-imf-ui-component` or state attributes. The complete CSS contract is in
44
+ [styling.md](styling.md).