Skip to content

Make the calendar mobile-first and usable on small screens - #35

Merged
castroofelipee merged 8 commits into
mainfrom
feature/calendar-mobile-first
Aug 12, 2026
Merged

castroofelipee merged 8 commits into
mainfrom
feature/calendar-mobile-first

Conversation

@castroofelipee

Copy link
Copy Markdown
Owner

The calendar was built for desktop only. On a phone the sidebar took a fixed 16rem out of an already narrow viewport, and the week grid forced seven day columns into whatever was left — roughly 40px per day. Event details opened as a popover anchored beside the tapped block, which on a phone meant clamped against a screen edge, on top of the event it was describing.

Approach

Mobile-first with progressive enhancement, scoped to the calendar module rather than rewriting all 7k lines of globals.css. Base styles are the small-screen layout; @media (min-width: 48rem) layers the desktop layout on top.

Breakpoint decisions that can't be expressed in CSS (column count, whether to anchor a popover) go through a new useMediaQuery hook built on useSyncExternalStore. Its server snapshot is always false, so SSR and hydration both render the mobile baseline and the wider layout is applied after.

Changes

Week grid restructure — the day header, all-day row, and hour grid were three sibling grids, each with its own inline grid-template-columns. They stayed aligned only because none could scroll sideways. They now share one scroll container: the header block sticks to the top, the time gutter sticks to the

❯ me passa texto e descricao do PR

aligned only because none could scroll sideways. They now share one scroll container: the header block sticks to the top, the time gutter sticks to the left, and the column count travels as a --week-columns custom property so one stylesheet rule sizes every row. Narrow screens scroll columns sideways at a legible minimum width; from 48rem the minimum drops to zero and the grid fills the pane exactly as before.

Sidebar → drawer below 48rem: slides over the calendar, scrim and Escape close it, picking a day or creating an event closes it, inert while off-canvas so nothing off-screen sits in the tab order. Unchanged static column (including the collapsed rail) from 48rem up.

Viewport-driven defaults — view and sidebar state are null until the user chooses. A phone opens on a single day with the drawer shut; a desktop opens on the full week with the sidebar out; an explicit choice survives a resize or rotation.

Drag/resize gated on (pointer: fine) — draggable blocks set touch-action: none so the pointer handlers own the gesture, which on touch also swallowed vertical scrolling over every event and turned an imprecise tap into an accidental reschedule. Touch now falls back to tap-to-open-details.

Event details → bottom sheet below 48rem, with the anchoring measurement skipped entirely in that mode. Anchored popover unchanged above it.

Also: explicit viewport export (device-width, viewport-fit=cover, theme color), 44px touch targets, safe-area insets, prefers-reduced-motion on the drawer transition, compact app header with the wordmark hidden below 30rem, event dialog padding and stacked date/time fields on narrow screens.

Adds the missing .week-event-time-small rule — the markup referenced the class but the stylesheet never defined it. Pre-existing, but it only shows on short blocks, which is most of them once columns get narrow.

Tradeoff worth knowing

Mobile-first plus SSR means desktop renders day-view / collapsed-sidebar for one frame after hydration, and fires one extra events fetch on mount. Avoiding it needs either a blocking inline script or an accepted hydration mismatch — neither seemed worth it, but happy to revisit if the flash is annoying in practice.left, and the column count travels as a --week-columns custom property so one stylesheet rule sizes every row. Narrow screens scroll columns sideways at a legible minimum width; from 48rem the minimum drops to zero and the grid fills the pane exactly as before.

Sidebar → drawer below 48rem: slides over the calendar, scrim and Escape close it, picking a day or creating an event closes it, inert while off-canvas so nothing off-screen sits in the tab order. Unchanged static column (including the collapsed rail) from 48rem up.

Viewport-driven defaults — view and sidebar state are null until the user chooses. A phone opens on a single day with the drawer shut; a desktop opens on the full week with the sidebar out; an explicit choice survives a resize or rotation.

