@vizejs/vite-plugin-musea 0.58.0 → 0.60.0

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