youiyoux 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,36 @@
1
1
  # youiyoux
2
2
 
3
- A shareable report on any repo's UI, with no setup:
3
+ **Your agent rebuilds components you already have. youiyoux catches it.**
4
+
5
+ UI context and verification for coding agents: it tells the agent what your repo already has before
6
+ it writes UI, then checks what it wrote.
4
7
 
5
8
  ```sh
6
- npx youiyoux # in a repo: scan it, write the report
7
- npx youiyoux path/to/repo
9
+ npx youiyoux # scan this repo
10
+ npx youiyoux path/to/repo # or another one; --out <dir> writes elsewhere
8
11
  ```
9
12
 
10
- It prints five plain-English lines and writes three files to the repo (or `--out <dir>`), and nothing else:
13
+ No config, no account. You get:
14
+
15
+ - `youiyoux-report.html`: near-duplicate components and which to keep, hardcoded values your theme already has, off-library imports, and the top fixes with their next steps. One offline page.
16
+ - `youiyoux-card.png`: a 1200×630 card with the headline numbers.
17
+ - `youiyoux-summary.md`: paste it into a PR or issue.
18
+
19
+ ## For your agent
11
20
 
12
- | File | What |
13
- | --- | --- |
14
- | `youiyoux-report.html` | One self-contained page (no network requests, light and dark, readable on a phone): projects (one section per workspace package with UI files), UI libraries, the component inventory, near-duplicate groups, hardcoded values a theme token already holds, off-library imports and the top fixes, each with a concrete next step |
15
- | `youiyoux-card.png` | A 1200×630 share card with the repo's name and the headline numbers |
16
- | `youiyoux-summary.md` | A short summary for a README or pull request |
21
+ ```sh
22
+ npx youiyoux agents install # rule in AGENTS.md + CLAUDE.md, MCP server for Claude Code + Codex
23
+ npx youiyoux hooks install # check after each Claude Code edit, and at git commit
24
+ ```
17
25
 
18
- **Rendered looks.** When a Chromium is installed (the Playwright headless shell, or `YOUI_CHROMIUM`), the most-used components are drawn with the project's own code and CSS and embedded. Otherwise the report says how: `npx youiyoux --render` asks before downloading the headless shell (~100 MB, once); `--yes` allows it without asking. Everything else works without a browser.
26
+ - `ui_find_component` (MCP): before writing a component, the existing one with its import, props and a real usage.
27
+ - `npx youiyoux check --diff`: after UI edits, what the change adds, each with its fix.
28
+ - `ui_render` (MCP) or `npx youiyoux render <file>`: screenshots light and dark, accessibility issues, a diff against base.
19
29
 
20
- **Safe to share by default.** Paths are relative to the repo. Includes relative paths, component names, class lists, counts and rendered images when available. Captions and fixes omit literal prop values and text content; source lines require `--include-code`. Home paths, usernames, environment or `.env` values, and credentials in URLs are removed. `--include-code` adds source lines and full look captions (keys and tokens in it still removed).
30
+ ## Privacy
21
31
 
22
- **Set up youiyoux for your agent:** `npx youiyoux agents install` tells Claude Code and Codex to look up the project's existing components before writing new ones. Every other `youiyoux` command runs the same way (`npx youiyoux check --diff`), The `youi` bin remains a silent compatibility alias.
32
+ Runs locally; nothing is uploaded, no telemetry. The report includes relative paths, component names, class lists, counts and rendered images when available. No source lines unless `--include-code`; home paths, usernames, env values and credentials are stripped. Rendered looks need a headless Chromium (`--render` offers to download it, ~100 MB, once); specimen images are cached in `~/.cache/youiyoux/specimens`.
23
33
 
24
- Node 20 or later. Nothing is uploaded, there is no telemetry and no account.
34
+ Node 20 or later. The `youi` bin is a silent alias.
25
35
 
