Two things left: make the panel look like your product, and prove it works.
Brand and accent
The panel’s own declaration carries the name and accent:
export default Panel.make('admin')
.path('/admin')
.guard('web')
.login({ model: () => import('#models/user') })
.brand({ name: 'Blog Admin' })
.resources(registry)accent is expanded at runtime into an eleven-step ramp
(--adonia-accent-50 … --adonia-accent-950) by a pure function that runs
identically on server and client, so the server-rendered HTML already carries
the finished ramp and hydration never flashes. Named Tailwind colours, hex,
rgb() and oklch() are all accepted; anything unparseable falls back to the
default accent rather than blanking the admin.
The ramp keeps your hue and chroma but imposes a fixed lightness scale — which
is what guarantees accent-50 is always a tint you can put text on and
accent-600 is always a surface accent-50 reads against, whatever colour you
picked.
Tokens
Every visual decision reads a --adonia-* custom property; no component
hard-codes a colour, radius or spacing step. Override them anywhere in your own
CSS, or from the client entry:
// inertia/adonia.ts
import { adonia } from '@adonia/ui'
export default adonia({
theme: {
darkMode: 'class',
cssVariables: {
'--adonia-radius': '10px',
'--adonia-density': '0.28rem',
'--adonia-font-family': '"Inter var", system-ui, sans-serif',
},
},
})The spacing scale is calc(var(--adonia-density) * n), so that one density line
takes the whole panel from comfortable to compact. darkMode picks a strategy:
'system' (the default, pure CSS), 'class' (a user toggle persisted to
localStorage, exposed through useColorScheme()), or 'off'.
Two slots let you add chrome without ejecting anything:
adonia({
registry: (r) => {
r.slot('topbar.right', ThemeToggle)
r.slot('sidebar.footer', StorageMeter)
},
})If a page needs more than that, node ace adonia:eject resource_index swaps the
thin re-export for the full source and you own it from then on. Field- and
cell-level customisation is a smaller hammer: r.field('app/map-point', MapPoint)
registers a component for a descriptor type, and node ace adonia:field writes
both halves for you.
Finally, the Tailwind preset maps the tokens onto theme keys so your own markup can use them:
// tailwind.config.ts
import adonia from '@adonia/ui/tailwind'
export default { presets: [adonia] }Utilities land as bg-adonia-surface, text-adonia-text-muted,
rounded-adonia, p-adonia-4. Every value is a var() reference, so changing
the accent or the colour scheme repaints without regenerating a class.
Run the panel’s tests
cd examples/blog-admin
node ace.js testTests 82 passed (82)The suite that matters for everything you just built is
tests/functional/adonia_tutorial.spec.ts — one group per chapter of this
tutorial, driven through the real HTTP stack:
| Group | Asserts |
|---|---|
| the panel and its navigation | brand, nav order and grouping, every resource serves its index |
schema, validation, inputErrorsBag |
grid root, flat state keys, per-field error bag, relation existence |
| the table | declared columns and filters, materialized relation cells, filter/search/sort behaviour |
| authorization | per-row can divergence, and a 403 on the write that ignores it |
| relations | picker targets and options endpoint, pivot diff on create and edit |
| the image field | node rules and urls.upload, a rejected upload, a promoted one |
| reactivity | the reactive block, server re-derivation, prune-and-require mirroring |
Write yours the same way — through the panel, not around it. For the parts that
do not need a server, @adonia/core/testing compiles a resource in-process:
import { compileFor, validateWith, assertFillOmits } from '@adonia/core/testing'
const descriptor = compileFor(resource, 'create')
const { errors } = await validateWith(resource, 'create', { title: '' })
await assertFillOmits(resource, 'edit', { status: 'draft', publishedAt: '…' }, ['publishedAt'])Those run in milliseconds and are the right place to pin a validation rule or a mirroring invariant; keep the HTTP suite for the wiring. See testing for the whole harness, and CI for the pipeline that runs both.
Where to go next
- Resources — every static and hook the class supports.
- Fields reference — a page per field type.
- Filters and search — custom filters, search drivers, cursor pagination.
- Error model and observability — what a failure looks like and what it emits.
- Protocol v1 — the wire format, if you are building a custom field or a second client.
Actions and widgets render through the same restrained chrome and token set.
Use topbar.right and sidebar.footer for app-specific additions. Continue with
tenancy and nested resources for
tenant switching and parent-scoped navigation.