Drag/resize gated on (pointer: fine) — draggable blocks set touch-action: none so the pointer handlers own the gesture, which on touch also swallowed vertical scrolling over every event and turned an imprecise tap into an accidental reschedule. Touch now falls back to tap-to-open-details.

Event details → bottom sheet below 48rem, with the anchoring measurement skipped entirely in that mode. Anchored popover unchanged above it.

Also: explicit viewport export (device-width, viewport-fit=cover, theme color), 44px touch targets, safe-area insets, prefers-reduced-motion on the drawer transition, compact app header with the wordmark hidden below 30rem, event dialog padding and stacked date/time fields on narrow screens.

Adds the missing `.week-even

Closes #34

castroofelipee and others added 8 commits August 12, 2026 19:41
Wraps matchMedia in useSyncExternalStore with a server snapshot of false, so
the first paint is always the mobile baseline and wider layouts are layered on
after hydration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sets width=device-width, viewport-fit=cover for notched devices, and a theme
color matching the app surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The day header, all-day row, and hour grid were three sibling grids with an
inline grid-template-columns each. They stayed aligned only because none of
them could scroll sideways, which forced seven columns into the viewport width
and left roughly 40px per day on a phone.

They now share one scroll container: the header block sticks to the top, the
time gutter sticks to the left, and the column count travels as a
--week-columns custom property so a single stylesheet rule sizes every row.
Narrow screens scroll the columns sideways at a legible minimum width; from
48rem up the minimum drops to zero and the grid fills the pane as before.

The rest of the week-view styling is rewritten mobile-first around that: the
header stacks, controls grow to 44px touch targets, and the desktop spacing
moves into a min-width block. Also adds the missing .week-event-time-small
rule, which the markup referenced but the stylesheet never defined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Draggable event blocks set touch-action: none so the pointer handlers can own
the gesture. On a touch screen that also swallows vertical scrolling over
every event, making the grid hard to pan, and turns an imprecise tap into an
accidental reschedule.

Drag and resize are now gated on (pointer: fine), so touch devices fall back
to tapping an event to open its details.

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

Below 48rem the sidebar slides over the calendar instead of taking a fixed
16rem out of it: a scrim closes it, Escape closes it, picking a day or
creating an event closes it, and it is inert while off-canvas so nothing
off-screen stays in the tab order. From 48rem up it is the static column it
has always been, including the collapsed rail.

The view and sidebar defaults now follow the viewport rather than being
hardcoded open and weekly. Both are stored as null until the user chooses,
which means a phone opens on a single day with the drawer shut, a desktop
opens on the full week with the sidebar out, and an explicit choice survives
a resize or rotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the bar to 3.5rem, hides the brand wordmark below 30rem so the menu and
action buttons keep their room, grows the icon buttons to a 44px touch target,
and pads for the left and right safe-area insets. The 4rem desktop header is
restored from 48rem up.

Also makes .workspace-content a positioned ancestor so the calendar drawer can
be absolutely placed inside it, clipped below the header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The detail card was always positioned beside the clicked event. On a phone
there is no room beside anything, so it ended up clamped against an edge on
top of the block it describes.

Below 48rem it is now a full-width bottom sheet, scrollable and padded for the
bottom safe area, with a dimmed backdrop. The anchoring measurement is skipped
entirely in that mode, so there is nothing to flip or clamp. From 48rem up the
anchored popover is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trades the 4rem top inset for 1rem plus the bottom safe area so the form is
not pushed off a short viewport, contains overscroll to the backdrop, and
stacks the paired date and time fields into a column below 30rem where two
side-by-side inputs no longer fit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@castroofelipee castroofelipee self-assigned this Aug 12, 2026
@castroofelipee
castroofelipee merged commit f42111b into main Aug 12, 2026
2 checks passed
@castroofelipee
castroofelipee deleted the feature/calendar-mobile-first branch August 12, 2026 22:47
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.

Make calendar view mobile-first and responsive for smaller screens

1 participant