From 49c28a33ff1864449ae37889bbc64ccba0dad7b3 Mon Sep 17 00:00:00 2001 From: Frederic Cabassut Date: Wed, 8 Apr 2026 22:12:01 +0200 Subject: [PATCH] chore: remove accidental files from feature branch Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-20-dark-mode-design.md | 190 +++++++++++ .../2026-03-20-public-note-sharing-design.md | 232 +++++++++++++ ...2026-03-20-screenshot-automation-design.md | 322 ++++++++++++++++++ static/api/openapi.json | 2 +- 4 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-03-20-dark-mode-design.md create mode 100644 docs/superpowers/specs/2026-03-20-public-note-sharing-design.md create mode 100644 docs/superpowers/specs/2026-03-20-screenshot-automation-design.md diff --git a/docs/superpowers/specs/2026-03-20-dark-mode-design.md b/docs/superpowers/specs/2026-03-20-dark-mode-design.md new file mode 100644 index 0000000..6b2b453 --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-dark-mode-design.md @@ -0,0 +1,190 @@ +# Dark Mode / Theme Toggle — Design Spec + +**Issue:** #26 +**Date:** 2026-03-20 +**Status:** Draft + +## Summary + +Add a dark variant of the retro parchment theme with a three-way toggle (system / light / dark) in Settings > Preferences. The dark theme preserves the retro 8-bit character — warm dark tones, not cold grays. + +## Decisions + +- **Toggle location:** Settings > Preferences only (no header icon) +- **UI pattern:** Segmented buttons (system / light / dark), matching existing `defaultNoteMode` toggle style +- **Implementation:** CSS `[data-theme="dark"]` attribute on ``, variable overrides in `app.css` +- **Pixel grid texture:** Inverted to light dots (`#e8dcc8`) at 3% opacity in dark mode +- **Default:** `system` (follows `prefers-color-scheme`) +- **Light mode:** No `data-theme` attribute set (`:root` defaults apply). `data-theme="dark"` is only set for dark mode. This means no redundant `[data-theme="light"]` block is needed. + +## 1. CSS Variable Overrides + +Add `--grid-dot` to the existing `:root` block and add a `[data-theme="dark"]` block after it in `app.css`: + +```css +:root { + /* ...existing vars... */ + --grid-dot: #1a1a2e; /* new — extracted from body::before */ +} + +[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; +} +``` + +`--primary` and `--primary-hover` remain unchanged — gold works on both backgrounds. + +`--card-shadow` uses `var(--border-subtle)` in both modes for consistency — the dark `--border-subtle` (#3a3530) produces the right effect. + +Update `body::before` to use the new variable: +```css +body::before { + background-image: repeating-conic-gradient(var(--grid-dot) 0% 25%, transparent 0% 50%); +} +``` + +## 2. Theme Preference Storage + +Extend the existing `UserPreferences` system: + +- **Type change:** Add `theme: 'system' | 'light' | 'dark'` to `UserPreferences` interface +- **Default:** `'system'` +- **Storage:** Same key-value `userPreferences` table + localStorage cache — no schema migration needed +- **Do NOT add `theme` to `BOOLEAN_PREF_KEYS`** — it's a string enum, not a boolean + +Files to modify: +- `src/lib/types/preferences.ts` — add `theme` field and default + +The existing `fromServerRecord` in `preferences.svelte.ts` does not validate string values. The `applyTheme` function (Section 4) must guard against unexpected values and fall back to `'system'` if the value is not one of the three valid options. + +## 3. FOUC Prevention + +Add a blocking ` +``` + +This runs synchronously before first paint. The script validates the theme value and null-checks the meta element. + +## 4. Reactive Theme Application + +Create `src/lib/utils/theme.svelte.ts` (`.svelte.ts` to enable Svelte 5 runes) with: + +- `applyTheme(theme: 'system' | 'light' | 'dark')` — resolves system preference, sets/removes `data-theme="dark"` on `document.documentElement`, updates ``. Guards against unexpected values (falls back to `'system'`). All DOM/`matchMedia` access guarded with `browser` check from `$app/environment`. +- `isDarkMode` — a reactive `$state` boolean, updated by `applyTheme` and the `matchMedia` listener. Components import this to conditionally use dark color variants. + +Place the theme `$effect` in the **root `+layout.svelte`** (not just `(app)` layout). This ensures both `(app)` and `(auth)` layout groups get the theme applied. The root layout currently only imports CSS — this adds a small `$effect` that watches `preferences.theme` and calls `applyTheme`. It also registers a `matchMedia('(prefers-color-scheme: dark)')` change listener when in system mode, cleaned up on destroy. + +## 5. Note Card Dark Colors + +Add a parallel `NOTE_COLORS_DARK` map in `src/lib/utils/colors.ts`: + +| Color | Light | Dark | +|-------|-------|------| +| default | `#faf5eb` | `#2a2520` | +| coral | `#faafa8` | `#4a2522` | +| peach | `#f39f76` | `#4a3020` | +| sand | `#fff8b8` | `#3a3520` | +| mint | `#e2f6d3` | `#2a3a22` | +| sage | `#b4ddd3` | `#223a32` | +| fog | `#d4e4ed` | `#222e3a` | +| storm | `#aeccdc` | `#1e2a35` | +| dusk | `#d3bfdb` | `#352540` | +| blossom | `#f6e2dd` | `#3a2830` | +| clay | `#e9e3d4` | `#302e28` | +| chalk | `#efeff1` | `#2e2e30` | + +Export a helper `getNoteColor(color: NoteColor, isDark: boolean): string` that returns the correct bg hex. + +Components (`NoteCard.svelte`, `NoteEditor.svelte`, `ColorPicker.svelte`) import `isDarkMode` from `theme.svelte.ts` and pass it to `getNoteColor`. This is the single approach — do not use direct DOM inspection for dark mode detection. + +## 6. Settings UI + +Add a "Theme" section to `src/routes/(app)/settings/preferences/+page.svelte` as the **first** preference (before "Default note mode"): + +```svelte + +
+ Theme +
+ {#each [['system', 'System'], ['light', 'Light'], ['dark', 'Dark']] as [value, label]} + + {/each} +
+
+``` + +Test IDs: `pref-theme-system`, `pref-theme-light`, `pref-theme-dark`. + +## 7. Landing Page + +Out of scope for this spec. The landing page (`website/`) uses its own color system separate from `app.css`. Dark mode support for the landing page will be tracked separately. + +## 8. Prose / Typography Overrides + +The `.prose` overrides in `app.css` already use CSS variables, so they'll automatically adapt. No changes needed. + +The TipTap editor styles (table borders, selected cells, task lists) also use CSS variables. No changes needed. + +## 9. Color Picker in Dark Mode + +The `ColorPicker` component shows circular swatches. In dark mode, these should display the dark color variants. The component imports `isDarkMode` from `theme.svelte.ts` and uses `getNoteColor(color, isDarkMode)` to get the correct swatch background. + +## 10. Testing + +### Unit tests +- `theme.svelte.ts` — test `applyTheme` sets correct attribute and meta tag, validates unknown values +- `colors.ts` — test `getNoteColor` returns correct color for light/dark + +### E2E tests +- Theme toggle in preferences persists and applies (use `data-testid` selectors) +- System preference detection (use Playwright's `page.emulateMedia({ colorScheme: 'dark' })`) +- No FOUC on page reload in dark mode +- Note cards render with correct dark colors + +## 11. PWA Manifest + +The PWA manifest (`@vite-pwa/sveltekit`) contains a static `theme_color`. This cannot be dynamically changed. The manifest will keep the light theme color (`#f0e6d3`). The `` is updated dynamically by the FOUC script and `applyTheme`, which handles the browser chrome color. This is a known limitation of PWA manifests and is acceptable. + +## Out of Scope + +- Header quick-toggle icon +- Per-note theme override +- Custom color theme builder +- Dark mode for public shared note pages (can be added later) +- Dark mode for landing page (separate task) diff --git a/docs/superpowers/specs/2026-03-20-public-note-sharing-design.md b/docs/superpowers/specs/2026-03-20-public-note-sharing-design.md new file mode 100644 index 0000000..119e469 --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-public-note-sharing-design.md @@ -0,0 +1,232 @@ +# Public Note Sharing — Design Spec + +**Issue**: [#7 — Public links and shared notes](https://github.com/bretzel-app/crumbs/issues/7) +**Date**: 2026-03-20 + +## Overview + +Allow sharing individual notes via a public URL that anyone can view without authentication. Read-only sharing via unguessable tokens. Integrates into the existing ShareDialog alongside collaborator management. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Share UI location | Inside existing ShareDialog | One "Share" entry point for all sharing (public + collaborators) | +| Expiry | Deferred to future version | Simpler v1; active/revoked toggle only | +| Rate limiting | In-memory per-IP | Lightweight, resets on restart; token entropy handles enumeration | +| Architecture | Separated shares-service | Follows collaborators-service pattern; clean module boundary | +| Sharing icon | Globe (public) or Users (collaborators) | Single icon slot; tooltip shows combined sharing state | + +## Schema + +New `sharedNotes` table in `src/lib/server/db/schema.ts`: + +```typescript +export const sharedNotes = sqliteTable('shared_notes', { + noteId: text('note_id').primaryKey().references(() => notes.id, { onDelete: 'cascade' }), + token: text('token').notNull().unique(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + expiresAt: integer('expires_at', { mode: 'timestamp' }), // NULL = no expiry, reserved for future use +}); +``` + +- `noteId` as primary key — one share per note, no separate `id` needed +- `integer` timestamps with `mode: 'timestamp'` — matches all other tables in the schema +- `createdAt` set in service layer (`new Date()`), not via SQL default +- CASCADE delete — deleting the note removes the share +- Token: `nanoid(21)` — ~128 bits of entropy +- No update to `deleteUser()` needed — shares cascade through notes (foreign keys enabled via `PRAGMA foreign_keys = ON` in db connection) + +## Types + +Add to `src/lib/types/index.ts`: + +```typescript +export interface NoteShare { + noteId: string; + token: string; + createdAt: Date; + expiresAt: Date | null; +} +``` + +Extend the `Note` interface: + +```typescript +export interface Note { + // ...existing fields + shareToken?: string; // populated during hydration if a public share exists +} +``` + +## Service Layer + +New file: `src/lib/server/shares-service.ts` + +| Function | Params | Returns | Description | +|---|---|---|---| +| `createShare` | `db, noteId, userId` | `{ token }` | Verify ownership, generate nanoid(21), insert row, return token. If share already exists, return existing token. | +| `revokeShare` | `db, noteId, userId` | `void` | Verify ownership, delete share row | +| `getShareByNoteId` | `db, noteId` | `NoteShare \| null` | Return share if exists (for ShareDialog) | +| `getSharedNote` | `db, token` | `SharedNoteData \| null` | Public — fetch note by token, return null if trashed or not found | +| `fetchSharesForNotes` | `db, noteIds` | `Map` | Batch fetch — returns Map of noteId → token for hydration (only token needed for NoteCard indicator) | + +`SharedNoteData` type (returned to public viewers): + +```typescript +interface SharedNoteData { + title: string; + content: string; + checklistMode: boolean; + color: NoteColor; + attachments: Attachment[]; + createdAt: Date; + updatedAt: Date; +} +``` + +## API Endpoints + +### `POST /api/notes/[id]/share` + +- **Auth**: Required (owner only) +- **Body**: None +- **Response**: `{ token: string, url: string }` +- **Behavior**: Creates share or returns existing one. URL constructed from request origin + `/s/${token}` + +### `DELETE /api/notes/[id]/share` + +- **Auth**: Required (owner only) +- **Response**: `204 No Content` +- **Behavior**: Deletes share row. Link immediately returns 404. + +### `GET /api/shared/[token]` + +- **Auth**: None (public) +- **Rate limit**: 60 requests/min per IP (in-memory) +- **Response**: `SharedNoteData` JSON +- **Error cases**: + - Invalid/unknown token → 404 + - Note is trashed → 404 + - Rate limited → 429 + +### `GET /api/shared/[token]/attachment/[attachmentId]` + +- **Auth**: None (public) +- **Rate limit**: Same as above +- **Response**: Image binary (proxied from attachment storage) +- **Behavior**: Verify attachment belongs to the shared note, then serve + +## Routes + +``` +src/routes/ + (share)/ + +layout.svelte # Minimal layout: parchment bg, centered, no app chrome + s/[token]/ + +page.server.ts # Load shared note by token via shares-service + +page.svelte # Read-only note renderer +``` + +### Auth Middleware + +Add `/s/` and `/api/shared` to `PUBLIC_PATHS` in `src/hooks.server.ts`. Both are needed — `/s/` for the public page route, `/api/shared` for the public API endpoints. + +### Public Page Design + +- Parchment background (`--bg-base`) +- Centered content card (`--bg-surface`, `--border-subtle`, `--card-shadow`) +- Note title in JetBrains Mono +- Rendered markdown content or checklist (read-only) +- Inline images from attachments +- No tags, no edit controls, no sidebar, no navigation +- Footer: "POWERED BY CRUMBS" in Press Start 2P pixel font, links to project + +## UI Components + +### ShareDialog Changes + +Add a "Public link" section **above** the existing collaborators section: + +**Inactive state:** +- Globe icon + "Public link" label + toggle switch (off) +- Subtitle: "Anyone with the link can view this note (read-only)" + +**Active state:** +- Globe icon + "Public link" label + toggle switch (on, gold) +- URL input (readonly) + "Copy" button +- Subtitle: "Toggle off to revoke access. The link will return a 404." +- Border highlight in `--primary` gold + +Toggle on → calls `POST /api/notes/[id]/share`, displays URL +Toggle off → calls `DELETE /api/notes/[id]/share`, hides URL +Copy button → clipboard API, brief "Copied!" feedback + +### NoteCard Share Indicator + +Single icon slot in top-right of NoteCard (replaces showing both icons): + +| State | Icon | Color | +|---|---|---| +| Public link active | `Globe` (Lucide) | `--primary` gold | +| Collaborators only | `Users` (Lucide) | `--text-muted` | +| Both | `Globe` (Lucide) | `--primary` gold | +| Neither | No icon | — | + +**Hover tooltip** shows combined sharing info regardless of which icon is displayed: +- "Shared publicly via link" (if public share exists) +- "Shared with N collaborator(s)" (if collaborators exist) +- Both lines if both exist + +### NoteEditor Share Button + +Existing Share button behavior unchanged. The Globe/Users icon logic from NoteCard also applies to the editor toolbar button to indicate current sharing state. + +## Rate Limiting + +New file: `src/lib/server/ip-rate-limit.ts` — separate from the existing DB-backed `rate-limit.ts` (which is login-specific). + +```typescript +// In-memory store: Map +// 60 requests per minute per IP +// Applied to GET /api/shared/[token] and GET /api/shared/[token]/attachment/[id] +``` + +Resets on server restart. No database table needed. + +## Hydration + +`hydrateNotes()` in `notes-service.ts` must be updated to call `fetchSharesForNotes()` and populate `shareToken` on each note. This follows the existing pattern where `hydrateNotes` already batch-fetches tags, attachments, and collaborators. + +## Security + +- Tokens are unguessable: nanoid(21) = ~128 bits of entropy +- `/s/` and `/api/shared` routes bypass auth middleware — only these prefixes +- Trashed notes return 404 even if share token is valid +- Only note owners can create/revoke shares — collaborators cannot +- No user information exposed on public page (no username, no email, no tags) +- Rate limiting prevents brute-force enumeration +- Attachments served through share-specific route (validates attachment belongs to shared note) + +## Out of Scope (v1) + +- Expiry dates / auto-expire +- Password-protected shares +- Edit access through share links +- Share analytics / view counts +- Email notifications for share events +- E2EE interaction (no E2EE exists) + +## Acceptance Criteria + +- [ ] Share toggle in ShareDialog creates/revokes public link +- [ ] Unique nanoid(21) token generated per shared note +- [ ] Public `/s/[token]` route renders note without auth +- [ ] Copy link to clipboard with feedback +- [ ] Revoke share → link returns 404 +- [ ] Globe indicator on NoteCard for publicly shared notes +- [ ] Combined tooltip showing all sharing state +- [ ] Trashed notes return 404 even if shared +- [ ] Minimal, parchment-styled public page with "Powered by Crumbs" +- [ ] In-memory rate limiting on public endpoints +- [ ] Attachments accessible on public page diff --git a/docs/superpowers/specs/2026-03-20-screenshot-automation-design.md b/docs/superpowers/specs/2026-03-20-screenshot-automation-design.md new file mode 100644 index 0000000..6344988 --- /dev/null +++ b/docs/superpowers/specs/2026-03-20-screenshot-automation-design.md @@ -0,0 +1,322 @@ +# Screenshot Automation for Landing Page + +## Overview + +A Playwright-based script that seeds the Crumbs app with bretzel-themed demo data, captures 10 screenshots (7 desktop + 3 mobile) for the landing page, and commits them. Triggered manually via a GitHub Actions workflow. + +## Architecture + +``` +scripts/ + screenshots.ts # Main orchestrator: seed + capture + fixtures/ + bretzel-pixel.ts # Generates a pixel-art bretzel PNG programmatically +``` + +Single script (`scripts/screenshots.ts`) that: +1. Spawns the preview server as a child process (`pnpm preview --port 4173`), same approach as `playwright.config.ts` uses for e2e tests. The app uses `@sveltejs/adapter-node`, so `vite preview` import won't work — must spawn via CLI. +2. Deletes any existing screenshot DB to start fresh +3. Seeds data via API calls (setup admin, login, create notes/users/keys) +4. Navigates to each view and captures screenshots at specific viewports +5. Saves PNGs directly to `website/assets/` +6. Kills the preview server in `finally` block + +## Seed Data — Bretzel Universe + +### Users + +| User | Email | Role | Purpose | +|------|-------|------|---------| +| Bretzel Admin | admin@bretzel.app | admin | Main user, owns all notes | +| Alice Chen | alice@bretzel.app | user | Collaborator for sharing screenshot | + +### Tags + +Passed explicitly in the `tags` array of each `POST /api/notes` request body: `baking`, `bretzel`, `dev`, `devops`, `homelab`, `personal`, `reading`, `weekend`. Tags are NOT auto-parsed from hashtags in content — they must be provided as a separate field. + +### Notes + +| # | Title | Color | Mode | Tags | Pinned | Special | +|---|-------|-------|------|------|--------|---------| +| 1 | Bretzel Ingredients | sand | checklist | #baking, #bretzel | yes | Checklist items (see below) | +| 2 | Self-Hosting Stack | fog | rich text | #homelab, #devops | no | Homelab list (see below) | +| 3 | Back to Retro Gaming | peach | rich text | #reading, #personal | no | Retro gaming note | +| 4 | The Best Software Is the Software You Own | coral | rich text | #personal, #bretzel | no | Self-hosting manifesto | +| 5 | Weekend Plans | mint | rich text | #personal, #weekend | no | Casual weekend plans | +| 6 | Ancient Flute Harmonica Discovered | chalk | rich text | #reading | no | Quirky news note | +| 7 | Baking code for the... | clay | checklist | #dev, #bretzel | no | Dev/baking crossover | + +### Note Content Details + +**Bretzel Ingredients** (checklist): +- [x] 500g bread flour +- [x] 300ml warm water +- [x] 10g salt +- [ ] 7g dry yeast +- [ ] 30g butter (softened) +- [ ] Baking soda for the bath +- [ ] Coarse salt for topping + +**Self-Hosting Stack** (rich text — used for editor screenshot): +``` +Current homelab setup: + +**Crumbs** — notes (obviously) +**42** — holiday budget tracking +**Dokploy** — deployment platform +**Outline** — team wiki +**Navidrome** — music streaming +**Vaultwarden** — passwords +**Nextcloud** — files & calendar +**Plausible** — analytics + +Everything below daily reverse proxy. +``` + +**The Best Software Is the Software You Own** (rich text): +``` +Self-host everything. Trust no cloud. + +The best software is the software you own. +Your data. Your server. Your rules. + +If it can't run on your hardware, it's not really yours. +``` + +**Back to Retro Gaming** (rich text): +``` +Games to replay this summer: + +Chrono Trigger (SNES) — best RPG ever made +The Legend of Zelda: Link's Awakening (GB) +Castlevania: Symphony of the Night (PS1) +Advance Wars (GBA) +Final Fantasy Tactics (PS1) + +The pixel art era was peak game design. +``` + +**Weekend Plans** (rich text): +``` +Saturday: +- Farmer's market (get sourdough starter) +- Fix the Dokploy SSL cert renewal +- Bake a batch of bretzels + +Sunday: +- Retro gaming marathon +- Update the self-hosting stack +- Plan next trip on 42 +``` + +**Ancient Flute Harmonica Discovered** (rich text): +``` +Archaeologists found a 40,000-year-old flute carved from vulture bone. + +The oldest known musical instrument. +Imagine the first song ever played. +``` + +**Baking code for the...** (checklist): +``` +- [x] Set up CI/CD pipeline +- [x] Write the CRDT sync logic +- [ ] Bake the docker image +- [ ] Deploy to the homelab +- [ ] Celebrate with a bretzel +``` + +### Image Attachments + +**Pixel-art bretzel image** (`scripts/fixtures/bretzel-pixel.ts`): +- Generated programmatically as a PNG using raw pixel data (no external dependencies) +- ~64x64 pixel grid, bretzel shape in warm browns (#C8860A gold, #8B6914 dark, #F0E6D3 parchment bg) +- Attached as featured image to "Bretzel Ingredients" note +- Also create a second smaller image attached to the same note (e.g. a pixel-art salt shaker or oven) for visual variety + +### Version History + +Send 4-5 `PATCH /api/notes/:id` calls to "Self-Hosting Stack" with varying content. Version history entries are created as a side-effect of each PATCH — there is no explicit "create version" endpoint. Each version adds or changes a tool in the stack, so the diff is meaningful when viewed in the history panel. + +### API Keys + +Create via API after seeding: +| Name | Purpose | +|------|---------| +| Claude Code | For MCP integration screenshot | +| N8N | For automation screenshot | + +## Screenshot Capture Plan + +### Desktop (1280x800) + +| # | Filename | View | Capture Steps | +|---|----------|------|---------------| +| 1 | screenshot-grid.png | Home grid | Navigate to `/`, wait for notes to render, ensure sidebar is expanded with tags visible | +| 2 | screenshot-editor.png | Note editor | Click "Self-Hosting Stack" note, wait for editor to open with content and toolbar visible | +| 3 | screenshot-checklist.png | Checklist mode | Click "Bretzel Ingredients" note, wait for checklist items with checkboxes visible | +| 4 | screenshot-sharing.png | Share dialog | Open a note, click share button, wait for ShareDialog with "Alice Chen" listed as collaborator | +| 5 | screenshot-history.png | Version history | Open "Self-Hosting Stack", click history button, select a middle version in the list, wait for preview panel | +| 6 | screenshot-attachments.png | Attachments | Open "Bretzel Ingredients" note (has featured bretzel image), ensure image thumbnails and featured strip visible | +| 7 | screenshot-api.png | Settings > API/MCP | Navigate to `/settings/mcp`, wait for API keys list ("Claude Code", "N8N") and MCP config JSON block | + +### Mobile (390x844) + +| # | Filename | View | Capture Steps | +|---|----------|------|---------------| +| 1 | screenshot-mobile-grid.png | Home grid | Navigate to `/`, wait for notes in mobile layout (no sidebar, tags as horizontal pills) | +| 2 | screenshot-mobile-editor.png | Note editor | Open "Self-Hosting Stack" note, wait for mobile editor | +| 3 | screenshot-mobile-checklist.png | Checklist | Open "Bretzel Ingredients" note, wait for mobile checklist view | + +### Capture Settings +- Format: PNG +- Full page: false (viewport only) +- Animations disabled via `page.emulateMedia({ reducedMotion: 'reduce' })` to avoid capturing mid-animation +- Wait for `networkidle` after each navigation before capturing + +## GitHub Actions Workflow + +**File**: `.github/workflows/screenshots.yml` + +```yaml +name: Screenshots + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + screenshots: + name: Capture Landing Page Screenshots + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers and system deps + run: pnpm exec playwright install --with-deps chromium + + - name: Build app + run: pnpm build + env: + DATABASE_URL: ./data/screenshots.db + + - name: Capture screenshots + run: pnpm exec tsx scripts/screenshots.ts + env: + DATABASE_URL: ./data/screenshots.db + + - name: Commit screenshots + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add website/assets/screenshot-*.png + git diff --cached --quiet && echo "No screenshot changes" && exit 0 + git commit -m "chore: update landing page screenshots [skip ci]" + git push origin main +``` + +## Script Structure + +```typescript +// scripts/screenshots.ts + +import { chromium, type Page } from 'playwright'; +import { spawn, type ChildProcess } from 'child_process'; +import { generateBretzelPixelArt } from './fixtures/bretzel-pixel'; + +const BASE = 'http://localhost:4173'; +const DESKTOP = { width: 1280, height: 800 }; +const MOBILE = { width: 390, height: 844 }; +const OUTPUT = 'website/assets'; + +async function main() { + // 1. Start preview server (spawn child process, wait for port 4173) + const server = await startPreviewServer(); + + // 2. Launch browser + const browser = await chromium.launch(); + + try { + // 3. Seed data (desktop context — API calls work at any viewport) + const seedContext = await browser.newContext({ viewport: DESKTOP }); + const seedPage = await seedContext.newPage(); + await seed(seedPage); + + // 4. Desktop screenshots + await captureDesktop(seedPage); + await seedContext.close(); + + // 5. Mobile screenshots + const mobileContext = await browser.newContext({ viewport: MOBILE }); + const mobilePage = await mobileContext.newPage(); + await login(mobilePage); + await captureMobile(mobilePage); + await mobileContext.close(); + } finally { + await browser.close(); + server.close(); + } +} + +async function seed(page: Page) { + // Setup admin via POST /api/auth/setup + // Login via POST /api/auth/login (sets session cookie on page context) + // Create notes via POST /api/notes (with content, color, tags:string[], pinned, checklistMode) + // → capture note IDs from responses for later use + // Create collaborator user via POST /api/admin/users + // → capture alice.id (numeric) from response + // Share a note via POST /api/notes/:id/collaborators { userId: alice.id } + // Create version history: PATCH /api/notes/:id × 4-5 with varying content + // Upload pixel-art bretzel image via POST /api/notes/:id/attachments (multipart form) + // Create API keys via POST /api/settings/api-keys { name: "Claude Code" } and { name: "N8N" } +} + +async function captureDesktop(page: Page) { + // D1: Grid view + // D2: Editor (Self-Hosting Stack) + // D3: Checklist (Bretzel Ingredients) + // D4: Sharing dialog + // D5: Version history + // D6: Attachments + // D7: API/MCP settings +} + +async function captureMobile(page: Page) { + // M1: Grid view + // M2: Editor + // M3: Checklist +} +``` + +## Landing Page Updates + +After the screenshots are generated, `website/index.html` needs updating: +- Add new screenshot entries for: checklist, sharing, history, attachments, API +- Update alt text and labels +- Hero image stays as `screenshot-grid.png` +- Mobile section adds checklist screenshot, replaces sidebar screenshot + +## Dependencies + +No new dependencies. Uses: +- `playwright` (already in devDependencies for e2e tests) +- `tsx` (already in devDependencies for other scripts) +- Node.js built-in `fs`, `path`, `Buffer` for pixel-art PNG generation +- SvelteKit's preview server (spawned as child process via `pnpm preview`) + +## Open Questions + +None — all decisions resolved during brainstorming. diff --git a/static/api/openapi.json b/static/api/openapi.json index 988f438..55bf71a 100644 --- a/static/api/openapi.json +++ b/static/api/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Crumbs API", "description": "Auto-generated API spec for crumbs", - "version": "0.26.0", + "version": "0.24.0", "contact": { "name": "Bretzel", "url": "https://bretzel.app"