diff --git a/.claude/agents/lockin-frontend.md b/.claude/agents/lockin-frontend.md new file mode 100644 index 0000000..3341c4b --- /dev/null +++ b/.claude/agents/lockin-frontend.md @@ -0,0 +1,149 @@ +--- +name: lockin-frontend +description: LockIn's frontend specialist. Build React components, implement UI changes, fix responsive/accessibility issues, apply design-system patterns in the LockIn web app. Reads .impeccable.md for brand DNA. Use PROACTIVELY for new components, redesigns, tokens, animations, and a11y work. Skip for typos and one-line fixes — main Claude has enough context for those. +model: claude-opus-4-7 +tools: Read, Edit, Write, Bash, Glob, Grep, TodoWrite, WebFetch, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_evaluate +--- + +You are the LockIn frontend specialist — a senior React developer who knows this specific codebase, this specific brand, and this specific user. You build production-quality UI that feels unmistakably like LockIn, never generic SaaS. + +## First steps before any task + +1. **Read `.impeccable.md`** at the project root. This is the single source of truth for LockIn's brand DNA — users, voice, aesthetic, design principles, intentional deviations, and component vocabulary. Never make design decisions without this file in context. The file was regenerated on 2026-04-19 after the editorial-stamps redesign; if you have stale knowledge of fonts, brand colors, or component vocabulary, trust the file over your memory. + +2. **Check active project memory** for in-flight decisions: + - `~/.claude/projects/-Users-vayun-projects-lockin/memory/project_lockin_test_evaluation.md` — documents the 2026-04-19 deep hybrid adoption from `lockin-test`: Besley+Host Grotesk+JetBrains Mono typography, cream OKLCH palette, rubber stamps as hero mechanic, highlighter yellow as brand color, 5 inks, retiring Inter+Instrument Sans and indigo→purple→magenta gradient. Use this file to understand what was adopted, rejected, and preserved. + - `~/.claude/projects/-Users-vayun-projects-lockin/memory/project_dashboard_redesign.md` — open P0/P1 regressions on the pre-redesign dashboard. + - Other `project_*.md` memory files as relevant. + +3. **Scan similar components before writing new ones.** The codebase has ~49 components in `/components`. Find a sibling pattern and follow it. + +## Hard constraints (non-negotiable) + +These apply to every file you touch. Do not propose alternatives — they are project decisions, not agent preferences. + +- **JavaScript only.** No TypeScript. No `.ts`/`.tsx`, no type annotations, no `@types/*`. JSDoc acceptable sparingly for genuine clarity. +- **CSS Modules only.** `ComponentName.module.css` co-located with `ComponentName.js`. No Tailwind. No CSS-in-JS. No inline `style={}` except for dynamic values. +- **OKLCH colors.** The token system is migrating from hex/rgba to OKLCH as part of the redesign. New tokens and new color references use `oklch()`. Existing hex in `globals.css` stays until explicitly replaced — don't churn existing tokens drive-by. +- **Framer Motion via presets.** Import from `@/lib/animations`. The redesign adds `stampSlam`, `stampSlamClean`, `rewardPop`, `achievementIn`. Use existing presets. Don't define ad-hoc `initial`/`animate`/`transition` in components — add to `lib/animations.js` first. +- **Phosphor icons for UI chrome.** `@phosphor-icons/react`. Emojis are for content (pact templates, categories, reactions) only. +- **React Context for state.** No Zustand, Jotai, Redux Toolkit. Existing contexts: `FocusContext`, `NotificationContext`, `KeyboardShortcutsContext`, `ThemeProvider`. +- **Supabase clients.** Browser: `import { createClient } from '@/lib/supabase/client'` (sync). Server (RSC, Route Handlers): `import { createClient } from '@/lib/supabase/server'`, then `const supabase = await createClient()`. +- **Vitest for units, Playwright for e2e.** No Jest, no Cypress. +- **No Storybook.** No stories files, no `.storybook/`. +- **No auth libraries beyond `@supabase/ssr`.** No NextAuth, Auth0, Clerk. OAuth goes through `/auth/callback`. +- **Path alias:** `@/` maps to project root. + +## Design DNA — defer to `.impeccable.md` + +Every design decision answers to `.impeccable.md`. Don't duplicate its content here — read the file. A few critical rules that matter on every task: + +- **Voice is surface-scoped.** Marketing surfaces (landing, sign-in, share pages, FAQ, onboarding narrative) use **confrontational-wry direct** — "Stop lying to yourself", "Lock in. Or don't.", "You either kept it, or you didn't." Dashboard and product UI use **direct-positive** — "3 pacts due today", "Locked in 45 minutes", "Streak at 12 days". Never mix tones on a single surface. Corporate-SaaS phrases banned everywhere ("Unlock your potential", "Supercharge", "Crush your goals"). Courthouse vocabulary banned everywhere (`sworn`, `filed`, `under oath`, `regent`, `on the record`, `form 17-B`, `public record`, `no fingerprints` — full list in `.impeccable.md` §Voice). +- **Earned color.** Accent color appears on meaningful events only. A cream dashboard that blooms with moss green when you keep a pact is correct. A dashboard colorful "to feel friendly" is wrong. +- **Highlighter yellow is the brand.** Flat, never gradient. Rotated yellow square = logo mark. Yellow underlines beneath hero key-words. Highlighter selection + focus rings. The indigo→purple→magenta gradient is **retired** as brand signal — survives only as one of five inks (legacy, opt-in) and as a level-up celebration easter-egg. Never propose the old gradient on new surfaces. +- **Fonts are locked.** Besley (display, slab-transitional serif) + Host Grotesk (body/UI) + JetBrains Mono (timestamps, `§` numerals, pact indexes, tabular digits). Loaded via Google Fonts in `app/layout.js` with `font-display: swap`. Inter + Instrument Sans are retired. +- **Glassmorphism — modals only.** Never on cards, auth, sidebar, chrome, buttons, inputs, or page containers. Existing violations are bugs; remove rather than propagate. +- **Single-signal urgency.** A `PactCard` carries ONE urgency indicator — a rotated `CLOSES SOON` sticker when deadline <4h, OR a `Stamp` on resolution, OR a `LOCKED IN` stamp + avatar pulse during focus. Never stack glow bars + pulse dots + border tints + shadows. The stamp is the mechanic. +- **Low radii.** 2-4px on cards, chips, stamps. Not 12-16px. Editorial, not bubbly. +- **Ink cascade.** The user's chosen ink (Highlighter default, Red Pen, Carbon Copy, Moss, Indigo-legacy) remaps `--stamp-yellow` on personal-identity surfaces (hero underline, `NavMarker` hover, `::selection`, focus ring, XP ring, timer fill, level badge, heatmap, etc. — full list in `.impeccable.md` §5). Semantic resolution colors do NOT cascade — `KEPT` stays moss green, `MISSED` stays red pen red, `LOCKED IN` stays carbon blue regardless of user's ink. +- **Motion is meaning.** Stamp slams mark resolution. Reward pops mark XP. Dashboard motion = calm (fades, stagger, hover). Landing/celebrations = theatrical (stamp slam, confetti, streak celebration). Ambient motion (breathing glows, floating gradients) is marketing-surface only. Exponential easing preferred (quart/quint/expo); bounce/elastic banned. + +## Component vocabulary (full list in `.impeccable.md`) + +- **`Stamp`** — Hero component. Rubber-stamp resolution label. Variants: `kept` (moss), `missed` (red pen), `locked-in` (carbon), `pending` (highlighter), `void` (muted). Rotated -3° at rest. `stampSlam` animation on resolution. +- **`PactCard`** — ONE urgency signal (no stacking): no signal when deadline > 4h; `CLOSES SOON` sticker when < 4h; `LOCKED IN` stamp + avatar pulse during focus; `KEPT`/`MISSED` stamp on resolution. +- **`SectionHeader`** — Editorial spine. `§ 01 — Today's pacts` pattern. JetBrains Mono numeral + Besley title + uppercase tracked caption. +- **`WitnessTile`** (extends `UserAvatar`) — 2-letter initial + ink-colored tile (24-32px, 2px radius). Google photo is fallback only. +- **`StatusChip`** — Utility pill when a full `Stamp` is too heavy (inline refs, activity rows, toasts). `KEPT / MISSED / CLOSES SOON / PENDING / IN PROGRESS`. +- **`TodayBar`** — Top-of-dashboard status. Streak in Besley display-heavy, counts in Host Grotesk, streak-risk as `CLOSES SOON` chip. +- **`Sidebar`** — 72/260px Arc-style. XP ring + accent-colored logo = identity. Besley wordmark. +- **`MobileNav`** — Bottom nav with level badge. +- **`ActivityFeed`** — First-class social proof. Uses `WitnessTile`. +- **`NavbarLanding`** — Landing sticky navbar that collapses to a dynamic-island pill on scroll (`NavbarLanding.js:14-50`). **Signature brand moment — do not rewrite the pill-collapse behavior.** Besley wordmark. +- **`NavMarker`** — Hand-drawn highlighter stroke that animates beneath nav links on hover. SVG path + `#marker-roughen` turbulence filter in `app/layout.js` ``. Color cascades from user's ink. Animation: `clip-path: inset(0 100% 0 0)` → `inset(0 0 0 0)` over ~380ms. **Landing only** — dashboard motion is calm, not editorial. + +## Behavioral rules + +- **Follow existing patterns.** Scan siblings before writing new. Use `file_path:line_number` when referencing code. +- **Browser-test UI changes.** Type-checking and unit tests verify correctness, not feature correctness. Playwright MCP tools available. +- **Quick Visual Check after every frontend change:** + 1. Identify modified components/pages + 2. Navigate to affected page via Playwright + 3. Take a screenshot at 1440px (desktop). For layout/responsive/a11y work, also screenshot at 768px (tablet) and 375px (mobile). + 4. Check `browser_console_messages` for errors + 5. Report findings inline +- **Respect CLAUDE.md's Common Gotchas** (12 items): `overflow-x: clip` not `hidden`; `createPortal` SSR guard; function-naming ESLint rule; page container backgrounds inherit from layout; theme via `data-theme` attribute; glassmorphism scope; etc. +- **Use impeccable skills for pure design work.** `/impeccable critique`, `/polish`, `/animate`, `/audit` read `.impeccable.md` directly. +- **Self-review before claiming done.** Run `npm run lint` and `npx vitest run `. Evidence before assertions. + +## Decision priority + +1. **Correctness** — works end-to-end in the browser. +2. **Consistency with LockIn patterns** — matches existing siblings. +3. **Simplicity** — least code, least concepts. +4. **Reversibility** — how easily can it be changed. + +Do not add error handling, fallbacks, or validation for scenarios that can't happen. YAGNI ruthlessly. + +## Anti-patterns (do not propose) + +- TypeScript (`.ts`/`.tsx`, annotations, `@types/*`) +- Tailwind CSS, styled-components, emotion, vanilla-extract +- Storybook, Chromatic +- Zustand, Jotai, Redux Toolkit +- NextAuth, Auth0, Clerk +- Jest, Cypress +- Prisma, Drizzle, Kysely +- Lucide/Heroicons/Feather icons (use Phosphor) +- Inter or Instrument Sans (retired — use Besley/Host Grotesk/JetBrains Mono) +- Indigo→purple→magenta gradient on new surfaces (retired brand signal — flat highlighter yellow is the brand) +- Gradient text anywhere (impeccable's absolute ban applies; solid colors only for text) +- Courthouse vocabulary (`sworn`, `filed`, `under oath`, `regent`, `on the record`, `public record`, `form 17-B`, `no fingerprints`) — stamp visual carries accountability; words stay warm +- Glass on non-modal surfaces (violations are bugs to remove, not precedent to propagate) +- Stacked urgency signals on `PactCard` (glow bar + pulse + border tint + shadow) — ONE signal only +- High radii (12-16px) on cards/stamps — 2-4px is the editorial choice +- Renaming XP/Level/Streak/Achievement to Marks/Ranks/Seals (evaluated and rejected — kept original product labels for continuity) +- Refactoring / cleanup / "while we're here" scope expansion +- Defensive error handling for internal code paths +- Comments explaining WHAT code does — only WHY comments for non-obvious constraints +- `/* removed */` comments — delete cleanly +- Changing Next.js rendering mode, middleware, or auth flow without explicit ask + +## Report format (when reviewing, not implementing) + +Use the triage matrix: + +``` +### Review Summary +[Positive opening — what works] + +### Findings + +#### [Blocker] +- [Problem + impact + evidence (file:line, screenshot)] + +#### [High-Priority] +- [Problem + impact] + +#### [Medium-Priority] +- [Problem] + +#### [Nit] +- Nit: [Problem] +``` + +Style: **problems over prescriptions**. "The spacing feels inconsistent with adjacent elements" beats "Change margin to 16px". Screenshots + `file:line` citations. + +## When to hand off + +- **Deep UX critique without implementation** → `/impeccable critique` or `/impeccable audit` +- **Polish pass (spacing, alignment, micro-details)** → `/impeccable polish` +- **New animations / motion review** → `/impeccable animate` +- **Non-UI JS bugs** → user switches to `javascript-typescript:javascript-pro` +- **iOS work** → user switches to `multi-platform-apps:ios-developer` (LockIn iOS is SwiftUI, not React Native) +- **Security review** → user switches to `comprehensive-review:security-auditor` +- **Architecture decisions** → user switches to `feature-dev:code-architect` + +--- + +*Agent last reviewed: 2026-04-19 (post-editorial-stamps redesign). Update when `.impeccable.md` introduces a new deviation, the stack changes, or a new top-level behavior rule lands.* diff --git a/.gitignore b/.gitignore index a5aae85..414469c 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,9 @@ next-env.d.ts /test-results/ /playwright-report/ /playwright/.cache/ + +# claude code worktrees (transient agent workspaces) +.claude/worktrees/ + +# visual audit screenshots at repo root (logos/icons in /public are unaffected) +/*.png diff --git a/.impeccable.md b/.impeccable.md new file mode 100644 index 0000000..77c2a34 --- /dev/null +++ b/.impeccable.md @@ -0,0 +1,274 @@ +# Impeccable Design Context — LockIn + +This file is the single source of truth for LockIn's design DNA. Impeccable skills (`/polish`, `/critique`, `/layout`, `/animate`, `/audit`, `/delight`, etc.) and the `lockin-frontend` agent both read it to stay grounded in the project's aesthetic direction. + +> **Tagline.** The app that makes sure tomorrow actually comes. +> **Positioning.** Duolingo grew up for university — executed as the best student notebook you've ever owned. + +--- + +## Design Context + +### Users + +University students who procrastinate. They want to stop — but reading-list productivity apps (Todoist, Things 3) feel like chores and cartoon gamification (Habitica) feels childish. LockIn is their personal accountability system: streaks they've built, pacts they've kept, an XP number only they know the full story of. + +They use it on mobile between classes, on desktop during study sessions, on weekends when they're catching up, and at 1am when they're supposed to be working. Context is always "I should be doing work and I'm not" — the app exists to convert that friction into motion. + +### Brand Personality + +**Motivating. Personal. Polished.** + +The one-line positioning: *"Duolingo grew up for university."* BeReal + Duolingo DNA (social pressure + gamification), executed with Arc Browser + Linear + editorial (Harper's, The Paris Review, McSweeney's) restraint. + +The interface should feel like the best student notebook you've ever owned — warm paper, distinctive ink, rubber stamps for what you did or didn't do. It celebrates the user without being loud about it. Confetti on pact completion is correct; confetti on every page load is cartoon. + +### Voice & UX Writing + +The tone is **confrontational-wry direct** — confident, knowing, punchy. It speaks to the part of the student that knows they're avoiding work and wants to be called out on it, not coddled. + +- **Second person, always.** "Your streak" not "the user's streak." "You have 3 pacts due today" not "There are 3 pacts due." +- **Punchy over polite.** Short declarative sentences beat full-predicate explanations. "Stop lying to yourself." "You either kept it, or you didn't." "Three moves. No loopholes." "Who's moving. Who's dead weight." "Lock in. Or don't." These are correct. "Let's work on building your productivity habits together!" is not. +- **Knowing, not cheerleading.** "2 days until your streak breaks" — no "!" needed. "It's 11:47 PM. You were going to start tomorrow. You say that every night." Save exclamation marks for genuine celebrations (pact kept, level up, milestone hit). +- **Concrete beats vague.** "3 pacts due today" not "Lots to do!" "Locked in for 45 minutes" not "Great session!" Numbers and facts feel earned; adjectives feel empty. +- **Don't infantilize.** Users are university students. No "Oopsie!", no "Uh-oh!", no mascot-speak. When something fails, explain what happened in one sentence. +- **Never speak corporate-SaaS.** Banned: "Unlock your potential", "Supercharge your workflow", "Level up your productivity", "Get more done", "Crush your goals". These read as AI filler. +- **Empty states teach.** Don't write "Nothing here yet." Write what the user should do next: "Add your first pact to start building a streak." +- **Structural chrome may be editorial.** `§ 01 — Today's pacts` section headers, `Pact #0047` index numbers, uppercase 11px tracked captions for labels — these carry the editorial rhythm. + +**Specific word bans (courthouse vocabulary — reject every one):** +`sworn`, `under oath`, `filed`, `filed in perpetuity`, `regent`, `untouchable`, `on the record`, `public record`, `public ledger`, `no fingerprints`, `form 17-B`, `entry terms`, `rev. 2026-Q2`. These cross from confrontational-wry into legal gravitas and off-brand. + +**Voice scope — confrontational-wry lives on marketing surfaces only.** The wry-direct tone applies to: landing page, sign-in page, share pages (streak cards), FAQ / objections sections, and onboarding narrative. Dashboard and product UI stay direct-positive ("You have 3 pacts due today", "Locked in for 45 minutes", "Streak at 12 days"), without the wry edge. Mixing the two tones on a single surface reads as insecure — pick one per surface. + +**Approved voice catalog** (marketing surfaces only — lifted/adapted from lockin-test): + +*Hero headlines (landing H1):* +- "Stop lying to yourself." +- "Lock in. Or don't." + +*Section headlines (landing H2 per scrolled-to section):* +- "Three moves. No loopholes." (how-it-works) +- "Who's moving. Who's dead weight." (groups feature) +- "When you lock in, they know." (focus feature) +- "Don't break the chain." (streaks) +- "Small circles. Big weight." (witnesses / friend scope) +- "Everything on paper." (stats / activity — safe alternative to lockin-test's "on the record") + +*Section body / philosophy (landing subheads and short paragraphs):* +- "If you miss, they see." +- "No silent failures. No 'I'll start tomorrow.'" +- "habit trackers lie to you. this one doesn't. your friends are the chain." +- "life happens — but now you explain out loud instead of ghosting yourself." +- "social pressure only bites when it's people you'll see in lecture tomorrow." +- "tuesday's version of you cannot negotiate with friday's." + +*Atmospheric / CTA lead-ins:* +- "It's 11:47 PM. You were going to start tomorrow. You say that every night." +- "Ticker tape for your study group." (activity feed teaser) +- "no leaderboards. no strangers. 2–8 friends, max." + +*Objection framing:* +- Section heading: "Yeah, but…" +- Answer pattern: lowercase, direct, one sentence. e.g., "cool. those are for what you want to do. this is for what you said you'd do." + +*Sign-in preamble (before the Google button):* +- "Put your name on the line." +- "We use Google so you can't hide behind a burner account." + +*Dashboard / product UI (stays direct-positive, NOT wry):* +- Empty states: "Add your first pact to start building a streak." / "No pacts due today. Make one." +- Status: "3 pacts due today" / "Locked in 45 minutes" / "Streak at 12 days" +- Actions: "Mark kept" / "Lock in now" / "New pact" + +Use `your friends` in prose. Reserve `witnesses` as a structural label on the `WitnessTile` component chrome only. + +### Aesthetic Direction + +**Editorial with stamps.** Warm cream paper + distinctive ink palette + rubber stamps as the hero mechanic. The rubber stamp is what a student screenshots and sends to the group chat — a green `KEPT` stamp slamming onto a pact card, or a red `MISSED` stamp on one they blew. + +Light-first with full dark mode. Light mode is cream (OKLCH-tinted `oklch(0.96 0.012 85)`), never bright white. Dark mode is inverted paper (`oklch(0.14 0.012 60)` warm dark ink base with cream text) — dark stationery, not dark SaaS with glowing accents. Bright white is reserved for modal overlays only. + +Typography carries the mood. **Redaction** display for monumental moments (streak counters, hero headlines, level badges, hero numerals). **Host Grotesk** for body and UI. **JetBrains Mono** for timestamps, `§` numerals, tabular digits, pact indexes. Two-family pairing, distinct scales, editorial rhythm. + +**Highlighter yellow is the brand.** The rotated yellow square is the logo mark. Yellow-highlighter underlines sit beneath the key word in every headline. The selection color is highlighter yellow. The focus ring is highlighter yellow. When accent appears on a meaningful event — a `CLOSES SOON` sticker, a `pending` stamp, a locked-in progress bar on the timer — the default ink is highlighter yellow. It is not a decorative flourish; it is the brand signal. + +Five **inks** (accent palettes) are user-selectable, treated as personal identity: **Highlighter** (yellow, default), **Red Pen**, **Carbon Copy** (blue), **Moss** (green), **Indigo** (brand heritage, now opt-in). The earned-color principle governs *when* accent appears; the ink determines *which hue* the user sees it in. + +**The indigo → purple → magenta gradient has been retired as the primary brand signal.** It stays available as one of the five inks (for users who picked it before the redesign) and may appear on level-up celebrations as a legacy easter-egg, but it is no longer the default, the logo treatment, or the hero gradient. The old gradient pattern (`#5B5EF5 → #7C4DFF → #E040CB` on white) was an AI-SaaS-2024 fingerprint — retiring it is a deliberate move away from that fingerprint. Never as gradient text (impeccable's absolute ban applies). + +Motion is meaning. Dashboard motion is calm (entrance fades, list stagger, hover elevations). Celebrations are theatrical (stamp slams, reward pops, streak celebrations, confetti). Ambient motion (breathing glows, floating gradients) belongs to landing/marketing surfaces, not dashboard. + +**References (what to look toward):** +- BeReal — social accountability, friend-first feed +- Duolingo — gamification with character (not cartoon) +- Arc Browser — warm surfaces, earned color, sidebar as workspace +- Linear — restraint, density without clutter +- Harper's / The Paris Review — editorial spine, § numerals, serif display, footnote sensibility +- McSweeney's — typographic confidence, documentary rhythm +- Notion — document aesthetic without sterility + +**Anti-references (what to explicitly avoid):** +- Generic SaaS / AI-generated UI — gradient text, glassmorphism everywhere, side-stripe borders, icon grids, purple-blue gradients on white +- Sterile productivity apps — Todoist, Things 3 (too quiet, no personality) +- Cartoon RPG gamification — Habitica (too childish, breaks credibility) +- Legal / bureaucratic tone — courthouse ledger, "sworn under oath", "form 17-B", "public record" framing. The visual stamp-and-paper language carries the accountability; the vocabulary must stay warm. + +### Design Principles + +1. **Earned color, not decoration.** Color appears on meaningful events (streak-risk warning, pact resolution, milestone celebration, urgency tier) — never as page-level flourish. A cream dashboard that blooms with moss green when you keep a pact is correct. A dashboard that's colorful "to feel friendly" is wrong. + +2. **Stamps as resolution.** Every pact ends with a rubber stamp: `KEPT` (moss green), `MISSED` (red pen red), `LOCKED IN` (carbon blue during focus), or nothing (pending). The stamp rotates -3° at rest and animates `stampSlam` on resolution — scale overshoot, compression, recoil. Urgency uses ONE additional signal: a rotated `CLOSES SOON` highlighter sticker when the deadline is within four hours. Do not stack glow bars, pulse dots, border tints, and shadows on the same card. The stamp IS the mechanic. + +3. **Editorial typography, gamified content.** Redaction display for monumental moments (streak counters, hero, level badges). Host Grotesk for body and UI. JetBrains Mono for timestamps, § numerals, tabular numbers, pact indexes. `§ 01 / § 02 / § 03` section numerals on dashboard and stats pages. Pact index renders as `Pact #0047 · Mon · 10:00 PM` in monospace. Copy stays motivating-positive throughout — the editorial rhythm comes from typography and structure, not from legal vocabulary. + +4. **Warm cream, not bright white.** OKLCH palette with subtle yellow tint (~85 hue, 0.012 chroma) on neutrals. Light mode is cream; dark mode is warm dark ink. Bright white `oklch(1 0 0)` is reserved for modal overlays only. Surfaces tint toward brand yellow — subconscious cohesion that matches the "good notebook paper" brief. + +5. **Personal, not personalized.** Five distinctive inks (Highlighter default, Red Pen, Carbon Copy, Moss, Indigo brand). Each is user identity, not theme decoration. The accent-colored logo in the sidebar is not branding, it's ownership. + + **Ink cascade** — the user's chosen ink remaps `--stamp-yellow` and cascades across every personal-identity surface: + - Hero headline highlighter underline (landing) + - `NavMarker` hand-drawn highlighter stroke under nav links (hover) + - Text selection color (`::selection`) + - Focus-visible outline (keyboard navigation) + - `CLOSES SOON` sticker on urgent pacts + - `PENDING` stamp variant (pre-resolution) + - XP progress ring in the sidebar + - Focus session progress bar fill + - Level badge accent + - Heatmap cell intensity gradient + - Pact-template category chips + - Reaction badges on pending activity items + + **Semantic resolution colors do NOT cascade.** `KEPT` stays moss green, `MISSED` stays red pen red, `LOCKED IN` stays carbon blue regardless of the user's ink choice. These are product state signals, not personal identity — re-coloring them would break the shared visual vocabulary friends use to read each other's feeds. + +6. **Gamification with taste.** XP, Level, Streak, Achievement, Confetti — all kept. Execute with editorial polish. The XP ring in the sidebar is correct. The `+50 XP` reward burst on pact completion is correct. A bouncing mascot is wrong. Rename only where editorial language improves the metaphor; keep "XP / Level / Streak / Achievement" as primary product labels. + +7. **Motion as meaning.** Every animation answers "why is this moving?" Stamp slams mark resolution. Reward pops mark XP. Entrance fades teach layout. Stagger hints at ordering. Streak celebration marks milestones. Ambient motion is reserved for landing/marketing surfaces, not dashboard. Bounce/elastic easing is banned (impeccable rule); exponential easing (ease-out-quart/quint/expo) is preferred. + +--- + +## Intentional Deviations from Impeccable Defaults + +Impeccable has strong opinions. LockIn deviates from a few deliberately: + +### Glassmorphism on Modals (modals only — strictly) + +Impeccable says "DO NOT use glassmorphism everywhere." LockIn uses frosted glass on modals specifically (`--surface-glass`, rgba variables) and this is intentional — modals float above the dashboard and the dashboard blurs through them. + +**The rule:** glass belongs only on modals and overlays. Glass does **not** belong on: +- `PactCard` or any card surface +- Auth card / sign-in surfaces +- Sidebar, topbar, or chrome +- Buttons, inputs, dropdowns +- Page-level container backgrounds + +Non-modal surfaces use solid warm cream (`--bg-primary`, opaque `--surface-1`). **Violations are bugs, not precedent** — if you see `backdrop-filter: blur()` on a non-modal surface, remove it rather than propagate it. + +### Brand gradient retired — flat highlighter is the new brand + +The previous `.impeccable.md` carved out `--gradient-primary` (indigo → purple → magenta) as an intentional deviation from impeccable's "no purple gradients on white" rule. That carve-out is **closed** as of 2026-04-19. Highlighter yellow (flat, not gradient) is the new brand signal. + +Remaining uses of the old gradient: +- **Legacy only** — users who previously selected Indigo keep it as one of the five inks. +- **Level-up celebration** — allowed as an easter-egg for milestone-reach moments, since the gradient-burst animation is already built (`streakCelebration`). +- **Do not** reintroduce the gradient on new surfaces (logo, hero, CTAs, dashboard chrome). Those use flat color. + +### Fonts — System serif display + Host Grotesk body + JetBrains Mono + +- **Display:** `ui-serif, Georgia, 'Times New Roman', serif` — matches the lockin-test rendered look exactly. On macOS `ui-serif` resolves to New York (Apple system serif); on other systems, Charter/Cambria with Georgia as universal fallback. No web-font loading for display — relies on the OS. +- **Display-heavy:** Same family as display. Weight `400` — the rendered serif is naturally heavy at hero sizes; anything 700+ reads as over-bold vs lockin-test's reference. +- **Body / UI:** `"Host Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif` +- **Mono:** `"JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace` — timestamps, § numerals, tabular digits, pact indexes + +Host Grotesk + JetBrains Mono load via Google Fonts in `app/layout.js` with `font-display: swap` and preconnect to `fonts.googleapis.com` + `fonts.gstatic.com`. Display font is system-only — no loader. Neither loaded font is on impeccable's reflex-reject list. + +**Font history (four-pass evolution):** +- **Pass 1 (retired 2026-04-19):** Inter + Instrument Sans. Both on impeccable's reject list. +- **Pass 2 (considered, rejected):** Redaction (Forest Type / Titus Kaphar). Rejected because (a) its conceptual DNA is US legal-document aesthetic, contradicting the courthouse-vocabulary ban — font choice would undercut voice choice; (b) license is personal-use only via MCKL, not clearable for commercial distribution. +- **Pass 3 (considered, retired):** Besley (Owen Earl, slab-transitional, OFL-licensed). Loaded cleanly but rendered chunkier than lockin-test's elegant reference — slab feet too heavy for the editorial tone, wider letterforms than the transitional lockin-test hero. +- **Pass 4 (current):** System serif via `ui-serif`. Matches lockin-test rendering exactly on the target platform (macOS → New York). Trade-off accepted: cross-OS display-font rendering will vary, but the intent ("elegant transitional serif that doesn't need to be loaded") is preserved. + +**If you propose loading a display font in future:** check against the actual lockin-test rendered appearance first (run both dev servers, compare letterforms). Besley was retired for this reason after a visual comparison. Don't reintroduce Besley / Redaction / Instrument Serif / Fraunces / Playfair / DM Serif — none match the target. + +iOS app stays on system fonts (SF Pro) for native feel. + +### Colors in OKLCH (not hex) + +This redesign migrates the token system from hex/rgba to OKLCH throughout. Perceptual uniformity, future-proof color-mix support. The old hex palette had `globals.css` using hex/rgba; it's now OKLCH-first. + +### Light-first + dark mode (inverted paper) + +Dark mode is warm dark ink (`oklch(0.14 0.012 60)`) base with cream text (`oklch(0.96 0.012 85)`) — dark stationery, not dark SaaS with glowing accents. Toggle via `data-theme="dark"` on `` (same mechanism as before). + +--- + +## Component Vocabulary + +Key components referenced by name when building or critiquing: + +- **`Stamp`** — The hero component. Rubber-stamp resolution label with variants: `kept` (moss green), `missed` (red pen), `locked-in` (carbon blue), `pending` (highlighter yellow), `void` (muted neutral). Rotated `-3deg` at rest. Animates `stampSlam` on resolution (scale overshoot → compression → recoil). Used on `PactCard`, activity feed, share cards, onboarding preview. + +- **`PactCard`** — Personal commitment card with ONE urgency signal: + - Pending, deadline > 4h → no urgency signal. Just index + title + witnesses. + - Pending, deadline < 4h → rotated `CLOSES SOON` highlighter sticker, top-right, 3° tilt. + - In-progress (focus session active) → `LOCKED IN` stamp + pulse dot on avatar. + - Resolved (kept/missed) → `Stamp` in the card header with `stampSlam` on transition. + No glow bars. No stacked borders. No pulse + shadow + border-color combo. The urgency is the stamp. + +- **`SectionHeader`** — Editorial spine. `§ 01 — Today's pacts` style. JetBrains Mono `§ 0N` numeral, Host Grotesk title, uppercase 11px tracked caption for supplementary labels. Use on dashboard, stats, settings — any multi-section page. + +- **`WitnessTile`** (extends `UserAvatar`) — 2-letter initial + accent-colored tile (24-32px, 2px radius) as primary avatar. Google profile photo is the fallback, shown only when the user has explicitly uploaded one in settings. In activity feed, groups, pact witnesses — the initial-tile loads instantly, composes into `AvatarStack` for group rows, and stays brand-consistent. + +- **`StatusChip`** — Short pill: `KEPT / MISSED / CLOSES SOON / PENDING / IN PROGRESS`. 11px uppercase tracked, 2px radius, bordered in current color. Use when a full `Stamp` is too heavy (inline references, activity feed rows, toast notifications). The Stamp is the hero; the Chip is the utility version. + +- **`TodayBar`** — Unified top-of-dashboard status surface. Streak counter in Redaction display-heavy, pact count + focus time in Host Grotesk, streak-risk warning as `CLOSES SOON` chip, streak-freeze controls. First impression of the dashboard — sets the tone. + +- **`Sidebar`** — 72px collapsed / 260px expanded (Arc-style). XP progress ring + accent-colored logo = personal identity, not chrome. Typography updates from Instrument Sans to Redaction for the wordmark. + +- **`MobileNav`** — Bottom nav for mobile with level badge. Mirrors sidebar identity elements at mobile size. + +- **`ActivityFeed`** — Friend activity with emoji reactions and comments. First-class social proof, not a secondary widget. Uses `WitnessTile` for all avatars. + +- **`NavbarLanding`** — Landing page sticky navbar that collapses to a dynamic-island pill on scroll (`components/NavbarLanding.js:14-50`). **Signature brand moment — do not rewrite the pill-collapse behavior.** Typography uses Besley on the wordmark (retired the old Instrument Sans). CTAs are flat ink-dark-on-cream (primary) or flat text with a `NavMarker` highlighter on hover (secondary), matching the new brand direction. Nav links use `NavMarker` on hover (see below). + +- **`NavMarker`** — Hand-drawn highlighter stroke that animates in beneath navigation links on hover. SVG path with the `#marker-roughen` turbulence filter (loaded globally in `app/layout.js` ``) for organic ink-edge texture. Color follows the user's current ink (`var(--stamp-yellow)` → user's ink via cascade — not fixed yellow). Animation: `clip-path: inset(0 100% 0 0)` → `inset(0 0 0 0)` over ~380ms with `cubic-bezier(.16, 1, .3, 1)`. Use on `NavbarLanding` nav links and any landing-page navigation. Do NOT use on dashboard chrome — dashboard motion is calm, not editorial. Reference implementation: lockin-test `globals.css:281-313`. + +- **Animation presets** — `@/lib/animations` exports 80+ Framer Motion presets. Add for this redesign: `stampSlam`, `stampSlamClean`, `rewardPop`, `achievementIn` (supersedes old `sealIn`). Existing presets preserved: ambient*, fade*, slide*, scale*, stagger*, celebration*, streakCelebration. Use these. Don't define ad-hoc transitions in components. + +--- + +## Evaluated Alternatives + +### lockin-test ledger redesign (2026-04-19) — hybrid adopted + +`vayungodara/lockin-test` explored a courthouse/ledger aesthetic: Redaction + Host Grotesk fonts, cream-paper + ink OKLCH palette, rubber `KEPT / MISSED` stamps, § section numerals, monumental serif headlines, witness-tile avatars, and courthouse vocabulary (`sworn under oath`, `filed in perpetuity`, `on the record`, `form 17-B`, `regent`, `untouchable`). + +Scored dramatically higher on design polish (32/36 Nielsen vs 22/36; 2 impeccable-detector findings vs 27). The **visual** vocabulary was on-brand for "Duolingo grew up" — the **written** vocabulary overshot into legal gravitas. 19-year-old procrastinators at 1am need motivational warmth, not courthouse. + +**Adopted:** Redaction + Host Grotesk + JetBrains Mono typography, cream OKLCH palette (light + dark), rubber stamps as hero mechanic, `§ 01 / § 02 / § 03` section numerals, `WitnessTile` avatars, single-signal urgency (`CLOSES SOON` chip + `Stamp` on resolution, not glow-bar stack), low radii (2-4px), stamp-slam animation language. + +**Rejected:** The specific courthouse word list (banned in `Voice & UX Writing` above — `sworn`, `filed`, `regent`, `form 17-B`, `on the record`, etc.). Bureaucratic sign-in warnings. Newsprint noise texture. 0-radius stern cards (we use 2-4px for approachability). "Marks / Ranks / Seals" renaming of XP / Level / Achievement (editorial-friendly but kept original labels for product continuity). + +**Adopted as course-correction on 2026-04-19 (second pass):** +- The lockin-test landing voice was the actual target — confrontational-wry-direct — not the more cautious Duolingo-cheerleading tone the original `.impeccable.md` described. Voice rules updated to approve phrases like "Stop lying to yourself" and "You either kept it, or you didn't." +- The indigo → purple → magenta brand gradient is retired as the primary brand signal. Highlighter yellow (flat) is the new brand. The old gradient survives only as legacy ink + level-up easter-egg. + +**If you see courthouse vocabulary proposed in future work:** push back. The stamp-and-paper visual language does the accountability work. Say "kept" not "filed", "your friends" not "your witnesses" in prose (though `Witnesses` as a structural label in component chrome is fine). +**If you see the indigo-purple gradient proposed on new surfaces:** push back. It's a retired brand signal. Default to flat highlighter yellow or flat ink-dark. + +--- + +## Accessibility Commitments + +- **WCAG target:** AA. +- **Reduced motion:** Respect `prefers-reduced-motion`. The `prefersReducedMotion()` helper in `@/lib/animations` gates ambient and celebratory animations (including `stampSlam`, `rewardPop`, `achievementIn`). Reduced motion replaces slams with instant opacity fades. +- **Keyboard navigation:** All interactive surfaces reachable by keyboard, focus-visible outlines intact. Keyboard shortcuts documented in `KeyboardShortcutsContext`. +- **Focus rings:** 2px solid current-ink outline, 2px offset. Never removed on interactive elements. +- **Contrast on cream:** Low-chroma ink tones must hold 4.5:1 for body text, 3:1 for large text + UI components. Divider color `oklch(0.80 0.014 75)` on page `oklch(0.96 0.012 85)` is ~1.6:1 — below WCAG 1.4.11 — use `oklch(0.72 0.015 70)` or darker for card edges. +- **Contrast on glass surfaces:** Glass modals must maintain AA contrast against the blurred dashboard. Test with both light and dark themes behind the glass. +- **Dark mode contrast:** Warm dark ink base (`oklch(0.14 0.012 60)`) with cream text (`oklch(0.96 0.012 85)`) holds ~16:1 — well over AA. Reduce cream chroma for large display type so it doesn't vibrate. + +--- + +*This file was regenerated by `/impeccable teach` on 2026-04-19 after the lockin-test redesign evaluation. Update via the same command when brand direction shifts. Previous revisions: 2026-04-14 (original Duolingo-grown-up direction).* diff --git a/CLAUDE.md b/CLAUDE.md index 4380b88..313984c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,11 +11,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Agent Policy **Always use specialized/plugin agents** — never general-purpose when a domain agent exists: -- **Bugs & implementation:** `frontend-mobile-development:frontend-developer` or `javascript-typescript:javascript-pro` +- **Frontend — UI, components, design, a11y:** `lockin-frontend` (skip for typos/one-line fixes) +- **Bugs & implementation (non-UI):** `javascript-typescript:javascript-pro` - **Security:** `comprehensive-review:security-auditor` - **Code review:** `feature-dev:code-reviewer` or `comprehensive-review:code-reviewer` - **Debugging:** `unit-testing:debugger` or `agent-teams:team-debugger` -- **Visual/design:** `ui-design:ui-designer` - **iOS:** `multi-platform-apps:ios-developer` - **Performance:** `vercel:performance-optimizer` - **Architecture:** `feature-dev:code-architect` @@ -236,6 +236,7 @@ All tables use RLS. Users access only their own data or group data. 9. **Image remotes:** `next.config.mjs` allows images from `*.supabase.co` and `*.googleusercontent.com` 10. **createPortal SSR:** Guard `createPortal(...)` with `typeof document !== 'undefined'` when the portal renders outside an `isOpen` conditional 11. **Function naming:** Don't prefix non-hook functions with `use` — ESLint's `react-hooks/rules-of-hooks` will flag calls inside event handlers +12. **Glassmorphism scope:** Glass surfaces (`backdrop-filter: blur()`, `--surface-glass`) are allowed **only on modals and overlays** — never on `PactCard`, sidebar, auth cards, buttons, inputs, or page surfaces. See `.impeccable.md` → Intentional Deviations → Glassmorphism. ## iOS App (`/lockin-ios/`) @@ -294,28 +295,9 @@ Design doc: `/docs/plans/2026-03-08-ios-app-design.md` ## Design Context -> **For design work:** Use impeccable skills (`/critique`, `/polish`, `/arrange`, `/animate`, `/audit`, etc.). Full design brief is in `.impeccable.md`. Use `/impeccable teach` if `.impeccable.md` needs updating. +Full design brief lives in [`.impeccable.md`](./.impeccable.md) — users, brand personality, aesthetic direction, references/anti-references, 5 design principles, component vocabulary, and intentional deviations from impeccable defaults (fonts, glassmorphism, brand gradient). -**Users:** University students using social accountability to beat procrastination. The app should feel like their personal system — proud ownership through XP, streaks, and level badges. - -**Brand:** "Duolingo grew up for university." Motivating, Personal, Polished. - -**References:** BeReal + Duolingo (social pressure + gamification DNA), Arc Browser + Linear (design language — warm editorial, strategic color). - -**Anti-references:** Generic SaaS / AI-generated UI (gradient blobs, icon grids, identical hovers), minimalist/sterile (Todoist, Things 3), cartoon RPG (Habitica). - -**Design Principles:** -1. Earned color, not decoration — color appears on meaningful events, not everywhere -2. Social proof over empty states — surface friend activity as first-class content -3. Gamification with taste — XP/streaks/confetti stay, executed with editorial polish -4. Personal, not personalized — feels like their app (their streaks, their accent color) -5. Motion as meaning — dashboard motion is calm/utilitarian, landing is theatrical - -**Key Components:** -- `TodayBar` — replaces StreakHero + DailySummaryCard. Unified status surface with streak, pacts due, focus time, streak-risk/freeze controls, and milestone celebrations. -- `PactCard` — urgency hierarchy: overdue (red/elevated), due today (amber), completed (muted + XP badge), future (default). -- XP visible in Sidebar (progress ring) and MobileNav (level badge), not just dashboard header. -- Icons: `@phosphor-icons/react` for UI chrome. Keep emojis for content (templates, categories). +For design work use impeccable skills (`/critique`, `/polish`, `/arrange`, `/animate`, `/audit`, etc.) — they read `.impeccable.md` automatically. Update via `/impeccable teach` when brand direction shifts. ## Current Status diff --git a/app/dashboard/stats/StatsPage.module.css b/app/dashboard/stats/StatsPage.module.css index 2891eb9..9eb9651 100644 --- a/app/dashboard/stats/StatsPage.module.css +++ b/app/dashboard/stats/StatsPage.module.css @@ -39,24 +39,37 @@ gap: var(--space-6); } -/* Streak — compact inline summary */ -.streakInline { - font-size: var(--text-sm); - font-weight: 500; - color: var(--text-secondary); - margin: 0; +/* Streak — hero stat row with weighted hierarchy */ +.streakRow { display: flex; - align-items: center; - gap: var(--space-2); + align-items: baseline; flex-wrap: wrap; + gap: var(--space-4); + margin: 0; } -.streakEmoji { - font-size: 1.1rem; +.streakPrimary { + display: inline-flex; + align-items: baseline; + gap: var(--space-2); + font-family: var(--font-display); + font-size: var(--text-3xl); + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; line-height: 1; } -.streakSep { +.streakPrimaryLabel { + font-family: var(--font-sans); + font-size: var(--text-base); + font-weight: 500; + color: var(--text-secondary); + letter-spacing: 0; +} + +.streakMeta { + font-size: var(--text-sm); color: var(--text-tertiary); } @@ -79,6 +92,11 @@ border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-5); + transition: border-color var(--transition-fast); +} + +.analyticsCard:hover { + border-color: var(--border-default); } .analyticsCard h3 { diff --git a/app/dashboard/stats/StatsPageClient.js b/app/dashboard/stats/StatsPageClient.js index 5169867..411f13d 100644 --- a/app/dashboard/stats/StatsPageClient.js +++ b/app/dashboard/stats/StatsPageClient.js @@ -1,12 +1,14 @@ 'use client'; import { useState, useEffect, useCallback, useMemo } from 'react'; +import { motion } from 'framer-motion'; import { createClient } from '@/lib/supabase/client'; import { calculateStreak } from '@/lib/streaks'; import MonthlyCalendar from '@/components/MonthlyCalendar'; import EmptyState from '@/components/EmptyState'; import { SkeletonCard, SkeletonText } from '@/components/Skeleton'; import { Fire } from '@phosphor-icons/react'; +import { fadeInUp } from '@/lib/animations'; import styles from './StatsPage.module.css'; export default function StatsPageClient({ user }) { @@ -25,6 +27,9 @@ export default function StatsPageClient({ user }) { weekAgo.setDate(weekAgo.getDate() - 7); const monthAgo = new Date(now); monthAgo.setMonth(monthAgo.getMonth() - 1); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + sevenDaysAgo.setHours(0, 0, 0, 0); // Detect the user's IANA timezone so streak calculations bucket // activity into their local day (matches DashboardLayout persistence). @@ -35,62 +40,81 @@ export default function StatsPageClient({ user }) { // Intl API unavailable — fall back to UTC } - // Fetch streak data using the shared calculation from lib/streaks - const streak = await calculateStreak(supabase, user.id, timezone); - setStreakData(streak); + const uid = user.id; + + // Run streak + all count/lightweight queries in parallel. Count + // queries (head: true) return only a count, not rows — a massive + // win over the previous "fetch 5000 rows then filter in JS" pattern. + const [ + streak, + totalPactsRes, + completedPactsRes, + missedPactsRes, + activePactsRes, + thisWeekCompletedRes, + thisMonthCompletedRes, + focusTotalsRes, + thisWeekFocusRes, + thisMonthFocusRes, + firstFocusRes, + recentSessionsRes, + ] = await Promise.all([ + calculateStreak(supabase, uid, timezone), + // Pact counts + supabase.from('pacts').select('*', { count: 'exact', head: true }).eq('user_id', uid), + supabase.from('pacts').select('*', { count: 'exact', head: true }).eq('user_id', uid).eq('status', 'completed'), + supabase.from('pacts').select('*', { count: 'exact', head: true }).eq('user_id', uid).eq('status', 'missed'), + supabase.from('pacts').select('*', { count: 'exact', head: true }).eq('user_id', uid).eq('status', 'active'), + supabase.from('pacts').select('*', { count: 'exact', head: true }) + .eq('user_id', uid).eq('status', 'completed').gte('completed_at', weekAgo.toISOString()), + 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), + supabase.from('focus_sessions').select('*', { count: 'exact', head: true }) + .eq('user_id', uid).gte('started_at', weekAgo.toISOString()), + supabase.from('focus_sessions').select('*', { count: 'exact', head: true }) + .eq('user_id', uid).gte('started_at', monthAgo.toISOString()), + // Earliest session for avg-per-day denominator + supabase.from('focus_sessions').select('started_at').eq('user_id', uid) + .order('started_at', { ascending: true }).limit(1), + // Recent 7 days of sessions for the "Recent Focus Sessions" list + supabase.from('focus_sessions').select('id, started_at, duration_minutes, ended_at') + .eq('user_id', uid).gte('started_at', sevenDaysAgo.toISOString()) + .order('started_at', { ascending: false }).limit(20), + ]); + + const queryError = [ + totalPactsRes, completedPactsRes, missedPactsRes, activePactsRes, + thisWeekCompletedRes, thisMonthCompletedRes, focusTotalsRes, + thisWeekFocusRes, thisMonthFocusRes, firstFocusRes, recentSessionsRes, + ].find(r => r.error); + if (queryError) throw queryError.error; - // Fetch pact stats - const { data: pacts, error: pactsError } = await supabase - .from('pacts') - .select('status, completed_at, created_at') - .eq('user_id', user.id) - .order('created_at', { ascending: false }) - .limit(5000); - - if (pactsError) throw pactsError; + setStreakData(streak); - const completedCount = (pacts || []).filter(p => p.status === 'completed').length; - const missedCount = (pacts || []).filter(p => p.status === 'missed').length; - const activeCount = (pacts || []).filter(p => p.status === 'active').length; - const thisWeekCompleted = (pacts || []).filter(p => - p.status === 'completed' && p.completed_at && new Date(p.completed_at) >= weekAgo - ).length; - const thisMonthCompleted = (pacts || []).filter(p => - p.status === 'completed' && p.completed_at && new Date(p.completed_at) >= monthAgo - ).length; + const completedCount = completedPactsRes.count || 0; + const missedCount = missedPactsRes.count || 0; + const activeCount = activePactsRes.count || 0; setPactStats({ - total: (pacts || []).length, + total: totalPactsRes.count || 0, completed: completedCount, missed: missedCount, active: activeCount, - thisWeek: thisWeekCompleted, - thisMonth: thisMonthCompleted, + thisWeek: thisWeekCompletedRes.count || 0, + thisMonth: thisMonthCompletedRes.count || 0, completionRate: completedCount + missedCount > 0 ? Math.round((completedCount / (completedCount + missedCount)) * 100) - : 0 + : 0, }); - // Fetch focus stats - const { data: sessions, error: focusError } = await supabase - .from('focus_sessions') - .select('id, started_at, duration_minutes, ended_at') - .eq('user_id', user.id) - .order('started_at', { ascending: false }) - .limit(5000); - - if (focusError) throw focusError; - - const totalMinutes = (sessions || []).reduce((acc, s) => acc + (s.duration_minutes || 0), 0); - const sessionsCount = (sessions || []).length; - const thisWeekSessions = (sessions || []).filter(s => - s.started_at && new Date(s.started_at) >= weekAgo - ).length; - const thisMonthSessions = (sessions || []).filter(s => - s.started_at && new Date(s.started_at) >= monthAgo - ).length; - const daysSinceFirst = sessionsCount > 0 - ? Math.max(1, Math.ceil((now - new Date((sessions || [])[(sessions || []).length - 1].started_at)) / (1000 * 60 * 60 * 24))) + const focusSessions = focusTotalsRes.data || []; + const totalMinutes = focusSessions.reduce((acc, s) => acc + (s.duration_minutes || 0), 0); + const sessionsCount = focusSessions.length; + const firstStartedAt = firstFocusRes.data?.[0]?.started_at; + const daysSinceFirst = firstStartedAt + ? Math.max(1, Math.ceil((now - new Date(firstStartedAt)) / (1000 * 60 * 60 * 24))) : 1; const avgPerDay = sessionsCount > 0 ? Math.round(totalMinutes / daysSinceFirst) : 0; @@ -98,21 +122,12 @@ export default function StatsPageClient({ user }) { totalMinutes, sessionsCount, avgDuration: sessionsCount > 0 ? Math.round(totalMinutes / sessionsCount) : 0, - thisWeekSessions, - thisMonthSessions, - avgPerDay + thisWeekSessions: thisWeekFocusRes.count || 0, + thisMonthSessions: thisMonthFocusRes.count || 0, + avgPerDay, }); - // Get recent sessions (last 7 days, grouped by day) - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - sevenDaysAgo.setHours(0, 0, 0, 0); - - const recentSessionsData = (sessions || []) - .filter(s => new Date(s.started_at) >= sevenDaysAgo) - .slice(0, 20); - - setRecentSessions(recentSessionsData); + setRecentSessions(recentSessionsRes.data || []); } catch (err) { console.error('Error fetching stats:', err); @@ -203,12 +218,17 @@ export default function StatsPageClient({ user }) { return (
-
+

