@vizejs/vite-plugin-musea 0.101.0 → 0.104.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.
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
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";
1
+ import { a as collectRequestBody, c as resolveInside, d as serializeScriptValue, f as validateDevApiRequest, i as HttpError, l as resolveInsideAny, n as generateGalleryModule, o as createDevSessionToken, s as decodeUrlComponent, u as resolveUrlPathInside } from "./gallery-zu8hc8Lc.mjs";
2
+ import { i as MuseaVrtRunner, n as generateVrtJsonReport, r as generateVrtReport } from "./vrt-Cv1PK1EF.mjs";
3
+ import { t as MuseaA11yRunner } from "./a11y-62l8G1tr.mjs";
4
+ import { n as writeArtFile, t as generateArtFile } from "./autogen-CywxMrJH.mjs";
4
5
  import { createRequire } from "node:module";
5
6
  import { transformWithEsbuild } from "vite";
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
8
9
  import { vizeConfigStore } from "@vizejs/vite-plugin";
9
- import { randomBytes, timingSafeEqual } from "node:crypto";
10
10
  import { fileURLToPath } from "node:url";
11
11
  //#region src/native-loader.ts
12
12
  /**
@@ -91,147 +91,6 @@ function analyzeSfcFallback(source, _options) {
91
91
  }
92
92
  }
93
93
  //#endregion
94
- //#region src/security.ts
95
- const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
96
- var HttpError = class extends Error {
97
- status;
98
- constructor(message, status) {
99
- super(message);
100
- this.name = "HttpError";
101
- this.status = status;
102
- }
103
- };
104
- function createDevSessionToken() {
105
- return randomBytes(32).toString("base64url");
106
- }
107
- function realpathNearest(targetPath) {
108
- let current = path.resolve(targetPath);
109
- const missingParts = [];
110
- while (true) try {
111
- const real = fs.realpathSync.native(current);
112
- return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
113
- } catch {
114
- const parent = path.dirname(current);
115
- if (parent === current) return path.resolve(targetPath);
116
- missingParts.push(path.basename(current));
117
- current = parent;
118
- }
119
- }
120
- function isResolvedPathInside(parentDir, candidatePath) {
121
- const parent = path.resolve(parentDir);
122
- const candidate = path.resolve(candidatePath);
123
- const relative = path.relative(parent, candidate);
124
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
125
- }
126
- function isPathInsideAny(parentDirs, candidatePath) {
127
- const candidate = realpathNearest(candidatePath);
128
- return parentDirs.some((parentDir) => isResolvedPathInside(realpathNearest(parentDir), candidate));
129
- }
130
- function resolveInside(parentDir, candidatePath, label = "path") {
131
- return resolveInsideAny([parentDir], candidatePath, label);
132
- }
133
- function resolveInsideAny(parentDirs, candidatePath, label = "path") {
134
- if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
135
- if (parentDirs.length === 0) throw new HttpError(`No allowed directories configured for ${label}`, 500);
136
- const parent = path.resolve(parentDirs[0] ?? ".");
137
- const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
138
- if (!isPathInsideAny(parentDirs, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
139
- return resolved;
140
- }
141
- function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
142
- const rawPath = requestUrl.split(/[?#]/, 1)[0] || "/";
143
- let pathname;
144
- try {
145
- pathname = decodeURIComponent(rawPath);
146
- } catch {
147
- throw new HttpError(`${label} is not valid URL encoding`, 400);
148
- }
149
- pathname = pathname.replaceAll("\\", "/");
150
- if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
151
- return resolveInside(parentDir, `.${pathname}`, label);
152
- }
153
- function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
154
- return new Promise((resolve, reject) => {
155
- let body = "";
156
- let size = 0;
157
- let completed = false;
158
- req.on("data", (chunk) => {
159
- if (completed) return;
160
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
161
- size += buffer.byteLength;
162
- if (size > limit) {
163
- completed = true;
164
- reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
165
- return;
166
- }
167
- body += buffer.toString("utf-8");
168
- });
169
- req.on("end", () => {
170
- if (!completed) {
171
- completed = true;
172
- resolve(body);
173
- }
174
- });
175
- req.on("error", (error) => {
176
- if (!completed) {
177
- completed = true;
178
- reject(error);
179
- }
180
- });
181
- });
182
- }
183
- function validateDevApiRequest(req, sessionToken) {
184
- const originError = validateOrigin(req);
185
- if (originError) return originError;
186
- if (!isUnsafeMethod(req.method)) return null;
187
- if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
188
- if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
189
- return null;
190
- }
191
- function serializeScriptValue(value) {
192
- return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
193
- switch (char) {
194
- case "<": return "\\u003C";
195
- case ">": return "\\u003E";
196
- case "&": return "\\u0026";
197
- case "\u2028": return "\\u2028";
198
- case "\u2029": return "\\u2029";
199
- default: return char;
200
- }
201
- });
202
- }
203
- function isUnsafeMethod(method) {
204
- return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
205
- }
206
- function isJsonRequest(req) {
207
- return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
208
- }
209
- function validateOrigin(req) {
210
- if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
211
- const origin = getHeader(req, "origin");
212
- if (!origin) return null;
213
- const host = getHeader(req, "host");
214
- if (!host) return new HttpError("Missing Host header", 400);
215
- try {
216
- if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
217
- } catch {
218
- return new HttpError("Invalid Origin header", 400);
219
- }
220
- return null;
221
- }
222
- function hasValidSessionToken(req, expectedToken) {
223
- const actualToken = getHeader(req, "x-musea-session");
224
- if (!actualToken) return false;
225
- const actual = Buffer.from(actualToken);
226
- const expected = Buffer.from(expectedToken);
227
- return actual.length === expected.length && timingSafeEqual(actual, expected);
228
- }
229
- function getHeader(req, name) {
230
- const value = req.headers[name];
231
- if (Array.isArray(value)) return value[0];
232
- return value;
233
- }
234
- //#endregion
235
94
  //#region src/component-source.ts
236
95
  function allowedSourceRoots(root, scanRoots = []) {
237
96
  return [...new Set([root, ...scanRoots].map((sourceRoot) => path.resolve(sourceRoot)))];
@@ -620,324 +479,6 @@ export default ${toPascalCase(defaultVariant.name)};
620
479
  return code;
621
480
  }
622
481
  //#endregion
623
- //#region src/gallery/styles-base.css?inline
624
- 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";
625
- //#endregion
626
- //#region src/gallery/styles-layout.css?inline
627
- 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";
628
- //#endregion
629
- //#region src/gallery/styles-components.css?inline
630
- 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";
631
- //#endregion
632
- //#region src/gallery/styles.ts
633
- /**
634
- * CSS theme variables and style definitions for the Musea gallery.
635
- *
636
- * CSS is split into separate .css files and imported as text
637
- * via tsdown's `?inline` support.
638
- */
639
- /**
640
- * Generate the full gallery CSS styles string.
641
- */
642
- function generateGalleryStyles() {
643
- return `${styles_base_default}\n${styles_layout_default}\n${styles_components_default}`;
644
- }
645
- //#endregion
646
- //#region src/gallery/template.ts
647
- /**
648
- * HTML structure and inline JS generation for the Musea gallery.
649
- *
650
- * Extracted from gallery.ts to keep file sizes manageable.
651
- */
652
- function escapeHtmlAttribute(value) {
653
- return value.replace(/[&<>"']/g, (char) => {
654
- switch (char) {
655
- case "&": return "&amp;";
656
- case "<": return "&lt;";
657
- case ">": return "&gt;";
658
- case "\"": return "&quot;";
659
- case "'": return "&#39;";
660
- default: return char;
661
- }
662
- });
663
- }
664
- /**
665
- * Generate the gallery HTML body (header, sidebar, content, and inline script).
666
- */
667
- function generateGalleryBody(basePath) {
668
- return `
669
- <header class="header">
670
- <div class="header-left">
671
- <a href="${escapeHtmlAttribute(basePath)}" class="logo">
672
- <svg class="logo-svg" width="32" height="32" viewBox="0 0 200 200" fill="none">
673
- <g transform="translate(30, 25) scale(1.2)">
674
- <g transform="translate(15, 10) skewX(-15)">
675
- <path d="M 65 0 L 40 60 L 70 20 L 65 0 Z" fill="currentColor"/>
676
- <path d="M 20 0 L 40 60 L 53 13 L 20 0 Z" fill="currentColor"/>
677
- </g>
678
- </g>
679
- <g transform="translate(110, 120)">
680
- <line x1="5" y1="10" x2="5" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
681
- <line x1="60" y1="10" x2="60" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
682
- <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"/>
683
- <rect x="15" y="18" width="14" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.7"/>
684
- <rect x="36" y="18" width="14" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.7"/>
685
- <rect x="23" y="35" width="18" height="12" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.6"/>
686
- </g>
687
- </svg>
688
- Musea
689
- </a>
690
- <span class="header-subtitle">Component Gallery</span>
691
- </div>
692
- <div class="search-container">
693
- <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
694
- <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
695
- </svg>
696
- <input type="text" class="search-input" placeholder="Search components..." id="search">
697
- </div>
698
- </header>
699
-
700
- <main class="main">
701
- <aside class="sidebar" id="sidebar">
702
- <div class="loading">
703
- <div class="loading-spinner"></div>
704
- Loading...
705
- </div>
706
- </aside>
707
- <section class="content" id="content">
708
- <div class="empty-state">
709
- <div class="empty-state-icon">
710
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
711
- <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"/>
712
- <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"/>
713
- <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"/>
714
- </svg>
715
- </div>
716
- <div class="empty-state-title">Select a component</div>
717
- <div class="empty-state-text">Choose a component from the sidebar to view its variants and documentation</div>
718
- </div>
719
- </section>
720
- </main>`;
721
- }
722
- /**
723
- * Generate the gallery inline script (SPA logic).
724
- */
725
- function generateGalleryScript(basePath) {
726
- return `
727
- const basePath = ${serializeScriptValue(basePath)};
728
- let arts = [];
729
- let selectedArt = null;
730
- let searchQuery = '';
731
-
732
- async function loadArts() {
733
- try {
734
- const res = await fetch(basePath + '/api/arts');
735
- arts = await res.json();
736
- renderSidebar();
737
- } catch (e) {
738
- console.error('Failed to load arts:', e);
739
- document.getElementById('sidebar').innerHTML = '<div class="loading">Failed to load</div>';
740
- }
741
- }
742
-
743
- function renderSidebar() {
744
- const sidebar = document.getElementById('sidebar');
745
- const categories = {};
746
-
747
- const filtered = searchQuery
748
- ? arts.filter(a => a.metadata.title.toLowerCase().includes(searchQuery.toLowerCase()))
749
- : arts;
750
-
751
- for (const art of filtered) {
752
- const cat = art.metadata.category || 'Components';
753
- if (!categories[cat]) categories[cat] = [];
754
- categories[cat].push(art);
755
- }
756
-
757
- if (Object.keys(categories).length === 0) {
758
- sidebar.innerHTML = '<div class="sidebar-section"><div class="loading">No components found</div></div>';
759
- return;
760
- }
761
-
762
- let html = '';
763
- for (const [category, items] of Object.entries(categories)) {
764
- const escapedCategory = escapeHtml(category);
765
- html += '<div class="sidebar-section">';
766
- html += '<div class="category-header" data-category="' + escapedCategory + '">';
767
- 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>';
768
- html += '<span>' + escapedCategory + '</span>';
769
- html += '<span class="category-count">' + items.length + '</span>';
770
- html += '</div>';
771
- html += '<ul class="art-list" data-category="' + escapedCategory + '">';
772
- for (const art of items) {
773
- const active = selectedArt?.path === art.path ? 'active' : '';
774
- const variantCount = art.variants?.length || 0;
775
- html += '<li class="art-item ' + active + '" data-path="' + escapeHtml(art.path) + '">';
776
- html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
777
- html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
778
- html += '</li>';
779
- }
780
- html += '</ul>';
781
- html += '</div>';
782
- }
783
-
784
- sidebar.innerHTML = html;
785
-
786
- sidebar.querySelectorAll('.art-item').forEach(item => {
787
- item.addEventListener('click', () => {
788
- const artPath = item.dataset.path;
789
- selectedArt = arts.find(a => a.path === artPath);
790
- renderSidebar();
791
- renderContent();
792
- });
793
- });
794
-
795
- sidebar.querySelectorAll('.category-header').forEach(header => {
796
- header.addEventListener('click', () => {
797
- header.classList.toggle('collapsed');
798
- const list = header.parentElement?.querySelector('.art-list');
799
- if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
800
- });
801
- });
802
- }
803
-
804
- function renderContent() {
805
- const content = document.getElementById('content');
806
- if (!selectedArt) {
807
- content.innerHTML = \`
808
- <div class="empty-state">
809
- <div class="empty-state-icon">
810
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
811
- <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"/>
812
- <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"/>
813
- <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"/>
814
- </svg>
815
- </div>
816
- <div class="empty-state-title">Select a component</div>
817
- <div class="empty-state-text">Choose a component from the sidebar to view its variants</div>
818
- </div>
819
- \`;
820
- return;
821
- }
822
-
823
- const meta = selectedArt.metadata;
824
- const tags = meta.tags || [];
825
- const variantCount = selectedArt.variants?.length || 0;
826
-
827
- let html = '<div class="content-inner">';
828
- html += '<div class="content-header">';
829
- html += '<h1 class="content-title">' + escapeHtml(meta.title) + '</h1>';
830
- if (meta.description) {
831
- html += '<p class="content-description">' + escapeHtml(meta.description) + '</p>';
832
- }
833
- html += '<div class="content-meta">';
834
- 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>';
835
- if (meta.category) {
836
- 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>';
837
- }
838
- for (const tag of tags) {
839
- html += '<span class="meta-tag">#' + escapeHtml(tag) + '</span>';
840
- }
841
- html += '</div>';
842
- html += '</div>';
843
-
844
- html += '<div class="gallery">';
845
- for (const variant of selectedArt.variants) {
846
- const previewUrl = basePath + '/preview?art=' + encodeURIComponent(selectedArt.path) + '&variant=' + encodeURIComponent(variant.name);
847
- const escapedPreviewUrl = escapeHtml(previewUrl);
848
-
849
- html += '<div class="variant-card">';
850
- html += '<div class="variant-preview">';
851
- html += '<iframe src="' + escapedPreviewUrl + '" loading="lazy" title="' + escapeHtml(variant.name) + '"></iframe>';
852
- html += '</div>';
853
- html += '<div class="variant-info">';
854
- html += '<div>';
855
- html += '<span class="variant-name">' + escapeHtml(variant.name) + '</span>';
856
- if (variant.isDefault) html += ' <span class="variant-badge">Default</span>';
857
- html += '</div>';
858
- html += '<div class="variant-actions">';
859
- html += '<button class="variant-action-btn" title="Open in new tab" data-preview-url="' + escapedPreviewUrl + '"><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>';
860
- html += '</div>';
861
- html += '</div>';
862
- html += '</div>';
863
- }
864
- html += '</div>';
865
- html += '</div>';
866
-
867
- content.innerHTML = html;
868
-
869
- content.querySelectorAll('.variant-action-btn[data-preview-url]').forEach(button => {
870
- button.addEventListener('click', () => {
871
- const previewUrl = button.dataset.previewUrl;
872
- if (previewUrl) window.open(previewUrl, '_blank', 'noopener');
873
- });
874
- });
875
- }
876
-
877
- function escapeHtml(str) {
878
- if (!str) return '';
879
- return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
880
- }
881
-
882
- // Search
883
- document.getElementById('search').addEventListener('input', (e) => {
884
- searchQuery = e.target.value;
885
- renderSidebar();
886
- });
887
-
888
- // Keyboard shortcut for search
889
- document.addEventListener('keydown', (e) => {
890
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
891
- e.preventDefault();
892
- document.getElementById('search').focus();
893
- }
894
- });
895
-
896
- loadArts();`;
897
- }
898
- //#endregion
899
- //#region src/gallery/index.ts
900
- /**
901
- * Gallery HTML generation for the Musea component gallery.
902
- *
903
- * Contains the inline gallery SPA template (used as a fallback when the
904
- * pre-built gallery is not available) and the gallery virtual module.
905
- */
906
- /**
907
- * Generate the inline gallery HTML page.
908
- */
909
- function generateGalleryHtml(basePath, devSessionToken, themeConfig) {
910
- const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
911
- return `<!DOCTYPE html>
912
- <html lang="en">
913
- <head>
914
- <meta charset="UTF-8">
915
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
916
- <title>Musea - Component Gallery</title>
917
- <script>window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}<\/script>
918
- <style>${generateGalleryStyles()}
919
- </style>
920
- </head>
921
- <body>${generateGalleryBody(basePath)}
922
-
923
- <script type="module">${generateGalleryScript(basePath)}
924
- <\/script>
925
- </body>
926
- </html>`;
927
- }
928
- /**
929
- * Generate the virtual gallery module code.
930
- */
931
- function generateGalleryModule(basePath) {
932
- return `
933
- export const basePath = ${serializeScriptValue(basePath)};
934
- export async function loadArts() {
935
- const res = await fetch(basePath + '/api/arts');
936
- return res.json();
937
- }
938
- `;
939
- }
940
- //#endregion
941
482
  //#region src/preview/addons.ts
