Skip to content
Closed
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
12 changes: 12 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "mapgen-studio",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "dev"],
"cwd": "apps/mapgen-studio",
"port": 5173
}
]
}
149 changes: 149 additions & 0 deletions apps/mapgen-studio/.interface-design/system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# MapGen Studio — Design System (decisions)

> **This file holds DECISIONS, not extraction.** The as-built baseline lives in
> [`../system.md`](../system.md). When they disagree, this file wins — it is the
> target the redesign realizes. Confirmed with the user 2026-06-08. Downstream
> slices and agents APPLY this; they do not re-litigate it.
>
> Realized + value-tuned in the P1 implementation slice (Tailwind v4 + shadcn).
> Starting token values below are the committed intent; exact final values are
> validated against the running app during P1.

## Direction

**Cartographer's instrument.** A dense, dark-first precision tool for *sculpting a
world by adjusting generation parameters and reading the map*. The deck.gl map is
the hero and carries all terrain color; the chrome is a recessed graphite
instrument bezel that never competes with it. **The instrument makes way for the
matter:** it lives in its own robust substrate layers but never steals the show.
Keep the existing DNA — dense, border-tiered, micro-typography, a single restrained
cool accent — but make every choice intentional.

**Accent decision (user-confirmed, refined 2026-06-08):** the accent is an
**elevated cool-steel slate**, not a vivid hue. Teal/cyan was considered and set
aside *because the map is the matter* — a saturated accent would pull the eye to
the chrome. The commodity tell today is not "slate" itself; it is that slate is
*unintentional* (declared indigo, used gray, no system). The fix is to make slate
**deliberate and robust**: a committed cool-steel hue with a defined ramp, a
rigorous substrate-elevation system, and a contour signature carried by
**luminance** rather than color. Craft, not chroma.

- **Who:** a Civ7 map-mod author in long, dark, tweak-and-observe sessions.
- **Verb:** tune a recipe → watch the world respond → run it in-game.
- **Feel:** quiet, technical, precise. Like a DAW / topographic survey console.
- **Light mode:** dark-first, but ship a genuinely correct light theme via
shadcn's single `.dark` class strategy (one switch, no prop drilling).

## Signature

The **contour line**, carried by **luminance, not a loud hue**. The map domain's
defining mark (topographic contour / graticule) becomes the interface's signature:

- **Focus** renders as a 1px *contour ring* in a brighter cool-steel slate — a
luminance step above the chrome, not a saturated glow block.
- **Active/selected** states use a thin steel rule/underline (a contour), not a
filled slab — fills are reserved for the one primary action, itself a restrained
elevated slate.
- **Hypsometric mini-legends:** elevation/rainfall/biome readouts use real
cartographic ramps (relief greens→tans→snow; bathymetric blues) so data viz
belongs to the product's world. This is the one place real color appears in the
chrome — and it appears as *the matter*, the same domain color the map carries,
not as instrument identity.

## Primary craft lever (from the live app, 2026-06-08)

A screenshot of the running app revealed the real departure is **not color** — it
is **flatness**. Page, panel, and nested surfaces sit ~4% lightness apart with no
floating-layer shadows, so borders carry all structure and the UI reads as one
hollow dark plane (the squint test shows almost no hierarchy). The empty map
center reads as *dead space*, not *a stage awaiting matter*.

So P1's highest-value work, in order:
1. **Robust substrate elevation** — widen the (still whisper-quiet) page→panel→
nested lightness tiers + a real border progression so structure is *felt*. This
is what makes a neutral slate instrument feel machined instead of flat.
2. Elevate the slate as quiet identity (committed steel hue, contour-luminance focus).
3. Make "awaiting matter" intentional — the empty stage frames the map
(graticule/contour), reads as ready, not hollow.
4. Under the hood: named tokens, real overlay motion, `.dark`-class theming repair.

## Foundation & depth

- **Foundation:** cool graphite/ink, near-monochrome. Same hue across surfaces;
hierarchy by lightness step only (never different hues per surface).
- **Depth:** **borders-only**, tiered by background lightness step + 1px hairline.
This substrate-elevation system is where the craft lives — the layering must be
*felt, not seen* (whisper-quiet lightness steps; the squint test must still show
hierarchy with nothing jumping out). A robust tier system is what makes a neutral
slate read as intentional rather than flat. Shadows appear ONLY on floating
layers (dialog/popover/toast). `backdrop-blur` on chrome that floats over the
map. Do not mix depth strategies.
- **Accent:** ONE elevated cool-steel slate, used with restraint (primary action +
active/focus contours). It replaces the commodity declared-indigo-but-used-gray
non-choice by being *committed and systematic*, not by being louder.

## Tokens (HSL, shadcn `:root` / `.dark`)

Dark is the default app theme. Values are the committed starting point.

