@imfusion/web-ui 0.6.1-dev.27.gfc6e5abb → 0.6.1-dev.3.g8b2855c3
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,30 +1,26 @@
|
|
|
1
1
|
# Library setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Library wiring (styles import + `WebUIProvider`) for anyone using `@imfusion/web-ui` — `imf-web-ui-setup library-setup`
|
|
4
|
+
proposes a bootstrap change, `imf-web-ui-audit library-setup` is the read-only health check.
|
|
4
5
|
|
|
5
6
|
## Entry-point wiring
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
- Every consumer entry point has exactly two lines, in this order:
|
|
8
9
|
|
|
9
|
-
```tsx
|
|
10
|
-
import "@imfusion/web-ui/styles.css";
|
|
11
|
-
import {
|
|
10
|
+
```tsx
|
|
11
|
+
import "@imfusion/web-ui/styles.css";
|
|
12
|
+
import { WebUIProvider, Button } from "@imfusion/web-ui";
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Components outside the provider do not receive the library's provider context. Keep upstream imports behind the library; see
|
|
23
|
-
[library-boundary.md](library-boundary.md).
|
|
15
|
+
- Wrap the app root in `<WebUIProvider>` once — components rendered outside it lack the theme and CSS-variable context they
|
|
16
|
+
expect.
|
|
17
|
+
- Never import a Base UI (or other upstream) stylesheet or component directly — everything a web-ui component needs is inside
|
|
18
|
+
`styles.css` and the package's own exports. What may be reached for when the library lacks a component:
|
|
19
|
+
[library boundary](library-boundary.md).
|
|
24
20
|
|
|
25
21
|
## Symptoms of broken setup
|
|
26
22
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- An integration
|
|
30
|
-
`imf-web-ui-components` for
|
|
23
|
+
- Components render but look unstyled → the `styles.css` import is missing from the entry point.
|
|
24
|
+
- Components render but ignore the theme (wrong colors, CSS variables not resolving) → mounted outside `<WebUIProvider>`.
|
|
25
|
+
- An integration component throws on import → its optional peer dependency is not installed; check the component's
|
|
26
|
+
description in the docgen index (`imf-web-ui-components`) for which peer to add to `package.json`.
|
|
@@ -1,39 +1,53 @@
|
|
|
1
1
|
# npm project
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The npm side of an ImFusion frontend: `package.json`, scripts, dependencies. `imf-web-ui-setup` audits against this file.
|
|
4
4
|
|
|
5
5
|
## package.json
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
7
|
+
```jsonc
|
|
8
|
+
{
|
|
9
|
+
"name": "@imfusion/scan-review",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"private": true, // only when the package is not meant to be published
|
|
12
|
+
"engines": { "node": ">=22" },
|
|
13
|
+
"imports": { "#/*": "./src/*" }
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `"type": "module"` always.
|
|
18
|
+
- `"private": true` only for apps that never publish.
|
|
19
|
+
- Pin Node via `engines.node` or `.nvmrc` — never a personal version manager's config.
|
|
20
|
+
- Wire the `#/` alias through `imports` ([project-structure.md](project-structure.md)).
|
|
11
21
|
|
|
12
22
|
## Scripts
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
| Script |
|
|
17
|
-
| ------------------ |
|
|
18
|
-
| `dev` |
|
|
19
|
-
| `build` |
|
|
20
|
-
| `verify:
|
|
21
|
-
| `verify:
|
|
22
|
-
| `verify:
|
|
23
|
-
| `verify:
|
|
24
|
-
| `verify:
|
|
25
|
-
| `verify:
|
|
26
|
-
| `verify:
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
Same name, same meaning, every repo — "what can I run to check this?" is tab-completing `verify:`.
|
|
25
|
+
|
|
26
|
+
| Script | Runs |
|
|
27
|
+
| ------------------ | --------------------------------------------------------------------- |
|
|
28
|
+
| `dev` | dev server |
|
|
29
|
+
| `build` | production build |
|
|
30
|
+
| `verify:deps` | exact-pin check over `dependencies` and `devDependencies` |
|
|
31
|
+
| `verify:format` | `prettier --check .` |
|
|
32
|
+
| `verify:lint` | `eslint . --cache --max-warnings=0` |
|
|
33
|
+
| `verify:typecheck` | `tsc --noEmit` (or `tsc -b --noEmit` in a project-references setup) |
|
|
34
|
+
| `verify:tests` | `vitest run` |
|
|
35
|
+
| `verify:knip` | `knip` — dead-code and unused-export check |
|
|
36
|
+
| `verify:staged` | staged-file subset, called by the pre-commit hook |
|
|
37
|
+
| `verify:full` | every `verify:*` check plus the build; what CI runs |
|
|
38
|
+
| `format` | `prettier --write .` |
|
|
39
|
+
| `lint` | `eslint . --cache --fix` |
|
|
40
|
+
| `git:config` | see [git.md](git.md); run by hand once per clone, named in the README |
|
|
41
|
+
|
|
42
|
+
- **Every check is `verify:*`** — one namespace for everything that reads and reports.
|
|
43
|
+
- **Write-mode scripts keep the tool name** (`format`, `lint`): changing files isn't verifying — no prefix, no `:fix` suffix.
|
|
44
|
+
- `verify:staged` is fast and partial: staged-file format/lint plus the relevant project-wide checks for staged source or
|
|
45
|
+
config changes, including `verify:knip` — a passing commit is not CI green.
|
|
46
|
+
- `verify:full` is the CI gate.
|
|
32
47
|
|
|
33
48
|
## Dependencies
|
|
34
49
|
|
|
35
|
-
Pin
|
|
36
|
-
`.npmrc`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
scripts.
|
|
50
|
+
- **Pin exactly** — no `^`, `~`, or `latest`, in `dependencies` and `devDependencies` alike: `verify:deps` catches drift,
|
|
51
|
+
`save-exact=true` in `.npmrc` prevents it.
|
|
52
|
+
- **`ignore-scripts=true` in `.npmrc`** — blocks lifecycle scripts on install (the supply-chain vector); setup that matters
|
|
53
|
+
is a command someone runs, not a hook that fires on install.
|
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
# Project structure
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Kebab-case throughout, folders and files alike; exported symbols stay PascalCase — only the filename is kebab.
|
|
4
4
|
|
|
5
5
|
## Layout
|
|
6
6
|
|
|
7
|
-
```
|
|
7
|
+
```
|
|
8
8
|
src/
|
|
9
|
-
main.tsx
|
|
10
|
-
routes/
|
|
11
|
-
__root.tsx
|
|
12
|
-
_public/route
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
main.tsx # providers and router bootstrap; one WebUIProvider at the app root
|
|
10
|
+
routes/ # TanStack Router file-based routes; routing only — they compose, they don't fetch inline
|
|
11
|
+
__root.tsx # document-wide error/not-found boundary and Outlet
|
|
12
|
+
_public/ # anonymous route group; resolves the current user when public pages need it
|
|
13
|
+
route.tsx
|
|
14
|
+
_app/ # pathless authenticated route group; guards the subtree before children render
|
|
15
|
+
route.tsx
|
|
16
|
+
api/ # one folder per API topic — layout and behaviour in data.md
|
|
17
|
+
auth/ # one current-user query plus server-owned login/logout URL helpers
|
|
18
|
+
components/ # grouped by kind — anatomy in components.md
|
|
19
|
+
app-shell/ # optional persistent authenticated chrome: brand, navigation, and session action
|
|
20
|
+
http/ # transport: client, error normalisation — the only transport-aware place (data.md)
|
|
21
|
+
lib/ # framework-free helpers, each with a colocated .test.ts when it has decisions to test
|
|
22
|
+
auth/
|
|
23
|
+
login-url.ts
|
|
24
|
+
login-url.test.ts
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
The authenticated route group may contain the persistent `AppShell`. The full data layout is in [data.md](data.md), and
|
|
24
|
-
component folders are in [components.md](components.md).
|
|
27
|
+
- Each subtree's rules live with its topic: [data.md](data.md) for `api/` and `http/`, [components.md](components.md) for
|
|
28
|
+
`components/`.
|
|
25
29
|
|
|
26
30
|
## Application boundary
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
- Start every frontend with the root providers, a current-user query, and pathless `_public/` and `_app/` route groups —
|
|
33
|
+
contract in [authentication.md](authentication.md); add feature routes only after that boundary exists.
|
|
34
|
+
- Greenfield: `_app/` starts with a minimal `AppShell` — ImFusion logo, route navigation, and a stable session-action area
|
|
35
|
+
around the route `Outlet`.
|
|
36
|
+
- Omit the shell only when the human explicitly says the product has no persistent authenticated navigation; the `_app/`
|
|
37
|
+
guard stays.
|
|
38
|
+
- An established project preserves its working choice; public pages can use a small branded header instead.
|
|
31
39
|
|
|
32
40
|
## Imports
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import { Button } from "#/components/button";
|
|
38
|
-
import { formatDate } from "./format-date";
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Wire `#/` once through `package.json`'s `imports` field and the matching TypeScript paths. It uses Node's `#` subpath prefix,
|
|
42
|
-
so it cannot conflict with an npm package scope.
|
|
42
|
+
- `#/` alias for anything outside the current folder, plain `./` for siblings.
|
|
43
|
+
- The alias is always `#/` → `src/`: `#` is Node's own subpath-import prefix, so it can't collide with an npm scope.
|
|
44
|
+
- Wire it once, through `package.json`'s `imports` field (toolchain-native), with a matching tsconfig `paths` entry.
|
|
@@ -1,52 +1,90 @@
|
|
|
1
1
|
# React
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
House defaults for React around `@imfusion/web-ui`. Links are for you, the agent — authoritative when a case here is
|
|
4
|
+
ambiguous; hand them to the human only if asked.
|
|
5
5
|
|
|
6
6
|
## Composition: pages (smart containers), partials, dumb components
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **
|
|
14
|
-
|
|
8
|
+
The architecture principle, top-down — rooted in Dan Abramov's
|
|
9
|
+
[Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) and
|
|
10
|
+
[Thinking in React](https://react.dev/learn/thinking-in-react). Styling lives exclusively in dumb and layout components,
|
|
11
|
+
nowhere else:
|
|
12
|
+
|
|
13
|
+
- **Pages are the smart containers.** The route component in `routes/` is the container — don't go looking for `*Container`
|
|
14
|
+
files. It owns how things _work_: loaders prefetch, the component fetches data, orchestrates, composes partials and passes
|
|
15
|
+
data down. Zero styling — a page wanting CSS (two panels side by side) means a layout component, not an inline style. A
|
|
16
|
+
standalone container earns its place only off-route (a modal fetching its own data).
|
|
17
|
+
- **Partials** are pure composition: reusable units of dumb and layout components with light logic and zero styling of their
|
|
18
|
+
own, extracted because they read as a unit. A partial with a CSS file is a red flag — the styled element wants to be its
|
|
19
|
+
own dumb component, or the wrapper wants to be a layout component.
|
|
20
|
+
- **Layout components** own _arrangement_ and nothing else: `Stack`-/`Row`-based wrappers with token gaps, a page grid, a
|
|
21
|
+
section frame.
|
|
22
|
+
- **Dumb components** own how things _look_: style and compose library primitives, take plain data and callbacks as props; no
|
|
23
|
+
fetching, routing, or business logic. All non-layout styling lives here and only here — logic-free dumb components (the
|
|
24
|
+
`@imfusion/web-ui` layer) keep screens restylable, testable with plain props, resilient to library updates.
|
|
25
|
+
- Wrapping library primitives in app-level dumb components: [library-boundary.md](library-boundary.md).
|
|
15
26
|
|
|
16
27
|
```tsx
|
|
17
|
-
|
|
28
|
+
// Dumb — renders what it's given
|
|
29
|
+
function UserCard({ name, role, onEdit }: { name: string; role: string; onEdit: () => void }) {
|
|
18
30
|
return (
|
|
19
31
|
<Card.Root>
|
|
20
|
-
<
|
|
21
|
-
|
|
32
|
+
<Card.Content>
|
|
33
|
+
<Typo>{name}</Typo>
|
|
34
|
+
<Chip>{role}</Chip>
|
|
35
|
+
</Card.Content>
|
|
36
|
+
<Card.Footer>
|
|
37
|
+
<Button onClick={onEdit}>Edit</Button>
|
|
38
|
+
</Card.Footer>
|
|
22
39
|
</Card.Root>
|
|
23
40
|
);
|
|
24
41
|
}
|
|
25
42
|
```
|
|
26
43
|
|
|
44
|
+
```tsx
|
|
45
|
+
// Smart — knows where data comes from, renders the dumb component
|
|
46
|
+
function UserCardContainer({ userId }: { userId: string }) {
|
|
47
|
+
const user = useUserQuery(userId);
|
|
48
|
+
const openEditor = useEditorNavigation(userId);
|
|
49
|
+
return <UserCard name={user.data.name} role={user.data.role} onEdit={openEditor} />;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
27
53
|
## Compose, don't configure
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
55
|
+
- Build screen-level pieces by composing primitives (`Stack`, `Row`, `Card`, dumb components), not one component with a dozen
|
|
56
|
+
boolean props — a prop list that reads like a settings page wanted to be two or three components.
|
|
57
|
+
- Lift state shared between siblings to the nearest common parent
|
|
58
|
+
([Sharing State Between Components](https://react.dev/learn/sharing-state-between-components)); never sync copies.
|
|
31
59
|
|
|
32
60
|
## Put state where its truth lives
|
|
33
61
|
|
|
34
|
-
|
|
62
|
+
Work down this list; stop at the first match:
|
|
35
63
|
|
|
36
|
-
1. **URL state
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
1. **URL state** — shareable via the address bar (filters, sort, pagination, active tab) → TanStack Router search params;
|
|
65
|
+
back button and copied links come free. These cross an external boundary — parse per [validation.md](validation.md).
|
|
66
|
+
2. **Server state** — from an API → TanStack Query's cache ([data.md](data.md)); never copy it into `useState` — that's how
|
|
67
|
+
stale-UI bugs are born.
|
|
68
|
+
3. **Subtree state** — resets on leave (wizard progress) → React context.
|
|
69
|
+
4. **Client state** — app-wide and persistent → TanStack Store, and only now: tiers 1–2 usually dissolve the "we need a
|
|
70
|
+
store" instinct.
|
|
71
|
+
5. **Local state** — one component's own (input value, open/closed, a draft, a hover flag) → `useState`. Most state is local;
|
|
72
|
+
keep it in the component, no library.
|
|
41
73
|
|
|
42
|
-
|
|
74
|
+
- A form turning drafts into a submitted domain value validates that boundary per [validation.md](validation.md).
|
|
75
|
+
- Shape the state itself per [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure) —
|
|
76
|
+
especially its rules on redundant and duplicated state.
|
|
43
77
|
|
|
44
78
|
## Effects: last resort, and named
|
|
45
79
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
- No `useEffect` for: derived values (render or `useMemo`), responses to user actions (the event handler), server sync (the
|
|
81
|
+
data-fetching layer). Effects only synchronize with systems _outside_ React.
|
|
82
|
+
- Read [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) — the definitive misuse catalog —
|
|
83
|
+
before every effect you're tempted to write.
|
|
84
|
+
- Extract a genuine effect into a custom hook named for its purpose (`useSyncedScroll`, `useDocumentTitle`, `useHotkey`),
|
|
85
|
+
never an anonymous inline `useEffect`: the name documents intent, the hook isolates the dependency array. Pattern:
|
|
86
|
+
[Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks).
|
|
87
|
+
- The rare effect that stays inline still names its callback, so intent survives without a comment:
|
|
50
88
|
|
|
51
89
|
```tsx
|
|
52
90
|
useEffect(
|
|
@@ -57,7 +95,15 @@ useEffect(
|
|
|
57
95
|
);
|
|
58
96
|
```
|
|
59
97
|
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
- A dependency sourced from a hook or query return: destructure the primitive first — [typescript.md](typescript.md#naming).
|
|
99
|
+
|
|
100
|
+
## Reading list
|
|
101
|
+
|
|
102
|
+
Consult while building; each is the authority for its topic:
|
|
62
103
|
|
|
63
|
-
|
|
104
|
+
- [Thinking in React](https://react.dev/learn/thinking-in-react) — decomposition and one-way data flow
|
|
105
|
+
- [Keeping Components Pure](https://react.dev/learn/keeping-components-pure) — why dumb components stay dumb
|
|
106
|
+
- [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure) — shaping state without duplication
|
|
107
|
+
- [Sharing State Between Components](https://react.dev/learn/sharing-state-between-components) — lifting state
|
|
108
|
+
- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) — the effect misuse catalog
|
|
109
|
+
- [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) — named effects live here
|
|
@@ -1,91 +1,88 @@
|
|
|
1
1
|
# Styling
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
How app CSS is written and how it meets `@imfusion/web-ui`.
|
|
4
4
|
|
|
5
5
|
## CSS authoring
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
- Nest
|
|
10
|
-
- Lift repeated values into custom properties, especially when a state or variant changes several descendants.
|
|
11
|
-
- Keep structurally different rules explicit; not every repeated value needs an abstraction.
|
|
7
|
+
- **CSS Modules with native CSS only** — no Sass, no CSS-in-JS, no utility-class framework: nesting and custom properties
|
|
8
|
+
already cover DRY. Stylesheet location and ownership: [components.md](components.md).
|
|
9
|
+
- Nest pseudo-elements, states, and child selectors under the root so the prefix is written once:
|
|
12
10
|
|
|
13
|
-
```css
|
|
14
|
-
.root {
|
|
15
|
-
|
|
11
|
+
```css
|
|
12
|
+
.root {
|
|
13
|
+
transition: clip-path var(--ease);
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
&:focus-visible {
|
|
16
|
+
outline: var(--imf-ui-border-size-2) solid var(--imf-ui-color-fg-support);
|
|
17
|
+
}
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
&[data-disabled] {
|
|
20
|
+
opacity: 0.5;
|
|
21
|
+
}
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
```
|
|
23
|
+
```
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
- Lift a repeated literal (an easing, a colour-math result) into a custom property; custom properties also carry
|
|
26
|
+
per-state/per-variant values down the tree — native parameterisation, no mixin needed.
|
|
27
|
+
- Don't merge rules that only look similar — structurally different output (three distinct `clip-path` polygons) isn't
|
|
28
|
+
repetition; keep it explicit.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
defect. Keep a literal only when no token represents it, such as a hairline or a clip-path percentage; give that literal a
|
|
31
|
-
named custom property when it is part of the component's design.
|
|
30
|
+
## Build custom UI from tokens
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
- Custom UI the library doesn't cover (a stat widget, a custom panel) uses names from the shipped token index
|
|
33
|
+
([tokens.md](tokens.md)) for color, size, radius, and type — that's what makes it look native and survive theme changes.
|
|
34
|
+
- A hex code or a magic `px` next to a concept the tokens already name is a defect.
|
|
35
|
+
- Geometry that can't be a token (a clip-path percentage, a hairline `1px`) → named custom property at the top of the
|
|
36
|
+
stylesheet, invariant written next to it. Don't silently approximate to the nearest token.
|
|
35
37
|
|
|
36
38
|
## Override through the sanctioned seams
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- your own
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
40
|
+
- All library styles live in the `imf-ui.components` CSS layer, so any plain selector you write wins — that's the whole
|
|
41
|
+
override contract.
|
|
42
|
+
- Target the stable hooks: `data-imf-ui-component` attributes and your own classes/wrappers.
|
|
43
|
+
- Never target the library's internal class names — they are generated and change without notice.
|
|
44
|
+
- Never `!important` — needing it means you're targeting the wrong thing.
|
|
45
|
+
- Style against state via data attributes (`[data-checked]`, `[data-disabled]`, `[data-popup-open]`) — components expose
|
|
46
|
+
state there, so never maintain your own state classes. `className` is purely a styling surface:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
[data-imf-ui-component="Switch"][data-checked] {
|
|
50
|
+
outline: 2px solid var(--imf-ui-color-bg-positive);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
52
53
|
|
|
53
54
|
## The color system
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`data-imf-ui-color-scheme="light" | "dark"` on `<html>`.
|
|
60
|
-
|
|
61
|
-
## Browser floor
|
|
56
|
+
- Roles: **surfaces** (`main` canvas, `support` panels, `minor` popovers); **brand** (identity, full saturation) vs
|
|
57
|
+
**primary** (contrast-tuned, CTAs) — distinct roles on purpose; **status** (`negative`, `warning`, `positive`, `info`);
|
|
58
|
+
three **accent** slots.
|
|
59
|
+
- **Controls** are the 80/20 customization surface — override one and every derived token shifts:
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```css
|
|
67
|
-
.subtle {
|
|
68
|
-
background: color-mix(in oklch, var(--chip-bg) 15%, transparent);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
@supports (color: oklch(from red l c h)) {
|
|
72
|
-
.subtle {
|
|
73
|
-
background: oklch(from var(--chip-bg) l c h / 0.15);
|
|
61
|
+
```css
|
|
62
|
+
:root {
|
|
63
|
+
--imf-ui-color-primary-hue: 30; /* every primary semantic token follows */
|
|
74
64
|
}
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Semantic tokens** are what you consume: `--imf-ui-color-bg-{name}` per role, a flat
|
|
68
|
+
`--imf-ui-color-fg-{main|support|minor|oncolor|…}` ladder for text. Borders and rings draw from `fg-*`.
|
|
69
|
+
- Push a hue control into a pale corner → you own overriding the matching `fg-*` token.
|
|
70
|
+
- Color scheme is `<html data-imf-ui-color-scheme="light|dark">`, set by the provider; scheme-specific styling selects via
|
|
71
|
+
that attribute.
|
|
77
72
|
|
|
78
73
|
## Responsive styling
|
|
79
74
|
|
|
80
|
-
|
|
75
|
+
- Tune components per breakpoint by overriding their `--imf-ui-*` variables inside your own media queries — custom properties
|
|
76
|
+
cross the `@media` boundary, props don't (why sizing is variables, not a `width` prop):
|
|
81
77
|
|
|
82
|
-
```css
|
|
83
|
-
@media (min-width: 768px) {
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
```css
|
|
79
|
+
@media (min-width: 768px) {
|
|
80
|
+
.my-shell {
|
|
81
|
+
--imf-ui-appshell-navbar-width: 22rem;
|
|
82
|
+
}
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
```
|
|
84
|
+
```
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
`useMediaQuery(minWidth("md"))
|
|
86
|
+
- Write literal breakpoint values — the library's `@custom-media` aliases are build-internal and don't ship.
|
|
87
|
+
- JS only for behaviour (render a burger menu on mobile): `useMediaQuery(minWidth("md"))`. Never for styling CSS can do.
|
|
88
|
+
- No responsive props — `size={{ sm: … }}` objects are deliberately not offered; write the media query.
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
-
Test decisions
|
|
3
|
+
Test the **decisions**, not the rendering.
|
|
4
4
|
|
|
5
5
|
## Naming
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Colocated tests use `.test.ts` / `.test.tsx` (e.g. `login-url.test.ts`) — not `.spec.*` or plural `.tests.*`.
|
|
8
8
|
|
|
9
9
|
## What gets a test
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
- **Pure logic gets unit tests** — pricing, permissions, date math, parsing, reducers: cheap, fast, and where bugs actually
|
|
12
|
+
hide.
|
|
13
|
+
- **Presentational components generally don't**: a component mapping props onto web-ui primitives has no logic of its own —
|
|
14
|
+
asserting a `<Button>` rendered tests React, not your code.
|
|
15
|
+
- **Behaviour a user performs gets an interaction test** — a validating form, a stepped flow — through the interface the user
|
|
16
|
+
has (roles, labels, visible text), not through internals.
|
|
17
|
+
- **Extract the decision out of a hook and test it as a plain function** — easier than a render harness; a hook that only
|
|
18
|
+
wraps a browser API has no decision to extract.
|
|
19
|
+
- The measure isn't coverage percentage — it's whether a failing test tells you something you didn't already know.
|
|
19
20
|
|
|
20
21
|
## Layer-specific recipes
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
- Data layer: stub `fetch`, run the real query client — the recipe is in [data.md](data.md).
|
|
24
|
+
- A schema whose constraints or transforms encode product behavior gets focused accepted, rejected, and transformed cases:
|
|
25
|
+
[validation.md](validation.md).
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
# Tokens
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Token index
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
node_modules/@imfusion/web-ui/src/llms/tokens.gen.json
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
It contains the shipped token names and authored defaults. Look up the exact entry instead of guessing a plausible
|
|
12
|
-
`--imf-ui-*` name or copying a token list into the project.
|
|
5
|
+
- `@imfusion/web-ui` ships the generated token index at `node_modules/@imfusion/web-ui/src/llms/tokens.gen.json` — the only
|
|
6
|
+
source for consumer token names and authored default values.
|
|
7
|
+
- Look up the exact entry there; never invent, recall, or duplicate a plausible `--imf-ui-*` name.
|