@agent-scope/site 1.19.0 → 1.20.1

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/dist/index.cjs CHANGED
@@ -10,9 +10,26 @@ function normalizeOptions(options) {
10
10
  outputDir: options?.outputDir ?? ".reactscope/site",
11
11
  basePath: options?.basePath ?? "/",
12
12
  compliancePath: options?.compliancePath ?? "",
13
- title: options?.title ?? "Scope \u2014 Component Gallery"
13
+ tokenFilePath: options?.tokenFilePath ?? "",
14
+ title: options?.title ?? "Scope \u2014 Component Gallery",
15
+ iconPatterns: options?.iconPatterns ?? []
14
16
  };
15
17
  }
18
+ function flattenTokens(obj, prefix, result) {
19
+ for (const [key, value] of Object.entries(obj)) {
20
+ const path = prefix ? `${prefix}.${key}` : key;
21
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && "value" in value && "type" in value) {
22
+ const v = value;
23
+ result.push({
24
+ path,
25
+ value: String(v.value),
26
+ type: String(v.type)
27
+ });
28
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
29
+ flattenTokens(value, path, result);
30
+ }
31
+ }
32
+ }
16
33
  async function readJsonFile(filePath) {
17
34
  const content = await promises.readFile(filePath, "utf-8");
18
35
  return JSON.parse(content);
@@ -27,7 +44,7 @@ async function readSiteData(options) {
27
44
  const jsonFiles = entries.filter((f) => f.endsWith(".json"));
28
45
  await Promise.all(
29
46
  jsonFiles.map(async (file) => {
30
- const componentName = file.replace(/\.json$/, "");
47
+ const componentName = file.replace(/(?:\.error)?\.json$/, "");
31
48
  const filePath = path.join(rendersDir, file);
32
49
  try {
33
50
  const renderData = await readJsonFile(filePath);
@@ -53,15 +70,63 @@ async function readSiteData(options) {
53
70
  );
54
71
  }
55
72
  }
73
+ let tokenEntries;
74
+ let tokenMeta;
75
+ let siteTokenThemes;
76
+ if (options.tokenFilePath) {
77
+ try {
78
+ const tokenFile = await readJsonFile(options.tokenFilePath);
79
+ const entries = [];
80
+ flattenTokens(tokenFile.tokens, "", entries);
81
+ tokenEntries = entries;
82
+ tokenMeta = tokenFile.meta;
83
+ if (tokenFile.themes && Object.keys(tokenFile.themes).length > 0) {
84
+ siteTokenThemes = tokenFile.themes;
85
+ }
86
+ } catch (err) {
87
+ console.warn(
88
+ `[scope/site] Warning: could not read token file ${options.tokenFilePath}:`,
89
+ err instanceof Error ? err.message : String(err)
90
+ );
91
+ }
92
+ }
93
+ let playgroundDefaults;
94
+ try {
95
+ const defaultsPath = path.join(options.inputDir, "playground-defaults.json");
96
+ const defaultsRaw = await readJsonFile(defaultsPath);
97
+ playgroundDefaults = new Map(Object.entries(defaultsRaw));
98
+ } catch {
99
+ }
56
100
  return {
57
101
  manifest,
58
102
  renders,
59
103
  complianceBatch,
60
- options
104
+ tokenEntries,
105
+ tokenThemes: siteTokenThemes,
106
+ tokenMeta,
107
+ options,
108
+ playgroundDefaults
61
109
  };
62
110
  }
63
111
 
64
112
  // src/utils.ts
113
+ function matchGlob(pattern, value) {
114
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
115
+ const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
116
+ return new RegExp(`^${regexStr}$`, "i").test(value);
117
+ }
118
+ function domTreeToSvg(node) {
119
+ if (node.tag === "#text") {
120
+ return node.text ?? "";
121
+ }
122
+ const attrs = Object.entries(node.attrs).map(([k, v]) => ` ${k}="${escapeHtml(v)}"`).join("");
123
+ const children = node.children.map((child) => domTreeToSvg(child)).join("");
124
+ const textContent = node.text?.trim() ?? "";
125
+ if (!children && !textContent) {
126
+ return `<${node.tag}${attrs} />`;
127
+ }
128
+ return `<${node.tag}${attrs}>${textContent}${children}</${node.tag}>`;
129
+ }
65
130
  function escapeHtml(str) {
66
131
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
67
132
  }
@@ -71,7 +136,7 @@ function slugify(name) {
71
136
  function renderDOMTree(node, depth = 0) {
72
137
  const indent = " ".repeat(depth);
73
138
  const attrsHtml = Object.entries(node.attrs).map(([k, v]) => {
74
- const display = v.length > 60 ? v.slice(0, 57) + "\u2026" : v;
139
+ const display = v.length > 60 ? `${v.slice(0, 57)}\u2026` : v;
75
140
  return ` <span class="dom-attr-name">${escapeHtml(k)}</span>=<span class="dom-attr-value">"${escapeHtml(display)}"</span>`;
76
141
  }).join("");
77
142
  const nodeIdAttr = node.nodeId !== void 0 ? ` data-node-id="${node.nodeId}"` : "";
@@ -90,6 +155,73 @@ ${indent}<summary><span class="dom-tag-open">&lt;${escapeHtml(node.tag)}${attrsH
90
155
  ${textHtml}${childrenHtml}${indent}<span class="dom-tag-close">&lt;/${escapeHtml(node.tag)}&gt;</span>
91
156
  </details>`;
92
157
  }
158
+ function propControlHtml(name, prop, overrideDefault) {
159
+ const escapedName = escapeHtml(name);
160
+ const dataAttrs = `data-prop-name="${escapedName}" data-prop-type="${escapeHtml(prop.type)}"`;
161
+ const rawDefault = overrideDefault ?? prop.default;
162
+ const defaultVal = rawDefault !== void 0 ? rawDefault.replace(/^['"]|['"]$/g, "") : void 0;
163
+ switch (prop.type) {
164
+ case "boolean": {
165
+ const checked = defaultVal === "true" ? " checked" : "";
166
+ return `<div class="pg-control-row">
167
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
168
+ <input class="pg-checkbox" type="checkbox" id="pg-${escapedName}" ${dataAttrs}${checked} />
169
+ </div>`;
170
+ }
171
+ case "number": {
172
+ const numVal = defaultVal ?? (prop.required ? "0" : "");
173
+ return `<div class="pg-control-row">
174
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
175
+ <input class="pg-input" type="number" id="pg-${escapedName}" ${dataAttrs} value="${escapeHtml(numVal)}" />
176
+ </div>`;
177
+ }
178
+ case "union": {
179
+ if (prop.values && prop.values.length > 0) {
180
+ const options = prop.values.map((v) => {
181
+ const sel = defaultVal === v ? " selected" : "";
182
+ return `<option value="${escapeHtml(v)}"${sel}>${escapeHtml(v)}</option>`;
183
+ }).join("");
184
+ const emptyOption = prop.required ? "" : '<option value="">\u2014</option>';
185
+ return `<div class="pg-control-row">
186
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
187
+ <select class="pg-select" id="pg-${escapedName}" ${dataAttrs}>${emptyOption}${options}</select>
188
+ </div>`;
189
+ }
190
+ const textVal = defaultVal ?? "";
191
+ return `<div class="pg-control-row">
192
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
193
+ <input class="pg-input" type="text" id="pg-${escapedName}" ${dataAttrs} value="${escapeHtml(textVal)}" placeholder="${escapeHtml(prop.rawType)}" />
194
+ </div>`;
195
+ }
196
+ case "string": {
197
+ const strVal = defaultVal ?? (prop.required ? "" : "");
198
+ return `<div class="pg-control-row">
199
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
200
+ <input class="pg-input" type="text" id="pg-${escapedName}" ${dataAttrs} value="${escapeHtml(strVal)}" />
201
+ </div>`;
202
+ }
203
+ case "function":
204
+ return `<div class="pg-control-row">
205
+ <label class="pg-control-label">${escapedName}</label>
206
+ <span class="pg-control-readonly">${escapeHtml(prop.rawType)}</span>
207
+ </div>`;
208
+ case "node":
209
+ case "element": {
210
+ const nodeVal = defaultVal ?? "";
211
+ return `<div class="pg-control-row">
212
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
213
+ <input class="pg-input" type="text" id="pg-${escapedName}" ${dataAttrs} value="${escapeHtml(nodeVal)}" placeholder="text content" />
214
+ </div>`;
215
+ }
216
+ default: {
217
+ const otherVal = defaultVal ?? "";
218
+ return `<div class="pg-control-row">
219
+ <label class="pg-control-label" for="pg-${escapedName}">${escapedName}</label>
220
+ <input class="pg-input" type="text" id="pg-${escapedName}" ${dataAttrs} value="${escapeHtml(otherVal)}" placeholder="JSON" />
221
+ </div>`;
222
+ }
223
+ }
224
+ }
93
225
  function propTableRow(name, prop) {
94
226
  const valuesHtml = prop.values && prop.values.length > 0 ? `<br><span style="color:var(--color-muted);font-size:11px">${prop.values.map((v) => escapeHtml(v)).join(" | ")}</span>` : "";
95
227
  const defaultHtml = prop.default !== void 0 ? escapeHtml(prop.default) : "\u2014";
@@ -106,6 +238,7 @@ function generateCSS() {
106
238
  const css = `@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono:wght@400;700&display=swap');
107
239
 
108
240
  :root {
241
+ color-scheme: light;
109
242
  --color-text: #0f0f0f;
110
243
  --color-muted: #6b7280;
111
244
  --color-border: #e5e7eb;
@@ -116,6 +249,31 @@ function generateCSS() {
116
249
  --color-success: #16a34a;
117
250
  --color-warn: #d97706;
118
251
  --color-error: #dc2626;
252
+ --color-on-accent: #fff;
253
+ --color-preview-bg: #f8f8f8;
254
+ --color-code-text: #e2e8f0;
255
+ --badge-complex-border: #fbbf24;
256
+ --badge-complex-color: #92400e;
257
+ --badge-complex-bg: #fffbeb;
258
+ --badge-simple-border: #6ee7b7;
259
+ --badge-simple-color: #065f46;
260
+ --badge-simple-bg: #ecfdf5;
261
+ --badge-memo-border: #a5b4fc;
262
+ --badge-memo-color: #3730a3;
263
+ --badge-memo-bg: #eef2ff;
264
+ --badge-internal-border: #d1d5db;
265
+ --badge-internal-color: #9ca3af;
266
+ --badge-internal-bg: #f3f4f6;
267
+ --pill-on-bg: #dcfce7;
268
+ --pill-on-color: #166534;
269
+ --pill-off-bg: #fef3c7;
270
+ --pill-off-color: #92400e;
271
+ --violation-bg: #fef2f2;
272
+ --violation-border: #fecaca;
273
+ --violation-color: #991b1b;
274
+ --color-bg-selected: #eef2ff;
275
+ --overlay-bg: rgba(0,0,0,0.2);
276
+ --slideout-shadow: -4px 0 24px rgba(0,0,0,0.12);
119
277
  --font-body: 'Inter', system-ui, -apple-system, sans-serif;
120
278
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
121
279
  --radius: 6px;
@@ -123,6 +281,47 @@ function generateCSS() {
123
281
  --shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
124
282
  }
125
283
 
284
+ [data-theme="dark"] {
285
+ color-scheme: dark;
286
+ --color-text: #f3f4f6;
287
+ --color-muted: #9ca3af;
288
+ --color-border: #1f2937;
289
+ --color-bg: #111827;
290
+ --color-bg-subtle: #1f2937;
291
+ --color-bg-code: #0d1117;
292
+ --color-accent: #60a5fa;
293
+ --color-success: #4ade80;
294
+ --color-warn: #fbbf24;
295
+ --color-error: #f87171;
296
+ --color-on-accent: #fff;
297
+ --color-preview-bg: #1f2937;
298
+ --color-code-text: #e2e8f0;
299
+ --badge-complex-border: #92400e;
300
+ --badge-complex-color: #fcd34d;
301
+ --badge-complex-bg: rgba(120,53,15,0.3);
302
+ --badge-simple-border: #065f46;
303
+ --badge-simple-color: #6ee7b7;
304
+ --badge-simple-bg: rgba(6,78,59,0.3);
305
+ --badge-memo-border: #4338ca;
306
+ --badge-memo-color: #a5b4fc;
307
+ --badge-memo-bg: rgba(67,56,202,0.2);
308
+ --badge-internal-border: #4b5563;
309
+ --badge-internal-color: #9ca3af;
310
+ --badge-internal-bg: #374151;
311
+ --pill-on-bg: rgba(22,101,52,0.3);
312
+ --pill-on-color: #86efac;
313
+ --pill-off-bg: rgba(146,64,14,0.3);
314
+ --pill-off-color: #fde68a;
315
+ --violation-bg: rgba(153,27,27,0.2);
316
+ --violation-border: #991b1b;
317
+ --violation-color: #fca5a5;
318
+ --color-bg-selected: rgba(99,102,241,0.15);
319
+ --overlay-bg: rgba(0,0,0,0.5);
320
+ --slideout-shadow: -4px 0 24px rgba(0,0,0,0.4);
321
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
322
+ --shadow: 0 2px 4px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
323
+ }
324
+
126
325
  *, *::before, *::after { box-sizing: border-box; }
127
326
 
128
327
  body {
@@ -147,7 +346,13 @@ body {
147
346
  padding: 0 24px;
148
347
  gap: 16px;
149
348
  }
150
- .top-nav .site-title { font-weight: 600; font-size: 15px; text-decoration: none; color: var(--color-text); }
349
+ .top-nav .site-title { font-weight: 600; font-size: 15px; text-decoration: none; color: var(--color-text); display: flex; align-items: center; gap: 8px; }
350
+ .top-nav .site-logo { flex-shrink: 0; }
351
+ .top-nav .breadcrumbs { display: flex; align-items: center; gap: 0; font-size: 14px; color: var(--color-text-muted); }
352
+ .top-nav .breadcrumbs a, .top-nav .breadcrumbs span { text-decoration: none; color: var(--color-text-muted); }
353
+ .top-nav .breadcrumbs a:hover { color: var(--color-text); }
354
+ .top-nav .breadcrumbs span:last-child { color: var(--color-text); font-weight: 500; }
355
+ .top-nav .breadcrumbs a::before, .top-nav .breadcrumbs span::before { content: "/"; margin: 0 8px; color: var(--color-border); }
151
356
  .top-nav .spacer { flex: 1; }
152
357
  .search-box {
153
358
  border: 1px solid var(--color-border);
@@ -161,6 +366,24 @@ body {
161
366
  }
162
367
  .search-box:focus { border-color: var(--color-accent); background: var(--color-bg); }
163
368
 
369
+ /* Theme toggle */
370
+ .theme-toggle {
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ width: 32px;
375
+ height: 32px;
376
+ border: 1px solid var(--color-border);
377
+ border-radius: var(--radius);
378
+ background: var(--color-bg-subtle);
379
+ color: var(--color-muted);
380
+ cursor: pointer;
381
+ transition: color 0.15s, border-color 0.15s;
382
+ padding: 0;
383
+ flex-shrink: 0;
384
+ }
385
+ .theme-toggle:hover { color: var(--color-text); border-color: var(--color-muted); }
386
+
164
387
  /* Page layout */
165
388
  .page-layout {
166
389
  display: flex;
@@ -258,9 +481,9 @@ body {
258
481
  color: var(--color-muted);
259
482
  background: var(--color-bg-subtle);
260
483
  }
261
- .badge.complex { border-color: #fbbf24; color: #92400e; background: #fffbeb; }
262
- .badge.simple { border-color: #6ee7b7; color: #065f46; background: #ecfdf5; }
263
- .badge.memoized { border-color: #a5b4fc; color: #3730a3; background: #eef2ff; }
484
+ .badge.complex { border-color: var(--badge-complex-border); color: var(--badge-complex-color); background: var(--badge-complex-bg); }
485
+ .badge.simple { border-color: var(--badge-simple-border); color: var(--badge-simple-color); background: var(--badge-simple-bg); }
486
+ .badge.memoized { border-color: var(--badge-memo-border); color: var(--badge-memo-color); background: var(--badge-memo-bg); }
264
487
 
265
488
  .filepath {
266
489
  font-family: var(--font-mono);
@@ -283,6 +506,17 @@ body {
283
506
  .section-header p { color: var(--color-muted); margin: 0; font-size: 13px; }
284
507
 
285
508
  /* Not generated placeholder */
509
+ .render-failure {
510
+ background: var(--violation-bg);
511
+ border: 1px solid var(--violation-border);
512
+ border-radius: var(--radius);
513
+ padding: 12px 16px;
514
+ color: var(--violation-color);
515
+ font-size: 13px;
516
+ margin-bottom: 16px;
517
+ }
518
+ .matrix-cell-error { color: var(--color-error); font-size: 11px; font-weight: 600; }
519
+
286
520
  .not-generated {
287
521
  background: var(--color-bg-subtle);
288
522
  border: 1px dashed var(--color-border);
@@ -298,7 +532,7 @@ body {
298
532
  border: 1px solid var(--color-border);
299
533
  border-radius: var(--radius);
300
534
  overflow: hidden;
301
- background: #f8f8f8;
535
+ background: var(--color-preview-bg);
302
536
  display: inline-block;
303
537
  max-width: 100%;
304
538
  }
@@ -328,10 +562,50 @@ body {
328
562
  .prop-required { color: var(--color-error); font-size: 11px; }
329
563
  .prop-default { font-family: var(--font-mono); font-size: 11px; color: var(--color-muted); }
330
564
 
565
+ /* Inherited props section */
566
+ .inherited-props-section { margin-top: 20px; }
567
+ .inherited-props-heading {
568
+ font-size: 11px;
569
+ font-weight: 600;
570
+ text-transform: uppercase;
571
+ letter-spacing: 0.05em;
572
+ color: var(--color-muted);
573
+ padding-bottom: 8px;
574
+ border-bottom: 1px solid var(--color-border);
575
+ margin-bottom: 8px;
576
+ }
577
+ .inherited-group {
578
+ border: 1px solid var(--color-border);
579
+ border-radius: 6px;
580
+ margin-bottom: 6px;
581
+ }
582
+ .inherited-group summary {
583
+ padding: 6px 12px;
584
+ font-size: 12px;
585
+ font-weight: 500;
586
+ color: var(--color-muted);
587
+ cursor: pointer;
588
+ user-select: none;
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 6px;
592
+ }
593
+ .inherited-group summary:hover { color: var(--color-text); }
594
+ .inherited-group[open] summary { border-bottom: 1px solid var(--color-border); }
595
+ .inherited-group-count {
596
+ font-size: 10px;
597
+ font-weight: 400;
598
+ color: var(--color-muted);
599
+ opacity: 0.7;
600
+ }
601
+ .inherited-group .props-table { font-size: 12px; }
602
+ .inherited-group .props-table td,
603
+ .inherited-group .props-table th { padding: 4px 12px; }
604
+
331
605
  /* Code blocks */
332
606
  pre.code-block {
333
607
  background: var(--color-bg-code);
334
- color: #e2e8f0;
608
+ color: var(--color-code-text);
335
609
  border-radius: var(--radius);
336
610
  padding: 16px 20px;
337
611
  overflow-x: auto;
@@ -391,13 +665,13 @@ pre.code-block {
391
665
  .pill-on {
392
666
  display: inline-flex; align-items: center; gap: 4px;
393
667
  padding: 2px 8px; border-radius: 999px;
394
- background: #dcfce7; color: #166534;
668
+ background: var(--pill-on-bg); color: var(--pill-on-color);
395
669
  font-size: 11px; font-weight: 500;
396
670
  }
397
671
  .pill-off {
398
672
  display: inline-flex; align-items: center; gap: 4px;
399
673
  padding: 2px 8px; border-radius: 999px;
400
- background: #fef3c7; color: #92400e;
674
+ background: var(--pill-off-bg); color: var(--pill-off-color);
401
675
  font-size: 11px; font-weight: 500;
402
676
  }
403
677
 
@@ -424,12 +698,12 @@ pre.code-block {
424
698
  .violation-list { list-style: none; padding: 0; margin: 0; }
425
699
  .violation-list li {
426
700
  padding: 8px 12px;
427
- background: #fef2f2;
428
- border: 1px solid #fecaca;
701
+ background: var(--violation-bg);
702
+ border: 1px solid var(--violation-border);
429
703
  border-radius: var(--radius);
430
704
  margin-bottom: 6px;
431
705
  font-size: 13px;
432
- color: #991b1b;
706
+ color: var(--violation-color);
433
707
  }
434
708
  .a11y-role-badge { font-family: var(--font-mono); font-size: 11px; background: var(--color-bg-subtle); border: 1px solid var(--color-border); padding: 2px 6px; border-radius: 4px; }
435
709
 
@@ -491,7 +765,7 @@ details.dom-node > summary::-webkit-details-marker { display: none; }
491
765
  }
492
766
  .component-card:hover { box-shadow: var(--shadow); }
493
767
  .card-preview {
494
- background: #f8f8f8;
768
+ background: var(--color-preview-bg);
495
769
  height: 160px;
496
770
  overflow: hidden;
497
771
  display: flex;
@@ -519,245 +793,1779 @@ details.dom-node > summary::-webkit-details-marker { display: none; }
519
793
  .content-body { padding: 20px 16px; }
520
794
  .analysis-grid { grid-template-columns: 1fr; }
521
795
  .composition-lists { grid-template-columns: 1fr; }
522
- }`;
523
- return `<style>
524
- ${css}
525
- </style>`;
526
796
  }
527
-
528
- // src/templates/layout.ts
529
- function sidebarLinks(components, currentSlug, basePath) {
530
- const links = components.sort().map((name) => {
531
- const slug = slugify(name);
532
- const isActive = slug === currentSlug;
533
- return `<a href="${basePath}${slug}.html" class="${isActive ? "active" : ""}">${escapeHtml(name)}</a>`;
534
- }).join("\n");
535
- return `<div class="sidebar-heading">Components</div>
536
- ${links}`;
797
+ /* Collection section headers in sidebar */
798
+ .sidebar-collection-header {
799
+ font-size: 10px;
800
+ font-weight: 700;
801
+ letter-spacing: 0.08em;
802
+ text-transform: uppercase;
803
+ color: var(--color-muted);
804
+ padding: 12px 16px 4px;
805
+ margin-top: 4px;
806
+ border-top: 1px solid var(--color-border);
537
807
  }
538
- function htmlShell(options) {
539
- const { title, body, sidebar, onThisPage, basePath } = options;
540
- return `<!DOCTYPE html>
541
- <html lang="en">
542
- <head>
543
- <meta charset="UTF-8" />
544
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
545
- <title>${escapeHtml(title)}</title>
546
- ${generateCSS()}
547
- </head>
548
- <body>
549
- <nav class="top-nav">
550
- <a class="site-title" href="${basePath}index.html">Scope</a>
551
- <div class="spacer"></div>
552
- <input
553
- class="search-box"
554
- type="search"
555
- placeholder="Search components\u2026"
556
- id="sidebar-search"
557
- aria-label="Search components"
558
- />
559
- </nav>
560
- <div class="page-layout">
561
- <nav class="sidebar" id="sidebar">
562
- ${sidebar}
563
- </nav>
564
- <div class="main-content">
565
- <div class="content-body">
566
- ${body}
567
- </div>
568
- <nav class="on-this-page">
569
- <h4>On this page</h4>
570
- ${onThisPage}
571
- </nav>
572
- </div>
573
- </div>
574
- <script>
575
- (function () {
576
- var search = document.getElementById('sidebar-search');
577
- var sidebar = document.getElementById('sidebar');
578
- if (!search || !sidebar) return;
579
- search.addEventListener('input', function () {
580
- var q = search.value.toLowerCase();
581
- var links = sidebar.querySelectorAll('a');
582
- links.forEach(function (link) {
583
- var text = link.textContent || '';
584
- link.style.display = text.toLowerCase().includes(q) ? '' : 'none';
585
- });
586
- });
587
- })();
588
- </script>
589
- </body>
590
- </html>`;
808
+ .sidebar-collection-header:first-child {
809
+ margin-top: 0;
810
+ border-top: none;
591
811
  }
592
812
 
593
- // src/templates/component-detail.ts
594
- var SECTIONS = [
595
- "Playground",
596
- "Matrix",
597
- "Docs",
598
- "Analysis",
599
- "X-Ray",
600
- "Tokens",
601
- "Accessibility",
602
- "Composition",
603
- "Responsive",
604
- "Stress Tests"
605
- ];
606
- function notGenerated(message = "Not generated") {
607
- return `<div class="not-generated">${escapeHtml(message)}</div>`;
813
+ /* Collection section headers on index page */
814
+ .collection-section {
815
+ margin-bottom: 40px;
608
816
  }
609
- function sectionWrap(id, title, description, content) {
610
- return `<section class="section" id="${id}">
611
- <div class="section-header">
612
- <h2>${escapeHtml(title)}</h2>
613
- <p>${escapeHtml(description)}</p>
614
- </div>
615
- ${content}
616
- </section>`;
817
+ .collection-section-header {
818
+ margin-bottom: 16px;
819
+ padding-bottom: 10px;
820
+ border-bottom: 2px solid var(--color-border);
617
821
  }
618
- function renderPlayground(name, data) {
619
- const component = data.manifest.components[name];
620
- const render = data.renders.get(name);
621
- const props = component ? Object.entries(component.props) : [];
622
- const propsTable = props.length > 0 ? `<table class="props-table">
623
- <thead>
624
- <tr>
625
- <th>Prop</th>
626
- <th>Type</th>
627
- <th>Required</th>
628
- <th>Default</th>
629
- </tr>
630
- </thead>
631
- <tbody>
632
- ${props.map(([n, p]) => propTableRow(n, p)).join("\n ")}
633
- </tbody>
634
- </table>` : `<p style="color:var(--color-muted);font-size:13px">No props defined.</p>`;
635
- const renderW = render?.width != null ? render.width : void 0;
636
- const renderH = render?.height != null ? render.height : void 0;
637
- const renderSizeAttr = renderW != null && renderH != null ? ` width="${renderW}" height="${renderH}"` : "";
638
- const renderHtml = render?.screenshot ? `<div class="render-preview">
639
- <img src="data:image/png;base64,${render.screenshot}" alt="${escapeHtml(name)} render"${renderSizeAttr} />
640
- </div>` : notGenerated("Render not generated. Run scope render to produce screenshots.");
641
- return sectionWrap(
642
- "playground",
643
- "Playground",
644
- "Props reference and rendered preview.",
645
- `${propsTable}
646
- <div style="margin-top:24px">${renderHtml}</div>`
647
- );
822
+ .collection-section-header h2 {
823
+ font-size: 18px;
824
+ font-weight: 600;
825
+ margin: 0 0 4px;
826
+ color: var(--color-text);
648
827
  }
649
- function renderMatrix(name, data) {
650
- const render = data.renders.get(name);
651
- if (!render?.cells || render.cells.length === 0) {
652
- return sectionWrap(
653
- "matrix",
654
- "Matrix",
655
- "Prop combination matrix renders.",
656
- notGenerated("Matrix renders not generated. Run scope render --matrix to produce a matrix.")
657
- );
658
- }
659
- const cells = render.cells;
660
- const cols = Math.ceil(Math.sqrt(cells.length));
661
- const cellsHtml = cells.map((cell) => {
662
- const label = cell.axisValues.join(" / ");
663
- const imgHtml = cell.screenshot ? `<img class="scope-screenshot" src="data:image/png;base64,${cell.screenshot}" alt="${escapeHtml(label)}" />` : `<span style="color:var(--color-muted);font-size:11px">${cell.error ? escapeHtml(cell.error) : "failed"}</span>`;
664
- return `<div class="matrix-cell">${imgHtml}<div class="cell-label">${escapeHtml(label)}</div></div>`;
665
- }).join("\n");
666
- const grid = `<div class="matrix-grid" style="grid-template-columns: repeat(${cols}, 1fr)">
667
- ${cellsHtml}
668
- </div>`;
669
- return sectionWrap("matrix", "Matrix", "Prop combination matrix renders.", grid);
828
+ .collection-section-header p {
829
+ font-size: 13px;
830
+ color: var(--color-muted);
831
+ margin: 0;
670
832
  }
671
- function renderDocs() {
672
- return sectionWrap(
673
- "docs",
674
- "Docs",
675
- "Component documentation.",
676
- `<p style="color:var(--color-muted);font-size:13px">No documentation file found for this component.</p>`
677
- );
833
+
834
+ /* Collection gallery (index page) */
835
+ .collection-gallery-section { margin-bottom: 32px; }
836
+ .gallery-section-title {
837
+ font-size: 16px;
838
+ font-weight: 600;
839
+ margin: 0 0 12px;
840
+ color: var(--color-text);
678
841
  }
679
- function renderAnalysis(name, data) {
680
- const component = data.manifest.components[name];
681
- if (!component) {
682
- return sectionWrap("analysis", "Analysis", "Static analysis results.", notGenerated());
683
- }
684
- const propCount = Object.keys(component.props).length;
685
- const hookCount = component.detectedHooks.length;
686
- const sideEffectCount = component.sideEffects.fetches.length + (component.sideEffects.timers ? 1 : 0) + component.sideEffects.subscriptions.length + (component.sideEffects.globalListeners ? 1 : 0);
687
- const statsGrid = `<div class="stats-grid">
688
- <div class="stat-card">
689
- <div class="stat-label">Complexity</div>
690
- <div class="stat-value"><span class="badge ${component.complexityClass}">${escapeHtml(component.complexityClass)}</span></div>
691
- </div>
692
- <div class="stat-card">
693
- <div class="stat-label">Props</div>
694
- <div class="stat-value">${propCount}</div>
695
- </div>
696
- <div class="stat-card">
697
- <div class="stat-label">Hooks</div>
698
- <div class="stat-value">${hookCount}</div>
699
- </div>
700
- <div class="stat-card">
701
- <div class="stat-label">Side Effects</div>
702
- <div class="stat-value">${sideEffectCount}</div>
703
- </div>
704
- <div class="stat-card">
705
- <div class="stat-label">Export</div>
706
- <div class="stat-value" style="font-size:14px">${escapeHtml(component.exportType)}</div>
707
- </div>
708
- <div class="stat-card">
709
- <div class="stat-label">Memoized</div>
710
- <div class="stat-value" style="font-size:14px">${component.memoized ? "Yes" : "No"}</div>
711
- </div>
712
- <div class="stat-card">
713
- <div class="stat-label">forwardedRef</div>
714
- <div class="stat-value" style="font-size:14px">${component.forwardedRef ? "Yes" : "No"}</div>
715
- </div>
716
- </div>`;
717
- function tagList(items) {
718
- if (items.length === 0)
719
- return `<span style="color:var(--color-muted);font-size:12px">None</span>`;
720
- return `<div class="tag-list">${items.map((i) => `<span class="tag">${escapeHtml(i)}</span>`).join("")}</div>`;
721
- }
722
- const sideEffectItems = [
723
- ...component.sideEffects.fetches.map((f) => `fetch: ${f}`),
724
- ...component.sideEffects.timers ? ["timers"] : [],
725
- ...component.sideEffects.subscriptions.map((s) => `sub: ${s}`),
726
- ...component.sideEffects.globalListeners ? ["global listeners"] : []
727
- ];
728
- const analysisGrid = `<div class="analysis-grid">
729
- <div class="analysis-card">
730
- <h3>Detected Hooks</h3>
731
- <div class="value">${tagList(component.detectedHooks)}</div>
732
- </div>
733
- <div class="analysis-card">
734
- <h3>Required Contexts</h3>
735
- <div class="value">${tagList(component.requiredContexts)}</div>
736
- </div>
737
- <div class="analysis-card">
738
- <h3>HOC Wrappers</h3>
739
- <div class="value">${tagList(component.hocWrappers)}</div>
740
- </div>
741
- <div class="analysis-card">
742
- <h3>Side Effects</h3>
743
- <div class="value">${tagList(sideEffectItems)}</div>
744
- </div>
745
- </div>`;
746
- return sectionWrap(
747
- "analysis",
748
- "Analysis",
749
- "Static analysis results for this component.",
750
- `${statsGrid}${analysisGrid}`
751
- );
842
+ .collection-gallery {
843
+ display: grid;
844
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
845
+ gap: 12px;
752
846
  }
753
- function renderXRay(name, data) {
847
+ .collection-tile {
848
+ display: flex;
849
+ flex-direction: column;
850
+ border: 1px solid var(--color-border);
851
+ border-radius: var(--radius);
852
+ overflow: hidden;
853
+ text-decoration: none;
854
+ color: var(--color-text);
855
+ transition: box-shadow 0.15s, border-color 0.15s;
856
+ }
857
+ .collection-tile:hover {
858
+ box-shadow: var(--shadow);
859
+ border-color: var(--color-accent);
860
+ }
861
+ .coll-tile-thumbs {
862
+ display: grid;
863
+ grid-template-columns: 1fr 1fr;
864
+ gap: 1px;
865
+ background: var(--color-border);
866
+ height: 100px;
867
+ overflow: hidden;
868
+ }
869
+ .coll-thumb {
870
+ display: block;
871
+ width: 100%;
872
+ height: 100%;
873
+ object-fit: cover;
874
+ background: var(--color-preview-bg);
875
+ }
876
+ .coll-thumb-empty { background: var(--color-bg-subtle); }
877
+ .coll-tile-body { padding: 10px 14px; }
878
+ .coll-tile-name { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
879
+ .coll-tile-desc {
880
+ font-size: 12px;
881
+ color: var(--color-muted);
882
+ margin-bottom: 4px;
883
+ display: -webkit-box;
884
+ -webkit-line-clamp: 2;
885
+ -webkit-box-orient: vertical;
886
+ overflow: hidden;
887
+ }
888
+ .coll-tile-count {
889
+ font-size: 11px;
890
+ color: var(--color-muted);
891
+ font-weight: 500;
892
+ }
893
+
894
+ /* Sidebar overview link */
895
+ .sidebar a:first-child {
896
+ font-weight: 500;
897
+ margin-bottom: 4px;
898
+ }
899
+
900
+ /* Internal component badge */
901
+ .badge-internal {
902
+ display: inline-flex;
903
+ align-items: center;
904
+ padding: 1px 6px;
905
+ border-radius: 999px;
906
+ font-size: 10px;
907
+ font-weight: 500;
908
+ border: 1px solid var(--badge-internal-border);
909
+ color: var(--badge-internal-color);
910
+ background: var(--badge-internal-bg);
911
+ margin-left: 4px;
912
+ vertical-align: middle;
913
+ line-height: 1.4;
914
+ }
915
+
916
+ /* Playground */
917
+ .pg-container {
918
+ display: flex;
919
+ flex-direction: row;
920
+ gap: 16px;
921
+ }
922
+ .pg-controls {
923
+ width: 260px;
924
+ flex-shrink: 0;
925
+ border: 1px solid var(--color-border);
926
+ border-radius: var(--radius);
927
+ background: var(--color-bg-subtle);
928
+ padding: 12px 16px;
929
+ max-height: 500px;
930
+ overflow-y: auto;
931
+ }
932
+ .pg-controls-header {
933
+ font-size: 11px;
934
+ font-weight: 600;
935
+ text-transform: uppercase;
936
+ letter-spacing: 0.05em;
937
+ color: var(--color-muted);
938
+ margin-bottom: 10px;
939
+ }
940
+ .pg-control-row {
941
+ display: flex;
942
+ flex-direction: column;
943
+ gap: 4px;
944
+ margin-bottom: 10px;
945
+ }
946
+ .pg-control-row:last-child { margin-bottom: 0; }
947
+ .pg-control-label {
948
+ font-family: var(--font-mono);
949
+ font-size: 10px;
950
+ font-weight: 600;
951
+ color: var(--color-muted);
952
+ letter-spacing: 0.03em;
953
+ }
954
+ .pg-input, .pg-select {
955
+ width: 100%;
956
+ border: 1px solid var(--color-border);
957
+ border-radius: var(--radius);
958
+ padding: 4px 6px;
959
+ font-family: var(--font-body);
960
+ font-size: 12px;
961
+ background: var(--color-bg);
962
+ color: var(--color-text);
963
+ outline: none;
964
+ }
965
+ .pg-select {
966
+ appearance: none;
967
+ -webkit-appearance: none;
968
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
969
+ background-repeat: no-repeat;
970
+ background-position: right 6px center;
971
+ padding-right: 22px;
972
+ padding-left: 4px;
973
+ }
974
+ .pg-input:focus, .pg-select:focus {
975
+ border-color: var(--color-accent);
976
+ }
977
+ .pg-checkbox {
978
+ width: 16px;
979
+ height: 16px;
980
+ accent-color: var(--color-accent);
981
+ cursor: pointer;
982
+ }
983
+ .pg-control-readonly {
984
+ font-family: var(--font-mono);
985
+ font-size: 11px;
986
+ color: var(--color-muted);
987
+ font-style: italic;
988
+ }
989
+
990
+ /* Preview wrapper with checkerboard background */
991
+ .pg-preview-wrapper {
992
+ flex: 1;
993
+ min-width: 0;
994
+ border: 1px solid var(--color-border);
995
+ border-radius: var(--radius);
996
+ overflow: hidden;
997
+ position: relative;
998
+ display: flex;
999
+ flex-direction: column;
1000
+ }
1001
+ .pg-iframe-area {
1002
+ flex: 1;
1003
+ background-color: #fff;
1004
+ background-image:
1005
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
1006
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
1007
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
1008
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
1009
+ background-size: 16px 16px;
1010
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
1011
+ }
1012
+ .pg-preview-wrapper.pg-dark .pg-iframe-area {
1013
+ background-color: #1a1a1a;
1014
+ background-image:
1015
+ linear-gradient(45deg, #252525 25%, transparent 25%),
1016
+ linear-gradient(-45deg, #252525 25%, transparent 25%),
1017
+ linear-gradient(45deg, transparent 75%, #252525 75%),
1018
+ linear-gradient(-45deg, transparent 75%, #252525 75%);
1019
+ }
1020
+
1021
+ /* Toolbar */
1022
+ .pg-toolbar {
1023
+ display: flex;
1024
+ align-items: center;
1025
+ gap: 2px;
1026
+ padding: 4px 6px;
1027
+ background: var(--color-bg-subtle);
1028
+ border-bottom: 1px solid var(--color-border);
1029
+ flex-shrink: 0;
1030
+ }
1031
+ .pg-toolbar-btn {
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ justify-content: center;
1035
+ width: 28px;
1036
+ height: 28px;
1037
+ border: none;
1038
+ border-radius: var(--radius);
1039
+ background: none;
1040
+ color: var(--color-muted);
1041
+ cursor: pointer;
1042
+ transition: background 0.1s, color 0.1s;
1043
+ padding: 0;
1044
+ flex-shrink: 0;
1045
+ }
1046
+ .pg-toolbar-btn:hover { background: var(--color-border); color: var(--color-text); }
1047
+ .pg-toolbar-btn.active { background: var(--color-accent); color: var(--color-on-accent); }
1048
+ .pg-toolbar-sep {
1049
+ width: 1px;
1050
+ height: 16px;
1051
+ background: var(--color-border);
1052
+ margin: 0 4px;
1053
+ flex-shrink: 0;
1054
+ }
1055
+ .pg-toolbar-spacer { flex: 1; }
1056
+
1057
+ /* iframe */
1058
+ .pg-iframe {
1059
+ display: block;
1060
+ width: 100%;
1061
+ min-height: 300px;
1062
+ height: 300px;
1063
+ border: none;
1064
+ background: transparent;
1065
+ color-scheme: normal;
1066
+ transition: max-width 0.2s ease;
1067
+ }
1068
+
1069
+ /* Fullscreen overlay */
1070
+ .pg-preview-wrapper.pg-fullscreen {
1071
+ position: fixed;
1072
+ inset: 0;
1073
+ z-index: 999;
1074
+ border-radius: 0;
1075
+ border: none;
1076
+ }
1077
+ .pg-preview-wrapper.pg-fullscreen .pg-iframe-area { flex: 1; overflow: auto; }
1078
+ .pg-preview-wrapper.pg-fullscreen .pg-iframe { height: 100%; min-height: 100%; }
1079
+
1080
+ .pg-static-fallback {
1081
+ margin-top: 16px;
1082
+ }
1083
+ .pg-static-fallback summary {
1084
+ font-size: 12px;
1085
+ color: var(--color-muted);
1086
+ cursor: pointer;
1087
+ user-select: none;
1088
+ margin-bottom: 8px;
1089
+ }
1090
+
1091
+ @media (max-width: 768px) {
1092
+ .pg-container { flex-direction: column; }
1093
+ .pg-controls { width: 100%; max-height: none; }
1094
+ }
1095
+
1096
+ /* Full-width content body (no on-this-page nav) */
1097
+ .content-body-full { max-width: none; padding: 0; }
1098
+
1099
+ /* Two-column detail layout */
1100
+ .detail-columns {
1101
+ display: grid;
1102
+ grid-template-columns: 1fr 1fr;
1103
+ gap: 0;
1104
+ }
1105
+ .detail-main {
1106
+ min-width: 0;
1107
+ padding: 32px 32px 32px 40px;
1108
+ }
1109
+ .detail-inspector {
1110
+ position: sticky;
1111
+ top: 52px;
1112
+ align-self: start;
1113
+ height: calc(100vh - 52px);
1114
+ overflow-y: auto;
1115
+ border-left: 1px solid var(--color-border);
1116
+ padding: 16px 24px 32px;
1117
+ background: var(--color-bg-subtle);
1118
+ }
1119
+ .detail-inspector .section { padding: 20px 0; }
1120
+ .detail-inspector .section-header { margin-bottom: 12px; }
1121
+ .detail-inspector .section-header h2 { font-size: 15px; }
1122
+ .detail-inspector .section-header p { font-size: 12px; }
1123
+ .detail-inspector .stats-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; }
1124
+ .detail-inspector .stat-card { padding: 10px; }
1125
+ .detail-inspector .stat-card .stat-value { font-size: 18px; }
1126
+ .detail-inspector .analysis-grid { grid-template-columns: 1fr; gap: 8px; }
1127
+ .detail-inspector .composition-lists { grid-template-columns: 1fr; gap: 8px; }
1128
+
1129
+ @media (max-width: 1200px) {
1130
+ .detail-columns { grid-template-columns: 1fr; }
1131
+ .detail-main { padding: 20px 16px; }
1132
+ .detail-inspector {
1133
+ position: static;
1134
+ height: auto;
1135
+ border-left: none;
1136
+ border-top: 1px solid var(--color-border);
1137
+ padding: 20px 16px;
1138
+ background: none;
1139
+ }
1140
+ }
1141
+
1142
+ /* Icon browser */
1143
+ .icon-browser-header { margin-bottom: 24px; }
1144
+ .icon-browser-header h1 { font-size: 28px; font-weight: 700; margin: 0 0 4px; }
1145
+ .icon-browser-header p { color: var(--color-muted); font-size: 14px; margin: 0; }
1146
+
1147
+ .icon-grid {
1148
+ display: grid;
1149
+ grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
1150
+ gap: 2px;
1151
+ }
1152
+
1153
+ .icon-cell {
1154
+ display: flex;
1155
+ flex-direction: column;
1156
+ align-items: center;
1157
+ justify-content: center;
1158
+ gap: 6px;
1159
+ padding: 12px 4px 8px;
1160
+ border: 1px solid transparent;
1161
+ border-radius: var(--radius);
1162
+ cursor: pointer;
1163
+ background: none;
1164
+ font-family: var(--font-body);
1165
+ transition: background 0.1s, border-color 0.1s;
1166
+ }
1167
+ .icon-cell:hover {
1168
+ background: var(--color-bg-subtle);
1169
+ border-color: var(--color-border);
1170
+ }
1171
+ .icon-cell.selected {
1172
+ background: var(--color-bg-selected);
1173
+ border-color: var(--color-accent);
1174
+ }
1175
+
1176
+ .icon-cell-preview {
1177
+ width: 40px;
1178
+ height: 40px;
1179
+ display: flex;
1180
+ align-items: center;
1181
+ justify-content: center;
1182
+ overflow: hidden;
1183
+ }
1184
+ .icon-cell-svg { display: flex; align-items: center; justify-content: center; }
1185
+ .icon-cell-svg svg { width: 28px; height: 28px; }
1186
+ .icon-no-preview {
1187
+ font-size: 16px;
1188
+ font-weight: 600;
1189
+ color: var(--color-muted);
1190
+ }
1191
+
1192
+ .icon-cell-name {
1193
+ font-size: 10px;
1194
+ color: var(--color-muted);
1195
+ text-align: center;
1196
+ line-height: 1.2;
1197
+ max-width: 100%;
1198
+ overflow: hidden;
1199
+ text-overflow: ellipsis;
1200
+ white-space: nowrap;
1201
+ }
1202
+
1203
+ /* Icon slide-out panel */
1204
+ .icon-slideout-overlay {
1205
+ position: fixed;
1206
+ inset: 0;
1207
+ background: var(--overlay-bg);
1208
+ z-index: 200;
1209
+ }
1210
+ .icon-slideout {
1211
+ position: fixed;
1212
+ top: 0;
1213
+ right: 0;
1214
+ width: 380px;
1215
+ max-width: 100vw;
1216
+ height: 100vh;
1217
+ background: var(--color-bg);
1218
+ border-left: 1px solid var(--color-border);
1219
+ box-shadow: var(--slideout-shadow);
1220
+ z-index: 201;
1221
+ display: flex;
1222
+ flex-direction: column;
1223
+ overflow-y: auto;
1224
+ animation: icon-slide-in 0.2s ease-out;
1225
+ }
1226
+ .icon-slideout[hidden] { display: none; }
1227
+ .icon-slideout-overlay[hidden] { display: none; }
1228
+ @keyframes icon-slide-in {
1229
+ from { transform: translateX(100%); }
1230
+ to { transform: translateX(0); }
1231
+ }
1232
+
1233
+ .icon-slideout-header {
1234
+ display: flex;
1235
+ align-items: center;
1236
+ justify-content: space-between;
1237
+ padding: 20px 24px 16px;
1238
+ border-bottom: 1px solid var(--color-border);
1239
+ flex-shrink: 0;
1240
+ }
1241
+ .icon-slideout-name {
1242
+ font-size: 18px;
1243
+ font-weight: 600;
1244
+ margin: 0;
1245
+ word-break: break-word;
1246
+ }
1247
+ .icon-slideout-close {
1248
+ background: none;
1249
+ border: none;
1250
+ color: var(--color-muted);
1251
+ cursor: pointer;
1252
+ padding: 4px;
1253
+ border-radius: var(--radius);
1254
+ display: flex;
1255
+ align-items: center;
1256
+ justify-content: center;
1257
+ transition: background 0.1s, color 0.1s;
1258
+ }
1259
+ .icon-slideout-close:hover {
1260
+ background: var(--color-bg-subtle);
1261
+ color: var(--color-text);
1262
+ }
1263
+
1264
+ .icon-slideout-body {
1265
+ flex: 1;
1266
+ padding: 0 24px 24px;
1267
+ overflow-y: auto;
1268
+ }
1269
+
1270
+ .icon-slideout-preview-lg {
1271
+ display: flex;
1272
+ align-items: center;
1273
+ justify-content: center;
1274
+ padding: 40px 32px;
1275
+ background: var(--color-bg-subtle);
1276
+ border: 1px solid var(--color-border);
1277
+ border-radius: var(--radius);
1278
+ margin: 20px 0;
1279
+ }
1280
+ .icon-slideout-preview-lg svg {
1281
+ width: 100%;
1282
+ max-width: 120px;
1283
+ height: auto;
1284
+ }
1285
+
1286
+ .icon-detail-section {
1287
+ padding: 16px 0;
1288
+ border-bottom: 1px solid var(--color-border);
1289
+ }
1290
+ .icon-detail-section:last-child { border-bottom: none; }
1291
+ .icon-detail-section-title {
1292
+ font-size: 11px;
1293
+ font-weight: 600;
1294
+ letter-spacing: 0.06em;
1295
+ text-transform: uppercase;
1296
+ color: var(--color-muted);
1297
+ margin-bottom: 10px;
1298
+ }
1299
+
1300
+ /* Size previews */
1301
+ .icon-size-row {
1302
+ display: flex;
1303
+ gap: 16px;
1304
+ align-items: flex-end;
1305
+ }
1306
+ .icon-size-cell {
1307
+ display: flex;
1308
+ flex-direction: column;
1309
+ align-items: center;
1310
+ gap: 6px;
1311
+ }
1312
+ .icon-size-cell span {
1313
+ font-size: 10px;
1314
+ color: var(--color-muted);
1315
+ font-weight: 500;
1316
+ }
1317
+ .icon-size-preview {
1318
+ display: flex;
1319
+ align-items: center;
1320
+ justify-content: center;
1321
+ padding: 8px;
1322
+ background: var(--color-bg-subtle);
1323
+ border: 1px solid var(--color-border);
1324
+ border-radius: var(--radius);
1325
+ }
1326
+
1327
+ /* Action buttons */
1328
+ .icon-slideout-actions {
1329
+ display: flex;
1330
+ flex-wrap: wrap;
1331
+ gap: 8px;
1332
+ }
1333
+ .icon-action-btn {
1334
+ display: inline-flex;
1335
+ align-items: center;
1336
+ justify-content: center;
1337
+ padding: 7px 14px;
1338
+ border-radius: var(--radius);
1339
+ border: 1px solid var(--color-border);
1340
+ background: var(--color-bg);
1341
+ color: var(--color-text);
1342
+ text-decoration: none;
1343
+ font-family: var(--font-body);
1344
+ font-size: 12px;
1345
+ font-weight: 500;
1346
+ cursor: pointer;
1347
+ transition: background 0.1s, border-color 0.1s;
1348
+ }
1349
+ .icon-action-btn:hover {
1350
+ background: var(--color-bg-subtle);
1351
+ border-color: var(--color-muted);
1352
+ }
1353
+ .icon-action-primary {
1354
+ background: var(--color-accent);
1355
+ border-color: var(--color-accent);
1356
+ color: var(--color-on-accent);
1357
+ }
1358
+ .icon-action-primary:hover {
1359
+ opacity: 0.9;
1360
+ background: var(--color-accent);
1361
+ border-color: var(--color-accent);
1362
+ }
1363
+
1364
+ /* Compiled size */
1365
+ .icon-compiled-size {
1366
+ font-family: var(--font-mono);
1367
+ font-size: 14px;
1368
+ font-weight: 600;
1369
+ }
1370
+
1371
+ /* Props */
1372
+ .icon-prop-row {
1373
+ display: flex;
1374
+ flex-wrap: wrap;
1375
+ gap: 4px 12px;
1376
+ align-items: baseline;
1377
+ padding: 6px 0;
1378
+ border-bottom: 1px solid var(--color-border);
1379
+ }
1380
+ .icon-prop-row:last-child { border-bottom: none; }
1381
+ .icon-prop-name {
1382
+ font-family: var(--font-mono);
1383
+ font-size: 12px;
1384
+ font-weight: 600;
1385
+ }
1386
+ .icon-prop-type {
1387
+ font-family: var(--font-mono);
1388
+ font-size: 11px;
1389
+ color: var(--color-accent);
1390
+ }
1391
+ .icon-prop-meta {
1392
+ font-size: 11px;
1393
+ color: var(--color-muted);
1394
+ }
1395
+ .icon-prop-required { color: var(--color-error); }
1396
+ .icon-prop-optional { color: var(--color-muted); }
1397
+ .icon-prop-default {
1398
+ font-family: var(--font-mono);
1399
+ font-size: 10px;
1400
+ color: var(--color-muted);
1401
+ margin-left: 4px;
1402
+ }
1403
+
1404
+ /* Keywords */
1405
+ .icon-keywords {
1406
+ display: flex;
1407
+ flex-wrap: wrap;
1408
+ gap: 4px;
1409
+ }
1410
+ .icon-keyword-tag {
1411
+ display: inline-block;
1412
+ padding: 2px 8px;
1413
+ background: var(--color-bg-subtle);
1414
+ border: 1px solid var(--color-border);
1415
+ border-radius: 999px;
1416
+ font-size: 11px;
1417
+ color: var(--color-muted);
1418
+ }
1419
+
1420
+ /* Usage list */
1421
+ .icon-usage-list {
1422
+ display: flex;
1423
+ flex-direction: column;
1424
+ gap: 4px;
1425
+ }
1426
+ .icon-usage-link {
1427
+ display: block;
1428
+ padding: 4px 0;
1429
+ font-size: 13px;
1430
+ color: var(--color-accent);
1431
+ text-decoration: none;
1432
+ }
1433
+ .icon-usage-link:hover { text-decoration: underline; }
1434
+
1435
+ @media (max-width: 768px) {
1436
+ .icon-slideout { width: 100vw; }
1437
+ }
1438
+
1439
+ /* CMD-K trigger button */
1440
+ .cmdk-trigger {
1441
+ display: inline-flex;
1442
+ align-items: center;
1443
+ gap: 8px;
1444
+ border: 1px solid var(--color-border);
1445
+ border-radius: var(--radius);
1446
+ padding: 6px 10px 6px 12px;
1447
+ font-family: var(--font-body);
1448
+ font-size: 13px;
1449
+ color: var(--color-muted);
1450
+ background: var(--color-bg-subtle);
1451
+ cursor: pointer;
1452
+ transition: border-color 0.15s, color 0.15s;
1453
+ height: 34px;
1454
+ min-width: 200px;
1455
+ }
1456
+ .cmdk-trigger:hover {
1457
+ border-color: var(--color-muted);
1458
+ color: var(--color-text);
1459
+ }
1460
+ .cmdk-trigger svg { flex-shrink: 0; opacity: 0.5; }
1461
+ .cmdk-trigger span { flex: 1; text-align: left; }
1462
+ .cmdk-trigger kbd {
1463
+ font-family: var(--font-body);
1464
+ font-size: 10px;
1465
+ font-weight: 500;
1466
+ padding: 2px 5px;
1467
+ border: 1px solid var(--color-border);
1468
+ border-radius: 4px;
1469
+ background: var(--color-bg);
1470
+ color: var(--color-muted);
1471
+ line-height: 1;
1472
+ flex-shrink: 0;
1473
+ }
1474
+
1475
+ /* CMD-K overlay */
1476
+ .cmdk-overlay {
1477
+ position: fixed;
1478
+ inset: 0;
1479
+ background: var(--overlay-bg);
1480
+ z-index: 300;
1481
+ animation: cmdk-fade-in 0.1s ease-out;
1482
+ }
1483
+ .cmdk-overlay[hidden] { display: none; }
1484
+
1485
+ @keyframes cmdk-fade-in {
1486
+ from { opacity: 0; }
1487
+ to { opacity: 1; }
1488
+ }
1489
+
1490
+ /* CMD-K dialog */
1491
+ .cmdk-dialog {
1492
+ position: fixed;
1493
+ top: min(20vh, 140px);
1494
+ left: 50%;
1495
+ transform: translateX(-50%);
1496
+ width: 520px;
1497
+ max-width: calc(100vw - 32px);
1498
+ background: var(--color-bg);
1499
+ border: 1px solid var(--color-border);
1500
+ border-radius: 12px;
1501
+ box-shadow: 0 16px 70px rgba(0,0,0,0.2);
1502
+ z-index: 301;
1503
+ display: flex;
1504
+ flex-direction: column;
1505
+ overflow: hidden;
1506
+ animation: cmdk-dialog-in 0.15s ease-out;
1507
+ }
1508
+ .cmdk-dialog[hidden] { display: none; }
1509
+
1510
+ @keyframes cmdk-dialog-in {
1511
+ from { opacity: 0; transform: translateX(-50%) scale(0.96); }
1512
+ to { opacity: 1; transform: translateX(-50%) scale(1); }
1513
+ }
1514
+
1515
+ /* CMD-K input */
1516
+ .cmdk-input-wrapper {
1517
+ display: flex;
1518
+ align-items: center;
1519
+ gap: 10px;
1520
+ padding: 14px 16px;
1521
+ border-bottom: 1px solid var(--color-border);
1522
+ }
1523
+ .cmdk-input-wrapper svg { flex-shrink: 0; color: var(--color-muted); }
1524
+ .cmdk-input {
1525
+ flex: 1;
1526
+ border: none;
1527
+ outline: none;
1528
+ background: none;
1529
+ font-family: var(--font-body);
1530
+ font-size: 15px;
1531
+ color: var(--color-text);
1532
+ padding: 0;
1533
+ }
1534
+ .cmdk-input::placeholder { color: var(--color-muted); }
1535
+
1536
+ /* CMD-K results list */
1537
+ .cmdk-list {
1538
+ max-height: 340px;
1539
+ overflow-y: auto;
1540
+ padding: 6px;
1541
+ }
1542
+ .cmdk-list:empty { display: none; }
1543
+
1544
+ .cmdk-item {
1545
+ display: flex;
1546
+ align-items: center;
1547
+ gap: 10px;
1548
+ padding: 8px 10px;
1549
+ border-radius: var(--radius);
1550
+ text-decoration: none;
1551
+ color: var(--color-text);
1552
+ cursor: pointer;
1553
+ transition: background 0.06s;
1554
+ }
1555
+ .cmdk-item-active {
1556
+ background: var(--color-bg-selected);
1557
+ }
1558
+ .cmdk-item-icon {
1559
+ display: flex;
1560
+ align-items: center;
1561
+ justify-content: center;
1562
+ width: 28px;
1563
+ height: 28px;
1564
+ border-radius: var(--radius);
1565
+ background: var(--color-bg-subtle);
1566
+ border: 1px solid var(--color-border);
1567
+ color: var(--color-muted);
1568
+ flex-shrink: 0;
1569
+ }
1570
+ .cmdk-item-name {
1571
+ flex: 1;
1572
+ font-size: 13px;
1573
+ font-weight: 500;
1574
+ min-width: 0;
1575
+ overflow: hidden;
1576
+ text-overflow: ellipsis;
1577
+ white-space: nowrap;
1578
+ }
1579
+ .cmdk-item-collection {
1580
+ font-size: 11px;
1581
+ color: var(--color-muted);
1582
+ flex-shrink: 0;
1583
+ }
1584
+ .cmdk-item-type {
1585
+ font-size: 10px;
1586
+ font-weight: 500;
1587
+ text-transform: uppercase;
1588
+ letter-spacing: 0.04em;
1589
+ color: var(--color-muted);
1590
+ opacity: 0.7;
1591
+ flex-shrink: 0;
1592
+ }
1593
+
1594
+ /* CMD-K empty state */
1595
+ .cmdk-empty {
1596
+ padding: 32px 16px;
1597
+ text-align: center;
1598
+ color: var(--color-muted);
1599
+ font-size: 13px;
1600
+ }
1601
+ .cmdk-empty[hidden] { display: none; }
1602
+
1603
+ /* CMD-K footer */
1604
+ .cmdk-footer {
1605
+ display: flex;
1606
+ align-items: center;
1607
+ gap: 16px;
1608
+ padding: 8px 16px;
1609
+ border-top: 1px solid var(--color-border);
1610
+ font-size: 11px;
1611
+ color: var(--color-muted);
1612
+ }
1613
+ .cmdk-footer kbd {
1614
+ font-family: var(--font-body);
1615
+ font-size: 10px;
1616
+ font-weight: 500;
1617
+ padding: 1px 4px;
1618
+ border: 1px solid var(--color-border);
1619
+ border-radius: 3px;
1620
+ background: var(--color-bg-subtle);
1621
+ margin-right: 2px;
1622
+ }
1623
+
1624
+ @media (max-width: 768px) {
1625
+ .cmdk-trigger { min-width: 0; }
1626
+ .cmdk-trigger span { display: none; }
1627
+ .cmdk-dialog { top: 16px; }
1628
+ }
1629
+
1630
+ /* Token browser */
1631
+ .tb-header { margin-bottom: 24px; }
1632
+ .tb-header h1 { font-size: 28px; font-weight: 700; margin: 0 0 6px; }
1633
+ .tb-header p { color: var(--color-muted); font-size: 14px; margin: 0; }
1634
+ .tb-updated { font-size: 12px; color: var(--color-muted); }
1635
+
1636
+ .tb-section {
1637
+ padding: 28px 0;
1638
+ border-bottom: 1px solid var(--color-border);
1639
+ }
1640
+ .tb-section:last-child { border-bottom: none; }
1641
+ .tb-section-title {
1642
+ font-size: 18px;
1643
+ font-weight: 600;
1644
+ margin: 0 0 16px;
1645
+ text-transform: capitalize;
1646
+ }
1647
+
1648
+ /* Color scales */
1649
+ .tb-scale { margin-bottom: 20px; }
1650
+ .tb-scale-name {
1651
+ font-size: 12px;
1652
+ font-weight: 600;
1653
+ text-transform: capitalize;
1654
+ color: var(--color-muted);
1655
+ margin-bottom: 6px;
1656
+ letter-spacing: 0.03em;
1657
+ }
1658
+ .tb-scale-row {
1659
+ display: flex;
1660
+ gap: 0;
1661
+ border-radius: var(--radius);
1662
+ overflow: hidden;
1663
+ border: 1px solid var(--color-border);
1664
+ }
1665
+ .tb-swatch {
1666
+ flex: 1;
1667
+ min-width: 0;
1668
+ padding: 24px 6px 8px;
1669
+ display: flex;
1670
+ flex-direction: column;
1671
+ align-items: center;
1672
+ gap: 2px;
1673
+ cursor: default;
1674
+ transition: transform 0.1s, box-shadow 0.1s;
1675
+ position: relative;
1676
+ }
1677
+ .tb-swatch:hover {
1678
+ transform: scaleY(1.08);
1679
+ z-index: 1;
1680
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1681
+ }
1682
+ .tb-swatch-label {
1683
+ font-size: 10px;
1684
+ font-weight: 600;
1685
+ opacity: 0.85;
1686
+ }
1687
+ .tb-swatch-value {
1688
+ font-size: 9px;
1689
+ font-family: var(--font-mono);
1690
+ opacity: 0.7;
1691
+ white-space: nowrap;
1692
+ overflow: hidden;
1693
+ text-overflow: ellipsis;
1694
+ max-width: 100%;
1695
+ }
1696
+
1697
+ /* Swatch pair (light + dark) */
1698
+ .tb-swatch-pair {
1699
+ flex: 1;
1700
+ min-width: 0;
1701
+ display: flex;
1702
+ flex-direction: column;
1703
+ }
1704
+ .tb-swatch-pair .tb-swatch {
1705
+ border-radius: 0;
1706
+ border: none;
1707
+ }
1708
+ .tb-swatch-dark {
1709
+ padding: 4px 6px;
1710
+ display: flex;
1711
+ align-items: center;
1712
+ justify-content: center;
1713
+ min-height: 24px;
1714
+ }
1715
+ .tb-swatch-dark .tb-swatch-value {
1716
+ font-size: 8px;
1717
+ font-family: var(--font-mono);
1718
+ opacity: 0.8;
1719
+ }
1720
+
1721
+ .tb-standalone-colors {
1722
+ display: flex;
1723
+ flex-wrap: wrap;
1724
+ gap: 8px;
1725
+ margin-top: 12px;
1726
+ }
1727
+ .tb-swatch-pair-single {
1728
+ display: flex;
1729
+ flex-direction: column;
1730
+ width: 80px;
1731
+ border-radius: var(--radius);
1732
+ overflow: hidden;
1733
+ border: 1px solid var(--color-border);
1734
+ }
1735
+ .tb-swatch-pair-single .tb-swatch-single {
1736
+ border: none;
1737
+ border-radius: 0;
1738
+ width: auto;
1739
+ min-width: auto;
1740
+ flex: none;
1741
+ }
1742
+ .tb-swatch-dark-single {
1743
+ padding: 4px 6px;
1744
+ display: flex;
1745
+ align-items: center;
1746
+ justify-content: center;
1747
+ min-height: 22px;
1748
+ }
1749
+ .tb-swatch-dark-single .tb-swatch-value {
1750
+ font-size: 8px;
1751
+ font-family: var(--font-mono);
1752
+ opacity: 0.8;
1753
+ }
1754
+ .tb-swatch-single {
1755
+ width: 80px;
1756
+ min-width: 80px;
1757
+ flex: 0 0 80px;
1758
+ height: 72px;
1759
+ border-radius: var(--radius);
1760
+ border: 1px solid var(--color-border);
1761
+ justify-content: flex-end;
1762
+ }
1763
+
1764
+ /* Scale badge */
1765
+ .tb-scale-badge {
1766
+ display: inline-block;
1767
+ font-size: 9px;
1768
+ font-weight: 500;
1769
+ text-transform: uppercase;
1770
+ letter-spacing: 0.06em;
1771
+ padding: 1px 5px;
1772
+ border-radius: 3px;
1773
+ background: var(--color-bg-code);
1774
+ color: var(--color-code-text);
1775
+ margin-left: 6px;
1776
+ vertical-align: middle;
1777
+ }
1778
+
1779
+ /* Spacing */
1780
+ .tb-spacing-row {
1781
+ display: grid;
1782
+ grid-template-columns: 140px 80px 1fr;
1783
+ align-items: center;
1784
+ gap: 12px;
1785
+ padding: 8px 0;
1786
+ border-bottom: 1px solid var(--color-border);
1787
+ }
1788
+ .tb-spacing-row:last-child { border-bottom: none; }
1789
+ .tb-spacing-label {
1790
+ font-family: var(--font-mono);
1791
+ font-size: 12px;
1792
+ font-weight: 500;
1793
+ color: var(--color-text);
1794
+ }
1795
+ .tb-spacing-value {
1796
+ font-family: var(--font-mono);
1797
+ font-size: 12px;
1798
+ color: var(--color-muted);
1799
+ }
1800
+ .tb-spacing-bar-wrapper {
1801
+ height: 20px;
1802
+ background: var(--color-bg-subtle);
1803
+ border-radius: 3px;
1804
+ overflow: hidden;
1805
+ }
1806
+ .tb-spacing-bar {
1807
+ height: 100%;
1808
+ background: var(--color-accent);
1809
+ border-radius: 3px;
1810
+ opacity: 0.5;
1811
+ min-width: 2px;
1812
+ }
1813
+
1814
+ /* Typography */
1815
+ .tb-typo-row {
1816
+ display: grid;
1817
+ grid-template-columns: 160px 1fr auto;
1818
+ align-items: baseline;
1819
+ gap: 16px;
1820
+ padding: 10px 0;
1821
+ border-bottom: 1px solid var(--color-border);
1822
+ }
1823
+ .tb-typo-row:last-child { border-bottom: none; }
1824
+ .tb-typo-label {
1825
+ font-family: var(--font-mono);
1826
+ font-size: 12px;
1827
+ font-weight: 500;
1828
+ }
1829
+ .tb-typo-value {
1830
+ font-family: var(--font-mono);
1831
+ font-size: 12px;
1832
+ color: var(--color-muted);
1833
+ }
1834
+ .tb-typo-sample {
1835
+ font-size: 18px;
1836
+ color: var(--color-text);
1837
+ }
1838
+
1839
+ /* Generic table */
1840
+ .tb-generic-table { width: 100%; border-collapse: collapse; font-size: 13px; }
1841
+ .tb-generic-table th {
1842
+ text-align: left;
1843
+ font-weight: 600;
1844
+ font-size: 11px;
1845
+ text-transform: uppercase;
1846
+ letter-spacing: 0.05em;
1847
+ color: var(--color-muted);
1848
+ padding: 8px 12px;
1849
+ border-bottom: 2px solid var(--color-border);
1850
+ }
1851
+ .tb-generic-table td {
1852
+ padding: 8px 12px;
1853
+ border-bottom: 1px solid var(--color-border);
1854
+ vertical-align: middle;
1855
+ }
1856
+ .tb-generic-table tr:last-child td { border-bottom: none; }
1857
+ .tb-generic-path {
1858
+ font-family: var(--font-mono);
1859
+ font-size: 12px;
1860
+ color: var(--color-accent);
1861
+ }
1862
+ .tb-generic-value {
1863
+ font-family: var(--font-mono);
1864
+ font-size: 12px;
1865
+ }
1866
+ .tb-generic-type {
1867
+ font-size: 11px;
1868
+ color: var(--color-muted);
1869
+ text-transform: capitalize;
1870
+ }
1871
+
1872
+ @media (max-width: 768px) {
1873
+ .tb-scale-row { flex-wrap: wrap; }
1874
+ .tb-swatch { min-width: 48px; }
1875
+ .tb-spacing-row { grid-template-columns: 1fr 1fr; }
1876
+ .tb-spacing-bar-wrapper { display: none; }
1877
+ .tb-typo-row { grid-template-columns: 1fr 1fr; }
1878
+ }`;
1879
+ return `<style>
1880
+ ${css}
1881
+ </style>`;
1882
+ }
1883
+
1884
+ // src/templates/layout.ts
1885
+ function buildSearchItems(data) {
1886
+ const items = [];
1887
+ const basePath = data.options.basePath;
1888
+ const iconPatterns = data.options.iconPatterns;
1889
+ for (const [name, component] of Object.entries(data.manifest.components)) {
1890
+ const isIcon = iconPatterns.length > 0 && iconPatterns.some(
1891
+ (p) => matchGlob(p, component.filePath) || matchGlob(p, component.displayName)
1892
+ );
1893
+ if (component.internal && !isIcon) continue;
1894
+ items.push({
1895
+ name,
1896
+ type: isIcon ? "icon" : "component",
1897
+ url: `${basePath}${slugify(name)}.html`,
1898
+ collection: component.collection,
1899
+ keywords: (component.keywords ?? []).join(" ")
1900
+ });
1901
+ }
1902
+ return items.sort((a, b) => a.name.localeCompare(b.name));
1903
+ }
1904
+ function sidebarLinks(data, currentSlug) {
1905
+ const { components } = data.manifest;
1906
+ const collections = data.manifest.collections ?? [];
1907
+ const basePath = data.options.basePath;
1908
+ const hasIcons = data.options.iconPatterns.length > 0;
1909
+ const visibleEntries = Object.entries(components).filter(([, c]) => !c.internal);
1910
+ const collectionMap = /* @__PURE__ */ new Map();
1911
+ for (const col of collections) {
1912
+ collectionMap.set(col.name, []);
1913
+ }
1914
+ const ungrouped = [];
1915
+ for (const [name, component] of visibleEntries) {
1916
+ if (component.collection) {
1917
+ if (!collectionMap.has(component.collection)) {
1918
+ collectionMap.set(component.collection, []);
1919
+ }
1920
+ collectionMap.get(component.collection)?.push(name);
1921
+ } else {
1922
+ ungrouped.push(name);
1923
+ }
1924
+ }
1925
+ const hasAnyCollections = collectionMap.size > 0 || ungrouped.length < visibleEntries.length;
1926
+ function renderLinks(names) {
1927
+ return names.sort().map((name) => {
1928
+ const slug = slugify(name);
1929
+ const isActive = slug === currentSlug;
1930
+ return `<a href="${basePath}${slug}.html" class="${isActive ? "active" : ""}">${escapeHtml(name)}</a>`;
1931
+ }).join("\n");
1932
+ }
1933
+ const hasTokens = (data.tokenEntries?.length ?? 0) > 0;
1934
+ const overviewLink = `<a href="${basePath}index.html" class="${currentSlug === null ? "active" : ""}">Overview</a>`;
1935
+ const tokensLink = hasTokens ? `<a href="${basePath}tokens.html" class="${currentSlug === "tokens" ? "active" : ""}">Tokens</a>` : "";
1936
+ const iconsLink = hasIcons ? `<a href="${basePath}icons.html" class="${currentSlug === "icons" ? "active" : ""}">Icons</a>` : "";
1937
+ if (!hasAnyCollections) {
1938
+ const links = renderLinks(visibleEntries.map(([name]) => name));
1939
+ return `${overviewLink}
1940
+ ${tokensLink}
1941
+ ${iconsLink}
1942
+ <div class="sidebar-heading">Components</div>
1943
+ ${links}`;
1944
+ }
1945
+ const sections = [overviewLink];
1946
+ if (tokensLink) sections.push(tokensLink);
1947
+ if (iconsLink) sections.push(iconsLink);
1948
+ for (const [colName, names] of collectionMap) {
1949
+ if (names.length === 0) continue;
1950
+ sections.push(
1951
+ `<div class="sidebar-collection-header">${escapeHtml(colName)}</div>
1952
+ ${renderLinks(names)}`
1953
+ );
1954
+ }
1955
+ if (ungrouped.length > 0) {
1956
+ sections.push(
1957
+ `<div class="sidebar-collection-header">Ungrouped</div>
1958
+ ${renderLinks(ungrouped)}`
1959
+ );
1960
+ }
1961
+ return sections.join("\n");
1962
+ }
1963
+ function htmlShell(options) {
1964
+ const {
1965
+ title,
1966
+ body,
1967
+ sidebar,
1968
+ onThisPage,
1969
+ basePath,
1970
+ searchItems = [],
1971
+ breadcrumbs = []
1972
+ } = options;
1973
+ const searchItemsJson = JSON.stringify(searchItems).replace(/<\/script>/gi, "<\\/script>");
1974
+ const logoSvg = `<svg class="site-logo" width="22" height="22" viewBox="0 0 1200 1200" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="m627.03 392.79c93.695 14.414 169.37 86.488 180.18 183.79h90.09c18.02 0 18.02 50.449 0 50.449h-90.09c-10.812 93.695-86.488 169.37-180.18 180.18v90.09c0 18.02-50.449 18.02-50.449 0v-90.09c-97.297-10.812-169.37-86.488-183.79-180.18h-86.488c-18.02 0-18.02-50.449 0-50.449h86.488c14.414-97.297 86.488-169.37 183.79-183.79v-86.488c0-18.02 50.449-21.621 50.449 0zm486.49-313.51h-201.8c-18.02 0-18.02-50.449 0-50.449h237.84c7.207 0 14.414 7.207 14.414 14.414v237.84c0 18.02-50.449 18.02-50.449 0zm7.207 1034.2v-201.8c0-18.02 50.449-18.02 50.449 0v237.84c0 7.207-3.6055 14.414-10.812 14.414h-241.44c-18.02 0-18.02-50.449 0-50.449zm-1030.6 7.207h198.2c21.621 0 21.621 50.449 0 50.449h-237.84c-7.207 0-10.812-3.6055-10.812-10.812v-241.44c0-18.02 50.449-18.02 50.449 0zm-10.812-1030.6v198.2c0 21.621-50.449 21.621-50.449 0v-237.84c0-7.207 7.207-10.812 14.414-10.812h237.84c18.02 0 18.02 50.449 0 50.449zm547.75 356.76v129.73h129.73c-10.812-68.469-64.863-118.92-129.73-129.73zm129.73 180.18h-129.73v129.73c64.863-10.812 118.92-64.863 129.73-129.73zm-180.18 129.73v-129.73h-129.73c10.812 64.863 61.262 118.92 129.73 129.73zm-129.73-180.18h129.73v-129.73c-68.469 10.812-118.92 61.262-129.73 129.73z" fill-rule="evenodd"/></svg>`;
1975
+ const searchSvg = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
1976
+ const compSvg = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>`;
1977
+ const iconSvg = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`;
1978
+ return `<!DOCTYPE html>
1979
+ <html lang="en">
1980
+ <head>
1981
+ <meta charset="UTF-8" />
1982
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1983
+ <script>(function(){var t=localStorage.getItem('scope-theme');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t)}else if(window.matchMedia&&window.matchMedia('(prefers-color-scheme:dark)').matches){document.documentElement.setAttribute('data-theme','dark')}else{document.documentElement.setAttribute('data-theme','light')}})();</script>
1984
+ <title>${escapeHtml(title)}</title>
1985
+ ${generateCSS()}
1986
+ </head>
1987
+ <body>
1988
+ <nav class="top-nav">
1989
+ <a class="site-title" href="${basePath}index.html">${logoSvg}<span>Scope</span></a>${breadcrumbs.length > 0 ? `<nav class="breadcrumbs" aria-label="Breadcrumb">${breadcrumbs.map((b) => b.url ? `<a href="${b.url}">${escapeHtml(b.label)}</a>` : `<span>${escapeHtml(b.label)}</span>`).join("")}</nav>` : ""}
1990
+ <div class="spacer"></div>
1991
+ <button class="cmdk-trigger" id="cmdk-trigger" type="button">
1992
+ ${searchSvg}
1993
+ <span>Search\u2026</span>
1994
+ <kbd>\u2318K</kbd>
1995
+ </button>
1996
+ <button class="theme-toggle" id="theme-toggle" type="button" aria-label="Toggle theme" title="Toggle theme"></button>
1997
+ </nav>
1998
+
1999
+ <div class="cmdk-overlay" id="cmdk-overlay" hidden></div>
2000
+ <div class="cmdk-dialog" id="cmdk-dialog" hidden role="dialog" aria-label="Search components">
2001
+ <div class="cmdk-input-wrapper">
2002
+ ${searchSvg}
2003
+ <input class="cmdk-input" id="cmdk-input" placeholder="Jump to component or icon\u2026" autocomplete="off" spellcheck="false" />
2004
+ </div>
2005
+ <div class="cmdk-list" id="cmdk-list"></div>
2006
+ <div class="cmdk-empty" id="cmdk-empty" hidden>No results found.</div>
2007
+ <div class="cmdk-footer">
2008
+ <span><kbd>\u2191\u2193</kbd> navigate</span>
2009
+ <span><kbd>\u21B5</kbd> open</span>
2010
+ <span><kbd>esc</kbd> close</span>
2011
+ </div>
2012
+ </div>
2013
+
2014
+ <div class="page-layout">
2015
+ <nav class="sidebar" id="sidebar">
2016
+ ${sidebar}
2017
+ </nav>
2018
+ <div class="main-content">
2019
+ <div class="content-body${onThisPage ? "" : " content-body-full"}">
2020
+ ${body}
2021
+ </div>
2022
+ ${onThisPage ? `<nav class="on-this-page">
2023
+ <h4>On this page</h4>
2024
+ ${onThisPage}
2025
+ </nav>` : ""}
2026
+ </div>
2027
+ </div>
2028
+ <script>
2029
+ (function () {
2030
+ var ITEMS = ${searchItemsJson};
2031
+ var overlay = document.getElementById('cmdk-overlay');
2032
+ var dialog = document.getElementById('cmdk-dialog');
2033
+ var input = document.getElementById('cmdk-input');
2034
+ var list = document.getElementById('cmdk-list');
2035
+ var trigger = document.getElementById('cmdk-trigger');
2036
+ var emptyEl = document.getElementById('cmdk-empty');
2037
+ if (!overlay || !dialog || !input || !list || !trigger) return;
2038
+
2039
+ var activeIdx = 0;
2040
+ var filtered = ITEMS.slice();
2041
+ var compIcon = ${JSON.stringify(compSvg)};
2042
+ var icoIcon = ${JSON.stringify(iconSvg)};
2043
+
2044
+ function render() {
2045
+ if (filtered.length === 0) {
2046
+ list.innerHTML = '';
2047
+ list.style.display = 'none';
2048
+ if (emptyEl) emptyEl.hidden = false;
2049
+ return;
2050
+ }
2051
+ if (emptyEl) emptyEl.hidden = true;
2052
+ list.style.display = '';
2053
+ list.innerHTML = filtered.map(function (item, i) {
2054
+ var cls = 'cmdk-item' + (i === activeIdx ? ' cmdk-item-active' : '');
2055
+ var icon = item.type === 'icon' ? icoIcon : compIcon;
2056
+ var col = item.collection ? '<span class="cmdk-item-collection">' + item.collection + '</span>' : '';
2057
+ return '<a class="' + cls + '" href="' + item.url + '" data-idx="' + i + '">' +
2058
+ '<span class="cmdk-item-icon">' + icon + '</span>' +
2059
+ '<span class="cmdk-item-name">' + item.name + '</span>' +
2060
+ col +
2061
+ '<span class="cmdk-item-type">' + item.type + '</span>' +
2062
+ '</a>';
2063
+ }).join('');
2064
+ }
2065
+
2066
+ function filter(q) {
2067
+ if (!q) {
2068
+ filtered = ITEMS.slice();
2069
+ } else {
2070
+ filtered = ITEMS.filter(function (item) {
2071
+ var haystack = item.name.toLowerCase();
2072
+ if (item.collection) haystack += ' ' + item.collection.toLowerCase();
2073
+ if (item.keywords) haystack += ' ' + item.keywords.toLowerCase();
2074
+ return haystack.includes(q);
2075
+ });
2076
+ }
2077
+ activeIdx = 0;
2078
+ render();
2079
+ }
2080
+
2081
+ function open() {
2082
+ overlay.hidden = false;
2083
+ dialog.hidden = false;
2084
+ input.value = '';
2085
+ filter('');
2086
+ input.focus();
2087
+ }
2088
+
2089
+ function close() {
2090
+ overlay.hidden = true;
2091
+ dialog.hidden = true;
2092
+ }
2093
+
2094
+ function scrollActive() {
2095
+ var el = list.querySelector('.cmdk-item-active');
2096
+ if (el) el.scrollIntoView({ block: 'nearest' });
2097
+ }
2098
+
2099
+ document.addEventListener('keydown', function (e) {
2100
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2101
+ e.preventDefault();
2102
+ if (dialog.hidden) open(); else close();
2103
+ return;
2104
+ }
2105
+ if (dialog.hidden) return;
2106
+ if (e.key === 'Escape') { e.preventDefault(); close(); }
2107
+ if (e.key === 'ArrowDown') { e.preventDefault(); activeIdx = Math.min(activeIdx + 1, filtered.length - 1); render(); scrollActive(); }
2108
+ if (e.key === 'ArrowUp') { e.preventDefault(); activeIdx = Math.max(activeIdx - 1, 0); render(); scrollActive(); }
2109
+ if (e.key === 'Enter') { e.preventDefault(); if (filtered[activeIdx]) window.location.href = filtered[activeIdx].url; }
2110
+ });
2111
+
2112
+ overlay.addEventListener('click', close);
2113
+ trigger.addEventListener('click', open);
2114
+ input.addEventListener('input', function () { filter(input.value.toLowerCase()); });
2115
+
2116
+ list.addEventListener('mousemove', function (e) {
2117
+ var item = e.target.closest('.cmdk-item');
2118
+ if (item) {
2119
+ var idx = parseInt(item.getAttribute('data-idx'), 10);
2120
+ if (idx !== activeIdx) { activeIdx = idx; render(); }
2121
+ }
2122
+ });
2123
+
2124
+ list.addEventListener('click', function (e) {
2125
+ var item = e.target.closest('.cmdk-item');
2126
+ if (item) { e.preventDefault(); window.location.href = item.getAttribute('href'); }
2127
+ });
2128
+ })();
2129
+ (function () {
2130
+ var btn = document.getElementById('theme-toggle');
2131
+ if (!btn) return;
2132
+ var sunSvg = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
2133
+ var moonSvg = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
2134
+ function setIcon() {
2135
+ btn.innerHTML = document.documentElement.getAttribute('data-theme') === 'dark' ? moonSvg : sunSvg;
2136
+ }
2137
+ setIcon();
2138
+ btn.addEventListener('click', function () {
2139
+ var current = document.documentElement.getAttribute('data-theme');
2140
+ var next = current === 'dark' ? 'light' : 'dark';
2141
+ document.documentElement.setAttribute('data-theme', next);
2142
+ localStorage.setItem('scope-theme', next);
2143
+ setIcon();
2144
+ });
2145
+ })();
2146
+ </script>
2147
+ </body>
2148
+ </html>`;
2149
+ }
2150
+
2151
+ // src/templates/component-detail.ts
2152
+ function notGenerated(message = "Not generated") {
2153
+ return `<div class="not-generated">${escapeHtml(message)}</div>`;
2154
+ }
2155
+ function renderFailure(message, heuristicFlags = []) {
2156
+ const hintHtml = heuristicFlags.length > 0 ? `<div class="render-failure-hints"><strong>Failure hints:</strong> ${escapeHtml(heuristicFlags.join(", "))}</div>` : "";
2157
+ return `<div class="render-failure"><strong>Render failed:</strong> ${escapeHtml(message)}${hintHtml}</div>`;
2158
+ }
2159
+ function sectionWrap(id, title, description, content) {
2160
+ return `<section class="section" id="${id}">
2161
+ <div class="section-header">
2162
+ <h2>${escapeHtml(title)}</h2>
2163
+ <p>${escapeHtml(description)}</p>
2164
+ </div>
2165
+ ${content}
2166
+ </section>`;
2167
+ }
2168
+ function renderPlayground(name, data) {
2169
+ const component = data.manifest.components[name];
2170
+ const render = data.renders.get(name);
2171
+ const slug = slugify(name);
2172
+ const allProps = component ? Object.entries(component.props) : [];
2173
+ const ownProps = allProps.filter(([, p]) => p.source !== "inherited");
2174
+ const editableTypes = /* @__PURE__ */ new Set([
2175
+ "string",
2176
+ "number",
2177
+ "boolean",
2178
+ "union",
2179
+ "array",
2180
+ "any",
2181
+ "unknown",
2182
+ "other",
2183
+ "node",
2184
+ "element"
2185
+ ]);
2186
+ const editableProps = ownProps.filter(([, p]) => editableTypes.has(p.type));
2187
+ const hasChildren = allProps.some(([n]) => n === "children");
2188
+ const childrenInEditable = editableProps.some(([n]) => n === "children");
2189
+ if (hasChildren && !childrenInEditable) {
2190
+ const childrenProp = allProps.find(([n]) => n === "children");
2191
+ if (childrenProp) editableProps.unshift(childrenProp);
2192
+ }
2193
+ const scopeDefaults = data.playgroundDefaults?.get(name) ?? {};
2194
+ function getOverrideDefault(propName, prop) {
2195
+ const scopeVal = scopeDefaults[propName];
2196
+ if (scopeVal !== void 0) return String(scopeVal);
2197
+ if (propName === "children" && (prop.type === "node" || prop.type === "element")) return name;
2198
+ return void 0;
2199
+ }
2200
+ const controlsHtml = editableProps.length > 0 ? `<div class="pg-controls" id="pg-controls-${escapeHtml(slug)}">
2201
+ <div class="pg-controls-header">Props</div>
2202
+ ${editableProps.map(([n, p]) => propControlHtml(n, p, getOverrideDefault(n, p))).join("\n ")}
2203
+ </div>` : "";
2204
+ const iframeSrc = `${data.options.basePath}playground/${slug}.html`;
2205
+ const svgSun = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
2206
+ const svgMoon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
2207
+ const svgMonitor = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
2208
+ const svgPhone = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>';
2209
+ const svgTablet = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>';
2210
+ const svgExpand = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
2211
+ const svgClose = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
2212
+ const es = escapeHtml(slug);
2213
+ const playgroundHtml = render?.errorMessage ? "" : `<div class="pg-container" id="pg-container-${es}">
2214
+ ${controlsHtml}
2215
+ <div class="pg-preview-wrapper" id="pg-preview-${es}">
2216
+ <div class="pg-toolbar" id="pg-toolbar-${es}">
2217
+ <button class="pg-toolbar-btn pg-theme-btn" id="pg-theme-${es}" title="Toggle dark mode">${svgSun}</button>
2218
+ <span class="pg-toolbar-sep"></span>
2219
+ <button class="pg-toolbar-btn pg-device-btn active" data-width="" title="Auto">${svgMonitor}</button>
2220
+ <button class="pg-toolbar-btn pg-device-btn" data-width="375" title="Mobile (375px)">${svgPhone}</button>
2221
+ <button class="pg-toolbar-btn pg-device-btn" data-width="768" title="Tablet (768px)">${svgTablet}</button>
2222
+ <span class="pg-toolbar-sep"></span>
2223
+ <span class="pg-toolbar-spacer"></span>
2224
+ <button class="pg-toolbar-btn pg-fullscreen-btn" id="pg-fs-${es}" title="Fullscreen">${svgExpand}</button>
2225
+ </div>
2226
+ <div class="pg-iframe-area" id="pg-iframe-area-${es}">
2227
+ <iframe
2228
+ class="pg-iframe"
2229
+ id="pg-iframe-${es}"
2230
+ src="${escapeHtml(iframeSrc)}"
2231
+ loading="lazy"
2232
+ allowtransparency="true"
2233
+ ></iframe>
2234
+ </div>
2235
+ </div>
2236
+ </div>
2237
+ <script>
2238
+ (function() {
2239
+ var iframe = document.getElementById("pg-iframe-${es}");
2240
+ var container = document.getElementById("pg-container-${es}");
2241
+ var preview = document.getElementById("pg-preview-${es}");
2242
+ var toolbar = document.getElementById("pg-toolbar-${es}");
2243
+ var iframeArea = document.getElementById("pg-iframe-area-${es}");
2244
+ var themeBtn = document.getElementById("pg-theme-${es}");
2245
+ var fsBtn = document.getElementById("pg-fs-${es}");
2246
+ if (!iframe || !container || !preview) return;
2247
+
2248
+ // --- Props ---
2249
+ function collectProps() {
2250
+ var props = {};
2251
+ var controls = container.querySelectorAll("[data-prop-name]");
2252
+ for (var i = 0; i < controls.length; i++) {
2253
+ var el = controls[i];
2254
+ var propName = el.getAttribute("data-prop-name");
2255
+ if (!propName) continue;
2256
+ if (el.type === "checkbox") {
2257
+ props[propName] = el.checked;
2258
+ } else {
2259
+ var v = (el.value || "").trim();
2260
+ if (v === "") continue;
2261
+ try { props[propName] = JSON.parse(v); } catch(_) { props[propName] = v; }
2262
+ }
2263
+ }
2264
+ return props;
2265
+ }
2266
+
2267
+ function sendProps() {
2268
+ if (!iframe.contentWindow) return;
2269
+ iframe.contentWindow.postMessage({ type: "scope-playground-props", props: collectProps() }, "*");
2270
+ }
2271
+
2272
+ var propsSent = false;
2273
+ function sendOnce() {
2274
+ if (!propsSent) {
2275
+ propsSent = true;
2276
+ setTimeout(function() { sendProps(); applyTheme(); }, 50);
2277
+ }
2278
+ }
2279
+ iframe.addEventListener("load", sendOnce);
2280
+ try { if (iframe.contentDocument && iframe.contentDocument.readyState === "complete") sendOnce(); } catch(e) {}
2281
+ container.addEventListener("input", sendProps);
2282
+ container.addEventListener("change", sendProps);
2283
+
2284
+ window.addEventListener("message", function(e) {
2285
+ if (e.data && e.data.type === "scope-playground-height" && e.source === iframe.contentWindow) {
2286
+ var h = Math.max(300, e.data.height + 32);
2287
+ iframe.style.height = h + "px";
2288
+ }
2289
+ });
2290
+
2291
+ // --- Dark / Light mode ---
2292
+ var isDark = document.documentElement.getAttribute("data-theme") === "dark";
2293
+ var userOverride = false;
2294
+
2295
+ function applyTheme() {
2296
+ themeBtn.innerHTML = isDark ? ${JSON.stringify(svgMoon)} : ${JSON.stringify(svgSun)};
2297
+ themeBtn.classList.toggle("active", isDark);
2298
+ preview.classList.toggle("pg-dark", isDark);
2299
+ if (iframe.contentWindow) {
2300
+ iframe.contentWindow.postMessage({ type: "scope-playground-theme", theme: isDark ? "dark" : "light" }, "*");
2301
+ }
2302
+ }
2303
+
2304
+ // Apply initial theme from global setting
2305
+ applyTheme();
2306
+
2307
+ themeBtn.addEventListener("click", function() {
2308
+ userOverride = true;
2309
+ isDark = !isDark;
2310
+ applyTheme();
2311
+ });
2312
+
2313
+ // Follow global theme changes unless the user has manually toggled
2314
+ new MutationObserver(function() {
2315
+ if (userOverride) return;
2316
+ isDark = document.documentElement.getAttribute("data-theme") === "dark";
2317
+ applyTheme();
2318
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
2319
+
2320
+ // --- Device size ---
2321
+ var deviceBtns = toolbar.querySelectorAll(".pg-device-btn");
2322
+ for (var d = 0; d < deviceBtns.length; d++) {
2323
+ deviceBtns[d].addEventListener("click", function() {
2324
+ for (var j = 0; j < deviceBtns.length; j++) deviceBtns[j].classList.remove("active");
2325
+ this.classList.add("active");
2326
+ var w = this.getAttribute("data-width");
2327
+ iframe.style.maxWidth = w ? w + "px" : "";
2328
+ iframe.style.margin = w ? "0 auto" : "";
2329
+ });
2330
+ }
2331
+
2332
+ // --- Fullscreen ---
2333
+ function exitFullscreen() {
2334
+ preview.classList.remove("pg-fullscreen");
2335
+ fsBtn.innerHTML = ${JSON.stringify(svgExpand)};
2336
+ fsBtn.title = "Fullscreen";
2337
+ }
2338
+ fsBtn.addEventListener("click", function() {
2339
+ var isFs = preview.classList.toggle("pg-fullscreen");
2340
+ fsBtn.innerHTML = isFs ? ${JSON.stringify(svgClose)} : ${JSON.stringify(svgExpand)};
2341
+ fsBtn.title = isFs ? "Exit fullscreen" : "Fullscreen";
2342
+ });
2343
+ document.addEventListener("keydown", function(e) {
2344
+ if (e.key === "Escape" && preview.classList.contains("pg-fullscreen")) exitFullscreen();
2345
+ });
2346
+ })();
2347
+ </script>`;
2348
+ const renderW = render?.width != null ? render.width : void 0;
2349
+ const renderH = render?.height != null ? render.height : void 0;
2350
+ const renderSizeAttr = renderW != null && renderH != null ? ` width="${renderW}" height="${renderH}"` : "";
2351
+ const playgroundError = render?.errorMessage ? renderFailure(
2352
+ `${render.errorMessage}
2353
+ Inspect .reactscope/renders/${name}.error.json and rerun scope render component ${name}.`,
2354
+ render.heuristicFlags ?? []
2355
+ ) : render && !render.screenshot && !render.svgContent ? renderFailure(
2356
+ `No static preview was generated for ${name}. Check .reactscope/run-summary.json and rerun scope render all.`
2357
+ ) : "";
2358
+ const screenshotFallback = render?.screenshot ? `<details class="pg-static-fallback">
2359
+ <summary>Static Preview</summary>
2360
+ <div class="render-preview">
2361
+ <img src="data:image/png;base64,${render.screenshot}" alt="${escapeHtml(name)} render"${renderSizeAttr} />
2362
+ </div>
2363
+ </details>` : "";
2364
+ return sectionWrap(
2365
+ "playground",
2366
+ "Playground",
2367
+ "Interactive component preview with prop controls.",
2368
+ `${playgroundError}${playgroundHtml}${screenshotFallback}`
2369
+ );
2370
+ }
2371
+ function renderProps(name, data) {
2372
+ const component = data.manifest.components[name];
2373
+ if (!component) {
2374
+ return sectionWrap("props", "Props", "Component prop definitions.", notGenerated());
2375
+ }
2376
+ const allProps = Object.entries(component.props);
2377
+ const ownProps = allProps.filter(([, p]) => p.source !== "inherited");
2378
+ const inheritedProps = allProps.filter(([, p]) => p.source === "inherited");
2379
+ function propsTableHtml(rows) {
2380
+ return `<table class="props-table">
2381
+ <thead>
2382
+ <tr>
2383
+ <th>Prop</th>
2384
+ <th>Type</th>
2385
+ <th>Required</th>
2386
+ <th>Default</th>
2387
+ </tr>
2388
+ </thead>
2389
+ <tbody>
2390
+ ${rows.map(([n, p]) => propTableRow(n, p)).join("\n ")}
2391
+ </tbody>
2392
+ </table>`;
2393
+ }
2394
+ const GROUP_LABELS = {
2395
+ html: "HTML Attributes",
2396
+ react: "React Props",
2397
+ aria: "ARIA Attributes"
2398
+ };
2399
+ function renderInheritedGroup(groupName, rows) {
2400
+ const label = GROUP_LABELS[groupName] ?? groupName;
2401
+ return `<details class="inherited-group">
2402
+ <summary>${escapeHtml(label)} <span class="inherited-group-count">${rows.length}</span></summary>
2403
+ ${propsTableHtml(rows)}
2404
+ </details>`;
2405
+ }
2406
+ if (allProps.length === 0) {
2407
+ return sectionWrap(
2408
+ "props",
2409
+ "Props",
2410
+ "Component prop definitions.",
2411
+ `<p style="color:var(--color-muted);font-size:13px">No props defined.</p>`
2412
+ );
2413
+ }
2414
+ let content;
2415
+ if (inheritedProps.length === 0) {
2416
+ content = propsTableHtml(allProps);
2417
+ } else {
2418
+ const ownTable = ownProps.length > 0 ? propsTableHtml(ownProps) : `<p style="color:var(--color-muted);font-size:13px">No component-specific props.</p>`;
2419
+ const groups = /* @__PURE__ */ new Map();
2420
+ for (const entry of inheritedProps) {
2421
+ const group = entry[1].sourceGroup ?? "other";
2422
+ const list = groups.get(group) ?? [];
2423
+ list.push(entry);
2424
+ groups.set(group, list);
2425
+ }
2426
+ const order = ["html", "react", "aria"];
2427
+ const sortedKeys = [
2428
+ ...order.filter((k) => groups.has(k)),
2429
+ ...[...groups.keys()].filter((k) => !order.includes(k)).sort()
2430
+ ];
2431
+ const groupHtml = sortedKeys.map((key) => renderInheritedGroup(key, groups.get(key) ?? [])).join("\n");
2432
+ content = `${ownTable}
2433
+ <div class="inherited-props-section">
2434
+ <div class="inherited-props-heading">${inheritedProps.length} inherited prop${inheritedProps.length === 1 ? "" : "s"}</div>
2435
+ ${groupHtml}
2436
+ </div>`;
2437
+ }
2438
+ return sectionWrap(
2439
+ "props",
2440
+ "Props",
2441
+ `${allProps.length} prop${allProps.length === 1 ? "" : "s"} defined.`,
2442
+ content
2443
+ );
2444
+ }
2445
+ function renderMatrix(name, data) {
2446
+ const render = data.renders.get(name);
2447
+ if (!render?.cells || render.cells.length === 0) {
2448
+ return sectionWrap(
2449
+ "matrix",
2450
+ "Matrix",
2451
+ "Prop combination matrix renders.",
2452
+ render?.errorMessage ? renderFailure(
2453
+ `${render.errorMessage}
2454
+ Matrix renders unavailable because the base render failed. Inspect .reactscope/renders/${name}.error.json.`,
2455
+ render.heuristicFlags ?? []
2456
+ ) : notGenerated(
2457
+ "Matrix renders not generated. Run scope render matrix <Component> to produce a matrix."
2458
+ )
2459
+ );
2460
+ }
2461
+ const cells = render.cells;
2462
+ const cols = Math.ceil(Math.sqrt(cells.length));
2463
+ const cellsHtml = cells.map((cell) => {
2464
+ const label = cell.axisValues.join(" / ");
2465
+ const imgHtml = cell.screenshot ? `<img class="scope-screenshot" src="data:image/png;base64,${cell.screenshot}" alt="${escapeHtml(label)}" />` : `<span class="matrix-cell-error">${cell.error ? escapeHtml(cell.error) : "failed"}</span>`;
2466
+ return `<div class="matrix-cell">${imgHtml}<div class="cell-label">${escapeHtml(label)}</div></div>`;
2467
+ }).join("\n");
2468
+ const grid = `<div class="matrix-grid" style="grid-template-columns: repeat(${cols}, 1fr)">
2469
+ ${cellsHtml}
2470
+ </div>`;
2471
+ return sectionWrap("matrix", "Matrix", "Prop combination matrix renders.", grid);
2472
+ }
2473
+ function renderDocs() {
2474
+ return sectionWrap(
2475
+ "docs",
2476
+ "Docs",
2477
+ "Component documentation.",
2478
+ `<p style="color:var(--color-muted);font-size:13px">No documentation file found for this component.</p>`
2479
+ );
2480
+ }
2481
+ function renderAnalysis(name, data) {
2482
+ const component = data.manifest.components[name];
2483
+ if (!component) {
2484
+ return sectionWrap("analysis", "Analysis", "Static analysis results.", notGenerated());
2485
+ }
2486
+ const propCount = Object.keys(component.props).length;
2487
+ const hookCount = component.detectedHooks.length;
2488
+ const sideEffectCount = component.sideEffects.fetches.length + (component.sideEffects.timers ? 1 : 0) + component.sideEffects.subscriptions.length + (component.sideEffects.globalListeners ? 1 : 0);
2489
+ const statsGrid = `<div class="stats-grid">
2490
+ <div class="stat-card">
2491
+ <div class="stat-label">Complexity</div>
2492
+ <div class="stat-value"><span class="badge ${component.complexityClass}">${escapeHtml(component.complexityClass)}</span></div>
2493
+ </div>
2494
+ <div class="stat-card">
2495
+ <div class="stat-label">Props</div>
2496
+ <div class="stat-value">${propCount}</div>
2497
+ </div>
2498
+ <div class="stat-card">
2499
+ <div class="stat-label">Hooks</div>
2500
+ <div class="stat-value">${hookCount}</div>
2501
+ </div>
2502
+ <div class="stat-card">
2503
+ <div class="stat-label">Side Effects</div>
2504
+ <div class="stat-value">${sideEffectCount}</div>
2505
+ </div>
2506
+ <div class="stat-card">
2507
+ <div class="stat-label">Export</div>
2508
+ <div class="stat-value" style="font-size:14px">${escapeHtml(component.exportType)}</div>
2509
+ </div>
2510
+ <div class="stat-card">
2511
+ <div class="stat-label">Memoized</div>
2512
+ <div class="stat-value" style="font-size:14px">${component.memoized ? "Yes" : "No"}</div>
2513
+ </div>
2514
+ <div class="stat-card">
2515
+ <div class="stat-label">forwardedRef</div>
2516
+ <div class="stat-value" style="font-size:14px">${component.forwardedRef ? "Yes" : "No"}</div>
2517
+ </div>
2518
+ </div>`;
2519
+ function tagList(items) {
2520
+ if (items.length === 0)
2521
+ return `<span style="color:var(--color-muted);font-size:12px">None</span>`;
2522
+ return `<div class="tag-list">${items.map((i) => `<span class="tag">${escapeHtml(i)}</span>`).join("")}</div>`;
2523
+ }
2524
+ const sideEffectItems = [
2525
+ ...component.sideEffects.fetches.map((f) => `fetch: ${f}`),
2526
+ ...component.sideEffects.timers ? ["timers"] : [],
2527
+ ...component.sideEffects.subscriptions.map((s) => `sub: ${s}`),
2528
+ ...component.sideEffects.globalListeners ? ["global listeners"] : []
2529
+ ];
2530
+ const analysisGrid = `<div class="analysis-grid">
2531
+ <div class="analysis-card">
2532
+ <h3>Detected Hooks</h3>
2533
+ <div class="value">${tagList(component.detectedHooks)}</div>
2534
+ </div>
2535
+ <div class="analysis-card">
2536
+ <h3>Required Contexts</h3>
2537
+ <div class="value">${tagList(component.requiredContexts)}</div>
2538
+ </div>
2539
+ <div class="analysis-card">
2540
+ <h3>HOC Wrappers</h3>
2541
+ <div class="value">${tagList(component.hocWrappers)}</div>
2542
+ </div>
2543
+ <div class="analysis-card">
2544
+ <h3>Side Effects</h3>
2545
+ <div class="value">${tagList(sideEffectItems)}</div>
2546
+ </div>
2547
+ </div>`;
2548
+ return sectionWrap(
2549
+ "analysis",
2550
+ "Analysis",
2551
+ "Static analysis results for this component.",
2552
+ `${statsGrid}${analysisGrid}`
2553
+ );
2554
+ }
2555
+ function renderXRay(name, data) {
754
2556
  const render = data.renders.get(name);
755
2557
  if (!render?.dom) {
756
2558
  return sectionWrap(
757
2559
  "x-ray",
758
2560
  "X-Ray",
759
2561
  "DOM structure and computed styles.",
760
- notGenerated("X-Ray data not generated. Run scope render with DOM capture enabled.")
2562
+ render?.errorMessage ? renderFailure(
2563
+ `${render.errorMessage}
2564
+ X-Ray data unavailable because render failed. Inspect .reactscope/renders/${name}.error.json.`,
2565
+ render.heuristicFlags ?? []
2566
+ ) : notGenerated(
2567
+ "X-Ray data not generated. Run scope render component <Component> with DOM capture enabled."
2568
+ )
761
2569
  );
762
2570
  }
763
2571
  const dom = render.dom;
@@ -768,11 +2576,14 @@ function renderXRay(name, data) {
768
2576
  if (m?.[1] !== void 0) nodeStylesMap[parseInt(m[1], 10)] = styles;
769
2577
  }
770
2578
  }
771
- const nodeStylesJson = escapeHtml(JSON.stringify(nodeStylesMap));
2579
+ const nodeStylesJson = JSON.stringify(JSON.stringify(nodeStylesMap)).replace(
2580
+ /<\/script>/gi,
2581
+ "<\\/script>"
2582
+ );
772
2583
  const treeHtml = `<div class="dom-tree">${renderDOMTree(dom.tree)}</div>`;
773
2584
  const stylesPanelHtml = `
774
2585
  <div id="xray-styles-panel-${escapeHtml(name)}" class="xray-styles-panel" style="display:none;margin-top:16px;border:1px solid var(--color-border);border-radius:6px;overflow:hidden">
775
- <div class="xray-styles-header" style="padding:8px 12px;background:var(--color-surface-2);font-size:12px;font-weight:600;color:var(--color-muted);display:flex;justify-content:space-between;align-items:center">
2586
+ <div class="xray-styles-header" style="padding:8px 12px;background:var(--color-bg-subtle);font-size:12px;font-weight:600;color:var(--color-muted);display:flex;justify-content:space-between;align-items:center">
776
2587
  <span id="xray-styles-label-${escapeHtml(name)}">\u2014 no node selected \u2014</span>
777
2588
  <button onclick="document.getElementById('xray-styles-panel-${escapeHtml(name)}').style.display='none'" style="background:none;border:none;cursor:pointer;color:var(--color-muted);font-size:16px;line-height:1">\xD7</button>
778
2589
  </div>
@@ -797,7 +2608,7 @@ function renderXRay(name, data) {
797
2608
  var tag = el.tagName === "DETAILS" ? el.querySelector("summary .dom-tag-open") : el;
798
2609
  label.textContent = tag ? tag.textContent.trim().slice(0, 60) : "node #" + id;
799
2610
  body.innerHTML = Object.entries(styles).map(function(e) {
800
- return "<tr><td>" + e[0] + "</td><td style="font-family:monospace">" + e[1] + "</td></tr>";
2611
+ return "<tr><td>" + e[0] + "</td><td style='font-family:monospace'>" + e[1] + "</td></tr>";
801
2612
  }).join("");
802
2613
  panel.style.display = "block";
803
2614
  // Highlight selected node
@@ -896,9 +2707,11 @@ function renderComposition(name, data) {
896
2707
  if (items.length === 0) {
897
2708
  return `<div class="comp-list"><h3>${escapeHtml(title)}</h3><p style="color:var(--color-muted);font-size:12px">None</p></div>`;
898
2709
  }
899
- const liItems = items.map(
900
- (n) => `<li><a href="${data.options.basePath}${slugify(n)}.html">${escapeHtml(n)}</a></li>`
901
- ).join("");
2710
+ const liItems = items.map((n) => {
2711
+ const compData = data.manifest.components[n];
2712
+ const internalBadge = compData?.internal === true ? `<span class="badge-internal">internal</span>` : "";
2713
+ return `<li><a href="${data.options.basePath}${slugify(n)}.html">${escapeHtml(n)}</a>${internalBadge}</li>`;
2714
+ }).join("");
902
2715
  return `<div class="comp-list"><h3>${escapeHtml(title)}</h3><ul>${liItems}</ul></div>`;
903
2716
  }
904
2717
  return sectionWrap(
@@ -923,15 +2736,10 @@ function renderComponentDetail(name, data) {
923
2736
  <div class="meta">${complexityBadge}${memoizedBadge}${exportBadge}</div>
924
2737
  ${filepath}
925
2738
  </div>`;
926
- const sections = [
2739
+ const mainSections = [
927
2740
  renderPlayground(name, data),
928
2741
  renderMatrix(name, data),
929
2742
  renderDocs(),
930
- renderAnalysis(name, data),
931
- renderXRay(name, data),
932
- renderTokens(name, data),
933
- renderAccessibility(name, data),
934
- renderComposition(name, data),
935
2743
  sectionWrap(
936
2744
  "responsive",
937
2745
  "Responsive",
@@ -947,34 +2755,105 @@ function renderComponentDetail(name, data) {
947
2755
  notGenerated("Stress tests not generated. Stress render runs are a future feature.")
948
2756
  )
949
2757
  ];
950
- const body = `${header}${sections.join("\n")}`;
951
- const onThisPage = SECTIONS.map((s) => {
952
- const id = s.toLowerCase().replace(/\s+/g, "-");
953
- return `<a href="#${id}">${escapeHtml(s)}</a>`;
954
- }).join("\n");
955
- const componentNames = Object.keys(data.manifest.components);
956
- const sidebar = sidebarLinks(componentNames, slug, data.options.basePath);
2758
+ const inspectorSections = [
2759
+ renderProps(name, data),
2760
+ renderXRay(name, data),
2761
+ renderTokens(name, data),
2762
+ renderAccessibility(name, data),
2763
+ renderAnalysis(name, data),
2764
+ renderComposition(name, data)
2765
+ ];
2766
+ const body = `<div class="detail-columns">
2767
+ <div class="detail-main">
2768
+ ${header}
2769
+ ${mainSections.join("\n")}
2770
+ </div>
2771
+ <div class="detail-inspector">
2772
+ ${inspectorSections.join("\n")}
2773
+ </div>
2774
+ </div>`;
2775
+ const sidebar = sidebarLinks(data, slug);
2776
+ const basePath = data.options.basePath;
2777
+ const breadcrumbs = [];
2778
+ if (component?.collection) {
2779
+ breadcrumbs.push({
2780
+ label: component.collection,
2781
+ url: `${basePath}index.html#collection-${slugify(component.collection)}`
2782
+ });
2783
+ }
2784
+ breadcrumbs.push({ label: name });
957
2785
  return htmlShell({
958
2786
  title: `${name} \u2014 ${data.options.title}`,
959
2787
  body,
960
2788
  sidebar,
961
- onThisPage,
962
- basePath: data.options.basePath
2789
+ onThisPage: "",
2790
+ basePath,
2791
+ searchItems: buildSearchItems(data),
2792
+ breadcrumbs
963
2793
  });
964
2794
  }
965
2795
 
966
2796
  // src/templates/component-index.ts
2797
+ function renderCard(name, data) {
2798
+ const component = data.manifest.components[name];
2799
+ if (!component) return "";
2800
+ const slug = slugify(name);
2801
+ const render = data.renders.get(name);
2802
+ const allPropEntries = Object.entries(component.props);
2803
+ const ownPropCount = allPropEntries.filter(([, p]) => p.source !== "inherited").length;
2804
+ const totalPropCount = allPropEntries.length;
2805
+ const hasInherited = ownPropCount < totalPropCount;
2806
+ const propCount = ownPropCount;
2807
+ const hookCount = component.detectedHooks.length;
2808
+ const previewHtml = render?.screenshot ? `<img class="scope-screenshot" src="data:image/png;base64,${render.screenshot}" alt="${escapeHtml(name)}" />` : `<span class="no-preview">No preview</span>`;
2809
+ return `<a class="component-card" href="${data.options.basePath}${slug}.html" data-name="${escapeHtml(name.toLowerCase())}">
2810
+ <div class="card-preview">${previewHtml}</div>
2811
+ <div class="card-body">
2812
+ <div class="card-name">${escapeHtml(name)}</div>
2813
+ <div class="card-meta">
2814
+ <span>${propCount} prop${propCount === 1 ? "" : "s"}${hasInherited ? ` <span style="color:var(--color-muted)">(+${totalPropCount - ownPropCount})</span>` : ""}</span>
2815
+ <span class="badge ${component.complexityClass}" style="font-size:10px">${escapeHtml(component.complexityClass)}</span>
2816
+ ${hookCount > 0 ? `<span>${hookCount} hooks</span>` : ""}
2817
+ </div>
2818
+ </div>
2819
+ </a>`;
2820
+ }
967
2821
  function renderComponentIndex(data) {
968
- const components = Object.entries(data.manifest.components);
969
- const totalCount = components.length;
970
- const simpleCount = components.filter(([, c]) => c.complexityClass === "simple").length;
971
- const complexCount = components.filter(([, c]) => c.complexityClass === "complex").length;
972
- const memoizedCount = components.filter(([, c]) => c.memoized).length;
2822
+ const allComponents = Object.entries(data.manifest.components);
2823
+ const collections = data.manifest.collections ?? [];
2824
+ const visibleComponents = allComponents.filter(([, c]) => !c.internal);
2825
+ const collectionMap = /* @__PURE__ */ new Map();
2826
+ for (const col of collections) {
2827
+ collectionMap.set(col.name, []);
2828
+ }
2829
+ const ungrouped = [];
2830
+ for (const [name, component] of visibleComponents) {
2831
+ if (component.collection) {
2832
+ if (!collectionMap.has(component.collection)) {
2833
+ collectionMap.set(component.collection, []);
2834
+ }
2835
+ collectionMap.get(component.collection)?.push(name);
2836
+ } else {
2837
+ ungrouped.push(name);
2838
+ }
2839
+ }
2840
+ const totalCount = allComponents.length;
2841
+ const visibleCount = visibleComponents.length;
2842
+ const nonEmptyCollections = [...collectionMap.values()].filter(
2843
+ (names) => names.length > 0
2844
+ ).length;
2845
+ const simpleCount = visibleComponents.filter(([, c]) => c.complexityClass === "simple").length;
2846
+ const complexCount = visibleComponents.filter(([, c]) => c.complexityClass === "complex").length;
2847
+ const memoizedCount = visibleComponents.filter(([, c]) => c.memoized).length;
973
2848
  const statsGrid = `<div class="stats-grid">
974
2849
  <div class="stat-card">
975
2850
  <div class="stat-label">Total Components</div>
976
2851
  <div class="stat-value">${totalCount}</div>
977
2852
  </div>
2853
+ <div class="stat-card">
2854
+ <div class="stat-label">Collections</div>
2855
+ <div class="stat-value">${nonEmptyCollections}</div>
2856
+ </div>
978
2857
  <div class="stat-card">
979
2858
  <div class="stat-label">Simple</div>
980
2859
  <div class="stat-value">${simpleCount}</div>
@@ -988,31 +2867,81 @@ function renderComponentIndex(data) {
988
2867
  <div class="stat-value">${memoizedCount}</div>
989
2868
  </div>
990
2869
  </div>`;
991
- const cards = components.sort(([a], [b]) => a.localeCompare(b)).map(([name, component]) => {
992
- const slug = slugify(name);
993
- const render = data.renders.get(name);
994
- const propCount = Object.keys(component.props).length;
995
- const hookCount = component.detectedHooks.length;
996
- const previewHtml = render?.screenshot ? `<img class="scope-screenshot" src="data:image/png;base64,${render.screenshot}" alt="${escapeHtml(name)}" />` : `<span class="no-preview">No preview</span>`;
997
- return `<a class="component-card" href="${data.options.basePath}${slug}.html" data-name="${escapeHtml(name.toLowerCase())}">
998
- <div class="card-preview">${previewHtml}</div>
999
- <div class="card-body">
1000
- <div class="card-name">${escapeHtml(name)}</div>
1001
- <div class="card-meta">
1002
- <span>${propCount} props</span>
1003
- <span class="badge ${component.complexityClass}" style="font-size:10px">${escapeHtml(component.complexityClass)}</span>
1004
- ${hookCount > 0 ? `<span>${hookCount} hooks</span>` : ""}
1005
- </div>
2870
+ const hasCollections = collectionMap.size > 0 || visibleComponents.some(([, c]) => c.collection);
2871
+ const collectionDescriptions = /* @__PURE__ */ new Map();
2872
+ for (const col of collections) {
2873
+ if (col.description) {
2874
+ collectionDescriptions.set(col.name, col.description);
2875
+ }
2876
+ }
2877
+ function renderCollectionTile(colName, names, description) {
2878
+ const slug = colName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2879
+ const descHtml = description ? `<div class="coll-tile-desc">${escapeHtml(description)}</div>` : "";
2880
+ const thumbs = names.slice(0, 4).map((n) => {
2881
+ const render = data.renders.get(n);
2882
+ return render?.screenshot ? `<img class="coll-thumb" src="data:image/png;base64,${render.screenshot}" alt="${escapeHtml(n)}" />` : `<span class="coll-thumb coll-thumb-empty"></span>`;
2883
+ }).join("");
2884
+ return `<a class="collection-tile" href="#collection-${slug}">
2885
+ <div class="coll-tile-thumbs">${thumbs}</div>
2886
+ <div class="coll-tile-body">
2887
+ <div class="coll-tile-name">${escapeHtml(colName)}</div>
2888
+ ${descHtml}
2889
+ <div class="coll-tile-count">${names.length} component${names.length === 1 ? "" : "s"}</div>
1006
2890
  </div>
1007
2891
  </a>`;
1008
- }).join("\n");
1009
- const cardGrid = `<div class="component-grid" id="component-grid">${cards}</div>`;
2892
+ }
2893
+ let collectionGallery = "";
2894
+ if (hasCollections) {
2895
+ const tileParts = [];
2896
+ for (const [colName, names] of collectionMap) {
2897
+ if (names.length === 0) continue;
2898
+ tileParts.push(renderCollectionTile(colName, names, collectionDescriptions.get(colName)));
2899
+ }
2900
+ if (ungrouped.length > 0) {
2901
+ tileParts.push(renderCollectionTile("Ungrouped", ungrouped));
2902
+ }
2903
+ collectionGallery = `<div class="collection-gallery-section">
2904
+ <h2 class="gallery-section-title">Collections</h2>
2905
+ <div class="collection-gallery">${tileParts.join("\n")}</div>
2906
+ </div>`;
2907
+ }
2908
+ let cardSections;
2909
+ if (!hasCollections) {
2910
+ const cards = visibleComponents.sort(([a], [b]) => a.localeCompare(b)).map(([name]) => renderCard(name, data)).join("\n");
2911
+ cardSections = `<div class="component-grid" id="component-grid">${cards}</div>`;
2912
+ } else {
2913
+ const sectionParts = [];
2914
+ for (const [colName, names] of collectionMap) {
2915
+ if (names.length === 0) continue;
2916
+ const slug = colName.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2917
+ const description = collectionDescriptions.get(colName);
2918
+ const descHtml = description ? `<p>${escapeHtml(description)}</p>` : "";
2919
+ const cards = names.sort().map((name) => renderCard(name, data)).join("\n");
2920
+ sectionParts.push(`<div class="collection-section" id="collection-${slug}">
2921
+ <div class="collection-section-header">
2922
+ <h2>${escapeHtml(colName)}</h2>
2923
+ ${descHtml}
2924
+ </div>
2925
+ <div class="component-grid">${cards}</div>
2926
+ </div>`);
2927
+ }
2928
+ if (ungrouped.length > 0) {
2929
+ const cards = ungrouped.sort().map((name) => renderCard(name, data)).join("\n");
2930
+ sectionParts.push(`<div class="collection-section" id="collection-ungrouped">
2931
+ <div class="collection-section-header">
2932
+ <h2>Ungrouped</h2>
2933
+ <p>Components not assigned to a collection.</p>
2934
+ </div>
2935
+ <div class="component-grid">${cards}</div>
2936
+ </div>`);
2937
+ }
2938
+ cardSections = `<div id="component-grid">${sectionParts.join("\n")}</div>`;
2939
+ }
1010
2940
  const filterScript = `<script>
1011
2941
  (function () {
1012
2942
  var grid = document.getElementById('component-grid');
1013
- var search = document.getElementById('sidebar-search');
1014
- if (!grid || !search) return;
1015
2943
  var indexSearch = document.getElementById('index-search');
2944
+ if (!grid || !indexSearch) return;
1016
2945
  function filter(q) {
1017
2946
  var cards = grid.querySelectorAll('.component-card');
1018
2947
  cards.forEach(function (card) {
@@ -1020,28 +2949,29 @@ function renderComponentIndex(data) {
1020
2949
  card.style.display = name.includes(q) ? '' : 'none';
1021
2950
  });
1022
2951
  }
1023
- search.addEventListener('input', function () { filter(search.value.toLowerCase()); });
1024
- if (indexSearch) {
1025
- indexSearch.addEventListener('input', function () { filter(indexSearch.value.toLowerCase()); });
1026
- }
2952
+ indexSearch.addEventListener('input', function () { filter(indexSearch.value.toLowerCase()); });
1027
2953
  })();
1028
2954
  </script>`;
1029
2955
  const header = `<div class="index-header">
1030
2956
  <h1>${escapeHtml(data.options.title)}</h1>
1031
- <p>${totalCount} component${totalCount === 1 ? "" : "s"} analysed</p>
2957
+ <p>${visibleCount} component${visibleCount === 1 ? "" : "s"} analysed</p>
1032
2958
  <input class="search-box" type="search" id="index-search" placeholder="Filter components\u2026" style="margin-top:12px" />
1033
2959
  </div>`;
1034
- const body = `${header}${statsGrid}${cardGrid}${filterScript}`;
1035
- const componentNames = Object.keys(data.manifest.components);
1036
- const sidebar = sidebarLinks(componentNames, null, data.options.basePath);
1037
- const onThisPage = `<a href="#top">Overview</a>
1038
- <a href="#component-grid">Components</a>`;
2960
+ const body = `${header}${statsGrid}${collectionGallery}${cardSections}${filterScript}`;
2961
+ const sidebar = sidebarLinks(data, null);
2962
+ const onThisPageParts = [`<a href="#top">Overview</a>`];
2963
+ if (hasCollections) {
2964
+ onThisPageParts.push(`<a href="#collection-gallery">Collections</a>`);
2965
+ }
2966
+ onThisPageParts.push(`<a href="#component-grid">Components</a>`);
2967
+ const onThisPage = onThisPageParts.join("\n");
1039
2968
  return htmlShell({
1040
2969
  title: data.options.title,
1041
2970
  body,
1042
2971
  sidebar,
1043
2972
  onThisPage,
1044
- basePath: data.options.basePath
2973
+ basePath: data.options.basePath,
2974
+ searchItems: buildSearchItems(data)
1045
2975
  });
1046
2976
  }
1047
2977
 
@@ -1143,8 +3073,7 @@ function renderDashboard(data) {
1143
3073
  <p style="color:var(--color-muted);font-size:14px">Overview of all ${totalCount} analysed components.</p>
1144
3074
  </div>`;
1145
3075
  const body = `${header}${statsGrid}${complexitySection}${topPropsSection}${complianceSection}`;
1146
- const componentNames = Object.keys(data.manifest.components);
1147
- const sidebar = sidebarLinks(componentNames, null, data.options.basePath);
3076
+ const sidebar = sidebarLinks(data, null);
1148
3077
  const onThisPage = `<a href="#top">Overview</a>
1149
3078
  <a href="#complexity">Complexity</a>
1150
3079
  <a href="#top-props">Top by Props</a>
@@ -1154,7 +3083,585 @@ ${data.complianceBatch ? '<a href="#compliance">Compliance</a>' : ""}`;
1154
3083
  body,
1155
3084
  sidebar,
1156
3085
  onThisPage,
1157
- basePath: data.options.basePath
3086
+ basePath: data.options.basePath,
3087
+ searchItems: buildSearchItems(data),
3088
+ breadcrumbs: [{ label: "Dashboard" }]
3089
+ });
3090
+ }
3091
+
3092
+ // src/templates/icon-browser.ts
3093
+ function isIconComponent(filePath, displayName, patterns) {
3094
+ return patterns.some((p) => matchGlob(p, filePath) || matchGlob(p, displayName));
3095
+ }
3096
+ function getIconSvg(name, data) {
3097
+ const render = data.renders.get(name);
3098
+ if (render?.svgContent) return render.svgContent;
3099
+ if (render?.dom?.tree) return domTreeToSvg(render.dom.tree);
3100
+ return void 0;
3101
+ }
3102
+ function generateJsxSnippet(name, component) {
3103
+ const ownProps = Object.entries(component.props).filter(([, p]) => p.source !== "inherited");
3104
+ const propStr = ownProps.filter(([, p]) => p.required).map(([pName, p]) => {
3105
+ if (p.type === "string") return `${pName}=""`;
3106
+ if (p.type === "number") return `${pName}={0}`;
3107
+ if (p.type === "boolean") return pName;
3108
+ return `${pName}={\u2026}`;
3109
+ }).join(" ");
3110
+ const propsSection = propStr ? ` ${propStr}` : "";
3111
+ return `import { ${name} } from "${component.filePath.replace(/\.tsx?$/, "")}";
3112
+
3113
+ <${name}${propsSection} />`;
3114
+ }
3115
+ function renderIconBrowser(data) {
3116
+ const patterns = data.options.iconPatterns;
3117
+ const allComponents = Object.entries(data.manifest.components);
3118
+ const icons = allComponents.filter(([, c]) => isIconComponent(c.filePath, c.displayName, patterns)).sort(([a], [b]) => a.localeCompare(b));
3119
+ const iconDataEntries = icons.map(([name, component]) => {
3120
+ const svg = getIconSvg(name, data);
3121
+ const svgBytes = svg ? new TextEncoder().encode(svg).length : 0;
3122
+ const slug = slugify(name);
3123
+ const keywords = component.keywords ?? [];
3124
+ const usedBy = component.composedBy ?? [];
3125
+ const jsx = generateJsxSnippet(name, component);
3126
+ const ownProps = Object.entries(component.props).filter(([, p]) => p.source !== "inherited").map(([pName, p]) => ({
3127
+ name: pName,
3128
+ type: p.type,
3129
+ rawType: p.rawType,
3130
+ required: p.required,
3131
+ default: p.default,
3132
+ values: p.values
3133
+ }));
3134
+ return {
3135
+ name,
3136
+ slug,
3137
+ svg: svg ?? "",
3138
+ svgBytes,
3139
+ keywords,
3140
+ usedBy,
3141
+ jsx,
3142
+ props: ownProps,
3143
+ filePath: component.filePath
3144
+ };
3145
+ });
3146
+ const iconCards = icons.map(([name]) => {
3147
+ const svg = getIconSvg(name, data);
3148
+ const keywords = (data.manifest.components[name]?.keywords ?? []).join(" ");
3149
+ const slug = slugify(name);
3150
+ const previewHtml = svg ? `<div class="icon-cell-svg">${svg}</div>` : `<span class="icon-no-preview">${escapeHtml(name.slice(0, 2))}</span>`;
3151
+ return `<button class="icon-cell" data-name="${escapeHtml(name.toLowerCase())}" data-slug="${slug}" data-keywords="${escapeHtml(keywords.toLowerCase())}" type="button">
3152
+ <div class="icon-cell-preview">${previewHtml}</div>
3153
+ <div class="icon-cell-name">${escapeHtml(name)}</div>
3154
+ </button>`;
3155
+ }).join("\n");
3156
+ const slideOutPanel = `<div class="icon-slideout-overlay" id="icon-slideout-overlay" hidden></div>
3157
+ <div class="icon-slideout" id="icon-slideout" hidden>
3158
+ <div class="icon-slideout-header">
3159
+ <h2 class="icon-slideout-name" id="icon-slideout-name"></h2>
3160
+ <button class="icon-slideout-close" id="icon-slideout-close" type="button" aria-label="Close">
3161
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
3162
+ </button>
3163
+ </div>
3164
+
3165
+ <div class="icon-slideout-body">
3166
+ <div class="icon-slideout-preview-lg" id="icon-slideout-preview-lg"></div>
3167
+
3168
+ <div class="icon-detail-section">
3169
+ <div class="icon-detail-section-title">Sizes</div>
3170
+ <div class="icon-size-row">
3171
+ <div class="icon-size-cell"><div class="icon-size-preview" id="icon-size-lg"></div><span>lg (48)</span></div>
3172
+ <div class="icon-size-cell"><div class="icon-size-preview" id="icon-size-md"></div><span>md (32)</span></div>
3173
+ <div class="icon-size-cell"><div class="icon-size-preview" id="icon-size-sm"></div><span>sm (24)</span></div>
3174
+ <div class="icon-size-cell"><div class="icon-size-preview" id="icon-size-xs"></div><span>xs (16)</span></div>
3175
+ </div>
3176
+ </div>
3177
+
3178
+ <div class="icon-detail-section">
3179
+ <div class="icon-slideout-actions">
3180
+ <a class="icon-action-btn icon-action-primary" id="icon-slideout-link" href="#">View component page</a>
3181
+ <button class="icon-action-btn" id="icon-copy-svg" type="button">Copy SVG</button>
3182
+ <button class="icon-action-btn" id="icon-copy-jsx" type="button">Copy JSX</button>
3183
+ </div>
3184
+ </div>
3185
+
3186
+ <div class="icon-detail-section">
3187
+ <div class="icon-detail-section-title">Compiled size</div>
3188
+ <div class="icon-compiled-size" id="icon-compiled-size"></div>
3189
+ </div>
3190
+
3191
+ <div class="icon-detail-section" id="icon-props-section" hidden>
3192
+ <div class="icon-detail-section-title">Props</div>
3193
+ <div id="icon-props-list"></div>
3194
+ </div>
3195
+
3196
+ <div class="icon-detail-section" id="icon-keywords-section" hidden>
3197
+ <div class="icon-detail-section-title">Keywords</div>
3198
+ <div class="icon-keywords" id="icon-keywords"></div>
3199
+ </div>
3200
+
3201
+ <div class="icon-detail-section" id="icon-usage-section" hidden>
3202
+ <div class="icon-detail-section-title">Used by</div>
3203
+ <div class="icon-usage-list" id="icon-usage-list"></div>
3204
+ </div>
3205
+ </div>
3206
+ </div>`;
3207
+ const body = `<div class="icon-browser">
3208
+ <div class="icon-browser-header">
3209
+ <h1>Icons</h1>
3210
+ <p>${icons.length} icon${icons.length === 1 ? "" : "s"}</p>
3211
+ <input class="search-box" type="search" id="icon-search" placeholder="Search icons\u2026" style="margin-top:12px" />
3212
+ </div>
3213
+ <div class="icon-grid" id="icon-grid">
3214
+ ${iconCards}
3215
+ </div>
3216
+ </div>
3217
+ ${slideOutPanel}
3218
+ <script>
3219
+ (function () {
3220
+ var ICON_DATA = ${JSON.stringify(iconDataEntries)};
3221
+ var grid = document.getElementById('icon-grid');
3222
+ var search = document.getElementById('icon-search');
3223
+ var slideout = document.getElementById('icon-slideout');
3224
+ var overlay = document.getElementById('icon-slideout-overlay');
3225
+ var closeBtn = document.getElementById('icon-slideout-close');
3226
+ var nameEl = document.getElementById('icon-slideout-name');
3227
+ var previewLg = document.getElementById('icon-slideout-preview-lg');
3228
+ var sizeLg = document.getElementById('icon-size-lg');
3229
+ var sizeMd = document.getElementById('icon-size-md');
3230
+ var sizeSm = document.getElementById('icon-size-sm');
3231
+ var sizeXs = document.getElementById('icon-size-xs');
3232
+ var linkEl = document.getElementById('icon-slideout-link');
3233
+ var copySvgBtn = document.getElementById('icon-copy-svg');
3234
+ var copyJsxBtn = document.getElementById('icon-copy-jsx');
3235
+ var compiledSizeEl = document.getElementById('icon-compiled-size');
3236
+ var propsSection = document.getElementById('icon-props-section');
3237
+ var propsList = document.getElementById('icon-props-list');
3238
+ var keywordsSection = document.getElementById('icon-keywords-section');
3239
+ var keywordsEl = document.getElementById('icon-keywords');
3240
+ var usageSection = document.getElementById('icon-usage-section');
3241
+ var usageList = document.getElementById('icon-usage-list');
3242
+ var basePath = ${JSON.stringify(data.options.basePath)};
3243
+ var currentData = null;
3244
+
3245
+ if (!grid) return;
3246
+
3247
+ function filter(q) {
3248
+ var cells = grid.querySelectorAll('.icon-cell');
3249
+ cells.forEach(function (cell) {
3250
+ var name = cell.getAttribute('data-name') || '';
3251
+ var kw = cell.getAttribute('data-keywords') || '';
3252
+ var show = name.includes(q) || kw.includes(q);
3253
+ cell.style.display = show ? '' : 'none';
3254
+ });
3255
+ }
3256
+
3257
+ if (search) {
3258
+ search.addEventListener('input', function () { filter(search.value.toLowerCase()); });
3259
+ }
3260
+
3261
+ function setSvgSize(container, svg, px) {
3262
+ if (!container || !svg) return;
3263
+ container.innerHTML = svg;
3264
+ var svgEl = container.querySelector('svg');
3265
+ if (svgEl) { svgEl.setAttribute('width', String(px)); svgEl.setAttribute('height', String(px)); }
3266
+ }
3267
+
3268
+ function openSlideout(slug) {
3269
+ var iconData = ICON_DATA.find(function (d) { return d.slug === slug; });
3270
+ if (!iconData || !slideout) return;
3271
+ currentData = iconData;
3272
+
3273
+ if (nameEl) nameEl.textContent = iconData.name;
3274
+ if (previewLg) previewLg.innerHTML = iconData.svg;
3275
+
3276
+ setSvgSize(sizeLg, iconData.svg, 48);
3277
+ setSvgSize(sizeMd, iconData.svg, 32);
3278
+ setSvgSize(sizeSm, iconData.svg, 24);
3279
+ setSvgSize(sizeXs, iconData.svg, 16);
3280
+
3281
+ if (linkEl) linkEl.href = basePath + iconData.slug + '.html';
3282
+
3283
+ if (compiledSizeEl) {
3284
+ var bytes = iconData.svgBytes;
3285
+ var label = bytes < 1024 ? bytes + ' B' : (bytes / 1024).toFixed(1) + ' KB';
3286
+ compiledSizeEl.textContent = label;
3287
+ }
3288
+
3289
+ if (propsSection && propsList) {
3290
+ if (iconData.props.length > 0) {
3291
+ propsSection.hidden = false;
3292
+ propsList.innerHTML = iconData.props.map(function (p) {
3293
+ var typeStr = p.values && p.values.length > 0 ? p.values.join(' | ') : p.rawType;
3294
+ var reqStr = p.required ? '<span class="icon-prop-required">required</span>' : '<span class="icon-prop-optional">optional</span>';
3295
+ var defStr = p.default ? ' <span class="icon-prop-default">' + p.default + '</span>' : '';
3296
+ return '<div class="icon-prop-row"><div class="icon-prop-name">' + p.name + '</div><div class="icon-prop-type">' + typeStr + '</div><div class="icon-prop-meta">' + reqStr + defStr + '</div></div>';
3297
+ }).join('');
3298
+ } else {
3299
+ propsSection.hidden = true;
3300
+ }
3301
+ }
3302
+
3303
+ if (keywordsSection && keywordsEl) {
3304
+ if (iconData.keywords.length > 0) {
3305
+ keywordsSection.hidden = false;
3306
+ keywordsEl.innerHTML = iconData.keywords.map(function (kw) {
3307
+ return '<span class="icon-keyword-tag">' + kw + '</span>';
3308
+ }).join('');
3309
+ } else {
3310
+ keywordsSection.hidden = true;
3311
+ }
3312
+ }
3313
+
3314
+ if (usageSection && usageList) {
3315
+ if (iconData.usedBy.length > 0) {
3316
+ usageSection.hidden = false;
3317
+ usageList.innerHTML = iconData.usedBy.map(function (u) {
3318
+ var uSlug = u.replace(/([A-Z])/g, function (m) { return '-' + m.toLowerCase(); }).replace(/^-/, '').replace(/[^a-z0-9-]/g, '-');
3319
+ return '<a class="icon-usage-link" href="' + basePath + uSlug + '.html">' + u + '</a>';
3320
+ }).join('');
3321
+ } else {
3322
+ usageSection.hidden = true;
3323
+ }
3324
+ }
3325
+
3326
+ grid.querySelectorAll('.icon-cell.selected').forEach(function (el) { el.classList.remove('selected'); });
3327
+ var activeCell = grid.querySelector('[data-slug="' + slug + '"]');
3328
+ if (activeCell) activeCell.classList.add('selected');
3329
+
3330
+ slideout.hidden = false;
3331
+ if (overlay) overlay.hidden = false;
3332
+ }
3333
+
3334
+ function closeSlideout() {
3335
+ if (slideout) slideout.hidden = true;
3336
+ if (overlay) overlay.hidden = true;
3337
+ currentData = null;
3338
+ grid.querySelectorAll('.icon-cell.selected').forEach(function (el) { el.classList.remove('selected'); });
3339
+ }
3340
+
3341
+ grid.addEventListener('click', function (e) {
3342
+ var cell = e.target.closest('.icon-cell');
3343
+ if (!cell) return;
3344
+ var slug = cell.getAttribute('data-slug');
3345
+ if (slug) openSlideout(slug);
3346
+ });
3347
+
3348
+ if (closeBtn) closeBtn.addEventListener('click', closeSlideout);
3349
+ if (overlay) overlay.addEventListener('click', closeSlideout);
3350
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeSlideout(); });
3351
+
3352
+ function copyToClipboard(text, btn) {
3353
+ navigator.clipboard.writeText(text).then(function () {
3354
+ var orig = btn.textContent;
3355
+ btn.textContent = 'Copied!';
3356
+ setTimeout(function () { btn.textContent = orig; }, 1500);
3357
+ });
3358
+ }
3359
+
3360
+ if (copySvgBtn) {
3361
+ copySvgBtn.addEventListener('click', function () {
3362
+ if (currentData && currentData.svg) copyToClipboard(currentData.svg, copySvgBtn);
3363
+ });
3364
+ }
3365
+ if (copyJsxBtn) {
3366
+ copyJsxBtn.addEventListener('click', function () {
3367
+ if (currentData && currentData.jsx) copyToClipboard(currentData.jsx, copyJsxBtn);
3368
+ });
3369
+ }
3370
+ })();
3371
+ </script>`;
3372
+ const sidebar = sidebarLinks(data, "icons");
3373
+ return htmlShell({
3374
+ title: `Icons \u2014 ${data.options.title}`,
3375
+ body,
3376
+ sidebar,
3377
+ onThisPage: '<a href="#top">Icon Browser</a>',
3378
+ basePath: data.options.basePath,
3379
+ searchItems: buildSearchItems(data),
3380
+ breadcrumbs: [{ label: "Icons" }]
3381
+ });
3382
+ }
3383
+
3384
+ // src/templates/token-browser.ts
3385
+ var HEX_RE = /^#(?:[0-9a-fA-F]{3,8})$/;
3386
+ var COLOR_FN_RE = /^(?:rgba?|hsla?|oklch|oklab|lch|lab|color|hwb)\(/;
3387
+ function isColorValue(value) {
3388
+ return HEX_RE.test(value) || COLOR_FN_RE.test(value);
3389
+ }
3390
+ function estimateLuminance(value) {
3391
+ const v = value.trim();
3392
+ if (HEX_RE.test(v)) {
3393
+ const c = v.replace("#", "");
3394
+ const full = c.length <= 4 ? c.slice(0, 3).split("").map((ch) => ch + ch).join("") : c.slice(0, 6);
3395
+ const r = parseInt(full.slice(0, 2), 16);
3396
+ const g = parseInt(full.slice(2, 4), 16);
3397
+ const b = parseInt(full.slice(4, 6), 16);
3398
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
3399
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
3400
+ }
3401
+ const rgbMatch = v.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/);
3402
+ if (rgbMatch) {
3403
+ const r = parseFloat(rgbMatch[1] ?? "0");
3404
+ const g = parseFloat(rgbMatch[2] ?? "0");
3405
+ const b = parseFloat(rgbMatch[3] ?? "0");
3406
+ const alphaMatch = v.match(/,\s*([\d.]+)\s*\)$/);
3407
+ const alpha = alphaMatch ? parseFloat(alphaMatch[1] ?? "1") : 1;
3408
+ const blendR = r * alpha + 255 * (1 - alpha);
3409
+ const blendG = g * alpha + 255 * (1 - alpha);
3410
+ const blendB = b * alpha + 255 * (1 - alpha);
3411
+ return (0.299 * blendR + 0.587 * blendG + 0.114 * blendB) / 255;
3412
+ }
3413
+ const oklchMatch = v.match(/^oklch\(\s*([\d.]+)/);
3414
+ if (oklchMatch) {
3415
+ const L = parseFloat(oklchMatch[1] ?? "0.5");
3416
+ return L;
3417
+ }
3418
+ const oklabMatch = v.match(/^oklab\(\s*([\d.]+)/);
3419
+ if (oklabMatch) {
3420
+ return parseFloat(oklabMatch[1] ?? "0.5");
3421
+ }
3422
+ const hslMatch = v.match(/^hsla?\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*([\d.]+)%/);
3423
+ if (hslMatch) {
3424
+ return parseFloat(hslMatch[1] ?? "50") / 100;
3425
+ }
3426
+ const colorMixMatch = v.match(/^color-mix\(.*black\s+([\d.]+)%/);
3427
+ if (colorMixMatch) {
3428
+ const blackPct = parseFloat(colorMixMatch[1] ?? "50") / 100;
3429
+ return 1 - blackPct;
3430
+ }
3431
+ return null;
3432
+ }
3433
+ function contrastColor(value) {
3434
+ const lum = estimateLuminance(value);
3435
+ if (lum === null) return "#fff";
3436
+ return lum > 0.55 ? "#000" : "#fff";
3437
+ }
3438
+ function groupByTopLevel(tokens) {
3439
+ const map = /* @__PURE__ */ new Map();
3440
+ for (const token of tokens) {
3441
+ const top = token.path.split(".")[0] ?? "other";
3442
+ if (!map.has(top)) map.set(top, []);
3443
+ map.get(top)?.push(token);
3444
+ }
3445
+ return Array.from(map.entries()).map(([name, toks]) => ({ name, tokens: toks }));
3446
+ }
3447
+ function renderSwatch(token, darkValue) {
3448
+ const label = token.path.split(".").pop() ?? "";
3449
+ const fg = contrastColor(token.value);
3450
+ const darkHtml = darkValue ? (() => {
3451
+ const darkFg = contrastColor(darkValue);
3452
+ return `<div class="tb-swatch-dark" style="background:${escapeHtml(darkValue)};color:${darkFg}" title="dark: ${escapeHtml(darkValue)}">
3453
+ <span class="tb-swatch-value">${escapeHtml(darkValue)}</span>
3454
+ </div>`;
3455
+ })() : "";
3456
+ return `<div class="tb-swatch-pair" title="${escapeHtml(token.path)}: ${escapeHtml(token.value)}">
3457
+ <div class="tb-swatch" style="background:${escapeHtml(token.value)};color:${fg}">
3458
+ <span class="tb-swatch-label">${escapeHtml(label)}</span>
3459
+ <span class="tb-swatch-value">${escapeHtml(token.value)}</span>
3460
+ </div>${darkHtml}
3461
+ </div>`;
3462
+ }
3463
+ function renderColorGroup(group, darkOverrides) {
3464
+ const subgroups = /* @__PURE__ */ new Map();
3465
+ const standalone = [];
3466
+ for (const token of group.tokens) {
3467
+ const parts = token.path.split(".");
3468
+ if (parts.length >= 3) {
3469
+ const sub = parts.slice(0, 2).join(".");
3470
+ if (!subgroups.has(sub)) subgroups.set(sub, []);
3471
+ subgroups.get(sub)?.push(token);
3472
+ } else {
3473
+ standalone.push(token);
3474
+ }
3475
+ }
3476
+ const scaleHtml = Array.from(subgroups.entries()).map(([scaleName, tokens]) => {
3477
+ const hasDark = tokens.some((t) => darkOverrides?.[t.path] !== void 0);
3478
+ const swatches = tokens.map((t) => renderSwatch(t, darkOverrides?.[t.path])).join("\n");
3479
+ const displayName = scaleName.split(".").pop() ?? scaleName;
3480
+ const darkLabel = hasDark ? `<span class="tb-scale-badge">dark</span>` : "";
3481
+ return `<div class="tb-scale">
3482
+ <div class="tb-scale-name">${escapeHtml(displayName)}${darkLabel}</div>
3483
+ <div class="tb-scale-row">${swatches}</div>
3484
+ </div>`;
3485
+ }).join("\n");
3486
+ const standaloneHtml = standalone.length > 0 ? `<div class="tb-standalone-colors">${standalone.map((t) => {
3487
+ const label = t.path.split(".").pop() ?? t.path;
3488
+ const darkVal = darkOverrides?.[t.path];
3489
+ const fg = contrastColor(t.value);
3490
+ let darkPart = "";
3491
+ if (darkVal) {
3492
+ const darkFg = contrastColor(darkVal);
3493
+ darkPart = `<div class="tb-swatch-dark-single" style="background:${escapeHtml(darkVal)};color:${darkFg}" title="dark: ${escapeHtml(darkVal)}"><span class="tb-swatch-value">${escapeHtml(darkVal)}</span></div>`;
3494
+ }
3495
+ return `<div class="tb-swatch-pair-single">
3496
+ <div class="tb-swatch tb-swatch-single" style="background:${escapeHtml(t.value)};color:${fg}" title="${escapeHtml(t.path)}: ${escapeHtml(t.value)}">
3497
+ <span class="tb-swatch-label">${escapeHtml(label)}</span>
3498
+ <span class="tb-swatch-value">${escapeHtml(t.value)}</span>
3499
+ </div>${darkPart}
3500
+ </div>`;
3501
+ }).join("\n")}</div>` : "";
3502
+ return `<div class="tb-section" id="tb-${escapeHtml(group.name)}">
3503
+ <h2 class="tb-section-title">${escapeHtml(group.name)}</h2>
3504
+ ${scaleHtml}${standaloneHtml}
3505
+ </div>`;
3506
+ }
3507
+ function renderSpacingGroup(group) {
3508
+ const rows = group.tokens.map((t) => {
3509
+ const label = t.path.split(".").slice(1).join(".") || t.path;
3510
+ return `<div class="tb-spacing-row">
3511
+ <div class="tb-spacing-label">${escapeHtml(label)}</div>
3512
+ <div class="tb-spacing-value">${escapeHtml(t.value)}</div>
3513
+ <div class="tb-spacing-bar-wrapper"><div class="tb-spacing-bar" style="width:min(${escapeHtml(t.value)}, 100%)"></div></div>
3514
+ </div>`;
3515
+ }).join("\n");
3516
+ return `<div class="tb-section" id="tb-${escapeHtml(group.name)}">
3517
+ <h2 class="tb-section-title">${escapeHtml(group.name)}</h2>
3518
+ ${rows}
3519
+ </div>`;
3520
+ }
3521
+ function renderTypographyGroup(group) {
3522
+ const rows = group.tokens.map((t) => {
3523
+ const label = t.path.split(".").slice(1).join(".") || t.path;
3524
+ const sampleStyle = t.type === "fontFamily" ? `font-family:${escapeHtml(t.value)}` : t.type === "dimension" ? `font-size:${escapeHtml(t.value)}` : "";
3525
+ return `<div class="tb-typo-row">
3526
+ <div class="tb-typo-label">${escapeHtml(label)}</div>
3527
+ <div class="tb-typo-value">${escapeHtml(t.value)}</div>
3528
+ ${sampleStyle ? `<div class="tb-typo-sample" style="${sampleStyle}">Aa</div>` : ""}
3529
+ </div>`;
3530
+ }).join("\n");
3531
+ return `<div class="tb-section" id="tb-${escapeHtml(group.name)}">
3532
+ <h2 class="tb-section-title">${escapeHtml(group.name)}</h2>
3533
+ ${rows}
3534
+ </div>`;
3535
+ }
3536
+ function renderGenericGroup(group, darkOverrides) {
3537
+ const isAllColors = group.tokens.every((t) => isColorValue(t.value));
3538
+ if (isAllColors) return renderColorGroup(group, darkOverrides);
3539
+ const hasDarkColumn = group.tokens.some((t) => darkOverrides?.[t.path] !== void 0);
3540
+ const rows = group.tokens.map((t) => {
3541
+ const label = t.path.split(".").slice(1).join(".") || t.path;
3542
+ const swatchHtml = isColorValue(t.value) ? `<span class="token-value-swatch" style="background:${escapeHtml(t.value)}"></span>` : "";
3543
+ const darkVal = darkOverrides?.[t.path];
3544
+ const darkCell = hasDarkColumn ? `<td class="tb-generic-value">${darkVal ? `${isColorValue(darkVal) ? `<span class="token-value-swatch" style="background:${escapeHtml(darkVal)}"></span>` : ""}${escapeHtml(darkVal)}` : `<span style="color:var(--color-muted)">\u2014</span>`}</td>` : "";
3545
+ return `<tr>
3546
+ <td class="tb-generic-path">${escapeHtml(label)}</td>
3547
+ <td class="tb-generic-value">${swatchHtml}${escapeHtml(t.value)}</td>
3548
+ ${darkCell}
3549
+ <td class="tb-generic-type">${escapeHtml(t.type)}</td>
3550
+ </tr>`;
3551
+ }).join("\n");
3552
+ const darkHeader = hasDarkColumn ? "<th>Dark</th>" : "";
3553
+ return `<div class="tb-section" id="tb-${escapeHtml(group.name)}">
3554
+ <h2 class="tb-section-title">${escapeHtml(group.name)}</h2>
3555
+ <table class="tb-generic-table">
3556
+ <thead><tr><th>Token</th><th>Value</th>${darkHeader}<th>Type</th></tr></thead>
3557
+ <tbody>${rows}</tbody>
3558
+ </table>
3559
+ </div>`;
3560
+ }
3561
+ function renderGroup(group, darkOverrides) {
3562
+ const allTypes = new Set(group.tokens.map((t) => t.type));
3563
+ const allColors = group.tokens.every((t) => t.type === "color" || isColorValue(t.value));
3564
+ const allDimensions = group.tokens.every((t) => t.type === "dimension");
3565
+ const hasTypography = allTypes.has("fontFamily") || allTypes.has("fontWeight");
3566
+ if (allColors && group.tokens.length > 0) return renderColorGroup(group, darkOverrides);
3567
+ if (allDimensions && group.tokens.length > 0) return renderSpacingGroup(group);
3568
+ if (hasTypography) return renderTypographyGroup(group);
3569
+ return renderGenericGroup(group, darkOverrides);
3570
+ }
3571
+ function renderTokenBrowser(data) {
3572
+ const tokens = data.tokenEntries ?? [];
3573
+ if (tokens.length === 0) {
3574
+ const body2 = `<div class="tb-header">
3575
+ <h1>Design Tokens</h1>
3576
+ <p>No tokens loaded. Run <code>scope tokens init</code> to generate a token file, then pass <code>--tokens</code> to <code>scope site build</code>.</p>
3577
+ </div>`;
3578
+ return htmlShell({
3579
+ title: `Tokens \u2014 ${data.options.title}`,
3580
+ body: body2,
3581
+ sidebar: sidebarLinks(data, "tokens"),
3582
+ onThisPage: "",
3583
+ basePath: data.options.basePath,
3584
+ searchItems: buildSearchItems(data),
3585
+ breadcrumbs: [{ label: "Tokens" }]
3586
+ });
3587
+ }
3588
+ const groups = groupByTopLevel(tokens);
3589
+ const themes = data.tokenThemes ?? {};
3590
+ const darkOverrides = {
3591
+ ...themes["dark"],
3592
+ ...themes["dark-high-contrast"]
3593
+ };
3594
+ const darkCount = Object.keys(darkOverrides).length;
3595
+ const colorGroups = groups.filter(
3596
+ (g) => g.tokens.every((t) => t.type === "color" || isColorValue(t.value))
3597
+ );
3598
+ const otherGroups = groups.filter(
3599
+ (g) => !g.tokens.every((t) => t.type === "color" || isColorValue(t.value))
3600
+ );
3601
+ const totalColors = colorGroups.reduce((sum, g) => sum + g.tokens.length, 0);
3602
+ const totalOther = otherGroups.reduce((sum, g) => sum + g.tokens.length, 0);
3603
+ const meta = data.tokenMeta;
3604
+ const subtitle = meta?.name ? ` \u2014 ${escapeHtml(meta.name)}` : "";
3605
+ const lastUpdated = meta?.lastUpdated ? `<span class="tb-updated">Last updated: ${escapeHtml(meta.lastUpdated)}</span>` : "";
3606
+ const darkStatCard = darkCount > 0 ? `<div class="stat-card"><div class="stat-label">Dark Overrides</div><div class="stat-value">${darkCount}</div></div>` : "";
3607
+ const statsHtml = `<div class="stats-grid" style="margin-bottom:32px">
3608
+ <div class="stat-card"><div class="stat-label">Total Tokens</div><div class="stat-value">${tokens.length}</div></div>
3609
+ <div class="stat-card"><div class="stat-label">Colors</div><div class="stat-value">${totalColors}</div></div>
3610
+ <div class="stat-card"><div class="stat-label">Other</div><div class="stat-value">${totalOther}</div></div>
3611
+ <div class="stat-card"><div class="stat-label">Groups</div><div class="stat-value">${groups.length}</div></div>
3612
+ ${darkStatCard}
3613
+ </div>`;
3614
+ const searchHtml = `<input class="search-box" type="search" id="tb-search" placeholder="Search tokens\u2026" style="margin-bottom:24px;width:100%;max-width:400px" />`;
3615
+ const sectionsHtml = groups.map((g) => renderGroup(g, darkCount > 0 ? darkOverrides : void 0)).join("\n");
3616
+ const onThisPage = groups.map(
3617
+ (g) => `<a href="#tb-${escapeHtml(g.name)}">${escapeHtml(g.name)} <span style="opacity:0.5;font-size:10px">(${g.tokens.length})</span></a>`
3618
+ ).join("\n");
3619
+ const body = `<div class="tb-header">
3620
+ <h1>Design Tokens${subtitle}</h1>
3621
+ <p>${tokens.length} tokens across ${groups.length} groups${lastUpdated ? ` \xB7 ${lastUpdated}` : ""}</p>
3622
+ </div>
3623
+ ${statsHtml}
3624
+ ${searchHtml}
3625
+ <div id="tb-sections">
3626
+ ${sectionsHtml}
3627
+ </div>
3628
+ <script>
3629
+ (function () {
3630
+ var search = document.getElementById('tb-search');
3631
+ var sections = document.getElementById('tb-sections');
3632
+ if (!search || !sections) return;
3633
+
3634
+ search.addEventListener('input', function () {
3635
+ var q = search.value.toLowerCase();
3636
+ var swatches = sections.querySelectorAll('.tb-swatch, .tb-spacing-row, .tb-typo-row, .tb-generic-table tr');
3637
+ var sectionEls = sections.querySelectorAll('.tb-section');
3638
+
3639
+ if (!q) {
3640
+ swatches.forEach(function (el) { el.style.display = ''; });
3641
+ sectionEls.forEach(function (el) { el.style.display = ''; });
3642
+ return;
3643
+ }
3644
+
3645
+ swatches.forEach(function (el) {
3646
+ var title = (el.getAttribute('title') || el.textContent || '').toLowerCase();
3647
+ el.style.display = title.includes(q) ? '' : 'none';
3648
+ });
3649
+
3650
+ sectionEls.forEach(function (sec) {
3651
+ var visible = sec.querySelectorAll('.tb-swatch:not([style*="display: none"]), .tb-spacing-row:not([style*="display: none"]), .tb-typo-row:not([style*="display: none"]), .tb-generic-table tr:not([style*="display: none"])');
3652
+ sec.style.display = visible.length > 0 ? '' : 'none';
3653
+ });
3654
+ });
3655
+ })();
3656
+ </script>`;
3657
+ return htmlShell({
3658
+ title: `Tokens \u2014 ${data.options.title}`,
3659
+ body,
3660
+ sidebar: sidebarLinks(data, "tokens"),
3661
+ onThisPage,
3662
+ basePath: data.options.basePath,
3663
+ searchItems: buildSearchItems(data),
3664
+ breadcrumbs: [{ label: "Tokens" }]
1158
3665
  });
1159
3666
  }
1160
3667
 
@@ -1179,8 +3686,21 @@ async function buildSite(options) {
1179
3686
  const dashboardHtml = renderDashboard(data);
1180
3687
  await promises.writeFile(path.join(normalizedOptions.outputDir, "dashboard.html"), dashboardHtml, "utf-8");
1181
3688
  console.log(`[scope/site] \u2713 dashboard.html`);
3689
+ let extraPages = 0;
3690
+ if (normalizedOptions.iconPatterns.length > 0) {
3691
+ const iconsHtml = renderIconBrowser(data);
3692
+ await promises.writeFile(path.join(normalizedOptions.outputDir, "icons.html"), iconsHtml, "utf-8");
3693
+ console.log(`[scope/site] \u2713 icons.html`);
3694
+ extraPages++;
3695
+ }
3696
+ if (data.tokenEntries && data.tokenEntries.length > 0) {
3697
+ const tokensHtml = renderTokenBrowser(data);
3698
+ await promises.writeFile(path.join(normalizedOptions.outputDir, "tokens.html"), tokensHtml, "utf-8");
3699
+ console.log(`[scope/site] \u2713 tokens.html (${data.tokenEntries.length} tokens)`);
3700
+ extraPages++;
3701
+ }
1182
3702
  console.log(
1183
- `[scope/site] Done. Site written to ${normalizedOptions.outputDir} (${componentNames.length + 2} files).`
3703
+ `[scope/site] Done. Site written to ${normalizedOptions.outputDir} (${componentNames.length + 2 + extraPages} files).`
1184
3704
  );
1185
3705
  }
1186
3706