From cfba3f40ed33dd2c081a8c8ea063fc8b10087cf7 Mon Sep 17 00:00:00 2001 From: fs Date: Thu, 23 Apr 2026 23:50:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(responsive):=20mobile=20polish=20=E2=80=94?= =?UTF-8?q?=20topbar,=20drawer,=20footer,=20tabs,=20notification=20dropdow?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- config/assets.php | 1 + .../components/app-notification-dropdown.css | 27 +++- web/css/base/variables.base.css | 12 ++ web/css/components/app-breadcrumb.css | 9 ++ web/css/components/app-detail-drawer.css | 2 +- web/css/components/app-details.css | 8 +- web/css/components/app-footer.css | 106 +++++++++++++++ web/css/components/app-list-tabs.css | 122 +++++++++++++----- web/css/components/app-list-toolbar.css | 29 +++++ web/css/components/app-page-editor.css | 2 +- web/css/core.css | 2 + web/css/layout/app-aside-icon-bar.css | 53 +++----- web/css/layout/app-shell.css | 23 ++-- web/css/layout/app-sidebar.css | 14 +- web/css/layout/app-topbar.css | 71 ++++++++-- web/js/app-init.js | 13 ++ web/js/components/app-list-tabs.js | 91 +++++++++++++ 17 files changed, 472 insertions(+), 113 deletions(-) create mode 100644 web/css/components/app-footer.css create mode 100644 web/js/components/app-list-tabs.js diff --git a/config/assets.php b/config/assets.php index c043afb..25113e8 100644 --- a/config/assets.php +++ b/config/assets.php @@ -18,6 +18,7 @@ return [ 'css/components/app-flash.css', 'css/components/app-brand.css', 'css/components/app-file-upload.css', + 'css/components/app-footer.css', ], 'core' => [ 'css/core.css', diff --git a/modules/notifications/web/css/components/app-notification-dropdown.css b/modules/notifications/web/css/components/app-notification-dropdown.css index f471e13..820abc0 100644 --- a/modules/notifications/web/css/components/app-notification-dropdown.css +++ b/modules/notifications/web/css/components/app-notification-dropdown.css @@ -1,6 +1,5 @@ @layer components { ul.app-notification-dropdown { - width: 360px; padding: 0; --app-notification-text: var(--app-dropdown-color); --app-notification-muted: var(--app-muted-color); @@ -9,6 +8,32 @@ --app-notification-focus: var(--app-primary-focus); } + /* Dropdown sizing — must out-specify the global + `details.dropdown > summary + ul { width: 100%; min-width: fit-content }` + rule in app-shell.css, otherwise the content's intrinsic width wins and + the dropdown overflows the viewport on mobile. */ + .app-header details.dropdown > summary + ul.app-notification-dropdown { + width: min(360px, calc(100vw - var(--app-spacing) * 2)); + max-width: min(360px, calc(100vw - var(--app-spacing) * 2)); + min-width: 0; + } + + /* Below md, the bell is NOT the rightmost topbar item (bookmark / tenant / + account sit to the right of it). Anchoring the dropdown with `right: 0` + relative to the bell's
therefore lands its left edge off-viewport + on narrow screens. Switch to viewport-anchored `position: fixed` so the + dropdown hugs the viewport's right edge instead of the bell. */ + @media (max-width: 767px) { + .app-header details.dropdown > summary + ul.app-notification-dropdown { + position: fixed; + top: var(--app-topbar-height); + right: var(--app-spacing); + left: auto; + inset-inline-end: var(--app-spacing); + inset-inline-start: auto; + } + } + .app-notification-dropdown__header { padding: 12px 16px; border-bottom: 1px solid var(--app-notification-border); diff --git a/web/css/base/variables.base.css b/web/css/base/variables.base.css index 3da32dd..ccf48f2 100644 --- a/web/css/base/variables.base.css +++ b/web/css/base/variables.base.css @@ -81,6 +81,18 @@ --z-drawer: 95; --z-drawer-backdrop: 90; + /* + * Responsive breakpoint system — canonical values, align all @media rules to these. + * CSS custom props cannot be used inside @media queries, so these are documentation only. + * Use them verbatim in px. Do not introduce one-off breakpoints (640, 720, 900, 968, 1100, ...). + * --bp-xs: 400px — tiny phones (iPhone SE, Pixel 4a portrait) + * --bp-sm: 576px — large phones / phablets + * --bp-md: 768px — tablets portrait, sidebar/drawer switch + * --bp-lg: 1024px — tablets landscape / small laptops, two-column details activates + * --bp-xl: 1280px — desktops + * --bp-2xl: 1536px — large desktops + */ + /* Typography tokens defined in typography.tokens.css */ } diff --git a/web/css/components/app-breadcrumb.css b/web/css/components/app-breadcrumb.css index 23737f9..9ae7045 100644 --- a/web/css/components/app-breadcrumb.css +++ b/web/css/components/app-breadcrumb.css @@ -90,4 +90,13 @@ font-weight: var(--font-medium); pointer-events: none; } + + /* Breadcrumb is desktop-only chrome — the hamburger drawer owns navigation + below md. Must live in the components layer to override .app-breadcrumb's + own display:flex (cross-layer specificity does not apply). */ + @media (max-width: 767px) { + .app-breadcrumb { + display: none; + } + } } diff --git a/web/css/components/app-detail-drawer.css b/web/css/components/app-detail-drawer.css index 5216796..38ea068 100644 --- a/web/css/components/app-detail-drawer.css +++ b/web/css/components/app-detail-drawer.css @@ -117,7 +117,7 @@ /* Inside the drawer, force single-column layout for any .app-details-container regardless of viewport width (the grid rule in app-details.css triggers on - viewport ≥968px, which is wrong for a 720px drawer). Any page that uses the + viewport ≥1024px, which is wrong for a 720px drawer). Any page that uses the standard details container gets stacked main + aside automatically. */ .app-detail-drawer .app-details-container, .app-detail-drawer .app-details-container:has(> aside) { diff --git a/web/css/components/app-details.css b/web/css/components/app-details.css index dd05fdf..32fd45d 100644 --- a/web/css/components/app-details.css +++ b/web/css/components/app-details.css @@ -5,7 +5,7 @@ } .app-details-container > section > form [data-tab-panel] { - max-width: 70ch; + max-width: min(70ch, 100%); } .app-details-container > section > form [data-tab-panel-wide] { @@ -137,11 +137,7 @@ } } - @media (min-width: 968px) { - /* .app-details-container > section { - padding: calc(var(--app-spacing) * 2) calc(var(--app-spacing) * 2) - calc(var(--app-spacing) * 2) calc(var(--app-spacing) * 0); - } */ + @media (min-width: 1024px) { .app-details-container:has(> aside) { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 300px); diff --git a/web/css/components/app-footer.css b/web/css/components/app-footer.css new file mode 100644 index 0000000..201b9dc --- /dev/null +++ b/web/css/components/app-footer.css @@ -0,0 +1,106 @@ +@layer components { + /* Site footer — utility nav with copyright, legal links, language switcher. + Shared between default shell, login, and public page layouts. Mobile-first. */ + footer.site-footer { + border-top: 1px solid var(--app-muted-border-color); + padding-block: var(--app-spacing); + padding-inline: var(--app-spacing); + color: var(--app-muted-color); + } + + footer.site-footer > nav { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: calc(var(--app-spacing) * 0.5) var(--app-spacing); + overflow: visible; + } + + footer.site-footer ul { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: calc(var(--app-spacing) * 0.25) calc(var(--app-spacing) * 0.75); + margin: 0; + padding: 0; + list-style: none; + } + + footer.site-footer li { + margin: 0; + padding: 0; + display: inline-flex; + align-items: center; + } + + /* Reset Pico's zero-width-space before-marker on nav li. */ + footer.site-footer li::before { + content: none; + } + + footer.site-footer a { + color: var(--app-muted-color); + text-decoration: none; + padding-inline: calc(var(--app-spacing) * 0.25); + padding-block: calc(var(--app-spacing) * 0.25); + margin: 0; + border-radius: var(--app-border-radius); + transition: + color var(--app-transition), + background-color var(--app-transition); + } + + footer.site-footer a:hover { + color: var(--app-color); + background: color-mix(in srgb, var(--app-contrast) 6%, transparent); + } + + footer.site-footer a:focus-visible { + outline: 2px solid var(--app-primary); + outline-offset: -2px; + color: var(--app-color); + } + + footer.site-footer small { + font-size: var(--text-xs); + line-height: var(--leading-tight); + } + + /* Language switcher (inline