@tasco-fe/theme 0.0.0-stage → 3.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tasco Framework Core contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @tasco-fe/theme
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Design token (single source of truth) cho thương hiệu Tasco, suy ra:
4
+ - **antdv theme** — đối tượng cho prop `theme` của `<a-config-provider>`
5
+ - **CSS variables** `--tasco-*` cho style ngoài antdv
6
+
7
+ Package TS thuần, **không** phụ thuộc runtime vào ant-design-vue.
8
+
9
+ ## API
10
+ ```ts
11
+ import {
12
+ lightTokens, darkTokens, tokensByMode, // token gốc
13
+ getAntdTheme, antdThemeLight, antdThemeDark, type ThemeMode, type AntdThemeConfig,
14
+ cssVars, cssVarsText,
15
+ } from '@tasco-fe/theme'
16
+ ```
17
+
18
+ ## Dùng với ConfigProvider (sẽ đặt trong @tasco-fe/nuxt-layer-base)
19
+ ```vue
20
+ <script setup lang="ts">
21
+ import { theme } from 'ant-design-vue'
22
+ import { getAntdTheme } from '@tasco-fe/theme'
23
+
24
+ const mode = 'light'
25
+ const antdTheme = {
26
+ ...getAntdTheme(mode),
27
+ algorithm: mode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <a-config-provider :theme="antdTheme"><slot /></a-config-provider>
33
+ </template>
34
+ ```
35
+ > `algorithm` set ở layer (nơi có antdv) để theme package giữ độc lập.
36
+
37
+ ## Inject CSS variables
38
+ ```ts
39
+ import { cssVarsText } from '@tasco-fe/theme'
40
+ // trong nuxt.config app.head:
41
+ useHead({ style: [{ innerHTML: cssVarsText('light') }] })
42
+ ```
43
+
44
+ ## Tuỳ biến
45
+ Sửa `src/tokens.ts` — đổi 1 chỗ, lan toả cả antdv lẫn CSS vars. Thay placeholder màu bằng brand guideline thật.
@@ -0,0 +1,199 @@
1
+ /** Bảng màu theo vai trò. Mỗi field phát ra một biến CSS `--tasco-color-<tên-kebab>` (xem `cssVars`). */
2
+ interface ColorTokens {
3
+ /** Màu thương hiệu chính (navy): nút chính, header bảng, link đang chọn. Theme tối là màu chữ. */
4
+ primary: string;
5
+ /** Trạng thái hover của `primary`. */
6
+ primaryHover: string;
7
+ /** Trạng thái nhấn của `primary`; nền đặt chữ trắng ở theme tối. */
8
+ primaryActive: string;
9
+ /** Nền mờ của primary (chip, hover nhẹ). */
10
+ primarySoft: string;
11
+ /** Màu nền thương hiệu (pill menu active); chữ trên nền này dùng navy `sidebarBottom`. */
12
+ accent: string;
13
+ /** Nền mờ của accent. */
14
+ accentSoft: string;
15
+ /** Chữ/icon màu accent trên nền sáng (accentSoft, surface) — `accent` quá sáng để làm chữ. */
16
+ accentText: string;
17
+ /** Thành công — màu nền và seed antdv; làm chữ thì dùng `successText`. */
18
+ success: string;
19
+ /** Nền mờ của success. */
20
+ successSoft: string;
21
+ /** Chữ/icon màu success trên nền sáng (successSoft, surface). */
22
+ successText: string;
23
+ /** Cảnh báo — màu nền và seed antdv; làm chữ thì dùng `warningText`. */
24
+ warning: string;
25
+ /** Nền mờ của warning. */
26
+ warningSoft: string;
27
+ /** Chữ/icon màu warning trên nền sáng (warningSoft, surface). */
28
+ warningText: string;
29
+ /** Lỗi, hành động phá huỷ — dùng làm chữ được ngay. */
30
+ error: string;
31
+ /** Nền mờ của error. */
32
+ errorSoft: string;
33
+ /** Thông tin trung tính — dùng làm chữ được ngay. */
34
+ info: string;
35
+ /** Nền mờ của info; cũng là nền hàng đang chọn của bảng. */
36
+ infoSoft: string;
37
+ /** Màu liên kết. */
38
+ link: string;
39
+ /** Nền mờ của liên kết. */
40
+ linkSoft: string;
41
+ /** Chữ chính. */
42
+ text: string;
43
+ /** Chữ phụ: mô tả, nhãn trường. */
44
+ textSecondary: string;
45
+ /** Chữ mờ: chú thích, footer — vẫn đạt tương phản AA. */
46
+ textMuted: string;
47
+ /** Chữ nhạt nhất: placeholder, disabled — dưới ngưỡng AA, không dùng cho nội dung cần đọc. */
48
+ textSubtle: string;
49
+ /** Nền mặc định của card/panel. */
50
+ surface: string;
51
+ /** Nền nổi (popover/dropdown/modal). */
52
+ surfaceElevated: string;
53
+ /** Nền dịu (section, header bảng nhạt). */
54
+ surfaceMuted: string;
55
+ /** Nền trang. */
56
+ page: string;
57
+ /** Viền mặc định. */
58
+ border: string;
59
+ /** Viền đậm: nút outline, ô nhập khi cần nổi bật. */
60
+ borderStrong: string;
61
+ /** Điểm đầu gradient dọc của sidebar. */
62
+ sidebarTop: string;
63
+ /** Điểm cuối gradient sidebar; cũng là màu chữ trên nền `accent`. */
64
+ sidebarBottom: string;
65
+ }
66
+ /**
67
+ * Mật độ component (px). `controlHeight*` áp qua global token của antdv; phần còn lại phát ra biến CSS để
68
+ * `base.css` ghi đè padding/margin, vì antdv 4 bỏ qua `theme.components`.
69
+ */
70
+ interface DensityTokens {
71
+ /** Chiều cao control cỡ nhỏ (px) — Input/Select/Button/Picker `size="small"`. */
72
+ controlHeightSM: number;
73
+ /** Chiều cao control mặc định (px). */
74
+ controlHeight: number;
75
+ /** Chiều cao control cỡ lớn (px). */
76
+ controlHeightLG: number;
77
+ /** Padding dọc thân Card (px). */
78
+ cardPaddingBlock: number;
79
+ /** Padding ngang thân và header Card (px). */
80
+ cardPaddingInline: number;
81
+ /** Chiều cao header Card (px). */
82
+ cardHeadingHeight: number;
83
+ /** Cỡ chữ tiêu đề Card (px). */
84
+ cardHeadingFontSize: number;
85
+ /** Khoảng cách dưới mỗi Form item (px). */
86
+ formItemMarginBottom: number;
87
+ /** Padding dọc ô Table (px). */
88
+ tableCellPaddingBlock: number;
89
+ /** Padding ngang ô Table (px). */
90
+ tableCellPaddingInline: number;
91
+ /** Padding thân Modal / Drawer (px). */
92
+ modalBodyPadding: number;
93
+ }
94
+ /** Toàn bộ design token của một chế độ (sáng hoặc tối). */
95
+ interface TascoTokens {
96
+ /** Bảng màu theo vai trò. */
97
+ color: ColorTokens;
98
+ /** Bo góc cơ bản (px). */
99
+ radius: number;
100
+ /** Bo góc lớn (card, modal) (px). */
101
+ radiusLg: number;
102
+ /** Font chữ thân (Quicksand + font hệ thống dự phòng). */
103
+ fontFamily: string;
104
+ /** Font cho tiêu đề/heading (có thể trùng fontFamily). */
105
+ fontFamilyHeading: string;
106
+ /** Cỡ chữ cơ bản (px). */
107
+ fontSize: number;
108
+ /** Chiều cao control cơ bản (px) — alias của density.controlHeight. */
109
+ controlHeight: number;
110
+ /** Mật độ component (height/padding/margin). */
111
+ density: DensityTokens;
112
+ /** Đơn vị spacing cơ bản (px). */
113
+ spacingUnit: number;
114
+ /** Gradient cho nút/hero primary. */
115
+ gradientPrimary: string;
116
+ /** Gradient cho nút danger. */
117
+ gradientDanger: string;
118
+ /** Shadow card. */
119
+ shadowCard: string;
120
+ /** Shadow phần tử nổi (dropdown/modal). */
121
+ shadowRaised: string;
122
+ }
123
+ /** Token chế độ sáng. */
124
+ declare const lightTokens: TascoTokens;
125
+ /** Token chế độ tối. */
126
+ declare const darkTokens: TascoTokens;
127
+ /** Token theo chế độ — tra bằng `ThemeMode`. */
128
+ declare const tokensByMode: {
129
+ readonly light: TascoTokens;
130
+ readonly dark: TascoTokens;
131
+ };
132
+
133
+ /** Chế độ giao diện. */
134
+ type ThemeMode = 'light' | 'dark';
135
+ /** Cấu hình `theme` cho `<a-config-provider>` — tương thích cấu trúc với prop `theme` của antdv. */
136
+ interface AntdThemeConfig {
137
+ /** Global token của antdv (màu seed, font, bo góc, chiều cao control…). */
138
+ token?: Record<string, unknown>;
139
+ /** Token cấp component — antdv 4.2.6 BỎ QUA trường này; tinh chỉnh component làm bằng CSS trong `base.css`. */
140
+ components?: Record<string, Record<string, unknown>>;
141
+ }
142
+ /**
143
+ * Cấu hình `theme` của antdv cho một chế độ. Chưa gồm `algorithm` — nơi có antdv tự thêm
144
+ * `theme.darkAlgorithm`/`theme.defaultAlgorithm` (layer làm trong `app.vue`).
145
+ */
146
+ declare function getAntdTheme(mode?: ThemeMode): AntdThemeConfig;
147
+ /** `getAntdTheme('light')` tính sẵn. */
148
+ declare const antdThemeLight: AntdThemeConfig;
149
+ /** `getAntdTheme('dark')` tính sẵn. */
150
+ declare const antdThemeDark: AntdThemeConfig;
151
+
152
+ /** Map tên biến CSS `--tasco-*` → giá trị của một chế độ; số đo đã kèm đơn vị `px`. */
153
+ declare function cssVars(mode?: ThemeMode): Record<string, string>;
154
+ /**
155
+ * Sinh chuỗi CSS để inject vào `<head>` (vd qua `app.head.style` trong layer).
156
+ * @param selector mặc định `:root`.
157
+ */
158
+ declare function cssVarsText(mode?: ThemeMode, selector?: string): string;
159
+
160
+ /**
161
+ * Relative luminance theo WCAG 2.x: 0 (đen) → 1 (trắng). Nhận hex `#rgb` / `#rrggbb`, không phân biệt hoa thường;
162
+ * chuỗi khác (tên màu, `rgb()`, hex có alpha) → ném lỗi.
163
+ */
164
+ declare function luminanceOf(color: string): number;
165
+ /**
166
+ * Tỉ lệ tương phản WCAG 2.x giữa hai màu: 1 (trùng màu) → 21 (đen/trắng); đổi chỗ chữ và nền cho cùng kết quả.
167
+ * Ngưỡng AA: ≥ 4.5 cho chữ thường, ≥ 3 cho chữ lớn và thành phần đồ hoạ. Kết quả không làm tròn vì WCAG không cho
168
+ * làm tròn lên (4.499 là chưa đạt 4.5).
169
+ */
170
+ declare function contrastRatioOf(foreground: string, background: string): number;
171
+
172
+ /** Ngưỡng tương phản WCAG 2.x AA cho chữ cỡ thường. */
173
+ declare const AA_TEXT_CONTRAST = 4.5;
174
+ /** Một cặp màu chữ/nền cần đạt tương phản AA. */
175
+ interface ContrastPair {
176
+ /** Màu chữ: tên biến CSS bỏ tiền tố `--tasco-` (vd `color-primary`) hoặc hex cố định (`#fff`). */
177
+ fg: string;
178
+ /** Màu nền, ghi như `fg`; gradient thì xét mọi điểm màu. */
179
+ bg: string;
180
+ /** Nơi dùng — đổi token là biết component nào bị ảnh hưởng. */
181
+ usedBy: string;
182
+ /** Chỉ áp dụng ở các chế độ này; bỏ trống = cả hai. */
183
+ modes?: ThemeMode[];
184
+ /**
185
+ * Chế độ đang biết là CHƯA đạt ngưỡng → lý do. Test khẳng định cặp vẫn dưới ngưỡng, nên khi sửa đạt rồi test sẽ
186
+ * đỏ nhắc xoá mục này để cặp được khoá như các cặp khác.
187
+ */
188
+ knownFailures?: Partial<Record<ThemeMode, string>>;
189
+ }
190
+ /** Cặp chữ/nền đã khoá tương phản AA, xếp theo nhóm màu. */
191
+ declare const contrastPairs: readonly ContrastPair[];
192
+ /**
193
+ * Tương phản thấp nhất của một cặp ở một chế độ: nền gradient thì lấy điểm màu tệ nhất. Tên biến không tồn tại →
194
+ * ném lỗi.
195
+ */
196
+ declare function pairContrastOf(pair: ContrastPair, mode: ThemeMode): number;
197
+
198
+ export { AA_TEXT_CONTRAST, antdThemeDark, antdThemeLight, contrastPairs, contrastRatioOf, cssVars, cssVarsText, darkTokens, getAntdTheme, lightTokens, luminanceOf, pairContrastOf, tokensByMode };
199
+ export type { AntdThemeConfig, ColorTokens, ContrastPair, DensityTokens, TascoTokens, ThemeMode };
@@ -0,0 +1,199 @@
1
+ /** Bảng màu theo vai trò. Mỗi field phát ra một biến CSS `--tasco-color-<tên-kebab>` (xem `cssVars`). */
2
+ interface ColorTokens {
3
+ /** Màu thương hiệu chính (navy): nút chính, header bảng, link đang chọn. Theme tối là màu chữ. */
4
+ primary: string;
5
+ /** Trạng thái hover của `primary`. */
6
+ primaryHover: string;
7
+ /** Trạng thái nhấn của `primary`; nền đặt chữ trắng ở theme tối. */
8
+ primaryActive: string;
9
+ /** Nền mờ của primary (chip, hover nhẹ). */
10
+ primarySoft: string;
11
+ /** Màu nền thương hiệu (pill menu active); chữ trên nền này dùng navy `sidebarBottom`. */
12
+ accent: string;
13
+ /** Nền mờ của accent. */
14
+ accentSoft: string;
15
+ /** Chữ/icon màu accent trên nền sáng (accentSoft, surface) — `accent` quá sáng để làm chữ. */
16
+ accentText: string;
17
+ /** Thành công — màu nền và seed antdv; làm chữ thì dùng `successText`. */
18
+ success: string;
19
+ /** Nền mờ của success. */
20
+ successSoft: string;
21
+ /** Chữ/icon màu success trên nền sáng (successSoft, surface). */
22
+ successText: string;
23
+ /** Cảnh báo — màu nền và seed antdv; làm chữ thì dùng `warningText`. */
24
+ warning: string;
25
+ /** Nền mờ của warning. */
26
+ warningSoft: string;
27
+ /** Chữ/icon màu warning trên nền sáng (warningSoft, surface). */
28
+ warningText: string;
29
+ /** Lỗi, hành động phá huỷ — dùng làm chữ được ngay. */
30
+ error: string;
31
+ /** Nền mờ của error. */
32
+ errorSoft: string;
33
+ /** Thông tin trung tính — dùng làm chữ được ngay. */
34
+ info: string;
35
+ /** Nền mờ của info; cũng là nền hàng đang chọn của bảng. */
36
+ infoSoft: string;
37
+ /** Màu liên kết. */
38
+ link: string;
39
+ /** Nền mờ của liên kết. */
40
+ linkSoft: string;
41
+ /** Chữ chính. */
42
+ text: string;
43
+ /** Chữ phụ: mô tả, nhãn trường. */
44
+ textSecondary: string;
45
+ /** Chữ mờ: chú thích, footer — vẫn đạt tương phản AA. */
46
+ textMuted: string;
47
+ /** Chữ nhạt nhất: placeholder, disabled — dưới ngưỡng AA, không dùng cho nội dung cần đọc. */
48
+ textSubtle: string;
49
+ /** Nền mặc định của card/panel. */
50
+ surface: string;
51
+ /** Nền nổi (popover/dropdown/modal). */
52
+ surfaceElevated: string;
53
+ /** Nền dịu (section, header bảng nhạt). */
54
+ surfaceMuted: string;
55
+ /** Nền trang. */
56
+ page: string;
57
+ /** Viền mặc định. */
58
+ border: string;
59
+ /** Viền đậm: nút outline, ô nhập khi cần nổi bật. */
60
+ borderStrong: string;
61
+ /** Điểm đầu gradient dọc của sidebar. */
62
+ sidebarTop: string;
63
+ /** Điểm cuối gradient sidebar; cũng là màu chữ trên nền `accent`. */
64
+ sidebarBottom: string;
65
+ }
66
+ /**
67
+ * Mật độ component (px). `controlHeight*` áp qua global token của antdv; phần còn lại phát ra biến CSS để
68
+ * `base.css` ghi đè padding/margin, vì antdv 4 bỏ qua `theme.components`.
69
+ */
70
+ interface DensityTokens {
71
+ /** Chiều cao control cỡ nhỏ (px) — Input/Select/Button/Picker `size="small"`. */
72
+ controlHeightSM: number;
73
+ /** Chiều cao control mặc định (px). */
74
+ controlHeight: number;
75
+ /** Chiều cao control cỡ lớn (px). */
76
+ controlHeightLG: number;
77
+ /** Padding dọc thân Card (px). */
78
+ cardPaddingBlock: number;
79
+ /** Padding ngang thân và header Card (px). */
80
+ cardPaddingInline: number;
81
+ /** Chiều cao header Card (px). */
82
+ cardHeadingHeight: number;
83
+ /** Cỡ chữ tiêu đề Card (px). */
84
+ cardHeadingFontSize: number;
85
+ /** Khoảng cách dưới mỗi Form item (px). */
86
+ formItemMarginBottom: number;
87
+ /** Padding dọc ô Table (px). */
88
+ tableCellPaddingBlock: number;
89
+ /** Padding ngang ô Table (px). */
90
+ tableCellPaddingInline: number;
91
+ /** Padding thân Modal / Drawer (px). */
92
+ modalBodyPadding: number;
93
+ }
94
+ /** Toàn bộ design token của một chế độ (sáng hoặc tối). */
95
+ interface TascoTokens {
96
+ /** Bảng màu theo vai trò. */
97
+ color: ColorTokens;
98
+ /** Bo góc cơ bản (px). */
99
+ radius: number;
100
+ /** Bo góc lớn (card, modal) (px). */
101
+ radiusLg: number;
102
+ /** Font chữ thân (Quicksand + font hệ thống dự phòng). */
103
+ fontFamily: string;
104
+ /** Font cho tiêu đề/heading (có thể trùng fontFamily). */
105
+ fontFamilyHeading: string;
106
+ /** Cỡ chữ cơ bản (px). */
107
+ fontSize: number;
108
+ /** Chiều cao control cơ bản (px) — alias của density.controlHeight. */
109
+ controlHeight: number;
110
+ /** Mật độ component (height/padding/margin). */
111
+ density: DensityTokens;
112
+ /** Đơn vị spacing cơ bản (px). */
113
+ spacingUnit: number;
114
+ /** Gradient cho nút/hero primary. */
115
+ gradientPrimary: string;
116
+ /** Gradient cho nút danger. */
117
+ gradientDanger: string;
118
+ /** Shadow card. */
119
+ shadowCard: string;
120
+ /** Shadow phần tử nổi (dropdown/modal). */
121
+ shadowRaised: string;
122
+ }
123
+ /** Token chế độ sáng. */
124
+ declare const lightTokens: TascoTokens;
125
+ /** Token chế độ tối. */
126
+ declare const darkTokens: TascoTokens;
127
+ /** Token theo chế độ — tra bằng `ThemeMode`. */
128
+ declare const tokensByMode: {
129
+ readonly light: TascoTokens;
130
+ readonly dark: TascoTokens;
131
+ };
132
+
133
+ /** Chế độ giao diện. */
134
+ type ThemeMode = 'light' | 'dark';
135
+ /** Cấu hình `theme` cho `<a-config-provider>` — tương thích cấu trúc với prop `theme` của antdv. */
136
+ interface AntdThemeConfig {
137
+ /** Global token của antdv (màu seed, font, bo góc, chiều cao control…). */
138
+ token?: Record<string, unknown>;
139
+ /** Token cấp component — antdv 4.2.6 BỎ QUA trường này; tinh chỉnh component làm bằng CSS trong `base.css`. */
140
+ components?: Record<string, Record<string, unknown>>;
141
+ }
142
+ /**
143
+ * Cấu hình `theme` của antdv cho một chế độ. Chưa gồm `algorithm` — nơi có antdv tự thêm
144
+ * `theme.darkAlgorithm`/`theme.defaultAlgorithm` (layer làm trong `app.vue`).
145
+ */
146
+ declare function getAntdTheme(mode?: ThemeMode): AntdThemeConfig;
147
+ /** `getAntdTheme('light')` tính sẵn. */
148
+ declare const antdThemeLight: AntdThemeConfig;
149
+ /** `getAntdTheme('dark')` tính sẵn. */
150
+ declare const antdThemeDark: AntdThemeConfig;
151
+
152
+ /** Map tên biến CSS `--tasco-*` → giá trị của một chế độ; số đo đã kèm đơn vị `px`. */
153
+ declare function cssVars(mode?: ThemeMode): Record<string, string>;
154
+ /**
155
+ * Sinh chuỗi CSS để inject vào `<head>` (vd qua `app.head.style` trong layer).
156
+ * @param selector mặc định `:root`.
157
+ */
158
+ declare function cssVarsText(mode?: ThemeMode, selector?: string): string;
159
+
160
+ /**
161
+ * Relative luminance theo WCAG 2.x: 0 (đen) → 1 (trắng). Nhận hex `#rgb` / `#rrggbb`, không phân biệt hoa thường;
162
+ * chuỗi khác (tên màu, `rgb()`, hex có alpha) → ném lỗi.
163
+ */
164
+ declare function luminanceOf(color: string): number;
165
+ /**
166
+ * Tỉ lệ tương phản WCAG 2.x giữa hai màu: 1 (trùng màu) → 21 (đen/trắng); đổi chỗ chữ và nền cho cùng kết quả.
167
+ * Ngưỡng AA: ≥ 4.5 cho chữ thường, ≥ 3 cho chữ lớn và thành phần đồ hoạ. Kết quả không làm tròn vì WCAG không cho
168
+ * làm tròn lên (4.499 là chưa đạt 4.5).
169
+ */
170
+ declare function contrastRatioOf(foreground: string, background: string): number;
171
+
172
+ /** Ngưỡng tương phản WCAG 2.x AA cho chữ cỡ thường. */
173
+ declare const AA_TEXT_CONTRAST = 4.5;
174
+ /** Một cặp màu chữ/nền cần đạt tương phản AA. */
175
+ interface ContrastPair {
176
+ /** Màu chữ: tên biến CSS bỏ tiền tố `--tasco-` (vd `color-primary`) hoặc hex cố định (`#fff`). */
177
+ fg: string;
178
+ /** Màu nền, ghi như `fg`; gradient thì xét mọi điểm màu. */
179
+ bg: string;
180
+ /** Nơi dùng — đổi token là biết component nào bị ảnh hưởng. */
181
+ usedBy: string;
182
+ /** Chỉ áp dụng ở các chế độ này; bỏ trống = cả hai. */
183
+ modes?: ThemeMode[];
184
+ /**
185
+ * Chế độ đang biết là CHƯA đạt ngưỡng → lý do. Test khẳng định cặp vẫn dưới ngưỡng, nên khi sửa đạt rồi test sẽ
186
+ * đỏ nhắc xoá mục này để cặp được khoá như các cặp khác.
187
+ */
188
+ knownFailures?: Partial<Record<ThemeMode, string>>;
189
+ }
190
+ /** Cặp chữ/nền đã khoá tương phản AA, xếp theo nhóm màu. */
191
+ declare const contrastPairs: readonly ContrastPair[];
192
+ /**
193
+ * Tương phản thấp nhất của một cặp ở một chế độ: nền gradient thì lấy điểm màu tệ nhất. Tên biến không tồn tại →
194
+ * ném lỗi.
195
+ */
196
+ declare function pairContrastOf(pair: ContrastPair, mode: ThemeMode): number;
197
+
198
+ export { AA_TEXT_CONTRAST, antdThemeDark, antdThemeLight, contrastPairs, contrastRatioOf, cssVars, cssVarsText, darkTokens, getAntdTheme, lightTokens, luminanceOf, pairContrastOf, tokensByMode };
199
+ export type { AntdThemeConfig, ColorTokens, ContrastPair, DensityTokens, TascoTokens, ThemeMode };
package/dist/index.mjs ADDED
@@ -0,0 +1,347 @@
1
+ const fontStack = "'Quicksand', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
2
+ const fontHeadingStack = "'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
3
+ const density = {
4
+ controlHeightSM: 28,
5
+ controlHeight: 36,
6
+ controlHeightLG: 44,
7
+ cardPaddingBlock: 12,
8
+ cardPaddingInline: 16,
9
+ cardHeadingHeight: 36,
10
+ cardHeadingFontSize: 14,
11
+ formItemMarginBottom: 16,
12
+ tableCellPaddingBlock: 8,
13
+ tableCellPaddingInline: 8,
14
+ modalBodyPadding: 16
15
+ };
16
+ const lightTokens = {
17
+ color: {
18
+ primary: "#203368",
19
+ primaryHover: "#2f4b8f",
20
+ primaryActive: "#182544",
21
+ primarySoft: "#eef3fb",
22
+ // Cam thương hiệu chỉ làm nền: chữ trắng trên nó chỉ 2.16:1 nên chữ trên pill menu dùng navy sidebarBottom (7.03:1),
23
+ // chữ/icon cam dùng accentText (≥ 4.5:1 trên accentSoft, surface, infoSoft).
24
+ accent: "#ff9800",
25
+ accentSoft: "#fff1d6",
26
+ accentText: "#9e5d02",
27
+ // success/warning còn là seed antdv: làm tối seed thì tint nền antdv sinh ra (Alert, Tag…) xỉn theo, nên giữ nguyên và
28
+ // tách màu chữ *Text (≥ 4.5:1 trên nền soft, surface, infoSoft). error đổi thẳng vì đỏ đậm vẫn cho tint nền như cũ.
29
+ success: "#089b00",
30
+ successSoft: "#ecfdf5",
31
+ successText: "#087e02",
32
+ warning: "#d97706",
33
+ warningSoft: "#fff7ed",
34
+ warningText: "#a55904",
35
+ // ≥ 4.5:1 trên errorSoft, surface, infoSoft và khi làm nền cho chữ trắng (Badge, hover item danger của Dropdown antdv);
36
+ // #ee0033 cũ chỉ 4.08:1 trên errorSoft.
37
+ error: "#d71431",
38
+ errorSoft: "#fff1f0",
39
+ info: "#0f4c81",
40
+ infoSoft: "#e8f2ff",
41
+ // ≥ 4.5:1 trên surface, surfaceMuted, page và linkSoft (#1576f4 cũ chỉ 4.26:1 trên surface).
42
+ link: "#1068d6",
43
+ linkSoft: "#e8f2ff",
44
+ text: "#10213f",
45
+ textSecondary: "#4b5563",
46
+ textMuted: "#5f6b85",
47
+ textSubtle: "#9ca3af",
48
+ surface: "#ffffff",
49
+ surfaceElevated: "#ffffff",
50
+ surfaceMuted: "#f6f8fc",
51
+ page: "#edf2f8",
52
+ border: "#d8deea",
53
+ borderStrong: "#bcc6da",
54
+ sidebarTop: "#213368",
55
+ sidebarBottom: "#182544"
56
+ },
57
+ radius: 0,
58
+ radiusLg: 0,
59
+ fontFamily: fontStack,
60
+ fontFamilyHeading: fontHeadingStack,
61
+ fontSize: 14,
62
+ controlHeight: 36,
63
+ density,
64
+ spacingUnit: 8,
65
+ gradientPrimary: "linear-gradient(135deg, #203368, #2f4b8f)",
66
+ // Chữ trắng của nút danger ≥ 4.5:1 dọc cả dải, kể cả khi hover sáng lên (brightness 1.08); #f43f5e cũ chỉ 3.67:1.
67
+ gradientDanger: "linear-gradient(135deg, #cf1444, #d71431)",
68
+ shadowCard: "0 12px 28px rgba(16, 33, 63, 0.08)",
69
+ shadowRaised: "0 18px 40px rgba(16, 33, 63, 0.14)"
70
+ };
71
+ const darkTokens = {
72
+ color: {
73
+ // Nền tối đảo vai trò: primary/primaryHover là màu CHỮ/icon (≥ 4.5:1 trên primarySoft, surface, surfaceElevated) nên
74
+ // quá sáng để đặt chữ trắng lên; nền có chữ trắng dùng primaryActive (≥ 7:1) — xem base.css và gradientPrimary.
75
+ primary: "#7090dc",
76
+ primaryHover: "#94ade6",
77
+ primaryActive: "#35549e",
78
+ primarySoft: "#1a2740",
79
+ accent: "#ffa726",
80
+ accentSoft: "#3a2c12",
81
+ // Nền tối: màu gốc đã đủ sáng để làm chữ (≥ 4.5:1 trên nền soft, surface) nên *Text trùng màu gốc.
82
+ accentText: "#ffa726",
83
+ success: "#22c55e",
84
+ successSoft: "#10241a",
85
+ successText: "#22c55e",
86
+ warning: "#fbbf24",
87
+ warningSoft: "#2a2110",
88
+ warningText: "#fbbf24",
89
+ error: "#f87171",
90
+ errorSoft: "#2c1416",
91
+ info: "#60a5fa",
92
+ infoSoft: "#13243d",
93
+ link: "#60a5fa",
94
+ linkSoft: "#13243d",
95
+ text: "#f8fafc",
96
+ textSecondary: "#c0cadb",
97
+ textMuted: "#9aa6bf",
98
+ textSubtle: "#6b7689",
99
+ surface: "#162033",
100
+ surfaceElevated: "#1a2740",
101
+ surfaceMuted: "#111827",
102
+ page: "#0f1724",
103
+ border: "#31415f",
104
+ borderStrong: "#4b5f86",
105
+ sidebarTop: "#1c2c52",
106
+ sidebarBottom: "#0e1a33"
107
+ },
108
+ radius: 0,
109
+ radiusLg: 0,
110
+ fontFamily: fontStack,
111
+ fontFamilyHeading: fontHeadingStack,
112
+ fontSize: 14,
113
+ controlHeight: 36,
114
+ density,
115
+ spacingUnit: 8,
116
+ // Không dùng primary làm điểm cuối: chữ trắng của CButton primary phải ≥ 4.5:1 dọc cả dải (kể cả khi hover sáng lên).
117
+ gradientPrimary: "linear-gradient(135deg, #35549e, #4466b8)",
118
+ // error theme tối là màu chữ (sáng) nên không làm điểm cuối được (chữ trắng chỉ 2.77:1) → dùng chung dải với theme sáng.
119
+ gradientDanger: "linear-gradient(135deg, #cf1444, #d71431)",
120
+ shadowCard: "0 12px 28px rgba(0, 0, 0, 0.35)",
121
+ shadowRaised: "0 18px 40px rgba(0, 0, 0, 0.5)"
122
+ };
123
+ const tokensByMode = {
124
+ light: lightTokens,
125
+ dark: darkTokens
126
+ };
127
+
128
+ function toAntdToken(t) {
129
+ return {
130
+ colorPrimary: t.color.primary,
131
+ colorSuccess: t.color.success,
132
+ colorWarning: t.color.warning,
133
+ colorError: t.color.error,
134
+ colorInfo: t.color.info,
135
+ colorLink: t.color.link,
136
+ colorLinkHover: t.color.primaryHover,
137
+ colorText: t.color.text,
138
+ colorTextSecondary: t.color.textSecondary,
139
+ colorTextTertiary: t.color.textMuted,
140
+ colorTextQuaternary: t.color.textSubtle,
141
+ colorBorder: t.color.border,
142
+ colorBorderSecondary: t.color.border,
143
+ colorBgBase: t.color.surface,
144
+ colorBgContainer: t.color.surface,
145
+ colorBgElevated: t.color.surfaceElevated,
146
+ colorBgLayout: t.color.page,
147
+ borderRadius: t.radius,
148
+ borderRadiusLG: t.radiusLg,
149
+ fontFamily: t.fontFamily,
150
+ fontSize: t.fontSize,
151
+ // Height control qua GLOBAL token (antd honor; theme.components thì không).
152
+ controlHeight: t.density.controlHeight,
153
+ controlHeightSM: t.density.controlHeightSM,
154
+ controlHeightLG: t.density.controlHeightLG,
155
+ boxShadowSecondary: t.shadowRaised,
156
+ wireframe: false
157
+ };
158
+ }
159
+ function getAntdTheme(mode = "light") {
160
+ const t = tokensByMode[mode];
161
+ return { token: toAntdToken(t) };
162
+ }
163
+ const antdThemeLight = getAntdTheme("light");
164
+ const antdThemeDark = getAntdTheme("dark");
165
+
166
+ function cssVars(mode = "light") {
167
+ const t = tokensByMode[mode];
168
+ const c = t.color;
169
+ return {
170
+ "--tasco-color-primary": c.primary,
171
+ "--tasco-color-primary-hover": c.primaryHover,
172
+ "--tasco-color-primary-active": c.primaryActive,
173
+ "--tasco-color-primary-soft": c.primarySoft,
174
+ "--tasco-color-accent": c.accent,
175
+ "--tasco-color-accent-soft": c.accentSoft,
176
+ "--tasco-color-accent-text": c.accentText,
177
+ "--tasco-color-success": c.success,
178
+ "--tasco-color-success-soft": c.successSoft,
179
+ "--tasco-color-success-text": c.successText,
180
+ "--tasco-color-warning": c.warning,
181
+ "--tasco-color-warning-soft": c.warningSoft,
182
+ "--tasco-color-warning-text": c.warningText,
183
+ "--tasco-color-error": c.error,
184
+ "--tasco-color-error-soft": c.errorSoft,
185
+ "--tasco-color-info": c.info,
186
+ "--tasco-color-info-soft": c.infoSoft,
187
+ "--tasco-color-link": c.link,
188
+ "--tasco-color-link-soft": c.linkSoft,
189
+ "--tasco-color-text": c.text,
190
+ "--tasco-color-text-secondary": c.textSecondary,
191
+ "--tasco-color-text-muted": c.textMuted,
192
+ "--tasco-color-text-subtle": c.textSubtle,
193
+ "--tasco-color-surface": c.surface,
194
+ "--tasco-color-surface-elevated": c.surfaceElevated,
195
+ "--tasco-color-surface-muted": c.surfaceMuted,
196
+ "--tasco-color-page": c.page,
197
+ "--tasco-color-border": c.border,
198
+ "--tasco-color-border-strong": c.borderStrong,
199
+ "--tasco-color-sidebar-top": c.sidebarTop,
200
+ "--tasco-color-sidebar-bottom": c.sidebarBottom,
201
+ "--tasco-radius": `${t.radius}px`,
202
+ "--tasco-radius-lg": `${t.radiusLg}px`,
203
+ // Mật độ component (base.css dùng để override padding/height/margin).
204
+ "--tasco-control-height-sm": `${t.density.controlHeightSM}px`,
205
+ "--tasco-control-height-lg": `${t.density.controlHeightLG}px`,
206
+ "--tasco-card-padding-block": `${t.density.cardPaddingBlock}px`,
207
+ "--tasco-card-padding-inline": `${t.density.cardPaddingInline}px`,
208
+ "--tasco-card-heading-height": `${t.density.cardHeadingHeight}px`,
209
+ "--tasco-card-heading-font-size": `${t.density.cardHeadingFontSize}px`,
210
+ "--tasco-form-item-margin": `${t.density.formItemMarginBottom}px`,
211
+ "--tasco-table-cell-padding-block": `${t.density.tableCellPaddingBlock}px`,
212
+ "--tasco-table-cell-padding-inline": `${t.density.tableCellPaddingInline}px`,
213
+ "--tasco-modal-body-padding": `${t.density.modalBodyPadding}px`,
214
+ "--tasco-font-family": t.fontFamily,
215
+ "--tasco-font-family-heading": t.fontFamilyHeading,
216
+ "--tasco-font-size": `${t.fontSize}px`,
217
+ "--tasco-control-height": `${t.controlHeight}px`,
218
+ "--tasco-spacing-unit": `${t.spacingUnit}px`,
219
+ "--tasco-gradient-primary": t.gradientPrimary,
220
+ "--tasco-gradient-danger": t.gradientDanger,
221
+ "--tasco-shadow-card": t.shadowCard,
222
+ "--tasco-shadow-raised": t.shadowRaised
223
+ };
224
+ }
225
+ function cssVarsText(mode = "light", selector = ":root") {
226
+ const body = Object.entries(cssVars(mode)).map(([key, value]) => ` ${key}: ${value};`).join("\n");
227
+ return `${selector} {
228
+ ${body}
229
+ }`;
230
+ }
231
+
232
+ const HEX_COLOR_RE = /^#([\da-f]{3}|[\da-f]{6})$/i;
233
+ function parseHexColor(color) {
234
+ const hex = HEX_COLOR_RE.exec(color)?.[1];
235
+ if (!hex) return null;
236
+ const full = hex.length === 3 ? [...hex].map((digit) => digit + digit).join("") : hex;
237
+ const channelAt = (offset) => Number.parseInt(full.slice(offset, offset + 2), 16);
238
+ return [channelAt(0), channelAt(2), channelAt(4)];
239
+ }
240
+ function toLinearChannel(channel) {
241
+ const c = channel / 255;
242
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
243
+ }
244
+ function luminanceOf(color) {
245
+ const rgb = parseHexColor(color);
246
+ if (!rgb) {
247
+ throw new Error(
248
+ `[@tasco-fe/theme] M\xE0u "${color}" kh\xF4ng h\u1EE3p l\u1EC7 \u2014 c\u1EA7n hex #rgb/#rrggbb (m\xE0u c\xF3 alpha ph\u1EA3i tr\u1ED9n v\u1EDBi n\u1EC1n tr\u01B0\u1EDBc).`
249
+ );
250
+ }
251
+ const [r, g, b] = rgb;
252
+ return 0.2126 * toLinearChannel(r) + 0.7152 * toLinearChannel(g) + 0.0722 * toLinearChannel(b);
253
+ }
254
+ function contrastRatioOf(foreground, background) {
255
+ const a = luminanceOf(foreground);
256
+ const b = luminanceOf(background);
257
+ return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
258
+ }
259
+
260
+ const AA_TEXT_CONTRAST = 4.5;
261
+ const HEX_RE = /#[\da-f]{3,8}\b/gi;
262
+ const SUBTLE_TEXT = "text-subtle l\xE0 m\xE0u placeholder/disabled c\u1EE7a antdv (colorTextQuaternary)";
263
+ const DESIGN_DOCS = "c\u1EB7p d\xF9ng \u0111\u01B0\u1EE3c \u1EDF trang M\xE0u & t\u01B0\u01A1ng ph\u1EA3n c\u1EE7a docs";
264
+ const contrastPairs = [
265
+ // Primary & link
266
+ { fg: "color-primary", bg: "color-primary-soft", usedBy: "CTag primary, CButton ghost, icon CStatistic" },
267
+ { fg: "color-primary-hover", bg: "color-primary-soft", usedBy: "CButton ghost :hover, ::selection theme t\u1ED1i" },
268
+ {
269
+ fg: "color-primary",
270
+ bg: "color-surface",
271
+ usedBy: "CButton secondary/outline/link, m\u1EE5c \u0111ang ch\u1ECDn c\u1EE7a CTopNav, Pagination, Tabs"
272
+ },
273
+ { fg: "color-primary-hover", bg: "color-surface", usedBy: "CButton secondary/outline/link :hover" },
274
+ { fg: "color-primary", bg: "color-surface-elevated", usedBy: "CButton trong modal/drawer/popover" },
275
+ { fg: "color-link", bg: "color-surface", usedBy: 'CTable "Xo\xE1 t\u1EA5t c\u1EA3" tr\xEAn thanh l\u1ECDc' },
276
+ { fg: "color-link", bg: "color-surface-elevated", usedBy: "th\u1EBB <a> trong modal/drawer/popover" },
277
+ { fg: "color-link", bg: "color-surface-muted", usedBy: "th\u1EBB <a> trong h\xE0ng s\u1ECDc CTable" },
278
+ { fg: "color-link", bg: "color-page", usedBy: "th\u1EBB <a> tr\xEAn n\u1EC1n trang" },
279
+ // Chữ trên nền đậm. Theme tối primary/error là màu chữ (sáng), nên base.css đặt chữ trắng lên primary-active /
280
+ // gradient-danger thay cho primary / error.
281
+ {
282
+ fg: "#fff",
283
+ bg: "color-primary",
284
+ usedBy: "header b\u1EA3ng, CCard primary, n\xFAt primary antdv, ng\xE0y \u0111ang ch\u1ECDn DatePicker",
285
+ modes: ["light"]
286
+ },
287
+ {
288
+ fg: "#fff",
289
+ bg: "color-primary-active",
290
+ usedBy: "CChatMessage, c\u1ED9t sort header b\u1EA3ng; theme t\u1ED1i: header b\u1EA3ng, CCard primary, n\xFAt antdv, DatePicker"
291
+ },
292
+ { fg: "#fff", bg: "gradient-primary", usedBy: "CButton primary, tab \u0111ang ch\u1ECDn c\u1EE7a drawer Thi\u1EBFt l\u1EADp CTable" },
293
+ {
294
+ fg: "#fff",
295
+ bg: "gradient-danger",
296
+ usedBy: "CButton danger, n\xFAt primary danger antdv, hover item danger Dropdown theme t\u1ED1i"
297
+ },
298
+ { fg: "#fff", bg: "color-error", usedBy: "Badge, hover item danger Dropdown antdv", modes: ["light"] },
299
+ { fg: "color-sidebar-bottom", bg: "color-accent", usedBy: "CSideNav m\u1EE5c :hover/\u0111ang ch\u1ECDn" },
300
+ // Chữ chính
301
+ {
302
+ fg: "color-text",
303
+ bg: "color-surface",
304
+ usedBy: "m\u1EE5c ch\u01B0a ch\u1ECDn c\u1EE7a CTopNav, ti\xEAu \u0111\u1EC1 CCard, gi\xE1 tr\u1ECB CStatistic, n\xFAt icon CTable"
305
+ },
306
+ // Chữ phụ
307
+ { fg: "color-text-muted", bg: "color-surface-muted", usedBy: "CTag default" },
308
+ { fg: "color-text-muted", bg: "color-surface", usedBy: "nh\xE3n CStatistic, nh\xE3n thanh l\u1ECDc CTable, footer CAppLayout" },
309
+ { fg: "color-text-muted", bg: "color-surface-elevated", usedBy: "g\u1EE3i \xFD trong drawer Thi\u1EBFt l\u1EADp CTable" },
310
+ {
311
+ fg: "color-text-subtle",
312
+ bg: "color-surface",
313
+ usedBy: "m\xF4 t\u1EA3 CEmpty",
314
+ knownFailures: { light: SUBTLE_TEXT, dark: SUBTLE_TEXT }
315
+ },
316
+ // Semantic: accent/success/warning làm chữ thì dùng biến *-text; primary/error/info dùng màu gốc.
317
+ { fg: "color-accent-text", bg: "color-accent-soft", usedBy: "CTag accent, icon CStatistic" },
318
+ { fg: "color-success-text", bg: "color-success-soft", usedBy: "CTag success, icon CStatistic" },
319
+ { fg: "color-warning-text", bg: "color-warning-soft", usedBy: "CTag warning, icon CStatistic" },
320
+ { fg: "color-error", bg: "color-error-soft", usedBy: "CTag error, icon CStatistic" },
321
+ { fg: "color-info", bg: "color-info-soft", usedBy: "CTag info, icon CStatistic" },
322
+ { fg: "color-accent-text", bg: "color-surface", usedBy: DESIGN_DOCS },
323
+ { fg: "color-success-text", bg: "color-surface", usedBy: "ch\u1EEF CStatus ho\u1EA1t \u0111\u1ED9ng, trend t\u0103ng CStatistic" },
324
+ { fg: "color-warning-text", bg: "color-surface", usedBy: DESIGN_DOCS },
325
+ { fg: "color-error", bg: "color-surface", usedBy: "ch\u1EEF CStatus kh\xF4ng ho\u1EA1t \u0111\u1ED9ng, trend gi\u1EA3m CStatistic" },
326
+ { fg: "color-info", bg: "color-surface", usedBy: DESIGN_DOCS },
327
+ { fg: "color-error", bg: "color-surface-elevated", usedBy: "item danger c\u1EE7a Dropdown trong menu thao t\xE1c" },
328
+ { fg: "color-success-text", bg: "color-info-soft", usedBy: "ch\u1EEF CStatus trong h\xE0ng \u0111ang ch\u1ECDn c\u1EE7a b\u1EA3ng" },
329
+ { fg: "color-error", bg: "color-info-soft", usedBy: "ch\u1EEF CStatus trong h\xE0ng \u0111ang ch\u1ECDn c\u1EE7a b\u1EA3ng" }
330
+ ];
331
+ function colorsOf(vars, ref) {
332
+ if (ref.startsWith("#")) return [ref];
333
+ const colors = vars[`--tasco-${ref}`]?.match(HEX_RE);
334
+ if (!colors) {
335
+ throw new Error(`[@tasco-fe/theme] --tasco-${ref} kh\xF4ng c\xF3 trong cssVars ho\u1EB7c kh\xF4ng ch\u1EE9a m\xE0u hex \u2014 s\u1EEDa t\xEAn trong c\u1EB7p.`);
336
+ }
337
+ return colors;
338
+ }
339
+ function pairContrastOf(pair, mode) {
340
+ const vars = cssVars(mode);
341
+ const backgrounds = colorsOf(vars, pair.bg);
342
+ return Math.min(
343
+ ...colorsOf(vars, pair.fg).flatMap((fg) => backgrounds.map((bg) => contrastRatioOf(fg, bg)))
344
+ );
345
+ }
346
+
347
+ export { AA_TEXT_CONTRAST, antdThemeDark, antdThemeLight, contrastPairs, contrastRatioOf, cssVars, cssVarsText, darkTokens, getAntdTheme, lightTokens, luminanceOf, pairContrastOf, tokensByMode };
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@tasco-fe/theme",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "3.0.0-beta.0",
4
+ "description": "Design token + theme antdv (ConfigProvider) + CSS variables cho framework Tasco",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.mjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs"
15
+ },
16
+ "./base.css": "./styles/base.css"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "styles"
21
+ ],
22
+ "devDependencies": {
23
+ "@vitest/coverage-v8": "^3.0.0",
24
+ "eslint": "^10.10.0",
25
+ "typescript": "^5.7.3",
26
+ "unbuild": "^3.0.1",
27
+ "vitest": "^3.0.0",
28
+ "@tasco-fe/eslint-config": "3.0.0-beta.0",
29
+ "@tasco-fe/tsconfig": "3.0.0-beta.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "scripts": {
36
+ "build": "unbuild",
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "eslint .",
39
+ "test": "vitest run --coverage",
40
+ "clean": "rm -rf dist .turbo coverage"
41
+ }
6
42
  }
