orbital-ds 0.7.0 → 0.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.
Files changed (109) hide show
  1. package/AGENTS.md +145 -80
  2. package/README.md +87 -73
  3. package/components.manifest.json +1326 -1267
  4. package/contract.json +226 -168
  5. package/dist/hooks/use-ds-messages.d.ts +36 -0
  6. package/dist/hooks/use-ds-messages.js +12 -0
  7. package/dist/hooks/use-ds-motion.d.ts +17 -0
  8. package/dist/hooks/use-ds-motion.js +50 -0
  9. package/dist/lib/i18n.d.ts +46 -0
  10. package/dist/lib/i18n.js +109 -0
  11. package/dist/lib/motion.d.ts +74 -0
  12. package/dist/lib/motion.js +97 -0
  13. package/dist/styles.css +876 -528
  14. package/dist/themes/friendly.css +362 -318
  15. package/dist/themes/friendly.meta.json +75 -14
  16. package/dist/themes/friendly.scoped.css +362 -318
  17. package/dist/themes/qratia.meta.json +42 -12
  18. package/dist/themes/tactile.css +575 -429
  19. package/dist/themes/tactile.fonts.css +3 -2
  20. package/dist/themes/tactile.guide.md +82 -49
  21. package/dist/themes/tactile.meta.json +175 -16
  22. package/dist/themes/tactile.scoped.css +575 -429
  23. package/dist/themes/twitter.css +354 -298
  24. package/dist/themes/twitter.meta.json +83 -61
  25. package/dist/themes/twitter.scoped.css +354 -298
  26. package/dist/themes/zyro.css +443 -389
  27. package/dist/themes/zyro.meta.json +67 -14
  28. package/dist/themes/zyro.scoped.css +443 -389
  29. package/dist/ui/accordion.js +5 -5
  30. package/dist/ui/alert-dialog.js +3 -3
  31. package/dist/ui/alert.d.ts +1 -1
  32. package/dist/ui/alert.js +9 -3
  33. package/dist/ui/attachment.d.ts +1 -1
  34. package/dist/ui/attachment.js +4 -4
  35. package/dist/ui/avatar.js +6 -6
  36. package/dist/ui/ayuda.js +3 -2
  37. package/dist/ui/badge.d.ts +1 -1
  38. package/dist/ui/badge.js +9 -3
  39. package/dist/ui/banner.d.ts +14 -0
  40. package/dist/ui/banner.js +88 -0
  41. package/dist/ui/breadcrumb.js +7 -3
  42. package/dist/ui/bubble.js +6 -6
  43. package/dist/ui/button-group.js +4 -4
  44. package/dist/ui/button.d.ts +1 -1
  45. package/dist/ui/button.js +5 -5
  46. package/dist/ui/calendar.js +33 -6
  47. package/dist/ui/card.js +4 -4
  48. package/dist/ui/carousel.js +15 -8
  49. package/dist/ui/chart.js +7 -3
  50. package/dist/ui/checkbox.js +1 -1
  51. package/dist/ui/combobox.js +7 -7
  52. package/dist/ui/command.js +13 -11
  53. package/dist/ui/context-menu.js +8 -8
  54. package/dist/ui/dialog.js +10 -6
  55. package/dist/ui/drawer.js +9 -9
  56. package/dist/ui/dropdown-menu.js +8 -8
  57. package/dist/ui/empty.js +3 -3
  58. package/dist/ui/field.js +2 -2
  59. package/dist/ui/hover-card.js +1 -1
  60. package/dist/ui/i18n-provider.d.ts +11 -0
  61. package/dist/ui/i18n-provider.js +18 -0
  62. package/dist/ui/icon-tile.d.ts +1 -1
  63. package/dist/ui/input-group.d.ts +1 -1
  64. package/dist/ui/input-group.js +11 -9
  65. package/dist/ui/input-otp.js +3 -3
  66. package/dist/ui/input.js +1 -1
  67. package/dist/ui/item.d.ts +1 -1
  68. package/dist/ui/item.js +3 -3
  69. package/dist/ui/kbd.js +1 -1
  70. package/dist/ui/label.js +1 -1
  71. package/dist/ui/marker.d.ts +1 -1
  72. package/dist/ui/marker.js +2 -2
  73. package/dist/ui/menubar.js +9 -9
  74. package/dist/ui/message-scroller.js +5 -2
  75. package/dist/ui/message.js +2 -2
  76. package/dist/ui/motion-provider.d.ts +11 -0
  77. package/dist/ui/motion-provider.js +16 -0
  78. package/dist/ui/native-select.js +4 -4
  79. package/dist/ui/navigation-menu.js +7 -6
  80. package/dist/ui/pagination.js +19 -11
  81. package/dist/ui/popover.js +2 -2
  82. package/dist/ui/progress.js +2 -2
  83. package/dist/ui/questionnaire.js +20 -15
  84. package/dist/ui/radio-group.js +2 -2
  85. package/dist/ui/resizable.js +2 -2
  86. package/dist/ui/scroll-area.js +3 -3
  87. package/dist/ui/select.js +6 -6
  88. package/dist/ui/separator.js +1 -1
  89. package/dist/ui/sheet.js +6 -4
  90. package/dist/ui/sidebar.js +42 -30
  91. package/dist/ui/skeleton.js +1 -1
  92. package/dist/ui/slider.js +6 -6
  93. package/dist/ui/sonner.js +25 -8
  94. package/dist/ui/spinner.js +4 -1
  95. package/dist/ui/switch.js +2 -2
  96. package/dist/ui/table.js +3 -3
  97. package/dist/ui/tabs.js +5 -5
  98. package/dist/ui/textarea.js +1 -1
  99. package/dist/ui/toast.js +15 -12
  100. package/dist/ui/toggle-group.js +2 -2
  101. package/dist/ui/toggle.js +6 -4
  102. package/dist/ui/tooltip.js +2 -2
  103. package/docs/CONTRACT.md +183 -153
  104. package/package.json +160 -142
  105. package/scripts/brand-core.mjs +153 -0
  106. package/scripts/doctor.mjs +128 -7
  107. package/scripts/i18n.mjs +111 -0
  108. package/scripts/init.mjs +294 -57
  109. package/scripts/setup.mjs +99 -0
