Design tokens - Fix WCAG AA text failures: darken light muted text and lighten dark tertiary text so 13-14px copy clears 4.5:1 on every surface. - Rebuild the dark surface ladder in correct values (oklch compresses to near-black below ~15%), giving visible steps: content, chrome, cards. - Establish a 3-layer shell (deep content, lighter sidebar/topbar chrome, lifted cards) with crisp hairline rings and a restrained card shadow. - Unify every surface to one neutral temperature (pure gray); neutralize the cool-tinted sidebar text and legacy surface tokens. - Convert all color tokens to oklch (neutrals + brand). - Unify inputs to the recessed token; fix invisible dark placeholders. - One shared --shadow-dropdown for all menus/listboxes/command palette. Settings sidebar - Sticky contained-card rail with subtle scrollbar; collapsible sub-section groups (active open by default), cross-page section links. - Mobile: collapsible disclosure with animated open, outside/Escape dismiss, press feedback, and reduced-motion support. Mobile - Consolidate the resource header (title + status + links on one row). - Tighten settings-page vertical rhythm. - Rebuild the main sidebar as an animated shadcn-style sheet (slide/fade, Escape, scroll lock, in-panel close, close-on-navigate). Fixes - #11532: selected server invisible in the light-mode terminal toolbar. Docs: update DESIGN.md tokens, shell layering, and temperature rules.
139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
import { initializeTerminalComponent } from './terminal.js';
|
|
import { registerLivewireRequestFailureHandler } from './livewire-request-failure.js';
|
|
|
|
document.addEventListener('livewire:init', () => {
|
|
registerLivewireRequestFailureHandler(window.Livewire);
|
|
});
|
|
|
|
// Livewire 3.5.19+ re-applies `x-cloak` to morphed elements during wire:navigate
|
|
// (via replaceHtmlAttributes). With `[x-cloak]{display:none}` on the app wrapper,
|
|
// this blanks the whole page on every navigation until Alpine re-processes it.
|
|
// Strip leftover x-cloak after each navigation; the initial-load FOUC guard stays.
|
|
document.addEventListener('livewire:navigated', () => {
|
|
document.querySelectorAll('[x-cloak]').forEach((el) => el.removeAttribute('x-cloak'));
|
|
});
|
|
|
|
// Register the terminal data provider before Alpine initializes the page.
|
|
// Keeping this registration independent from the current route also makes it
|
|
// available before Alpine processes terminal markup after wire:navigate.
|
|
document.addEventListener('alpine:init', initializeTerminalComponent);
|
|
|
|
/**
|
|
* Smooth-scroll a settings section into view, then flash its border for 500ms
|
|
* after the scroll has settled. Starting the flash immediately makes long
|
|
* jumps (top → bottom) finish scrolling after the animation has already ended.
|
|
*
|
|
* @param {string} id
|
|
*/
|
|
window.scrollToSettingsSection = function scrollToSettingsSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
if (typeof el._sectionHighlightCleanup === 'function') {
|
|
el._sectionHighlightCleanup();
|
|
}
|
|
|
|
const runHighlight = () => {
|
|
el.classList.remove('is-section-highlight');
|
|
// Force reflow so the 500ms highlight can re-run on repeated clicks.
|
|
void el.offsetWidth;
|
|
el.classList.add('is-section-highlight');
|
|
el._sectionHighlightTimer = window.setTimeout(() => {
|
|
el.classList.remove('is-section-highlight');
|
|
}, 500);
|
|
};
|
|
|
|
let finished = false;
|
|
let rafId = 0;
|
|
let scrollEndHandler = null;
|
|
|
|
const cleanup = () => {
|
|
if (rafId) {
|
|
window.cancelAnimationFrame(rafId);
|
|
rafId = 0;
|
|
}
|
|
if (scrollEndHandler) {
|
|
window.removeEventListener('scrollend', scrollEndHandler);
|
|
scrollEndHandler = null;
|
|
}
|
|
if (el._sectionHighlightTimer) {
|
|
window.clearTimeout(el._sectionHighlightTimer);
|
|
el._sectionHighlightTimer = null;
|
|
}
|
|
};
|
|
|
|
const finish = () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
finished = true;
|
|
cleanup();
|
|
runHighlight();
|
|
};
|
|
|
|
el._sectionHighlightCleanup = () => {
|
|
finished = true;
|
|
cleanup();
|
|
el.classList.remove('is-section-highlight');
|
|
el._sectionHighlightCleanup = null;
|
|
};
|
|
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
// Prefer the native scrollend event when the browser fires it.
|
|
scrollEndHandler = () => finish();
|
|
window.addEventListener('scrollend', scrollEndHandler, { once: true });
|
|
|
|
// Fallback: wait until the target's Y position is stable for a few frames
|
|
// (covers browsers without scrollend, and no-op scrolls when already in view).
|
|
let lastTop = null;
|
|
let stableFrames = 0;
|
|
let frames = 0;
|
|
const maxFrames = 180; // ~3s safety cap
|
|
|
|
const tick = () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
frames += 1;
|
|
const top = el.getBoundingClientRect().top;
|
|
|
|
if (lastTop !== null && Math.abs(top - lastTop) < 0.5) {
|
|
stableFrames += 1;
|
|
} else {
|
|
stableFrames = 0;
|
|
}
|
|
lastTop = top;
|
|
|
|
// Skip the first couple frames so we don't flash before smooth scroll starts.
|
|
if (frames > 4 && stableFrames >= 4) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
if (frames >= maxFrames) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
rafId = window.requestAnimationFrame(tick);
|
|
};
|
|
|
|
rafId = window.requestAnimationFrame(tick);
|
|
};
|
|
|
|
// When a settings sub-section link navigates across pages (href="route#section-id"),
|
|
// scroll to that section once the destination page has rendered.
|
|
function scrollToHashSettingsSection() {
|
|
const hash = window.location.hash;
|
|
if (!hash || hash.length < 2) {
|
|
return;
|
|
}
|
|
const id = decodeURIComponent(hash.slice(1));
|
|
window.requestAnimationFrame(() => window.scrollToSettingsSection?.(id));
|
|
}
|
|
document.addEventListener('livewire:navigated', scrollToHashSettingsSection);
|
|
document.addEventListener('DOMContentLoaded', scrollToHashSettingsSection);
|