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/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs4 from "fs";
5
- import path4 from "path";
4
+ import fs6 from "fs";
5
+ import path6 from "path";
6
6
  import { Command } from "commander";
7
7
  import pc2 from "picocolors";
8
8
 
9
9
  // src/config/loader.ts
10
- import fs from "fs";
11
- import path from "path";
10
+ import fs3 from "fs";
11
+ import path3 from "path";
12
12
  import { pathToFileURL } from "url";
13
13
 
14
14
  // src/config/presets/berserk.ts
@@ -97,6 +97,9 @@ var berserkPreset = {
97
97
  disallowRawHtml: {
98
98
  button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
99
99
  select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
100
+ textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
101
+ progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
102
+ dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true },
100
103
  table: { replaceWith: "Table", importFrom: "@/components/ui/table", checkRawElement: true },
101
104
  thead: { replaceWith: "TableHeader", importFrom: "@/components/ui/table", checkRawElement: true },
102
105
  tbody: { replaceWith: "TableBody", importFrom: "@/components/ui/table", checkRawElement: true },
@@ -148,7 +151,8 @@ var berserkPreset = {
148
151
  "table-no-horizontal-stacking": "error",
149
152
  "table-windowed-pagination": "error",
150
153
  "tabs-component-required": "error",
151
- "component-inventory-active": "error"
154
+ "component-inventory-active": "error",
155
+ "no-wrapping-compact-elements": "error"
152
156
  }
153
157
  };
154
158
 
@@ -185,7 +189,10 @@ var defaultPreset = {
185
189
  components: {
186
190
  disallowRawHtml: {
187
191
  button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
188
- select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true }
192
+ select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
193
+ textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
194
+ progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
195
+ dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true }
189
196
  },
190
197
  disallowPseudoChips: true
191
198
  },
@@ -196,7 +203,8 @@ var defaultPreset = {
196
203
  "no-pseudo-chips": "warn",
197
204
  "no-chip-colored-buttons": "error",
198
205
  "no-text-arrows": "error",
199
- "no-backdrop-blur": "warn"
206
+ "no-backdrop-blur": "warn",
207
+ "no-wrapping-compact-elements": "warn"
200
208
  }
201
209
  };
202
210
 
@@ -260,6 +268,221 @@ var pythonPreset = {
260
268
  }
261
269
  };
262
270
 
271
+ // src/detector/index.ts
272
+ import fs2 from "fs";
273
+ import path2 from "path";
274
+
275
+ // src/detector/ignores.ts
276
+ import fs from "fs";
277
+ import path from "path";
278
+ var DEFAULT_IGNORES = [
279
+ "**/node_modules/**",
280
+ "**/.next/**",
281
+ "**/dist/**",
282
+ "**/build/**",
283
+ "**/out/**",
284
+ "**/.turbo/**",
285
+ "**/.git/**",
286
+ "**/coverage/**",
287
+ "**/.venv/**",
288
+ "**/venv/**",
289
+ "**/env/**",
290
+ "**/__pycache__/**",
291
+ "**/.pytest_cache/**",
292
+ "**/*.pyc",
293
+ // Non-UI code
294
+ "**/api/**",
295
+ "**/*.test.*",
296
+ "**/*.spec.*",
297
+ "**/__tests__/**",
298
+ "**/*.stories.*",
299
+ "**/generated/**"
300
+ ];
301
+ function getSmartIgnores(cwd) {
302
+ const result = new Set(DEFAULT_IGNORES);
303
+ const gitignorePath = path.resolve(cwd, ".gitignore");
304
+ if (fs.existsSync(gitignorePath)) {
305
+ try {
306
+ const content = fs.readFileSync(gitignorePath, "utf-8");
307
+ const lines = content.split(/\r?\n/);
308
+ for (let line of lines) {
309
+ line = line.trim();
310
+ if (!line || line.startsWith("#")) continue;
311
+ if (line.startsWith("/")) {
312
+ line = line.substring(1);
313
+ }
314
+ if (line.endsWith("/")) {
315
+ line = line.slice(0, -1);
316
+ }
317
+ if (line) {
318
+ result.add(`**/${line}/**`);
319
+ result.add(`**/${line}`);
320
+ }
321
+ }
322
+ } catch {
323
+ }
324
+ }
325
+ return Array.from(result);
326
+ }
327
+
328
+ // src/detector/index.ts
329
+ function safeReadJson(filePath) {
330
+ try {
331
+ if (fs2.existsSync(filePath)) {
332
+ const raw = fs2.readFileSync(filePath, "utf-8");
333
+ return JSON.parse(raw);
334
+ }
335
+ } catch {
336
+ }
337
+ return null;
338
+ }
339
+ function detectProject(cwd = process.cwd()) {
340
+ const pkg = safeReadJson(path2.resolve(cwd, "package.json"));
341
+ const deps = { ...pkg?.dependencies || {}, ...pkg?.devDependencies || {} };
342
+ const hasRequirements = fs2.existsSync(path2.resolve(cwd, "requirements.txt"));
343
+ const hasPyproject = fs2.existsSync(path2.resolve(cwd, "pyproject.toml"));
344
+ const hasPython = hasRequirements || hasPyproject || fs2.existsSync(path2.resolve(cwd, "setup.py"));
345
+ let isStreamlit = false;
346
+ if (hasPython) {
347
+ if (hasRequirements) {
348
+ try {
349
+ const reqContent = fs2.readFileSync(path2.resolve(cwd, "requirements.txt"), "utf-8");
350
+ if (reqContent.toLowerCase().includes("streamlit")) {
351
+ isStreamlit = true;
352
+ }
353
+ } catch {
354
+ }
355
+ }
356
+ }
357
+ const isPnpmMonorepo = fs2.existsSync(path2.resolve(cwd, "pnpm-workspace.yaml"));
358
+ const isTurbo = fs2.existsSync(path2.resolve(cwd, "turbo.json"));
359
+ let monorepoWorkspaces = [];
360
+ if (Array.isArray(pkg?.workspaces)) {
361
+ monorepoWorkspaces = pkg.workspaces;
362
+ } else if (pkg?.workspaces?.packages) {
363
+ monorepoWorkspaces = pkg.workspaces.packages;
364
+ }
365
+ const isMonorepo = isPnpmMonorepo || isTurbo || monorepoWorkspaces.length > 0;
366
+ let framework = "unknown";
367
+ if (deps["next"]) {
368
+ framework = "nextjs";
369
+ } else if (deps["vite"]) {
370
+ framework = "vite";
371
+ } else if (deps["@remix-run/react"]) {
372
+ framework = "remix";
373
+ } else if (deps["astro"]) {
374
+ framework = "astro";
375
+ } else if (deps["react-scripts"]) {
376
+ framework = "react-cra";
377
+ } else if (isStreamlit) {
378
+ framework = "streamlit";
379
+ } else if (hasPython) {
380
+ framework = "python-general";
381
+ }
382
+ const libraries = [];
383
+ const hasShadcnConfig = fs2.existsSync(path2.resolve(cwd, "components.json"));
384
+ if (hasShadcnConfig || deps["@radix-ui/react-slot"]) {
385
+ libraries.push("shadcn");
386
+ }
387
+ if (Object.keys(deps).some((d) => d.startsWith("@radix-ui/"))) {
388
+ if (!libraries.includes("shadcn")) libraries.push("radix");
389
+ }
390
+ if (deps["tailwindcss"]) {
391
+ libraries.push("tailwind");
392
+ }
393
+ if (deps["lucide-react"] || deps["lucide"]) {
394
+ libraries.push("lucide");
395
+ }
396
+ if (deps["@mui/material"] || deps["@material-ui/core"]) {
397
+ libraries.push("mui");
398
+ }
399
+ if (deps["@chakra-ui/react"]) {
400
+ libraries.push("chakra");
401
+ }
402
+ const hasTailwind = Boolean(deps["tailwindcss"] || fs2.existsSync(path2.resolve(cwd, "tailwind.config.js")) || fs2.existsSync(path2.resolve(cwd, "tailwind.config.ts")));
403
+ let tailwindVersion;
404
+ if (hasTailwind) {
405
+ const rawVersion = deps["tailwindcss"] || "";
406
+ if (rawVersion.includes("^4") || rawVersion.includes("~4") || rawVersion.startsWith("4")) {
407
+ tailwindVersion = "v4";
408
+ } else {
409
+ tailwindVersion = "v3";
410
+ }
411
+ }
412
+ const hasTypeScript = Boolean(deps["typescript"] || fs2.existsSync(path2.resolve(cwd, "tsconfig.json")));
413
+ const suggestedIncludes = [];
414
+ const extGroup = hasTypeScript ? "{tsx,jsx,ts,js}" : "{jsx,js}";
415
+ const candidateDirs = [
416
+ "frontend/src",
417
+ "frontend",
418
+ "apps/web/src",
419
+ "apps/web",
420
+ "client/src",
421
+ "client",
422
+ "src",
423
+ "app",
424
+ "pages",
425
+ "components"
426
+ ];
427
+ let foundUiDir = false;
428
+ for (const dir of candidateDirs) {
429
+ if (fs2.existsSync(path2.resolve(cwd, dir))) {
430
+ if (dir === "frontend" && fs2.existsSync(path2.resolve(cwd, "frontend/src"))) continue;
431
+ if (dir === "client" && fs2.existsSync(path2.resolve(cwd, "client/src"))) continue;
432
+ if (dir === "apps/web" && fs2.existsSync(path2.resolve(cwd, "apps/web/src"))) continue;
433
+ suggestedIncludes.push(`${dir}/**/*.${extGroup}`);
434
+ foundUiDir = true;
435
+ break;
436
+ }
437
+ }
438
+ if (hasPython) {
439
+ suggestedIncludes.push("**/*.py");
440
+ } else if (!foundUiDir) {
441
+ suggestedIncludes.push(`**/*.${extGroup}`);
442
+ }
443
+ const suggestedIgnores = getSmartIgnores(cwd);
444
+ if (hasShadcnConfig || libraries.includes("shadcn")) {
445
+ suggestedIgnores.push("**/components/ui/**");
446
+ }
447
+ const tokenCandidates = [
448
+ "frontend/src/lib/tokens.ts",
449
+ "frontend/src/tokens.ts",
450
+ "src/lib/tokens.ts",
451
+ "src/tokens.ts",
452
+ "src/design/tokens.ts",
453
+ "styles/tokens.css",
454
+ "src/app/globals.css",
455
+ "frontend/src/index.css",
456
+ "src/index.css"
457
+ ];
458
+ let tokensFile;
459
+ for (const candidate of tokenCandidates) {
460
+ if (fs2.existsSync(path2.resolve(cwd, candidate))) {
461
+ tokensFile = candidate;
462
+ break;
463
+ }
464
+ }
465
+ let preset = "berserk";
466
+ if (framework === "streamlit" || framework === "python-general") {
467
+ preset = "python";
468
+ }
469
+ return {
470
+ root: cwd,
471
+ isMonorepo,
472
+ monorepoWorkspaces,
473
+ framework,
474
+ libraries,
475
+ hasTailwind,
476
+ tailwindVersion,
477
+ hasTypeScript,
478
+ hasPython,
479
+ suggestedIncludes,
480
+ suggestedIgnores,
481
+ tokensFile,
482
+ preset
483
+ };
484
+ }
485
+
263
486
  // src/config/loader.ts
