design-system-enforcer 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -60,6 +60,7 @@ interface RulesConfig {
60
60
  "table-windowed-pagination"?: RuleSeverity;
61
61
  "tabs-component-required"?: RuleSeverity;
62
62
  "component-inventory-active"?: RuleSeverity;
63
+ "no-wrapping-compact-elements"?: RuleSeverity;
63
64
  }
64
65
  interface DseConfig {
65
66
  preset?: "berserk" | "recommended" | "minimal" | "python";
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ interface RulesConfig {
60
60
  "table-windowed-pagination"?: RuleSeverity;
61
61
  "tabs-component-required"?: RuleSeverity;
62
62
  "component-inventory-active"?: RuleSeverity;
63
+ "no-wrapping-compact-elements"?: RuleSeverity;
63
64
  }
64
65
  interface DseConfig {
65
66
  preset?: "berserk" | "recommended" | "minimal" | "python";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config/loader.ts
4
- import fs from "fs";
5
- import path from "path";
4
+ import fs3 from "fs";
5
+ import path3 from "path";
6
6
  import { pathToFileURL } from "url";
7
7
 
8
8
  // src/config/presets/berserk.ts
@@ -91,6 +91,9 @@ var berserkPreset = {
91
91
  disallowRawHtml: {
92
92
  button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
93
93
  select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
94
+ textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
95
+ progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
96
+ dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true },
94
97
  table: { replaceWith: "Table", importFrom: "@/components/ui/table", checkRawElement: true },
95
98
  thead: { replaceWith: "TableHeader", importFrom: "@/components/ui/table", checkRawElement: true },
96
99
  tbody: { replaceWith: "TableBody", importFrom: "@/components/ui/table", checkRawElement: true },
@@ -142,7 +145,8 @@ var berserkPreset = {
142
145
  "table-no-horizontal-stacking": "error",
143
146
  "table-windowed-pagination": "error",
144
147
  "tabs-component-required": "error",
145
- "component-inventory-active": "error"
148
+ "component-inventory-active": "error",
149
+ "no-wrapping-compact-elements": "error"
146
150
  }
147
151
  };
148
152
 
@@ -179,7 +183,10 @@ var defaultPreset = {
179
183
  components: {
180
184
  disallowRawHtml: {
181
185
  button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
182
- select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true }
186
+ select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
187
+ textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
188
+ progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
189
+ dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true }
183
190
  },
184
191
  disallowPseudoChips: true
185
192
  },
@@ -190,7 +197,8 @@ var defaultPreset = {
190
197
  "no-pseudo-chips": "warn",
191
198
  "no-chip-colored-buttons": "error",
192
199
  "no-text-arrows": "error",
193
- "no-backdrop-blur": "warn"
200
+ "no-backdrop-blur": "warn",
201
+ "no-wrapping-compact-elements": "warn"
194
202
  }
195
203
  };
196
204
 
@@ -254,6 +262,221 @@ var pythonPreset = {
254
262
  }
255
263
  };
256
264
 
