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
28 changes: 28 additions & 0 deletions apps/mapgen-studio/.interface-design/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,31 @@ decisions (see `docs/projects/mapgen-studio-redesign/pass-2-design-fixes.md`):
- **One Run CTA.** The footer is the run console (seed, auto-run, run, run-in-game);
panel-local duplicates are removed.
- Control density (button/input/switch/field-row dimensions above) is unchanged.

## Pass-3 amendment (2026-06-11, user-grounded): spacing substrate + form elevation + console split

Pass-3 grounding found the spacing scale above had **never rendered**: an
unlayered `* { margin:0; padding:0 }` reset in `index.html` outranks Tailwind
v4's `@layer utilities`, zeroing every padding/margin utility app-wide. Decisions
(see `docs/projects/mapgen-studio-redesign/pass-3-design-fixes.md`):

- **No unlayered author CSS.** `index.html` may carry only the pre-paint flash
guard (`body` colors); resets belong to Tailwind preflight (layered). Any
future global CSS outside `index.css` must live in a cascade layer.
- **Form surface elevation (config panel):** nesting is expressed by *surfaces,
not indent rules*. Panel (popover) → stage card (card, one recess) → **group
well** (one further recess: `bg-background/40`-class tint, `border-border-subtle`,
rounded, padded). Two surface tiers maximum; deeper nesting differentiates by
eyebrow heading + rhythm only. Arrays ride the same well treatment. `border-l`
indent ladders are removed.
- **Form rhythm (4px base):** 4px within a field block (label/input/help),
8px between fields, 12px between groups and between stage cards. Owned by the
FORM constant in `rjsfTemplates.tsx`.
- **Footer = two consoles.** A centered **Studio console** (status, last run,
seed, reroll, auto-run, Run) and a right-docked, named **Game console**
(live Civ7 status/sync, autoplay, Run in Game + status, save-deploy chip).
Studio↔game relation chips (Current/Stale/Previous, stale sync ring) live on
the Game console; studio dirty state stays on the Studio console.
- **Explore toolbar groups by target:** VIEW (camera fit, edges overlay) and
DATA (render, space, era, variant, overlay) clusters with eyebrow labels;
the debug toggle is a DATA-list filter and lives on the DATA section header.
130 changes: 130 additions & 0 deletions docs/projects/mapgen-studio-redesign/pass-3-design-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Pass 3 — Spacing substrate, config surface, console split, explore toolbar

> Frame for the third user-grounded fix wave (2026-06-11, evening). Same
> discipline as Pass 2 (`pass-2-design-fixes.md`): OpenSpec slice per change,
> Graphite stack (never submitted), a change is done only when SEEN in the
> running app.

## The reframe (perspective pass)

The user asked for "a full, thoughtful design pass from the ground up" on the
config UI's padding and margins. Grounding in the live page found something
prior passes missed: **the spacing system has never rendered.**

