Server and client

@comitor/ui is built one module per source file, so each file keeps its own "use client" directive. That makes "can a React Server Component call this?" a real question with a per-name answer — and it is not the answer most people guess. Seven functions that do nothing but concatenate class strings throw when a server page calls them.

This page is the complete inventory. It is also itself a Server Component, and the class strings and constant values further down were computed by it at build time.

Where the line is drawn

The package builds with tsup at bundle: false: every source file is transpiled to its own ESM module rather than merged into a few chunks. That is not a tree-shaking nicety, it is the reason the boundary works at all. Bundled output would push a "use client" line into the middle of a chunk, where it means nothing, and drag static primitives to the client with it.

The rule

A file's exports inherit that file's directive. Anything exported from a "use client" file reaches the server as a client reference — a component, a hook, a class builder, a string constant, all the same.

What it is not

It is not about what the code does. A pure function in a client file is still a client reference. And it is not about rendering: a Server Component may freely render <Dialog> or <Button>. Calling is the blocked verb, not rendering.

Because the cut is per file, one export can be moved to the safe side simply by moving it to a neighbouring file with no directive. That is exactly what every *-constants.ts module in the package is for.

Two failure modes

Crossing the boundary wrongly fails in two different ways, and telling them apart is most of what you need to know. Which one you get depends on whether the thing behind the boundary is a function or a value.

Loud — a function

Calling it throws at render, and names itself in the message:

Attempted to call buttonVariants() from the server but buttonVariants is on the client.

Annoying, but honest. The build stops and tells you the name to fix.

Silent — a value

Reading it gives you a client-reference proxy, so every property reads:

undefined

No error, no warning, and TypeScript still reports the literal type. This is the one that reaches production.

The trap: seven class builders that throw

These seven are the easiest thing in the package to trip over, precisely because nothing about them suggests a boundary. They take an options object and return a string. They touch no state, no DOM, no browser API. They fail anyway, because each one is declared in the same file as its client component.

NameDeclared inFrom a Server Component
buttonVariantscomponents/ui/button.tsxThrows
toggleVariantscomponents/ui/toggle.tsxThrows
fieldVariantscomponents/ui/field.tsxThrows
inputGroupAddonVariantscomponents/ui/input-group.tsxThrows
inputGroupButtonVariantscomponents/ui/input-group.tsxThrows
progressTrackVariantscomponents/ui/progress.tsxThrows
progressBarVariantscomponents/ui/progress.tsxThrows
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'.

Two ways out

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>
  )
}
client wrapper
// 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>
}

The nine that are fine

Learning that one class builder is stuck tells you nothing about the next one. Nine more, plus cn, live in files that carry no directive and are callable from a server page. There is no naming convention separating the two lists — this is the list.

NameDeclared inFrom a Server Component
alertVariantscomponents/ui/alert.tsxYes
badgeVariantscomponents/ui/badge.tsxYes
buttonGroupVariantscomponents/ui/button-group.tsxYes
cardVariantscomponents/ui/card.tsxYes
emptyMediaVariantscomponents/ui/empty.tsxYes
itemVariantscomponents/ui/item.tsxYes
itemMediaVariantscomponents/ui/item.tsxYes
navigationMenuTriggerStylecomponents/ui/navigation-menu.tsxYes
statCardIconVariantscomponents/composite/stat-card.tsxYes
cnlib/cn.tsYes

Computed on the server

This page has no "use client". The four strings below were produced by calling the helpers during the server render of this route — they are printed, not transcribed.

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

The quiet one: data behind a client file

A constant re-exported through a "use client" file is not blocked — it simply arrives hollow. The RSC boundary cuts by file, so a data object crossing it becomes a client-reference proxy: every property reads undefined, an array reports a length of zero, and no diagnostic fires anywhere.

the bug
// 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.
the fix
// 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'

Every pure constant in the package lives in a directive-free module for this reason: *-constants.ts beside each client component, chart/chart-colors.ts, shell/types.ts, shell/contrast-constants.ts, shell/display-axis-constants.ts, components/ui/tabs-variants.ts, and since 3.0.0 the language tables in i18n/messages.ts and i18n/locale.ts. Each barrel exports them straight from those files. These are the values a server layout or a server page actually reads — and the third column is what this page read, during its own server render:

Read from a Server ComponentEntryValue
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'

The storage keys matter most: a root layout is a Server Component, and it renders the anti-flash script for the contrast, density and text-size axes. If those three keys read undefined, the script writes to the wrong key and every display preference is silently forgotten on reload.

Hooks and toast commands

Nothing subtle here, and both groups fail loudly. All 26 hooks in the package are client-only — useShell and useIsMac as much as the stores of the components ported from Dice UI in 2.1.0 (useRating, useStepper, useTimePicker, usePhoneInput, useCropper, useKeyValueStore, useBanner, useBanners) and the two language hooks of 3.0.0 — as are the three imperative toast calls, toast, dismissToast and toastPromise. Called from a Server Component, each one throws with its own name in the message.

They are listed because a complete inventory is the point of this page, not because anybody expects them to work on a server render.

Language: the provider is client, the tables are not

Since 3.0.0 every default string in the package comes from one context, and it splits along the same per-file line as everything else here. ComitorLocaleProvider is a client component, and so are the two hooks that read it, useComitorLocale and useComitorMessages. A Server Component renders the provider — that is exactly what a root layout does, with a string locale — but cannot call the hooks.

The tables, and the functions that pick from them, are pure: comitorMessagesEn, comitorMessagesVi, COMITOR_MESSAGES, getComitorMessages(locale, override?), mergeComitorMessages, getComitorLanguage and DEFAULT_COMITOR_LOCALE. They are what a server render, a route handler or an email template reads to produce text in the user’s language with no React context to ask — the last four rows of the constants table above were read exactly that way.

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.

One rule is new in kind: a function cannot cross the boundary. Every key that puts a number into a sentence — members(count) in the shell table, the range and page lines of table pagination — is a function, because word order around a number changes with the language. A Server Component may call one; it may not pass one as a prop to a client component. So an override that contains a function, on the provider’s messages or on a component’s labels, is declared in a client file.

function-valued keys
// ✗ 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>

Reading the provider moved three components across the line: Spinner, Breadcrumb and Pagination carry "use client" since 3.0.0, because their accessible names now come from it. A server page still renders all three; the one thing it cannot do with a client component — hand it a function prop — never applies, since none of the three takes one. Pagination even gained: its links call buttonVariants, so while the file was directive-free, rendering PaginationLink straight from a server page hit the very error at the top of this page. RouteTabs went the other way on purpose: it takes LinkComponent — a component, so a function — from server layouts, so the file stays directive-free and only its <nav>, which needs the provider for its name, became an internal client leaf. And all 18 components ported from Dice UI in 2.1.0 are client components; their store hooks are in the Hooks group of the table below.

What is safe on a server page

The short version, before the full table.

Types

Every exported type and interface. Types are erased at build, so they never had a runtime side to be stuck on.

Tokens

@comitor/ui/tokens in full — the gold, ash and accent scales, semantic and status colours, layout and typography. tokens.ts has no directive and never will.

Constants

Every DEFAULT_* label set (the English tables, since 3.0.0), every storage key and attribute name, CHART_COLORS, VI_DATE_FORMAT, SELECT_EMPTY_VALUE, STATUS_TONES, MASK_PATTERNS, the Tabs class maps — and the language tables themselves, comitorMessagesEn and comitorMessagesVi.

Pure helpers

cn, the Vietnamese text utilities, the status helpers, the shell nav and app-launcher utilities, the three display-axis type guards, MaskInput’s masking engine, the data-grid formatters and filter helpers — and getComitorMessages, mergeComitorMessages and getComitorLanguage, for text in the user’s language.

Nine class builders

alertVariants, badgeVariants, buttonGroupVariants, cardVariants, emptyMediaVariants, itemVariants, itemMediaVariants, navigationMenuTriggerStyle, statCardIconVariants.

The brand marks and the seed script

ComitorLogo and ComitorLockup are static SVG with no directive, so a server page renders them without shipping any JavaScript. DisplayPrefsSeedScript (3.7.0) is directive-free too, and renders straight in the root layout.

Every callable name

Components are omitted: a Server Component can render any of them. What follows is everything you might call or read — built from the re-export graph of all nine entries, so it is the complete list rather than a selection.

