Skip to content

style(web): wrap the calendar in one frame and dissolve the header into it - #956

Draft
NatnaelTaddese wants to merge 3 commits into
feature/events-cardsfrom
feature/calendar-unified-frame
Draft

style(web): wrap the calendar in one frame and dissolve the header into it#956
NatnaelTaddese wants to merge 3 commits into
feature/events-cardsfrom
feature/calendar-unified-frame

Conversation

@NatnaelTaddese

@NatnaelTaddese NatnaelTaddese commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #938 — please merge that first.

Why

On /dashboard the calendar pane reads as two unrelated components stacked on each other, and the cause is a grammar inversion between the two panes:

  • Sidebar — flat header (SyncStatus, EventGraph sit directly on bg-background, no border) above a gap-1.5 stack of rounded-2xl elevated cards.
  • Calendar pane — the inverse: a rounded-2xl elevated card header floating 6px above a bare, edge-faded grid.

So the header was the only carded header in the app, sitting on the only bare body. The gap-1.5 and the grid's mx-px border compensation were patches over that seam rather than fixes for it.

What changed

One border now wraps the entire calendar, and inside it the toolbar band is lifted on bg-background-elevated and dissolves downward into the page-colored grid. The outline stops belonging to the header and starts belonging to the calendar.

  • calendar-frame.tsx — border/rounding/shadow move from the <header> to the outer container, which stays transparent so its interior inherits bg-background and matches the sticky hour gutter. The toolbar row takes bg-background-elevated; the column-header row gets an aria-hidden masked fill layer behind it.
  • week-grid.tsx / month-grid.tsx — the grids' vertical edge ramp becomes bottom-only.

Two decisions worth flagging for review:

The fade is scoped to the column-header element, not to a percentage of the header. The week strip's column header is HEADER_HEIGHT + bandHeight (it grows with the all-day band added in #938); the month grid's is unsized, ~30px. A percentage stop would land mid-toolbar in one view and below it in the other. Scoping it to the column-header row makes "dissolve from just under the toolbar" true in both, with no constant to keep in sync, and it stretches with the all-day band on its own. All-day pills render in the relative layer above the fill, so they stay fully opaque.

Both grids fade at the bottom only. Left as they were, the grid would wash in over its top 24px exactly where the header dissolves out, giving a pale dead band at the merge point. The day header's column-rule fade is deliberately untouched — its upward ramp runs opposite to the new surface fade over the same band, so the column rules strengthen as the fill evaporates. That counter-motion is the effect.

mx-px is gone: it existed only to compensate for the header card's own 1px border, and header and grid now share one content box inside a single border.

Verified

bun run types and bun run lint pass. Checked live in both themes and both views, signed in with events loaded.

  • Dark, week view — one outline encloses the calendar; the toolbar band reads as lifted and dissolves cleanly through the day header into the grid. Column rules stay continuous and aligned across the seam on all seven columns, so dropping mx-px did not shift them. The rules strengthen as the fill evaporates, which is the intended counter-motion.
  • Light, week and month view — frame, rules, bottom-edge fade, month grid's horizontal edge ramp and the emerald today pill all correct.
  • All-day band — on a week with all-day events the column header grows 56px → 78px and the fade stretches with it; pills render at full opacity above the dissolving fill, as intended.
  • Week↔month toggle — switches cleanly in both directions with no layout break at the frame.
  • Computed styles confirm the wiring — frame background: transparent with a 1px border at 16px radius, the toolbar on the elevated fill at a 15px top radius, and the fill layer masked with linear-gradient(rgb(0,0,0) 0%, rgba(0,0,0,0) 100%) under mask-composite: intersect.

Follow-up: these styles are now Tailwind, not inline

Review asked why any of this was written as inline style={{}} when the app is Tailwind-first (4 arbitrary-property usages in the whole codebase, and no mask-* utilities anywhere). There was no reason: tailwindcss@4.2.1 was already the pinned version when the first mask constant was written, and its edge-mask utilities shipped in 4.1.

Every mask, the inner radius and the four view-transition-names now use utilities. Each substitution was checked against the compiled CSS and against getComputedStyle on the live components:

