vibe-design-system 2.8.81 → 2.9.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/bin/init.js +112 -10
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +269 -11
- package/vds-core-template/story-generator.mjs +321 -29
package/bin/init.js
CHANGED
|
@@ -241,12 +241,22 @@ function detectFramework(pkg) {
|
|
|
241
241
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
242
242
|
if (deps["next"]) return "nextjs";
|
|
243
243
|
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
244
|
+
if (deps["react-scripts"]) return "cra"; // Phase E2 — Create React App
|
|
245
|
+
// Phase F — Vue/Svelte/Angular detection
|
|
246
|
+
if (deps["vue"] || deps["nuxt"]) return "vue";
|
|
247
|
+
if (deps["svelte"] || deps["@sveltejs/kit"]) return "svelte";
|
|
248
|
+
if (deps["@angular/core"]) return "angular";
|
|
244
249
|
return "vite";
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
/** Framework'e göre Storybook framework paket adı */
|
|
248
253
|
function storybookFrameworkPackage(framework) {
|
|
249
254
|
if (framework === "nextjs") return "@storybook/nextjs";
|
|
255
|
+
if (framework === "cra") return "@storybook/react-webpack5"; // Phase E2 — CRA uses webpack5
|
|
256
|
+
// Phase F — Cross-framework support
|
|
257
|
+
if (framework === "vue") return "@storybook/vue3-vite";
|
|
258
|
+
if (framework === "svelte") return "@storybook/svelte-vite";
|
|
259
|
+
if (framework === "angular") return "@storybook/angular";
|
|
250
260
|
return "@storybook/react-vite"; // vite & remix
|
|
251
261
|
}
|
|
252
262
|
|
|
@@ -257,6 +267,48 @@ function buildStorybookMainTs(framework, srcPrefix) {
|
|
|
257
267
|
? `\n "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",`
|
|
258
268
|
: "";
|
|
259
269
|
|
|
270
|
+
// Phase F — Vue framework
|
|
271
|
+
if (framework === "vue") {
|
|
272
|
+
return `import type { StorybookConfig } from "@storybook/vue3-vite";
|
|
273
|
+
|
|
274
|
+
const config: StorybookConfig = {
|
|
275
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
276
|
+
addons: ["@storybook/addon-essentials"],
|
|
277
|
+
framework: { name: "@storybook/vue3-vite", options: {} },
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export default config;
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Phase F — Svelte framework
|
|
285
|
+
if (framework === "svelte") {
|
|
286
|
+
return `import type { StorybookConfig } from "@storybook/svelte-vite";
|
|
287
|
+
|
|
288
|
+
const config: StorybookConfig = {
|
|
289
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|ts|svelte)"],
|
|
290
|
+
addons: ["@storybook/addon-essentials"],
|
|
291
|
+
framework: { name: "@storybook/svelte-vite", options: {} },
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export default config;
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Phase F — Angular framework
|
|
299
|
+
if (framework === "angular") {
|
|
300
|
+
return `import type { StorybookConfig } from "@storybook/angular";
|
|
301
|
+
|
|
302
|
+
const config: StorybookConfig = {
|
|
303
|
+
stories: ["../${srcPrefix}/**/*.stories.@(js|ts)"],
|
|
304
|
+
addons: ["@storybook/addon-essentials"],
|
|
305
|
+
framework: { name: "@storybook/angular", options: {} },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export default config;
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
260
312
|
if (framework === "nextjs") {
|
|
261
313
|
return `import type { StorybookConfig } from "@storybook/nextjs";
|
|
262
314
|
|
|
@@ -297,10 +349,36 @@ const config: StorybookConfig = {
|
|
|
297
349
|
options: {},
|
|
298
350
|
},
|
|
299
351
|
async viteFinal(config) {
|
|
352
|
+
// Phase C2 — read tsconfig.json paths and inject as Vite aliases
|
|
353
|
+
const extraAliases = (() => {
|
|
354
|
+
try {
|
|
355
|
+
const tsConfigPaths = [
|
|
356
|
+
path.resolve(process.cwd(), "tsconfig.json"),
|
|
357
|
+
path.resolve(process.cwd(), "tsconfig.app.json"),
|
|
358
|
+
path.resolve(process.cwd(), "${srcPrefix}", "..", "tsconfig.json"),
|
|
359
|
+
];
|
|
360
|
+
for (const tcp of tsConfigPaths) {
|
|
361
|
+
if (!require("fs").existsSync(tcp)) continue;
|
|
362
|
+
const raw = JSON.parse(require("fs").readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
|
|
363
|
+
const paths = raw?.compilerOptions?.paths || {};
|
|
364
|
+
const aliases: Record<string, string> = {};
|
|
365
|
+
for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
|
|
366
|
+
const cleanAlias = alias.replace(/\/\*$/, "");
|
|
367
|
+
const target = targets[0]?.replace(/\/\*$/, "") || "";
|
|
368
|
+
if (cleanAlias && target && cleanAlias !== "@") {
|
|
369
|
+
aliases[cleanAlias] = path.resolve(process.cwd(), target);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (Object.keys(aliases).length > 0) return aliases;
|
|
373
|
+
}
|
|
374
|
+
} catch (_) {}
|
|
375
|
+
return {};
|
|
376
|
+
})();
|
|
300
377
|
return mergeConfig(config, {
|
|
301
378
|
resolve: {
|
|
302
379
|
alias: {
|
|
303
380
|
"@": path.resolve(process.cwd(), "${srcPrefix}"),
|
|
381
|
+
...extraAliases,
|
|
304
382
|
},
|
|
305
383
|
},
|
|
306
384
|
});
|
|
@@ -673,18 +751,42 @@ if (!pkg) {
|
|
|
673
751
|
// Monorepo kök dizininde mi?
|
|
674
752
|
const monorepoPackages = detectMonorepoPackages(projectRoot);
|
|
675
753
|
if (monorepoPackages.length > 0 && pkg.workspaces) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
754
|
+
// Phase E3 — Monorepo guidance: show all workspaces with React deps, auto-install in first one
|
|
755
|
+
const reactPackages = monorepoPackages.filter(p => {
|
|
756
|
+
try {
|
|
757
|
+
const pkgPath = path.join(p, "package.json");
|
|
758
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
759
|
+
const wpkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
760
|
+
const deps = { ...wpkg.dependencies, ...wpkg.devDependencies };
|
|
761
|
+
return !!deps["react"];
|
|
762
|
+
} catch (_) { return false; }
|
|
763
|
+
});
|
|
764
|
+
const targetPackages = reactPackages.length > 0 ? reactPackages : monorepoPackages;
|
|
765
|
+
|
|
766
|
+
console.log("📦 Monorepo tespit edildi — React içeren workspace'ler:");
|
|
767
|
+
for (const p of targetPackages) {
|
|
680
768
|
const rel = path.relative(projectRoot, p);
|
|
681
|
-
console.
|
|
769
|
+
console.log(` → ${rel}`);
|
|
770
|
+
}
|
|
771
|
+
console.log("");
|
|
772
|
+
console.log("💡 Her workspace için ayrı kurulum komutları:");
|
|
773
|
+
for (const p of targetPackages) {
|
|
774
|
+
const rel = path.relative(projectRoot, p);
|
|
775
|
+
console.log(` cd ${rel} && npx vibe-design-system init`);
|
|
776
|
+
}
|
|
777
|
+
console.log("");
|
|
778
|
+
if (!process.env.VDS_COMPONENTS_DIR) {
|
|
779
|
+
// Auto-select first React workspace and continue installation there
|
|
780
|
+
const firstReact = targetPackages[0];
|
|
781
|
+
if (firstReact) {
|
|
782
|
+
const rel = path.relative(projectRoot, firstReact);
|
|
783
|
+
console.log(`🚀 İlk React workspace'e otomatik kurulum yapılıyor: ${rel}`);
|
|
784
|
+
console.log(" (VDS_COMPONENTS_DIR env değişkeni ile farklı bir workspace seçebilirsiniz)\n");
|
|
785
|
+
process.chdir(firstReact);
|
|
786
|
+
// Update projectRoot for remaining steps
|
|
787
|
+
Object.assign(global, { __vdsProjectRoot: firstReact });
|
|
788
|
+
}
|
|
682
789
|
}
|
|
683
|
-
console.warn("\n Örnek kullanım:");
|
|
684
|
-
const firstRel = path.relative(projectRoot, monorepoPackages[0]);
|
|
685
|
-
console.warn(` cd ${firstRel} && npx vibe-design-system init`);
|
|
686
|
-
console.warn("\n VDS'yi bu dizinden çalıştırmaya devam etmek istiyorsanız,");
|
|
687
|
-
console.warn(" VDS_COMPONENTS_DIR env değişkeni ile hedef klasörü belirtin.\n");
|
|
688
790
|
}
|
|
689
791
|
|
|
690
792
|
const framework = detectFramework(pkg);
|
package/package.json
CHANGED
|
@@ -945,7 +945,7 @@ function extractComponentSuggestions() {
|
|
|
945
945
|
|
|
946
946
|
/** src/pages/ içinde tanımlı ama src/components'a çıkarılmamış visual section'ları listele (component adayı raporu). */
|
|
947
947
|
function extractUnreleasedSectionCandidates() {
|
|
948
|
-
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
948
|
+
if (!PAGES_DIR || !fs.existsSync(PAGES_DIR)) return [];
|
|
949
949
|
const suggestions = extractComponentSuggestions();
|
|
950
950
|
return suggestions.map((s) => ({
|
|
951
951
|
suggestedName: s.suggestedName,
|
|
@@ -1393,6 +1393,58 @@ function extractFoundations() {
|
|
|
1393
1393
|
// Also use first found as primary (for legacy logic below)
|
|
1394
1394
|
const cssToRead = allCssCandidates.find((p) => fs.existsSync(p)) || "";
|
|
1395
1395
|
|
|
1396
|
+
// Phase C — JSON token file reader (W3C DTCG + Style Dictionary)
|
|
1397
|
+
{
|
|
1398
|
+
const tokenFileCandidates = [
|
|
1399
|
+
path.join(PROJECT_ROOT, "tokens.json"),
|
|
1400
|
+
path.join(PROJECT_ROOT, "design-tokens.json"),
|
|
1401
|
+
path.join(PROJECT_ROOT, "src", "tokens.json"),
|
|
1402
|
+
path.join(PROJECT_ROOT, "src", "design-tokens.json"),
|
|
1403
|
+
path.join(PROJECT_ROOT, "tokens", "tokens.json"),
|
|
1404
|
+
path.join(PROJECT_ROOT, "tokens", "design-tokens.json"),
|
|
1405
|
+
];
|
|
1406
|
+
// Also scan tokens/ directory for any .json files
|
|
1407
|
+
const tokensDir = path.join(PROJECT_ROOT, "tokens");
|
|
1408
|
+
if (fs.existsSync(tokensDir)) {
|
|
1409
|
+
try {
|
|
1410
|
+
for (const f of fs.readdirSync(tokensDir)) {
|
|
1411
|
+
if (f.endsWith(".json")) tokenFileCandidates.push(path.join(tokensDir, f));
|
|
1412
|
+
}
|
|
1413
|
+
} catch (_) {}
|
|
1414
|
+
}
|
|
1415
|
+
for (const tf of tokenFileCandidates) {
|
|
1416
|
+
if (!fs.existsSync(tf)) continue;
|
|
1417
|
+
try {
|
|
1418
|
+
const raw = JSON.parse(fs.readFileSync(tf, "utf-8"));
|
|
1419
|
+
// W3C DTCG: { "color": { "primary": { "$value": "#...", "$type": "color" } } }
|
|
1420
|
+
// Style Dictionary: { "color-primary": { "value": "#..." } }
|
|
1421
|
+
const flatTokens = {};
|
|
1422
|
+
function flattenDTCG(obj, prefix) {
|
|
1423
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1424
|
+
if (v && typeof v === "object") {
|
|
1425
|
+
if ("$value" in v) {
|
|
1426
|
+
if (!v.$type || v.$type === "color") {
|
|
1427
|
+
flatTokens[(prefix ? prefix + "-" : "") + k] = v.$value;
|
|
1428
|
+
}
|
|
1429
|
+
} else if ("value" in v) {
|
|
1430
|
+
flatTokens[(prefix ? prefix + "-" : "") + k] = v.value;
|
|
1431
|
+
} else {
|
|
1432
|
+
flattenDTCG(v, (prefix ? prefix + "-" : "") + k);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
flattenDTCG(raw, "");
|
|
1438
|
+
for (const [name, value] of Object.entries(flatTokens)) {
|
|
1439
|
+
const cleanName = name.replace(/^color[-_]/i, "").replace(/^-/, "");
|
|
1440
|
+
if (!colors[cleanName] && (typeof value === "string") && (value.startsWith("#") || /^(rgb|hsl|oklch|transparent)/.test(value))) {
|
|
1441
|
+
colors[cleanName] = { value, hex: value };
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
} catch (_) {}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1396
1448
|
try {
|
|
1397
1449
|
if (cssChunks.length > 0) {
|
|
1398
1450
|
const css = cssChunks.join("\n");
|
|
@@ -1429,11 +1481,33 @@ function extractFoundations() {
|
|
|
1429
1481
|
}
|
|
1430
1482
|
}
|
|
1431
1483
|
|
|
1432
|
-
//
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1484
|
+
// Phase B — @font-face declarations (variable fonts, custom webfonts)
|
|
1485
|
+
const fontFaceDecls = [];
|
|
1486
|
+
const fontFaceRe = /@font-face\s*\{([^}]+)\}/g;
|
|
1487
|
+
let ffm;
|
|
1488
|
+
while ((ffm = fontFaceRe.exec(css)) !== null) {
|
|
1489
|
+
const block = ffm[1];
|
|
1490
|
+
const fam = block.match(/font-family:\s*['"]?([^;'"]+)['"]?;/)?.[1]?.trim();
|
|
1491
|
+
const wgt = block.match(/font-weight:\s*([^;]+);/)?.[1]?.trim();
|
|
1492
|
+
const sty = block.match(/font-style:\s*([^;]+);/)?.[1]?.trim() || "normal";
|
|
1493
|
+
if (fam) {
|
|
1494
|
+
const isVariable = wgt ? /^\d+\s+\d+$/.test(wgt.trim()) : false;
|
|
1495
|
+
fontFaceDecls.push({ family: fam, weight: wgt || null, style: sty, isVariable });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (fontFaceDecls.length > 0) typography.fontFaces = fontFaceDecls;
|
|
1499
|
+
|
|
1500
|
+
// body { font-family } — Tailwind v4 sets font on html/:host, NOT body
|
|
1501
|
+
// Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
|
|
1502
|
+
const bodyMatch =
|
|
1503
|
+
css.match(/\bbody\s*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1504
|
+
css.match(/html\s*(?:,\s*:host)?\s*\{[^}]*font-family:\s*([^;]+);/); // Tailwind v4
|
|
1435
1505
|
if (bodyMatch) typography.body = bodyMatch[1].trim();
|
|
1436
|
-
|
|
1506
|
+
// Mono: code/kbd/samp/pre block (Tailwind v4 uses code,kbd,samp,pre not code,pre,.font-mono)
|
|
1507
|
+
const monoMatch =
|
|
1508
|
+
css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1509
|
+
css.match(/code,\s*kbd[^{]*\{[^}]*font-family:\s*([^;]+);/) ||
|
|
1510
|
+
css.match(/\bcode\b[^{,]*\{[^}]*font-family:\s*([^;]+);/);
|
|
1437
1511
|
if (monoMatch) typography.mono = monoMatch[1].trim();
|
|
1438
1512
|
// CSS custom properties for fonts (--font-sans, --font-mono, --font-display, etc.)
|
|
1439
1513
|
const fontVarRe = /--font([\w-]*):\s*([^;\n]+);/g;
|
|
@@ -1444,14 +1518,27 @@ function extractFoundations() {
|
|
|
1444
1518
|
if (!typography[key]) typography[key] = val;
|
|
1445
1519
|
}
|
|
1446
1520
|
// Resolve var(--font-*) references in body/mono to actual font names
|
|
1447
|
-
//
|
|
1521
|
+
// Also handles Tailwind v4 --default-font-family / --default-mono-font-family chain
|
|
1448
1522
|
function resolveTypoVar(val) {
|
|
1449
1523
|
if (!val || !val.startsWith("var(")) return val;
|
|
1524
|
+
// Direct --font-* reference: var(--font-sans) → typography.fontSans
|
|
1450
1525
|
const m = val.match(/var\(--font-([\w-]+)\)/);
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1526
|
+
if (m) {
|
|
1527
|
+
const key = `font-${m[1]}`.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
1528
|
+
return (typography[key] && !typography[key].startsWith("var(")) ? typography[key] : val;
|
|
1529
|
+
}
|
|
1530
|
+
// Tailwind v4: var(--default-font-family, ...) → resolve via fontSans
|
|
1531
|
+
if (val.includes("--default-font-family") && !val.includes("--default-mono")) {
|
|
1532
|
+
if (typography.fontSans && !typography.fontSans.startsWith("var(")) return typography.fontSans;
|
|
1533
|
+
// Fallback: strip the var() wrapper to get the fallback list
|
|
1534
|
+
return val.replace(/^var\(--default-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
|
|
1535
|
+
}
|
|
1536
|
+
// Tailwind v4: var(--default-mono-font-family, ...) → resolve via fontMono
|
|
1537
|
+
if (val.includes("--default-mono-font-family")) {
|
|
1538
|
+
if (typography.fontMono && !typography.fontMono.startsWith("var(")) return typography.fontMono;
|
|
1539
|
+
return val.replace(/^var\(--default-mono-font-family,\s*/, "").replace(/\)\s*$/, "").trim() || val;
|
|
1540
|
+
}
|
|
1541
|
+
return val;
|
|
1455
1542
|
}
|
|
1456
1543
|
if (typography.body) typography.body = resolveTypoVar(typography.body);
|
|
1457
1544
|
if (typography.mono) typography.mono = resolveTypoVar(typography.mono);
|
|
@@ -1956,6 +2043,41 @@ function extractFoundations() {
|
|
|
1956
2043
|
if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
|
|
1957
2044
|
}
|
|
1958
2045
|
|
|
2046
|
+
// Phase B — next/font detection
|
|
2047
|
+
{
|
|
2048
|
+
const nextFonts = [];
|
|
2049
|
+
const allTsxForFonts = getAllTsxJsxInDir(SRC_DIR);
|
|
2050
|
+
const nextFontRe = /import\s*\{\s*([\w,\s]+)\s*\}\s*from\s*['"]next\/font\/(google|local)['"]/g;
|
|
2051
|
+
const fontCallRe = /const\s+(\w+)\s*=\s*(\w+)\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
2052
|
+
for (const file of allTsxForFonts) {
|
|
2053
|
+
let src;
|
|
2054
|
+
try { src = fs.readFileSync(file, "utf-8"); } catch (_) { continue; }
|
|
2055
|
+
nextFontRe.lastIndex = 0;
|
|
2056
|
+
let nm;
|
|
2057
|
+
while ((nm = nextFontRe.exec(src)) !== null) {
|
|
2058
|
+
const source = `next/font/${nm[2]}`;
|
|
2059
|
+
const imported = nm[1].split(",").map(s => s.trim()).filter(Boolean);
|
|
2060
|
+
for (const fontName of imported) {
|
|
2061
|
+
fontCallRe.lastIndex = 0;
|
|
2062
|
+
let cm;
|
|
2063
|
+
while ((cm = fontCallRe.exec(src)) !== null) {
|
|
2064
|
+
if (cm[2] === fontName) {
|
|
2065
|
+
const varM = cm[3].match(/variable:\s*['"]([^'"]+)['"]/);
|
|
2066
|
+
const subM = cm[3].match(/subsets?:\s*\[([^\]]+)\]/);
|
|
2067
|
+
nextFonts.push({
|
|
2068
|
+
source,
|
|
2069
|
+
family: fontName,
|
|
2070
|
+
variable: varM ? varM[1] : null,
|
|
2071
|
+
subsets: subM ? subM[1].replace(/['"]/g, "").split(",").map(s=>s.trim()) : [],
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (nextFonts.length > 0) typography.nextFonts = nextFonts;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1959
2081
|
return {
|
|
1960
2082
|
colors: foundationsColors,
|
|
1961
2083
|
typography,
|
|
@@ -2089,6 +2211,88 @@ function extractTokenUsage() {
|
|
|
2089
2211
|
};
|
|
2090
2212
|
}
|
|
2091
2213
|
|
|
2214
|
+
/**
|
|
2215
|
+
* Phase A — Color Intelligence v2.9.0
|
|
2216
|
+
* Counts how many times each color token is used in src/ files.
|
|
2217
|
+
* Tracks bg/text/border/ring/fill/stroke/other categories and dark: usage separately.
|
|
2218
|
+
* O(files) complexity — single readFileSync per file, one combined regex for all color names.
|
|
2219
|
+
*/
|
|
2220
|
+
function extractColorUsage(colorTokenNames) {
|
|
2221
|
+
if (!fs.existsSync(SRC_DIR) || !colorTokenNames || colorTokenNames.length === 0) return null;
|
|
2222
|
+
const allSrcFiles = getAllTsxJsxInDir(SRC_DIR).filter(f => !f.includes("stories"));
|
|
2223
|
+
if (allSrcFiles.length === 0) return null;
|
|
2224
|
+
const prefixes = ["bg","text","border","ring","fill","stroke","from","to","via","shadow","outline","decoration","placeholder","accent"];
|
|
2225
|
+
const escapedNames = colorTokenNames.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2226
|
+
const combined = new RegExp(
|
|
2227
|
+
`\\b(dark:)?(${prefixes.join("|")})-(${escapedNames.join("|")})(?:\\/\\d+)?\\b`,
|
|
2228
|
+
"g"
|
|
2229
|
+
);
|
|
2230
|
+
const usage = {};
|
|
2231
|
+
const fileMap = {};
|
|
2232
|
+
for (const file of allSrcFiles) {
|
|
2233
|
+
const compName = path.basename(file, path.extname(file));
|
|
2234
|
+
let content;
|
|
2235
|
+
try { content = fs.readFileSync(path.join(SRC_DIR, file), "utf-8"); } catch (_) { continue; }
|
|
2236
|
+
combined.lastIndex = 0;
|
|
2237
|
+
let m;
|
|
2238
|
+
while ((m = combined.exec(content)) !== null) {
|
|
2239
|
+
const isDark = !!m[1];
|
|
2240
|
+
const prefix = m[2];
|
|
2241
|
+
const colorName = m[3];
|
|
2242
|
+
if (!usage[colorName]) {
|
|
2243
|
+
usage[colorName] = { bg:0, text:0, border:0, ring:0, fill:0, stroke:0, other:0, total:0, dark:0 };
|
|
2244
|
+
}
|
|
2245
|
+
if (isDark) { usage[colorName].dark++; continue; }
|
|
2246
|
+
const cat = prefix === "bg" ? "bg" : prefix === "text" ? "text" : prefix === "border" ? "border"
|
|
2247
|
+
: prefix === "ring" ? "ring" : prefix === "fill" ? "fill" : prefix === "stroke" ? "stroke" : "other";
|
|
2248
|
+
usage[colorName][cat]++;
|
|
2249
|
+
usage[colorName].total++;
|
|
2250
|
+
if (!fileMap[colorName]) fileMap[colorName] = new Map();
|
|
2251
|
+
fileMap[colorName].set(compName, (fileMap[colorName].get(compName) || 0) + 1);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
for (const [name, u] of Object.entries(usage)) {
|
|
2255
|
+
const sorted = [...(fileMap[name]?.entries() || [])].sort((a,b) => b[1]-a[1]).slice(0, 5);
|
|
2256
|
+
u.topFiles = sorted.map(([f]) => f);
|
|
2257
|
+
}
|
|
2258
|
+
return Object.keys(usage).length > 0 ? usage : null;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Phase E — Dark mode strategy detection v2.13.0
|
|
2263
|
+
* Detects whether the project uses class-based, media-query, or data-attribute dark mode.
|
|
2264
|
+
*/
|
|
2265
|
+
function detectDarkModeStrategy(projectRoot) {
|
|
2266
|
+
// 1. Check tailwind.config.js/ts
|
|
2267
|
+
const twConfigs = ["tailwind.config.js","tailwind.config.ts","tailwind.config.mjs","tailwind.config.cjs"];
|
|
2268
|
+
for (const cfg of twConfigs) {
|
|
2269
|
+
const p = path.join(projectRoot, cfg);
|
|
2270
|
+
if (!fs.existsSync(p)) continue;
|
|
2271
|
+
try {
|
|
2272
|
+
const src = fs.readFileSync(p, "utf-8");
|
|
2273
|
+
const m = src.match(/darkMode\s*:\s*['"]?(class|media|selector)['"]?/);
|
|
2274
|
+
if (m) return m[1] === "selector" ? "data-attribute" : m[1];
|
|
2275
|
+
} catch (_) {}
|
|
2276
|
+
}
|
|
2277
|
+
// 2. Check CSS files for .dark {} or [data-theme="dark"] or @media (prefers-color-scheme)
|
|
2278
|
+
const cssCandidates = [
|
|
2279
|
+
path.join(projectRoot, "src", "index.css"),
|
|
2280
|
+
path.join(projectRoot, "src", "globals.css"),
|
|
2281
|
+
path.join(projectRoot, "src", "styles", "globals.css"),
|
|
2282
|
+
path.join(projectRoot, "app", "globals.css"),
|
|
2283
|
+
];
|
|
2284
|
+
for (const cp of cssCandidates) {
|
|
2285
|
+
if (!fs.existsSync(cp)) continue;
|
|
2286
|
+
try {
|
|
2287
|
+
const css = fs.readFileSync(cp, "utf-8");
|
|
2288
|
+
if (/\[data-theme\s*=\s*['"]dark['"]\]/.test(css)) return "data-attribute";
|
|
2289
|
+
if (/\.dark\s*[{,]/.test(css)) return "class";
|
|
2290
|
+
if (/@media\s*\(prefers-color-scheme\s*:\s*dark\)/.test(css)) return "media";
|
|
2291
|
+
} catch (_) {}
|
|
2292
|
+
}
|
|
2293
|
+
return "unknown";
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2092
2296
|
function extractButtonUsage() {
|
|
2093
2297
|
if (!fs.existsSync(SRC_DIR)) return null;
|
|
2094
2298
|
const files = getAllTsxJsxInDir(SRC_DIR);
|
|
@@ -2147,6 +2351,50 @@ function extractButtonUsage() {
|
|
|
2147
2351
|
};
|
|
2148
2352
|
}
|
|
2149
2353
|
|
|
2354
|
+
/**
|
|
2355
|
+
* Phase D — CSS Modules Support v2.12.0
|
|
2356
|
+
* Reads .module.css files imported by a component and extracts design-relevant CSS properties.
|
|
2357
|
+
*/
|
|
2358
|
+
function extractCssModuleTokens(compFilePath) {
|
|
2359
|
+
if (!compFilePath || !fs.existsSync(compFilePath)) return null;
|
|
2360
|
+
let src;
|
|
2361
|
+
try { src = fs.readFileSync(compFilePath, "utf-8"); } catch (_) { return null; }
|
|
2362
|
+
// Find `import styles from './Foo.module.css'` or named imports
|
|
2363
|
+
const moduleImportRe = /import\s+(?:[\w{},\s]+\s+from\s+)?['"]([^'"]+\.module\.css)['"]/g;
|
|
2364
|
+
const dir = path.dirname(compFilePath);
|
|
2365
|
+
const classNames = [];
|
|
2366
|
+
const properties = {};
|
|
2367
|
+
let found = false;
|
|
2368
|
+
let m;
|
|
2369
|
+
while ((m = moduleImportRe.exec(src)) !== null) {
|
|
2370
|
+
const cssPath = path.resolve(dir, m[1]);
|
|
2371
|
+
if (!fs.existsSync(cssPath)) continue;
|
|
2372
|
+
let css;
|
|
2373
|
+
try { css = fs.readFileSync(cssPath, "utf-8"); } catch (_) { continue; }
|
|
2374
|
+
found = true;
|
|
2375
|
+
// Extract class names and design-relevant properties
|
|
2376
|
+
const classBlockRe = /\.(\w[\w-]*)\s*\{([^}]+)\}/g;
|
|
2377
|
+
let cb;
|
|
2378
|
+
while ((cb = classBlockRe.exec(css)) !== null) {
|
|
2379
|
+
const className = cb[1];
|
|
2380
|
+
if (!classNames.includes(className)) classNames.push(className);
|
|
2381
|
+
const block = cb[2];
|
|
2382
|
+
const DESIGN_PROPS = ["background","background-color","color","font-family","font-size","font-weight","padding","padding-top","padding-right","padding-bottom","padding-left","margin","border-radius","gap","border","border-color","border-width","box-shadow","opacity","width","height"];
|
|
2383
|
+
for (const prop of DESIGN_PROPS) {
|
|
2384
|
+
const re = new RegExp(`${prop}:\\s*([^;]+);`);
|
|
2385
|
+
const pm = block.match(re);
|
|
2386
|
+
if (pm) {
|
|
2387
|
+
if (!properties[prop]) properties[prop] = [];
|
|
2388
|
+
const val = pm[1].trim();
|
|
2389
|
+
if (!properties[prop].includes(val)) properties[prop].push(val);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (!found || classNames.length === 0) return null;
|
|
2395
|
+
return { classNames, properties };
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2150
2398
|
/**
|
|
2151
2399
|
* Scan all non-component source files and count how many times each variant value
|
|
2152
2400
|
* is explicitly used for each component (e.g. <Button variant="destructive">).
|
|
@@ -2247,7 +2495,11 @@ function scan() {
|
|
|
2247
2495
|
}
|
|
2248
2496
|
const tokens = extractTailwindTokens(content);
|
|
2249
2497
|
const isPageComponent = isComplexPageComponent(content);
|
|
2250
|
-
|
|
2498
|
+
const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) };
|
|
2499
|
+
// Phase D — CSS Module tokens
|
|
2500
|
+
const cssModuleTokens = extractCssModuleTokens(COMPONENTS_DIR ? path.join(COMPONENTS_DIR, rel) : null);
|
|
2501
|
+
if (cssModuleTokens) comp.cssModuleTokens = cssModuleTokens;
|
|
2502
|
+
results.push(comp);
|
|
2251
2503
|
}
|
|
2252
2504
|
if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
|
|
2253
2505
|
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
@@ -2271,6 +2523,8 @@ function scan() {
|
|
|
2271
2523
|
|
|
2272
2524
|
const foundations = extractFoundations();
|
|
2273
2525
|
foundations.icons = extractLucideIconsUsed(SRC_DIR);
|
|
2526
|
+
// Phase E — Dark mode strategy
|
|
2527
|
+
foundations.darkModeStrategy = detectDarkModeStrategy(PROJECT_ROOT);
|
|
2274
2528
|
foundations.brand = { assets: extractBrandAssets() };
|
|
2275
2529
|
const buttonUsage = extractButtonUsage();
|
|
2276
2530
|
if (buttonUsage) {
|
|
@@ -2280,6 +2534,10 @@ function scan() {
|
|
|
2280
2534
|
if (tokenUsage) {
|
|
2281
2535
|
foundations.tokenUsage = tokenUsage;
|
|
2282
2536
|
}
|
|
2537
|
+
// Phase A — Color Intelligence
|
|
2538
|
+
const colorTokenNames = Object.keys(foundations.colors || {}).filter(k => k !== "_dark");
|
|
2539
|
+
const colorUsage = extractColorUsage(colorTokenNames);
|
|
2540
|
+
if (colorUsage) foundations.colorUsage = colorUsage;
|
|
2283
2541
|
const componentSuggestions = extractComponentSuggestions();
|
|
2284
2542
|
const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
|
|
2285
2543
|
const output = {
|
|
@@ -1792,6 +1792,138 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
|
|
|
1792
1792
|
}
|
|
1793
1793
|
lines.push(`};`);
|
|
1794
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
// --- Design Tokens story (same block as generateStoryFile) ---
|
|
1797
|
+
{
|
|
1798
|
+
const compTokens = Array.isArray(comp.tokens) ? comp.tokens : [];
|
|
1799
|
+
const foundColors = FOUNDATIONS_DATA?.colors || {};
|
|
1800
|
+
if (compTokens.length >= 3) {
|
|
1801
|
+
const cleanTokens = compTokens.filter(t => !/:/.test(t));
|
|
1802
|
+
const colorRaw = cleanTokens.filter(t =>
|
|
1803
|
+
/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
|
|
1804
|
+
!/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
|
|
1805
|
+
);
|
|
1806
|
+
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
|
|
1807
|
+
const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
|
|
1808
|
+
const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
|
|
1809
|
+
const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
|
|
1810
|
+
const resolvedColors = colorRaw.map(token => {
|
|
1811
|
+
const m = token.match(/^(?:bg|text|border|ring|from|to|fill|stroke)-(.+)$/);
|
|
1812
|
+
const key = m ? m[1] : null;
|
|
1813
|
+
const baseKey = key ? key.replace(/\/[\d.]+$/, "") : null;
|
|
1814
|
+
const entry = key ? (foundColors[key] || (baseKey !== key ? foundColors[baseKey] : null)) : null;
|
|
1815
|
+
const isValidCssColor = (v) => /^#[0-9a-fA-F]{3,8}$/.test(v) || /^(rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\s*\(/.test(v) || v === 'transparent';
|
|
1816
|
+
const hex = entry?.hex && isValidCssColor(entry.hex) ? entry.hex : null;
|
|
1817
|
+
return { token, hex, label: baseKey || key };
|
|
1818
|
+
});
|
|
1819
|
+
const hasContent = resolvedColors.length > 0 || spacingRaw.length > 0 || typographyRaw.length > 0 || radiusRaw.length > 0 || animRaw.length > 0;
|
|
1820
|
+
if (hasContent) {
|
|
1821
|
+
lines.push("");
|
|
1822
|
+
lines.push(`export const Tokens: Story = {`);
|
|
1823
|
+
lines.push(` name: "Design Tokens",`);
|
|
1824
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
1825
|
+
lines.push(` render: () => {`);
|
|
1826
|
+
lines.push(` const colorTokens = ${JSON.stringify(resolvedColors)};`);
|
|
1827
|
+
lines.push(` const spacingTokens = ${JSON.stringify(spacingRaw)};`);
|
|
1828
|
+
lines.push(` const typographyTokens = ${JSON.stringify(typographyRaw)};`);
|
|
1829
|
+
lines.push(` const radiusTokens = ${JSON.stringify(radiusRaw)};`);
|
|
1830
|
+
lines.push(` const animationTokens = ${JSON.stringify(animRaw)};`);
|
|
1831
|
+
lines.push(` const chip = (label: string, bg: string, color: string) => (`);
|
|
1832
|
+
lines.push(` <span key={label} style={{ fontFamily: "monospace", fontSize: 11, background: bg, color, padding: "3px 9px", borderRadius: 5, border: \`1px solid \${bg === "#f9fafb" ? "#e5e7eb" : bg}\`, whiteSpace: "nowrap" as any }}>{label}</span>`);
|
|
1833
|
+
lines.push(` );`);
|
|
1834
|
+
lines.push(` const section = (title: string, children: any) => (`);
|
|
1835
|
+
lines.push(` <section style={{ marginBottom: 28 }}>`);
|
|
1836
|
+
lines.push(` <p style={{ margin: "0 0 10px", fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em" }}>{title}</p>`);
|
|
1837
|
+
lines.push(` {children}`);
|
|
1838
|
+
lines.push(` </section>`);
|
|
1839
|
+
lines.push(` );`);
|
|
1840
|
+
lines.push(` return (`);
|
|
1841
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh", width: "100%" }}>`);
|
|
1842
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>Design Tokens</h2>`);
|
|
1843
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 32px" }}>Tailwind utilities used in <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code> — resolved to project values.</p>`);
|
|
1844
|
+
lines.push(` {colorTokens.length > 0 && section("Color", (`);
|
|
1845
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>`);
|
|
1846
|
+
lines.push(` {colorTokens.map(({ token, hex, label }) => (`);
|
|
1847
|
+
lines.push(` <div key={token} style={{ display: "flex", alignItems: "center", gap: 7, padding: "7px 12px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#f9fafb" }}>`);
|
|
1848
|
+
lines.push(` {hex && <span style={{ display: "inline-block", width: 16, height: 16, borderRadius: 4, background: hex, border: "1px solid rgba(0,0,0,0.1)", flexShrink: 0 }} />}`);
|
|
1849
|
+
lines.push(` <code style={{ fontSize: 12, color: "#374151", fontWeight: 600 }}>{token}</code>`);
|
|
1850
|
+
lines.push(` {hex && <span style={{ fontSize: 11, color: "#9ca3af" }}>{hex}</span>}`);
|
|
1851
|
+
lines.push(` </div>`);
|
|
1852
|
+
lines.push(` ))}`);
|
|
1853
|
+
lines.push(` </div>`);
|
|
1854
|
+
lines.push(` ))}`);
|
|
1855
|
+
lines.push(` {spacingTokens.length > 0 && section("Spacing", (`);
|
|
1856
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{spacingTokens.map(t => chip(t, "#faf5ff", "#6d28d9"))}</div>`);
|
|
1857
|
+
lines.push(` ))}`);
|
|
1858
|
+
lines.push(` {typographyTokens.length > 0 && section("Typography", (`);
|
|
1859
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{typographyTokens.map(t => chip(t, "#fffbeb", "#92400e"))}</div>`);
|
|
1860
|
+
lines.push(` ))}`);
|
|
1861
|
+
lines.push(` {radiusTokens.length > 0 && section("Border Radius", (`);
|
|
1862
|
+
lines.push(` <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-end" }}>`);
|
|
1863
|
+
lines.push(` {radiusTokens.map(t => {`);
|
|
1864
|
+
lines.push(` const px = t === "rounded-none" ? 0 : t === "rounded-sm" ? 2 : t === "rounded" ? 4 : t === "rounded-md" ? 6 : t === "rounded-lg" ? 8 : t === "rounded-xl" ? 12 : t === "rounded-2xl" ? 16 : t === "rounded-3xl" ? 24 : t === "rounded-full" ? 9999 : 4;`);
|
|
1865
|
+
lines.push(` return (`);
|
|
1866
|
+
lines.push(` <div key={t} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>`);
|
|
1867
|
+
lines.push(` <div style={{ width: 44, height: 44, background: "#6366f1", borderRadius: px }} />`);
|
|
1868
|
+
lines.push(` <code style={{ fontSize: 10, color: "#6b7280" }}>{t}</code>`);
|
|
1869
|
+
lines.push(` </div>`);
|
|
1870
|
+
lines.push(` );`);
|
|
1871
|
+
lines.push(` })}`);
|
|
1872
|
+
lines.push(` </div>`);
|
|
1873
|
+
lines.push(` ))}`);
|
|
1874
|
+
lines.push(` {animationTokens.length > 0 && section("Motion", (`);
|
|
1875
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{animationTokens.map(t => chip(t, "#f0fdf4", "#166534"))}</div>`);
|
|
1876
|
+
lines.push(` ))}`);
|
|
1877
|
+
lines.push(` </div>`);
|
|
1878
|
+
lines.push(` );`);
|
|
1879
|
+
lines.push(` },`);
|
|
1880
|
+
lines.push(`};`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
// Phase D2 — CSS Module Classes section
|
|
1884
|
+
const cssModTokens = comp.cssModuleTokens || null;
|
|
1885
|
+
if (cssModTokens && Array.isArray(cssModTokens.classNames) && cssModTokens.classNames.length > 0) {
|
|
1886
|
+
lines.push("");
|
|
1887
|
+
lines.push(`export const CssModuleClasses: Story = {`);
|
|
1888
|
+
lines.push(` name: "CSS Module Classes",`);
|
|
1889
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
1890
|
+
lines.push(` render: () => {`);
|
|
1891
|
+
lines.push(` const classNames = ${JSON.stringify(cssModTokens.classNames)};`);
|
|
1892
|
+
lines.push(` const properties = ${JSON.stringify(cssModTokens.properties)};`);
|
|
1893
|
+
lines.push(` return (`);
|
|
1894
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
1895
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>CSS Module Classes</h2>`);
|
|
1896
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 24px" }}>Classes from <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.module.css</code> files imported by <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code></p>`);
|
|
1897
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 32 }}>`);
|
|
1898
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
1899
|
+
lines.push(` <span key={cls} style={{ fontFamily: "monospace", fontSize: 12, background: "#f0fdf4", color: "#166534", padding: "4px 10px", borderRadius: 6, border: "1px solid #bbf7d0" }}>source: CSS Module</span>`);
|
|
1900
|
+
lines.push(` ))}`);
|
|
1901
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
1902
|
+
lines.push(` <code key={cls} style={{ fontSize: 12, background: "#f9fafb", color: "#374151", padding: "4px 10px", borderRadius: 6, border: "1px solid #e5e7eb" }}>.{cls}</code>`);
|
|
1903
|
+
lines.push(` ))}`);
|
|
1904
|
+
lines.push(` </div>`);
|
|
1905
|
+
lines.push(` {Object.entries(properties).length > 0 && (`);
|
|
1906
|
+
lines.push(` <div>`);
|
|
1907
|
+
lines.push(` <p style={{ fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 12 }}>Extracted Properties</p>`);
|
|
1908
|
+
lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
|
|
1909
|
+
lines.push(` {Object.entries(properties).map(([prop, values]: [string, string[]]) => (`);
|
|
1910
|
+
lines.push(` <div key={prop} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#f9fafb" }}>`);
|
|
1911
|
+
lines.push(` <code style={{ fontSize: 11, color: "#6b7280", minWidth: 140, flexShrink: 0 }}>{prop}</code>`);
|
|
1912
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>`);
|
|
1913
|
+
lines.push(` {values.map((v: string) => <code key={v} style={{ fontSize: 11, background: "#fff", color: "#374151", padding: "1px 6px", borderRadius: 4, border: "1px solid #e5e7eb" }}>{v}</code>)}`);
|
|
1914
|
+
lines.push(` </div>`);
|
|
1915
|
+
lines.push(` </div>`);
|
|
1916
|
+
lines.push(` ))}`);
|
|
1917
|
+
lines.push(` </div>`);
|
|
1918
|
+
lines.push(` </div>`);
|
|
1919
|
+
lines.push(` )}`);
|
|
1920
|
+
lines.push(` </div>`);
|
|
1921
|
+
lines.push(` );`);
|
|
1922
|
+
lines.push(` },`);
|
|
1923
|
+
lines.push(`};`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1795
1927
|
return lines.join("\n");
|
|
1796
1928
|
}
|
|
1797
1929
|
|
|
@@ -2115,42 +2247,46 @@ function buildStoryFileContent(comp) {
|
|
|
2115
2247
|
// Component-specific stories for non-variant components (Input, Textarea, etc.)
|
|
2116
2248
|
// Skip buildSpecialStories when multi-dimension detection found CVA dimensions —
|
|
2117
2249
|
// generic detection produces better stories than hardcoded ones.
|
|
2250
|
+
let generatedSpecialStories = false;
|
|
2118
2251
|
if (!hasDimensions) {
|
|
2119
2252
|
const specialStories = buildSpecialStories(componentName, variants);
|
|
2120
2253
|
if (specialStories) {
|
|
2121
2254
|
lines.push(specialStories);
|
|
2122
|
-
|
|
2255
|
+
generatedSpecialStories = true;
|
|
2256
|
+
// Do NOT return here — fall through to Tokens + Usage stories below
|
|
2123
2257
|
}
|
|
2124
2258
|
}
|
|
2125
2259
|
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2260
|
+
if (!generatedSpecialStories) {
|
|
2261
|
+
// Profile-driven render + children — single source of truth via getStoryProfile()
|
|
2262
|
+
const useSafeWrapper = profile === "SAFE";
|
|
2263
|
+
const RenderTarget = useSafeWrapper ? "SafeWrapper" : "ComponentRef";
|
|
2264
|
+
const argsFallback = !useSafeWrapper && (componentName && RENDER_ARGS_FALLBACKS[componentName]) || "";
|
|
2265
|
+
const renderLine = buildProfileRenderLine(profile, RenderTarget, argsFallback);
|
|
2266
|
+
const childrenArgLine = buildProfileChildrenArgLine(profile);
|
|
2132
2267
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2268
|
+
if (hasDimensions) {
|
|
2269
|
+
const multiStories = buildMultiDimensionStories(
|
|
2270
|
+
variantMap, renderLine, childrenArgLine, defaultArgLines, componentName,
|
|
2271
|
+
comp.variantUsage || null
|
|
2272
|
+
);
|
|
2273
|
+
if (multiStories) {
|
|
2274
|
+
lines.push(multiStories);
|
|
2275
|
+
}
|
|
2276
|
+
} else {
|
|
2277
|
+
// No dimensions detected: single Default story
|
|
2278
|
+
lines.push(`export const Default: Story = {`);
|
|
2279
|
+
lines.push(renderLine);
|
|
2280
|
+
const storyArgLines = [];
|
|
2281
|
+
if (childrenArgLine(componentName)) storyArgLines.push(childrenArgLine(componentName));
|
|
2282
|
+
for (const line of defaultArgLines) storyArgLines.push(line);
|
|
2283
|
+
if (storyArgLines.length > 0) {
|
|
2284
|
+
lines.push(` args: {`);
|
|
2285
|
+
for (const line of storyArgLines) lines.push(line);
|
|
2286
|
+
lines.push(` },`);
|
|
2287
|
+
}
|
|
2288
|
+
lines.push(`};`);
|
|
2152
2289
|
}
|
|
2153
|
-
lines.push(`};`);
|
|
2154
2290
|
}
|
|
2155
2291
|
|
|
2156
2292
|
// --- Project-specific usage stories ---
|
|
@@ -2190,7 +2326,7 @@ function buildStoryFileContent(comp) {
|
|
|
2190
2326
|
/^(bg|text|border|ring|from|to|fill|stroke)-/.test(t) &&
|
|
2191
2327
|
!/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\d)/.test(t)
|
|
2192
2328
|
);
|
|
2193
|
-
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]
|
|
2329
|
+
const spacingRaw = cleanTokens.filter(t => /^(p[xylrbt]?-|m[xylrbt]?-|gap|space-[xy]|w-|h-|min-[wh]|max-[wh]|size-)/.test(t));
|
|
2194
2330
|
const typographyRaw = cleanTokens.filter(t => /^(text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-)/.test(t));
|
|
2195
2331
|
const radiusRaw = cleanTokens.filter(t => /^rounded/.test(t));
|
|
2196
2332
|
const animRaw = cleanTokens.filter(t => /^(transition|duration|animate|ease|delay)-/.test(t));
|
|
@@ -2271,6 +2407,48 @@ function buildStoryFileContent(comp) {
|
|
|
2271
2407
|
lines.push(`};`);
|
|
2272
2408
|
}
|
|
2273
2409
|
}
|
|
2410
|
+
// Phase D2 — CSS Module Classes section
|
|
2411
|
+
const cssModTokens = comp.cssModuleTokens || null;
|
|
2412
|
+
if (cssModTokens && Array.isArray(cssModTokens.classNames) && cssModTokens.classNames.length > 0) {
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
lines.push(`export const CssModuleClasses: Story = {`);
|
|
2415
|
+
lines.push(` name: "CSS Module Classes",`);
|
|
2416
|
+
lines.push(` parameters: { layout: "fullscreen" },`);
|
|
2417
|
+
lines.push(` render: () => {`);
|
|
2418
|
+
lines.push(` const classNames = ${JSON.stringify(cssModTokens.classNames)};`);
|
|
2419
|
+
lines.push(` const properties = ${JSON.stringify(cssModTokens.properties)};`);
|
|
2420
|
+
lines.push(` return (`);
|
|
2421
|
+
lines.push(` <div style={{ padding: 40, background: "#fff", fontFamily: "system-ui,sans-serif", color: "#111", minHeight: "100vh" }}>`);
|
|
2422
|
+
lines.push(` <h2 style={{ fontSize: 22, fontWeight: 700, margin: "0 0 6px" }}>CSS Module Classes</h2>`);
|
|
2423
|
+
lines.push(` <p style={{ fontSize: 13, color: "#6b7280", margin: "0 0 24px" }}>Classes from <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.module.css</code> files imported by <code style={{ background: "#f3f4f6", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>${componentName}</code></p>`);
|
|
2424
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 32 }}>`);
|
|
2425
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
2426
|
+
lines.push(` <span key={cls} style={{ fontFamily: "monospace", fontSize: 12, background: "#f0fdf4", color: "#166534", padding: "4px 10px", borderRadius: 6, border: "1px solid #bbf7d0" }}>source: CSS Module</span>`);
|
|
2427
|
+
lines.push(` ))}`);
|
|
2428
|
+
lines.push(` {classNames.map((cls: string) => (`);
|
|
2429
|
+
lines.push(` <code key={cls} style={{ fontSize: 12, background: "#f9fafb", color: "#374151", padding: "4px 10px", borderRadius: 6, border: "1px solid #e5e7eb" }}>.{cls}</code>`);
|
|
2430
|
+
lines.push(` ))}`);
|
|
2431
|
+
lines.push(` </div>`);
|
|
2432
|
+
lines.push(` {Object.entries(properties).length > 0 && (`);
|
|
2433
|
+
lines.push(` <div>`);
|
|
2434
|
+
lines.push(` <p style={{ fontSize: 11, fontWeight: 700, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 12 }}>Extracted Properties</p>`);
|
|
2435
|
+
lines.push(` <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>`);
|
|
2436
|
+
lines.push(` {Object.entries(properties).map(([prop, values]: [string, string[]]) => (`);
|
|
2437
|
+
lines.push(` <div key={prop} style={{ display: "flex", gap: 8, alignItems: "flex-start", padding: "8px 12px", border: "1px solid #e5e7eb", borderRadius: 6, background: "#f9fafb" }}>`);
|
|
2438
|
+
lines.push(` <code style={{ fontSize: 11, color: "#6b7280", minWidth: 140, flexShrink: 0 }}>{prop}</code>`);
|
|
2439
|
+
lines.push(` <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>`);
|
|
2440
|
+
lines.push(` {values.map((v: string) => <code key={v} style={{ fontSize: 11, background: "#fff", color: "#374151", padding: "1px 6px", borderRadius: 4, border: "1px solid #e5e7eb" }}>{v}</code>)}`);
|
|
2441
|
+
lines.push(` </div>`);
|
|
2442
|
+
lines.push(` </div>`);
|
|
2443
|
+
lines.push(` ))}`);
|
|
2444
|
+
lines.push(` </div>`);
|
|
2445
|
+
lines.push(` </div>`);
|
|
2446
|
+
lines.push(` )}`);
|
|
2447
|
+
lines.push(` </div>`);
|
|
2448
|
+
lines.push(` );`);
|
|
2449
|
+
lines.push(` },`);
|
|
2450
|
+
lines.push(`};`);
|
|
2451
|
+
}
|
|
2274
2452
|
}
|
|
2275
2453
|
|
|
2276
2454
|
// --- Usage story ---
|
|
@@ -2502,7 +2680,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2502
2680
|
const hex = c?.hex || c?.value || "";
|
|
2503
2681
|
const u = colorUsage[name] || null;
|
|
2504
2682
|
return { name, hex, cssVar: `--${name}`, description: DESCRIPTIONS[name] || "", usage: u };
|
|
2505
|
-
})
|
|
2683
|
+
})
|
|
2684
|
+
.sort((a, b) => (b.usage?.total || 0) - (a.usage?.total || 0));
|
|
2506
2685
|
return { key: group.key, label: group.label, colors };
|
|
2507
2686
|
}).filter((g) => g.colors.length > 0);
|
|
2508
2687
|
|
|
@@ -2566,6 +2745,11 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2566
2745
|
" ×{usage.total}",
|
|
2567
2746
|
" </span>",
|
|
2568
2747
|
" )}",
|
|
2748
|
+
" {usage && usage.dark > 0 && (",
|
|
2749
|
+
" <span style={{ position: \"absolute\", top: 8, left: 8, fontSize: 9, fontWeight: 700, padding: \"2px 6px\", borderRadius: 99, background: \"rgba(109,40,217,0.85)\", color: \"#fff\", backdropFilter: \"blur(2px)\" }}>",
|
|
2750
|
+
" dark ×{usage.dark}",
|
|
2751
|
+
" </span>",
|
|
2752
|
+
" )}",
|
|
2569
2753
|
" </div>",
|
|
2570
2754
|
" {/* Info */}",
|
|
2571
2755
|
" <div style={{ padding: \"12px 14px\" }}>",
|
|
@@ -2617,6 +2801,74 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2617
2801
|
" </div>",
|
|
2618
2802
|
" ),",
|
|
2619
2803
|
"};",
|
|
2804
|
+
"",
|
|
2805
|
+
"export const UsedOnly: Story = {",
|
|
2806
|
+
" name: \"Used Colors\",",
|
|
2807
|
+
" render: () => {",
|
|
2808
|
+
` const usedGroups = colorGroups.map(g => ({ ...g, colors: g.colors.filter(c => c.usage && c.usage.total > 0) })).filter(g => g.colors.length > 0);`,
|
|
2809
|
+
" return (",
|
|
2810
|
+
" <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
|
|
2811
|
+
" <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Used Colors</h2>",
|
|
2812
|
+
" <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 40px\" }}>Only colors actually used in source — sorted by usage frequency</p>",
|
|
2813
|
+
" {usedGroups.map(group => (",
|
|
2814
|
+
" <div key={group.key} style={{ marginBottom: 40 }}>",
|
|
2815
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 16px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>{group.label}</h3>",
|
|
2816
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 12 }}>",
|
|
2817
|
+
" {group.colors.map(({ name, hex, usage }) => (",
|
|
2818
|
+
" <div key={name} style={{ display: \"flex\", flexDirection: \"column\", gap: 4, width: 80 }}>",
|
|
2819
|
+
" <div style={{ width: 80, height: 48, borderRadius: 8, background: hex, border: \"1px solid #e5e7eb\" }} />",
|
|
2820
|
+
" <code style={{ fontSize: 10, color: \"#374151\", wordBreak: \"break-all\" as any }}>{name}</code>",
|
|
2821
|
+
" <span style={{ fontSize: 10, color: \"#6b7280\" }}>×{usage?.total}</span>",
|
|
2822
|
+
" </div>",
|
|
2823
|
+
" ))}",
|
|
2824
|
+
" </div>",
|
|
2825
|
+
" </div>",
|
|
2826
|
+
" ))}",
|
|
2827
|
+
" {usedGroups.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
|
|
2828
|
+
" </div>",
|
|
2829
|
+
" );",
|
|
2830
|
+
" },",
|
|
2831
|
+
"};",
|
|
2832
|
+
"",
|
|
2833
|
+
"export const ColorUsageByComponent: Story = {",
|
|
2834
|
+
" name: \"Component → Color\",",
|
|
2835
|
+
" render: () => {",
|
|
2836
|
+
" // Build reverse map: component → colors used",
|
|
2837
|
+
" const compMap: Record<string, {name:string;hex:string;total:number}[]> = {};",
|
|
2838
|
+
" for (const group of colorGroups) {",
|
|
2839
|
+
" for (const { name, hex, usage } of group.colors) {",
|
|
2840
|
+
" if (!usage || usage.total === 0) continue;",
|
|
2841
|
+
" for (const comp of (usage.topFiles || [])) {",
|
|
2842
|
+
" if (!compMap[comp]) compMap[comp] = [];",
|
|
2843
|
+
" if (!compMap[comp].find(c => c.name === name)) compMap[comp].push({ name, hex, total: usage.total });",
|
|
2844
|
+
" }",
|
|
2845
|
+
" }",
|
|
2846
|
+
" }",
|
|
2847
|
+
" const compEntries = Object.entries(compMap).sort((a,b) => b[1].length - a[1].length).slice(0, 30);",
|
|
2848
|
+
" return (",
|
|
2849
|
+
" <div style={{ fontFamily: \"system-ui,sans-serif\", padding: 32, background: \"#fff\", minHeight: \"100vh\", width: \"100%\", color: \"#111\" }}>",
|
|
2850
|
+
" <h2 style={{ fontSize: 20, fontWeight: 700, margin: \"0 0 4px\" }}>Component → Color</h2>",
|
|
2851
|
+
" <p style={{ fontSize: 13, color: \"#888\", margin: \"0 0 32px\" }}>Which components use which color tokens</p>",
|
|
2852
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 16 }}>",
|
|
2853
|
+
" {compEntries.map(([comp, colors]) => (",
|
|
2854
|
+
" <div key={comp} style={{ padding: \"14px 18px\", border: \"1px solid #e5e7eb\", borderRadius: 10, background: \"#f9fafb\" }}>",
|
|
2855
|
+
" <div style={{ fontSize: 13, fontWeight: 700, color: \"#111\", marginBottom: 10, fontFamily: \"monospace\" }}>{comp}</div>",
|
|
2856
|
+
" <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6 }}>",
|
|
2857
|
+
" {colors.map(({ name, hex }) => (",
|
|
2858
|
+
" <div key={name} style={{ display: \"flex\", alignItems: \"center\", gap: 5, background: \"#fff\", border: \"1px solid #e5e7eb\", borderRadius: 6, padding: \"3px 9px\" }}>",
|
|
2859
|
+
" <span style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 2, background: hex, border: \"1px solid rgba(0,0,0,0.1)\", flexShrink: 0 }} />",
|
|
2860
|
+
" <code style={{ fontSize: 11, color: \"#374151\" }}>{name}</code>",
|
|
2861
|
+
" </div>",
|
|
2862
|
+
" ))}",
|
|
2863
|
+
" </div>",
|
|
2864
|
+
" </div>",
|
|
2865
|
+
" ))}",
|
|
2866
|
+
" {compEntries.length === 0 && <p style={{ color: \"#9ca3af\", fontSize: 14 }}>No colorUsage data — run scan.mjs to generate.</p>}",
|
|
2867
|
+
" </div>",
|
|
2868
|
+
" </div>",
|
|
2869
|
+
" );",
|
|
2870
|
+
" },",
|
|
2871
|
+
"};",
|
|
2620
2872
|
].join("\n");
|
|
2621
2873
|
|
|
2622
2874
|
fs.writeFileSync(path.join(foundationsDir, "Colors.stories.tsx"), colorsContent, "utf-8");
|
|
@@ -2708,6 +2960,9 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2708
2960
|
? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
|
|
2709
2961
|
const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
|
|
2710
2962
|
|
|
2963
|
+
const fontFaces = Array.isArray(typo.fontFaces) ? typo.fontFaces : [];
|
|
2964
|
+
const nextFonts = Array.isArray(typo.nextFonts) ? typo.nextFonts : [];
|
|
2965
|
+
|
|
2711
2966
|
const typoContent = [
|
|
2712
2967
|
"import React from \"react\";",
|
|
2713
2968
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
@@ -2723,6 +2978,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2723
2978
|
`const weightRows: { token: string; value: string }[] = ${JSON.stringify(weightRows)};`,
|
|
2724
2979
|
`const weightRowsIsDefault: boolean = ${weightRowsIsDefault};`,
|
|
2725
2980
|
`const sansFamily = ${JSON.stringify(sansFamily)};`,
|
|
2981
|
+
`const fontFamilyRows: { token: string; value: string }[] = ${JSON.stringify(familyRows)};`,
|
|
2982
|
+
`const fontSourceRows: { type: string; family: string; weight: string | null; isVariable: boolean; variable: string | null }[] = ${JSON.stringify([...fontFaces.map(f => ({ type: "css", family: f.family, weight: f.weight, isVariable: f.isVariable, variable: null })), ...nextFonts.map(f => ({ type: "next", family: f.family, weight: null, isVariable: false, variable: f.variable }))])};`,
|
|
2726
2983
|
"",
|
|
2727
2984
|
"export const Default: Story = {",
|
|
2728
2985
|
" render: () => (",
|
|
@@ -2801,6 +3058,26 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2801
3058
|
" </>",
|
|
2802
3059
|
" )}",
|
|
2803
3060
|
"",
|
|
3061
|
+
" {/* Phase B4 — Letter Specimens */}",
|
|
3062
|
+
" {fontFamilyRows.length > 0 && (",
|
|
3063
|
+
" <div style={{ marginBottom: 48 }}>",
|
|
3064
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 24px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>Letter Specimens</h3>",
|
|
3065
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 32 }}>",
|
|
3066
|
+
" {fontFamilyRows.map(({ token, value }) => {",
|
|
3067
|
+
" const family = value.split(',')[0].trim().replace(/['\\\"/]/g, '');",
|
|
3068
|
+
" return (",
|
|
3069
|
+
" <div key={token} style={{ padding: 24, border: \"1px solid #e5e7eb\", borderRadius: 12, background: \"#fafafa\" }}>",
|
|
3070
|
+
" <div style={{ fontSize: 11, fontWeight: 600, color: \"#9ca3af\", marginBottom: 12, textTransform: \"uppercase\", letterSpacing: \"0.08em\", fontFamily: \"system-ui\" }}>{token} — {value.length > 50 ? value.slice(0,50)+'…' : value}</div>",
|
|
3071
|
+
" <div style={{ fontFamily: value, fontSize: 48, fontWeight: 700, color: \"#111\", lineHeight: 1.1, marginBottom: 8 }}>Aa Bb Cc</div>",
|
|
3072
|
+
" <div style={{ fontFamily: value, fontSize: 18, color: \"#374151\", lineHeight: 1.6, marginBottom: 8 }}>The quick brown fox jumps over the lazy dog</div>",
|
|
3073
|
+
" <div style={{ fontFamily: value, fontSize: 14, color: \"#6b7280\", letterSpacing: \"0.1em\" }}>0123456789 ! @ # $ %</div>",
|
|
3074
|
+
" </div>",
|
|
3075
|
+
" );",
|
|
3076
|
+
" })}",
|
|
3077
|
+
" </div>",
|
|
3078
|
+
" </div>",
|
|
3079
|
+
" )}",
|
|
3080
|
+
"",
|
|
2804
3081
|
" {/* ── FONT WEIGHTS ── */}",
|
|
2805
3082
|
" {weightRowsIsDefault && <p style={{ fontSize: 12, color: \"#92400e\", background: \"#fef3c7\", border: \"1px solid #fde68a\", borderRadius: 6, padding: \"6px 12px\", marginBottom: 12, display: \"inline-block\" }}>ℹ️ Tailwind defaults — no custom font weights found in this project</p>}",
|
|
2806
3083
|
" {weightRows.length > 0 && (",
|
|
@@ -2819,6 +3096,21 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2819
3096
|
" </div>",
|
|
2820
3097
|
" </>",
|
|
2821
3098
|
" )}",
|
|
3099
|
+
" {fontSourceRows.length > 0 && (",
|
|
3100
|
+
" <div style={{ marginBottom: 48 }}>",
|
|
3101
|
+
" <h3 style={{ fontSize: 14, fontWeight: 700, textTransform: \"uppercase\", letterSpacing: \"0.08em\", color: \"#6b7280\", margin: \"0 0 16px\", borderBottom: \"1px solid #e5e7eb\", paddingBottom: 8 }}>Font Sources</h3>",
|
|
3102
|
+
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>",
|
|
3103
|
+
" {fontSourceRows.map((f, i) => (",
|
|
3104
|
+
" <div key={i} style={{ display: \"flex\", alignItems: \"center\", gap: 12, padding: \"10px 16px\", border: \"1px solid #e5e7eb\", borderRadius: 8, background: \"#f9fafb\" }}>",
|
|
3105
|
+
" <span style={{ fontSize: 10, fontWeight: 700, padding: \"2px 7px\", borderRadius: 4, background: f.type === 'next' ? '#dbeafe' : '#d1fae5', color: f.type === 'next' ? '#1e40af' : '#065f46' }}>{f.type === 'next' ? 'next/font' : '@font-face'}</span>",
|
|
3106
|
+
" <code style={{ fontSize: 12, fontWeight: 600, color: \"#111\" }}>{f.family}</code>",
|
|
3107
|
+
" {f.isVariable && <span style={{ fontSize: 10, padding: \"2px 7px\", borderRadius: 4, background: \"#fef9c3\", color: \"#854d0e\" }}>variable {f.weight}</span>}",
|
|
3108
|
+
" {f.variable && <code style={{ fontSize: 10, color: \"#9ca3af\" }}>{f.variable}</code>}",
|
|
3109
|
+
" </div>",
|
|
3110
|
+
" ))}",
|
|
3111
|
+
" </div>",
|
|
3112
|
+
" </div>",
|
|
3113
|
+
" )}",
|
|
2822
3114
|
" </div>",
|
|
2823
3115
|
" ),",
|
|
2824
3116
|
"};",
|