@vizejs/vite-plugin-musea 0.0.1-alpha.100

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.js ADDED
@@ -0,0 +1,2760 @@
1
+ import { MuseaA11yRunner } from "./a11y-C6xqILwZ.js";
2
+ import { generateArtFile, writeArtFile } from "./autogen-ymQnARZK.js";
3
+ import { MuseaVrtRunner, generateVrtJsonReport, generateVrtReport } from "./vrt-DP87vGIA.js";
4
+ import path from "node:path";
5
+ import { createRequire } from "node:module";
6
+ import fs from "node:fs";
7
+ import { vizeConfigStore } from "@vizejs/vite-plugin";
8
+
9
+ //#region src/style-dictionary.ts
10
+ /**
11
+ * Parse Style Dictionary tokens file.
12
+ */
13
+ async function parseTokens(tokensPath) {
14
+ const absolutePath = path.resolve(tokensPath);
15
+ const stat = await fs.promises.stat(absolutePath);
16
+ if (stat.isDirectory()) return parseTokenDirectory(absolutePath);
17
+ const content = await fs.promises.readFile(absolutePath, "utf-8");
18
+ const tokens = JSON.parse(content);
19
+ return flattenTokens(tokens);
20
+ }
21
+ /**
22
+ * Parse tokens from a directory.
23
+ */
24
+ async function parseTokenDirectory(dirPath) {
25
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
26
+ const categories = [];
27
+ for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
28
+ const filePath = path.join(dirPath, entry.name);
29
+ const content = await fs.promises.readFile(filePath, "utf-8");
30
+ const tokens = JSON.parse(content);
31
+ const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
32
+ categories.push({
33
+ name: formatCategoryName(categoryName),
34
+ tokens: extractTokens(tokens),
35
+ subcategories: extractSubcategories(tokens)
36
+ });
37
+ }
38
+ return categories;
39
+ }
40
+ /**
41
+ * Flatten nested token structure into categories.
42
+ */
43
+ function flattenTokens(tokens, prefix = []) {
44
+ const categories = [];
45
+ for (const [key, value] of Object.entries(tokens)) {
46
+ if (isTokenValue(value)) continue;
47
+ if (typeof value === "object" && value !== null) {
48
+ const categoryTokens = extractTokens(value);
49
+ const subcategories = flattenTokens(value, [...prefix, key]);
50
+ if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
51
+ name: formatCategoryName(key),
52
+ tokens: categoryTokens,
53
+ subcategories: subcategories.length > 0 ? subcategories : void 0
54
+ });
55
+ }
56
+ }
57
+ return categories;
58
+ }
59
+ /**
60
+ * Extract token values from an object.
61
+ */
62
+ function extractTokens(obj) {
63
+ const tokens = {};
64
+ for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
65
+ return tokens;
66
+ }
67
+ /**
68
+ * Extract subcategories from an object.
69
+ */
70
+ function extractSubcategories(obj) {
71
+ const subcategories = [];
72
+ for (const [key, value] of Object.entries(obj)) if (!isTokenValue(value) && typeof value === "object" && value !== null) {
73
+ const categoryTokens = extractTokens(value);
74
+ const nested = extractSubcategories(value);
75
+ if (Object.keys(categoryTokens).length > 0 || nested && nested.length > 0) subcategories.push({
76
+ name: formatCategoryName(key),
77
+ tokens: categoryTokens,
78
+ subcategories: nested
79
+ });
80
+ }
81
+ return subcategories.length > 0 ? subcategories : void 0;
82
+ }
83
+ /**
84
+ * Check if a value is a token definition.
85
+ */
86
+ function isTokenValue(value) {
87
+ if (typeof value !== "object" || value === null) return false;
88
+ const obj = value;
89
+ return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number");
90
+ }
91
+ /**
92
+ * Normalize token to DesignToken interface.
93
+ */
94
+ function normalizeToken(raw) {
95
+ const token = {
96
+ value: raw.value,
97
+ type: raw.type,
98
+ description: raw.description,
99
+ attributes: raw.attributes
100
+ };
101
+ if (raw.$tier === "primitive" || raw.$tier === "semantic") token.$tier = raw.$tier;
102
+ if (typeof raw.$reference === "string") token.$reference = raw.$reference;
103
+ return token;
104
+ }
105
+ /**
106
+ * Format category name for display.
107
+ */
108
+ function formatCategoryName(name) {
109
+ return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
110
+ }
111
+ /**
112
+ * Flatten nested categories into a flat map keyed by dot-path.
113
+ */
114
+ function buildTokenMap(categories, prefix = []) {
115
+ const map = {};
116
+ for (const cat of categories) {
117
+ const catKey = cat.name.toLowerCase().replace(/\s+/g, "-");
118
+ const catPath = [...prefix, catKey];
119
+ for (const [name, token] of Object.entries(cat.tokens)) {
120
+ const dotPath = [...catPath, name].join(".");
121
+ map[dotPath] = token;
122
+ }
123
+ if (cat.subcategories) {
124
+ const subMap = buildTokenMap(cat.subcategories, catPath);
125
+ Object.assign(map, subMap);
126
+ }
127
+ }
128
+ return map;
129
+ }
130
+ const REFERENCE_PATTERN = /^\{(.+)\}$/;
131
+ const MAX_RESOLVE_DEPTH = 10;
132
+ /**
133
+ * Resolve references in categories, setting $tier, $reference, and $resolvedValue.
134
+ */
135
+ function resolveReferences(categories, tokenMap) {
136
+ for (const cat of categories) {
137
+ for (const token of Object.values(cat.tokens)) resolveTokenReference(token, tokenMap);
138
+ if (cat.subcategories) resolveReferences(cat.subcategories, tokenMap);
139
+ }
140
+ }
141
+ function resolveTokenReference(token, tokenMap) {
142
+ if (typeof token.value === "string") {
143
+ const match = token.value.match(REFERENCE_PATTERN);
144
+ if (match) {
145
+ token.$tier = token.$tier ?? "semantic";
146
+ token.$reference = match[1];
147
+ token.$resolvedValue = resolveValue(match[1], tokenMap, 0, new Set());
148
+ return;
149
+ }
150
+ }
151
+ token.$tier = token.$tier ?? "primitive";
152
+ }
153
+ function resolveValue(ref, tokenMap, depth, visited) {
154
+ if (depth >= MAX_RESOLVE_DEPTH || visited.has(ref)) return void 0;
155
+ visited.add(ref);
156
+ const target = tokenMap[ref];
157
+ if (!target) return void 0;
158
+ if (typeof target.value === "string") {
159
+ const match = target.value.match(REFERENCE_PATTERN);
160
+ if (match) return resolveValue(match[1], tokenMap, depth + 1, visited);
161
+ }
162
+ return target.value;
163
+ }
164
+ /**
165
+ * Read raw JSON token file.
166
+ */
167
+ async function readRawTokenFile(tokensPath) {
168
+ const content = await fs.promises.readFile(tokensPath, "utf-8");
169
+ return JSON.parse(content);
170
+ }
171
+ /**
172
+ * Write raw JSON token file atomically (write tmp, rename).
173
+ */
174
+ async function writeRawTokenFile(tokensPath, data) {
175
+ const tmpPath = tokensPath + ".tmp";
176
+ await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
177
+ await fs.promises.rename(tmpPath, tokensPath);
178
+ }
179
+ /**
180
+ * Set a token at a dot-separated path in the raw JSON structure.
181
+ */
182
+ function setTokenAtPath(data, dotPath, token) {
183
+ const parts = dotPath.split(".");
184
+ let current = data;
185
+ for (let i = 0; i < parts.length - 1; i++) {
186
+ const key = parts[i];
187
+ if (typeof current[key] !== "object" || current[key] === null) current[key] = {};
188
+ current = current[key];
189
+ }
190
+ const leafKey = parts[parts.length - 1];
191
+ const raw = { value: token.value };
192
+ if (token.type) raw.type = token.type;
193
+ if (token.description) raw.description = token.description;
194
+ if (token.$tier) raw.$tier = token.$tier;
195
+ if (token.$reference) raw.$reference = token.$reference;
196
+ if (token.attributes) raw.attributes = token.attributes;
197
+ current[leafKey] = raw;
198
+ }
199
+ /**
200
+ * Delete a token at a dot-separated path, cleaning empty parents.
201
+ */
202
+ function deleteTokenAtPath(data, dotPath) {
203
+ const parts = dotPath.split(".");
204
+ const parents = [];
205
+ let current = data;
206
+ for (let i = 0; i < parts.length - 1; i++) {
207
+ const key = parts[i];
208
+ if (typeof current[key] !== "object" || current[key] === null) return false;
209
+ parents.push({
210
+ obj: current,
211
+ key
212
+ });
213
+ current = current[key];
214
+ }
215
+ const leafKey = parts[parts.length - 1];
216
+ if (!(leafKey in current)) return false;
217
+ delete current[leafKey];
218
+ for (let i = parents.length - 1; i >= 0; i--) {
219
+ const { obj, key } = parents[i];
220
+ const child = obj[key];
221
+ if (Object.keys(child).length === 0) delete obj[key];
222
+ else break;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Validate that a semantic reference points to an existing token and has no cycles.
228
+ */
229
+ function validateSemanticReference(tokenMap, reference, selfPath) {
230
+ if (!tokenMap[reference]) return {
231
+ valid: false,
232
+ error: `Reference target "${reference}" does not exist`
233
+ };
234
+ const visited = new Set();
235
+ if (selfPath) visited.add(selfPath);
236
+ let current = reference;
237
+ let depth = 0;
238
+ while (depth < MAX_RESOLVE_DEPTH) {
239
+ if (visited.has(current)) return {
240
+ valid: false,
241
+ error: `Circular reference detected at "${current}"`
242
+ };
243
+ visited.add(current);
244
+ const target = tokenMap[current];
245
+ if (!target) break;
246
+ if (typeof target.value === "string") {
247
+ const match = target.value.match(REFERENCE_PATTERN);
248
+ if (match) {
249
+ current = match[1];
250
+ depth++;
251
+ continue;
252
+ }
253
+ }
254
+ break;
255
+ }
256
+ if (depth >= MAX_RESOLVE_DEPTH) return {
257
+ valid: false,
258
+ error: "Reference chain too deep (max 10)"
259
+ };
260
+ return { valid: true };
261
+ }
262
+ /**
263
+ * Find all tokens that reference the given path.
264
+ */
265
+ function findDependentTokens(tokenMap, targetPath) {
266
+ const dependents = [];
267
+ for (const [path$1, token] of Object.entries(tokenMap)) if (typeof token.value === "string") {
268
+ const match = token.value.match(REFERENCE_PATTERN);
269
+ if (match && match[1] === targetPath) dependents.push(path$1);
270
+ }
271
+ return dependents;
272
+ }
273
+ /**
274
+ * Generate HTML documentation for tokens.
275
+ */
276
+ function generateTokensHtml(categories) {
277
+ const renderToken = (name, token) => {
278
+ const isColor = typeof token.value === "string" && (token.value.startsWith("#") || token.value.startsWith("rgb") || token.value.startsWith("hsl") || token.type === "color");
279
+ return `
280
+ <div class="token">
281
+ <div class="token-preview">
282
+ ${isColor ? `<div class="color-swatch" style="background: ${token.value}"></div>` : ""}
283
+ </div>
284
+ <div class="token-info">
285
+ <div class="token-name">${name}</div>
286
+ <div class="token-value">${token.value}</div>
287
+ ${token.description ? `<div class="token-description">${token.description}</div>` : ""}
288
+ </div>
289
+ </div>
290
+ `;
291
+ };
292
+ const renderCategory = (category, level = 2) => {
293
+ const heading = `h${Math.min(level, 6)}`;
294
+ let html = `<${heading}>${category.name}</${heading}>`;
295
+ html += "<div class=\"tokens-grid\">";
296
+ for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
297
+ html += "</div>";
298
+ if (category.subcategories) for (const sub of category.subcategories) html += renderCategory(sub, level + 1);
299
+ return html;
300
+ };
301
+ return `<!DOCTYPE html>
302
+ <html lang="en">
303
+ <head>
304
+ <meta charset="UTF-8">
305
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
306
+ <title>Design Tokens - Musea</title>
307
+ <style>
308
+ :root {
309
+ --musea-bg: #0d0d0d;
310
+ --musea-bg-secondary: #1a1815;
311
+ --musea-text: #e6e9f0;
312
+ --musea-text-muted: #7b8494;
313
+ --musea-accent: #a34828;
314
+ --musea-border: #3a3530;
315
+ }
316
+ * { box-sizing: border-box; margin: 0; padding: 0; }
317
+ body {
318
+ font-family: 'Inter', -apple-system, sans-serif;
319
+ background: var(--musea-bg);
320
+ color: var(--musea-text);
321
+ line-height: 1.6;
322
+ padding: 2rem;
323
+ }
324
+ h1 { margin-bottom: 2rem; color: var(--musea-accent); }
325
+ h2 { margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--musea-border); }
326
+ h3, h4, h5, h6 { margin: 1.5rem 0 0.75rem; }
327
+ .tokens-grid {
328
+ display: grid;
329
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
330
+ gap: 1rem;
331
+ margin-bottom: 1.5rem;
332
+ }
333
+ .token {
334
+ background: var(--musea-bg-secondary);
335
+ border: 1px solid var(--musea-border);
336
+ border-radius: 8px;
337
+ padding: 1rem;
338
+ display: flex;
339
+ gap: 1rem;
340
+ align-items: center;
341
+ }
342
+ .token-preview {
343
+ flex-shrink: 0;
344
+ width: 48px;
345
+ height: 48px;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ }
350
+ .color-swatch {
351
+ width: 48px;
352
+ height: 48px;
353
+ border-radius: 8px;
354
+ border: 1px solid var(--musea-border);
355
+ }
356
+ .token-info {
357
+ flex: 1;
358
+ min-width: 0;
359
+ }
360
+ .token-name {
361
+ font-weight: 600;
362
+ font-family: 'JetBrains Mono', monospace;
363
+ font-size: 0.875rem;
364
+ }
365
+ .token-value {
366
+ color: var(--musea-text-muted);
367
+ font-family: 'JetBrains Mono', monospace;
368
+ font-size: 0.75rem;
369
+ word-break: break-all;
370
+ }
371
+ .token-description {
372
+ color: var(--musea-text-muted);
373
+ font-size: 0.75rem;
374
+ margin-top: 0.25rem;
375
+ }
376
+ </style>
377
+ </head>
378
+ <body>
379
+ <h1>Design Tokens</h1>
380
+ ${categories.map((cat) => renderCategory(cat)).join("")}
381
+ </body>
382
+ </html>`;
383
+ }
384
+ /**
385
+ * Generate Markdown documentation for tokens.
386
+ */
387
+ function generateTokensMarkdown(categories) {
388
+ const renderCategory = (category, level = 2) => {
389
+ const heading = "#".repeat(level);
390
+ let md = `\n${heading} ${category.name}\n\n`;
391
+ if (Object.keys(category.tokens).length > 0) {
392
+ md += "| Token | Value | Description |\n";
393
+ md += "|-------|-------|-------------|\n";
394
+ for (const [name, token] of Object.entries(category.tokens)) {
395
+ const desc = token.description || "-";
396
+ md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
397
+ }
398
+ md += "\n";
399
+ }
400
+ if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
401
+ return md;
402
+ };
403
+ let markdown = "# Design Tokens\n\n";
404
+ markdown += `> Generated by Musea on ${new Date().toISOString()}\n`;
405
+ for (const category of categories) markdown += renderCategory(category);
406
+ return markdown;
407
+ }
408
+ /**
409
+ * Style Dictionary plugin for Musea.
410
+ */
411
+ async function processStyleDictionary(config) {
412
+ const categories = await parseTokens(config.tokensPath);
413
+ const outputDir = config.outputDir ?? ".vize/tokens";
414
+ const outputFormat = config.outputFormat ?? "html";
415
+ await fs.promises.mkdir(outputDir, { recursive: true });
416
+ let content;
417
+ let filename;
418
+ switch (outputFormat) {
419
+ case "html":
420
+ content = generateTokensHtml(categories);
421
+ filename = "tokens.html";
422
+ break;
423
+ case "markdown":
424
+ content = generateTokensMarkdown(categories);
425
+ filename = "tokens.md";
426
+ break;
427
+ case "json":
428
+ default:
429
+ content = JSON.stringify({ categories }, null, 2);
430
+ filename = "tokens.json";
431
+ }
432
+ const outputPath = path.join(outputDir, filename);
433
+ await fs.promises.writeFile(outputPath, content, "utf-8");
434
+ console.log(`[musea] Generated token documentation: ${outputPath}`);
435
+ return {
436
+ categories,
437
+ metadata: {
438
+ name: path.basename(config.tokensPath),
439
+ generatedAt: new Date().toISOString()
440
+ }
441
+ };
442
+ }
443
+ /**
444
+ * Normalize a token value for comparison.
445
+ * - Lowercase, trim
446
+ * - Leading-zero: `.5rem` → `0.5rem`
447
+ * - Short hex: `#fff` → `#ffffff`
448
+ */
449
+ function normalizeTokenValue(value) {
450
+ let v = String(value).trim().toLowerCase();
451
+ const shortHex = v.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
452
+ if (shortHex) {
453
+ const [, r, g, b, a] = shortHex;
454
+ v = a ? `#${r}${r}${g}${g}${b}${b}${a}${a}` : `#${r}${r}${g}${g}${b}${b}`;
455
+ }
456
+ v = v.replace(/(?<![0-9])\.(\d)/g, "0.$1");
457
+ return v;
458
+ }
459
+ const STYLE_BLOCK_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
460
+ const CSS_PROPERTY_RE = /^\s*([\w-]+)\s*:\s*(.+?)\s*;?\s*$/;
461
+ /**
462
+ * Scan art file sources for token value matches in `<style>` blocks.
463
+ */
464
+ function scanTokenUsage(artFiles, tokenMap) {
465
+ const valueLookup = new Map();
466
+ for (const [tokenPath, token] of Object.entries(tokenMap)) {
467
+ const rawValue = token.$resolvedValue ?? token.value;
468
+ const normalized = normalizeTokenValue(rawValue);
469
+ if (!normalized) continue;
470
+ const existing = valueLookup.get(normalized);
471
+ if (existing) existing.push(tokenPath);
472
+ else valueLookup.set(normalized, [tokenPath]);
473
+ }
474
+ const usageMap = {};
475
+ for (const [artPath, artInfo] of artFiles) {
476
+ let source;
477
+ try {
478
+ source = fs.readFileSync(artPath, "utf-8");
479
+ } catch {
480
+ continue;
481
+ }
482
+ const allLines = source.split("\n");
483
+ const styleRegions = [];
484
+ let match;
485
+ STYLE_BLOCK_RE.lastIndex = 0;
486
+ while ((match = STYLE_BLOCK_RE.exec(source)) !== null) {
487
+ const beforeMatch = source.slice(0, match.index);
488
+ const startTag = source.slice(match.index, match.index + match[0].indexOf(match[1]));
489
+ const startLine = beforeMatch.split("\n").length + startTag.split("\n").length - 1;
490
+ styleRegions.push({
491
+ startLine,
492
+ content: match[1]
493
+ });
494
+ }
495
+ for (const region of styleRegions) {
496
+ const lines = region.content.split("\n");
497
+ for (let i = 0; i < lines.length; i++) {
498
+ const line = lines[i];
499
+ const propMatch = line.match(CSS_PROPERTY_RE);
500
+ if (!propMatch) continue;
501
+ const property = propMatch[1];
502
+ const valueStr = propMatch[2];
503
+ const valueParts = valueStr.split(/\s+/);
504
+ for (const part of valueParts) {
505
+ const normalizedPart = normalizeTokenValue(part);
506
+ const matchingTokens = valueLookup.get(normalizedPart);
507
+ if (!matchingTokens) continue;
508
+ const lineNumber = region.startLine + i;
509
+ const lineContent = allLines[lineNumber - 1]?.trim() ?? line.trim();
510
+ for (const tokenPath of matchingTokens) {
511
+ if (!usageMap[tokenPath]) usageMap[tokenPath] = [];
512
+ let entry = usageMap[tokenPath].find((e) => e.artPath === artPath);
513
+ if (!entry) {
514
+ entry = {
515
+ artPath,
516
+ artTitle: artInfo.metadata.title,
517
+ artCategory: artInfo.metadata.category,
518
+ matches: []
519
+ };
520
+ usageMap[tokenPath].push(entry);
521
+ }
522
+ if (!entry.matches.some((m) => m.line === lineNumber && m.property === property)) entry.matches.push({
523
+ line: lineNumber,
524
+ lineContent,
525
+ property
526
+ });
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+ return usageMap;
533
+ }
534
+
535
+ //#endregion
536
+ //#region src/index.ts
537
+ const VIRTUAL_MUSEA_PREFIX = "\0musea:";
538
+ const VIRTUAL_GALLERY = "\0musea-gallery";
539
+ const VIRTUAL_MANIFEST = "\0musea-manifest";
540
+ let native = null;
541
+ function loadNative() {
542
+ if (native) return native;
543
+ const require = createRequire(import.meta.url);
544
+ try {
545
+ native = require("@vizejs/native");
546
+ return native;
547
+ } catch (e) {
548
+ throw new Error(`Failed to load @vizejs/native. Make sure it's installed and built:\n${String(e)}`);
549
+ }
550
+ }
551
+ /**
552
+ * Build the theme config object from plugin options for runtime injection.
553
+ */
554
+ function buildThemeConfig(theme) {
555
+ if (!theme) return void 0;
556
+ if (typeof theme === "string") return { default: theme };
557
+ const themes = Array.isArray(theme) ? theme : [theme];
558
+ const custom = {};
559
+ for (const t of themes) custom[t.name] = {
560
+ base: t.base,
561
+ colors: t.colors
562
+ };
563
+ return {
564
+ default: themes[0].name,
565
+ custom
566
+ };
567
+ }
568
+ /**
569
+ * Create Musea Vite plugin.
570
+ */
571
+ function musea(options = {}) {
572
+ let include = options.include ?? ["**/*.art.vue"];
573
+ let exclude = options.exclude ?? ["node_modules/**", "dist/**"];
574
+ let basePath = options.basePath ?? "/__musea__";
575
+ let storybookCompat = options.storybookCompat ?? false;
576
+ const storybookOutDir = options.storybookOutDir ?? ".storybook/stories";
577
+ let inlineArt = options.inlineArt ?? false;
578
+ const tokensPath = options.tokensPath;
579
+ const themeConfig = buildThemeConfig(options.theme);
580
+ let config;
581
+ let server = null;
582
+ const artFiles = new Map();
583
+ const mainPlugin = {
584
+ name: "vite-plugin-musea",
585
+ enforce: "pre",
586
+ config() {
587
+ return { resolve: { alias: { vue: "vue/dist/vue.esm-bundler.js" } } };
588
+ },
589
+ configResolved(resolvedConfig) {
590
+ config = resolvedConfig;
591
+ const vizeConfig = vizeConfigStore.get(resolvedConfig.root);
592
+ if (vizeConfig?.musea) {
593
+ const mc = vizeConfig.musea;
594
+ if (!options.include && mc.include) include = mc.include;
595
+ if (!options.exclude && mc.exclude) exclude = mc.exclude;
596
+ if (!options.basePath && mc.basePath) basePath = mc.basePath;
597
+ if (options.storybookCompat === void 0 && mc.storybookCompat !== void 0) storybookCompat = mc.storybookCompat;
598
+ if (options.inlineArt === void 0 && mc.inlineArt !== void 0) inlineArt = mc.inlineArt;
599
+ }
600
+ },
601
+ configureServer(devServer) {
602
+ server = devServer;
603
+ devServer.middlewares.use(basePath, async (req, res, next) => {
604
+ const url = req.url || "/";
605
+ if (url === "/" || url === "/index.html" || url.startsWith("/tokens") || url.startsWith("/component/")) {
606
+ const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
607
+ const indexHtmlPath = path.join(galleryDistDir, "index.html");
608
+ try {
609
+ await fs.promises.access(indexHtmlPath);
610
+ let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
611
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
612
+ html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script></head>`);
613
+ html = await devServer.transformIndexHtml(basePath + url, html);
614
+ res.setHeader("Content-Type", "text/html");
615
+ res.end(html);
616
+ return;
617
+ } catch {
618
+ const html = generateGalleryHtml(basePath, themeConfig);
619
+ res.setHeader("Content-Type", "text/html");
620
+ res.end(html);
621
+ return;
622
+ }
623
+ }
624
+ if (url.startsWith("/assets/")) {
625
+ const galleryDistDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "gallery");
626
+ const filePath = path.join(galleryDistDir, url);
627
+ try {
628
+ const stat = await fs.promises.stat(filePath);
629
+ if (stat.isFile()) {
630
+ const content = await fs.promises.readFile(filePath);
631
+ const ext = path.extname(filePath);
632
+ const mimeTypes = {
633
+ ".js": "application/javascript",
634
+ ".css": "text/css",
635
+ ".svg": "image/svg+xml",
636
+ ".png": "image/png",
637
+ ".ico": "image/x-icon",
638
+ ".woff2": "font/woff2",
639
+ ".woff": "font/woff"
640
+ };
641
+ res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
642
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
643
+ res.end(content);
644
+ return;
645
+ }
646
+ } catch {}
647
+ }
648
+ next();
649
+ });
650
+ devServer.middlewares.use(`${basePath}/preview-module`, async (req, res, _next) => {
651
+ const url = new URL(req.url || "", `http://localhost`);
652
+ const artPath = url.searchParams.get("art");
653
+ const variantName = url.searchParams.get("variant");
654
+ if (!artPath || !variantName) {
655
+ res.statusCode = 400;
656
+ res.end("Missing art or variant parameter");
657
+ return;
658
+ }
659
+ const art = artFiles.get(artPath);
660
+ if (!art) {
661
+ res.statusCode = 404;
662
+ res.end("Art not found");
663
+ return;
664
+ }
665
+ const variant = art.variants.find((v) => v.name === variantName);
666
+ if (!variant) {
667
+ res.statusCode = 404;
668
+ res.end("Variant not found");
669
+ return;
670
+ }
671
+ const variantComponentName = toPascalCase(variant.name);
672
+ const moduleCode = generatePreviewModule(art, variantComponentName, variant.name);
673
+ try {
674
+ const result = await devServer.transformRequest(`virtual:musea-preview:${artPath}:${variantName}`);
675
+ if (result) {
676
+ res.setHeader("Content-Type", "application/javascript");
677
+ res.setHeader("Cache-Control", "no-cache");
678
+ res.end(result.code);
679
+ return;
680
+ }
681
+ } catch {}
682
+ res.setHeader("Content-Type", "application/javascript");
683
+ res.setHeader("Cache-Control", "no-cache");
684
+ res.end(moduleCode);
685
+ });
686
+ devServer.middlewares.use(`${basePath}/preview`, async (req, res, _next) => {
687
+ const url = new URL(req.url || "", `http://localhost`);
688
+ const artPath = url.searchParams.get("art");
689
+ const variantName = url.searchParams.get("variant");
690
+ if (!artPath || !variantName) {
691
+ res.statusCode = 400;
692
+ res.end("Missing art or variant parameter");
693
+ return;
694
+ }
695
+ const art = artFiles.get(artPath);
696
+ if (!art) {
697
+ res.statusCode = 404;
698
+ res.end("Art not found");
699
+ return;
700
+ }
701
+ const variant = art.variants.find((v) => v.name === variantName);
702
+ if (!variant) {
703
+ res.statusCode = 404;
704
+ res.end("Variant not found");
705
+ return;
706
+ }
707
+ const rawHtml = generatePreviewHtml(art, variant, basePath);
708
+ const html = await devServer.transformIndexHtml(`${basePath}/preview?art=${encodeURIComponent(artPath)}&variant=${encodeURIComponent(variantName)}`, rawHtml);
709
+ res.setHeader("Content-Type", "text/html");
710
+ res.end(html);
711
+ });
712
+ devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
713
+ const url = new URL(req.url || "", "http://localhost");
714
+ const artPath = decodeURIComponent(url.pathname.slice(1));
715
+ if (!artPath) {
716
+ next();
717
+ return;
718
+ }
719
+ const art = artFiles.get(artPath);
720
+ if (!art) {
721
+ res.statusCode = 404;
722
+ res.end("Art not found: " + artPath);
723
+ return;
724
+ }
725
+ try {
726
+ const virtualId = `virtual:musea-art:${artPath}`;
727
+ const result = await devServer.transformRequest(virtualId);
728
+ if (result) {
729
+ res.setHeader("Content-Type", "application/javascript");
730
+ res.setHeader("Cache-Control", "no-cache");
731
+ res.end(result.code);
732
+ } else {
733
+ const moduleCode = generateArtModule(art, artPath);
734
+ res.setHeader("Content-Type", "application/javascript");
735
+ res.end(moduleCode);
736
+ }
737
+ } catch (err) {
738
+ console.error("[musea] Failed to transform art module:", err);
739
+ const moduleCode = generateArtModule(art, artPath);
740
+ res.setHeader("Content-Type", "application/javascript");
741
+ res.end(moduleCode);
742
+ }
743
+ });
744
+ devServer.middlewares.use(`${basePath}/api`, async (req, res, next) => {
745
+ const sendJson = (data, status = 200) => {
746
+ res.statusCode = status;
747
+ res.setHeader("Content-Type", "application/json");
748
+ res.end(JSON.stringify(data));
749
+ };
750
+ const sendError = (message, status = 500) => {
751
+ sendJson({ error: message }, status);
752
+ };
753
+ if (req.url === "/arts" && req.method === "GET") {
754
+ sendJson(Array.from(artFiles.values()));
755
+ return;
756
+ }
757
+ if (req.url === "/tokens/usage" && req.method === "GET") {
758
+ if (!tokensPath) {
759
+ sendJson({});
760
+ return;
761
+ }
762
+ try {
763
+ const absoluteTokensPath = path.resolve(config.root, tokensPath);
764
+ const categories = await parseTokens(absoluteTokensPath);
765
+ const tokenMap = buildTokenMap(categories);
766
+ resolveReferences(categories, tokenMap);
767
+ const resolvedTokenMap = buildTokenMap(categories);
768
+ const usage = scanTokenUsage(artFiles, resolvedTokenMap);
769
+ sendJson(usage);
770
+ } catch (e) {
771
+ console.error("[musea] Failed to scan token usage:", e);
772
+ sendJson({});
773
+ }
774
+ return;
775
+ }
776
+ if (req.url === "/tokens" && req.method === "GET") {
777
+ if (!tokensPath) {
778
+ sendJson({
779
+ categories: [],
780
+ tokenMap: {},
781
+ meta: {
782
+ filePath: "",
783
+ tokenCount: 0,
784
+ primitiveCount: 0,
785
+ semanticCount: 0
786
+ }
787
+ });
788
+ return;
789
+ }
790
+ try {
791
+ const absoluteTokensPath = path.resolve(config.root, tokensPath);
792
+ const categories = await parseTokens(absoluteTokensPath);
793
+ const tokenMap = buildTokenMap(categories);
794
+ resolveReferences(categories, tokenMap);
795
+ const resolvedTokenMap = buildTokenMap(categories);
796
+ let primitiveCount = 0;
797
+ let semanticCount = 0;
798
+ for (const token of Object.values(resolvedTokenMap)) if (token.$tier === "semantic") semanticCount++;
799
+ else primitiveCount++;
800
+ sendJson({
801
+ categories,
802
+ tokenMap: resolvedTokenMap,
803
+ meta: {
804
+ filePath: absoluteTokensPath,
805
+ tokenCount: Object.keys(resolvedTokenMap).length,
806
+ primitiveCount,
807
+ semanticCount
808
+ }
809
+ });
810
+ } catch (e) {
811
+ console.error("[musea] Failed to load tokens:", e);
812
+ sendJson({
813
+ categories: [],
814
+ tokenMap: {},
815
+ error: String(e)
816
+ });
817
+ }
818
+ return;
819
+ }
820
+ if (req.url === "/tokens" && req.method === "POST") {
821
+ if (!tokensPath) {
822
+ sendError("No tokens path configured", 400);
823
+ return;
824
+ }
825
+ let body = "";
826
+ req.on("data", (chunk) => {
827
+ body += chunk;
828
+ });
829
+ req.on("end", async () => {
830
+ try {
831
+ const { path: dotPath, token } = JSON.parse(body);
832
+ if (!dotPath || !token || token.value === void 0) {
833
+ sendError("Missing required fields: path, token.value", 400);
834
+ return;
835
+ }
836
+ const absoluteTokensPath = path.resolve(config.root, tokensPath);
837
+ const rawData = await readRawTokenFile(absoluteTokensPath);
838
+ const currentCategories = await parseTokens(absoluteTokensPath);
839
+ const currentMap = buildTokenMap(currentCategories);
840
+ if (currentMap[dotPath]) {
841
+ sendError(`Token already exists at path "${dotPath}"`, 409);
842
+ return;
843
+ }
844
+ if (token.$reference) {
845
+ const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
846
+ if (!validation.valid) {
847
+ sendError(validation.error, 400);
848
+ return;
849
+ }
850
+ token.value = `{${token.$reference}}`;
851
+ token.$tier = "semantic";
852
+ }
853
+ setTokenAtPath(rawData, dotPath, token);
854
+ await writeRawTokenFile(absoluteTokensPath, rawData);
855
+ const categories = await parseTokens(absoluteTokensPath);
856
+ const tokenMap = buildTokenMap(categories);
857
+ resolveReferences(categories, tokenMap);
858
+ const resolvedTokenMap = buildTokenMap(categories);
859
+ sendJson({
860
+ categories,
861
+ tokenMap: resolvedTokenMap
862
+ }, 201);
863
+ } catch (e) {
864
+ sendError(e instanceof Error ? e.message : String(e));
865
+ }
866
+ });
867
+ return;
868
+ }
869
+ if (req.url === "/tokens" && req.method === "PUT") {
870
+ if (!tokensPath) {
871
+ sendError("No tokens path configured", 400);
872
+ return;
873
+ }
874
+ let body = "";
875
+ req.on("data", (chunk) => {
876
+ body += chunk;
877
+ });
878
+ req.on("end", async () => {
879
+ try {
880
+ const { path: dotPath, token } = JSON.parse(body);
881
+ if (!dotPath || !token || token.value === void 0) {
882
+ sendError("Missing required fields: path, token.value", 400);
883
+ return;
884
+ }
885
+ const absoluteTokensPath = path.resolve(config.root, tokensPath);
886
+ if (token.$reference) {
887
+ const currentCategories = await parseTokens(absoluteTokensPath);
888
+ const currentMap = buildTokenMap(currentCategories);
889
+ const validation = validateSemanticReference(currentMap, token.$reference, dotPath);
890
+ if (!validation.valid) {
891
+ sendError(validation.error, 400);
892
+ return;
893
+ }
894
+ token.value = `{${token.$reference}}`;
895
+ token.$tier = "semantic";
896
+ }
897
+ const rawData = await readRawTokenFile(absoluteTokensPath);
898
+ setTokenAtPath(rawData, dotPath, token);
899
+ await writeRawTokenFile(absoluteTokensPath, rawData);
900
+ const categories = await parseTokens(absoluteTokensPath);
901
+ const tokenMap = buildTokenMap(categories);
902
+ resolveReferences(categories, tokenMap);
903
+ const resolvedTokenMap = buildTokenMap(categories);
904
+ sendJson({
905
+ categories,
906
+ tokenMap: resolvedTokenMap
907
+ });
908
+ } catch (e) {
909
+ sendError(e instanceof Error ? e.message : String(e));
910
+ }
911
+ });
912
+ return;
913
+ }
914
+ if (req.url === "/tokens" && req.method === "DELETE") {
915
+ if (!tokensPath) {
916
+ sendError("No tokens path configured", 400);
917
+ return;
918
+ }
919
+ let body = "";
920
+ req.on("data", (chunk) => {
921
+ body += chunk;
922
+ });
923
+ req.on("end", async () => {
924
+ try {
925
+ const { path: dotPath } = JSON.parse(body);
926
+ if (!dotPath) {
927
+ sendError("Missing required field: path", 400);
928
+ return;
929
+ }
930
+ const absoluteTokensPath = path.resolve(config.root, tokensPath);
931
+ const currentCategories = await parseTokens(absoluteTokensPath);
932
+ const currentMap = buildTokenMap(currentCategories);
933
+ const dependents = findDependentTokens(currentMap, dotPath);
934
+ const rawData = await readRawTokenFile(absoluteTokensPath);
935
+ const deleted = deleteTokenAtPath(rawData, dotPath);
936
+ if (!deleted) {
937
+ sendError(`Token not found at path "${dotPath}"`, 404);
938
+ return;
939
+ }
940
+ await writeRawTokenFile(absoluteTokensPath, rawData);
941
+ const categories = await parseTokens(absoluteTokensPath);
942
+ const tokenMap = buildTokenMap(categories);
943
+ resolveReferences(categories, tokenMap);
944
+ const resolvedTokenMap = buildTokenMap(categories);
945
+ sendJson({
946
+ categories,
947
+ tokenMap: resolvedTokenMap,
948
+ dependentsWarning: dependents.length > 0 ? dependents : void 0
949
+ });
950
+ } catch (e) {
951
+ sendError(e instanceof Error ? e.message : String(e));
952
+ }
953
+ });
954
+ return;
955
+ }
956
+ if (req.url?.startsWith("/arts/") && req.method === "PUT") {
957
+ const rest = req.url.slice(6);
958
+ const sourceMatch = rest.match(/^(.+)\/source$/);
959
+ if (sourceMatch) {
960
+ const artPath = decodeURIComponent(sourceMatch[1]);
961
+ const art = artFiles.get(artPath);
962
+ if (!art) {
963
+ sendError("Art not found", 404);
964
+ return;
965
+ }
966
+ let body = "";
967
+ req.on("data", (chunk) => {
968
+ body += chunk;
969
+ });
970
+ req.on("end", async () => {
971
+ try {
972
+ const { source } = JSON.parse(body);
973
+ if (typeof source !== "string") {
974
+ sendError("Missing required field: source", 400);
975
+ return;
976
+ }
977
+ await fs.promises.writeFile(artPath, source, "utf-8");
978
+ await processArtFile(artPath);
979
+ sendJson({ success: true });
980
+ } catch (e) {
981
+ sendError(e instanceof Error ? e.message : String(e));
982
+ }
983
+ });
984
+ return;
985
+ }
986
+ next();
987
+ return;
988
+ }
989
+ if (req.url?.startsWith("/arts/") && req.method === "GET") {
990
+ const rest = req.url.slice(6);
991
+ const sourceMatch = rest.match(/^(.+)\/source$/);
992
+ const paletteMatch = rest.match(/^(.+)\/palette$/);
993
+ const analysisMatch = rest.match(/^(.+)\/analysis$/);
994
+ const docsMatch = rest.match(/^(.+)\/docs$/);
995
+ const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
996
+ if (sourceMatch) {
997
+ const artPath$1 = decodeURIComponent(sourceMatch[1]);
998
+ const art$1 = artFiles.get(artPath$1);
999
+ if (!art$1) {
1000
+ sendError("Art not found", 404);
1001
+ return;
1002
+ }
1003
+ try {
1004
+ const source = await fs.promises.readFile(artPath$1, "utf-8");
1005
+ sendJson({
1006
+ source,
1007
+ path: artPath$1
1008
+ });
1009
+ } catch (e) {
1010
+ sendError(e instanceof Error ? e.message : String(e));
1011
+ }
1012
+ return;
1013
+ }
1014
+ if (paletteMatch) {
1015
+ const artPath$1 = decodeURIComponent(paletteMatch[1]);
1016
+ const art$1 = artFiles.get(artPath$1);
1017
+ if (!art$1) {
1018
+ sendError("Art not found", 404);
1019
+ return;
1020
+ }
1021
+ try {
1022
+ const source = await fs.promises.readFile(artPath$1, "utf-8");
1023
+ const binding = loadNative();
1024
+ if (binding.generateArtPalette) {
1025
+ const palette = binding.generateArtPalette(source, { filename: artPath$1 });
1026
+ sendJson(palette);
1027
+ } else sendJson({
1028
+ title: art$1.metadata.title,
1029
+ controls: [],
1030
+ groups: [],
1031
+ json: "{}",
1032
+ typescript: ""
1033
+ });
1034
+ } catch (e) {
1035
+ sendError(e instanceof Error ? e.message : String(e));
1036
+ }
1037
+ return;
1038
+ }
1039
+ if (analysisMatch) {
1040
+ const artPath$1 = decodeURIComponent(analysisMatch[1]);
1041
+ const art$1 = artFiles.get(artPath$1);
1042
+ if (!art$1) {
1043
+ sendError("Art not found", 404);
1044
+ return;
1045
+ }
1046
+ try {
1047
+ const resolvedComponentPath = art$1.isInline && art$1.componentPath ? art$1.componentPath : art$1.metadata.component ? path.isAbsolute(art$1.metadata.component) ? art$1.metadata.component : path.resolve(path.dirname(artPath$1), art$1.metadata.component) : null;
1048
+ if (resolvedComponentPath) {
1049
+ const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
1050
+ const binding = loadNative();
1051
+ if (binding.analyzeSfc) {
1052
+ const analysis = binding.analyzeSfc(source, { filename: resolvedComponentPath });
1053
+ sendJson(analysis);
1054
+ } else sendJson({
1055
+ props: [],
1056
+ emits: []
1057
+ });
1058
+ } else sendJson({
1059
+ props: [],
1060
+ emits: []
1061
+ });
1062
+ } catch (e) {
1063
+ sendError(e instanceof Error ? e.message : String(e));
1064
+ }
1065
+ return;
1066
+ }
1067
+ if (docsMatch) {
1068
+ const artPath$1 = decodeURIComponent(docsMatch[1]);
1069
+ const art$1 = artFiles.get(artPath$1);
1070
+ if (!art$1) {
1071
+ sendError("Art not found", 404);
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const source = await fs.promises.readFile(artPath$1, "utf-8");
1076
+ const binding = loadNative();
1077
+ if (binding.generateArtDoc) {
1078
+ const doc = binding.generateArtDoc(source, { filename: artPath$1 });
1079
+ let markdown = doc.markdown || "";
1080
+ const componentName = art$1.metadata.title || "Component";
1081
+ markdown = markdown.replace(/<Self(\s|>|\/)/g, `<${componentName}$1`).replace(/<\/Self>/g, `</${componentName}>`);
1082
+ markdown = markdown.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
1083
+ const lines = code.split("\n");
1084
+ let minIndent = Infinity;
1085
+ for (const line of lines) if (line.trim()) {
1086
+ const indent = line.match(/^(\s*)/)?.[1].length || 0;
1087
+ minIndent = Math.min(minIndent, indent);
1088
+ }
1089
+ if (minIndent === Infinity) minIndent = 0;
1090
+ const formatted = lines.map((line) => line.slice(minIndent)).join("\n");
1091
+ return "```" + lang + "\n" + formatted + "```";
1092
+ });
1093
+ sendJson({
1094
+ ...doc,
1095
+ markdown
1096
+ });
1097
+ } else sendJson({
1098
+ markdown: "",
1099
+ title: art$1.metadata.title,
1100
+ variant_count: art$1.variants.length
1101
+ });
1102
+ } catch (e) {
1103
+ sendError(e instanceof Error ? e.message : String(e));
1104
+ }
1105
+ return;
1106
+ }
1107
+ if (a11yMatch) {
1108
+ const artPath$1 = decodeURIComponent(a11yMatch[1]);
1109
+ const _variantName = decodeURIComponent(a11yMatch[2]);
1110
+ const art$1 = artFiles.get(artPath$1);
1111
+ if (!art$1) {
1112
+ sendError("Art not found", 404);
1113
+ return;
1114
+ }
1115
+ sendJson({
1116
+ violations: [],
1117
+ passes: 0,
1118
+ incomplete: 0
1119
+ });
1120
+ return;
1121
+ }
1122
+ const artPath = decodeURIComponent(rest);
1123
+ const art = artFiles.get(artPath);
1124
+ if (art) sendJson(art);
1125
+ else sendError("Art not found", 404);
1126
+ return;
1127
+ }
1128
+ if (req.url === "/preview-with-props" && req.method === "POST") {
1129
+ let body = "";
1130
+ req.on("data", (chunk) => {
1131
+ body += chunk;
1132
+ });
1133
+ req.on("end", () => {
1134
+ try {
1135
+ const { artPath: reqArtPath, variantName, props: propsOverride } = JSON.parse(body);
1136
+ const art = artFiles.get(reqArtPath);
1137
+ if (!art) {
1138
+ sendError("Art not found", 404);
1139
+ return;
1140
+ }
1141
+ const variant = art.variants.find((v) => v.name === variantName);
1142
+ if (!variant) {
1143
+ sendError("Variant not found", 404);
1144
+ return;
1145
+ }
1146
+ const variantComponentName = toPascalCase(variant.name);
1147
+ const moduleCode = generatePreviewModuleWithProps(art, variantComponentName, variant.name, propsOverride);
1148
+ res.setHeader("Content-Type", "application/javascript");
1149
+ res.end(moduleCode);
1150
+ } catch (e) {
1151
+ sendError(e instanceof Error ? e.message : String(e));
1152
+ }
1153
+ });
1154
+ return;
1155
+ }
1156
+ if (req.url === "/generate" && req.method === "POST") {
1157
+ let body = "";
1158
+ req.on("data", (chunk) => {
1159
+ body += chunk;
1160
+ });
1161
+ req.on("end", async () => {
1162
+ try {
1163
+ const { componentPath: reqComponentPath, options: autogenOptions } = JSON.parse(body);
1164
+ const { generateArtFile: genArt } = await import("./autogen.js");
1165
+ const result = await genArt(reqComponentPath, autogenOptions);
1166
+ sendJson({
1167
+ generated: true,
1168
+ componentName: result.componentName,
1169
+ variants: result.variants,
1170
+ artFileContent: result.artFileContent
1171
+ });
1172
+ } catch (e) {
1173
+ sendError(e instanceof Error ? e.message : String(e));
1174
+ }
1175
+ });
1176
+ return;
1177
+ }
1178
+ if (req.url === "/run-vrt" && req.method === "POST") {
1179
+ let body = "";
1180
+ req.on("data", (chunk) => {
1181
+ body += chunk;
1182
+ });
1183
+ req.on("end", async () => {
1184
+ try {
1185
+ const { artPath, updateSnapshots } = JSON.parse(body);
1186
+ const { MuseaVrtRunner: MuseaVrtRunner$1 } = await import("./vrt.js");
1187
+ const runner = new MuseaVrtRunner$1({ snapshotDir: path.resolve(config.root, ".vize/snapshots") });
1188
+ const port = devServer.config.server.port || 5173;
1189
+ const baseUrl = `http://localhost:${port}`;
1190
+ let artsToTest = Array.from(artFiles.values());
1191
+ if (artPath) artsToTest = artsToTest.filter((a) => a.path === artPath);
1192
+ await runner.start();
1193
+ const results = await runner.runTests(artsToTest, baseUrl, { updateSnapshots });
1194
+ const summary = runner.getSummary(results);
1195
+ await runner.stop();
1196
+ sendJson({
1197
+ success: true,
1198
+ summary,
1199
+ results: results.map((r) => ({
1200
+ artPath: r.artPath,
1201
+ variantName: r.variantName,
1202
+ viewport: r.viewport.name,
1203
+ passed: r.passed,
1204
+ isNew: r.isNew,
1205
+ diffPercentage: r.diffPercentage,
1206
+ error: r.error
1207
+ }))
1208
+ });
1209
+ } catch (e) {
1210
+ sendError(e instanceof Error ? e.message : String(e));
1211
+ }
1212
+ });
1213
+ return;
1214
+ }
1215
+ next();
1216
+ });
1217
+ devServer.watcher.on("change", async (file) => {
1218
+ if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
1219
+ await processArtFile(file);
1220
+ console.log(`[musea] Reloaded: ${path.relative(config.root, file)}`);
1221
+ }
1222
+ if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
1223
+ const hadArt = artFiles.has(file);
1224
+ const source = await fs.promises.readFile(file, "utf-8");
1225
+ if (source.includes("<art")) {
1226
+ await processArtFile(file);
1227
+ console.log(`[musea] Reloaded inline art: ${path.relative(config.root, file)}`);
1228
+ } else if (hadArt) {
1229
+ artFiles.delete(file);
1230
+ console.log(`[musea] Removed inline art: ${path.relative(config.root, file)}`);
1231
+ }
1232
+ }
1233
+ });
1234
+ devServer.watcher.on("add", async (file) => {
1235
+ if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
1236
+ await processArtFile(file);
1237
+ console.log(`[musea] Added: ${path.relative(config.root, file)}`);
1238
+ }
1239
+ if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue")) {
1240
+ const source = await fs.promises.readFile(file, "utf-8");
1241
+ if (source.includes("<art")) {
1242
+ await processArtFile(file);
1243
+ console.log(`[musea] Added inline art: ${path.relative(config.root, file)}`);
1244
+ }
1245
+ }
1246
+ });
1247
+ devServer.watcher.on("unlink", (file) => {
1248
+ if (artFiles.has(file)) {
1249
+ artFiles.delete(file);
1250
+ console.log(`[musea] Removed: ${path.relative(config.root, file)}`);
1251
+ }
1252
+ });
1253
+ return () => {
1254
+ devServer.httpServer?.once("listening", () => {
1255
+ const address = devServer.httpServer?.address();
1256
+ if (address && typeof address === "object") {
1257
+ const protocol = devServer.config.server.https ? "https" : "http";
1258
+ const rawHost = address.address;
1259
+ const host = rawHost === "::" || rawHost === "::1" || rawHost === "0.0.0.0" || rawHost === "127.0.0.1" ? "localhost" : rawHost;
1260
+ const port = address.port;
1261
+ const url = `${protocol}://${host}:${port}${basePath}`;
1262
+ console.log();
1263
+ console.log(` \x1b[36m➜\x1b[0m \x1b[1mMusea Gallery:\x1b[0m \x1b[36m${url}\x1b[0m`);
1264
+ }
1265
+ });
1266
+ };
1267
+ },
1268
+ async buildStart() {
1269
+ const files = await scanArtFiles(config.root, include, exclude, inlineArt);
1270
+ console.log(`[musea] Found ${files.length} art files`);
1271
+ for (const file of files) await processArtFile(file);
1272
+ if (storybookCompat) await generateStorybookFiles(artFiles, config.root, storybookOutDir);
1273
+ },
1274
+ resolveId(id) {
1275
+ if (id === VIRTUAL_GALLERY) return VIRTUAL_GALLERY;
1276
+ if (id === VIRTUAL_MANIFEST) return VIRTUAL_MANIFEST;
1277
+ if (id.startsWith("virtual:musea-preview:")) return "\0musea-preview:" + id.slice(22);
1278
+ if (id.startsWith("virtual:musea-art:")) {
1279
+ const artPath = id.slice(18);
1280
+ if (artFiles.has(artPath)) return "\0musea-art:" + artPath;
1281
+ }
1282
+ if (id.endsWith(".art.vue")) {
1283
+ const resolved = path.resolve(config.root, id);
1284
+ if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved;
1285
+ }
1286
+ if (inlineArt && id.endsWith(".vue") && !id.endsWith(".art.vue")) {
1287
+ const resolved = path.resolve(config.root, id);
1288
+ if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved;
1289
+ }
1290
+ return null;
1291
+ },
1292
+ load(id) {
1293
+ if (id === VIRTUAL_GALLERY) return generateGalleryModule(basePath);
1294
+ if (id === VIRTUAL_MANIFEST) return generateManifestModule(artFiles);
1295
+ if (id.startsWith("\0musea-preview:")) {
1296
+ const rest = id.slice(15);
1297
+ const lastColonIndex = rest.lastIndexOf(":");
1298
+ if (lastColonIndex !== -1) {
1299
+ const artPath = rest.slice(0, lastColonIndex);
1300
+ const variantName = rest.slice(lastColonIndex + 1);
1301
+ const art = artFiles.get(artPath);
1302
+ if (art) {
1303
+ const variantComponentName = toPascalCase(variantName);
1304
+ return generatePreviewModule(art, variantComponentName, variantName);
1305
+ }
1306
+ }
1307
+ }
1308
+ if (id.startsWith("\0musea-art:")) {
1309
+ const artPath = id.slice(11);
1310
+ const art = artFiles.get(artPath);
1311
+ if (art) return generateArtModule(art, artPath);
1312
+ }
1313
+ if (id.startsWith(VIRTUAL_MUSEA_PREFIX)) {
1314
+ const realPath = id.slice(VIRTUAL_MUSEA_PREFIX.length);
1315
+ const art = artFiles.get(realPath);
1316
+ if (art) return generateArtModule(art, realPath);
1317
+ }
1318
+ return null;
1319
+ },
1320
+ async handleHotUpdate(ctx) {
1321
+ const { file } = ctx;
1322
+ if (file.endsWith(".art.vue") && artFiles.has(file)) {
1323
+ await processArtFile(file);
1324
+ const virtualId = VIRTUAL_MUSEA_PREFIX + file;
1325
+ const modules = server?.moduleGraph.getModulesByFile(virtualId);
1326
+ if (modules) return [...modules];
1327
+ }
1328
+ if (inlineArt && file.endsWith(".vue") && !file.endsWith(".art.vue") && artFiles.has(file)) {
1329
+ await processArtFile(file);
1330
+ const virtualId = VIRTUAL_MUSEA_PREFIX + file;
1331
+ const modules = server?.moduleGraph.getModulesByFile(virtualId);
1332
+ if (modules) return [...modules];
1333
+ }
1334
+ return void 0;
1335
+ }
1336
+ };
1337
+ async function processArtFile(filePath) {
1338
+ try {
1339
+ const source = await fs.promises.readFile(filePath, "utf-8");
1340
+ const binding = loadNative();
1341
+ const parsed = binding.parseArt(source, { filename: filePath });
1342
+ if (!parsed.variants || parsed.variants.length === 0) return;
1343
+ const isInline = !filePath.endsWith(".art.vue");
1344
+ const info = {
1345
+ path: filePath,
1346
+ metadata: {
1347
+ title: parsed.metadata.title || (isInline ? path.basename(filePath, ".vue") : ""),
1348
+ description: parsed.metadata.description,
1349
+ component: isInline ? void 0 : parsed.metadata.component,
1350
+ category: parsed.metadata.category,
1351
+ tags: parsed.metadata.tags,
1352
+ status: parsed.metadata.status,
1353
+ order: parsed.metadata.order
1354
+ },
1355
+ variants: parsed.variants.map((v) => ({
1356
+ name: v.name,
1357
+ template: v.template,
1358
+ isDefault: v.is_default,
1359
+ skipVrt: v.skip_vrt
1360
+ })),
1361
+ hasScriptSetup: parsed.has_script_setup,
1362
+ hasScript: parsed.has_script,
1363
+ styleCount: parsed.style_count,
1364
+ isInline,
1365
+ componentPath: isInline ? filePath : void 0
1366
+ };
1367
+ artFiles.set(filePath, info);
1368
+ } catch (e) {
1369
+ console.error(`[musea] Failed to process ${filePath}:`, e);
1370
+ }
1371
+ }
1372
+ return [mainPlugin];
1373
+ }
1374
+ function shouldProcess(file, include, exclude, root) {
1375
+ const relative = path.relative(root, file);
1376
+ for (const pattern of exclude) if (matchGlob(relative, pattern)) return false;
1377
+ for (const pattern of include) if (matchGlob(relative, pattern)) return true;
1378
+ return false;
1379
+ }
1380
+ function matchGlob(filepath, pattern) {
1381
+ const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*(?!\*)/g, "[^/]*");
1382
+ return new RegExp(`^${regex}$`).test(filepath);
1383
+ }
1384
+ async function scanArtFiles(root, include, exclude, scanInlineArt = false) {
1385
+ const files = [];
1386
+ async function scan(dir) {
1387
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1388
+ for (const entry of entries) {
1389
+ const fullPath = path.join(dir, entry.name);
1390
+ const relative = path.relative(root, fullPath);
1391
+ let excluded = false;
1392
+ for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
1393
+ excluded = true;
1394
+ break;
1395
+ }
1396
+ if (excluded) continue;
1397
+ if (entry.isDirectory()) await scan(fullPath);
1398
+ else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
1399
+ for (const pattern of include) if (matchGlob(relative, pattern)) {
1400
+ files.push(fullPath);
1401
+ break;
1402
+ }
1403
+ } else if (scanInlineArt && entry.isFile() && entry.name.endsWith(".vue") && !entry.name.endsWith(".art.vue")) {
1404
+ const content = await fs.promises.readFile(fullPath, "utf-8");
1405
+ if (content.includes("<art")) files.push(fullPath);
1406
+ }
1407
+ }
1408
+ }
1409
+ await scan(root);
1410
+ return files;
1411
+ }
1412
+ function generateGalleryHtml(basePath, themeConfig) {
1413
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
1414
+ return `<!DOCTYPE html>
1415
+ <html lang="en">
1416
+ <head>
1417
+ <meta charset="UTF-8">
1418
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1419
+ <title>Musea - Component Gallery</title>
1420
+ <script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}</script>
1421
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1422
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1423
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
1424
+ <style>
1425
+ :root {
1426
+ --musea-bg-primary: #0d0d0d;
1427
+ --musea-bg-secondary: #1a1815;
1428
+ --musea-bg-tertiary: #252220;
1429
+ --musea-bg-elevated: #2d2a27;
1430
+ --musea-accent: #a34828;
1431
+ --musea-accent-hover: #c45a32;
1432
+ --musea-accent-subtle: rgba(163, 72, 40, 0.15);
1433
+ --musea-text: #e6e9f0;
1434
+ --musea-text-secondary: #c4c9d4;
1435
+ --musea-text-muted: #7b8494;
1436
+ --musea-border: #3a3530;
1437
+ --musea-border-subtle: #2a2725;
1438
+ --musea-success: #4ade80;
1439
+ --musea-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
1440
+ --musea-radius-sm: 6px;
1441
+ --musea-radius-md: 8px;
1442
+ --musea-radius-lg: 12px;
1443
+ --musea-transition: 0.15s ease;
1444
+ }
1445
+
1446
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1447
+
1448
+ body {
1449
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1450
+ background: var(--musea-bg-primary);
1451
+ color: var(--musea-text);
1452
+ min-height: 100vh;
1453
+ line-height: 1.5;
1454
+ -webkit-font-smoothing: antialiased;
1455
+ }
1456
+
1457
+ /* Header */
1458
+ .header {
1459
+ background: var(--musea-bg-secondary);
1460
+ border-bottom: 1px solid var(--musea-border);
1461
+ padding: 0 1.5rem;
1462
+ height: 56px;
1463
+ display: flex;
1464
+ align-items: center;
1465
+ justify-content: space-between;
1466
+ position: sticky;
1467
+ top: 0;
1468
+ z-index: 100;
1469
+ }
1470
+
1471
+ .header-left {
1472
+ display: flex;
1473
+ align-items: center;
1474
+ gap: 1.5rem;
1475
+ }
1476
+
1477
+ .logo {
1478
+ display: flex;
1479
+ align-items: center;
1480
+ gap: 0.5rem;
1481
+ font-size: 1.125rem;
1482
+ font-weight: 700;
1483
+ color: var(--musea-accent);
1484
+ text-decoration: none;
1485
+ }
1486
+
1487
+ .logo-svg {
1488
+ width: 32px;
1489
+ height: 32px;
1490
+ flex-shrink: 0;
1491
+ }
1492
+
1493
+ .logo-icon svg {
1494
+ width: 16px;
1495
+ height: 16px;
1496
+ color: white;
1497
+ }
1498
+
1499
+ .header-subtitle {
1500
+ color: var(--musea-text-muted);
1501
+ font-size: 0.8125rem;
1502
+ font-weight: 500;
1503
+ padding-left: 1.5rem;
1504
+ border-left: 1px solid var(--musea-border);
1505
+ }
1506
+
1507
+ .search-container {
1508
+ position: relative;
1509
+ width: 280px;
1510
+ }
1511
+
1512
+ .search-input {
1513
+ width: 100%;
1514
+ background: var(--musea-bg-tertiary);
1515
+ border: 1px solid var(--musea-border);
1516
+ border-radius: var(--musea-radius-md);
1517
+ padding: 0.5rem 0.75rem 0.5rem 2.25rem;
1518
+ color: var(--musea-text);
1519
+ font-size: 0.8125rem;
1520
+ outline: none;
1521
+ transition: border-color var(--musea-transition), background var(--musea-transition);
1522
+ }
1523
+
1524
+ .search-input::placeholder {
1525
+ color: var(--musea-text-muted);
1526
+ }
1527
+
1528
+ .search-input:focus {
1529
+ border-color: var(--musea-accent);
1530
+ background: var(--musea-bg-elevated);
1531
+ }
1532
+
1533
+ .search-icon {
1534
+ position: absolute;
1535
+ left: 0.75rem;
1536
+ top: 50%;
1537
+ transform: translateY(-50%);
1538
+ color: var(--musea-text-muted);
1539
+ pointer-events: none;
1540
+ }
1541
+
1542
+ /* Layout */
1543
+ .main {
1544
+ display: grid;
1545
+ grid-template-columns: 260px 1fr;
1546
+ min-height: calc(100vh - 56px);
1547
+ }
1548
+
1549
+ /* Sidebar */
1550
+ .sidebar {
1551
+ background: var(--musea-bg-secondary);
1552
+ border-right: 1px solid var(--musea-border);
1553
+ overflow-y: auto;
1554
+ overflow-x: hidden;
1555
+ }
1556
+
1557
+ .sidebar::-webkit-scrollbar {
1558
+ width: 6px;
1559
+ }
1560
+
1561
+ .sidebar::-webkit-scrollbar-track {
1562
+ background: transparent;
1563
+ }
1564
+
1565
+ .sidebar::-webkit-scrollbar-thumb {
1566
+ background: var(--musea-border);
1567
+ border-radius: 3px;
1568
+ }
1569
+
1570
+ .sidebar-section {
1571
+ padding: 0.75rem;
1572
+ }
1573
+
1574
+ .category-header {
1575
+ display: flex;
1576
+ align-items: center;
1577
+ gap: 0.5rem;
1578
+ padding: 0.625rem 0.75rem;
1579
+ font-size: 0.6875rem;
1580
+ font-weight: 600;
1581
+ text-transform: uppercase;
1582
+ letter-spacing: 0.08em;
1583
+ color: var(--musea-text-muted);
1584
+ cursor: pointer;
1585
+ user-select: none;
1586
+ border-radius: var(--musea-radius-sm);
1587
+ transition: background var(--musea-transition);
1588
+ }
1589
+
1590
+ .category-header:hover {
1591
+ background: var(--musea-bg-tertiary);
1592
+ }
1593
+
1594
+ .category-icon {
1595
+ width: 16px;
1596
+ height: 16px;
1597
+ transition: transform var(--musea-transition);
1598
+ }
1599
+
1600
+ .category-header.collapsed .category-icon {
1601
+ transform: rotate(-90deg);
1602
+ }
1603
+
1604
+ .category-count {
1605
+ margin-left: auto;
1606
+ background: var(--musea-bg-tertiary);
1607
+ padding: 0.125rem 0.375rem;
1608
+ border-radius: 4px;
1609
+ font-size: 0.625rem;
1610
+ }
1611
+
1612
+ .art-list {
1613
+ list-style: none;
1614
+ margin-top: 0.25rem;
1615
+ }
1616
+
1617
+ .art-item {
1618
+ display: flex;
1619
+ align-items: center;
1620
+ gap: 0.625rem;
1621
+ padding: 0.5rem 0.75rem 0.5rem 1.75rem;
1622
+ border-radius: var(--musea-radius-sm);
1623
+ cursor: pointer;
1624
+ font-size: 0.8125rem;
1625
+ color: var(--musea-text-secondary);
1626
+ transition: all var(--musea-transition);
1627
+ position: relative;
1628
+ }
1629
+
1630
+ .art-item::before {
1631
+ content: '';
1632
+ position: absolute;
1633
+ left: 0.75rem;
1634
+ top: 50%;
1635
+ transform: translateY(-50%);
1636
+ width: 6px;
1637
+ height: 6px;
1638
+ border-radius: 50%;
1639
+ background: var(--musea-border);
1640
+ transition: background var(--musea-transition);
1641
+ }
1642
+
1643
+ .art-item:hover {
1644
+ background: var(--musea-bg-tertiary);
1645
+ color: var(--musea-text);
1646
+ }
1647
+
1648
+ .art-item:hover::before {
1649
+ background: var(--musea-text-muted);
1650
+ }
1651
+
1652
+ .art-item.active {
1653
+ background: var(--musea-accent-subtle);
1654
+ color: var(--musea-accent-hover);
1655
+ }
1656
+
1657
+ .art-item.active::before {
1658
+ background: var(--musea-accent);
1659
+ }
1660
+
1661
+ .art-variant-count {
1662
+ margin-left: auto;
1663
+ font-size: 0.6875rem;
1664
+ color: var(--musea-text-muted);
1665
+ opacity: 0;
1666
+ transition: opacity var(--musea-transition);
1667
+ }
1668
+
1669
+ .art-item:hover .art-variant-count {
1670
+ opacity: 1;
1671
+ }
1672
+
1673
+ /* Content */
1674
+ .content {
1675
+ background: var(--musea-bg-primary);
1676
+ overflow-y: auto;
1677
+ }
1678
+
1679
+ .content-inner {
1680
+ max-width: 1400px;
1681
+ margin: 0 auto;
1682
+ padding: 2rem;
1683
+ }
1684
+
1685
+ .content-header {
1686
+ margin-bottom: 2rem;
1687
+ }
1688
+
1689
+ .content-title {
1690
+ font-size: 1.5rem;
1691
+ font-weight: 700;
1692
+ margin-bottom: 0.5rem;
1693
+ }
1694
+
1695
+ .content-description {
1696
+ color: var(--musea-text-muted);
1697
+ font-size: 0.9375rem;
1698
+ max-width: 600px;
1699
+ }
1700
+
1701
+ .content-meta {
1702
+ display: flex;
1703
+ align-items: center;
1704
+ gap: 1rem;
1705
+ margin-top: 1rem;
1706
+ }
1707
+
1708
+ .meta-tag {
1709
+ display: inline-flex;
1710
+ align-items: center;
1711
+ gap: 0.375rem;
1712
+ padding: 0.25rem 0.625rem;
1713
+ background: var(--musea-bg-secondary);
1714
+ border: 1px solid var(--musea-border);
1715
+ border-radius: var(--musea-radius-sm);
1716
+ font-size: 0.75rem;
1717
+ color: var(--musea-text-muted);
1718
+ }
1719
+
1720
+ .meta-tag svg {
1721
+ width: 12px;
1722
+ height: 12px;
1723
+ }
1724
+
1725
+ /* Gallery Grid */
1726
+ .gallery {
1727
+ display: grid;
1728
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1729
+ gap: 1.25rem;
1730
+ }
1731
+
1732
+ /* Variant Card */
1733
+ .variant-card {
1734
+ background: var(--musea-bg-secondary);
1735
+ border: 1px solid var(--musea-border);
1736
+ border-radius: var(--musea-radius-lg);
1737
+ overflow: hidden;
1738
+ transition: all var(--musea-transition);
1739
+ }
1740
+
1741
+ .variant-card:hover {
1742
+ border-color: var(--musea-text-muted);
1743
+ box-shadow: var(--musea-shadow);
1744
+ transform: translateY(-2px);
1745
+ }
1746
+
1747
+ .variant-preview {
1748
+ aspect-ratio: 16 / 10;
1749
+ background: var(--musea-bg-tertiary);
1750
+ display: flex;
1751
+ align-items: center;
1752
+ justify-content: center;
1753
+ position: relative;
1754
+ overflow: hidden;
1755
+ }
1756
+
1757
+ .variant-preview iframe {
1758
+ width: 100%;
1759
+ height: 100%;
1760
+ border: none;
1761
+ background: white;
1762
+ }
1763
+
1764
+ .variant-preview-placeholder {
1765
+ color: var(--musea-text-muted);
1766
+ font-size: 0.8125rem;
1767
+ text-align: center;
1768
+ padding: 1rem;
1769
+ }
1770
+
1771
+ .variant-preview-code {
1772
+ font-family: 'SF Mono', 'Fira Code', monospace;
1773
+ font-size: 0.75rem;
1774
+ color: var(--musea-text-muted);
1775
+ background: var(--musea-bg-primary);
1776
+ padding: 1rem;
1777
+ overflow: auto;
1778
+ max-height: 100%;
1779
+ width: 100%;
1780
+ }
1781
+
1782
+ .variant-info {
1783
+ padding: 1rem;
1784
+ border-top: 1px solid var(--musea-border);
1785
+ display: flex;
1786
+ align-items: center;
1787
+ justify-content: space-between;
1788
+ }
1789
+
1790
+ .variant-name {
1791
+ font-weight: 600;
1792
+ font-size: 0.875rem;
1793
+ }
1794
+
1795
+ .variant-badge {
1796
+ font-size: 0.625rem;
1797
+ font-weight: 600;
1798
+ text-transform: uppercase;
1799
+ letter-spacing: 0.04em;
1800
+ padding: 0.1875rem 0.5rem;
1801
+ border-radius: 4px;
1802
+ background: var(--musea-accent-subtle);
1803
+ color: var(--musea-accent);
1804
+ }
1805
+
1806
+ .variant-actions {
1807
+ display: flex;
1808
+ gap: 0.5rem;
1809
+ }
1810
+
1811
+ .variant-action-btn {
1812
+ width: 28px;
1813
+ height: 28px;
1814
+ border: none;
1815
+ background: var(--musea-bg-tertiary);
1816
+ border-radius: var(--musea-radius-sm);
1817
+ color: var(--musea-text-muted);
1818
+ cursor: pointer;
1819
+ display: flex;
1820
+ align-items: center;
1821
+ justify-content: center;
1822
+ transition: all var(--musea-transition);
1823
+ }
1824
+
1825
+ .variant-action-btn:hover {
1826
+ background: var(--musea-bg-elevated);
1827
+ color: var(--musea-text);
1828
+ }
1829
+
1830
+ .variant-action-btn svg {
1831
+ width: 14px;
1832
+ height: 14px;
1833
+ }
1834
+
1835
+ /* Empty State */
1836
+ .empty-state {
1837
+ display: flex;
1838
+ flex-direction: column;
1839
+ align-items: center;
1840
+ justify-content: center;
1841
+ min-height: 400px;
1842
+ text-align: center;
1843
+ padding: 2rem;
1844
+ }
1845
+
1846
+ .empty-state-icon {
1847
+ width: 80px;
1848
+ height: 80px;
1849
+ background: var(--musea-bg-secondary);
1850
+ border-radius: var(--musea-radius-lg);
1851
+ display: flex;
1852
+ align-items: center;
1853
+ justify-content: center;
1854
+ margin-bottom: 1.5rem;
1855
+ }
1856
+
1857
+ .empty-state-icon svg {
1858
+ width: 40px;
1859
+ height: 40px;
1860
+ color: var(--musea-text-muted);
1861
+ }
1862
+
1863
+ .empty-state-title {
1864
+ font-size: 1.125rem;
1865
+ font-weight: 600;
1866
+ margin-bottom: 0.5rem;
1867
+ }
1868
+
1869
+ .empty-state-text {
1870
+ color: var(--musea-text-muted);
1871
+ font-size: 0.875rem;
1872
+ max-width: 300px;
1873
+ }
1874
+
1875
+ /* Loading */
1876
+ .loading {
1877
+ display: flex;
1878
+ align-items: center;
1879
+ justify-content: center;
1880
+ min-height: 200px;
1881
+ color: var(--musea-text-muted);
1882
+ gap: 0.75rem;
1883
+ }
1884
+
1885
+ .loading-spinner {
1886
+ width: 20px;
1887
+ height: 20px;
1888
+ border: 2px solid var(--musea-border);
1889
+ border-top-color: var(--musea-accent);
1890
+ border-radius: 50%;
1891
+ animation: spin 0.8s linear infinite;
1892
+ }
1893
+
1894
+ @keyframes spin {
1895
+ to { transform: rotate(360deg); }
1896
+ }
1897
+
1898
+ /* Responsive */
1899
+ @media (max-width: 768px) {
1900
+ .main {
1901
+ grid-template-columns: 1fr;
1902
+ }
1903
+ .sidebar {
1904
+ display: none;
1905
+ }
1906
+ .header-subtitle {
1907
+ display: none;
1908
+ }
1909
+ }
1910
+ </style>
1911
+ </head>
1912
+ <body>
1913
+ <header class="header">
1914
+ <div class="header-left">
1915
+ <a href="${basePath}" class="logo">
1916
+ <svg class="logo-svg" width="32" height="32" viewBox="0 0 200 200" fill="none">
1917
+ <defs>
1918
+ <linearGradient id="metal-grad" x1="0%" y1="0%" x2="100%" y2="20%">
1919
+ <stop offset="0%" stop-color="#f0f2f5"/>
1920
+ <stop offset="50%" stop-color="#9ca3b0"/>
1921
+ <stop offset="100%" stop-color="#e07048"/>
1922
+ </linearGradient>
1923
+ <linearGradient id="metal-grad-dark" x1="0%" y1="0%" x2="100%" y2="30%">
1924
+ <stop offset="0%" stop-color="#d0d4dc"/>
1925
+ <stop offset="60%" stop-color="#6b7280"/>
1926
+ <stop offset="100%" stop-color="#c45530"/>
1927
+ </linearGradient>
1928
+ </defs>
1929
+ <g transform="translate(40, 40)">
1930
+ <g transform="skewX(-12)">
1931
+ <path d="M 100 0 L 60 120 L 105 30 L 100 0 Z" fill="url(#metal-grad-dark)" stroke="#4b5563" stroke-width="0.5"/>
1932
+ <path d="M 30 0 L 60 120 L 80 20 L 30 0 Z" fill="url(#metal-grad)" stroke-width="0.5" stroke-opacity="0.4"/>
1933
+ </g>
1934
+ </g>
1935
+ <g transform="translate(110, 120)">
1936
+ <line x1="5" y1="10" x2="5" y2="50" stroke="#e07048" stroke-width="3" stroke-linecap="round"/>
1937
+ <line x1="60" y1="10" x2="60" y2="50" stroke="#e07048" stroke-width="3" stroke-linecap="round"/>
1938
+ <path d="M 0 10 L 32.5 0 L 65 10" fill="none" stroke="#e07048" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
1939
+ <rect x="15" y="18" width="14" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.7"/>
1940
+ <rect x="36" y="18" width="14" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.7"/>
1941
+ <rect x="23" y="35" width="18" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.6"/>
1942
+ </g>
1943
+ </svg>
1944
+ Musea
1945
+ </a>
1946
+ <span class="header-subtitle">Component Gallery</span>
1947
+ </div>
1948
+ <div class="search-container">
1949
+ <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1950
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
1951
+ </svg>
1952
+ <input type="text" class="search-input" placeholder="Search components..." id="search">
1953
+ </div>
1954
+ </header>
1955
+
1956
+ <main class="main">
1957
+ <aside class="sidebar" id="sidebar">
1958
+ <div class="loading">
1959
+ <div class="loading-spinner"></div>
1960
+ Loading...
1961
+ </div>
1962
+ </aside>
1963
+ <section class="content" id="content">
1964
+ <div class="empty-state">
1965
+ <div class="empty-state-icon">
1966
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1967
+ <path d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5Z"/>
1968
+ <path d="M4 13a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6Z"/>
1969
+ <path d="M16 13a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6Z"/>
1970
+ </svg>
1971
+ </div>
1972
+ <div class="empty-state-title">Select a component</div>
1973
+ <div class="empty-state-text">Choose a component from the sidebar to view its variants and documentation</div>
1974
+ </div>
1975
+ </section>
1976
+ </main>
1977
+
1978
+ <script type="module">
1979
+ const basePath = '${basePath}';
1980
+ let arts = [];
1981
+ let selectedArt = null;
1982
+ let searchQuery = '';
1983
+
1984
+ async function loadArts() {
1985
+ try {
1986
+ const res = await fetch(basePath + '/api/arts');
1987
+ arts = await res.json();
1988
+ renderSidebar();
1989
+ } catch (e) {
1990
+ console.error('Failed to load arts:', e);
1991
+ document.getElementById('sidebar').innerHTML = '<div class="loading">Failed to load</div>';
1992
+ }
1993
+ }
1994
+
1995
+ function renderSidebar() {
1996
+ const sidebar = document.getElementById('sidebar');
1997
+ const categories = {};
1998
+
1999
+ const filtered = searchQuery
2000
+ ? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))
2001
+ : arts;
2002
+
2003
+ for (const art of filtered) {
2004
+ const cat = art.metadata.category || 'Components';
2005
+ if (!categories[cat]) categories[cat] = [];
2006
+ categories[cat].push(art);
2007
+ }
2008
+
2009
+ if (Object.keys(categories).length === 0) {
2010
+ sidebar.innerHTML = '<div class="sidebar-section"><div class="loading">No components found</div></div>';
2011
+ return;
2012
+ }
2013
+
2014
+ let html = '';
2015
+ for (const [category, items] of Object.entries(categories)) {
2016
+ html += '<div class="sidebar-section">';
2017
+ html += '<div class="category-header" data-category="' + category + '">';
2018
+ html += '<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>';
2019
+ html += '<span>' + category + '</span>';
2020
+ html += '<span class="category-count">' + items.length + '</span>';
2021
+ html += '</div>';
2022
+ html += '<ul class="art-list" data-category="' + category + '">';
2023
+ for (const art of items) {
2024
+ const active = selectedArt?.path === art.path ? 'active' : '';
2025
+ const variantCount = art.variants?.length || 0;
2026
+ html += '<li class="art-item ' + active + '" data-path="' + art.path + '">';
2027
+ html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
2028
+ html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
2029
+ html += '</li>';
2030
+ }
2031
+ html += '</ul>';
2032
+ html += '</div>';
2033
+ }
2034
+
2035
+ sidebar.innerHTML = html;
2036
+
2037
+ sidebar.querySelectorAll('.art-item').forEach(item => {
2038
+ item.addEventListener('click', () => {
2039
+ const artPath = item.dataset.path;
2040
+ selectedArt = arts.find(a => a.path === artPath);
2041
+ renderSidebar();
2042
+ renderContent();
2043
+ });
2044
+ });
2045
+
2046
+ sidebar.querySelectorAll('.category-header').forEach(header => {
2047
+ header.addEventListener('click', () => {
2048
+ header.classList.toggle('collapsed');
2049
+ const list = sidebar.querySelector('.art-list[data-category="' + header.dataset.category + '"]');
2050
+ if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
2051
+ });
2052
+ });
2053
+ }
2054
+
2055
+ function renderContent() {
2056
+ const content = document.getElementById('content');
2057
+ if (!selectedArt) {
2058
+ content.innerHTML = \`
2059
+ <div class="empty-state">
2060
+ <div class="empty-state-icon">
2061
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
2062
+ <path d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5Z"/>
2063
+ <path d="M4 13a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6Z"/>
2064
+ <path d="M16 13a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-6Z"/>
2065
+ </svg>
2066
+ </div>
2067
+ <div class="empty-state-title">Select a component</div>
2068
+ <div class="empty-state-text">Choose a component from the sidebar to view its variants</div>
2069
+ </div>
2070
+ \`;
2071
+ return;
2072
+ }
2073
+
2074
+ const meta = selectedArt.metadata;
2075
+ const tags = meta.tags || [];
2076
+ const variantCount = selectedArt.variants?.length || 0;
2077
+
2078
+ let html = '<div class="content-inner">';
2079
+ html += '<div class="content-header">';
2080
+ html += '<h1 class="content-title">' + escapeHtml(meta.title) + '</h1>';
2081
+ if (meta.description) {
2082
+ html += '<p class="content-description">' + escapeHtml(meta.description) + '</p>';
2083
+ }
2084
+ html += '<div class="content-meta">';
2085
+ html += '<span class="meta-tag"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
2086
+ if (meta.category) {
2087
+ html += '<span class="meta-tag"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' + escapeHtml(meta.category) + '</span>';
2088
+ }
2089
+ for (const tag of tags) {
2090
+ html += '<span class="meta-tag">#' + escapeHtml(tag) + '</span>';
2091
+ }
2092
+ html += '</div>';
2093
+ html += '</div>';
2094
+
2095
+ html += '<div class="gallery">';
2096
+ for (const variant of selectedArt.variants) {
2097
+ const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);
2098
+
2099
+ html += '<div class="variant-card">';
2100
+ html += '<div class="variant-preview">';
2101
+ html += '<iframe src="' + previewUrl + '" loading="lazy" title="' + escapeHtml(variant.name) + '"></iframe>';
2102
+ html += '</div>';
2103
+ html += '<div class="variant-info">';
2104
+ html += '<div>';
2105
+ html += '<span class="variant-name">' + escapeHtml(variant.name) + '</span>';
2106
+ if (variant.isDefault) html += ' <span class="variant-badge">Default</span>';
2107
+ html += '</div>';
2108
+ html += '<div class="variant-actions">';
2109
+ html += '<button class="variant-action-btn" title="Open in new tab" onclick="window.open(\\'' + previewUrl + '\\', \\'_blank\\')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>';
2110
+ html += '</div>';
2111
+ html += '</div>';
2112
+ html += '</div>';
2113
+ }
2114
+ html += '</div>';
2115
+ html += '</div>';
2116
+
2117
+ content.innerHTML = html;
2118
+ }
2119
+
2120
+ function escapeHtml(str) {
2121
+ if (!str) return '';
2122
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2123
+ }
2124
+
2125
+ // Search
2126
+ document.getElementById('search').addEventListener('input', (e) => {
2127
+ searchQuery = e.target.value;
2128
+ renderSidebar();
2129
+ });
2130
+
2131
+ // Keyboard shortcut for search
2132
+ document.addEventListener('keydown', (e) => {
2133
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2134
+ e.preventDefault();
2135
+ document.getElementById('search').focus();
2136
+ }
2137
+ });
2138
+
2139
+ loadArts();
2140
+ </script>
2141
+ </body>
2142
+ </html>`;
2143
+ }
2144
+ function generateGalleryModule(basePath) {
2145
+ return `
2146
+ export const basePath = '${basePath}';
2147
+ export async function loadArts() {
2148
+ const res = await fetch(basePath + '/api/arts');
2149
+ return res.json();
2150
+ }
2151
+ `;
2152
+ }
2153
+ const MUSEA_ADDONS_INIT_CODE = `
2154
+ function __museaInitAddons(container, variantName) {
2155
+ // === DOM event capture ===
2156
+ // Note: mousemove, mouseenter, mouseleave, pointermove are excluded as they are too noisy
2157
+ const CAPTURE_EVENTS = ['click','dblclick','input','change','submit','focus','blur','keydown','keyup','mousedown','mouseup','wheel','contextmenu','pointerdown','pointerup'];
2158
+ for (const evt of CAPTURE_EVENTS) {
2159
+ container.addEventListener(evt, (e) => {
2160
+ // Extract raw event properties
2161
+ const rawEvent = {
2162
+ type: e.type,
2163
+ bubbles: e.bubbles,
2164
+ cancelable: e.cancelable,
2165
+ composed: e.composed,
2166
+ defaultPrevented: e.defaultPrevented,
2167
+ eventPhase: e.eventPhase,
2168
+ isTrusted: e.isTrusted,
2169
+ timeStamp: e.timeStamp,
2170
+ };
2171
+ // Mouse/Pointer event properties
2172
+ if ('clientX' in e) {
2173
+ rawEvent.clientX = e.clientX;
2174
+ rawEvent.clientY = e.clientY;
2175
+ rawEvent.screenX = e.screenX;
2176
+ rawEvent.screenY = e.screenY;
2177
+ rawEvent.pageX = e.pageX;
2178
+ rawEvent.pageY = e.pageY;
2179
+ rawEvent.offsetX = e.offsetX;
2180
+ rawEvent.offsetY = e.offsetY;
2181
+ rawEvent.button = e.button;
2182
+ rawEvent.buttons = e.buttons;
2183
+ rawEvent.altKey = e.altKey;
2184
+ rawEvent.ctrlKey = e.ctrlKey;
2185
+ rawEvent.metaKey = e.metaKey;
2186
+ rawEvent.shiftKey = e.shiftKey;
2187
+ }
2188
+ // Keyboard event properties
2189
+ if ('key' in e) {
2190
+ rawEvent.key = e.key;
2191
+ rawEvent.code = e.code;
2192
+ rawEvent.repeat = e.repeat;
2193
+ rawEvent.altKey = e.altKey;
2194
+ rawEvent.ctrlKey = e.ctrlKey;
2195
+ rawEvent.metaKey = e.metaKey;
2196
+ rawEvent.shiftKey = e.shiftKey;
2197
+ }
2198
+ // Input event properties
2199
+ if ('inputType' in e) {
2200
+ rawEvent.inputType = e.inputType;
2201
+ rawEvent.data = e.data;
2202
+ }
2203
+ // Wheel event properties
2204
+ if ('deltaX' in e) {
2205
+ rawEvent.deltaX = e.deltaX;
2206
+ rawEvent.deltaY = e.deltaY;
2207
+ rawEvent.deltaZ = e.deltaZ;
2208
+ rawEvent.deltaMode = e.deltaMode;
2209
+ }
2210
+ const payload = {
2211
+ name: evt,
2212
+ target: e.target?.tagName,
2213
+ timestamp: Date.now(),
2214
+ source: 'dom',
2215
+ rawEvent,
2216
+ variantName
2217
+ };
2218
+ if (e.target && 'value' in e.target) {
2219
+ payload.value = e.target.value;
2220
+ }
2221
+ window.parent.postMessage({ type: 'musea:event', payload }, '*');
2222
+ }, true);
2223
+ }
2224
+
2225
+ // === Message handler for parent commands ===
2226
+ let measureActive = false;
2227
+ let measureOverlay = null;
2228
+ let measureLabel = null;
2229
+
2230
+ function toggleStyleById(id, enabled, css) {
2231
+ let el = document.getElementById(id);
2232
+ if (enabled) {
2233
+ if (!el) {
2234
+ el = document.createElement('style');
2235
+ el.id = id;
2236
+ el.textContent = css;
2237
+ document.head.appendChild(el);
2238
+ }
2239
+ } else {
2240
+ if (el) el.remove();
2241
+ }
2242
+ }
2243
+
2244
+ function createMeasureOverlay() {
2245
+ if (measureOverlay) return;
2246
+ measureOverlay = document.createElement('div');
2247
+ measureOverlay.id = 'musea-measure-overlay';
2248
+ measureOverlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
2249
+ document.body.appendChild(measureOverlay);
2250
+
2251
+ measureLabel = document.createElement('div');
2252
+ measureLabel.className = 'musea-measure-label';
2253
+ measureLabel.style.cssText = 'position:fixed;background:#333;color:#fff;font-size:11px;padding:2px 6px;border-radius:3px;pointer-events:none;z-index:100000;display:none;';
2254
+ document.body.appendChild(measureLabel);
2255
+ }
2256
+
2257
+ function removeMeasureOverlay() {
2258
+ if (measureOverlay) { measureOverlay.remove(); measureOverlay = null; }
2259
+ if (measureLabel) { measureLabel.remove(); measureLabel = null; }
2260
+ }
2261
+
2262
+ function onMeasureMouseMove(e) {
2263
+ if (!measureActive || !measureOverlay) return;
2264
+ const el = document.elementFromPoint(e.clientX, e.clientY);
2265
+ if (!el || el === measureOverlay || el === measureLabel) return;
2266
+
2267
+ const rect = el.getBoundingClientRect();
2268
+ const cs = getComputedStyle(el);
2269
+ const mt = parseFloat(cs.marginTop) || 0;
2270
+ const mr = parseFloat(cs.marginRight) || 0;
2271
+ const mb = parseFloat(cs.marginBottom) || 0;
2272
+ const ml = parseFloat(cs.marginLeft) || 0;
2273
+ const bt = parseFloat(cs.borderTopWidth) || 0;
2274
+ const br = parseFloat(cs.borderRightWidth) || 0;
2275
+ const bb = parseFloat(cs.borderBottomWidth) || 0;
2276
+ const blw = parseFloat(cs.borderLeftWidth) || 0;
2277
+ const pt = parseFloat(cs.paddingTop) || 0;
2278
+ const pr = parseFloat(cs.paddingRight) || 0;
2279
+ const pb = parseFloat(cs.paddingBottom) || 0;
2280
+ const pl = parseFloat(cs.paddingLeft) || 0;
2281
+
2282
+ const cw = rect.width - blw - br - pl - pr;
2283
+ const ch = rect.height - bt - bb - pt - pb;
2284
+
2285
+ measureOverlay.innerHTML = ''
2286
+ // Margin
2287
+ + '<div style="position:fixed;background:rgba(255,165,0,0.3);'
2288
+ + 'left:' + (rect.left - ml) + 'px;top:' + (rect.top - mt) + 'px;'
2289
+ + 'width:' + (rect.width + ml + mr) + 'px;height:' + mt + 'px;"></div>'
2290
+ + '<div style="position:fixed;background:rgba(255,165,0,0.3);'
2291
+ + 'left:' + (rect.left - ml) + 'px;top:' + (rect.bottom) + 'px;'
2292
+ + 'width:' + (rect.width + ml + mr) + 'px;height:' + mb + 'px;"></div>'
2293
+ + '<div style="position:fixed;background:rgba(255,165,0,0.3);'
2294
+ + 'left:' + (rect.left - ml) + 'px;top:' + rect.top + 'px;'
2295
+ + 'width:' + ml + 'px;height:' + rect.height + 'px;"></div>'
2296
+ + '<div style="position:fixed;background:rgba(255,165,0,0.3);'
2297
+ + 'left:' + rect.right + 'px;top:' + rect.top + 'px;'
2298
+ + 'width:' + mr + 'px;height:' + rect.height + 'px;"></div>'
2299
+ // Border
2300
+ + '<div style="position:fixed;background:rgba(255,255,0,0.3);'
2301
+ + 'left:' + rect.left + 'px;top:' + rect.top + 'px;'
2302
+ + 'width:' + rect.width + 'px;height:' + bt + 'px;"></div>'
2303
+ + '<div style="position:fixed;background:rgba(255,255,0,0.3);'
2304
+ + 'left:' + rect.left + 'px;top:' + (rect.bottom - bb) + 'px;'
2305
+ + 'width:' + rect.width + 'px;height:' + bb + 'px;"></div>'
2306
+ + '<div style="position:fixed;background:rgba(255,255,0,0.3);'
2307
+ + 'left:' + rect.left + 'px;top:' + (rect.top + bt) + 'px;'
2308
+ + 'width:' + blw + 'px;height:' + (rect.height - bt - bb) + 'px;"></div>'
2309
+ + '<div style="position:fixed;background:rgba(255,255,0,0.3);'
2310
+ + 'left:' + (rect.right - br) + 'px;top:' + (rect.top + bt) + 'px;'
2311
+ + 'width:' + br + 'px;height:' + (rect.height - bt - bb) + 'px;"></div>'
2312
+ // Padding
2313
+ + '<div style="position:fixed;background:rgba(144,238,144,0.3);'
2314
+ + 'left:' + (rect.left + blw) + 'px;top:' + (rect.top + bt) + 'px;'
2315
+ + 'width:' + (rect.width - blw - br) + 'px;height:' + pt + 'px;"></div>'
2316
+ + '<div style="position:fixed;background:rgba(144,238,144,0.3);'
2317
+ + 'left:' + (rect.left + blw) + 'px;top:' + (rect.bottom - bb - pb) + 'px;'
2318
+ + 'width:' + (rect.width - blw - br) + 'px;height:' + pb + 'px;"></div>'
2319
+ + '<div style="position:fixed;background:rgba(144,238,144,0.3);'
2320
+ + 'left:' + (rect.left + blw) + 'px;top:' + (rect.top + bt + pt) + 'px;'
2321
+ + 'width:' + pl + 'px;height:' + (rect.height - bt - bb - pt - pb) + 'px;"></div>'
2322
+ + '<div style="position:fixed;background:rgba(144,238,144,0.3);'
2323
+ + 'left:' + (rect.right - br - pr) + 'px;top:' + (rect.top + bt + pt) + 'px;'
2324
+ + 'width:' + pr + 'px;height:' + (rect.height - bt - bb - pt - pb) + 'px;"></div>'
2325
+ // Content
2326
+ + '<div style="position:fixed;background:rgba(100,149,237,0.3);'
2327
+ + 'left:' + (rect.left + blw + pl) + 'px;top:' + (rect.top + bt + pt) + 'px;'
2328
+ + 'width:' + cw + 'px;height:' + ch + 'px;"></div>';
2329
+
2330
+ // Label
2331
+ measureLabel.textContent = Math.round(rect.width) + ' x ' + Math.round(rect.height);
2332
+ measureLabel.style.display = 'block';
2333
+ measureLabel.style.left = (rect.right + 8) + 'px';
2334
+ measureLabel.style.top = rect.top + 'px';
2335
+ }
2336
+
2337
+ window.addEventListener('message', (e) => {
2338
+ if (!e.data?.type?.startsWith('musea:')) return;
2339
+ const { type, payload } = e.data;
2340
+ switch (type) {
2341
+ case 'musea:set-background': {
2342
+ if (payload.pattern === 'checkerboard') {
2343
+ document.body.style.background = '';
2344
+ document.body.classList.add('musea-bg-checkerboard');
2345
+ } else {
2346
+ document.body.classList.remove('musea-bg-checkerboard');
2347
+ document.body.style.background = payload.color || '';
2348
+ }
2349
+ break;
2350
+ }
2351
+ case 'musea:toggle-outline': {
2352
+ toggleStyleById('musea-outline', payload.enabled,
2353
+ '* { outline: 1px solid rgba(255, 0, 0, 0.3) !important; }');
2354
+ break;
2355
+ }
2356
+ case 'musea:toggle-measure': {
2357
+ measureActive = payload.enabled;
2358
+ if (measureActive) {
2359
+ createMeasureOverlay();
2360
+ document.addEventListener('mousemove', onMeasureMouseMove);
2361
+ } else {
2362
+ document.removeEventListener('mousemove', onMeasureMouseMove);
2363
+ removeMeasureOverlay();
2364
+ }
2365
+ break;
2366
+ }
2367
+ case 'musea:set-props': {
2368
+ // Store props for remount - handled by preview module
2369
+ if (window.__museaSetProps) {
2370
+ window.__museaSetProps(payload.props || {});
2371
+ }
2372
+ break;
2373
+ }
2374
+ case 'musea:set-slots': {
2375
+ // Store slots for remount - handled by preview module
2376
+ if (window.__museaSetSlots) {
2377
+ window.__museaSetSlots(payload.slots || {});
2378
+ }
2379
+ break;
2380
+ }
2381
+ case 'musea:run-a11y': {
2382
+ // Run axe-core a11y test
2383
+ (async () => {
2384
+ try {
2385
+ // Dynamically load axe-core from CDN if not already loaded
2386
+ if (!window.axe) {
2387
+ const script = document.createElement('script');
2388
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
2389
+ script.crossOrigin = 'anonymous';
2390
+ await new Promise((resolve, reject) => {
2391
+ script.onload = resolve;
2392
+ script.onerror = reject;
2393
+ document.head.appendChild(script);
2394
+ });
2395
+ }
2396
+ // Run axe-core on the .musea-variant container only (not the full document)
2397
+ const context = document.querySelector('.musea-variant') || document;
2398
+ const results = await window.axe.run(context, {
2399
+ // Run all rules without restrictions for comprehensive testing
2400
+ resultTypes: ['violations', 'incomplete', 'passes']
2401
+ });
2402
+ window.parent.postMessage({
2403
+ type: 'musea:a11y-result',
2404
+ payload: {
2405
+ violations: results.violations.map(v => ({
2406
+ id: v.id,
2407
+ impact: v.impact,
2408
+ description: v.description,
2409
+ helpUrl: v.helpUrl,
2410
+ nodes: v.nodes.map(n => ({
2411
+ html: n.html,
2412
+ target: n.target,
2413
+ failureSummary: n.failureSummary
2414
+ }))
2415
+ })),
2416
+ passes: results.passes.length,
2417
+ incomplete: results.incomplete.length
2418
+ }
2419
+ }, '*');
2420
+ } catch (err) {
2421
+ window.parent.postMessage({
2422
+ type: 'musea:a11y-result',
2423
+ payload: {
2424
+ error: err instanceof Error ? err.message : String(err),
2425
+ violations: [],
2426
+ passes: 0,
2427
+ incomplete: 0
2428
+ }
2429
+ }, '*');
2430
+ }
2431
+ })();
2432
+ break;
2433
+ }
2434
+ }
2435
+ });
2436
+
2437
+ // Notify parent that iframe is ready
2438
+ window.parent.postMessage({ type: 'musea:ready', payload: {} }, '*');
2439
+ }
2440
+ `;
2441
+ function generatePreviewModule(art, variantComponentName, variantName) {
2442
+ const artModuleId = `virtual:musea-art:${art.path}`;
2443
+ const escapedVariantName = escapeTemplate(variantName);
2444
+ return `
2445
+ import { createApp, reactive, h } from 'vue';
2446
+ import * as artModule from '${artModuleId}';
2447
+
2448
+ const container = document.getElementById('app');
2449
+
2450
+ ${MUSEA_ADDONS_INIT_CODE}
2451
+
2452
+ let currentApp = null;
2453
+ const propsOverride = reactive({});
2454
+ const slotsOverride = reactive({ default: '' });
2455
+
2456
+ window.__museaSetProps = (props) => {
2457
+ // Clear old keys
2458
+ for (const key of Object.keys(propsOverride)) {
2459
+ delete propsOverride[key];
2460
+ }
2461
+ Object.assign(propsOverride, props);
2462
+ };
2463
+
2464
+ window.__museaSetSlots = (slots) => {
2465
+ Object.assign(slotsOverride, slots);
2466
+ };
2467
+
2468
+ async function mount() {
2469
+ try {
2470
+ // Get the specific variant component
2471
+ const VariantComponent = artModule['${variantComponentName}'];
2472
+ const RawComponent = artModule.__component__;
2473
+
2474
+ if (!VariantComponent) {
2475
+ throw new Error('Variant component "${variantComponentName}" not found in art module');
2476
+ }
2477
+
2478
+ // Create and mount the app
2479
+ const app = createApp(VariantComponent);
2480
+ container.innerHTML = '';
2481
+ container.className = 'musea-variant';
2482
+ app.mount(container);
2483
+ currentApp = app;
2484
+
2485
+ console.log('[musea-preview] Mounted variant: ${escapedVariantName}');
2486
+ __museaInitAddons(container, '${escapedVariantName}');
2487
+
2488
+ // Override set-props to remount with raw component + props
2489
+ if (RawComponent) {
2490
+ window.__museaSetProps = (props) => {
2491
+ for (const key of Object.keys(propsOverride)) {
2492
+ delete propsOverride[key];
2493
+ }
2494
+ Object.assign(propsOverride, props);
2495
+ remountWithProps(RawComponent);
2496
+ };
2497
+ window.__museaSetSlots = (slots) => {
2498
+ Object.assign(slotsOverride, slots);
2499
+ remountWithProps(RawComponent);
2500
+ };
2501
+ }
2502
+ } catch (error) {
2503
+ console.error('[musea-preview] Failed to mount:', error);
2504
+ container.innerHTML = \`
2505
+ <div class="musea-error">
2506
+ <div class="musea-error-title">Failed to render component</div>
2507
+ <div>\${error.message}</div>
2508
+ <pre>\${error.stack || ''}</pre>
2509
+ </div>
2510
+ \`;
2511
+ }
2512
+ }
2513
+
2514
+ function remountWithProps(Component) {
2515
+ if (currentApp) {
2516
+ currentApp.unmount();
2517
+ }
2518
+ const app = createApp({
2519
+ setup() {
2520
+ return () => {
2521
+ const slotFns = {};
2522
+ if (slotsOverride.default) {
2523
+ slotFns.default = () => h('span', { innerHTML: slotsOverride.default });
2524
+ }
2525
+ return h('div', { class: 'musea-variant' }, [
2526
+ h(Component, { ...propsOverride }, slotFns)
2527
+ ]);
2528
+ };
2529
+ }
2530
+ });
2531
+ container.innerHTML = '';
2532
+ app.mount(container);
2533
+ currentApp = app;
2534
+ }
2535
+
2536
+ mount();
2537
+ `;
2538
+ }
2539
+ function generateManifestModule(artFiles) {
2540
+ const arts = Array.from(artFiles.values());
2541
+ return `export const arts = ${JSON.stringify(arts, null, 2)};`;
2542
+ }
2543
+ function generateArtModule(art, filePath) {
2544
+ let componentImportPath;
2545
+ let componentName;
2546
+ if (art.isInline && art.componentPath) {
2547
+ componentImportPath = art.componentPath;
2548
+ componentName = path.basename(art.componentPath, ".vue");
2549
+ } else if (art.metadata.component) {
2550
+ const comp = art.metadata.component;
2551
+ componentImportPath = path.isAbsolute(comp) ? comp : path.resolve(path.dirname(filePath), comp);
2552
+ componentName = path.basename(comp, ".vue");
2553
+ }
2554
+ let code = `
2555
+ // Auto-generated module for: ${path.basename(filePath)}
2556
+ import { defineComponent, h } from 'vue';
2557
+ `;
2558
+ if (componentImportPath && componentName) {
2559
+ code += `import ${componentName} from '${componentImportPath}';\n`;
2560
+ code += `export const __component__ = ${componentName};\n`;
2561
+ }
2562
+ code += `
2563
+ export const metadata = ${JSON.stringify(art.metadata)};
2564
+ export const variants = ${JSON.stringify(art.variants)};
2565
+ `;
2566
+ for (const variant of art.variants) {
2567
+ const variantComponentName = toPascalCase(variant.name);
2568
+ let template = variant.template;
2569
+ if (componentName) template = template.replace(/<Self/g, `<${componentName}`).replace(/<\/Self>/g, `</${componentName}>`);
2570
+ const escapedTemplate = template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
2571
+ const fullTemplate = `<div class="musea-variant" data-variant="${variant.name}">${escapedTemplate}</div>`;
2572
+ if (componentName) code += `
2573
+ export const ${variantComponentName} = {
2574
+ name: '${variantComponentName}',
2575
+ components: { ${componentName} },
2576
+ template: \`${fullTemplate}\`,
2577
+ };
2578
+ `;
2579
+ else code += `
2580
+ export const ${variantComponentName} = {
2581
+ name: '${variantComponentName}',
2582
+ template: \`${fullTemplate}\`,
2583
+ };
2584
+ `;
2585
+ }
2586
+ const defaultVariant = art.variants.find((v) => v.isDefault) || art.variants[0];
2587
+ if (defaultVariant) code += `
2588
+ export default ${toPascalCase(defaultVariant.name)};
2589
+ `;
2590
+ return code;
2591
+ }
2592
+ async function generateStorybookFiles(artFiles, root, outDir) {
2593
+ const binding = loadNative();
2594
+ const outputDir = path.resolve(root, outDir);
2595
+ await fs.promises.mkdir(outputDir, { recursive: true });
2596
+ for (const [filePath, _art] of artFiles) try {
2597
+ const source = await fs.promises.readFile(filePath, "utf-8");
2598
+ const csf = binding.artToCsf(source, { filename: filePath });
2599
+ const outputPath = path.join(outputDir, csf.filename);
2600
+ await fs.promises.writeFile(outputPath, csf.code, "utf-8");
2601
+ console.log(`[musea] Generated: ${path.relative(root, outputPath)}`);
2602
+ } catch (e) {
2603
+ console.error(`[musea] Failed to generate CSF for ${filePath}:`, e);
2604
+ }
2605
+ }
2606
+ function toPascalCase(str) {
2607
+ return str.split(/[\s\-_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
2608
+ }
2609
+ function escapeTemplate(str) {
2610
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
2611
+ }
2612
+ function generatePreviewModuleWithProps(art, variantComponentName, variantName, propsOverride) {
2613
+ const artModuleId = `virtual:musea-art:${art.path}`;
2614
+ const escapedVariantName = escapeTemplate(variantName);
2615
+ const propsJson = JSON.stringify(propsOverride);
2616
+ return `
2617
+ import { createApp, h } from 'vue';
2618
+ import * as artModule from '${artModuleId}';
2619
+
2620
+ const container = document.getElementById('app');
2621
+ const propsOverride = ${propsJson};
2622
+
2623
+ ${MUSEA_ADDONS_INIT_CODE}
2624
+
2625
+ async function mount() {
2626
+ try {
2627
+ const VariantComponent = artModule['${variantComponentName}'];
2628
+ if (!VariantComponent) {
2629
+ throw new Error('Variant component "${variantComponentName}" not found');
2630
+ }
2631
+
2632
+ const WrappedComponent = {
2633
+ render() {
2634
+ return h(VariantComponent, propsOverride);
2635
+ }
2636
+ };
2637
+
2638
+ const app = createApp(WrappedComponent);
2639
+ container.innerHTML = '';
2640
+ container.className = 'musea-variant';
2641
+ app.mount(container);
2642
+ console.log('[musea-preview] Mounted variant: ${escapedVariantName} with props override');
2643
+ __museaInitAddons(container, '${escapedVariantName}');
2644
+ } catch (error) {
2645
+ console.error('[musea-preview] Failed to mount:', error);
2646
+ container.innerHTML = '<div class="musea-error"><div class="musea-error-title">Failed to render</div><div>' + error.message + '</div></div>';
2647
+ }
2648
+ }
2649
+
2650
+ mount();
2651
+ `;
2652
+ }
2653
+ function generatePreviewHtml(art, variant, basePath) {
2654
+ const previewModuleUrl = `${basePath}/preview-module?art=${encodeURIComponent(art.path)}&variant=${encodeURIComponent(variant.name)}`;
2655
+ return `<!DOCTYPE html>
2656
+ <html lang="en">
2657
+ <head>
2658
+ <meta charset="UTF-8">
2659
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2660
+ <title>${escapeHtml(art.metadata.title)} - ${escapeHtml(variant.name)}</title>
2661
+ <style>
2662
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2663
+ html, body {
2664
+ width: 100%;
2665
+ height: 100%;
2666
+ }
2667
+ body {
2668
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2669
+ background: #ffffff;
2670
+ }
2671
+ .musea-variant {
2672
+ padding: 1.5rem;
2673
+ display: flex;
2674
+ align-items: center;
2675
+ justify-content: center;
2676
+ min-height: 100vh;
2677
+ }
2678
+ .musea-error {
2679
+ color: #dc2626;
2680
+ background: #fef2f2;
2681
+ border: 1px solid #fecaca;
2682
+ border-radius: 8px;
2683
+ padding: 1rem;
2684
+ font-size: 0.875rem;
2685
+ max-width: 400px;
2686
+ }
2687
+ .musea-error-title {
2688
+ font-weight: 600;
2689
+ margin-bottom: 0.5rem;
2690
+ }
2691
+ .musea-error pre {
2692
+ font-family: monospace;
2693
+ font-size: 0.75rem;
2694
+ white-space: pre-wrap;
2695
+ word-break: break-all;
2696
+ margin-top: 0.5rem;
2697
+ padding: 0.5rem;
2698
+ background: #fff;
2699
+ border-radius: 4px;
2700
+ }
2701
+ .musea-loading {
2702
+ display: flex;
2703
+ align-items: center;
2704
+ gap: 0.75rem;
2705
+ color: #6b7280;
2706
+ font-size: 0.875rem;
2707
+ }
2708
+ .musea-spinner {
2709
+ width: 20px;
2710
+ height: 20px;
2711
+ border: 2px solid #e5e7eb;
2712
+ border-top-color: #3b82f6;
2713
+ border-radius: 50%;
2714
+ animation: spin 0.8s linear infinite;
2715
+ }
2716
+ @keyframes spin { to { transform: rotate(360deg); } }
2717
+
2718
+ /* Musea Addons: Checkerboard background for transparent mode */
2719
+ .musea-bg-checkerboard {
2720
+ background-image:
2721
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
2722
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
2723
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
2724
+ linear-gradient(-45deg, transparent 75%, #ccc 75%) !important;
2725
+ background-size: 20px 20px !important;
2726
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0 !important;
2727
+ }
2728
+
2729
+ /* Musea Addons: Measure label */
2730
+ .musea-measure-label {
2731
+ position: fixed;
2732
+ background: #333;
2733
+ color: #fff;
2734
+ font-size: 11px;
2735
+ padding: 2px 6px;
2736
+ border-radius: 3px;
2737
+ pointer-events: none;
2738
+ z-index: 100000;
2739
+ }
2740
+ </style>
2741
+ </head>
2742
+ <body>
2743
+ <div id="app" class="musea-variant" data-art="${escapeHtml(art.path)}" data-variant="${escapeHtml(variant.name)}">
2744
+ <div class="musea-loading">
2745
+ <div class="musea-spinner"></div>
2746
+ Loading component...
2747
+ </div>
2748
+ </div>
2749
+ <script type="module" src="${previewModuleUrl}"></script>
2750
+ </body>
2751
+ </html>`;
2752
+ }
2753
+ function escapeHtml(str) {
2754
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
2755
+ }
2756
+ var src_default = musea;
2757
+
2758
+ //#endregion
2759
+ export { MuseaA11yRunner, MuseaVrtRunner, buildTokenMap, src_default as default, generateArtFile, generateTokensHtml, generateTokensMarkdown, generateVrtJsonReport, generateVrtReport, musea, parseTokens, processStyleDictionary, resolveReferences, scanTokenUsage, writeArtFile };
2760
+ //# sourceMappingURL=index.js.map