From f922b4457eb1dad88b784463607da6a5e8621a77 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 18:36:21 -0400 Subject: [PATCH] Keep shell sidebar at 208 px including its padding The desktop sidebar declared width: 208px with 16px side padding under content-box sizing, so it rendered 240 px wide. Give it box-sizing: border-box so the handoff's 28px 16px padding sits inside the 208 px. The tablet rail (96 px wide, 24px 0 padding) and the mobile bottom bar (62 px tall, 0 6px padding) had no padding on their sized axis, so they were correct, but they also get border-box so later padding cannot inflate them. The "M" mark (48 px) and rail pills (56x36) have no padding or border. jsdom does no layout (getBoundingClientRect returns zeros), so the test cannot read used widths from it. A new outerSize() helper in src/test/cssRules.ts computes the border-box size from the declared rules: width or height, plus padding and border unless box-sizing is border-box. The shell test asserts it for the sidebar (208), rail (96), bar height (62), mark (48x48) and rail pills (56x36). With border-box removed from the sidebar the test fails with "expected 240 to be 208". Co-Authored-By: Claude Opus 5.5 --- web/src/shell/AppShell.test.tsx | 10 +++++++++- web/src/shell/BottomBar.module.css | 1 + web/src/shell/Rail.module.css | 1 + web/src/shell/Sidebar.module.css | 1 + web/src/test/cssRules.ts | 25 +++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/web/src/shell/AppShell.test.tsx b/web/src/shell/AppShell.test.tsx index 539231c..a7ba038 100644 --- a/web/src/shell/AppShell.test.tsx +++ b/web/src/shell/AppShell.test.tsx @@ -2,7 +2,7 @@ import { render, screen, within } from '@testing-library/react' import { MemoryRouter } from 'react-router' import { afterEach, describe, expect, it } from 'vitest' import App from '../App.tsx' -import { declaredStyle } from '../test/cssRules.ts' +import { declaredStyle, outerSize } from '../test/cssRules.ts' import { expectMinHitArea } from '../test/hitArea.ts' import { mockViewport } from '../test/matchMedia.ts' import bottomBarStyles from './BottomBar.module.css' @@ -69,6 +69,7 @@ describe('AppShell', () => { expect(bar.right).toBe('12px') expect(bar.bottom).toBe('calc(16px + env(safe-area-inset-bottom))') expect(bar.height).toBe('62px') + expect(outerSize(nav(), 'height')).toBe(62) expect(bar.background).toBe('var(--color-text)') expect(bar.borderRadius).toBe('var(--radius-pill)') }) @@ -111,9 +112,12 @@ describe('AppShell', () => { expect(screen.getAllByRole('navigation')).toHaveLength(1) expect(nav()).toHaveClass(railStyles.rail!) expect(declaredStyle(nav()).width).toBe('96px') + expect(outerSize(nav(), 'width')).toBe(96) expect(declaredStyle(nav()).background).toBe('var(--color-surface)') const mark = within(nav()).getByText('M') expect(declaredStyle(mark).width).toBe('48px') + expect(outerSize(mark, 'width')).toBe(48) + expect(outerSize(mark, 'height')).toBe(48) expect(declaredStyle(mark).background).toBe('var(--color-accent)') expect(screen.queryByText('MealQuest')).not.toBeInTheDocument() expect(screen.queryByText('Our kitchen')).not.toBeInTheDocument() @@ -122,6 +126,8 @@ describe('AppShell', () => { const pill = link.firstElementChild! expect(declaredStyle(pill).width).toBe('56px') expect(declaredStyle(pill).height).toBe('36px') + expect(outerSize(pill, 'width')).toBe(56) + expect(outerSize(pill, 'height')).toBe(36) expect(declaredStyle(link).fontSize).toBe('11px') expect(declaredStyle(link).fontWeight).toBe('700') expectMinHitArea(link) @@ -138,6 +144,8 @@ describe('AppShell', () => { expect(declaredStyle(sidebar).width).toBe('208px') expect(declaredStyle(sidebar).background).toBe('var(--color-surface)') expect(declaredStyle(sidebar).padding).toBe('28px 16px') + // The 16 px side padding sits inside the 208 px, not outside it. + expect(outerSize(sidebar, 'width')).toBe(208) const wordmark = within(sidebar).getByText('MealQuest') expect(declaredStyle(wordmark).fontFamily).toBe('var(--font-heading)') diff --git a/web/src/shell/BottomBar.module.css b/web/src/shell/BottomBar.module.css index 92d8cb7..6d4e044 100644 --- a/web/src/shell/BottomBar.module.css +++ b/web/src/shell/BottomBar.module.css @@ -5,6 +5,7 @@ bottom: calc(16px + env(safe-area-inset-bottom)); height: 62px; padding: 0 6px; + box-sizing: border-box; display: flex; justify-content: space-around; align-items: center; diff --git a/web/src/shell/Rail.module.css b/web/src/shell/Rail.module.css index d84eefd..f9da188 100644 --- a/web/src/shell/Rail.module.css +++ b/web/src/shell/Rail.module.css @@ -1,6 +1,7 @@ .rail { width: 96px; padding: 24px 0; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; diff --git a/web/src/shell/Sidebar.module.css b/web/src/shell/Sidebar.module.css index 05f223c..4aa11ce 100644 --- a/web/src/shell/Sidebar.module.css +++ b/web/src/shell/Sidebar.module.css @@ -1,6 +1,7 @@ .sidebar { width: 208px; padding: 28px 16px; + box-sizing: border-box; display: flex; flex-direction: column; background: var(--color-surface); diff --git a/web/src/test/cssRules.ts b/web/src/test/cssRules.ts index e8413aa..3260596 100644 --- a/web/src/test/cssRules.ts +++ b/web/src/test/cssRules.ts @@ -48,3 +48,28 @@ export function animationKeyframes(element: Element): CSSKeyframesRule { if (!keyframes) throw new Error(`no @keyframes named ${JSON.stringify(name)}`) return keyframes } + +function px(value: string, what: string): number { + if (value === '') return 0 + const match = /^(\d+(?:\.\d+)?)px$/.exec(value) + if (!match) throw new Error(`${what} ${JSON.stringify(value)} is not a px length`) + return Number(match[1]) +} + +// The border-box size the element's declared rules give it on one axis: +// width or height, plus padding and border unless box-sizing is border-box. +// jsdom does no layout (getBoundingClientRect is all zeros), so this is the +// way to catch padding that would push a fixed box past its specified size. +export function outerSize(element: Element, axis: 'width' | 'height'): number { + const style = declaredStyle(element) + const size = px(style.getPropertyValue(axis), axis) + if (style.boxSizing === 'border-box') return size + const sides = axis === 'width' ? ['left', 'right'] : ['top', 'bottom'] + return sides.reduce( + (total, side) => + total + + px(style.getPropertyValue(`padding-${side}`), `padding-${side}`) + + px(style.getPropertyValue(`border-${side}-width`), `border-${side}-width`), + size, + ) +}