From 349eb45332b092e3cf5d99ce6ebc22e0dc9f560b Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:13:17 +0200 Subject: [PATCH 1/2] feat: group tabs in the top bar and add a second row navItems entries may now carry a group key. The top bar shows one entry for each group that has at least one child (Print Hub, placed before Spools), and a second row lists the children while one of them is open. Choosing a group opens the child that was open last, or its first child. A group without children is not shown, so nothing changes visibly until a feature adds its first tab to Print Hub. --- multiace/i18n/de.json | 1 + multiace/i18n/en.json | 1 + multiace/i18n/zh.json | 1 + multiace/web/frontend/app.js | 51 +++++++++++++++++++++++++++++++- multiace/web/frontend/index.html | 11 +++++-- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/multiace/i18n/de.json b/multiace/i18n/de.json index a699c31a..5e9e447b 100644 --- a/multiace/i18n/de.json +++ b/multiace/i18n/de.json @@ -12,6 +12,7 @@ "ws_error": "WS-Fehler" }, "tabs": { + "print_hub": "Print Hub", "dashboard": "Übersicht", "dry": "Trocknen", "config": "Konfiguration", diff --git a/multiace/i18n/en.json b/multiace/i18n/en.json index ee3569f0..2ea0944d 100644 --- a/multiace/i18n/en.json +++ b/multiace/i18n/en.json @@ -12,6 +12,7 @@ "ws_error": "WS error" }, "tabs": { + "print_hub": "Print Hub", "dashboard": "Dashboard", "dry": "Dry", "config": "Config", diff --git a/multiace/i18n/zh.json b/multiace/i18n/zh.json index 4bdfb204..5503671a 100644 --- a/multiace/i18n/zh.json +++ b/multiace/i18n/zh.json @@ -12,6 +12,7 @@ "ws_error": "WebSocket 错误" }, "tabs": { + "print_hub": "打印中心", "dashboard": "控制台", "dry": "烘干", "config": "配置", diff --git a/multiace/web/frontend/app.js b/multiace/web/frontend/app.js index aa311303..b2d07fec 100644 --- a/multiace/web/frontend/app.js +++ b/multiace/web/frontend/app.js @@ -72,6 +72,55 @@ createApp({ } return items; }); + // Groups of tabs. An entry with `group: ""` is a child of that group: + // the top bar shows ONE entry for the group (placed before the entry named + // in `before`) and a second row lists the children while one of them is + // open. A group with no children is not shown, so adding a feature's tab + // is the only thing that makes its group appear. + const NAV_GROUPS = [ + {key: "printhub", labelKey: "ui.tabs.print_hub", before: "spools"}, + ]; + const groupLast = reactive({}); + for (const g of NAV_GROUPS) { + try { groupLast[g.key] = localStorage.getItem("multiace.tabGroup." + g.key) || ""; } + catch (e) { groupLast[g.key] = ""; } + } + const groupOf = (key) => { + const it = navItems.value.find(i => i.key === key); + return it && it.group ? it.group : null; + }; + const activeGroup = computed(() => groupOf(tab.value)); + // Remember the last child opened in each group, per browser. + watch(tab, (v) => { + const g = groupOf(v); + if (!g) return; + groupLast[g] = v; + try { localStorage.setItem("multiace.tabGroup." + g, v); } catch (e) {} + }); + const topItems = computed(() => { + const out = []; + const addGroup = (g) => { + const children = navItems.value.filter(i => i.group === g.key); + if (children.length) { + out.push({key: "group:" + g.key, group: g.key, children, label: t(g.labelKey)}); + } + }; + for (const it of navItems.value) { + if (it.group) continue; + for (const g of NAV_GROUPS) if (g.before === it.key) addGroup(g); + out.push(it); + } + return out; + }); + const subItems = computed(() => + activeGroup.value ? navItems.value.filter(i => i.group === activeGroup.value) : []); + const topActive = (it) => it.group ? activeGroup.value === it.group : tab.value === it.key; + // A group opens the child that was open last, else its first child. + function openNav(it) { + if (!it.group) { tab.value = it.key; return; } + const last = groupLast[it.group]; + tab.value = (it.children.find(c => c.key === last) || it.children[0]).key; + } function pluginIframeSrc(p) { const u = (p && p.ui_url) || "/"; return `/plugin/${p.name}${u.startsWith("/") ? u : "/" + u}`; @@ -7435,7 +7484,7 @@ createApp({ headPlanMapping, multiTopology, slotHead, pinLabel, updateState, updateCheck, updateApply, debugState, debugEnable, debugDisable, - navItems, + navItems, topItems, subItems, topActive, openNav, plugins, refreshPlugins, pluginIframeSrc, notifications, notifWarnOnly, notifTime, dismissNotification, dismissAllNotifications, confirmDialog, okConfirm, altConfirm, cancelConfirm, confirmInputError, diff --git a/multiace/web/frontend/index.html b/multiace/web/frontend/index.html index be857a84..fe32519c 100644 --- a/multiace/web/frontend/index.html +++ b/multiace/web/frontend/index.html @@ -311,8 +311,8 @@

