Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ ClawDnD's MCP servers depend on these packages:
- **httpx** (BSD-3-Clause) — HTTP client for the dnd5eapi.co fallback.
- **kokoro** (Apache-2.0) — local text-to-speech (added with the voice backend).

## Bundled browser runtime assets

The local OpenWorlds viewer surface vendors these files under
`viewer/openworlds/vendor/` so the packaged macOS app can render without live CDN
dependencies:

- **React 18.3.1** (`react-18.3.1.development.js`, MIT)
- **ReactDOM 18.3.1** (`react-dom-18.3.1.development.js`, MIT)
- **Babel Standalone 7.29.0** (`babel-standalone-7.29.0.min.js`, MIT)
- **Google Fonts families** (`Cinzel`, `Cormorant Garamond`,
`IM Fell English`, `JetBrains Mono`, open font licenses)

See `viewer/openworlds/vendor/THIRD_PARTY_NOTICES.md` for source URLs and file
hashes. These development-mode runtime files are a fidelity bridge for the first
local packaged surface; release hardening should replace Babel-in-browser with a
compiled bundle while keeping all assets local.

## Structural references — learn-from only (NO code copied)

These projects informed ClawDnD's design. After verifying their licensing
Expand Down
48 changes: 48 additions & 0 deletions viewer/openworlds/SOURCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# OpenWorlds Surface Source Contract

This directory hosts the first exact-fidelity OpenWorlds browser surface for the
local viewer. It is intentionally a viewer asset bundle, not a new engine,
rules, voice, or campaign-state authority.

## Source Artifact

- Source artifact: `OpenWorlds.zip`
- Source SHA256: `8e9e2b885764fd3492b74b2d02eda5db9827eb087121054e8ca52e9ace10fd0a`
- Extraction label used during review: `openworlds-design-2026-05-25/openworlds`
- Provenance: internal design handoff for ClawDnD/OpenWorlds implementation, not
a blanket license for third-party screenshots, uploads, or generated images.

## Included Source Files

The production viewer route maps source files into this directory as follows:

| Source artifact entry | Repo path |
| --- | --- |
| `openworlds/Open Worlds.html` | `viewer/openworlds/index.html` |
| `openworlds/styles.css` | `viewer/openworlds/styles.css` |
| `openworlds/data.js` | `viewer/openworlds/data.js` |
| `openworlds/app.jsx` | `viewer/openworlds/app.jsx` |
| `openworlds/chrome.jsx` | `viewer/openworlds/chrome.jsx` |
| `openworlds/tooltip.jsx` | `viewer/openworlds/tooltip.jsx` |
| `openworlds/toast.jsx` | `viewer/openworlds/toast.jsx` |
| `openworlds/camp-sidebar.jsx` | `viewer/openworlds/camp-sidebar.jsx` |
| `openworlds/screen-*.jsx` | `viewer/openworlds/screen-*.jsx` |

## Intentional Exclusions

- `openworlds/tweaks-panel.jsx` is excluded from the production route. The app
still keeps the prototype tweak defaults in `app.jsx`, but no Open Design
sandbox panel is loaded.
- `openworlds/screenshots/*`, uploaded images, and reference captures are not
included. They remain reference-only unless a later PR clears provenance.
- Live CDN runtime imports are not used. React, ReactDOM, Babel standalone, and
the Google font files required by the prototype are vendored under
`viewer/openworlds/vendor/`.

## State Boundary

For this PR, `data.js` is non-canonical demo data used only to render the exact
surface. Future PRs replace those rows with read-only viewer APIs. The browser
may read viewer endpoints and, once backed actions exist, post player intent to
`POST /move`; it must not write `snapshot.json`, `play-state`, `qa/state`,
inventory, quests, XP, world clocks, or companion state directly.
179 changes: 179 additions & 0 deletions viewer/openworlds/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* App router + tweaks */

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"palette": "warm",
"ornaments": true,
"candle": true
}/*EDITMODE-END*/;

