Files
Christopher Wiebe 953bb985ad M5: Codex design system (dark theme) + kitchen-sink
Programmatic Theme builder + reusable popover and stepper widgets,
ported from CharacterCreator.zip's :root design tokens. Kitchen-sink
scene exercises every primitive for visual eyeballing.

CodexPalette.cs:
  Color tokens lifted verbatim from the React prototype's `:root`
  block (--bg, --ink, --gild, --seal, etc.). Variable names mirror
  the CSS so the audit trail stays readable. Spacing locked at the
  prototype's normal density (--gap=24, --pad=28, --radius=2).

  Scope cut: only the Dark theme ships. The React prototype designed
  Parchment, Dark, and Blood as switchable variations — user direction
  during M5 is that only Dark (leather + candlelight) is wanted for
  this game. Parchment/Blood code dropped, plan doc updated to match
  (§1 goal #5, §4.5 UI map, §5 M5 scope, §10 resolved decisions #4).
  No runtime theme switcher.

CodexTheme.Build():
  Programmatically constructs a Godot Theme from CodexPalette.Dark
  plus CodexSpacing/CodexType tokens. Configures Panel, Card,
  CodexPopover styleboxes; Label variations for H1..H4, CodexTitle,
  Eyebrow, Meta, ValidationOk/Error, CardName/Body/Meta, StepperNum/
  Name; Button + PrimaryButton + GhostButton variants; LineEdit,
  CheckBox, scrollbar styling.

  Fonts: looks for CormorantGaramond / CrimsonPro / JetBrainsMono
  TTFs in res://Fonts/ (or Content/Fonts/) and graceful-falls-back to
  Godot defaults if missing. M5 ships with no fonts in repo; user can
  drop them in later for typography parity with the React prototype.

CodexPopover.cs:
  Hoverable text trigger + floating PanelContainer, mirrors
  src/trait-hint.jsx. Viewport-clamps horizontally and vertically;
  flips above the trigger if there's no room below; 80 ms grace
  period when moving cursor from trigger to popover. Detriment
  variant uses the seal-coloured stylebox. Future TraitName /
  SkillChip / BonusPill widgets layer className differences on top.

CodexStepper.cs:
  Roman-numeral horizontal stepper with Pending / Active / Complete /
  Locked states. Active step gets a 2-px gild underline, Complete
  shows a ✓ in seal-red, Locked shows ✕ + 0.45 modulate. Emits
  StepClicked(int) for non-locked rows. M5 is decorative — M6 wires
  the signal to the character-creation state machine.

KitchenSink.cs + Main.cs --codex-test:
  Verification scene rendering every primitive (header, stepper,
  buttons, inputs, cards, trait popovers). Clicks log to console.
  Fonts default to Godot's Noto Sans until res://Fonts/ is populated.

Closes M5 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M6 (title + character creation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:29:22 -07:00

230 lines
8.0 KiB
C#

using Godot;
using Theriapolis.GodotHost.UI.Widgets;
namespace Theriapolis.GodotHost.UI;
/// <summary>
/// M5 kitchen-sink. Renders every primitive from the codex design system
/// (single Dark theme — Parchment and Blood dropped from scope per port
/// plan §10) so we can eyeball parity against screenshots of the React
/// prototype. Launch via:
/// godot --path Theriapolis.Godot --codex-test
/// </summary>
public partial class KitchenSink : Control
{
private Panel _root = null!;
private CodexStepper _stepper = null!;
public override void _Ready()
{
AnchorRight = 1f;
AnchorBottom = 1f;
OffsetRight = 0;
OffsetBottom = 0;
_root = new Panel { ThemeTypeVariation = "Panel" };
_root.AnchorRight = 1f;
_root.AnchorBottom = 1f;
AddChild(_root);
ApplyTheme();
BuildContent();
}
private void ApplyTheme()
{
_root.Theme = CodexTheme.Build();
// The root Panel's stylebox covers its rect; fill the *outer*
// viewport (which Godot draws with its default clear colour) so
// screenshots don't show engine grey along any edge.
RenderingServer.SetDefaultClearColor(CodexPalette.Dark.BgDeep);
}
private void BuildContent()
{
var margin = new MarginContainer();
margin.AddThemeConstantOverride("margin_left", 36);
margin.AddThemeConstantOverride("margin_right", 36);
margin.AddThemeConstantOverride("margin_top", 28);
margin.AddThemeConstantOverride("margin_bottom", 28);
margin.AnchorRight = 1f;
margin.AnchorBottom = 1f;
_root.AddChild(margin);
var scroll = new ScrollContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
margin.AddChild(scroll);
var col = new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.ExpandFill,
};
col.AddThemeConstantOverride("separation", 24);
scroll.AddChild(col);
BuildHeader(col);
BuildStepperSection(col);
BuildButtonsSection(col);
BuildInputsSection(col);
BuildCardSection(col);
BuildPopoverSection(col);
}
private void BuildHeader(VBoxContainer col)
{
var h = new HBoxContainer();
col.AddChild(h);
var titleCol = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
h.AddChild(titleCol);
titleCol.AddChild(new Label { Text = "THERIAPOLIS · CODEX OF BECOMING", ThemeTypeVariation = "CodexTitle" });
titleCol.AddChild(new Label { Text = "Kitchen sink — every primitive, three themes", ThemeTypeVariation = "Eyebrow" });
var meta = new Label { Text = "M5 · DESIGN AUDIT", ThemeTypeVariation = "Meta" };
h.AddChild(meta);
}
private void BuildStepperSection(VBoxContainer col)
{
var section = MakeSection(col, "STEPPER");
_stepper = new CodexStepper();
_stepper.SizeFlagsHorizontal = SizeFlags.ExpandFill;
section.AddChild(_stepper);
_stepper.SetSteps(
new[] { "Clade", "Species", "Calling", "Subclass", "History", "Abilities", "Skills", "Sign" },
new[]
{
CodexStepper.StepState.Complete,
CodexStepper.StepState.Complete,
CodexStepper.StepState.Complete,
CodexStepper.StepState.Active,
CodexStepper.StepState.Pending,
CodexStepper.StepState.Locked,
CodexStepper.StepState.Locked,
CodexStepper.StepState.Locked,
});
_stepper.StepClicked += (int idx) => GD.Print($"[kitchen-sink] Stepper clicked index={idx}");
}
private void BuildButtonsSection(VBoxContainer col)
{
var section = MakeSection(col, "BUTTONS");
var row = new HBoxContainer();
row.AddThemeConstantOverride("separation", 12);
section.AddChild(row);
row.AddChild(new Button { Text = "Default" });
row.AddChild(new Button { Text = "Primary", ThemeTypeVariation = "PrimaryButton" });
row.AddChild(new Button { Text = "Ghost", ThemeTypeVariation = "GhostButton" });
var disabled = new Button { Text = "Disabled", Disabled = true };
row.AddChild(disabled);
}
private void BuildInputsSection(VBoxContainer col)
{
var section = MakeSection(col, "INPUTS");
var row = new HBoxContainer();
row.AddThemeConstantOverride("separation", 12);
section.AddChild(row);
var le = new LineEdit
{
PlaceholderText = "Enter your name...",
CustomMinimumSize = new Vector2(280, 0),
};
row.AddChild(le);
row.AddChild(new CheckBox { Text = "Toggle option" });
}
private void BuildCardSection(VBoxContainer col)
{
var section = MakeSection(col, "CARDS");
var grid = new HBoxContainer();
grid.AddThemeConstantOverride("separation", CodexSpacing.Gap);
section.AddChild(grid);
AddCard(grid, "Canidae", "Predator · medium", "Strong-jawed pack hunters of the inland forest.", selected: false);
AddCard(grid, "Felidae", "Predator · medium", "Solitary stalkers, claw-tipped and sharp of ear.", selected: true);
AddCard(grid, "Mustelidae", "Predator · small", "Burrowers and threadkillers of the river edges.", selected: false);
}
private void AddCard(HBoxContainer parent, string name, string meta, string body, bool selected)
{
var panel = new PanelContainer
{
ThemeTypeVariation = "Card",
CustomMinimumSize = new Vector2(240, 0),
};
if (selected && _root.Theme is not null && _root.Theme.HasStylebox("panel_selected", "Card"))
{
var box = _root.Theme.GetStylebox("panel_selected", "Card");
panel.AddThemeStyleboxOverride("panel", box);
}
var v = new VBoxContainer();
v.AddThemeConstantOverride("separation", 6);
panel.AddChild(v);
v.AddChild(new Label { Text = name, ThemeTypeVariation = "CardName" });
v.AddChild(new Label { Text = meta.ToUpperInvariant(), ThemeTypeVariation = "CardMeta" });
v.AddChild(new Label
{
Text = body,
ThemeTypeVariation = "CardBody",
AutowrapMode = TextServer.AutowrapMode.WordSmart,
CustomMinimumSize = new Vector2(220, 0),
});
parent.AddChild(panel);
}
private void BuildPopoverSection(VBoxContainer col)
{
var section = MakeSection(col, "TRAIT POPOVERS · HOVER");
var row = new HBoxContainer();
row.AddThemeConstantOverride("separation", 18);
section.AddChild(row);
row.AddChild(new CodexPopover
{
TriggerText = "Pack Tactics",
Title = "Pack Tactics",
Tag = "active",
Description = "Once per turn, when an ally is adjacent to your target, gain advantage on the attack.",
});
row.AddChild(new CodexPopover
{
TriggerText = "Glass Bones",
Title = "Glass Bones",
Description = "Critical hits against you deal an extra 1d6 damage.",
Detriment = true,
});
row.AddChild(new CodexPopover
{
TriggerText = "Athletics",
Title = "Athletics",
Tag = "STR",
Description = "Climbs, jumps, swims, and brute physical contests.",
});
row.AddChild(new CodexPopover
{
TriggerText = "+2",
Title = "STR modifier",
Description = "+1 from Canidae · +1 from Wolf",
});
}
private static VBoxContainer MakeSection(VBoxContainer col, string title)
{
var box = new VBoxContainer();
box.AddThemeConstantOverride("separation", 8);
col.AddChild(box);
box.AddChild(new Label { Text = title, ThemeTypeVariation = "Eyebrow" });
var inner = new VBoxContainer();
inner.AddThemeConstantOverride("separation", 8);
box.AddChild(inner);
return inner;
}
}