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

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,1512 @@
1
+ import { MuseaVrtRunner, generateVrtJsonReport, generateVrtReport } from "./vrt-DRwtnkE5.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ //#region src/style-dictionary.ts
7
+ /**
8
+ * Parse Style Dictionary tokens file.
9
+ */
10
+ async function parseTokens(tokensPath) {
11
+ const absolutePath = path.resolve(tokensPath);
12
+ const stat = await fs.promises.stat(absolutePath);
13
+ if (stat.isDirectory()) return parseTokenDirectory(absolutePath);
14
+ const content = await fs.promises.readFile(absolutePath, "utf-8");
15
+ const tokens = JSON.parse(content);
16
+ return flattenTokens(tokens);
17
+ }
18
+ /**
19
+ * Parse tokens from a directory.
20
+ */
21
+ async function parseTokenDirectory(dirPath) {
22
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
23
+ const categories = [];
24
+ for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
25
+ const filePath = path.join(dirPath, entry.name);
26
+ const content = await fs.promises.readFile(filePath, "utf-8");
27
+ const tokens = JSON.parse(content);
28
+ const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
29
+ categories.push({
30
+ name: formatCategoryName(categoryName),
31
+ tokens: extractTokens(tokens),
32
+ subcategories: extractSubcategories(tokens)
33
+ });
34
+ }
35
+ return categories;
36
+ }
37
+ /**
38
+ * Flatten nested token structure into categories.
39
+ */
40
+ function flattenTokens(tokens, prefix = []) {
41
+ const categories = [];
42
+ for (const [key, value] of Object.entries(tokens)) {
43
+ if (isTokenValue(value)) continue;
44
+ if (typeof value === "object" && value !== null) {
45
+ const categoryTokens = extractTokens(value);
46
+ const subcategories = flattenTokens(value, [...prefix, key]);
47
+ if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
48
+ name: formatCategoryName(key),
49
+ tokens: categoryTokens,
50
+ subcategories: subcategories.length > 0 ? subcategories : void 0
51
+ });
52
+ }
53
+ }
54
+ return categories;
55
+ }
56
+ /**
57
+ * Extract token values from an object.
58
+ */
59
+ function extractTokens(obj) {
60
+ const tokens = {};
61
+ for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
62
+ return tokens;
63
+ }
64
+ /**
65
+ * Extract subcategories from an object.
66
+ */
67
+ function extractSubcategories(obj) {
68
+ const subcategories = [];
69
+ for (const [key, value] of Object.entries(obj)) if (!isTokenValue(value) && typeof value === "object" && value !== null) {
70
+ const categoryTokens = extractTokens(value);
71
+ const nested = extractSubcategories(value);
72
+ if (Object.keys(categoryTokens).length > 0 || nested && nested.length > 0) subcategories.push({
73
+ name: formatCategoryName(key),
74
+ tokens: categoryTokens,
75
+ subcategories: nested
76
+ });
77
+ }
78
+ return subcategories.length > 0 ? subcategories : void 0;
79
+ }
80
+ /**
81
+ * Check if a value is a token definition.
82
+ */
83
+ function isTokenValue(value) {
84
+ if (typeof value !== "object" || value === null) return false;
85
+ const obj = value;
86
+ return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number");
87
+ }
88
+ /**
89
+ * Normalize token to DesignToken interface.
90
+ */
91
+ function normalizeToken(raw) {
92
+ return {
93
+ value: raw.value,
94
+ type: raw.type,
95
+ description: raw.description,
96
+ attributes: raw.attributes
97
+ };
98
+ }
99
+ /**
100
+ * Format category name for display.
101
+ */
102
+ function formatCategoryName(name) {
103
+ return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
104
+ }
105
+ /**
106
+ * Generate HTML documentation for tokens.
107
+ */
108
+ function generateTokensHtml(categories) {
109
+ const renderToken = (name, token) => {
110
+ const isColor = typeof token.value === "string" && (token.value.startsWith("#") || token.value.startsWith("rgb") || token.value.startsWith("hsl") || token.type === "color");
111
+ return `
112
+ <div class="token">
113
+ <div class="token-preview">
114
+ ${isColor ? `<div class="color-swatch" style="background: ${token.value}"></div>` : ""}
115
+ </div>
116
+ <div class="token-info">
117
+ <div class="token-name">${name}</div>
118
+ <div class="token-value">${token.value}</div>
119
+ ${token.description ? `<div class="token-description">${token.description}</div>` : ""}
120
+ </div>
121
+ </div>
122
+ `;
123
+ };
124
+ const renderCategory = (category, level = 2) => {
125
+ const heading = `h${Math.min(level, 6)}`;
126
+ let html = `<${heading}>${category.name}</${heading}>`;
127
+ html += "<div class=\"tokens-grid\">";
128
+ for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
129
+ html += "</div>";
130
+ if (category.subcategories) for (const sub of category.subcategories) html += renderCategory(sub, level + 1);
131
+ return html;
132
+ };
133
+ return `<!DOCTYPE html>
134
+ <html lang="en">
135
+ <head>
136
+ <meta charset="UTF-8">
137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
138
+ <title>Design Tokens - Musea</title>
139
+ <style>
140
+ :root {
141
+ --musea-bg: #0d0d0d;
142
+ --musea-bg-secondary: #1a1815;
143
+ --musea-text: #e6e9f0;
144
+ --musea-text-muted: #7b8494;
145
+ --musea-accent: #a34828;
146
+ --musea-border: #3a3530;
147
+ }
148
+ * { box-sizing: border-box; margin: 0; padding: 0; }
149
+ body {
150
+ font-family: 'Inter', -apple-system, sans-serif;
151
+ background: var(--musea-bg);
152
+ color: var(--musea-text);
153
+ line-height: 1.6;
154
+ padding: 2rem;
155
+ }
156
+ h1 { margin-bottom: 2rem; color: var(--musea-accent); }
157
+ h2 { margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--musea-border); }
158
+ h3, h4, h5, h6 { margin: 1.5rem 0 0.75rem; }
159
+ .tokens-grid {
160
+ display: grid;
161
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
162
+ gap: 1rem;
163
+ margin-bottom: 1.5rem;
164
+ }
165
+ .token {
166
+ background: var(--musea-bg-secondary);
167
+ border: 1px solid var(--musea-border);
168
+ border-radius: 8px;
169
+ padding: 1rem;
170
+ display: flex;
171
+ gap: 1rem;
172
+ align-items: center;
173
+ }
174
+ .token-preview {
175
+ flex-shrink: 0;
176
+ width: 48px;
177
+ height: 48px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ }
182
+ .color-swatch {
183
+ width: 48px;
184
+ height: 48px;
185
+ border-radius: 8px;
186
+ border: 1px solid var(--musea-border);
187
+ }
188
+ .token-info {
189
+ flex: 1;
190
+ min-width: 0;
191
+ }
192
+ .token-name {
193
+ font-weight: 600;
194
+ font-family: 'JetBrains Mono', monospace;
195
+ font-size: 0.875rem;
196
+ }
197
+ .token-value {
198
+ color: var(--musea-text-muted);
199
+ font-family: 'JetBrains Mono', monospace;
200
+ font-size: 0.75rem;
201
+ word-break: break-all;
202
+ }
203
+ .token-description {
204
+ color: var(--musea-text-muted);
205
+ font-size: 0.75rem;
206
+ margin-top: 0.25rem;
207
+ }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <h1>Design Tokens</h1>
212
+ ${categories.map((cat) => renderCategory(cat)).join("")}
213
+ </body>
214
+ </html>`;
215
+ }
216
+ /**
217
+ * Generate Markdown documentation for tokens.
218
+ */
219
+ function generateTokensMarkdown(categories) {
220
+ const renderCategory = (category, level = 2) => {
221
+ const heading = "#".repeat(level);
222
+ let md = `\n${heading} ${category.name}\n\n`;
223
+ if (Object.keys(category.tokens).length > 0) {
224
+ md += "| Token | Value | Description |\n";
225
+ md += "|-------|-------|-------------|\n";
226
+ for (const [name, token] of Object.entries(category.tokens)) {
227
+ const desc = token.description || "-";
228
+ md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
229
+ }
230
+ md += "\n";
231
+ }
232
+ if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
233
+ return md;
234
+ };
235
+ let markdown = "# Design Tokens\n\n";
236
+ markdown += `> Generated by Musea on ${new Date().toISOString()}\n`;
237
+ for (const category of categories) markdown += renderCategory(category);
238
+ return markdown;
239
+ }
240
+ /**
241
+ * Style Dictionary plugin for Musea.
242
+ */
243
+ async function processStyleDictionary(config) {
244
+ const categories = await parseTokens(config.tokensPath);
245
+ const outputDir = config.outputDir ?? ".vize/tokens";
246
+ const outputFormat = config.outputFormat ?? "html";
247
+ await fs.promises.mkdir(outputDir, { recursive: true });
248
+ let content;
249
+ let filename;
250
+ switch (outputFormat) {
251
+ case "html":
252
+ content = generateTokensHtml(categories);
253
+ filename = "tokens.html";
254
+ break;
255
+ case "markdown":
256
+ content = generateTokensMarkdown(categories);
257
+ filename = "tokens.md";
258
+ break;
259
+ case "json":
260
+ default:
261
+ content = JSON.stringify({ categories }, null, 2);
262
+ filename = "tokens.json";
263
+ }
264
+ const outputPath = path.join(outputDir, filename);
265
+ await fs.promises.writeFile(outputPath, content, "utf-8");
266
+ console.log(`[musea] Generated token documentation: ${outputPath}`);
267
+ return {
268
+ categories,
269
+ metadata: {
270
+ name: path.basename(config.tokensPath),
271
+ generatedAt: new Date().toISOString()
272
+ }
273
+ };
274
+ }
275
+
276
+ //#endregion
277
+ //#region src/index.ts
278
+ const VIRTUAL_MUSEA_PREFIX = "\0musea:";
279
+ const VIRTUAL_GALLERY = "\0musea-gallery";
280
+ const VIRTUAL_MANIFEST = "\0musea-manifest";
281
+ let native = null;
282
+ function loadNative() {
283
+ if (native) return native;
284
+ const require = createRequire(import.meta.url);
285
+ try {
286
+ native = require("@vizejs/native");
287
+ return native;
288
+ } catch (e) {
289
+ throw new Error(`Failed to load @vizejs/native. Make sure it's installed and built:\n${String(e)}`);
290
+ }
291
+ }
292
+ /**
293
+ * Create Musea Vite plugin.
294
+ */
295
+ function musea(options = {}) {
296
+ const include = options.include ?? ["**/*.art.vue"];
297
+ const exclude = options.exclude ?? ["node_modules/**", "dist/**"];
298
+ const basePath = options.basePath ?? "/__musea__";
299
+ const storybookCompat = options.storybookCompat ?? false;
300
+ const storybookOutDir = options.storybookOutDir ?? ".storybook/stories";
301
+ let config;
302
+ let server = null;
303
+ const artFiles = new Map();
304
+ const mainPlugin = {
305
+ name: "vite-plugin-musea",
306
+ enforce: "pre",
307
+ config() {
308
+ return { resolve: { alias: { vue: "vue/dist/vue.esm-bundler.js" } } };
309
+ },
310
+ configResolved(resolvedConfig) {
311
+ config = resolvedConfig;
312
+ },
313
+ configureServer(devServer) {
314
+ server = devServer;
315
+ devServer.middlewares.use(basePath, async (req, res, next) => {
316
+ if (req.url === "/" || req.url === "/index.html") {
317
+ const html = generateGalleryHtml(basePath);
318
+ res.setHeader("Content-Type", "text/html");
319
+ res.end(html);
320
+ return;
321
+ }
322
+ next();
323
+ });
324
+ devServer.middlewares.use(`${basePath}/preview-module`, async (req, res, _next) => {
325
+ const url = new URL(req.url || "", `http://localhost`);
326
+ const artPath = url.searchParams.get("art");
327
+ const variantName = url.searchParams.get("variant");
328
+ if (!artPath || !variantName) {
329
+ res.statusCode = 400;
330
+ res.end("Missing art or variant parameter");
331
+ return;
332
+ }
333
+ const art = artFiles.get(artPath);
334
+ if (!art) {
335
+ res.statusCode = 404;
336
+ res.end("Art not found");
337
+ return;
338
+ }
339
+ const variant = art.variants.find((v) => v.name === variantName);
340
+ if (!variant) {
341
+ res.statusCode = 404;
342
+ res.end("Variant not found");
343
+ return;
344
+ }
345
+ const variantComponentName = toPascalCase(variant.name);
346
+ const moduleCode = generatePreviewModule(art, variantComponentName, variant.name);
347
+ try {
348
+ const result = await devServer.transformRequest(`virtual:musea-preview:${artPath}:${variantName}`);
349
+ if (result) {
350
+ res.setHeader("Content-Type", "application/javascript");
351
+ res.setHeader("Cache-Control", "no-cache");
352
+ res.end(result.code);
353
+ return;
354
+ }
355
+ } catch {}
356
+ res.setHeader("Content-Type", "application/javascript");
357
+ res.setHeader("Cache-Control", "no-cache");
358
+ res.end(moduleCode);
359
+ });
360
+ devServer.middlewares.use(`${basePath}/preview`, async (req, res, _next) => {
361
+ const url = new URL(req.url || "", `http://localhost`);
362
+ const artPath = url.searchParams.get("art");
363
+ const variantName = url.searchParams.get("variant");
364
+ if (!artPath || !variantName) {
365
+ res.statusCode = 400;
366
+ res.end("Missing art or variant parameter");
367
+ return;
368
+ }
369
+ const art = artFiles.get(artPath);
370
+ if (!art) {
371
+ res.statusCode = 404;
372
+ res.end("Art not found");
373
+ return;
374
+ }
375
+ const variant = art.variants.find((v) => v.name === variantName);
376
+ if (!variant) {
377
+ res.statusCode = 404;
378
+ res.end("Variant not found");
379
+ return;
380
+ }
381
+ const rawHtml = generatePreviewHtml(art, variant, basePath);
382
+ const html = await devServer.transformIndexHtml(`${basePath}/preview?art=${encodeURIComponent(artPath)}&variant=${encodeURIComponent(variantName)}`, rawHtml);
383
+ res.setHeader("Content-Type", "text/html");
384
+ res.end(html);
385
+ });
386
+ devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
387
+ const url = new URL(req.url || "", "http://localhost");
388
+ const artPath = decodeURIComponent(url.pathname.slice(1));
389
+ if (!artPath) {
390
+ next();
391
+ return;
392
+ }
393
+ const art = artFiles.get(artPath);
394
+ if (!art) {
395
+ res.statusCode = 404;
396
+ res.end("Art not found: " + artPath);
397
+ return;
398
+ }
399
+ try {
400
+ const virtualId = `virtual:musea-art:${artPath}`;
401
+ const result = await devServer.transformRequest(virtualId);
402
+ if (result) {
403
+ res.setHeader("Content-Type", "application/javascript");
404
+ res.setHeader("Cache-Control", "no-cache");
405
+ res.end(result.code);
406
+ } else {
407
+ const moduleCode = generateArtModule(art, artPath);
408
+ res.setHeader("Content-Type", "application/javascript");
409
+ res.end(moduleCode);
410
+ }
411
+ } catch (err) {
412
+ console.error("[musea] Failed to transform art module:", err);
413
+ const moduleCode = generateArtModule(art, artPath);
414
+ res.setHeader("Content-Type", "application/javascript");
415
+ res.end(moduleCode);
416
+ }
417
+ });
418
+ devServer.middlewares.use(`${basePath}/api`, async (req, res, next) => {
419
+ if (req.url === "/arts" && req.method === "GET") {
420
+ res.setHeader("Content-Type", "application/json");
421
+ res.end(JSON.stringify(Array.from(artFiles.values())));
422
+ return;
423
+ }
424
+ if (req.url?.startsWith("/arts/") && req.method === "GET") {
425
+ const artPath = decodeURIComponent(req.url.slice(6));
426
+ const art = artFiles.get(artPath);
427
+ if (art) {
428
+ res.setHeader("Content-Type", "application/json");
429
+ res.end(JSON.stringify(art));
430
+ } else {
431
+ res.statusCode = 404;
432
+ res.end(JSON.stringify({ error: "Art not found" }));
433
+ }
434
+ return;
435
+ }
436
+ next();
437
+ });
438
+ devServer.watcher.on("change", async (file) => {
439
+ if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
440
+ await processArtFile(file);
441
+ console.log(`[musea] Reloaded: ${path.relative(config.root, file)}`);
442
+ }
443
+ });
444
+ devServer.watcher.on("add", async (file) => {
445
+ if (file.endsWith(".art.vue") && shouldProcess(file, include, exclude, config.root)) {
446
+ await processArtFile(file);
447
+ console.log(`[musea] Added: ${path.relative(config.root, file)}`);
448
+ }
449
+ });
450
+ devServer.watcher.on("unlink", (file) => {
451
+ if (artFiles.has(file)) {
452
+ artFiles.delete(file);
453
+ console.log(`[musea] Removed: ${path.relative(config.root, file)}`);
454
+ }
455
+ });
456
+ },
457
+ async buildStart() {
458
+ const files = await scanArtFiles(config.root, include, exclude);
459
+ console.log(`[musea] Found ${files.length} art files`);
460
+ for (const file of files) await processArtFile(file);
461
+ if (storybookCompat) await generateStorybookFiles(artFiles, config.root, storybookOutDir);
462
+ },
463
+ resolveId(id) {
464
+ if (id === VIRTUAL_GALLERY) return VIRTUAL_GALLERY;
465
+ if (id === VIRTUAL_MANIFEST) return VIRTUAL_MANIFEST;
466
+ if (id.startsWith("virtual:musea-preview:")) return "\0musea-preview:" + id.slice(22);
467
+ if (id.startsWith("virtual:musea-art:")) {
468
+ const artPath = id.slice(18);
469
+ if (artFiles.has(artPath)) return "\0musea-art:" + artPath;
470
+ }
471
+ if (id.endsWith(".art.vue")) {
472
+ const resolved = path.resolve(config.root, id);
473
+ if (artFiles.has(resolved)) return VIRTUAL_MUSEA_PREFIX + resolved;
474
+ }
475
+ return null;
476
+ },
477
+ load(id) {
478
+ if (id === VIRTUAL_GALLERY) return generateGalleryModule(basePath);
479
+ if (id === VIRTUAL_MANIFEST) return generateManifestModule(artFiles);
480
+ if (id.startsWith("\0musea-preview:")) {
481
+ const rest = id.slice(15);
482
+ const lastColonIndex = rest.lastIndexOf(":");
483
+ if (lastColonIndex !== -1) {
484
+ const artPath = rest.slice(0, lastColonIndex);
485
+ const variantName = rest.slice(lastColonIndex + 1);
486
+ const art = artFiles.get(artPath);
487
+ if (art) {
488
+ const variantComponentName = toPascalCase(variantName);
489
+ return generatePreviewModule(art, variantComponentName, variantName);
490
+ }
491
+ }
492
+ }
493
+ if (id.startsWith("\0musea-art:")) {
494
+ const artPath = id.slice(11);
495
+ const art = artFiles.get(artPath);
496
+ if (art) return generateArtModule(art, artPath);
497
+ }
498
+ if (id.startsWith(VIRTUAL_MUSEA_PREFIX)) {
499
+ const realPath = id.slice(VIRTUAL_MUSEA_PREFIX.length);
500
+ const art = artFiles.get(realPath);
501
+ if (art) return generateArtModule(art, realPath);
502
+ }
503
+ return null;
504
+ },
505
+ async handleHotUpdate(ctx) {
506
+ const { file } = ctx;
507
+ if (file.endsWith(".art.vue") && artFiles.has(file)) {
508
+ await processArtFile(file);
509
+ const virtualId = VIRTUAL_MUSEA_PREFIX + file;
510
+ const modules = server?.moduleGraph.getModulesByFile(virtualId);
511
+ if (modules) return [...modules];
512
+ }
513
+ return void 0;
514
+ }
515
+ };
516
+ async function processArtFile(filePath) {
517
+ try {
518
+ const source = await fs.promises.readFile(filePath, "utf-8");
519
+ const binding = loadNative();
520
+ const parsed = binding.parseArt(source, { filename: filePath });
521
+ const info = {
522
+ path: filePath,
523
+ metadata: {
524
+ title: parsed.metadata.title,
525
+ description: parsed.metadata.description,
526
+ component: parsed.metadata.component,
527
+ category: parsed.metadata.category,
528
+ tags: parsed.metadata.tags,
529
+ status: parsed.metadata.status,
530
+ order: parsed.metadata.order
531
+ },
532
+ variants: parsed.variants.map((v) => ({
533
+ name: v.name,
534
+ template: v.template,
535
+ isDefault: v.is_default,
536
+ skipVrt: v.skip_vrt
537
+ })),
538
+ hasScriptSetup: parsed.has_script_setup,
539
+ hasScript: parsed.has_script,
540
+ styleCount: parsed.style_count
541
+ };
542
+ artFiles.set(filePath, info);
543
+ } catch (e) {
544
+ console.error(`[musea] Failed to process ${filePath}:`, e);
545
+ }
546
+ }
547
+ return [mainPlugin];
548
+ }
549
+ function shouldProcess(file, include, exclude, root) {
550
+ const relative = path.relative(root, file);
551
+ for (const pattern of exclude) if (matchGlob(relative, pattern)) return false;
552
+ for (const pattern of include) if (matchGlob(relative, pattern)) return true;
553
+ return false;
554
+ }
555
+ function matchGlob(filepath, pattern) {
556
+ const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*(?!\*)/g, "[^/]*");
557
+ return new RegExp(`^${regex}$`).test(filepath);
558
+ }
559
+ async function scanArtFiles(root, include, exclude) {
560
+ const files = [];
561
+ async function scan(dir) {
562
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
563
+ for (const entry of entries) {
564
+ const fullPath = path.join(dir, entry.name);
565
+ const relative = path.relative(root, fullPath);
566
+ let excluded = false;
567
+ for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
568
+ excluded = true;
569
+ break;
570
+ }
571
+ if (excluded) continue;
572
+ if (entry.isDirectory()) await scan(fullPath);
573
+ else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
574
+ for (const pattern of include) if (matchGlob(relative, pattern)) {
575
+ files.push(fullPath);
576
+ break;
577
+ }
578
+ }
579
+ }
580
+ }
581
+ await scan(root);
582
+ return files;
583
+ }
584
+ function generateGalleryHtml(basePath) {
585
+ return `<!DOCTYPE html>
586
+ <html lang="en">
587
+ <head>
588
+ <meta charset="UTF-8">
589
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
590
+ <title>Musea - Component Gallery</title>
591
+ <link rel="preconnect" href="https://fonts.googleapis.com">
592
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
593
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
594
+ <style>
595
+ :root {
596
+ --musea-bg-primary: #0d0d0d;
597
+ --musea-bg-secondary: #1a1815;
598
+ --musea-bg-tertiary: #252220;
599
+ --musea-bg-elevated: #2d2a27;
600
+ --musea-accent: #a34828;
601
+ --musea-accent-hover: #c45a32;
602
+ --musea-accent-subtle: rgba(163, 72, 40, 0.15);
603
+ --musea-text: #e6e9f0;
604
+ --musea-text-secondary: #c4c9d4;
605
+ --musea-text-muted: #7b8494;
606
+ --musea-border: #3a3530;
607
+ --musea-border-subtle: #2a2725;
608
+ --musea-success: #4ade80;
609
+ --musea-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
610
+ --musea-radius-sm: 6px;
611
+ --musea-radius-md: 8px;
612
+ --musea-radius-lg: 12px;
613
+ --musea-transition: 0.15s ease;
614
+ }
615
+
616
+ * { box-sizing: border-box; margin: 0; padding: 0; }
617
+
618
+ body {
619
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
620
+ background: var(--musea-bg-primary);
621
+ color: var(--musea-text);
622
+ min-height: 100vh;
623
+ line-height: 1.5;
624
+ -webkit-font-smoothing: antialiased;
625
+ }
626
+
627
+ /* Header */
628
+ .header {
629
+ background: var(--musea-bg-secondary);
630
+ border-bottom: 1px solid var(--musea-border);
631
+ padding: 0 1.5rem;
632
+ height: 56px;
633
+ display: flex;
634
+ align-items: center;
635
+ justify-content: space-between;
636
+ position: sticky;
637
+ top: 0;
638
+ z-index: 100;
639
+ }
640
+
641
+ .header-left {
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 1.5rem;
645
+ }
646
+
647
+ .logo {
648
+ display: flex;
649
+ align-items: center;
650
+ gap: 0.5rem;
651
+ font-size: 1.125rem;
652
+ font-weight: 700;
653
+ color: var(--musea-accent);
654
+ text-decoration: none;
655
+ }
656
+
657
+ .logo-svg {
658
+ width: 32px;
659
+ height: 32px;
660
+ flex-shrink: 0;
661
+ }
662
+
663
+ .logo-icon svg {
664
+ width: 16px;
665
+ height: 16px;
666
+ color: white;
667
+ }
668
+
669
+ .header-subtitle {
670
+ color: var(--musea-text-muted);
671
+ font-size: 0.8125rem;
672
+ font-weight: 500;
673
+ padding-left: 1.5rem;
674
+ border-left: 1px solid var(--musea-border);
675
+ }
676
+
677
+ .search-container {
678
+ position: relative;
679
+ width: 280px;
680
+ }
681
+
682
+ .search-input {
683
+ width: 100%;
684
+ background: var(--musea-bg-tertiary);
685
+ border: 1px solid var(--musea-border);
686
+ border-radius: var(--musea-radius-md);
687
+ padding: 0.5rem 0.75rem 0.5rem 2.25rem;
688
+ color: var(--musea-text);
689
+ font-size: 0.8125rem;
690
+ outline: none;
691
+ transition: border-color var(--musea-transition), background var(--musea-transition);
692
+ }
693
+
694
+ .search-input::placeholder {
695
+ color: var(--musea-text-muted);
696
+ }
697
+
698
+ .search-input:focus {
699
+ border-color: var(--musea-accent);
700
+ background: var(--musea-bg-elevated);
701
+ }
702
+
703
+ .search-icon {
704
+ position: absolute;
705
+ left: 0.75rem;
706
+ top: 50%;
707
+ transform: translateY(-50%);
708
+ color: var(--musea-text-muted);
709
+ pointer-events: none;
710
+ }
711
+
712
+ /* Layout */
713
+ .main {
714
+ display: grid;
715
+ grid-template-columns: 260px 1fr;
716
+ min-height: calc(100vh - 56px);
717
+ }
718
+
719
+ /* Sidebar */
720
+ .sidebar {
721
+ background: var(--musea-bg-secondary);
722
+ border-right: 1px solid var(--musea-border);
723
+ overflow-y: auto;
724
+ overflow-x: hidden;
725
+ }
726
+
727
+ .sidebar::-webkit-scrollbar {
728
+ width: 6px;
729
+ }
730
+
731
+ .sidebar::-webkit-scrollbar-track {
732
+ background: transparent;
733
+ }
734
+
735
+ .sidebar::-webkit-scrollbar-thumb {
736
+ background: var(--musea-border);
737
+ border-radius: 3px;
738
+ }
739
+
740
+ .sidebar-section {
741
+ padding: 0.75rem;
742
+ }
743
+
744
+ .category-header {
745
+ display: flex;
746
+ align-items: center;
747
+ gap: 0.5rem;
748
+ padding: 0.625rem 0.75rem;
749
+ font-size: 0.6875rem;
750
+ font-weight: 600;
751
+ text-transform: uppercase;
752
+ letter-spacing: 0.08em;
753
+ color: var(--musea-text-muted);
754
+ cursor: pointer;
755
+ user-select: none;
756
+ border-radius: var(--musea-radius-sm);
757
+ transition: background var(--musea-transition);
758
+ }
759
+
760
+ .category-header:hover {
761
+ background: var(--musea-bg-tertiary);
762
+ }
763
+
764
+ .category-icon {
765
+ width: 16px;
766
+ height: 16px;
767
+ transition: transform var(--musea-transition);
768
+ }
769
+
770
+ .category-header.collapsed .category-icon {
771
+ transform: rotate(-90deg);
772
+ }
773
+
774
+ .category-count {
775
+ margin-left: auto;
776
+ background: var(--musea-bg-tertiary);
777
+ padding: 0.125rem 0.375rem;
778
+ border-radius: 4px;
779
+ font-size: 0.625rem;
780
+ }
781
+
782
+ .art-list {
783
+ list-style: none;
784
+ margin-top: 0.25rem;
785
+ }
786
+
787
+ .art-item {
788
+ display: flex;
789
+ align-items: center;
790
+ gap: 0.625rem;
791
+ padding: 0.5rem 0.75rem 0.5rem 1.75rem;
792
+ border-radius: var(--musea-radius-sm);
793
+ cursor: pointer;
794
+ font-size: 0.8125rem;
795
+ color: var(--musea-text-secondary);
796
+ transition: all var(--musea-transition);
797
+ position: relative;
798
+ }
799
+
800
+ .art-item::before {
801
+ content: '';
802
+ position: absolute;
803
+ left: 0.75rem;
804
+ top: 50%;
805
+ transform: translateY(-50%);
806
+ width: 6px;
807
+ height: 6px;
808
+ border-radius: 50%;
809
+ background: var(--musea-border);
810
+ transition: background var(--musea-transition);
811
+ }
812
+
813
+ .art-item:hover {
814
+ background: var(--musea-bg-tertiary);
815
+ color: var(--musea-text);
816
+ }
817
+
818
+ .art-item:hover::before {
819
+ background: var(--musea-text-muted);
820
+ }
821
+
822
+ .art-item.active {
823
+ background: var(--musea-accent-subtle);
824
+ color: var(--musea-accent-hover);
825
+ }
826
+
827
+ .art-item.active::before {
828
+ background: var(--musea-accent);
829
+ }
830
+
831
+ .art-variant-count {
832
+ margin-left: auto;
833
+ font-size: 0.6875rem;
834
+ color: var(--musea-text-muted);
835
+ opacity: 0;
836
+ transition: opacity var(--musea-transition);
837
+ }
838
+
839
+ .art-item:hover .art-variant-count {
840
+ opacity: 1;
841
+ }
842
+
843
+ /* Content */
844
+ .content {
845
+ background: var(--musea-bg-primary);
846
+ overflow-y: auto;
847
+ }
848
+
849
+ .content-inner {
850
+ max-width: 1400px;
851
+ margin: 0 auto;
852
+ padding: 2rem;
853
+ }
854
+
855
+ .content-header {
856
+ margin-bottom: 2rem;
857
+ }
858
+
859
+ .content-title {
860
+ font-size: 1.5rem;
861
+ font-weight: 700;
862
+ margin-bottom: 0.5rem;
863
+ }
864
+
865
+ .content-description {
866
+ color: var(--musea-text-muted);
867
+ font-size: 0.9375rem;
868
+ max-width: 600px;
869
+ }
870
+
871
+ .content-meta {
872
+ display: flex;
873
+ align-items: center;
874
+ gap: 1rem;
875
+ margin-top: 1rem;
876
+ }
877
+
878
+ .meta-tag {
879
+ display: inline-flex;
880
+ align-items: center;
881
+ gap: 0.375rem;
882
+ padding: 0.25rem 0.625rem;
883
+ background: var(--musea-bg-secondary);
884
+ border: 1px solid var(--musea-border);
885
+ border-radius: var(--musea-radius-sm);
886
+ font-size: 0.75rem;
887
+ color: var(--musea-text-muted);
888
+ }
889
+
890
+ .meta-tag svg {
891
+ width: 12px;
892
+ height: 12px;
893
+ }
894
+
895
+ /* Gallery Grid */
896
+ .gallery {
897
+ display: grid;
898
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
899
+ gap: 1.25rem;
900
+ }
901
+
902
+ /* Variant Card */
903
+ .variant-card {
904
+ background: var(--musea-bg-secondary);
905
+ border: 1px solid var(--musea-border);
906
+ border-radius: var(--musea-radius-lg);
907
+ overflow: hidden;
908
+ transition: all var(--musea-transition);
909
+ }
910
+
911
+ .variant-card:hover {
912
+ border-color: var(--musea-text-muted);
913
+ box-shadow: var(--musea-shadow);
914
+ transform: translateY(-2px);
915
+ }
916
+
917
+ .variant-preview {
918
+ aspect-ratio: 16 / 10;
919
+ background: var(--musea-bg-tertiary);
920
+ display: flex;
921
+ align-items: center;
922
+ justify-content: center;
923
+ position: relative;
924
+ overflow: hidden;
925
+ }
926
+
927
+ .variant-preview iframe {
928
+ width: 100%;
929
+ height: 100%;
930
+ border: none;
931
+ background: white;
932
+ }
933
+
934
+ .variant-preview-placeholder {
935
+ color: var(--musea-text-muted);
936
+ font-size: 0.8125rem;
937
+ text-align: center;
938
+ padding: 1rem;
939
+ }
940
+
941
+ .variant-preview-code {
942
+ font-family: 'SF Mono', 'Fira Code', monospace;
943
+ font-size: 0.75rem;
944
+ color: var(--musea-text-muted);
945
+ background: var(--musea-bg-primary);
946
+ padding: 1rem;
947
+ overflow: auto;
948
+ max-height: 100%;
949
+ width: 100%;
950
+ }
951
+
952
+ .variant-info {
953
+ padding: 1rem;
954
+ border-top: 1px solid var(--musea-border);
955
+ display: flex;
956
+ align-items: center;
957
+ justify-content: space-between;
958
+ }
959
+
960
+ .variant-name {
961
+ font-weight: 600;
962
+ font-size: 0.875rem;
963
+ }
964
+
965
+ .variant-badge {
966
+ font-size: 0.625rem;
967
+ font-weight: 600;
968
+ text-transform: uppercase;
969
+ letter-spacing: 0.04em;
970
+ padding: 0.1875rem 0.5rem;
971
+ border-radius: 4px;
972
+ background: var(--musea-accent-subtle);
973
+ color: var(--musea-accent);
974
+ }
975
+
976
+ .variant-actions {
977
+ display: flex;
978
+ gap: 0.5rem;
979
+ }
980
+
981
+ .variant-action-btn {
982
+ width: 28px;
983
+ height: 28px;
984
+ border: none;
985
+ background: var(--musea-bg-tertiary);
986
+ border-radius: var(--musea-radius-sm);
987
+ color: var(--musea-text-muted);
988
+ cursor: pointer;
989
+ display: flex;
990
+ align-items: center;
991
+ justify-content: center;
992
+ transition: all var(--musea-transition);
993
+ }
994
+
995
+ .variant-action-btn:hover {
996
+ background: var(--musea-bg-elevated);
997
+ color: var(--musea-text);
998
+ }
999
+
1000
+ .variant-action-btn svg {
1001
+ width: 14px;
1002
+ height: 14px;
1003
+ }
1004
+
1005
+ /* Empty State */
1006
+ .empty-state {
1007
+ display: flex;
1008
+ flex-direction: column;
1009
+ align-items: center;
1010
+ justify-content: center;
1011
+ min-height: 400px;
1012
+ text-align: center;
1013
+ padding: 2rem;
1014
+ }
1015
+
1016
+ .empty-state-icon {
1017
+ width: 80px;
1018
+ height: 80px;
1019
+ background: var(--musea-bg-secondary);
1020
+ border-radius: var(--musea-radius-lg);
1021
+ display: flex;
1022
+ align-items: center;
1023
+ justify-content: center;
1024
+ margin-bottom: 1.5rem;
1025
+ }
1026
+
1027
+ .empty-state-icon svg {
1028
+ width: 40px;
1029
+ height: 40px;
1030
+ color: var(--musea-text-muted);
1031
+ }
1032
+
1033
+ .empty-state-title {
1034
+ font-size: 1.125rem;
1035
+ font-weight: 600;
1036
+ margin-bottom: 0.5rem;
1037
+ }
1038
+
1039
+ .empty-state-text {
1040
+ color: var(--musea-text-muted);
1041
+ font-size: 0.875rem;
1042
+ max-width: 300px;
1043
+ }
1044
+
1045
+ /* Loading */
1046
+ .loading {
1047
+ display: flex;
1048
+ align-items: center;
1049
+ justify-content: center;
1050
+ min-height: 200px;
1051
+ color: var(--musea-text-muted);
1052
+ gap: 0.75rem;
1053
+ }
1054
+
1055
+ .loading-spinner {
1056
+ width: 20px;
1057
+ height: 20px;
1058
+ border: 2px solid var(--musea-border);
1059
+ border-top-color: var(--musea-accent);
1060
+ border-radius: 50%;
1061
+ animation: spin 0.8s linear infinite;
1062
+ }
1063
+
1064
+ @keyframes spin {
1065
+ to { transform: rotate(360deg); }
1066
+ }
1067
+
1068
+ /* Responsive */
1069
+ @media (max-width: 768px) {
1070
+ .main {
1071
+ grid-template-columns: 1fr;
1072
+ }
1073
+ .sidebar {
1074
+ display: none;
1075
+ }
1076
+ .header-subtitle {
1077
+ display: none;
1078
+ }
1079
+ }
1080
+ </style>
1081
+ </head>
1082
+ <body>
1083
+ <header class="header">
1084
+ <div class="header-left">
1085
+ <a href="${basePath}" class="logo">
1086
+ <svg class="logo-svg" width="32" height="32" viewBox="0 0 200 200" fill="none">
1087
+ <defs>
1088
+ <linearGradient id="metal-grad" x1="0%" y1="0%" x2="100%" y2="20%">
1089
+ <stop offset="0%" stop-color="#f0f2f5"/>
1090
+ <stop offset="50%" stop-color="#9ca3b0"/>
1091
+ <stop offset="100%" stop-color="#e07048"/>
1092
+ </linearGradient>
1093
+ <linearGradient id="metal-grad-dark" x1="0%" y1="0%" x2="100%" y2="30%">
1094
+ <stop offset="0%" stop-color="#d0d4dc"/>
1095
+ <stop offset="60%" stop-color="#6b7280"/>
1096
+ <stop offset="100%" stop-color="#c45530"/>
1097
+ </linearGradient>
1098
+ </defs>
1099
+ <g transform="translate(40, 40)">
1100
+ <g transform="skewX(-12)">
1101
+ <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"/>
1102
+ <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"/>
1103
+ </g>
1104
+ </g>
1105
+ <g transform="translate(110, 120)">
1106
+ <line x1="5" y1="10" x2="5" y2="50" stroke="#e07048" stroke-width="3" stroke-linecap="round"/>
1107
+ <line x1="60" y1="10" x2="60" y2="50" stroke="#e07048" stroke-width="3" stroke-linecap="round"/>
1108
+ <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"/>
1109
+ <rect x="15" y="18" width="14" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.7"/>
1110
+ <rect x="36" y="18" width="14" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.7"/>
1111
+ <rect x="23" y="35" width="18" height="12" rx="1" fill="none" stroke="#e07048" stroke-width="1.5" opacity="0.6"/>
1112
+ </g>
1113
+ </svg>
1114
+ Musea
1115
+ </a>
1116
+ <span class="header-subtitle">Component Gallery</span>
1117
+ </div>
1118
+ <div class="search-container">
1119
+ <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1120
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
1121
+ </svg>
1122
+ <input type="text" class="search-input" placeholder="Search components..." id="search">
1123
+ </div>
1124
+ </header>
1125
+
1126
+ <main class="main">
1127
+ <aside class="sidebar" id="sidebar">
1128
+ <div class="loading">
1129
+ <div class="loading-spinner"></div>
1130
+ Loading...
1131
+ </div>
1132
+ </aside>
1133
+ <section class="content" id="content">
1134
+ <div class="empty-state">
1135
+ <div class="empty-state-icon">
1136
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1137
+ <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"/>
1138
+ <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"/>
1139
+ <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"/>
1140
+ </svg>
1141
+ </div>
1142
+ <div class="empty-state-title">Select a component</div>
1143
+ <div class="empty-state-text">Choose a component from the sidebar to view its variants and documentation</div>
1144
+ </div>
1145
+ </section>
1146
+ </main>
1147
+
1148
+ <script type="module">
1149
+ const basePath = '${basePath}';
1150
+ let arts = [];
1151
+ let selectedArt = null;
1152
+ let searchQuery = '';
1153
+
1154
+ async function loadArts() {
1155
+ try {
1156
+ const res = await fetch(basePath + '/api/arts');
1157
+ arts = await res.json();
1158
+ renderSidebar();
1159
+ } catch (e) {
1160
+ console.error('Failed to load arts:', e);
1161
+ document.getElementById('sidebar').innerHTML = '<div class="loading">Failed to load</div>';
1162
+ }
1163
+ }
1164
+
1165
+ function renderSidebar() {
1166
+ const sidebar = document.getElementById('sidebar');
1167
+ const categories = {};
1168
+
1169
+ const filtered = searchQuery
1170
+ ? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))
1171
+ : arts;
1172
+
1173
+ for (const art of filtered) {
1174
+ const cat = art.metadata.category || 'Components';
1175
+ if (!categories[cat]) categories[cat] = [];
1176
+ categories[cat].push(art);
1177
+ }
1178
+
1179
+ if (Object.keys(categories).length === 0) {
1180
+ sidebar.innerHTML = '<div class="sidebar-section"><div class="loading">No components found</div></div>';
1181
+ return;
1182
+ }
1183
+
1184
+ let html = '';
1185
+ for (const [category, items] of Object.entries(categories)) {
1186
+ html += '<div class="sidebar-section">';
1187
+ html += '<div class="category-header" data-category="' + category + '">';
1188
+ 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>';
1189
+ html += '<span>' + category + '</span>';
1190
+ html += '<span class="category-count">' + items.length + '</span>';
1191
+ html += '</div>';
1192
+ html += '<ul class="art-list" data-category="' + category + '">';
1193
+ for (const art of items) {
1194
+ const active = selectedArt?.path === art.path ? 'active' : '';
1195
+ const variantCount = art.variants?.length || 0;
1196
+ html += '<li class="art-item ' + active + '" data-path="' + art.path + '">';
1197
+ html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
1198
+ html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
1199
+ html += '</li>';
1200
+ }
1201
+ html += '</ul>';
1202
+ html += '</div>';
1203
+ }
1204
+
1205
+ sidebar.innerHTML = html;
1206
+
1207
+ sidebar.querySelectorAll('.art-item').forEach(item => {
1208
+ item.addEventListener('click', () => {
1209
+ const artPath = item.dataset.path;
1210
+ selectedArt = arts.find(a => a.path === artPath);
1211
+ renderSidebar();
1212
+ renderContent();
1213
+ });
1214
+ });
1215
+
1216
+ sidebar.querySelectorAll('.category-header').forEach(header => {
1217
+ header.addEventListener('click', () => {
1218
+ header.classList.toggle('collapsed');
1219
+ const list = sidebar.querySelector('.art-list[data-category="' + header.dataset.category + '"]');
1220
+ if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
1221
+ });
1222
+ });
1223
+ }
1224
+
1225
+ function renderContent() {
1226
+ const content = document.getElementById('content');
1227
+ if (!selectedArt) {
1228
+ content.innerHTML = \`
1229
+ <div class="empty-state">
1230
+ <div class="empty-state-icon">
1231
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1232
+ <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"/>
1233
+ <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"/>
1234
+ <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"/>
1235
+ </svg>
1236
+ </div>
1237
+ <div class="empty-state-title">Select a component</div>
1238
+ <div class="empty-state-text">Choose a component from the sidebar to view its variants</div>
1239
+ </div>
1240
+ \`;
1241
+ return;
1242
+ }
1243
+
1244
+ const meta = selectedArt.metadata;
1245
+ const tags = meta.tags || [];
1246
+ const variantCount = selectedArt.variants?.length || 0;
1247
+
1248
+ let html = '<div class="content-inner">';
1249
+ html += '<div class="content-header">';
1250
+ html += '<h1 class="content-title">' + escapeHtml(meta.title) + '</h1>';
1251
+ if (meta.description) {
1252
+ html += '<p class="content-description">' + escapeHtml(meta.description) + '</p>';
1253
+ }
1254
+ html += '<div class="content-meta">';
1255
+ 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>';
1256
+ if (meta.category) {
1257
+ 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>';
1258
+ }
1259
+ for (const tag of tags) {
1260
+ html += '<span class="meta-tag">#' + escapeHtml(tag) + '</span>';
1261
+ }
1262
+ html += '</div>';
1263
+ html += '</div>';
1264
+
1265
+ html += '<div class="gallery">';
1266
+ for (const variant of selectedArt.variants) {
1267
+ const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);
1268
+
1269
+ html += '<div class="variant-card">';
1270
+ html += '<div class="variant-preview">';
1271
+ html += '<iframe src="' + previewUrl + '" loading="lazy" title="' + escapeHtml(variant.name) + '"></iframe>';
1272
+ html += '</div>';
1273
+ html += '<div class="variant-info">';
1274
+ html += '<div>';
1275
+ html += '<span class="variant-name">' + escapeHtml(variant.name) + '</span>';
1276
+ if (variant.isDefault) html += ' <span class="variant-badge">Default</span>';
1277
+ html += '</div>';
1278
+ html += '<div class="variant-actions">';
1279
+ 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>';
1280
+ html += '</div>';
1281
+ html += '</div>';
1282
+ html += '</div>';
1283
+ }
1284
+ html += '</div>';
1285
+ html += '</div>';
1286
+
1287
+ content.innerHTML = html;
1288
+ }
1289
+
1290
+ function escapeHtml(str) {
1291
+ if (!str) return '';
1292
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1293
+ }
1294
+
1295
+ // Search
1296
+ document.getElementById('search').addEventListener('input', (e) => {
1297
+ searchQuery = e.target.value;
1298
+ renderSidebar();
1299
+ });
1300
+
1301
+ // Keyboard shortcut for search
1302
+ document.addEventListener('keydown', (e) => {
1303
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1304
+ e.preventDefault();
1305
+ document.getElementById('search').focus();
1306
+ }
1307
+ });
1308
+
1309
+ loadArts();
1310
+ </script>
1311
+ </body>
1312
+ </html>`;
1313
+ }
1314
+ function generateGalleryModule(basePath) {
1315
+ return `
1316
+ export const basePath = '${basePath}';
1317
+ export async function loadArts() {
1318
+ const res = await fetch(basePath + '/api/arts');
1319
+ return res.json();
1320
+ }
1321
+ `;
1322
+ }
1323
+ function generatePreviewModule(art, variantComponentName, variantName) {
1324
+ const artModuleId = `virtual:musea-art:${art.path}`;
1325
+ const escapedVariantName = escapeTemplate(variantName);
1326
+ return `
1327
+ import { createApp } from 'vue';
1328
+ import * as artModule from '${artModuleId}';
1329
+
1330
+ const container = document.getElementById('app');
1331
+
1332
+ async function mount() {
1333
+ try {
1334
+ // Get the specific variant component
1335
+ const VariantComponent = artModule['${variantComponentName}'];
1336
+
1337
+ if (!VariantComponent) {
1338
+ throw new Error('Variant component "${variantComponentName}" not found in art module');
1339
+ }
1340
+
1341
+ // Create and mount the app
1342
+ const app = createApp(VariantComponent);
1343
+ container.innerHTML = '';
1344
+ container.className = 'musea-variant';
1345
+ app.mount(container);
1346
+
1347
+ console.log('[musea-preview] Mounted variant: ${escapedVariantName}');
1348
+ } catch (error) {
1349
+ console.error('[musea-preview] Failed to mount:', error);
1350
+ container.innerHTML = \`
1351
+ <div class="musea-error">
1352
+ <div class="musea-error-title">Failed to render component</div>
1353
+ <div>\${error.message}</div>
1354
+ <pre>\${error.stack || ''}</pre>
1355
+ </div>
1356
+ \`;
1357
+ }
1358
+ }
1359
+
1360
+ mount();
1361
+ `;
1362
+ }
1363
+ function generateManifestModule(artFiles) {
1364
+ const arts = Array.from(artFiles.values());
1365
+ return `export const arts = ${JSON.stringify(arts, null, 2)};`;
1366
+ }
1367
+ function generateArtModule(art, filePath) {
1368
+ const componentPath = art.metadata.component;
1369
+ let resolvedComponentPath = componentPath;
1370
+ if (componentPath && !path.isAbsolute(componentPath)) {
1371
+ const artDir = path.dirname(filePath);
1372
+ resolvedComponentPath = path.resolve(artDir, componentPath);
1373
+ }
1374
+ const componentName = componentPath ? path.basename(componentPath, ".vue") : null;
1375
+ let code = `
1376
+ // Auto-generated module for: ${path.basename(filePath)}
1377
+ import { defineComponent, h } from 'vue';
1378
+ `;
1379
+ if (resolvedComponentPath && componentName) code += `import ${componentName} from '${resolvedComponentPath}';\n`;
1380
+ code += `
1381
+ export const metadata = ${JSON.stringify(art.metadata)};
1382
+ export const variants = ${JSON.stringify(art.variants)};
1383
+ `;
1384
+ for (const variant of art.variants) {
1385
+ const variantComponentName = toPascalCase(variant.name);
1386
+ const escapedTemplate = variant.template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
1387
+ const fullTemplate = `<div class="musea-variant" data-variant="${variant.name}">${escapedTemplate}</div>`;
1388
+ if (componentName) code += `
1389
+ export const ${variantComponentName} = {
1390
+ name: '${variantComponentName}',
1391
+ components: { ${componentName} },
1392
+ template: \`${fullTemplate}\`,
1393
+ };
1394
+ `;
1395
+ else code += `
1396
+ export const ${variantComponentName} = {
1397
+ name: '${variantComponentName}',
1398
+ template: \`${fullTemplate}\`,
1399
+ };
1400
+ `;
1401
+ }
1402
+ const defaultVariant = art.variants.find((v) => v.isDefault) || art.variants[0];
1403
+ if (defaultVariant) code += `
1404
+ export default ${toPascalCase(defaultVariant.name)};
1405
+ `;
1406
+ return code;
1407
+ }
1408
+ async function generateStorybookFiles(artFiles, root, outDir) {
1409
+ const binding = loadNative();
1410
+ const outputDir = path.resolve(root, outDir);
1411
+ await fs.promises.mkdir(outputDir, { recursive: true });
1412
+ for (const [filePath, _art] of artFiles) try {
1413
+ const source = await fs.promises.readFile(filePath, "utf-8");
1414
+ const csf = binding.artToCsf(source, { filename: filePath });
1415
+ const outputPath = path.join(outputDir, csf.filename);
1416
+ await fs.promises.writeFile(outputPath, csf.code, "utf-8");
1417
+ console.log(`[musea] Generated: ${path.relative(root, outputPath)}`);
1418
+ } catch (e) {
1419
+ console.error(`[musea] Failed to generate CSF for ${filePath}:`, e);
1420
+ }
1421
+ }
1422
+ function toPascalCase(str) {
1423
+ return str.split(/[\s\-_]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
1424
+ }
1425
+ function escapeTemplate(str) {
1426
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
1427
+ }
1428
+ function generatePreviewHtml(art, variant, basePath) {
1429
+ const previewModuleUrl = `${basePath}/preview-module?art=${encodeURIComponent(art.path)}&variant=${encodeURIComponent(variant.name)}`;
1430
+ return `<!DOCTYPE html>
1431
+ <html lang="en">
1432
+ <head>
1433
+ <meta charset="UTF-8">
1434
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1435
+ <title>${escapeHtml(art.metadata.title)} - ${escapeHtml(variant.name)}</title>
1436
+ <style>
1437
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1438
+ html, body {
1439
+ width: 100%;
1440
+ height: 100%;
1441
+ }
1442
+ body {
1443
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1444
+ background: #ffffff;
1445
+ }
1446
+ .musea-variant {
1447
+ padding: 1.5rem;
1448
+ display: flex;
1449
+ align-items: center;
1450
+ justify-content: center;
1451
+ min-height: 100vh;
1452
+ }
1453
+ .musea-error {
1454
+ color: #dc2626;
1455
+ background: #fef2f2;
1456
+ border: 1px solid #fecaca;
1457
+ border-radius: 8px;
1458
+ padding: 1rem;
1459
+ font-size: 0.875rem;
1460
+ max-width: 400px;
1461
+ }
1462
+ .musea-error-title {
1463
+ font-weight: 600;
1464
+ margin-bottom: 0.5rem;
1465
+ }
1466
+ .musea-error pre {
1467
+ font-family: monospace;
1468
+ font-size: 0.75rem;
1469
+ white-space: pre-wrap;
1470
+ word-break: break-all;
1471
+ margin-top: 0.5rem;
1472
+ padding: 0.5rem;
1473
+ background: #fff;
1474
+ border-radius: 4px;
1475
+ }
1476
+ .musea-loading {
1477
+ display: flex;
1478
+ align-items: center;
1479
+ gap: 0.75rem;
1480
+ color: #6b7280;
1481
+ font-size: 0.875rem;
1482
+ }
1483
+ .musea-spinner {
1484
+ width: 20px;
1485
+ height: 20px;
1486
+ border: 2px solid #e5e7eb;
1487
+ border-top-color: #3b82f6;
1488
+ border-radius: 50%;
1489
+ animation: spin 0.8s linear infinite;
1490
+ }
1491
+ @keyframes spin { to { transform: rotate(360deg); } }
1492
+ </style>
1493
+ </head>
1494
+ <body>
1495
+ <div id="app" class="musea-variant" data-art="${escapeHtml(art.path)}" data-variant="${escapeHtml(variant.name)}">
1496
+ <div class="musea-loading">
1497
+ <div class="musea-spinner"></div>
1498
+ Loading component...
1499
+ </div>
1500
+ </div>
1501
+ <script type="module" src="${previewModuleUrl}"></script>
1502
+ </body>
1503
+ </html>`;
1504
+ }
1505
+ function escapeHtml(str) {
1506
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
1507
+ }
1508
+ var src_default = musea;
1509
+
1510
+ //#endregion
1511
+ export { MuseaVrtRunner, src_default as default, generateTokensHtml, generateTokensMarkdown, generateVrtJsonReport, generateVrtReport, musea, parseTokens, processStyleDictionary };
1512
+ //# sourceMappingURL=index.js.map