@imfusion/web-ui 0.5.1-dev.25.gf0207a31 → 0.5.1-dev.3.g8e0a047a

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 (55) hide show
  1. package/README.md +55 -148
  2. package/bin/install-skill.js +180 -0
  3. package/dist/index.d.ts +0 -2
  4. package/dist/index.js +4317 -4956
  5. package/dist/integrations/image-display-options.js +1 -1
  6. package/dist/style.css +1 -1
  7. package/dist/{tabs-CVp_SgBl.js → tabs-DqBFSqq6.js} +1 -1
  8. package/package.json +24 -35
  9. package/src/docgen/doc.gen.json +0 -327
  10. package/src/llms/llms.gen.txt +0 -12
  11. package/src/llms/skills/imf-web-ui/SKILL.md +11 -15
  12. package/src/llms/skills/imf-web-ui-components/SKILL.md +1 -1
  13. package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +93 -0
  14. package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +133 -0
  15. package/src/llms/skills/{imf-web-ui-frontend-conventions/references/react.md → imf-web-ui-frontend-patterns/references/react-patterns.md} +12 -26
  16. package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +201 -0
  17. package/src/llms/skills/imf-web-ui-setup/SKILL.md +57 -0
  18. package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
  19. package/src/llms/skills/imf-web-ui-ux/references/forms.md +1 -1
  20. package/bin/install.js +0 -343
  21. package/bin/install.test.ts +0 -166
  22. package/dist/build/vite-css-module-names/index.d.ts +0 -20
  23. package/dist/build/vite-css-module-names.js +0 -17
  24. package/dist/components/field/field.d.ts +0 -104
  25. package/dist/components/field/field.meta.d.ts +0 -2
  26. package/dist/components/field/index.d.ts +0 -2
  27. package/dist/components/fieldset/fieldset.d.ts +0 -29
  28. package/dist/components/fieldset/fieldset.meta.d.ts +0 -2
  29. package/dist/components/fieldset/index.d.ts +0 -2
  30. package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +0 -71
  31. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/baseline-staleness.sh +0 -17
  32. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/post-tool-use.sh +0 -21
  33. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/session-start.sh +0 -4
  34. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/user-prompt-submit.sh +0 -4
  35. package/src/llms/skills/imf-web-ui-agent-setup/templates/settings.json +0 -37
  36. package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +0 -45
  37. package/src/llms/skills/imf-web-ui-frontend-conventions/references/assets.md +0 -23
  38. package/src/llms/skills/imf-web-ui-frontend-conventions/references/class-names.md +0 -42
  39. package/src/llms/skills/imf-web-ui-frontend-conventions/references/components.md +0 -63
  40. package/src/llms/skills/imf-web-ui-frontend-conventions/references/data.md +0 -196
  41. package/src/llms/skills/imf-web-ui-frontend-conventions/references/docs-structure.md +0 -44
  42. package/src/llms/skills/imf-web-ui-frontend-conventions/references/git.md +0 -33
  43. package/src/llms/skills/imf-web-ui-frontend-conventions/references/library-boundary.md +0 -37
  44. package/src/llms/skills/imf-web-ui-frontend-conventions/references/npm-project.md +0 -57
  45. package/src/llms/skills/imf-web-ui-frontend-conventions/references/project-structure.md +0 -21
  46. package/src/llms/skills/imf-web-ui-frontend-conventions/references/stack.md +0 -39
  47. package/src/llms/skills/imf-web-ui-frontend-conventions/references/styling.md +0 -91
  48. package/src/llms/skills/imf-web-ui-frontend-conventions/references/testing.md +0 -16
  49. package/src/llms/skills/imf-web-ui-frontend-conventions/references/tooling.md +0 -76
  50. package/src/llms/skills/imf-web-ui-frontend-conventions/references/typescript.md +0 -45
  51. package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +0 -89
  52. package/src/llms/skills/imf-web-ui-frontend-setup/templates/AGENTS.md +0 -34
  53. package/src/llms/skills/imf-web-ui-frontend-setup/templates/README.md +0 -27
  54. package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +0 -36
  55. package/src/llms/skills/imf-web-ui-update/SKILL.md +0 -165
