---
path: /domains/arete/affirmations
surface: customer
domain: arete
auth: signed-in
source: apps/oshun/web/src/app/domains/arete/affirmations/page.tsx
status: walked
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'
---

# Affirmations · Arete · OSHUN

## 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 revealed** — `isRevealed === true` runs `aff-card-reveal`
      animation
- [ ] **Daily card hidden (mid-transition)** — between prev/next,
      `isRevealed     = false`, then `setTimeout` 100ms re-true to retrigger
      animation
- [x] **Tab switching** — instant; no skeleton; covered by
      `arete-affirmations-accessibility.spec.ts`
- [ ] **Empty `dailyAffirmations`** — `if (!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)

- [x] **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

- [x] **"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
- [x] **"Next" arrow** (button, `aria-label="Next"`)
  - Function: `handleNext` — increments `currentIndex` with wraparound
  - Touch target: 44×44 px; covered by Playwright bounding-box assertion
- [x] **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

- [x] **Category filters** (buttons)
  - Function: `All` and each category call `setFilterCategory(...)`
  - Screen reader: active filter exposes `aria-pressed="true"`
  - Touch target: `min-height: 44px`
- [x] **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

- [x] **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"`
- [x] **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`](./arete.md)
- Adjacent `/domains/arete/*` pages — see [`arete.md`](./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
- [x] Dot indicators use native buttons with names, 44 px activation boxes, and
      `aria-current`
- [x] Prev/next arrow touch targets are 44 px
- [ ] State mutations don't persist across page reloads (no API persistence)
- [x] Browse favorite toggles expose accessible names and `aria-pressed`; the
      action is intentionally local and still has no API persistence
