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
+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 {