@imfusion/web-ui 0.6.1-dev.27.gfc6e5abb → 0.6.1-dev.3.g5b432448
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 +169 -102
- package/dist/{code-C_56u-Vk.js → code-Blo48PGr.js} +2 -2
- package/dist/components/stack/stack.d.ts +1 -1
- package/dist/icons/icon-config-provider.d.ts +8 -0
- package/dist/icons/icon-context.d.ts +4 -0
- package/dist/{icons-Cy1HAosO.js → icons-wBmF0U2x.js} +1 -1
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +830 -1009
- package/dist/integrations/code-highlight/highlighter.d.ts +0 -24
- package/dist/integrations/code-highlight.js +47 -80
- package/dist/integrations/image-display-options.js +2 -2
- package/dist/provider/web-ui-provider.d.ts +3 -3
- package/dist/style.css +1 -1
- package/dist/{tabs-DIe1Utiy.js → tabs-CMKvMF4E.js} +0 -2
- package/package.json +4 -5
- package/src/docgen/doc.gen.json +1 -389
- package/src/llms/install-templates/AGENTS.md +18 -15
- package/src/llms/llms.gen.txt +33 -39
- package/src/llms/skills/imf-web-ui/SKILL.md +39 -30
- package/src/llms/skills/imf-web-ui-audit/SKILL.md +102 -50
- package/src/llms/skills/imf-web-ui-components/SKILL.md +104 -47
- package/src/llms/skills/imf-web-ui-conventions/SKILL.md +52 -44
- package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +0 -1
- package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +62 -40
- package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +12 -11
- package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +46 -31
- package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +23 -18
- package/src/llms/skills/imf-web-ui-conventions/topics/components.md +69 -20
- package/src/llms/skills/imf-web-ui-conventions/topics/data.md +146 -50
- package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +23 -17
- package/src/llms/skills/imf-web-ui-conventions/topics/git.md +20 -15
- package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +19 -28
- package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +16 -20
- package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +42 -28
- package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +30 -28
- package/src/llms/skills/imf-web-ui-conventions/topics/react.md +74 -28
- package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +62 -65
- package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +14 -12
- package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +4 -9
- package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +68 -40
- package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +50 -26
- package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +25 -19
- package/src/llms/skills/imf-web-ui-setup/SKILL.md +64 -45
- package/src/llms/skills/imf-web-ui-update/SKILL.md +114 -48
- package/src/llms/skills/imf-web-ui-ux/SKILL.md +92 -64
- package/src/llms/skills/imf-web-ui-ux/references/forms.md +36 -16
- package/src/llms/skills/imf-web-ui-ux/references/usability-heuristics.md +27 -14
- package/src/llms/skills/imf-web-ui-ux/references/visual-design.md +38 -22
- package/src/llms/tokens.gen.json +5 -5
- package/dist/codegen/gen-code-highlight-theme.d.ts +0 -1
- package/dist/components/toast/index.d.ts +0 -2
- package/dist/components/toast/toast.d.ts +0 -200
- package/dist/components/toast/toast.meta.d.ts +0 -2
- package/dist/icons/icon-config.d.ts +0 -12
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
# Class names in components
|
|
2
2
|
|
|
3
|
-
|
|
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).
|
|
4
5
|
|
|
5
6
|
## CVA is the only tool
|
|
6
7
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
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.
|
|
12
15
|
|
|
13
16
|
```tsx
|
|
17
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
18
|
+
appearance?: "outline" | "solid";
|
|
19
|
+
inline?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const chip = cva(classes.root, {
|
|
15
23
|
variants: {
|
|
16
|
-
appearance: {
|
|
17
|
-
outline: classes.appearanceOutline,
|
|
18
|
-
solid: classes.appearanceSolid
|
|
19
|
-
},
|
|
24
|
+
appearance: { outline: classes.appearanceOutline, solid: classes.appearanceSolid },
|
|
20
25
|
inline: { false: null, true: classes.inline }
|
|
21
26
|
}
|
|
22
27
|
});
|
|
@@ -28,18 +33,18 @@ export function Chip({ appearance = "outline", inline = false, className, ...pro
|
|
|
28
33
|
|
|
29
34
|
## Merging `className`
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
35
42
|
|
|
36
43
|
```tsx
|
|
37
44
|
className={state => cx(classes.root, typeof className === "function" ? className(state) : className)}
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
A closed component should omit `className`, not accept and ignore it.
|
|
41
|
-
|
|
42
47
|
## Shared CVA modules
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
# Components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
How a component lives on disk and how dumb it stays. Roles, state, effects: [react.md](react.md); styling:
|
|
4
4
|
[styling.md](styling.md).
|
|
5
5
|
|
|
6
6
|
## As dumb as possible
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
8
|
+
- Props-fed, never self-fetching: no query hook, no store access, no route awareness — render what's given, report events
|
|
9
|
+
upward.
|
|
10
|
+
- Presentational logic (formatting a label, deriving a display state) is fine; owning data is not.
|
|
11
|
+
- Local UI state (`isOpen`, a draft value) is allowed; anything whose truth lives elsewhere is not ([react.md](react.md)).
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
|
|
14
|
+
// components/user-card/user-card.tsx
|
|
15
|
+
interface Props {
|
|
15
16
|
name: string;
|
|
16
17
|
onEdit: () => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export function UserCard({ name, onEdit }:
|
|
20
|
+
export function UserCard({ name, onEdit }: Props) {
|
|
20
21
|
return (
|
|
21
22
|
<Card.Root>
|
|
22
|
-
<Typo
|
|
23
|
+
<Typo>{name}</Typo>
|
|
23
24
|
<Button onClick={onEdit}>Edit</Button>
|
|
24
25
|
</Card.Root>
|
|
25
26
|
);
|
|
@@ -28,25 +29,73 @@ export function UserCard({ name, onEdit }: UserCardProps) {
|
|
|
28
29
|
|
|
29
30
|
## Grouping
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
33
35
|
|
|
34
36
|
## One file or a folder
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
- More than one file (sub-components, styles, tests) → a folder with an `index.ts` that only re-exports; single-file
|
|
39
|
+
components stay single files, directly in their group.
|
|
40
|
+
- Imports read `#/components/data-table`, so the inside can be restructured without touching call sites.
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
The full anatomy of a folder component:
|
|
43
|
+
|
|
44
|
+
```
|
|
40
45
|
components/data-table/
|
|
41
|
-
data-table.tsx
|
|
42
|
-
data-table.
|
|
43
|
-
data-table.
|
|
44
|
-
|
|
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
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
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
|
+
```
|
|
48
96
|
|
|
49
97
|
## Colocation
|
|
50
98
|
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
@@ -1,111 +1,207 @@
|
|
|
1
1
|
# Data
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The in-house pattern **for TanStack Query + Router**, the default stack ([tooling.md](tooling.md)); other data layers keep
|
|
4
|
+
the boundary principles — validate at the edge, errors as values — not this file layout.
|
|
5
5
|
|
|
6
6
|
## The shape
|
|
7
7
|
|
|
8
|
-
|
|
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.
|
|
9
18
|
|
|
10
19
|
```text
|
|
11
20
|
src/
|
|
12
21
|
api/
|
|
13
|
-
topics.ts
|
|
14
|
-
index.ts
|
|
22
|
+
topics.ts # one topic registry, reused by keys and namespaces
|
|
23
|
+
index.ts # topic barrels composed as api.<topic>
|
|
15
24
|
<topic>/
|
|
16
|
-
index.ts
|
|
17
|
-
keys.ts
|
|
18
|
-
queries.ts
|
|
19
|
-
mutations.ts
|
|
20
|
-
types.ts
|
|
21
|
-
types.test.ts
|
|
22
|
-
http/
|
|
23
|
-
lib/
|
|
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
|
|
24
33
|
```
|
|
25
34
|
|
|
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
|
-
|
|
29
35
|
## The network boundary validates
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
TypeScript type.
|
|
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).
|
|
33
39
|
|
|
34
40
|
## Errors are values
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
38
44
|
|
|
39
45
|
## Topic registry and key factory
|
|
40
46
|
|
|
41
|
-
|
|
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.
|
|
42
50
|
|
|
43
51
|
```ts
|
|
52
|
+
// api/topics.ts — one list of topic names, ApiTopic derived from it
|
|
44
53
|
export const API_TOPICS = { AUTH: "auth", USER: "user", ORDERS: "orders" } as const;
|
|
45
54
|
export type ApiTopic = (typeof API_TOPICS)[keyof typeof API_TOPICS];
|
|
46
|
-
type QueryKey<Topic extends ApiTopic> = readonly [Topic, ...ReadonlyArray<unknown>] | readonly [Topic];
|
|
55
|
+
export type QueryKey<Topic extends ApiTopic> = readonly [Topic, ...ReadonlyArray<unknown>] | readonly [Topic];
|
|
56
|
+
export type QueryKeyFactory<Topic extends ApiTopic> = (...args: never[]) => QueryKey<Topic>;
|
|
47
57
|
|
|
48
58
|
export function createApiKeys<
|
|
49
59
|
Topic extends ApiTopic,
|
|
50
|
-
QueryKeys extends Record<string, QueryKey<Topic> |
|
|
60
|
+
QueryKeys extends Record<string, QueryKey<Topic> | QueryKeyFactory<Topic>>,
|
|
51
61
|
MutationKeys extends Record<string, QueryKey<Topic>>
|
|
52
62
|
>(topic: Topic, keys: { queries: QueryKeys; mutations: MutationKeys }) {
|
|
53
63
|
return { topic: [topic] as const, queries: keys.queries, mutations: keys.mutations };
|
|
54
64
|
}
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
Use the factory in each topic so every key starts with that topic:
|
|
58
|
-
|
|
59
67
|
```ts
|
|
68
|
+
// api/user/keys.ts — createApiKeys enforces topic-first
|
|
69
|
+
import { API_TOPICS, createApiKeys } from "../topics";
|
|
70
|
+
|
|
60
71
|
export const keys = createApiKeys(API_TOPICS.USER, {
|
|
61
|
-
queries: {
|
|
62
|
-
|
|
63
|
-
byId: (id: string) => [API_TOPICS.USER, "detail", id]
|
|
64
|
-
},
|
|
65
|
-
mutations: { update: [API_TOPICS.USER, "update"] }
|
|
72
|
+
queries: { details: ["user", "details"], byId: (id: string) => ["user", "detail", id] },
|
|
73
|
+
mutations: { update: ["user", "update"] }
|
|
66
74
|
});
|
|
75
|
+
|
|
76
|
+
// api/user/index.ts — the topic barrel
|
|
77
|
+
export * from "./keys";
|
|
78
|
+
export * from "./queries";
|
|
79
|
+
export * from "./mutations";
|
|
67
80
|
```
|
|
68
81
|
|
|
69
|
-
|
|
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
|
+
```
|
|
70
97
|
|
|
71
98
|
## Options factories and naming
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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).
|
|
77
106
|
|
|
78
107
|
```ts
|
|
79
|
-
|
|
108
|
+
// api/user/queries.ts — returns options; keys come from the factory
|
|
109
|
+
import { queryOptions, type QueryOptions } from "@tanstack/react-query";
|
|
110
|
+
import { bffClient } from "../http/bff-client";
|
|
111
|
+
import { userSchema, type User } from "./types";
|
|
112
|
+
import { keys } from "./keys";
|
|
113
|
+
|
|
114
|
+
export function getDetails(args: { options?: Omit<QueryOptions<User>, "queryKey" | "queryFn"> } = {}) {
|
|
80
115
|
return queryOptions({
|
|
81
|
-
...options,
|
|
116
|
+
...args.options,
|
|
82
117
|
queryKey: keys.queries.details,
|
|
83
118
|
queryFn: async () => userSchema.parse(await bffClient("/me"))
|
|
84
119
|
});
|
|
85
120
|
}
|
|
86
121
|
```
|
|
87
122
|
|
|
88
|
-
|
|
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
|
+
```
|
|
89
139
|
|
|
90
140
|
## Router context access
|
|
91
141
|
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
94
146
|
|
|
95
|
-
|
|
147
|
+
```tsx
|
|
148
|
+
// routes/__root.tsx
|
|
149
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
150
|
+
import type { API } from "../api";
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
152
|
+
interface RouterContext {
|
|
153
|
+
api: API;
|
|
154
|
+
queryClient: QueryClient;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// router.tsx
|
|
160
|
+
import { createRouter } from "@tanstack/react-router";
|
|
161
|
+
import { api } from "./api";
|
|
162
|
+
import { createQueryClient } from "./query-client";
|
|
163
|
+
|
|
164
|
+
const queryClient = createQueryClient();
|
|
165
|
+
const router = createRouter({ routeTree, context: { api, queryClient } });
|
|
166
|
+
router.update({ context: { api, queryClient } });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// routes/user.tsx
|
|
171
|
+
export const Route = createFileRoute("/user")({
|
|
172
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(context.api.user.getDetails()),
|
|
173
|
+
component: UserPage
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function UserPage() {
|
|
177
|
+
const context = Route.useRouteContext();
|
|
178
|
+
const user = useSuspenseQuery(context.api.user.getDetails());
|
|
179
|
+
const updateUser = useMutation(
|
|
180
|
+
context.api.user.update({
|
|
181
|
+
onSuccess: () => context.queryClient.invalidateQueries({ queryKey: context.api.orders.keys.topic })
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
return <input defaultValue={user.data.name} onBlur={e => updateUser.mutate({ name: e.target.value })} />;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
100
187
|
|
|
101
|
-
|
|
188
|
+
## Automatic invalidation
|
|
102
189
|
|
|
103
|
-
|
|
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).
|
|
104
197
|
|
|
105
198
|
```ts
|
|
199
|
+
// query-client.ts
|
|
106
200
|
export function createQueryClient() {
|
|
107
201
|
const queryClient = new QueryClient({
|
|
108
|
-
defaultOptions: {
|
|
202
|
+
defaultOptions: {
|
|
203
|
+
mutations: { meta: { autoInvalidate: true } }
|
|
204
|
+
},
|
|
109
205
|
mutationCache: new MutationCache({
|
|
110
206
|
onSuccess: async (_data, _variables, _context, mutation) => {
|
|
111
207
|
if (!mutation.meta?.autoInvalidate) return;
|
|
@@ -120,6 +216,6 @@ export function createQueryClient() {
|
|
|
120
216
|
|
|
121
217
|
## Testing the data layer
|
|
122
218
|
|
|
123
|
-
Stub
|
|
124
|
-
|
|
125
|
-
[testing.md](testing.md).
|
|
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).
|
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
# Documentation structure
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
documentation. The setup skill can propose the shape; the audit skill can check it.
|
|
3
|
+
What a repo documents, where, and how. `imf-web-ui-setup` scaffolds this shape from its `templates/` and audits it.
|
|
5
4
|
|
|
6
5
|
## The shape
|
|
7
6
|
|
|
8
|
-
```
|
|
7
|
+
```
|
|
9
8
|
<app>/
|
|
10
|
-
README.md
|
|
11
|
-
AGENTS.md
|
|
9
|
+
README.md # humans: what the app is, setup, scripts
|
|
10
|
+
AGENTS.md # agents: tooling and architecture pointers into docs/ — restates nothing
|
|
12
11
|
docs/
|
|
13
|
-
index.md
|
|
14
|
-
<topic>.md
|
|
12
|
+
index.md # registers every doc with a one-line "covers" summary
|
|
13
|
+
<topic>.md # repo-unique content only
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
## Repo docs hold only what is unique to the repo
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
21
22
|
|
|
22
23
|
## How docs are written
|
|
23
24
|
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
30
35
|
|
|
31
36
|
## Staleness at commit time
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
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).
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
# Git
|
|
2
2
|
|
|
3
|
-
Use the repository's tracked configuration and keep verification scopes clear.
|
|
4
|
-
|
|
5
3
|
## git:config
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
- One script holds the repo's git configuration, run by hand once per clone (the README names it):
|
|
8
6
|
|
|
9
|
-
```
|
|
10
|
-
git config core.hooksPath .githooks
|
|
11
|
-
git config pull.rebase true
|
|
12
|
-
git config merge.ff only
|
|
7
|
+
```
|
|
8
|
+
git config core.hooksPath .githooks && git config pull.rebase true && git config merge.ff only
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
|
|
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.
|
|
16
14
|
|
|
17
15
|
## Verify scopes
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Two scopes, both blocking:
|
|
20
18
|
|
|
21
|
-
- **staged
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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".
|
|
25
26
|
|
|
26
27
|
## Staleness at commit time
|
|
27
28
|
|
|
28
|
-
Pre-commit also runs advisory checks
|
|
29
|
-
|
|
29
|
+
Pre-commit also runs the advisory staleness checks — warn, never block:
|
|
30
|
+
|
|
31
|
+
- **Vendored baseline** — `.agents/hooks/imf-web-ui/baseline-staleness.sh` compares installed `imf-web-ui-*` skill markers
|
|
32
|
+
against the installed package version; the fix it names is `npx web-ui-install`.
|
|
33
|
+
- **Docs** — a staged change that invalidates a doc updates it in the same commit ([docs-structure.md](docs-structure.md)).
|
|
34
|
+
An agent commit workflow, where the repo has one, carries a staged docs audit.
|
|
@@ -1,44 +1,35 @@
|
|
|
1
1
|
# Library boundary
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
integration seams.
|
|
3
|
+
The contract between an app and `@imfusion/web-ui`.
|
|
5
4
|
|
|
6
5
|
## Stay behind the library
|
|
7
6
|
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
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.
|
|
11
12
|
|
|
12
13
|
## Wrap primitives when the app has a reason to
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
restriction
|
|
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.
|
|
16
22
|
|
|
17
|
-
Derive
|
|
23
|
+
## Derive types, don't import them
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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.
|
|
35
27
|
|
|
36
28
|
## Integrations own their peers
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
`package.json
|
|
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.
|
|
40
32
|
|
|
41
33
|
## Styling crosses the boundary through seams
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
[styling.md](styling.md).
|
|
35
|
+
- Tokens in, sanctioned selectors at the edge, never the library's internals — the full contract is [styling.md](styling.md).
|