One mechanic: click anything with depth and a panel opens to the right. The parent stays.
No pages, no modals, no tabs. A serializable stack of panels (Miller columns, evolved) with pinned references that survive navigation, URL-synced state, and a registry that sizes every panel. Stax is as much a design system as a navigation model: a complete WhitePaper UI language (serif display, mono data, one accent and its ramp, exact interior margins, stroke icons, six mandatory states) shipped as tokens. Change the accent in Settings and the whole system follows.
Live demo: stax-agentik-oss-projects.vercel.app
The URL hash IS the workspace, and it reads like a path you could have guessed. Every link below opens the panels it names, on the live demo, and a committed test drives all of them so a dead one cannot ship:
| Link | What it opens |
|---|---|
#/analytics/blotter |
An execution blotter: a fill stream, notional by venue, a cost block in bps |
#/analytics/cfo |
A monthly close: the P&L walk from revenue to EBITDA, plan against actual |
#/console/keys |
API keys: masked secrets, reveal once on creation, roll and revoke as row actions |
#/crm/acme/jo/refonte |
Four panels deep in one URL: accounts, account, contact, opportunity |
#/studio/terminal |
A terminal as a panel: mono scrollback in the body, the prompt in the foot |
#/canvas |
A Figma-class whiteboard as a panel; nodes drill to inspectors |
#/data |
Airtable-class tables where every row opens as a Notion-class page |
#/moonbase/scorecard |
A weighted rubric: criteria in bands on a discrete rail, total and verdict derived |
#/moonbase/reconcile |
Two sides that should pair: a confidence whose weight scales, and the reason for it |
#/moonbase/killcheck |
The verdict first, then the gates that produced it. One blocking failure kills it |
#/moonbase/entry |
The front door is a panel: three entrances, one identity, one component |
#/moonbase/refusal |
Denial as a designed state, each refusal with what it enforces, leaks and traps |
Old links never rot. The previous type~key form and the original
percent-encoded JSON both still decode, and they upgrade themselves: open one and
the address bar rewrites to the short form.
| Path | What it is |
|---|---|
frameword/packages/panels-core |
The pure TypeScript engine: reducer, intents, laws, the URL codec. Zero dependencies |
frameword/packages/panels-react |
React bindings: provider, registry, URL sync, persistence |
frameword/apps/crm-specimen |
The specimen app: the WhitePaper design system, a component gallery, 86 dashboard blocks with live demos, a multi-board canvas, notes and tasks, data tables, the agent drawer, four financial surfaces |
frameword/packages/stax-migrate |
The migration engine: a zero-dependency CLI that drives a full refonte of any legacy app to the panel grammar via Claude Code or Codex, gated mechanically |
frameword/packages/create-stax-app |
The app installer: picks a template (next by default, vite for the original starter), vendors the engine at a commit it resolves first, and records it all in stax.lock.json |
frameword/templates/next |
The Next template: App Router + Convex behind the single src/graph.ts seam, all ten head layouts, both hosts. Builds green before any Convex deployment exists |
frameword/templates/starter |
The Vite + React template, the original starter, scaffolded with -t vite |
frameword/templates/overlays/clerk |
The opt-in Clerk auth overlay, applied over the template by --clerk |
DESIGN-SPEC.md |
The pixel contract: panel anatomy, interior margins, type and number laws, the accent ramp, and the conversion tables |
agents.md |
The working contract for a coding agent: where to add a panel, the URL rules, the design laws, the exact gate commands |
frameword/apps/crm-specimen/e2e |
The committed regression suite: the design laws as 50 Playwright specs, run in CI |
git clone https://github.com/agentik-os/stax
cd stax/frameword
bun install # workspaces: panels-core, panels-react, crm-specimen, stax-migrate
bun run test # 142 tests: the engine laws + the CLI cases
bun run dev # the specimen at http://localhost:5799Use bun run test, not bare bun test: the latter sweeps the Playwright e2e
specs into bun's own runner, which cannot execute them.
The 50-spec regression suite runs against a built preview, or against any running
app via BASE:
cd apps/crm-specimen
bunx vite build && bunx playwright test -c e2e/playwright.config.ts # preview on :4173
BASE=http://localhost:5799 bunx playwright test -c e2e/playwright.config.ts # or a live URLProduction build of the specimen: cd apps/crm-specimen && bunx vite build
(static dist/, deploy anywhere).
node frameword/packages/create-stax-app/index.mjs my-app -t next # the default; -t vite for the original starter
cd my-app && bun install && bun dev--clerk layers the auth overlay on top, --from <checkout> scaffolds from a
local clone instead of fetching, and every scaffold writes stax.lock.json
recording the exact commit it vendored.
Either template gives you the shell (provider, stage, panel anatomy, drills,
foot), the vendored engine under packages/ as editable source, and
DESIGN-SPEC.md, the same contract this repo enforces. The next template is
App Router with Convex behind the single src/graph.ts seam: it renders a
local seed graph before any Convex deployment exists, and the one-time backend
step lives in NEEDS-OPERATOR.md. The vite template is the original starter:
edit src/domain.ts and the app grows panels.
The engine is two packages, a pure reducer and thin React bindings:
import { WorkspaceProvider, useWorkspace } from "@frameword/panels-react";
// 1 · the registry maps every panelType to a width class (S 380, M 480, L 640, XL 800)
const REGISTRY = { space: { size: "L" }, account: { size: "M" }, contact: { size: "M" } };
// 2 · wrap the app. URL sync and localStorage persistence are built in.
// slugCodec is optional: without it the URL falls back to the type~key form.
<WorkspaceProvider registry={REGISTRY} urlSync storageKey="my-app" slugCodec={slugCodec}>
<Shell />
</WorkspaceProvider>;
// 3 · drive it with intents. The entire UI derives from WorkspaceState.
function Row({ panelId, account }) {
const ws = useWorkspace();
return (
<button onClick={() =>
ws.openDetail(panelId, { panelType: "account", resourceKey: "acc:" + account.id })}>
{account.name} {/* opens the next panel to the right, the parent stays */}
</button>
);
}Seven intents cover everything: openSpace, openDetail, pinPanel and
unpinPanel, closePanel, navigateTo, openPath. mod+Z and
shift+mod+Z undo and redo every workspace intent out of the box.
For a backend instead of localStorage, pass storage={yourAdapter}
({ load(), save(state) }). Async loads reconcile UNDER the current URL: the
URL's thread always wins.
import type { StorageAdapter } from "@frameword/panels-react";
const convexAdapter = (client, userId): StorageAdapter => ({
load: () => client.query(api.workspace.get, { userId }),
save: (state) => void client.mutation(api.workspace.put, { userId, state }),
});encodeLocation and decodeLocation take an optional SlugCodec. The engine
knows a slug is a string and never what "blotter" means; the app supplies the
mapping (see frameword/apps/crm-specimen/src/slugs.ts for a worked one). Four
rules: the space is named by its public slug, the space root is never written, a
node's slug is its key with the namespace stripped, and on a collision the most
public surface wins the bare word while the others keep a qualified form. Three
forms decode and they can mix inside one URL, which is what keeps every old link
alive.
A panel's head is a data plan, not a hardcoded render: headPlan(layout, ctx) is
a pure function and the Panel just renders what it returns. Ten treatments ship
(dense-bar by default, then echo, no-subtitle, bar-title, focus-only,
scroll-collapse, first-run, spine, density, editorial), selectable per
device in Settings. The default moves the identity into the panel bar and gives
the body back about 89px, taking panel chrome from 28% of the height to 13%.
The data grammar can express anything, which is exactly the trap. A position book
genuinely is a table. An execution blotter is a fill stream plus a cost block, a
treasury is a cash ladder with a reconciliation state, a monthly close is the P&L
walk. Figures are derived and reconcile across panels, the sign inverts on cost
lines, and state comes from tokens so the comparison survives dark mode. No chart
library: bars are divs. Live at #/analytics/blotter, #/analytics/treasury,
#/analytics/cfo.
The grammar can express anything, so the failure mode is to point a table at
every domain and call the flexibility a decision. stax-migrate shapes is the
router that prevents it: 26 shapes keyed on what the data IS rather than what
the legacy screen was called, each with the grammar to build, the capability it
demonstrates, the anti-pattern it prevents, and a live panel to copy.
node $CLI shapes # the whole router
node $CLI shapes "events in time" # or describe the data| the data is | shape |
|---|---|
| a row set the user EDITS | grid |
| events in time | stream |
| entities compared by magnitude, each with a state | ladder |
| a computation descending to a result | walk |
| weighted criteria rolling to a decision | scorecard |
| two sets that should pair up but do not yet | match |
| one total split by an ownership key | allocation |
| ordered checks where one failure stops the thing | gate |
| a session the user drives by typing | scrollback |
| the front door, and the state a product is judged on | entry, refusal |
Fifteen more in the catalog. The rule the whole thing exists for: grid is ONE
shape of twenty six, correct only when the user edits the rows. If the first
question is "what happened", "how do these compare" or "where did we land", the
answer is stream, ladder or walk, never a table.
The workspace is an API. window.stax exposes the serializable state and every
intent (getState, find, open, actions, act, pin, close, undo,
redo), the same action registry the foot and the palette read. The demo
drawer's slash commands (/open acme, /actions, /run new-row, /undo) are
that bridge, spoken. The full agent contract is agents.md.
⌘K opens the palette. "Open devtools" inspects the live state and time-travels
the intent history; "Copy workspace link" gzips the full arrangement into
#ws=...; ? prints the keyboard map; "Play the tour" has the bridge
demonstrate the grammar on itself.
stax-migrate is a program, not a prompt. It rebuilds a legacy app on the panel
grammar and retransforms its UI at pixel granularity, via Claude Code or Codex,
in gated phases, at an integration level you contract up front, so a migration
can never end up "10% integrated and quietly done".
| level | you get | the gates accept |
|---|---|---|
full (default) |
100% integrated, everything migrated, old UI purged | migrated only |
standard |
every row terminal; legacy surfaces may stay embedded | migrated, wrapped or deferred with cited reasons |
starter |
your chosen core spaces at 100% | migrated, out-of-scope with cited reasons |
shell |
the Stax shell wraps the app, every route panel-reachable | migrated, wrapped with cited reasons |
CLI=$(pwd)/frameword/packages/stax-migrate/index.mjs # not published to npm: call it by path
node $CLI init <your-legacy-app> --level full
node $CLI run <your-legacy-app> --agent claude # drive ONE phase (or codex)
node $CLI status <your-legacy-app> # three coverage matrices
node $CLI contract <your-legacy-app> # the honesty check, exit 1 on breach
node $CLI proof <your-legacy-app> # the layout proof (see below)node $CLI audit transfer <legacy> --legacy-url <old> # BEFORE: ready to move? what is lost?
node $CLI audit stax <app> --url <live> # DURING: does it obey its own laws?
node $CLI audit cohesion <app> --url <live> # AFTER: do matrices and product agree?The mechanical half runs the real gates and records, per probe, the exact
command, its exit code and its captured output. The forensic half is the
staxaudit skill, which reads a file it cannot fake. A protocol that only
reasons produces phases nobody ran.
unreachable is an ABORT and never a score: an app that was not running has no
verdict, and reporting it as a failure slanders the app while hiding that nobody
looked. Generic audits (/uiuxaudit, /a11yaudit, /flowaudit and six more)
load skills/staxaudit/STAX-LENS.md when they detect a Stax target, so they
stop reporting the framework's own contracts as defects and start auditing the
mechanic instead.
stax-migrate proof renders every matrix row as a panel at the size that row
declares, under the source it was transcribed from, and publishes its own
coverage. Seven disciplines, enforced by the generator rather than asked for in
prose:
- Every render carries its row. No citation, and it draws as furniture.
- The unknown is DRAWN as unknown, and its caption says what would settle it. An empty cell is never skipped and never filled with a plausible guess.
- A disputed value is never silently picked. Two matrices describing one surface differently are both shown, and the disagreement is counted.
- A placeholder is dashed, so measured never looks like invented.
- Every substitution is declared: what, what for, why.
- The counts are published. A proof that does not count itself is a brochure.
- The invariant is proven by repetition, once per manifest.
Run it at phase 4 and again at phase 8. The first count is large and that is the correct output: it IS the work list. The second must be lower, because a count that never falls is a conversion that filled its status column and learned nothing.
Nine phases run recon, the feature and data matrices, the UI inventory, the mapping passes, the scaffold, the migration batches, an adversarial coverage re-crawl, and acceptance. Three matrices are the law: every feature and sub-feature is a row (F-012, F-012.1), every visual atom is a row (E-041 is one icon, one card style, one spacing value), and every table and server function is a row bound to the panel that reads it and the foot action that writes it. The pipeline refuses to complete while a single row of any matrix blocks the contracted level, and every skipped row must carry its reason.
Other things the CLI does:
node $CLI patterns "api key" # 20 proven SaaS-admin screens, with a live reference panel
node $CLI data scan <dir> --write # extract tables, functions, rpc, realtime + call sites, with file:line
node $CLI data check <dir> # exit 1 until 100% of the backend is bound
node $CLI parity --url https://your-app.example # the 100% transfer gate, capability by capability
node $CLI verify --url <live> --themes light,dark # Playwright-scan the real page for the design laws
node $CLI doctor <dir> # adoption health: contract, pending upgrades, token drift
node $CLI theme --from "#e11d48" # a full OKLCH accent ramp, both themesThe backend survives untouched. Convex, Supabase, Prisma, REST and tRPC all keep working, because panels are a view grammar over your existing queries and mutations. The scan is programmatic (80%), the AI only maps each row to its reading panel and writing action (20%).
Already on Stax? You never re-migrate. upgrade diffs your project against
the versioned catalog (U-001 to U-040), each unit a DETECT / APPLY / VERIFY brief
an agent applies and you record after checking the evidence:
node $CLI upgrade . # applied vs pending
node $CLI upgrade plan U-040 . # print one unit's brief
node $CLI upgrade run . --agent claude # apply the next unit
node $CLI upgrade done U-040 . # record it (operator-gated, never the agent)The same intelligence is browsable inside the app at
#/prompts: ten master
prompts, M1 to M10, covering the forensic inventory, the grammar mapping, the
design integration, the enterprise strangler and the agent bridge.
- Opening a space replaces the active thread; pinned references ride across it.
- One action zone per panel, the foot. Never a floating button.
- Parents stay visible, depth reads left to right, ⌘K goes anywhere.
- State is a serializable list; the URL, the crumbbar and the agent context all derive from it.
- Tokens only. Change the accent and the whole system follows.
MIT. Built by Agentik OS.