@vestow/theme 0.1.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/css/tokens.css ADDED
@@ -0,0 +1,627 @@
1
+ /* =============================================================================
2
+ Design tokens — nguồn duy nhất của màu sắc / bo tròn / spacing ngữ nghĩa.
3
+ KHÔNG hardcode màu trong component. Mọi component chỉ dùng utility Tailwind
4
+ trỏ tới các token dưới đây (vd: bg-background, text-muted-foreground).
5
+
6
+ Hệ màu dùng oklch: nội suy đều hơn hsl, chỉnh độ sáng không lệch tone.
7
+
8
+ File này là CSS THUẦN — không `@theme`, không `@custom-variant`, không
9
+ `@apply`. Trình duyệt nào cũng đọc được, framework nào cũng nạp được.
10
+ Phần cầu nối sang Tailwind v4 nằm riêng ở `tailwind.css`; đừng thêm at-rule
11
+ của Tailwind vào đây, nếu không woodblock-angular (không có Tailwind) sẽ
12
+ không dùng được bộ token này nữa.
13
+
14
+ Các nấc màu gốc (`--color-slate-900`…) đến từ `primitives.css` — file tự
15
+ sinh, chép đúng giá trị của bảng màu Tailwind. Xem `scripts/gen-primitives.mjs`.
16
+ ========================================================================== */
17
+ /* `.light` đi kèm `:root` để có thể ép một VÙNG về theme sáng — trang docs cần
18
+ hiện màu của cả hai theme cạnh nhau. `.dark` vốn đã là class nên dùng được
19
+ ngay; light thì trước đây chỉ có ở :root, không scope được. */
20
+ :root,
21
+ .light {
22
+ /* --- radius --- */
23
+ --radius: 0.625rem;
24
+
25
+ /* --- ám màu mặt nền ---
26
+ Nấc màu gốc chỉ còn góp ĐỘ SÁNG; chroma và hue của cả nhóm trung tính do
27
+ hai token dưới đây quyết định, và mỗi bảng màu khai đè `--tint-hue` của
28
+ riêng nó.
29
+
30
+ Vì sao không chỉ chọn một họ trung tính có sẵn: Tailwind chỉ cho HAI họ có
31
+ sắc thật — slate 0,042 và gray 0,034 — và cả hai đều ở hue xanh lam ~265.
32
+ Không có bản cam, tím hay teal tương đương, nên Solar/Nebula/Aurora dùng họ
33
+ nào cũng chỉ tới 0,006–0,011, tức nhạt gấp bốn tới bảy lần bảng mặc định.
34
+
35
+ Và KHÔNG lấy hue từ chính nấc gốc (`oklch(from … l var(…) h)`): ở chroma
36
+ 0,001 thì hue là số vô nghĩa — `stone-50` khai hue 106, tức OLIVE. Nâng
37
+ chroma lên mà giữ hue đó là mặt nền Solar ra xanh lá. Hue phải khai tay.
38
+
39
+ Đường cong chroma dưới đây chép đúng của thang slate, nên Hyperion giữ
40
+ nguyên mọi giá trị (chỉ hue phẳng lại: thang slate vốn trôi 248 -> 266 dọc
41
+ ramp, ở chroma này lệch đó không nhìn ra). Bảng nào không với tới trần sRGB
42
+ thì kẹp ở 85% trần và khai đè tại chỗ — chỉ Solar (950) và Aurora (900,
43
+ 950) rơi vào diện đó, xem palettes.css. */
44
+ --tint-hue: 265.755;
45
+ /* Hệ số cho cả thang, để một bảng tắt hẳn trục này mà không phải khai đè
46
+ chín con số. Eclipse là ca duy nhất: chroma 0 chính là định nghĩa của nó,
47
+ mà nó KHÔNG tự khai hết mọi mặt nền — `surfaces.css` ở theme kính khai đè
48
+ --card/--popover/--sidebar/--code-bg, nên thiếu hệ số này thì bật kính lên
49
+ là Eclipse ám xanh lam theo slate. */
50
+ --tint-scale: 1;
51
+ --tint-c-50: calc(0.003 * var(--tint-scale));
52
+ --tint-c-100: calc(0.007 * var(--tint-scale));
53
+ --tint-c-200: calc(0.013 * var(--tint-scale));
54
+ --tint-c-300: calc(0.022 * var(--tint-scale));
55
+ --tint-c-400: calc(0.04 * var(--tint-scale));
56
+ --tint-c-500: calc(0.046 * var(--tint-scale));
57
+ --tint-c-700: calc(0.044 * var(--tint-scale));
58
+ --tint-c-800: calc(0.041 * var(--tint-scale));
59
+ --tint-c-900: calc(0.042 * var(--tint-scale));
60
+ --tint-c-950: calc(0.042 * var(--tint-scale));
61
+
62
+ /* --- surface --- */
63
+ --background: var(--color-white);
64
+ --foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
65
+
66
+ --card: var(--color-white);
67
+ --card-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
68
+
69
+ --popover: var(--color-white);
70
+ --popover-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
71
+
72
+ /* --- brand --- */
73
+ --primary: var(--color-blue-600);
74
+ --primary-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
75
+
76
+ --secondary: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
77
+ --secondary-foreground: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
78
+
79
+ --muted: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
80
+ --muted-foreground: oklch(from var(--color-slate-500) l var(--tint-c-500) var(--tint-hue));
81
+
82
+ --accent: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
83
+ /* Highlight của MỤC MENU đang trỏ tới. Tách khỏi `--accent` vì trục
84
+ `data-menu-accent` phải đổi được riêng phần menu: `--accent` còn được
85
+ Sidebar, Calendar và vài chỗ hover khác đọc, đổi thẳng nó là đổi cả những
86
+ chỗ không ai xin. Mặc định trỏ vào `--accent` nên không đổi gì sẵn có. */
87
+ --menu-accent: var(--accent);
88
+ --menu-accent-foreground: var(--accent-foreground);
89
+ /* Mặt nền của tấm menu. Tách khỏi `--popover` vì trục `data-menu-appearance`
90
+ phải hạ được độ đục của RIÊNG menu — Popover và Tooltip cũng đọc
91
+ `--popover` thì không được đi theo. Mặc định trỏ thẳng vào nó nên không đổi
92
+ gì sẵn có.
93
+
94
+ Không tự trộn được từ chính `--popover` trong khối `translucent` nếu dùng
95
+ chung một tên: custom property tham chiếu chính nó trên CÙNG phần tử là một
96
+ vòng lặp, CSS coi cả hai là không hợp lệ. Hai tên thì không. */
97
+ --menu-surface: var(--popover);
98
+ --accent-foreground: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
99
+
100
+ /* --- status: admin cần đủ 4 trạng thái, không chỉ destructive ---
101
+ Các cặp nền/chữ ở đây đều đã ĐO đạt WCAG AA (>= 4.5:1). Đổi một trong hai
102
+ thì đo lại, đừng chỉnh theo cảm giác: --success và --info từng là 0.6/0.62
103
+ và chỉ đạt 3.57 / 3.36 với chữ trắng. */
104
+ --destructive: var(--color-red-600);
105
+ --destructive-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
106
+
107
+ --success: var(--color-green-700);
108
+ --success-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
109
+
110
+ /* NGOẠI LỆ về màu CHỮ, không phải về việc neo: nó vẫn trỏ vào một nấc như
111
+ mọi token khác (luật 3 của check-tokens cũng không cho phép literal).
112
+ Chỗ khác biệt là bốn sắc kia đặt chữ TRẮNG lên nền, còn vàng thì không
113
+ nấc amber/yellow nào vừa đủ tối cho chữ trắng vừa còn ra màu vàng — sớm
114
+ nhất là amber-700, đã ngả nâu. Vàng vốn là màu sáng nên nó dùng chữ TỐI. */
115
+ --warning: var(--color-amber-500);
116
+ --warning-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
117
+
118
+ --info: var(--color-cyan-700);
119
+ --info-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
120
+
121
+ /* --- soft: nền pha loãng + chữ giữ nguyên sắc, kiểu "soft-danger" của Bootstrap.
122
+ Để trong suốt chứ không trộn với màu nền cụ thể: chip soft nằm trên card, trên
123
+ nền trang, và cả trong Alert đã có tint — trong suốt thì tự hoà theo mặt dưới.
124
+ Khai riêng cho sáng/tối vì modifier kiểu `bg-x/10` chỉ được một con số cho cả
125
+ hai theme, mà nền tối nuốt tint nên cần đậm gần gấp đôi. */
126
+ /* Chữ soft: hạ độ sáng và ĐẨY chroma lên 1.2×. Pha với đen như trước vừa
127
+ tối vừa làm mất độ tươi (chroma 0.245 -> 0.17) nên nhìn đục.
128
+ Vàng phải hạ sâu hơn hẳn (0.24 so với 0.08) vì sắc vàng vốn sáng. */
129
+
130
+ /* Nền của nút, tách khỏi --x để theme kính hạ được độ đục mà không kéo theo
131
+ `text-primary`, `border-primary`, `ring-primary`. Mặc định là màu đặc. */
132
+ --primary-fill: var(--primary);
133
+ --primary-fill-hover: color-mix(in oklab, var(--primary) 90%, transparent);
134
+ --secondary-fill: var(--secondary);
135
+ --secondary-fill-hover: color-mix(in oklab, var(--secondary) 90%, transparent);
136
+ --destructive-fill: var(--destructive);
137
+ --destructive-fill-hover: color-mix(in oklab, var(--destructive) 90%, transparent);
138
+ --success-fill: var(--success);
139
+ --success-fill-hover: color-mix(in oklab, var(--success) 90%, transparent);
140
+ --warning-fill: var(--warning);
141
+ --warning-fill-hover: color-mix(in oklab, var(--warning) 90%, transparent);
142
+ --info-fill: var(--info);
143
+ --info-fill-hover: color-mix(in oklab, var(--info) 90%, transparent);
144
+
145
+ /* Ba mặt của thanh trượt, tách khỏi --muted / --primary / --background vì
146
+ theme kính phải hạ độ đục của RIÊNG chúng: `bg-muted` thì đã trong mờ sẵn,
147
+ nhưng `bg-primary` và `bg-background` vẫn đục kín, nên phần đã chọn và cái
148
+ núm là hai mảng đặc nằm giữa một giao diện kính. Mặc định là màu đặc,
149
+ surfaces.css khai đè khi bật data-surface="glass". */
150
+ --slider-track: var(--muted);
151
+ --slider-range: var(--primary);
152
+ --slider-thumb: var(--background);
153
+ --slider-thumb-border: var(--primary);
154
+
155
+ --primary-soft: color-mix(in oklab, var(--primary) 14%, var(--control-base));
156
+ --primary-soft-border: color-mix(in oklab, var(--primary) 26%, var(--control-base));
157
+ --primary-soft-foreground: oklch(from var(--primary) calc(l - 0.08) calc(c * 1.2) h);
158
+ --primary-soft-hover: color-mix(in oklab, var(--primary) 20%, var(--control-base));
159
+
160
+ --destructive-soft: color-mix(in oklab, var(--destructive) 14%, var(--control-base));
161
+ --destructive-soft-border: color-mix(in oklab, var(--destructive) 26%, var(--control-base));
162
+ --destructive-soft-foreground: oklch(from var(--destructive) calc(l - 0.14) calc(c * 1.2) h);
163
+ --destructive-soft-hover: color-mix(in oklab, var(--destructive) 20%, var(--control-base));
164
+
165
+ --success-soft: color-mix(in oklab, var(--success) 14%, var(--control-base));
166
+ --success-soft-border: color-mix(in oklab, var(--success) 26%, var(--control-base));
167
+ --success-soft-foreground: oklch(from var(--success) calc(l - 0.08) calc(c * 1.2) h);
168
+ --success-soft-hover: color-mix(in oklab, var(--success) 20%, var(--control-base));
169
+
170
+ /* Chữ soft ở chế độ sáng phải tối hơn màu gốc — ĐO mới biết: để nguyên
171
+ var(--x) thì primary 3.4:1, success 3.2:1, destructive 4.4:1, trượt hết AA.
172
+ Pha với đen theo tỉ lệ nên nó vẫn đổi đúng theo bảng màu đang chọn. */
173
+ --warning-soft: color-mix(in oklab, var(--warning) 14%, var(--control-base));
174
+ --warning-soft-border: color-mix(in oklab, var(--warning) 30%, var(--control-base));
175
+ --warning-soft-foreground: oklch(from var(--warning) calc(l - 0.24) calc(c * 1.2) h);
176
+ --warning-soft-hover: color-mix(in oklab, var(--warning) 22%, var(--control-base));
177
+
178
+ --info-soft: color-mix(in oklab, var(--info) 14%, var(--control-base));
179
+ --info-soft-border: color-mix(in oklab, var(--info) 26%, var(--control-base));
180
+ --info-soft-foreground: oklch(from var(--info) calc(l - 0.08) calc(c * 1.2) h);
181
+ --info-soft-hover: color-mix(in oklab, var(--info) 20%, var(--control-base));
182
+
183
+ /* Bề mặt: mặc định không lọc gì, không nền gì.
184
+ surfaces.css bật cả hai lên khi chọn glass. */
185
+ /* Nền mà các tint soft tô đè lên. `transparent` nghĩa là soft ăn theo mặt
186
+ nền phía sau — đúng ở theme thường, nhưng ở glass thì phía sau là gradient
187
+ sáng nên chữ soft mất tương phản. surfaces.css đổi nó thành màu đục. */
188
+ --control-base: transparent;
189
+
190
+ /* Hai mặt của vật liệu kính, dùng bởi surfaces.css khi bật
191
+ data-surface="glass": viền sáng ở mép, và vệt sáng bên trong tạo cảm giác
192
+ tấm kính có ĐỘ DÀY. Thiếu cái thứ hai thì bề mặt chỉ là một ô mờ phẳng.
193
+
194
+ Luôn trắng, cố tình KHÔNG đổi theo sáng/tối: đây là mặt phản sáng của vật
195
+ liệu, không phải màu chữ.
196
+
197
+ Từng có cả một BỘ nút kính đi kèm ở đây — nền, nền hover, chữ, và một style
198
+ "shade" nền tối, nhân thêm cho năm sắc màu. Đã gỡ: kính là vật liệu của BỀ
199
+ MẶT (`data-surface`), không phải một biến thể của nút, nên mọi nút tự thành
200
+ kính mà không nút nào phải khai gì.
201
+
202
+ Con số đáng giữ lại từ đợt đó, vì nó giải thích vì sao KHÔNG nên dựng lại:
203
+ chữ trắng trên kính trong suốt đo được 10,7:1 ở chỗ ảnh tối nhưng chỉ
204
+ **1,35:1** ở chỗ sáng, và blur không cứu được (pixel xấu nhất
205
+ 187,224,224 → 185,224,224). Trách nhiệm tương phản thuộc về MẶT NỀN chứ
206
+ không phải cái nút: media phải đủ tối, hoặc phủ scrim lên media. Nút không
207
+ tự biết phía sau nó là gì. */
208
+ --glass-border: color-mix(in oklab, var(--color-white) 30%, transparent);
209
+ --glass-highlight: color-mix(in oklab, var(--color-white) 22%, transparent);
210
+
211
+ --surface-filter: none;
212
+ --surface-backdrop: none;
213
+ /* Nền header dính trên cùng. Là token để theme glass hạ độ đục xuống được —
214
+ 88% đục thì kính ở dưới trông rất lệch so với phần thân trang. */
215
+ --header-surface: color-mix(in oklab, var(--background) 88%, transparent);
216
+
217
+ /* --- line / focus --- */
218
+ --border: oklch(from var(--color-slate-200) l var(--tint-c-200) var(--tint-hue));
219
+ --input: oklch(from var(--color-slate-200) l var(--tint-c-200) var(--tint-hue));
220
+ --ring: var(--color-blue-600);
221
+
222
+ /* Vòng focus HAI LỚP, dùng chung cho mọi control nhập liệu.
223
+
224
+ Lớp trong ĐẶC và lớp ngoài loãng, không phải hai lớp loãng: viền 1px đổi
225
+ sang `--ring` cộng lớp trong 1px là chỉ báo focus dày 2px liền mạch —
226
+ đúng mức WCAG 2.4.13 đòi. Lớp ngoài chỉ là quầng trang trí, hạ nó xuống
227
+ dưới ngưỡng tương phản cũng không mất chỉ báo.
228
+
229
+ Bản không hợp lệ đổi sắc chứ KHÔNG bỏ quầng: bỏ đi thì field sai lại là
230
+ field khó thấy đang focus nhất. */
231
+ --focus-ring: 0 0 0 1px var(--ring), 0 0 0 4px color-mix(in oklab, var(--ring) 18%, transparent);
232
+ --focus-ring-invalid: 0 0 0 1px var(--destructive), 0 0 0 4px color-mix(in oklab, var(--destructive) 18%, transparent);
233
+
234
+ /* --- sidebar: tách riêng để đổi theme sidebar không đụng tới app --- */
235
+ --sidebar: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
236
+ --sidebar-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
237
+ --sidebar-primary: var(--color-blue-600);
238
+ --sidebar-primary-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
239
+ --sidebar-accent: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
240
+ --sidebar-accent-foreground: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
241
+ --sidebar-border: oklch(from var(--color-slate-200) l var(--tint-c-200) var(--tint-hue));
242
+ --sidebar-ring: var(--color-blue-600);
243
+
244
+ /* --- chart: năm màu chuỗi, neo theo sắc của --primary ---
245
+ Trải theo CẢ sắc lẫn ĐỘ SÁNG. Trải theo sắc thôi là chưa đủ: đo bằng
246
+ luminance thì bản cũ có hai màu chỉ chênh 0,007 — in đen trắng hay với
247
+ người khiếm sắc là trùng nhau. Bản này khoảng cách nhỏ nhất 0,045, và mọi
248
+ màu đạt tối thiểu 3,13:1 trên mặt card (WCAG 1.4.11).
249
+ Bốn bảng màu còn lại khai đè bộ này trong palettes.css. */
250
+ --chart-1: var(--color-blue-600);
251
+ --chart-2: var(--color-cyan-600);
252
+ --chart-3: var(--color-violet-900);
253
+ --chart-4: var(--color-sky-800);
254
+ --chart-5: var(--color-indigo-400);
255
+
256
+ /* Chữ/icon đặt TRỰC TIẾP trên một màu chuỗi (nhãn trong cột, chữ cái trong
257
+ avatar). Không thể dùng chung MỘT màu chữ cho cả năm: thang màu chuỗi cố ý
258
+ trải rộng độ sáng, nên cùng một màu chữ sẽ đọc được ở đầu này và biến mất ở
259
+ đầu kia — bản cũ chỉ có --chart-foreground trắng và đã hỏng sẵn (chữ trắng
260
+ trên chart-3 sáng chỉ đạt 3,0:1, trên blue-400 tối chỉ 2,64:1).
261
+ Cú pháp màu tương đối tự chọn đen hay trắng theo độ sáng của chính nền:
262
+ L < 0,58 thì chữ trắng, ngược lại chữ đen. Ngưỡng dò bằng cách quét 0,46–0,68
263
+ trên cả 50 màu chuỗi: 0,58 cho tương phản thấp nhất cao nhất (4,74:1).
264
+ HỆ SỐ 1000 KHÔNG PHẢI SỐ TUỲ Ý. Nó quyết định độ dốc của bậc thang: với
265
+ hệ số 100, một màu nằm sát ngưỡng cho ra giá trị NẰM GIỮA 0 và 1, tức chữ
266
+ màu xám. Đã dính thật — red-600 có L = 0,577, lệch ngưỡng 0,003, ra
267
+ clamp(0; 0,3; 1) = 0,3 và tương phản tụt còn 2,85:1. Hệ số 1000 thu dải mập
268
+ mờ xuống 0,001, mà màu gần ngưỡng nhất trong cả 50 màu vẫn cách 0,003. Khai một lần là đủ cho cả năm
269
+ bảng màu, vì var(--chart-N) resolve theo bảng đang bật. */
270
+ --chart-1-foreground: oklch(from var(--chart-1) clamp(0, (0.58 - l) * 1000, 1) 0 h);
271
+ --chart-2-foreground: oklch(from var(--chart-2) clamp(0, (0.58 - l) * 1000, 1) 0 h);
272
+ --chart-3-foreground: oklch(from var(--chart-3) clamp(0, (0.58 - l) * 1000, 1) 0 h);
273
+ --chart-4-foreground: oklch(from var(--chart-4) clamp(0, (0.58 - l) * 1000, 1) 0 h);
274
+ --chart-5-foreground: oklch(from var(--chart-5) clamp(0, (0.58 - l) * 1000, 1) 0 h);
275
+ /* --- code: bảng màu tô cú pháp, neo theo GitHub Light ---
276
+ Đây là theme shiki mà shadcn dùng cho khối code trong tài liệu. Mỗi dòng
277
+ là nấc Tailwind gần nhất với màu tương ứng của nó (ΔE ≤ 0.08). Nền dùng
278
+ luôn --secondary nên tự đổi theo bảng màu đang chọn. */
279
+ --code-bg: var(--secondary);
280
+ --code-fg: var(--foreground);
281
+ --code-comment: var(--color-gray-600);
282
+ --code-keyword: var(--color-red-700);
283
+ --code-entity: var(--color-purple-700);
284
+ --code-tag: var(--color-green-800);
285
+ --code-component: var(--color-blue-700);
286
+ --code-string: var(--color-blue-950);
287
+ --code-number: var(--color-blue-700);
288
+ }
289
+
290
+ .dark {
291
+ --background: oklch(from var(--color-slate-950) l var(--tint-c-950) var(--tint-hue));
292
+ --foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
293
+
294
+ --card: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
295
+ --card-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
296
+
297
+ --popover: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
298
+ --popover-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
299
+
300
+ --primary: var(--color-blue-600);
301
+ --primary-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
302
+
303
+ --secondary: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
304
+ --secondary-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
305
+
306
+ --muted: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
307
+ --muted-foreground: oklch(from var(--color-slate-400) l var(--tint-c-400) var(--tint-hue));
308
+
309
+ --accent: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
310
+ --accent-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
311
+
312
+ /* Bản tối dùng CÙNG nấc với bản sáng. Trần độ sáng của nền nút do chữ trắng
313
+ quyết định (>= 4.5:1), không liên quan tới nền trang — nên nấc sáng nhất
314
+ còn đạt là một, cho cả hai theme. Nấc tối hơn thì chữ dễ đọc hơn nhưng nút
315
+ chìm vào nền tối: blue-700 tách còn 2.76, blue-800 còn 2.13.
316
+ Vì vậy chữ ở đây là màu SÁNG, khác bản trước. */
317
+ --destructive: var(--color-red-600);
318
+ --destructive-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
319
+
320
+ --success: var(--color-green-700);
321
+ --success-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
322
+
323
+ --warning: var(--color-yellow-500);
324
+ --warning-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
325
+
326
+ --info: var(--color-cyan-700);
327
+ --info-foreground: oklch(from var(--color-slate-50) l var(--tint-c-50) var(--tint-hue));
328
+
329
+ /* Nền tối cần tint đậm hơn. Chữ phải pha thêm trắng: dùng thẳng màu gốc thì
330
+ destructive chỉ đạt 4.0:1 trên nền soft — đo được, không phải phỏng đoán. */
331
+ --primary-soft: color-mix(in oklab, var(--primary) 22%, var(--control-base));
332
+ --primary-soft-border: color-mix(in oklab, var(--primary) 34%, var(--control-base));
333
+ --primary-soft-hover: color-mix(in oklab, var(--primary) 32%, var(--control-base));
334
+
335
+ --destructive-soft: color-mix(in oklab, var(--destructive) 22%, var(--control-base));
336
+ --destructive-soft-border: color-mix(in oklab, var(--destructive) 34%, var(--control-base));
337
+ --destructive-soft-hover: color-mix(in oklab, var(--destructive) 32%, var(--control-base));
338
+
339
+ --success-soft: color-mix(in oklab, var(--success) 22%, var(--control-base));
340
+ --success-soft-border: color-mix(in oklab, var(--success) 34%, var(--control-base));
341
+ --success-soft-hover: color-mix(in oklab, var(--success) 32%, var(--control-base));
342
+
343
+ --warning-soft: color-mix(in oklab, var(--warning) 22%, var(--control-base));
344
+ --warning-soft-border: color-mix(in oklab, var(--warning) 34%, var(--control-base));
345
+ --warning-soft-hover: color-mix(in oklab, var(--warning) 32%, var(--control-base));
346
+
347
+ --info-soft: color-mix(in oklab, var(--info) 22%, var(--control-base));
348
+ --info-soft-border: color-mix(in oklab, var(--info) 34%, var(--control-base));
349
+ --info-soft-hover: color-mix(in oklab, var(--info) 32%, var(--control-base));
350
+
351
+ --primary-soft-foreground: oklch(from var(--primary) calc(l + 0.18) calc(c * 1.1) h);
352
+ /* Đỏ cộng 0.21 chứ không 0.18 như bốn sắc kia: chip soft đặt trên `--card`
353
+ (chứ không trên nền trang) ở Solar tối đo được 4.49 — hụt AA đúng 0.008.
354
+ Solar có --card sáng nhất trong năm bảng nên lớp tint đỏ 22% nằm trên đó
355
+ bị nâng lên cao nhất. Xem mục đo lại trong CLAUDE.md. */
356
+ --destructive-soft-foreground: oklch(from var(--destructive) calc(l + 0.21) calc(c * 1.1) h);
357
+ --success-soft-foreground: oklch(from var(--success) calc(l + 0.18) calc(c * 1.1) h);
358
+ --warning-soft-foreground: oklch(from var(--warning) calc(l + 0.06) calc(c * 1.1) h);
359
+ --info-soft-foreground: oklch(from var(--info) calc(l + 0.18) calc(c * 1.1) h);
360
+
361
+ --border: color-mix(in oklab, var(--color-white) 12%, transparent);
362
+ --input: color-mix(in oklab, var(--color-white) 16%, transparent);
363
+ --ring: var(--color-blue-400);
364
+
365
+ /* Quầng đậm hơn bản sáng (18% -> 30%): nền tối nuốt tint, để nguyên 18%
366
+ thì lớp ngoài gần như không thấy. Cùng cách xử lý như nhóm token soft. */
367
+ --focus-ring: 0 0 0 1px var(--ring), 0 0 0 4px color-mix(in oklab, var(--ring) 30%, transparent);
368
+ --focus-ring-invalid: 0 0 0 1px var(--destructive), 0 0 0 4px color-mix(in oklab, var(--destructive) 30%, transparent);
369
+
370
+ --sidebar: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
371
+ --sidebar-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
372
+ --sidebar-primary: var(--color-blue-400);
373
+ --sidebar-primary-foreground: oklch(from var(--color-slate-950) l var(--tint-c-950) var(--tint-hue));
374
+ --sidebar-accent: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
375
+ --sidebar-accent-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
376
+ --sidebar-border: color-mix(in oklab, var(--color-white) 12%, transparent);
377
+ --sidebar-ring: var(--color-blue-400);
378
+
379
+ --chart-1: var(--color-blue-400);
380
+ --chart-2: var(--color-cyan-400);
381
+ --chart-3: var(--color-violet-500);
382
+ --chart-4: var(--color-sky-400);
383
+ --chart-5: var(--color-indigo-400);
384
+ --code-bg: var(--secondary);
385
+ --code-fg: var(--foreground);
386
+ --code-comment: var(--color-gray-400);
387
+ --code-keyword: var(--color-red-400);
388
+ --code-entity: var(--color-violet-400);
389
+ --code-tag: var(--color-green-300);
390
+ --code-component: var(--color-blue-300);
391
+ --code-string: var(--color-sky-300);
392
+ --code-number: var(--color-blue-300);
393
+ }
394
+
395
+ /* Bo tròn suy ra từ `--radius`. Khai ở đây (chứ không chỉ trong `@theme` của
396
+ Tailwind) để consumer không dùng Tailwind vẫn có `var(--radius-md)`.
397
+
398
+ Thang NHÂN TỈ LỆ, không cộng trừ px. Trước đây là -4 / -2 / 0 / +4 / +8, và
399
+ ở mỏ neo mặc định 0.625rem hai cách cho KẾT QUẢ TRÙNG KHÍT (6 / 8 / 10 / 14 /
400
+ 18px) — các hệ số dưới đây chính là tỉ lệ đo được từ thang cũ, nên đổi sang
401
+ nhân không dịch một pixel nào của bảng gốc lẫn bốn bảng màu hiện có.
402
+
403
+ Cộng trừ px SỤP ở mỏ neo nhỏ, và đó là lý do phải đổi. Ở `--radius: 3px`
404
+ thang cũ ra -1 / 1 / 3 / 7 / 11: nấc nhỏ bị kẹp về 0, nấc lớn vọt lên gần
405
+ gấp bốn lần mỏ neo, tức hai đầu thang giãn ra đúng lúc thiết kế muốn nó co
406
+ lại. Nhân tỉ lệ ra 1,8 / 2,4 / 3 / 4,2 / 5,4 — cả thang nằm gọn trong 2-5px,
407
+ giữ nguyên thứ tự và giữ nguyên tương quan.
408
+
409
+ Hệ quả là `--radius` mới thật sự là MỘT mặc định cho cả hệ: đổi nó thì mọi
410
+ component đi theo cùng tỉ lệ, còn chỗ nào cần khác thì tự khai class tại chỗ.
411
+ Trước khi đổi, một mỏ neo nhỏ làm thang vỡ ra chứ không co lại.
412
+ `[data-style]` PHẢI có trong danh sách này, và đó không phải chỗ thừa.
413
+ `calc(var(--radius) …)` giải ngay tại phần tử mang khai báo, rồi chỉ có KẾT
414
+ QUẢ mới thừa kế xuống. Nên đặt `--radius` trên một phần tử CON của `.light`
415
+ thì năm nấc đã tính xong ở `.light` bằng giá trị CŨ — ghi đè im lặng không
416
+ có tác dụng. Đã đo trên `/create`: `--radius` chạy 0rem → 1.4rem mà
417
+ `rounded-lg` đứng nguyên 10px, tức cả ô chọn bo góc lẫn `data-style="nova"`
418
+ đều vô hiệu khi trục hình khối nằm ở một khối lồng thay vì ở `<html>`.
419
+
420
+ Cùng lớp lỗi với "bảng màu đè token kính" và với ba lần lặp attribute ở
421
+ `styles.css`: quy tắc đúng ở `:root` không tự đúng ở khối lồng. */
422
+ :root,
423
+ .light,
424
+ .dark,
425
+ [data-style] {
426
+ --radius-sm: calc(var(--radius) * 0.6);
427
+ --radius-md: calc(var(--radius) * 0.8);
428
+ --radius-lg: var(--radius);
429
+ --radius-xl: calc(var(--radius) * 1.4);
430
+ --radius-2xl: calc(var(--radius) * 1.8);
431
+ }
432
+
433
+ /* Geist cho chữ, Geist Mono cho code — đúng bộ font bản thiết kế dùng.
434
+ Webfont nạp từ Google Fonts trong index.html của app (có subset vietnamese),
435
+ không thêm package nào.
436
+ `tailwind.css` ánh xạ hai biến này vào `@theme`, nhờ đó Tailwind lấy chúng
437
+ làm --default-font-family / --default-mono-font-family. */
438
+ :root {
439
+ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
440
+ /* Họ chữ cho TIÊU ĐỀ. Mặc định trỏ vào `--font-sans` nên không đổi gì cho tới
441
+ khi có người đặt lại nó — nhưng nó là một token RIÊNG, không phải bí danh:
442
+ đổi nó không kéo theo chữ thân bài, và ngược lại.
443
+
444
+ Người đọc nó là rule `h1..h6` trong `@layer base` của `src/variants`,
445
+ và `--typeset-font-heading` của lớp văn xuôi rơi lui về đây. */
446
+ --font-heading: var(--font-sans);
447
+ --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
448
+ }
449
+
450
+ /* Số đo của CONTROL — tầng component, và nó phải TỰ KIẾM chỗ đứng.
451
+
452
+ TIÊU CHÍ KẾT NẠP: một số đo chỉ thành token khi có thứ BÊN NGOÀI component
453
+ phải với vào trong nó — một trục theme, một ràng buộc CSS, hoặc một component
454
+ khác đang chép lại nó. Muốn đổi số đo cho vui thì ghi đè bằng `class`; `cn()`
455
+ lo việc đó và tailwind-merge đã đo là thắng.
456
+
457
+ Đây đúng là tiêu chí mà 30 token component sẵn có (--sidebar-*, --code-*,
458
+ --slider-*, --header-surface, --chart-active-bar) vốn đã tuân theo: cả 30 đều
459
+ sinh ra vì theme kính hoặc bảng màu phải với vào trong một component, không
460
+ cái nào sinh ra vì ai đó muốn tuỳ biến. Khác biệt duy nhất là chúng đều là
461
+ MÀU, còn nhóm dưới đây là SỐ ĐO.
462
+
463
+ ĐỪNG làm bản cạn kiệt kiểu PrimeVue. Đo rồi: 44 cỡ trên 12 component, mỗi cỡ
464
+ 3–5 số đo là ra 130–220 token — nhiều hơn cả bộ token ngữ nghĩa hiện có, và
465
+ mỗi token là một lời hứa tương thích ngược không rút lại được.
466
+
467
+ Neo vào `--spacing` chứ không ghi rem cứng, để bên rescale thang spacing thì
468
+ control đi theo. */
469
+ :root {
470
+ /* Ô nhập. `h-[30px]` từng bị chép nguyên văn vào 4 component, và cỡ `sm` đã
471
+ TRÔI thành ba số khác nhau — xem chú thích ở cuối khối. 30px cố ý nằm giữa
472
+ h-7 (28) và h-8 (32), nên hệ số là 7.5. */
473
+ --ui-field-height-sm: calc(var(--spacing) * 7.5);
474
+ --ui-field-height-md: calc(var(--spacing) * 9);
475
+ --ui-field-padding-x-sm: calc(var(--spacing) * 2.5);
476
+ --ui-field-padding-x-md: calc(var(--spacing) * 3);
477
+ --ui-field-text-sm: 12px;
478
+
479
+ /* Nút và Toggle. `Toggle` là một nút giữ trạng thái nên nó CỐ Ý soi theo
480
+ thang cỡ của `Button` — trước đây bằng cách chép nguyên văn cả chuỗi class.
481
+ Chỉ có xs lệch padding, ba cỡ còn lại dùng chung một giá trị, nên khai một
482
+ token thay vì ba. */
483
+ --ui-control-height-xs: calc(var(--spacing) * 6);
484
+ --ui-control-height-sm: calc(var(--spacing) * 7);
485
+ --ui-control-height-md: calc(var(--spacing) * 8);
486
+ --ui-control-height-lg: calc(var(--spacing) * 9);
487
+ --ui-control-padding-x-xs: calc(var(--spacing) * 2);
488
+ --ui-control-padding-x: calc(var(--spacing) * 2.5);
489
+
490
+ /* Nhịp phản hồi khi bấm. `140ms` bị chép ở 4 component control (Button,
491
+ Toggle, Checkbox, RadioGroup) và `100ms` ở 2 — cùng lớp lỗi với h-[30px].
492
+ Không nằm trong thang `--duration-*` chung vì đây là nhịp riêng của control:
493
+ nó phải nhanh hơn 150ms để cú bấm thấy "dính tay". */
494
+ --ui-control-duration: 140ms;
495
+ --ui-control-duration-press: 100ms;
496
+ }
497
+
498
+ /* Tầng chồng.
499
+
500
+ Tailwind KHÔNG có thang z-index (utility `z-10` là số tĩnh, không đọc token),
501
+ nên repo tự khai. Bảy nấc dưới đây ĐO trên toàn repo chứ không bịa cho tròn —
502
+ mỗi nấc đều đang có chỗ dùng thật:
503
+
504
+ 1 NavigationMenuIndicator, cột sticky của DataTable
505
+ 10 nút copy trong CodeBlock, header dính của bảng, tay cầm Splitter (14 chỗ)
506
+ 20 nội dung nổi trên phần trang trí của cùng khối — SidebarRail
507
+ 30 thanh dính của layout — AdminLayout
508
+ 40 đảo nổi — KitTocIsland
509
+ 50 overlay: Dialog, Popover, Tooltip, Select, Sheet, HoverCard (9 chỗ)
510
+ 100 SkipLink — phải trên tất cả, kể cả overlay
511
+
512
+ Đọc bằng `z-(--z-overlay)`; không cần khai trong `@theme inline` vì Tailwind
513
+ không có namespace z-index để mà ánh xạ vào.
514
+
515
+ Thang này chỉ nói về tầng TOÀN CỤC. Thứ tự chồng CỤC BỘ giữa các phần tử anh
516
+ em trong cùng một component thì không thuộc về đây và cứ viết thẳng — lớp phủ
517
+ của SpeedDial dùng `-z-10` để nằm dưới nút và các mục của chính nó, đặt tên
518
+ cho nó trong thang toàn cục là nói sai về phạm vi của nó. */
519
+ :root {
520
+ --z-base: 1;
521
+ --z-raised: 10;
522
+ --z-elevated: 20;
523
+ --z-sticky: 30;
524
+ --z-floating: 40;
525
+ --z-overlay: 50;
526
+ --z-skip-link: 100;
527
+ }
528
+
529
+ /* Thời lượng chuyển động. Tailwind cũng không có thang này (`duration-200` là
530
+ số tĩnh). Ba giá trị, đúng bằng ba giá trị repo đang dùng: 150ms ở 5 chỗ,
531
+ 200ms ở 22 chỗ, 300ms ở 2 chỗ.
532
+
533
+ CẨN THẬN khi đụng tới easing: `--ease-out` mà `primitives.css` chép từ
534
+ Tailwind là `cubic-bezier(0, 0, 0.2, 1)`, còn TỪ KHOÁ `ease-out` của CSS là
535
+ `cubic-bezier(0, 0, 0.58, 1)`. Hai đường cong khác nhau — đổi từ khoá sang
536
+ token là đổi cả cảm giác chuyển động, không phải dọn dẹp. */
537
+ :root {
538
+ --duration-fast: 150ms;
539
+ --duration-base: 200ms;
540
+ --duration-slow: 300ms;
541
+ }
542
+
543
+ /* Animation mở/đóng theo chiều cao.
544
+
545
+ Khai riêng cho Collapsible và Accordion cho khớp tên biến của reka. ĐÃ ĐO:
546
+ AccordionContent thực ra đặt CẢ HAI biến cùng lúc
547
+ (--reka-accordion-content-height và --reka-collapsible-content-height đều ra
548
+ 59.5px), nên dùng chung cũng chạy. Tách ra là để đọc code khỏi phải nhớ chi
549
+ tiết đó, không phải vì bắt buộc.
550
+
551
+ ĐỌC BA TÊN BIẾN, vì mỗi bên đặt một tiền tố: reka ghi
552
+ `--reka-*-content-height`, Radix ghi `--radix-*-content-height`, và cây
553
+ vanilla — không có primitive nào để mà mượn tên — ghi `--hy-*-content-height`.
554
+ Một phần tử chỉ do MỘT bên dựng nên không bao giờ có hai cái cùng lúc, và
555
+ `var()` lồng rơi lui đúng sang tên còn lại. Đây là chỗ duy nhất trong bộ token
556
+ phải biết tên riêng của primitive — thà nhận nó ở một chỗ còn hơn để mỗi cây
557
+ khai một bản `@keyframes` rồi bốn bản trôi khỏi nhau.
558
+ (Radix cũng đặt cả hai: nó gán
559
+ `--radix-accordion-content-height: var(--radix-collapsible-content-height)`.)
560
+
561
+ `@keyframes` để ở tầng ngoài cùng chứ không lồng trong `@theme`: đây là CSS
562
+ thuần nên consumer không dùng Tailwind vẫn viết được
563
+ `animation: var(--animate-accordion-down)`. */
564
+ :root {
565
+ --animate-collapsible-down: collapsible-down var(--duration-base) ease-out;
566
+ --animate-collapsible-up: collapsible-up var(--duration-base) ease-out;
567
+ --animate-accordion-down: accordion-down var(--duration-base) ease-out;
568
+ --animate-accordion-up: accordion-up var(--duration-base) ease-out;
569
+ }
570
+
571
+ @keyframes collapsible-down {
572
+ from { height: 0; }
573
+ to { height: var(--reka-collapsible-content-height, var(--radix-collapsible-content-height, var(--hy-collapsible-content-height))); }
574
+ }
575
+
576
+ @keyframes collapsible-up {
577
+ from { height: var(--reka-collapsible-content-height, var(--radix-collapsible-content-height, var(--hy-collapsible-content-height))); }
578
+ to { height: 0; }
579
+ }
580
+
581
+ @keyframes accordion-down {
582
+ from { height: 0; }
583
+ to { height: var(--reka-accordion-content-height, var(--radix-accordion-content-height, var(--hy-accordion-content-height))); }
584
+ }
585
+
586
+ @keyframes accordion-up {
587
+ from { height: var(--reka-accordion-content-height, var(--radix-accordion-content-height, var(--hy-accordion-content-height))); }
588
+ to { height: 0; }
589
+ }
590
+
591
+ /* Người bị rối loạn tiền đình có thể chóng mặt, buồn nôn vì chuyển động trên
592
+ màn hình. Hệ điều hành cho họ bật cờ này và CSS phải nghe theo.
593
+
594
+ Trước đó repo chỉ rắc `motion-reduce:` ở 8 component — nghĩa là phần lớn
595
+ transition và toàn bộ animation vào/ra của Dialog, Sheet, Accordion vẫn chạy.
596
+ Đặt ở đây thì không component nào phải nhớ nữa.
597
+
598
+ BA điều cố ý KHÔNG làm:
599
+
600
+ 1. Không đụng `transform`. Nghe thì hợp lý vì slide/scale là thứ cần bỏ,
601
+ nhưng SpeedDial dùng `translate()` để ĐẶT VỊ TRÍ chứ không phải để tạo
602
+ hiệu ứng — vô hiệu hoá nó là dồn cả tám mục chồng lên nút chính.
603
+ 2. Không hạ `animation-iteration-count` xuống 1. Spinner quay vô hạn, cắt đi
604
+ là chỉ báo tải đứng im — hỏng chứ không phải trợ năng. Chuyển động lặp
605
+ liên tục, không có điểm đầu cuối, vốn không phải thứ cờ này nhắm tới.
606
+ 3. Không xoá hẳn transition, chỉ rút còn 1ms. Tài liệu nói rõ: giảm chuyển
607
+ động KHÔNG phải bỏ phản hồi — trạng thái vẫn phải đổi, chỉ là đổi ngay
608
+ thay vì trượt tới. */
609
+ @media (prefers-reduced-motion: reduce) {
610
+ *,
611
+ *::before,
612
+ *::after {
613
+ transition-duration: 1ms !important;
614
+ transition-delay: 0ms !important;
615
+ animation-duration: 1ms !important;
616
+ animation-delay: 0ms !important;
617
+ }
618
+
619
+ /* Spinner là ngoại lệ — xem lý do 2 ở trên. */
620
+ .animate-spin {
621
+ animation-duration: 1s !important;
622
+ }
623
+
624
+ html {
625
+ scroll-behavior: auto !important;
626
+ }
627
+ }