Node detail: box every tab, layer corners by z-order

All tabs now render as full boxes at all times (muted by default,
accent when active) instead of only the active one — so every tab's
width is fixed regardless of selection, and labels never shift
column when the active tab changes. Tabs share a single vertical at
each seam; the shared corner "opens" only at the strip's leading
edge and at the active tab's own left edge, "closing" (ceding to the
tab on its left) everywhere else, so the active tab reads as lifted
in front of its neighbours.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Sonnet 5 committed 2026-07-26 13:33:35 -04:00
1 parent a0f91f3b9c
commit 76125f7402
2 files changed
+57 -35

No files matched your search

+38 -25
View File
@@ -910,19 +910,22 @@ def node_type_legend(indent: str = "") -> Text:
def tab_strip(labels: Sequence[str], active: int, width: int) -> Group:
"""A boxed tab strip: the active view drawn as a notebook tab, the rest sat beside it muted.
"""A boxed tab strip: every tab always boxed, the active one lit on top of the rest.
THE navigation header for a screen that pages one full-height view at a time between a
handful of named views (the node detail page's ``Info`` / ``Routes``) instead of stacking
them. A blank line of air above the strip plus the strip's own line was already the going
rate, so the strip spends a third line on an actual tab shape instead of a plain heading:
the active label sits boxed in accent-coloured rounded corners, its bottom edge opening
into a full-width rule that reads as the seam between chrome and page. The inactive tabs
sit beside the box muted and unboxed, so the alternative view stays visible without
competing. A lone tab collapses to the plain accent heading
(:func:`~meshterm.ui.menus.section_heading`'s ``── Label ──`` form) since there is nothing
to switch between. The owning screen switches the active index (``←→``, with
``Tab``/``Shift+Tab`` as a backup); the strip itself is pure presentation.
them. Every tab is always drawn as a full box — muted by default, accent when active —
instead of only the active one; this keeps every tab's width fixed regardless of which
is selected, so switching tabs never shifts the labels that come after it. Tabs share a
vertical between neighbours (one glyph, not two), and each shared corner is drawn as if
the tab to the left sits on top of the tab to the right: the corner "opens" (rounds
toward the tab starting there) only at the very first tab and at the active tab's own
left edge, and "closes" (rounds back, ceding the position to the tab on its left)
everywhere else — so the active tab is the one exception that always wins both of its
corners, reading as lifted in front of its neighbours. A lone tab collapses to the plain
accent heading (:func:`~meshterm.ui.menus.section_heading`'s ``── Label ──`` form) since
there is nothing to switch between or layer. The owning screen switches the active index
(``←→``, with ``Tab``/``Shift+Tab`` as a backup); the strip itself is pure presentation.
Args:
labels: The tab names in display order.
@@ -930,31 +933,41 @@ def tab_strip(labels: Sequence[str], active: int, width: int) -> Group:
width: The strip's render width — the closing rule fills out to it.
Returns:
A :class:`~rich.console.Group` of one line (a lone tab, or none) or three (the box's
top border, the boxed + muted labels, and the closing rule).
A :class:`~rich.console.Group` of one line (a lone tab, or none) or three (the shared
top border, the boxed labels, and the shared bottom border).
"""
if not labels:
return Group(Text(""))
if len(labels) == 1:
return Group(Text(f"── {labels[0]} ──", style="accent", no_wrap=True))
n = len(labels)
inner = [f" {label} " for label in labels]
def owner(junction: int) -> int:
"""Which tab's corner glyph sits at this junction — see the docstring's layering rule."""
return junction if junction == 0 or junction == active else junction - 1
def corner_style(junction: int) -> str:
return "accent" if owner(junction) == active else "muted"
top = Text(no_wrap=True)
mid = Text(no_wrap=True)
bot = Text(no_wrap=True)
for i, label in enumerate(labels):
if i:
top.append(" ")
mid.append(" ")
bot.append("──", style="muted")
if i == active:
inner = f" {label} "
top.append("╭" + "─" * len(inner) + "╮", style="accent")
mid.append("│" + inner + "│", style="accent")
bot.append(("╰" if i == 0 else "┴") + "─" * len(inner) + "┴", style="accent")
else:
top.append(" " * len(label))
mid.append(label, style="muted")
bot.append("─" * len(label), style="muted")
opens = i == 0 or i == active
style = corner_style(i)
top.append("╭" if opens else "╮", style=style)
mid.append("│", style=style)
bot.append("╰" if opens else "╯", style=style)
tab_style = "accent" if i == active else "muted"
top.append("─" * len(inner[i]), style=tab_style)
mid.append(inner[i], style=tab_style)
bot.append("─" * len(inner[i]), style=tab_style)
last_style = corner_style(n)
top.append("╮", style=last_style)
mid.append("│", style=last_style)
bot.append("╯", style=last_style)
bot.append("─" * max(0, width - len(bot.plain)), style="muted")
return Group(top, mid, bot)
+19 -10
View File
@@ -124,21 +124,30 @@ def test_signal_row_summarizes_snr_and_rssi() -> None:
# --- the tab strip --------------------------------------------------------------
def test_tab_strip_boxes_the_active_tab_and_mutes_the_rest() -> None:
"""The active view reads as a boxed notebook tab; the others sit beside it bare."""
group = tab_strip(["Map", "Routes"], 1, width=17)
def test_tab_strip_boxes_every_tab_and_lights_the_active_one() -> None:
"""Every tab is always boxed; only the active tab's corners and fill read as lit."""
group = tab_strip(["Map", "Routes"], 1, width=20)
top, mid, bot = group.renderables
assert mid.plain == "Map │ Routes │" # active tab boxed, inactive bare
assert top.plain == " ╭──────────╮" # box top over "Routes" only
assert bot.plain == "─────┴──────────┴" # rule joins the box's open bottom
assert len(top.plain) == len(mid.plain) == len(bot.plain) == 17 # no fill needed yet
assert mid.plain == "│ Map │ Routes │" # both tabs boxed, sharing one vertical
assert top.plain == "╭───────╭──────────╮" # active tab's left corner opens (wins the seam)
assert bot.plain == "╰───────╰──────────╯" # mirrored on the bottom border
assert len(top.plain) == len(mid.plain) == len(bot.plain) == 20 # no fill needed yet
def test_tab_strip_labels_hold_position_across_the_active_index() -> None:
"""Selecting a different tab only relights corners — no label ever shifts column."""
mid_first = tab_strip(["Map", "Routes"], 0, width=20).renderables[1]
mid_second = tab_strip(["Map", "Routes"], 1, width=20).renderables[1]
assert mid_first.plain == mid_second.plain == "│ Map │ Routes │"
top_first = tab_strip(["Map", "Routes"], 0, width=20).renderables[0]
assert top_first.plain == "╭───────╮──────────╮" # first tab keeps the plain leading corner
def test_tab_strip_rule_fills_out_to_the_render_width() -> None:
"""The closing rule under the strip extends to the full render width."""
_, _, bot = tab_strip(["Map", "Routes"], 1, width=20).renderables
assert bot.plain == "─────┴──────────┴───" # 17 chars of box/rule, then 3 chars filler
assert len(bot.plain) == 20
_, _, bot = tab_strip(["Map", "Routes"], 1, width=25).renderables
assert bot.plain == "╰───────╰──────────╯─────" # 20 chars of box/rule, then 5 chars filler
assert len(bot.plain) == 25
def test_tab_strip_collapses_a_lone_tab_to_a_plain_heading() -> None: