⋯}
+ items={[
+ { label: "Rename", icon: "✏️", onClick: rename },
+ { label: "Duplicate", icon: "📋", onClick: duplicate },
+ { divider: true },
+ { label: "Delete", icon: "🗑️", danger: true, onClick: remove },
+ ]}
+/>
+```
+
+`align="end"` hangs the panel from the trigger's right edge (for menus near the right edge of the viewport).
diff --git a/Web app designs/components/overlay/Modal.d.ts b/Web app designs/components/overlay/Modal.d.ts
new file mode 100644
index 0000000..a3902dc
--- /dev/null
+++ b/Web app designs/components/overlay/Modal.d.ts
@@ -0,0 +1,19 @@
+import type { ReactNode } from "react";
+
+export interface ModalProps {
+ open: boolean;
+ onClose?: () => void;
+ title?: ReactNode;
+ children: ReactNode;
+ /** Footer slot — typically right-aligned Button(s). */
+ footer?: ReactNode;
+ className?: string;
+}
+
+/**
+ * Modal — centered glass dialog over a blurred scrim. Click the scrim or the
+ * ✕ to close (wire `onClose`). Render conditionally on `open`.
+ *
+ * @dsCard group="Components"
+ */
+export function Modal(props: ModalProps): JSX.Element | null;
diff --git a/Web app designs/components/overlay/Modal.jsx b/Web app designs/components/overlay/Modal.jsx
new file mode 100644
index 0000000..1950723
--- /dev/null
+++ b/Web app designs/components/overlay/Modal.jsx
@@ -0,0 +1,17 @@
+export function Modal({ open, onClose, title, children, footer, className = "" }) {
+ if (!open) return null;
+ return (
+ { if (e.target === e.currentTarget) onClose && onClose(); }}>
+
+ {title != null && (
+
+
{title}
+ ✕
+
+ )}
+
{children}
+ {footer &&
{footer}
}
+
+
+ );
+}
diff --git a/Web app designs/components/overlay/Modal.prompt.md b/Web app designs/components/overlay/Modal.prompt.md
new file mode 100644
index 0000000..1c35e70
--- /dev/null
+++ b/Web app designs/components/overlay/Modal.prompt.md
@@ -0,0 +1,14 @@
+**Modal** — centered glass dialog. Scrim dims + blurs the page; the panel pops in.
+
+```jsx
+const [open, setOpen] = useState(false);
+ setOpen(false)} title="Remove channel"
+ footer={<>
+ setOpen(false)}>Cancel
+ Remove
+ >}>
+ This disconnects the channel and deletes its saved overlays.
+
+```
+
+Always control visibility via `open` (unmount when closed, don't just hide) so entrance animation replays each time.
diff --git a/Web app designs/components/overlay/Tooltip.d.ts b/Web app designs/components/overlay/Tooltip.d.ts
new file mode 100644
index 0000000..fefbb75
--- /dev/null
+++ b/Web app designs/components/overlay/Tooltip.d.ts
@@ -0,0 +1,16 @@
+import type { ReactNode } from "react";
+
+export interface TooltipProps {
+ label: ReactNode;
+ /** The element that triggers the tooltip on hover. */
+ children: ReactNode;
+ className?: string;
+}
+
+/**
+ * Tooltip — small dark bubble that appears above its child on hover. Pure
+ * CSS (no portal/positioning lib) — fine for icon buttons and short labels.
+ *
+ * @dsCard group="Components"
+ */
+export function Tooltip(props: TooltipProps): JSX.Element;
diff --git a/Web app designs/components/overlay/Tooltip.jsx b/Web app designs/components/overlay/Tooltip.jsx
new file mode 100644
index 0000000..52350a3
--- /dev/null
+++ b/Web app designs/components/overlay/Tooltip.jsx
@@ -0,0 +1,8 @@
+export function Tooltip({ label, children, className = "" }) {
+ return (
+
+ {children}
+ {label}
+
+ );
+}
diff --git a/Web app designs/components/overlay/Tooltip.prompt.md b/Web app designs/components/overlay/Tooltip.prompt.md
new file mode 100644
index 0000000..983b50d
--- /dev/null
+++ b/Web app designs/components/overlay/Tooltip.prompt.md
@@ -0,0 +1,9 @@
+**Tooltip** — hover bubble for icon buttons and truncated labels.
+
+```jsx
+
+ 📋
+
+```
+
+Wraps its child inline; positions above center. Keep labels to a few words.
diff --git a/Web app designs/components/overlay/overlay.card.html b/Web app designs/components/overlay/overlay.card.html
new file mode 100644
index 0000000..7c56dbf
--- /dev/null
+++ b/Web app designs/components/overlay/overlay.card.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/about.html b/Web app designs/github-pages/about.html
new file mode 100644
index 0000000..c3f0f79
--- /dev/null
+++ b/Web app designs/github-pages/about.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+About — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
+
+
BearddOddity
+
Variety streamer. Solo developer. Same person, same energy, different keyboard shortcuts.
+
+
+
+
+
What I'm about
+
I stream variety — whatever game's got my attention that week — on Twitch and Kick as BearddOddity . Off stream (and sometimes on it), I build the tools I actually use for streaming myself, starting with StatusForge , a native presence engine that keeps my channel's category accurate without a browser extension or a subscription.
+
Everything here is built solo — no team, no VC, just whatever gets the job done well enough to ship, then gets refined live in front of chat. If you watch me stream, there's a decent chance you're watching the next feature get built (or break).
+
This site is the home base for that: the tools, the guides for setting them up, and the running log of what's changing.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/assets/logos/bearddoddity-mascot-head.png b/Web app designs/github-pages/assets/logos/bearddoddity-mascot-head.png
new file mode 100644
index 0000000..944ec4f
Binary files /dev/null and b/Web app designs/github-pages/assets/logos/bearddoddity-mascot-head.png differ
diff --git a/Web app designs/github-pages/assets/logos/bearddoddity-mascot-headshot.svg b/Web app designs/github-pages/assets/logos/bearddoddity-mascot-headshot.svg
new file mode 100644
index 0000000..53284aa
--- /dev/null
+++ b/Web app designs/github-pages/assets/logos/bearddoddity-mascot-headshot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Web app designs/github-pages/assets/logos/bearddoddity-mascot.svg b/Web app designs/github-pages/assets/logos/bearddoddity-mascot.svg
new file mode 100644
index 0000000..a2eb62c
--- /dev/null
+++ b/Web app designs/github-pages/assets/logos/bearddoddity-mascot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Web app designs/github-pages/assets/logos/joystick-mark.svg b/Web app designs/github-pages/assets/logos/joystick-mark.svg
new file mode 100644
index 0000000..3515877
--- /dev/null
+++ b/Web app designs/github-pages/assets/logos/joystick-mark.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Web app designs/github-pages/assets/logos/streamerbot.svg b/Web app designs/github-pages/assets/logos/streamerbot.svg
new file mode 100644
index 0000000..a948dd5
--- /dev/null
+++ b/Web app designs/github-pages/assets/logos/streamerbot.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Web app designs/github-pages/assets/nav.js b/Web app designs/github-pages/assets/nav.js
new file mode 100644
index 0000000..ecb9272
--- /dev/null
+++ b/Web app designs/github-pages/assets/nav.js
@@ -0,0 +1,11 @@
+// Mobile nav toggle for The Oddity Forge site — shared across pages.
+document.addEventListener("DOMContentLoaded", () => {
+ const burger = document.querySelector(".bd-header-burger");
+ const panel = document.querySelector(".bd-header-mobile-panel");
+ if (!burger || !panel) return;
+ burger.addEventListener("click", () => {
+ const open = panel.style.display === "flex";
+ panel.style.display = open ? "none" : "flex";
+ burger.setAttribute("aria-expanded", String(!open));
+ });
+});
diff --git a/Web app designs/github-pages/assets/statusforge-icon.png b/Web app designs/github-pages/assets/statusforge-icon.png
new file mode 100644
index 0000000..d1756ce
Binary files /dev/null and b/Web app designs/github-pages/assets/statusforge-icon.png differ
diff --git a/Web app designs/github-pages/blog/index.html b/Web app designs/github-pages/blog/index.html
new file mode 100644
index 0000000..f7a72d1
--- /dev/null
+++ b/Web app designs/github-pages/blog/index.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+Devlog — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
Devlog
+
Notes on what's being built, broken, and fixed — usually written right after it happened on stream.
+
+
+
+
+
+
+
🛠️
+
First post coming soon
+
This is where devlog entries will land as they're written. Check back after the next build session.
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/disclosure.html b/Web app designs/github-pages/disclosure.html
new file mode 100644
index 0000000..4248f28
--- /dev/null
+++ b/Web app designs/github-pages/disclosure.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+Disclosure — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
Disclosure
+
What's behind this site and the software it links to — plainly stated.
+
+
+
+
+
AI-assisted development
+
Code and site content here are built with Claude (Anthropic) as a development assistant — writing, reviewing, and shipping is directed and reviewed by me. Nothing on this site or in StatusForge is AI-generated without a human deciding it goes in.
+
+
Third-party game data
+
The Game Database and cover art shown for StatusForge aren't original content — metadata is scraped and merged from public APIs:
+
+ Steam
+ GOG
+ RAWG
+ IGDB (Twitch-authenticated)
+ SteamGridDB (cover art / logos)
+
+
All trademarks, cover art, and game names belong to their respective owners.
+
+
Data & privacy
+
StatusForge doesn't collect analytics or telemetry. OAuth tokens for Twitch/Kick are stored in your OS's own keychain, not sent anywhere except directly to Twitch/Kick's own APIs when pushing a category update. The only outbound network calls the app makes on its own are: the platform APIs you connect (Twitch/Kick), the metadata sources above (only when you scan a game), and a GitHub Releases check for app updates.
+
+
Unsigned software
+
Windows builds aren't signed with a paid code-signing certificate, so SmartScreen will warn on first run — this is expected, not a sign something's wrong. macOS builds are unsigned and un-notarized (no Apple Developer account behind this project yet), so macOS requires a manual right-click → Open the first time.
+
+
Last updated: July 2026
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/guides/index.html b/Web app designs/github-pages/guides/index.html
new file mode 100644
index 0000000..8c6aaaf
--- /dev/null
+++ b/Web app designs/github-pages/guides/index.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+Guides — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
Setup Guides
+
Step-by-step walkthroughs for getting the tools running.
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/guides/overlay-setup.html b/Web app designs/github-pages/guides/overlay-setup.html
new file mode 100644
index 0000000..0ad839d
--- /dev/null
+++ b/Web app designs/github-pages/guides/overlay-setup.html
@@ -0,0 +1,109 @@
+
+
+
+
+
+Overlay Setup Guide — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
StatusForge Guide
+
Adding Overlays to OBS / Streamlabs
+
Get your game/category info showing on screen in under a minute.
+
+
+
+
+
1. Open the Dashboard
+
Launch StatusForge and make sure the engine is running (top of the Dashboard shows the engine status). The overlay picker lives on the Dashboard — pick from four layouts:
+
+ Horizontal Left / Horizontal Right — game cover + title, anchored to one side
+ Vertical — tall layout for side-panel placement
+ Logo Only — just the game's logo, no cover art or text
+
+
+
2. Copy the widget URL
+
Each overlay is served locally by StatusForge itself — no external hosting, no account needed. The URL follows this pattern:
+
http://127.0.0.1:53735/forge-widget/<your-widget-token>/<layout>.html
+
Your widget token is unique per install and shown (partially masked) on the Dashboard. Copying a layout from the picker copies the full URL with the token already filled in — you shouldn't need to type it by hand.
+
+
3. Add it as a Browser Source
+
+ In OBS/Streamlabs, add a new source → Browser
+ Paste the widget URL into the URL field
+ Set the width/height to match your chosen layout (table below)
+ Leave Shutdown source when not visible unchecked, so it keeps polling in the background
+
+
+
+
+ Layout Width Height
+
+ Horizontal Left / Right 850 480
+ Vertical 360 620
+ Logo Only 560 280
+
+
+
+
+
+
ℹ️
+
The overlay fades in automatically when a game is detected and fades out after your configured fade timer (Settings → Detection Engine). No game running = no overlay on screen, so it's safe to leave the Browser Source active for your whole stream.
+
+
+
Troubleshooting
+
+ Blank overlay: confirm the engine is running (Dashboard status) and that 127.0.0.1:53735 isn't blocked by a firewall rule.
+ Wrong game showing: check Settings → Detection Engine for the scan interval/grace period — a very short grace period can flicker between games during alt-tabbing.
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/guides/spark-dual-pc.html b/Web app designs/github-pages/guides/spark-dual-pc.html
new file mode 100644
index 0000000..3251a2d
--- /dev/null
+++ b/Web app designs/github-pages/guides/spark-dual-pc.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+SPARK Dual-PC Setup Guide — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
StatusForge Guide
+
SPARK Dual-PC Link Setup
+
Game on one PC, stream from another — no cloud relay, signed over your own LAN.
+
+
+
+
+
+
+
→ udp/53735 →
+
Streaming PC
runs StatusForge (Hub)
+
+
+
1. Install on both machines
+
+ SPARK on the PC that's actually running the game
+ StatusForge on the PC that's streaming/broadcasting
+
+
Both need to be on the same local network (LAN or same Wi-Fi).
+
+
2. Set a matching PIN
+
Set the same 4-digit PIN in both apps:
+
+ SPARK: main window, PIN field
+ StatusForge: Settings → Detection Engine → SPARK Dual-PC Link
+
+
Optionally set a matching pairing key on both sides too — it's mixed into the shared secret for a stronger signature than the PIN alone.
+
+
3. Let them find each other
+
SPARK broadcasts signed heartbeats on udp/53735; the StatusForge Hub announces itself on udp/53736. Packets with the wrong PIN, missing signature, or a tampered payload are rejected outright. Once paired, your overlays update exactly as they would from local detection — same widgets, same behavior.
+
+
+
ℹ️
+
Enabling the SPARK link on the StatusForge side stops that PC's own local scanner from reporting detections, so you don't get two conflicting game-state sources fighting each other.
+
+
+
Firewall notes
+
+ Windows: the installer adds firewall allow rules automatically (and removes them on uninstall) — nothing to do manually.
+ Linux: allow the two UDP ports if you run a firewall: sudo ufw allow 53735/udp && sudo ufw allow 53736/udp
+ macOS: accept the incoming-connection prompt on first run, or add the app manually under System Settings → Network → Firewall → Options.
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/index.html b/Web app designs/github-pages/index.html
new file mode 100644
index 0000000..d195d38
--- /dev/null
+++ b/Web app designs/github-pages/index.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+The Oddity Forge — BearddOddity
+
+
+
+
+
+
+
+
+
+
+
+
Live & Building
+
The Oddity Forge
+
Streamer tools, built and battle-tested on stream. I'm BearddOddity — variety streamer by night, solo developer by... also night. This is where the tools I build, the games I play, and the setup guides for both all live.
+
+
+
+
+
+
+
+
Projects
+
What's shipped and downloadable right now.
+
+
+
+
+
+
StatusForge
+
+
Native presence engine for streamers. Detects the game you're playing and broadcasts live status to Twitch, Kick, and your overlay widgets — pure Rust, no Python, no cloud dependency.
+
+ 🪟 Windows
+ 🍎 macOS
+ 🐧 Linux
+
+
+
+
More projects coming as they're made public.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/statusforge/download.html b/Web app designs/github-pages/statusforge/download.html
new file mode 100644
index 0000000..be9fa19
--- /dev/null
+++ b/Web app designs/github-pages/statusforge/download.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+Download StatusForge — BearddOddity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Download StatusForge
+
Pick your platform below. All installers are signed for auto-update and built by CI from source.
+
+
+
+
+
v0.5.0
+
+
+
+
🍎
+
macOS
+
Apple Silicon (M-series) only. Unsigned/un-notarized — right-click the app → Open, or run xattr -dr com.apple.quarantine on it.
+
Disk Image .dmg
+
+
+
+
+ See all releases on GitHub
+
+
+
+
+
+
+
+
diff --git a/Web app designs/github-pages/statusforge/index.html b/Web app designs/github-pages/statusforge/index.html
new file mode 100644
index 0000000..70d7ef0
--- /dev/null
+++ b/Web app designs/github-pages/statusforge/index.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+StatusForge — The Oddity Forge
+
+
+
+
+
+
+
+
+
+
+
+
v0.5.0
+
+
StatusForge
+
A native presence engine for streamers. Detects the game you're playing and broadcasts live status to Twitch, Kick, and your overlay widgets — pure Rust, no Python, no cloud dependency.
+
+
+ 🪟 Windows
+ 🍎 macOS
+ 🐧 Linux
+
+
+
+
+
+
+
How it works
No account, no cloud relay — everything runs locally.
+
+
1
Detect
A native scanner watches running processes and window titles to figure out what you're playing.
+
2
Look up
Matched against a local game database, merged from Steam, GOG, RAWG, IGDB, and SteamGridDB metadata.
+
3
Broadcast
Twitch/Kick category updates go out automatically, and your overlay widgets update in real time.
+
+
+
+
+
+
+
+
+
🎮
Native Game Detection
Pure Rust process/window-title scanner — no browser extensions, no manual game list to maintain.
+
📡
Twitch & Kick Category Sync
Automatic category updates on game change, with built-in cooldowns to stay well under platform rate limits.
+
🖥️
Overlay Widgets
Four layouts (horizontal left/right, vertical, logo-only) served locally as Browser Sources for OBS/Streamlabs.
+
🔗
SPARK Dual-PC Link
Detect on your gaming PC, stream from another — signed heartbeats over your own LAN, no cloud in between.
+
🗂️
Scraped Metadata Library
Cover art and game info merged from multiple sources into a local, editable game database.
+
🔒
Local-First, No Telemetry
OAuth tokens live in your OS keychain. Platform pushes can be toggled off entirely if you just want local overlays.
+
+
+
+
+
+
+
Set it up
+
Guides for getting overlays and dual-PC linking running.
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/guidelines/brand-integrations.card.html b/Web app designs/guidelines/brand-integrations.card.html
new file mode 100644
index 0000000..aae655e
--- /dev/null
+++ b/Web app designs/guidelines/brand-integrations.card.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
Joystick.tv
+
+
+
+
Streamer.bot
+
+
diff --git a/Web app designs/guidelines/brand-marks.card.html b/Web app designs/guidelines/brand-marks.card.html
new file mode 100644
index 0000000..71a2490
--- /dev/null
+++ b/Web app designs/guidelines/brand-marks.card.html
@@ -0,0 +1,17 @@
+
+
+
+
+
StatusForge
Rich presence engine
+
StreamerSuite
Streamer toolkit hub
+
+
BearddOddity
Studio mark (compact)
+
BearddOddity Mascot
Full mascot — hero & about use
+
diff --git a/Web app designs/guidelines/color-accent.card.html b/Web app designs/guidelines/color-accent.card.html
new file mode 100644
index 0000000..7b98710
--- /dev/null
+++ b/Web app designs/guidelines/color-accent.card.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
CTA Gradient
#9333EA → #4F46E5
+
diff --git a/Web app designs/guidelines/color-platform.card.html b/Web app designs/guidelines/color-platform.card.html
new file mode 100644
index 0000000..68dcb0b
--- /dev/null
+++ b/Web app designs/guidelines/color-platform.card.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
Streamer.bot
#A257ED → #0B72FF
+
Joystick.tv
#06899C → #0EE6C8
+
+
+
diff --git a/Web app designs/guidelines/color-semantic.card.html b/Web app designs/guidelines/color-semantic.card.html
new file mode 100644
index 0000000..d25000b
--- /dev/null
+++ b/Web app designs/guidelines/color-semantic.card.html
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/Web app designs/guidelines/color-surfaces.card.html b/Web app designs/guidelines/color-surfaces.card.html
new file mode 100644
index 0000000..4fd676e
--- /dev/null
+++ b/Web app designs/guidelines/color-surfaces.card.html
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/Web app designs/guidelines/color-text.card.html b/Web app designs/guidelines/color-text.card.html
new file mode 100644
index 0000000..cb6ab9e
--- /dev/null
+++ b/Web app designs/guidelines/color-text.card.html
@@ -0,0 +1,16 @@
+
+
+
+
+
Strong — headings & values white / .92
+
Body — default reading text white / .80
+
Muted — nav, secondary white / .55
+
Faint — labels, captions white / .38
+
Dim — placeholders white / .28
+
diff --git a/Web app designs/guidelines/iconography.card.html b/Web app designs/guidelines/iconography.card.html
new file mode 100644
index 0000000..15e780b
--- /dev/null
+++ b/Web app designs/guidelines/iconography.card.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
1 · Inline stroke SVGs — functional UI glyphs
+
+
Spec: stroke="currentColor", stroke-width ~1.5–2, round caps/joins, viewBox 0 0 24 24 — Heroicons-style outline. If a needed glyph isn't in a source repo, match this spec; Lucide or Heroicons (outline) are the sanctioned CDN fallback.
+
+
+
2 · Emoji — compact category glyphs (nav, section heads, app icons)
+
+
Intentional brand texture, not filler — reuse the same emoji for the same concept every time. Never used decoratively in body copy.
+
+
+
3 · Brand/platform logos — see the "Product marks" and "Integrations" cards
+
Twitch, Kick, Joystick.tv, Streamer.bot marks live as first-class SVG assets in assets/logos/, kept in their real colors/shapes rather than redrawn.
+
+
diff --git a/Web app designs/guidelines/motion.card.html b/Web app designs/guidelines/motion.card.html
new file mode 100644
index 0000000..c5d2a6d
--- /dev/null
+++ b/Web app designs/guidelines/motion.card.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
🎮
+
Breathe
+
bd-anim-breathe · 3s
+
+
+
📚
+
Glint
+
bd-glint-overlay · 3.2s
+
+
+
+
Shimmer
+
bd-shimmer-bg · 2.2s
+
+
+
+
Pulse (glow)
+
bd-anim-pulse · 1.8s
+
+
+
+
Breathe — game/cover art while a stream or session is live (slow scale, near-imperceptible).
+
Glint — a light sweep across active cover art; reads as "still working", not decorative.
+
Shimmer — Skeleton loading placeholders only.
+
Pulse — a status dot announcing live/connected state.
+
Interaction timing — 0.15–0.2s ease for hover/press; cubic-bezier(0.16,1,0.3,1) for card/toast entrances; toasts slide in from the right.
+
Reduced motion — every loop and transition collapses to a single 0.01ms frame under prefers-reduced-motion (global rule in effects.css) — nothing here needs a manual opt-out.
+
+
diff --git a/Web app designs/guidelines/spacing-radii.card.html b/Web app designs/guidelines/spacing-radii.card.html
new file mode 100644
index 0000000..2e00b30
--- /dev/null
+++ b/Web app designs/guidelines/spacing-radii.card.html
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/Web app designs/guidelines/spacing-scale.card.html b/Web app designs/guidelines/spacing-scale.card.html
new file mode 100644
index 0000000..4544d8d
--- /dev/null
+++ b/Web app designs/guidelines/spacing-scale.card.html
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/Web app designs/guidelines/type-families.card.html b/Web app designs/guidelines/type-families.card.html
new file mode 100644
index 0000000..4d5d83f
--- /dev/null
+++ b/Web app designs/guidelines/type-families.card.html
@@ -0,0 +1,14 @@
+
+
+
+
+
--font-ui · system · streamer tools chrome Presence engine running
+
--font-sans · Inter · PanelForge Continue reading Vol. 12
+
--font-mono · JetBrains Mono · data tok_9146 · 00:42:18 · 65%
+
diff --git a/Web app designs/guidelines/type-scale.card.html b/Web app designs/guidelines/type-scale.card.html
new file mode 100644
index 0000000..c9ca162
--- /dev/null
+++ b/Web app designs/guidelines/type-scale.card.html
@@ -0,0 +1,17 @@
+
+
+
+
+
3xl · 56 / 700 Forge
+
2xl · 40 / 700 Display heading
+
xl · 28 / 600 Section heading
+
lg · 20 / 600 Card title
+
sm · 13 / 500 Body & navigation text — the default UI size.
+
xs · 12 / 600 Buttons & values
+
micro · 10 / 600 Data label
+
diff --git a/Web app designs/readme.md b/Web app designs/readme.md
new file mode 100644
index 0000000..e92133d
--- /dev/null
+++ b/Web app designs/readme.md
@@ -0,0 +1,144 @@
+# BearddOddity Design System
+
+Design system for **BearddOddity** — a solo-developer studio that builds dark, glassy, high-density desktop tools for streamers and readers. Every product is a Tauri + React app that treats the screen like a piece of gear: near-black canvases, glassmorphism panels, a Twitch-purple accent, and tiny mono readouts everywhere.
+
+This project is the reusable brand surface behind those apps: tokens, fonts, reusable React primitives, foundation specimen cards, and full-screen UI-kit recreations.
+
+---
+
+## Products represented
+
+Five source repositories, three of them with real UI:
+
+| Product | Repo | What it is | Visual world |
+|---|---|---|---|
+| **StreamerSuite** | `BearddOddity/StreamerSuite` | Flagship "ultimate streaming companion" — a plugin-registry shell launching chat, OBS scene switching, sound board, alerts, timers, stats | Forge (near-black + purple) |
+| **StatusForge.io** | `BearddOddity/StatusForge.io` | Rich-presence engine — detects your game/activity and pushes it to Twitch/Kick/Streamer.bot via generated overlays. **The most developed, best-loved surface.** | Forge |
+| **PanelForge** | `BearddOddity/Comic-Reader` | Cross-platform comic reader (desktop/tablet/phone) | Forge (shared palette) |
+| **oauth-callback** | `BearddOddity/oauth-callback` | Cloudflare Worker: one branded OAuth login/callback page for Kick/Twitch/Joystick.tv. Backend only (styled login page). | — |
+| **comic-scraper-api** | `BearddOddity/comic-scraper-api` | Backend catalog/scraper that feeds PanelForge's Browse. No UI. | — |
+
+> **Explore the sources.** These repos are the ground truth for anything you build. If you have access, read them directly — the token values, glass recipes, and screen layouts here were lifted from their real CSS and components, not reconstructed from memory:
+> - https://github.com/BearddOddity/StreamerSuite
+> - https://github.com/BearddOddity/StatusForge.io
+> - https://github.com/BearddOddity/Comic-Reader
+> - https://github.com/BearddOddity/oauth-callback
+> - https://github.com/BearddOddity/comic-scraper-api
+
+Every product shares **one** color palette — no per-app re-skin. `#050505` canvas, Twitch-purple `#9146ff` accent, everywhere: StreamerSuite, StatusForge, and PanelForge alike. PanelForge keeps its own type voice (Inter body font vs. the streamer tools' native system stack) but draws every color from the same tokens as everything else.
+
+---
+
+## CONTENT FUNDAMENTALS
+
+How the products talk. Copy is **functional, lowercase-leaning, and terse** — this is software for people who already know streaming, not marketing.
+
+- **Voice:** confident, plain, a little playful. Product taglines are short and declarative: *"Ultimate Streaming Companion"*, *"All the tools a streamer needs, in one place"*. No exclamation-mark hype.
+- **Person:** addresses the tool's state, not the user. Labels are nouns and imperatives — *Start Engine*, *Browse Overlays*, *Add channel*, *Multi-Chat* — rarely "you".
+- **Casing:** **UPPERCASE micro-labels** for data/state (`LIVE`, `POLLING`, `SYNCED`, `STANDBY`, `CPU`, `GENRE`) at wide letter-spacing; Title Case for buttons and nav; sentence case for descriptions.
+- **Density over prose:** a card says *"12 pads · hotkeys on"*, not a sentence. Middot `·` separates inline facts. Counts are explicit — *"3 channels"*, *"12 / 40"*.
+- **Status language is a fixed vocabulary:** `LIVE` / `OFFLINE`, `Connected` / `Offline`, `Engine Online` / `Engine Offline`, `ON` / `OFF`, `SYNCED` / `STANDBY`, `ongoing` / `completed` / `hiatus`.
+- **Playful moments are earned:** an easter-egg dev unlock toasts *"🔓 Dev Tools unlocked"*; empty states are gentle (*"No channels yet. Click + Add to connect."*). Product/feature names lean on a **"Forge"** motif (StatusForge, PanelForge) and portmanteaus (ChatConfluence, StreamerSuite).
+- **Emoji** are used deliberately as compact glyphs in app icons, nav items and section heads (⏳ 📚 ⚙️ 💬 🎛️ ✨ 🔀 📋) — never as decorative filler in body copy.
+- **Mono for machine text:** URLs, tokens, counts, timestamps, and percentages render in JetBrains Mono / system mono.
+
+---
+
+## VISUAL FOUNDATIONS
+
+The look is **dark glassmorphism with a neon-purple pulse.** Everything is layered black-alpha glass over a near-black canvas, edged with 1px white hairlines and lit by a single accent.
+
+- **Canvas & mood:** `#050505` everywhere. Very dark, cool, high-contrast. A faint accent radial-glow sometimes bleeds from a corner; PanelForge additionally floats a soft page background layer.
+- **Color:** monochrome white-on-black foundation + **one** dominant accent, Twitch purple `#9146ff`, shared by every product. Semantic hues are reserved for state only — green success/`on`, red error/danger, amber warn/`hiatus`, cyan info/`completed`. Platform brand colors (Twitch purple, Kick green `#53fc18`) appear as small accents on chips and chat.
+- **Surfaces / cards:** never solid grey. Cards are `rgba(0,0,0,0.45)` + `backdrop-filter: blur(10–16px)` + `1px solid rgba(255,255,255,0.08)` border + a **white top-inset highlight** (`inset 0 1px 0 rgba(255,255,255,0.04)`) that sells the glass edge. Elevation is a 3-tier alpha ladder (surface-1/2/3) plus a stronger glass tier.
+- **Typography:** system UI stack for app chrome (deliberate "native app" feel); Inter for PanelForge; JetBrains Mono for all data. Runs **small and dense** — 10–13px chrome, uppercase 10px micro-labels — with a big jump to bold display sizes (28–56px) for headings.
+- **Corner radii:** generous. 12px controls, 16px cards/modals, up to 24–32px for PanelForge panels, full pills for badges and status chips.
+- **Borders:** hairline white-alpha (`.05`–`.15`). Focus/active promotes the border to an accent-tinted `color-mix` plus a soft glow.
+- **Shadows:** two systems — **elevation** (soft, large, black: `0 8px 32px rgba(0,0,0,.3)`, modals `0 24px 64px`) and **glow** (accent-colored: `0 0 20px rgba(145,70,255,.15)`, focus ring `0 0 0 3px rgba(145,70,255,.1)`). The inset top-highlight is on almost every glass element.
+- **Transparency & blur:** heavy and intentional. Sidebars, toolbars, modals, selects, toasts and menus all use backdrop-blur so the canvas glow shows through. Backdrops dim to `rgba(0,0,0,.6)` + `blur(6px)`.
+- **Backgrounds:** solid near-black + optional accent radial glow; no stock photos, no busy gradients. PanelForge covers carry animated **shimmer / sparkle / sun-glare** overlays; StatusForge game covers **breathe** (slow scale) and **glint** (a light sweep) while active.
+- **Animation:** quick and eased (`0.2s`), `cubic-bezier(0.16,1,0.3,1)` for card entrances. Toasts **slide in** from the right; modals slide/scale in; status dots **pulse**; covers breathe/glint on infinite loops. Everything respects `prefers-reduced-motion`.
+- **Hover states:** buttons lift `translateY(-1px/-2px)` and gain a shadow; glass cards raise border-alpha + shadow; nav items fill to white-6%; the accent gradient lightens.
+- **Press states:** return to `translateY(0)` (the lift collapses); active nav/toggle gets the accent-tinted fill + glow + a slightly scaled icon tile.
+- **Layout:** fixed app chrome — a left sidebar (240px, collapsible to a 68px icon rail) or a 48px top bar; scrollable main; slim 6px custom scrollbars. Content is card-based and dense, often multi-column grids with vertical hairline dividers.
+
+---
+
+## ICONOGRAPHY
+
+BearddOddity has **no single icon library**; it mixes three approaches, and this system follows the source exactly rather than inventing a set:
+
+1. **Inline stroke SVGs** for functional UI glyphs (search, settings gear, hamburger, chevrons, copy, close). Consistent style: `stroke="currentColor"`, `stroke-width` ~1.5–2, round caps/joins, `viewBox 0 0 24 24` — i.e. **Heroicons-style outline**. When you need an icon not present in the source, match that spec; if you pull from a CDN, **Lucide** or **Heroicons (outline)** are the closest and should be flagged as a substitution.
+2. **Emoji as compact category glyphs** — app icons and nav/section tiles (⏳ 📚 ⚙️ 💬 🎛️ ✨ 🔀 📋 📊 ⏱️ 📝). This is intentional brand texture, not filler. Reuse the same emoji for the same concept.
+3. **Brand/platform logos** as SVG, kept as first-class assets — Twitch, Kick, Joystick.tv, Streamer.bot, in multiple colorways.
+
+**Assets copied into `assets/logos/`:**
+- Platform marks (third-party): `{twitch,kick}-mark.svg` (single-path, `currentColor` — recolor via CSS `color`) plus fixed-color variants `twitch-{black,white,purple,ice}.svg` / `kick-{black,white,green}.svg`; `joystick-mark.svg` (full-color) and `joysticktv-{dark,light}.svg`; `streamerbot.svg` (gradient).
+- First-party marks: `panelforge-mark.svg` (the comic-reader favicon — kept in its own indigo gradient as a standalone logo mark, distinct from the shared UI accent) + `panelforge-icon.png`; `statusroom-{white,black}.svg` (StatusForge's line-art "scan room" glyph).
+- **BearddOddity studio mark (added):** `bearddoddity-mascot-head.png` (400×400 angry-bear head-and-shoulders icon, capped ballcap with "B" patch — use as favicon/avatar/app-icon-sized studio mark), `bearddoddity-mascot-full.webp` (full mascot, arms crossed, neon rim-light — raster fallback), `bearddoddity-mascot.svg` (vector full mascot, arms crossed, cyan/magenta neon rim glow, transparent bg, 4000×4000 viewBox — preferred over the .webp for hero/about use since it scales cleanly), `bearddoddity-wordmark-b.png` (minimal gradient "B" beard-glyph mark, purple→blue — use where a compact single-color-adjacent brand mark is needed, e.g. nav/footer, in place of plain type).
+
+**A real studio mark now exists** (added by the user, July 2026) — the three files above. Use `bearddoddity-wordmark-b.png` for compact brand placement (nav, footer, favicon-adjacent) and `bearddoddity-mascot-head.png` / `-full.webp` for the mascot proper (hero art, about page, merch-style moments). The earlier "render the name in plain type" guidance is superseded; keep plain-type wordmarks only where no mark fits (dense app chrome titles, etc).
+
+---
+
+## Foundation & specimens
+
+Foundation specimen cards live in `guidelines/` and populate the Design System tab (groups **Colors**, **Type**, **Spacing**, **Brand**): accent, semantic, surfaces, text ladder and platform swatches; font families, type scale; spacing scale and radii; integration logos and product marks; a **Motion** card (breathe/glint/shimmer/pulse loops + interaction timing) and an **Iconography** card (stroke-icon spec, emoji set, logo pointer).
+
+---
+
+## Components
+
+Reusable React primitives (grouped under `components/`), each with a `.d.ts` contract, `.prompt.md`, and a `@dsCard` demo. Import from `window.BearddOddityDesignSystem_726917`.
+
+- **core/** — `Button`, `Card`, `Badge`, `Chip`, `StatusDot`, `SectionHead`, `Divider`, `Avatar`, `AvatarGroup`, `StatCard`, `PricingCard`
+- **forms/** — `Input`, `Select`, `RangeSlider`, `FieldSection`, `Checkbox`, `RadioGroup`, `Switch`
+- **feedback/** — `Toast`, `ProgressBar`, `Alert`, `Skeleton`, `EmptyState`, `ToastManager`
+- **navigation/** — `NavItem`, `Sidebar`, `Toolbar`, `Breadcrumbs`, `Pagination`
+- **media/** — `CoverImage`
+- **layout/** — `Container`, `Header`, `Footer`, `Hero` *(web/marketing page structure)*
+- **overlay/** — `Modal`, `Drawer`, `Tooltip`, `Menu`
+- **disclosure/** — `Tabs`, `Accordion`
+- **data/** — `Table`
+
+These map 1:1 to the source apps' shared primitives (`ui.tsx`, `primitives.tsx`, and the unified glass utility classes in each app's `index.css`). Consumers should compose these rather than re-implementing glass panels by hand.
+
+### Intentional additions
+- `CoverImage`, `RangeSlider`, `Divider`, `SectionHead` are promoted to named primitives here because the source repeats them as inline markup/utility classes across screens; naming them keeps consumer kits consistent.
+- `layout/`, `overlay/`, `disclosure/`, `data/`, `Avatar`, `Breadcrumbs`, `Pagination`, `Alert` were added to extend the same dark-glass/purple language (kept as-is, unmodified) to general websites and web apps — landing pages, docs, dashboards — beyond the three source products. They reuse existing tokens; no new colors were introduced. `--font-heading` (Montserrat) was added alongside the existing `--font-ui`/`--font-sans` as an additive display-type option for these surfaces; app chrome is untouched.
+- `Checkbox`, `RadioGroup`, `Switch` (form controls), `Skeleton`, `EmptyState`, `ToastManager` (feedback/loading/zero-state patterns), and `StatCard`/`PricingCard` (promoted from inline markup in the Landing/Dashboard templates) round out what a general web app needs day-to-day. `Header` gained a responsive hamburger/mobile-panel mode below 720px.
+- `Chip` (core), `Sidebar` (navigation) and `Menu` (overlay) were promoted from markup every UI kit/redesign was hand-rolling inline (`.chip` filter pills, a 240px/68px nav rail, a kebab dropdown) — same tokens, now reusable primitives with `.d.ts`/cards.
+
+---
+
+## UI kits
+
+Full-screen, interactive recreations in `ui_kits/` (each a self-contained `index.html`):
+
+- **`ui_kits/statusforge/`** — Status Room dashboard (the showcase). Start/Stop engine, browse overlays, expand metadata.
+- **`ui_kits/streamersuite/`** — app launcher → ChatConfluence multi-platform chat. *(ChatConfluence is legacy — see `templates/multi-chat-viewer` below.)*
+- **`ui_kits/panelforge/`** — comic-reader library → detail → fullscreen reader, same palette as the rest.
+
+**`templates/multi-chat-viewer/`** is the design system's new, modern multi-platform chat surface and is meant to fully replace ChatConfluence (the outdated chat panel inside `ui_kits/streamersuite`). It unifies Twitch, Kick, YouTube, TikTok, Joystick.tv and Rumble into one live feed with split-column and focus views, pinning, moderation actions, cross-platform send, sub/emote-only mode badges, TTS, theme presets, and overlay-style display settings — all on shared design-system tokens/components. New multi-chat work should build on this template, not on ChatConfluence.
+
+---
+
+## Root manifest / index
+
+- `styles.css` — **the one file consumers link.** `@import` manifest only.
+- `tokens/` — `fonts.css`, `colors.css`, `typography.css`, `spacing.css`, `effects.css`, `components.css` (glass utility classes lifted from source).
+- `components/{core,forms,feedback,navigation,media,layout,overlay,disclosure,data}/` — primitives + `.d.ts` + `.prompt.md` + `@dsCard` demos.
+- `templates/` — full-page starting points (`landing-page`, `app-dashboard`, `docs-page`, `login`, `error-404`, `multi-chat-viewer`) built from the components above.
+- `guidelines/` — foundation specimen cards.
+- `ui_kits/{statusforge,streamersuite,panelforge}/` — screen recreations.
+- `assets/logos/` — platform + product marks.
+- `SKILL.md` — Agent-Skills-compatible entry point.
+
+---
+
+## Caveats & substitutions
+
+- **Fonts** (Inter, JetBrains Mono) load from **Google Fonts**, not self-hosted binaries — none were present in the source. Swap in licensed copies under `assets/fonts/` if you have them.
+- The streamer apps intentionally use the **native system font** for chrome; that's a design choice, not a missing font.
+- Icons are mixed inline-SVG + emoji + brand logos (see ICONOGRAPHY). No monolithic icon set is shipped; CDN Lucide/Heroicons-outline is the sanctioned fallback.
+- **Studio logo:** `assets/logos/bearddoddity-mascot-head.png` (icon), `-mascot-full.webp` / `-mascot.svg` (full mascot, SVG preferred), `-wordmark-b.png` (minimal mark). Added July 2026 — supersedes the earlier "no logo, plain type only" rule below.
diff --git a/Web app designs/redesigns/PanelForge Redesign.html b/Web app designs/redesigns/PanelForge Redesign.html
new file mode 100644
index 0000000..9ae76d3
--- /dev/null
+++ b/Web app designs/redesigns/PanelForge Redesign.html
@@ -0,0 +1,174 @@
+
+
+
+
+PanelForge — Redesign
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/redesigns/StatusForge Redesign.html b/Web app designs/redesigns/StatusForge Redesign.html
new file mode 100644
index 0000000..39e1a10
--- /dev/null
+++ b/Web app designs/redesigns/StatusForge Redesign.html
@@ -0,0 +1,188 @@
+
+
+
+
+StatusForge — Redesign
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/redesigns/StreamerSuite Redesign.html b/Web app designs/redesigns/StreamerSuite Redesign.html
new file mode 100644
index 0000000..6ed07b7
--- /dev/null
+++ b/Web app designs/redesigns/StreamerSuite Redesign.html
@@ -0,0 +1,214 @@
+
+
+
+
+StreamerSuite — Redesign
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web app designs/screenshots/01-overlap-check.png b/Web app designs/screenshots/01-overlap-check.png
new file mode 100644
index 0000000..d7b7ce8
Binary files /dev/null and b/Web app designs/screenshots/01-overlap-check.png differ
diff --git a/Web app designs/screenshots/02-overlap-check.png b/Web app designs/screenshots/02-overlap-check.png
new file mode 100644
index 0000000..a1157e2
Binary files /dev/null and b/Web app designs/screenshots/02-overlap-check.png differ
diff --git a/Web app designs/screenshots/alerts-config.png b/Web app designs/screenshots/alerts-config.png
new file mode 100644
index 0000000..ce5c079
Binary files /dev/null and b/Web app designs/screenshots/alerts-config.png differ
diff --git a/Web app designs/screenshots/check.png b/Web app designs/screenshots/check.png
new file mode 100644
index 0000000..055a55f
Binary files /dev/null and b/Web app designs/screenshots/check.png differ
diff --git a/Web app designs/screenshots/collide-check.png b/Web app designs/screenshots/collide-check.png
new file mode 100644
index 0000000..ce5c079
Binary files /dev/null and b/Web app designs/screenshots/collide-check.png differ
diff --git a/Web app designs/screenshots/collide-check2.png b/Web app designs/screenshots/collide-check2.png
new file mode 100644
index 0000000..a1157e2
Binary files /dev/null and b/Web app designs/screenshots/collide-check2.png differ
diff --git a/Web app designs/screenshots/menu-blend.png b/Web app designs/screenshots/menu-blend.png
new file mode 100644
index 0000000..a67c60e
Binary files /dev/null and b/Web app designs/screenshots/menu-blend.png differ
diff --git a/Web app designs/screenshots/menu-blend2.png b/Web app designs/screenshots/menu-blend2.png
new file mode 100644
index 0000000..408005c
Binary files /dev/null and b/Web app designs/screenshots/menu-blend2.png differ
diff --git a/Web app designs/screenshots/menu-open.png b/Web app designs/screenshots/menu-open.png
new file mode 100644
index 0000000..a67c60e
Binary files /dev/null and b/Web app designs/screenshots/menu-open.png differ
diff --git a/Web app designs/screenshots/menu-open2.png b/Web app designs/screenshots/menu-open2.png
new file mode 100644
index 0000000..36a9664
Binary files /dev/null and b/Web app designs/screenshots/menu-open2.png differ
diff --git a/Web app designs/screenshots/overlap-check3.png b/Web app designs/screenshots/overlap-check3.png
new file mode 100644
index 0000000..a1157e2
Binary files /dev/null and b/Web app designs/screenshots/overlap-check3.png differ
diff --git a/Web app designs/styles.css b/Web app designs/styles.css
new file mode 100644
index 0000000..82de8c7
--- /dev/null
+++ b/Web app designs/styles.css
@@ -0,0 +1,13 @@
+/* ============================================================================
+ BearddOddity Design System — global entry point
+ Consumers link THIS file. It is an @import manifest only; all real
+ declarations live in the token files below (and their closure).
+ ============================================================================ */
+
+@import "./tokens/fonts.css";
+@import "./tokens/colors.css";
+@import "./tokens/typography.css";
+@import "./tokens/spacing.css";
+@import "./tokens/effects.css";
+@import "./tokens/components.css";
+@import "./tokens/web-components.css";
diff --git a/Web app designs/support.js b/Web app designs/support.js
new file mode 100644
index 0000000..16da97a
--- /dev/null
+++ b/Web app designs/support.js
@@ -0,0 +1,1687 @@
+// 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);
+ 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 + ">" + t + ">"
+ );
+ 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("(?)" + real + "(?=[\\s>])", "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("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/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 BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js";
+ var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y";
+ 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;
+ babelLoading = new Promise((res, rej) => {
+ const s = document.createElement("script");
+ s.src = BABEL_URL;
+ s.integrity = BABEL_SRI;
+ 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(() => 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;
+ doc.documentElement.dataset.theme = 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";
+ fetch(url).then((res) => {
+ if (!res.ok) {
+ console.error(
+ "[dc-runtime] sibling fetch for <" + name + "/> failed:",
+ url,
+ "returned",
+ res.status,
+ "\u2014 the reference renders as an empty placeholder."
+ );
+ return "";
+ }
+ return res.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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
AlertsHub
+
+
{{ historyCount }} alerts fired today
+
+
+
+
+
+
+
+
+
Overlay Preview
+
OBS Browser Source · transparent
+
+
+
1920×1080 · alerts.html
+
+
+
+
{{ activeAlert.icon }}
+
+
{{ activeAlert.title }}
+
{{ activeAlert.sub }}
+
+
+
+
+
+
+
+
+
+
+
+
Alert Settings
+
+ Enabled
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Sound
+
+
+
+
+
+
+
Accent Color
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t.icon }}
+
+
{{ t.label }}
+
{{ t.desc }}
+
+
+
+
+
+
+
+
+
+
+
Recent Activity
+
last {{ historyCount }}
+
+
+
+
+
{{ h.icon }}
+
+
{{ h.typeLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+