diff --git a/examples/app_hub/assets/hub-landing.js b/examples/app_hub/assets/hub-landing.js
index 912b2a1..70dfa77 100644
--- a/examples/app_hub/assets/hub-landing.js
+++ b/examples/app_hub/assets/hub-landing.js
@@ -116,13 +116,7 @@
}
function bindLeftNav() {
- var pane = document.getElementById('landing-nav-pane');
- var handle = document.getElementById('landing-nav-handle');
- if (handle && pane) {
- handle.addEventListener('click', function () {
- pane.classList.toggle('open');
- });
- }
+ /* nav_shell.js handles #landing-nav-pane / #landing-nav-handle */
}
function initFromUrl() {
diff --git a/examples/app_hub/assets/i18n/hub-en.json b/examples/app_hub/assets/i18n/hub-en.json
index 0e961f1..2a68924 100644
--- a/examples/app_hub/assets/i18n/hub-en.json
+++ b/examples/app_hub/assets/i18n/hub-en.json
@@ -16,6 +16,14 @@
"nav.graphs": "Graphs",
"nav.remote": "Remote access",
"nav.short_links": "Short links",
+ "nav.short_links_desc": "Mint bearer tokens and shorten URLs for s.f0xx.org.",
+ "nav.console_desc": "Crash triage home and workspace cards.",
+ "nav.git_desc": "Browse repos, branches, and tags.",
+ "nav.issues_desc": "Browse and triage crash reports.",
+ "nav.tickets_desc": "Roadmap tasks, QA items, and tags.",
+ "nav.graphs_desc": "Sessions, crashes, and device activity.",
+ "nav.remote_desc": "WireGuard sessions and device reachability.",
+ "nav.builder_desc": "Docker CI for APK baking and OTA artifacts.",
"nav.builder": "Builder",
"nav.toggle": "Navigation",
"nav.security": "Security",
diff --git a/examples/app_hub/assets/i18n/hub-ru.json b/examples/app_hub/assets/i18n/hub-ru.json
index b7a7429..21492a1 100644
--- a/examples/app_hub/assets/i18n/hub-ru.json
+++ b/examples/app_hub/assets/i18n/hub-ru.json
@@ -16,6 +16,14 @@
"nav.graphs": "Графики",
"nav.remote": "Удалённый доступ",
"nav.short_links": "Короткие ссылки",
+ "nav.short_links_desc": "Токены и сокращение URL для s.f0xx.org.",
+ "nav.console_desc": "Главная консоли и карточки разделов.",
+ "nav.git_desc": "Репозитории, ветки и теги.",
+ "nav.issues_desc": "Просмотр и разбор отчётов о сбоях.",
+ "nav.tickets_desc": "Задачи roadmap, QA и теги.",
+ "nav.graphs_desc": "Сессии, сбои и активность устройств.",
+ "nav.remote_desc": "Сессии WireGuard и доступ к устройствам.",
+ "nav.builder_desc": "Docker CI для сборки APK и OTA.",
"nav.builder": "Сборщик",
"nav.toggle": "Навигация",
"nav.security": "Безопасность",
diff --git a/examples/app_hub/hub-landing.css b/examples/app_hub/hub-landing.css
index d4c264e..c08f253 100644
--- a/examples/app_hub/hub-landing.css
+++ b/examples/app_hub/hub-landing.css
@@ -5,12 +5,19 @@
--landing-footer-h: 40px;
--landing-left-w: 56px;
--landing-rail-w: 44px;
- overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ min-height: 100dvh;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: clip;
+ overflow-y: auto;
}
/* (1) Top header */
.landing-top {
- position: fixed;
+ position: sticky;
top: 0;
left: 0;
right: 0;
@@ -145,11 +152,11 @@
/* Frame: (2) left + (3) body + space for (4) rail */
.landing-frame {
- position: fixed;
- top: var(--landing-header-h);
- left: 0;
- right: 0;
- bottom: var(--landing-footer-h);
+ position: relative;
+ flex: 1 1 auto;
+ width: 100%;
+ max-width: 100%;
+ min-height: calc(100dvh - var(--landing-header-h) - var(--landing-footer-h));
display: flex;
z-index: 5;
pointer-events: none;
@@ -162,8 +169,9 @@
flex-direction: column;
flex: 0 0 var(--landing-left-w);
width: var(--landing-left-w);
- height: 100%;
- max-height: 100%;
+ align-self: stretch;
+ min-height: 100%;
+ max-height: none;
overflow: hidden;
z-index: 3;
pointer-events: auto;
@@ -180,15 +188,16 @@
border-top: 1px solid var(--border);
}
.landing-left.nav-pane.open {
- width: 200px;
- flex-basis: 200px;
+ width: 240px;
+ flex-basis: 240px;
}
/* (3) Body — full viewport pages, scroll snap */
.landing-scroll {
flex: 1;
min-width: 0;
- height: 100%;
+ align-self: stretch;
+ min-height: 100%;
overflow-y: auto;
overflow-x: hidden;
scroll-snap-type: y mandatory;
diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php
index 53e281f..146ebee 100644
--- a/examples/app_hub/index.php
+++ b/examples/app_hub/index.php
@@ -38,7 +38,7 @@ function hub_h(string $s): string {
-
+
+
@@ -137,14 +138,14 @@ function hub_h(string $s): string {
diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css
index 24f14df..da41db8 100644
--- a/examples/crash_reporter/backend/public/assets/css/app.css
+++ b/examples/crash_reporter/backend/public/assets/css/app.css
@@ -27,6 +27,11 @@
--row-open: rgba(37, 99, 235, .06);
}
* { box-sizing: border-box; }
+html {
+ width: 100%;
+ max-width: 100%;
+ overflow-x: clip;
+}
[hidden] {
display: none !important;
pointer-events: none !important;
@@ -47,23 +52,28 @@ body {
background: var(--bg);
color: var(--text);
min-height: 100vh;
+ min-height: 100dvh;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: clip;
}
body:has(.shell) {
display: block;
- overflow: hidden;
- height: 100vh;
- height: 100dvh;
+ overflow-x: clip;
+ overflow-y: auto;
+ height: auto;
+ min-height: 100vh;
+ min-height: 100dvh;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell {
display: flex;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: var(--footer-h, 40px);
- min-height: 0;
+ position: relative;
+ width: 100%;
+ max-width: 100%;
+ min-height: calc(100dvh - var(--footer-h, 40px));
+ min-height: calc(100vh - var(--footer-h, 40px));
overflow: hidden;
z-index: 1;
}
@@ -93,7 +103,7 @@ a:hover { text-decoration: underline; }
display: flex;
flex-direction: column;
}
-.nav-pane.open { width: 200px; }
+.nav-pane.open { width: 240px; }
.nav-handle {
position: absolute;
top: 8px;
@@ -147,17 +157,40 @@ a:hover { text-decoration: underline; }
}
.nav-pane.open .nav-link {
width: 100%;
+ height: auto;
+ min-height: 40px;
margin: 0;
- padding: 0 12px;
+ padding: 8px 12px;
justify-content: flex-start;
+ align-items: flex-start;
gap: 10px;
}
+.nav-pane.open .nav-link > .nav-icon {
+ flex-shrink: 0;
+ margin-top: 2px;
+}
.nav-link.active,
.nav-link:hover {
background: rgba(61, 139, 253, .22);
color: var(--accent);
text-decoration: none;
}
+.nav-text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+ flex: 1 1 auto;
+ opacity: 0;
+ max-width: 0;
+ overflow: hidden;
+ transition: opacity .18s ease, max-width .22s ease;
+}
+.nav-pane.open .nav-text {
+ opacity: 1;
+ max-width: 11rem;
+ transition-delay: .06s;
+}
.nav-label {
flex: 0 1 auto;
min-width: 0;
@@ -166,23 +199,50 @@ a:hover { text-decoration: underline; }
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
+ text-overflow: ellipsis;
+}
+.nav-desc {
+ display: none;
+ font-size: 11px;
+ font-weight: 400;
+ line-height: 1.25;
+ color: var(--muted);
+ white-space: normal;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+.nav-pane.open .nav-desc {
+ display: -webkit-box;
+}
+.nav-link > .nav-label {
opacity: 0;
max-width: 0;
transition: opacity .18s ease, max-width .22s ease;
}
-.nav-pane.open .nav-label {
+.nav-pane.open .nav-link > .nav-label {
opacity: 1;
max-width: 9rem;
transition-delay: .06s;
}
+.nav-text .nav-label {
+ opacity: 1;
+ max-width: none;
+ transition: none;
+}
+.nav-handle.is-dragging {
+ cursor: grabbing;
+}
@media (prefers-reduced-motion: reduce) {
.nav-pane,
.nav-link,
.nav-label,
+ .nav-text,
.locale-code {
transition: none;
}
- .nav-pane.open .nav-label,
+ .nav-pane.open .nav-link > .nav-label,
+ .nav-pane.open .nav-text,
.nav-pane.open .locale-code {
transition-delay: 0s;
}
diff --git a/examples/crash_reporter/backend/public/assets/i18n/en.json b/examples/crash_reporter/backend/public/assets/i18n/en.json
index 72f590d..2685063 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/en.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/en.json
@@ -5,6 +5,17 @@
"nav.tickets": "Tickets",
"nav.logout": "Logout",
"nav.toggle": "Navigation",
+ "nav.home_desc": "Console home and workspace cards.",
+ "nav.reports_desc": "Browse and triage crash reports.",
+ "nav.tickets_desc": "Roadmap tasks, QA items, and tags.",
+ "nav.graphs": "Graphs",
+ "nav.graphs_desc": "Sessions, crashes, and device activity.",
+ "nav.remote": "Remote access",
+ "nav.remote_desc": "WireGuard sessions and device reachability.",
+ "nav.access": "Access",
+ "nav.access_desc": "Roles, privilege sets, and operators.",
+ "nav.builder": "Builder",
+ "nav.builder_desc": "Docker CI for APK baking and OTA artifacts.",
"theme.label": "Theme",
"theme.dark": "Dark",
"theme.light": "Light",
diff --git a/examples/crash_reporter/backend/public/assets/i18n/ru.json b/examples/crash_reporter/backend/public/assets/i18n/ru.json
index 4c22080..eda8815 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/ru.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/ru.json
@@ -15,6 +15,17 @@
"ticket.detail_title": "Задача",
"nav.logout": "Выход",
"nav.toggle": "Навигация",
+ "nav.home_desc": "Главная консоли и карточки разделов.",
+ "nav.reports_desc": "Просмотр и разбор отчётов о сбоях.",
+ "nav.tickets_desc": "Задачи roadmap, QA и теги.",
+ "nav.graphs": "Графики",
+ "nav.graphs_desc": "Сессии, сбои и активность устройств.",
+ "nav.remote": "Удалённый доступ",
+ "nav.remote_desc": "Сессии WireGuard и доступ к устройствам.",
+ "nav.access": "Доступ",
+ "nav.access_desc": "Роли, наборы привилегий и операторы.",
+ "nav.builder": "Сборщик",
+ "nav.builder_desc": "Docker CI для сборки APK и OTA.",
"theme.label": "Тема",
"theme.dark": "Тёмная",
"theme.light": "Светлая",
diff --git a/examples/crash_reporter/backend/public/assets/js/app.js b/examples/crash_reporter/backend/public/assets/js/app.js
index 8d606f8..9ce9c3e 100644
--- a/examples/crash_reporter/backend/public/assets/js/app.js
+++ b/examples/crash_reporter/backend/public/assets/js/app.js
@@ -119,14 +119,7 @@
}
function initNav() {
- const nav = document.getElementById('nav-pane');
- const handle = document.getElementById('nav-handle');
- if (!nav || !handle) return;
- handle.addEventListener('click', () => {
- const open = nav.classList.toggle('open');
- handle.setAttribute('aria-expanded', open ? 'true' : 'false');
- });
- handle.setAttribute('aria-expanded', nav.classList.contains('open') ? 'true' : 'false');
+ /* nav_shell.js handles toggle + drag on #nav-pane / #nav-handle */
}
function markReportViewed(id) {
diff --git a/examples/crash_reporter/backend/public/assets/js/nav_shell.js b/examples/crash_reporter/backend/public/assets/js/nav_shell.js
new file mode 100644
index 0000000..895d39f
--- /dev/null
+++ b/examples/crash_reporter/backend/public/assets/js/nav_shell.js
@@ -0,0 +1,83 @@
+/**
+ * Collapsible left nav: click toggle + horizontal drag on handle (open on drag right).
+ */
+(function () {
+ 'use strict';
+
+ var DRAG_THRESHOLD_PX = 20;
+
+ function setOpen(nav, handle, open) {
+ nav.classList.toggle('open', open);
+ if (handle) {
+ handle.setAttribute('aria-expanded', open ? 'true' : 'false');
+ }
+ }
+
+ function bindNavShell(paneId, handleId) {
+ var nav = document.getElementById(paneId);
+ var handle = document.getElementById(handleId);
+ if (!nav || !handle) return;
+
+ setOpen(nav, handle, nav.classList.contains('open'));
+
+ var drag = null;
+ var suppressClick = false;
+
+ handle.addEventListener('click', function (ev) {
+ if (suppressClick) {
+ suppressClick = false;
+ ev.preventDefault();
+ return;
+ }
+ setOpen(nav, handle, !nav.classList.contains('open'));
+ });
+
+ handle.addEventListener('pointerdown', function (ev) {
+ if (ev.button !== 0) return;
+ drag = { x: ev.clientX, moved: false };
+ handle.setPointerCapture(ev.pointerId);
+ handle.classList.add('is-dragging');
+ ev.preventDefault();
+ });
+
+ handle.addEventListener('pointermove', function (ev) {
+ if (!drag || !handle.hasPointerCapture(ev.pointerId)) return;
+ var dx = ev.clientX - drag.x;
+ if (Math.abs(dx) < 4) return;
+ drag.moved = true;
+ if (dx > DRAG_THRESHOLD_PX) {
+ setOpen(nav, handle, true);
+ drag.x = ev.clientX;
+ } else if (dx < -DRAG_THRESHOLD_PX) {
+ setOpen(nav, handle, false);
+ drag.x = ev.clientX;
+ }
+ });
+
+ function endDrag(ev) {
+ if (!drag) return;
+ if (drag.moved) suppressClick = true;
+ drag = null;
+ handle.classList.remove('is-dragging');
+ if (handle.hasPointerCapture(ev.pointerId)) {
+ handle.releasePointerCapture(ev.pointerId);
+ }
+ }
+
+ handle.addEventListener('pointerup', endDrag);
+ handle.addEventListener('pointercancel', endDrag);
+ }
+
+ window.initNavShell = bindNavShell;
+
+ function autoInit() {
+ bindNavShell('nav-pane', 'nav-handle');
+ bindNavShell('landing-nav-pane', 'landing-nav-handle');
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', autoInit);
+ } else {
+ autoInit();
+ }
+})();
diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php
index 9041f0e..70bb251 100644
--- a/examples/crash_reporter/backend/views/layout.php
+++ b/examples/crash_reporter/backend/views/layout.php
@@ -26,6 +26,7 @@
})();
+
@@ -70,7 +71,10 @@
aria-label="Home"
title="Home">
- Home
+
+ Home
+ Console home and workspace cards.
+
@@ -80,7 +84,10 @@
aria-label="Reports"
title="Reports">
- Reports
+
+ Reports
+ Browse and triage crash reports.
+
@@ -90,7 +97,10 @@
aria-label="Tickets"
title="Tickets">
- Tickets
+
+ Tickets
+ Roadmap tasks, QA items, and tags.
+
@@ -99,7 +109,10 @@
aria-label="Graphs"
title="Graphs">
- Graphs
+
+ Graphs
+ Sessions, crashes, and device activity.
+
@@ -109,7 +122,10 @@
aria-label="Remote access"
title="Remote access">
- Remote access
+
+ Remote access
+ WireGuard sessions and device reachability.
+
@@ -120,7 +136,10 @@
aria-label="Short links"
title="Short links">
- Short links
+
+ Short links
+ Mint bearer tokens and shorten URLs for s.f0xx.org.
+
@@ -131,7 +150,10 @@
aria-label="Access control"
title="Access control">
- Access
+
+ Access
+ Roles, privilege sets, and operators.
+
@@ -141,7 +163,10 @@
aria-label="Builder"
title="Builder">
- Builder
+
+ Builder
+ Docker CI for APK baking and OTA artifacts.
+