feat: admin panel — right-aligned sidebar navigation with two-column … (#52)

* feat: admin panel — right-aligned sidebar navigation with two-column modals

Replace the horizontal tab bar (11 tabs, overflowing on standard monitors)
with a grouped sidebar on the right side, matching the admin button's
position in the header for natural spatial flow.

Sidebar: 5 groups (Identity, Automation, Governance, Observe, System) with
12 nav items including new Settings stub. Always visible on desktop (180px),
off-canvas drawer on mobile (<700px) sliding from right with backdrop.

Admin button: toggle behavior (click again to return to overview), active
state with amber highlight + top accent line, aria-expanded management.

Breadcrumb: shows active tab ("Admin / Users", "Admin / Audit", etc).

Modals: WS Template and Schedule create/edit forms restructured into
two-column grid (820px) with "Identity"/"Model Config" and
"Schedule"/"Execution" column headings. All modals gain max-height: 85vh
+ overflow-y: auto safety net. Modal z-index bumped to 600 (above sidebar).

Also: "Tokens" renamed to "API Tokens", redundant "Server default"
placeholders removed from model config fields, view fade-in transition,
comprehensive ARIA (grouped sidebar, aria-hidden on mobile, focus return
on drawer close), reduced-motion support.

* fix: address Copilot review — aria-orientation, settings permission gate, inert sidebar

- Add aria-orientation="vertical" to sidebar tablist for assistive tech
- Gate Settings tab behind admin.users permission so empty-state logic
  works correctly when user has no admin permissions
- Use inert attribute on mobile sidebar when closed to prevent keyboard
  focus from reaching off-canvas controls
- Add resize listener to sync aria-hidden/inert when crossing the
  700px mobile breakpoint
This commit is contained in:
Patrick Buckley
2026-03-13 19:50:35 -07:00
committed by GitHub
parent ccd1c1a9ad
commit 73cacc8ad6
4 changed files with 633 additions and 234 deletions
+186 -37
View File
@@ -13,13 +13,25 @@ var _cfTrapHandler = null;
var _adminWatches = [];
var _confirmCallbackFn = null;
var _confirmTriggerEl = null;
var _mobileSidebarOpen = false;
// ---------------------------------------------------------------------------
// View switching (called from app.js showOverview/drillDown pattern)
// ---------------------------------------------------------------------------
function showAdmin() {
/* global currentView */
/* global currentView, showOverview */
// Toggle: if already in admin view, go back to overview
if (currentView === "admin") {
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
showOverview();
return;
}
currentView = "admin";
document.getElementById("view-overview").style.display = "none";
document.getElementById("view-node").style.display = "none";
@@ -28,9 +40,16 @@ function showAdmin() {
document.getElementById("breadcrumb").style.display = "";
document.getElementById("breadcrumb-label").textContent = "Admin";
document.getElementById("main").scrollTop = 0;
// Highlight admin button as active
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.add("active");
adminBtn.setAttribute("aria-expanded", "true");
}
history.pushState({ view: "admin" }, "");
// Permission gating: hide tabs the user cannot access
// Permission gating: hide nav items the user cannot access
var perms = sessionStorage.getItem("turnstone_permissions") || "";
var tabPerms = {
users: "admin.users",
@@ -44,29 +63,65 @@ function showAdmin() {
"ws-templates": "admin.ws_templates",
usage: "admin.usage",
audit: "admin.audit",
settings: "admin.users",
};
if (perms) {
var permSet = perms.split(",");
var tabs = document.querySelectorAll(".admin-tab");
for (var i = 0; i < tabs.length; i++) {
var tabName = tabs[i].getAttribute("data-tab");
var navItems = document.querySelectorAll(".admin-nav");
for (var i = 0; i < navItems.length; i++) {
var tabName = navItems[i].getAttribute("data-tab");
var needed = tabPerms[tabName];
if (needed && permSet.indexOf(needed) < 0) {
tabs[i].style.display = "none";
navItems[i].style.display = "none";
} else {
tabs[i].style.display = "";
navItems[i].style.display = "";
}
}
}
// Switch to the first visible tab
var visibleTabs = document.querySelectorAll(
'.admin-tab:not([style*="display: none"])',
);
if (visibleTabs.length > 0) {
switchAdminTab(visibleTabs[0].getAttribute("data-tab"));
// Hide groups where all children are permission-hidden
var groups = document.querySelectorAll(".admin-sidebar-group");
for (var g = 0; g < groups.length; g++) {
var visibleInGroup = groups[g].querySelectorAll(
'.admin-nav:not([style*="display: none"])',
);
groups[g].style.display = visibleInGroup.length > 0 ? "" : "none";
}
// Mobile: ensure sidebar starts hidden + inert; desktop: ensure it's accessible
var sidebar = document.getElementById("admin-sidebar");
if (window.innerWidth <= 700) {
_mobileSidebarOpen = false;
sidebar.classList.add("collapsed");
sidebar.classList.remove("open");
sidebar.setAttribute("aria-hidden", "true");
sidebar.setAttribute("inert", "");
} else {
// No tabs visible — show empty state instead of loading an inaccessible tab
sidebar.removeAttribute("aria-hidden");
sidebar.removeAttribute("inert");
}
// Mobile backdrop listener (idempotent)
var backdrop = document.getElementById("admin-sidebar-backdrop");
if (backdrop && !backdrop._listenerAttached) {
backdrop.addEventListener("click", function () {
if (_mobileSidebarOpen) {
_toggleMobileSidebar();
var mt = document.getElementById("admin-mobile-toggle");
if (mt) mt.focus();
}
});
backdrop._listenerAttached = true;
}
// Switch to the first visible nav item
var visibleNavs = document.querySelectorAll(
'.admin-nav:not([style*="display: none"])',
);
if (visibleNavs.length > 0) {
switchAdminTab(visibleNavs[0].getAttribute("data-tab"));
} else {
// No tabs visible — show empty state
var panels = document.querySelectorAll(".admin-panel");
for (var j = 0; j < panels.length; j++) panels[j].style.display = "none";
var empty = document.getElementById("admin-no-permissions");
@@ -75,23 +130,54 @@ function showAdmin() {
empty.id = "admin-no-permissions";
empty.className = "dashboard-empty";
empty.textContent = "You do not have permissions to view any admin tabs.";
document.getElementById("view-admin").appendChild(empty);
document.getElementById("admin-content").appendChild(empty);
}
empty.style.display = "";
}
}
function _injectMobileToggle(tab) {
var toggle = document.getElementById("admin-mobile-toggle");
if (!toggle) {
toggle = document.createElement("button");
toggle.id = "admin-mobile-toggle";
toggle.className = "admin-mobile-toggle";
toggle.setAttribute("aria-label", "Open navigation");
toggle.onclick = function () {
_mobileSidebarOpen = false;
_toggleMobileSidebar();
};
}
var panel = document.getElementById("admin-" + tab);
if (panel) {
var toolbar = panel.querySelector(".admin-toolbar");
if (toolbar) toolbar.insertBefore(toggle, toolbar.firstChild);
}
}
function _toggleMobileSidebar() {
_mobileSidebarOpen = !_mobileSidebarOpen;
var sidebar = document.getElementById("admin-sidebar");
sidebar.classList.toggle("open", _mobileSidebarOpen);
sidebar.classList.toggle("collapsed", !_mobileSidebarOpen);
sidebar.setAttribute("aria-hidden", _mobileSidebarOpen ? "false" : "true");
if (_mobileSidebarOpen) sidebar.removeAttribute("inert");
else sidebar.setAttribute("inert", "");
var backdrop = document.getElementById("admin-sidebar-backdrop");
if (backdrop) backdrop.classList.toggle("visible", _mobileSidebarOpen);
}
function switchAdminTab(tab) {
_adminTab = tab;
// Hide no-permissions empty state if it was showing
var noPerms = document.getElementById("admin-no-permissions");
if (noPerms) noPerms.style.display = "none";
var tabs = document.querySelectorAll(".admin-tab");
for (var i = 0; i < tabs.length; i++) {
var isActive = tabs[i].getAttribute("data-tab") === tab;
tabs[i].classList.toggle("active", isActive);
tabs[i].setAttribute("aria-selected", isActive ? "true" : "false");
tabs[i].setAttribute("tabindex", isActive ? "0" : "-1");
var navItems = document.querySelectorAll(".admin-nav");
for (var i = 0; i < navItems.length; i++) {
var isActive = navItems[i].getAttribute("data-tab") === tab;
navItems[i].classList.toggle("active", isActive);
navItems[i].setAttribute("aria-selected", isActive ? "true" : "false");
navItems[i].setAttribute("tabindex", isActive ? "0" : "-1");
}
var panels = [
"users",
@@ -105,6 +191,7 @@ function switchAdminTab(tab) {
"ws-templates",
"usage",
"audit",
"settings",
];
for (var p = 0; p < panels.length; p++) {
var el = document.getElementById("admin-" + panels[p]);
@@ -125,6 +212,21 @@ function switchAdminTab(tab) {
_populateAuditUserFilter();
loadGovAudit();
}
if (tab === "settings") loadSettings();
// Update breadcrumb with active tab label
var activeNav = document.querySelector('.admin-nav[data-tab="' + tab + '"]');
var label = activeNav ? activeNav.textContent : tab;
var bcLabel = document.getElementById("breadcrumb-label");
if (bcLabel) bcLabel.textContent = "Admin / " + label;
// Inject mobile hamburger toggle into active panel's toolbar
_injectMobileToggle(tab);
// On mobile, auto-close sidebar after tab selection
if (window.innerWidth <= 700 && _mobileSidebarOpen) {
_toggleMobileSidebar();
}
}
// ---------------------------------------------------------------------------
@@ -1574,33 +1676,70 @@ document.addEventListener("keydown", function (e) {
return;
}
}
// Close mobile sidebar drawer on Escape
if (_mobileSidebarOpen && window.innerWidth <= 700) {
e.preventDefault();
_toggleMobileSidebar();
var mt = document.getElementById("admin-mobile-toggle");
if (mt) mt.focus();
return;
}
});
// Tab arrow key navigation
// Sidebar arrow key navigation (vertical)
(function () {
var tablist = document.querySelector(".admin-tabs");
if (!tablist) return;
tablist.addEventListener("keydown", function (e) {
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
var allTabs = document.querySelectorAll(
'.admin-tab:not([style*="display: none"])',
var sidebar = document.getElementById("admin-sidebar");
if (!sidebar) return;
sidebar.addEventListener("keydown", function (e) {
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
e.preventDefault();
var allNavs = document.querySelectorAll(
'.admin-nav:not([style*="display: none"])',
);
var tabOrder = [];
for (var ti = 0; ti < allTabs.length; ti++) {
tabOrder.push(allTabs[ti].getAttribute("data-tab"));
var navOrder = [];
for (var ni = 0; ni < allNavs.length; ni++) {
navOrder.push(allNavs[ni].getAttribute("data-tab"));
}
if (tabOrder.length === 0) return;
var idx = tabOrder.indexOf(_adminTab);
if (e.key === "ArrowRight") idx = (idx + 1) % tabOrder.length;
else idx = (idx - 1 + tabOrder.length) % tabOrder.length;
switchAdminTab(tabOrder[idx]);
if (navOrder.length === 0) return;
var idx = navOrder.indexOf(_adminTab);
if (e.key === "ArrowDown") idx = (idx + 1) % navOrder.length;
else idx = (idx - 1 + navOrder.length) % navOrder.length;
switchAdminTab(navOrder[idx]);
var btn = document.querySelector(
'.admin-tab[data-tab="' + tabOrder[idx] + '"]',
'.admin-nav[data-tab="' + navOrder[idx] + '"]',
);
if (btn) btn.focus();
});
})();
// Sync sidebar aria-hidden/inert when crossing mobile/desktop breakpoint
(function () {
var resizeTimer;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
if (typeof currentView === "undefined" || currentView !== "admin") return;
var sidebar = document.getElementById("admin-sidebar");
if (!sidebar) return;
var isMobile = window.innerWidth <= 700;
var backdrop = document.getElementById("admin-sidebar-backdrop");
if (isMobile && !_mobileSidebarOpen) {
sidebar.setAttribute("aria-hidden", "true");
sidebar.setAttribute("inert", "");
sidebar.classList.add("collapsed");
sidebar.classList.remove("open");
if (backdrop) backdrop.classList.remove("visible");
} else if (!isMobile) {
sidebar.removeAttribute("aria-hidden");
sidebar.removeAttribute("inert");
sidebar.classList.remove("collapsed", "open");
if (backdrop) backdrop.classList.remove("visible");
_mobileSidebarOpen = false;
}
}, 150);
});
})();
// ---------------------------------------------------------------------------
// Confirm Modal (reusable styled replacement for confirm())
// ---------------------------------------------------------------------------
@@ -1644,6 +1783,16 @@ function _confirmCallback() {
hideConfirmModal();
}
// ---------------------------------------------------------------------------
// Settings (stub — full implementation is a separate project)
// ---------------------------------------------------------------------------
function loadSettings() {
var el = document.getElementById("admin-settings-content");
if (el)
el.innerHTML = '<div class="dashboard-empty">Settings coming soon</div>';
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
+20
View File
@@ -383,6 +383,11 @@ function showOverview() {
document.getElementById("view-filtered").style.display = "none";
var adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
document.getElementById("breadcrumb").style.display = "none";
document.getElementById("main").scrollTop = 0;
if (clusterState) renderFromState();
@@ -924,6 +929,11 @@ function drillDownToNode(nodeId, serverUrl) {
document.getElementById("view-filtered").style.display = "none";
var adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
document.getElementById("breadcrumb").style.display = "";
document.getElementById("breadcrumb-label").textContent = nodeId;
var link = document.getElementById("node-link");
@@ -973,6 +983,11 @@ function drillDownByState(state) {
document.getElementById("view-filtered").style.display = "";
var adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
document.getElementById("breadcrumb").style.display = "";
var sd = STATE_DISPLAY[state] || STATE_DISPLAY.idle;
document.getElementById("breadcrumb-label").textContent =
@@ -995,6 +1010,11 @@ function drillDownByNode(nodeId) {
document.getElementById("view-filtered").style.display = "";
var adminView = document.getElementById("view-admin");
if (adminView) adminView.style.display = "none";
var adminBtn = document.getElementById("admin-btn");
if (adminBtn) {
adminBtn.classList.remove("active");
adminBtn.setAttribute("aria-expanded", "false");
}
document.getElementById("breadcrumb").style.display = "";
document.getElementById("breadcrumb-label").textContent = nodeId;
document.getElementById("filtered-title").textContent =
+234 -167
View File
@@ -16,7 +16,7 @@
<span id="cluster-summary" aria-live="polite"></span>
<span id="status-bar" role="status" aria-live="polite"></span>
<button id="new-ws-btn" class="header-btn header-btn-accent" onclick="showNewWsModal()" title="Create workstream">+ new</button>
<button id="admin-btn" class="header-btn" onclick="showAdmin()" title="User &amp; token administration">admin</button>
<button id="admin-btn" class="header-btn" onclick="showAdmin()" title="User &amp; token administration" aria-expanded="false" aria-controls="view-admin">admin</button>
<button id="logout-btn" class="header-btn" onclick="logout()" style="display:none">logout</button>
<button id="theme-toggle" class="header-btn" onclick="toggleTheme()" aria-label="Toggle light/dark theme">&#9790;</button>
</div>
@@ -77,19 +77,41 @@
<!-- ADMIN PANEL -->
<div id="view-admin" style="display:none">
<div class="admin-tabs" role="tablist">
<button id="tab-users" class="admin-tab active" data-tab="users" role="tab" aria-selected="true" aria-controls="admin-users" tabindex="0" onclick="switchAdminTab('users')">Users</button>
<button id="tab-tokens" class="admin-tab" data-tab="tokens" role="tab" aria-selected="false" aria-controls="admin-tokens" tabindex="-1" onclick="switchAdminTab('tokens')">Tokens</button>
<button id="tab-channels" class="admin-tab" data-tab="channels" role="tab" aria-selected="false" aria-controls="admin-channels" tabindex="-1" onclick="switchAdminTab('channels')">Channels</button>
<button id="tab-schedules" class="admin-tab" data-tab="schedules" role="tab" aria-selected="false" aria-controls="admin-schedules" tabindex="-1" onclick="switchAdminTab('schedules')">Schedules</button>
<button id="tab-watches" class="admin-tab" data-tab="watches" role="tab" aria-selected="false" aria-controls="admin-watches" tabindex="-1" onclick="switchAdminTab('watches')">Watches</button>
<button id="tab-roles" class="admin-tab" data-tab="roles" role="tab" aria-selected="false" aria-controls="admin-roles" tabindex="-1" onclick="switchAdminTab('roles')">Roles</button>
<button id="tab-policies" class="admin-tab" data-tab="policies" role="tab" aria-selected="false" aria-controls="admin-policies" tabindex="-1" onclick="switchAdminTab('policies')">Policies</button>
<button id="tab-templates" class="admin-tab" data-tab="templates" role="tab" aria-selected="false" aria-controls="admin-templates" tabindex="-1" onclick="switchAdminTab('templates')">Templates</button>
<button id="tab-ws-templates" class="admin-tab" data-tab="ws-templates" role="tab" aria-selected="false" aria-controls="admin-ws-templates" tabindex="-1" onclick="switchAdminTab('ws-templates')">WS Templates</button>
<button id="tab-usage" class="admin-tab" data-tab="usage" role="tab" aria-selected="false" aria-controls="admin-usage" tabindex="-1" onclick="switchAdminTab('usage')">Usage</button>
<button id="tab-audit" class="admin-tab" data-tab="audit" role="tab" aria-selected="false" aria-controls="admin-audit" tabindex="-1" onclick="switchAdminTab('audit')">Audit</button>
</div>
<div id="admin-layout" class="admin-layout">
<!-- Sidebar navigation -->
<nav id="admin-sidebar" class="admin-sidebar" role="tablist" aria-label="Admin navigation" aria-orientation="vertical">
<div class="admin-sidebar-group" data-group="identity" role="group" aria-label="Identity">
<div class="admin-sidebar-group-label" aria-hidden="true">Identity</div>
<button id="tab-users" class="admin-nav active" data-tab="users" role="tab" aria-selected="true" aria-controls="admin-users" tabindex="0" onclick="switchAdminTab('users')">Users</button>
<button id="tab-tokens" class="admin-nav" data-tab="tokens" role="tab" aria-selected="false" aria-controls="admin-tokens" tabindex="-1" onclick="switchAdminTab('tokens')">API Tokens</button>
<button id="tab-channels" class="admin-nav" data-tab="channels" role="tab" aria-selected="false" aria-controls="admin-channels" tabindex="-1" onclick="switchAdminTab('channels')">Channels</button>
</div>
<div class="admin-sidebar-group" data-group="automation" role="group" aria-label="Automation">
<div class="admin-sidebar-group-label" aria-hidden="true">Automation</div>
<button id="tab-schedules" class="admin-nav" data-tab="schedules" role="tab" aria-selected="false" aria-controls="admin-schedules" tabindex="-1" onclick="switchAdminTab('schedules')">Schedules</button>
<button id="tab-watches" class="admin-nav" data-tab="watches" role="tab" aria-selected="false" aria-controls="admin-watches" tabindex="-1" onclick="switchAdminTab('watches')">Watches</button>
</div>
<div class="admin-sidebar-group" data-group="governance" role="group" aria-label="Governance">
<div class="admin-sidebar-group-label" aria-hidden="true">Governance</div>
<button id="tab-roles" class="admin-nav" data-tab="roles" role="tab" aria-selected="false" aria-controls="admin-roles" tabindex="-1" onclick="switchAdminTab('roles')">Roles</button>
<button id="tab-policies" class="admin-nav" data-tab="policies" role="tab" aria-selected="false" aria-controls="admin-policies" tabindex="-1" onclick="switchAdminTab('policies')">Policies</button>
<button id="tab-templates" class="admin-nav" data-tab="templates" role="tab" aria-selected="false" aria-controls="admin-templates" tabindex="-1" onclick="switchAdminTab('templates')">Templates</button>
<button id="tab-ws-templates" class="admin-nav" data-tab="ws-templates" role="tab" aria-selected="false" aria-controls="admin-ws-templates" tabindex="-1" onclick="switchAdminTab('ws-templates')">WS Templates</button>
</div>
<div class="admin-sidebar-group" data-group="observe" role="group" aria-label="Observe">
<div class="admin-sidebar-group-label" aria-hidden="true">Observe</div>
<button id="tab-usage" class="admin-nav" data-tab="usage" role="tab" aria-selected="false" aria-controls="admin-usage" tabindex="-1" onclick="switchAdminTab('usage')">Usage</button>
<button id="tab-audit" class="admin-nav" data-tab="audit" role="tab" aria-selected="false" aria-controls="admin-audit" tabindex="-1" onclick="switchAdminTab('audit')">Audit</button>
</div>
<div class="admin-sidebar-group" data-group="system" role="group" aria-label="System">
<div class="admin-sidebar-group-label" aria-hidden="true">System</div>
<button id="tab-settings" class="admin-nav" data-tab="settings" role="tab" aria-selected="false" aria-controls="admin-settings" tabindex="-1" onclick="switchAdminTab('settings')">Settings</button>
</div>
</nav>
<div id="admin-sidebar-backdrop" class="admin-sidebar-backdrop" aria-hidden="true"></div>
<!-- Content area -->
<div id="admin-content" class="admin-content">
<!-- Users Tab -->
<div id="admin-users" class="admin-panel" role="tabpanel" aria-labelledby="tab-users">
@@ -111,7 +133,7 @@
<!-- Tokens Tab -->
<div id="admin-tokens" class="admin-panel" role="tabpanel" aria-labelledby="tab-tokens" style="display:none">
<div class="admin-toolbar">
<span class="section-header" style="margin:0">TOKENS</span>
<span class="section-header" style="margin:0">API TOKENS</span>
<label for="admin-token-user" class="sr-only">Filter tokens by user</label>
<select id="admin-token-user" onchange="loadAdminTokens()">
<option value="">Select user...</option>
@@ -324,6 +346,19 @@
<div class="dashboard-empty">Loading audit log...</div>
</div>
</div>
<!-- Settings Tab -->
<div id="admin-settings" class="admin-panel" role="tabpanel" aria-labelledby="tab-settings" style="display:none">
<div class="admin-toolbar">
<span class="section-header" style="margin:0">SETTINGS</span>
</div>
<div id="admin-settings-content">
<div class="dashboard-empty">Settings coming soon</div>
</div>
</div>
</div><!-- /admin-content -->
</div><!-- /admin-layout -->
</div>
</div>
@@ -479,44 +514,52 @@ window.TURNSTONE_KB_SHORTCUTS = [
<div id="create-schedule-box" class="admin-modal admin-modal-wide">
<h2 id="create-schedule-title">New Schedule</h2>
<div id="create-schedule-error" role="alert" aria-live="assertive"></div>
<label for="cs-name">Name</label>
<input id="cs-name" type="text" placeholder="Daily health check" autocomplete="off">
<label for="cs-desc">Description <span class="label-hint">optional</span></label>
<input id="cs-desc" type="text" placeholder="" autocomplete="off">
<label for="cs-type">Schedule type</label>
<select id="cs-type" onchange="toggleScheduleTypeFields()">
<option value="cron">Cron (recurring)</option>
<option value="at">At (one-shot)</option>
</select>
<div id="cs-cron-group">
<label for="cs-cron">Cron expression</label>
<input id="cs-cron" type="text" placeholder="0 9 * * MON-FRI" autocomplete="off" spellcheck="false" aria-describedby="cs-cron-hint">
<span id="cs-cron-hint" class="label-hint" style="display:block;margin-top:3px">min hour day month weekday</span>
<div class="modal-columns">
<div class="modal-col">
<div class="modal-col-heading">Schedule</div>
<label for="cs-name">Name</label>
<input id="cs-name" type="text" placeholder="Daily health check" autocomplete="off">
<label for="cs-desc">Description <span class="label-hint">optional</span></label>
<input id="cs-desc" type="text" placeholder="" autocomplete="off">
<label for="cs-type">Schedule type</label>
<select id="cs-type" onchange="toggleScheduleTypeFields()">
<option value="cron">Cron (recurring)</option>
<option value="at">At (one-shot)</option>
</select>
<div id="cs-cron-group">
<label for="cs-cron">Cron expression</label>
<input id="cs-cron" type="text" placeholder="0 9 * * MON-FRI" autocomplete="off" spellcheck="false" aria-describedby="cs-cron-hint">
<span id="cs-cron-hint" class="label-hint" style="display:block;margin-top:3px">min hour day month weekday</span>
</div>
<div id="cs-at-group" style="display:none">
<label for="cs-at">Run at</label>
<input id="cs-at" type="datetime-local">
</div>
<label for="cs-target">Target</label>
<select id="cs-target" onchange="toggleScheduleNodeField()">
<option value="auto">Auto (best available)</option>
<option value="pool">Pool (any bridge)</option>
<option value="all">All nodes</option>
<option value="node">Specific node...</option>
</select>
<div id="cs-node-group" style="display:none">
<label for="cs-node">Node ID</label>
<input id="cs-node" type="text" placeholder="node-001" autocomplete="off" spellcheck="false">
</div>
</div>
<div class="modal-col">
<div class="modal-col-heading">Execution</div>
<label for="cs-model">Model <span class="label-hint">optional</span></label>
<input id="cs-model" type="text" placeholder="Default model" autocomplete="off">
<label for="cs-template">Template <span class="label-hint">optional</span></label>
<input id="cs-template" type="text" placeholder="Prompt template name" autocomplete="off">
<label for="cs-ws-template">WS Template <span class="label-hint">optional</span></label>
<select id="cs-ws-template"><option value="">None</option></select>
<label for="cs-message">Initial message</label>
<textarea id="cs-message" rows="3" placeholder="What should the workstream do?"></textarea>
<label class="admin-checkbox"><input id="cs-autoapprove" type="checkbox"> Auto-approve tool calls</label>
</div>
</div>
<div id="cs-at-group" style="display:none">
<label for="cs-at">Run at</label>
<input id="cs-at" type="datetime-local">
</div>
<label for="cs-target">Target</label>
<select id="cs-target" onchange="toggleScheduleNodeField()">
<option value="auto">Auto (best available)</option>
<option value="pool">Pool (any bridge)</option>
<option value="all">All nodes</option>
<option value="node">Specific node...</option>
</select>
<div id="cs-node-group" style="display:none">
<label for="cs-node">Node ID</label>
<input id="cs-node" type="text" placeholder="node-001" autocomplete="off" spellcheck="false">
</div>
<label for="cs-model">Model <span class="label-hint">optional</span></label>
<input id="cs-model" type="text" placeholder="Default model" autocomplete="off">
<label for="cs-template">Template <span class="label-hint">optional</span></label>
<input id="cs-template" type="text" placeholder="Prompt template name" autocomplete="off">
<label for="cs-ws-template">WS Template <span class="label-hint">optional &mdash; workstream profile</span></label>
<select id="cs-ws-template"><option value="">None</option></select>
<label for="cs-message">Initial message</label>
<textarea id="cs-message" rows="3" placeholder="What should the workstream do?"></textarea>
<label class="admin-checkbox"><input id="cs-autoapprove" type="checkbox"> Auto-approve tool calls</label>
<div class="modal-buttons">
<button class="modal-cancel" onclick="hideCreateScheduleModal()">Cancel</button>
<button id="cs-submit" class="modal-submit" onclick="submitCreateSchedule()">Create</button>
@@ -530,44 +573,52 @@ window.TURNSTONE_KB_SHORTCUTS = [
<h2 id="edit-schedule-title">Edit Schedule</h2>
<div id="edit-schedule-error" role="alert" aria-live="assertive"></div>
<input id="es-id" type="hidden">
<label for="es-name">Name</label>
<input id="es-name" type="text" autocomplete="off">
<label for="es-desc">Description</label>
<input id="es-desc" type="text" autocomplete="off">
<label for="es-type">Schedule type</label>
<select id="es-type" onchange="toggleEditScheduleTypeFields()">
<option value="cron">Cron (recurring)</option>
<option value="at">At (one-shot)</option>
</select>
<div id="es-cron-group">
<label for="es-cron">Cron expression</label>
<input id="es-cron" type="text" autocomplete="off" spellcheck="false">
<div class="modal-columns">
<div class="modal-col">
<div class="modal-col-heading">Schedule</div>
<label for="es-name">Name</label>
<input id="es-name" type="text" autocomplete="off">
<label for="es-desc">Description</label>
<input id="es-desc" type="text" autocomplete="off">
<label for="es-type">Schedule type</label>
<select id="es-type" onchange="toggleEditScheduleTypeFields()">
<option value="cron">Cron (recurring)</option>
<option value="at">At (one-shot)</option>
</select>
<div id="es-cron-group">
<label for="es-cron">Cron expression</label>
<input id="es-cron" type="text" autocomplete="off" spellcheck="false">
</div>
<div id="es-at-group" style="display:none">
<label for="es-at">Run at</label>
<input id="es-at" type="datetime-local">
</div>
<label for="es-target">Target</label>
<select id="es-target" onchange="toggleEditScheduleNodeField()">
<option value="auto">Auto (best available)</option>
<option value="pool">Pool (any bridge)</option>
<option value="all">All nodes</option>
<option value="node">Specific node...</option>
</select>
<div id="es-node-group" style="display:none">
<label for="es-node">Node ID</label>
<input id="es-node" type="text" autocomplete="off" spellcheck="false">
</div>
</div>
<div class="modal-col">
<div class="modal-col-heading">Execution</div>
<label for="es-model">Model</label>
<input id="es-model" type="text" autocomplete="off">
<label for="es-template">Template <span class="label-hint">optional</span></label>
<input id="es-template" type="text" autocomplete="off">
<label for="es-ws-template">WS Template <span class="label-hint">optional</span></label>
<select id="es-ws-template"><option value="">None</option></select>
<label for="es-message">Initial message</label>
<textarea id="es-message" rows="3"></textarea>
<label class="admin-checkbox"><input id="es-autoapprove" type="checkbox"> Auto-approve tool calls</label>
<label class="admin-checkbox"><input id="es-enabled" type="checkbox"> Enabled</label>
</div>
</div>
<div id="es-at-group" style="display:none">
<label for="es-at">Run at</label>
<input id="es-at" type="datetime-local">
</div>
<label for="es-target">Target</label>
<select id="es-target" onchange="toggleEditScheduleNodeField()">
<option value="auto">Auto (best available)</option>
<option value="pool">Pool (any bridge)</option>
<option value="all">All nodes</option>
<option value="node">Specific node...</option>
</select>
<div id="es-node-group" style="display:none">
<label for="es-node">Node ID</label>
<input id="es-node" type="text" autocomplete="off" spellcheck="false">
</div>
<label for="es-model">Model</label>
<input id="es-model" type="text" autocomplete="off">
<label for="es-template">Template <span class="label-hint">optional</span></label>
<input id="es-template" type="text" autocomplete="off">
<label for="es-ws-template">WS Template <span class="label-hint">optional</span></label>
<select id="es-ws-template"><option value="">None</option></select>
<label for="es-message">Initial message</label>
<textarea id="es-message" rows="3"></textarea>
<label class="admin-checkbox"><input id="es-autoapprove" type="checkbox"> Auto-approve tool calls</label>
<label class="admin-checkbox"><input id="es-enabled" type="checkbox"> Enabled</label>
<div class="modal-buttons">
<button class="modal-cancel" onclick="hideEditScheduleModal()">Cancel</button>
<button id="es-submit" class="modal-submit" onclick="submitEditSchedule()">Save</button>
@@ -745,46 +796,54 @@ window.TURNSTONE_KB_SHORTCUTS = [
<div id="create-wst-box" class="admin-modal admin-modal-wide">
<h2 id="create-wst-title">Create Workstream Template</h2>
<div id="create-wst-error" role="alert" aria-live="assertive"></div>
<label for="cwst-name">Name</label>
<input id="cwst-name" type="text" placeholder="e.g. Code Review Agent" autocomplete="off">
<label for="cwst-description">Description <span class="label-hint">optional</span></label>
<input id="cwst-description" type="text" placeholder="Brief description" autocomplete="off">
<label>System Prompt Source</label>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-inline" type="radio" name="cwst-src" value="inline" checked onchange="toggleWstPromptSource()"> Inline</label>
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-ref" type="radio" name="cwst-src" value="ref" onchange="toggleWstPromptSource()"> Prompt Template</label>
<div class="modal-columns">
<div class="modal-col">
<div class="modal-col-heading">Identity</div>
<label for="cwst-name">Name</label>
<input id="cwst-name" type="text" placeholder="e.g. Code Review Agent" autocomplete="off">
<label for="cwst-description">Description <span class="label-hint">optional</span></label>
<input id="cwst-description" type="text" placeholder="Brief description" autocomplete="off">
<label>System Prompt Source</label>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-inline" type="radio" name="cwst-src" value="inline" checked onchange="toggleWstPromptSource()"> Inline</label>
<label class="admin-checkbox" style="margin-top:0"><input id="cwst-src-ref" type="radio" name="cwst-src" value="ref" onchange="toggleWstPromptSource()"> Prompt Template</label>
</div>
<div id="cwst-inline-section">
<label for="cwst-system-prompt">System Prompt <span class="label-hint">inline text</span></label>
<textarea id="cwst-system-prompt" rows="4" placeholder="You are a..."></textarea>
</div>
<div id="cwst-ref-section" style="display:none">
<label for="cwst-prompt-template">Prompt Template <span class="label-hint">reference by name</span></label>
<select id="cwst-prompt-template"><option value="">None</option></select>
</div>
<label class="admin-checkbox"><input id="cwst-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="cwst-auto-approve-tools">Auto-approve tools <span class="label-hint">comma-separated</span></label>
<input id="cwst-auto-approve-tools" type="text" placeholder="e.g. read_file, list_directory" autocomplete="off">
</div>
<div class="modal-col">
<div class="modal-col-heading">Model Config</div>
<label for="cwst-model">Model</label>
<input id="cwst-model" type="text" autocomplete="off">
<label for="cwst-temperature">Temperature <span class="label-hint">0.02.0</span></label>
<input id="cwst-temperature" type="number" step="0.1" min="0" max="2" autocomplete="off">
<label for="cwst-reasoning-effort">Reasoning effort</label>
<select id="cwst-reasoning-effort">
<option value="">Default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="none">None</option>
<option value="max">Max</option>
</select>
<label for="cwst-max-tokens">Max tokens <span class="label-hint">0 = default</span></label>
<input id="cwst-max-tokens" type="number" min="0" autocomplete="off">
<label for="cwst-agent-max-turns">Agent max turns <span class="label-hint">0 = default</span></label>
<input id="cwst-agent-max-turns" type="number" min="0" autocomplete="off">
<label for="cwst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
<input id="cwst-token-budget" type="number" value="0" min="0">
<label class="admin-checkbox"><input id="cwst-enabled" type="checkbox" checked> Enabled</label>
</div>
</div>
<div id="cwst-inline-section">
<label for="cwst-system-prompt">System Prompt <span class="label-hint">inline text</span></label>
<textarea id="cwst-system-prompt" rows="4" placeholder="You are a..."></textarea>
</div>
<div id="cwst-ref-section" style="display:none">
<label for="cwst-prompt-template">Prompt Template <span class="label-hint">reference by name</span></label>
<select id="cwst-prompt-template"><option value="">None</option></select>
</div>
<label for="cwst-model">Model <span class="label-hint">optional — server default if empty</span></label>
<input id="cwst-model" type="text" placeholder="Default model" autocomplete="off">
<label class="admin-checkbox"><input id="cwst-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="cwst-auto-approve-tools">Auto-approve tools <span class="label-hint">comma-separated tool names</span></label>
<input id="cwst-auto-approve-tools" type="text" placeholder="e.g. read_file, list_directory" autocomplete="off">
<label for="cwst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
<input id="cwst-token-budget" type="number" value="0" min="0">
<label for="cwst-temperature">Temperature <span class="label-hint">optional — 0.0-2.0, empty = server default</span></label>
<input id="cwst-temperature" type="number" step="0.1" min="0" max="2" placeholder="Server default" autocomplete="off">
<label for="cwst-reasoning-effort">Reasoning effort <span class="label-hint">optional</span></label>
<select id="cwst-reasoning-effort">
<option value="">Server default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="none">None</option>
<option value="max">Max</option>
</select>
<label for="cwst-max-tokens">Max tokens <span class="label-hint">optional — 0 = server default</span></label>
<input id="cwst-max-tokens" type="number" min="0" placeholder="Server default" autocomplete="off">
<label for="cwst-agent-max-turns">Agent max turns <span class="label-hint">optional — 0 = server default</span></label>
<input id="cwst-agent-max-turns" type="number" min="0" placeholder="Server default" autocomplete="off">
<label class="admin-checkbox"><input id="cwst-enabled" type="checkbox" checked> Enabled</label>
<div class="modal-buttons">
<button class="modal-cancel" onclick="hideCreateWsTemplateModal()">Cancel</button>
<button id="cwst-submit" class="modal-submit" onclick="submitCreateWsTemplate()">Create</button>
@@ -798,46 +857,54 @@ window.TURNSTONE_KB_SHORTCUTS = [
<h2 id="edit-wst-title">Edit Workstream Template</h2>
<div id="edit-wst-error" role="alert" aria-live="assertive"></div>
<input id="ewst-id" type="hidden">
<label for="ewst-name">Name</label>
<input id="ewst-name" type="text" autocomplete="off">
<label for="ewst-description">Description</label>
<input id="ewst-description" type="text" autocomplete="off">
<label>System Prompt Source</label>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-inline" type="radio" name="ewst-src" value="inline" checked onchange="toggleEditWstPromptSource()"> Inline</label>
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-ref" type="radio" name="ewst-src" value="ref" onchange="toggleEditWstPromptSource()"> Prompt Template</label>
<div class="modal-columns">
<div class="modal-col">
<div class="modal-col-heading">Identity</div>
<label for="ewst-name">Name</label>
<input id="ewst-name" type="text" autocomplete="off">
<label for="ewst-description">Description</label>
<input id="ewst-description" type="text" autocomplete="off">
<label>System Prompt Source</label>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-inline" type="radio" name="ewst-src" value="inline" checked onchange="toggleEditWstPromptSource()"> Inline</label>
<label class="admin-checkbox" style="margin-top:0"><input id="ewst-src-ref" type="radio" name="ewst-src" value="ref" onchange="toggleEditWstPromptSource()"> Prompt Template</label>
</div>
<div id="ewst-inline-section">
<label for="ewst-system-prompt">System Prompt</label>
<textarea id="ewst-system-prompt" rows="4"></textarea>
</div>
<div id="ewst-ref-section" style="display:none">
<label for="ewst-prompt-template">Prompt Template</label>
<select id="ewst-prompt-template"><option value="">None</option></select>
</div>
<label class="admin-checkbox"><input id="ewst-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="ewst-auto-approve-tools">Auto-approve tools</label>
<input id="ewst-auto-approve-tools" type="text" autocomplete="off">
</div>
<div class="modal-col">
<div class="modal-col-heading">Model Config</div>
<label for="ewst-model">Model</label>
<input id="ewst-model" type="text" autocomplete="off">
<label for="ewst-temperature">Temperature <span class="label-hint">0.02.0</span></label>
<input id="ewst-temperature" type="number" step="0.1" min="0" max="2" autocomplete="off">
<label for="ewst-reasoning-effort">Reasoning effort</label>
<select id="ewst-reasoning-effort">
<option value="">Default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="none">None</option>
<option value="max">Max</option>
</select>
<label for="ewst-max-tokens">Max tokens <span class="label-hint">0 = default</span></label>
<input id="ewst-max-tokens" type="number" min="0" autocomplete="off">
<label for="ewst-agent-max-turns">Agent max turns <span class="label-hint">0 = default</span></label>
<input id="ewst-agent-max-turns" type="number" min="0" autocomplete="off">
<label for="ewst-token-budget">Token budget <span class="label-hint">0 = unlimited</span></label>
<input id="ewst-token-budget" type="number" value="0" min="0">
<label class="admin-checkbox"><input id="ewst-enabled" type="checkbox" checked> Enabled</label>
</div>
</div>
<div id="ewst-inline-section">
<label for="ewst-system-prompt">System Prompt</label>
<textarea id="ewst-system-prompt" rows="4"></textarea>
</div>
<div id="ewst-ref-section" style="display:none">
<label for="ewst-prompt-template">Prompt Template</label>
<select id="ewst-prompt-template"><option value="">None</option></select>
</div>
<label for="ewst-model">Model</label>
<input id="ewst-model" type="text" autocomplete="off">
<label class="admin-checkbox"><input id="ewst-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="ewst-auto-approve-tools">Auto-approve tools</label>
<input id="ewst-auto-approve-tools" type="text" autocomplete="off">
<label for="ewst-token-budget">Token budget</label>
<input id="ewst-token-budget" type="number" value="0" min="0">
<label for="ewst-temperature">Temperature <span class="label-hint">optional — 0.0-2.0, empty = server default</span></label>
<input id="ewst-temperature" type="number" step="0.1" min="0" max="2" placeholder="Server default" autocomplete="off">
<label for="ewst-reasoning-effort">Reasoning effort <span class="label-hint">optional</span></label>
<select id="ewst-reasoning-effort">
<option value="">Server default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="none">None</option>
<option value="max">Max</option>
</select>
<label for="ewst-max-tokens">Max tokens <span class="label-hint">optional</span></label>
<input id="ewst-max-tokens" type="number" min="0" placeholder="Server default" autocomplete="off">
<label for="ewst-agent-max-turns">Agent max turns <span class="label-hint">optional</span></label>
<input id="ewst-agent-max-turns" type="number" min="0" placeholder="Server default" autocomplete="off">
<label class="admin-checkbox"><input id="ewst-enabled" type="checkbox" checked> Enabled</label>
<div class="modal-buttons">
<button class="modal-cancel" onclick="hideEditWsTemplateModal()">Cancel</button>
<button id="ewst-submit" class="modal-submit" onclick="submitEditWsTemplate()">Save</button>
+193 -30
View File
@@ -543,6 +543,30 @@
outline-offset: 2px;
}
/* Admin button — top accent line + active state */
#admin-btn {
position: relative;
overflow: visible;
}
#admin-btn::before {
content: '';
position: absolute;
top: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
border-radius: 0 0 1px 1px;
opacity: 0;
transition: opacity 0.2s ease;
}
#admin-btn.active::before { opacity: 1; }
#admin-btn.active {
color: var(--accent);
border-color: var(--accent);
background: var(--accent-dim);
}
/* ==========================================================================
New Workstream Modal
========================================================================== */
@@ -702,41 +726,126 @@
}
/* ==========================================================================
Admin panel
Admin panel — sidebar layout
========================================================================== */
.admin-tabs {
#view-admin { animation: admin-fadein 0.15s ease-out; }
@keyframes admin-fadein { from { opacity: 0; } to { opacity: 1; } }
.admin-layout {
display: flex;
gap: 2px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 0;
min-height: 0;
flex: 1;
}
.admin-tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--fg-dim);
/* Sidebar — right-aligned to match header admin button position */
.admin-sidebar {
width: 180px;
flex-shrink: 0;
background: var(--bg-surface);
border-left: 1px solid var(--border-strong);
padding: 4px 0;
overflow-y: auto;
order: 1;
}
/* Group labels */
.admin-sidebar-group { margin-bottom: 2px; }
.admin-sidebar-group-label {
font-family: var(--font-display);
font-size: 11px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 8px 16px 10px;
letter-spacing: 0.12em;
color: var(--fg-dim);
padding: 12px 16px 4px;
}
.admin-sidebar-group:first-child .admin-sidebar-group-label {
padding-top: 4px;
}
/* Nav items */
.admin-nav {
display: block;
width: 100%;
background: none;
border: none;
border-right: 2px solid transparent;
color: var(--fg-dim);
font-family: var(--font-display);
font-size: 12px;
font-weight: 500;
padding: 6px 14px 6px 16px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
text-align: left;
transition: color 0.15s, border-color 0.15s, background 0.15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.admin-tab:hover { color: var(--fg); }
.admin-tab.active {
.admin-nav:hover { color: var(--fg); background: var(--bg-highlight); border-right-color: var(--border-strong); }
.admin-nav.active {
color: var(--accent);
border-bottom-color: var(--accent);
border-right-color: var(--accent);
background: var(--accent-dim);
}
.admin-tab:focus-visible {
.admin-nav:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
border-radius: var(--radius-sm);
}
/* Content area */
.admin-content {
flex: 1;
min-width: 0;
padding-right: 20px;
}
/* Mobile backdrop */
.admin-sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 499;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.admin-sidebar-backdrop.visible {
opacity: 1;
pointer-events: auto;
}
/* Mobile menu toggle — visible only on mobile, lives in toolbars */
.admin-mobile-toggle {
display: none;
background: none;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--fg-dim);
width: 28px;
height: 28px;
cursor: pointer;
align-items: center;
justify-content: center;
margin-right: 8px;
flex-shrink: 0;
padding: 0;
}
.admin-mobile-toggle::before {
content: '';
display: block;
width: 14px;
height: 2px;
background: currentColor;
box-shadow: 0 4px 0 currentColor, 0 8px 0 currentColor;
}
.admin-mobile-toggle:hover { color: var(--fg); }
@media (max-width: 700px) {
.admin-mobile-toggle { display: flex; }
}
.admin-toolbar {
display: flex;
align-items: center;
@@ -900,8 +1009,30 @@
.watch-active { color: var(--green); font-weight: 500; }
.watch-completed { color: var(--accent); }
/* Wide modal variant for schedule forms */
.admin-modal-wide { width: 480px; }
/* Two-column form layout for wide modals */
.modal-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.modal-col > label:first-child,
.modal-col > .modal-col-heading + label { margin-top: 0; }
.modal-col-heading {
font-family: var(--font-display);
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: 10px;
}
.modal-columns > .modal-col:first-child {
border-right: 1px solid var(--border);
padding-right: 12px;
}
.modal-columns > .modal-col:last-child {
padding-left: 12px;
}
/* Checkbox labels inside admin modals */
.admin-modal label.admin-checkbox {
@@ -930,16 +1061,32 @@
padding: 32px;
width: 380px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.5), 0 0 80px -20px var(--accent-dim);
position: relative;
}
.admin-modal.admin-modal-wide { width: 820px; }
@media (max-width: 700px) {
.admin-modal.admin-modal-wide { width: auto; }
.modal-columns { grid-template-columns: 1fr; gap: 20px 0; }
.modal-columns > .modal-col:first-child {
border-right: none;
padding-right: 0;
border-bottom: 1px solid var(--border);
padding-bottom: 16px;
}
.modal-columns > .modal-col:last-child { padding-left: 0; }
}
.admin-modal::before {
content: '';
position: absolute;
top: -1px; left: 20%; right: 20%;
top: 0; left: 20%; right: 20%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
border-radius: 1px;
z-index: 1;
pointer-events: none;
}
.admin-modal h2 {
font-family: var(--font-display);
@@ -1030,7 +1177,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
z-index: 600;
}
/* Token display (show-once) */
@@ -1076,13 +1223,27 @@
}
/* ==========================================================================
Admin tabs — horizontal scroll for 10+ tabs
Admin sidebar — mobile off-canvas drawer
========================================================================== */
.admin-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
flex-wrap: nowrap;
scrollbar-width: thin;
@media (max-width: 700px) {
.admin-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: auto;
width: 220px;
z-index: 500;
background: var(--bg-surface);
border-left: 1px solid var(--border-strong);
border-right: none;
transform: translateX(100%);
transition: transform 0.25s ease;
padding-top: 48px;
}
.admin-sidebar.open { transform: translateX(0); }
.admin-sidebar.collapsed { transform: translateX(100%); width: 220px; }
.admin-content { padding-right: 0; }
}
/* ==========================================================================
@@ -1349,7 +1510,9 @@
.node-link, .dash-cell-node, .pagination button { transition: none; }
.dash-row.has-link::after, .node-group-header::before { transition: none; }
#new-ws-box select, #new-ws-box input, #new-ws-buttons button { transition: none; }
.admin-tab, .admin-row, .admin-btn-danger, .admin-btn-action { transition: none; }
.admin-nav, .admin-row, .admin-btn-danger, .admin-btn-action { transition: none; }
.admin-sidebar, .admin-sidebar-backdrop { transition: none; }
#view-admin { animation: none; }
.admin-action-btn, .modal-cancel, .modal-submit { transition: none; }
.admin-modal input, .admin-modal select { transition: none; }
}