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.
- package/AGENTS.md +145 -80
- package/README.md +87 -73
- package/components.manifest.json +1326 -1267
- package/contract.json +226 -168
- package/dist/hooks/use-ds-messages.d.ts +36 -0
- package/dist/hooks/use-ds-messages.js +12 -0
- package/dist/hooks/use-ds-motion.d.ts +17 -0
- package/dist/hooks/use-ds-motion.js +50 -0
- package/dist/lib/i18n.d.ts +46 -0
- package/dist/lib/i18n.js +109 -0
- package/dist/lib/motion.d.ts +74 -0
- package/dist/lib/motion.js +97 -0
- package/dist/styles.css +876 -528
- package/dist/themes/friendly.css +362 -318
- package/dist/themes/friendly.meta.json +75 -14
- package/dist/themes/friendly.scoped.css +362 -318
- package/dist/themes/qratia.meta.json +42 -12
- package/dist/themes/tactile.css +575 -429
- package/dist/themes/tactile.fonts.css +3 -2
- package/dist/themes/tactile.guide.md +82 -49
- package/dist/themes/tactile.meta.json +175 -16
- package/dist/themes/tactile.scoped.css +575 -429
- package/dist/themes/twitter.css +354 -298
- package/dist/themes/twitter.meta.json +83 -61
- package/dist/themes/twitter.scoped.css +354 -298
- package/dist/themes/zyro.css +443 -389
- package/dist/themes/zyro.meta.json +67 -14
- package/dist/themes/zyro.scoped.css +443 -389
- package/dist/ui/accordion.js +5 -5
- package/dist/ui/alert-dialog.js +3 -3
- package/dist/ui/alert.d.ts +1 -1
- package/dist/ui/alert.js +9 -3
- package/dist/ui/attachment.d.ts +1 -1
- package/dist/ui/attachment.js +4 -4
- package/dist/ui/avatar.js +6 -6
- package/dist/ui/ayuda.js +3 -2
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/badge.js +9 -3
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +88 -0
- package/dist/ui/breadcrumb.js +7 -3
- package/dist/ui/bubble.js +6 -6
- package/dist/ui/button-group.js +4 -4
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/button.js +5 -5
- package/dist/ui/calendar.js +33 -6
- package/dist/ui/card.js +4 -4
- package/dist/ui/carousel.js +15 -8
- package/dist/ui/chart.js +7 -3
- package/dist/ui/checkbox.js +1 -1
- package/dist/ui/combobox.js +7 -7
- package/dist/ui/command.js +13 -11
- package/dist/ui/context-menu.js +8 -8
- package/dist/ui/dialog.js +10 -6
- package/dist/ui/drawer.js +9 -9
- package/dist/ui/dropdown-menu.js +8 -8
- package/dist/ui/empty.js +3 -3
- package/dist/ui/field.js +2 -2
- package/dist/ui/hover-card.js +1 -1
- package/dist/ui/i18n-provider.d.ts +11 -0
- package/dist/ui/i18n-provider.js +18 -0
- package/dist/ui/icon-tile.d.ts +1 -1
- package/dist/ui/input-group.d.ts +1 -1
- package/dist/ui/input-group.js +11 -9
- package/dist/ui/input-otp.js +3 -3
- package/dist/ui/input.js +1 -1
- package/dist/ui/item.d.ts +1 -1
- package/dist/ui/item.js +3 -3
- package/dist/ui/kbd.js +1 -1
- package/dist/ui/label.js +1 -1
- package/dist/ui/marker.d.ts +1 -1
- package/dist/ui/marker.js +2 -2
- package/dist/ui/menubar.js +9 -9
- package/dist/ui/message-scroller.js +5 -2
- package/dist/ui/message.js +2 -2
- package/dist/ui/motion-provider.d.ts +11 -0
- package/dist/ui/motion-provider.js +16 -0
- package/dist/ui/native-select.js +4 -4
- package/dist/ui/navigation-menu.js +7 -6
- package/dist/ui/pagination.js +19 -11
- package/dist/ui/popover.js +2 -2
- package/dist/ui/progress.js +2 -2
- package/dist/ui/questionnaire.js +20 -15
- package/dist/ui/radio-group.js +2 -2
- package/dist/ui/resizable.js +2 -2
- package/dist/ui/scroll-area.js +3 -3
- package/dist/ui/select.js +6 -6
- package/dist/ui/separator.js +1 -1
- package/dist/ui/sheet.js +6 -4
- package/dist/ui/sidebar.js +42 -30
- package/dist/ui/skeleton.js +1 -1
- package/dist/ui/slider.js +6 -6
- package/dist/ui/sonner.js +25 -8
- package/dist/ui/spinner.js +4 -1
- package/dist/ui/switch.js +2 -2
- package/dist/ui/table.js +3 -3
- package/dist/ui/tabs.js +5 -5
- package/dist/ui/textarea.js +1 -1
- package/dist/ui/toast.js +15 -12
- package/dist/ui/toggle-group.js +2 -2
- package/dist/ui/toggle.js +6 -4
- package/dist/ui/tooltip.js +2 -2
- package/docs/CONTRACT.md +183 -153
- package/package.json +160 -142
- package/scripts/brand-core.mjs +153 -0
- package/scripts/doctor.mjs +128 -7
- package/scripts/i18n.mjs +111 -0
- package/scripts/init.mjs +294 -57
- package/scripts/setup.mjs +99 -0
package/package.json
CHANGED
|
@@ -1,142 +1,160 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "orbital-ds",
|
|
3
|
-
"version": "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
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"./
|
|
36
|
-
"./
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
"optional": true
|
|
91
|
-
},
|
|
92
|
-
"
|
|
93
|
-
"optional": true
|
|
94
|
-
},
|
|
95
|
-
"
|
|
96
|
-
"optional": true
|
|
97
|
-
},
|
|
98
|
-
"react
|
|
99
|
-
"optional": true
|
|
100
|
-
},
|
|
101
|
-
"
|
|
102
|
-
"optional": true
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"react":
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"react
|
|
128
|
-
"react-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
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
|
+
|
package/scripts/doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
196
|
-
|
|
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
|
|
199
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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)' },
|