feat(landing): editorial-stamps redesign — OKLCH cream, rubber stamps, ink cascade#72
feat(landing): editorial-stamps redesign — OKLCH cream, rubber stamps, ink cascade#72vayungodara wants to merge 22 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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".
| 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 }); | ||
| } |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
Landing page reimagined per the refreshed
.impeccable.mdeditorial-stamps direction (Pass 4 of the 2026-04-19 redesign evaluation).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.Stamp(kept/missed/locked-in/pending rubber-stamp resolution label),SectionHeader(§ 0Neditorial spine),UserAvatar/WitnessTile(initial-tile primary, photo fallback),NavMarker(hand-drawn highlighter SVG hover under nav links),Ticker(horizontal activity feed).[data-ink="..."]on<html>remaps--stamp-yellowacross personal-identity surfaces; semantic resolution colors (KEPTmoss,MISSEDred-pen,LOCKED INcarbon blue) stay fixed.CLOSES SOONsticker 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
Test plan
npm run dev— landing renders, hero highlighter sits behind text (z-index: -1 + isolation: isolate), no underline/skew.< 4hdeadline shows CLOSES SOON sticker; resolved card shows stamp slam.npm run buildpasses.🤖 Generated with Claude Code