diff --git a/apps/web/src/layout/AppLayout.tsx b/apps/web/src/layout/AppLayout.tsx
index cd0ab18..afae63a 100644
--- a/apps/web/src/layout/AppLayout.tsx
+++ b/apps/web/src/layout/AppLayout.tsx
@@ -64,7 +64,10 @@ export function AppLayout(): React.JSX.Element {
{!collapsed && }
-
+ {/* tabIndex: the main area is the app's scroll container; on
+ pages without focusable content (e.g. legal texts) keyboard
+ users could otherwise not scroll it (#165, WCAG 2.1.1). */}
+
{/* Outside the scroll container: always visible at the bottom
diff --git a/apps/web/src/layout/TopBar.tsx b/apps/web/src/layout/TopBar.tsx
index ee9fff5..577d42f 100644
--- a/apps/web/src/layout/TopBar.tsx
+++ b/apps/web/src/layout/TopBar.tsx
@@ -96,7 +96,8 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
onClick={() => setSearchOpen(true)}
aria-label={t('search:open')}
>
- {t('search:open')}
+
+ {t('search:open')}
)}
{searchOpen && user && setSearchOpen(false)} />}
diff --git a/apps/web/src/styles/base.css b/apps/web/src/styles/base.css
index 3cedb5a..4dc4299 100644
--- a/apps/web/src/styles/base.css
+++ b/apps/web/src/styles/base.css
@@ -53,7 +53,9 @@ button {
/* Layout skeleton */
.app {
display: grid;
- grid-template-rows: var(--topbar-height) 1fr;
+ /* minmax: the topbar row may grow when its controls wrap on narrow
+ viewports (issue #165, WCAG 1.4.10). */
+ grid-template-rows: minmax(var(--topbar-height), auto) 1fr;
height: 100%;
}
@@ -70,6 +72,10 @@ button {
align-items: center;
gap: var(--space-4);
padding: 0 var(--space-4);
+ /* Same #100 pattern as .app-body: without this the topbar's min-content
+ width sizes the shared grid column and forces page-level horizontal
+ scrolling on narrow viewports (issue #165, WCAG 1.4.10). */
+ min-width: 0;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
/* Own stacking context above the scrolling `.main` content: the nav comes
@@ -91,6 +97,39 @@ button {
flex: 1;
}
+/* Narrow viewports (issue #165, WCAG 1.4.10): the topbar wraps onto a
+ second row instead of pushing the page wider than the viewport; every
+ control stays visible and reachable. The search button drops to its
+ icon (the aria-label carries the name). The expanded sidebar becomes
+ an overlay above the content — 256 px beside a 320 px viewport would
+ leave the content a sliver and force page-level horizontal scrolling. */
+@media (max-width: 40rem) {
+ .topbar {
+ flex-wrap: wrap;
+ row-gap: 0;
+ padding: var(--space-1) var(--space-2);
+ }
+
+ .topbar__search-text {
+ display: none;
+ }
+
+ .app-body {
+ position: relative;
+ }
+
+ .sidebar:not(.sidebar--collapsed) {
+ position: absolute;
+ inset: 0 auto 0 0;
+ z-index: 25;
+ box-shadow: 2px 0 8px rgb(0 0 0 / 0.15);
+ }
+
+ .sidebar-resizer {
+ display: none;
+ }
+}
+
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
@@ -715,6 +754,9 @@ button {
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
+ /* Wrap instead of widening the page on narrow viewports (#165). */
+ flex-wrap: wrap;
+ min-width: 0;
padding: var(--space-2) var(--space-8);
border-top: 1px solid var(--color-border);
background: var(--color-bg);
@@ -958,6 +1000,9 @@ button {
.editor-page__title {
flex: 1;
+ /* An input's intrinsic size-attribute width must not set the page's
+ minimum width on narrow viewports (#165). */
+ min-width: 0;
border: none;
background: none;
font-family: var(--font-heading);
@@ -3784,6 +3829,9 @@ ul[data-type='task_list'] li p:last-of-type {
@media (max-width: 60rem) {
.settings-layout {
flex-direction: column;
+ /* Override the row layout's flex-start: stacked, the content column
+ must stretch to the viewport instead of its widest control (#165). */
+ align-items: stretch;
}
.settings-nav {