orbital-ds 0.1.1 → 0.3.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.
Files changed (110) hide show
  1. package/AGENTS.md +57 -0
  2. package/README.md +22 -14
  3. package/components.manifest.json +1255 -0
  4. package/contract.json +82 -0
  5. package/dist/styles.css +486 -347
  6. package/dist/themes/qratia.css +14 -14
  7. package/dist/themes/qratia.fonts.css +2 -0
  8. package/dist/themes/qratia.scoped.css +247 -0
  9. package/dist/themes/zyro.css +343 -0
  10. package/dist/themes/zyro.fonts.css +2 -0
  11. package/dist/themes/zyro.scoped.css +343 -0
  12. package/dist/ui/accordion.d.ts +9 -0
  13. package/dist/ui/accordion.js +79 -0
  14. package/dist/ui/alert-dialog.d.ts +23 -0
  15. package/dist/ui/alert-dialog.js +173 -0
  16. package/dist/ui/alert.d.ts +13 -0
  17. package/dist/ui/alert.js +77 -0
  18. package/dist/ui/aspect-ratio.d.ts +7 -0
  19. package/dist/ui/aspect-ratio.js +22 -0
  20. package/dist/ui/attachment.d.ts +26 -0
  21. package/dist/ui/attachment.js +197 -0
  22. package/dist/ui/avatar.js +1 -1
  23. package/dist/ui/badge.d.ts +1 -1
  24. package/dist/ui/badge.js +1 -1
  25. package/dist/ui/bubble.d.ts +19 -0
  26. package/dist/ui/bubble.js +115 -0
  27. package/dist/ui/button-group.d.ts +15 -0
  28. package/dist/ui/button-group.js +82 -0
  29. package/dist/ui/button.d.ts +5 -3
  30. package/dist/ui/button.js +9 -6
  31. package/dist/ui/calendar.d.ts +15 -0
  32. package/dist/ui/calendar.js +190 -0
  33. package/dist/ui/card.js +4 -4
  34. package/dist/ui/carousel.d.ts +33 -0
  35. package/dist/ui/carousel.js +199 -0
  36. package/dist/ui/checkbox.js +1 -1
  37. package/dist/ui/combobox.d.ts +26 -0
  38. package/dist/ui/combobox.js +281 -0
  39. package/dist/ui/command.d.ts +22 -0
  40. package/dist/ui/command.js +183 -0
  41. package/dist/ui/context-menu.d.ts +31 -0
  42. package/dist/ui/context-menu.js +242 -0
  43. package/dist/ui/dialog.js +2 -2
  44. package/dist/ui/direction.d.ts +1 -0
  45. package/dist/ui/direction.js +8 -0
  46. package/dist/ui/drawer.d.ts +18 -0
  47. package/dist/ui/drawer.js +208 -0
  48. package/dist/ui/dropdown-menu.d.ts +1 -1
  49. package/dist/ui/dropdown-menu.js +13 -9
  50. package/dist/ui/empty.d.ts +15 -0
  51. package/dist/ui/empty.js +102 -0
  52. package/dist/ui/field.d.ts +28 -0
  53. package/dist/ui/field.js +217 -0
  54. package/dist/ui/hover-card.d.ts +8 -0
  55. package/dist/ui/hover-card.js +45 -0
  56. package/dist/ui/input-group.d.ts +22 -0
  57. package/dist/ui/input-group.js +145 -0
  58. package/dist/ui/input-otp.d.ts +13 -0
  59. package/dist/ui/input-otp.js +82 -0
  60. package/dist/ui/input.js +1 -1
  61. package/dist/ui/item.d.ts +26 -0
  62. package/dist/ui/item.js +195 -0
  63. package/dist/ui/kbd.d.ts +6 -0
  64. package/dist/ui/kbd.js +29 -0
  65. package/dist/ui/marker.d.ts +13 -0
  66. package/dist/ui/marker.js +71 -0
  67. package/dist/ui/menubar.d.ts +31 -0
  68. package/dist/ui/menubar.js +259 -0
  69. package/dist/ui/message-scroller.d.ts +16 -0
  70. package/dist/ui/message-scroller.js +120 -0
  71. package/dist/ui/message.d.ts +12 -0
  72. package/dist/ui/message.js +90 -0
  73. package/dist/ui/native-select.d.ts +10 -0
  74. package/dist/ui/native-select.js +63 -0
  75. package/dist/ui/navigation-menu.d.ts +15 -0
  76. package/dist/ui/navigation-menu.js +162 -0
  77. package/dist/ui/pagination.d.ts +22 -0
  78. package/dist/ui/pagination.js +128 -0
  79. package/dist/ui/popover.js +2 -2
  80. package/dist/ui/questionnaire.d.ts +24 -0
  81. package/dist/ui/questionnaire.js +331 -0
  82. package/dist/ui/radio-group.d.ts +8 -0
  83. package/dist/ui/radio-group.js +39 -0
  84. package/dist/ui/resizable.d.ts +10 -0
  85. package/dist/ui/resizable.js +46 -0
  86. package/dist/ui/scroll-area.js +1 -1
  87. package/dist/ui/select.js +5 -5
  88. package/dist/ui/sheet.js +4 -4
  89. package/dist/ui/sidebar.js +8 -8
  90. package/dist/ui/slider.d.ts +6 -0
  91. package/dist/ui/slider.js +53 -0
  92. package/dist/ui/spinner.d.ts +5 -0
  93. package/dist/ui/spinner.js +9 -0
  94. package/dist/ui/switch.d.ts +8 -0
  95. package/dist/ui/switch.js +32 -0
  96. package/dist/ui/table.js +2 -2
  97. package/dist/ui/tabs.d.ts +1 -1
  98. package/dist/ui/tabs.js +1 -1
  99. package/dist/ui/textarea.js +1 -1
  100. package/dist/ui/toast.d.ts +19 -0
  101. package/dist/ui/toast.js +193 -0
  102. package/dist/ui/toggle-group.d.ts +14 -0
  103. package/dist/ui/toggle-group.js +77 -0
  104. package/dist/ui/toggle.d.ts +12 -0
  105. package/dist/ui/toggle.js +43 -0
  106. package/dist/ui/tooltip.js +3 -3
  107. package/docs/CONTRACT.md +123 -0
  108. package/package.json +138 -95
  109. package/scripts/doctor.mjs +202 -0
  110. package/scripts/manifest.mjs +79 -0