265
+ // src/detector/index.ts
266
+ import fs2 from "fs";
267
+ import path2 from "path";
268
+
269
+ // src/detector/ignores.ts
270
+ import fs from "fs";
271
+ import path from "path";
272
+ var DEFAULT_IGNORES = [
273
+ "**/node_modules/**",
274
+ "**/.next/**",
275
+ "**/dist/**",
276
+ "**/build/**",
277
+ "**/out/**",
278
+ "**/.turbo/**",
279
+ "**/.git/**",
280
+ "**/coverage/**",
281
+ "**/.venv/**",
282
+ "**/venv/**",
283
+ "**/env/**",
284
+ "**/__pycache__/**",
285
+ "**/.pytest_cache/**",
286
+ "**/*.pyc",
287
+ // Non-UI code
288
+ "**/api/**",
289
+ "**/*.test.*",
290
+ "**/*.spec.*",
291
+ "**/__tests__/**",
292
+ "**/*.stories.*",
293
+ "**/generated/**"
294
+ ];
295
+ function getSmartIgnores(cwd) {
296
+ const result = new Set(DEFAULT_IGNORES);
297
+ const gitignorePath = path.resolve(cwd, ".gitignore");
298
+ if (fs.existsSync(gitignorePath)) {
299
+ try {
300
+ const content = fs.readFileSync(gitignorePath, "utf-8");
301
+ const lines = content.split(/\r?\n/);
302
+ for (let line of lines) {
303
+ line = line.trim();
304
+ if (!line || line.startsWith("#")) continue;
305
+ if (line.startsWith("/")) {
306
+ line = line.substring(1);
307
+ }
308
+ if (line.endsWith("/")) {
309
+ line = line.slice(0, -1);
310
+ }
311
+ if (line) {
312
+ result.add(`**/${line}/**`);
313
+ result.add(`**/${line}`);
314
+ }
315
+ }
316
+ } catch {
317
+ }
318
+ }
319
+ return Array.from(result);
320
+ }
321
+
322
+ // src/detector/index.ts
323
+ function safeReadJson(filePath) {
324
+ try {
325
+ if (fs2.existsSync(filePath)) {
326
+ const raw = fs2.readFileSync(filePath, "utf-8");
327
+ return JSON.parse(raw);
328
+ }
329
+ } catch {
330
+ }
331
+ return null;
332
+ }
333
+ function detectProject(cwd = process.cwd()) {
334
+ const pkg = safeReadJson(path2.resolve(cwd, "package.json"));
335
+ const deps = { ...pkg?.dependencies || {}, ...pkg?.devDependencies || {} };
336
+ const hasRequirements = fs2.existsSync(path2.resolve(cwd, "requirements.txt"));
337
+ const hasPyproject = fs2.existsSync(path2.resolve(cwd, "pyproject.toml"));
338
+ const hasPython = hasRequirements || hasPyproject || fs2.existsSync(path2.resolve(cwd, "setup.py"));
339
+ let isStreamlit = false;
340
+ if (hasPython) {
341
+ if (hasRequirements) {
342
+ try {
343
+ const reqContent = fs2.readFileSync(path2.resolve(cwd, "requirements.txt"), "utf-8");
344
+ if (reqContent.toLowerCase().includes("streamlit")) {
345
+ isStreamlit = true;
346
+ }
347
+ } catch {
348
+ }
349
+ }
350
+ }
351
+ const isPnpmMonorepo = fs2.existsSync(path2.resolve(cwd, "pnpm-workspace.yaml"));
352
+ const isTurbo = fs2.existsSync(path2.resolve(cwd, "turbo.json"));
353
+ let monorepoWorkspaces = [];
354
+ if (Array.isArray(pkg?.workspaces)) {
355
+ monorepoWorkspaces = pkg.workspaces;
356
+ } else if (pkg?.workspaces?.packages) {
357
+ monorepoWorkspaces = pkg.workspaces.packages;
358
+ }
359
+ const isMonorepo = isPnpmMonorepo || isTurbo || monorepoWorkspaces.length > 0;
360
+ let framework = "unknown";
361
+ if (deps["next"]) {
362
+ framework = "nextjs";
363
+ } else if (deps["vite"]) {
364
+ framework = "vite";
365
+ } else if (deps["@remix-run/react"]) {
366
+ framework = "remix";
367
+ } else if (deps["astro"]) {
368
+ framework = "astro";
369
+ } else if (deps["react-scripts"]) {
370
+ framework = "react-cra";
371
+ } else if (isStreamlit) {
372
+ framework = "streamlit";
373
+ } else if (hasPython) {
374
+ framework = "python-general";
375
+ }
376
+ const libraries = [];
377
+ const hasShadcnConfig = fs2.existsSync(path2.resolve(cwd, "components.json"));
378
+ if (hasShadcnConfig || deps["@radix-ui/react-slot"]) {
379
+ libraries.push("shadcn");
380
+ }
381
+ if (Object.keys(deps).some((d) => d.startsWith("@radix-ui/"))) {
382
+ if (!libraries.includes("shadcn")) libraries.push("radix");
383
+ }
384
+ if (deps["tailwindcss"]) {
385
+ libraries.push("tailwind");
386
+ }
387
+ if (deps["lucide-react"] || deps["lucide"]) {
388
+ libraries.push("lucide");
389
+ }
390
+ if (deps["@mui/material"] || deps["@material-ui/core"]) {
391
+ libraries.push("mui");
392
+ }
393
+ if (deps["@chakra-ui/react"]) {
394
+ libraries.push("chakra");
395
+ }
396
+ const hasTailwind = Boolean(deps["tailwindcss"] || fs2.existsSync(path2.resolve(cwd, "tailwind.config.js")) || fs2.existsSync(path2.resolve(cwd, "tailwind.config.ts")));
397
+ let tailwindVersion;
398
+ if (hasTailwind) {
399
+ const rawVersion = deps["tailwindcss"] || "";
400
+ if (rawVersion.includes("^4") || rawVersion.includes("~4") || rawVersion.startsWith("4")) {
401
+ tailwindVersion = "v4";
402
+ } else {
403
+ tailwindVersion = "v3";
404
+ }
405
+ }
406
+ const hasTypeScript = Boolean(deps["typescript"] || fs2.existsSync(path2.resolve(cwd, "tsconfig.json")));
407
+ const suggestedIncludes = [];
408
+ const extGroup = hasTypeScript ? "{tsx,jsx,ts,js}" : "{jsx,js}";
409
+ const candidateDirs = [
410
+ "frontend/src",
411
+ "frontend",
412
+ "apps/web/src",
413
+ "apps/web",
414
+ "client/src",
415
+ "client",
416
+ "src",
417
+ "app",
418
+ "pages",
419
+ "components"
420
+ ];
421
+ let foundUiDir = false;
422
+ for (const dir of candidateDirs) {
423
+ if (fs2.existsSync(path2.resolve(cwd, dir))) {
424
+ if (dir === "frontend" && fs2.existsSync(path2.resolve(cwd, "frontend/src"))) continue;
425
+ if (dir === "client" && fs2.existsSync(path2.resolve(cwd, "client/src"))) continue;
426
+ if (dir === "apps/web" && fs2.existsSync(path2.resolve(cwd, "apps/web/src"))) continue;
427
+ suggestedIncludes.push(`${dir}/**/*.${extGroup}`);
428
+ foundUiDir = true;
429
+ break;
430
+ }
431
+ }
432
+ if (hasPython) {
433
+ suggestedIncludes.push("**/*.py");
434
+ } else if (!foundUiDir) {
435
+ suggestedIncludes.push(`**/*.${extGroup}`);
436
+ }
437
+ const suggestedIgnores = getSmartIgnores(cwd);
438
+ if (hasShadcnConfig || libraries.includes("shadcn")) {
439
+ suggestedIgnores.push("**/components/ui/**");
440
+ }
441
+ const tokenCandidates = [
442
+ "frontend/src/lib/tokens.ts",
443
+ "frontend/src/tokens.ts",
444
+ "src/lib/tokens.ts",
445
+ "src/tokens.ts",
446
+ "src/design/tokens.ts",
447
+ "styles/tokens.css",
448
+ "src/app/globals.css",
449
+ "frontend/src/index.css",
450
+ "src/index.css"
451
+ ];
452
+ let tokensFile;
453
+ for (const candidate of tokenCandidates) {
454
+ if (fs2.existsSync(path2.resolve(cwd, candidate))) {
455
+ tokensFile = candidate;
456
+ break;
457
+ }
458
+ }
459
+ let preset = "berserk";
460
+ if (framework === "streamlit" || framework === "python-general") {
461
+ preset = "python";
462
+ }
463
+ return {
464
+ root: cwd,
465
+ isMonorepo,
466
+ monorepoWorkspaces,
467
+ framework,
468
+ libraries,
469
+ hasTailwind,
470
+ tailwindVersion,
471
+ hasTypeScript,
472
+ hasPython,
473
+ suggestedIncludes,
474
+ suggestedIgnores,
475
+ tokensFile,
476
+ preset
477
+ };
478
+ }
479
+
257
480
  // src/config/loader.ts