package/bin/install.js DELETED
@@ -1,343 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Installs this package's agent tooling into the consumer project: the
4
- // skill family (../src/llms/skills/imf-web-ui*/) and the agent lifecycle
5
- // hooks the frontend-setup skill ships as templates. Runs in the
6
- // CONSUMER's environment, so it may only use this package's real runtime
7
- // dependencies (@clack/prompts). Never wire this into a postinstall hook:
8
- // ambient script execution on `npm install` is a live supply-chain attack
9
- // vector — install stays an explicit, user-run command.
10
- //
11
- // The skills cross-reference each other, so they install as one bundle.
12
- // When both targets are selected, .agents/skills/ holds the real copy and
13
- // .claude/skills/ symlinks it (this repo's own convention) so the two
14
- // can't drift apart.
15
- //
16
- // Flags: bare = skills only; --hooks adds the agent lifecycle hooks (driven
17
- // by the imf-web-ui-agent-setup skill, which checks the repo's existing hooks
18
- // first). --target claude|agents (repeatable) for scripted installs,
19
- // --reconfigure to re-open the target prompt on an existing install.
20
-
21
- import {
22
- chmodSync,
23
- cpSync,
24
- existsSync,
25
- lstatSync,
26
- mkdirSync,
27
- readdirSync,
28
- readFileSync,
29
- rmSync,
30
- symlinkSync,
31
- writeFileSync
32
- } from "node:fs";
33
- import { dirname, relative, resolve } from "node:path";
34
- import { fileURLToPath } from "node:url";
35
- import * as p from "@clack/prompts";
36
-
37
- const SKILL_PREFIX = "imf-web-ui";
38
- const VERSION_MARKER = ".imf-web-ui-skill-version.json";
39
-
40
- const here = dirname(fileURLToPath(import.meta.url));
41
- // Source is relative to THIS SCRIPT's location (inside node_modules), not
42
- // the consumer's cwd — the script is invoked from the consumer's project
43
- // root, but the skills it copies ship alongside this file in the package.
44
- const skillsRoot = resolve(here, "..", "src", "llms", "skills");
45
- const packageJson = JSON.parse(readFileSync(resolve(here, "..", "package.json"), "utf-8"));
46
- const currentVersion = packageJson.version;
47
-
48
- // Destination is relative to the consumer's project root (cwd), since
49
- // that's where their `.claude/` or `.agents/` directory lives.
50
- const projectRoot = process.cwd();
51
-
52
- const TARGETS = {
53
- claude: { label: "Claude Code", root: resolve(projectRoot, ".claude", "skills") },
54
- agents: { label: "Vendor-neutral (.agents/)", root: resolve(projectRoot, ".agents", "skills") }
55
- };
56
-
57
- function discoverSkills() {
58
- if (!existsSync(skillsRoot)) return [];
59
- return readdirSync(skillsRoot, { withFileTypes: true })
60
- .filter(entry => entry.isDirectory() && entry.name.startsWith(SKILL_PREFIX))
61
- .map(entry => entry.name)
62
- .sort();
63
- }
64
-
65
- function displayPath(absPath) {
66
- return `./${relative(projectRoot, absPath)}`;
67
- }
68
-
69
- function readInstalledVersion(dir) {
70
- const markerPath = resolve(dir, VERSION_MARKER);
71
- if (!existsSync(markerPath)) return null;
72
- try {
73
- return JSON.parse(readFileSync(markerPath, "utf-8")).version ?? null;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- function writeRealCopy(sourceDir, destDir) {
80
- mkdirSync(dirname(destDir), { recursive: true });
81
- // force: true makes re-running after a version bump overwrite cleanly.
82
- cpSync(sourceDir, destDir, { recursive: true, force: true });
83
- writeFileSync(resolve(destDir, VERSION_MARKER), JSON.stringify({ version: currentVersion }, null, 2) + "\n");
84
- }
85
-
86
- function writeSymlink(linkPath, targetPath) {
87
- mkdirSync(dirname(linkPath), { recursive: true });
88
- // lstat (not existsSync, which follows symlinks) catches a broken/stale
89
- // symlink left over from a prior run, not just a real file or directory.
90
- if (lstatSync(linkPath, { throwIfNoEntry: false })) {
91
- rmSync(linkPath, { recursive: true, force: true });
92
- }
93
- symlinkSync(relative(dirname(linkPath), targetPath), linkPath);
94
- }
95
-
96
- function removeOrphanedSkills(selected, skills, both) {
97
- const bundledSkills = new Set(skills);
98
- const selectedTargets = both ? ["agents", "claude"] : selected;
99
-
100
- for (const key of selectedTargets) {
101
- const { root } = TARGETS[key];
102
- if (!existsSync(root)) continue;
103
-
104
- const orphanedSkills = readdirSync(root).filter(name => name.startsWith(`${SKILL_PREFIX}-`) && !bundledSkills.has(name));
105
- for (const name of orphanedSkills) {
106
- rmSync(resolve(root, name), { recursive: true, force: true });
107
- // In the two-target layout, .claude/ is a symlinked view of the
108
- // canonical .agents/ copy. Remove its matching link along with the
109
- // orphaned canonical skill.
110
- if (both && key === "agents") {
111
- rmSync(resolve(TARGETS.claude.root, name), { recursive: true, force: true });
112
- }
113
- p.log.info(`Removed orphaned skill: ${name} (no longer in this bundle)`);
114
- }
115
- }
116
- }
117
-
118
- // A prior install is recoverable from the filesystem, so a re-run doesn't
119
- // re-ask: a target counts as chosen when any bundled skill is present under
120
- // it. Symlinks count — they're how the both-targets layout represents
121
- // .claude/, and lstat avoids following them into the real copy.
122
- function detectInstalledTargets(skills) {
123
- return Object.keys(TARGETS).filter(key =>
124
- skills.some(name => lstatSync(resolve(TARGETS[key].root, name), { throwIfNoEntry: false }))
125
- );
126
- }
127
-
128
- // Hook templates ship inside the agent-setup skill so they're readable as
129
- // part of its documentation; the installer copies the scripts into the
130
- // consumer's .agents/hooks/ and merges the registrations into
131
- // .claude/settings.json.
132
- const hooksSourceDir = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "hooks");
133
- const hooksSettingsTemplate = resolve(skillsRoot, "imf-web-ui-agent-setup", "templates", "settings.json");
134
- // Installer-owned subdirectory: refreshed wholesale on every run, so a repo's
135
- // own hooks in .agents/hooks/ are never touched.
136
- const hooksDestDir = resolve(projectRoot, ".agents", "hooks", "imf-web-ui");
137
- const settingsPath = resolve(projectRoot, ".claude", "settings.json");
138
-
139
- function installHookScripts() {
140
- const scripts = readdirSync(hooksSourceDir).filter(name => name.endsWith(".sh"));
141
- mkdirSync(hooksDestDir, { recursive: true });
142
- for (const name of scripts) {
143
- const dest = resolve(hooksDestDir, name);
144
- cpSync(resolve(hooksSourceDir, name), dest, { force: true });
145
- chmodSync(dest, 0o755);
146
- }
147
- return scripts.length;
148
- }
149
-
150
- // Merge, never clobber: a registration is added only when no existing entry
151
- // for that event already runs the same command, so re-runs are idempotent
152
- // and hand-written settings survive.
153
- function mergeHookRegistrations() {
154
- const template = JSON.parse(readFileSync(hooksSettingsTemplate, "utf-8"));
155
- let settings = {};
156
- if (existsSync(settingsPath)) {
157
- try {
158
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
159
- } catch {
160
- return null;
161
- }
162
- }
163
- settings.hooks ??= {};
164
- let added = 0;
165
- for (const [event, entries] of Object.entries(template.hooks)) {
166
- settings.hooks[event] ??= [];
167
- for (const entry of entries) {
168
- const commands = entry.hooks.map(hook => hook.command);
169
- const present = settings.hooks[event].some(existing =>
170
- (existing.hooks ?? []).some(hook => commands.includes(hook.command))
171
- );
172
- if (!present) {
173
- settings.hooks[event].push(entry);
174
- added++;
175
- }
176
- }
177
- }
178
- mkdirSync(dirname(settingsPath), { recursive: true });
179
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
180
- return added;
181
- }
182
-
183
- // The AGENTS.md baseline note lives inside a fenced block owned by this
184
- // installer. The template between the markers is the source of truth; an
185
- // existing AGENTS.md gets the block replaced in place (or appended when
186
- // absent), everything outside the fence is untouched. No AGENTS.md at all
187
- // is left alone — scaffolding one is the setup skill's job.
188
- const AGENTS_BLOCK_BEGIN = "<!-- imf-web-ui:begin";
189
- const AGENTS_BLOCK_END = "<!-- imf-web-ui:end -->";
190
- const agentsTemplatePath = resolve(skillsRoot, "imf-web-ui-frontend-setup", "templates", "AGENTS.md");
191
- const agentsPath = resolve(projectRoot, "AGENTS.md");
192
-
193
- function extractAgentsBlock(content) {
194
- const begin = content.indexOf(AGENTS_BLOCK_BEGIN);
195
- const end = content.indexOf(AGENTS_BLOCK_END);
196
- if (begin === -1 || end === -1) return null;
197
- return content.slice(begin, end + AGENTS_BLOCK_END.length);
198
- }
199
-
200
- function upsertAgentsBlock() {
201
- if (!existsSync(agentsPath)) return "absent";
202
- const block = extractAgentsBlock(readFileSync(agentsTemplatePath, "utf-8"));
203
- if (!block) return "absent";
204
- const current = readFileSync(agentsPath, "utf-8");
205
- const existing = extractAgentsBlock(current);
206
- const next = existing ? current.replace(existing, block) : `${current.trimEnd()}\n\n${block}\n`;
207
- if (next === current) return "unchanged";
208
- writeFileSync(agentsPath, next);
209
- return existing ? "updated" : "added";
210
- }
211
-
212
- // --target claude|agents (repeatable) selects targets without the
213
- // interactive prompt — for CI and scripted installs.
214
- function parseTargetFlags(argv) {
215
- const targets = [];
216
- for (let i = 0; i < argv.length; i++) {
217
- if (argv[i] !== "--target") continue;
218
- const value = argv[i + 1];
219
- if (!value || !(value in TARGETS)) {
220
- console.error(`--target expects one of: ${Object.keys(TARGETS).join(", ")}`);
221
- process.exit(1);
222
- }
223
- targets.push(value);
224
- i++;
225
- }
226
- return targets;
227
- }
228
-
229
- async function main() {
230
- const skills = discoverSkills();
231
- const argv = process.argv.slice(2);
232
- const flagTargets = parseTargetFlags(argv);
233
- const reconfigure = argv.includes("--reconfigure");
234
- // Bare invocation installs skills only. Hooks are opt-in via --hooks — the
235
- // imf-web-ui-agent-setup skill drives that after checking what the repo
236
- // already registers; the binary stays the dumb mechanical tail.
237
- const wantHooks = argv.includes("--hooks");
238
- const wantSkills = argv.includes("--skills") || !wantHooks;
239
-
240
- const components = [wantSkills && `${skills.length} skills`, wantHooks && "agent hooks"].filter(Boolean);
241
- p.intro(`@imfusion/web-ui install — ${components.join(" + ")}`);
242
-
243
- if (wantSkills && skills.length === 0) {
244
- p.log.error(`No skills found at ${skillsRoot}. Reinstall @imfusion/web-ui and try again.`);
245
- p.outro("Nothing installed.");
246
- process.exitCode = 1;
247
- return;
248
- }
249
-
250
- if (wantHooks) {
251
- const scriptCount = installHookScripts();
252
- const added = mergeHookRegistrations();
253
- p.log.success(`Agent hooks -> ${displayPath(hooksDestDir)} (${scriptCount} scripts)`);
254
- if (added === null) {
255
- p.log.warn(`${displayPath(settingsPath)} is not valid JSON — registrations not merged, fix it and re-run.`);
256
- } else if (added > 0) {
257
- p.log.success(`Registered ${added} hook(s) in ${displayPath(settingsPath)}`);
258
- } else {
259
- p.log.info(`Hook registrations already present in ${displayPath(settingsPath)}`);
260
- }
261
- if (!wantSkills) {
262
- p.outro("Done — hooks installed.");
263
- return;
264
- }
265
- }
266
-
267
- const installedTargets = detectInstalledTargets(skills);
268
-
269
- const existingVersions = Object.values(TARGETS)
270
- .flatMap(({ root }) => skills.map(name => readInstalledVersion(resolve(root, name))))
271
- .filter(Boolean);
272
- if (existingVersions.length > 0 && existingVersions.every(v => v === currentVersion)) {
273
- p.log.info(`Already up to date (v${currentVersion}). Re-running will overwrite with the same content.`);
274
- } else if (existingVersions.some(v => v !== currentVersion)) {
275
- const from = existingVersions.find(v => v !== currentVersion);
276
- p.log.info(`Updating installed skills from v${from} to v${currentVersion}.`);
277
- }
278
-
279
- p.log.message(`Skills in this bundle:\n${skills.map(name => ` - ${name}`).join("\n")}`);
280
-
281
- let selected;
282
- if (flagTargets.length > 0) {
283
- selected = flagTargets;
284
- p.log.info(`Targets from --target flags: ${selected.join(", ")}`);
285
- } else if (installedTargets.length > 0 && !reconfigure) {
286
- selected = installedTargets;
287
- p.log.info(
288
- `Refreshing the existing install: ${selected.map(key => displayPath(TARGETS[key].root)).join(", ")}` +
289
- ` — pass --reconfigure to choose different targets.`
290
- );
291
- } else {
292
- selected = await p.multiselect({
293
- message: "Install into which skill directory (or directories)?",
294
- options: Object.entries(TARGETS).map(([key, { label, root }]) => ({
295
- value: key,
296
- label,
297
- hint: displayPath(root)
298
- })),
299
- required: true
300
- });
301
-
302
- if (p.isCancel(selected)) {
303
- p.cancel("Cancelled — nothing installed.");
304
- return;
305
- }
306
- }
307
-
308
- const both = selected.includes("claude") && selected.includes("agents");
309
-
310
- for (const name of skills) {
311
- const sourceDir = resolve(skillsRoot, name);
312
- if (both) {
313
- // .agents/ is the canonical real copy; .claude/ aliases it via symlink.
314
- const realDir = resolve(TARGETS.agents.root, name);
315
- writeRealCopy(sourceDir, realDir);
316
- writeSymlink(resolve(TARGETS.claude.root, name), realDir);
317
- } else {
318
- for (const key of selected) {
319
- writeRealCopy(sourceDir, resolve(TARGETS[key].root, name));
320
- }
321
- }
322
- }
323
-
324
- removeOrphanedSkills(selected, skills, both);
325
-
326
- if (both) {
327
- p.log.success(`Vendor-neutral (.agents/) -> ${displayPath(TARGETS.agents.root)}/${SKILL_PREFIX}*`);
328
- p.log.success(`Claude Code -> ${displayPath(TARGETS.claude.root)}/${SKILL_PREFIX}* (symlinks -> .agents/)`);
329
- } else {
330
- for (const key of selected) {
331
- p.log.success(`${TARGETS[key].label} -> ${displayPath(TARGETS[key].root)}/${SKILL_PREFIX}*`);
332
- }
333
- }
334
-
335
- const agentsResult = upsertAgentsBlock();
336
- if (agentsResult === "updated" || agentsResult === "added") {
337
- p.log.success(`Refreshed the imf-web-ui block in ${displayPath(agentsPath)}`);
338
- }
339
-
340
- p.outro(`Done — ${skills.length} skills installed${wantHooks ? " + agent hooks" : ""}.`);
341
- }
342
-
343
- await main();
@@ -1,166 +0,0 @@
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
-
35
- beforeEach(() => {
36
- consumer = mkdtempSync(join(tmpdir(), "web-ui-install-"));
37
- });
38
-
39
- describe("web-ui-install", () => {
40
- it("installs skills into both targets with version markers", () => {
41
- runInstall(consumer, "--skills", "--target", "agents", "--target", "claude");
42
-
43
- const installed = readdirSync(join(consumer, ".agents", "skills"));
44
- expect(installed).toContain("imf-web-ui");
45
- expect(installed).toContain("imf-web-ui-frontend-conventions");
46
- expect(installed).toContain("imf-web-ui-frontend-setup");
47
-
48
- for (const name of installed) {
49
- expect(existsSync(join(consumer, ".agents", "skills", name, ".imf-web-ui-skill-version.json"))).toBe(true);
50
- expect(lstatSync(join(consumer, ".claude", "skills", name)).isSymbolicLink()).toBe(true);
51
- }
52
- });
53
-
54
- it("removes skills that are no longer included in the bundle", () => {
55
- const orphan = "imf-web-ui-retired-skill";
56
- const agentsOrphan = join(consumer, ".agents", "skills", orphan);
57
- mkdirSync(agentsOrphan, { recursive: true });
58
- writeFileSync(join(agentsOrphan, "SKILL.md"), "# Retired skill\n");
59
- const claudeOrphan = join(consumer, ".claude", "skills", orphan);
60
- mkdirSync(join(consumer, ".claude", "skills"), { recursive: true });
61
- symlinkSync(agentsOrphan, claudeOrphan);
62
- expect(lstatSync(claudeOrphan).isSymbolicLink()).toBe(true);
63
-
64
- const result = runInstall(consumer, "--skills", "--target", "agents", "--target", "claude");
65
-
66
- expect(existsSync(agentsOrphan)).toBe(false);
67
- expect(lstatSync(claudeOrphan, { throwIfNoEntry: false })).toBeUndefined();
68
- expect(result.stdout).toContain(`Removed orphaned skill: ${orphan} (no longer in this bundle)`);
69
- });
70
-
71
- it("installs hooks into an installer-owned directory and registers them", () => {
72
- runInstall(consumer, "--hooks");
73
-
74
- const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
75
- const scripts = readdirSync(hookDir);
76
- expect(scripts).toEqual(expect.arrayContaining(["session-start.sh", "user-prompt-submit.sh", "post-tool-use.sh"]));
77
- for (const script of scripts) {
78
- expect(statSync(join(hookDir, script)).mode & 0o100).toBeTruthy();
79
- }
80
-
81
- const settings = readSettings(consumer);
82
- expect(Object.keys(settings.hooks)).toEqual(expect.arrayContaining(["SessionStart", "UserPromptSubmit", "PostToolUse"]));
83
- // --hooks alone installs no skills
84
- expect(existsSync(join(consumer, ".agents", "skills"))).toBe(false);
85
- });
86
-
87
- it("merges registrations without clobbering existing settings, idempotently", () => {
88
- mkdirSync(join(consumer, ".claude"), { recursive: true });
89
- writeFileSync(
90
- join(consumer, ".claude", "settings.json"),
91
- JSON.stringify({
92
- permissions: { allow: ["Bash(git *)"] },
93
- hooks: { PostToolUse: [{ matcher: "Edit", hooks: [{ type: "command", command: "echo existing" }] }] }
94
- })
95
- );
96
-
97
- runInstall(consumer, "--hooks");
98
- runInstall(consumer, "--hooks");
99
-
100
- const settings = readSettings(consumer);
101
- expect(settings.permissions?.allow).toEqual(["Bash(git *)"]);
102
- const commands = settings.hooks.PostToolUse.flatMap(entry => entry.hooks.map(hook => hook.command));
103
- expect(commands).toContain("echo existing");
104
- // the shipped registration is added exactly once across both runs
105
- expect(commands.filter(command => command.includes("post-tool-use.sh"))).toHaveLength(1);
106
- });
107
-
108
- it("upserts the fenced AGENTS.md block and leaves the rest untouched", () => {
109
- const agentsPath = join(consumer, "AGENTS.md");
110
- writeFileSync(
111
- agentsPath,
112
- "# 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"
113
- );
114
-
115
- runInstall(consumer, "--skills", "--target", "agents");
116
- const updated = readFileSync(agentsPath, "utf-8");
117
- expect(updated).toContain("Repo-specific intro.");
118
- expect(updated).toContain("Repo-specific outro.");
119
- expect(updated).not.toContain("old content");
120
- expect(updated).toContain("resynced with `npx web-ui-install`");
121
-
122
- // no fence yet → block is appended; missing AGENTS.md entirely → left alone
123
- writeFileSync(agentsPath, "# AGENTS.md\n\nNo fence here.\n");
124
- runInstall(consumer, "--skills", "--target", "agents");
125
- const appended = readFileSync(agentsPath, "utf-8");
126
- expect(appended).toContain("No fence here.");
127
- expect(appended).toContain("<!-- imf-web-ui:begin");
128
- });
129
-
130
- it("ships hooks that stay advisory: exit 0 with and without a usable payload", () => {
131
- runInstall(consumer, "--hooks");
132
- const hookDir = join(consumer, ".agents", "hooks", "imf-web-ui");
133
-
134
- const noFile = spawnSync("sh", [join(hookDir, "post-tool-use.sh")], { input: "{}", encoding: "utf-8" });
135
- expect(noFile.status).toBe(0);
136
-
137
- const missingFile = spawnSync("sh", [join(hookDir, "post-tool-use.sh")], {
138
- input: JSON.stringify({ tool_input: { file_path: join(consumer, "nope.ts") } }),
139
- encoding: "utf-8"
140
- });
141
- expect(missingFile.status).toBe(0);
142
-
143
- const sessionStart = spawnSync("sh", [join(hookDir, "session-start.sh")], { encoding: "utf-8" });
144
- expect(sessionStart.status).toBe(0);
145
- expect(sessionStart.stdout).toContain("imf-web-ui-frontend-conventions");
146
-
147
- // no node_modules in the consumer → silent success
148
- const staleness = spawnSync("sh", [join(hookDir, "baseline-staleness.sh")], { cwd: consumer, encoding: "utf-8" });
149
- expect(staleness.status).toBe(0);
150
- expect(staleness.stdout).toBe("");
151
- });
152
-
153
- it("warns from the staleness script when skill markers lag the package", () => {
154
- runInstall(consumer, "--skills", "--hooks", "--target", "agents");
155
- const pkgDir = join(consumer, "node_modules", "@imfusion", "web-ui");
156
- mkdirSync(pkgDir, { recursive: true });
157
- writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ version: "99.0.0" }));
158
-
159
- const result = spawnSync("sh", [join(consumer, ".agents", "hooks", "imf-web-ui", "baseline-staleness.sh")], {
160
- cwd: consumer,
161
- encoding: "utf-8"
162
- });
163
- expect(result.status).toBe(0);
164
- expect(result.stdout).toContain("npx web-ui-install");
165
- });
166
- });
@@ -1,20 +0,0 @@
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;
@@ -1,17 +0,0 @@
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 };
@@ -1,104 +0,0 @@
1
- import { Field as FieldPrimitive } from '@base-ui/react/field';
2
- type UpstreamRoot = React.ComponentProps<typeof FieldPrimitive.Root>;
3
- type UpstreamItem = React.ComponentProps<typeof FieldPrimitive.Item>;
4
- type UpstreamLabel = React.ComponentProps<typeof FieldPrimitive.Label>;
5
- type UpstreamDescription = React.ComponentProps<typeof FieldPrimitive.Description>;
6
- type UpstreamControl = React.ComponentProps<typeof FieldPrimitive.Control>;
7
- type UpstreamError = React.ComponentProps<typeof FieldPrimitive.Error>;
8
- type UpstreamValidity = React.ComponentProps<typeof FieldPrimitive.Validity>;
9
- type RootProps = Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style"> & {
10
- name?: string;
11
- actionsRef?: UpstreamRoot["actionsRef"];
12
- dirty?: boolean;
13
- touched?: boolean;
14
- /** @default false */
15
- disabled?: boolean;
16
- invalid?: boolean;
17
- validate?: UpstreamRoot["validate"];
18
- /** @default "onSubmit" */
19
- validationMode?: UpstreamRoot["validationMode"];
20
- /** @default 0 */
21
- validationDebounceTime?: number;
22
- className?: UpstreamRoot["className"];
23
- style?: UpstreamRoot["style"];
24
- render?: UpstreamRoot["render"];
25
- };
26
- /**
27
- * Field.Root — groups a label, control, validation state, and supporting copy.
28
- *
29
- * @doc https://base-ui.com/react/components/field
30
- * @llm https://base-ui.com/react/components/field.md
31
- */
32
- export declare function Root({ className, ...props }: RootProps): import("react/jsx-runtime").JSX.Element;
33
- export declare namespace Root {
34
- var displayName: string;
35
- }
36
- type ItemProps = Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style"> & {
37
- /** @default false */
38
- disabled?: boolean;
39
- className?: UpstreamItem["className"];
40
- style?: UpstreamItem["style"];
41
- render?: UpstreamItem["render"];
42
- };
43
- /** Field.Item — groups one checkable option with its label and description. */
44
- export declare function Item({ className, ...props }: ItemProps): import("react/jsx-runtime").JSX.Element;
45
- export declare namespace Item {
46
- var displayName: string;
47
- }
48
- type LabelProps = Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "className" | "style"> & {
49
- /** @default true */
50
- nativeLabel?: boolean;
51
- className?: UpstreamLabel["className"];
52
- style?: UpstreamLabel["style"];
53
- render?: UpstreamLabel["render"];
54
- };
55
- /** Field.Label — an accessible label automatically associated with the control. */
56
- export declare function Label({ className, ...props }: LabelProps): import("react/jsx-runtime").JSX.Element;
57
- export declare namespace Label {
58
- var displayName: string;
59
- }
60
- type DescriptionProps = Omit<React.HTMLAttributes<HTMLParagraphElement>, "className" | "style"> & {
61
- className?: UpstreamDescription["className"];
62
- style?: UpstreamDescription["style"];
63
- render?: UpstreamDescription["render"];
64
- };
65
- /** Field.Description — supporting information for a field. */
66
- export declare function Description({ className, ...props }: DescriptionProps): import("react/jsx-runtime").JSX.Element;
67
- export declare namespace Description {
68
- var displayName: string;
69
- }
70
- type ControlProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "className" | "style" | "defaultValue"> & {
71
- defaultValue?: UpstreamControl["defaultValue"];
72
- onValueChange?: UpstreamControl["onValueChange"];
73
- className?: UpstreamControl["className"];
74
- style?: UpstreamControl["style"];
75
- render?: UpstreamControl["render"];
76
- };
77
- /** Field.Control — a native input control with Field labeling and validation. */
78
- export declare function Control({ className, ...props }: ControlProps): import("react/jsx-runtime").JSX.Element;
79
- export declare namespace Control {
80
- var displayName: string;
81
- }
82
- type ErrorProps = Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style"> & {
83
- match?: UpstreamError["match"];
84
- className?: UpstreamError["className"];
85
- style?: UpstreamError["style"];
86
- render?: UpstreamError["render"];
87
- };
88
- /** Field.Error — an inline validation message shown when its match condition fails. */
89
- export declare function Error({ className, ...props }: ErrorProps): import("react/jsx-runtime").JSX.Element;
90
- export declare namespace Error {
91
- var displayName: string;
92
- }
93
- type ValidityProps = {
94
- children: UpstreamValidity["children"];
95
- };
96
- /**
97
- * Field.Validity — renderless access to the field validity data for custom messages.
98
- * Its function child owns the rendered element and therefore its identity and styling.
99
- */
100
- export declare function Validity({ children }: ValidityProps): import("react/jsx-runtime").JSX.Element;
101
- export declare namespace Validity {
102
- var displayName: string;
103
- }
104
- export {};
@@ -1,2 +0,0 @@
1
- import { ComponentMeta } from '../../types/meta';
2
- export declare const fieldMeta: ComponentMeta;
@@ -1,2 +0,0 @@
1
- export * as Field from './field';
2
- export { fieldMeta } from './field.meta';