package/package.json CHANGED
@@ -1,95 +1,138 @@
1
- {
2
- "name": "orbital-ds",
3
- "version": "0.1.1",
4
- "description": "Sistema de diseño genérico: estructura común (tokens semánticos, capa base) + temas intercambiables + primitivos sobre Base UI y Tailwind v4.",
5
- "keywords": [
6
- "design-system",
7
- "design-tokens",
8
- "theme",
9
- "tailwindcss",
10
- "base-ui",
11
- "shadcn",
12
- "react"
13
- ],
14
- "license": "MIT",
15
- "type": "module",
16
- "sideEffects": [
17
- "*.css"
18
- ],
19
- "files": [
20
- "dist",
21
- "README.md",
22
- "LICENSE"
23
- ],
24
- "exports": {
25
- "./styles.css": "./dist/styles.css",
26
- "./themes/*.css": "./dist/themes/*.css",
27
- "./ui/*": {
28
- "types": "./dist/ui/*.d.ts",
29
- "import": "./dist/ui/*.js"
30
- },
31
- "./lib/*": {
32
- "types": "./dist/lib/*.d.ts",
33
- "import": "./dist/lib/*.js"
34
- },
35
- "./hooks/*": {
36
- "types": "./dist/hooks/*.d.ts",
37
- "import": "./dist/hooks/*.js"
38
- },
39
- "./package.json": "./package.json"
40
- },
41
- "scripts": {
42
- "build": "tsup && node scripts/copy-css.mjs",
43
- "dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
44
- "typecheck": "tsc --noEmit",
45
- "verify": "pnpm typecheck && pnpm build"
46
- },
47
- "peerDependencies": {
48
- "@base-ui/react": ">=1.7",
49
- "class-variance-authority": ">=0.7",
50
- "clsx": ">=2",
51
- "lucide-react": ">=0.400",
52
- "react": ">=19",
53
- "react-dom": ">=19",
54
- "tailwind-merge": ">=3",
55
- "tailwindcss": ">=4.1",
56
- "tw-animate-css": ">=1.3",
57
- "next-themes": ">=0.4",
58
- "recharts": ">=3",
59
- "sonner": ">=2"
60
- },
61
- "peerDependenciesMeta": {
62
- "next-themes": {
63
- "optional": true
64
- },
65
- "recharts": {
66
- "optional": true
67
- },
68
- "sonner": {
69
- "optional": true
70
- }
71
- },
72
- "devDependencies": {
73
- "@base-ui/react": "^1.7.0",
74
- "@types/react": "^19.2.0",
75
- "@types/react-dom": "^19.2.0",
76
- "class-variance-authority": "^0.7.1",
77
- "clsx": "^2.1.1",
78
- "lucide-react": "^0.577.0",
79
- "next-themes": "^0.4.6",
80
- "react": "^19.2.0",
81
- "react-dom": "^19.2.0",
82
- "recharts": "3.8.0",
83
- "sonner": "^2.0.8",
84
- "tailwind-merge": "^3.0.2",
85
- "tailwindcss": "^4.1.18",
86
- "tsup": "^8.5.0",
87
- "tw-animate-css": "^1.3.6",
88
- "typescript": "^5.9.0"
89
- },
90
- "pnpm": {
91
- "onlyBuiltDependencies": [
92
- "esbuild"
93
- ]
94
- }
95
- }
1
+ {
2
+ "name": "orbital-ds",
3
+ "version": "0.3.0",
4
+ "description": "Sistema de diseño genérico: estructura común + temas con personalidad (qratia, zyro) + ~60 componentes sobre Base UI y Tailwind v4, con contrato verificable y legible por agentes.",
5
+ "keywords": [
6
+ "design-system",
7
+ "design-tokens",
8
+ "theme",
9
+ "tailwindcss",
10
+ "base-ui",
11
+ "shadcn",
12
+ "react"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "sideEffects": [
17
+ "*.css"
18
+ ],
19
+ "files": [
20
+ "dist",
21
+ "scripts/doctor.mjs",
22
+ "scripts/manifest.mjs",
23
+ "contract.json",
24
+ "components.manifest.json",
25
+ "docs",
26
+ "AGENTS.md",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "exports": {
31
+ "./styles.css": "./dist/styles.css",
32
+ "./themes/*.css": "./dist/themes/*.css",
33
+ "./ui/*": {
34
+ "types": "./dist/ui/*.d.ts",
35
+ "import": "./dist/ui/*.js"
36
+ },
37
+ "./lib/*": {
38
+ "types": "./dist/lib/*.d.ts",
39
+ "import": "./dist/lib/*.js"
40
+ },
41
+ "./hooks/*": {
42
+ "types": "./dist/hooks/*.d.ts",
43
+ "import": "./dist/hooks/*.js"
44
+ },
45
+ "./package.json": "./package.json",
46
+ "./contract.json": "./contract.json",
47
+ "./components.manifest.json": "./components.manifest.json"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup && node scripts/build-css.mjs",
51
+ "dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
52
+ "typecheck": "tsc --noEmit",
53
+ "verify": "pnpm check:ds && pnpm typecheck && pnpm build",
54
+ "manifest": "node scripts/manifest.mjs",
55
+ "check:ds": "node scripts/doctor.mjs"
56
+ },
57
+ "peerDependencies": {
58
+ "@base-ui/react": ">=1.9",
59
+ "class-variance-authority": ">=0.7",
60
+ "clsx": ">=2",
61
+ "lucide-react": ">=0.400",
62
+ "react": ">=19",
63
+ "react-dom": ">=19",
64
+ "tailwind-merge": ">=3",
65
+ "tailwindcss": ">=4.1",
66
+ "tw-animate-css": ">=1.3",
67
+ "next-themes": ">=0.4",
68
+ "recharts": ">=3",
69
+ "sonner": ">=2",
70
+ "@shadcn/react": ">=0.3",
71
+ "cmdk": ">=1.1",
72
+ "date-fns": ">=4",
73
+ "embla-carousel-react": ">=8.6",
74
+ "input-otp": ">=1.5",
75
+ "react-day-picker": ">=10",
76
+ "react-resizable-panels": ">=4"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "next-themes": {
80
+ "optional": true
81
+ },
82
+ "recharts": {
83
+ "optional": true
84
+ },
85
+ "sonner": {
86
+ "optional": true
87
+ },
88
+ "@shadcn/react": {
89
+ "optional": true
90
+ },
91
+ "cmdk": {
92
+ "optional": true
93
+ },
94
+ "date-fns": {
95
+ "optional": true
96
+ },
97
+ "embla-carousel-react": {
98
+ "optional": true
99
+ },
100
+ "input-otp": {
101
+ "optional": true
102
+ },
103
+ "react-day-picker": {
104
+ "optional": true
105
+ },
106
+ "react-resizable-panels": {
107
+ "optional": true
108
+ }
109
+ },
110
+ "devDependencies": {
111
+ "@base-ui/react": "^1.9.0",
112
+ "@types/react": "^19.2.0",
113
+ "@types/react-dom": "^19.2.0",
114
+ "class-variance-authority": "^0.7.1",
115
+ "clsx": "^2.1.1",
116
+ "lucide-react": "^0.577.0",
117
+ "next-themes": "^0.4.6",
118
+ "react": "^19.2.0",
119
+ "react-dom": "^19.2.0",
120
+ "recharts": "3.8.0",
121
+ "sonner": "^2.0.8",
122
+ "tailwind-merge": "^3.0.2",
123
+ "tailwindcss": "^4.1.18",
124
+ "tsup": "^8.5.0",
125
+ "tw-animate-css": "^1.3.6",
126
+ "typescript": "^5.9.0",
127
+ "@shadcn/react": "^0.3.1",
128
+ "cmdk": "^1.1.1",
129
+ "date-fns": "^4.4.0",
130
+ "embla-carousel-react": "^8.6.0",
131
+ "input-otp": "^1.5.0",
132
+ "react-day-picker": "^10.0.2",
133
+ "react-resizable-panels": "^4.14.4"
134
+ },
135
+ "bin": {
136
+ "orbital-ds-doctor": "./scripts/doctor.mjs"
137
+ }
138
+ }
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * orbital-ds doctor — comprueba que el sistema se cumple.
4
+ *
5
+ * 1. CONTRATO: cada tema (themes/*.css) define en :root todos los tokens de
6
+ * contract.json y la fuente `--ds-font-sans`. En .dark solo avisa.
7
+ * 2. REPETIDOS: ninguna variable se define dos veces en el mismo bloque.
8
+ * 3. SOMBRAS: «sin sombra» es `0 0 #0000`, nunca `none` (una variable con
9
+ * `none` invalida todo el box-shadow, anillo de foco incluido).
10
+ * 4. FUGAS: el tema base (contract.baseTheme) está aplicado siempre; todo tema
11
+ * con ámbito debe definir cada `--ds-*` que defina el base, o su valor se
12
+ * colaría al cambiar de tema.
13
+ * 5. VALORES FIJOS: ningún componente (ui/*.tsx) escribe colores, z-index,
14
+ * duraciones ni sombras a mano; todo sale de tokens.
15
+ * 6. API PÚBLICA: no desaparece ningún componente ni export respecto a
16
+ * components.manifest.json (huella de la última versión). Lo nuevo solo avisa.
17
+ *
18
+ * Uso:
19
+ * node scripts/doctor.mjs # temas y componentes del paquete
20
+ * node scripts/doctor.mjs --css app.css # además, un CSS de una app (repetidos)
21
+ *
22
+ * Sale con código 1 si hay errores (los avisos no fallan).
23
+ */
24
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
25
+ import { join, dirname, resolve } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
29
+ const contract = JSON.parse(readFileSync(join(ROOT, 'contract.json'), 'utf8'));
30
+
31
+ const errors = [];
32
+ const warnings = [];
33
+ const read = (p) => readFileSync(p, 'utf8');
34
+ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
35
+
36
+ /** Cuerpo de cada bloque `selector { ... }` (respeta llaves anidadas). */
37
+ function blocks(css, selectorRe) {
38
+ const out = [];
39
+ const re = new RegExp(selectorRe.source + '\\s*\\{', 'g');
40
+ let m;
41
+ while ((m = re.exec(css))) {
42
+ let depth = 1;
43
+ let i = re.lastIndex;
44
+ const start = i;
45
+ while (i < css.length && depth > 0) {
46
+ if (css[i] === '{') depth++;
47
+ else if (css[i] === '}') depth--;
48
+ i++;
49
+ }
50
+ out.push(css.slice(start, i - 1));
51
+ re.lastIndex = i;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** Declaraciones `--x: valor;` del nivel superior de un bloque (no las anidadas). */
57
+ function declarations(body) {
58
+ let depth = 0;
59
+ let top = '';
60
+ for (const ch of body) {
61
+ if (ch === '{') depth++;
62
+ else if (ch === '}') depth--;
63
+ else if (depth === 0) top += ch;
64
+ }
65
+ return [...top.matchAll(/(^|[;\s])(--[a-z0-9-]+)\s*:\s*([^;]*);?/gi)].map((m) => ({ name: m[2], value: m[3].trim() }));
66
+ }
67
+
68
+ const themeInfo = {}; // nombre → { rootVars:Set, darkVars:Set }
69
+
70
+ function checkTheme(file, name) {
71
+ const label = `tema ${name}`;
72
+ const css = stripComments(read(file));
73
+ const roots = blocks(css, /(?:^|[\s}])\:root/);
74
+ const darks = blocks(css, /(?:^|[\s}])\.dark/);
75
+
76
+ const rootDecl = roots.flatMap(declarations);
77
+ const darkDecl = darks.flatMap(declarations);
78
+ const rootVars = new Set(rootDecl.map((d) => d.name));
79
+ const darkVars = new Set(darkDecl.map((d) => d.name));
80
+ themeInfo[name] = { rootVars, darkVars };
81
+
82
+ for (const t of contract.root) {
83
+ if (!rootVars.has(`--${t}`)) errors.push(`${label}: falta el token obligatorio --${t} en :root`);
84
+ }
85
+ for (const f of contract.fonts) {
86
+ if (!rootVars.has(`--${f}`)) errors.push(`${label}: falta la fuente --${f} en :root`);
87
+ }
88
+ if (darks.length) {
89
+ for (const t of contract.dark.avisar_si_falta) {
90
+ if (!darkVars.has(`--${t}`)) warnings.push(`${label}: .dark no redefine --${t} (hereda el valor claro)`);
91
+ }
92
+ } else {
93
+ warnings.push(`${label}: no define piel .dark`);
94
+ }
95
+
96
+ // Repetidos (solo declaraciones del nivel superior de cada bloque)
97
+ for (const [kind, list] of [[':root', roots], ['.dark', darks]]) {
98
+ list.forEach((body, idx) => {
99
+ const seen = new Map();
100
+ for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
101
+ for (const [v, n] of seen) {
102
+ if (n > 1) errors.push(`${label}: ${v} definido ${n} veces en el mismo bloque ${kind} (#${idx + 1})`);
103
+ }
104
+ });
105
+ }
106
+
107
+ // Sombras: `none` invalida el box-shadow
108
+ for (const d of [...rootDecl, ...darkDecl]) {
109
+ if (/^--ds-(?:shadow|sidebar-shadow)/.test(d.name) && /^none$/i.test(d.value)) {
110
+ errors.push(`${label}: ${d.name}: none — usa «0 0 #0000» (none invalida todo el box-shadow)`);
111
+ }
112
+ }
113
+ }
114
+
115
+ // 1+2+3) temas
116
+ const themesDir = join(ROOT, existsSync(join(ROOT, 'src/themes')) ? 'src/themes' : 'dist/themes');
117
+ const themeFiles = readdirSync(themesDir).filter(
118
+ (n) => n.endsWith('.css') && !n.endsWith('.fonts.css') && !n.endsWith('.scoped.css'),
119
+ );
120
+ for (const f of themeFiles) checkTheme(join(themesDir, f), f.replace(/\.css$/, ''));
121
+
122
+ // 4) fugas desde el tema base
123
+ const base = contract.baseTheme;
124
+ if (base && themeInfo[base]) {
125
+ const baseDs = [...themeInfo[base].rootVars].filter((v) => v.startsWith('--ds-'));
126
+ for (const [name, info] of Object.entries(themeInfo)) {
127
+ if (name === base) continue;
128
+ for (const v of baseDs) {
129
+ if (!info.rootVars.has(v)) {
130
+ errors.push(`tema ${name}: no define ${v}, que sí define el tema base «${base}» — su valor se colaría al cambiar de tema`);
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // CSS de una app (opcional)
137
+ const cssArgIdx = process.argv.indexOf('--css');
138
+ if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
139
+ const css = stripComments(read(resolve(process.argv[cssArgIdx + 1])));
140
+ blocks(css, /(?:^|[\s}])\:root/).forEach((body, idx) => {
141
+ const seen = new Map();
142
+ for (const d of declarations(body)) seen.set(d.name, (seen.get(d.name) ?? 0) + 1);
143
+ for (const [v, n] of seen) if (n > 1) errors.push(`app css: ${v} definido ${n} veces en :root (#${idx + 1})`);
144
+ });
145
+ }
146
+
147
+ // 5) valores fijos en componentes
148
+ const NO_HARDCODE = [
149
+ { re: /#[0-9a-fA-F]{3,8}\b/g, why: 'color hexadecimal', skip: ['chart.tsx'] },
150
+ { re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
151
+ { re: /(?<![\w-])z-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'z-index numérico (usa --ds-z-*)' },
152
+ { re: /(?<![\w-])duration-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'duración fija (usa --ds-motion-*)' },
153
+ { re: /(?<![\w-])(?:bg|text|border)-(?:black|white)(?:\/\d+)?(?![\w-])/g, why: 'color fijo (usa un token de color)' },
154
+ { re: /(?<![\w-])shadow-(?:xs|sm|md|lg|xl|2xl)(?![\w-])/g, why: 'sombra de la escala Tailwind (usa shadow-card/control/raised/overlay/popover)' },
155
+ { re: /(?<![\w-])text-\[\d+px\]/g, why: 'tamaño de texto arbitrario (usa la escala estándar)' },
156
+ { re: /(?<![\w-])rounded-(?:3xl|4xl)(?![\w-])/g, why: 'radio fuera de la rampa (usa rounded-control/overlay o la rampa sm…2xl)' },
157
+ { re: /(?<![\w-])(?:bg|text|border|ring|fill|stroke)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\d{2,3}(?![\w-])/g, why: 'color de la paleta Tailwind (usa tokens del tema)' },
158
+ ];
159
+
160
+ const uiDir = join(ROOT, 'src/ui');
161
+ if (existsSync(uiDir)) {
162
+ for (const f of readdirSync(uiDir).filter((n) => n.endsWith('.tsx'))) {
163
+ const lines = read(join(uiDir, f)).split(/\r?\n/);
164
+ lines.forEach((line, i) => {
165
+ const code = line.replace(/\/\/.*$/, '');
166
+ for (const rule of NO_HARDCODE) {
167
+ if (rule.skip?.includes(f)) continue;
168
+ const hits = code.match(rule.re);
169
+ if (hits) errors.push(`ui/${f}:${i + 1}: ${rule.why} → ${[...new Set(hits)].join(', ')}`);
170
+ }
171
+ });
172
+ }
173
+ }
174
+
175
+ // 6) API pública frente a la huella commiteada
176
+ const manifestPath = join(ROOT, 'components.manifest.json');
177
+ if (existsSync(manifestPath) && existsSync(uiDir)) {
178
+ const { buildManifest } = await import('./manifest.mjs');
179
+ const before = JSON.parse(read(manifestPath)).components;
180
+ const now = buildManifest().components;
181
+ for (const [name, comp] of Object.entries(before)) {
182
+ if (!now[name]) {
183
+ errors.push(`API pública: desaparece el componente «${name}» (¿versión mayor? regenera con pnpm manifest)`);
184
+ continue;
185
+ }
186
+ for (const e of comp.exports) {
187
+ if (!now[name].exports.includes(e)) {
188
+ errors.push(`API pública: «${name}» ya no exporta ${e} (¿versión mayor? regenera con pnpm manifest)`);
189
+ }
190
+ }
191
+ }
192
+ const added = Object.keys(now).filter((n) => !before[n]);
193
+ if (added.length) warnings.push(`API pública: componentes nuevos sin registrar en el manifiesto (${added.join(', ')}); ejecuta pnpm manifest`);
194
+ }
195
+
196
+ for (const w of warnings) console.log(`AVISO ${w}`);
197
+ for (const e of errors) console.log(`ERROR ${e}`);
198
+ if (errors.length) {
199
+ console.log(`\ndoctor: ${errors.length} error(es), ${warnings.length} aviso(s)`);
200
+ process.exit(1);
201
+ }
202
+ console.log(`doctor: contrato, repetidos, sombras, fugas, valores fijos y API pública correctos (${warnings.length} aviso(s)).`);
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Manifiesto de componentes de orbital-ds (`components.manifest.json`).
4
+ *
5
+ * Lo leen los agentes de IA (qué hay, cómo se importa, qué `data-slot` expone,
6
+ * qué dependencias opcionales pide) y `pnpm check:ds` lo usa como HUELLA de la API
7
+ * pública: si desaparece un componente o un export respecto al manifiesto
8
+ * commiteado, doctor falla salvo que se haga una versión mayor.
9
+ *
10
+ * node scripts/manifest.mjs # regenera components.manifest.json
11
+ */
12
+ import { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';
13
+ import { join, dirname, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
+ const UI = join(ROOT, 'src/ui');
18
+
19
+ // Dependencias que SIEMPRE están (peers obligatorios): no se listan como opcionales.
20
+ const REQUIRED = new Set([
21
+ 'react', 'react-dom', 'class-variance-authority', 'clsx', 'tailwind-merge', 'lucide-react',
22
+ ]);
23
+
24
+ function parseExports(src) {
25
+ const names = new Set();
26
+ for (const m of src.matchAll(/export\s*\{([^}]*)\}/g)) {
27
+ for (const part of m[1].split(',')) {
28
+ const n = part.trim().split(/\s+as\s+/i).pop()?.trim();
29
+ if (n) names.add(n);
30
+ }
31
+ }
32
+ for (const m of src.matchAll(/export\s+(?:async\s+)?(?:function|const|class)\s+([A-Za-z0-9_]+)/g)) names.add(m[1]);
33
+ return [...names].sort();
34
+ }
35
+
36
+ function parseSlots(src) {
37
+ return [...new Set([...src.matchAll(/data-slot="([a-z0-9-]+)"/g)].map((m) => m[1]))].sort();
38
+ }
39
+
40
+ function parseOptionalPeers(src) {
41
+ const peers = new Set();
42
+ for (const m of src.matchAll(/from\s+"([^"]+)"/g)) {
43
+ const spec = m[1];
44
+ if (spec.startsWith('.')) continue;
45
+ const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
46
+ if (pkg === '@base-ui/react' || REQUIRED.has(pkg)) continue;
47
+ peers.add(pkg);
48
+ }
49
+ return [...peers].sort();
50
+ }
51
+
52
+ export function buildManifest() {
53
+ const components = {};
54
+ for (const f of readdirSync(UI).filter((n) => n.endsWith('.tsx')).sort()) {
55
+ const src = readFileSync(join(UI, f), 'utf8');
56
+ const name = f.replace(/\.tsx$/, '');
57
+ components[name] = {
58
+ import: `orbital-ds/ui/${name}`,
59
+ exports: parseExports(src),
60
+ slots: parseSlots(src),
61
+ optionalPeers: parseOptionalPeers(src),
62
+ };
63
+ }
64
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
65
+ return {
66
+ package: pkg.name,
67
+ version: pkg.version,
68
+ engine: '@base-ui/react',
69
+ docs: ['README.md', 'docs/CONTRACT.md', 'AGENTS.md', 'contract.json'],
70
+ components,
71
+ };
72
+ }
73
+
74
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
75
+ const out = join(ROOT, 'components.manifest.json');
76
+ const manifest = buildManifest();
77
+ writeFileSync(out, JSON.stringify(manifest, null, 2) + '\n');
78
+ console.log(`manifest: ${Object.keys(manifest.components).length} componentes → components.manifest.json`);
79
+ }