Compare commits

...
3 Commits
Author SHA1 Message Date
luxick c0b7ebf0c2 Increase dropdown button on mobile 2026-09-01 20:05:23 +02:00
luxick 07bb3b002e Remove remaining mentions of fabs 2026-09-01 19:55:40 +02:00
luxick 96a58ca472 reorganize header menu 2026-09-01 19:44:08 +02:00
11 changed files with 91 additions and 130 deletions
+11
View File
@@ -56,3 +56,14 @@ document.addEventListener('keydown', function (e) {
menu.classList.remove('is-open'); menu.classList.remove('is-open');
}); });
}); });
document.addEventListener('DOMContentLoaded', function () {
var trigger = document.querySelector('[data-action="actions-menu"]');
if (!trigger) return;
wireDropdown(trigger);
var menu = trigger.parentElement.querySelector('.dropdown-menu');
if (!menu) return;
trigger.addEventListener('click', function () {
trigger.setAttribute('aria-expanded', menu.classList.contains('is-open') ? true : false);
});
});
+11 -1
View File
@@ -29,7 +29,17 @@
<input class="input search-input" type="search" name="q" value="{{block "searchQuery" .}}{{end}}" placeholder="Search…" title="Search (F)" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /> <input class="input search-input" type="search" name="q" value="{{block "searchQuery" .}}{{end}}" placeholder="Search…" title="Search (F)" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
</form> </form>
{{end}} {{end}}
<div class="header-actions row">{{block "headerActions" .}}{{end}}</div> <div class="header-actions row">
{{if .EditMode}}{{block "headerActions" .}}{{end}}{{else}}
<span class="dropdown actions">
<button type="button" class="btn actions-toggle" data-action="actions-menu" aria-haspopup="true" aria-expanded="false" title="Actions">ACTIONS</button>
<div class="dropdown-menu align-right actions-menu">
<div class="actions-special"></div>
{{template "headerActions" . }}
</div>
</span>
{{end}}
</div>
</header> </header>
<div class="shell"> <div class="shell">
{{if not .EditMode}}<aside class="tree-sidebar col"> {{if not .EditMode}}<aside class="tree-sidebar col">
+1 -6
View File
@@ -26,7 +26,7 @@
closeBtn = document.createElement('button'); closeBtn = document.createElement('button');
closeBtn.type = 'button'; closeBtn.type = 'button';
closeBtn.className = 'btn btn-fab overlay-close'; closeBtn.className = 'btn overlay-close';
closeBtn.title = 'Close'; closeBtn.title = 'Close';
closeBtn.setAttribute('aria-label', 'Close'); closeBtn.setAttribute('aria-label', 'Close');
closeBtn.textContent = '×'; closeBtn.textContent = '×';
@@ -35,11 +35,6 @@
bodyEl = document.createElement('div'); bodyEl = document.createElement('div');
bodyEl.className = 'overlay-body'; bodyEl.className = 'overlay-body';
// Any navigation or anchor activation inside a rail dismisses the
// overlay: tree/calendar links navigate away, TOC links scroll the
// page underneath — either way the user should see the result, and
// closing synchronously (before navigation) avoids a bfcache snapshot
// that would restore the overlay open on back-nav.
bodyEl.addEventListener('click', function (e) { bodyEl.addEventListener('click', function (e) {
if (e.target.closest('a')) close(); if (e.target.closest('a')) close();
}); });
+15
View File
@@ -0,0 +1,15 @@
document.addEventListener("DOMContentLoaded", function () {
var aside = document.querySelector("aside.sidebar");
var specials = document.querySelector(".actions-special");
if (!aside || !aside.children.length || !specials) return;
var item = document.createElement("button");
item.type = "button";
item.className = "btn btn-block";
item.title = "Contents";
item.textContent = "CONTENTS";
item.addEventListener("click", function () {
if (typeof openOverlay === "function") openOverlay(aside);
});
specials.appendChild(item);
});
-16
View File
@@ -1,16 +0,0 @@
// Wires the header-right [ACTIONS] dropdown. The buttons inside keep calling
// the existing page/actions.js handlers (newPage / movePage / deletePage) and
// the ?edit link, so the N/E/M keyboard shortcuts continue to work whether or
// not the menu is open. Toggling reuses the generic wireDropdown mechanism
// (global-shortcuts.js), which also handles outside-click / Escape close.
(function () {
var trigger = document.querySelector('[data-action="page-actions"]');
if (!trigger || typeof wireDropdown !== 'function') return;
wireDropdown(trigger);
var menu = trigger.parentElement.querySelector('.dropdown-menu');
if (!menu) return;
// Keep aria-expanded in sync with the menu's open state.
trigger.addEventListener('click', function () {
trigger.setAttribute('aria-expanded', menu.classList.contains('is-open') ? 'true' : 'false');
});
})();
+8 -16
View File
@@ -1,21 +1,13 @@
{{define "headScripts"}}<script src="/_/page/actions.js"></script>{{end}} {{define "headScripts"}}<script src="/_/page/actions.js"></script>{{end}}
{{define "headerActions"}}{{if .CanEdit}} {{define "headerActions"}}{{if .CanEdit}}
<span class="dropdown"> <button class="btn btn-block" onclick="newPage()" title="New page (N)">NEW</button>
<button type="button" class="btn" data-action="page-actions" aria-haspopup="true" aria-expanded="false" title="Page actions">ACTIONS</button> <button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager">REVEAL</button>
<div class="dropdown-menu align-right"> {{if not .IsRoot}}
<button class="btn btn-block" onclick="newPage()" title="New page (N)">NEW PAGE</button> <button class="btn btn-block" onclick="movePage()" title="Move page (M)">MOVE</button>
<a class="btn btn-block" href="?edit" title="Edit page (E)">EDIT PAGE</a> <button class="btn btn-block danger" onclick="deletePage()" title="Delete page">DELETE</button>
<button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager">REVEAL ON CLIENT</button> {{end}}
{{if not .IsRoot}} <a class="btn btn-block" href="?edit" title="Edit page (E)">EDIT</a>
<button class="btn btn-block" onclick="movePage()" title="Move page (M)">MOVE PAGE</button>
{{end}}
{{if not .IsRoot}}
<button class="btn btn-block danger" onclick="deletePage()" title="Delete page">DELETE PAGE</button>
{{end}}
</div>
</span>
<script src="/_/page/header-actions.js"></script>
{{end}}{{end}} {{end}}{{end}}
{{define "content"}} {{define "content"}}
@@ -63,7 +55,7 @@
{{if .Content}} {{if .Content}}
<script src="/_/page/sections.js"></script> <script src="/_/page/sections.js"></script>
{{end}} {{end}}
<script src="/_/page/sidebar-fab.js"></script> <script src="/_/page/contents.js"></script>
{{end}} {{end}}
{{define "aside"}}{{if .SidebarWidget}}<aside class="sidebar">{{.SidebarWidget}}</aside>{{end}}{{end}} {{define "aside"}}{{if .SidebarWidget}}<aside class="sidebar">{{.SidebarWidget}}</aside>{{end}}{{end}}
-15
View File
@@ -1,15 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
var aside = document.querySelector("aside.sidebar");
if (!aside || !aside.children.length) return;
var fab = document.createElement("button");
fab.type = "button";
fab.className = "btn btn-fab fab fab-rail";
fab.title = "Contents";
fab.setAttribute("aria-label", "Contents");
fab.textContent = "≡";
fab.addEventListener("click", function () {
if (typeof openOverlay === "function") openOverlay(aside);
});
document.body.appendChild(fab);
});
-1
View File
@@ -53,7 +53,6 @@ function createSearchPage(name) {
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
wireDropdown(document.querySelector('[data-action="actions-drop"]'));
var createLink = document.querySelector('[data-create-page]'); var createLink = document.querySelector('[data-create-page]');
if (createLink) { if (createLink) {
+4 -9
View File
@@ -42,15 +42,10 @@
{{end}} {{end}}
{{end}} {{end}}
{{define "headerActions"}}
<button class="btn btn-block" onclick="rebuildIndex()" title="Rebuild search index">REBUILD INDEX</button>
{{end}}
{{define "footerExtras"}} {{define "footerExtras"}}
{{if not .IndexBuiltAt.IsZero}}<span class="muted">· Index: {{.IndexBuiltAt.Format "2006-01-02 15:04"}}</span>{{end}} {{if not .IndexBuiltAt.IsZero}}<span class="muted">· Index: {{.IndexBuiltAt.Format "2006-01-02 15:04"}}</span>{{end}}
{{end}} {{end}}
{{define "extras"}}
<div class="fab dropdown">
<button class="btn btn-fab" data-action="actions-drop" title="Actions" aria-label="Actions">≡</button>
<div class="dropdown-menu align-right open-up">
<button class="btn btn-block" onclick="rebuildIndex()" title="Rebuild search index">REBUILD INDEX</button>
</div>
</div>
{{end}}
+27 -41
View File
@@ -217,22 +217,9 @@ footer {
padding: 0.3rem var(--space-3); padding: 0.3rem var(--space-3);
white-space: nowrap; white-space: nowrap;
} }
.btn-fab { .btn[hidden] {display: none;}
background: var(--bg-panel);
border: var(--border);
width: 3rem;
height: 3rem;
font-size: 1.5rem;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-fab::before, .btn-fab::after { content: none; }
.btn-fab:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.danger { color: var(--danger); } .danger { color: var(--danger); }
.danger:hover { color: var(--danger-hover); } .danger:hover { color: var(--danger-hover); }
/* Selected segmented-toggle button (view-settings modal). */
.btn.is-active { color: var(--primary-hover); } .btn.is-active { color: var(--primary-hover); }
/* === Form controls === /* === Form controls ===
@@ -403,6 +390,8 @@ main > h2 {
.dropdown-menu.is-open { display: block; } .dropdown-menu.is-open { display: block; }
.dropdown-menu.scrollable { max-height: 23rem; overflow-y: auto; } .dropdown-menu.scrollable { max-height: 23rem; overflow-y: auto; }
.actions-special > .btn {text-align: left;}
/* === Suggestion dropdown (header search + editor link picker) === /* === Suggestion dropdown (header search + editor link picker) ===
Anchored to a position:relative host. Mirrors .dropdown-menu visuals with Anchored to a position:relative host. Mirrors .dropdown-menu visuals with
a dashed border and bg-panel-hover for the active row. */ a dashed border and bg-panel-hover for the active row. */
@@ -501,15 +490,6 @@ body.edit-mode footer { max-width: none; }
border-bottom: var(--border-dashed); border-bottom: var(--border-dashed);
} }
/* === Floating action button ===
Standalone FAB buttons (the tree rail, the right rail) are mobile-only and
stack bottom-right: the tree FAB anchors the bottom, the right-rail FAB sits
above it. Wrapped FABs (the search actions dropdown) stay visible on desktop
and, on mobile, sit in the upper slot so they never overlap the tree FAB. */
.fab { position: fixed; bottom: var(--space-4); right: var(--space-4); z-index: 50; }
button.fab { display: none; }
.fab-rail { bottom: calc(var(--space-4) + 3rem + var(--space-2)); }
/* === Companion status === */ /* === Companion status === */
.companion-status { margin-left: auto; } .companion-status { margin-left: auto; }
.companion-icon { font-size: 0.9rem; line-height: 1; padding: 0.1rem 0.3rem; } .companion-icon { font-size: 0.9rem; line-height: 1; padding: 0.1rem 0.3rem; }
@@ -691,7 +671,13 @@ aside.sidebar:empty { display: none; }
border-bottom: var(--border-dashed); border-bottom: var(--border-dashed);
flex-shrink: 0; flex-shrink: 0;
} }
.overlay-close { width: 2.5rem; height: 2.5rem; font-size: 1.5rem; } .overlay-close {
width: 2.5rem;
height: 2.5rem;
font-size: 1.5rem;
}
.overlay-close::before, .overlay-close::after { content: none; }
.overlay-close:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.overlay-body { .overlay-body {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -755,12 +741,7 @@ aside.sidebar:empty { display: none; }
} }
.tree-row.is-active > .tree-name { color: var(--primary-hover); } .tree-row.is-active > .tree-name { color: var(--primary-hover); }
/* === Tree sidebar (persistent left navigation rail) === /* Tree sidebar (persistent navigation) */
Reuses the .tree-row / .tree-children / .tree-name / .tree-chevron modules.
Desktop: a full-height flex column in the app-shell (composes with .col)
holding the tree's own scroll region. Mobile: not laid out inline — its
content is surfaced through the Overlay via the stacked FABs (see
responsive). */
.tree-sidebar { .tree-sidebar {
width: var(--tree-width); width: var(--tree-width);
flex-shrink: 0; flex-shrink: 0;
@@ -851,20 +832,25 @@ aside.sidebar:empty { display: none; }
/* === Responsive === */ /* === Responsive === */
@media (max-width: 1100px) { @media (max-width: 1100px) {
/* Rails are not laid out inline on mobile — their content is surfaced
through the Overlay via the stacked FABs. The <aside> elements stay in
the DOM (their JS renders into them and the Overlay moves them in/out);
we only pull them out of the shell flow here. */
.tree-sidebar, .sidebar { display: none; } .tree-sidebar, .sidebar { display: none; }
/* Compact three-column header: logo left, search stretches the middle,
actions right. */
header { grid-template-columns: auto 1fr auto; } header { grid-template-columns: auto 1fr auto; }
.search-form { width: 100%; max-width: none; justify-self: stretch; } .search-form { width: 100%; max-width: none; justify-self: stretch; }
/* Reveal the mobile FAB stack (tree at the bottom, right rail above it). .actions-menu .btn-block {
The search actions dropdown FAB moves to the upper slot too so it never min-height: 2.75rem;
overlaps the tree FAB. */ padding: 0.8rem var(--space-4);
button.fab { display: inline-flex; } border-top: var(--border-dashed);
.fab.dropdown { bottom: calc(var(--space-4) + 3rem + var(--space-2)); } }
.actions-menu > .btn-block:first-child,
.actions-special > .btn-block:first-child { border-top: none; }
.actions-menu .btn-block:hover,
.actions-menu .btn-block:active { background: var(--bg-panel-hover); }
}
/* Show the actions menu items directly in the bar in desktop size */
@media (min-width: 1101px) {
.actions-toggle, .actions-special {display: none;}
.actions, .actions-menu, .actions.dropdown-menu.is-open {display: contents;}
.actions-menu .btn-block { width: auto; padding: 0;}
} }
@media (max-width: 600px) { @media (max-width: 600px) {
+14 -25
View File
@@ -1,11 +1,3 @@
// Persistent left navigation rail. Renders the wiki folder/file tree, expanded
// to the current page's ancestor chain, and lets the user navigate by clicking
// folders (links) or open files locally via the companion (a.tree-file, wired
// in companion.js). Shares the .tree-* CSS and the ?tree endpoint with
// tree-picker.js but does not reuse its row builder — that one is modal-select
// behavior, this one is navigation behavior. Hidden in edit mode (the server
// omits the container).
(function () { (function () {
// This script owns the .tree-scroll child of the aside, not the aside itself. // This script owns the .tree-scroll child of the aside, not the aside itself.
var container = document.querySelector('aside.tree-sidebar .tree-scroll'); var container = document.querySelector('aside.tree-sidebar .tree-scroll');
@@ -188,24 +180,21 @@
}); });
} }
// Mobile: the tree FAB sits at the bottom of the stacked FAB group and function setupMenuItem() {
// opens the tree's scroll container in the full-viewport Overlay var specials = document.querySelector('.actions-special');
// (overlay.js). The container always exists when not editing, so — per the if (!specials) return;
// layout spec — the FAB always renders; the overlay auto-closes when a
// folder/file link inside it navigates. Hidden on desktop by .fab CSS. var item = document.createElement("button");
function setupFab() { item.type = "button";
var fab = document.createElement('button'); item.className = "btn btn-block";
fab.type = 'button'; item.title = "Folders";
fab.className = 'btn btn-fab fab fab-tree'; item.textContent = "FOLDERS";
fab.title = 'Folder tree'; item.addEventListener("click", function () {
fab.setAttribute('aria-label', 'Folder tree'); if (typeof openOverlay === "function") openOverlay(container);
fab.innerHTML = '<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter"><path d="M1 6h14v8H1zm0 0V4h5l1 2"/></svg>'; });
fab.addEventListener('click', function () { specials.appendChild(item);
if (typeof openOverlay === 'function') openOverlay(container);
});
document.body.appendChild(fab);
} }
render(); render();
setupFab(); setupMenuItem();
})(); })();