@imfusion/web-ui 0.10.1-dev.37.g9147f90f → 0.10.1-dev.45.ga6a3c86a

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.
@@ -1,8 +1,21 @@
1
1
  import { Plugin, UserConfig } from 'vite';
2
- /** Returns `undefined` for an installed package; only a checkout outside `node_modules` needs the config. */
3
- export declare function localWebUILinkConfig(root: string): UserConfig | undefined;
2
+ interface LinkConfig extends UserConfig {
3
+ test?: {
4
+ server?: {
5
+ deps?: {
6
+ inline?: (string | RegExp)[];
7
+ };
8
+ };
9
+ };
10
+ }
4
11
  /**
5
- * Lets a consumer link a local checkout (`"@imfusion/web-ui": "file:../web-ui"`) without loading two copies of React in
6
- * client dev and build. SSR is not covered. Does nothing for an installed package, so it can stay registered.
12
+ * Returns `undefined` unless `node_modules/@imfusion/web-ui` under `root` is a symlink. A symlinked checkout resolves its
13
+ * own copy of every peer, so the peers are deduped for Vite and the checkout's dependencies are inlined for Vitest.
14
+ */
15
+ export declare function localWebUILinkConfig(root: string): LinkConfig | undefined;
16
+ /**
17
+ * Lets a consumer keep `localWebUILink()` registered for good. It does nothing for an installed package. With a
18
+ * `file:` dependency on a Web UI checkout it makes the checkout share the app's React.
7
19
  */
8
20
  export declare function localWebUILink(): Plugin;
21
+ export {};
package/dist/vite.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { n as e } from "./paths-B5Ht3ETc.js";
2
- import { readFileSync as t, readdirSync as n, realpathSync as r } from "node:fs";
3
- import { dirname as i, join as a, resolve as o, sep as s } from "node:path";
2
+ import { existsSync as t, readFileSync as n, readdirSync as r, realpathSync as i } from "node:fs";
3
+ import { dirname as a, join as o, resolve as s } from "node:path";
4
4
  import { fileURLToPath as c } from "node:url";
5
5
  //#region src/vite/brand-assets.ts
