Note: This is a planning artifact generated by
/peak-workflow:setup. Sections marked with (to be completed during implementation) will be populated as epics are implemented. Run/peak-workflow:refresh-docsafter completing epics to update this document to reflect the as-built system.
Summit is a single-page habit tracker: users add habits, mark them done for the day, track a
streak count per habit, and filter by active/archived status. It is a client-only SPA with no
backend — all state is persisted in the browser's localStorage. It is built as the public
reference example for the peak-workflow plugin.
| Layer | Technology | Purpose |
|---|---|---|
| Language | TypeScript | Application logic and type safety |
| Build tool / dev server | Vite | Local dev server, bundling, build-time constants |
| Package manager | npm | Dependency management |
| Persistence | localStorage |
Client-side storage of habits and completion state |
| Styling | PeakFlames Design System (vendored token CSS + .pf-* classes) |
Brand canvas, type ramp, and component visual treatment |
No external data sources or APIs. All application state lives in the browser's
localStorage — no database, no network calls.
N/A — this is a client-only SPA with no backend or API surface.
N/A — there is no backend.
Single-page app with no router — one screen, rendered imperatively with the DOM API (no framework). Component hierarchy (decomposed across Epics Yz4JE9Z, WKhBuVK, and e3mj8uq):
src/styles/peakflames/ holds the PeakFlames Design System's token CSS, vendored
byte-identical from the design project (styles.css plus 10 tokens/*.css files — see
design-notes.md §14). src/styles/main.css imports it as its first line, then layers
Summit-specific layout (flex containers, gaps, list-row padding) and a small number of state
overrides the vendored .pf-* classes can't express (the amber, not flame, streak value; a
desaturated done-button treatment). Components add .pf-* classes alongside their existing
class names — see Key Components in docs/implementation-plan/phase-3-frontend/epic-R5e7z3Y-*.
src/main.ts — bootstrap: emits the startup console.info line, then mounts the app
src/App.ts — mountApp(root) + render(root): composition root
renderAddHabitForm() — src/components/AddHabitForm.ts
renderFilterToggle() — src/components/FilterToggle.ts
renderStreakHint() — src/components/StreakHint.ts (shown only in Active view)
renderHabitList() — src/components/HabitList.ts
renderHabitCard() — src/components/HabitCard.ts (per habit)
renderStreakBadge() — src/components/StreakBadge.ts (displays streak count)
renderEmptyState() — src/components/EmptyState.ts (when no habits in view)
renderFooter() — src/components/Footer.ts
State is split across four layers:
- Persistence:
src/storage/habitStore.tsandsrc/storage/streakRecalculation.tsloadHabits()/saveHabits()against the namespacedsummit.habitskey inlocalStorage, holding the full dataset as a single JSON arraytodayISO()(local-dateYYYY-MM-DD, not UTC) andrecalculateAll()for load-time streak staleness recalculation
- Domain models:
src/models/Habit.ts— theHabittype:{ name, streak, lastCompletedDate, archived } - Mutation layer:
src/state/habitActions.tsaddHabit(habits, name)— inserts a new habit, callssaveHabits()archiveHabit(habits, target)/unarchiveHabit(habits, target)— mutate archived state, callsaveHabits()markDone(habits, target, today)— applies streak arithmetic viamarkDoneStreak(), persists viasaveHabits()
- Streak logic:
src/state/streakLogic.tsmarkDoneStreak(habit, today)— pure function implementing increment/no-op/reset rules: returns no-op if already done today, increments if completed yesterday, resets to 1 otherwise
- View state:
src/state/viewState.tscreateViewState()— encapsulates the current filter view (Active / Archived)filterHabits(habits, view)— filters the habit list by view
mountApp loads and recalculates on every mount, then persists the recalculated result back
(so a staleness reset survives the session) before the first render. Every subsequent mutation
(add, done-today toggle, archive/unarchive) calls saveHabits() synchronously before
re-rendering, guaranteeing the write completes before the next user action is possible.
The app version is a compile-time constant (__APP_VERSION__), injected by Vite's
define in vite.config.ts from package.json's version field — not imported into
src/ at runtime — per CLAUDE.md's "Version single source of truth" convention. The same
vite.config.ts config also configures Vitest (test.environment: 'jsdom', setupFiles: ['./tests/setup.ts']), so __APP_VERSION__ is substituted in unit tests too.
Test environment note: Node 22+'s experimental native localStorage global shadows
jsdom's working implementation, leaving localStorage non-functional in Vitest tests.
tests/setup.ts works around this by installing a minimal in-memory MemoryStorage
polyfill (implements Storage) onto globalThis before tests run — the app itself touches
only the real browser localStorage API.
N/A — no background jobs, scheduled tasks, or hosted services.
npm run build runs tsc --noEmit for type checking, then vite build, producing a static
dist/ directory (HTML, JS, CSS) with no server-side component. npm run preview serves
that dist/ output locally for a production-like smoke check.
Summit deploys to GitHub Pages (project page at https://peakflames.github.io/summit/).
.github/workflows/deploy-pages.yml runs lint, tests, and the build on every push to main,
then publishes dist/ via actions/upload-pages-artifact + actions/deploy-pages. Because
Pages serves project pages from /<repo>/ rather than /, the build sets
base: '/summit/' in vite.config.ts — gated behind a GH_PAGES=true env var the workflow
sets, so local dev/build/preview are unaffected. See CLAUDE.md's Deployment section
for details.
No authentication or authorization — single-user, client-only application with no server-side data. No secrets are expected in this codebase (see CLAUDE.md's Security Baseline).