Commit graph

14 commits

Author SHA1 Message Date
4aaf0957dd fix: nine test-note follow-ups
1. Markdown preview in the admin edit panel now re-renders from the
   textarea's current value on every toggle (dynamic-imports marked on
   the client). Previously the panel showed the server-rendered seed
   value forever, so new dispatches always previewed empty.

2. Pulse sub-form drops the opens_at field (opens on dispatch publish
   automatically) and changes closes_at to a date input — the chosen
   day is treated as end-of-day in the DB.

3. /dispatches/[slug] reading width widened 50% (720 → 1080px).

4. Roadmap display_order cascades on insert / update / delete:
   inserting at N bumps N..end up by 1, deleting N pulls N+1..end
   down by 1, moving from A to B shifts the intermediate range by 1
   in the appropriate direction. Order stays dense — no gaps, no
   collisions. All three transitions run in a transaction.

5. /roadmap always anchors at scrollLeft=0 on mount so the first
   milestone aligns with the content-column left edge. Previously
   the page jumped to the last-shipping milestone, which felt random
   once items past the viewport landed.

6. Events admin list shows the actual date (fmtDateTime) instead of
   "in 3 days" — easier to scan when planning across months.

7. duration_label is auto-computed from starts_at + ends_at on save
   (minutes < 90, hours < 4, "Half day", "Full day", "N days").
   The manual field is gone from the admin form; the column on the
   member-facing event pages keeps reading the stored value as before.

8. Pulse hero still skips office hours per the existing logic — no
   change. Confirmed via the test note's clarification.

9. Pulse "also coming up" strip relabeled to Previous + Upcoming.
   Previous = most recent past non-office-hours event. Upcoming =
   next non-office-hours event after the hero. Each card now carries
   a small terracotta eyebrow with the label.

