Files
onefinity-firmware/docs/mocks/v09_full_ux.html
Henrik Muehe ef4658aaf6 Plan: V09 full UX redesign mock + implementation plan
- docs/mocks/v09_full_ux.html — high-fidelity 1920x1080 mock
  showing the proposed Control / Program / Console / Settings tab
  layout with the V09 flat slate jog/macro palette and an underline
  ribbon header tab style.
- plans/2026-04-30_ux_redesign.md — phased implementation plan to
  port index.pug + control-view.pug to the new shell while keeping
  hash routing and existing settings/admin views intact.
2026-04-30 20:00:03 +02:00

901 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Onefinity · V09 · Full UX</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/css/all.min.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box}
html,body{margin:0;font-family:'Inter',system-ui,sans-serif;background:#0f172a;color:#e5e7eb}
.mono{font-family:'JetBrains Mono',monospace}
/* ---------- HOST CHROME ---------- */
.host{min-height:100vh;display:flex;flex-direction:column;background:radial-gradient(circle at 30% 0%,#374151,#0f172a 60%);}
.topbar{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;padding:.7rem 1rem;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.08);position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);}
.topbar .brand{display:flex;align-items:center;gap:.5rem;font-weight:800;color:#fff}
.stripe-logo-sm{background:repeating-linear-gradient(135deg,#a7c7a3 0 6px,transparent 6px 14px);width:26px;height:26px;border-radius:6px}
.pill{padding:.3rem .65rem;border-radius:9999px;font-size:.75rem;font-weight:700;background:rgba(255,255,255,.08);color:#cbd5e1}
.seg-host{display:inline-flex;background:rgba(255,255,255,.05);border-radius:9999px;padding:3px;gap:3px}
.seg-host button{padding:.4rem .85rem;border-radius:9999px;font-size:.78rem;font-weight:700;color:#cbd5e1}
.seg-host button.on{background:#fde047;color:#0f172a}
.toggle{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .7rem;border-radius:8px;background:rgba(255,255,255,.08);font-size:.75rem;font-weight:600;color:#e5e7eb;cursor:pointer}
.toggle.on{background:#22c55e;color:#0b1220}
.stage{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:1rem;overflow:auto}
.scaler-viewport{position:relative;flex:0 0 auto}
.scaler{position:absolute;top:0;left:0;width:1920px;height:auto;transform-origin:top left;transition:transform .2s}
/* ---------- KIOSK (1920x1080) ---------- */
.kiosk{
width:1920px;height:1080px;overflow:hidden;border-radius:14px;position:relative;
box-shadow:0 30px 60px rgba(0,0,0,.5);
display:flex;flex-direction:column;
background:#ffffff;color:#0f172a;
}
/* Header */
.head{
flex:0 0 96px;height:96px;
display:flex;align-items:center;gap:18px;
padding:0 24px;background:#ffffff;border-bottom:1px solid #e5e7eb;
}
.brand-blk{display:flex;align-items:center;gap:14px}
.menu-btn{width:54px;height:54px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;color:#0f172a;display:inline-flex;align-items:center;justify-content:center;font-size:1.1rem}
.menu-btn:hover{background:#e2e8f0}
.brand-logo{width:42px;height:42px;border-radius:8px;background:repeating-linear-gradient(135deg,#a7c7a3 0 6px,transparent 6px 14px)}
.brand-name{font-weight:900;font-size:22px;letter-spacing:-.01em}
/* Underline-ribbon tab style (V02) */
.kiosk-tabs{display:inline-flex;gap:0;margin-right:auto;padding-left:18px;align-items:stretch;height:96px}
.ktab{
position:relative;
height:96px;padding:0 26px;
background:transparent;border:none;border-radius:0;
color:#475569;font-size:1.05rem;font-weight:700;
display:inline-flex;align-items:center;gap:.55rem;cursor:pointer;
transition:color .15s;
}
.ktab i{font-size:1.1rem;color:#94a3b8;transition:color .15s}
.ktab:hover{color:#0f172a}
.ktab:hover i{color:#475569}
.ktab.active{color:#0f172a}
.ktab.active i{color:#0f172a}
.ktab.active::after{
content:"";position:absolute;left:14px;right:14px;bottom:0;
height:5px;background:#fde047;border-radius:5px 5px 0 0;
}
.ktab .ktab-badge{background:#fee2e2;color:#991b1b;font-size:.7rem;padding:3px 8px;border-radius:9999px;font-weight:800;line-height:1}
.ktab.active .ktab-badge{background:#fde047;color:#0f172a}
.sys-btn{display:inline-flex;align-items:center;gap:.55rem;height:54px;padding:0 1.1rem;border-radius:14px;background:#f1f5f9;border:1px solid #e2e8f0;color:#0f172a;font-size:.9rem;font-weight:600}
.sys-btn .pip{width:9px;height:9px;border-radius:9999px;background:#22c55e}
.state-badge{display:inline-flex;align-items:center;gap:.6rem;height:54px;padding:0 1.1rem;border-radius:14px;background:#dcfce7;color:#166534;font-weight:800;font-size:1rem;letter-spacing:.04em}
.state-badge .dot{width:10px;height:10px;border-radius:9999px;background:currentColor;position:relative}
.state-badge .dot::after{content:"";position:absolute;inset:-3px;border-radius:9999px;border:2px solid currentColor;opacity:.5;animation:pls 1.6s ease-out infinite}
@keyframes pls{0%{transform:scale(.7);opacity:.6}100%{transform:scale(2.2);opacity:0}}
.estop{
width:88px;height:88px;background:#dc2626;color:#fff;font-weight:900;
clip-path:polygon(30% 0,70% 0,100% 30%,100% 70%,70% 100%,30% 100%,0 70%,0 30%);
display:flex;align-items:center;justify-content:center;
border:3px solid #fff;box-shadow:0 0 0 3px #b91c1c, 0 8px 20px rgba(220,38,38,.35);font-size:1rem;letter-spacing:.05em
}
/* Body */
.body{flex:1;display:flex;flex-direction:column;background:#f1f5f9;min-height:0}
.panel{display:none;flex:1;min-height:0;flex-direction:column;padding:18px;gap:14px}
.panel.active{display:flex}
/* ----------------------- V09 jog/macro palette ----------------------- */
/* Flat soft slate, no shadow */
:root{
--jog-bg:#3f4b63;
--jog-hover:#4a5777;
--jog-dir-bg:#5b6885;
--jog-dir-hover:#6a779a;
--jog-ghost-bg:#8c97ad;
--jog-ghost-hover:#9ba6bb;
--jog-ink:#fff;
--jog-ghost-ink:#0f172a;
}
/* JOG */
.jog-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;display:flex;flex-direction:column;padding:18px;min-height:0}
.jog-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.jog-title{font-size:18px;font-weight:700;color:#0f172a}
.jog-title .step{color:#0ea5e9;font-family:'JetBrains Mono',monospace}
.step-seg{display:inline-flex;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:14px;padding:4px}
.step-seg button{height:48px;min-width:64px;padding:0 1rem;border-radius:11px;font-size:1rem;font-weight:800;color:#475569;cursor:pointer}
.step-seg button.active{background:#0f172a;color:#fde047}
.jog-grid{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(4,1fr);gap:10px;flex:1;min-height:0}
.jbtn{
border-radius:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
user-select:none;-webkit-tap-highlight-color:transparent;cursor:pointer;
font-weight:700;font-size:1.05rem;border:none;
transition:transform .06s, background .15s;
background:var(--jog-bg);color:var(--jog-ink);
}
.jbtn:hover{background:var(--jog-hover)}
.jbtn:active{transform:scale(.97)}
.jbtn .ico{font-size:1.6rem}
.jbtn .lbl{font-size:.8rem;color:inherit;opacity:.85;font-weight:600}
.jbtn.dir{background:var(--jog-dir-bg)} .jbtn.dir:hover{background:var(--jog-dir-hover)}
.jbtn.ghost{background:var(--jog-ghost-bg);color:var(--jog-ghost-ink)} .jbtn.ghost:hover{background:var(--jog-ghost-hover)}
/* DRO + STATUS */
.control-grid{display:grid;grid-template-columns:720px 1fr;gap:18px;flex:1;min-height:0}
.right-col{display:grid;grid-template-rows:1fr 158px;gap:18px;min-height:0}
.dro-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;overflow:hidden;display:flex;flex-direction:column}
.dro-head{display:grid;grid-template-columns:84px 1.4fr 1fr 1fr 170px 170px 280px;column-gap:.75rem;align-items:center;padding:14px 22px;background:#f8fafc;border-bottom:1px solid #e5e7eb;font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#94a3b8}
.dro-row{display:grid;grid-template-columns:84px 1.4fr 1fr 1fr 170px 170px 280px;column-gap:.75rem;align-items:center;padding:14px 22px;border-bottom:1px solid #f1f5f9;flex:1;min-height:0}
.dro-row:last-child{border-bottom:none}
.dro-axis{font-weight:900;font-size:46px;line-height:1}
.dro-pos{font-family:'JetBrains Mono',monospace;font-size:36px;font-weight:800}
.dro-pos .u{font-size:14px;color:#94a3b8;font-weight:500;margin-left:6px}
.dro-sec{font-family:'JetBrains Mono',monospace;font-size:18px;color:#64748b;font-weight:600}
.axis-x{color:#dc2626} .axis-y{color:#16a34a} .axis-z{color:#2563eb} .axis-w{color:#7c3aed}
.chip{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .7rem;border-radius:9999px;font-size:.78rem;font-weight:700}
.chip-green{background:#dcfce7;color:#166534}
.chip-amber{background:#fef3c7;color:#92400e}
.chip-red{background:#fee2e2;color:#991b1b}
.chip-slate{background:#e2e8f0;color:#334155}
.chip-blue{background:#dbeafe;color:#1e40af}
.icon-btn{
width:72px;height:72px;border-radius:14px;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;
color:#334155;background:#f1f5f9;border:1px solid #e2e8f0;
font-size:1.45rem
}
.icon-btn:hover{background:#e2e8f0}
.actions-cell{display:flex;justify-content:flex-end;gap:10px}
.z-highlight{background:rgba(254,243,199,.4)}
.status-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;min-height:0}
.stat-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;padding:18px 22px;display:flex;flex-direction:column;justify-content:center}
.stat-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.14em;color:#94a3b8}
.stat-val{font-family:'JetBrains Mono',monospace;font-size:30px;font-weight:800;margin-top:6px}
.stat-val.ok{color:#166534}
.stat-sub{font-size:13px;color:#64748b;margin-top:2px}
/* MACROS */
.macro-row{display:grid;grid-template-columns:repeat(8,1fr);gap:12px;flex:0 0 auto}
.macro-btn{
height:84px;border-radius:14px;border:none;cursor:pointer;
color:#fff;background:#3f4b63;
font-weight:800;font-size:1rem;
display:flex;align-items:center;justify-content:center;gap:.6rem;
transition:transform .06s, background .15s
}
.macro-btn:hover{background:#4a5777}
.macro-btn:active{transform:translateY(2px)}
.macro-btn .mnum{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px;background:#fde047;color:#0f172a;font-size:.85rem;font-weight:900}
.macro-btn .micon{font-size:1.1rem;opacity:.75}
/* =============================================================
PROGRAM PANEL
============================================================= */
.program-card{background:#fff;border:1px solid #e5e7eb;border-radius:18px;display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
.ptab-bar{display:flex;align-items:center;gap:6px;border-bottom:1px solid #e5e7eb;flex:0 0 auto;background:#fff;padding:0 18px}
.ptab{height:60px;padding:0 22px;font-weight:700;color:#64748b;border-bottom:3px solid transparent;font-size:1rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
.ptab:hover{color:#0f172a}
.ptab.active{color:#0f172a;border-bottom-color:#0f172a}
.ptab .ptab-badge{background:#fde047;color:#0f172a;font-size:.7rem;padding:2px 7px;border-radius:9999px;font-weight:900}
.action-bar{display:flex;align-items:center;gap:12px;padding:18px;flex-wrap:wrap;border-bottom:1px solid #f1f5f9}
.action-btn{height:84px;padding:0 24px;border-radius:14px;background:#3f4b63;color:#fff;border:none;cursor:pointer;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;font-weight:800;font-size:.9rem;letter-spacing:.04em;transition:background .15s}
.action-btn:hover{background:#4a5777}
.action-btn .ico{font-size:1.4rem}
.action-btn.run{background:#16a34a}
.action-btn.run:hover{background:#15803d}
.action-btn.stop{background:#0f172a}
.action-btn.stop:hover{background:#1e293b}
.action-btn.danger{background:#fee2e2;color:#7f1d1d}
.action-btn.danger:hover{background:#fecaca}
.action-btn.danger .ico{color:#dc2626}
.file-bar{display:flex;align-items:center;gap:10px;padding:14px 18px;flex-wrap:wrap;border-bottom:1px solid #f1f5f9}
.file-btn{height:54px;padding:0 18px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;font-weight:700;color:#0f172a;font-size:.9rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
.file-btn:hover{background:#e2e8f0}
.file-select{height:54px;padding:0 16px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-weight:600;color:#0f172a;font-size:.9rem;display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}
.file-select .caret{color:#94a3b8;margin-left:.5rem}
.file-select.primary{background:#fff;border:2px solid #0ea5e9;flex:1;min-width:300px}
.program-body{flex:1;display:grid;grid-template-columns:1fr 600px;min-height:0}
.gcode{font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.6;background:#fafafa;border-right:1px solid #f1f5f9;padding:14px 0;overflow:auto;color:#1e293b}
.gline{display:grid;grid-template-columns:60px 1fr;gap:14px;padding:1px 18px 1px 0}
.gline:nth-child(odd){background:#f4f4f5}
.gline .gn{color:#f59e0b;text-align:right;font-weight:700}
.gline.cur{background:#dbeafe !important}
.gline.cur .gn{color:#1e40af}
.gcomment{color:#64748b}
.gword{color:#0f172a}
.gnum{color:#16a34a}
.viewer{display:flex;flex-direction:column;min-height:0}
.viewer-3d{flex:1;background:#0b1220;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center}
.viewer-tools{display:flex;gap:8px;padding:14px;border-top:1px solid #f1f5f9;background:#fff;flex-wrap:wrap}
.vtool{height:60px;width:60px;border-radius:12px;background:#f1f5f9;border:1px solid #e2e8f0;color:#475569;display:inline-flex;align-items:center;justify-content:center;font-size:1.2rem;cursor:pointer}
.vtool:hover{background:#e2e8f0}
.vtool.on{background:#0f172a;color:#fff;border-color:#0f172a}
.vinfo{padding:14px 18px;background:#fff;font-size:13px;color:#64748b;border-top:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center}
.vinfo .ext{color:#0f172a;font-weight:600}
/* =============================================================
MESSAGES PANEL
============================================================= */
.messages{display:none;flex-direction:column;flex:1;min-height:0;padding:18px;gap:12px;overflow:auto}
.messages.active{display:flex}
.msg{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:18px 22px;display:grid;grid-template-columns:54px 1fr auto;gap:18px;align-items:flex-start}
.msg .mi{width:54px;height:54px;border-radius:12px;display:inline-flex;align-items:center;justify-content:center;font-size:1.4rem}
.msg.error{border-left:6px solid #dc2626}
.msg.error .mi{background:#fee2e2;color:#991b1b}
.msg.warn{border-left:6px solid #f59e0b}
.msg.warn .mi{background:#fef3c7;color:#92400e}
.msg.info{border-left:6px solid #0ea5e9}
.msg.info .mi{background:#dbeafe;color:#1e40af}
.msg.ok{border-left:6px solid #16a34a}
.msg.ok .mi{background:#dcfce7;color:#166534}
.msg .mtitle{font-weight:800;font-size:1.05rem;color:#0f172a}
.msg .mtime{font-size:.8rem;color:#94a3b8;margin-top:2px}
.msg .mbody{margin-top:6px;color:#475569;font-size:.95rem;line-height:1.5}
.msg .mbody .mono{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:.85rem}
.msg .mactions{display:flex;gap:8px}
.mbtn{height:48px;padding:0 16px;border-radius:10px;background:#f1f5f9;border:1px solid #e2e8f0;font-weight:700;color:#0f172a;font-size:.85rem;cursor:pointer}
.mbtn:hover{background:#e2e8f0}
.mbtn.primary{background:#0f172a;color:#fff;border-color:#0f172a}
.mbtn.primary:hover{background:#1e293b}
/* =============================================================
INDICATORS PANEL
============================================================= */
.indicators{display:none;flex:1;min-height:0;padding:18px;gap:14px;overflow:auto;grid-template-columns:repeat(4,1fr);grid-auto-rows:min-content}
.indicators.active{display:grid}
.ind{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px}
.ind-label{font-size:.8rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:#94a3b8}
.ind-val{font-family:'JetBrains Mono',monospace;font-size:1.6rem;font-weight:800;color:#0f172a}
.ind-state{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:700;color:#475569}
.ind-state .dot{width:10px;height:10px;border-radius:9999px}
.ind .progress{height:8px;background:#f1f5f9;border-radius:9999px;overflow:hidden;margin-top:4px}
.ind .progress > div{height:100%;background:#0ea5e9}
.ind.full{grid-column:span 2}
/* =============================================================
MDI PANEL
============================================================= */
.mdi{display:none;flex-direction:column;flex:1;min-height:0;padding:18px;gap:14px}
.mdi.active{display:flex}
.mdi-input{
background:#0b1220;color:#86efac;border:1px solid #1e293b;border-radius:14px;
padding:22px 24px;font-family:'JetBrains Mono',monospace;font-size:1.4rem;font-weight:600;
display:flex;align-items:center;gap:.6rem;
}
.mdi-input .prompt{color:#475569}
.mdi-input .cursor{display:inline-block;width:14px;height:1.4rem;background:#86efac;animation:blink 1s steps(2,end) infinite;vertical-align:middle}
@keyframes blink{50%{opacity:0}}
.mdi-keys{display:grid;grid-template-columns:repeat(8,1fr);gap:8px;flex:0 0 auto}
.mkey{height:64px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-weight:800;font-size:1.05rem;color:#0f172a;cursor:pointer;font-family:'JetBrains Mono',monospace}
.mkey:hover{background:#f1f5f9}
.mkey.send{background:#16a34a;color:#fff;border-color:#15803d;grid-column:span 2;font-family:'Inter',sans-serif;font-size:.95rem;letter-spacing:.04em}
.mkey.send:hover{background:#15803d}
.mkey.clear{background:#fee2e2;color:#7f1d1d;border-color:#fca5a5;font-family:'Inter',sans-serif;font-size:.95rem;letter-spacing:.04em}
.mdi-history{flex:1;background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:14px 18px;overflow:auto;font-family:'JetBrains Mono',monospace;font-size:.95rem}
.mdi-history .h-row{display:grid;grid-template-columns:80px 1fr auto;gap:14px;padding:6px 0;border-bottom:1px solid #f1f5f9;align-items:center}
.mdi-history .h-time{color:#94a3b8;font-size:.8rem}
.mdi-history .h-cmd{color:#0f172a;font-weight:700}
.mdi-history .h-status{color:#16a34a;font-weight:700;font-size:.8rem}
.mdi-history .h-status.err{color:#dc2626}
/* =============================================================
SETTINGS PANEL
============================================================= */
.settings{display:none;flex:1;min-height:0;padding:18px;gap:14px;overflow:auto;grid-template-columns:280px 1fr}
.settings.active{display:grid}
.set-side{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:10px;display:flex;flex-direction:column;gap:4px;height:fit-content}
.set-item{height:56px;padding:0 16px;border-radius:10px;display:flex;align-items:center;gap:.6rem;color:#475569;font-weight:700;cursor:pointer}
.set-item:hover{background:#f1f5f9}
.set-item.active{background:#0f172a;color:#fff}
.set-content{display:flex;flex-direction:column;gap:14px}
.set-card{background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:22px}
.set-title{font-weight:800;font-size:1.1rem;color:#0f172a;margin-bottom:14px}
.set-row{display:grid;grid-template-columns:280px 1fr auto;gap:14px;align-items:center;padding:14px 0;border-bottom:1px solid #f1f5f9}
.set-row:last-child{border-bottom:none}
.set-row .label{font-weight:700;color:#0f172a;font-size:.95rem}
.set-row .desc{color:#64748b;font-size:.85rem;margin-top:2px}
.set-row .val{font-family:'JetBrains Mono',monospace;color:#475569}
.set-input{height:48px;padding:0 14px;border-radius:10px;border:1px solid #e2e8f0;background:#fff;font-family:'JetBrains Mono',monospace;font-size:.95rem;color:#0f172a;min-width:200px}
.set-toggle{width:54px;height:30px;border-radius:9999px;background:#cbd5e1;position:relative;cursor:pointer;transition:background .15s}
.set-toggle::after{content:"";position:absolute;left:3px;top:3px;width:24px;height:24px;border-radius:9999px;background:#fff;transition:transform .15s}
.set-toggle.on{background:#16a34a}
.set-toggle.on::after{transform:translateX(24px)}
</style>
</head>
<body>
<div class="host">
<div class="topbar">
<div class="brand">
<div class="stripe-logo-sm"></div>
ONEFINITY · V09 · Full UX preview
</div>
<span class="pill">Click the inner tabs to navigate</span>
<div style="margin-left:auto"></div>
<button id="oneToOne" class="toggle">1:1</button>
<button id="fitBtn" class="toggle on">Fit</button>
<span id="scaleInfo" class="pill mono">100%</span>
</div>
<div class="stage" id="stage">
<div class="scaler-viewport" id="viewport">
<div class="scaler" id="scaler">
<!-- ============= KIOSK ============= -->
<div class="kiosk">
<header class="head">
<div class="brand-blk">
<div class="brand-logo"></div>
<div class="brand-name">ONEFINITY</div>
</div>
<div class="kiosk-tabs">
<button class="ktab active" data-target="control"><i class="fa-solid fa-gamepad"></i> Control</button>
<button class="ktab" data-target="program"><i class="fa-solid fa-list-ol"></i> Program</button>
<button class="ktab" data-target="console"><i class="fa-solid fa-terminal"></i> Console <span class="ktab-badge">2</span></button>
<button class="ktab" data-target="settings"><i class="fa-solid fa-sliders"></i> Settings</button>
</div>
<button class="sys-btn"><span class="pip"></span> All systems · view <i class="fa-solid fa-chevron-down" style="font-size:10px;opacity:.6"></i></button>
<span class="state-badge"><span class="dot"></span> READY</span>
<button class="estop">STOP</button>
</header>
<div class="body">
<!-- ============= CONTROL ============= -->
<div class="panel active" data-panel="control">
<div class="control-grid">
<!-- jog -->
<div class="jog-card">
<div class="jog-head">
<div class="jog-title">Jog · step <span class="step">10mm</span></div>
<div class="step-seg">
<button>0.1</button><button>1</button><button class="active">10</button><button>100</button>
</div>
</div>
<div class="jog-grid">
<button class="jbtn dir"><i class="fa-solid fa-arrow-up ico" style="transform:rotate(-45deg)"></i></button>
<button class="jbtn">Y+</button>
<button class="jbtn dir"><i class="fa-solid fa-arrow-up ico" style="transform:rotate(45deg)"></i></button>
<button class="jbtn">Z+</button>
<button class="jbtn">X</button>
<button class="jbtn ghost"><span class="lbl">XY</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
<button class="jbtn">X+</button>
<button class="jbtn ghost"><span class="lbl">Z</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
<button class="jbtn dir"><i class="fa-solid fa-arrow-down ico" style="transform:rotate(45deg)"></i></button>
<button class="jbtn">Y</button>
<button class="jbtn dir"><i class="fa-solid fa-arrow-down ico" style="transform:rotate(-45deg)"></i></button>
<button class="jbtn">Z</button>
<button class="jbtn"><i class="fa-solid fa-arrow-down ico"></i><span class="lbl">W</span></button>
<button class="jbtn ghost"><span class="lbl">W</span><span style="font-size:1rem;font-weight:700">Origin</span></button>
<button class="jbtn"><i class="fa-solid fa-arrow-up ico"></i><span class="lbl">W+</span></button>
<button class="jbtn"><i class="fa-solid fa-house ico"></i><span class="lbl">Home</span></button>
</div>
</div>
<!-- DRO + status -->
<div class="right-col">
<div class="dro-card">
<div class="dro-head">
<div>Axis</div><div>Position</div><div>Absolute</div><div>Offset</div><div>State</div><div>Toolpath</div><div style="text-align:right">Actions</div>
</div>
<div class="dro-row">
<div class="dro-axis axis-x">X</div>
<div class="dro-pos">0.000<span class="u">mm</span></div>
<div class="dro-sec">0.000</div>
<div class="dro-sec">0.000</div>
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
<div class="actions-cell">
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
</div>
</div>
<div class="dro-row">
<div class="dro-axis axis-y">Y</div>
<div class="dro-pos">0.000<span class="u">mm</span></div>
<div class="dro-sec">0.000</div>
<div class="dro-sec">0.000</div>
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
<div class="actions-cell">
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
</div>
</div>
<div class="dro-row z-highlight">
<div class="dro-axis axis-z">Z</div>
<div class="dro-pos">0.000<span class="u">mm</span></div>
<div class="dro-sec">0.000</div>
<div class="dro-sec">0.000</div>
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
<div><span class="chip chip-amber"><i class="fa-solid fa-triangle-exclamation"></i> Over</span></div>
<div class="actions-cell">
<button class="icon-btn"><i class="fa-solid fa-gear"></i></button>
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
</div>
</div>
<div class="dro-row">
<div class="dro-axis axis-w">W</div>
<div class="dro-pos">0.000<span class="u">mm</span></div>
<div class="dro-sec">0.000</div>
<div class="dro-sec" style="opacity:.4"></div>
<div><span class="chip chip-amber"><i class="fa-solid fa-question"></i> Unhomed</span></div>
<div><span class="chip chip-green"><i class="fa-solid fa-check"></i> OK</span></div>
<div class="actions-cell">
<button class="icon-btn"><i class="fa-solid fa-location-dot"></i></button>
<button class="icon-btn"><i class="fa-solid fa-house"></i></button>
</div>
</div>
</div>
<div class="status-strip">
<div class="stat-card"><div class="stat-label">State</div><div class="stat-val ok">READY</div><div class="stat-sub">No alerts</div></div>
<div class="stat-card"><div class="stat-label">Velocity / Feed</div><div class="stat-val">0 · 0</div><div class="stat-sub">m/min · mm/min</div></div>
<div class="stat-card"><div class="stat-label">Spindle</div><div class="stat-val">0 (0)</div><div class="stat-sub">RPM (commanded / actual)</div></div>
<div class="stat-card"><div class="stat-label">Job</div><div class="stat-val">0 / 1,785</div><div class="stat-sub">Line · 19:07 remaining</div></div>
</div>
</div>
</div>
<!-- macros -->
<div class="macro-row">
<button class="macro-btn"><span class="mnum">1</span><i class="fa-solid fa-circle-play micon"></i> Macro 1</button>
<button class="macro-btn"><span class="mnum">2</span><i class="fa-solid fa-circle-play micon"></i> Macro 2</button>
<button class="macro-btn"><span class="mnum">3</span><i class="fa-solid fa-circle-play micon"></i> Macro 3</button>
<button class="macro-btn"><span class="mnum">4</span><i class="fa-solid fa-circle-play micon"></i> Macro 4</button>
<button class="macro-btn"><span class="mnum">5</span><i class="fa-solid fa-circle-play micon"></i> Macro 5</button>
<button class="macro-btn"><span class="mnum">6</span><i class="fa-solid fa-circle-play micon"></i> Macro 6</button>
<button class="macro-btn"><span class="mnum">7</span><i class="fa-solid fa-circle-play micon"></i> Macro 7</button>
<button class="macro-btn"><span class="mnum">8</span><i class="fa-solid fa-circle-play micon"></i> Macro 8</button>
</div>
</div>
<!-- ============= PROGRAM ============= -->
<div class="panel" data-panel="program" style="padding:0;gap:0">
<div class="program-card" style="margin:18px;border-radius:18px">
<!-- Auto sub-panel -->
<div class="auto-sub" data-sub="auto" style="display:flex;flex-direction:column;flex:1;min-height:0">
<div class="action-bar">
<button class="action-btn run"><i class="fa-solid fa-play ico"></i><span>RUN</span></button>
<button class="action-btn stop"><i class="fa-solid fa-stop ico"></i><span>STOP</span></button>
<button class="action-btn"><i class="fa-solid fa-folder-arrow-up ico"></i><span>UPLOAD FOLDER</span></button>
<button class="action-btn"><i class="fa-solid fa-file-arrow-up ico"></i><span>UPLOAD FILE</span></button>
<button class="action-btn"><i class="fa-solid fa-file-arrow-down ico"></i><span>DOWNLOAD FILE</span></button>
<button class="action-btn danger"><i class="fa-solid fa-trash ico"></i><span>DELETE</span></button>
</div>
<div class="file-bar">
<button class="file-btn"><i class="fa-solid fa-folder-plus"></i> Create Folder</button>
<button class="file-btn"><i class="fa-solid fa-folder-minus"></i> Delete Folder</button>
<span class="file-select"><i class="fa-solid fa-folder-open" style="color:#64748b"></i> Default folder <i class="fa-solid fa-chevron-down caret"></i></span>
<span class="file-select primary"><i class="fa-solid fa-file-code" style="color:#0ea5e9"></i> thin-rough.nc <i class="fa-solid fa-chevron-down caret" style="margin-left:auto"></i></span>
<span class="file-select"><i class="fa-solid fa-arrow-down-wide-short" style="color:#64748b"></i> By Upload Date <i class="fa-solid fa-chevron-down caret"></i></span>
</div>
<div class="program-body">
<div class="gcode" id="gcode-list"></div>
<div class="viewer">
<div class="viewer-3d">
<svg viewBox="0 0 400 220" style="width:100%;height:100%">
<defs>
<pattern id="gridv" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#1e293b" stroke-width="1"/>
</pattern>
</defs>
<rect width="400" height="220" fill="url(#gridv)"/>
<rect x="40" y="80" width="320" height="60" stroke="#475569" stroke-width="1" fill="none" stroke-dasharray="3 3"/>
<text x="40" y="74" fill="#64748b" font-size="9" font-family="monospace">Stock: 250 × 25 × 16 mm</text>
<!-- toolpath -->
<path d="M40,110 L360,110 M40,100 L360,100 M40,120 L360,120 M40,90 L360,90 M40,130 L360,130" stroke="#22c55e" stroke-width="1.4" fill="none" opacity=".8"/>
<path d="M40,110 L40,80 L60,80 L60,110 M80,110 L80,80 L100,80 L100,110 M120,110 L120,80 L140,80 L140,110" stroke="#ef4444" stroke-width="1.4" fill="none" opacity=".8"/>
<circle cx="40" cy="110" r="3" fill="#22c55e"/>
<circle cx="360" cy="110" r="3" fill="#ef4444"/>
<text x="46" y="108" fill="#22c55e" font-size="8" font-family="monospace">START</text>
<text x="332" y="108" fill="#ef4444" font-size="8" font-family="monospace">END</text>
<!-- axes gizmo -->
<g transform="translate(28,196)">
<line x1="0" y1="0" x2="22" y2="0" stroke="#ef4444" stroke-width="2"/>
<line x1="0" y1="0" x2="0" y2="-22" stroke="#3b82f6" stroke-width="2"/>
<line x1="0" y1="0" x2="-12" y2="12" stroke="#22c55e" stroke-width="2"/>
<text x="24" y="4" fill="#ef4444" font-size="9" font-family="monospace">X</text>
<text x="-4" y="-26" fill="#3b82f6" font-size="9" font-family="monospace">Z</text>
<text x="-22" y="22" fill="#22c55e" font-size="9" font-family="monospace">Y</text>
</g>
</svg>
</div>
<div class="viewer-tools">
<button class="vtool" title="Fit"><i class="fa-solid fa-expand"></i></button>
<button class="vtool on" title="Tool"><i class="fa-solid fa-screwdriver-wrench"></i></button>
<button class="vtool" title="Stock"><i class="fa-solid fa-cube"></i></button>
<button class="vtool" title="Origin"><i class="fa-solid fa-up-right-and-down-left-from-center"></i></button>
<button class="vtool" title="Top"><i class="fa-solid fa-square"></i></button>
<button class="vtool" title="Front"><i class="fa-solid fa-square-full"></i></button>
<button class="vtool" title="Iso"><i class="fa-solid fa-cubes"></i></button>
</div>
<div class="vinfo">
<span><span class="ext">thin-rough.nc</span> · 1,785 lines · 12.4 KB</span>
<span class="mono">est. 19:07</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ============= CONSOLE ============= -->
<div class="panel" data-panel="console" style="padding:0;gap:0">
<div class="program-card" style="margin:18px;border-radius:18px">
<div class="ptab-bar">
<button class="ptab active" data-ptab="mdi"><i class="fa-solid fa-keyboard"></i> MDI</button>
<button class="ptab" data-ptab="messages"><i class="fa-solid fa-comment-dots"></i> Messages <span class="ptab-badge">2</span></button>
<button class="ptab" data-ptab="indicators"><i class="fa-solid fa-bell"></i> Indicators</button>
</div>
<!-- MDI sub-panel -->
<div class="mdi active" data-sub="mdi">
<div class="mdi-input">
<span class="prompt">G&gt;</span>
<span class="mono">G0 X100 Y50 F2000</span>
<span class="cursor"></span>
</div>
<div class="mdi-keys">
<button class="mkey">G0</button>
<button class="mkey">G1</button>
<button class="mkey">G2</button>
<button class="mkey">G3</button>
<button class="mkey">G28</button>
<button class="mkey">G92</button>
<button class="mkey">M3</button>
<button class="mkey">M5</button>
<button class="mkey">X</button>
<button class="mkey">Y</button>
<button class="mkey">Z</button>
<button class="mkey">W</button>
<button class="mkey">F</button>
<button class="mkey">S</button>
<button class="mkey clear">CLEAR</button>
<button class="mkey send">SEND ↵</button>
</div>
<div class="mdi-history">
<div class="h-row"><span class="h-time">19:42:11</span><span class="h-cmd">G21</span><span class="h-status">✓ ok</span></div>
<div class="h-row"><span class="h-time">19:42:14</span><span class="h-cmd">G90</span><span class="h-status">✓ ok</span></div>
<div class="h-row"><span class="h-time">19:43:02</span><span class="h-cmd">G0 Y12.800</span><span class="h-status">✓ ok</span></div>
<div class="h-row"><span class="h-time">19:43:08</span><span class="h-cmd">G0 Z19.040</span><span class="h-status">✓ ok</span></div>
<div class="h-row"><span class="h-time">19:43:30</span><span class="h-cmd">G1 Z-20 F800</span><span class="h-status err">✗ blocked: Z over travel</span></div>
<div class="h-row"><span class="h-time">19:44:01</span><span class="h-cmd">G0 Z5</span><span class="h-status">✓ ok</span></div>
</div>
</div>
<!-- Messages sub-panel -->
<div class="messages" data-sub="messages">
<div class="msg warn">
<div class="mi"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div>
<div style="display:flex;align-items:baseline;gap:.6rem">
<div class="mtitle">Z toolpath exceeds soft-limit</div>
<div class="mtime">2 min ago · sticky</div>
</div>
<div class="mbody">Loaded program reaches <span class="mono">Z = -16.500</span>. Configured soft-limit is <span class="mono">Z = -15.000</span>. Adjust the Z origin or set a deeper soft-limit before running.</div>
</div>
<div class="mactions">
<button class="mbtn">Open settings</button>
<button class="mbtn primary">Acknowledge</button>
</div>
</div>
<div class="msg info">
<div class="mi"><i class="fa-solid fa-circle-info"></i></div>
<div>
<div style="display:flex;align-items:baseline;gap:.6rem">
<div class="mtitle">Camera offline</div>
<div class="mtime">12 min ago</div>
</div>
<div class="mbody">Camera at <span class="mono">10.1.10.55:8554</span> did not respond on last poll. Live preview disabled.</div>
</div>
<div class="mactions">
<button class="mbtn">Retry</button>
<button class="mbtn">Dismiss</button>
</div>
</div>
<div class="msg ok">
<div class="mi"><i class="fa-solid fa-check"></i></div>
<div>
<div style="display:flex;align-items:baseline;gap:.6rem">
<div class="mtitle">File uploaded · thin-rough.nc</div>
<div class="mtime">21 min ago</div>
</div>
<div class="mbody">1,785 lines · 12.4 KB · checksum verified.</div>
</div>
<div class="mactions">
<button class="mbtn">Open</button>
</div>
</div>
<div class="msg error">
<div class="mi"><i class="fa-solid fa-circle-xmark"></i></div>
<div>
<div style="display:flex;align-items:baseline;gap:.6rem">
<div class="mtitle">WiFi: not connected</div>
<div class="mtime">1 h ago</div>
</div>
<div class="mbody">Falling back to wired ethernet. SSID <span class="mono">workshop-2g</span> last seen 53 min ago.</div>
</div>
<div class="mactions">
<button class="mbtn">Network…</button>
<button class="mbtn">Mute</button>
</div>
</div>
</div>
<!-- Indicators sub-panel -->
<div class="indicators" data-sub="indicators">
<div class="ind">
<div class="ind-label">Spindle Load</div>
<div class="ind-val">0 %</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> idle</div>
<div class="progress"><div style="width:0%"></div></div>
</div>
<div class="ind">
<div class="ind-label">Spindle Temp</div>
<div class="ind-val">24 °C</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> nominal</div>
<div class="progress"><div style="width:24%"></div></div>
</div>
<div class="ind">
<div class="ind-label">Driver Voltage</div>
<div class="ind-val">48.1 V</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
</div>
<div class="ind">
<div class="ind-label">Coolant</div>
<div class="ind-val">OFF</div>
<div class="ind-state"><span class="dot" style="background:#94a3b8"></span> standby</div>
</div>
<div class="ind">
<div class="ind-label">Limit X</div>
<div class="ind-val" style="color:#16a34a">CLEAR</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
</div>
<div class="ind">
<div class="ind-label">Limit Y</div>
<div class="ind-val" style="color:#16a34a">CLEAR</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
</div>
<div class="ind">
<div class="ind-label">Limit Z</div>
<div class="ind-val" style="color:#dc2626">BLOCKED</div>
<div class="ind-state"><span class="dot" style="background:#dc2626"></span> over-travel</div>
</div>
<div class="ind">
<div class="ind-label">Probe</div>
<div class="ind-val">OPEN</div>
<div class="ind-state"><span class="dot" style="background:#94a3b8"></span> not contacted</div>
</div>
<div class="ind">
<div class="ind-label">E-Stop</div>
<div class="ind-val" style="color:#16a34a">RELEASED</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> safe</div>
</div>
<div class="ind">
<div class="ind-label">Door</div>
<div class="ind-val">CLOSED</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
</div>
<div class="ind">
<div class="ind-label">Air Pressure</div>
<div class="ind-val">6.2 bar</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> ok</div>
<div class="progress"><div style="width:62%"></div></div>
</div>
<div class="ind">
<div class="ind-label">Vacuum</div>
<div class="ind-val">0.81 bar</div>
<div class="ind-state"><span class="dot" style="background:#16a34a"></span> hold</div>
<div class="progress"><div style="width:81%"></div></div>
</div>
</div>
</div>
</div>
<!-- ============= SETTINGS ============= -->
<div class="panel" data-panel="settings" style="padding:0;gap:0">
<div class="settings active" style="padding:18px">
<div class="set-side">
<div class="set-item active"><i class="fa-solid fa-display"></i> Display & Units</div>
<div class="set-item"><i class="fa-solid fa-arrows-up-down-left-right"></i> Motion</div>
<div class="set-item"><i class="fa-solid fa-bolt"></i> Spindle</div>
<div class="set-item"><i class="fa-solid fa-shield-halved"></i> Safety / Soft-limits</div>
<div class="set-item"><i class="fa-solid fa-network-wired"></i> Network</div>
<div class="set-item"><i class="fa-solid fa-video"></i> Camera</div>
<div class="set-item"><i class="fa-solid fa-keyboard"></i> Macros</div>
<div class="set-item"><i class="fa-solid fa-circle-info"></i> About</div>
</div>
<div class="set-content">
<div class="set-card">
<div class="set-title">Display & Units</div>
<div class="set-row">
<div>
<div class="label">Display Units</div>
<div class="desc">Position, feed and dimensions throughout the UI.</div>
</div>
<div><div class="step-seg" style="display:inline-flex"><button class="active">METRIC</button><button>IMPERIAL</button></div></div>
<div></div>
</div>
<div class="set-row">
<div>
<div class="label">Decimal places</div>
<div class="desc">Position readout precision.</div>
</div>
<div><input class="set-input" value="3" /></div>
<div class="val">04</div>
</div>
<div class="set-row">
<div>
<div class="label">Pulse-dot animation</div>
<div class="desc">Animate status badges (ready, idle, alarm).</div>
</div>
<div><div class="set-toggle on"></div></div>
<div></div>
</div>
<div class="set-row">
<div>
<div class="label">Theme</div>
<div class="desc">Pick a tile finish.</div>
</div>
<div><span class="file-select"><i class="fa-solid fa-palette" style="color:#64748b"></i> V09 · Flat soft slate <i class="fa-solid fa-chevron-down caret"></i></span></div>
<div></div>
</div>
</div>
<div class="set-card">
<div class="set-title">Network</div>
<div class="set-row">
<div>
<div class="label">IP Address</div>
<div class="desc">Wired ethernet, DHCP.</div>
</div>
<div><span class="mono" style="font-size:1.05rem;font-weight:700">10.1.10.55</span></div>
<div><button class="mbtn">Edit</button></div>
</div>
<div class="set-row">
<div>
<div class="label">WiFi</div>
<div class="desc">Wireless network connection.</div>
</div>
<div><span class="chip chip-red"><i class="fa-solid fa-wifi"></i> Not connected</span></div>
<div><button class="mbtn primary">Configure</button></div>
</div>
<div class="set-row">
<div>
<div class="label">Hostname</div>
<div class="desc">Used in mDNS / Bonjour discovery.</div>
</div>
<div><input class="set-input" value="onefinity-shop.local" style="width:300px" /></div>
<div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ----- Build G-code list -----
const gcodeLines = [
[1,'G21','word'],[2,'; X = along blank, Z = tool entry from top, Y fixed','c'],
[3,'; Y fixed to blank center: 12.800','c'],[4,'; nominal rapid: 3200.0 mm/min','c'],
[5,'; stock top Z: -0.960','c'],[6,'; deepest allowed cut Z: -16.500','c'],
[7,'G21','word'],[8,'G90','word'],[9,'G0 Y12.800','word'],
[10,'G0 Z19.040','word'],[11,'; rough pass 1 radius=18.540','c'],
[12,'G0 X0.000','word'],[13,'G1 Z-0.710 F800.000','word cur'],
[14,'G1 Z-0.960 F200.000','word'],[15,'G4 P0.250','word'],
[16,'G1 X249.500 F200.000','word'],[17,'G4 P0.250','word'],
[18,'G0 Z19.040','word'],[19,'; rough pass 2 radius=17.540','c'],
[20,'G0 X0.000','word'],[21,'G1 Z-1.710 F800.000','word'],
[22,'G1 Z-1.960 F200.000','word'],[23,'G4 P0.250','word'],
[24,'G1 X249.500 F200.000','word'],[25,'G4 P0.250','word'],
[26,'G0 Z19.040','word'],[27,'; rough pass 3 radius=16.540','c'],
[28,'G0 X0.000','word'],[29,'G1 Z-2.710 F800.000','word'],
[30,'G1 Z-2.960 F200.000','word'],[31,'G4 P0.250','word'],
[32,'G1 X249.500 F200.000','word'],[33,'G4 P0.250','word'],
[34,'G0 Z19.040','word'],[35,'; rough pass 4 radius=15.540','c'],
];
document.getElementById('gcode-list').innerHTML = gcodeLines.map(([n,t,cls])=>{
const isComment = cls.includes('c');
const isCur = cls.includes('cur');
const cls2 = 'gline' + (isCur?' cur':'');
const inner = isComment ? `<span class="gcomment">${t}</span>` : `<span class="gword">${t}</span>`;
return `<div class="${cls2}"><span class="gn">${n}</span><span>${inner}</span></div>`;
}).join('');
// ----- Top tab switching (Control / Program / Settings) -----
document.querySelectorAll('.ktab').forEach(b=>{
b.addEventListener('click', ()=>{
const target = b.dataset.target;
document.querySelectorAll('.ktab').forEach(x=>x.classList.remove('active'));
b.classList.add('active');
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.querySelector(`.panel[data-panel="${target}"]`).classList.add('active');
applyScale();
});
});
// ----- Console sub-tab switching (MDI / Messages / Indicators) -----
function showSub(name){
document.querySelectorAll('.ptab').forEach(x=>x.classList.toggle('active', x.dataset.ptab===name));
document.querySelectorAll('[data-sub]').forEach(s=>{
const on = s.dataset.sub===name;
if(s.classList.contains('messages') || s.classList.contains('indicators') || s.classList.contains('mdi')){
s.classList.toggle('active', on);
}
});
}
document.querySelectorAll('.ptab').forEach(b=>{
b.addEventListener('click', ()=>{ showSub(b.dataset.ptab); });
});
// Default Console sub: MDI active
document.querySelectorAll('.messages[data-sub], .indicators[data-sub]').forEach(s=>s.classList.remove('active'));
// ----- Scaling -----
const stage = document.getElementById('stage');
const scaler = document.getElementById('scaler');
const viewport = document.getElementById('viewport');
const fitBtn = document.getElementById('fitBtn');
const oneToOne = document.getElementById('oneToOne');
const scaleInfo = document.getElementById('scaleInfo');
let mode = 'fit';
function activeKioskHeight(){
const m = document.querySelector('.kiosk');
return m ? Math.max(1080, m.offsetHeight) : 1080;
}
function applyScale(){
let s;
if(mode==='1:1'){
s = 1; scaleInfo.textContent = '100% · 1920px wide';
} else {
const sw = stage.clientWidth - 32;
s = Math.min(sw/1920, 1);
scaleInfo.textContent = Math.round(s*100) + '% · 1920px wide';
}
const h = activeKioskHeight();
scaler.style.transform = `scale(${s})`;
viewport.style.width = (1920 * s) + 'px';
viewport.style.height = (h * s) + 'px';
}
window.addEventListener('resize', applyScale);
fitBtn.addEventListener('click', ()=>{ mode='fit'; fitBtn.classList.add('on'); oneToOne.classList.remove('on'); applyScale(); });
oneToOne.addEventListener('click', ()=>{ mode='1:1'; oneToOne.classList.add('on'); fitBtn.classList.remove('on'); applyScale(); });
applyScale();
</script>
</body>
</html>