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ênKhai trongTừ một Server Component
buttonVariantscomponents/ui/button.tsxNém lỗi
toggleVariantscomponents/ui/toggle.tsxNém lỗi
fieldVariantscomponents/ui/field.tsxNém lỗi
inputGroupAddonVariantscomponents/ui/input-group.tsxNém lỗi
inputGroupButtonVariantscomponents/ui/input-group.tsxNém lỗi
progressTrackVariantscomponents/ui/progress.tsxNém lỗi
progressBarVariantscomponents/ui/progress.tsxNém lỗi
app/pricing/page.tsx
// 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

asChild
// 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>
  )
}
wrapper client
// 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ênKhai trongTừ một Server Component
alertVariantscomponents/ui/alert.tsxĐược
badgeVariantscomponents/ui/badge.tsxĐược
buttonGroupVariantscomponents/ui/button-group.tsxĐược
cardVariantscomponents/ui/card.tsxĐược
emptyMediaVariantscomponents/ui/empty.tsxĐược
itemVariantscomponents/ui/item.tsxĐược
itemMediaVariantscomponents/ui/item.tsxĐược
navigationMenuTriggerStylecomponents/ui/navigation-menu.tsxĐược
statCardIconVariantscomponents/composite/stat-card.tsxĐược
cnlib/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ả.

lỗi
// 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.
cách sửa
// 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 ComponentEntryGiá 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/ui1200
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/ui20
DEFAULT_DATE_PRESETS.length@comitor/ui3
DEFAULT_DATE_RANGE_PRESETS.length@comitor/ui5
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 đó.

Server Component
// 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.