258
481
  var CONFIG_FILES = [
259
482
  "dse.config.js",
@@ -268,17 +491,18 @@ function defineConfig(config) {
268
491
  return config;
269
492
  }
270
493
  async function loadConfig(cwd = process.cwd(), customPath) {
494
+ const detected = detectProject(cwd);
271
495
  let userConfig = {};
272
496
  let foundConfigPath = null;
273
497
  if (customPath) {
274
- const resolved = path.resolve(cwd, customPath);
275
- if (fs.existsSync(resolved)) {
498
+ const resolved = path3.resolve(cwd, customPath);
499
+ if (fs3.existsSync(resolved)) {
276
500
  foundConfigPath = resolved;
277
501
  }
278
502
  } else {
279
503
  for (const filename of CONFIG_FILES) {
280
- const candidate = path.resolve(cwd, filename);
281
- if (fs.existsSync(candidate)) {
504
+ const candidate = path3.resolve(cwd, filename);
505
+ if (fs3.existsSync(candidate)) {
282
506
  foundConfigPath = candidate;
283
507
  break;
284
508
  }
@@ -286,7 +510,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
286
510
  }
287
511
  if (foundConfigPath) {
288
512
  if (foundConfigPath.endsWith(".json") || foundConfigPath.endsWith(".dserc")) {
289
- const content = fs.readFileSync(foundConfigPath, "utf-8");
513
+ const content = fs3.readFileSync(foundConfigPath, "utf-8");
290
514
  try {
291
515
  userConfig = JSON.parse(content);
292
516
  } catch (err) {
@@ -299,7 +523,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
299
523
  userConfig = mod.default || mod;
300
524
  } catch {
301
525
  try {
302
- const content = fs.readFileSync(foundConfigPath, "utf-8");
526
+ const content = fs3.readFileSync(foundConfigPath, "utf-8");
303
527
  const cjsReq = createCjsEvaluator(foundConfigPath, content);
304
528
  userConfig = cjsReq;
305
529
  } catch (innerErr) {
@@ -308,18 +532,19 @@ async function loadConfig(cwd = process.cwd(), customPath) {
308
532
  }
309
533
  }
310
534
  }
311
- let presetName = userConfig.preset;
312
- if (!presetName) {
313
- const isPython = fs.existsSync(path.resolve(cwd, "requirements.txt")) || fs.existsSync(path.resolve(cwd, "pyproject.toml")) || fs.existsSync(path.resolve(cwd, ".streamlit")) || fs.existsSync(path.resolve(cwd, "manage.py"));
314
- presetName = isPython ? "python" : "berserk";
315
- }
535
+ let presetName = userConfig.preset || detected.preset;
316
536
  let basePreset = berserkPreset;
317
537
  if (presetName === "python") {
318
538
  basePreset = pythonPreset;
319
539
  } else if (presetName === "recommended") {
320
540
  basePreset = defaultPreset;
321
541
  }
322
- return mergeConfigs(basePreset, userConfig);
542
+ const autonomousDefaults = {
543
+ include: detected.suggestedIncludes,
544
+ exclude: detected.suggestedIgnores
545
+ };
546
+ const withAutonomous = mergeConfigs(basePreset, autonomousDefaults);
547
+ return mergeConfigs(withAutonomous, userConfig);
323
548
  }
324
549
  function createCjsEvaluator(filePath, content) {
325
550
  if (content.includes("export default")) {
@@ -369,8 +594,8 @@ function mergeConfigs(base, overrides) {
369
594
  }
370
595
 
371
596
  // src/rules/runner.ts
372
- import fs2 from "fs";
373
- import path2 from "path";
597
+ import fs4 from "fs";
598
+ import path4 from "path";
374
599
  import fg from "fast-glob";
375
600
 
376
601
  // src/parser/ast.ts
@@ -783,16 +1008,52 @@ function checkRawHtmlElements(tagName, attributes, pos, filePath, addViolation,
783
1008
  pos.line,
784
1009
  pos.col,
785
1010
  "no-raw-html-elements",
786
- `Raw browser default '<input type="checkbox">' detected. Use standard design system '<Checkbox>' or '<Toggle>'.`,
1011
+ `Raw browser default '<input type="checkbox">' detected. Never use unstyled browser checkboxes. Use standard design system '<Checkbox>' or '<Toggle>'.`,
1012
+ lineSnippet
1013
+ );
1014
+ } else if (typeVal === "radio") {
1015
+ addViolation(
1016
+ filePath,
1017
+ pos.line,
1018
+ pos.col,
1019
+ "no-raw-html-elements",
1020
+ `Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
1021
+ lineSnippet
1022
+ );
1023
+ } else if (typeVal === "range") {
1024
+ addViolation(
1025
+ filePath,
1026
+ pos.line,
1027
+ pos.col,
1028
+ "no-raw-html-elements",
1029
+ `Raw browser default '<input type="range">' detected. Never use unstyled browser sliders. Use standard design system '<Slider>'.`,
1030
+ lineSnippet
1031
+ );
1032
+ } else if (typeVal === "file") {
1033
+ addViolation(
1034
+ filePath,
1035
+ pos.line,
1036
+ pos.col,
1037
+ "no-raw-html-elements",
1038
+ `Raw browser default '<input type="file">' detected. Never display native unstyled file picker inputs. Use a custom styled dropzone or trigger.`,
1039
+ lineSnippet
1040
+ );
1041
+ } else if (typeVal === "color") {
1042
+ addViolation(
1043
+ filePath,
1044
+ pos.line,
1045
+ pos.col,
1046
+ "no-raw-html-elements",
1047
+ `Raw browser default '<input type="color">' detected. Never use native browser color pickers. Use a custom color picker component.`,
787
1048
  lineSnippet
788
1049
  );
789
- } else if (["text", "email", "search"].includes(typeVal)) {
1050
+ } else if (["text", "email", "search", "password", "number", "tel", "url"].includes(typeVal)) {
790
1051
  addViolation(
791
1052
  filePath,
792
1053
  pos.line,
793
1054
  pos.col,
794
1055
  "no-raw-html-elements",
795
- `Raw '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
1056
+ `Raw browser default '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
796
1057
  lineSnippet
797
1058
  );
798
1059
  }
@@ -1050,6 +1311,9 @@ function checkPseudoChips(tagName, attributes, pos, filePath, addViolation, line
1050
1311
  function checkElementClasses(node, tagName, attributes, pos, filePath, addViolation, sourceFile, config) {
1051
1312
  if (!attributes.className) return;
1052
1313
  const isButton = ["Button", "IconButton", "ButtonyIcon", "FloatingActionButton"].includes(tagName);
1314
+ const isChip = ["Chip", "Badge", "Tag", "StatusBadge"].includes(tagName);
1315
+ const isTab = ["Tab", "TabTrigger", "TabsTrigger"].includes(tagName);
1316
+ const isCompactElement = isButton || isChip || isTab;
1053
1317
  const isCheckbox = tagName === "Checkbox" || attributes.type?.initializer?.getText(sourceFile)?.includes("checkbox") || attributes.role?.initializer?.getText(sourceFile)?.includes("checkbox") || Boolean(attributes["aria-checked"]) || attributes.className && /\b(?:size-5|w-5\s+h-5)\b/.test(attributes.className.rawText);
1054
1318
  const lineSnippet = sourceFile.getFullText().split("\n")[pos.line - 1] || "";
1055
1319
  const classStrings = extractClassNames(attributes.className.initializer, sourceFile);
@@ -1089,8 +1353,33 @@ function checkElementClasses(node, tagName, attributes, pos, filePath, addViolat
1089
1353
  );
1090
1354
  }
1091
1355
  }
1356
+ if (isCompactElement && config.rules?.["no-wrapping-compact-elements"] !== "off") {
1357
+ if (dec.core === "flex-wrap" || dec.core === "whitespace-normal" || dec.core === "whitespace-pre-wrap") {
1358
+ addViolation(
1359
+ filePath,
1360
+ nodePos.line,
1361
+ nodePos.col,
1362
+ "no-wrapping-compact-elements",
1363
+ `Disallowed text wrapping class '${dec.core}' on <${tagName}>. Compact interactive elements (buttons, chips, badges, tabs) must never wrap text across multiple lines.`,
1364
+ lineSnippet
1365
+ );
1366
+ }
1367
+ }
1092
1368
  });
1093
1369
  });
1370
+ if (tagName === "TableHead" && config.rules?.["no-wrapping-compact-elements"] !== "off") {
1371
+ const rawClass = attributes.className?.rawText || "";
1372
+ if (/\b(?:flex-wrap|whitespace-normal|whitespace-pre-wrap)\b/.test(rawClass)) {
1373
+ addViolation(
1374
+ filePath,
1375
+ pos.line,
1376
+ pos.col,
1377
+ "no-wrapping-compact-elements",
1378
+ `Disallowed text wrapping on <TableHead>. Column headers must remain single-line ('whitespace-nowrap') to preserve table column alignment.`,
1379
+ lineSnippet
1380
+ );
1381
+ }
1382
+ }
1094
1383
  }
1095
1384
 
1096
1385
  // src/rules/regex-rules.ts
@@ -1311,8 +1600,8 @@ function runPythonRules(content, filePath, addViolation, config) {
1311
1600
 
1312
1601
  // src/rules/runner.ts
1313
1602
  async function runDseScan(config, cwd = process.cwd()) {
1314
- let targetDir = path2.resolve(cwd, config.targetDir || "src");
1315
- if (!fs2.existsSync(targetDir)) {
1603
+ let targetDir = path4.resolve(cwd, config.targetDir || "src");
1604
+ if (!fs4.existsSync(targetDir)) {
1316
1605
  targetDir = cwd;
1317
1606
  }
1318
1607
  const includePatterns = config.include || ["**/*.{tsx,jsx,ts,js}"];
@@ -1344,7 +1633,7 @@ async function runDseScan(config, cwd = process.cwd()) {
1344
1633
  const violations = [];
1345
1634
  const seenViolationKeys = /* @__PURE__ */ new Set();
1346
1635
  const addViolation = (file, line, col, rule, message, snippet) => {
1347
- const normFile = path2.relative(cwd, file).replace(/\\/g, "/");
1636
+ const normFile = path4.relative(cwd, file).replace(/\\/g, "/");
1348
1637
  const key = `${normFile}:${line}:${rule}`;
1349
1638
  if (seenViolationKeys.has(key)) return;
1350
1639
  seenViolationKeys.add(key);
@@ -1362,7 +1651,7 @@ async function runDseScan(config, cwd = process.cwd()) {
1362
1651
  };
1363
1652
  let scannedJsxFiles = false;
1364
1653
  for (const filePath of files) {
1365
- const content = fs2.readFileSync(filePath, "utf-8");
1654
+ const content = fs4.readFileSync(filePath, "utf-8");
1366
1655
  const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || filePath.endsWith(".ts") || filePath.endsWith(".js");
1367
1656
  const isPythonOrTemplate = filePath.endsWith(".py") || filePath.endsWith(".html") || filePath.endsWith(".jinja") || filePath.endsWith(".jinja2") || filePath.endsWith(".css");
1368
1657
  if (isJsx) {
@@ -1483,8 +1772,8 @@ function formatJson(result) {
1483
1772
  }
1484
1773
 
1485
1774
  // src/scaffold/index.ts
1486
- import fs3 from "fs";
1487
- import path3 from "path";
1775
+ import fs5 from "fs";
1776
+ import path5 from "path";
1488
1777
 
1489
1778
  // src/scaffold/templates/tokens.ts
1490
1779
  var tokensTemplate = `/**
@@ -1627,7 +1916,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1627
1916
  ({ className = "", variant = "primary", size = "default", children, ...props }, ref) => {
1628
1917
  // Standard 36px height, 8px radius, font-medium, strictly neutral palette
1629
1918
  const baseClasses =
1630
- "inline-flex items-center justify-center font-medium text-sm rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none";
1919
+ "inline-flex items-center justify-center font-medium text-sm rounded-md whitespace-nowrap shrink-0 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none cursor-pointer";
1920
+
1631
1921
 
1632
1922
  const variantClasses = {
1633
1923
  primary: "bg-neutral-900 text-white hover:bg-neutral-800 shadow-2xs",
@@ -1668,7 +1958,8 @@ export const Chip: React.FC<ChipProps> = ({
1668
1958
  }) => {
1669
1959
  // Standard 36px height, 8px radius, 14px typography
1670
1960
  const baseClasses =
1671
- "inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium gap-1.5 select-none";
1961
+ "inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium whitespace-nowrap shrink-0 gap-1.5 select-none";
1962
+
1672
1963
 
1673
1964
  const variantClasses = {
1674
1965
  neutral: "bg-neutral-100 text-neutral-800 border border-neutral-200",
@@ -1838,12 +2129,12 @@ export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
1838
2129
  ({ className = "", icon, resizable = true, children, ...props }, ref) => (
1839
2130
  <th
1840
2131
  ref={ref}
1841
- className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative \${className}\`}
2132
+ className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative whitespace-nowrap \${className}\`}
1842
2133
  {...props}
1843
2134
  >
1844
2135
  <div className="flex items-center gap-2">
1845
2136
  {icon && <span className="text-neutral-500 shrink-0">{icon}</span>}
1846
- <span>{children}</span>
2137
+ <span className="whitespace-nowrap">{children}</span>
1847
2138
  </div>
1848
2139
  </th>
1849
2140
  )
@@ -1889,7 +2180,7 @@ export const Tabs: React.FC<TabsProps> = ({ items, activeId, onChange, className
1889
2180
  type="button"
1890
2181
  disabled={tab.disabled}
1891
2182
  onClick={() => onChange(tab.id)}
1892
- className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-all select-none \${
2183
+ className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium whitespace-nowrap shrink-0 transition-all select-none \${
1893
2184
  isActive
1894
2185
  ? "bg-white text-neutral-900 shadow-2xs"
1895
2186
  : "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200/50"
@@ -1981,8 +2272,8 @@ var COMPONENT_TEMPLATES = {
1981
2272
  };
1982
2273
  function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
1983
2274
  if (preset === "python") {
1984
- const jsonPath = path3.resolve(cwd, "dse.config.json");
1985
- if (fs3.existsSync(jsonPath)) return jsonPath;
2275
+ const jsonPath = path5.resolve(cwd, "dse.config.json");
2276
+ if (fs5.existsSync(jsonPath)) return jsonPath;
1986
2277
  const content2 = JSON.stringify(
1987
2278
  {
1988
2279
  preset: "python",
@@ -1999,11 +2290,11 @@ function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
1999
2290
  null,
2000
2291
  2
2001
2292
  ) + "\n";
2002
- fs3.writeFileSync(jsonPath, content2, "utf-8");
2293
+ fs5.writeFileSync(jsonPath, content2, "utf-8");
2003
2294
  return jsonPath;
2004
2295
  }
2005
- const configPath = path3.resolve(cwd, "dse.config.js");
2006
- if (fs3.existsSync(configPath)) {
2296
+ const configPath = path5.resolve(cwd, "dse.config.js");
2297
+ if (fs5.existsSync(configPath)) {
2007
2298
  return configPath;
2008
2299
  }
2009
2300
  const content = `import { defineConfig } from "design-system-enforcer";
@@ -2031,21 +2322,21 @@ export default defineConfig({
2031
2322
  ],
2032
2323
  });
2033
2324
  `;
2034
- fs3.writeFileSync(configPath, content, "utf-8");
2325
+ fs5.writeFileSync(configPath, content, "utf-8");
2035
2326
  return configPath;
2036
2327
  }
2037
2328
  function scaffoldTokens(cwd = process.cwd()) {
2038
- const libDir = path3.resolve(cwd, "src/lib");
2039
- const appDir = path3.resolve(cwd, "src/app");
2040
- if (!fs3.existsSync(libDir)) fs3.mkdirSync(libDir, { recursive: true });
2041
- if (!fs3.existsSync(appDir)) fs3.mkdirSync(appDir, { recursive: true });
2042
- const tokensPath = path3.join(libDir, "tokens.ts");
2043
- if (!fs3.existsSync(tokensPath)) {
2044
- fs3.writeFileSync(tokensPath, tokensTemplate, "utf-8");
2045
- }
2046
- const cssPath = path3.join(appDir, "globals.css");
2047
- if (!fs3.existsSync(cssPath)) {
2048
- fs3.writeFileSync(cssPath, cssTemplate, "utf-8");
2329
+ const libDir = path5.resolve(cwd, "src/lib");
2330
+ const appDir = path5.resolve(cwd, "src/app");
2331
+ if (!fs5.existsSync(libDir)) fs5.mkdirSync(libDir, { recursive: true });
2332
+ if (!fs5.existsSync(appDir)) fs5.mkdirSync(appDir, { recursive: true });
2333
+ const tokensPath = path5.join(libDir, "tokens.ts");
2334
+ if (!fs5.existsSync(tokensPath)) {
2335
+ fs5.writeFileSync(tokensPath, tokensTemplate, "utf-8");
2336
+ }
2337
+ const cssPath = path5.join(appDir, "globals.css");
2338
+ if (!fs5.existsSync(cssPath)) {
2339
+ fs5.writeFileSync(cssPath, cssTemplate, "utf-8");
2049
2340
  }
2050
2341
  return { tokensPath, cssPath };
2051
2342
  }
@@ -2055,13 +2346,13 @@ function scaffoldComponent(componentName, cwd = process.cwd()) {
2055
2346
  if (!template) {
2056
2347
  return null;
2057
2348
  }
2058
- const uiDir = path3.resolve(cwd, "src/components/ui");
2059
- if (!fs3.existsSync(uiDir)) {
2060
- fs3.mkdirSync(uiDir, { recursive: true });
2349
+ const uiDir = path5.resolve(cwd, "src/components/ui");
2350
+ if (!fs5.existsSync(uiDir)) {
2351
+ fs5.mkdirSync(uiDir, { recursive: true });
2061
2352
  }
2062
- const targetPath = path3.join(uiDir, template.filename);
2063
- if (!fs3.existsSync(targetPath)) {
2064
- fs3.writeFileSync(targetPath, template.content, "utf-8");
2353
+ const targetPath = path5.join(uiDir, template.filename);
2354
+ if (!fs5.existsSync(targetPath)) {
2355
+ fs5.writeFileSync(targetPath, template.content, "utf-8");
2065
2356
  }
2066
2357
  return targetPath;
2067
2358
  }