function App() {
const [state, setState] = React.useState(window.INITIAL_STATE || {});
const [screen, setScreen] = React.useState("launcher");
const [campMode, setCampMode] = React.useState(false);
const [t, setTweak] = (window.useTweaks
? window.useTweaks(TWEAK_DEFAULTS)
: [TWEAK_DEFAULTS, () => {}]);

React.useEffect(() => {
document.documentElement.setAttribute("data-palette", t.palette || "warm");
}, [t.palette]);

// Keyboard shortcuts
React.useEffect(() => {
const onKey = (e) => {
const target = e.target;
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (
target instanceof Element &&
(target.closest("input, textarea, select, [contenteditable='true']") ||
target.isContentEditable)
) {
return;
}
const map = {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"t": "table",
"x": "combat",
"p": "dialogue",
"m": "map",
"c": "character",
"i": "inventory",
"f": "forge",
"r": "relations",
"j": "journal",
"b": "bestiary",
"a": "acts",
"$": "merchant",
"w": "launcher",
"n": "create",
"s": "seed",
",": "settings",
"?": "settings",
};
const id = map[e.key.toLowerCase()];
if (id) {
e.preventDefault();
setScreen(id);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);

const navigate = (id, opts) => {
if (opts?.openCamp) setCampMode(true);
setScreen(id);
};

const campaigns = Array.isArray(state?.campaigns) ? state.campaigns : [];
const current =
campaigns.find((c) => c.id === state?.activeCampaign) ||
campaigns[0] ||
{ title: "Open Worlds", day: "" };

return (
<div className="window">
<TitleBar
campaign={current.title}
location={SCREEN_TITLES[screen]}
day={current.day}
/>
<div className="app">
<NavRail current={screen} onNavigate={navigate} />
<div style={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
<TabBar current={screen} onNavigate={navigate} />
<div className="stage" style={{ flex: 1, minHeight: 0 }}>
<div className="parchment">
<div className="stage-inner">
<ScreenRouter screen={screen} state={state} setState={setState} onNavigate={navigate} campMode={campMode} setCampMode={setCampMode} />
</div>
</div>
</div>
</div>
</div>

{window.TweaksPanel && (
<window.TweaksPanel title="Codex Tweaks">
<window.TweakSection label="Palette" />
<window.TweakRadio
label="Tone"
value={t.palette}
onChange={(v) => setTweak("palette", v)}
options={[
{ value: "warm", label: "Warm" },
{ value: "cool", label: "Aged" },
{ value: "dark", label: "Walnut" },
]}
/>

<window.TweakSection label="Atmosphere" />
<window.TweakToggle
label="Candle glow"
value={t.candle}
onChange={(v) => setTweak("candle", v)}
/>
<window.TweakToggle
label="Corner filigree"
value={t.ornaments}
onChange={(v) => setTweak("ornaments", v)}
/>

<window.TweakSection label="Jump to a screen" />
{[...window.NAV_GROUPS, window.NAV_BOTTOM].flatMap((g) => g.tabs).map((t) => (
<window.TweakButton
key={t.id}
label={t.label}
onClick={() => navigate(t.id)}
/>
))}
</window.TweaksPanel>
)}
</div>
);
}

const SCREEN_TITLES = {
launcher: "Chronicles",
table: "The Session",
combat: "Battle",
character: "Heroes",
create: "Creation Plane",
forge: "Item Forge",
relations: "Relations",
acts: "Acts",
seed: "World Seed",
inventory: "Stash",
map: "World Atlas",
journal: "Quest Journal",
bestiary: "Codex",
merchant: "The Market",
dialogue: "Parley",
settings: "Setting",
};

function ScreenRouter({ screen, state, setState, onNavigate, campMode, setCampMode }) {
switch (screen) {
case "launcher": return <ScreenLauncher state={state} setState={setState} onNavigate={onNavigate} />;
case "table": return <ScreenTable state={state} setState={setState} onNavigate={onNavigate} />;
case "combat": return <ScreenCombat state={state} setState={setState} onNavigate={onNavigate} />;
case "character": return <ScreenCharacter state={state} setState={setState} onNavigate={onNavigate} />;
case "create": return <ScreenCreate state={state} setState={setState} onNavigate={onNavigate} />;
case "forge": return <ScreenForge state={state} setState={setState} onNavigate={onNavigate} />;
case "relations": return <ScreenRelations state={state} setState={setState} onNavigate={onNavigate} />;
case "acts": return <ScreenActs state={state} setState={setState} onNavigate={onNavigate} />;
case "seed": return <ScreenSeed state={state} setState={setState} onNavigate={onNavigate} />;
case "inventory": return <ScreenInventory state={state} setState={setState} onNavigate={onNavigate} />;
case "map": return <ScreenMap state={state} setState={setState} onNavigate={onNavigate} campMode={campMode} setCampMode={setCampMode} />;
case "journal": return <ScreenJournal state={state} setState={setState} onNavigate={onNavigate} />;
case "bestiary": return <ScreenBestiary state={state} setState={setState} onNavigate={onNavigate} />;
case "merchant": return <ScreenMerchant state={state} setState={setState} onNavigate={onNavigate} />;
case "dialogue": return <ScreenDialogue state={state} setState={setState} onNavigate={onNavigate} />;
case "settings": return <ScreenSettings state={state} setState={setState} onNavigate={onNavigate} />;
default: return <ScreenLauncher state={state} setState={setState} onNavigate={onNavigate} />;
}
}

ReactDOM.createRoot(document.getElementById("root")).render(
<ToastProvider>
<App />
</ToastProvider>
);
Loading
Loading