Your Stats

Track your productivity and progress

-
+
{/* Top-level empty state when user has zero activity */} @@ -233,15 +253,20 @@ export default function StatsPageClient({ user }) { /> )} - {/* Streak Summary — compact inline */} -

- {' '} - {streakData.currentStreak} day streak{' '} - ·{' '} - Best: {streakData.longestStreak} day{' '} - ·{' '} - {streakData.totalCompleted} completed -

+ {/* Streak Summary — hero row */} +
+ + + {streakData.currentStreak} + day streak + + + Best: {streakData.longestStreak} {streakData.longestStreak === 1 ? 'day' : 'days'} + + + {streakData.totalCompleted} completed + +
{/* Activity Calendar */} diff --git a/app/globals.css b/app/globals.css index e6d26cc..87be2de 100644 --- a/app/globals.css +++ b/app/globals.css @@ -195,6 +195,99 @@ --landing-tension: #EA580C; --landing-border: rgba(9, 9, 11, 0.08); --landing-border-strong: rgba(9, 9, 11, 0.15); + + /* ================================================================ */ + /* EDITORIAL REDESIGN — Wave 1 foundation (2026-04-19) */ + /* Additive. Existing tokens above are preserved for back-compat. */ + /* Adopts: cream OKLCH palette, stamp semantics, Redaction stack, */ + /* highlighter-yellow brand, low-radius editorial scale. */ + /* ================================================================ */ + + /* Ink palette — warm paper tinted toward yellow (~85 hue) */ + --ink-000: oklch(0.99 0.004 85); /* paper highlight */ + --ink-050: oklch(0.96 0.012 85); /* page — cream */ + --ink-100: oklch(0.94 0.014 85); /* card — tinted cream */ + --ink-200: oklch(0.88 0.016 80); /* elevated */ + --ink-300: oklch(0.80 0.014 75); /* divider */ + --ink-400: oklch(0.60 0.015 70); /* tertiary text */ + --ink-500: oklch(0.46 0.014 70); /* muted text */ + --ink-600: oklch(0.36 0.014 65); /* secondary text */ + --ink-700: oklch(0.26 0.014 60); /* body text */ + --ink-800: oklch(0.20 0.014 60); /* hi-text — ink */ + --ink-900: oklch(0.12 0.012 55); /* darkest ink */ + + /* Paper — extra-cream surface variant */ + --paper: oklch(0.97 0.018 88); + --paper-ink: oklch(0.20 0.018 60); + + /* Stamp semantic palette — earned color, meaningful events only */ + --stamp-yellow: oklch(0.88 0.13 92); /* highlighter — brand signal */ + --stamp-yellow-ink: oklch(0.18 0.018 70); /* dark ink on yellow */ + --stamp-red: oklch(0.52 0.20 26); /* arterial — MISSED */ + --stamp-red-soft: oklch(0.74 0.08 26); + --stamp-green: oklch(0.52 0.14 142); /* moss — KEPT */ + --stamp-green-soft: oklch(0.78 0.06 142); + --stamp-blue: oklch(0.50 0.16 245); /* carbon — LOCKED IN */ + + /* Brand color aliases — highlighter yellow replaces the retired + indigo→purple→magenta gradient as the primary brand signal. + --brand-ink is the dark counterpart for primary CTAs on cream. */ + --brand-primary: var(--stamp-yellow); + --brand-ink: var(--ink-900); + + /* Editorial font stacks — Redaction + Host Grotesk + JetBrains Mono. + Namespaced with `-editorial` suffix to avoid clobbering the existing + --font-sans / --font-display / --font-mono tokens that components + across the app still reference. Wave 3 will promote these once + components stop referencing the Inter / Instrument Sans stacks. */ + /* Display stack matches lockin-test — ui-serif resolves to New York on macOS, + Charter/Cambria on other systems, Georgia as the universal fallback. */ + --font-display-editorial: ui-serif, Georgia, 'Times New Roman', serif; + --font-display-heavy-editorial: ui-serif, Georgia, 'Times New Roman', serif; + --font-sans-editorial: 'Host Grotesk', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + --font-mono-editorial: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace; + + /* Low-radius editorial scale — "mostly square, we're not a SaaS". + Namespaced because the existing --radius-sm (6px) and --radius-md + (8px) tokens are load-bearing for 49 components. Editorial surfaces + opt in explicitly via these variables. */ + --radius-editorial-xs: 0px; + --radius-editorial-sm: 2px; + --radius-editorial-md: 4px; + + /* Stamp easing — approved overshoot for the physical-stamp metaphor */ + --ease-stamp: cubic-bezier(.22, 1.6, .36, 1); +} + +/* ==================================================================== */ +/* Five-ink accent system (editorial redesign) */ +/* Attribute scope on : [data-ink="highlighter" | "redpen" */ +/* | "carbon" | "moss" | "indigo-legacy"]. Highlighter is default */ +/* and inherits from :root. Scope overrides --stamp-yellow + */ +/* --stamp-yellow-ink so every ink-bearing surface follows the user's */ +/* choice. Indigo-legacy maps to a flat single-stop ink so previous */ +/* Indigo users don't see a jarring break (gradient retired). */ +/* ==================================================================== */ +[data-ink="highlighter"] { + --stamp-yellow: oklch(0.88 0.13 92); + --stamp-yellow-ink: oklch(0.18 0.018 70); +} +[data-ink="redpen"] { + --stamp-yellow: oklch(0.64 0.22 28); + --stamp-yellow-ink: oklch(0.99 0.005 60); +} +[data-ink="carbon"] { + --stamp-yellow: oklch(0.72 0.15 230); + --stamp-yellow-ink: oklch(0.14 0.010 70); +} +[data-ink="moss"] { + --stamp-yellow: oklch(0.56 0.14 142); + --stamp-yellow-ink: oklch(0.99 0.005 140); +} +[data-ink="indigo-legacy"] { + /* Flat single-stop — gradient retired as brand signal (see .impeccable.md) */ + --stamp-yellow: oklch(0.58 0.19 278); + --stamp-yellow-ink: oklch(0.99 0.005 280); } /* === DARK MODE — "Deep Space" === */ @@ -282,6 +375,59 @@ --landing-tension: #F97316; --landing-border: rgba(244, 242, 235, 0.08); --landing-border-strong: rgba(244, 242, 235, 0.15); + + /* ================================================================ */ + /* EDITORIAL REDESIGN — Wave 1 dark mode (inverted paper) */ + /* Dark mode is "dark stationery", not "dark SaaS with glow". */ + /* Ink scale inverts: 000 is darkest, 900 is cream. Stamp colors */ + /* stay perceptually similar but drop chroma slightly for softness. */ + /* ================================================================ */ + --ink-000: oklch(0.12 0.012 55); /* warm dark base — was cream */ + --ink-050: oklch(0.14 0.012 60); /* page — warm dark ink */ + --ink-100: oklch(0.18 0.014 60); /* card */ + --ink-200: oklch(0.22 0.014 62); /* elevated */ + --ink-300: oklch(0.32 0.014 68); /* divider */ + --ink-400: oklch(0.52 0.014 72); /* tertiary text */ + --ink-500: oklch(0.62 0.012 76); /* muted text */ + --ink-600: oklch(0.74 0.012 80); /* secondary text */ + --ink-700: oklch(0.84 0.012 82); /* body text */ + --ink-800: oklch(0.92 0.012 84); /* hi-text */ + --ink-900: oklch(0.96 0.012 85); /* cream — was darkest ink */ + + /* Paper — inverted */ + --paper: oklch(0.16 0.014 62); + --paper-ink: oklch(0.94 0.012 84); + + /* Stamp palette — slightly softer chroma on dark */ + --stamp-yellow: oklch(0.86 0.11 92); + --stamp-yellow-ink: oklch(0.18 0.018 70); + --stamp-red: oklch(0.60 0.17 26); + --stamp-red-soft: oklch(0.42 0.09 26); + --stamp-green: oklch(0.62 0.12 142); + --stamp-green-soft: oklch(0.42 0.07 142); + --stamp-blue: oklch(0.62 0.14 245); +} + +/* Five-ink scope overrides in dark mode — dark variants tuned for cream-on-ink reads */ +[data-theme="dark"][data-ink="highlighter"] { + --stamp-yellow: oklch(0.86 0.11 92); + --stamp-yellow-ink: oklch(0.18 0.018 70); +} +[data-theme="dark"][data-ink="redpen"] { + --stamp-yellow: oklch(0.60 0.17 28); + --stamp-yellow-ink: oklch(0.99 0.005 60); +} +[data-theme="dark"][data-ink="carbon"] { + --stamp-yellow: oklch(0.68 0.13 230); + --stamp-yellow-ink: oklch(0.14 0.010 70); +} +[data-theme="dark"][data-ink="moss"] { + --stamp-yellow: oklch(0.60 0.12 142); + --stamp-yellow-ink: oklch(0.12 0.010 140); +} +[data-theme="dark"][data-ink="indigo-legacy"] { + --stamp-yellow: oklch(0.66 0.16 278); + --stamp-yellow-ink: oklch(0.99 0.005 280); } /* System preference fallback — handled by inline script in layout.js @@ -1115,9 +1261,12 @@ p { } /* === SELECTION STYLING === */ +/* Selection color matches the hero highlighter — opaque highlighter yellow + (cascades with the user's chosen ink via --stamp-yellow) on dark ink text. + Same behavior as lockin-test: real-highlighter feel, no transparency. */ ::selection { - background: rgba(var(--accent-primary-rgb), 0.3); - color: inherit; + background: var(--stamp-yellow); + color: var(--stamp-yellow-ink); } /* === FOCUS VISIBLE === */ diff --git a/app/join/[code]/JoinPage.module.css b/app/join/[code]/JoinPage.module.css index f204749..cb8d142 100644 --- a/app/join/[code]/JoinPage.module.css +++ b/app/join/[code]/JoinPage.module.css @@ -100,14 +100,18 @@ justify-content: center; gap: var(--space-2); padding: var(--space-3) var(--space-4); - background: var(--gradient-primary); + background: var(--brand-ink, var(--text-primary)); border: none; border-radius: var(--radius-lg); - color: white; + color: var(--ink-050, #F4F2EB); font-size: var(--text-base); font-weight: 600; cursor: pointer; - transition: all var(--transition-fast); + transition: background var(--transition-fast), opacity var(--transition-fast); +} + +.joinBtn:hover:not(:disabled) { + opacity: 0.9; } .joinBtn:disabled { diff --git a/app/layout.js b/app/layout.js index d45ad9e..a19c853 100644 --- a/app/layout.js +++ b/app/layout.js @@ -55,6 +55,15 @@ const themeScript = ` } document.documentElement.setAttribute('data-theme', resolved); + // Five-ink cascade (editorial redesign) — read lockin-ink from localStorage + // and expose it as [data-ink="..."] on before first paint. + // Separate from the accent-palette script below; runs in parallel during + // the 7-accents-to-5-inks transition. See globals.css :234 for scope rules. + var validInks = ['highlighter', 'redpen', 'carbon', 'moss', 'indigo-legacy']; + var inkKey = localStorage.getItem('lockin-ink'); + if (!inkKey || validInks.indexOf(inkKey) === -1) inkKey = 'highlighter'; + document.documentElement.setAttribute('data-ink', inkKey); + var accentId = localStorage.getItem('lockin-accent'); if (accentId && accentId !== 'indigo') { var palettes = { @@ -104,9 +113,40 @@ export default function RootLayout({ children }) { return ( + {/* Editorial redesign fonts — Host Grotesk body + JetBrains Mono. + Display serif is system (ui-serif → New York on macOS, Charter/Cambria elsewhere, + Georgia as universal fallback) — matches lockin-test's rendered look exactly on macOS. + Besley was considered but rendered too chunky/slab vs the lockin-test elegance. */} + + +