@imfusion/web-ui 0.5.1-dev.6.gece7a7b5 → 0.6.0

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 (98) hide show
  1. package/README.md +90 -43
  2. package/bin/install.js +428 -0
  3. package/bin/install.test.ts +329 -0
  4. package/dist/build/vite-css-module-names/index.d.ts +20 -0
  5. package/dist/build/vite-css-module-names.js +17 -0
  6. package/dist/chunk-DmhlhrBa.js +11 -0
  7. package/dist/code-Blo48PGr.js +136 -0
  8. package/dist/codegen/gen-icons.d.ts +24 -0
  9. package/dist/components/callout/callout.d.ts +1 -1
  10. package/dist/components/chip-link/chip-link.d.ts +1 -1
  11. package/dist/components/code/code.d.ts +10 -18
  12. package/dist/components/field/field.d.ts +104 -0
  13. package/dist/components/field/field.meta.d.ts +2 -0
  14. package/dist/components/field/index.d.ts +2 -0
  15. package/dist/components/fieldset/fieldset.d.ts +29 -0
  16. package/dist/components/fieldset/fieldset.meta.d.ts +2 -0
  17. package/dist/components/fieldset/index.d.ts +2 -0
  18. package/dist/components/icon/icon.d.ts +16 -0
  19. package/dist/components/icon/icon.meta.d.ts +2 -0
  20. package/dist/components/icon/index.d.ts +4 -0
  21. package/dist/components/icon/types.d.ts +2 -0
  22. package/dist/components/input/input.d.ts +3 -1
  23. package/dist/components/typo/typo.d.ts +2 -2
  24. package/dist/docgen/component-sources.d.ts +7 -0
  25. package/dist/hooks/index.d.ts +1 -0
  26. package/dist/hooks/use-resize-observer.d.ts +2 -0
  27. package/dist/icons/catalog.gen.d.ts +8357 -0
  28. package/dist/icons/icon-config-provider.d.ts +8 -0
  29. package/dist/icons/icon-context.d.ts +4 -0
  30. package/dist/icons/icons.gen.d.ts +1672 -0
  31. package/dist/icons/index.d.ts +3 -0
  32. package/dist/icons-wBmF0U2x.js +78 -0
  33. package/dist/icons.js +2 -0
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.js +1793 -12210
  36. package/dist/integrations/code-highlight/code-highlight.d.ts +8 -6
  37. package/dist/integrations/code-highlight/highlighter.d.ts +32 -3
  38. package/dist/integrations/code-highlight/language-patterns.d.ts +7 -0
  39. package/dist/integrations/code-highlight/languages/cmake.d.ts +1 -0
  40. package/dist/integrations/code-highlight/languages/cpp.d.ts +1 -0
  41. package/dist/integrations/code-highlight/languages/python.d.ts +1 -0
  42. package/dist/integrations/code-highlight.js +198 -59
  43. package/dist/integrations/image-display-options.js +70 -69
  44. package/dist/llms/gen-tokens.d.ts +7 -0
  45. package/dist/meta-CySnRuVp.js +21 -0
  46. package/dist/provider/web-ui-provider.d.ts +4 -1
  47. package/dist/style.css +1 -1
  48. package/dist/tabs-CMKvMF4E.js +369 -0
  49. package/package.json +48 -26
  50. package/src/docgen/doc.gen.json +381 -38
  51. package/src/llms/icon-catalog.gen.json +11203 -0
  52. package/src/llms/install-templates/AGENTS.md +34 -0
  53. package/src/llms/install-templates/codex-hooks.json +44 -0
  54. package/src/llms/install-templates/hooks/baseline-staleness.sh +17 -0
  55. package/src/llms/install-templates/hooks/session-start.sh +5 -0
  56. package/src/llms/install-templates/hooks/stop.sh +18 -0
  57. package/src/llms/install-templates/hooks/subagent-start.sh +5 -0
  58. package/src/llms/install-templates/hooks/user-prompt-submit.sh +5 -0
  59. package/src/llms/install-templates/settings.json +45 -0
  60. package/src/llms/llms.gen.txt +17 -0
  61. package/src/llms/skills/imf-web-ui/SKILL.md +13 -12
  62. package/src/llms/skills/imf-web-ui-audit/SKILL.md +119 -0
  63. package/src/llms/skills/imf-web-ui-components/SKILL.md +56 -3
  64. package/src/llms/skills/imf-web-ui-conventions/SKILL.md +57 -0
  65. package/src/llms/skills/imf-web-ui-conventions/templates/AUDIT_CHECKLIST.md +141 -0
  66. package/src/llms/skills/imf-web-ui-conventions/templates/REPORT.md +45 -0
  67. package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +82 -0
  68. package/src/llms/skills/imf-web-ui-conventions/topics/assets.md +27 -0
  69. package/src/llms/skills/imf-web-ui-conventions/topics/authentication.md +65 -0
  70. package/src/llms/skills/imf-web-ui-conventions/topics/class-names.md +50 -0
  71. package/src/llms/skills/imf-web-ui-conventions/topics/components.md +101 -0
  72. package/src/llms/skills/imf-web-ui-conventions/topics/data.md +221 -0
  73. package/src/llms/skills/imf-web-ui-conventions/topics/docs-structure.md +40 -0
  74. package/src/llms/skills/imf-web-ui-conventions/topics/git.md +34 -0
  75. package/src/llms/skills/imf-web-ui-conventions/topics/library-boundary.md +35 -0
  76. package/src/llms/skills/imf-web-ui-conventions/topics/library-setup.md +26 -0
  77. package/src/llms/skills/imf-web-ui-conventions/topics/npm-project.md +53 -0
  78. package/src/llms/skills/imf-web-ui-conventions/topics/project-structure.md +44 -0
  79. package/src/llms/skills/imf-web-ui-conventions/topics/react.md +109 -0
  80. package/src/llms/skills/imf-web-ui-conventions/topics/styling.md +88 -0
  81. package/src/llms/skills/imf-web-ui-conventions/topics/testing.md +25 -0
  82. package/src/llms/skills/imf-web-ui-conventions/topics/tokens.md +7 -0
  83. package/src/llms/skills/imf-web-ui-conventions/topics/tooling.md +116 -0
  84. package/src/llms/skills/imf-web-ui-conventions/topics/typescript.md +73 -0
  85. package/src/llms/skills/imf-web-ui-conventions/topics/validation.md +62 -0
  86. package/src/llms/skills/imf-web-ui-setup/SKILL.md +67 -37
  87. package/src/llms/skills/imf-web-ui-update/SKILL.md +157 -0
  88. package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
  89. package/src/llms/skills/imf-web-ui-ux/references/forms.md +2 -2
  90. package/src/llms/tokens.gen.json +887 -0
  91. package/bin/install-skill.js +0 -180
  92. package/dist/code-qBbqAHK-.js +0 -190
  93. package/dist/meta-B8C51eyL.js +0 -74
  94. package/dist/tabs-DqBFSqq6.js +0 -3789
  95. package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -93
  96. package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +0 -133
  97. package/src/llms/skills/imf-web-ui-frontend-patterns/references/react-patterns.md +0 -94
  98. package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +0 -201
