From 49bc6b22c0f52d33191fe4f367d241f562059ee6 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 16:06:23 -0500 Subject: [PATCH] docs(mockups): add the UI/systems bible (11 screens + README) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit High-fidelity HTML/CSS/JS design references for the tactical-CRPG direction — title, character creation, character sheet, inventory, world map, quest log, shop, pause/settings, dialogue, combat HUD, main exploration window. README is the handoff spec (visual system, per-screen state models, Godot translation notes). Not production code — the source of truth for the game's look + systems. Co-Authored-By: Claude Opus 4.8 (1M context) --- mockups/Character Creation.dc.html | 191 +++ mockups/Character Sheet.dc.html | 221 +++ mockups/Combat HUD.dc.html | 434 ++++++ mockups/Dialogue.dc.html | 208 +++ mockups/Inventory.dc.html | 292 ++++ mockups/Main Window + Wireframes.dc.html | 363 +++++ mockups/Pause Menu.dc.html | 204 +++ mockups/Quest Log.dc.html | 264 ++++ mockups/README.md | 210 +++ mockups/Shop.dc.html | 247 ++++ mockups/Title Screen.dc.html | 99 ++ mockups/World Map.dc.html | 221 +++ mockups/support.js | 1717 ++++++++++++++++++++++ 13 files changed, 4671 insertions(+) create mode 100644 mockups/Character Creation.dc.html create mode 100644 mockups/Character Sheet.dc.html create mode 100644 mockups/Combat HUD.dc.html create mode 100644 mockups/Dialogue.dc.html create mode 100644 mockups/Inventory.dc.html create mode 100644 mockups/Main Window + Wireframes.dc.html create mode 100644 mockups/Pause Menu.dc.html create mode 100644 mockups/Quest Log.dc.html create mode 100644 mockups/README.md create mode 100644 mockups/Shop.dc.html create mode 100644 mockups/Title Screen.dc.html create mode 100644 mockups/World Map.dc.html create mode 100644 mockups/support.js diff --git a/mockups/Character Creation.dc.html b/mockups/Character Creation.dc.html new file mode 100644 index 0000000..d404122 --- /dev/null +++ b/mockups/Character Creation.dc.html @@ -0,0 +1,191 @@ + + + + + + + + + + + + + +
+ + +
+
+
WHO WILL YOU BE?
+ +
+
+
CHARACTER
MODEL
+
+
+ +
+
{{ displayName }}
+
{{ raceName }} · {{ className }}
+
+ +
+
THE DM SIZES YOU UP
+
{{ originText }}
+
+
+ + +
+
+
Create Your Character
+
THE WORLD HAS REAL PROBLEMS AND DOES NOT CARE ABOUT YOU
+
+ + +
+
RACE
+
+ +
+
{{ race.name }}
+
{{ race.desc }}
+
{{ race.trait }}
+ +
+
CHOSEN
+
+
+
+
+
+ + +
+
CALLING
+
+ +
+
{{ cls.name }}{{ cls.trait }}
+
{{ cls.desc }}
+ +
+
CHOSEN
+
+
+
+
+
+ + +
+
+ ABILITY SCORES + points left: {{ remaining }} + ⟳ re-roll + rolled 3d6 — that roll is your floor, spend up only +
+
+ +
+ +
+
PRIMARY
+
+
{{ a.key }}
+
{{ a.val }}
+
rolled {{ a.base }} {{ a.plusLabel }}
+
+
+
+
+
+
+ + +
+
+
NAME
+ +
+
+
ENTER THE WORLD
+ no second chances out there +
+
+
+ +
+
+ + + diff --git a/mockups/Character Sheet.dc.html b/mockups/Character Sheet.dc.html new file mode 100644 index 0000000..ddc1193 --- /dev/null +++ b/mockups/Character Sheet.dc.html @@ -0,0 +1,221 @@ + + + + + + + + + + + + + +
+ + +
+
+
CHARACTER
+ +
+
PORTRAIT
+
+ +
+
+
Vexcca the Nameless
+
HUMAN · SELLSWORD · LEVEL 4
+
+ +
+
EXPERIENCE3,450 / 5,000
+
+
+ +
+
HP 42 / 60
+
MP 18 / 30
+
+ +
+ ⚠ Infected bite — left arm + Hungover +
+
+
+ + +
+ +
+
+
Character Sheet
+
WHAT YOU ARE, WRITTEN DOWN — AND WHAT'S WRONG WITH YOU
+
+
+
PROF
+2
+
HIT DIE
d10
+
ESC ✕
+
+
+ + +
+ + +
+
+
ATTRIBUTES
+
+ +
+
{{ a.key }}
+
{{ a.mod }}
+
score {{ a.val }}
+
+
+
+
+
+
SAVING THROWS
+
+ +
+ {{ s.dot }} + {{ s.key }} + {{ s.val }} +
+
+
+
+
+ + +
+
+
IN A FIGHT
+
+ +
+
{{ c.label }}
+
{{ c.val }}
+
+
+
+
+
+
SKILLS
+
+ +
+ {{ k.dot }} + {{ k.name }} + {{ k.abil }} + {{ k.val }} +
+
+
+
+
+ + +
+
+
TALENTS
+
+ +
+
{{ t.name }}
+
{{ t.desc }}
+
+
+
+
+
+
AFFLICTIONS
+
+ +
+
{{ x.name }}
+
{{ x.desc }}
+
+
+
+
+
+
THE DM'S NOTES
+
They call you Nameless because you burned the papers that said otherwise. Four contracts, three scars, and one debt that keeps finding you no matter which city you bleed in.
+
+
+ +
+
+ +
+
+ + + diff --git a/mockups/Combat HUD.dc.html b/mockups/Combat HUD.dc.html new file mode 100644 index 0000000..be261fb --- /dev/null +++ b/mockups/Combat HUD.dc.html @@ -0,0 +1,434 @@ + + + + + + + + + + + + + +
+
+ + +
+
+
ROUND
+
{{ round }}
+
+
+ +
+
{{ u.nowLabel }}
+
+ {{ u.glyph }} +
+
+
+
{{ u.name }}
+
+
+
+
+ + +
+ + +
+
+
YOUR PARTY · AI
+
+ +
+
{{ a.glyph }}
+
+
{{ a.name }}
+
+
+
{{ a.hpText }}
+
+
+
+
+ +
+
⌂ THE DUNGEON MASTER
+
+ +
{{ l.text }}
+
+
+
+
+ + +
+
{{ turnTitle }}
+
+ + +
+
+ + +
+
+ +
+
+
+ {{ k.glyph }} +
+
+
+
+
+ + +
{{ overText }}
+
+
+
ISOMETRIC BATTLEFIELD · placeholder tokens & tiles
+
+ + +
+
ENEMIES · click to target
+
+ +
+
{{ e.glyph }}
+
+
{{ e.name }}◎ TARGET
+
+
{{ e.hpText }}· AC {{ e.ac }}{{ e.statusText }}
+
+
+
+
+
+ +
+ + +
+ +
+
VX
+
+
Vexcca
+
HP {{ pHpText }}
+
+
ACTION
+
BONUS
+
MOVE 30ft
+
+
+
+ + +
+ +
+
{{ ab.costLabel }}{{ ab.key }}
+
{{ ab.name }}
+
{{ ab.desc }}
+
+
+
+ + +
+
{{ turnSub }}
+
{{ btnLabel }}
+
+
+ +
+
+ + + diff --git a/mockups/Dialogue.dc.html b/mockups/Dialogue.dc.html new file mode 100644 index 0000000..7f663e7 --- /dev/null +++ b/mockups/Dialogue.dc.html @@ -0,0 +1,208 @@ + + + + + + + + + + + + + +
+ +
+
SCENE · GALLOWSMOOR · THE BACK ALLEY, RAINING
+ +
+ + +
+ +
NPC PORTRAIT
+ +
+
Grud Ninefingers
+
BEASTFOLK · FENCE · KNOWS EVERYONE
+ +
+
DISPOSITION{{ dispLabel }}
+
+
+
+
+ + +
+ +
{{ banner }}
+
+ +
{{ speaker }}
+ + +
{{ narration }}
+
+ + +
“{{ line }}”
+
+ +
+ + +
YOUR RESPONSE
+
+ +
+ {{ c.num }} + + {{ c.tag }} + + {{ c.text }} + + 🔒 {{ c.note }} + +
+
+
+
+ +
+
+
+ + + diff --git a/mockups/Inventory.dc.html b/mockups/Inventory.dc.html new file mode 100644 index 0000000..e1140a3 --- /dev/null +++ b/mockups/Inventory.dc.html @@ -0,0 +1,292 @@ + + + + + + + + + + + + + +
+ + +
+
+ +
+
EQUIPMENT
+
Vexcca the Nameless
+
HUMAN · SELLSWORD · LVL 4
+
+ +
+
+ +
PAPER
DOLL
+ + +
+
+ + {{ s.glyph }} +
+
+ + {{ s.label }} + + +
+
+
+
+
+
+
+ +
+
ARMOR
14
+
DAMAGE
12–17
+
HP
42/60
+
MP
18/30
+
+
+ + +
+ +
+
+
The Satchel
+
EVERYTHING YOU'VE STOLEN, LOOTED OR REFUSED TO DROP
+
+
+
GOLD
◈ 1,240
+
+
ENCUMBRANCE{{ weightText }}
+
+
+
ESC ✕
+
+
+ +
+ +
{{ t.label }}
+
+
+ +
+ +
+ + {{ c.glyph }} +
+ {{ c.qtyLabel }} +
+ +
+
+
+
+
+ +
+ +
+ +
+ {{ selGlyph }} +
+
+ +
+
{{ selName }}
+
{{ selType }}
+
+ +
▸ {{ line }}
+
+
+
"{{ selFlavor }}"
+
+ +
+
WEIGHT{{ selWeight }}
+
VALUE◈ {{ selValue }}
+
+ +
EQUIP
+
+ +
USE
+
+
DROP
+
+
+
+
+ +
Select something. Turn it over in your hands.
+
+
+
+ +
+
+ + + diff --git a/mockups/Main Window + Wireframes.dc.html b/mockups/Main Window + Wireframes.dc.html new file mode 100644 index 0000000..82a3184 --- /dev/null +++ b/mockups/Main Window + Wireframes.dc.html @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + +
+
21b built out — full 1920×1080
+