264
487
  var CONFIG_FILES = [
265
488
  "dse.config.js",
@@ -271,17 +494,18 @@ var CONFIG_FILES = [
271
494
  ".dserc"
272
495
  ];
273
496
  async function loadConfig(cwd = process.cwd(), customPath) {
497
+ const detected = detectProject(cwd);
274
498
  let userConfig = {};
275
499
  let foundConfigPath = null;
276
500
  if (customPath) {
277
- const resolved = path.resolve(cwd, customPath);
278
- if (fs.existsSync(resolved)) {
501
+ const resolved = path3.resolve(cwd, customPath);
502
+ if (fs3.existsSync(resolved)) {
279
503
  foundConfigPath = resolved;
280
504
  }
281
505
  } else {
282
506
  for (const filename of CONFIG_FILES) {
283
- const candidate = path.resolve(cwd, filename);
284
- if (fs.existsSync(candidate)) {
507
+ const candidate = path3.resolve(cwd, filename);
508
+ if (fs3.existsSync(candidate)) {
285
509
  foundConfigPath = candidate;
286
510
  break;
287
511
  }
@@ -289,7 +513,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
289
513
  }
290
514
  if (foundConfigPath) {
291
515
  if (foundConfigPath.endsWith(".json") || foundConfigPath.endsWith(".dserc")) {
292
- const content = fs.readFileSync(foundConfigPath, "utf-8");
516
+ const content = fs3.readFileSync(foundConfigPath, "utf-8");
293
517
  try {
294
518
  userConfig = JSON.parse(content);
295
519
  } catch (err) {
@@ -302,7 +526,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
302
526
  userConfig = mod.default || mod;
303
527
  } catch {
304
528
  try {
305
- const content = fs.readFileSync(foundConfigPath, "utf-8");
529
+ const content = fs3.readFileSync(foundConfigPath, "utf-8");
306
530
  const cjsReq = createCjsEvaluator(foundConfigPath, content);
307
531
  userConfig = cjsReq;
308
532
  } catch (innerErr) {
@@ -311,18 +535,19 @@ async function loadConfig(cwd = process.cwd(), customPath) {
311
535
  }
312
536
  }
313
537
  }
314
- let presetName = userConfig.preset;
315
- if (!presetName) {
316
- 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"));
317
- presetName = isPython ? "python" : "berserk";
318
- }
538
+ let presetName = userConfig.preset || detected.preset;
319
539
  let basePreset = berserkPreset;
320
540
  if (presetName === "python") {
321
541
  basePreset = pythonPreset;
322
542
  } else if (presetName === "recommended") {
323
543
  basePreset = defaultPreset;
324
544
  }
325
- return mergeConfigs(basePreset, userConfig);
545
+ const autonomousDefaults = {
546
+ include: detected.suggestedIncludes,
547
+ exclude: detected.suggestedIgnores
548
+ };
549
+ const withAutonomous = mergeConfigs(basePreset, autonomousDefaults);
550
+ return mergeConfigs(withAutonomous, userConfig);
326
551
  }
327
552
  function createCjsEvaluator(filePath, content) {
328
553
  if (content.includes("export default")) {
@@ -372,8 +597,8 @@ function mergeConfigs(base, overrides) {
372
597
  }
373
598
 
374
599
  // src/rules/runner.ts
375
- import fs2 from "fs";
376
- import path2 from "path";
600
+ import fs4 from "fs";
601
+ import path4 from "path";
377
602
  import fg from "fast-glob";
378
603
 
379
604
  // src/parser/ast.ts
@@ -786,16 +1011,52 @@ function checkRawHtmlElements(tagName, attributes, pos, filePath, addViolation,
786
1011
  pos.line,
787
1012
  pos.col,
788
1013
  "no-raw-html-elements",
789
- `Raw browser default '<input type="checkbox">' detected. Use standard design system '<Checkbox>' or '<Toggle>'.`,
1014
+ `Raw browser default '<input type="checkbox">' detected. Never use unstyled browser checkboxes. Use standard design system '<Checkbox>' or '<Toggle>'.`,
1015
+ lineSnippet
1016
+ );
1017
+ } else if (typeVal === "radio") {
1018
+ addViolation(
1019
+ filePath,
1020
+ pos.line,
1021
+ pos.col,
1022
+ "no-raw-html-elements",
1023
+ `Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
790
1024
  lineSnippet
791
1025
  );
792
- } else if (["text", "email", "search"].includes(typeVal)) {
1026
+ } else if (typeVal === "range") {
793
1027
  addViolation(
794
1028
  filePath,
795
1029
  pos.line,
796
1030
  pos.col,
797
1031
  "no-raw-html-elements",
798
- `Raw '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
1032
+ `Raw browser default '<input type="range">' detected. Never use unstyled browser sliders. Use standard design system '<Slider>'.`,
1033
+ lineSnippet
1034
+ );
1035
+ } else if (typeVal === "file") {
1036
+ addViolation(
1037
+ filePath,
1038
+ pos.line,
1039
+ pos.col,
1040
+ "no-raw-html-elements",
1041
+ `Raw browser default '<input type="file">' detected. Never display native unstyled file picker inputs. Use a custom styled dropzone or trigger.`,
1042
+ lineSnippet
1043
+ );
1044
+ } else if (typeVal === "color") {
1045
+ addViolation(
1046
+ filePath,
1047
+ pos.line,
1048
+ pos.col,
1049
+ "no-raw-html-elements",
1050
+ `Raw browser default '<input type="color">' detected. Never use native browser color pickers. Use a custom color picker component.`,
1051
+ lineSnippet
1052
+ );
1053
+ } else if (["text", "email", "search", "password", "number", "tel", "url"].includes(typeVal)) {
1054
+ addViolation(
1055
+ filePath,
1056
+ pos.line,
1057
+ pos.col,
1058
+ "no-raw-html-elements",
1059
+ `Raw browser default '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
799
1060
  lineSnippet
800
1061
  );
801
1062
  }
@@ -1053,6 +1314,9 @@ function checkPseudoChips(tagName, attributes, pos, filePath, addViolation, line
1053
1314
  function checkElementClasses(node, tagName, attributes, pos, filePath, addViolation, sourceFile, config) {
1054
1315
  if (!attributes.className) return;
1055
1316
  const isButton = ["Button", "IconButton", "ButtonyIcon", "FloatingActionButton"].includes(tagName);
1317
+ const isChip = ["Chip", "Badge", "Tag", "StatusBadge"].includes(tagName);
1318
+ const isTab = ["Tab", "TabTrigger", "TabsTrigger"].includes(tagName);
1319
+ const isCompactElement = isButton || isChip || isTab;
1056
1320
  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);
1057
1321
  const lineSnippet = sourceFile.getFullText().split("\n")[pos.line - 1] || "";
1058
1322
  const classStrings = extractClassNames(attributes.className.initializer, sourceFile);
@@ -1092,8 +1356,33 @@ function checkElementClasses(node, tagName, attributes, pos, filePath, addViolat
1092
1356
  );
1093
1357
  }
1094
1358
  }
1359
+ if (isCompactElement && config.rules?.["no-wrapping-compact-elements"] !== "off") {
1360
+ if (dec.core === "flex-wrap" || dec.core === "whitespace-normal" || dec.core === "whitespace-pre-wrap") {
1361
+ addViolation(
1362
+ filePath,
1363
+ nodePos.line,
1364
+ nodePos.col,
1365
+ "no-wrapping-compact-elements",
1366
+ `Disallowed text wrapping class '${dec.core}' on <${tagName}>. Compact interactive elements (buttons, chips, badges, tabs) must never wrap text across multiple lines.`,
1367
+ lineSnippet
1368
+ );
1369
+ }
1370
+ }
1095
1371
  });
1096
1372
  });
1373
+ if (tagName === "TableHead" && config.rules?.["no-wrapping-compact-elements"] !== "off") {
1374
+ const rawClass = attributes.className?.rawText || "";
1375
+ if (/\b(?:flex-wrap|whitespace-normal|whitespace-pre-wrap)\b/.test(rawClass)) {
1376
+ addViolation(
1377
+ filePath,
1378
+ pos.line,
1379
+ pos.col,
1380
+ "no-wrapping-compact-elements",
1381
+ `Disallowed text wrapping on <TableHead>. Column headers must remain single-line ('whitespace-nowrap') to preserve table column alignment.`,
1382
+ lineSnippet
1383
+ );
1384
+ }
1385
+ }
1097
1386
  }
1098
1387
 
1099
1388
  // src/rules/regex-rules.ts
@@ -1314,8 +1603,8 @@ function runPythonRules(content, filePath, addViolation, config) {
1314
1603
 
1315
1604
  // src/rules/runner.ts
1316
1605
  async function runDseScan(config, cwd = process.cwd()) {
1317
- let targetDir = path2.resolve(cwd, config.targetDir || "src");
1318
- if (!fs2.existsSync(targetDir)) {
1606
+ let targetDir = path4.resolve(cwd, config.targetDir || "src");
1607
+ if (!fs4.existsSync(targetDir)) {
1319
1608
  targetDir = cwd;
1320
1609
  }
1321
1610
  const includePatterns = config.include || ["**/*.{tsx,jsx,ts,js}"];
@@ -1347,7 +1636,7 @@ async function runDseScan(config, cwd = process.cwd()) {
1347
1636
  const violations = [];
1348
1637
  const seenViolationKeys = /* @__PURE__ */ new Set();
1349
1638
  const addViolation = (file, line, col, rule, message, snippet) => {
1350
- const normFile = path2.relative(cwd, file).replace(/\\/g, "/");
1639
+ const normFile = path4.relative(cwd, file).replace(/\\/g, "/");
1351
1640
  const key = `${normFile}:${line}:${rule}`;
1352
1641
  if (seenViolationKeys.has(key)) return;
1353
1642
  seenViolationKeys.add(key);
@@ -1365,7 +1654,7 @@ async function runDseScan(config, cwd = process.cwd()) {
1365
1654
  };
1366
1655
  let scannedJsxFiles = false;
1367
1656
  for (const filePath of files) {
1368
- const content = fs2.readFileSync(filePath, "utf-8");
1657
+ const content = fs4.readFileSync(filePath, "utf-8");
1369
1658
  const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || filePath.endsWith(".ts") || filePath.endsWith(".js");
1370
1659
  const isPythonOrTemplate = filePath.endsWith(".py") || filePath.endsWith(".html") || filePath.endsWith(".jinja") || filePath.endsWith(".jinja2") || filePath.endsWith(".css");
1371
1660
  if (isJsx) {
@@ -1486,8 +1775,8 @@ function formatJson(result) {
1486
1775
  }
1487
1776
 
1488
1777
  // src/scaffold/index.ts
1489
- import fs3 from "fs";
1490
- import path3 from "path";
1778
+ import fs5 from "fs";
1779
+ import path5 from "path";
1491
1780
 
1492
1781
  // src/scaffold/templates/tokens.ts
1493
1782
  var tokensTemplate = `/**
@@ -1630,7 +1919,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1630
1919
  ({ className = "", variant = "primary", size = "default", children, ...props }, ref) => {
1631
1920
  // Standard 36px height, 8px radius, font-medium, strictly neutral palette
1632
1921
  const baseClasses =
1633
- "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";
1922
+ "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";
1923
+
1634
1924
 
1635
1925
  const variantClasses = {
1636
1926
  primary: "bg-neutral-900 text-white hover:bg-neutral-800 shadow-2xs",
@@ -1671,7 +1961,8 @@ export const Chip: React.FC<ChipProps> = ({
1671
1961
  }) => {
1672
1962
  // Standard 36px height, 8px radius, 14px typography
1673
1963
  const baseClasses =
1674
- "inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium gap-1.5 select-none";
1964
+ "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";
1965
+
1675
1966
 
1676
1967
  const variantClasses = {
1677
1968
  neutral: "bg-neutral-100 text-neutral-800 border border-neutral-200",
@@ -1841,12 +2132,12 @@ export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
1841
2132
  ({ className = "", icon, resizable = true, children, ...props }, ref) => (
1842
2133
  <th
1843
2134
  ref={ref}
1844
- className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative \${className}\`}
2135
+ className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative whitespace-nowrap \${className}\`}
1845
2136
  {...props}
1846
2137
  >
1847
2138
  <div className="flex items-center gap-2">
1848
2139
  {icon && <span className="text-neutral-500 shrink-0">{icon}</span>}
1849
- <span>{children}</span>
2140
+ <span className="whitespace-nowrap">{children}</span>
1850
2141
  </div>
1851
2142
  </th>
1852
2143
  )
@@ -1892,7 +2183,7 @@ export const Tabs: React.FC<TabsProps> = ({ items, activeId, onChange, className
1892
2183
  type="button"
1893
2184
  disabled={tab.disabled}
1894
2185
  onClick={() => onChange(tab.id)}
1895
- className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-all select-none \${
2186
+ 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 \${
1896
2187
  isActive
1897
2188
  ? "bg-white text-neutral-900 shadow-2xs"
1898
2189
  : "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200/50"
@@ -1984,8 +2275,8 @@ var COMPONENT_TEMPLATES = {
1984
2275
  };
1985
2276
  function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
1986
2277
  if (preset === "python") {
1987
- const jsonPath = path3.resolve(cwd, "dse.config.json");
1988
- if (fs3.existsSync(jsonPath)) return jsonPath;
2278
+ const jsonPath = path5.resolve(cwd, "dse.config.json");
2279
+ if (fs5.existsSync(jsonPath)) return jsonPath;
1989
2280
  const content2 = JSON.stringify(
1990
2281
  {
1991
2282
  preset: "python",
@@ -2002,11 +2293,11 @@ function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
2002
2293
  null,
2003
2294
  2
2004
2295
  ) + "\n";
2005
- fs3.writeFileSync(jsonPath, content2, "utf-8");
2296
+ fs5.writeFileSync(jsonPath, content2, "utf-8");
2006
2297
  return jsonPath;
2007
2298
  }
2008
- const configPath = path3.resolve(cwd, "dse.config.js");
2009
- if (fs3.existsSync(configPath)) {
2299
+ const configPath = path5.resolve(cwd, "dse.config.js");
2300
+ if (fs5.existsSync(configPath)) {
2010
2301
  return configPath;
2011
2302
  }
2012
2303
  const content = `import { defineConfig } from "design-system-enforcer";
@@ -2034,21 +2325,21 @@ export default defineConfig({
2034
2325
  ],
2035
2326
  });
2036
2327
  `;
2037
- fs3.writeFileSync(configPath, content, "utf-8");
2328
+ fs5.writeFileSync(configPath, content, "utf-8");
2038
2329
  return configPath;
2039
2330
  }
2040
2331
  function scaffoldTokens(cwd = process.cwd()) {
2041
- const libDir = path3.resolve(cwd, "src/lib");
2042
- const appDir = path3.resolve(cwd, "src/app");
2043
- if (!fs3.existsSync(libDir)) fs3.mkdirSync(libDir, { recursive: true });
2044
- if (!fs3.existsSync(appDir)) fs3.mkdirSync(appDir, { recursive: true });
2045
- const tokensPath = path3.join(libDir, "tokens.ts");
2046
- if (!fs3.existsSync(tokensPath)) {
2047
- fs3.writeFileSync(tokensPath, tokensTemplate, "utf-8");
2048
- }
2049
- const cssPath = path3.join(appDir, "globals.css");
2050
- if (!fs3.existsSync(cssPath)) {
2051
- fs3.writeFileSync(cssPath, cssTemplate, "utf-8");
2332
+ const libDir = path5.resolve(cwd, "src/lib");
2333
+ const appDir = path5.resolve(cwd, "src/app");
2334
+ if (!fs5.existsSync(libDir)) fs5.mkdirSync(libDir, { recursive: true });
2335
+ if (!fs5.existsSync(appDir)) fs5.mkdirSync(appDir, { recursive: true });
2336
+ const tokensPath = path5.join(libDir, "tokens.ts");
2337
+ if (!fs5.existsSync(tokensPath)) {
2338
+ fs5.writeFileSync(tokensPath, tokensTemplate, "utf-8");
2339
+ }
2340
+ const cssPath = path5.join(appDir, "globals.css");
2341
+ if (!fs5.existsSync(cssPath)) {
2342
+ fs5.writeFileSync(cssPath, cssTemplate, "utf-8");
2052
2343
  }
2053
2344
  return { tokensPath, cssPath };
2054
2345
  }
@@ -2058,13 +2349,13 @@ function scaffoldComponent(componentName, cwd = process.cwd()) {
2058
2349
  if (!template) {
2059
2350
  return null;
2060
2351
  }
2061
- const uiDir = path3.resolve(cwd, "src/components/ui");
2062
- if (!fs3.existsSync(uiDir)) {
2063
- fs3.mkdirSync(uiDir, { recursive: true });
2352
+ const uiDir = path5.resolve(cwd, "src/components/ui");
2353
+ if (!fs5.existsSync(uiDir)) {
2354
+ fs5.mkdirSync(uiDir, { recursive: true });
2064
2355
  }
2065
- const targetPath = path3.join(uiDir, template.filename);
2066
- if (!fs3.existsSync(targetPath)) {
2067
- fs3.writeFileSync(targetPath, template.content, "utf-8");
2356
+ const targetPath = path5.join(uiDir, template.filename);
2357
+ if (!fs5.existsSync(targetPath)) {
2358
+ fs5.writeFileSync(targetPath, template.content, "utf-8");
2068
2359
  }
2069
2360
  return targetPath;
2070
2361
  }
@@ -2134,7 +2425,7 @@ var program = new Command();
2134
2425
  program.name("dse").description("Design System Enforcer (DSE) - AST-based Linter and Scaffold Engine").version("1.0.0");
2135
2426
  program.command("check [targetDir]", { isDefault: true }).description("Scan codebase and enforce Design System rules").option("-c, --config <path>", "Path to custom dse.config.js / dse.config.json").option("-f, --format <format>", "Output format: 'stylish' | 'json'", "stylish").option("--strict", "Treat warnings as errors", false).action(async (targetDirArg, options) => {
2136
2427
  try {
2137
- const cwd = targetDirArg ? path4.resolve(process.cwd(), targetDirArg) : process.cwd();
2428
+ const cwd = targetDirArg ? path6.resolve(process.cwd(), targetDirArg) : process.cwd();
2138
2429
  const config = await loadConfig(cwd, options.config);
2139
2430
  const result = await runDseScan(config, cwd);
2140
2431
  if (options.format === "json") {
@@ -2154,18 +2445,18 @@ program.command("check [targetDir]", { isDefault: true }).description("Scan code
2154
2445
  program.command("init").description("Initialize DSE configuration and optionally scaffold base primitives").option("--preset <name>", "Preset to use: 'berserk' | 'recommended' | 'python'").option("--with-primitives", "Scaffold full base primitives and tokens suite", false).action((options) => {
2155
2446
  const cwd = process.cwd();
2156
2447
  console.log(pc2.bold(pc2.cyan("\nInitializing Design System Enforcer (DSE)...")));
2157
- const isPythonProject = fs4.existsSync(path4.resolve(cwd, "requirements.txt")) || fs4.existsSync(path4.resolve(cwd, "pyproject.toml")) || fs4.existsSync(path4.resolve(cwd, ".streamlit"));
2448
+ const isPythonProject = fs6.existsSync(path6.resolve(cwd, "requirements.txt")) || fs6.existsSync(path6.resolve(cwd, "pyproject.toml")) || fs6.existsSync(path6.resolve(cwd, ".streamlit"));
2158
2449
  const preset = options.preset || (isPythonProject ? "python" : "berserk");
2159
2450
  const configPath = scaffoldConfig(cwd, preset);
2160
- console.log(pc2.green("\u2714 Created config:"), pc2.bold(path4.relative(cwd, configPath)), pc2.dim(`(preset: ${preset})`));
2161
- const pkgPath = path4.resolve(cwd, "package.json");
2162
- if (fs4.existsSync(pkgPath)) {
2451
+ console.log(pc2.green("\u2714 Created config:"), pc2.bold(path6.relative(cwd, configPath)), pc2.dim(`(preset: ${preset})`));
2452
+ const pkgPath = path6.resolve(cwd, "package.json");
2453
+ if (fs6.existsSync(pkgPath)) {
2163
2454
  try {
2164
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
2455
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2165
2456
  pkg.scripts = pkg.scripts || {};
2166
2457
  if (!pkg.scripts.dse) {
2167
2458
  pkg.scripts.dse = "dse";
2168
- fs4.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2459
+ fs6.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2169
2460
  console.log(pc2.green("\u2714 Added script 'dse' to package.json"));
2170
2461
  }
2171
2462
  } catch (err) {
@@ -2176,7 +2467,7 @@ program.command("init").description("Initialize DSE configuration and optionally
2176
2467
  console.log(pc2.cyan("Scaffolding primitives and theme tokens..."));
2177
2468
  const created = scaffoldAllPrimitives(cwd);
2178
2469
  created.forEach((file) => {
2179
- console.log(pc2.green("\u2714 Created:"), pc2.dim(path4.relative(cwd, file)));
2470
+ console.log(pc2.green("\u2714 Created:"), pc2.dim(path6.relative(cwd, file)));
2180
2471
  });
2181
2472
  } else {
2182
2473
  console.log(
@@ -2191,7 +2482,7 @@ program.command("add <components...>").description("Scaffold specific rule-compl
2191
2482
  for (const comp of components) {
2192
2483
  const filePath = scaffoldComponent(comp, cwd);
2193
2484
  if (filePath) {
2194
- console.log(pc2.green("\u2714 Created:"), pc2.bold(path4.relative(cwd, filePath)));
2485
+ console.log(pc2.green("\u2714 Created:"), pc2.bold(path6.relative(cwd, filePath)));
2195
2486
  } else {
2196
2487
  console.log(
2197
2488
  pc2.yellow(`\u26A0 Unknown component '${comp}'. Available: ${Object.keys(COMPONENT_TEMPLATES).join(", ")}`)
@@ -2205,7 +2496,7 @@ program.command("scaffold").description("Scaffold all base primitives and HSL to
2205
2496
  console.log(pc2.bold(pc2.cyan("\nScaffolding full design system suite...")));
2206
2497
  const created = scaffoldAllPrimitives(cwd);
2207
2498
  created.forEach((file) => {
2208
- console.log(pc2.green("\u2714 Created:"), pc2.bold(path4.relative(cwd, file)));
2499
+ console.log(pc2.green("\u2714 Created:"), pc2.bold(path6.relative(cwd, file)));
2209
2500
  });
2210
2501
  console.log(pc2.bold(pc2.green("\n\u2714 All design system primitives scaffolded successfully!\n")));
2211
2502
  });
@@ -2214,9 +2505,9 @@ program.command("export-rules").description("Export rules in Markdown format for
2214
2505
  const config = await loadConfig(cwd);
2215
2506
  const rulesMd = generateMarkdownRules(config);
2216
2507
  if (options.write) {
2217
- const targetFile = path4.resolve(cwd, options.write);
2218
- if (fs4.existsSync(targetFile)) {
2219
- let existing = fs4.readFileSync(targetFile, "utf-8");
2508
+ const targetFile = path6.resolve(cwd, options.write);
2509
+ if (fs6.existsSync(targetFile)) {
2510
+ let existing = fs6.readFileSync(targetFile, "utf-8");
2220
2511
  const startTag = "<!-- BEGIN:design-system-enforcer-rules -->";
2221
2512
  const endTag = "<!-- END:design-system-enforcer-rules -->";
2222
2513
  if (existing.includes(startTag) && existing.includes(endTag)) {
@@ -2228,15 +2519,34 @@ program.command("export-rules").description("Export rules in Markdown format for
2228
2519
  ${rulesMd.trim()}
2229
2520
  `;
2230
2521
  }
2231
- fs4.writeFileSync(targetFile, existing, "utf-8");
2522
+ fs6.writeFileSync(targetFile, existing, "utf-8");
2232
2523
  console.log(pc2.green(`\u2714 Updated rules in ${options.write}`));
2233
2524
  } else {
2234
- fs4.writeFileSync(targetFile, rulesMd, "utf-8");
2525
+ fs6.writeFileSync(targetFile, rulesMd, "utf-8");
2235
2526
  console.log(pc2.green(`\u2714 Created ${options.write} with design system rules`));
2236
2527
  }
2237
2528
  } else {
2238
2529
  console.log(rulesMd);
2239
2530
  }
2240
2531
  });
2532
+ program.command("detect [targetDir]").description("Inspect project architecture, framework, libraries, and design tokens autonomously").action((targetDirArg) => {
2533
+ const cwd = targetDirArg ? path6.resolve(process.cwd(), targetDirArg) : process.cwd();
2534
+ const info = detectProject(cwd);
2535
+ console.log(pc2.bold(pc2.cyan("\n\u{1F50D} DSE Autonomous Project Diagnostics\n")));
2536
+ console.log(` ${pc2.bold("Target Directory:")} ${info.root}`);
2537
+ console.log(` ${pc2.bold("Framework:")} ${pc2.green(info.framework)}`);
2538
+ console.log(` ${pc2.bold("Monorepo:")} ${info.isMonorepo ? pc2.yellow("Yes") : "No"}`);
2539
+ if (info.isMonorepo && info.monorepoWorkspaces.length > 0) {
2540
+ console.log(` ${pc2.bold("Workspaces:")} ${info.monorepoWorkspaces.join(", ")}`);
2541
+ }
2542
+ console.log(` ${pc2.bold("Libraries:")} ${info.libraries.length ? pc2.magenta(info.libraries.join(", ")) : "None"}`);
2543
+ console.log(` ${pc2.bold("Tailwind CSS:")} ${info.hasTailwind ? pc2.green(info.tailwindVersion || "Yes") : "No"}`);
2544
+ console.log(` ${pc2.bold("TypeScript:")} ${info.hasTypeScript ? "Yes" : "No"}`);
2545
+ console.log(` ${pc2.bold("Tokens File:")} ${info.tokensFile ? pc2.cyan(info.tokensFile) : pc2.dim("None (using preset defaults)")}`);
2546
+ console.log(` ${pc2.bold("Default Preset:")} ${pc2.yellow(info.preset)}`);
2547
+ console.log(` ${pc2.bold("Auto Includes:")} ${info.suggestedIncludes.join(", ")}`);
2548
+ console.log(` ${pc2.bold("Auto Ignores:")} ${info.suggestedIgnores.slice(0, 5).join(", ")} (+${Math.max(0, info.suggestedIgnores.length - 5)} more)`);
2549
+ console.log("");
2550
+ });
2241
2551
  program.parse(process.argv);
2242
2552
  //# sourceMappingURL=cli.js.map