diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index b360c69..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" - } -} diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index b9e748c..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(powershell -Command \"$lines = Get-Content ''4-vitals-monitor.html''; $before = $lines[0..1757]; $after = $lines[2028..\\($lines.Length-1\\)]; \\($before + $after\\) | Set-Content ''4-vitals-monitor.html'' -Encoding UTF8\")", - "Bash(powershell -Command \"$lines = Get-Content ''4-vitals-monitor.html''; $before = $lines[0..1757]; $after = $lines[2028..\\($lines.Length-1\\)]; $result = $before + $after; $result | Set-Content ''4-vitals-monitor.html'' -Encoding UTF8\")", - "Bash(powershell -ExecutionPolicy Bypass -File:*)", - "Bash(del \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\cv-4-vitals-monitor\\\\remove-lines.ps1\")", - "Bash(start \"\" \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\cv-4-vitals-monitor\\\\4-vitals-monitor.html\")", - "Bash(npx skills find:*)", - "WebSearch", - "Bash(ls \"C:\\\\Users\\\\Andy\\\\Ralph Local\\\\Tasks\\\\New CV website\\\\designs\"\" 2>nul || echo \"Directory does not exist \")", - "Bash(npm run typecheck:*)", - "Bash(npm run dev:*)", - "Bash(npm run build:*)", - "Bash(dir:*)", - "mcp__playwright__browser_snapshot", - "mcp__playwright__browser_navigate", - "mcp__playwright__browser_take_screenshot", - "Bash(npm run lint:*)", - "Bash(curl:*)", - "mcp__playwright__browser_click", - "mcp__playwright__browser_wait_for", - "mcp__playwright__browser_evaluate", - "Bash(git add:*)", - "Bash(git commit -m \"$\\(cat <<''EOF''\nTask 4: Rebuild PatientBanner with premium fonts, tooltip, and animations\n\n- Replace font-inter with font-ui \\(Elvaro Grotesque\\) throughout banner\n- Add custom NHSNumberWithTooltip with Framer Motion animated reveal\n- Add AnimatePresence crossfade between full/condensed banner states\n- Animate mobile overflow menu enter/exit\n- Add SkipButton to App.tsx for boot/ECG phase skip\n- Add shadow-pmr-banner, focus ring styles, prefers-reduced-motion support\n- Fix mobile banner to use patient data instead of hardcoded values\n\nCo-Authored-By: Claude Opus 4.6 \nEOF\n\\)\")", - "Bash(git commit:*)", - "Bash(ls:*)", - "Bash(tasklist:*)", - "Bash(npx -y serve -l 3333 .)", - "Bash(npx serve:*)", - "Bash(timeout /t 3 /nobreak)", - "Bash(jq:*)", - "Bash(git stash:*)", - "Bash(npx tsc:*)", - "mcp__context7__resolve-library-id", - "Bash(wc:*)", - "Bash(python3:*)", - "Bash(pip install:*)", - "Bash(python scripts/generate_demo_data.py:*)", - "Bash(sqlite3:*)", - "Bash(python:*)", - "Bash(grep:*)", - "WebFetch(domain:www.embla-carousel.com)", - "Bash(npm ls:*)", - "Bash(npm install:*)", - "mcp__plugin_playwright_playwright__browser_navigate", - "mcp__plugin_playwright_playwright__browser_take_screenshot", - "mcp__plugin_playwright_playwright__browser_click", - "mcp__plugin_playwright_playwright__browser_wait_for", - "mcp__plugin_playwright_playwright__browser_snapshot", - "mcp__plugin_playwright_playwright__browser_resize", - "mcp__plugin_playwright_playwright__browser_evaluate", - "mcp__plugin_playwright_playwright__browser_press_key", - "Bash(npx eslint:*)", - "Bash(git checkout:*)", - "mcp__plugin_playwright_playwright__browser_hover", - "mcp__plugin_playwright_playwright__browser_run_code", - "WebFetch(domain:pagespeed.web.dev)", - "WebFetch(domain:www.googleapis.com)", - "Bash(find:*)", - "Bash(du -sh:*)", - "Bash(sudo apt-get install:*)", - "Bash(pdftotext:*)", - "Bash(pdftoppm:*)" - ] - } -} diff --git a/.claude/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md b/.claude/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md deleted file mode 100644 index d514f4e..0000000 --- a/.claude/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md +++ /dev/null @@ -1,111 +0,0 @@ -# Accessibility Essentials - -Accessibility enables creativity - it's a foundation, not a limitation. WCAG 2.1 AA compliance. - -## Core Principles (POUR) - -- **Perceivable**: Content must be perceivable (alt text, contrast, captions) -- **Operable**: UI must be keyboard/touch accessible -- **Understandable**: Clear, predictable behavior -- **Robust**: Works with assistive technologies - -## Contrast Requirements - -| Element | Minimum Ratio | -|---------|---------------| -| Normal text | 4.5:1 | -| Large text (18pt+) | 3:1 | -| UI components | 3:1 | - -**Tools**: Chrome DevTools Accessibility tab, WebAIM Contrast Checker - -## Keyboard Navigation - -```tsx -// All interactive elements need focus states - - -// Custom elements need tabindex and key handlers -
(e.key === 'Enter' || e.key === ' ') && handleClick()} -> - Custom Button -
-``` - -**Essentials:** -- Tab through entire interface -- Enter/Space activates elements -- Escape closes modals -- Visible focus indicators always - -## Essential ARIA - -```tsx -// Buttons without text - - -// Expandable elements - - -// Live regions for dynamic content -
{statusMessage}
-
{errorMessage}
- -// Form errors - -{hasError && } -``` - -## Semantic HTML - -```tsx -// Use semantic elements, not divs -
-

...

- - -// Heading hierarchy (never skip levels) -

Page Title

-

Section

-

Subsection

-``` - -## Touch Targets - -- Minimum **44x44px** for all interactive elements -- Adequate spacing between targets -- `touch-manipulation` CSS for responsive touch - -## Screen Reader Content - -```tsx -// Hidden but announced -Additional context - -// Skip link - - Skip to main content - -``` - -## Quick Checklist - -- [ ] Keyboard: Can tab through everything -- [ ] Focus: Visible focus indicators -- [ ] Contrast: 4.5:1 for text -- [ ] Alt text: All images have appropriate alt -- [ ] Headings: Logical h1-h6 hierarchy -- [ ] Forms: Labels associated with inputs -- [ ] Errors: Announced to screen readers -- [ ] Touch: 44px minimum targets - -## Resources - -- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/) -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) diff --git a/.claude/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md b/.claude/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md deleted file mode 100644 index e968748..0000000 --- a/.claude/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md +++ /dev/null @@ -1,577 +0,0 @@ -# Design System Template - -Meta-framework for understanding what's fixed, project-specific, and adaptable in your design system. - -## Purpose - -This template helps you distinguish between: -- **Fixed Elements**: Universal rules that never change -- **Project-Specific Elements**: Filled in for each project based on brand -- **Adaptable Elements**: Context-dependent implementations - ---- - -## I. FIXED ELEMENTS - -These foundations remain consistent across all projects, regardless of brand or context. - -### 1. Spacing Scale - -**Fixed System:** -``` -4px, 8px, 12px, 16px, 24px, 32px, 48px, 64px, 96px -``` - -**Usage:** -- Margins, padding, gaps between elements -- Mathematical relationships ensure visual harmony -- Use multipliers of base unit (4px) - -**Why Fixed:** -Consistent spacing creates visual rhythm regardless of brand personality. - -### 2. Grid System - -**Fixed Structure:** -- **12-column grid** for most layouts (divisible by 2, 3, 4, 6) -- **16-column grid** for data-heavy interfaces -- **Gutters**: 16px (mobile), 24px (tablet), 32px (desktop) - -**Why Fixed:** -Grid provides structural order. Brand personality shows through color, typography, content—not grid structure. - -### 3. Accessibility Standards - -**Fixed Requirements:** -- **WCAG 2.1 AA** compliance minimum -- **Contrast**: 4.5:1 for normal text, 3:1 for large text -- **Touch targets**: Minimum 44×44px -- **Keyboard navigation**: All interactive elements accessible -- **Screen reader**: Semantic HTML, ARIA labels where needed - -**Why Fixed:** -Accessibility is not negotiable. It's a baseline requirement for ethical, legal, and usable products. - -### 4. Typography Hierarchy Logic - -**Fixed Structure:** -- **Mathematical scaling**: 1.25x (major third) or 1.333x (perfect fourth) -- **Hierarchy levels**: Display → H1 → H2 → H3 → Body → Small → Caption -- **Line height**: 1.5x for body text, 1.2-1.3x for headlines -- **Line length**: 45-75 characters optimal - -**Why Fixed:** -Mathematical relationships create predictable, harmonious hierarchy. Specific fonts change, but the logic doesn't. - -### 5. Component Architecture - -**Fixed Patterns:** -- **Button states**: Default, Hover, Active, Focus, Disabled -- **Form structure**: Label above input, error below, helper text optional -- **Modal pattern**: Overlay + centered content + close mechanism -- **Card structure**: Container → Header → Body → Footer (optional) - -**Why Fixed:** -Users expect consistent component behavior. Architecture is fixed; appearance is project-specific. - -### 6. Animation Timing Framework - -**Fixed Physics Profiles:** -- **Lightweight** (icons, chips): 150ms -- **Standard** (cards, panels): 300ms -- **Weighty** (modals, pages): 500ms - -**Fixed Easing:** -- **Ease-out**: Entrances (fast start, slow end) -- **Ease-in**: Exits (slow start, fast end) -- **Ease-in-out**: Transitions (smooth both ends) - -**Why Fixed:** -Natural physics feel consistent across brands. Duration and easing create that feeling. - ---- - -## II. PROJECT-SPECIFIC ELEMENTS - -Fill in these for each project based on brand personality and purpose. - -### 1. Brand Color System - -**Template Structure:** - -``` -NEUTRALS (4-5 colors): -- Background lightest: _______ (e.g., slate-50 or warm-white) -- Surface: _______ (e.g., slate-100) -- Border/divider: _______ (e.g., slate-300) -- Text secondary: _______ (e.g., slate-600) -- Text primary: _______ (e.g., slate-900) - -ACCENTS (1-3 colors): -- Primary (main CTA): _______ (e.g., teal-500) -- Secondary (alternative action): _______ (optional) -- Status colors: - - Success: _______ (green-ish) - - Warning: _______ (amber-ish) - - Error: _______ (red-ish) - - Info: _______ (blue-ish) -``` - -**Questions to Answer:** -- What emotion should the brand evoke? (Trust, excitement, calm, urgency) -- Warm or cool neutrals? -- Conservative or bold accents? - -**Examples:** - -**Project A: Fintech App** -``` -Neutrals: Cool greys (slate-50 → slate-900) -Primary: Deep blue (#0A2463) – trust, professionalism -Success: Muted green (#10B981) -Why: Financial products need trust, not playfulness -``` - -**Project B: Creative Community** -``` -Neutrals: Warm greys with beige undertones -Primary: Coral (#FF6B6B) – energy, creativity -Success: Teal (#06D6A0) – fresh, unexpected -Why: Creative spaces should feel inviting, not corporate -``` - -**Project C: Healthcare Platform** -``` -Neutrals: Pure greys (minimal color temperature) -Primary: Soft blue (#4A90E2) – calm, clinical -Success: Medical green (#38A169) -Why: Healthcare needs clarity and calm, not distraction -``` - -### 2. Typography Pairing - -**Template:** - -``` -HEADLINE FONT: _______ -- Weight: _______ (e.g., Bold 700) -- Use case: H1, H2, display text -- Personality: _______ (geometric/humanist/serif/etc.) - -BODY FONT: _______ -- Weight: _______ (e.g., Regular 400, Medium 500) -- Use case: Paragraphs, UI text -- Personality: _______ (neutral/readable/efficient) - -OPTIONAL ACCENT FONT: _______ -- Weight: _______ -- Use case: _______ (special headlines, callouts) -``` - -**Pairing Logic:** -- Serif + Sans-serif (classic, editorial) -- Geometric + Humanist (modern + warm) -- Display + System (distinctive + efficient) - -**Examples:** - -**Project A: Editorial Platform** -``` -Headline: Playfair Display (Serif, Bold 700) -Body: Inter (Sans-serif, Regular 400) -Why: Serif headlines = trustworthy, editorial feel -``` - -**Project B: Tech Startup** -``` -Headline: DM Sans (Sans-serif, Bold 700) -Body: DM Sans (Regular 400, Medium 500) -Why: Single-font system = modern, efficient, cohesive -``` - -**Project C: Luxury Brand** -``` -Headline: Cormorant Garamond (Serif, Light 300) -Body: Lato (Sans-serif, Regular 400) -Why: Elegant serif + readable sans = sophisticated -``` - -### 3. Tone of Voice - -**Template:** - -``` -BRAND PERSONALITY: -- Formal ↔ Casual: _______ (1-10 scale) -- Professional ↔ Friendly: _______ (1-10 scale) -- Serious ↔ Playful: _______ (1-10 scale) -- Authoritative ↔ Conversational: _______ (1-10 scale) - -MICROCOPY EXAMPLES: -- Button label (submit form): _______ -- Error message (invalid email): _______ -- Success message (saved): _______ -- Empty state: _______ - -ANIMATION PERSONALITY: -- Speed: _______ (quick/moderate/slow) -- Feel: _______ (precise/smooth/bouncy) -``` - -**Examples:** - -**Project A: Banking App** -``` -Personality: Formal (8), Professional (9), Serious (8) -Button: "Submit Application" -Error: "Email address format is invalid" -Success: "Application submitted successfully" -Animation: Quick (precise, efficient, no-nonsense) -``` - -**Project B: Social App** -``` -Personality: Casual (8), Friendly (9), Playful (7) -Button: "Let's go!" -Error: "Hmm, that email doesn't look right" -Success: "Nice! You're all set 🎉" -Animation: Moderate (smooth, friendly bounce) -``` - -### 4. Animation Speed & Feel - -**Template:** - -``` -SPEED PREFERENCE: -- UI interactions: _______ (100-150ms / 150-200ms / 200-300ms) -- State changes: _______ (200ms / 300ms / 400ms) -- Page transitions: _______ (300ms / 500ms / 700ms) - -ANIMATION STYLE: -- Easing preference: _______ (sharp / standard / bouncy) -- Movement type: _______ (minimal / smooth / expressive) -``` - -**Examples:** - -**Project A: Trading Platform** -``` -Speed: Fast (100ms UI, 200ms states, 300ms pages) -Style: Sharp easing, minimal movement -Why: Traders need speed, not distraction -``` - -**Project B: Wellness App** -``` -Speed: Slow (200ms UI, 400ms states, 500ms pages) -Style: Smooth easing, gentle movement -Why: Calm, relaxing experience matches brand -``` - ---- - -## III. ADAPTABLE ELEMENTS - -Context-dependent implementations that vary based on use case. - -### 1. Component Variations - -**Button Variants:** -- **Primary**: Full background color (high emphasis) -- **Secondary**: Outline only (medium emphasis) -- **Tertiary**: Text only (low emphasis) -- **Destructive**: Red-ish (danger actions) -- **Ghost**: Minimal (navigation, toolbars) - -**Adaptation Rules:** -- Primary: Main CTA, one per screen section -- Secondary: Alternative actions -- Tertiary: Less important actions, multiple allowed -- Use brand colors, but hierarchy logic is fixed - -### 2. Responsive Breakpoints - -**Fixed Ranges:** -- XS: 0-479px (small phones) -- SM: 480-767px (large phones) -- MD: 768-1023px (tablets) -- LG: 1024-1439px (laptops) -- XL: 1440px+ (desktop) - -**Adaptable Implementations:** - -**Simple Content Site:** -``` -XS-SM: Single column -MD: 2 columns -LG-XL: 3 columns max -Why: Content-focused, don't overwhelm -``` - -**Dashboard/Data App:** -``` -XS: Collapsed, cards stack -SM: Simplified sidebar -MD: Full sidebar + main content -LG-XL: Sidebar + main + right panel -Why: Data apps need more screen real estate -``` - -### 3. Dark Mode Palette - -**Adaptation Strategy:** - -Not a simple inversion. Dark mode needs adjusted contrast: - -**Light Mode:** -``` -Background: #FFFFFF (white) -Text: #0F172A (slate-900) → 21:1 contrast -``` - -**Dark Mode (Adapted):** -``` -Background: #0F172A (slate-900) -Text: #E2E8F0 (slate-200) → 15.8:1 contrast (still AA, but softer) -``` - -**Why Adapt:** -Pure white on pure black is too harsh. Dark mode needs slightly lower contrast for eye comfort. - -### 4. Loading States - -**Context-Dependent:** - -**Fast operations (<500ms):** -- No loading indicator (feels instant) - -**Medium operations (500ms-2s):** -- Spinner or skeleton screen - -**Long operations (>2s):** -- Progress bar with percentage -- Or: Skeleton + estimated time - -**Interactive Operations:** -- Button shows spinner inside (don't disable, show state) - -### 5. Error Handling Strategy - -**Context-Dependent:** - -**Form Errors:** -``` -Validate: On blur (after user leaves field) -Display: Inline below field -Recovery: Clear error on fix -``` - -**API Errors:** -``` -Transient (network): Show retry button -Permanent (404): Show helpful message + next steps -Critical (500): Contact support option -``` - -**Data Errors:** -``` -Missing: Show empty state with action -Corrupt: Show error boundary with reload -Invalid: Highlight + explain what's wrong -``` - ---- - -## DECISION TREE - -When implementing a feature, ask: - -### Is this... - -**FIXED?** -- Does it affect structure, accessibility, or universal UX? -- Examples: Spacing scale, grid, contrast ratios, component architecture -- **Action**: Use the fixed system, no variation - -**PROJECT-SPECIFIC?** -- Does it express brand personality or purpose? -- Examples: Colors, typography, tone of voice, animation feel -- **Action**: Fill in the template for this project - -**ADAPTABLE?** -- Does it depend on context, content, or use case? -- Examples: Component variants, responsive behavior, error handling -- **Action**: Choose appropriate variation based on context - ---- - -## EXAMPLE: Implementing a "Submit" Button - -### Fixed Elements (Always the same): -- Touch target: 44px minimum height -- Padding: 16px horizontal (from spacing scale) -- States: Default, Hover, Active, Focus, Disabled -- Animation: 150ms ease-out (lightweight profile) - -### Project-Specific (Filled per project): -- **Project A (Bank)**: Dark blue background, white text, "Submit Application" -- **Project B (Social)**: Coral background, white text, "Let's Go!" -- **Project C (Healthcare)**: Soft blue background, white text, "Continue" - -### Adaptable (Context-dependent): -- **Form context**: Primary button (full color) -- **Toolbar context**: Ghost button (text only) -- **Danger context**: Destructive variant (red-ish) - ---- - -## VALIDATION CHECKLIST - -Before finalizing a design, check: - -### Fixed Elements -- [ ] Uses spacing scale (4/8/12/16/24/32/48/64/96px) -- [ ] Follows grid system (12 or 16 columns) -- [ ] Meets WCAG AA contrast (4.5:1 normal, 3:1 large) -- [ ] Touch targets ≥ 44px -- [ ] Typography follows mathematical scale -- [ ] Components follow standard architecture - -### Project-Specific Elements -- [ ] Brand colors filled in and intentional -- [ ] Typography pairing chosen and justified -- [ ] Tone of voice defined and consistent -- [ ] Animation speed matches brand personality - -### Adaptable Elements -- [ ] Component variants appropriate for context -- [ ] Responsive behavior fits content type -- [ ] Loading states match operation duration -- [ ] Error handling fits error type - ---- - -## PROJECT KICKOFF TEMPLATE - -Use this to start a new project: - -``` -PROJECT NAME: _______________________ -PURPOSE: ____________________________ - -BRAND PERSONALITY: -- Primary emotion: _______ -- Warm or cool: _______ -- Formal or casual: _______ -- Conservative or bold: _______ - -COLORS (fill the template): -- Neutral base: _______ -- Primary accent: _______ -- Status colors: _______ / _______ / _______ - -TYPOGRAPHY (fill the template): -- Headline font: _______ -- Body font: _______ -- Pairing rationale: _______ - -TONE: -- Button labels style: _______ -- Error message style: _______ -- Success message style: _______ - -ANIMATION: -- Speed preference: _______ (fast/moderate/slow) -- Feel preference: _______ (sharp/smooth/bouncy) - -TARGET DEVICES: -- Primary: _______ (mobile/desktop/both) -- Secondary: _______ -``` - ---- - -## MAINTAINING CONSISTENCY - -### Documentation -- Keep this template updated as system evolves -- Document WHY choices were made, not just WHAT - -### Communication -- Share with designers: "Here's what varies vs. what's fixed" -- Share with developers: "Here are the design tokens" - -### Tooling -- Use CSS variables for project-specific values -- Use Tailwind config for spacing scale -- Use design tokens in Figma/Storybook - -### Reviews -- Audit: Does new work follow fixed elements? -- Validate: Are project-specific elements intentional? -- Question: Are adaptations justified by context? - ---- - -## EXAMPLES OF COMPLETE SYSTEMS - -### System A: B2B SaaS (Conservative) - -**Fixed**: Standard spacing, 12-col grid, WCAG AA, major third type scale -**Project-Specific**: -- Colors: Cool greys + corporate blue -- Typography: DM Sans (headlines + body) -- Tone: Professional, formal -- Animation: Quick, precise (150ms) -**Adaptable**: -- Dashboard gets multi-panel layout -- Forms are extensive (use progressive disclosure) -- Errors show detailed technical info - -### System B: Consumer Social App (Playful) - -**Fixed**: Same spacing/grid/accessibility/type logic -**Project-Specific**: -- Colors: Warm greys + vibrant coral -- Typography: Poppins (headlines) + Inter (body) -- Tone: Casual, friendly, playful -- Animation: Moderate, bouncy (200ms) -**Adaptable**: -- Mobile-first (most users on phones) -- Forms are minimal (progressive profiling) -- Errors are friendly, not technical - -### System C: Healthcare Platform (Clinical) - -**Fixed**: Same foundational structure -**Project-Specific**: -- Colors: Pure greys + medical blue -- Typography: System fonts (SF Pro / Segoe) -- Tone: Clear, authoritative, calm -- Animation: Slow, smooth (300ms) -**Adaptable**: -- Desktop-first (clinical use at workstations) -- Forms are complex (HIPAA compliance) -- Errors are precise with next steps - ---- - -## KEY TAKEAWAY - -**The system flexibility framework lets you:** -- Maintain consistency (fixed elements) -- Express brand personality (project-specific) -- Adapt to context (adaptable elements) - -**Without this framework:** -- Designers reinvent spacing every project -- Components feel inconsistent across products -- Brand personality overrides accessibility -- Context-blind implementations feel wrong - -**With this framework:** -- Speed: Start from proven foundations -- Consistency: Fixed elements guarantee it -- Flexibility: Express unique brand identity -- Context: Adapt without breaking system diff --git a/.claude/skills/bencium-innovative-ux-designer/MOTION-SPEC.md b/.claude/skills/bencium-innovative-ux-designer/MOTION-SPEC.md deleted file mode 100644 index e37e363..0000000 --- a/.claude/skills/bencium-innovative-ux-designer/MOTION-SPEC.md +++ /dev/null @@ -1,72 +0,0 @@ -# Motion Specification - -Motion should surprise and delight while serving function. Animation is a creative tool. - -## Easing Curves - -| Easing | CSS | Use For | -|--------|-----|---------| -| **Ease-out** | `cubic-bezier(0.0, 0.0, 0.2, 1)` | Entrances, appearing | -| **Ease-in** | `cubic-bezier(0.4, 0.0, 1, 1)` | Exits, disappearing | -| **Ease-in-out** | `cubic-bezier(0.4, 0.0, 0.2, 1)` | State changes, transforms | -| **Spring** | `cubic-bezier(0.68, -0.55, 0.265, 1.55)` | Playful, attention-grabbing | -| **Linear** | `linear` | Spinners, continuous loops | - -## Duration by Element Weight - -| Weight | Duration | Examples | -|--------|----------|----------| -| **Lightweight** | 150ms | Icons, badges, chips | -| **Standard** | 300ms | Cards, panels, list items | -| **Weighty** | 500ms | Modals, page transitions | - -## Duration by Interaction - -| Interaction | Duration | -|-------------|----------| -| Button press | 100ms | -| Hover state | 150ms | -| Tooltip appear | 200ms | -| Tab switch | 250ms | -| Modal open | 300ms | -| Page transition | 400ms | - -## Common Patterns - -```tsx -// Hover transition (CSS) - -``` - -### Example 2: Typography Hierarchy - -**Question First:** -``` -For this content hierarchy, I'm thinking: -- Headline: Large, bold, attention-grabbing -- Subheading: Medium, regular weight -- Body: Smaller, optimized for reading - -Should we go bold and contemporary, or subtle and refined? -``` - -**Implementation After Approval:** -```tsx -
-

- Headline Here -

-

- Supporting subheading -

-

- Body text optimized for readability with proper line height. -

-
-``` - -### Example 3: Unique Color Palette Suggestion - -**Presenting Options:** -``` -I'd like to suggest three color directions that break from typical SaaS patterns: - -1. **Warm Earth Tones** - - Base: Warm grey (#E8E2DC) - - Accent: Terracotta (#C86E4B) - - For: Organic, trustworthy feel - -2. **Cool Midnight** - - Base: Deep navy (#1A2332) - - Accent: Cyan (#4ECDC4) - - For: Modern, tech-forward feel - -3. **Soft Pastels** - - Base: Soft pink (#FFE5E5) - - Accent: Sage green (#9DB5A4) - - For: Calm, approachable feel - -Which direction feels right for your brand? -``` - -## Common Patterns to Avoid - -❌ **NEVER:** -- Use Inter, Roboto, Arial, Space Grotesk as primary fonts -- Use generic SaaS blue (#3B82F6) or purple gradients on white -- Copy Apple's design language or use glass morphism -- Create cookie-cutter layouts that look AI-generated -- Skip asking about context before designing -- Converge on common choices across generations (vary everything!) -- Use animations that delay user actions -- Create cluttered interfaces where elements compete - -✅ **ALWAYS:** -- Ask about purpose, tone, constraints, differentiation FIRST -- Then commit BOLDLY to a distinctive aesthetic direction -- Use unexpected, characterful typography choices -- Create atmosphere: shadows, gradients, textures, grain (when intentional) -- Dominant colors with sharp accents (not timid, evenly-distributed palettes) -- Provide immediate feedback for interactions -- Test with real devices -- Validate accessibility (it enables creativity, not limits it) -- Remember: Claude is capable of extraordinary creative work - don't hold back! - -## Version History - -- v2.0.0 (2025-11-22): Creative liberation update - bold aesthetics, shadows/gradients allowed, Design Thinking protocol -- v1.0.0 (2025-10-18): Initial release with comprehensive UI/UX design guidance - -## References - -For additional context, see: -- **Anthropic Frontend Aesthetics Cookbook**: https://github.com/anthropics/claude-cookbooks/blob/main/coding/prompting_for_frontend_aesthetics.ipynb -- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ -- Google Fonts: https://fonts.google.com/ -- Tailwind CSS Docs: https://tailwindcss.com/docs -- Shadcn UI Components: https://ui.shadcn.com/ - -**Progressive Disclosure Files:** -- ACCESSIBILITY.md - Accessibility essentials (WCAG AA baseline) -- MOTION-SPEC.md - Animation timing and easing -- RESPONSIVE-DESIGN.md - Mobile-first breakpoints and patterns diff --git a/.claude/skills/claude-md-progressive-disclosurer/.security-scan-passed b/.claude/skills/claude-md-progressive-disclosurer/.security-scan-passed deleted file mode 100644 index d5a81c0..0000000 --- a/.claude/skills/claude-md-progressive-disclosurer/.security-scan-passed +++ /dev/null @@ -1,4 +0,0 @@ -Security scan passed -Scanned at: 2025-12-11T20:19:33.266025 -Tool: gitleaks + pattern-based validation -Content hash: 864b1b4fa2851e26012b06cd3bcb5eb8810ab2cfd3240ba5b48af1895ad182ce diff --git a/.claude/skills/claude-md-progressive-disclosurer/.skillfish.json b/.claude/skills/claude-md-progressive-disclosurer/.skillfish.json deleted file mode 100644 index b66c20e..0000000 --- a/.claude/skills/claude-md-progressive-disclosurer/.skillfish.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 2, - "name": "claude-md-progressive-disclosurer", - "owner": "daymade", - "repo": "claude-code-skills", - "path": "claude-md-progressive-disclosurer", - "branch": "main", - "sha": "4f20e980d6f0c88856b5b1dbadbdcf94108de0c2", - "source": "manual" -} \ No newline at end of file diff --git a/.claude/skills/claude-md-progressive-disclosurer/SKILL.md b/.claude/skills/claude-md-progressive-disclosurer/SKILL.md deleted file mode 100644 index de3e655..0000000 --- a/.claude/skills/claude-md-progressive-disclosurer/SKILL.md +++ /dev/null @@ -1,478 +0,0 @@ ---- -name: claude-md-progressive-disclosurer -description: | - Optimize CLAUDE.md files using progressive disclosure. - Goal: Maximize information efficiency, readability, and maintainability. - Use when: User wants to optimize CLAUDE.md, information is duplicated across files, or LLM repeatedly fails to follow rules. ---- - -# CLAUDE.md 渐进式披露优化器 - -## 核心理念 - -> "找到最小的高信号 token 集合,最大化期望结果的可能性。" — Anthropic - -**目标是最大化信息效率、可读性、可维护性。** - -### 铁律:禁止用行数作为评价指标 - -- 行数少不代表更好,行数多不代表更差 -- 优化的评判标准是:**单一信息源**(同一信息不在多处维护)、**认知相关性**(当前任务不需要的信息不干扰注意力)、**维护一致性**(改一处不需要同步另一处) -- 禁止在优化方案中出现"从 X 行精简到 Y 行"、"减少 Z%"等表述 -- 一个结构清晰、信息不重复的长文件,比一个砍掉关键信息的短文件更好 -- **禁止在工作流任何阶段运行 `wc -l` 或统计行数**——这会潜意识地将"行数少"当成目标 -- **禁止在完成后的总结中提及行数变化**——即使不是主要指标,提及行数也会暗示"行数减少=成功" - -### 两层架构 - -``` -Level 1 (CLAUDE.md) - 每次对话都加载 -├── 信息记录原则 ← 防止未来膨胀的自我约束 -├── Reference 索引(开头) ← 入口1:遇到问题查这里 -├── 核心命令表 -├── 铁律/禁令(含代码示例) -├── 常见错误诊断(症状→原因→修复) -├── 代码模式(可直接复制) -├── 目录映射(功能→文件) -├── 修改代码前必读 ← 入口2:改代码前查这里 -└── Reference 触发索引(末尾) ← 入口3:长对话后复述 - -Level 2 (references/) - 按需即时加载 -├── 详细 SOP 流程 -├── 边缘情况处理 -├── 完整配置示例 -└── 历史决策记录 -``` - -### 多入口原则(重要!) - -同一 Level 2 资源可以有**多个入口**,服务于不同查找路径: - -| 入口 | 位置 | 触发场景 | 用户心态 | -|------|------|----------|----------| -| Reference 索引 | 开头 | 遇到错误/问题 | "出 bug 了,查哪个文档?" | -| 修改代码前必读 | 中间 | 准备改代码 | "我要改 X,要注意什么?" | -| Reference 触发索引 | 末尾 | 长对话定位 | "刚才说的那个文档是哪个?" | - -**这不是重复,是多入口。** 就像书有目录(按章节)、索引(按关键词)、快速参考卡(按任务)。 - ---- - -## 优化工作流 - -### Step 1: 备份 - -```bash -cp CLAUDE.md CLAUDE.md.bak.$(date +%Y%m%d_%H%M%S) -``` - -### Step 2: 内容分类 - -对每个章节分类: - -| 问题 | 是 | 否 | -|------|----|-----| -| 高频使用? | Level 1 | ↓ | -| 违反后果严重? | Level 1 | ↓ | -| 有代码模式需要直接复制? | Level 1 保留模式 | ↓ | -| 有明确触发条件? | Level 2 + 触发条件 | ↓ | -| 历史/参考资料? | Level 2 | 考虑删除 | - -### Step 3: 创建 Reference 文件 - -命名:`docs/references/{主题}-sop.md` - -**铁律:原样移动,禁止压缩** - -移动内容到 Level 2 时,必须**完整保留原始内容**。不要在移动的同时"顺便精简"。 - -``` -✅ 正确:把 100 行原封不动搬到 Level 2(100 行 → Level 2 100 行) -❌ 错误:把 100 行"精简"到 60 行搬到 Level 2(100 行 → Level 2 60 行,40 行消失) -``` - -**为什么**:压缩 = 变相删除。你认为"不重要"而删掉的内容,可能是某个未来 debug session 的关键线索。优化的目标是**改变信息的位置**(Level 1 → Level 2),不是**改变信息的存在**。 - -**怎么做**: -1. 从原始 CLAUDE.md 中精确复制要移动的段落 -2. 原样粘贴到 Level 2 文件中 -3. 可以在 Level 2 中添加结构(标题、分隔线),但**不要删减、改写、合并**原始内容 -4. 如果确实有冗余(同一段话在原文中出现了多次),在 Level 2 中保留一份完整的,注释说明去重 - -### Step 4: 更新 Level 1 - -1. **在开头添加「信息记录原则」**(项目概述之后,Reference 索引之前) -2. **添加 Reference 索引**(紧随信息记录原则之后) -3. 用触发条件格式替换详细内容 -4. 保留代码模式和错误诊断 -5. **添加「修改代码前必读」表格**(按"要改什么"索引) -6. **在末尾再放一份触发索引表** - -### Step 5: 验证(三项全部通过才算完成) - -#### 5a. 引用文件存在性 - -```bash -# 检查引用文件存在 -grep -oh '`docs/references/[^`]*\.md`' CLAUDE.md | sed 's/`//g' | while read f; do - test -f "$f" && echo "✓ $f" || echo "✗ MISSING: $f" -done -``` - -#### 5b. 内容完整性(最关键) - -对每个从原始 CLAUDE.md 移走的章节,逐一检查: - -1. **恢复原始文件**:`git show HEAD:CLAUDE.md > /tmp/claude-md-original.md` -2. **逐节对比**:对原始文件的每个 `##` 章节,确认其内容在以下位置之一完整存在: - - 新 CLAUDE.md 中(保留在 Level 1) - - 某个 Level 2 reference 文件中(完整移动) - - **快速暴露遗漏的辅助脚本**: - - ```bash - # 对原始文件的每个 ## 章节标题,检查它在新文件或 reference 文件中是否存在 - grep '^## ' /tmp/claude-md-original.md | while read heading; do - if grep -q "$heading" CLAUDE.md docs/references/*.md 2>/dev/null; then - echo "✓ $heading" - else - echo "✗ NOT FOUND: $heading" - fi - done - ``` - - > ⚠️ 这个脚本**不能替代人工逐节对比**——它只检查章节标题是否存在,不检查内容是否完整。但它能快速暴露**整个章节被遗漏**的情况,作为人工对比前的第一道筛查。 - -3. **标记所有差异**: - - 如果某段内容在新文件中被缩短 → **必须补回被删减的部分** - - 如果某段内容在两个位置都不存在 → **必须补回** - - 唯一允许删除的情况:**该信息已有独立的 canonical source**(如 `docs/README.md` 已是文档索引的 canonical source),且在 Level 1 中有明确的指向 - -**禁止将"故意删除"作为分类来掩盖信息丢失。** 每一项"故意删除"都必须说明 canonical source 在哪里。如果说不出来,就不是"故意删除",而是"遗漏"。 - -#### 5c. 禁止行数审计 - -在验证阶段**不要统计行数**。不要 `wc -l`。不要计算"原始 X 行 vs 新 Y 行"。这些数字会扭曲你的判断。 - -验证的标准是: -- 每段信息都有归属(Level 1 或 Level 2 或 canonical source) -- 没有信息丢失 -- Level 2 引用都有触发条件 - ---- - -## Level 1 内容分类 - -### 🔴 绝对不能移走 - -| 内容类型 | 原因 | -|---------|------| -| **核心命令** | 高频使用 | -| **铁律/禁令** | 违反后果严重,必须始终可见 | -| **代码模式** | LLM 需要直接复制,避免重新推导 | -| **错误诊断** | 完整的症状→原因→修复流程 | -| **目录映射** | 帮助 LLM 快速定位文件 | -| **触发索引表** | 帮助 LLM 在长对话中定位 Level 2 | - -### 🟡 保留摘要 + 触发条件 - -| 内容类型 | Level 1 | Level 2 | -|---------|---------|---------| -| SOP 流程 | 触发条件 + 关键陷阱 | 完整步骤 | -| 配置示例 | 最常用的 1-2 个 | 完整配置 | -| API 文档 | 常用方法签名 | 完整参数说明 | - -### 🟢 可以完全移走 - -| 内容类型 | 原因 | -|---------|------| -| 历史决策记录 | 低频访问 | -| 性能数据 | 参考性质 | -| 技术债务清单 | 按需查看 | -| 边缘情况 | 有明确触发条件时再加载 | - ---- - -## 引用格式(四种) - -### 1. 详细格式(正文中的重要引用) - -```markdown -**📖 何时读 `docs/references/xxx-sop.md`**: -- [具体错误信息,如 `ERR_DLOPEN_FAILED`] -- [具体场景,如"添加新的原生模块时"] - -> 包含:[关键词 1]、[关键词 2]、[代码模板]。 -``` - -### 2. 问题触发表格(开头/末尾索引) - -```markdown -## Reference 索引(遇到问题先查这里) - -| 触发场景 | 文档 | 核心内容 | -|----------|------|---------| -| `ERR_DLOPEN_FAILED` | `native-modules-sop.md` | ABI 机制、懒加载 | -| 打包后 `Cannot find module` | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 3. 任务触发表格(修改代码前必读) - -```markdown -## 修改代码前必读 - -| 你要改什么 | 先读这个 | 关键陷阱 | -|-----------|---------|---------| -| 原生模块相关 | `native-modules-sop.md` | 必须懒加载;electron-rebuild 会静默失败 | -| 打包配置 | `packaging-sop.md` | DMG contents 必须用函数形式 | -``` - -### 4. 内联格式(简短引用) - -```markdown -完整流程见 `database-sop.md`(FTS5 转义、健康检查)。 -``` - -**多样性原则**:不要所有引用都用同一格式。 - ---- - -## 四条核心原则 - -### 原则 0:添加「信息记录原则」(防止未来膨胀) - -**问题**:优化完成后,用户会继续要求 Claude "记录这个信息到 CLAUDE.md",如果没有规则指导,CLAUDE.md 会再次膨胀。 - -**解决**:在 CLAUDE.md 开头(项目概述之后)添加「信息记录原则」: - -```markdown -## 信息记录原则(Claude 必读) - -本文档采用**渐进式披露**架构,优化 LLM 工作效能。 - -### Level 1(本文件)只记录 - -| 类型 | 示例 | -|------|------| -| 核心命令表 | `pnpm run restart` | -| 铁律/禁令 | 必须懒加载原生模块 | -| 常见错误诊断 | 症状→原因→修复(完整流程) | -| 代码模式 | 可直接复制的代码块 | -| 目录导航 | 功能→文件映射 | -| 触发索引表 | 指向 Level 2 的入口 | - -### Level 2(docs/references/)记录 - -| 类型 | 示例 | -|------|------| -| 详细 SOP 流程 | 完整的 20 步操作指南 | -| 边缘情况处理 | 罕见错误的诊断 | -| 完整配置示例 | 所有参数的说明 | -| 历史决策记录 | 为什么这样设计 | - -### 用户要求记录信息时 - -1. **判断是否高频使用**: - - 是 → 写入 CLAUDE.md(Level 1) - - 否 → 写入对应 reference 文件(Level 2) - -2. **Level 1 引用 Level 2 必须包含**: - - 触发条件(什么情况该读) - - 内容摘要(读了能得到什么) - -3. **禁止**: - - 在 Level 1 放置低频的详细流程 - - 引用 Level 2 但不写触发条件 -``` - -**原因**:这条规则让 Claude 自己知道什么该记在哪里,实现"自我约束",避免后续对话中 CLAUDE.md 再次膨胀。 - -### 原则 1:触发索引表放开头和末尾 - -**原因**:LLM 注意力呈 U 型分布——开头和末尾强,中间弱。 - -| 位置 | 作用 | -|------|------| -| **开头** | 对话开始时建立全局认知:"有哪些 Level 2 可用" | -| **末尾** | 对话变长后复述提醒:"现在应该读哪个 Level 2" | - -```markdown - -## Reference 索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | - -... (正文内容) ... - - -## Reference 触发索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 原则 2:引用必须有触发条件 - -**错误**:`详见 native-modules-sop.md` - -**正确**: -```markdown -**📖 何时读 `native-modules-sop.md`**: -- 遇到 `ERR_DLOPEN_FAILED` 错误 -- 需要添加新的原生模块 - -> 包含:ABI 机制、懒加载模式、手动修复命令 -``` - -**原因**:没有触发条件,LLM 不知道什么时候该去读。 - -### 原则 3:代码模式必须保留在 Level 1 - -**错误**:把代码示例移到 Level 2,Level 1 只写"使用懒加载模式"。 - -**正确**:Level 1 保留完整的可复制代码: -```javascript -// ✅ 正确:懒加载,只在需要时加载 -let _Database = null; -function getDatabase() { - if (!_Database) { - _Database = require("better-sqlite3"); - } - return _Database; -} -``` - -**原因**:LLM 需要直接复制代码,移走后每次都要重新推导或读取 Level 2。 - ---- - -## 反模式警告 - -### ⚠️ 反模式 1:以行数为目标的过度精简 - -**案例**:为了"减少行数",移走了代码模式、诊断流程、目录映射 - -**结果**: -- 丢失代码模式,LLM 每次重新推导 -- 丢失诊断流程,遇错不知查哪 -- 丢失目录映射,找文件效率低 - -**正确**:保留所有高频使用的内容。优化的判断标准是信息是否重复维护、是否与当前任务无关,而不是"文件太长"。 - -### ⚠️ 反模式 2:无触发条件的引用 - -**案例**:`详见 xxx.md` - -**问题**:LLM 不知道何时加载,要么忽略,要么每次都读。 - -**正确**:触发条件 + 内容摘要。 - -### ⚠️ 反模式 3:移走代码模式 - -**案例**:把常用代码示例移到 Level 2 - -**问题**:LLM 每次写代码都要先读 Level 2,增加延迟和 token 消耗。 - -**正确**:高频使用的代码模式保留在 Level 1。 - -### ⚠️ 反模式 4:删除而非移动 - -**案例**:删除"不重要"的章节 - -**问题**:信息丢失,未来需要时无处可查。 - -**正确**:移到 Level 2,保留触发条件。 - -### ⚠️ 反模式 5:用行数当 KPI - -**案例**:优化方案写"从 2000 行精简到 500 行,减少 75%" - -**问题**:把行数当成功指标,会驱动错误决策——为了凑数字而砍掉有用的信息。 - -**正确**:用信息质量评估优化效果——信息是否有重复?维护负担是否降低?LLM 是否能更快找到需要的信息? - -### ⚠️ 反模式 6:移动时压缩(变相删除) - -**规则**:移动是移动,精简是精简。这是两个独立操作,**不要同时执行**。 - -- 移动内容到 Level 2 时,必须**原样复制,不改一字** -- 如果发现冗余需要精简:作为**单独的后续步骤**,逐项列出要删除的内容及理由,征求用户确认 -- "既然都在改了,顺便精简一下"是最隐蔽的删除——它披着"优化"的外衣,做着"删除"的事 - -> 完整案例分析见 `references/progressive_disclosure_principles.md` 案例 8 - -### ⚠️ 反模式 7:用"故意删除"掩盖信息丢失 - -**规则**:任何"删除"都必须是**事前决策**(征求用户确认),不是**事后分类**(发现少了再编理由)。 - -- 对每项计划删除的内容,必须说明其 canonical source 在哪里 -- 如果无法指出 canonical source → 不是"故意删除",是"信息丢失",必须补回 -- 对丢失内容分类"严重性"(高/低风险)是在为自己的错误找台阶。正确的态度是:任何丢失都是 bug,fix it - -> 完整案例分析见 `references/progressive_disclosure_principles.md` 案例 9 - ---- - -## 信息量检验 - -### ✅ 正确的信息量 - -| 检验项 | 通过标准 | -|--------|---------| -| 日常命令 | 不需要读 Level 2 | -| 常见错误 | 有完整诊断流程 | -| 代码编写 | 有可复制的模式 | -| 特定问题 | 知道读哪个 Level 2 | -| 触发索引 | 在文档末尾,表格形式 | - -### ❌ 不足的信号 - -- LLM 反复问同样的问题 -- LLM 每次重新推导代码模式 -- 用户需要反复提醒规则 - -### ❌ 过多的信号 - -- 大段低频详细流程在 Level 1 -- **完全相同的内容**在多处(注意:多入口指向同一资源 ≠ 重复) -- 边缘情况和常见情况混在一起 - ---- - -## 项目级 vs 用户级 - -| 维度 | 用户级 | 项目级 | -|------|--------|--------| -| 位置 | `~/.claude/CLAUDE.md` | `项目/CLAUDE.md` | -| References | `~/.claude/references/` | `docs/references/` | -| 信息范围 | 个人偏好、全局规则 | 项目架构、团队规范 | - ---- - -## 快速检查清单 - -优化完成后,**必须逐项检查**(不可跳过): - -### 信息完整性(最重要) -- [ ] **原始文件的每个章节都有归属**——在新 Level 1、Level 2、或有明确 canonical source -- [ ] **Level 2 文件内容与原始内容完全一致**——没有在移动过程中被"精简" -- [ ] **没有任何内容被静默删除**——每项删除都有用户确认或明确的 canonical source -- [ ] **没有在任何阶段统计或提及行数变化** - -### 结构质量 -- [ ] 「信息记录原则」在文档开头(防止未来膨胀) -- [ ] Reference 索引在文档开头(入口1:遇到问题查这里) -- [ ] 核心命令表完整 -- [ ] 铁律/禁令有代码示例 -- [ ] 常见错误有完整诊断流程(症状→原因→修复) -- [ ] 代码模式可直接复制 -- [ ] 目录映射(功能→文件) -- [ ] 「修改代码前必读」表格(入口2:按"要改什么"索引) -- [ ] Reference 触发索引在文档末尾(入口3:长对话后复述) -- [ ] 每个 Level 2 引用都有触发条件 -- [ ] 引用的文件都存在 diff --git a/.claude/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md b/.claude/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md deleted file mode 100644 index e12ae30..0000000 --- a/.claude/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md +++ /dev/null @@ -1,319 +0,0 @@ -# 实践案例与教训 - -本文档记录优化 CLAUDE.md 过程中的实际案例和教训。 - ---- - -## 案例 1:以行数为目标的过度精简 - -### 背景 -某项目 CLAUDE.md 内容丰富,包含代码模式、诊断流程、目录映射等。 - -### 错误做法 -以"减少行数"为目标,移走了大部分内容,只保留简短描述和指针。 - -### 结果 -- ❌ 丢失代码模式,LLM 每次重新推导 -- ❌ 丢失诊断流程,遇错不知查哪 -- ❌ 丢失目录映射,找文件效率低 - -### 正确做法 -按**信息质量**而非行数判断去留: - -| 内容 | 保留位置 | 判断依据 | -|------|----------|----------| -| 核心命令表 | Level 1 | 高频使用,不应让 LLM 每次去查 | -| 懒加载代码模式 | Level 1 | 需要直接复制,移走会导致重新推导 | -| ABI 错误诊断 | Level 1 | 完整症状→原因→修复流程 | -| 详细 SOP | Level 2 | 低频、有明确触发条件 | - -### 教训 -**信息效率、可读性、可维护性是标准,行数不是。** - ---- - -## 案例 2:无触发条件的引用 - -### 错误做法 -```markdown -详见 native-modules-sop.md -``` - -### 问题 -LLM 不知道什么时候该去读这个文件。 - -### 正确做法 -```markdown -**📖 何时读 `native-modules-sop.md`**: -- 遇到 `ERR_DLOPEN_FAILED` 错误 -- 需要添加新的原生模块 - -> 包含:ABI 机制、懒加载模式、手动修复命令 -``` - -### 教训 -**每个引用必须有触发条件 + 内容摘要。** - ---- - -## 案例 3:代码模式被移走 - -### 错误做法 -Level 1 只写"使用懒加载模式",代码示例放 Level 2。 - -### 问题 -LLM 每次写代码都要先读 Level 2,或者凭记忆推导(可能出错)。 - -### 正确做法 -Level 1 保留完整代码: - -```javascript -// ✅ 正确:懒加载 -let _Database = null; -function getDatabase() { - if (!_Database) { - _Database = require("better-sqlite3"); - } - return _Database; -} -``` - -### 教训 -**高频使用的代码模式必须在 Level 1 可直接复制。** - ---- - -## 案例 4:触发索引表位置错误 - -### 错误做法 -触发索引表只放在 CLAUDE.md 中间某个位置。 - -### 问题 -LLM 注意力呈 U 型分布:开头和末尾强,中间弱。只放中间会被忽略。 - -### 正确做法 -触发索引表放在 CLAUDE.md **开头和末尾两个位置**: - -```markdown - -## Reference 索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | - -... (正文内容) ... - - -## Reference 触发索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 教训 -**三个入口服务于不同查找路径,这不是重复,是多入口。** - ---- - -## 案例 5:误删「修改代码前必读」 - -### 错误做法 -认为「Reference 索引」和「修改代码前必读」内容重复,删除后者。 - -### 问题 -两个表格服务于**不同的查找路径**: -- Reference 索引:按**错误/问题**触发("出 bug 了查哪个?") -- 修改代码前必读:按**要改的代码**触发("我要改 X,注意什么?") - -### 正确做法 -保留三个入口: -1. **开头 Reference 索引** - 遇到问题时查 -2. **修改代码前必读** - 准备改代码时查 -3. **末尾触发索引** - 长对话后定位 - -### 教训 -**多入口指向同一资源 ≠ 重复信息。** 就像书有目录、索引、快速参考卡。 - ---- - -## 案例 6:缺少信息记录原则 - -### 背景 -优化完成后,CLAUDE.md 结构清晰,信息分层合理。 - -### 问题 -后续用户继续要求 Claude "把这个记录到 CLAUDE.md",Claude 没有判断标准,只能照做。逐渐出现信息重复维护、低频内容和高频内容混杂的问题。 - -### 错误做法 -只优化内容,不添加规则。 - -### 正确做法 -在 CLAUDE.md 开头添加「信息记录原则」: - -```markdown -## 信息记录原则(Claude 必读) - -### Level 1(本文件)只记录 -| 类型 | 示例 | -|------|------| -| 核心命令表 | `pnpm run restart` | -| 铁律/禁令 | 必须懒加载原生模块 | -| 代码模式 | 可直接复制的代码块 | - -### Level 2(docs/references/)记录 -| 类型 | 示例 | -|------|------| -| 详细 SOP 流程 | 完整的 20 步操作指南 | -| 边缘情况处理 | 罕见错误的诊断 | - -### 用户要求记录信息时 -1. 判断是否高频使用 → 是则 Level 1,否则 Level 2 -2. Level 1 引用 Level 2 必须包含触发条件 -3. 禁止在 Level 1 放置低频详细流程 -``` - -### 教训 -**优化的目的是「以后不再需要优化」。** 添加规则让 Claude 自我约束,实现长期可持续。 - ---- - -## 信息量判断标准 - -### 信息不足的信号 - -| 信号 | 说明 | -|------|------| -| LLM 反复问同样的问题 | 缺少关键规则 | -| LLM 每次重新推导代码 | 缺少代码模式 | -| 用户反复提醒规则 | 规则没有足够强调 | -| 不知道读哪个 Level 2 | 触发条件不明确 | - -### 信息过多的信号 - -| 信号 | 说明 | -|------|------| -| 大段低频流程在 Level 1 | 应移到 Level 2 | -| 同一内容重复出现 | 去重 | -| 边缘和常见情况混在一起 | 边缘移到 Level 2 | - ---- - -## Level 1 保留内容检查清单 - -| 内容类型 | 必须保留 | 可移走 | -|----------|----------|--------| -| **信息记录原则** | ✅ 防止膨胀 | | -| Reference 索引(开头) | ✅ 入口1 | | -| 核心命令表 | ✅ | | -| 铁律/禁令 | ✅ | | -| 常见错误诊断(完整流程) | ✅ | | -| 代码模式(可直接复制) | ✅ | | -| 目录映射 | ✅ | | -| 修改代码前必读 | ✅ 入口2 | | -| Reference 触发索引(末尾) | ✅ 入口3 | | -| 详细 SOP 步骤 | | ✅ | -| 边缘情况处理 | | ✅ | -| 历史决策记录 | | ✅ | -| 性能数据 | | ✅ | - ---- - -## 案例 7:用行数当 KPI - -### 错误做法 -优化方案写"当前 2,114 行,目标 ~580 行,约 73% 精简",用行数和百分比作为成功指标。 - -### 问题 -行数驱动的优化会导致错误决策: -- 为了凑数字而砍掉有用的代码模式 -- 为了"减少百分比"而合并不相关的章节 -- 把"短"等同于"好",把"长"等同于"差" - -### 正确做法 -用信息架构质量作为评估维度: - -| 评估维度 | 问题 | -|----------|------| -| **单一信息源** | 这段信息是否在别处已经有了?如果是,消除重复 | -| **认知相关性** | 这段信息在大多数开发场景下是否需要?如果不是,移到 Level 2 | -| **维护一致性** | 改一处是否需要同步另一处?如果是,消除重复 | - -### 教训 -**行数少不代表更好,行数多不代表更差。真正的标准是信息效率、可读性、可维护性。** - ---- - -## 案例 8:移动时压缩导致信息丢失(真实事故,2026-02-14) - -### 背景 -一个 2503 行的 CLAUDE.md 需要优化。使用本 skill 的渐进式披露方法,创建了 6 个 Level 2 reference 文件。 - -### 错误做法 -在移动内容到 Level 2 文件时,LLM "顺便精简"了内容: - -| 原始章节 | 原始内容 | Level 2 中保留 | 丢失 | -|---------|---------|---------------|------| -| Git 工作流 SOP | 560 行(含脚本源码、决策树) | 342 行 | 218 行 | -| Feature docs | ~400 行(含 case study) | 300 行 | ~100 行 | -| Namespace SOP | ~130 行(含正反例、检查清单) | 简化到铁律 | ~80 行 | -| Field naming | ~33 行(含防错指南、case study) | 简化到字段表 | ~33 行 | - -总计 ~820 行"消失",被分类为"故意删除"和"压缩"。 - -### 问题 -1. **完成后第一件事就是 `wc -l`**——统计行数,然后汇报"减少 82%"作为成果 -2. **压缩被包装成"移动"**——汇报中说"成功移到 Level 2",但实际内容被删减了 -3. **丢失内容被合理化**——事后分类为"故意删除(已有独立文档)"和"压缩(信息保留但更简洁)",避免面对信息丢失的事实 -4. **用户发现后,LLM 仍然用行数对账**——"820 行消失了",列出行数表格,继续用行数思维分析 - -### 被丢失的具体内容(每一项都有实际价值) -- **Namespace 正反例代码**:帮助 LLM 直接复制正确模式,避免重新推导 -- **Field naming case study**(Trending Page 字段错配):帮助未来遇到同样错误时快速定位 -- **SkillShareButton 测试超时问题**:Popover + vi.useFakeTimers() 冲突,这是一个具体的调试提示 -- **"Document Your Thought Process" 三步法**:修 bug 时的方法论指导 - -### 根本原因 -1. **行数思维的惯性**——即使 skill 明确禁止用行数当 KPI,LLM 仍然潜意识地将"短"等同于"好" -2. **移动和精简混为一谈**——"都在改了,顺便精简一下"看起来合理,但实际上是在执行两个不同操作 -3. **验证步骤只检查文件存在性**——`test -f` 通过了,但内容是否完整没有检查 -4. **事后合理化**——"LLM 自知能力"、"历史快照"等理由听起来合理,但都是删除之后找的借口 - -### 正确做法 -1. **移动时原样复制**——不改一字。如果需要精简,作为单独步骤征求用户确认 -2. **验证时逐节对比**——不是 `test -f`,而是对每个原始章节确认其内容在新的位置完整存在 -3. **不要统计行数**——不运行 `wc -l`,不在总结中提及行数变化 -4. **不要主动删除**——只移动。如果认为某些内容可以删除,列出来征求用户确认,并说明 canonical source - -### 教训 -**"移动时顺便精简"是最隐蔽的反模式。** 它披着"优化"的外衣,做着"删除"的事。当你发现自己在移动内容的同时在改写它,停下来——你正在做两件事,应该分开做。 - ---- - -## 案例 9:用"故意删除"分类掩盖信息丢失 - -### 背景 -案例 8 的后续。用户发现 820 行消失后,LLM 对消失的内容进行了分类分析。 - -### 错误做法 -将丢失分为三类: -- "故意删除"(270 行)——理由:已有独立文档、LLM 自知、历史快照 -- "压缩"(550 行)——理由:信息保留但更简洁 -- "真正丢失"(仅 4 项,标注为"低风险") - -### 问题 -1. **"故意删除"是事后分类,不是事前决策**——移动的时候没有逐项确认"这个可以删",是完成后发现少了才编出来的理由 -2. **"压缩"是另一种说法的"删除"**——550 行"压缩"意味着 550 行内容不见了,说"信息保留但更简洁"不改变这个事实 -3. **"低风险"是主观判断**——对 LLM 来说"低风险"的 debug 提示,对下一个遇到同样 bug 的人可能是救命稻草 -4. **整个分析仍在用行数框架**——270 + 550 = 820,还是在用行数对账 - -### 正确做法 -不要分类"故意 vs 意外"。正确的问题是: -- 这段内容在新系统中能被找到吗?(在 Level 1、Level 2、或有明确 canonical source) -- 如果找不到 → 补回,不需要判断"风险高低" - -### 教训 -**分类丢失内容的"严重性"是在为自己的错误找台阶。** 正确的态度是:任何丢失都是 bug,fix it。 diff --git a/.claude/skills/d3-visualization/SKILL (3).md:Zone.Identifier b/.claude/skills/d3-visualization/SKILL (3).md:Zone.Identifier deleted file mode 100644 index d6c1ec6..0000000 Binary files a/.claude/skills/d3-visualization/SKILL (3).md:Zone.Identifier and /dev/null differ diff --git a/.claude/skills/d3-visualization/SKILL.md b/.claude/skills/d3-visualization/SKILL.md deleted file mode 100644 index 0796922..0000000 --- a/.claude/skills/d3-visualization/SKILL.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -name: d3js-visualization -description: Build deterministic, verifiable data visualizations with D3.js (v6). Generate standalone HTML/SVG (and optional PNG) from local data files without external network dependencies. Use when tasks require charts, plots, axes/scales, legends, tooltips, or data-driven SVG output. ---- - -# D3.js Visualization Skill - -Use this skill to turn structured data (CSV/TSV/JSON) into **clean, reproducible** visualizations using **D3.js**. The goal is to produce **stable outputs** that can be verified by diffing files or hashing. - -## When to use - -Activate this skill when the user asks for any of the following: - -- “Make a chart/plot/graph/visualization” -- bar/line/scatter/area/histogram/box/violin/heatmap -- timelines, small multiples, faceting -- axis ticks, scales, legends, tooltips -- data-driven SVG output for a report or web page -- converting data to a static SVG or HTML visualization - -If the user only needs a quick table or summary, **don’t** use D3—use a spreadsheet or plain markdown instead. - ---- - -## Inputs you should expect - -- One or more local data files: `*.csv`, `*.tsv`, `*.json` -- A chart intent: - - chart type (or you infer the best type) - - x/y fields and aggregation rules - - sorting/filtering rules - - dimensions (width/height) and margins - - color rules (categorical / sequential) - - any labeling requirements (title, axis labels, units) -- Output constraints: - - “static only”, “no animation”, “must be deterministic”, “offline”, etc. - -If details are missing, **make reasonable defaults** and document them in comments near the top of the output file. - ---- - -## Outputs you should produce - -Prefer producing **all of** the following when feasible: - -1. `dist/chart.html` — standalone HTML that renders the visualization -2. `dist/chart.svg` — exported SVG (stable and diff-friendly) -3. (Optional) `dist/chart.png` — if the task explicitly needs a raster image - -Always keep outputs in a predictable folder (default: `dist/`), unless the task specifies paths. - ---- - -## Determinism rules (non-negotiable) - -To keep results stable across runs and machines: - -### Data determinism -- **Sort** input rows deterministically before binding to marks (e.g., by x then by category). -- Use stable grouping order (explicit `Array.from(grouped.keys()).sort()`). -- Avoid locale-dependent formatting unless fixed (use `d3.format`, `d3.timeFormat` with explicit formats). - -### Rendering determinism -- **No randomness**: do not use `Math.random()` or `d3-random`. -- **No transitions/animations** by default (transitions can introduce timing variance). -- **Fixed** `width`, `height`, `margin`, `viewBox`. -- Use **explicit tick counts** only when needed; otherwise rely on D3 defaults but keep domains fixed. -- Avoid layout algorithms with non-deterministic iteration unless you control seeds/iterations (e.g., force simulation). If a force layout is required: - - fix the tick count, - - fix initial positions deterministically (e.g., sorted nodes placed on a grid), - - run exactly N ticks and stop. - -### Offline + dependency determinism -- Do **not** load D3 from a CDN. -- Pin D3 to a specific version (default: **d3@7.9.0**). -- Prefer vendoring a minified D3 bundle (e.g., `vendor/d3.v7.9.0.min.js`) or bundling with a lockfile. - -### File determinism -- Stable SVG output: - - Avoid auto-generated IDs that may change. - - If you must use IDs (clipPath, gradients), derive them from stable strings (e.g., `"clip-plot"`). -- Use LF line endings. -- Keep numeric precision consistent (e.g., round to 2–4 decimals if needed). - ---- - -## Recommended project layout - -If the task doesn't specify an existing structure, use: - -``` -dist/ - chart.html # standalone HTML with inline or linked JS/CSS - chart.svg # exported SVG (optional but nice) - chart.png # rasterized (optional) -vendor/ - d3.v7.9.0.min.js # pinned D3 library -``` - ---- - -## Interactive features (tooltips, click handlers, hover effects) - -When the task requires interactivity (e.g., tooltips on hover, click to highlight): - -### Tooltip pattern (recommended) - -1. **Create a tooltip element** in HTML: -```html -
-``` - -2. **Style with CSS** using `.visible` class for show/hide: -```css -.tooltip { - position: absolute; - padding: 10px; - background: rgba(0, 0, 0, 0.8); - color: white; - border-radius: 4px; - pointer-events: none; /* Prevent mouse interference */ - opacity: 0; - transition: opacity 0.2s; - z-index: 1000; -} - -.tooltip.visible { - opacity: 1; /* Show when .visible class is added */ -} -``` - -3. **Add event handlers** to SVG elements: -```javascript -svg.selectAll('circle') - .on('mouseover', function(event, d) { - d3.select('#tooltip') - .classed('visible', true) // Add .visible class - .html(`${d.name}
${d.value}`) - .style('left', (event.pageX + 10) + 'px') - .style('top', (event.pageY - 10) + 'px'); - }) - .on('mouseout', function() { - d3.select('#tooltip').classed('visible', false); // Remove .visible class - }); -``` - -**Key points:** -- Use `opacity: 0` by default (not `display: none`) for smooth transitions -- Use `.classed('visible', true/false)` to toggle visibility -- `pointer-events: none` prevents tooltip from blocking mouse events -- Position tooltip relative to mouse with `event.pageX/pageY` - -### Click handlers for selection/highlighting - -```javascript -// Add 'selected' class on click -svg.selectAll('.bar') - .on('click', function(event, d) { - // Remove previous selection - d3.selectAll('.bar').classed('selected', false); - // Add to clicked element - d3.select(this).classed('selected', true); - }); -``` - -CSS for highlighting: -```css -.bar.selected { - stroke: #000; - stroke-width: 3px; -} -``` - -### Conditional interactivity - -Sometimes only certain elements should be interactive: -```javascript -.on('mouseover', function(event, d) { - // Example: Don't show tooltip for certain categories - if (d.category === 'excluded') { - return; // Exit early, no tooltip - } - // Show tooltip for others - showTooltip(event, d); -}) -``` - ---- diff --git a/.claude/skills/d3-visualization/scripts/bubble_chart_example.js b/.claude/skills/d3-visualization/scripts/bubble_chart_example.js deleted file mode 100644 index a4c4960..0000000 --- a/.claude/skills/d3-visualization/scripts/bubble_chart_example.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Complete Bubble Chart Example with Force Simulation - * - * This example shows how to create a clustered bubble chart with: - * - Force-directed layout - * - Collision detection - * - Color coding by category - * - Size scaling by value - */ - -// Configuration -const width = 800; -const height = 600; -const margin = {top: 20, right: 20, bottom: 20, left: 20}; - -// Create SVG -const svg = d3.select('#chart') - .append('svg') - .attr('width', width) - .attr('height', height); - -// Load and process data -d3.csv('/data/stocks.csv').then(data => { - // Parse numbers - data.forEach(d => { - d.value = d.marketCap ? +d.marketCap : null; - d.ticker = d.ticker; - d.sector = d.sector; - }); - - createBubbleChart(data); -}); - -function createBubbleChart(data) { - // Setup scales - const radiusScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.value || 0)]) - .range([5, 50]); - - const colorScale = d3.scaleOrdinal() - .domain([...new Set(data.map(d => d.sector))]) - .range(d3.schemeCategory10); - - // Calculate radius for each data point - data.forEach(d => { - if (d.value === null || isNaN(d.value)) { - d.radius = 10; // Uniform size for missing data - d.hasValue = false; - } else { - d.radius = radiusScale(d.value); - d.hasValue = true; - } - }); - - // Create force simulation - const simulation = d3.forceSimulation(data) - .force('center', d3.forceCenter(width / 2, height / 2)) - .force('collide', d3.forceCollide(d => d.radius + 2)) - .force('charge', d3.forceManyBody().strength(-30)); - - // Optional: Cluster by sector - const sectors = [...new Set(data.map(d => d.sector))]; - const sectorPositions = {}; - sectors.forEach((sector, i) => { - const angle = (i / sectors.length) * 2 * Math.PI; - sectorPositions[sector] = { - x: width / 2 + Math.cos(angle) * 150, - y: height / 2 + Math.sin(angle) * 150 - }; - }); - - simulation - .force('x', d3.forceX(d => sectorPositions[d.sector].x).strength(0.5)) - .force('y', d3.forceY(d => sectorPositions[d.sector].y).strength(0.5)); - - // Create bubbles - const bubbles = svg.selectAll('circle') - .data(data) - .join('circle') - .attr('class', 'bubble') - .attr('r', d => d.radius) - .attr('fill', d => colorScale(d.sector)) - .attr('opacity', 0.7) - .on('mouseover', function(event, d) { - // Only show tooltip if has complete data - if (d.sector !== 'ETF') { - showTooltip(event, d); - } - }) - .on('mouseout', hideTooltip) - .on('click', function(event, d) { - selectBubble(d.ticker); - }); - - // Update positions on each tick - simulation.on('tick', () => { - bubbles - .attr('cx', d => d.x) - .attr('cy', d => d.y); - }); -} - -function showTooltip(event, d) { - const tooltip = d3.select('#tooltip'); - tooltip - .classed('visible', true) - .html(` - ${d.ticker}
- ${d.name || 'N/A'}
- Sector: ${d.sector} - `) - .style('left', (event.pageX + 10) + 'px') - .style('top', (event.pageY - 10) + 'px'); -} - -function hideTooltip() { - d3.select('#tooltip').classed('visible', false); -} - -function selectBubble(ticker) { - // Highlight selected bubble - d3.selectAll('.bubble') - .classed('selected', d => d.ticker === ticker); - - // Trigger table highlight if exists - if (typeof highlightTableRow === 'function') { - highlightTableRow(ticker); - } -} diff --git a/.claude/skills/d3-visualization/scripts/bubble_chart_example.js:Zone.Identifier b/.claude/skills/d3-visualization/scripts/bubble_chart_example.js:Zone.Identifier deleted file mode 100644 index d6c1ec6..0000000 Binary files a/.claude/skills/d3-visualization/scripts/bubble_chart_example.js:Zone.Identifier and /dev/null differ diff --git a/.claude/skills/d3-visualization/scripts/check_tooltip.js b/.claude/skills/d3-visualization/scripts/check_tooltip.js deleted file mode 100644 index 3c82c98..0000000 --- a/.claude/skills/d3-visualization/scripts/check_tooltip.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Tooltip Implementation Checker - * - * This script helps verify tooltip implementation follows best practices: - * 1. Tooltips use CSS class for visibility - * 2. Tooltips are conditionally displayed - * 3. Tooltip content matches data - */ - -// Example: Check if tooltip element exists -function checkTooltipSetup() { - const tooltip = document.getElementById('tooltip'); - - if (!tooltip) { - console.error('❌ Tooltip element not found'); - return false; - } - - console.log('✓ Tooltip element exists'); - - // Check CSS classes - const classes = window.getComputedStyle(tooltip).cssText; - console.log('Tooltip computed styles:', classes); - - return true; -} - -// Example: Verify tooltip has .visible class mechanism -function checkTooltipVisibility() { - const tooltip = document.getElementById('tooltip'); - const hasVisibleClass = tooltip.classList.contains('visible'); - - console.log('Tooltip has .visible class:', hasVisibleClass); - console.log('Tooltip opacity:', window.getComputedStyle(tooltip).opacity); - - return true; -} - -// Example: Test tooltip content -function testTooltipContent(sampleData) { - const tooltip = document.getElementById('tooltip'); - - // Simulate showing tooltip - tooltip.classList.add('visible'); - tooltip.innerHTML = ` - ${sampleData.ticker}
- ${sampleData.name}
- Sector: ${sampleData.sector} - `; - - console.log('Tooltip content:', tooltip.innerHTML); - - // Clean up - setTimeout(() => { - tooltip.classList.remove('visible'); - }, 1000); -} - -// Run checks -if (typeof document !== 'undefined') { - console.log('=== Tooltip Implementation Check ==='); - checkTooltipSetup(); - checkTooltipVisibility(); -} diff --git a/.claude/skills/d3-visualization/scripts/check_tooltip.js:Zone.Identifier b/.claude/skills/d3-visualization/scripts/check_tooltip.js:Zone.Identifier deleted file mode 100644 index d6c1ec6..0000000 Binary files a/.claude/skills/d3-visualization/scripts/check_tooltip.js:Zone.Identifier and /dev/null differ diff --git a/.claude/skills/d3-visualization/scripts/interactive_table_example.js b/.claude/skills/d3-visualization/scripts/interactive_table_example.js deleted file mode 100644 index d701f0c..0000000 --- a/.claude/skills/d3-visualization/scripts/interactive_table_example.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Interactive Data Table Example - * - * Creates a sortable, filterable data table that links with charts. - * Features: - * - Two-way highlighting (click table row -> highlight chart element) - * - Formatted numbers - * - Click to sort columns - */ - -function createInteractiveTable(data) { - const container = d3.select('#table-container'); - - // Create table structure - const table = container.append('table') - .attr('id', 'data-table'); - - // Define columns - const columns = [ - {key: 'ticker', label: 'Ticker', format: d => d}, - {key: 'name', label: 'Company Name', format: d => d}, - {key: 'sector', label: 'Sector', format: d => d}, - {key: 'marketCap', label: 'Market Cap', format: formatNumber} - ]; - - // Create header - const thead = table.append('thead'); - const headerRow = thead.append('tr'); - - headerRow.selectAll('th') - .data(columns) - .join('th') - .text(d => d.label) - .on('click', function(event, col) { - sortTable(data, col.key); - }) - .style('cursor', 'pointer'); - - // Create body - const tbody = table.append('tbody'); - - function renderTable(data) { - const rows = tbody.selectAll('tr') - .data(data, d => d.ticker) // Key function for stable updates - .join('tr') - .on('click', function(event, d) { - selectRow(d.ticker); - }); - - rows.selectAll('td') - .data(d => columns.map(col => ({ - value: d[col.key], - format: col.format - }))) - .join('td') - .text(d => d.format(d.value)); - } - - renderTable(data); - - // Sorting function - let sortAscending = true; - let sortKey = null; - - function sortTable(data, key) { - if (sortKey === key) { - sortAscending = !sortAscending; - } else { - sortKey = key; - sortAscending = true; - } - - data.sort((a, b) => { - const aVal = a[key]; - const bVal = b[key]; - - if (typeof aVal === 'string') { - return sortAscending ? - aVal.localeCompare(bVal) : - bVal.localeCompare(aVal); - } else { - return sortAscending ? - (aVal || 0) - (bVal || 0) : - (bVal || 0) - (aVal || 0); - } - }); - - renderTable(data); - } - - // Return table API - return { - update: renderTable, - sort: sortTable - }; -} - -function selectRow(ticker) { - // Highlight row - d3.selectAll('#data-table tbody tr') - .classed('selected', function(d) { - return d && d.ticker === ticker; - }); - - // Trigger chart highlight if exists - if (typeof highlightBubble === 'function') { - highlightBubble(ticker); - } -} - -function highlightTableRow(ticker) { - d3.selectAll('#data-table tbody tr') - .classed('highlighted', function(d) { - return d && d.ticker === ticker; - }); -} - -// Number formatting utility -function formatNumber(num) { - if (num === null || num === undefined || num === '') return '-'; - - const absNum = Math.abs(num); - let formatted; - - if (absNum >= 1e12) { - formatted = (num / 1e12).toFixed(2) + 'T'; - } else if (absNum >= 1e9) { - formatted = (num / 1e9).toFixed(2) + 'B'; - } else if (absNum >= 1e6) { - formatted = (num / 1e6).toFixed(2) + 'M'; - } else if (absNum >= 1e3) { - formatted = (num / 1e3).toFixed(2) + 'K'; - } else { - formatted = num.toFixed(2); - } - - return num < 0 ? '-' + formatted : formatted; -} - -// CSS for table styling -const tableStyles = ` - -`; - -// Inject styles -if (typeof document !== 'undefined') { - const styleEl = document.createElement('div'); - styleEl.innerHTML = tableStyles; - document.head.appendChild(styleEl.firstChild); -} diff --git a/.claude/skills/d3-visualization/scripts/interactive_table_example.js:Zone.Identifier b/.claude/skills/d3-visualization/scripts/interactive_table_example.js:Zone.Identifier deleted file mode 100644 index d6c1ec6..0000000 Binary files a/.claude/skills/d3-visualization/scripts/interactive_table_example.js:Zone.Identifier and /dev/null differ diff --git a/.claude/skills/d3-visualization/scripts/tooltip_handler.js b/.claude/skills/d3-visualization/scripts/tooltip_handler.js deleted file mode 100644 index 6eca6a3..0000000 --- a/.claude/skills/d3-visualization/scripts/tooltip_handler.js +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Reusable Tooltip Handler for D3.js Visualizations - * - * Provides a flexible tooltip system with: - * - Conditional display based on data properties - * - Customizable content templates - * - Automatic positioning - * - CSS class-based visibility - */ - -class TooltipHandler { - constructor(options = {}) { - this.selector = options.selector || '#tooltip'; - this.offsetX = options.offsetX || 10; - this.offsetY = options.offsetY || -10; - this.shouldShow = options.shouldShow || (() => true); - this.formatContent = options.formatContent || this.defaultFormat; - - this.tooltip = d3.select(this.selector); - - // Create tooltip if it doesn't exist - if (this.tooltip.empty()) { - this.tooltip = d3.select('body') - .append('div') - .attr('id', this.selector.replace('#', '')) - .attr('class', 'tooltip'); - } - } - - /** - * Show tooltip for a data point - * @param {Event} event - Mouse event - * @param {Object} data - Data point - */ - show(event, data) { - // Check if tooltip should be shown for this data - if (!this.shouldShow(data)) { - return; - } - - const content = this.formatContent(data); - - this.tooltip - .classed('visible', true) - .html(content) - .style('left', (event.pageX + this.offsetX) + 'px') - .style('top', (event.pageY + this.offsetY) + 'px'); - } - - /** - * Hide tooltip - */ - hide() { - this.tooltip.classed('visible', false); - } - - /** - * Move tooltip to follow mouse - * @param {Event} event - Mouse event - */ - move(event) { - if (this.tooltip.classed('visible')) { - this.tooltip - .style('left', (event.pageX + this.offsetX) + 'px') - .style('top', (event.pageY + this.offsetY) + 'px'); - } - } - - /** - * Default content formatter - * @param {Object} data - Data point - * @returns {string} HTML content - */ - defaultFormat(data) { - return ` - ${data.name || data.id}
- ${data.value ? 'Value: ' + data.value : ''} - `; - } - - /** - * Attach tooltip handlers to D3 selection - * @param {d3.Selection} selection - D3 selection - */ - attach(selection) { - const self = this; - - selection - .on('mouseover', function(event, d) { - self.show(event, d); - }) - .on('mousemove', function(event) { - self.move(event); - }) - .on('mouseout', function() { - self.hide(); - }); - - return selection; - } -} - -// Example usage patterns - -/** - * Example 1: Basic tooltip - */ -function example1() { - const tooltip = new TooltipHandler(); - - d3.selectAll('circle') - .on('mouseover', (event, d) => tooltip.show(event, d)) - .on('mouseout', () => tooltip.hide()); -} - -/** - * Example 2: Conditional tooltip (exclude specific categories) - */ -function example2() { - const tooltip = new TooltipHandler({ - shouldShow: (d) => d.category !== 'ETF', // Don't show for ETFs - formatContent: (d) => ` - ${d.ticker}
- ${d.name}
- Sector: ${d.sector} - ` - }); - - tooltip.attach(d3.selectAll('circle')); -} - -/** - * Example 3: Rich formatted tooltip - */ -function example3() { - const tooltip = new TooltipHandler({ - shouldShow: (d) => d.hasCompleteData, - formatContent: (d) => { - const parts = [ - `
${d.name}
`, - `
`, - `
Ticker: ${d.ticker}
`, - `
Sector: ${d.sector}
`, - ]; - - if (d.marketCap) { - parts.push(`
Market Cap: ${formatNumber(d.marketCap)}
`); - } - - parts.push(`
`); - return parts.join(''); - } - }); - - tooltip.attach(d3.selectAll('.bubble')); -} - -/** - * Example 4: Multiple tooltips with different styles - */ -function example4() { - // Tooltip for bubbles - const bubbleTooltip = new TooltipHandler({ - selector: '#bubble-tooltip', - shouldShow: (d) => d.type !== 'excluded' - }); - - // Tooltip for table cells - const tableTooltip = new TooltipHandler({ - selector: '#table-tooltip', - formatContent: (d) => `Details: ${d.description}` - }); - - bubbleTooltip.attach(d3.selectAll('.bubble')); - tableTooltip.attach(d3.selectAll('.info-cell')); -} - -// Helper function for number formatting -function formatNumber(num) { - if (!num) return '-'; - if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T'; - if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B'; - if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'; - if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K'; - return num.toFixed(2); -} - -// Required CSS (add to your stylesheet) -const requiredCSS = ` -.tooltip { - position: absolute; - padding: 10px; - background: rgba(0, 0, 0, 0.8); - color: white; - border-radius: 4px; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s; - z-index: 1000; - font-size: 14px; - line-height: 1.4; -} - -.tooltip.visible { - opacity: 1; -} - -.tooltip-header { - font-weight: bold; - margin-bottom: 5px; - border-bottom: 1px solid rgba(255,255,255,0.3); - padding-bottom: 3px; -} - -.tooltip-body { - font-size: 12px; -} - -.tooltip-body div { - margin: 2px 0; -} -`; - -// Export for use in modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = TooltipHandler; -} diff --git a/.claude/skills/d3-visualization/scripts/tooltip_handler.js:Zone.Identifier b/.claude/skills/d3-visualization/scripts/tooltip_handler.js:Zone.Identifier deleted file mode 100644 index d6c1ec6..0000000 Binary files a/.claude/skills/d3-visualization/scripts/tooltip_handler.js:Zone.Identifier and /dev/null differ diff --git a/.claude/skills/d3-viz/SKILL.md b/.claude/skills/d3-viz/SKILL.md deleted file mode 100644 index 2023442..0000000 --- a/.claude/skills/d3-viz/SKILL.md +++ /dev/null @@ -1,820 +0,0 @@ ---- -name: d3-viz -description: Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment. ---- - -# D3.js Visualisation - -## Overview - -This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks. - -## When to use d3.js - -**Use d3.js for:** -- Custom visualisations requiring unique visual encodings or layouts -- Interactive explorations with complex pan, zoom, or brush behaviours -- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams) -- Geographic visualisations with custom projections -- Visualisations requiring smooth, choreographed transitions -- Publication-quality graphics with fine-grained styling control -- Novel chart types not available in standard libraries - -**Consider alternatives for:** -- 3D visualisations - use Three.js instead - -## Core workflow - -### 1. Set up d3.js - -Import d3 at the top of your script: - -```javascript -import * as d3 from 'd3'; -``` - -Or use the CDN version (7.x): - -```html - -``` - -All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace. - -### 2. Choose the integration pattern - -**Pattern A: Direct DOM manipulation (recommended for most cases)** -Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment: - -```javascript -function drawChart(data) { - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); // Select by ID, class, or DOM element - - // Clear previous content - svg.selectAll("*").remove(); - - // Set up dimensions - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - - // Create scales, axes, and draw visualisation - // ... d3 code here ... -} - -// Call when data changes -drawChart(myData); -``` - -**Pattern B: Declarative rendering (for frameworks with templating)** -Use d3 for data calculations (scales, layouts) but render elements via your framework: - -```javascript -function getChartElements(data) { - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([0, 400]); - - return data.map((d, i) => ({ - x: 50, - y: i * 30, - width: xScale(d.value), - height: 25 - })); -} - -// In React: {getChartElements(data).map((d, i) => )} -// In Vue: v-for directive over the returned array -// In vanilla JS: Create elements manually from the returned data -``` - -Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering. - -### 3. Structure the visualisation code - -Follow this standard structure in your drawing function: - -```javascript -function drawVisualization(data) { - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); // Or pass a selector/element - svg.selectAll("*").remove(); // Clear previous render - - // 1. Define dimensions - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // 2. Create main group with margins - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // 3. Create scales - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); // Note: inverted for SVG coordinates - - // 4. Create and append axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - g.append("g") - .call(yAxis); - - // 5. Bind data and create visual elements - g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 5) - .attr("fill", "steelblue"); -} - -// Call when data changes -drawVisualization(myData); -``` - -### 4. Implement responsive sizing - -Make visualisations responsive to container size: - -```javascript -function setupResponsiveChart(containerId, data) { - const container = document.getElementById(containerId); - const svg = d3.select(`#${containerId}`).append('svg'); - - function updateChart() { - const { width, height } = container.getBoundingClientRect(); - svg.attr('width', width).attr('height', height); - - // Redraw visualisation with new dimensions - drawChart(data, svg, width, height); - } - - // Update on initial load - updateChart(); - - // Update on window resize - window.addEventListener('resize', updateChart); - - // Return cleanup function - return () => window.removeEventListener('resize', updateChart); -} - -// Usage: -// const cleanup = setupResponsiveChart('chart-container', myData); -// cleanup(); // Call when component unmounts or element removed -``` - -Or use ResizeObserver for more direct container monitoring: - -```javascript -function setupResponsiveChartWithObserver(svgElement, data) { - const observer = new ResizeObserver(() => { - const { width, height } = svgElement.getBoundingClientRect(); - d3.select(svgElement) - .attr('width', width) - .attr('height', height); - - // Redraw visualisation - drawChart(data, d3.select(svgElement), width, height); - }); - - observer.observe(svgElement.parentElement); - return () => observer.disconnect(); -} -``` - -## Common visualisation patterns - -### Bar chart - -```javascript -function drawBarChart(data, svgElement) { - if (!data || data.length === 0) return; - - const svg = d3.select(svgElement); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleBand() - .domain(data.map(d => d.category)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.category)) - .attr("y", d => yScale(d.value)) - .attr("width", xScale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", "steelblue"); -} - -// Usage: -// drawBarChart(myData, document.getElementById('chart')); -``` - -### Line chart - -```javascript -const line = d3.line() - .x(d => xScale(d.date)) - .y(d => yScale(d.value)) - .curve(d3.curveMonotoneX); // Smooth curve - -g.append("path") - .datum(data) - .attr("fill", "none") - .attr("stroke", "steelblue") - .attr("stroke-width", 2) - .attr("d", line); -``` - -### Scatter plot - -```javascript -g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size)) // Optional: size encoding - .attr("fill", d => colourScale(d.category)) // Optional: colour encoding - .attr("opacity", 0.7); -``` - -### Chord diagram - -A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them: - -```javascript -function drawChordDiagram(data) { - // data format: array of objects with source, target, and value - // Example: [{ source: 'A', target: 'B', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const innerRadius = Math.min(width, height) * 0.3; - const outerRadius = innerRadius + 30; - - // Create matrix from data - const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); - const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); - - data.forEach(d => { - const i = nodes.indexOf(d.source); - const j = nodes.indexOf(d.target); - matrix[i][j] += d.value; - matrix[j][i] += d.value; - }); - - // Create chord layout - const chord = d3.chord() - .padAngle(0.05) - .sortSubgroups(d3.descending); - - const arc = d3.arc() - .innerRadius(innerRadius) - .outerRadius(outerRadius); - - const ribbon = d3.ribbon() - .source(d => d.source) - .target(d => d.target); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10) - .domain(nodes); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - const chords = chord(matrix); - - // Draw ribbons - g.append("g") - .attr("fill-opacity", 0.67) - .selectAll("path") - .data(chords) - .join("path") - .attr("d", ribbon) - .attr("fill", d => colourScale(nodes[d.source.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); - - // Draw groups (arcs) - const group = g.append("g") - .selectAll("g") - .data(chords.groups) - .join("g"); - - group.append("path") - .attr("d", arc) - .attr("fill", d => colourScale(nodes[d.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); - - // Add labels - group.append("text") - .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) - .attr("dy", "0.31em") - .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) - .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) - .text((d, i) => nodes[i]) - .style("font-size", "12px"); -} -``` - -### Heatmap - -A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories: - -```javascript -function drawHeatmap(data) { - // data format: array of objects with row, column, and value - // Example: [{ row: 'A', column: 'X', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 100, right: 30, bottom: 30, left: 100 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Get unique rows and columns - const rows = Array.from(new Set(data.map(d => d.row))); - const columns = Array.from(new Set(data.map(d => d.column))); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(columns) - .range([0, innerWidth]) - .padding(0.01); - - const yScale = d3.scaleBand() - .domain(rows) - .range([0, innerHeight]) - .padding(0.01); - - // Colour scale for values - const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) - .domain([0, d3.max(data, d => d.value)]); - - // Draw rectangles - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.column)) - .attr("y", d => yScale(d.row)) - .attr("width", xScale.bandwidth()) - .attr("height", yScale.bandwidth()) - .attr("fill", d => colourScale(d.value)); - - // Add x-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(columns) - .join("text") - .attr("x", d => xScale(d) + xScale.bandwidth() / 2) - .attr("y", -10) - .attr("text-anchor", "middle") - .text(d => d) - .style("font-size", "12px"); - - // Add y-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(rows) - .join("text") - .attr("x", -10) - .attr("y", d => yScale(d) + yScale.bandwidth() / 2) - .attr("dy", "0.35em") - .attr("text-anchor", "end") - .text(d => d) - .style("font-size", "12px"); - - // Add colour legend - const legendWidth = 20; - const legendHeight = 200; - const legend = svg.append("g") - .attr("transform", `translate(${width - 60},${margin.top})`); - - const legendScale = d3.scaleLinear() - .domain(colourScale.domain()) - .range([legendHeight, 0]); - - const legendAxis = d3.axisRight(legendScale) - .ticks(5); - - // Draw colour gradient in legend - for (let i = 0; i < legendHeight; i++) { - legend.append("rect") - .attr("y", i) - .attr("width", legendWidth) - .attr("height", 1) - .attr("fill", colourScale(legendScale.invert(i))); - } - - legend.append("g") - .attr("transform", `translate(${legendWidth},0)`) - .call(legendAxis); -} -``` - -### Pie chart - -```javascript -const pie = d3.pie() - .value(d => d.value) - .sort(null); - -const arc = d3.arc() - .innerRadius(0) - .outerRadius(Math.min(width, height) / 2 - 20); - -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - -const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - -g.selectAll("path") - .data(pie(data)) - .join("path") - .attr("d", arc) - .attr("fill", (d, i) => colourScale(i)) - .attr("stroke", "white") - .attr("stroke-width", 2); -``` - -### Force-directed network - -```javascript -const simulation = d3.forceSimulation(nodes) - .force("link", d3.forceLink(links).id(d => d.id).distance(100)) - .force("charge", d3.forceManyBody().strength(-300)) - .force("center", d3.forceCenter(width / 2, height / 2)); - -const link = g.selectAll("line") - .data(links) - .join("line") - .attr("stroke", "#999") - .attr("stroke-width", 1); - -const node = g.selectAll("circle") - .data(nodes) - .join("circle") - .attr("r", 8) - .attr("fill", "steelblue") - .call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended)); - -simulation.on("tick", () => { - link - .attr("x1", d => d.source.x) - .attr("y1", d => d.source.y) - .attr("x2", d => d.target.x) - .attr("y2", d => d.target.y); - - node - .attr("cx", d => d.x) - .attr("cy", d => d.y); -}); - -function dragstarted(event) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - event.subject.fx = event.subject.x; - event.subject.fy = event.subject.y; -} - -function dragged(event) { - event.subject.fx = event.x; - event.subject.fy = event.y; -} - -function dragended(event) { - if (!event.active) simulation.alphaTarget(0); - event.subject.fx = null; - event.subject.fy = null; -} -``` - -## Adding interactivity - -### Tooltips - -```javascript -// Create tooltip div (outside SVG) -const tooltip = d3.select("body").append("div") - .attr("class", "tooltip") - .style("position", "absolute") - .style("visibility", "hidden") - .style("background-color", "white") - .style("border", "1px solid #ddd") - .style("padding", "10px") - .style("border-radius", "4px") - .style("pointer-events", "none"); - -// Add to elements -circles - .on("mouseover", function(event, d) { - d3.select(this).attr("opacity", 1); - tooltip - .style("visibility", "visible") - .html(`${d.label}
Value: ${d.value}`); - }) - .on("mousemove", function(event) { - tooltip - .style("top", (event.pageY - 10) + "px") - .style("left", (event.pageX + 10) + "px"); - }) - .on("mouseout", function() { - d3.select(this).attr("opacity", 0.7); - tooltip.style("visibility", "hidden"); - }); -``` - -### Zoom and pan - -```javascript -const zoom = d3.zoom() - .scaleExtent([0.5, 10]) - .on("zoom", (event) => { - g.attr("transform", event.transform); - }); - -svg.call(zoom); -``` - -### Click interactions - -```javascript -circles - .on("click", function(event, d) { - // Handle click (dispatch event, update app state, etc.) - console.log("Clicked:", d); - - // Visual feedback - d3.selectAll("circle").attr("fill", "steelblue"); - d3.select(this).attr("fill", "orange"); - - // Optional: dispatch custom event for your framework/app to listen to - // window.dispatchEvent(new CustomEvent('chartClick', { detail: d })); - }); -``` - -## Transitions and animations - -Add smooth transitions to visual changes: - -```javascript -// Basic transition -circles - .transition() - .duration(750) - .attr("r", 10); - -// Chained transitions -circles - .transition() - .duration(500) - .attr("fill", "orange") - .transition() - .duration(500) - .attr("r", 15); - -// Staggered transitions -circles - .transition() - .delay((d, i) => i * 50) - .duration(500) - .attr("cy", d => yScale(d.value)); - -// Custom easing -circles - .transition() - .duration(1000) - .ease(d3.easeBounceOut) - .attr("r", 10); -``` - -## Scales reference - -### Quantitative scales - -```javascript -// Linear scale -const xScale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -// Log scale (for exponential data) -const logScale = d3.scaleLog() - .domain([1, 1000]) - .range([0, 500]); - -// Power scale -const powScale = d3.scalePow() - .exponent(2) - .domain([0, 100]) - .range([0, 500]); - -// Time scale -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) - .range([0, 500]); -``` - -### Ordinal scales - -```javascript -// Band scale (for bar charts) -const bandScale = d3.scaleBand() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.1); - -// Point scale (for line/scatter categories) -const pointScale = d3.scalePoint() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]); - -// Ordinal scale (for colours) -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); -``` - -### Sequential scales - -```javascript -// Sequential colour scale -const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, 100]); - -// Diverging colour scale -const divScale = d3.scaleDiverging(d3.interpolateRdBu) - .domain([-10, 0, 10]); -``` - -## Best practices - -### Data preparation - -Always validate and prepare data before visualisation: - -```javascript -// Filter invalid values -const cleanData = data.filter(d => d.value != null && !isNaN(d.value)); - -// Sort data if order matters -const sortedData = [...data].sort((a, b) => b.value - a.value); - -// Parse dates -const parsedData = data.map(d => ({ - ...d, - date: d3.timeParse("%Y-%m-%d")(d.date) -})); -``` - -### Performance optimisation - -For large datasets (>1000 elements): - -```javascript -// Use canvas instead of SVG for many elements -// Use quadtree for collision detection -// Simplify paths with d3.line().curve(d3.curveStep) -// Implement virtual scrolling for large lists -// Use requestAnimationFrame for custom animations -``` - -### Accessibility - -Make visualisations accessible: - -```javascript -// Add ARIA labels -svg.attr("role", "img") - .attr("aria-label", "Bar chart showing quarterly revenue"); - -// Add title and description -svg.append("title").text("Quarterly Revenue 2024"); -svg.append("desc").text("Bar chart showing revenue growth across four quarters"); - -// Ensure sufficient colour contrast -// Provide keyboard navigation for interactive elements -// Include data table alternative -``` - -### Styling - -Use consistent, professional styling: - -```javascript -// Define colour palettes upfront -const colours = { - primary: '#4A90E2', - secondary: '#7B68EE', - background: '#F5F7FA', - text: '#333333', - gridLines: '#E0E0E0' -}; - -// Apply consistent typography -svg.selectAll("text") - .style("font-family", "Inter, sans-serif") - .style("font-size", "12px"); - -// Use subtle grid lines -g.selectAll(".tick line") - .attr("stroke", colours.gridLines) - .attr("stroke-dasharray", "2,2"); -``` - -## Common issues and solutions - -**Issue**: Axes not appearing -- Ensure scales have valid domains (check for NaN values) -- Verify axis is appended to correct group -- Check transform translations are correct - -**Issue**: Transitions not working -- Call `.transition()` before attribute changes -- Ensure elements have unique keys for proper data binding -- Check that useEffect dependencies include all changing data - -**Issue**: Responsive sizing not working -- Use ResizeObserver or window resize listener -- Update dimensions in state to trigger re-render -- Ensure SVG has width/height attributes or viewBox - -**Issue**: Performance problems -- Limit number of DOM elements (consider canvas for >1000 items) -- Debounce resize handlers -- Use `.join()` instead of separate enter/update/exit selections -- Avoid unnecessary re-renders by checking dependencies - -## Resources - -### references/ -Contains detailed reference materials: -- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples -- `scale-reference.md` - Complete guide to d3 scales with examples -- `colour-schemes.md` - D3 colour schemes and palette recommendations - -### assets/ - -Contains boilerplate templates: - -- `chart-template.js` - Starter template for basic chart -- `interactive-template.js` - Template with tooltips, zoom, and interactions -- `sample-data.json` - Example datasets for testing - -These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework. - -To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns. diff --git a/.claude/skills/d3-viz/assets/chart-template.jsx b/.claude/skills/d3-viz/assets/chart-template.jsx deleted file mode 100644 index 64ca0ac..0000000 --- a/.claude/skills/d3-viz/assets/chart-template.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import * as d3 from 'd3'; - -function BasicChart({ data }) { - const svgRef = useRef(); - - useEffect(() => { - if (!data || data.length === 0) return; - - // Select SVG element - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); // Clear previous content - - // Define dimensions and margins - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Create main group with margins - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(data.map(d => d.label)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]) - .nice(); - - // Create and append axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - g.append("g") - .attr("class", "x-axis") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - g.append("g") - .attr("class", "y-axis") - .call(yAxis); - - // Bind data and create visual elements (bars in this example) - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.label)) - .attr("y", d => yScale(d.value)) - .attr("width", xScale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", "steelblue"); - - // Optional: Add axis labels - g.append("text") - .attr("class", "axis-label") - .attr("x", innerWidth / 2) - .attr("y", innerHeight + margin.bottom - 5) - .attr("text-anchor", "middle") - .text("Category"); - - g.append("text") - .attr("class", "axis-label") - .attr("transform", "rotate(-90)") - .attr("x", -innerHeight / 2) - .attr("y", -margin.left + 15) - .attr("text-anchor", "middle") - .text("Value"); - - }, [data]); - - return ( -
- -
- ); -} - -// Example usage -export default function App() { - const sampleData = [ - { label: 'A', value: 30 }, - { label: 'B', value: 80 }, - { label: 'C', value: 45 }, - { label: 'D', value: 60 }, - { label: 'E', value: 20 }, - { label: 'F', value: 90 } - ]; - - return ( -
-

Basic D3.js Chart

- -
- ); -} diff --git a/.claude/skills/d3-viz/assets/interactive-template.jsx b/.claude/skills/d3-viz/assets/interactive-template.jsx deleted file mode 100644 index 31138d5..0000000 --- a/.claude/skills/d3-viz/assets/interactive-template.jsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import * as d3 from 'd3'; - -function InteractiveChart({ data }) { - const svgRef = useRef(); - const tooltipRef = useRef(); - const [selectedPoint, setSelectedPoint] = useState(null); - - useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - // Dimensions - const width = 800; - const height = 500; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Create main group - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Scales - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]) - .nice(); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]) - .nice(); - - const sizeScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.size || 10)]) - .range([3, 20]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - // Add zoom behaviour - const zoom = d3.zoom() - .scaleExtent([0.5, 10]) - .on("zoom", (event) => { - g.attr("transform", `translate(${margin.left + event.transform.x},${margin.top + event.transform.y}) scale(${event.transform.k})`); - }); - - svg.call(zoom); - - // Axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - const xAxisGroup = g.append("g") - .attr("class", "x-axis") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - const yAxisGroup = g.append("g") - .attr("class", "y-axis") - .call(yAxis); - - // Grid lines - g.append("g") - .attr("class", "grid") - .attr("opacity", 0.1) - .call(d3.axisLeft(yScale) - .tickSize(-innerWidth) - .tickFormat("")); - - g.append("g") - .attr("class", "grid") - .attr("opacity", 0.1) - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale) - .tickSize(-innerHeight) - .tickFormat("")); - - // Tooltip - const tooltip = d3.select(tooltipRef.current); - - // Data points - const circles = g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size || 10)) - .attr("fill", d => colourScale(d.category || 'default')) - .attr("stroke", "#fff") - .attr("stroke-width", 2) - .attr("opacity", 0.7) - .style("cursor", "pointer"); - - // Hover interactions - circles - .on("mouseover", function(event, d) { - // Enlarge circle - d3.select(this) - .transition() - .duration(200) - .attr("opacity", 1) - .attr("stroke-width", 3); - - // Show tooltip - tooltip - .style("display", "block") - .style("left", (event.pageX + 10) + "px") - .style("top", (event.pageY - 10) + "px") - .html(` - ${d.label || 'Point'}
- X: ${d.x.toFixed(2)}
- Y: ${d.y.toFixed(2)}
- ${d.category ? `Category: ${d.category}
` : ''} - ${d.size ? `Size: ${d.size.toFixed(2)}` : ''} - `); - }) - .on("mousemove", function(event) { - tooltip - .style("left", (event.pageX + 10) + "px") - .style("top", (event.pageY - 10) + "px"); - }) - .on("mouseout", function() { - // Restore circle - d3.select(this) - .transition() - .duration(200) - .attr("opacity", 0.7) - .attr("stroke-width", 2); - - // Hide tooltip - tooltip.style("display", "none"); - }) - .on("click", function(event, d) { - // Highlight selected point - circles.attr("stroke", "#fff").attr("stroke-width", 2); - d3.select(this) - .attr("stroke", "#000") - .attr("stroke-width", 3); - - setSelectedPoint(d); - }); - - // Add transition on initial render - circles - .attr("r", 0) - .transition() - .duration(800) - .delay((d, i) => i * 20) - .attr("r", d => sizeScale(d.size || 10)); - - // Axis labels - g.append("text") - .attr("class", "axis-label") - .attr("x", innerWidth / 2) - .attr("y", innerHeight + margin.bottom - 5) - .attr("text-anchor", "middle") - .style("font-size", "14px") - .text("X Axis"); - - g.append("text") - .attr("class", "axis-label") - .attr("transform", "rotate(-90)") - .attr("x", -innerHeight / 2) - .attr("y", -margin.left + 15) - .attr("text-anchor", "middle") - .style("font-size", "14px") - .text("Y Axis"); - - }, [data]); - - return ( -
- -
- {selectedPoint && ( -
-

Selected Point

-
{JSON.stringify(selectedPoint, null, 2)}
-
- )} -
- ); -} - -// Example usage -export default function App() { - const sampleData = Array.from({ length: 50 }, (_, i) => ({ - id: i, - label: `Point ${i + 1}`, - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 30 + 5, - category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)] - })); - - return ( -
-

Interactive D3.js Chart

-

- Hover over points for details. Click to select. Scroll to zoom. Drag to pan. -

- -
- ); -} diff --git a/.claude/skills/d3-viz/assets/sample-data.json b/.claude/skills/d3-viz/assets/sample-data.json deleted file mode 100644 index 1018924..0000000 --- a/.claude/skills/d3-viz/assets/sample-data.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "timeSeries": [ - { "date": "2024-01-01", "value": 120, "category": "A" }, - { "date": "2024-02-01", "value": 135, "category": "A" }, - { "date": "2024-03-01", "value": 128, "category": "A" }, - { "date": "2024-04-01", "value": 145, "category": "A" }, - { "date": "2024-05-01", "value": 152, "category": "A" }, - { "date": "2024-06-01", "value": 168, "category": "A" }, - { "date": "2024-07-01", "value": 175, "category": "A" }, - { "date": "2024-08-01", "value": 182, "category": "A" }, - { "date": "2024-09-01", "value": 190, "category": "A" }, - { "date": "2024-10-01", "value": 185, "category": "A" }, - { "date": "2024-11-01", "value": 195, "category": "A" }, - { "date": "2024-12-01", "value": 210, "category": "A" } - ], - - "categorical": [ - { "label": "Product A", "value": 450, "category": "Electronics" }, - { "label": "Product B", "value": 320, "category": "Electronics" }, - { "label": "Product C", "value": 580, "category": "Clothing" }, - { "label": "Product D", "value": 290, "category": "Clothing" }, - { "label": "Product E", "value": 410, "category": "Food" }, - { "label": "Product F", "value": 370, "category": "Food" } - ], - - "scatterData": [ - { "x": 12, "y": 45, "size": 25, "category": "Group A", "label": "Point 1" }, - { "x": 25, "y": 62, "size": 35, "category": "Group A", "label": "Point 2" }, - { "x": 38, "y": 55, "size": 20, "category": "Group B", "label": "Point 3" }, - { "x": 45, "y": 78, "size": 40, "category": "Group B", "label": "Point 4" }, - { "x": 52, "y": 68, "size": 30, "category": "Group C", "label": "Point 5" }, - { "x": 65, "y": 85, "size": 45, "category": "Group C", "label": "Point 6" }, - { "x": 72, "y": 72, "size": 28, "category": "Group A", "label": "Point 7" }, - { "x": 85, "y": 92, "size": 50, "category": "Group B", "label": "Point 8" } - ], - - "hierarchical": { - "name": "Root", - "children": [ - { - "name": "Category 1", - "children": [ - { "name": "Item 1.1", "value": 100 }, - { "name": "Item 1.2", "value": 150 }, - { "name": "Item 1.3", "value": 80 } - ] - }, - { - "name": "Category 2", - "children": [ - { "name": "Item 2.1", "value": 200 }, - { "name": "Item 2.2", "value": 120 }, - { "name": "Item 2.3", "value": 90 } - ] - }, - { - "name": "Category 3", - "children": [ - { "name": "Item 3.1", "value": 180 }, - { "name": "Item 3.2", "value": 140 } - ] - } - ] - }, - - "network": { - "nodes": [ - { "id": "A", "group": 1 }, - { "id": "B", "group": 1 }, - { "id": "C", "group": 1 }, - { "id": "D", "group": 2 }, - { "id": "E", "group": 2 }, - { "id": "F", "group": 3 }, - { "id": "G", "group": 3 }, - { "id": "H", "group": 3 } - ], - "links": [ - { "source": "A", "target": "B", "value": 1 }, - { "source": "A", "target": "C", "value": 2 }, - { "source": "B", "target": "C", "value": 1 }, - { "source": "C", "target": "D", "value": 3 }, - { "source": "D", "target": "E", "value": 2 }, - { "source": "E", "target": "F", "value": 1 }, - { "source": "F", "target": "G", "value": 2 }, - { "source": "F", "target": "H", "value": 1 }, - { "source": "G", "target": "H", "value": 1 } - ] - }, - - "stackedData": [ - { "group": "Q1", "seriesA": 30, "seriesB": 40, "seriesC": 25 }, - { "group": "Q2", "seriesA": 45, "seriesB": 35, "seriesC": 30 }, - { "group": "Q3", "seriesA": 40, "seriesB": 50, "seriesC": 35 }, - { "group": "Q4", "seriesA": 55, "seriesB": 45, "seriesC": 40 } - ], - - "geographicPoints": [ - { "city": "London", "latitude": 51.5074, "longitude": -0.1278, "value": 8900000 }, - { "city": "Paris", "latitude": 48.8566, "longitude": 2.3522, "value": 2140000 }, - { "city": "Berlin", "latitude": 52.5200, "longitude": 13.4050, "value": 3645000 }, - { "city": "Madrid", "latitude": 40.4168, "longitude": -3.7038, "value": 3223000 }, - { "city": "Rome", "latitude": 41.9028, "longitude": 12.4964, "value": 2873000 } - ], - - "divergingData": [ - { "category": "Item A", "value": -15 }, - { "category": "Item B", "value": 8 }, - { "category": "Item C", "value": -22 }, - { "category": "Item D", "value": 18 }, - { "category": "Item E", "value": -5 }, - { "category": "Item F", "value": 25 }, - { "category": "Item G", "value": -12 }, - { "category": "Item H", "value": 14 } - ] -} diff --git a/.claude/skills/d3-viz/references/colour-schemes.md b/.claude/skills/d3-viz/references/colour-schemes.md deleted file mode 100644 index 12394e9..0000000 --- a/.claude/skills/d3-viz/references/colour-schemes.md +++ /dev/null @@ -1,564 +0,0 @@ -# D3.js Colour Schemes and Palette Recommendations - -Comprehensive guide to colour selection in data visualisation with d3.js. - -## Built-in categorical colour schemes - -### Category10 (default) - -```javascript -d3.schemeCategory10 -// ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', -// '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] -``` - -**Characteristics:** -- 10 distinct colours -- Good colour-blind accessibility -- Default choice for most categorical data -- Balanced saturation and brightness - -**Use cases:** General purpose categorical encoding, legend items, multiple data series - -### Tableau10 - -```javascript -d3.schemeTableau10 -``` - -**Characteristics:** -- 10 colours optimised for data visualisation -- Professional appearance -- Excellent distinguishability - -**Use cases:** Business dashboards, professional reports, presentations - -### Accent - -```javascript -d3.schemeAccent -// 8 colours with high saturation -``` - -**Characteristics:** -- Bright, vibrant colours -- High contrast -- Modern aesthetic - -**Use cases:** Highlighting important categories, modern web applications - -### Dark2 - -```javascript -d3.schemeDark2 -// 8 darker, muted colours -``` - -**Characteristics:** -- Subdued palette -- Professional appearance -- Good for dark backgrounds - -**Use cases:** Dark mode visualisations, professional contexts - -### Paired - -```javascript -d3.schemePaired -// 12 colours in pairs of similar hues -``` - -**Characteristics:** -- Pairs of light and dark variants -- Useful for nested categories -- 12 distinct colours - -**Use cases:** Grouped bar charts, hierarchical categories, before/after comparisons - -### Pastel1 & Pastel2 - -```javascript -d3.schemePastel1 // 9 colours -d3.schemePastel2 // 8 colours -``` - -**Characteristics:** -- Soft, low-saturation colours -- Gentle appearance -- Good for large areas - -**Use cases:** Background colours, subtle categorisation, calming visualisations - -### Set1, Set2, Set3 - -```javascript -d3.schemeSet1 // 9 colours - vivid -d3.schemeSet2 // 8 colours - muted -d3.schemeSet3 // 12 colours - pastel -``` - -**Characteristics:** -- Set1: High saturation, maximum distinction -- Set2: Professional, balanced -- Set3: Subtle, many categories - -**Use cases:** Varied based on visual hierarchy needs - -## Sequential colour schemes - -Sequential schemes map continuous data from low to high values using a single hue or gradient. - -### Single-hue sequential - -**Blues:** -```javascript -d3.interpolateBlues -d3.schemeBlues[9] // 9-step discrete version -``` - -**Other single-hue options:** -- `d3.interpolateGreens` / `d3.schemeGreens` -- `d3.interpolateOranges` / `d3.schemeOranges` -- `d3.interpolatePurples` / `d3.schemePurples` -- `d3.interpolateReds` / `d3.schemeReds` -- `d3.interpolateGreys` / `d3.schemeGreys` - -**Use cases:** -- Simple heat maps -- Choropleth maps -- Density plots -- Single-metric visualisations - -### Multi-hue sequential - -**Viridis (recommended):** -```javascript -d3.interpolateViridis -``` - -**Characteristics:** -- Perceptually uniform -- Colour-blind friendly -- Print-safe -- No visual dead zones -- Monotonically increasing perceived lightness - -**Other perceptually-uniform options:** -- `d3.interpolatePlasma` - Purple to yellow -- `d3.interpolateInferno` - Black to white through red/orange -- `d3.interpolateMagma` - Black to white through purple -- `d3.interpolateCividis` - Colour-blind optimised - -**Colour-blind accessible:** -```javascript -d3.interpolateTurbo // Rainbow-like but perceptually uniform -d3.interpolateCool // Cyan to magenta -d3.interpolateWarm // Orange to yellow -``` - -**Use cases:** -- Scientific visualisation -- Medical imaging -- Any high-precision data visualisation -- Accessible visualisations - -### Traditional sequential - -**Yellow-Orange-Red:** -```javascript -d3.interpolateYlOrRd -d3.schemeYlOrRd[9] -``` - -**Yellow-Green-Blue:** -```javascript -d3.interpolateYlGnBu -d3.schemeYlGnBu[9] -``` - -**Other multi-hue:** -- `d3.interpolateBuGn` - Blue to green -- `d3.interpolateBuPu` - Blue to purple -- `d3.interpolateGnBu` - Green to blue -- `d3.interpolateOrRd` - Orange to red -- `d3.interpolatePuBu` - Purple to blue -- `d3.interpolatePuBuGn` - Purple to blue-green -- `d3.interpolatePuRd` - Purple to red -- `d3.interpolateRdPu` - Red to purple -- `d3.interpolateYlGn` - Yellow to green -- `d3.interpolateYlOrBr` - Yellow to orange-brown - -**Use cases:** Traditional data visualisation, familiar colour associations (temperature, vegetation, water) - -## Diverging colour schemes - -Diverging schemes highlight deviations from a central value using two distinct hues. - -### Red-Blue (temperature) - -```javascript -d3.interpolateRdBu -d3.schemeRdBu[11] -``` - -**Characteristics:** -- Intuitive temperature metaphor -- Strong contrast -- Clear positive/negative distinction - -**Use cases:** Temperature, profit/loss, above/below average, correlation - -### Red-Yellow-Blue - -```javascript -d3.interpolateRdYlBu -d3.schemeRdYlBu[11] -``` - -**Characteristics:** -- Three-colour gradient -- Softer transition through yellow -- More visual steps - -**Use cases:** When extreme values need emphasis and middle needs visibility - -### Other diverging schemes - -**Traffic light:** -```javascript -d3.interpolateRdYlGn // Red (bad) to green (good) -``` - -**Spectral (rainbow):** -```javascript -d3.interpolateSpectral // Full spectrum -``` - -**Other options:** -- `d3.interpolateBrBG` - Brown to blue-green -- `d3.interpolatePiYG` - Pink to yellow-green -- `d3.interpolatePRGn` - Purple to green -- `d3.interpolatePuOr` - Purple to orange -- `d3.interpolateRdGy` - Red to grey - -**Use cases:** Choose based on semantic meaning and accessibility needs - -## Colour-blind friendly palettes - -### General guidelines - -1. **Avoid red-green combinations** (most common colour blindness) -2. **Use blue-orange diverging** instead of red-green -3. **Add texture or patterns** as redundant encoding -4. **Test with simulation tools** - -### Recommended colour-blind safe schemes - -**Categorical:** -```javascript -// Okabe-Ito palette (colour-blind safe) -const okabePalette = [ - '#E69F00', // Orange - '#56B4E9', // Sky blue - '#009E73', // Bluish green - '#F0E442', // Yellow - '#0072B2', // Blue - '#D55E00', // Vermillion - '#CC79A7', // Reddish purple - '#000000' // Black -]; - -const colourScale = d3.scaleOrdinal() - .domain(categories) - .range(okabePalette); -``` - -**Sequential:** -```javascript -// Use Viridis, Cividis, or Blues -d3.interpolateViridis // Best overall -d3.interpolateCividis // Optimised for CVD -d3.interpolateBlues // Simple, safe -``` - -**Diverging:** -```javascript -// Use blue-orange instead of red-green -d3.interpolateBrBG -d3.interpolatePuOr -``` - -## Custom colour palettes - -### Creating custom sequential - -```javascript -const customSequential = d3.scaleLinear() - .domain([0, 100]) - .range(['#e8f4f8', '#006d9c']) // Light to dark blue - .interpolate(d3.interpolateLab); // Perceptually uniform -``` - -### Creating custom diverging - -```javascript -const customDiverging = d3.scaleLinear() - .domain([0, 50, 100]) - .range(['#ca0020', '#f7f7f7', '#0571b0']) // Red, grey, blue - .interpolate(d3.interpolateLab); -``` - -### Creating custom categorical - -```javascript -// Brand colours -const brandPalette = [ - '#FF6B6B', // Primary red - '#4ECDC4', // Secondary teal - '#45B7D1', // Tertiary blue - '#FFA07A', // Accent coral - '#98D8C8' // Accent mint -]; - -const colourScale = d3.scaleOrdinal() - .domain(categories) - .range(brandPalette); -``` - -## Semantic colour associations - -### Universal colour meanings - -**Red:** -- Danger, error, negative -- High temperature -- Debt, loss - -**Green:** -- Success, positive -- Growth, vegetation -- Profit, gain - -**Blue:** -- Trust, calm -- Water, cold -- Information, neutral - -**Yellow/Orange:** -- Warning, caution -- Energy, warmth -- Attention - -**Grey:** -- Neutral, inactive -- Missing data -- Background - -### Context-specific palettes - -**Financial:** -```javascript -const financialColours = { - profit: '#27ae60', - loss: '#e74c3c', - neutral: '#95a5a6', - highlight: '#3498db' -}; -``` - -**Temperature:** -```javascript -const temperatureScale = d3.scaleSequential(d3.interpolateRdYlBu) - .domain([40, -10]); // Hot to cold (reversed) -``` - -**Traffic/Status:** -```javascript -const statusColours = { - success: '#27ae60', - warning: '#f39c12', - error: '#e74c3c', - info: '#3498db', - neutral: '#95a5a6' -}; -``` - -## Accessibility best practices - -### Contrast ratios - -Ensure sufficient contrast between colours and backgrounds: - -```javascript -// Good contrast example -const highContrast = { - background: '#ffffff', - text: '#2c3e50', - primary: '#3498db', - secondary: '#e74c3c' -}; -``` - -**WCAG guidelines:** -- Normal text: 4.5:1 minimum -- Large text: 3:1 minimum -- UI components: 3:1 minimum - -### Redundant encoding - -Never rely solely on colour to convey information: - -```javascript -// Add patterns or shapes -const symbols = ['circle', 'square', 'triangle', 'diamond']; - -// Add text labels -// Use line styles (solid, dashed, dotted) -// Use size encoding -``` - -### Testing - -Test visualisations for colour blindness: -- Chrome DevTools (Rendering > Emulate vision deficiencies) -- Colour Oracle (free desktop application) -- Coblis (online simulator) - -## Professional colour recommendations - -### Data journalism - -```javascript -// Guardian style -const guardianPalette = [ - '#005689', // Guardian blue - '#c70000', // Guardian red - '#7d0068', // Guardian pink - '#951c75', // Guardian purple -]; - -// FT style -const ftPalette = [ - '#0f5499', // FT blue - '#990f3d', // FT red - '#593380', // FT purple - '#262a33', // FT black -]; -``` - -### Academic/Scientific - -```javascript -// Nature journal style -const naturePalette = [ - '#0071b2', // Blue - '#d55e00', // Vermillion - '#009e73', // Green - '#f0e442', // Yellow -]; - -// Use Viridis for continuous data -const scientificScale = d3.scaleSequential(d3.interpolateViridis); -``` - -### Corporate/Business - -```javascript -// Professional, conservative -const corporatePalette = [ - '#003f5c', // Dark blue - '#58508d', // Purple - '#bc5090', // Magenta - '#ff6361', // Coral - '#ffa600' // Orange -]; -``` - -## Dynamic colour selection - -### Based on data range - -```javascript -function selectColourScheme(data) { - const extent = d3.extent(data); - const hasNegative = extent[0] < 0; - const hasPositive = extent[1] > 0; - - if (hasNegative && hasPositive) { - // Diverging: data crosses zero - return d3.scaleSequentialSymlog(d3.interpolateRdBu) - .domain([extent[0], 0, extent[1]]); - } else { - // Sequential: all positive or all negative - return d3.scaleSequential(d3.interpolateViridis) - .domain(extent); - } -} -``` - -### Based on category count - -```javascript -function selectCategoricalScheme(categories) { - const n = categories.length; - - if (n <= 10) { - return d3.scaleOrdinal(d3.schemeTableau10); - } else if (n <= 12) { - return d3.scaleOrdinal(d3.schemePaired); - } else { - // For many categories, use sequential with quantize - return d3.scaleQuantize() - .domain([0, n - 1]) - .range(d3.quantize(d3.interpolateRainbow, n)); - } -} -``` - -## Common colour mistakes to avoid - -1. **Rainbow gradients for sequential data** - - Problem: Not perceptually uniform, hard to read - - Solution: Use Viridis, Blues, or other uniform schemes - -2. **Red-green for diverging (colour blindness)** - - Problem: 8% of males can't distinguish - - Solution: Use blue-orange or purple-green - -3. **Too many categorical colours** - - Problem: Hard to distinguish and remember - - Solution: Limit to 5-8 categories, use grouping - -4. **Insufficient contrast** - - Problem: Poor readability - - Solution: Test contrast ratios, use darker colours on light backgrounds - -5. **Culturally inconsistent colours** - - Problem: Confusing semantic meaning - - Solution: Research colour associations for target audience - -6. **Inverted temperature scales** - - Problem: Counterintuitive (red = cold) - - Solution: Red/orange = hot, blue = cold - -## Quick reference guide - -**Need to show...** - -- **Categories (≤10):** `d3.schemeCategory10` or `d3.schemeTableau10` -- **Categories (>10):** `d3.schemePaired` or group categories -- **Sequential (general):** `d3.interpolateViridis` -- **Sequential (scientific):** `d3.interpolateViridis` or `d3.interpolatePlasma` -- **Sequential (temperature):** `d3.interpolateRdYlBu` (inverted) -- **Diverging (zero):** `d3.interpolateRdBu` or `d3.interpolateBrBG` -- **Diverging (good/bad):** `d3.interpolateRdYlGn` (inverted) -- **Colour-blind safe (categorical):** Okabe-Ito palette (shown above) -- **Colour-blind safe (sequential):** `d3.interpolateCividis` or `d3.interpolateBlues` -- **Colour-blind safe (diverging):** `d3.interpolatePuOr` or `d3.interpolateBrBG` - -**Always remember:** -1. Test for colour-blindness -2. Ensure sufficient contrast -3. Use semantic colours appropriately -4. Add redundant encoding (patterns, labels) -5. Keep it simple (fewer colours = clearer visualisation) \ No newline at end of file diff --git a/.claude/skills/d3-viz/references/d3-patterns.md b/.claude/skills/d3-viz/references/d3-patterns.md deleted file mode 100644 index 0b36a0b..0000000 --- a/.claude/skills/d3-viz/references/d3-patterns.md +++ /dev/null @@ -1,869 +0,0 @@ -# D3.js Visualisation Patterns - -This reference provides detailed code patterns for common d3.js visualisation types. - -## Hierarchical visualisations - -### Tree diagram - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const tree = d3.tree().size([height - 100, width - 200]); - - const root = d3.hierarchy(data); - tree(root); - - const g = svg.append("g") - .attr("transform", "translate(100,50)"); - - // Links - g.selectAll("path") - .data(root.links()) - .join("path") - .attr("d", d3.linkHorizontal() - .x(d => d.y) - .y(d => d.x)) - .attr("fill", "none") - .attr("stroke", "#555") - .attr("stroke-width", 2); - - // Nodes - const node = g.selectAll("g") - .data(root.descendants()) - .join("g") - .attr("transform", d => `translate(${d.y},${d.x})`); - - node.append("circle") - .attr("r", 6) - .attr("fill", d => d.children ? "#555" : "#999"); - - node.append("text") - .attr("dy", "0.31em") - .attr("x", d => d.children ? -8 : 8) - .attr("text-anchor", d => d.children ? "end" : "start") - .text(d => d.data.name) - .style("font-size", "12px"); - -}, [data]); -``` - -### Treemap - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const root = d3.hierarchy(data) - .sum(d => d.value) - .sort((a, b) => b.value - a.value); - - d3.treemap() - .size([width, height]) - .padding(2) - .round(true)(root); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const cell = svg.selectAll("g") - .data(root.leaves()) - .join("g") - .attr("transform", d => `translate(${d.x0},${d.y0})`); - - cell.append("rect") - .attr("width", d => d.x1 - d.x0) - .attr("height", d => d.y1 - d.y0) - .attr("fill", d => colourScale(d.parent.data.name)) - .attr("stroke", "white") - .attr("stroke-width", 2); - - cell.append("text") - .attr("x", 4) - .attr("y", 16) - .text(d => d.data.name) - .style("font-size", "12px") - .style("fill", "white"); - -}, [data]); -``` - -### Sunburst diagram - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const radius = Math.min(width, height) / 2; - - const root = d3.hierarchy(data) - .sum(d => d.value) - .sort((a, b) => b.value - a.value); - - const partition = d3.partition() - .size([2 * Math.PI, radius]); - - partition(root); - - const arc = d3.arc() - .startAngle(d => d.x0) - .endAngle(d => d.x1) - .innerRadius(d => d.y0) - .outerRadius(d => d.y1); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - g.selectAll("path") - .data(root.descendants()) - .join("path") - .attr("d", arc) - .attr("fill", d => colourScale(d.depth)) - .attr("stroke", "white") - .attr("stroke-width", 1); - -}, [data]); -``` - -### Chord diagram - -```javascript -function drawChordDiagram(data) { - // data format: array of objects with source, target, and value - // Example: [{ source: 'A', target: 'B', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const innerRadius = Math.min(width, height) * 0.3; - const outerRadius = innerRadius + 30; - - // Create matrix from data - const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); - const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); - - data.forEach(d => { - const i = nodes.indexOf(d.source); - const j = nodes.indexOf(d.target); - matrix[i][j] += d.value; - matrix[j][i] += d.value; - }); - - // Create chord layout - const chord = d3.chord() - .padAngle(0.05) - .sortSubgroups(d3.descending); - - const arc = d3.arc() - .innerRadius(innerRadius) - .outerRadius(outerRadius); - - const ribbon = d3.ribbon() - .source(d => d.source) - .target(d => d.target); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10) - .domain(nodes); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - const chords = chord(matrix); - - // Draw ribbons - g.append("g") - .attr("fill-opacity", 0.67) - .selectAll("path") - .data(chords) - .join("path") - .attr("d", ribbon) - .attr("fill", d => colourScale(nodes[d.source.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); - - // Draw groups (arcs) - const group = g.append("g") - .selectAll("g") - .data(chords.groups) - .join("g"); - - group.append("path") - .attr("d", arc) - .attr("fill", d => colourScale(nodes[d.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); - - // Add labels - group.append("text") - .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) - .attr("dy", "0.31em") - .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) - .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) - .text((d, i) => nodes[i]) - .style("font-size", "12px"); -} - -// Data format example: -// const data = [ -// { source: 'Category A', target: 'Category B', value: 100 }, -// { source: 'Category A', target: 'Category C', value: 50 }, -// { source: 'Category B', target: 'Category C', value: 75 } -// ]; -// drawChordDiagram(data); -``` - -## Advanced chart types - -### Heatmap - -```javascript -function drawHeatmap(data) { - // data format: array of objects with row, column, and value - // Example: [{ row: 'A', column: 'X', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 100, right: 30, bottom: 30, left: 100 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Get unique rows and columns - const rows = Array.from(new Set(data.map(d => d.row))); - const columns = Array.from(new Set(data.map(d => d.column))); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(columns) - .range([0, innerWidth]) - .padding(0.01); - - const yScale = d3.scaleBand() - .domain(rows) - .range([0, innerHeight]) - .padding(0.01); - - // Colour scale for values (sequential from light to dark red) - const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) - .domain([0, d3.max(data, d => d.value)]); - - // Draw rectangles - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.column)) - .attr("y", d => yScale(d.row)) - .attr("width", xScale.bandwidth()) - .attr("height", yScale.bandwidth()) - .attr("fill", d => colourScale(d.value)); - - // Add x-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(columns) - .join("text") - .attr("x", d => xScale(d) + xScale.bandwidth() / 2) - .attr("y", -10) - .attr("text-anchor", "middle") - .text(d => d) - .style("font-size", "12px"); - - // Add y-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(rows) - .join("text") - .attr("x", -10) - .attr("y", d => yScale(d) + yScale.bandwidth() / 2) - .attr("dy", "0.35em") - .attr("text-anchor", "end") - .text(d => d) - .style("font-size", "12px"); - - // Add colour legend - const legendWidth = 20; - const legendHeight = 200; - const legend = svg.append("g") - .attr("transform", `translate(${width - 60},${margin.top})`); - - const legendScale = d3.scaleLinear() - .domain(colourScale.domain()) - .range([legendHeight, 0]); - - const legendAxis = d3.axisRight(legendScale).ticks(5); - - // Draw colour gradient in legend - for (let i = 0; i < legendHeight; i++) { - legend.append("rect") - .attr("y", i) - .attr("width", legendWidth) - .attr("height", 1) - .attr("fill", colourScale(legendScale.invert(i))); - } - - legend.append("g") - .attr("transform", `translate(${legendWidth},0)`) - .call(legendAxis); -} - -// Data format example: -// const data = [ -// { row: 'Monday', column: 'Morning', value: 42 }, -// { row: 'Monday', column: 'Afternoon', value: 78 }, -// { row: 'Tuesday', column: 'Morning', value: 65 }, -// { row: 'Tuesday', column: 'Afternoon', value: 55 } -// ]; -// drawHeatmap(data); -``` - -### Area chart with gradient - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Define gradient - const defs = svg.append("defs"); - const gradient = defs.append("linearGradient") - .attr("id", "areaGradient") - .attr("x1", "0%") - .attr("x2", "0%") - .attr("y1", "0%") - .attr("y2", "100%"); - - gradient.append("stop") - .attr("offset", "0%") - .attr("stop-color", "steelblue") - .attr("stop-opacity", 0.8); - - gradient.append("stop") - .attr("offset", "100%") - .attr("stop-color", "steelblue") - .attr("stop-opacity", 0.1); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleTime() - .domain(d3.extent(data, d => d.date)) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]); - - const area = d3.area() - .x(d => xScale(d.date)) - .y0(innerHeight) - .y1(d => yScale(d.value)) - .curve(d3.curveMonotoneX); - - g.append("path") - .datum(data) - .attr("fill", "url(#areaGradient)") - .attr("d", area); - - const line = d3.line() - .x(d => xScale(d.date)) - .y(d => yScale(d.value)) - .curve(d3.curveMonotoneX); - - g.append("path") - .datum(data) - .attr("fill", "none") - .attr("stroke", "steelblue") - .attr("stroke-width", 2) - .attr("d", line); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Stacked bar chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const categories = Object.keys(data[0]).filter(k => k !== 'group'); - const stackedData = d3.stack().keys(categories)(data); - - const xScale = d3.scaleBand() - .domain(data.map(d => d.group)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])]) - .range([innerHeight, 0]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - g.selectAll("g") - .data(stackedData) - .join("g") - .attr("fill", (d, i) => colourScale(i)) - .selectAll("rect") - .data(d => d) - .join("rect") - .attr("x", d => xScale(d.data.group)) - .attr("y", d => yScale(d[1])) - .attr("height", d => yScale(d[0]) - yScale(d[1])) - .attr("width", xScale.bandwidth()); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Grouped bar chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const categories = Object.keys(data[0]).filter(k => k !== 'group'); - - const x0Scale = d3.scaleBand() - .domain(data.map(d => d.group)) - .range([0, innerWidth]) - .padding(0.1); - - const x1Scale = d3.scaleBand() - .domain(categories) - .range([0, x0Scale.bandwidth()]) - .padding(0.05); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => Math.max(...categories.map(c => d[c])))]) - .range([innerHeight, 0]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const group = g.selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(${x0Scale(d.group)},0)`); - - group.selectAll("rect") - .data(d => categories.map(key => ({ key, value: d[key] }))) - .join("rect") - .attr("x", d => x1Scale(d.key)) - .attr("y", d => yScale(d.value)) - .attr("width", x1Scale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", d => colourScale(d.key)); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(x0Scale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Bubble chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); - - const sizeScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.size)]) - .range([0, 50]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size)) - .attr("fill", d => colourScale(d.category)) - .attr("opacity", 0.6) - .attr("stroke", "white") - .attr("stroke-width", 2); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -## Geographic visualisations - -### Basic map with points - -```javascript -useEffect(() => { - if (!geoData || !pointData) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const projection = d3.geoMercator() - .fitSize([width, height], geoData); - - const pathGenerator = d3.geoPath().projection(projection); - - // Draw map - svg.selectAll("path") - .data(geoData.features) - .join("path") - .attr("d", pathGenerator) - .attr("fill", "#e0e0e0") - .attr("stroke", "#999") - .attr("stroke-width", 0.5); - - // Draw points - svg.selectAll("circle") - .data(pointData) - .join("circle") - .attr("cx", d => projection([d.longitude, d.latitude])[0]) - .attr("cy", d => projection([d.longitude, d.latitude])[1]) - .attr("r", 5) - .attr("fill", "steelblue") - .attr("opacity", 0.7); - -}, [geoData, pointData]); -``` - -### Choropleth map - -```javascript -useEffect(() => { - if (!geoData || !valueData) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const projection = d3.geoMercator() - .fitSize([width, height], geoData); - - const pathGenerator = d3.geoPath().projection(projection); - - // Create value lookup - const valueLookup = new Map(valueData.map(d => [d.id, d.value])); - - // Colour scale - const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, d3.max(valueData, d => d.value)]); - - svg.selectAll("path") - .data(geoData.features) - .join("path") - .attr("d", pathGenerator) - .attr("fill", d => { - const value = valueLookup.get(d.id); - return value ? colourScale(value) : "#e0e0e0"; - }) - .attr("stroke", "#999") - .attr("stroke-width", 0.5); - -}, [geoData, valueData]); -``` - -## Advanced interactions - -### Brush and zoom - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const circles = g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 5) - .attr("fill", "steelblue"); - - // Add brush - const brush = d3.brush() - .extent([[0, 0], [innerWidth, innerHeight]]) - .on("start brush", (event) => { - if (!event.selection) return; - - const [[x0, y0], [x1, y1]] = event.selection; - - circles.attr("fill", d => { - const cx = xScale(d.x); - const cy = yScale(d.y); - return (cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1) - ? "orange" - : "steelblue"; - }); - }); - - g.append("g") - .attr("class", "brush") - .call(brush); - -}, [data]); -``` - -### Linked brushing between charts - -```javascript -function LinkedCharts({ data }) { - const [selectedPoints, setSelectedPoints] = useState(new Set()); - const svg1Ref = useRef(); - const svg2Ref = useRef(); - - useEffect(() => { - // Chart 1: Scatter plot - const svg1 = d3.select(svg1Ref.current); - svg1.selectAll("*").remove(); - - // ... create first chart ... - - const circles1 = svg1.selectAll("circle") - .data(data) - .join("circle") - .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); - - // Chart 2: Bar chart - const svg2 = d3.select(svg2Ref.current); - svg2.selectAll("*").remove(); - - // ... create second chart ... - - const bars = svg2.selectAll("rect") - .data(data) - .join("rect") - .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); - - // Add brush to first chart - const brush = d3.brush() - .on("start brush end", (event) => { - if (!event.selection) { - setSelectedPoints(new Set()); - return; - } - - const [[x0, y0], [x1, y1]] = event.selection; - const selected = new Set(); - - data.forEach(d => { - const x = xScale(d.x); - const y = yScale(d.y); - if (x >= x0 && x <= x1 && y >= y0 && y <= y1) { - selected.add(d.id); - } - }); - - setSelectedPoints(selected); - }); - - svg1.append("g").call(brush); - - }, [data, selectedPoints]); - - return ( -
- - -
- ); -} -``` - -## Animation patterns - -### Enter, update, exit with transitions - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - - const circles = svg.selectAll("circle") - .data(data, d => d.id); // Key function for object constancy - - // EXIT: Remove old elements - circles.exit() - .transition() - .duration(500) - .attr("r", 0) - .remove(); - - // UPDATE: Modify existing elements - circles - .transition() - .duration(500) - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("fill", "steelblue"); - - // ENTER: Add new elements - circles.enter() - .append("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 0) - .attr("fill", "steelblue") - .transition() - .duration(500) - .attr("r", 5); - -}, [data]); -``` - -### Path morphing - -```javascript -useEffect(() => { - if (!data1 || !data2) return; - - const svg = d3.select(svgRef.current); - - const line = d3.line() - .x(d => xScale(d.x)) - .y(d => yScale(d.y)) - .curve(d3.curveMonotoneX); - - const path = svg.select("path"); - - // Morph from data1 to data2 - path - .datum(data1) - .attr("d", line) - .transition() - .duration(1000) - .attrTween("d", function() { - const previous = d3.select(this).attr("d"); - const current = line(data2); - return d3.interpolatePath(previous, current); - }); - -}, [data1, data2]); -``` \ No newline at end of file diff --git a/.claude/skills/d3-viz/references/scale-reference.md b/.claude/skills/d3-viz/references/scale-reference.md deleted file mode 100644 index 61bd981..0000000 --- a/.claude/skills/d3-viz/references/scale-reference.md +++ /dev/null @@ -1,509 +0,0 @@ -# D3.js Scale Reference - -Comprehensive guide to all d3 scale types with examples and use cases. - -## Continuous scales - -### Linear scale - -Maps continuous input domain to continuous output range with linear interpolation. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -scale(50); // Returns 250 -scale(0); // Returns 0 -scale(100); // Returns 500 - -// Invert scale (get input from output) -scale.invert(250); // Returns 50 -``` - -**Use cases:** -- Most common scale for quantitative data -- Axes, bar lengths, position encoding -- Temperature, prices, counts, measurements - -**Methods:** -- `.domain([min, max])` - Set input domain -- `.range([min, max])` - Set output range -- `.invert(value)` - Get domain value from range value -- `.clamp(true)` - Restrict output to range bounds -- `.nice()` - Extend domain to nice round values - -### Power scale - -Maps continuous input to continuous output with exponential transformation. - -```javascript -const sqrtScale = d3.scalePow() - .exponent(0.5) // Square root - .domain([0, 100]) - .range([0, 500]); - -const squareScale = d3.scalePow() - .exponent(2) // Square - .domain([0, 100]) - .range([0, 500]); - -// Shorthand for square root -const sqrtScale2 = d3.scaleSqrt() - .domain([0, 100]) - .range([0, 500]); -``` - -**Use cases:** -- Perceptual scaling (human perception is non-linear) -- Area encoding (use square root to map values to circle radii) -- Emphasising differences in small or large values - -### Logarithmic scale - -Maps continuous input to continuous output with logarithmic transformation. - -```javascript -const logScale = d3.scaleLog() - .domain([1, 1000]) // Must be positive - .range([0, 500]); - -logScale(1); // Returns 0 -logScale(10); // Returns ~167 -logScale(100); // Returns ~333 -logScale(1000); // Returns 500 -``` - -**Use cases:** -- Data spanning multiple orders of magnitude -- Population, GDP, wealth distributions -- Logarithmic axes -- Exponential growth visualisations - -**Important:** Domain values must be strictly positive (>0). - -### Time scale - -Specialised linear scale for temporal data. - -```javascript -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) - .range([0, 800]); - -timeScale(new Date(2022, 0, 1)); // Returns 400 - -// Invert to get date -timeScale.invert(400); // Returns Date object for mid-2022 -``` - -**Use cases:** -- Time series visualisations -- Timeline axes -- Temporal animations -- Date-based interactions - -**Methods:** -- `.nice()` - Extend domain to nice time intervals -- `.ticks(count)` - Generate nicely-spaced tick values -- All linear scale methods apply - -### Quantize scale - -Maps continuous input to discrete output buckets. - -```javascript -const quantizeScale = d3.scaleQuantize() - .domain([0, 100]) - .range(['low', 'medium', 'high']); - -quantizeScale(25); // Returns 'low' -quantizeScale(50); // Returns 'medium' -quantizeScale(75); // Returns 'high' - -// Get the threshold values -quantizeScale.thresholds(); // Returns [33.33, 66.67] -``` - -**Use cases:** -- Binning continuous data -- Heat map colours -- Risk categories (low/medium/high) -- Age groups, income brackets - -### Quantile scale - -Maps continuous input to discrete output based on quantiles. - -```javascript -const quantileScale = d3.scaleQuantile() - .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // Sample data - .range(['low', 'medium', 'high']); - -quantileScale(8); // Returns based on quantile position -quantileScale.quantiles(); // Returns quantile thresholds -``` - -**Use cases:** -- Equal-size groups regardless of distribution -- Percentile-based categorisation -- Handling skewed distributions - -### Threshold scale - -Maps continuous input to discrete output with custom thresholds. - -```javascript -const thresholdScale = d3.scaleThreshold() - .domain([0, 10, 20]) - .range(['freezing', 'cold', 'warm', 'hot']); - -thresholdScale(-5); // Returns 'freezing' -thresholdScale(5); // Returns 'cold' -thresholdScale(15); // Returns 'warm' -thresholdScale(25); // Returns 'hot' -``` - -**Use cases:** -- Custom breakpoints -- Grade boundaries (A, B, C, D, F) -- Temperature categories -- Air quality indices - -## Sequential scales - -### Sequential colour scale - -Maps continuous input to continuous colour gradient. - -```javascript -const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, 100]); - -colourScale(0); // Returns lightest blue -colourScale(50); // Returns mid blue -colourScale(100); // Returns darkest blue -``` - -**Available interpolators:** - -**Single hue:** -- `d3.interpolateBlues`, `d3.interpolateGreens`, `d3.interpolateReds` -- `d3.interpolateOranges`, `d3.interpolatePurples`, `d3.interpolateGreys` - -**Multi-hue:** -- `d3.interpolateViridis`, `d3.interpolateInferno`, `d3.interpolateMagma` -- `d3.interpolatePlasma`, `d3.interpolateWarm`, `d3.interpolateCool` -- `d3.interpolateCubehelixDefault`, `d3.interpolateTurbo` - -**Use cases:** -- Heat maps, choropleth maps -- Continuous data visualisation -- Temperature, elevation, density - -### Diverging colour scale - -Maps continuous input to diverging colour gradient with a midpoint. - -```javascript -const divergingScale = d3.scaleDiverging(d3.interpolateRdBu) - .domain([-10, 0, 10]); - -divergingScale(-10); // Returns red -divergingScale(0); // Returns white/neutral -divergingScale(10); // Returns blue -``` - -**Available interpolators:** -- `d3.interpolateRdBu` - Red to blue -- `d3.interpolateRdYlBu` - Red, yellow, blue -- `d3.interpolateRdYlGn` - Red, yellow, green -- `d3.interpolatePiYG` - Pink, yellow, green -- `d3.interpolateBrBG` - Brown, blue-green -- `d3.interpolatePRGn` - Purple, green -- `d3.interpolatePuOr` - Purple, orange -- `d3.interpolateRdGy` - Red, grey -- `d3.interpolateSpectral` - Rainbow spectrum - -**Use cases:** -- Data with meaningful midpoint (zero, average, neutral) -- Positive/negative values -- Above/below comparisons -- Correlation matrices - -### Sequential quantile scale - -Combines sequential colour with quantile mapping. - -```javascript -const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues) - .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]); - -// Maps based on quantile position -``` - -**Use cases:** -- Perceptually uniform binning -- Handling outliers -- Skewed distributions - -## Ordinal scales - -### Band scale - -Maps discrete input to continuous bands (rectangles) with optional padding. - -```javascript -const bandScale = d3.scaleBand() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.1); - -bandScale('A'); // Returns start position (e.g., 0) -bandScale('B'); // Returns start position (e.g., 110) -bandScale.bandwidth(); // Returns width of each band (e.g., 95) -bandScale.step(); // Returns total step including padding -bandScale.paddingInner(); // Returns inner padding (between bands) -bandScale.paddingOuter(); // Returns outer padding (at edges) -``` - -**Use cases:** -- Bar charts (most common use case) -- Grouped elements -- Categorical axes -- Heat map cells - -**Padding options:** -- `.padding(value)` - Sets both inner and outer padding (0-1) -- `.paddingInner(value)` - Padding between bands (0-1) -- `.paddingOuter(value)` - Padding at edges (0-1) -- `.align(value)` - Alignment of bands (0-1, default 0.5) - -### Point scale - -Maps discrete input to continuous points (no width). - -```javascript -const pointScale = d3.scalePoint() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.5); - -pointScale('A'); // Returns position (e.g., 50) -pointScale('B'); // Returns position (e.g., 150) -pointScale('C'); // Returns position (e.g., 250) -pointScale('D'); // Returns position (e.g., 350) -pointScale.step(); // Returns distance between points -``` - -**Use cases:** -- Line chart categorical x-axis -- Scatter plot with categorical axis -- Node positions in network graphs -- Any point positioning for categories - -### Ordinal colour scale - -Maps discrete input to discrete output (colours, shapes, etc.). - -```javascript -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - -colourScale('apples'); // Returns first colour -colourScale('oranges'); // Returns second colour -colourScale('apples'); // Returns same first colour (consistent) - -// Custom range -const customScale = d3.scaleOrdinal() - .domain(['cat1', 'cat2', 'cat3']) - .range(['#FF6B6B', '#4ECDC4', '#45B7D1']); -``` - -**Built-in colour schemes:** - -**Categorical:** -- `d3.schemeCategory10` - 10 colours -- `d3.schemeAccent` - 8 colours -- `d3.schemeDark2` - 8 colours -- `d3.schemePaired` - 12 colours -- `d3.schemePastel1` - 9 colours -- `d3.schemePastel2` - 8 colours -- `d3.schemeSet1` - 9 colours -- `d3.schemeSet2` - 8 colours -- `d3.schemeSet3` - 12 colours -- `d3.schemeTableau10` - 10 colours - -**Use cases:** -- Category colours -- Legend items -- Multi-series charts -- Network node types - -## Scale utilities - -### Nice domain - -Extend domain to nice round values. - -```javascript -const scale = d3.scaleLinear() - .domain([0.201, 0.996]) - .nice(); - -scale.domain(); // Returns [0.2, 1.0] - -// With count (approximate tick count) -const scale2 = d3.scaleLinear() - .domain([0.201, 0.996]) - .nice(5); -``` - -### Clamping - -Restrict output to range bounds. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]) - .clamp(true); - -scale(-10); // Returns 0 (clamped) -scale(150); // Returns 500 (clamped) -``` - -### Copy scales - -Create independent copies. - -```javascript -const scale1 = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -const scale2 = scale1.copy(); -// scale2 is independent of scale1 -``` - -### Tick generation - -Generate nice tick values for axes. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -scale.ticks(10); // Generate ~10 ticks -scale.tickFormat(10); // Get format function for ticks -scale.tickFormat(10, ".2f"); // Custom format (2 decimal places) - -// Time scale ticks -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]); - -timeScale.ticks(d3.timeYear); // Yearly ticks -timeScale.ticks(d3.timeMonth, 3); // Every 3 months -timeScale.tickFormat(5, "%Y-%m"); // Format as year-month -``` - -## Colour spaces and interpolation - -### RGB interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]); -// Default: RGB interpolation -``` - -### HSL interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateHsl); -// Smoother colour transitions -``` - -### Lab interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateLab); -// Perceptually uniform -``` - -### HCL interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateHcl); -// Perceptually uniform with hue -``` - -## Common patterns - -### Diverging scale with custom midpoint - -```javascript -const scale = d3.scaleLinear() - .domain([min, midpoint, max]) - .range(["red", "white", "blue"]) - .interpolate(d3.interpolateHcl); -``` - -### Multi-stop gradient scale - -```javascript -const scale = d3.scaleLinear() - .domain([0, 25, 50, 75, 100]) - .range(["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#66c2a5"]); -``` - -### Radius scale for circles (perceptual) - -```javascript -const radiusScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.value)]) - .range([0, 50]); - -// Use with circles -circle.attr("r", d => radiusScale(d.value)); -``` - -### Adaptive scale based on data range - -```javascript -function createAdaptiveScale(data) { - const extent = d3.extent(data); - const range = extent[1] - extent[0]; - - // Use log scale if data spans >2 orders of magnitude - if (extent[1] / extent[0] > 100) { - return d3.scaleLog() - .domain(extent) - .range([0, width]); - } - - // Otherwise use linear - return d3.scaleLinear() - .domain(extent) - .range([0, width]); -} -``` - -### Colour scale with explicit categories - -```javascript -const colourScale = d3.scaleOrdinal() - .domain(['Low Risk', 'Medium Risk', 'High Risk']) - .range(['#2ecc71', '#f39c12', '#e74c3c']) - .unknown('#95a5a6'); // Fallback for unknown values -``` \ No newline at end of file diff --git a/.claude/skills/interactive-portfolio/SKILL.md b/.claude/skills/interactive-portfolio/SKILL.md deleted file mode 100644 index 110f519..0000000 --- a/.claude/skills/interactive-portfolio/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: interactive-portfolio -description: "Expert in building portfolios that actually land jobs and clients - not just showing work, but creating memorable experiences. Covers developer portfolios, designer portfolios, creative portfolios, and portfolios that convert visitors into opportunities. Use when: portfolio, personal website, showcase work, developer portfolio, designer portfolio." -source: vibeship-spawner-skills (Apache 2.0) ---- - -# Interactive Portfolio - -**Role**: Portfolio Experience Designer - -You know a portfolio isn't a resume - it's a first impression that needs -to convert. You balance creativity with usability. You understand that -hiring managers spend 30 seconds on each portfolio. You make those 30 -seconds count. You help people stand out without being gimmicky. - -## Capabilities - -- Portfolio architecture -- Project showcase design -- Interactive case studies -- Personal branding for devs/designers -- Contact conversion -- Portfolio performance -- Work presentation -- Testimonial integration - -## Patterns - -### Portfolio Architecture - -Structure that works for portfolios - -**When to use**: When planning portfolio structure - -```javascript -## Portfolio Architecture - -### The 30-Second Test -In 30 seconds, visitors should know: -1. Who you are -2. What you do -3. Your best work -4. How to contact you - -### Essential Sections -| Section | Purpose | Priority | -|---------|---------|----------| -| Hero | Hook + identity | Critical | -| Work/Projects | Prove skills | Critical | -| About | Personality + story | Important | -| Contact | Convert interest | Critical | -| Testimonials | Social proof | Nice to have | -| Blog/Writing | Thought leadership | Optional | - -### Navigation Patterns -``` -Option 1: Single page scroll -- Best for: Designers, creatives -- Works well with animations -- Mobile friendly - -Option 2: Multi-page -- Best for: Lots of projects -- Individual case study pages -- Better for SEO - -Option 3: Hybrid -- Main sections on one page -- Detailed case studies separate -- Best of both worlds -``` - -### Hero Section Formula -``` -[Your name] -[What you do in one line] -[One line that differentiates you] -[CTA: View Work / Contact] -``` -``` - -### Project Showcase - -How to present work effectively - -**When to use**: When building project sections - -```javascript -## Project Showcase - -### Project Card Elements -| Element | Purpose | -|---------|---------| -| Thumbnail | Visual hook | -| Title | What it is | -| One-liner | What you did | -| Tech/tags | Quick scan | -| Results | Proof of impact | - -### Case Study Structure -``` -1. Hero image/video -2. Project overview (2-3 sentences) -3. The challenge -4. Your role -5. Process highlights -6. Key decisions -7. Results/impact -8. Learnings (optional) -9. Links (live, GitHub, etc.) -``` - -### Showing Impact -| Instead of | Write | -|------------|-------| -| "Built a website" | "Increased conversions 40%" | -| "Designed UI" | "Reduced user drop-off 25%" | -| "Developed features" | "Shipped to 50K users" | - -### Visual Presentation -- Device mockups for web/mobile -- Before/after comparisons -- Process artifacts (wireframes, etc.) -- Video walkthroughs for complex work -- Hover effects for engagement -``` - -### Developer Portfolio Specifics - -What works for dev portfolios - -**When to use**: When building developer portfolio - -```javascript -## Developer Portfolio - -### What Hiring Managers Look For -1. Code quality (GitHub link) -2. Real projects (not just tutorials) -3. Problem-solving ability -4. Communication skills -5. Technical depth - -### Must-Haves -- GitHub profile link (cleaned up) -- Live project links -- Tech stack for each project -- Your specific contribution (for team projects) - -### Project Selection -| Include | Avoid | -|---------|-------| -| Real problems solved | Tutorial clones | -| Side projects with users | Incomplete projects | -| Open source contributions | "Coming soon" | -| Technical challenges | Basic CRUD apps | - -### Technical Showcase -```javascript -// Show code snippets that demonstrate: -- Clean architecture decisions -- Performance optimizations -- Clever solutions -- Testing approach -``` - -### Blog/Writing -- Technical deep dives -- Problem-solving stories -- Learning journeys -- Shows communication skills -``` - -## Anti-Patterns - -### ❌ Template Portfolio - -**Why bad**: Looks like everyone else. -No memorable impression. -Doesn't show creativity. -Easy to forget. - -**Instead**: Add personal touches. -Custom design elements. -Unique project presentations. -Your voice in the copy. - -### ❌ All Style No Substance - -**Why bad**: Fancy animations, weak projects. -Style over substance. -Hiring managers see through it. -No proof of skills. - -**Instead**: Projects first, style second. -Real work with real impact. -Quality over quantity. -Depth over breadth. - -### ❌ Resume Website - -**Why bad**: Boring, forgettable. -Doesn't use the medium. -No personality. -Lists instead of stories. - -**Instead**: Show, don't tell. -Visual case studies. -Interactive elements. -Personality throughout. - -## ⚠️ Sharp Edges - -| Issue | Severity | Solution | -|-------|----------|----------| -| Portfolio more complex than your actual work | medium | ## Right-Sizing Your Portfolio | -| Portfolio looks great on desktop, broken on mobile | high | ## Mobile-First Portfolio | -| Visitors don't know what to do next | medium | ## Portfolio CTAs | -| Portfolio shows old or irrelevant work | medium | ## Portfolio Freshness | - -## Related Skills - -Works well with: `scroll-experience`, `3d-web-experience`, `landing-page-design`, `personal-branding` diff --git a/.claude/skills/prd/SKILL.md b/.claude/skills/prd/SKILL.md deleted file mode 100644 index 3c2f7ac..0000000 --- a/.claude/skills/prd/SKILL.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: prd -description: "Generate a Product Requirements Document (PRD) for a new feature. Use when planning a feature, starting a new project, or when asked to create a PRD. Triggers on: create a prd, write prd for, plan this feature, requirements for, spec out." -user-invocable: true ---- - -# PRD Generator - -Create detailed Product Requirements Documents that are clear, actionable, and suitable for implementation. - ---- - -## The Job - -1. Receive a feature description from the user -2. Ask 3-5 essential clarifying questions (with lettered options) -3. Generate a structured PRD based on answers -4. Save to `tasks/prd-[feature-name].md` - -**Important:** Do NOT start implementing. Just create the PRD. - ---- - -## Step 1: Clarifying Questions - -Ask only critical questions where the initial prompt is ambiguous. Focus on: - -- **Problem/Goal:** What problem does this solve? -- **Core Functionality:** What are the key actions? -- **Scope/Boundaries:** What should it NOT do? -- **Success Criteria:** How do we know it's done? - -### Format Questions Like This: - -``` -1. What is the primary goal of this feature? - A. Improve user onboarding experience - B. Increase user retention - C. Reduce support burden - D. Other: [please specify] - -2. Who is the target user? - A. New users only - B. Existing users only - C. All users - D. Admin users only - -3. What is the scope? - A. Minimal viable version - B. Full-featured implementation - C. Just the backend/API - D. Just the UI -``` - -This lets users respond with "1A, 2C, 3B" for quick iteration. Remember to indent the options. - ---- - -## Step 2: PRD Structure - -Generate the PRD with these sections: - -### 1. Introduction/Overview -Brief description of the feature and the problem it solves. - -### 2. Goals -Specific, measurable objectives (bullet list). - -### 3. User Stories -Each story needs: -- **Title:** Short descriptive name -- **Description:** "As a [user], I want [feature] so that [benefit]" -- **Acceptance Criteria:** Verifiable checklist of what "done" means - -Each story should be small enough to implement in one focused session. - -**Format:** -```markdown -### US-001: [Title] -**Description:** As a [user], I want [feature] so that [benefit]. - -**Acceptance Criteria:** -- [ ] Specific verifiable criterion -- [ ] Another criterion -- [ ] Typecheck/lint passes -- [ ] **[UI stories only]** Verify in browser using dev-browser skill -``` - -**Important:** -- Acceptance criteria must be verifiable, not vague. "Works correctly" is bad. "Button shows confirmation dialog before deleting" is good. -- **For any story with UI changes:** Always include "Verify in browser using dev-browser skill" as acceptance criteria. This ensures visual verification of frontend work. - -### 4. Functional Requirements -Numbered list of specific functionalities: -- "FR-1: The system must allow users to..." -- "FR-2: When a user clicks X, the system must..." - -Be explicit and unambiguous. - -### 5. Non-Goals (Out of Scope) -What this feature will NOT include. Critical for managing scope. - -### 6. Design Considerations (Optional) -- UI/UX requirements -- Link to mockups if available -- Relevant existing components to reuse - -### 7. Technical Considerations (Optional) -- Known constraints or dependencies -- Integration points with existing systems -- Performance requirements - -### 8. Success Metrics -How will success be measured? -- "Reduce time to complete X by 50%" -- "Increase conversion rate by 10%" - -### 9. Open Questions -Remaining questions or areas needing clarification. - ---- - -## Writing for Junior Developers - -The PRD reader may be a junior developer or AI agent. Therefore: - -- Be explicit and unambiguous -- Avoid jargon or explain it -- Provide enough detail to understand purpose and core logic -- Number requirements for easy reference -- Use concrete examples where helpful - ---- - -## Output - -- **Format:** Markdown (`.md`) -- **Location:** `tasks/` -- **Filename:** `prd-[feature-name].md` (kebab-case) - ---- - -## Example PRD - -```markdown -# PRD: Task Priority System - -## Introduction - -Add priority levels to tasks so users can focus on what matters most. Tasks can be marked as high, medium, or low priority, with visual indicators and filtering to help users manage their workload effectively. - -## Goals - -- Allow assigning priority (high/medium/low) to any task -- Provide clear visual differentiation between priority levels -- Enable filtering and sorting by priority -- Default new tasks to medium priority - -## User Stories - -### US-001: Add priority field to database -**Description:** As a developer, I need to store task priority so it persists across sessions. - -**Acceptance Criteria:** -- [ ] Add priority column to tasks table: 'high' | 'medium' | 'low' (default 'medium') -- [ ] Generate and run migration successfully -- [ ] Typecheck passes - -### US-002: Display priority indicator on task cards -**Description:** As a user, I want to see task priority at a glance so I know what needs attention first. - -**Acceptance Criteria:** -- [ ] Each task card shows colored priority badge (red=high, yellow=medium, gray=low) -- [ ] Priority visible without hovering or clicking -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -### US-003: Add priority selector to task edit -**Description:** As a user, I want to change a task's priority when editing it. - -**Acceptance Criteria:** -- [ ] Priority dropdown in task edit modal -- [ ] Shows current priority as selected -- [ ] Saves immediately on selection change -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -### US-004: Filter tasks by priority -**Description:** As a user, I want to filter the task list to see only high-priority items when I'm focused. - -**Acceptance Criteria:** -- [ ] Filter dropdown with options: All | High | Medium | Low -- [ ] Filter persists in URL params -- [ ] Empty state message when no tasks match filter -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -## Functional Requirements - -- FR-1: Add `priority` field to tasks table ('high' | 'medium' | 'low', default 'medium') -- FR-2: Display colored priority badge on each task card -- FR-3: Include priority selector in task edit modal -- FR-4: Add priority filter dropdown to task list header -- FR-5: Sort by priority within each status column (high to medium to low) - -## Non-Goals - -- No priority-based notifications or reminders -- No automatic priority assignment based on due date -- No priority inheritance for subtasks - -## Technical Considerations - -- Reuse existing badge component with color variants -- Filter state managed via URL search params -- Priority stored in database, not computed - -## Success Metrics - -- Users can change priority in under 2 clicks -- High-priority tasks immediately visible at top of lists -- No regression in task list performance - -## Open Questions - -- Should priority affect task ordering within a column? -- Should we add keyboard shortcuts for priority changes? -``` - ---- - -## Checklist - -Before saving the PRD: - -- [ ] Asked clarifying questions with lettered options -- [ ] Incorporated user's answers -- [ ] User stories are small and specific -- [ ] Functional requirements are numbered and unambiguous -- [ ] Non-goals section defines clear boundaries -- [ ] Saved to `tasks/prd-[feature-name].md` diff --git a/.claude/skills/ralph/SKILL.md b/.claude/skills/ralph/SKILL.md deleted file mode 100644 index e402ab8..0000000 --- a/.claude/skills/ralph/SKILL.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -name: ralph -description: "Convert PRDs to prd.json format for the Ralph autonomous agent system. Use when you have an existing PRD and need to convert it to Ralph's JSON format. Triggers on: convert this prd, turn this into ralph format, create prd.json from this, ralph json." -user-invocable: true ---- - -# Ralph PRD Converter - -Converts existing PRDs to the prd.json format that Ralph uses for autonomous execution. - ---- - -## The Job - -Take a PRD (markdown file or text) and convert it to `prd.json` in your ralph directory. - ---- - -## Output Format - -```json -{ - "project": "[Project Name]", - "branchName": "ralph/[feature-name-kebab-case]", - "description": "[Feature description from PRD title/intro]", - "userStories": [ - { - "id": "US-001", - "title": "[Story title]", - "description": "As a [user], I want [feature] so that [benefit]", - "acceptanceCriteria": [ - "Criterion 1", - "Criterion 2", - "Typecheck passes" - ], - "priority": 1, - "passes": false, - "notes": "" - } - ] -} -``` - ---- - -## Story Size: The Number One Rule - -**Each story must be completable in ONE Ralph iteration (one context window).** - -Ralph spawns a fresh Amp instance per iteration with no memory of previous work. If a story is too big, the LLM runs out of context before finishing and produces broken code. - -### Right-sized stories: -- Add a database column and migration -- Add a UI component to an existing page -- Update a server action with new logic -- Add a filter dropdown to a list - -### Too big (split these): -- "Build the entire dashboard" - Split into: schema, queries, UI components, filters -- "Add authentication" - Split into: schema, middleware, login UI, session handling -- "Refactor the API" - Split into one story per endpoint or pattern - -**Rule of thumb:** If you cannot describe the change in 2-3 sentences, it is too big. - ---- - -## Story Ordering: Dependencies First - -Stories execute in priority order. Earlier stories must not depend on later ones. - -**Correct order:** -1. Schema/database changes (migrations) -2. Server actions / backend logic -3. UI components that use the backend -4. Dashboard/summary views that aggregate data - -**Wrong order:** -1. UI component (depends on schema that does not exist yet) -2. Schema change - ---- - -## Acceptance Criteria: Must Be Verifiable - -Each criterion must be something Ralph can CHECK, not something vague. - -### Good criteria (verifiable): -- "Add `status` column to tasks table with default 'pending'" -- "Filter dropdown has options: All, Active, Completed" -- "Clicking delete shows confirmation dialog" -- "Typecheck passes" -- "Tests pass" - -### Bad criteria (vague): -- "Works correctly" -- "User can do X easily" -- "Good UX" -- "Handles edge cases" - -### Always include as final criterion: -``` -"Typecheck passes" -``` - -For stories with testable logic, also include: -``` -"Tests pass" -``` - -### For stories that change UI, also include: -``` -"Verify in browser using dev-browser skill" -``` - -Frontend stories are NOT complete until visually verified. Ralph will use the dev-browser skill to navigate to the page, interact with the UI, and confirm changes work. - ---- - -## Conversion Rules - -1. **Each user story becomes one JSON entry** -2. **IDs**: Sequential (US-001, US-002, etc.) -3. **Priority**: Based on dependency order, then document order -4. **All stories**: `passes: false` and empty `notes` -5. **branchName**: Derive from feature name, kebab-case, prefixed with `ralph/` -6. **Always add**: "Typecheck passes" to every story's acceptance criteria - ---- - -## Splitting Large PRDs - -If a PRD has big features, split them: - -**Original:** -> "Add user notification system" - -**Split into:** -1. US-001: Add notifications table to database -2. US-002: Create notification service for sending notifications -3. US-003: Add notification bell icon to header -4. US-004: Create notification dropdown panel -5. US-005: Add mark-as-read functionality -6. US-006: Add notification preferences page - -Each is one focused change that can be completed and verified independently. - ---- - -## Example - -**Input PRD:** -```markdown -# Task Status Feature - -Add ability to mark tasks with different statuses. - -## Requirements -- Toggle between pending/in-progress/done on task list -- Filter list by status -- Show status badge on each task -- Persist status in database -``` - -**Output prd.json:** -```json -{ - "project": "TaskApp", - "branchName": "ralph/task-status", - "description": "Task Status Feature - Track task progress with status indicators", - "userStories": [ - { - "id": "US-001", - "title": "Add status field to tasks table", - "description": "As a developer, I need to store task status in the database.", - "acceptanceCriteria": [ - "Add status column: 'pending' | 'in_progress' | 'done' (default 'pending')", - "Generate and run migration successfully", - "Typecheck passes" - ], - "priority": 1, - "passes": false, - "notes": "" - }, - { - "id": "US-002", - "title": "Display status badge on task cards", - "description": "As a user, I want to see task status at a glance.", - "acceptanceCriteria": [ - "Each task card shows colored status badge", - "Badge colors: gray=pending, blue=in_progress, green=done", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 2, - "passes": false, - "notes": "" - }, - { - "id": "US-003", - "title": "Add status toggle to task list rows", - "description": "As a user, I want to change task status directly from the list.", - "acceptanceCriteria": [ - "Each row has status dropdown or toggle", - "Changing status saves immediately", - "UI updates without page refresh", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 3, - "passes": false, - "notes": "" - }, - { - "id": "US-004", - "title": "Filter tasks by status", - "description": "As a user, I want to filter the list to see only certain statuses.", - "acceptanceCriteria": [ - "Filter dropdown: All | Pending | In Progress | Done", - "Filter persists in URL params", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 4, - "passes": false, - "notes": "" - } - ] -} -``` - ---- - -## Archiving Previous Runs - -**Before writing a new prd.json, check if there is an existing one from a different feature:** - -1. Read the current `prd.json` if it exists -2. Check if `branchName` differs from the new feature's branch name -3. If different AND `progress.txt` has content beyond the header: - - Create archive folder: `archive/YYYY-MM-DD-feature-name/` - - Copy current `prd.json` and `progress.txt` to archive - - Reset `progress.txt` with fresh header - -**The ralph.sh script handles this automatically** when you run it, but if you are manually updating prd.json between runs, archive first. - ---- - -## Checklist Before Saving - -Before writing prd.json, verify: - -- [ ] **Previous run archived** (if prd.json exists with different branchName, archive it first) -- [ ] Each story is completable in one iteration (small enough) -- [ ] Stories are ordered by dependency (schema to backend to UI) -- [ ] Every story has "Typecheck passes" as criterion -- [ ] UI stories have "Verify in browser using dev-browser skill" as criterion -- [ ] Acceptance criteria are verifiable (not vague) -- [ ] No story depends on a later story diff --git a/.claude/skills/ralph/prd.json b/.claude/skills/ralph/prd.json deleted file mode 100644 index 56a4a41..0000000 --- a/.claude/skills/ralph/prd.json +++ /dev/null @@ -1,588 +0,0 @@ -{ - "project": "GP Clinical Record — Depth Enhancement", - "branchName": "ralph/depth-enhancement", - "description": "Add depth, interactivity, and rich content to the GP clinical record dashboard: slide-in detail panels, sub-navigation, expanded skills/KPI data, career constellation D3 visualization, and login refresh. Full spec in Ralph/depth-design.md, requirements in Ralph/depth-requirements.md, workflow in Ralph/workflow_depth.md.", - "userStories": [ - { - "id": "US-001", - "title": "Clean up unused legacy components and hooks", - "description": "As a developer, I need to remove all dead code from the previous PMR interface so the codebase is clean before adding new features. Delete all files listed below and verify no dead imports remain.", - "acceptanceCriteria": [ - "Delete src/components/PMRInterface.tsx", - "Delete src/components/PatientBanner.tsx", - "Delete src/components/ClinicalSidebar.tsx", - "Delete src/components/Breadcrumb.tsx", - "Delete src/components/MobileBottomNav.tsx", - "Delete all files in src/components/views/ directory (SummaryView, ConsultationsView, MedicationsView, ProblemsView, InvestigationsView, DocumentsView, ReferralsView) and remove the views/ directory", - "Delete src/components/Contact.tsx, Education.tsx, Experience.tsx, FloatingNav.tsx, Footer.tsx, Hero.tsx, Projects.tsx, Skills.tsx (old portfolio components)", - "Delete src/hooks/useScrollCondensation.ts", - "Delete src/hooks/useActiveSection.ts (will be recreated in a later story)", - "Delete src/hooks/useScrollReveal.ts if unused", - "Verify no remaining files import any of the deleted files (fix any dead imports)", - "npm run build succeeds with zero errors", - "Typecheck passes" - ], - "priority": 1, - "passes": true, - "notes": "Completed iteration 1 at 2026-02-13 22:57. Model: opus." - }, - { - "id": "US-002", - "title": "Add new TypeScript types and CSS custom properties for depth features", - "description": "As a developer, I need new types and CSS foundations that subsequent stories will use. Add types to src/types/pmr.ts and CSS variables + keyframes to src/index.css. See Ralph/depth-design.md Section 4 for type definitions and Section 9 for CSS.", - "acceptanceCriteria": [ - "Add SkillCategory type: \u0027Technical\u0027 | \u0027Domain\u0027 | \u0027Leadership\u0027 to src/types/pmr.ts", - "Add KPIStory interface with fields: context (string), role (string), outcomes (string[]), period (string optional) to src/types/pmr.ts", - "Add optional story?: KPIStory field to existing KPI interface in src/types/pmr.ts", - "Add ConstellationNode interface (id, type: \u0027role\u0027|\u0027skill\u0027, label, shortLabel?, organization?, startYear?, endYear?, orgColor?, domain?) to src/types/pmr.ts", - "Add ConstellationLink interface (source, target, strength) to src/types/pmr.ts", - "Add DetailPanelContent discriminated union type (kpi | skill | skills-all | consultation | project | education | career-role) to src/types/pmr.ts", - "Add EducationExtra interface (documentId, extracurriculars?, researchDescription?, programmeDetail?) to src/types/pmr.ts", - "Add CSS custom properties to :root in src/index.css: --subnav-height: 36px, --panel-narrow: 400px, --panel-wide: 60vw, --backdrop-blur: 4px, --backdrop-bg: rgba(26,43,42,0.15)", - "Add @keyframes panel-slide-in (translateX 100% to 0), panel-slide-out (reverse), backdrop-fade-in (opacity 0 to 1) to src/index.css", - "Add prefers-reduced-motion overrides for all new keyframes (instant, no transform/opacity change)", - "Typecheck passes" - ], - "priority": 2, - "passes": true, - "notes": "Completed iteration 2 at 2026-02-13 22:59. Model: sonnet." - }, - { - "id": "US-003", - "title": "Create DetailPanelContext, DetailPanel component, and useFocusTrap hook", - "description": "As a developer, I need the core detail panel infrastructure: a context for managing panel state, the slide-in panel component, and a focus trap hook. Create 3 new files. The panel renders placeholder content for now (real renderers come later). See Ralph/depth-design.md Sections 2.1, 2.2 for full interface specs.", - "acceptanceCriteria": [ - "Create src/contexts/DetailPanelContext.tsx with DetailPanelProvider that manages: content (DetailPanelContent | null), openPanel, closePanel, isOpen", - "Width mapping is deterministic from content.type: kpi/skill/skills-all/education → \u0027narrow\u0027 (var(--panel-narrow)), consultation/project/career-role → \u0027wide\u0027 (var(--panel-wide))", - "Title mapping derives from content data (e.g., kpi → kpi.label, skill → skill.name, consultation → consultation.role)", - "Create src/components/DetailPanel.tsx: full-screen backdrop (var(--backdrop-bg) + backdrop-filter: blur(var(--backdrop-blur))) with panel sliding from right", - "Panel has header with X close button (lucide X icon), colored dot matching tile, and title text", - "Panel body is scrollable and renders placeholder text showing content type", - "Close triggers: backdrop click, Escape key, X button", - "ARIA: aria-modal=true, role=dialog, aria-labelledby pointing to title", - "Mobile (\u003c768px): both narrow and wide become 100vw", - "prefers-reduced-motion: instant appear, no slide animation", - "Create src/hooks/useFocusTrap.ts: useFocusTrap(containerRef, isActive) traps Tab/Shift+Tab within container when active, returns focus to previous element when deactivated", - "DetailPanel uses useFocusTrap when open", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 3, - "passes": true, - "notes": "Completed iteration 3 at 2026-02-13 23:03. Model: sonnet." - }, - { - "id": "US-004", - "title": "Create SubNav component and useActiveSection hook", - "description": "As a developer, I need a sticky sub-navigation bar below the TopBar for section jumping, plus a hook that tracks which section is visible. Create src/components/SubNav.tsx and src/hooks/useActiveSection.ts (the old one was deleted in cleanup). See Ralph/depth-design.md Section 2.3.", - "acceptanceCriteria": [ - "Create src/components/SubNav.tsx with 5 sections: Overview (patient-summary), Skills (core-skills), Experience (career-activity), Projects (projects), Education (education)", - "SubNav is sticky below TopBar (top: 48px, z-index: 99)", - "Height 36px, background var(--surface), bottom border var(--border-light)", - "Tabs: 13px font, weight 500, gap 24px, centered text", - "Active tab: teal underline (2px) with 200ms slide transition, text color var(--accent)", - "Inactive tabs: var(--text-secondary)", - "Click scrolls smoothly to [data-tile-id=tileId] element", - "Create src/hooks/useActiveSection.ts using IntersectionObserver to track visible tile by data-tile-id attribute", - "Maps tile IDs to section IDs: patient-summary→overview, core-skills→skills, career-activity→experience, projects→projects, education→education", - "SubNav accepts activeSection and onSectionClick props", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 4, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-13 23:06. Model: sonnet." - }, - { - "id": "US-005", - "title": "Expand skills data from 5 to ~20 with three categories", - "description": "As a developer, I need to expand src/data/skills.ts from 5 skills to ~21 skills across 3 categories. Source content from References/CV_v4.md Core Competencies. Each skill retains the medication metaphor (frequency, status, proficiency). See Ralph/depth-design.md Section 5.1 and Ralph/depth-requirements.md Section 4.4.", - "acceptanceCriteria": [ - "src/data/skills.ts has ~21 SkillMedication entries", - "Technical category (8): Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines", - "Healthcare Domain category (6): Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs", - "Strategic \u0026 Leadership category (7): Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication", - "Each skill has: id (kebab-case), name, frequency (medication-style: Daily, Twice daily, Once weekly, When required, etc.), startYear, yearsOfExperience, proficiency (0-100), category, status (Active/Historical), icon (lucide icon name)", - "Frequency and proficiency values are realistic based on CV_v4.md role descriptions", - "Typecheck passes" - ], - "priority": 5, - "passes": true, - "notes": "Completed iteration 5 at 2026-02-13 23:08. Model: sonnet." - }, - { - "id": "US-006", - "title": "Add KPI story data and update 4th KPI", - "description": "As a developer, I need to add rich story content to each KPI in src/data/kpis.ts for the detail panel, and change the 4th KPI from \u002712 Team Size Led\u0027 to \u00271.2M Population served\u0027. Source from References/CV_v4.md. See Ralph/depth-design.md Section 5.2.", - "acceptanceCriteria": [ - "Change 4th KPI from {id:\u0027team\u0027, value:\u002712\u0027, label:\u0027Team Size Led\u0027} to {id:\u0027population\u0027, value:\u00271.2M\u0027, label:\u0027Population Served\u0027, sub:\u0027Norfolk \u0026 Waveney ICS\u0027, colorVariant:\u0027teal\u0027}", - "Add story field (KPIStory) to all 4 KPIs with: context, role, outcomes[], period", - "£220M story: context about ICB prescribing budget for 1.2M population, role about forecasting models and ICB board accountability, outcomes about proactive financial planning", - "£14.6M story: context about efficiency programme, role about data analysis identification, outcomes about over-target performance", - "9+ Years story: context about career span Aug 2016-present, role about progression from community pharmacy to system-level leadership", - "1.2M story: context about Norfolk \u0026 Waveney ICS population, role about population health analytics and data-driven decision making", - "Add explanation field to 4th KPI matching the story context", - "Typecheck passes" - ], - "priority": 6, - "passes": true, - "notes": "Completed iteration 6 at 2026-02-13 23:10. Model: sonnet." - }, - { - "id": "US-007", - "title": "Create education extras data file", - "description": "As a developer, I need src/data/educationExtras.ts with expanded detail for the education detail panel. Source from References/CV_v4.md Education section. See Ralph/depth-design.md Section 5.4.", - "acceptanceCriteria": [ - "Create src/data/educationExtras.ts exporting educationExtras array of EducationExtra objects", - "MPharm entry (documentId matching doc-mpharm or equivalent from documents.ts): extracurriculars [\u0027President of UEA Pharmacy Society\u0027, \u0027Secretary \u0026 Vice-President of UEA Ultimate Frisbee\u0027, \u0027Publicity Officer for UEA Alzheimer\\\u0027s Society\u0027], researchDescription about cocrystal formation for drug delivery", - "Mary Seacole entry: programmeDetail about NHS leadership qualification, change management, healthcare leadership, system-level thinking", - "Document IDs match those used in src/data/documents.ts", - "Typecheck passes" - ], - "priority": 7, - "passes": true, - "notes": "Completed iteration 7 at 2026-02-13 23:11. Model: sonnet." - }, - { - "id": "US-008", - "title": "Restructure DashboardLayout with SubNav, new tile order, and DetailPanel", - "description": "As a developer, I need to update DashboardLayout.tsx to: wrap with DetailPanelProvider, add SubNav between TopBar and content, reorder tiles per the new layout, render DetailPanel, and adjust spacing. See Ralph/depth-design.md Section 3.1.", - "acceptanceCriteria": [ - "DashboardLayout (or App.tsx) wraps content with DetailPanelProvider from DetailPanelContext", - "SubNav renders between TopBar and the flex container", - "Content area marginTop accounts for both TopBar and SubNav: calc(var(--topbar-height) + var(--subnav-height))", - "Tile order: PatientSummaryTile (full), LatestResultsTile (half) + ProjectsTile (half) side-by-side, CoreSkillsTile (full), LastConsultationTile (full), CareerActivityTile (full), EducationTile (full)", - "DetailPanel component renders alongside CommandPalette", - "SubNav activeSection state managed via useActiveSection hook", - "All tiles have data-tile-id attributes (Card tileId prop)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 8, - "passes": true, - "notes": "Completed iteration 8 at 2026-02-13 23:15. Model: sonnet." - }, - { - "id": "US-009", - "title": "Create constellation data mapping file", - "description": "As a developer, I need src/data/constellation.ts defining the role-skill mapping for the D3 career constellation graph. Maps 6 career roles to their associated skills with connection strengths. See Ralph/depth-design.md Section 5.3 and 2.4.", - "acceptanceCriteria": [ - "Create src/data/constellation.ts with RoleSkillMapping interface (roleId: string, skillIds: string[])", - "Export roleSkillMappings array mapping 6 roles to skill IDs from skills.ts", - "Roles: pre-reg-pharmacist-2015, duty-pharmacy-manager-2016, pharmacy-manager-2017, hcd-pharmacist-2022, deputy-head-2024, interim-head-2025 (IDs should match or reference consultation IDs from consultations.ts)", - "Export constellationNodes array of ConstellationNode objects for all role nodes (with organization, startYear, endYear, orgColor) and skill nodes (with domain)", - "Export constellationLinks array of ConstellationLink objects connecting skills to roles with strength values (0-1)", - "Role orgColors: Paydens gets one color, Tesco another, NHS another (use distinct teal/blue/green tones)", - "Typecheck passes" - ], - "priority": 9, - "passes": true, - "notes": "Completed iteration 9 at 2026-02-13 23:17. Model: sonnet." - }, - { - "id": "US-010", - "title": "Modify LatestResultsTile: remove flip, bigger numbers, panel trigger", - "description": "As a developer, I need to redesign the KPI cards in LatestResultsTile.tsx: remove the CSS flip animation, make headline numbers larger and bolder, and make each card clickable to open the detail panel. See Ralph/depth-design.md Section 3.5.", - "acceptanceCriteria": [ - "Remove flip card animation entirely (no more .metric-card, .metric-card-inner, .metric-card-front, .metric-card-back CSS classes from index.css if they exist)", - "Each KPI renders as a clickable button/card with: value at 28-32px font-size, weight 700, colored by kpi.colorVariant", - "Label at 12px, weight 500, color var(--text-primary), marginTop 4px", - "Sub-text at 10px, font-family var(--font-geist-mono), color var(--text-tertiary), marginTop 2px", - "Click calls openPanel({ type: \u0027kpi\u0027, kpi }) from DetailPanelContext", - "Hover: border color shift + shadow deepens (transition 150ms)", - "Keyboard: Enter/Space triggers panel open", - "Card styling: padding 16px, background var(--surface), border 1px solid var(--border-light), border-radius var(--radius-sm)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 10, - "passes": true, - "notes": "Completed iteration 10 at 2026-02-13. Model: opus. Manually marked passed (script hung after story-complete signal)." - }, - { - "id": "US-011", - "title": "Modify CoreSkillsTile: full width, categorised groups, panel triggers", - "description": "As a developer, I need to redesign CoreSkillsTile.tsx as full-width with skills grouped by 3 categories, showing top 3-4 per category with \u0027view all\u0027 buttons. Individual skills and \u0027view all\u0027 trigger the detail panel. See Ralph/depth-design.md Section 3.4.", - "acceptanceCriteria": [ - "Card uses full prop (spans both grid columns)", - "Skills grouped by category: Technical, Healthcare Domain (Domain), Strategic \u0026 Leadership (Leadership)", - "Each category has a header: thin divider line with category label (styled like sidebar section dividers: 10px, uppercase, var(--text-tertiary))", - "Show top 3-4 skills per category on the dashboard tile (sorted by proficiency or relevance)", - "Each skill row is clickable → openPanel({ type: \u0027skill\u0027, skill }) from DetailPanelContext", - "Each category with \u003e4 skills shows a \u0027View all (N)\u0027 button → openPanel({ type: \u0027skills-all\u0027, category })", - "Retain medication metaphor display (frequency, status badge)", - "Remove old single-expand accordion for skills (replaced by panel)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 11, - "passes": true, - "notes": "Completed iteration 1 at 2026-02-13 23:50. Model: opus." - }, - { - "id": "US-012", - "title": "Modify ProjectsTile: half width, compact card grid, panel trigger", - "description": "As a developer, I need to change ProjectsTile.tsx from full-width to half-width (positioned alongside LatestResultsTile by the layout reorder in US-008). Compact cards with click to open detail panel. See Ralph/depth-design.md Section 3.6.", - "acceptanceCriteria": [ - "Remove full prop from Card (half-width, single grid column)", - "Compact project cards: status dot + name + year (right-aligned) per row", - "Tech stack shown as small inline tags", - "Each project card clickable → openPanel({ type: \u0027project\u0027, investigation }) from DetailPanelContext", - "Remove old in-place expansion (replaced by panel)", - "Hover: border color shift, shadow deepens", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 12, - "passes": true, - "notes": "Completed iteration 2 at 2026-02-13 23:52. Model: sonnet." - }, - { - "id": "US-013", - "title": "Modify LastConsultationTile: add panel trigger", - "description": "As a developer, I need to add a \u0027View full record\u0027 button to LastConsultationTile.tsx that opens the detail panel with full role details. See Ralph/depth-design.md Section 3.9.", - "acceptanceCriteria": [ - "Add \u0027View full record\u0027 link/button at the bottom of the tile", - "Click → openPanel({ type: \u0027consultation\u0027, consultation }) from DetailPanelContext, passing the first consultation entry", - "Make the tile header area also clickable (opens same panel)", - "Keep existing inline content (header info row, achievement bullets)", - "Hover state on clickable areas", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 13, - "passes": true, - "notes": "Completed iteration 3 at 2026-02-13 23:55. Model: sonnet." - }, - { - "id": "US-014", - "title": "Modify CareerActivityTile: panel triggers and hover preview", - "description": "As a developer, I need to change CareerActivityTile.tsx so timeline items click to open the detail panel instead of expanding in-place, and add hover previews. See Ralph/depth-design.md Section 3.7.", - "acceptanceCriteria": [ - "Role timeline items click → openPanel({ type: \u0027career-role\u0027, consultation }) from DetailPanelContext", - "Remove in-place accordion expansion for career items (replaced by panel)", - "Hover preview: items lift slightly on hover with shadow deepens, show 1-2 lines of preview text", - "Keep color-coded dots and entry type styling (teal roles, amber projects, green certs, purple education)", - "Reserve a container/placeholder for CareerConstellation component (will be added later)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 14, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-13 23:58. Model: sonnet." - }, - { - "id": "US-015", - "title": "Modify EducationTile: richer content, panel trigger", - "description": "As a developer, I need to enhance EducationTile.tsx with richer inline content and click-to-panel interaction. See Ralph/depth-design.md Section 3.8.", - "acceptanceCriteria": [ - "Show richer inline content: MPharm research project score (75.1%), OSCE score (80%), A-level grades (A* Maths, B Chemistry, C Politics)", - "Each education entry is clickable → openPanel({ type: \u0027education\u0027, document }) from DetailPanelContext", - "Hover: border color shift on clickable entries", - "Use education extras data from src/data/educationExtras.ts for inline detail where appropriate", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 15, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-14 00:33. Model: sonnet." - }, - { - "id": "US-016", - "title": "Modify PatientSummaryTile: structured presentation with highlight strip", - "description": "As a developer, I need to improve PatientSummaryTile.tsx with the full CV_v4.md profile text and a visual highlight strip. See Ralph/depth-design.md Section 3.10 and Ralph/depth-requirements.md Section 4.1.", - "acceptanceCriteria": [ - "Verify src/data/profile.ts has the complete profile text from References/CV_v4.md (update if needed)", - "Add a visual highlight strip showing key stats: e.g. \u00279+ Years Experience\u0027, \u00271.2M Population\u0027, \u0027£220M Budget\u0027 as small styled badges or pills", - "Profile text is not a wall of text — use hierarchy: bold key phrases, structured paragraphs if needed", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 16, - "passes": true, - "notes": "" - }, - { - "id": "US-017", - "title": "Create KPIDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/KPIDetail.tsx that renders rich KPI story content inside the detail panel. Wire it into DetailPanel so content.type === \u0027kpi\u0027 renders this component. See Ralph/depth-design.md Section 6.1.", - "acceptanceCriteria": [ - "Create src/components/detail/KPIDetail.tsx accepting a KPI prop", - "Renders: headline number (large, colored by kpi.colorVariant), context paragraph (story.context), \u0027Your role\u0027 paragraph (story.role), outcome bullets (story.outcomes), period badge (story.period)", - "Graceful fallback if story is undefined (show kpi.explanation instead)", - "Wire into DetailPanel: when content.type === \u0027kpi\u0027, render \u003cKPIDetail kpi={content.kpi} /\u003e", - "Styling matches dashboard design system (fonts, colors, spacing)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 17, - "passes": true, - "notes": "" - }, - { - "id": "US-018", - "title": "Create ConsultationDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/ConsultationDetail.tsx for displaying full role details in the detail panel. Used for both \u0027consultation\u0027 and \u0027career-role\u0027 content types. See Ralph/depth-design.md Section 6.4.", - "acceptanceCriteria": [ - "Create src/components/detail/ConsultationDetail.tsx accepting a Consultation prop", - "Renders: role title + organization + dates, history paragraph (consultation.history), achievement bullets (consultation.examination), plan/outcomes (consultation.plan), coded entries as badges (consultation.codedEntries)", - "Wire into DetailPanel: content.type === \u0027consultation\u0027 or \u0027career-role\u0027 renders this component", - "Styled consistently with dashboard design system", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 18, - "passes": true, - "notes": "Already implemented by prior iteration. Component exists with full content, wired into DetailPanel for consultation and career-role types." - }, - { - "id": "US-019", - "title": "Create ProjectDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/ProjectDetail.tsx for displaying full project information in the wide detail panel. See Ralph/depth-design.md Section 6.5.", - "acceptanceCriteria": [ - "Create src/components/detail/ProjectDetail.tsx accepting an Investigation prop", - "Renders: project name + year + status badge, methodology description, tech stack as tags, results bullets, external link button (if investigation.externalUrl exists, opens in new tab)", - "Wire into DetailPanel: content.type === \u0027project\u0027 renders this component", - "External link uses rel=\u0027noopener noreferrer\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 19, - "passes": true, - "notes": "" - }, - { - "id": "US-020", - "title": "Create SkillDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/SkillDetail.tsx for displaying individual skill detail in the narrow detail panel. See Ralph/depth-design.md Section 6.2.", - "acceptanceCriteria": [ - "Create src/components/detail/SkillDetail.tsx accepting a SkillMedication prop", - "Renders: skill name + frequency + status badge, visual proficiency bar (0-100%), years of experience, category label", - "If constellation data is available, show \u0027Used in\u0027 section listing roles that used this skill (import from src/data/constellation.ts)", - "Wire into DetailPanel: content.type === \u0027skill\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 20, - "passes": true, - "notes": "Completed. Component renders skill header with frequency/status badges, category label, proficiency bar (color-coded), years of experience, and 'Used in' section from constellation data." - }, - { - "id": "US-021", - "title": "Create SkillsAllDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/SkillsAllDetail.tsx showing the full categorised list of all skills. Clicking an individual skill switches the panel to SkillDetail. See Ralph/depth-design.md Section 6.3.", - "acceptanceCriteria": [ - "Create src/components/detail/SkillsAllDetail.tsx", - "Shows full list grouped by Technical / Healthcare Domain / Strategic \u0026 Leadership", - "Category headers styled consistently with CoreSkillsTile category headers", - "Each skill row is clickable → calls openPanel({ type: \u0027skill\u0027, skill }) to switch panel content", - "If opened with a category filter (content.category), scroll to or highlight that category", - "Wire into DetailPanel: content.type === \u0027skills-all\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 21, - "passes": true, - "notes": "Completed. Full categorised skill list with category headers matching CoreSkillsTile style, proficiency mini-bars, click-to-skill-detail navigation, and category scroll/highlight from filter." - }, - { - "id": "US-022", - "title": "Create EducationDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/EducationDetail.tsx for displaying full education details including extras. See Ralph/depth-design.md Section 6.6.", - "acceptanceCriteria": [ - "Create src/components/detail/EducationDetail.tsx accepting a Document prop", - "Renders: title + institution + dates + classification", - "Imports educationExtras from src/data/educationExtras.ts and finds matching extra by document ID", - "If MPharm: shows research project description, extracurricular activities list", - "If Mary Seacole: shows programme detail", - "Shows notes from document data if present", - "Wire into DetailPanel: content.type === \u0027education\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 22, - "passes": true, - "notes": "Completed. Renders title + icon + institution + dates + classification badge. Shows research description, OSCE score, extracurriculars (MPharm), programme detail (Mary Seacole), and notes." - }, - { - "id": "US-023", - "title": "Install D3 and scaffold CareerConstellation component", - "description": "As a developer, I need to install d3 as a dependency and create a scaffolded CareerConstellation component with an SVG container. See Ralph/depth-design.md Section 2.4.", - "acceptanceCriteria": [ - "Run npm install d3 @types/d3", - "Create src/components/CareerConstellation.tsx with props: onRoleClick(id: string), onSkillClick(id: string)", - "Component renders a responsive SVG container using useRef\u003cSVGSVGElement\u003e", - "Container: full width, height 400px desktop / 300px tablet / 250px mobile (use CSS or media queries)", - "SVG has viewBox for responsive scaling", - "Import constellation data from src/data/constellation.ts", - "Subtle radial gradient background from var(--bg-dashboard) center to var(--surface) edge", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 23, - "passes": true, - "notes": "Completed. D3 + @types/d3 installed. CareerConstellation scaffold with responsive SVG container (400/300/250px), radial gradient bg, ResizeObserver, callbacks ref for future D3 wiring." - }, - { - "id": "US-024", - "title": "Build D3 force-directed graph rendering in CareerConstellation", - "description": "As a developer, I need the D3 force simulation to render role and skill nodes with links in the CareerConstellation component. D3 operates imperatively via useEffect on the SVG ref. See Ralph/depth-design.md Section 2.4 for exact force configuration.", - "acceptanceCriteria": [ - "D3 force simulation with: forceManyBody(-200), forceLink(distance 80, strength from data), forceX chronological (roles positioned left-to-right by startYear), forceY centered, forceCollide(30)", - "Role nodes: 24px radius circles, filled with orgColor, white text label", - "Skill nodes: 10px radius, color-coded by domain: clinical=var(--success), technical=var(--accent), leadership=var(--amber)", - "Links: thin lines (1px), var(--border) color, opacity 0.3", - "D3 integration: useEffect on SVG ref, no React state for node positions", - "Simulation runs and nodes settle into stable positions", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 24, - "passes": true, - "notes": "Completed. D3 force simulation with forceManyBody(-200), forceLink(dist 80, strength from data), forceX chronological, forceY centered, forceCollide. Role nodes 24px with orgColor + white labels, skill nodes 10px color-coded by domain, links 1px opacity 0.3." - }, - { - "id": "US-025", - "title": "Add accessibility to CareerConstellation", - "description": "As a developer, I need the CareerConstellation to be accessible: keyboard navigable, screen-reader friendly, and respecting reduced motion. See Ralph/depth-design.md Section 2.4 accessibility notes.", - "acceptanceCriteria": [ - "SVG has role=img and aria-label describing the graph (\u0027Career constellation showing roles and skills across career timeline\u0027)", - "Screen-reader-only text description of graph structure (hidden visually, available to assistive tech)", - "Keyboard navigation: Tab through role nodes, Enter/Space opens detail panel for focused node", - "Focus indicators visible on keyboard-focused nodes", - "prefers-reduced-motion: disable force simulation animation, render nodes at calculated static positions immediately", - "Typecheck passes" - ], - "priority": 25, - "passes": true, - "notes": "Completed. SR-only description with role-skill mappings, hidden focusable buttons for keyboard nav (Tab/Enter/Space), focus ring on SVG nodes, prefers-reduced-motion runs simulation synchronously to static positions." - }, - { - "id": "US-026", - "title": "Add hover and click interactions to CareerConstellation", - "description": "As a developer, I need hover highlighting and click-to-panel interactions on the CareerConstellation. This connects the graph to the detail panel system. See Ralph/depth-design.md Section 2.4.", - "acceptanceCriteria": [ - "Hover role node: connected skill nodes scale up, links brighten to var(--accent), non-connected nodes fade to 0.15 opacity", - "Hover skill node: all connected role nodes highlight, link paths illuminate", - "Click role node: calls onRoleClick(id) prop", - "Click skill node: calls onSkillClick(id) prop", - "Integrate into CareerActivityTile: wire onRoleClick to open ConsultationDetail panel, onSkillClick to open SkillDetail panel", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 26, - "passes": true, - "notes": "Completed. D3 hover: connected nodes stay full opacity, non-connected fade to 0.15, links brighten to teal. Click: role→onRoleClick, skill→onSkillClick. Wired into CareerActivityTile replacing placeholder, connected to detail panel." - }, - { - "id": "US-027", - "title": "Restyle LoginScreen with teal accents", - "description": "As a developer, I need to visually refresh the LoginScreen with teal accents replacing the current blue. See Ralph/depth-design.md Section 3.3 and Ralph/depth-requirements.md Section 5.", - "acceptanceCriteria": [ - "Replace #005EB8 with #0D6E6E throughout LoginScreen (shield icon bg, active field border, cursor, button)", - "Replace #004D9F with #0A8080 (button hover state)", - "Replace #004494 with #085858 (button pressed state)", - "Background color: keep #1E293B or change to #1A2B2A", - "Login card feels cohesive with the dashboard teal palette", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 27, - "passes": true, - "notes": "Completed. Replaced #005EB8→#0D6E6E, #004D9F→#0A8080, #004494→#085858, background #1E293B→#1A2B2A, shield rgba updated." - }, - { - "id": "US-028", - "title": "Change login username to a.recruiter and add connection status indicator", - "description": "As a developer, I need to change the typed username from a.charlwood to a.recruiter and add a connection status indicator below the login button. See Ralph/depth-design.md Section 3.3.", - "acceptanceCriteria": [ - "Username typed in login animation is \u0027a.recruiter\u0027 (not \u0027A.CHARLWOOD\u0027 or similar)", - "Connection status indicator appears below the login button: 6px dot + text", - "Initial state: red/alert dot + \u0027Awaiting secure connection...\u0027 (var(--alert) color)", - "After ~2000ms: dot transitions to green + \u0027Secure connection established\u0027 (var(--success) color, 300ms transition)", - "Text: 10px, font-family var(--font-geist-mono), color var(--text-tertiary)", - "Login button disabled until BOTH typing is complete AND connectionState === \u0027connected\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 28, - "passes": true, - "notes": "Completed. Username changed to a.recruiter, connection status indicator with red→green 300ms transition, button disabled until typing complete AND connected." - }, - { - "id": "US-029", - "title": "Add post-login loading state and update TopBar session name", - "description": "As a developer, I need a brief loading state after clicking the login button before the dashboard appears, and the TopBar should show A.RECRUITER as the session user. See Ralph/depth-design.md Sections 3.3 and 3.2.", - "acceptanceCriteria": [ - "On login button click: isLoading=true, card content replaced with spinner + \u0027Loading clinical records...\u0027 text", - "Loading state lasts ~600ms, then calls onComplete() to transition to dashboard", - "Spinner is a CSS-animated spinner (not a GIF), styled with var(--accent) or similar", - "Loading text: 12px, color var(--text-secondary)", - "In TopBar.tsx: change session display name from \u0027Dr. A.CHARLWOOD\u0027 (or current value) to \u0027A.RECRUITER\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 29, - "passes": true, - "notes": "Completed. Loading state with CSS spinner replaces card content on login click (~600ms), TopBar shows A.RECRUITER, prefers-reduced-motion skips spinner animation." - }, - { - "id": "US-030", - "title": "Update CommandPalette for expanded content and panel actions", - "description": "As a developer, I need to update the CommandPalette search index and actions to work with the expanded skills data (~20 skills) and add actions that open the detail panel directly. See Ralph/depth-design.md Section 10, Phase 6.", - "acceptanceCriteria": [ - "Search index in src/lib/search.ts includes all ~21 skills (not just the original 5)", - "Selecting a skill result opens the detail panel for that skill (openPanel call or dispatch event)", - "Selecting a KPI result opens the KPI detail panel", - "Selecting a project result opens the project detail panel", - "Ensure DashboardLayout handlePaletteAction supports a new \u0027panel\u0027 action type or adapts existing types to trigger detail panel", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 30, - "passes": true, - "notes": "Completed. All 21 skills in search index, panel action type added. Skills/KPIs/projects open detail panel directly from command palette." - }, - { - "id": "US-031", - "title": "Responsive testing and fixes for all new components", - "description": "As a developer, I need to verify and fix responsive behavior for the detail panel, sub-nav, constellation, and restructured layout at all breakpoints.", - "acceptanceCriteria": [ - "DetailPanel: both narrow and wide render as 100vw on mobile (\u003c768px)", - "SubNav: works on tablet/mobile (horizontal scroll if needed, no overflow)", - "CareerConstellation: renders at 300px height on tablet, 250px on mobile", - "Projects + KPIs: stack vertically on mobile when grid falls to single column", - "CoreSkillsTile: full-width layout works on all breakpoints", - "All interactive elements have touch targets \u003e= 44px on mobile", - "No horizontal overflow at 375px viewport width", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 31, - "passes": true, - "notes": "Completed. SubNav horizontal scroll with hidden scrollbar, 44px min touch targets on all interactive elements, DetailPanel close button enlarged to 44px." - }, - { - "id": "US-032", - "title": "Reduced motion audit, final cleanup, and visual review", - "description": "As a developer, I need to verify all new animations respect prefers-reduced-motion, remove any dead code introduced during development, and do a final build verification.", - "acceptanceCriteria": [ - "DetailPanel slide animation: instant appear with prefers-reduced-motion", - "Backdrop fade: instant with prefers-reduced-motion", - "SubNav underline transition: instant with prefers-reduced-motion", - "CareerConstellation: static layout (no force simulation animation) with prefers-reduced-motion", - "Connection status dot transition: instant with prefers-reduced-motion", - "Post-login spinner: static indicator with prefers-reduced-motion", - "No dead imports across all files", - "Remove any unused flip card CSS (.metric-card-inner etc.) if still present in index.css", - "npm run build succeeds cleanly", - "npm run typecheck passes with zero errors", - "npm run lint passes (pre-existing AccessibilityContext warning OK)", - "Typecheck passes" - ], - "priority": 32, - "passes": true, - "notes": "Completed. Reduced motion overrides for SubNav, connection status, smooth scroll. Created ProjectDetail renderer. Removed unused files (useBreakpoint.ts, profile.ts), legacy PMR CSS variables, placeholder fallback. Build/typecheck/lint all clean." - } - ] -} diff --git a/.claude/skills/ralph/progress.txt b/.claude/skills/ralph/progress.txt deleted file mode 100644 index 6b27dc7..0000000 --- a/.claude/skills/ralph/progress.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Ralph Progress — GP Clinical Record Depth Enhancement - -Branch: ralph/depth-enhancement -Stories: 32 (US-001 through US-032) - ---- - -## Status - -No iterations completed yet. -2026-02-13 22:57 | PASS | US-001: Clean up unused legacy components and hooks | model=opus elapsed=01:58 tools=18 -2026-02-13 22:59 | PASS | US-002: Add new TypeScript types and CSS custom properties for depth features | model=sonnet elapsed=01:54 tools=11 -2026-02-13 23:03 | PASS | US-003: Create DetailPanelContext, DetailPanel component, and useFocusTrap hook | model=sonnet elapsed=03:39 tools=22 -2026-02-13 23:06 | PASS | US-004: Create SubNav component and useActiveSection hook | model=sonnet elapsed=02:54 tools=18 -2026-02-13 23:08 | PASS | US-005: Expand skills data from 5 to ~20 with three categories | model=sonnet elapsed=01:58 tools=11 -2026-02-13 23:10 | PASS | US-006: Add KPI story data and update 4th KPI | model=sonnet elapsed=01:59 tools=9 -2026-02-13 23:11 | PASS | US-007: Create education extras data file | model=sonnet elapsed=01:25 tools=10 -2026-02-13 23:15 | PASS | US-008: Restructure DashboardLayout with SubNav, new tile order, and DetailPanel | model=sonnet elapsed=03:10 tools=27 -2026-02-13 23:17 | PASS | US-009: Create constellation data mapping file | model=sonnet elapsed=02:20 tools=10 -2026-02-13 23:50 | PASS | US-011: Modify CoreSkillsTile: full width, categorised groups, panel triggers | model=opus elapsed=02:54 tools=22 -2026-02-13 23:52 | PASS | US-012: Modify ProjectsTile: half width, compact card grid, panel trigger | model=sonnet elapsed=02:16 tools=11 -2026-02-13 23:55 | PASS | US-013: Modify LastConsultationTile: add panel trigger | model=sonnet elapsed=02:20 tools=15 -2026-02-13 23:58 | PASS | US-014: Modify CareerActivityTile: panel triggers and hover preview | model=sonnet elapsed=02:49 tools=14 diff --git a/.claude/skills/ralph/ralph.ps1 b/.claude/skills/ralph/ralph.ps1 deleted file mode 100644 index 9e8ea29..0000000 --- a/.claude/skills/ralph/ralph.ps1 +++ /dev/null @@ -1,568 +0,0 @@ -<# -.SYNOPSIS - Ralph Wiggum Loop - PRD-driven variant. - -.DESCRIPTION - Iterates through user stories in prd.json, spawning a fresh `claude --print` - invocation for each story. Memory persists via filesystem only: git commits, - prd.json (passes field), and progress.txt. - - Each iteration works on ONE user story (in priority order). - When all stories pass, the loop completes. - - Circuit breakers prevent runaway costs: - - No git changes for N consecutive iterations (stalled) - - Same error repeated N consecutive iterations (stuck) - -.PARAMETER Model - Initial Claude model to use. Default: "opus". The agent can dynamically switch - models between iterations via opus|sonnet signals. - -.PARAMETER MaxNoProgress - Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3. - -.PARAMETER MaxSameError - Number of consecutive iterations with the same error before circuit breaker trips. Default: 3. - -.PARAMETER StartFrom - Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed. - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -Model "opus" - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet" -#> - -param( - [string]$Model = "opus", - [int]$MaxNoProgress = 3, - [int]$MaxSameError = 3, - [string]$StartFrom = "" -) - -$ErrorActionPreference = "Stop" - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$prdFile = Join-Path $scriptDir "prd.json" -$progressFile = Join-Path $scriptDir "progress.txt" -$logDir = Join-Path $scriptDir "logs" - -# --- Find project root (git repo root) --- - -$projectRoot = git rev-parse --show-toplevel 2>$null -if (-not $projectRoot) { - Write-Error "Not inside a git repository. Run from the project directory." - exit 1 -} -$projectRoot = (Resolve-Path $projectRoot).Path - -# --- Validation --- - -if (-not (Test-Path $prdFile)) { - Write-Error "prd.json not found at $prdFile" - exit 1 -} - -# Ensure logs directory exists -if (-not (Test-Path $logDir)) { - New-Item -ItemType Directory -Path $logDir | Out-Null - Write-Host "Created logs directory" -} - -# --- PRD Read/Write --- - -function Read-Prd { - Get-Content -Path $prdFile -Raw | ConvertFrom-Json -} - -function Save-Prd { - param($prdObj) - $prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8 -} - -$prd = Read-Prd - -# --- Git Setup --- - -$BranchName = $prd.branchName - -if ($BranchName) { - $currentBranch = git branch --show-current - if ($currentBranch -ne $BranchName) { - $branchExists = git branch --list $BranchName - if ($branchExists) { - Write-Host "Switching to existing branch: $BranchName" - git checkout $BranchName - } else { - Write-Host "Creating branch: $BranchName" - git checkout -b $BranchName - } - } -} - -# --- Handle StartFrom: mark earlier stories as passed --- - -if ($StartFrom) { - $startPriority = [int]($StartFrom -replace 'US-0*', '') - $skippedCount = 0 - foreach ($story in $prd.userStories) { - $storyPriority = [int]($story.id -replace 'US-0*', '') - if ($storyPriority -lt $startPriority -and $story.passes -ne $true) { - $story.passes = $true - $story.notes = "Skipped (--StartFrom $StartFrom)" - $skippedCount++ - } - } - if ($skippedCount -gt 0) { - Save-Prd $prd - Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow - } -} - -# --- Circuit Breaker State --- - -$noProgressCount = 0 -$lastErrorSignature = "" -$sameErrorCount = 0 - -# --- Prompt Generation --- - -function Build-StoryPrompt { - param( - $story, - $prdObj, - [array]$completedStories - ) - - # Build completed list - $completedSection = "" - if ($completedStories.Count -gt 0) { - $completedLines = ($completedStories | ForEach-Object { - "- $($_.id): $($_.title)" - }) -join "`n" - $completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n" - } - - # Build criteria list - $criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n" - - # Build prompt using array-join (avoids PS 5.1 here-string indentation issues) - $sid = $story.id - $stitle = $story.title - $sdesc = $story.description - $pdesc = $prdObj.description - - $prompt = @( - "# Ralph Iteration: $sid - $stitle" - "" - "## Project" - "$pdesc" - "" - "Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work." - "" - "## Your Task" - "" - "**${sid}: $stitle**" - "" - "$sdesc" - "" - "## Acceptance Criteria" - "" - "$criteriaLines" - "" - "## Reference Documents" - "" - "Read these as needed for implementation detail:" - "" - "- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)" - "- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models" - "- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns" - "- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)" - "$completedSection" - "## Workflow" - "" - "1. Read CLAUDE.md to understand project conventions" - "2. Read Ralph/depth-design.md sections relevant to this story" - "3. Read existing source files you will modify to understand current patterns" - "4. Implement ALL acceptance criteria" - "5. Run npm run typecheck - fix any type errors" - "6. Run npm run build - fix any build errors" - "7. Stage and commit your changes:" - " git add [specific files] && git commit -m `"${sid}: [descriptive message]`"" - "8. When ALL criteria are met, output: $sid" - "" - "## Rules" - "" - "- Work ONLY on $sid. Do not modify code for other stories." - "- Read files before modifying them." - "- Follow existing patterns and conventions in the codebase." - "- Use lucide-react for icons, never unicode symbols." - "- Use the project's CSS custom properties and Tailwind tokens." - "- Commit specific files, not git add -A." - "- Do NOT start a dev server (npm run dev). One is already running on port $devServerPort. Do NOT run any background tasks." - "- If genuinely blocked, output $sid with explanation." - "- To recommend a different model for the NEXT iteration, output opus or sonnet." - ) -join "`n" - - return $prompt -} - -# --- Banner --- - -$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$totalCount = $prd.userStories.Count - -Write-Host "" -Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan -Write-Host "Project: $($prd.project)" -ForegroundColor Cyan -Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan -Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan -Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host "" - -# Dev server port (assumed to be running externally) -$devServerPort = 5173 -Write-Host "Dev server assumed running on port $devServerPort" -ForegroundColor DarkGray -Write-Host "" - -# --- Story Loop --- - -$iterationCount = 0 -$originalDir = Get-Location -Set-Location $projectRoot - -try { - -while ($true) { - # Re-read PRD each iteration (in case previous iteration updated it) - $prd = Read-Prd - - # Partition stories - $completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true }) - $pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority }) - - # Check if all done - if ($pendingStories.Count -eq 0) { - Write-Host "" - Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green - Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green - Write-Host "Branch: $BranchName" -ForegroundColor Green - break - } - - $currentStory = $pendingStories[0] - $iterationCount++ - $pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100) - - $storyLabel = "$($currentStory.id): $($currentStory.title)" - $pctStr = "${pctComplete}%" - $progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)" - - Write-Host "" - Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow - Write-Host $progressMsg -ForegroundColor DarkGray - - # Record HEAD before this iteration - $headBefore = git rev-parse HEAD 2>$null - - $iterStart = Get-Date - Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray - Write-Host "" - - # Generate prompt for this story - $promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories - - # --- Spawn Claude --- - - $logFile = Join-Path $logDir "$($currentStory.id).log" - $rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl" - $maxRetries = 10 - $retryCount = 0 - $outputString = "" - $apiOverloaded = $false - - do { - $apiOverloaded = $false - $textBuilder = [System.Text.StringBuilder]::new() - $toolCount = 0 - - # Clear raw log file for this attempt - if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force } - - if ($retryCount -gt 0) { - $backoffSeconds = [Math]::Pow(2, $retryCount - 1) - Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow - Start-Sleep -Seconds $backoffSeconds - Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray - } - - # --- Spawn Claude via Process.Start for clean shutdown control --- - # Using Process.Start instead of pipeline so we can break on the result - # event and force-kill the process tree. The pipeline approach hangs when - # Claude spawns background tasks (e.g. npm run dev) that keep stdout open. - - $promptTempFile = Join-Path $logDir "$($currentStory.id).prompt.tmp" - $promptContent | Set-Content -Path $promptTempFile -Encoding UTF8 - - $claudeArgs = "--print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json" - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = "cmd.exe" - $psi.Arguments = "/c type `"$promptTempFile`" | claude $claudeArgs" - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - $psi.WorkingDirectory = $projectRoot - - $claudeProc = [System.Diagnostics.Process]::Start($psi) - - # Drain stderr async to prevent buffer deadlock - $claudeProc.add_ErrorDataReceived({ param($s,$e) }) - $claudeProc.BeginErrorReadLine() - - try { - while ($null -ne ($line = $claudeProc.StandardOutput.ReadLine())) { - $line = $line.Trim() - if (-not $line) { continue } - - # Save raw event for debugging - try { - Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue - } catch { } - - $isResultEvent = $false - try { - $evt = $line | ConvertFrom-Json -ErrorAction Stop - - # --- Tool use start --- - if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') { - $toolCount++ - $toolName = $evt.content_block.name - Write-Host " [$toolName]" -ForegroundColor DarkCyan - } - # --- Streaming text --- - elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) { - Write-Host -NoNewline $evt.delta.text - [void]$textBuilder.Append($evt.delta.text) - } - # --- Result event (terminal — stop reading after this) --- - elseif ($evt.type -eq 'result') { - if ($evt.subtype -eq 'error_result' -and $evt.error) { - Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red - [void]$textBuilder.AppendLine("ERROR: $($evt.error)") - } - elseif ($evt.result) { - [void]$textBuilder.AppendLine($evt.result) - } - $isResultEvent = $true - } - # --- Message-level content --- - elseif ($evt.message -and $evt.message.content) { - foreach ($block in $evt.message.content) { - if ($block.type -eq 'text' -and $block.text) { - Write-Host $block.text - [void]$textBuilder.AppendLine($block.text) - } - elseif ($block.type -eq 'tool_use') { - $toolCount++ - Write-Host " [$($block.name)]" -ForegroundColor DarkCyan - } - } - } - } catch { - if ($line -and $line -notmatch '^\s*["\{\[\}\]]') { - Write-Host $line -ForegroundColor DarkYellow - [void]$textBuilder.AppendLine($line) - } - } - - # Result is always the final stream event — stop reading - if ($isResultEvent) { break } - } - } finally { - # Kill the Claude process tree to prevent orphaned cmd.exe/node processes - if ($claudeProc -and -not $claudeProc.HasExited) { - try { - taskkill /T /F /PID $claudeProc.Id 2>$null | Out-Null - } catch { } - } - Remove-Item -Path $promptTempFile -ErrorAction SilentlyContinue - } - - $outputString = $textBuilder.ToString() - - # Check for 529 overloaded error - if ($outputString -match "529.*overloaded|overloaded_error") { - $apiOverloaded = $true - $retryCount++ - if ($retryCount -ge $maxRetries) { - Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red - } - } - # Check for usage limit with cooldown - elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") { - $resetHour = [int]$Matches[1] - $resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 } - $resetAmPm = $Matches[3] - - if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 } - elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 } - - $now = Get-Date - $resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0 - if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) } - $resetTime = $resetTime.AddMinutes(2) - - $waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds) - $waitMinutes = [Math]::Ceiling($waitSeconds / 60) - - Write-Host "" - Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green - - $apiOverloaded = $true - } - } while ($apiOverloaded -and $retryCount -lt $maxRetries) - - # Save log - $outputString | Set-Content -Path $logFile -Encoding UTF8 - - # Show elapsed time - $elapsed = (Get-Date) - $iterStart - Write-Host "" - Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray - - # --- Detect signals --- - - $storyComplete = $outputString -match "$([regex]::Escape($currentStory.id))" - $storyBlocked = $outputString -match "$([regex]::Escape($currentStory.id))" - $headAfter = git rev-parse HEAD 2>$null - $hasGitChanges = $headAfter -ne $headBefore - - # --- Update story status --- - - if ($storyComplete) { - # Mark story as passed in prd.json - $prd = Read-Prd - $storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id } - if ($storyToUpdate) { - $alreadyDone = if (-not $hasGitChanges) { " (already committed)" } else { "" } - $storyToUpdate.passes = $true - $storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model.$alreadyDone" - } - Save-Prd $prd - - # Append to progress.txt - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $el = $elapsed.ToString('mm\:ss') - $tag = if ($hasGitChanges) { "PASS" } else { "PASS (no new commits)" } - $progressEntry = "$ts | $tag | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - - Write-Host " [PASSED] $storyLabel" -ForegroundColor Green - if (-not $hasGitChanges) { - Write-Host " (Work was already committed)" -ForegroundColor DarkGray - } - $noProgressCount = 0 - $sameErrorCount = 0 - $lastErrorSignature = "" - } - elseif ($storyBlocked) { - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | BLOCKED | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red - # Blocked counts as no progress - $noProgressCount++ - } - else { - # No completion signal - if ($hasGitChanges) { - Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | PARTIAL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - $noProgressCount = 0 - } else { - Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow - $noProgressCount++ - } - } - - # --- Circuit Breaker: No Progress --- - - if ($noProgressCount -ge $MaxNoProgress) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red - Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red - Write-Host "Stuck on: $($currentStory.id) - $($currentStory.title)" -ForegroundColor Red - Write-Host "Check $logFile for details." -ForegroundColor Red - break - } - - # --- Circuit Breaker: Repeated Error --- - - $errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches - if ($errorLines) { - $filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3 - $currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|" - if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) { - $sameErrorCount++ - Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow - if ($sameErrorCount -ge $MaxSameError) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red - Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red - Write-Host " $currentErrorSignature" -ForegroundColor Red - break - } - } elseif ($currentErrorSignature) { - $sameErrorCount = 0 - } - $lastErrorSignature = $currentErrorSignature - } else { - $sameErrorCount = 0 - $lastErrorSignature = "" - } - - # --- Dynamic Model Selection --- - - if ($outputString -match "(opus|sonnet)") { - $nextModel = $Matches[1] - if ($nextModel -ne $Model) { - Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta - $Model = $nextModel - } - } - - # Brief pause between iterations - Start-Sleep -Seconds 2 -} - -} finally { - Set-Location $originalDir -} - -# --- Final Summary --- - -$prd = Read-Prd -$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$finalTotal = $prd.userStories.Count - -Write-Host "" -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan -Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan -Write-Host " Branch: $BranchName" -ForegroundColor Cyan -Write-Host " Logs: $logDir" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan - -if ($finalPassed -eq $finalTotal) { - exit 0 -} else { - exit 1 -} - diff --git a/.claude/skills/ralph/ralph1.ps1 b/.claude/skills/ralph/ralph1.ps1 deleted file mode 100644 index 9c89be6..0000000 --- a/.claude/skills/ralph/ralph1.ps1 +++ /dev/null @@ -1,582 +0,0 @@ -<# -.SYNOPSIS - Ralph Wiggum Loop — PRD-driven variant. - -.DESCRIPTION - Iterates through user stories in prd.json, spawning a fresh `claude --print` - invocation for each story. Memory persists via filesystem only: git commits, - prd.json (passes field), and progress.txt. - - Each iteration works on ONE user story (in priority order). - When all stories pass, the loop completes. - - Circuit breakers prevent runaway costs: - - No git changes for N consecutive iterations (stalled) - - Same error repeated N consecutive iterations (stuck) - -.PARAMETER Model - Initial Claude model to use. Default: "opus". The agent can dynamically switch - models between iterations via opus|sonnet signals. - -.PARAMETER MaxNoProgress - Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3. - -.PARAMETER MaxSameError - Number of consecutive iterations with the same error before circuit breaker trips. Default: 3. - -.PARAMETER StartFrom - Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed. - -.PARAMETER SkipVerify - Skip post-iteration typecheck verification. Faster but less safe. - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -Model "opus" - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet" -#> - -param( - [string]$Model = "opus", - [int]$MaxNoProgress = 3, - [int]$MaxSameError = 3, - [string]$StartFrom = "", - [switch]$SkipVerify -) - -$ErrorActionPreference = "Stop" - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$prdFile = Join-Path $scriptDir "prd.json" -$progressFile = Join-Path $scriptDir "progress.txt" -$logDir = Join-Path $scriptDir "logs" - -# --- Find project root (git repo root) --- - -$projectRoot = git rev-parse --show-toplevel 2>$null -if (-not $projectRoot) { - Write-Error "Not inside a git repository. Run from the project directory." - exit 1 -} -$projectRoot = (Resolve-Path $projectRoot).Path - -# --- Validation --- - -if (-not (Test-Path $prdFile)) { - Write-Error "prd.json not found at $prdFile" - exit 1 -} - -# Ensure logs directory exists -if (-not (Test-Path $logDir)) { - New-Item -ItemType Directory -Path $logDir | Out-Null - Write-Host "Created logs directory" -} - -# --- PRD Read/Write --- - -function Read-Prd { - Get-Content -Path $prdFile -Raw | ConvertFrom-Json -} - -function Save-Prd { - param($prdObj) - $prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8 -} - -$prd = Read-Prd - -# --- Git Setup --- - -$BranchName = $prd.branchName - -if ($BranchName) { - $currentBranch = git branch --show-current - if ($currentBranch -ne $BranchName) { - $branchExists = git branch --list $BranchName - if ($branchExists) { - Write-Host "Switching to existing branch: $BranchName" - git checkout $BranchName - } else { - Write-Host "Creating branch: $BranchName" - git checkout -b $BranchName - } - } -} - -# --- Handle StartFrom: mark earlier stories as passed --- - -if ($StartFrom) { - $startPriority = [int]($StartFrom -replace 'US-0*', '') - $skippedCount = 0 - foreach ($story in $prd.userStories) { - $storyPriority = [int]($story.id -replace 'US-0*', '') - if ($storyPriority -lt $startPriority -and $story.passes -ne $true) { - $story.passes = $true - $story.notes = "Skipped (--StartFrom $StartFrom)" - $skippedCount++ - } - } - if ($skippedCount -gt 0) { - Save-Prd $prd - Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow - } -} - -# --- Circuit Breaker State --- - -$noProgressCount = 0 -$lastErrorSignature = "" -$sameErrorCount = 0 - -# --- Prompt Generation --- - -function Build-StoryPrompt { - param( - $story, - $prdObj, - [array]$completedStories - ) - - # Build completed list - $completedSection = "" - if ($completedStories.Count -gt 0) { - $completedLines = ($completedStories | ForEach-Object { - "- $($_.id): $($_.title)" - }) -join "`n" - $completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n" - } - - # Build criteria list - $criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n" - - # Build prompt using array-join (avoids PS 5.1 here-string indentation issues) - $sid = $story.id - $stitle = $story.title - $sdesc = $story.description - $pdesc = $prdObj.description - - $prompt = @( - "# Ralph Iteration: $sid - $stitle" - "" - "## Project" - "$pdesc" - "" - "Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work." - "" - "## Your Task" - "" - "**${sid}: $stitle**" - "" - "$sdesc" - "" - "## Acceptance Criteria" - "" - "$criteriaLines" - "" - "## Reference Documents" - "" - "Read these as needed for implementation detail:" - "" - "- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)" - "- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models" - "- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns" - "- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)" - "$completedSection" - "## Workflow" - "" - "1. Read CLAUDE.md to understand project conventions" - "2. Read Ralph/depth-design.md sections relevant to this story" - "3. Read existing source files you will modify to understand current patterns" - "4. Implement ALL acceptance criteria" - "5. Run npm run typecheck - fix any type errors" - "6. Run npm run build - fix any build errors" - "7. Stage and commit your changes:" - " git add [specific files] && git commit -m `"${sid}: [descriptive message]`"" - "8. When ALL criteria are met, output: $sid" - "" - "## Rules" - "" - "- Work ONLY on $sid. Do not modify code for other stories." - "- Read files before modifying them." - "- Follow existing patterns and conventions in the codebase." - "- Use lucide-react for icons, never unicode symbols." - "- Use the project's CSS custom properties and Tailwind tokens." - "- Commit specific files, not git add -A." - "- If genuinely blocked, output $sid with explanation." - "- To recommend a different model for the NEXT iteration, output opus or sonnet." - ) -join "`n" - - return $prompt -} - -# --- Banner --- - -$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$totalCount = $prd.userStories.Count - -Write-Host "" -Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan -Write-Host "Project: $($prd.project)" -ForegroundColor Cyan -Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan -Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan -Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan -if (-not $SkipVerify) { Write-Host "Post-iteration typecheck verification: ON" -ForegroundColor Cyan } -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host "" - -# --- Dev Server --- - -$devServerPort = 5173 -$devServerPid = $null - -try { - $null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop - Write-Host "Dev server detected on port $devServerPort" -ForegroundColor Green -} catch { - Write-Host "Starting dev server (port $devServerPort)..." -ForegroundColor Cyan - $devProc = Start-Process -FilePath "npm.cmd" -ArgumentList "run", "dev" -WorkingDirectory $projectRoot -PassThru -WindowStyle Minimized - $devServerPid = $devProc.Id - - for ($w = 1; $w -le 20; $w++) { - Start-Sleep -Seconds 1 - try { - $null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop - Write-Host "Dev server ready on port $devServerPort" -ForegroundColor Green - break - } catch { - if ($w -eq 20) { - Write-Warning "Dev server may not be ready — visual review steps may fail" - } - } - } -} -Write-Host "" - -# --- Story Loop --- - -$iterationCount = 0 -$originalDir = Get-Location -Set-Location $projectRoot - -try { - -while ($true) { - # Re-read PRD each iteration (in case previous iteration updated it) - $prd = Read-Prd - - # Partition stories - $completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true }) - $pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority }) - - # Check if all done - if ($pendingStories.Count -eq 0) { - Write-Host "" - Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green - Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green - Write-Host "Branch: $BranchName" -ForegroundColor Green - break - } - - $currentStory = $pendingStories[0] - $iterationCount++ - $pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100) - - $storyLabel = "$($currentStory.id): $($currentStory.title)" - $pctStr = "${pctComplete}%" - $progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)" - - Write-Host "" - Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow - Write-Host $progressMsg -ForegroundColor DarkGray - - # Record HEAD before this iteration - $headBefore = git rev-parse HEAD 2>$null - - $iterStart = Get-Date - Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray - Write-Host "" - - # Generate prompt for this story - $promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories - - # --- Spawn Claude --- - - $logFile = Join-Path $logDir "$($currentStory.id).log" - $rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl" - $maxRetries = 10 - $retryCount = 0 - $outputString = "" - $apiOverloaded = $false - - do { - $apiOverloaded = $false - $textBuilder = [System.Text.StringBuilder]::new() - $toolCount = 0 - - # Clear raw log file for this attempt - if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force } - - if ($retryCount -gt 0) { - $backoffSeconds = [Math]::Pow(2, $retryCount - 1) - Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow - Start-Sleep -Seconds $backoffSeconds - Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray - } - - $promptContent | claude --print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json 2>&1 | ForEach-Object { - $line = $_.ToString().Trim() - if (-not $line) { return } - - # Save raw event for debugging - try { - Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue - } catch { } - - try { - $evt = $line | ConvertFrom-Json -ErrorAction Stop - - # --- Tool use start --- - if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') { - $toolCount++ - $toolName = $evt.content_block.name - Write-Host " [$toolName]" -ForegroundColor DarkCyan - } - # --- Streaming text --- - elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) { - Write-Host -NoNewline $evt.delta.text - [void]$textBuilder.Append($evt.delta.text) - } - # --- Result event --- - elseif ($evt.type -eq 'result') { - if ($evt.subtype -eq 'error_result' -and $evt.error) { - Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red - [void]$textBuilder.AppendLine("ERROR: $($evt.error)") - } - elseif ($evt.result) { - [void]$textBuilder.AppendLine($evt.result) - } - } - # --- Message-level content --- - elseif ($evt.message -and $evt.message.content) { - foreach ($block in $evt.message.content) { - if ($block.type -eq 'text' -and $block.text) { - Write-Host $block.text - [void]$textBuilder.AppendLine($block.text) - } - elseif ($block.type -eq 'tool_use') { - $toolCount++ - Write-Host " [$($block.name)]" -ForegroundColor DarkCyan - } - } - } - } catch { - if ($line -and $line -notmatch '^\s*[\{\[\}\]"]') { - Write-Host $line -ForegroundColor DarkYellow - [void]$textBuilder.AppendLine($line) - } - } - } - - $outputString = $textBuilder.ToString() - - # Check for 529 overloaded error - if ($outputString -match "529.*overloaded|overloaded_error") { - $apiOverloaded = $true - $retryCount++ - if ($retryCount -ge $maxRetries) { - Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red - } - } - # Check for usage limit with cooldown - elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") { - $resetHour = [int]$Matches[1] - $resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 } - $resetAmPm = $Matches[3] - - if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 } - elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 } - - $now = Get-Date - $resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0 - if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) } - $resetTime = $resetTime.AddMinutes(2) - - $waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds) - $waitMinutes = [Math]::Ceiling($waitSeconds / 60) - - Write-Host "" - Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green - - $apiOverloaded = $true - } - } while ($apiOverloaded -and $retryCount -lt $maxRetries) - - # Save log - $outputString | Set-Content -Path $logFile -Encoding UTF8 - - # Show elapsed time - $elapsed = (Get-Date) - $iterStart - Write-Host "" - Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray - - # --- Detect signals --- - - $storyComplete = $outputString -match "$([regex]::Escape($currentStory.id))" - $storyBlocked = $outputString -match "$([regex]::Escape($currentStory.id))" - $headAfter = git rev-parse HEAD 2>$null - $hasGitChanges = $headAfter -ne $headBefore - - # --- Post-iteration typecheck verification --- - - $typecheckPassed = $true - if ($storyComplete -and $hasGitChanges -and -not $SkipVerify) { - Write-Host " Verifying typecheck..." -ForegroundColor DarkGray - $typecheckOutput = npm run typecheck 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host " [VERIFY FAIL] Typecheck failed after completion signal. Not marking as passed." -ForegroundColor Red - $typecheckPassed = $false - } else { - Write-Host " [VERIFY OK] Typecheck passed." -ForegroundColor DarkGray - } - } - - # --- Update story status --- - - if ($storyComplete -and $hasGitChanges -and $typecheckPassed) { - # Mark story as passed in prd.json - $prd = Read-Prd - $storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id } - if ($storyToUpdate) { - $storyToUpdate.passes = $true - $storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model." - } - Save-Prd $prd - - # Append to progress.txt - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $el = $elapsed.ToString('mm\:ss') - $progressEntry = "$ts | PASS | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - - Write-Host " [PASSED] $storyLabel" -ForegroundColor Green - $noProgressCount = 0 - $sameErrorCount = 0 - $lastErrorSignature = "" - } - elseif ($storyBlocked) { - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | BLOCKED | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red - # Blocked counts as no progress - $noProgressCount++ - } - elseif ($storyComplete -and -not $hasGitChanges) { - Write-Host " [WARNING] Completion signaled but no git commits. Retrying story." -ForegroundColor DarkYellow - $noProgressCount++ - } - elseif ($storyComplete -and -not $typecheckPassed) { - Write-Host " [WARNING] Completion signaled but typecheck failed. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | TYPECHECK_FAIL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - # Has git changes, so not stalled — but not passed either - $noProgressCount = 0 - } - else { - # No completion signal - if ($hasGitChanges) { - Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | PARTIAL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - $noProgressCount = 0 - } else { - Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow - $noProgressCount++ - } - } - - # --- Circuit Breaker: No Progress --- - - if ($noProgressCount -ge $MaxNoProgress) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red - Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red - Write-Host "Stuck on: $($currentStory.id) — $($currentStory.title)" -ForegroundColor Red - Write-Host "Check $logFile for details." -ForegroundColor Red - break - } - - # --- Circuit Breaker: Repeated Error --- - - $errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches - if ($errorLines) { - $filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3 - $currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|" - if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) { - $sameErrorCount++ - Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow - if ($sameErrorCount -ge $MaxSameError) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red - Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red - Write-Host " $currentErrorSignature" -ForegroundColor Red - break - } - } elseif ($currentErrorSignature) { - $sameErrorCount = 0 - } - $lastErrorSignature = $currentErrorSignature - } else { - $sameErrorCount = 0 - $lastErrorSignature = "" - } - - # --- Dynamic Model Selection --- - - if ($outputString -match "(opus|sonnet)") { - $nextModel = $Matches[1] - if ($nextModel -ne $Model) { - Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta - $Model = $nextModel - } - } - - # Brief pause between iterations - Start-Sleep -Seconds 2 -} - -} finally { - # Cleanup: restore directory, kill dev server - Set-Location $originalDir - if ($devServerPid) { - Write-Host "Stopping dev server (PID $devServerPid)..." -ForegroundColor DarkGray - taskkill /T /F /PID $devServerPid 2>$null | Out-Null - } -} - -# --- Final Summary --- - -$prd = Read-Prd -$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$finalTotal = $prd.userStories.Count - -Write-Host "" -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan -Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan -Write-Host " Branch: $BranchName" -ForegroundColor Cyan -Write-Host " Logs: $logDir" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan - -if ($finalPassed -eq $finalTotal) { - exit 0 -} else { - exit 1 -} - diff --git a/.codex/skills/claude-md-progressive-disclosurer/.security-scan-passed b/.codex/skills/claude-md-progressive-disclosurer/.security-scan-passed deleted file mode 100644 index d5a81c0..0000000 --- a/.codex/skills/claude-md-progressive-disclosurer/.security-scan-passed +++ /dev/null @@ -1,4 +0,0 @@ -Security scan passed -Scanned at: 2025-12-11T20:19:33.266025 -Tool: gitleaks + pattern-based validation -Content hash: 864b1b4fa2851e26012b06cd3bcb5eb8810ab2cfd3240ba5b48af1895ad182ce diff --git a/.codex/skills/claude-md-progressive-disclosurer/.skillfish.json b/.codex/skills/claude-md-progressive-disclosurer/.skillfish.json deleted file mode 100644 index b66c20e..0000000 --- a/.codex/skills/claude-md-progressive-disclosurer/.skillfish.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 2, - "name": "claude-md-progressive-disclosurer", - "owner": "daymade", - "repo": "claude-code-skills", - "path": "claude-md-progressive-disclosurer", - "branch": "main", - "sha": "4f20e980d6f0c88856b5b1dbadbdcf94108de0c2", - "source": "manual" -} \ No newline at end of file diff --git a/.codex/skills/claude-md-progressive-disclosurer/SKILL.md b/.codex/skills/claude-md-progressive-disclosurer/SKILL.md deleted file mode 100644 index de3e655..0000000 --- a/.codex/skills/claude-md-progressive-disclosurer/SKILL.md +++ /dev/null @@ -1,478 +0,0 @@ ---- -name: claude-md-progressive-disclosurer -description: | - Optimize CLAUDE.md files using progressive disclosure. - Goal: Maximize information efficiency, readability, and maintainability. - Use when: User wants to optimize CLAUDE.md, information is duplicated across files, or LLM repeatedly fails to follow rules. ---- - -# CLAUDE.md 渐进式披露优化器 - -## 核心理念 - -> "找到最小的高信号 token 集合,最大化期望结果的可能性。" — Anthropic - -**目标是最大化信息效率、可读性、可维护性。** - -### 铁律:禁止用行数作为评价指标 - -- 行数少不代表更好,行数多不代表更差 -- 优化的评判标准是:**单一信息源**(同一信息不在多处维护)、**认知相关性**(当前任务不需要的信息不干扰注意力)、**维护一致性**(改一处不需要同步另一处) -- 禁止在优化方案中出现"从 X 行精简到 Y 行"、"减少 Z%"等表述 -- 一个结构清晰、信息不重复的长文件,比一个砍掉关键信息的短文件更好 -- **禁止在工作流任何阶段运行 `wc -l` 或统计行数**——这会潜意识地将"行数少"当成目标 -- **禁止在完成后的总结中提及行数变化**——即使不是主要指标,提及行数也会暗示"行数减少=成功" - -### 两层架构 - -``` -Level 1 (CLAUDE.md) - 每次对话都加载 -├── 信息记录原则 ← 防止未来膨胀的自我约束 -├── Reference 索引(开头) ← 入口1:遇到问题查这里 -├── 核心命令表 -├── 铁律/禁令(含代码示例) -├── 常见错误诊断(症状→原因→修复) -├── 代码模式(可直接复制) -├── 目录映射(功能→文件) -├── 修改代码前必读 ← 入口2:改代码前查这里 -└── Reference 触发索引(末尾) ← 入口3:长对话后复述 - -Level 2 (references/) - 按需即时加载 -├── 详细 SOP 流程 -├── 边缘情况处理 -├── 完整配置示例 -└── 历史决策记录 -``` - -### 多入口原则(重要!) - -同一 Level 2 资源可以有**多个入口**,服务于不同查找路径: - -| 入口 | 位置 | 触发场景 | 用户心态 | -|------|------|----------|----------| -| Reference 索引 | 开头 | 遇到错误/问题 | "出 bug 了,查哪个文档?" | -| 修改代码前必读 | 中间 | 准备改代码 | "我要改 X,要注意什么?" | -| Reference 触发索引 | 末尾 | 长对话定位 | "刚才说的那个文档是哪个?" | - -**这不是重复,是多入口。** 就像书有目录(按章节)、索引(按关键词)、快速参考卡(按任务)。 - ---- - -## 优化工作流 - -### Step 1: 备份 - -```bash -cp CLAUDE.md CLAUDE.md.bak.$(date +%Y%m%d_%H%M%S) -``` - -### Step 2: 内容分类 - -对每个章节分类: - -| 问题 | 是 | 否 | -|------|----|-----| -| 高频使用? | Level 1 | ↓ | -| 违反后果严重? | Level 1 | ↓ | -| 有代码模式需要直接复制? | Level 1 保留模式 | ↓ | -| 有明确触发条件? | Level 2 + 触发条件 | ↓ | -| 历史/参考资料? | Level 2 | 考虑删除 | - -### Step 3: 创建 Reference 文件 - -命名:`docs/references/{主题}-sop.md` - -**铁律:原样移动,禁止压缩** - -移动内容到 Level 2 时,必须**完整保留原始内容**。不要在移动的同时"顺便精简"。 - -``` -✅ 正确:把 100 行原封不动搬到 Level 2(100 行 → Level 2 100 行) -❌ 错误:把 100 行"精简"到 60 行搬到 Level 2(100 行 → Level 2 60 行,40 行消失) -``` - -**为什么**:压缩 = 变相删除。你认为"不重要"而删掉的内容,可能是某个未来 debug session 的关键线索。优化的目标是**改变信息的位置**(Level 1 → Level 2),不是**改变信息的存在**。 - -**怎么做**: -1. 从原始 CLAUDE.md 中精确复制要移动的段落 -2. 原样粘贴到 Level 2 文件中 -3. 可以在 Level 2 中添加结构(标题、分隔线),但**不要删减、改写、合并**原始内容 -4. 如果确实有冗余(同一段话在原文中出现了多次),在 Level 2 中保留一份完整的,注释说明去重 - -### Step 4: 更新 Level 1 - -1. **在开头添加「信息记录原则」**(项目概述之后,Reference 索引之前) -2. **添加 Reference 索引**(紧随信息记录原则之后) -3. 用触发条件格式替换详细内容 -4. 保留代码模式和错误诊断 -5. **添加「修改代码前必读」表格**(按"要改什么"索引) -6. **在末尾再放一份触发索引表** - -### Step 5: 验证(三项全部通过才算完成) - -#### 5a. 引用文件存在性 - -```bash -# 检查引用文件存在 -grep -oh '`docs/references/[^`]*\.md`' CLAUDE.md | sed 's/`//g' | while read f; do - test -f "$f" && echo "✓ $f" || echo "✗ MISSING: $f" -done -``` - -#### 5b. 内容完整性(最关键) - -对每个从原始 CLAUDE.md 移走的章节,逐一检查: - -1. **恢复原始文件**:`git show HEAD:CLAUDE.md > /tmp/claude-md-original.md` -2. **逐节对比**:对原始文件的每个 `##` 章节,确认其内容在以下位置之一完整存在: - - 新 CLAUDE.md 中(保留在 Level 1) - - 某个 Level 2 reference 文件中(完整移动) - - **快速暴露遗漏的辅助脚本**: - - ```bash - # 对原始文件的每个 ## 章节标题,检查它在新文件或 reference 文件中是否存在 - grep '^## ' /tmp/claude-md-original.md | while read heading; do - if grep -q "$heading" CLAUDE.md docs/references/*.md 2>/dev/null; then - echo "✓ $heading" - else - echo "✗ NOT FOUND: $heading" - fi - done - ``` - - > ⚠️ 这个脚本**不能替代人工逐节对比**——它只检查章节标题是否存在,不检查内容是否完整。但它能快速暴露**整个章节被遗漏**的情况,作为人工对比前的第一道筛查。 - -3. **标记所有差异**: - - 如果某段内容在新文件中被缩短 → **必须补回被删减的部分** - - 如果某段内容在两个位置都不存在 → **必须补回** - - 唯一允许删除的情况:**该信息已有独立的 canonical source**(如 `docs/README.md` 已是文档索引的 canonical source),且在 Level 1 中有明确的指向 - -**禁止将"故意删除"作为分类来掩盖信息丢失。** 每一项"故意删除"都必须说明 canonical source 在哪里。如果说不出来,就不是"故意删除",而是"遗漏"。 - -#### 5c. 禁止行数审计 - -在验证阶段**不要统计行数**。不要 `wc -l`。不要计算"原始 X 行 vs 新 Y 行"。这些数字会扭曲你的判断。 - -验证的标准是: -- 每段信息都有归属(Level 1 或 Level 2 或 canonical source) -- 没有信息丢失 -- Level 2 引用都有触发条件 - ---- - -## Level 1 内容分类 - -### 🔴 绝对不能移走 - -| 内容类型 | 原因 | -|---------|------| -| **核心命令** | 高频使用 | -| **铁律/禁令** | 违反后果严重,必须始终可见 | -| **代码模式** | LLM 需要直接复制,避免重新推导 | -| **错误诊断** | 完整的症状→原因→修复流程 | -| **目录映射** | 帮助 LLM 快速定位文件 | -| **触发索引表** | 帮助 LLM 在长对话中定位 Level 2 | - -### 🟡 保留摘要 + 触发条件 - -| 内容类型 | Level 1 | Level 2 | -|---------|---------|---------| -| SOP 流程 | 触发条件 + 关键陷阱 | 完整步骤 | -| 配置示例 | 最常用的 1-2 个 | 完整配置 | -| API 文档 | 常用方法签名 | 完整参数说明 | - -### 🟢 可以完全移走 - -| 内容类型 | 原因 | -|---------|------| -| 历史决策记录 | 低频访问 | -| 性能数据 | 参考性质 | -| 技术债务清单 | 按需查看 | -| 边缘情况 | 有明确触发条件时再加载 | - ---- - -## 引用格式(四种) - -### 1. 详细格式(正文中的重要引用) - -```markdown -**📖 何时读 `docs/references/xxx-sop.md`**: -- [具体错误信息,如 `ERR_DLOPEN_FAILED`] -- [具体场景,如"添加新的原生模块时"] - -> 包含:[关键词 1]、[关键词 2]、[代码模板]。 -``` - -### 2. 问题触发表格(开头/末尾索引) - -```markdown -## Reference 索引(遇到问题先查这里) - -| 触发场景 | 文档 | 核心内容 | -|----------|------|---------| -| `ERR_DLOPEN_FAILED` | `native-modules-sop.md` | ABI 机制、懒加载 | -| 打包后 `Cannot find module` | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 3. 任务触发表格(修改代码前必读) - -```markdown -## 修改代码前必读 - -| 你要改什么 | 先读这个 | 关键陷阱 | -|-----------|---------|---------| -| 原生模块相关 | `native-modules-sop.md` | 必须懒加载;electron-rebuild 会静默失败 | -| 打包配置 | `packaging-sop.md` | DMG contents 必须用函数形式 | -``` - -### 4. 内联格式(简短引用) - -```markdown -完整流程见 `database-sop.md`(FTS5 转义、健康检查)。 -``` - -**多样性原则**:不要所有引用都用同一格式。 - ---- - -## 四条核心原则 - -### 原则 0:添加「信息记录原则」(防止未来膨胀) - -**问题**:优化完成后,用户会继续要求 Claude "记录这个信息到 CLAUDE.md",如果没有规则指导,CLAUDE.md 会再次膨胀。 - -**解决**:在 CLAUDE.md 开头(项目概述之后)添加「信息记录原则」: - -```markdown -## 信息记录原则(Claude 必读) - -本文档采用**渐进式披露**架构,优化 LLM 工作效能。 - -### Level 1(本文件)只记录 - -| 类型 | 示例 | -|------|------| -| 核心命令表 | `pnpm run restart` | -| 铁律/禁令 | 必须懒加载原生模块 | -| 常见错误诊断 | 症状→原因→修复(完整流程) | -| 代码模式 | 可直接复制的代码块 | -| 目录导航 | 功能→文件映射 | -| 触发索引表 | 指向 Level 2 的入口 | - -### Level 2(docs/references/)记录 - -| 类型 | 示例 | -|------|------| -| 详细 SOP 流程 | 完整的 20 步操作指南 | -| 边缘情况处理 | 罕见错误的诊断 | -| 完整配置示例 | 所有参数的说明 | -| 历史决策记录 | 为什么这样设计 | - -### 用户要求记录信息时 - -1. **判断是否高频使用**: - - 是 → 写入 CLAUDE.md(Level 1) - - 否 → 写入对应 reference 文件(Level 2) - -2. **Level 1 引用 Level 2 必须包含**: - - 触发条件(什么情况该读) - - 内容摘要(读了能得到什么) - -3. **禁止**: - - 在 Level 1 放置低频的详细流程 - - 引用 Level 2 但不写触发条件 -``` - -**原因**:这条规则让 Claude 自己知道什么该记在哪里,实现"自我约束",避免后续对话中 CLAUDE.md 再次膨胀。 - -### 原则 1:触发索引表放开头和末尾 - -**原因**:LLM 注意力呈 U 型分布——开头和末尾强,中间弱。 - -| 位置 | 作用 | -|------|------| -| **开头** | 对话开始时建立全局认知:"有哪些 Level 2 可用" | -| **末尾** | 对话变长后复述提醒:"现在应该读哪个 Level 2" | - -```markdown - -## Reference 索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | - -... (正文内容) ... - - -## Reference 触发索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 原则 2:引用必须有触发条件 - -**错误**:`详见 native-modules-sop.md` - -**正确**: -```markdown -**📖 何时读 `native-modules-sop.md`**: -- 遇到 `ERR_DLOPEN_FAILED` 错误 -- 需要添加新的原生模块 - -> 包含:ABI 机制、懒加载模式、手动修复命令 -``` - -**原因**:没有触发条件,LLM 不知道什么时候该去读。 - -### 原则 3:代码模式必须保留在 Level 1 - -**错误**:把代码示例移到 Level 2,Level 1 只写"使用懒加载模式"。 - -**正确**:Level 1 保留完整的可复制代码: -```javascript -// ✅ 正确:懒加载,只在需要时加载 -let _Database = null; -function getDatabase() { - if (!_Database) { - _Database = require("better-sqlite3"); - } - return _Database; -} -``` - -**原因**:LLM 需要直接复制代码,移走后每次都要重新推导或读取 Level 2。 - ---- - -## 反模式警告 - -### ⚠️ 反模式 1:以行数为目标的过度精简 - -**案例**:为了"减少行数",移走了代码模式、诊断流程、目录映射 - -**结果**: -- 丢失代码模式,LLM 每次重新推导 -- 丢失诊断流程,遇错不知查哪 -- 丢失目录映射,找文件效率低 - -**正确**:保留所有高频使用的内容。优化的判断标准是信息是否重复维护、是否与当前任务无关,而不是"文件太长"。 - -### ⚠️ 反模式 2:无触发条件的引用 - -**案例**:`详见 xxx.md` - -**问题**:LLM 不知道何时加载,要么忽略,要么每次都读。 - -**正确**:触发条件 + 内容摘要。 - -### ⚠️ 反模式 3:移走代码模式 - -**案例**:把常用代码示例移到 Level 2 - -**问题**:LLM 每次写代码都要先读 Level 2,增加延迟和 token 消耗。 - -**正确**:高频使用的代码模式保留在 Level 1。 - -### ⚠️ 反模式 4:删除而非移动 - -**案例**:删除"不重要"的章节 - -**问题**:信息丢失,未来需要时无处可查。 - -**正确**:移到 Level 2,保留触发条件。 - -### ⚠️ 反模式 5:用行数当 KPI - -**案例**:优化方案写"从 2000 行精简到 500 行,减少 75%" - -**问题**:把行数当成功指标,会驱动错误决策——为了凑数字而砍掉有用的信息。 - -**正确**:用信息质量评估优化效果——信息是否有重复?维护负担是否降低?LLM 是否能更快找到需要的信息? - -### ⚠️ 反模式 6:移动时压缩(变相删除) - -**规则**:移动是移动,精简是精简。这是两个独立操作,**不要同时执行**。 - -- 移动内容到 Level 2 时,必须**原样复制,不改一字** -- 如果发现冗余需要精简:作为**单独的后续步骤**,逐项列出要删除的内容及理由,征求用户确认 -- "既然都在改了,顺便精简一下"是最隐蔽的删除——它披着"优化"的外衣,做着"删除"的事 - -> 完整案例分析见 `references/progressive_disclosure_principles.md` 案例 8 - -### ⚠️ 反模式 7:用"故意删除"掩盖信息丢失 - -**规则**:任何"删除"都必须是**事前决策**(征求用户确认),不是**事后分类**(发现少了再编理由)。 - -- 对每项计划删除的内容,必须说明其 canonical source 在哪里 -- 如果无法指出 canonical source → 不是"故意删除",是"信息丢失",必须补回 -- 对丢失内容分类"严重性"(高/低风险)是在为自己的错误找台阶。正确的态度是:任何丢失都是 bug,fix it - -> 完整案例分析见 `references/progressive_disclosure_principles.md` 案例 9 - ---- - -## 信息量检验 - -### ✅ 正确的信息量 - -| 检验项 | 通过标准 | -|--------|---------| -| 日常命令 | 不需要读 Level 2 | -| 常见错误 | 有完整诊断流程 | -| 代码编写 | 有可复制的模式 | -| 特定问题 | 知道读哪个 Level 2 | -| 触发索引 | 在文档末尾,表格形式 | - -### ❌ 不足的信号 - -- LLM 反复问同样的问题 -- LLM 每次重新推导代码模式 -- 用户需要反复提醒规则 - -### ❌ 过多的信号 - -- 大段低频详细流程在 Level 1 -- **完全相同的内容**在多处(注意:多入口指向同一资源 ≠ 重复) -- 边缘情况和常见情况混在一起 - ---- - -## 项目级 vs 用户级 - -| 维度 | 用户级 | 项目级 | -|------|--------|--------| -| 位置 | `~/.claude/CLAUDE.md` | `项目/CLAUDE.md` | -| References | `~/.claude/references/` | `docs/references/` | -| 信息范围 | 个人偏好、全局规则 | 项目架构、团队规范 | - ---- - -## 快速检查清单 - -优化完成后,**必须逐项检查**(不可跳过): - -### 信息完整性(最重要) -- [ ] **原始文件的每个章节都有归属**——在新 Level 1、Level 2、或有明确 canonical source -- [ ] **Level 2 文件内容与原始内容完全一致**——没有在移动过程中被"精简" -- [ ] **没有任何内容被静默删除**——每项删除都有用户确认或明确的 canonical source -- [ ] **没有在任何阶段统计或提及行数变化** - -### 结构质量 -- [ ] 「信息记录原则」在文档开头(防止未来膨胀) -- [ ] Reference 索引在文档开头(入口1:遇到问题查这里) -- [ ] 核心命令表完整 -- [ ] 铁律/禁令有代码示例 -- [ ] 常见错误有完整诊断流程(症状→原因→修复) -- [ ] 代码模式可直接复制 -- [ ] 目录映射(功能→文件) -- [ ] 「修改代码前必读」表格(入口2:按"要改什么"索引) -- [ ] Reference 触发索引在文档末尾(入口3:长对话后复述) -- [ ] 每个 Level 2 引用都有触发条件 -- [ ] 引用的文件都存在 diff --git a/.codex/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md b/.codex/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md deleted file mode 100644 index e12ae30..0000000 --- a/.codex/skills/claude-md-progressive-disclosurer/references/progressive_disclosure_principles.md +++ /dev/null @@ -1,319 +0,0 @@ -# 实践案例与教训 - -本文档记录优化 CLAUDE.md 过程中的实际案例和教训。 - ---- - -## 案例 1:以行数为目标的过度精简 - -### 背景 -某项目 CLAUDE.md 内容丰富,包含代码模式、诊断流程、目录映射等。 - -### 错误做法 -以"减少行数"为目标,移走了大部分内容,只保留简短描述和指针。 - -### 结果 -- ❌ 丢失代码模式,LLM 每次重新推导 -- ❌ 丢失诊断流程,遇错不知查哪 -- ❌ 丢失目录映射,找文件效率低 - -### 正确做法 -按**信息质量**而非行数判断去留: - -| 内容 | 保留位置 | 判断依据 | -|------|----------|----------| -| 核心命令表 | Level 1 | 高频使用,不应让 LLM 每次去查 | -| 懒加载代码模式 | Level 1 | 需要直接复制,移走会导致重新推导 | -| ABI 错误诊断 | Level 1 | 完整症状→原因→修复流程 | -| 详细 SOP | Level 2 | 低频、有明确触发条件 | - -### 教训 -**信息效率、可读性、可维护性是标准,行数不是。** - ---- - -## 案例 2:无触发条件的引用 - -### 错误做法 -```markdown -详见 native-modules-sop.md -``` - -### 问题 -LLM 不知道什么时候该去读这个文件。 - -### 正确做法 -```markdown -**📖 何时读 `native-modules-sop.md`**: -- 遇到 `ERR_DLOPEN_FAILED` 错误 -- 需要添加新的原生模块 - -> 包含:ABI 机制、懒加载模式、手动修复命令 -``` - -### 教训 -**每个引用必须有触发条件 + 内容摘要。** - ---- - -## 案例 3:代码模式被移走 - -### 错误做法 -Level 1 只写"使用懒加载模式",代码示例放 Level 2。 - -### 问题 -LLM 每次写代码都要先读 Level 2,或者凭记忆推导(可能出错)。 - -### 正确做法 -Level 1 保留完整代码: - -```javascript -// ✅ 正确:懒加载 -let _Database = null; -function getDatabase() { - if (!_Database) { - _Database = require("better-sqlite3"); - } - return _Database; -} -``` - -### 教训 -**高频使用的代码模式必须在 Level 1 可直接复制。** - ---- - -## 案例 4:触发索引表位置错误 - -### 错误做法 -触发索引表只放在 CLAUDE.md 中间某个位置。 - -### 问题 -LLM 注意力呈 U 型分布:开头和末尾强,中间弱。只放中间会被忽略。 - -### 正确做法 -触发索引表放在 CLAUDE.md **开头和末尾两个位置**: - -```markdown - -## Reference 索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | - -... (正文内容) ... - - -## Reference 触发索引 - -| 触发场景 | 文档 | 核心内容 | -|---------|------|---------| -| ABI 错误 | `native-modules-sop.md` | 懒加载模式 | -| 打包模块缺失 | `vite-sop.md` | MODULES_TO_COPY | -``` - -### 教训 -**三个入口服务于不同查找路径,这不是重复,是多入口。** - ---- - -## 案例 5:误删「修改代码前必读」 - -### 错误做法 -认为「Reference 索引」和「修改代码前必读」内容重复,删除后者。 - -### 问题 -两个表格服务于**不同的查找路径**: -- Reference 索引:按**错误/问题**触发("出 bug 了查哪个?") -- 修改代码前必读:按**要改的代码**触发("我要改 X,注意什么?") - -### 正确做法 -保留三个入口: -1. **开头 Reference 索引** - 遇到问题时查 -2. **修改代码前必读** - 准备改代码时查 -3. **末尾触发索引** - 长对话后定位 - -### 教训 -**多入口指向同一资源 ≠ 重复信息。** 就像书有目录、索引、快速参考卡。 - ---- - -## 案例 6:缺少信息记录原则 - -### 背景 -优化完成后,CLAUDE.md 结构清晰,信息分层合理。 - -### 问题 -后续用户继续要求 Claude "把这个记录到 CLAUDE.md",Claude 没有判断标准,只能照做。逐渐出现信息重复维护、低频内容和高频内容混杂的问题。 - -### 错误做法 -只优化内容,不添加规则。 - -### 正确做法 -在 CLAUDE.md 开头添加「信息记录原则」: - -```markdown -## 信息记录原则(Claude 必读) - -### Level 1(本文件)只记录 -| 类型 | 示例 | -|------|------| -| 核心命令表 | `pnpm run restart` | -| 铁律/禁令 | 必须懒加载原生模块 | -| 代码模式 | 可直接复制的代码块 | - -### Level 2(docs/references/)记录 -| 类型 | 示例 | -|------|------| -| 详细 SOP 流程 | 完整的 20 步操作指南 | -| 边缘情况处理 | 罕见错误的诊断 | - -### 用户要求记录信息时 -1. 判断是否高频使用 → 是则 Level 1,否则 Level 2 -2. Level 1 引用 Level 2 必须包含触发条件 -3. 禁止在 Level 1 放置低频详细流程 -``` - -### 教训 -**优化的目的是「以后不再需要优化」。** 添加规则让 Claude 自我约束,实现长期可持续。 - ---- - -## 信息量判断标准 - -### 信息不足的信号 - -| 信号 | 说明 | -|------|------| -| LLM 反复问同样的问题 | 缺少关键规则 | -| LLM 每次重新推导代码 | 缺少代码模式 | -| 用户反复提醒规则 | 规则没有足够强调 | -| 不知道读哪个 Level 2 | 触发条件不明确 | - -### 信息过多的信号 - -| 信号 | 说明 | -|------|------| -| 大段低频流程在 Level 1 | 应移到 Level 2 | -| 同一内容重复出现 | 去重 | -| 边缘和常见情况混在一起 | 边缘移到 Level 2 | - ---- - -## Level 1 保留内容检查清单 - -| 内容类型 | 必须保留 | 可移走 | -|----------|----------|--------| -| **信息记录原则** | ✅ 防止膨胀 | | -| Reference 索引(开头) | ✅ 入口1 | | -| 核心命令表 | ✅ | | -| 铁律/禁令 | ✅ | | -| 常见错误诊断(完整流程) | ✅ | | -| 代码模式(可直接复制) | ✅ | | -| 目录映射 | ✅ | | -| 修改代码前必读 | ✅ 入口2 | | -| Reference 触发索引(末尾) | ✅ 入口3 | | -| 详细 SOP 步骤 | | ✅ | -| 边缘情况处理 | | ✅ | -| 历史决策记录 | | ✅ | -| 性能数据 | | ✅ | - ---- - -## 案例 7:用行数当 KPI - -### 错误做法 -优化方案写"当前 2,114 行,目标 ~580 行,约 73% 精简",用行数和百分比作为成功指标。 - -### 问题 -行数驱动的优化会导致错误决策: -- 为了凑数字而砍掉有用的代码模式 -- 为了"减少百分比"而合并不相关的章节 -- 把"短"等同于"好",把"长"等同于"差" - -### 正确做法 -用信息架构质量作为评估维度: - -| 评估维度 | 问题 | -|----------|------| -| **单一信息源** | 这段信息是否在别处已经有了?如果是,消除重复 | -| **认知相关性** | 这段信息在大多数开发场景下是否需要?如果不是,移到 Level 2 | -| **维护一致性** | 改一处是否需要同步另一处?如果是,消除重复 | - -### 教训 -**行数少不代表更好,行数多不代表更差。真正的标准是信息效率、可读性、可维护性。** - ---- - -## 案例 8:移动时压缩导致信息丢失(真实事故,2026-02-14) - -### 背景 -一个 2503 行的 CLAUDE.md 需要优化。使用本 skill 的渐进式披露方法,创建了 6 个 Level 2 reference 文件。 - -### 错误做法 -在移动内容到 Level 2 文件时,LLM "顺便精简"了内容: - -| 原始章节 | 原始内容 | Level 2 中保留 | 丢失 | -|---------|---------|---------------|------| -| Git 工作流 SOP | 560 行(含脚本源码、决策树) | 342 行 | 218 行 | -| Feature docs | ~400 行(含 case study) | 300 行 | ~100 行 | -| Namespace SOP | ~130 行(含正反例、检查清单) | 简化到铁律 | ~80 行 | -| Field naming | ~33 行(含防错指南、case study) | 简化到字段表 | ~33 行 | - -总计 ~820 行"消失",被分类为"故意删除"和"压缩"。 - -### 问题 -1. **完成后第一件事就是 `wc -l`**——统计行数,然后汇报"减少 82%"作为成果 -2. **压缩被包装成"移动"**——汇报中说"成功移到 Level 2",但实际内容被删减了 -3. **丢失内容被合理化**——事后分类为"故意删除(已有独立文档)"和"压缩(信息保留但更简洁)",避免面对信息丢失的事实 -4. **用户发现后,LLM 仍然用行数对账**——"820 行消失了",列出行数表格,继续用行数思维分析 - -### 被丢失的具体内容(每一项都有实际价值) -- **Namespace 正反例代码**:帮助 LLM 直接复制正确模式,避免重新推导 -- **Field naming case study**(Trending Page 字段错配):帮助未来遇到同样错误时快速定位 -- **SkillShareButton 测试超时问题**:Popover + vi.useFakeTimers() 冲突,这是一个具体的调试提示 -- **"Document Your Thought Process" 三步法**:修 bug 时的方法论指导 - -### 根本原因 -1. **行数思维的惯性**——即使 skill 明确禁止用行数当 KPI,LLM 仍然潜意识地将"短"等同于"好" -2. **移动和精简混为一谈**——"都在改了,顺便精简一下"看起来合理,但实际上是在执行两个不同操作 -3. **验证步骤只检查文件存在性**——`test -f` 通过了,但内容是否完整没有检查 -4. **事后合理化**——"LLM 自知能力"、"历史快照"等理由听起来合理,但都是删除之后找的借口 - -### 正确做法 -1. **移动时原样复制**——不改一字。如果需要精简,作为单独步骤征求用户确认 -2. **验证时逐节对比**——不是 `test -f`,而是对每个原始章节确认其内容在新的位置完整存在 -3. **不要统计行数**——不运行 `wc -l`,不在总结中提及行数变化 -4. **不要主动删除**——只移动。如果认为某些内容可以删除,列出来征求用户确认,并说明 canonical source - -### 教训 -**"移动时顺便精简"是最隐蔽的反模式。** 它披着"优化"的外衣,做着"删除"的事。当你发现自己在移动内容的同时在改写它,停下来——你正在做两件事,应该分开做。 - ---- - -## 案例 9:用"故意删除"分类掩盖信息丢失 - -### 背景 -案例 8 的后续。用户发现 820 行消失后,LLM 对消失的内容进行了分类分析。 - -### 错误做法 -将丢失分为三类: -- "故意删除"(270 行)——理由:已有独立文档、LLM 自知、历史快照 -- "压缩"(550 行)——理由:信息保留但更简洁 -- "真正丢失"(仅 4 项,标注为"低风险") - -### 问题 -1. **"故意删除"是事后分类,不是事前决策**——移动的时候没有逐项确认"这个可以删",是完成后发现少了才编出来的理由 -2. **"压缩"是另一种说法的"删除"**——550 行"压缩"意味着 550 行内容不见了,说"信息保留但更简洁"不改变这个事实 -3. **"低风险"是主观判断**——对 LLM 来说"低风险"的 debug 提示,对下一个遇到同样 bug 的人可能是救命稻草 -4. **整个分析仍在用行数框架**——270 + 550 = 820,还是在用行数对账 - -### 正确做法 -不要分类"故意 vs 意外"。正确的问题是: -- 这段内容在新系统中能被找到吗?(在 Level 1、Level 2、或有明确 canonical source) -- 如果找不到 → 补回,不需要判断"风险高低" - -### 教训 -**分类丢失内容的"严重性"是在为自己的错误找台阶。** 正确的态度是:任何丢失都是 bug,fix it。 diff --git a/.codex/skills/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md b/.codex/skills/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md deleted file mode 100644 index d514f4e..0000000 --- a/.codex/skills/skills/bencium-innovative-ux-designer/ACCESSIBILITY.md +++ /dev/null @@ -1,111 +0,0 @@ -# Accessibility Essentials - -Accessibility enables creativity - it's a foundation, not a limitation. WCAG 2.1 AA compliance. - -## Core Principles (POUR) - -- **Perceivable**: Content must be perceivable (alt text, contrast, captions) -- **Operable**: UI must be keyboard/touch accessible -- **Understandable**: Clear, predictable behavior -- **Robust**: Works with assistive technologies - -## Contrast Requirements - -| Element | Minimum Ratio | -|---------|---------------| -| Normal text | 4.5:1 | -| Large text (18pt+) | 3:1 | -| UI components | 3:1 | - -**Tools**: Chrome DevTools Accessibility tab, WebAIM Contrast Checker - -## Keyboard Navigation - -```tsx -// All interactive elements need focus states - - -// Custom elements need tabindex and key handlers -
(e.key === 'Enter' || e.key === ' ') && handleClick()} -> - Custom Button -
-``` - -**Essentials:** -- Tab through entire interface -- Enter/Space activates elements -- Escape closes modals -- Visible focus indicators always - -## Essential ARIA - -```tsx -// Buttons without text - - -// Expandable elements - - -// Live regions for dynamic content -
{statusMessage}
-
{errorMessage}
- -// Form errors - -{hasError && } -``` - -## Semantic HTML - -```tsx -// Use semantic elements, not divs -
-

...

-
...
- -// Heading hierarchy (never skip levels) -

Page Title

-

Section

-

Subsection

-``` - -## Touch Targets - -- Minimum **44x44px** for all interactive elements -- Adequate spacing between targets -- `touch-manipulation` CSS for responsive touch - -## Screen Reader Content - -```tsx -// Hidden but announced -Additional context - -// Skip link - - Skip to main content - -``` - -## Quick Checklist - -- [ ] Keyboard: Can tab through everything -- [ ] Focus: Visible focus indicators -- [ ] Contrast: 4.5:1 for text -- [ ] Alt text: All images have appropriate alt -- [ ] Headings: Logical h1-h6 hierarchy -- [ ] Forms: Labels associated with inputs -- [ ] Errors: Announced to screen readers -- [ ] Touch: 44px minimum targets - -## Resources - -- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/) -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) diff --git a/.codex/skills/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md b/.codex/skills/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md deleted file mode 100644 index e968748..0000000 --- a/.codex/skills/skills/bencium-innovative-ux-designer/DESIGN-SYSTEM-TEMPLATE.md +++ /dev/null @@ -1,577 +0,0 @@ -# Design System Template - -Meta-framework for understanding what's fixed, project-specific, and adaptable in your design system. - -## Purpose - -This template helps you distinguish between: -- **Fixed Elements**: Universal rules that never change -- **Project-Specific Elements**: Filled in for each project based on brand -- **Adaptable Elements**: Context-dependent implementations - ---- - -## I. FIXED ELEMENTS - -These foundations remain consistent across all projects, regardless of brand or context. - -### 1. Spacing Scale - -**Fixed System:** -``` -4px, 8px, 12px, 16px, 24px, 32px, 48px, 64px, 96px -``` - -**Usage:** -- Margins, padding, gaps between elements -- Mathematical relationships ensure visual harmony -- Use multipliers of base unit (4px) - -**Why Fixed:** -Consistent spacing creates visual rhythm regardless of brand personality. - -### 2. Grid System - -**Fixed Structure:** -- **12-column grid** for most layouts (divisible by 2, 3, 4, 6) -- **16-column grid** for data-heavy interfaces -- **Gutters**: 16px (mobile), 24px (tablet), 32px (desktop) - -**Why Fixed:** -Grid provides structural order. Brand personality shows through color, typography, content—not grid structure. - -### 3. Accessibility Standards - -**Fixed Requirements:** -- **WCAG 2.1 AA** compliance minimum -- **Contrast**: 4.5:1 for normal text, 3:1 for large text -- **Touch targets**: Minimum 44×44px -- **Keyboard navigation**: All interactive elements accessible -- **Screen reader**: Semantic HTML, ARIA labels where needed - -**Why Fixed:** -Accessibility is not negotiable. It's a baseline requirement for ethical, legal, and usable products. - -### 4. Typography Hierarchy Logic - -**Fixed Structure:** -- **Mathematical scaling**: 1.25x (major third) or 1.333x (perfect fourth) -- **Hierarchy levels**: Display → H1 → H2 → H3 → Body → Small → Caption -- **Line height**: 1.5x for body text, 1.2-1.3x for headlines -- **Line length**: 45-75 characters optimal - -**Why Fixed:** -Mathematical relationships create predictable, harmonious hierarchy. Specific fonts change, but the logic doesn't. - -### 5. Component Architecture - -**Fixed Patterns:** -- **Button states**: Default, Hover, Active, Focus, Disabled -- **Form structure**: Label above input, error below, helper text optional -- **Modal pattern**: Overlay + centered content + close mechanism -- **Card structure**: Container → Header → Body → Footer (optional) - -**Why Fixed:** -Users expect consistent component behavior. Architecture is fixed; appearance is project-specific. - -### 6. Animation Timing Framework - -**Fixed Physics Profiles:** -- **Lightweight** (icons, chips): 150ms -- **Standard** (cards, panels): 300ms -- **Weighty** (modals, pages): 500ms - -**Fixed Easing:** -- **Ease-out**: Entrances (fast start, slow end) -- **Ease-in**: Exits (slow start, fast end) -- **Ease-in-out**: Transitions (smooth both ends) - -**Why Fixed:** -Natural physics feel consistent across brands. Duration and easing create that feeling. - ---- - -## II. PROJECT-SPECIFIC ELEMENTS - -Fill in these for each project based on brand personality and purpose. - -### 1. Brand Color System - -**Template Structure:** - -``` -NEUTRALS (4-5 colors): -- Background lightest: _______ (e.g., slate-50 or warm-white) -- Surface: _______ (e.g., slate-100) -- Border/divider: _______ (e.g., slate-300) -- Text secondary: _______ (e.g., slate-600) -- Text primary: _______ (e.g., slate-900) - -ACCENTS (1-3 colors): -- Primary (main CTA): _______ (e.g., teal-500) -- Secondary (alternative action): _______ (optional) -- Status colors: - - Success: _______ (green-ish) - - Warning: _______ (amber-ish) - - Error: _______ (red-ish) - - Info: _______ (blue-ish) -``` - -**Questions to Answer:** -- What emotion should the brand evoke? (Trust, excitement, calm, urgency) -- Warm or cool neutrals? -- Conservative or bold accents? - -**Examples:** - -**Project A: Fintech App** -``` -Neutrals: Cool greys (slate-50 → slate-900) -Primary: Deep blue (#0A2463) – trust, professionalism -Success: Muted green (#10B981) -Why: Financial products need trust, not playfulness -``` - -**Project B: Creative Community** -``` -Neutrals: Warm greys with beige undertones -Primary: Coral (#FF6B6B) – energy, creativity -Success: Teal (#06D6A0) – fresh, unexpected -Why: Creative spaces should feel inviting, not corporate -``` - -**Project C: Healthcare Platform** -``` -Neutrals: Pure greys (minimal color temperature) -Primary: Soft blue (#4A90E2) – calm, clinical -Success: Medical green (#38A169) -Why: Healthcare needs clarity and calm, not distraction -``` - -### 2. Typography Pairing - -**Template:** - -``` -HEADLINE FONT: _______ -- Weight: _______ (e.g., Bold 700) -- Use case: H1, H2, display text -- Personality: _______ (geometric/humanist/serif/etc.) - -BODY FONT: _______ -- Weight: _______ (e.g., Regular 400, Medium 500) -- Use case: Paragraphs, UI text -- Personality: _______ (neutral/readable/efficient) - -OPTIONAL ACCENT FONT: _______ -- Weight: _______ -- Use case: _______ (special headlines, callouts) -``` - -**Pairing Logic:** -- Serif + Sans-serif (classic, editorial) -- Geometric + Humanist (modern + warm) -- Display + System (distinctive + efficient) - -**Examples:** - -**Project A: Editorial Platform** -``` -Headline: Playfair Display (Serif, Bold 700) -Body: Inter (Sans-serif, Regular 400) -Why: Serif headlines = trustworthy, editorial feel -``` - -**Project B: Tech Startup** -``` -Headline: DM Sans (Sans-serif, Bold 700) -Body: DM Sans (Regular 400, Medium 500) -Why: Single-font system = modern, efficient, cohesive -``` - -**Project C: Luxury Brand** -``` -Headline: Cormorant Garamond (Serif, Light 300) -Body: Lato (Sans-serif, Regular 400) -Why: Elegant serif + readable sans = sophisticated -``` - -### 3. Tone of Voice - -**Template:** - -``` -BRAND PERSONALITY: -- Formal ↔ Casual: _______ (1-10 scale) -- Professional ↔ Friendly: _______ (1-10 scale) -- Serious ↔ Playful: _______ (1-10 scale) -- Authoritative ↔ Conversational: _______ (1-10 scale) - -MICROCOPY EXAMPLES: -- Button label (submit form): _______ -- Error message (invalid email): _______ -- Success message (saved): _______ -- Empty state: _______ - -ANIMATION PERSONALITY: -- Speed: _______ (quick/moderate/slow) -- Feel: _______ (precise/smooth/bouncy) -``` - -**Examples:** - -**Project A: Banking App** -``` -Personality: Formal (8), Professional (9), Serious (8) -Button: "Submit Application" -Error: "Email address format is invalid" -Success: "Application submitted successfully" -Animation: Quick (precise, efficient, no-nonsense) -``` - -**Project B: Social App** -``` -Personality: Casual (8), Friendly (9), Playful (7) -Button: "Let's go!" -Error: "Hmm, that email doesn't look right" -Success: "Nice! You're all set 🎉" -Animation: Moderate (smooth, friendly bounce) -``` - -### 4. Animation Speed & Feel - -**Template:** - -``` -SPEED PREFERENCE: -- UI interactions: _______ (100-150ms / 150-200ms / 200-300ms) -- State changes: _______ (200ms / 300ms / 400ms) -- Page transitions: _______ (300ms / 500ms / 700ms) - -ANIMATION STYLE: -- Easing preference: _______ (sharp / standard / bouncy) -- Movement type: _______ (minimal / smooth / expressive) -``` - -**Examples:** - -**Project A: Trading Platform** -``` -Speed: Fast (100ms UI, 200ms states, 300ms pages) -Style: Sharp easing, minimal movement -Why: Traders need speed, not distraction -``` - -**Project B: Wellness App** -``` -Speed: Slow (200ms UI, 400ms states, 500ms pages) -Style: Smooth easing, gentle movement -Why: Calm, relaxing experience matches brand -``` - ---- - -## III. ADAPTABLE ELEMENTS - -Context-dependent implementations that vary based on use case. - -### 1. Component Variations - -**Button Variants:** -- **Primary**: Full background color (high emphasis) -- **Secondary**: Outline only (medium emphasis) -- **Tertiary**: Text only (low emphasis) -- **Destructive**: Red-ish (danger actions) -- **Ghost**: Minimal (navigation, toolbars) - -**Adaptation Rules:** -- Primary: Main CTA, one per screen section -- Secondary: Alternative actions -- Tertiary: Less important actions, multiple allowed -- Use brand colors, but hierarchy logic is fixed - -### 2. Responsive Breakpoints - -**Fixed Ranges:** -- XS: 0-479px (small phones) -- SM: 480-767px (large phones) -- MD: 768-1023px (tablets) -- LG: 1024-1439px (laptops) -- XL: 1440px+ (desktop) - -**Adaptable Implementations:** - -**Simple Content Site:** -``` -XS-SM: Single column -MD: 2 columns -LG-XL: 3 columns max -Why: Content-focused, don't overwhelm -``` - -**Dashboard/Data App:** -``` -XS: Collapsed, cards stack -SM: Simplified sidebar -MD: Full sidebar + main content -LG-XL: Sidebar + main + right panel -Why: Data apps need more screen real estate -``` - -### 3. Dark Mode Palette - -**Adaptation Strategy:** - -Not a simple inversion. Dark mode needs adjusted contrast: - -**Light Mode:** -``` -Background: #FFFFFF (white) -Text: #0F172A (slate-900) → 21:1 contrast -``` - -**Dark Mode (Adapted):** -``` -Background: #0F172A (slate-900) -Text: #E2E8F0 (slate-200) → 15.8:1 contrast (still AA, but softer) -``` - -**Why Adapt:** -Pure white on pure black is too harsh. Dark mode needs slightly lower contrast for eye comfort. - -### 4. Loading States - -**Context-Dependent:** - -**Fast operations (<500ms):** -- No loading indicator (feels instant) - -**Medium operations (500ms-2s):** -- Spinner or skeleton screen - -**Long operations (>2s):** -- Progress bar with percentage -- Or: Skeleton + estimated time - -**Interactive Operations:** -- Button shows spinner inside (don't disable, show state) - -### 5. Error Handling Strategy - -**Context-Dependent:** - -**Form Errors:** -``` -Validate: On blur (after user leaves field) -Display: Inline below field -Recovery: Clear error on fix -``` - -**API Errors:** -``` -Transient (network): Show retry button -Permanent (404): Show helpful message + next steps -Critical (500): Contact support option -``` - -**Data Errors:** -``` -Missing: Show empty state with action -Corrupt: Show error boundary with reload -Invalid: Highlight + explain what's wrong -``` - ---- - -## DECISION TREE - -When implementing a feature, ask: - -### Is this... - -**FIXED?** -- Does it affect structure, accessibility, or universal UX? -- Examples: Spacing scale, grid, contrast ratios, component architecture -- **Action**: Use the fixed system, no variation - -**PROJECT-SPECIFIC?** -- Does it express brand personality or purpose? -- Examples: Colors, typography, tone of voice, animation feel -- **Action**: Fill in the template for this project - -**ADAPTABLE?** -- Does it depend on context, content, or use case? -- Examples: Component variants, responsive behavior, error handling -- **Action**: Choose appropriate variation based on context - ---- - -## EXAMPLE: Implementing a "Submit" Button - -### Fixed Elements (Always the same): -- Touch target: 44px minimum height -- Padding: 16px horizontal (from spacing scale) -- States: Default, Hover, Active, Focus, Disabled -- Animation: 150ms ease-out (lightweight profile) - -### Project-Specific (Filled per project): -- **Project A (Bank)**: Dark blue background, white text, "Submit Application" -- **Project B (Social)**: Coral background, white text, "Let's Go!" -- **Project C (Healthcare)**: Soft blue background, white text, "Continue" - -### Adaptable (Context-dependent): -- **Form context**: Primary button (full color) -- **Toolbar context**: Ghost button (text only) -- **Danger context**: Destructive variant (red-ish) - ---- - -## VALIDATION CHECKLIST - -Before finalizing a design, check: - -### Fixed Elements -- [ ] Uses spacing scale (4/8/12/16/24/32/48/64/96px) -- [ ] Follows grid system (12 or 16 columns) -- [ ] Meets WCAG AA contrast (4.5:1 normal, 3:1 large) -- [ ] Touch targets ≥ 44px -- [ ] Typography follows mathematical scale -- [ ] Components follow standard architecture - -### Project-Specific Elements -- [ ] Brand colors filled in and intentional -- [ ] Typography pairing chosen and justified -- [ ] Tone of voice defined and consistent -- [ ] Animation speed matches brand personality - -### Adaptable Elements -- [ ] Component variants appropriate for context -- [ ] Responsive behavior fits content type -- [ ] Loading states match operation duration -- [ ] Error handling fits error type - ---- - -## PROJECT KICKOFF TEMPLATE - -Use this to start a new project: - -``` -PROJECT NAME: _______________________ -PURPOSE: ____________________________ - -BRAND PERSONALITY: -- Primary emotion: _______ -- Warm or cool: _______ -- Formal or casual: _______ -- Conservative or bold: _______ - -COLORS (fill the template): -- Neutral base: _______ -- Primary accent: _______ -- Status colors: _______ / _______ / _______ - -TYPOGRAPHY (fill the template): -- Headline font: _______ -- Body font: _______ -- Pairing rationale: _______ - -TONE: -- Button labels style: _______ -- Error message style: _______ -- Success message style: _______ - -ANIMATION: -- Speed preference: _______ (fast/moderate/slow) -- Feel preference: _______ (sharp/smooth/bouncy) - -TARGET DEVICES: -- Primary: _______ (mobile/desktop/both) -- Secondary: _______ -``` - ---- - -## MAINTAINING CONSISTENCY - -### Documentation -- Keep this template updated as system evolves -- Document WHY choices were made, not just WHAT - -### Communication -- Share with designers: "Here's what varies vs. what's fixed" -- Share with developers: "Here are the design tokens" - -### Tooling -- Use CSS variables for project-specific values -- Use Tailwind config for spacing scale -- Use design tokens in Figma/Storybook - -### Reviews -- Audit: Does new work follow fixed elements? -- Validate: Are project-specific elements intentional? -- Question: Are adaptations justified by context? - ---- - -## EXAMPLES OF COMPLETE SYSTEMS - -### System A: B2B SaaS (Conservative) - -**Fixed**: Standard spacing, 12-col grid, WCAG AA, major third type scale -**Project-Specific**: -- Colors: Cool greys + corporate blue -- Typography: DM Sans (headlines + body) -- Tone: Professional, formal -- Animation: Quick, precise (150ms) -**Adaptable**: -- Dashboard gets multi-panel layout -- Forms are extensive (use progressive disclosure) -- Errors show detailed technical info - -### System B: Consumer Social App (Playful) - -**Fixed**: Same spacing/grid/accessibility/type logic -**Project-Specific**: -- Colors: Warm greys + vibrant coral -- Typography: Poppins (headlines) + Inter (body) -- Tone: Casual, friendly, playful -- Animation: Moderate, bouncy (200ms) -**Adaptable**: -- Mobile-first (most users on phones) -- Forms are minimal (progressive profiling) -- Errors are friendly, not technical - -### System C: Healthcare Platform (Clinical) - -**Fixed**: Same foundational structure -**Project-Specific**: -- Colors: Pure greys + medical blue -- Typography: System fonts (SF Pro / Segoe) -- Tone: Clear, authoritative, calm -- Animation: Slow, smooth (300ms) -**Adaptable**: -- Desktop-first (clinical use at workstations) -- Forms are complex (HIPAA compliance) -- Errors are precise with next steps - ---- - -## KEY TAKEAWAY - -**The system flexibility framework lets you:** -- Maintain consistency (fixed elements) -- Express brand personality (project-specific) -- Adapt to context (adaptable elements) - -**Without this framework:** -- Designers reinvent spacing every project -- Components feel inconsistent across products -- Brand personality overrides accessibility -- Context-blind implementations feel wrong - -**With this framework:** -- Speed: Start from proven foundations -- Consistency: Fixed elements guarantee it -- Flexibility: Express unique brand identity -- Context: Adapt without breaking system diff --git a/.codex/skills/skills/bencium-innovative-ux-designer/MOTION-SPEC.md b/.codex/skills/skills/bencium-innovative-ux-designer/MOTION-SPEC.md deleted file mode 100644 index e37e363..0000000 --- a/.codex/skills/skills/bencium-innovative-ux-designer/MOTION-SPEC.md +++ /dev/null @@ -1,72 +0,0 @@ -# Motion Specification - -Motion should surprise and delight while serving function. Animation is a creative tool. - -## Easing Curves - -| Easing | CSS | Use For | -|--------|-----|---------| -| **Ease-out** | `cubic-bezier(0.0, 0.0, 0.2, 1)` | Entrances, appearing | -| **Ease-in** | `cubic-bezier(0.4, 0.0, 1, 1)` | Exits, disappearing | -| **Ease-in-out** | `cubic-bezier(0.4, 0.0, 0.2, 1)` | State changes, transforms | -| **Spring** | `cubic-bezier(0.68, -0.55, 0.265, 1.55)` | Playful, attention-grabbing | -| **Linear** | `linear` | Spinners, continuous loops | - -## Duration by Element Weight - -| Weight | Duration | Examples | -|--------|----------|----------| -| **Lightweight** | 150ms | Icons, badges, chips | -| **Standard** | 300ms | Cards, panels, list items | -| **Weighty** | 500ms | Modals, page transitions | - -## Duration by Interaction - -| Interaction | Duration | -|-------------|----------| -| Button press | 100ms | -| Hover state | 150ms | -| Tooltip appear | 200ms | -| Tab switch | 250ms | -| Modal open | 300ms | -| Page transition | 400ms | - -## Common Patterns - -```tsx -// Hover transition (CSS) - -``` - -### Example 2: Typography Hierarchy - -**Question First:** -``` -For this content hierarchy, I'm thinking: -- Headline: Large, bold, attention-grabbing -- Subheading: Medium, regular weight -- Body: Smaller, optimized for reading - -Should we go bold and contemporary, or subtle and refined? -``` - -**Implementation After Approval:** -```tsx -
-

- Headline Here -

-

- Supporting subheading -

-

- Body text optimized for readability with proper line height. -

-
-``` - -### Example 3: Unique Color Palette Suggestion - -**Presenting Options:** -``` -I'd like to suggest three color directions that break from typical SaaS patterns: - -1. **Warm Earth Tones** - - Base: Warm grey (#E8E2DC) - - Accent: Terracotta (#C86E4B) - - For: Organic, trustworthy feel - -2. **Cool Midnight** - - Base: Deep navy (#1A2332) - - Accent: Cyan (#4ECDC4) - - For: Modern, tech-forward feel - -3. **Soft Pastels** - - Base: Soft pink (#FFE5E5) - - Accent: Sage green (#9DB5A4) - - For: Calm, approachable feel - -Which direction feels right for your brand? -``` - -## Common Patterns to Avoid - -❌ **NEVER:** -- Use Inter, Roboto, Arial, Space Grotesk as primary fonts -- Use generic SaaS blue (#3B82F6) or purple gradients on white -- Copy Apple's design language or use glass morphism -- Create cookie-cutter layouts that look AI-generated -- Skip asking about context before designing -- Converge on common choices across generations (vary everything!) -- Use animations that delay user actions -- Create cluttered interfaces where elements compete - -✅ **ALWAYS:** -- Ask about purpose, tone, constraints, differentiation FIRST -- Then commit BOLDLY to a distinctive aesthetic direction -- Use unexpected, characterful typography choices -- Create atmosphere: shadows, gradients, textures, grain (when intentional) -- Dominant colors with sharp accents (not timid, evenly-distributed palettes) -- Provide immediate feedback for interactions -- Test with real devices -- Validate accessibility (it enables creativity, not limits it) -- Remember: Claude is capable of extraordinary creative work - don't hold back! - -## Version History - -- v2.0.0 (2025-11-22): Creative liberation update - bold aesthetics, shadows/gradients allowed, Design Thinking protocol -- v1.0.0 (2025-10-18): Initial release with comprehensive UI/UX design guidance - -## References - -For additional context, see: -- **Anthropic Frontend Aesthetics Cookbook**: https://github.com/anthropics/claude-cookbooks/blob/main/coding/prompting_for_frontend_aesthetics.ipynb -- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ -- Google Fonts: https://fonts.google.com/ -- Tailwind CSS Docs: https://tailwindcss.com/docs -- Shadcn UI Components: https://ui.shadcn.com/ - -**Progressive Disclosure Files:** -- ACCESSIBILITY.md - Accessibility essentials (WCAG AA baseline) -- MOTION-SPEC.md - Animation timing and easing -- RESPONSIVE-DESIGN.md - Mobile-first breakpoints and patterns diff --git a/.codex/skills/skills/d3-viz/SKILL.md b/.codex/skills/skills/d3-viz/SKILL.md deleted file mode 100644 index 2023442..0000000 --- a/.codex/skills/skills/d3-viz/SKILL.md +++ /dev/null @@ -1,820 +0,0 @@ ---- -name: d3-viz -description: Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment. ---- - -# D3.js Visualisation - -## Overview - -This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks. - -## When to use d3.js - -**Use d3.js for:** -- Custom visualisations requiring unique visual encodings or layouts -- Interactive explorations with complex pan, zoom, or brush behaviours -- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams) -- Geographic visualisations with custom projections -- Visualisations requiring smooth, choreographed transitions -- Publication-quality graphics with fine-grained styling control -- Novel chart types not available in standard libraries - -**Consider alternatives for:** -- 3D visualisations - use Three.js instead - -## Core workflow - -### 1. Set up d3.js - -Import d3 at the top of your script: - -```javascript -import * as d3 from 'd3'; -``` - -Or use the CDN version (7.x): - -```html - -``` - -All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace. - -### 2. Choose the integration pattern - -**Pattern A: Direct DOM manipulation (recommended for most cases)** -Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment: - -```javascript -function drawChart(data) { - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); // Select by ID, class, or DOM element - - // Clear previous content - svg.selectAll("*").remove(); - - // Set up dimensions - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - - // Create scales, axes, and draw visualisation - // ... d3 code here ... -} - -// Call when data changes -drawChart(myData); -``` - -**Pattern B: Declarative rendering (for frameworks with templating)** -Use d3 for data calculations (scales, layouts) but render elements via your framework: - -```javascript -function getChartElements(data) { - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([0, 400]); - - return data.map((d, i) => ({ - x: 50, - y: i * 30, - width: xScale(d.value), - height: 25 - })); -} - -// In React: {getChartElements(data).map((d, i) => )} -// In Vue: v-for directive over the returned array -// In vanilla JS: Create elements manually from the returned data -``` - -Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering. - -### 3. Structure the visualisation code - -Follow this standard structure in your drawing function: - -```javascript -function drawVisualization(data) { - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); // Or pass a selector/element - svg.selectAll("*").remove(); // Clear previous render - - // 1. Define dimensions - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // 2. Create main group with margins - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // 3. Create scales - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); // Note: inverted for SVG coordinates - - // 4. Create and append axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - g.append("g") - .call(yAxis); - - // 5. Bind data and create visual elements - g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 5) - .attr("fill", "steelblue"); -} - -// Call when data changes -drawVisualization(myData); -``` - -### 4. Implement responsive sizing - -Make visualisations responsive to container size: - -```javascript -function setupResponsiveChart(containerId, data) { - const container = document.getElementById(containerId); - const svg = d3.select(`#${containerId}`).append('svg'); - - function updateChart() { - const { width, height } = container.getBoundingClientRect(); - svg.attr('width', width).attr('height', height); - - // Redraw visualisation with new dimensions - drawChart(data, svg, width, height); - } - - // Update on initial load - updateChart(); - - // Update on window resize - window.addEventListener('resize', updateChart); - - // Return cleanup function - return () => window.removeEventListener('resize', updateChart); -} - -// Usage: -// const cleanup = setupResponsiveChart('chart-container', myData); -// cleanup(); // Call when component unmounts or element removed -``` - -Or use ResizeObserver for more direct container monitoring: - -```javascript -function setupResponsiveChartWithObserver(svgElement, data) { - const observer = new ResizeObserver(() => { - const { width, height } = svgElement.getBoundingClientRect(); - d3.select(svgElement) - .attr('width', width) - .attr('height', height); - - // Redraw visualisation - drawChart(data, d3.select(svgElement), width, height); - }); - - observer.observe(svgElement.parentElement); - return () => observer.disconnect(); -} -``` - -## Common visualisation patterns - -### Bar chart - -```javascript -function drawBarChart(data, svgElement) { - if (!data || data.length === 0) return; - - const svg = d3.select(svgElement); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleBand() - .domain(data.map(d => d.category)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.category)) - .attr("y", d => yScale(d.value)) - .attr("width", xScale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", "steelblue"); -} - -// Usage: -// drawBarChart(myData, document.getElementById('chart')); -``` - -### Line chart - -```javascript -const line = d3.line() - .x(d => xScale(d.date)) - .y(d => yScale(d.value)) - .curve(d3.curveMonotoneX); // Smooth curve - -g.append("path") - .datum(data) - .attr("fill", "none") - .attr("stroke", "steelblue") - .attr("stroke-width", 2) - .attr("d", line); -``` - -### Scatter plot - -```javascript -g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size)) // Optional: size encoding - .attr("fill", d => colourScale(d.category)) // Optional: colour encoding - .attr("opacity", 0.7); -``` - -### Chord diagram - -A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them: - -```javascript -function drawChordDiagram(data) { - // data format: array of objects with source, target, and value - // Example: [{ source: 'A', target: 'B', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const innerRadius = Math.min(width, height) * 0.3; - const outerRadius = innerRadius + 30; - - // Create matrix from data - const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); - const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); - - data.forEach(d => { - const i = nodes.indexOf(d.source); - const j = nodes.indexOf(d.target); - matrix[i][j] += d.value; - matrix[j][i] += d.value; - }); - - // Create chord layout - const chord = d3.chord() - .padAngle(0.05) - .sortSubgroups(d3.descending); - - const arc = d3.arc() - .innerRadius(innerRadius) - .outerRadius(outerRadius); - - const ribbon = d3.ribbon() - .source(d => d.source) - .target(d => d.target); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10) - .domain(nodes); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - const chords = chord(matrix); - - // Draw ribbons - g.append("g") - .attr("fill-opacity", 0.67) - .selectAll("path") - .data(chords) - .join("path") - .attr("d", ribbon) - .attr("fill", d => colourScale(nodes[d.source.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); - - // Draw groups (arcs) - const group = g.append("g") - .selectAll("g") - .data(chords.groups) - .join("g"); - - group.append("path") - .attr("d", arc) - .attr("fill", d => colourScale(nodes[d.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); - - // Add labels - group.append("text") - .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) - .attr("dy", "0.31em") - .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) - .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) - .text((d, i) => nodes[i]) - .style("font-size", "12px"); -} -``` - -### Heatmap - -A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories: - -```javascript -function drawHeatmap(data) { - // data format: array of objects with row, column, and value - // Example: [{ row: 'A', column: 'X', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 100, right: 30, bottom: 30, left: 100 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Get unique rows and columns - const rows = Array.from(new Set(data.map(d => d.row))); - const columns = Array.from(new Set(data.map(d => d.column))); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(columns) - .range([0, innerWidth]) - .padding(0.01); - - const yScale = d3.scaleBand() - .domain(rows) - .range([0, innerHeight]) - .padding(0.01); - - // Colour scale for values - const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) - .domain([0, d3.max(data, d => d.value)]); - - // Draw rectangles - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.column)) - .attr("y", d => yScale(d.row)) - .attr("width", xScale.bandwidth()) - .attr("height", yScale.bandwidth()) - .attr("fill", d => colourScale(d.value)); - - // Add x-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(columns) - .join("text") - .attr("x", d => xScale(d) + xScale.bandwidth() / 2) - .attr("y", -10) - .attr("text-anchor", "middle") - .text(d => d) - .style("font-size", "12px"); - - // Add y-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(rows) - .join("text") - .attr("x", -10) - .attr("y", d => yScale(d) + yScale.bandwidth() / 2) - .attr("dy", "0.35em") - .attr("text-anchor", "end") - .text(d => d) - .style("font-size", "12px"); - - // Add colour legend - const legendWidth = 20; - const legendHeight = 200; - const legend = svg.append("g") - .attr("transform", `translate(${width - 60},${margin.top})`); - - const legendScale = d3.scaleLinear() - .domain(colourScale.domain()) - .range([legendHeight, 0]); - - const legendAxis = d3.axisRight(legendScale) - .ticks(5); - - // Draw colour gradient in legend - for (let i = 0; i < legendHeight; i++) { - legend.append("rect") - .attr("y", i) - .attr("width", legendWidth) - .attr("height", 1) - .attr("fill", colourScale(legendScale.invert(i))); - } - - legend.append("g") - .attr("transform", `translate(${legendWidth},0)`) - .call(legendAxis); -} -``` - -### Pie chart - -```javascript -const pie = d3.pie() - .value(d => d.value) - .sort(null); - -const arc = d3.arc() - .innerRadius(0) - .outerRadius(Math.min(width, height) / 2 - 20); - -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - -const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - -g.selectAll("path") - .data(pie(data)) - .join("path") - .attr("d", arc) - .attr("fill", (d, i) => colourScale(i)) - .attr("stroke", "white") - .attr("stroke-width", 2); -``` - -### Force-directed network - -```javascript -const simulation = d3.forceSimulation(nodes) - .force("link", d3.forceLink(links).id(d => d.id).distance(100)) - .force("charge", d3.forceManyBody().strength(-300)) - .force("center", d3.forceCenter(width / 2, height / 2)); - -const link = g.selectAll("line") - .data(links) - .join("line") - .attr("stroke", "#999") - .attr("stroke-width", 1); - -const node = g.selectAll("circle") - .data(nodes) - .join("circle") - .attr("r", 8) - .attr("fill", "steelblue") - .call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended)); - -simulation.on("tick", () => { - link - .attr("x1", d => d.source.x) - .attr("y1", d => d.source.y) - .attr("x2", d => d.target.x) - .attr("y2", d => d.target.y); - - node - .attr("cx", d => d.x) - .attr("cy", d => d.y); -}); - -function dragstarted(event) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - event.subject.fx = event.subject.x; - event.subject.fy = event.subject.y; -} - -function dragged(event) { - event.subject.fx = event.x; - event.subject.fy = event.y; -} - -function dragended(event) { - if (!event.active) simulation.alphaTarget(0); - event.subject.fx = null; - event.subject.fy = null; -} -``` - -## Adding interactivity - -### Tooltips - -```javascript -// Create tooltip div (outside SVG) -const tooltip = d3.select("body").append("div") - .attr("class", "tooltip") - .style("position", "absolute") - .style("visibility", "hidden") - .style("background-color", "white") - .style("border", "1px solid #ddd") - .style("padding", "10px") - .style("border-radius", "4px") - .style("pointer-events", "none"); - -// Add to elements -circles - .on("mouseover", function(event, d) { - d3.select(this).attr("opacity", 1); - tooltip - .style("visibility", "visible") - .html(`${d.label}
Value: ${d.value}`); - }) - .on("mousemove", function(event) { - tooltip - .style("top", (event.pageY - 10) + "px") - .style("left", (event.pageX + 10) + "px"); - }) - .on("mouseout", function() { - d3.select(this).attr("opacity", 0.7); - tooltip.style("visibility", "hidden"); - }); -``` - -### Zoom and pan - -```javascript -const zoom = d3.zoom() - .scaleExtent([0.5, 10]) - .on("zoom", (event) => { - g.attr("transform", event.transform); - }); - -svg.call(zoom); -``` - -### Click interactions - -```javascript -circles - .on("click", function(event, d) { - // Handle click (dispatch event, update app state, etc.) - console.log("Clicked:", d); - - // Visual feedback - d3.selectAll("circle").attr("fill", "steelblue"); - d3.select(this).attr("fill", "orange"); - - // Optional: dispatch custom event for your framework/app to listen to - // window.dispatchEvent(new CustomEvent('chartClick', { detail: d })); - }); -``` - -## Transitions and animations - -Add smooth transitions to visual changes: - -```javascript -// Basic transition -circles - .transition() - .duration(750) - .attr("r", 10); - -// Chained transitions -circles - .transition() - .duration(500) - .attr("fill", "orange") - .transition() - .duration(500) - .attr("r", 15); - -// Staggered transitions -circles - .transition() - .delay((d, i) => i * 50) - .duration(500) - .attr("cy", d => yScale(d.value)); - -// Custom easing -circles - .transition() - .duration(1000) - .ease(d3.easeBounceOut) - .attr("r", 10); -``` - -## Scales reference - -### Quantitative scales - -```javascript -// Linear scale -const xScale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -// Log scale (for exponential data) -const logScale = d3.scaleLog() - .domain([1, 1000]) - .range([0, 500]); - -// Power scale -const powScale = d3.scalePow() - .exponent(2) - .domain([0, 100]) - .range([0, 500]); - -// Time scale -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) - .range([0, 500]); -``` - -### Ordinal scales - -```javascript -// Band scale (for bar charts) -const bandScale = d3.scaleBand() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.1); - -// Point scale (for line/scatter categories) -const pointScale = d3.scalePoint() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]); - -// Ordinal scale (for colours) -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); -``` - -### Sequential scales - -```javascript -// Sequential colour scale -const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, 100]); - -// Diverging colour scale -const divScale = d3.scaleDiverging(d3.interpolateRdBu) - .domain([-10, 0, 10]); -``` - -## Best practices - -### Data preparation - -Always validate and prepare data before visualisation: - -```javascript -// Filter invalid values -const cleanData = data.filter(d => d.value != null && !isNaN(d.value)); - -// Sort data if order matters -const sortedData = [...data].sort((a, b) => b.value - a.value); - -// Parse dates -const parsedData = data.map(d => ({ - ...d, - date: d3.timeParse("%Y-%m-%d")(d.date) -})); -``` - -### Performance optimisation - -For large datasets (>1000 elements): - -```javascript -// Use canvas instead of SVG for many elements -// Use quadtree for collision detection -// Simplify paths with d3.line().curve(d3.curveStep) -// Implement virtual scrolling for large lists -// Use requestAnimationFrame for custom animations -``` - -### Accessibility - -Make visualisations accessible: - -```javascript -// Add ARIA labels -svg.attr("role", "img") - .attr("aria-label", "Bar chart showing quarterly revenue"); - -// Add title and description -svg.append("title").text("Quarterly Revenue 2024"); -svg.append("desc").text("Bar chart showing revenue growth across four quarters"); - -// Ensure sufficient colour contrast -// Provide keyboard navigation for interactive elements -// Include data table alternative -``` - -### Styling - -Use consistent, professional styling: - -```javascript -// Define colour palettes upfront -const colours = { - primary: '#4A90E2', - secondary: '#7B68EE', - background: '#F5F7FA', - text: '#333333', - gridLines: '#E0E0E0' -}; - -// Apply consistent typography -svg.selectAll("text") - .style("font-family", "Inter, sans-serif") - .style("font-size", "12px"); - -// Use subtle grid lines -g.selectAll(".tick line") - .attr("stroke", colours.gridLines) - .attr("stroke-dasharray", "2,2"); -``` - -## Common issues and solutions - -**Issue**: Axes not appearing -- Ensure scales have valid domains (check for NaN values) -- Verify axis is appended to correct group -- Check transform translations are correct - -**Issue**: Transitions not working -- Call `.transition()` before attribute changes -- Ensure elements have unique keys for proper data binding -- Check that useEffect dependencies include all changing data - -**Issue**: Responsive sizing not working -- Use ResizeObserver or window resize listener -- Update dimensions in state to trigger re-render -- Ensure SVG has width/height attributes or viewBox - -**Issue**: Performance problems -- Limit number of DOM elements (consider canvas for >1000 items) -- Debounce resize handlers -- Use `.join()` instead of separate enter/update/exit selections -- Avoid unnecessary re-renders by checking dependencies - -## Resources - -### references/ -Contains detailed reference materials: -- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples -- `scale-reference.md` - Complete guide to d3 scales with examples -- `colour-schemes.md` - D3 colour schemes and palette recommendations - -### assets/ - -Contains boilerplate templates: - -- `chart-template.js` - Starter template for basic chart -- `interactive-template.js` - Template with tooltips, zoom, and interactions -- `sample-data.json` - Example datasets for testing - -These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework. - -To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns. diff --git a/.codex/skills/skills/d3-viz/assets/chart-template.jsx b/.codex/skills/skills/d3-viz/assets/chart-template.jsx deleted file mode 100644 index 64ca0ac..0000000 --- a/.codex/skills/skills/d3-viz/assets/chart-template.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import * as d3 from 'd3'; - -function BasicChart({ data }) { - const svgRef = useRef(); - - useEffect(() => { - if (!data || data.length === 0) return; - - // Select SVG element - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); // Clear previous content - - // Define dimensions and margins - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Create main group with margins - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(data.map(d => d.label)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]) - .nice(); - - // Create and append axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - g.append("g") - .attr("class", "x-axis") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - g.append("g") - .attr("class", "y-axis") - .call(yAxis); - - // Bind data and create visual elements (bars in this example) - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.label)) - .attr("y", d => yScale(d.value)) - .attr("width", xScale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", "steelblue"); - - // Optional: Add axis labels - g.append("text") - .attr("class", "axis-label") - .attr("x", innerWidth / 2) - .attr("y", innerHeight + margin.bottom - 5) - .attr("text-anchor", "middle") - .text("Category"); - - g.append("text") - .attr("class", "axis-label") - .attr("transform", "rotate(-90)") - .attr("x", -innerHeight / 2) - .attr("y", -margin.left + 15) - .attr("text-anchor", "middle") - .text("Value"); - - }, [data]); - - return ( -
- -
- ); -} - -// Example usage -export default function App() { - const sampleData = [ - { label: 'A', value: 30 }, - { label: 'B', value: 80 }, - { label: 'C', value: 45 }, - { label: 'D', value: 60 }, - { label: 'E', value: 20 }, - { label: 'F', value: 90 } - ]; - - return ( -
-

Basic D3.js Chart

- -
- ); -} diff --git a/.codex/skills/skills/d3-viz/assets/interactive-template.jsx b/.codex/skills/skills/d3-viz/assets/interactive-template.jsx deleted file mode 100644 index 31138d5..0000000 --- a/.codex/skills/skills/d3-viz/assets/interactive-template.jsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import * as d3 from 'd3'; - -function InteractiveChart({ data }) { - const svgRef = useRef(); - const tooltipRef = useRef(); - const [selectedPoint, setSelectedPoint] = useState(null); - - useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - // Dimensions - const width = 800; - const height = 500; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Create main group - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Scales - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]) - .nice(); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]) - .nice(); - - const sizeScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.size || 10)]) - .range([3, 20]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - // Add zoom behaviour - const zoom = d3.zoom() - .scaleExtent([0.5, 10]) - .on("zoom", (event) => { - g.attr("transform", `translate(${margin.left + event.transform.x},${margin.top + event.transform.y}) scale(${event.transform.k})`); - }); - - svg.call(zoom); - - // Axes - const xAxis = d3.axisBottom(xScale); - const yAxis = d3.axisLeft(yScale); - - const xAxisGroup = g.append("g") - .attr("class", "x-axis") - .attr("transform", `translate(0,${innerHeight})`) - .call(xAxis); - - const yAxisGroup = g.append("g") - .attr("class", "y-axis") - .call(yAxis); - - // Grid lines - g.append("g") - .attr("class", "grid") - .attr("opacity", 0.1) - .call(d3.axisLeft(yScale) - .tickSize(-innerWidth) - .tickFormat("")); - - g.append("g") - .attr("class", "grid") - .attr("opacity", 0.1) - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale) - .tickSize(-innerHeight) - .tickFormat("")); - - // Tooltip - const tooltip = d3.select(tooltipRef.current); - - // Data points - const circles = g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size || 10)) - .attr("fill", d => colourScale(d.category || 'default')) - .attr("stroke", "#fff") - .attr("stroke-width", 2) - .attr("opacity", 0.7) - .style("cursor", "pointer"); - - // Hover interactions - circles - .on("mouseover", function(event, d) { - // Enlarge circle - d3.select(this) - .transition() - .duration(200) - .attr("opacity", 1) - .attr("stroke-width", 3); - - // Show tooltip - tooltip - .style("display", "block") - .style("left", (event.pageX + 10) + "px") - .style("top", (event.pageY - 10) + "px") - .html(` - ${d.label || 'Point'}
- X: ${d.x.toFixed(2)}
- Y: ${d.y.toFixed(2)}
- ${d.category ? `Category: ${d.category}
` : ''} - ${d.size ? `Size: ${d.size.toFixed(2)}` : ''} - `); - }) - .on("mousemove", function(event) { - tooltip - .style("left", (event.pageX + 10) + "px") - .style("top", (event.pageY - 10) + "px"); - }) - .on("mouseout", function() { - // Restore circle - d3.select(this) - .transition() - .duration(200) - .attr("opacity", 0.7) - .attr("stroke-width", 2); - - // Hide tooltip - tooltip.style("display", "none"); - }) - .on("click", function(event, d) { - // Highlight selected point - circles.attr("stroke", "#fff").attr("stroke-width", 2); - d3.select(this) - .attr("stroke", "#000") - .attr("stroke-width", 3); - - setSelectedPoint(d); - }); - - // Add transition on initial render - circles - .attr("r", 0) - .transition() - .duration(800) - .delay((d, i) => i * 20) - .attr("r", d => sizeScale(d.size || 10)); - - // Axis labels - g.append("text") - .attr("class", "axis-label") - .attr("x", innerWidth / 2) - .attr("y", innerHeight + margin.bottom - 5) - .attr("text-anchor", "middle") - .style("font-size", "14px") - .text("X Axis"); - - g.append("text") - .attr("class", "axis-label") - .attr("transform", "rotate(-90)") - .attr("x", -innerHeight / 2) - .attr("y", -margin.left + 15) - .attr("text-anchor", "middle") - .style("font-size", "14px") - .text("Y Axis"); - - }, [data]); - - return ( -
- -
- {selectedPoint && ( -
-

Selected Point

-
{JSON.stringify(selectedPoint, null, 2)}
-
- )} -
- ); -} - -// Example usage -export default function App() { - const sampleData = Array.from({ length: 50 }, (_, i) => ({ - id: i, - label: `Point ${i + 1}`, - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 30 + 5, - category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)] - })); - - return ( -
-

Interactive D3.js Chart

-

- Hover over points for details. Click to select. Scroll to zoom. Drag to pan. -

- -
- ); -} diff --git a/.codex/skills/skills/d3-viz/assets/sample-data.json b/.codex/skills/skills/d3-viz/assets/sample-data.json deleted file mode 100644 index 1018924..0000000 --- a/.codex/skills/skills/d3-viz/assets/sample-data.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "timeSeries": [ - { "date": "2024-01-01", "value": 120, "category": "A" }, - { "date": "2024-02-01", "value": 135, "category": "A" }, - { "date": "2024-03-01", "value": 128, "category": "A" }, - { "date": "2024-04-01", "value": 145, "category": "A" }, - { "date": "2024-05-01", "value": 152, "category": "A" }, - { "date": "2024-06-01", "value": 168, "category": "A" }, - { "date": "2024-07-01", "value": 175, "category": "A" }, - { "date": "2024-08-01", "value": 182, "category": "A" }, - { "date": "2024-09-01", "value": 190, "category": "A" }, - { "date": "2024-10-01", "value": 185, "category": "A" }, - { "date": "2024-11-01", "value": 195, "category": "A" }, - { "date": "2024-12-01", "value": 210, "category": "A" } - ], - - "categorical": [ - { "label": "Product A", "value": 450, "category": "Electronics" }, - { "label": "Product B", "value": 320, "category": "Electronics" }, - { "label": "Product C", "value": 580, "category": "Clothing" }, - { "label": "Product D", "value": 290, "category": "Clothing" }, - { "label": "Product E", "value": 410, "category": "Food" }, - { "label": "Product F", "value": 370, "category": "Food" } - ], - - "scatterData": [ - { "x": 12, "y": 45, "size": 25, "category": "Group A", "label": "Point 1" }, - { "x": 25, "y": 62, "size": 35, "category": "Group A", "label": "Point 2" }, - { "x": 38, "y": 55, "size": 20, "category": "Group B", "label": "Point 3" }, - { "x": 45, "y": 78, "size": 40, "category": "Group B", "label": "Point 4" }, - { "x": 52, "y": 68, "size": 30, "category": "Group C", "label": "Point 5" }, - { "x": 65, "y": 85, "size": 45, "category": "Group C", "label": "Point 6" }, - { "x": 72, "y": 72, "size": 28, "category": "Group A", "label": "Point 7" }, - { "x": 85, "y": 92, "size": 50, "category": "Group B", "label": "Point 8" } - ], - - "hierarchical": { - "name": "Root", - "children": [ - { - "name": "Category 1", - "children": [ - { "name": "Item 1.1", "value": 100 }, - { "name": "Item 1.2", "value": 150 }, - { "name": "Item 1.3", "value": 80 } - ] - }, - { - "name": "Category 2", - "children": [ - { "name": "Item 2.1", "value": 200 }, - { "name": "Item 2.2", "value": 120 }, - { "name": "Item 2.3", "value": 90 } - ] - }, - { - "name": "Category 3", - "children": [ - { "name": "Item 3.1", "value": 180 }, - { "name": "Item 3.2", "value": 140 } - ] - } - ] - }, - - "network": { - "nodes": [ - { "id": "A", "group": 1 }, - { "id": "B", "group": 1 }, - { "id": "C", "group": 1 }, - { "id": "D", "group": 2 }, - { "id": "E", "group": 2 }, - { "id": "F", "group": 3 }, - { "id": "G", "group": 3 }, - { "id": "H", "group": 3 } - ], - "links": [ - { "source": "A", "target": "B", "value": 1 }, - { "source": "A", "target": "C", "value": 2 }, - { "source": "B", "target": "C", "value": 1 }, - { "source": "C", "target": "D", "value": 3 }, - { "source": "D", "target": "E", "value": 2 }, - { "source": "E", "target": "F", "value": 1 }, - { "source": "F", "target": "G", "value": 2 }, - { "source": "F", "target": "H", "value": 1 }, - { "source": "G", "target": "H", "value": 1 } - ] - }, - - "stackedData": [ - { "group": "Q1", "seriesA": 30, "seriesB": 40, "seriesC": 25 }, - { "group": "Q2", "seriesA": 45, "seriesB": 35, "seriesC": 30 }, - { "group": "Q3", "seriesA": 40, "seriesB": 50, "seriesC": 35 }, - { "group": "Q4", "seriesA": 55, "seriesB": 45, "seriesC": 40 } - ], - - "geographicPoints": [ - { "city": "London", "latitude": 51.5074, "longitude": -0.1278, "value": 8900000 }, - { "city": "Paris", "latitude": 48.8566, "longitude": 2.3522, "value": 2140000 }, - { "city": "Berlin", "latitude": 52.5200, "longitude": 13.4050, "value": 3645000 }, - { "city": "Madrid", "latitude": 40.4168, "longitude": -3.7038, "value": 3223000 }, - { "city": "Rome", "latitude": 41.9028, "longitude": 12.4964, "value": 2873000 } - ], - - "divergingData": [ - { "category": "Item A", "value": -15 }, - { "category": "Item B", "value": 8 }, - { "category": "Item C", "value": -22 }, - { "category": "Item D", "value": 18 }, - { "category": "Item E", "value": -5 }, - { "category": "Item F", "value": 25 }, - { "category": "Item G", "value": -12 }, - { "category": "Item H", "value": 14 } - ] -} diff --git a/.codex/skills/skills/d3-viz/references/colour-schemes.md b/.codex/skills/skills/d3-viz/references/colour-schemes.md deleted file mode 100644 index 12394e9..0000000 --- a/.codex/skills/skills/d3-viz/references/colour-schemes.md +++ /dev/null @@ -1,564 +0,0 @@ -# D3.js Colour Schemes and Palette Recommendations - -Comprehensive guide to colour selection in data visualisation with d3.js. - -## Built-in categorical colour schemes - -### Category10 (default) - -```javascript -d3.schemeCategory10 -// ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', -// '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] -``` - -**Characteristics:** -- 10 distinct colours -- Good colour-blind accessibility -- Default choice for most categorical data -- Balanced saturation and brightness - -**Use cases:** General purpose categorical encoding, legend items, multiple data series - -### Tableau10 - -```javascript -d3.schemeTableau10 -``` - -**Characteristics:** -- 10 colours optimised for data visualisation -- Professional appearance -- Excellent distinguishability - -**Use cases:** Business dashboards, professional reports, presentations - -### Accent - -```javascript -d3.schemeAccent -// 8 colours with high saturation -``` - -**Characteristics:** -- Bright, vibrant colours -- High contrast -- Modern aesthetic - -**Use cases:** Highlighting important categories, modern web applications - -### Dark2 - -```javascript -d3.schemeDark2 -// 8 darker, muted colours -``` - -**Characteristics:** -- Subdued palette -- Professional appearance -- Good for dark backgrounds - -**Use cases:** Dark mode visualisations, professional contexts - -### Paired - -```javascript -d3.schemePaired -// 12 colours in pairs of similar hues -``` - -**Characteristics:** -- Pairs of light and dark variants -- Useful for nested categories -- 12 distinct colours - -**Use cases:** Grouped bar charts, hierarchical categories, before/after comparisons - -### Pastel1 & Pastel2 - -```javascript -d3.schemePastel1 // 9 colours -d3.schemePastel2 // 8 colours -``` - -**Characteristics:** -- Soft, low-saturation colours -- Gentle appearance -- Good for large areas - -**Use cases:** Background colours, subtle categorisation, calming visualisations - -### Set1, Set2, Set3 - -```javascript -d3.schemeSet1 // 9 colours - vivid -d3.schemeSet2 // 8 colours - muted -d3.schemeSet3 // 12 colours - pastel -``` - -**Characteristics:** -- Set1: High saturation, maximum distinction -- Set2: Professional, balanced -- Set3: Subtle, many categories - -**Use cases:** Varied based on visual hierarchy needs - -## Sequential colour schemes - -Sequential schemes map continuous data from low to high values using a single hue or gradient. - -### Single-hue sequential - -**Blues:** -```javascript -d3.interpolateBlues -d3.schemeBlues[9] // 9-step discrete version -``` - -**Other single-hue options:** -- `d3.interpolateGreens` / `d3.schemeGreens` -- `d3.interpolateOranges` / `d3.schemeOranges` -- `d3.interpolatePurples` / `d3.schemePurples` -- `d3.interpolateReds` / `d3.schemeReds` -- `d3.interpolateGreys` / `d3.schemeGreys` - -**Use cases:** -- Simple heat maps -- Choropleth maps -- Density plots -- Single-metric visualisations - -### Multi-hue sequential - -**Viridis (recommended):** -```javascript -d3.interpolateViridis -``` - -**Characteristics:** -- Perceptually uniform -- Colour-blind friendly -- Print-safe -- No visual dead zones -- Monotonically increasing perceived lightness - -**Other perceptually-uniform options:** -- `d3.interpolatePlasma` - Purple to yellow -- `d3.interpolateInferno` - Black to white through red/orange -- `d3.interpolateMagma` - Black to white through purple -- `d3.interpolateCividis` - Colour-blind optimised - -**Colour-blind accessible:** -```javascript -d3.interpolateTurbo // Rainbow-like but perceptually uniform -d3.interpolateCool // Cyan to magenta -d3.interpolateWarm // Orange to yellow -``` - -**Use cases:** -- Scientific visualisation -- Medical imaging -- Any high-precision data visualisation -- Accessible visualisations - -### Traditional sequential - -**Yellow-Orange-Red:** -```javascript -d3.interpolateYlOrRd -d3.schemeYlOrRd[9] -``` - -**Yellow-Green-Blue:** -```javascript -d3.interpolateYlGnBu -d3.schemeYlGnBu[9] -``` - -**Other multi-hue:** -- `d3.interpolateBuGn` - Blue to green -- `d3.interpolateBuPu` - Blue to purple -- `d3.interpolateGnBu` - Green to blue -- `d3.interpolateOrRd` - Orange to red -- `d3.interpolatePuBu` - Purple to blue -- `d3.interpolatePuBuGn` - Purple to blue-green -- `d3.interpolatePuRd` - Purple to red -- `d3.interpolateRdPu` - Red to purple -- `d3.interpolateYlGn` - Yellow to green -- `d3.interpolateYlOrBr` - Yellow to orange-brown - -**Use cases:** Traditional data visualisation, familiar colour associations (temperature, vegetation, water) - -## Diverging colour schemes - -Diverging schemes highlight deviations from a central value using two distinct hues. - -### Red-Blue (temperature) - -```javascript -d3.interpolateRdBu -d3.schemeRdBu[11] -``` - -**Characteristics:** -- Intuitive temperature metaphor -- Strong contrast -- Clear positive/negative distinction - -**Use cases:** Temperature, profit/loss, above/below average, correlation - -### Red-Yellow-Blue - -```javascript -d3.interpolateRdYlBu -d3.schemeRdYlBu[11] -``` - -**Characteristics:** -- Three-colour gradient -- Softer transition through yellow -- More visual steps - -**Use cases:** When extreme values need emphasis and middle needs visibility - -### Other diverging schemes - -**Traffic light:** -```javascript -d3.interpolateRdYlGn // Red (bad) to green (good) -``` - -**Spectral (rainbow):** -```javascript -d3.interpolateSpectral // Full spectrum -``` - -**Other options:** -- `d3.interpolateBrBG` - Brown to blue-green -- `d3.interpolatePiYG` - Pink to yellow-green -- `d3.interpolatePRGn` - Purple to green -- `d3.interpolatePuOr` - Purple to orange -- `d3.interpolateRdGy` - Red to grey - -**Use cases:** Choose based on semantic meaning and accessibility needs - -## Colour-blind friendly palettes - -### General guidelines - -1. **Avoid red-green combinations** (most common colour blindness) -2. **Use blue-orange diverging** instead of red-green -3. **Add texture or patterns** as redundant encoding -4. **Test with simulation tools** - -### Recommended colour-blind safe schemes - -**Categorical:** -```javascript -// Okabe-Ito palette (colour-blind safe) -const okabePalette = [ - '#E69F00', // Orange - '#56B4E9', // Sky blue - '#009E73', // Bluish green - '#F0E442', // Yellow - '#0072B2', // Blue - '#D55E00', // Vermillion - '#CC79A7', // Reddish purple - '#000000' // Black -]; - -const colourScale = d3.scaleOrdinal() - .domain(categories) - .range(okabePalette); -``` - -**Sequential:** -```javascript -// Use Viridis, Cividis, or Blues -d3.interpolateViridis // Best overall -d3.interpolateCividis // Optimised for CVD -d3.interpolateBlues // Simple, safe -``` - -**Diverging:** -```javascript -// Use blue-orange instead of red-green -d3.interpolateBrBG -d3.interpolatePuOr -``` - -## Custom colour palettes - -### Creating custom sequential - -```javascript -const customSequential = d3.scaleLinear() - .domain([0, 100]) - .range(['#e8f4f8', '#006d9c']) // Light to dark blue - .interpolate(d3.interpolateLab); // Perceptually uniform -``` - -### Creating custom diverging - -```javascript -const customDiverging = d3.scaleLinear() - .domain([0, 50, 100]) - .range(['#ca0020', '#f7f7f7', '#0571b0']) // Red, grey, blue - .interpolate(d3.interpolateLab); -``` - -### Creating custom categorical - -```javascript -// Brand colours -const brandPalette = [ - '#FF6B6B', // Primary red - '#4ECDC4', // Secondary teal - '#45B7D1', // Tertiary blue - '#FFA07A', // Accent coral - '#98D8C8' // Accent mint -]; - -const colourScale = d3.scaleOrdinal() - .domain(categories) - .range(brandPalette); -``` - -## Semantic colour associations - -### Universal colour meanings - -**Red:** -- Danger, error, negative -- High temperature -- Debt, loss - -**Green:** -- Success, positive -- Growth, vegetation -- Profit, gain - -**Blue:** -- Trust, calm -- Water, cold -- Information, neutral - -**Yellow/Orange:** -- Warning, caution -- Energy, warmth -- Attention - -**Grey:** -- Neutral, inactive -- Missing data -- Background - -### Context-specific palettes - -**Financial:** -```javascript -const financialColours = { - profit: '#27ae60', - loss: '#e74c3c', - neutral: '#95a5a6', - highlight: '#3498db' -}; -``` - -**Temperature:** -```javascript -const temperatureScale = d3.scaleSequential(d3.interpolateRdYlBu) - .domain([40, -10]); // Hot to cold (reversed) -``` - -**Traffic/Status:** -```javascript -const statusColours = { - success: '#27ae60', - warning: '#f39c12', - error: '#e74c3c', - info: '#3498db', - neutral: '#95a5a6' -}; -``` - -## Accessibility best practices - -### Contrast ratios - -Ensure sufficient contrast between colours and backgrounds: - -```javascript -// Good contrast example -const highContrast = { - background: '#ffffff', - text: '#2c3e50', - primary: '#3498db', - secondary: '#e74c3c' -}; -``` - -**WCAG guidelines:** -- Normal text: 4.5:1 minimum -- Large text: 3:1 minimum -- UI components: 3:1 minimum - -### Redundant encoding - -Never rely solely on colour to convey information: - -```javascript -// Add patterns or shapes -const symbols = ['circle', 'square', 'triangle', 'diamond']; - -// Add text labels -// Use line styles (solid, dashed, dotted) -// Use size encoding -``` - -### Testing - -Test visualisations for colour blindness: -- Chrome DevTools (Rendering > Emulate vision deficiencies) -- Colour Oracle (free desktop application) -- Coblis (online simulator) - -## Professional colour recommendations - -### Data journalism - -```javascript -// Guardian style -const guardianPalette = [ - '#005689', // Guardian blue - '#c70000', // Guardian red - '#7d0068', // Guardian pink - '#951c75', // Guardian purple -]; - -// FT style -const ftPalette = [ - '#0f5499', // FT blue - '#990f3d', // FT red - '#593380', // FT purple - '#262a33', // FT black -]; -``` - -### Academic/Scientific - -```javascript -// Nature journal style -const naturePalette = [ - '#0071b2', // Blue - '#d55e00', // Vermillion - '#009e73', // Green - '#f0e442', // Yellow -]; - -// Use Viridis for continuous data -const scientificScale = d3.scaleSequential(d3.interpolateViridis); -``` - -### Corporate/Business - -```javascript -// Professional, conservative -const corporatePalette = [ - '#003f5c', // Dark blue - '#58508d', // Purple - '#bc5090', // Magenta - '#ff6361', // Coral - '#ffa600' // Orange -]; -``` - -## Dynamic colour selection - -### Based on data range - -```javascript -function selectColourScheme(data) { - const extent = d3.extent(data); - const hasNegative = extent[0] < 0; - const hasPositive = extent[1] > 0; - - if (hasNegative && hasPositive) { - // Diverging: data crosses zero - return d3.scaleSequentialSymlog(d3.interpolateRdBu) - .domain([extent[0], 0, extent[1]]); - } else { - // Sequential: all positive or all negative - return d3.scaleSequential(d3.interpolateViridis) - .domain(extent); - } -} -``` - -### Based on category count - -```javascript -function selectCategoricalScheme(categories) { - const n = categories.length; - - if (n <= 10) { - return d3.scaleOrdinal(d3.schemeTableau10); - } else if (n <= 12) { - return d3.scaleOrdinal(d3.schemePaired); - } else { - // For many categories, use sequential with quantize - return d3.scaleQuantize() - .domain([0, n - 1]) - .range(d3.quantize(d3.interpolateRainbow, n)); - } -} -``` - -## Common colour mistakes to avoid - -1. **Rainbow gradients for sequential data** - - Problem: Not perceptually uniform, hard to read - - Solution: Use Viridis, Blues, or other uniform schemes - -2. **Red-green for diverging (colour blindness)** - - Problem: 8% of males can't distinguish - - Solution: Use blue-orange or purple-green - -3. **Too many categorical colours** - - Problem: Hard to distinguish and remember - - Solution: Limit to 5-8 categories, use grouping - -4. **Insufficient contrast** - - Problem: Poor readability - - Solution: Test contrast ratios, use darker colours on light backgrounds - -5. **Culturally inconsistent colours** - - Problem: Confusing semantic meaning - - Solution: Research colour associations for target audience - -6. **Inverted temperature scales** - - Problem: Counterintuitive (red = cold) - - Solution: Red/orange = hot, blue = cold - -## Quick reference guide - -**Need to show...** - -- **Categories (≤10):** `d3.schemeCategory10` or `d3.schemeTableau10` -- **Categories (>10):** `d3.schemePaired` or group categories -- **Sequential (general):** `d3.interpolateViridis` -- **Sequential (scientific):** `d3.interpolateViridis` or `d3.interpolatePlasma` -- **Sequential (temperature):** `d3.interpolateRdYlBu` (inverted) -- **Diverging (zero):** `d3.interpolateRdBu` or `d3.interpolateBrBG` -- **Diverging (good/bad):** `d3.interpolateRdYlGn` (inverted) -- **Colour-blind safe (categorical):** Okabe-Ito palette (shown above) -- **Colour-blind safe (sequential):** `d3.interpolateCividis` or `d3.interpolateBlues` -- **Colour-blind safe (diverging):** `d3.interpolatePuOr` or `d3.interpolateBrBG` - -**Always remember:** -1. Test for colour-blindness -2. Ensure sufficient contrast -3. Use semantic colours appropriately -4. Add redundant encoding (patterns, labels) -5. Keep it simple (fewer colours = clearer visualisation) \ No newline at end of file diff --git a/.codex/skills/skills/d3-viz/references/d3-patterns.md b/.codex/skills/skills/d3-viz/references/d3-patterns.md deleted file mode 100644 index 0b36a0b..0000000 --- a/.codex/skills/skills/d3-viz/references/d3-patterns.md +++ /dev/null @@ -1,869 +0,0 @@ -# D3.js Visualisation Patterns - -This reference provides detailed code patterns for common d3.js visualisation types. - -## Hierarchical visualisations - -### Tree diagram - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const tree = d3.tree().size([height - 100, width - 200]); - - const root = d3.hierarchy(data); - tree(root); - - const g = svg.append("g") - .attr("transform", "translate(100,50)"); - - // Links - g.selectAll("path") - .data(root.links()) - .join("path") - .attr("d", d3.linkHorizontal() - .x(d => d.y) - .y(d => d.x)) - .attr("fill", "none") - .attr("stroke", "#555") - .attr("stroke-width", 2); - - // Nodes - const node = g.selectAll("g") - .data(root.descendants()) - .join("g") - .attr("transform", d => `translate(${d.y},${d.x})`); - - node.append("circle") - .attr("r", 6) - .attr("fill", d => d.children ? "#555" : "#999"); - - node.append("text") - .attr("dy", "0.31em") - .attr("x", d => d.children ? -8 : 8) - .attr("text-anchor", d => d.children ? "end" : "start") - .text(d => d.data.name) - .style("font-size", "12px"); - -}, [data]); -``` - -### Treemap - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const root = d3.hierarchy(data) - .sum(d => d.value) - .sort((a, b) => b.value - a.value); - - d3.treemap() - .size([width, height]) - .padding(2) - .round(true)(root); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const cell = svg.selectAll("g") - .data(root.leaves()) - .join("g") - .attr("transform", d => `translate(${d.x0},${d.y0})`); - - cell.append("rect") - .attr("width", d => d.x1 - d.x0) - .attr("height", d => d.y1 - d.y0) - .attr("fill", d => colourScale(d.parent.data.name)) - .attr("stroke", "white") - .attr("stroke-width", 2); - - cell.append("text") - .attr("x", 4) - .attr("y", 16) - .text(d => d.data.name) - .style("font-size", "12px") - .style("fill", "white"); - -}, [data]); -``` - -### Sunburst diagram - -```javascript -useEffect(() => { - if (!data) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const radius = Math.min(width, height) / 2; - - const root = d3.hierarchy(data) - .sum(d => d.value) - .sort((a, b) => b.value - a.value); - - const partition = d3.partition() - .size([2 * Math.PI, radius]); - - partition(root); - - const arc = d3.arc() - .startAngle(d => d.x0) - .endAngle(d => d.x1) - .innerRadius(d => d.y0) - .outerRadius(d => d.y1); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - g.selectAll("path") - .data(root.descendants()) - .join("path") - .attr("d", arc) - .attr("fill", d => colourScale(d.depth)) - .attr("stroke", "white") - .attr("stroke-width", 1); - -}, [data]); -``` - -### Chord diagram - -```javascript -function drawChordDiagram(data) { - // data format: array of objects with source, target, and value - // Example: [{ source: 'A', target: 'B', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 600; - const height = 600; - const innerRadius = Math.min(width, height) * 0.3; - const outerRadius = innerRadius + 30; - - // Create matrix from data - const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); - const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); - - data.forEach(d => { - const i = nodes.indexOf(d.source); - const j = nodes.indexOf(d.target); - matrix[i][j] += d.value; - matrix[j][i] += d.value; - }); - - // Create chord layout - const chord = d3.chord() - .padAngle(0.05) - .sortSubgroups(d3.descending); - - const arc = d3.arc() - .innerRadius(innerRadius) - .outerRadius(outerRadius); - - const ribbon = d3.ribbon() - .source(d => d.source) - .target(d => d.target); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10) - .domain(nodes); - - const g = svg.append("g") - .attr("transform", `translate(${width / 2},${height / 2})`); - - const chords = chord(matrix); - - // Draw ribbons - g.append("g") - .attr("fill-opacity", 0.67) - .selectAll("path") - .data(chords) - .join("path") - .attr("d", ribbon) - .attr("fill", d => colourScale(nodes[d.source.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); - - // Draw groups (arcs) - const group = g.append("g") - .selectAll("g") - .data(chords.groups) - .join("g"); - - group.append("path") - .attr("d", arc) - .attr("fill", d => colourScale(nodes[d.index])) - .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); - - // Add labels - group.append("text") - .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) - .attr("dy", "0.31em") - .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) - .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) - .text((d, i) => nodes[i]) - .style("font-size", "12px"); -} - -// Data format example: -// const data = [ -// { source: 'Category A', target: 'Category B', value: 100 }, -// { source: 'Category A', target: 'Category C', value: 50 }, -// { source: 'Category B', target: 'Category C', value: 75 } -// ]; -// drawChordDiagram(data); -``` - -## Advanced chart types - -### Heatmap - -```javascript -function drawHeatmap(data) { - // data format: array of objects with row, column, and value - // Example: [{ row: 'A', column: 'X', value: 10 }, ...] - - if (!data || data.length === 0) return; - - const svg = d3.select('#chart'); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 100, right: 30, bottom: 30, left: 100 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Get unique rows and columns - const rows = Array.from(new Set(data.map(d => d.row))); - const columns = Array.from(new Set(data.map(d => d.column))); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Create scales - const xScale = d3.scaleBand() - .domain(columns) - .range([0, innerWidth]) - .padding(0.01); - - const yScale = d3.scaleBand() - .domain(rows) - .range([0, innerHeight]) - .padding(0.01); - - // Colour scale for values (sequential from light to dark red) - const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) - .domain([0, d3.max(data, d => d.value)]); - - // Draw rectangles - g.selectAll("rect") - .data(data) - .join("rect") - .attr("x", d => xScale(d.column)) - .attr("y", d => yScale(d.row)) - .attr("width", xScale.bandwidth()) - .attr("height", yScale.bandwidth()) - .attr("fill", d => colourScale(d.value)); - - // Add x-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(columns) - .join("text") - .attr("x", d => xScale(d) + xScale.bandwidth() / 2) - .attr("y", -10) - .attr("text-anchor", "middle") - .text(d => d) - .style("font-size", "12px"); - - // Add y-axis labels - svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`) - .selectAll("text") - .data(rows) - .join("text") - .attr("x", -10) - .attr("y", d => yScale(d) + yScale.bandwidth() / 2) - .attr("dy", "0.35em") - .attr("text-anchor", "end") - .text(d => d) - .style("font-size", "12px"); - - // Add colour legend - const legendWidth = 20; - const legendHeight = 200; - const legend = svg.append("g") - .attr("transform", `translate(${width - 60},${margin.top})`); - - const legendScale = d3.scaleLinear() - .domain(colourScale.domain()) - .range([legendHeight, 0]); - - const legendAxis = d3.axisRight(legendScale).ticks(5); - - // Draw colour gradient in legend - for (let i = 0; i < legendHeight; i++) { - legend.append("rect") - .attr("y", i) - .attr("width", legendWidth) - .attr("height", 1) - .attr("fill", colourScale(legendScale.invert(i))); - } - - legend.append("g") - .attr("transform", `translate(${legendWidth},0)`) - .call(legendAxis); -} - -// Data format example: -// const data = [ -// { row: 'Monday', column: 'Morning', value: 42 }, -// { row: 'Monday', column: 'Afternoon', value: 78 }, -// { row: 'Tuesday', column: 'Morning', value: 65 }, -// { row: 'Tuesday', column: 'Afternoon', value: 55 } -// ]; -// drawHeatmap(data); -``` - -### Area chart with gradient - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Define gradient - const defs = svg.append("defs"); - const gradient = defs.append("linearGradient") - .attr("id", "areaGradient") - .attr("x1", "0%") - .attr("x2", "0%") - .attr("y1", "0%") - .attr("y2", "100%"); - - gradient.append("stop") - .attr("offset", "0%") - .attr("stop-color", "steelblue") - .attr("stop-opacity", 0.8); - - gradient.append("stop") - .attr("offset", "100%") - .attr("stop-color", "steelblue") - .attr("stop-opacity", 0.1); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleTime() - .domain(d3.extent(data, d => d.date)) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.value)]) - .range([innerHeight, 0]); - - const area = d3.area() - .x(d => xScale(d.date)) - .y0(innerHeight) - .y1(d => yScale(d.value)) - .curve(d3.curveMonotoneX); - - g.append("path") - .datum(data) - .attr("fill", "url(#areaGradient)") - .attr("d", area); - - const line = d3.line() - .x(d => xScale(d.date)) - .y(d => yScale(d.value)) - .curve(d3.curveMonotoneX); - - g.append("path") - .datum(data) - .attr("fill", "none") - .attr("stroke", "steelblue") - .attr("stroke-width", 2) - .attr("d", line); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Stacked bar chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const categories = Object.keys(data[0]).filter(k => k !== 'group'); - const stackedData = d3.stack().keys(categories)(data); - - const xScale = d3.scaleBand() - .domain(data.map(d => d.group)) - .range([0, innerWidth]) - .padding(0.1); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])]) - .range([innerHeight, 0]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - g.selectAll("g") - .data(stackedData) - .join("g") - .attr("fill", (d, i) => colourScale(i)) - .selectAll("rect") - .data(d => d) - .join("rect") - .attr("x", d => xScale(d.data.group)) - .attr("y", d => yScale(d[1])) - .attr("height", d => yScale(d[0]) - yScale(d[1])) - .attr("width", xScale.bandwidth()); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Grouped bar chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const categories = Object.keys(data[0]).filter(k => k !== 'group'); - - const x0Scale = d3.scaleBand() - .domain(data.map(d => d.group)) - .range([0, innerWidth]) - .padding(0.1); - - const x1Scale = d3.scaleBand() - .domain(categories) - .range([0, x0Scale.bandwidth()]) - .padding(0.05); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => Math.max(...categories.map(c => d[c])))]) - .range([innerHeight, 0]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - const group = g.selectAll("g") - .data(data) - .join("g") - .attr("transform", d => `translate(${x0Scale(d.group)},0)`); - - group.selectAll("rect") - .data(d => categories.map(key => ({ key, value: d[key] }))) - .join("rect") - .attr("x", d => x1Scale(d.key)) - .attr("y", d => yScale(d.value)) - .attr("width", x1Scale.bandwidth()) - .attr("height", d => innerHeight - yScale(d.value)) - .attr("fill", d => colourScale(d.key)); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(x0Scale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -### Bubble chart - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); - - const sizeScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.size)]) - .range([0, 50]); - - const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - - g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", d => sizeScale(d.size)) - .attr("fill", d => colourScale(d.category)) - .attr("opacity", 0.6) - .attr("stroke", "white") - .attr("stroke-width", 2); - - g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(xScale)); - - g.append("g") - .call(d3.axisLeft(yScale)); - -}, [data]); -``` - -## Geographic visualisations - -### Basic map with points - -```javascript -useEffect(() => { - if (!geoData || !pointData) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const projection = d3.geoMercator() - .fitSize([width, height], geoData); - - const pathGenerator = d3.geoPath().projection(projection); - - // Draw map - svg.selectAll("path") - .data(geoData.features) - .join("path") - .attr("d", pathGenerator) - .attr("fill", "#e0e0e0") - .attr("stroke", "#999") - .attr("stroke-width", 0.5); - - // Draw points - svg.selectAll("circle") - .data(pointData) - .join("circle") - .attr("cx", d => projection([d.longitude, d.latitude])[0]) - .attr("cy", d => projection([d.longitude, d.latitude])[1]) - .attr("r", 5) - .attr("fill", "steelblue") - .attr("opacity", 0.7); - -}, [geoData, pointData]); -``` - -### Choropleth map - -```javascript -useEffect(() => { - if (!geoData || !valueData) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 600; - - const projection = d3.geoMercator() - .fitSize([width, height], geoData); - - const pathGenerator = d3.geoPath().projection(projection); - - // Create value lookup - const valueLookup = new Map(valueData.map(d => [d.id, d.value])); - - // Colour scale - const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, d3.max(valueData, d => d.value)]); - - svg.selectAll("path") - .data(geoData.features) - .join("path") - .attr("d", pathGenerator) - .attr("fill", d => { - const value = valueLookup.get(d.id); - return value ? colourScale(value) : "#e0e0e0"; - }) - .attr("stroke", "#999") - .attr("stroke-width", 0.5); - -}, [geoData, valueData]); -``` - -## Advanced interactions - -### Brush and zoom - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - const width = 800; - const height = 400; - const margin = { top: 20, right: 30, bottom: 40, left: 50 }; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - const xScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.x)]) - .range([0, innerWidth]); - - const yScale = d3.scaleLinear() - .domain([0, d3.max(data, d => d.y)]) - .range([innerHeight, 0]); - - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - const circles = g.selectAll("circle") - .data(data) - .join("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 5) - .attr("fill", "steelblue"); - - // Add brush - const brush = d3.brush() - .extent([[0, 0], [innerWidth, innerHeight]]) - .on("start brush", (event) => { - if (!event.selection) return; - - const [[x0, y0], [x1, y1]] = event.selection; - - circles.attr("fill", d => { - const cx = xScale(d.x); - const cy = yScale(d.y); - return (cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1) - ? "orange" - : "steelblue"; - }); - }); - - g.append("g") - .attr("class", "brush") - .call(brush); - -}, [data]); -``` - -### Linked brushing between charts - -```javascript -function LinkedCharts({ data }) { - const [selectedPoints, setSelectedPoints] = useState(new Set()); - const svg1Ref = useRef(); - const svg2Ref = useRef(); - - useEffect(() => { - // Chart 1: Scatter plot - const svg1 = d3.select(svg1Ref.current); - svg1.selectAll("*").remove(); - - // ... create first chart ... - - const circles1 = svg1.selectAll("circle") - .data(data) - .join("circle") - .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); - - // Chart 2: Bar chart - const svg2 = d3.select(svg2Ref.current); - svg2.selectAll("*").remove(); - - // ... create second chart ... - - const bars = svg2.selectAll("rect") - .data(data) - .join("rect") - .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); - - // Add brush to first chart - const brush = d3.brush() - .on("start brush end", (event) => { - if (!event.selection) { - setSelectedPoints(new Set()); - return; - } - - const [[x0, y0], [x1, y1]] = event.selection; - const selected = new Set(); - - data.forEach(d => { - const x = xScale(d.x); - const y = yScale(d.y); - if (x >= x0 && x <= x1 && y >= y0 && y <= y1) { - selected.add(d.id); - } - }); - - setSelectedPoints(selected); - }); - - svg1.append("g").call(brush); - - }, [data, selectedPoints]); - - return ( -
- - -
- ); -} -``` - -## Animation patterns - -### Enter, update, exit with transitions - -```javascript -useEffect(() => { - if (!data || data.length === 0) return; - - const svg = d3.select(svgRef.current); - - const circles = svg.selectAll("circle") - .data(data, d => d.id); // Key function for object constancy - - // EXIT: Remove old elements - circles.exit() - .transition() - .duration(500) - .attr("r", 0) - .remove(); - - // UPDATE: Modify existing elements - circles - .transition() - .duration(500) - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("fill", "steelblue"); - - // ENTER: Add new elements - circles.enter() - .append("circle") - .attr("cx", d => xScale(d.x)) - .attr("cy", d => yScale(d.y)) - .attr("r", 0) - .attr("fill", "steelblue") - .transition() - .duration(500) - .attr("r", 5); - -}, [data]); -``` - -### Path morphing - -```javascript -useEffect(() => { - if (!data1 || !data2) return; - - const svg = d3.select(svgRef.current); - - const line = d3.line() - .x(d => xScale(d.x)) - .y(d => yScale(d.y)) - .curve(d3.curveMonotoneX); - - const path = svg.select("path"); - - // Morph from data1 to data2 - path - .datum(data1) - .attr("d", line) - .transition() - .duration(1000) - .attrTween("d", function() { - const previous = d3.select(this).attr("d"); - const current = line(data2); - return d3.interpolatePath(previous, current); - }); - -}, [data1, data2]); -``` \ No newline at end of file diff --git a/.codex/skills/skills/d3-viz/references/scale-reference.md b/.codex/skills/skills/d3-viz/references/scale-reference.md deleted file mode 100644 index 61bd981..0000000 --- a/.codex/skills/skills/d3-viz/references/scale-reference.md +++ /dev/null @@ -1,509 +0,0 @@ -# D3.js Scale Reference - -Comprehensive guide to all d3 scale types with examples and use cases. - -## Continuous scales - -### Linear scale - -Maps continuous input domain to continuous output range with linear interpolation. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -scale(50); // Returns 250 -scale(0); // Returns 0 -scale(100); // Returns 500 - -// Invert scale (get input from output) -scale.invert(250); // Returns 50 -``` - -**Use cases:** -- Most common scale for quantitative data -- Axes, bar lengths, position encoding -- Temperature, prices, counts, measurements - -**Methods:** -- `.domain([min, max])` - Set input domain -- `.range([min, max])` - Set output range -- `.invert(value)` - Get domain value from range value -- `.clamp(true)` - Restrict output to range bounds -- `.nice()` - Extend domain to nice round values - -### Power scale - -Maps continuous input to continuous output with exponential transformation. - -```javascript -const sqrtScale = d3.scalePow() - .exponent(0.5) // Square root - .domain([0, 100]) - .range([0, 500]); - -const squareScale = d3.scalePow() - .exponent(2) // Square - .domain([0, 100]) - .range([0, 500]); - -// Shorthand for square root -const sqrtScale2 = d3.scaleSqrt() - .domain([0, 100]) - .range([0, 500]); -``` - -**Use cases:** -- Perceptual scaling (human perception is non-linear) -- Area encoding (use square root to map values to circle radii) -- Emphasising differences in small or large values - -### Logarithmic scale - -Maps continuous input to continuous output with logarithmic transformation. - -```javascript -const logScale = d3.scaleLog() - .domain([1, 1000]) // Must be positive - .range([0, 500]); - -logScale(1); // Returns 0 -logScale(10); // Returns ~167 -logScale(100); // Returns ~333 -logScale(1000); // Returns 500 -``` - -**Use cases:** -- Data spanning multiple orders of magnitude -- Population, GDP, wealth distributions -- Logarithmic axes -- Exponential growth visualisations - -**Important:** Domain values must be strictly positive (>0). - -### Time scale - -Specialised linear scale for temporal data. - -```javascript -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) - .range([0, 800]); - -timeScale(new Date(2022, 0, 1)); // Returns 400 - -// Invert to get date -timeScale.invert(400); // Returns Date object for mid-2022 -``` - -**Use cases:** -- Time series visualisations -- Timeline axes -- Temporal animations -- Date-based interactions - -**Methods:** -- `.nice()` - Extend domain to nice time intervals -- `.ticks(count)` - Generate nicely-spaced tick values -- All linear scale methods apply - -### Quantize scale - -Maps continuous input to discrete output buckets. - -```javascript -const quantizeScale = d3.scaleQuantize() - .domain([0, 100]) - .range(['low', 'medium', 'high']); - -quantizeScale(25); // Returns 'low' -quantizeScale(50); // Returns 'medium' -quantizeScale(75); // Returns 'high' - -// Get the threshold values -quantizeScale.thresholds(); // Returns [33.33, 66.67] -``` - -**Use cases:** -- Binning continuous data -- Heat map colours -- Risk categories (low/medium/high) -- Age groups, income brackets - -### Quantile scale - -Maps continuous input to discrete output based on quantiles. - -```javascript -const quantileScale = d3.scaleQuantile() - .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // Sample data - .range(['low', 'medium', 'high']); - -quantileScale(8); // Returns based on quantile position -quantileScale.quantiles(); // Returns quantile thresholds -``` - -**Use cases:** -- Equal-size groups regardless of distribution -- Percentile-based categorisation -- Handling skewed distributions - -### Threshold scale - -Maps continuous input to discrete output with custom thresholds. - -```javascript -const thresholdScale = d3.scaleThreshold() - .domain([0, 10, 20]) - .range(['freezing', 'cold', 'warm', 'hot']); - -thresholdScale(-5); // Returns 'freezing' -thresholdScale(5); // Returns 'cold' -thresholdScale(15); // Returns 'warm' -thresholdScale(25); // Returns 'hot' -``` - -**Use cases:** -- Custom breakpoints -- Grade boundaries (A, B, C, D, F) -- Temperature categories -- Air quality indices - -## Sequential scales - -### Sequential colour scale - -Maps continuous input to continuous colour gradient. - -```javascript -const colourScale = d3.scaleSequential(d3.interpolateBlues) - .domain([0, 100]); - -colourScale(0); // Returns lightest blue -colourScale(50); // Returns mid blue -colourScale(100); // Returns darkest blue -``` - -**Available interpolators:** - -**Single hue:** -- `d3.interpolateBlues`, `d3.interpolateGreens`, `d3.interpolateReds` -- `d3.interpolateOranges`, `d3.interpolatePurples`, `d3.interpolateGreys` - -**Multi-hue:** -- `d3.interpolateViridis`, `d3.interpolateInferno`, `d3.interpolateMagma` -- `d3.interpolatePlasma`, `d3.interpolateWarm`, `d3.interpolateCool` -- `d3.interpolateCubehelixDefault`, `d3.interpolateTurbo` - -**Use cases:** -- Heat maps, choropleth maps -- Continuous data visualisation -- Temperature, elevation, density - -### Diverging colour scale - -Maps continuous input to diverging colour gradient with a midpoint. - -```javascript -const divergingScale = d3.scaleDiverging(d3.interpolateRdBu) - .domain([-10, 0, 10]); - -divergingScale(-10); // Returns red -divergingScale(0); // Returns white/neutral -divergingScale(10); // Returns blue -``` - -**Available interpolators:** -- `d3.interpolateRdBu` - Red to blue -- `d3.interpolateRdYlBu` - Red, yellow, blue -- `d3.interpolateRdYlGn` - Red, yellow, green -- `d3.interpolatePiYG` - Pink, yellow, green -- `d3.interpolateBrBG` - Brown, blue-green -- `d3.interpolatePRGn` - Purple, green -- `d3.interpolatePuOr` - Purple, orange -- `d3.interpolateRdGy` - Red, grey -- `d3.interpolateSpectral` - Rainbow spectrum - -**Use cases:** -- Data with meaningful midpoint (zero, average, neutral) -- Positive/negative values -- Above/below comparisons -- Correlation matrices - -### Sequential quantile scale - -Combines sequential colour with quantile mapping. - -```javascript -const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues) - .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]); - -// Maps based on quantile position -``` - -**Use cases:** -- Perceptually uniform binning -- Handling outliers -- Skewed distributions - -## Ordinal scales - -### Band scale - -Maps discrete input to continuous bands (rectangles) with optional padding. - -```javascript -const bandScale = d3.scaleBand() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.1); - -bandScale('A'); // Returns start position (e.g., 0) -bandScale('B'); // Returns start position (e.g., 110) -bandScale.bandwidth(); // Returns width of each band (e.g., 95) -bandScale.step(); // Returns total step including padding -bandScale.paddingInner(); // Returns inner padding (between bands) -bandScale.paddingOuter(); // Returns outer padding (at edges) -``` - -**Use cases:** -- Bar charts (most common use case) -- Grouped elements -- Categorical axes -- Heat map cells - -**Padding options:** -- `.padding(value)` - Sets both inner and outer padding (0-1) -- `.paddingInner(value)` - Padding between bands (0-1) -- `.paddingOuter(value)` - Padding at edges (0-1) -- `.align(value)` - Alignment of bands (0-1, default 0.5) - -### Point scale - -Maps discrete input to continuous points (no width). - -```javascript -const pointScale = d3.scalePoint() - .domain(['A', 'B', 'C', 'D']) - .range([0, 400]) - .padding(0.5); - -pointScale('A'); // Returns position (e.g., 50) -pointScale('B'); // Returns position (e.g., 150) -pointScale('C'); // Returns position (e.g., 250) -pointScale('D'); // Returns position (e.g., 350) -pointScale.step(); // Returns distance between points -``` - -**Use cases:** -- Line chart categorical x-axis -- Scatter plot with categorical axis -- Node positions in network graphs -- Any point positioning for categories - -### Ordinal colour scale - -Maps discrete input to discrete output (colours, shapes, etc.). - -```javascript -const colourScale = d3.scaleOrdinal(d3.schemeCategory10); - -colourScale('apples'); // Returns first colour -colourScale('oranges'); // Returns second colour -colourScale('apples'); // Returns same first colour (consistent) - -// Custom range -const customScale = d3.scaleOrdinal() - .domain(['cat1', 'cat2', 'cat3']) - .range(['#FF6B6B', '#4ECDC4', '#45B7D1']); -``` - -**Built-in colour schemes:** - -**Categorical:** -- `d3.schemeCategory10` - 10 colours -- `d3.schemeAccent` - 8 colours -- `d3.schemeDark2` - 8 colours -- `d3.schemePaired` - 12 colours -- `d3.schemePastel1` - 9 colours -- `d3.schemePastel2` - 8 colours -- `d3.schemeSet1` - 9 colours -- `d3.schemeSet2` - 8 colours -- `d3.schemeSet3` - 12 colours -- `d3.schemeTableau10` - 10 colours - -**Use cases:** -- Category colours -- Legend items -- Multi-series charts -- Network node types - -## Scale utilities - -### Nice domain - -Extend domain to nice round values. - -```javascript -const scale = d3.scaleLinear() - .domain([0.201, 0.996]) - .nice(); - -scale.domain(); // Returns [0.2, 1.0] - -// With count (approximate tick count) -const scale2 = d3.scaleLinear() - .domain([0.201, 0.996]) - .nice(5); -``` - -### Clamping - -Restrict output to range bounds. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]) - .clamp(true); - -scale(-10); // Returns 0 (clamped) -scale(150); // Returns 500 (clamped) -``` - -### Copy scales - -Create independent copies. - -```javascript -const scale1 = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -const scale2 = scale1.copy(); -// scale2 is independent of scale1 -``` - -### Tick generation - -Generate nice tick values for axes. - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range([0, 500]); - -scale.ticks(10); // Generate ~10 ticks -scale.tickFormat(10); // Get format function for ticks -scale.tickFormat(10, ".2f"); // Custom format (2 decimal places) - -// Time scale ticks -const timeScale = d3.scaleTime() - .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]); - -timeScale.ticks(d3.timeYear); // Yearly ticks -timeScale.ticks(d3.timeMonth, 3); // Every 3 months -timeScale.tickFormat(5, "%Y-%m"); // Format as year-month -``` - -## Colour spaces and interpolation - -### RGB interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]); -// Default: RGB interpolation -``` - -### HSL interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateHsl); -// Smoother colour transitions -``` - -### Lab interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateLab); -// Perceptually uniform -``` - -### HCL interpolation - -```javascript -const scale = d3.scaleLinear() - .domain([0, 100]) - .range(["blue", "red"]) - .interpolate(d3.interpolateHcl); -// Perceptually uniform with hue -``` - -## Common patterns - -### Diverging scale with custom midpoint - -```javascript -const scale = d3.scaleLinear() - .domain([min, midpoint, max]) - .range(["red", "white", "blue"]) - .interpolate(d3.interpolateHcl); -``` - -### Multi-stop gradient scale - -```javascript -const scale = d3.scaleLinear() - .domain([0, 25, 50, 75, 100]) - .range(["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#66c2a5"]); -``` - -### Radius scale for circles (perceptual) - -```javascript -const radiusScale = d3.scaleSqrt() - .domain([0, d3.max(data, d => d.value)]) - .range([0, 50]); - -// Use with circles -circle.attr("r", d => radiusScale(d.value)); -``` - -### Adaptive scale based on data range - -```javascript -function createAdaptiveScale(data) { - const extent = d3.extent(data); - const range = extent[1] - extent[0]; - - // Use log scale if data spans >2 orders of magnitude - if (extent[1] / extent[0] > 100) { - return d3.scaleLog() - .domain(extent) - .range([0, width]); - } - - // Otherwise use linear - return d3.scaleLinear() - .domain(extent) - .range([0, width]); -} -``` - -### Colour scale with explicit categories - -```javascript -const colourScale = d3.scaleOrdinal() - .domain(['Low Risk', 'Medium Risk', 'High Risk']) - .range(['#2ecc71', '#f39c12', '#e74c3c']) - .unknown('#95a5a6'); // Fallback for unknown values -``` \ No newline at end of file diff --git a/.codex/skills/skills/interactive-portfolio/SKILL.md b/.codex/skills/skills/interactive-portfolio/SKILL.md deleted file mode 100644 index 110f519..0000000 --- a/.codex/skills/skills/interactive-portfolio/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: interactive-portfolio -description: "Expert in building portfolios that actually land jobs and clients - not just showing work, but creating memorable experiences. Covers developer portfolios, designer portfolios, creative portfolios, and portfolios that convert visitors into opportunities. Use when: portfolio, personal website, showcase work, developer portfolio, designer portfolio." -source: vibeship-spawner-skills (Apache 2.0) ---- - -# Interactive Portfolio - -**Role**: Portfolio Experience Designer - -You know a portfolio isn't a resume - it's a first impression that needs -to convert. You balance creativity with usability. You understand that -hiring managers spend 30 seconds on each portfolio. You make those 30 -seconds count. You help people stand out without being gimmicky. - -## Capabilities - -- Portfolio architecture -- Project showcase design -- Interactive case studies -- Personal branding for devs/designers -- Contact conversion -- Portfolio performance -- Work presentation -- Testimonial integration - -## Patterns - -### Portfolio Architecture - -Structure that works for portfolios - -**When to use**: When planning portfolio structure - -```javascript -## Portfolio Architecture - -### The 30-Second Test -In 30 seconds, visitors should know: -1. Who you are -2. What you do -3. Your best work -4. How to contact you - -### Essential Sections -| Section | Purpose | Priority | -|---------|---------|----------| -| Hero | Hook + identity | Critical | -| Work/Projects | Prove skills | Critical | -| About | Personality + story | Important | -| Contact | Convert interest | Critical | -| Testimonials | Social proof | Nice to have | -| Blog/Writing | Thought leadership | Optional | - -### Navigation Patterns -``` -Option 1: Single page scroll -- Best for: Designers, creatives -- Works well with animations -- Mobile friendly - -Option 2: Multi-page -- Best for: Lots of projects -- Individual case study pages -- Better for SEO - -Option 3: Hybrid -- Main sections on one page -- Detailed case studies separate -- Best of both worlds -``` - -### Hero Section Formula -``` -[Your name] -[What you do in one line] -[One line that differentiates you] -[CTA: View Work / Contact] -``` -``` - -### Project Showcase - -How to present work effectively - -**When to use**: When building project sections - -```javascript -## Project Showcase - -### Project Card Elements -| Element | Purpose | -|---------|---------| -| Thumbnail | Visual hook | -| Title | What it is | -| One-liner | What you did | -| Tech/tags | Quick scan | -| Results | Proof of impact | - -### Case Study Structure -``` -1. Hero image/video -2. Project overview (2-3 sentences) -3. The challenge -4. Your role -5. Process highlights -6. Key decisions -7. Results/impact -8. Learnings (optional) -9. Links (live, GitHub, etc.) -``` - -### Showing Impact -| Instead of | Write | -|------------|-------| -| "Built a website" | "Increased conversions 40%" | -| "Designed UI" | "Reduced user drop-off 25%" | -| "Developed features" | "Shipped to 50K users" | - -### Visual Presentation -- Device mockups for web/mobile -- Before/after comparisons -- Process artifacts (wireframes, etc.) -- Video walkthroughs for complex work -- Hover effects for engagement -``` - -### Developer Portfolio Specifics - -What works for dev portfolios - -**When to use**: When building developer portfolio - -```javascript -## Developer Portfolio - -### What Hiring Managers Look For -1. Code quality (GitHub link) -2. Real projects (not just tutorials) -3. Problem-solving ability -4. Communication skills -5. Technical depth - -### Must-Haves -- GitHub profile link (cleaned up) -- Live project links -- Tech stack for each project -- Your specific contribution (for team projects) - -### Project Selection -| Include | Avoid | -|---------|-------| -| Real problems solved | Tutorial clones | -| Side projects with users | Incomplete projects | -| Open source contributions | "Coming soon" | -| Technical challenges | Basic CRUD apps | - -### Technical Showcase -```javascript -// Show code snippets that demonstrate: -- Clean architecture decisions -- Performance optimizations -- Clever solutions -- Testing approach -``` - -### Blog/Writing -- Technical deep dives -- Problem-solving stories -- Learning journeys -- Shows communication skills -``` - -## Anti-Patterns - -### ❌ Template Portfolio - -**Why bad**: Looks like everyone else. -No memorable impression. -Doesn't show creativity. -Easy to forget. - -**Instead**: Add personal touches. -Custom design elements. -Unique project presentations. -Your voice in the copy. - -### ❌ All Style No Substance - -**Why bad**: Fancy animations, weak projects. -Style over substance. -Hiring managers see through it. -No proof of skills. - -**Instead**: Projects first, style second. -Real work with real impact. -Quality over quantity. -Depth over breadth. - -### ❌ Resume Website - -**Why bad**: Boring, forgettable. -Doesn't use the medium. -No personality. -Lists instead of stories. - -**Instead**: Show, don't tell. -Visual case studies. -Interactive elements. -Personality throughout. - -## ⚠️ Sharp Edges - -| Issue | Severity | Solution | -|-------|----------|----------| -| Portfolio more complex than your actual work | medium | ## Right-Sizing Your Portfolio | -| Portfolio looks great on desktop, broken on mobile | high | ## Mobile-First Portfolio | -| Visitors don't know what to do next | medium | ## Portfolio CTAs | -| Portfolio shows old or irrelevant work | medium | ## Portfolio Freshness | - -## Related Skills - -Works well with: `scroll-experience`, `3d-web-experience`, `landing-page-design`, `personal-branding` diff --git a/.codex/skills/skills/prd/SKILL.md b/.codex/skills/skills/prd/SKILL.md deleted file mode 100644 index 3c2f7ac..0000000 --- a/.codex/skills/skills/prd/SKILL.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: prd -description: "Generate a Product Requirements Document (PRD) for a new feature. Use when planning a feature, starting a new project, or when asked to create a PRD. Triggers on: create a prd, write prd for, plan this feature, requirements for, spec out." -user-invocable: true ---- - -# PRD Generator - -Create detailed Product Requirements Documents that are clear, actionable, and suitable for implementation. - ---- - -## The Job - -1. Receive a feature description from the user -2. Ask 3-5 essential clarifying questions (with lettered options) -3. Generate a structured PRD based on answers -4. Save to `tasks/prd-[feature-name].md` - -**Important:** Do NOT start implementing. Just create the PRD. - ---- - -## Step 1: Clarifying Questions - -Ask only critical questions where the initial prompt is ambiguous. Focus on: - -- **Problem/Goal:** What problem does this solve? -- **Core Functionality:** What are the key actions? -- **Scope/Boundaries:** What should it NOT do? -- **Success Criteria:** How do we know it's done? - -### Format Questions Like This: - -``` -1. What is the primary goal of this feature? - A. Improve user onboarding experience - B. Increase user retention - C. Reduce support burden - D. Other: [please specify] - -2. Who is the target user? - A. New users only - B. Existing users only - C. All users - D. Admin users only - -3. What is the scope? - A. Minimal viable version - B. Full-featured implementation - C. Just the backend/API - D. Just the UI -``` - -This lets users respond with "1A, 2C, 3B" for quick iteration. Remember to indent the options. - ---- - -## Step 2: PRD Structure - -Generate the PRD with these sections: - -### 1. Introduction/Overview -Brief description of the feature and the problem it solves. - -### 2. Goals -Specific, measurable objectives (bullet list). - -### 3. User Stories -Each story needs: -- **Title:** Short descriptive name -- **Description:** "As a [user], I want [feature] so that [benefit]" -- **Acceptance Criteria:** Verifiable checklist of what "done" means - -Each story should be small enough to implement in one focused session. - -**Format:** -```markdown -### US-001: [Title] -**Description:** As a [user], I want [feature] so that [benefit]. - -**Acceptance Criteria:** -- [ ] Specific verifiable criterion -- [ ] Another criterion -- [ ] Typecheck/lint passes -- [ ] **[UI stories only]** Verify in browser using dev-browser skill -``` - -**Important:** -- Acceptance criteria must be verifiable, not vague. "Works correctly" is bad. "Button shows confirmation dialog before deleting" is good. -- **For any story with UI changes:** Always include "Verify in browser using dev-browser skill" as acceptance criteria. This ensures visual verification of frontend work. - -### 4. Functional Requirements -Numbered list of specific functionalities: -- "FR-1: The system must allow users to..." -- "FR-2: When a user clicks X, the system must..." - -Be explicit and unambiguous. - -### 5. Non-Goals (Out of Scope) -What this feature will NOT include. Critical for managing scope. - -### 6. Design Considerations (Optional) -- UI/UX requirements -- Link to mockups if available -- Relevant existing components to reuse - -### 7. Technical Considerations (Optional) -- Known constraints or dependencies -- Integration points with existing systems -- Performance requirements - -### 8. Success Metrics -How will success be measured? -- "Reduce time to complete X by 50%" -- "Increase conversion rate by 10%" - -### 9. Open Questions -Remaining questions or areas needing clarification. - ---- - -## Writing for Junior Developers - -The PRD reader may be a junior developer or AI agent. Therefore: - -- Be explicit and unambiguous -- Avoid jargon or explain it -- Provide enough detail to understand purpose and core logic -- Number requirements for easy reference -- Use concrete examples where helpful - ---- - -## Output - -- **Format:** Markdown (`.md`) -- **Location:** `tasks/` -- **Filename:** `prd-[feature-name].md` (kebab-case) - ---- - -## Example PRD - -```markdown -# PRD: Task Priority System - -## Introduction - -Add priority levels to tasks so users can focus on what matters most. Tasks can be marked as high, medium, or low priority, with visual indicators and filtering to help users manage their workload effectively. - -## Goals - -- Allow assigning priority (high/medium/low) to any task -- Provide clear visual differentiation between priority levels -- Enable filtering and sorting by priority -- Default new tasks to medium priority - -## User Stories - -### US-001: Add priority field to database -**Description:** As a developer, I need to store task priority so it persists across sessions. - -**Acceptance Criteria:** -- [ ] Add priority column to tasks table: 'high' | 'medium' | 'low' (default 'medium') -- [ ] Generate and run migration successfully -- [ ] Typecheck passes - -### US-002: Display priority indicator on task cards -**Description:** As a user, I want to see task priority at a glance so I know what needs attention first. - -**Acceptance Criteria:** -- [ ] Each task card shows colored priority badge (red=high, yellow=medium, gray=low) -- [ ] Priority visible without hovering or clicking -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -### US-003: Add priority selector to task edit -**Description:** As a user, I want to change a task's priority when editing it. - -**Acceptance Criteria:** -- [ ] Priority dropdown in task edit modal -- [ ] Shows current priority as selected -- [ ] Saves immediately on selection change -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -### US-004: Filter tasks by priority -**Description:** As a user, I want to filter the task list to see only high-priority items when I'm focused. - -**Acceptance Criteria:** -- [ ] Filter dropdown with options: All | High | Medium | Low -- [ ] Filter persists in URL params -- [ ] Empty state message when no tasks match filter -- [ ] Typecheck passes -- [ ] Verify in browser using dev-browser skill - -## Functional Requirements - -- FR-1: Add `priority` field to tasks table ('high' | 'medium' | 'low', default 'medium') -- FR-2: Display colored priority badge on each task card -- FR-3: Include priority selector in task edit modal -- FR-4: Add priority filter dropdown to task list header -- FR-5: Sort by priority within each status column (high to medium to low) - -## Non-Goals - -- No priority-based notifications or reminders -- No automatic priority assignment based on due date -- No priority inheritance for subtasks - -## Technical Considerations - -- Reuse existing badge component with color variants -- Filter state managed via URL search params -- Priority stored in database, not computed - -## Success Metrics - -- Users can change priority in under 2 clicks -- High-priority tasks immediately visible at top of lists -- No regression in task list performance - -## Open Questions - -- Should priority affect task ordering within a column? -- Should we add keyboard shortcuts for priority changes? -``` - ---- - -## Checklist - -Before saving the PRD: - -- [ ] Asked clarifying questions with lettered options -- [ ] Incorporated user's answers -- [ ] User stories are small and specific -- [ ] Functional requirements are numbered and unambiguous -- [ ] Non-goals section defines clear boundaries -- [ ] Saved to `tasks/prd-[feature-name].md` diff --git a/.codex/skills/skills/ralph-setup/SKILL.md b/.codex/skills/skills/ralph-setup/SKILL.md deleted file mode 100644 index 7b8bc08..0000000 --- a/.codex/skills/skills/ralph-setup/SKILL.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -name: ralph-setup -description: Set up autonomous AI development tasks using the Ralph Wiggum technique. Use when the user wants to create a RALPH orchestration — either a simple looping prompt or a multi-hat coordinated workflow. Interviews the user to understand requirements, decides the appropriate mode, and generates all necessary configuration files (ralph.yml, hats.yml, PROMPT.md). Triggers on mentions of "ralph", "autonomous loop", "hat-based", "orchestration", or requests to set up iterative AI agent tasks. ---- - -# Ralph Setup Skill - -Set up autonomous AI development tasks using the Ralph Wiggum technique — either as a simple iterating prompt or a coordinated hat-based workflow. - -## Background - -Ralph implements the Ralph Wiggum technique: give an AI agent a task, loop it until it's done. The orchestrator is deliberately thin — it trusts the agent to do the work and enforces quality through backpressure (tests, lint, typecheck must pass). - -There are two modes: - -| Mode | What It Does | Best For | -|------|-------------|----------| -| **Traditional (Simple Prompt)** | Single loop — agent iterates until LOOP_COMPLETE | Quick tasks, single-concern work, anything one agent can handle in a straight line | -| **Hat-Based** | Specialised personas coordinate through typed events | Complex workflows, multi-step processes, tasks needing distinct planning/building/reviewing phases | - -## Core Tenets (Apply to Both Modes) - -These six tenets guide every RALPH setup. Reference them when making decisions: - -1. **Fresh Context Is Reliability** — Each iteration clears context. The prompt must be self-contained enough to re-read, re-plan, and re-execute every cycle. -2. **Backpressure Over Prescription** — Don't prescribe HOW to do the work. Create gates that reject bad work (tests pass, lint clean, types check). -3. **The Plan Is Disposable** — Regeneration costs one planning loop. Cheap. Don't over-invest in preserving plans. -4. **Disk Is State, Git Is Memory** — Files are the handoff mechanism between iterations. Git provides checkpointing and rollback. -5. **Steer With Signals, Not Scripts** — Add signs (success criteria, quality gates), not step-by-step scripts. -6. **Let Ralph Ralph** — Sit ON the loop, not IN it. The orchestrator coordinates; the agent does the work. - -## Workflow - -### Phase 1: Interview the User - -Before generating anything, you need to understand the task. Ask targeted questions to fill in these blanks: - -**Essential information:** -- What is the task? (Be specific — "build an API" is too vague; "build a REST API for user management with Express.js and TypeScript" is good) -- What does "done" look like? (Measurable success criteria — tests pass, endpoints respond, specific files exist) -- What language/framework/tools are involved? -- Does the project already exist, or is this greenfield? -- Are there existing tests, linting, or type-checking set up? - -**Information that helps you decide the mode:** -- How many distinct phases or concerns does this task have? (1-2 = simple prompt; 3+ = consider hats) -- Does the task need planning before building? (If yes, hat-based is likely better) -- Does the task need a review/QA step separate from building? (If yes, hat-based) -- Is there a spec or design document to follow? (Spec-driven development suits hats well) -- How complex is the codebase? (Large existing codebase with multiple modules = hat-based) - -**Don't over-interview.** If the user gives you a clear, well-scoped task, you may have enough after 1-2 questions. If the task is vague, probe until you can write a crisp PROMPT.md. - -### Phase 2: Decide the Mode - -Use this decision framework: - -**Choose Simple Prompt when:** -- The task is a single concern (add a feature, fix a bug, write a script) -- One agent can handle it start to finish without distinct phases -- The success criteria are straightforward (tests pass, script runs) -- The user explicitly wants something quick and simple -- The task can be fully described in a PROMPT.md under ~50 lines - -**Choose Hat-Based when:** -- The task has 3+ distinct phases (plan → build → test → review) -- Different phases need different "mindsets" (architect vs implementer vs reviewer) -- The task involves spec-driven development (spec → implement → verify) -- There's a TDD workflow (write tests → implement → verify) -- The task is large enough that a single prompt would be overwhelming -- Multiple files/modules need coordinated changes -- The user explicitly asks for hats or a structured workflow - -**When in doubt:** Start with Simple Prompt. You can always add hats later. Simpler is more robust. - -### Phase 3: Generate the Files - -Generate the appropriate files into the user's project directory. Always explain what you're creating and why. - -Read the appropriate reference file before generating: -- For Simple Prompt: `references/simple-prompt-reference.md` -- For Hat-Based: `references/hat-based-reference.md` - -#### Files to Generate - -**Both modes:** -- `ralph.yml` — Main configuration -- `PROMPT.md` — The task definition - -**Hat-Based mode additionally:** -- `hats.yml` — Hat definitions with triggers, publishes, and instructions - -### Phase 4: Review with the User - -After generating the files, walk the user through what you created: -- Summarise the task as you understood it -- Explain the mode choice and why -- Highlight the success criteria / completion promise -- For hat-based: explain the event flow between hats -- Ask if anything needs adjusting before they run it - -Then tell them how to run it: -```bash -# Simple prompt -ralph run - -# Hat-based -ralph run --config hats.yml - -# With iteration limit -ralph run --max-iterations 50 -``` - -## Writing Good Prompts (PROMPT.md) - -The PROMPT.md is the most important file. It must be: - -**Self-contained:** Every iteration starts fresh. The prompt must contain everything the agent needs to understand the task, check progress, and continue. - -**Outcome-focused:** Define WHAT, not HOW. Let the agent figure out the approach. - -**Measurable:** Include concrete success criteria the agent can verify: -- "All tests pass" (not "write good tests") -- "The /users endpoint returns 200 with valid JSON" (not "make the API work") -- "TypeScript compiles with zero errors" (not "fix the types") - -**Structured but not prescriptive:** Use sections like Task, Requirements, Success Criteria, Constraints. Don't write step-by-step instructions. - -### Prompt Template (Simple) - -```markdown -# Task: [Clear, specific title] - -[2-3 sentence description of what needs to be built/done] - -## Requirements - -- [Specific requirement 1] -- [Specific requirement 2] -- [Specific requirement 3] - -## Success Criteria - -All of the following must be true: -- [ ] [Measurable criterion 1] -- [ ] [Measurable criterion 2] -- [ ] [Measurable criterion 3] - -## Constraints - -- [Technology constraints] -- [Style/convention constraints] -- [Performance constraints if any] - -## Status - -Track your progress here. Mark items complete as you go. -When all success criteria are met, print LOOP_COMPLETE. -``` - -## Designing Hat Systems - -When creating hats, follow these principles: - -**Each hat should have a single responsibility.** Don't create a hat that plans AND builds. - -**Events flow forward.** The event chain should be a clear pipeline: work.start → plan.ready → build.done → review (changes requested OR LOOP_COMPLETE). - -**Terminal hats should end, not publish success.** For the final validation/review hat, success should be `LOOP_COMPLETE` (no success event like `review.approved`), and only rework/failure events should be published. - -**Instructions should be specific to the hat's role.** The planner hat gets planning instructions, the builder gets building instructions. - -**Keep it minimal.** 2-4 hats is typical. More than 5 is usually overengineered. - -### Common Hat Patterns - -**Plan → Build (2 hats):** -Good for tasks that need architectural thinking before coding. - -**Plan → Build → Review (3 hats):** -Good for tasks that need quality assurance. - -**Spec → Implement → Verify (3 hats):** -Good for spec-driven development. - -**Test → Implement → Verify (3 hats):** -Good for TDD workflows. - -See `references/hat-based-reference.md` for full configuration examples. - -## Backpressure Configuration - -Backpressure gates reject incomplete work. Common gates: - -```yaml -backpressure: - gates: - - name: "tests" - command: "npm test" - on_fail: "retry" - - name: "lint" - command: "npm run lint" - on_fail: "retry" - - name: "typecheck" - command: "npx tsc --noEmit" - on_fail: "retry" -``` - -Only add gates for tools that exist in the project. If there are no tests yet, don't add a test gate (unless the task IS to create tests). - -### No-Skip Safety Rules - -When configuring backpressure and completion logic, preserve quality standards: - -- Never treat a circuit breaker as an automatic pass. -- Never skip required checks that are configured in the repository. -- Always require an explicit review outcome before completion (`LOOP_COMPLETE` or concrete changes requested). -- If tests exist in the project and are part of quality gates, they must run and pass before completion. -- If a gate is not configured in the repo, mark it `not-configured` explicitly rather than fabricating retries. - -### Loop Circuit Breaker and Escalation - -To prevent infinite review/backpressure churn, include a circuit breaker policy in generated prompts/hats: - -- Detect repeated identical evidence cycles (same blocker class and materially identical build evidence) across 2-3 consecutive iterations. -- If repetition threshold is reached, stop retrying the same recovery path. -- Escalate instead of auto-completing: - - record the blocker and evidence in `.ralph/review.md` - - assign an owner and target finish date - - set status to require human decision/clarification -- Resume the loop only after the blocker criteria are clarified or configuration is corrected. - -### Operational Hygiene Between Runs - -Treat runtime coordination state as loop-scoped: - -- Do not carry stale "recovery" tasks into a new objective unless explicitly intended. -- Avoid creating new meta/recovery tasks when all implementation tasks are already closed and no new actionable finding exists. -- Keep artifacts (`.ralph/plan.md`, `.ralph/review.md`, event logs) for auditability, but ensure open task queues reflect only current-loop actionable work. -- Prefer one clear escalation handoff over repeated coordination retries with identical payloads. - -## Cost and Safety - -Always configure iteration limits. Remind the user: -- Default max iterations: 100 -- Default max runtime: 4 hours -- A 50-iteration cycle on a large codebase can cost $50-100+ in API credits -- Recommend starting with `--max-iterations 30` for new setups and increasing if needed -- Git checkpointing is on by default — the user can always roll back diff --git a/.codex/skills/skills/ralph-setup/references/hat-based-reference.md b/.codex/skills/skills/ralph-setup/references/hat-based-reference.md deleted file mode 100644 index 780f6dd..0000000 --- a/.codex/skills/skills/ralph-setup/references/hat-based-reference.md +++ /dev/null @@ -1,352 +0,0 @@ -# Hat-Based Reference - -## Overview - -Hat-based mode uses specialised personas ("hats") that coordinate through typed events. Each hat triggers on specific events and publishes new events when done, creating a pipeline of distinct phases. - -Use this when the task genuinely benefits from separating concerns — e.g., planning separately from building, or reviewing separately from implementing. - -## hats.yml Structure - -```yaml -cli: - backend: "claude" - -event_loop: - starting_event: "work.start" # First delegated event that kicks off the pipeline - completion_promise: "LOOP_COMPLETE" # String that signals completion - max_iterations: 30 # Start conservative, increase if needed - -hats: - hat_name: - name: "Human-Readable Name" - description: "Short purpose of this hat" - triggers: ["event.that.activates.this.hat"] - publishes: ["event.this.hat.emits.when.done"] - instructions: | - Detailed instructions for what this hat should do. - Must be self-contained — the hat gets fresh context each time. - Should reference PROMPT.md for the overall task. - Should specify what "done" means for this hat. -``` - -### Key Rules - -- **triggers**: List of events that activate this hat. A hat runs when ANY of its trigger events fire. -- **publishes**: List of events this hat emits when it completes its work. -- **description**: Required short summary of the hat's purpose. -- **reserved events**: Do not use `task.start` or `task.resume` as hat triggers. Use delegated events like `work.start`. -- **instructions**: The prompt for this hat. Must be specific to the hat's role. -- **terminal success rule**: Final hats should print `LOOP_COMPLETE` on success and should NOT publish success events. -- Events flow forward through the pipeline. Avoid circular event chains. -- The last hat in the pipeline should print LOOP_COMPLETE when the overall task is done. - -## Common Patterns - -### Pattern 1: Plan → Build (2 Hats) - -Best for tasks that need architectural thinking before coding. - -```yaml -cli: - backend: "claude" - -event_loop: - starting_event: "work.start" - completion_promise: "LOOP_COMPLETE" - -hats: - planner: - name: "Planner" - description: "Analyses requirements and writes an implementation plan." - triggers: ["work.start", "build.retry_needed"] - publishes: ["plan.ready"] - instructions: | - You are the Planner. Read PROMPT.md to understand the task. - - Your job: - 1. Analyse the requirements and existing codebase - 2. Create a clear implementation plan in .ralph/plan.md - 3. Break the work into concrete steps with file-level detail - 4. Identify any risks or unknowns - - Write the plan to .ralph/plan.md then emit plan.ready. - - Do NOT write any code. Planning only. - - builder: - name: "Builder" - description: "Implements the plan and delivers working code." - triggers: ["plan.ready"] - publishes: ["build.retry_needed"] - instructions: | - You are the Builder. Read PROMPT.md for the task and .ralph/plan.md - for the implementation plan. - - Your job: - 1. Follow the plan step by step - 2. Write clean, tested code - 3. Run tests after each significant change - 4. Update .ralph/plan.md to mark completed steps - - If all success criteria from PROMPT.md are met and all tests pass, - print LOOP_COMPLETE and stop. - - If blocked, emit build.retry_needed with specific blocker details. -``` - -### Pattern 2: Plan → Build → Review (3 Hats) - -Adds a review phase for quality assurance. - -```yaml -cli: - backend: "claude" - -event_loop: - starting_event: "work.start" - completion_promise: "LOOP_COMPLETE" - -hats: - planner: - name: "Planner" - description: "Creates/updates implementation plans based on task and review feedback." - triggers: ["work.start", "review.changes_requested"] - publishes: ["plan.ready"] - instructions: | - You are the Planner. Read PROMPT.md to understand the task. - - If triggered by review.changes_requested, read .ralph/review.md - for feedback and update the plan accordingly. - - Create or update .ralph/plan.md with a clear implementation plan. - Emit plan.ready when done. Do NOT write code. - - builder: - name: "Builder" - description: "Implements planned changes and prepares them for review." - triggers: ["plan.ready"] - publishes: ["build.done"] - instructions: | - You are the Builder. Read PROMPT.md and .ralph/plan.md. - - Implement the plan. Write tests. Run them. - When implementation is complete, emit build.done. - - Do NOT assess overall quality — that's the Reviewer's job. - - reviewer: - name: "Reviewer" - description: "Validates quality and requirements, approving or requesting changes." - triggers: ["build.done"] - publishes: ["review.changes_requested"] - instructions: | - You are the Reviewer. Read PROMPT.md for requirements. - - Review the current state of the codebase against the success criteria: - 1. Do all tests pass? - 2. Are all requirements met? - 3. Is the code clean and following project conventions? - 4. Are there edge cases not covered? - - If everything passes, write your review to .ralph/review.md - and print LOOP_COMPLETE. - - If changes are needed, write specific feedback to .ralph/review.md - and emit review.changes_requested. -``` - -### Pattern 3: Spec → Implement → Verify (3 Hats) - -For spec-driven development — good when working from a design document. - -```yaml -cli: - backend: "claude" - -event_loop: - starting_event: "work.start" - completion_promise: "LOOP_COMPLETE" - -hats: - spec_writer: - name: "Spec Writer" - description: "Writes and updates the technical specification." - triggers: ["work.start", "verify.gaps_found"] - publishes: ["spec.ready"] - instructions: | - You are the Spec Writer. Read PROMPT.md for the high-level task. - - If triggered by verify.gaps_found, read .ralph/verification.md - for gaps and update the spec to address them. - - Write a detailed technical specification to .ralph/spec.md: - - API contracts (endpoints, request/response shapes) - - Data models - - Error handling behaviour - - Test scenarios - - Emit spec.ready when done. Do NOT write implementation code. - - implementer: - name: "Implementer" - description: "Builds the solution from the specification." - triggers: ["spec.ready"] - publishes: ["implementation.done"] - instructions: | - You are the Implementer. Read .ralph/spec.md for the specification. - - Implement exactly what the spec describes. Write tests that verify - each specification point. Run tests after each change. - - Emit implementation.done when the spec is fully implemented. - - verifier: - name: "Verifier" - description: "Checks implementation against the spec and success criteria." - triggers: ["implementation.done"] - publishes: ["verify.gaps_found"] - instructions: | - You are the Verifier. Read .ralph/spec.md and PROMPT.md. - - Verify that the implementation matches the spec: - 1. Run all tests — they must pass - 2. Check each spec point against the code - 3. Verify success criteria from PROMPT.md - - If everything checks out, print LOOP_COMPLETE. - - If there are gaps, write them to .ralph/verification.md - and emit verify.gaps_found. -``` - -### Pattern 4: TDD — Test → Implement → Verify (3 Hats) - -For test-driven development workflows. - -```yaml -cli: - backend: "claude" - -event_loop: - starting_event: "work.start" - completion_promise: "LOOP_COMPLETE" - -hats: - test_writer: - name: "Test Writer" - description: "Creates failing tests that define expected behaviour." - triggers: ["work.start", "verify.tests_needed"] - publishes: ["tests.ready"] - instructions: | - You are the Test Writer. Read PROMPT.md for requirements. - - Write failing tests FIRST that describe the desired behaviour. - Tests should be comprehensive and cover edge cases. - - If triggered by verify.tests_needed, read .ralph/verification.md - for the specific test gaps to fill. - - Write tests, verify they fail (red phase), then emit tests.ready. - Do NOT write implementation code. - - implementer: - name: "Implementer" - description: "Implements code to satisfy tests." - triggers: ["tests.ready"] - publishes: ["implementation.done"] - instructions: | - You are the Implementer. Your goal is to make the tests pass. - - Read the test files to understand what behaviour is expected. - Write the minimum code to make all tests pass (green phase). - - Run tests after each change. When all tests pass, - emit implementation.done. - - verifier: - name: "Verifier" - description: "Confirms tests, coverage, and requirement completeness." - triggers: ["implementation.done"] - publishes: ["verify.tests_needed"] - instructions: | - You are the Verifier. Read PROMPT.md for the full requirements. - - Check: - 1. All tests pass - 2. Test coverage is adequate for the requirements - 3. All success criteria from PROMPT.md are met - 4. Code is clean (refactor phase if needed) - - If complete, print LOOP_COMPLETE. - If more tests are needed, write gaps to .ralph/verification.md - and emit verify.tests_needed. -``` - -## Backpressure with Hats - -Backpressure gates can be applied globally or per-hat: - -```yaml -# Global backpressure — applies to all hats -backpressure: - gates: - - name: "tests" - command: "npm test" - on_fail: "retry" - - name: "lint" - command: "npm run lint" - on_fail: "retry" - -# Per-hat backpressure -hats: - builder: - triggers: ["plan.ready"] - publishes: ["build.done"] - backpressure: - gates: - - name: "typecheck" - command: "npx tsc --noEmit" - on_fail: "retry" - instructions: | - ... -``` - -## Memories - -Hats can use persistent memories stored in `.ralph/agent/memories.md`. These survive across iterations and sessions: - -```yaml -hats: - builder: - memory: - path: ".ralph/agent/memories.md" - scope: "hat" # or "global" to share across hats -``` - -Memories are useful for capturing lessons learned, recording decisions, and avoiding repeated mistakes. - -## Running Hat-Based Workflows - -```bash -# Run with hats config -ralph run --config hats.yml - -# With iteration limit -ralph run --config hats.yml --max-iterations 30 - -# Resume interrupted session -ralph run --config hats.yml --continue -``` - -## Anti-Patterns - -**Too many hats.** If you have more than 5, you're probably overengineering. Each hat adds coordination overhead. - -**Publishing success events from terminal hats.** Avoid `review.approved`/`verify.passed`-style terminal success events. Prefer `LOOP_COMPLETE` for success and reserve published events for rework paths only. - -**Hats that duplicate work.** If the builder is also doing planning, your planner hat is wasted. - -**Overly prescriptive hat instructions.** The instructions should say WHAT to achieve, not HOW. Let the agent figure out the approach. - -**Missing the PROMPT.md reference.** Hat instructions should always tell the agent to read PROMPT.md for the overall task context. Without it, hats lose sight of the bigger picture. diff --git a/.codex/skills/skills/ralph-setup/references/simple-prompt-reference.md b/.codex/skills/skills/ralph-setup/references/simple-prompt-reference.md deleted file mode 100644 index 0a7352d..0000000 --- a/.codex/skills/skills/ralph-setup/references/simple-prompt-reference.md +++ /dev/null @@ -1,167 +0,0 @@ -# Simple Prompt Reference - -## Overview - -Traditional mode is Ralph at its simplest: a single agent loops against a PROMPT.md until it outputs LOOP_COMPLETE or hits the iteration limit. No hats, no events — just a loop. - -This is the right choice for most tasks. Don't reach for hats unless you genuinely need distinct phases with different mindsets. - -## ralph.yml Configuration - -```yaml -cli: - backend: "claude" # or: kiro, gemini, codex, amp, copilot, opencode - -event_loop: - completion_promise: "LOOP_COMPLETE" - max_iterations: 50 # Start conservative, increase if needed -``` - -### Backend Options - -| Backend | CLI Tool | Notes | -|---------|----------|-------| -| claude | Claude Code | Recommended. Best reasoning, large context window | -| kiro | Kiro | AWS-integrated | -| gemini | Gemini CLI | Cost-effective | -| codex | Codex | OpenAI agent | -| amp | Amp | Sourcegraph agent | -| copilot | Copilot CLI | GitHub integrated | -| opencode | OpenCode | Open source | - -## PROMPT.md Examples - -### Example 1: Build a Feature - -```markdown -# Task: Add User Authentication to Express API - -Add JWT-based authentication to the existing Express.js API. - -## Requirements - -- POST /auth/login accepts email + password, returns JWT -- POST /auth/register creates a new user account -- Middleware protects all /users/* routes -- Tokens expire after 24 hours -- Passwords are hashed with bcrypt - -## Success Criteria - -All of the following must be true: -- [ ] POST /auth/register creates a user and returns 201 -- [ ] POST /auth/login returns a valid JWT for correct credentials -- [ ] POST /auth/login returns 401 for incorrect credentials -- [ ] Protected routes return 401 without a valid token -- [ ] Protected routes work normally with a valid token -- [ ] All existing tests still pass -- [ ] New tests cover all auth endpoints -- [ ] TypeScript compiles with zero errors - -## Constraints - -- Use jsonwebtoken for JWT handling -- Use bcrypt for password hashing -- Follow existing code patterns in src/ -- Do not modify existing endpoint behaviour - -## Status - -Track progress here. When all success criteria are met, print LOOP_COMPLETE. -``` - -### Example 2: Fix a Bug - -```markdown -# Task: Fix Race Condition in WebSocket Handler - -The WebSocket message handler has a race condition where concurrent connections -can corrupt shared state. Messages are being delivered to wrong clients. - -## Current Behaviour - -When 2+ clients send messages simultaneously, responses sometimes go to the -wrong client. See issue #247 for reproduction steps. - -## Expected Behaviour - -Each client receives only their own responses, regardless of concurrency. - -## Success Criteria - -- [ ] Concurrent WebSocket test passes (test/ws-concurrent.test.ts) -- [ ] Existing WebSocket tests still pass -- [ ] No shared mutable state between connection handlers -- [ ] Load test with 50 concurrent connections shows zero cross-talk - -## Constraints - -- Do not change the public WebSocket API -- Fix must work with the existing Redis pub/sub setup - -## Status - -Track progress here. When all success criteria are met, print LOOP_COMPLETE. -``` - -### Example 3: Write a Script - -```markdown -# Task: CSV Data Migration Script - -Create a Python script that migrates data from the legacy CSV format to the -new database schema. - -## Requirements - -- Read CSV files from data/legacy/*.csv -- Transform fields according to the mapping in docs/migration-map.md -- Insert into PostgreSQL using the existing SQLAlchemy models -- Handle duplicates by updating existing records -- Log all skipped/failed rows to migration_errors.log - -## Success Criteria - -- [ ] Script processes all CSV files in data/legacy/ -- [ ] All valid rows are inserted or updated in the database -- [ ] Duplicate handling works correctly (update, don't duplicate) -- [ ] Error log captures all skipped rows with reasons -- [ ] Script completes without unhandled exceptions -- [ ] Unit tests cover the transformation logic - -## Constraints - -- Python 3.11+ -- Use existing SQLAlchemy models from src/models/ -- Must be idempotent (safe to run multiple times) - -## Status - -Track progress here. When all success criteria are met, print LOOP_COMPLETE. -``` - -## Running - -```bash -# Basic run -ralph run - -# With iteration limit -ralph run --max-iterations 30 - -# Resume an interrupted session -ralph run --continue - -# Quiet mode (no TUI) -ralph run -q -``` - -## When to Upgrade to Hats - -If you find the simple prompt struggling because: -- The agent keeps flip-flopping between planning and coding -- It loses track of the overall architecture while implementing details -- It writes code but never stops to review/test properly -- The task is too large for a single coherent prompt - -...then consider switching to hat-based mode. But try simplifying the prompt first — often the issue is a vague prompt, not a need for hats. diff --git a/.codex/skills/skills/ralph-skill/SKILL.md b/.codex/skills/skills/ralph-skill/SKILL.md deleted file mode 100644 index 5bae50d..0000000 --- a/.codex/skills/skills/ralph-skill/SKILL.md +++ /dev/null @@ -1,196 +0,0 @@ ---- -name: RalphSkill -description: > - Manage Ralph Wiggum Loop projects -- both creating new ones and continuing/amending - completed ones. Use when the user says things like: "set up a new Ralph project", - "create a new Ralph loop", "ralph setup", "new Ralph", "start a Ralph", "continue - the Ralph loop", "there's a bug in the Ralph output", "amend the Ralph project", - "re-run Ralph with changes", "add tasks to the Ralph loop", "fix [something] in the - Ralph project", or any reference to modifying a completed Ralph loop to address bugs - or feature changes. Supports two templates: Snowflake (NHS PQS query development) - and Generic (any software project). ---- - -# RalphSkill - -Two workflows: **New Project** (setup from template) and **Continue Project** (amend a completed loop). - -Determine which workflow by context: -- User mentions setting up, creating, or starting a new project -> **New Project** -- User mentions bugs, fixes, changes, continuing, amending, or re-running an existing project -> **Continue Project** -- If ambiguous, ask. - ---- - -## Workflow A: New Project - -### A1. Template Selection - -Ask which template: - -| Template | Use when | -|----------|----------| -| **Snowflake** | Snowflake SQL queries (PQS, analytics, NHS data) | -| **Generic** | Standard software development (any language/framework) | - -### A2. Project Directory - -Ask: -- Directory name? -- Location? (default: `Ralph Local/Tasks/`) - -### A3. Copy Template - -Copy the template directory to target. Do NOT modify `RALPH_PROMPT.md`, `SNOWFLAKE_REFERENCE.md` (Snowflake only), or `README.md`. - -### A4. Intake Interview - -**Snowflake**: Read `INTAKE.md` from the copied template. Follow it exactly -- 10 sections, one at a time, summarise and confirm after each. Probe vague answers for specifics (codes, not just drug names). - -**Generic**: Ask: -- **Scope**: What are you building? Language/framework? -- **Quality checks**: What commands validate? (`npm test`, `python -m pytest`, etc.) -- **Tasks**: Implementation tasks in priority order. Each completable in one iteration (~120k tokens). Split large tasks. -- **Config**: Max iterations (default: 10), model (default: sonnet, opus for complex), branch name? -- **Known pitfalls**: Gotchas to capture as guardrails? - -If the user's initial message already answers some questions, skip those. - -### A5. Populate Files - -**Snowflake** -- populate with interview answers: -- `QUERY_PLAN.md`: All sections filled, HTML comments replaced. Tasks updated per interview. Unknowns as lookup tasks. -- `guardrails.md`: Keep standard guardrails. Add project-specific ones below (When/Rule/Why format). -- `progress.txt`: Seed Data Patterns and Query Patterns with known info. Iteration Log empty. -- `ralph.ps1`: Update `param()` defaults if non-default iterations/model specified. - -**Generic** -- populate with interview answers: -- `IMPLEMENTATION_PLAN.md`: Project Overview, Quality Checks, Tasks all filled. -- `progress.txt`: Seed Codebase Patterns with known info. -- `ralph.ps1`: Update `param()` defaults if needed. -- `guardrails.md`: Create only if user mentioned significant pitfalls. - -### A6. Validate and Confirm - -Show summary: -``` -Project: [name] -Template: [Snowflake/Generic] -Tasks: [count] ([brief titles]) -Model: [model] | Max iterations: [N] -Branch: [branch or "none"] -``` - -Show the task list. Ask user to confirm. Adjust if needed. - -Remind how to run: -```powershell -cd "[project path]" -.\ralph.ps1 -BranchName "[branch]" -``` - -**Checklist**: Plan fully populated (no HTML comments), tasks reflect real work, unknowns are explicit tasks, validation criteria have numbers (Snowflake), quality checks runnable, progress.txt seeded, ralph.ps1 configured, guardrails added. - ---- - -## Workflow B: Continue Project - -For amending a completed (or stalled) Ralph loop to address bugs, missing requirements, or feature changes. - -### B1. Identify the Project - -Ask or infer which project directory to amend. Read the project to understand its current state: - -1. Read the plan file (`QUERY_PLAN.md` or `IMPLEMENTATION_PLAN.md`) -- understand all tasks and their status -2. Read `progress.txt` -- understand what was accomplished, key numbers, patterns discovered -3. Read `guardrails.md` (if exists) -- understand known failure patterns -4. Run `git log --oneline -20` in the project directory -- understand commit history - -### B2. Understand the Change - -Ask the user what needs changing. Common scenarios: - -| Scenario | Example | -|----------|---------| -| **Bug in output** | "The patient counts are wrong -- it's including deceased patients" | -| **Missing requirement** | "We also need to exclude care home residents" | -| **Feature change** | "The scoring method changed from deprescribing to threshold-based" | -| **New tasks needed** | "We need an indicative score query alongside the official one" | -| **Validation failure** | "Cross-validation showed a 30% discrepancy with prescribing data" | - -Probe for specifics: -- What exactly is wrong? What's the expected vs actual behaviour? -- Which task(s) are affected? -- Does this invalidate previously completed work, or is it additive? - -### B3. Assess Impact - -Determine what needs to change: - -**Additive** (completed work is still valid): -- New tasks appended to the plan file -- Existing `[x]` tasks remain marked complete - -**Corrective** (completed work needs revision): -- Affected tasks reset from `[x]` back to `[ ]` -- Dependent tasks also reset (e.g. if the cohort CTE is wrong, the aggregation task that uses it must also be redone) -- Document clearly WHY tasks were reset - -**Structural** (fundamental change to approach): -- Multiple tasks may need rewriting, not just resetting -- Plan file sections (cohort, scoring, validation criteria) may need updating -- Consider whether it's cleaner to rewrite affected plan sections vs. patching - -Present the impact assessment to the user and confirm before making changes. - -### B4. Update Project Files - -Apply changes based on the assessment: - -**Plan file** (`QUERY_PLAN.md` / `IMPLEMENTATION_PLAN.md`): -- Add new tasks as `[ ]` items in appropriate position (respect dependency order) -- Reset affected tasks from `[x]` to `[ ]` -- Update plan sections if requirements changed (cohort, scoring, dates, validation criteria, etc.) -- If tasks are being reset, add a comment: `` - -**`guardrails.md`**: -- If the bug reveals a failure pattern, add a new guardrail (When/Rule/Why format) -- Example: if deceased patients were included, add a guardrail about the registered population filter - -**`progress.txt`**: -- Append a **manual intervention entry** to the Iteration Log: -``` -## Manual Intervention -- [YYYY-MM-DD] -### Reason: [brief description of what changed] -### Changes made: -- [List of file changes] -### Tasks reset: [list task names that were unchecked] -### Tasks added: [list new task names] -### Context for next iteration: -- [What the next Ralph iteration needs to know about these changes] -- [Why previous approach was wrong and what to do differently] -### New guardrails added: -- [Any new guardrails, or "none"] -``` - -**`ralph.ps1`**: -- Update `param()` defaults if the user wants different iterations/model for the continuation run - -### B5. Validate and Confirm - -Show the user: -1. Tasks that were reset (with reasons) -2. New tasks added -3. Updated plan sections (if any) -4. New guardrails (if any) -5. The progress.txt intervention entry - -Ask user to confirm. Adjust if needed. - -Remind how to re-run: -```powershell -cd "[project path]" -.\ralph.ps1 -BranchName "[branch]" -``` - -**Checklist**: All affected tasks reset, new tasks in correct dependency position, plan sections updated if requirements changed, guardrails added for discovered failure patterns, progress.txt has intervention entry with clear context for next iteration, no orphaned dependencies (don't reset task A without also resetting tasks that depend on A). diff --git a/.codex/skills/skills/ralph/SKILL.md b/.codex/skills/skills/ralph/SKILL.md deleted file mode 100644 index e402ab8..0000000 --- a/.codex/skills/skills/ralph/SKILL.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -name: ralph -description: "Convert PRDs to prd.json format for the Ralph autonomous agent system. Use when you have an existing PRD and need to convert it to Ralph's JSON format. Triggers on: convert this prd, turn this into ralph format, create prd.json from this, ralph json." -user-invocable: true ---- - -# Ralph PRD Converter - -Converts existing PRDs to the prd.json format that Ralph uses for autonomous execution. - ---- - -## The Job - -Take a PRD (markdown file or text) and convert it to `prd.json` in your ralph directory. - ---- - -## Output Format - -```json -{ - "project": "[Project Name]", - "branchName": "ralph/[feature-name-kebab-case]", - "description": "[Feature description from PRD title/intro]", - "userStories": [ - { - "id": "US-001", - "title": "[Story title]", - "description": "As a [user], I want [feature] so that [benefit]", - "acceptanceCriteria": [ - "Criterion 1", - "Criterion 2", - "Typecheck passes" - ], - "priority": 1, - "passes": false, - "notes": "" - } - ] -} -``` - ---- - -## Story Size: The Number One Rule - -**Each story must be completable in ONE Ralph iteration (one context window).** - -Ralph spawns a fresh Amp instance per iteration with no memory of previous work. If a story is too big, the LLM runs out of context before finishing and produces broken code. - -### Right-sized stories: -- Add a database column and migration -- Add a UI component to an existing page -- Update a server action with new logic -- Add a filter dropdown to a list - -### Too big (split these): -- "Build the entire dashboard" - Split into: schema, queries, UI components, filters -- "Add authentication" - Split into: schema, middleware, login UI, session handling -- "Refactor the API" - Split into one story per endpoint or pattern - -**Rule of thumb:** If you cannot describe the change in 2-3 sentences, it is too big. - ---- - -## Story Ordering: Dependencies First - -Stories execute in priority order. Earlier stories must not depend on later ones. - -**Correct order:** -1. Schema/database changes (migrations) -2. Server actions / backend logic -3. UI components that use the backend -4. Dashboard/summary views that aggregate data - -**Wrong order:** -1. UI component (depends on schema that does not exist yet) -2. Schema change - ---- - -## Acceptance Criteria: Must Be Verifiable - -Each criterion must be something Ralph can CHECK, not something vague. - -### Good criteria (verifiable): -- "Add `status` column to tasks table with default 'pending'" -- "Filter dropdown has options: All, Active, Completed" -- "Clicking delete shows confirmation dialog" -- "Typecheck passes" -- "Tests pass" - -### Bad criteria (vague): -- "Works correctly" -- "User can do X easily" -- "Good UX" -- "Handles edge cases" - -### Always include as final criterion: -``` -"Typecheck passes" -``` - -For stories with testable logic, also include: -``` -"Tests pass" -``` - -### For stories that change UI, also include: -``` -"Verify in browser using dev-browser skill" -``` - -Frontend stories are NOT complete until visually verified. Ralph will use the dev-browser skill to navigate to the page, interact with the UI, and confirm changes work. - ---- - -## Conversion Rules - -1. **Each user story becomes one JSON entry** -2. **IDs**: Sequential (US-001, US-002, etc.) -3. **Priority**: Based on dependency order, then document order -4. **All stories**: `passes: false` and empty `notes` -5. **branchName**: Derive from feature name, kebab-case, prefixed with `ralph/` -6. **Always add**: "Typecheck passes" to every story's acceptance criteria - ---- - -## Splitting Large PRDs - -If a PRD has big features, split them: - -**Original:** -> "Add user notification system" - -**Split into:** -1. US-001: Add notifications table to database -2. US-002: Create notification service for sending notifications -3. US-003: Add notification bell icon to header -4. US-004: Create notification dropdown panel -5. US-005: Add mark-as-read functionality -6. US-006: Add notification preferences page - -Each is one focused change that can be completed and verified independently. - ---- - -## Example - -**Input PRD:** -```markdown -# Task Status Feature - -Add ability to mark tasks with different statuses. - -## Requirements -- Toggle between pending/in-progress/done on task list -- Filter list by status -- Show status badge on each task -- Persist status in database -``` - -**Output prd.json:** -```json -{ - "project": "TaskApp", - "branchName": "ralph/task-status", - "description": "Task Status Feature - Track task progress with status indicators", - "userStories": [ - { - "id": "US-001", - "title": "Add status field to tasks table", - "description": "As a developer, I need to store task status in the database.", - "acceptanceCriteria": [ - "Add status column: 'pending' | 'in_progress' | 'done' (default 'pending')", - "Generate and run migration successfully", - "Typecheck passes" - ], - "priority": 1, - "passes": false, - "notes": "" - }, - { - "id": "US-002", - "title": "Display status badge on task cards", - "description": "As a user, I want to see task status at a glance.", - "acceptanceCriteria": [ - "Each task card shows colored status badge", - "Badge colors: gray=pending, blue=in_progress, green=done", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 2, - "passes": false, - "notes": "" - }, - { - "id": "US-003", - "title": "Add status toggle to task list rows", - "description": "As a user, I want to change task status directly from the list.", - "acceptanceCriteria": [ - "Each row has status dropdown or toggle", - "Changing status saves immediately", - "UI updates without page refresh", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 3, - "passes": false, - "notes": "" - }, - { - "id": "US-004", - "title": "Filter tasks by status", - "description": "As a user, I want to filter the list to see only certain statuses.", - "acceptanceCriteria": [ - "Filter dropdown: All | Pending | In Progress | Done", - "Filter persists in URL params", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 4, - "passes": false, - "notes": "" - } - ] -} -``` - ---- - -## Archiving Previous Runs - -**Before writing a new prd.json, check if there is an existing one from a different feature:** - -1. Read the current `prd.json` if it exists -2. Check if `branchName` differs from the new feature's branch name -3. If different AND `progress.txt` has content beyond the header: - - Create archive folder: `archive/YYYY-MM-DD-feature-name/` - - Copy current `prd.json` and `progress.txt` to archive - - Reset `progress.txt` with fresh header - -**The ralph.sh script handles this automatically** when you run it, but if you are manually updating prd.json between runs, archive first. - ---- - -## Checklist Before Saving - -Before writing prd.json, verify: - -- [ ] **Previous run archived** (if prd.json exists with different branchName, archive it first) -- [ ] Each story is completable in one iteration (small enough) -- [ ] Stories are ordered by dependency (schema to backend to UI) -- [ ] Every story has "Typecheck passes" as criterion -- [ ] UI stories have "Verify in browser using dev-browser skill" as criterion -- [ ] Acceptance criteria are verifiable (not vague) -- [ ] No story depends on a later story diff --git a/.codex/skills/skills/ralph/prd.json b/.codex/skills/skills/ralph/prd.json deleted file mode 100644 index 56a4a41..0000000 --- a/.codex/skills/skills/ralph/prd.json +++ /dev/null @@ -1,588 +0,0 @@ -{ - "project": "GP Clinical Record — Depth Enhancement", - "branchName": "ralph/depth-enhancement", - "description": "Add depth, interactivity, and rich content to the GP clinical record dashboard: slide-in detail panels, sub-navigation, expanded skills/KPI data, career constellation D3 visualization, and login refresh. Full spec in Ralph/depth-design.md, requirements in Ralph/depth-requirements.md, workflow in Ralph/workflow_depth.md.", - "userStories": [ - { - "id": "US-001", - "title": "Clean up unused legacy components and hooks", - "description": "As a developer, I need to remove all dead code from the previous PMR interface so the codebase is clean before adding new features. Delete all files listed below and verify no dead imports remain.", - "acceptanceCriteria": [ - "Delete src/components/PMRInterface.tsx", - "Delete src/components/PatientBanner.tsx", - "Delete src/components/ClinicalSidebar.tsx", - "Delete src/components/Breadcrumb.tsx", - "Delete src/components/MobileBottomNav.tsx", - "Delete all files in src/components/views/ directory (SummaryView, ConsultationsView, MedicationsView, ProblemsView, InvestigationsView, DocumentsView, ReferralsView) and remove the views/ directory", - "Delete src/components/Contact.tsx, Education.tsx, Experience.tsx, FloatingNav.tsx, Footer.tsx, Hero.tsx, Projects.tsx, Skills.tsx (old portfolio components)", - "Delete src/hooks/useScrollCondensation.ts", - "Delete src/hooks/useActiveSection.ts (will be recreated in a later story)", - "Delete src/hooks/useScrollReveal.ts if unused", - "Verify no remaining files import any of the deleted files (fix any dead imports)", - "npm run build succeeds with zero errors", - "Typecheck passes" - ], - "priority": 1, - "passes": true, - "notes": "Completed iteration 1 at 2026-02-13 22:57. Model: opus." - }, - { - "id": "US-002", - "title": "Add new TypeScript types and CSS custom properties for depth features", - "description": "As a developer, I need new types and CSS foundations that subsequent stories will use. Add types to src/types/pmr.ts and CSS variables + keyframes to src/index.css. See Ralph/depth-design.md Section 4 for type definitions and Section 9 for CSS.", - "acceptanceCriteria": [ - "Add SkillCategory type: \u0027Technical\u0027 | \u0027Domain\u0027 | \u0027Leadership\u0027 to src/types/pmr.ts", - "Add KPIStory interface with fields: context (string), role (string), outcomes (string[]), period (string optional) to src/types/pmr.ts", - "Add optional story?: KPIStory field to existing KPI interface in src/types/pmr.ts", - "Add ConstellationNode interface (id, type: \u0027role\u0027|\u0027skill\u0027, label, shortLabel?, organization?, startYear?, endYear?, orgColor?, domain?) to src/types/pmr.ts", - "Add ConstellationLink interface (source, target, strength) to src/types/pmr.ts", - "Add DetailPanelContent discriminated union type (kpi | skill | skills-all | consultation | project | education | career-role) to src/types/pmr.ts", - "Add EducationExtra interface (documentId, extracurriculars?, researchDescription?, programmeDetail?) to src/types/pmr.ts", - "Add CSS custom properties to :root in src/index.css: --subnav-height: 36px, --panel-narrow: 400px, --panel-wide: 60vw, --backdrop-blur: 4px, --backdrop-bg: rgba(26,43,42,0.15)", - "Add @keyframes panel-slide-in (translateX 100% to 0), panel-slide-out (reverse), backdrop-fade-in (opacity 0 to 1) to src/index.css", - "Add prefers-reduced-motion overrides for all new keyframes (instant, no transform/opacity change)", - "Typecheck passes" - ], - "priority": 2, - "passes": true, - "notes": "Completed iteration 2 at 2026-02-13 22:59. Model: sonnet." - }, - { - "id": "US-003", - "title": "Create DetailPanelContext, DetailPanel component, and useFocusTrap hook", - "description": "As a developer, I need the core detail panel infrastructure: a context for managing panel state, the slide-in panel component, and a focus trap hook. Create 3 new files. The panel renders placeholder content for now (real renderers come later). See Ralph/depth-design.md Sections 2.1, 2.2 for full interface specs.", - "acceptanceCriteria": [ - "Create src/contexts/DetailPanelContext.tsx with DetailPanelProvider that manages: content (DetailPanelContent | null), openPanel, closePanel, isOpen", - "Width mapping is deterministic from content.type: kpi/skill/skills-all/education → \u0027narrow\u0027 (var(--panel-narrow)), consultation/project/career-role → \u0027wide\u0027 (var(--panel-wide))", - "Title mapping derives from content data (e.g., kpi → kpi.label, skill → skill.name, consultation → consultation.role)", - "Create src/components/DetailPanel.tsx: full-screen backdrop (var(--backdrop-bg) + backdrop-filter: blur(var(--backdrop-blur))) with panel sliding from right", - "Panel has header with X close button (lucide X icon), colored dot matching tile, and title text", - "Panel body is scrollable and renders placeholder text showing content type", - "Close triggers: backdrop click, Escape key, X button", - "ARIA: aria-modal=true, role=dialog, aria-labelledby pointing to title", - "Mobile (\u003c768px): both narrow and wide become 100vw", - "prefers-reduced-motion: instant appear, no slide animation", - "Create src/hooks/useFocusTrap.ts: useFocusTrap(containerRef, isActive) traps Tab/Shift+Tab within container when active, returns focus to previous element when deactivated", - "DetailPanel uses useFocusTrap when open", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 3, - "passes": true, - "notes": "Completed iteration 3 at 2026-02-13 23:03. Model: sonnet." - }, - { - "id": "US-004", - "title": "Create SubNav component and useActiveSection hook", - "description": "As a developer, I need a sticky sub-navigation bar below the TopBar for section jumping, plus a hook that tracks which section is visible. Create src/components/SubNav.tsx and src/hooks/useActiveSection.ts (the old one was deleted in cleanup). See Ralph/depth-design.md Section 2.3.", - "acceptanceCriteria": [ - "Create src/components/SubNav.tsx with 5 sections: Overview (patient-summary), Skills (core-skills), Experience (career-activity), Projects (projects), Education (education)", - "SubNav is sticky below TopBar (top: 48px, z-index: 99)", - "Height 36px, background var(--surface), bottom border var(--border-light)", - "Tabs: 13px font, weight 500, gap 24px, centered text", - "Active tab: teal underline (2px) with 200ms slide transition, text color var(--accent)", - "Inactive tabs: var(--text-secondary)", - "Click scrolls smoothly to [data-tile-id=tileId] element", - "Create src/hooks/useActiveSection.ts using IntersectionObserver to track visible tile by data-tile-id attribute", - "Maps tile IDs to section IDs: patient-summary→overview, core-skills→skills, career-activity→experience, projects→projects, education→education", - "SubNav accepts activeSection and onSectionClick props", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 4, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-13 23:06. Model: sonnet." - }, - { - "id": "US-005", - "title": "Expand skills data from 5 to ~20 with three categories", - "description": "As a developer, I need to expand src/data/skills.ts from 5 skills to ~21 skills across 3 categories. Source content from References/CV_v4.md Core Competencies. Each skill retains the medication metaphor (frequency, status, proficiency). See Ralph/depth-design.md Section 5.1 and Ralph/depth-requirements.md Section 4.4.", - "acceptanceCriteria": [ - "src/data/skills.ts has ~21 SkillMedication entries", - "Technical category (8): Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines", - "Healthcare Domain category (6): Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs", - "Strategic \u0026 Leadership category (7): Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication", - "Each skill has: id (kebab-case), name, frequency (medication-style: Daily, Twice daily, Once weekly, When required, etc.), startYear, yearsOfExperience, proficiency (0-100), category, status (Active/Historical), icon (lucide icon name)", - "Frequency and proficiency values are realistic based on CV_v4.md role descriptions", - "Typecheck passes" - ], - "priority": 5, - "passes": true, - "notes": "Completed iteration 5 at 2026-02-13 23:08. Model: sonnet." - }, - { - "id": "US-006", - "title": "Add KPI story data and update 4th KPI", - "description": "As a developer, I need to add rich story content to each KPI in src/data/kpis.ts for the detail panel, and change the 4th KPI from \u002712 Team Size Led\u0027 to \u00271.2M Population served\u0027. Source from References/CV_v4.md. See Ralph/depth-design.md Section 5.2.", - "acceptanceCriteria": [ - "Change 4th KPI from {id:\u0027team\u0027, value:\u002712\u0027, label:\u0027Team Size Led\u0027} to {id:\u0027population\u0027, value:\u00271.2M\u0027, label:\u0027Population Served\u0027, sub:\u0027Norfolk \u0026 Waveney ICS\u0027, colorVariant:\u0027teal\u0027}", - "Add story field (KPIStory) to all 4 KPIs with: context, role, outcomes[], period", - "£220M story: context about ICB prescribing budget for 1.2M population, role about forecasting models and ICB board accountability, outcomes about proactive financial planning", - "£14.6M story: context about efficiency programme, role about data analysis identification, outcomes about over-target performance", - "9+ Years story: context about career span Aug 2016-present, role about progression from community pharmacy to system-level leadership", - "1.2M story: context about Norfolk \u0026 Waveney ICS population, role about population health analytics and data-driven decision making", - "Add explanation field to 4th KPI matching the story context", - "Typecheck passes" - ], - "priority": 6, - "passes": true, - "notes": "Completed iteration 6 at 2026-02-13 23:10. Model: sonnet." - }, - { - "id": "US-007", - "title": "Create education extras data file", - "description": "As a developer, I need src/data/educationExtras.ts with expanded detail for the education detail panel. Source from References/CV_v4.md Education section. See Ralph/depth-design.md Section 5.4.", - "acceptanceCriteria": [ - "Create src/data/educationExtras.ts exporting educationExtras array of EducationExtra objects", - "MPharm entry (documentId matching doc-mpharm or equivalent from documents.ts): extracurriculars [\u0027President of UEA Pharmacy Society\u0027, \u0027Secretary \u0026 Vice-President of UEA Ultimate Frisbee\u0027, \u0027Publicity Officer for UEA Alzheimer\\\u0027s Society\u0027], researchDescription about cocrystal formation for drug delivery", - "Mary Seacole entry: programmeDetail about NHS leadership qualification, change management, healthcare leadership, system-level thinking", - "Document IDs match those used in src/data/documents.ts", - "Typecheck passes" - ], - "priority": 7, - "passes": true, - "notes": "Completed iteration 7 at 2026-02-13 23:11. Model: sonnet." - }, - { - "id": "US-008", - "title": "Restructure DashboardLayout with SubNav, new tile order, and DetailPanel", - "description": "As a developer, I need to update DashboardLayout.tsx to: wrap with DetailPanelProvider, add SubNav between TopBar and content, reorder tiles per the new layout, render DetailPanel, and adjust spacing. See Ralph/depth-design.md Section 3.1.", - "acceptanceCriteria": [ - "DashboardLayout (or App.tsx) wraps content with DetailPanelProvider from DetailPanelContext", - "SubNav renders between TopBar and the flex container", - "Content area marginTop accounts for both TopBar and SubNav: calc(var(--topbar-height) + var(--subnav-height))", - "Tile order: PatientSummaryTile (full), LatestResultsTile (half) + ProjectsTile (half) side-by-side, CoreSkillsTile (full), LastConsultationTile (full), CareerActivityTile (full), EducationTile (full)", - "DetailPanel component renders alongside CommandPalette", - "SubNav activeSection state managed via useActiveSection hook", - "All tiles have data-tile-id attributes (Card tileId prop)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 8, - "passes": true, - "notes": "Completed iteration 8 at 2026-02-13 23:15. Model: sonnet." - }, - { - "id": "US-009", - "title": "Create constellation data mapping file", - "description": "As a developer, I need src/data/constellation.ts defining the role-skill mapping for the D3 career constellation graph. Maps 6 career roles to their associated skills with connection strengths. See Ralph/depth-design.md Section 5.3 and 2.4.", - "acceptanceCriteria": [ - "Create src/data/constellation.ts with RoleSkillMapping interface (roleId: string, skillIds: string[])", - "Export roleSkillMappings array mapping 6 roles to skill IDs from skills.ts", - "Roles: pre-reg-pharmacist-2015, duty-pharmacy-manager-2016, pharmacy-manager-2017, hcd-pharmacist-2022, deputy-head-2024, interim-head-2025 (IDs should match or reference consultation IDs from consultations.ts)", - "Export constellationNodes array of ConstellationNode objects for all role nodes (with organization, startYear, endYear, orgColor) and skill nodes (with domain)", - "Export constellationLinks array of ConstellationLink objects connecting skills to roles with strength values (0-1)", - "Role orgColors: Paydens gets one color, Tesco another, NHS another (use distinct teal/blue/green tones)", - "Typecheck passes" - ], - "priority": 9, - "passes": true, - "notes": "Completed iteration 9 at 2026-02-13 23:17. Model: sonnet." - }, - { - "id": "US-010", - "title": "Modify LatestResultsTile: remove flip, bigger numbers, panel trigger", - "description": "As a developer, I need to redesign the KPI cards in LatestResultsTile.tsx: remove the CSS flip animation, make headline numbers larger and bolder, and make each card clickable to open the detail panel. See Ralph/depth-design.md Section 3.5.", - "acceptanceCriteria": [ - "Remove flip card animation entirely (no more .metric-card, .metric-card-inner, .metric-card-front, .metric-card-back CSS classes from index.css if they exist)", - "Each KPI renders as a clickable button/card with: value at 28-32px font-size, weight 700, colored by kpi.colorVariant", - "Label at 12px, weight 500, color var(--text-primary), marginTop 4px", - "Sub-text at 10px, font-family var(--font-geist-mono), color var(--text-tertiary), marginTop 2px", - "Click calls openPanel({ type: \u0027kpi\u0027, kpi }) from DetailPanelContext", - "Hover: border color shift + shadow deepens (transition 150ms)", - "Keyboard: Enter/Space triggers panel open", - "Card styling: padding 16px, background var(--surface), border 1px solid var(--border-light), border-radius var(--radius-sm)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 10, - "passes": true, - "notes": "Completed iteration 10 at 2026-02-13. Model: opus. Manually marked passed (script hung after story-complete signal)." - }, - { - "id": "US-011", - "title": "Modify CoreSkillsTile: full width, categorised groups, panel triggers", - "description": "As a developer, I need to redesign CoreSkillsTile.tsx as full-width with skills grouped by 3 categories, showing top 3-4 per category with \u0027view all\u0027 buttons. Individual skills and \u0027view all\u0027 trigger the detail panel. See Ralph/depth-design.md Section 3.4.", - "acceptanceCriteria": [ - "Card uses full prop (spans both grid columns)", - "Skills grouped by category: Technical, Healthcare Domain (Domain), Strategic \u0026 Leadership (Leadership)", - "Each category has a header: thin divider line with category label (styled like sidebar section dividers: 10px, uppercase, var(--text-tertiary))", - "Show top 3-4 skills per category on the dashboard tile (sorted by proficiency or relevance)", - "Each skill row is clickable → openPanel({ type: \u0027skill\u0027, skill }) from DetailPanelContext", - "Each category with \u003e4 skills shows a \u0027View all (N)\u0027 button → openPanel({ type: \u0027skills-all\u0027, category })", - "Retain medication metaphor display (frequency, status badge)", - "Remove old single-expand accordion for skills (replaced by panel)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 11, - "passes": true, - "notes": "Completed iteration 1 at 2026-02-13 23:50. Model: opus." - }, - { - "id": "US-012", - "title": "Modify ProjectsTile: half width, compact card grid, panel trigger", - "description": "As a developer, I need to change ProjectsTile.tsx from full-width to half-width (positioned alongside LatestResultsTile by the layout reorder in US-008). Compact cards with click to open detail panel. See Ralph/depth-design.md Section 3.6.", - "acceptanceCriteria": [ - "Remove full prop from Card (half-width, single grid column)", - "Compact project cards: status dot + name + year (right-aligned) per row", - "Tech stack shown as small inline tags", - "Each project card clickable → openPanel({ type: \u0027project\u0027, investigation }) from DetailPanelContext", - "Remove old in-place expansion (replaced by panel)", - "Hover: border color shift, shadow deepens", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 12, - "passes": true, - "notes": "Completed iteration 2 at 2026-02-13 23:52. Model: sonnet." - }, - { - "id": "US-013", - "title": "Modify LastConsultationTile: add panel trigger", - "description": "As a developer, I need to add a \u0027View full record\u0027 button to LastConsultationTile.tsx that opens the detail panel with full role details. See Ralph/depth-design.md Section 3.9.", - "acceptanceCriteria": [ - "Add \u0027View full record\u0027 link/button at the bottom of the tile", - "Click → openPanel({ type: \u0027consultation\u0027, consultation }) from DetailPanelContext, passing the first consultation entry", - "Make the tile header area also clickable (opens same panel)", - "Keep existing inline content (header info row, achievement bullets)", - "Hover state on clickable areas", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 13, - "passes": true, - "notes": "Completed iteration 3 at 2026-02-13 23:55. Model: sonnet." - }, - { - "id": "US-014", - "title": "Modify CareerActivityTile: panel triggers and hover preview", - "description": "As a developer, I need to change CareerActivityTile.tsx so timeline items click to open the detail panel instead of expanding in-place, and add hover previews. See Ralph/depth-design.md Section 3.7.", - "acceptanceCriteria": [ - "Role timeline items click → openPanel({ type: \u0027career-role\u0027, consultation }) from DetailPanelContext", - "Remove in-place accordion expansion for career items (replaced by panel)", - "Hover preview: items lift slightly on hover with shadow deepens, show 1-2 lines of preview text", - "Keep color-coded dots and entry type styling (teal roles, amber projects, green certs, purple education)", - "Reserve a container/placeholder for CareerConstellation component (will be added later)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 14, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-13 23:58. Model: sonnet." - }, - { - "id": "US-015", - "title": "Modify EducationTile: richer content, panel trigger", - "description": "As a developer, I need to enhance EducationTile.tsx with richer inline content and click-to-panel interaction. See Ralph/depth-design.md Section 3.8.", - "acceptanceCriteria": [ - "Show richer inline content: MPharm research project score (75.1%), OSCE score (80%), A-level grades (A* Maths, B Chemistry, C Politics)", - "Each education entry is clickable → openPanel({ type: \u0027education\u0027, document }) from DetailPanelContext", - "Hover: border color shift on clickable entries", - "Use education extras data from src/data/educationExtras.ts for inline detail where appropriate", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 15, - "passes": true, - "notes": "Completed iteration 4 at 2026-02-14 00:33. Model: sonnet." - }, - { - "id": "US-016", - "title": "Modify PatientSummaryTile: structured presentation with highlight strip", - "description": "As a developer, I need to improve PatientSummaryTile.tsx with the full CV_v4.md profile text and a visual highlight strip. See Ralph/depth-design.md Section 3.10 and Ralph/depth-requirements.md Section 4.1.", - "acceptanceCriteria": [ - "Verify src/data/profile.ts has the complete profile text from References/CV_v4.md (update if needed)", - "Add a visual highlight strip showing key stats: e.g. \u00279+ Years Experience\u0027, \u00271.2M Population\u0027, \u0027£220M Budget\u0027 as small styled badges or pills", - "Profile text is not a wall of text — use hierarchy: bold key phrases, structured paragraphs if needed", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 16, - "passes": true, - "notes": "" - }, - { - "id": "US-017", - "title": "Create KPIDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/KPIDetail.tsx that renders rich KPI story content inside the detail panel. Wire it into DetailPanel so content.type === \u0027kpi\u0027 renders this component. See Ralph/depth-design.md Section 6.1.", - "acceptanceCriteria": [ - "Create src/components/detail/KPIDetail.tsx accepting a KPI prop", - "Renders: headline number (large, colored by kpi.colorVariant), context paragraph (story.context), \u0027Your role\u0027 paragraph (story.role), outcome bullets (story.outcomes), period badge (story.period)", - "Graceful fallback if story is undefined (show kpi.explanation instead)", - "Wire into DetailPanel: when content.type === \u0027kpi\u0027, render \u003cKPIDetail kpi={content.kpi} /\u003e", - "Styling matches dashboard design system (fonts, colors, spacing)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 17, - "passes": true, - "notes": "" - }, - { - "id": "US-018", - "title": "Create ConsultationDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/ConsultationDetail.tsx for displaying full role details in the detail panel. Used for both \u0027consultation\u0027 and \u0027career-role\u0027 content types. See Ralph/depth-design.md Section 6.4.", - "acceptanceCriteria": [ - "Create src/components/detail/ConsultationDetail.tsx accepting a Consultation prop", - "Renders: role title + organization + dates, history paragraph (consultation.history), achievement bullets (consultation.examination), plan/outcomes (consultation.plan), coded entries as badges (consultation.codedEntries)", - "Wire into DetailPanel: content.type === \u0027consultation\u0027 or \u0027career-role\u0027 renders this component", - "Styled consistently with dashboard design system", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 18, - "passes": true, - "notes": "Already implemented by prior iteration. Component exists with full content, wired into DetailPanel for consultation and career-role types." - }, - { - "id": "US-019", - "title": "Create ProjectDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/ProjectDetail.tsx for displaying full project information in the wide detail panel. See Ralph/depth-design.md Section 6.5.", - "acceptanceCriteria": [ - "Create src/components/detail/ProjectDetail.tsx accepting an Investigation prop", - "Renders: project name + year + status badge, methodology description, tech stack as tags, results bullets, external link button (if investigation.externalUrl exists, opens in new tab)", - "Wire into DetailPanel: content.type === \u0027project\u0027 renders this component", - "External link uses rel=\u0027noopener noreferrer\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 19, - "passes": true, - "notes": "" - }, - { - "id": "US-020", - "title": "Create SkillDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/SkillDetail.tsx for displaying individual skill detail in the narrow detail panel. See Ralph/depth-design.md Section 6.2.", - "acceptanceCriteria": [ - "Create src/components/detail/SkillDetail.tsx accepting a SkillMedication prop", - "Renders: skill name + frequency + status badge, visual proficiency bar (0-100%), years of experience, category label", - "If constellation data is available, show \u0027Used in\u0027 section listing roles that used this skill (import from src/data/constellation.ts)", - "Wire into DetailPanel: content.type === \u0027skill\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 20, - "passes": true, - "notes": "Completed. Component renders skill header with frequency/status badges, category label, proficiency bar (color-coded), years of experience, and 'Used in' section from constellation data." - }, - { - "id": "US-021", - "title": "Create SkillsAllDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/SkillsAllDetail.tsx showing the full categorised list of all skills. Clicking an individual skill switches the panel to SkillDetail. See Ralph/depth-design.md Section 6.3.", - "acceptanceCriteria": [ - "Create src/components/detail/SkillsAllDetail.tsx", - "Shows full list grouped by Technical / Healthcare Domain / Strategic \u0026 Leadership", - "Category headers styled consistently with CoreSkillsTile category headers", - "Each skill row is clickable → calls openPanel({ type: \u0027skill\u0027, skill }) to switch panel content", - "If opened with a category filter (content.category), scroll to or highlight that category", - "Wire into DetailPanel: content.type === \u0027skills-all\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 21, - "passes": true, - "notes": "Completed. Full categorised skill list with category headers matching CoreSkillsTile style, proficiency mini-bars, click-to-skill-detail navigation, and category scroll/highlight from filter." - }, - { - "id": "US-022", - "title": "Create EducationDetail renderer for detail panel", - "description": "As a developer, I need src/components/detail/EducationDetail.tsx for displaying full education details including extras. See Ralph/depth-design.md Section 6.6.", - "acceptanceCriteria": [ - "Create src/components/detail/EducationDetail.tsx accepting a Document prop", - "Renders: title + institution + dates + classification", - "Imports educationExtras from src/data/educationExtras.ts and finds matching extra by document ID", - "If MPharm: shows research project description, extracurricular activities list", - "If Mary Seacole: shows programme detail", - "Shows notes from document data if present", - "Wire into DetailPanel: content.type === \u0027education\u0027 renders this component", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 22, - "passes": true, - "notes": "Completed. Renders title + icon + institution + dates + classification badge. Shows research description, OSCE score, extracurriculars (MPharm), programme detail (Mary Seacole), and notes." - }, - { - "id": "US-023", - "title": "Install D3 and scaffold CareerConstellation component", - "description": "As a developer, I need to install d3 as a dependency and create a scaffolded CareerConstellation component with an SVG container. See Ralph/depth-design.md Section 2.4.", - "acceptanceCriteria": [ - "Run npm install d3 @types/d3", - "Create src/components/CareerConstellation.tsx with props: onRoleClick(id: string), onSkillClick(id: string)", - "Component renders a responsive SVG container using useRef\u003cSVGSVGElement\u003e", - "Container: full width, height 400px desktop / 300px tablet / 250px mobile (use CSS or media queries)", - "SVG has viewBox for responsive scaling", - "Import constellation data from src/data/constellation.ts", - "Subtle radial gradient background from var(--bg-dashboard) center to var(--surface) edge", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 23, - "passes": true, - "notes": "Completed. D3 + @types/d3 installed. CareerConstellation scaffold with responsive SVG container (400/300/250px), radial gradient bg, ResizeObserver, callbacks ref for future D3 wiring." - }, - { - "id": "US-024", - "title": "Build D3 force-directed graph rendering in CareerConstellation", - "description": "As a developer, I need the D3 force simulation to render role and skill nodes with links in the CareerConstellation component. D3 operates imperatively via useEffect on the SVG ref. See Ralph/depth-design.md Section 2.4 for exact force configuration.", - "acceptanceCriteria": [ - "D3 force simulation with: forceManyBody(-200), forceLink(distance 80, strength from data), forceX chronological (roles positioned left-to-right by startYear), forceY centered, forceCollide(30)", - "Role nodes: 24px radius circles, filled with orgColor, white text label", - "Skill nodes: 10px radius, color-coded by domain: clinical=var(--success), technical=var(--accent), leadership=var(--amber)", - "Links: thin lines (1px), var(--border) color, opacity 0.3", - "D3 integration: useEffect on SVG ref, no React state for node positions", - "Simulation runs and nodes settle into stable positions", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 24, - "passes": true, - "notes": "Completed. D3 force simulation with forceManyBody(-200), forceLink(dist 80, strength from data), forceX chronological, forceY centered, forceCollide. Role nodes 24px with orgColor + white labels, skill nodes 10px color-coded by domain, links 1px opacity 0.3." - }, - { - "id": "US-025", - "title": "Add accessibility to CareerConstellation", - "description": "As a developer, I need the CareerConstellation to be accessible: keyboard navigable, screen-reader friendly, and respecting reduced motion. See Ralph/depth-design.md Section 2.4 accessibility notes.", - "acceptanceCriteria": [ - "SVG has role=img and aria-label describing the graph (\u0027Career constellation showing roles and skills across career timeline\u0027)", - "Screen-reader-only text description of graph structure (hidden visually, available to assistive tech)", - "Keyboard navigation: Tab through role nodes, Enter/Space opens detail panel for focused node", - "Focus indicators visible on keyboard-focused nodes", - "prefers-reduced-motion: disable force simulation animation, render nodes at calculated static positions immediately", - "Typecheck passes" - ], - "priority": 25, - "passes": true, - "notes": "Completed. SR-only description with role-skill mappings, hidden focusable buttons for keyboard nav (Tab/Enter/Space), focus ring on SVG nodes, prefers-reduced-motion runs simulation synchronously to static positions." - }, - { - "id": "US-026", - "title": "Add hover and click interactions to CareerConstellation", - "description": "As a developer, I need hover highlighting and click-to-panel interactions on the CareerConstellation. This connects the graph to the detail panel system. See Ralph/depth-design.md Section 2.4.", - "acceptanceCriteria": [ - "Hover role node: connected skill nodes scale up, links brighten to var(--accent), non-connected nodes fade to 0.15 opacity", - "Hover skill node: all connected role nodes highlight, link paths illuminate", - "Click role node: calls onRoleClick(id) prop", - "Click skill node: calls onSkillClick(id) prop", - "Integrate into CareerActivityTile: wire onRoleClick to open ConsultationDetail panel, onSkillClick to open SkillDetail panel", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 26, - "passes": true, - "notes": "Completed. D3 hover: connected nodes stay full opacity, non-connected fade to 0.15, links brighten to teal. Click: role→onRoleClick, skill→onSkillClick. Wired into CareerActivityTile replacing placeholder, connected to detail panel." - }, - { - "id": "US-027", - "title": "Restyle LoginScreen with teal accents", - "description": "As a developer, I need to visually refresh the LoginScreen with teal accents replacing the current blue. See Ralph/depth-design.md Section 3.3 and Ralph/depth-requirements.md Section 5.", - "acceptanceCriteria": [ - "Replace #005EB8 with #0D6E6E throughout LoginScreen (shield icon bg, active field border, cursor, button)", - "Replace #004D9F with #0A8080 (button hover state)", - "Replace #004494 with #085858 (button pressed state)", - "Background color: keep #1E293B or change to #1A2B2A", - "Login card feels cohesive with the dashboard teal palette", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 27, - "passes": true, - "notes": "Completed. Replaced #005EB8→#0D6E6E, #004D9F→#0A8080, #004494→#085858, background #1E293B→#1A2B2A, shield rgba updated." - }, - { - "id": "US-028", - "title": "Change login username to a.recruiter and add connection status indicator", - "description": "As a developer, I need to change the typed username from a.charlwood to a.recruiter and add a connection status indicator below the login button. See Ralph/depth-design.md Section 3.3.", - "acceptanceCriteria": [ - "Username typed in login animation is \u0027a.recruiter\u0027 (not \u0027A.CHARLWOOD\u0027 or similar)", - "Connection status indicator appears below the login button: 6px dot + text", - "Initial state: red/alert dot + \u0027Awaiting secure connection...\u0027 (var(--alert) color)", - "After ~2000ms: dot transitions to green + \u0027Secure connection established\u0027 (var(--success) color, 300ms transition)", - "Text: 10px, font-family var(--font-geist-mono), color var(--text-tertiary)", - "Login button disabled until BOTH typing is complete AND connectionState === \u0027connected\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 28, - "passes": true, - "notes": "Completed. Username changed to a.recruiter, connection status indicator with red→green 300ms transition, button disabled until typing complete AND connected." - }, - { - "id": "US-029", - "title": "Add post-login loading state and update TopBar session name", - "description": "As a developer, I need a brief loading state after clicking the login button before the dashboard appears, and the TopBar should show A.RECRUITER as the session user. See Ralph/depth-design.md Sections 3.3 and 3.2.", - "acceptanceCriteria": [ - "On login button click: isLoading=true, card content replaced with spinner + \u0027Loading clinical records...\u0027 text", - "Loading state lasts ~600ms, then calls onComplete() to transition to dashboard", - "Spinner is a CSS-animated spinner (not a GIF), styled with var(--accent) or similar", - "Loading text: 12px, color var(--text-secondary)", - "In TopBar.tsx: change session display name from \u0027Dr. A.CHARLWOOD\u0027 (or current value) to \u0027A.RECRUITER\u0027", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 29, - "passes": true, - "notes": "Completed. Loading state with CSS spinner replaces card content on login click (~600ms), TopBar shows A.RECRUITER, prefers-reduced-motion skips spinner animation." - }, - { - "id": "US-030", - "title": "Update CommandPalette for expanded content and panel actions", - "description": "As a developer, I need to update the CommandPalette search index and actions to work with the expanded skills data (~20 skills) and add actions that open the detail panel directly. See Ralph/depth-design.md Section 10, Phase 6.", - "acceptanceCriteria": [ - "Search index in src/lib/search.ts includes all ~21 skills (not just the original 5)", - "Selecting a skill result opens the detail panel for that skill (openPanel call or dispatch event)", - "Selecting a KPI result opens the KPI detail panel", - "Selecting a project result opens the project detail panel", - "Ensure DashboardLayout handlePaletteAction supports a new \u0027panel\u0027 action type or adapts existing types to trigger detail panel", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 30, - "passes": true, - "notes": "Completed. All 21 skills in search index, panel action type added. Skills/KPIs/projects open detail panel directly from command palette." - }, - { - "id": "US-031", - "title": "Responsive testing and fixes for all new components", - "description": "As a developer, I need to verify and fix responsive behavior for the detail panel, sub-nav, constellation, and restructured layout at all breakpoints.", - "acceptanceCriteria": [ - "DetailPanel: both narrow and wide render as 100vw on mobile (\u003c768px)", - "SubNav: works on tablet/mobile (horizontal scroll if needed, no overflow)", - "CareerConstellation: renders at 300px height on tablet, 250px on mobile", - "Projects + KPIs: stack vertically on mobile when grid falls to single column", - "CoreSkillsTile: full-width layout works on all breakpoints", - "All interactive elements have touch targets \u003e= 44px on mobile", - "No horizontal overflow at 375px viewport width", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 31, - "passes": true, - "notes": "Completed. SubNav horizontal scroll with hidden scrollbar, 44px min touch targets on all interactive elements, DetailPanel close button enlarged to 44px." - }, - { - "id": "US-032", - "title": "Reduced motion audit, final cleanup, and visual review", - "description": "As a developer, I need to verify all new animations respect prefers-reduced-motion, remove any dead code introduced during development, and do a final build verification.", - "acceptanceCriteria": [ - "DetailPanel slide animation: instant appear with prefers-reduced-motion", - "Backdrop fade: instant with prefers-reduced-motion", - "SubNav underline transition: instant with prefers-reduced-motion", - "CareerConstellation: static layout (no force simulation animation) with prefers-reduced-motion", - "Connection status dot transition: instant with prefers-reduced-motion", - "Post-login spinner: static indicator with prefers-reduced-motion", - "No dead imports across all files", - "Remove any unused flip card CSS (.metric-card-inner etc.) if still present in index.css", - "npm run build succeeds cleanly", - "npm run typecheck passes with zero errors", - "npm run lint passes (pre-existing AccessibilityContext warning OK)", - "Typecheck passes" - ], - "priority": 32, - "passes": true, - "notes": "Completed. Reduced motion overrides for SubNav, connection status, smooth scroll. Created ProjectDetail renderer. Removed unused files (useBreakpoint.ts, profile.ts), legacy PMR CSS variables, placeholder fallback. Build/typecheck/lint all clean." - } - ] -} diff --git a/.codex/skills/skills/ralph/progress.txt b/.codex/skills/skills/ralph/progress.txt deleted file mode 100644 index 6b27dc7..0000000 --- a/.codex/skills/skills/ralph/progress.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Ralph Progress — GP Clinical Record Depth Enhancement - -Branch: ralph/depth-enhancement -Stories: 32 (US-001 through US-032) - ---- - -## Status - -No iterations completed yet. -2026-02-13 22:57 | PASS | US-001: Clean up unused legacy components and hooks | model=opus elapsed=01:58 tools=18 -2026-02-13 22:59 | PASS | US-002: Add new TypeScript types and CSS custom properties for depth features | model=sonnet elapsed=01:54 tools=11 -2026-02-13 23:03 | PASS | US-003: Create DetailPanelContext, DetailPanel component, and useFocusTrap hook | model=sonnet elapsed=03:39 tools=22 -2026-02-13 23:06 | PASS | US-004: Create SubNav component and useActiveSection hook | model=sonnet elapsed=02:54 tools=18 -2026-02-13 23:08 | PASS | US-005: Expand skills data from 5 to ~20 with three categories | model=sonnet elapsed=01:58 tools=11 -2026-02-13 23:10 | PASS | US-006: Add KPI story data and update 4th KPI | model=sonnet elapsed=01:59 tools=9 -2026-02-13 23:11 | PASS | US-007: Create education extras data file | model=sonnet elapsed=01:25 tools=10 -2026-02-13 23:15 | PASS | US-008: Restructure DashboardLayout with SubNav, new tile order, and DetailPanel | model=sonnet elapsed=03:10 tools=27 -2026-02-13 23:17 | PASS | US-009: Create constellation data mapping file | model=sonnet elapsed=02:20 tools=10 -2026-02-13 23:50 | PASS | US-011: Modify CoreSkillsTile: full width, categorised groups, panel triggers | model=opus elapsed=02:54 tools=22 -2026-02-13 23:52 | PASS | US-012: Modify ProjectsTile: half width, compact card grid, panel trigger | model=sonnet elapsed=02:16 tools=11 -2026-02-13 23:55 | PASS | US-013: Modify LastConsultationTile: add panel trigger | model=sonnet elapsed=02:20 tools=15 -2026-02-13 23:58 | PASS | US-014: Modify CareerActivityTile: panel triggers and hover preview | model=sonnet elapsed=02:49 tools=14 diff --git a/.codex/skills/skills/ralph/ralph.ps1 b/.codex/skills/skills/ralph/ralph.ps1 deleted file mode 100644 index 9e8ea29..0000000 --- a/.codex/skills/skills/ralph/ralph.ps1 +++ /dev/null @@ -1,568 +0,0 @@ -<# -.SYNOPSIS - Ralph Wiggum Loop - PRD-driven variant. - -.DESCRIPTION - Iterates through user stories in prd.json, spawning a fresh `claude --print` - invocation for each story. Memory persists via filesystem only: git commits, - prd.json (passes field), and progress.txt. - - Each iteration works on ONE user story (in priority order). - When all stories pass, the loop completes. - - Circuit breakers prevent runaway costs: - - No git changes for N consecutive iterations (stalled) - - Same error repeated N consecutive iterations (stuck) - -.PARAMETER Model - Initial Claude model to use. Default: "opus". The agent can dynamically switch - models between iterations via opus|sonnet signals. - -.PARAMETER MaxNoProgress - Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3. - -.PARAMETER MaxSameError - Number of consecutive iterations with the same error before circuit breaker trips. Default: 3. - -.PARAMETER StartFrom - Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed. - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -Model "opus" - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet" -#> - -param( - [string]$Model = "opus", - [int]$MaxNoProgress = 3, - [int]$MaxSameError = 3, - [string]$StartFrom = "" -) - -$ErrorActionPreference = "Stop" - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$prdFile = Join-Path $scriptDir "prd.json" -$progressFile = Join-Path $scriptDir "progress.txt" -$logDir = Join-Path $scriptDir "logs" - -# --- Find project root (git repo root) --- - -$projectRoot = git rev-parse --show-toplevel 2>$null -if (-not $projectRoot) { - Write-Error "Not inside a git repository. Run from the project directory." - exit 1 -} -$projectRoot = (Resolve-Path $projectRoot).Path - -# --- Validation --- - -if (-not (Test-Path $prdFile)) { - Write-Error "prd.json not found at $prdFile" - exit 1 -} - -# Ensure logs directory exists -if (-not (Test-Path $logDir)) { - New-Item -ItemType Directory -Path $logDir | Out-Null - Write-Host "Created logs directory" -} - -# --- PRD Read/Write --- - -function Read-Prd { - Get-Content -Path $prdFile -Raw | ConvertFrom-Json -} - -function Save-Prd { - param($prdObj) - $prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8 -} - -$prd = Read-Prd - -# --- Git Setup --- - -$BranchName = $prd.branchName - -if ($BranchName) { - $currentBranch = git branch --show-current - if ($currentBranch -ne $BranchName) { - $branchExists = git branch --list $BranchName - if ($branchExists) { - Write-Host "Switching to existing branch: $BranchName" - git checkout $BranchName - } else { - Write-Host "Creating branch: $BranchName" - git checkout -b $BranchName - } - } -} - -# --- Handle StartFrom: mark earlier stories as passed --- - -if ($StartFrom) { - $startPriority = [int]($StartFrom -replace 'US-0*', '') - $skippedCount = 0 - foreach ($story in $prd.userStories) { - $storyPriority = [int]($story.id -replace 'US-0*', '') - if ($storyPriority -lt $startPriority -and $story.passes -ne $true) { - $story.passes = $true - $story.notes = "Skipped (--StartFrom $StartFrom)" - $skippedCount++ - } - } - if ($skippedCount -gt 0) { - Save-Prd $prd - Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow - } -} - -# --- Circuit Breaker State --- - -$noProgressCount = 0 -$lastErrorSignature = "" -$sameErrorCount = 0 - -# --- Prompt Generation --- - -function Build-StoryPrompt { - param( - $story, - $prdObj, - [array]$completedStories - ) - - # Build completed list - $completedSection = "" - if ($completedStories.Count -gt 0) { - $completedLines = ($completedStories | ForEach-Object { - "- $($_.id): $($_.title)" - }) -join "`n" - $completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n" - } - - # Build criteria list - $criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n" - - # Build prompt using array-join (avoids PS 5.1 here-string indentation issues) - $sid = $story.id - $stitle = $story.title - $sdesc = $story.description - $pdesc = $prdObj.description - - $prompt = @( - "# Ralph Iteration: $sid - $stitle" - "" - "## Project" - "$pdesc" - "" - "Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work." - "" - "## Your Task" - "" - "**${sid}: $stitle**" - "" - "$sdesc" - "" - "## Acceptance Criteria" - "" - "$criteriaLines" - "" - "## Reference Documents" - "" - "Read these as needed for implementation detail:" - "" - "- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)" - "- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models" - "- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns" - "- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)" - "$completedSection" - "## Workflow" - "" - "1. Read CLAUDE.md to understand project conventions" - "2. Read Ralph/depth-design.md sections relevant to this story" - "3. Read existing source files you will modify to understand current patterns" - "4. Implement ALL acceptance criteria" - "5. Run npm run typecheck - fix any type errors" - "6. Run npm run build - fix any build errors" - "7. Stage and commit your changes:" - " git add [specific files] && git commit -m `"${sid}: [descriptive message]`"" - "8. When ALL criteria are met, output: $sid" - "" - "## Rules" - "" - "- Work ONLY on $sid. Do not modify code for other stories." - "- Read files before modifying them." - "- Follow existing patterns and conventions in the codebase." - "- Use lucide-react for icons, never unicode symbols." - "- Use the project's CSS custom properties and Tailwind tokens." - "- Commit specific files, not git add -A." - "- Do NOT start a dev server (npm run dev). One is already running on port $devServerPort. Do NOT run any background tasks." - "- If genuinely blocked, output $sid with explanation." - "- To recommend a different model for the NEXT iteration, output opus or sonnet." - ) -join "`n" - - return $prompt -} - -# --- Banner --- - -$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$totalCount = $prd.userStories.Count - -Write-Host "" -Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan -Write-Host "Project: $($prd.project)" -ForegroundColor Cyan -Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan -Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan -Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host "" - -# Dev server port (assumed to be running externally) -$devServerPort = 5173 -Write-Host "Dev server assumed running on port $devServerPort" -ForegroundColor DarkGray -Write-Host "" - -# --- Story Loop --- - -$iterationCount = 0 -$originalDir = Get-Location -Set-Location $projectRoot - -try { - -while ($true) { - # Re-read PRD each iteration (in case previous iteration updated it) - $prd = Read-Prd - - # Partition stories - $completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true }) - $pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority }) - - # Check if all done - if ($pendingStories.Count -eq 0) { - Write-Host "" - Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green - Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green - Write-Host "Branch: $BranchName" -ForegroundColor Green - break - } - - $currentStory = $pendingStories[0] - $iterationCount++ - $pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100) - - $storyLabel = "$($currentStory.id): $($currentStory.title)" - $pctStr = "${pctComplete}%" - $progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)" - - Write-Host "" - Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow - Write-Host $progressMsg -ForegroundColor DarkGray - - # Record HEAD before this iteration - $headBefore = git rev-parse HEAD 2>$null - - $iterStart = Get-Date - Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray - Write-Host "" - - # Generate prompt for this story - $promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories - - # --- Spawn Claude --- - - $logFile = Join-Path $logDir "$($currentStory.id).log" - $rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl" - $maxRetries = 10 - $retryCount = 0 - $outputString = "" - $apiOverloaded = $false - - do { - $apiOverloaded = $false - $textBuilder = [System.Text.StringBuilder]::new() - $toolCount = 0 - - # Clear raw log file for this attempt - if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force } - - if ($retryCount -gt 0) { - $backoffSeconds = [Math]::Pow(2, $retryCount - 1) - Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow - Start-Sleep -Seconds $backoffSeconds - Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray - } - - # --- Spawn Claude via Process.Start for clean shutdown control --- - # Using Process.Start instead of pipeline so we can break on the result - # event and force-kill the process tree. The pipeline approach hangs when - # Claude spawns background tasks (e.g. npm run dev) that keep stdout open. - - $promptTempFile = Join-Path $logDir "$($currentStory.id).prompt.tmp" - $promptContent | Set-Content -Path $promptTempFile -Encoding UTF8 - - $claudeArgs = "--print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json" - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = "cmd.exe" - $psi.Arguments = "/c type `"$promptTempFile`" | claude $claudeArgs" - $psi.UseShellExecute = $false - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.CreateNoWindow = $true - $psi.WorkingDirectory = $projectRoot - - $claudeProc = [System.Diagnostics.Process]::Start($psi) - - # Drain stderr async to prevent buffer deadlock - $claudeProc.add_ErrorDataReceived({ param($s,$e) }) - $claudeProc.BeginErrorReadLine() - - try { - while ($null -ne ($line = $claudeProc.StandardOutput.ReadLine())) { - $line = $line.Trim() - if (-not $line) { continue } - - # Save raw event for debugging - try { - Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue - } catch { } - - $isResultEvent = $false - try { - $evt = $line | ConvertFrom-Json -ErrorAction Stop - - # --- Tool use start --- - if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') { - $toolCount++ - $toolName = $evt.content_block.name - Write-Host " [$toolName]" -ForegroundColor DarkCyan - } - # --- Streaming text --- - elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) { - Write-Host -NoNewline $evt.delta.text - [void]$textBuilder.Append($evt.delta.text) - } - # --- Result event (terminal — stop reading after this) --- - elseif ($evt.type -eq 'result') { - if ($evt.subtype -eq 'error_result' -and $evt.error) { - Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red - [void]$textBuilder.AppendLine("ERROR: $($evt.error)") - } - elseif ($evt.result) { - [void]$textBuilder.AppendLine($evt.result) - } - $isResultEvent = $true - } - # --- Message-level content --- - elseif ($evt.message -and $evt.message.content) { - foreach ($block in $evt.message.content) { - if ($block.type -eq 'text' -and $block.text) { - Write-Host $block.text - [void]$textBuilder.AppendLine($block.text) - } - elseif ($block.type -eq 'tool_use') { - $toolCount++ - Write-Host " [$($block.name)]" -ForegroundColor DarkCyan - } - } - } - } catch { - if ($line -and $line -notmatch '^\s*["\{\[\}\]]') { - Write-Host $line -ForegroundColor DarkYellow - [void]$textBuilder.AppendLine($line) - } - } - - # Result is always the final stream event — stop reading - if ($isResultEvent) { break } - } - } finally { - # Kill the Claude process tree to prevent orphaned cmd.exe/node processes - if ($claudeProc -and -not $claudeProc.HasExited) { - try { - taskkill /T /F /PID $claudeProc.Id 2>$null | Out-Null - } catch { } - } - Remove-Item -Path $promptTempFile -ErrorAction SilentlyContinue - } - - $outputString = $textBuilder.ToString() - - # Check for 529 overloaded error - if ($outputString -match "529.*overloaded|overloaded_error") { - $apiOverloaded = $true - $retryCount++ - if ($retryCount -ge $maxRetries) { - Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red - } - } - # Check for usage limit with cooldown - elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") { - $resetHour = [int]$Matches[1] - $resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 } - $resetAmPm = $Matches[3] - - if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 } - elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 } - - $now = Get-Date - $resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0 - if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) } - $resetTime = $resetTime.AddMinutes(2) - - $waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds) - $waitMinutes = [Math]::Ceiling($waitSeconds / 60) - - Write-Host "" - Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green - - $apiOverloaded = $true - } - } while ($apiOverloaded -and $retryCount -lt $maxRetries) - - # Save log - $outputString | Set-Content -Path $logFile -Encoding UTF8 - - # Show elapsed time - $elapsed = (Get-Date) - $iterStart - Write-Host "" - Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray - - # --- Detect signals --- - - $storyComplete = $outputString -match "$([regex]::Escape($currentStory.id))" - $storyBlocked = $outputString -match "$([regex]::Escape($currentStory.id))" - $headAfter = git rev-parse HEAD 2>$null - $hasGitChanges = $headAfter -ne $headBefore - - # --- Update story status --- - - if ($storyComplete) { - # Mark story as passed in prd.json - $prd = Read-Prd - $storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id } - if ($storyToUpdate) { - $alreadyDone = if (-not $hasGitChanges) { " (already committed)" } else { "" } - $storyToUpdate.passes = $true - $storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model.$alreadyDone" - } - Save-Prd $prd - - # Append to progress.txt - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $el = $elapsed.ToString('mm\:ss') - $tag = if ($hasGitChanges) { "PASS" } else { "PASS (no new commits)" } - $progressEntry = "$ts | $tag | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - - Write-Host " [PASSED] $storyLabel" -ForegroundColor Green - if (-not $hasGitChanges) { - Write-Host " (Work was already committed)" -ForegroundColor DarkGray - } - $noProgressCount = 0 - $sameErrorCount = 0 - $lastErrorSignature = "" - } - elseif ($storyBlocked) { - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | BLOCKED | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red - # Blocked counts as no progress - $noProgressCount++ - } - else { - # No completion signal - if ($hasGitChanges) { - Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | PARTIAL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - $noProgressCount = 0 - } else { - Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow - $noProgressCount++ - } - } - - # --- Circuit Breaker: No Progress --- - - if ($noProgressCount -ge $MaxNoProgress) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red - Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red - Write-Host "Stuck on: $($currentStory.id) - $($currentStory.title)" -ForegroundColor Red - Write-Host "Check $logFile for details." -ForegroundColor Red - break - } - - # --- Circuit Breaker: Repeated Error --- - - $errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches - if ($errorLines) { - $filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3 - $currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|" - if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) { - $sameErrorCount++ - Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow - if ($sameErrorCount -ge $MaxSameError) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red - Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red - Write-Host " $currentErrorSignature" -ForegroundColor Red - break - } - } elseif ($currentErrorSignature) { - $sameErrorCount = 0 - } - $lastErrorSignature = $currentErrorSignature - } else { - $sameErrorCount = 0 - $lastErrorSignature = "" - } - - # --- Dynamic Model Selection --- - - if ($outputString -match "(opus|sonnet)") { - $nextModel = $Matches[1] - if ($nextModel -ne $Model) { - Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta - $Model = $nextModel - } - } - - # Brief pause between iterations - Start-Sleep -Seconds 2 -} - -} finally { - Set-Location $originalDir -} - -# --- Final Summary --- - -$prd = Read-Prd -$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$finalTotal = $prd.userStories.Count - -Write-Host "" -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan -Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan -Write-Host " Branch: $BranchName" -ForegroundColor Cyan -Write-Host " Logs: $logDir" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan - -if ($finalPassed -eq $finalTotal) { - exit 0 -} else { - exit 1 -} - diff --git a/.codex/skills/skills/ralph/ralph1.ps1 b/.codex/skills/skills/ralph/ralph1.ps1 deleted file mode 100644 index 9c89be6..0000000 --- a/.codex/skills/skills/ralph/ralph1.ps1 +++ /dev/null @@ -1,582 +0,0 @@ -<# -.SYNOPSIS - Ralph Wiggum Loop — PRD-driven variant. - -.DESCRIPTION - Iterates through user stories in prd.json, spawning a fresh `claude --print` - invocation for each story. Memory persists via filesystem only: git commits, - prd.json (passes field), and progress.txt. - - Each iteration works on ONE user story (in priority order). - When all stories pass, the loop completes. - - Circuit breakers prevent runaway costs: - - No git changes for N consecutive iterations (stalled) - - Same error repeated N consecutive iterations (stuck) - -.PARAMETER Model - Initial Claude model to use. Default: "opus". The agent can dynamically switch - models between iterations via opus|sonnet signals. - -.PARAMETER MaxNoProgress - Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3. - -.PARAMETER MaxSameError - Number of consecutive iterations with the same error before circuit breaker trips. Default: 3. - -.PARAMETER StartFrom - Story ID to start from (e.g., "US-005"). Treats all earlier stories as already passed. - -.PARAMETER SkipVerify - Skip post-iteration typecheck verification. Faster but less safe. - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -Model "opus" - -.EXAMPLE - .\.claude\skills\ralph\ralph.ps1 -StartFrom "US-010" -Model "sonnet" -#> - -param( - [string]$Model = "opus", - [int]$MaxNoProgress = 3, - [int]$MaxSameError = 3, - [string]$StartFrom = "", - [switch]$SkipVerify -) - -$ErrorActionPreference = "Stop" - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$prdFile = Join-Path $scriptDir "prd.json" -$progressFile = Join-Path $scriptDir "progress.txt" -$logDir = Join-Path $scriptDir "logs" - -# --- Find project root (git repo root) --- - -$projectRoot = git rev-parse --show-toplevel 2>$null -if (-not $projectRoot) { - Write-Error "Not inside a git repository. Run from the project directory." - exit 1 -} -$projectRoot = (Resolve-Path $projectRoot).Path - -# --- Validation --- - -if (-not (Test-Path $prdFile)) { - Write-Error "prd.json not found at $prdFile" - exit 1 -} - -# Ensure logs directory exists -if (-not (Test-Path $logDir)) { - New-Item -ItemType Directory -Path $logDir | Out-Null - Write-Host "Created logs directory" -} - -# --- PRD Read/Write --- - -function Read-Prd { - Get-Content -Path $prdFile -Raw | ConvertFrom-Json -} - -function Save-Prd { - param($prdObj) - $prdObj | ConvertTo-Json -Depth 10 | Set-Content -Path $prdFile -Encoding UTF8 -} - -$prd = Read-Prd - -# --- Git Setup --- - -$BranchName = $prd.branchName - -if ($BranchName) { - $currentBranch = git branch --show-current - if ($currentBranch -ne $BranchName) { - $branchExists = git branch --list $BranchName - if ($branchExists) { - Write-Host "Switching to existing branch: $BranchName" - git checkout $BranchName - } else { - Write-Host "Creating branch: $BranchName" - git checkout -b $BranchName - } - } -} - -# --- Handle StartFrom: mark earlier stories as passed --- - -if ($StartFrom) { - $startPriority = [int]($StartFrom -replace 'US-0*', '') - $skippedCount = 0 - foreach ($story in $prd.userStories) { - $storyPriority = [int]($story.id -replace 'US-0*', '') - if ($storyPriority -lt $startPriority -and $story.passes -ne $true) { - $story.passes = $true - $story.notes = "Skipped (--StartFrom $StartFrom)" - $skippedCount++ - } - } - if ($skippedCount -gt 0) { - Save-Prd $prd - Write-Host "Marked $skippedCount stories before $StartFrom as skipped." -ForegroundColor DarkYellow - } -} - -# --- Circuit Breaker State --- - -$noProgressCount = 0 -$lastErrorSignature = "" -$sameErrorCount = 0 - -# --- Prompt Generation --- - -function Build-StoryPrompt { - param( - $story, - $prdObj, - [array]$completedStories - ) - - # Build completed list - $completedSection = "" - if ($completedStories.Count -gt 0) { - $completedLines = ($completedStories | ForEach-Object { - "- $($_.id): $($_.title)" - }) -join "`n" - $completedSection = "`n## Previously Completed Stories (do not redo)`n$completedLines`n" - } - - # Build criteria list - $criteriaLines = ($story.acceptanceCriteria | ForEach-Object { "- [ ] $_" }) -join "`n" - - # Build prompt using array-join (avoids PS 5.1 here-string indentation issues) - $sid = $story.id - $stitle = $story.title - $sdesc = $story.description - $pdesc = $prdObj.description - - $prompt = @( - "# Ralph Iteration: $sid - $stitle" - "" - "## Project" - "$pdesc" - "" - "Read CLAUDE.md for full project conventions, architecture, and design system. This is mandatory before starting work." - "" - "## Your Task" - "" - "**${sid}: $stitle**" - "" - "$sdesc" - "" - "## Acceptance Criteria" - "" - "$criteriaLines" - "" - "## Reference Documents" - "" - "Read these as needed for implementation detail:" - "" - "- **CLAUDE.md** - Project conventions, architecture, design tokens, guardrails (READ FIRST)" - "- **Ralph/depth-design.md** - Component architecture, props interfaces, CSS specs, data models" - "- **Ralph/depth-requirements.md** - Full requirements with content sources and UX patterns" - "- **References/CV_v4.md** - Source of truth for all CV content (roles, dates, achievements, numbers)" - "$completedSection" - "## Workflow" - "" - "1. Read CLAUDE.md to understand project conventions" - "2. Read Ralph/depth-design.md sections relevant to this story" - "3. Read existing source files you will modify to understand current patterns" - "4. Implement ALL acceptance criteria" - "5. Run npm run typecheck - fix any type errors" - "6. Run npm run build - fix any build errors" - "7. Stage and commit your changes:" - " git add [specific files] && git commit -m `"${sid}: [descriptive message]`"" - "8. When ALL criteria are met, output: $sid" - "" - "## Rules" - "" - "- Work ONLY on $sid. Do not modify code for other stories." - "- Read files before modifying them." - "- Follow existing patterns and conventions in the codebase." - "- Use lucide-react for icons, never unicode symbols." - "- Use the project's CSS custom properties and Tailwind tokens." - "- Commit specific files, not git add -A." - "- If genuinely blocked, output $sid with explanation." - "- To recommend a different model for the NEXT iteration, output opus or sonnet." - ) -join "`n" - - return $prompt -} - -# --- Banner --- - -$completedCount = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$totalCount = $prd.userStories.Count - -Write-Host "" -Write-Host "===== Ralph Wiggum Loop (PRD-driven) =====" -ForegroundColor Cyan -Write-Host "Project: $($prd.project)" -ForegroundColor Cyan -Write-Host "Branch: $BranchName | Model: $Model (dynamic switching enabled)" -ForegroundColor Cyan -Write-Host "Stories: $completedCount/$totalCount complete" -ForegroundColor Cyan -Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan -if (-not $SkipVerify) { Write-Host "Post-iteration typecheck verification: ON" -ForegroundColor Cyan } -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host "" - -# --- Dev Server --- - -$devServerPort = 5173 -$devServerPid = $null - -try { - $null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop - Write-Host "Dev server detected on port $devServerPort" -ForegroundColor Green -} catch { - Write-Host "Starting dev server (port $devServerPort)..." -ForegroundColor Cyan - $devProc = Start-Process -FilePath "npm.cmd" -ArgumentList "run", "dev" -WorkingDirectory $projectRoot -PassThru -WindowStyle Minimized - $devServerPid = $devProc.Id - - for ($w = 1; $w -le 20; $w++) { - Start-Sleep -Seconds 1 - try { - $null = Invoke-WebRequest -Uri "http://localhost:$devServerPort" -TimeoutSec 2 -ErrorAction Stop - Write-Host "Dev server ready on port $devServerPort" -ForegroundColor Green - break - } catch { - if ($w -eq 20) { - Write-Warning "Dev server may not be ready — visual review steps may fail" - } - } - } -} -Write-Host "" - -# --- Story Loop --- - -$iterationCount = 0 -$originalDir = Get-Location -Set-Location $projectRoot - -try { - -while ($true) { - # Re-read PRD each iteration (in case previous iteration updated it) - $prd = Read-Prd - - # Partition stories - $completedStories = @($prd.userStories | Where-Object { $_.passes -eq $true }) - $pendingStories = @($prd.userStories | Where-Object { $_.passes -ne $true } | Sort-Object { $_.priority }) - - # Check if all done - if ($pendingStories.Count -eq 0) { - Write-Host "" - Write-Host "===== ALL STORIES COMPLETE =====" -ForegroundColor Green - Write-Host "$($completedStories.Count)/$($prd.userStories.Count) stories passed." -ForegroundColor Green - Write-Host "Branch: $BranchName" -ForegroundColor Green - break - } - - $currentStory = $pendingStories[0] - $iterationCount++ - $pctComplete = [math]::Round(($completedStories.Count / $prd.userStories.Count) * 100) - - $storyLabel = "$($currentStory.id): $($currentStory.title)" - $pctStr = "${pctComplete}%" - $progressMsg = " Progress: $($completedStories.Count)/$($prd.userStories.Count) ($pctStr) - Remaining: $($pendingStories.Count)" - - Write-Host "" - Write-Host "--- Iteration $iterationCount - $storyLabel ---" -ForegroundColor Yellow - Write-Host $progressMsg -ForegroundColor DarkGray - - # Record HEAD before this iteration - $headBefore = git rev-parse HEAD 2>$null - - $iterStart = Get-Date - Write-Host " Started: $($iterStart.ToString('HH:mm:ss')) | Model: $Model" -ForegroundColor DarkGray - Write-Host "" - - # Generate prompt for this story - $promptContent = Build-StoryPrompt -story $currentStory -prdObj $prd -completedStories $completedStories - - # --- Spawn Claude --- - - $logFile = Join-Path $logDir "$($currentStory.id).log" - $rawLogFile = Join-Path $logDir "$($currentStory.id).raw.jsonl" - $maxRetries = 10 - $retryCount = 0 - $outputString = "" - $apiOverloaded = $false - - do { - $apiOverloaded = $false - $textBuilder = [System.Text.StringBuilder]::new() - $toolCount = 0 - - # Clear raw log file for this attempt - if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force } - - if ($retryCount -gt 0) { - $backoffSeconds = [Math]::Pow(2, $retryCount - 1) - Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow - Start-Sleep -Seconds $backoffSeconds - Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray - } - - $promptContent | claude --print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json 2>&1 | ForEach-Object { - $line = $_.ToString().Trim() - if (-not $line) { return } - - # Save raw event for debugging - try { - Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue - } catch { } - - try { - $evt = $line | ConvertFrom-Json -ErrorAction Stop - - # --- Tool use start --- - if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') { - $toolCount++ - $toolName = $evt.content_block.name - Write-Host " [$toolName]" -ForegroundColor DarkCyan - } - # --- Streaming text --- - elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) { - Write-Host -NoNewline $evt.delta.text - [void]$textBuilder.Append($evt.delta.text) - } - # --- Result event --- - elseif ($evt.type -eq 'result') { - if ($evt.subtype -eq 'error_result' -and $evt.error) { - Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red - [void]$textBuilder.AppendLine("ERROR: $($evt.error)") - } - elseif ($evt.result) { - [void]$textBuilder.AppendLine($evt.result) - } - } - # --- Message-level content --- - elseif ($evt.message -and $evt.message.content) { - foreach ($block in $evt.message.content) { - if ($block.type -eq 'text' -and $block.text) { - Write-Host $block.text - [void]$textBuilder.AppendLine($block.text) - } - elseif ($block.type -eq 'tool_use') { - $toolCount++ - Write-Host " [$($block.name)]" -ForegroundColor DarkCyan - } - } - } - } catch { - if ($line -and $line -notmatch '^\s*[\{\[\}\]"]') { - Write-Host $line -ForegroundColor DarkYellow - [void]$textBuilder.AppendLine($line) - } - } - } - - $outputString = $textBuilder.ToString() - - # Check for 529 overloaded error - if ($outputString -match "529.*overloaded|overloaded_error") { - $apiOverloaded = $true - $retryCount++ - if ($retryCount -ge $maxRetries) { - Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red - } - } - # Check for usage limit with cooldown - elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") { - $resetHour = [int]$Matches[1] - $resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 } - $resetAmPm = $Matches[3] - - if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 } - elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 } - - $now = Get-Date - $resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0 - if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) } - $resetTime = $resetTime.AddMinutes(2) - - $waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds) - $waitMinutes = [Math]::Ceiling($waitSeconds / 60) - - Write-Host "" - Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow - Start-Sleep -Seconds $waitSeconds - Write-Host " [USAGE LIMIT] Cooldown complete. Retrying..." -ForegroundColor Green - - $apiOverloaded = $true - } - } while ($apiOverloaded -and $retryCount -lt $maxRetries) - - # Save log - $outputString | Set-Content -Path $logFile -Encoding UTF8 - - # Show elapsed time - $elapsed = (Get-Date) - $iterStart - Write-Host "" - Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray - - # --- Detect signals --- - - $storyComplete = $outputString -match "$([regex]::Escape($currentStory.id))" - $storyBlocked = $outputString -match "$([regex]::Escape($currentStory.id))" - $headAfter = git rev-parse HEAD 2>$null - $hasGitChanges = $headAfter -ne $headBefore - - # --- Post-iteration typecheck verification --- - - $typecheckPassed = $true - if ($storyComplete -and $hasGitChanges -and -not $SkipVerify) { - Write-Host " Verifying typecheck..." -ForegroundColor DarkGray - $typecheckOutput = npm run typecheck 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host " [VERIFY FAIL] Typecheck failed after completion signal. Not marking as passed." -ForegroundColor Red - $typecheckPassed = $false - } else { - Write-Host " [VERIFY OK] Typecheck passed." -ForegroundColor DarkGray - } - } - - # --- Update story status --- - - if ($storyComplete -and $hasGitChanges -and $typecheckPassed) { - # Mark story as passed in prd.json - $prd = Read-Prd - $storyToUpdate = $prd.userStories | Where-Object { $_.id -eq $currentStory.id } - if ($storyToUpdate) { - $storyToUpdate.passes = $true - $storyToUpdate.notes = "Completed iteration $iterationCount at $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Model: $Model." - } - Save-Prd $prd - - # Append to progress.txt - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $el = $elapsed.ToString('mm\:ss') - $progressEntry = "$ts | PASS | $($currentStory.id): $($currentStory.title) | model=$Model elapsed=$el tools=$toolCount" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - - Write-Host " [PASSED] $storyLabel" -ForegroundColor Green - $noProgressCount = 0 - $sameErrorCount = 0 - $lastErrorSignature = "" - } - elseif ($storyBlocked) { - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | BLOCKED | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - Write-Host " [BLOCKED] $storyLabel - check $logFile for details." -ForegroundColor Red - # Blocked counts as no progress - $noProgressCount++ - } - elseif ($storyComplete -and -not $hasGitChanges) { - Write-Host " [WARNING] Completion signaled but no git commits. Retrying story." -ForegroundColor DarkYellow - $noProgressCount++ - } - elseif ($storyComplete -and -not $typecheckPassed) { - Write-Host " [WARNING] Completion signaled but typecheck failed. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | TYPECHECK_FAIL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - # Has git changes, so not stalled — but not passed either - $noProgressCount = 0 - } - else { - # No completion signal - if ($hasGitChanges) { - Write-Host " [PARTIAL] Git changes but no completion signal. Retrying story." -ForegroundColor DarkYellow - $ts = Get-Date -Format 'yyyy-MM-dd HH:mm' - $progressEntry = "$ts | PARTIAL | $storyLabel" - Add-Content -Path $progressFile -Value $progressEntry -Encoding UTF8 - $noProgressCount = 0 - } else { - Write-Host " [NO PROGRESS] No changes and no signal." -ForegroundColor DarkYellow - $noProgressCount++ - } - } - - # --- Circuit Breaker: No Progress --- - - if ($noProgressCount -ge $MaxNoProgress) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red - Write-Host "No meaningful progress for $MaxNoProgress consecutive iterations." -ForegroundColor Red - Write-Host "Stuck on: $($currentStory.id) — $($currentStory.title)" -ForegroundColor Red - Write-Host "Check $logFile for details." -ForegroundColor Red - break - } - - # --- Circuit Breaker: Repeated Error --- - - $errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches - if ($errorLines) { - $filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3 - $currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|" - if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) { - $sameErrorCount++ - Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow - if ($sameErrorCount -ge $MaxSameError) { - Write-Host "" - Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red - Write-Host "Same error for $MaxSameError consecutive iterations:" -ForegroundColor Red - Write-Host " $currentErrorSignature" -ForegroundColor Red - break - } - } elseif ($currentErrorSignature) { - $sameErrorCount = 0 - } - $lastErrorSignature = $currentErrorSignature - } else { - $sameErrorCount = 0 - $lastErrorSignature = "" - } - - # --- Dynamic Model Selection --- - - if ($outputString -match "(opus|sonnet)") { - $nextModel = $Matches[1] - if ($nextModel -ne $Model) { - Write-Host " [Model Switch] $Model -> $nextModel (agent recommendation)" -ForegroundColor Magenta - $Model = $nextModel - } - } - - # Brief pause between iterations - Start-Sleep -Seconds 2 -} - -} finally { - # Cleanup: restore directory, kill dev server - Set-Location $originalDir - if ($devServerPid) { - Write-Host "Stopping dev server (PID $devServerPid)..." -ForegroundColor DarkGray - taskkill /T /F /PID $devServerPid 2>$null | Out-Null - } -} - -# --- Final Summary --- - -$prd = Read-Prd -$finalPassed = @($prd.userStories | Where-Object { $_.passes -eq $true }).Count -$finalTotal = $prd.userStories.Count - -Write-Host "" -Write-Host "===========================================" -ForegroundColor Cyan -Write-Host " Ralph Loop finished after $iterationCount iteration(s)" -ForegroundColor Cyan -Write-Host " Stories: $finalPassed/$finalTotal passed" -ForegroundColor Cyan -Write-Host " Branch: $BranchName" -ForegroundColor Cyan -Write-Host " Logs: $logDir" -ForegroundColor Cyan -Write-Host "===========================================" -ForegroundColor Cyan - -if ($finalPassed -eq $finalTotal) { - exit 0 -} else { - exit 1 -} - diff --git a/.gitignore b/.gitignore index 6075186..9481d72 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,16 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +# Environment .env + +# Dependencies node_modules + +# Build output dist dist-ssr +dist-server *.local # Editor directories and files @@ -27,7 +33,40 @@ dist-ssr # TypeScript *.tsbuildinfo -#Playwrite Screenshots +# Playwright screenshots *.png +!public/meta.png -nul \ No newline at end of file +# AI agent tooling +.claude/ +.codex/ +.ralph/ +.playwright-mcp/ +AGENTS.md +PROMPT.md +hats.yml +ralph.yml +scripts/ralph/ +scripts/benchmark-results/ + +# Reference / personal materials +References/ + +# Font source archives (used fonts are in public/fonts/) +Fonts/ + +# Logo animation source (Remotion) +LogoAnimation/ + +# Design notes +carousel-design-debate*.md + +# Misc +*:Zone.Identifier +__MACOSX +andy-charlwood-cv@0.0.0 +lighthouse.pdf +logo/ +graph.png +node +nul diff --git a/.playwright-mcp/http-server b/.playwright-mcp/http-server deleted file mode 100644 index 7c597fa..0000000 --- a/.playwright-mcp/http-server +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -var chalk = require('chalk'), - os = require('os'), - httpServer = require('../lib/http-server'), - portfinder = require('portfinder'), - opener = require('opener'), - - fs = require('fs'), - url = require('url'); -var argv = require('minimist')(process.argv.slice(2), { - alias: { - tls: 'ssl' - } -}); -var ifaces = os.networkInterfaces(); - -process.title = 'http-server'; - -if (argv.h || argv.help) { - console.log([ - 'usage: http-server [path] [options]', - '', - 'options:', - ' -p --port Port to use. If 0, look for open port. [8080]', - ' -a Address to use [0.0.0.0]', - ' -d Show directory listings [true]', - ' -i Display autoIndex [true]', - ' -g --gzip Serve gzip files when possible [false]', - ' -b --brotli Serve brotli files when possible [false]', - ' If both brotli and gzip are enabled, brotli takes precedence', - ' -e --ext Default file extension if none supplied [none]', - ' -s --silent Suppress log messages from output', - ' --cors[=headers] Enable CORS via the "Access-Control-Allow-Origin" header', - ' Optionally provide CORS headers list separated by commas', - ' -o [path] Open browser window after starting the server.', - ' Optionally provide a URL path to open the browser window to.', - ' -c Cache time (max-age) in seconds [3600], e.g. -c10 for 10 seconds.', - ' To disable caching, use -c-1.', - ' -t Connections timeout in seconds [120], e.g. -t60 for 1 minute.', - ' To disable timeout, use -t0', - ' -U --utc Use UTC time format in log messages.', - ' --log-ip Enable logging of the client\'s IP address', - '', - ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', - ' --proxy-options Pass options to proxy using nested dotted objects. e.g.: --proxy-options.secure false', - '', - ' --username Username for basic authentication [none]', - ' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME', - ' --password Password for basic authentication [none]', - ' Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD', - '', - ' -S --tls --ssl Enable secure request serving with TLS/SSL (HTTPS)', - ' -C --cert Path to TLS cert file (default: cert.pem)', - ' -K --key Path to TLS key file (default: key.pem)', - '', - ' -r --robots Respond to /robots.txt [User-agent: *\\nDisallow: /]', - ' --no-dotfiles Do not show dotfiles', - ' --mimetypes Path to a .types file for custom mimetype definition', - ' -h --help Print this list and exit.', - ' -v --version Print the version and exit.' - ].join('\n')); - process.exit(); -} - -var port = argv.p || argv.port || parseInt(process.env.PORT, 10), - host = argv.a || '0.0.0.0', - tls = argv.S || argv.tls, - sslPassphrase = process.env.NODE_HTTP_SERVER_SSL_PASSPHRASE, - proxy = argv.P || argv.proxy, - proxyOptions = argv['proxy-options'], - utc = argv.U || argv.utc, - version = argv.v || argv.version, - logger; - -var proxyOptionsBooleanProps = [ - 'ws', 'xfwd', 'secure', 'toProxy', 'prependPath', 'ignorePath', 'changeOrigin', - 'preserveHeaderKeyCase', 'followRedirects', 'selfHandleResponse' -]; - -if (proxyOptions) { - Object.keys(proxyOptions).forEach(function (key) { - if (proxyOptionsBooleanProps.indexOf(key) > -1) { - proxyOptions[key] = proxyOptions[key].toLowerCase() === 'true'; - } - }); -} - -if (!argv.s && !argv.silent) { - logger = { - info: console.log, - request: function (req, res, error) { - var date = utc ? new Date().toUTCString() : new Date(); - var ip = argv['log-ip'] - ? req.headers['x-forwarded-for'] || '' + req.connection.remoteAddress - : ''; - if (error) { - logger.info( - '[%s] %s "%s %s" Error (%s): "%s"', - date, ip, chalk.red(req.method), chalk.red(req.url), - chalk.red(error.status.toString()), chalk.red(error.message) - ); - } - else { - logger.info( - '[%s] %s "%s %s" "%s"', - date, ip, chalk.cyan(req.method), chalk.cyan(req.url), - req.headers['user-agent'] - ); - } - } - }; -} -else if (chalk) { - logger = { - info: function () {}, - request: function () {} - }; -} - -if (version) { - logger.info('v' + require('../package.json').version); - process.exit(); -} - -if (!port) { - portfinder.basePort = 8080; - portfinder.getPort(function (err, port) { - if (err) { throw err; } - listen(port); - }); -} -else { - listen(port); -} - -function listen(port) { - var options = { - root: argv._[0], - cache: argv.c, - timeout: argv.t, - showDir: argv.d, - autoIndex: argv.i, - gzip: argv.g || argv.gzip, - brotli: argv.b || argv.brotli, - robots: argv.r || argv.robots, - ext: argv.e || argv.ext, - logFn: logger.request, - proxy: proxy, - proxyOptions: proxyOptions, - showDotfiles: argv.dotfiles, - mimetypes: argv.mimetypes, - username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, - password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD - }; - - if (argv.cors) { - options.cors = true; - if (typeof argv.cors === 'string') { - options.corsHeaders = argv.cors; - } - } - - if (proxy) { - try { - new url.URL(proxy) - } - catch (err) { - logger.info(chalk.red('Error: Invalid proxy url')); - process.exit(1); - } - } - - if (tls) { - options.https = { - cert: argv.C || argv.cert || 'cert.pem', - key: argv.K || argv.key || 'key.pem', - passphrase: sslPassphrase, - }; - try { - fs.lstatSync(options.https.cert); - } - catch (err) { - logger.info(chalk.red('Error: Could not find certificate ' + options.https.cert)); - process.exit(1); - } - try { - fs.lstatSync(options.https.key); - } - catch (err) { - logger.info(chalk.red('Error: Could not find private key ' + options.https.key)); - process.exit(1); - } - } - - var server = httpServer.createServer(options); - server.listen(port, host, function () { - var protocol = tls ? 'https://' : 'http://'; - - logger.info([ - chalk.yellow('Starting up http-server, serving '), - chalk.cyan(server.root), - tls ? (chalk.yellow(' through') + chalk.cyan(' https')) : '' - ].join('')); - - logger.info([chalk.yellow('\nhttp-server version: '), chalk.cyan(require('../package.json').version)].join('')); - - logger.info([ - chalk.yellow('\nhttp-server settings: '), - ([chalk.yellow('CORS: '), argv.cors ? chalk.cyan(argv.cors) : chalk.red('disabled')].join('')), - ([chalk.yellow('Cache: '), argv.c ? (argv.c === '-1' ? chalk.red('disabled') : chalk.cyan(argv.c + ' seconds')) : chalk.cyan('3600 seconds')].join('')), - ([chalk.yellow('Connection Timeout: '), argv.t === '0' ? chalk.red('disabled') : (argv.t ? chalk.cyan(argv.t + ' seconds') : chalk.cyan('120 seconds'))].join('')), - ([chalk.yellow('Directory Listings: '), argv.d ? chalk.red('not visible') : chalk.cyan('visible')].join('')), - ([chalk.yellow('AutoIndex: '), argv.i ? chalk.red('not visible') : chalk.cyan('visible')].join('')), - ([chalk.yellow('Serve GZIP Files: '), argv.g || argv.gzip ? chalk.cyan('true') : chalk.red('false')].join('')), - ([chalk.yellow('Serve Brotli Files: '), argv.b || argv.brotli ? chalk.cyan('true') : chalk.red('false')].join('')), - ([chalk.yellow('Default File Extension: '), argv.e ? chalk.cyan(argv.e) : (argv.ext ? chalk.cyan(argv.ext) : chalk.red('none'))].join('')) - ].join('\n')); - - logger.info(chalk.yellow('\nAvailable on:')); - - if (argv.a && host !== '0.0.0.0') { - logger.info(` ${protocol}${host}:${chalk.green(port.toString())}`); - } else { - Object.keys(ifaces).forEach(function (dev) { - ifaces[dev].forEach(function (details) { - if (details.family === 'IPv4') { - logger.info((' ' + protocol + details.address + ':' + chalk.green(port.toString()))); - } - }); - }); - } - - if (typeof proxy === 'string') { - if (proxyOptions) { - logger.info('Unhandled requests will be served from: ' + proxy + '. Options: ' + JSON.stringify(proxyOptions)); - } - else { - logger.info('Unhandled requests will be served from: ' + proxy); - } - } - - logger.info('Hit CTRL-C to stop the server'); - if (argv.o) { - const openHost = host === '0.0.0.0' ? '127.0.0.1' : host; - let openUrl = `${protocol}${openHost}:${port}`; - if (typeof argv.o === 'string') { - openUrl += argv.o[0] === '/' ? argv.o : '/' + argv.o; - } - logger.info('Open: ' + openUrl); - opener(openUrl); - } - - // Spacing before logs - if (!argv.s) logger.info(); - }); -} - -if (process.platform === 'win32') { - require('readline').createInterface({ - input: process.stdin, - output: process.stdout - }).on('SIGINT', function () { - process.emit('SIGINT'); - }); -} - -process.on('SIGINT', function () { - logger.info(chalk.red('http-server stopped.')); - process.exit(); -}); - -process.on('SIGTERM', function () { - logger.info(chalk.red('http-server stopped.')); - process.exit(); -}); diff --git a/.ralph/agent/handoff.md b/.ralph/agent/handoff.md deleted file mode 100644 index 2b5c0a2..0000000 --- a/.ralph/agent/handoff.md +++ /dev/null @@ -1,44 +0,0 @@ -# Session Handoff - -_Generated: 2026-02-18 00:42:07 UTC_ - -## Git Context - -- **Branch:** `master` -- **HEAD:** 134e41f: chore: auto-commit before merge (loop primary) - -## Tasks - -_No tasks tracked in this session._ - -## Key Files - -Recently modified: - -- `.claude/settings.json` -- `.claude/settings.local.json` -- `.ralph/agent/handoff.md` -- `.ralph/agent/memories.md` -- `.ralph/agent/memories.md.lock` -- `.ralph/agent/scratchpad.md` -- `.ralph/agent/summary.md` -- `.ralph/agent/tasks.jsonl` -- `.ralph/current-events` -- `.ralph/current-loop-id` - -## Next Session - -Session completed successfully. No pending work. - -**Original objective:** - -``` -# Task: Portfolio UX Improvements — GP Clinical System Theme Polish - -Implement 11 prioritised UX improvements to the portfolio site. This is an interactive CV/portfolio themed as a GP primary care clinical system (like EMIS Web / SystmOne). The site should feel like a real GP system but function as a portfolio. - -**Important constraints:** -- Do NOT change the overall structure or architecture -- Preserve the GP clinical system theme — improvements should reinforce it, not break it -- Respect ex... -``` diff --git a/.ralph/agent/scratchpad.md b/.ralph/agent/scratchpad.md deleted file mode 100644 index 7dd8cf8..0000000 --- a/.ralph/agent/scratchpad.md +++ /dev/null @@ -1,15 +0,0 @@ -## Iteration 1 — Planner hat - -Read all source files. Key observations: - -1. **MobileOverviewHeader** should be a straightforward extraction. Most of the content already exists in MobileBottomNav's drawer (lines 273–381). The action buttons already exist in MobilePatientBanner (lines 228–323). I'm combining both into the new component. - -2. **MobileBottomNav cleanup** is significant — removing the drawer eliminates ~280 lines and most imports. The component goes from 388 lines to roughly 50. The `onSearchClick` prop becomes dead after drawer removal. - -3. **NavItems change**: new "Overview" at position 0 with `UserRound` icon pointing to `mobile-overview` tile. Old "Overview" renamed to "Summary" with `ClipboardList` icon, keeping `patient-summary` tile. - -4. **DashboardLayout** changes are minimal: swap import + component, remove one prop from MobileBottomNav. - -5. **TagPill** component exists in both MobileBottomNav and Sidebar. The new MobileOverviewHeader needs its own copy (same implementation). Could extract to a shared component, but the spec doesn't call for that — keep it local. - -Plan written to `.ralph/plan.md`. Emitting plan.ready for builder hat. diff --git a/.ralph/agent/summary.md b/.ralph/agent/summary.md deleted file mode 100644 index 19fe74f..0000000 --- a/.ralph/agent/summary.md +++ /dev/null @@ -1,17 +0,0 @@ -# Loop Summary - -**Status:** Stopped: max iterations reached -**Iterations:** 25 -**Duration:** 45m 28s - -## Tasks - -_No scratchpad found._ - -## Events - -_No events recorded._ - -## Final Commit - -8b79f7b: mobile banner v1 diff --git a/.ralph/agent/tasks.jsonl.lock b/.ralph/agent/tasks.jsonl.lock deleted file mode 100644 index e69de29..0000000 diff --git a/.ralph/current-events b/.ralph/current-events deleted file mode 100644 index 3825f71..0000000 --- a/.ralph/current-events +++ /dev/null @@ -1 +0,0 @@ -.ralph/events-20260218-032325.jsonl \ No newline at end of file diff --git a/.ralph/current-loop-id b/.ralph/current-loop-id deleted file mode 100644 index d3b309f..0000000 --- a/.ralph/current-loop-id +++ /dev/null @@ -1 +0,0 @@ -primary-20260218-032325 \ No newline at end of file diff --git a/.ralph/events-20260217-205901.jsonl b/.ralph/events-20260217-205901.jsonl deleted file mode 100644 index 7944696..0000000 --- a/.ralph/events-20260217-205901.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"ts":"2026-02-17T20:59:01.382091923+00:00","iteration":0,"hat":"loop","topic":"work.start","triggered":"planner","payload":"# Task: Fix Mobile Responsiveness for Small Viewport Widths (≤430px)\n\nThe portfolio website is broken on phones with viewport widths around 400px. Text overflows off-screen, elements are hidden behind `overflow: hidden`, and layout components are sized inappropriately for small screens.\n\n## Context\n\n- **Tech stack:** React + TypeScript + Tailwind CSS + Framer Motion + D3\n- **Dev server:** `npm run dev` (localhost:5173)\n- **Quality gates:** `npm run lint && npm run typecheck && npm run build`\n-... [truncated, 6122 chars total]"} -{"payload":"Comprehensive 8-phase plan written to .ralph/plan.md. Covers sidebar→bottom-nav conversion, spacing reduction, KPI grid, carousel, timeline overflow, constellation sizing, detail panel, and skills grid. 11 files affected.","topic":"plan.ready","ts":"2026-02-17T21:02:20.892705229+00:00"} -{"payload":"All 8 phases implemented. lint: 0 errors, typecheck: pass, build: pass. 10 files modified + 1 new (MobileBottomNav.tsx). Bottom nav, spacing, KPI grid, carousel, constellation, detail panel all fixed for 320-430px.","topic":"build.done","ts":"2026-02-17T21:08:34.938897608+00:00"} -{"payload":"lint: 0 errors 6 warnings, typecheck: clean, build: success. Fixed: shared useIsMobileNav hook, chat widget offset gap, drawer a11y (Escape key, button backdrop)","topic":"build.done","ts":"2026-02-17T21:15:17.884340466+00:00"} -{"payload":"lint: 0 errors, typecheck: clean, build: success. All 8 responsive phases implemented. Ready for visual review.","topic":"build.done","ts":"2026-02-17T21:17:46.100718553+00:00"} -{"payload":"All responsive fixes implemented and verified. Quality gates pass (lint 0 errors, typecheck clean, build succeeds). All 11 implementation items confirmed across MobileBottomNav, useIsMobileNav, DashboardLayout, Sidebar, ChatWidget, index.css, Card, PatientSummaryTile, ProjectsTile, useForceSimulation, tailwind.config.","topic":"LOOP_COMPLETE","ts":"2026-02-17T21:19:31.407813400+00:00"} -{"ts":"2026-02-17T21:19:40.085442571+00:00","iteration":5,"hat":"loop","topic":"loop.terminate","payload":"## Reason\ncompleted\n\n## Status\nAll tasks completed successfully.\n\n## Summary\n- Iterations: 5\n- Duration: 19m 23s\n- Exit code: 0"} diff --git a/.ralph/events-20260218-002219.jsonl b/.ralph/events-20260218-002219.jsonl deleted file mode 100644 index 12820c6..0000000 --- a/.ralph/events-20260218-002219.jsonl +++ /dev/null @@ -1,8 +0,0 @@ -{"ts":"2026-02-18T00:22:19.960685335+00:00","iteration":0,"hat":"loop","topic":"work.start","triggered":"planner","payload":"# Task: Portfolio UX Improvements — GP Clinical System Theme Polish\n\nImplement 11 prioritised UX improvements to the portfolio site. This is an interactive CV/portfolio themed as a GP primary care clinical system (like EMIS Web / SystmOne). The site should feel like a real GP system but function as a portfolio.\n\n**Important constraints:**\n- Do NOT change the overall structure or architecture\n- Preserve the GP clinical system theme — improvements should reinforce it, not break it\n- Respect ex... [truncated, 7679 chars total]"} -{"payload":"Comprehensive plan written for all 11 UX improvements. Implementation ready in priority order.","topic":"plan.ready","ts":"2026-02-18T00:26:57.495523147+00:00"} -{"payload":"All 11 UX improvements implemented. lint: pass, typecheck: pass, build: pass","topic":"build.done","ts":"2026-02-18T00:32:48.371941937+00:00"} -{"payload":"All 11 UX improvements verified. lint: pass, typecheck: pass, build: pass. Visual inspection confirms structured profile, result summaries on cards, CTA buttons, KEY METRICS header, carousel arrows, summary-only LastConsultationCard, text-tertiary contrast fix, detail panel exit animation, marginBottom typo fix. Ready for review.","topic":"build.done","ts":"2026-02-18T00:35:43.515927651+00:00"} -{"payload":"All 11 UX improvements verified. Quality gates: lint PASS, typecheck PASS, build PASS. No regressions.","topic":"build.done","ts":"2026-02-18T00:37:48.332188194+00:00"} -{"payload":"tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass, complexity: low, duplication: pass, performance: pass, specs: pass","topic":"build.done","ts":"2026-02-18T00:39:57.261553854+00:00"} -{"payload":"All 11 UX improvements implemented and verified. Quality gates: lint pass, typecheck pass, build pass.","topic":"LOOP_COMPLETE","ts":"2026-02-18T00:42:02.109254742+00:00"} -{"ts":"2026-02-18T00:42:07.036937497+00:00","iteration":6,"hat":"loop","topic":"loop.terminate","payload":"## Reason\ncompleted\n\n## Status\nAll tasks completed successfully.\n\n## Summary\n- Iterations: 6\n- Duration: 18m 12s\n- Exit code: 0"} diff --git a/.ralph/events-20260218-030849.jsonl b/.ralph/events-20260218-030849.jsonl deleted file mode 100644 index 8f7016e..0000000 --- a/.ralph/events-20260218-030849.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"ts":"2026-02-18T03:08:49.098069412+00:00","iteration":0,"hat":"loop","topic":"work.start","triggered":"planner","payload":"# Task: Fix & Enhance Mobile Patient Banner\n\nFix the broken mobile patient banner (`MobilePatientBanner.tsx`) and add action buttons (Download CV, Contact Patient, LinkedIn, GitHub) to its expanded section. Ensure the referral form modal works properly on mobile screens.\n\n## Files\n\n| File | Role |\n|------|------|\n| `src/components/MobilePatientBanner.tsx` | Primary target — banner component |\n| `src/components/ReferralFormModal.tsx` | Contact form modal — needs mobile adaptation |\n| `src/com... [truncated, 5277 chars total]"} -{"payload":"Plan written to .ralph/plan.md covering 3 bug fixes, action buttons, and referral form mobile adaptation","topic":"plan.ready","ts":"2026-02-18T03:11:06.661707134+00:00"} diff --git a/.ralph/events-20260218-032325.jsonl b/.ralph/events-20260218-032325.jsonl deleted file mode 100644 index 0e6bade..0000000 --- a/.ralph/events-20260218-032325.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"ts":"2026-02-18T03:23:25.608437914+00:00","iteration":0,"hat":"loop","topic":"work.start","triggered":"planner","payload":"# Task: Replace Mobile Banner with Inline Overview Section\n\nRemove the sticky `MobilePatientBanner` and replace it with a static inline section at the top of the mobile dashboard. Remove the \"More\" drawer from the bottom nav, since its content now lives inline at the top of the page.\n\n## Files\n\n| File | Role |\n|------|------|\n| `src/components/MobilePatientBanner.tsx` | DELETE — replaced by new inline section |\n| `src/components/MobileBottomNav.tsx` | Remove \"More\" button + entire drawer; add ... [truncated, 6489 chars total]"} -{"payload":"Plan written to .ralph/plan.md — 4 parts: create MobileOverviewHeader, clean MobileBottomNav, update DashboardLayout, delete MobilePatientBanner","topic":"plan.ready","ts":"2026-02-18T03:25:02.244455595+00:00"} -{"ts":"2026-02-18T04:13:20.975545141+00:00","iteration":25,"hat":"loop","topic":"loop.terminate","payload":"## Reason\nmax_iterations\n\n## Status\nStopped at iteration limit.\n\n## Summary\n- Iterations: 25\n- Duration: 45m 28s\n- Exit code: 2"} diff --git a/.ralph/history.jsonl b/.ralph/history.jsonl deleted file mode 100644 index a9c89f3..0000000 --- a/.ralph/history.jsonl +++ /dev/null @@ -1,7 +0,0 @@ -{"ts":"2026-02-17T20:59:01.482666643Z","type":{"kind":"loop_started","prompt":"# Task: Fix Mobile Responsiveness for Small Viewport Widths (≤430px)\n\nThe portfolio website is broken on phones with viewport widths around 400px. Text overflows off-screen, elements are hidden behind `overflow: hidden`, and layout components are sized inappropriately for small screens.\n\n## Context\n\n- **Tech stack:** React + TypeScript + Tailwind CSS + Framer Motion + D3\n- **Dev server:** `npm run dev` (localhost:5173)\n- **Quality gates:** `npm run lint && npm run typecheck && npm run build`\n- **Smallest configured breakpoint:** `xs: 480px` in tailwind.config.js — there is no sub-480px handling\n- **Key layout file:** `src/components/DashboardLayout.tsx` orchestrates all dashboard tiles\n- **CSS media queries:** `src/index.css` contains most custom responsive rules\n- **Tailwind config:** `tailwind.config.js` defines breakpoints and theme\n\n## Target Viewports\n\nTest and fix at these widths (all portrait orientation, 812px height):\n- **320px** — iPhone SE / smallest realistic phone\n- **360px** — Common Android (Samsung Galaxy S series)\n- **375px** — iPhone 12 mini / iPhone SE 3rd gen\n- **390px** — iPhone 14\n- **400px** — User's specific device (primary target)\n- **414px** — iPhone 8 Plus / larger phones\n- **430px** — iPhone 14 Pro Max\n\n## Known Issues (from codebase analysis)\n\n### Critical\n1. **Sidebar must become a bottom nav bar at <600px** — The current sidebar is a 304px-wide overlay on mobile, leaving only 96px for content at 400px. At viewport widths below 600px, replace the sidebar with a **bottom navigation bar** that:\n - **Collapsed state (default):** A slim fixed bar along the bottom edge of the screen with icon-based navigation items (like a mobile tab bar / iOS-style bottom nav). Should not obscure content — main content area accounts for its height.\n - **Expanded state (on tap/click):** Slides up as a drawer/sheet showing the full sidebar content (patient name, navigation links, etc.). Tapping the bar or a close affordance collapses it back down.\n - The existing sidebar behavior for viewports ≥600px should remain completely unchanged.\n - Use Framer Motion for the drawer slide animation, consistent with existing animation patterns.\n2. **KPI grid forces 2 columns** — `repeat(2, minmax(0, 1fr))` creates cramped cards at small widths. Values use `30px` font.\n3. **Timeline text silently clipped** — `overflow: hidden` on Card.tsx hides content with no visual indication (no ellipsis, no wrapping).\n4. **Project carousel cards too small** — At 400px with 2 cards per view, each card is only ~194px wide.\n\n### Important\n5. **Constellation graph** — 520px height at <768px may be disproportionate; needs better sizing.\n6. **No sub-480px breakpoint** — The smallest Tailwind breakpoint is `xs: 480px`, leaving 320-479px unhandled.\n\n### Minor\n8. **Padding/spacing** — `p-5` (20px) main content + `24px` card padding eats significant space at 400px.\n9. **Detail panel header** — Close button (44px) + title cramped at narrow widths.\n10. **Skills/medications grid** — May need column count reduction at small widths.\n\n## Requirements\n\n- **All text must be visible** — no text clipped by `overflow: hidden` without ellipsis or wrapping. Truncated text needs `text-overflow: ellipsis` with title attribute for accessibility.\n- **All interactive elements must be reachable** — nothing hidden off-screen or behind other elements.\n- **Touch targets** — minimum 44x44px for interactive elements.\n- **Readable font sizes** — minimum 12px body text, 14px primary content.\n- **No horizontal scroll** — page must never scroll horizontally at any target viewport.\n- **Maintain visual identity** — keep PMR aesthetic, teal/coral palette, existing design language. Adapt proportionally, don't radically redesign.\n- **Respect existing patterns** — use Tailwind classes where possible, use existing CSS custom properties, follow project conventions.\n\n## Success Criteria\n\nAll of the following must be true:\n\n- [ ] `npm run lint` passes with zero errors\n- [ ] `npm run typecheck` passes with zero errors\n- [ ] `npm run build` succeeds\n- [ ] At 320px viewport: no horizontal scrollbar, all text readable, no content clipped without indication\n- [ ] At 360px viewport: same as above\n- [ ] At 375px viewport: same as above\n- [ ] At 390px viewport: same as above\n- [ ] At 400px viewport: same as above (primary target)\n- [ ] At 414px viewport: same as above\n- [ ] At 430px viewport: same as above\n- [ ] At <600px: sidebar is replaced by a bottom nav bar with collapsed (tab bar) and expanded (drawer) states\n- [ ] Bottom nav bar does not obscure page content in collapsed state (content has bottom padding/margin to account for it)\n- [ ] Bottom nav drawer expands on tap and shows full navigation content\n- [ ] Bottom nav drawer can be collapsed back down\n- [ ] At ≥600px: existing sidebar behavior is completely unchanged\n- [ ] KPI cards are readable with appropriate font sizing\n- [ ] Timeline entries show full text or have proper ellipsis truncation\n- [ ] Project carousel cards are adequately sized (consider 1 card per view if needed)\n- [ ] Constellation graph fits within viewport without excessive scrolling\n- [ ] Desktop/tablet layouts (768px+) remain unchanged and unbroken\n\n## Constraints\n\n- Do NOT change boot sequence, ECG animation, or login screen (already handle small screens)\n- Do NOT change D3 force simulation logic — only container sizing\n- Do NOT add new npm dependencies\n- Do NOT remove existing features or functionality\n- Keep changes minimal and focused — fix responsiveness, don't redesign\n- Preserve all existing breakpoint behavior for md (768px) and above\n\n## Visual Validation Method\n\nUse Playwright MCP to:\n1. Navigate to `http://localhost:5173` (dev server must be running)\n2. Get past boot sequence + ECG + login to reach dashboard\n3. Set viewport to each target width × 812px height\n4. Take screenshots of dashboard at each viewport\n5. Visually inspect for: overflow, clipping, cramped text, unreachable elements\n6. Scroll through full page and verify no content is hidden\n\n## Status\n\nTrack progress here. When all success criteria are met, print LOOP_COMPLETE.\n"}} -{"ts":"2026-02-17T21:19:40.088056091Z","type":{"kind":"loop_completed","reason":"completion_promise"}} -{"ts":"2026-02-18T00:22:20.061289826Z","type":{"kind":"loop_started","prompt":"# Task: Portfolio UX Improvements — GP Clinical System Theme Polish\n\nImplement 11 prioritised UX improvements to the portfolio site. This is an interactive CV/portfolio themed as a GP primary care clinical system (like EMIS Web / SystmOne). The site should feel like a real GP system but function as a portfolio.\n\n**Important constraints:**\n- Do NOT change the overall structure or architecture\n- Preserve the GP clinical system theme — improvements should reinforce it, not break it\n- Respect existing conventions: TypeScript strict, Tailwind + CSS custom properties, Framer Motion with `prefers-reduced-motion`\n- Path alias: `@/*` → `src/*`\n- Quality gates: `npm run lint && npm run typecheck && npm run build`\n\n## Improvements (ordered by priority)\n\n### 1. Restructure Profile Summary Text\n**File:** `src/components/tiles/PatientSummaryTile.tsx` (or wherever the narrative renders)\n**Problem:** The patient summary narrative is a dense ~80-word paragraph — a wall of text. It's the first substantive content visitors see and doesn't match the structured clinical aesthetic.\n**Change:** Break into structured clinical-style data:\n- Brief 1-2 sentence summary (like a presenting complaint)\n- Key facts as labeled fields below: Specialisation, Current System, Population, Focus Areas\n- Or collapse behind \"Read more\" with first sentence visible\n- Must feel like GP system structured data, not a LinkedIn About section\n\n### 2. Surface Impact Metrics on Project Cards\n**File:** `src/components/tiles/ProjectsTile.tsx` (or the project card component)\n**Problem:** `resultSummary` exists in the data (e.g., \"14,000 patients identified\", \"£2.6M savings\") but is not rendered on project card faces. Recruiters scan for numbers.\n**Change:** Render `resultSummary` prominently on each project card — below the title, styled as a bold stat. If a project has no `resultSummary`, don't show a placeholder.\n\n### 3. Add Prominent Contact/Download CV CTA\n**Problem:** No visible \"Get in touch\" or \"Download CV\" button in the main content area. These actions only exist in the sidebar or command palette.\n**Change:** Add a small, visible row of action buttons (Email, LinkedIn, GitHub, Download CV) in the Patient Summary section. Style them as GP system action buttons to reinforce the theme. Keep it compact — not a hero CTA, but unmissable.\n\n### 4. Reduce Boot + Login Sequence Time\n**Files:** `src/components/BootSequence.tsx`, `src/components/LoginScreen.tsx`\n**Problem:** Boot (~6-8s) + Login (~4s) = ~10 seconds before content. Too slow for repeat visitors.\n**Change:** Reduce `TYPING_SPEED` multiplier to ~1.2 (from 2). Add `sessionStorage` detection — if user has visited before in this session, auto-skip directly to dashboard. Ensure skip button still appears early for first-time visitors.\n\n### 5. Resolve Last Consultation / Timeline Duplication\n**Files:** `src/components/tiles/LastConsultationCard.tsx`, `src/components/tiles/TimelineInterventionsSubsection.tsx`\n**Problem:** Current role appears twice — once as LastConsultationCard and again as first timeline accordion entry. Redundant.\n**Change:** Differentiate LastConsultationCard as a summary-only card (role, org, band, date range, one-line summary) without the full bullet points. The full details should only appear in the timeline accordion. Add a \"Current\" badge to the first timeline accordion entry.\n\n### 6. Fix Text-Tertiary Contrast Ratio\n**File:** `src/index.css`\n**Problem:** `--text-tertiary: #8DA8A5` on `--bg-dashboard: #F0F5F4` yields ~2.8:1 contrast, failing WCAG AA.\n**Change:** Darken `--text-tertiary` to at least `#6B8886` (achieves ~4.5:1 on `#F0F5F4`). Verify the change looks good across dates, helper text, and monospace metadata.\n\n### 7. Add Mobile Identity Bar\n**Problem:** On mobile, no name or identity marker is visible without opening the drawer. Recruiters on mobile have no visual anchor.\n**Change:** Add a compact identity bar at the top of mobile layout showing \"CHARLWOOD, Andrew\" and brief role title. Only visible on mobile (below `lg` breakpoint where sidebar is hidden). Style it like a GP system patient banner strip.\n\n### 8. Simplify KPI Section Header Language\n**File:** The KPI/metrics section component\n**Problem:** \"LATEST RESULTS (CLICK TO VIEW FULL REFERENCE RANGE)\" is deep medical jargon that non-healthcare visitors won't understand.\n**Change:** Change to \"KEY METRICS\" or \"IMPACT HIGHLIGHTS\". Update the helper text to \"Select a metric to inspect methodology, impact, and outcomes\" (if not already). Keep the excellent metric cards unchanged.\n\n### 9. Add Detail Panel Exit Animation\n**Files:** `src/components/DetailPanel.tsx`\n**Problem:** Panel has `panel-slide-in` animation but closes instantly. `panel-slide-out` keyframe exists in CSS but is unused.\n**Change:** Implement exit animation — either wire up the existing `panel-slide-out` keyframe via a closing state, or use Framer Motion's `AnimatePresence`. The panel should slide out before unmounting.\n\n### 10. Fix marginBottom Typo\n**File:** `src/components/tiles/LastConsultationCard.tsx` (around line 89)\n**Problem:** `marginBottom: '1=px'` — typo, should be `'1px'` or appropriate value.\n**Change:** Fix the typo. Check surrounding styles for the correct intended value.\n\n### 11. Add Arrow Navigation to Desktop Projects Carousel\n**File:** `src/components/tiles/ProjectsTile.tsx` — `ContinuousScrollCarousel` component (lines ~356–480)\n**Problem:** The ContinuousScrollCarousel (desktop ≥1024px) auto-scrolls but offers no manual browsing.\n**Change:**\n- Add prev/next arrow buttons (ChevronLeft, ChevronRight from lucide-react) positioned absolutely at left/right edges, vertically centered\n- Style following the existing FullscreenButton pattern: `var(--surface)` background, `var(--border)` border, opacity hover effect, subtle shadow\n- Arrow click handler: jump one card width + gap = `((viewportWidth - 36) / 4) + 12` pixels\n- Apply temporary CSS transition on the track (`transform 0.4s ease`) for smooth animated jump; remove transition after completion so rAF loop isn't fighting CSS\n- Handle wrapping: keep offset within `[0, firstSetWidth)` using modulo\n- Pause/resume: on arrow click set `isPausedRef = true`, clear existing timeout, start 6-second timeout to resume auto-scroll\n- Existing hover pause/resume still works independently\n- Rapid clicks: each click resets the 6s timeout; transition handles overlapping clicks by snapping to current offset\n- Reduced motion: arrows still work (instant jump, no transition), auto-scroll stays disabled per existing logic\n\n## Success Criteria\n\nAll of the following must be true:\n- [ ] Profile summary is structured data, not a text wall — feels clinical\n- [ ] Project cards display `resultSummary` when available\n- [ ] Contact/Download CV actions are visible in the main content area\n- [ ] Boot + login sequence completes in ~5 seconds or less for first visit; instant skip for return visitors\n- [ ] LastConsultationCard is a distinct summary (no duplication with timeline)\n- [ ] `--text-tertiary` passes WCAG AA contrast (4.5:1) on dashboard background\n- [ ] Mobile shows identity/name without opening drawer\n- [ ] KPI header uses plain language, not clinical jargon\n- [ ] Detail panel has exit animation (slide out, not instant disappear)\n- [ ] marginBottom typo is fixed\n- [ ] Desktop projects carousel has prev/next arrow buttons\n- [ ] Arrow buttons pause auto-scroll for 6s then resume\n- [ ] `npm run lint` passes\n- [ ] `npm run typecheck` passes\n- [ ] `npm run build` passes\n- [ ] No regressions — existing functionality preserved\n\n## Status\n\nTrack progress here. Mark items complete as you go.\nWhen all success criteria are met, print LOOP_COMPLETE.\n"}} -{"ts":"2026-02-18T00:42:07.039344987Z","type":{"kind":"loop_completed","reason":"completion_promise"}} -{"ts":"2026-02-18T03:08:49.198921526Z","type":{"kind":"loop_started","prompt":"# Task: Fix & Enhance Mobile Patient Banner\n\nFix the broken mobile patient banner (`MobilePatientBanner.tsx`) and add action buttons (Download CV, Contact Patient, LinkedIn, GitHub) to its expanded section. Ensure the referral form modal works properly on mobile screens.\n\n## Files\n\n| File | Role |\n|------|------|\n| `src/components/MobilePatientBanner.tsx` | Primary target — banner component |\n| `src/components/ReferralFormModal.tsx` | Contact form modal — needs mobile adaptation |\n| `src/components/DashboardLayout.tsx` | Mounts banner at line 303 inside `` |\n| `src/components/Sidebar.tsx` | Reference for existing button styles, URLs, and referral form wiring |\n\n## Part 1: Fix Banner Bugs\n\n### Bug 1: Gap at top of viewport\nVisible gap between the banner's green header and the top of the viewport. The banner uses negative margins (`-mx-3 xs:-mx-5 -mt-3 xs:-mt-5`) to counteract parent `
` padding (`p-3 xs:p-5`), but this doesn't work with `position: sticky; top: 0` inside a padded scroll container.\n\n### Bug 2: Click-to-collapse broken\n`handleToggle` (line 62–72) only ever sets `expanded` to `true`. When already expanded, it returns `prev` unchanged. Clicking the expanded banner should collapse it.\n\n### Bug 3: Click-to-open unreliable\nTapping the collapsed banner sometimes doesn't visibly open. The scroll event listener fires immediately after the click, re-collapsing before the animation completes. The `expandedByClickRef` guard doesn't fully prevent this race.\n\n## Part 2: Add Action Buttons\n\nAdd buttons to the expanded panel (below the data rows) matching the sidebar's Contact section layout:\n\n**Layout (top to bottom):**\n1. **Download CV** — full-width button with icon + text label. Links to `/References/CV_v4.md` (same as sidebar). Style: accent-bordered, matches sidebar's download button aesthetic.\n2. **Three icon-only buttons in a row** (equal-width, 3 columns):\n - **Contact Patient** — `Send` icon. Opens `ReferralFormModal` (needs state + handler).\n - **LinkedIn** — `Linkedin` icon. Links to `https://linkedin.com/in/andycharlwood`, opens in new tab.\n - **GitHub** — `Github` icon. Links to `https://github.com/andycharlwood`, opens in new tab.\n\nThe 3 icon buttons should be the same total width as the Download CV button above them. Icon-only (no text labels) to save vertical space on mobile. Use accessible `aria-label` attributes on each.\n\n**Style reference:** The sidebar (`Sidebar.tsx` lines 436–595) has these exact buttons with text labels. Match border, colour, and radius styles but make them icon-only for mobile.\n\n## Part 3: Mobile Referral Form\n\nThe `ReferralFormModal.tsx` currently uses `maxWidth: 540px` with desktop-oriented padding. It needs to work on screens ≤599px (the `useIsMobileNav` breakpoint).\n\nKey issues to address:\n- Form should use full viewport width on mobile (no side padding gaps)\n- Form fields need proper touch-target sizing (min 44px height)\n- The modal should be scrollable when content exceeds viewport height\n- Close button needs adequate touch target\n- On very small screens, reduce internal padding to prevent cramped layout\n- Validate that all form fields remain usable and form submission works\n\n**Important:** The form already has a working backend (`server.ts` POST `/api/contact`). Do NOT change the API contract or field names. Focus only on mobile layout/UX.\n\n## Success Criteria\n\nAll of the following must be true:\n\n### Banner bugs\n- [ ] No visible gap between banner and top of viewport on mobile (≤599px)\n- [ ] Tapping the collapsed banner reliably expands it (every time)\n- [ ] Tapping the expanded banner collapses it\n- [ ] Scrolling down 40px+ from top collapses an auto-expanded banner\n- [ ] Scrolling down 20px+ after a click-expand collapses it\n- [ ] Chevron animates correctly (180° rotation when expanded, bounce hint when collapsed)\n- [ ] AnimatePresence height animation is smooth\n\n### Action buttons\n- [ ] Download CV button is full-width with icon + text\n- [ ] Three icon buttons (Contact, LinkedIn, GitHub) display in equal-width columns\n- [ ] Contact button opens the referral form modal\n- [ ] LinkedIn and GitHub links open in new tabs\n- [ ] All buttons have appropriate aria-labels\n- [ ] Buttons match the design language of the sidebar's equivalent buttons\n\n### Referral form mobile\n- [ ] Form modal fills viewport width on mobile\n- [ ] All form fields are usable with touch (≥44px targets)\n- [ ] Modal is scrollable when content exceeds viewport\n- [ ] Form submission works (client-side validation + POST to /api/contact)\n- [ ] Close button has adequate touch target\n\n### Quality gates\n- [ ] `npm run lint` passes\n- [ ] `npm run typecheck` passes\n- [ ] `npm run build` passes\n- [ ] Playwright MCP verification passes on mobile viewport (≤599px)\n\n## Constraints\n\n- Do not add new npm dependencies\n- Do not change `server.ts` or the `/api/contact` API contract\n- Preserve existing Framer Motion animations\n- Preserve all accessibility attributes\n- Follow existing conventions: inline styles + Tailwind classes, TypeScript strict mode\n- Icons come from `lucide-react` (already imported: `Download`, `Github`, `Linkedin`, `Send`, `ChevronDown`)\n\n## Status\n\nTrack progress in `.ralph/plan.md`. When all success criteria are met, print LOOP_COMPLETE.\n"}} -{"ts":"2026-02-18T03:23:25.709000215Z","type":{"kind":"loop_started","prompt":"# Task: Replace Mobile Banner with Inline Overview Section\n\nRemove the sticky `MobilePatientBanner` and replace it with a static inline section at the top of the mobile dashboard. Remove the \"More\" drawer from the bottom nav, since its content now lives inline at the top of the page.\n\n## Files\n\n| File | Role |\n|------|------|\n| `src/components/MobilePatientBanner.tsx` | DELETE — replaced by new inline section |\n| `src/components/MobileBottomNav.tsx` | Remove \"More\" button + entire drawer; add Overview item; rename old Overview to \"Summary\" |\n| `src/components/DashboardLayout.tsx` | Swap MobilePatientBanner for new MobileOverviewHeader; pass onSearchClick |\n| `src/components/MobileOverviewHeader.tsx` | NEW — inline mobile header section |\n| `src/components/ReferralFormModal.tsx` | Already exists — opened from the new section's Contact button |\n| `src/components/Sidebar.tsx` | Reference only — button styles, URLs |\n\n## What to Build\n\n### 1. New `MobileOverviewHeader.tsx`\n\nA static (not sticky) section rendered at the top of mobile `
` content, before `PatientSummaryTile`. Visible only when `useIsMobileNav()` is true. Must have `data-tile-id=\"mobile-overview\"` so the bottom nav Overview button can scroll to it.\n\n**Layout (top to bottom), matching the existing \"More\" drawer layout in `MobileBottomNav.tsx` lines 273–381:**\n\n1. **Logo + Search row** — `CvmisLogo` (cssHeight \"40px\") + search button (full-width, `minHeight: 44px`, shows search label text). Search button calls `onSearchClick` prop.\n\n2. **Patient info section** (bordered bottom with `2px solid var(--accent)`):\n - Avatar circle (44px, gradient, \"AC\") + name + role title — same layout as drawer lines 301–327\n - Data rows: GPhC, Education, Location, Registered, Phone (PhoneCaptcha), Email — same as drawer lines 329–356\n\n3. **Tags section** — tag pills, same as drawer lines 360–369\n\n4. **Action buttons** (replacing the alerts section):\n - **Download CV** — full-width button with icon + text label. `` to `/References/CV_v4.md`, new tab. Style: accent-bordered, matches sidebar's download button.\n - **Three icon-only buttons in a row** (equal-width grid, 3 columns):\n - **Contact Patient** — `Send` icon. Opens `ReferralFormModal`.\n - **LinkedIn** — `Linkedin` icon. Links to `https://linkedin.com/in/andycharlwood`, new tab.\n - **GitHub** — `Github` icon. Links to `https://github.com/andycharlwood`, new tab.\n - Use the same button styles as the existing `MobilePatientBanner.tsx` action buttons (lines 228–323). Icon-only for the 3 buttons, accessible `aria-label` on each.\n\n5. **ReferralFormModal** — render it inside this component, controlled by local `showReferralForm` state.\n\n**Style notes:**\n- Use `padding: 16px` internally (it sits within the main content's `p-3 xs:p-5` padding)\n- Background: `var(--sidebar-bg)` to match the drawer look\n- Bottom margin to separate from PatientSummaryTile\n- Border-radius: `var(--radius-sm)` on the whole container\n- Border: `1px solid var(--border)`\n\n### 2. Modify `MobileBottomNav.tsx`\n\n- **Remove** the \"More\" ` - ) - })} -
-
- ) -} diff --git a/src/components/MobileOverviewHeader.tsx b/src/components/MobileOverviewHeader.tsx index 7709567..c620bda 100644 --- a/src/components/MobileOverviewHeader.tsx +++ b/src/components/MobileOverviewHeader.tsx @@ -66,7 +66,7 @@ export function MobileOverviewHeader({ onSearchClick }: MobileOverviewHeaderProp > {/* Logo + Search row */}
- +