v0.146.0 — nav polish: styled scrollbars + collapsible sidebar groups

UI-only; no behavioural or backup/restore surface touched. Green: build, vet,
tests all pass; template_id_gate, emoji_gate, native_confirm_gate,
offbox_rename_gate, mojibake_gate, app_row_dedup_gate all PASS.

Scrollbars (style.css): the platform default reads as a bright chunky stripe
against the navy and competes with the content it scrolls. Now thin and
hairline-coloured — scrollbar-width/scrollbar-color for Firefox AND
::-webkit-scrollbar (8px, thumb --line, hover --text-3, --radius) for
WebKit/Blink, because neither alone covers the browsers customers use. The two
surfaces that actually scroll take their own panel background as the track
(.sidebar -> --bg-2, html -> --bg-0) so the gutter never shows as a lighter
channel. Tokens only, no raw hexes.

Collapsible nav groups (layout.html + style.css, vanilla JS, no framework):
Tárhely, Biztonsági mentés and Megosztás become accordions with a chevron;
exactly one open at a time, clicking the open one closes it. Groups without
sub-items are untouched plain links. Hungarian labels unchanged.

 - The header is a real <button>, so keyboard and AT reachability come for free
   instead of being simulated with tabindex/role on a div. aria-expanded +
   aria-controls + a :focus-visible outline.
 - Nothing became unreachable when the header stopped being a link: every
   group's landing page is ALSO its first sub-item (/storage -> Meghajtók,
   /backups -> Áttekintés, /sharing -> Hálózati megosztás). Checked before the
   conversion, not assumed.
 - Progressive enhancement: the group containing the active page is rendered
   open SERVER-SIDE (.is-open), so the right group is open before any JS runs
   and stays open if JS never does. The listener only handles clicks.
 - No layout jump: collapse animates grid-template-rows 0fr -> 1fr (with
   min-height:0 + overflow:hidden on the sub-list) rather than max-height, so
   it animates to the content's REAL height and there is no magic number to
   drift when a group gains or loses an item — the specific way a max-height
   accordion rots. The toggle reserves its 3px active border as transparent so
   becoming active adds no width shift. .18s transitions, and both the collapse
   and the chevron rotation are disabled under prefers-reduced-motion.

