@imfusion/web-ui 0.5.1-dev.2.gf11bbed1 → 0.5.1-dev.21.g2accd17d

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 (46) hide show
  1. package/README.md +120 -32
  2. package/bin/install.js +343 -0
  3. package/bin/install.test.ts +166 -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/components/logo/logo.d.ts +1 -1
  7. package/dist/index.js +4 -2
  8. package/dist/style.css +1 -1
  9. package/package.json +34 -24
  10. package/src/docgen/doc.gen.json +1 -1
  11. package/src/llms/skills/imf-web-ui/SKILL.md +15 -11
  12. package/src/llms/skills/imf-web-ui-agent-setup/SKILL.md +71 -0
  13. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/baseline-staleness.sh +17 -0
  14. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/post-tool-use.sh +21 -0
  15. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/session-start.sh +4 -0
  16. package/src/llms/skills/imf-web-ui-agent-setup/templates/hooks/user-prompt-submit.sh +4 -0
  17. package/src/llms/skills/imf-web-ui-agent-setup/templates/settings.json +37 -0
  18. package/src/llms/skills/imf-web-ui-components/SKILL.md +1 -1
  19. package/src/llms/skills/imf-web-ui-frontend-conventions/SKILL.md +45 -0
  20. package/src/llms/skills/imf-web-ui-frontend-conventions/references/assets.md +23 -0
  21. package/src/llms/skills/imf-web-ui-frontend-conventions/references/class-names.md +42 -0
  22. package/src/llms/skills/imf-web-ui-frontend-conventions/references/components.md +63 -0
  23. package/src/llms/skills/imf-web-ui-frontend-conventions/references/data.md +196 -0
  24. package/src/llms/skills/imf-web-ui-frontend-conventions/references/docs-structure.md +44 -0
  25. package/src/llms/skills/imf-web-ui-frontend-conventions/references/git.md +33 -0
  26. package/src/llms/skills/imf-web-ui-frontend-conventions/references/library-boundary.md +37 -0
  27. package/src/llms/skills/imf-web-ui-frontend-conventions/references/npm-project.md +57 -0
  28. package/src/llms/skills/imf-web-ui-frontend-conventions/references/project-structure.md +21 -0
  29. package/src/llms/skills/{imf-web-ui-frontend-patterns/references/react-patterns.md → imf-web-ui-frontend-conventions/references/react.md} +26 -12
  30. package/src/llms/skills/imf-web-ui-frontend-conventions/references/stack.md +39 -0
  31. package/src/llms/skills/imf-web-ui-frontend-conventions/references/styling.md +91 -0
  32. package/src/llms/skills/imf-web-ui-frontend-conventions/references/testing.md +16 -0
  33. package/src/llms/skills/imf-web-ui-frontend-conventions/references/tooling.md +76 -0
  34. package/src/llms/skills/imf-web-ui-frontend-conventions/references/typescript.md +45 -0
  35. package/src/llms/skills/imf-web-ui-frontend-setup/SKILL.md +89 -0
  36. package/src/llms/skills/imf-web-ui-frontend-setup/templates/AGENTS.md +34 -0
  37. package/src/llms/skills/imf-web-ui-frontend-setup/templates/README.md +27 -0
  38. package/src/llms/skills/imf-web-ui-library-setup/SKILL.md +36 -0
  39. package/src/llms/skills/imf-web-ui-update/SKILL.md +119 -0
  40. package/src/llms/skills/imf-web-ui-ux/SKILL.md +4 -4
  41. package/src/llms/skills/imf-web-ui-ux/references/forms.md +1 -1
  42. package/bin/install-skill.js +0 -180
  43. package/src/llms/skills/imf-web-ui-frontend-patterns/SKILL.md +0 -93
  44. package/src/llms/skills/imf-web-ui-frontend-patterns/references/code-conventions.md +0 -133
  45. package/src/llms/skills/imf-web-ui-imfusion-frontend-setup/SKILL.md +0 -201
  46. package/src/llms/skills/imf-web-ui-setup/SKILL.md +0 -57
@@ -0,0 +1,166 @@
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
+ });
@@ -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 };
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
2
2
  import { VariantProps } from 'class-variance-authority';
3
3
  declare const logo: (props?: ({
4
4
  size?: "sm" | "md" | "lg" | "hero" | "xs" | "xl" | "fit" | null | undefined;
5
- variant?: "main" | "inherit" | "support" | "minor" | "brand" | null | undefined;
5
+ variant?: "main" | "inherit" | "support" | "minor" | "brand" | "oncolor" | null | undefined;
6
6
  } & import('class-variance-authority/types').ClassProp) | undefined) => string;
7
7
  type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> & VariantProps<typeof logo> & {
8
8
  /** Logo source — an image URL or an inline SVG node. Required; this primitive carries no brand default (see `ImFusionLogo` for our own mark). */
package/dist/index.js CHANGED
@@ -301,7 +301,7 @@ function Jn({ value: e, label: t = "Copy", copiedLabel: n = "Copied", variant: i
301
301
  "data-copied": c || void 0,
302
302
  "aria-label": c ? n : t,
303
303
  onClick: () => l(e),
304
- className: r(Kn.root, typeof o == "string" ? o : void 0),
304
+ className: (e) => r(Kn.root, typeof o == "function" ? o(e) : o),
305
305
  children: /* @__PURE__ */ X(u, {
306
306
  size: 15,
307
307
  strokeWidth: 2,
@@ -10593,6 +10593,7 @@ var Xh = {
10593
10593
  variantInherit: "imf-ui-logo-module-variantInherit",
10594
10594
  variantMain: "imf-ui-logo-module-variantMain",
10595
10595
  variantMinor: "imf-ui-logo-module-variantMinor",
10596
+ variantOncolor: "imf-ui-logo-module-variantOncolor",
10596
10597
  variantSupport: "imf-ui-logo-module-variantSupport"
10597
10598
  }, Qh = n(Zh.root, { variants: {
10598
10599
  size: {
@@ -10609,7 +10610,8 @@ var Xh = {
10609
10610
  main: Zh.variantMain,
10610
10611
  support: Zh.variantSupport,
10611
10612
  minor: Zh.variantMinor,
10612
- brand: Zh.variantBrand
10613
+ brand: Zh.variantBrand,
10614
+ oncolor: Zh.variantOncolor
10613
10615
  }
10614
10616
  } });
10615
10617
  function $h({ src: e, alt: t, size: n = "md", variant: r = "brand", height: i, className: a, style: o, ...s }) {