Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: remove old planning docs

authored by

Patrick Dewey and committed by tangled.org d3359f00 cd30309f

-4235
.cells/.lock

This is a binary file and will not be displayed.

-332
.cells/AGENTS.md
··· 1 - # Cells: Agent Guide 2 - 3 - This guide explains how to use the cells work management system. Cells are atomic, dependency-aware units of work designed for coordination between humans and AI agents. 4 - 5 - ## Quick Reference 6 - 7 - ```bash 8 - # View available work 9 - cells list # List active cells 10 - cells list --status open # Show only unclaimed cells 11 - cells show <id> # View cell details 12 - 13 - # Claim and work on a cell 14 - cells claim <id> # Claim a cell (assigns to you) 15 - cells start <id> # Mark as in_progress 16 - cells complete <id> # Mark as completed 17 - 18 - # Start an agent session for a cell 19 - cells run <id> # Create workspace and launch agent 20 - cells run <id> --agent opencode # Use specific agent harness 21 - 22 - # Dependencies 23 - cells dep tree <id> # View dependency tree 24 - cells dep add <id> --needs <x> # Add dependency 25 - ``` 26 - 27 - ## Core Concepts 28 - 29 - ### What is a Cell? 30 - 31 - A cell is a discrete unit of work with: 32 - - **Title**: Brief description of the task 33 - - **Description**: Detailed requirements and acceptance criteria 34 - - **Status**: Current state (open, claimed, in_progress, blocked, completed, cancelled) 35 - - **Priority**: Urgency level (critical, high, normal, low) 36 - - **Dependencies**: Other cells that must complete first 37 - - **Change binding**: Optional link to a jj/git change 38 - 39 - ### Status Lifecycle 40 - 41 - ``` 42 - open → claimed → in_progress → completed 43 - │ │ │ 44 - │ │ └──→ blocked ──→ (back to claimed/in_progress when unblocked) 45 - │ │ 46 - └───────┴──────────────→ cancelled 47 - ``` 48 - 49 - | Status | Meaning | 50 - |--------|---------| 51 - | open | Available for anyone to claim | 52 - | claimed | Assigned but work not started | 53 - | in_progress | Actively being worked on | 54 - | blocked | Waiting on dependencies | 55 - | completed | Work finished successfully | 56 - | cancelled | Work abandoned | 57 - 58 - ## Agent Workflow 59 - 60 - ### 1. Finding Work 61 - 62 - When starting a work session, find available cells: 63 - 64 - ```bash 65 - # List all active (non-completed, non-cancelled) cells 66 - cells list 67 - 68 - # Find unclaimed work 69 - cells list --status open 70 - 71 - # Find work assigned to you 72 - cells list --mine 73 - 74 - # See high-priority items 75 - cells list --priority critical 76 - cells list --priority high 77 - ``` 78 - 79 - ### 2. Starting an Agent Session 80 - 81 - The easiest way to work on a cell is to use `cells run`: 82 - 83 - ```bash 84 - cells run <cell-id> 85 - ``` 86 - 87 - This command: 88 - 1. Creates a dedicated jj workspace for the cell 89 - 2. Claims the cell and binds it to the workspace's change 90 - 3. Launches your configured agent (claude-code, opencode, etc.) in the workspace 91 - 4. Passes the cell's context as the initial prompt 92 - 93 - Options: 94 - - `--agent <name>`: Use a specific agent harness (e.g., `--agent opencode`) 95 - - `--no-claim`: Skip claiming/binding (for continuing previous work) 96 - 97 - The agent runs in your current terminal. When it exits, you're back in your original directory. 98 - 99 - ### 3. Manual Workflow (Alternative) 100 - 101 - If you prefer to work without `cells run`: 102 - 103 - ```bash 104 - # Claim the cell 105 - cells claim <cell-id> 106 - 107 - # Start work 108 - cells start <cell-id> 109 - 110 - # ... do your work ... 111 - 112 - # Complete the cell 113 - cells complete <cell-id> 114 - ``` 115 - 116 - ### 4. Checking Dependencies 117 - 118 - Before working, verify the cell isn't blocked: 119 - 120 - ```bash 121 - cells show <cell-id> # Check "Blocked by" field 122 - cells dep tree <cell-id> # View full dependency tree 123 - ``` 124 - 125 - If blocked, either: 126 - - Work on the blocking cells first 127 - - Wait for another agent to complete them 128 - - Discuss with the manager agent about reprioritization 129 - 130 - ### 5. Adding Notes 131 - 132 - Document progress, blockers, or important decisions: 133 - 134 - ```bash 135 - cells note <cell-id> "Implemented the basic structure, need to add tests" 136 - cells note <cell-id> "Blocked: waiting for API design decision" 137 - ``` 138 - 139 - Notes create a timestamped log visible to all agents and humans. 140 - 141 - ### 6. Completing Work 142 - 143 - When the task is done: 144 - 145 - ```bash 146 - cells complete <cell-id> 147 - ``` 148 - 149 - **Before completing, verify**: 150 - - All acceptance criteria in the description are met 151 - - Tests pass (if applicable) 152 - - Code is committed (if applicable) 153 - 154 - ### 7. Handling Blockers 155 - 156 - If you discover a dependency that wasn't captured: 157 - 158 - ```bash 159 - # Add the dependency 160 - cells dep add <your-cell> --needs <blocking-cell> 161 - ``` 162 - 163 - This automatically transitions your cell to `blocked` status. 164 - 165 - When the blocking cell completes, you can resume: 166 - 167 - ```bash 168 - cells unblock <cell-id> 169 - cells start <cell-id> 170 - ``` 171 - 172 - ## Multi-Agent Coordination 173 - 174 - ### Communication Protocol 175 - 176 - Agents communicate through cells, not direct messages: 177 - 178 - 1. **Manager → Worker**: Creates cells, sets priorities, assigns work 179 - 2. **Worker → Manager**: Updates status, adds notes, completes cells 180 - 3. **Worker → Worker**: Dependencies between cells 181 - 182 - ### Avoiding Conflicts 183 - 184 - - **Always claim before working**: Prevents duplicate work 185 - - **Use short IDs**: First 6-8 characters usually sufficient (e.g., `01KG8V76`) 186 - - **Check status before operations**: Another agent may have modified the cell 187 - 188 - ### Creating New Cells 189 - 190 - If you discover work that should be tracked: 191 - 192 - ```bash 193 - cells new "Implement error handling" \ 194 - -d "Add proper error handling to the API endpoints" \ 195 - -p high \ 196 - -l backend -l api 197 - ``` 198 - 199 - For work that depends on your current task: 200 - 201 - ```bash 202 - # Create the new cell 203 - cells new "Add unit tests for error handling" 204 - 205 - # Make it depend on your current work 206 - cells dep add <new-cell-id> --needs <your-cell-id> 207 - ``` 208 - 209 - ## jj/Git Integration 210 - 211 - Cells can be bound to version control changes: 212 - 213 - ```bash 214 - # Bind to current jj change 215 - cells bind <cell-id> 216 - 217 - # Bind to specific change 218 - cells bind <cell-id> --change <change-id> 219 - 220 - # Remove binding 221 - cells unbind <cell-id> 222 - 223 - # Verify all bindings are valid 224 - cells sync 225 - ``` 226 - 227 - **Best Practice**: Bind a cell to its change before starting work. This creates traceability between tasks and code. 228 - 229 - When using `cells run`, the cell is automatically bound to the workspace's change. 230 - 231 - ## Agent Harness Configuration 232 - 233 - The agent harness configuration lives in `.cells/config.json`: 234 - 235 - ```json 236 - { 237 - "workspaces_dir": ".worktrees", 238 - "agent": { 239 - "default": "claude-code", 240 - "harnesses": { 241 - "claude-code": { 242 - "command": "claude", 243 - "args": ["-p", "{prompt}"] 244 - }, 245 - "opencode": { 246 - "command": "opencode", 247 - "args": ["-p", "{prompt}"] 248 - } 249 - } 250 - } 251 - } 252 - ``` 253 - 254 - The `{prompt}` placeholder is replaced with the cell's context (title, description, notes). 255 - 256 - ## Priority Levels 257 - 258 - | Priority | Use When | 259 - |----------|----------| 260 - | critical | Production issues, security vulnerabilities, blocking all other work | 261 - | high | Important features, significant bugs, time-sensitive work | 262 - | normal | Standard development tasks (default) | 263 - | low | Nice-to-haves, minor improvements, tech debt | 264 - 265 - Cells are sorted by priority in list output. Always check for critical/high priority work first. 266 - 267 - ## Error Handling 268 - 269 - ### Common Issues 270 - 271 - **"invalid transition"**: Check the current status. Some transitions aren't allowed: 272 - - Can't claim an in_progress cell 273 - - Can't complete a blocked cell 274 - - Can't modify completed/cancelled cells 275 - 276 - **"cell not found"**: The ID prefix might be ambiguous. Use more characters. 277 - 278 - **"ambiguous ID prefix"**: Multiple cells match. Use a longer prefix or full ID. 279 - 280 - ### Recovery 281 - 282 - If something goes wrong: 283 - 284 - ```bash 285 - # View current state 286 - cells show <cell-id> 287 - 288 - # Cancel and start fresh 289 - cells cancel <cell-id> 290 - cells new "Retry: <original title>" 291 - ``` 292 - 293 - ## Best Practices 294 - 295 - 1. **Atomic tasks**: Keep cells small and focused. One clear objective per cell. 296 - 297 - 2. **Clear descriptions**: Include acceptance criteria. Another agent should understand what "done" means. 298 - 299 - 3. **Update status promptly**: Don't leave cells in stale states. Other agents rely on accurate status. 300 - 301 - 4. **Document decisions**: Use notes liberally. Future agents (or humans) will thank you. 302 - 303 - 5. **Respect dependencies**: Don't work around blocked status. The dependency exists for a reason. 304 - 305 - 6. **Release what you can't finish**: If you're stuck or need to stop, release the cell so others can help. 306 - 307 - 7. **Check before claiming**: Verify the cell is actually open and unblocked. 308 - 309 - 8. **Use `cells run` for isolated work**: Each cell gets its own workspace, preventing conflicts with other work. 310 - 311 - ## File Locations 312 - 313 - - `.cells/cells.jsonl` - All cell data (JSONL format) 314 - - `.cells/config.json` - Repository configuration 315 - - `.cells/AGENTS.md` - This guide 316 - 317 - The `.cells/` directory should be version controlled so all agents share the same state. 318 - 319 - ## Workspaces 320 - 321 - When using `cells run`, workspaces are created at: 322 - 323 - ``` 324 - <repo>/ 325 - ├── .cells/ # Cell data and config 326 - ├── .worktrees/ # Default workspaces directory 327 - │ ├── cell-01HX.../ # Workspace for cell 01HX... 328 - │ └── cell-01HY.../ # Workspace for cell 01HY... 329 - └── (main workspace) 330 - ``` 331 - 332 - Each workspace is an independent jj workspace with its own working copy. This allows multiple agents to work on different cells simultaneously without interference.
-341
.cells/JJ_GUIDE.md
··· 1 - # Using jj (Jujutsu) with Cells 2 - 3 - This guide explains how to use jj (Jujutsu) version control with the cells work management system. Cells integrates with jj to provide isolated workspaces and change tracking for each unit of work. 4 - 5 - ## Quick Reference 6 - 7 - ```bash 8 - # Check current change 9 - jj log -r @ # Show current change 10 - jj status # Show working copy status 11 - jj diff # Show uncommitted changes 12 - 13 - # Describe your work 14 - jj desc -m "feat: your message" # Set change description 15 - 16 - # View history 17 - jj log # Show commit log 18 - jj log -r 'all()' # Show all changes including hidden 19 - 20 - # Working with changes 21 - jj new # Create new change on top of current 22 - jj squash # Squash current change into parent 23 - jj edit <change-id> # Edit an existing change 24 - 25 - # Rebasing 26 - jj rebase -r @ -d <dest> # Rebase current change 27 - jj rebase -s <source> -d <dest> # Rebase a subtree 28 - ``` 29 - 30 - ## How Cells Uses jj 31 - 32 - ### Workspace Isolation 33 - 34 - When you run `cells run <id>`, cells: 35 - 36 - 1. Creates a dedicated jj workspace at `.worktrees/cell-<id>` 37 - 2. Each workspace has its own working copy 38 - 3. Sets the change description using conventional commits 39 - 4. Binds the cell to the workspace's change-id 40 - 41 - This allows multiple agents to work on different cells simultaneously without conflicts. 42 - 43 - ### Automatic Description 44 - 45 - `cells run` automatically sets your jj change description using conventional commit format: 46 - 47 - ``` 48 - <type>: <cell title> 49 - 50 - <cell description> 51 - 52 - Cell: <cell-id> 53 - ``` 54 - 55 - The commit type is inferred from: 56 - - Cell labels (e.g., `bug` → `fix`, `feature` → `feat`) 57 - - Title keywords (e.g., "Fix ..." → `fix`, "Add ..." → `feat`) 58 - 59 - ### Change Binding 60 - 61 - Cells tracks which jj change corresponds to each cell via the `change_id` field. This provides: 62 - - Traceability between tasks and code 63 - - Ability to verify work was committed 64 - - Integration with `cells sync` to check for orphaned cells 65 - 66 - ## Conventional Commits 67 - 68 - Use conventional commit format for all change descriptions: 69 - 70 - ``` 71 - <type>[optional scope]: <description> 72 - 73 - [optional body] 74 - 75 - [optional footer(s)] 76 - ``` 77 - 78 - ### Types 79 - 80 - | Type | When to Use | 81 - |------|-------------| 82 - | `feat` | New feature or capability | 83 - | `fix` | Bug fix | 84 - | `docs` | Documentation only | 85 - | `style` | Formatting, no code change | 86 - | `refactor` | Code change that neither fixes nor adds | 87 - | `perf` | Performance improvement | 88 - | `test` | Adding or updating tests | 89 - | `chore` | Maintenance tasks | 90 - | `ci` | CI/CD changes | 91 - | `build` | Build system changes | 92 - 93 - ### Examples 94 - 95 - ```bash 96 - # Feature 97 - jj desc -m "feat: add user authentication 98 - 99 - Implement JWT-based authentication with refresh tokens. 100 - 101 - Cell: 01KG8Y8JVT37" 102 - 103 - # Bug fix 104 - jj desc -m "fix: prevent null pointer in user lookup 105 - 106 - Handle case where user record is deleted mid-session. 107 - 108 - Cell: 01KG8Y8JVT38" 109 - 110 - # Documentation 111 - jj desc -m "docs: add API endpoint documentation 112 - 113 - Cell: 01KG8Y8JVT39" 114 - ``` 115 - 116 - ## Common Workflows 117 - 118 - ### Starting Work on a Cell 119 - 120 - ```bash 121 - # Let cells create the workspace and set description 122 - cells run <cell-id> 123 - 124 - # You're now in the workspace with description set 125 - # Just start coding! 126 - ``` 127 - 128 - ### Making Progress 129 - 130 - ```bash 131 - # Check what you've changed 132 - jj status 133 - jj diff 134 - 135 - # jj auto-commits on every change, so just keep working 136 - # Optionally update description as you go 137 - jj desc -m "feat: updated description of progress" 138 - ``` 139 - 140 - ### Creating Checkpoints 141 - 142 - ```bash 143 - # Create a new change for the next phase of work 144 - jj new -m "feat: continue implementation" 145 - 146 - # Previous work is now in parent change 147 - ``` 148 - 149 - ### Squashing Work 150 - 151 - When you have multiple changes for one cell: 152 - 153 - ```bash 154 - # View your changes 155 - jj log 156 - 157 - # Squash current into parent 158 - jj squash 159 - 160 - # Or squash with a new message 161 - jj squash -m "feat: complete feature implementation" 162 - ``` 163 - 164 - ### Rebasing onto Main 165 - 166 - When your work is done and needs to merge: 167 - 168 - ```bash 169 - # First, identify the main branch 170 - jj log -r 'trunk()' 171 - 172 - # Rebase your changes onto trunk 173 - jj rebase -r @ -d trunk() 174 - 175 - # Or rebase a whole subtree 176 - jj rebase -s <first-change> -d trunk() 177 - ``` 178 - 179 - ### Handling Conflicts 180 - 181 - ```bash 182 - # If rebase causes conflicts 183 - jj status # Shows conflicted files 184 - 185 - # Option 1: Resolve in editor 186 - # Edit the conflicted files, remove conflict markers 187 - 188 - # Option 2: Use jj resolve 189 - jj resolve # Interactive resolution 190 - 191 - # After resolving 192 - jj squash # Squash resolution into conflicted change 193 - ``` 194 - 195 - ## Workspace Management 196 - 197 - ### Listing Workspaces 198 - 199 - ```bash 200 - # Via cells 201 - cells workspace list 202 - 203 - # Via jj 204 - jj workspace list 205 - ``` 206 - 207 - ### Cleaning Up 208 - 209 - ```bash 210 - # Clean workspaces for completed cells 211 - cells workspace clean 212 - 213 - # Clean a specific cell's workspace 214 - cells workspace clean <cell-id> 215 - 216 - # Clean orphaned workspaces 217 - cells workspace clean-orphaned 218 - 219 - # Or manually via jj 220 - jj workspace forget <workspace-name> 221 - rm -rf .worktrees/<workspace-name> 222 - ``` 223 - 224 - ### Switching Workspaces 225 - 226 - ```bash 227 - # Just cd to the workspace 228 - cd .worktrees/cell-01KG8Y8J 229 - 230 - # Or start fresh with cells run 231 - cells run <cell-id> 232 - ``` 233 - 234 - ## Best Practices 235 - 236 - ### 1. Let Cells Manage Workspaces 237 - 238 - Don't manually create workspaces for cells. Use `cells run` to ensure: 239 - - Proper naming convention 240 - - Change binding 241 - - Description setting 242 - - Cell status updates 243 - 244 - ### 2. Keep Changes Atomic 245 - 246 - Each cell should correspond to one logical change: 247 - - If you create multiple changes, squash before completing 248 - - Use `jj new` for work-in-progress checkpoints, then squash 249 - 250 - ### 3. Write Good Descriptions 251 - 252 - ```bash 253 - # Bad 254 - jj desc -m "updates" 255 - 256 - # Good 257 - jj desc -m "feat: add retry logic to API client 258 - 259 - Implement exponential backoff with jitter for transient failures. 260 - Max 3 retries with 1s initial delay. 261 - 262 - Cell: 01KG8Y8JVT37" 263 - ``` 264 - 265 - ### 4. Rebase Before Completing 266 - 267 - Before marking a cell complete: 268 - 269 - ```bash 270 - # Ensure your change is based on latest 271 - jj rebase -r @ -d trunk() 272 - 273 - # Verify it applies cleanly 274 - jj status 275 - 276 - # Then complete the cell 277 - cells complete <cell-id> --cleanup 278 - ``` 279 - 280 - ### 5. Use `jj log` Liberally 281 - 282 - ```bash 283 - # See what you're working on 284 - jj log -r @ 285 - 286 - # See the full picture 287 - jj log -r '::@' 288 - 289 - # See all changes in all workspaces 290 - jj log -r 'all()' 291 - ``` 292 - 293 - ## Troubleshooting 294 - 295 - ### "not a jj repository" 296 - 297 - You're not in a jj-managed directory. Either: 298 - - Navigate to your repo root 299 - - Use `cells run` which handles workspace navigation 300 - 301 - ### "workspace already exists" 302 - 303 - The workspace already exists. Use: 304 - ```bash 305 - cells run <cell-id> # Reuses existing workspace 306 - ``` 307 - 308 - ### "conflicting changes" 309 - 310 - After a rebase: 311 - ```bash 312 - jj status # See conflicts 313 - # Edit files to resolve 314 - jj squash # Apply resolution 315 - ``` 316 - 317 - ### "change not found" 318 - 319 - The change-id in the cell no longer exists: 320 - ```bash 321 - cells sync # Check all bindings 322 - cells unbind <cell-id> # Clear stale binding 323 - ``` 324 - 325 - ## jj vs git 326 - 327 - | Operation | jj | git | 328 - |-----------|-----|-----| 329 - | Commit | Automatic | `git commit` | 330 - | Describe | `jj desc -m` | `git commit --amend` | 331 - | New change | `jj new` | `git commit` | 332 - | Rebase | `jj rebase` | `git rebase` | 333 - | Squash | `jj squash` | `git rebase -i` | 334 - | View log | `jj log` | `git log` | 335 - | View diff | `jj diff` | `git diff` | 336 - 337 - Key differences: 338 - - jj has no staging area - all changes are always "staged" 339 - - jj auto-commits on every file change 340 - - jj changes are mutable until pushed 341 - - jj supports multiple concurrent workspaces natively
-19
.cells/cells.jsonl
··· 1 - {"id":"01KGA7T026JJ8H9EK24K17QRA4","title":"Improve loading UX with progress bars or transitions","description":"Loading on HTMX could be snappier with loading bars or transitionary animations.\n\nOptions:\n- Loading bar waiting until everything is loaded\n- Transitionary animations between skeleton and full loads\n- Reconsider if skeleton loading is needed with SSR (probably yes due to PDS data fetches)\n\nAcceptance criteria:\n- Smoother loading experience\n- No jarring transitions\n- Consistent loading indicators","status":"open","priority":"normal","labels":["frontend","ux","htmx"],"created_at":"2026-01-31T14:37:42.342432216Z","updated_at":"2026-01-31T14:37:42.342432216Z"} 2 - {"id":"01KGA7T04HWD93MJQDKZ62XQNB","title":"Fix skeleton headers to match actual table headers","description":"Headers in loading skeletons need to exactly match final table headers.\n\nCurrent issue:\n- Profile refresh shows brew headers for all tabs\n- Inconsistent column counts (brew_list: 6 cols, profile: 5 cols, manage: 5 cols)\n\nAcceptance criteria:\n- Skeleton headers match actual content headers\n- Tab-specific skeletons or dynamic skeletons based on active tab\n- Consistent column structure","status":"open","priority":"normal","labels":["frontend","ux","skeleton"],"created_at":"2026-01-31T14:37:42.417653543Z","updated_at":"2026-01-31T14:37:42.417653543Z"} 3 - {"id":"01KGA7T08BAWPB977KSHZNS8NE","title":"Revision pass on about and terms text","description":"Review and revise text content in about and terms pages for clarity and accuracy.\n\nAcceptance criteria:\n- Clear, concise messaging\n- No typos or grammar issues\n- Accurate information","status":"claimed","priority":"low","assignee":"patrick","labels":["content","documentation"],"created_at":"2026-01-31T14:37:42.539757583Z","updated_at":"2026-01-31T15:30:48.329882372Z"} 4 - {"id":"01KGA7T0AE0R3F8PRHX761W03E","title":"Implement profile picture caching in database","description":"Cache profile pictures to database to avoid reloading them frequently. This may already be partially implemented - needs investigation.\n\nAcceptance criteria:\n- Profile pictures cached in database\n- Reduced PDS API calls for avatar fetching\n- Cache invalidation strategy\n- Performance improvement","status":"open","priority":"high","labels":["backend","performance","caching"],"created_at":"2026-01-31T14:37:42.606749189Z","updated_at":"2026-01-31T14:37:42.606749189Z"} 5 - {"id":"01KGA7TZG6AP8FJJTW5GAA60X5","title":"Evaluate merging manage and profile pages","description":"Consider if manage, profile, and brew list should be separate pages.\n\nOptions:\n- Merge manage and profile\n- Merge brew list and manage\n- Keep separate\n\nNeeds design decision before implementation.\n\nAcceptance criteria:\n- Evaluate user flow and UX\n- Document decision rationale\n- Implementation plan if merge is chosen","status":"open","priority":"low","labels":["design","ux","frontend"],"created_at":"2026-01-31T14:38:14.534537889Z","updated_at":"2026-01-31T14:38:14.534537889Z"} 6 - {"id":"01KGA7TZMPFGZH6R6ZMASAD2GE","title":"Design nested modal system for entity creation","description":"Enable creating related entities from within modals (e.g., create roaster from within bean modal).\n\nDesign idea:\n- Transition that moves first modal left\n- Opens second modal to the right\n- Smooth nested flow\n\nAcceptance criteria:\n- Design nested modal UX\n- Smooth transitions\n- Good visual hierarchy\n- Can return to parent modal\n- Data flows correctly between modals","status":"open","priority":"low","labels":["design","ux","modals","future"],"created_at":"2026-01-31T14:38:14.678025134Z","updated_at":"2026-02-15T16:06:00Z"} 7 - {"id":"01KGA7TZTT05WWENTHR41X4RQH","title":"Replace inline buttons with button components","description":"buttons.templ defines PrimaryButton/SecondaryButton but they're rarely used.\nMost places use inline button classes.\n\nAcceptance criteria:\n- All buttons use button components\n- Remove inline button class usage\n- Consistent button styling\n- Component-based approach","status":"open","priority":"low","labels":["refactor","frontend","components"],"created_at":"2026-01-31T14:38:14.874329448Z","updated_at":"2026-01-31T14:38:14.874329448Z"} 8 - {"id":"01KGA7VXDTVY5W11QJ1VPZ5M3Y","title":"Implement settings menu","description":"Create settings menu with the following features:\n\n1. Private mode - Don't show in community feed (records still public via PDS API)\n2. Dev mode - Show DID, copy DID in profiles (remove 'logged in as \u003cdid\u003e' from home)\n3. Toggle for table view vs future post-style view\n\nAcceptance criteria:\n- Settings page/modal created\n- All three settings implemented\n- Settings persisted per user\n- UI updated based on settings","status":"blocked","priority":"normal","blocked_by":["01KGA7VXFQABCAQV83A6C49WEY"],"labels":["feature","frontend","settings"],"created_at":"2026-01-31T14:38:45.178941765Z","updated_at":"2026-01-31T14:39:00.016324424Z"} 9 - {"id":"01KGA7VXFQABCAQV83A6C49WEY","title":"Design post-style record view (mobile-friendly)","description":"LARGE FEATURE: Complete record styling refactor from table-style to mobile-friendly post-style.\n\nSimilar to Bluesky posts format.\nShould include setting to use legacy table view.\n\nThis is a major redesign - to be done later down the line.\n\nAcceptance criteria:\n- Design post-style layout\n- Mobile-friendly and responsive\n- Legacy table view option\n- Smooth migration path\n- Better UX on mobile devices","status":"open","priority":"low","labels":["feature","design","frontend","mobile","future"],"created_at":"2026-01-31T14:38:45.23927703Z","updated_at":"2026-01-31T14:38:45.23927703Z"} 10 - {"id":"01KGA7VXHR31MSHEKH91C41TYM","title":"Add loading progress bars to page navigation","description":"Consider adding loading bars to page loads (above header perhaps).\n\nSeparate nicer/prettier loading bar would also be nice on brews page.\n\nAcceptance criteria:\n- Loading bar visible during page navigation\n- Positioned appropriately (above header)\n- Smooth animations\n- Better perceived performance","status":"open","priority":"low","labels":["feature","frontend","ux"],"created_at":"2026-01-31T14:38:45.304772299Z","updated_at":"2026-01-31T14:38:45.304772299Z"} 11 - {"id":"01KGA7VXM0F3A1R10BC3V29A0Y","title":"Verify context flows through all methods","description":"From CLAUDE.md Known Issues: Context should flow through methods (some fixed, verify all paths).\n\nAudit codebase to ensure context.Context is properly passed through all method chains.\n\nAcceptance criteria:\n- All methods accept and use context\n- No context.Background() in handlers\n- Context cancellation works correctly\n- Request timeouts respected","status":"open","priority":"high","labels":["backend","refactor","correctness"],"created_at":"2026-01-31T14:38:45.376033105Z","updated_at":"2026-01-31T14:38:45.376033105Z"} 12 - {"id":"01KGA7VXP6P4SMB1MTYFFM3WZH","title":"Fix cache race conditions with copy-on-write","description":"From CLAUDE.md Known Issues: Cache race conditions need copy-on-write pattern.\n\nCurrent caching implementation may have race conditions.\n\nAcceptance criteria:\n- Implement copy-on-write pattern\n- No data races (verified with -race flag)\n- Thread-safe cache operations\n- Performance maintained or improved","status":"open","priority":"high","labels":["backend","concurrency","bug","cache"],"created_at":"2026-01-31T14:38:45.446031585Z","updated_at":"2026-01-31T14:38:45.446031585Z"} 13 - {"id":"01KGA7VXR4EVGR470P4M9JH80C","title":"Add CID validation on record updates","description":"From CLAUDE.md Known Issues: Missing CID validation on record updates (AT Protocol best practice).\n\nImplement CID (Content Identifier) validation when updating records to ensure record hasn't changed.\n\nAcceptance criteria:\n- CID validation on all record updates\n- Proper error handling for CID mismatches\n- Follows AT Protocol best practices\n- Tests covering CID validation","status":"open","priority":"normal","labels":["backend","atproto","validation"],"created_at":"2026-01-31T14:38:45.508526626Z","updated_at":"2026-01-31T14:38:45.508526626Z"} 14 - {"id":"01KGA7VXT4S4VGYZ2FDE1RR2XX","title":"Implement rate limiting for PDS calls","description":"From CLAUDE.md Known Issues: Rate limiting for PDS calls not implemented.\n\nAdd rate limiting to prevent excessive API calls to PDS servers.\n\nAcceptance criteria:\n- Rate limiting implemented for PDS API calls\n- Configurable limits\n- Proper error handling when rate limited\n- Backoff strategy\n- Monitoring/logging of rate limit hits","status":"open","priority":"high","labels":["backend","performance","atproto"],"created_at":"2026-01-31T14:38:45.572884867Z","updated_at":"2026-01-31T14:38:45.572884867Z"} 15 - {"id":"01KGA7VXVZQBMZX6RVG3B49TYW","title":"Consider migration from BoltDB to SQLite","description":"Far future consideration: Maybe swap from BoltDB to SQLite using non-cgo library.\n\nThis is exploratory - need to evaluate:\n- Benefits of SQLite vs BoltDB\n- Migration effort\n- Performance impact\n- Query capabilities\n\nAcceptance criteria:\n- Evaluate pros/cons\n- Document decision\n- If proceeding: migration plan and implementation","status":"claimed","priority":"low","assignee":"patrick","labels":["backend","database","future","evaluation"],"created_at":"2026-01-31T14:38:45.631852717Z","updated_at":"2026-01-31T15:31:07.948160371Z"} 16 - {"id":"01KGDXVR86CB2H44DJHKN4Z9M0","title":"Implement comments for social interactions","description":"Add a comment lexicon and implement comment functionality across the app.\n\n## Lexicon: social.arabica.alpha.comment\n\nThe comment record should:\n- Use `com.atproto.repo.strongRef` for the subject (URI + CID for immutability)\n- Support commenting on any arabica.social lexicon type (beans, roasters, grinders, brewers, brews, and other comments)\n- Include text field (max 1000 chars / 300 graphemes as per AT Protocol conventions)\n- Include createdAt timestamp\n- Use TID as record key\n- NOTE: This cell implements flat comments only. Threaded/nested comments are handled in a separate cell.\n\n## Backend Implementation\n\n1. Create lexicon file: `lexicons/social.arabica.alpha.comment.json`\n2. Add NSID constant in `internal/atproto/nsid.go`\n3. Add Comment model in `internal/models/models.go`\n4. Add record conversion functions in `internal/atproto/records.go`\n5. Add Store interface methods: CreateComment, DeleteComment, GetCommentsForSubject, GetUserComments\n6. Implement in AtprotoStore\n7. Update firehose indexing to track comments\n\n## Frontend Implementation\n\n1. Add comment section component below brew detail view\n2. Add comment form (authenticated users only)\n3. Display comment count on records in feed\n4. Update comment counts in response to firehose events\n5. Show commenter profile info (avatar, handle)\n\n## Acceptance Criteria\n\n- Users can comment on any arabica.social record\n- Comments are stored in the user's PDS (actor-owned data)\n- Comment counts display on records in the feed\n- Comments visible on record detail views\n- Real-time comment count updates via firehose","status":"open","priority":"normal","labels":["backend","frontend","atproto"],"created_at":"2026-02-02T01:00:51.846427126Z","updated_at":"2026-02-02T01:00:51.846427126Z"} 17 - {"id":"01KGDXW31PHFMBE3WTV83HPET1","title":"Implement comment threading","description":"Add support for threaded/nested comments, building on the flat comment system.\n\n## Lexicon Changes\n\nUpdate `social.arabica.alpha.comment` to add optional threading fields:\n- `parent`: Optional strongRef to parent comment (for replies)\n- `root`: Optional strongRef to root subject (maintains context when replying to comments)\n\nAlternatively, consider a separate reply field or keeping comments flat with UI-level threading.\n\n## Backend Implementation\n\n1. Update comment model to include parent/root references\n2. Add methods to fetch comment threads: GetCommentThread, GetReplies\n3. Update firehose indexing to track parent-child relationships\n4. Add depth limits for threading (prevent infinite nesting)\n\n## Frontend Implementation\n\n1. Design threaded comment UI (indentation, collapse/expand)\n2. Add 'reply' button on comments\n3. Show reply context when replying\n4. Consider max nesting depth for display (e.g., 3-4 levels)\n5. Mobile-friendly thread navigation\n\n## Design Considerations\n\n- How deep should threading go? (Recommend max 3-4 levels visible, then flatten)\n- How to handle deleted parent comments?\n- Should users be notified when someone replies to their comment?\n- Performance: lazy-load deep threads vs eager-load\n\n## Acceptance Criteria\n\n- Users can reply directly to comments\n- Thread structure is visually clear\n- Threads can be collapsed/expanded\n- Works well on mobile devices\n- Parent context shown when replying","status":"blocked","priority":"low","blocked_by":["01KGDXVR86CB2H44DJHKN4Z9M0"],"labels":["frontend","atproto"],"created_at":"2026-02-02T01:01:02.902692829Z","updated_at":"2026-02-02T01:01:06.361891137Z"} 18 - {"id":"01KJ36P2BWM4HSZV6GNX0BJB1F","title":"Implement lightweight For You feed algorithm","description":"Add a 'For You' algorithmic feed tab alongside the existing chronological feed. This should be a lightweight scoring system that ranks posts based on:\n\n**Scoring Factors:**\n1. **Engagement score**: likes (weight 3x) + comments (weight 2x) on the post\n2. **Time decay**: Score multiplied by a decay factor based on post age. Use exponential decay with a half-life of ~24 hours so recent engaged content surfaces while popular older content still has a chance.\n3. **Type diversity**: After scoring, apply a diversity pass to avoid showing too many of the same record type in a row. If 3+ consecutive items are the same type, interleave with the next different-type item.\n4. **Social proximity** (future): Boost posts from users the viewer has interacted with (liked their content, commented on their posts). This requires building a per-user interaction graph from the like/comment indexes.\n\n**Implementation approach:**\n- Add a new `FeedSortForYou` sort option alongside recent/popular\n- Add a `scoreForYouItem(item *FeedItem, viewerDID string) float64` function in the firehose index\n- Fetch ~100 recent items, score them, apply diversity, return top N\n- Add a 'For You' tab in the feed filter bar UI (only for authenticated users)\n- Cache scored results per-viewer with short TTL (1-2 min) to avoid re-scoring on pagination\n\n**Key files:**\n- `internal/firehose/index.go` - Add scoring logic and ForYou query\n- `internal/feed/service.go` - Add FeedSortForYou constant\n- `internal/firehose/adapter.go` - Pass through ForYou sort\n- `internal/handlers/feed.go` - Handle sort=foryou param\n- `internal/web/pages/feed.templ` - Add For You tab\n\n**Dependencies:**\n- Relies on existing BucketByTime, BucketLikeCounts, BucketCommentCounts, BucketLikesByActor indexes\n- Social proximity scoring depends on being able to query BucketLikesByActor efficiently","status":"open","priority":"normal","created_at":"2026-02-22T17:34:47.67684351Z","updated_at":"2026-02-22T17:34:47.67684351Z"} 19 - {"id":"01KJ37P3RD4X4P7B8F5EWE6NWB","title":"Add Prometheus metrics endpoint","description":"Add prometheus/client_golang instrumentation to Arabica with a /metrics endpoint. Minimal but useful instrumentation points:\n\n## Metrics to implement\n\n### HTTP middleware (internal/middleware/)\n- `arabica_http_requests_total` counter: labels: method, path, status\n- `arabica_http_request_duration_seconds` histogram: labels: method, path\n\n### Firehose consumer (internal/firehose/)\n- `arabica_firehose_events_total` counter: labels: collection, operation\n- `arabica_firehose_connection_state` gauge: 1=connected, 0=disconnected\n- `arabica_firehose_errors_total` counter\n\n### PDS client (internal/atproto/)\n- `arabica_pds_requests_total` counter: labels: method, collection\n- `arabica_pds_request_duration_seconds` histogram: labels: method\n- `arabica_pds_errors_total` counter: labels: method\n\n### Feed service (internal/feed/)\n- `arabica_feed_cache_hits_total` counter\n- `arabica_feed_cache_misses_total` counter\n\n## Implementation notes\n- Add `github.com/prometheus/client_golang` dependency\n- Expose /metrics endpoint in routing.go (no auth required)\n- Keep cardinality low: normalize path labels (e.g. /brews/{id} -\u003e /brews/:id)\n- Create a grafana/arabica-prometheus.json dashboard alongside the existing log-based one\n- Existing log-based dashboard is at grafana/arabica-logs.json for reference","status":"completed","priority":"normal","assignee":"patrick","labels":["backend","observability"],"created_at":"2026-02-22T17:52:17.677417112Z","updated_at":"2026-02-22T17:57:56.326244513Z","completed_at":"2026-02-22T17:57:56.315753758Z"}
-18
.cells/config.json
··· 1 - { 2 - "version": 1, 3 - "created_at": "2026-01-31T14:36:42.850744809Z", 4 - "agent": { 5 - "default": "claude-code", 6 - "harnesses": { 7 - "claude-code": { 8 - "command": "claude", 9 - "args": [] 10 - }, 11 - "opencode": { 12 - "command": "opencode", 13 - "args": [] 14 - } 15 - } 16 - } 17 - } 18 -
-641
docs/firehose-plan.md
··· 1 - # Firehose Integration Plan for Arabica 2 - 3 - ## Executive Summary 4 - 5 - This document proposes refactoring Arabica's home page feed to consume events from the AT Protocol firehose via Jetstream, replacing the current polling-based approach. This will provide real-time updates, dramatically reduce API calls, and improve scalability. 6 - 7 - **Recommendation:** Implement Jetstream consumer with local BoltDB index as Phase 1, with optional Slingshot/Constellation integration in Phase 2. 8 - 9 - --- 10 - 11 - ## Problem Statement 12 - 13 - ### Current Architecture 14 - 15 - The feed service (`internal/feed/service.go`) polls each registered user's PDS directly: 16 - 17 - ``` 18 - For N registered users: 19 - - N profile fetches 20 - - N × 5 collection fetches (brew, bean, roaster, grinder, brewer) 21 - - N × 4 reference resolution fetches 22 - - Total: ~10N API calls per refresh 23 - ``` 24 - 25 - ### Issues 26 - 27 - | Problem | Impact | 28 - | ------------------------ | ----------------------------------- | 29 - | High API call volume | Risk of rate limiting as users grow | 30 - | 5-minute cache staleness | Users don't see recent activity | 31 - | N+1 query pattern | Linear scaling, O(N) per refresh | 32 - | PDS dependency | Feed fails if any PDS is slow/down | 33 - | No real-time updates | Requires manual refresh | 34 - 35 - --- 36 - 37 - ## Proposed Solution: Jetstream Consumer 38 - 39 - ### Architecture Overview 40 - 41 - ``` 42 - ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ 43 - │ AT Protocol │ │ Jetstream │ │ Arabica │ 44 - │ Firehose │────▶│ (Public/Self) │────▶│ Consumer │ 45 - │ (all records) │ │ JSON over WS │ │ (background) │ 46 - └─────────────────┘ └──────────────────┘ └────────┬────────┘ 47 - 48 - 49 - ┌─────────────────┐ 50 - │ Feed Index │ 51 - │ (BoltDB) │ 52 - └────────┬────────┘ 53 - 54 - 55 - ┌─────────────────┐ 56 - │ HTTP Handler │ 57 - │ (instant) │ 58 - └─────────────────┘ 59 - ``` 60 - 61 - ### How It Works 62 - 63 - 1. **Background Consumer** connects to Jetstream WebSocket 64 - 2. **Filters** for `social.arabica.alpha.*` collections 65 - 3. **Indexes** incoming events into local BoltDB 66 - 4. **Serves** feed requests instantly from local index 67 - 5. **Fallback** to direct polling if consumer disconnects 68 - 69 - ### Benefits 70 - 71 - | Metric | Current | With Jetstream | 72 - | --------------------- | ---------------- | ----------------- | 73 - | API calls per refresh | ~10N | 0 | 74 - | Feed latency | 5 min cache | Real-time (<1s) | 75 - | PDS dependency | High | None (after sync) | 76 - | User discovery | Manual registry | Automatic | 77 - | Scalability | O(N) per request | O(1) per request | 78 - 79 - --- 80 - 81 - ## Technical Design 82 - 83 - ### 1. Jetstream Client Configuration 84 - 85 - ```go 86 - // internal/firehose/config.go 87 - 88 - type JetstreamConfig struct { 89 - // Public endpoints (fallback rotation) 90 - Endpoints []string 91 - 92 - // Filter to Arabica collections only 93 - WantedCollections []string 94 - 95 - // Optional: filter to registered DIDs only 96 - // Leave empty to discover all Arabica users 97 - WantedDids []string 98 - 99 - // Enable zstd compression (~56% bandwidth reduction) 100 - Compress bool 101 - 102 - // Cursor file path for restart recovery 103 - CursorFile string 104 - } 105 - 106 - func DefaultConfig() *JetstreamConfig { 107 - return &JetstreamConfig{ 108 - Endpoints: []string{ 109 - "wss://jetstream1.us-east.bsky.network/subscribe", 110 - "wss://jetstream2.us-east.bsky.network/subscribe", 111 - "wss://jetstream1.us-west.bsky.network/subscribe", 112 - "wss://jetstream2.us-west.bsky.network/subscribe", 113 - }, 114 - WantedCollections: []string{ 115 - "social.arabica.alpha.brew", 116 - "social.arabica.alpha.bean", 117 - "social.arabica.alpha.roaster", 118 - "social.arabica.alpha.grinder", 119 - "social.arabica.alpha.brewer", 120 - }, 121 - Compress: true, 122 - CursorFile: "jetstream-cursor.txt", 123 - } 124 - } 125 - ``` 126 - 127 - ### 2. Event Processing 128 - 129 - ```go 130 - // internal/firehose/consumer.go 131 - 132 - type Consumer struct { 133 - config *JetstreamConfig 134 - index *FeedIndex 135 - client *jetstream.Client 136 - cursor atomic.Int64 137 - connected atomic.Bool 138 - } 139 - 140 - func (c *Consumer) handleEvent(ctx context.Context, event *models.Event) error { 141 - if event.Kind != "commit" || event.Commit == nil { 142 - return nil 143 - } 144 - 145 - commit := event.Commit 146 - 147 - // Only process Arabica collections 148 - if !strings.HasPrefix(commit.Collection, "social.arabica.alpha.") { 149 - return nil 150 - } 151 - 152 - switch commit.Operation { 153 - case "create", "update": 154 - return c.index.UpsertRecord(ctx, event.Did, commit) 155 - case "delete": 156 - return c.index.DeleteRecord(ctx, event.Did, commit.Collection, commit.RKey) 157 - } 158 - 159 - // Update cursor for recovery 160 - c.cursor.Store(event.TimeUS) 161 - 162 - return nil 163 - } 164 - ``` 165 - 166 - ### 3. Feed Index Schema (BoltDB) 167 - 168 - ```go 169 - // internal/firehose/index.go 170 - 171 - // BoltDB Buckets: 172 - // - "records" : {at-uri} -> {record JSON + metadata} 173 - // - "by_time" : {timestamp:at-uri} -> {} (for chronological queries) 174 - // - "by_did" : {did:at-uri} -> {} (for user-specific queries) 175 - // - "by_type" : {collection:timestamp:at-uri} -> {} (for type filtering) 176 - // - "profiles" : {did} -> {profile JSON} (cached profiles) 177 - // - "cursor" : "jetstream" -> {cursor value} 178 - 179 - type FeedIndex struct { 180 - db *bbolt.DB 181 - } 182 - 183 - type IndexedRecord struct { 184 - URI string `json:"uri"` 185 - DID string `json:"did"` 186 - Collection string `json:"collection"` 187 - RKey string `json:"rkey"` 188 - Record json.RawMessage `json:"record"` 189 - CID string `json:"cid"` 190 - IndexedAt time.Time `json:"indexed_at"` 191 - } 192 - 193 - func (idx *FeedIndex) GetRecentFeed(ctx context.Context, limit int) ([]*FeedItem, error) { 194 - // Query by_time bucket in reverse order 195 - // Hydrate with profile data from profiles bucket 196 - // Return feed items instantly from local data 197 - } 198 - ``` 199 - 200 - ### 4. Profile Resolution 201 - 202 - Profiles are not part of Arabica's lexicons, so we need a strategy: 203 - 204 - **Option A: Lazy Loading (Recommended for Phase 1)** 205 - 206 - ```go 207 - func (idx *FeedIndex) resolveProfile(ctx context.Context, did string) (*Profile, error) { 208 - // Check local cache first 209 - if profile := idx.getCachedProfile(did); profile != nil { 210 - return profile, nil 211 - } 212 - 213 - // Fetch from public API and cache 214 - profile, err := publicClient.GetProfile(ctx, did) 215 - if err != nil { 216 - return nil, err 217 - } 218 - 219 - idx.cacheProfile(did, profile, 1*time.Hour) 220 - return profile, nil 221 - } 222 - ``` 223 - 224 - **Option B: Slingshot Integration (Phase 2)** 225 - 226 - ```go 227 - // Use Slingshot's resolveMiniDoc for faster profile resolution 228 - func (idx *FeedIndex) resolveProfileViaSlingshot(ctx context.Context, did string) (*Profile, error) { 229 - url := fmt.Sprintf("https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=%s", did) 230 - // Returns {did, handle, pds} in one call 231 - } 232 - ``` 233 - 234 - ### 5. Reference Resolution 235 - 236 - Brews reference beans, grinders, and brewers. The index already has these records: 237 - 238 - ```go 239 - func (idx *FeedIndex) resolveBrew(ctx context.Context, brew *IndexedRecord) (*FeedItem, error) { 240 - var record map[string]interface{} 241 - json.Unmarshal(brew.Record, &record) 242 - 243 - item := &FeedItem{RecordType: "brew"} 244 - 245 - // Resolve bean reference from local index 246 - if beanRef, ok := record["beanRef"].(string); ok { 247 - if bean := idx.getRecord(beanRef); bean != nil { 248 - item.Bean = recordToBean(bean) 249 - } 250 - } 251 - 252 - // Similar for grinder, brewer references 253 - // All from local index - no API calls 254 - 255 - return item, nil 256 - } 257 - ``` 258 - 259 - ### 6. Fallback and Resilience 260 - 261 - ```go 262 - // internal/firehose/consumer.go 263 - 264 - func (c *Consumer) Run(ctx context.Context) error { 265 - for { 266 - select { 267 - case <-ctx.Done(): 268 - return ctx.Err() 269 - default: 270 - if err := c.connectAndConsume(ctx); err != nil { 271 - log.Warn().Err(err).Msg("jetstream connection lost, reconnecting...") 272 - 273 - // Exponential backoff 274 - time.Sleep(c.backoff.NextBackOff()) 275 - 276 - // Rotate to next endpoint 277 - c.rotateEndpoint() 278 - continue 279 - } 280 - } 281 - } 282 - } 283 - 284 - func (c *Consumer) connectAndConsume(ctx context.Context) error { 285 - cursor := c.loadCursor() 286 - 287 - // Rewind cursor slightly to handle duplicates safely 288 - if cursor > 0 { 289 - cursor -= 5 * time.Second.Microseconds() 290 - } 291 - 292 - return c.client.ConnectAndRead(ctx, &cursor) 293 - } 294 - ``` 295 - 296 - ### 7. Feed Service Integration 297 - 298 - ```go 299 - // internal/feed/service.go (modified) 300 - 301 - type Service struct { 302 - registry *Registry 303 - publicClient *atproto.PublicClient 304 - cache *publicFeedCache 305 - 306 - // New: firehose index 307 - firehoseIndex *firehose.FeedIndex 308 - useFirehose bool 309 - } 310 - 311 - func (s *Service) GetRecentRecords(ctx context.Context, limit int) ([]*FeedItem, error) { 312 - // Prefer firehose index if available and populated 313 - if s.useFirehose && s.firehoseIndex.IsReady() { 314 - return s.firehoseIndex.GetRecentFeed(ctx, limit) 315 - } 316 - 317 - // Fallback to polling (existing code) 318 - return s.getRecentRecordsViaPolling(ctx, limit) 319 - } 320 - ``` 321 - 322 - --- 323 - 324 - ## Implementation Phases 325 - 326 - ### Phase 1: Core Jetstream Consumer (2 weeks) 327 - 328 - **Goal:** Replace polling with firehose consumption for the feed. 329 - 330 - **Tasks:** 331 - 332 - 1. Create `internal/firehose/` package 333 - - `config.go` - Jetstream configuration 334 - - `consumer.go` - WebSocket consumer with reconnection 335 - - `index.go` - BoltDB-backed feed index 336 - - `scheduler.go` - Event processing scheduler 337 - 338 - 2. Integrate with existing feed service 339 - - Add feature flag: `ARABICA_USE_FIREHOSE=true` (just use a cli flag) 340 - - Keep polling as fallback 341 - 342 - 3. Handle profile resolution 343 - - Cache profiles locally with 1-hour TTL 344 - - Lazy fetch on first access 345 - - Background refresh for active users 346 - 347 - 4. Cursor management 348 - - Persist cursor to survive restarts 349 - - Rewind on reconnection for safety 350 - 351 - **Deliverables:** 352 - 353 - - Real-time feed updates 354 - - Reduced API calls to near-zero 355 - - Automatic user discovery (anyone using Arabica lexicons) 356 - 357 - ### Phase 2: Slingshot Optimization (1 week) 358 - 359 - **Goal:** Faster profile and record hydration. 360 - 361 - **Tasks:** 362 - 363 - 1. Add Slingshot client (`internal/atproto/slingshot.go`) 364 - 2. Use `resolveMiniDoc` for profile resolution 365 - 3. Use Slingshot as fallback for missing records 366 - 367 - **Deliverables:** 368 - 369 - - Faster profile loading 370 - - Resilience to slow PDS endpoints 371 - 372 - ### Phase 3: Constellation for Social (1 week) 373 - 374 - **Goal:** Enable like/comment counts when social features are added. 375 - 376 - **Tasks:** 377 - 378 - 1. Add Constellation client (`internal/atproto/constellation.go`) 379 - 2. Query backlinks for interaction counts 380 - 3. Display counts on feed items 381 - 382 - **Deliverables:** 383 - 384 - - Like count on brews 385 - - Comment count on brews 386 - - Foundation for social features 387 - 388 - ### Phase 4: Spacedust for Real-time Notifications (Future) 389 - 390 - **Goal:** Push notifications for interactions. 391 - 392 - **Tasks:** 393 - 394 - 1. Subscribe to Spacedust for user's content interactions 395 - 2. Build notification storage and API 396 - 3. WebSocket to frontend for live updates 397 - 398 - --- 399 - 400 - ## Data Flow Comparison 401 - 402 - ### Before (Polling) 403 - 404 - ``` 405 - User Request → Check Cache → [Cache Miss] → Poll N PDSes → Build Feed → Return 406 - 407 - ~10N API calls 408 - 5-10 second latency 409 - ``` 410 - 411 - ### After (Jetstream) 412 - 413 - ``` 414 - Jetstream → Consumer → Index (BoltDB) 415 - 416 - User Request → Query Index → Return 417 - 418 - 0 API calls 419 - <10ms latency 420 - ``` 421 - 422 - --- 423 - 424 - ## Automatic User Discovery 425 - 426 - A major benefit of firehose consumption is automatic user discovery: 427 - 428 - **Current:** Users must explicitly register via `/api/feed/register` 429 - 430 - **With Jetstream:** Any user who creates an Arabica record is automatically indexed 431 - 432 - ```go 433 - // When we see a new DID creating Arabica records 434 - func (c *Consumer) handleNewUser(did string) { 435 - // Auto-register for feed 436 - c.registry.Register(did) 437 - 438 - // Fetch and cache their profile 439 - go c.index.fetchAndCacheProfile(did) 440 - 441 - // Backfill their existing records 442 - go c.backfillUser(did) 443 - } 444 - ``` 445 - 446 - This could replace the manual registry entirely, or supplement it for "featured" users. 447 - 448 - --- 449 - 450 - ## Backfill Strategy 451 - 452 - When starting fresh or discovering a new user, we need historical data: 453 - 454 - **Option A: Direct PDS Fetch (Simple)** 455 - 456 - ```go 457 - func (c *Consumer) backfillUser(ctx context.Context, did string) error { 458 - for _, collection := range arabicaCollections { 459 - records, _ := publicClient.ListRecords(ctx, did, collection, 100) 460 - for _, record := range records { 461 - c.index.UpsertFromPDS(record) 462 - } 463 - } 464 - return nil 465 - } 466 - ``` 467 - 468 - **Option B: Slingshot Fetch (Faster)** 469 - 470 - ```go 471 - func (c *Consumer) backfillUserViaSlingshot(ctx context.Context, did string) error { 472 - // Single endpoint, pre-cached records 473 - // Same API as PDS but faster 474 - } 475 - ``` 476 - 477 - **Option C: Jetstream Cursor Rewind (Events Only)** 478 - 479 - - Rewind cursor to desired point in time 480 - - Replay events (no records available before cursor) 481 - - Limited to ~24h of history typically 482 - 483 - **Recommendation:** Use Option A for Phase 1, add Option B in Phase 2. 484 - 485 - --- 486 - 487 - ## Configuration 488 - 489 - ```bash 490 - # Environment variables 491 - 492 - # Enable firehose-based feed (default: false during rollout) 493 - ARABICA_USE_FIREHOSE=true 494 - 495 - # Jetstream endpoint (default: public Bluesky instances) 496 - JETSTREAM_URL=wss://jetstream1.us-east.bsky.network/subscribe 497 - 498 - # Optional: self-hosted Jetstream 499 - # JETSTREAM_URL=ws://localhost:6008/subscribe 500 - 501 - # Feed index database path 502 - ARABICA_FEED_INDEX_PATH=~/.local/share/arabica/feed-index.db 503 - 504 - # Profile cache TTL (default: 1h) 505 - ARABICA_PROFILE_CACHE_TTL=1h 506 - 507 - # Optional: Slingshot endpoint for Phase 2 508 - # SLINGSHOT_URL=https://slingshot.microcosm.blue 509 - 510 - # Optional: Constellation endpoint for Phase 3 511 - # CONSTELLATION_URL=https://constellation.microcosm.blue 512 - ``` 513 - 514 - --- 515 - 516 - ## Monitoring and Metrics 517 - 518 - ```go 519 - // Prometheus metrics to track firehose health 520 - 521 - var ( 522 - eventsReceived = prometheus.NewCounterVec( 523 - prometheus.CounterOpts{ 524 - Name: "arabica_firehose_events_total", 525 - Help: "Total events received from Jetstream", 526 - }, 527 - []string{"collection", "operation"}, 528 - ) 529 - 530 - indexSize = prometheus.NewGauge( 531 - prometheus.GaugeOpts{ 532 - Name: "arabica_feed_index_records", 533 - Help: "Number of records in feed index", 534 - }, 535 - ) 536 - 537 - consumerLag = prometheus.NewGauge( 538 - prometheus.GaugeOpts{ 539 - Name: "arabica_firehose_lag_seconds", 540 - Help: "Lag between event time and processing time", 541 - }, 542 - ) 543 - 544 - connectionState = prometheus.NewGauge( 545 - prometheus.GaugeOpts{ 546 - Name: "arabica_firehose_connected", 547 - Help: "1 if connected to Jetstream, 0 otherwise", 548 - }, 549 - ) 550 - ) 551 - ``` 552 - 553 - --- 554 - 555 - ## Risk Assessment 556 - 557 - | Risk | Mitigation | 558 - | ----------------------- | --------------------------------------------- | 559 - | Jetstream unavailable | Fallback to polling, rotate endpoints | 560 - | Index corruption | Rebuild from backfill, periodic snapshots | 561 - | Duplicate events | Idempotent upserts using AT-URI as key | 562 - | Missing historical data | Backfill on startup and new user discovery | 563 - | High event volume | Filter to Arabica collections only (~0 noise) | 564 - | Profile resolution lag | Local cache with background refresh | 565 - 566 - --- 567 - 568 - ## Open Questions 569 - 570 - 1. **Should we remove the registry entirely?** 571 - - Pro: Simpler, automatic discovery 572 - - Con: Lose ability to curate "featured" users 573 - - Recommendation: Keep registry for admin features, but don't require it for feed inclusion 574 - 575 - 2. **Self-host Jetstream or use public?** 576 - - Public is free and reliable 577 - - Self-host gives control and removes dependency 578 - - Recommendation: Start with public, evaluate self-hosting if issues arise 579 - 580 - 3. **How long to keep historical data?** 581 - - Option: Rolling 30-day window 582 - - Option: Keep everything (disk is cheap) 583 - - Recommendation: Keep 90 days, prune older records 584 - 585 - 4. **Real-time feed updates to frontend?** 586 - - Could push new items via WebSocket/SSE 587 - - Or just reduce cache TTL to ~30 seconds 588 - - Recommendation: Phase 1 just reduces staleness; real-time push is future enhancement 589 - 590 - --- 591 - 592 - ## Alternatives Considered 593 - 594 - ### 1. Tap (Bluesky's Full Sync Tool) 595 - 596 - **Pros:** Full verification, automatic backfill, collection signal mode 597 - **Cons:** Heavy operational overhead, overkill for current scale 598 - **Verdict:** Revisit when user base exceeds 500+ 599 - 600 - ### 2. Direct Firehose Consumption 601 - 602 - **Pros:** No Jetstream dependency 603 - **Cons:** Complex CBOR/CAR parsing, high bandwidth 604 - **Verdict:** Jetstream provides the simplicity we need 605 - 606 - ### 3. Slingshot as Primary Data Source 607 - 608 - **Pros:** Pre-cached records, single endpoint 609 - **Cons:** Still polling-based, no real-time 610 - **Verdict:** Use as optimization layer, not primary 611 - 612 - ### 4. Spacedust Instead of Jetstream 613 - 614 - **Pros:** Link-focused, lightweight 615 - **Cons:** Only links, no full records 616 - **Verdict:** Use for notifications, not feed content 617 - 618 - --- 619 - 620 - ## Success Criteria 621 - 622 - | Metric | Target | 623 - | -------------------------- | ----------------------- | 624 - | Feed latency | <100ms (from >5s) | 625 - | API calls per feed request | 0 (from ~10N) | 626 - | Time to see new content | <5s (from 5min) | 627 - | Feed availability | 99.9% (with fallback) | 628 - | New user discovery | Automatic (from manual) | 629 - 630 - --- 631 - 632 - ## References 633 - 634 - - [Jetstream GitHub](https://github.com/bluesky-social/jetstream) 635 - - [Jetstream Blog Post](https://docs.bsky.app/blog/jetstream) 636 - - [Jetstream Go Client](https://pkg.go.dev/github.com/bluesky-social/jetstream/pkg/client) 637 - - [Microcosm.blue Services](https://microcosm.blue/) 638 - - [Constellation API](https://constellation.microcosm.blue/) 639 - - [Slingshot API](https://slingshot.microcosm.blue/) 640 - - [Existing Evaluation: Jetstream/Tap](./jetstream-tap-evaluation.md) 641 - - [Existing Evaluation: Microcosm Tools](./microcosm-tools-evaluation.md)
-16
docs/future-witness-cache.md
··· 1 - # Witness Cache 2 - 3 - Paul Frazee: 4 - 5 - I'm increasingly convinced that many Atmosphere backends start with a local "witness cache" of the repositories. 6 - A witness cache is a copy of the repository records, plus a timestamp of when the record was indexed (the "witness time") which you want to keep 7 - 8 - The key feature is: you can replay it 9 - 10 - With local replay, you can add new tables or indexes to your backend and quickly backfill the data. If you don't have a witness cache, you would have to do backfill from the network, which is slow 11 - 12 - RocksDB or other LSMs are good candidates for a witness cache (good write throughput) 13 - 14 - Clickhouse and DuckDB are also good candidates (good compression ratio) 15 - 16 - ## TODO
-314
docs/jetstream-tap-evaluation.md
··· 1 - # Jetstream and Tap Evaluation for Arabica 2 - 3 - ## Executive Summary 4 - 5 - This document evaluates two AT Protocol synchronization tools - **Jetstream** and **Tap** - for potential integration with Arabica. These tools could help reduce API requests for the community feed feature and simplify real-time data synchronization. 6 - 7 - **Recommendation:** Consider Jetstream for community feed improvements in the near term; Tap is overkill for Arabica's current scale but valuable for future growth. 8 - 9 - --- 10 - 11 - ## Background: Current Arabica Architecture 12 - 13 - Arabica currently interacts with AT Protocol in two ways: 14 - 15 - 1. **Authenticated User Operations** (`internal/atproto/store.go`) 16 - - Direct XRPC calls to user's PDS for CRUD operations 17 - - Per-session in-memory cache (5-minute TTL) 18 - - Each user's data stored in their own PDS 19 - 20 - 2. **Community Feed** (`internal/feed/service.go`) 21 - - Polls registered users' PDSes to aggregate recent activity 22 - - Fetches profiles, brews, beans, roasters, grinders, brewers from each user 23 - - Public feed cached for 5 minutes 24 - - **Problem:** N+1 query pattern - each registered user requires multiple API calls 25 - 26 - ### Current Feed Inefficiency 27 - 28 - For N registered users, the feed service makes approximately: 29 - - N profile fetches 30 - - N x 5 collection fetches (brew, bean, roaster, grinder, brewer) for recent items 31 - - N x 4 collection fetches for reference resolution 32 - - **Total: ~10N API calls per feed refresh** 33 - 34 - --- 35 - 36 - ## Tool 1: Jetstream 37 - 38 - ### What It Is 39 - 40 - Jetstream is a streaming service that consumes the AT Protocol firehose (`com.atproto.sync.subscribeRepos`) and converts it into lightweight JSON events. It's operated by Bluesky at public endpoints. 41 - 42 - **Public Instances:** 43 - - `jetstream1.us-east.bsky.network` 44 - - `jetstream2.us-east.bsky.network` 45 - - `jetstream1.us-west.bsky.network` 46 - - `jetstream2.us-west.bsky.network` 47 - 48 - ### Key Features 49 - 50 - | Feature | Description | 51 - |---------|-------------| 52 - | JSON Output | Simple JSON instead of CBOR/CAR binary encoding | 53 - | Filtering | Filter by collection (NSID) or repo (DID) | 54 - | Compression | ~56% smaller messages with zstd compression | 55 - | Low Latency | Real-time event delivery | 56 - | Easy to Use | Standard WebSocket connection | 57 - 58 - ### Jetstream Event Example 59 - 60 - ```json 61 - { 62 - "did": "did:plc:eygmaihciaxprqvxpfvl6flk", 63 - "time_us": 1725911162329308, 64 - "kind": "commit", 65 - "commit": { 66 - "rev": "3l3qo2vutsw2b", 67 - "operation": "create", 68 - "collection": "social.arabica.alpha.brew", 69 - "rkey": "3l3qo2vuowo2b", 70 - "record": { 71 - "$type": "social.arabica.alpha.brew", 72 - "method": "pourover", 73 - "rating": 4, 74 - "createdAt": "2024-09-09T19:46:02.102Z" 75 - }, 76 - "cid": "bafyreidwaivazkwu67xztlmuobx35hs2lnfh3kolmgfmucldvhd3sgzcqi" 77 - } 78 - } 79 - ``` 80 - 81 - ### How Arabica Could Use Jetstream 82 - 83 - **Use Case: Real-time Community Feed** 84 - 85 - Instead of polling each user's PDS every 5 minutes, Arabica could: 86 - 87 - 1. Subscribe to Jetstream filtered by: 88 - - `wantedCollections`: `social.arabica.alpha.*` 89 - - `wantedDids`: List of registered feed users 90 - 91 - 2. Maintain a local feed index updated in real-time 92 - 93 - 3. Serve feed directly from local index (instant response, no API calls) 94 - 95 - **Implementation Sketch:** 96 - 97 - ```go 98 - // Subscribe to Jetstream for Arabica collections 99 - ws, _ := websocket.Dial("wss://jetstream1.us-east.bsky.network/subscribe?" + 100 - "wantedCollections=social.arabica.alpha.brew&" + 101 - "wantedCollections=social.arabica.alpha.bean&" + 102 - "wantedDids=" + strings.Join(registeredDids, "&wantedDids=")) 103 - 104 - // Process events in background goroutine 105 - for { 106 - var event JetstreamEvent 107 - ws.ReadJSON(&event) 108 - 109 - switch event.Commit.Collection { 110 - case "social.arabica.alpha.brew": 111 - feedIndex.AddBrew(event.DID, event.Commit.Record) 112 - case "social.arabica.alpha.bean": 113 - feedIndex.AddBean(event.DID, event.Commit.Record) 114 - } 115 - } 116 - ``` 117 - 118 - ### Jetstream Tradeoffs 119 - 120 - | Pros | Cons | 121 - |------|------| 122 - | Dramatically reduces API calls | No cryptographic verification of data | 123 - | Real-time updates (sub-second latency) | Requires persistent WebSocket connection | 124 - | Simple JSON format | Trust relationship with Jetstream operator | 125 - | Can filter by collection/DID | Not part of formal AT Protocol spec | 126 - | Free public instances available | No built-in backfill mechanism | 127 - 128 - ### Jetstream Verdict for Arabica 129 - 130 - **Recommended for:** Community feed real-time updates 131 - 132 - **Not suitable for:** Authenticated user operations (those need direct PDS calls) 133 - 134 - **Effort estimate:** Medium (1-2 weeks) 135 - - Add WebSocket client for Jetstream 136 - - Build local feed index (could use BoltDB or in-memory) 137 - - Handle reconnection/cursor management 138 - - Still need initial backfill via direct API 139 - 140 - --- 141 - 142 - ## Tool 2: Tap 143 - 144 - ### What It Is 145 - 146 - Tap is a synchronization tool for AT Protocol that handles the complexity of repo synchronization. It subscribes to a Relay and outputs filtered, verified events. Tap is more comprehensive than Jetstream but requires running your own instance. 147 - 148 - **Repository:** `github.com/bluesky-social/indigo/cmd/tap` 149 - 150 - ### Key Features 151 - 152 - | Feature | Description | 153 - |---------|-------------| 154 - | Automatic Backfill | Fetches complete history when tracking new repos | 155 - | Verification | MST integrity checks, signature validation | 156 - | Recovery | Auto-resyncs if repo becomes desynchronized | 157 - | Flexible Delivery | WebSocket, fire-and-forget, or webhooks | 158 - | Filtered Output | DID and collection filtering | 159 - 160 - ### Tap Operating Modes 161 - 162 - 1. **Dynamic (default):** Add DIDs via API as needed 163 - 2. **Collection Signal:** Auto-track repos with records in specified collection 164 - 3. **Full Network:** Mirror entire AT Protocol network (resource-intensive) 165 - 166 - ### How Arabica Could Use Tap 167 - 168 - **Use Case: Complete Feed Infrastructure** 169 - 170 - Tap could replace the entire feed polling mechanism: 171 - 172 - 1. Run Tap instance with `TAP_SIGNAL_COLLECTION=social.arabica.alpha.brew` 173 - 2. Tap automatically discovers and tracks users who create brew records 174 - 3. Feed service consumes events from local Tap instance 175 - 4. No manual user registration needed - Tap discovers users automatically 176 - 177 - **Collection Signal Mode:** 178 - 179 - ```bash 180 - # Start Tap to auto-track repos with Arabica records 181 - TAP_SIGNAL_COLLECTION=social.arabica.alpha.brew \ 182 - go run ./cmd/tap --disable-acks=true 183 - ``` 184 - 185 - **Webhook Delivery (Serverless-friendly):** 186 - 187 - Tap can POST events to an HTTP endpoint, making it compatible with serverless architectures: 188 - 189 - ```bash 190 - # Tap sends events to Arabica webhook 191 - TAP_WEBHOOK_URL=https://arabica.example/api/feed-webhook \ 192 - go run ./cmd/tap 193 - ``` 194 - 195 - ### Tap Tradeoffs 196 - 197 - | Pros | Cons | 198 - |------|------| 199 - | Automatic backfill when adding repos | Requires running your own service | 200 - | Full cryptographic verification | More operational complexity | 201 - | Handles cursor management | Resource requirements (DB, network) | 202 - | Auto-discovers users via collection signal | Overkill for small user bases | 203 - | Webhook support for serverless | Still in beta | 204 - 205 - ### Tap Verdict for Arabica 206 - 207 - **Recommended for:** Future growth when feed has many users 208 - 209 - **Not suitable for:** Current scale (< 100 registered users) 210 - 211 - **Effort estimate:** High (2-4 weeks) 212 - - Deploy and operate Tap service 213 - - Integrate webhook or WebSocket consumer 214 - - Migrate feed service to consume from Tap 215 - - Handle Tap service reliability/monitoring 216 - 217 - --- 218 - 219 - ## Comparison Matrix 220 - 221 - | Aspect | Current Polling | Jetstream | Tap | 222 - |--------|----------------|-----------|-----| 223 - | API Calls per Refresh | ~10N | 0 (after connection) | 0 (after backfill) | 224 - | Latency | 5 min cache | Real-time | Real-time | 225 - | Backfill | Full fetch each time | Manual | Automatic | 226 - | Verification | Trusts PDS | Trusts Jetstream | Full verification | 227 - | Operational Cost | None | None (public) | Run own service | 228 - | Complexity | Low | Medium | High | 229 - | User Discovery | Manual registry | Manual | Auto via collection | 230 - | Recommended Scale | < 50 users | 50-1000 users | 1000+ users | 231 - 232 - --- 233 - 234 - ## Recommendation 235 - 236 - ### Short Term (Now - 6 months) 237 - 238 - **Stick with current polling + caching approach** 239 - 240 - Rationale: 241 - - Current implementation works 242 - - User base is small 243 - - Polling N users with caching is acceptable 244 - 245 - **Consider adding Jetstream for feed** if: 246 - - Feed latency becomes user-visible issue 247 - - Registered users exceed ~50 248 - - API rate limiting becomes a problem 249 - 250 - ### Medium Term (6-12 months) 251 - 252 - **Implement Jetstream integration** 253 - 254 - 1. Add background Jetstream consumer 255 - 2. Build local feed index (BoltDB or SQLite) 256 - 3. Serve feed from local index 257 - 4. Keep polling as fallback for backfill 258 - 259 - ### Long Term (12+ months) 260 - 261 - **Evaluate Tap when:** 262 - - User base exceeds 500+ registered users 263 - - Want automatic user discovery 264 - - Need cryptographic verification for social features (likes, comments) 265 - - Building moderation/anti-abuse features 266 - 267 - --- 268 - 269 - ## Implementation Notes 270 - 271 - ### Jetstream Client Library 272 - 273 - Bluesky provides a Go client library: 274 - 275 - ```go 276 - import "github.com/bluesky-social/jetstream/pkg/client" 277 - ``` 278 - 279 - ### Tap TypeScript Library 280 - 281 - For frontend integration: 282 - 283 - ```typescript 284 - import { TapClient } from '@atproto/tap'; 285 - ``` 286 - 287 - ### Connection Resilience 288 - 289 - Both tools require handling: 290 - - WebSocket reconnection 291 - - Cursor persistence across restarts 292 - - Backpressure when events arrive faster than processing 293 - 294 - ### Caching Integration 295 - 296 - Can coexist with current `SessionCache`: 297 - - Jetstream/Tap updates the local index 298 - - Local index serves feed requests 299 - - SessionCache continues for authenticated user operations 300 - 301 - --- 302 - 303 - ## Related Documentation 304 - 305 - - Jetstream GitHub: https://github.com/bluesky-social/jetstream 306 - - Tap README: https://github.com/bluesky-social/indigo/blob/main/cmd/tap/README.md 307 - - Jetstream Blog Post: https://docs.bsky.app/blog/jetstream 308 - - Tap Blog Post: https://docs.bsky.app/blog/introducing-tap 309 - 310 - --- 311 - 312 - ## Note on "Constellation" and "Slingshot" 313 - 314 - These terms don't appear to correspond to official AT Protocol tools as of this evaluation. If these refer to specific community projects or internal codenames, please provide additional context for evaluation.
-1089
docs/plans/2026-03-24-clean-craft-ui-overhaul.md
··· 1 - # Clean Craft UI Overhaul Implementation Plan 2 - 3 - > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 - 5 - **Goal:** Overhaul Arabica's visual design from gradient-heavy brown-on-brown to clean white cards on warm cream, with CSS custom properties for future dark mode support. 6 - 7 - **Architecture:** The overhaul is split into two layers: (1) CSS-only changes that redefine existing component classes — this covers ~80% of the visual change with zero template edits, and (2) targeted template edits for structural changes like removing nested content boxes and adding type indicators. CSS custom properties are introduced from the start so dark mode is a color swap later, not a rewrite. 8 - 9 - **Tech Stack:** Tailwind CSS (config + `@apply` in app.css), Templ templates, no new dependencies. 10 - 11 - **Key files:** 12 - - `static/css/app.css` — all component class definitions 13 - - `tailwind.config.js` — color palette and theme 14 - - `internal/web/components/layout.templ` — body background, CSS version 15 - - `internal/web/components/header.templ` — nav bar 16 - - `internal/web/components/footer.templ` — footer 17 - - `internal/web/components/shared.templ` — shared components (WelcomeCard, PageHeader, etc.) 18 - - `internal/web/pages/feed.templ` — feed card structure 19 - - `internal/web/components/record_*.templ` — feed content boxes (brew, bean, roaster, grinder, brewer) 20 - - `internal/web/components/entity_tables.templ` — entity list cards 21 - - `internal/web/components/action_bar.templ` — feed card wrappers 22 - - `internal/web/components/profile_brew_card.templ` — profile brew cards 23 - 24 - --- 25 - 26 - ## Task 1: CSS Custom Properties Foundation 27 - 28 - Introduce CSS custom properties for all semantic colors so that component classes reference tokens instead of hardcoded Tailwind values. This is the dark-mode foundation — changing these variables later switches the entire theme. 29 - 30 - **Files:** 31 - - Modify: `static/css/app.css` (add `:root` block at top, before `@tailwind` directives) 32 - - Modify: `tailwind.config.js` (add `cream` color) 33 - 34 - **Step 1: Add CSS custom properties to app.css** 35 - 36 - Add this block at the very top of `static/css/app.css`, before the `@tailwind` directives but after the `@font-face` declarations: 37 - 38 - ```css 39 - /* ======================================== 40 - Design Tokens (CSS Custom Properties) 41 - Light theme (default) 42 - ======================================== */ 43 - :root { 44 - /* Page */ 45 - --page-bg: #FAF7F5; 46 - --page-text: #3d2319; 47 - 48 - /* Cards */ 49 - --card-bg: #FFFFFF; 50 - --card-border: #eaddd7; 51 - --card-shadow: rgba(61, 35, 25, 0.06); 52 - --card-shadow-hover: rgba(61, 35, 25, 0.10); 53 - 54 - /* Surfaces (inset areas inside cards) */ 55 - --surface-bg: rgba(250, 247, 245, 0.5); 56 - --surface-border: #f2e8e5; 57 - 58 - /* Header */ 59 - --header-bg-from: #4a2c2a; 60 - --header-bg-to: #3d2319; 61 - --header-border: #7f5539; 62 - --header-text: #FAF7F5; 63 - 64 - /* Text hierarchy */ 65 - --text-primary: #3d2319; 66 - --text-secondary: #4a2c2a; 67 - --text-muted: #7f5539; 68 - --text-faint: #bfa094; 69 - --text-placeholder: #d2bab0; 70 - 71 - /* Interactive */ 72 - --btn-primary-bg: #4a2c2a; 73 - --btn-primary-bg-hover: #3d2319; 74 - --btn-primary-text: #FAF7F5; 75 - --btn-secondary-bg: #FFFFFF; 76 - --btn-secondary-border: #e0cec7; 77 - --btn-secondary-text: #6b4423; 78 - --btn-secondary-bg-hover: #FAF7F5; 79 - 80 - /* Forms */ 81 - --input-bg: #FFFFFF; 82 - --input-border: #e0cec7; 83 - --input-border-focus: #7f5539; 84 - --input-ring-focus: rgba(127, 85, 57, 0.15); 85 - --input-bg-focus: rgba(250, 247, 245, 0.3); 86 - 87 - /* Tables */ 88 - --table-bg: #FFFFFF; 89 - --table-header-bg: #FAF7F5; 90 - --table-border: #eaddd7; 91 - --table-row-hover: #FAF7F5; 92 - --table-divider: #f2e8e5; 93 - 94 - /* Modals */ 95 - --modal-bg: #FFFFFF; 96 - --modal-border: #eaddd7; 97 - --modal-backdrop: rgba(0, 0, 0, 0.4); 98 - 99 - /* Feed type indicators (left border) */ 100 - --type-brew: #6b4423; 101 - --type-bean: #d97706; 102 - --type-recipe: #bfa094; 103 - --type-roaster: #d2bab0; 104 - --type-grinder: #d2bab0; 105 - --type-brewer: #d2bab0; 106 - 107 - /* Shadows */ 108 - --shadow-sm: 0 1px 3px var(--card-shadow); 109 - --shadow-md: 0 4px 12px var(--card-shadow-hover); 110 - --shadow-lg: 0 10px 25px var(--card-shadow-hover); 111 - 112 - /* Footer */ 113 - --footer-bg: #FAF7F5; 114 - --footer-border: #eaddd7; 115 - } 116 - ``` 117 - 118 - **Step 2: Add `cream` color to tailwind.config.js** 119 - 120 - Add a `cream` color to the colors object in `tailwind.config.js`: 121 - 122 - ```js 123 - cream: { 124 - 50: "#FAF7F5", 125 - }, 126 - ``` 127 - 128 - This lets templates use `bg-cream-50` for the page background if needed. 129 - 130 - **Step 3: Verify build** 131 - 132 - Run: `just style && go vet ./...` 133 - Expected: Clean build, no errors. No visual changes yet (properties defined but not consumed). 134 - 135 - **Step 4: Commit** 136 - 137 - ```bash 138 - git add static/css/app.css tailwind.config.js 139 - git commit -m "feat: add CSS custom properties foundation for theme support" 140 - ``` 141 - 142 - --- 143 - 144 - ## Task 2: Redefine Core Component Classes 145 - 146 - Rewrite the component class definitions in `app.css` to use the CSS custom properties and implement the Clean Craft visual style. This single file change transforms the entire app's appearance. 147 - 148 - **Files:** 149 - - Modify: `static/css/app.css` (rewrite `@layer components` block) 150 - 151 - **Step 1: Rewrite card classes** 152 - 153 - Replace the existing card definitions: 154 - 155 - ```css 156 - /* Cards and Containers */ 157 - .card { 158 - background: var(--card-bg); 159 - border: 1px solid var(--card-border); 160 - @apply rounded-xl; 161 - box-shadow: var(--shadow-sm); 162 - transition: box-shadow 200ms ease; 163 - } 164 - 165 - .card:hover { 166 - box-shadow: var(--shadow-md); 167 - } 168 - 169 - .card-inner { 170 - @apply p-6; 171 - } 172 - 173 - .card-sm { 174 - background: var(--card-bg); 175 - border: 1px solid var(--card-border); 176 - @apply rounded-lg; 177 - box-shadow: var(--shadow-sm); 178 - } 179 - 180 - /* Section box for lighter content areas */ 181 - .section-box { 182 - background: var(--surface-bg); 183 - @apply rounded-lg p-4; 184 - } 185 - ``` 186 - 187 - **Step 2: Rewrite button classes** 188 - 189 - ```css 190 - /* Buttons */ 191 - .btn { 192 - @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer; 193 - } 194 - 195 - .btn-primary { 196 - @apply btn text-white; 197 - background: var(--btn-primary-bg); 198 - } 199 - 200 - .btn-primary:hover { 201 - background: var(--btn-primary-bg-hover); 202 - } 203 - 204 - .btn-secondary { 205 - @apply btn; 206 - background: var(--btn-secondary-bg); 207 - color: var(--btn-secondary-text); 208 - border: 1px solid var(--btn-secondary-border); 209 - } 210 - 211 - .btn-secondary:hover { 212 - background: var(--btn-secondary-bg-hover); 213 - } 214 - 215 - .btn-tertiary { 216 - @apply btn text-white; 217 - background: var(--btn-primary-bg); 218 - } 219 - 220 - .btn-tertiary:hover { 221 - background: var(--btn-primary-bg-hover); 222 - } 223 - 224 - .btn-link { 225 - color: var(--text-muted); 226 - @apply font-medium underline transition-colors cursor-pointer; 227 - } 228 - 229 - .btn-link:hover { 230 - color: var(--text-primary); 231 - } 232 - 233 - .btn-danger { 234 - @apply text-red-600 hover:text-red-800 font-medium underline transition-colors cursor-pointer; 235 - } 236 - ``` 237 - 238 - Note: `.btn-tertiary` is redefined to match `.btn-primary` (no more gradient). It's used in 1 place (`shared.templ:206`). We keep the class to avoid template churn but visually unify it. 239 - 240 - **Step 3: Rewrite form classes** 241 - 242 - ```css 243 - /* Forms */ 244 - .form-label { 245 - @apply block text-sm font-medium mb-2; 246 - color: var(--text-primary); 247 - } 248 - 249 - .form-input { 250 - @apply rounded-lg shadow-sm text-base py-2 px-3; 251 - background: var(--input-bg); 252 - border: 1px solid var(--input-border); 253 - color: var(--text-primary); 254 - transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease; 255 - } 256 - 257 - .form-input:focus { 258 - border-color: var(--input-border-focus); 259 - box-shadow: 0 0 0 2px var(--input-ring-focus); 260 - background: var(--input-bg-focus); 261 - outline: none; 262 - } 263 - 264 - .form-input::placeholder { 265 - color: var(--text-placeholder); 266 - } 267 - 268 - .form-input-lg { 269 - @apply form-input py-3 px-4; 270 - } 271 - 272 - .form-select { 273 - @apply form-input truncate max-w-full min-w-0; 274 - } 275 - 276 - .form-textarea { 277 - @apply form-input min-h-[100px]; 278 - } 279 - ``` 280 - 281 - **Step 4: Rewrite table classes** 282 - 283 - ```css 284 - /* Tables */ 285 - .table-container { 286 - background: var(--table-bg); 287 - border: 1px solid var(--table-border); 288 - @apply rounded-lg overflow-hidden; 289 - box-shadow: var(--shadow-sm); 290 - } 291 - 292 - .table { 293 - @apply min-w-full; 294 - border-collapse: collapse; 295 - } 296 - 297 - .table-header { 298 - background: var(--table-header-bg); 299 - border-bottom: 1px solid var(--table-border); 300 - } 301 - 302 - .table-th { 303 - @apply px-6 py-3 text-left text-xs font-medium uppercase tracking-wider; 304 - color: var(--text-muted); 305 - } 306 - 307 - .table-body { 308 - background: var(--table-bg); 309 - } 310 - 311 - .table-body tr { 312 - border-bottom: 1px solid var(--table-divider); 313 - } 314 - 315 - .table-body tr:last-child { 316 - border-bottom: none; 317 - } 318 - 319 - .table-row { 320 - transition: background-color 150ms ease; 321 - } 322 - 323 - .table-row:hover { 324 - background: var(--table-row-hover); 325 - } 326 - 327 - .table-td { 328 - @apply px-6 py-4 whitespace-nowrap text-sm; 329 - color: var(--text-secondary); 330 - } 331 - ``` 332 - 333 - **Step 5: Rewrite modal classes** 334 - 335 - ```css 336 - /* Modals */ 337 - .modal-backdrop { 338 - @apply fixed inset-0 flex items-center justify-center z-50 p-4; 339 - background: var(--modal-backdrop); 340 - backdrop-filter: blur(4px); 341 - } 342 - 343 - .modal-content { 344 - background: var(--modal-bg); 345 - border: 1px solid var(--modal-border); 346 - @apply rounded-xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto; 347 - box-shadow: var(--shadow-lg); 348 - } 349 - 350 - .modal-title { 351 - @apply text-xl font-semibold mb-4; 352 - color: var(--text-primary); 353 - } 354 - 355 - /* Native Dialog Element */ 356 - .modal-dialog { 357 - @apply p-0 bg-transparent border-none shadow-none max-w-md w-full; 358 - } 359 - 360 - .modal-dialog::backdrop { 361 - background: var(--modal-backdrop); 362 - backdrop-filter: blur(4px); 363 - } 364 - 365 - /* Dialog content wrapper (nested inside dialog) */ 366 - .modal-dialog .modal-content { 367 - background: var(--modal-bg); 368 - border: 1px solid var(--modal-border); 369 - @apply rounded-xl p-6 w-full max-h-[90vh] overflow-y-auto; 370 - box-shadow: var(--shadow-lg); 371 - } 372 - ``` 373 - 374 - **Step 6: Rewrite feed component classes** 375 - 376 - ```css 377 - /* Feed Components */ 378 - .feed-card { 379 - background: var(--card-bg); 380 - border: 1px solid var(--card-border); 381 - @apply rounded-lg p-3 sm:p-4 transition-shadow; 382 - box-shadow: var(--shadow-sm); 383 - } 384 - 385 - .feed-card:hover { 386 - box-shadow: var(--shadow-md); 387 - } 388 - 389 - .feed-content-box { 390 - background: var(--surface-bg); 391 - @apply rounded-lg p-3 sm:p-4; 392 - } 393 - 394 - .feed-content-box-sm { 395 - background: var(--surface-bg); 396 - @apply rounded-lg p-2 sm:p-3; 397 - } 398 - ``` 399 - 400 - Note: We keep `.feed-content-box` and `.feed-content-box-sm` but restyle them as subtle surface tints (no border, no backdrop-blur). This way existing templates work immediately. Task 4 removes the wrapper elements from templates where possible. 401 - 402 - **Step 7: Rewrite remaining component classes** 403 - 404 - Update avatar, text utility, badge, link, action, dropdown, and comment classes. The key changes are: 405 - 406 - - Avatar rings: keep as-is (they're fine) 407 - - Text utilities: reference CSS variables 408 - - Badges: keep as-is (amber accent works) 409 - - Links: reference CSS variables 410 - - Action buttons: remove brown-100 background, use transparent with hover 411 - - Dropdowns: use card-bg variable 412 - - Comments: reference variables for borders/backgrounds 413 - 414 - For text utilities: 415 - ```css 416 - /* Text Utilities */ 417 - .text-helper { 418 - @apply text-sm mt-1; 419 - color: var(--text-muted); 420 - } 421 - 422 - .text-meta { 423 - @apply text-xs; 424 - color: var(--text-muted); 425 - } 426 - 427 - .text-meta-sm { 428 - @apply text-sm; 429 - color: var(--text-muted); 430 - } 431 - 432 - .text-label { 433 - color: var(--text-muted); 434 - } 435 - ``` 436 - 437 - For action buttons and bars: 438 - ```css 439 - /* Action Bar */ 440 - .action-bar { 441 - @apply flex items-center gap-2 mt-3 pt-3; 442 - border-top: 1px solid var(--surface-border); 443 - } 444 - 445 - .brew-view-actions .action-bar { 446 - @apply mt-0 pt-0 border-t-0; 447 - } 448 - 449 - .comment-item .action-bar { 450 - @apply mt-1 border-t-0 gap-1 rounded-lg px-1.5 py-1 inline-flex items-center; 451 - background: var(--surface-bg); 452 - } 453 - 454 - .action-btn { 455 - @apply inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer min-h-[44px]; 456 - color: var(--text-muted); 457 - background: transparent; 458 - } 459 - 460 - .action-btn:hover { 461 - background: var(--surface-bg); 462 - color: var(--text-secondary); 463 - } 464 - ``` 465 - 466 - For dropdowns: 467 - ```css 468 - .action-menu { 469 - @apply absolute left-1/2 -translate-x-1/2 w-36 rounded-lg py-1 z-50; 470 - background: var(--card-bg); 471 - border: 1px solid var(--card-border); 472 - box-shadow: var(--shadow-md); 473 - } 474 - 475 - .dropdown-menu { 476 - @apply absolute right-0 mt-2 w-48 rounded-lg py-1 z-50; 477 - background: var(--card-bg); 478 - border: 1px solid var(--card-border); 479 - box-shadow: var(--shadow-md); 480 - } 481 - 482 - .dropdown-item { 483 - @apply block px-4 py-2 text-sm transition-colors; 484 - color: var(--text-muted); 485 - } 486 - 487 - .dropdown-item:hover { 488 - background: var(--surface-bg); 489 - } 490 - ``` 491 - 492 - For like/share/comment buttons, suggestions: 493 - ```css 494 - .like-btn { 495 - @apply inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-sm font-medium transition-colors min-h-[44px]; 496 - } 497 - 498 - .like-btn-liked { 499 - @apply like-btn text-red-600; 500 - background: transparent; 501 - animation: like-pop 400ms ease-out; 502 - } 503 - 504 - .like-btn-liked:hover { 505 - background: var(--surface-bg); 506 - } 507 - 508 - .like-btn-unliked { 509 - @apply like-btn; 510 - color: var(--text-muted); 511 - background: transparent; 512 - animation: like-shrink 200ms ease-out; 513 - } 514 - 515 - .like-btn-unliked:hover { 516 - background: var(--surface-bg); 517 - } 518 - 519 - .share-btn { 520 - @apply inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-sm font-medium transition-colors min-h-[44px]; 521 - color: var(--text-muted); 522 - background: transparent; 523 - } 524 - 525 - .share-btn:hover { 526 - background: var(--surface-bg); 527 - } 528 - 529 - .comment-btn { 530 - @apply inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-sm font-medium transition-colors min-h-[44px]; 531 - color: var(--text-muted); 532 - background: transparent; 533 - } 534 - 535 - .comment-btn:hover { 536 - background: var(--surface-bg); 537 - } 538 - ``` 539 - 540 - For suggestions dropdown: 541 - ```css 542 - .suggestions-dropdown { 543 - @apply absolute z-50 left-0 right-0 mt-1 rounded-lg max-h-48 overflow-y-auto; 544 - background: var(--card-bg); 545 - border: 1px solid var(--card-border); 546 - box-shadow: var(--shadow-md); 547 - } 548 - 549 - .suggestions-item { 550 - @apply w-full text-left px-3 py-2 flex items-center gap-2 transition-colors cursor-pointer last:border-b-0; 551 - border-bottom: 1px solid var(--surface-border); 552 - } 553 - 554 - .suggestions-item:hover { 555 - background: var(--surface-bg); 556 - } 557 - ``` 558 - 559 - For comments: 560 - ```css 561 - .comment-section { 562 - @apply mt-8 pt-6; 563 - border-top: 2px solid var(--card-border); 564 - } 565 - 566 - .comment-login-prompt { 567 - @apply flex items-center gap-3 rounded-lg p-4 mb-5 border border-dashed; 568 - background: var(--surface-bg); 569 - border-color: var(--card-border); 570 - } 571 - 572 - .comment-compose { 573 - @apply rounded-lg p-4 mb-5 flex flex-col gap-2; 574 - background: var(--surface-bg); 575 - border: 1px solid var(--card-border); 576 - } 577 - 578 - .comment-textarea { 579 - @apply w-full rounded-lg px-3 py-2.5 text-base resize-none transition-colors focus:ring-0 focus:outline-none; 580 - background: var(--card-bg); 581 - border: 1px solid var(--card-border); 582 - color: var(--text-primary); 583 - } 584 - 585 - .comment-textarea::placeholder { 586 - color: var(--text-placeholder); 587 - } 588 - 589 - .comment-textarea:focus { 590 - border-color: var(--input-border-focus); 591 - } 592 - 593 - .comment-item { 594 - @apply relative rounded-lg p-3 transition-colors; 595 - } 596 - 597 - .comment-item:hover { 598 - background: var(--surface-bg); 599 - } 600 - 601 - .comment-thread-line { 602 - @apply absolute left-0 top-3 bottom-3 w-0.5 rounded-full; 603 - background: var(--card-border); 604 - } 605 - 606 - .comment-reply-btn { 607 - @apply inline-flex items-center gap-1 transition-colors text-xs font-medium; 608 - color: var(--text-placeholder); 609 - } 610 - 611 - .comment-reply-btn:hover { 612 - color: var(--text-muted); 613 - } 614 - 615 - .comment-delete-btn { 616 - @apply transition-colors; 617 - color: var(--text-placeholder); 618 - } 619 - 620 - .comment-delete-btn:hover { 621 - color: var(--text-muted); 622 - } 623 - 624 - .comment-reply-form { 625 - @apply flex flex-col gap-2 rounded-lg p-3; 626 - background: var(--surface-bg); 627 - border: 1px solid var(--card-border); 628 - } 629 - ``` 630 - 631 - **Step 8: Rebuild CSS and verify build** 632 - 633 - Run: `just style && go vet ./... && go build ./...` 634 - Expected: Clean build. 635 - 636 - **Step 9: Commit** 637 - 638 - ```bash 639 - git add static/css/app.css 640 - git commit -m "feat: redefine component classes with Clean Craft styling and CSS variables" 641 - ``` 642 - 643 - --- 644 - 645 - ## Task 3: Update Layout, Header, and Footer 646 - 647 - Update the structural templates to use the new color system. 648 - 649 - **Files:** 650 - - Modify: `internal/web/components/layout.templ` 651 - - Modify: `internal/web/components/header.templ` 652 - - Modify: `internal/web/components/footer.templ` 653 - 654 - **Step 1: Update layout.templ** 655 - 656 - Change the `<html>` tag's inline background: 657 - ``` 658 - style="background-color: #fdf8f6;" → style="background-color: #FAF7F5;" 659 - ``` 660 - 661 - Change the `<body>` tag: 662 - ``` 663 - class="bg-brown-50 min-h-full flex flex-col" 664 - style="background-color: #fdf8f6;" 665 - ``` 666 - to: 667 - ``` 668 - class="min-h-full flex flex-col" 669 - style="background-color: var(--page-bg); color: var(--page-text);" 670 - ``` 671 - 672 - Bump the CSS version: 673 - ``` 674 - output.css?v=0.6.1 → output.css?v=0.7.0 675 - ``` 676 - 677 - **Step 2: Update header.templ** 678 - 679 - Change the nav element from: 680 - ``` 681 - class="sticky top-0 z-50 bg-gradient-to-br from-brown-800 to-brown-900 text-white shadow-xl border-b-2 border-brown-600" 682 - ``` 683 - to: 684 - ``` 685 - class="sticky top-0 z-50 text-white" 686 - style="background: linear-gradient(135deg, var(--header-bg-from), var(--header-bg-to)); border-bottom: 1px solid var(--header-border);" 687 - ``` 688 - 689 - Remove `shadow-xl` from the nav — the border provides sufficient separation. Add `box-shadow: var(--shadow-sm);` to the style attribute if a subtle shadow is wanted. 690 - 691 - Reduce padding in the container div: 692 - ``` 693 - class="container mx-auto px-4 py-4" → class="container mx-auto px-4 py-3" 694 - ``` 695 - 696 - Make the ALPHA badge smaller: 697 - ``` 698 - class="text-xs bg-amber-400 text-brown-900 px-2 py-1 rounded-md font-semibold shadow-sm" 699 - ``` 700 - to: 701 - ``` 702 - class="text-[10px] bg-amber-400 text-brown-900 px-1.5 py-0.5 rounded font-semibold" 703 - ``` 704 - 705 - **Step 3: Update footer.templ** 706 - 707 - Change the footer from: 708 - ``` 709 - class="mt-auto border-t border-brown-200 bg-brown-50" 710 - ``` 711 - to: 712 - ``` 713 - class="mt-auto" 714 - style="background: var(--footer-bg); border-top: 1px solid var(--footer-border);" 715 - ``` 716 - 717 - **Step 4: Regenerate templ, rebuild CSS, verify** 718 - 719 - Run: `templ generate && just style && go vet ./... && go build ./...` 720 - Expected: Clean build. 721 - 722 - **Step 5: Commit** 723 - 724 - ```bash 725 - git add internal/web/components/layout.templ internal/web/components/header.templ internal/web/components/footer.templ 726 - git commit -m "feat: update layout, header, footer for Clean Craft theme" 727 - ``` 728 - 729 - --- 730 - 731 - ## Task 4: Add Feed Card Type Indicators 732 - 733 - Add colored left borders to feed cards to distinguish record types (brew, bean, recipe, etc.) at a glance. 734 - 735 - **Files:** 736 - - Modify: `static/css/app.css` (add type indicator classes) 737 - - Modify: `internal/web/components/action_bar.templ` (add type class to feed card wrapper) 738 - - Modify: `internal/web/pages/feed.templ` (add type class where feed cards are rendered) 739 - 740 - **Step 1: Add type indicator CSS classes** 741 - 742 - Add to `app.css` after the `.feed-card` definition: 743 - 744 - ```css 745 - /* Feed card type indicators */ 746 - .feed-card-brew { 747 - border-left: 3px solid var(--type-brew); 748 - } 749 - 750 - .feed-card-bean { 751 - border-left: 3px solid var(--type-bean); 752 - } 753 - 754 - .feed-card-recipe { 755 - border-left: 3px solid var(--type-recipe); 756 - } 757 - 758 - .feed-card-roaster { 759 - border-left: 3px solid var(--type-roaster); 760 - } 761 - 762 - .feed-card-grinder { 763 - border-left: 3px solid var(--type-grinder); 764 - } 765 - 766 - .feed-card-brewer { 767 - border-left: 3px solid var(--type-brewer); 768 - } 769 - ``` 770 - 771 - **Step 2: Identify where feed cards are rendered with type context** 772 - 773 - Read the following files to understand how the feed card type is available in the template context: 774 - - `internal/web/components/action_bar.templ` — the `FeedCard` component that wraps all feed items 775 - - `internal/web/pages/feed.templ` — where feed items are rendered 776 - 777 - The feed card wrapper likely receives a type string (e.g., from `FeedItem.Collection` or similar). Add the appropriate `feed-card-{type}` class based on this value. 778 - 779 - **Important:** Read the actual template code to determine exact prop names and conditional logic. The plan cannot specify exact line numbers because the template structure may vary. The key pattern is: 780 - 781 - ```go 782 - // In the feed card wrapper component, add the type class: 783 - class={ templ.Classes( 784 - "feed-card", 785 - templ.KV("feed-card-brew", props.Type == "brew"), 786 - templ.KV("feed-card-bean", props.Type == "bean"), 787 - // ... etc 788 - ) } 789 - ``` 790 - 791 - **Step 3: Rebuild and verify** 792 - 793 - Run: `templ generate && just style && go vet ./... && go build ./...` 794 - 795 - **Step 4: Commit** 796 - 797 - ```bash 798 - git add static/css/app.css internal/web/components/action_bar.templ internal/web/pages/feed.templ 799 - git commit -m "feat: add colored left-border type indicators to feed cards" 800 - ``` 801 - 802 - --- 803 - 804 - ## Task 5: Clean Up Template Inline Styles 805 - 806 - Several templates use inline Tailwind gradient classes and shadow overrides that bypass the component classes. These need updating to match the new system. 807 - 808 - **Files:** 809 - - Modify: `internal/web/components/shared.templ` 810 - - Modify: `internal/web/pages/about.templ` 811 - - Modify: `internal/web/pages/atproto.templ` 812 - 813 - **Step 1: Update shared.templ** 814 - 815 - In `WelcomeCard`: change `class="card p-8 mb-8"` — the `card` class now handles styling. Keep `p-8 mb-8`. 816 - 817 - In `WelcomeAuthenticated`: remove the inline `shadow-lg hover:shadow-xl` from button links. The `.btn-primary` and `.btn-tertiary` classes handle it now. Example: 818 - ``` 819 - class="btn-primary block text-center py-4 px-6 rounded-xl shadow-lg hover:shadow-xl" 820 - ``` 821 - becomes: 822 - ``` 823 - class="btn-primary block text-center py-4 px-6 rounded-xl" 824 - ``` 825 - 826 - In `EmptyState`: remove the inline `shadow-lg hover:shadow-xl` from the action link. 827 - 828 - In `PageHeader`: change the action button default from `"btn-primary shadow-lg hover:shadow-xl"` to just `"btn-primary"`. 829 - 830 - In `AboutInfoCard`: change from inline gradient classes: 831 - ``` 832 - class="bg-gradient-to-br from-amber-50 to-brown-100 rounded-xl p-6 border-2 border-brown-300 shadow-lg mb-6" 833 - ``` 834 - to: 835 - ``` 836 - class="card p-6 mb-6" 837 - ``` 838 - (or keep as a special card with amber tint if desired — read the actual usage context first) 839 - 840 - **Step 2: Update about.templ and atproto.templ** 841 - 842 - These pages use extensive inline gradient classes for feature sections. Read each file and replace: 843 - - `bg-gradient-to-br from-brown-100 to-brown-200` → `card` class or inline `background: var(--card-bg);` 844 - - `shadow-xl` / `shadow-lg` → remove (cards get shadow from class) 845 - - `border-2 border-brown-300` → `border border-brown-200` or let card class handle it 846 - 847 - Be careful with these pages — they have custom layouts. Don't break the structure, just update the color/shadow treatment. 848 - 849 - **Step 3: Rebuild and verify** 850 - 851 - Run: `templ generate && just style && go vet ./... && go build ./...` 852 - 853 - **Step 4: Commit** 854 - 855 - ```bash 856 - git add internal/web/components/shared.templ internal/web/pages/about.templ internal/web/pages/atproto.templ 857 - git commit -m "refactor: remove inline gradient/shadow overrides from templates" 858 - ``` 859 - 860 - --- 861 - 862 - ## Task 6: Typography Refinements 863 - 864 - Downsize the typography scale and update section title treatment. 865 - 866 - **Files:** 867 - - Modify: `static/css/app.css` (update typography classes) 868 - - Modify: `internal/web/components/shared.templ` (PageHeader title size) 869 - 870 - **Step 1: Update typography classes in app.css** 871 - 872 - ```css 873 - /* Typography */ 874 - .section-title { 875 - @apply text-xs font-semibold uppercase tracking-widest mb-4; 876 - color: var(--text-faint); 877 - } 878 - 879 - .page-title { 880 - @apply text-2xl font-semibold; 881 - color: var(--text-primary); 882 - } 883 - ``` 884 - 885 - **Step 2: Update PageHeader in shared.templ** 886 - 887 - Change the heading from `text-3xl font-bold` to use the `.page-title` class: 888 - ``` 889 - <h2 class="text-3xl font-bold text-brown-900">{ props.Title }</h2> 890 - ``` 891 - becomes: 892 - ``` 893 - <h2 class="page-title">{ props.Title }</h2> 894 - ``` 895 - 896 - **Step 3: Search for other `text-3xl` usages** 897 - 898 - Grep for `text-3xl` across templ files. Update each to `text-2xl font-semibold` or use `.page-title` class. Key locations: 899 - - `manage.templ` — page title 900 - - `notifications.templ` — page title 901 - - `recipe_explore.templ` — page title 902 - - `brew_form.templ` — page title 903 - - `shared.templ` — WelcomeCard title 904 - 905 - **Step 4: Rebuild and verify** 906 - 907 - Run: `templ generate && just style && go vet ./... && go build ./...` 908 - 909 - **Step 5: Commit** 910 - 911 - ```bash 912 - git add static/css/app.css internal/web/components/shared.templ [other modified templ files] 913 - git commit -m "feat: refine typography scale — smaller titles, uppercase section labels" 914 - ``` 915 - 916 - --- 917 - 918 - ## Task 7: Remove Table Row Stagger Animation 919 - 920 - The stagger animation on table rows feels gimmicky for data tables. Remove it while keeping feed card stagger. 921 - 922 - **Files:** 923 - - Modify: `static/css/app.css` (remove table row animation rules) 924 - 925 - **Step 1: Remove table row stagger CSS** 926 - 927 - Delete these rules from app.css (around lines 556-577): 928 - 929 - ```css 930 - /* Table rows slide in with stagger effect (dynamic content) */ 931 - .table-body tr { 932 - animation: fade-in-slide-up 300ms ease-out backwards; 933 - } 934 - 935 - .table-body tr:nth-child(1) { animation-delay: 0ms; } 936 - .table-body tr:nth-child(2) { animation-delay: 30ms; } 937 - .table-body tr:nth-child(3) { animation-delay: 60ms; } 938 - .table-body tr:nth-child(4) { animation-delay: 90ms; } 939 - .table-body tr:nth-child(5) { animation-delay: 120ms; } 940 - .table-body tr:nth-child(n + 6) { animation-delay: 150ms; } 941 - ``` 942 - 943 - **Step 2: Rebuild** 944 - 945 - Run: `just style` 946 - 947 - **Step 3: Commit** 948 - 949 - ```bash 950 - git add static/css/app.css 951 - git commit -m "refactor: remove table row stagger animation" 952 - ``` 953 - 954 - --- 955 - 956 - ## Task 8: Update Form Input Focus Behavior 957 - 958 - Remove the `translateY(-1px)` focus lift on form elements. Clean Craft uses a subtle background tint change on focus instead, which is already handled by the new `.form-input` definition. 959 - 960 - **Files:** 961 - - Modify: `static/css/app.css` (remove focus transform rules) 962 - 963 - **Step 1: Remove focus transform** 964 - 965 - Delete these rules (around lines 647-660): 966 - 967 - ```css 968 - .form-input:focus, 969 - .form-select:focus, 970 - .form-textarea:focus { 971 - transform: translateY(-1px); 972 - } 973 - ``` 974 - 975 - Also update the transition rule to remove `transform`: 976 - ```css 977 - .form-input, 978 - .form-select, 979 - .form-textarea { 980 - transition: 981 - border-color 100ms ease, 982 - box-shadow 100ms ease, 983 - transform 50ms ease; 984 - } 985 - ``` 986 - Change to: 987 - ```css 988 - .form-input, 989 - .form-select, 990 - .form-textarea { 991 - transition: 992 - border-color 150ms ease, 993 - box-shadow 150ms ease, 994 - background-color 150ms ease; 995 - } 996 - ``` 997 - 998 - Note: If the new `.form-input` definition in Task 2 already includes its own transition, this separate rule may be redundant. Check whether it's still needed after Task 2 is applied. If the Task 2 definition already has transition on the class itself, delete this separate rule entirely. 999 - 1000 - **Step 2: Rebuild** 1001 - 1002 - Run: `just style` 1003 - 1004 - **Step 3: Commit** 1005 - 1006 - ```bash 1007 - git add static/css/app.css 1008 - git commit -m "refactor: replace form focus lift with background tint transition" 1009 - ``` 1010 - 1011 - --- 1012 - 1013 - ## Task 9: Visual QA and Polish 1014 - 1015 - Manual review pass to catch inconsistencies. 1016 - 1017 - **Files:** Various — depends on findings. 1018 - 1019 - **Step 1: Run the dev server** 1020 - 1021 - Run: `go run cmd/server/main.go` 1022 - 1023 - **Step 2: Visual checklist** 1024 - 1025 - Check each page and verify: 1026 - 1027 - - [ ] **Home page:** WelcomeCard renders as white card on cream background. No gradient. Login form inputs have 1px borders. 1028 - - [ ] **Feed:** Feed cards are white with subtle shadow. Type indicators show colored left borders. Action buttons are transparent (no background) until hover. 1029 - - [ ] **Brew form:** All inputs have 1px borders. Focus shows tint change + ring. No translateY lift. 1030 - - [ ] **Brew view:** Detail fields use section-box with subtle tint. Card is white. 1031 - - [ ] **Manage page:** Tables are white with light header. No gradient backgrounds. 1032 - - [ ] **Profile:** Stats cards are white. Tab content loads properly. 1033 - - [ ] **Recipe explore:** Cards are white. Detail panel matches. 1034 - - [ ] **Modals:** White background, subtle border, no gradient. 1035 - - [ ] **Header:** Slightly shorter, ALPHA badge smaller. Shadow subtle or absent. 1036 - - [ ] **Footer:** Clean, matches cream background. 1037 - - [ ] **Mobile:** Check all of the above at < 640px width. 1038 - 1039 - **Step 3: Fix any issues found** 1040 - 1041 - Address visual inconsistencies discovered during QA. Common issues to watch for: 1042 - - Templates with hardcoded `bg-brown-100` or `bg-brown-50` that should now be `bg-white` or use variables 1043 - - Inline `shadow-*` classes that override the component class shadow 1044 - - `border-2` on inputs that weren't caught in the component class rewrite (inline overrides in templates) 1045 - - Text color classes that should be updated (`text-brown-800` → just inherit from parent or use variable) 1046 - 1047 - **Step 4: Commit fixes** 1048 - 1049 - ```bash 1050 - git add -A 1051 - git commit -m "fix: visual QA polish for Clean Craft overhaul" 1052 - ``` 1053 - 1054 - --- 1055 - 1056 - ## Task 10: Bump CSS Version and Final Build Check 1057 - 1058 - **Files:** 1059 - - Modify: `internal/web/components/layout.templ` (verify CSS version bumped) 1060 - 1061 - **Step 1: Verify CSS version** 1062 - 1063 - The version should already be `0.7.0` from Task 3. Confirm it's correct. 1064 - 1065 - **Step 2: Full build and vet** 1066 - 1067 - Run: `templ generate && just style && go vet ./... && go build ./... && go test ./...` 1068 - 1069 - Expected: All pass. 1070 - 1071 - **Step 3: Final commit if needed** 1072 - 1073 - If any last fixes were made: 1074 - ```bash 1075 - git add -A 1076 - git commit -m "chore: final Clean Craft overhaul build verification" 1077 - ``` 1078 - 1079 - --- 1080 - 1081 - ## Future Work (Not in This Plan) 1082 - 1083 - These are noted for later and should NOT be done in this implementation: 1084 - 1085 - 1. **Dark mode (Option B):** Add `@media (prefers-color-scheme: dark)` block redefining all CSS variables with espresso/cream values. Also add a manual toggle. All the structural work is done — this is purely a color variable swap. 1086 - 1087 - 2. **SVG icon system:** Replace emoji icons (📍🔥🌱⚖️🏭) with SVG icons from Lucide or Phosphor. Separate task, requires icon selection and template updates. 1088 - 1089 - 3. **Feed content box removal:** The `.feed-content-box` wrappers are restyled but still exist in templates. A future cleanup can remove them entirely and let content sit directly in the feed card, but this is optional since the restyled version (subtle tint, no border) already looks clean.
-987
docs/ui-comparison.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>Arabica UI Overhaul — Option Comparison</title> 7 - <style> 8 - /* ===== Shared Font ===== */ 9 - @font-face { 10 - font-family: 'Iosevka Patrick'; 11 - src: url('../static/fonts/IosevkaPatrickNerdFont-Regular.woff2') format('woff2'); 12 - font-weight: 400; 13 - font-style: normal; 14 - font-display: swap; 15 - } 16 - @font-face { 17 - font-family: 'Iosevka Patrick'; 18 - src: url('../static/fonts/IosevkaPatrickNerdFont-Medium.woff2') format('woff2'); 19 - font-weight: 500; 20 - font-style: normal; 21 - font-display: swap; 22 - } 23 - @font-face { 24 - font-family: 'Iosevka Patrick'; 25 - src: url('../static/fonts/IosevkaPatrickNerdFont-SemiBold.woff2') format('woff2'); 26 - font-weight: 600; 27 - font-style: normal; 28 - font-display: swap; 29 - } 30 - 31 - * { margin: 0; padding: 0; box-sizing: border-box; } 32 - 33 - body { 34 - font-family: 'Iosevka Patrick', ui-monospace, monospace; 35 - background: #1a1a1a; 36 - color: #ccc; 37 - min-height: 100vh; 38 - } 39 - 40 - /* ===== Layout ===== */ 41 - .picker { 42 - display: flex; 43 - justify-content: center; 44 - gap: 12px; 45 - padding: 24px 16px 16px; 46 - position: sticky; 47 - top: 0; 48 - z-index: 100; 49 - background: #1a1a1a; 50 - border-bottom: 1px solid #333; 51 - } 52 - .picker button { 53 - font-family: inherit; 54 - padding: 8px 24px; 55 - border-radius: 8px; 56 - border: 1px solid #444; 57 - background: #222; 58 - color: #aaa; 59 - font-size: 13px; 60 - font-weight: 500; 61 - cursor: pointer; 62 - transition: all 150ms; 63 - } 64 - .picker button:hover { border-color: #666; color: #ddd; } 65 - .picker button.active-a { background: #FAF7F5; color: #3d2319; border-color: #d2bab0; } 66 - .picker button.active-b { background: #1C1210; color: #FAF7F5; border-color: #3D2D24; box-shadow: 0 0 12px rgba(251,191,36,0.15); } 67 - 68 - .panels { 69 - display: grid; 70 - grid-template-columns: 1fr 1fr; 71 - gap: 0; 72 - max-width: 1400px; 73 - margin: 0 auto; 74 - } 75 - @media (max-width: 900px) { 76 - .panels { grid-template-columns: 1fr; } 77 - } 78 - 79 - .panel { 80 - padding: 32px 24px; 81 - min-height: 100vh; 82 - } 83 - .panel-label { 84 - font-size: 11px; 85 - font-weight: 600; 86 - letter-spacing: 0.1em; 87 - text-transform: uppercase; 88 - margin-bottom: 24px; 89 - padding-bottom: 8px; 90 - } 91 - .component-group { 92 - margin-bottom: 32px; 93 - } 94 - .component-group h3 { 95 - font-size: 11px; 96 - font-weight: 500; 97 - text-transform: uppercase; 98 - letter-spacing: 0.08em; 99 - margin-bottom: 12px; 100 - opacity: 0.5; 101 - } 102 - 103 - /* ===== Divider ===== */ 104 - .divider { 105 - width: 1px; 106 - background: #333; 107 - position: absolute; 108 - left: 50%; 109 - top: 0; 110 - bottom: 0; 111 - } 112 - 113 - /* ========================================================== 114 - OPTION A: "Clean Craft" 115 - ========================================================== */ 116 - .a { 117 - background: #FAF7F5; 118 - color: #3d2319; 119 - } 120 - .a .panel-label { 121 - color: #bfa094; 122 - border-bottom: 1px solid #eaddd7; 123 - } 124 - .a .component-group h3 { color: #7f5539; } 125 - 126 - /* -- A: Header -- */ 127 - .a-header { 128 - display: flex; 129 - align-items: center; 130 - justify-content: space-between; 131 - background: linear-gradient(135deg, #4a2c2a, #3d2319); 132 - padding: 0 16px; 133 - height: 48px; 134 - border-radius: 10px 10px 0 0; 135 - margin-bottom: 0; 136 - } 137 - .a-header-logo { 138 - color: #FAF7F5; 139 - font-weight: 600; 140 - font-size: 14px; 141 - display: flex; 142 - align-items: center; 143 - gap: 8px; 144 - } 145 - .a-header-badge { 146 - font-size: 9px; 147 - font-weight: 600; 148 - background: #fbbf24; 149 - color: #3d2319; 150 - padding: 2px 6px; 151 - border-radius: 4px; 152 - letter-spacing: 0.05em; 153 - } 154 - .a-header-avatar { 155 - width: 28px; 156 - height: 28px; 157 - border-radius: 50%; 158 - background: #6b4423; 159 - border: 2px solid #bfa094; 160 - display: flex; 161 - align-items: center; 162 - justify-content: center; 163 - color: #e0cec7; 164 - font-size: 11px; 165 - font-weight: 500; 166 - } 167 - 168 - /* -- A: Cards -- */ 169 - .a-card { 170 - background: #FFFFFF; 171 - border: 1px solid #eaddd7; 172 - border-radius: 12px; 173 - padding: 16px; 174 - box-shadow: 0 1px 3px rgba(61,35,25,0.06); 175 - transition: box-shadow 200ms; 176 - } 177 - .a-card:hover { 178 - box-shadow: 0 4px 12px rgba(61,35,25,0.1); 179 - } 180 - 181 - /* -- A: Feed Card -- */ 182 - .a-feed-card { 183 - background: #FFFFFF; 184 - border: 1px solid #eaddd7; 185 - border-left: 3px solid #6b4423; 186 - border-radius: 12px; 187 - padding: 16px; 188 - box-shadow: 0 1px 3px rgba(61,35,25,0.06); 189 - transition: box-shadow 200ms; 190 - } 191 - .a-feed-card:hover { 192 - box-shadow: 0 4px 12px rgba(61,35,25,0.1); 193 - } 194 - .a-feed-card.type-bean { border-left-color: #d97706; } 195 - .a-feed-card.type-recipe { border-left-color: #bfa094; } 196 - 197 - .a-feed-meta { 198 - display: flex; 199 - align-items: center; 200 - gap: 8px; 201 - margin-bottom: 8px; 202 - } 203 - .a-feed-avatar { 204 - width: 28px; 205 - height: 28px; 206 - border-radius: 50%; 207 - background: #e0cec7; 208 - flex-shrink: 0; 209 - display: flex; 210 - align-items: center; 211 - justify-content: center; 212 - font-size: 11px; 213 - font-weight: 500; 214 - color: #7f5539; 215 - } 216 - .a-feed-meta-text { 217 - font-size: 12px; 218 - color: #7f5539; 219 - } 220 - .a-feed-meta-text strong { 221 - color: #3d2319; 222 - font-weight: 600; 223 - } 224 - .a-feed-action { 225 - font-size: 13px; 226 - color: #4a2c2a; 227 - margin-bottom: 12px; 228 - } 229 - .a-feed-action a { 230 - color: #6b4423; 231 - text-decoration: underline; 232 - text-underline-offset: 2px; 233 - text-decoration-color: #d2bab0; 234 - } 235 - 236 - .a-feed-inset { 237 - background: rgba(253,248,246,0.5); 238 - border-radius: 8px; 239 - padding: 12px; 240 - margin-bottom: 12px; 241 - } 242 - .a-feed-inset-title { 243 - font-size: 13px; 244 - font-weight: 600; 245 - color: #3d2319; 246 - margin-bottom: 4px; 247 - } 248 - .a-feed-inset-detail { 249 - font-size: 12px; 250 - color: #7f5539; 251 - line-height: 1.6; 252 - } 253 - .a-feed-inset-detail .val { 254 - color: #4a2c2a; 255 - font-weight: 500; 256 - } 257 - .a-rating { 258 - display: inline-flex; 259 - align-items: center; 260 - gap: 4px; 261 - font-size: 12px; 262 - font-weight: 500; 263 - background: #fef3c7; 264 - color: #78350f; 265 - padding: 2px 10px; 266 - border-radius: 99px; 267 - } 268 - .a-tasting { 269 - font-size: 12px; 270 - font-style: italic; 271 - color: #4a2c2a; 272 - margin-top: 8px; 273 - padding-top: 8px; 274 - border-top: 1px solid #f2e8e5; 275 - } 276 - 277 - .a-action-bar { 278 - display: flex; 279 - align-items: center; 280 - gap: 4px; 281 - padding-top: 8px; 282 - } 283 - .a-action-btn { 284 - display: inline-flex; 285 - align-items: center; 286 - gap: 4px; 287 - padding: 6px 10px; 288 - border-radius: 6px; 289 - font-size: 12px; 290 - color: #7f5539; 291 - background: transparent; 292 - border: none; 293 - cursor: pointer; 294 - font-family: inherit; 295 - transition: background 150ms; 296 - } 297 - .a-action-btn:hover { background: #f2e8e5; } 298 - .a-action-btn svg { width: 14px; height: 14px; } 299 - 300 - /* -- A: Buttons -- */ 301 - .a-btn-primary { 302 - display: inline-flex; 303 - align-items: center; 304 - justify-content: center; 305 - padding: 8px 20px; 306 - border-radius: 8px; 307 - font-family: inherit; 308 - font-size: 13px; 309 - font-weight: 500; 310 - background: #4a2c2a; 311 - color: #FAF7F5; 312 - border: none; 313 - cursor: pointer; 314 - transition: background 150ms; 315 - } 316 - .a-btn-primary:hover { background: #3d2319; } 317 - 318 - .a-btn-secondary { 319 - display: inline-flex; 320 - align-items: center; 321 - justify-content: center; 322 - padding: 8px 20px; 323 - border-radius: 8px; 324 - font-family: inherit; 325 - font-size: 13px; 326 - font-weight: 500; 327 - background: #FFFFFF; 328 - color: #6b4423; 329 - border: 1px solid #e0cec7; 330 - cursor: pointer; 331 - transition: all 150ms; 332 - } 333 - .a-btn-secondary:hover { background: #FAF7F5; border-color: #d2bab0; } 334 - 335 - /* -- A: Form -- */ 336 - .a-form-group { margin-bottom: 12px; } 337 - .a-form-label { 338 - display: block; 339 - font-size: 11px; 340 - font-weight: 500; 341 - color: #3d2319; 342 - margin-bottom: 4px; 343 - text-transform: uppercase; 344 - letter-spacing: 0.03em; 345 - } 346 - .a-form-input { 347 - width: 100%; 348 - padding: 8px 12px; 349 - border-radius: 8px; 350 - border: 1px solid #e0cec7; 351 - font-family: inherit; 352 - font-size: 13px; 353 - color: #3d2319; 354 - background: #FFFFFF; 355 - outline: none; 356 - transition: all 150ms; 357 - } 358 - .a-form-input:focus { 359 - border-color: #7f5539; 360 - box-shadow: 0 0 0 2px rgba(127,85,57,0.15); 361 - background: rgba(253,248,246,0.3); 362 - } 363 - .a-form-input::placeholder { color: #d2bab0; } 364 - 365 - /* -- A: Section Title -- */ 366 - .a-section-title { 367 - font-size: 11px; 368 - font-weight: 600; 369 - color: #bfa094; 370 - text-transform: uppercase; 371 - letter-spacing: 0.1em; 372 - margin-bottom: 12px; 373 - } 374 - 375 - /* -- A: Table -- */ 376 - .a-table-wrap { 377 - background: #FFFFFF; 378 - border: 1px solid #eaddd7; 379 - border-radius: 10px; 380 - overflow: hidden; 381 - box-shadow: 0 1px 3px rgba(61,35,25,0.06); 382 - } 383 - .a-table { width: 100%; border-collapse: collapse; } 384 - .a-table th { 385 - text-align: left; 386 - padding: 10px 14px; 387 - font-size: 10px; 388 - font-weight: 600; 389 - color: #7f5539; 390 - text-transform: uppercase; 391 - letter-spacing: 0.08em; 392 - background: #FAF7F5; 393 - border-bottom: 1px solid #eaddd7; 394 - } 395 - .a-table td { 396 - padding: 10px 14px; 397 - font-size: 12px; 398 - color: #4a2c2a; 399 - border-bottom: 1px solid #f2e8e5; 400 - } 401 - .a-table tr:last-child td { border-bottom: none; } 402 - .a-table tr:hover td { background: #FAF7F5; } 403 - 404 - 405 - /* ========================================================== 406 - OPTION B: "Roasted" 407 - ========================================================== */ 408 - .b { 409 - background: #0F0A08; 410 - color: #FAF7F5; 411 - } 412 - .b .panel-label { 413 - color: #C4A898; 414 - border-bottom: 1px solid #2E211B; 415 - } 416 - .b .component-group h3 { color: #C4A898; } 417 - 418 - /* grain */ 419 - .b { position: relative; } 420 - .b::before { 421 - content: ""; 422 - position: absolute; 423 - inset: 0; 424 - pointer-events: none; 425 - opacity: 0.04; 426 - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); 427 - background-repeat: repeat; 428 - background-size: 256px; 429 - z-index: 1; 430 - } 431 - .b > * { position: relative; z-index: 2; } 432 - 433 - /* -- B: Header -- */ 434 - .b-header { 435 - display: flex; 436 - align-items: center; 437 - justify-content: space-between; 438 - background: #0F0A08; 439 - padding: 0 16px; 440 - height: 48px; 441 - border-radius: 10px 10px 0 0; 442 - border-bottom: 1px solid #2E211B; 443 - } 444 - .b-header-logo { 445 - color: #FAF7F5; 446 - font-weight: 600; 447 - font-size: 14px; 448 - display: flex; 449 - align-items: center; 450 - gap: 8px; 451 - } 452 - .b-header-badge { 453 - font-size: 9px; 454 - font-weight: 600; 455 - background: #fbbf24; 456 - color: #0F0A08; 457 - padding: 2px 6px; 458 - border-radius: 4px; 459 - letter-spacing: 0.05em; 460 - } 461 - .b-header-avatar { 462 - width: 28px; 463 - height: 28px; 464 - border-radius: 50%; 465 - background: #3D2D24; 466 - border: 2px solid #C4A898; 467 - display: flex; 468 - align-items: center; 469 - justify-content: center; 470 - color: #E0CEC4; 471 - font-size: 11px; 472 - font-weight: 500; 473 - } 474 - 475 - /* -- B: Cards -- */ 476 - .b-card { 477 - background: #1C1210; 478 - border: 1px solid #2E211B; 479 - border-radius: 12px; 480 - padding: 16px; 481 - transition: background 200ms, border-color 200ms; 482 - } 483 - .b-card:hover { 484 - background: #241A16; 485 - border-color: #3D2D24; 486 - } 487 - 488 - /* -- B: Feed Card -- */ 489 - .b-feed-card { 490 - background: #1C1210; 491 - border: 1px solid #2E211B; 492 - border-radius: 12px; 493 - padding: 16px; 494 - transition: background 200ms, border-color 200ms; 495 - } 496 - .b-feed-card:hover { 497 - background: #241A16; 498 - border-color: #3D2D24; 499 - } 500 - 501 - .b-feed-meta { 502 - display: flex; 503 - align-items: center; 504 - gap: 8px; 505 - margin-bottom: 8px; 506 - } 507 - .b-feed-avatar { 508 - width: 28px; 509 - height: 28px; 510 - border-radius: 50%; 511 - background: #3D2D24; 512 - flex-shrink: 0; 513 - display: flex; 514 - align-items: center; 515 - justify-content: center; 516 - font-size: 11px; 517 - font-weight: 500; 518 - color: #C4A898; 519 - } 520 - .b-feed-meta-text { 521 - font-size: 12px; 522 - color: #C4A898; 523 - } 524 - .b-feed-meta-text strong { 525 - color: #FAF7F5; 526 - font-weight: 600; 527 - } 528 - .b-type-dot { 529 - display: inline-block; 530 - width: 6px; 531 - height: 6px; 532 - border-radius: 50%; 533 - background: #fbbf24; 534 - margin-right: 4px; 535 - vertical-align: middle; 536 - } 537 - .b-type-dot.bean { background: #E0CEC4; } 538 - .b-type-dot.recipe { background: #C4553A; } 539 - 540 - .b-feed-action { 541 - font-size: 13px; 542 - color: #E0CEC4; 543 - margin-bottom: 12px; 544 - } 545 - .b-feed-action a { 546 - color: #fbbf24; 547 - text-decoration: underline; 548 - text-underline-offset: 2px; 549 - text-decoration-color: rgba(251,191,36,0.3); 550 - } 551 - 552 - .b-feed-inset { 553 - background: #241A16; 554 - border-radius: 8px; 555 - padding: 12px; 556 - margin-bottom: 12px; 557 - } 558 - .b-feed-inset-title { 559 - font-size: 13px; 560 - font-weight: 600; 561 - color: #FAF7F5; 562 - margin-bottom: 4px; 563 - } 564 - .b-feed-inset-detail { 565 - font-size: 12px; 566 - color: #C4A898; 567 - line-height: 1.6; 568 - } 569 - .b-feed-inset-detail .val { 570 - color: #FAF7F5; 571 - font-weight: 500; 572 - } 573 - .b-rating { 574 - display: inline-flex; 575 - align-items: center; 576 - gap: 4px; 577 - font-size: 12px; 578 - font-weight: 500; 579 - background: rgba(251,191,36,0.15); 580 - color: #fbbf24; 581 - padding: 2px 10px; 582 - border-radius: 99px; 583 - } 584 - .b-tasting { 585 - font-size: 12px; 586 - font-style: italic; 587 - color: #C4A898; 588 - margin-top: 8px; 589 - padding-top: 8px; 590 - border-top: 1px solid #2E211B; 591 - } 592 - 593 - .b-action-bar { 594 - display: flex; 595 - align-items: center; 596 - gap: 4px; 597 - padding-top: 8px; 598 - } 599 - .b-action-btn { 600 - display: inline-flex; 601 - align-items: center; 602 - gap: 4px; 603 - padding: 6px 10px; 604 - border-radius: 6px; 605 - font-size: 12px; 606 - color: #C4A898; 607 - background: transparent; 608 - border: none; 609 - cursor: pointer; 610 - font-family: inherit; 611 - transition: all 150ms; 612 - } 613 - .b-action-btn:hover { background: #241A16; color: #FAF7F5; } 614 - .b-action-btn svg { width: 14px; height: 14px; } 615 - 616 - /* -- B: Section Title -- */ 617 - .b-section-title { 618 - font-size: 11px; 619 - font-weight: 600; 620 - color: #fbbf24; 621 - text-transform: uppercase; 622 - letter-spacing: 0.15em; 623 - margin-bottom: 12px; 624 - } 625 - 626 - /* -- B: Buttons -- */ 627 - .b-btn-primary { 628 - display: inline-flex; 629 - align-items: center; 630 - justify-content: center; 631 - padding: 8px 20px; 632 - border-radius: 8px; 633 - font-family: inherit; 634 - font-size: 13px; 635 - font-weight: 500; 636 - background: linear-gradient(135deg, #C4553A, #A3412D); 637 - color: #FAF7F5; 638 - border: none; 639 - cursor: pointer; 640 - transition: all 150ms; 641 - } 642 - .b-btn-primary:hover { filter: brightness(1.1); } 643 - 644 - .b-btn-secondary { 645 - display: inline-flex; 646 - align-items: center; 647 - justify-content: center; 648 - padding: 8px 20px; 649 - border-radius: 8px; 650 - font-family: inherit; 651 - font-size: 13px; 652 - font-weight: 500; 653 - background: #241A16; 654 - color: #E0CEC4; 655 - border: 1px solid #3D2D24; 656 - cursor: pointer; 657 - transition: all 150ms; 658 - } 659 - .b-btn-secondary:hover { background: #2E211B; border-color: #4a3830; } 660 - 661 - /* -- B: Form -- */ 662 - .b-form-group { margin-bottom: 12px; } 663 - .b-form-label { 664 - display: block; 665 - font-size: 11px; 666 - font-weight: 500; 667 - color: #C4A898; 668 - margin-bottom: 4px; 669 - text-transform: uppercase; 670 - letter-spacing: 0.05em; 671 - } 672 - .b-form-input { 673 - width: 100%; 674 - padding: 8px 12px; 675 - border-radius: 8px; 676 - border: 1px solid #3D2D24; 677 - font-family: inherit; 678 - font-size: 13px; 679 - color: #FAF7F5; 680 - background: #241A16; 681 - outline: none; 682 - transition: all 150ms; 683 - } 684 - .b-form-input:focus { 685 - border-color: #fbbf24; 686 - box-shadow: 0 0 0 2px rgba(251,191,36,0.15); 687 - } 688 - .b-form-input::placeholder { color: #5a4a40; } 689 - 690 - /* -- B: Table -- */ 691 - .b-table-wrap { 692 - background: #1C1210; 693 - border: 1px solid #2E211B; 694 - border-radius: 10px; 695 - overflow: hidden; 696 - } 697 - .b-table { width: 100%; border-collapse: collapse; } 698 - .b-table th { 699 - text-align: left; 700 - padding: 10px 14px; 701 - font-size: 10px; 702 - font-weight: 600; 703 - color: #C4A898; 704 - text-transform: uppercase; 705 - letter-spacing: 0.08em; 706 - background: #241A16; 707 - border-bottom: 1px solid #2E211B; 708 - } 709 - .b-table td { 710 - padding: 10px 14px; 711 - font-size: 12px; 712 - color: #E0CEC4; 713 - border-bottom: 1px solid #1C1210; 714 - } 715 - .b-table tr:last-child td { border-bottom: none; } 716 - .b-table tr:hover td { background: #241A16; } 717 - </style> 718 - </head> 719 - <body> 720 - 721 - <div class="picker"> 722 - <button class="active-a" disabled>← Option A: "Clean Craft"</button> 723 - <button class="active-b" disabled>Option B: "Roasted" →</button> 724 - </div> 725 - 726 - <div class="panels" style="position:relative;"> 727 - 728 - <!-- ==================== OPTION A ==================== --> 729 - <div class="panel a"> 730 - <div class="panel-label">Option A — "Clean Craft"</div> 731 - 732 - <!-- Header --> 733 - <div class="component-group"> 734 - <h3>Header</h3> 735 - <div class="a-header"> 736 - <div class="a-header-logo">☕ arabica <span class="a-header-badge">ALPHA</span></div> 737 - <div class="a-header-avatar">PK</div> 738 - </div> 739 - </div> 740 - 741 - <!-- Feed Card: Brew --> 742 - <div class="component-group"> 743 - <h3>Feed Card — Brew</h3> 744 - <div class="a-feed-card"> 745 - <div class="a-feed-meta"> 746 - <div class="a-feed-avatar">JM</div> 747 - <div class="a-feed-meta-text"><strong>Jordan M.</strong> · @jordan.coffee · 2h</div> 748 - </div> 749 - <div class="a-feed-action">brewed with <a href="#">Ethiopian Sidamo</a></div> 750 - <div class="a-feed-inset"> 751 - <div class="a-feed-inset-title">Sweet Bloom · V60</div> 752 - <div class="a-feed-inset-detail"> 753 - <span class="val">15g</span> → <span class="val">250g</span> · <span class="val">1:16.7</span> · <span class="val">93°C</span> · <span class="val">3:15</span> 754 - </div> 755 - <div style="margin-top:6px;"><span class="a-rating">⭐ 8.5</span></div> 756 - <div class="a-tasting">"Bright citrus with chocolate finish, clean body"</div> 757 - </div> 758 - <div class="a-action-bar"> 759 - <button class="a-action-btn"> 760 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> 761 - 3 762 - </button> 763 - <button class="a-action-btn"> 764 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg> 765 - 12 766 - </button> 767 - <button class="a-action-btn"> 768 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> 769 - Share 770 - </button> 771 - </div> 772 - </div> 773 - </div> 774 - 775 - <!-- Feed Card: Bean --> 776 - <div class="component-group"> 777 - <h3>Feed Card — Bean</h3> 778 - <div class="a-feed-card type-bean"> 779 - <div class="a-feed-meta"> 780 - <div class="a-feed-avatar">SK</div> 781 - <div class="a-feed-meta-text"><strong>Sam K.</strong> · @samk.bsky · 5h</div> 782 - </div> 783 - <div class="a-feed-action">added a new bean: <a href="#">Guatemala Huehuetenango</a></div> 784 - <div class="a-feed-inset"> 785 - <div class="a-feed-inset-detail"> 786 - Onyx Coffee Lab · Medium roast · Washed<br> 787 - Toffee, red apple, cocoa 788 - </div> 789 - </div> 790 - <div class="a-action-bar"> 791 - <button class="a-action-btn"> 792 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg> 793 - 4 794 - </button> 795 - </div> 796 - </div> 797 - </div> 798 - 799 - <!-- Buttons --> 800 - <div class="component-group"> 801 - <h3>Buttons</h3> 802 - <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;"> 803 - <button class="a-btn-primary">Log Brew</button> 804 - <button class="a-btn-secondary">Cancel</button> 805 - </div> 806 - </div> 807 - 808 - <!-- Form --> 809 - <div class="component-group"> 810 - <h3>Form Inputs</h3> 811 - <div class="a-card" style="max-width:320px;"> 812 - <div class="a-form-group"> 813 - <label class="a-form-label">Coffee (g)</label> 814 - <input class="a-form-input" type="text" placeholder="15" /> 815 - </div> 816 - <div class="a-form-group"> 817 - <label class="a-form-label">Water (g)</label> 818 - <input class="a-form-input" type="text" placeholder="250" /> 819 - </div> 820 - <div class="a-form-group"> 821 - <label class="a-form-label">Tasting Notes</label> 822 - <input class="a-form-input" type="text" placeholder="Bright, citrus, clean..." /> 823 - </div> 824 - <div style="margin-top:16px;"> 825 - <button class="a-btn-primary" style="width:100%;">Save Brew</button> 826 - </div> 827 - </div> 828 - </div> 829 - 830 - <!-- Section Title --> 831 - <div class="component-group"> 832 - <h3>Section Title</h3> 833 - <div class="a-section-title">Recent Brews</div> 834 - <div style="font-size:12px;color:#7f5539;">Section content would appear here...</div> 835 - </div> 836 - 837 - <!-- Table --> 838 - <div class="component-group"> 839 - <h3>Table</h3> 840 - <div class="a-table-wrap"> 841 - <table class="a-table"> 842 - <thead> 843 - <tr><th>Bean</th><th>Roaster</th><th>Origin</th><th>Roast</th></tr> 844 - </thead> 845 - <tbody> 846 - <tr><td>Ethiopian Sidamo</td><td>Sweet Bloom</td><td>Ethiopia</td><td>Light</td></tr> 847 - <tr><td>Guatemala Huehue</td><td>Onyx</td><td>Guatemala</td><td>Medium</td></tr> 848 - <tr><td>Kenya Nyeri AA</td><td>Counter Culture</td><td>Kenya</td><td>Light</td></tr> 849 - </tbody> 850 - </table> 851 - </div> 852 - </div> 853 - 854 - </div> 855 - 856 - <!-- ==================== OPTION B ==================== --> 857 - <div class="panel b"> 858 - <div class="panel-label">Option B — "Roasted"</div> 859 - 860 - <!-- Header --> 861 - <div class="component-group"> 862 - <h3>Header</h3> 863 - <div class="b-header"> 864 - <div class="b-header-logo">☕ arabica <span class="b-header-badge">ALPHA</span></div> 865 - <div class="b-header-avatar">PK</div> 866 - </div> 867 - </div> 868 - 869 - <!-- Feed Card: Brew --> 870 - <div class="component-group"> 871 - <h3>Feed Card — Brew</h3> 872 - <div class="b-feed-card"> 873 - <div class="b-feed-meta"> 874 - <div class="b-feed-avatar">JM</div> 875 - <div class="b-feed-meta-text"><strong>Jordan M.</strong> · @jordan.coffee · 2h</div> 876 - </div> 877 - <div class="b-feed-action"><span class="b-type-dot"></span>brewed with <a href="#">Ethiopian Sidamo</a></div> 878 - <div class="b-feed-inset"> 879 - <div class="b-feed-inset-title">Sweet Bloom · V60</div> 880 - <div class="b-feed-inset-detail"> 881 - <span class="val">15g</span> → <span class="val">250g</span> · <span class="val">1:16.7</span> · <span class="val">93°C</span> · <span class="val">3:15</span> 882 - </div> 883 - <div style="margin-top:6px;"><span class="b-rating">⭐ 8.5</span></div> 884 - <div class="b-tasting">"Bright citrus with chocolate finish, clean body"</div> 885 - </div> 886 - <div class="b-action-bar"> 887 - <button class="b-action-btn"> 888 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> 889 - 3 890 - </button> 891 - <button class="b-action-btn"> 892 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg> 893 - 12 894 - </button> 895 - <button class="b-action-btn"> 896 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> 897 - Share 898 - </button> 899 - </div> 900 - </div> 901 - </div> 902 - 903 - <!-- Feed Card: Bean --> 904 - <div class="component-group"> 905 - <h3>Feed Card — Bean</h3> 906 - <div class="b-feed-card"> 907 - <div class="b-feed-meta"> 908 - <div class="b-feed-avatar">SK</div> 909 - <div class="b-feed-meta-text"><strong>Sam K.</strong> · @samk.bsky · 5h</div> 910 - </div> 911 - <div class="b-feed-action"><span class="b-type-dot bean"></span>added a new bean: <a href="#">Guatemala Huehuetenango</a></div> 912 - <div class="b-feed-inset"> 913 - <div class="b-feed-inset-detail"> 914 - Onyx Coffee Lab · Medium roast · Washed<br> 915 - Toffee, red apple, cocoa 916 - </div> 917 - </div> 918 - <div class="b-action-bar"> 919 - <button class="b-action-btn"> 920 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg> 921 - 4 922 - </button> 923 - </div> 924 - </div> 925 - </div> 926 - 927 - <!-- Buttons --> 928 - <div class="component-group"> 929 - <h3>Buttons</h3> 930 - <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;"> 931 - <button class="b-btn-primary">Log Brew</button> 932 - <button class="b-btn-secondary">Cancel</button> 933 - </div> 934 - </div> 935 - 936 - <!-- Form --> 937 - <div class="component-group"> 938 - <h3>Form Inputs</h3> 939 - <div class="b-card" style="max-width:320px;"> 940 - <div class="b-form-group"> 941 - <label class="b-form-label">Coffee (g)</label> 942 - <input class="b-form-input" type="text" placeholder="15" /> 943 - </div> 944 - <div class="b-form-group"> 945 - <label class="b-form-label">Water (g)</label> 946 - <input class="b-form-input" type="text" placeholder="250" /> 947 - </div> 948 - <div class="b-form-group"> 949 - <label class="b-form-label">Tasting Notes</label> 950 - <input class="b-form-input" type="text" placeholder="Bright, citrus, clean..." /> 951 - </div> 952 - <div style="margin-top:16px;"> 953 - <button class="b-btn-primary" style="width:100%;">Save Brew</button> 954 - </div> 955 - </div> 956 - </div> 957 - 958 - <!-- Section Title --> 959 - <div class="component-group"> 960 - <h3>Section Title</h3> 961 - <div class="b-section-title">Recent Brews</div> 962 - <div style="font-size:12px;color:#C4A898;">Section content would appear here...</div> 963 - </div> 964 - 965 - <!-- Table --> 966 - <div class="component-group"> 967 - <h3>Table</h3> 968 - <div class="b-table-wrap"> 969 - <table class="b-table"> 970 - <thead> 971 - <tr><th>Bean</th><th>Roaster</th><th>Origin</th><th>Roast</th></tr> 972 - </thead> 973 - <tbody> 974 - <tr><td>Ethiopian Sidamo</td><td>Sweet Bloom</td><td>Ethiopia</td><td>Light</td></tr> 975 - <tr><td>Guatemala Huehue</td><td>Onyx</td><td>Guatemala</td><td>Medium</td></tr> 976 - <tr><td>Kenya Nyeri AA</td><td>Counter Culture</td><td>Kenya</td><td>Light</td></tr> 977 - </tbody> 978 - </table> 979 - </div> 980 - </div> 981 - 982 - </div> 983 - 984 - </div> 985 - 986 - </body> 987 - </html>
-232
docs/ui-overhaul-option-a.md
··· 1 - # Option A: "Clean Craft" — Modern Minimal with Warmth 2 - 3 - **Vibe:** A well-designed tool for coffee people. Clean surfaces, generous whitespace, subtle depth. Feels professional without losing the coffee identity. 4 - 5 - **Core principle:** Remove visual noise so the *data* becomes the design. 6 - 7 - ## Design Direction 8 - 9 - Strip away gradients, heavy shadows, and nested containers. Replace with flat white cards on a warm cream background. Let typography weight and spacing create hierarchy instead of color and depth. 10 - 11 - The monospace font stays everywhere — it's the brand. But we size it down (monospace reads ~15% larger than proportional) and use weight contrast more aggressively to create hierarchy. 12 - 13 - ## Color Changes 14 - 15 - | Token | Current | Proposed | Reason | 16 - |-------|---------|----------|--------| 17 - | Page background | `brown-50` (#fdf8f6) | `#FAF7F5` (new `cream`) | Warmer, less pink-tinted | 18 - | Card background | gradient brown-100→200 | `#FFFFFF` flat white | Cards pop via contrast with cream bg | 19 - | Card border | `brown-300` | `brown-200` | Lighter border, less "boxed in" | 20 - | Card shadow | `shadow-xl` | `shadow-sm`, `shadow-md` on hover | Quieter at rest, responsive on interaction | 21 - | Feed card bg | gradient brown-50→100 | `#FFFFFF` flat white | Same as primary cards | 22 - | Table bg | gradient brown-100→200 | `#FFFFFF` with `brown-100` header | Clean, scannable | 23 - | Modal bg | gradient brown-100→200 | `#FFFFFF` | Consistent with card treatment | 24 - 25 - Keep the existing brown palette for text, borders, and accents. Keep amber for ratings and badges. The palette itself is good — the problem is overuse of mid-tones as backgrounds. 26 - 27 - ## Typography Changes 28 - 29 - | Element | Current | Proposed | 30 - |---------|---------|----------| 31 - | Page title | `text-3xl font-bold` | `text-2xl font-semibold` | 32 - | Section title | `text-2xl font-bold` | `text-base font-semibold uppercase tracking-wider text-brown-500` | 33 - | Card heading | `text-xl font-bold` | `text-base font-semibold` | 34 - | Body text | `text-base` | `text-sm` | 35 - | Meta/labels | `text-xs` | `text-xs font-medium text-brown-500` | 36 - 37 - Rationale: Monospace at `text-base` (16px) feels large. Dropping body to `text-sm` (14px) and headings proportionally gives the same visual weight as a proportional font at standard sizes. 38 - 39 - Section titles become small uppercase labels — a common pattern in tools like Linear, Notion, and GitHub that creates clear sections without shouting. 40 - 41 - ## Card System 42 - 43 - ### Current 44 - ``` 45 - .card = gradient bg + rounded-xl + shadow-xl + border-brown-300 46 - .feed-card = gradient bg + rounded-lg + shadow-md + border-brown-200 47 - .section-box = bg-brown-50 + rounded-lg + border-brown-200 48 - ``` 49 - 50 - Three card types, all slightly different. Feed cards have a nested `.feed-content-box` inside them creating card-in-card. 51 - 52 - ### Proposed 53 - ``` 54 - .card = bg-white rounded-xl border border-brown-200 shadow-sm 55 - hover:shadow-md transition-shadow 56 - .card-sm = bg-white rounded-lg border border-brown-200 shadow-sm 57 - .feed-card = bg-white rounded-lg border border-brown-200 shadow-sm 58 - hover:shadow-md transition-shadow 59 - ``` 60 - 61 - Key changes: 62 - - **Kill the nested content box.** Feed card content lives directly in the card. No `.feed-content-box` wrapper. 63 - - **One visual language.** All cards are white, rounded, with thin borders and minimal shadow. 64 - - **Type indicator via left border.** Feed cards get a `border-l-3` colored by record type: 65 - - Brew: `brown-700` 66 - - Bean: `amber-600` 67 - - Recipe: `brown-500` 68 - - Roaster/Grinder/Brewer: `brown-400` 69 - 70 - This replaces the need for emoji or labels to distinguish record types at a glance. 71 - 72 - ### Section box 73 - ``` 74 - .section-box = bg-brown-50/50 rounded-lg p-4 75 - (no border — just the subtle background tint) 76 - ``` 77 - 78 - Used inside detail views for grouping related data. Lighter treatment than a card. 79 - 80 - ## Button System 81 - 82 - ### Current (5 variants) 83 - ``` 84 - .btn-primary = gradient brown-700→900 + shadow-md 85 - .btn-secondary = bg-brown-300 86 - .btn-tertiary = gradient brown-500→600 87 - .btn-link = text underline 88 - .btn-danger = red text underline 89 - ``` 90 - 91 - ### Proposed (3 variants) 92 - ``` 93 - .btn-primary = bg-brown-800 text-white rounded-lg 94 - hover:bg-brown-900 transition-colors 95 - .btn-secondary = bg-white border border-brown-300 text-brown-700 rounded-lg 96 - hover:bg-brown-50 transition-colors 97 - .btn-danger = text-red-600 hover:text-red-800 underline 98 - ``` 99 - 100 - Drop gradients on buttons. Drop `.btn-tertiary` — audit uses and convert to primary or secondary. The link-style `.btn-link` merges into the general `.link` class. 101 - 102 - ## Form System 103 - 104 - ### Current 105 - ``` 106 - .form-input = border-2 border-brown-300 rounded-lg 107 - focus:border-brown-600 focus:ring-brown-600 108 - ``` 109 - 110 - ### Proposed 111 - ``` 112 - .form-input = border border-brown-300 rounded-lg bg-white 113 - focus:border-brown-600 focus:ring-1 focus:ring-brown-600 114 - focus:bg-brown-50/30 115 - placeholder:text-brown-400 116 - ``` 117 - 118 - Changes: 119 - - Border from 2px to 1px (less heavy) 120 - - Subtle background tint on focus (instead of just border change) 121 - - Keep the focus `translateY(-1px)` lift — it's a nice touch 122 - - Ring reduced to `ring-1` (thinner, more refined) 123 - 124 - ## Shadow Depth Scale 125 - 126 - | Level | Class | Usage | 127 - |-------|-------|-------| 128 - | 0 | `shadow-none` | Flat surfaces, inline elements | 129 - | 1 | `shadow-sm` | Cards at rest, tables, section boxes | 130 - | 2 | `shadow-md` | Hovered cards, dropdowns, action menus | 131 - | 3 | `shadow-lg` | Modals, popovers, floating UI | 132 - 133 - Current uses shadow-sm through shadow-2xl inconsistently. This simplifies to 3 levels with clear rules. 134 - 135 - ## Navigation 136 - 137 - **Keep** the dark brown gradient header — it's a strong anchor that works well. 138 - 139 - **Refine:** 140 - - Reduce ALPHA badge prominence (smaller, `text-[10px]`) 141 - - Replace hard `border-b` with `shadow-sm` for softer separation 142 - - Shrink header height slightly (48px instead of ~56px) 143 - 144 - ## Feed Cards — Detailed Layout 145 - 146 - ``` 147 - ┌─ border-l-3 brown-700 ─────────────────┐ 148 - │ │ 149 - │ ○ Display Name · @handle · 2h │ 150 - │ brewed with Ethiopian Sidamo │ 151 - │ │ 152 - │ ┌─ bg-brown-50/50 ──────────────────┐ │ 153 - │ │ Sweet Bloom · V60 │ │ 154 - │ │ 15g → 250g · 1:16.7 · ⭐ 8.5 │ │ 155 - │ │ │ │ 156 - │ │ "Bright citrus, chocolate finish" │ │ 157 - │ └────────────────────────────────────┘ │ 158 - │ │ 159 - │ 💬 3 ♡ 12 ↗ Share │ 160 - └──────────────────────────────────────────┘ 161 - ``` 162 - 163 - The inner area uses a section-box (subtle bg tint, no border) instead of the current bordered content box. Action bar has no top border — just spacing. 164 - 165 - ## Table Styling 166 - 167 - ### Current 168 - ``` 169 - .table-container = gradient bg + shadow-md + border 170 - .table-header = bg-brown-200 171 - .table-body = bg-brown-100 172 - ``` 173 - 174 - ### Proposed 175 - ``` 176 - .table-container = bg-white rounded-lg border border-brown-200 shadow-sm overflow-hidden 177 - .table-header = bg-brown-50 178 - .table-body = bg-white divide-y divide-brown-100 179 - .table-row = hover:bg-brown-50 transition-colors 180 - ``` 181 - 182 - Clean, standard table styling. Header is barely tinted. Rows divide with thin lines. Hover highlights the row. 183 - 184 - ## Animations 185 - 186 - **Keep:** Staggered feed card entry, modal transitions, like pop/shrink, form focus lift. 187 - 188 - **Remove:** Table row stagger (too much motion for data tables — feels gimmicky). 189 - 190 - **Add:** Subtle `opacity` transition on card border-left color when filtering feed by type. 191 - 192 - ## Implementation Phases 193 - 194 - ### Phase 1: Foundation (CSS-only, no template changes) 195 - 1. Update `tailwind.config.js` — add `cream` color 196 - 2. Rewrite card/button/form/table classes in `app.css` 197 - 3. Update `layout.templ` body background 198 - 4. Bump CSS cache version 199 - 200 - ### Phase 2: Template Cleanup 201 - 1. Remove `.feed-content-box` wrappers from feed templates 202 - 2. Add `border-l-3` type indicators to feed cards 203 - 3. Simplify section titles (uppercase label pattern) 204 - 4. Remove `.btn-tertiary` uses 205 - 206 - ### Phase 3: Detail Polish 207 - 1. Refine form layouts 208 - 2. Update modal content styling 209 - 3. Audit shadow usage across all templates 210 - 4. Test responsive behavior 211 - 212 - ## Tradeoffs 213 - 214 - | Pro | Con | 215 - |-----|-----| 216 - | Immediately more professional | Less personality than current | 217 - | Easier to maintain (fewer special cases) | Could feel generic if not careful | 218 - | Better feed scannability at scale | Left-border type system is a new concept to learn | 219 - | Lighter page weight (no gradients) | White cards on cream is a common pattern | 220 - | Clear visual hierarchy | Less "cozy coffee shop" feeling | 221 - | Works well with future dark mode | Requires discipline to not drift back to decoration | 222 - 223 - ## Risk: Becoming Generic 224 - 225 - The biggest risk with clean minimal is looking like every other SaaS tool. Mitigations: 226 - - **Monospace font** is the primary differentiator — keep it everywhere 227 - - **Brown palette** prevents blue/gray sameness 228 - - **Grain overlay** (keep at current opacity) adds tactile quality 229 - - **Left-border accents** give the feed a distinctive pattern 230 - - **Dark nav** provides a strong visual anchor 231 - 232 - The identity comes from the font + color combination, not from gradients and shadows.
-246
docs/ui-overhaul-option-b.md
··· 1 - # Option B: "Roasted" — Bold, Dark, Editorial 2 - 3 - **Vibe:** Specialty coffee packaging meets editorial magazine. Dark surfaces with warm highlights. The app *feels* like coffee — like opening a bag of fresh beans. 4 - 5 - **Core principle:** High contrast and bold typography make data dramatic. 6 - 7 - ## Design Direction 8 - 9 - Invert the current model. Instead of brown-tinted light backgrounds, go dark. Deep espresso-brown as the base with cream/white cards floating above. The dark ground creates natural depth without shadows. Typography goes bigger and bolder — every page has a clear headline moment. 10 - 11 - The monospace font becomes a statement rather than a quirk. At large sizes against dark backgrounds, monospace reads as intentional and editorial. 12 - 13 - ## Color System 14 - 15 - ### New Dark Palette 16 - 17 - | Token | Hex | Usage | 18 - |-------|-----|-------| 19 - | `espresso-950` | `#0F0A08` | Deepest background (page bg) | 20 - | `espresso-900` | `#1C1210` | Card backgrounds, primary surface | 21 - | `espresso-850` | `#241A16` | Elevated surfaces, hover states | 22 - | `espresso-800` | `#2E211B` | Borders, dividers | 23 - | `espresso-700` | `#3D2D24` | Secondary borders, subtle elements | 24 - | `cream-50` | `#FAF7F5` | Primary text on dark | 25 - | `cream-100` | `#F2E8E0` | Secondary text on dark | 26 - | `cream-200` | `#E0CEC4` | Muted text, labels | 27 - | `cream-300` | `#C4A898` | Placeholder text, disabled | 28 - | `amber-400` | `#FBBF24` | Primary accent — ratings, highlights | 29 - | `ember-500` | `#C4553A` | Secondary accent — actions, CTAs | 30 - | `ember-600` | `#A3412D` | Hover state for ember accent | 31 - 32 - ### Usage Rules 33 - 34 - - **Dark surfaces layered:** Page (`950`) → Section (`900`) → Card (`850`) creates depth without shadows 35 - - **Warm borders:** `espresso-800` borders, never gray 36 - - **Text contrast:** `cream-50` for headings (≥7:1 ratio), `cream-100` for body (≥4.5:1), `cream-200` for meta 37 - - **Accent restraint:** Amber for data (ratings, stats). Ember for interactive (buttons, links). Never both in the same element. 38 - - **No gradients on surfaces.** Flat dark colors. Gradients only on accent elements (primary button, hero treatments). 39 - 40 - ## Typography Changes 41 - 42 - | Element | Current | Proposed | 43 - |---------|---------|----------| 44 - | Page title | `text-3xl font-bold` | `text-3xl font-semibold text-cream-50 tracking-tight` | 45 - | Section title | `text-2xl font-bold` | `text-lg font-semibold text-amber-400 uppercase tracking-widest` | 46 - | Card heading | `text-xl font-bold` | `text-lg font-semibold text-cream-50` | 47 - | Body text | `text-base` | `text-sm text-cream-100` | 48 - | Meta/labels | `text-xs` | `text-xs font-medium text-cream-300 uppercase tracking-wider` | 49 - | Data values | same as body | `text-sm font-medium text-cream-50 tabular-nums` | 50 - 51 - Key difference from Option A: **Section titles use amber accent** as a color label, giving each section a warm highlight. Data values get their own treatment — medium weight, tabular numbers — because in a coffee tracking app, the numbers *are* the content. 52 - 53 - ## Card System 54 - 55 - ### Proposed 56 - ``` 57 - .card = bg-espresso-900 rounded-xl border border-espresso-800 58 - (no shadow — depth comes from surface layering) 59 - .card-hover = hover:bg-espresso-850 hover:border-espresso-700 transition-colors 60 - .feed-card = bg-espresso-900 rounded-lg border border-espresso-800 61 - hover:bg-espresso-850 transition-colors 62 - ``` 63 - 64 - **No shadows at all on cards.** Dark themes get depth from layered surface colors (Material Design 3 dark theme pattern). Shadows on dark backgrounds look muddy. 65 - 66 - **Content areas** inside cards use a slightly lighter surface: 67 - ``` 68 - .card-inset = bg-espresso-850 rounded-lg p-3 69 - (no border — just the shade difference) 70 - ``` 71 - 72 - This replaces both `.section-box` and `.feed-content-box` — a single "recessed area" concept. 73 - 74 - ### Type indicator 75 - Instead of left-border (which can get lost on dark), use a **small colored dot** before the action text: 76 - - Brew: `amber-400` dot 77 - - Bean: `cream-200` dot 78 - - Recipe: `ember-500` dot 79 - 80 - Or a subtle top-border accent (2px) on the card — visible but not dominant. 81 - 82 - ## Button System 83 - 84 - ### Proposed 85 - ``` 86 - .btn-primary = bg-gradient-to-r from-ember-500 to-ember-600 text-cream-50 rounded-lg 87 - hover:from-ember-600 hover:to-ember-600 transition-all 88 - .btn-secondary = bg-espresso-850 border border-espresso-700 text-cream-100 rounded-lg 89 - hover:bg-espresso-800 hover:border-espresso-700 transition-colors 90 - .btn-danger = text-red-400 hover:text-red-300 underline 91 - ``` 92 - 93 - Primary button gets the one gradient in the system — the warm ember accent. This makes CTAs unmistakable against the dark background. Secondary buttons are ghost-style (slightly lighter than the surface). 94 - 95 - ## Form System 96 - 97 - ``` 98 - .form-input = bg-espresso-850 border border-espresso-700 rounded-lg 99 - text-cream-50 placeholder:text-cream-300 100 - focus:border-amber-400 focus:ring-1 focus:ring-amber-400 101 - ``` 102 - 103 - Dark inputs with amber focus ring. The focus state is dramatic and clear — amber on dark brown is high contrast without being harsh. 104 - 105 - **Form labels:** `text-cream-200 text-xs font-medium uppercase tracking-wider` — small, quiet, functional. 106 - 107 - ## Navigation 108 - 109 - ### Proposed 110 - The current dark header is already close to the right direction. Refinements: 111 - 112 - ``` 113 - Header bg: espresso-950 (deepest dark, matches page) 114 - with a subtle bottom border in espresso-800 115 - ``` 116 - 117 - Since the page is now dark too, the header blends seamlessly. It's separated by the border, not a background change. This creates a more immersive, app-like feel. 118 - 119 - **ALPHA badge:** `bg-amber-400 text-espresso-950` (inverted — amber background, dark text). Small, punchy. 120 - 121 - **User dropdown:** `bg-espresso-900 border border-espresso-800` — matches card styling. 122 - 123 - ## Feed Cards — Detailed Layout 124 - 125 - ``` 126 - ┌─ bg-espresso-900 border-espresso-800 ───┐ 127 - │ │ 128 - │ ○ Display Name · @handle · 2h │ 129 - │ ● brewed with Ethiopian Sidamo │ ← amber dot for brew type 130 - │ │ 131 - │ ┌─ bg-espresso-850 ─────────────────┐ │ 132 - │ │ Sweet Bloom · V60 │ │ ← cream-100 text 133 - │ │ 15g → 250g · 1:16.7 │ │ ← cream-50 data values 134 - │ │ │ │ 135 - │ │ ⭐ 8.5 │ │ ← amber badge 136 - │ │ │ │ 137 - │ │ "Bright citrus, chocolate finish" │ │ ← cream-200 italic 138 - │ └────────────────────────────────────┘ │ 139 - │ │ 140 - │ 💬 3 ♡ 12 ↗ Share │ ← cream-300, hover cream-50 141 - └──────────────────────────────────────────┘ 142 - ``` 143 - 144 - The inset area (`.card-inset`) provides visual grouping without borders. On dark backgrounds, even a small shade difference reads clearly. 145 - 146 - ## Table Styling 147 - 148 - ``` 149 - .table-container = bg-espresso-900 rounded-lg border border-espresso-800 overflow-hidden 150 - .table-header = bg-espresso-850 border-b border-espresso-800 151 - .table-th = text-cream-300 text-xs font-medium uppercase tracking-wider 152 - .table-body = divide-y divide-espresso-800 153 - .table-row = hover:bg-espresso-850 transition-colors 154 - .table-td = text-cream-100 text-sm 155 - ``` 156 - 157 - Clean, dark table. Header row is barely differentiated. Dividers between rows. On hover, rows lighten slightly. 158 - 159 - ## Animations 160 - 161 - **Keep:** Staggered feed card entry, modal transitions, like pop/shrink. 162 - 163 - **Modify:** 164 - - Feed card entry: Use `opacity` + `translateY(6px)` (shorter travel on dark — movement reads more clearly against dark backgrounds) 165 - - Modal backdrop: `bg-black/60` (needs to be darker since the page is already dark) 166 - 167 - **Add:** 168 - - Subtle `glow` on amber accent elements: `box-shadow: 0 0 20px rgba(251,191,36,0.1)` — very subtle warm halo 169 - - Card hover: border transitions from `espresso-800` to `espresso-700` (warm reveal) 170 - 171 - **Remove:** Table row stagger, form focus lift (feels odd on dark). 172 - 173 - ## Texture & Atmosphere 174 - 175 - **Grain overlay:** Increase from `0.025` to `0.04` opacity. Grain reads better on dark backgrounds and adds significant tactile quality. This is one of the biggest differentiators — the paper-grain-on-dark effect feels like a coffee bag or craft packaging. 176 - 177 - **Optional:** Subtle warm vignette on the page body: 178 - ```css 179 - body::after { 180 - content: ""; 181 - position: fixed; 182 - inset: 0; 183 - pointer-events: none; 184 - background: radial-gradient(ellipse at center, transparent 50%, rgba(15,10,8,0.3) 100%); 185 - } 186 - ``` 187 - 188 - This darkens the edges slightly, creating a cozy, focused feel. Can be skipped if it feels heavy. 189 - 190 - ## Implementation Phases 191 - 192 - ### Phase 1: Foundation 193 - 1. Extend `tailwind.config.js` with `espresso` and `cream` color scales 194 - 2. Rewrite `app.css` component classes for dark surfaces 195 - 3. Update `layout.templ` body background + text colors 196 - 4. Update `header.templ` to match dark theme 197 - 5. Bump CSS cache version 198 - 199 - ### Phase 2: Template Updates 200 - 1. Update all page templates — swap brown-* text utilities to cream-* 201 - 2. Replace `.feed-content-box` with `.card-inset` 202 - 3. Update form styling (dark inputs, amber focus) 203 - 4. Update modal styling 204 - 205 - ### Phase 3: Polish 206 - 1. Audit contrast ratios (WCAG AA minimum) 207 - 2. Add subtle glow effects on accent elements 208 - 3. Tune grain overlay opacity 209 - 4. Test all states (hover, focus, active, disabled) on dark 210 - 211 - ### Phase 4: Accessibility Audit 212 - Dark themes have higher risk of contrast failures. Must verify: 213 - - All text meets WCAG AA (4.5:1 for body, 3:1 for large text) 214 - - Focus indicators are visible 215 - - Disabled states are distinguishable 216 - - Form validation errors are readable 217 - 218 - ## Tradeoffs 219 - 220 - | Pro | Con | 221 - |-----|-----| 222 - | Extremely distinctive — memorable identity | Harder to implement correctly (contrast, accessibility) | 223 - | Dark mode is practical (morning/evening brew logging) | More CSS to maintain (dark needs different strategies) | 224 - | High contrast makes data pop | Polarizing — some users hate dark UIs | 225 - | Grain texture reads beautifully on dark | Heavier visual treatment, more code for atmosphere | 226 - | Feels premium, like specialty coffee packaging | Template changes are more extensive (every text color) | 227 - | Monospace font becomes a bold statement | No easy "light mode" toggle without a full second theme | 228 - | Natural depth from surface layering (no shadows needed) | Photos/avatars need extra treatment to not look jarring | 229 - 230 - ## Risk: Too Dark / Oppressive 231 - 232 - The biggest risk is the UI feeling heavy or hard to read. Mitigations: 233 - - **Cream text, not white.** Pure white (#fff) on dark brown is harsh. Warm cream (#FAF7F5) reduces eye strain. 234 - - **Layered surfaces** prevent "black void" feeling — there's always subtle differentiation. 235 - - **Amber accents** add warmth and break up the dark expanse. 236 - - **Generous spacing** — dark UIs need more whitespace (darkspace?) to breathe than light ones. 237 - - **Grain overlay** prevents "screen" feeling, adds organic quality. 238 - 239 - ## Risk: Light Mode Demand 240 - 241 - If users request light mode later, you'd need to: 242 - 1. Define all component colors via CSS custom properties (not Tailwind classes directly) 243 - 2. Create a parallel set of light-theme values 244 - 3. Use `prefers-color-scheme` or a toggle 245 - 246 - This is significant work. If you think light mode will be needed within 6 months, Option A is a safer starting point (and can *add* dark mode later more easily than B can add light mode).