@@ -0,0 +1,339 @@
1
+ /* Base global stylesheet của framework Tasco.
2
+ * Inject MỘT LẦN ở layer (nuxt.config `css: ['@tasco-fe/theme/base.css']`).
3
+ * Chỉ chứa tinh chỉnh global nhẹ — màu sắc lấy từ biến `--tasco-*` (do layer
4
+ * sinh từ cssVarsText). Phần lớn giao diện do antd theme token lo; file này
5
+ * xử lý những thứ antd không cấu hình được (scrollbar, font nền, selection...).
6
+ * KHÔNG phụ thuộc Tailwind. */
7
+
8
+ :root {
9
+ color-scheme: light;
10
+ /* Khai báo font TĨNH ở đây (không chỉ inject runtime) để @nuxt/fonts quét được
11
+ * tên 'Quicksand'/'Montserrat' khi build và tự tải, self-host. cssVarsText cũng
12
+ * set 2 biến này nhưng cùng giá trị — ở đây là "neo" cho việc dò font.
13
+ * Đồng bộ với @tasco-fe/theme tokens (fontStack/fontHeadingStack). */
14
+ --tasco-font-family: 'Quicksand', -apple-system, BlinkMacSystemFont, 'Segoe UI',
15
+ Roboto, 'Helvetica Neue', Arial, sans-serif;
16
+ --tasco-font-family-heading: 'Montserrat', -apple-system, BlinkMacSystemFont,
17
+ 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
18
+ }
19
+
20
+ /* Dark mode: useThemeMode gắn [data-theme='dark'] lên <html>; biến --tasco-* đã
21
+ * được cssVarsText('dark') ghi đè, ở đây chỉ cần đổi color-scheme cho form
22
+ * control/scrollbar gốc của trình duyệt. */
23
+ :root[data-theme='dark'] {
24
+ color-scheme: dark;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ font-family: var(--tasco-font-family);
30
+ font-size: var(--tasco-font-size);
31
+ color: var(--tasco-color-text);
32
+ background: var(--tasco-color-page);
33
+ -webkit-font-smoothing: antialiased;
34
+ -moz-osx-font-smoothing: grayscale;
35
+ }
36
+
37
+ a {
38
+ color: var(--tasco-color-link);
39
+ }
40
+
41
+ ::selection {
42
+ background: var(--tasco-color-primary-soft);
43
+ color: var(--tasco-color-primary-active);
44
+ }
45
+ /* Theme tối: primary-active là tông nền (tối) → chữ đang bôi chọn trên primary-soft chỉ ~2:1; đổi sang tông primary sáng nhất. */
46
+ :root[data-theme='dark'] ::selection {
47
+ color: var(--tasco-color-primary-hover);
48
+ }
49
+
50
+ /* Scrollbar mảnh, đồng nhất giữa các trang. */
51
+ ::-webkit-scrollbar {
52
+ width: 8px;
53
+ height: 8px;
54
+ }
55
+ ::-webkit-scrollbar-track {
56
+ background: transparent;
57
+ }
58
+ ::-webkit-scrollbar-thumb {
59
+ background: rgba(16, 33, 63, 0.18);
60
+ border-radius: 999px;
61
+ }
62
+ ::-webkit-scrollbar-thumb:hover {
63
+ background: rgba(16, 33, 63, 0.32);
64
+ }
65
+
66
+ /* ─────────────────────────────────────────────────────────────────────────
67
+ * Bảng (Table) — dấu ấn Tasco: header nền primary, chữ trắng.
68
+ * Style qua global CSS vì ant-design-vue 4.x bỏ qua theme.components.
69
+ * ───────────────────────────────────────────────────────────────────────── */
70
+ .ant-table-wrapper .ant-table-thead > tr > th,
71
+ .ant-table-wrapper .ant-table-thead > tr > td {
72
+ background: var(--tasco-color-primary) !important;
73
+ color: #fff !important;
74
+ font-weight: 400;
75
+ white-space: nowrap;
76
+ padding-block: var(--tasco-table-cell-padding-block, 8px) !important;
77
+ padding-inline: var(--tasco-table-cell-padding-inline, 8px) !important;
78
+ }
79
+ /* Vạch ngăn dọc giữa các cột header: trắng mờ, đủ rõ trên nền primary. */
80
+ .ant-table-wrapper .ant-table-thead > tr > th::before {
81
+ background-color: rgba(255, 255, 255, 0.6) !important;
82
+ }
83
+ /* Icon sort/filter trên header chuyển sáng để hợp nền tối. */
84
+ .ant-table-wrapper .ant-table-thead .ant-table-column-sorter,
85
+ .ant-table-wrapper .ant-table-thead .ant-table-filter-trigger {
86
+ color: rgba(255, 255, 255, 0.65) !important;
87
+ }
88
+ .ant-table-wrapper .ant-table-thead th.ant-table-column-has-sorters:hover,
89
+ .ant-table-wrapper .ant-table-thead th.ant-table-column-sort {
90
+ background: var(--tasco-color-primary-active) !important;
91
+ }
92
+ /* Theme tối: primary là màu chữ (sáng), chữ trắng trên nó chỉ ~3:1 → header dùng primary-active; cột đang sort/hover
93
+ * phủ thêm lớp tối để vẫn tối hơn header một nấc như theme sáng. */
94
+ :root[data-theme='dark'] .ant-table-wrapper .ant-table-thead > tr > th,
95
+ :root[data-theme='dark'] .ant-table-wrapper .ant-table-thead > tr > td {
96
+ background: var(--tasco-color-primary-active) !important;
97
+ }
98
+ :root[data-theme='dark'] .ant-table-wrapper .ant-table-thead th.ant-table-column-has-sorters:hover,
99
+ :root[data-theme='dark'] .ant-table-wrapper .ant-table-thead th.ant-table-column-sort {
100
+ background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--tasco-color-primary-active) !important;
101
+ }
102
+ /* Hàng được chọn dùng nền info-soft thay vì xám mặc định. */
103
+ .ant-table-wrapper .ant-table-tbody > tr.ant-table-row-selected > td {
104
+ background: var(--tasco-color-info-soft) !important;
105
+ }
106
+ /* Ô gọn hơn cho mật độ dữ liệu cao. Chừa hàng đo cột (measure-row, antdv chèn khi bảng có
107
+ * scroll/sticky) — nếu bị cộng padding sẽ thành khoảng trống 16px ngay dưới header. */
108
+ .ant-table-wrapper .ant-table-tbody > tr:not(.ant-table-measure-row) > td {
109
+ padding-block: var(--tasco-table-cell-padding-block, 8px) !important;
110
+ padding-inline: var(--tasco-table-cell-padding-inline, 8px) !important;
111
+ }
112
+
113
+ /* Tab đang chọn in đậm cho dễ nhận biết. */
114
+ .ant-tabs-tab-active .ant-tabs-tab-btn {
115
+ font-weight: 700;
116
+ }
117
+
118
+ /* ─────────────────────────────────────────────────────────────────────────
119
+ * Nền primary chữ trắng do antdv tự vẽ — theme tối.
120
+ * antdv suy màu nền từ seed `colorPrimary` (trộn 85% với #141414 ở theme tối). Primary theme tối là màu chữ (sáng) nên
121
+ * chữ trắng trên nền suy ra chỉ ~4:1, không seed nào đạt ≥ 4.5:1 cho cả hai vai trò → nút primary thuần antdv
122
+ * (Modal.confirm của useConfirm, nút Gửi của CChat…) và ô ngày đang chọn của DatePicker/RangePicker dùng primary-active.
123
+ * CButton có gradient riêng nên loại trừ. Chỗ antdv khác còn nền colorPrimary + chữ trắng mà core chưa dùng nên chưa phủ
124
+ * (vd Steps, Radio solid, Tag checkable, Menu theme dark, Badge.Ribbon).
125
+ * ───────────────────────────────────────────────────────────────────────── */
126
+ :root[data-theme='dark'] .ant-btn-primary:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-dangerous):not(.ant-btn-background-ghost):not(.c-btn) {
127
+ background: var(--tasco-color-primary-active) !important;
128
+ }
129
+ /* Nền bị ghim bằng !important nên hover báo hiệu bằng độ sáng (giống CButton). */
130
+ :root[data-theme='dark'] .ant-btn-primary:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-dangerous):not(.ant-btn-background-ghost):not(.c-btn):hover {
131
+ filter: brightness(1.12);
132
+ }
133
+ :root[data-theme='dark'] .ant-picker-cell-in-view.ant-picker-cell-selected .ant-picker-cell-inner,
134
+ :root[data-theme='dark'] .ant-picker-cell-in-view.ant-picker-cell-range-start .ant-picker-cell-inner,
135
+ :root[data-theme='dark'] .ant-picker-cell-in-view.ant-picker-cell-range-end .ant-picker-cell-inner {
136
+ background: var(--tasco-color-primary-active) !important;
137
+ }
138
+
139
+ /* ─────────────────────────────────────────────────────────────────────────
140
+ * Nút primary danger thuần antdv (vd `okButtonProps: { danger: true }` của Modal/Popconfirm) — cả hai theme.
141
+ * antdv tô nền bằng colorError: theme tối nó trộn seed error (màu chữ, sáng) nên chữ trắng chỉ ~3.6:1; theme sáng lúc hover
142
+ * nền colorErrorHover sáng hơn (~4.2:1). Dùng chung gradient-danger + hover với CButton danger (≥ 4.5:1 kể cả khi hover).
143
+ * ───────────────────────────────────────────────────────────────────────── */
144
+ .ant-btn-primary.ant-btn-dangerous:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-background-ghost):not(.c-btn) {
145
+ background: var(--tasco-gradient-danger) !important;
146
+ }
147
+ .ant-btn-primary.ant-btn-dangerous:not(:disabled):not(.ant-btn-disabled):not(.ant-btn-background-ghost):not(.c-btn):hover {
148
+ filter: brightness(1.08);
149
+ }
150
+
151
+ /* ─────────────────────────────────────────────────────────────────────────
152
+ * Chữ màu primary / error do antdv tự vẽ — theme tối.
153
+ * antdv trộn seed 85% với #141414 nên chữ colorPrimary (#627dbe) chỉ ~4:1 trên surface, colorError (#d66363) ~4.1:1 trên
154
+ * surface-elevated; token primary/error đã ≥ 4.5:1 nên dùng lại. Phủ chỗ core dùng: trang đang chọn của Pagination (CTable),
155
+ * tab đang chọn, item danger của Dropdown (menu thao tác dòng — hover nền gradient-danger như nút danger). Chỗ antdv khác còn
156
+ * chữ màu suy ra mà core chưa dùng nên chưa phủ (vd Radio.Button đang chọn, item đang chọn của Dropdown, chữ lỗi Form trong
157
+ * Modal/Drawer, số của Badge).
158
+ * ───────────────────────────────────────────────────────────────────────── */
159
+ :root[data-theme='dark'] .ant-pagination:not(.ant-pagination-disabled) .ant-pagination-item-active {
160
+ border-color: var(--tasco-color-primary) !important;
161
+ }
162
+ :root[data-theme='dark'] .ant-pagination:not(.ant-pagination-disabled) .ant-pagination-item-active a {
163
+ color: var(--tasco-color-primary) !important;
164
+ }
165
+ :root[data-theme='dark'] .ant-pagination:not(.ant-pagination-disabled) .ant-pagination-item-active:hover {
166
+ border-color: var(--tasco-color-primary-hover) !important;
167
+ }
168
+ :root[data-theme='dark'] .ant-pagination:not(.ant-pagination-disabled) .ant-pagination-item-active:hover a {
169
+ color: var(--tasco-color-primary-hover) !important;
170
+ }
171
+ :root[data-theme='dark'] .ant-tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
172
+ color: var(--tasco-color-primary) !important;
173
+ }
174
+ :root[data-theme='dark'] .ant-tabs .ant-tabs-ink-bar {
175
+ background: var(--tasco-color-primary) !important;
176
+ }
177
+ :root[data-theme='dark'] .ant-dropdown .ant-dropdown-menu .ant-dropdown-menu-item-danger:not(.ant-dropdown-menu-item-disabled) {
178
+ color: var(--tasco-color-error) !important;
179
+ }
180
+ :root[data-theme='dark'] .ant-dropdown .ant-dropdown-menu .ant-dropdown-menu-item-danger:not(.ant-dropdown-menu-item-disabled):hover {
181
+ color: #fff !important;
182
+ background: var(--tasco-gradient-danger) !important;
183
+ }
184
+
185
+ /* ─────────────────────────────────────────────────────────────────────────
186
+ * MẬT ĐỘ COMPONENT (padding / margin) — chỉnh tập trung qua density tokens
187
+ * (@tasco-fe/theme → biến --tasco-*). Dùng global CSS vì ant-design-vue 4.x bỏ
188
+ * qua theme.components. Height của control đã set qua global token controlHeight*.
189
+ * ───────────────────────────────────────────────────────────────────────── */
190
+ /* Card — padding thân. */
191
+ .ant-card > .ant-card-body {
192
+ padding: var(--tasco-card-padding-block, 12px) var(--tasco-card-padding-inline, 16px);
193
+ }
194
+ /* Card — padding header (theo trục ngang của body, cao gọn). */
195
+ .ant-card > .ant-card-head {
196
+ padding-inline: var(--tasco-card-padding-inline, 16px) !important;
197
+ min-height: var(--tasco-card-heading-height, 56px) !important;
198
+ }
199
+ .c-card--collapse .ant-collapse .ant-collapse-header {
200
+ border-bottom: 1px solid var(--tasco-color-border);
201
+ padding: 0 var(--tasco-card-padding-inline, 16px) !important;
202
+ height: var(--tasco-card-heading-height, 56px) !important;
203
+ align-items: center;
204
+ }
205
+ .c-card-primary > .ant-card-head, .c-card-primary .ant-collapse-header {
206
+ background: var(--tasco-color-primary);
207
+ color: #ffffff !important;
208
+ }
209
+ .c-card-primary .c-card__title {
210
+ color: #ffffff !important;
211
+ }
212
+ /* Theme tối: chữ trắng trên primary (sáng) chỉ ~3:1 → header nền primary-active. */
213
+ :root[data-theme='dark'] .c-card-primary > .ant-card-head,
214
+ :root[data-theme='dark'] .c-card-primary .ant-collapse-header {
215
+ background: var(--tasco-color-primary-active);
216
+ }
217
+ /* Type: outline — header gạch dưới primary, không nền; tiêu đề primary. */
218
+ .c-card-outline > .ant-card-head,
219
+ .c-card-outline .ant-collapse-header {
220
+ border-bottom: 2px solid var(--tasco-color-primary) !important;
221
+ }
222
+ .c-card-outline .c-card__title {
223
+ color: var(--tasco-color-primary);
224
+ }
225
+ /* Type: filled — header nền dịu. */
226
+ .c-card-filled > .ant-card-head,
227
+ .c-card-filled .ant-collapse-header {
228
+ background: var(--tasco-color-surface-muted) !important;
229
+ }
230
+ /* Type: ghost — nền trong suốt. */
231
+ .c-card-ghost {
232
+ background: transparent !important;
233
+ }
234
+
235
+ /* Form item — khoảng cách dưới. */
236
+ .ant-form-item {
237
+ margin-bottom: var(--tasco-form-item-margin, 16px) !important;
238
+ }
239
+ /* Modal / Drawer — padding thân. */
240
+ .ant-modal .ant-modal-body,
241
+ .ant-drawer .ant-drawer-body {
242
+ padding: var(--tasco-modal-body-padding, 16px) !important;
243
+ }
244
+
245
+ /* ─────────────────────────────────────────────────────────────────────────
246
+ * Sidebar nav (CSideNav) — dựa trên override OneAuto (#left-side-menu trong
247
+ * main.css): menu trong suốt (sider lo gradient), chữ + icon trắng UPPERCASE,
248
+ * item con thụt 35px bo tròn pill, hover/selected nền cam accent chữ navy.
249
+ * ───────────────────────────────────────────────────────────────────────── */
250
+ .c-sidenav.ant-menu-dark,
251
+ .c-sidenav.ant-menu-dark .ant-menu-sub {
252
+ background: transparent !important;
253
+ }
254
+ .c-sidenav .ant-menu-title-content {
255
+ text-transform: uppercase;
256
+ font-weight: 600;
257
+ }
258
+ .c-sidenav .ant-menu-title-content,
259
+ .c-sidenav .tabler-icon,
260
+ .c-sidenav .ant-menu-item .anticon {
261
+ color: #fff !important;
262
+ }
263
+ .c-sidenav > .ant-menu-item {
264
+ padding: 0 1rem !important;
265
+ }
266
+ .c-sidenav > .ant-menu-submenu > .ant-menu-submenu-title {
267
+ padding-left: 16px !important;
268
+ }
269
+ /* Item con: thụt vào + bo tròn pill. */
270
+ .c-sidenav .ant-menu-sub .ant-menu-item {
271
+ margin-left: 35px !important;
272
+ padding-left: 15px !important;
273
+ border-radius: 9999px !important;
274
+ }
275
+ /* Hover & active đều nền cam. Chữ + icon trên nền cam dùng navy của sidebar: chữ trắng trên cam chỉ ~2:1, navy ≥ 7:1 ở
276
+ * cả hai theme. */
277
+ .c-sidenav .ant-menu-item:hover,
278
+ .c-sidenav .ant-menu-item-selected {
279
+ background-color: var(--tasco-color-accent) !important;
280
+ color: var(--tasco-color-sidebar-bottom) !important;
281
+ }
282
+ .c-sidenav .ant-menu-item:hover .ant-menu-title-content,
283
+ .c-sidenav .ant-menu-item:hover .tabler-icon,
284
+ .c-sidenav .ant-menu-item:hover .anticon,
285
+ .c-sidenav .ant-menu-item-selected .ant-menu-title-content,
286
+ .c-sidenav .ant-menu-item-selected .tabler-icon,
287
+ .c-sidenav .ant-menu-item-selected .anticon {
288
+ color: var(--tasco-color-sidebar-bottom) !important;
289
+ }
290
+ /* Khi thu gọn: submenu cha đang chọn nền cam. */
291
+ .c-sidenav.ant-menu-inline-collapsed .ant-menu-submenu-selected > .ant-menu-submenu-title {
292
+ background-color: var(--tasco-color-accent) !important;
293
+ color: var(--tasco-color-sidebar-bottom) !important;
294
+ }
295
+ .c-sidenav.ant-menu-inline-collapsed .ant-menu-submenu-selected > .ant-menu-submenu-title .tabler-icon,
296
+ .c-sidenav.ant-menu-inline-collapsed .ant-menu-submenu-selected > .ant-menu-submenu-title .anticon {
297
+ color: var(--tasco-color-sidebar-bottom) !important;
298
+ }
299
+
300
+ /* Top nav (CTopNav) — menu module ngang: UPPERCASE bold, active đổi sang primary + gạch chân kiểu tab ở đáy header (giống
301
+ * #top-header-menu). Mục chưa chọn dùng chữ `text`: navy #253264 tô cứng trước đây chỉ ~1.3:1 trên surface theme tối,
302
+ * `text` ≥ 15:1 ở cả hai theme và trùng màu antdv tô khi hover (`colorText`). */
303
+ .c-topnav.ant-menu-horizontal {
304
+ height: 100%;
305
+ line-height: normal;
306
+ color: var(--tasco-color-text);
307
+ border-bottom: none;
308
+ }
309
+ .c-topnav .ant-menu-title-content {
310
+ text-transform: uppercase;
311
+ font-weight: 700;
312
+ }
313
+ /* Item cao trọn header → ink-bar (::after, bottom:0) nằm sát đáy header. */
314
+ .c-topnav.ant-menu-horizontal > .ant-menu-item,
315
+ .c-topnav.ant-menu-horizontal > .ant-menu-submenu {
316
+ height: 100% !important;
317
+ top: 0 !important;
318
+ margin-block: 0 !important;
319
+ display: inline-flex !important;
320
+ align-items: center;
321
+ color: var(--tasco-color-text);
322
+ }
323
+ /* Gạch chân: dày 2px, sát đáy header, ôm theo bề rộng item (trừ padding). */
324
+ .c-topnav.ant-menu-horizontal > .ant-menu-item::after,
325
+ .c-topnav.ant-menu-horizontal > .ant-menu-submenu::after {
326
+ inset-inline: 16px !important;
327
+ bottom: 0 !important;
328
+ border-bottom-width: 2px !important;
329
+ }
330
+ .c-topnav.ant-menu-horizontal > .ant-menu-item-selected,
331
+ .c-topnav.ant-menu-horizontal > .ant-menu-submenu-selected {
332
+ color: var(--tasco-color-primary) !important;
333
+ }
334
+ .c-topnav.ant-menu-horizontal > .ant-menu-item-selected::after,
335
+ .c-topnav.ant-menu-horizontal > .ant-menu-item:hover::after,
336
+ .c-topnav.ant-menu-horizontal > .ant-menu-submenu-selected::after,
337
+ .c-topnav.ant-menu-horizontal > .ant-menu-submenu:hover::after {
338
+ border-bottom-color: var(--tasco-color-primary) !important;
339
+ }