mUlt1ACE :title="t('ui.common.firmware') + ' ' + printerFw">· {{ printerName }}

@@ -333,6 +333,13 @@

mUlt1ACE {{ connText }} + +
{{ t('ui.common.please_restart') }}
{{ t('ui.inbox.banner', {name: state.preflight_inbox?.name}) }} From efc38faece19dc8df9ac36d5c815a043a4322545 Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:14:57 +0200 Subject: [PATCH 2/2] feat: style the tab group row in classic, dark and light The second row uses the same underline marker as the top bar in the new design (theme tokens only, so the light shade follows) and the existing button look in classic. On phones the row scrolls horizontally and the buttons keep a 44px touch height. --- multiace/web/frontend/style-new.css | 16 ++++++++++++++++ multiace/web/frontend/style.css | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/multiace/web/frontend/style-new.css b/multiace/web/frontend/style-new.css index 86c7376f..e2534353 100644 --- a/multiace/web/frontend/style-new.css +++ b/multiace/web/frontend/style-new.css @@ -146,6 +146,20 @@ html[data-theme="new"] nav button.active { background: transparent; color: var(--nt-fg-strong); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); } +/* Second row: the children of the open tab group. Same underline marker as + the top bar, one step smaller. */ +html[data-theme="new"] .subnav { + gap: 0; padding: 0 20px; overflow-x: auto; + background: var(--bg); border-bottom: 1px solid var(--border); +} +html[data-theme="new"] .subnav button { + background: transparent; border: 0; border-radius: 0; padding: 10px 16px; + font-size: 13px; font-weight: 500; color: var(--muted); white-space: nowrap; +} +html[data-theme="new"] .subnav button:hover { color: var(--fg); } +html[data-theme="new"] .subnav button.active { + color: var(--nt-fg-strong); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); +} html[data-theme="new"] header .upload-btn { order: 5; margin-left: 0; border: 0; border-left: 1px solid var(--border); border-radius: 0; background: var(--accent); color: var(--accent-fg); @@ -559,6 +573,8 @@ html[data-theme="new"] .toolhead-card.nt-head .toolhead-actions { grid-area: act order: 6; flex: 1 1 100%; overflow-x: auto; border-top: 1px solid var(--border); } html[data-theme="new"] header nav button { padding: 12px 14px; white-space: nowrap; } + html[data-theme="new"] .subnav { padding: 0 8px; } + html[data-theme="new"] .subnav button { min-height: 44px; padding: 12px 14px; } html[data-theme="new"] header .conn { margin-left: 0; min-height: 44px; } html[data-theme="new"] header .upload-btn { min-height: 44px; } html[data-theme="new"] main { padding: 0 12px 16px; } diff --git a/multiace/web/frontend/style.css b/multiace/web/frontend/style.css index 257302ce..4a837709 100644 --- a/multiace/web/frontend/style.css +++ b/multiace/web/frontend/style.css @@ -94,6 +94,22 @@ nav button { } nav button:hover { border-color: var(--accent); } nav button.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); } +/* Second row: the children of the open tab group. */ +.subnav { + display: flex; gap: .3rem; padding: .4rem 1rem; overflow-x: auto; + border-bottom: 1px solid var(--border); background: var(--card); +} +.subnav button { + background: transparent; color: var(--fg); + border: 1px solid var(--border); + padding: .3rem .8rem; cursor: pointer; + border-radius: 4px; font-size: .85rem; white-space: nowrap; +} +.subnav button:hover { border-color: var(--accent); } +.subnav button.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); } +@media (max-width: 700px) { + .subnav button { min-height: 44px; } +} .upload-input { display: none; } .upload-btn { margin-left: auto;