vibe-design-system 2.8.82 → 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 +241 -2
- package/vds-core-template/story-generator.mjs +199 -1
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,6 +1481,22 @@ function extractFoundations() {
|
|
|
1429
1481
|
}
|
|
1430
1482
|
}
|
|
1431
1483
|
|
|
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
|
+
|
|
1432
1500
|
// body { font-family } — Tailwind v4 sets font on html/:host, NOT body
|
|
1433
1501
|
// Use [^}]* to stay within block boundaries ([\s\S]*? crosses blocks → mono font false-positive)
|
|
1434
1502
|
const bodyMatch =
|
|
@@ -1975,6 +2043,41 @@ function extractFoundations() {
|
|
|
1975
2043
|
if (arbFonts.size > 0) typography.arbitraryFonts = Array.from(arbFonts);
|
|
1976
2044
|
}
|
|
1977
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
|
+
|
|
1978
2081
|
return {
|
|
1979
2082
|
colors: foundationsColors,
|
|
1980
2083
|
typography,
|
|
@@ -2108,6 +2211,88 @@ function extractTokenUsage() {
|
|
|
2108
2211
|
};
|
|
2109
2212
|
}
|
|
2110
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
|
+
|
|
2111
2296
|
function extractButtonUsage() {
|
|
2112
2297
|
if (!fs.existsSync(SRC_DIR)) return null;
|
|
2113
2298
|
const files = getAllTsxJsxInDir(SRC_DIR);
|
|
@@ -2166,6 +2351,50 @@ function extractButtonUsage() {
|
|
|
2166
2351
|
};
|
|
2167
2352
|
}
|
|
2168
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
|
+
|
|
2169
2398
|
/**
|
|
2170
2399
|
* Scan all non-component source files and count how many times each variant value
|
|
2171
2400
|
* is explicitly used for each component (e.g. <Button variant="destructive">).
|
|
@@ -2266,7 +2495,11 @@ function scan() {
|
|
|
2266
2495
|
}
|
|
2267
2496
|
const tokens = extractTailwindTokens(content);
|
|
2268
2497
|
const isPageComponent = isComplexPageComponent(content);
|
|
2269
|
-
|
|
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);
|
|
2270
2503
|
}
|
|
2271
2504
|
if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
|
|
2272
2505
|
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
@@ -2290,6 +2523,8 @@ function scan() {
|
|
|
2290
2523
|
|
|
2291
2524
|
const foundations = extractFoundations();
|
|
2292
2525
|
foundations.icons = extractLucideIconsUsed(SRC_DIR);
|
|
2526
|
+
// Phase E — Dark mode strategy
|
|
2527
|
+
foundations.darkModeStrategy = detectDarkModeStrategy(PROJECT_ROOT);
|
|
2293
2528
|
foundations.brand = { assets: extractBrandAssets() };
|
|
2294
2529
|
const buttonUsage = extractButtonUsage();
|
|
2295
2530
|
if (buttonUsage) {
|
|
@@ -2299,6 +2534,10 @@ function scan() {
|
|
|
2299
2534
|
if (tokenUsage) {
|
|
2300
2535
|
foundations.tokenUsage = tokenUsage;
|
|
2301
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;
|
|
2302
2541
|
const componentSuggestions = extractComponentSuggestions();
|
|
2303
2542
|
const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
|
|
2304
2543
|
const output = {
|
|
@@ -1880,6 +1880,48 @@ function buildRecipeStoryContent(comp, componentName, importPath, title, source,
|
|
|
1880
1880
|
lines.push(`};`);
|
|
1881
1881
|
}
|
|
1882
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
|
+
}
|
|
1883
1925
|
}
|
|
1884
1926
|
|
|
1885
1927
|
return lines.join("\n");
|
|
@@ -2365,6 +2407,48 @@ function buildStoryFileContent(comp) {
|
|
|
2365
2407
|
lines.push(`};`);
|
|
2366
2408
|
}
|
|
2367
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
|
+
}
|
|
2368
2452
|
}
|
|
2369
2453
|
|
|
2370
2454
|
// --- Usage story ---
|
|
@@ -2596,7 +2680,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2596
2680
|
const hex = c?.hex || c?.value || "";
|
|
2597
2681
|
const u = colorUsage[name] || null;
|
|
2598
2682
|
return { name, hex, cssVar: `--${name}`, description: DESCRIPTIONS[name] || "", usage: u };
|
|
2599
|
-
})
|
|
2683
|
+
})
|
|
2684
|
+
.sort((a, b) => (b.usage?.total || 0) - (a.usage?.total || 0));
|
|
2600
2685
|
return { key: group.key, label: group.label, colors };
|
|
2601
2686
|
}).filter((g) => g.colors.length > 0);
|
|
2602
2687
|
|
|
@@ -2660,6 +2745,11 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2660
2745
|
" ×{usage.total}",
|
|
2661
2746
|
" </span>",
|
|
2662
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
|
+
" )}",
|
|
2663
2753
|
" </div>",
|
|
2664
2754
|
" {/* Info */}",
|
|
2665
2755
|
" <div style={{ padding: \"12px 14px\" }}>",
|
|
@@ -2711,6 +2801,74 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2711
2801
|
" </div>",
|
|
2712
2802
|
" ),",
|
|
2713
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
|
+
"};",
|
|
2714
2872
|
].join("\n");
|
|
2715
2873
|
|
|
2716
2874
|
fs.writeFileSync(path.join(foundationsDir, "Colors.stories.tsx"), colorsContent, "utf-8");
|
|
@@ -2802,6 +2960,9 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2802
2960
|
? `'${typo.arbitraryFonts[0]}', sans-serif` : null;
|
|
2803
2961
|
const sansFamily = typo.fontSans || typo.body || firstArbitraryFont || typo.tailwindSans || "system-ui, sans-serif";
|
|
2804
2962
|
|
|
2963
|
+
const fontFaces = Array.isArray(typo.fontFaces) ? typo.fontFaces : [];
|
|
2964
|
+
const nextFonts = Array.isArray(typo.nextFonts) ? typo.nextFonts : [];
|
|
2965
|
+
|
|
2805
2966
|
const typoContent = [
|
|
2806
2967
|
"import React from \"react\";",
|
|
2807
2968
|
"import type { Meta, StoryObj } from \"@storybook/react\";",
|
|
@@ -2817,6 +2978,8 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2817
2978
|
`const weightRows: { token: string; value: string }[] = ${JSON.stringify(weightRows)};`,
|
|
2818
2979
|
`const weightRowsIsDefault: boolean = ${weightRowsIsDefault};`,
|
|
2819
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 }))])};`,
|
|
2820
2983
|
"",
|
|
2821
2984
|
"export const Default: Story = {",
|
|
2822
2985
|
" render: () => (",
|
|
@@ -2895,6 +3058,26 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2895
3058
|
" </>",
|
|
2896
3059
|
" )}",
|
|
2897
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
|
+
"",
|
|
2898
3081
|
" {/* ── FONT WEIGHTS ── */}",
|
|
2899
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>}",
|
|
2900
3083
|
" {weightRows.length > 0 && (",
|
|
@@ -2913,6 +3096,21 @@ function writeFoundationsStories(foundations, components) {
|
|
|
2913
3096
|
" </div>",
|
|
2914
3097
|
" </>",
|
|
2915
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
|
+
" )}",
|
|
2916
3114
|
" </div>",
|
|
2917
3115
|
" ),",
|
|
2918
3116
|
"};",
|