From 80ebb023448c045d8588c58ae0d3a692be2c4fd1 Mon Sep 17 00:00:00 2001 From: Frederic Cabassut Date: Fri, 20 Mar 2026 22:36:54 +0100 Subject: [PATCH] docs: add dark mode implementation plan 10-task plan covering CSS variables, preferences, FOUC prevention, reactive theme application, component updates, and testing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../superpowers/plans/2026-03-20-dark-mode.md | 781 ++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-20-dark-mode.md diff --git a/docs/superpowers/plans/2026-03-20-dark-mode.md b/docs/superpowers/plans/2026-03-20-dark-mode.md new file mode 100644 index 0000000..8f15f0c --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-dark-mode.md @@ -0,0 +1,781 @@ +# Dark Mode Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a dark parchment theme variant with system/light/dark toggle in Settings > Preferences. + +**Architecture:** CSS `[data-theme="dark"]` attribute on `` overrides CSS variables in `app.css`. A blocking inline script in `app.html` prevents FOUC. Theme preference uses the existing `UserPreferences` key-value system. A reactive `isDarkMode` rune in `theme.svelte.ts` drives note card color switching. + +**Tech Stack:** SvelteKit, Svelte 5 runes, CSS custom properties, Tailwind CSS v4, Vitest, Playwright + +**Spec:** `docs/superpowers/specs/2026-03-20-dark-mode-design.md` + +--- + +## File Map + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `src/app.css` | Add `--grid-dot` to `:root`, add `[data-theme="dark"]` block, update `body::before` | +| Modify | `src/app.html` | Add FOUC-prevention inline script | +| Modify | `src/lib/types/preferences.ts` | Add `theme` field to `UserPreferences` | +| Create | `src/lib/utils/theme.svelte.ts` | `applyTheme()`, reactive `isDarkMode` rune | +| Modify | `src/lib/utils/colors.ts` | Add `NOTE_COLORS_DARK`, export `getNoteColor()` | +| Modify | `src/routes/+layout.svelte` | Add theme `$effect` watcher | +| Modify | `src/routes/(app)/settings/preferences/+page.svelte` | Add theme segmented toggle | +| Modify | `src/lib/components/NoteCard.svelte` | Use `getNoteColor()` with `isDarkMode` | +| Modify | `src/lib/components/NoteEditor.svelte` | Use `getNoteColor()` with `isDarkMode` | +| Modify | `src/lib/components/ColorPicker.svelte` | Use dark color swatches when `isDarkMode` | +| Skip | `src/routes/(share)/s/[token]/+page.svelte` | Out of scope per spec (public share pages) | +| Modify | `.gitignore` | Add `.superpowers/` | +| Create | `tests/unit/theme.test.ts` | Unit tests for `applyTheme` and `isDarkMode` | +| Create | `tests/unit/colors.test.ts` | Unit tests for `getNoteColor` | +| Create | `tests/e2e/dark-mode.spec.ts` | E2E tests for theme toggle, persistence, system detection | + +--- + +### Task 1: CSS Dark Theme Variables + +**Files:** +- Modify: `src/app.css:6-23` (`:root` block) and `src/app.css:36` (`body::before`) + +- [ ] **Step 1: Add `--grid-dot` to `:root` and add `[data-theme="dark"]` block** + +In `src/app.css`, add `--grid-dot: #1a1a2e;` to the end of the `:root` block, then add the dark theme block after `:root`: + +```css +:root { + --bg-base: #f0e6d3; + --bg-surface: #faf5eb; + --primary: #C8860A; + --primary-hover: #E8A020; + --text: #1a1a2e; + --text-muted: #6b6272; + --border: #1a1a2e; + --border-subtle: #d4cabb; + --destructive: #a63d2f; + --error-bg: #f5e0db; + --error-border: #d4a69e; + --error-text: #8b2e1f; + --success-bg: #dce8d4; + --success-text: #3a5a2e; + --card-shadow: 2px 2px 0px var(--border-subtle); + --card-shadow-hover: 3px 3px 0px var(--primary); + --grid-dot: #1a1a2e; +} + +[data-theme="dark"] { + --bg-base: #1a1715; + --bg-surface: #2a2520; + --text: #e8dcc8; + --text-muted: #9a8e7e; + --border: #e8dcc8; + --border-subtle: #3a3530; + --destructive: #d4604e; + --error-bg: #3a2020; + --error-border: #5a3030; + --error-text: #e8a090; + --success-bg: #203020; + --success-text: #90c880; + --card-shadow: 2px 2px 0px var(--border-subtle); + --card-shadow-hover: 3px 3px 0px var(--primary); + --grid-dot: #e8dcc8; +} +``` + +- [ ] **Step 2: Update `body::before` to use `--grid-dot` variable** + +Replace the hardcoded color in the `body::before` rule: + +```css +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + opacity: 0.03; + background-image: repeating-conic-gradient(var(--grid-dot) 0% 25%, transparent 0% 50%); + background-size: 4px 4px; + z-index: 0; +} +``` + +- [ ] **Step 3: Verify by temporarily adding `data-theme="dark"` to `app.html`** + +In `src/app.html`, temporarily change `` to ``. Run `make dev` and visually confirm dark background and light text. Then revert the change. + +- [ ] **Step 4: Commit** + +```bash +git add src/app.css +git commit -m "feat: add dark theme CSS variables and grid-dot extraction" +``` + +--- + +### Task 2: Theme Preference Type + +**Files:** +- Modify: `src/lib/types/preferences.ts` + +- [ ] **Step 1: Add `theme` field to `UserPreferences`** + +```typescript +export interface UserPreferences { + theme: 'system' | 'light' | 'dark'; + defaultNoteMode: 'richtext' | 'markdown'; + defaultNoteColor: NoteColor; + hideFooter: boolean; + sidebarDefaultState: 'open' | 'collapsed'; + notifyOnShare: boolean; + notifyOnCollabRemoved: boolean; + notifyOnNoteDeleted: boolean; +} + +export const DEFAULT_PREFERENCES: UserPreferences = { + theme: 'system', + defaultNoteMode: 'richtext', + defaultNoteColor: 'default', + hideFooter: false, + sidebarDefaultState: 'open', + notifyOnShare: true, + notifyOnCollabRemoved: true, + notifyOnNoteDeleted: true +}; +``` + +Do NOT add `theme` to `BOOLEAN_PREF_KEYS`. + +- [ ] **Step 2: Commit** + +```bash +git add src/lib/types/preferences.ts +git commit -m "feat: add theme preference type (system/light/dark)" +``` + +--- + +### Task 3: Theme Utility — `applyTheme` and `isDarkMode` + +**Files:** +- Create: `src/lib/utils/theme.svelte.ts` +- Test: `tests/unit/theme.test.ts` + +- [ ] **Step 1: Write failing tests for theme utility** + +Note: `theme.svelte.ts` uses module-scope `$state` which requires the Svelte compiler. The Vitest node environment does not process `.svelte.ts` runes correctly, so we test only the DOM side-effects of `applyTheme` (attribute and meta tag changes). The reactive `isDarkMode` / `getIsDarkMode()` behavior is covered by E2E tests instead. + +Create `tests/unit/theme.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Mock $app/environment +vi.mock('$app/environment', () => ({ browser: true })); + +describe('applyTheme', () => { + let applyTheme: (theme: 'system' | 'light' | 'dark') => void; + + beforeEach(async () => { + // Reset DOM + document.documentElement.removeAttribute('data-theme'); + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) meta.remove(); + const metaEl = document.createElement('meta'); + metaEl.setAttribute('name', 'theme-color'); + metaEl.setAttribute('content', '#f0e6d3'); + document.head.appendChild(metaEl); + + // Reset module state + vi.resetModules(); + const mod = await import('$lib/utils/theme.svelte.js'); + applyTheme = mod.applyTheme; + }); + + it('sets data-theme="dark" for dark mode', () => { + applyTheme('dark'); + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + }); + + it('removes data-theme for light mode', () => { + document.documentElement.setAttribute('data-theme', 'dark'); + applyTheme('light'); + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + }); + + it('updates meta theme-color for dark mode', () => { + applyTheme('dark'); + const meta = document.querySelector('meta[name="theme-color"]'); + expect(meta?.getAttribute('content')).toBe('#1a1715'); + }); + + it('updates meta theme-color for light mode', () => { + applyTheme('dark'); + applyTheme('light'); + const meta = document.querySelector('meta[name="theme-color"]'); + expect(meta?.getAttribute('content')).toBe('#f0e6d3'); + }); + + it('falls back to system for unknown values', () => { + // matchMedia returns false by default in jsdom → resolves to light + applyTheme('invalid' as 'system' | 'light' | 'dark'); + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + }); + + it('resolves system preference to dark when prefers-color-scheme is dark', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: query === '(prefers-color-scheme: dark)', + media: query, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn() + })) + }); + applyTheme('system'); + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm vitest run tests/unit/theme.test.ts` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement `theme.svelte.ts`** + +Create `src/lib/utils/theme.svelte.ts`: + +```typescript +import { browser } from '$app/environment'; + +const LIGHT_THEME_COLOR = '#f0e6d3'; +const DARK_THEME_COLOR = '#1a1715'; + +let darkMode = $state(false); + +export function getIsDarkMode(): boolean { + return darkMode; +} + +export function applyTheme(theme: 'system' | 'light' | 'dark'): void { + if (!browser) return; + + // Validate input + if (theme !== 'system' && theme !== 'light' && theme !== 'dark') { + theme = 'system'; + } + + // Resolve system preference + let resolved = theme; + if (theme === 'system') { + resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + const isDark = resolved === 'dark'; + darkMode = isDark; + + if (isDark) { + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) { + meta.setAttribute('content', isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR); + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm vitest run tests/unit/theme.test.ts` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/utils/theme.svelte.ts tests/unit/theme.test.ts +git commit -m "feat: add theme utility with applyTheme and isDarkMode" +``` + +--- + +### Task 4: Note Card Dark Colors + +**Files:** +- Modify: `src/lib/utils/colors.ts` +- Test: `tests/unit/colors.test.ts` + +- [ ] **Step 1: Write failing tests for `getNoteColor`** + +Create `tests/unit/colors.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { getNoteColor, NOTE_COLORS, NOTE_COLORS_DARK } from '$lib/utils/colors.js'; + +describe('getNoteColor', () => { + it('returns light color when isDark is false', () => { + expect(getNoteColor('default', false)).toBe('#faf5eb'); + expect(getNoteColor('coral', false)).toBe('#faafa8'); + }); + + it('returns dark color when isDark is true', () => { + expect(getNoteColor('default', true)).toBe('#2a2520'); + expect(getNoteColor('coral', true)).toBe('#4a2522'); + }); + + it('returns default color for unknown color name', () => { + expect(getNoteColor('unknown' as any, false)).toBe('#faf5eb'); + expect(getNoteColor('unknown' as any, true)).toBe('#2a2520'); + }); + + it('has matching keys in light and dark color maps', () => { + const lightKeys = Object.keys(NOTE_COLORS).sort(); + const darkKeys = Object.keys(NOTE_COLORS_DARK).sort(); + expect(lightKeys).toEqual(darkKeys); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `pnpm vitest run tests/unit/colors.test.ts` +Expected: FAIL — `getNoteColor` and `NOTE_COLORS_DARK` not found + +- [ ] **Step 3: Add dark colors and helper to `colors.ts`** + +Update `src/lib/utils/colors.ts`: + +```typescript +import type { NoteColor } from '$lib/types/index.js'; + +export const NOTE_COLORS: Record = { + default: { bg: '#faf5eb', label: 'Default' }, + coral: { bg: '#faafa8', label: 'Coral' }, + peach: { bg: '#f39f76', label: 'Peach' }, + sand: { bg: '#fff8b8', label: 'Sand' }, + mint: { bg: '#e2f6d3', label: 'Mint' }, + sage: { bg: '#b4ddd3', label: 'Sage' }, + fog: { bg: '#d4e4ed', label: 'Fog' }, + storm: { bg: '#aeccdc', label: 'Storm' }, + dusk: { bg: '#d3bfdb', label: 'Dusk' }, + blossom: { bg: '#f6e2dd', label: 'Blossom' }, + clay: { bg: '#e9e3d4', label: 'Clay' }, + chalk: { bg: '#efeff1', label: 'Chalk' } +}; + +export const NOTE_COLORS_DARK: Record = { + default: { bg: '#2a2520', label: 'Default' }, + coral: { bg: '#4a2522', label: 'Coral' }, + peach: { bg: '#4a3020', label: 'Peach' }, + sand: { bg: '#3a3520', label: 'Sand' }, + mint: { bg: '#2a3a22', label: 'Mint' }, + sage: { bg: '#223a32', label: 'Sage' }, + fog: { bg: '#222e3a', label: 'Fog' }, + storm: { bg: '#1e2a35', label: 'Storm' }, + dusk: { bg: '#352540', label: 'Dusk' }, + blossom: { bg: '#3a2830', label: 'Blossom' }, + clay: { bg: '#302e28', label: 'Clay' }, + chalk: { bg: '#2e2e30', label: 'Chalk' } +}; + +export function getNoteColor(color: NoteColor, isDark: boolean): string { + const map = isDark ? NOTE_COLORS_DARK : NOTE_COLORS; + return (map[color] ?? map.default).bg; +} + +export const COLOR_OPTIONS = Object.entries(NOTE_COLORS).map(([value, { label }]) => ({ + value: value as NoteColor, + label +})); +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `pnpm vitest run tests/unit/colors.test.ts` +Expected: All PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/utils/colors.ts tests/unit/colors.test.ts +git commit -m "feat: add dark note card colors and getNoteColor helper" +``` + +--- + +### Task 5: FOUC Prevention Script + +**Files:** +- Modify: `src/app.html` + +- [ ] **Step 1: Add inline script to `app.html`** + +Insert after the `` tag (line 6) and before `%sveltekit.head%` (line 15): + +```html + +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/app.html +git commit -m "feat: add FOUC-prevention script for dark mode" +``` + +--- + +### Task 6: Reactive Theme in Root Layout + +**Files:** +- Modify: `src/routes/+layout.svelte` + +- [ ] **Step 1: Add theme `$effect` to root layout** + +The root layout currently imports CSS and registers the PWA service worker. Add the theme `$effect` here so both `(app)` and `(auth)` layouts get the theme applied. **Important:** Keep `initPreferences()` in `(app)/+layout.svelte` — do NOT move it here. The `(app)` layout reads `sidebarDefaultState` immediately after init, and moving init to the root layout would create a race condition. The root layout only watches the preference reactively via `$effect`, which fires after preferences are loaded. + +On auth pages (login/setup), `initPreferences()` hasn't been called, so `prefs.theme` will be `'system'` (the default). The FOUC script already handles the initial theme from localStorage, and the `$effect` will apply the correct theme once it runs. + +```svelte + + + + {@html webManifest} + + +{@render children()} +``` + +**Note:** `initPreferences()` stays in `(app)/+layout.svelte`. The root layout only adds the theme `$effect` and does not call `initPreferences()`. + +- [ ] **Step 2: Verify dev server works** + +Run `make dev`, open the app, go to Settings > Preferences. The theme toggle doesn't exist yet, but the app should render without errors. Check browser console for any issues. + +- [ ] **Step 4: Commit** + +```bash +git add src/routes/+layout.svelte src/routes/\(app\)/+layout.svelte +git commit -m "feat: add reactive theme watcher in root layout" +``` + +--- + +### Task 7: Theme Toggle in Settings + +**Files:** +- Modify: `src/routes/(app)/settings/preferences/+page.svelte` + +- [ ] **Step 1: Add theme segmented toggle** + +Add as the first preference section (before "Default note mode"): + +```svelte + +
+ Theme +
+ {#each [['system', 'System'], ['light', 'Light'], ['dark', 'Dark']] as [value, label]} + + {/each} +
+
+``` + +- [ ] **Step 2: Manually test** + +Run `make dev`. Go to Settings > Preferences. Click System / Light / Dark and confirm: +- Clicking "Dark" immediately applies dark theme +- Clicking "Light" reverts to light theme +- Clicking "System" follows OS preference +- Refreshing the page persists the choice (no FOUC) + +- [ ] **Step 3: Commit** + +```bash +git add src/routes/\(app\)/settings/preferences/+page.svelte +git commit -m "feat: add theme toggle to preferences settings" +``` + +--- + +### Task 8: Update Components to Use Dark Note Colors + +**Files:** +- Modify: `src/lib/components/NoteCard.svelte:2,29-32` +- Modify: `src/lib/components/NoteEditor.svelte:12,194-197` +- Modify: `src/lib/components/ColorPicker.svelte` + +**Important:** `getIsDarkMode()` must be called **inside** `$effect` or `$derived` blocks for reactivity. Calling it as a top-level `const` would capture the value once and never update when the theme changes. Each component already has an `$effect` block where it computes card/bg styles — add `getIsDarkMode()` inside those existing blocks. + +Public share page (`src/routes/(share)/s/[token]/+page.svelte`) is out of scope per the spec. + +- [ ] **Step 1: Update `NoteCard.svelte`** + +Replace `NOTE_COLORS` import with `getNoteColor` and `getIsDarkMode`: + +```svelte + +import { getNoteColor } from '$lib/utils/colors.js'; +import { getIsDarkMode } from '$lib/utils/theme.svelte.js'; + + +$effect(() => { + // ... existing logic ... + const bg = getNoteColor(note.color, getIsDarkMode()); + cardStyle = `background-color: ${bg}`; +}); +``` + +Remove the `NOTE_COLORS` import. `getIsDarkMode()` is called inside the `$effect` so it re-runs when the theme changes. + +- [ ] **Step 2: Update `NoteEditor.svelte`** + +Same pattern — inside the existing `$effect` where `bgStyle` is computed: + +```svelte +import { getNoteColor } from '$lib/utils/colors.js'; +import { getIsDarkMode } from '$lib/utils/theme.svelte.js'; + + +bgStyle = `background-color: ${getNoteColor(color, getIsDarkMode())}`; +``` + +Remove the `NOTE_COLORS` import. + +- [ ] **Step 3: Update `ColorPicker.svelte`** + +The ColorPicker shows circular swatches. Use `$derived` for the dark mode check so swatches update reactively: + +```svelte +import { COLOR_OPTIONS, getNoteColor } from '$lib/utils/colors.js'; +import { getIsDarkMode } from '$lib/utils/theme.svelte.js'; + + +style="background-color: {getNoteColor(option.value, getIsDarkMode())}" +``` + +Remove `NOTE_COLORS` import if no longer needed. + +- [ ] **Step 4: Verify all components render correctly in both themes** + +Run `make dev`. Toggle between light and dark in Settings > Preferences. Check: +- Note cards show correct dark/light background colors +- Color picker shows dark/light swatches +- Note editor uses correct background color + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/components/NoteCard.svelte src/lib/components/NoteEditor.svelte src/lib/components/ColorPicker.svelte +git commit -m "feat: use dark note card colors in all components" +``` + +--- + +### Task 9: E2E Tests + +**Files:** +- Create: `tests/e2e/dark-mode.spec.ts` + +- [ ] **Step 1: Write E2E tests** + +Create `tests/e2e/dark-mode.spec.ts`: + +```typescript +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Dark mode', () => { + test('Scenario: Theme toggle persists selection across page reload', async ({ + authenticatedPage: page + }) => { + // Given the user is on the preferences page + await page.goto('/settings/preferences'); + + // When the user selects dark theme + await page.getByTestId('pref-theme-dark').click(); + + // Then the page applies dark theme + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + // When the page is reloaded + await page.reload(); + + // Then the dark theme persists without flash + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + await expect(page.getByTestId('pref-theme-dark')).toHaveClass(/font-medium/); + }); + + test('Scenario: Light mode removes dark theme attribute', async ({ + authenticatedPage: page + }) => { + // Given dark theme is active + await page.goto('/settings/preferences'); + await page.getByTestId('pref-theme-dark').click(); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + // When the user selects light theme + await page.getByTestId('pref-theme-light').click(); + + // Then the dark theme attribute is removed + await expect(page.locator('html')).not.toHaveAttribute('data-theme'); + }); + + test('Scenario: System mode follows OS dark preference', async ({ + authenticatedPage: page + }) => { + // Given the OS prefers dark mode + await page.emulateMedia({ colorScheme: 'dark' }); + + // When the user selects system theme + await page.goto('/settings/preferences'); + await page.getByTestId('pref-theme-system').click(); + + // Then the page applies dark theme from system preference + // Use toHaveAttribute with timeout to handle async $effect execution + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark', { timeout: 5000 }); + }); + + test('Scenario: Note cards use dark colors in dark mode', async ({ + authenticatedPage: page + }) => { + // Given dark theme is active + await page.goto('/settings/preferences'); + await page.getByTestId('pref-theme-dark').click(); + + // When viewing the notes page + await page.goto('/'); + + // Then note cards should have dark background colors + const noteCard = page.getByTestId('note-card').first(); + if (await noteCard.isVisible()) { + const bg = await noteCard.evaluate((el) => getComputedStyle(el).backgroundColor); + // Dark default color is #2a2520 = rgb(42, 37, 32) + expect(bg).not.toBe('rgb(250, 245, 235)'); // Not the light default + } + }); +}); +``` + +- [ ] **Step 2: Run E2E tests** + +Run: `pnpm test:e2e tests/e2e/dark-mode.spec.ts` +Expected: All PASS + +- [ ] **Step 3: Commit** + +```bash +git add tests/e2e/dark-mode.spec.ts +git commit -m "test: add E2E tests for dark mode toggle and persistence" +``` + +--- + +### Task 10: Housekeeping and Type Check + +**Files:** +- Modify: `.gitignore` + +- [ ] **Step 1: Add `.superpowers/` to `.gitignore`** + +Append to `.gitignore`: + +``` +# Superpowers brainstorm artifacts +.superpowers/ +``` + +- [ ] **Step 2: Run type check** + +Run: `pnpm check` +Expected: No errors. If there are type errors, fix them. + +- [ ] **Step 3: Run full test suite** + +Run: `make test` +Expected: All unit and E2E tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add .gitignore +git commit -m "chore: add .superpowers/ to gitignore" +```