1b at real scale. Added a slide-in system dock on the right edge of the world (Inventory / Character / Quests / Map / Spellbook / Party) — click the tab to slide it in and out. The bottom row is now labeled as quick-select consumables pulled from your inventory, with an End Turn control for the turn-based loop, and the narration book gains a free-text action line for that tabletop feel.

+
+
+
2aThe full main window. Try the dock tab (right edge of the world). Everything is placeholder art — real isometric render, item icons and portraits drop into the marked slots.
+
+ + +
+
+
ISOMETRIC WORLD VIEWparty & enemies on the tile grid · real render here
+ + +
+
TURN ORDER
+
+
EL
17
+
DW
12
+
HU
09
+
+
+
+ + +
MINIMAP
+ + +
+
I
Inventory
+
C
Character
+
J
Quest Log
+
K
Spellbook
+
M
World Map
+
P
Party
+
+ +
{{ handleArrow }}
+ + +
+ +
+
HP 42 / 60
+
MP 18 / 30
+
GOLD◈ 1,240 gp
+
+ +
+
QUICK-SELECT · consumables from inventory
+
+
1Salve
+
2Draught
+
3Bomb
+
4Antidote
+
5Ration
+
6Torch
+
7
+
8
+
+
+ +
+
END TURN
+ ↵ enter +
+
+
+ + +
+
+ THE DUNGEON MASTER + ROUND 4 · THE LOWER WARD +
+
+

The alley stinks of piss and rot. Rain beads on the cobbles and runs black into the gutter. Your left arm has gone hot to the touch — the bite you took in the sewers three nights ago is turning, and the fever with it.

+

The beastfolk fence weighs your coin purse in one clawed hand, unhurried. His good eye drifts from the silver to the soft flesh of your throat, and back again.

+

"Thin," he says. "For what you're asking."

+

✗ Sleight of Hand (14) failed. His grip closes hard around your wrist.

+
+
+
▸ Your turn. What do you do?
+
+
1 · Pay the fence double and keep your hand where he can see it
+
2 · Draw your blade — he's closer than his guards
+
3 · Lie — tell him the rest is coming with your dwarf
+
+
+ + …or describe your own action +
+
+
+ +
+
+
+

Try next: "the dock should be icons, not text labels" · "add a combat-log tab to the narration book" · "now sketch the character-creation screen" · "sketch the inventory screen the I button opens" · "make the book parchment darker / grittier"

+
+ +
+
1Main window — 4 layout directions
+

1920×1080 in-game HUD. Common ingredients across all four: an isometric tile view, a turn-by-turn Dungeon Master narration feed, HP / MP / stamina, a keyed action hotbar, and inventory / currency quick-slots. What changes is where the narration lives and how ornate the frame is — the two axes that matter most for a DnD-table feel. Rough sketches; ignore exact colors.

+
+ + +
+
1aImmersive orb bar (Diablo 2 / Grim Dawn). Full-bleed world, everything floats over it. Narration is a translucent log you can collapse. Most game-first, least reading-focused.
+
+
ISOMETRIC WORLD VIEWparty & enemies on the tile grid
+ +
+
TURN ORDER
+
+
EL
+
DW
+
HU
+
+
+
+ +
MINIMAP
+ +
+
⌂ DUNGEON MASTER ▾
+
The alley stinks of piss and rot. Your left arm's gone hot — the bite is turning. The fence weighs your coin, then your throat.
+
␣ continue · click to expand log
+
+ +
+ +
HP 42/60
+ +
+
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
+
+ +
+
+
+
+
+
+
+
◈ 1,240 gp
+
+
MP 18/30
+
+
+
+
+ + +
+
1bTabletop split — the DM's screen. World on the left, a permanent narration column on the right like an open book. Best for reading-heavy, story-first turns. Leans hardest into the DnD-table feel.
+
+ +
+
ISOMETRIC WORLD VIEW
+ +
+
TURN ORDER
+
+
EL
+
DW
+
HU
+
+
+
+ +
MINIMAP
+ +
+
+
HP 42/60
+
MP 18/30
+
GOLD◈ 1,240 gp
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ THE DUNGEON MASTER + ROUND 4 +
+
+

The alley stinks of piss and rot. Rain beads on the cobbles. Your left arm has gone hot to the touch — the bite from the sewers is turning.

+

The beastfolk fence weighs your coin in one clawed hand, then looks at your throat.

+

✗ Sleight of Hand failed. His grip closes on your wrist.

+
+
+
▸ Your turn. What do you do?
+
+
1 · Pay the fence double
+
2 · Draw your blade
+
3 · Lie about the coin
+
+
+
+
+
+ + +
+
1cCinematic narration box. World up top, a wide DM text box across the bottom with inline choices — classic JRPG/CRPG framing. Balances world and story; hotbar tray straddles the seam.
+
+ +
+
ISOMETRIC WORLD VIEW
+ +
+
EL
+
+
+
+
+
+
+
MINIMAP
+
+ +
+
1
+
2
+
3
+
4
+
5
+
6
+
+
+
+ +
+
+
THE DUNGEON MASTERROUND 4
+

The alley stinks of piss and rot. Your left arm's gone hot — the bite is turning. The beastfolk fence weighs your coin, then your throat. Your Sleight of Hand failed; his grip closes on your wrist.

+
+
+
▸ What do you do?
+
1 · Pay double
+
2 · Draw your blade
+
3 · Lie about the coin
+
+
+
+
+ + +
+
1dOrnate gothic frame. A carved border wraps the whole screen; stats and map sit in corner cartouches, narration in a footer ribbon. Most decorative — the "dark fantasy" end of the range.
+
+ +
ISOMETRIC WORLD VIEW
+ +
+
+ +
+
+
+
+ +
+
EL
+
+
HP
+
MP
+
ST
+
+
+ +
MINIMAP
+ +
+ THE DM ▸ + The alley stinks of piss and rot; your arm's gone hot with fever. The fence weighs your coin, then your throat. Sleight of Hand failed. +
+ +
+
+
+
1
+
2
+
3
+
4
+
5
+
6
+
+
+
+
+
+ +
+

Try next: "expand 1b to full 1920×1080 detail" · "combine 1a's orbs with 1b's narration column" · "sketch the character-creation screen" · "sketch the inventory / character hub tabs" · "make 1d less ornate"

+
+
+ + + diff --git a/mockups/Pause Menu.dc.html b/mockups/Pause Menu.dc.html new file mode 100644 index 0000000..b019181 --- /dev/null +++ b/mockups/Pause Menu.dc.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + +
+ +
+ + +
+
+ + +
+
+
+
THE WORLD WAITS
+
Paused
+
CHAPTER II · THE LOWER WARD
+
+
+
▸ Resume
+
Save Game
+
Load Game
+
Quit to Title
+
+
+
PLAYTIME · 14:22:07
+
AUTOSAVED · 6 min ago
+
Permadeath is on. Be careful out there.
+
+
+ + +
+
+
Settings
+
RESTORE DEFAULTS
+
+ +
+ +
{{ t.label }}
+
+
+ +
+ +
+
+
{{ c.label }}
+
{{ c.desc }}
+
+
+ + {{ c.valueLabel }} + + + +
+
+ + + + +
+ + {{ k }} + +
+
+
+
+
+
+
+ +
+
+
+ + + diff --git a/mockups/Quest Log.dc.html b/mockups/Quest Log.dc.html new file mode 100644 index 0000000..2e3f018 --- /dev/null +++ b/mockups/Quest Log.dc.html @@ -0,0 +1,264 @@ + + + + + + + + + + + + + +
+ + +
+
+ +
+
JOURNAL
+
Deeds & Debts
+
+ +
+ +
{{ t.label }}
+
+
+ +
+ +
+
+ {{ q.title }} + +
+
+ {{ q.typeLabel }} + · + {{ q.loc }} + {{ q.progressText }} +
+
+
+
+
+ + +
+ + +
COMPLETED
+
+ +
FAILED
+
+ + +
+
{{ selTypeLabel }}
+
{{ selTitle }}
+
+ GIVEN BY · {{ selGiver }} + WHERE · {{ selLoc }} +
+
+ + +
{{ selDesc }}
+ + +
+
OBJECTIVES
+
+ +
+ {{ o.box }} + {{ o.text }} + CURRENT +
+
+
+
+ + +
+
+
ON COMPLETION
+
+
GOLD
◈ {{ rewardGold }}
+
XP
{{ rewardXp }}
+ +
ITEM
{{ ri }}
+
+
+
+ +
+
{{ trackLabel }}
+
Abandon
+
+
+
+
+ +
+
+ + + diff --git a/mockups/README.md b/mockups/README.md new file mode 100644 index 0000000..54d7028 --- /dev/null +++ b/mockups/README.md @@ -0,0 +1,210 @@ +# Handoff: Code of Conquest — Game UI + +## Overview +**Code of Conquest** is a single-player, **top-down / isometric, turn-based RPG** in a gritty high-fantasy world ("The Margreave"). The party is the player character plus **AI-controlled companions** (NPCs); the story is delivered turn-by-turn by a **"Dungeon Master" (DM)** narrator, in the spirit of a tabletop D&D session. Target platform: **Godot 4.7**. Target render resolution: **1920×1080** (16:9). + +This bundle contains **11 UI screens** designed as a single, consistent visual system. They are meant to be translated into **Godot 4.7 scenes** (Control-node UI). + +**Tone — read this first.** The whole game is written and styled to be **"gritty, not grim."** The reference is Batman/DC comics, *not* Warhammer and *not* Care Bears. People get hangovers, wounds get infected, people curse and bleed — but it is not nihilistic. All placeholder copy in these files reflects that tone and can be reused or rewritten in the same voice. Recurring tagline: *"The world has real problems, and it does not care about you."* + +--- + +## About the Design Files +The files here are **design references authored in HTML/CSS/JS** — high-fidelity prototypes showing the intended look, layout, and behavior. **They are not production code to ship.** The task is to **recreate each screen as a Godot 4.7 scene** using Godot's own UI system (Control nodes, Theme resources, `TextureRect`/`NinePatchRect`, `Label`/`RichTextLabel`, `Container` nodes) and to port the interaction logic to **GDScript**. Where these mocks use placeholder boxes (dashed rectangles labeled "PORTRAIT", "KEY ART", "ISOMETRIC WORLD VIEW", etc.), those are **art slots** to be filled with real game art. + +### How the files are built (and how to view them) +Each screen is a **`.dc.html` component**: an HTML file with a template plus a small JavaScript logic class (a React-like `Component` with `state` + a `renderVals()` method). They depend on the runtime **`support.js`** included in this folder. To preview a screen, serve this folder over a local static server (e.g. `python3 -m http.server`) and open the `.dc.html` file — opening via `file://` may not load `support.js`. + +**You do not need to run them to implement them.** Each `.dc.html` is readable as plain source: +- The **template** section = the markup + inline styles (all styling is inline; there are no external stylesheets). This gives you exact layout, sizes, and colors. +- The **`class Component extends DCLogic` / `renderVals()`** section = the **state model and interaction logic** (initiative order, point-buy math, shop economy, dialogue tree, etc.). Port this logic to GDScript. + +### Fidelity +**High-fidelity.** Colors, typography, spacing, and interactions are final-intent. Recreate the UI faithfully, then swap placeholder art boxes for real assets. One exception: **`Main Window + Wireframes.dc.html`** contains *both* the hi-fi main HUD (top, labeled option "2a") *and* four earlier low-fi wireframe explorations (below it) kept for context — build from **2a**. + +--- + +## Global Design System + +### Canvas +- Every screen is a fixed **1920×1080** stage. Design for a 16:9 reference resolution and anchor/scale from there. +- Overlays (Pause Menu) dim + blur the scene behind them. + +### The core layout motif — "Dark Bay + Parchment" +Almost every screen is a **two-panel split**: +- **Dark "bay"** (left on most screens; right on Dialogue/Shop): a textured near-black panel for portraits, the world/battlefield, a merchant, or an NPC. Built from a **diagonal stripe** `repeating-linear-gradient(135deg, #26221c 0, #26221c 16px, #221e18 16px, #221e18 32px)` plus a **radial vignette** overlay `radial-gradient(120% 70% at 50% 38%, transparent 32–45%, rgba(0,0,0,.5–.62) 100%)`. +- **Parchment "sheet/journal/page"** (the content side): vertical gradient `linear-gradient(180deg, #ece2ca, #e4d8bd)`, with content cards in `#f1e8d2` bordered `#bfa878`. +- The two panels are divided by a **4px `#1a1512` border** (`border-left`). + +Keep this split and these two textures consistent — it's the game's UI identity. + +### Color tokens (exact hex) +**Backgrounds / dark** +- Near-black stage: `#0a0806`, `#0f0d0b` +- Dark bay stripe: `#26221c` / `#221e18` (also seen `#201a14`/`#1a150f`, `#211d17`/`#1d1914`) +- Dark translucent panels: `rgba(18,15,11,.7)`, `rgba(14,11,9,.85)`, `rgba(10,8,6,.9)` +- Dark borders (light→dark): `#6b6152`, `#5a5348`, `#4a4238`, `#3a352e`, `#1a1512` + +**Parchment** +- Sheet gradient: `#ece2ca` → `#e4d8bd`; log/page variants `#e7dcc4`→`#ddd0b2`, `#ece2ca`→`#e4d8bd` +- Cards: `#f1e8d2`; darker insets `#e6d9b8`, `#e4d6b6`, `#d9c9a2`, `#d3c39a` +- Parchment borders: `#bfa878`, `#cbb684`, `#a99464`, `#8f7a4a` +- Ink text: headings `#3a2f1c` / `#4a3520`, body `#4a3f2c`, muted `#6a5a3a` / `#7a684a`, labels `#5a4326` +- Parchment gold-ink accents: `#8f6a2a`, `#8a7748`, `#a9885a` + +**Text on dark** +- Bone/cream (primary): `#e8d9b6`, `#e8ddc8`, `#f0e6d0`, `#f4e6cf` +- Secondary: `#c9bda3`, `#d8ccb2` +- Muted mono: `#9a8f78`, `#8a7f6c`, `#7a7060`, `#5a5040` + +**Accent hues (all deliberately muted / desaturated for grit)** +- **Blood red** (primary CTA, danger, selection, enemies): `#8f3a34` (core), `#a5423a`, `#7a2f29`, `#b8524a`, `#a5442e`, `#6f2620` +- **Gold / brass** (highlights, currency, primary-stat, "current turn"): `#a9843f`, `#c8963f`, `#8f6a2a`, `#a5772a`, `#c9a45a`, `#d9b46a` +- **Steel blue-teal** (allies, mana): `#5f8a9a`, `#4f7ab8`, `#243f6f`, `#9caccb` +- **Green** (stamina, success, "cleared"): `#5f8a4f`, `#8f9a4f`, `#7a9a5a`, `#2f7a4a` + +**Stat bars** +- HP: `linear-gradient(90deg, #b8524a, #8f3a34)` on track `#2a251f` border `#4a4238` +- MP: `linear-gradient(90deg, #4f7ab8, #243f6f)` +- Stamina: `#8f9a4f` + +**Rarity (items)** — colored 3px bar at the bottom of each item tile + used for item-name text: +- common `#8a8378` · uncommon `#5f8a4f` · rare `#4f7ab8` · epic `#8a5fb8` · legendary `#c8963f` + +### Typography +Three families, used consistently: +1. **Georgia** (serif) — headings, the logo (bold up to 116px), and all **narrative/DM/flavor body text** (often *italic*). This is the "prose" voice. +2. **Architects Daughter** (Google Font, handwritten) — accent display type: nameplates, big stat numbers, section flourishes, DM-header labels, hand-inked feel. Load it, or substitute a comparable hand-drawn font in Godot. +3. **Monospace** (`ui-monospace`/system mono) — all **UI chrome**: labels, keybinds, numeric readouts, tags/chips. Usually **UPPERCASE** with `letter-spacing` `0.06–0.24em`. +- Plain **system-ui sans** is used for button labels and item names. + +### Reusable patterns (build these as shared Godot components/Theme styles) +- **Item tile**: rounded square, 2px rarity-colored border on filled slots, a short mono glyph/abbreviation centered (placeholder for an icon), a 3px rarity bar near the bottom, optional `×N` stack count. Empty slots are dashed/recessed. +- **Selection states**: red (`#8f3a34`) ring + a small mono tag (`CHOSEN` / `◎ TARGET`) for the player's pick; **gold** (`#8f6a2a`/`#c8963f`) ring + `PRIMARY` tag for system-highlighted things. +- **Tag/chip**: mono uppercase, ~10–11px, colored text + matching 1px border + faint tinted background; semantic color (red = aggressive/danger, gold = pay/faith/primary, grey = neutral/locked). +- **Tabs**: pill buttons; active = solid `#8f3a34` bg with cream text; inactive = `#efe6cf`/dark with muted text + `#cbb684` border. +- **Primary button (CTA)**: `linear-gradient(180deg, #a5423a, #7a2f29)`, `#8f3a34` border, cream text, `box-shadow: 0 4px 16px rgba(143,58,52,.35)`; hover brightens to `#b8524a`/`#8f3a34`. +- **Danger/"FAILED"/"COMBAT" stamps**: 4px colored border, heavy mono uppercase, rotated ~ -6° to -11°, ~72% opacity. +- **Art-slot placeholder**: dashed `#5a5348` or `#4a4238` border, a stripe fill, and a centered mono caption naming what art goes there. + +--- + +## Screens / Views + +> For each screen: read the `.dc.html` template for exact pixel values and the `Component` class for the state model. Summaries below cover purpose, layout, key components, interactions, and state. + +### 1. Title Screen — `Title Screen.dc.html` +- **Purpose:** Main menu / first impression. +- **Layout:** Full-bleed atmospheric background; left-aligned content column (`padding: 0 120px`) vertically centered. +- **Components:** Background = warm-to-black radial `radial-gradient(120% 90% at 70% 20%, #2a2018, #160f0a 45%, #0a0806)` + a faint diagonal-stripe key-art placeholder + an off-screen fire glow (`radial-gradient` bottom, animated opacity) + heavy vignette + **drifting ember particles** (10 small colored dots, `@keyframes ember` rising 460px over 7–11s with fade). Logo: kicker "A DARK FANTASY RPG" (mono, `letter-spacing:.42em`, `#a9843f`), title **"CODE OF CONQUEST"** (Georgia 800, 116px, `#e8ddc8`, two lines), a gold rule, and the tagline (Architects Daughter, 20px, `#b0a488`). Menu list below. +- **Interactions / state:** `state.sel` (index). Menu items: New Game, Continue (with save-info subline "CHAPTER II · VEXCCA · 14:22:07"), Load Game, Settings, Credits, Quit. A gold ◆ marker shows on the selected item (opacity toggle); selected item text brightens to `#f0e6d0`; hover brightens any item. Footer: version "v0.4.1 · BUILT IN GODOT 4.7 · © 2026" (left) and a `[↑ ↓] CHOOSE [↵] ENTER` hint (right). + +### 2. Character Creation — `Character Creation.dc.html` +- **Purpose:** Build a character before play. +- **Layout:** Dark portrait bay (left, 660px) + parchment character sheet (right, 1260px). +- **Left (bay):** "WHO WILL YOU BE?" label, a tall **model art slot** with ‹ › rotate arrows, a live **nameplate** (`displayName` in Architects Daughter + "`RACE · CLASS`"), and a **"THE DM SIZES YOU UP"** parchment inset showing a live gritty **origin blurb**. +- **Right (sheet):** Header; **RACE** (4 selectable cards: Human / Elf / Dwarf / Beastfolk, each with a gritty one-liner + trait); **CALLING** (5 cards: Sellsword / Cutpurse / Hedge-Mage / Bonesetter / Trapper, each tagged with its primary stat); **ABILITY SCORES**; footer with a **NAME** text field + "ENTER THE WORLD" CTA. +- **Stats (important, game-specific):** Six stats exist — **STR · DEX · CON · FTH (Faith) · MAG (Magic) · LCK (Luck)** — but only **five are visible**. **LCK is hidden from the player entirely**: it is rolled silently in the background (kept in state, never rendered — no number, no bar, no row); the player only ever "feels" it through opening-scene prose. Do not surface LCK anywhere in the UI. + - Each visible stat is **rolled to a base via 3d6** (die/formula is flagged `TUNABLE` in the logic — treat as configurable). The rolled value is a **hard floor**. + - There is a shared **+3 spend-up pool**, **additive only**: a `+`-only stepper per row, a "points left" counter, **no minus button**. A green `+N` badge shows points added to a row. + - The selected **class's primary stat** is highlighted (gold ring + `PRIMARY` tag): Sellsword→STR, Cutpurse→DEX, Hedge-Mage→MAG, Bonesetter→FTH, Trapper→DEX. + - A **⟳ re-roll** re-rolls all floors (including hidden LCK) and clears spent points. +- **State:** `race`, `cls`, `rolled{STR..LCK}`, `spent{5 visible}`, `name`. Selecting race/class updates highlight + the origin blurb + nameplate. See `renderVals()` for the point-buy guards. + +### 3. Character Sheet — `Character Sheet.dc.html` +- **Purpose:** Persistent character overview (opened in-game with **C**). +- **Layout:** Dark portrait bay (left, 560px) + parchment sheet (right, 1360px, three columns). +- **Left:** portrait slot, nameplate, **XP bar**, **HP/MP** vitals, and prominent **condition badges** (e.g. "⚠ Infected bite — left arm", "Hungover"). +- **Right, col A:** **ATTRIBUTES** (modifier-forward: big signed modifier + small score; 5 visible stats, LCK still hidden) and **SAVING THROWS** (proficiency ● / ○ dots + signed value). +- **Right, col B:** **"IN A FIGHT"** derived-stat grid (Armor, Initiative, Speed, Carry, Melee, Ranged, Spell DC, Death Saves) and a two-column **SKILLS** list (proficiency dot + ability tag + value). +- **Right, col C:** **TALENTS** (name + description cards), **AFFLICTIONS** (gritty, color-coded by severity red `#8f3a34` / amber `#a5772a`), and a **"THE DM'S NOTES"** bio (italic, dashed-border parchment). +- **State:** mostly static display; `renderVals()` computes ability modifiers `floor((score-10)/2)` and skill/save values from a `+2` proficiency bonus. Example character: Vexcca, human Sellsword, lvl 4. + +### 4. Inventory — `Inventory.dc.html` +- **Purpose:** Equipment + carried items (opened with **I**). +- **Layout:** Dark paper-doll (left, 620px) + parchment "Satchel" (right, 1300px). +- **Left:** a central model slot with **12 equipment slots** positioned around it (head, amulet, chest, cloak, gloves, belt, mainhand, offhand, ring×2, legs, boots); filled slots show a rarity bar; a derived stat strip (Armor / Damage / HP / MP). +- **Right:** header with **gold** and an **encumbrance bar** (fills amber→red via `weightColor`), **category tabs** (All / Weapons / Armor / Consumables / Misc) that filter live, a **12×5 item grid**, and an **item detail card** at the bottom. +- **Interactions / state:** `sel` (selected item id), `tab`. Clicking any grid item or equipped slot selects it → detail card updates (rarity-colored name, type, stat lines, flavor, weight/value, and context actions Equip / Use / Drop shown conditionally). Tabs filter the grid. Item data (name, cat, rarity, glyph, weight, value, stats, flavor) is in the `items()` map — reuse it as seed data. + +### 5. World Map — `World Map.dc.html` +- **Purpose:** Region travel (opened with **M**). +- **Layout:** Parchment map (left, flex) + dark location-detail panel (right, 560px). +- **Map:** header ("The Margreave" + subtitle), a **legend**, the map surface (radial parchment; **terrain illustration is an art slot**), a **compass** and **scale bar**. On it: **location markers** — settlements = circles, dungeons/ruins = diamonds, hostile = red, uncharted = grey **fog "?" markers**; the current town **pulses**. **Roads** are dashed SVG ``s between locations; a **red planned-route line** runs from the current location to the selected one. +- **Detail panel:** selected location's type + status tag, a **danger rating (☠)**, gritty description, any **quests here**, and travel time + a "SET OUT ▸" CTA (or "You are here" at the current node). +- **Interactions / state:** `sel` (location id). Clicking a *discovered* marker selects it (uncharted markers are non-interactive). Locations carry `x,y` (0–100 %), `status` (current/hostile/discovered/undiscovered), `danger` (0–4), `desc`, `quests[]`. Travel time is computed from map distance. Marker/road projection math is in `renderVals()`. + +### 6. Quest Log — `Quest Log.dc.html` +- **Purpose:** Track quests (opened with **J**). +- **Layout:** Dark journal (left, 620px) + parchment "open page" (right, 1300px). +- **Left:** **tabs with counts** (Active / Completed / Failed) and a scrollable quest list — each entry shows type color (main = gold, side = green, bounty = red), location, progress (`2/4`), and a ★ on the tracked quest. +- **Right:** selected quest's type/giver/location, a DM-voice brief, an **objectives checklist** (done items struck through; the active one flagged `CURRENT`), and an **"ON COMPLETION"** rewards row (gold / XP / item). **Track (★/☆)** + Abandon for active quests; completed/failed quests get a rotated **COMPLETED / FAILED stamp**. +- **State:** `sel`, `tab`, `tracked`. Switching tab auto-selects the first quest in it. Quest data (type, status, giver, loc, desc, objectives[{text,done}], rewards) is in `questData()`. + +### 7. Shop — `Shop.dc.html` +- **Purpose:** Buy/sell with a fence. +- **Layout:** Parchment wares (left, 1180px) + dark merchant panel (right, 740px). +- **Left:** header with **your purse** + **the merchant's coin**, **Buy/Sell tabs**, and item rows (rarity tile, name/type, stock-or-owned qty, price). +- **Right:** merchant "Grud Ninefingers" (portrait slot + a **reaction line** that changes as you deal), the selected item's detail, the deal price, and a BUY/SELL CTA. +- **Interactions / state (a real economy loop):** `mode` (buy/sell), `sel`, `gold`, `merchantGold`, `stock{}`, `owned{}`, `msg`. **Buy** deducts your gold, moves one unit from stock→owned, adds to merchant coin. **Sell** pays you ~40% of value, moves item owned→stock, deducts merchant coin. The CTA **disables with a gritty reason** when you're broke, the merchant is tapped out, an item is sold out, or worthless (value 0). Buy price = item value; sell price = `round(value*0.4)`. + +### 8. Pause Menu / Settings — `Pause Menu.dc.html` +- **Purpose:** In-game pause + settings (overlay). +- **Layout:** The scene is **dimmed + blurred** (`rgba(8,6,4,.72)` + `backdrop-filter: blur(2px)`); a centered **1560×920 ornate-framed panel** (8px `#1a1512` border + inner 1.5px `#a9843f` hairline). Left rail (dark, 400px) + right settings (parchment). +- **Left rail:** "Paused", chapter, action buttons (Resume [primary] / Save Game / Load Game / Quit to Title), and footer status (playtime, last autosave, a permadeath warning). +- **Right:** **SETTINGS** with four tabs — **Gameplay / Video / Audio / Controls** — each rendering a list of controls. +- **Controls (fully interactive):** custom **toggle switches**, native **range sliders** (styled — thumb `#8f6a2a`, track `#cbb684`), native **selects**, and **keybind rows** (key-cap chips). Gameplay: difficulty select, permadeath/gore/tutorial toggles, narration-speed slider, autosave select. Video: window mode/resolution selects, brightness slider, VSync/motion-blur/damage-flash toggles. Audio: master/music/effects/ambience sliders + subtitles toggle. Controls: keybinds (WASD, E, LMB, ↵, I, C, J, M) + mouse-sensitivity slider + invert-Y toggle. Note the keybinds here are the canonical in-game key map. +- **State:** `tab` + a `s{}` settings object. Controls are generated per tab in `renderVals()` (`slider`/`toggle`/`select`/`keybind` helpers) — a good template for a data-driven settings menu in Godot. + +### 9. Dialogue / Conversation — `Dialogue.dc.html` +- **Purpose:** Branching NPC conversation. +- **Layout:** Dimmed scene; NPC side (left, 640px) + parchment conversation (right, flex). +- **Left:** NPC portrait slot, nameplate (Grud the fence), and a **live disposition meter** (Hostile → Wary → Neutral → Warming, color-shifting bar). +- **Right:** speaker label, a DM-voice **narration beat** (italic), the NPC's **spoken line** (Georgia 34px, in “curly quotes”), and a numbered **response list**. +- **Choices:** each response can carry a **skill/approach tag chip** (`INTIMIDATE · STR`, `HAGGLE · DEX`, `FAITH`, `PAY ◈40` — colored red/gold/grey) and can be **gated/disabled** (🔒 with a requirement note). +- **Interactions / state:** `node` (current dialogue node), `disp` (0–100 disposition). Selecting a response navigates a real **dialogue tree** (`nodes()`), applies a disposition delta (`dd`), and can dead-end into a **COMBAT** stamp or a clean exit; terminal nodes offer to restart. Port `nodes()` as your dialogue-graph data. + +### 10. Combat HUD / Turn Tracker — `Combat HUD.dc.html` +- **Purpose:** The turn-based fight screen. **The player controls only their own character; the DM resolves the AI companions' and enemies' turns.** +- **Layout (flex column):** top **turn tracker** ribbon → middle row (left party+log, center battlefield, right enemies) → bottom command bar. +- **Turn tracker:** every combatant in **initiative order** with mini HP; the active unit is lifted + pulsing (`@keyframes cturn`); downed units marked ✕; color-coded (gold = you, teal = AI party, red = enemies) + a **ROUND** counter. +- **Center — isometric battle board:** a **7×6 diamond-tile grid** (CSS `clip-path` diamonds, iso projection `x=(c-r)*44, y=(c+r)*23`) with **unit tokens** on tiles (colored disc + initial + ground shadow + HP bar). Live with state: active unit's tile outlines **gold** and its token pulses; on your turn reachable tiles tint **teal (move range, Manhattan ≤2)**; the targeted enemy's tile outlines red and its token gets a **dashed red reticle**; dead units grey out; a VICTORY/DEFEAT stamp overlays on end. This is the placeholder for the real isometric battlefield — token positions come from `POS()`. +- **Left:** "YOUR PARTY · AI" companion cards (HP) + a parchment **DM combat log** (newest first). +- **Right:** **ENEMIES** list — click to **target** (selected gets a red border + `◎ TARGET`). +- **Bottom:** active-hero panel (portrait, HP, **action-economy pips**: Action ● / Bonus ● / Move) + **ability bar** (Falchion Slash, Reckless Swing, Shield Bash, Second Wind, Bile Bomb — each tagged ACTION/BONUS with a keybind) + the **turn-action button** (End Turn on your turn; "RESOLVE {name} ▸" on an AI turn). +- **Interactions / state:** `hp{}` per unit, `turnPos` (index into initiative order), `round`, `target`, `used{action,bonus}`, `over` (null/victory/defeat), `log[]`. On your turn: select an enemy, then use abilities (each spends its Action/Bonus, rolls damage from the ability's range, applies to target or heals self, logs a DM line); **End Turn** advances initiative (resetting your economy when it returns to you). On an AI turn: **Resolve** plays out that unit's action (enemies hit your side, allies hit enemies) with a narrated log line, then advances. Combat ends at all-enemies-down (victory) or player-down (defeat). Unit stats (init, max HP, AC, glyph) are in `base()`; abilities in `abilityDefs()`. **This state machine is the spec for your GDScript turn manager.** + +### 11. Main Window + Wireframes — `Main Window + Wireframes.dc.html` +- **Purpose:** The core exploration HUD (the "tabletop split" main game screen). **Build from option "2a" at the top** — the four items below it are earlier low-fi wireframes kept only for context. +- **2a layout:** isometric world view (left, ~1180px) + a permanent **narration "book"** (right, ~740px) where the DM tells the story turn-by-turn with inline response choices. +- **World side:** a **turn-order rail** (initiative portraits, active ringed gold, downed ✕), a **minimap**, a **slide-in system dock** (Inventory / Character / Quest Log / Spellbook / World Map / Party — toggled by a gold tab, animates in/out), and a bottom command bar: **HP/MP bars + gold**, a **quick-select consumables** row (drawn from inventory), and an **End Turn** button. +- **Book side (parchment):** "THE DUNGEON MASTER" header, narration paragraphs, an action-result callout, numbered response choices, and a free-text **"describe your own action"** line (reinforcing the tabletop feel). +- **State:** `dockOpen` (the system dock slide toggle). The rest is layout. + +--- + +## Godot 4.7 Translation Notes +- **Resolution:** author scenes at 1920×1080; set the project's stretch mode to `canvas_items` with a 1920×1080 reference and `keep` aspect (or `expand`) so the fixed layouts scale cleanly. +- **Palette + type → Theme:** put the color tokens and the three font families into a shared **`Theme`** resource with named `StyleBox`es for the recurring pieces (parchment card, dark panel, primary button, tab active/inactive, item tile, tag/chip). Every screen pulls from it — that's what keeps the look consistent. +- **Panels/containers:** the two-panel splits map to `HBoxContainer`/`HSplitContainer`; stat/skill/ability grids to `GridContainer`; lists to `VBoxContainer` (+ `ScrollContainer`). Use `MarginContainer` for the paddings noted in the templates. +- **Textures:** parchment gradient, dark stripe, and vignette are best baked as texture assets (or `NinePatchRect` + a shader) rather than reproduced as many gradient nodes. Art slots (portraits, key art, terrain, isometric world/battlefield) become `TextureRect`/`SubViewport`. +- **Narrative text:** use `RichTextLabel` for DM narration / quest briefs / dialogue lines (supports the italic + inline colored callouts). +- **Logic → GDScript:** the `Component` classes are the behavior spec. The highest-value ports: the **combat turn manager** (Combat HUD), the **dialogue graph** (Dialogue `nodes()`), the **shop economy** (Shop), and **point-buy character creation** (with the hidden-LCK rule). Reuse the seed data maps (items, quests, locations, units, abilities) as starting `Resource`s / dictionaries. +- **Fonts:** "Architects Daughter" (Google Fonts, OFL) and Georgia — bundle equivalents; if Georgia is unavailable, substitute a comparable serif and keep weights/sizes. + +## Assets +No real art is included — all imagery is **placeholder art slots** (dashed boxes with captions naming what belongs there): character portraits/models, world/battlefield isometric renders, the world-map terrain illustration, key art, item/ability icons (currently short mono glyphs like `FAL`, `SLV`, `RNG`), and NPC/merchant mugshots. Icons are represented by 2–4-letter abbreviations to be replaced with real icon assets. Fonts: Architects Daughter (Google Fonts) + Georgia (system serif). + +## Files +- `Title Screen.dc.html` +- `Character Creation.dc.html` +- `Character Sheet.dc.html` +- `Inventory.dc.html` +- `World Map.dc.html` +- `Quest Log.dc.html` +- `Shop.dc.html` +- `Pause Menu.dc.html` +- `Dialogue.dc.html` +- `Combat HUD.dc.html` +- `Main Window + Wireframes.dc.html` (build from option **2a**) +- `support.js` — runtime required only to *preview* the `.dc.html` files in a browser; not needed for the Godot implementation. diff --git a/mockups/Shop.dc.html b/mockups/Shop.dc.html new file mode 100644 index 0000000..1e72c89 --- /dev/null +++ b/mockups/Shop.dc.html @@ -0,0 +1,247 @@ + + + + + + + + + + + + + +
+ + +
+ +
+
+
The Crooked Coin
+
NO REFUNDS · NO QUESTIONS · NO CITY GUARD
+
+
+
YOUR PURSE
◈ {{ gold }}
+
GRUD'S COIN
◈ {{ merchantGold }}
+
+
+ +
+ +
{{ t.label }}
+
+
+ +
+ +
+
+ {{ r.glyph }} +
+
+
+
{{ r.name }}
+
{{ r.typeLabel }}
+
+
{{ r.qtyText }}
+
◈ {{ r.price }}
+
+
+
+
+ + +
+
+ +
+
MUG
SHOT
+
+
Grud Ninefingers
+
BEASTFOLK FENCE · GALLOWSMOOR
+
+
+
{{ merchantMsg }}
+ +
+ + +
+
+
+ {{ selGlyph }} +
+
+
+
{{ selName }}
+
{{ selType }}
+
+ +
▸ {{ line }}
+
+
+
+
+
"{{ selFlavor }}"
+
+ + +
+
+ {{ priceLabel }} + ◈ {{ dealPrice }} +
+
{{ actionLabel }}
+
{{ actionNote }}
+
+
+ +
+
+ + + diff --git a/mockups/Title Screen.dc.html b/mockups/Title Screen.dc.html new file mode 100644 index 0000000..c116e75 --- /dev/null +++ b/mockups/Title Screen.dc.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +
+ + +
+
KEY ART DROPS IN HERE
a lone figure · the gallows · rain
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
+
A DARK FANTASY RPG
+
CODE OF
CONQUEST
+
+
The world has real problems, and it does not care about you.
+
+ + +
+ +
+ +
+ {{ m.label }} +
{{ m.sub }}
+
+
+
+
+
+ + +
v0.4.1 · BUILT IN GODOT 4.7 · © 2026
+
[ ↑ ↓ ] CHOOSE   [ ↵ ] ENTER
+ +
+
+ + + diff --git a/mockups/World Map.dc.html b/mockups/World Map.dc.html new file mode 100644 index 0000000..c2319a6 --- /dev/null +++ b/mockups/World Map.dc.html @@ -0,0 +1,221 @@ + + + + + + + + + + + + + +
+ + +
+ +
+
+
WORLD MAP
+
The Margreave
+
A DROWNED, DEBT-RIDDEN CORNER OF THE WORLD
+
+ +
+
Settlement
+
Dungeon / Ruin
+
Hostile
+
Uncharted
+
+
+ + +
+ +
HAND-DRAWN REGION MAP ART DROPS IN HERE
coastline · mires · roads · the mountains that hate you
+ +
+ + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
?
+
+ +
+
+
+
{{ m.name }}
+
+
+ + +
+
N
+
+
+ +
≈ 3 DAYS' WALK
+
+
+ + +
+
+
LOCATION
+
+
{{ selName }}
+
+ {{ selType }} + {{ selStatus }} +
+
+ +
+ DANGER + {{ dangerFull }}{{ dangerEmpty }} +
+ +
{{ selDesc }}
+ + +
+
QUESTS HERE
+
+ +
{{ q }}
+
+
+
+
+ +
+ +
You are here. For now.
+
+ +
TRAVEL TIME{{ travelDays }}
+
SET OUT ▸
+
+
+
+ +
+
+ + + diff --git a/mockups/support.js b/mockups/support.js new file mode 100644 index 0000000..0b22f5e --- /dev/null +++ b/mockups/support.js @@ -0,0 +1,1717 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + if (!window.__resources) { + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + } + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/bundled.ts + function bundledBlob(url) { + const blobs = window.__resourceBlobs; + const b = blobs ? blobs[url.split("#")[0]] : void 0; + return b instanceof Blob ? b : null; + } + + // src/cdn.ts + var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; + var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; + var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; + var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + s.crossOrigin = "anonymous"; + } + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => { + const pre = bundledBlob(url); + if (pre) return pre.text(); + return fetch(url).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + const res = window.__resources; + const pre = res ? res[url] : void 0; + const target = typeof pre === "string" && pre ? pre : url; + const blob = bundledBlob(target); + (blob ? blob.text() : fetch(target).then((res2) => { + if (!res2.ok) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '" failed:', + url, + "returned", + res2.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res2.text(); + })).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '":', + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + '[dc-runtime] sibling fetch for "' + name + '" threw:', + url, + e + ) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: