HardYards/web/world/editor.html

228 lines
9.6 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>HARD YARDS — yard editor</title>
<!--
The yard editor. Lane A owns this file (SPRINT14 gate 1).
It lives in web/ and therefore DEPLOYS with the game, and that is intended:
site JSON carries no secrets, and a public "build a yard" toy is a feature.
Nothing here is loaded by index.html — the game never imports editor.js.
── THE DOM SEAM CONTRACT (Lane B's SCORE IT, Lane C's wind authoring) ────────
B and C build INTO this page. Everything they need is below and in editor.js;
the same way E's invoice kit got .letterhead/.brief/.jobsheet in SPRINT12,
this page publishes classes and mount points so three lanes can edit one UI
without editing one file.
MOUNT: never append to #ed-side by hand. Call
const { root, body } = EDITOR.mountPanel({ id: 'score', title: 'SCORE IT', order: 40 });
and fill `body`. `order` fixes the rail's vertical order across lanes so two
lanes landing in the same sprint can't fight over it. Reserved orders:
10 site 20 palette 30 inspector 40 SCORE IT (B) 50 wind (C)
80 validation 90 export
Calling mountPanel twice with one id returns the SAME panel (idempotent), so a
re-render is a re-fill, not a duplicate.
CLASSES (the contract — style with these, don't invent):
.ed-panel .ed-panel-title .ed-panel-body panel chrome (mountPanel makes these)
.ed-row one label+control line
.ed-label the label half of a row
.ed-btn .ed-btn.primary .ed-btn.danger buttons
.ed-num .ed-sel .ed-text number / select / text+textarea
.ed-card .ed-card-head .ed-card-row .ed-kv a RESULT card (B's score goes here)
.ed-ok .ed-warn .ed-err verdict colouring, on any element
.ed-note muted small print
.ed-tag inline pill (a flag, a count)
Anything you need that isn't here: add it, and say so in THREADS — a class
that only one lane knows about is the thing this contract exists to prevent.
The scene seams (three.js side) are on the EDITOR object — see editor.js's
header for the full API. The short version C needs:
EDITOR.overlay a THREE.Group, cleared on every rebuild — gizmos live here
EDITOR.raycastGround(ev) → {x,y,z}|null
EDITOR.registerTool({...}) / EDITOR.setTool(id)
EDITOR.site / EDITOR.markDirty() read+mutate the live site, then say so
EDITOR.on('change'|'rebuild'|'select', fn)
-->
<style>
:root {
color-scheme: dark;
--bg: #10161b;
--panel: #172129;
--panel2: #1d2831;
--line: #2b3a45;
--ink: #dde5ea;
--dim: #8598a5;
--accent: #7ee0ff;
--ok: #7fce6a;
--warn: #ffc46b;
--err: #ff6b6b;
}
* { box-sizing: border-box; }
html, body {
margin: 0; height: 100%; overflow: hidden;
background: var(--bg); color: var(--ink);
font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}
#ed-root { display: flex; height: 100%; }
/* --- viewport ---------------------------------------------------------- */
#ed-viewport { position: relative; flex: 1 1 auto; min-width: 0; }
#ed-canvas { display: block; width: 100%; height: 100%; }
#ed-hint {
position: absolute; left: 10px; bottom: 10px; margin: 0;
color: #fff; text-shadow: 0 1px 3px #000; white-space: pre; pointer-events: none;
opacity: .85;
}
#ed-readout {
position: absolute; right: 10px; top: 10px; margin: 0; text-align: right;
color: #fff; text-shadow: 0 1px 3px #000; white-space: pre; pointer-events: none;
}
#ed-toolbar {
position: absolute; left: 10px; top: 10px; display: flex; gap: 6px; flex-wrap: wrap;
max-width: calc(100% - 220px);
}
/* --- side rail --------------------------------------------------------- */
#ed-side {
flex: 0 0 340px; height: 100%; overflow-y: auto; overflow-x: hidden;
background: var(--panel); border-left: 1px solid var(--line);
}
#ed-side::-webkit-scrollbar { width: 9px; }
#ed-side::-webkit-scrollbar-thumb { background: #2f3f4b; border-radius: 5px; }
/* --- the class contract ------------------------------------------------ */
.ed-panel { border-bottom: 1px solid var(--line); }
.ed-panel-title {
margin: 0; padding: 9px 12px; font-size: 11px; font-weight: 700;
letter-spacing: .14em; color: var(--accent); background: var(--panel2);
display: flex; align-items: center; justify-content: space-between; gap: 8px;
cursor: pointer; user-select: none;
}
.ed-panel-title::after { content: '▾'; color: var(--dim); font-size: 10px; }
.ed-panel.collapsed .ed-panel-title::after { content: '▸'; }
.ed-panel.collapsed .ed-panel-body { display: none; }
.ed-panel-body { padding: 10px 12px 12px; }
.ed-row { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; }
.ed-row:last-child { margin-bottom: 0; }
.ed-label { flex: 0 0 84px; color: var(--dim); }
.ed-row > .ed-num, .ed-row > .ed-sel, .ed-row > .ed-text { flex: 1 1 auto; min-width: 0; }
.ed-btn {
font: inherit; padding: 4px 9px; border-radius: 4px; cursor: pointer;
background: #24313b; color: var(--ink); border: 1px solid var(--line);
}
.ed-btn:hover { background: #2d3d49; }
.ed-btn:active { transform: translateY(1px); }
.ed-btn.on { background: #1d4a5c; border-color: #3d7f96; color: #cdf3ff; }
.ed-btn.primary { background: #1f5a34; border-color: #2f7d49; color: #d6ffdf; }
.ed-btn.primary:hover { background: #266c3e; }
.ed-btn.danger { background: #58211f; border-color: #7d2b2b; color: #ffd9d6; }
.ed-btn[disabled] { opacity: .45; cursor: not-allowed; }
.ed-num, .ed-sel, .ed-text {
font: inherit; padding: 3px 6px; border-radius: 4px;
background: #0e1418; color: var(--ink); border: 1px solid var(--line);
}
.ed-num { width: 74px; }
textarea.ed-text { width: 100%; resize: vertical; min-height: 62px; line-height: 1.5; }
.ed-card {
border: 1px solid var(--line); border-radius: 5px; background: var(--panel2);
padding: 8px 10px; margin: 0 0 8px;
}
.ed-card-head {
font-weight: 700; letter-spacing: .06em; margin: 0 0 6px;
display: flex; justify-content: space-between; gap: 8px;
}
.ed-card-row { display: flex; justify-content: space-between; gap: 10px; padding: 1px 0; }
.ed-kv { color: var(--dim); }
.ed-ok { color: var(--ok); }
.ed-warn { color: var(--warn); }
.ed-err { color: var(--err); }
.ed-note { color: var(--dim); font-size: 11px; line-height: 1.5; }
.ed-tag {
display: inline-block; padding: 0 5px; border-radius: 3px; font-size: 10px;
background: #24313b; border: 1px solid var(--line); color: var(--dim);
}
.ed-tag.ed-err { background: #3a1618; border-color: #7d2b2b; }
.ed-tag.ed-ok { background: #12321c; border-color: #2c6b3c; }
ul.ed-list { margin: 0; padding: 0; list-style: none; }
ul.ed-list li {
padding: 3px 6px; border-radius: 3px; cursor: pointer;
display: flex; justify-content: space-between; gap: 8px;
}
ul.ed-list li:hover { background: #22303a; }
ul.ed-list li.sel { background: #1d4a5c; color: #cdf3ff; }
#ed-boot-error {
position: absolute; inset: 0; display: none; padding: 28px;
background: #10161b; color: var(--err); white-space: pre-wrap; overflow: auto;
}
</style>
<!--
Required. world.js's dress() pulls GLTFLoader, which imports the BARE
specifier 'three'. Without this map the throw is swallowed by dress()'s
per-asset try/catch and every rating_hint silently becomes 1.0 — the
invincible-house gotcha in docs/MANUAL.md. It has cost this repo a day; do
not remove it from any page that dresses a yard.
-->
<script type="importmap">
{ "imports": { "three": "./vendor/three.module.js", "three/addons/": "./vendor/addons/" } }
</script>
</head>
<body>
<div id="ed-root">
<div id="ed-viewport">
<canvas id="ed-canvas"></canvas>
<div id="ed-toolbar"></div>
<pre id="ed-readout"></pre>
<pre id="ed-hint"></pre>
<div id="ed-boot-error"></div>
</div>
<div id="ed-side"></div>
</div>
<script type="module">
import { createEditor } from './js/editor.js';
try {
const EDITOR = await createEditor({
canvas: document.getElementById('ed-canvas'),
side: document.getElementById('ed-side'),
toolbar: document.getElementById('ed-toolbar'),
readout: document.getElementById('ed-readout'),
hint: document.getElementById('ed-hint'),
});
// The two lane panels, mounted AFTER createEditor resolves because both need
// a live EDITOR — B's reads `globalThis.EDITOR` on import (self-registering,
// nothing to call), C's exports a mount function. Landed by the integrator at
// SPRINT14 integration: both lanes built against A's published seams and
// asked for this one line each; A had finished by then, so it's mine. Inside
// the try on purpose — a panel that throws is an editor that says so on the
// glass rather than a rail with a hole in it.
await import('./js/editor_score.js');
const { mountWindPanel } = await import('./js/editor.wind.js');
await mountWindPanel(EDITOR);
} catch (err) {
// The editor is a tool, so it fails LOUD and on the glass. index.html's boot
// failure writes into a hidden #dev div and shows a stranger a blank page
// (SPRINT14 pool item) — this page does not repeat that.
const box = document.getElementById('ed-boot-error');
box.style.display = 'block';
box.textContent = `EDITOR BOOT FAILED\n\n${err && err.stack ? err.stack : err}`;
throw err;
}
</script>
</body>
</html>