Admin
FoundationStableTome-native admin shell that wraps Payload's admin. Sales-tool v0.
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKENThen install:
npm install @wabbit/tome-admin
Overview
@wabbit/tome-admin
Tome-native admin shell that wraps Payload's admin panel. Ships a domain-grouped sidebar with license/capability-gated navigation, a modular per-role dashboard (19 built-in widgets), a command palette, and tabbed edit-view composition — all installed additively onto an existing payload.config.ts, never by forking Payload's admin.
See CHANGELOG.md for the patch history. Prior 0.1.0-alpha scaffold status is retired; the shell, Sidebar v2, and modular dashboards described below are shipped.
Install
npm install @wabbit/tome-admin// root layout
import '@wabbit/tome-admin/styles.css'Peer dependencies (required unless marked optional):
| Package | Range | |---|---| | payload | >=3.70 | | @payloadcms/ui | >=3.70 | | @wabbit/tome-core | >=1.20.0 <2.0.0 (the theme switcher reads @wabbit/tome-core/registry/themeRegistry; the sidebar also needs isAdminNavDomain) | | @wabbit/tome-ui | >=0.13.0 <1.0.0 | | @wabbit/tome-motion | >=0.2.0 <1.0.0 | | @wabbit/tome-admin-pro | >=0.1.0-rc.0 — optional; absence fails open (nav simply serializes no license claim) | | react | >=19.0.0 | | react-dom | >=19.0.0 | | lucide-react | >=0.400.0 | | next | >=15.0.0 | | cmdk | ^1.0.0 | | @radix-ui/react-avatar | ^1.1.0 | | @radix-ui/react-collapsible | ^1.1.0 | | @radix-ui/react-dialog | ^1.1.0 | | @radix-ui/react-dropdown-menu | ^2.1.0 | | @radix-ui/react-scroll-area | ^1.2.0 | | @radix-ui/react-separator | ^1.1.0 | | @radix-ui/react-slot | ^1.1.0 | | @radix-ui/react-tabs | ^1.1.0 | | @radix-ui/react-tooltip | ^1.1.0 |
`payload generate:importmap` is required after every install or upgrade. Payload's root admin.components.* slots only accept PayloadComponent path-strings (a string import path or false) — not React component references — so mergeAdminComponents() wires in string paths like @wabbit/tome-admin/payload/entrypoints/Nav#default, and Payload's importmap has to be regenerated to resolve them.
Peer dependencies
Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).
| Peer | Range | Required | |---|---|---| | @payloadcms/ui | >=3.70 | yes | | @radix-ui/react-avatar | ^1.1.0 | yes | | @radix-ui/react-collapsible | ^1.1.0 | yes | | @radix-ui/react-dialog | ^1.1.0 | yes | | @radix-ui/react-dropdown-menu | ^2.1.0 | yes | | @radix-ui/react-scroll-area | ^1.2.0 | yes | | @radix-ui/react-separator | ^1.1.0 | yes | | @radix-ui/react-slot | ^1.1.0 | yes | | @radix-ui/react-tabs | ^1.1.0 | yes | | @radix-ui/react-tooltip | ^1.1.0 | yes | | @wabbit/tome-admin-pro | >=0.1.0-rc.0 | no (optional) | | @wabbit/tome-core | >=1.20.0 <2.0.0 | yes | | @wabbit/tome-motion | >=0.2.0 <1.0.0 | yes | | @wabbit/tome-ui | >=0.13.0 <1.0.0 | yes | | cmdk | ^1.0.0 | yes | | lucide-react | >=0.400.0 | yes | | next | >=15.0.0 | yes | | payload | >=3.70 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes |
Quickstart
// payload.config.ts
import { buildConfig } from 'payload'
// Registers the 19 built-in widgets. Must be imported BEFORE mergeAdminComponents runs.
import '@wabbit/tome-admin/widgets/starter-kit'
import { mergeAdminComponents } from '@wabbit/tome-admin/payload'
export default buildConfig(
mergeAdminComponents(
{
// your Payload config
},
{
shell: true, // Nav/Icon/Logo full-replacement + Providers prepend
dashboard: true, // installs tome-admin-layouts + the widget resolver
consumerNav: [
// optional — site-owned nav groups that don't fit a layer's canonical label
{ layer: 'my-site', group: 'Marketing', domain: 'site', order: 10 },
],
consumerName: 'my-site',
brand: {
// optional — your site's identity in the shell
name: 'My Site',
Logo: '/components/admin/Logo#default',
Icon: '/components/admin/Icon#default',
},
},
),
)brand is optional and every field in it is optional. brand.name replaces "Default team" in the sidebar header and is the wordmark on the shell's default login Logo (serialized onto admin.custom.tomeBrand). brand.Logo / brand.Icon are import-map paths to your own components and, when set, take the admin.components.graphics.Logo / Icon slots instead of the shell's. Leave brand out and the shell renders exactly as before.
brand?: { name?: string; Logo?: string; Icon?: string }Then run payload generate:importmap and import @wabbit/tome-admin/styles.css in your root layout.
The gotcha that bites: dashboard: true copies the widget registry into admin.dashboard.widgets at the moment mergeAdminComponents runs. Nothing registers widgets for you — without the starter-kit import (or individual @wabbit/tome-admin/widgets/<slug> imports, or your own registerWidget calls) placed above the call, the dashboard installs with zero Tome widgets and every role gets an empty page. The same ordering applies to @wabbit/tome-admin-pro: import it above the call too.
To pre-author per-role layouts instead of relying on the auto layout, seed them from onInit:
import { seedTomeAdminLayouts } from '@wabbit/tome-admin/payload/seed-layouts'
// inside buildConfig({ ... })
onInit: async (payload) => {
await seedTomeAdminLayouts(payload) // skips roles that already have a row
},shell and dashboard are independent opt-in flags (both default false for backward compatibility); shell wins for scalar slots (Nav/Icon/Logo) when combined with the legacy probe flag. The admin-ui-prefs collection (pin storage) is installed unconditionally on every mergeAdminComponents call. The GET /api/tome-admin/me-capabilities endpoint is registered whenever at least one of shell / dashboard / probe is enabled — a bare passthrough call (all three omitted) skips it along with the rest of the merge.
Module Surface
| Subpath | Contents | |---|---| | . | Root barrel — AdminShell, AdminShellProviders, Sidebar (+ SidebarHeader/SidebarBody/SidebarFooter)/SidebarProvider/useSidebar, Header/SubHeader/Breadcrumbs, UserMenu/TeamSwitcher, EditViewTabs family, InlineEditSheet/InlineEditSheetClose, EmptyState, NavGroup/NavItem, ThemeSwitcher/ThemeProvider/useTheme (the sidebar theme switcher: writes html[data-tome-theme], offers the themes mergeAdminComponents read from @wabbit/tome-core/registry/themeRegistry — import each theme's ./register first, or pass themes; ThemePackSwitcher/ThemePackProvider/useThemePack remain as deprecated aliases for one minor), keyboard-shortcut hooks (useKeyboardShortcut, useShortcutHint, useIsMac, formatShortcut), chrome motion presets, and the command-palette exports (including groupCommandsByScope) | | ./styles.css | Shell CSS | | ./payload | mergeAdminComponents(), mergeCollectionAdminComponents() | | ./payload/entrypoints/{Nav,Icon,Logo,Providers,EditDefault,ListView} | Path-string targets mergeAdminComponents wires into admin.components.* | | ./payload/layouts-collection | tomeAdminLayoutsCollection, TOME_ADMIN_LAYOUTS_SLUG, WIDGET_WIDTH_OPTIONS | | ./payload/seed-layouts | seedTomeAdminLayouts(payload, { layouts?, overwrite? }) — idempotent per-role layout seeder (skips roles that already have a row unless overwrite: true), race-safe under concurrent onInit workers; TOME_ADMIN_DEFAULT_LAYOUTS is the built-in set it seeds when layouts is omitted | | ./command | CommandPalette, CommandProvider, DefaultCommandRegistrar / PayloadDefaultCommandRegistrar, useCommandPalette, useCommands, useRegisterCommand, groupCommandsByScope | | ./widgets | Author surface — defineWidget, registerWidget, withWidgetContext; registry inspection getRegisteredWidgets, getRegisteredWidget, resetWidgetRegistry (test-only); Payload adapters toPayloadWidget, toPayloadWidgetInstance | | ./widgets/{activity-feed,pending-actions,launchpad,system-health,kpi-card,content-stats,form-submissions,media-storage,analytics,redirects,seo-health,user-sessions,welcome-banner,quick-actions,api-keys,notifications,approval-queue,scheduled-publishing,jobs-status} | The 19 built-in widgets, each importable standalone for tree-shaking | | ./widgets/starter-kit | Side-effect import that registers all 19 built-in widgets; also exports each widget definition and the TOME_ADMIN_STARTER_WIDGETS array | | ./dashboard/resolver | createDashboardResolver({ keepDefaultCollections?, layoutFetcher? }) — builds the per-request admin.dashboard.defaultLayout function |
Admin shell (Phase 1)
{ shell: true } replaces Payload's default Nav with the Tome sidebar, swaps the graphics Icon/Logo slots, and prepends the Tome provider tree via admin.components.providers. The sidebar groups collections/globals by a two-level hierarchy — a fixed set of top-level domains (content / commerce / people / learning / site / system, plus an other fallback) — populated from a manifest resolver, not from Payload's raw admin.group strings.
Sidebar v2 — nav manifests, domains, licensing
Layers self-register AdminNavManifest entries ({ layer, group, domain, order?, capability?, iconName?, licenseTier?, lockedBehavior? }) against @wabbit/tome-core's layer registry; mergeAdminComponents reads every registered layer's manifest at config-build time and serializes it (component refs stripped — only the JSON-safe iconName survives) onto admin.custom.tomeAdminNavManifests for the client-side Nav.tsx to render. Consumer-owned groups that don't fit a layer's canonical label go through the separate consumerNav option instead of the shared registry, so a consumer label can never silently outrank a platform layer's claim on a group name.
License tiers (free < pro < enterprise, cumulative) are resolved once at config-build time from the optional @wabbit/tome-admin-pro/license package (lazy required — admin-pro absence fails open: no tomeLicense claim is serialized, so a sidebar never locks itself out on a fresh core-only install) and serialized to admin.custom.tomeLicense. Per-user capabilities are different in kind — they vary per request, so they can't ride the static config bridge — and are instead fetched by the client from GET /api/tome-admin/me-capabilities, registered automatically as part of mergeAdminComponents. Both the sidebar's capability gate and the dashboard's widget filter resolve through the same resolveUserCapabilities / resolveUserCapabilitiesFromReq module, so nav and dashboard visibility can never diverge.
Sidebar v2 exists because layer-registered manifests and a fixed domain taxonomy replaced Payload's raw admin.group strings — that decoupling is what lets a license tier or a per-user capability gate the same nav item without every layer author having to know about licensing.
Modular dashboards + widgets (v0.2)
{ dashboard: true } installs the tome-admin-layouts collection (one row per role, authored via the admin panel) and a createDashboardResolver() instance as admin.dashboard.defaultLayout. At render, the resolver: (1) reads the viewer's primary role (the first entry when the user has several), (2) fetches that role's authored layout row, (3) uses an "auto" layout — every registered, capability-passing widget in registration order — when the viewer has no role or no row exists for it; with a row, unknown widget slugs are skipped, and under the row's default fallbackMode: 'auto' every registered widget the row did not place is appended after the authored ones ('hide-unknown' shows only the authored widgets), and (4) always capability-filters every placed widget through the shared resolver before emitting WidgetInstance[]. Pass { dashboard: { keepDefaultCollections: true } } to prepend Payload's native collections widget, or { dashboard: { fullReplace: '<path>' } } to bypass the widget/resolver path entirely with a consumer-owned dashboard view.
Widgets ship as a 6-file scaffold per slug (see src/widgets/activity-feed/ as the reference implementation):
| File | Role | |---|---| | widget.ts | Pure TomeWidget definition via defineWidget() — no side effects, safe to import in tests | | register.ts | Side-effectful registerWidget() call, importing only ./widget — safe at config-build time (doesn't pull in the view or its CSS) | | compute.ts | Data-fetching/shaping logic extracted from the component so it's unit-testable without a React renderer | | ActivityFeed.tsx (wrapper) | The server component Payload's widget slot actually mounts, wrapped in withWidgetContext for { user, can, payload, req, widgetData } and self-gating on capability as defense in depth | | ActivityFeedView.tsx (View) | Pure presentational component — no data access | | index.ts | Module entrypoint — imports ./register (registers as a side effect) + the wrapper component, re-exports the widget def and View, default-exports the wrapper |
19 built-in widgets ship this way today: activity-feed, pending-actions, launchpad, system-health, kpi-card, content-stats, form-submissions, media-storage, analytics, redirects, seo-health, user-sessions, welcome-banner, quick-actions, api-keys, notifications, approval-queue, scheduled-publishing, jobs-status. tier: 'pro' widgets never reach the registry on an unlicensed install — the gate is enforced in @wabbit/tome-admin-pro's own register.ts, not here.
Command palette
./command ships CommandPalette (cmdk's Command primitive for fuzzy filter + keyboard selection, wrapped in a Radix Dialog for portal/overlay/focus-trap — cmdk ships neither), a CommandProvider/registry pair, and DefaultCommandRegistrar (+ a Payload-aware PayloadDefaultCommandRegistrar variant that statically imports @payloadcms/ui — required for Turbopack's CJS-of-ESM interop, see CHANGELOG 0.1.1). Consumers register additional commands via useRegisterCommand.
Edit view composition
EditViewTabs (+ EditViewTabsSplitRoot / EditViewTabsBar / EditViewTabsPanels) groups Payload field groups under a @wabbit/tome-ui Tabs primitive for use inside a collection's EditDefault entrypoint — split mode renders the tab bar in SubHeader's tabs slot with panels rendered separately. mergeCollectionAdminComponents() is the Phase 2 sibling to mergeAdminComponents(): it walks config.collections[*] and additively assigns the stock EditDefault/ListView entrypoints per collection (slug-allowlist or '*'), without overwriting a collection's existing view override.
Testing
Widget logic is split into compute.ts/widget.ts so it can be unit-tested without rendering. For registry-dependent tests, call resetWidgetRegistry() (from @wabbit/tome-admin/widgets) in beforeEach, and drive the dashboard resolver with a stub fetcher instead of a live Payload:
import { createDashboardResolver } from '@wabbit/tome-admin/dashboard/resolver'
const resolve = createDashboardResolver({ layoutFetcher: async () => null }) // null = no authored row → auto layout
const widgets = await resolve({ req })Links
- Changelog:
CHANGELOG.md
Decisions that shaped this package
- Nav resolution is authority-ranked (platform > platform-default table > `consumerNav` > synthetic), not first-registration-wins — the old model let a consumer's registration order nondeterministically decide label collisions (live case:
'Marketing'claimed by bothtome-marketingand a consumer's ownconsumerNav); precedence now dedupes deterministically and warns on collision instead of silently picking whichever registered first. - A `PLATFORM_DEFAULT_MANIFESTS` backstop closes the "other" pile with zero per-package edits — seven packages (Intake/Forms/Deals/Accounts/AI/Gamification/RPG) shipped sidebar collections with an
admin.groupbut no manifest; rather than requiring each package to publish a manifest, the default table maps known labels to a domain so they resolve correctly without a republish. - Universal Payload/core collections (payload-jobs, users, better-auth, admin-ui-prefs) route to System/Auth by a slug set, not a label match — these collections have no
admin.groupa consumer site can set, so they were structurally uncategorizable and cluttered every install's "other" pile identically;PLATFORM_SYSTEM_SLUGSslots them intosystemdirectly, while consumer-owned collections still resolve by label. - License tier is resolved once at config-build time via a lazy, optional `@wabbit/tome-admin-pro/license` read; per-user capability is fetched per-request from `GET /api/tome-admin/me-capabilities` — these are deliberately two different mechanisms because license is an install/tenant-scoped property known server-side at build time, while capability varies per user per request and can't ride the static config bridge; both the sidebar and the dashboard widget filter resolve through the same shared module so nav and dashboard visibility can never diverge.
- License-gated nav items default to visible-locked with an upgrade affordance, never hidden — a locked item still tells the user the capability exists, and admin-pro's absence fails open (no license claim serialized) rather than locking out a fresh core-only install.
Exports
@wabbit/tome-admin@wabbit/tome-admin/styles.css@wabbit/tome-admin/payload@wabbit/tome-admin/payload/entrypoints/Nav@wabbit/tome-admin/payload/entrypoints/Icon@wabbit/tome-admin/payload/entrypoints/Logo@wabbit/tome-admin/payload/entrypoints/Providers@wabbit/tome-admin/payload/entrypoints/EditDefault@wabbit/tome-admin/payload/entrypoints/ListView@wabbit/tome-admin/command@wabbit/tome-admin/widgets@wabbit/tome-admin/widgets/activity-feed@wabbit/tome-admin/widgets/pending-actions@wabbit/tome-admin/widgets/launchpad@wabbit/tome-admin/widgets/system-health@wabbit/tome-admin/widgets/kpi-card@wabbit/tome-admin/widgets/content-stats@wabbit/tome-admin/widgets/form-submissions@wabbit/tome-admin/widgets/media-storage@wabbit/tome-admin/widgets/analytics@wabbit/tome-admin/widgets/redirects@wabbit/tome-admin/widgets/seo-health@wabbit/tome-admin/widgets/user-sessions@wabbit/tome-admin/widgets/welcome-banner@wabbit/tome-admin/widgets/quick-actions@wabbit/tome-admin/widgets/api-keys@wabbit/tome-admin/widgets/notifications@wabbit/tome-admin/widgets/approval-queue@wabbit/tome-admin/widgets/scheduled-publishing@wabbit/tome-admin/widgets/jobs-status@wabbit/tome-admin/widgets/starter-kit@wabbit/tome-admin/dashboard/resolver@wabbit/tome-admin/payload/layouts-collection@wabbit/tome-admin/payload/seed-layouts@wabbit/tome-admin/payload/entrypoints/*
Changelog
228b2e4: Consumer branding for the admin shell, and the sidebar now marks the current page. - **`mergeAdminComponents` takes an optional `brand` option.** The shell used to hardcode its identity: the login Logo always said "Tome Admin", the sidebar header always said "Default team", and the shell's Logo/Icon always overrode a site's own `admin.components.graphics`. Now `brand.name` shows in the sidebar header and as the default Logo wordmark (via `admin.custom.tomeBrand`), and `brand.Logo` / `brand.Icon` (import-map paths) take the graphics slots instead of the shell's. Consumers who do not pass `brand` see no change. - **Fix: the sidebar highlights the page you are on.** `NavItem` supported `active` but no caller ever passed it, so nothing was highlighted and no link carried `aria-current`. The grouped nav and the Pinned section now compare the current path to each link (exact match or a `/…` sub-path, so `/admin/collections/media` does not light up `/admin/collections/media-folders`) and set `data-active` and `aria-current="page"`. This reads the path with `usePathname()` from `next/navigation`, so `next` (`>=15.0.0`) is now a declared peer dependency; every Payload admin host already has it. - **Fix: dashboard widgets render for relationship-shaped roles.** 0.8.2 fixed the dashboard resolver, but each widget then ran its own sync `can()` against `req.user`, which on sites whose users carry `roles` as a relationship has role ids only, so every widget rendered nothing. `withWidgetContext` now awaits `canAsync(user, req)` first, populating `_populatedRoles` (cached on `req.context`); it is a no-op when roles are already readable. Widgets wrapped by it now return a Promise, which Payload supports for server-component widgets.
- 228b2e4: Consumer branding for the admin shell, and the sidebar now marks the current page. - **`mergeAdminComponents` takes an optional `brand` option.** The shell used to hardcode its identity: the login Logo always said "Tome Admin", the sidebar header always said "Default team", and the shell's Logo/Icon always overrode a site's own `admin.components.graphics`. Now `brand.name` shows in the sidebar header and as the default Logo wordmark (via `admin.custom.tomeBrand`), and `brand.Logo` / `brand.Icon` (import-map paths) take the graphics slots instead of the shell's. Consumers who do not pass `brand` see no change. - **Fix: the sidebar highlights the page you are on.** `NavItem` supported `active` but no caller ever passed it, so nothing was highlighted and no link carried `aria-current`. The grouped nav and the Pinned section now compare the current path to each link (exact match or a `/…` sub-path, so `/admin/collections/media` does not light up `/admin/collections/media-folders`) and set `data-active` and `aria-current="page"`. This reads the path with `usePathname()` from `next/navigation`, so `next` (`>=15.0.0`) is now a declared peer dependency; every Payload admin host already has it. - **Fix: dashboard widgets render for relationship-shaped roles.** 0.8.2 fixed the dashboard resolver, but each widget then ran its own sync `can()` against `req.user`, which on sites whose users carry `roles` as a relationship has role ids only, so every widget rendered nothing. `withWidgetContext` now awaits `canAsync(user, req)` first, populating `_populatedRoles` (cached on `req.context`); it is a no-op when roles are already readable. Widgets wrapped by it now return a Promise, which Payload supports for server-component widgets.
479b5ae: The dashboard resolves roles for users whose roles are a relationship, and the `@wabbit/tome-ui` peer floor is now `>=0.13.0`. Dashboard: `createDashboardResolver` read the viewer's role only from `_populatedRoles` or a `role` slug array. A site whose `users` carry `roles` as a relationship has neither in the JWT (only role ids), so no role resolved, every capability-gated widget failed its check, and the dashboard rendered empty ("There are no widgets on your dashboard"). The resolver now awaits `resolveUserCapabilitiesFromReq(req)` first, the same hydration the sidebar already uses: it populates `_populatedRoles` with one depth-1 user read when roles are not readable, and is a no-op otherwise. Found on a consumer site. Peer floor: `shell/SidebarProvider` imports `@wabbit/tome-ui/utils/useMediaQuery`, which `@wabbit/tome-ui` 0.9.0–0.9.8 and 0.12.x do not ship, so a consumer inside the old `>=0.9.0` range failed `next build` with "Module not found". The floor moves to the first release line that ships the subpath continuously.
- 479b5ae: The dashboard resolves roles for users whose roles are a relationship, and the `@wabbit/tome-ui` peer floor is now `>=0.13.0`. Dashboard: `createDashboardResolver` read the viewer's role only from `_populatedRoles` or a `role` slug array. A site whose `users` carry `roles` as a relationship has neither in the JWT (only role ids), so no role resolved, every capability-gated widget failed its check, and the dashboard rendered empty ("There are no widgets on your dashboard"). The resolver now awaits `resolveUserCapabilitiesFromReq(req)` first, the same hydration the sidebar already uses: it populates `_populatedRoles` with one depth-1 user read when roles are not readable, and is a no-op otherwise. Found on a consumer site. Peer floor: `shell/SidebarProvider` imports `@wabbit/tome-ui/utils/useMediaQuery`, which `@wabbit/tome-ui` 0.9.0–0.9.8 and 0.12.x do not ship, so a consumer inside the old `>=0.9.0` range failed `next build` with "Module not found". The floor moves to the first release line that ships the subpath continuously.
775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
- 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
e59a3ad: **BREAKING:** the sidebar `ThemeSwitcher` writes `data-tome-theme`, lists registered themes, and `DEFAULT_AVAILABLE_PACKS` is removed. It replaces `ThemePackSwitcher`, which wrote `data-tome-pack` and offered a hard-coded list; the themes now come from `@wabbit/tome-core`'s theme registry. **Migration:** upgrade `@wabbit/tome-core` to 1.20 or later (the peer floor moves from 1.3). Import each theme package's `./register` subpath before calling `mergeAdminComponents`, or pass `themes` to it; with no themes registered the switcher offers only "Base". Rename `ThemePackSwitcher`/`ThemePackProvider`/`useThemePack` to `ThemeSwitcher`/`ThemeProvider`/`useTheme`; the old names still work for one minor and now write `data-tome-theme`. `availableThemePacks` on `AdminShellProviders`/`AdminShell` is deprecated in favour of `themes`. - The previous default list named three empty stylesheets that restyled nothing, and the one working theme was never offered. `mergeAdminComponents` now serializes `listThemes()` onto `admin.custom.tomeThemes`; the `Providers` entrypoint passes it to the switcher as data, so no client component reads the registry. - Items are labelled from each theme's manifest `label`; a theme with several palettes lists each palette, written as `data-tome-palette`. - The choice persists under `tome-admin-theme`. A value saved under the old `tome-admin-theme-pack` key is migrated once, and kept only if it names a registered theme. - The palette command "Toggle theme pack" is now "Cycle site theme" (same command id). - The jobs-status widget heading reads the declared `--tome-type-size-xl` (it read the undeclared `--tome-text-xl`).
- e59a3ad: **BREAKING:** the sidebar `ThemeSwitcher` writes `data-tome-theme`, lists registered themes, and `DEFAULT_AVAILABLE_PACKS` is removed. It replaces `ThemePackSwitcher`, which wrote `data-tome-pack` and offered a hard-coded list; the themes now come from `@wabbit/tome-core`'s theme registry. **Migration:** upgrade `@wabbit/tome-core` to 1.20 or later (the peer floor moves from 1.3). Import each theme package's `./register` subpath before calling `mergeAdminComponents`, or pass `themes` to it; with no themes registered the switcher offers only "Base". Rename `ThemePackSwitcher`/`ThemePackProvider`/`useThemePack` to `ThemeSwitcher`/`ThemeProvider`/`useTheme`; the old names still work for one minor and now write `data-tome-theme`. `availableThemePacks` on `AdminShellProviders`/`AdminShell` is deprecated in favour of `themes`. - The previous default list named three empty stylesheets that restyled nothing, and the one working theme was never offered. `mergeAdminComponents` now serializes `listThemes()` onto `admin.custom.tomeThemes`; the `Providers` entrypoint passes it to the switcher as data, so no client component reads the registry. - Items are labelled from each theme's manifest `label`; a theme with several palettes lists each palette, written as `data-tome-palette`. - The choice persists under `tome-admin-theme`. A value saved under the old `tome-admin-theme-pack` key is migrated once, and kept only if it names a registered theme. - The palette command "Toggle theme pack" is now "Cycle site theme" (same command id). - The jobs-status widget heading reads the declared `--tome-type-size-xl` (it read the undeclared `--tome-text-xl`).
6530765: CSS files are now copied to `dist/` by a post-build script instead of a tsup `onSuccess` hook; no behaviour change, and the published `dist/` is identical.
- 6530765: CSS files are now copied to `dist/` by a post-build script instead of a tsup `onSuccess` hook; no behaviour change, and the published `dist/` is identical.
f900b58: Dashboard widget dates now render in a pinned `en-US` locale and UTC, so every deploy host produces the same markup. Affects the activity feed, pending actions, API keys, form submissions, notifications and user sessions widgets. The jobs-status widget and the content-stats / KPI fallbacks now count rows with a one-row paginated read instead of loading every matching row; counts are unchanged.
- f900b58: Dashboard widget dates now render in a pinned `en-US` locale and UTC, so every deploy host produces the same markup. Affects the activity feed, pending actions, API keys, form submissions, notifications and user sessions widgets. The jobs-status widget and the content-stats / KPI fallbacks now count rows with a one-row paginated read instead of loading every matching row; counts are unchanged.
b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
- b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
- 73081e6: Drop the unused `sonner` peer dependency. `sonner` was declared as a REQUIRED peer (`^1.5.0 || ^2.0.0`) and imported by nothing — the only reference in the package was a comment in `shell/Providers.tsx` reserving a Toaster portal mount that was never built ("not strictly required by Phase 3 exit criteria"). Every consumer that mounted the admin shell was therefore installing a toast library the shell never loads, and a strict-peer installer warned about it. Removing it is the correct direction rather than making it optional: an optional peer still advertises a capability that does not exist. The build seam is untouched — `tsup.config.ts` still externalises `sonner`, so the day the Toaster actually mounts, only the manifest has to move. The comment in `Providers.tsx` now records that trigger explicitly instead of leaving the reader to infer it from a dangling peer. No behaviour change: nothing imported it, so nothing can break. Consumers that installed `sonner` only to satisfy this peer can drop it.
- 637db74: SystemHealth status dots use the bare `--tome-color-success` / `--tome-color-warning` tokens like every sibling widget, dropping two literal hex fallbacks — the only stylelint `color-no-hex` failures in the admin shell, which had kept `lint:css` off the CI gate.
- 0836ef5: dist now raw-Node loadable: relative specifiers get explicit extensions post-build. `build` gains `&& node ../../scripts/fix-dist-extensions.mjs --strict` as its last step, joining the 13 packages that already ran it. tsup builds `bundle: false` and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM `ERR_MODULE_NOT_FOUND`; CJS worse, `require('./x')` finds the ESM `.js` twin and Node 22+ `require(esm)` then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, `generate:types`, `generate:importmap`, ops scripts, codegen tools. No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. Two supporting changes made the wiring possible, both in repo scripts rather than package source. `fix-dist-extensions.mjs` now skips bundler-asset specifiers (`.css`, `.module.css`, `.scss`, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since `--strict` exited 1 on any package with a relative stylesheet import. Dotted MODULE names (`./config.meta`, `./x.variants`, `./y.demo`) are deliberately NOT treated as assets and still get `.js`/`.cjs` appended. `assert-node-loadable.mjs` gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under `node_modules` is a peer SKIP (next@15 has no exports map, so `next/image` fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as `SyntaxError: Unexpected token '.'` raised from inside the stylesheet). Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a `directory import` missing `/index`). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.
- 73081e6: Manifest metadata: `homepage`, `bugs`, `engines`. All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change. - `homepage` deep-links to that package README on GitHub (`.../tree/main/packages/<dir>#readme`). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want. - `bugs.url` points at the repo issue tracker, so a paying customer has a place to report a defect that is not email. - `engines.node` is `>=22`, matching the root `engines` and `.nvmrc` set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for `node --test`, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version. The forcing function ships with the change: `scripts/assert-manifest-metadata.mjs` (root `pnpm assert:manifest-metadata`, wired into `platform-discipline.yml` beside `assert:license-metadata`) fails when any publishable manifest lacks `description`, `repository.directory` matching its own folder, `homepage`, `bugs`, `engines.node` equal to the repo floor, `license`, `files` or `sideEffects`. It reported 138 violations before this change and 0 after.
- 73081e6: README peer tables, and the gate that now requires them. Sixteen packages declared `peerDependencies` and documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin's `sonner` peer, has its own changeset). Each of the sixteen gains a `## Peer dependencies` section generated from its own `package.json` — `| Peer | Range | Required |`, one row per peer, the range verbatim, `no (optional)` read from `peerDependenciesMeta`, plus one sentence on what is a real `dependency` rather than a peer and why the optional ones are optional. The worst omissions this surfaced: `@wabbit/tome-core` documented 2 of its 13 peers and left out both `next` and `@payloadcms/richtext-lexical`, which are required; `@wabbit/tome-admin` listed 5 of 20; `@wabbit/tome-readout` and `@wabbit/tome-sc` listed none. Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still `>=18` after the peer floor moved to `>=19.0.0` — and none of the eight listed `react-dom` at all. Those tables are retired in favour of the generated one, with a line saying what they used to claim so the next reader does not reinstate them. The forcing function ships with the fix: `scripts/assert-readme-contract.mjs` now FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existing `Optional?` and `Notes` third columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work. Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it. `CONTRIBUTING.md`'s assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.
5fc2a42: The sidebar rail now carries its own expand/collapse toggle, rendered by Sidebar itself below the consumer's header slot — previously the only visible toggle lived in tome-admin's shell Header, which consumers using Payload's own admin header (Wabbit prod) never mount, so an accidental Cmd/Ctrl+B collapsed the sidebar with no visible way back. Also guards useKeyboardShortcut against undefined event.key (autofill/synthetic events threw a TypeError in the console).
- 5fc2a42: The sidebar rail now carries its own expand/collapse toggle, rendered by Sidebar itself below the consumer's header slot — previously the only visible toggle lived in tome-admin's shell Header, which consumers using Payload's own admin header (Wabbit prod) never mount, so an accidental Cmd/Ctrl+B collapsed the sidebar with no visible way back. Also guards useKeyboardShortcut against undefined event.key (autofill/synthetic events threw a TypeError in the console).
71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
- 71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
- 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
- 36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.
- a93f478: Dashboard performance: nine widget compute waterfalls parallelized (activity-feed, approval-queue, pending-actions, seo-health, scheduled-publishing's per-collection scans; user-sessions, kpi-card, form-submissions independent-fetch folds; admin-pro DevDrawer panel gathers) — per-collection error isolation and result ordering preserved exactly, all 717+87 tests unmodified and green. Also: `useMeCapabilities` extracted from the Nav entrypoint to `nav/useMeCapabilities.ts` with in-flight dedup + TTL cache; `usePinnedItems` gains optional `initialItems`/`initialRowId` seeding (non-breaking) + dedup; CommandPalette query reset moved to mount-by-construction (state lives in the dialog body now); SidebarProvider's mobile-close moved from an effect to the matchMedia event source.
- aef2725: Monolith decompositions (behavior- and markup-preserving; public APIs unchanged; markup identity mechanically verified per file): forms' FieldRenderer 633→84 via a field-control registry + shared FieldChrome (consent/checkbox byte-identical branches merged) and TomeForm 656→451 via four extracted hooks (the ordering-critical resolver sync deliberately stays inline, documented); rpg's CharacterSheet 841→130 across panels + three editing hooks + persistence hook (the StrictMode XP-ledger charRef guard preserved verbatim); gallery's GalleryIndex 1032→431 (BlockThumb/BlockCard/Toolbar/useFilteredCatalog siblings, T2's debounce+memo preserved); webgl's WebglCanvasProvider 938→546 (useTransitionOrchestrator + useCanvasRenderer extracted; settle thresholds hoisted to named consts); admin's mergeAdminComponents 828→404 orchestrator + four helpers (all docblocks relocated, 717 tests unmodified) and Nav's config-reading now typed (6 of 8 `as any` casts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalized `xpTotal` on sheet load/save hot paths (full recompute stays at the XP-recording reconciliation point).
dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.
- dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.
- 77bf66b: Fix a duplicate-key race in `seedTomeAdminLayouts` (`payload/seed-layouts.ts`) that could crash a consumer's Payload `onInit` on a fresh/empty database under concurrency — e.g. a Next.js build's "collecting page data" phase, which spawns many parallel worker processes each triggering Payload init against the same DB. Same defect class as `@wabbit/tome-core`'s `initializeRoles` fix (see its companion changeset), but worse: this seeder's check-then-create (`find` → `create`) had **no** try/catch at all, so the losing worker's duplicate-key rejection propagated uncaught. **Uniqueness ground truth**: the `tome-admin-layouts` collection's `role` field carries `unique: true` (`payload/layouts-collection.ts`), so a real DB-level constraint exists on every supported adapter — the failure mode was a crash, not silent duplicate rows; no schema change needed. **Fix**: catch the create rejection and detect the conflict via Payload's canonical, adapter-agnostic `ValidationError` shape (`instanceof ValidationError` + `data.errors[].path === 'role'`) — all DB adapters normalize their native unique-constraint violations into this shape. On a detected conflict, re-fetch by role to confirm the row now exists; if confirmed, honor the seeder's stated semantics: with `overwrite: false` (default) the winner's row is kept, with `overwrite: true` the winner's row is updated with the seed data (identical between racing workers, so idempotent). A conflict the re-fetch cannot confirm, and any non-unique error, is rethrown rather than silently swallowed. Sequential/single-worker behavior is unchanged.
admin.hidden now works in the sidebar: mergeAdminComponents bridges static-hidden collection/global slugs to the client via admin.custom.tomeHiddenNavSlugs (Payload strips admin.hidden from client config, so the resolver's check could never fire in production); PLATFORM_HIDDEN_SLUGS hides Payload's five sanitize-injected internals (payload-jobs/kv/locked-documents/migrations/preferences) by default with the admin.custom.tomeShowHiddenSystemSlugs consumer opt-in knob. PinnedSection mirrors both channels. Function-valued hidden remains visible by design.
- admin.hidden now works in the sidebar: mergeAdminComponents bridges static-hidden collection/global slugs to the client via admin.custom.tomeHiddenNavSlugs (Payload strips admin.hidden from client config, so the resolver's check could never fire in production); PLATFORM_HIDDEN_SLUGS hides Payload's five sanitize-injected internals (payload-jobs/kv/locked-documents/migrations/preferences) by default with the admin.custom.tomeShowHiddenSystemSlugs consumer opt-in knob. PinnedSection mirrors both channels. Function-valued hidden remains visible by design.
Admin label polish + formatted commerce money columns: explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
- Admin label polish + formatted commerce money columns: explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
- 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
**lms-ui — CSS Modules → plain CSS rename (minor, consumer-visible).** `packages/lms-ui/src/components/*/index.module.css` renamed to `index.css` across 30+ components; exports map in `package.json` updated to match (`./components/*` now point to `index.css` under `dist/`). Consumers switch from `import styles from './index.module.css'` to side-effect `import './index.css'`. The previous layout was rejected by Next.js because the CSS used attribute-based global selectors (`[data-layout="three-column"]`), which CSS Modules flag as non-pure. This rename unblocks cross-package CSS `@import` from consumer barrels (e.g. a consumer's `src/styles/tome-lms-ui.css`). Also bumps the build script to add `NODE_OPTIONS=--max-old-space-size=8192` (DTS was OOM'ing against the peer type graph) and adds `cross-env` as a devDep. **admin — Payload 3.x entrypoint alignment + Turbopack cmdk fix (patch).** `DefaultCommandRegistrar` was split: the Payload-aware variant lives in a new `PayloadDefaultCommandRegistrar.tsx` with a static ESM import of `@payloadcms/ui`. Root cause: Next 15 Turbopack's CJS-of-ESM interop returned `useConfig` as not-a-function under the prior `require('@payloadcms/ui')` lazy-load path. Edit/List/Nav entrypoints now render `<DefaultEditView>` and siblings with `DocumentViewClientProps`, matching Payload 3.x's full-replacement slot contract (the prior HOC shape assumed `children` that Payload never delivered). No public API surface changes.
- **lms-ui — CSS Modules → plain CSS rename (minor, consumer-visible).** `packages/lms-ui/src/components/*/index.module.css` renamed to `index.css` across 30+ components; exports map in `package.json` updated to match (`./components/*` now point to `index.css` under `dist/`). Consumers switch from `import styles from './index.module.css'` to side-effect `import './index.css'`. The previous layout was rejected by Next.js because the CSS used attribute-based global selectors (`[data-layout="three-column"]`), which CSS Modules flag as non-pure. This rename unblocks cross-package CSS `@import` from consumer barrels (e.g. a consumer's `src/styles/tome-lms-ui.css`). Also bumps the build script to add `NODE_OPTIONS=--max-old-space-size=8192` (DTS was OOM'ing against the peer type graph) and adds `cross-env` as a devDep. **admin — Payload 3.x entrypoint alignment + Turbopack cmdk fix (patch).** `DefaultCommandRegistrar` was split: the Payload-aware variant lives in a new `PayloadDefaultCommandRegistrar.tsx` with a static ESM import of `@payloadcms/ui`. Root cause: Next 15 Turbopack's CJS-of-ESM interop returned `useConfig` as not-a-function under the prior `require('@payloadcms/ui')` lazy-load path. Edit/List/Nav entrypoints now render `<DefaultEditView>` and siblings with `DocumentViewClientProps`, matching Payload 3.x's full-replacement slot contract (the prior HOC shape assumed `children` that Payload never delivered). No public API surface changes.