diff --git a/specs/DESIGN-LANGUAGE.md b/specs/DESIGN-LANGUAGE.md new file mode 100644 index 00000000..4fc1da42 --- /dev/null +++ b/specs/DESIGN-LANGUAGE.md @@ -0,0 +1,495 @@ +# Keyboardia Design Language + +A comprehensive guide to the visual and interaction language of Keyboardia. + +--- + +## Brand Identity + +### Tagline + +**Create/Collaborate. Remix. Share.** + +Three distinct concepts, each with its own color: +- **Create/Collaborate** (orange) — The Glitch angle: instant creation, real-time multiplayer +- **Remix** (purple) — The GitHub angle: fork any session, build on others' work +- **Share** (teal) — The SoundCloud angle: publish and discover music + +### Logo + +`keyboardia.svg` — Clean, minimal mark at 80-120px on landing page. + +--- + +## Color System + +### CSS Variables (Defined in index.css) + +These are the actual CSS custom properties defined in `:root`: + +```css +/* Backgrounds */ +--color-bg: #121212; +--color-surface: #1e1e1e; +--color-surface-elevated: #2a2a2a; + +/* Borders */ +--color-border: #3a3a3a; +--color-border-light: #4a4a4a; + +/* Accent */ +--color-accent: #e85a30; +--color-accent-light: #f07048; +--color-accent-glow: rgba(232, 90, 48, 0.6); + +/* Playhead */ +--color-playhead: #ffffff; +--color-playhead-glow: rgba(255, 255, 255, 0.4); + +/* Semantic */ +--color-secondary: #d4a054; +--color-info: #4a9ece; +--color-success: #4abb8b; +--color-purple: #9b59b6; + +/* Text */ +--color-text: rgba(255, 255, 255, 0.87); +--color-text-muted: rgba(255, 255, 255, 0.5); +``` + +### Background Layers (Conceptual) + +A progression from deepest black to elevated surfaces. Not all are CSS variables — some are used as literal hex values: + +| Hex | Usage | CSS Variable? | +|-----|-------|---------------| +| `#0a0a0a` | Landing page, fullscreen backgrounds | No | +| `#121212` | App background, root | `--color-bg` | +| `#1a1a1a` | Transport bar, panels | No | +| `#1e1e1e` | Cards, panels, bottom sheets | `--color-surface` | +| `#252525` | Input backgrounds, controls | No | +| `#2a2a2a` | Elevated cards, inactive steps | `--color-surface-elevated` | +| `#333333` | Hover states, active surfaces | No | + +### Border Progression (Conceptual) + +| Hex | Usage | CSS Variable? | +|-----|-------|---------------| +| `#333333` | Panel borders | No | +| `#3a3a3a` | Default borders | `--color-border` | +| `#444444` | Control borders | No | +| `#4a4a4a` | Hover borders, beat markers | `--color-border-light` | +| `#555555` | Interactive elements | No | +| `#666666` | Focused elements | No | + +### Brand Orange (Primary Accent) + +The signature color — energy, action, active state. + +| Token | Hex | Usage | +|-------|-----|-------| +| `--color-accent` | `#e85a30` | Active steps, CTA buttons, primary actions | +| `--color-accent-light` | `#f07048` | Hover states on accent | +| `--color-brand` | `#ff6b35` | Brand text, headlines | +| `--color-accent-glow` | `rgba(232, 90, 48, 0.6)` | Active step glow, shadows | + +### Semantic Colors + +| Token | Hex | Meaning | Examples | +|-------|-----|---------|----------| +| `--color-purple` | `#9b59b6` | Modes, Parameter Locks | Chromatic mode, p-lock borders, Remix word | +| `--color-info` | `#4a9ece` | Pitch, Selection | Pitch badges, selected state | +| `--color-success` | `#4abb8b` | Positive, Source | Copy source, add buttons | +| `--color-secondary` | `#d4a054` | Volume, Warmth | Volume badges | +| `--color-teal` | `#4ecdc4` | Multiplayer, Share | Share word, avatar rings, presence | +| `--color-cyan` | `#00bcd4` | Effects, FX | Effects panel, FX toggle | + +### State Colors + +| State | Color | Hex | +|-------|-------|-----| +| Playing | White border | `#ffffff` | +| Error | Red | `#e74c3c` | +| Warning | Yellow | `#f1c40f` | +| Muted | Yellow | `#f1c40f` | +| Solo | Purple | `#9b59b6` | +| Recording | Red pulse | `#e74c3c` | +| Bypassed | Orange-red | `#ff5722` | +| Active | Green | `#4caf50` | + +### Text Hierarchy + +| Level | Color | CSS Variable | Usage | +|-------|-------|--------------|-------| +| Primary | `rgba(255, 255, 255, 0.87)` | `--color-text` | Headlines, values, active labels | +| Muted | `rgba(255, 255, 255, 0.5)` | `--color-text-muted` | Hints, inactive labels, descriptions | +| Disabled | `#666666` | — | Disabled controls, timestamps | +| Faint | `#444444` | — | Subtle hints, placeholders | + +Note: Only two text colors are defined as CSS variables. Other opacity levels (0.9, 0.7, etc.) are used directly in CSS where needed. + +--- + +## Effects Color Coding + +Each effect has a distinct color for quick identification: + +| Effect | Color | Hex | +|--------|-------|-----| +| Reverb | Purple | `#9c27b0` | +| Delay | Blue | `#2196f3` | +| Chorus | Green | `#4caf50` | +| Distortion | Orange-red | `#ff5722` | + +This extends to slider thumbs, labels, and indicators. + +--- + +## Instrument Category Colors + +Dynamic `--category-color` CSS variable set per instrument category in SamplePicker. Defined in `sample-constants.ts`: + +| Category | Hex | Color Name | Contents | +|----------|-----|------------|----------| +| Drums | `#e67e22` | Orange | Kick, Snare, Hi-Hat, Clap, etc. | +| Bass | `#9b59b6` | Purple | Bass, Sub, synth basses | +| Keys | `#3498db` | Blue | Piano, Rhodes, Wurli, Organ | +| Leads | `#e91e63` | Pink | Lead, Pluck, synth leads | +| Pads | `#2ecc71` | Green | Pad, Chord, synth pads | +| FX | `#00bcd4` | Cyan | Zap, Noise, synth FX | + +--- + +## Multiplayer Identity Colors + +Players get Google Docs-style anonymous identities (e.g., "Red Fox", "Teal Penguin"). The identity system uses 18 colors × 73 animals = 1,314 unique combinations. + +Defined in `utils/identity.ts`: + +```typescript +const IDENTITY_COLORS = [ + '#E53935', // Red + '#D81B60', // Pink + '#8E24AA', // Purple + '#5E35B1', // Deep Purple + '#3949AB', // Indigo + '#1E88E5', // Blue + '#039BE5', // Light Blue + '#00ACC1', // Cyan + '#00897B', // Teal + '#43A047', // Green + '#7CB342', // Light Green + '#C0CA33', // Lime + '#FDD835', // Yellow + '#FFB300', // Amber + '#FB8C00', // Orange + '#F4511E', // Deep Orange + '#6D4C41', // Brown + '#757575', // Grey +]; +``` + +### How It Works + +- Player ID is hashed to deterministically select a color + animal +- Same player ID always gets the same identity +- CSS variables `--player-color`, `--player-color-light`, `--player-color-glow` are set per-player + +### Cursor & Attribution + +- Player cursors show identity color with animal name tooltip +- Remote step changes flash with player's color (600ms animation) +- Avatar stack shows colored circles with animal initials + +--- + +## Typography + +### Font Stack + +```css +font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +``` + +Native system fonts for performance and platform consistency. + +### Weights + +| Weight | Usage | +|--------|-------| +| 400 | Body text, descriptions | +| 500 | Track names, sample names | +| 600 | Button labels, section headers | +| 700 | Headlines, numeric values (BPM, step count) | +| 800 | Landing page brand name | + +### Sizes + +| Size | Usage | +|------|-------| +| `5rem` | Landing page brand (desktop) | +| `3rem` | Landing page brand (mobile) | +| `2rem` | Landing tagline | +| `1.25rem` | CTA buttons | +| `1rem` | Section headers, feature titles | +| `0.875rem` | Body text | +| `12px` | Button labels, control labels | +| `11px` | Parameter values, small labels | +| `10px` | Upper-case labels (UPPERCASE) | +| `9px` | Badges, tiny labels | +| `8px` | Micro labels | + +### Monospace + +```css +font-family: monospace; +``` + +Used for: +- Numeric displays (BPM, step count) +- Parameter values +- Pitch labels (+12, -5) + +--- + +## Spacing Scale + +Based on 4px increments. These are conceptual guidelines — not CSS variables: + +| Name | Value | Usage | +|------|-------|-------| +| xs | 4px | Icon gaps, tight spacing | +| sm | 8px | Between related elements | +| md | 12px | Button padding, card padding | +| lg | 16px | Section spacing | +| xl | 24px | Major section gaps | +| 2xl | 32px | Landing page sections | +| 3xl | 48px | Feature card gaps | + +--- + +## Border Radius + +Conceptual scale — values used directly in CSS, not as variables: + +| Name | Value | Usage | +|------|-------|-------| +| xs | 2-3px | Badges, step cells, tiny elements | +| sm | 4px | Step cells (desktop) | +| md | 6px | Buttons, input fields | +| lg | 8px | Panels, track rows, thumbnails, step cells (mobile) | +| xl | 12px | Bottom sheets, large cards, containers | +| pill | 60px | CTA buttons | +| circle | 50% | Play button, avatars | + +--- + +## Step Cell States + +The step sequencer grid is the core interface: + +| State | Background | Border | Notes | +|-------|------------|--------|-------| +| Inactive | `#2a2a2a` | `#3a3a3a` | Empty step | +| Inactive:hover | `#3a3a3a` | `#4a4a4a` | Hover feedback | +| Active | `#e85a30` | `#f07048` | Has a note | +| Active:hover | `#f07048` | + white glow | Editable hint | +| Playing | any | `#ffffff` 3px | Playhead position | +| Selected | any | `#4a9ece` | P-lock editing | +| Has P-lock | any | `#9b59b6` | Has parameter lock | +| Dimmed | 20% opacity | — | Beyond track length | +| Beat start | — | Left border `#4a4a4a` 3px | Every 4 steps | + +--- + +## Animation Principles + +### Timings + +| Type | Duration | Easing | +|------|----------|--------| +| Micro-interaction | 100-150ms | `ease` | +| State change | 150-200ms | `ease` or `ease-out` | +| Hover transitions | 200ms | `ease` | +| Landing page entrance | 1000ms (1s) | `ease-out` | +| Staggered entrance | +200ms per item | `ease-out` | +| Exit | 200ms | `ease-in` | + +### Landing Page Sequence + +Staggered entrance for dramatic effect: + +1. **0.0s** — Logo (scale + fade) +2. **0.2s** — Brand name (slide up) +3. **0.4s** — Tagline (slide up) +4. **0.6s** — CTA button (slide up) +5. **0.8s** — Features (slide up) +6. **1.0s** — Step demo (slide up) +7. **1.2s** — Examples section + +### What Animates + +**Do animate:** +- Entrance/exit of elements +- Hover states (subtle) +- Button press feedback +- Selection states +- Toast notifications +- Bottom sheet open/close + +**Don't animate:** +- Playhead (causes flicker at high BPM) +- Step activation (too frequent) +- Parameter value changes + +### Step Grid Demo + +The landing page step grid animates at 150ms intervals, showing a beat pattern. Single row, 16 steps. + +--- + +## Shadows + +| Level | Shadow | Usage | +|-------|--------|-------| +| Subtle | `0 1px 2px rgba(0,0,0,0.2)` | Badges, small elevations | +| Card | `0 2px 8px rgba(0,0,0,0.3)` | Cards, dropdowns | +| Panel | `0 4px 12px rgba(0,0,0,0.3)` | Panels, popovers | +| Modal | `0 8px 32px rgba(0,0,0,0.5)` | Bottom sheets, modals | +| CTA | `0 4px 20px rgba(232, 90, 48, 0.4)` | Primary CTA button | + +--- + +## Icon Language + +Emoji are used sparingly for feature descriptions: + +| Emoji | Meaning | +|-------|---------| +| 🎹 | Creation, sequencer | +| 👥 | Multiplayer, collaboration | +| 🔀 | Remix, fork | +| ▶ / ⏸ | Play / Pause | +| ● | Drum mode | +| ♪ | Chromatic mode | +| ↕ | Draggable control | +| → | Navigation, next | + +--- + +## Responsive Breakpoints + +| Breakpoint | Width | Layout Changes | +|------------|-------|----------------| +| Mobile | < 480px | Single column, larger touch targets (48px), horizontal scroll | +| Tablet | 480-768px | 2-column where appropriate, 44px touch targets | +| Desktop | > 768px | Full layout, 36px step cells, grid layouts | + +### Mobile-Specific + +- Minimum touch target: 44px (preferably 48px) +- Horizontal scroll for step grid +- Bottom sheets instead of dropdowns +- Transport bar visible (hidden on desktop) +- Scroll snap for step cells + +--- + +## Accessibility + +### Color Contrast + +- Text on dark backgrounds: minimum 4.5:1 ratio +- Active steps: high contrast orange on near-black +- Disabled states: reduced opacity (0.4-0.5) + +### Focus States + +Components should support keyboard navigation with visible focus states: + +```css +:focus-visible { + outline: 2px solid var(--color-info); + outline-offset: 2px; +} +``` + +**Status**: Not yet implemented globally. Individual components handle focus styling. + +### Motion (Aspirational) + +Respect `prefers-reduced-motion`: +- Disable staggered entrances +- Reduce animation durations +- Keep essential feedback (playhead) + +**Status**: Not yet implemented. Future improvement. + +--- + +## Grid Thumbnails + +Session previews use a condensed step grid as "album art": + +- 4 rows (tracks) × 16 columns (steps) +- Active steps: `#ff6b35` +- Inactive steps: `#2a2a2a` +- 2px gap between cells +- Background: `#1a1a1a` +- Border radius: 8px top, 0 bottom (card layout) + +For tracks > 16 steps, condense using OR logic: +``` +column[n] = steps[n*2] || steps[n*2+1] +``` + +--- + +## Dark Mode Only + +Keyboardia is dark-mode only. No light theme planned. + +Rationale: +- Studio/music software convention +- Better for low-light environments +- Reduces eye strain during extended sessions +- LEDs and active elements "glow" against dark + +--- + +## Design Principles Summary + +From UI-PHILOSOPHY.md: + +1. **Controls live where they act** — Buttons on the thing they affect +2. **Visual feedback is immediate** — No confirmation dialogs +3. **Modes are visible, not hidden** — State is always shown +4. **Progressive disclosure through gesture** — Click vs Shift+click +5. **One screen, no navigation** — Everything visible at once + +### The Test + +For any new feature: +1. Can I see the effect immediately? +2. Is the control on or near the thing it affects? +3. Does it require mode switching or navigation? +4. Would this work on a device with no screen? +5. Can I discover it by experimenting? + +--- + +## File Reference + +### CSS Files +- `/app/src/index.css` — CSS variables, global tokens +- `/app/src/components/LandingPage/LandingPage.css` — Landing page styles +- `/app/src/components/StepCell.css` — Step sequencer cells +- `/app/src/components/EffectsPanel.css` — Effects panel +- `/app/src/components/TrackRow.css` — Track row layout +- `/app/src/components/TransportBar.css` — Mobile transport +- `/app/src/components/SamplePicker.css` — Instrument picker (uses `--category-color`) +- `/app/src/components/AvatarStack.css` — Multiplayer avatars + +### TypeScript Files +- `/app/src/components/sample-constants.ts` — Instrument category colors +- `/app/src/utils/identity.ts` — Multiplayer identity colors (18-color palette) diff --git a/specs/LANDING-PAGE.md b/specs/LANDING-PAGE.md new file mode 100644 index 00000000..ce681326 --- /dev/null +++ b/specs/LANDING-PAGE.md @@ -0,0 +1,807 @@ +# Landing Page Specification + +## Product Vision + +Keyboardia is a real-time multiplayer collaborative music synthesizer with GitHub-style sharing, remixing, and publishing. + +**Tagline:** Create/Collaborate. Remix. Share. + +- **Create/Collaborate** — The Glitch angle: instant creation, real-time multiplayer +- **Remix** — The GitHub angle: fork any session, build on others' work +- **Share** — The SoundCloud angle: publish and discover music + +--- + +## Design Philosophy + +### What We Kept +- Solid dark background (#0a0a0a) +- Solid brand color (#ff6b35, #e85a30) +- Staggered entrance animations +- Animated step grid demo at bottom +- Three-word colored tagline + +### What We Removed +- Gradients (too busy) +- Pulsing animations (distracting) +- Floating music notes (cheesy) +- Glowing orb (unnecessary) +- Subtitle (redundant) + +### Core Principle +The landing page should feel **confident and minimal**. Let the product speak. The step grid demo at the bottom provides visual interest without overwhelming. + +--- + +## Layout + +### Desktop (≥768px) + +``` +┌────────────────────────────────────────────────────────────────┐ +│ │ +│ [keyboardia.svg] │ +│ │ +│ KEYBOARDIA │ +│ │ +│ Create/Collaborate. Remix. Share. │ +│ (orange) (purple) (teal) │ +│ │ +│ [ Start your first session → ] │ +│ │ +│ ──────────────────────────────────────────────────────────── │ +│ │ +│ 🎹 Instant Creation 👥 Multiplayer 🔀 Remix Anything │ +│ Jump straight into Share a link. Fork any session. │ +│ a step sequencer. Jam together Build on others' │ +│ in real-time. work. │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────┐ │ +│ │ ▓░░░▓░░░▓░░░▓░ │ ← Step grid demo │ +│ │ ░░▓░░░▓░░░▓░░░ │ (animated) │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ░░░░▓░░░░░░░▓░ │ │ +│ └────────────────┘ │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ Examples to remix │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ ▓▓░░▓▓░░▓▓░░ │ │ ▓░▓░▓░▓░▓░▓░ │ │ ▓▓▓░░░▓▓▓░░░ │ │ +│ │ ░░▓▓░░▓▓░░▓▓ │ │ ░▓░▓░▓░▓░▓░▓ │ │ ░░░▓▓▓░░░▓▓▓ │ │ +│ │ ▓░▓░▓░▓░▓░▓░ │ │ ▓▓░░▓▓░░▓▓░░ │ │ ▓░░▓░░▓░░▓░░ │ │ +│ │ ░▓░▓░▓░▓░▓░▓ │ │ ░░▓▓░░▓▓░░▓▓ │ │ ░▓▓░▓▓░▓▓░▓▓ │ │ +│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ +│ │ Four on the │ │ Polyrhythmic │ │ Trap Beat │ │ +│ │ Floor │ │ Evolution │ │ │ │ +│ │ 120 BPM │ │ 118 BPM │ │ 140 BPM │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### Mobile (<768px) + +``` +┌─────────────────────┐ +│ │ +│ [keyboardia.svg] │ +│ │ +│ KEYBOARDIA │ +│ │ +│ Create/Collaborate │ +│ Remix. Share. │ +│ │ +│ [Start first session]│ +│ │ +├─────────────────────┤ +│ │ +│ 🎹 Instant Creation│ +│ Jump straight into │ +│ a step sequencer. │ +│ │ +│ 👥 Multiplayer │ +│ Share a link. │ +│ Jam in real-time. │ +│ │ +│ 🔀 Remix Anything │ +│ Fork any session. │ +│ │ +├─────────────────────┤ +│ ┌───────────────┐ │ +│ │ ▓░░░▓░░░▓░░░▓ │ │ +│ │ ░░▓░░░▓░░░▓░░ │ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ░░░░▓░░░░░░░▓ │ │ +│ └───────────────┘ │ +│ (step grid demo) │ +│ │ +├─────────────────────┤ +│ │ +│ Examples to remix │ +│ │ +│ ←────────────────→ │ (horizontal scroll) +│ ┌────────┐┌────────┐│ +│ │▓▓░░▓▓░░││▓░▓░▓░▓░││ +│ │░░▓▓░░▓▓││░▓░▓░▓░▓││ +│ │▓░▓░▓░▓░││▓▓░░▓▓░░││ +│ ├────────┤├────────┤│ +│ │Four on ││Poly- ││ +│ │Floor ││rhythmic││ +│ │120 BPM ││118 BPM ││ +│ └────────┘└────────┘│ +│ │ +└─────────────────────┘ +``` + +--- + +## Current Implementation + +### What Exists (`app/src/components/LandingPage/`) + +**LandingPage.tsx** +- Logo (keyboardia.svg) +- Brand name ("Keyboardia") +- Tagline with colored words +- CTA button ("Start your first session") +- Three feature cards (Instant Creation, Multiplayer, Remix Anything) +- Animated step grid demo at bottom + +**LandingPage.css** +- Solid #0a0a0a background +- Staggered entrance animations (logo → brand → tagline → CTA → features → demo) +- Mobile responsive breakpoint at 768px + +### Colors + +| Element | Color | +|---------|-------| +| Background | #0a0a0a | +| Brand text | #ff6b35 | +| CTA button | #e85a30 (slight variation) | +| Create word | #ff6b35 (orange) | +| Remix word | #9b59b6 (purple) | +| Share word | #4ecdc4 (teal) | +| Separators | rgba(255,255,255,0.3) | +| Active step | #e85a30 | +| Inactive step | #2a2a2a | + +--- + +## Example Sessions Feature + +### Overview + +The landing page showcases a curated selection of example sessions to inspire new users. These are hardcoded, published (immutable) sessions that demonstrate what's possible with Keyboardia. + +### Design Goals + +1. **Inspire** — Show creative potential through diverse musical examples +2. **Simple** — No API calls, no featured session management, just data +3. **Fresh** — Random subset on each page load keeps experience varied +4. **Action-oriented** — One click to listen, then remix + +### Data Structure + +```typescript +// app/src/data/example-sessions.ts + +interface ExampleSession { + uuid: string; // Published session UUID (links to /s/{uuid}) + name: string; // Display name + tempo: number; // BPM for display + tracks: ExampleTrack[]; // Simplified track data for thumbnail +} + +interface ExampleTrack { + steps: boolean[]; // Step pattern (up to 16 for thumbnail) +} + +// UUIDs are generated by the seed script and committed to this file. +// These placeholder UUIDs will be replaced with real ones after seeding. +export const EXAMPLE_SESSIONS: ExampleSession[] = [ + { + uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // Placeholder - replaced by seed script + name: "Four on the Floor", + tempo: 120, + tracks: [ + { steps: [true,false,false,false,true,false,false,false,true,false,false,false,true,false,false,false] }, + { steps: [false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false] }, + { steps: [true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false] }, + { steps: [false,false,false,false,false,false,false,false,true,false,false,false,false,false,false,false] }, + ] + }, + { + uuid: "b2c3d4e5-f678-90ab-cdef-234567890abc", // Placeholder - replaced by seed script + name: "Polyrhythmic Evolution", + tempo: 118, + tracks: [ + { steps: [true,false,false,false,false,false,true,false,false,false,false,false,false,true,false,false] }, + { steps: [false,false,true,false,false,true,false,false,true,false,false,false,false,true,false,false] }, + { steps: [true,false,true,false,true,false,true,true,false,true,false,true,false,true,true,false] }, + { steps: [true,false,true,true,false,true,false,true,true,false,true,false,true,true,false,true] }, + ] + }, + // ... 10-15 total examples (UUIDs generated by seed script) +]; +``` + +### Random Selection + +On each page load, select 3 random sessions from the pool: + +```typescript +function getRandomExamples(count: number = 3): ExampleSession[] { + const shuffled = [...EXAMPLE_SESSIONS].sort(() => Math.random() - 0.5); + return shuffled.slice(0, count); +} + +// Usage in component +function LandingPage() { + // Compute once on mount, stable for session + const [examples] = useState(() => getRandomExamples(3)); + // ... +} +``` + +### Grid Thumbnail Component + +The step pattern becomes the session's visual identity — like album art. + +**Condensing Logic (for tracks with >16 steps)** + +```typescript +function condenseSteps(steps: boolean[], targetColumns: number = 16): boolean[] { + if (steps.length <= targetColumns) { + return [...steps, ...Array(targetColumns - steps.length).fill(false)]; + } + + const ratio = steps.length / targetColumns; + const condensed: boolean[] = []; + + for (let i = 0; i < targetColumns; i++) { + const start = Math.floor(i * ratio); + const end = Math.floor((i + 1) * ratio); + condensed.push(steps.slice(start, end).some(Boolean)); // OR logic + } + + return condensed; +} +``` + +**Component** + +```typescript +function GridThumbnail({ tracks }: { tracks: ExampleTrack[] }) { + const displayTracks = tracks.slice(0, 4); // Max 4 rows + + return ( +
+ {displayTracks.map((track, i) => ( +
+ {condenseSteps(track.steps).map((active, j) => ( +
+ ))} +
+ ))} +
+ ); +} +``` + +**Styles** + +```css +.grid-thumbnail { + display: grid; + grid-template-rows: repeat(4, 1fr); + gap: 2px; + padding: 12px; + background: #1a1a1a; + border-radius: 8px 8px 0 0; + aspect-ratio: 16 / 4; +} + +.thumbnail-row { + display: grid; + grid-template-columns: repeat(16, 1fr); + gap: 2px; +} + +.thumbnail-cell { + aspect-ratio: 1; + background: #2a2a2a; + border-radius: 2px; +} + +.thumbnail-cell.active { + background: #e85a30; +} +``` + +### Example Card Component + +```typescript +function ExampleCard({ session }: { session: ExampleSession }) { + const navigate = () => { + window.location.href = `/s/${session.uuid}`; + }; + + return ( + + ); +} +``` + +### Click Behavior + +1. User clicks example card +2. Navigate to `/s/{uuid}` (published session) +3. User sees full grid (read-only, published session) +4. User can press play to listen +5. User clicks "Remix" to create their own editable copy + +### Examples Section Styles + +```css +.examples-section { + width: 100%; + max-width: 900px; + margin: 40px 0; +} + +.examples-section h2 { + text-align: center; + color: #888; + font-size: 1rem; + font-weight: 400; + margin-bottom: 20px; +} + +.examples-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.example-card { + background: none; + border: 1px solid #333; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; + overflow: hidden; +} + +.example-card:hover { + border-color: #ff6b35; + transform: translateY(-2px); +} + +.example-info { + padding: 12px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.example-name { + color: #fff; + font-size: 0.9rem; + font-weight: 500; +} + +.example-tempo { + color: #666; + font-size: 0.8rem; +} + +/* Mobile: horizontal scroll */ +@media (max-width: 768px) { + .examples-grid { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + gap: 12px; + padding-bottom: 12px; + } + + .example-card { + flex: 0 0 200px; + scroll-snap-align: start; + } +} +``` + +### Source Content + +Example sessions are derived from existing JSON files in `app/scripts/sessions/`: + +| File | Example Name | Key Feature | +|------|-------------|-------------| +| `polyrhythmic-evolution.json` | Polyrhythmic Evolution | Odd-length patterns (5,7,11,13,17,19,23 steps) | +| `afrobeat-groove.json` | Afrobeat Groove | Polyrhythmic groove | +| `ambient-soundscape.json` | Ambient Soundscape | Slow evolving textures | +| `edm-drop-section.json` | EDM Drop | Build and release | +| `progressive-house-build.json` | Progressive House | Layered progression | +| (to be created) | Four on the Floor | Classic house pattern | +| (to be created) | Trap Beat | Hi-hat rolls, 808 patterns | +| (to be created) | Breakbeat | Syncopated drums | +| (to be created) | Lo-Fi Beat | Relaxed, dusty | + +--- + +## Example Session Lifecycle + +### Overview + +Example sessions follow a **pre-commit workflow**: + +1. Create/edit JSON file in `app/scripts/sessions/` +2. Run seed script to publish to production server +3. Commit the resulting UUIDs to `app/src/data/example-sessions.ts` + +UUIDs are stable and permanent. Once published, an example session cannot be modified (immutable). + +### Seeding Process + +**Step 1: Create the seed script** + +```typescript +// app/scripts/seed-examples.ts + +import { readFileSync, readdirSync } from 'fs'; + +const API_BASE = 'https://keyboardia.com/api/sessions'; +const EXAMPLES_TO_SEED = [ + 'polyrhythmic-evolution.json', + 'afrobeat-groove.json', + 'ambient-soundscape.json', + // ... add files to seed +]; + +async function seedExamples() { + const results = []; + + for (const filename of EXAMPLES_TO_SEED) { + const json = readFileSync(`./sessions/${filename}`, 'utf-8'); + const data = JSON.parse(json); + + // 1. Create session + const createRes = await fetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + state: { + tracks: data.tracks, + tempo: data.tempo, + swing: data.swing ?? 0, + version: 1, + }, + name: data.name, + }), + }); + const { id } = await createRes.json(); + + // 2. Publish (make immutable) + await fetch(`${API_BASE}/${id}/publish`, { method: 'POST' }); + + // 3. Extract thumbnail data (first 4 tracks, condensed to 16 steps) + const thumbnailTracks = data.tracks.slice(0, 4).map(track => ({ + steps: condenseToThumbnail(track.steps, track.stepCount || 16), + })); + + results.push({ + uuid: id, + name: data.name, + tempo: data.tempo, + tracks: thumbnailTracks, + }); + + console.log(`✓ ${data.name} → ${id}`); + } + + // Output TypeScript for example-sessions.ts + console.log('\n// Copy to app/src/data/example-sessions.ts:\n'); + console.log(`export const EXAMPLE_SESSIONS = ${JSON.stringify(results, null, 2)};`); +} + +function condenseToThumbnail(steps: boolean[], stepCount: number): boolean[] { + const actualSteps = steps.slice(0, stepCount); + if (actualSteps.length <= 16) { + return [...actualSteps, ...Array(16 - actualSteps.length).fill(false)]; + } + // Condense using OR logic + const ratio = actualSteps.length / 16; + return Array.from({ length: 16 }, (_, i) => { + const start = Math.floor(i * ratio); + const end = Math.floor((i + 1) * ratio); + return actualSteps.slice(start, end).some(Boolean); + }); +} + +seedExamples(); +``` + +**Step 2: Run the script** + +```bash +cd app/scripts +npx ts-node seed-examples.ts +``` + +**Step 3: Commit the output** + +Copy the generated TypeScript to `app/src/data/example-sessions.ts` and commit. + +### Updating an Example + +Examples are **immutable once published**. To update: + +1. Modify the JSON file +2. Run seed script (creates NEW UUID) +3. Update `example-sessions.ts` with new UUID +4. Old UUID continues to work (redirects or shows archived version) + +--- + +## Nominating New Examples + +### Criteria + +An example session should: + +1. **Demonstrate a genre or technique** — Not just random notes +2. **Sound good on first play** — Immediately engaging +3. **Be visually distinctive** — Thumbnail should look different from others +4. **Use 4-8 tracks** — Enough complexity, not overwhelming +5. **Be 16-128 steps** — Long enough to develop, short enough to loop + +### Diversity Goals + +The example set should cover: + +- [ ] Classic 4/4 (house, techno) +- [ ] Breakbeat/hip-hop +- [ ] Polyrhythmic/world +- [ ] Ambient/atmospheric +- [ ] Melodic/synth-heavy +- [ ] Experimental/glitchy + +### How to Nominate + +1. **Create the session** in Keyboardia +2. **Export to JSON** (or create manually in `app/scripts/sessions/`) +3. **Add to the seed list** in `seed-examples.ts` +4. **Open a PR** with: + - The JSON file + - Updated `EXAMPLES_TO_SEED` array + - Brief description of what makes it a good example + +### JSON File Format + +```json +{ + "name": "Example Name", + "description": "Why this is a good example", + "tracks": [ + { + "id": "unique-track-id", + "name": "Track Name", + "sampleId": "kick", + "steps": [true, false, ...], + "parameterLocks": [null, {"pitch": 5}, ...], + "volume": 0.8, + "muted": false, + "playbackMode": "oneshot", + "transpose": 0, + "stepCount": 16 + } + ], + "tempo": 120, + "swing": 0 +} +``` + +--- + +## Published Session View + +When a user navigates to a published session (`/s/{uuid}` where `immutable: true`): + +### Visual Changes + +| Element | Normal Session | Published Session | +|---------|---------------|-------------------| +| Header | Session name (editable) | Session name + "Published" badge | +| Edit controls | Visible | Hidden | +| Step cells | Clickable (toggle) | Not clickable (display only) | +| Add track button | Visible | Hidden | +| Delete track button | Visible | Hidden | +| Transport | Play/Pause/Tempo/Swing | Play/Pause only (no editing) | +| Primary CTA | Share | **Remix** (prominent) | + +### Remix Button + +``` +┌─────────────────────────────────────────────────────┐ +│ [Published] Polyrhythmic Evolution │ +│ │ +│ [▶ Play] [ Remix → ] │ +│ │ +│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ +│ │ ██ │ │ │ ██ │ │ ██ │ │ │ │ +│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ +│ ... (read-only step grid) │ +└─────────────────────────────────────────────────────┘ +``` + +### Remix Flow + +1. User clicks "Remix" +2. `POST /api/sessions/{uuid}/remix` creates editable copy +3. URL updates to new session ID +4. User now has full edit controls +5. "Remixed from: [Original Name]" shown in header + +--- + +## Error Handling + +### Example Sessions + +Example sessions are hardcoded and committed to the repository. No runtime validation is performed on the landing page — the data is trusted. + +If an example UUID becomes invalid (e.g., session was deleted from the database), the user will see a "Session not found" error when clicking through. This is acceptable because: + +1. Example UUIDs are stable (published sessions are immutable) +2. Deletions would be intentional and rare +3. The fix is to update `example-sessions.ts` and redeploy + +**No API calls are made on the landing page to validate examples.** + +### Published Session Not Found + +If user navigates directly to a published session that doesn't exist: + +1. Show "Session not found" message +2. Offer "Create new session" button +3. Do NOT auto-create (user may have mistyped URL) + +--- + +## Open Questions + +### All Resolved + +| Question | Decision | Rationale | +|----------|----------|-----------| +| Seed at deploy vs pre-commit? | **Pre-commit** | UUIDs committed to repo, stable across deploys | +| How many examples? | **10-15** total, show **3** | Enough variety, not overwhelming | +| Click behavior? | Navigate to published session | User clicks Remix when ready | +| Random selection algorithm | **True random** | Simple, no genre weighting needed | +| Session name in URL | **No** — `/s/{uuid}` only | Keep URLs clean, avoid slug complexity | +| Analytics | **No** | Non-goal, not tracking example clicks | +| Audio preview on hover | **No** | Keep it simple, user clicks to hear | +| Localization | **No** | English only for example names | + +--- + +## Component Structure + +``` +LandingPage/ +├── LandingPage.tsx # Main layout (exists) +├── LandingPage.css # Styles (exists) +├── GridThumbnail.tsx # Reusable grid preview (to create) +├── GridThumbnail.css +├── ExamplesSection.tsx # Examples grid (to create) +├── ExamplesSection.css +└── index.ts # Exports (exists) +``` + +--- + +## Implementation Phases + +### Phase 1: Static Examples +1. Create `app/src/data/example-sessions.ts` with 3-5 initial examples +2. Create `GridThumbnail` component (static thumbnail) +3. Create `ExampleCard` component (click navigates to session) +4. Create `ExamplesSection` component +5. Add examples section to `LandingPage` +6. Style for desktop and mobile + +### Phase 2: Full Content +1. Create additional JSON session files (10-15 total) +2. Create seed script (`app/scripts/seed-examples.ts`) +3. Publish sessions to production with stable UUIDs +4. Update `EXAMPLE_SESSIONS` array with all sessions +5. Test random selection and navigation + +### Phase 3: Published Session View +1. Ensure published sessions load correctly at `/s/{uuid}` +2. Show "Published" badge on published sessions +3. Enable "Remix" button to create editable copy +4. Hide edit controls on published sessions +5. Make step cells read-only (non-interactive) + +--- + +## Dependencies + +### Existing (no changes needed) +- `LandingPage` component +- Session loading at `/s/{uuid}` +- CSS architecture + +### Changes Required +- Published session support (immutable flag) — exists in API +- Remix functionality from published sessions — exists +- Read-only mode for step cells — new + +--- + +## Non-Goals + +- No API to fetch featured sessions (hardcoded data only) +- No admin interface to manage examples +- No analytics on which examples are clicked +- No user-generated featured content +- No templates section (keep it simple — users start blank or remix an example) +- No weighted/smart random selection (true random only) +- No session name slugs in URLs (`/s/{uuid}` only) +- No localization of example names +- No audio preview on hover (user clicks through to hear) + +--- + +## Routing + +```typescript +// main.tsx (current implementation) +const [showApp, setShowApp] = useState(false); +const [sessionId, setSessionId] = useState(null); + +useEffect(() => { + const path = window.location.pathname; + // UUIDs are 36 characters: 8-4-4-4-12 hex digits + const match = path.match(/^\/s\/([a-f0-9-]{36})$/); + if (match) { + setSessionId(match[1]); + setShowApp(true); + } +}, []); + +// "/" shows LandingPage +// "/s/{uuid}" shows App with session (UUID format only) +``` + +--- + +## Animation Timings + +Staggered entrance sequence: +1. **0.0s** — Logo entrance (scale + fade) +2. **0.2s** — Brand name (slide up + fade) +3. **0.4s** — Tagline (slide up + fade) +4. **0.6s** — CTA button (slide up + fade) +5. **0.8s** — Features section (slide up + fade) +6. **1.0s** — Step grid demo (slide up + fade) +7. **1.2s** — Examples section (to be added) + +All animations use `ease-out` timing.