6
- var l = i(c(import.meta.resolve("@imfusion/web-ui/assets/favicon/favicon.svg"))), u = n(l).map((e) => ({
6
+ var l = a(c(import.meta.resolve("@imfusion/web-ui/assets/favicon/favicon.svg"))), u = r(l).map((e) => ({
7
7
  name: e,
8
- source: t(a(l, e))
8
+ source: n(o(l, e))
9
9
  })), d = {
10
10
  ".ico": "image/x-icon",
11
11
  ".png": "image/png",
@@ -20,11 +20,7 @@ function p() {
20
20
  configureServer(e) {
21
21
  e.middlewares.use((e, t, n) => {
22
22
  let r = u.find(({ name: t }) => e.url?.split("?")[0] === `/${t}`);
23
- if (!r) {
24
- n();
25
- return;
26
- }
27
- t.setHeader("Content-Type", f(r.name)), t.end(r.source);
23
+ r ? (t.setHeader("Content-Type", f(r.name)), t.end(r.source)) : n();
28
24
  });
29
25
  },
30
26
  generateBundle() {
@@ -38,33 +34,42 @@ function p() {
38
34
  }
39
35
  //#endregion
40
36
  //#region src/vite/local-web-ui-link.ts
41
- function m(e) {
42
- if (r(e).split(s).includes("node_modules")) return;
43
- let n = JSON.parse(t(o(e, "package.json"), "utf-8")), i = typeof n == "object" && n ? Reflect.get(n, "peerDependencies") : {};
44
- if (typeof i != "object" || !i) throw Error(`${e}/package.json has no peerDependencies`);
45
- return { resolve: { dedupe: Object.keys(i) } };
37
+ var m = [
38
+ "/@base-ui/",
39
+ "/@floating-ui/",
40
+ "/use-sync-external-store/"
41
+ ];
42
+ function h(r) {
43
+ let a = s(i(r), "node_modules", "@imfusion", "web-ui");
44
+ if (!t(a) || i(a) === a) return;
45
+ let o = JSON.parse(n(s(e, "package.json"), "utf-8")), c = typeof o == "object" && o ? Reflect.get(o, "peerDependencies") : {};
46
+ if (typeof c != "object" || !c) throw Error(`${e}/package.json has no peerDependencies`);
47
+ return {
48
+ resolve: { dedupe: Object.keys(c) },
49
+ test: { server: { deps: { inline: m.map((e) => new RegExp(e)) } } }
50
+ };
46
51
  }
47
- function h() {
52
+ function g() {
48
53
  return {
49
54
  name: "imf-ui:local-web-ui-link",
50
- config: () => m(e)
55
+ config: (e) => h(s(e.root ?? process.cwd()))
51
56
  };
52
57
  }
53
58
  //#endregion
54
59
  //#region src/vite/readable-css-module-names.ts
55
- function g(e, t) {
60
+ function _(e, t) {
56
61
  let { lightningcss: n, modules: r, transformer: i } = e.css ?? {}, a = n?.cssModules;
57
62
  if (typeof a != "boolean" && r !== !1 && !a?.pattern && !(typeof r == "object" && r.generateScopedName)) return i === "postcss" ? { css: { modules: { generateScopedName: t } } } : { css: {
58
63
  transformer: "lightningcss",
59
64
  lightningcss: { cssModules: { pattern: t } }
60
65
  } };
61
66
  }
62
- function _({ prefix: e }) {
67
+ function v({ prefix: e }) {
63
68
  let t = `${e}-[name]-[local]`;
64
69
  return {
65
70
  name: "imf-ui:readable-css-module-names",
66
- config: (e) => g(e, t)
71
+ config: (e) => _(e, t)
67
72
  };
68
73
  }
69
74
  //#endregion
70
- export { p as imfusionBrandAssets, h as localWebUILink, _ as readableCssModuleNames };
75
+ export { p as imfusionBrandAssets, g as localWebUILink, v as readableCssModuleNames };
@@ -278,19 +278,19 @@ function me(e) {
278
278
  if (i.length > 0) {
279
279
  for (let e of i) console.error(`No skills installed for ${e}. Run \`npx web-ui install skills --harness=${e}\` first.`);
280
280
  process.exitCode = 1;
281
- return;
282
- }
283
- v.intro("@imfusion/web-ui install hooks");
284
- try {
285
- de(r);
286
- } catch (e) {
287
- if (e instanceof U) {
288
- console.error(e.message), process.exitCode = 1;
289
- return;
281
+ } else {
282
+ v.intro("@imfusion/web-ui install hooks");
283
+ try {
284
+ de(r);
285
+ } catch (e) {
286
+ if (e instanceof U) {
287
+ console.error(e.message), process.exitCode = 1;
288
+ return;
289
+ }
290
+ throw e;
290
291
  }
291
- throw e;
292
+ v.outro("Done — agent hooks installed.");
292
293
  }
293
- v.outro("Done — agent hooks installed.");
294
294
  }
295
295
  var Q = new ee();
296
296
  Q.name("web-ui").description("Command-line tools for @imfusion/web-ui").version(P);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imfusion/web-ui",
3
- "version": "0.10.1-dev.37.g9147f90f",
3
+ "version": "0.10.1-dev.45.ga6a3c86a",
4
4
  "description": "The official Web UI component library for ImFusion web apps",
5
5
  "author": "ImFusion GmbH",
6
6
  "homepage": "https://imfusion.com",
@@ -138,7 +138,7 @@
138
138
  "@types/react": "19.2.14",
139
139
  "@types/react-dom": "19.2.3",
140
140
  "@vitejs/plugin-react": "6.0.1",
141
- "@vitest/browser-playwright": "5.0.3",
141
+ "@vitest/browser-playwright": "5.0.4",
142
142
  "concurrently": "9.2.1",
143
143
  "culori": "4.0.2",
144
144
  "fast-xml-parser": "5.11.2",
@@ -148,19 +148,21 @@
148
148
  "mermaid": "12.0.0",
149
149
  "playwright": "1.60.0",
150
150
  "prettier": "3.8.3",
151
+ "react": "19.2.5",
151
152
  "react-docgen-typescript": "2.4.0",
153
+ "react-dom": "19.2.5",
152
154
  "remark-gfm": "4.0.1",
153
155
  "storybook": "10.6.2",
154
156
  "tsx": "4.21.0",
155
157
  "typescript": "6.0.3",
156
- "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
158
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
157
159
  "vite-plugin-dts": "5.0.0",
158
- "vite-plus": "1.1.0",
159
- "vitest": "5.0.3"
160
+ "vite-plus": "1.2.0",
161
+ "vitest": "5.0.4"
160
162
  },
161
163
  "overrides": {
162
- "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
163
- "vitest": "5.0.3",
164
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
165
+ "vitest": "5.0.4",
164
166
  "storybook": {
165
167
  ".": "$storybook",
166
168
  "vite-plus": "$vite-plus"
@@ -118,7 +118,7 @@
118
118
  "app-shell": {
119
119
  "root": {
120
120
  "name": "AppShell.Root",
121
- "description": "",
121
+ "description": "AppShell — application layout shell.\n\nThe classic dashboard frame: collapsible navbar/aside sidebars, a page\nheader, an optional footer, and a scrolling main content area. Also called\nan app layout, application frame, dashboard shell, or sidebar layout.\n\nComposed of eight sub-components used with dot-notation: `AppShell.Root` wraps\n`AppShell.Header`, `AppShell.Navbar`, `AppShell.Aside`, `AppShell.Footer`,\n`AppShell.Main`, and (inside a sidebar) `AppShell.Section` and `AppShell.NavItem`.\n\nSizing is CSS-driven — override the `--imf-ui-appshell-*` custom properties\nrather than passing dimension props. Below the responsive breakpoints the\nnavbar (`md`) and aside (`lg`) open as drawers, and the shell renders the\nnavbar's toggle itself.\n\nCopied and adapted from Mantine's AppShell; no upstream runtime dependency.\nMIT licensed (see Mantine repo).",
122
122
  "props": [
123
123
  {
124
124
  "name": "ref",
@@ -141,6 +141,13 @@
141
141
  "defaultValue": "Close panel",
142
142
  "description": "Accessible name of the close button shown while the aside is a drawer."
143
143
  },
144
+ {
145
+ "name": "defaultOpen",
146
+ "required": false,
147
+ "type": "boolean",
148
+ "defaultValue": null,
149
+ "description": "Whether the drawer starts open when `open` is omitted."
150
+ },
144
151
  {
145
152
  "name": "onOpenChange",
146
153
  "required": false,
@@ -185,68 +192,6 @@
185
192
  }
186
193
  ]
187
194
  },
188
- {
189
- "name": "AppShell.Burger",
190
- "description": "AppShell.Burger — the menu toggle: three sheared bands that cross while the navbar drawer is open. With no props it\nopens and closes that drawer. Takes its color from the surface and hides from `md` up, where the navbar is docked, and\nsteps back while the drawer is open, so the drawer's own close is the only one shown.",
191
- "props": [
192
- {
193
- "name": "className",
194
- "required": false,
195
- "type": "string | ((state: ButtonState) => string)",
196
- "defaultValue": null,
197
- "description": "CSS class applied to the element, or a function that\nreturns a class based on the component's state."
198
- },
199
- {
200
- "name": "focusableWhenDisabled",
201
- "required": false,
202
- "type": "boolean",
203
- "defaultValue": "false",
204
- "description": "Whether the button should be focusable when disabled."
205
- },
206
- {
207
- "name": "label",
208
- "required": false,
209
- "type": "string",
210
- "defaultValue": null,
211
- "description": "Accessible name. Defaults to \"Open navigation\" or \"Close navigation\" from `opened`."
212
- },
213
- {
214
- "name": "nativeButton",
215
- "required": false,
216
- "type": "boolean",
217
- "defaultValue": "true",
218
- "description": "Whether the component renders a native `<button>` element when replacing it\nvia the `render` prop.\nSet to `false` if the rendered element is not a button (for example, `<div>`)."
219
- },
220
- {
221
- "name": "opened",
222
- "required": false,
223
- "type": "boolean",
224
- "defaultValue": null,
225
- "description": "Draws the cross and sets `aria-expanded`. Defaults to whether the navbar drawer is open."
226
- },
227
- {
228
- "name": "ref",
229
- "required": false,
230
- "type": "Ref<HTMLDivElement>",
231
- "defaultValue": null,
232
- "description": "Allows getting a ref to the component instance.\nOnce the component unmounts, React will set `ref.current` to `null`\n(or call the ref with `null` if you passed a callback ref).\n@see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}"
233
- },
234
- {
235
- "name": "render",
236
- "required": false,
237
- "type": "| ReactElement<\n unknown,\n string | JSXElementConstructor<any>\n >\n| ComponentRenderFn<HTMLProps, ButtonState>",
238
- "defaultValue": null,
239
- "description": "Allows you to replace the component's HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render."
240
- },
241
- {
242
- "name": "style",
243
- "required": false,
244
- "type": "| CSSProperties\n| ((state: ButtonState) => CSSProperties)",
245
- "defaultValue": null,
246
- "description": "Style applied to the element, or a function that\nreturns a style object based on the component's state."
247
- }
248
- ]
249
- },
250
195
  {
251
196
  "name": "AppShell.Footer",
252
197
  "description": "",
@@ -262,7 +207,7 @@
262
207
  },
263
208
  {
264
209
  "name": "AppShell.Header",
265
- "description": "",
210
+ "description": "AppShell.Header — the page row: a title and actions at every width. Below `md` it sits under the shell's bar, and from\n`md` it sits beside the rail.",
266
211
  "props": [
267
212
  {
268
213
  "name": "ref",
@@ -297,11 +242,18 @@
297
242
  "defaultValue": null,
298
243
  "description": "The signed-in account at the foot of the rail, usually an `AccountCard`. Without it the foot is omitted."
299
244
  },
245
+ {
246
+ "name": "defaultOpen",
247
+ "required": false,
248
+ "type": "boolean",
249
+ "defaultValue": null,
250
+ "description": "Whether the drawer starts open when `open` is omitted."
251
+ },
300
252
  {
301
253
  "name": "logo",
302
254
  "required": false,
303
255
  "type": "ReactNode",
304
- "defaultValue": "<ImFusionLogo variant={variant === \"branded\" ? \"oncolor\" : \"brand\"} size=\"sm\" />",
256
+ "defaultValue": "variant === \"branded\" ? oncolorLogo : brandLogo",
305
257
  "description": "The brand at the top of the rail. Defaults to the ImFusion logo; `null` leaves the row empty."
306
258
  },
307
259
  {
@@ -336,7 +288,7 @@
336
288
  },
337
289
  {
338
290
  "name": "AppShell.NavItem",
339
- "description": "AppShell.NavItem — a navbar link: icon, mono label, and an optional description. Takes its color from the navbar;\n`aria-current=\"page\"` underlines the label. Pass a router link through `render`.",
291
+ "description": "AppShell.NavItem — a navbar link: icon, mono label, and an optional description. Takes its color from the navbar;\n`aria-current=\"page\"` underlines the label. Pass a router link through `render`. Followed inside the navbar drawer,\nit closes the drawer.",
340
292
  "props": [
341
293
  {
342
294
  "name": "description",
@@ -4,6 +4,8 @@
4
4
  # and warns on drift. Always exits 0 — staleness never blocks a commit.
5
5
  PKG=node_modules/@imfusion/web-ui/package.json
6
6
  [ -f "$PKG" ] || exit 0
7
+ # A symlinked checkout reports its own dev version.
8
+ [ -L node_modules/@imfusion/web-ui ] && exit 0
7
9
  CURRENT=$(node -p "require('./$PKG').version" 2>/dev/null) || exit 0
8
10
 
9
11
  # .claude/skills may be a symlink to .agents/skills; skip markers already read.
@@ -10,9 +10,12 @@ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
10
10
  [ -n "$(git status --porcelain -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.css' 2>/dev/null)" ] || exit 0
11
11
  # Gate only turns that actually edited files — a tree left dirty by an earlier
12
12
  # session must not block a question-answering turn. Edit evidence: Write/Edit
13
- # tool calls (Claude transcript) or apply_patch calls (Codex rollout).
13
+ # tool calls (Claude transcript) or apply_patch calls (Codex rollout), searched
14
+ # only after the turn's opening entry: the last real user prompt (not a tool
15
+ # result or injected meta message) or the last Codex task_started event.
14
16
  TRANSCRIPT=$(printf '%s' "$PAYLOAD" | sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
15
17
  [ -f "$TRANSCRIPT" ] || exit 0
16
- grep -q '"name":"Write"\|"name":"Edit"\|"name":"MultiEdit"\|"name":"NotebookEdit"\|apply_patch' "$TRANSCRIPT" || exit 0
18
+ TURN_START=$(grep -En '"type":"user"|"type":"task_started"' "$TRANSCRIPT" | grep -Ev '"type":"tool_result"|"isMeta":true' | tail -n 1 | cut -d: -f1)
19
+ tail -n +$(( ${TURN_START:-0} + 1 )) "$TRANSCRIPT" | grep -Eq '"name":"Write"|"name":"Edit"|"name":"MultiEdit"|"name":"NotebookEdit"|"name":"apply_patch"' || exit 0
17
20
  printf '{"decision":"block","reason":"web-ui stop gate: this turn changed source files. Run the project verification (verify:typecheck, verify:lint, and verify:format at minimum), fix what it reports, then finish. If it already ran after the last change, say so and finish."}\n'
18
21
  exit 0
@@ -60,7 +60,11 @@ Classify every result as one of these:
60
60
 
61
61
  Use the report contract's severity, `path:line` evidence, concrete impact, and smallest next action. Unverified entries use
62
62
  that same structure: unavailable runtime evidence, the impact of the unknown, and the check that resolves it. Do not promote
63
- an optional tool or a working alternative to a missing finding.
63
+ an optional tool or a working alternative to a missing finding. A `vite.config.ts` without `localWebUILink()` registered from
64
+ `@imfusion/web-ui/vite` is Missing. A `tsconfig.json` without the `paths` block from the `tooling` topic is Missing too. Each
65
+ next action is the add from that topic. A major or minor mismatch between the project and Web UI on a package listed under
66
+ Aligned runtime packages in the `tooling` topic is a Deviation. Report both versions, with
67
+ `npm install <package>@<Web UI's version>` as the fix.
64
68
 
65
69
  ## Topics
66
70
 
@@ -109,7 +109,8 @@ sections one to one — change a topic's sections and this file follows in the s
109
109
  - [ ] Oxfmt
110
110
  - [ ] Oxlint and TypeScript
111
111
  - [ ] Pre-commit checks
112
- - [ ] Vite plugins
112
+ - [ ] Vite plugins, including `localWebUILink()`
113
+ - [ ] Aligned runtime packages
113
114
 
114
115
  ## npm project — `npm-project`
115
116
 
@@ -119,10 +119,10 @@ Register Web UI's Vite plugins in the application configuration:
119
119
  ```ts
120
120
  import { defineConfig } from "vite-plus";
121
121
  import react from "@vitejs/plugin-react";
122
- import { imfusionBrandAssets, readableCssModuleNames } from "@imfusion/web-ui/vite";
122
+ import { imfusionBrandAssets, localWebUILink, readableCssModuleNames } from "@imfusion/web-ui/vite";
123
123
 
124
124
  export default defineConfig({
125
- plugins: [react(), imfusionBrandAssets(), readableCssModuleNames({ prefix: "app" })]
125
+ plugins: [react(), imfusionBrandAssets(), readableCssModuleNames({ prefix: "app" }), localWebUILink()]
126
126
  });
127
127
  ```
128
128
 
@@ -130,5 +130,32 @@ export default defineConfig({
130
130
  Register `readableCssModuleNames` in every compiler that processes the app's CSS, including development, Storybook, and
131
131
  production. A compiler left out produces different class names and styles that appear to work only in some environments.
132
132
 
133
- `localWebUILink` dedupes the library's peer dependencies when `@imfusion/web-ui` is linked from a local checkout
134
- (`file:../web-ui`), so the app loads one React. It does nothing for an installed package and does not cover server rendering.
133
+ ## Aligned runtime packages
134
+
135
+ Web UI and the app run the same versions of these packages. The `peerDependencies` of
136
+ `node_modules/@imfusion/web-ui/package.json` give the accepted ranges, and its `devDependencies` give the version Web UI
137
+ builds and tests with. A matching major and minor is the rule; a patch difference is fine.
138
+
139
+ - `react`, `react-dom`
140
+ - `@types/react`, `@types/react-dom`
141
+ - `@base-ui/react`, which brings `@floating-ui/*` with it
142
+ - `vite-plus`
143
+ - `vite`, the `npm:@voidzero-dev/vite-plus-core` alias that `vite-plus` provides
144
+
145
+ ## Version drift
146
+
147
+ Aligned versions are the primary fix. The plugin and the `paths` block below catch the drift that slips through.
148
+
149
+ `localWebUILink` stays registered for good. It does nothing for an installed package. When `@imfusion/web-ui` is a symlinked
150
+ checkout, it keeps one React in dev, build, and Vitest.
151
+
152
+ Add this to `compilerOptions` in `tsconfig.json`, merged by hand into an existing `paths`. For a symlinked checkout it keeps
153
+ one set of React and Vite types for `tsc`, and it does nothing for an installed package:
154
+
155
+ ```json
156
+ "paths": {
157
+ "react": ["./node_modules/@types/react"],
158
+ "react/*": ["./node_modules/@types/react/*"],
159
+ "vite": ["./node_modules/vite"]
160
+ }
161
+ ```
@@ -45,10 +45,18 @@ Include these pieces unless the existing project already has them or the user re
45
45
  - one server-backed current-user query at the route boundary;
46
46
  - a minimal `AppShell` in `_app/`, unless the user says the product has no persistent authenticated navigation;
47
47
  - an API topic folder for data shown by the first screen;
48
- - the root provider and library wiring.
48
+ - the root provider and library wiring;
49
+ - the Vite plugins below.
49
50
 
50
51
  The plan describes the exact starter files. It does not copy a generic starter project.
51
52
 
53
+ ## Vite plugins
54
+
55
+ Propose a `vite.config.ts` that registers `imfusionBrandAssets()`, `readableCssModuleNames()`, and `localWebUILink()` from
56
+ `@imfusion/web-ui/vite`, next to the plugins the project already has. Register `localWebUILink()` in every project: without a
57
+ symlinked checkout it does nothing. Also propose the `tsconfig.json` `paths` block. Both keep one React and one set of types
58
+ when the package is a symlinked checkout. The `tooling` topic has the exact config.
59
+
52
60
  ## Installer steps
53
61
 
54
62
  For `agent-tooling`, list the `npx web-ui install` commands as steps for the human. Read existing registrations first and do
@@ -96,6 +96,13 @@ changed. A diff shows what moved, not the step order you continue under, so it d
96
96
  the current step using the updated guidance, keeping the original baseline, user choices, and completed changes in mind.
97
97
  Check for newly required steps without repeating completed mutations or expanding the approved scope.
98
98
 
99
+ ## Align the runtime packages
100
+
101
+ After the install, compare the app's installed versions of the packages listed under Aligned runtime packages in the
102
+ `tooling` topic with Web UI's own `package.json` in `node_modules/@imfusion/web-ui`. Report every major or minor mismatch and
103
+ propose `npm install <package>@<Web UI's version>` for each. Make the change as its own commit, in this same update, after
104
+ the base commit and with its own approval.
105
+
99
106
  ## Verify the base update
100
107
 
101
108
  After every mutating command, compare the status with the preflight snapshot. Stop on an unexpected path or overlapping