Typecheck clean, build clean, 147/147 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:46:06 +02:00
220f8e0290 style(roadmap): align first milestone with content column left edge
The first route item now starts where the dispatch banner's left edge
sits (the page's content-max column), instead of 60px from the
viewport edge. Looks intentional now — the route and the dispatch
banner share a vertical anchor.

- computeRouteLayout now accepts optional paddingLeft / paddingRight
  that override the symmetric paddingX. Existing call sites and
  tests are unchanged.
- RoadmapRoute SSR + client recompute set paddingLeft = max(60,
  (vw - 1152) / 2), so on viewports ≤ 1152px nothing moves (degrades
  gracefully) and on wider screens the start migrates inward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:06:30 +02:00
f90480bc8b feat(route): unified scroll — drag with momentum + wheel + animated glide
Replaces the snap-based scroll-by-arrow model with one that handles
three input modalities feeding the same scroll state. Result: the
track glides continuously instead of clicking into milestones.

Stripped:
  - scroll-snap-type: x mandatory on .rr-scroll
  - scroll-behavior: smooth on .rr-scroll
  - scroll-snap-align: center on .rr-dot
  - The fades-only updateNav stub from commit 2

Added on .rr-scroll:
  - cursor: grab → grabbing on .rr-dragging
  - touch-action: pan-y so vertical page-scroll passes through on mobile
    while horizontal drags activate the route's own drag handler
  - user-select: none stops text selection mid-drag
  - .rr-dragging .rr-card { pointer-events: none } so a hover-reveal
    can't pop open while the track is being dragged

Script (vanilla, ~140 lines):
  - animateScrollTo(target, durationMs): cubic-ease-out via RAF.
    Cancels any existing momentum or animation before starting.
  - Wheel handler: vertical deltaY translates to horizontal scrollLeft
    when |deltaX| < |deltaY|; horizontal trackpad gestures pass through
    1:1 unscaled. preventDefault on this scroll element only — vertical
    wheel elsewhere on the page scrolls the page as normal.
  - Pointer-drag: pointerdown captures the start position + scrollLeft;
    pointermove updates scrollLeft and tracks velocity in px/ms over
    the most recent sample. setPointerCapture for cross-element drag.
  - Momentum on release: signed velocity × 16ms decays at 0.93 per
    frame, stops below 0.4 px/frame. Direction inverted because
    dragging right moves scrollLeft left.
  - Click vs drag discrimination at 5px total movement: under 5px,
    the synthetic click passes through (card navigates); over 5px,
    a capturing-phase click suppressor on the scroll element eats
    the next click so a drag-then-release-over-a-card doesn't
    accidentally navigate.
  - Advance arrow click now runs animateScrollTo(scrollLeft + 60% of
    viewport, 480ms) instead of the placeholder native scrollBy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:19:13 +02:00
acbb722a0a feat(route): single right-edge advance arrow — forward-only affordance
Replaces the prev/next button pair removed last commit with a single
viewport-anchored circular control.

  - 48px diameter, 1px terracotta border, terracotta chevron on cream.
  - Hover / focus-visible fills to terracotta with cream chevron and a
    1.06× scale-up.
  - Anchored absolute inside .rr-wrap (already position: relative):
    right: 32px / top: 50% / translateY(-50%).
  - Toggles .rr-at-end (opacity 0.25, pointer-events: none) when the
    scroll container reaches its right edge.
  - First-load hint: .rr-hint class added 100ms after mount fires a
    rr-advance-pulse keyframe three times (iteration-count: 3) — soft
    8px shadow ring in 15% terracotta pulses out and back. Animation
    stops naturally; no JS cleanup needed.

No left arrow on purpose — the path reads past → future, and the
user's instinct at any milestone is 'what's next?' The right arrow
earns its keep by hinting the existence of more track beyond the
visible window. A symmetric left arrow would just be noise.

Click handler today: scrollBy({behavior: 'smooth'}) by 60% of viewport
width. Step 4 replaces this with a custom-animated glide and adds the
drag + wheel scroll modalities.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:17:26 +02:00
22a55aa073 feat(roadmap): centred 'Roadmap' page header + legend escapes route component
Page-level rebuild. /roadmap.astro renders a centred header block that
reads as one calm vertical stack:

  Roadmap        ← 11px tracked uppercase eyebrow (--on-surface-variant)
  Roadmap        ← 48px serif h1, single word
  A live picture of the work…   ← 14px subtitle, 480px max-width

The eyebrow + h1 read 'Roadmap → Roadmap' on purpose — the tracked
uppercase eyebrow primes the eye for the serif headline, and the
repetition feels confident rather than redundant. If it starts to grate
in practice, the eyebrow's the easy drop.

'What we are building.' is gone. The earlier 'The route' sub-header
inside <RoadmapRoute> is gone. The two prev/next arrow buttons in that
sub-header are gone (the single right-edge advance arrow lands in the
next commit).

Legend moves out of <RoadmapRoute> and into /roadmap.astro as the page's
final block. With the route still .rr-fullbleed, this lets the legend
return to centred content-column width — exactly the spec's
'header-centred / route-wide / legend-centred' rhythm.

Mid-state in this commit: the advance arrow doesn't exist yet, so
there's no in-page scroll affordance beyond the still-active scroll-snap
behaviour. Step 3 adds the arrow; step 4 strips the snap and adds drag
+ wheel + animated glide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:15:47 +02:00
d7c13d3c99 feat(route): full-bleed escape + client recompute on real viewport width
Two coupled changes so the route actually uses the page width:

.rr-wrap.rr-desktop gains the .rr-fullbleed class which uses the canonical
calc(50% - 50vw) margin trick to break out of the parent .page max-width.
The headline, dispatch banner, route section header, and legend all stay
inside the content column at 72rem; only the route itself widens to the
viewport. Visually reads as a magazine spread — the section header lands
centred, then the path spreads outward beneath.

Viewport-aware layout: SSR still uses the 1100 default (we can't know
the client viewport server-side), but a new mount script on
RoadmapRoute recomputes the layout against window.innerWidth and
updates:
  - .rr-track width (via inline style)
  - .rr-path-svg width attribute
  - .rr-path-d d attribute (rebuilt from the same cubic-bezier
    formula the SSR helper uses, with the live itemX values; itemY
    comes from per-milestone data-y attributes since amplitude
    doesn't change with viewport)
  - .rr-milestone left positions

Resize: 120ms debounced handler runs the same recompute + refreshes
the arrow/fade nav state. Each milestone keeps its same data-y, so
only the horizontal spread changes — the river's vertical shape is
preserved on resize.

Initial-scroll into shipping rewired to read the .rr-current
milestone's live `style.left` after recompute, not the SSR-computed
data-initial-x value (which is now stale once the client redoes the
math).

.rr-scroll horizontal padding 60 → 80 + scroll-padding-{left,right}
60 → 80 so first/last cards have breathing room inside the now-
viewport-wide container.

Smoke as Lars: rr-fullbleed class on the wrap, data-y attributes on
each milestone, rr-path-svg id present. The SVG width and itemX
positions land at viewport-derived values after mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:56:41 +02:00
83503fe7a3 fix(route): overflow-y: visible — hover cards never clip vertically again
The root cause of the hover-clipping we kept band-aiding with larger
track-heights: a scroll container with overflow-x: auto implicitly
clips on the perpendicular axis too. Explicit overflow-y: visible
lets cards expand above and below the track freely.

Implementation matches the spec's belt-and-braces pattern. New layered
markup:

  .rr-wrap          → position: relative, anchors fades
  .rr-scroll        → overflow-x: auto + overflow-y: visible,
                      padding 60/60/80, scroll-padding 60/60 sides
  .rr-scroll-inner  → structural, no layout effect
  .rr-track         → positioned at the inner wrapper

The padding-top: 60px / padding-bottom: 80px on .rr-scroll gives cards
room to grow above and below the track without ever hitting a clip
boundary, even on browsers that mis-handle the overflow-x/y mix.

Edge fades reposition: top: 60px / bottom: 80px (was 0 / 16) so they
only cover the track itself, not the overflow padding zones above and
below where hover-expanded cards now live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:35:02 +02:00
b4df8e10f1 fix(route): span ~80% of viewport + scroll padding back to 60 + track 420
Three coupled fixes to the route's geometry:

- computeRouteLayout's width calculation flipped to Math.max(viewport
  * 0.80, itemCount * minSpacing + padding * 2). On a wide screen with
  few items the 80% target wins and the path stretches across the
  page; once item count makes the data-driven width exceed 80% (the
  carousel case), the data-driven value wins and the track extends
  past the viewport unchanged.
- .rr-scroll horizontal padding 140 → 60 each side. The previous 140
  was overcompensating; with the new 80% target the milestones already
  sit inside their own breathing room. 60 is card-half + 30px buffer,
  enough for a 220px card centred under a dot 60px from the edge.
  scroll-padding kept in sync at 60 for snap-stop landings.
- trackHeight default 580 → 420; midY 290 → 210. The 580 was bandaging
  the vertical-clipping issue — that fix lands in the next commit. With
  the clip properly addressed, 420 fits the path's amplitude 120 swing
  cleanly with no wasted vertical space.

Tests rewritten to match the new width semantics: 1 item @ 1000 →
920px (0.8 * 1000 + 120); 3 items @ 1400 → 1240px; 20 items @ 800 →
6200px (data-driven wins). midY assertion 290 → 210.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:32:29 +02:00
ac52e97c28 feat(route): drop progress dots, move legend below, vary path amplitude
Three coupled changes that all serve the same goal — less furniture
above the route, more honest information below it.

Progress dots gone. At 5 pills × ~400px per pill the strip was too
coarse to feel meaningful; the arrows + edge fades already
communicate scroll position. .rr-progress markup, the script logic
that updated the .active class, and the .rr-progress / .rr-progress-dot
styles are all deleted.

Legend moves from beside 'The route' in the section header to below
the track, centred. Reading order is now title → walk the path → key,
which is the order it makes sense in. The header collapses to just
the title on the left and the two arrow buttons on the right.

Path amplitude is no longer constant. computeRouteLayout multiplies
the base amplitude (120) by a per-item factor that ramps 0.78 (first
off-axis item) → 1.18 (last item), so closer-in items swing tighter
and further-out items swing wider. The visual effect is subtle but
the path now feels hand-planned instead of strictly sinusoidal.

Test updated to verify the multiplier — |itemY[2] - midY| now exceeds
|itemY[1] - midY| in the 3-item case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:58:59 +02:00
f8d88ed760 fix(route): 'you are here' marker is unmistakable + dot animates with hover
The previous .rr-current was a 1.15× scale on top of an animated pulsing
::after ring — subtle, and the pulse was easy to miss against the cream
ground. Replaced with a static box-shadow ring at 6px offset in 45%
terracotta, plus a 1.3× scale on the dot itself. The pulse is gone;
the ring is now visible at rest, which is what the marker needs to do.

Hover/focus on a milestone card now scales its sibling dot via :has():
  - any card hover/focus → its dot 1.15
  - the current-shipping card hover/focus → its dot 1.4
The dot acknowledges that you've engaged with its card. Cleaner than
tying scroll position or click state.

:has() ships in every evergreen browser since 2023; older Firefox just
won't grow the dot, which degrades to no harm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:56:49 +02:00
fde07b1f11 fix(route): track 580 + 140px scroll-container padding — no more clipping
The route was clipping at three places: top and bottom of hovered
cards (the track was only 460 tall) and at the left/right viewport
edges (first card half-off-screen at scrollLeft 0, last card off the
right at scrollEnd).

Track height: default trackHeight in roadmap-layout 460 → 580; .rr-track
inline-style and the SVG height matched. midY now 290. Path centreline
stays in the visual centre and gains 60px breathing room above + 60px
below — which is exactly the room a hovered card needs to expand into.

Scroll-container padding: .rr-scroll gains 140px of horizontal padding
plus matching scroll-padding-left/right so snap-stops land cleanly.
The 140 figure is 220px card-width / 2 + 30px buffer, so the first and
last cards have a full card-width of clear space inside the viewport
at the scroll extremes.

Layout helper test verifies midY === 290.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:56:01 +02:00
1325422056 feat(route): mobile vertical timeline at <768px
The treasure-map metaphor is a desktop affordance — on phones it
becomes a vertical timeline instead. Markup was rendered alongside the
desktop track in commit RM.5; this commit adds the styles to actually
show it and hides the desktop bits.

Per-row layout — 32px / 1fr grid:
- Left column holds the 12px round status-coloured dot and a 1px
  rgba(0,0,0,0.18) vertical line continuing down to the next dot. The
  last row has no line (rendered conditionally in the markup), so the
  trail ends cleanly at the final milestone.
- Right column holds the same eyebrow / serif title / description /
  trailing line — but always visible. No hover, no reveal. Reading is
  the only interaction.

Arrow buttons and progress dots both hidden at this breakpoint. The
mobile timeline needs no JS — pure markup + CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:43:35 +02:00
d49882b3f9 feat(route): nav script — arrows, fades, progress dots, initial scroll
Vanilla TS script at the bottom of RoadmapRoute.astro. No library.

- Arrows scrollBy ±72% of the scroll-container's clientWidth, smooth
  behaviour. Disabled at scroll start/end.
- Edge fades (.rr-fade-left / -right) flip opacity 0↔1 at scroll start /
  end so the affordance disappears when there's nowhere further to go.
- Progress dots track scrollLeft/(scrollWidth-clientWidth) percentage,
  bucketing into dots.length slots. Active dot gets .active (themed in
  CSS as --on-surface).
- On mount, the script reads section.data-initial-x — the SVG x position
  of the most recent shipping milestone (computed server-side from the
  layout helper) — and scrolls so that x sits ~25% from the left edge
  of the viewport. Clamped to [0, scrollWidth-clientWidth]. Member opens
  /roadmap and immediately sees one shipped item + several ahead-of-them
  items, not the very start of history.
- setTimeout(update, 50) re-measures after first paint settles
  (especially relevant when SVG fonts or other late-arriving assets
  shift the trackWidth by a couple of px).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:43:07 +02:00
7bd4902b9d feat(component): RoadmapRoute — SVG path + milestones + hover-reveal cards (static)
The treasure-map. Static render only; nav script lands in the next commit.

Section header: serif 'The route' + tracked-uppercase legend
(Shipping / In beta / Exploring / Considering) on the left; two 32px
round arrow buttons on the right (matching the /pulse RoadmapCarousel
chrome).

Body — desktop layout (.rr-desktop):
- Outer .rr-wrap holds an overflow-x: auto .rr-scroll with snap-x.
- Track is sized to layout.trackWidth × 460. Cubic-bezier SVG path
  rendered behind milestones, stroked with a horizontal gradient that
  fades from #2a2520 / 0.55 alpha through to #2a2520 / 0.15 at the
  travelled-stop position (computed by travelledStopFor in step 3).
- Each milestone is a 14px round dot in its status colour, with a 5px
  cream halo cutting the path beneath. The 'you are here' marker (most
  recent shipping item) gets a 1.15× scale + a quiet 2.4s pulse ring.
- Cards hang from each dot via a 1px / 30px vertical connector, on the
  alternating cardSide returned by layout. .rr-card is the anchor target;
  hover and :focus-visible both reveal the description + trailing line
  via max-height + opacity transitions, so keyboard tab is a first-class
  interaction (no mouse required).
- Trailing line: item.metadata_text if set, else 'Shaped by {first
  names}' if attributed_members non-empty, else nothing.
- Edge fades on both sides for scroll affordance (left fade hidden when
  at scrollLeft 0; right fade hides when at scrollEnd — the JS in step 6
  will toggle their opacity).

Progress dots row underneath — count = max(2, min(6, ceil(items/2))).
First dot starts active; nav script will move it.

Mobile vertical fallback (.rr-mobile) markup is included now but kept
display:none on desktop. Step 7 turns it on at the (max-width: 767px)
breakpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:42:32 +02:00