graview-pages-design
graview-pages-design is one of Graview's 31 authoring skills: instructions an AI coding assistant such as Claude Code or Codex reads before it changes a Graview app. What it teaches: Give a Graview app a product design over its routed face — its own shell under the one bar, every surface replaced, its tabs lit from the section a kind's pages are in, and a trail that says where a person is — and verify it by rendering each page and opening it, rather than assuming the derived pages are gone.
Installed into a project by graview skills install ., and read by whichever assistant is working beside you.
The skill
A product design over the pages face
graview-pages takes the routed face from derived to one page in the app's own words. This is the rung after: the registry's three surfaces and two page types, all replaced, read as a product rather than an admin panel.
createPageRegistry<S, PageComponent<S>>(schema)
.surface("shell", Shell) // the design's own, under the bar
.surface("home", Home)
.surface("problems", Problems)
.register("plot", "list", Plots).register("plot", "record", PlotRecord)
.register("gardener", "list", Gardeners).register("gardener", "record", GardenerRecord)
.section(["plot", "planting"], "The grounds", { path: "/places/the-grounds" })
// ... every kindThe shell surface receives { context, children }. apps/todo/src/ui/design.tsx is the finished example: all a person tries there works: sort, filter and group come from ArrangeBar and live in useSearchParams in the shared words (a list you arranged is a link); a record edits where it is shown, heading included, through editableFields(store, id); an act's form opens where the act is, prefilled, with the store's own refusal said at the press; the problems page is an inbox, its repairs ordered by rankedRepairs; and every empty state offers the way out of itself. apps/seedbed/src/ui/design.tsx is the other end: an almanac drawn on the page and in the scene's lens from one model.
Where a person is
A design usually replaces some pages and leaves the rest derived, and a person who follows a link into a derived page must still know where they are. The face says it; a design never rebuilds it by matching pluralSlug against the path.
registry.section(kinds, label, { path })names the part of the app a kind's pages belong to — its list, its records, its pictures — as the design's own tabs name it.pathis the section's own page, basename-relative; without one the trail says the section and links nowhere.registry.sectionOf(kind)reads it back.usePlaceHere(), in anythingPagesAppdraws (the shell, a page, a route): `{ as: "home" | "list" | "record" | "place" | "page", kind?, id?, label, section?, trail: [{ label, href?, path?, current? }] }`. Light a tab fromsection(orkind), draw a title fromlabel. Each step'shrefcarries the face's base;pathis basename-relative, for<Link to>andnavigate().placeHere(context, path, { registry, base })says the same of a path for a bar drawn OUTSIDE the face's router, told where it is bysteering.atoronNavigate.- Its own tabs, in the arrangement's order. `arrangedEntries(kinds, pages.order, views.places())
(@graview/core`) is the kinds' lists with a host's view namedview:<name>at its place among them; leave out a kindpages.hidenames unless it is where the person is. - The trail. Each derived list, record and picture opens with one: a
navcalled "You are here", the section and the kind's list as links, the page itself last witharia-current="page", out of sight because the heading says it. A list with no section draws none. A shell that places<PageTrail />puts THE trail there and the derived pages draw none at their heads; one that draws its own fromusePlaceHere()sayssurface("shell", Shell, { without: ["trail"] }).
Find and the way back, in the design's bar
Both are the face's, whichever shell draws it: a shell that places <PageFind> or <PageUndo> says where they go, else they are drawn around it (Find in a bar above, the way back docked at the corner), and surface("shell", Shell, { without: ["find", "undo"] }) goes without.
<nav className="my-bar">
<PageFind context={context} narrowsLists={false} size="sm" className="my-find" />
<PageUndo context={context} compact size="sm" className="my-undo" />
</nav>compactis the arrow and "Undo": what it takes back ("Take back “Rename to …”") is its accessible name and its title. Uncompact, the sentence is cut at 18rem (--graview-undo-widest), whole in its title.size:"sm"(30 px, the app bar's tools),"md"(36 px), or the bar's own height — a number of pixels or a CSS length — said as--graview-control-height.classNamegoes on Undo's button and on Find's form (its box is.graview-find-box). Their own look is rules behind:where, so one class of the design's outweighs it: never!important, never a CSS::afterover Undo's words.
What every design must keep doing
- Read the graph through one model.
readGarden(store)turns nodes, edges and violations into the design's words; every page reads it and none reaches forstore.graph. The scene's lens reads the same model. - Show nothing the graph does not hold. Copy that changes with the app — a pitch, a chart's tags, a section's words — is records of a kind, seeded, never a JSON file the design imports:
graview checknames a page that reads one (face-reads-content-beside-the-graph), and Find, Ask and the scene see records alone. - Take the act list from the derivation. `recordFacts(store, id, { principal }).actions
andkindFacts(store, kind, …).actions`, never a scan ofstore.allMutations();withheldare struck through with the policy's sentence (store.permitssays the same at the press), not dropped. - Style through the theme's tokens (
--graview-ground,-panel,-ink,-edge,-warn,-accent,-font-display,-font-body), tinted withcolor-mixfor the design's own paper. Never a color that works in one scheme only — nor a logo:<AppMark brand={context.brand} />draws the brand's, the dark one in the dark (graview-logo). - Keep landmarks and targets honest. One
main— asectionwhencontext.embedded, and UNNAMED, or a page holding two embeds has two regions with one name. Controls at least 24px, AA contrast. Every size inrem: at 200% text a flex or grid item's automatic minimum is its CONTENT's, so one un-wrappable row pushes the whole column off the screen —minmax(0, 1fr)andmin-width: 0on the column,flex-wrapon the row. Measure rather than trust:-ink-faintfails AA on an 11px label, and the accent fails on the warning ground. `node scripts/verify-pages.mjs` runs axe over every route, both widths, both schemes. - Say where a person is on every page, derived or not, as above — one trail, not one from the design and one from the face.
Then find out whether it worked
pnpm build && npx graview check ./dist/domain/app.js # the declaration is still whole
pnpm test # render every page you registeredRender each registered page with PagesApp and initialPath (and the basename it ships under) in a test, and assert what usePlaceHere() says there and that one nav[aria-label="You are here"] is drawn (apps/seedbed/tests/integration/chapters.test.ts). Then open it: a design that passes its tests and reads like an admin panel has replaced nothing.
What the check cannot see
- Whether the design's words are the domain's. The derived pages use the declaration's
descriptionandinverse; a design that writes its own sentences must keep them true as the declaration changes. - Whether a section's label is the one a person would look for: the trail says what the registry says.
- Whether a page gives every act
facts.actionsoffers room: only a person can see that.