Design Tokens
Design tokens là nguồn sự thật duy nhất cho tất cả các giá trị giao diện trên nền tảng VENI-AI — màu sắc, bo góc, cỡ chữ, và màu trạng thái. Chúng được định nghĩa một lần tại packages/design-tokens/ và phân phối đến từng ứng dụng dưới dạng file CSS đã được commit trực tiếp vào repo.
1. Tại Sao Dùng Vendored Thay Vì Package?
Mỗi SCS app là hoàn toàn độc lập — repo riêng, Docker build riêng, CI/CD riêng. Dùng workspace:* sẽ gắn kết app với monorepo và phá vỡ tính độc lập khi build.
Thay vào đó, mỗi app lưu bản sao tokens.css của riêng mình. Cập nhật được áp dụng theo lịch trình của từng team qua lệnh veni update-tokens.
packages/design-tokens/dist/css/tokens.css ← nguồn gốc (đã commit)
│
├── veni update-tokens
│
├── shell/ui/src/themes/tokens.css ← bản sao shell
├── templates/veni-service/ui/src/themes/tokens.css ← bản sao template
└── apps/<name>/ui/src/themes/tokens.css ← bản sao từng app2. Cấu Trúc Package
packages/design-tokens/
├── tokens/
│ ├── primitives/
│ │ ├── colors.json # Bảng màu thô: blue, slate, red, green, amber
│ │ └── radius.json # --radius: 0.5rem
│ └── semantic/
│ ├── light.json # Vai trò token cho light mode
│ └── dark.json # Ghi đè cho dark mode
├── style-dictionary.config.mjs # Script build (Style Dictionary v3)
└── dist/css/tokens.css # ✅ File đầu ra — đã được commit vào repo3. Các Tầng Token
Primitives
Giá trị thô có tên. Không bao giờ tham chiếu trực tiếp trong component.
// tokens/primitives/colors.json
{
"color": {
"blue": {
"600": { "value": "#2563eb", "type": "color" }
},
"slate": {
"900": { "value": "#0f172a", "type": "color" }
}
}
}Semantic Tokens
Vai trò có tên ánh xạ đến các primitive. Đây là giá trị mà component sử dụng.
// tokens/semantic/light.json
{
"semantic": {
"primary": { "value": "{color.blue.600}", "type": "color" },
"background": { "value": "{color.slate.50}", "type": "color" },
"foreground": { "value": "{color.slate.900}", "type": "color" },
"border": { "value": "{color.slate.200}", "type": "color" }
}
}// tokens/semantic/dark.json — ghi đè dưới class .dark
{
"semantic-dark": {
"primary": { "value": "{color.blue.500}", "type": "color" },
"background": { "value": "{color.slate.900}", "type": "color" }
}
}4. File Đầu Ra
Style Dictionary phân giải tất cả tham chiếu và tạo ra tokens.css duy nhất:
/* Primitives */
:root {
--radius: 0.5rem;
--font-size: 16px;
--font-weight-normal: 400;
--font-weight-medium: 500;
}
/* Semantic — Light */
:root {
--background: #f8fafc;
--foreground: #0f172a;
--primary: #2563eb;
--primary-foreground: #ffffff;
--border: #e2e8f0;
/* ... */
}
/* Semantic — Dark */
.dark {
--background: #0f172a;
--primary: #3b82f6;
/* ... */
}
/* Tailwind v4 @theme map */
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
--color-border: var(--border);
--radius-sm: calc(var(--radius) - 4px);
--radius-lg: var(--radius);
/* ... */
}Block @theme inline cho Tailwind v4 biết cần tạo utility class nào (bg-primary, text-foreground, border-border, v.v.).
5. Danh Sách Token
| Token | Light | Dark | Sử dụng |
|---|---|---|---|
--background | #f8fafc | #0f172a | Nền trang |
--foreground | #0f172a | #f8fafc | Chữ mặc định |
--primary | #2563eb | #3b82f6 | Thương hiệu, nút bấm |
--primary-foreground | #ffffff | #ffffff | Chữ trên primary |
--secondary | #f1f5f9 | #334155 | Bề mặt phụ |
--muted | #f1f5f9 | #334155 | Nền mờ |
--muted-foreground | #64748b | #94a3b8 | Chữ mờ |
--accent | #f1f5f9 | #334155 | Trạng thái hover |
--destructive | #dc2626 | #ef4444 | Lỗi, xóa |
--border | #e2e8f0 | #334155 | Viền |
--input | transparent | #334155 | Viền input |
--input-background | #f8fafc | #1e293b | Nền input |
--ring | #2563eb | #3b82f6 | Focus ring |
--card | #ffffff | #1e293b | Bề mặt card |
--sidebar | #ffffff | #1e293b | Sidebar |
--sidebar-primary | #2563eb | #3b82f6 | Nav item active |
--success | #10b981 | #10b981 | Thành công |
--warning | #f59e0b | #f59e0b | Cảnh báo |
--info | #3b82f6 | #3b82f6 | Thông tin |
--chart-1…5 | Thang xanh | Thang xanh | Biểu đồ |
--radius | 0.5rem | — | Bo góc |
6. Cách App Sử Dụng Token
Mỗi app có tokens.css đã commit, được import tại điểm vào CSS:
/* ui/src/themes/index.css */
@import 'tailwindcss';
@import './tokens.css'; /* ← design tokens đã vendor */
@import './themes.css'; /* ← ghi đè theme tuỳ chọn */
@custom-variant dark (&:is(.dark *));Không cần dependency npm. Không có request mạng runtime. File hoàn toàn tự chứa.
Dùng token trong component
// ✅ Luôn dùng Tailwind class dựa trên token — tự thích nghi dark mode
<div className="bg-background text-foreground">
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
<p className="text-muted-foreground text-sm">
<div className="border border-border rounded-lg">
<input className="bg-input-background border-input focus:ring-ring">
// ❌ Không bao giờ hardcode màu
<div className="bg-[#f8fafc]">
<button className="bg-blue-600">Dark mode được áp dụng bằng cách thêm .dark vào <html> (qua next-themes):
const { setTheme } = useTheme();
setTheme('dark'); // thêm class="dark" vào <html>7. Lệnh CLI
Cập nhật token cho tất cả app
Build lại dist/css/tokens.css và sao chép vào shell, template, và tất cả apps/*/ui/src/themes/tokens.css đã có file:
veni update-tokensTuỳ chọn:
| Flag | Mô tả |
|---|---|
--no-build | Bỏ qua build, chỉ sync từ dist/css/tokens.css hiện có |
Chỉ build lại token
cd packages/design-tokens && bun run build8. Thêm Token Mới
Bước 1. Thêm giá trị primitive nếu cần (tokens/primitives/colors.json):
"purple": {
"500": { "value": "#8b5cf6", "type": "color" }
}Bước 2. Thêm semantic token vào light.json và dark.json:
// light.json
"brand-accent": { "value": "{color.purple.500}", "type": "color" }
// dark.json
"brand-accent": { "value": "{color.purple.400}", "type": "color" }Bước 3. Build và sync:
veni update-tokensBước 4. Commit file tokens.css đã cập nhật trong từng app repo. Token mới có sẵn dưới dạng --brand-accent (CSS var) và bg-brand-accent / text-brand-accent (Tailwind class).
9. Scaffold App Mới
Khi chạy veni create app <name>, CLI tự động sao chép packages/design-tokens/dist/css/tokens.css vào apps/<name>/ui/src/themes/tokens.css. App mới đã sẵn sàng dùng token ngay, không cần thiết lập thêm.
10. Ghi Đè Theme Theo App
App có thể ghi đè từng token mà không cần sửa tokens.css. Template đi kèm với themes/themes.css:
/* Áp dụng qua className trên root element */
.theme-blue {
--primary: var(--color-blue-700);
--sidebar-primary: var(--color-blue-600);
}
.theme-green {
--primary: var(--color-lime-600);
--sidebar-primary: var(--color-lime-600);
}Các class này ghi đè semantic token lúc runtime — tokens.css không bị sửa đổi.