Skip to content

The collapsed sidebar rail, and Focus's front door - #11

Merged
malinfossum merged 17 commits into
mainfrom
feat/sidebar-rail
Aug 17, 2026
Merged

The collapsed sidebar rail, and Focus's front door#11
malinfossum merged 17 commits into
mainfrom
feat/sidebar-rail

Conversation

@malinfossum

Copy link
Copy Markdown
Owner

Rebuilds the collapsed 48px sidebar as a status-and-navigation rail with no loss of function, and promotes the wordmark into the Focus hero card. Implements docs/superpowers/specs/2026-08-17-ignite-sidebar-rail-design.md (follow-up items 5 and 6).

What changed

  • Rail tiles: 44px hit targets painted as 36px tiles, centred in the rail; the active tint lives on the tile, so nothing overflows the 48px column.
  • Accessible names moved onto the buttons via aria-label with painted children aria-hidden — area buttons previously had no accessible name at all when collapsed.
  • Count badges: each tile carries the same open-task count as the expanded row, as a corner badge; zero is not painted. Contrast measured in both themes (4.66:1 light / 6.43:1 dark after fixing a 4.38:1 AA failure the spec's own default value carried).
  • Focus hero card: the wordmark is now a bordered, tinted card with the day's counts — still one control, so Plan 3's single-door decision stands. Counts derive from the new shared src/utils/focus-counts.js, used by the page header, the Focus tabs and the card, so the three cannot disagree.
  • ⋯ menu in the rail: corner trigger revealed by opacity only (never display/visibility, so it stays in the tab order), permanently visible under @media (hover: none), popovers layered above the capture picker and toast.
  • Rename works collapsed: the field and icon picker open as a popover beside the tile; the expanded→collapsed force-close is deleted, so collapsing mid-rename no longer commits it.
  • New areas are usable: onAddArea seeds a "Tasks" section (composed in the controller, keeping one-model-one-store).
  • Toggle painted as a rail tile per spec §3.6.
  • Reduced-motion now covers the collapse animation (body carries the grid transition).

Verification

258 tests / 17 files, Biome clean. Spec §10 records the sweep: 10 of 12 manual steps PASS plus light-theme repeats; reduced-motion and coarse-pointer-at-desktop are recorded as NOT VERIFIED (tooling cannot emulate them) with static CSS reads instead. Still owed on a real device: Tab traversal, Enter on the ⋯ menu, arrow keys in the icon picker. Axe was not run (axe-core deliberately not added); a manual DOM/ARIA/contrast check stands in.

The 48px rail was never designed — it is the expanded sidebar with three
display:none rules switching things off, which is why area buttons have no
accessible name at all, renaming is unusable, and the active tint overflows
a column that only has 16px of content width.

Rebuild it as a status-and-navigation strip with no loss of function: one DOM
rendered as a CSS variant (sidebarCollapsed is also true on mobile, so the
view cannot branch on it), labels clipped instead of removed, counts as corner
badges, the menu trigger as a hover-revealed corner affordance, and rename as
a popover anchored to the tile. The wordmark becomes the Focus hero card,
giving the landing surface a real front door without adding a second control
pointing at one page.

Stress-tested across security, privacy, accessibility and loopholes; 11
findings folded in, including the hover-reveal being dead on touchscreen
laptops, the sidebar toggle never being specified despite being the only way
out of the rail, and overflow-y:visible being load-bearing for popover escape.

Covers follow-up items 5 and 6. Item 3 (section titles) is a named non-goal.
Two claims survived the stress-test edits into the summary rows while the
body already contradicted them: the header still listed item 3 as covered
when §9 makes it a non-goal, and D5 still said hover-reveal "costs nothing
on touch" when §3.3 now specifies @media (hover: none) precisely because a
touchscreen laptop gets the rail with no hover state.
Nine tasks, each with its own test cycle and commit: the shared focus-counts
helper, accessible names, new-area section seeding, rail geometry, count
badges, the Focus hero card, the corner menu trigger with its motion and
coarse-pointer guards, rename as a popover, and the verification sweep.

Orders the accessibility fix ahead of every CSS task so no later change can
reintroduce the unnamed-button defect.
The focusSectionIds filter existed byte-for-byte in controller.js and
views/focus.js. The sidebar hero card needs the same numbers, and a third
copy is how the page header and the sidebar start disagreeing on screen.
Area buttons had no accessible name at all when collapsed: the icon span is
aria-hidden and the label and count were display:none, which removed the
button's entire name. Clipping the label cannot fix it, because that span is
the visible label when expanded. aria-label states the name once, correctly,
in both states.
onAddArea created an area with no section, so nothing could be filed into it
and it was not a valid move target until a section was added by hand.
The rail kept #sidebar's 1rem padding, leaving 48px with 16px of content
width, so tiles, the wordmark and the active tint all overflowed it. Pad the
rail itself, centre its children, and make the icon the painted 36px tile
while the button stays a 44px target. The toggle and + become tiles instead
of an unstyled box and a hidden row.
…y order

Task 4 stopped hiding .sidebar__menu-btn but its out-of-flow positioning
doesn't land until Task 7, so a real area row centred two 44px children
(the area tile and the menu button) inside the 48px rail and both bled off
the edges. Restore the hide for both .sidebar__menu-btn and
.sidebar__rename-input, per the plan's corrected Task 4 Step 2, with a
comment explaining the hide and Task 7's position: absolute are one change
split across two tasks.

Also move the collapsed-rail block after the base .sidebar__* rules it
overrides instead of before them, so specificity stays ascending in source
order (matching the convention already documented elsewhere in this file)
and Biome's 5 noDescendingSpecificity warnings clear without touching any
selector or declaration.
…tile

.sidebar__icon declared inline-size/block-size: 36px in the collapsed
rail, but flex children default to flex-shrink: 1, so the still-visible
.sidebar__count sibling was squeezing it to ~28px. Add flex-shrink: 0 so
the tile actually renders at its declared 36px, since Task 5's badge
positioning is specified against that real size.

Plan doc corrected to match (docs/superpowers/plans/2026-08-17-ignite-sidebar-rail.md).
.sidebar__count was still a flex sibling of the icon inside the 44px
button. Fixing the icon to a true 36px (flex-shrink: 0) exposed the
same root cause differently: the count's min-content width now pushed
the row past 44px, bleeding the icon ~2.3px past the rail's left edge
and the count ~1.3px past the right. Add .sidebar__count to the
collapsed display: none rule alongside .sidebar__name and
.sidebar__add-text; Task 5 un-hides it in the same change that gives
it position: absolute.
Same number the expanded row shows, inset so it cannot overhang the rail's
border. Zero is not painted in the rail.
Focus is the landing surface but had no visible front door — the wordmark
read as a logo. Promote it into a bordered, tinted card carrying the day's
counts, still one control so there is no second door onto the same page.
The ⋯ was display:none when collapsed, which removed it from the tab order
entirely. Move it to the tile's corner, reveal it with opacity so it stays
keyboard-reachable, keep it permanently visible where hover does not exist,
and anchor rail popovers above the capture picker and toast layers.

Also brings the collapse animation under prefers-reduced-motion: it lives on
body, which the existing block did not cover.
The rename field was display:none while the row still entered rename mode, so
the user was put into a state with nothing to type into and focus fell to
<body>. Position the field and the icon picker beside the tile instead.

Drops the expanded-to-collapsed force-close with it: it existed only because
collapsing destroyed the input, and keeping it would now commit a rename the
user is still typing.
…ents

- .sidebar__home-badge used --surface-1 on --accent (4.38:1 in light
  theme, under the 4.5:1 AA floor) — copied from .sidebar__count
  before that same pairing was fixed to --surface-2. Match it.
- Delete the dead `display: none` rule for .sidebar__menu-btn: an
  identical selector later in the same block already sets
  display: block/position: absolute, so the hide never did anything.
  Rewrite the block comment above it to describe the actual mechanism
  (opacity-only reveal, out of flow via the live rule).
- Rewrite the .sidebar__icon flex-shrink comment: it cited a "until
  Task 5" state that no longer exists now the count is out of flow.
- Spec: replace remaining "clipped label" references (§3.7, §8, §9)
  with the aria-label mechanism that actually shipped (D3/§3.3);
  leave D3's own rationale, which correctly describes the rejected
  alternative, as is.
Spec 3.6 specifies the toggle as a rail tile like any other: a 44px hit
target with a 36px painted surface. It shipped with the 44px box and
border: 0, leaving it the only rail control with no painted affordance and
a hover that was a colour shift on a bare glyph. The glyph span becomes the
painted 36px tile, mirroring .sidebar__icon.

Verified in the rail at 1926px: toggle 44x44 at [1.5, 45.5], glyph 36x36,
radius 10, surface-3 paint; 258 tests and Biome clean.
@malinfossum
malinfossum merged commit 17f00b6 into main Aug 17, 2026
1 check passed
@malinfossum malinfossum mentioned this pull request Aug 17, 2026
@malinfossum
malinfossum deleted the feat/sidebar-rail branch August 17, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant