@vestow/theme
v0.10.0
Published
Theme của WoodBlock: Design Token dạng CSS thuần, registry chín trục, controller vanilla và definePreset. Không biết framework nào.
Readme
@vestow/theme
Theme của WoodBlock, trọn gói: giá trị token dạng CSS thuần, và tầng chạy
đọc lựa chọn đã lưu rồi ghi tám trục lên <html>.
| | dạng | nạp thế nào |
|---|---|---|
| giá trị token, tám trục theme, tầng token component | CSS thuần | @import "@vestow/theme/css/index.css" |
| entry cho package component (Tailwind + @layer dùng chung) | CSS | @import "@vestow/theme/css/app.css" |
| registry tám trục, controller, definePreset | TypeScript | import { themeController } from '@vestow/theme' |
Hai nửa độc lập: nạp CSS không kéo về một byte JavaScript, và import JS không kéo
về một byte CSS. Một app Angular chỉ cần @import bộ token thì không phải chạm
tới nửa kia.
Phần TS là vanilla — không import framework nào, không chạm document ở tầng
module, nên import được trong SSR.
Ba entry CSS, xếp từ dưới lên
| file | thêm gì | ai nạp |
|---|---|---|
| css/index.css | 8 file token, CSS thuần — không một at-rule Tailwind nào | app không dùng Tailwind |
| css/tailwind.css | @custom-variant dark + @theme inline | app dùng Tailwind, tự khai @layer |
| css/app.css | @import "tailwindcss" source(none), tw-animate-css, và các khối @layer dùng chung (.scroll-edge, .scroll-area-x, reset base) | @vestow/vue |
Hai file đứng ngoài index.css vì cái giá đo được: charts.css (3.084 B, trục
data-chart) và typeset.css (2.703 B, lớp văn xuôi .typeset). App nào cần thì tự
@import.
css/primitives.css là file TỰ SINH từ tailwindcss/theme.css — đừng sửa tay;
pnpm -F @vestow/theme gen sinh lại, và pnpm lint bắt file cũ.
Controller
import { themeController } from '@vestow/theme'
themeController.init() // đọc localStorage, áp lên <html>, nghe `storage`
themeController.setPalette('solar')
themeController.subscribe(state => console.log(state.isDark))Vỏ bọc cho từng framework chỉ còn việc nối subscribe() vào hệ reactivity của
nó. Bản Vue ở vue/packages/ui/src/composables/useThemeController.ts dài 48
dòng — đó là toàn bộ phần code riêng của Vue cho tám trục.
Viết vỏ bọc mới thì đọc src/controller.ts trước; đừng viết lại logic. Chỉ
cần một bên quên style.colorScheme hay gõ sai khoá localStorage là người dùng
đổi framework sẽ mất lựa chọn đã lưu — mà cả hai lỗi đó đều không làm gì gãy,
chỉ âm thầm sai.
Preset có tên — @vestow/theme/presets
Năm preset, đúng năm bảng màu: Hyperion (mặc định), Solar, Nebula, Aurora,
Eclipse. Mỗi cái là một ThemePreset — object thường, tự dựng hay spread được:
import { Solar } from '@vestow/theme/presets'
themeController.setDefaults(Solar.axes) // TRƯỚC init()
themeController.init()Bên Vue thì app.use(createWoodBlock({ theme: { preset: Solar } })) làm đúng việc đó.
setDefaults khác setPalette ở đúng một chỗ, và đó là toàn bộ lý do nó tồn
tại: nó KHÔNG ghi localStorage. Preset là giao diện ban đầu; người dùng đã chọn
bảng màu khác thì lựa chọn đó thắng ở mọi lần tải trang sau. Một preset gọi
setPalette thì mỗi lần tải trang lại đè lựa chọn của người dùng.
Mỗi preset chỉ chốt palette, cố ý không chốt base: bảng màu đã tự mang họ
trung tính của nó, và bộ số AA của palettes.css đo trên đúng tổ hợp đó.
Tuỳ biến theo cách PrimeVue — một preset gốc, một object ghi đè:
import { definePreset } from '@vestow/theme'
import { Nebula } from '@vestow/theme/presets'
const MyBrand = definePreset(Nebula, {
name: 'MyBrand',
axes: { base: 'stone', style: 'nova' }, // gộp nông
tokens: { base: { button: { radius: '2px' } } }, // gộp sâu
})Hàm này thuần tuý: trả preset mới, không sửa Nebula, không đụng DOM.
base: 'slate' không ép slate lên bảng màu khác Hyperion: nó là mặc định nên không
có data-base nào để CSS bám vào, và nền rơi về họ của chính bảng màu (Solar + slate
ra stone). Bảy họ còn lại luôn thắng họ của bảng màu.
Ngoài năm preset bảng màu, module còn xuất một preset cho mỗi giá trị của hai trục kia —
hình khối (Vega · Nova · Maia · Lyra · Mira · Luma · Sera · Rhea), họ trung tính (Slate · Zinc · Stone · Mist · Taupe · Olive
· Mauve · Neutral) — cùng ba mảng palettePresets / stylePresets / basePresets, theo đúng thứ tự registry (presets là tên cũ của palettePresets).
Chúng sinh ra để dựng bộ chọn giao diện (lặp mảng, hiện name, gọi setStyle), không
phải để lồng definePreset(definePreset(...)) — cấu hình cố định thì viết axes như trên.
definePreset — một tham số: ghi token lên trang
Ghi đè token lúc chạy, cùng vai với definePreset của PrimeVue. Dùng cho đúng
một ca: giá trị chỉ biết lúc chạy — màu thương hiệu đọc từ API, theme người
dùng lưu trong tài khoản, bản xem trước trong trình dựng theme. Ngoài ca đó thì
viết :root { --primary: … } trong CSS của app vẫn gọn hơn.
import { definePreset } from '@vestow/theme'
definePreset({
base: { radius: '2px' },
light: { primary: { $: 'oklch(0.55 0.2 30)', foreground: '#fff' } },
dark: { primary: '#7dd3fc' },
})Tên token có kiểu: gõ sai là lỗi biên dịch kèm gợi ý "did you mean", không
phải một biến rỗng và một cái nút trong suốt. 221 tên, sinh từ chính CSS của
@vestow/theme.
Hai dạng khoá, dùng lẫn nhau được
| | |
|---|---|
| cây | { button: { radius: '2px' } } — dạng button.border.radius của PrimeVue |
| phẳng | { '--button-radius': '2px' } — đúng tên CSS |
Không mơ hồ, vì tên custom property luôn bắt đầu bằng -- còn nhánh thì không
bao giờ. Trộn hai dạng trong một object cũng được.
Dạng phẳng không bỏ đi: nó là thứ duy nhất viết được một tên tính lúc chạy, và khi debug thì nó đọc thẳng ra tên CSS thật, khỏi phải dịch ngược.
Khoá $ là giá trị của chính nhánh. Cần nó vì 56/221 tên vừa là lá vừa
là nhánh — --accent cạnh --accent-foreground, --button-radius cạnh
--button-radius-sm — mà một khoá không thể vừa mang chuỗi vừa mang object.
Đúng vai root trong preset của PrimeVue, chỉ ngắn hơn và không bao giờ đụng
một đoạn tên thật.
{ button: { radius: '2px' } } // chỉ --button-radius
{ button: { radius: { $: '2px', sm: '4px' } } } // cả --button-radius và -smChuỗi trần đặt vào một nhánh là lối tắt cho $.
Cây SUY RA từ tên CSS, không phải ngược lại. Đó là chỗ khác PrimeVue: bên họ tên CSS sinh từ object cấu hình, ở đây object sinh từ CSS — nên không có nguồn thứ hai để mà trôi, và thêm một token vào CSS là cây tự có nhánh mới.
Render phía server thì dùng presetToCss() — bản thuần, không đụng DOM.
Nhúng kết quả vào một thẻ <style id="woodblock-preset"> ngay trong HTML trả về:
trang không chớp màu, và definePreset() phía trình duyệt nhận lại đúng thẻ đó
thay vì thêm thẻ thứ hai.
Ba điều phải biết trước khi dùng, chi tiết ở đầu src/preset.ts:
- Giá trị là chuỗi CSS, JS không hiểu nó. Cố ý — token ở đây viết bằng
color-mix(in oklab, …)vàoklch(from …); mô hình hoá lại mấy phép đó trong JS là viết lại một engine màu, và đã đo được là nó ra số khác trình duyệt. - Preset ghi ở
:root, nên nó không đè được token mà một rule khác khai lại trên phần tử CON — khai báo trực tiếp luôn thắng giá trị thừa kế. - Đặc hiệu là
:rootlặp 5 lần, con số ĐO chứ không chọn cho tròn: rule đặc hiệu nhất đang khai token ở tầng:rootlà (0,4,0).
Tám trục
| trục | ghi lên <html> | khoá localStorage | mặc định |
|---|---|---|---|
| chế độ màu | class .dark + style.color-scheme | hyperion-color-mode | system |
| bảng màu | data-palette | hyperion-palette | hyperion |
| hình khối | data-style | hyperion-style | vega |
| họ trung tính | data-base | hyperion-base | slate |
| bộ màu biểu đồ | data-chart | hyperion-chart-palette | theme |
| màu menu | data-menu-color | hyperion-menu-color | default |
| bề mặt menu | data-menu-appearance | hyperion-menu-appearance | solid |
| highlight menu | data-menu-accent | hyperion-menu-accent | subtle |
Giá trị mặc định không có khối CSS nào, nên controller gỡ hẳn thuộc tính
thay vì ghi vào. Các trục độc lập — Solar + lyra hay Nebula + stone đều
dùng được.
Kính đã gỡ hẳn. Trục
data-surfacegỡ ở 0.8.0; từ 0.9.0 propglasscủa@vestow/vuecũng gỡ, cùng mọi token--glass-*/--liquid-*và hai thuộc tínhdata-glass/data-glass-leveltrên<html>. Khoáhyperion-surface,hyperion-glass-enabled/hyperion-glass-levelcòn trong localStorage của người dùng cũ thì vô hại: không còn gì đọc chúng. Trụcdata-menu-appearance="translucent"thì ở lại — nó là trục của menu, có token riêng.
Tiền tố hyperion- là một định danh lưu trữ, không phải tên thương hiệu. Đổi nó
là xoá lựa chọn đã lưu của mọi người dùng hiện có.
Lệnh
pnpm -F @vestow/theme gen # sinh lại primitives.css + token-names.ts từ CSS
pnpm -F @vestow/theme check # kiểm nó không trôi (chạy sẵn trong `pnpm lint`)
pnpm -F @vestow/theme build # biên dịch TS ra dist/ (chỉ cần khi publish)Publish
exports trỏ vào src/ cho tiện phát triển trong workspace; publishConfig
đổi sang dist/ lúc pnpm publish, và prepack chạy gen + build trước.
files đóng gói css/ và dist/. Phần CSS đi thẳng, không qua build; phần TS
thì prepack biên dịch trước.
Hai dependency runtime — tailwindcss và tw-animate-css — chỉ phục vụ
css/app.css. App nạp css/index.css (CSS thuần) không chạm tới chúng.