`index.html` ships an inline, *unlayered* reset — `* { margin: 0; padding: 0 }`.
Tailwind v4 emits every utility inside `@layer utilities`, and unlayered author
styles take cascade priority over ALL layered styles regardless of specificity.
Result: every `p-*`/`px-*`/`py-*`/`m-*` utility in the app computes to **0px**.
Gaps, heights, and widths survive (the reset doesn't set them), which is why the
layout half-works while every surface's internal texture is crushed. The old
Tailwind v3 build emitted unlayered utilities (specificity won, the reset was
harmless); the P1 migration to v4's native cascade layers silently inverted the
cascade. Proven live: deleting the rule in the running page restores 12px row
padding, 10px card padding, padded buttons/inputs/headers app-wide.

So the highest-leverage move is not redesigning spacing values — it is
**restoring the substrate (D0), then designing on truth (D1–D3)**. Every visual
decision made in Passes 1–2 was tuned against a zero-padding render; expect a
small re-tune wave after D0, owned by the slices that own those surfaces.

Second bootstrap defect found in the same file: the pre-paint theme script
reads `localStorage["mapgen-studio:theme"]`, but the app persists the
preference under `theme-preference`. The bootstrap reads a key nobody writes —
a light-theme user gets a dark pre-paint flash on every load. Same slice (D0).

## Hard core (unchanged + new)

- Behavior parity: run/poll/localStorage/transport semantics untouched.
- Control density tokens (h-7/h-8 controls, 11px data text) unchanged.
- Single `.dark` class theming; tokens only, no raw hex in chrome.
- OpenSpec `--strict` per slice; Graphite-only branch ops; stack never submitted.
- **NEW (user law, 2026-06-11): no hard-coded config overrides anywhere** — not
in app code, not in tests. A literal pipeline-config value duplicating the
recipe's source of truth is a defect to remove on sight (D4). Migration/
round-trip fixtures that test *user data shapes* (legacy formats, preset
payloads) are not overrides of defaults and remain legitimate, but must not
mirror real default values as expectations.

## Issues → changes

| # | Observation (grounded) | Root cause | Change |
|---|---|---|---|
| 1 | All padding/margins "really bad" app-wide | Unlayered `*` reset beats `@layer utilities` | **D0** remove reset from index.html (Tailwind preflight already provides a layered one) |
| 2 | Light users get dark pre-paint flash | Bootstrap reads stale localStorage key | **D0** align bootstrap to `theme-preference` |
| 3 | Config nesting reads as code-indent ladder, not "slate" | depth≥2 groups are heading + `border-l` indent; no surface tiers | **D1** surface-elevation scheme: stage card → recessed group wells; cap at two surface tiers, deeper nesting by type/spacing only |
| 4 | No chunking rhythm in form (fields ≈ groups ≈ sections) | gap-0.5/1/1.5/2 ad hoc, within-group == between-group | **D1** codified form rhythm on the 4px base: 4 within-field / 8 field↔field / 12 group↔group; amend `.interface-design/system.md` |
| 5 | Two studio bars + game controls interleaved in footer | AppFooter grew accretively; no ownership boundary | **D2** split: one centered **Studio console** (status · last run · seed · reroll · auto-run · Run), one right-docked named **Game console** (live status/sync, autoplay, Run in Game + status/retry/diagnostics, save-deploy chip) |
| 6 | Studio↔game state legibility must survive the split | Relation cues exist (stale ring, Current/Stale/Previous chip) but are scattered | **D2** bridge: relation chips live on the Game console, computed against current studio state; studio dirty state stays on the Studio console (Modified + Run ring) |
| 7 | Explore bottom buttons uncategorized, zig-zag layout | View toolbar mixes camera, map-overlay, data, and list-filter controls | **D3** regroup by target: VIEW (fit, edges overlay) / DATA (render, space, era, variant, overlay); consistent label-left/control-right rows |
| 8 | Debug toggle misplaced | `showDebugLayers` filters the DATA *list*, it is not a view control | **D3** move it to the DATA section header (list filter lives with the list) |
| 9 | 330-line hand-maintained default-config duplicate | `src/ui/data/defaultConfig.ts` predates recipe artifacts; app never imports it; two test blocks pin its shape | **D4** delete file; retarget those tests at `STANDARD_RECIPE_CONFIG` (the generated artifact) |

## D1 design (the config surface, decided)

- **Elevation, not indentation.** Panel (popover, 11%) → stage card (card, 9%,
one step recessed — already true) → **group well**: one further recess
(`bg-background/40`-class tint over card, `border-border-subtle`, rounded,
p-2). Groups read as machined slots in the slate, matching the instrument-
bezel direction. Arrays unify onto the same well treatment.
- **Two surface tiers maximum.** Nesting deeper than stage→group does not add
surfaces; it differentiates by eyebrow heading (text-label uppercase muted)
and the group↔group rhythm. Indent ladders (`border-l` + `pl-*`) are removed.
- **Rhythm:** 4px within a field block (label/input/help), 8px between fields,
12px between groups and between stage cards. One place owns these (the FORM
constant in rjsfTemplates).
- Field labels stay `text-foreground` medium (Pass-2); descriptions stay muted
and are NOT removed (docs are part of the instrument).

## D2 design (consoles, decided — user proposal incorporated)

Deliberation: accepted. The current footer already contains a latent boundary
(status/last-run + run controls are studio-runtime; live chip + autoplay +
run-in-game are Civ7-runtime). Making it structural gives the game side room to
grow (user: more live-game controls coming). One deviation from the literal
ask: **Run in Game moves to the Game console** (it commands the live game),
even though it sits in the run-controls bar today; the save-deploy status chip
follows it. Bars: centered Studio console (true centering, independent of right
content), right-pinned Game console with an eyebrow identity label ("Civ7" /
radio glyph) so the area is *named*, modular component `GameConsole` so future
controls land there.

## D3 design (explore toolbar, decided)

Inventory + categorization (verified in code):

| Control | Acts on | Disposition |
|---|---|---|
| Fit to view | camera | VIEW cluster |
| Edges toggle | map overlay (`showEdgeOverlay`) | VIEW cluster |
| Render mode segment | selected data's rendering | DATA cluster |
| Space segment | selected data's coordinate space | DATA cluster |
| Era mode+slider | selected data slice | DATA cluster (conditional) |
| Variant select | selected data slice | DATA cluster (conditional) |
| Overlay select + opacity | selected data correlation | DATA cluster (conditional) |
| Debug toggle | DATA **list** filter | moves to DATA section header |

No control is senseless; none removed. Layout: two eyebrow-labeled clusters
(VIEW / DATA) with consistent label-left/control-right rows replacing the
current left/right zig-zag.

## Slices (stacked on design/theme-class-sync)

1. `design/pass3-frame` — this doc, system.md amendment, OpenSpec changes.
2. `design/cascade-reset` — **D0** (root; everything downstream re-grounds on it).
3. `design/no-hardcoded-defaults` — **D4** (independent hygiene, early so later
test work inherits the law).
4. `design/config-surface` — **D1**.
5. `design/footer-consoles` — **D2**.
6. `design/explore-toolbar-2` — **D3**.

Verification contract: per slice — OpenSpec `--strict`, tsc, vitest green, and
live visual proof on :5173; final gate — dark + light screenshots of the full
app, DOM-computed spacing spot-checks (px-3 ⇒ 12px), build + worker bundle.

## Falsifier

If after D0+D1 the config surface still reads cramped or hierarchy-flat, the
spacing *values* and tier tints are wrong (design problem), not the mechanism —
iterate values in D1's slice. If the Game console split confuses state instead
of clarifying it, the bridge chips are insufficient and D2 reverts to a single
bar with internal grouping (user explicitly allowed refusal).
52 changes: 52 additions & 0 deletions openspec/changes/mapgen-studio-config-surface/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Context

The config form is the instrument's main working surface (Pass-2 made the dock
340px for it). Its nesting idiom predates the design system: headings +
`border-l` indents, with arrays on a divergent muted-box treatment. The user
asked for a ground-up pass: flat, "slate feeling", elevation-based grouping.
This slice lands AFTER the spacing-substrate repair, so declared paddings
actually render; values below were chosen against the restored baseline.