@@ -0,0 +1,329 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ statSync,
11
+ symlinkSync,
12
+ writeFileSync
13
+ } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const installer = resolve(fileURLToPath(new URL(".", import.meta.url)), "install.js");
19
+
20
+ function runInstall(cwd: string, ...args: string[]) {
21
+ const result = spawnSync("node", [installer, ...args], { cwd, encoding: "utf-8" });
22
+ expect(result.status).toBe(0);
23
+ return result;
24
+ }
25
+
26
+ function readSettings(cwd: string) {
27
+ return JSON.parse(readFileSync(join(cwd, ".claude", "settings.json"), "utf-8")) as {
28
+ permissions?: { allow: string[] };
29
+ hooks: Record<string, { hooks: { command?: string }[] }[]>;
30
+ };
31
+ }
32
+
33
+ let consumer: string;
34
+ const retiredSkillNames = [
35
+ ["agent", "setup"],
36
+ ["library", "setup"]
37
+ ].map(parts => `imf-web-ui-${parts.join("-")}`);
38
+
39
+ beforeEach(() => {
40
+ consumer = mkdtempSync(join(tmpdir(), "web-ui-install-"));
41
+ });
42
+
43
+ describe("web-ui-install", () => {
44
+ it("installs skills into both targets with version markers", () => {
45
+ runInstall(consumer, "--skills", "--target", "agents", "--target", "claude");
46
+
47
+ const installed = readdirSync(join(consumer, ".agents", "skills"));
48
+ expect(installed).toContain("imf-web-ui");
49
+ expect(installed).toContain("imf-web-ui-conventions");
50
+ expect(installed).toContain("imf-web-ui-setup");
51
+ expect(installed).toContain("imf-web-ui-audit");
52
+ expect(installed).toHaveLength(7);
53
+ for (const name of retiredSkillNames) expect(installed).not.toContain(name);
54
+
55
+ for (const name of installed) {
56
+ expect(existsSync(join(consumer, ".agents", "skills", name, ".imf-web-ui-skill-version.json"))).toBe(true);
57
+ expect(lstatSync(join(consumer, ".claude", "skills", name)).isSymbolicLink()).toBe(true);
58
+ }
59
+ });
60
+
61
+ it("removes skills that are no longer included in the bundle", () => {
62
+ const orphan = "imf-web-ui-retired-skill";
63
+ const agentsOrphan = join(consumer, ".agents", "skills", orphan);
64
+ mkdirSync(agentsOrphan, { recursive: true });
65
+ writeFileSync(join(agentsOrphan, "SKILL.md"), "# Retired skill\n");
66
+ const claudeOrphan = join(consumer, ".claude", "skills", orphan);
67
+ mkdirSync(join(consumer, ".claude", "skills"), { recursive: true });
68
+ symlinkSync(agentsOrphan, claudeOrphan);
69
+ expect(lstatSync(claudeOrphan).isSymbolicLink()).toBe(true);
70
+
71
+ const result = runInstall(consumer, "--skills", "--target", "agents", "--target", "claude");
72
+
73
+ expect(existsSync(agentsOrphan)).toBe(false);
74
+ expect(lstatSync(claudeOrphan, { throwIfNoEntry: false })).toBeUndefined();
75
+ expect(result.stdout).toContain(`Removed orphaned skill: ${orphan} (no longer in this bundle)`);
76
+ });
77
+
78
+ it("installs hooks into an installer-owned directory and registers them for both hosts", () => {
79
+ runInstall(consumer, "--hooks", "--target", "agents");
80
+
81
+ const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
82
+ const scripts = readdirSync(hookDir);
83
+ expect(scripts).toEqual(expect.arrayContaining(["session-start.sh", "subagent-start.sh", "user-prompt-submit.sh"]));
84
+ for (const script of scripts) {
85
+ expect(statSync(join(hookDir, script)).mode & 0o100).toBeTruthy();
86
+ }
87
+
88
+ const settings = readSettings(consumer);
89
+ expect(Object.keys(settings.hooks)).toEqual(
90
+ expect.arrayContaining(["SessionStart", "SubagentStart", "UserPromptSubmit"])
91
+ );
92
+
93
+ const codexHooks = JSON.parse(readFileSync(join(consumer, ".codex", "hooks.json"), "utf-8")) as {
94
+ hooks: Record<string, { hooks: { command?: string }[] }[]>;
95
+ };
96
+ expect(Object.keys(codexHooks.hooks)).toEqual(Object.keys(settings.hooks));
97
+ const codexCommands = Object.values(codexHooks.hooks).flatMap(entries =>
98
+ entries.flatMap(entry => entry.hooks.map(hook => hook.command))
99
+ );
100
+ for (const command of codexCommands) {
101
+ expect(command).toContain("$(git rev-parse --show-toplevel)/.agents/hooks/imf-web-ui/");
102
+ expect(command).not.toContain("CLAUDE_PROJECT_DIR");
103
+ }
104
+
105
+ expect(readdirSync(join(consumer, ".agents", "skills"))).toHaveLength(7);
106
+ });
107
+
108
+ it("prunes a stale hook script and its registrations on refresh", () => {
109
+ runInstall(consumer, "--hooks", "--target", "agents");
110
+ const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
111
+ writeFileSync(join(hookDir, "retired-hook.sh"), "#!/usr/bin/env sh\nexit 0\n");
112
+ const settingsFile = join(consumer, ".claude", "settings.json");
113
+ const settings = readSettings(consumer);
114
+ settings.hooks.PostToolUse = [
115
+ {
116
+ hooks: [{ type: "command", command: '"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/retired-hook.sh"' }]
117
+ } as never
118
+ ];
119
+ writeFileSync(settingsFile, JSON.stringify(settings));
120
+
121
+ runInstall(consumer, "--hooks", "--target", "agents");
122
+
123
+ expect(existsSync(join(hookDir, "retired-hook.sh"))).toBe(false);
124
+ const refreshed = readSettings(consumer);
125
+ expect(refreshed.hooks.PostToolUse).toBeUndefined();
126
+ });
127
+
128
+ it("prunes per hook, keeping consumer hooks and customized commands in the same entry", () => {
129
+ runInstall(consumer, "--hooks", "--target", "agents");
130
+ const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
131
+ writeFileSync(join(hookDir, "retired-hook.sh"), "#!/usr/bin/env sh\nexit 0\n");
132
+ const settingsFile = join(consumer, ".claude", "settings.json");
133
+ const settings = readSettings(consumer);
134
+ const customized = '"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/session-start.sh" 2>/dev/null';
135
+ settings.hooks.PostToolUse = [
136
+ {
137
+ hooks: [
138
+ { type: "command", command: '"$CLAUDE_PROJECT_DIR/.agents/hooks/imf-web-ui/retired-hook.sh"' },
139
+ { type: "command", command: "npm run my-check" },
140
+ { type: "command", command: customized }
141
+ ]
142
+ } as never
143
+ ];
144
+ writeFileSync(settingsFile, JSON.stringify(settings));
145
+
146
+ runInstall(consumer, "--hooks", "--target", "agents");
147
+
148
+ const commands = readSettings(consumer).hooks.PostToolUse.flatMap(entry => entry.hooks.map(hook => hook.command));
149
+ expect(commands).toEqual(["npm run my-check", customized]);
150
+ });
151
+
152
+ it("keeps a consumer-adjusted codex command across re-runs, without duplicating it", () => {
153
+ runInstall(consumer, "--hooks", "--target", "agents");
154
+ const codexPath = join(consumer, ".codex", "hooks.json");
155
+ const adjusted = '"$(git rev-parse --show-toplevel)/frontend/.agents/hooks/imf-web-ui/session-start.sh"';
156
+ const codexHooks = JSON.parse(readFileSync(codexPath, "utf-8")) as {
157
+ hooks: Record<string, { hooks: { command?: string }[] }[]>;
158
+ };
159
+ codexHooks.hooks.SessionStart[0].hooks[0].command = adjusted;
160
+ writeFileSync(codexPath, JSON.stringify(codexHooks));
161
+
162
+ runInstall(consumer, "--hooks", "--target", "agents");
163
+
164
+ const refreshed = JSON.parse(readFileSync(codexPath, "utf-8")) as {
165
+ hooks: Record<string, { hooks: { command?: string }[] }[]>;
166
+ };
167
+ const commands = refreshed.hooks.SessionStart.flatMap(entry => entry.hooks.map(hook => hook.command));
168
+ expect(commands).toEqual([adjusted]);
169
+ });
170
+
171
+ it("merges registrations without clobbering existing settings, idempotently", () => {
172
+ mkdirSync(join(consumer, ".claude"), { recursive: true });
173
+ writeFileSync(
174
+ join(consumer, ".claude", "settings.json"),
175
+ JSON.stringify({
176
+ permissions: { allow: ["Bash(git *)"] },
177
+ hooks: { PostToolUse: [{ matcher: "Edit", hooks: [{ type: "command", command: "echo existing" }] }] }
178
+ })
179
+ );
180
+
181
+ runInstall(consumer, "--hooks", "--target", "agents");
182
+ runInstall(consumer, "--hooks", "--target", "agents");
183
+
184
+ const settings = readSettings(consumer);
185
+ expect(settings.permissions?.allow).toEqual(["Bash(git *)"]);
186
+ const foreign = settings.hooks.PostToolUse.flatMap(entry => entry.hooks.map(hook => hook.command));
187
+ expect(foreign).toContain("echo existing");
188
+ // the shipped registration is added exactly once across both runs
189
+ const commands = settings.hooks.UserPromptSubmit.flatMap(entry => entry.hooks.map(hook => hook.command));
190
+ expect(commands.filter(command => command?.includes("user-prompt-submit.sh"))).toHaveLength(1);
191
+ });
192
+
193
+ it("recognises registrations written with braced ${CLAUDE_PROJECT_DIR}", () => {
194
+ mkdirSync(join(consumer, ".claude"), { recursive: true });
195
+ writeFileSync(
196
+ join(consumer, ".claude", "settings.json"),
197
+ JSON.stringify({
198
+ hooks: {
199
+ UserPromptSubmit: [
200
+ {
201
+ hooks: [
202
+ {
203
+ type: "command",
204
+ command: '"${CLAUDE_PROJECT_DIR}/.agents/hooks/imf-web-ui/user-prompt-submit.sh"'
205
+ }
206
+ ]
207
+ },
208
+ // a foreign entry carrying no `command` at all must not throw
209
+ { matcher: "Bash", hooks: [{ type: "command", args: ["${CLAUDE_PROJECT_DIR}/other/gate.mjs"] }] }
210
+ ]
211
+ }
212
+ })
213
+ );
214
+
215
+ runInstall(consumer, "--hooks", "--target", "agents");
216
+
217
+ const settings = readSettings(consumer);
218
+ const commands = settings.hooks.UserPromptSubmit.flatMap(entry => entry.hooks.map(hook => hook.command));
219
+ expect(commands.filter(command => command?.includes("user-prompt-submit.sh"))).toHaveLength(1);
220
+ expect(commands).toContain('"${CLAUDE_PROJECT_DIR}/.agents/hooks/imf-web-ui/user-prompt-submit.sh"');
221
+ });
222
+
223
+ it("does not touch settings.json when every registration is already present", () => {
224
+ runInstall(consumer, "--hooks", "--target", "agents");
225
+ const settingsFile = join(consumer, ".claude", "settings.json");
226
+ const settings = readSettings(consumer);
227
+ settings.hooks.PreToolUse = [{ hooks: [{ command: "./other/gate.mjs" }] }];
228
+ const before = JSON.stringify(settings, null, 4) + "\n";
229
+ writeFileSync(settingsFile, before);
230
+
231
+ const result = runInstall(consumer, "--hooks", "--target", "agents");
232
+
233
+ expect(readFileSync(settingsFile, "utf-8")).toBe(before);
234
+ expect(result.stdout).toContain("Hook registrations already present");
235
+ });
236
+
237
+ it("upserts the fenced AGENTS.md block and leaves the rest untouched", () => {
238
+ const agentsPath = join(consumer, "AGENTS.md");
239
+ writeFileSync(
240
+ agentsPath,
241
+ "# AGENTS.md\n\nRepo-specific intro.\n\n<!-- imf-web-ui:begin stale -->\nold content\n<!-- imf-web-ui:end -->\n\nRepo-specific outro.\n"
242
+ );
243
+
244
+ runInstall(consumer, "--skills", "--target", "agents");
245
+ const updated = readFileSync(agentsPath, "utf-8");
246
+ expect(updated).toContain("Repo-specific intro.");
247
+ expect(updated).toContain("Repo-specific outro.");
248
+ expect(updated).not.toContain("old content");
249
+ expect(updated).toContain("resynced with `npx web-ui-install`");
250
+
251
+ // no fence yet → block is appended; missing AGENTS.md entirely → left alone
252
+ writeFileSync(agentsPath, "# AGENTS.md\n\nNo fence here.\n");
253
+ runInstall(consumer, "--skills", "--target", "agents");
254
+ const appended = readFileSync(agentsPath, "utf-8");
255
+ expect(appended).toContain("No fence here.");
256
+ expect(appended).toContain("<!-- imf-web-ui:begin");
257
+ });
258
+
259
+ it("ships injection hooks that exit 0 and echo their fixed one-liners", () => {
260
+ runInstall(consumer, "--hooks", "--target", "agents");
261
+ const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
262
+
263
+ const sessionStart = spawnSync("sh", [join(hookDir, "session-start.sh")], { encoding: "utf-8" });
264
+ expect(sessionStart.status).toBe(0);
265
+ expect(sessionStart.stdout).toContain("imf-web-ui skill");
266
+
267
+ // Subagents get the same router pointer, byte for byte.
268
+ const subagentStart = spawnSync("sh", [join(hookDir, "subagent-start.sh")], { encoding: "utf-8" });
269
+ expect(subagentStart.status).toBe(0);
270
+ expect(subagentStart.stdout).toBe(sessionStart.stdout);
271
+
272
+ const promptSubmit = spawnSync("sh", [join(hookDir, "user-prompt-submit.sh")], { encoding: "utf-8" });
273
+ expect(promptSubmit.status).toBe(0);
274
+ expect(promptSubmit.stdout).toContain("imf-web-ui-components");
275
+
276
+ // no node_modules in the consumer → silent success
277
+ const staleness = spawnSync("sh", [join(hookDir, "baseline-staleness.sh")], { cwd: consumer, encoding: "utf-8" });
278
+ expect(staleness.status).toBe(0);
279
+ expect(staleness.stdout).toBe("");
280
+ });
281
+
282
+ it("gates the turn end only after in-session edits, and never loops", () => {
283
+ runInstall(consumer, "--hooks", "--target", "agents");
284
+ const stopHook = join(consumer, ".agents", "hooks", "imf-web-ui", "stop.sh");
285
+ const transcript = join(consumer, "transcript.jsonl");
286
+ const runStop = (input: string) => spawnSync("sh", [stopHook], { cwd: consumer, input, encoding: "utf-8" });
287
+
288
+ // outside a git repo → silent pass
289
+ const noGit = runStop("{}");
290
+ expect(noGit.status).toBe(0);
291
+ expect(noGit.stdout).toBe("");
292
+
293
+ spawnSync("git", ["init", "--quiet", consumer]);
294
+ mkdirSync(join(consumer, "src"), { recursive: true });
295
+ writeFileSync(join(consumer, "src", "app.ts"), "export const x = 1;\n");
296
+ writeFileSync(transcript, '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Write"}]}}\n');
297
+ const payload = JSON.stringify({ transcript_path: transcript, stop_hook_active: false });
298
+
299
+ const blocked = runStop(payload);
300
+ expect(blocked.status).toBe(0);
301
+ expect(blocked.stdout).toContain('"decision":"block"');
302
+ expect(blocked.stdout).toContain("web-ui stop gate:");
303
+
304
+ // re-entry after a block → silent pass
305
+ const reentry = runStop(JSON.stringify({ transcript_path: transcript, stop_hook_active: true }));
306
+ expect(reentry.status).toBe(0);
307
+ expect(reentry.stdout).toBe("");
308
+
309
+ // dirty tree, but this session's transcript shows no edits → silent pass
310
+ writeFileSync(transcript, '{"type":"user","message":{"content":"a question"}}\n');
311
+ const questionTurn = runStop(payload);
312
+ expect(questionTurn.status).toBe(0);
313
+ expect(questionTurn.stdout).toBe("");
314
+ });
315
+
316
+ it("warns from the staleness script when skill markers lag the package", () => {
317
+ runInstall(consumer, "--skills", "--hooks", "--target", "agents");
318
+ const pkgDir = join(consumer, "node_modules", "@imfusion", "web-ui");
319
+ mkdirSync(pkgDir, { recursive: true });
320
+ writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ version: "99.0.0" }));
321
+
322
+ const result = spawnSync("sh", [join(consumer, ".agents", "hooks", "imf-web-ui", "baseline-staleness.sh")], {
323
+ cwd: consumer,
324
+ encoding: "utf-8"
325
+ });
326
+ expect(result.status).toBe(0);
327
+ expect(result.stdout).toContain("npx web-ui-install");
328
+ });
329
+ });
@@ -0,0 +1,20 @@
1
+ import { Plugin } from 'vite';
2
+ export interface ReadableCssModuleNamesOptions {
3
+ /** Namespace for every generated class name. Short, stable, app-scoped (`imf-ui`, `acme`). */
4
+ prefix: string;
5
+ }
6
+ /**
7
+ * Generates readable CSS Module class names instead of the default hash, so the DOM
8
+ * stays legible in devtools and browser automation. `button.module.css` yields
9
+ * `{prefix}-button-module-root` — `[name]` is the filename, which keeps its `.module`
10
+ * suffix.
11
+ *
12
+ * Register it in every tool that compiles the CSS (the app build AND Storybook). A
13
+ * compiler left out generates different class names for the same source file and its
14
+ * styles silently don't apply.
15
+ *
16
+ * Defaults to opting into Lightning CSS, which drops `css.modules` options it has no
17
+ * equivalent for (`localsConvention`). Set `css.transformer: "postcss"` to stay on
18
+ * the PostCSS pipeline.
19
+ */
20
+ export declare function readableCssModuleNames({ prefix }: ReadableCssModuleNamesOptions): Plugin;
@@ -0,0 +1,17 @@
1
+ //#region src/build/vite-css-module-names/index.ts
2
+ function e(e, t) {
3
+ let { lightningcss: n, modules: r, transformer: i } = e.css ?? {}, a = n?.cssModules;
4
+ if (!(typeof a == "boolean" || r === !1) && !a?.pattern && !(typeof r == "object" && r.generateScopedName)) return i === "postcss" ? { css: { modules: { generateScopedName: t } } } : { css: {
5
+ transformer: "lightningcss",
6
+ lightningcss: { cssModules: { pattern: t } }
7
+ } };
8
+ }
9
+ function t({ prefix: t }) {
10
+ let n = `${t}-[name]-[local]`;
11
+ return {
12
+ name: "imf-ui:readable-css-module-names",
13
+ config: (t) => e(t, n)
14
+ };
15
+ }
16
+ //#endregion
17
+ export { t as readableCssModuleNames };
@@ -0,0 +1,11 @@
1
+ //#region \0rolldown/runtime.js
2
+ var e = Object.defineProperty, t = (t, n) => {
3
+ let r = {};
4
+ for (var i in t) e(r, i, {
5
+ get: t[i],
6
+ enumerable: !0
7
+ });
8
+ return n || e(r, Symbol.toStringTag, { value: "Module" }), r;
9
+ };
10
+ //#endregion
11
+ export { t };
@@ -0,0 +1,136 @@
1
+ import { t as e } from "./chunk-DmhlhrBa.js";
2
+ import { t } from "./meta-CySnRuVp.js";
3
+ import { Ma as n, Qt as r, Xo as i, hn as a, r as o } from "./icons-wBmF0U2x.js";
4
+ import { cx as s } from "class-variance-authority";
5
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
+ import { useCallback as u, useEffect as d, useRef as f, useState as p } from "react";
7
+ //#region src/hooks/use-clipboard.ts
8
+ function m({ timeout: e = 1500 } = {}) {
9
+ let [t, n] = p(!1), r = f(void 0);
10
+ return d(() => () => clearTimeout(r.current), []), {
11
+ copied: t,
12
+ copy: u((t) => {
13
+ navigator.clipboard.writeText(t).then(() => {
14
+ n(!0), clearTimeout(r.current), r.current = setTimeout(() => n(!1), e);
15
+ });
16
+ }, [e])
17
+ };
18
+ }
19
+ var h = {
20
+ block: "imf-ui-code-module-block",
21
+ copy: "imf-ui-code-module-copy",
22
+ icon: "imf-ui-code-module-icon",
23
+ inline: "imf-ui-code-module-inline",
24
+ inlineCopy: "imf-ui-code-module-inlineCopy",
25
+ inlineLink: "imf-ui-code-module-inlineLink",
26
+ language: "imf-ui-code-module-language",
27
+ pre: "imf-ui-code-module-pre",
28
+ separator: "imf-ui-code-module-separator"
29
+ }, g = /* @__PURE__ */ e({
30
+ Block: () => y,
31
+ Inline: () => _,
32
+ InlineLink: () => v
33
+ });
34
+ function _({ className: e, copyable: t = !1, children: r, ...a }) {
35
+ let { copied: u, copy: d } = m(), p = f(null), g = u ? n : i;
36
+ return /* @__PURE__ */ l("code", {
37
+ ...a,
38
+ "data-imf-ui-component": "Code.Inline",
39
+ className: s(h.inline, e),
40
+ children: [
41
+ /* @__PURE__ */ c("span", {
42
+ ref: p,
43
+ children: r
44
+ }),
45
+ t && /* @__PURE__ */ c("span", { className: h.separator }),
46
+ t && /* @__PURE__ */ c("button", {
47
+ type: "button",
48
+ className: h.inlineCopy,
49
+ "data-copied": u || void 0,
50
+ "aria-label": u ? "Copied" : "Copy",
51
+ onClick: () => d(p.current?.textContent ?? ""),
52
+ children: /* @__PURE__ */ c(o, {
53
+ glyph: g,
54
+ size: 12,
55
+ strokeWidth: 2,
56
+ "aria-hidden": !0
57
+ })
58
+ })
59
+ ]
60
+ });
61
+ }
62
+ _.displayName = "Code.Inline";
63
+ function v({ className: e, kind: t = "external", children: n, ...i }) {
64
+ let u = t === "external" ? a : r;
65
+ return /* @__PURE__ */ l("a", {
66
+ ...t === "external" ? {
67
+ target: "_blank",
68
+ rel: "noreferrer"
69
+ } : {},
70
+ ...i,
71
+ "data-imf-ui-component": "Code.InlineLink",
72
+ "data-kind": t,
73
+ className: s(h.inline, h.inlineLink, e),
74
+ children: [
75
+ n,
76
+ /* @__PURE__ */ c("span", { className: h.separator }),
77
+ /* @__PURE__ */ c("span", {
78
+ className: h.icon,
79
+ "aria-hidden": !0,
80
+ children: /* @__PURE__ */ c(o, {
81
+ glyph: u,
82
+ size: 12,
83
+ strokeWidth: 2
84
+ })
85
+ })
86
+ ]
87
+ });
88
+ }
89
+ v.displayName = "Code.InlineLink";
90
+ function y({ className: e, language: t, hideCopy: r = !1, children: a, ...u }) {
91
+ let { copied: d, copy: p } = m(), g = f(null), _ = d ? n : i;
92
+ return /* @__PURE__ */ l("div", {
93
+ "data-imf-ui-component": "Code.Block",
94
+ className: s(h.block, e),
95
+ children: [
96
+ t != null && /* @__PURE__ */ c("span", {
97
+ className: h.language,
98
+ children: t
99
+ }),
100
+ !r && /* @__PURE__ */ c("button", {
101
+ type: "button",
102
+ className: h.copy,
103
+ "data-copied": d || void 0,
104
+ "aria-label": d ? "Copied" : "Copy",
105
+ onClick: () => p(g.current?.textContent ?? ""),
106
+ children: /* @__PURE__ */ c(o, {
107
+ glyph: _,
108
+ size: 12,
109
+ strokeWidth: 2,
110
+ "aria-hidden": !0
111
+ })
112
+ }),
113
+ /* @__PURE__ */ c("pre", {
114
+ ...u,
115
+ className: h.pre,
116
+ children: /* @__PURE__ */ c("code", {
117
+ ref: g,
118
+ children: a
119
+ })
120
+ })
121
+ ]
122
+ });
123
+ }
124
+ y.displayName = "Code.Block";
125
+ //#endregion
126
+ //#region src/components/code/code.meta.ts
127
+ var b = {
128
+ name: "Code",
129
+ category: t.display,
130
+ status: "stable",
131
+ namespace: !0,
132
+ description: "Displays source code — Code.Inline for a fragment in running text, Code.Block for a fenced block with an optional language label and copy button. Also called a code snippet or code block.",
133
+ origin: "native"
134
+ };
135
+ //#endregion
136
+ export { g as a, v as i, y as n, m as o, _ as r, b as t };
@@ -0,0 +1,24 @@
1
+ export type IconMetadata = {
2
+ filename: string;
3
+ category: string;
4
+ tags: string[];
5
+ };
6
+ export type IconCatalogRecord = {
7
+ name: string;
8
+ category: string;
9
+ tags: string[];
10
+ style: "regular" | "solid";
11
+ };
12
+ type IconMetadataSource = {
13
+ commit: string;
14
+ url: string;
15
+ };
16
+ export declare function parseIconMetadata(csv: string): IconMetadata[];
17
+ export declare function validateIconMetadataSource(source: string): IconMetadataSource;
18
+ export declare function buildIconCatalog(iconNames: readonly string[], metadata: readonly IconMetadata[]): IconCatalogRecord[];
19
+ export declare function renderIconAliases(iconNames: readonly string[]): string;
20
+ export declare function renderIconCatalogJson(records: readonly IconCatalogRecord[]): string;
21
+ export declare function renderIconCatalog(records: readonly IconCatalogRecord[]): string;
22
+ export declare function writeIconAliases(): void;
23
+ export declare function writeIconCatalog(): void;
24
+ export {};
@@ -19,7 +19,7 @@ declare namespace Root {
19
19
  type IconProps = React.HTMLAttributes<HTMLSpanElement>;
20
20
  /**
21
21
  * Callout.Icon — the leading status glyph. With no children it renders the
22
- * variant's default lucide icon; pass a custom icon as children to override.
22
+ * variant's default local icon; pass a custom icon as children to override.
23
23
  * `aria-hidden` (the variant is conveyed by copy, not the glyph) and inherits
24
24
  * the status colour via `currentColor`.
25
25
  */
@@ -11,7 +11,7 @@ type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & VariantProps<typeof
11
11
  /**
12
12
  * ChipLink — anchor styled as a Chip. Reuses Chip's shared variants so the
13
13
  * variant and appearance axes stay aligned, and auto-appends a directional icon
14
- * (Lucide `ArrowUpRight` for external, `ArrowRight` for internal).
14
+ * (local Web UI `ArrowUpRight` for external, `ArrowRight` for internal).
15
15
  */
16
16
  export declare function ChipLink({ variant, appearance, inline, kind, className, children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
17
17
  export {};
@@ -1,19 +1,15 @@
1
1
  type InlineProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
2
- /** The inline code, as a raw string — also the value copied when `copyable`. */
3
- children: string;
2
+ /** Code content shown in the chip. The copy button copies its rendered text. */
3
+ children: React.ReactNode;
4
4
  /** Show a small copy button on the right, behind a hairline separator. @default false */
5
5
  copyable?: boolean;
6
- /** Pre-rendered display content (e.g. syntax-highlighted tokens) shown in place of `children`, which stays the copied value. The code-highlight integration fills this slot. */
7
- highlighted?: React.ReactNode;
8
6
  };
9
7
  /**
10
8
  * Code.Inline — an inline code fragment (a `<code>` chip) for use inside running
11
- * text. Dumb: styling only — syntax coloring arrives pre-rendered through the
12
- * `highlighted` slot (the code-highlight integration supplies it). `Typo.InlineCode`
13
- * renders this under the hood so inline-code chrome is defined in one place.
14
- * Children must be a string so an opted-in copy button has clean text to copy.
9
+ * text. `Typo.InlineCode` renders this under the hood so inline-code chrome is
10
+ * defined in one place. The copy button copies the rendered text.
15
11
  */
16
- export declare function Inline({ className, copyable, highlighted, children, ...props }: InlineProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function Inline({ className, copyable, children, ...props }: InlineProps): import("react/jsx-runtime").JSX.Element;
17
13
  export declare namespace Inline {
18
14
  var displayName: string;
19
15
  }
@@ -36,24 +32,20 @@ export declare namespace InlineLink {
36
32
  var displayName: string;
37
33
  }
38
34
  type BlockProps = Omit<React.HTMLAttributes<HTMLPreElement>, "children"> & {
39
- /** The code to display, as a raw string — also the value copied by the copy button. */
40
- children: string;
35
+ /** Code content shown in the block. The copy button copies its rendered text. */
36
+ children: React.ReactNode;
41
37
  /** Language label shown in the corner notch. Omit to hide the notch. */
42
38
  language?: string;
43
39
  /** Hide the copy button. @default false */
44
40
  hideCopy?: boolean;
45
- /** Pre-rendered display content (e.g. syntax-highlighted tokens) shown in place of `children`, which stays the copied value. The code-highlight integration fills this slot. */
46
- highlighted?: React.ReactNode;
47
41
  };
48
42
  /**
49
43
  * Code.Block — a fenced code block on a flat brand surface, with an optional
50
44
  * chamfered language notch (top-left) and a copy button (top-right). The
51
- * surface follows the global corner-radius token when a preset opts in.
52
- * Dumb: styling only — syntax coloring arrives pre-rendered through the
53
- * `highlighted` slot (the code-highlight integration supplies it). Children
54
- * must be a string so the copy button has clean text to copy.
45
+ * surface follows the global corner-radius token when a preset opts in. The
46
+ * copy button copies the rendered text.
55
47
  */
56
- export declare function Block({ className, language, hideCopy, highlighted, children, ...props }: BlockProps): import("react/jsx-runtime").JSX.Element;
48
+ export declare function Block({ className, language, hideCopy, children, ...props }: BlockProps): import("react/jsx-runtime").JSX.Element;
57
49
  export declare namespace Block {
58
50
  var displayName: string;
59
51
  }