Pre-existing and deliberately NOT bundled: docker_run_volume_path_gate.py still
fails on internal/appexport/estimate.go:179. That is ROADMAP R-29, unrelated to
this change, verified to fail identically on the untouched tree, and R-29 itself
says not to bundle it into an unrelated feature commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
2026-07-18 20:50:46 +02:00
parent 57be785bad
commit 24d23b80bd
4 changed files with 174 additions and 6 deletions
+41
View File
@@ -1,5 +1,46 @@
## Changelog
### v0.146.0 — nav polish: styled scrollbars + collapsible sidebar groups (2026-07-18)
UI-only; no behavioural or backup/restore surface touched. Green:
`go build ./... && go vet ./... && go test ./...` all pass; `template_id_gate` + `emoji_gate` +
`native_confirm_gate` + `offbox_rename_gate` + `mojibake_gate` + `app_row_dedup_gate` all PASS.
- **Scrollbars (`style.css`).** The platform default is a light, chunky bar that reads as a bright
stripe against the navy and competes with the content it is scrolling. Now thin and hairline
coloured: `scrollbar-width: thin` + `scrollbar-color` for Firefox, `::-webkit-scrollbar` (8px,
thumb `--line`, hover `--text-3`, `--radius`) for WebKit/Blink — **both** declared, because
neither alone covers the browsers customers actually use. The two surfaces that really scroll take
their own panel background as the track (`.sidebar``--bg-2`, `html``--bg-0`) so the gutter
never shows through as a lighter channel. Tokens only, no raw hexes.
- **Collapsible nav groups (`layout.html` + `style.css`, vanilla JS — no framework).** Tárhely,
Biztonsági mentés and Megosztás are now accordions with a chevron indicator; **exactly one is open
at a time**, and clicking the open one closes it. Groups without sub-items (Vezérlőpult,
Alkalmazások, Rendszermonitor, Debug) are untouched plain links. Hungarian labels unchanged.
- **The header is a real `<button>`**, so keyboard and assistive-tech reachability come for free
rather than being simulated with `tabindex`/`role` on a div. It carries `aria-expanded` +
`aria-controls`, and a `:focus-visible` outline.
- **Nothing became unreachable when the header stopped being a link:** every group's own landing
page is *also* its first sub-item (`/storage` → Meghajtók, `/backups` → Áttekintés, `/sharing`
Hálózati megosztás). This was checked before the conversion, not assumed.
- **Progressive enhancement:** the group containing the active page is rendered open
**server-side** (`.is-open`), so the correct group is open before any JS executes and stays open
if JS never runs. The listener only handles clicks.
- **No layout jump:** the collapse animates `grid-template-rows: 0fr → 1fr` (with `min-height: 0`
+ `overflow: hidden` on the sub-list) rather than `max-height`. That animates to the content's
REAL height, so there is no magic number to drift when a group gains or loses an item — the
specific way a `max-height` accordion rots. The toggle reserves its 3px active border as
`transparent` so becoming active adds no width shift. Transitions are .18s and both the collapse
and the chevron rotation are disabled under `prefers-reduced-motion: reduce`.
**Note (unchanged, pre-existing):** `docker_run_volume_path_gate.py` still fails on
`internal/appexport/estimate.go:179`. That is ROADMAP **R-29**, it is unrelated to this change, and
it was verified to fail identically on the untouched tree — deliberately not bundled here, per
R-29's own "do not bundle (a) into an unrelated feature commit".
### v0.145.0 — R-7b: share data enters the live backup runs (Model B) + samba liveness (2026-07-18)
**The „Felhőmentés" toggle on the Megosztás page is now true.** Before this release a customer could
+12
View File
@@ -413,6 +413,18 @@ Per-app export creates a self-contained `.fab` file (tar.gz, optionally encrypte
The backup system implements a **3-2-1 backup architecture**. Each tier is a **complete,
self-sufficient backup** — any single tier can fully restore an app.
**Sidebar behaviour (v0.146.0).** Groups that own sub-pages — Tárhely, Biztonsági mentés,
Megosztás — render as **accordions**: the header is a real `<button class="nav-group-toggle">`
(keyboard- and AT-reachable for free) carrying a chevron, and **exactly one group is open at a
time**. The group containing the active page is rendered open **server-side** (`.is-open` in
`layout.html`), so the right group is already open before any JS runs and stays open if JS never
does; the vanilla listener in `layout.html` only handles clicks. Groups without sub-items
(Vezérlőpult, Alkalmazások, Rendszermonitor, Debug) are plain links, unchanged. Making the header a
button cost no reachability because **every group's own landing page is also its first sub-item**
(`/storage` → Meghajtók, `/backups` → Áttekintés, `/sharing` → Hálózati megosztás). Collapse uses
`grid-template-rows: 0fr → 1fr` rather than `max-height`, so it animates to the content's real
height with no magic number to drift as item counts change.
**Page map (v0.124.0 IA split — sidebar children under Biztonsági mentés):**
| Route | Page | Sections |
+29 -6
View File
@@ -40,6 +40,23 @@
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
/* Collapsible sidebar groups (v0.146.0). Progressive enhancement: the server already
renders the group containing the active page with .is-open, so the correct group is
open before this runs and stays open if JS never does. Exactly one group open at a
time; clicking the open one closes it. */
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
@@ -53,22 +70,28 @@
<ul class="nav-links">
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/storage" class="{{if or (eq .Page "storage") (eq .Page "storage-network")}}active{{end}}"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely</a>
<ul class="nav-links nav-links-sub nav-links-nested">
{{$storageOpen := or (eq .Page "storage") (eq .Page "storage-network")}}
<li class="nav-group{{if $storageOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $storageOpen}} active{{end}}" aria-expanded="{{if $storageOpen}}true{{else}}false{{end}}" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}">Meghajtók</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">Hálózati tárhely</a></li>
</ul>
</li>
<li><a href="/backups" class="{{if or (eq .Page "backups") (eq .Page "backups-remote") (eq .Page "backups-apps") (eq .Page "backups-restore")}}active{{end}}"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a>
<ul class="nav-links nav-links-sub nav-links-nested">
{{$backupsOpen := or (eq .Page "backups") (eq .Page "backups-remote") (eq .Page "backups-apps") (eq .Page "backups-restore")}}
<li class="nav-group{{if $backupsOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $backupsOpen}} active{{end}}" aria-expanded="{{if $backupsOpen}}true{{else}}false{{end}}" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Áttekintés</a></li>
<li><a href="/backups/remote" class="{{if eq .Page "backups-remote"}}active{{end}}">Távoli mentés</a></li>
<li><a href="/backups/apps" class="{{if eq .Page "backups-apps"}}active{{end}}">Alkalmazások</a></li>
<li><a href="/backups/restore" class="{{if eq .Page "backups-restore"}}active{{end}}">Visszaállítás</a></li>
</ul>
</li>
<li><a href="/sharing" class="{{if eq .Page "sharing"}}active{{end}}"><svg class="ico"><use href="#i-share"/></svg>Megosztás</a>
<ul class="nav-links nav-links-sub nav-links-nested">
{{$sharingOpen := eq .Page "sharing"}}
<li class="nav-group{{if $sharingOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $sharingOpen}} active{{end}}" aria-expanded="{{if $sharingOpen}}true{{else}}false{{end}}" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="{{if eq .Page "sharing"}}active{{end}}">Hálózati megosztás</a></li>
</ul>
</li>
@@ -3309,6 +3309,98 @@ span.mono, .mono { font-family: var(--font-data); }
border-left: 3px solid var(--blue);
}
/* ============================================================================
Nav polish (v0.146.0) scrollbars + collapsible sidebar groups.
============================================================================ */
/* Scrollbars. The platform default is a light, chunky bar that reads as a bright
stripe against the navy and competes with the content it is scrolling. Thin +
hairline-coloured puts it back in the background where it belongs. Firefox gets
the standard two-value form; WebKit/Blink the pseudo-elements. Both are declared
because neither alone covers the browsers customers actually use. */
* {
scrollbar-width: thin;
scrollbar-color: var(--line) transparent;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
background: var(--line);
border-radius: var(--radius);
}
*::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
/* Track takes the surrounding panel's own background, so the gutter never shows
through as a lighter channel on the two surfaces that actually scroll. */
.sidebar { scrollbar-color: var(--line) var(--bg-2); }
.sidebar::-webkit-scrollbar-track { background: var(--bg-2); }
html { scrollbar-color: var(--line) var(--bg-0); }
html::-webkit-scrollbar-track { background: var(--bg-0); }
/* Collapsible nav groups. The row is a real <button> (keyboard + AT reachable for
free); every group's own landing page is also its first sub-item, so nothing
became unreachable when the header stopped being a link.
The open/close uses grid-template-rows 0fr -> 1fr rather than max-height: it
animates to the content's REAL height, so there is no magic number to drift and
no jump when a group's item count changes. */
.nav-group {
display: grid;
grid-template-rows: auto 0fr;
transition: grid-template-rows .18s ease;
}
.nav-group.is-open { grid-template-rows: auto 1fr; }
.nav-group > .nav-links-nested {
overflow: hidden;
min-height: 0; /* required, or the grid row refuses to collapse below content height */
}
@media (prefers-reduced-motion: reduce) {
.nav-group { transition: none; }
}
/* The toggle inherits the nav link's look exactly it must not read as a form
control sitting in a list of links. */
.nav-group-toggle {
display: flex;
align-items: center;
gap: .6rem;
width: 100%;
padding: .75rem 1.5rem;
background: none;
border: none;
border-left: 3px solid transparent; /* reserved so .active adds no width shift */
color: var(--text-2);
font-family: inherit;
font-size: .95rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: color 0.2s ease, background 0.2s ease;
}
.nav-group-toggle:hover {
color: var(--blue-bright);
background: var(--blue-dim);
}
.nav-group-toggle.active {
color: var(--blue-bright);
background: var(--blue-dim);
border-left-color: var(--blue);
}
.nav-group-toggle:focus-visible {
outline: 2px solid var(--blue-bright);
outline-offset: -2px;
}
.nav-chevron {
margin-left: auto;
width: 14px;
height: 14px;
color: var(--text-3);
transition: transform .18s ease;
}
.nav-group.is-open .nav-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
.nav-chevron { transition: none; }
}
/* Unified drive view enrichment slot breathing room between the agent tag row,
the agent action row, and the registry action row below. */
.drive-agent-extra {