Nền tảng
Server và client
@comitor/ui được build mỗi file nguồn thành một module, nên mỗi file giữ chỉ thị "use client" của riêng nó. Vì vậy "React Server Component có gọi được thứ này không?" là một câu hỏi thật, trả lời theo từng tên — và câu trả lời không như phần lớn mọi người đoán. Bảy hàm chỉ làm mỗi việc nối chuỗi class lại ném lỗi khi một trang server gọi chúng.
Trang này là bản kiểm kê đầy đủ. Bản thân nó cũng là một Server Component, và các chuỗi class cùng giá trị hằng ở phía dưới do chính nó tính lúc build.
Ranh giới nằm ở đâu
Gói build bằng tsup với bundle: false: mỗi file nguồn được transpile thành một module ESM riêng chứ không gộp vào vài chunk. Đó không phải chuyện tree-shaking cho đẹp, mà là lý do ranh giới hoạt động được. Đầu ra đã bundle sẽ đẩy một dòng "use client" vào giữa một chunk, nơi nó vô nghĩa, và kéo cả các primitive tĩnh sang client theo.
Luật
Export của một file thừa hưởng chỉ thị của file đó. Mọi thứ export từ một file "use client" tới server dưới dạng tham chiếu client — component, hook, hàm dựng class, hằng chuỗi, đều như nhau.
Điều nó không phải
Nó không liên quan tới việc mã làm gì. Một hàm thuần trong file client vẫn là tham chiếu client. Và nó không liên quan tới việc render: Server Component cứ việc render <Dialog> hay <Button>. Bị chặn là động từ gọi, không phải render.
Vì ranh giới cắt theo file, một export có thể chuyển sang phía an toàn chỉ bằng cách dời nó sang một file bên cạnh không có chỉ thị. Mọi module *-constants.ts trong gói tồn tại chính là để làm việc đó.
Hai kiểu hỏng
Vượt ranh giới sai cách thì hỏng theo hai kiểu khác nhau, và phân biệt được chúng là phần lớn những gì cần biết. Gặp kiểu nào tuỳ vào thứ nằm sau ranh giới là một hàm hay một giá trị.
Ồn ào — một hàm
Gọi nó là ném lỗi lúc render, và thông báo nêu đúng tên nó:
Attempted to call buttonVariants() from the server but buttonVariants is on the client.
Phiền, nhưng thẳng thắn. Build dừng lại và cho biết tên cần sửa.
Lặng lẽ — một giá trị
Đọc nó trả về một proxy tham chiếu client, nên mọi thuộc tính đều đọc ra:
undefined
Không lỗi, không cảnh báo, và TypeScript vẫn báo kiểu literal. Đây là kiểu lọt tới production.
Cái bẫy: bảy hàm dựng class ném lỗi
Bảy hàm này là thứ dễ vấp nhất trong gói, chính vì không có gì ở chúng gợi ra một ranh giới. Chúng nhận một object tuỳ chọn và trả về một chuỗi. Chúng không chạm vào state, DOM hay API trình duyệt nào. Vậy mà vẫn hỏng, vì mỗi hàm khai trong cùng file với client component của nó.
| Tên | Khai trong | Từ một Server Component |
|---|---|---|
| buttonVariants | components/ui/button.tsx | Ném lỗi |
| toggleVariants | components/ui/toggle.tsx | Ném lỗi |
| fieldVariants | components/ui/field.tsx | Ném lỗi |
| inputGroupAddonVariants | components/ui/input-group.tsx | Ném lỗi |
| inputGroupButtonVariants | components/ui/input-group.tsx | Ném lỗi |
| progressTrackVariants | components/ui/progress.tsx | Ném lỗi |
| progressBarVariants | components/ui/progress.tsx | Ném lỗi |
// app/pricing/page.tsx — a Server Component. No 'use client' in this file.
import { buttonVariants } from '@comitor/ui'
export default function PricingPage() {
return (
<a href="/get-started" className={buttonVariants({ variant: 'outline' })}>
Get started
</a>
)
}
// Build fails:
// Attempted to call buttonVariants() from the server but buttonVariants is
// on the client.
//
// buttonVariants only concatenates strings. It fails anyway, because it is
// declared in components/ui/button.tsx, and that file starts with 'use client'.Hai lối ra
// The usual fix. A Server Component may RENDER a client component — that is the
// whole point of the boundary. It just may not CALL a function declared in a
// client file. Button takes asChild, so it hands its classes to your own <a>.
import { Button } from '@comitor/ui'
export default function PricingPage() {
return (
<Button asChild variant="outline">
<a href="/get-started">Get started</a>
</Button>
)
}// When you genuinely need the string, move the call across the boundary once.
// components/link-button.tsx
'use client'
import type { ComponentProps } from 'react'
import { buttonVariants, cn } from '@comitor/ui'
type LinkButtonProps = ComponentProps<'a'> & {
variant?: 'default' | 'outline' | 'ghost' | 'link'
size?: 'sm' | 'md' | 'lg'
}
export function LinkButton({ variant, size, className, ...props }: LinkButtonProps) {
return <a className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
// app/pricing/page.tsx — still a Server Component
import { LinkButton } from '@/components/link-button'
export default function PricingPage() {
return <LinkButton href="/get-started" variant="outline">Get started</LinkButton>
}Chín hàm không sao
Biết một hàm dựng class bị kẹt không cho biết gì về hàm kế tiếp. Chín hàm nữa, cộng cn, nằm trong file không có chỉ thị và gọi được từ trang server. Không có quy ước đặt tên nào tách hai danh sách — danh sách chính là đây.
| Tên | Khai trong | Từ một Server Component |
|---|---|---|
| alertVariants | components/ui/alert.tsx | Được |
| badgeVariants | components/ui/badge.tsx | Được |
| buttonGroupVariants | components/ui/button-group.tsx | Được |
| cardVariants | components/ui/card.tsx | Được |
| emptyMediaVariants | components/ui/empty.tsx | Được |
| itemVariants | components/ui/item.tsx | Được |
| itemMediaVariants | components/ui/item.tsx | Được |
| navigationMenuTriggerStyle | components/ui/navigation-menu.tsx | Được |
| statCardIconVariants | components/composite/stat-card.tsx | Được |
| cn | lib/cn.ts | Được |
Tính trên server
Trang này không có "use client". Bốn chuỗi dưới đây được sinh ra bằng cách gọi các helper trong lúc server render route này — chúng được in ra, không phải chép tay.
- badgeVariants({ variant: 'success', size: 'sm' })
- inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none bg-green/10 dark:bg-green/20 text-green-ink px-1.5 py-0.5 text-[10px]
- statCardIconVariants({ tone: 'primary' })
- flex size-7 shrink-0 items-center justify-center rounded-lg bg-primary/20 text-primary-ink
- itemMediaVariants({ variant: 'icon' })
- flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5 size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4
- cn('px-3 py-2 text-sm', 'px-4')
- py-2 text-sm px-4
Kiểu lặng lẽ: dữ liệu nằm sau một file client
Một hằng được re-export qua một file "use client" không bị chặn — nó tới nơi nhưng rỗng ruột. Ranh giới RSC cắt theo file, nên một object dữ liệu đi qua nó trở thành proxy tham chiếu client: mọi thuộc tính đọc ra undefined, mảng báo độ dài bằng không, và không có chẩn đoán nào bật lên ở đâu cả.
// The shape of the quiet bug. Same boundary, no error.
// The constant sits in the right module — and still arrives hollow, because the
// only path from the entry to it crosses a client file.
// chart/comitor-chart.tsx
'use client'
export { CHART_COLORS } from './chart-colors.js' // ← re-exported THROUGH a client file
// app/report/page.tsx — a Server Component
import { CHART_COLORS } from '@comitor/ui/chart'
CHART_COLORS.gold // undefined — not 'var(--chart-1)'
Object.keys(CHART_COLORS) // [] — the object arrives with no keys
// Nothing throws. Nothing warns. TypeScript still reports the literal type, so
// the editor stays green too. The chart renders with every series undefined.// What the package does instead. The constant lives in a directive-free module,
// and the barrel exports it STRAIGHT from there — routing it through the client
// file would put it back behind the boundary.
// shell/contrast-constants.ts — no directive, on purpose
export const CONTRAST_STORAGE_KEY = 'comitor-contrast'
// shell/index.ts
export * from './contrast-constants.js' // not via contrast-provider.tsx
export * from './contrast-provider.js'
// app/layout.tsx — a Server Component
import { CONTRAST_STORAGE_KEY } from '@comitor/ui/shell'
CONTRAST_STORAGE_KEY // 'comitor-contrast'Mọi hằng thuần trong gói nằm trong module không có chỉ thị vì lý do này: *-constants.ts cạnh mỗi client component, chart/chart-colors.ts, shell/types.ts, shell/contrast-constants.ts, shell/display-axis-constants.ts, components/ui/tabs-variants.ts, và từ 3.0.0 là các bảng ngôn ngữ trong i18n/messages.ts và i18n/locale.ts. Mỗi barrel export chúng thẳng từ các file đó. Đây là các giá trị mà một layout hay một trang server thật sự đọc — và cột thứ ba là thứ trang này đọc được, trong lần server render của chính nó:
| Đọc từ một Server Component | Entry | Giá trị |
|---|---|---|
| CHART_COLORS.gold | @comitor/ui/chart | 'var(--chart-1)' |
| CHART_SERIES_COLORS[0] | @comitor/ui/chart | 'var(--chart-1)' |
| DEFAULT_MARKDOWN_EDITOR_LABELS.editorLabel | @comitor/ui/editor | 'Editor' |
| CONTRAST_STORAGE_KEY | @comitor/ui/shell | 'comitor-contrast' |
| DENSITY_STORAGE_KEY | @comitor/ui/shell | 'comitor-density' |
| FONT_SIZE_STORAGE_KEY | @comitor/ui/shell | 'comitor-font-size' |
| CONTRAST_ATTRIBUTE | @comitor/ui/shell | 'data-contrast' |
| DISPLAY_PREF_STORAGE_KEYS.theme | @comitor/ui/shell | 'comitor-theme' |
| KEY_SEQUENCE_TIMEOUT_MS | @comitor/ui | 1200 |
| GANTT_SCALES.join(' · ') | @comitor/ui | 'day · week · month · quarter · year' |
| moveTreeItems([a, b], { ids: ['a'], parentId: 'b', index: 0, kind: 'indent' }).map((i) => `${i.id}→${i.parentId}`) | @comitor/ui | 'b→null · a→b' |
| LEGACY_AVATAR_TONE_ALIASES.navy | @comitor/ui | 'blue' |
| SELECT_EMPTY_VALUE | @comitor/ui | '__empty__' |
| VI_DATE_FORMAT | @comitor/ui | 'dd/MM/yyyy' |
| DEFAULT_PAGE_SIZE_OPTIONS[0] | @comitor/ui | 20 |
| DEFAULT_DATE_PRESETS.length | @comitor/ui | 3 |
| DEFAULT_DATE_RANGE_PRESETS.length | @comitor/ui | 5 |
| DEFAULT_DATA_TABLE_LABELS.emptyTitle | @comitor/ui | 'No data yet' |
| DEFAULT_TABLE_PAGINATION_LABELS.region | @comitor/ui | 'Pagination' |
| DEFAULT_COMITOR_LOCALE | @comitor/ui | 'en' |
| getComitorLanguage('vi-VN') | @comitor/ui | 'vi' |
| comitorMessagesVi.dataTable.emptyTitle | @comitor/ui | 'Chưa có dữ liệu' |
| getComitorMessages('vi').tablePagination.region | @comitor/ui | 'Phân trang' |
Các khoá lưu trữ quan trọng nhất: root layout là một Server Component, và nó render script chống nháy cho các trục tương phản, mật độ và cỡ chữ. Nếu ba khoá đó đọc ra undefined, script ghi vào sai khoá và mọi tuỳ chọn hiển thị lặng lẽ bị quên sau khi tải lại.
Hook và lệnh toast
Không có gì tinh vi ở đây, và cả hai nhóm đều hỏng ồn ào. Cả 26 hook trong gói đều chỉ chạy ở client — từ useShell, useIsMac tới store của các component chuyển từ Dice UI ở 2.1.0 (useRating, useStepper, useTimePicker, usePhoneInput, useCropper, useKeyValueStore, useBanner, useBanners) và hai hook ngôn ngữ của 3.0.0 — và ba lệnh toast kiểu mệnh lệnh, toast, dismissToast và toastPromise, cũng vậy. Gọi từ một Server Component, mỗi thứ ném lỗi có tên của chính nó trong thông báo.
Chúng có mặt vì mục đích của trang này là một bản kiểm kê đầy đủ, chứ không phải vì ai đó trông chờ chúng chạy được khi server render.
Ngôn ngữ: provider là client, các bảng thì không
Từ 3.0.0, mọi chuỗi mặc định của gói đến từ một context, và nó cũng tách theo đúng đường ranh theo file như mọi thứ khác ở đây. ComitorLocaleProvider là một client component, hai hook đọc nó cũng vậy: useComitorLocale và useComitorMessages. Server Component render được provider — root layout làm đúng việc đó, với locale là một chuỗi — nhưng không gọi được các hook.
Các bảng, và các hàm chọn từ chúng, là thuần: comitorMessagesEn, comitorMessagesVi, COMITOR_MESSAGES, getComitorMessages(locale, override?), mergeComitorMessages, getComitorLanguage và DEFAULT_COMITOR_LOCALE. Đây là thứ một lần server render, một route handler hay một mẫu email đọc để sinh chữ theo ngôn ngữ của người dùng khi không có context React nào để hỏi — bốn hàng cuối của bảng hằng phía trên được đọc đúng theo cách đó.
// app/invoices/empty.tsx — a Server Component. No hook and no provider: the string
// tables are plain data, so the server picks the user's language itself.
import { getComitorMessages } from '@comitor/ui'
import { getLocale } from 'next-intl/server'
export default async function NoInvoices() {
const { dataTable } = getComitorMessages(await getLocale())
return <p>{dataTable.emptyTitle}</p> // 'No data yet' · 'Chưa có dữ liệu'
}
// The same call works in a route handler, a Server Action or an email template —
// anywhere text must be in the user's language and there is no React tree to
// hold a ComitorLocaleProvider.Có một luật mới về bản chất: hàm không đi qua được ranh giới. Mọi khoá đặt một con số vào câu — members(count) trong bảng shell, dòng khoảng và dòng trang của phân trang bảng — đều là hàm, vì trật tự từ quanh con số đổi theo ngôn ngữ. Server Component được gọi một hàm như vậy; nó không được truyền hàm đó làm prop cho client component. Nên một override có chứa hàm, ở messages của provider hay ở labels của một component, phải khai trong một file client.
// ✗ app/layout.tsx — a Server Component. `members` is a function, and React refuses
// it at the boundary: "Functions cannot be passed directly to Client Components".
<ComitorLocaleProvider locale={locale} messages={{ shell: { members: (n) => `${n} seats` } }}>
// ✓ app/providers.tsx — the same override, declared on the client side.
'use client'
import { ComitorLocaleProvider, getComitorLanguage } from '@comitor/ui'
import type { ComitorLanguage, ComitorMessagesOverride } from '@comitor/ui'
// Module scope keeps the reference stable; one per language, because an override
// is written in one.
const SEATS: Record<ComitorLanguage, ComitorMessagesOverride> = {
en: { shell: { members: (count) => `${count} ${count === 1 ? 'seat' : 'seats'}` } },
vi: { shell: { members: (count) => `${count} chỗ` } },
}
export function Providers({ locale, children }: { locale: string; children: React.ReactNode }) {
return (
<ComitorLocaleProvider locale={locale} messages={SEATS[getComitorLanguage(locale)]}>
{children}
</ComitorLocaleProvider>
)
}
// …and the root layout passes only a string: <Providers locale={locale}>{children}</Providers>Việc đọc provider đã đẩy ba component sang phía bên kia: Spinner, Breadcrumb và Pagination mang "use client" từ 3.0.0, vì tên truy cập của chúng giờ lấy từ provider. Trang server vẫn render được cả ba; điều duy nhất nó không làm được với client component — truyền cho nó một prop là hàm — không bao giờ xảy ra, vì không cái nào trong ba nhận prop như vậy. Pagination còn được lợi: link của nó gọi buttonVariants, nên hồi file còn chưa có chỉ thị, render PaginationLink thẳng từ trang server dính đúng lỗi ở đầu trang này. RouteTabs cố ý đi hướng ngược lại: nó nhận LinkComponent — một component, tức là một hàm — từ layout server, nên file giữ không chỉ thị và chỉ có <nav> của nó, thứ cần provider để lấy tên, thành một lá client nội bộ. Và cả 18 component chuyển từ Dice UI ở 2.1.0 đều là client component; hook store của chúng nằm trong nhóm Hook của bảng dưới.
Thứ gì an toàn trên trang server
Bản ngắn, trước bảng đầy đủ.
Type
Mọi type và interface được export. Type bị xoá lúc build, nên chúng chưa bao giờ có phần runtime để bị kẹt.
Token
@comitor/ui/tokens trọn vẹn — thang gold, ash và accent, màu ngữ nghĩa và màu trạng thái, layout và typography. tokens.ts không có chỉ thị và sẽ không bao giờ có.
Hằng
Mọi bộ nhãn DEFAULT_* (bảng tiếng Anh, từ 3.0.0), mọi khoá lưu trữ và tên thuộc tính, CHART_COLORS, VI_DATE_FORMAT, SELECT_EMPTY_VALUE, STATUS_TONES, MASK_PATTERNS, các map class của Tabs — và chính các bảng ngôn ngữ, comitorMessagesEn và comitorMessagesVi.
Helper thuần
cn, các tiện ích chữ tiếng Việt, các helper trạng thái, tiện ích nav của shell và của launcher, ba type guard của trục hiển thị, bộ máy che nhập của MaskInput, các hàm định dạng và helper lọc của data-grid — và getComitorMessages, mergeComitorMessages, getComitorLanguage cho chữ theo ngôn ngữ của người dùng.
Chín hàm dựng class
alertVariants, badgeVariants, buttonGroupVariants, cardVariants, emptyMediaVariants, itemVariants, itemMediaVariants, navigationMenuTriggerStyle, statCardIconVariants.
Dấu hiệu thương hiệu và script gieo
ComitorLogo và ComitorLockup là SVG tĩnh không có chỉ thị, nên trang server render chúng mà không gửi kèm JavaScript nào. DisplayPrefsSeedScript (3.7.0) cũng không có chỉ thị, và render thẳng trong root layout.
Mọi tên có thể gọi
Component được bỏ qua: Server Component render được bất kỳ cái nào. Phần dưới là mọi thứ có thể gọi hoặc đọc — dựng từ đồ thị re-export của cả chín entry, nên đây là danh sách đầy đủ chứ không phải một phần chọn lọc.
| Tên | Entry | Server? | Ghi chú |
|---|---|---|---|
Hàm dựng class (cva) Nhóm duy nhất mà câu trả lời thật sự lẫn lộn. Không gì trong tên hay chữ ký phân biệt được hai nửa — chỉ có chỉ thị ở file mà mỗi hàm tình cờ nằm trong. | |||
| buttonVariants | @comitor/ui | Ném lỗi | Nằm cạnh client component Button |
| toggleVariants | @comitor/ui | Ném lỗi | Nằm cạnh Toggle |
| fieldVariants | @comitor/ui | Ném lỗi | Nằm cạnh Field |
| inputGroupAddonVariants | @comitor/ui | Ném lỗi | Nằm cạnh InputGroup |
| inputGroupButtonVariants | @comitor/ui | Ném lỗi | Nằm cạnh InputGroup |
| progressTrackVariants | @comitor/ui | Ném lỗi | Nằm cạnh Progress |
| progressBarVariants | @comitor/ui | Ném lỗi | Nằm cạnh Progress |
| alertVariants | @comitor/ui | Được | alert.tsx không có chỉ thị |
| badgeVariants | @comitor/ui | Được | badge.tsx không có chỉ thị |
| buttonGroupVariants | @comitor/ui | Được | button-group.tsx không có chỉ thị |
| cardVariants | @comitor/ui | Được | card.tsx không có chỉ thị |
| emptyMediaVariants | @comitor/ui | Được | empty.tsx không có chỉ thị |
| itemVariants | @comitor/ui | Được | item.tsx không có chỉ thị |
| itemMediaVariants | @comitor/ui | Được | item.tsx không có chỉ thị |
| navigationMenuTriggerStyle | @comitor/ui | Được | navigation-menu.tsx không có chỉ thị |
| statCardIconVariants | @comitor/ui | Được | stat-card.tsx không có chỉ thị |
Hook Mọi hook trong gói đều chỉ chạy ở client, và hook nào cũng ném lỗi ồn ào. Không có bất ngờ — nhóm này có mặt cho đủ. | |||
| useIsMac | @comitor/ui | Ném lỗi | Đọc navigator sau khi mount |
| useKeySequences | @comitor/ui | Ném lỗi | Bộ nghe chuỗi phím toàn cục — 3.7.0 |
| useCarousel | @comitor/ui | Ném lỗi | Context carousel của Embla |
| useComitorLocale | @comitor/ui | Ném lỗi | Context ngôn ngữ: locale, language, messages, dateFnsLocale |
| useComitorMessages | @comitor/ui | Ném lỗi | Một namespace trong bảng chữ của provider |
| useRating | @comitor/ui | Ném lỗi | Store của Rating — 2.1.0, từ Dice UI |
| useStepper | @comitor/ui | Ném lỗi | Store của Stepper — 2.1.0, từ Dice UI |
| useTimePicker | @comitor/ui | Ném lỗi | Store của TimePicker — 2.1.0, từ Dice UI |
| usePhoneInput | @comitor/ui | Ném lỗi | Store của PhoneInput — 2.1.0, từ Dice UI |
| useCropper | @comitor/ui | Ném lỗi | Store của Cropper — 2.1.0, từ Dice UI |
| useKeyValueStore | @comitor/ui | Ném lỗi | Store của KeyValue — 2.1.0, từ Dice UI |
| useBanner | @comitor/ui | Ném lỗi | Context riêng của một Banner — 2.1.0, từ Dice UI |
| useBanners | @comitor/ui | Ném lỗi | Hàng đợi banner: thêm, gỡ, xoá hết — 2.1.0 |
| useColorPicker | @comitor/ui | Ném lỗi | Store của ColorPicker — 3.15.0, từ Dice UI |
| useShell | @comitor/ui/shell | Ném lỗi | Context của shell |
| useContrast | @comitor/ui/shell | Ném lỗi | Context của trục tương phản |
| useDensity | @comitor/ui/shell | Ném lỗi | Context của trục mật độ |
| useFontSize | @comitor/ui/shell | Ném lỗi | Context của trục cỡ chữ |
| usePalette | @comitor/ui/shell | Ném lỗi | Context của trục bộ màu theme — 3.15.0 |
| useChart | @comitor/ui/chart | Ném lỗi | Context cấu hình của ChartContainer |
| useDataGrid | @comitor/ui/data-grid | Ném lỗi | Instance bảng và các handler của grid |
| useDataGridMessages | @comitor/ui/data-grid | Ném lỗi | Bảng chữ đang áp dụng của grid |
| useDataGridPresence | @comitor/ui/data-grid | Ném lỗi | Ai khác đang ở trên một ô |
| useDataGridUndoRedo | @comitor/ui/data-grid | Ném lỗi | Lịch sử hoàn tác và làm lại |
| useFormField | @comitor/ui/form | Ném lỗi | Context field của react-hook-form |
| useFileUpload | @comitor/ui/uploader | Ném lỗi | Dựng một instance Uppy và nối các sự kiện của nó |
Lệnh toast Các lời gọi trình duyệt kiểu mệnh lệnh. Chúng vốn không bao giờ chạy được từ server render, và chúng nói thẳng điều đó. | |||
| toast | @comitor/ui | Ném lỗi | Lệnh của Sonner |
| dismissToast | @comitor/ui | Ném lỗi | Lệnh của Sonner |
| toastPromise | @comitor/ui | Ném lỗi | Lệnh của Sonner |
Hàm helper Tất cả đều thuần, đều nằm trong module không có chỉ thị, đều gọi được từ Server Component — và từ Server Action, và từ route handler. Có một ngoại lệ, ở cuối nhóm. | |||
| cn | @comitor/ui | Được | clsx + tailwind-merge |
| getComitorMessages | @comitor/ui | Được | Trọn bảng chữ của một locale, cộng một override tuỳ chọn — việc của provider, không cần React |
| mergeComitorMessages | @comitor/ui | Được | Gộp một override vào một bảng, từng namespace một |
| getComitorLanguage | @comitor/ui | Được | Locale → bảng có sẵn: mọi vi-* là vi, còn lại là en |
| dateFormatForLocale | @comitor/ui | Được | Đi cùng VI_DATE_FORMAT |
| toSelectValue | @comitor/ui | Được | Đi cùng SELECT_EMPTY_VALUE |
| fromSelectValue | @comitor/ui | Được | Đi cùng SELECT_EMPTY_VALUE |
| applyMask | @comitor/ui | Được | Bộ máy che nhập của MaskInput — 2.1.0 |
| applyCurrencyMask | @comitor/ui | Được | Bộ máy che nhập của MaskInput |
| applyPercentageMask | @comitor/ui | Được | Bộ máy che nhập của MaskInput |
| getUnmaskedValue | @comitor/ui | Được | Bộ máy che nhập của MaskInput |
| toUnmaskedIndex | @comitor/ui | Được | Ánh xạ vị trí con trỏ của MaskInput |
| fromUnmaskedIndex | @comitor/ui | Được | Ánh xạ vị trí con trỏ của MaskInput |
| getAvatarToneClasses | @comitor/ui | Được | Tông cố định suy từ một cái tên |
| getAvatarToneColors | @comitor/ui · @comitor/ui/tokens | Được | Cặp mã hex cho canvas và PDF |
| getAvatarToneIndex | @comitor/ui · @comitor/ui/tokens | Được | Tông cố định suy từ một cái tên |
| getStatusConfig | @comitor/ui | Được | Tra bảng trạng thái |
| getStatusClasses | @comitor/ui | Được | Tra bảng trạng thái |
| getStatusLabel | @comitor/ui | Được | Tra bảng trạng thái |
| sortByStatusPriority | @comitor/ui | Được | Sắp theo bảng trạng thái |
| getVietnameseInitials | @comitor/ui · @comitor/ui/shell | Được | Cùng một hàm, hai entry |
| normalizeVi | @comitor/ui · @comitor/ui/shell | Được | Cùng một hàm, hai entry |
| removeVietnameseTones | @comitor/ui | Được | Tiện ích chuỗi |
| matchesSearch | @comitor/ui | Được | Điều kiện tìm kiếm không phân biệt dấu |
| truncate | @comitor/ui | Được | Tiện ích chuỗi |
| toNavGroups | @comitor/ui/shell | Được | Chuẩn hoá hình dạng nav |
| flattenNavItems | @comitor/ui/shell | Được | Chuẩn hoá hình dạng nav |
| toSearchParams | @comitor/ui/shell | Được | Nhận object searchParams của một trang server |
| isNavItemActive | @comitor/ui/shell | Được | So khớp route đang mở |
| isNavBranchActive | @comitor/ui/shell | Được | So khớp route đang mở |
| getEntitledApps | @comitor/ui/shell | Được | Lọc app cho launcher |
| getLockedApps | @comitor/ui/shell | Được | Lọc app cho launcher |
| resolveCurrentApp | @comitor/ui/shell | Được | Tra app hiện tại cho launcher |
| appAccentStyle | @comitor/ui/shell | Được | Object style inline cho accent của app |
| isContrastMode | @comitor/ui/shell | Được | Type guard |
| isDensityMode | @comitor/ui/shell | Được | Type guard |
| isFontSizeMode | @comitor/ui/shell | Được | Type guard |
| isThemePalette | @comitor/ui/shell | Được | Type guard — 3.15.0 |
| parseDisplayPrefsSeed | @comitor/ui/shell | Được | Lọc mọi giá trị thành mặc định hiển thị hợp lệ — 3.7.0 |
| advanceKeySequence · normalizeSequenceKey | @comitor/ui | Được | key-sequence.ts, các luật bên dưới useKeySequences — 3.7.0 |
| isEditableTarget | @comitor/ui | Được | Cùng file, import an toàn; nó đọc DOM, nên gọi nó ở trình duyệt |
| moveTreeItems · computeTreeRollup · computeTreeNumbering · groupTreeChildren | @comitor/ui | Được | tree-view-constants.ts — áp một lần di chuyển của TreeView trong Server Action trước khi ghi — 3.12.0 |
| formatDate · formatDateForDisplay | @comitor/ui/data-grid | Được | Nhận một locale date-fns tuỳ chọn; không có thì theo định dạng en-US của date-fns (3.0.0) |
| formatDateToString · parseLocalDate | @comitor/ui/data-grid | Được | Date ⇄ chuỗi yyyy-MM-dd được lưu |
| formatFileSize · generateFileId | @comitor/ui/data-grid | Được | Ô file |
| getCellKey · parseCellKey | @comitor/ui/data-grid | Được | Hàng và cột ⇄ khoá ô |
| getColumnVariant · getEmptyCellValue | @comitor/ui/data-grid | Được | Variant theo ô: icon và nhãn, giá trị rỗng |
| getRowHeightValue · getLineCount | @comitor/ui/data-grid | Được | Chiều cao hàng → pixel, số dòng |
| getDataGridMessages | @comitor/ui/data-grid | Được | Bảng chữ của một ngôn ngữ grid |
| getDefaultOperator · getOperatorsForVariant · getFilterFn | @comitor/ui/data-grid | Được | Toán tử lọc và hàm lọc |
| getDataGridSelectColumn | @comitor/ui/data-grid | Ném lỗi | Ngoại lệ: khai trong một file client. Nó dựng cột checkbox, nên gọi nó ở nơi định nghĩa cột — trong một client component |
Hằng Nhóm sẽ hỏng lặng lẽ nếu đặt sai chỗ. Mỗi hằng ở đây khai trong một module không có chỉ thị và được export thẳng từ barrel, nên server render đọc được giá trị thật. Từ 3.0.0, mọi DEFAULT_*_LABELS giữ bảng tiếng Anh, và không component nào đọc nó nữa — chúng đọc ComitorLocaleProvider. | |||
| comitorMessagesEn | @comitor/ui | Được | i18n/messages.ts — bảng tiếng Anh, định ra mọi hình dạng |
| comitorMessagesVi | @comitor/ui | Được | i18n/messages.ts — bảng tiếng Việt |
| COMITOR_MESSAGES | @comitor/ui | Được | i18n/messages.ts — cả hai, khoá theo ngôn ngữ |
| DEFAULT_COMITOR_LOCALE | @comitor/ui | Được | i18n/locale.ts |
| CHART_COLORS | @comitor/ui/chart | Được | chart/chart-colors.ts |
| CHART_SERIES_COLORS | @comitor/ui/chart | Được | chart/chart-colors.ts |
| CONTRAST_MODES | @comitor/ui/shell | Được | shell/contrast-constants.ts |
| CONTRAST_ATTRIBUTE | @comitor/ui/shell | Được | shell/contrast-constants.ts |
| CONTRAST_STORAGE_KEY | @comitor/ui/shell | Được | Root layout đọc |
| DEFAULT_CONTRAST_TOGGLE_LABELS | @comitor/ui/shell | Được | shell/contrast-constants.ts |
| DENSITY_MODES | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| DENSITY_ATTRIBUTE | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| DENSITY_STORAGE_KEY | @comitor/ui/shell | Được | Root layout đọc |
| DEFAULT_DENSITY | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| FONT_SIZE_MODES | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| FONT_SIZE_ATTRIBUTE | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| FONT_SIZE_STORAGE_KEY | @comitor/ui/shell | Được | Root layout đọc |
| DEFAULT_FONT_SIZE | @comitor/ui/shell | Được | shell/display-axis-constants.ts |
| THEME_PALETTES | @comitor/ui/shell | Được | shell/display-axis-constants.ts — 3.15.0 |
| PALETTE_ATTRIBUTE | @comitor/ui/shell | Được | shell/display-axis-constants.ts — 3.15.0 |
| PALETTE_STORAGE_KEY | @comitor/ui/shell | Được | Root layout đọc — 3.15.0 |
| DEFAULT_PALETTE | @comitor/ui/shell | Được | shell/display-axis-constants.ts — 3.15.0 |
| DEFAULT_SHELL_LABELS | @comitor/ui/shell | Được | shell/types.ts |
| DEFAULT_COMMAND_PALETTE_LABELS | @comitor/ui | Được | command-palette-constants.ts |
| DEFAULT_DATA_TABLE_LABELS | @comitor/ui | Được | data-table-constants.ts |
| DEFAULT_DATE_PICKER_LABELS | @comitor/ui | Được | date-picker-constants.ts |
| DEFAULT_DATE_PRESETS | @comitor/ui | Được | date-picker-constants.ts |
| DEFAULT_DATE_RANGE_PRESETS | @comitor/ui | Được | date-picker-constants.ts |
| VI_DATE_FORMAT | @comitor/ui | Được | date-picker-constants.ts |
| DEFAULT_SEARCH_FILTER_BAR_LABELS | @comitor/ui | Được | search-filter-bar-constants.tsx |
| DEFAULT_TABLE_PAGINATION_LABELS | @comitor/ui | Được | table-pagination-constants.ts |
| DEFAULT_PAGE_SIZE_OPTIONS | @comitor/ui | Được | table-pagination-constants.ts |
| SELECT_EMPTY_VALUE | @comitor/ui | Được | form-field-constants.ts |
| DEFAULT_ACTION_BAR_LABELS | @comitor/ui | Được | action-bar-constants.ts |
| DEFAULT_CROPPER_LABELS | @comitor/ui | Được | cropper-constants.ts |
| DEFAULT_KANBAN_LABELS | @comitor/ui | Được | kanban-constants.ts |
| DEFAULT_KEY_VALUE_LABELS | @comitor/ui | Được | key-value-constants.ts |
| DEFAULT_PHONE_INPUT_LABELS | @comitor/ui | Được | phone-input-constants.ts |
| DEFAULT_STEPPER_LABELS | @comitor/ui | Được | stepper-constants.ts |
| DEFAULT_TIME_PICKER_LABELS | @comitor/ui | Được | time-picker-constants.ts |
| DEFAULT_TOUR_LABELS | @comitor/ui | Được | tour-constants.ts |
| MASK_PATTERNS | @comitor/ui | Được | mask-input-constants.ts |
| KEY_SEQUENCE_TIMEOUT_MS | @comitor/ui | Được | key-sequence.ts — 3.7.0 |
| DISPLAY_PREF_VALUES · DISPLAY_PREF_STORAGE_KEYS | @comitor/ui/shell | Được | shell/display-prefs-seed.tsx — 3.7.0 |
| DEFAULT_FILE_UPLOAD_LABELS | @comitor/ui/uploader | Được | uploader/file-upload-constants.ts |
| DEFAULT_IMAGE_UPLOAD_FIELD_LABELS | @comitor/ui/uploader | Được | Cùng file. Barrel import nó từ đó, không qua field client |
| DEFAULT_MARKDOWN_EDITOR_LABELS | @comitor/ui/editor | Được | editor/markdown-editor-constants.ts — không import gói @tiptap nào, nên đọc nó không kéo editor vào |
| TEXT_ · NUMBER_ · DATE_ · SELECT_ · BOOLEAN_FILTER_OPERATORS | @comitor/ui/data-grid | Được | data-grid/filters.ts |
| DATA_GRID_MESSAGES · DEFAULT_DATA_GRID_LOCALE | @comitor/ui/data-grid | Được | data-grid/messages.ts |
| dataGridMessages · dataGridMessagesEn | @comitor/ui/data-grid | Được | data-grid/messages-en.ts |
| dataGridMessagesVi | @comitor/ui/data-grid | Được | data-grid/messages-vi.ts |
| STATUS_TONES | @comitor/ui | Được | status-config.ts |
| AVATAR_TONE_NAMES | @comitor/ui | Được | lib/avatar-tone.ts |
| LEGACY_AVATAR_TONE_ALIASES | @comitor/ui | Được | lib/avatar-tone.ts — navy, ash, neutral → blue, purple, pink; được export từ 3.12.1 |
| GANTT_SCALES | @comitor/ui | Được | gantt-constants.ts — 3.12.0 |
| tabsListVariants | @comitor/ui | Được | tabs-variants.ts — một map thường, không phải cva |
| tabsTriggerVariants | @comitor/ui | Được | tabs-variants.ts — một map thường, không phải cva |
| TABS_TRIGGER_BASE | @comitor/ui | Được | tabs-variants.ts |
| gold · themePalettes · brandNeutral · ash · accent · accentInk · accentForeground · semantic · highContrast · status · chart · layout · typography | @comitor/ui · @comitor/ui/tokens | Được | Toàn bộ bản sao token. tokens.ts không có chỉ thị và sẽ không bao giờ có |
Dấu hiệu thương hiệu Cả hai cố ý không có chỉ thị để trang server render SVG tĩnh và không gửi JavaScript nào cho chúng. | |||
| ComitorLogo | @comitor/ui/shell | Được | SVG tĩnh, không có chỉ thị |
| ComitorLockup | @comitor/ui/shell | Được | SVG tĩnh, không có chỉ thị |
Script gieo Không có chỉ thị vì cùng lý do: nó thuộc về root layout, vốn là một Server Component. | |||
| DisplayPrefsSeedScript | @comitor/ui/shell | Được | Script inline gieo mặc định hiển thị của tài khoản trước lần vẽ đầu tiên — 3.7.0 |
Danh sách giữ đúng bằng cách nào
Lỗi ranh giới lặng lẽ thì review không bắt được, nên gói chặn nó trước khi phát hành. Ba bước kiểm chạy ở mỗi bản phát hành.
Chỉ thị sống sót qua build
Mọi file nguồn bắt đầu bằng "use client" phải sinh ra một file dist cũng bắt đầu bằng nó. Mất chỉ thị thì một client component trông như một server component, và lỗi chỉ lộ ra ở app dùng gói sau khi phát hành.
Không hằng thuần nào nằm trong file client
Một giá trị dữ liệu khai trong file "use client" bị chặn ngay. Bước kiểm nhận ra dữ liệu qua biểu thức khởi tạo — literal object, mảng, chuỗi, số hoặc boolean — nên component viết dạng cva(…) hay forwardRef(…) được miễn: khởi tạo của chúng là một lời gọi, không phải dữ liệu. Cũng vì luật này mà từ 3.0.0 mọi DEFAULT_*_LABELS được viết { ...xEn } chứ không phải = xEn: khi gán thẳng từ một identifier, số hằng bước kiểm thấy được rơi từ 58 xuống 47, mà không có gì đỏ lên.
Không hằng nào tới entry qua một file client
Bước chặt hơn. Một hằng có thể nằm đúng trong module thuần của nó mà vẫn bị dẫn tới barrel qua một re-export ở file client, và thế là tái tạo đúng lỗi kia. Bước kiểm đi theo đồ thị re-export từ cả chín entry và chặn mọi tên mà đường duy nhất tới nó đi qua một file client.
Bước 3 tồn tại vì bước 2 không đủ, và điều đó đã được đo chứ không phải đoán. Đặt CHART_COLORS vào module thuần riêng — tự nó là đúng, nên bước 1 và 2 vẫn xanh — nhưng để barrel đi tới nó qua file chart client, thì một lần build Next thật lại đưa cho trang server một object không có khoá nào.