V1 Web PWA · Surface walkthrough

Affirmations · Arete · OSHUN

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
8sections4 minread

On this page

Context. surface customer · domain arete · route /domains/arete/affirmations · auth signed-in · source apps/oshun/web/src/app/domains/arete/affirmations/page.tsx

Last walked. 2026-06-28 automated Playwright walk — authenticated direct route, daily card arrow and position controls, tab selected state, Browse filters, favorite toggle state, custom creation, route-level axe scan, render, expected content, and console/page-errors verified. Evidence: apps/oshun/web/e2e/arete-affirmations-accessibility.spec.ts

Purpose#

Internal /domains/arete/* alternate surface for affirmations: a tabbed workspace with a daily card, browse / create / schedule / journal / streak views. Backed by fixture constants from @/lib/arete/arete-extended-simulation rather than a live BFF. Distinct from the polished /arete/* Lilith surfaces — heavier, more app-like, client-rendered.

Entry points#

  • Direct URL / bookmark — yes (signed-in)
  • From /domains/arete/* peer navigation — no explicit cross-link in source; each /domains/arete/<x> page renders its own component without shared chrome
  • No link from /arete polished surface — verify

Layout regions#

page.tsx is a 'use client' page: renders an SR-only heading "Affirmations · Arete · OSHUN" and <AreteAffirmations />. The component lives at apps/oshun/web/src/components/domains/arete/AreteAffirmations.tsx.

  • Container: max-width: 900px; margin: 0 auto; padding: 24px
  • Header: Sparkles icon + h2 "Affirmations" + subtitle "Daily positive affirmations to transform your mindset and build self-belief"
  • Tab bar (6 tabs, overflowX: 'auto'):
    • daily — Daily Card (Sparkles icon)
    • browse — Browse (BookOpen icon)
    • create — Create (Plus icon)
    • schedule — Schedule (Clock icon)
    • journal — Journal (Edit3 icon)
    • streak — Streak (Flame icon)
  • Content area (rendered per active tab):
    • daily<DailyCardView /> — gradient-backed card with one affirmation, prev/next arrows, dots indicator, 3 stat tiles (Day Streak, Favorites, Days Practiced) — driven by AFFIRMATIONS, AFFIRMATION_STREAK
    • browse<BrowseView />
    • create<CreateView />
    • schedule<ScheduleView />
    • journal<JournalView /> (driven by AFFIRMATION_JOURNAL_ENTRIES)
    • streak<StreakView />

States#

  • Loading — N/A: client component with sync fixture import; no async boundary
  • Daily card revealedisRevealed === true runs aff-card-reveal animation
  • Daily card hidden (mid-transition) — between prev/next, isRevealed = false, then setTimeout 100ms re-true to retrigger animation
  • Tab switching — instant; no skeleton; covered by arete-affirmations-accessibility.spec.ts
  • Empty dailyAffirmationsif (!current) return null short-circuits to nothing
  • Empty AFFIRMATIONS (favorites/others both empty) — same null return path
  • Error (recoverable) — relies on app-level error boundary; no error.tsx under /domains/arete
  • Offline — purely client-side fixture; renders identically offline once the bundle is loaded
  • Gated — N/A
  • Standalone PWA — no shell chrome; the page mounts directly with no LWebShell or customer nav around it (compare to /arete which has full Lilith customer nav)

Interactions#

Tab bar (6 tabs)#

  • Daily Card / Browse / Create / Schedule / Journal / Streak (buttons)
    • Function: each calls setActiveTab(tab.id) to toggle active view
    • Keyboard: native <button> tab stops; Browser assertion covers tab switching via click, and native Enter/Space behavior is retained
    • Screen reader: role="tab", aria-selected, and stable tabpanel association expose selected state
    • Touch target: min-height: 44px
    • Active style: accent background tint and accent color
    • Telemetry: none

Daily card#

  • "Previous" arrow (button, aria-label="Previous")
    • Function: handlePrev — decrements currentIndex with wraparound; flickers isRevealed to retrigger the entry animation
    • Screen reader: announces "Previous"
    • Touch target: 44×44 px; covered by Playwright bounding-box assertion
  • "Next" arrow (button, aria-label="Next")
    • Function: handleNext — increments currentIndex with wraparound
    • Touch target: 44×44 px; covered by Playwright bounding-box assertion
  • Dot indicator (buttons named "Show affirmation N of 6")
    • Function: clicking a dot jumps to that affirmation
    • Keyboard: native button activation; Playwright focuses a position button and presses Enter to verify movement
    • Screen reader: group label "Affirmation card position"; active button uses aria-current="true"
  • Category chip — shows AFFIRMATION_CATEGORY_LABELS[current.category]; presentational

Stats row (Day Streak / Favorites / Days Practiced)#

  • Stat tile — flame icon + numeral + label; static; no interaction

Browse view#

  • Category filters (buttons)
    • Function: All and each category call setFilterCategory(...)
    • Screen reader: active filter exposes aria-pressed="true"
    • Touch target: min-height: 44px
  • Favorite heart (button per affirmation)
    • Function: toggles the local favorites Set
    • Screen reader: accessible name changes between "Remove from favorites: ..." and "Save to favorites: ..."
    • State: exposes aria-pressed
    • Persistence: local component state only; no API write

Create view#

  • Affirmation text / category fields
    • Function: textarea stores the custom affirmation text; select stores the category
    • Screen reader: controls expose aria-label="Affirmation text" and aria-label="Affirmation category"
  • Create Affirmation (button)
    • Function: appends a custom affirmation to local component state and renders it under "Your Custom Affirmations"
    • Persistence: local component state only; no API write

Data & contracts#

  • Reads: import-time AFFIRMATIONS, AFFIRMATION_SCHEDULE, AFFIRMATION_JOURNAL_ENTRIES, AFFIRMATION_STREAK, AFFIRMATION_CATEGORY_COLORS, AFFIRMATION_CATEGORY_LABELS from @/lib/arete/arete-extended-simulation. No BFF calls.
  • Writes: none — useState mutations are local to the component and not persisted
  • Realtime: none
  • Caching: client bundle only
  • Auth/role check: app-level middleware

Cross-references#

  • Domain hub: arete.md
  • Adjacent /domains/arete/* pages — see arete.md for full list
  • No direct counterpart in /arete/* polished routes (affirmations is a domains-namespace-only surface)
  • Component source: apps/oshun/web/src/components/domains/arete/AreteAffirmations.tsx
  • Simulation data: apps/oshun/web/src/lib/arete/arete-extended-simulation.ts
  • Types: apps/oshun/web/src/lib/arete/arete-extended-types.ts

Open questions / known gaps#

  • No shared chrome with the polished /arete Lilith room — verify whether /domains/arete/* is a V1-shipping customer surface or a design-system internal showcase
  • Dot indicators use native buttons with names, 44 px activation boxes, and aria-current
  • Prev/next arrow touch targets are 44 px
  • State mutations don't persist across page reloads (no API persistence)
  • Browse favorite toggles expose accessible names and aria-pressed; the action is intentionally local and still has no API persistence