Files
newbury-exhibit/public/admin.html
T
2026-07-06 19:25:57 +10:00

167 lines
8.5 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>
<script type="module" src="js/admin.js?v=1"></script>
</body>
</html>