feat(#994): tabbar overflow menu — settings and logs in ⋮ by default

Add a ⋮ overflow button at the right end of the dashboard tab strip.

SETTINGS and LOGS always live in the overflow dropdown (marked
data-overflow="default" in index.html — never rendered in the main bar).

Dynamic overflow: when the bar is too narrow to fit all remaining tabs,
rightmost tabs spill into the dropdown right-to-left. Implemented via
JS measurement + ResizeObserver; no CSS-only relayout trick needed.

The ⋮ button:
- hidden when the dropdown is empty (wide screens with only the default
  tabs overflowed, which are always there anyway → button always shown)
- gets .tabbar-overflow-active when the current hash tab is inside
- dropdown items clone the tab label + count badge from the original
- closes on outside click / Escape

MutationObserver on the tabbar catches P33RS/M4TR1X hidden-attribute
changes so the overflow recalculates when those tabs are shown/hidden.
This commit is contained in:
iris 2026-06-01 19:39:17 +02:00
commit e9d22ef2d3
3 changed files with 271 additions and 14 deletions

View file

@ -45,6 +45,7 @@ body.dashboard-shell {
.tabbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.2em;
padding: 0.5em 1em 0;
@ -98,6 +99,104 @@ body.dashboard-shell {
color: var(--red);
}
/* Overflowed tabs are hidden from the bar (moved to the ⋮ dropdown). */
.tabbar .tab[data-overflow="default"],
.tabbar .tab.tab-overflowed {
display: none;
}
/* ── overflow ⋮ button + dropdown ────────────────────────── */
.tabbar-overflow {
position: relative;
flex-shrink: 0;
margin-left: auto; /* push to the far right */
align-self: stretch;
display: flex;
align-items: center;
}
.tabbar-overflow-btn {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
color: var(--muted);
font-size: 1.2em;
line-height: 1;
padding: 0.3em 0.5em;
cursor: pointer;
transition: color 0.15s, background 0.15s;
white-space: nowrap;
}
.tabbar-overflow-btn:hover {
color: var(--purple);
background: rgba(203, 166, 247, 0.06);
}
.tabbar-overflow-btn.tabbar-overflow-active {
color: var(--purple);
border-color: var(--purple-dim);
}
.tabbar-overflow-dropdown {
position: absolute;
right: 0;
top: calc(100% + 2px);
z-index: 100;
background: var(--surface0);
border: 1px solid var(--surface2);
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
list-style: none;
margin: 0;
padding: 0.3em 0;
min-width: 12em;
white-space: nowrap;
}
.tabbar-overflow-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6em;
width: 100%;
background: none;
border: none;
color: var(--text);
font-family: inherit;
font-size: 0.88em;
letter-spacing: 0.05em;
text-align: left;
text-decoration: none;
padding: 0.45em 1em;
cursor: pointer;
}
.tabbar-overflow-item:hover {
background: var(--surface1);
color: var(--purple);
}
.tabbar-overflow-item-active {
color: var(--purple);
}
.tabbar-overflow-badge {
display: inline-block;
background: var(--purple-dim);
color: var(--purple);
border-radius: 999px;
padding: 0 0.5em;
min-width: 1.4em;
text-align: center;
font-size: 0.82em;
font-variant-numeric: tabular-nums;
font-weight: bold;
}
.tabbar-overflow-badge.tab-count-attn {
background: rgba(243, 139, 168, 0.22);
color: var(--red);
}
/* Tab pane visibility show only the active one. The .tab-pane-active
class is set by tabs.js based on the URL hash; default (no hash)
resolves to SW4RM. */