Skip to content

Customize Admin from manifests ​

Select --admin and mount native Admin from @aotter/mantle-admin. The app serves its assets and owns authentication. UI and SDK are separate packages.

Schema uiSchema selects fields, widgets, list columns, enum tabs and child navigation. The compiler derives sortable fields from indexed required/non-null fields; it emits selected system/statistics indexes in initial DDL. Apply those indexes through an explicit migration on existing databases.

Admin Views provide custom read lists; their SELECT outputs, column lineage and uiSchema.list define display/search/filter metadata. Procedures and MCP admin Triggers define operations with declared authorization, input checks and optional row targets. A hidden button never authorizes an operation.

Generic publishing edits apply to drafts; publishing transitions retain their atomic guards and OCC. Read-only collections refuse generic edits while declared Procedures can still run. Ref operations receive the caller/transaction-bound Store.

Use defineAdmin components for app-owned cells, inputs, record asides and actions; server work is a Procedure, a View or an application route. Keep exact conflict/uncertain-outcome handling in the shared UI controller.

Theme ​

Admin is themed with CSS variables. Import your own stylesheet from src/admin.tsx (import "./admin.css") and override the public tokens on :root for light and on .dark for dark:

css
:root {
  --primary: #0f766e;
  --primary-foreground: #ffffff;
  --radius: 0.5rem;
}
.dark {
  --primary: #2dd4bf;
  --primary-foreground: #042f2e;
}

Admin marks the active theme as both the .dark class and data-theme="dark" (or "light") on the root. Dark tokens and Tailwind's dark: variant apply when either is present, so [data-theme="dark"] {} is equivalent to .dark {}. Your overrides apply in the browser Admin and, because the entry imports your CSS, in the MCP App too, where the host's neutral colours are laid over them.

TokenMeaning
--backgroundPage background
--foregroundDefault text
--cardCard and panel surface
--card-foregroundText on a card
--popoverMenu, popover and dialog surface
--popover-foregroundText on a popover
--primaryBrand color: primary buttons, active states
--primary-foregroundText on --primary
--secondarySecondary button surface
--secondary-foregroundText on --secondary
--mutedSubtle surface: table stripes, disabled areas
--muted-foregroundSecondary text
--accentHover and selected surface
--accent-foregroundText on --accent
--destructiveDestructive actions and errors
--borderBorders and dividers
--inputForm control borders
--ringFocus ring
--radiusBase corner radius; the other radii derive from it
--successSuccess status color
--warningWarning status color
--infoInformational status color
--chart-1Chart series 1
--chart-2Chart series 2
--chart-3Chart series 3
--chart-4Chart series 4
--chart-5Chart series 5
--sidebarSidebar surface
--sidebar-foregroundSidebar text
--sidebar-primarySidebar active item
--sidebar-primary-foregroundText on --sidebar-primary
--sidebar-accentSidebar hover surface
--sidebar-accent-foregroundText on --sidebar-accent
--sidebar-borderSidebar borders
--sidebar-ringSidebar focus ring
--app-baseSolid color behind the page
--app-backgroundPage backdrop (any CSS background, such as a gradient)

Every other variable, including --mantle-blue-* and --chrome-glass, is internal and may change without notice.

Eject a component ​

@aotter/mantle-admin ships a shadcn registry of starting-point components for the defineAdmin slots. Ejecting copies one into your source: you own that file and edit it freely, while the hooks it calls (@aotter/mantle-admin/hooks) and the slot prop types (@aotter/mantle-admin/app) keep upgrading with the package. The registry is inside the installed package, so the copy matches the version you run and no registry host is involved (the shadcn CLI itself downloads from npm and needs network). Install from the package, in the app's root:

bash
npx shadcn@4 add ./node_modules/@aotter/mantle-admin/dist/registry/admin-cell.json

The file lands in src/admin/<item>.tsx (the item's target ignores aliases). The CLI needs a tsconfig.json and a components.json in the app's root; this minimal one works, because no alias or stylesheet is used:

json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": { "config": "", "css": "", "baseColor": "neutral", "cssVariables": true },
  "aliases": { "components": "@/components", "utils": "@/lib/utils" }
}

The CLI removes a comment above a file's first import, so each item keeps its header below the imports. Import it in src/admin.tsx and name the slot, replacing the sample collection, field, View and operation with your own:

tsx
import { AdminCell } from "./admin/admin-cell";

export default defineAdmin(admin, { components: { "cell:requests.status": AdminCell } });
ItemSlotsSurface
admin-cellcell:view:<view>.<column>both (also reusable as a browser collection cell)
admin-inputinput:<schema>.<field>browser
admin-record-actionsrecord.actions:<schema> (runs an operation with useOperation)browser
admin-record-asiderecord.aside:<schema> (lists a View's rows with useView)browser
admin-sidebarSidebar (renderDefault with a footer link)browser
admin-homeHome (renderDefault under a section that reads a View)browser

Each file starts with a comment naming its surface and rules, and imports only react, @aotter/mantle-admin/app and @aotter/mantle-admin/hooks. A both component also renders in the Admin MCP App, inside a host's sandboxed iframe, so keep it to these rules:

  • Read data only through useView and useOperation; useEntries, useEntry, useCollectionTotal, useMe and useBadges read unavailable there. Declare what a card needs as an admin View.
  • Link only with <AdminLink> or useOpenLink(); never window.open or target="_blank".
  • No fetch, no document.cookie or localStorage authentication, no remote origin that the host's CSP does not allow, and no assumed width.
  • Where useOperation returns no run (a host without server tools), show the control disabled with a note rather than throwing or vanishing.
  • Branch on useAdminSurface() (or the surface prop) if you must, and render something sensible in both branches.