178 lines
8.9 KiB
HTML
178 lines
8.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Newbury Exhibit — scene workshop</title>
|
|
<style>
|
|
:root {
|
|
--red:#f65151; --yellow:#fff35d; --blue:#529eff; --cyan:#51eaf1;
|
|
--bg:#070910; --panel:#0e1220; --panel2:#141a2c; --line:#1f2740;
|
|
--ink:#e8eaf2; --dim:#7f8aa3; --dimmer:#566077;
|
|
--anchor:#00e5c0; --blocker:#c98a4b; --sel:#ffd24a;
|
|
--mono:'SF Mono','Roboto Mono',ui-monospace,Menlo,Consolas,monospace;
|
|
--sans:'Inter','Segoe UI',system-ui,sans-serif;
|
|
}
|
|
* { box-sizing:border-box; margin:0; padding:0; }
|
|
html,body { height:100%; background:var(--bg); color:var(--ink); font-family:var(--sans);
|
|
overflow:hidden; -webkit-font-smoothing:antialiased; }
|
|
button,input,select { font-family:inherit; color:inherit; }
|
|
|
|
#app { display:grid; grid-template-columns:1fr 332px; grid-template-rows:42px 1fr;
|
|
grid-template-areas:"top top" "view side"; height:100%; }
|
|
|
|
/* top bar */
|
|
#top { grid-area:top; display:flex; align-items:center; gap:14px; padding:0 14px;
|
|
background:var(--panel); border-bottom:1px solid var(--line); }
|
|
#top .brand { font-weight:700; font-size:13px; letter-spacing:-0.01em; }
|
|
#top .brand span { color:var(--cyan); }
|
|
#top .meta { font-family:var(--mono); font-size:11px; color:var(--dim); }
|
|
#top .spacer { flex:1; }
|
|
#top button { background:var(--panel2); border:1px solid var(--line); border-radius:7px;
|
|
padding:6px 12px; font-size:12px; font-weight:600; cursor:pointer; }
|
|
#top button:hover { border-color:var(--dimmer); }
|
|
#top button.primary { background:linear-gradient(135deg,var(--blue),var(--cyan)); color:#06121f; border:0; }
|
|
#top button.primary:disabled { opacity:0.5; cursor:default; }
|
|
#dirty { width:7px; height:7px; border-radius:50%; background:var(--sel); display:none; }
|
|
#dirty.show { display:inline-block; }
|
|
|
|
/* viewport */
|
|
#view { grid-area:view; position:relative; }
|
|
#c { display:block; width:100%; height:100%; }
|
|
#viewhud { position:absolute; left:12px; bottom:12px; font-family:var(--mono);
|
|
font-size:11px; color:var(--dim); background:rgba(7,9,16,0.7); border:1px solid var(--line);
|
|
border-radius:7px; padding:7px 10px; line-height:1.6; pointer-events:none; }
|
|
#viewhud b { color:var(--ink); }
|
|
#toolbar { position:absolute; left:12px; top:12px; display:flex; gap:6px; }
|
|
#toolbar button { background:rgba(14,18,32,0.9); border:1px solid var(--line); border-radius:7px;
|
|
width:38px; height:38px; cursor:pointer; font-size:15px; display:grid; place-items:center; }
|
|
#toolbar button.active { border-color:var(--cyan); color:var(--cyan); background:rgba(81,234,241,0.1); }
|
|
#toolbar .sep { width:1px; background:var(--line); margin:4px 2px; }
|
|
|
|
/* side panel */
|
|
#side { grid-area:side; background:var(--panel); border-left:1px solid var(--line);
|
|
display:flex; flex-direction:column; overflow:hidden; }
|
|
.tabs { display:flex; border-bottom:1px solid var(--line); }
|
|
.tab { flex:1; padding:9px 4px; text-align:center; font-size:11px; font-weight:600;
|
|
color:var(--dim); cursor:pointer; border-bottom:2px solid transparent; text-transform:uppercase;
|
|
letter-spacing:0.04em; }
|
|
.tab.active { color:var(--ink); border-bottom-color:var(--cyan); }
|
|
.tab .n { font-family:var(--mono); color:var(--dimmer); margin-left:3px; }
|
|
|
|
.panel { flex:1; overflow-y:auto; padding:12px; display:none; }
|
|
.panel.active { display:block; }
|
|
.panel::-webkit-scrollbar { width:9px; }
|
|
.panel::-webkit-scrollbar-thumb { background:var(--line); border-radius:5px; }
|
|
|
|
.row { display:flex; gap:7px; align-items:center; margin-bottom:8px; }
|
|
.row label { font-size:11px; color:var(--dim); width:74px; flex-shrink:0; }
|
|
.row input, .row select { flex:1; min-width:0; background:var(--panel2); border:1px solid var(--line);
|
|
border-radius:6px; padding:6px 8px; font-size:12px; font-family:var(--mono); }
|
|
.row input:focus, .row select:focus { outline:none; border-color:var(--cyan); }
|
|
.xyz { display:flex; gap:5px; }
|
|
.xyz input { width:100%; text-align:right; }
|
|
.xyz .axis { position:relative; flex:1; }
|
|
.xyz .axis::before { content:attr(data-a); position:absolute; left:6px; top:50%;
|
|
transform:translateY(-50%); font-size:9px; color:var(--dimmer); font-family:var(--mono);
|
|
pointer-events:none; }
|
|
.xyz .axis input { padding-left:16px; }
|
|
|
|
.list-item { background:var(--panel2); border:1px solid var(--line); border-radius:8px;
|
|
padding:9px 10px; margin-bottom:7px; cursor:pointer; }
|
|
.list-item:hover { border-color:var(--dimmer); }
|
|
.list-item.sel { border-color:var(--sel); box-shadow:0 0 0 1px var(--sel) inset; }
|
|
.list-item .li-top { display:flex; align-items:center; gap:7px; }
|
|
.li-dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; }
|
|
.li-name { font-size:12px; font-weight:600; flex:1; overflow:hidden; text-overflow:ellipsis;
|
|
white-space:nowrap; }
|
|
.li-tag { font-family:var(--mono); font-size:10px; color:var(--dim); }
|
|
.li-sub { font-family:var(--mono); font-size:10px; color:var(--dimmer); margin-top:3px; }
|
|
|
|
.section-h { font-size:10px; text-transform:uppercase; letter-spacing:0.06em; color:var(--dimmer);
|
|
margin:14px 0 8px; font-weight:700; }
|
|
.section-h:first-child { margin-top:0; }
|
|
|
|
.btn-add { width:100%; background:var(--panel2); border:1px dashed var(--line); border-radius:8px;
|
|
padding:9px; font-size:12px; font-weight:600; color:var(--dim); cursor:pointer; }
|
|
.btn-add:hover { border-color:var(--cyan); color:var(--cyan); }
|
|
.btn-del { background:rgba(246,81,81,0.12); border:1px solid rgba(246,81,81,0.3); color:var(--red);
|
|
border-radius:6px; padding:6px 10px; font-size:11px; font-weight:600; cursor:pointer; }
|
|
.btn-sm { background:var(--panel2); border:1px solid var(--line); border-radius:6px; padding:5px 9px;
|
|
font-size:11px; cursor:pointer; }
|
|
.btn-sm:hover { border-color:var(--dimmer); }
|
|
|
|
.hint { font-size:11px; color:var(--dimmer); line-height:1.5; margin:6px 0; }
|
|
.empty { text-align:center; color:var(--dimmer); font-size:12px; padding:30px 10px; }
|
|
.pathpt { display:flex; gap:5px; align-items:center; margin-bottom:5px; }
|
|
.pathpt .idx { font-family:var(--mono); font-size:10px; color:var(--dimmer); width:16px; }
|
|
.swatches { display:flex; gap:6px; }
|
|
.sw { width:26px; height:26px; border-radius:6px; cursor:pointer; border:2px solid transparent; }
|
|
.sw.sel { border-color:#fff; }
|
|
|
|
#toast { position:fixed; bottom:18px; left:50%; transform:translateX(-50%) translateY(80px);
|
|
background:var(--panel2); border:1px solid var(--line); border-radius:9px; padding:10px 16px;
|
|
font-size:12.5px; transition:transform 0.3s; z-index:50; max-width:80vw; }
|
|
#toast.show { transform:translateX(-50%) translateY(0); }
|
|
#toast.ok { border-color:rgba(81,234,241,0.4); }
|
|
#toast.err { border-color:rgba(246,81,81,0.5); color:#ffb4b4; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<div id="top">
|
|
<div class="brand">Newbury <span>scene workshop</span></div>
|
|
<div class="meta" id="worldMeta">—</div>
|
|
<div class="spacer"></div>
|
|
<span id="dirty" title="unsaved changes"></span>
|
|
<button id="reloadBtn" title="Discard changes and reload from server">Reload</button>
|
|
<button id="exportBtn" title="Download scene.json">Export</button>
|
|
<button id="saveBtn" class="primary" disabled>Save to server</button>
|
|
</div>
|
|
|
|
<div id="view">
|
|
<canvas id="c"></canvas>
|
|
<div id="toolbar">
|
|
<button data-tool="select" class="active" title="Select / move (V)">⤢</button>
|
|
<div class="sep"></div>
|
|
<button data-tool="addSpawn" title="Add ghost spawn (G)">👻</button>
|
|
<button data-tool="addBlocker" title="Add building blocker (B)">▦</button>
|
|
<button data-tool="addAnchor" title="Add anchor (A)">◎</button>
|
|
</div>
|
|
<div id="viewhud">
|
|
cursor <b id="cursorPos">—</b><br />
|
|
<span id="selInfo">nothing selected</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="side">
|
|
<div class="tabs">
|
|
<div class="tab active" data-tab="world">World</div>
|
|
<div class="tab" data-tab="anchors">Anchors<span class="n" id="nAnchors">0</span></div>
|
|
<div class="tab" data-tab="spawns">Ghosts<span class="n" id="nSpawns">0</span></div>
|
|
<div class="tab" data-tab="blockers">Buildings<span class="n" id="nBlockers">0</span></div>
|
|
</div>
|
|
|
|
<div class="panel active" id="panel-world"></div>
|
|
<div class="panel" id="panel-anchors"></div>
|
|
<div class="panel" id="panel-spawns"></div>
|
|
<div class="panel" id="panel-blockers"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast"></div>
|
|
|
|
<!-- Import map so OrbitControls' bare `from 'three'` resolves.
|
|
Without this the module import fails and the editor never boots. -->
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module" src="js/admin.js?v=2"></script>
|
|
</body>
|
|
</html>
|