## Decisions

### 1. Recess, don't raise

Wells tint toward the page token (`bg-background/40` over `card`), not toward
`muted` (which is *lighter* than card and reads as raised chips). The
instrument language is machined slots in a slate bezel: panel (popover 11%) →
stage card (card 9%) → group well (≈7% effective). Controls keep their own
input-background wells inside, which stay legible on the darker ground.

### 2. Two surface tiers, hard cap

Schema nesting is unbounded; surface nesting is not. Stage→group gets the only
two tiers. Depth≥3 groups render heading + rhythm inside the parent well.
Rationale: each recess step costs contrast budget against the 4–5% lightness
range the whole substrate lives in; three tiers would either crush text
contrast or force lighter tints that read as raised.

### 3. One rhythm constant

`FORM.rhythm` (names, not numbers, at call sites): `fieldGap` 4px ⇒ `gap-1`,
`siblingGap` 8px ⇒ `gap-2`, `groupGap` 12px ⇒ `gap-3`, well padding `p-2`.
The root object template and stage card content adopt `siblingGap`; group/stage
boundaries adopt `groupGap`. This makes the Gestalt chunking auditable in one
place instead of scattered `gap-*` literals.

### 4. Heading tier inversion

Today depth-2 headings are `text-xs font-semibold text-foreground` — brighter
than field labels were before Pass-2, and competing with them after. Inverted:
group headings become eyebrows (`text-label` uppercase, muted), the well's
geometry carries the grouping, and field labels stay the brightest scan line.
Stage titles keep `text-sm font-semibold` (the card already separates them).

## Risks

- Well tint over `card` must survive light mode (`background` is *lighter*
than `card` in light theme, so `bg-background/40` raises instead of
recesses there). Verify both themes; if light mode inverts, use an explicit
token pair or `dark:` variant — decided at implementation against the
running app, recorded in the slice.
- 340px minus card padding minus well padding leaves ~280px for field rows —
wider than the pre-Pass-2 panel total; acceptable.
52 changes: 52 additions & 0 deletions openspec/changes/mapgen-studio-config-surface/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Config surface: elevation-based nesting + codified form rhythm

## Why

The user judged the config panel's padding/margins "really bad" and its
nesting/grouping representation poor — it should feel "smooth and slate", with
groups displayed flat on a proper surface-elevation scheme. Grounded diagnosis
(post spacing-substrate repair, which restores the *mechanism*):

1. **Nesting is an indent ladder, not a surface system.** Depth≥2 groups render
as a heading plus `border-l` + `pl-2.5` — a code-editor idiom that stacks
left rules, eats horizontal room at 340px, and contradicts the instrument's
substrate-elevation language (system.md: depth is carried by lightness
tiers, not lines).
2. **No chunking rhythm.** Within-field, field↔field, and group↔group spacing
are near-identical ad-hoc gaps (`gap-0.5/1/1.5/2`), so the form has no
Gestalt grouping — everything reads as one undifferentiated column.
3. Arrays use a different nested treatment (`bg-muted/40` boxes) than object
groups, so sibling structures read as different species.

## Target Authority Refs