package/package.json CHANGED
@@ -1,142 +1,160 @@
1
- {
2
- "name": "orbital-ds",
3
- "version": "0.7.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
- "contract.json",
23
- "components.manifest.json",
24
- "docs",
25
- "AGENTS.md",
26
- "README.md",
27
- "LICENSE",
28
- "scripts/init.mjs",
29
- "scripts/manifest.mjs"
30
- ],
31
- "exports": {
32
- "./styles.css": "./dist/styles.css",
33
- "./themes/*.css": "./dist/themes/*.css",
34
- "./themes/*.md": "./dist/themes/*.md",
35
- "./themes/*.meta.json": "./dist/themes/*.meta.json",
36
- "./ui/*": {
37
- "types": "./dist/ui/*.d.ts",
38
- "import": "./dist/ui/*.js"
39
- },
40
- "./lib/*": {
41
- "types": "./dist/lib/*.d.ts",
42
- "import": "./dist/lib/*.js"
43
- },
44
- "./hooks/*": {
45
- "types": "./dist/hooks/*.d.ts",
46
- "import": "./dist/hooks/*.js"
47
- },
48
- "./package.json": "./package.json",
49
- "./contract.json": "./contract.json",
50
- "./components.manifest.json": "./components.manifest.json"
51
- },
52
- "peerDependencies": {
53
- "@base-ui/react": ">=1.9",
54
- "class-variance-authority": ">=0.7",
55
- "clsx": ">=2",
56
- "lucide-react": ">=0.400",
57
- "react": ">=19",
58
- "react-dom": ">=19",
59
- "tailwind-merge": ">=3",
60
- "tailwindcss": ">=4.1",
61
- "tw-animate-css": ">=1.3",
62
- "next-themes": ">=0.4",
63
- "recharts": ">=3",
64
- "sonner": ">=2",
65
- "@shadcn/react": ">=0.3",
66
- "cmdk": ">=1.1",
67
- "date-fns": ">=4",
68
- "embla-carousel-react": ">=8.6",
69
- "input-otp": ">=1.5",
70
- "react-day-picker": ">=10",
71
- "react-resizable-panels": ">=4"
72
- },
73
- "peerDependenciesMeta": {
74
- "next-themes": {
75
- "optional": true
76
- },
77
- "recharts": {
78
- "optional": true
79
- },
80
- "sonner": {
81
- "optional": true
82
- },
83
- "@shadcn/react": {
84
- "optional": true
85
- },
86
- "cmdk": {
87
- "optional": true
88
- },
89
- "date-fns": {
90
- "optional": true
91
- },
92
- "embla-carousel-react": {
93
- "optional": true
94
- },
95
- "input-otp": {
96
- "optional": true
97
- },
98
- "react-day-picker": {
99
- "optional": true
100
- },
101
- "react-resizable-panels": {
102
- "optional": true
103
- }
104
- },
105
- "devDependencies": {
106
- "@base-ui/react": "^1.9.0",
107
- "@types/react": "^19.2.0",
108
- "@types/react-dom": "^19.2.0",
109
- "class-variance-authority": "^0.7.1",
110
- "clsx": "^2.1.1",
111
- "lucide-react": "^0.577.0",
112
- "next-themes": "^0.4.6",
113
- "react": "^19.2.0",
114
- "react-dom": "^19.2.0",
115
- "recharts": "3.8.0",
116
- "sonner": "^2.0.8",
117
- "tailwind-merge": "^3.0.2",
118
- "tailwindcss": "^4.1.18",
119
- "tsup": "^8.5.0",
120
- "tw-animate-css": "^1.3.6",
121
- "typescript": "^5.9.0",
122
- "@shadcn/react": "^0.3.1",
123
- "cmdk": "^1.1.1",
124
- "date-fns": "^4.4.0",
125
- "embla-carousel-react": "^8.6.0",
126
- "input-otp": "^1.5.0",
127
- "react-day-picker": "^10.0.2",
128
- "react-resizable-panels": "^4.14.4"
129
- },
130
- "bin": {
131
- "orbital-ds-doctor": "./scripts/doctor.mjs",
132
- "orbital-ds-init": "./scripts/init.mjs"
133
- },
134
- "scripts": {
135
- "build": "tsup && node scripts/build-css.mjs",
136
- "dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
137
- "typecheck": "tsc --noEmit",
138
- "verify": "pnpm check:ds && pnpm typecheck && pnpm build",
139
- "manifest": "node scripts/manifest.mjs",
140
- "check:ds": "node scripts/doctor.mjs"
141
- }
142
- }
1
+ {
2
+ "name": "orbital-ds",
3
+ "version": "0.9.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
+ "contract.json",
23
+ "components.manifest.json",
24
+ "docs",
25
+ "AGENTS.md",
26
+ "README.md",
27
+ "LICENSE",
28
+ "scripts/init.mjs",
29
+ "scripts/manifest.mjs",
30
+ "scripts/brand-core.mjs",
31
+ "scripts/setup.mjs",
32
+ "scripts/i18n.mjs"
33
+ ],
34
+ "exports": {
35
+ "./styles.css": "./dist/styles.css",
36
+ "./themes/*.css": "./dist/themes/*.css",
37
+ "./themes/*.md": "./dist/themes/*.md",
38
+ "./themes/*.meta.json": "./dist/themes/*.meta.json",
39
+ "./ui/*": {
40
+ "types": "./dist/ui/*.d.ts",
41
+ "import": "./dist/ui/*.js"
42
+ },
43
+ "./lib/*": {
44
+ "types": "./dist/lib/*.d.ts",
45
+ "import": "./dist/lib/*.js"
46
+ },
47
+ "./hooks/*": {
48
+ "types": "./dist/hooks/*.d.ts",
49
+ "import": "./dist/hooks/*.js"
50
+ },
51
+ "./package.json": "./package.json",
52
+ "./contract.json": "./contract.json",
53
+ "./components.manifest.json": "./components.manifest.json"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup && node scripts/build-css.mjs",
57
+ "dev": "tsup --watch --onSuccess \"node scripts/copy-css.mjs\"",
58
+ "typecheck": "tsc --noEmit",
59
+ "verify": "pnpm check:ds && pnpm typecheck && pnpm showcase:typecheck && pnpm build",
60
+ "manifest": "node scripts/manifest.mjs",
61
+ "check:ds": "node scripts/doctor.mjs",
62
+ "showcase": "vite --config showcase/vite.config.ts",
63
+ "showcase:build": "vite build --config showcase/vite.config.ts",
64
+ "showcase:typecheck": "tsc --noEmit -p showcase"
65
+ },
66
+ "peerDependencies": {
67
+ "@base-ui/react": ">=1.9",
68
+ "@shadcn/react": ">=0.3",
69
+ "class-variance-authority": ">=0.7",
70
+ "clsx": ">=2",
71
+ "cmdk": ">=1.1",
72
+ "date-fns": ">=4",
73
+ "embla-carousel-react": ">=8.6",
74
+ "input-otp": ">=1.5",
75
+ "lucide-react": ">=0.400",
76
+ "motion": ">=12",
77
+ "next-themes": ">=0.4",
78
+ "react": ">=19",
79
+ "react-day-picker": ">=10",
80
+ "react-dom": ">=19",
81
+ "react-resizable-panels": ">=4",
82
+ "recharts": ">=3",
83
+ "sonner": ">=2",
84
+ "tailwind-merge": ">=3",
85
+ "tailwindcss": ">=4.1",
86
+ "tw-animate-css": ">=1.3"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "next-themes": {
90
+ "optional": true
91
+ },
92
+ "recharts": {
93
+ "optional": true
94
+ },
95
+ "sonner": {
96
+ "optional": true
97
+ },
98
+ "@shadcn/react": {
99
+ "optional": true
100
+ },
101
+ "cmdk": {
102
+ "optional": true
103
+ },
104
+ "date-fns": {
105
+ "optional": true
106
+ },
107
+ "embla-carousel-react": {
108
+ "optional": true
109
+ },
110
+ "input-otp": {
111
+ "optional": true
112
+ },
113
+ "react-day-picker": {
114
+ "optional": true
115
+ },
116
+ "react-resizable-panels": {
117
+ "optional": true
118
+ },
119
+ "motion": {
120
+ "optional": true
121
+ }
122
+ },
123
+ "devDependencies": {
124
+ "@base-ui/react": "^1.9.0",
125
+ "@shadcn/react": "^0.3.1",
126
+ "@tailwindcss/vite": "^4.3.3",
127
+ "@types/react": "^19.2.0",
128
+ "@types/react-dom": "^19.2.0",
129
+ "@vitejs/plugin-react": "^6.1.2",
130
+ "class-variance-authority": "^0.7.1",
131
+ "clsx": "^2.1.1",
132
+ "cmdk": "^1.1.1",
133
+ "date-fns": "^4.4.0",
134
+ "embla-carousel-react": "^8.6.0",
135
+ "input-otp": "^1.5.0",
136
+ "lucide-react": "^0.577.0",
137
+ "motion": "^14.1.0",
138
+ "next-themes": "^0.4.6",
139
+ "react": "^19.2.0",
140
+ "react-day-picker": "^10.0.2",
141
+ "react-dom": "^19.2.0",
142
+ "react-resizable-panels": "^4.14.4",
143
+ "recharts": "3.8.0",
144
+ "sonner": "^2.0.8",
145
+ "tailwind-merge": "^3.0.2",
146
+ "tailwindcss": "^4.1.18",
147
+ "tsup": "^8.5.0",
148
+ "tw-animate-css": "^1.3.6",
149
+ "typescript": "^5.9.0",
150
+ "vite": "^8.3.4"
151
+ },
152
+ "bin": {
153
+ "orbital-ds-doctor": "./scripts/doctor.mjs",
154
+ "orbital-ds-init": "./scripts/init.mjs",
155
+ "orbital-ds-i18n": "./scripts/i18n.mjs"
156
+ },
157
+ "dependencies": {
158
+ "@clack/prompts": "^1.8.1"
159
+ }
160
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * orbital-ds · núcleo de MARCA: color (OKLCH) y derivación de una marca completa a
3
+ * partir del primario, con comprobación de contraste. Sin dependencias ni APIs de
4
+ * Node: lo usan `orbital-ds-init` (terminal) y el escaparate (navegador), así que
5
+ * los dos calculan exactamente lo mismo.
6
+ */
7
+ /* ═══════════════════════════════ COLOR ═══════════════════════════════════════ */
8
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
9
+ const toGamma = (x) => (x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055);
10
+ const toLinear = (x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4);
11
+
12
+ /** OKLCH → sRGB lineal (sin recortar). */
13
+ function oklchLinear({ l, c, h }) {
14
+ const a = c * Math.cos((h * Math.PI) / 180), b = c * Math.sin((h * Math.PI) / 180);
15
+ const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
16
+ const m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
17
+ const s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
18
+ return [4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_, -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_, -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_];
19
+ }
20
+ const inGamut = (lin) => lin.every((v) => v >= -1e-4 && v <= 1 + 1e-4);
21
+
22
+ /** OKLCH → {r,g,b} 0..255, reduciendo el croma hasta caber en sRGB. */
23
+ function oklchToRgb({ l, c, h }) {
24
+ let cc = c;
25
+ if (!inGamut(oklchLinear({ l, c: cc, h }))) {
26
+ let lo = 0, hi = c;
27
+ for (let i = 0; i < 24; i++) { const mid = (lo + hi) / 2; if (inGamut(oklchLinear({ l, c: mid, h }))) lo = mid; else hi = mid; }
28
+ cc = lo;
29
+ }
30
+ const lin = oklchLinear({ l, c: cc, h }).map(clamp01);
31
+ const [r, g, b] = lin.map((v) => Math.round(255 * toGamma(v)));
32
+ return { r, g, b };
33
+ }
34
+ /** {r,g,b} 0..255 → OKLCH. */
35
+ export function rgbToOklch({ r, g, b }) {
36
+ const [lr, lg, lb] = [r, g, b].map((v) => toLinear(v / 255));
37
+ const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
38
+ const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
39
+ const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
40
+ const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
41
+ const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
42
+ const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
43
+ const c = Math.hypot(A, B);
44
+ let h = (Math.atan2(B, A) * 180) / Math.PI; if (h < 0) h += 360;
45
+ return { l: L, c, h: c < 1e-4 ? 0 : h };
46
+ }
47
+ export const hex = ({ r, g, b }) => '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
48
+ const rgba = ({ r, g, b }, a) => `rgba(${r}, ${g}, ${b}, ${a})`;
49
+ export const ok = (l, c, h) => oklchToRgb({ l, c, h });
50
+
51
+ function lumin({ r, g, b }) { const f = (v) => toLinear(v / 255); return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }
52
+ export function ratio(a, b) { const [x, y] = [lumin(a), lumin(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); }
53
+
54
+ /** Color CSS → {r,g,b}. Acepta #hex, rgb(), hsl() y oklch(). */
55
+ export function parseColor(raw) {
56
+ const v = raw.trim().toLowerCase();
57
+ let m = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
58
+ if (m) { let h = m[1]; if (h.length === 3) h = [...h].map((c) => c + c).join(''); return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) }; }
59
+ m = v.match(/^rgb\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)\s*\)$/);
60
+ if (m) return { r: +m[1], g: +m[2], b: +m[3] };
61
+ m = v.match(/^hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%\s*\)$/);
62
+ if (m) {
63
+ const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100;
64
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
65
+ const f = (t) => { t = (t + 1) % 1; return t < 1 / 6 ? p + (q - p) * 6 * t : t < 1 / 2 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p; };
66
+ return { r: Math.round(255 * f(h + 1 / 3)), g: Math.round(255 * f(h)), b: Math.round(255 * f(h - 1 / 3)) };
67
+ }
68
+ m = v.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)\s*\)$/);
69
+ if (m) return oklchToRgb({ l: m[2] ? +m[1] / 100 : +m[1], c: +m[3], h: +m[4] });
70
+ return null;
71
+ }
72
+
73
+ /* ═══════════════════════════════ DERIVACIÓN ══════════════════════════════════ */
74
+ /** Ajusta la luminosidad de `color` (OKLCH) en la dirección dada hasta que `bien(rgb)` sea cierto. */
75
+ function ajustar(color, dir, bien) {
76
+ const o = rgbToOklch(color);
77
+ for (let i = 0; i <= 80; i++) {
78
+ const c = oklchToRgb({ ...o, l: clamp01(o.l + dir * 0.01 * i) });
79
+ if (bien(c)) return c;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Deriva la marca. Devuelve { claro, oscuro, avisos } con los tokens (sin el prefijo --).
86
+ * `acento` y `marcador` solo si el tema tiene gestos.
87
+ */
88
+ export function derivar(primario, { acento, marcador = true, gestos = false }) {
89
+ const avisos = [];
90
+ const P = rgbToOklch(primario);
91
+ const H = P.c < 0.02 ? 255 : P.h; // primario casi gris: neutros azulados por defecto
92
+ const tinte = Math.min(0.02, 0.008 + P.c * 0.05);
93
+
94
+ /* ── piel clara ── */
95
+ const papel = ok(0.965, tinte, H), tinta = ok(0.235, 0.05, H), blanco = { r: 255, g: 255, b: 255 };
96
+ const suave = ok(0.925, tinte + 0.008, H);
97
+ let prim = primario;
98
+ const textoSobre = (c) => (ratio(blanco, c) >= ratio(tinta, c) ? blanco : tinta);
99
+ if (ratio(textoSobre(prim), prim) < 4.5) {
100
+ const m = ajustar(prim, -1, (c) => ratio(blanco, c) >= 4.5);
101
+ if (m) { avisos.push(`primario claro ajustado a ${hex(m)} para que el texto sobre él llegue a 4.5:1 (era ${hex(prim)})`); prim = m; }
102
+ }
103
+ const primFg = textoSobre(prim);
104
+ const enlace = ajustar(prim, -1, (c) => ratio(c, papel) >= 4.5 && ratio(c, blanco) >= 4.5) ?? prim;
105
+ const enlaceHover = ajustar(enlace, -1, (c) => rgbToOklch(c).l <= rgbToOklch(enlace).l - 0.07) ?? enlace;
106
+ const brillo = oklchToRgb({ ...rgbToOklch(prim), l: Math.min(0.95, rgbToOklch(prim).l + 0.08) });
107
+
108
+ const acentoC = acento ?? ok(0.82, 0.16, (H + 180) % 360);
109
+ const claro = {
110
+ primary: hex(prim), 'primary-foreground': hex(primFg), ring: hex(prim), link: hex(enlace), 'link-hover': hex(enlaceHover),
111
+ brand: hex(prim), 'brand-bright': hex(brillo), 'sidebar-primary': hex(prim), 'sidebar-primary-foreground': hex(primFg), 'sidebar-ring': hex(prim),
112
+ 'chart-1': hex(prim), 'accent-foreground': hex(tinta),
113
+ background: hex(papel), foreground: hex(tinta), card: '#ffffff', 'card-foreground': hex(tinta), popover: '#ffffff', 'popover-foreground': hex(tinta),
114
+ secondary: hex(suave), 'secondary-foreground': hex(tinta), muted: hex(suave), 'muted-foreground': rgba(tinta, 0.72),
115
+ border: rgba(tinta, 0.14), input: hex(tinta),
116
+ sidebar: hex(suave), 'sidebar-foreground': hex(tinta), 'sidebar-accent': '#ffffff', 'sidebar-accent-foreground': hex(tinta), 'sidebar-border': rgba(tinta, 0.26),
117
+ inset: hex(suave), 'inset-foreground': hex(tinta), neutral: rgba(tinta, 0.72), 'neutral-soft': rgba(tinta, 0.08),
118
+ };
119
+ if (gestos) {
120
+ claro['ds-key-outline'] = hex(tinta); claro['ds-key-edge'] = rgba(tinta, 0.26);
121
+ claro['ds-marker'] = marcador ? rgba(acentoC, 0.62) : 'transparent';
122
+ }
123
+
124
+ /* ── piel oscura ── */
125
+ const fondoO = ok(0.19, 0.03, H), tarjetaO = ok(0.25, 0.04, H), suaveO = ok(0.22, 0.035, H), textoO = ok(0.95, 0.012, H);
126
+ let primO = oklchToRgb({ ...P, l: Math.max(P.l, 0.74), c: Math.min(P.c, 0.2) });
127
+ const primOFg = fondoO;
128
+ if (ratio(primOFg, primO) < 4.5) primO = ajustar(primO, +1, (c) => ratio(primOFg, c) >= 4.5) ?? primO;
129
+ const enlaceO = ajustar(primO, +1, (c) => ratio(c, fondoO) >= 4.5) ?? primO;
130
+ const enlaceHoverO = oklchToRgb({ ...rgbToOklch(enlaceO), l: Math.min(0.95, rgbToOklch(enlaceO).l + 0.07) });
131
+ const oscuro = {
132
+ primary: hex(primO), 'primary-foreground': hex(primOFg), ring: hex(primO), link: hex(enlaceO), 'link-hover': hex(enlaceHoverO),
133
+ brand: hex(primO), 'brand-bright': hex(enlaceO), 'sidebar-primary': hex(primO), 'sidebar-primary-foreground': hex(primOFg), 'sidebar-ring': hex(primO),
134
+ 'chart-1': hex(primO), 'accent-foreground': hex(textoO),
135
+ background: hex(fondoO), foreground: hex(textoO), card: hex(tarjetaO), 'card-foreground': hex(textoO), popover: hex(tarjetaO), 'popover-foreground': hex(textoO),
136
+ secondary: hex(suaveO), 'secondary-foreground': hex(textoO), muted: hex(suaveO), 'muted-foreground': rgba(textoO, 0.72),
137
+ border: rgba(textoO, 0.14), input: rgba(textoO, 0.92),
138
+ sidebar: hex(suaveO), 'sidebar-foreground': hex(textoO), 'sidebar-accent': hex(tarjetaO), 'sidebar-accent-foreground': hex(textoO), 'sidebar-border': rgba(textoO, 0.26),
139
+ inset: hex(suaveO), 'inset-foreground': hex(textoO), neutral: rgba(textoO, 0.72), 'neutral-soft': rgba(textoO, 0.1),
140
+ };
141
+ if (gestos) {
142
+ oscuro['ds-key-outline'] = rgba(textoO, 0.92); oscuro['ds-key-edge'] = rgba(textoO, 0.26);
143
+ oscuro['ds-marker'] = marcador ? rgba(acentoC, 0.45) : 'transparent';
144
+ }
145
+
146
+ /* ── comprobación ── */
147
+ const pares = [
148
+ ['claro', 'texto/papel', tinta, papel], ['claro', 'texto/tarjeta', tinta, blanco], ['claro', 'texto sobre primario', primFg, prim],
149
+ ['claro', 'enlace/papel', enlace, papel], ['oscuro', 'texto/fondo', textoO, fondoO], ['oscuro', 'texto sobre primario', primOFg, primO], ['oscuro', 'enlace/fondo', enlaceO, fondoO],
150
+ ].map(([piel, nombre, f, b]) => ({ piel, nombre, r: ratio(f, b) }));
151
+ return { claro, oscuro, avisos, pares, muestras: { papel, tinta, prim, suave, acentoC, fondoO, textoO, primO } };
152
+ }
153
+
@@ -14,6 +14,9 @@
14
14
  * duraciones ni sombras a mano; todo sale de tokens.
15
15
  * 6. API PÚBLICA: no desaparece ningún componente ni export respecto a
16
16
  * components.manifest.json (huella de la última versión). Lo nuevo solo avisa.
17
+ * 7. PERSONALIDAD: las reglas de un tema solo usan variables y estilizan familias
18
+ * completas de piezas (contract.slotFamilies). Error si la ficha del tema dice
19
+ * `"conformance": "strict"`; aviso en los demás.
17
20
  *
18
21
  * Uso:
19
22
  * node scripts/doctor.mjs # temas y componentes del paquete
@@ -192,11 +195,48 @@ for (const f of themeFiles) {
192
195
  for (const mm of css.slice(i, j - 1).matchAll(/--([\w-]+):\s*([^;]+);/g)) out[mm[1]] = mm[2];
193
196
  return out;
194
197
  };
195
- for (const f of themeFiles) {
196
- const tema = f.replace(/\.css$/, '');
198
+ // Qué se mide: cada tema y, con `--css`, la marca/variante de la app ENCIMA de su tema base
199
+ // (`--theme <t>` o un comentario `@base-theme <t>` en el fichero).
200
+ const objetivos = themeFiles.map((f) => {
197
201
  const css = stripComments(read(join(themesDir, f)));
198
- const claro = varsDe(css, ':root');
199
- for (const [piel, vars] of [['claro', claro], ['oscuro', { ...claro, ...varsDe(css, '\\.dark') }]]) {
202
+ const tema = f.replace(/\.css$/, '');
203
+ return { tema, etiqueta: `tema ${tema}`, claro: varsDe(css, ':root'), oscuro: varsDe(css, '\\.dark') };
204
+ });
205
+ const iCss = process.argv.indexOf('--css');
206
+ if (iCss > -1 && process.argv[iCss + 1]) {
207
+ const crudo = read(resolve(process.argv[iCss + 1]));
208
+ const iTheme = process.argv.indexOf('--theme');
209
+ const base = (iTheme > -1 && process.argv[iTheme + 1]) || crudo.match(/@base-theme\s+([\w-]+)/)?.[1] || null;
210
+ const t = base && objetivos.find((o) => o.tema === base);
211
+ const app = stripComments(crudo);
212
+ if (t) {
213
+ objetivos.push({
214
+ tema: base,
215
+ etiqueta: `marca ${process.argv[iCss + 1]} (sobre ${base})`,
216
+ claro: { ...t.claro, ...varsDe(app, ':root') },
217
+ oscuro: { ...t.oscuro, ...varsDe(app, '\\.dark') },
218
+ });
219
+ } else {
220
+ warnings.push('app css: no sé sobre qué tema medir el contraste; pasa --theme <tema> o pon «@base-theme <tema>» en el fichero');
221
+ }
222
+ }
223
+ for (const { tema, etiqueta, claro, oscuro } of objetivos) {
224
+ // Resuelve `var(--x)` (y `var(--x, respaldo)`) dentro del mismo tema: un color derivado
225
+ // de otro (p. ej. `--sidebar-primary: var(--primary)`) también se mide.
226
+ const resolver = (vars) => {
227
+ const out = {};
228
+ for (const [k, v0] of Object.entries(vars)) {
229
+ let v = v0.trim();
230
+ for (let n = 0; n < 8; n++) {
231
+ const m = v.match(/^var\(--([\w-]+)(?:\s*,\s*([\s\S]+))?\)$/);
232
+ if (!m) break;
233
+ v = (vars[m[1]] ?? m[2] ?? '').trim();
234
+ }
235
+ out[k] = v;
236
+ }
237
+ return out;
238
+ };
239
+ for (const [piel, vars] of [['claro', resolver(claro)], ['oscuro', resolver({ ...claro, ...oscuro })]]) {
200
240
  const lienzo = parseColor(vars.background ?? '#fff');
201
241
  if (!lienzo) continue;
202
242
  for (const estado of ['info', 'danger', 'warning', 'success', 'neutral']) {
@@ -205,7 +245,7 @@ for (const f of themeFiles) {
205
245
  if (!fg || !suave) continue;
206
246
  const fondo = over(suave, over(lienzo, { r: 255, g: 255, b: 255, a: 1 }));
207
247
  const r = ratio(over(fg, fondo), fondo);
208
- if (r < 4.5) errors.push(`tema ${tema} (${piel}): ${estado} sobre ${estado}-soft = ${r.toFixed(2)}:1 (mínimo 4.5:1)`);
248
+ if (r < 4.5) errors.push(`${etiqueta} (${piel}): ${estado} sobre ${estado}-soft = ${r.toFixed(2)}:1 (mínimo 4.5:1)`);
209
249
  }
210
250
 
211
251
  // Pares principales de texto sobre su superficie. AA (4.5:1) es el mínimo exigible; las
@@ -229,11 +269,11 @@ for (const f of themeFiles) {
229
269
  const r = ratio(over(fgRaw, bg), bg);
230
270
  const par = `${fgName}/${bgName}`;
231
271
  if (r < 4.5) {
232
- const msg = `tema ${tema} (${piel}): ${par} = ${r.toFixed(2)}:1 (mínimo AA 4.5:1)`;
272
+ const msg = `${etiqueta} (${piel}): ${par} = ${r.toFixed(2)}:1 (mínimo AA 4.5:1)`;
233
273
  if (excepciones.includes(par)) warnings.push(`${msg} — excepción declarada en la ficha`);
234
274
  else errors.push(`${msg}; corrígelo o decláralo en ${tema}.meta.json → a11y.exceptions`);
235
275
  } else if (process.argv.includes('--aaa') && r < 7 && (par === 'foreground/background' || par === 'card-foreground/card')) {
236
- warnings.push(`tema ${tema} (${piel}): ${par} = ${r.toFixed(2)}:1 (objetivo AAA 7:1 para el texto del cuerpo)`);
276
+ warnings.push(`${etiqueta} (${piel}): ${par} = ${r.toFixed(2)}:1 (objetivo AAA 7:1 para el texto del cuerpo)`);
237
277
  }
238
278
  }
239
279
  }
@@ -254,6 +294,79 @@ if (base && themeInfo[base]) {
254
294
  }
255
295
  }
256
296
 
297
+ // 7) PERSONALIDAD: solo variables y familias completas.
298
+ // a) Una regla de personalidad (cualquier propiedad que no sea `--x`) no escribe
299
+ // medidas, colores, pesos, opacidades ni duraciones: pide un hueco a una variable.
300
+ // b) Si estiliza una pieza de una familia (contract.slotFamilies), estiliza todas.
301
+ // Error en los temas con `"conformance": "strict"` en su ficha; aviso en los demás.
302
+ {
303
+ /** Declaraciones de propiedades normales (no `--x`) de un CSS, con su selector. */
304
+ const propertyDecls = (css) => {
305
+ const out = [];
306
+ const stack = [];
307
+ let buf = '';
308
+ for (const ch of css) {
309
+ if (ch === '{') { stack.push(buf.trim()); buf = ''; }
310
+ else if (ch === ';' || ch === '}') {
311
+ const d = buf.trim();
312
+ const m = d.match(/^([a-z-]+)\s*:\s*([\s\S]+)$/i);
313
+ if (stack.length && m && !d.startsWith('--')) out.push({ selector: stack.at(-1), prop: m[1], value: m[2].trim() });
314
+ buf = '';
315
+ if (ch === '}') stack.pop();
316
+ } else buf += ch;
317
+ }
318
+ return out;
319
+ };
320
+ const LITERALES = [
321
+ { re: /(?<![\w.-])\d*\.?\d+(?:px|rem|em|ch|vh|vw)\b/g, why: 'medida' },
322
+ { re: /#[0-9a-f]{3,8}\b/gi, why: 'color' },
323
+ { re: /\b(?:rgba?|hsla?|oklch|oklab)\(/gi, why: 'color' },
324
+ { re: /(?<![\w.-])\d*\.?\d+m?s\b/g, why: 'duración' },
325
+ ];
326
+ const NUMERICAS = { 'font-weight': 'peso', opacity: 'opacidad', 'z-index': 'capa', 'line-height': 'interlineado' };
327
+ const familias = Object.entries(contract.slotFamilies?.families ?? {});
328
+ for (const f of themeFiles) {
329
+ const tema = f.replace(/\.css$/, '');
330
+ let estricto = false;
331
+ try { estricto = JSON.parse(read(join(themesDir, `${tema}.meta.json`))).conformance === 'strict'; } catch { /* lo avisa 1b */ }
332
+ const report = (msg) => (estricto ? errors : warnings).push(`tema ${tema}: ${msg}`);
333
+ const decls = propertyDecls(stripComments(read(join(themesDir, f))));
334
+
335
+ const vistos = new Set();
336
+ for (const { selector, prop, value } of decls) {
337
+ const sinVar = value.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, '');
338
+ const hits = LITERALES.flatMap((r) => (sinVar.match(r.re) ?? []).map((h) => `${r.why} ${h}`));
339
+ if (NUMERICAS[prop] && /^\d*\.?\d+$/.test(value)) hits.push(`${NUMERICAS[prop]} ${value}`);
340
+ for (const h of hits) {
341
+ const clave = `${selector}|${prop}|${h}`;
342
+ if (vistos.has(clave)) continue;
343
+ vistos.add(clave);
344
+ report(`personalidad con valor fijo (${h}) en «${prop}» de ${selector.replace(/\s+/g, ' ')} — usa una variable`);
345
+ }
346
+ }
347
+
348
+ const estilizados = new Set(decls.flatMap((d) => [...d.selector.matchAll(/data-slot=['"]?([\w-]+)/g)].map((m) => m[1])));
349
+ for (const [familia, slots] of familias) {
350
+ const tiene = slots.filter((s) => estilizados.has(s));
351
+ if (!tiene.length || tiene.length === slots.length) continue;
352
+ const faltan = slots.filter((s) => !estilizados.has(s));
353
+ report(`familia «${familia}» incompleta: estiliza ${tiene.join(', ')} pero no ${faltan.join(', ')}`);
354
+ }
355
+ }
356
+ }
357
+
358
+ // 8) ESCAPARATE: cada componente tiene su demo (showcase/src/demos/<componente>.tsx), para
359
+ // que todo se pueda ver con cada tema. Solo en el repo del paquete (no en una app).
360
+ {
361
+ const demosDir = join(ROOT, 'showcase/src/demos');
362
+ const uiDir8 = join(ROOT, 'src/ui');
363
+ if (existsSync(demosDir) && existsSync(uiDir8)) {
364
+ const demos = new Set(readdirSync(demosDir).filter((n) => n.endsWith('.tsx')).map((n) => n.replace(/\.tsx$/, '')));
365
+ const faltan = readdirSync(uiDir8).filter((n) => n.endsWith('.tsx')).map((n) => n.replace(/\.tsx$/, '')).filter((n) => !demos.has(n));
366
+ if (faltan.length) errors.push(`escaparate: componentes sin demo en showcase/src/demos/ (${faltan.join(', ')})`);
367
+ }
368
+ }
369
+
257
370
  // CSS de una app (opcional)
258
371
  const cssArgIdx = process.argv.indexOf('--css');
259
372
  if (cssArgIdx > -1 && process.argv[cssArgIdx + 1]) {
@@ -278,6 +391,14 @@ const NO_HARDCODE = [
278
391
  { re: /\b(?:rgb|rgba|hsl|hsla|oklch)\((?!from_)/g, why: 'color literal' },
279
392
  { re: /(?<![\w-])z-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'z-index numérico (usa --ds-z-*)' },
280
393
  { re: /(?<![\w-])duration-(?!0(?![\w-]))\d+(?![\w-])/g, why: 'duración fija (usa --ds-motion-*)' },
394
+ { re: /(?<![a-zA-Z0-9.-])\d*\.?\d+m?s(?=[\])_,\s])/g, why: 'duración dentro de un valor arbitrario (usa --ds-motion-*)' },
395
+ { re: /(?<![\w-])ease-\[(?:cubic-bezier|steps)\(/g, why: 'curva de aceleración escrita (usa ease-(--ds-ease-*))' },
396
+ { re: /(?<![\w-])z-\[[^\]]*\d{2,}/g, why: 'capa numérica dentro de un valor arbitrario (usa --ds-z-*)' },
397
+ // i18n: los textos que ve o escucha el usuario salen de useDsMessages() (orbital-ds/lib/i18n).
398
+ { re: /aria-(?:label|roledescription)="[A-Za-z][^"]*"/g, why: 'texto accesible escrito a mano (usa useDsMessages)' },
399
+ { re: /sr-only">[^<{]*[A-Za-z][^<{]*</g, why: 'texto para lector de pantalla escrito a mano (usa useDsMessages)' },
400
+ { re: /\?\?\s*"[A-Z][^"]*"/g, why: 'texto por defecto escrito a mano (usa useDsMessages)' },
401
+ { re: /(?<![\w-])(?:title|description|text|placeholder)\s*=\s*"[A-Z][^"]*"/g, why: 'texto de interfaz escrito a mano (usa useDsMessages)' },
281
402
  { re: /(?<![\w-])(?:bg|text|border)-(?:black|white)(?:\/\d+)?(?![\w-])/g, why: 'color fijo (usa un token de color)' },
282
403
  { re: /(?<![\w-])shadow-(?:xs|sm|md|lg|xl|2xl)(?![\w-])/g, why: 'sombra de la escala Tailwind (usa shadow-card/control/raised/overlay/popover)' },
283
404
  { re: /(?<![\w-])text-\[\d+px\]/g, why: 'tamaño de texto arbitrario (usa la escala estándar)' },