NameEntryServer?Note

Class builders (cva)

The one group where the answer is genuinely mixed. Nothing in the name or the signature tells the two halves apart — only the directive on the file each one happens to live in.

buttonVariants@comitor/uiThrowsShips beside the Button client component
toggleVariants@comitor/uiThrowsShips beside Toggle
fieldVariants@comitor/uiThrowsShips beside Field
inputGroupAddonVariants@comitor/uiThrowsShips beside InputGroup
inputGroupButtonVariants@comitor/uiThrowsShips beside InputGroup
progressTrackVariants@comitor/uiThrowsShips beside Progress
progressBarVariants@comitor/uiThrowsShips beside Progress
alertVariants@comitor/uiYesalert.tsx carries no directive
badgeVariants@comitor/uiYesbadge.tsx carries no directive
buttonGroupVariants@comitor/uiYesbutton-group.tsx carries no directive
cardVariants@comitor/uiYescard.tsx carries no directive
emptyMediaVariants@comitor/uiYesempty.tsx carries no directive
itemVariants@comitor/uiYesitem.tsx carries no directive
itemMediaVariants@comitor/uiYesitem.tsx carries no directive
navigationMenuTriggerStyle@comitor/uiYesnavigation-menu.tsx carries no directive
statCardIconVariants@comitor/uiYesstat-card.tsx carries no directive

Hooks

Every hook in the package is client-only, and every one of them throws loudly. No surprises here — this group is listed for completeness.

useIsMac@comitor/uiThrowsReads navigator after mount
useKeySequences@comitor/uiThrowsGlobal key-sequence listener — 3.7.0
useCarousel@comitor/uiThrowsEmbla carousel context
useComitorLocale@comitor/uiThrowsThe language context: locale, language, messages, dateFnsLocale
useComitorMessages@comitor/uiThrowsOne namespace of the provider’s string table
useRating@comitor/uiThrowsRating’s store — 2.1.0, from Dice UI
useStepper@comitor/uiThrowsStepper’s store — 2.1.0, from Dice UI
useTimePicker@comitor/uiThrowsTimePicker’s store — 2.1.0, from Dice UI
usePhoneInput@comitor/uiThrowsPhoneInput’s store — 2.1.0, from Dice UI
useCropper@comitor/uiThrowsCropper’s store — 2.1.0, from Dice UI
useKeyValueStore@comitor/uiThrowsKeyValue’s store — 2.1.0, from Dice UI
useBanner@comitor/uiThrowsOne Banner’s own context — 2.1.0, from Dice UI
useBanners@comitor/uiThrowsThe banner queue: add, remove, clear — 2.1.0
useColorPicker@comitor/uiThrowsColorPicker’s store — 3.15.0, from Dice UI
useShell@comitor/ui/shellThrowsShell context
useContrast@comitor/ui/shellThrowsContrast axis context
useDensity@comitor/ui/shellThrowsDensity axis context
useFontSize@comitor/ui/shellThrowsText-size axis context
usePalette@comitor/ui/shellThrowsTheme-palette axis context — 3.15.0
useChart@comitor/ui/chartThrowsChartContainer config context
useDataGrid@comitor/ui/data-gridThrowsThe grid’s table instance and handlers
useDataGridMessages@comitor/ui/data-gridThrowsThe grid’s string table in force
useDataGridPresence@comitor/ui/data-gridThrowsWho else is on a cell
useDataGridUndoRedo@comitor/ui/data-gridThrowsUndo and redo history
useFormField@comitor/ui/formThrowsreact-hook-form field context
useFileUpload@comitor/ui/uploaderThrowsBuilds an Uppy instance and wires its events

Toast commands

Imperative browser calls. They were never going to work from a server render, and they say so.

toast@comitor/uiThrowsSonner command
dismissToast@comitor/uiThrowsSonner command
toastPromise@comitor/uiThrowsSonner command

Helper functions

All pure, all in directive-free modules, all callable from a Server Component — and from a Server Action, and from a route handler. One exception, at the end of the group.