942
483
  /**
943
484
  * Addon initialization code for Musea preview iframes.
@@ -1597,6 +1138,15 @@ mount();
1597
1138
  //#endregion
1598
1139
  //#region src/server-middleware.ts
1599
1140
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
1141
+ const galleryAssetMimeTypes = {
1142
+ ".js": "application/javascript",
1143
+ ".css": "text/css",
1144
+ ".svg": "image/svg+xml",
1145
+ ".png": "image/png",
1146
+ ".ico": "image/x-icon",
1147
+ ".woff2": "font/woff2",
1148
+ ".woff": "font/woff"
1149
+ };
1600
1150
  function resolveGalleryDistDir() {
1601
1151
  return path.resolve(moduleDir, "gallery");
1602
1152
  }
@@ -1624,6 +1174,29 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToke
1624
1174
  html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
1625
1175
  return devServer.transformIndexHtml(url, html);
1626
1176
  }
1177
+ async function generateFallbackGalleryHtml(basePath, devSessionToken, themeConfig) {
1178
+ const { generateGalleryHtml } = await import("./gallery-zu8hc8Lc.mjs").then((n) => n.t);
1179
+ return generateGalleryHtml(basePath, devSessionToken, themeConfig);
1180
+ }
1181
+ async function serveGalleryAsset(galleryDistDir, requestUrl, res) {
1182
+ try {
1183
+ const filePath = resolveUrlPathInside(galleryDistDir, requestUrl, "asset path");
1184
+ if (!(await fs.promises.stat(filePath)).isFile()) return false;
1185
+ const content = await fs.promises.readFile(filePath);
1186
+ const ext = path.extname(filePath);
1187
+ res.setHeader("Content-Type", galleryAssetMimeTypes[ext] || "application/octet-stream");
1188
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1189
+ res.end(content);
1190
+ return true;
1191
+ } catch (error) {
1192
+ if (error instanceof HttpError) {
1193
+ res.statusCode = error.status;
1194
+ res.end(error.message);
1195
+ return true;
1196
+ }
1197
+ return false;
1198
+ }
1199
+ }
1627
1200
  /**
1628
1201
  * Register all Musea middleware on the given dev server.
1629
1202
  *
@@ -1656,39 +1229,14 @@ function registerMiddleware(devServer, ctx) {
1656
1229
  res.end(sourceHtml);
1657
1230
  return;
1658
1231
  }
1659
- const html = generateGalleryHtml(basePath, devSessionToken, themeConfig);
1232
+ const html = await generateFallbackGalleryHtml(basePath, devSessionToken, themeConfig);
1660
1233
  res.setHeader("Content-Type", "text/html");
1661
1234
  res.end(html);
1662
1235
  return;
1663
1236
  }
1664
1237
  }
1665
1238
  if (url.startsWith("/assets/")) {
1666
- const galleryDistDir = resolveGalleryDistDir();
1667
- try {
1668
- const filePath = resolveUrlPathInside(galleryDistDir, url, "asset path");
1669
- if ((await fs.promises.stat(filePath)).isFile()) {
1670
- const content = await fs.promises.readFile(filePath);
1671
- const ext = path.extname(filePath);
1672
- res.setHeader("Content-Type", {
1673
- ".js": "application/javascript",
1674
- ".css": "text/css",
1675
- ".svg": "image/svg+xml",
1676
- ".png": "image/png",
1677
- ".ico": "image/x-icon",
1678
- ".woff2": "font/woff2",
1679
- ".woff": "font/woff"
1680
- }[ext] || "application/octet-stream");
1681
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1682
- res.end(content);
1683
- return;
1684
- }
1685
- } catch (error) {
1686
- if (error instanceof HttpError) {
1687
- res.statusCode = error.status;
1688
- res.end(error.message);
1689
- return;
1690
- }
1691
- }
1239
+ if (await serveGalleryAsset(resolveGalleryDistDir(), url, res)) return;
1692
1240
  }
1693
1241
  next();
1694
1242
  });
@@ -1767,7 +1315,17 @@ function registerMiddleware(devServer, ctx) {
1767
1315
  });
1768
1316
  devServer.middlewares.use(`${basePath}/art`, async (req, res, next) => {
1769
1317
  const url = new URL(req.url || "", "http://localhost");
1770
- const artPath = decodeURIComponent(url.pathname.slice(1));
1318
+ let artPath;
1319
+ try {
1320
+ artPath = decodeUrlComponent(url.pathname.slice(1), "art path");
1321
+ } catch (error) {
1322
+ if (error instanceof HttpError) {
1323
+ res.statusCode = error.status;
1324
+ res.end(error.message);
1325
+ return;
1326
+ }
1327
+ throw error;
1328
+ }
1771
1329
  if (!artPath) {
1772
1330
  next();
1773
1331
  return;
@@ -1805,122 +1363,32 @@ function registerMiddleware(devServer, ctx) {
1805
1363
  });
1806
1364
  }
1807
1365
  //#endregion
1808
- //#region src/tokens/parser.ts
1809
- /**
1810
- * Token parsing utilities for Style Dictionary integration.
1811
- *
1812
- * Reads and parses design token files (JSON) and directories,
1813
- * flattening nested structures into categorized token collections.
1814
- */
1815
- /**
1816
- * Parse Style Dictionary tokens file.
1817
- */
1818
- async function parseTokens(tokensPath) {
1819
- const absolutePath = path.resolve(tokensPath);
1820
- if ((await fs.promises.stat(absolutePath)).isDirectory()) return parseTokenDirectory(absolutePath);
1821
- const content = await fs.promises.readFile(absolutePath, "utf-8");
1822
- return flattenTokens(JSON.parse(content));
1366
+ //#region src/tokens/native.ts
1367
+ function tokenNative() {
1368
+ return loadNative();
1823
1369
  }
1824
- /**
1825
- * Parse tokens from a directory.
1826
- */
1827
- async function parseTokenDirectory(dirPath) {
1828
- const mergedTokens = createRecord();
1829
- await mergeTokenDirectory(mergedTokens, dirPath);
1830
- return flattenTokens(mergedTokens);
1831
- }
1832
- async function mergeTokenDirectory(target, dirPath) {
1833
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1834
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1835
- const fullPath = path.join(dirPath, entry.name);
1836
- if (entry.isDirectory()) {
1837
- await mergeTokenDirectory(target, fullPath);
1838
- continue;
1839
- }
1840
- if (!entry.isFile() || !entry.name.endsWith(".json") && !entry.name.endsWith(".tokens.json")) continue;
1841
- const content = await fs.promises.readFile(fullPath, "utf-8");
1842
- deepMergeTokenTrees(target, JSON.parse(content));
1843
- }
1844
- }
1845
- function deepMergeTokenTrees(target, source) {
1846
- for (const [key, value] of Object.entries(source)) {
1847
- const existing = target[key];
1848
- if (isPlainObject(existing) && isPlainObject(value) && !isTokenValue(existing) && !isTokenValue(value)) {
1849
- deepMergeTokenTrees(existing, value);
1850
- continue;
1851
- }
1852
- target[key] = cloneTokenTree(value);
1853
- }
1854
- }
1855
- /**
1856
- * Flatten nested token structure into categories.
1857
- */
1858
- function flattenTokens(tokens, prefix = []) {
1859
- const categories = [];
1860
- for (const [key, value] of Object.entries(tokens)) {
1861
- if (isTokenValue(value)) continue;
1862
- if (typeof value === "object" && value !== null) {
1863
- const categoryTokens = extractTokens(value);
1864
- const subcategories = flattenTokens(value, [...prefix, key]);
1865
- if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
1866
- name: formatCategoryName(key),
1867
- tokens: categoryTokens,
1868
- subcategories: subcategories.length > 0 ? subcategories : void 0
1869
- });
1870
- }
1370
+ function normalizeCategories(categories) {
1371
+ for (const category of categories) {
1372
+ category.tokens = nullRecord(category.tokens);
1373
+ if (category.subcategories) normalizeCategories(category.subcategories);
1871
1374
  }
1872
1375
  return categories;
1873
1376
  }
