Guidance for Claude Code when working in the admin app inside the mx-core monorepo.
This is the MX Space admin dashboard — a React 19 SPA (package @mx-admin/admin).
It lives at apps/admin within the mx-core monorepo and is built locally during the
core build/release; it is no longer downloaded from GitHub releases. The built output
(apps/admin/dist) is served by the sibling backend app apps/core under the route
/proxy/qaqdmin. Built with Base UI primitives, React Router (HashRouter), TanStack
Query, Sonner, and a Tailwind v4 layer.
Run from the monorepo root (the workspace install is governed by the root):
pnpm -C apps/admin dev # Start Vite dev server (opens browser automatically)
pnpm -C apps/admin build # Production build → apps/admin/dist
pnpm -C apps/admin lint # oxlint
pnpm -C apps/admin lint:fix # oxlint --fix
pnpm -C apps/admin typecheck # tsc --noEmitEquivalently, target the package directly: pnpm --filter @mx-admin/admin run build.
Scope checks to changed files only — never run lint/typecheck/build over the whole
tree just to verify a small edit. One-off file typecheck:
pnpm -C apps/admin exec tsc --noEmit --pretty false.
Env vars live in apps/admin/.env (see apps/admin/.env.example). VITE_APP_BASE_API
is the mx-core API endpoint. All vars are optional and fall back to empty/same-origin,
so a build with empty env will not crash.
- React 19 + TSX, react-compiler enabled via Babel (
@rolldown/plugin-babel) - Base UI (
@base-ui/react) — headless primitives; UI wrappers live inapps/admin/src/ui/ - React Router 7 (
HashRouter) —apps/admin/src/routes.tsxmaps route → lazy view - Tailwind v4 via
@tailwindcss/vite(@import 'tailwindcss'insrc/index.css) - TanStack Query — created in
apps/admin/src/query-client.ts, mounted inproviders.tsx - Sonner — toast layer mounted alongside the query provider
- Socket.IO —
src/socket/SocketBridgehangs off the authenticated shell - better-auth + passkey for login; auth gate in
App.tsx(checkLoggedquery) wraps everything except/setup,/setup-api,/login
main.tsx → App.tsx (mounts providers, HashRouter, auth gate, installs theme tokens
via installThemeTokens) → admin shell (nav chrome + SocketBridge + routes). All views
in routes.tsx are lazy()-loaded and wrapped in <Suspense>; add new pages by
registering a lazy import there.
import { something } from '~/utils/...' // ~ → apps/admin/srcAPI services use the fetch-based helpers in apps/admin/src/api/http.ts.
When using TanStack Query, extract arrays with:
select: (res: any) => Array.isArray(res) ? res : res?.data ?? []Error Classes:
BusinessError— application-level errors (4xx responses)SystemError— network/server errors (5xx responses, network failures)
phone:— max-width: 768pxtablet:— max-width: 1023pxdesktop:— min-width: 1024px
After modifying code, run focused type checking and linting on the changed files only. Run a production build before reporting completion for broad application changes.
All gray colors MUST use neutral instead of gray to match the Vercel-style design:
- Good:
text-neutral-500,bg-neutral-800,border-neutral-200 - Bad:
text-gray-500,bg-gray-800,border-gray-200
This rule applies only to raw color usage. For app surfaces, foregrounds, and borders prefer the Design System v2 semantic tokens documented below.
Do NOT use arbitrary font sizes (e.g. text-[11px], text-[13px]). Use standard
Tailwind classes:
| Purpose | Class | Size | Use Case |
|---|---|---|---|
| Page title | text-2xl |
24px | Main page titles |
| Section title | text-xl |
20px | Section headers |
| Card/Modal title | text-lg |
18px | Card titles, modal headers |
| Secondary title | text-base |
16px | Sub-headings, stats |
| Body text | text-sm |
14px | List items, form labels, buttons |
| Metadata | text-xs |
12px | Timestamps, badges, descriptions |
The admin uses a token-driven Design System v2 (Notion-warm aesthetic, larger radii,
cobalt accent). Tokens live in apps/admin/src/styles/tokens.css (@theme block plus
.dark { … } overrides). Spec: docs/superpowers/specs/2026-05-30-admin-ui-softening-design.md.
Three-layer surface model. Always reach for these tokens before raw bg-white /
bg-neutral-*.
| Token | Tailwind | Purpose |
|---|---|---|
--color-surface-page |
bg-surface-page |
Outer shell, html background |
--color-surface-card |
bg-surface-card |
Primary content containers |
--color-surface-inset |
bg-surface-inset |
Empty states, code blocks, hover tint, in-card panels |
--color-surface-overlay |
bg-surface-overlay |
Popovers, dropdowns, tooltips |
| Token | Tailwind | Use |
|---|---|---|
--color-fg |
text-fg |
Main copy, titles |
--color-fg-muted |
text-fg-muted |
Labels, sub-copy, meta |
--color-fg-subtle |
text-fg-subtle |
Placeholders, disabled, decorative dots/icons |
| Token | Tailwind | Use |
|---|---|---|
--color-border |
border-border |
Card edges, inputs, dividers (soft hairline rgba) |
--color-border-strong |
border-border-strong |
Hover, active, focus emphasis |
Warm cobalt blue. Used for primary CTAs, links, focus rings, selected-row tints.
| Token | Tailwind | Use |
|---|---|---|
--color-accent |
bg-accent, text-accent, ring-accent |
Primary CTA bg, link/focus color |
--color-accent-hover |
bg-accent-hover |
Hover for primary CTA |
--color-accent-soft |
bg-accent-soft |
Selected row tint, soft accent fills |
| Tailwind | Value | Use |
|---|---|---|
rounded-xs |
6px | Tiny chips |
rounded-sm |
8px | Buttons, inputs, dropdown items |
rounded-md |
10px | List rows, code blocks, large inputs, tooltip cards |
rounded-lg |
12px | Cards, modals, drawers, popovers |
rounded-xl |
14px | Empty states, hero cards |
rounded-full |
pill | Status pills, tags, badges |
| Tailwind | Use |
|---|---|
shadow-xs |
Input rest, ghost lift |
shadow-sm |
Cards, panels, mobile row cards |
shadow-md |
Popovers, dropdowns, chart tooltip |
shadow-lg |
Modals, drawers |
Single rule, applied via:
focus-visible:outline-hidden focus-visible:ring-[3px] focus-visible:ring-accent/15
No ring-offset-*. The 3px soft-glow replaces the older 2px solid frame.
Import from ~/ui/data/StatusPill. Six semantic tones — use only for these states:
live,draft,error,scheduled,archived,pending
import { StatusPill } from '~/ui/data/StatusPill'
<StatusPill tone="live">Published</StatusPill>For app-specific labels (whispers, language tags, role badges, etc.) keep inline <Badge />
or custom chips — StatusPill is reserved for the six standard publication-lifecycle tones.
Import from ~/ui/patterns/EmptyState for standard "nothing here" surfaces:
import { EmptyState } from '~/ui/patterns/EmptyState'
<EmptyState
icon={<Inbox className="size-6" />}
title="No drafts yet"
description="Drafts you save will appear here."
action={<Button>Write something</Button>}
/>Layout is --surface-inset background, rounded-xl, icon tile on --surface-card
with shadow-xs. Two-line copy (title + helper), optional CTA below.
--color-primary, --color-primary-shallow, --color-primary-deep remain as
aliases pointing at the new accent palette so older code keeps working. They are
written by installThemeTokens() in src/theme.ts. New code uses
--color-accent* directly; do not introduce new --color-primary* references.
StatusPill'sarchivedtone usesneutral-{100,700}(admin convention) instead of the spec'sgray-{100,600}. This is intentional — admin neutral-over-gray rule applies even to the pattern.- Inter is loaded via Google Fonts CDN (
@import url(...)inindex.css) rather than self-hosted, per the PR1 deferral. Self-hosting (Latin subset, ~80KB local woff2) is a future polish. - Sidebar nav has a right-aligned tabular count column plumbed in the component, but the route data model does not yet carry per-route counts. Populate when the model surfaces them.
New admin views must follow the master-detail / content-layout convention. The reusable
shells live in apps/admin/src/ui/layout/:
content-layout.tsx— list+detail pages (comments, drafts, topics)page-layout.tsx— page shell with header- companion pieces in the same dir (
header-back-button.tsx,sidebar-*,resize-handle.tsx)
apps/admin/vite.config.mts— Vite + react-compiler + Tailwind + checker;baseusesVITE_APP_PUBLIC_URLin production (empty = relative paths, the safe default); the html plugin injectsWEB_URL/GATEWAY/BASE_APIintowindow.injectDataapps/admin/src/theme.ts— CSS token installation for the shellapps/admin/src/index.css— global stylesheet + Tailwind layerapps/admin/src/constants/env.ts— resolves API/web/gateway URLs (injected env first, thenVITE_APP_*)
Two channels publish the dashboard (full detail in ../../docs/admin-monorepo-migration.md):
- With a core release (
v*tag):release.ymlbuilds admin, bundles it into the server zip + Docker image, and publishes it to Cloudflare R2. - Independently (admin-only fix, no core release): run
../../scripts/release-admin.sh [patch|minor|major]— bumpspackage.json, tagsadmin-v*, andadmin-release.ymlbuilds + publishes to R2.
The version baseline is 8.x+ (above the retired GitHub channel) so a freshly bundled
build supersedes any copy previously downloaded into the server's data directory.
The AI agent chat surface lives under apps/admin/src/features/write/components/agent/
and apps/admin/src/api/ai-agent.ts. After the pi-ai migration:
- Transport —
apps/admin/src/api/ai-agent.tsconsumes the JSON-framedAiAgentSseEventunion via the shared TypeBox schema imported from the neutral@mx-space/aipackage (packages/ai/src/ai-agent-sse.ts). Each SSE line is a singledata: <json>\n\nevent — there is noevent:prefix line. The transport parses each frame and dispatches typed events to the session manager. - Session manager — buffers a draft
AssistantMessageper turn, accumulating text/thinking blocks bycontentIndex. Tool-call blocks are ONLY committed ontoolcall_end; partialtoolcall_deltais dropped on abort or network drop. - Multi-block rendering —
MessageBubblerenders text, thinking, and toolcall blocks in monotoniccontentIndexorder. Toolcall events for haklex (insert_node/replace_node) wire back to the lexical editor via a callback prop, NOT global state. - Network drop — the transport surfaces a
connection lostUI without crashing. Covered byapps/admin/src/features/write/components/agent/*.test.tsx(jsdom vitest integration tests + a 50-frame interleaved fixture). - Provider config —
AIProviderDrawerexposes 3 provider types (OpenAICompatible,Anthropic,Generic), with a modelComboboxsourced fromGET /api/ai/registry/models(10-minute stale, build-hash cache key). Unknown legacy localStorage values (openai,openrouter) are rewritten toopenai-compatibleon app boot byapps/admin/src/bootstrap/migrate-legacy-provider-type.ts. ThecontextWindowandmaxTokensnumeric inputs only render when the typed model id is NOT in the registry (case-insensitive trim match).
See apps/core/CLAUDE.md for the server-side wire-format invariants.
- apps/core — backend API server (NestJS), the sibling workspace app. Serves the built
admin under
/proxy/qaqdminand reads it from disk at<assetRoot>/index.html. - Shiroi — Next.js blog frontend, located at
../../Shiroi(relative to repo root). - haklex — rich editor packages (
@haklex/*), consumed as published dependencies.
Rich editor work is integrated as ordinary React components.