ui: V09 redesign - Control/Program/Console/Settings shell

Replaces the legacy side-menu chrome with a 4-tab top header.

- index.pug: tablet/kiosk fit-to-viewport script, header tab nav,
  estop/state badges in header.
- app.js: route hash to (control|program|console|<settings-family>),
  multi-section settings shell.
- control-view: header DRO, jog grid, MDI/probe/macros panels.
- program-view + program-mixin: file browser + toolpath preview +
  run/pause/stop, replaces the legacy 'macros' tab content.
- console-view: MDI shell, message log, indicators.
- settings-shell-view: rail-driven inner pages (Display & Units,
  Probing, G-code & Motion, Macros, Network, etc.).
- settings-view: filter Svelte SettingsView to one rail section.
- SettingsView.svelte: tag every section with data-sec=… so the
  filter above can hide non-matching ones.
- style.styl: ~2700 lines of V09 layout, DRO, jog grid, status
  strip, and tablet/kiosk variants.

No A-axis / auxiliary-axis content lives on this branch.
This commit is contained in:
2026-05-03 14:11:29 +02:00
parent c10f5c053a
commit 94072253d4
16 changed files with 4165 additions and 1876 deletions

View File

@@ -0,0 +1,169 @@
"use strict";
// Wrapper that adds a left-rail navigator around the settings family
// of views (Settings, Admin General, Admin Network, Tool, IO, Motor,
// Macros, Help, Cheat Sheet). The inner view is selected by the URL
// hash (parsed in app.js) and exposed as $root.sub_tab.
// Vue 1 has trouble making child components reactive to `$root.sub_tab`
// changes (whether via computed, watch, or prop binding through
// `<component :is>`). The shell instead listens to `hashchange`
// directly and parses the hash itself, mirroring app.js's logic, then
// keeps a local data prop `sub` that the template binds to. This is
// the only path that updates the rail's `:class` reactively.
module.exports = {
template: "#settings-shell-view-template",
props: ["config", "template", "state", "index"],
components: {
"settings-view-inner": require("./settings-view"),
"admin-general-view": require("./admin-general-view"),
"admin-network-view": require("./admin-network-view"),
"motor-view": require("./motor-view"),
"tool-view": require("./tool-view"),
"io-view": require("./io-view"),
"macros-view": require("./macros"),
"help-view": require("./help-view"),
"cheat-sheet-view": {
template: "#cheat-sheet-view-template",
data: function () {
return { showUnimplemented: false };
},
},
},
data: function () {
return {
sub: this.$root.sub_tab || "settings",
ridx: this.$root.index, // local copy of the motor index
// Whether the controller config has streamed in. The Svelte
// settings views crash on first paint with the placeholder
// config (settings.units / settings.easy-adapter / motion.*
// are all undefined). Gate the inner mount on this flag.
config_ready: false,
rail_items: [
{ sub: "settings", href: "#settings", icon: "fa-display", label: "Display & Units" },
{ sub: "probing", href: "#probing", icon: "fa-bullseye", label: "Probing" },
{ sub: "gcode", href: "#gcode", icon: "fa-code", label: "G-code & Motion" },
{ sub: "macros", href: "#macros", icon: "fa-keyboard", label: "Macros" },
{ sub: "cheat-sheet", href: "#cheat-sheet", icon: "fa-book", label: "G-code Cheat Sheet" },
{ sub: "admin-network", href: "#admin-network", icon: "fa-network-wired", label: "Network" },
{ sub: "admin-general", href: "#admin-general", icon: "fa-shield-halved", label: "General / Firmware" },
{ sub: "tool", href: "#tool", icon: "fa-bolt", label: "Spindle & Tool" },
{ sub: "io", href: "#io", icon: "fa-plug", label: "I/O" },
{ section: "Motors" },
{ sub: "motor", motor: 0, href: "#motor:0", icon: "fa-arrows-up-down-left-right", label: "Motor 0" },
{ sub: "motor", motor: 1, href: "#motor:1", icon: "fa-arrows-up-down-left-right", label: "Motor 1" },
{ sub: "motor", motor: 2, href: "#motor:2", icon: "fa-arrows-up-down-left-right", label: "Motor 2" },
{ sub: "motor", motor: 3, href: "#motor:3", icon: "fa-arrows-up-down-left-right", label: "Motor 3" },
{ section: " " },
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
],
};
},
ready: function () {
this._onHash = () => this.refresh_from_hash();
window.addEventListener("hashchange", this._onHash);
this.refresh_from_hash();
this._configPoll = setInterval(() => {
const c = this.$root && this.$root.config;
const ready = !!(c && c.full_version && c.full_version !== "<loading>"
&& c.settings && typeof c.settings === "object");
if (ready !== this.config_ready) this.config_ready = ready;
}, 200);
},
attached: function () {
// Vue 1 fires `attached` whenever the component is inserted into
// the DOM (which happens on every route change because the outer
// <component :is> recreates the instance). Re-bind the listener
// here so it works even after detach/attach cycles.
if (!this._onHash) {
this._onHash = () => this.refresh_from_hash();
}
window.addEventListener("hashchange", this._onHash);
this.refresh_from_hash();
},
detached: function () {
if (this._onHash) {
window.removeEventListener("hashchange", this._onHash);
}
},
beforeDestroy: function () {
if (this._onHash) {
window.removeEventListener("hashchange", this._onHash);
}
if (this._configPoll) clearInterval(this._configPoll);
},
methods: {
refresh_from_hash: function () {
const hash = location.hash.substr(1) || "settings";
const parts = hash.split(":");
this.sub = parts[0] || "settings";
this.ridx = parts[1] !== undefined ? parts[1] : -1;
},
is_active: function (item) {
if (!item || item.section) return false;
if (item.sub !== this.sub) return false;
if (item.sub === "motor") {
return "" + item.motor === "" + this.ridx;
}
return true;
},
on_rail_click: function (item, ev) {
if (!item) return;
// Always preventDefault on rail clicks. Letting the browser
// anchor-scroll to <div id="settings"> etc. inside .app-body
// can pull the .app-head out of view; we drive navigation
// ourselves through location.hash and our hashchange handler.
if (ev && ev.preventDefault) ev.preventDefault();
if (item.anchor) {
// Soft-link rail items use a #settings hash plus an in-page
// anchor scroll once the Svelte page has mounted. We scroll
// ONLY the .settings-content overflow container by setting
// its scrollTop directly — element.scrollIntoView() walks all
// ancestor scroll containers and can tug the .app-body / html
// layout, which under tablet mode pulls the fixed header out
// of view.
if (location.hash !== item.href) location.hash = item.href;
const reset = () => {
// Force any inadvertent ancestor scroll back to 0 before
// we move .settings-content explicitly.
window.scrollTo(0, 0);
const body = document.querySelector(".app-body");
if (body) body.scrollTop = 0;
document.documentElement.scrollTop = 0;
};
setTimeout(() => {
reset();
const el = document.getElementById(item.anchor);
const scroller = document.querySelector(".settings-content");
if (el && scroller) {
const elTop = el.getBoundingClientRect().top;
const scTop = scroller.getBoundingClientRect().top;
scroller.scrollTop = scroller.scrollTop + (elTop - scTop) - 12;
}
// Re-assert ancestor scroll = 0 in case the assignment above
// moved things.
requestAnimationFrame(reset);
}, 320);
} else {
if (location.hash !== item.href) location.hash = item.href;
// Reset .app-body scroll so each route starts at the top.
const body = document.querySelector(".app-body");
if (body) body.scrollTop = 0;
}
},
showShutdownDialog: function () {
SvelteComponents.showDialog("Shutdown");
},
},
};