@designtools/shadows 0.1.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/cli.js ADDED
@@ -0,0 +1,1393 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../core/src/cli/bootstrap.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+
7
+ // ../core/src/scanner/detect-framework.ts
8
+ import fs from "fs/promises";
9
+ import path from "path";
10
+ async function detectFramework(projectRoot) {
11
+ const pkgPath = path.join(projectRoot, "package.json");
12
+ let pkg = {};
13
+ try {
14
+ pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
15
+ } catch {
16
+ }
17
+ const deps = {
18
+ ...pkg.dependencies,
19
+ ...pkg.devDependencies
20
+ };
21
+ let name = "unknown";
22
+ let appDirCandidates;
23
+ let componentDirCandidates;
24
+ if (deps.next) {
25
+ name = "nextjs";
26
+ appDirCandidates = ["app", "src/app"];
27
+ componentDirCandidates = ["components/ui", "src/components/ui"];
28
+ } else if (deps["@remix-run/react"] || deps["@remix-run/node"]) {
29
+ name = "remix";
30
+ appDirCandidates = ["app/routes", "src/routes"];
31
+ componentDirCandidates = ["components/ui", "app/components/ui", "src/components/ui"];
32
+ } else if (deps.vite) {
33
+ name = "vite";
34
+ appDirCandidates = ["src/pages", "src/routes", "src", "pages"];
35
+ componentDirCandidates = ["components/ui", "src/components/ui"];
36
+ } else {
37
+ appDirCandidates = ["app", "src", "pages"];
38
+ componentDirCandidates = ["components/ui", "src/components/ui"];
39
+ }
40
+ const appResult = await findDir(projectRoot, appDirCandidates);
41
+ const componentResult = await findDir(projectRoot, componentDirCandidates);
42
+ const componentFileCount = componentResult.exists ? await countFiles(projectRoot, componentResult.dir, ".tsx") : 0;
43
+ return {
44
+ name,
45
+ appDir: appResult.dir,
46
+ appDirExists: appResult.exists,
47
+ componentDir: componentResult.dir,
48
+ componentDirExists: componentResult.exists,
49
+ componentFileCount,
50
+ cssFiles: await findCssFiles(projectRoot)
51
+ };
52
+ }
53
+ async function findDir(root, candidates) {
54
+ for (const candidate of candidates) {
55
+ const full = path.join(root, candidate);
56
+ try {
57
+ const stat = await fs.stat(full);
58
+ if (stat.isDirectory()) return { dir: candidate, exists: true };
59
+ } catch {
60
+ }
61
+ }
62
+ return { dir: candidates[0], exists: false };
63
+ }
64
+ async function countFiles(root, dir, ext) {
65
+ const full = path.join(root, dir);
66
+ try {
67
+ const entries = await fs.readdir(full);
68
+ return entries.filter((e) => e.endsWith(ext)).length;
69
+ } catch {
70
+ return 0;
71
+ }
72
+ }
73
+ async function findCssFiles(projectRoot) {
74
+ const candidates = [
75
+ "app/globals.css",
76
+ "src/app/globals.css",
77
+ "app/global.css",
78
+ "src/globals.css",
79
+ "src/index.css",
80
+ "src/app.css",
81
+ "styles/globals.css"
82
+ ];
83
+ const found = [];
84
+ for (const candidate of candidates) {
85
+ try {
86
+ await fs.access(path.join(projectRoot, candidate));
87
+ found.push(candidate);
88
+ } catch {
89
+ }
90
+ }
91
+ return found;
92
+ }
93
+
94
+ // ../core/src/cli/bootstrap.ts
95
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
96
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
97
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
98
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
99
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
100
+ async function bootstrap(config) {
101
+ const args = process.argv.slice(2);
102
+ let targetPort = config.defaultTargetPort;
103
+ let toolPort = config.defaultToolPort;
104
+ for (let i = 0; i < args.length; i++) {
105
+ if (args[i] === "--port" && args[i + 1]) {
106
+ targetPort = parseInt(args[i + 1], 10);
107
+ i++;
108
+ }
109
+ if (args[i] === "--tool-port" && args[i + 1]) {
110
+ toolPort = parseInt(args[i + 1], 10);
111
+ i++;
112
+ }
113
+ }
114
+ const projectRoot = process.cwd();
115
+ console.log("");
116
+ console.log(` ${bold(config.name)}`);
117
+ console.log("");
118
+ const pkgPath = path2.join(projectRoot, "package.json");
119
+ if (!fs2.existsSync(pkgPath)) {
120
+ console.log(` ${red("\u2717")} No package.json found`);
121
+ console.log(` ${dim("Run this command from your project root.")}`);
122
+ console.log("");
123
+ process.exit(1);
124
+ }
125
+ const framework = await detectFramework(projectRoot);
126
+ const frameworkLabel = framework.name === "nextjs" ? "Next.js" : framework.name === "remix" ? "Remix" : framework.name === "vite" ? "Vite" : "Unknown";
127
+ console.log(` ${green("\u2713")} Framework ${frameworkLabel}`);
128
+ if (framework.appDirExists) {
129
+ console.log(` ${green("\u2713")} App dir ${framework.appDir}/`);
130
+ } else {
131
+ console.log(` ${yellow("\u26A0")} App dir ${dim("not found \u2014 route detection won't be available")}`);
132
+ }
133
+ if (framework.componentDirExists) {
134
+ console.log(
135
+ ` ${green("\u2713")} Components ${framework.componentDir}/ ${dim(`(${framework.componentFileCount} files)`)}`
136
+ );
137
+ } else {
138
+ console.log(` ${yellow("\u26A0")} Components ${dim("not found \u2014 component editing won't be available")}`);
139
+ }
140
+ if (framework.cssFiles.length > 0) {
141
+ console.log(` ${green("\u2713")} CSS files ${framework.cssFiles[0]}`);
142
+ } else {
143
+ console.log(` ${yellow("\u26A0")} CSS files ${dim("no CSS files found")}`);
144
+ }
145
+ if (config.extraChecks) {
146
+ const lines = await config.extraChecks(framework, projectRoot);
147
+ for (const line of lines) {
148
+ const icon = line.status === "ok" ? green("\u2713") : line.status === "warn" ? yellow("\u26A0") : red("\u2717");
149
+ console.log(` ${icon} ${line.label.padEnd(14)} ${line.detail}`);
150
+ if (line.hint) {
151
+ console.log(` ${dim(line.hint)}`);
152
+ }
153
+ if (line.status === "error") {
154
+ console.log("");
155
+ process.exit(1);
156
+ }
157
+ }
158
+ }
159
+ console.log("");
160
+ const targetUrl = `http://localhost:${targetPort}`;
161
+ try {
162
+ await fetch(targetUrl, { signal: AbortSignal.timeout(2e3) });
163
+ console.log(` ${green("\u2713")} Target ${targetUrl}`);
164
+ } catch {
165
+ console.log(` ${red("\u2717")} No dev server at ${targetUrl}`);
166
+ console.log(` ${dim("Start your dev server first, then run this command.")}`);
167
+ console.log(` ${dim(`Use --port to specify a different port.`)}`);
168
+ console.log("");
169
+ process.exit(1);
170
+ }
171
+ console.log(` ${green("\u2713")} Tool http://localhost:${toolPort}`);
172
+ console.log("");
173
+ return { framework, targetPort, toolPort, projectRoot };
174
+ }
175
+
176
+ // src/server/index.ts
177
+ import path10 from "path";
178
+ import fs11 from "fs";
179
+ import { fileURLToPath } from "url";
180
+
181
+ // ../core/src/server/create-server.ts
182
+ import express from "express";
183
+ import { createProxyMiddleware } from "http-proxy-middleware";
184
+ import httpProxy from "http-proxy";
185
+ import { WebSocketServer } from "ws";
186
+ import fs3 from "fs";
187
+ import zlib from "zlib";
188
+ import open from "open";
189
+ import { createServer as createViteServer } from "vite";
190
+ import { transform } from "esbuild";
191
+ import react from "@vitejs/plugin-react";
192
+ import tailwindcss from "@tailwindcss/vite";
193
+ async function createToolServer(config) {
194
+ const app = express();
195
+ const projectRoot = process.cwd();
196
+ const targetUrl = `http://localhost:${config.targetPort}`;
197
+ const injectScriptUrl = config.injectScriptUrl || "/tool-inject.js";
198
+ app.use(express.json());
199
+ let compiledInjectCache = null;
200
+ app.get(injectScriptUrl, async (_req, res) => {
201
+ try {
202
+ if (!compiledInjectCache) {
203
+ const raw = fs3.readFileSync(config.injectScriptPath, "utf-8");
204
+ if (config.injectScriptPath.endsWith(".ts")) {
205
+ const result = await transform(raw, { loader: "ts" });
206
+ compiledInjectCache = result.code;
207
+ } else {
208
+ compiledInjectCache = raw;
209
+ }
210
+ }
211
+ res.type("application/javascript").send(compiledInjectCache);
212
+ } catch (err) {
213
+ console.error("Inject script compile error:", err.message);
214
+ res.status(500).send(`/* Inject script compile error: ${err.message} */`);
215
+ }
216
+ });
217
+ app.use(
218
+ "/proxy",
219
+ createProxyMiddleware({
220
+ target: targetUrl,
221
+ changeOrigin: true,
222
+ pathRewrite: { "^/proxy": "" },
223
+ selfHandleResponse: true,
224
+ on: {
225
+ proxyRes: (proxyRes, _req, res) => {
226
+ const contentType = proxyRes.headers["content-type"] || "";
227
+ const isHtml = contentType.includes("text/html");
228
+ if (isHtml) {
229
+ const encoding = proxyRes.headers["content-encoding"];
230
+ let stream = proxyRes;
231
+ if (encoding === "gzip") {
232
+ stream = proxyRes.pipe(zlib.createGunzip());
233
+ } else if (encoding === "br") {
234
+ stream = proxyRes.pipe(zlib.createBrotliDecompress());
235
+ } else if (encoding === "deflate") {
236
+ stream = proxyRes.pipe(zlib.createInflate());
237
+ }
238
+ const chunks = [];
239
+ stream.on("data", (chunk) => {
240
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
241
+ });
242
+ stream.on("end", () => {
243
+ const body = Buffer.concat(chunks).toString("utf-8");
244
+ const injected = body.replace(
245
+ "</body>",
246
+ `<script src="${injectScriptUrl}"></script></body>`
247
+ );
248
+ const headers = { ...proxyRes.headers };
249
+ delete headers["content-length"];
250
+ delete headers["content-encoding"];
251
+ delete headers["transfer-encoding"];
252
+ res.writeHead(proxyRes.statusCode || 200, headers);
253
+ res.end(injected);
254
+ });
255
+ } else {
256
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
257
+ proxyRes.pipe(res);
258
+ }
259
+ }
260
+ }
261
+ })
262
+ );
263
+ app.use(
264
+ createProxyMiddleware({
265
+ target: targetUrl,
266
+ changeOrigin: true,
267
+ pathFilter: (p) => p.startsWith("/_next") || p.startsWith("/__nextjs")
268
+ })
269
+ );
270
+ const wsProxy = httpProxy.createProxyServer({
271
+ target: targetUrl,
272
+ ws: true
273
+ });
274
+ wsProxy.on("error", (err) => {
275
+ console.error("WS proxy error:", err.message);
276
+ });
277
+ const vite = await createViteServer({
278
+ configFile: false,
279
+ root: config.clientRoot,
280
+ plugins: [react(), tailwindcss()],
281
+ server: { middlewareMode: true },
282
+ appType: "spa"
283
+ });
284
+ app.use(vite.middlewares);
285
+ const server = app.listen(config.toolPort, () => {
286
+ console.log(` Tool running at http://localhost:${config.toolPort}`);
287
+ open(`http://localhost:${config.toolPort}`);
288
+ });
289
+ const wss = new WebSocketServer({ noServer: true });
290
+ wss.on("connection", (ws) => {
291
+ ws.send(JSON.stringify({ type: "connected" }));
292
+ });
293
+ server.on("upgrade", (req, socket, head) => {
294
+ const url = req.url || "";
295
+ if (url === "/ws") {
296
+ wss.handleUpgrade(req, socket, head, (ws) => {
297
+ wss.emit("connection", ws, req);
298
+ });
299
+ } else if (url.startsWith("/_next") || url.startsWith("/__nextjs")) {
300
+ wsProxy.ws(req, socket, head);
301
+ } else {
302
+ socket.destroy();
303
+ }
304
+ });
305
+ return { app, wss, projectRoot };
306
+ }
307
+
308
+ // src/server/api/write-shadows.ts
309
+ import { Router } from "express";
310
+ import fs5 from "fs/promises";
311
+ import path4 from "path";
312
+
313
+ // src/server/scanner/presets/w3c-design-tokens.ts
314
+ import fs4 from "fs/promises";
315
+ import path3 from "path";
316
+ async function findDesignTokenFiles(projectRoot) {
317
+ const candidates = [
318
+ "tokens",
319
+ "design-tokens",
320
+ "src/tokens",
321
+ "src/design-tokens",
322
+ "styles/tokens",
323
+ "."
324
+ ];
325
+ const found = [];
326
+ for (const dir of candidates) {
327
+ try {
328
+ const fullDir = path3.join(projectRoot, dir);
329
+ const entries = await fs4.readdir(fullDir);
330
+ for (const entry of entries) {
331
+ if (entry.endsWith(".tokens.json") || entry.endsWith(".tokens")) {
332
+ found.push(path3.join(dir, entry));
333
+ }
334
+ }
335
+ } catch {
336
+ }
337
+ }
338
+ return found;
339
+ }
340
+ async function scanDesignTokenShadows(projectRoot, tokenFiles) {
341
+ const files = tokenFiles || await findDesignTokenFiles(projectRoot);
342
+ const tokens = [];
343
+ for (const file of files) {
344
+ try {
345
+ const content = await fs4.readFile(path3.join(projectRoot, file), "utf-8");
346
+ const parsed = JSON.parse(content);
347
+ extractShadowTokens(parsed, [], file, tokens);
348
+ } catch {
349
+ }
350
+ }
351
+ return tokens;
352
+ }
353
+ function extractShadowTokens(obj, pathParts, filePath, results) {
354
+ if (!obj || typeof obj !== "object") return;
355
+ if (obj.$type === "shadow" && obj.$value !== void 0) {
356
+ const tokenPath = pathParts.join(".");
357
+ const name = pathParts[pathParts.length - 1] || tokenPath;
358
+ results.push({
359
+ name,
360
+ value: obj.$value,
361
+ cssValue: w3cShadowToCss(obj.$value),
362
+ description: obj.$description,
363
+ filePath,
364
+ tokenPath
365
+ });
366
+ return;
367
+ }
368
+ const groupType = obj.$type;
369
+ for (const [key, child] of Object.entries(obj)) {
370
+ if (key.startsWith("$")) continue;
371
+ if (child && typeof child === "object") {
372
+ const childObj = child;
373
+ if (groupType === "shadow" && childObj.$value !== void 0 && !childObj.$type) {
374
+ const tokenPath = [...pathParts, key].join(".");
375
+ results.push({
376
+ name: key,
377
+ value: childObj.$value,
378
+ cssValue: w3cShadowToCss(childObj.$value),
379
+ description: childObj.$description,
380
+ filePath,
381
+ tokenPath
382
+ });
383
+ } else {
384
+ extractShadowTokens(childObj, [...pathParts, key], filePath, results);
385
+ }
386
+ }
387
+ }
388
+ }
389
+ function w3cShadowToCss(value) {
390
+ if (Array.isArray(value)) {
391
+ return value.map(singleW3cToCss).join(", ");
392
+ }
393
+ return singleW3cToCss(value);
394
+ }
395
+ function singleW3cToCss(v) {
396
+ const parts = [
397
+ v.offsetX || "0px",
398
+ v.offsetY || "0px",
399
+ v.blur || "0px",
400
+ v.spread || "0px",
401
+ v.color || "rgb(0, 0, 0, 0.1)"
402
+ ];
403
+ return parts.join(" ");
404
+ }
405
+ function cssToW3cShadow(cssValue) {
406
+ if (!cssValue || cssValue === "none") {
407
+ return { offsetX: "0px", offsetY: "0px", blur: "0px", spread: "0px", color: "transparent" };
408
+ }
409
+ const parts = [];
410
+ let depth = 0;
411
+ let current = "";
412
+ for (const char of cssValue) {
413
+ if (char === "(") depth++;
414
+ if (char === ")") depth--;
415
+ if (char === "," && depth === 0) {
416
+ parts.push(current.trim());
417
+ current = "";
418
+ } else {
419
+ current += char;
420
+ }
421
+ }
422
+ if (current.trim()) parts.push(current.trim());
423
+ const shadows = parts.map(parseSingleCssToW3c).filter((s) => s !== null);
424
+ if (shadows.length === 1) return shadows[0];
425
+ return shadows;
426
+ }
427
+ function parseSingleCssToW3c(shadow) {
428
+ const trimmed = shadow.trim();
429
+ if (!trimmed) return null;
430
+ const withoutInset = trimmed.replace(/^inset\s+/, "");
431
+ let color = "rgb(0, 0, 0, 0.1)";
432
+ let measurements = withoutInset;
433
+ const colorPatterns = [
434
+ /\s+((?:rgb|rgba|oklch|hsl|hsla)\([^)]+\))$/,
435
+ /\s+(#[\da-fA-F]{3,8})$/,
436
+ /\s+((?:black|white|transparent|currentColor))$/i
437
+ ];
438
+ for (const pattern of colorPatterns) {
439
+ const match = measurements.match(pattern);
440
+ if (match) {
441
+ color = match[1];
442
+ measurements = measurements.slice(0, match.index).trim();
443
+ break;
444
+ }
445
+ }
446
+ const dims = measurements.split(/\s+/);
447
+ if (dims.length < 2) return null;
448
+ return {
449
+ offsetX: dims[0] || "0px",
450
+ offsetY: dims[1] || "0px",
451
+ blur: dims[2] || "0px",
452
+ spread: dims[3] || "0px",
453
+ color
454
+ };
455
+ }
456
+ function buildDesignTokensJson(shadows) {
457
+ const tokens = {};
458
+ for (const shadow of shadows) {
459
+ tokens[shadow.name] = {
460
+ $type: "shadow",
461
+ $value: cssToW3cShadow(shadow.value),
462
+ ...shadow.description ? { $description: shadow.description } : {}
463
+ };
464
+ }
465
+ return tokens;
466
+ }
467
+ async function writeDesignTokensFile(filePath, tokens) {
468
+ const content = JSON.stringify(tokens, null, 2) + "\n";
469
+ await fs4.writeFile(filePath, content, "utf-8");
470
+ }
471
+ async function updateDesignTokenShadow(filePath, tokenPath, newCssValue) {
472
+ const content = await fs4.readFile(filePath, "utf-8");
473
+ const tokens = JSON.parse(content);
474
+ const pathParts = tokenPath.split(".");
475
+ let current = tokens;
476
+ for (let i = 0; i < pathParts.length - 1; i++) {
477
+ current = current[pathParts[i]];
478
+ if (!current) throw new Error(`Token path "${tokenPath}" not found`);
479
+ }
480
+ const lastKey = pathParts[pathParts.length - 1];
481
+ if (!current[lastKey]) {
482
+ throw new Error(`Token "${tokenPath}" not found`);
483
+ }
484
+ current[lastKey].$value = cssToW3cShadow(newCssValue);
485
+ await fs4.writeFile(filePath, JSON.stringify(tokens, null, 2) + "\n", "utf-8");
486
+ }
487
+
488
+ // src/server/api/write-shadows.ts
489
+ function createShadowsRouter(projectRoot) {
490
+ const router = Router();
491
+ router.post("/", async (req, res) => {
492
+ try {
493
+ const { filePath, variableName, value, selector } = req.body;
494
+ const fullPath = path4.join(projectRoot, filePath);
495
+ if (selector === "scss") {
496
+ let scss = await fs5.readFile(fullPath, "utf-8");
497
+ scss = writeShadowToScss(scss, variableName, value);
498
+ await fs5.writeFile(fullPath, scss, "utf-8");
499
+ } else if (selector === "@theme") {
500
+ let css = await fs5.readFile(fullPath, "utf-8");
501
+ css = writeShadowToTheme(css, variableName, value);
502
+ await fs5.writeFile(fullPath, css, "utf-8");
503
+ } else {
504
+ let css = await fs5.readFile(fullPath, "utf-8");
505
+ css = writeShadowToSelector(css, selector, variableName, value);
506
+ await fs5.writeFile(fullPath, css, "utf-8");
507
+ }
508
+ res.json({ ok: true, filePath, variableName, value });
509
+ } catch (err) {
510
+ console.error("Shadow write error:", err);
511
+ res.status(500).json({ error: err.message });
512
+ }
513
+ });
514
+ router.post("/create", async (req, res) => {
515
+ try {
516
+ const { filePath, variableName, value, selector } = req.body;
517
+ const fullPath = path4.join(projectRoot, filePath);
518
+ if (selector === "scss") {
519
+ let scss;
520
+ try {
521
+ scss = await fs5.readFile(fullPath, "utf-8");
522
+ } catch {
523
+ scss = "";
524
+ }
525
+ scss = addShadowToScss(scss, variableName, value);
526
+ await fs5.writeFile(fullPath, scss, "utf-8");
527
+ } else if (selector === "@theme") {
528
+ let css = await fs5.readFile(fullPath, "utf-8");
529
+ css = addShadowToTheme(css, variableName, value);
530
+ await fs5.writeFile(fullPath, css, "utf-8");
531
+ } else {
532
+ let css = await fs5.readFile(fullPath, "utf-8");
533
+ css = addShadowToSelector(css, selector, variableName, value);
534
+ await fs5.writeFile(fullPath, css, "utf-8");
535
+ }
536
+ res.json({ ok: true, filePath, variableName, value });
537
+ } catch (err) {
538
+ console.error("Shadow create error:", err);
539
+ res.status(500).json({ error: err.message });
540
+ }
541
+ });
542
+ router.post("/design-token", async (req, res) => {
543
+ try {
544
+ const { filePath, tokenPath, value } = req.body;
545
+ const fullPath = path4.join(projectRoot, filePath);
546
+ await updateDesignTokenShadow(fullPath, tokenPath, value);
547
+ res.json({ ok: true, filePath, tokenPath, value });
548
+ } catch (err) {
549
+ console.error("Design token write error:", err);
550
+ res.status(500).json({ error: err.message });
551
+ }
552
+ });
553
+ router.post("/export-tokens", async (req, res) => {
554
+ try {
555
+ const { filePath, shadows } = req.body;
556
+ const fullPath = path4.join(projectRoot, filePath);
557
+ const tokens = buildDesignTokensJson(shadows);
558
+ await fs5.mkdir(path4.dirname(fullPath), { recursive: true });
559
+ await writeDesignTokensFile(fullPath, tokens);
560
+ res.json({ ok: true, filePath, tokenCount: shadows.length });
561
+ } catch (err) {
562
+ console.error("Token export error:", err);
563
+ res.status(500).json({ error: err.message });
564
+ }
565
+ });
566
+ return router;
567
+ }
568
+ function writeShadowToSelector(css, selector, variableName, newValue) {
569
+ const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
570
+ const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
571
+ if (blockStart === -1) {
572
+ throw new Error(`Selector "${selector}" not found in CSS file`);
573
+ }
574
+ const openBrace = css.indexOf("{", blockStart);
575
+ let depth = 1;
576
+ let pos = openBrace + 1;
577
+ while (depth > 0 && pos < css.length) {
578
+ if (css[pos] === "{") depth++;
579
+ if (css[pos] === "}") depth--;
580
+ pos++;
581
+ }
582
+ const blockEnd = pos;
583
+ let block = css.slice(openBrace + 1, blockEnd - 1);
584
+ const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
585
+ const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
586
+ if (!tokenRegex.test(block)) {
587
+ throw new Error(`Variable "${variableName}" not found in "${selector}" block`);
588
+ }
589
+ block = block.replace(tokenRegex, `$1${newValue}$3`);
590
+ return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
591
+ }
592
+ function writeShadowToTheme(css, variableName, newValue) {
593
+ const themeMatch = css.match(/@theme\s*\{/);
594
+ if (!themeMatch) {
595
+ throw new Error("No @theme block found in CSS file");
596
+ }
597
+ const blockStart = themeMatch.index;
598
+ const openBrace = css.indexOf("{", blockStart);
599
+ let depth = 1;
600
+ let pos = openBrace + 1;
601
+ while (depth > 0 && pos < css.length) {
602
+ if (css[pos] === "{") depth++;
603
+ if (css[pos] === "}") depth--;
604
+ pos++;
605
+ }
606
+ const blockEnd = pos;
607
+ let block = css.slice(openBrace + 1, blockEnd - 1);
608
+ const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
609
+ const tokenRegex = new RegExp(`(${varEscaped}\\s*:\\s*)([^;]+)(;)`, "g");
610
+ if (tokenRegex.test(block)) {
611
+ block = block.replace(tokenRegex, `$1${newValue}$3`);
612
+ } else {
613
+ block = block.trimEnd() + `
614
+ ${variableName}: ${newValue};
615
+ `;
616
+ }
617
+ return css.slice(0, openBrace + 1) + block + css.slice(blockEnd - 1);
618
+ }
619
+ function addShadowToSelector(css, selector, variableName, value) {
620
+ const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
621
+ const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
622
+ if (blockStart === -1) {
623
+ return css + `
624
+ ${selector} {
625
+ ${variableName}: ${value};
626
+ }
627
+ `;
628
+ }
629
+ const openBrace = css.indexOf("{", blockStart);
630
+ let depth = 1;
631
+ let pos = openBrace + 1;
632
+ while (depth > 0 && pos < css.length) {
633
+ if (css[pos] === "{") depth++;
634
+ if (css[pos] === "}") depth--;
635
+ pos++;
636
+ }
637
+ const blockEnd = pos;
638
+ const block = css.slice(openBrace + 1, blockEnd - 1);
639
+ const newBlock = block.trimEnd() + `
640
+ ${variableName}: ${value};
641
+ `;
642
+ return css.slice(0, openBrace + 1) + newBlock + css.slice(blockEnd - 1);
643
+ }
644
+ function addShadowToTheme(css, variableName, value) {
645
+ const themeMatch = css.match(/@theme\s*\{/);
646
+ if (!themeMatch) {
647
+ return css + `
648
+ @theme {
649
+ ${variableName}: ${value};
650
+ }
651
+ `;
652
+ }
653
+ return writeShadowToTheme(css, variableName, value);
654
+ }
655
+ function writeShadowToScss(scss, variableName, newValue) {
656
+ const varEscaped = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
657
+ const regex = new RegExp(
658
+ `(${varEscaped}\\s*:\\s*)(.+?)(\\s*(?:!default)?\\s*;)`,
659
+ "g"
660
+ );
661
+ if (!regex.test(scss)) {
662
+ throw new Error(`Sass variable "${variableName}" not found in SCSS file`);
663
+ }
664
+ return scss.replace(regex, `$1${newValue}$3`);
665
+ }
666
+ function addShadowToScss(scss, variableName, value) {
667
+ const line = `${variableName}: ${value};
668
+ `;
669
+ return scss.endsWith("\n") ? scss + line : scss + "\n" + line;
670
+ }
671
+
672
+ // src/server/scanner/index.ts
673
+ import { Router as Router2 } from "express";
674
+
675
+ // ../core/src/scanner/scan-tokens.ts
676
+ import fs6 from "fs/promises";
677
+ import path5 from "path";
678
+ async function scanTokens(projectRoot, framework) {
679
+ if (framework.cssFiles.length === 0) {
680
+ return { tokens: [], cssFilePath: "", groups: {} };
681
+ }
682
+ const cssFilePath = framework.cssFiles[0];
683
+ const fullPath = path5.join(projectRoot, cssFilePath);
684
+ const css = await fs6.readFile(fullPath, "utf-8");
685
+ const rootTokens = parseBlock(css, ":root");
686
+ const darkTokens = parseBlock(css, ".dark");
687
+ const tokenMap = /* @__PURE__ */ new Map();
688
+ for (const [name, value] of rootTokens) {
689
+ const def = {
690
+ name,
691
+ category: categorizeToken(name, value),
692
+ group: getTokenGroup(name),
693
+ lightValue: value,
694
+ darkValue: darkTokens.get(name) || "",
695
+ colorFormat: detectColorFormat(value)
696
+ };
697
+ tokenMap.set(name, def);
698
+ }
699
+ for (const [name, value] of darkTokens) {
700
+ if (!tokenMap.has(name)) {
701
+ tokenMap.set(name, {
702
+ name,
703
+ category: categorizeToken(name, value),
704
+ group: getTokenGroup(name),
705
+ lightValue: "",
706
+ darkValue: value,
707
+ colorFormat: detectColorFormat(value)
708
+ });
709
+ }
710
+ }
711
+ const tokens = Array.from(tokenMap.values());
712
+ const groups = {};
713
+ for (const token of tokens) {
714
+ if (!groups[token.group]) groups[token.group] = [];
715
+ groups[token.group].push(token);
716
+ }
717
+ return { tokens, cssFilePath, groups };
718
+ }
719
+ function parseBlock(css, selector) {
720
+ const tokens = /* @__PURE__ */ new Map();
721
+ const selectorEscaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
722
+ const blockStart = css.search(new RegExp(`${selectorEscaped}\\s*\\{`));
723
+ if (blockStart === -1) return tokens;
724
+ const openBrace = css.indexOf("{", blockStart);
725
+ let depth = 1;
726
+ let pos = openBrace + 1;
727
+ while (depth > 0 && pos < css.length) {
728
+ if (css[pos] === "{") depth++;
729
+ if (css[pos] === "}") depth--;
730
+ pos++;
731
+ }
732
+ const block = css.slice(openBrace + 1, pos - 1);
733
+ const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
734
+ let match;
735
+ while ((match = propRegex.exec(block)) !== null) {
736
+ tokens.set(match[1], match[2].trim());
737
+ }
738
+ return tokens;
739
+ }
740
+ function categorizeToken(name, value) {
741
+ if (value.includes("oklch") || value.includes("hsl") || value.includes("rgb") || value.startsWith("#")) {
742
+ return "color";
743
+ }
744
+ if (name.includes("radius")) return "radius";
745
+ if (name.includes("shadow")) return "shadow";
746
+ if (name.includes("spacing")) return "spacing";
747
+ if (name.includes("font") || name.includes("text") || name.includes("tracking") || name.includes("leading")) {
748
+ return "typography";
749
+ }
750
+ if (value.endsWith("rem") || value.endsWith("px") || value.endsWith("em")) {
751
+ if (name.includes("radius")) return "radius";
752
+ return "spacing";
753
+ }
754
+ return "other";
755
+ }
756
+ function getTokenGroup(name) {
757
+ const n = name.replace(/^--/, "");
758
+ const scaleMatch = n.match(/^([\w]+)-\d+$/);
759
+ if (scaleMatch) return scaleMatch[1];
760
+ const semanticPrefixes = [
761
+ "primary",
762
+ "secondary",
763
+ "neutral",
764
+ "success",
765
+ "destructive",
766
+ "warning"
767
+ ];
768
+ for (const prefix of semanticPrefixes) {
769
+ if (n === prefix || n.startsWith(`${prefix}-`)) return prefix;
770
+ }
771
+ if (["background", "foreground", "card", "card-foreground", "popover", "popover-foreground"].includes(n)) {
772
+ return "surface";
773
+ }
774
+ if (["border", "input", "ring", "muted", "muted-foreground", "accent", "accent-foreground"].includes(n)) {
775
+ return "utility";
776
+ }
777
+ if (n.startsWith("chart")) return "chart";
778
+ if (n.startsWith("sidebar")) return "sidebar";
779
+ if (n.startsWith("radius")) return "radius";
780
+ if (n.startsWith("shadow")) return "shadow";
781
+ return "other";
782
+ }
783
+ function detectColorFormat(value) {
784
+ if (value.includes("oklch")) return "oklch";
785
+ if (value.includes("hsl")) return "hsl";
786
+ if (value.includes("rgb")) return "rgb";
787
+ if (value.startsWith("#")) return "hex";
788
+ return null;
789
+ }
790
+
791
+ // ../core/src/scanner/scan-routes.ts
792
+ import fs7 from "fs/promises";
793
+ import path6 from "path";
794
+ async function scanRoutes(projectRoot, framework) {
795
+ if (framework.name === "nextjs") {
796
+ return scanNextJsRoutes(projectRoot, framework.appDir);
797
+ }
798
+ return scanGenericRoutes(projectRoot, framework.appDir);
799
+ }
800
+ async function scanNextJsRoutes(projectRoot, appDir) {
801
+ const routes = [];
802
+ const fullAppDir = path6.join(projectRoot, appDir);
803
+ try {
804
+ await scanNextJsDir(fullAppDir, appDir, "", routes);
805
+ } catch {
806
+ }
807
+ return { routes };
808
+ }
809
+ async function scanNextJsDir(fullDir, appDir, urlPrefix, routes) {
810
+ let entries;
811
+ try {
812
+ entries = await fs7.readdir(fullDir, { withFileTypes: true });
813
+ } catch {
814
+ return;
815
+ }
816
+ const hasPage = entries.some(
817
+ (e) => e.isFile() && (e.name === "page.tsx" || e.name === "page.jsx")
818
+ );
819
+ if (hasPage) {
820
+ const pageFile = entries.find(
821
+ (e) => e.name === "page.tsx" || e.name === "page.jsx"
822
+ );
823
+ routes.push({
824
+ urlPath: urlPrefix || "/",
825
+ filePath: path6.join(
826
+ appDir,
827
+ urlPrefix.replace(/^\//, ""),
828
+ pageFile.name
829
+ )
830
+ });
831
+ }
832
+ for (const entry of entries) {
833
+ if (!entry.isDirectory()) continue;
834
+ if (entry.name.startsWith("_") || entry.name === "node_modules") continue;
835
+ let segment = entry.name;
836
+ if (segment.startsWith("(") && segment.endsWith(")")) {
837
+ await scanNextJsDir(
838
+ path6.join(fullDir, segment),
839
+ appDir,
840
+ urlPrefix,
841
+ routes
842
+ );
843
+ continue;
844
+ }
845
+ if (segment.startsWith("[") && segment.endsWith("]")) {
846
+ segment = `:${segment.slice(1, -1)}`;
847
+ }
848
+ if (segment === "api") continue;
849
+ await scanNextJsDir(
850
+ path6.join(fullDir, entry.name),
851
+ appDir,
852
+ `${urlPrefix}/${segment}`,
853
+ routes
854
+ );
855
+ }
856
+ }
857
+ async function scanGenericRoutes(projectRoot, srcDir) {
858
+ const routes = [];
859
+ const fullDir = path6.join(projectRoot, srcDir);
860
+ try {
861
+ const files = await fs7.readdir(fullDir, { withFileTypes: true });
862
+ for (const file of files) {
863
+ if (file.isFile() && (file.name.endsWith(".tsx") || file.name.endsWith(".jsx"))) {
864
+ const name = file.name.replace(/\.(tsx|jsx)$/, "");
865
+ const urlPath = name === "index" ? "/" : `/${name}`;
866
+ routes.push({
867
+ urlPath,
868
+ filePath: path6.join(srcDir, file.name)
869
+ });
870
+ }
871
+ }
872
+ } catch {
873
+ }
874
+ return { routes };
875
+ }
876
+
877
+ // ../core/src/scanner/detect-styling.ts
878
+ import fs8 from "fs/promises";
879
+ import path7 from "path";
880
+ async function detectStylingSystem(projectRoot, framework) {
881
+ const pkgPath = path7.join(projectRoot, "package.json");
882
+ let pkg = {};
883
+ try {
884
+ pkg = JSON.parse(await fs8.readFile(pkgPath, "utf-8"));
885
+ } catch {
886
+ }
887
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
888
+ if (deps.tailwindcss) {
889
+ const version = deps.tailwindcss;
890
+ const isV4 = version.startsWith("^4") || version.startsWith("~4") || version.startsWith("4");
891
+ if (isV4) {
892
+ const hasDarkMode3 = await checkDarkMode(projectRoot, framework.cssFiles);
893
+ return {
894
+ type: "tailwind-v4",
895
+ cssFiles: framework.cssFiles,
896
+ scssFiles: [],
897
+ hasDarkMode: hasDarkMode3
898
+ };
899
+ }
900
+ const configCandidates = [
901
+ "tailwind.config.ts",
902
+ "tailwind.config.js",
903
+ "tailwind.config.mjs",
904
+ "tailwind.config.cjs"
905
+ ];
906
+ let configPath;
907
+ for (const candidate of configCandidates) {
908
+ try {
909
+ await fs8.access(path7.join(projectRoot, candidate));
910
+ configPath = candidate;
911
+ break;
912
+ } catch {
913
+ }
914
+ }
915
+ const hasDarkMode2 = await checkDarkMode(projectRoot, framework.cssFiles);
916
+ return {
917
+ type: "tailwind-v3",
918
+ configPath,
919
+ cssFiles: framework.cssFiles,
920
+ scssFiles: [],
921
+ hasDarkMode: hasDarkMode2
922
+ };
923
+ }
924
+ if (deps.bootstrap) {
925
+ const hasDarkMode2 = await checkDarkMode(projectRoot, framework.cssFiles);
926
+ const scssFiles = await findBootstrapScssFiles(projectRoot);
927
+ return {
928
+ type: "bootstrap",
929
+ cssFiles: framework.cssFiles,
930
+ scssFiles,
931
+ hasDarkMode: hasDarkMode2
932
+ };
933
+ }
934
+ const hasDarkMode = await checkDarkMode(projectRoot, framework.cssFiles);
935
+ const hasCustomProps = await checkCustomProperties(projectRoot, framework.cssFiles);
936
+ if (hasCustomProps) {
937
+ return {
938
+ type: "css-variables",
939
+ cssFiles: framework.cssFiles,
940
+ scssFiles: [],
941
+ hasDarkMode
942
+ };
943
+ }
944
+ return {
945
+ type: framework.cssFiles.length > 0 ? "plain-css" : "unknown",
946
+ cssFiles: framework.cssFiles,
947
+ scssFiles: [],
948
+ hasDarkMode
949
+ };
950
+ }
951
+ async function checkDarkMode(projectRoot, cssFiles) {
952
+ for (const file of cssFiles) {
953
+ try {
954
+ const css = await fs8.readFile(path7.join(projectRoot, file), "utf-8");
955
+ if (css.includes(".dark") || css.includes('[data-theme="dark"]') || css.includes("prefers-color-scheme: dark")) {
956
+ return true;
957
+ }
958
+ } catch {
959
+ }
960
+ }
961
+ return false;
962
+ }
963
+ async function checkCustomProperties(projectRoot, cssFiles) {
964
+ for (const file of cssFiles) {
965
+ try {
966
+ const css = await fs8.readFile(path7.join(projectRoot, file), "utf-8");
967
+ if (/--[\w-]+\s*:/.test(css)) {
968
+ return true;
969
+ }
970
+ } catch {
971
+ }
972
+ }
973
+ return false;
974
+ }
975
+ async function findBootstrapScssFiles(projectRoot) {
976
+ const candidates = [
977
+ "src/scss/_variables.scss",
978
+ "src/scss/_custom.scss",
979
+ "src/scss/custom.scss",
980
+ "src/styles/_variables.scss",
981
+ "src/styles/variables.scss",
982
+ "assets/scss/_variables.scss",
983
+ "scss/_variables.scss",
984
+ "styles/_variables.scss"
985
+ ];
986
+ const found = [];
987
+ for (const candidate of candidates) {
988
+ try {
989
+ await fs8.access(path7.join(projectRoot, candidate));
990
+ found.push(candidate);
991
+ } catch {
992
+ }
993
+ }
994
+ return found;
995
+ }
996
+
997
+ // src/server/scanner/scan-shadows.ts
998
+ import fs10 from "fs/promises";
999
+ import path9 from "path";
1000
+
1001
+ // src/server/scanner/presets/tailwind.ts
1002
+ var TAILWIND_SHADOW_PRESETS = [
1003
+ {
1004
+ name: "shadow-2xs",
1005
+ value: "0 1px rgb(0 0 0 / 0.05)"
1006
+ },
1007
+ {
1008
+ name: "shadow-xs",
1009
+ value: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
1010
+ },
1011
+ {
1012
+ name: "shadow-sm",
1013
+ value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
1014
+ },
1015
+ {
1016
+ name: "shadow",
1017
+ value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
1018
+ },
1019
+ {
1020
+ name: "shadow-md",
1021
+ value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
1022
+ },
1023
+ {
1024
+ name: "shadow-lg",
1025
+ value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
1026
+ },
1027
+ {
1028
+ name: "shadow-xl",
1029
+ value: "0 25px 50px -12px rgb(0 0 0 / 0.25)"
1030
+ },
1031
+ {
1032
+ name: "shadow-2xl",
1033
+ value: "0 50px 100px -20px rgb(0 0 0 / 0.25)"
1034
+ },
1035
+ {
1036
+ name: "shadow-inner",
1037
+ value: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"
1038
+ },
1039
+ {
1040
+ name: "shadow-none",
1041
+ value: "none"
1042
+ }
1043
+ ];
1044
+
1045
+ // src/server/scanner/presets/bootstrap.ts
1046
+ import fs9 from "fs/promises";
1047
+ import path8 from "path";
1048
+ var BOOTSTRAP_SHADOW_PRESETS = [
1049
+ {
1050
+ name: "box-shadow-sm",
1051
+ value: "0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)"
1052
+ },
1053
+ {
1054
+ name: "box-shadow",
1055
+ value: "0 0.5rem 1rem rgba(0, 0, 0, 0.15)"
1056
+ },
1057
+ {
1058
+ name: "box-shadow-lg",
1059
+ value: "0 1rem 3rem rgba(0, 0, 0, 0.175)"
1060
+ },
1061
+ {
1062
+ name: "box-shadow-inset",
1063
+ value: "inset 0 1px 2px rgba(0, 0, 0, 0.075)"
1064
+ }
1065
+ ];
1066
+ async function scanBootstrapScssOverrides(projectRoot, scssFiles) {
1067
+ const overrides = [];
1068
+ for (const file of scssFiles) {
1069
+ try {
1070
+ const content = await fs9.readFile(path8.join(projectRoot, file), "utf-8");
1071
+ const lines = content.split("\n");
1072
+ for (const line of lines) {
1073
+ const match = line.match(
1074
+ /\$(box-shadow(?:-sm|-lg|-inset)?)\s*:\s*(.+?)(?:\s*!default)?\s*;/
1075
+ );
1076
+ if (match) {
1077
+ const sassName = match[1];
1078
+ let value = match[2].trim();
1079
+ value = resolveBootstrapSassColors(value);
1080
+ overrides.push({
1081
+ name: sassName,
1082
+ value,
1083
+ sassVariable: `$${sassName}`,
1084
+ cssVariable: `--bs-${sassName}`,
1085
+ filePath: file
1086
+ });
1087
+ }
1088
+ }
1089
+ } catch {
1090
+ }
1091
+ }
1092
+ return overrides;
1093
+ }
1094
+ async function scanBootstrapCssOverrides(projectRoot, cssFiles) {
1095
+ const overrides = [];
1096
+ for (const file of cssFiles) {
1097
+ try {
1098
+ const content = await fs9.readFile(path8.join(projectRoot, file), "utf-8");
1099
+ const propRegex = /(--bs-box-shadow(?:-sm|-lg|-inset)?)\s*:\s*([^;]+);/g;
1100
+ let match;
1101
+ while ((match = propRegex.exec(content)) !== null) {
1102
+ const cssVar = match[1];
1103
+ const value = match[2].trim();
1104
+ const name = cssVar.replace(/^--bs-/, "");
1105
+ overrides.push({
1106
+ name,
1107
+ value,
1108
+ sassVariable: `$${name}`,
1109
+ cssVariable: cssVar,
1110
+ filePath: file
1111
+ });
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }
1116
+ return overrides;
1117
+ }
1118
+ function resolveBootstrapSassColors(value) {
1119
+ return value.replace(/rgba\(\$black,\s*([\d.]+)\)/g, "rgba(0, 0, 0, $1)").replace(/rgba\(\$white,\s*([\d.]+)\)/g, "rgba(255, 255, 255, $1)").replace(/\$black/g, "#000").replace(/\$white/g, "#fff");
1120
+ }
1121
+
1122
+ // src/server/scanner/scan-shadows.ts
1123
+ async function scanShadows(projectRoot, framework, styling) {
1124
+ const shadows = [];
1125
+ const cssFilePath = framework.cssFiles[0] || "";
1126
+ const customShadows = await scanCustomShadows(projectRoot, framework.cssFiles);
1127
+ const overriddenNames = new Set(customShadows.map((s) => s.name));
1128
+ if (styling.type === "tailwind-v4" || styling.type === "tailwind-v3") {
1129
+ addPresets(shadows, TAILWIND_SHADOW_PRESETS, overriddenNames);
1130
+ } else if (styling.type === "bootstrap") {
1131
+ await addBootstrapShadows(shadows, projectRoot, styling, overriddenNames);
1132
+ }
1133
+ const designTokenFiles = await findDesignTokenFiles(projectRoot);
1134
+ if (designTokenFiles.length > 0) {
1135
+ const tokenShadows = await scanDesignTokenShadows(projectRoot, designTokenFiles);
1136
+ for (const token of tokenShadows) {
1137
+ if (!overriddenNames.has(token.name)) {
1138
+ shadows.push({
1139
+ name: token.name,
1140
+ value: token.cssValue,
1141
+ source: "design-token",
1142
+ isOverridden: false,
1143
+ layers: parseShadowValue(token.cssValue),
1144
+ tokenPath: token.tokenPath,
1145
+ tokenFilePath: token.filePath
1146
+ });
1147
+ }
1148
+ }
1149
+ }
1150
+ for (const custom of customShadows) {
1151
+ shadows.push({
1152
+ ...custom,
1153
+ isOverridden: true
1154
+ });
1155
+ }
1156
+ shadows.sort((a, b) => {
1157
+ const order = { custom: 0, "design-token": 1, "framework-preset": 2 };
1158
+ const aOrder = order[a.source] ?? 3;
1159
+ const bOrder = order[b.source] ?? 3;
1160
+ if (aOrder !== bOrder) return aOrder - bOrder;
1161
+ return a.name.localeCompare(b.name);
1162
+ });
1163
+ return { shadows, cssFilePath, stylingType: styling.type, designTokenFiles };
1164
+ }
1165
+ function addPresets(shadows, presets, overriddenNames) {
1166
+ for (const preset of presets) {
1167
+ if (!overriddenNames.has(preset.name)) {
1168
+ shadows.push({
1169
+ name: preset.name,
1170
+ value: preset.value,
1171
+ source: "framework-preset",
1172
+ isOverridden: false,
1173
+ layers: parseShadowValue(preset.value)
1174
+ });
1175
+ }
1176
+ }
1177
+ }
1178
+ async function addBootstrapShadows(shadows, projectRoot, styling, overriddenNames) {
1179
+ const scssOverrides = await scanBootstrapScssOverrides(projectRoot, styling.scssFiles);
1180
+ const scssOverrideMap = new Map(scssOverrides.map((o) => [o.name, o]));
1181
+ const cssOverrides = await scanBootstrapCssOverrides(projectRoot, styling.cssFiles);
1182
+ const cssOverrideMap = new Map(cssOverrides.map((o) => [o.name, o]));
1183
+ for (const preset of BOOTSTRAP_SHADOW_PRESETS) {
1184
+ if (overriddenNames.has(preset.name)) continue;
1185
+ const scssOverride = scssOverrideMap.get(preset.name);
1186
+ const cssOverride = cssOverrideMap.get(preset.name);
1187
+ const override = cssOverride || scssOverride;
1188
+ if (override) {
1189
+ shadows.push({
1190
+ name: preset.name,
1191
+ value: override.value,
1192
+ source: "framework-preset",
1193
+ isOverridden: true,
1194
+ layers: parseShadowValue(override.value),
1195
+ cssVariable: override.cssVariable,
1196
+ sassVariable: override.sassVariable
1197
+ });
1198
+ } else {
1199
+ shadows.push({
1200
+ name: preset.name,
1201
+ value: preset.value,
1202
+ source: "framework-preset",
1203
+ isOverridden: false,
1204
+ layers: parseShadowValue(preset.value),
1205
+ cssVariable: `--bs-${preset.name}`,
1206
+ sassVariable: `$${preset.name}`
1207
+ });
1208
+ }
1209
+ }
1210
+ }
1211
+ async function scanCustomShadows(projectRoot, cssFiles) {
1212
+ const shadows = [];
1213
+ for (const file of cssFiles) {
1214
+ try {
1215
+ const css = await fs10.readFile(path9.join(projectRoot, file), "utf-8");
1216
+ const rootTokens = parseBlock(css, ":root");
1217
+ for (const [name, value] of rootTokens) {
1218
+ if (name.includes("shadow") || isShadowValue(value)) {
1219
+ shadows.push({
1220
+ name: name.replace(/^--/, ""),
1221
+ value,
1222
+ source: "custom",
1223
+ isOverridden: true,
1224
+ layers: parseShadowValue(value),
1225
+ cssVariable: name
1226
+ });
1227
+ }
1228
+ }
1229
+ const themeMatch = css.match(/@theme\s*\{([\s\S]*?)\}/);
1230
+ if (themeMatch) {
1231
+ const themeBlock = themeMatch[1];
1232
+ const propRegex = /(--shadow[\w-]*)\s*:\s*([^;]+);/g;
1233
+ let match;
1234
+ while ((match = propRegex.exec(themeBlock)) !== null) {
1235
+ const name = match[1].replace(/^--/, "");
1236
+ if (!shadows.find((s) => s.name === name)) {
1237
+ shadows.push({
1238
+ name,
1239
+ value: match[2].trim(),
1240
+ source: "custom",
1241
+ isOverridden: true,
1242
+ layers: parseShadowValue(match[2].trim()),
1243
+ cssVariable: match[1]
1244
+ });
1245
+ }
1246
+ }
1247
+ }
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ return shadows;
1252
+ }
1253
+ function isShadowValue(value) {
1254
+ return /\d+px\s+\d+px/.test(value) || value.includes("inset");
1255
+ }
1256
+ function parseShadowValue(value) {
1257
+ if (!value || value === "none") return [];
1258
+ const parts = [];
1259
+ let depth = 0;
1260
+ let current = "";
1261
+ for (const char of value) {
1262
+ if (char === "(") depth++;
1263
+ if (char === ")") depth--;
1264
+ if (char === "," && depth === 0) {
1265
+ parts.push(current.trim());
1266
+ current = "";
1267
+ } else {
1268
+ current += char;
1269
+ }
1270
+ }
1271
+ if (current.trim()) parts.push(current.trim());
1272
+ return parts.map(parseSingleShadow).filter((s) => s !== null);
1273
+ }
1274
+ function parseSingleShadow(shadow) {
1275
+ const trimmed = shadow.trim();
1276
+ if (!trimmed) return null;
1277
+ const inset = trimmed.startsWith("inset");
1278
+ const withoutInset = inset ? trimmed.replace(/^inset\s*/, "") : trimmed;
1279
+ let color = "rgb(0 0 0 / 0.1)";
1280
+ let measurements = withoutInset;
1281
+ const colorPatterns = [
1282
+ /\s+((?:rgb|rgba|oklch|hsl|hsla)\([^)]+\))$/,
1283
+ /\s+(#[\da-fA-F]{3,8})$/,
1284
+ /\s+((?:black|white|transparent|currentColor))$/i
1285
+ ];
1286
+ for (const pattern of colorPatterns) {
1287
+ const match = measurements.match(pattern);
1288
+ if (match) {
1289
+ color = match[1];
1290
+ measurements = measurements.slice(0, match.index).trim();
1291
+ break;
1292
+ }
1293
+ }
1294
+ const parts = measurements.split(/\s+/);
1295
+ if (parts.length < 2) return null;
1296
+ return {
1297
+ offsetX: parts[0] || "0",
1298
+ offsetY: parts[1] || "0",
1299
+ blur: parts[2] || "0",
1300
+ spread: parts[3] || "0",
1301
+ color,
1302
+ inset
1303
+ };
1304
+ }
1305
+
1306
+ // src/server/scanner/index.ts
1307
+ var cachedScan = null;
1308
+ async function runScan(projectRoot) {
1309
+ const framework = await detectFramework(projectRoot);
1310
+ const styling = await detectStylingSystem(projectRoot, framework);
1311
+ const [tokens, shadows, routes] = await Promise.all([
1312
+ scanTokens(projectRoot, framework),
1313
+ scanShadows(projectRoot, framework, styling),
1314
+ scanRoutes(projectRoot, framework)
1315
+ ]);
1316
+ cachedScan = { framework, styling, tokens, shadows, routes };
1317
+ return cachedScan;
1318
+ }
1319
+ function createShadowsScanRouter(projectRoot) {
1320
+ const router = Router2();
1321
+ runScan(projectRoot).then(() => {
1322
+ console.log(" Project scanned successfully");
1323
+ }).catch((err) => {
1324
+ console.error(" Scan error:", err.message);
1325
+ });
1326
+ router.get("/all", async (_req, res) => {
1327
+ try {
1328
+ const result = cachedScan || await runScan(projectRoot);
1329
+ res.json(result);
1330
+ } catch (err) {
1331
+ res.status(500).json({ error: err.message });
1332
+ }
1333
+ });
1334
+ router.get("/shadows", async (_req, res) => {
1335
+ try {
1336
+ const result = cachedScan || await runScan(projectRoot);
1337
+ res.json(result.shadows);
1338
+ } catch (err) {
1339
+ res.status(500).json({ error: err.message });
1340
+ }
1341
+ });
1342
+ router.post("/rescan", async (_req, res) => {
1343
+ try {
1344
+ const result = await runScan(projectRoot);
1345
+ res.json(result);
1346
+ } catch (err) {
1347
+ res.status(500).json({ error: err.message });
1348
+ }
1349
+ });
1350
+ return router;
1351
+ }
1352
+
1353
+ // src/server/index.ts
1354
+ var __dirname = path10.dirname(fileURLToPath(import.meta.url));
1355
+ var packageRoot = fs11.existsSync(path10.join(__dirname, "../package.json")) ? path10.resolve(__dirname, "..") : path10.resolve(__dirname, "../..");
1356
+ async function startShadowsServer(preflight) {
1357
+ const clientRoot = path10.join(packageRoot, "src/client");
1358
+ const injectScriptPath = path10.join(packageRoot, "../core/src/inject/selection.ts");
1359
+ const compiledInject = path10.join(packageRoot, "dist/inject/selection.js");
1360
+ const actualInjectPath = fs11.existsSync(compiledInject) ? compiledInject : injectScriptPath;
1361
+ const { app, wss, projectRoot } = await createToolServer({
1362
+ targetPort: preflight.targetPort,
1363
+ toolPort: preflight.toolPort,
1364
+ clientRoot,
1365
+ injectScriptPath: actualInjectPath
1366
+ });
1367
+ app.use("/api/shadows", createShadowsRouter(projectRoot));
1368
+ app.use("/scan", createShadowsScanRouter(projectRoot));
1369
+ return { app, wss, projectRoot };
1370
+ }
1371
+
1372
+ // src/cli.ts
1373
+ bootstrap({
1374
+ name: "Design Tools \u2014 Shadows",
1375
+ defaultTargetPort: 3e3,
1376
+ defaultToolPort: 4410,
1377
+ extraChecks: async (framework) => {
1378
+ const lines = [];
1379
+ if (framework.cssFiles.length > 0) {
1380
+ lines.push({
1381
+ status: "ok",
1382
+ label: "Shadows",
1383
+ detail: "Will scan CSS files for shadow definitions"
1384
+ });
1385
+ }
1386
+ return lines;
1387
+ }
1388
+ }).then((result) => {
1389
+ startShadowsServer(result).catch((err) => {
1390
+ console.error("Failed to start server:", err);
1391
+ process.exit(1);
1392
+ });
1393
+ });