| Token | Dark | Light | Role |
|---|---|---|---|
| `--background` | `240 20% 5%` | `240 20% 96%` | page / deck.gl backdrop tint |
| `--foreground` | `240 14% 92%` | `215 28% 17%` | primary text |
| `--card` | `240 9% 9%` | `0 0% 100%` | panels, headers |
| `--card-foreground` | `240 14% 92%` | `215 28% 17%` | |
| `--popover` | `240 8% 11%` | `0 0% 100%` | one tier above card (dropdowns) |
| `--popover-foreground` | `240 14% 92%` | `215 28% 17%` | |
| `--muted` | `240 8% 13%` | `240 14% 93%` | hover/active surface, tags |
| `--muted-foreground` | `240 6% 56%` | `220 9% 46%` | secondary/label text |
| `--primary` | `216 18% 42%` | `216 22% 32%` | elevated cool-steel slate accent |
| `--primary-foreground` | `210 20% 96%` | `0 0% 100%` | text on primary |
| `--border` | `240 9% 18%` | `220 13% 91%` | workhorse hairline |
| `--input` | `240 9% 25%` | `220 13% 86%` | control border |
| `--ring` | `212 28% 60%` | `216 25% 45%` | focus contour (luminance step, not chroma) |
| `--destructive` | `350 70% 58%` | `350 72% 48%` | rose (commit; kill rose/red drift) |
| `--destructive-foreground` | `0 0% 98%` | `0 0% 100%` | |
| `--radius` | `0.25rem` | — | base; card = `0.5rem` |

Secondary text tiers: a tertiary/muted level (`240 7% 38%` dark) for placeholders/
help. Semantic (used sparingly, slightly desaturated in dark): success
`160 55% 45%`, warning `40 80% 55%`, info = the steel-slate accent (no separate
info hue — keep the chrome monochrome-cool).

## Type

- **Sans:** Inter. **Mono:** JetBrains Mono (tabular numerals for data). Self-host
(remove render-blocking Google Fonts `@import`).
- **Named scale (replace ad-hoc `text-[11px]`):** `text-data` 11px / `text-label`
10px uppercase `tracking-wider` / `text-xs` 12px / `text-sm` 14px (dialog
titles). Weights: medium (500) default emphasis, semibold (600) headers. No bold
in chrome. 11px is the body default; 10px the eyebrow label.

## Spacing / radius / motion

- **Spacing base 4px.** Scale: 4, 6, 8, 10, 12, 16. Field rows `py-1`, label↔input
`gap-3`. Cards `p-2.5`. Nested `p-2`.
- **Radius:** 4px inputs/buttons/tags, 8px cards/dialogs/dropdowns, full for pills/
switch. (Drop the 6px/`xl`/`2xl` noise.)
- **Motion:** real, quiet, deceleration easing — install the Tailwind v4 animate
layer so the (currently dead) `animate-in` overlay transitions actually run.
150ms fast / 200ms normal. No spring/bounce. `animate-pulse-subtle` stays for
running state.

## Theming mechanism (the repair)

- Single **`.dark` class** on `<html>` (shadcn strategy). Set Tailwind
`darkMode: 'class'` (v4 equivalent). One theme switch writes the class.
- **Delete** `createTheme()` runtime string interpolation (JIT-invisible — a real
bug), the `lightMode` boolean prop threaded through ~24 files, the duplicate
`colors.light|dark` config, and the dead `--spacing-*`/`--radius-*` vars. The
`lightMode`-prop removal lands as components migrate (P3/P4); P1 establishes the
`.dark` system and tokens so both can briefly coexist without conflict.

## Component patterns (preserve as-built dimensions, re-skin onto shadcn)

Keep the dense dimensions from the extraction baseline — Button `h-8`/`h-7`, Input
`h-7`, Switch 36×20, Field row `justify-between gap-3 py-1` with `min-w-[96px]`
label, Card `rounded-lg border p-2.5`, eyebrow label uppercase 10px. The migration
changes the *mechanism* (Radix shadcn primitives, token-driven) and adds the
contour-focus signature + motion — not the density.
21 changes: 21 additions & 0 deletions apps/mapgen-studio/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
21 changes: 18 additions & 3 deletions apps/mapgen-studio/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapGen Studio</title>
<script>
// Apply theme before first paint (no flash). Default dark; `.dark` drives
// the design tokens in index.css. localStorage key written by the app.
(function () {
try {
var stored = localStorage.getItem("mapgen-studio:theme");
if (stored ? stored === "dark" : true) {
document.documentElement.classList.add("dark");
}
} catch (e) {
document.documentElement.classList.add("dark");
}
})();
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Pre-React flash color matches the dark page token (index.css --background). */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0d0d11;
color: #e8e8ed;
min-height: 100vh;
}
</style>
Expand Down
8 changes: 5 additions & 3 deletions apps/mapgen-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@civ7/plugin-mods": "workspace:*",
"@civ7/studio-server": "workspace:*",
"@deck.gl/core": "^9.0.43",
"@fontsource/inter": "^5.1.1",
"@fontsource/jetbrains-mono": "^5.1.1",
"@deck.gl/layers": "^9.0.43",
"@deck.gl/react": "^9.0.43",
"@orpc/client": "^1.14.4",
Expand All @@ -44,12 +46,12 @@
"typebox": "^1.0.80"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.13",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "latest",
"postcss": "latest",
"tailwindcss": "3.4.17",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
Expand Down
6 changes: 0 additions & 6 deletions apps/mapgen-studio/postcss.config.js

This file was deleted.

Loading
Loading