docs: record editor-first UI-scene convention (ADR 0001)

Capture the direction change from M3-b: UI screens are authored editor-first
as .tscn node trees (previewable/arrangeable in the Godot editor), scripts do
on-load work only — never build the tree in _ready(). Add ADR 0001, a
"UI scene conventions" how-to in client/docs, a §16 mandate in the charter,
and mark M3-a/M3-b done in the roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 10:04:57 -05:00
parent 58880554c2
commit c933d12504
5 changed files with 140 additions and 5 deletions

View File

@@ -12,6 +12,49 @@ Put here:
Cross-cutting design (anything touching both client and api) goes in the root [`/docs`](../../docs), not here.
## UI scene conventions (shell + all screens)
Screens are **authored editor-first** — the layout lives in the `.tscn`, not in
code. See ADR [0001](../../docs/adr/0001-editor-first-ui-scenes.md) for the why;
this is the how. Reference: `scenes/shell/{MainWindowShell,SystemDock,NarrationBook}.tscn`
+ `scripts/ui/shell/*.gd`.
- **The `.tscn` owns the tree.** Author every node in the editor: containers,
panels, labels, buttons, art-slot placeholders. Assign the shared theme on the
scene root (`theme = game_theme.tres`) and set each node's `theme_type_variation`
(from `ThemeKeys`) in the editor, so the scene previews themed. Instance
sub-screens (`PackedScene` ext_resource) rather than rebuilding them.
- **The script does on-load work only.** `@onready var x := $Path/To/Node` refs,
signal/service wiring, animations, and binding state into the authored nodes
(label text, colours from `Palette`). Do **not** create nodes in `_ready()`.
- **Data-driven repeats = fixed authored slots, bound at runtime.** Author enough
turn-order tokens / consumable slots / etc. in the editor; the script fills the
first N from state and hides the rest. Previewable now; systems write them later.
- **Scene-root sizing.** Give the root a fixed size via offsets (e.g. 1920×1080)
for editor preview, but **no full-rect anchors on the root** — a root Control run
via F6 collapses to (0,0) under full-rect anchors. Keep `size = get_viewport_rect().size`
on `_ready` + a `size_changed` connection (the M3-a collapse lesson).
- **Button styling → Button-base variations only.** Never put a PanelContainer
variation (`DarkPanel`, `ParchmentCard`) on a `Button`; it has no `normal/hover/
pressed` styleboxes so the button renders unskinned. Use the Button-base
variations (`PrimaryCTA`, `Tab`, `TabActive`, `Chip`, `DockButton`, `ParchmentButton`).
- **`RichTextLabel` prose** reads its dark serif ink from the theme's base
`RichTextLabel` style (it does *not* inherit `default_font`/`default_color`).
Just add the `RichTextLabel` — don't override its colour per-node.
- **Theme is generated.** All colours live in `palette.gd`; `game_theme.tres` is
built by `scripts/theme/build_game_theme.gd` (source of truth). To add/adjust a
variation, edit the builder + `theme_keys.gd`, then regenerate:
`godot --headless -s res://scripts/theme/build_game_theme.gd`. Never hand-edit
the `.tres` or put a hex literal in a script or `.tscn`.
- **Wiring the DM loop / injectable services.** Follow the shell: a `service`
var settable *between* `instantiate()` and `add_child()` lets tests inject a
`FakeTransport`-backed `DmService` so on-load calls stay hermetic; `_ready`
builds the real `HttpTransport`-backed one when none was injected.
- **Visual gate.** Headless GUT proves structure (nodes mount, sizes non-zero,
bindings run); it cannot prove rendering. Every screen needs a **human F6
eyeball** before it's done. Note shader surfaces (`DarkBay`, vignette) render
their texture only on an F6 run, not in the editor.
## Narrate live smoke (M2 client HTTP loop)
The `narrate_harness` scene is a throwaway proof of the client → `/dm/narrate`