cn@comitor/uiYesclsx + tailwind-merge
getComitorMessages@comitor/uiYesA locale’s whole string table, plus an optional override — the provider’s work, without React
mergeComitorMessages@comitor/uiYesMerges an override into a table, namespace by namespace
getComitorLanguage@comitor/uiYesLocale → built-in table: vi-anything is vi, the rest en
dateFormatForLocale@comitor/uiYesShips with VI_DATE_FORMAT
toSelectValue@comitor/uiYesShips with SELECT_EMPTY_VALUE
fromSelectValue@comitor/uiYesShips with SELECT_EMPTY_VALUE
applyMask@comitor/uiYesMaskInput’s masking engine — 2.1.0
applyCurrencyMask@comitor/uiYesMaskInput’s masking engine
applyPercentageMask@comitor/uiYesMaskInput’s masking engine
getUnmaskedValue@comitor/uiYesMaskInput’s masking engine
toUnmaskedIndex@comitor/uiYesMaskInput caret mapping
fromUnmaskedIndex@comitor/uiYesMaskInput caret mapping
getAvatarToneClasses@comitor/uiYesDeterministic tone from a name
getAvatarToneColors@comitor/ui · @comitor/ui/tokensYesHex pair for canvas and PDF work
getAvatarToneIndex@comitor/ui · @comitor/ui/tokensYesDeterministic tone from a name
getStatusConfig@comitor/uiYesStatus map lookup
getStatusClasses@comitor/uiYesStatus map lookup
getStatusLabel@comitor/uiYesStatus map lookup
sortByStatusPriority@comitor/uiYesStatus map ordering
getVietnameseInitials@comitor/ui · @comitor/ui/shellYesSame function, two entries
normalizeVi@comitor/ui · @comitor/ui/shellYesSame function, two entries
removeVietnameseTones@comitor/uiYesString utility
matchesSearch@comitor/uiYesTone-insensitive search predicate
truncate@comitor/uiYesString utility
toNavGroups@comitor/ui/shellYesNav shape normaliser
flattenNavItems@comitor/ui/shellYesNav shape normaliser
toSearchParams@comitor/ui/shellYesAccepts the searchParams object of a server page
isNavItemActive@comitor/ui/shellYesActive-route matching
isNavBranchActive@comitor/ui/shellYesActive-route matching
getEntitledApps@comitor/ui/shellYesApp launcher filtering
getLockedApps@comitor/ui/shellYesApp launcher filtering
resolveCurrentApp@comitor/ui/shellYesApp launcher lookup
appAccentStyle@comitor/ui/shellYesInline style object for the app accent
isContrastMode@comitor/ui/shellYesType guard
isDensityMode@comitor/ui/shellYesType guard
isFontSizeMode@comitor/ui/shellYesType guard
isThemePalette@comitor/ui/shellYesType guard — 3.15.0
parseDisplayPrefsSeed@comitor/ui/shellYesFilters any value to valid display defaults — 3.7.0
advanceKeySequence · normalizeSequenceKey@comitor/uiYeskey-sequence.ts, the rules under useKeySequences — 3.7.0
isEditableTarget@comitor/uiYesSame file, safe to import; it reads the DOM, so call it in the browser
moveTreeItems · computeTreeRollup · computeTreeNumbering · groupTreeChildren@comitor/uiYestree-view-constants.ts — apply a TreeView move in a Server Action before writing it — 3.12.0
formatDate · formatDateForDisplay@comitor/ui/data-gridYesTake an optional date-fns locale; without one, date-fns’ en-US format (3.0.0)
formatDateToString · parseLocalDate@comitor/ui/data-gridYesDate ⇄ the stored yyyy-MM-dd
formatFileSize · generateFileId@comitor/ui/data-gridYesFile cells
getCellKey · parseCellKey@comitor/ui/data-gridYesRow and column ⇄ cell key
getColumnVariant · getEmptyCellValue@comitor/ui/data-gridYesPer cell variant: icon and label, empty value
getRowHeightValue · getLineCount@comitor/ui/data-gridYesRow height → pixels, lines
getDataGridMessages@comitor/ui/data-gridYesOne grid language’s string table
getDefaultOperator · getOperatorsForVariant · getFilterFn@comitor/ui/data-gridYesFilter operators and the filter function
getDataGridSelectColumn@comitor/ui/data-gridThrowsThe exception: declared in a client file. It builds the checkbox column, so call it where columns are defined — in a client component

