Skip to content

feat(landing): editorial-stamps redesign — OKLCH cream, rubber stamps, ink cascade#72

Open
vayungodara wants to merge 22 commits into
mainfrom
redesign/2026-04-19-editorial-stamps
Open

feat(landing): editorial-stamps redesign — OKLCH cream, rubber stamps, ink cascade#72
vayungodara wants to merge 22 commits into
mainfrom
redesign/2026-04-19-editorial-stamps

Conversation

@vayungodara

Copy link
Copy Markdown
Owner

Summary

Landing page reimagined per the refreshed .impeccable.md editorial-stamps direction (Pass 4 of the 2026-04-19 redesign evaluation).

  • New design DNA: OKLCH cream paper palette (light + inverted-paper dark), system serif (ui-serif → New York on macOS) + Host Grotesk + JetBrains Mono, retired indigo→purple→magenta gradient as primary brand signal in favor of flat highlighter yellow.
  • New components: Stamp (kept/missed/locked-in/pending rubber-stamp resolution label), SectionHeader (§ 0N editorial spine), UserAvatar/WitnessTile (initial-tile primary, photo fallback), NavMarker (hand-drawn highlighter SVG hover under nav links), Ticker (horizontal activity feed).
  • Ink cascade: [data-ink="..."] on <html> remaps --stamp-yellow across personal-identity surfaces; semantic resolution colors (KEPT moss, MISSED red-pen, LOCKED IN carbon blue) stay fixed.
  • Voice: confrontational-wry on marketing surfaces ("Stop lying to yourself", "Three moves. No loopholes."); dashboard stays direct-positive (separate scope).
  • PactCard: single-signal urgency — one CLOSES SOON sticker OR one stamp, never stacked. Removed glow bars and side-stripe accents.

Scope notes

