@vestow/vue
v0.10.0
Published
Bộ component Vue 3 của WoodBlock: 85 component trên nền Reka UI, kèm 27 bảng cva và bộ chuỗi i18n của chính thư viện. Token và theme ở @vestow/theme.
Readme
@vestow/vue
85 component Vue 3, phần lớn dựng trên Reka UI, tô bằng Tailwind v4 và
@vestow/theme. Bộ này là phần Vue của WoodBlock.
Nó mang theo hai tầng mà trước đây là package riêng, và mang vì cùng một lý do — chúng không dùng được nếu thiếu component đọc chúng:
| trong package | là gì |
|---|---|
| src/variants | 27 bảng cva, cn(), vòng focus, class menu, hình học chart |
| src/i18n | locale controller + 126 khoá mà chính component phát ra (aria-label của Carousel, nút Đóng của Dialog, lỗi Zod) |
Thứ đứng riêng là @vestow/theme: Design Token dạng CSS thuần cộng registry chín trục.
App nào chỉ cần token — kể cả app không dùng Vue — nạp mình nó là đủ.
Package export thẳng .vue source, không qua build — app mới là bên biên dịch. Đó là
chủ ý: Tailwind phải quét được class thật, và app tự quyết mức tree-shake. Kéo theo vài
ràng buộc ở phần cài đặt bên dưới, đọc kỹ ba mục có dấu ⚠.
Cài
pnpm add @vestow/vue @vestow/theme
pnpm add -D typescript tailwindcss @tailwindcss/vite @vitejs/plugin-vue⚠ typescript là bắt buộc, kể cả app JavaScript thuần
@vue/compiler-sfc cần TypeScript để phân giải type trong defineProps<T>, mà component
ở đây khai props bằng type import từ reka-ui. Thiếu nó thì build đổ hàng loạt với đúng
một câu, lặp lại cho từng file:
[@vue/compiler-sfc] Failed to load TypeScript, which is required for
resolving imported types.Đã đo: 297 lỗi, tất cả cùng gốc này.
⚠ Và typescript phải là 6.x, không phải 7
Cái bẫy tệ hơn, vì pnpm add -D typescript hôm nay kéo về 7.x và mọi thứ trông như đã
đúng cho tới lúc build:
[@vue/compiler-sfc] No fs option provided to `compileScript` in non-Node environment.
File system access is required for resolving imported types.Đo trên một app trống cài từ tarball: [email protected] → 298 lỗi; hạ xuống
[email protected] → build xanh, không sửa gì khác.
pnpm add -D "typescript@~6.0.2"Nguyên nhân, đo chứ không đoán. typescript@7 là bản viết lại bằng Go, và entry mặc
định của nó chỉ còn hai trường:
require('typescript') // -> { version: '7.0.2', versionMajorMinor: '7.0' }Toàn bộ compiler API dời sang typescript/unstable/* với hình dạng khác hẳn — một lớp
API/Project/Program thay cho namespace ts.* phẳng. Còn @vue/compiler-sfc gọi
thẳng API cũ: ts.sys để đọc file, rồi ts.findConfigFile / ts.resolveModuleName để
tìm ra reka-ui khi bạn viết defineProps<AccordionTriggerProps>. Với TS 7 thì ts.sys
là undefined, và câu "non-Node environment" ở trên là compiler-sfc suy ra nhầm rằng nó
đang chạy trong trình duyệt.
Cấp fs thủ công KHÔNG cứu được — đã thử, vì nó chỉ chữa được nửa đầu:
| | lỗi |
|---|---|
| TS 7 | 298 |
| TS 7 + vue({ script: { fs } }) | 168 — ts2.findConfigFile is not a function |
| TS 6 | 0 |
fs lo được import tương đối (../../lib/types); import bare (reka-ui) thì
bắt buộc đi qua bộ phân giải module của TypeScript, và bộ đó không tồn tại ở TS 7.
Nên đây không phải lựa chọn của package này: chừng nào @vue/compiler-sfc chưa port sang
API mới thì mọi thư viện Vue ship .vue source đều đứng ở TS 6.
Vẫn dùng được TS 7 — cài song song, cách chính thức của Microsoft
Ràng buộc thật không phải "cấm TS 7", mà là require('typescript') phải trả về API 6.x.
TypeScript 7.0 (08/07/2026) phát hành có chủ ý mà không có API — bản thay thế hẹn ở
7.1 — và ghi thẳng trong thông báo phát hành: "Projects using Vue, MDX, Astro, Svelte, and
others will need to continue using TypeScript 6.0 for now." Kèm theo là gói tương thích
@typescript/typescript6.
Đo trên app thử ở trên, và nó chạy:
pnpm add -D "typescript@npm:@typescript/[email protected]" "typescript7@npm:[email protected]"| | kết quả |
|---|---|
| vite build (compiler-sfc thấy API 6.x) | xanh, CSS đủ class |
| vue-tsc --noEmit | chạy |
| tsc --version | 7.0.2 — binary đến từ gói TS 7 |
| tsc --noEmit trên file .ts thuần | bắt lỗi bằng TS 7 |
Tức phần TypeScript thuần của bạn chạy TS 7, chỉ riêng đường biên dịch SFC là đọc API 6.x.
(Microsoft bày chiều ngược lại — typescript là 7 còn gói tương thích cấp tsc6 — nhưng
với một dự án Vue thì phải để tên typescript cho bản 6, vì compiler-sfc không cho đổi chỗ
nó tìm.)
Mốc cần theo dõi: TypeScript 7.1, kế hoạch phát hành stable 24/11/2026, trong đó có
mục "Stabilize API". Port compiler-sfc sang API đó là việc của Vue và chưa có mốc; issue
TS-native ở vuejs/language-tools đã đóng với nhãn upstream: typescript. Theo dõi phía
Vue và phía TS, đừng theo dõi phía đây.
Cả hai ràng buộc TypeScript ở mục này là cái giá trực tiếp của việc export .vue source.
Một package build sẵn ra .js + .d.ts thì không dính cái nào — đó cũng là đường thoát
nếu ngày nào TS 6 hết đường lùi.
⚠ pnpm 11 phải cho vue-demi chạy build script
reka-ui (qua @floating-ui/vue) và @tanstack/vue-form đều dùng nó, và postinstall của
nó là thứ chọn shim đúng cho Vue 3. Trong pnpm-workspace.yaml:
allowBuilds:
vue-demi: true
esbuild: true
# maplibre-gl đi kèm @unovis/ts cho phần bản đồ. Không dùng bản đồ thì để chặn.
maplibre-gl: falseNạp CSS
Đúng hai dòng, và thứ tự có ý nghĩa:
@import "@vestow/vue/styles.css";
@source "./";⚠ Đừng khai @source trỏ vào node_modules
@vestow/vue/styles.css đã tự khai @source "../" cho chính nó, mà Tailwind phân giải
@source tương đối theo file CSS chứa nó — nên nằm trong node_modules nó vẫn trỏ
đúng vào source của mình. Bạn chỉ cần khai vùng của app bạn.
Nó cũng đã @import "tailwindcss" source(none) và nạp sẵn token. Viết thêm
@import "tailwindcss" ở file của bạn là nạp hai lần — đo được, dư 19 KB CSS thô.
Nghiệm thu không phải bằng mắt: build xong grep file CSS tìm ui-button và --focus-ring.
Có thì @source đúng; DOM render đủ mà sạch class là dấu hiệu vùng quét bị hụt.
Dùng
// main.ts
import { createApp } from 'vue'
import { themeController } from '@vestow/theme'
import App from './App.vue'
import './style.css'
themeController.init()
createApp(App).mount('#app')<script setup lang="ts">
import { Badge, Button, Card, CardContent } from '@vestow/vue'
</script>
<template>
<Card>
<CardContent>
<Button variant="destructive" fill="soft">Xoá</Button>
<Badge>Mới</Badge>
</CardContent>
</Card>
</template>themeController.init() là thứ đọc localStorage và áp ba trục giao diện lên <html>.
Không gọi thì component vẫn chạy, chỉ luôn ở bảng màu mặc định và không nhớ lựa chọn.
Hoặc: một lời gọi app.use, với preset có tên
Cùng cách gọi với PrimeVue:
import { createWoodBlock } from '@vestow/vue'
import { Solar } from '@vestow/theme/presets'
createApp(App)
.use(createWoodBlock({
theme: { preset: Solar },
}))
.mount('#app')Tuỳ biến theo đúng cách PrimeVue làm với definePreset(Aura, {...}): 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: palette 'nebula' giữ
tokens: { base: { button: { radius: '2px' } } }, // gộp SÂU theo base / light / dark
})
app.use(createWoodBlock({ theme: { preset: MyBrand } }))| trường | là gì | đi tới |
|---|---|---|
| axes | giá trị mặc định của các trục — bảng giá trị ngay dưới | themeController.setDefaults() |
| tokens | ghi đè token, đè lên trục đang bật | definePreset() một tham số |
Hàm này thuần tuý: trả về preset mới, không sửa Nebula, không đụng DOM — nên mở rộng
tiếp từ MyBrand được. Ghi đè một chuỗi lên một nhánh (accent: 'red' lên
accent: { foreground }) chỉ đổi --accent, không xoá --accent-foreground.
Preset gốc — năm bảng màu
Đúng năm bảng màu đã đo tương phản AA:
| preset | primary | trung tính đi kèm |
|---|---|---|
| Hyperion | xanh lam — mặc định | slate |
| Solar | cam | stone |
| Nebula | tím | zinc |
| Aurora | xanh ngọc | mist |
| Eclipse | đơn sắc | neutral |
Giá trị của axes
| trục | giá trị |
|---|---|
| style — bo góc, đường kẻ, độ thoáng | vega (mặc định) · nova bo lớn, kẻ mờ · maia gần vuông · lyra vuông, kẻ đậm · mira mềm nhất · luma kẻ gần như không thấy · sera dày đặc · rhea mềm, kẻ đậm |
| base — họ trung tính | slate (mặc định) · zinc · stone · mist · taupe · olive · mauve · neutral |
| palette | năm id của năm preset gốc |
| colorMode · chart · menuAccent · menuColor · menuAppearance | các trục còn lại của themeController |
axes có kiểu: gõ style: 'novaa' là lỗi biên dịch.
base: 'slate'nghĩa là "để bảng màu tự quyết", không phải "ép slate".slatelà mặc định nên CSS không có khối nào cho nó và controller gỡ hẳndata-base— nền rơi về họ mà bảng màu mang theo. Đo được: Solar +slatera--tint-hue: 56(stone), không phải 265. Bảy họ còn lại thì luôn thắng họ của bảng màu, kể cả Eclipse.
Trục
surface(data-surface="glass") đã gỡ ở 0.8.0, cùnguseSurface,Solid/GlassvàsurfacePresets. Propglassthay nó ở 0.8.0 cũng đã gỡ ở 0.9.0 — xem mục Button bên dưới.
Preset là giao diện BAN ĐẦU, không phải giao diện CỐ ĐỊNH. axes đi qua
themeController.setDefaults() — hàm này KHÔNG ghi localStorage. Người dùng đã đổi bảng
màu thì lựa chọn của họ thắng preset ở mọi lần tải trang sau.
Preset theo từng trục — cho bộ chọn giao diện, không phải để lồng
@vestow/theme/presets còn xuất một preset cho mỗi giá trị trục (Nova, Stone…)
cùng ba mảng palettePresets / stylePresets / basePresets. Chúng
sinh ra để dựng bộ chọn — lặp qua mảng, hiện name, gọi setStyle(p.axes.style) — chứ
không phải để lồng definePreset(definePreset(...)). Muốn một cấu hình cố định thì viết
axes như khối đầu mục này.
definePresetvới một tham số vẫn là hàm cũ — ghi token thẳng lên trang. Hai cách gọi phân biệt bằng số tham số, và kiểu trả về (ThemePreset/void) khác nhau nên dùng nhầm thìtscbáo.
Plugin là vỏ mỏng: nó gọi setDefaults(axes) → definePreset(tokens) →
themeController.init(), theo đúng thứ tự đó và trước mount(). Không giữ state riêng,
không provide, không đăng ký component toàn cục — gọi rời ba hàm đó vẫn đúng y như trước.
Plugin KHÔNG đụng tới i18n — đó là một trục khác hẳn theme. Cần thì import và gọi tay:
import { setupZodMessages } from '@vestow/vue/form'
import { i18nController } from '@vestow/vue/i18n'
i18nController.addMessages('vi', { 'ui.close': 'Tắt' }) // ghi đè chuỗi thư viện
setupZodMessages() // lỗi zod theo ngôn ngữ đang bậti18nController.init() không bắt buộc: component đầu tiên gọi useI18n() sẽ tự gọi nó.
SSR: gọi được (mọi hàm tự bỏ qua khi không có document), nhưng controller vẫn là
singleton ở tầng module — hai request trên cùng một server dùng chung state. Plugin này
không giải quyết chuyện đó.
Hai trục của Button và Badge
| prop | giá trị | nghĩa |
|---|---|---|
| variant | default, secondary, destructive, success, warning, info, link | màu |
| fill | solid (mặc định), soft, surface, outline, ghost | kiểu tô theo Radix Themes |
0.8.0:
toneđã gỡ khỏi Button, Badge và Chip.tone="soft"đổi thànhfill="soft"— nền nhạt hơn, không viền. Cần giữ viền thìfill="surface".
0.9.0: kính đã gỡ hẳn. Prop
glasskhông còn trên Button, Card, PopoverContent, HoverCardContent, DialogContent, SheetContent, Sidebar, Toolbar, Toaster, SpeedDial và VideoPlayer;useGlassEffect/useGlassEnabled, các chuỗi classglass*ở@vestow/vue/variantsvà mục "Kính" trongPaletteMenugỡ theo. Chỗ gọi còn sótglasskhông có tác dụng gì — Vue thả nó xuống DOM thành một thuộc tính thừa. Lý do: không nấc kính nào bảo đảm 4,5:1, và ngoài trang giới thiệu thì phía sau các bề mặt đó hiếm khi có gì để làm mờ.Menu trong mờ (
data-menu-appearance="translucent") thì ở lại — nó là trục của menu.
0.9.0:
variant="outline"vàvariant="ghost"của Button đã gỡ. Viếtvariant="secondary" fill="outline"/fill="ghost"— diện mạo giữ nguyên: viền--input, hover--accent. Các sắc khác vớifill="outline"vẫn dùng viền đậm của thang vai trò.variant="link"ở lại vì chưa có kiểu tô tương đương.
Import lẻ
exports mở sẵn ba nhánh con, dùng khi không muốn kéo cả index.ts:
import Button from '@vestow/vue/components/button/Button.vue'
import { cn } from '@vestow/vue/lib/utils'
import { useI18n } from '@vestow/vue/composables/useI18n'
// Hai tầng không-Vue, dùng được mà không kéo component nào:
import { buttonVariants } from '@vestow/vue/variants/components/button'
import { i18nController, locales } from '@vestow/vue/i18n'Subpath cho component kéo gói nặng
Năm nhóm component cần một thư viện lớn đi kèm. Chúng không nằm ở barrel gốc, mà ở subpath riêng, và thư viện đi kèm là peer tuỳ chọn: app không dùng thì không cài.
| import từ | gồm | cài thêm |
|---|---|---|
| @vestow/vue/chart | AreaChart, BarChart, LineChart, PieChart, RadarChart, RadialChart, ChartContainer… | @unovis/ts @unovis/vue |
| @vestow/vue/data-table | DataTable, DataTablePagination, DataTableFacetedFilter, adapter phân trang… | @tanstack/vue-table @tanstack/vue-virtual |
| @vestow/vue/tree-table | TreeTable | @tanstack/vue-table |
| @vestow/vue/form | Form, FormField, FormItem…, setupZodMessages | @tanstack/vue-form zod |
| @vestow/vue/carousel | Carousel và các phần con | embla-carousel embla-carousel-vue |
import { Button } from '@vestow/vue'
import { BarChart } from '@vestow/vue/chart'Barrel gốc import tĩnh mọi component nó export, nên chỉ cần một component ở đó với tới
@unovis là app không cài @unovis build hỏng — kể cả khi không dùng biểu đồ. Vì vậy
test/barrel.test.ts đi theo import từ index.ts và đỏ nếu chạm tới một peer tuỳ chọn.
TableQuery và các kiểu tham số phân trang vẫn ở barrel gốc (DataView, useTableQuery
dùng chúng) và không phụ thuộc TanStack.
0.9.0: trước đây năm nhóm này ở
@vestow/vuevà các thư viện kia là dependency thật. Đổi import sang subpath và tự cài thư viện đi kèm.
Peer
vue là peer dependency (^3.5.42). @vestow/theme là dependency thật — cài
@vestow/vue là có luôn. Các peer tuỳ chọn ở bảng trên.
Publish
Không có bước build: files đóng gói index.ts + src/, và exports trỏ thẳng vào
source ở cả chế độ phát triển lẫn sau khi publish. Đóng gói thử mà không cần registry:
pnpm -F @vestow/vue pack --pack-destination /tmpMuốn thử cài tarball ở một project khác thì @vestow/theme cũng phải có mặt — nó là
dependency thật, nên pnpm sẽ đi tìm trên registry. Trỏ nó về tarball local bằng
overrides trong pnpm-workspace.yaml của project đó, hoặc dựng một registry local
(npx verdaccio) rồi publish cả hai vào đó.
overrides:
'@vestow/theme': file:/đường/dẫn/vestow-theme-0.1.1.tgz