Constants

The group that would fail silently if it were placed wrongly. Each of these is declared in a module with no directive and exported straight from the barrel, so a server render reads the real value. Since 3.0.0 every DEFAULT_*_LABELS holds the English table, and no component reads it any more — they read ComitorLocaleProvider.

comitorMessagesEn@comitor/uiYesi18n/messages.ts — the English table, which defines every shape
comitorMessagesVi@comitor/uiYesi18n/messages.ts — the Vietnamese table
COMITOR_MESSAGES@comitor/uiYesi18n/messages.ts — both, keyed by language
DEFAULT_COMITOR_LOCALE@comitor/uiYesi18n/locale.ts
CHART_COLORS@comitor/ui/chartYeschart/chart-colors.ts
CHART_SERIES_COLORS@comitor/ui/chartYeschart/chart-colors.ts
CONTRAST_MODES@comitor/ui/shellYesshell/contrast-constants.ts
CONTRAST_ATTRIBUTE@comitor/ui/shellYesshell/contrast-constants.ts
CONTRAST_STORAGE_KEY@comitor/ui/shellYesRead by the root layout
DEFAULT_CONTRAST_TOGGLE_LABELS@comitor/ui/shellYesshell/contrast-constants.ts
DENSITY_MODES@comitor/ui/shellYesshell/display-axis-constants.ts
DENSITY_ATTRIBUTE@comitor/ui/shellYesshell/display-axis-constants.ts
DENSITY_STORAGE_KEY@comitor/ui/shellYesRead by the root layout
DEFAULT_DENSITY@comitor/ui/shellYesshell/display-axis-constants.ts
FONT_SIZE_MODES@comitor/ui/shellYesshell/display-axis-constants.ts
FONT_SIZE_ATTRIBUTE@comitor/ui/shellYesshell/display-axis-constants.ts
FONT_SIZE_STORAGE_KEY@comitor/ui/shellYesRead by the root layout
DEFAULT_FONT_SIZE@comitor/ui/shellYesshell/display-axis-constants.ts
THEME_PALETTES@comitor/ui/shellYesshell/display-axis-constants.ts — 3.15.0
PALETTE_ATTRIBUTE@comitor/ui/shellYesshell/display-axis-constants.ts — 3.15.0
PALETTE_STORAGE_KEY@comitor/ui/shellYesRead by the root layout — 3.15.0
DEFAULT_PALETTE@comitor/ui/shellYesshell/display-axis-constants.ts — 3.15.0
DEFAULT_SHELL_LABELS@comitor/ui/shellYesshell/types.ts
DEFAULT_COMMAND_PALETTE_LABELS@comitor/uiYescommand-palette-constants.ts
DEFAULT_DATA_TABLE_LABELS@comitor/uiYesdata-table-constants.ts
DEFAULT_DATE_PICKER_LABELS@comitor/uiYesdate-picker-constants.ts
DEFAULT_DATE_PRESETS@comitor/uiYesdate-picker-constants.ts
DEFAULT_DATE_RANGE_PRESETS@comitor/uiYesdate-picker-constants.ts
VI_DATE_FORMAT@comitor/uiYesdate-picker-constants.ts
DEFAULT_SEARCH_FILTER_BAR_LABELS@comitor/uiYessearch-filter-bar-constants.tsx
DEFAULT_TABLE_PAGINATION_LABELS@comitor/uiYestable-pagination-constants.ts
DEFAULT_PAGE_SIZE_OPTIONS@comitor/uiYestable-pagination-constants.ts
SELECT_EMPTY_VALUE@comitor/uiYesform-field-constants.ts
DEFAULT_ACTION_BAR_LABELS@comitor/uiYesaction-bar-constants.ts
DEFAULT_CROPPER_LABELS@comitor/uiYescropper-constants.ts
DEFAULT_KANBAN_LABELS@comitor/uiYeskanban-constants.ts
DEFAULT_KEY_VALUE_LABELS@comitor/uiYeskey-value-constants.ts
DEFAULT_PHONE_INPUT_LABELS@comitor/uiYesphone-input-constants.ts
DEFAULT_STEPPER_LABELS@comitor/uiYesstepper-constants.ts
DEFAULT_TIME_PICKER_LABELS@comitor/uiYestime-picker-constants.ts
DEFAULT_TOUR_LABELS@comitor/uiYestour-constants.ts
MASK_PATTERNS@comitor/uiYesmask-input-constants.ts
KEY_SEQUENCE_TIMEOUT_MS@comitor/uiYeskey-sequence.ts — 3.7.0
DISPLAY_PREF_VALUES · DISPLAY_PREF_STORAGE_KEYS@comitor/ui/shellYesshell/display-prefs-seed.tsx — 3.7.0
DEFAULT_FILE_UPLOAD_LABELS@comitor/ui/uploaderYesuploader/file-upload-constants.ts
DEFAULT_IMAGE_UPLOAD_FIELD_LABELS@comitor/ui/uploaderYesSame file. The barrel imports it from there, not through the client field
DEFAULT_MARKDOWN_EDITOR_LABELS@comitor/ui/editorYeseditor/markdown-editor-constants.ts — imports no @tiptap package, so reading it pulls no editor in
TEXT_ · NUMBER_ · DATE_ · SELECT_ · BOOLEAN_FILTER_OPERATORS@comitor/ui/data-gridYesdata-grid/filters.ts
DATA_GRID_MESSAGES · DEFAULT_DATA_GRID_LOCALE@comitor/ui/data-gridYesdata-grid/messages.ts
dataGridMessages · dataGridMessagesEn@comitor/ui/data-gridYesdata-grid/messages-en.ts
dataGridMessagesVi@comitor/ui/data-gridYesdata-grid/messages-vi.ts
STATUS_TONES@comitor/uiYesstatus-config.ts
AVATAR_TONE_NAMES@comitor/uiYeslib/avatar-tone.ts
LEGACY_AVATAR_TONE_ALIASES@comitor/uiYeslib/avatar-tone.ts — navy, ash, neutral → blue, purple, pink; exported since 3.12.1
GANTT_SCALES@comitor/uiYesgantt-constants.ts — 3.12.0
tabsListVariants@comitor/uiYestabs-variants.ts — a plain map, not cva
tabsTriggerVariants@comitor/uiYestabs-variants.ts — a plain map, not cva
TABS_TRIGGER_BASE@comitor/uiYestabs-variants.ts
gold · themePalettes · brandNeutral · ash · accent · accentInk · accentForeground · semantic · highContrast · status · chart · layout · typography@comitor/ui · @comitor/ui/tokensYesThe whole token mirror. tokens.ts has no directive and never will