- `docs/projects/mapgen-studio-redesign/pass-3-design-fixes.md` (issues 3–4; D1 design)
- `apps/mapgen-studio/.interface-design/system.md` (§Pass-3 amendment: form
surface elevation, form rhythm; §Foundation & depth: borders-only lightness tiers)

## What Changes

All in `src/features/configOverrides/rjsfTemplates.tsx` (+ the FORM constant):

- **Group wells replace indent ladders.** Depth≥2 object groups render as one
recessed surface tier inside the stage card: tinted toward the page token
(`bg-background/40`-class), `border-border-subtle`, rounded, padded. The
`border-l` indent idiom is deleted. Deeper nesting (depth≥3) adds no further
surfaces — eyebrow headings + rhythm only (two surface tiers maximum).
- **Arrays unify onto the same well treatment** as object groups.
- **Form rhythm is codified** in the FORM constant on the 4px base: 4px within
a field block (label/input/description/help), 8px between fields, 12px
between groups and between stage cards; group-well padding 8px; stage card
padding stays `p-2.5`.
- Group headings move to the eyebrow tier (`text-label` uppercase muted) so
field labels (foreground, Pass-2) remain the brightest scan anchors inside a
card.
- `transparentPaths`, error/alert contracts, gs-comments rendering, and widget
wiring are untouched (behavior parity).

## Impact

- Affected specs: `mapgen-studio`
- Affected code: `apps/mapgen-studio/src/features/configOverrides/rjsfTemplates.tsx`,
`apps/mapgen-studio/test/config/rjsfFieldTemplateErrors.test.tsx` (class
assertions), `.interface-design/system.md` (already amended in the frame).
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## ADDED Requirements

### Requirement: Config Nesting Is Expressed By Surface Elevation

Config-form nesting SHALL be expressed by surface tiers, not indent rules:
stage sections render as cards, groups within a stage render as one recessed
well surface (page-token tint, subtle border, rounded, padded), and nesting
deeper than stage→group adds no additional surface tiers.

#### Scenario: Group renders as a recessed well
- **WHEN** an object group (depth ≥ 2, not transparent) renders inside a stage card
- **THEN** it renders as a single recessed surface (tinted toward the page token with a subtle border and padding)
- **AND** no `border-l` indent rule is used to mark its nesting

#### Scenario: Deep nesting stays at two surface tiers
- **WHEN** a group contains a further nested group (depth ≥ 3)
- **THEN** the inner group differentiates by heading tier and spacing only, with no third surface tier

#### Scenario: Arrays ride the same well treatment
- **WHEN** an array field renders
- **THEN** its container uses the same well surface as object groups

### Requirement: The Config Form Has A Codified Spacing Rhythm

The config form SHALL chunk by a codified rhythm on the 4px base — tighter
within a field block than between fields, and tighter between fields than
between groups/stage sections — owned by a single constant in the rjsf
templates.

#### Scenario: Within-field is tighter than field-to-field
- **WHEN** a field renders its label, input, and description
- **THEN** the vertical gap inside the field block (4px) is smaller than the gap separating it from sibling fields (8px)

#### Scenario: Groups chunk wider than fields
- **WHEN** sibling groups or stage sections render
- **THEN** they are separated by a wider step (12px) than field-to-field spacing

### Requirement: Group Headings Sit On The Eyebrow Tier

Group headings inside stage cards SHALL use the eyebrow label tier (uppercase
`text-label`, muted), keeping field labels (foreground) the brightest text
inside a card.

#### Scenario: Field labels outrank group headings in the squint test
- **WHEN** a stage card with grouped fields renders
- **THEN** field labels render on the foreground tier while group headings render uppercase on the muted eyebrow tier
20 changes: 20 additions & 0 deletions openspec/changes/mapgen-studio-config-surface/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. Implementation

- [ ] 1.1 Codify `FORM` rhythm + well surface classes in `rjsfTemplates.tsx`
(fieldGap 4 / siblingGap 8 / groupGap 12; well = page-tint + subtle
border + rounded + p-2).
- [ ] 1.2 Object template: depth≥2 groups render as wells (drop `border-l` +
`pl-*` ladder); depth≥3 adds heading + rhythm only; headings move to the
eyebrow tier.
- [ ] 1.3 Array template: unify onto the well treatment.
- [ ] 1.4 Apply rhythm to root/stage content gaps.
- [ ] 1.5 Update `rjsfFieldTemplateErrors.test.tsx` assertions if class names
shifted; add a well/no-indent assertion for a nested group.

## 2. Verification

- [ ] 2.1 `bun run openspec -- validate mapgen-studio-config-surface --strict`
- [ ] 2.2 tsc + mapgen-studio vitest green
- [ ] 2.3 Visual on :5173, dark AND light: stage card → group well reads as two
recess steps (squint test shows chunking); no left-rule indents; rhythm
visibly tiered (field < sibling < group). Screenshot both themes.
Loading