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}) }} 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;