Destinations were one flat list, so building a second set meant overwriting the
first. They now live in named groups, listed down the left: click a group and
its destinations appear.
This is not a new concept bolted on. The .td5 header carries a company count
with room for twelve, each with its own name and its own pointer table, the
.tp5 has "Company Sum:", and TP5's manual has you create a company before you
can add any destinations. A group is that company.
- a saved project (.tp5) holds every group, which the old software can still
open, and reloading one restores them all
- a .td5 is written from the selected group, since one file is what goes on
one SD card. That keeps every exported file the exact shape already proven
against the bus, rather than a multi-company layout with no reference file
to check against
- deleting a group takes its destinations with it, so it is undoable too
state.destinations and state.company are now accessors onto the active group,
so everything built on the flat list keeps working untouched.
Also fixes the custom sign size inputs showing when a preset was selected:
.field sets display:flex, which beats the [hidden] attribute's display:none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
255 lines
12 KiB
HTML
255 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>DestoGod — Bus Destination Sign Editor</title>
|
||
<style>
|
||
:root{
|
||
--bg:#14161a; --panel:#1c1f26; --panel2:#232732; --line:#31374a;
|
||
--ink:#e8ecf5; --dim:#98a2b8; --amber:#ffb000; --amber-dim:#3a2a05;
|
||
--accent:#4a9eff; --good:#3ddc84; --warn:#ffb74d; --bad:#ff6b6b;
|
||
--r:10px;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%}
|
||
body{
|
||
margin:0; background:var(--bg); color:var(--ink);
|
||
font:14px/1.45 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||
display:flex; flex-direction:column; overflow:hidden;
|
||
}
|
||
button,input,select,textarea{font:inherit;color:inherit}
|
||
button{
|
||
background:var(--panel2); border:1px solid var(--line); color:var(--ink);
|
||
padding:8px 14px; border-radius:8px; cursor:pointer; white-space:nowrap;
|
||
}
|
||
button:hover:not(:disabled){background:#2c3242; border-color:#465073}
|
||
button:disabled{opacity:.4; cursor:default}
|
||
button.primary{background:var(--accent); border-color:var(--accent); color:#08131f; font-weight:650}
|
||
button.primary:hover:not(:disabled){background:#68b0ff}
|
||
button.ghost{background:transparent}
|
||
button.danger:hover:not(:disabled){background:#3a2226; border-color:#7d3b43; color:#ffb3b3}
|
||
input[type=text],input[type=number],select,textarea{
|
||
background:#12141a; border:1px solid var(--line); border-radius:8px; padding:8px 10px; width:100%;
|
||
}
|
||
input:focus,select:focus,textarea:focus{outline:2px solid var(--accent); outline-offset:-1px; border-color:transparent}
|
||
label{display:block; font-size:12px; color:var(--dim); margin-bottom:5px; font-weight:600; letter-spacing:.02em}
|
||
|
||
/* ---------- top bar ---------- */
|
||
header{
|
||
display:flex; align-items:center; gap:14px; padding:12px 18px;
|
||
background:var(--panel); border-bottom:1px solid var(--line); flex:0 0 auto; flex-wrap:wrap;
|
||
}
|
||
.brand{display:flex; align-items:baseline; gap:9px; margin-right:4px}
|
||
.brand b{font-size:19px; letter-spacing:-.02em}
|
||
.brand span{font-size:11px; color:var(--dim)}
|
||
.field{display:flex; align-items:center; gap:7px}
|
||
.field label{margin:0}
|
||
.field input,.field select{width:auto; min-width:120px}
|
||
/* .field sets display:flex, which beats the [hidden] default of display:none */
|
||
#custom-size[hidden]{display:none}
|
||
.spacer{flex:1}
|
||
|
||
/* ---------- layout ---------- */
|
||
main{flex:1; display:grid; grid-template-columns:290px 1fr; min-height:0}
|
||
#groups{overflow-y:auto; padding:6px; max-height:34vh; flex:0 0 auto; border-bottom:1px solid var(--line)}
|
||
.grp{display:flex; align-items:center; gap:8px; padding:7px 9px; border-radius:8px; cursor:pointer; border:1px solid transparent}
|
||
.grp:hover{background:var(--panel2)}
|
||
.grp.sel{background:#20304a; border-color:#37517d}
|
||
.grp .gnm{flex:1; min-width:0; font-size:13px; font-weight:650; white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
|
||
.grp .gct{font-size:10px; color:var(--dim); font-variant-numeric:tabular-nums}
|
||
.grp .del{flex:0 0 auto; width:22px; height:22px; padding:0; line-height:1; font-size:15px; border-radius:6px;
|
||
background:transparent; border:1px solid transparent; color:var(--dim); opacity:0; transition:opacity .12s ease}
|
||
.grp:hover .del,.grp.sel .del{opacity:1}
|
||
.grp .del:hover{background:#3a2226; border-color:#7d3b43; color:#ffb3b3}
|
||
#sidebar{
|
||
background:var(--panel); border-right:1px solid var(--line);
|
||
display:flex; flex-direction:column; min-height:0;
|
||
}
|
||
.side-head{
|
||
display:flex; align-items:center; justify-content:space-between; gap:8px;
|
||
padding:11px 12px; border-bottom:1px solid var(--line);
|
||
}
|
||
.side-head h2{margin:0; font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--dim)}
|
||
#list{overflow-y:auto; flex:1; padding:6px}
|
||
.item{
|
||
display:flex; align-items:center; gap:9px; padding:7px 9px; border-radius:8px;
|
||
cursor:pointer; border:1px solid transparent;
|
||
}
|
||
.item:hover{background:var(--panel2)}
|
||
.item.sel{background:#20304a; border-color:#37517d}
|
||
.item .num{font-size:10px; color:var(--dim); width:20px; text-align:right; flex:0 0 auto; font-variant-numeric:tabular-nums}
|
||
.item .body{min-width:0; flex:1}
|
||
.item .nm{font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600}
|
||
.item canvas{display:block; image-rendering:pixelated; margin-top:3px; opacity:.85}
|
||
.item .tag{font-size:10px; color:var(--dim)}
|
||
.item .tag.warn{color:var(--warn)}
|
||
.item .del{
|
||
flex:0 0 auto; width:24px; height:24px; padding:0; line-height:1; font-size:16px;
|
||
border-radius:6px; background:transparent; border:1px solid transparent; color:var(--dim);
|
||
opacity:0; transition:opacity .12s ease;
|
||
}
|
||
.item:hover .del,.item.sel .del{opacity:1}
|
||
.item .del:hover{background:#3a2226; border-color:#7d3b43; color:#ffb3b3}
|
||
.empty{padding:26px 16px; text-align:center; color:var(--dim); font-size:13px}
|
||
#toast button{
|
||
margin-left:12px; padding:3px 11px; font-size:12px; font-weight:650;
|
||
background:var(--accent); border-color:var(--accent); color:#08131f;
|
||
}
|
||
|
||
/* ---------- editor ---------- */
|
||
#editor{overflow-y:auto; padding:20px 24px; min-height:0}
|
||
.wrap{max-width:900px; margin:0 auto}
|
||
.card{background:var(--panel); border:1px solid var(--line); border-radius:var(--r); padding:18px; margin-bottom:16px}
|
||
.card > h3{margin:0 0 14px; font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--dim)}
|
||
.row{display:flex; gap:12px; flex-wrap:wrap}
|
||
.row > *{flex:1; min-width:150px}
|
||
|
||
/* ---------- LED preview ---------- */
|
||
.led-stage{
|
||
background:#000; border:1px solid #2a2f3d; border-radius:8px; padding:14px;
|
||
display:flex; justify-content:center; overflow:hidden;
|
||
}
|
||
.led-stage canvas{display:block; image-rendering:pixelated}
|
||
.meta{display:flex; gap:14px; flex-wrap:wrap; margin-top:11px; font-size:12px; color:var(--dim)}
|
||
.meta b{color:var(--ink); font-weight:600}
|
||
.pill{
|
||
display:inline-flex; align-items:center; gap:5px; padding:2px 9px; border-radius:20px;
|
||
font-size:11px; font-weight:650; border:1px solid;
|
||
}
|
||
.pill.ok{color:var(--good); border-color:#235b3c; background:#12251b}
|
||
.pill.scroll{color:var(--warn); border-color:#6b4d1c; background:#241c0c}
|
||
.pill.bad{color:var(--bad); border-color:#6b2b2b; background:#241111}
|
||
|
||
/* ---------- pages ---------- */
|
||
.page{border:1px solid var(--line); border-radius:9px; padding:14px; margin-bottom:11px; background:var(--panel2)}
|
||
.page-head{display:flex; align-items:center; gap:10px; margin-bottom:11px}
|
||
.page-head .lbl{font-size:11px; text-transform:uppercase; letter-spacing:.07em; color:var(--dim); font-weight:700}
|
||
.seg{display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden}
|
||
.seg button{border:0; border-radius:0; padding:7px 13px; background:transparent; font-size:13px}
|
||
.seg button.on{background:var(--accent); color:#08131f; font-weight:650}
|
||
.hint{font-size:12px; color:var(--dim); margin-top:7px}
|
||
.hint.warn{color:var(--warn)}
|
||
|
||
/* ---------- modal / drop ---------- */
|
||
dialog{
|
||
border:1px solid var(--line); background:var(--panel); color:var(--ink);
|
||
border-radius:var(--r); padding:0; max-width:560px; width:92vw;
|
||
}
|
||
dialog::backdrop{background:#000a; backdrop-filter:blur(2px)}
|
||
dialog .dlg-body{padding:20px}
|
||
dialog h3{margin:0 0 6px; font-size:16px}
|
||
dialog p{margin:0 0 14px; color:var(--dim); font-size:13px}
|
||
dialog .dlg-foot{display:flex; justify-content:flex-end; gap:9px; padding:14px 20px; border-top:1px solid var(--line)}
|
||
#map-preview{margin-top:16px; border:1px solid var(--line); border-radius:8px; overflow:auto; max-height:230px}
|
||
#map-preview table{border-collapse:collapse; width:100%; font-size:12px}
|
||
#map-preview th{
|
||
position:sticky; top:0; background:var(--panel2); text-align:left; padding:7px 10px;
|
||
font-size:10px; text-transform:uppercase; letter-spacing:.06em; color:var(--dim); white-space:nowrap;
|
||
}
|
||
#map-preview td{padding:6px 10px; border-top:1px solid var(--line); white-space:nowrap}
|
||
#map-preview .led{color:var(--amber); font-weight:600}
|
||
#map-preview .nm{font-weight:650}
|
||
#map-count{margin-top:9px; font-size:12px; color:var(--dim)}
|
||
textarea{min-height:170px; resize:vertical; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:13px}
|
||
#drop{
|
||
position:fixed; inset:0; background:#0b0e14e6; display:none; place-items:center;
|
||
z-index:99; font-size:20px; color:var(--accent); border:3px dashed var(--accent); border-radius:14px;
|
||
}
|
||
#drop.on{display:grid}
|
||
#toast{
|
||
position:fixed; bottom:20px; left:50%; transform:translateX(-50%) translateY(80px);
|
||
background:var(--panel2); border:1px solid var(--line); padding:11px 18px; border-radius:9px;
|
||
transition:transform .22s ease, opacity .22s ease; opacity:0; z-index:100; font-size:13px; max-width:80vw;
|
||
}
|
||
#toast.on{transform:translateX(-50%) translateY(0); opacity:1}
|
||
#toast.err{border-color:#7d3b43; color:#ffc9c9}
|
||
.welcome{max-width:560px; margin:8vh auto; text-align:center}
|
||
.welcome h1{font-size:26px; margin:0 0 10px}
|
||
.welcome p{color:var(--dim); margin:0 0 22px}
|
||
.welcome .btns{display:flex; gap:10px; justify-content:center; flex-wrap:wrap}
|
||
kbd{background:#12141a; border:1px solid var(--line); border-bottom-width:2px; border-radius:5px; padding:1px 6px; font-size:11px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="brand"><b>DestoGod</b><span>bus destination signs</span></div>
|
||
<div class="field"><label for="company">Group name</label><input type="text" id="company" maxlength="16" placeholder="Group name"></div>
|
||
<div class="field"><label for="model">Bus / sign</label><select id="model"></select></div>
|
||
<div class="field" id="custom-size" hidden>
|
||
<input type="number" id="cust-w" min="8" max="1024" step="8" title="Sign width in pixels" style="width:78px">
|
||
<span style="color:var(--dim)">×</span>
|
||
<input type="number" id="cust-h" min="8" max="64" step="8" title="Sign height in pixels" style="width:66px">
|
||
<span style="color:var(--dim);font-size:12px">pixels</span>
|
||
</div>
|
||
<div class="spacer"></div>
|
||
<button id="btn-open">Open file…</button>
|
||
<button id="btn-sheet" title="Import destinations from Excel or CSV">Import spreadsheet…</button>
|
||
<button id="btn-export-tp5">Save project (.tp5)</button>
|
||
<button class="primary" id="btn-export">Export for bus (.td5)</button>
|
||
<input type="file" id="file" accept=".td5,.tp5,.xlsx,.xlsm,.csv" hidden>
|
||
</header>
|
||
|
||
<main>
|
||
<aside id="sidebar">
|
||
<div class="side-head">
|
||
<h2>Groups (<span id="gcount">1</span>)</h2>
|
||
<button class="ghost" id="btn-group-add" title="New group">+</button>
|
||
</div>
|
||
<div id="groups"></div>
|
||
<div class="side-head">
|
||
<h2>Destinations (<span id="count">0</span>)</h2>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="ghost" id="btn-bulk" title="Add many at once">Paste list</button>
|
||
<button class="ghost" id="btn-add" title="Add one">+</button>
|
||
<button class="ghost danger" id="btn-clear" title="Delete every destination (you can undo)">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div id="list"></div>
|
||
</aside>
|
||
<section id="editor"></section>
|
||
</main>
|
||
|
||
<div id="drop">Drop a .td5, .tp5, spreadsheet or CSV to open</div>
|
||
<div id="toast"></div>
|
||
|
||
<dialog id="mapdlg">
|
||
<div class="dlg-body">
|
||
<h3>Import from spreadsheet</h3>
|
||
<p>Check the two columns below are the right ones, then import. Everything can still be edited afterwards.</p>
|
||
<div class="row">
|
||
<div>
|
||
<label for="map-name">Name on the driver's controller</label>
|
||
<select id="map-name"></select>
|
||
</div>
|
||
<div>
|
||
<label for="map-text">Text shown on the sign</label>
|
||
<select id="map-text"></select>
|
||
</div>
|
||
</div>
|
||
<div id="map-preview"></div>
|
||
<div id="map-count"></div>
|
||
</div>
|
||
<div class="dlg-foot">
|
||
<button id="map-cancel" class="ghost">Cancel</button>
|
||
<button id="map-ok" class="primary">Import</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="bulk">
|
||
<div class="dlg-body">
|
||
<h3>Paste a list of destinations</h3>
|
||
<p>One per line. This is the fast way to build a whole list — far quicker than adding them one at a time.</p>
|
||
<textarea id="bulk-text" placeholder="SCHOOL BUS CHARTER RAIL BUS CARMEL COLLEGE"></textarea>
|
||
</div>
|
||
<div class="dlg-foot">
|
||
<button id="bulk-cancel" class="ghost">Cancel</button>
|
||
<button id="bulk-ok" class="primary">Add them</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<script type="module" src="./main.mjs"></script>
|
||
</body>
|
||
</html>
|