Brand marks

Both are deliberately directive-free so a server page renders static SVG and ships no JavaScript for them.

ComitorLogo@comitor/ui/shellYesStatic SVG, no directive
ComitorLockup@comitor/ui/shellYesStatic SVG, no directive

Seed script

Directive-free for the same reason: it belongs in the root layout, which is a Server Component.

DisplayPrefsSeedScript@comitor/ui/shellYesInline script that seeds account display defaults before first paint — 3.7.0

How the list stays true

A silent boundary bug cannot be caught by review, so the package gates it before publish. Three checks run on every release.

1

The directive survives the build

Every source file that starts with "use client" must produce a dist file that starts with it too. A dropped directive would make a client component look like a server one, and the failure would only show up in a consuming app after publish.

2

No pure constant sits in a client file

A data value declared inside a "use client" file fails the gate outright. The gate knows data by its initialiser — an object, array, string, number or boolean literal — so components written as cva(…) or forwardRef(…) are exempt: their initialiser is a call, not data. The same rule is why, since 3.0.0, every DEFAULT_*_LABELS is written { ...xEn } rather than = xEn: assigned straight from an identifier, the constants the gate could see fell from 58 to 47, and nothing turned red.

3

No constant reaches an entry through a client file

The stricter one. A constant can sit correctly in its own pure module and still be routed to the barrel via a client re-export, which reproduces the bug exactly. The gate walks the re-export graph from all nine entries and fails any name whose only path crosses a client file.

Check 3 exists because check 2 was not enough, and that was measured rather than assumed. Put CHART_COLORS in its own pure module — correct on its own, so checks 1 and 2 both stay green — but let the barrel reach it through the client chart file, and a real Next build hands a server page an object with zero keys again.