khoá là hàm
// ✗ 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ênEntryServer?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/uiNém lỗiNằm cạnh client component Button
toggleVariants@comitor/uiNém lỗiNằm cạnh Toggle
fieldVariants@comitor/uiNém lỗiNằm cạnh Field
inputGroupAddonVariants@comitor/uiNém lỗiNằm cạnh InputGroup
inputGroupButtonVariants@comitor/uiNém lỗiNằm cạnh InputGroup
progressTrackVariants@comitor/uiNém lỗiNằm cạnh Progress
progressBarVariants@comitor/uiNém lỗiNằm cạnh Progress
alertVariants@comitor/uiĐượcalert.tsx không có chỉ thị
badgeVariants@comitor/uiĐượcbadge.tsx không có chỉ thị
buttonGroupVariants@comitor/uiĐượcbutton-group.tsx không có chỉ thị
cardVariants@comitor/uiĐượccard.tsx không có chỉ thị
emptyMediaVariants@comitor/uiĐượcempty.tsx không có chỉ thị
itemVariants@comitor/uiĐượcitem.tsx không có chỉ thị
itemMediaVariants@comitor/uiĐượcitem.tsx không có chỉ thị
navigationMenuTriggerStyle@comitor/uiĐượcnavigation-menu.tsx không có chỉ thị
statCardIconVariants@comitor/uiĐượcstat-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/uiNém lỗiĐọc navigator sau khi mount
useKeySequences@comitor/uiNém lỗiBộ nghe chuỗi phím toàn cục — 3.7.0
useCarousel@comitor/uiNém lỗiContext carousel của Embla
useComitorLocale@comitor/uiNém lỗiContext ngôn ngữ: locale, language, messages, dateFnsLocale
useComitorMessages@comitor/uiNém lỗiMột namespace trong bảng chữ của provider
useRating@comitor/uiNém lỗiStore của Rating — 2.1.0, từ Dice UI
useStepper@comitor/uiNém lỗiStore của Stepper — 2.1.0, từ Dice UI
useTimePicker@comitor/uiNém lỗiStore của TimePicker — 2.1.0, từ Dice UI
usePhoneInput@comitor/uiNém lỗiStore của PhoneInput — 2.1.0, từ Dice UI
useCropper@comitor/uiNém lỗiStore của Cropper — 2.1.0, từ Dice UI
useKeyValueStore@comitor/uiNém lỗiStore của KeyValue — 2.1.0, từ Dice UI
useBanner@comitor/uiNém lỗiContext riêng của một Banner — 2.1.0, từ Dice UI
useBanners@comitor/uiNém lỗiHàng đợi banner: thêm, gỡ, xoá hết — 2.1.0
useColorPicker@comitor/uiNém lỗiStore của ColorPicker — 3.15.0, từ Dice UI
useShell@comitor/ui/shellNém lỗiContext của shell
useContrast@comitor/ui/shellNém lỗiContext của trục tương phản
useDensity@comitor/ui/shellNém lỗiContext của trục mật độ
useFontSize@comitor/ui/shellNém lỗiContext của trục cỡ chữ
usePalette@comitor/ui/shellNém lỗiContext của trục bộ màu theme — 3.15.0
useChart@comitor/ui/chartNém lỗiContext cấu hình của ChartContainer
useDataGrid@comitor/ui/data-gridNém lỗiInstance bảng và các handler của grid
useDataGridMessages@comitor/ui/data-gridNém lỗiBảng chữ đang áp dụng của grid
useDataGridPresence@comitor/ui/data-gridNém lỗiAi khác đang ở trên một ô
useDataGridUndoRedo@comitor/ui/data-gridNém lỗiLịch sử hoàn tác và làm lại
useFormField@comitor/ui/formNém lỗiContext field của react-hook-form
useFileUpload@comitor/ui/uploaderNém lỗiDự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/uiNém lỗiLệnh của Sonner
dismissToast@comitor/uiNém lỗiLệnh của Sonner
toastPromise@comitor/uiNém lỗiLệ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Đượcclsx + tailwind-merge
getComitorMessages@comitor/uiĐượcTrọ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ĐượcGộp một override vào một bảng, từng namespace một
getComitorLanguage@comitor/uiĐượcLocale → 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ĐượcBộ máy che nhập của MaskInput — 2.1.0
applyCurrencyMask@comitor/uiĐượcBộ máy che nhập của MaskInput
applyPercentageMask@comitor/uiĐượcBộ máy che nhập của MaskInput
getUnmaskedValue@comitor/uiĐượcBộ 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ĐượcTông cố định suy từ một cái tên
getAvatarToneColors@comitor/ui · @comitor/ui/tokensĐượcCặp mã hex cho canvas và PDF
getAvatarToneIndex@comitor/ui · @comitor/ui/tokensĐượcTông cố định suy từ một cái tên
getStatusConfig@comitor/uiĐượcTra bảng trạng thái
getStatusClasses@comitor/uiĐượcTra bảng trạng thái
getStatusLabel@comitor/uiĐượcTra bảng trạng thái
sortByStatusPriority@comitor/uiĐượcSắp theo bảng trạng thái
getVietnameseInitials@comitor/ui · @comitor/ui/shellĐượcCùng một hàm, hai entry
normalizeVi@comitor/ui · @comitor/ui/shellĐượcCùng một hàm, hai entry
removeVietnameseTones@comitor/uiĐượcTiệ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ĐượcTiện ích chuỗi
toNavGroups@comitor/ui/shellĐượcChuẩn hoá hình dạng nav
flattenNavItems@comitor/ui/shellĐượcChuẩn hoá hình dạng nav
toSearchParams@comitor/ui/shellĐượcNhận object searchParams của một trang server
isNavItemActive@comitor/ui/shellĐượcSo khớp route đang mở
isNavBranchActive@comitor/ui/shellĐượcSo khớp route đang mở
getEntitledApps@comitor/ui/shellĐượcLọc app cho launcher
getLockedApps@comitor/ui/shellĐượcLọc app cho launcher
resolveCurrentApp@comitor/ui/shellĐượcTra app hiện tại cho launcher
appAccentStyle@comitor/ui/shellĐượcObject style inline cho accent của app
isContrastMode@comitor/ui/shellĐượcType guard
isDensityMode@comitor/ui/shellĐượcType guard
isFontSizeMode@comitor/ui/shellĐượcType guard
isThemePalette@comitor/ui/shellĐượcType guard — 3.15.0
parseDisplayPrefsSeed@comitor/ui/shellĐượcLọc mọi giá trị thành mặc định hiển thị hợp lệ — 3.7.0
advanceKeySequence · normalizeSequenceKey@comitor/uiĐượckey-sequence.ts, các luật bên dưới useKeySequences — 3.7.0
isEditableTarget@comitor/uiĐượcCùng file, import an toàn; nó đọc DOM, nên gọi nó ở trình duyệt
moveTreeItems · computeTreeRollup · computeTreeNumbering · groupTreeChildren@comitor/uiĐượctree-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ĐượcNhậ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ĐượcDate ⇄ chuỗi yyyy-MM-dd được lưu
formatFileSize · generateFileId@comitor/ui/data-gridĐượcÔ file
getCellKey · parseCellKey@comitor/ui/data-gridĐượcHàng và cột ⇄ khoá ô
getColumnVariant · getEmptyCellValue@comitor/ui/data-gridĐượcVariant theo ô: icon và nhãn, giá trị rỗng
getRowHeightValue · getLineCount@comitor/ui/data-gridĐượcChiều cao hàng → pixel, số dòng
getDataGridMessages@comitor/ui/data-gridĐượcBảng chữ của một ngôn ngữ grid
getDefaultOperator · getOperatorsForVariant · getFilterFn@comitor/ui/data-gridĐượcToán tử lọc và hàm lọc
getDataGridSelectColumn@comitor/ui/data-gridNém lỗiNgoạ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Đượci18n/messages.ts — bảng tiếng Anh, định ra mọi hình dạng
comitorMessagesVi@comitor/uiĐượci18n/messages.ts — bảng tiếng Việt
COMITOR_MESSAGES@comitor/uiĐượci18n/messages.ts — cả hai, khoá theo ngôn ngữ
DEFAULT_COMITOR_LOCALE@comitor/uiĐượci18n/locale.ts
CHART_COLORS@comitor/ui/chartĐượcchart/chart-colors.ts
CHART_SERIES_COLORS@comitor/ui/chartĐượcchart/chart-colors.ts
CONTRAST_MODES@comitor/ui/shellĐượcshell/contrast-constants.ts
CONTRAST_ATTRIBUTE@comitor/ui/shellĐượcshell/contrast-constants.ts
CONTRAST_STORAGE_KEY@comitor/ui/shellĐượcRoot layout đọc
DEFAULT_CONTRAST_TOGGLE_LABELS@comitor/ui/shellĐượcshell/contrast-constants.ts
DENSITY_MODES@comitor/ui/shellĐượcshell/display-axis-constants.ts
DENSITY_ATTRIBUTE@comitor/ui/shellĐượcshell/display-axis-constants.ts
DENSITY_STORAGE_KEY@comitor/ui/shellĐượcRoot layout đọc
DEFAULT_DENSITY@comitor/ui/shellĐượcshell/display-axis-constants.ts
FONT_SIZE_MODES@comitor/ui/shellĐượcshell/display-axis-constants.ts
FONT_SIZE_ATTRIBUTE@comitor/ui/shellĐượcshell/display-axis-constants.ts
FONT_SIZE_STORAGE_KEY@comitor/ui/shellĐượcRoot layout đọc
DEFAULT_FONT_SIZE@comitor/ui/shellĐượcshell/display-axis-constants.ts
THEME_PALETTES@comitor/ui/shellĐượcshell/display-axis-constants.ts — 3.15.0
PALETTE_ATTRIBUTE@comitor/ui/shellĐượcshell/display-axis-constants.ts — 3.15.0
PALETTE_STORAGE_KEY@comitor/ui/shellĐượcRoot layout đọc — 3.15.0
DEFAULT_PALETTE@comitor/ui/shellĐượcshell/display-axis-constants.ts — 3.15.0
DEFAULT_SHELL_LABELS@comitor/ui/shellĐượcshell/types.ts
DEFAULT_COMMAND_PALETTE_LABELS@comitor/uiĐượccommand-palette-constants.ts
DEFAULT_DATA_TABLE_LABELS@comitor/uiĐượcdata-table-constants.ts
DEFAULT_DATE_PICKER_LABELS@comitor/uiĐượcdate-picker-constants.ts
DEFAULT_DATE_PRESETS@comitor/uiĐượcdate-picker-constants.ts
DEFAULT_DATE_RANGE_PRESETS@comitor/uiĐượcdate-picker-constants.ts
VI_DATE_FORMAT@comitor/uiĐượcdate-picker-constants.ts
DEFAULT_SEARCH_FILTER_BAR_LABELS@comitor/uiĐượcsearch-filter-bar-constants.tsx
DEFAULT_TABLE_PAGINATION_LABELS@comitor/uiĐượctable-pagination-constants.ts
DEFAULT_PAGE_SIZE_OPTIONS@comitor/uiĐượctable-pagination-constants.ts
SELECT_EMPTY_VALUE@comitor/uiĐượcform-field-constants.ts
DEFAULT_ACTION_BAR_LABELS@comitor/uiĐượcaction-bar-constants.ts
DEFAULT_CROPPER_LABELS@comitor/uiĐượccropper-constants.ts
DEFAULT_KANBAN_LABELS@comitor/uiĐượckanban-constants.ts
DEFAULT_KEY_VALUE_LABELS@comitor/uiĐượckey-value-constants.ts
DEFAULT_PHONE_INPUT_LABELS@comitor/uiĐượcphone-input-constants.ts
DEFAULT_STEPPER_LABELS@comitor/uiĐượcstepper-constants.ts
DEFAULT_TIME_PICKER_LABELS@comitor/uiĐượctime-picker-constants.ts
DEFAULT_TOUR_LABELS@comitor/uiĐượctour-constants.ts
MASK_PATTERNS@comitor/uiĐượcmask-input-constants.ts
KEY_SEQUENCE_TIMEOUT_MS@comitor/uiĐượckey-sequence.ts — 3.7.0
DISPLAY_PREF_VALUES · DISPLAY_PREF_STORAGE_KEYS@comitor/ui/shellĐượcshell/display-prefs-seed.tsx — 3.7.0
DEFAULT_FILE_UPLOAD_LABELS@comitor/ui/uploaderĐượcuploader/file-upload-constants.ts
DEFAULT_IMAGE_UPLOAD_FIELD_LABELS@comitor/ui/uploaderĐượcCùng file. Barrel import nó từ đó, không qua field client
DEFAULT_MARKDOWN_EDITOR_LABELS@comitor/ui/editorĐượceditor/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Đượcdata-grid/filters.ts
DATA_GRID_MESSAGES · DEFAULT_DATA_GRID_LOCALE@comitor/ui/data-gridĐượcdata-grid/messages.ts
dataGridMessages · dataGridMessagesEn@comitor/ui/data-gridĐượcdata-grid/messages-en.ts
dataGridMessagesVi@comitor/ui/data-gridĐượcdata-grid/messages-vi.ts
STATUS_TONES@comitor/uiĐượcstatus-config.ts
AVATAR_TONE_NAMES@comitor/uiĐượclib/avatar-tone.ts
LEGACY_AVATAR_TONE_ALIASES@comitor/uiĐượclib/avatar-tone.ts — navy, ash, neutral → blue, purple, pink; được export từ 3.12.1
GANTT_SCALES@comitor/uiĐượcgantt-constants.ts — 3.12.0
tabsListVariants@comitor/uiĐượctabs-variants.ts — một map thường, không phải cva
tabsTriggerVariants@comitor/uiĐượctabs-variants.ts — một map thường, không phải cva
TABS_TRIGGER_BASE@comitor/uiĐượctabs-variants.ts
gold · themePalettes · brandNeutral · ash · accent · accentInk · accentForeground · semantic · highContrast · status · chart · layout · typography@comitor/ui · @comitor/ui/tokensĐượcToà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ĐượcSVG tĩnh, không có chỉ thị
ComitorLockup@comitor/ui/shellĐượcSVG 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ĐượcScript 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.

1

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.

2

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.

3

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.