The diff includes a handful of non-landing files (Sidebar.js, TodayBar.*, app/dashboard/stats/*, app/share/streak/*, lib/confetti.js, lib/supabase/middleware.js) carried from prior fix commits the branch pre-dated. Those are minor, not structural — flagged here so reviewers know which files belong to the landing redesign vs. carried-over fixes.

What's NOT in this PR

  • Navbar dynamic-island → sticky-bar swap (next session, separate branch)
  • Dashboard editorial port (next session, separate branch)
  • Ink-picker UI in settings (next session)

Test plan

  • npm run dev — landing renders, hero highlighter sits behind text (z-index: -1 + isolation: isolate), no underline/skew.
  • Light/dark mode toggle renders cream / inverted-paper as expected, semantic stamp colors hold across both.
  • Hover each nav link → NavMarker highlighter swipe animates, color follows current ink.
  • Ticker scrolls without layout jank.
  • PactCard with < 4h deadline shows CLOSES SOON sticker; resolved card shows stamp slam.
  • npm run build passes.
  • Mobile (390px) hero + nav still readable.

🤖 Generated with Claude Code

vayungodara and others added 22 commits April 14, 2026 17:34
Closes 5 Notion findings:

- **LI-302** MonthlyCalendar cutoff: grid was clipping weeks 2-6 vertically
  (overflow: hidden). Changed to overflow-x: clip + explicit
  grid-template-rows for all 6 week rows.
- **LI-300** Streak mismatch: TodayBar read denormalized
  profiles.current_streak; Stats called calculateStreak(). Unified —
  TodayBar now also uses calculateStreak() in its Promise.all so both
  surfaces share one source.
- **LI-314** StatsPageClient fetched 5000 pacts + 5000 focus sessions
  then filtered in JS. Replaced with 12 parallel Supabase count queries
  (head: true).
- **LI-334** Stats page polish: added fadeInUp entrance + editorial
  streak row (promoted count to text-3xl bold, pluralized Best: N
  day(s), semantic 3-slot layout). Removed AI-slop identical-card
  hover (translateY) — kept only border-color transition per dashboard
  calm-motion principle. Replaced deprecated Fire icon with Flame.
- **LI-337** Landing TTFB: app/page.js had force-dynamic + server
  getUser() adding ~3s TTFB. Moved authenticated redirect fully into
  lib/supabase/middleware.js (with validated returnTo), dropped
  force-dynamic. Landing now renders as ○ (Static) — verified via
  build route table.

Also: fixed unused `options` destructure in middleware cookie forEach.

LI-270 (partnerships N+1) was already fixed in a prior commit — code
uses supabase.rpc('notify_partner') with p_recipients array (not a
loop). Notion row updated to reflect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- **Share button slow**: `/share/streak` ran 3 sequential server queries
  (auth → profile → 366-day streak scan) with no loading state.
  Parallelized profile+streak via Promise.all and added `loading.js`
  Suspense skeleton mirroring the share card so the transition feels
  designed instead of blank.
- **Streak emoji consistency**: Dashboard TodayBar rendered streak as
  🔥 emoji; Stats page uses Phosphor `<Flame>`. CLAUDE.md mandates
  Phosphor for UI chrome, emojis for content. Unified TodayBar to
  Phosphor `<Flame size={24} weight="fill" color="var(--urgency-amber)" />`
  matching Stats. Milestone celebration wrapper + iconPulse animation
  preserved.

Also includes an earlier lossless refactor by the impeccable:critique
run: Design Context moved from CLAUDE.md into dedicated `.impeccable.md`
(108-line design brief that impeccable skills auto-read).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- **TodayBar perf**: the streak unification introduced two sequential
  Promise.all blocks (4 queries → wait → 3 queries), making the second
  wave block on the first. Merged into a single Promise.all of 7
  parallel queries. Also removed now-unused `current_streak` and
  `last_activity_date` columns from the profile select since streak is
  now derived from calculateStreak() and the "broken if >1 day" guard
  was dropped with it.
- **CompactActivityCard emoji**: activity card on dashboard still
  rendered 🔥 literal. Swapped to Phosphor <Flame size={20}
  weight="fill" color="var(--urgency-amber)" /> to match TodayBar +
  Stats page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dashboard header was using <Fire> + var(--warning); Stats uses <Flame>
+ var(--urgency-amber). Different icon shape and color. Swapped to
<Flame> + --urgency-amber so streak indicators match across surfaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User preferred the Dashboard's rounder Fire silhouette + warning color
over the thinner Flame + urgency-amber. Swapped the other 3 surfaces
(Stats page, TodayBar, CompactActivityCard) to match Dashboard instead
of the other way around.

All 4 streak surfaces now: <Fire> + var(--warning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
checkStreakAtRisk reads profiles.current_streak (denormalized DB
column) while TodayBar display uses calculateStreak (live derivation).
When they disagree — DB says 1 but live calc says 0 — the banner
showed "Complete a pact to save your 1-day streak!" while the streak
badge showed "0 day streak!". Reconciled at the TodayBar level: if
live streak is 0, force atRisk to false regardless of what the DB
column says.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- **Sidebar**: ChartLineUp's fill weight is mostly strokes with a tiny
  area under the line, so when selected it doesn't visually pop like
  the House/Timer/GearSix icons. Swapped to ChartBar — bars have a
  meaningful fill variant that matches the active-state treatment of
  the other sidebar icons.
- **TodayBar circle flame**: reverted to literal 🔥 emoji per
  preference. The Dashboard header Fire icon and Stats page Fire icon
  remain — only the circle-contained flame in TodayBar changed.

Also dropped the now-unused Fire import from TodayBar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChartLineUp's fill weight is mostly strokes (just a line with a small
filled area underneath), so when selected it didn't visually pop like
House/Timer/GearSix whose fill variants are meaningful solid shapes.
Swapped to ChartBar — three bars, solid fill, matches the chunkier
active-state treatment of the other sidebar icons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adding font-size to .streakIcon so the 🔥 emoji renders at its
original 24px. Mobile breakpoints (768px/480px) now scale font-size
instead of the removed SVG width/height overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent-Logs-Url: https://github.com/vayungodara/lockin/sessions/983c1983-c7da-48ba-a7f7-210ed5a49473

Co-authored-by: vayungodara <187050579+vayungodara@users.noreply.github.com>
Confetti was reported missing on completion. The single 80-particle
burst was easy to miss (short ticks, tight spread). Beefed up the
`useConfetti` hook used by PactCard:

- Center burst: 90 particles, spread 75, startVelocity 45, ticks 90
  (was 80 particles, spread 70, default velocity, default ticks)
- 150ms later: two side cannons (50 particles each from left/right
  edges, angles 60°/120°, spread 100)
- Explicit `disableForReducedMotion: false` on the canvas-confetti
  config so the library doesn't silently opt out on its own — the
  outer `prefersReducedMotion()` guard already handles user intent

Result: three-burst choreography matching the richer patterns already
used by fireSideConfetti/fireMilestoneConfetti. Harder to miss if the
canvas is rendering at all, and any stray CSS that might be clipping
a small burst is unlikely to clip three different origins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The outer reduced-motion guard in useConfetti().fire would silently
bail on OSes with motion reduced. Pact completion confetti is a
deliberate celebration moment core to the product experience — should
fire for every user on every completion. Removed the outer guard; kept
canvas-confetti's own `disableForReducedMotion: false` override inside
the burst options as a belt-and-suspenders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Confetti still not appearing after the amplified-burst + motion-guard
removal. Most likely canvas-confetti isn't making it into the client
bundle, or the top-level `import confetti from 'canvas-confetti'` is
failing silently under Next 16's RSC boundary.

- Switched to dynamic `await import('canvas-confetti')` — forces
  client-only resolution and surfaces load failures
- Added DOM fallback renderer (position:fixed container, brand-colored
  rects with gravity/drag physics) so even if the canvas-confetti
  module is completely missing, confetti visibly fires
- Module + per-call errors are logged to console with [confetti] prefix
- Kept all exports with matching behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 parallel debug agents (runtime/bundle/history) traced the confetti
regression. Findings:
- canvas-confetti was bundling fine in production all along
- The chain of speculative fixes (dynamic import, DOM fallback, etc)
  over-engineered the helper without solving the actual symptom
- Actual cause: prefers-reduced-motion suppression at two layers — a
  JS helper guard AND canvas-confetti's built-in disableForReducedMotion
  default — both silently returned without firing
- Latent bug: isCompletingRef.current was set true on click but only
  reset in error paths of commitComplete/commitMiss, so after the
  first successful complete-and-undo cycle the card was deadlocked

Changes:
- `lib/confetti.js` rewritten clean (169 → 107 lines). Top-level
  canvas-confetti import, no dynamic import, no DOM fallback, no
  console noise, no prefersReducedMotion() helper. Every confetti
  call explicitly sets `disableForReducedMotion: false` so the
  library's internal guard cannot suppress the burst
- `components/PactCard.js` moves `isCompletingRef.current = false`
  into the finally block of both handleComplete and handleMiss so
  the ref always resets on success or failure. Removed the redundant
  resets from the commitComplete/commitMiss catch blocks

User requirement was explicit: confetti must fire every completion
regardless of OS reduce-motion setting — it's a one-shot deliberate
celebration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
canvas-confetti's canvas was created and appended to document.body
correctly, but was still not visually appearing after every other
fix. Either (a) something environment-specific is blocking the
canvas from painting, or (b) a browser-specific quirk with the
library's canvas rendering.

Switched to a pure DOM + Web Animations API implementation:

- Each burst creates a position:fixed z-index:9999 container on body
- Particles are plain <span> elements with inline styles
- Physics (gravity + drag + fade) via requestAnimationFrame
- No canvas, no external library, no reduced-motion logic
- API-compatible with canvas-confetti: fireConfetti,
  fireConfettiFromElement, fireSideConfetti, fireStars,
  fireMilestoneConfetti, useConfetti all preserved

If this still doesn't render, the issue is an environmental overlay
or portal with higher z-index that we haven't identified. DOM nodes
with explicit inline styles should be virtually impossible to hide
without an explicit rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. [IMPORTANT] returnTo lost for unauthenticated deep-link visitors.
   LandingPageClient now reads returnTo from URL via useSearchParams()
   (client-side) instead of props. Page stays static (Suspense wrapper
   in app/page.js). Restores OAuth callback ?next= flow for /join/[code]
   and /share/streak redirects.
2. [NIT] focus_sessions lifetime query: re-added .limit(5000) to prevent
   silent truncation at PostgREST default 1000 rows.
3. [NIT] Stats page error handling: all 11 parallel query responses now
   checked for .error (was only 3). Partial failures surface error UI
   instead of showing plausible-but-wrong zero values.
4. [NIT] Middleware returnTo fragment handling: replaced string indexOf
   with URL object for proper pathname/search/hash decomposition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root-level *.png artifacts (current-*, llm-*, redesign-*, wave*-*, test-*)
are visual-audit captures from impeccable design iteration, not source.
.claude/worktrees/ holds transient agent workspaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…serif + Host Grotesk + JetBrains Mono, ink cascade, new motion presets

Refresh of LockIn design DNA per the editorial-stamps direction
(Pass 4 of the 2026-04-19 redesign evaluation):

- .impeccable.md/CLAUDE.md: anchor rewritten — voice scope split
  (confrontational-wry on marketing, direct-positive on dashboard),
  five-ink cascade with semantic resolution colors held fixed,
  retired indigo→purple→magenta as primary brand signal in favor
  of flat highlighter yellow.

- app/globals.css: tokens migrated hex/rgba → OKLCH. Cream paper light
  mode (oklch(0.96 0.012 85)), inverted-paper dark. data-ink="..."
  on <html> remaps --stamp-yellow across personal-identity surfaces;
  --kept/--missed/--locked-in stay constant.

- app/layout.js: Host Grotesk + JetBrains Mono Google Fonts loaders
  with font-display: swap + preconnect. Display fall back to
  ui-serif (no web font for hero; OS-rendered New York on macOS).
  marker-roughen turbulence filter <defs> mounted globally for
  NavMarker SVG strokes.

- lib/animations.js: stampSlam, stampSlamClean, rewardPop,
  achievementIn presets added. prefersReducedMotion gates them.

- next.config.mjs: CSP allows fonts.googleapis.com + fonts.gstatic.com.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cker

Five new editorial-stamps components feeding the landing redesign and,
later, the dashboard.

- Stamp: rubber-stamp resolution label with kept/missed/locked-in/
  pending/void variants. Rotated -3deg at rest, animates stampSlam
  on resolution. The hero mechanic — what users screenshot.

- SectionHeader: editorial spine. § 0N JetBrains Mono numeral +
  Host Grotesk title + 11px tracked uppercase caption.

- UserAvatar / WitnessTile: 2-letter initial + accent-colored tile
  primary, Google profile photo as opt-in fallback. Loads instantly,
  composes into AvatarStack for groups.

- NavMarker: SVG hand-drawn highlighter stroke that animates in
  beneath nav links on hover. Uses #marker-roughen turbulence filter
  for organic ink-edge texture. Color follows current ink via
  --stamp-yellow cascade.

- Ticker: horizontal scrolling activity feed for landing page social
  proof.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Landing page reimagined per .impeccable.md anchor:

- LandingPageClient: hero with opaque-yellow highlighter behind the
  key word (sits via z-index: -1 + isolation: isolate, no underline,
  no skew). Section IDs #features, #how-it-works, #witnesses,
  #objections feed the navbar anchors. Voice is confrontational-wry
  ("Stop lying to yourself", "Three moves. No loopholes.") —
  marketing-only.

- page.module.css: editorial scaffolding — section spacing rhythm,
  ink-cascade-aware highlighter, ticker placement.

- NavbarLanding: keeps the dynamic-island pill collapse for now
  (Wave 0 of the next session retires it for a sticky bar). Wordmark
  swapped to system serif (ui-serif → New York on macOS) per the
  Pass 4 font decision. Flat highlighter yellow rotated logoMark
  replaces the retired indigo-purple-gradient raster logo.
  NavMarker hover swipe under nav links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PactCard refreshed to embody the .impeccable.md "stamps as resolution"
principle — one urgency signal at a time, never stacked:

- Pending, deadline > 4h: index + title + witnesses, no urgency chrome.
- Pending, deadline < 4h: rotated CLOSES SOON highlighter sticker
  (3deg tilt) in the top-right.
- In-progress (focus session active): LOCKED IN stamp + pulse dot
  on the active witness avatar.
- Resolved: KEPT (moss) or MISSED (red-pen) Stamp slammed into the
  card header via stampSlam animation.

Removed: glow bars, stacked border colors, pulse + shadow + tint
combos. Removed: side-stripe accents (impeccable absolute ban).
Witness row uses the new UserAvatar/WitnessTile component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/join/[code]/JoinPage.module.css, app/not-found.module.css:
  small token swaps to OKLCH cream palette so they don't look
  marooned next to the redesigned landing.
- docs/Claude Code Tasks/daily-triage.md: visual audit notes from
  the redesign passes.
- docs/plans/2026-04-19-lockin-frontend-agent-spec.md: spec the
  lockin-frontend agent reads for project-aware UI work.
- docs/plans/2026-05-02-dashboard-redesign-prompt.md: brief for the
  next session — navbar swap + dashboard editorial port.
- .claude/agents/lockin-frontend.md: project-local agent definition
  that auto-loads .impeccable.md for design tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lockin Ready Ready Preview, Comment May 2, 2026 10:52am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 184d648c5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/confetti.js
Comment on lines 91 to 93
export function fireConfetti() {
if (prefersReducedMotion()) return;
confetti({
particleCount: 80,
spread: 70,
origin: { y: 0.6 },
colors: BRAND_COLORS,
zIndex: 9999,
});
fireParticles({ count: 80, originY: 0.6, velocity: 28 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect reduced-motion before triggering confetti

This rewrite removed the previous prefers-reduced-motion guard, so celebration effects now always run even for users who explicitly request reduced motion. In flows like streak milestones (TodayBar) and pact completion, that means animated particle bursts are forced on an accessibility-sensitive cohort and can cause discomfort/perf issues; please add the motion-preference check back at the shared confetti entry points.

Useful? React with 👍 / 👎.

supabase.from('pacts').select('*', { count: 'exact', head: true })
.eq('user_id', uid).eq('status', 'completed').gte('completed_at', monthAgo.toISOString()),
// Focus totals — only duration_minutes column, needed for lifetime sum/avg
supabase.from('focus_sessions').select('duration_minutes').eq('user_id', uid).limit(5000),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add stable ordering to limited focus-session totals query

The new totals query applies .limit(5000) without an order, which makes the subset of rows non-deterministic once a user has more than 5000 sessions. Because totalMinutes, sessionsCount, and avgPerDay are computed from that subset, stats can drift unpredictably between requests for heavy users; the previous implementation at least constrained this to the most recent 5000 rows with order('started_at', { ascending: false }).

Useful? React with 👍 / 👎.

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.

2 participants