1874
- /**
1875
- * Extract token values from an object.
1876
- */
1877
- function extractTokens(obj) {
1878
- const tokens = createRecord();
1879
- for (const [key, value] of Object.entries(obj)) if (isTokenValue(value)) tokens[key] = normalizeToken(value);
1880
- return tokens;
1881
- }
1882
- function cloneTokenTree(value) {
1883
- if (Array.isArray(value)) return value.map(cloneTokenTree);
1884
- if (isPlainObject(value)) {
1885
- const cloned = createRecord();
1886
- for (const [key, child] of Object.entries(value)) cloned[key] = cloneTokenTree(child);
1887
- return cloned;
1888
- }
1889
- return value;
1890
- }
1891
- /**
1892
- * Check if a value is a token definition.
1893
- */
1894
- function isTokenValue(value) {
1895
- if (typeof value !== "object" || value === null) return false;
1896
- const obj = value;
1897
- return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number") || "$value" in obj && (typeof obj.$value === "string" || typeof obj.$value === "number");
1898
- }
1899
- function isPlainObject(value) {
1900
- return typeof value === "object" && value !== null && !Array.isArray(value);
1901
- }
1902
- function createRecord() {
1903
- return Object.create(null);
1377
+ function nullRecord(record) {
1378
+ return Object.assign(Object.create(null), record);
1904
1379
  }
1380
+ //#endregion
1381
+ //#region src/tokens/parser.ts
1905
1382
  /**
1906
- * Normalize token to DesignToken interface.
1383
+ * Token parsing utilities for Style Dictionary integration.
1384
+ *
1385
+ * Thin native binding for design token files (JSON) and directories.
1907
1386
  */
1908
- function normalizeToken(raw) {
1909
- const token = {
1910
- value: raw.value ?? raw.$value,
1911
- type: raw.type ?? raw.$type,
1912
- description: raw.description,
1913
- attributes: raw.attributes
1914
- };
1915
- if (raw.$tier === "primitive" || raw.$tier === "semantic") token.$tier = raw.$tier;
1916
- if (typeof raw.$reference === "string") token.$reference = raw.$reference;
1917
- return token;
1918
- }
1919
1387
  /**
1920
- * Format category name for display.
1388
+ * Parse Style Dictionary tokens file or directory.
1921
1389
  */
1922
- function formatCategoryName(name) {
1923
- return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1390
+ async function parseTokens(tokensPath) {
1391
+ return normalizeCategories(tokenNative().parseDesignTokensFromPath(tokensPath));
1924
1392
  }
1925
1393
  //#endregion
1926
1394
  //#region src/tokens/usage.ts
@@ -2027,8 +1495,6 @@ function scanTokenUsage(artFiles, tokenMap) {
2027
1495
  * Handles building flat token maps from categories, resolving reference chains,
2028
1496
  * reading/writing raw token files, and validating semantic references.
2029
1497
  */
2030
- const REFERENCE_PATTERN = /^\{(.+)\}$/;
2031
- const MAX_RESOLVE_DEPTH = 10;
2032
1498
  const UNSAFE_TOKEN_PATH_SEGMENTS = new Set([
2033
1499
  "__proto__",
2034
1500
  "prototype",
@@ -2044,53 +1510,15 @@ function parseTokenPath(dotPath) {
2044
1510
  /**
2045
1511
  * Flatten nested categories into a flat map keyed by dot-path.
2046
1512
  */
2047
- function buildTokenMap(categories, prefix = []) {
2048
- const map = Object.create(null);
2049
- for (const cat of categories) {
2050
- const catKey = cat.name.toLowerCase().replace(/\s+/g, "-");
2051
- const catPath = [...prefix, catKey];
2052
- for (const [name, token] of Object.entries(cat.tokens)) {
2053
- const dotPath = [...catPath, name].join(".");
2054
- map[dotPath] = token;
2055
- }
2056
- if (cat.subcategories) {
2057
- const subMap = buildTokenMap(cat.subcategories, catPath);
2058
- Object.assign(map, subMap);
2059
- }
2060
- }
2061
- return map;
1513
+ function buildTokenMap(categories) {
1514
+ return nullRecord(tokenNative().buildDesignTokenMap(categories));
2062
1515
  }
2063
1516
  /**
2064
1517
  * Resolve references in categories, setting $tier, $reference, and $resolvedValue.
2065
1518
  */
2066
- function resolveReferences(categories, tokenMap) {
2067
- for (const cat of categories) {
2068
- for (const token of Object.values(cat.tokens)) resolveTokenReference(token, tokenMap);
2069
- if (cat.subcategories) resolveReferences(cat.subcategories, tokenMap);
2070
- }
2071
- }
2072
- function resolveTokenReference(token, tokenMap) {
2073
- if (typeof token.value === "string") {
2074
- const match = token.value.match(REFERENCE_PATTERN);
2075
- if (match) {
2076
- token.$tier = token.$tier ?? "semantic";
2077
- token.$reference = match[1];
2078
- token.$resolvedValue = resolveValue(match[1], tokenMap, 0, /* @__PURE__ */ new Set());
2079
- return;
2080
- }
2081
- }
2082
- token.$tier = token.$tier ?? "primitive";
2083
- }
2084
- function resolveValue(ref, tokenMap, depth, visited) {
2085
- if (depth >= MAX_RESOLVE_DEPTH || visited.has(ref)) return void 0;
2086
- visited.add(ref);
2087
- const target = tokenMap[ref];
2088
- if (!target) return void 0;
2089
- if (typeof target.value === "string") {
2090
- const match = target.value.match(REFERENCE_PATTERN);
2091
- if (match) return resolveValue(match[1], tokenMap, depth + 1, visited);
2092
- }
2093
- return target.value;
1519
+ function resolveReferences(categories, _tokenMap) {
1520
+ const resolved = tokenNative().resolveDesignTokenReferences(categories);
1521
+ categories.splice(0, categories.length, ...normalizeCategories(resolved.categories));
2094
1522
  }
2095
1523
  /**
2096
1524
  * Read raw JSON token file.
@@ -2158,48 +1586,13 @@ function deleteTokenAtPath(data, dotPath) {
2158
1586
  * Validate that a semantic reference points to an existing token and has no cycles.
2159
1587
  */
2160
1588
  function validateSemanticReference(tokenMap, reference, selfPath) {
2161
- if (!tokenMap[reference]) return {
2162
- valid: false,
2163
- error: `Reference target "${reference}" does not exist`
2164
- };
2165
- const visited = /* @__PURE__ */ new Set();
2166
- if (selfPath) visited.add(selfPath);
2167
- let current = reference;
2168
- let depth = 0;
2169
- while (depth < MAX_RESOLVE_DEPTH) {
2170
- if (visited.has(current)) return {
2171
- valid: false,
2172
- error: `Circular reference detected at "${current}"`
2173
- };
2174
- visited.add(current);
2175
- const target = tokenMap[current];
2176
- if (!target) break;
2177
- if (typeof target.value === "string") {
2178
- const match = target.value.match(REFERENCE_PATTERN);
2179
- if (match) {
2180
- current = match[1];
2181
- depth++;
2182
- continue;
2183
- }
2184
- }
2185
- break;
2186
- }
2187
- if (depth >= MAX_RESOLVE_DEPTH) return {
2188
- valid: false,
2189
- error: "Reference chain too deep (max 10)"
2190
- };
2191
- return { valid: true };
1589
+ return tokenNative().validateDesignTokenReference(tokenMap, reference, selfPath);
2192
1590
  }
2193
1591
  /**
2194
1592
  * Find all tokens that reference the given path.
2195
1593
  */
2196
1594
  function findDependentTokens(tokenMap, targetPath) {
2197
- const dependents = [];
2198
- for (const [path, token] of Object.entries(tokenMap)) if (typeof token.value === "string") {
2199
- const match = token.value.match(REFERENCE_PATTERN);
2200
- if (match && match[1] === targetPath) dependents.push(path);
2201
- }
2202
- return dependents;
1595
+ return tokenNative().findDependentDesignTokens(tokenMap, targetPath);
2203
1596
  }
2204
1597
  //#endregion
2205
1598
  //#region src/tokens/generator.ts
@@ -2210,6 +1603,10 @@ function findDependentTokens(tokenMap, targetPath) {
2210
1603
  * and provides the main processStyleDictionary orchestrator function.
2211
1604
  */
2212
1605
  const SAFE_CSS_COLOR_PATTERN = /^(?:#[0-9a-fA-F]{3,8}|(?:rgb|hsl)a?\(\s*[-+.\d,%\s]+\))$/;
1606
+ const DANGEROUS_PROTOCOL_PATTERN = /\b(javascript|vbscript):/gi;
1607
+ function escapeTokenText(value) {
1608
+ return escapeHtml(value).replace(DANGEROUS_PROTOCOL_PATTERN, "$1&#58;");
1609
+ }
2213
1610
  function safeCssColor(value, type) {
2214
1611
  if (typeof value !== "string") return null;
2215
1612
  const trimmed = value.trim();
@@ -2227,16 +1624,16 @@ function generateTokensHtml(categories) {
2227
1624
  ${color ? `<div class="color-swatch" style="background: ${color}"></div>` : ""}
2228
1625
  </div>
2229
1626
  <div class="token-info">
2230
- <div class="token-name">${escapeHtml(name)}</div>
2231
- <div class="token-value">${escapeHtml(String(token.value))}</div>
2232
- ${token.description ? `<div class="token-description">${escapeHtml(token.description)}</div>` : ""}
1627
+ <div class="token-name">${escapeTokenText(name)}</div>
1628
+ <div class="token-value">${escapeTokenText(String(token.value))}</div>
1629
+ ${token.description ? `<div class="token-description">${escapeTokenText(token.description)}</div>` : ""}
2233
1630
  </div>
2234
1631
  </div>
2235
1632
  `;
2236
1633
  };
2237
1634
  const renderCategory = (category, level = 2) => {
2238
1635
  const heading = `h${Math.min(level, 6)}`;
2239
- let html = `<${heading}>${escapeHtml(category.name)}</${heading}>`;
1636
+ let html = `<${heading}>${escapeTokenText(category.name)}</${heading}>`;
2240
1637
  html += "<div class=\"tokens-grid\">";
2241
1638
  for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
2242
1639
  html += "</div>";
@@ -2330,24 +1727,7 @@ function generateTokensHtml(categories) {
2330
1727
  * Generate Markdown documentation for tokens.
2331
1728
  */
2332
1729
  function generateTokensMarkdown(categories) {
2333
- const renderCategory = (category, level = 2) => {
2334
- let md = `\n${"#".repeat(level)} ${category.name}\n\n`;
2335
- if (Object.keys(category.tokens).length > 0) {
2336
- md += "| Token | Value | Description |\n";
2337
- md += "|-------|-------|-------------|\n";
2338
- for (const [name, token] of Object.entries(category.tokens)) {
2339
- const desc = token.description || "-";
2340
- md += `| \`${name}\` | \`${token.value}\` | ${desc} |\n`;
2341
- }
2342
- md += "\n";
2343
- }
2344
- if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
2345
- return md;
2346
- };
2347
- let markdown = "# Design Tokens\n\n";
2348
- markdown += `> Generated by Musea on ${(/* @__PURE__ */ new Date()).toISOString()}\n`;
2349
- for (const category of categories) markdown += renderCategory(category);
2350
- return markdown;
1730
+ return tokenNative().generateDesignTokensMarkdown(categories, (/* @__PURE__ */ new Date()).toISOString());
2351
1731
  }
2352
1732
  /**
2353
1733
  * Style Dictionary plugin for Musea.
@@ -2577,7 +1957,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
2577
1957
  */
2578
1958
  /** GET /api/arts/:path/palette */
2579
1959
  async function handleArtPalette(ctx, match, sendJson, sendError) {
2580
- const artPath = decodeURIComponent(match[1]);
1960
+ const artPath = decodeUrlComponent(match[1], "art path");
2581
1961
  const art = ctx.artFiles.get(artPath);
2582
1962
  if (!art) {
2583
1963
  sendError("Art not found", 404);
@@ -2655,7 +2035,7 @@ async function handleArtPalette(ctx, match, sendJson, sendError) {
2655
2035
  */
2656
2036
  /** GET /api/arts/:path/source */
2657
2037
  async function handleArtSource(ctx, match, sendJson, sendError) {
2658
- const artPath = decodeURIComponent(match[1]);
2038
+ const artPath = decodeUrlComponent(match[1], "art path");
2659
2039
  if (!ctx.artFiles.get(artPath)) {
2660
2040
  sendError("Art not found", 404);
2661
2041
  return;
@@ -2671,7 +2051,7 @@ async function handleArtSource(ctx, match, sendJson, sendError) {
2671
2051
  }
2672
2052
  /** GET /api/arts/:path/analysis */
2673
2053
  async function handleArtAnalysis(ctx, match, sendJson, sendError) {
2674
- const artPath = decodeURIComponent(match[1]);
2054
+ const artPath = decodeUrlComponent(match[1], "art path");
2675
2055
  const art = ctx.artFiles.get(artPath);
2676
2056
  if (!art) {
2677
2057
  sendError("Art not found", 404);
@@ -2694,7 +2074,7 @@ async function handleArtAnalysis(ctx, match, sendJson, sendError) {
2694
2074
  }
2695
2075
  /** GET /api/arts/:path/docs */
2696
2076
  async function handleArtDocs(ctx, match, sendJson, sendError) {
2697
- const artPath = decodeURIComponent(match[1]);
2077
+ const artPath = decodeUrlComponent(match[1], "art path");
2698
2078
  const art = ctx.artFiles.get(artPath);
2699
2079
  if (!art) {
2700
2080
  sendError("Art not found", 404);
@@ -2744,8 +2124,8 @@ async function handleArtDocs(ctx, match, sendJson, sendError) {
2744
2124
  }
2745
2125
  /** GET /api/arts/:path/variants/:name/a11y */
2746
2126
  function handleArtA11y(ctx, match, sendJson, sendError) {
2747
- const artPath = decodeURIComponent(match[1]);
2748
- decodeURIComponent(match[2]);
2127
+ const artPath = decodeUrlComponent(match[1], "art path");
2128
+ decodeUrlComponent(match[2], "variant name");
2749
2129
  if (!ctx.artFiles.get(artPath)) {
2750
2130
  sendError("Art not found", 404);
2751
2131
  return;
@@ -2917,7 +2297,7 @@ function createApiMiddleware(ctx) {
2917
2297
  if (url?.startsWith("/arts/") && req.method === "PUT") {
2918
2298
  const sourceMatch = url.slice(6).match(/^(.+)\/source$/);
2919
2299
  if (sourceMatch) {
2920
- const artPath = decodeURIComponent(sourceMatch[1]);
2300
+ const artPath = decodeUrlComponent(sourceMatch[1], "art path");
2921
2301
  if (!ctx.artFiles.get(artPath)) {
2922
2302
  sendError("Art not found", 404);
2923
2303
  return;
@@ -2964,7 +2344,7 @@ function createApiMiddleware(ctx) {
2964
2344
  handleArtA11y(ctx, a11yMatch, sendJson, sendError);
2965
2345
  return;
2966
2346
  }
2967
- const artPath = decodeURIComponent(rest);
2347
+ const artPath = decodeUrlComponent(rest, "art path");
2968
2348
  const art = ctx.artFiles.get(artPath);
2969
2349
  if (art) sendJson(art);
2970
2350
  else sendError("Art not found", 404);