@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.
@@ -0,0 +1,143 @@
1
+ /* =============================================================================
2
+ TOKEN CỦA RIÊNG BUTTON — tầng token theo COMPONENT, kiểu PrimeVue.
3
+
4
+ Vì sao có tầng này. Cho tới nay Button đọc thẳng 42 token DÙNG CHUNG
5
+ (`--primary`, `--destructive-soft`…), tức muốn chỉnh riêng cái nút thì phải
6
+ đổi một token mà Badge, Alert và hai chục chỗ khác cũng đang đọc. PrimeVue
7
+ giải bằng cách cho mỗi component một không gian tên riêng — 176 token cho
8
+ riêng Button của họ. Đây là bản tương đương cho repo này.
9
+
10
+ MỖI TOKEN MẶC ĐỊNH TRỎ VÀO TOKEN NGỮ NGHĨA. Đó là điều kiện để tầng này
11
+ không phá thứ đang có: chín trục theme vẫn chảy qua (`--primary` đổi theo
12
+ bảng màu thì `--button-default-bg` đổi theo), và diện mạo không dịch một
13
+ pixel. Cái được thêm là một mặt phẳng để ghi đè RIÊNG nút.
14
+
15
+ VÌ SAO ÍT HƠN 176. Bên họ 8 severity × 11, cộng `outlined` và `text` mỗi cái
16
+ nhân 8 nữa. Ở đây `soft` là MỘT trục tone áp cho 5 sắc, và `outline`/`ghost`/
17
+ `link` không nhân theo sắc — nên cùng độ phủ mà còn 65 token.
18
+
19
+ Đếm lại, đừng tin con số trên (nó đã sai một lần, ghi 73):
20
+
21
+ grep -cP '^\s*--button-[a-z-]+:' css/components/button.css
22
+
23
+ Nhóm `--ui-control-*` KHÔNG chép vào đây: chiều cao và đệm ngang dùng chung
24
+ với Toggle theo đúng chủ ý đã ghi ở `CLAUDE.md`, tách ra là dựng lại đúng
25
+ thứ việc gom chúng sinh ra để dập.
26
+ ========================================================================== */
27
+
28
+ :root {
29
+ /* --- cấu trúc --- */
30
+ --button-radius: var(--radius-lg);
31
+ --button-radius-sm: var(--radius-md);
32
+ --button-gap: calc(var(--spacing) * 1.5);
33
+ --button-gap-xs: var(--spacing);
34
+ --button-font-weight: 500;
35
+ --button-font-size: var(--text-sm);
36
+ --button-font-size-sm: 0.8rem;
37
+ --button-font-size-xs: var(--text-xs);
38
+ --button-icon-size: calc(var(--spacing) * 4);
39
+ --button-icon-size-xs: calc(var(--spacing) * 3);
40
+ --button-duration: var(--ui-control-duration);
41
+
42
+ /* Hai con số này TRƯỚC ĐÂY là literal nằm trong bảng cva — `active:scale-[0.97]`
43
+ và `disabled:opacity-50`. Đó đúng là thứ tầng token sinh ra để rút. */
44
+ --button-press-scale: 0.97;
45
+ --button-press-duration: var(--ui-control-duration-press);
46
+ --button-disabled-opacity: 0.5;
47
+
48
+ /* --- tone SOLID, sáu sắc ---
49
+ `-active` là nấc MỚI: repo chưa từng có token cho trạng thái nhấn, chỉ có
50
+ một phép thu nhỏ. Dẫn xuất theo đúng khuôn của `-fill-hover` (90% độ đục),
51
+ đi tiếp một nấc còn 80%. */
52
+ --button-default-bg: var(--primary-fill);
53
+ --button-default-bg-hover: var(--primary-fill-hover);
54
+ --button-default-bg-active: color-mix(in oklab, var(--primary) 80%, transparent);
55
+ --button-default-fg: var(--primary-foreground);
56
+
57
+ --button-secondary-bg: var(--secondary-fill);
58
+ --button-secondary-bg-hover: var(--secondary-fill-hover);
59
+ --button-secondary-bg-active: color-mix(in oklab, var(--secondary) 80%, transparent);
60
+ --button-secondary-fg: var(--secondary-foreground);
61
+
62
+ --button-destructive-bg: var(--destructive-fill);
63
+ --button-destructive-bg-hover: var(--destructive-fill-hover);
64
+ --button-destructive-bg-active: color-mix(in oklab, var(--destructive) 80%, transparent);
65
+ --button-destructive-fg: var(--destructive-foreground);
66
+
67
+ --button-success-bg: var(--success-fill);
68
+ --button-success-bg-hover: var(--success-fill-hover);
69
+ --button-success-bg-active: color-mix(in oklab, var(--success) 80%, transparent);
70
+ --button-success-fg: var(--success-foreground);
71
+
72
+ --button-warning-bg: var(--warning-fill);
73
+ --button-warning-bg-hover: var(--warning-fill-hover);
74
+ --button-warning-bg-active: color-mix(in oklab, var(--warning) 80%, transparent);
75
+ --button-warning-fg: var(--warning-foreground);
76
+
77
+ --button-info-bg: var(--info-fill);
78
+ --button-info-bg-hover: var(--info-fill-hover);
79
+ --button-info-bg-active: color-mix(in oklab, var(--info) 80%, transparent);
80
+ --button-info-fg: var(--info-foreground);
81
+
82
+ /* --- tone SOFT, năm sắc màu ---
83
+ Hover đổi THẲNG sang nền đặc và chữ trắng, không nội suy — lý do đo được ở
84
+ mục "Hover nút soft" trong CLAUDE.md: giữa transition tương phản sập còn
85
+ 1,09:1. Vì vậy `-soft-bg-hover` trỏ vào màu đặc chứ không phải một nấc pha. */
86
+ --button-default-soft-bg: var(--primary-soft);
87
+ --button-default-soft-bg-hover: var(--primary);
88
+ --button-default-soft-fg: var(--primary-soft-foreground);
89
+ --button-default-soft-fg-hover: var(--primary-foreground);
90
+
91
+ --button-destructive-soft-bg: var(--destructive-soft);
92
+ --button-destructive-soft-bg-hover: var(--destructive);
93
+ --button-destructive-soft-fg: var(--destructive-soft-foreground);
94
+ --button-destructive-soft-fg-hover: var(--destructive-foreground);
95
+
96
+ --button-success-soft-bg: var(--success-soft);
97
+ --button-success-soft-bg-hover: var(--success);
98
+ --button-success-soft-fg: var(--success-soft-foreground);
99
+ --button-success-soft-fg-hover: var(--success-foreground);
100
+
101
+ --button-warning-soft-bg: var(--warning-soft);
102
+ --button-warning-soft-bg-hover: var(--warning);
103
+ --button-warning-soft-fg: var(--warning-soft-foreground);
104
+ --button-warning-soft-fg-hover: var(--warning-foreground);
105
+
106
+ --button-info-soft-bg: var(--info-soft);
107
+ --button-info-soft-bg-hover: var(--info);
108
+ --button-info-soft-fg: var(--info-soft-foreground);
109
+ --button-info-soft-fg-hover: var(--info-foreground);
110
+
111
+ /* --- ba variant không mang sắc --- */
112
+ --button-outline-bg: var(--background);
113
+ --button-outline-bg-hover: var(--accent);
114
+ --button-outline-fg-hover: var(--accent-foreground);
115
+ /* KHÔNG dùng `--input`. Viền của nút outline là RANH GIỚI của control — nút
116
+ đó không có nền, nên không còn gì khác để nhận ra nó — mà `--input` chỉ đạt
117
+ 1,18 trên nền trang. Đo 20 tổ hợp (5 bảng màu × sáng/tối × trên nền trang
118
+ và trên card), WCAG 1.4.11 đòi 3:1:
119
+
120
+ --input 1,18 20/20 trượt
121
+ --border 1,03 20/20 trượt
122
+ --foreground 34% 2,23 10/20
123
+ --foreground 44% 3,22 0
124
+ --foreground 46% 3,45 0 <- chọn
125
+ --muted-foreground 4,57 0 (qua, nhưng đậm như chữ phụ)
126
+
127
+ Lấy 46% chứ không 44%: dư 0,22 là quá mỏng, repo từng bác đúng một biên như
128
+ vậy ở mục vòng focus. Và không lấy `--muted-foreground` vì viền đậm ngang
129
+ chữ phụ thì nút outline đọc ra như một cái khung.
130
+
131
+ Pha từ `--foreground` chứ không chốt một nấc xám — cùng cơ chế `styles.css`
132
+ dùng cho `--border`: một khai báo đúng cho cả hai chế độ, và tự ám theo
133
+ bảng màu.
134
+
135
+ `--input` GIỮ NGUYÊN cho ô nhập: ô nhập có nền riêng nên viền của nó không
136
+ phải thứ duy nhất nhận ra control. */
137
+ --button-outline-border: color-mix(in oklab, var(--foreground) 46%, transparent);
138
+
139
+ --button-ghost-bg-hover: var(--accent);
140
+ --button-ghost-fg-hover: var(--accent-foreground);
141
+
142
+ --button-link-fg: var(--primary);
143
+ }
package/css/index.css ADDED
@@ -0,0 +1,63 @@
1
+ /* =============================================================================
2
+ Bộ Design Token của WoodBlock — CSS THUẦN, không phụ thuộc framework nào.
3
+
4
+ @import "@vestow/theme/css/index.css";
5
+
6
+ App dùng Tailwind v4 thì nạp `tailwind.css` thay cho file này: nó nạp lại
7
+ đúng bốn file dưới đây rồi thêm phần ánh xạ sang utility.
8
+
9
+ Thứ tự nạp là BẮT BUỘC, và mỗi lần đảo đều đã có lỗi thật:
10
+ 1. primitives — nấc màu gốc, mọi token bên dưới trỏ vào đây.
11
+ 2. tokens — token ngữ nghĩa của bảng màu mặc định.
12
+ 3. palettes — ghi đè primary + nhóm trung tính theo `data-palette`.
13
+ 4. surfaces — ghi đè token bề mặt theo `data-surface`, phải sau palettes
14
+ nếu không bảng màu sẽ đè token kính và mặt kính đục hoàn toàn.
15
+ 5. bases — ghi đè HỌ TRUNG TÍNH theo `data-base`, tức chỉ `--tint-hue`
16
+ và `--tint-scale`. Phải sau palettes vì bảng màu cũng khai
17
+ hai token đó; cùng cái bẫy của (4).
18
+ 6. menus — ghi đè highlight của mục menu theo `data-menu-accent`.
19
+ 7. styles — ghi đè bo góc / đường kẻ / độ thoáng theo `data-style`.
20
+ Phải sau palettes vì hai bên khai chung `--radius`,
21
+ `--border`, `--input` ở cùng độ đặc hiệu (0,3,0), nên thứ
22
+ tự nạp mới là thứ quyết định. Cùng cái bẫy của (4).
23
+
24
+ HAI FILE CỐ Ý ĐỨNG NGOÀI danh sách này, và cùng một lý do: chúng chỉ có ích
25
+ cho app thật sự dùng tới, còn `index.css` thì mọi app đều nạp.
26
+
27
+ `charts.css` (trục `data-chart`, chọn bộ màu cho năm chuỗi biểu đồ) nạp riêng
28
+ bằng `@import "@vestow/theme/css/charts.css"`. Đo được **3.084 byte** CSS
29
+ thô — hơn cả `typeset.css`, mà app không có biểu đồ thì không dùng một dòng
30
+ nào. Bỏ qua nó KHÔNG làm gãy gì: biểu đồ vẫn lấy `--chart-*` theo bảng màu
31
+ như trước, chỉ là `data-chart` không còn tác dụng.
32
+
33
+ ⚠ Hệ quả phải biết: `themeController` VẪN ghi `data-chart` lên `<html>` dù
34
+ app có nạp file này hay không — controller không đọc được CSS. Gọi
35
+ `setChart('neutral')` trong một app quên `@import` thì thuộc tính hiện ra mà
36
+ màu không đổi, và không có gì báo. App nào phơi ô chọn bộ màu biểu đồ ra giao
37
+ diện thì phải nạp file này.
38
+
39
+ `typeset.css` CỐ Ý ĐỨNG NGOÀI danh sách này — nó là lớp văn xuôi opt-in, nạp
40
+ riêng bằng `@import "@vestow/theme/css/typeset.css"`. Đo được: gộp vào
41
+ đây thì mọi app phải trả **2.703 byte** CSS thô cho một lớp mà hôm nay chỉ
42
+ `vue/apps/kit` dựng ra.
43
+
44
+ `bases.css` thì Ở LẠI: nó chỉ khai hai token mỗi khối nên rẻ, và nó đổi MỌI
45
+ mặt trung tính — nền trang, card, viền, chữ mờ — nên app nào cũng thấy.
46
+
47
+ `styles.css` thì Ở LẠI (1.061 byte). Nó cũng là một trục theme như
48
+ `charts.css`, nên ranh giới ở đây không phải "trục thì vào, lớp thì ra" mà là
49
+ GIÁ trên SỐ NGƯỜI DÙNG: `data-style` đổi bo góc và độ thoáng của MỌI
50
+ component nên app nào cũng thấy, còn `data-chart` chỉ chạm vào biểu đồ.
51
+ ========================================================================== */
52
+
53
+ @import "./primitives.css";
54
+ @import "./tokens.css";
55
+ @import "./palettes.css";
56
+ @import "./surfaces.css";
57
+ @import "./bases.css";
58
+ @import "./menus.css";
59
+ @import "./styles.css";
60
+
61
+ /* 8. components — token của RIÊNG từng component, mỗi cái mặc định trỏ vào một
62
+ token ngữ nghĩa ở trên. Phải nạp SAU cả bảy file kia vì nó đọc chúng. */
63
+ @import "./components/button.css";
package/css/menus.css ADDED
@@ -0,0 +1,165 @@
1
+ /* =============================================================================
2
+ Trục HIGHLIGHT CỦA MENU — `data-menu-accent`, trục theme thứ bảy.
3
+
4
+ Trục nhỏ nhất trong bảy: mỗi khối chỉ khai HAI token. Nó đổi cách mục menu
5
+ đang trỏ tới được tô — mục hover, mục đang mở submenu, mục đang giữ tiêu điểm
6
+ bàn phím.
7
+
8
+ VÌ SAO KHÔNG ĐỔI THẲNG `--accent`: token đó còn được Sidebar, ô ngày trong
9
+ Calendar và vài chỗ hover khác đọc. Đổi nó là đổi cả những chỗ không ai xin.
10
+ `tokens.css` vì vậy tách ra `--menu-accent` / `--menu-accent-foreground`, mặc
11
+ định trỏ vào `--accent` nên không dịch một pixel nào cho tới khi trục này bật.
12
+ Người đọc hai token đó là `menu-styles.ts` của `src/variants` — bốn
13
+ chỗ, dùng chung cho DropdownMenu, ContextMenu, Menubar và Select.
14
+
15
+ `subtle` là mặc định nên KHÔNG có khối — cùng lẽ với `hyperion` / `solid` /
16
+ `default` / `theme` / `slate`.
17
+
18
+ Tương phản: `solid` dùng cặp `--primary` / `--primary-foreground`, đã nằm
19
+ trong 150 tổ hợp đo ở mục "Số đo sau khi neo" (thấp nhất 4,56). `muted` dùng
20
+ `--muted` với `--foreground`, tức mặt nền nhạt nhất với chữ đậm nhất — không
21
+ có cách nào để nó trượt.
22
+
23
+ ĐỘ ĐẶC HIỆU: attribute lặp ba lần. `tokens.css` khai hai token này ở `:root`
24
+ và `.dark`; khối `.dark` là (0,1,0) ngang với một attribute trần, nên ở chế độ
25
+ tối một `[data-menu-accent]` trần sẽ hoà rồi thua theo thứ tự nạp. Cùng cái
26
+ bẫy của `styles.css`, `charts.css` và `bases.css`.
27
+ ========================================================================== */
28
+
29
+ [data-menu-accent="solid"][data-menu-accent="solid"][data-menu-accent="solid"] {
30
+ --menu-accent: var(--primary);
31
+ --menu-accent-foreground: var(--primary-foreground);
32
+ }
33
+
34
+ /* Bản ĐẬM HƠN của `subtle`, không phải một màu khác.
35
+
36
+ Bản đầu của khối này viết `var(--muted)` và hoá ra là một no-op: đo được
37
+ `--muted` và `--accent` TRÙNG GIÁ TRỊ ở cả năm bảng màu (cùng nấc 100 của họ
38
+ trung tính). Một tuỳ chọn không đổi gì thì tệ hơn là không có tuỳ chọn — nó
39
+ bắt người dùng thử rồi tự hỏi mình bấm hụt chỗ nào.
40
+
41
+ Pha từ `--foreground` chứ không chốt một nấc xám, đúng cách `styles.css` làm
42
+ đường kẻ của style phẳng: một khai báo đúng cho cả hai chế độ, vì nền tối thì
43
+ `--foreground` gần trắng nên ra vệt sáng mờ. */
44
+ [data-menu-accent="muted"][data-menu-accent="muted"][data-menu-accent="muted"] {
45
+ --menu-accent: color-mix(in oklab, var(--foreground) 12%, transparent);
46
+ --menu-accent-foreground: var(--foreground);
47
+ }
48
+
49
+ /* =============================================================================
50
+ Trục MÀU CỦA MẶT MENU — `data-menu-color`, trục theme thứ tám.
51
+
52
+ `inverted` lật tấm menu sang phía đối diện của chế độ đang bật: menu tối trên
53
+ giao diện sáng, menu sáng trên giao diện tối.
54
+
55
+ KHÔNG ĐẺ TOKEN MỚI, và đây là chỗ khác `--menu-accent` ở đầu file. Token đó
56
+ buộc phải tách vì `--accent` bị đọc bởi Sidebar và Calendar — những phần tử
57
+ NGOÀI menu. Ở đây thì phạm vi đã tự khoanh bằng cây DOM: khối này đặt lại
58
+ `--popover` và bạn bè NGAY TRÊN phần tử `.ui-menu`, nên Popover và Tooltip
59
+ không dịch một pixel nào.
60
+
61
+ ĐỘ ĐẶC HIỆU: KHÔNG phải lặp attribute ba lần như `styles.css` / `charts.css`
62
+ / `bases.css`. Ba file đó khai lại token trên `:root`, tức đấu tay đôi với
63
+ khối `.dark` của `tokens.css`; còn ở đây khai trên một phần tử CON, mà khai
64
+ báo trực tiếp luôn thắng giá trị thừa kế bất kể độ đặc hiệu.
65
+
66
+ Giá trị lấy nguyên từ khối đối diện trong `tokens.css`, không nghĩ nấc mới —
67
+ nên cặp chữ/nền của menu lật đúng là cặp đã nằm trong 150 tổ hợp đã đo.
68
+
69
+ `--foreground` phải lật theo dù menu không dùng nó trực tiếp: `menu-accent`
70
+ ở giá trị `muted` pha từ chính nó, giữ nguyên là mục đang trỏ tới bị tô bằng
71
+ sắc của mặt nền BÊN NGOÀI menu.
72
+
73
+ `--border` thì KHÔNG chép: bản tối trong `tokens.css` là trắng 12% trong
74
+ suốt, mà một tấm menu tối nằm trên trang sáng thì viền đó tan mất. Pha từ
75
+ chính chữ của menu, đúng cách `styles.css` dựng đường kẻ của style phẳng. */
76
+ :root:not(.dark)[data-menu-color="inverted"] .ui-menu,
77
+ .light [data-menu-color="inverted"] .ui-menu {
78
+ --popover: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
79
+ --popover-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
80
+ --foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
81
+ --muted-foreground: oklch(from var(--color-slate-400) l var(--tint-c-400) var(--tint-hue));
82
+ --accent: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
83
+ --accent-foreground: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
84
+ --menu-surface: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
85
+ --border: color-mix(in oklab, var(--popover-foreground) 16%, transparent);
86
+ }
87
+
88
+ :root.dark[data-menu-color="inverted"] .ui-menu,
89
+ .dark [data-menu-color="inverted"] .ui-menu {
90
+ --popover: var(--color-white);
91
+ --popover-foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
92
+ --foreground: oklch(from var(--color-slate-900) l var(--tint-c-900) var(--tint-hue));
93
+ --muted-foreground: oklch(from var(--color-slate-500) l var(--tint-c-500) var(--tint-hue));
94
+ --accent: oklch(from var(--color-slate-100) l var(--tint-c-100) var(--tint-hue));
95
+ --accent-foreground: oklch(from var(--color-slate-800) l var(--tint-c-800) var(--tint-hue));
96
+ --menu-surface: var(--color-white);
97
+ --border: color-mix(in oklab, var(--popover-foreground) 16%, transparent);
98
+ }
99
+
100
+ /* =============================================================================
101
+ Trục VẬT LIỆU CỦA MẶT MENU — `data-menu-appearance`, trục theme thứ chín.
102
+
103
+ TỰ LOẠI MÌNH RA KHI THEME KÍNH ĐANG BẬT. Đó là điều tài liệu Liquid Glass của
104
+ Apple dặn thẳng: đừng chồng kính lên kính. Ở `data-surface="glass"` thì
105
+ `--popover` đã trong mờ và rule `[class~="bg-menu-surface"]` bên `surfaces.css`
106
+ đã gắn `backdrop-filter` cho đúng tấm menu này rồi — bật thêm ở đây là hai lớp
107
+ làm mờ chồng nhau, mà `backdrop-filter` không cộng dồn nên chỉ tổ đè mất bộ
108
+ lọc của theme kính.
109
+
110
+ Trộn từ `--popover` chứ không từ `--menu-surface`: một custom property tham
111
+ chiếu CHÍNH NÓ trên cùng phần tử là vòng lặp, CSS coi cả hai là không hợp lệ.
112
+ Đọc `--popover` thì vừa tránh được vòng lặp vừa tự ăn theo trục `inverted` ở
113
+ trên — hai trục ghép được, tấm menu lật màu vẫn trong mờ đúng nền của nó.
114
+
115
+ Độ đục 82%: dưới mức đó thì chữ mờ trong menu bắt đầu ăn màu của thứ nằm sau,
116
+ đúng ngưỡng mà mục "Tương phản trên nền kính" ở CLAUDE.md đã đo cho `--card`. */
117
+ :root:not([data-surface="glass"])[data-menu-appearance="translucent"] .ui-menu {
118
+ --menu-surface: color-mix(in oklab, var(--popover) 82%, transparent);
119
+
120
+ /* CHỮ MỜ PHẢI ĐẬM LÊN, nếu không trục này trượt AA.
121
+
122
+ Số đo trước khi sửa, lấy ở CẬN — tấm menu trong mờ nằm trên nội dung bất
123
+ kỳ nên phải kẹp giữa nền trắng thuần và đen thuần, không đo trên nền trang
124
+ tiện tay nhất:
125
+
126
+ | tổ hợp | chữ chính | chữ mờ |
127
+ | menu sáng (82%) | 11,68 | **3,12** |
128
+ | menu tối (82%) | 9,29 | **3,89** |
129
+
130
+ Hai đường chữa, đo cả hai rồi mới chọn:
131
+
132
+ | cách | chữ mờ |
133
+ | nâng độ đục 82 → 86% | 4,49 — **trượt** |
134
+ | nâng độ đục 82 → 90% | 5,14 — qua, nhưng hết trong |
135
+ | pha 68% về phía chữ chính | 4,19 — **vẫn trượt** |
136
+ | pha 78% về phía chữ chính | **5,48**, giữ nguyên 82% |
137
+
138
+ Chọn cách sau, đúng tiền lệ của mục "Tương phản trên nền kính": nâng độ đục
139
+ là hy sinh chính thứ trục này sinh ra để làm, còn màu chữ thì còn chỗ xoay.
140
+ Ở 5,48 chữ mờ vẫn nhạt hơn chữ chính rất xa (9,29–11,68) nên vẫn đọc ra là
141
+ "mờ".
142
+
143
+ Đừng chọn tỉ lệ bằng một phép quét viết tay trong JS: phép quét đó trộn
144
+ trong sRGB còn `color-mix(in oklab, …)` thì không, nên nó cho 4,57 ở mức
145
+ 68% trong khi giá trị THẬT là 4,19. Đặt số vào CSS rồi đọc lại computed
146
+ style mới là phép đo đúng.
147
+ Pha từ `--popover-foreground` và `--popover`, KHÔNG pha từ chính
148
+ `--muted-foreground`: nó được khai ngay trên phần tử này, nên đọc lại chính
149
+ nó là vòng lặp và CSS coi cả hai khai báo là không hợp lệ — cùng cái bẫy đã
150
+ ghi cho `--menu-surface` ngay dưới đây. */
151
+ --muted-foreground: color-mix(in oklab, var(--popover-foreground) 78%, var(--popover));
152
+
153
+ -webkit-backdrop-filter: blur(12px) saturate(1.5);
154
+ backdrop-filter: blur(12px) saturate(1.5);
155
+ }
156
+
157
+ /* Cùng lý do với khối kính: người dùng tắt hiệu ứng trong suốt thì menu phải
158
+ đục hẳn, không chỉ bớt mờ. */
159
+ @media (prefers-reduced-transparency: reduce) {
160
+ :root[data-menu-appearance="translucent"] .ui-menu {
161
+ --menu-surface: var(--popover);
162
+ -webkit-backdrop-filter: none;
163
+ backdrop-filter: none;
164
+ }
165
+ }
@@ -0,0 +1,292 @@
1
+ /* =============================================================================
2
+ Bảng màu thay thế, chép từ mockup "Vue3 Shadcn UI mockups".
3
+
4
+ Mỗi bảng chỉ khai lại những token nó đổi; phần còn lại vẫn kế thừa từ
5
+ tokens.css. Chọn bảng bằng `usePalette()` — nó đặt `data-palette` lên
6
+ thẻ <html>.
7
+
8
+ Selector khoá theo `.dark` là bắt buộc: nếu để `[data-palette="x"]` trần
9
+ thì file này nạp sau tokens.css sẽ đè luôn cả khối `.dark`, và bản tối
10
+ quay về màu của bản sáng.
11
+ ========================================================================== */
12
+
13
+ /* PHẠM VI: bảng màu chỉ đổi --primary và bộ trung tính (background, card,
14
+ secondary, muted, accent, border, sidebar…).
15
+
16
+ KHÔNG đụng tới --destructive / --success / --warning / --info. Hai lý do:
17
+ - Eclipse từng đặt chroma 0 cho chúng -> thông báo lỗi thành màu xám, mất
18
+ hết ý nghĩa. Bảng màu đơn sắc là nói về primary, không phải về trạng thái.
19
+ - Ba bảng kia ghi đè bằng giá trị riêng, vô hiệu hoá luôn phần chỉnh tương
20
+ phản làm ở tokens.css (--success sáng từng chỉ đạt 3.57:1).
21
+
22
+ Cũng không khai --code-* ở đây: xem ghi chú trong CLAUDE.md. */
23
+
24
+ /* MỘT HỌ TRUNG TÍNH cho mỗi bảng, ở CẢ khối sáng lẫn khối tối:
25
+ solar -> stone, nebula -> zinc, aurora -> mist, eclipse -> neutral.
26
+ Chỉ --primary / --accent / --sidebar-accent / --chart-* được ra khỏi họ đó.
27
+
28
+ Ở chroma 0,006 thì góc hue gần như vô nghĩa về mặt số học nên rất dễ mượn nhầm
29
+ họ mà không ai thấy: khối tối của nebula từng lấy gray + mauve, của aurora từng
30
+ lấy gray, và card lệch tông so với nền tới ΔE 0,03 (bốn ca đúng đều ≤ 0,002).
31
+ check-tokens.mjs KHÔNG bắt được — nó chỉ hỏi tên token, không hỏi họ màu. */
32
+
33
+ /* --- Solar: Cam nóng trên nền than ấm --- */
34
+
35
+ :root:not(.dark)[data-palette="solar"],
36
+ .light[data-palette="solar"] {
37
+ --tint-hue: 56.043;
38
+ --background:oklch(from var(--color-stone-50) l var(--tint-c-50) var(--tint-hue));
39
+ --foreground:oklch(from var(--color-stone-900) l var(--tint-c-900) var(--tint-hue));
40
+ --card:var(--color-white);
41
+ --popover:var(--color-white);
42
+ --primary:var(--color-orange-700);
43
+ --primary-foreground:oklch(from var(--color-stone-50) l var(--tint-c-50) var(--tint-hue));
44
+ --secondary:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
45
+ --secondary-foreground:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
46
+ --muted:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
47
+ --muted-foreground:oklch(from var(--color-stone-500) l var(--tint-c-500) var(--tint-hue));
48
+ --accent:var(--color-orange-100);
49
+ --accent-foreground:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
50
+ --border:oklch(from var(--color-stone-200) l var(--tint-c-200) var(--tint-hue));
51
+ --input:oklch(from var(--color-stone-200) l var(--tint-c-200) var(--tint-hue));
52
+ --ring:var(--color-orange-700);
53
+ --sidebar:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
54
+ --sidebar-accent:oklch(from var(--color-stone-200) l var(--tint-c-200) var(--tint-hue));
55
+ --radius:0.625rem;
56
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
57
+ Khoảng cách xám nhỏ nhất 0,048; thấp nhất 3,2:1 trên mặt card. */
58
+ --chart-1: var(--color-orange-700);
59
+ --chart-2: var(--color-yellow-800);
60
+ --chart-3: var(--color-rose-500);
61
+ --chart-4: var(--color-red-900);
62
+ --chart-5: var(--color-amber-600);
63
+ }
64
+
65
+ :root.dark[data-palette="solar"],
66
+ .dark[data-palette="solar"] {
67
+ --tint-hue: 56.043;
68
+ /* Hue cam-vàng cũng hẹp ở vùng tối: nấc 950 trần 0,045 nên kẹp ở 85%. */
69
+ --tint-c-950: calc(0.038 * var(--tint-scale));
70
+ --background:oklch(from var(--color-stone-950) l var(--tint-c-950) var(--tint-hue));
71
+ --foreground:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
72
+ --card:oklch(from var(--color-stone-900) l var(--tint-c-900) var(--tint-hue));
73
+ --popover:oklch(from var(--color-stone-900) l var(--tint-c-900) var(--tint-hue));
74
+ --primary:var(--color-orange-700);
75
+ --primary-foreground:oklch(from var(--color-stone-50) l var(--tint-c-50) var(--tint-hue));
76
+ --secondary:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
77
+ --secondary-foreground:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
78
+ --muted:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
79
+ --muted-foreground:oklch(from var(--color-stone-400) l var(--tint-c-400) var(--tint-hue));
80
+ --accent:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
81
+ --accent-foreground:oklch(from var(--color-stone-100) l var(--tint-c-100) var(--tint-hue));
82
+ --border:color-mix(in oklab, var(--color-white) 13%, transparent);
83
+ --input:color-mix(in oklab, var(--color-white) 17%, transparent);
84
+ --ring:var(--color-orange-700);
85
+ --sidebar:oklch(from var(--color-stone-900) l var(--tint-c-900) var(--tint-hue));
86
+ --sidebar-accent:oklch(from var(--color-stone-800) l var(--tint-c-800) var(--tint-hue));
87
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
88
+ Khoảng cách xám nhỏ nhất 0,072; thấp nhất 3,67:1 trên mặt card. */
89
+ --chart-1: var(--color-orange-600);
90
+ --chart-2: var(--color-yellow-400);
91
+ --chart-3: var(--color-rose-400);
92
+ --chart-4: var(--color-red-600);
93
+ --chart-5: var(--color-amber-500);
94
+ }
95
+
96
+ /* --- Nebula: Tím điện, nền trung tính lạnh --- */
97
+
98
+ :root:not(.dark)[data-palette="nebula"],
99
+ .light[data-palette="nebula"] {
100
+ --tint-hue: 285.885;
101
+ --background:var(--color-white);
102
+ --foreground:oklch(from var(--color-zinc-900) l var(--tint-c-900) var(--tint-hue));
103
+ --card:var(--color-white);
104
+ --popover:var(--color-white);
105
+ --primary:var(--color-purple-600);
106
+ --primary-foreground:oklch(from var(--color-zinc-50) l var(--tint-c-50) var(--tint-hue));
107
+ --secondary:oklch(from var(--color-zinc-100) l var(--tint-c-100) var(--tint-hue));
108
+ --secondary-foreground:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
109
+ --muted:oklch(from var(--color-zinc-100) l var(--tint-c-100) var(--tint-hue));
110
+ --muted-foreground:oklch(from var(--color-zinc-500) l var(--tint-c-500) var(--tint-hue));
111
+ --accent:var(--color-purple-100);
112
+ --accent-foreground:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
113
+ --border:oklch(from var(--color-zinc-200) l var(--tint-c-200) var(--tint-hue));
114
+ --input:oklch(from var(--color-zinc-200) l var(--tint-c-200) var(--tint-hue));
115
+ --ring:var(--color-purple-600);
116
+ --sidebar:oklch(from var(--color-zinc-50) l var(--tint-c-50) var(--tint-hue));
117
+ --sidebar-accent:var(--color-violet-100);
118
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
119
+ Khoảng cách xám nhỏ nhất 0,043; thấp nhất 3,13:1 trên mặt card. */
120
+ --chart-1: var(--color-purple-600);
121
+ --chart-2: var(--color-pink-500);
122
+ --chart-3: var(--color-indigo-400);
123
+ --chart-4: var(--color-fuchsia-800);
124
+ --chart-5: var(--color-violet-500);
125
+ }
126
+
127
+ :root.dark[data-palette="nebula"],
128
+ .dark[data-palette="nebula"] {
129
+ --tint-hue: 285.885;
130
+ --background:oklch(from var(--color-zinc-950) l var(--tint-c-950) var(--tint-hue));
131
+ --foreground:oklch(from var(--color-zinc-100) l var(--tint-c-100) var(--tint-hue));
132
+ --card:oklch(from var(--color-zinc-900) l var(--tint-c-900) var(--tint-hue));
133
+ --popover:oklch(from var(--color-zinc-900) l var(--tint-c-900) var(--tint-hue));
134
+ --primary:var(--color-purple-600);
135
+ --primary-foreground:oklch(from var(--color-zinc-50) l var(--tint-c-50) var(--tint-hue));
136
+ --secondary:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
137
+ --secondary-foreground:oklch(from var(--color-zinc-100) l var(--tint-c-100) var(--tint-hue));
138
+ --muted:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
139
+ --muted-foreground:oklch(from var(--color-zinc-400) l var(--tint-c-400) var(--tint-hue));
140
+ --accent:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
141
+ --accent-foreground:oklch(from var(--color-zinc-100) l var(--tint-c-100) var(--tint-hue));
142
+ --border:color-mix(in oklab, var(--color-white) 13%, transparent);
143
+ --input:color-mix(in oklab, var(--color-white) 17%, transparent);
144
+ --ring:var(--color-purple-600);
145
+ --sidebar:oklch(from var(--color-zinc-900) l var(--tint-c-900) var(--tint-hue));
146
+ --sidebar-accent:oklch(from var(--color-zinc-800) l var(--tint-c-800) var(--tint-hue));
147
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
148
+ Khoảng cách xám nhỏ nhất 0,041; thấp nhất 4,31:1 trên mặt card. */
149
+ --chart-1: var(--color-purple-500);
150
+ --chart-2: var(--color-pink-300);
151
+ --chart-3: var(--color-indigo-300);
152
+ --chart-4: var(--color-fuchsia-500);
153
+ --chart-5: var(--color-violet-400);
154
+ }
155
+
156
+ /* --- Aurora: Xanh mòng két trên navy sâu --- */
157
+
158
+ :root:not(.dark)[data-palette="aurora"],
159
+ .light[data-palette="aurora"] {
160
+ --tint-hue: 223.9;
161
+ --background:var(--color-white);
162
+ --foreground:oklch(from var(--color-mist-900) l var(--tint-c-900) var(--tint-hue));
163
+ --card:var(--color-white);
164
+ --popover:var(--color-white);
165
+ --primary:var(--color-teal-700);
166
+ --primary-foreground:oklch(from var(--color-mist-50) l var(--tint-c-50) var(--tint-hue));
167
+ --secondary:oklch(from var(--color-mist-100) l var(--tint-c-100) var(--tint-hue));
168
+ --secondary-foreground:oklch(from var(--color-mist-800) l var(--tint-c-800) var(--tint-hue));
169
+ --muted:oklch(from var(--color-mist-100) l var(--tint-c-100) var(--tint-hue));
170
+ --muted-foreground:oklch(from var(--color-mist-500) l var(--tint-c-500) var(--tint-hue));
171
+ --accent:var(--color-sky-100);
172
+ --accent-foreground:oklch(from var(--color-mist-800) l var(--tint-c-800) var(--tint-hue));
173
+ --border:oklch(from var(--color-mist-200) l var(--tint-c-200) var(--tint-hue));
174
+ --input:oklch(from var(--color-mist-200) l var(--tint-c-200) var(--tint-hue));
175
+ --ring:var(--color-teal-700);
176
+ --sidebar:oklch(from var(--color-mist-50) l var(--tint-c-50) var(--tint-hue));
177
+ --sidebar-accent:oklch(from var(--color-mist-200) l var(--tint-c-200) var(--tint-hue));
178
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
179
+ Khoảng cách xám nhỏ nhất 0,035; thấp nhất 3,22:1 trên mặt card. */
180
+ --chart-1: var(--color-teal-700);
181
+ --chart-2: var(--color-sky-900);
182
+ --chart-3: var(--color-green-600);
183
+ --chart-4: var(--color-cyan-800);
184
+ --chart-5: var(--color-emerald-600);
185
+ }
186
+
187
+ :root.dark[data-palette="aurora"],
188
+ .dark[data-palette="aurora"] {
189
+ --tint-hue: 223.9;
190
+ /* Hue teal-lam nằm đúng đáy gamut sRGB ở vùng tối: trần chỉ 0,033 (950)
191
+ và 0,043 (900), nên hai nấc này kẹp ở 85% trần thay vì theo slate. */
192
+ --tint-c-900: calc(0.037 * var(--tint-scale));
193
+ --tint-c-950: calc(0.028 * var(--tint-scale));
194
+ --background:oklch(from var(--color-mist-950) l var(--tint-c-950) var(--tint-hue));
195
+ --foreground:oklch(from var(--color-mist-100) l var(--tint-c-100) var(--tint-hue));
196
+ --card:oklch(from var(--color-mist-900) l var(--tint-c-900) var(--tint-hue));
197
+ --popover:oklch(from var(--color-mist-900) l var(--tint-c-900) var(--tint-hue));
198
+ --primary:var(--color-teal-700);
199
+ --primary-foreground:oklch(from var(--color-mist-50) l var(--tint-c-50) var(--tint-hue));
200
+ --secondary:oklch(from var(--color-mist-800) l var(--tint-c-800) var(--tint-hue));
201
+ --secondary-foreground:oklch(from var(--color-mist-100) l var(--tint-c-100) var(--tint-hue));
202
+ --muted:oklch(from var(--color-mist-800) l var(--tint-c-800) var(--tint-hue));
203
+ --muted-foreground:oklch(from var(--color-mist-400) l var(--tint-c-400) var(--tint-hue));
204
+ --accent:var(--color-cyan-950);
205
+ --accent-foreground:oklch(from var(--color-mist-100) l var(--tint-c-100) var(--tint-hue));
206
+ --border:color-mix(in oklab, var(--color-white) 13%, transparent);
207
+ --input:color-mix(in oklab, var(--color-white) 17%, transparent);
208
+ --ring:var(--color-teal-700);
209
+ --sidebar:oklch(from var(--color-mist-900) l var(--tint-c-900) var(--tint-hue));
210
+ --sidebar-accent:var(--color-cyan-950);
211
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
212
+ Khoảng cách xám nhỏ nhất 0,074; thấp nhất 3,59:1 trên mặt card. */
213
+ --chart-1: var(--color-teal-600);
214
+ --chart-2: var(--color-sky-300);
215
+ --chart-3: var(--color-green-700);
216
+ --chart-4: var(--color-cyan-500);
217
+ --chart-5: var(--color-emerald-400);
218
+ }
219
+
220
+ /* Eclipse = default theme của shadcn, chép nguyên. Cả chín giá trị xám shadcn
221
+ dùng đều trùng KHÍT một nấc của thang `neutral` trong Tailwind (0.145 -> 950,
222
+ 0.205 -> 900, 0.269 -> 800, 0.371 -> 700, 0.556 -> 500, 0.708 -> 400,
223
+ 0.922 -> 200, 0.97 -> 100, 0.985 -> 50) nên viết bằng token là khớp tuyệt đối,
224
+ không phải chép số. */
225
+ /* --- Eclipse: Đen trắng thuần, chroma bằng 0 --- */
226
+
227
+ :root:not(.dark)[data-palette="eclipse"],
228
+ .light[data-palette="eclipse"] {
229
+ /* Đơn sắc: tắt trục ám màu, xem --tint-scale trong tokens.css. */
230
+ --tint-scale: 0;
231
+ --background:var(--color-white);
232
+ --foreground:var(--color-black);
233
+ --card:var(--color-white);
234
+ --card-foreground:var(--color-black);
235
+ --popover:var(--color-white);
236
+ --popover-foreground:var(--color-black);
237
+ --primary:var(--color-black);
238
+ --primary-foreground:var(--color-neutral-50);
239
+ --secondary:var(--color-neutral-100);
240
+ --secondary-foreground:var(--color-neutral-900);
241
+ --muted:var(--color-neutral-100);
242
+ --muted-foreground:var(--color-neutral-500);
243
+ --accent:var(--color-neutral-100);
244
+ --accent-foreground:var(--color-neutral-900);
245
+ --border:var(--color-neutral-200);
246
+ --input:var(--color-neutral-200);
247
+ /* neutral-400 chỉ đạt 2.58:1 trên nền card — chỉ báo focus cần tối thiểu
248
+ 3:1 (WCAG 2.4.11). neutral-500 lên 4.68 mà vẫn đơn sắc. Bốn bảng kia
249
+ dùng primary có màu nên không dính; Eclipse lấy chính sắc xám làm ring. */
250
+ --ring:var(--color-neutral-500);
251
+ --sidebar:var(--color-neutral-50);
252
+ --sidebar-accent:var(--color-neutral-100);
253
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
254
+ Khoảng cách xám nhỏ nhất 0,016; thấp nhất 4,74:1 trên mặt card. */
255
+ --chart-1: var(--color-neutral-800);
256
+ --chart-2: var(--color-neutral-500);
257
+ --chart-3: var(--color-neutral-600);
258
+ --chart-4: var(--color-neutral-700);
259
+ --chart-5: var(--color-neutral-950);
260
+ }
261
+
262
+ :root.dark[data-palette="eclipse"],
263
+ .dark[data-palette="eclipse"] {
264
+ /* Đơn sắc: tắt trục ám màu, xem --tint-scale trong tokens.css. */
265
+ --tint-scale: 0;
266
+ --background:var(--color-neutral-950);
267
+ --foreground:var(--color-neutral-50);
268
+ --card:var(--color-neutral-900);
269
+ --card-foreground:var(--color-neutral-50);
270
+ --popover:var(--color-neutral-900);
271
+ --popover-foreground:var(--color-neutral-50);
272
+ --primary:var(--color-neutral-200);
273
+ --primary-foreground:var(--color-neutral-900);
274
+ --secondary:var(--color-neutral-800);
275
+ --secondary-foreground:var(--color-neutral-50);
276
+ --muted:var(--color-neutral-800);
277
+ --muted-foreground:var(--color-neutral-400);
278
+ --accent:var(--color-neutral-700);
279
+ --accent-foreground:var(--color-neutral-50);
280
+ --border:color-mix(in oklab, var(--color-white) 10%, transparent);
281
+ --input:color-mix(in oklab, var(--color-white) 15%, transparent);
282
+ --ring:var(--color-neutral-500);
283
+ --sidebar:var(--color-neutral-900);
284
+ --sidebar-accent:var(--color-neutral-800);
285
+ /* Màu chuỗi biểu đồ, lấy các họ lân cận sắc của --primary.
286
+ Khoảng cách xám nhỏ nhất 0,125; thấp nhất 3,78:1 trên mặt card. */
287
+ --chart-1: var(--color-neutral-300);
288
+ --chart-2: var(--color-neutral-100);
289
+ --chart-3: var(--color-neutral-200);
290
+ --chart-4: var(--color-neutral-400);
291
+ --chart-5: var(--color-neutral-500);
292
+ }