Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions apps/mapgen-studio/system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# mapgen-studio — Design System (extracted)

> Extracted from the current codebase (`src/**/*.tsx`, `src/index.css`, `tailwind.config.js`, `src/ui/hooks/useTheme.ts`) on 2026-06-08.
> This captures the **current** design language as-built. It is the baseline the shadcn migration must preserve (density, dark-first palette, border-only depth) while formalizing tokens.

## 1. Design DNA

A **dense, dark-first developer tool**. Think pro audio / DAW / data-explorer, not a marketing app. Information density is the point: micro-typography (10–11px), tight 4px spacing rhythm, hairline borders instead of shadows, and a restrained near-monochrome palette with a single cool accent. Map visualization (deck.gl) is the hero; chrome recedes.

- **Mood**: technical, quiet, high-density, focused.
- **Hierarchy mechanism**: borders + background-tier shifts (not shadow, not color).
- **Personality risk today**: reads as *commodity dark dashboard* — defaults dominate (gray accent, flat focus rings, no motion). The craft work is to make density feel *intentional*, not cramped.

## 2. Color

Two hand-maintained palettes (`light` / `dark`) live in **three** places that disagree (see audit): `tailwind.config.js`, `src/index.css` CSS vars, and `src/ui/hooks/useTheme.ts`. The values below are the de-facto source of truth (`useTheme.ts`, matched by hardcoded hex frequency in components).

### Dark (primary — app defaults to dark)
| Role | Value | Notes |
|---|---|---|
| bg / page | `#0a0a0f` | deck.gl backdrop tint |
| bg / panel | `#141418` | cards, headers |
| bg / panel-alt | `#111114` | |
| bg / nested | `#0f0f12` | inputs, nested cards |
| bg / hover | `#1a1a1f` | |
| bg / active | `#222228` | secondary buttons, tags |
| border / default | `#2a2a32` | **the workhorse hairline** (31 uses) |
| border / subtle | `#222228` | dividers (20 uses) |
| border / strong | `#3a3a44` | input focus border (14 uses) |
| text / primary | `#e8e8ed` | (36 uses — most frequent hex) |
| text / secondary | `#8a8a96` | labels (19 uses) |
| text / muted | `#5a5a66` | placeholders, help (14 uses) |
| accent / interactive | `#4b5563` | primary button, switch-on, active tag (19 uses) |
| accent (system var) | `#5e5ce6` | indigo — declared in CSS but **barely used** |

### Light
| Role | Value |
|---|---|
| bg / page | `#f5f5f7` |
| bg / panel | `#ffffff` |
| border / default | `#e5e7eb` / `#e5e5e5` (drifts) |
| text / primary | `#1f2937` (26 uses) |
| text / secondary | `#6b7280` (17 uses) |
| accent | `#4b5563` |

### Semantic / status
- error/destructive: `text-rose-400`, `bg-red-500`, `text-red-600` (inconsistent — rose vs red).
- success: `text-emerald-500`. info: `text-blue-500`.

**Observation:** the *declared* accent (`#5e5ce6` indigo) is not the *used* accent (`#4b5563` slate). The brand is effectively neutral-slate. This is the single biggest "commodity" tell.

## 3. Typography

- **Sans**: Inter (400/500/600/700). **Mono**: JetBrains Mono. Both via Google Fonts `@import` in `index.css` (render-blocking — flag).
- **Scale (actual, by frequency):**
| Token | Size | Uses | Role |
|---|---|---|---|
| — | **11px** | 44 | body / field text / values (the default) |
| — | **10px** | 38 | labels, uppercase eyebrows, captions |
| — | 12px | 7 | section subtitles |
| — | 13px | 4 | |
| text-sm (14px) | 14 | 4 | dialog titles |
| — | 9px | 3 | dense badges |
- **Weights**: `font-medium` (28), `font-semibold` (14). No bold in UI chrome.
- **Pattern**: uppercase + `tracking-wider` + 10px for eyebrow labels (e.g. AppHeader "Config", "Resources").
- **Type is set in arbitrary `text-[11px]` values**, not a named scale — every size is ad-hoc.

## 4. Spacing

- **Base unit: 4px.** Rhythm is tight.
- Scale by frequency: `gap-2`(8px, 47×) ≫ `px-3`(12px, 27×) > `py-2`(8px, 14×) > `gap-1`(4px, 13×) > `gap-1.5`(6px) > `py-2.5`(10px) > `p-2`/`p-2.5`(8/10px).
- **Effective scale: 4, 6, 8, 10, 12, 16 px.** Above 16px is rare (panel padding only).
- Field rows: `py-1` (4px) vertical, `gap-3` label↔input. Cards: `p-2.5` (10px). Nested cards: `p-2` (8px).

## 5. Radius

- `rounded` (4px, 28×) = default for inputs/buttons/tags.
- `rounded-lg` (8px, 15×) = cards, dialogs, dropdowns, toasts.
- `rounded-full` (8×) = pill tags, switch track.
- `rounded-md` (6px, 2×) = rare/inconsistent.
- **Effective scale: 4 / 8 / full.** (6px and config's `xl`/`2xl` are noise.)

## 6. Depth

- **Border-only.** 148 border utilities vs **7** shadows. Shadows appear only on floating layers (dialog `shadow-xl`, toast `shadow-lg`, dropdown `shadow-lg`).
- Tiering by **background lightness step** (page → panel → nested) + 1px hairline. This is the core depth language.
- `backdrop-blur-sm` (11×) on the floating header/setup bar — glassy chrome over the map.

## 7. Motion

- **Minimal.** `transition-colors` (29×) is ~all of it; `transition-transform` (5×) for chevron rotation; one `animate-pulse-subtle` (2s) for running state.
- Durations: 150ms (fast) / 200ms (normal), declared as CSS vars but mostly implicit Tailwind defaults.
- `animate-in fade-in-0 zoom-in-95` strings appear on dialog/toast/tooltip **but no `tailwindcss-animate` plugin is installed** — these classes are dead (no keyframes). Motion is effectively absent on overlays.

## 8. Component patterns

| Pattern | Spec (as-built) |
|---|---|
| **Button** | h-8 default / h-7 sm / h-7 icon; `px-3`; `rounded`; `text-sm`/`text-xs`; variants default(slate)/outline/secondary/ghost/link/destructive; CVA-driven. Focus ring `ring-1 ring-gray-400`. |
| **Input / Textarea / Select** | h-7; `text-[11px]`; `px-2.5`; `rounded`; 1px border; focus = border color shift + `ring-1`. Native `<select>` with custom chevron. |
| **Switch** | 36×20 track, 16px thumb, `rounded-full`, peer-checked slides; on = `#4b5563`. |
| **Checkbox** | 16px, `rounded`, Check icon on check. |
| **Field row** | `flex justify-between gap-3 py-1`; label `text-[11px] min-w-[96px]` secondary color, input flex-1. |
| **Card (stage section)** | `rounded-lg border p-2.5`, header (semibold) + `border-t` divider + content. |
| **Nested card / array item** | `rounded-md border p-2`, deeper bg tier. |
| **Tag / pill** | `rounded-full border px-2 py-1 text-[11px]`, active = slate fill. |
| **Dropdown menu** | hand-rolled: trigger button + `fixed inset-0` click-catcher + absolutely-positioned `rounded-lg border shadow-lg` list of `text-[11px]` rows. |
| **Dialog** | hand-rolled overlay `bg-black/50` + centered `max-w-[320px] rounded-lg border p-5 shadow-xl`. |
| **Toast** | bottom-center stack, `rounded-lg border shadow-lg`, icon + 12px message + dismiss. |
| **Eyebrow label** | uppercase, `tracking-wider`, 10px, muted. |

## 9. Theming mechanism (as-built — flag)

- Dark mode is delivered by a **`lightMode` boolean prop threaded through 24 files** + a `Theme` object from `createTheme()`, NOT by Tailwind `dark:` variants.
- Tailwind `darkMode` is unset → defaults to `media` (`prefers-color-scheme`). So `dark:` utilities (13 scattered uses) and the `lightMode` prop **fight each other**; a user who picks "light" while their OS is dark gets mixed results.
- `createTheme()` builds classes via **runtime string interpolation** (`` `bg-[${p.bg.page}]` ``) — Tailwind JIT cannot see these, so most of those classes never get generated. (See audit; this is a real bug, not just smell.)

## 10. Target north star (for the redesign)

Keep the DNA — dense, dark-first, border-tiered, micro-type, single accent — but:
1. Make the accent **intentional** (commit to indigo `#5e5ce6` or a deliberate slate; stop declaring one and using another).
2. Formalize the 11/10px type, 4px spacing, 4/8 radius, border-tier depth as **named tokens** (shadcn HSL CSS vars + `@theme`).
3. Replace `lightMode`-prop theming with shadcn's `.dark` class strategy (single switch, no prop drilling).
4. Add real overlay motion (`tailwindcss-animate` / tw-v4 `tw-animate-css`) so dialogs/toasts/dropdowns animate as the dead `animate-in` classes intended.
200 changes: 200 additions & 0 deletions docs/projects/mapgen-studio-redesign/00-GOAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# MapGen Studio Redesign — Active Goal & Frame

> Controlling scope for the systematic workstream. This is the closure test:
> work is done when this objective is met and its falsifier has not fired.
>
> **Controlling frame:** [`FRAME.md`](FRAME.md) (normative operating model +
> guardrails). On any conflict, FRAME → this goal → architecture → audits.
> **Status: ACTIVE (un-paused 2026-06-08).** All "PAUSED / wait-for-merge"
> sections below are **superseded** by FRAME §4 (Operating Model) and §7
> (revised phase order). They are retained only as history.

## Active goal (controlling, create-goal fallback record)

Active goal:
Rebuild `apps/mapgen-studio` into a top-1%, data-driven React 19 app —
design-system-first, decomposed architecture, real client-state vs server-data
boundaries (oRPC-native TanStack Query + Zustand), full canonical shadcn +
Tailwind v4, end-to-end types, intentional comments/organization — WITHOUT
changing map-gen, Deck.gl, recipe, or live-game behavior (parity is hard core).
Deliver as a Graphite stack of OpenSpec-backed slices, each a domino;
**never submit the stack**. Do NOT wait for the live-control stack to merge:
work off `main`, inspect the TOP of the live-control `codex/*` stack, and design
the exact studio↔control-oRPC integration seam to bind later (no FireTuner reads,
no re-implementing live reads). Build the design system FIRST (via `design:*`),
then consume it everywhere. Fully autonomous; confirm design *direction* once at
`design:init`. Falsifier: if decomposition/server migration can't preserve
parity, or "simpler" degrades the authoring workflow, or the designed seam is
incompatible with what lands on `main` — stop and tell the user.

## Active goal

Transform `apps/mapgen-studio` from a 3,010-line god-component (`App.tsx`) into a
top-1%, data-driven React 19 application: decomposed component architecture, real
state/server-data boundaries, a documented shadcn(Radix)+Tailwind design system,
accessibility + interaction craft, and comments/organization that read as
intentional — **without changing** map-generation, Deck.gl rendering, recipe
semantics, or live-game runtime behavior. Behavior parity is hard core.

## User-confirmed decisions

- **Design system:** adopt **full canonical shadcn/ui** (`components.json` + Radix),
replacing the hand-rolled primitives; formalize tokens + `system.md` via the
`design:*` skills. Go all the way.
- **State / data:** standardize server state on oRPC's **native** TanStack Query
integration (use it directly if it subsumes a separate client; otherwise
TanStack Query / React Query). **Zustand** for client / UI state. "Go all the way."
- **Serverside:** build a **native Effect + oRPC router mounted on a Bun server**,
replacing the ~660 lines of untyped Vite middleware currently in
`vite.config.ts`. Leverage native oRPC middleware/context and the native
oRPC × Effect capabilities. **Research this deeply and natively FIRST**, before
designing.
- **MANDATED bridge (user-confirmed):** use the **`effect-orpc`** library
specifically (`utopyin/effect-orpc`, v0.2.2) — handlers written as
`.effect(function* () {…})`, Effect error channel mapped to `ORPCError` via
`ORPCTaggedError`, services provided from an Effect `ManagedRuntime`/`Layer`.
It is pre-1.0 / single-maintainer: **isolate it to the `router/` layer** so the
blast radius of a breaking change is ~30 lines of manual `Effect.runPromise`
fallback. Verified install set: `@orpc/server@1.14.5`, `@orpc/client@1.14.5`,
`@orpc/contract@1.14.5`, `effect-orpc@0.2.2`, `effect@3.21.3`,
`@orpc/tanstack-query@1.14.5`, `@tanstack/react-query@5.101.0`, `zustand@5.0.14`.
- **Execution:** **fully autonomous through to a PR** (Graphite stack). No
per-gate approval required.

## Honest correction (controls scope)

The user said "stick with oRPC and Effect serverside, we already have it."
**They are NOT in the repo** — zero `@orpc/*` or `effect` imports anywhere in
`apps/` or `packages/`. The current "server" is hand-rolled `fetch`/middleware
inside `vite.config.ts`. So **oRPC × Effect × Bun is greenfield here**, not a
constraint to preserve. The intent (typed, native oRPC×Effect server) stands; the
"already have it" premise does not.

## Frame

- **In / foregrounded:** component decomposition, state & server-data
architecture, design-system formalization, accessibility & interaction craft,
intentional comments/organization, end-to-end type safety.
- **Exterior (out of scope):** map-generation algorithms, Deck.gl rendering math,
recipe semantics, game-runtime / mod behavior. The engine is correct; this is
the shell around it.
- **Hard core (never sacrificed):** behavior parity, end-to-end type safety, no
regression in the live-game control loop.
- **Falsifier:** if decomposition or the server migration cannot preserve
map-gen / live-control behavior, or if "simpler" measurably degrades the
authoring workflow, stop and re-scope.
- **Structural alternative considered & rejected:** a pure visual reskin
(tokens + polish only) — rejected because it leaves the 3k-line state tangle,
the actual ceiling on robustness.

## Method (civ7-systematic-workstream gates)

1. Deep oRPC × Effect × Bun + oRPC-native-TanStack research → canonical reference.
2. Current-state audit + design-system extraction + critique.
3. Target architecture spec + sliced redesign plan.
4. Implement in Graphite slices with worktree isolation.
5. Framed agent review (correctness, a11y, types, behavior parity).
6. Verify (typecheck / tests / build) + open PR.

## Status ledger

- [x] Gate 1 — Frame the workstream (this doc)
- [x] Gate 2 — Isolate repo state (branch `design/mapgen-studio-redesign`)
- [x] Gate 1-research — Deep oRPC×Effect×Bun + oRPC-TanStack reference (`research/01`, `research/02`)
- [x] Gate 3 — Diagnose / audit current state (`audit/03-06`, `system.md`)
- [x] Gate 4-7 — Corpus, target architecture, slice plan (`architecture/10`, `architecture/11`)
- [ ] Gate 8 — Implement slices (Phase A → G; see `architecture/11-slice-plan.md`)
- [ ] Gate 9-10 — Verify / runtime parity
- [ ] Gate 11 — Review wave
- [ ] Gate 12 — Close (PR)

## Execution log

- Branch: `design/mapgen-studio-redesign`. Strategy: one branch, one commit per
slice, one PR (not a multi-branch Graphite stack — lower risk for autonomous run).
- Committed `cae889456` (planning), `5dba36d35` (A1 contracts).

## ⚠ LIVE-CONTROL SUBSTRATE CORRECTION (2026-06-08) — Phase A/B PAUSED

User correction: **no FireTuner reads — there is a better way now**, and the
redesign must consume the **actual latest live-control code**, which is **not in
this base**.

Ground truth verified:
- `packages/civ7-control-orpc` is **empty (0 ts files)** on this branch. The new
**effect-oRPC control router + `Civ7IntelligenceBridge` ingress** (the target
read/action substrate) lives in the **570-branch live-control stack currently
being consolidated** — see `docs/projects/graphite-stack-integration/`
`LIVE-CONTROL-STACK-CONSOLIDATION-PLAYBOOK.md`. It is **not settled** (local-only,
no PRs, folding 570 → ≤50 semantic branches).
- The studio reads live state **today** via `@civ7/direct-control`
(`apps/mapgen-studio/vite.config.ts:32`) — exactly the surface being rebuilt.
**FireTuner is being demoted** to canary/parity/diagnostic; the in-game App-UI
controller bridge (oRPC/Effect) becomes baseline.
- The **mapgen-studio ↔ control "Studio link" is itself being rebuilt** inside
that stack (playbook rows 38, 42–43). So Phase A (replace the studio server)
would collide with / be superseded by that work.

Consequences:
- **`audit/05-server-contracts.md` and `architecture/10` §1 (server) are PROVISIONAL /
superseded** — they model the deprecated `@civ7/direct-control` surface. Re-baseline
against the consolidated control-oRPC contracts once that stack settles.
- **A1 (`@civ7/studio-server` contracts) is provisional** — kept on this isolated
branch for reference; will be re-derived from the settled substrate.
- Reference prior art for the target client stack (effect-oRPC + RPCLink +
oRPC-TanStack-Query + Zustand React shell) already exists in the repo's
**gt-stack-inspect** toolkit lane — mirror it when re-baselining (that lane is a
separate owner and out of scope to modify).

Lane status:
- ⏸ **PAUSED (blocked on live-control settle):** Phase A (server), Phase B (data
layer + live wiring), live-state-coupled parts of Phase D (decomposition).
- ✅ **Still valid (decoupled from live-control):** Phase C (Tailwind v4 + shadcn +
token/theming repair) and the presentational parts of Phase E. These are
app-local CSS + primitive components with no `@civ7/direct-control` coupling.
Carries minor merge risk against in-flight "Studio link" branches.
- ✅ **Still-valid audits:** `audit/03` (component architecture), `audit/04`
(design system), `audit/06` (TS rigor), `research/02` (client patterns).

## ⏸ WORKSTREAM PAUSED — resume conditions (decided 2026-06-08)

User decision: **full pause**; re-baseline source of truth = **`main`** (not the
in-flight stack). Do **not** proceed on any lane — including the decoupled design
system — until resumed.

**Resume trigger:** the consolidated control-oRPC / direct-control substrate
(effect-oRPC router + `Civ7IntelligenceBridge` ingress + rebuilt mapgen-studio
"Studio link") has **merged to `main`**. The user will signal, or I re-check `main`.

**On resume, before writing any code:**
1. `git checkout main && git pull`; rebase `design/mapgen-studio-redesign` onto it.
2. Re-audit the **settled** substrate on main: read the merged `@civ7/control-orpc`
router + contracts, the `Civ7IntelligenceBridge` invoke surface, how Tuner is
now scoped (canary/diagnostic), and how the studio is now wired to control.
3. **Re-baseline** `audit/05-server-contracts.md` and `architecture/10` §1 against
those real contracts. Re-derive A1 (`@civ7/studio-server`) — the studio server
should consume/extend the control-oRPC substrate, **not** re-implement reads and
**not** read FireTuner.
4. Mirror the repo's existing effect-oRPC + RPCLink + oRPC-TanStack + Zustand
pattern (gt-stack-inspect lane) for the client.
5. Then resume Phase A → G per `architecture/11-slice-plan.md`.

**Unchanged on resume (no re-audit needed):** the design-system, component-
architecture, and TS-rigor audits and the client-pattern research remain valid.

## Status ledger (ACTIVE — supersedes the paused ledger above)

Phase order per [`FRAME.md`](FRAME.md) §7 (design-system-first):

- [x] Frame, isolate, research, audit, target architecture, slice plan
- [x] Re-frame + un-pause + normative FRAME.md + goal reset (2026-06-08)
- [ ] **P1 Design system** (`design:*` chain) — FIRST DOMINO, in progress next
- [ ] P2 Client data layer (oRPC-native TanStack Query + Zustand, behind seam)
- [ ] P3 Component decomposition (un-god App.tsx onto the design system)
- [ ] P4 Primitives→shadcn + new components + rjsf re-skin + craft
- [ ] P5 Server (effect-oRPC on Bun, aligned to control-oRPC stack-top seam)
- [ ] P6 Rigor + dead code + comments
- [ ] P7 Verify + review wave → hand over **built, UNSUBMITTED** Graphite stack
- [~] A1 `@civ7/studio-server` contracts (PROVISIONAL — re-derive vs stack-top in P5)

Live-control: NOT waiting for merge. Inspect stack-top, design the seam (FRAME §6).
Loading
Loading