26
- Specimen images are cached outside the repo in `~/.cache/youiyoux/specimens` (`XDG_CACHE_HOME` overrides `~/.cache`; `YOUI_SPECIMEN_CACHE` overrides the specimen directory). Rendered images show the component as rendered, including visible labels.
36
+ Docs, agent reference and source: [github.com/ap9000/youiyoux](https://github.com/ap9000/youiyoux)
@@ -2374,9 +2374,9 @@ function renderInventory(report, outFile) {
2374
2374
  const roles = INVENTORY_ROLES.filter((role) => uses.some((l) => l.role === role));
2375
2375
  const width = Math.max(...projects.map((p) => p.length), 9);
2376
2376
  const cell = (role, project2) => {
2377
- const looks2 = uses.filter((l) => l.role === role && l.project === project2);
2378
- const families = new Set(looks2.flatMap((l) => l.family ? [l.family] : [])).size;
2379
- return looks2.length === 0 ? style.dim("\xB7") : `${families}f ${looks2.length}`;
2377
+ const looks = uses.filter((l) => l.role === role && l.project === project2);
2378
+ const families = new Set(looks.flatMap((l) => l.family ? [l.family] : [])).size;
2379
+ return looks.length === 0 ? style.dim("\xB7") : `${families}f ${looks.length}`;
2380
2380
  };
2381
2381
  const lines = [
2382
2382
  `${style.bold("inventory")} ${style.dim(`\xB7 ${plural(report.projects.length, "project")} \xB7 ${plural(report.summary.looks, "look")} in ${plural(report.families.length, "family", "families")}${report.summary.stories ? ` \xB7 ${plural(report.summary.stories, "story", "stories")} beside them` : ""}`)}`,
@@ -2560,7 +2560,7 @@ function plainAscii(text) {
2560
2560
  // ../cli/src/report/card.ts
2561
2561
  var CARD_WIDTH = 1200;
2562
2562
  var CARD_HEIGHT = 630;
2563
- var INK = { background: [11, 16, 32], tile: [22, 30, 54], accent: [124, 156, 255], text: [232, 236, 248], muted: [147, 160, 191], warn: [251, 191, 36], ok: [74, 222, 128] };
2563
+ var INK = { background: [8, 9, 10], track: [25, 26, 29], accent: [113, 112, 255], accent2: [69, 69, 147], accent3: [40, 40, 76], text: [247, 248, 248], muted: [138, 143, 152], ok: [143, 181, 164] };
2564
2564
  var Canvas = class {
2565
2565
  pixels;
2566
2566
  width;
@@ -2641,25 +2641,35 @@ function drawCard(input) {
2641
2641
  const canvas = new Canvas(CARD_WIDTH, CARD_HEIGHT, INK.background);
2642
2642
  const margin = 72;
2643
2643
  const inner = CARD_WIDTH - margin * 2;
2644
- canvas.rect(0, 0, CARD_WIDTH, 10, INK.accent);
2645
- canvas.text(margin, 64, "YOUIYOUX REPORT", 4, INK.accent);
2646
- const name = fit(input.repo, inner, 10, 5);
2647
- canvas.text(margin, 64 + 7 * 4 + 32, name.text, name.scale, INK.text);
2648
- const gap = 16;
2649
- const tiles = input.stats.slice(0, 4);
2650
- const tileWidth = Math.floor((inner - gap * (tiles.length - 1)) / Math.max(1, tiles.length));
2651
- const top = 290;
2652
- const tileHeight = 220;
2653
- tiles.forEach((stat3, i) => {
2654
- const x = margin + i * (tileWidth + gap);
2655
- canvas.rect(x, top, tileWidth, tileHeight, INK.tile, 18);
2656
- const value = fit(String(stat3.value), tileWidth - 48, 12, 6);
2657
- const tone = stat3.tone === "warn" ? INK.warn : stat3.tone === "ok" ? INK.ok : INK.text;
2658
- canvas.text(x + 24, top + 28, value.text, value.scale, tone);
2659
- stat3.label.slice(0, 2).forEach((line, n) => {
2660
- const label = fit(line, tileWidth - 48, 3, 2);
2661
- canvas.text(x + 24, top + 28 + 7 * 12 + 26 + n * 30, label.text, label.scale, INK.muted);
2644
+ canvas.text(margin, 64, "youiyoux report", 3, INK.muted);
2645
+ const name = fit(input.repo, inner, 9, 5);
2646
+ canvas.text(margin, 104, name.text, name.scale, INK.text);
2647
+ const total = input.parts.reduce((n, part) => n + part.places, 0);
2648
+ const value = fit(String(total), inner / 2, 14, 8);
2649
+ const valueWidth = canvas.text(margin, 236, value.text, value.scale, total > 0 ? INK.text : INK.ok);
2650
+ const words = total === 0 ? "places to fix: clean" : total === 1 ? "place to fix" : "places to fix";
2651
+ canvas.text(margin + valueWidth + 28, 236 + 7 * value.scale - 7 * 4, words, 4, INK.muted);
2652
+ const top = 380;
2653
+ const steps = [INK.accent, INK.accent2, INK.accent3];
2654
+ canvas.rect(margin, top, inner, 20, INK.track, 10);
2655
+ if (total > 0) {
2656
+ let x2 = margin;
2657
+ const shown2 = input.parts.filter((part) => part.places > 0);
2658
+ const room = inner - 6 * (shown2.length - 1);
2659
+ input.parts.forEach((part, i) => {
2660
+ if (part.places === 0) return;
2661
+ const last = part === shown2.at(-1);
2662
+ const width = last ? margin + inner - x2 : Math.max(10, Math.round(part.places / total * room));
2663
+ canvas.rect(x2, top, width, 20, steps[i] ?? INK.accent3, 10);
2664
+ x2 += width + 6;
2662
2665
  });
2666
+ }
2667
+ let x = margin;
2668
+ input.parts.forEach((part, i) => {
2669
+ canvas.rect(x, top + 52, 18, 18, part.places > 0 ? steps[i] ?? INK.accent3 : INK.track, 4);
2670
+ x += 32;
2671
+ x += canvas.text(x, top + 52, part.label.split(" ")[0], 3, INK.muted) + 16;
2672
+ x += canvas.text(x, top + 52, String(part.places), 3, part.places > 0 ? INK.text : INK.ok) + 40;
2663
2673
  });
2664
2674
  const credit = "npx youiyoux";
2665
2675
  const creditWidth = textWidth(credit, 3);
@@ -3119,22 +3129,24 @@ async function nearDuplicateGroups(input) {
3119
3129
  const where = keeperFacts.project.name && importedBy(keeperFacts) > 0 ? `${keeperFacts.project.name}, ${keeperFile}` : keeperFile;
3120
3130
  const lead = kept ? `Keep ${keeperName} (${where})` : `Create ${keeperName} in ${keeperFile} (a new file in ${extract.where}) with this shape`;
3121
3131
  const [one, many] = KIND_WORDS[kind];
3132
+ const components = new Set([kept, ...copies].flatMap((v) => v?.kind === "component" ? [v.id] : [])).size;
3122
3133
  findings2.push(finding(
3123
3134
  home.project.dir,
3124
3135
  `${plural2(members.length, one, many)} look the same (${shapeWords(family.shape)}) but are built ${sides.length} ways`,
3125
3136
  counted.length,
3126
3137
  [...copies].sort((a, b) => path(a).localeCompare(path(b)) || a.line - b.line).slice(0, SITES_PER_FINDING).map((v) => ({ at: `${path(v)}:${v.line}`, file: path(v), line: v.line })),
3127
3138
  lead,
3128
- steps
3139
+ steps,
3140
+ components
3129
3141
  ));
3130
3142
  }
3131
3143
  findings2.push(...sameNamed.map((g) => g.finding));
3132
3144
  return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
3133
3145
  }
3134
- function finding(project2, title, count, sites, lead, steps) {
3146
+ function finding(project2, title, count, sites, lead, steps, exported) {
3135
3147
  const shown2 = steps.slice(0, STEPS_SHOWN);
3136
3148
  const more = steps.length > shown2.length ? `; and ${plural2(steps.length - shown2.length, "more file")} (listed in the report)` : "";
3137
- return { kind: "near-duplicate", project: project2, title, detail: null, count, sites, lead, steps, next: `${lead}. ${shown2.join(". ")}${more}` };
3149
+ return { kind: "near-duplicate", project: project2, title, detail: null, count, sites, lead, steps, next: `${lead}. ${shown2.join(". ")}${more}`, exported };
3138
3150
  }
3139
3151
  function commonDir(files) {
3140
3152
  const parts = files.map((f) => f.split("/").slice(0, -1));
@@ -3189,7 +3201,7 @@ async function sameNamedGroups(facts, defs, usage, importText) {
3189
3201
  const lead = how === "same" ? `Keep ${name} (${keeperFile})` : how === "near" ? `Keep one ${name} (${keeperFile}) that takes what differs as props` : `Keep ${name} (${keeperFile}) as the one ${name}`;
3190
3202
  const sites = [keep, ...copies].slice(0, SITES_PER_FINDING).map((d) => ({ at: `${repoPath(dir, d.file)}:${d.line}`, file: repoPath(dir, d.file), line: d.line }));
3191
3203
  const title = how === "different" ? `${name} is defined in ${copies.length + 1} files as different components` : `${name} is defined in ${copies.length + 1} files with ${how === "same" ? "the same" : "near-identical"} structure`;
3192
- return { ids: [keep, ...copies].map((d) => idOf(d.file)), finding: finding(dir, title, copies.length, sites, lead, steps) };
3204
+ return { ids: [keep, ...copies].map((d) => idOf(d.file)), finding: finding(dir, title, copies.length, sites, lead, steps, [keep, ...copies].filter((d) => d.exported).length) };
3193
3205
  }
3194
3206
  return found;
3195
3207
  }
@@ -3373,7 +3385,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3373
3385
  const changeProject = target ? await inferProject(project2.root, target) : null;
3374
3386
  const moduleMap = new Map(modules.map((m) => [m.file, m]));
3375
3387
  const eligible = new Set(inventory?.definitions.filter((d) => d.project === projectId && uiDefinition(d, moduleMap)).map((d) => d.id));
3376
- const looks2 = inventory ? inventory.looks.filter((l) => l.project === projectId && !l.story && !isShowcase(l.file)) : [];
3388
+ const looks = inventory ? inventory.looks.filter((l) => l.project === projectId && !l.story && !isShowcase(l.file)) : [];
3377
3389
  let libraries = [];
3378
3390
  let unused = [];
3379
3391
  const showcaseFiles = /* @__PURE__ */ new Set();
@@ -3395,11 +3407,11 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3395
3407
  incomplete.push(`UI libraries were not read (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
3396
3408
  }
3397
3409
  const roles = INVENTORY_ROLES.flatMap((role) => {
3398
- const of = looks2.filter((l) => l.role === role);
3410
+ const of = looks.filter((l) => l.role === role);
3399
3411
  return of.length === 0 ? [] : [{ role, uses: of.length, families: new Set(of.flatMap((l) => l.family ? [l.family] : [])).size }];
3400
3412
  });
3401
3413
  const byComponent = /* @__PURE__ */ new Map();
3402
- for (const look of looks2) {
3414
+ for (const look of looks) {
3403
3415
  if (look.recipe?.kind !== "component") continue;
3404
3416
  const key2 = `${look.recipe.file}#${look.recipe.export}`;
3405
3417
  const entry = byComponent.get(key2) ?? { name: look.recipe.export, file: repoPath2(project2.dir, look.recipe.file), roles: [] };
@@ -3452,7 +3464,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3452
3464
  definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
3453
3465
  kits: inventory ? inventory.kits.filter((k) => k.project === projectId).map((k) => ({ path: repoPath2(project2.dir, k.path), kind: k.kind === "cva-folder" ? "CVA components" : "CSS classes", members: k.members.length })) : [],
3454
3466
  roles,
3455
- uses: looks2.length,
3467
+ uses: looks.length,
3456
3468
  components,
3457
3469
  hardcoded,
3458
3470
  offLibrary,
@@ -3466,6 +3478,8 @@ function topNext(f) {
3466
3478
  const text = f.lead && f.steps ? `${f.lead}${f.steps.length > 1 ? `, then change ${plural3(f.steps.length, "file")} (each listed in the report)` : `; ${f.steps[0].charAt(0).toLowerCase()}${f.steps[0].slice(1)}`}` : f.next;
3467
3479
  return `${text.charAt(0).toLowerCase()}${text.slice(1).replace(/\.$/, "")}`;
3468
3480
  }
3481
+ var DUPLICATE_RULE = "Duplicated components: groups of 2+ exported components that look alike or share a name.";
3482
+ var isComponentDuplicate = (f) => (f.exported ?? 0) >= 2;
3469
3483
  function summaryLines(model) {
3470
3484
  const t = model.totals;
3471
3485
  const top = model.projects.flatMap((p) => p.components).sort((a, b) => b.uses - a.uses)[0];
@@ -3473,7 +3487,7 @@ function summaryLines(model) {
3473
3487
  return [
3474
3488
  `${model.repo}: ${plural3(t.projects, "project")}, ${plural3(t.files, "file")} scanned; UI libraries: ${t.libraries.length > 0 ? t.libraries.join(", ") : "none found"}.`,
3475
3489
  `Components: ${t.definitions} defined, ${plural3(t.uses, "button, input, card or other UI element", "buttons, inputs, cards and other UI elements")} in use${top ? `; most used: ${top.name} (${plural3(top.uses, "use")})` : ""}.`,
3476
- `Near-duplicates: ${plural3(t.nearDuplicates, "group")} of components that look the same but are built more than once.`,
3490
+ `Near-duplicates: ${plural3(t.nearDuplicates, "group")} of UI that looks the same but is built more than once; ${t.componentDuplicates} of them between exported components.`,
3477
3491
  `Hardcoded values: ${plural3(t.hardcoded, "colour, spacing or radius", "colours, spacings and radii")} a theme token already holds; off-library imports: ${t.offLibrary}.`,
3478
3492
  first ? `Top fix: ${first.title.replace(/\.$/, "")}; ${topNext(first)}.` : `Nothing to fix first; next: ${NPX} agents install, so your agent keeps it that way.`
3479
3493
  ];
@@ -3504,6 +3518,7 @@ async function buildReportModel(dir, options = {}) {
3504
3518
  definitions: projects.reduce((n, p) => n + p.definitions, 0),
3505
3519
  uses: projects.reduce((n, p) => n + p.uses, 0),
3506
3520
  nearDuplicates: near.length,
3521
+ componentDuplicates: near.filter(isComponentDuplicate).length,
3507
3522
  hardcoded: projects.reduce((n, p) => n + p.hardcoded.reduce((m, f) => m + f.count, 0), 0),
3508
3523
  offLibrary: projects.reduce((n, p) => n + p.offLibrary.reduce((m, f) => m + f.count, 0), 0)
3509
3524
  };
@@ -3533,108 +3548,316 @@ var escape2 = (text) => String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;"
3533
3548
  var plural4 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3534
3549
  var anchor = (dir) => `p-${dir === "." ? "root" : dir.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
3535
3550
  var CSS = `
3536
- :root{color-scheme:light dark;--bg:#f7f8fb;--surface:#fff;--line:#e3e6ef;--text:#141826;--muted:#5b6478;--accent:#3651d6;--warn:#a15c00;--warn-bg:#fff4e0;--ok:#16794a;--code:#eef1f8}
3537
- @media (prefers-color-scheme:dark){:root{--bg:#0b1020;--surface:#121a30;--line:#25304f;--text:#e8ecf8;--muted:#a3adc6;--accent:#8ea6ff;--warn:#fbbf24;--warn-bg:#2b2410;--ok:#4ade80;--code:#1a2340}}
3551
+ :root{color-scheme:light dark;--canvas:#f4f5f7;--surface:#fcfcfd;--raised:#f6f7f9;--sunken:#f8f9fa;--line:#e3e4e8;--line-strong:#d0d2d8;--ink:#16171a;--ink-2:#3d4049;--muted:#6b6f78;--faint:#9ea2ab;--accent:#5e6ad2;--accent-soft:#eceefa;--success:#4f7a69;--success-soft:#eaf1ee;--danger:#ad5a43;--danger-soft:#f7ece8;--neutral-soft:#eef0f3;--k-dup:var(--accent);--k-hard:color-mix(in oklch,var(--accent) 55%,var(--surface));--k-off:color-mix(in oklch,var(--accent) 25%,var(--surface));--shadow:0 1px 2px rgba(16,17,20,.04)}
3552
+ @media (prefers-color-scheme:dark){:root{--canvas:#08090a;--surface:#0f1011;--raised:#151618;--sunken:#0b0c0d;--line:#1d1e21;--line-strong:#2b2c30;--ink:#f7f8f8;--ink-2:#d0d6e0;--muted:#8a8f98;--faint:#62666d;--accent:#7170ff;--accent-soft:#1a1b33;--success:#8fb5a4;--success-soft:#14201b;--danger:#d99a85;--danger-soft:#251713;--neutral-soft:#191a1d;--shadow:none}}
3538
3553
  *{box-sizing:border-box}
3539
- html{-webkit-text-size-adjust:100%}
3540
- body{margin:0;background:var(--bg);color:var(--text);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow-wrap:anywhere}
3541
- main{max-width:960px;margin:0 auto;padding:32px 20px 64px}
3542
- h1{font-size:clamp(28px,6vw,44px);line-height:1.1;margin:4px 0 8px}
3543
- h2{font-size:22px;margin:40px 0 12px}
3544
- h3{font-size:17px;margin:28px 0 8px}
3545
- p{margin:6px 0}
3546
- code,pre{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3547
- code{background:var(--code);border-radius:4px;padding:1px 5px}
3548
- pre{background:var(--code);border-radius:6px;padding:8px 10px;margin:4px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}
3549
- .eyebrow{color:var(--accent);font-weight:600;letter-spacing:.08em;text-transform:uppercase;font-size:13px;margin:0}
3550
- .meta,.muted,small{color:var(--muted)}
3551
- .summary{padding-left:20px}
3552
- .summary li{margin:4px 0}
3553
- .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin:20px 0 0;padding:0}
3554
- .stats div{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
3555
- .stats dt{color:var(--muted);font-size:14px}
3556
- .stats dd{margin:0;font-size:32px;font-weight:700;line-height:1.2}
3557
- .stats .warn dd{color:var(--warn)}
3558
- .stats .ok dd{color:var(--ok)}
3559
- .card,.finding,.agent{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:10px 0}
3560
- .agent{border-color:var(--accent)}
3554
+ html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;scroll-padding-top:16px}
3555
+ body{margin:0;background:var(--canvas);color:var(--ink);font:14px/1.5 Geist,"Geist Variable",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif;overflow-wrap:anywhere;-webkit-font-smoothing:antialiased}
3556
+ a{color:var(--accent);text-decoration:none}
3557
+ a:hover{text-decoration:underline}
3558
+ code,pre,.mono{font-family:"Geist Mono","Geist Mono Variable",ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
3559
+ code{background:var(--neutral-soft);border:1px solid var(--line);border-radius:5px;padding:1px 6px;color:var(--ink-2)}
3560
+ pre{background:var(--sunken);border:1px solid var(--line);border-radius:6px;padding:8px 10px;margin:4px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}
3561
+ h1,h2,h3,h4,p{margin:0}
3562
+ .shell{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}
3563
+ .rail{position:sticky;top:0;height:100vh;overflow-y:auto;border-right:1px solid var(--line);background:var(--surface);padding:16px 12px;display:flex;flex-direction:column;gap:16px}
3564
+ .brand{display:flex;align-items:center;gap:10px;padding:2px 6px;font-weight:600;font-size:14px}
3565
+ .mark{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:7px;background:var(--ink);color:var(--canvas);font-size:13px;font-weight:700}
3566
+ .rail nav{display:flex;flex-direction:column;gap:1px}
3567
+ .rail .group{font-size:11.5px;color:var(--faint);padding:12px 8px 4px}
3568
+ .rail a{display:flex;justify-content:space-between;align-items:center;gap:8px;height:30px;padding:0 8px;border-radius:6px;color:var(--ink-2);font-size:13px}
3569
+ .rail a:hover{background:var(--raised);color:var(--ink);text-decoration:none}
3570
+ .rail a .n{font-size:11.5px;color:var(--muted);font-variant-numeric:tabular-nums}
3571
+ .rail a .n.warn{color:var(--ink)}
3572
+ .rail .foot{margin-top:auto;font-size:11.5px;color:var(--faint);padding:0 8px}
3573
+ main{min-width:0;padding:28px 32px 64px;max-width:1120px;width:100%;margin:0 auto}
3574
+ .top{display:flex;flex-direction:column;gap:10px;padding-bottom:20px}
3575
+ .crumb{font-size:12px;color:var(--muted)}
3576
+ .top h1{font-size:24px;line-height:1.2;font-weight:650;letter-spacing:-.01em}
3577
+ .facts{display:flex;flex-wrap:wrap;gap:6px;padding:0;margin:0;list-style:none}
3578
+ .facts li{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 9px;border:1px solid var(--line);border-radius:6px;background:var(--surface);font-size:12px;color:var(--ink-2)}
3579
+ .facts li b{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
3580
+ .headline{display:flex;flex-direction:column;gap:14px;padding:4px 0 8px}
3581
+ .say{font-size:26px;line-height:1.3;font-weight:600;letter-spacing:-.015em;max-width:30em}
3582
+ .say b{font-variant-numeric:tabular-nums;font-weight:700}
3583
+ .say b.warn{color:var(--ink);box-shadow:inset 0 -.14em 0 var(--line-strong)}
3584
+ .say b.ok{color:var(--success)}
3585
+ .split{display:flex;height:10px;border-radius:5px;overflow:hidden;gap:2px;background:var(--neutral-soft)}
3586
+ .seg{display:block;height:100%;min-width:3px}
3587
+ .s0{background:var(--k-dup)}
3588
+ .s1{background:var(--k-hard)}
3589
+ .s2{background:var(--k-off)}
3590
+ .legend{display:flex;flex-wrap:wrap;align-items:center;gap:6px 18px;margin:0;padding:0;list-style:none;font-size:12.5px;color:var(--ink-2)}
3591
+ .legend a{color:inherit;display:inline-flex;align-items:center;gap:7px}
3592
+ .legend b{color:var(--ink);font-variant-numeric:tabular-nums}
3593
+ .legend .key{width:10px;height:10px;border-radius:3px;display:inline-block}
3594
+ .legend .key.zero{background:var(--line-strong)}
3595
+ .legend .clean{color:var(--success);font-size:11.5px}
3596
+ .legend .of{margin-left:auto;color:var(--muted);font-size:12px}
3597
+ .shapes{list-style:none;margin:0;padding:0}
3598
+ .shapes li{display:grid;grid-template-columns:120px minmax(0,1fr) 150px;align-items:center;gap:14px;padding:9px 0;border-top:1px solid var(--line)}
3599
+ .shapes li:first-child{border-top:0}
3600
+ .shapes .role{font-size:12.5px;color:var(--ink)}
3601
+ .shapes .sq{display:flex;flex-wrap:wrap;gap:4px;align-items:center;min-width:0}
3602
+ .shapes i,.panel header i.k{display:inline-block;width:14px;height:14px;border-radius:3px}
3603
+ .shapes i.one,i.k.one{background:var(--faint)}
3604
+ .shapes i.more,i.k.more{background:var(--accent-soft);box-shadow:inset 0 0 0 1.5px var(--accent)}
3605
+ .panel header i.k{width:10px;height:10px;vertical-align:-1px;margin:0 4px 0 10px}
3606
+ .shapes .over{font-size:11.5px;color:var(--muted);margin-left:4px}
3607
+ .shapes .unmeasured{font-size:12px;color:var(--faint);font-style:italic}
3608
+ .shapes .val{text-align:right;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
3609
+ .shapes .drift .val b{color:var(--ink)}
3610
+ .shapes .even .val b{color:var(--success)}
3611
+ section{scroll-margin-top:16px}
3612
+ .panel{background:var(--surface);border:1px solid var(--line);border-radius:10px;margin:16px 0 0;box-shadow:var(--shadow);overflow:hidden}
3613
+ .panel>header{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--raised)}
3614
+ .panel>header h2,.panel>header h3{font-size:14px;font-weight:600}
3615
+ .panel>header .aside{font-size:12px;color:var(--muted);text-align:right}
3616
+ .panel>.body{padding:14px 16px}
3617
+ .panel>.body>*+*{margin-top:10px}
3618
+ .panel>.body+.findings{border-top:1px solid var(--line)}
3619
+ .muted,small{color:var(--muted)}
3620
+ small{font-size:12px}
3621
+ .next{font-size:13px;color:var(--ink-2)}
3622
+ .next strong{color:var(--accent);font-weight:600}
3623
+ .none{color:var(--success);font-weight:500;font-size:13px}
3624
+ .incomplete{color:var(--muted);font-size:12.5px;margin-top:12px}
3625
+ .incomplete summary{color:var(--muted)}
3626
+ .unread{margin:8px 0 0;padding-left:18px;font-size:12px}
3627
+ .legend .alert{position:relative;margin-left:auto}
3628
+ .legend .alert+.of{margin-left:0}
3629
+ .legend .alert a{display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 8px;border:1px solid var(--line);border-radius:6px;color:var(--muted);font-size:11.5px}
3630
+ .legend .alert a:hover,.legend .alert a:focus-visible{color:var(--ink);border-color:var(--line-strong);text-decoration:none}
3631
+ .legend .alert i{display:inline-grid;place-items:center;width:13px;height:13px;border-radius:50%;background:var(--danger-soft);color:var(--danger);font-style:normal;font-size:9.5px;font-weight:700}
3632
+ .legend .tip{position:absolute;z-index:5;top:calc(100% + 6px);right:0;width:260px;padding:8px 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--surface);color:var(--ink-2);font-size:12px;line-height:1.45;box-shadow:0 8px 24px rgba(0,0,0,.12);opacity:0;visibility:hidden;transition:opacity .12s}
3633
+ .legend .alert:hover .tip,.legend .alert:focus-within .tip{opacity:1;visibility:visible}
3634
+ .unread-at{scroll-margin-top:16px}
3561
3635
  ol.findings,ul.findings{list-style:none;padding:0;margin:0}
3562
- .finding .title{font-weight:600;margin:0}
3563
- .sites{margin:6px 0;padding-left:18px;color:var(--muted)}
3564
- .sites li{margin:2px 0}
3565
- .next{margin-top:8px}
3566
- .steps{margin:4px 0 0;padding-left:22px;color:var(--muted);font-size:15px}
3567
- .steps li{margin:3px 0}
3568
- .more summary{cursor:pointer;color:var(--accent);margin:4px 0 0 4px;font-size:15px}
3569
- .next strong{color:var(--accent)}
3570
- .none{color:var(--ok);font-weight:600}
3571
- .incomplete{background:var(--warn-bg);color:var(--warn);border-radius:8px;padding:8px 12px}
3572
- .project{border-top:2px solid var(--line);margin-top:48px}
3573
- .project>h2{margin-top:24px}
3574
- .pills{display:flex;flex-wrap:wrap;gap:8px;padding:0;list-style:none;margin:6px 0}
3575
- .pills li{background:var(--code);border-radius:999px;padding:3px 12px;font-size:14px}
3576
- .rows{list-style:none;padding:0;margin:6px 0}
3577
- .rows li{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line);padding:6px 0}
3578
- .rows li span:last-child{color:var(--muted);text-align:right;white-space:nowrap}
3579
- .looks{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:12px}
3580
- .looks figure{margin:0;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px}
3581
- .looks .pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}
3582
- .looks img{display:block;width:100%;height:auto;border-radius:6px;border:1px solid var(--line)}
3583
- .looks figcaption{font-weight:600;margin-bottom:8px}
3584
- nav ul{padding-left:20px}
3585
- footer{margin-top:56px;color:var(--muted);font-size:14px;border-top:1px solid var(--line);padding-top:16px}
3636
+ .finding{padding:14px 16px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:8px}
3637
+ .finding:first-child{border-top:0}
3638
+ .head{display:flex;align-items:flex-start;gap:12px}
3639
+ .count{flex:none;width:44px;padding:4px 0 3px;border-radius:6px;background:var(--neutral-soft);color:var(--ink);font-weight:650;font-size:13px;line-height:1.1;display:flex;flex-direction:column;align-items:center;font-variant-numeric:tabular-nums}
3640
+ .count small{font-size:9.5px;font-weight:500;color:inherit;opacity:.8;letter-spacing:.02em}
3641
+ .head .what{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
3642
+ .finding .title{font-weight:600;font-size:13.5px;line-height:1.4}
3643
+ .finding .what .muted{font-size:12.5px}
3644
+ .chip::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--kind,var(--faint));order:-1}
3645
+ .chip:has(.swatch)::before{display:none}
3646
+ [data-kind=near-duplicate]{--kind:var(--k-dup)}
3647
+ [data-kind=hardcoded-value]{--kind:var(--k-hard)}
3648
+ [data-kind=off-library]{--kind:var(--k-off)}
3649
+ .chip{flex:none;display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 8px;border-radius:6px;background:var(--neutral-soft);color:var(--ink-2);font-size:11.5px;font-weight:500;white-space:nowrap}
3650
+ .swatch{display:inline-block;width:12px;height:12px;border-radius:3px;border:1px solid var(--line-strong);flex:none}
3651
+ .specs{display:flex;flex-wrap:wrap;gap:6px;padding:0 0 0 56px;margin:0;list-style:none}
3652
+ .specs li{height:22px;padding:0 8px;border-radius:6px;border:1px solid var(--line);background:var(--sunken);display:inline-flex;align-items:center;font-family:"Geist Mono","Geist Mono Variable",ui-monospace,"SF Mono",Menlo,monospace;font-size:11.5px;color:var(--ink-2)}
3653
+ .compare{margin-left:56px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);border:1px solid var(--line);border-radius:8px;overflow:hidden}
3654
+ .compare>.next,.compare>.steps,.compare>.more,.compare>.shot{margin:0;padding:10px 12px}
3655
+ .compare>.next{grid-column:1;grid-row:1 / span 2;border-right:1px solid var(--line);background:var(--sunken)}
3656
+ .compare>.next::before{content:"Canonical";display:block;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-2);margin-bottom:4px}
3657
+ .compare>.next strong{display:none}
3658
+ .compare>.steps{grid-column:2;list-style:none;font-size:12.5px;color:var(--ink-2);display:flex;flex-direction:column;gap:6px}
3659
+ .compare>.steps::before{content:"Replace";display:block;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
3660
+ .compare>.steps li,.compare>.more .steps li{padding-left:14px;position:relative}
3661
+ .compare>.steps li::before,.compare>.more .steps li::before{content:"";position:absolute;left:2px;top:.6em;width:5px;height:5px;border-radius:50%;background:var(--faint)}
3662
+ .compare>.more{grid-column:2;padding-top:0}
3663
+ .compare>.more .steps{list-style:none;padding:6px 0 0;margin:0;font-size:12.5px;color:var(--ink-2);display:flex;flex-direction:column;gap:6px}
3664
+ .compare>.shot{grid-column:1 / -1;border-top:1px solid var(--line);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;background:var(--sunken)}
3665
+ .shot figure{margin:0;display:flex;flex-direction:column;gap:6px;min-width:0}
3666
+ .shot figcaption{font-size:11.5px;color:var(--muted)}
3667
+ .shot img{display:block;width:100%;max-width:360px;height:140px;object-fit:cover;object-position:top left;border-radius:6px;border:1px solid var(--line);background:var(--surface)}
3668
+ .steps{margin:0;padding-left:20px;color:var(--ink-2);font-size:12.5px}
3669
+ .more summary{cursor:pointer;color:var(--accent);font-size:12.5px;list-style:none}
3670
+ .more summary::-webkit-details-marker{display:none}
3671
+ .more summary::before{content:"+ "}
3672
+ .more[open] summary::before{content:"\u2212 "}
3673
+ .sites{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 0 56px;padding:0;list-style:none}
3674
+ .sites li{font-size:12px;color:var(--muted);display:inline-flex;align-items:center}
3675
+ .sites.code{display:block}
3676
+ .sites.code li{display:block;margin:6px 0}
3677
+ .finding>.next{margin-left:56px}
3678
+ .rows{list-style:none;padding:0;margin:0}
3679
+ .rows li{display:flex;justify-content:space-between;gap:12px;border-top:1px solid var(--line);padding:8px 0;font-size:13px}
3680
+ .rows li:first-child{border-top:0}
3681
+ .rows li span:last-child{color:var(--muted);text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}
3682
+ .rows small{font-family:"Geist Mono","Geist Mono Variable",ui-monospace,"SF Mono",Menlo,monospace;font-size:11.5px;margin-left:6px}
3683
+ h4{font-size:12px;font-weight:600;color:var(--muted);margin-top:16px}
3684
+ .agent{display:flex;flex-direction:column;gap:4px;background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:10px;padding:12px 16px;margin:16px 0 0;font-size:13px}
3685
+ .agent strong{font-weight:600}
3686
+ .looks{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:12px}
3687
+ .looks figure{margin:0;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--sunken);min-width:0}
3688
+ .looks figcaption{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:8px 12px;border-bottom:1px solid var(--line);background:var(--surface);font-size:13px;font-weight:600}
3689
+ .looks figcaption small{min-width:0;font-weight:400;font-family:"Geist Mono","Geist Mono Variable",ui-monospace,"SF Mono",Menlo,monospace;font-size:11px;text-align:right}
3690
+ .looks .pair{display:grid;grid-template-columns:1fr 1fr}
3691
+ .looks img{display:block;width:100%;height:220px;object-fit:cover;object-position:top left}
3692
+ .looks figure>p{padding:8px 12px;border-top:1px solid var(--line);font-size:12px}
3693
+ .project{margin-top:40px}
3694
+ .project>h2{font-size:18px;font-weight:650;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
3695
+ .project>h2 small{font-family:"Geist Mono","Geist Mono Variable",ui-monospace,"SF Mono",Menlo,monospace;font-weight:400}
3696
+ .projects nav ul{list-style:none;padding:0;margin:0}
3697
+ .projects nav li{border-top:1px solid var(--line);padding:6px 0;font-size:13px}
3698
+ .projects nav li:first-child{border-top:0}
3699
+ footer{margin-top:48px;color:var(--muted);font-size:12px;border-top:1px solid var(--line);padding-top:16px;display:flex;flex-direction:column;gap:6px}
3700
+ @media (max-width:860px){
3701
+ .shell{display:block}
3702
+ .rail{position:sticky;top:0;z-index:2;height:auto;flex-direction:row;align-items:center;gap:8px;padding:8px 12px;border-right:0;border-bottom:1px solid var(--line);overflow-x:auto;overflow-y:hidden}
3703
+ .rail .group,.rail .foot,.brand span:last-child{display:none}
3704
+ .rail nav{flex-direction:row;gap:4px}
3705
+ .rail a{white-space:nowrap;height:28px;border:1px solid var(--line)}
3706
+ main{padding:20px 16px 48px}
3707
+ html{scroll-padding-top:60px}
3708
+ section{scroll-margin-top:60px}
3709
+ .panel>header .aside{display:none}
3710
+ .specs,.sites,.compare,.finding>.next{margin-left:0;padding-left:0}
3711
+ .compare{grid-template-columns:minmax(0,1fr)}
3712
+ .compare>.next{grid-row:auto;border-right:0;border-bottom:1px solid var(--line)}
3713
+ .compare>.steps,.compare>.more{grid-column:1}
3714
+ .compare>.shot{grid-template-columns:minmax(0,1fr)}
3715
+ .say{font-size:20px}
3716
+ .legend .of{margin-left:0}
3717
+ .shapes li{grid-template-columns:minmax(0,1fr) auto;gap:8px}
3718
+ .shapes .sq{grid-column:1 / -1;grid-row:2}
3719
+ }
3586
3720
  `;
3721
+ var FACT = /import \{[^}]*\} from &quot;[^&]*&quot;|var\(--[\w-]+\)|(?<![\w/.-])(?:[\w@.\[\]-]+\/)+[\w.\[\]-]+\.(?:tsx|ts|jsx|js|mjs|css|scss|vue|svelte|astro|html)(?::\d+)?|\b(?:color|radius|spacing|space|size|font|shadow|typography)\.[\w-]+/g;
3722
+ var rich = (text) => escape2(text).replace(FACT, (fact) => `<span class="mono">${fact}</span>`);
3587
3723
  function next(text) {
3588
- return `<p class="next"><strong>Next:</strong> ${escape2(text.replace(/\.$/, ""))}.</p>`;
3724
+ return `<p class="next"><strong>Next:</strong> ${rich(text.replace(/\.$/, ""))}.</p>`;
3589
3725
  }
3590
3726
  var STEPS_SHOWN2 = 5;
3591
- function finding2(f, showProject) {
3592
- const head = `<p class="title">${escape2(f.title)}</p>${f.detail ? `<p class="muted">${escape2(f.detail)}</p>` : ""}${showProject && f.project !== "." ? `<p class="muted">in ${escape2(f.project)}</p>` : ""}`;
3727
+ var KIND = { "near-duplicate": "Duplicate", "hardcoded-value": "Hardcoded", "off-library": "Off-library" };
3728
+ function splitMeasures(title) {
3729
+ const m = /\s*\(([^()]*\d[^()]*)\)/.exec(title);
3730
+ if (!m) return { text: title, measures: [] };
3731
+ return { text: (title.slice(0, m.index) + title.slice(m.index + m[0].length)).replace(/\s+/g, " ").trim(), measures: m[1].split(/,\s*/).filter(Boolean) };
3732
+ }
3733
+ var COLOUR = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\([0-9.,%\s/+-]+\)/;
3734
+ function swatch(title) {
3735
+ const colour = COLOUR.exec(title)?.[0];
3736
+ return colour ? `<span class="swatch" style="background:${escape2(colour)}" aria-hidden="true"></span>` : "";
3737
+ }
3738
+ function shots(f, looks) {
3739
+ if (looks.size === 0) return "";
3740
+ const kept = /\(([^()]+\.[a-z]+)\)/.exec(f.lead ?? "")?.[1];
3741
+ const copy = (f.steps ?? []).map((s) => /^In ([^,]+),/.exec(s)?.[1]).find((file) => file !== void 0 && file !== kept && looks.has(file));
3742
+ const pair = [];
3743
+ if (kept && looks.get(kept)?.light) pair.push(["Keep", looks.get(kept)]);
3744
+ if (copy && looks.get(copy)?.light) pair.push(["Copy", looks.get(copy)]);
3745
+ if (pair.length === 0) return "";
3746
+ return `<div class="shot">${pair.map(([label, look]) => `<figure><picture>${look.dark ? `<source media="(prefers-color-scheme: dark)" srcset="${look.dark}">` : ""}<img alt="${escape2(look.component)} (${label.toLowerCase()})" src="${look.light}"></picture><figcaption>${label} \xB7 ${escape2(look.component)} <span class="mono">${escape2(look.file)}</span></figcaption></figure>`).join("")}</div>`;
3747
+ }
3748
+ function finding2(f, showProject, looks) {
3749
+ const { text, measures } = f.kind === "near-duplicate" ? splitMeasures(f.title) : { text: f.title, measures: [] };
3750
+ const where = showProject && f.project !== "." ? `<p class="muted">in <span class="mono">${escape2(f.project)}</span></p>` : "";
3751
+ const head = `<div class="head"><span class="count">${f.count}<small>${f.count === 1 ? "place" : "places"}</small></span><div class="what"><p class="title">${rich(text)}</p>${f.detail ? `<p class="muted">${escape2(f.detail)}</p>` : ""}${where}</div><span class="chip">${f.kind === "hardcoded-value" ? swatch(f.title) : ""}${KIND[f.kind]}</span></div>`;
3752
+ const specs = measures.length > 0 ? `<ul class="specs">${measures.map((m) => `<li>${escape2(m)}</li>`).join("")}</ul>` : "";
3593
3753
  if (f.lead && f.steps && f.steps.length > 0) {
3594
- const item = (step) => `<li>${escape2(step.replace(/\.$/, ""))}</li>`;
3754
+ const item = (step) => `<li>${rich(step.replace(/\.$/, ""))}</li>`;
3595
3755
  const rest = f.steps.slice(STEPS_SHOWN2);
3596
3756
  const more2 = rest.length > 0 ? `<details class="more"><summary>and ${plural4(rest.length, "more file")}</summary><ol class="steps" start="${STEPS_SHOWN2 + 1}">${rest.map(item).join("")}</ol></details>` : "";
3597
- return `<li class="finding" data-kind="${f.kind}">${head}${next(f.lead)}<ol class="steps">${f.steps.slice(0, STEPS_SHOWN2).map(item).join("")}</ol>${more2}</li>`;
3757
+ return `<li class="finding" data-kind="${f.kind}">${head}${specs}<div class="compare">${next(f.lead)}<ol class="steps">${f.steps.slice(0, STEPS_SHOWN2).map(item).join("")}</ol>${more2}${shots(f, looks)}</div></li>`;
3598
3758
  }
3759
+ const code = f.sites.some((s) => s.code);
3599
3760
  const sites = f.sites.map((s) => `<li><code>${escape2(s.at)}</code>${s.code ? `<pre>${escape2(s.code)}</pre>` : ""}</li>`).join("");
3600
- const more = f.count > f.sites.length ? `<li>\u2026 and ${plural4(f.count - f.sites.length, "more place")}</li>` : "";
3601
- return `<li class="finding" data-kind="${f.kind}">${head}<ul class="sites">${sites}${more}</ul>${next(f.next)}</li>`;
3761
+ const more = f.count > f.sites.length ? `<li>+ ${plural4(f.count - f.sites.length, "more place")}</li>` : "";
3762
+ return `<li class="finding" data-kind="${f.kind}">${head}${specs}<ul class="sites${code ? " code" : ""}">${sites}${more}</ul>${next(f.next)}</li>`;
3763
+ }
3764
+ function findings(list, none, after, looks) {
3765
+ if (list.length === 0) return `<div class="body"><p class="none">${escape2(none)}</p>${next(after)}</div>`;
3766
+ return `<ul class="findings">${list.map((f) => finding2(f, false, looks)).join("")}</ul>`;
3602
3767
  }
3603
- function findings(list, none, after) {
3604
- if (list.length === 0) return `<p class="none">${escape2(none)}</p>${next(after)}`;
3605
- return `<ul class="findings">${list.map((f) => finding2(f, false)).join("")}</ul>`;
3768
+ function panel(name, id, title, aside, body, level = "h3") {
3769
+ return `<section class="panel" data-section="${name}" id="${id}"><header><${level}>${escape2(title)}</${level}>${aside ? `<span class="aside">${aside}</span>` : ""}</header>${body}</section>`;
3606
3770
  }
3607
- function projectSection(p, many) {
3771
+ function projectSection(p, many, looks) {
3772
+ const id = anchor(p.dir);
3608
3773
  const where = p.dir === "." ? "" : ` --project ${p.dir}`;
3609
- const libraries = p.libraries.length === 0 ? `<p class="none">No UI library found.</p>${next(`if it uses one, run ${NPX} detect ${p.dir} to see what was read`)}` : `<ul class="rows">${p.libraries.map((l) => `<li><span>${escape2(l.name)}${l.for.length > 0 ? ` <small>\xB7 ${escape2(l.for.join(", "))}</small>` : ""}</span><span>${escape2(l.confidence)} confidence</span></li>`).join("")}</ul>${p.unused.length > 0 ? `<p class="muted">Declared but never imported (not counted): ${escape2(p.unused.join(", "))}</p>` : ""}${next(`build new UI with these, not a new library: ${NPX} agents install tells your agent so`)}`;
3610
- const roles = p.roles.map((r) => `<li><span>${escape2(r.role)}</span><span>${plural4(r.uses, "use")} \xB7 ${plural4(r.families, "shape")}</span></li>`).join("");
3611
- const kits = p.kits.length > 0 ? `<p>Kits: ${p.kits.map((k) => `<code>${escape2(k.path)}</code> (${escape2(k.kind)}, ${plural4(k.members, "member")})`).join(", ")}</p>` : "";
3612
- const components = p.components.length > 0 ? `<h4>Most used</h4><ul class="rows">${p.components.map((c) => `<li><span>${escape2(c.name)} <small>${escape2(c.file)}</small></span><span>${plural4(c.uses, "use")}</span></li>`).join("")}</ul>` : "";
3613
- const inventory = `<p>${plural4(p.definitions, "component")} defined in ${plural4(p.files, "file")}; ${plural4(p.uses, "UI element")} in use (buttons, links styled as buttons, inputs, cards, badges, dialogs, tabs).</p>${kits}${roles ? `<ul class="rows">${roles}</ul>` : ""}${components}${next(`see every look grouped by shape with ${NPX} inventory${where}`)}`;
3614
- const incomplete = [...p.incomplete, ...p.failed.map((f) => `Not fully read: ${f}`)];
3615
- return `<section class="project" data-section="project" data-project="${escape2(p.dir)}" id="${anchor(p.dir)}">
3774
+ const libraries = p.libraries.length === 0 ? `<div class="body"><p class="none">No UI library found.</p>${next(`if it uses one, run ${NPX} detect ${p.dir} to see what was read`)}</div>` : `<div class="body"><ul class="rows">${p.libraries.map((l) => `<li><span>${escape2(l.name)}${l.for.length > 0 ? ` <small>${escape2(l.for.join(", "))}</small>` : ""}</span><span>${escape2(l.confidence)} confidence</span></li>`).join("")}</ul>${p.unused.length > 0 ? `<p class="muted">Declared but never imported (not counted): ${escape2(p.unused.join(", "))}</p>` : ""}${next(`build new UI with these, not a new library: ${NPX} agents install tells your agent so`)}</div>`;
3775
+ const kits = p.kits.length > 0 ? `<p class="muted">Kits: ${p.kits.map((k) => `<code>${escape2(k.path)}</code> (${escape2(k.kind)}, ${plural4(k.members, "member")})`).join(", ")}</p>` : "";
3776
+ const components = p.components.length > 0 ? `<h4>Most used</h4><ul class="rows">${p.components.map((c) => `<li><span>${escape2(c.name)}<small>${escape2(c.file)}</small></span><span>${plural4(c.uses, "use")}</span></li>`).join("")}</ul>` : "";
3777
+ const inventory = `<div class="body"><p>${plural4(p.definitions, "component")} defined in ${plural4(p.files, "file")}; ${plural4(p.uses, "UI element")} in use (buttons, links styled as buttons, inputs, cards, badges, dialogs, tabs).</p>${p.roles.length > 0 ? `<ul class="rows">${p.roles.map((r) => `<li><span class="mono">${escape2(r.role)}</span><span>${plural4(r.uses, "use")} \xB7 ${plural4(r.families, "shape")}</span></li>`).join("")}</ul>` : ""}${kits}${components}${next(`see every look grouped by shape with ${NPX} inventory${where}`)}</div>`;
3778
+ const unread = p.failed.length > 0 ? `<details class="incomplete more"><summary>${plural4(p.failed.length, "file")} not fully read; what is in them is unknown</summary><ul class="unread">${p.failed.map((f) => `<li>${rich(f)}</li>`).join("")}</ul></details>` : "";
3779
+ const count = (list) => list.length === 0 ? "None" : `${plural4(list.length, "finding")} \xB7 ${plural4(list.reduce((n, f) => n + f.count, 0), "place")}`;
3780
+ return `<section class="project" data-section="project" data-project="${escape2(p.dir)}" id="${id}">
3616
3781
  <h2>${escape2(many || p.dir !== "." ? p.dir : "This repo")}${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</h2>
3617
- ${incomplete.map((i) => `<p class="incomplete">${escape2(i)}</p>`).join("")}
3618
- <section data-section="libraries"><h3>UI libraries</h3>${libraries}</section>
3619
- <section data-section="inventory"><h3>Component inventory</h3>${inventory}</section>
3620
- <section data-section="near-duplicates"><h3>Near-duplicate groups</h3><p class="muted">Components that look the same (height, padding, corners, text size and weight) but are built more than once.</p>${findings(p.nearDuplicates, "None found: each look is built once.", `keep it that way: ${NPX} agents install has your agent reuse these components`)}</section>
3621
- <section data-section="hardcoded"><h3>Hardcoded values that should be tokens</h3><p class="muted">Colours, spacing and radii written as literals although a theme variable holds the same value.</p>${findings(p.hardcoded, "None found.", `run ${NPX} check --diff on each change to keep new literals out`)}</section>
3622
- <section data-section="off-library"><h3>Off-library imports</h3><p class="muted">Imports of an element from a library other than the one the rest of the code uses for it.</p>${findings(p.offLibrary, "None found.", `run ${NPX} check --diff on each change to catch a second library early`)}</section>
3782
+ ${p.incomplete.length > 0 || p.failed.length > 0 ? `<div class="unread-at" id="${id}-unread">${p.incomplete.map((i) => `<p class="incomplete">${escape2(i)}</p>`).join("")}${unread}</div>` : ""}
3783
+ ${panel("near-duplicates", `${id}-near`, "Near-duplicate groups", count(p.nearDuplicates), `<div class="body"><p class="muted">Components that look the same (height, padding, corners, text size and weight) but are built more than once.</p></div>${findings(p.nearDuplicates, "None found: each look is built once.", `keep it that way: ${NPX} agents install has your agent reuse these components`, looks)}`)}
3784
+ ${panel("hardcoded", `${id}-hardcoded`, "Hardcoded values that should be tokens", count(p.hardcoded), `<div class="body"><p class="muted">Colours, spacing and radii written as literals although a theme variable holds the same value.</p></div>${findings(p.hardcoded, "None found.", `run ${NPX} check --diff on each change to keep new literals out`, looks)}`)}
3785
+ ${panel("off-library", `${id}-off`, "Off-library imports", count(p.offLibrary), `<div class="body"><p class="muted">Imports of an element from a library other than the one the rest of the code uses for it.</p></div>${findings(p.offLibrary, "None found.", `run ${NPX} check --diff on each change to catch a second library early`, looks)}`)}
3786
+ ${panel("inventory", `${id}-inventory`, "Component inventory", `${plural4(p.definitions, "component")} \xB7 ${plural4(p.uses, "use")}`, inventory)}
3787
+ ${panel("libraries", `${id}-libraries`, "UI libraries", p.libraries.length > 0 ? plural4(p.libraries.length, "library", "libraries") : "", libraries)}
3623
3788
  ${p.notes.map((n) => `<p class="muted">${escape2(n.replace(/\.?$/, "."))}</p>`).join("")}
3624
3789
  </section>`;
3625
3790
  }
3626
- function looks(model) {
3791
+ function looksSection(model) {
3627
3792
  const drawn = model.projects.flatMap((p) => p.rendered.map((r) => ({ ...r, project: p.dir })));
3628
- const body = drawn.length > 0 ? `<div class="looks">${drawn.map((r) => `<figure data-component="${escape2(r.component)}"><figcaption>${escape2(r.component)} <small>${escape2(r.file)}</small></figcaption><div class="pair">${r.light ? `<img alt="${escape2(r.component)}, light" src="${r.light}">` : ""}${r.dark ? `<img alt="${escape2(r.component)}, dark" src="${r.dark}">` : ""}</div>${r.looks.length > 0 ? `<p class="muted">${escape2(r.looks.slice(0, 4).join(" \xB7 "))}</p>` : ""}</figure>`).join("")}</div>` : "";
3629
- return `<section data-section="looks" data-render="${model.render.state}"><h2>Rendered looks</h2>${body}<p class="${drawn.length > 0 ? "muted" : ""}">${escape2(model.render.note)}</p>${model.render.next ? next(model.render.next) : drawn.length > 0 ? next(`see a component after you change it with ${NPX} render <file>`) : next(model.render.state === "off" ? `run ${NPX} --render to draw the most-used components (it asks before downloading the ~100 MB headless Chromium, once)` : `run ${NPX} render <file> to see why a component could not be drawn`)}</section>`;
3793
+ const body = drawn.length > 0 ? `<div class="looks">${drawn.map((r) => `<figure data-component="${escape2(r.component)}"><figcaption>${escape2(r.component)} <small>${escape2(r.file)}</small></figcaption><div class="pair">${r.light ? `<img alt="${escape2(r.component)}, light" src="${r.light}">` : ""}${r.dark ? `<img alt="${escape2(r.component)}, dark" src="${r.dark}">` : ""}</div>${r.looks.length > 0 ? `<p class="muted">${plural4(r.looks.length, "look")} drawn, light and dark</p>` : ""}</figure>`).join("")}</div>` : "";
3794
+ const after = model.render.next ? next(model.render.next) : drawn.length > 0 ? next(`see a component after you change it with ${NPX} render <file>`) : next(model.render.state === "off" ? `run ${NPX} --render to draw the most-used components (it asks before downloading the ~100 MB headless Chromium, once)` : `run ${NPX} render <file> to see why a component could not be drawn`);
3795
+ return `<section class="panel" data-section="looks" data-render="${model.render.state}" id="looks"><header><h2>Rendered looks</h2><span class="aside">${drawn.length > 0 ? plural4(drawn.length, "component") : ""}</span></header><div class="body">${body}<p class="${drawn.length > 0 ? "muted" : ""}">${escape2(model.render.note)}</p>${after}</div></section>`;
3796
+ }
3797
+ function placesByKind(model) {
3798
+ const places = (pick) => model.projects.reduce((n, p) => n + pick(p).reduce((m, f) => m + f.count, 0), 0);
3799
+ return [
3800
+ { label: "Near-duplicates", places: places((p) => p.nearDuplicates) },
3801
+ { label: "Hardcoded values", places: places((p) => p.hardcoded) },
3802
+ { label: "Off-library imports", places: places((p) => p.offLibrary) }
3803
+ ];
3804
+ }
3805
+ function headline(model) {
3806
+ const t = model.totals;
3807
+ const [near, hard, off] = placesByKind(model);
3808
+ const parts = [
3809
+ { label: "Near-duplicates", places: near.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-near` },
3810
+ { label: "Hardcoded values", places: hard.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-hardcoded` },
3811
+ { label: "Off-library imports", places: off.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-off` }
3812
+ ];
3813
+ const total = parts.reduce((n2, part) => n2 + part.places, 0);
3814
+ const n = (value) => `<b class="${value > 0 ? "warn" : "ok"}">${value}</b>`;
3815
+ const unknown = model.projects.some((p) => p.incomplete.length > 0 || p.failed.length > 0);
3816
+ const analyses = model.projects.reduce((n2, p) => n2 + p.incomplete.length, 0);
3817
+ const unread = model.projects.reduce((n2, p) => n2 + p.failed.length, 0);
3818
+ const caveat = [analyses > 0 ? `${plural4(analyses, "analysis", "analyses")} could not run` : "", unread > 0 ? `${plural4(unread, "file")} could not be fully read` : ""].filter(Boolean).join(" and ");
3819
+ const said = [];
3820
+ if (t.nearDuplicates > 0) said.push(`${n(t.nearDuplicates)} ${t.nearDuplicates === 1 ? "look is" : "looks are"} built more than once`);
3821
+ if (t.hardcoded > 0) said.push(`${n(t.hardcoded)} ${t.hardcoded === 1 ? "value skips" : "values skip"} the theme`);
3822
+ if (t.offLibrary > 0) said.push(`${n(t.offLibrary)} ${t.offLibrary === 1 ? "import comes" : "imports come"} from a second library`);
3823
+ const sentence2 = said.length === 0 ? `Every look is built once and every value comes from the theme${unknown ? ", in what could be read" : ""}.` : `${said.length > 1 ? `${said.slice(0, -1).join(", ")} and ${said.at(-1)}` : said[0]}: <b class="warn">${total}</b> ${total === 1 ? "place" : "places"} to fix.`;
3824
+ const bar2 = total > 0 ? `<div class="split" role="img" aria-label="${parts.map((part) => `${part.label} ${part.places}`).join(", ")}">${parts.map((part, i) => part.places > 0 ? `<span class="seg s${i}" style="width:${(part.places / total * 100).toFixed(2)}%"></span>` : "").join("")}</div>` : "";
3825
+ const legend = `<ul class="legend">${parts.map((part, i) => `<li><a href="#${part.href}"><span class="key s${i}${part.places === 0 ? " zero" : ""}"></span>${escape2(part.label)} <b>${part.places}</b>${part.places === 0 ? ` <span class="clean">Clean</span>` : ""}</a></li>`).join("")}${unknown ? `<li class="alert"><a href="#${anchor(model.projects.find((p) => p.failed.length > 0 || p.incomplete.length > 0)?.dir ?? ".")}-unread" aria-describedby="unread-tip"><i aria-hidden="true">!</i>${analyses + unread} not read</a><span class="tip" id="unread-tip" role="tooltip">${escape2(caveat.charAt(0).toUpperCase() + caveat.slice(1))}: what is in them is unknown, not zero.</span></li>` : ""}<li class="of">${plural4(t.definitions, "component")} \xB7 ${plural4(t.uses, "UI element")} in use \xB7 ${plural4(t.files, "file")}</li></ul>`;
3826
+ return `<section class="headline" data-section="headline"><p class="say">${sentence2}</p>${bar2}${legend}</section>`;
3827
+ }
3828
+ var SQUARES = 40;
3829
+ function shapeMap(model) {
3830
+ const roles = /* @__PURE__ */ new Map();
3831
+ for (const p of model.projects) for (const r of p.roles) {
3832
+ const was = roles.get(r.role) ?? { uses: 0, families: 0 };
3833
+ roles.set(r.role, { uses: was.uses + r.uses, families: was.families + r.families });
3834
+ }
3835
+ if (roles.size === 0) return "";
3836
+ const rows = [...roles].map(([role, { uses, families }]) => {
3837
+ const shown2 = Math.min(families, SQUARES);
3838
+ const squares = families === 0 ? `<span class="unmeasured">shape not measured</span>` : `${Array.from({ length: shown2 }, (_, i) => `<i class="${i === 0 ? "one" : "more"}"></i>`).join("")}${families > shown2 ? `<span class="over">+${families - shown2}</span>` : ""}`;
3839
+ return `<li class="${families > 1 ? "drift" : families === 1 ? "even" : ""}"><span class="role mono">${escape2(role)}</span><span class="sq">${squares}</span><span class="val"><b>${families}</b> ${families === 1 ? "shape" : "shapes"} \xB7 ${plural4(uses, "use")}</span></li>`;
3840
+ }).join("");
3841
+ const many = model.projects.length > 1 ? ", summed over the projects" : "";
3842
+ return `<section class="panel" data-section="shapes" id="shapes"><header><h2>Shapes per element</h2><span class="aside"><i class="k one"></i>first shape <i class="k more"></i>each extra shape</span></header><div class="body"><p class="muted">One square per distinct look of each kind of element${many}. One square is consistent; every extra square is a copy someone built.</p><ul class="shapes">${rows}</ul></div></section>`;
3843
+ }
3844
+ function rail(model, many) {
3845
+ const t = model.totals;
3846
+ const link = (href, label, n, warn = false) => `<a href="#${href}"><span>${escape2(label)}</span>${n === void 0 ? "" : `<span class="n${warn && n > 0 ? " warn" : ""}">${n}</span>`}</a>`;
3847
+ const report = [link("overview", "Overview"), link("shapes", "Shapes"), link("top-fixes", "Top fixes", model.topFixes.length, true), link("looks", "Rendered looks")];
3848
+ const projects = many ? model.projects.map((p) => link(anchor(p.dir), p.dir, p.nearDuplicates.length + p.hardcoded.length + p.offLibrary.length, true)) : model.projects.flatMap((p) => {
3849
+ const id = anchor(p.dir);
3850
+ return [link(`${id}-near`, "Near-duplicates", t.nearDuplicates, true), link(`${id}-hardcoded`, "Hardcoded values", t.hardcoded, true), link(`${id}-off`, "Off-library imports", t.offLibrary, true), link(`${id}-inventory`, "Inventory", t.definitions), link(`${id}-libraries`, "UI libraries", t.libraries.length)];
3851
+ });
3852
+ return `<aside class="rail"><div class="brand"><span class="mark">Y</span><span>youiyoux</span></div><nav aria-label="Sections"><div class="group">Report</div>${report.join("")}<div class="group">${many ? "Projects" : "Findings"}</div>${projects.join("")}</nav><p class="foot">Self-contained. Nothing was uploaded.</p></aside>`;
3630
3853
  }
3631
3854
  function renderHtml(model) {
3632
3855
  const t = model.totals;
3633
3856
  const many = model.projects.length > 1;
3634
- const stat3 = (label, value, tone2 = "") => `<div class="${tone2}"><dt>${escape2(label)}</dt><dd>${value}</dd></div>`;
3635
- const tone = (n) => n > 0 ? "warn" : "ok";
3636
3857
  const date = model.generatedAt.slice(0, 10);
3637
- const top = model.topFixes.length > 0 ? `<ol class="findings">${model.topFixes.map((f) => finding2(f, many)).join("")}</ol>` : `<p class="none">Nothing to fix first.</p>${next(`${NPX} agents install, so your agent keeps it that way`)}`;
3858
+ const looks = new Map(model.projects.flatMap((p) => p.rendered.map((r) => [r.file, r])));
3859
+ const top = model.topFixes.length > 0 ? `<ol class="findings">${model.topFixes.map((f) => finding2(f, many, looks)).join("")}</ol>` : `<div class="body"><p class="none">Nothing to fix first.</p>${next(`${NPX} agents install, so your agent keeps it that way`)}</div>`;
3860
+ const libraries = t.libraries.length > 0 ? `<li>${escape2(t.libraries.join(" \xB7 "))}</li>` : "";
3638
3861
  return `<!doctype html>
3639
3862
  <html lang="en">
3640
3863
  <head>
@@ -3647,24 +3870,27 @@ function renderHtml(model) {
3647
3870
  <style>${CSS}</style>
3648
3871
  </head>
3649
3872
  <body>
3873
+ <div class="shell">
3874
+ ${rail(model, many)}
3650
3875
  <main>
3651
- <header>
3652
- <p class="eyebrow">youiyoux report</p>
3876
+ <header class="top" id="overview">
3877
+ <p class="crumb">youiyoux report</p>
3653
3878
  <h1>${escape2(model.repo)}</h1>
3654
- <p class="meta">${plural4(t.projects, "project")} \xB7 ${plural4(t.files, "file")} \xB7 scanned ${escape2(date)}</p>
3655
- <ul class="summary">${model.summary.map((line) => `<li>${escape2(line)}</li>`).join("")}</ul>
3656
- <dl class="stats">${stat3("Components defined", t.definitions)}${stat3("Near-duplicate groups", t.nearDuplicates, tone(t.nearDuplicates))}${stat3("Hardcoded values", t.hardcoded, tone(t.hardcoded))}${stat3("Off-library imports", t.offLibrary, tone(t.offLibrary))}</dl>
3879
+ <ul class="facts"><li><b>${t.files}</b> files</li><li><b>${t.projects}</b> ${t.projects === 1 ? "project" : "projects"}</li>${libraries}<li class="mono">${escape2(date)}</li><li class="mono">${(model.durationMs / 1e3).toFixed(1)} s</li></ul>
3657
3880
  </header>
3658
- <section data-section="top-fixes"><h2>Top fixes</h2><p class="muted">Ranked by how many places each one touches.</p>${top}</section>
3881
+ ${headline(model)}
3882
+ ${shapeMap(model)}
3883
+ ${panel("top-fixes", "top-fixes", "Top fixes", "Ranked by how many places each one touches", top, "h2")}
3659
3884
  <aside class="agent" data-section="agent"><p><strong>${escape2(model.agent)}</strong></p>${next(`${NPX} agents install, then restart your agent`)}</aside>
3660
- ${looks(model)}
3661
- <section data-section="projects"><h2>Projects</h2><p>${escape2(model.layout.charAt(0).toUpperCase() + model.layout.slice(1))}.</p>${many ? `<nav><ul>${model.projects.map((p) => `<li><a href="#${anchor(p.dir)}">${escape2(p.dir)}</a>${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</li>`).join("")}</ul></nav>` : ""}${next(many ? "open each project below; each has its own fixes" : "read this repo's section below")}</section>
3662
- ${model.projects.map((p) => projectSection(p, many)).join("\n")}
3885
+ ${looksSection(model)}
3886
+ <section class="panel projects" data-section="projects" id="projects"><header><h2>Projects</h2><span class="aside">${plural4(model.projects.length, "project")}</span></header><div class="body"><p>${escape2(model.layout.charAt(0).toUpperCase() + model.layout.slice(1))}.</p>${many ? `<nav><ul>${model.projects.map((p) => `<li><a href="#${anchor(p.dir)}">${escape2(p.dir)}</a>${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</li>`).join("")}</ul></nav>` : ""}${next(many ? "open each project below; each has its own fixes" : "read this repo's section below")}</div></section>
3887
+ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
3663
3888
  <footer>
3664
3889
  <p>Made with ${escape2(NPX)} on ${escape2(date)} in ${(model.durationMs / 1e3).toFixed(1)} s. This file is self-contained: it loads nothing from the network, and nothing was uploaded.</p>
3665
3890
  <p>Paths are relative to the repo. ${model.includeCode ? "Lines of code were included (--include-code); keys, tokens and environment values in them were removed." : "Includes relative paths, component names, class lists, counts and rendered images when available. Literal prop values and text content are omitted from captions and fixes; --include-code adds source lines and full captions."} Home paths, usernames, environment values and credentials were removed.</p>
3666
3891
  </footer>
3667
3892
  </main>
3893
+ </div>
3668
3894
  </body>
3669
3895
  </html>
3670
3896
  `;
@@ -3779,9 +4005,11 @@ function renderMarkdown(model, reportFile) {
3779
4005
  "",
3780
4006
  ...model.summary.map((line) => `- ${md(line)}`),
3781
4007
  "",
3782
- "| Projects | Components | Near-duplicate groups | Hardcoded values | Off-library imports |",
4008
+ "| Projects | Components | Duplicated components | Hardcoded values | Off-library imports |",
3783
4009
  "| ---: | ---: | ---: | ---: | ---: |",
3784
- `| ${t.projects} | ${t.definitions} | ${t.nearDuplicates} | ${t.hardcoded} | ${t.offLibrary} |`,
4010
+ `| ${t.projects} | ${t.definitions} | ${t.componentDuplicates} | ${t.hardcoded} | ${t.offLibrary} |`,
4011
+ "",
4012
+ md(DUPLICATE_RULE),
3785
4013
  ""
3786
4014
  ];
3787
4015
  if (model.topFixes.length > 0) {
@@ -3993,12 +4221,7 @@ async function runReport(options = {}, io = {}) {
3993
4221
  const t = clean.totals;
3994
4222
  const card = {
3995
4223
  repo: redactor.scrub(clean.repo),
3996
- stats: [
3997
- { value: t.definitions, label: ["components", "defined"] },
3998
- { value: t.nearDuplicates, label: ["near-", "duplicates"], tone: t.nearDuplicates > 0 ? "warn" : "ok" },
3999
- { value: t.hardcoded, label: ["hardcoded", "values"], tone: t.hardcoded > 0 ? "warn" : "ok" },
4000
- { value: t.offLibrary, label: ["off-library", "imports"], tone: t.offLibrary > 0 ? "warn" : "ok" }
4001
- ],
4224
+ parts: placesByKind(clean),
4002
4225
  footer: redactor.scrub(t.libraries.length > 0 ? `UI libraries: ${t.libraries.join(", ")}` : "No UI library found")
4003
4226
  };
4004
4227
  const html = redactor.scrub(renderHtml(clean));
@@ -4011,7 +4234,7 @@ async function runReport(options = {}, io = {}) {
4011
4234
  return { code: 0, files, model: clean };
4012
4235
  }
4013
4236
  const width = Math.max(...["Report", "Card", "Summary"].map((w) => w.length)) + 2;
4014
- const looks2 = clean.render.state === "drawn" ? "" : clean.render.state === "off" && !renderNote ? `; to add rendered looks, run ${NPX} --render` : "";
4237
+ const looks = clean.render.state === "drawn" ? "" : clean.render.state === "off" && !renderNote ? `; to add rendered looks, run ${NPX} --render` : "";
4015
4238
  print2([
4016
4239
  ...clean.summary,
4017
4240
  "",
@@ -4019,7 +4242,7 @@ async function runReport(options = {}, io = {}) {
4019
4242
  `${"Card".padEnd(width)}${shown(files.card)}`,
4020
4243
  `${"Summary".padEnd(width)}${shown(files.summary)}`,
4021
4244
  "",
4022
- `Next: open ${shown(files.report)} in a browser and start with its top fix; set up youiyoux for your agent with ${NPX} agents install${looks2}.`
4245
+ `Next: open ${shown(files.report)} in a browser and start with its top fix; set up youiyoux for your agent with ${NPX} agents install${looks}.`
4023
4246
  ].join("\n"));
4024
4247
  return { code: 0, files, model: clean };
4025
4248
  }
package/dist/youi.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __youiRequire } from "node:module"; const require = __youiRequire(import.meta.url);
3
3
  import {
4
4
  main
5
- } from "./chunks/chunk-UXLATKEQ.js";
5
+ } from "./chunks/chunk-CLNMVQZJ.js";
6
6
  import "./chunks/chunk-FUB7B3VK.js";
7
7
  import "./chunks/chunk-UHPK62Q5.js";
8
8
  import "./chunks/chunk-3ZHGONNA.js";
package/dist/youiyoux.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire as __youiRequire } from "node:module"; const require = __
3
3
  import {
4
4
  COMMAND_NAMES,
5
5
  main
6
- } from "./chunks/chunk-UXLATKEQ.js";
6
+ } from "./chunks/chunk-CLNMVQZJ.js";
7
7
  import "./chunks/chunk-FUB7B3VK.js";
8
8
  import "./chunks/chunk-UHPK62Q5.js";
9
9
  import "./chunks/chunk-3ZHGONNA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "youiyoux",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Your agent rebuilds components you already have. youiyoux catches it: npx youiyoux turns any repo into a shareable report of near-duplicate components, hardcoded token values and off-library imports, with the fixes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,8 +14,8 @@
14
14
  },
15
15
  "type": "module",
16
16
  "bin": {
17
- "youiyoux": "./dist/youiyoux.js",
18
- "youi": "./dist/youi.js"
17
+ "youiyoux": "dist/youiyoux.js",
18
+ "youi": "dist/youi.js"
19
19
  },
20
20
  "files": [
21
21
  "dist",
@@ -32,17 +32,12 @@
32
32
  "ai-agents",
33
33
  "claude-code",
34
34
  "codex",
35
- "cursor",
36
35
  "mcp",
37
36
  "tailwind",
38
37
  "shadcn",
39
38
  "react",
40
39
  "audit"
41
40
  ],
42
- "scripts": {
43
- "build": "node build.mjs",
44
- "prepack": "node build.mjs"
45
- },
46
41
  "dependencies": {
47
42
  "@ast-grep/napi": "0.45.3",
48
43
  "axe-core": "4.14.0",
@@ -51,11 +46,14 @@
51
46
  "typescript": "6.0.3"
52
47
  },
53
48
  "devDependencies": {
54
- "@youiyoux/cli": "workspace:*",
55
- "@youiyoux/core": "workspace:*",
56
- "@youiyoux/scanner": "workspace:*"
49
+ "@youiyoux/cli": "0.1.2",
50
+ "@youiyoux/core": "0.1.2",
51
+ "@youiyoux/scanner": "0.1.2"
57
52
  },
58
53
  "exports": {
59
54
  "./package.json": "./package.json"
55
+ },
56
+ "scripts": {
57
+ "build": "node build.mjs"
60
58
  }
61
- }
59
+ }