Keep shell sidebar at 208 px including its padding
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (push) Successful in 1m32s
CI / ci (pull_request) Successful in 1m31s

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:36:21 -04:00
co-authored by Claude Opus 5.5
parent 93e209c302
commit f922b4457e
5 changed files with 37 additions and 1 deletions
+9 -1
View File
@@ -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)')
+1
View File
@@ -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;
+1
View File
@@ -1,6 +1,7 @@
.rail {
width: 96px;
padding: 24px 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
+1
View File
@@ -1,6 +1,7 @@
.sidebar {
width: 208px;
padding: 28px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: var(--color-surface);
+25
View File
@@ -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,
)
}