was now
linear-gradient(to bottom, black, transparent) mask-b-from-0%
linear-gradient(to bottom, black calc(100% - 24px), transparent) mask-b-from-[calc(100%-24px)]
linear-gradient(to top, black 35%, transparent) mask-t-from-35%
the month grid's horizontal edge ramp mask-x-from-[calc(100%-24px)]
borderTop/BottomRadius: 15 rounded-t-/rounded-b-[calc(var(--radius-2xl)-1px)]
viewTransitionName: "calendar-header" [view-transition-name:calendar-header]

Two of these were worse as inline styles rather than merely different:

  • INNER_RADIUS = 15 was a magic number whose documented derivation from the frame's own radius was not enforced — change rounded-2xl and it goes quietly stale. calc(var(--radius-2xl) - 1px) keeps the derivation live, and still computes to 15px.
  • Each mask was written twice for -webkit-, and the month grid also hand-set maskComposite: "intersect" alongside the legacy WebkitMaskComposite: "source-in". Tailwind emits mask-composite: intersect for free; there is no browserslist here and Vite's baseline target is Safari 16, while unprefixed mask-image landed in 15.4. Zero -webkit- mask properties remain on the rendered tree.

maxHeight, the week grid's GUTTER_WIDTH/HEADER_HEIGHT geometry and the month grid's CELL_RULES stay inline — the first is a runtime prop and the rest are read by JS for scroll and layout math.

Net −39 lines. bun run types, bun run lint and all 468 tests pass. Verified visually against a session-free harness rendering the real WeekGrid and MonthGrid: the frame, the header dissolve, the bottom-only fades, the month grid's horizontal ramp and the week↔month transition all render as before, with the corners staying rounded mid-transition.

🤖 Generated with Claude Code

@NatnaelTaddese
NatnaelTaddese marked this pull request as draft August 27, 2026 15:21
NatnaelTaddese and others added 3 commits September 1, 2026 09:55
…to it

The header was the app's only carded header sitting on its only bare body:
a rounded-2xl elevated card floating 6px above an edge-faded grid, the
inverse of the sidebar's flat-header-over-card-stack grammar. The gap and
the grid's `mx-px` border compensation patched that seam rather than fixing
it.

Move the border, rounding and shadow from the header onto the frame so one
outline encloses the whole calendar, and lift only the toolbar band on
`bg-background-elevated`, dissolving it downward through the column header
into the page-colored grid.

The fade is scoped to the column-header element rather than a percentage of
the header, so it starts just under the toolbar in both views without a
constant to keep in sync, and stretches with the week view's all-day band.
Both grids now fade at the bottom edge only; a top ramp would wash in
exactly where the header dissolves out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

View transitions lift named elements into a flat overlay on the document
root rather than nesting them under their DOM ancestors, so the frame's
`overflow-hidden` and `rounded-2xl` stop clipping them mid-transition and
their filled children paint as squares overhanging the rounded corners.

The header used to carry both the rounding and the fill, so its snapshot
was already round; moving the rounding onto the unnamed frame left the
named children square. Round the two that reach a corner and have opaque
content: the toolbar at the top, and the grid at the bottom, where the
month view's last row of event pills is unaffected by the rule layer's
edge fade.

Both radii are the frame's 16px less its 1px border, which is exactly what
`overflow-hidden` already clipped them to, so nothing changes statically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The calendar hand-rolled every mask as an inline gradient string, wrote each
one twice for the -webkit- prefix, and in the month grid set mask-composite by
hand. Tailwind's edge-mask utilities shipped in 4.1 and 4.2.1 was already the
pinned version when these constants were written, so this was avoidable.

Each substitution emits the same CSS: mask-b-from-0%, mask-t-from-35%, and
mask-b/x-from-[calc(100%-24px)] compose to the same gradients under the
mask-composite: intersect the utilities set for themselves. Dropping the
-webkit- pairs is safe because Vite's baseline target is Safari 16 and
unprefixed mask-image landed in 15.4.

The frame's inner radius stops being the magic number 15 and becomes
calc(var(--radius-2xl) - 1px), so its stated derivation from the frame's own
radius survives a change to rounded-2xl instead of going quietly stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NatnaelTaddese
NatnaelTaddese force-pushed the feature/calendar-unified-frame branch from 6646d3b to c1dcb14 Compare September 1, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant