Unifies the breakpoint system and fixes cross-layer CSS bugs that kept the old mobile hide rules from ever taking effect. Breakpoints: - Document canonical xs/sm/md/lg/xl/2xl scale in variables.base.css - Replace 968px one-offs with 1024px (app-details, app-page-editor) - Make details tab-panel max-width responsive (min(70ch, 100%)) Topbar (< md): - Hide breadcrumb in components layer so cross-layer overrides actually win - Dock search icon alongside right-column icons via flex layout - Add xs (400px) tightening for hit-area preservation - Hide brand divider alongside hidden brand name - Switch hover states to theme-aware --app-dropdown-hover-background-color Mobile drawer: - Icon-bar now vertical like desktop (was horizontal top strip, effectively invisible) - Sidebar shifted 52px right so icon-bar and sidebar slide in as one 280px unit - Add breathing room above first nav item (20px top padding) List tabs: - Rewrite .app-list-tabs to horizontal scroll with hidden scrollbar, snap, and mask-image gradient fades; add initListTabs to scroll active into view - Register as runtime component in app-init.js List toolbar: - Stack wide controls (multi-select, search/text inputs) full-width below sm, keep compact controls at natural width Footer: - New dedicated app-footer.css with mobile-first stacking, border-top separator, theme-aware hover; remove redundant rules from app-shell.css Notification dropdown: - Width with min(360px, 100vw - 2rem) and min-width: 0 override global details > ul rule; switch to position: fixed below md so it hugs the viewport right edge instead of the non-rightmost bell button Content spacing: - Add padding-block-start to .app-main-content on mobile (was 0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
2.9 KiB
JavaScript
92 lines
2.9 KiB
JavaScript
/**
|
|
* List page tab bar — horizontal scroll behavior.
|
|
* Keeps tabs on a single line, scrolls the active tab into view on mount,
|
|
* and toggles has-overflow/at-start/at-end classes that drive the CSS
|
|
* gradient fade masks.
|
|
*/
|
|
import { resolveHost } from '../core/app-dom.js';
|
|
|
|
const BOUND_ATTR = 'data-app-list-tabs-bound';
|
|
const EDGE_TOLERANCE = 1;
|
|
|
|
const updateOverflow = (el) => {
|
|
const hasOverflow = el.scrollWidth - el.clientWidth > EDGE_TOLERANCE;
|
|
el.classList.toggle('has-overflow', hasOverflow);
|
|
if (!hasOverflow) {
|
|
el.classList.remove('at-start', 'at-end');
|
|
return;
|
|
}
|
|
const atStart = el.scrollLeft <= EDGE_TOLERANCE;
|
|
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - EDGE_TOLERANCE;
|
|
el.classList.toggle('at-start', atStart);
|
|
el.classList.toggle('at-end', atEnd);
|
|
};
|
|
|
|
const scrollActiveIntoView = (el) => {
|
|
const active = el.querySelector('.is-active');
|
|
if (!active) {
|
|
return;
|
|
}
|
|
const activeCenter = active.offsetLeft + active.offsetWidth / 2;
|
|
const target = activeCenter - el.clientWidth / 2;
|
|
const max = el.scrollWidth - el.clientWidth;
|
|
el.scrollLeft = Math.max(0, Math.min(target, max));
|
|
};
|
|
|
|
export function initListTabs(root = document, options = {}) {
|
|
const selector = String(options.selector || '.app-list-tabs').trim() || '.app-list-tabs';
|
|
const host = resolveHost(root);
|
|
const cleanupFns = [];
|
|
const bound = [];
|
|
|
|
const tabs = Array.from(host.querySelectorAll(selector));
|
|
tabs.forEach((el) => {
|
|
if (el.getAttribute(BOUND_ATTR) === '1') {
|
|
return;
|
|
}
|
|
el.setAttribute(BOUND_ATTR, '1');
|
|
bound.push(el);
|
|
|
|
// Initial scroll-into-view uses instant positioning (no smooth behavior
|
|
// before the strip has finished layout — smooth kicks in on user scroll).
|
|
const previousBehavior = el.style.scrollBehavior;
|
|
el.style.scrollBehavior = 'auto';
|
|
scrollActiveIntoView(el);
|
|
el.style.scrollBehavior = previousBehavior;
|
|
|
|
updateOverflow(el);
|
|
|
|
const onScroll = () => updateOverflow(el);
|
|
el.addEventListener('scroll', onScroll, { passive: true });
|
|
cleanupFns.push(() => el.removeEventListener('scroll', onScroll));
|
|
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
const resizeObserver = new ResizeObserver(() => updateOverflow(el));
|
|
resizeObserver.observe(el);
|
|
cleanupFns.push(() => resizeObserver.disconnect());
|
|
} else {
|
|
const onResize = () => updateOverflow(el);
|
|
window.addEventListener('resize', onResize, { passive: true });
|
|
cleanupFns.push(() => window.removeEventListener('resize', onResize));
|
|
}
|
|
});
|
|
|
|
const destroy = () => {
|
|
cleanupFns.forEach((fn) => {
|
|
try {
|
|
fn();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
});
|
|
cleanupFns.length = 0;
|
|
bound.forEach((el) => {
|
|
el.removeAttribute(BOUND_ATTR);
|
|
el.classList.remove('has-overflow', 'at-start', 'at-end');
|
|
});
|
|
bound.length = 0;
|
|
};
|
|
|
|
return { destroy };
|
|
}
|