diff --git a/ANIMATION_GUIDE.md b/ANIMATION_GUIDE.md new file mode 100644 index 0000000..e81f61a --- /dev/null +++ b/ANIMATION_GUIDE.md @@ -0,0 +1,1316 @@ +# Animation System Generation Guide for AI Agents + +Production animation system generation using perceptual timing research, easing curve psychophysics, spring mechanics, PAD-mapped motion personality, and scroll-driven reveal composition. Designed as agent-executable specification — every formula is code-ready. Calibrated for this project's specific design system, illustration vocabulary, and emotional targets. + +This guide is the motion counterpart to `COLOR_GUIDE.md` (color science, palette engineering), `SPATIAL_GUIDE.md` (spacing, curvature, proportions), and `ILLUSTRATION_GUIDE.md` (shape grammar, diagram construction). It takes three inputs — the validated color palette, spatial token system, and brand PAD emotional profile — and produces a complete, validated animation system: duration tokens, easing curves, spring presets, stagger algorithms, reveal sequences, and scroll-driven motion rules. + +Two phases: **Motion Strategy** (human-driven brand personality decisions grounded in perception science) and **Motion Engineering** (agent-executable math producing token values). The first phase establishes the kinetic personality of the brand. The second phase computes every token from that personality. + +**Prerequisite:** Complete `COLOR_GUIDE.md` Phases 1–3, `SPATIAL_GUIDE.md` Phases 1–2, and `ILLUSTRATION_GUIDE.md` Phases 1–4 first. This guide references the PAD emotional model, shape vocabulary, spatial token architecture, and semantic hue roles defined there. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +--- + +## Perceptual Timing Theory + +The foundation for all animation token values. These thresholds are the most empirically robust numbers in HCI — grounded in two landmark papers (Miller 1968; Card, Robertson & Mackinlay 1991) and replicated across five decades of usability research. + +### Human Perception Time Constants + +| Threshold | Value | Perceptual Meaning | Source | +|-----------|-------|-------------------|--------| +| Motion perception floor | ~40 ms | Below this, motion registers as a jump cut. No transition perceived. | Physiological: rod photoreceptor response | +| Instantaneous perception | ≤ 100 ms | System feels like a direct extension of user action. Causal link preserved. | Miller (1968); Nielsen (1993) | +| Action–feedback causal window | 100 ms | Any animation triggered by user action **must begin** within 100 ms or causality breaks | Miller (1968) via NNGroup | +| Human visual perception cycle | ~230 ms | Model Human Processor average perceptual cycle. Below this, sequential events merge. | Card, Moran & Newell (1983) | +| Animation fatigue onset | ≥ 500 ms | Anything longer than 400–500 ms is a net negative for user-triggered UI motion | NNGroup usability observations | +| Flow-of-thought disruption | > 1,000 ms | User feels "waited on." Mental task model breaks. Progress indicator required. | Miller (1968); Nielsen (1993) | +| Frame rate ceiling | 60 Hz (16.7 ms/frame) | Humans cannot reliably perceive differences above 60 fps | Physiological: flicker fusion | + +**Critical insight:** The 100 ms threshold applies to when animation *begins*, not when it ends. An animation with a `200ms animation-delay` before it starts creates a 200 ms blank gap that reads as system latency, not intentional design. The first frame of motion must appear within 100 ms of the triggering event. + +### Enter vs. Exit Asymmetry + +NNGroup documents a consistent directional asymmetry: elements entering the screen should animate ~25% longer than elements exiting. The asymmetry reflects the user's mental state: + +- **Enter (25% longer):** The user is waiting for content to appear. A slightly longer entrance feels considered. +- **Exit (25% shorter):** The user has already acted to dismiss. A fast exit feels responsive. + +``` +exit_duration = round(enter_duration × 0.75) +``` + +This rule applies to all enter/exit pairs: modals, drawers, dropdowns, tooltips, and scroll-reveal elements. + +### Perceived Performance Effects + +**Study (Nebraska-Lincoln, cited by NNGroup):** Users who saw a continuous moving progress bar were willing to wait **3× longer** than users who saw nothing. Satisfaction was higher even when objective wait time was identical. + +**NNGroup slideshow case study:** A widget that took 8 seconds to download received only **1%** of eye-tracking attention when users waited. Users who saw the completed version immediately spent **20%** of their time in that area. Conclusion: animation that substitutes for content is severely penalized. Animation that accompanies content arrival is beneficial. + +**Operative rule:** Reveal animations are beneficial to perceived performance only when they begin *as content arrives* (paint-time), not after it. Stagger sequences must complete within the first paint window (< 500 ms from first paint). + +--- + +## Phase 1: Motion Strategy (Human Judgment) + +Before computing animation tokens, ground the motion decisions in PAD-congruent personality. The agent assists with PAD alignment checks; the human makes the judgment call on brand kinetic personality. + +### Kinetic Personality Framework + +Animation operates on all three PAD axes simultaneously: + +| Motion Property | Primary PAD Axis | Direction | Research Basis | +|----------------|-----------------|-----------|---------------| +| Duration (speed) | Arousal | Faster ↑ Arousal; Slower ↓ Arousal | NNGroup timing research; Card et al. 1983 | +| Easing curve type | Pleasure | Expressive (bouncy) ↑ Pleasure; Productive (precise) ↓ Pleasure | IBM Carbon expressive/productive distinction | +| Translate distance | Arousal | Larger offset ↑ Arousal | Practitioner convergence (Valhead, Comeau) | +| Spring bounce | Pleasure | Higher bounce ↑ Pleasure | Apple WWDC 2023 spring animation principles | +| Stagger spread | Arousal | Tighter stagger ↑ Arousal (compressed reveal) | Card/Moran/Newell perception cycle threshold | +| Animation direction | — | Semantic only — matches spatial content model | NNGroup directionality research | +| Scale amplitude | Arousal | Larger scale range ↑ Arousal | Practitioner observation | + +### Motion Style: Productive vs. Expressive + +IBM Carbon (the most precisely specified open design system) formalizes two motion styles. The distinction maps directly to brand PAD target: + +| Style | Easing Character | Duration Bias | PAD Target | Appropriate For | +|-------|-----------------|---------------|------------|----------------| +| **Productive** | Sharp ease-out, precise settle | Shorter (50–300 ms) | Low A, Med D | Task UIs, technical tools, documentation | +| **Expressive** | Broader arc, more dramatic | Longer (200–500 ms) | High P, High A | Marketing, onboarding, consumer apps | + +**Design rule:** Select one style as the system default. Use the other sparingly — at most 1–2 "expressive moments" per page (hero entrance, primary CTA activation). Mixing styles without a hierarchy creates visual incoherence. + +### Kinetic Personality by Brand PAD Profile + +| Target PAD | Duration Bias | Stagger | Easing | Spring Bounce | Translate | +|-----------|--------------|---------|--------|---------------|-----------| +| Low A, Low D, High P (calm, warm) | Longer (200–350 ms) | 80–100 ms | Expressive entrance | 0.10–0.15 | 8–12px | +| Low A, Med D (technical, precise) | Moderate (150–250 ms) | 60–80 ms | Productive entrance | 0.0 | 8px | +| Med A, Med D (balanced, professional) | Moderate (200–300 ms) | 70–90 ms | Productive standard | 0.0–0.10 | 8–12px | +| High A, High D (energetic, urgent) | Shorter (100–200 ms) | 40–60 ms | Productive exit-biased | 0.0 | 4–8px | + +### Input: Brand Motion Profile + +The only human judgment calls. Everything downstream is computable math. + +| Parameter | Value | Rationale | +|-----------|-------|-----------| +| Target Pleasure | Low / Medium / High | From brand PAD profile | +| Target Arousal | Low / Medium / High | From brand PAD profile | +| Target Dominance | Low / Medium / High | From brand PAD profile | +| Motion style | Productive / Expressive / Hybrid | From brand context (task vs. marketing) | +| Default entrance type | Fade-translate / Fade-only / Scale | From content type and diagram density | + +--- + +## Phase 2: Motion Engineering (Agent Math) + +From this point forward, the agent generates the animation token system autonomously. The human provides perceptual feedback during validation. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +### Duration Token Generation + +#### Base Duration Formula + +```python +def compute_duration_tokens(arousal_level: str) -> dict: + """ + Generate duration tokens from brand arousal target. + + arousal_level: 'low' | 'medium' | 'high' + + Anchored to IBM Carbon's duration system (fast-01 through slow-02) + with arousal-based scaling. + + Research basis: + - 70ms floor: IBM Carbon fast-01 (toggle/button state) + - 400ms ceiling: NNGroup animation ceiling for user-triggered transitions + - 700ms ambient: IBM Carbon slow-02 (background overlay only) + """ + + # Base values from IBM Carbon, validated against NNGroup thresholds + BASE_TOKENS = { + 'instant': 70, # toggle, checkbox, button state — at perception floor + 'fast': 110, # opacity/color — no spatial displacement + 'subtle': 150, # small spatial move (<= 16px), icon swap + 'moderate': 240, # modal, drawer, dropdown, notification + 'deliberate': 400, # large panel, hero entrance, full-section reveal + 'ambient': 700, # background overlay, dimming — not user-triggered + } + + # Arousal scaling: lower arousal = slightly longer (more deliberate) + # Upper-bounded so we never exceed 500ms for user-triggered actions + AROUSAL_SCALE = { + 'low': 1.15, # calm, deliberate — extend by 15% + 'medium': 1.00, # balanced — no adjustment + 'high': 0.85, # energetic — compress by 15% + } + + scale = AROUSAL_SCALE[arousal_level] + tokens = {} + + for name, base_ms in BASE_TOKENS.items(): + if name == 'ambient': + # Ambient is always fixed — not user-triggered + tokens[name] = base_ms + else: + raw = base_ms * scale + # Round to nearest 10ms for legibility + tokens[name] = round(raw / 10) * 10 + # Enforce perception floor: never below 40ms + tokens[name] = max(40, tokens[name]) + # Enforce user-trigger ceiling: never above 500ms + if name != 'deliberate': + tokens[name] = min(500, tokens[name]) + + # Compute exit variants (75% of enter duration per NNGroup asymmetry rule) + for name in list(tokens.keys()): + if name not in ('instant', 'ambient'): + exit_ms = round(tokens[name] * 0.75 / 10) * 10 + tokens[f'{name}_exit'] = max(40, exit_ms) + + return tokens + +# Example execution for medium arousal (technical documentation brand): +# tokens = compute_duration_tokens('medium') +# Output: +# instant: 70ms (exit: n/a — state changes have no exit) +# fast: 110ms (exit: 80ms) +# subtle: 150ms (exit: 110ms) +# moderate: 240ms (exit: 180ms) +# deliberate: 400ms (exit: 300ms) +# ambient: 700ms (exit: n/a — ambient is always fade, no exit) +``` + +#### Duration Token CSS Output + +```css +/* Animation duration tokens + Generated from: compute_duration_tokens(arousal_level) + Research: IBM Carbon duration scale; NNGroup 100–500ms window; Miller (1968) */ + +--duration-instant: 70ms; /* toggle, checkbox, button state */ +--duration-fast: 110ms; /* opacity/color, badge update */ +--duration-subtle: 150ms; /* small spatial move, icon swap */ +--duration-moderate: 240ms; /* modal, drawer, dropdown */ +--duration-deliberate: 400ms; /* large panel, hero, section reveal */ +--duration-ambient: 700ms; /* background overlay only — not user-triggered */ + +/* Exit variants — 75% of enter (NNGroup asymmetry rule) */ +--duration-fast-exit: 80ms; +--duration-subtle-exit: 110ms; +--duration-moderate-exit: 180ms; +--duration-deliberate-exit: 300ms; +``` + +--- + +### Easing Curve Generation + +#### Easing Semantics + +All design system authorities converge on three mandatory custom curves. The CSS default `ease` keyword must NOT be used — it is tuned for neither enter nor exit and produces a sluggish feeling. Use `cubic-bezier()` always. + +| Direction | Curve Behavior | Semantic Role | +|-----------|---------------|---------------| +| **Enter (ease-out)** | Fast start, slow finish | Elements arriving — decelerate to rest naturally | +| **Exit (ease-in)** | Slow start, fast finish | Elements departing — accelerate, feel intentionally leaving | +| **Reposition (ease-in-out)** | Slow–fast–slow | Elements traversing from visible A to visible B | +| **Linear** | Constant velocity | Spinners, progress bars, video scrubbing only | + +**Polaris (Shopify) principle:** "A snappy animation starts rapidly and slows down toward the end." This describes ease-out as the default for most UI motion — content decelerates into its final position so it becomes readable as quickly as possible. + +#### Easing Curve Formula + +```python +def compute_easing_tokens(motion_style: str, pleasure_level: str) -> dict: + """ + Generate cubic-bezier easing tokens. + + motion_style: 'productive' | 'expressive' + pleasure_level: 'low' | 'medium' | 'high' + + Productive curves: sharper arcs, tighter control points — task focus + Expressive curves: broader arcs, more dramatic deceleration — emotional moments + + Source: IBM Carbon easing system; validated against Material Design M3 easing + """ + + CURVES = { + 'productive': { + # IBM Carbon productive style + 'enter': (0.00, 0.00, 0.38, 0.9), # steep entry, long ease-out tail + 'exit': (0.20, 0.00, 1.00, 0.9), # slow start, sharp acceleration out + 'standard': (0.20, 0.00, 0.38, 0.9), # within-viewport repositioning + }, + 'expressive': { + # IBM Carbon expressive style — broader arc, more dramatic + 'enter': (0.00, 0.00, 0.30, 1.0), # very fast entry, long ease-out + 'exit': (0.40, 0.14, 1.00, 1.0), # slow start, high-energy exit + 'standard': (0.40, 0.14, 0.30, 1.0), # dramatic repositioning + } + } + + # Pleasure interpolation: high pleasure → blend toward expressive even in productive systems + # This adds subtle warmth to the productive curves without full expressive adoption + curves = CURVES[motion_style].copy() + + if motion_style == 'productive' and pleasure_level == 'high': + # Soften the productive enter curve slightly toward expressive + curves['enter'] = (0.00, 0.00, 0.32, 0.95) + + # Material Design M3 equivalents for reference validation + # enter (ease-out): cubic-bezier(0.05, 0.7, 0.1, 1.0) ← M3 standard + # exit (ease-in): cubic-bezier(0.3, 0.0, 1.0, 1.0) ← M3 standard + # symmetric (ease-in-out): cubic-bezier(0.2, 0.0, 0.0, 1.0) ← M3 standard + + return { + 'enter': f'cubic-bezier{curves["enter"]}', + 'exit': f'cubic-bezier{curves["exit"]}', + 'standard': f'cubic-bezier{curves["standard"]}', + 'linear': 'linear', + } + +# For productive motion style (technical documentation, medium pleasure): +# enter: cubic-bezier(0.00, 0.00, 0.38, 0.9) +# exit: cubic-bezier(0.20, 0.00, 1.00, 0.9) +# standard: cubic-bezier(0.20, 0.00, 0.38, 0.9) +# linear: linear +``` + +#### Easing Token CSS Output + +```css +/* Easing curve tokens + Generated from: compute_easing_tokens(motion_style, pleasure_level) + Research: IBM Carbon productive/expressive system; Shopify Polaris "snappy" principle */ + +--ease-enter: cubic-bezier(0.00, 0.00, 0.38, 0.9); /* entrance: decelerate to rest */ +--ease-exit: cubic-bezier(0.20, 0.00, 1.00, 0.9); /* exit: accelerate away */ +--ease-standard: cubic-bezier(0.20, 0.00, 0.38, 0.9); /* reposition within viewport */ +--ease-linear: linear; /* spinners, progress, video only */ +``` + +--- + +### Spring Preset Generation + +Springs are for gesture-continuation and physics-based interactions only. They maintain velocity continuity: if a user releases a drag at speed, a spring correctly inherits that velocity and continues from it. A `cubic-bezier` animation cannot do this — it always starts from zero velocity. + +#### When to Use Springs vs. Cubic-Bezier + +| Trigger | Use | Rationale | +|---------|-----|-----------| +| User gesture release (swipe, drag) | Spring | Velocity continuity is required | +| Button click / toggle / keyboard | Cubic-bezier | Zero initial velocity; spring adds no value | +| Multi-property animation via gesture | Spring | Each property settles naturally at different rates | +| Modal open (no gesture velocity) | Cubic-bezier | Simpler; predictable duration | +| Pull-to-refresh, card flick | Spring | Physical continuation of gesture | +| Page-load reveal | Cubic-bezier | No user velocity to continue | + +#### Spring Parameter Formula + +```python +import math + +def compute_spring_presets(pleasure_level: str, arousal_level: str) -> dict: + """ + Generate spring animation presets from PAD targets. + + Returns parameters for both: + - Framer Motion / react-spring (stiffness, damping, mass) + - Apple SwiftUI perceptual model (duration, bounce) + + Research basis: + - Apple WWDC 2023: bounce=0.0 (critically damped) for standard UI + - Apple: bounce=0.1-0.2 only for gesture-driven physical continuation + - Framer Motion snappy: stiffness 300, damping 25 (designer docs) + + Critical damping coefficient: ζ = damping / (2 × sqrt(stiffness × mass)) + ζ = 1.0 → no overshoot (critically damped) + ζ < 1.0 → underdamped (bouncy) + ζ > 1.0 → overdamped (sluggish) + """ + + # Apple perceptual bounce by pleasure level + # bounce=0 = critically damped (pleasure-neutral, precise) + # bounce>0 = underdamped (warm, playful) + BOUNCE_BY_PLEASURE = { + 'low': 0.00, # no overshoot — clinical, precise + 'medium': 0.05, # barely perceptible tail — hint of warmth + 'high': 0.15, # gentle follow-through — approachable + } + + # Duration modifier by arousal (perceptual duration, not hard TTL) + DURATION_BY_AROUSAL = { + 'low': 350, # deliberate, calm + 'medium': 280, # balanced + 'high': 200, # snappy + } + + bounce = BOUNCE_BY_PLEASURE[pleasure_level] + perceptual_duration = DURATION_BY_AROUSAL[arousal_level] + + # Convert to Framer Motion physics parameters + # stiffness = (2π / period)² × mass, simplified for UI: + # period ≈ perceptual_duration / 1000 (in seconds) + # stiffness ≈ (2π / (period * 0.9))² * mass + mass = 1.0 + period = perceptual_duration / 1000.0 * 0.9 # 0.9 factor: spring settles faster than full period + stiffness = round((2 * math.pi / period) ** 2 * mass) + + # Damping from bounce level: + # ζ = 1 - bounce (roughly; Apple's model) + # damping = 2 × ζ × sqrt(stiffness × mass) + zeta = max(0.5, 1.0 - bounce) # floor at 0.5 — never oscillate more than half-cycle in UI + damping = round(2 * zeta * math.sqrt(stiffness * mass)) + + return { + # Standard spring: no overshoot — for non-gesture UI elements + 'ui': { + 'stiffness': stiffness, + 'damping': damping, + 'mass': mass, + 'apple_duration': perceptual_duration, + 'apple_bounce': bounce, + }, + # Gesture spring: slight follow-through — for drag-release + 'gesture': { + 'stiffness': round(stiffness * 0.65), # softer = more travel after release + 'damping': round(damping * 0.48), # less damping = more oscillation + 'mass': mass, + 'apple_duration': round(perceptual_duration * 1.1), + 'apple_bounce': min(bounce + 0.15, 0.30), # add follow-through; cap at 0.30 + }, + # Snappy spring: for toggles and confirmations that need physicality + 'snappy': { + 'stiffness': round(stiffness * 1.5), # stiffer = faster + 'damping': round(damping * 1.1), # slightly overdamped = no bounce + 'mass': mass, + 'apple_duration': round(perceptual_duration * 0.7), + 'apple_bounce': 0.00, + }, + } + +# Validate critical damping ratio for each preset: +def validate_damping_ratio(stiffness: float, damping: float, mass: float) -> float: + """Returns ζ — should be >= 1.0 for standard UI, >= 0.7 for gesture springs.""" + return damping / (2 * math.sqrt(stiffness * mass)) +``` + +--- + +### Stagger Algorithm + +#### Cognitive Basis for Stagger + +Stagger forces sequential visual scanning by exploiting rod photoreceptor motion sensitivity: each newly appearing element captures attention briefly before the next appears. This creates a directed reading path that would not exist if all elements appeared simultaneously. + +**Critical constraint:** The stagger must complete within the human visual perception cycle (230 ms) multiplied by a manageable count. If the total stagger duration exceeds 400–500 ms, the sequence reads as slow loading rather than intentional choreography. + +```python +def compute_stagger_parameters( + item_count: int, + arousal_level: str, + total_budget_ms: int = 400 +) -> dict: + """ + Compute stagger delay per item and validate sequence budget. + + item_count: number of items to stagger + arousal_level: 'low' | 'medium' | 'high' + total_budget_ms: max total sequence time (default 400ms) + + Research basis: + - 230ms: Model Human Processor visual perception cycle (Card/Moran/Newell) + - < 50ms per step: stagger collapses — reads as simultaneous + - > 100ms per step: reads as loading, not choreography + - Total cap: 400ms — beyond this, sequence reads as slow + + From perception cycle: if steps > 230ms apart, each reads as fully independent event. + Sweet spot for readable sequencing: 60–100ms per step. + """ + + # Base delay range by arousal + DELAY_RANGE = { + 'low': (80, 100), # deliberate stagger — reading is part of the experience + 'medium': (60, 80), # balanced + 'high': (40, 60), # compressed — snappy reveal + } + + min_delay, max_delay = DELAY_RANGE[arousal_level] + + # Budget-constrained delay: scale down if n × delay > budget + # Use item_count - 1 because first item has delay 0 + effective_count = max(1, item_count - 1) + budget_per_step = total_budget_ms / effective_count if effective_count > 0 else max_delay + + # Select delay: use max_delay if budget allows, else constrain + delay = min(max_delay, budget_per_step) + # Enforce minimum (below 40ms stagger collapses perceptually) + delay = max(40, delay) + # Round to nearest 10ms for clean CSS values + delay = round(delay / 10) * 10 + + total_duration = delay * effective_count + + return { + 'delay_per_item': delay, + 'total_sequence_ms': total_duration, + 'within_budget': total_duration <= total_budget_ms, + # CSS: animation-delay = index × delay_per_item + 'css_pattern': f'animation-delay: calc(var(--stagger-index) * {delay}ms)', + } + +# Practical output table (medium arousal, 400ms budget): +# 2 items: delay = 80ms, total = 80ms ✓ +# 3 items: delay = 80ms, total = 160ms ✓ +# 4 items: delay = 80ms, total = 240ms ✓ +# 5 items: delay = 80ms, total = 320ms ✓ +# 6 items: delay = 80ms, total = 400ms ✓ (at budget) +# 7 items: delay = 70ms, total = 420ms (compressed to fit) +# 10 items: delay = 40ms, total = 360ms (compressed to 40ms floor) +``` + +#### Stagger CSS Custom Properties Pattern + +```css +/* Stagger pattern: set --stagger-index on each child via JS or nth-child */ +.stagger-parent > * { + --stagger-index: 0; /* override per child */ + animation-delay: calc(var(--stagger-index) * var(--motion-stagger)); +} + +/* Duration tokens (medium arousal, 400ms budget) */ +--motion-stagger-sm: 60ms; /* 5+ items */ +--motion-stagger-md: 80ms; /* 3–4 items */ +--motion-stagger-lg: 100ms; /* 2 items */ +``` + +--- + +## Reveal Animation System (Page Load) + +### Entrance Type Selection + +Three canonical entrance patterns, ordered by cognitive overhead: + +| Type | CSS Properties | Cognitive Load | Best For | +|------|---------------|---------------|---------| +| **Fade + translate** (recommended) | `opacity`, `transform: translateY()` | Lowest | All page-load stagger, list reveals, section entrances | +| **Fade only** | `opacity` | Lowest | Content-heavy pages; when spatial context is unambiguous | +| **Scale + fade** | `opacity`, `transform: scale()` | Highest | Modal/dialog only; single focal element; never for lists | + +**Research basis:** Fade + small translate achieves the highest practitioner consensus for page-load context because: +1. The translate provides a subtle directional cue (spatial context) without imposing navigational meaning +2. 8–12px is below the threshold where the brain assigns "this came from outside the viewport" +3. The fade prevents a "pop" artifact that would read as a rendering glitch + +**Scale-in at page load:** Never use for lists or sequential stagger. Scaling multiple elements simultaneously reads as chaotic. Reserved for single focal elements (hero, dialog). + +### Entrance Motion Formulas + +```python +def compute_entrance_motion( + context: str, # 'page_load' | 'scroll_reveal' | 'modal' | 'navigation_forward' | 'navigation_back' + arousal_level: str, # 'low' | 'medium' | 'high' +) -> dict: + """ + Compute entrance animation properties by context and brand arousal. + + Direction rules (NNGroup spatial cognition research): + - page_load: top-to-bottom (elements settle downward — matches F-pattern reading) + - scroll_reveal: upward translate (elements rise from below — matches scroll direction) + - modal: scale from center (no spatial origin in reading flow) + - navigation_forward: left-to-right entry (spatial convention: forward = right) + - navigation_back: right-to-left entry (spatial convention: back = left) + + Research: Arrows on the right preferred for "forward" (Casasanto & Bottini 2022). + NNGroup: direction encodes spatial contract — breaking it causes user error. + """ + + # Translate distance by arousal (larger = more energetic, more attention) + # Practitioner consensus: 8–12px for page-load; 12–16px for scroll-reveal + # Cap: beyond 20px reads as theatrical / outside-viewport origin + TRANSLATE_BY_AROUSAL = { + 'low': 12, # deliberate settle + 'medium': 8, # standard + 'high': 6, # minimal — snappy appearance + } + + translate_px = TRANSLATE_BY_AROUSAL[arousal_level] + + CONTEXTS = { + 'page_load': { + # Elements settle downward from above: translateY(-px → 0) + # This is OPPOSITE of scroll reveal (which rises upward) + # Rationale: page load starts at top, reading order is downward + 'from': f'opacity: 0; transform: translateY(-{translate_px}px)', + 'to': 'opacity: 1; transform: translateY(0)', + 'easing': '--ease-enter', + }, + 'scroll_reveal': { + # Elements rise upward into viewport: translateY(px → 0) + # Matches scroll direction — content rises as user scrolls down + 'from': f'opacity: 0; transform: translateY({translate_px + 4}px)', + 'to': 'opacity: 1; transform: translateY(0)', + 'easing': '--ease-enter', + }, + 'modal': { + # Scale from center — no directional origin + # 0.96 scale: subtle enough to not feel theatrical + 'from': 'opacity: 0; transform: scale(0.96)', + 'to': 'opacity: 1; transform: scale(1)', + 'easing': '--ease-enter', + }, + 'navigation_forward': { + # Enter from left (left → right = forward in LTR reading cultures) + 'from': f'opacity: 0; transform: translateX(-{translate_px + 8}px)', + 'to': 'opacity: 1; transform: translateX(0)', + 'easing': '--ease-enter', + }, + 'navigation_back': { + # Enter from right (right → left = backward) + 'from': f'opacity: 0; transform: translateX({translate_px + 8}px)', + 'to': 'opacity: 1; transform: translateX(0)', + 'easing': '--ease-enter', + }, + } + + return CONTEXTS[context] +``` + +### Page Load Reveal: Sequencing Rules + +```python +def plan_page_reveal_sequence(sections: list[dict]) -> list[dict]: + """ + Plan the stagger sequence for a page load reveal. + + Stagger order must encode information hierarchy (NNGroup progressive disclosure): + "If stagger order does not match information priority, it actively misdirects attention." + + Rules: + 1. First element has delay=0 (no gap between paint and motion start) + 2. Hero/heading animates before body content + 3. Navigation/chrome animates simultaneously with or before hero + 4. Secondary content (sidebar, related links) animates last + 5. Decorative elements (dividers, background shapes) use fastest duration, last delay + + sections: list of dicts with 'role': 'nav' | 'hero' | 'body' | 'secondary' | 'decorative' + Returns same list with added 'delay_ms' and 'duration_token' fields. + """ + + PRIORITY_ORDER = ['nav', 'hero', 'body', 'secondary', 'decorative'] + STAGGER_MS = 80 # medium arousal default + + # Sort by priority, preserving relative order within same priority + sorted_sections = sorted(sections, key=lambda s: PRIORITY_ORDER.index(s.get('role', 'body'))) + + delay = 0 + for section in sorted_sections: + section['delay_ms'] = delay + section['duration_token'] = '--duration-moderate' if section.get('role') == 'hero' else '--duration-subtle' + delay += STAGGER_MS + + # Validate total sequence is within budget + total_ms = sorted_sections[-1]['delay_ms'] + 240 # last delay + moderate duration + assert total_ms <= 800, f"Reveal sequence too long: {total_ms}ms. Compress stagger." + + return sorted_sections +``` + +### Reveal CSS Keyframe Template + +```css +/* Page load reveal — hero entrance */ +@keyframes reveal-from-top { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Scroll-driven reveal — content rises into view */ +@keyframes reveal-from-bottom { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Modal entrance — scale from center */ +@keyframes reveal-modal { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Shared reveal class */ +.reveal { + animation-fill-mode: both; /* hold from-state before start, to-state after end */ + animation-timing-function: var(--ease-enter); + animation-duration: var(--duration-subtle); +} + +.reveal-hero { + animation-name: reveal-from-top; + animation-duration: var(--duration-moderate); +} + +.reveal-content { + animation-name: reveal-from-bottom; + animation-duration: var(--duration-subtle); +} + +/* Stagger via custom property */ +.stagger-item { + animation-delay: calc(var(--stagger-index, 0) * var(--motion-stagger-md, 80ms)); +} +``` + +--- + +## Scroll-Driven Animation System + +### Scroll Reveal: When to Animate + +**Operational rule:** Animate scroll-revealed content only when it is below the fold on first paint. Never animate content that is already visible on page load — this creates the appearance of elements "jumping" after the user has already read them. + +**Performance constraint:** Use the CSS `@scroll-timeline` API or `IntersectionObserver` — never `scroll` event listeners, which block the main thread and cause jank. + +```python +def compute_scroll_reveal_threshold( + element_height_px: int, + viewport_height_px: int = 900, +) -> dict: + """ + Compute IntersectionObserver threshold for scroll-reveal timing. + + Optimal reveal point: when 20% of the element is visible. + This gives the animation time to complete before the element + is fully in view — content is readable at animation end. + + If element is taller than viewport (e.g., full-screen sections), + reveal on first pixel entering viewport (threshold = 0). + """ + + if element_height_px >= viewport_height_px: + # Tall element: reveal on first pixel + threshold = 0.0 + rootMargin = '0px 0px -50px 0px' # 50px early trigger + else: + # Standard element: reveal when 20% is in view + threshold = 0.1 # 10% visible = trigger (conservative for fast scrollers) + rootMargin = '0px 0px -80px 0px' # 80px above bottom = reveal early + + return { + 'threshold': threshold, + 'rootMargin': rootMargin, + 'note': 'Apply reveal-content class on intersection; remove on disconnect for repeat reveals', + } +``` + +### Scroll Direction Awareness + +Elements that appear on scroll should animate in the direction consistent with scroll motion: + +| Scroll Direction | Element Enters From | Transform Start | +|-----------------|--------------------|-----------------| +| Scrolling down | Below viewport | `translateY(12px)` → `translateY(0)` | +| Scrolling up | Above viewport | `translateY(-12px)` → `translateY(0)` | +| Horizontal scroll | Right side | `translateX(12px)` → `translateX(0)` | + +**Research basis:** NNGroup directionality research — direction encodes a spatial contract. An element "rising to meet you" as you scroll down is spatially coherent. An element sliding down-to-up would imply the user is scrolling upward. + +### Parallax and Scroll-Linked Motion + +Parallax (where elements move at different rates from the scroll) is a high-arousal, high-attention-demand technique. Apply the following constraints: + +```python +def compute_parallax_parameters( + element_role: str, # 'hero_bg' | 'decorative' | 'content' + brand_arousal: str, # 'low' | 'medium' | 'high' +) -> dict: + """ + Compute parallax scroll rate multiplier. + + Parallax triggers vestibular sensitivity in ~10M Americans (vestibular.org). + WCAG 2.1 SC 2.3.3 requires prefers-reduced-motion compliance. + + Safe parallax range: 0.05–0.15× scroll rate. + Beyond 0.20×: motion sickness risk increases substantially. + Never apply to: content text, interactive elements. + Only apply to: decorative backgrounds, illustration elements. + """ + + if element_role == 'content': + # NEVER parallax content — it disrupts reading + return {'multiplier': 0.0, 'warning': 'Content elements must not use parallax'} + + MAX_MULTIPLIER = { + 'low': 0.05, # barely perceptible — calm brand + 'medium': 0.10, # gentle depth — balanced brand + 'high': 0.15, # noticeable depth — energetic brand (ceiling) + } + + multiplier = MAX_MULTIPLIER[brand_arousal] + + return { + 'multiplier': multiplier, + 'css': f'transform: translateY(calc(var(--scroll-y) * {multiplier}px))', + 'warning': 'Requires prefers-reduced-motion: reduce override → multiplier: 0', + } +``` + +--- + +## Diagram Animation System + +This section specifies how SVG diagram components are animated in this codebase. It consumes the motion tokens defined above; read it in tandem with `DESIGN_SYSTEM.md §Visual Elements`. + +### Figure Coherence Principles + +Three principles govern all animated figures. Every figure must satisfy all three before any animation is added. + +**1. Static Completeness (design target)** +The final settled state (phase=1) is the primary design artifact. Animation is progressive disclosure — it reveals this state; it does not create meaning. Design the phase=1 layout first. A reader who never scrolls must still understand the concept. If the figure requires motion to be comprehensible, the static design has failed. + +**2. Act-State Integrity (every phase is a valid composition)** +Every scroll position must yield a visually complete, balanced composition. Elements that have not arrived yet must have placeholder mass — ghost geometry or neutral fill — that preserves spatial balance. A diagram that looks broken or unbalanced at any act boundary violates this principle. Verify compositional balance at each act boundary before adding transitions. + +**3. Semantic Stagger (appearance order = causal order)** +The sequence in which elements appear must match the information hierarchy and causal order of the concept being illustrated. This is a hard rule, not a preference: +- Causal upstream elements must appear before downstream elements. +- Labels must appear after the elements they describe. +- Parallel, equal-weight elements must appear simultaneously — stagger imposes false hierarchy. + +### ScrollDrivenFigure: the wrapper contract + +`ScrollDrivenFigure` is the required wrapper for any animated diagram. It manages three-tier fallback based on browser capability and user preference: + +| Tier | Mechanism | Condition | +|------|-----------|-----------| +| Primary | CSS `animation-timeline: view()` via `@supports` | Modern browsers | +| Mirror | JS `scroll` listener → React context (`useAnimationPhase()`) | When CSS tier active — provides child component read access to phase | +| Fallback | IntersectionObserver one-shot reveal (`threshold: 0.15`) | CSS unsupported | +| Floor | `phase = 1`, `revealed = true` immediately | `prefers-reduced-motion: reduce` | + +**`phaseEnd` prop** (default `0.5`): controls when phase completes. With `phaseEnd={0.5}`, phase reaches 1.0 when the figure's bottom edge is at 50% viewport height. Complex narrative diagrams (IntroHookDiagram) use the default. Simple diagrams can use `phaseEnd={0.3}` for faster full-phase completion. + +**`useAnimationPhase()`**: the hook child components call to read phase (0–1). Cannot be called outside a `` tree. + +### The Act System + +The Act System maps scroll phase thresholds to named animation events. Define an `ACTS` array: + +```ts +const ACTS = [ + { id: 'arc', threshold: 0 }, // fires immediately on figure entry + { id: 'composing', threshold: 0.5 }, // fires at half-scroll + { id: 'dispatch', threshold: 0.80 }, +] as const; +``` + +Pass to `useActs(ACTS, phase)` which returns `{ wasReached, isCurrentAct }`: + +- **`wasReached(id)`** → `boolean`: fires the moment threshold is crossed. Use for **one-shot CSS class reveals** (adding `.entered` to nodes). Reverses when the user scrolls up — the phase is always reactive. +- **`isCurrentAct(id)`** → `boolean`: true for the topmost reached act. Use for **idle-state indicators** (`idle-ready-breathe`). +- **Monotonicity:** acts are not latched by default. When one-way semantics are required (e.g., worker nodes that should not un-bloom on scroll-back), add a `useRef` guard at the call site. + +**Threshold 0 acts** fire as soon as the figure enters the viewport. Use them for structural elements that should be present from the start rather than arriving mid-scroll. + +### Figure Entrance: Canonical Parameters + +`OperatorNode` and `AgentNode` at equivalent size roles use the **same entrance keyframe** with parameters scaled by size role: + +| Node type | Size | Role | Duration | Translate | Easing | +|-----------|------|------|----------|-----------|--------| +| `OperatorNode` | S=40 | Primary (human) | 300ms | `translateY(12px → 0)` | `var(--ease-enter)` | +| `AgentNode` | S=40 | Primary (orchestrator) | 300ms | `translateY(12px → 0)` | `var(--ease-enter)` | +| `AgentNode` | S=32 | Secondary (worker) | 250ms | `translateY(8px → 0)` | `var(--ease-enter)` | +| `OperatorNode` | S=32 | Secondary | 250ms | `translateY(8px → 0)` | `var(--ease-enter)` | + +**Semantic symmetry rule:** `OperatorNode` and `AgentNode` at equivalent size roles animate identically. Color (neutral vs. violet) carries the semantic distinction; motion does not layer on additional differentiation. + +Canonical keyframe (reuse in every diagram module CSS — do not redeclare with different values): + +```css +@keyframes actEnter { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +``` + +Base/entered pattern: + +```css +.actorNode { opacity: 0; transform: translateY(12px); } /* hidden until act */ +.actorNode.entered { animation: actEnter 300ms var(--ease-enter) both; } +``` + +### Idle State: idle-ready-breathe + +The global `idle-ready-breathe` class (defined in `custom.css`) signals that an agent node is active and waiting for input. Parameters: `4000ms ease-in-out infinite; scale(1 → 1.02)`. + +Apply via `isCurrentAct()`: + +```tsx +className={clsx(styles.actorNode, wasReached('orchestrator') && styles.entered, + isCurrentAct('orchestrator') && 'idle-ready-breathe')} +``` + +**Rule:** Apply to `AgentNode` only, never `OperatorNode`. Apply only to the node whose semantic role is "waiting for the next input" — typically the orchestrator during the phase between receiving a prompt and dispatching workers. Remove (by transitioning away from the current act) when the node dispatches or completes. + +### Ghost Placeholder Pattern + +Ghost placeholders provide visual mass for nodes that are anticipated but not yet revealed, preventing the diagram from feeling unbalanced before dispatch. + +Geometry: match the head squircle of the target `AgentNode` size exactly. For S=32: `x+2.4, y+2.4, width=27.2, height=27.2, rx=6.8`. + +Three CSS states — apply via mount guard + dispatch state: + +```css +.ghostWorker { opacity: 0; } +.ghostWorkerShown { opacity: 0.15; transition: opacity 300ms var(--ease-enter); } +.ghostWorkerHidden { opacity: 0; transition: opacity 200ms var(--ease-exit); } +``` + +Fade-out on dispatch (200ms) must complete before worker entrance (first worker delay: 200ms). Ghost workers are hidden in `prefers-reduced-motion: reduce` (`opacity: 0 !important`). + +### SVG Path Animation: Two Modes + +**Mode A — Act-gated CSS keyframe (guide arcs, one-shot):** + +``` +mount: dasharray = dashoffset = getTotalLength() +act reached → add .arcDraw class → CSS plays drawPath once +``` + +Use when the path should draw in one smooth motion at a specific scroll threshold, then remain drawn. Duration: 500ms `var(--ease-enter)`. + +**Mode B — JS scroll-driven continuous (fan arcs):** + +``` +useEffect([phase]) → dashoffset = length * (1 - t) +opacity toggled by phase threshold +``` + +Use when draw speed should track scroll velocity — the reader controls the pace. + +Fan arc stagger formula (per-arc phase offset): + +```ts +const fanT = (i: number) => { + const start = DISPATCH_START + i * PHASE_STAGGER; // e.g. 0.80 + i * 0.04 + return clamp((phase - start) / (DISPATCH_END - start), 0, 1); +}; +``` + +`PHASE_STAGGER = 0.04` produces ~80ms of scroll-equivalent stagger between arcs at typical scroll velocity. This is the phase-space equivalent of the ms-stagger in §Stagger Algorithm. + +### Artifact Travel: PromptIcon vs TravelingPromptCard + +Two distinct approaches; choice depends on trigger type: + +| Component | Animation mechanism | Trigger | Easing | +|-----------|---------------------|---------|--------| +| `PromptIcon` | Parent `transform: translate(pt.x, pt.y)` via `getPointAtLength` | Scroll phase (continuous) | Follows path geometry | +| `TravelingPromptCard` | SMIL `` with `begin="indefinite"` | Imperative: `motionRef.current.beginElement()` | `keySplines="0.20 0 0.38 0.9"` | + +**Use `PromptIcon` for scroll-driven diagrams.** The parent reads phase, queries the invisible `` path via `getPointAtLength(t * totalLength)`, and applies a `translate` transform. + +**Use `TravelingPromptCard` for trigger-based diagrams** (fixed-timeline or hover-activated). SMIL `animateMotion` does not work in ``-embedded SVG — only in inline React SVG. + +Opacity fade at arc end (prevents visual collision with destination node): + +```ts +const opacity = t < 0.7 ? 1 : 1 - (t - 0.7) / 0.3; +// Artifact is fully visible for first 70% of arc travel, fades over final 30% +``` + +Invisible `` path pattern: + +```tsx + + + {/* no stroke, no fill — pure geometry reference */} + +``` + +### Mount Guard (SSR Hydration Safety) + +Any element whose initial CSS class depends on a phase value (which cannot be computed server-side) needs a mount guard to prevent hydration mismatch: + +```tsx +const [mounted, setMounted] = useState(false); +useEffect(() => { setMounted(true); }, []); + +// In JSX: +className={clsx( + styles.ghostWorker, + mounted && !dispatched && styles.ghostWorkerShown, + dispatched && styles.ghostWorkerHidden, +)} +``` + +Without this, the server renders `mounted=false` (hidden), hydration matches, then the immediate `mounted=true` flip on client produces a one-frame flash. Apply to: ghost workers, idea lightbulb, any pre-phase element with a conditional show class. + +### CSS Module Scoping and Static Fallback Card + +Each diagram gets its own `.module.css`. Keyframes are module-scoped — do not import or reuse keyframe names from other modules (they'll be renamed by the bundler). + +**Static fallback card** (required for any diagram with traveling or scroll-driven animated elements): + +```css +.staticCard { display: none; } + +@media (prefers-reduced-motion: reduce) { + .staticCard { display: block; } /* static prompt shape at semantic midpoint of arc */ + .promptIcon { display: none; } /* hides the animated version */ +} +``` + +Placement rule: position the static card at the **semantic midpoint** of the main arc (where the concept is clearest), not at start or end state. Use dimmed opacity (0.35) for guide arcs in reduced-motion to preserve their "guide" semantic without implying motion. + +--- + +## Stagger Order and Information Hierarchy + +**Critical principle from NNGroup progressive disclosure research:** Animation-driven stagger creates a directed reading path by exploiting motion-attention capture. This benefit inverts to harm if the stagger order contradicts information hierarchy. + +### Stagger Order Rules + +| Content Type | Stagger Order Rule | +|-------------|-------------------| +| List items (sequential, ordered) | Top-to-bottom, left-to-right — matches reading order | +| Grid items (parallel, equal priority) | Simultaneous fade — stagger imposes false hierarchy | +| Form fields | Top-to-bottom — mirrors completion sequence | +| Navigation items | Simultaneously or as a unit — they are parallel | +| Diagram nodes | Animate in data-flow order (source first, sinks last) | +| Hero → body → secondary | Hero first; stagger each section as a unit | + +**Parallel content rule:** For content that is spatially parallel and equal-priority (e.g., a 3-column feature grid where all columns are equivalent), stagger imposes a false hierarchy. Use simultaneous fade. Reserve stagger for genuinely sequential content (steps, timelines, bullet points). + +```python +def select_stagger_strategy( + content_structure: str, # 'sequential' | 'parallel' | 'hierarchical' + item_count: int, +) -> str: + """ + Returns: 'stagger' | 'simultaneous' | 'grouped' + """ + if content_structure == 'parallel' and item_count <= 4: + # Equal-weight items: no stagger (would impose false hierarchy) + return 'simultaneous' + elif content_structure == 'sequential': + # Steps, bullet points, timelines: stagger in reading order + return 'stagger' + elif content_structure == 'hierarchical': + # Groups: animate each group as a unit, stagger between groups + return 'grouped' + elif item_count > 8: + # Too many individual staggers: group into buckets of 3–4 + return 'grouped' + else: + return 'stagger' +``` + +--- + +## Shape Vocabulary and Animation Congruence + +Illustration shape vocabulary (Smooth Circuit vs. Terminal Geometry from `ILLUSTRATION_GUIDE.md`) must be congruent with animation character. The same psychophysical principle that maps curved shapes to warmth maps curved easing to warmth. + +### Shape × Easing Congruence Table + +| Shape Family | Easing Style | Spring Bounce | Duration Bias | +|-------------|-------------|---------------|--------------| +| **Smooth Circuit** (circles, squircles, Bezier connectors) | Expressive entrance, gradual ease-out | 0.05–0.15 | Standard to deliberate | +| **Terminal Geometry** (diamonds, sharp rects, angular paths) | Productive, fast snap | 0.00 | Instant to subtle | +| **Positive valence** (success, AI, system, knowledge) | Ease-out with gentle tail | 0.05–0.10 | Standard | +| **High-arousal** (error, warning, code structure) | Near-linear or ease-in | 0.00 | Instant to fast | + +**Congruence rule:** An error dialog that bounces into view (spring bounce > 0) is semantically incoherent — it applies warm, playful motion to a negative-valence semantic state. Apply Terminal Geometry animation character to all error and warning elements: snap in, no overshoot, fast duration. + +```python +def get_animation_for_semantic(semantic_role: str) -> dict: + """ + Returns animation parameters congruent with semantic hue role. + + Maps semantic role → PAD axis emphasis → animation character. + Error and Warning use Terminal Geometry animation character regardless of brand default. + """ + + SEMANTIC_ANIMATION = { + 'error': {'duration': '--duration-fast', 'easing': '--ease-standard', 'bounce': 0.00, 'translate': '4px'}, + 'warning': {'duration': '--duration-fast', 'easing': '--ease-standard', 'bounce': 0.00, 'translate': '4px'}, + 'success': {'duration': '--duration-subtle', 'easing': '--ease-enter', 'bounce': 0.05, 'translate': '8px'}, + 'info': {'duration': '--duration-subtle', 'easing': '--ease-enter', 'bounce': 0.00, 'translate': '8px'}, + 'neutral': {'duration': '--duration-subtle', 'easing': '--ease-enter', 'bounce': 0.00, 'translate': '8px'}, + 'ai': {'duration': '--duration-moderate', 'easing': '--ease-enter', 'bounce': 0.05, 'translate': '8px'}, + 'system': {'duration': '--duration-subtle', 'easing': '--ease-standard', 'bounce': 0.00, 'translate': '6px'}, + } + + return SEMANTIC_ANIMATION.get(semantic_role, SEMANTIC_ANIMATION['neutral']) +``` + +--- + +## Brand-Specific Application: Agentic Coding + +This section applies the above system to the Agentic Coding design system specifically. + +### Brand PAD Profile + +| Axis | Level | Rationale | +|------|-------|-----------| +| Pleasure | Medium | Professional, clean, not cold. Smooth Circuit shapes as default. | +| Arousal | Low–Medium | Technical reference for focused work. Calm and efficient. | +| Dominance | Medium | Authoritative reference; not passive, not commanding. | + +**Motion style:** Productive. Technical reference documentation prioritizes task speed over emotional engagement. Expressive curves reserved for diagram reveals only. + +### Token Values for This Brand + +Execute these scripts to produce the final token set: + +```python +# Run to generate final token values for Agentic Coding brand +tokens = compute_duration_tokens(arousal_level='medium') +easing = compute_easing_tokens(motion_style='productive', pleasure_level='medium') +springs = compute_spring_presets(pleasure_level='medium', arousal_level='low') + +# Stagger for typical 4-item list +stagger_4 = compute_stagger_parameters(item_count=4, arousal_level='medium') +# → delay_per_item: 80ms, total: 240ms ✓ + +# Stagger for 8-item list +stagger_8 = compute_stagger_parameters(item_count=8, arousal_level='medium') +# → delay_per_item: 60ms (budget-compressed), total: 420ms → compress further to 50ms → 350ms ✓ +``` + +### Computed Token CSS for This Brand + +```css +/* ============================================================ + MOTION TOKENS — Agentic Coding Design System + Generated from: Motion Engineering Phase 2 + Brand: Productive style, medium arousal, medium pleasure + ============================================================ */ + +/* Duration */ +--duration-instant: 70ms; +--duration-fast: 110ms; +--duration-subtle: 150ms; +--duration-moderate: 240ms; +--duration-deliberate: 400ms; +--duration-ambient: 700ms; + +/* Exit variants (×0.75) */ +--duration-fast-exit: 80ms; +--duration-subtle-exit: 110ms; +--duration-moderate-exit: 180ms; +--duration-deliberate-exit: 300ms; + +/* Easing — productive style */ +--ease-enter: cubic-bezier(0.00, 0.00, 0.38, 0.9); +--ease-exit: cubic-bezier(0.20, 0.00, 1.00, 0.9); +--ease-standard: cubic-bezier(0.20, 0.00, 0.38, 0.9); +--ease-linear: linear; + +/* Stagger */ +--motion-stagger-sm: 60ms; /* 5–8 items */ +--motion-stagger-md: 80ms; /* 3–4 items */ +--motion-stagger-lg: 100ms; /* 2 items */ + +/* Reveal offsets */ +--motion-reveal-y-load: -8px; /* page load: elements settle downward into position */ +--motion-reveal-y-scroll: 12px; /* scroll reveal: elements rise upward into viewport */ +--motion-reveal-x-forward: -16px; /* navigation forward: slide right */ +--motion-reveal-x-back: 16px; /* navigation back: slide left */ +--motion-reveal-scale: 0.96; /* modal/dialog only */ +``` + +### Diagram Animation Rules + +> See [§ Diagram Animation System](#diagram-animation-system) for the complete specification. + +--- + +## Accessibility + +### prefers-reduced-motion + +**Reduced-motion renders the canonical representation.** The phase=1 settled state is the primary design artifact — animation is an enhancement layer for capable browsers, not the content itself. When `prefers-reduced-motion: reduce` is active, `ScrollDrivenFigure` sets `phase=1` immediately, rendering every figure in its fully-settled final state. This is not a degradation path; it is the baseline design. Animation is a progressive enhancement for browsers that support it and users who prefer it. + +WCAG 2.1 SC 2.3.3 (Level AAA) and SC 2.3.1 (Level A) cover motion sensitivity. Vestibular disorders affect approximately 10 million Americans. Large-scale motion can trigger nausea, headaches, and symptoms requiring bed rest. This is not aesthetic preference — ignoring it can cause physical harm. + +**Implementation requirement:** Every animation property must be neutralized by the following media query. This is not optional. + +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + transition-delay: 0ms !important; + scroll-behavior: auto !important; + } + + /* Preserve opacity transitions for content that uses them for show/hide logic */ + .reveal, + .stagger-item { + opacity: 1 !important; + transform: none !important; + } +} +``` + +**Do NOT use `animation-duration: 0` (zero):** Some browsers and screen readers interpret zero-duration animations differently from no animation. Use `0.01ms` — effectively instantaneous but technically animated. + +### State Change Without Motion + +When `prefers-reduced-motion: reduce` is active: +- Content appears instantly (no fade, no translate) +- Stagger is eliminated — all elements appear simultaneously +- Scroll-driven reveals trigger immediately on entering viewport (no animation) +- Progress indicators use color change only (no spinning, no sweeping) +- Diagrams render at final state — no draw-on animation + +### Minimum Animation Duration for Screen Readers + +Some screen reader users rely on transition events for navigation cues. Never set `animation-duration` below 40ms (even before reduced-motion override) — below 40ms, the browser may not fire `animationend` events reliably. + +--- + +## Validation Checklist + +For every generated animation sequence, verify: + +**Timing:** +- [ ] All user-triggered animations begin within 100 ms of trigger (Miller 1968) +- [ ] No user-triggered animation exceeds 500 ms duration +- [ ] Exit animations are ≤ 75% of corresponding enter duration +- [ ] No `animation-delay` on first element in any sequence + +**Easing:** +- [ ] No CSS `ease` keyword used — only custom `cubic-bezier()` or named tokens +- [ ] `linear` easing used only for spinners, progress bars, and video +- [ ] Entrance uses ease-out (decelerating); exit uses ease-in (accelerating) +- [ ] Repositioning uses ease-in-out; not ease-out (ease-out implies arrival, not transit) + +**Stagger:** +- [ ] Total stagger sequence ≤ 400 ms (compress per-item delay if needed) +- [ ] Stagger per step ≥ 40 ms (below this collapses to simultaneous) +- [ ] Stagger order matches information hierarchy +- [ ] Parallel equal-priority items use simultaneous fade, not stagger + +**Reveal (page load):** +- [ ] Page-load elements use `translateY(-8px → 0)` (settle downward, not rise) +- [ ] Scroll-reveal elements use `translateY(12px → 0)` (rise upward with scroll) +- [ ] Scale-in (`scale(0.96)`) used only for modals/dialogs — never lists +- [ ] Navigation forward: enter from left; navigation back: enter from right +- [ ] `animation-fill-mode: both` set on all reveal animations + +**Springs:** +- [ ] Springs used only for gesture-continuation (drag release, pull-to-refresh) +- [ ] `bounce: 0.0` (critically damped) for all non-gesture UI elements +- [ ] No spring bounce on error, warning, or danger elements + +**Shape × Easing congruence:** +- [ ] Error and warning elements use fast, snap-in animation (no bounce, no deliberate) +- [ ] Success and positive-valence elements may use subtle bounce (≤ 0.10) +- [ ] Diagram connectors draw after nodes (data-flow order maintained) + +**Accessibility:** +- [ ] `@media (prefers-reduced-motion: reduce)` block present in CSS +- [ ] All animations use `0.01ms !important` in reduced-motion block (not 0) +- [ ] Reveal classes reset to final state (`opacity: 1`, `transform: none`) in reduced-motion +- [ ] No `animation-duration` below 40ms anywhere (browser event reliability) +- [ ] Parallax multiplier set to `0` in reduced-motion override + +**Performance:** +- [ ] All animated properties are `opacity` and `transform` only — no `width`, `height`, `left`, `top`, `margin`, `padding` +- [ ] No `scroll` event listeners for animation — use `IntersectionObserver` or CSS `@scroll-timeline` +- [ ] `will-change: transform, opacity` applied only to actively animating elements (remove after animation ends) +- [ ] `animation-fill-mode: both` preferred over JavaScript state management for reveal + +**Diagram figures:** +- [ ] `OperatorNode` and `AgentNode` at equivalent size roles use identical `actEnter` animation +- [ ] `var(--ease-enter)` used, not `ease-out` keyword +- [ ] S=40 primary entrance: 300ms, `translateY(12px)`. S=32 secondary: 250ms, `translateY(8px)` +- [ ] `idle-ready-breathe` applied only to `AgentNode` during its "waiting for input" act +- [ ] Ghost placeholders geometrically match their target node's head squircle bounds +- [ ] Guide arcs (Mode A): CSS keyframe, 500ms, act-gated. Fan arcs (Mode B): JS scroll-driven +- [ ] `PromptIcon` + `getPointAtLength` for scroll-driven travel; `TravelingPromptCard` for triggers +- [ ] Mount guard applied to all elements with phase-conditional initial class +- [ ] Static fallback card present; placed at semantic arc midpoint, not start/end state +- [ ] Guide arcs in reduced-motion: `opacity: 0.35; stroke-dashoffset: 0 !important` + +--- + +## References + +### Perceptual Timing +- **Miller, G. A.** — "The magical number seven, plus or minus two" (*Psychological Review*, 1956). Foundation for the 1,000 ms flow threshold. +- **Miller, R. B.** — "Response time in man-computer conversational transactions" (*AFIPS Fall Joint Computer Conference*, 1968). Three response time thresholds: 100 ms, 1,000 ms, 10,000 ms. +- **Card, S. K., Moran, T. P., & Newell, A.** — *The Psychology of Human-Computer Interaction* (Lawrence Erlbaum, 1983). Model Human Processor: 230 ms visual perception cycle. +- **Card, S. K., Robertson, G. G., & Mackinlay, J. D.** — "The information visualizer" (*CHI '91*, 1991). Applied Miller thresholds to workstation interface timing. +- **Nielsen, J.** — "Response Times: The 3 Important Limits" (NNGroup, 1993; based on Miller 1968). 100 / 1,000 / 10,000 ms practitioner synthesis. +- **Nielsen Norman Group** — "The Ideal Duration and Easing for UI Animations" (2015, updated 2023). Practitioner synthesis of animation duration research. + +### Duration & Easing Systems +- **IBM Carbon Design System** — "Motion" documentation. Six-tier duration scale (fast-01 70 ms → slow-02 700 ms); productive vs. expressive easing curves. Published under Apache 2.0. +- **Apple Inc.** — WWDC 2023: "Wind down with SwiftUI animations." Spring animation perceptual model: `duration` + `bounce` parameters; bounce: 0.0 = critically damped; recommended 0.0–0.20 for UI. +- **Shopify Polaris** — "Motion" design system documentation. "Snappy" principle: fast start, slow end (ease-out default). +- **Material Design 3 (Google)** — "Motion" specification. Easing tokens: `cubic-bezier(0.05, 0.7, 0.1, 1.0)` standard ease-out. +- **MUI (Material UI)** — `theme.transitions` documentation. 225 ms entering, 195 ms leaving defaults. + +### Cognitive Effects of Animation +- **Pratt, J., et al.** — "Visual sudden-onset in peripheral vision triggers orienting" (*Psychological Science*, 2010, 21(12), 1724–1730). Motion anywhere in visual field triggers automatic attention reorientation. +- **Nielsen Norman Group** — "Animation for Attention and Comprehension" (2020). Animation directing attention to critical state changes; change blindness prevention. +- **Nielsen Norman Group** — "Skeleton Screens 101" (Mejtoft, Långström & Söderström 2018, referenced). Skeleton screens create "illusion of progress," reducing perceived loading time. +- **Nebraska-Lincoln study** (cited by NNGroup) — Users with animated progress bars tolerate **3× longer** wait vs. static/no indicator. Identical objective wait time. +- **Thomas, F., & Johnston, O.** — *The Illusion of Life: Disney Animation* (Hyperion, 1981). 12 principles of animation; timing, ease-in/ease-out, staging, follow-through operationalized by Carbon, Apple. + +### Perceived Performance +- **Nielsen Norman Group** — "Response Times: The 3 Important Limits." Slideshow case study: 1% vs. 20% eye-tracking attention differential. +- **WCAG 2.1 Success Criterion 2.3.3** — "Animation from Interactions" (Level AAA). Required: provide mechanism to disable motion. +- **vestibular.org** — Vestibular Disorders Association. ~10 million Americans affected by vestibular disorders; screen motion can trigger symptoms. + +### Spatial Cognition and Direction +- **Casasanto, D., & Bottini, R.** — "Mirror Reading Can Reverse the Flow of Time" and spatial metaphor research (*Frontiers in Psychology*, 2022). GOOD IS RIGHT; mental number line; cultural variation. +- **Nielsen Norman Group** — "Animation in UX: The Science of Motion and Spatial Cognition" (2022). Directionality encodes navigational contracts; zoom in = deeper, zoom out = broader. +- **Pratt et al. (2010)** — Peripheral motion capture (see above). Foundation for directional attention signaling. + +### Stagger and Progressive Disclosure +- **Card, S. K., Moran, T. P., & Newell, A.** — *The Psychology of Human-Computer Interaction* (1983). 230 ms perception cycle — interval below which sequential items merge perceptually. +- **Nielsen Norman Group** — "Progressive Disclosure" (2006, updated 2021). Sequential disclosure improves comprehension by forcing prioritization; working memory alignment. +- **Miller, G. A.** (1956) — 7 ± 2 chunks; modern revision: 4 ± 1 for complex information. Informs stagger item count limits. +- **Framer Motion documentation** — `stagger(0.3)` default for `whileInView` variants. 300 ms / item is starting point to override, not design recommendation. +- **Josh W. Comeau** — "Action-driven motion" (joshwcomeau.com). ~125 ms entrance for hover micro-interactions. Sequential item stagger practitioner reference. + +### Accessibility +- **WCAG 2.1 SC 2.3.3** — Animation from Interactions. Provides mechanism to disable motion triggered by interaction (Level AAA). +- **WCAG 2.1 SC 2.3.1** — Three Flashes or Below Threshold (Level A). No content flashes more than 3× per second. +- **MDN Web Docs** — `prefers-reduced-motion` media query. Implementation guidance. diff --git a/CLAUDE.md b/CLAUDE.md index 6e6b5b6..8e45829 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,10 @@ You are an expert technical writer specializing in explaining complex topics to ## Project Overview -This is **Agentic Coding**, a course designed for Senior Software Engineers. The course teaches experienced developers how to effectively leverage AI coding assistants in production environments. +This is **Agentic Coding**, a technical reference for Senior Software Engineers. It teaches experienced developers how to effectively leverage AI coding assistants in production environments. **Target Audience:** Senior engineers with 3+ years of professional experience -**Estimated Course Duration:** 24-33 hours of hands-on training +**Estimated Reading Time:** 24-33 hours of hands-on training ## Technology Stack @@ -56,7 +56,7 @@ npm run deploy # Deploy to GitHub Pages - Concise explanations - Code examples that compile and run -- Clear learning objectives per lesson +- Clear objectives per chapter - Hands-on exercises with real scenarios ## Key Configuration Files @@ -72,3 +72,7 @@ npm run deploy # Deploy to GitHub Pages - **URL:** https://agenticoding.ai - **Trigger:** Automatic on push to main branch - **Base URL:** `/` + +## Design System + +Read DESIGN_SYSTEM.md whenever visual work is involved diff --git a/COLOR_GUIDE.md b/COLOR_GUIDE.md new file mode 100644 index 0000000..2a96d63 --- /dev/null +++ b/COLOR_GUIDE.md @@ -0,0 +1,1167 @@ +# Color Scheme Generation Guide for AI Agents + +Production color palette generation using OKLCH color science, color emotion psychophysics, WCAG/APCA contrast validation, three-tier token architecture, and typography-aware accessibility engineering. Designed as agent-executable specification — every formula is code-ready. Domain-agnostic: works for any brand category. + +This guide has three phases: **Brand Strategy** (human-driven research and hue selection), **Palette Engineering** (agent-executable math), and **Typography, Localization & Accessibility** (agent verification when applying the palette to content). The first phase grounds the human operator in marketing science, color emotion research, and competitor analysis before they commit to a hue. The second phase takes that hue and generates a complete, validated token system. The third phase verifies that the palette produces legible, accessible results across font sizes, weights, scripts, and user capabilities. + +--- + +## Phase 1: Brand Strategy (Human Judgment) + +Before picking a hue, ground the decision in marketing science, color emotion research, competitor landscape, and audience psychology. The agent assists with research retrieval; the human makes the judgment call. + +### Distinctive Brand Assets Framework + +**Source:** Jenni Romaniuk, *Building Distinctive Brand Assets* (Oxford University Press, 2018). Based on the Ehrenberg-Bass Institute for Marketing Science (University of South Australia) — decades of empirical data across thousands of brands. + +Brand colors are evaluated on two axes: + +``` + HIGH UNIQUENESS + │ + Signature │ Aspirational + (own it, use │ (build fame + it heavily) │ to match) + │ + LOW FAME ──────────────┼──────────────── HIGH FAME + │ + Avoid │ Cemetery + (no value, │ (everyone uses it, + retire it) │ no one owns it) + │ + LOW UNIQUENESS +``` + +- **Fame**: % of category buyers who associate the element with *any* brand +- **Uniqueness**: % who associate it with *only your* brand + +A color shared by many competitors sits in the **Cemetery quadrant** — high fame, zero uniqueness. It triggers category recall ("a fintech app") but not brand recall ("*your* brand"). The goal is to move toward **Signature**: high fame *and* high uniqueness. + +**Key formula:** + +``` +Brand Salience = Σ (Category Entry Points × DBA Linkage Strength) +``` + +Where DBA Linkage Strength is the probability that encountering the asset triggers recall of *your* brand specifically. A unique color maximizes this linkage; a shared color dilutes it across competitors. + +### Sharp's Laws of Brand Growth + +**Source:** Byron Sharp, *How Brands Grow* (Oxford University Press, 2010); *How Brands Grow Part 2* (2016). + +Core empirical findings relevant to color selection: + +1. **Growth comes from penetration, not loyalty.** Market share differences are explained almost entirely by *how many people know you*, not how often they engage. Implication: maximize recognizability across the broadest audience. + +2. **Consumers perceive weak differentiation between rivals.** In practice, 72% of Coke drinkers also buy Pepsi. Meaningful product differences matter less than being *noticed and remembered*. Implication: invest in sensory distinctiveness (color, shape, sound), not messaging differentiation. + +3. **Mental availability drives growth.** The easier it is for someone to think of your brand in a buying/learning situation, the more likely they are to choose it. Mental availability is built through consistent, unique sensory cues encountered across many touchpoints. + +> *"Rather than striving for meaningful, perceived differentiation, marketers should seek meaningless distinctiveness."* +> — Byron Sharp + +**Application:** Copying the dominant color in your category (e.g., blue in finance, purple in AI, green in sustainability) is textbook category conformity. It maximizes category association but destroys brand-level recall. A distinctive color builds a Distinctive Brand Asset that is exclusively yours. + +### Color Emotion Science + +Color triggers measurable physiological and psychological responses along three orthogonal dimensions: Pleasure, Arousal, and Dominance (PAD). Crucially, **saturation and lightness drive emotional response more than hue does** — a finding that overturns the popular "red = exciting, blue = calm" simplification. + +**Sources:** Valdez & Mehrabian (1994), *J. Experimental Psychology: General*; Wilms & Oberfeld (2018), *Psychological Research*; Mehrabian & Russell (1974), *An Approach to Environmental Psychology*, MIT Press. + +#### Emotional Response by Color Property + +| Color Property | Pleasure (Valence) | Arousal | Dominance | Evidence Strength | +|---|---|---|---|---| +| **High lightness** | Strong ↑ | Moderate ↑ (at high sat only) | ↓ (lighter = approachable) | Very strong, cross-cultural | +| **High saturation** | Curvilinear — medium optimal | Strong ↑ (η² = .693) | ↑ (commanding) | Very strong | +| **Hue (red vs blue)** | Weak / non-significant (p = .051) | Red > Blue (high sat only) | Weak | Moderate, context-dependent | + +Key findings: + +1. **Saturation is the dominant emotional lever.** Effect size η² = .693 for arousal (Wilms & Oberfeld 2018) — the largest of any color property. Two blues at different saturations produce more divergent emotional responses than red vs. blue at the same saturation. + +2. **Medium saturation maximizes pleasantness; high saturation maximizes arousal.** These are different goals. Backgrounds and surfaces want medium chroma (pleasant, non-fatiguing). Alerts and CTAs want high chroma (attention-grabbing). Valence peaks at medium saturation (M = 5.82) and drops slightly at high saturation (M = 5.52). + +3. **Lightness → pleasure is the most reliable cross-cultural finding.** Confirmed across 30 nations (Jonauskaite et al. 2020, r = .88 cross-country agreement). Lighter colors consistently evoke positive responses; darker colors evoke authority and dominance. + +4. **Hue effects on valence are non-significant** when saturation and brightness are properly controlled (Wilms & Oberfeld 2018, p = .051). The blue > green > red preference ranking only emerges at high saturation levels. + +5. **Color emotion is context-dependent.** Elliot & Maier's Color-in-Context Theory (2012) demonstrated that the same color produces opposite behavioral responses depending on context — red increased approach motivation in romantic contexts but avoidance in achievement contexts. Any hue→emotion mapping without context specification is incomplete. + +6. **Ecological valence explains 80% of color preference.** Palmer & Schloss (2010) showed that people prefer colors associated with objects they like (blue → clear sky/water) and dislike colors associated with negative objects (brown → waste). These associations are partially domain-specific: terminal green for developers, institutional blue for finance, clinical white for healthcare. + +#### Critical Interaction Effects + +These interactions are as large as main effects — you cannot reason about dimensions independently (Wilms & Oberfeld 2018): + +| Interaction | η² | Implication | +|---|---|---| +| Hue × Saturation on arousal | .637 | Red-is-arousing only shows up at high saturation. A desaturated red feels similar to a desaturated blue. | +| Brightness × Saturation on arousal | .543 | Brightness only increases arousal when saturation is also high. A bright but desaturated color is not arousing. | +| Brightness × Saturation on valence | — | At low saturation, valence strongly depends on brightness. At high saturation, brightness matters less. | + +#### Physiological vs. Subjective Responses + +Skin conductance (autonomic arousal) correlates only moderately with self-reported arousal (r = 0.42). Saturation significantly affected skin conductance (η² = .184); hue did not (p = .113). People's felt responses and their bodies' responses are related but not identical. + +### Color Associations by Domain + +Hue associations are **contextual tendencies** modulated by saturation and lightness (per Elliot & Maier 2012), not fixed properties. At low saturation, hue-based associations largely disappear. The table below maps general associations and shows how domain context shifts their signal. + +| Color Family | General Associations | Example Domain Signals | +|---|---|---| +| Purple (260–280°) | Authority, innovation, wisdom | Education: "advanced"; AI: saturated/low uniqueness; Luxury: "premium" | +| Blue (220–250°) | Trust, reliability, competence | Finance: "institutional trust"; Tech: generic; Healthcare: "clinical calm" | +| Cyan (185–210°) | Precision, clarity, digital | Dev tools: "terminal culture"; Science: "analytical"; Health: "clinical precision" | +| Green (130–160°) | Growth, freshness, progression | Education: "leveling up"; Finance: "prosperity"; Sustainability: "natural" | +| Red (0–30°) | Energy, urgency, danger | Conflicts with error semantic — avoid as primary in any domain | +| Orange (30–55°) | Energy, warmth, boldness | Media: "creative energy"; Food: "appetite"; conflicts with warning semantic | +| Fuchsia (310–340°) | Boldness, modernity, creativity | Maximum distinctiveness; Fashion: "avant-garde"; can feel less formal | + +**Source for personality mappings:** Labrecque & Milne (2012), *J. Academy of Marketing Science* — Red → Excitement, Blue → Competence, White → Sincerity, Black/Purple/Pink → Sophistication, Brown → Ruggedness. Saturation amplifies the existing hue-personality association; it does not create new ones. + +Quantitative findings on color and cognition (SHIFT eLearning): +- Intentional color schemes amplify learning by **55–78%** and comprehension by **up to 73%** +- Cool colors (blue, green, purple) set focused moods for concentrated study +- Cognitive overload from too many bright colors is a significant risk — minimalism matters +- Colors used to segment information act as mnemonic aids for complex topics + +### Typeface Personality Science + +Font classifications trigger measurable personality associations analogous to color +emotion. Like color, typeface personality operates in PAD-adjacent space — but font +weight primarily modulates Dominance while color primarily modulates Pleasure/Arousal, +making them partially orthogonal. They should be selected jointly, not independently. + +#### Typeface Personality Dimensions + +Three dimensions (Brumberger 2003, factor analysis of 15 typefaces rated on 20 +adjective pairs): + +| Font Dimension | PAD Mapping | Description | +|---|---|---| +| Elegance | ≈ Arousal | Refined, sophisticated, distinguished | +| Directness | ≈ Dominance | Professional, stable, assertive | +| Friendliness | ≈ Pleasure | Warm, playful, approachable | + +#### Font Classification → PAD Profile + +(Synthesized from Brumberger 2003, Shaikh et al. 2006, Henderson et al. 2004, +Monotype/Neurons 2023) + +| Classification | Pleasure | Arousal | Dominance | Personality Keywords | +|---|---|---|---|---| +| Serif (Old Style) | Med | Low | Med-High | Trustworthy, traditional, scholarly | +| Serif (Transitional) | Med | Low-Med | High | Authoritative, refined, professional | +| Serif (Didone/Modern) | Med-High | Med | High | Elegant, dramatic, fashionable | +| Sans (Geometric) | Low-Med | Med | Med | Precise, cold, modern, innovative | +| Sans (Humanist) | High | Med | Med | Warm, friendly, accessible, readable | +| Sans (Neo-Grotesque) | Med | Low | Med | Neutral, corporate, invisible | +| Slab Serif | Low-Med | High | High | Bold, industrial, confident | +| Monospace | Low | Low | Med | Technical, precise, honest | +| Script (Formal) | High | Low | Low | Elegant, personal, feminine | +| Script (Casual) | High | Med | Low | Playful, creative, youthful | +| Display | Variable | High | High | Unique, bold, attention-grabbing | + +#### Font Weight as Independent Emotional Lever + +Weight operates on the Dominance axis independently of hue: + +| Weight Range | Dominance | Personality | +|---|---|---| +| 100–200 (Thin) | Low | Exclusive, delicate, luxury, minimal | +| 300 (Light) | Low-Med | Elegant, refined, airy | +| 400 (Regular) | Med | Neutral, readable, default | +| 500–600 (Medium) | Med-High | Confident, structured, clear | +| 700 (Bold) | High | Authoritative, urgent, confident | +| 800–900 (Black) | Very High | Maximum impact, commanding | + +Color saturation is the dominant arousal lever (η² = .693). Font weight is the +dominant dominance lever. They are partially orthogonal — bold + high chroma = +maximum arousal AND dominance (not merely additive). + +#### Font-Color Congruence + +Font personality and color emotion should align. When they conflict, the penalty +is real: 22% credibility loss for incongruent typography (Fox et al. 2007), and +incongruent signals create active confusion, not neutral averaging. + +Congruence examples: + +| Target Profile | Congruent Font | Congruent Color | Why It Works | +|---|---|---|---| +| Calm Trust | Humanist sans, traditional serif | Blue (210–240°), teal | Both signal accessibility + stability | +| Urgent Authority | Slab serif, bold geometric sans | Red, deep orange | Both signal dominance + arousal | +| Friendly Innovation | Humanist sans, rounded geometric | Cyan (185–210°), bright green | Both signal warmth + modernity | +| Luxury Elegance | Didone serif, light weights | Deep purple, gold | Both signal refinement + exclusivity | +| Technical Precision | Monospace, geometric sans | Cool neutrals, cyan | Both signal rationality + honesty | + +Incongruence red flags: +- Script font + saturated red → font says "elegant," color says "urgent" +- Heavy slab serif + pastel pink → font says "industrial," color says "soft" +- Comic Sans/casual script in any professional domain → immediate credibility loss + +#### Font Classification × Domain Associations + +Like color, font classifications carry domain associations. When font-domain and +color-domain signals align, the effect is multiplicative: + +| Domain | Expected Font | Expected Color | Mismatched Font Signal | +|---|---|---|---| +| Law, finance | Serif (transitional) | Navy blue, dark green | Sans-serif = "too casual" | +| Tech, SaaS | Sans (geometric/humanist) | Blue, cyan, purple | Serif = "dated" | +| Developer tools | Monospace + sans body | Cyan, green, neutral | Script = "not serious" | +| Healthcare | Humanist sans | Teal, green, blue | Display = "not trustworthy" | +| Luxury, fashion | Didone serif, thin weights | Gold, deep purple, black | Slab serif = "too industrial" | +| Education | Humanist sans, readable serif | Blue, green, warm accents | Monospace = "too technical" | + +#### Serif vs Sans-Serif: The Readability Non-Issue + +72 studies reviewed (Lund 1999): no meaningful readability difference between serif +and sans-serif on screen. Arditi & Cho (2005): the minor serif advantage sometimes +observed is a spacing artifact. Choose serif vs sans-serif for personality, not +readability. What actually affects legibility: x-height, open counters, character +differentiation, stroke uniformity, spacing (see Phase 3 § X-Height and Font Metrics). + +At sizes below ~10px on screen, serifs become noise and reduce reading speed +(Morris et al. 2002). At 18px+, they are decorative signal contributing to personality. + +#### Monospace: Developer Context + +Monospace carries precision/honesty associations (Shaikh et al. 2006: 40% chose +monospace for programming contexts) but also "dull/plain/conforming" personality. +For developer-facing brands wanting both technical credibility and approachability, +pair monospace (code blocks) with a humanist sans (body text) — the contrast creates +"warm but competent." + +Monospace typography is typically exempt from brand color — syntax highlighting +dominates. The font's personality contribution is structural (uniform advance width, +even typographic color) rather than chromatic. + +#### Cultural Variation + +Monotype/Neurons (2023, N=1,957, 8 countries): +- Humanist sans-serif scored highest for trust in 7 of 8 countries +- Germany uniquely preferred serif (Cotford) for trust +- Romance-language countries preferred classic serif styles +- Japan: low-contrast humanistic typefaces → innovation; brushstroke-feel → trust + +Font personality associations have significant cultural modulation. When targeting +non-English markets, validate font choice with locale-specific research. + +#### Evidence Quality Caveat + +Font classification → personality: strong evidence (multiple replicated studies). +Font-color interaction: weak evidence (inferential only, no controlled experiments +manipulating both variables simultaneously). The congruence framework above is +grounded in theory and converging practitioner evidence, not in direct measurement +of interaction effects. + +### Competitor Landscape Analysis + +The agent should research the current competitor color map before the human selects a hue. Use this template: + +``` +Research query: "[your category] brands and platforms primary colors [current year]" +``` + +**Competitor map template:** + +| Brand | Primary Hue | Hex | Sub-category | +|---|---|---|---| +| [Competitor 1] | ???° | #?????? | [sub-category] | +| [Competitor 2] | ???° | #?????? | [sub-category] | +| ... | | | | + +The agent populates this table via web research. The human reviews for completeness. + +**Key observations to surface:** +- Which hue zones are crowded (Cemetery quadrant — high fame, zero uniqueness)? +- Which category leaders have already differentiated away from the dominant hue? +- Which hue zones are unoccupied in your specific category? +- What is the minimum hue distance achievable from the nearest competitor (target ≥40°)? + +### Audience Credibility & Color-Brand Congruence + +**Sources:** Bottomley & Doyle (2006), *Marketing Theory*; Labrecque & Milne (2012), *J. Academy of Marketing Science*. + +Two findings create productive tension with Sharp's distinctiveness advice: + +1. **Color-product congruence increases processing fluency.** When a color "fits" the category, it increases brand recognition and positive evaluation (Bottomley & Doyle 2006). Consistent color usage can increase brand recognition by up to 80%. + +2. **Atypical colors grab attention but can decrease purchase intent.** Schema incongruity attracts notice but increases skepticism, especially in categories with strong color conventions. + +**Resolution:** Distinctiveness works when the brand has substance to back it up. Audiences in every domain evaluate credibility on content quality, transparency, and peer validation — not on whether your color matches the category leaders. A distinctive color signals confidence; a copied color signals imitation. + +**Brand personality hue mapping** (Labrecque & Milne 2012): + +| Hue | Brand Personality Dimension | +|---|---| +| Red | Excitement | +| Blue | Competence | +| White | Sincerity | +| Black, Purple, Pink | Sophistication | +| Brown | Ruggedness | + +High saturation amplifies the existing hue-personality association (e.g., saturated red = more exciting). It does not create new personality dimensions. + +### Hue Distance Formula + +When evaluating candidate hues against competitors and semantic roles, calculate angular distance on the 360° wheel: + +``` +distance(h1, h2) = min(|h1 − h2|, 360 − |h1 − h2|) +``` + +Minimum recommended distances: +- **≥30°** from any semantic role hue (error, warning, success) +- **≥40°** from the nearest direct competitor +- **≥60°** from the previous brand color (if rebranding) for the shift to be perceptible to casual viewers + +### Decision Framework + +Score each candidate hue across these dimensions: + +| Dimension | Weight | What to Evaluate | +|---|---|---| +| Differentiation | High | Hue distance from competitors; uniqueness in category | +| Semantic Safety | High | Hue distance from error (25°), warning (70°), success (155°) | +| Emotional Profile | Medium | Does the PAD profile at planned peak chroma match brand intent? (See Color Emotion Science) | +| Authority Signal | Medium | Color psychology fit for target audience | +| Dual-Mode WCAG | Medium | Can shade 600 pass AA on white AND shade 400 pass AA on dark? | +| Domain Fit | Medium | Does it signal appropriate values for your category? | +| Typeface Congruence | Medium | Does the chosen font's PAD profile align with the color's PAD profile? (See Typeface Personality Science) | +| Cultural Resonance | Low | Domain-specific subculture associations (varies by audience) | + +The human operator evaluates these qualitatively. The agent can compute hue distances and WCAG numbers to support the decision. Once a hue is chosen, everything below is math. + +--- + +## Phase 2: Palette Engineering (Agent Math) + +From this point forward, the agent generates the palette autonomously. The human provides perceptual feedback during validation. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherance!** + +## Why OKLCH Over HSL + +HSL is not perceptually uniform. `hsl(60,100%,50%)` (yellow) appears far brighter than `hsl(240,100%,50%)` (blue) at identical lightness values. OKLCH fixes this by design — equal numerical changes produce equal visual changes across all hues. + +Three channels: +- **L** (Lightness): 0.0–1.0 — perceptual brightness +- **C** (Chroma): 0.0–~0.37 — saturation intensity +- **H** (Hue): 0°–360° — position on the color wheel + +All palette math operates in OKLCH. sRGB hex is output-only. + +## Input: Source Hue Table + +The only human judgment calls. Everything downstream is computable math. + +| Role | Hue | Peak Chroma | Rationale | +|------|-----|-------------|-----------| +| Primary | _chosen_ | 0.13–0.15 | Brand identity, CTAs, active states | +| Neutral | _same as primary_ | 0.015 | Brand-tinted gray (same hue, ~10% chroma) | +| Error | 25° | 0.15 | Convention: red-family for danger | +| Warning | 70° | 0.12 | Convention: amber for caution | +| Success | 155° | 0.13 | Teal-green — colorblind-safe vs error red | + +Error is red because users expect it. Success uses teal-green (not pure green) because teal remains distinguishable from error-red under protanopia and deuteranopia. These are convention-fixed, not derived from the brand hue. + +### Primary Hue Selection Constraints + +The primary hue must avoid collision with semantic roles: + +| Zone | Hue Range | Conflict | +|------|-----------|----------| +| Avoid | 0°–50° | Error red (25°) | +| Avoid | 55°–85° | Warning amber (70°) | +| Avoid | 140°–170° | Success teal (155°) | +| Caution | _category-specific_ | Your category's dominant competitor hue zone — low uniqueness | + +Good candidate zones: 90°–135°, 175°–210°, 215°–255°, 285°–350°. + +## Shade Scale Generation + +### Lightness Curve (Non-Linear) + +11 stops mapping to shade names. Not evenly spaced — compressed at extremes for perceptual evenness: + +``` +Shade: 50 100 200 300 400 500 600 700 800 900 950 +Lightness: 0.97 0.93 0.87 0.78 0.69 0.60 0.51 0.43 0.36 0.29 0.25 +``` + +### Parabolic Chroma Curve + +Peak saturation at midtones, taper at extremes. Prevents oversaturated near-white or muddy near-black shades: + +``` +chroma(L) = peakChroma × max(0, 1 − ((L − 0.6) / 0.5)²) +``` + +- Vertex at L=0.6 (shade 500) — maximum chroma +- Denominator 0.5 controls parabola width +- Chroma reaches zero when L deviates ±0.5 from center +- Clamp to `[0, peakChroma]` + +> *Emotional rationale: The parabolic curve peaks at medium chroma (shade 500, L=0.60), which research shows maximizes pleasantness (Valdez & Mehrabian 1994). Extreme shades taper toward zero chroma, reducing arousal — appropriate for backgrounds and subtle UI surfaces. High-chroma midtones are reserved for interactive elements where attention (arousal) is needed.* + +### Dark Mode Inversion + +Same shade names, reversed lightness indices: + +``` +L_dark(i) = LIGHTNESS_STOPS[10 − i] +``` + +Shade 50 in dark mode gets the lightness of shade 950 (0.25), and vice versa. Chroma curve is recomputed from the new lightness — not copied. + +### Bezold-Brücke Hue Compensation + +Colors perceptually drift toward yellow (~65°) or blue (~250°) as lightness changes. For production accuracy, apply per-shade hue correction. The basic implementation uses fixed hue; production systems shift hue ±2–5° per shade to counteract the drift. + +## OKLCH → sRGB Conversion + +Four-step pipeline. All intermediate values are floating-point. + +### Step 1: OKLCH → OKLab + +``` +h_rad = H × π / 180 +a = C × cos(h_rad) +b = C × sin(h_rad) +``` + +### Step 2: OKLab → LMS (cube-root space) + +``` +l' = L + 0.3963377774·a + 0.2158037573·b +m' = L − 0.1055613458·a − 0.0638541728·b +s' = L − 0.0894841775·a − 1.2914855480·b + +l = l'³ +m = m'³ +s = s'³ +``` + +### Step 3: LMS → Linear sRGB + +``` +R_lin = +4.0767416621·l − 3.3077115913·m + 0.2309699292·s +G_lin = −1.2684380046·l + 2.6097574011·m − 0.3413193965·s +B_lin = −0.0041960863·l − 0.7034186147·m + 1.7076147010·s +``` + +### Step 4: Linear → Gamma sRGB + +``` +f(x) = 12.92·x if x ≤ 0.0031308 +f(x) = 1.055·x^(1/2.4) − 0.055 if x > 0.0031308 +``` + +Clamp each channel to `[0, 1]` before gamma. Values outside sRGB gamut are clamped by reducing chroma while preserving hue (gamut mapping). + +### Hex Output + +``` +hex = '#' + toHex(f(clamp(R_lin))) + toHex(f(clamp(G_lin))) + toHex(f(clamp(B_lin))) +toHex(x) = round(x × 255).toString(16).padStart(2, '0') +``` + +## WCAG Contrast Validation + +### Relative Luminance (ITU-R BT.709) + +``` +Y = 0.2126·R_lin + 0.7152·G_lin + 0.0722·B_lin +``` + +Where `R_lin`, `G_lin`, `B_lin` are the linear (pre-gamma) sRGB values, clamped to `[0, 1]`. + +### Contrast Ratio (WCAG 2.1 §1.4.3) + +``` +CR = (Y_lighter + 0.05) / (Y_darker + 0.05) +``` + +### Thresholds + +| Level | Normal text | Large text (≥18pt or ≥14pt bold) | +|-------|-------------|----------------------------------| +| AA | ≥ 4.5:1 | ≥ 3.0:1 | +| AAA | ≥ 7.0:1 | ≥ 4.5:1 | + +### Best Text Color Selection + +``` +white_CR = (1.0 + 0.05) / (Y_bg + 0.05) +black_CR = (Y_bg + 0.05) / (0.0 + 0.05) +text_color = white_CR ≥ black_CR ? white : black +``` + +The crossover occurs around shade 500–600. This is computed, not chosen. + +> **Note:** WCAG 2.x contrast ratios are the legal compliance baseline but have known perceptual inaccuracies, especially for dark color pairs (overestimation up to 250%). Phase 3 introduces APCA (Accessible Perceptual Contrast Algorithm) as a perceptual-truth layer. Run both; flag discrepancies. + +## Semantic Token Mapping + +### Three-Tier Architecture + +| Tier | Name | Example | Purpose | +|------|------|---------|---------| +| Primitive | Raw values | `--color-primary-500: oklch(0.60 0.13 195)` | Color space, no semantics | +| Semantic | Intent | `--color-primary: var(--color-primary-600)` | Meaning, theme-switchable | +| Component | Scoped | `--button-bg: var(--color-primary)` | Component-specific binding | + +Components reference semantic tokens, never primitives. A theme swap changes only the primitive→semantic mapping. + +### Light/Dark Semantic Mapping + +Use shade **600** for light-mode semantic colors (passes AA on white). Use shade **400** for dark-mode semantic colors (passes AA on dark backgrounds). + +| Semantic Token | Light Theme | Dark Theme | +|----------------|-------------|------------| +| `--bg-page` | neutral-50 | neutral-900 | +| `--bg-surface` | white | neutral-800 | +| `--bg-muted` | neutral-100 | neutral-700 | +| `--text-primary` | neutral-900 | neutral-50 | +| `--text-secondary` | neutral-500 | neutral-400 | +| `--border` | neutral-200 | neutral-700 | +| `--primary` | primary-600 | primary-400 | +| `--on-primary` | white | primary-950 | +| `--error` | error-600 | error-400 | +| `--on-error` | white | error-950 | +| `--error-subtle` | error-50 | error-950 | +| `--warning` | warning-600 | warning-400 | +| `--warning-subtle` | warning-50 | warning-950 | +| `--success` | success-600 | success-400 | +| `--success-subtle` | success-50 | success-950 | + +### Paired Foreground Tokens + +Every semantic background needs a paired foreground with guaranteed contrast ≥ 4.5:1. Material Design 3 calls these `on-primary`, `on-error`. Without explicit pairs, agents pick arbitrary text colors and contrast breaks silently. + +## Color Harmony (Decorative Only) + +Harmony rotation produces hue sets for data visualization, illustration, and accents. **Not** for semantic roles — those are convention-fixed. + +| Harmony | Rotation from base | Use case | +|---------|-------------------|----------| +| Monochromatic | 0° (shade scale only) | Single-brand UIs | +| Analogous | ±30° | Harmonious palettes, gradients | +| Complementary | 180° | CTAs, high-contrast accents | +| Split-complementary | 150° + 210° | Balanced accent pairs | +| Triadic | ±120° | Data visualization | +| Tetradic | 90° intervals | Complex UIs needing 4+ hues | + +Apply harmony at shade 500 lightness (L=0.60) using the parabolic chroma at that lightness. + +## Dark Mode Best Practices + +- Never use pure black (`#000000`) — causes halation (white text bleeds). Use dark grays: `#0d1117`, `#1a1a2e`, `#161b22`. +- Never use pure white (`#ffffff`) for body text on dark — use off-white: `#e6edf3`, `#d4d4d4`. +- Pure white is acceptable for headings and high-emphasis text. +- The 60-30-10 rule: 60% dark background, 30% mid-tone surfaces, 10% accent color. +- Neutral scale uses brand hue at 0.015 chroma — produces brand-tinted gray, not pure gray. + +> **Note:** Dark mode introduces additional perceptual effects (astigmatic halation, APCA polarity asymmetry, chroma overstatement) covered in Phase 3 § Accessibility Matrix > Dark Mode Accessibility. + +## Validation Checklist + +For every generated palette, verify: + +- [ ] 5 roles × 11 shades = 55 primitive tokens generated +- [ ] Every shade has both OKLCH and hex values +- [ ] Shade 600 of each role passes WCAG AA (≥4.5:1) on white +- [ ] Shade 400 of each role passes WCAG AA (≥4.5:1) on dark background +- [ ] Shade 700 provides AA fallback if 600 is marginal in light mode +- [ ] Neutral scale chroma capped at 0.015 +- [ ] Every semantic background has a paired `on-*` foreground token +- [ ] No out-of-gamut values (all hex channels in 00–ff) +- [ ] Primary hue ≥30° from any semantic role hue +- [ ] Light and dark theme mappings are complete and cross-validated + +Phase 3 extends this checklist with typography, localization, and accessibility verification items. If the palette will be applied to text content, complete both checklists. + +--- + +## Phase 3: Typography, Localization & Accessibility (Agent Verification) + +Phase 2 produces a validated color palette. Phase 3 verifies that the palette produces legible, accessible results when applied to actual text content. Contrast is not a property of two colors — it is a function of two colors + font size + font weight + x-height ratio + script complexity + rendering context + user capabilities. + +This phase is required when the palette will be used with text. It can be skipped if the palette is decorative-only (e.g., data visualization with labeled values). + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +### APCA: Next-Generation Contrast Model + +WCAG 2.x contrast ratios (Phase 2) remain the legal compliance baseline. APCA (Accessible Perceptual Contrast Algorithm) is the perceptual-truth layer used in WCAG 3.0 drafts. Use both; flag discrepancies. + +**Why APCA over WCAG 2.x:** + +WCAG 2.x uses a simple luminance ratio derived from a 1988 CRT standard. It has three documented failure modes: + +1. **False passes:** Dark-on-dark combinations that compute CR ≥ 4.5:1 but are actually unreadable (measure only Lc 33 in APCA). +2. **False fails:** Readable light-gray-on-white combinations that fail CR 3:1 but measure Lc 61 in APCA. +3. **Dark pair overestimation:** WCAG 2.x overestimates contrast for very dark color pairs by 200–250%. + +Additionally, WCAG 2.x is **polarity-blind** — it returns the same ratio for white-on-black and black-on-white. Human perception is asymmetric: light-on-dark produces halation effects that reduce readability compared to dark-on-light at equivalent luminance differences. + +**APCA Lc (Lightness Contrast):** + +APCA produces a signed value: positive for dark text on light background, negative for light text on dark background. Use the absolute value |Lc| for threshold comparison. The Lc scale is perceptually uniform — equal increments represent equal perceived changes across the entire luminance range. + +Reference implementation: `apca-w3` npm package (Myndex). Do not implement the algorithm manually — it involves piecewise power curves with multiple exponents. + +**Lc Thresholds by Use Case:** + +| Use Case | Minimum |Lc| | Preferred |Lc| | Notes | +|---|---|---|---| +| Body text (14–16px, weight 400) | 75 | 90 | Primary reading content | +| Subheadings (18–24px, weight 600–700) | 60 | 75 | Section navigation | +| Large headlines (32px+, weight 700+) | 45 | 60 | Display text | +| Secondary text (captions, bylines) | 45 | 60 | Metadata, timestamps | +| Placeholder text, disabled | 30 | 40 | Non-essential, ghosted | +| Decorative text, watermarks | 15 | 25 | Not intended to be read fluently | +| Non-text UI (icons, borders, focus rings) | 45 | 60 | WCAG 1.4.11 equivalent | + +**Dual-Model Rule:** + +``` +Run both WCAG 2.x CR and APCA Lc for every text-color pair. +Flag if: + - WCAG passes (CR ≥ 4.5) but APCA fails (|Lc| < 75 for body text) + - WCAG fails (CR < 4.5) but APCA passes (|Lc| ≥ 75 for body text) +Report both values. Use WCAG for legal compliance; use APCA for perceptual truth. +``` + +**Sources:** Andrew Somers, *APCA Readability Criterion* (W3C Silver/WCAG 3.0 draft); Somers, "The Realities And Myths Of Contrast And Color" (*Smashing Magazine*, 2022). + +### Font Size × Weight × Contrast Matrix + +Human contrast sensitivity is a function of **spatial frequency**. Thinner strokes at smaller sizes produce higher spatial frequency, which the visual system resolves with significantly lower contrast sensitivity. The same gray text on white appears less readable in weight 300 than weight 600 — identical luminance difference, but the perceived contrast drops because the spatial frequency is higher. + +APCA replaces WCAG's binary "normal/large text" with a continuous lookup table. Weight and size trade off against each other: + +**Minimum |Lc| by Font Size and Weight (APCA Silver Level):** + +| Size (px) | W100 | W200 | W300 | W400 | W500 | W600 | W700 | W800 | W900 | +|---|---|---|---|---|---|---|---|---|---| +| 12 | — | — | — | — | — | — | — | — | — | +| 14 | — | — | — | 100 | 100 | 90 | 75 | — | — | +| 15 | — | — | — | 100 | 90 | 75 | 70 | — | — | +| 16 | — | — | — | 90 | 75 | 70 | 60 | 60 | — | +| 18 | — | — | 100 | 75 | 70 | 60 | 55 | 55 | 55 | +| 21 | — | — | 90 | 70 | 60 | 55 | 50 | 50 | 50 | +| 24 | — | — | 75 | 60 | 55 | 50 | 45 | 45 | 45 | +| 28 | — | 100 | 70 | 55 | 50 | 45 | 43 | 43 | 43 | +| 32 | — | 90 | 65 | 50 | 45 | 43 | 40 | 40 | 40 | +| 36 | — | 75 | 60 | 45 | 43 | 40 | 38 | 38 | 38 | +| 42 | 100 | 70 | 55 | 43 | 40 | 38 | 35 | 35 | 35 | +| 48 | 90 | 60 | 50 | 40 | 38 | 35 | 33 | 33 | 33 | +| 60 | 75 | 55 | 45 | 38 | 35 | 33 | 30 | 30 | 30 | +| 72 | 60 | 50 | 40 | 35 | 33 | 30 | 30 | 30 | 30 | +| 96 | 50 | 45 | 35 | 33 | 30 | 30 | 30 | 30 | 30 | + +`—` = prohibited: insufficient stroke density for legibility at any contrast level. + +**Practical shade selection rule:** + +``` +Given: font_size, font_weight, background_shade +1. Look up required_Lc from the table above +2. Iterate candidate text shades from darkest (or lightest on dark bg): + text_shade = first shade where |Lc(text, bg)| >= required_Lc +3. If no shade passes: increase font_size or font_weight until a shade qualifies +``` + +> *Note: These are 2023 draft values from Myndex APCA Silver. The canonical source is the `apca-w3` package. Values may shift as the WCAG 3.0 specification finalizes.* + +**Source:** Myndex, *APCA Silver Level Font Lookup Tables* (2023 draft). + +### Text Chroma Limits (Helmholtz-Kohlrausch Effect) + +Text readability depends almost entirely on **luminance contrast**. The visual cortex resolves letter shapes through the achromatic luminance channel. Chromatic channels (OKLab a, b axes) process hue and saturation at much lower spatial resolution — they contribute to object categorization but cannot resolve fine text strokes. + +The **Helmholtz-Kohlrausch (H-K) effect** causes highly saturated colors to appear perceptually brighter than their measured luminance. Two color pairs with identical Lc values can produce vastly different reading comfort — the high-chroma pair causes halation and eye strain even though the computed contrast is equivalent. + +**Chroma Budget by Text Role (OKLCH C value):** + +| Text Role | Max Chroma | Rationale | +|---|---|---| +| Body text (paragraphs, lists) | C < 0.04 | Minimize H-K distortion, maximize stroke clarity | +| Labels, captions | C < 0.04 | Same spatial frequency as body text | +| Interactive text (links, buttons) | C < 0.08 | Brand expression allowed; must verify Lc independently of chroma | +| Display headings (28px+) | C < 0.12 | Larger size reduces spatial frequency, tolerates more chroma | +| UI surfaces (button fills, badges) | Full palette | No fine detail to resolve at surface level | +| Backgrounds | Full palette | Large area, low spatial frequency | +| Decorative / illustrative | Full palette | Not read as text | + +**Rule:** + +``` +For any token assigned as a text foreground color: + assert oklch_chroma(token) <= chroma_limit[role] +If brand color must appear as text, reduce chroma to the role limit and re-verify Lc. +``` + +**Sources:** Helmholtz (1867); von Kohlrausch (1935); Fairchild & Pirrotta (1991), "Predicting the brightness of different hues." + +### X-Height and Font Metrics + +The APCA lookup table assumes an **x-height ratio ≥ 0.5** (approximately Inter, Roboto, system sans-serif). CSS `font-size` specifies the em square — not the visible height of lowercase letters. Two fonts at `font-size: 16px` can differ by 38% in actual rendered letter height: + +| Font | x-height Ratio | Effective Size at 16px CSS | Category | +|---|---|---|---| +| Verdana | 0.55 | 17.6px | System sans | +| Inter | 0.54 | 17.3px | Modern sans | +| Roboto | 0.53 | 17.0px | Modern sans | +| Noto Sans | 0.52 | 16.6px | Universal sans | +| Noto Sans CJK | 0.52 | 16.6px (but see Localization) | CJK sans | +| Georgia | 0.48 | 15.4px | System serif | +| Times New Roman | 0.45 | 14.4px | System serif | +| Garamond | 0.41 | 13.1px | Classic serif | + +**Compensation formula:** + +``` +effective_size = css_font_size × (actual_x_height_ratio / 0.5) +``` + +Use `effective_size` when consulting the APCA font-size × weight lookup table. A font with x-height ratio 0.41 (Garamond) at 16px CSS is effectively 13.1px for contrast purposes — requiring significantly higher |Lc| than 16px Inter. + +**Font rendering caveat:** + +`-webkit-font-smoothing: antialiased` forces grayscale antialiasing, which reduces perceived weight and contrast by ~10–15% on standard-DPI displays (~96 DPI). On HiDPI (≥192 DPI), the effect is imperceptible. Gate it behind a resolution media query: + +```css +@media (min-resolution: 192dpi) { + body { -webkit-font-smoothing: antialiased; } +} +/* Standard DPI: leave as system default (subpixel rendering) */ +``` + +Palettes validated on Retina displays must be re-verified on standard-resolution monitors. + +### Localization Constraints + +The APCA lookup table and all preceding contrast calculations assume **Latin script with x-height ratio ~0.52**. Scripts with higher stroke complexity pack more detail into each glyph, creating higher spatial frequency at the same nominal size. This requires larger minimum sizes, heavier weights, and higher contrast targets. + +**Script Complexity and Minimum Thresholds:** + +| Script | Avg Strokes/Glyph | Min Body Size | Min Weight | Min Line-Height | Max Chars/Line | APCA Offset | +|---|---|---|---|---|---|---| +| Latin | 2–3 | 14px | 300 | 1.4× | 75 | 0 (baseline) | +| Cyrillic | 2–3 | 14px | 300 | 1.4× | 75 | 0 | +| Greek | 2–3 | 14px | 300 | 1.4× | 75 | 0 | +| CJK (Han/Kanji) | 8–12 | 16px (18px pref.) | 400 | 1.6× | 40 | −3px | +| Korean (Hangul) | 4–6 | 16px | 400 | 1.5× | 45 | −2px | +| Arabic | 3–5 (connected) | 16px | 400 | 1.7× | 50 | −2px | +| Devanagari | 4–6 | 16px | 400 | 1.8× | 55 | −2px | +| Thai | 3–5 (stacking) | 16px | 400 | 1.8× | 50 | −2px | +| Vietnamese (Latin+) | 2–3 | 14px (16px pref.) | 300 | 1.7× | 70 | 0 | + +**APCA Offset** means: when consulting the font-size × weight lookup table, subtract this value from the CSS font size to get the effective size for that script. CJK at 18px behaves like Latin at 15px for legibility purposes. + +**CJK-specific rules:** + +``` +CJK thin weights (100–200) are prohibited — stroke merging makes text illegible. +CJK body text under 20px: target |Lc| 90 minimum (not the standard 75). +CJK line width: max 40 characters (WCAG SC 1.4.8). +``` + +**Arabic-specific rules:** + +``` +Arabic harakat (vowel marks) at 14px render at 2–3px height — illegible on standard screens. +Minimum 16px for Arabic with diacritics; 18px preferred. +Connected cursive creates variable stroke widths within words — verify Lc at thinnest stroke points. +``` + +**Diacritical mark thresholds:** + +| Script | Feature | Minimum Size | Preferred Line-Height | +|---|---|---|---| +| Vietnamese | Stacked diacritics (e.g., ệ, ở) | 14px (16px pref.) | 1.7× | +| Thai | Up to 4 vertical stacking marks | 16px | 1.8× | +| Arabic | Harakat (shaddah + fathah stacking) | 16px (18px pref.) | 1.7× | +| Hebrew | Nikkud (vowel points) | 14px (16px pref.) | 1.5× | + +Universal safe line-height for any script with diacritical marks: **1.7×**. + +**Text expansion and line length:** + +Translation from English expands text, which pushes line length toward the upper bound where color fatigue increases: + +| Target Language | Expansion (medium strings) | Line Length Adjustment | +|---|---|---| +| German | +20–35% | Reduce max-width to ~58ch | +| Finnish | +30–40% | Reduce max-width to ~55ch | +| French, Spanish | +15–20% | Reduce max-width to ~62ch | +| Russian | +20% | Reduce max-width to ~60ch | +| CJK | −20% char count (full em-width) | Use max-width: 40ch (WCAG) | + +**Font fallback and metric consistency:** + +System CJK fonts across platforms (Hiragino Sans, Yu Gothic, PingFang, Malgun Gothic) have different ascent/descent ratios. A fallback font with a smaller x-height drops the effective visual size by ~13%, potentially failing contrast checks that passed for the primary font. + +Mitigation with CSS `@font-face` descriptors: + +```css +@font-face { + font-family: 'CJK Fallback'; + src: local('Noto Sans CJK SC'), local('Hiragino Sans'); + size-adjust: 113%; /* Compensate for x-height mismatch */ + ascent-override: 88%; /* Normalize vertical metrics */ +} +``` + +**Font loading performance budgets:** + +| Scope | WOFF2 Budget | Strategy | +|---|---|---| +| Latin-only | < 100 KB | Variable font, subset Latin + Latin Extended | +| Latin + 1 CJK region | < 500 KB | unicode-range subsetting via Google Fonts | +| Full multilingual (Noto) | Subset per locale | On-demand loading by detected script | + +> *Note: WCAG and APCA do not define script-specific contrast ratios. The offsets and thresholds above are derived from stroke-density analysis and spatial frequency research. They represent engineering best practice, not specification requirements.* + +### Accessibility Matrix + +#### Color Vision Deficiency (CVD) + +APCA solves **luminance contrast**. It does NOT solve **hue confusion**. Common CVD types (protan, deutan, tritan) have normal luminance perception — their problem is that certain hue pairs collapse to the same perceived color. Hue confusion must be solved through redundant encoding, not contrast algorithms. + +**Redundant encoding rule:** + +``` +Every piece of information conveyed by color MUST also be conveyed by +at least one non-color channel: icon shape, text label, pattern, position, or underline. +``` + +**Specific CVD hazards:** + +| CVD Type | Prevalence | Confused Pairs | Critical Hazard | +|---|---|---|---| +| Protanopia (no L-cone) | ~1% male | Red↔green, red↔black, red↔brown | Red on black: appears near-zero contrast | +| Deuteranopia (no M-cone) | ~1% male | Red↔green, green↔brown | Same red-green confusion as protan | +| Tritanopia (no S-cone) | ~0.01% | Blue↔yellow, blue↔green | Rare; covered by redundant encoding | +| Achromatopsia | ~0.003% | All hue pairs | Relies entirely on luminance contrast | + +**Safe categorical palette (Okabe-Ito):** + +For data visualization or any context requiring ≥6 distinguishable categories under all common CVD types: + +| Name | Hex | Approximate OKLCH | +|---|---|---| +| Black | #000000 | L=0 C=0 H=0 | +| Orange | #E69F00 | L=0.74 C=0.15 H=75 | +| Sky Blue | #56B4E9 | L=0.72 C=0.10 H=230 | +| Bluish Green | #009E73 | L=0.60 C=0.13 H=170 | +| Yellow | #F0E442 | L=0.92 C=0.17 H=100 | +| Blue | #0072B2 | L=0.50 C=0.12 H=245 | +| Vermillion | #D55E00 | L=0.56 C=0.18 H=40 | +| Reddish Purple | #CC79A7 | L=0.62 C=0.10 H=350 | + +**Source:** Okabe & Ito (2002), "Color Universal Design." + +#### Low Vision + +APCA introduces **contrast reserve** — the gap between bare legibility (just-noticeable difference) and fluent reading speed. Body text needs Lc 75 minimum for legibility, Lc 90 for comfortable sustained reading. Low vision users (20/70 to 20/200 acuity) operate with reduced contrast sensitivity and benefit disproportionately from the higher end of this range. + +| Text Role | Min |Lc| | Preferred |Lc| | Min x-height | Min Weight | +|---|---|---|---|---| +| Body text | 75 | 90 | 9px | 400 | +| Captions, secondary | 60 | 75 | 7px | 400 | +| Headlines | 45 | 60 | — | 600+ | +| Interactive (buttons, links) | 60 | 75 | 9px | 500 | + +#### Dyslexia (5–10% of Population) + +OpenDyslexic and similar "dyslexia fonts" have **no robust evidence of benefit** (Rello & Baeza-Yates 2013; Kuster et al. 2018). What does measurably improve dyslexic reading speed: + +``` +letter-spacing: >= 0.12em +word-spacing: >= 0.16em +line-height: >= 1.5× +font-family: sans-serif (no italic for body text) +background: cream/pastel preferred over pure white (reduces glare) +contrast: CR 15–18:1 preferred over maximum 21:1 (avoids "halation" on white) +text-align: left (never justified — uneven word spacing disrupts reading) +``` + +These thresholds align with WCAG SC 1.4.12 (Text Spacing) user override requirements — a palette designed for dyslexic readability automatically passes SC 1.4.12. + +**Source:** Rello & Baeza-Yates (2013), "Good fonts for dyslexia," *ASSETS '13*. + +#### Aging (60+ Population) + +By age 80, less than one-third of blue light (480 nm) passes through the yellowed crystalline lens. Pupil diameter decreases from ~6.6 mm (age 20–30) to ~5.3 mm (age 50+), reducing retinal illumination. Combined with presbyopia pushing screens farther away (reducing effective font size): + +``` +Rules for aging-accessible palettes: + - Body text: >= 18px, weight >= 400 + - Contrast: increase Lc targets by 15 over baseline (body text |Lc| 90+) + - Blue dependence: never use blue as the sole differentiator between states + - Blue text on dark backgrounds: particularly poor for aging eyes; + shift toward cyan (hue ~195°) or green to maintain luminance +``` + +#### Cognitive Load Limits + +Evidence converges on maximum complexity budgets beyond which comprehension degrades: + +``` +Font families: 1–2 maximum (3 with monospace for code) +Emphasis colors: 2–3 maximum (beyond neutral scale) +Categorical colors: 6–8 maximum (charts, tags, status indicators) +Line length: 50–75 characters (66 ideal) +Motion: respect prefers-reduced-motion — no parallax, auto-play, or rapid transitions +``` + +Font pairing: use contrasting strategy (different classifications with complementary +personalities, e.g., serif heading + sans body). Concordant pairing (two similar fonts) +risks monotony. Conflicting pairing (two display fonts) creates visual noise. A typical +three-font system: display/personality heading + neutral body + monospace code. + +~35% of users report motion sensitivity. The `prefers-reduced-motion` media query is essential for any animation or color transition. + +#### User Style Overrides + +Users may activate Windows High Contrast Mode (now "Contrast Themes"), browser zoom, text-only zoom, or custom stylesheets. A well-designed token system must survive these overrides. + +**Detection:** + +```css +@media (forced-colors: active) { + /* System colors replace all custom colors. + Borders, outlines, and text decorations remain visible. + Background-color differentiation is lost. */ +} +``` + +**Design rules for override resilience:** + +- Use `outline: 3px solid transparent` for focus states — invisible normally, becomes visible in forced-colors mode: + +```css +:focus-visible { + outline: 3px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 3px var(--primary); +} + +@media (forced-colors: active) { + :focus-visible { + outline-color: Highlight; + box-shadow: none; + } +} +``` + +- WCAG SC 1.4.12 requires that user-overridden text spacing (line-height 1.5×, letter-spacing 0.12em, word-spacing 0.16em, paragraph-spacing 2×) does not break layout. Never use fixed `height` on text containers; use `min-height` or no height constraint. +- Token naming must be purpose-based (`--text-primary`, `--bg-surface`), not appearance-based (`--dark-gray`, `--light-bg`). Purpose-based names remain meaningful after user overrides remap the actual colors. + +#### Color-Only Information (WCAG 1.4.1) + +``` +Triple encoding rule: + Every status indicator = color + icon + text + + Error: red border + ⚠ icon + "Error: ..." text + Success: green border + ✓ icon + "Success: ..." text + Warning: amber border + △ icon + "Warning: ..." text + + Links: must have underline (or equivalent non-color indicator), not just color + Charts: must have shapes/patterns/labels, not just color-coded bars/lines +``` + +#### Photosensitivity + +Saturated red flashes pose the highest seizure risk: + +``` +Red flash threshold: + R / (R + G + B) >= 0.8 AND chromaticity shift > 0.2 → seizure risk + +General flash threshold: + Maximum 3 flashes per second + Flashing area must be < 25% of a 10° visual field (~341 × 256 CSS pixels) + +Mitigation: + prefers-reduced-motion disables all flashing and animation + No large-area saturated red transitions at any speed +``` + +**Source:** WCAG SC 2.3.1; Harding & Harding (2010). + +#### Dark Mode Accessibility + +Astigmatism affects 30–60% of the population. On dark backgrounds, pupil dilation increases optical aberrations, causing bright text to visually "bleed" into surrounding dark pixels (**halation**). This effect compounds with thin font weights and high contrast. + +``` +Dark mode rules: + Background: never pure black (#000). Use #121212 to #1E1E1E (OKLCH L 0.15–0.18). + Body text: never pure white. Cap at neutral-200 range (OKLCH L ~0.87). + Chroma: reduce text and surface chroma by 20–30% (multiply C by 0.7–0.8). + Body text Lc: cap at |Lc| 85–90 (pure white on pure black = Lc ~106, which is excessive). + Weight: use weight >= 400 for body text; increase by one stop vs. light mode. + Size: increase font-size by 1–2px vs. light mode for equivalent readability. + Light toggle: always offer a light mode alternative — dark mode is not universally better. + Polarity: APCA Lc values differ for light-on-dark vs. dark-on-light. + Always compute for actual polarity, never assume symmetry. +``` + +### Variable Fonts as Accessibility Tools + +Three variable font axes directly interact with contrast requirements: + +**Optical size (`opsz`):** Automatically adjusts glyph design for the intended display size. At small sizes (8–14), the font thickens stems, opens counters, and increases x-height — effectively moving leftward (heavier) in the APCA weight table without changing `font-weight`. Enable with: + +```css +body { font-optical-sizing: auto; } +``` + +Fonts with `opsz` axis: Inter, Roboto Flex, Source Sans 3, Segoe UI Variable, SF Pro. + +**Grade (`GRAD`):** Changes perceived stroke density without altering character advance widths (no layout reflow). Use cases: +- Dark mode: increase grade by +50 to counteract halation-induced perceived weight loss +- Hover/active states: change perceived emphasis without layout shift + +```css +[data-theme='dark'] body { + font-variation-settings: 'GRAD' 50; +} +``` + +**Continuous weight (`wght`):** Variable fonts support any weight value (not just 100-step increments). This allows precise targeting of the exact stroke thickness needed to meet a specific Lc threshold at a given size, rather than rounding to 400 or 700. + +### Typography Layout and Color Fatigue + +Typographic layout properties interact with color to affect readability and eye strain during sustained reading: + +**Line length:** + +``` +Optimal: 45–75 characters (66 ideal) +Implementation: max-width: 66ch on text containers +CJK: max-width: 40ch (WCAG SC 1.4.8) +``` + +**Line height by context:** + +| Context | Line Height | Rationale | +|---|---|---| +| Body text (Latin) | 1.4–1.6× | Saccade return accuracy over multiple lines | +| Body text (CJK, Thai, Devanagari) | 1.6–1.8× | Stroke density + diacritical clearance | +| Body text (Arabic with harakat) | 1.7× | Vowel mark clearance | +| Headings | 1.1–1.2× | Tight for visual cohesion at large sizes | +| Code blocks | 1.3–1.5× | Monospace character alignment | + +**Color fatigue interactions:** + +- High contrast + long lines (>75ch) = increased saccade fatigue. If line length exceeds 75ch and cannot be reduced, target Lc 80 rather than 90 for body text. +- Saturated backgrounds + sustained reading = chromatic adaptation stress. Use backgrounds at OKLCH C < 0.02 for content surfaces. +- **Paper reading experience** (Somers): Surround the text block with a neutral surface (`--bg-surface`) at ~85% of page background luminance, creating a comfortable luminance transition. This reduces peak luminance while maintaining high Lc within the reading area. + +### Extended Validation Checklist (Phase 3) + +For every palette applied to text content, verify in addition to the Phase 2 checklist: + +**Contrast (dual-model):** +- [ ] Body text (14–16px, W400): |Lc| ≥ 75 AND CR ≥ 4.5:1 +- [ ] Subheadings: |Lc| ≥ 60 AND CR ≥ 3.0:1 (large text) +- [ ] Headlines (32px+): |Lc| ≥ 45 AND CR ≥ 3.0:1 +- [ ] Non-text UI (icons, borders): |Lc| ≥ 45 AND CR ≥ 3.0:1 +- [ ] All WCAG/APCA discrepancies flagged and documented + +**Typography:** +- [ ] Text chroma verified: body text C < 0.04, interactive C < 0.08 +- [ ] X-height ratio of chosen font ≥ 0.5 (or effective_size compensation applied) +- [ ] Font-smoothing antialiased gated behind ≥192dpi media query +- [ ] Line length: max-width set to 66ch (or script-appropriate value) +- [ ] Line height: ≥ 1.4× for Latin body, ≥ 1.6× for CJK/Arabic/Devanagari + +**Localization (if multilingual):** +- [ ] CJK body text: ≥ 16px, weight ≥ 400, APCA offset applied +- [ ] Arabic body text: ≥ 16px, harakat legible +- [ ] Diacritical scripts: line-height ≥ 1.7× +- [ ] Font fallback metrics adjusted (size-adjust, ascent-override) +- [ ] Text expansion tested for longest target languages + +**Accessibility:** +- [ ] All color-only signals have redundant encoding (icon + text) +- [ ] Error/success/warning states use triple encoding +- [ ] Links have non-color indicators (underline or equivalent) +- [ ] Dark mode: no pure black background, body text |Lc| capped at 85–90 +- [ ] Dark mode: chroma reduced 20–30% vs. light mode +- [ ] `@media (forced-colors: active)` tested — focus states and borders visible +- [ ] Text spacing override (SC 1.4.12) does not break layout +- [ ] No saturated-red flashing > 3 Hz +- [ ] `prefers-reduced-motion` respected + +## References + +### Color Science +- **Björn Ottosson** — OKLCH/OKLab color space specification (2020) +- **W3C** — WCAG 2.1 Success Criterion 1.4.3 (Contrast Minimum), 1.4.11 (Non-text Contrast) +- **ITU-R BT.709** — Relative luminance coefficients (R=0.2126, G=0.7152, B=0.0722) +- **CSS Color Level 4** — `oklch()` function specification +- **Bezold-Brücke effect** — Hue shift under varying luminance (psychophysics) +- **Material Design 3** — Paired foreground token pattern (`on-primary`, `on-error`) + +### Branding & Marketing Science +- **Sharp, B.** — *How Brands Grow* (Oxford University Press, 2010). Empirical evidence that brand growth is driven by penetration and mental availability, not perceived differentiation. Based on Ehrenberg-Bass Institute data across thousands of brands and multiple decades. +- **Sharp, B.** — *How Brands Grow Part 2* (Oxford University Press, 2016). Extended findings on emerging markets and services. Reinforces that distinctiveness drives recall while differentiation claims are poorly perceived by consumers. +- **Romaniuk, J.** — *Building Distinctive Brand Assets* (Oxford University Press, 2018). Operational framework for creating and measuring Distinctive Brand Assets (DBAs). Introduces the Fame × Uniqueness grid and DBA linkage measurement methodology. +- **Ehrenberg-Bass Institute for Marketing Science** — University of South Australia. The empirical research institution behind Sharp and Romaniuk's work. Studies spanning FMCG, technology, services, and B2B markets. + +### Color Emotion & Psychophysiology +- **Wilms, L., & Oberfeld, D.** — "Color and emotion: Effects of hue, saturation, and brightness" (*Psychological Research*, 2018). Most methodologically rigorous factorial study: 62 participants, 27 chromatic colors, CIE LCh colorimetric control, SAM + physiological measures. Saturation is the dominant driver of arousal (η² = .693). Hue did not significantly affect valence (p = .051). +- **Valdez, P., & Mehrabian, A.** — "Effects of color on emotions" (*Journal of Experimental Psychology: General*, 1994). Established that brightness is the strongest predictor of pleasure, saturation the strongest predictor of arousal. Set the methodological standard for independent manipulation of color dimensions. +- **Elliot, A. J., & Maier, M. A.** — "Color-in-context theory" (*Advances in Experimental Social Psychology*, 2012). Color's psychological effect is moderated by context — same color produces opposite responses (approach vs. avoidance) depending on the situation. +- **Jonauskaite, D., et al.** — "Universal patterns in color-emotion associations are further shaped by linguistic and geographic proximity" (*Psychological Science*, 2020). 4,598 participants across 30 nations. Cross-country agreement r = .88. Lightness-valence association confirmed cross-culturally. +- **Palmer, S. E., & Schloss, K. B.** — "An ecological valence theory of human color preferences" (*PNAS*, 2010). 80% of color preference variance explained by the affective valence of associated objects (WAVE model). +- **Mehrabian, A., & Russell, J. A.** — *An Approach to Environmental Psychology* (MIT Press, 1974). The Pleasure-Arousal-Dominance (PAD) framework for emotional response to environments. + +### Color Psychology, Education & Brand Personality +- **SHIFT eLearning** — Research on color's impact on learning outcomes. Findings: intentional color schemes amplify learning by 55–78% and comprehension by up to 73%. Cool colors (blue, green, purple) promote focused learning moods. +- **Labrecque, L. I., & Milne, G. R.** — "Exciting red and competent blue: The importance of color in marketing" (*Journal of the Academy of Marketing Science*, 2012). Mapped hues to Aaker's brand personality dimensions. Saturation amplifies existing hue-personality associations. +- **Bottomley, P. A., & Doyle, J. R.** — "The interactive effects of colors and products on perceptions of brand logo appropriateness" (*Marketing Theory*, 2006). Color-product congruence matters more than color alone. Atypical colors grab attention but decrease purchase intent. +- **Adams, F. M., & Osgood, C. E.** — "A cross-cultural study of the affective meanings of color" (*Journal of Cross-Cultural Psychology*, 1973). Semantic differential scales across 23 cultures. Brightness-valence association stable cross-culturally. + +### Typeface Personality & Classification +- **Brumberger, E.** — "The rhetoric of typography: The awareness and impact of typeface appropriateness" (*Technical Communication*, 2003). Factor analysis of 15 typefaces on 20 adjective pairs. Three personality dimensions: Elegance, Directness, Friendliness. +- **Shaikh, A.D., Chaparro, B.S., & Fox, D.** — "Perception of fonts: Perceived personality traits and uses" (*Usability News*, 2006). 78% consistency in font-domain matching. Monospace associated with programming contexts by 40% of participants. +- **Henderson, P.W., Giese, J.L., & Cote, J.A.** — "Impression management using typeface design" (*Journal of Marketing*, 2004). Typeface design characteristics systematically predict brand personality impressions. +- **Monotype & Neurons** — Cross-cultural typeface perception study (2023). N=1,957, 8 countries. Humanist sans scored highest for trust in 7/8 countries. Significant cultural modulation in serif/sans preference. +- **Arditi, A., & Cho, J.** — "Serifs and font legibility" (*Vision Research*, 2005). Minor serif advantage is a spacing artifact, not a serif benefit. +- **Lund, O.** — *Knowledge construction in typography: The case of legibility research and the legibility of sans serif typefaces* (PhD thesis, University of Reading, 1999). Review of 72 legibility studies: no meaningful serif vs sans-serif difference on screen. +- **Morris, R.A., et al.** — "Serifs slow RSVP reading at very small sizes but don't matter at larger sizes" (2002). Serifs become noise below ~10px; decorative signal at 18px+. +- **Fox, D., Shaikh, A.D., & Chaparro, B.S.** — "Effect of typeface appropriateness on the perception of documents" (2007). 22% credibility loss for incongruent typography. +- **Koch, B.E.** — *Emotional response to typographic design* (Doctoral dissertation, 2011). Emotional responses to type parallel color emotion on PAD-adjacent dimensions. + +### Contrast & Typography Science +- **Somers, A.** — *APCA Readability Criterion* (W3C Silver/WCAG 3.0 draft, 2022–present). Accessible Perceptual Contrast Algorithm. Polarity-sensitive, font-size-aware contrast model for WCAG 3.0. Reference implementation: `apca-w3` npm package (Myndex). +- **Somers, A.** — "The Realities And Myths Of Contrast And Color" (*Smashing Magazine*, 2022). Comprehensive explanation of spatial frequency, APCA vs. WCAG 2.x failures, and font-size × weight interaction with contrast. +- **Myndex** — *APCA Silver Level Font Lookup Tables* (2023 draft). Minimum Lc values by font size and weight. Derived from visual acuity and spatial frequency research. +- **Fairchild, M. D., & Pirrotta, E.** — "Predicting the brightness of different hues" (1991). Quantification of the Helmholtz-Kohlrausch effect: chromatic colors appear brighter than achromatic colors of equal luminance. +- **Whittaker, S. G., & Lovie-Kitchin, J.** — Research on critical size, critical contrast, and contrast reserve for reading. Referenced throughout APCA documentation as the empirical foundation for Lc threshold calibration. + +### Localization & Script Research +- **W3C Internationalization (i18n)** — Best practices for text sizing, line breaking, and vertical text across scripts. Script-specific typography considerations. +- **Noto Fonts** — Google's open-source font family covering 800+ languages. Variable font support for weight 100–900. CJK WOFF2 subset: ~200–500 KB with unicode-range. +- **WCAG SC 1.4.8** — Visual Presentation (AAA): specifies ≤40 characters per line for CJK scripts, ≤80 for Latin. + +### Accessibility Research +- **Rello, L., & Baeza-Yates, R.** — "Good fonts for dyslexia" (*ASSETS '13*, 2013). No evidence OpenDyslexic improves readability; letter spacing and sans-serif fonts show measurable improvement. +- **Kuster, S. M., et al.** — "Dyslexie font does not benefit reading in children with or without dyslexia" (*Annals of Dyslexia*, 2018). Replication confirming no benefit from specialized dyslexia fonts. +- **Okabe, M., & Ito, K.** — "Color Universal Design (CUD): How to make figures and presentations that are friendly to colorblind people" (2002). The Okabe-Ito palette: 8 colors distinguishable under all common CVD types. +- **Birch, J.** — "Worldwide prevalence of red-green color deficiency" (*J. Optical Society of America A*, 2012). ~8% of males, ~0.5% of females. +- **Harding, G., & Harding, P.** — "Photosensitive epilepsy and image safety" (*Applied Ergonomics*, 2010). Flash frequency and saturated-red area thresholds for seizure risk. +- **WCAG SC 1.4.1** — Use of Color: color must not be the only visual means of conveying information. +- **WCAG SC 1.4.11** — Non-text Contrast: UI components and graphical objects require 3:1 minimum contrast ratio. +- **WCAG SC 1.4.12** — Text Spacing: users must be able to override line-height (1.5×), letter-spacing (0.12em), word-spacing (0.16em), paragraph-spacing (2×) without loss of content or functionality. +- **WCAG SC 2.3.1** — Three Flashes or Below Threshold: no content flashes more than 3 times per second. diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md new file mode 100644 index 0000000..8ff11bb --- /dev/null +++ b/DESIGN_SYSTEM.md @@ -0,0 +1,896 @@ +# Agentic Coding — Design System + +You are implementing UI for a monochrome-first design system. Color exists only for semantic meaning. All surfaces, borders, and text are achromatic by default. + +## Constraints + +1. **Achromatic base** — Use pure gray for all surfaces, borders, and text. Do NOT use tinted neutrals. Instead, use the neutral palette (C:0.000) for all non-semantic elements. +2. **Color = meaning** — Apply chromatic color only for semantic callouts, diagrams, status indicators, and data viz. Before using any color, answer: "what does this hue mean here?" If there is no semantic answer, use neutral gray instead. +3. **Equal hue standing** — All 9 chromatic hues have equal weight. Do NOT treat any single hue as "the brand color." Instead, select hue based on semantic meaning (see Color Selection Procedure). +4. **Flat construction** — Do NOT use gradients, shadows, or glows. Instead, use solid fills, clean borders (1px solid), and whitespace for visual hierarchy. +5. **Typographic interaction** — Identify interactive elements by typography and shape. Use underlines + font-weight for links. Use dark/light fills for buttons. Do NOT rely on color to signal interactivity. Instead, use shape, weight, and underlines. +6. **Color budget: 60-30-10** — 60% achromatic surfaces, 30% elevated gray, 10% semantic color. Default to 95/5 for content pages. Reserve 60-30-10 for diagram-heavy pages only. +7. **Curved default, angular accent** — Use rounded forms (squircle containers, Bezier connectors) as the default shape vocabulary. Reserve angular forms (diamonds, chevrons, sharp miters) for high-arousal semantic states (error, warning, code). Do NOT mix angular containers with positive-valence content. Instead, match shape curvature to semantic valence (see Illustration System). +8. **Motion is purposive** — Every animated element must answer: "what does this motion orient, teach, or confirm?" If no answer, use no animation. Do NOT animate for decoration. Reserve looping motion (idle states) for semantic signals only: active authoring, AI processing, data flow, system readiness. Max 2 simultaneous idle loops per figure. +9. **Static completeness** — Design the final settled state first. Animation reveals content; it does not define it. Every figure must communicate its full concept in the phase=1 state with no motion. + +--- + +## Color Selection Procedure + +### Step 1: Determine semantic category + +| Hue | Semantic Role | Apply To | +|-----|--------------|----------| +| Error (H:25°) | Danger, critical | Error states, breaking changes, destructive actions | +| Warning (H:70°) | Caution, attention | Warnings, deprecation notices, hallucination risk | +| Success (H:155°) | Validated, complete | Completed states, validation passes, active connections | +| Cyan (H:195°) | System, code | System components, code generation, infrastructure | +| Indigo (H:250°) | Knowledge, data | Documentation, context retrieval, data references | +| Violet (H:285°) | AI transformation | AI processing, transformation steps, synthesis operations | +| Magenta (H:320°) | AI creative | LLM agents, creative processes, prompt engineering | +| Neutral | Human actor / base | Human actors, developer intent — achromatic by design | + +**Removed hues:** Lime (H:110°) aliased to Success — semantically overlapping at 45° gap. +Rose (H:355°) aliased to Neutral — human actors represented achromatic per the base principle. +CSS tokens `--visual-lime` and `--visual-rose` remain as aliases for backward compatibility. + +### Step 2: Select shade by context + +| Context | Light Mode Shade | Dark Mode Shade | +|---------|-----------------|-----------------| +| Semantic text and icons | Pareto-optimal¹ (WCAG AA ≥4.5:1 on white) | 400 (WCAG AA on #0d1117) | +| Subtle tinted backgrounds | 50 | 950 | +| Borders, decorative fills | 100–200 | 700–800 | +| Mid-tone accents | 500 | 500 | +| Darkest text on colored bg | 900 | — | + +### Step 3: Use CSS tokens (not raw hex) + +| Token | Light mode | Dark mode | OKLCH | +|-------|------------|-----------|-------| +| `--visual-error` | #ee0028 | #ec7069 | Pareto-optimal, H:25° | +| `--visual-warning` | #a76900 | #cd8c37 | Gamut-clipped C=0.125, H:70° | +| `--visual-success` | #00894d | #48b475 | C=0.137, H:155° | +| `--visual-cyan` | #008485 | #00b2b2 | Gamut-clipped C=0.095, H:195° | +| `--visual-indigo` | #307ac0 | #53a0ec | C=0.13, H:250° | +| `--visual-violet` | #736cc3 | #938eeb | C=0.13, H:285° | +| `--visual-magenta` | #9d5fab | #c07ecf | C=0.13, H:320° | +| `--visual-neutral` | #666666 | #9b9b9b | Achromatic | +| `--visual-lime` | → `--visual-success` | → `--visual-success` | Alias — removed from spectrum | +| `--visual-rose` | → `--visual-neutral` | → `--visual-neutral` | Alias — removed from spectrum | + +**Chroma normalization:** Categorical hues (indigo, violet, magenta) are capped at C=0.13 to +enforce visual equal-standing. The previous Pareto-optimal approach produced violet/magenta at +C≈0.29 — three times louder than cyan (C=0.095). The 1.37× residual variation is physical gamut +limits at constrained hue angles (cyan, warning). Error retains Pareto-optimal for its semantic +role as a high-arousal danger signal. + +### Step 4: For diagram region fills, use background tokens + +Transparent tints at 10% light / 15% dark: + +```css +--visual-bg-{hue}: color-mix(in srgb, var(--visual-{hue}) 10%, transparent); +/* Dark mode: 15% instead of 10% */ +``` + +All 10 hues (error, warning, lime, success, cyan, indigo, violet, magenta, rose, neutral) have `--visual-bg-*` tokens. + +--- + +## Typography + +### Font Assignment + +| Role | Font | CSS Variable | Weights | +|------|------|-------------|---------| +| Display / headings | Space Grotesk | `--font-display` | 600, 700 | +| Body text | Inter | `--font-body` | 400, 500, 600, 700, 800 | +| Code — default | Monaspace Neon | `--font-mono` | 400, 500, 600, 700 | +| Code — AI voice | Monaspace Argon | `--font-mono-ai` | 400, 500 | +| Code — spec/schema | Monaspace Xenon | `--font-mono-spec` | 400, 500, 600 | +| Code — human note | Monaspace Radon | `--font-mono-human` | 400 | +| Code — keyword/op | Monaspace Krypton | `--font-mono-keyword` | 400, 500 | + +```css +--font-display: 'Space Grotesk', system-ui, sans-serif; +--font-body: 'Inter', system-ui, sans-serif; +--font-mono: 'Monaspace Neon', monospace; +--font-mono-ai: 'Monaspace Argon', monospace; +--font-mono-spec: 'Monaspace Xenon', monospace; +--font-mono-human: 'Monaspace Radon', monospace; +--font-mono-keyword: 'Monaspace Krypton', monospace; +--font-mono-features: 'calt' 1, 'liga' 0; +``` + +Apply `--font-display` to `h1`, `h2`. Apply `--font-body` to body. Monaspace faces share identical metrics — mix freely. + +### OpenType Features + +Apply to all Monaspace containers: + +```css +font-feature-settings: var(--font-mono-features); +``` + +- `calt` ON — Texture healing for even visual density across the monospace grid. +- `liga` OFF — Prevents ambiguous ligatures. Opt in per-context via stylistic sets (`ss01`–`ss10`), never globally. + +### Typographic Voice Selection + +Color encodes *category*. Typeface encodes *speaker*. These axes are orthogonal — a keyword can be Krypton *and* cyan; a comment can be Radon *and* muted gray. + +| Face | Voice | Apply To | +|------|-------|----------| +| **Neon** | System / neutral | Source code, terminal output, config files, tool invocations. Default for all ``. | +| **Argon** | AI agent | LLM responses, agent reasoning traces, AI-generated explanations, ghost text. | +| **Xenon** | Authoritative / structural | Spec IDs, schema keys, API contracts, system boundary labels, constraint rules. | +| **Radon** | Human / informal | Code comments, developer notes, prompt drafts, TODO markers. | +| **Krypton** | Technical / mechanical | Keywords, operators, CLI flags, file paths in callouts, taxonomy labels. | + +### Voice Application by Content Type + +**Code blocks:** Neon base. Comments in Radon. Keywords in Krypton. AI output in Argon. + +**Prompt templates:** Human text in Radon. Placeholders in Xenon. Expected AI response in Argon. + +**Spec tables:** Spec IDs in Xenon. Verification methods in Neon. Rationale in Argon. + +**Diagram labels:** System/boundary names in Xenon. Agent labels in Argon. Human actors in Radon. Data flows in Krypton. + +**Inline code:** Default Neon. Override via utility class: ``, ``, etc. + +### Voice Constraints + +Voice faces: monospace only. Max 2 per block. Radon is scarce — not for emphasis. Fallback: `var(--font-mono)` → `monospace`. + +--- + +## Design Tokens + +### Token Architecture + +| Tier | Purpose | Naming | Example | +|------|---------|--------|---------| +| Primitive | Raw palette values | `{hue}-{shade}` | `cyan-600`, `neutral-200` | +| Semantic | UI surfaces, text, borders, illustration | `--{category}-{role}` | `--surface-page`, `--visual-cyan` | +| Component | Per-component overrides | (not in this document) | — | + +### Surface, Text & Border Tokens + +| Token | Light Mode | Dark Mode | +|-------|-----------|-----------| +| `--surface-page` | #ffffff | #0d1117 | +| `--surface-raised` | #f5f5f5 (neutral-50) | #161b22 | +| `--surface-muted` | #e8e8e8 (neutral-100) | #3d3d3d (neutral-800) | +| `--text-heading` | #2b2b2b (neutral-900) | #e8e8e8 (neutral-100) | +| `--text-body` | #505050 (neutral-700) | #d4d4d4 (neutral-200) | +| `--text-muted` | #808080 (neutral-500) | #9b9b9b (neutral-400) | +| `--border-subtle` | #d4d4d4 (neutral-200) | #3d3d3d (neutral-800) | +| `--border-default` | #b7b7b7 (neutral-300) | #505050 (neutral-700) | + +--- + +## Spatial System + +Base unit: 8px. All spacing and line-heights snap to 8px grid multiples. + +### Spacing Scale + +| Token | Value | +|-------|-------| +| `--space-0` | 0px | +| `--space-px` | 1px | +| `--space-0h` | 4px | +| `--space-1` | 8px | +| `--space-2` | 16px | +| `--space-3` | 24px | +| `--space-4` | 32px | +| `--space-5` | 48px | +| `--space-6` | 64px | +| `--space-7` | 80px | +| `--space-8` | 96px | +| `--space-9` | 128px | +| `--space-10` | 160px | + +| Purpose | Steps | Values | +|---------|-------|--------| +| Component padding | step 2–3 | 16–24px | +| Section gap | step 5–6 | 48–64px | +| Page margin | step 7–8 | 80–96px | + +Do NOT use arbitrary pixel values for spacing. Instead, use `--space-*` tokens. + +### Type Scale + +Minor Third (1.200), base 16px. Sizes integer-rounded. Line-heights 8px-snapped. + +| Token | Size | Line-Height | Role | +|-------|------|-------------|------| +| `--text-xs` | 11px | 24px (`--lh-sm`) | Fine print, captions | +| `--text-sm` | 13px | 24px (`--lh-sm`) | Secondary, metadata | +| `--text-base` | 16px | 24px (`--lh-sm`) | Body text | +| `--text-lg` | 19px | 32px (`--lh-lg`) | Lead paragraphs | +| `--text-xl` | 23px | 32px (`--lh-lg`) | h4 subheadings | +| `--text-2xl` | 28px | 40px (`--lh-2xl`) | h3 section headings | +| `--text-3xl` | 33px | 40px (`--lh-2xl`) | h2 major headings | +| `--text-4xl` | 40px | 48px (`--lh-3xl`) | h1 page titles | + +Do NOT use computed `line-height: 1.5`. Instead, assign `--lh-*` tokens. + +Apply `max-width: 66ch` to text containers. + +### Border Radius + +| Token | Value | +|-------|-------| +| `--radius-none` | 0px | +| `--radius-sm` | 4px | +| `--radius-md` | 8px | +| `--radius-lg` | 12px | +| `--radius-xl` | 16px | +| `--radius-2xl` | 24px | +| `--radius-full` | 9999px | + +| Context | Adjustment | Radius | +|---------|-----------|--------| +| Error/danger | -50% | 2px | +| Warning | -25% | 2px | +| Success | +25% | 4px | +| Neutral/info | default | 3px | +| Avatar | circle | 50% | +| Input fields | -25% | 2px | + +Do NOT apply high curvature (`--radius-full`) to error or warning elements. Instead, use `--radius-sm` or lower. + +Use `--radius-md` (8px) for cards and containers. Use `--radius-sm` (4px) for inputs and badges. + +Do NOT apply border-radius to accent-bordered elements (3–4px `border-left` or `border-top` callouts, blockquotes, step indicators). Radius rounds the accent stroke's endpoints into decoration that communicates nothing. Instead, use `border-radius: 0` and let the accent border terminate with sharp edges. Exception: cards with a full surrounding `border` (all four sides) may use `--radius-md` even when one side carries a thicker accent override. + +### Line Weight + +| Token | Thickness | Light Color | Dark Color | +|-------|-----------|-------------|------------| +| `--border-subtle` | 1px | #d4d4d4 (neutral-200) | #3d3d3d (neutral-800) | +| `--border-default` | 1px | #b7b7b7 (neutral-300) | #505050 (neutral-700) | +| `--border-emphasis` | 1px | #808080 (neutral-500) | #808080 (neutral-500) | +| `--border-strong` | 2px | #505050 (neutral-700) | #b7b7b7 (neutral-300) | +| `--border-accent` | 3px | semantic color | semantic color | + +Do NOT exceed 4 visible structural borders per viewport section. Instead, use spacing or surface tone for grouping. + +Reserve `--border-accent` (3px) for semantic callouts only. + +### Target Sizes + +| Token | Height | H-Padding | Use | +|-------|--------|-----------|-----| +| `--target-sm` | 32px | 16px | Tertiary actions, inline buttons, tags | +| `--target-md` | 40px | 24px | Secondary actions, form inputs | +| `--target-lg` | 48px | 32px | Primary actions, main CTAs | +| `--target-xl` | 56px | 48px | Hero CTAs, prominent actions | + +All interactive elements must be at least 24×24 CSS px (WCAG 2.2 AA). Primary actions use `--target-lg` (48px) to meet WCAG AAA. + +Do NOT place adjacent interactive elements closer than 8px apart. + +### Proximity Grouping + +| Relationship | Spacing | +|-------------|---------| +| Tightly related | 8px (`--space-1`) | +| Within-group | 16px (`--space-2`) | +| Between-group | 48px (`--space-5`) | +| Between-section | 64–96px (`--space-6` – `--space-8`) | + +Within-group spacing must be less than half the between-group spacing. + +Do NOT use equal spacing within and between groups. Instead, ensure at least a 2× step difference. + +--- + +## Content Composition + +### Page Reading Flow + +Content follows a single-column vertical flow. Block elements (figures, code blocks, admonitions) interrupt the prose column and span its full width. + +| Transition | Spacing | Token | +|-----------|---------|-------| +| Paragraph → paragraph | 16px | `--space-2` | +| Paragraph → block element | 32px | `--space-4` | +| Block element → paragraph | 32px | `--space-4` | +| Block element → block element | 24px | `--space-3` | +| Section heading → first element | 16px | `--space-2` | +| Last element → section heading | 64px | `--space-6` | + +Apply `max-width: 66ch` to prose containers. Block elements (figures, code blocks) may extend to the content column's full width but do NOT exceed it. + +Do NOT place a figure before its first textual reference. Instead, the figure appears immediately after the paragraph that introduces it. + +### Figure Integration + +Use semantic `
` and `
` for all visual block elements — diagrams, screenshots, illustrations, and annotated code. + +```html +
+ +
Figure 4.3 — Context window token allocation across three agent turns.
+
+``` + +| Property | Value | +|----------|-------| +| Caption font | `--text-sm` (13px) | +| Caption color | `--text-muted` | +| Caption spacing | `--space-1` above caption | +| Figure margin | `--space-4` top and bottom | +| Numbering | Optional, section-based: `Figure {section}.{n} —` | + +| Figure Type | Width | Alignment | +|------------|-------|-----------| +| Diagram (SVG) | 100% of content column | Centered via `margin-inline: auto` | +| Screenshot | Intrinsic, max 100% | Centered | +| Inline icon pair | Intrinsic | Inline with text | + +Do NOT use figures without captions. Every `
` must contain a `
` that describes the content. + +Do NOT use `` directly for diagrams or illustrations. Instead, wrap in `
` with a descriptive caption. + +### Progressive Disclosure + +| Pattern | Primitive | Use When | +|---------|-----------|----------| +| Collapsible depth | `
` / `` | Optional deep-dive, implementation detail, proof, or derivation | +| Parallel alternatives | `` | Multiple equivalent approaches (languages, frameworks, OS) | +| Semantic alert | Admonition (`:::type`) | Contextual warnings, tips, or prerequisites that interrupt flow | + +| Question | If Yes → | +|----------|----------| +| Is this content required to understand the main argument? | Keep inline — do NOT hide it | +| Does the reader choose one of N equivalent paths? | `` | +| Is this a tangent that only some readers need? | `
` | +| Does this interrupt flow with a warning, tip, or prerequisite? | Admonition | + +Do NOT hide critical content behind `
`. Instead, keep essential information in the primary prose flow. + +Do NOT nest disclosure patterns. A `
` inside a `` panel (or vice versa) adds cognitive overhead. Instead, flatten the structure. + +### Content Block Hierarchy + +Content occupies three tiers; tiers 1–2 must be self-sufficient. + +| Tier | Elements | Role | +|------|----------|------| +| 1 — Primary | Prose paragraphs, headings, inline code | Core argument and explanation | +| 2 — Secondary | Figures, code blocks, tables | Evidence, demonstration, specification | +| 3 — Tertiary | Admonitions, `
`, footnotes | Supplementary context, caveats, deep-dives | + +Do NOT place essential information exclusively in tier 3. Instead, state the key point in tier 1 prose, then elaborate in tier 3 if needed. + +Do NOT exceed 3 consecutive block elements (tier 2 or 3) without intervening prose. Instead, add a bridging sentence that connects the blocks to the argument. + +--- + +## UI Patterns + +### Buttons + +| Variant | Background | Border | Text | Use | +|---------|-----------|--------|------|-----| +| Primary CTA | cyan-600 (light) / cyan-400 (dark) | none | white | Main conversion action (1 per page max) | +| Primary | #2b2b2b (light) / #e8e8e8 (dark) | none | white (light) / dark (dark) | Standard primary actions | +| Outline | transparent | dark | dark | Secondary actions | +| Ghost | transparent | none | underlined dark | Tertiary / inline actions | + +Chromatic color is permitted on **Primary CTA only** — one per page, using `var(--visual-cyan)` as background. This is the sole exception to achromatic buttons. All other button variants remain neutral. Do NOT introduce additional hue variants. Hover: darken fill (shift to cyan-700/cyan-300). Do NOT change hue on hover. + +White text on cyan-600 (#007576) achieves 5.38:1 contrast ratio (WCAG AA). White text on cyan-400 (#00b2b2) in dark mode achieves sufficient contrast on the lighter teal fill. + +### Badges + +| Mode | Background | Text | Border | +|------|-----------|------|--------| +| Light | shade-50 | shade-600 | 1px solid shade-600 | +| Dark | shade-600 | white | none | + +Example (cyan light): `background: #d4fffe; color: #007576; border: 1px solid #007576;` +Example (cyan dark): `background: #007576; color: #fff;` + +### Cards + +- Border: `1px solid var(--border-default)`, `border-radius: var(--radius-md)` +- Background: `var(--surface-raised)` or `var(--surface-page)` +- Hover: shift border to neutral-400. Do NOT add color on hover. Instead, increase border contrast only. + +### Callout Borders + +- 3px left border in semantic color + colored label text. Body text stays neutral. +- Example: `border-left: 3px solid #1369b0;` with `TIP` +- Do NOT apply border-radius to callout containers. Instead, use `border-radius: 0`. The accent border's sharp endpoints reinforce its directional intent. + +### Interactive States + +| State | Treatment | +|-------|-----------| +| Rest | neutral border | +| Hover | border lightens (neutral-700 → neutral-400) | +| Focus | darker border (contrast shift) | +| Active | darker fill or inverted contrast | + +All state changes use contrast/weight shifts. Do NOT introduce hue changes on interaction states. Instead, shift lightness within the neutral palette. + +### Inputs + +- Border: neutral. Focus: darker border. +- Do NOT use colored focus rings. Instead, increase border contrast on focus. + +### Admonitions + +Docusaurus admonition types map to the semantic hue palette and use the Callout Borders pattern (3px left border + colored label). + +| Admonition | Hue | Token | Label Color | +|-----------|-----|-------|-------------| +| `:::tip` | Cyan (H:195°) | `--visual-cyan` | `var(--visual-cyan)` | +| `:::info` | Indigo (H:250°) | `--visual-indigo` | `var(--visual-indigo)` | +| `:::note` | Neutral | `--visual-neutral` | `var(--visual-neutral)` | +| `:::caution` | Warning (H:70°) | `--visual-warning` | `var(--visual-warning)` | +| `:::danger` | Error (H:25°) | `--visual-error` | `var(--visual-error)` | + +Body text inside admonitions stays `--text-body`. Only the label and left border carry the semantic color. + +Do NOT apply background tints to admonitions. Instead, use `--surface-raised` for the container background, matching the flat construction constraint. + +--- + +## Illustration System + +Curved forms (Smooth Circuit) are default. Angular forms (Terminal Geometry) reserved for high-arousal states. See `ILLUSTRATION_GUIDE.md`. + +### Actor Primitives + +All actor primitives share a bounding-box grid for visual equal-standing. + +#### Bounding Box Grid + +| Primitive | Emoji Ref | Bounding Box | Semantic Color | +|-----------|-----------|--------------|----------------| +| `OperatorNode` | 🧑‍💻 | 40×40 (primary) / 32×32 (worker) | Neutral (`--visual-neutral`) | +| `AgentNode` | 🤖 | 40×40 (primary) / 32×32 (worker) | Violet (`--visual-violet`) | +| `PromptCard` | 💬 | 72×40 (fixed) | Indigo (`--visual-indigo`) | + +`PromptCard` is wider (72px) because it is a document artifact, not an actor. It participates +in the 40px height grid of the actor nodes. + +All coordinates in `ActorNodes.tsx` are annotated: `// Computed via scripts/compute-actor-coords.js`. + +#### Size Encoding + +Hierarchy is expressed through **size only**, never through color. + +| Role | BB Size | Use | +|------|---------|-----| +| Primary / orchestrator | 40×40 | Top of hierarchy, one per level | +| Worker / delegate | 32×32 | Bottom of hierarchy, multiple parallel | + +Do NOT distinguish orchestrator from worker agents by color. Use 40→32 size differential only. +Do NOT place primary and worker actors in the same horizontal row without applying the size step. + +#### Construction + +**OperatorNode** — Smooth Circuit head, Terminal Geometry legs: +- Head circle: `r = BB × 0.15`. Fill `--visual-bg-neutral`, stroke `--visual-neutral` 2px. +- Shoulder U-path: Smooth Circuit (`stroke-linecap="round"` `stroke-linejoin="round"`). Span = BB × 0.90, corner radius = BB × 0.10. +- Legs: two lines (Terminal Geometry, `stroke-linecap="square"`) spreading to BB base. + +**AgentNode** — Smooth Circuit throughout: +- Head squircle: `rx = head_w × 0.25`. Fill `--visual-bg-violet`, stroke `--visual-violet` 2px. +- Eyes: two solid circles, `fill="var(--visual-violet)"` (no stroke). +- Mouth: rounded rect + 2 dividers (Terminal Geometry, rx=2). Fill `--visual-bg-cyan`, stroke/dividers `--visual-cyan` 1px. + +**PromptCard** — Smooth Circuit, positive-valence artifact: +- Body rect: `rx="8"`. Fill `--visual-bg-indigo`, stroke `--visual-indigo` 1.5px. +- Text stubs: 3 solid hairline rects at decreasing widths (44px → 36px → 28px). +- Optional tail: small right-triangle at lower-left corner, pointing toward the sending operator. + +#### Communication Medium + +`PromptCard` is the only valid operator→agent communication artifact. Do NOT use other +metaphors (looms, punch cards, pipes). Every edge connecting actor nodes carries either: +- A `PromptCard` artifact (for human→agent or agent→agent delegation), or +- A plain Bezier arc with arrowhead (for return flow / result propagation). + +--- + +### Shape Vocabulary + +| Family | Forms | Superellipse n | Default For | +|--------|-------|----------------|-------------| +| Smooth Circuit | Squircle containers, Bezier connectors, circular endpoints, round caps | n = 3–4 (squircle), n = 2 (circle) | Positive-valence: success, AI, system, knowledge, progress | +| Terminal Geometry | Diamond accents, chevron arrows, angular miters, bracket syntax | n = 1–1.5 (diamond), 45° angles | High-arousal: error, warning, code structure, human action | + +Do NOT use strict-rectangle-only construction (90° routing, sharp corners on all containers). Instead, use squircle containers (n=3–4) even for box-like elements. + +Do NOT apply angular containers (diamonds, sharp-cornered shapes) to success, AI, or system content. Instead, use squircle or circular containers for positive-valence elements. + +### Shape Selection Procedure + +Smooth Circuit for all hues except Error and Warning (Terminal Geometry). + +#### Step 1: Select container shape + +| Content Type | Container | SVG Implementation | +|-------------|-----------|-------------------| +| System node / module | Squircle | `` (40px box) or superellipse `` | +| Agent / AI process | Circle or pill | `` or `` | +| Data / knowledge | Rounded rect | `` | +| Human actor | Circle or squircle | `` or `` | +| Generic container | Squircle | `` to `` | +| Code / terminal | Diamond or sharp rect | `` (4-point) or `` | +| Error state | Sharp rect or diamond | `` or `` | +| Warning state | Triangle or diamond | `` (3-point up) | + +#### Step 3: Choose connector style + +| Connection Type | Connector | SVG Implementation | +|----------------|-----------|-------------------| +| Data flow (happy path) | Bezier curve | `` with `stroke-linecap="round"` | +| Error / rejection path | Angular polyline | `` with `stroke-linecap="square"` | +| Bidirectional | Bezier with markers at both ends | `marker-start` + `marker-end` | +| Optional / dashed | Bezier with dash array | `stroke-dasharray="6 4"` | + +### Stroke Weight Scale + +| Token | Value | Purpose | +|-------|-------|---------| +| `--stroke-fine` | 1px | Grid lines, hairlines, decorative rules | +| `--stroke-light` | 1.5px | Secondary connectors, annotations | +| `--stroke-default` | 2px | Standard connectors, internal details | +| `--stroke-medium` | 2.5px | Container outlines, primary shapes | +| `--stroke-heavy` | 3px | Emphasized elements, primary flow arrows | +| `--stroke-accent` | 4px | Semantic accent strokes (1 per diagram max) | + +Do NOT exceed 4px stroke width. Instead, use fill-weight or size increase for emphasis. + +Do NOT use more than 3 distinct stroke weights in a single diagram. + +### Arrow Markers + +| Type | Marker Size | refX | Polygon Points | +|------|------------|------|---------------| +| Standard | 6×6 | 5 | `0 0, 6 3, 0 6` | +| Large | 8×8 | 6 | `0 0, 8 4, 0 8` | + +Use Standard (6px) by default. Use Large (8px) only in narrow viewBox diagrams (< 400px wide). Arrow fill inherits from stroke color. + +### Construction Rules + +1. **Grid snap** — All anchor points snap to the 8px spatial grid (`--space-*` tokens). Minimum shape dimension = 8px. +2. **Preferred angles** — 15°, 30°, 45°, 60°, 75°, 90°. Other angles require justification. +3. **Proportional corner radius** — Scale `rx` proportionally: `rx = height × 0.25` (range 8–16px). Do NOT use a fixed `rx` regardless of box size. +4. **Minimum gap** — 8px (`--space-1`) between shapes. 16px (`--space-2`) between shape and label. +5. **Label placement** — Labels go directly adjacent to elements (spatial contiguity). Do NOT use a separate legend when inline labels fit. +6. **Coherence** — Every shape serves a communicative purpose. Do NOT add decorative shapes. + +### Flat Construction Enforcement + +Do NOT use SVG filters (``, `feDropShadow`, `feGaussianBlur`). Instead, use stroke-weight hierarchy and surface tone for emphasis. + +Do NOT use SVG `` or `` for fills. Instead, use solid `--visual-bg-*` tint tokens. + +Do NOT use `box-shadow` on diagram containers. Instead, use `border` with `--border-default` or semantic color. + +--- + +## Icon & Diagram Geometry + +### Icon Canvas + +| Token | Value | Purpose | +|-------|-------|---------| +| `--icon-sm` | 16px | Inline text icons | +| `--icon-md` | 24px | Default UI icons | +| `--icon-lg` | 32px | Card/callout icons | +| `--icon-xl` | 48px | Diagram node icons | +| `--icon-2xl` | 64px | Hero/feature icons | + +All icons use a square viewBox matching their canvas (e.g., `viewBox="0 0 24 24"` for `--icon-md`). Content fills 80% of the canvas; 10% padding on each side. + +Do NOT create icons at arbitrary sizes. Instead, use `--icon-*` tokens. + +### Icon Construction + +| Property | Value | +|----------|-------| +| `fill` | `none` (outline style) | +| `stroke` | `currentColor` | +| `stroke-width` | `2` (at 24px canvas) | +| `stroke-linecap` | `round` (Smooth Circuit) or `square` (Terminal Geometry) | +| `stroke-linejoin` | `round` (Smooth Circuit) or `miter` (Terminal Geometry) | + +### Diagram Sizing + +| Diagram Type | ViewBox Width | Typical Height | +|-------------|--------------|---------------| +| Inline icon pair | 100–200 | 48–64px | +| Flow diagram | 400–600 | 160–240px | +| System diagram | 500–1000 | 300–500px | +| Comparison (side-by-side) | 600–1000 | 200–400px | +| Figure caption | Same as parent figure | `--text-sm` line-height (24px) | + +All diagram containers use `width: 100%` with SVG `viewBox` controlling aspect ratio. Do NOT set fixed pixel widths on diagram containers. + +### Diagram Accessibility + +Every SVG diagram requires: + +1. `role="img"` on the `` element +2. `aria-label` describing the diagram's content and relationships +3. Semantic color paired with a non-color indicator (shape difference, label text, or pattern) + +Do NOT rely on color alone to distinguish diagram elements. Instead, combine color + shape + label. + +--- + +## Motion System + +Generated from `ANIMATION_GUIDE.md` Phase 2 (agent-executed math). Brand profile: productive style, medium arousal, medium pleasure. Do NOT override computed values. Re-run `ANIMATION_GUIDE.md` scripts if brand PAD profile changes. + +### Brand Motion Profile + +| PAD Axis | Level | Motion consequence | +|----------|-------|--------------------| +| Pleasure | Medium | Productive curves; slight warmth on diagram reveals | +| Arousal | Low–Medium | Moderate duration (150–240ms); 80ms stagger | +| Dominance | Medium | Ease-out settle; no spring bounce on standard UI | + +**Motion style:** Productive. Expressive curves reserved for actor diagram reveals only — never for UI chrome, buttons, or tables. + +### Motion Tokens + +```css +/* Duration */ +--duration-instant: 70ms; /* toggle, checkbox — at perception floor */ +--duration-fast: 110ms; /* opacity/color, badge update */ +--duration-subtle: 150ms; /* node entrance, icon swap */ +--duration-moderate: 240ms; /* connector draw, widget reveal */ +--duration-deliberate: 400ms; /* large panel, full-section reveal */ +--duration-ambient: 700ms; /* background overlay — not user-triggered */ + +/* Exit variants (×0.75 per NNGroup asymmetry rule) */ +--duration-fast-exit: 80ms; +--duration-subtle-exit: 110ms; +--duration-moderate-exit: 180ms; +--duration-deliberate-exit: 300ms; + +/* Easing — productive style */ +--ease-enter: cubic-bezier(0.00, 0.00, 0.38, 0.9); /* entrance: decelerate to rest */ +--ease-exit: cubic-bezier(0.20, 0.00, 1.00, 0.9); /* exit: accelerate away */ +--ease-standard: cubic-bezier(0.20, 0.00, 0.38, 0.9); /* reposition within viewport */ +--ease-linear: linear; /* spinners, progress bars only */ + +/* Stagger */ +--motion-stagger-sm: 60ms; /* 5–8 items */ +--motion-stagger-md: 80ms; /* 3–4 items */ +--motion-stagger-lg: 100ms; /* 2 items */ + +/* Reveal offsets */ +--motion-reveal-y-scroll: 12px; /* scroll-reveal: elements rise into viewport */ +--motion-reveal-y-load: -8px; /* page-load: elements settle downward */ +--motion-reveal-scale: 0.96; /* modal/dialog only — never for lists */ +``` + +Do NOT use the CSS `ease` keyword. Use `--ease-*` tokens exclusively. +Do NOT animate `width`, `height`, `left`, `top`, `margin`, or `padding`. Use `opacity` and `transform` only. + +### Figure Animation Grammar + +Five archetypes. Each has a fixed animation grammar. Do NOT mix grammars. + +| Archetype | Trigger | Acts | Entrance | Idle | Budget | +|-----------|---------|------|----------|------|--------| +| **Actor diagram** | scroll phase 0→1 | 4–6 phase-gated | `fadeIn + translateY(12px)` per node at `--duration-subtle`; connectors draw via `stroke-dashoffset` at `--duration-moderate` | cursor-blink / status-pulse / ready-breathe per act | ≤1000ms story | +| **Data viz** | scroll phase 0→1 | 2–3 | Containers simultaneous; connectors draw last | flow-drift (optional) | ≤600ms | +| **Interactive widget** | scroll reveal (IO) | 1 | Unit `fadeIn + translateY(12px)` at `--duration-moderate` | none — user-driven | ≤300ms | +| **Data table** | scroll reveal (IO) | 1 | Row stagger at `--motion-stagger-sm` (60ms/row) | `ping-once` on badges at row entry | ≤500ms | +| **Sticky narrative** | chapter ID | N | Chapter transition at `--duration-subtle`; exit at `--duration-subtle-exit` | per-chapter idle | chapter-driven | + +**Entrance keyframe (shared by all archetypes):** + +```css +@keyframes actEnter { + from { opacity: 0; transform: translateY(var(--motion-reveal-y-scroll)); } + to { opacity: 1; transform: translateY(0); } +} +/* apply: animation: actEnter --duration-subtle --ease-enter both; */ +``` + +**Connector draw-on:** + +```css +/* set stroke-dasharray = stroke-dashoffset = el.getTotalLength() via JS on mount */ +@keyframes drawPath { to { stroke-dashoffset: 0; } } +/* apply: animation: drawPath --duration-moderate --ease-enter both; */ +``` + +### Act System + +An **act** is a discrete visual state of a figure. Acts advance monotonically as scroll phase increases — never reverse. + +``` +phase 0→1 (from ScrollDrivenFigure context, or chapter ID from NarrativeFigure) + │ + ▼ +useActs(actDefs, phase) → { wasReached(id), isCurrentAct(id) } + │ + ├── wasReached(id) → element visible, settled appearance + ├── isCurrentAct(id) → apply idle class + └── !wasReached(id) → opacity: 0; pointer-events: none +``` + +Each `ActDef`: `{ id: string, threshold: number }` where threshold is 0–1 (phase-driven) or a chapter ID string (narrative-driven). + +**Do NOT** put act logic in CSS `animation-delay` chains. Use `useActs` + conditional class names. CSS modules define entrance keyframes; the hook controls when they fire. + +**Every act-state must be a visually complete, balanced composition.** Elements that arrive in later acts must have placeholder mass (ghost geometry, neutral fill) in earlier acts to preserve spatial balance. A diagram that looks broken at any act boundary violates this rule. + +### Idle Micro-animation Vocabulary + +All idle classes defined once in `custom.css`. Do NOT define idle keyframes per-component. + +| Class | Motion | Loop | Semantic use | +|-------|--------|------|-------------| +| `.idle-cursor-blink` | `opacity 1→0→1` step-end | 1000ms | Active authoring (PromptBubble during composing act) | +| `.idle-status-pulse` | `opacity 1→0.5→1` ease-in-out | 2000ms | AI processing; connector during transit | +| `.idle-flow-drift` | `translateX 0→2px→0` ease-in-out | 3000ms | Data moving through a channel | +| `.idle-ready-breathe` | `scale 1→1.02→1` ease-in-out | 4000ms | Agent idle; system ready | +| `.ping-once` | `scale 1→1.4; opacity 1→0` ease-out | 400ms, 1× | One-shot attention on act entry | + +Rules: +- Max **2 idle loops** simultaneously per figure. `cursor-blink` and `status-pulse` never together. +- `ping-once` is exempt (transient; `animation-iteration-count: 1`). +- Remove idle classes via `useActs` when the act advances — do NOT let them run in settled state. + +### Stagger Order Rules + +| Content | Order | +|---------|-------| +| Actor diagram nodes | Data-flow order (source → sink) | +| Connector drawing | After both connected nodes are settled | +| Labels / legends | Always last, after the elements they label | +| Data table rows | Top-to-bottom (reading order) | +| Parallel equal-weight grid | Simultaneous — stagger implies false hierarchy | +| Form fields | Top-to-bottom (completion order) | + +### Reduced-Motion Contract + +`ScrollDrivenFigure` enforces the entire contract at the wrapper level. Diagram components do NOT check `prefers-reduced-motion` directly. + +| Condition | Behavior | +|-----------|----------| +| `prefers-reduced-motion: reduce` | `ScrollDrivenFigure` sets `phase=1` on mount; all acts fire instantly; diagrams render in final settled state; idle loop classes removed | +| No JavaScript | Diagrams render in final settled state (SSR default) | +| No CSS scroll-timeline | `IntersectionObserver` fires `phase=1` on first intersection; entrance animations play time-based (correct degradation) | + +```css +@media (prefers-reduced-motion: reduce) { + .idle-cursor-blink, .idle-status-pulse, + .idle-flow-drift, .idle-ready-breathe, .ping-once { + animation: none !important; + } +} +``` + +Do NOT use `animation-duration: 0` — use `0.01ms`. Below 40ms, browsers may not fire `animationend` reliably. + +--- + +## Dark Mode + +| Property | Value | +|----------|-------| +| Page background | #0d1117 | +| Surface/cards | #161b22 | +| Muted surface | #3d3d3d (neutral-800) | +| Body text | #cdd6d6 (neutral-200) | +| Heading text | #e2eae9 (neutral-100) | +| Semantic colors | shade-400 (not shade-600) | +| Bg tint opacity | 15% (not 10%) | + +Do NOT use pure #000000 for backgrounds. Instead, use #0d1117. +Do NOT use pure #ffffff for text. Instead, use neutral-100 (#e2eae9) or neutral-200 (#cdd6d6). + +All `--visual-*` tokens shift from shade-600 to shade-400 in dark mode. + +--- + +## Accessibility + +- **WCAG AA contrast:** shade-600 on white ≥ 4.5:1. shade-400 on #0d1117 ≥ 4.5:1. +- **Redundant encoding:** Every color signal must pair with a non-color indicator (icon, text label, pattern, or underline). Do NOT convey information through color alone. Instead, always add icon + text label alongside color. +- **Error example:** red border + icon + "Error:" text label. Success: green border + icon + "Success:" text. +- **Links:** Must have underline or equivalent non-color indicator. +- **Colorblind-safe:** Success uses teal-green (H:155°), distinguishable from error red under protanopia/deuteranopia. + +--- + +## Brand Identity Assets + +### Logo Mark + +- `` monochrome glyph. Dark on light backgrounds, light on dark backgrounds. +- Do NOT create colored logo variants. The mark is always achromatic. + +### Favicon + +- SVG with `prefers-color-scheme` media query for light/dark switching +- `.ico` fallback at 32x32, Apple touch icon at 180x180 + +### Social Card + +- Dark background (#111111), white title text, optional 3px semantic accent line + +--- + +## Color Palettes (Reference) + +### Neutral — Achromatic (C:0.000) + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #f5f5f5 | #e8e8e8 | #d4d4d4 | #b7b7b7 | #9b9b9b | #808080 | #666666 | #505050 | #3d3d3d | #2b2b2b | #222222 | + +### Error — H:25° C:0.16 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #fff2f0 | #ffdfdc | #ffc3bd | #ff958d | #ec7069 | #ce514d | #ad3735 | #8d2324 | #701719 | #520e10 | #410b0c | + +### Warning — H:70° C:0.13 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #fff3e6 | #ffe3c3 | #fcca91 | #e6aa63 | #cd8c37 | #b17000 | #8e5900 | #704500 | #573400 | #402400 | #331b00 | + +### Lime — H:110° C:0.14 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #f7fac9 | #eaedb0 | #d8da8d | #bcbe5c | #a1a22b | #868600 | #6b6b00 | #535400 | #404000 | #2e2e00 | #242400 | + +### Success — H:155° C:0.14 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #ddffe8 | #bef8d1 | #9fe8b8 | #72ce95 | #48b475 | #1c985a | #007a44 | #006034 | #004a27 | #00361a | #002a13 | + +### Cyan — H:195° C:0.145 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #d4fffe | #a5faf9 | #7aeae9 | #2ad0d0 | #00b2b2 | #009393 | #007576 | #005c5c | #004747 | #003333 | #002828 | + +### Indigo — H:250° C:0.14 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #eef6ff | #d7eaff | #b4d8ff | #7cbdff | #53a0ec | #3284d0 | #1369b0 | #005190 | #003e71 | #002c54 | #002242 | + +### Violet — H:285° C:0.14 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #f4f4ff | #e5e5ff | #cfcfff | #b0adff | #938eeb | #7971d0 | #6057af | #4b4290 | #393172 | #282254 | #1f1b42 | + +### Magenta — H:320° C:0.14 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #fcf0ff | #f9ddff | #f2bffd | #da9de8 | #c07ecf | #a462b4 | #874895 | #6d3579 | #55265f | #3d1a45 | #301436 | + +### Rose — H:355° C:0.13 + +| 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 | +|---|---|---|---|---|---|---|---|---|---|---| +| #fff1f6 | #ffdde9 | #ffbfd7 | #f198bb | #d7799f | #bb5c84 | #9b436a | #7e3053 | #632240 | #48172d | #391223 | diff --git a/ILLUSTRATION_GUIDE.md b/ILLUSTRATION_GUIDE.md new file mode 100644 index 0000000..50b8a38 --- /dev/null +++ b/ILLUSTRATION_GUIDE.md @@ -0,0 +1,985 @@ +# Illustration & Diagram Generation Guide for AI Agents + +Production illustration generation using shape grammar formalism, parametric curve mathematics, cross-modal psychophysics, Gestalt composition constraints, and computational aesthetic validation. Designed as agent-executable specification — every formula is code-ready. Domain-agnostic: works for any brand category. + +This guide is the illustration counterpart to `COLOR_GUIDE.md` (color science, palette engineering, typography accessibility) and `SPATIAL_GUIDE.md` (spacing, curvature, proportions, target sizes). It takes three inputs — a validated color palette, a spatial token system, and a brand emotional profile (target PAD vector) — and produces a complete, validated illustration system: shape vocabulary, construction rules, composition algorithms, and aesthetic quality metrics. + +Four phases: **Illustration Strategy** (human-driven shape semantics and conceptual metaphor decisions grounded in perception science), **Shape Engineering** (agent-executable parametric math), **Composition & Layout** (agent-executable arrangement algorithms), and **Aesthetic Validation** (agent verification of complexity, balance, and congruence). The first phase grounds the human operator in shape psychophysics and visual communication research. The remaining phases are fully computable. + +**Prerequisite:** Complete `COLOR_GUIDE.md` Phases 1–3 and `SPATIAL_GUIDE.md` Phases 1–2 first. This guide references the PAD emotional model, color token architecture, font-color congruence framework, border radius system, spacing scale, and Gestalt proximity threshold defined there. + +--- + +## Phase 1: Illustration Strategy (Human Judgment) + +Before generating shapes or composing diagrams, ground the decisions in shape perception science, conceptual metaphor theory, and multimedia learning research. The agent assists with shape-emotion mapping and constraint validation; the human makes the judgment call on visual vocabulary and metaphor selection. + +### Shape Semantics Framework + +Geometric shapes carry measurable semantic associations. These associations are cross-culturally robust for basic forms but context-dependent for complex compositions — parallel to color-in-context theory (Elliot & Maier 2012, see `COLOR_GUIDE.md` Phase 1). + +**Sources:** Lundholm (1921); Aronoff et al. (1992); Larson et al. (2007); Dehaene et al. (2025), *eLife*. + +| Shape | Primary Semantic | Secondary Associations | Cultural Variations | +|---|---|---|---| +| Circle | Unity, wholeness, cycles, protection | Infinity, perfection, continuity | Zen: enlightenment; Chinese: heavens; Universal: sun/moon | +| Triangle (point-up) | Hierarchy, stability, ascent | Direction, dynamism, trinity | Religious trinity; navigation; fire element | +| Triangle (point-down) | Descent, instability, feminine energy | Funnel, concentration | Water element; inverted hierarchy | +| Square | Solidity, order, material world | Dependability, permanence | Chinese: earth; Native American: permanence | +| Hexagon | Balance, equilibrium, structure | Efficiency, tessellation | Beehive/nature; Star of David | +| Star (5-pointed) | Hope, protection, wholeness | Achievement, five elements | Islam: hope; Wicca: protection | +| Arrow | Direction, movement, causality | Progress, force, flow | Universal: path/trajectory; maps to PATH image schema | +| Rounded rectangle | Containment with approachability | Screens, cards, containers | Modern UI convention | + +**Neural basis:** Geometric shapes activate intraparietal and inferior temporal regions also involved in mathematical processing, indicating a strong link between geometric intuition and mathematical cognition (Dehaene et al. 2025). Humans encode shapes symbolically via discrete regularities (symmetries, parallelism), distinct from continuous visual processing. + +### Conceptual Metaphor Theory + +**Source:** Lakoff, G. & Johnson, M. (1980). *Metaphors We Live By*. University of Chicago Press. + +Humans understand abstract concepts by mapping them onto concrete spatial experiences. This operates through **image schemas** — recurring pre-conceptual patterns arising from embodied experience. Every diagram implicitly uses these schemas; making them explicit improves communication effectiveness. + +| Image Schema | Structure | Visual Representation | Abstract Mapping | +|---|---|---|---| +| CONTAINER | Inside, outside, boundary | Enclosed shapes, bordered regions | Inclusion/exclusion, scope, categories | +| PATH | Source, trajectory, goal | Arrows, lines, flow diagrams | Progress, process, narrative sequence | +| FORCE | Push, pull, resistance | Thickness, weight, directional emphasis | Causality, influence, agency | +| UP-DOWN | Vertical axis | Vertical positioning | GOOD IS UP, MORE IS UP, POWER IS UP | +| LINK | Connection between entities | Lines, connectors | Relationship, dependency | +| BALANCE | Equilibrium point | Symmetry, centering | Fairness, stability | + +**Spatial metaphors in design** (Casasanto & Bottini 2022, *Frontiers in Psychology*): +- **GOOD IS UP**: Positive valence associated with upward spatial position — reflected in UI patterns where success states appear above failure states. +- **GOOD IS RIGHT**: Positive emotions linked to right-side space. Products placed on the right are preferred. +- **Mental number line**: Smaller numbers instinctively mapped left, larger numbers right (reversed in right-to-left reading cultures). + +**Design implication:** Visual metaphors are not universal — the direction of mental timelines varies across cultures (left-to-right vs. right-to-left). Ignoring cultural variation in spatial metaphors degrades user experience. + +### Bouba/Kiki Cross-Modal Correspondences + +**Sources:** Kohler (1929), *Gestalt Psychology*; Ramachandran & Hubbard (2001), *J. Consciousness Studies*; Fort et al. (2022), *Scientific Reports*. + +Shape properties map systematically to abstract concepts through cross-modal correspondences. The bouba/kiki effect — first observed as "maluma/takete" by Kohler — demonstrates that rounded shapes associate with softness, warmth, and low pitch, while angular shapes associate with sharpness, coldness, and high pitch. Agreement rate: **95–98%** across cultures, replicated in preverbal infants as young as 4 months. + +| Shape Property | Abstract Mapping | Direction | Mechanism | +|---|---|---|---| +| Rounded/curvy | Lower pitch, larger size, heavier weight, slower speed | Round = low-frequency, large, heavy | Physical sound-shape correspondence | +| Angular/spiky | Higher pitch, smaller size, lighter weight, faster speed | Sharp = high-frequency, small, light | Abrupt spectral changes map to sharp contours | +| Symmetrical | Stability, order, competence | Regularity = trustworthiness | Processing fluency | +| Asymmetrical | Dynamism, creativity, novelty | Irregularity = energy | Effortful processing → arousal | + +**Quantitative findings:** +- Curvature preference meta-analysis: **Hedges' g = 0.39** (medium effect size) across tasks and contexts (Chuquichambi et al., CBS working paper). +- "Balance x Continuity" model predicts bouba/kiki with **mean R² = 0.60** (range 0.26–0.94) from existing datasets (Fort et al. 2022). + +**Implication for illustration:** Shape property choices are not arbitrary stylistic decisions — they carry measurable semantic weight. An angular illustration vocabulary signals precision/urgency; a rounded vocabulary signals warmth/approachability. These map directly onto the PAD model (see `SPATIAL_GUIDE.md` Phase 1 § Geometric Personality Framework). + +### Shape-Emotion PAD Mapping + +Extending `SPATIAL_GUIDE.md` Phase 1 with quantitative data from shape psychophysics: + +| Shape Property | Primary PAD Axis | Direction | Effect Size | Key Source | +|---|---|---|---|---| +| Curvature | Pleasure | Curved ↑ | g = 0.39 | Chuquichambi et al. (meta-analysis) | +| Angularity | Arousal + Dominance | Angular ↑ | Amygdala bilateral activation | Bar & Neta 2007 | +| Symmetry | Pleasure | Symmetric ↑ | Longer fixation, higher ratings | Frontiers in Psychology 2016 | +| Complexity (D) | Arousal | Mid-range ↑ | D = 1.3–1.5 sweet spot | Taylor et al. 2011 | +| Size | Dominance | Larger ↑ | Fitts' law, cross-domain | Fitts 1954 | +| Regularity | Pleasure + Dominance | Regular ↑ | Shorter description length | Feldman 2003 | +| Organic noise | Arousal (low) + Pleasure | Organic ↑ | 60% stress reduction at D ~1.4 | Taylor (Oregon lab) | + +**Critical moderator** (Leder et al. 2011, *Perception*): Curvature preference disappears when the object carries negative semantic valence. Applying high curvature to error states, destructive actions, or danger signals is incongruent and may reduce perceived severity. See `SPATIAL_GUIDE.md` Phase 2 § Curvature by Semantic Role. + +### Diagram Effectiveness Principles + +**Source:** Mayer, R. E. (2009). *Multimedia Learning* (2nd ed.). Cambridge University Press; Mayer & Fiorella (2014), *Cambridge Handbook of Multimedia Learning*. + +Mayer's Cognitive Theory of Multimedia Learning rests on three assumptions: dual-channel processing (visual + auditory), limited capacity per channel, and active processing (select, organize, integrate). These produce measurable design principles with replicated effect sizes: + +| Principle | Description | Studies Supporting | Median Effect Size (d) | +|---|---|---|---| +| Spatial Contiguity | Place words and pictures near each other | 22/22 | **1.10** | +| Temporal Contiguity | Present corresponding words and pictures simultaneously | 9/9 | **1.22** | +| Coherence | Exclude extraneous material | 23/23 | **0.86** | +| Redundancy | Graphics + narration > graphics + narration + text | 16/16 | **0.86** | +| Signaling | Add cues to highlight organization | 24/28 | **0.41** | +| Segmenting | Break complex information into user-paced segments | — | Moderate | +| Pre-training | Teach component names before processes | — | Moderate | + +**Application to illustration:** +- **Spatial contiguity (d = 1.10)** is the strongest effect. Labels must be placed directly adjacent to the element they describe — never in a separate legend when space permits. +- **Coherence (d = 0.86)** means decorative elements that do not support the message actively harm learning. Every shape must serve a communicative purpose. +- **Signaling (d = 0.41)** supports the use of visual hierarchy (weight, size, color) to guide attention through the diagram. + +### Icon and Pictogram Design Constraints + +**Source:** FHWA-RD-03-065 (2003), *Symbol Signs: Icon Design Guidelines*. Federal Highway Administration. + +The FHWA research defines mathematical constraints for recognizable symbols, grounded in Gestalt psychology: + +| Constraint | Specification | +|---|---| +| Design grid | 20 x 20 units | +| Minimum feature size | No significant detail smaller than 1 grid unit (5% of symbol area) | +| Minimum visual angle (detail) | 3 degrees for significant details | +| Minimum visual angle (stroke) | 2 degrees for line thickness | +| Detail budget rule | Only details whose removal would reduce recognition should be included | + +**Gestalt-based symbol principles (FHWA):** +1. **Figure/ground**: Clear, stable, solid relationship between symbol elements and background. +2. **Figure edges**: Solid shapes preferred over thin/dotted lines (except for depicting motion). +3. **Closure**: Closed figures without discontinuous lines or disjointed elements. +4. **Simplicity**: Include only necessary detail. Removing any essential detail should significantly reduce recognition. +5. **Unity**: All parts enclosed within a single boundary. + +### Decision Framework + +Score each candidate shape vocabulary against the brand's target PAD vector and communication goals: + +| Dimension | Weight | What to Evaluate | +|---|---|---| +| Semantic Clarity | High | Does each shape carry a clear, unambiguous meaning for the target audience? | +| PAD Congruence | High | Does the shape vocabulary's PAD profile match the brand's target? (See Shape-Emotion PAD Mapping) | +| Cross-Modal Fit | Medium | Do shapes align with bouba/kiki correspondences for intended concepts? | +| Diagram Effectiveness | Medium | Does the vocabulary support spatial contiguity (d = 1.10) and coherence (d = 0.86)? | +| Construction Feasibility | Medium | Can shapes be generated from parametric primitives? (See Phase 2) | +| Color System Fit | Medium | Does the shape vocabulary compose well with semantic color tokens? (See `COLOR_GUIDE.md`) | +| Cultural Safety | Low | Are shapes free of unintended cultural associations for the target audience? | +| Complexity Budget | Low | Is the total number of distinct shape types within cognitive load limits (6–8 categories max)? | + +The human operator evaluates these qualitatively against brand intent. The agent can compute PAD congruence checks and construction feasibility. Once the shape vocabulary is chosen, everything below is math. + +--- + +## Phase 2: Shape Engineering (Agent Math) + +From this point forward, the agent generates shape primitives autonomously. The human provides perceptual feedback during validation. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +## Shape Primitive Vocabulary + +All illustration primitives are parametric functions that emit SVG path data. An agent specifies shapes by parameter vectors — not by drawing pixels. + +### Superellipse (Lame Curve) + +The superellipse generalizes the ellipse into a family of curves controlled by an exponent parameter. It is the mathematical basis for "squircle" corners used in Apple's design language and the emerging CSS `corner-shape: superellipse()` specification. + +**Source:** Lame, G. (1818). *Examen des differentes methodes employees pour resoudre les problemes de geometrie*. Popularized by Piet Hein (1965) for urban design. + +**Implicit equation:** + +``` +|x/a|^n + |y/b|^n = 1 +``` + +**Parametric equations** (for SVG path generation, `0 <= t < 2*pi`): + +``` +x(t) = a * |cos(t)|^(2/n) * sgn(cos(t)) +y(t) = b * |sin(t)|^(2/n) * sgn(sin(t)) +``` + +where `a`, `b` are semi-axes and `n` controls curvature. + +**Shape behavior by exponent:** + +| n | Shape | Emotional Signal | +|---|---|---| +| 0 < n < 1 | Four-armed star (concave sides) | Sharp, aggressive, high arousal | +| n = 1 | Rhombus (diamond) | Angular, directional | +| 1 < n < 2 | Rounded rhombus (convex sides) | Transitional | +| n = 2 | Ellipse (circle when a = b) | Neutral, balanced | +| n > 2 | Rounded rectangle (squircle territory) | Soft, approachable | +| n = 4 | Squircle (a = b) | Maximum approachability | + +**CSS superellipse mapping** (emerging specification): + +``` +CSS corner-shape: superellipse(k) --> Lame exponent n = 2^k + +k = 0 --> n = 1 (bevel) +k = 1 --> n = 2 (circular arc, standard border-radius) +k = 2 --> n = 4 (squircle) +``` + +**SVG implementation:** Sample `t` at 64–128 points, compute `(x, y)` pairs, emit as SVG `` using `M` and `L` commands, or fit cubic Bezier curves for smoother output. + +### Gielis Superformula + +A polar-coordinate generalization of the superellipse that can describe an enormous variety of natural and abstract forms with a single equation. + +**Source:** Gielis, J. (2003). "A generic geometric transformation that unifies a wide range of natural and abstract shapes." *American Journal of Botany*, 90(3), 333–338. + +**Polar equation:** + +``` +r(phi) = ( |cos(m * phi / 4) / a|^n2 + |sin(m * phi / 4) / b|^n3 )^(-1/n1) +``` + +**Cartesian conversion:** + +``` +x = r(phi) * cos(phi) +y = r(phi) * sin(phi) +``` + +**Parameters:** + +| Parameter | Role | Typical Range | +|---|---|---| +| `a`, `b` | Scaling factors | Often 1.0 | +| `m` | Rotational symmetry (integer m gives m-fold) | 1–12 | +| `n1` | Overall roundness | 0.1–10.0 | +| `n2`, `n3` | Shape exponents (pinching, sharpness) | 0.1–10.0 | + +**Special cases:** + +``` +m=4, n1=n2=n3=2, a=b=1 --> circle +m=4, n1=2, n2=n3=2, a!=b --> ellipse +m=0 --> circle (no angular modulation) +Varying n1, n2, n3: starfish, flowers, leaves, diatoms, seed shapes +``` + +### Cubic Bezier Curves (SVG Standard) + +The workhorse of SVG path data, font outlines, and UI animation easing functions. + +**Source:** Bezier, P. (1968). French Patent 1,475,841. + +**General Bezier curve of degree n** with control points `P_0, ..., P_n`: + +``` +B(t) = SUM_{i=0}^{n} C(n,i) * (1-t)^(n-i) * t^i * P_i, 0 <= t <= 1 +``` + +where `C(n,i)` is the binomial coefficient. + +**Cubic Bezier** (4 control points, the SVG `C` command): + +``` +B(t) = (1-t)^3 * P0 + 3*(1-t)^2 * t * P1 + 3*(1-t) * t^2 * P2 + t^3 * P3 +``` + +- `P0`, `P3` are endpoints (on-curve). +- `P1`, `P2` are control handles (off-curve), defining tangent direction and magnitude. + +### Decorative Curves + +Parametric curves for borders, backgrounds, and ornamental elements. All emit `(x, y)` pairs for SVG path generation. + +| Curve | Parametric Equations | Key Parameters | Visual Character | +|---|---|---|---| +| Rose (Rhodonea) | `x = a * cos(k*t) * cos(t)`, `y = a * cos(k*t) * sin(t)` | `a` (amplitude), `k` (petals: odd k → k petals, even k → 2k petals) | Floral, radial symmetry | +| Lissajous | `x = A * sin(a*t + delta)`, `y = B * sin(b*t)` | `a/b` ratio (lobe count), `delta` (rotation) | Oscilloscope, harmonic | +| Hypotrochoid (Spirograph) | `x = (R-r)*cos(t) + d*cos((R-r)/r * t)`, `y = (R-r)*sin(t) - d*sin((R-r)/r * t)` | `R` (fixed circle), `r` (rolling), `d` (tracing distance) | Geometric, mandala-like | +| Epitrochoid | `x = (R+r)*cos(t) - d*cos((R+r)/r * t)`, `y = (R+r)*sin(t) - d*sin((R+r)/r * t)` | Same as hypotrochoid | Outer-rolling variant | + +**SVG implementation:** Iterate `t` from `0` to `2*pi * lcm(R, r) / R` (for spirograph) or `0` to `2*pi` (for rose/Lissajous with integer ratios). Emit polyline or fit Bezier curves. + +## Shape Grammar Formalism + +Shape grammars provide the formal framework for constructing complex illustrations from primitive shapes through rule-based transformations. + +**Sources:** Stiny, G. & Gips, J. (1972). "Shape Grammars and the Generative Specification of Painting and Sculpture." *Information Processing 71*; Knight, T. (2003). "Computing with ambiguity." *Environment and Planning B*. + +### Formal Definition + +A shape grammar is a 4-tuple: + +``` +SG = (S, L, R, I) +``` + +where: +- `S` — a finite set of shapes drawn from a shape algebra +- `L` — a finite set of labels (markers/tags) controlling rule application +- `R` — a finite set of production rules of the form `a -> b` (LHS -> RHS) +- `I` — an initial shape (axiom/start symbol) + +### Rule Application + +Given a current working shape `s` and a rule `a -> b`, the transformation produces a new shape `t`: + +``` +t = (s - a) + b +``` + +where `s - a` removes the matched subshape and `+ b` adds the replacement. + +### Embedding Relation + +A rule `a -> b` can only apply if `a` is a **part** (subshape) of `s`: + +``` +s . a = a (the product of s and a equals a) +``` + +This **embedding relation** is the most powerful aspect of shape grammars — it allows rules to match against any recognizable subshape, not just shapes explicitly placed by prior rules. This enables **emergence**: recognition of shapes that arise from spatial relationships of existing elements. + +### Weighted (Augmented) Shapes + +Shapes carry non-geometric attributes via tuple notation: + +``` +w_s = (s, a_s^1, a_s^2, a_s^3, ...) +``` + +where `s` is the geometric shape and `a_s^i` are attributes (color token, semantic role, weight, etc.). Operations (union, product, difference) apply separately across each component. + +### Agent-Executable Encoding + +A shape grammar encodes as a rule engine: +1. **Shape representation**: shapes as sets of line segments or parametric primitives, each with carrier and boundary +2. **Pattern matching**: detect subshapes via the embedding relation +3. **Rule selection**: when multiple rules match, choose stochastically, by priority, or exhaustively +4. **Transformation**: apply Euclidean transformations (translation, rotation, reflection, scaling) to embed the RHS + +## Construction Constraints + +Inspired by the IBM Design Language illustration system, which has one of the most rigorously formalized corporate illustration grammars. + +**Source:** IBM Design Language (ibm.com/design/language/illustration). + +### Grid Alignment + +``` +All anchor points snap to the spatial grid defined in SPATIAL_GUIDE.md. +Minimum shape dimension = base_unit (8px default). +Minimum negative space between shapes = base_unit. +Layered shapes require base_unit safe area. +``` + +### Shape Primitives + +Illustrations are constructed from: +- Squares, circles, rectangles, triangles (geometric foundation) +- Superellipses (controlled curvature) +- Cubic Bezier curves (organic forms built on geometric scaffolding) + +### Angle Constraints + +``` +Preferred angles: 15, 30, 45, 60, 75, 90 degrees +Other angles: permitted but require justification +``` + +### Radii Rules + +``` +Nested curved lines must maintain equal spacing with increasing radius. +No mixed corner radii within a nested design. +Border radius tokens from SPATIAL_GUIDE.md apply to all illustration containers. +``` + +## Noise and Organic Deformation + +For illustrations requiring natural, organic character (biology diagrams, nature scenes, terrain), apply noise-based deformation to geometric primitives. + +### Perlin Noise + +**Sources:** Perlin, K. (1985). "An Image Synthesizer." *ACM SIGGRAPH*, 19(3), 287–296; Perlin, K. (2002). "Improving Noise." *ACM Trans. Graphics*, 21(3), 681–682. + +**Improved fade function** (5th-degree smoothstep with zero first and second derivatives at endpoints): + +``` +fade(t) = t^3 * (t * (t * 6 - 15) + 10) +``` + +**2D Perlin noise algorithm:** +1. Divide space into unit cells. Assign pseudorandom unit gradient vector `g` at each grid vertex. +2. For point `(x, y)`: identify four surrounding grid corners. Compute offset vectors from each corner. +3. Dot products: for each corner `i`, compute `dot(g_i, offset_i)`. +4. Interpolation: bilinear interpolation using faded fractional coordinates: + +``` +u = fade(x - floor(x)) +v = fade(y - floor(y)) +result = lerp(v, + lerp(u, dot00, dot10), + lerp(u, dot01, dot11) +) +``` + +Output range: approximately `[-1, 1]`. + +### Fractal Brownian Motion (fBm) + +Layering multiple octaves of noise for richer, more natural-looking results: + +``` +fBm(x, y) = SUM_{i=0}^{octaves-1} amplitude_i * noise(frequency_i * x, frequency_i * y) + +where: + frequency_i = lacunarity^i (commonly lacunarity = 2.0) + amplitude_i = persistence^i (commonly persistence = 0.5) +``` + +**Normalization:** + +``` +fBm_normalized = fBm / SUM_{i=0}^{octaves-1} persistence^i +``` + +**Typical parameters:** `octaves` = 4–8, `lacunarity` = 2.0, `persistence` = 0.5. + +### SVG Path Deformation + +Apply noise-based displacement to any SVG path: + +``` +x_deformed = x_original + amplitude * noise(x_original * freq, y_original * freq) +y_deformed = y_original + amplitude * noise(x_original * freq + 1000, y_original * freq + 1000) +``` + +The offset (`+1000`) ensures x and y displacements use different noise values. `amplitude` controls distortion magnitude; `freq` controls granularity. + +## Recursive Systems (Reference) + +For fractal and plant-like illustration elements. These are reference formulas — the agent selects from them based on the illustration concept. + +### L-Systems (Lindenmayer Systems) + +**Source:** Prusinkiewicz, P. & Lindenmayer, A. (1990). *The Algorithmic Beauty of Plants*. Springer-Verlag. + +String-rewriting system with turtle graphics interpretation: +- `F` → move forward and draw +- `+` → turn left by angle +- `-` → turn right by angle +- `[` → push state (branch start) +- `]` → pop state (branch end) + +| Fractal | Axiom | Rules | Angle | +|---|---|---|---| +| Koch curve | `F` | `F -> F+F-F-F+F` | 90° | +| Sierpinski triangle | `A` | `A -> B-A-B`, `B -> A+B+A` | 60° | +| Dragon curve | `F` | `F -> F+G`, `G -> F-G` | 90° | +| Fractal plant | `X` | `X -> F+[[X]-X]-F[-FX]+X`, `F -> FF` | 25° | + +### Iterated Function Systems (IFS) + +**Source:** Barnsley, M. F. (1988). *Fractals Everywhere*. Academic Press. + +Contractive affine transformations applied iteratively: + +``` +f_i(x, y) = (a_i*x + b_i*y + e_i, c_i*x + d_i*y + f_i) +``` + +**Barnsley Fern coefficients:** + +| i | a | b | c | d | e | f | p | +|---|---|---|---|---|---|---|---| +| 1 | 0.00 | 0.00 | 0.00 | 0.16 | 0.00 | 0.00 | 0.01 | +| 2 | 0.85 | 0.04 | -0.04 | 0.85 | 0.00 | 1.60 | 0.85 | +| 3 | 0.20 | -0.26 | 0.23 | 0.22 | 0.00 | 1.60 | 0.07 | +| 4 | -0.15 | 0.28 | 0.26 | 0.24 | 0.00 | 0.44 | 0.07 | + +**Chaos Game algorithm:** Pick random point; iteratively select transformation `f_i` with probability `p_i`; apply to current point; plot. Repeat for 10,000–1,000,000+ iterations. + +--- + +## Phase 3: Composition & Layout (Agent Math) + +From this point forward, the agent composes shapes into diagrams and illustrations autonomously. The human provides perceptual feedback during validation. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +## Visual Balance Computation + +### Deviation of Center of Mass (DCM) + +The most rigorously validated measure of visual balance. Treats pixel luminance (or saliency) as physical mass and computes how far the composition's center of gravity deviates from its geometric center. + +**Source:** Hubner, R. & Fillinger, M. G. (2016). "Comparison of Objective Measures for Predicting Perceptual Balance and Visual Aesthetic Preference." *Frontiers in Psychology*, 7, 335. + +**Formula:** + +For an image where each pixel `(i, j)` has mass `m(i, j)` (luminance, saliency, or binary occupancy): + +``` +b_x = SUM(i * m(i,j)) / N (mass-weighted horizontal centroid) +b_y = SUM(j * m(i,j)) / N (mass-weighted vertical centroid) + +where N = SUM(m(i,j)) (total mass) +``` + +Normalized location: `b'_x = b_x / width` (ranges 0 to 1; geometric center = 0.5) + +Deviation: `d_x = 0.5 - b'_x`, `d_y = 0.5 - b'_y` + +Final DCM (as percentage): + +``` +DCM = 100 * sqrt(d_x^2 + d_y^2) +``` + +Lower DCM = higher balance. + +**Empirical validation:** N = 16 participants, 130 stimuli. DCM correlated with subjective balance ratings: r = -0.822, p < 0.001, **R² = 0.675**. DCM outperformed all other objective measures tested. + +### APB Symmetry Decomposition + +**Source:** Wilson, A. & Chatterjee, A. (2005). "The assessment of preference for balance." *Empirical Studies of the Arts*, 23(2), 165–180. + +Decomposes an image into symmetry measures across four axes (horizontal, vertical, main diagonal, anti-diagonal), each computed in two ways (halves and inner-outer): + +``` +Divide image into four vertical strips A1-A4. +h = (|[f(A1) + f(A2)] - [f(A3) + f(A4)]| / N) * 100 +h_io = (|[f(A1) + f(A4)] - [f(A2) + f(A3)]| / N) * 100 +``` + +Analogous measures for vertical (`v`, `v_io`), main diagonal (`md`, `md_io`), anti-diagonal (`ad`, `ad_io`). The APB score is the mean of all eight partial measures. Lower = more balanced. + +**Empirical validation:** Multiple regression R² = 0.751, F(8,121) = 45.5, p < 0.001. Horizontal component had the largest standardized beta. + +### Arnheim's Visual Weight Factors + +**Source:** Arnheim, R. (1974). *Art and Visual Perception*. University of California Press. + +Qualitative factors affecting visual weight, applicable to computing element-level mass for DCM: + +``` +visual_weight(element) = f(area, luminance_inverse, saturation, warmth, regularity, distance_from_center) +``` + +- Larger area = heavier +- Darker value = heavier +- Higher saturation = heavier +- Warm colors (red, orange) = heavier than cool (blue, green) +- Regular shapes = heavier than irregular +- Distance from center acts as lever arm (moment = weight × distance) + +## Gestalt Proximity Formalization + +**Sources:** Kubovy, M. & Wagemans, J. (1995), *Psychological Science*; Kubovy, M., Holcombe, A. O., & Wagemans, J. (1998), *Cognitive Psychology*; Kubovy, M. & van den Berg, M. (2008), *Psychological Review*. + +### The Pure Distance Law + +The probability of perceiving grouping along a given orientation is a distance-decay function: + +``` +log(p_k / p_base) = -alpha * (d_k / d_base - 1) +``` + +where `d_k` is the center-to-center distance between elements along orientation `k`, `d_base` is the shortest inter-element distance, and `alpha` is the empirically fitted attraction constant. + +**Key finding:** Grouping strength depends only on the **ratio** of local distances, not absolute distances. + +**Extension with similarity** (Kubovy & van den Berg, 2008): + +``` +log(p_k / p_base) = -alpha * (d_k / d_base - 1) + beta * delta_L +``` + +where `delta_L` is the luminance difference supporting grouping and `beta` is the similarity weight. + +**Application:** This formalizes the Gestalt proximity threshold from `SPATIAL_GUIDE.md` Phase 2 § Gestalt Proximity Threshold (`within_group_spacing < between_group_spacing * 0.5`) as a special case of the pure distance law. + +### Continuity as Curvature Minimization + +The visual system prefers contours that minimize total curvature: + +``` +minimize: INTEGRAL(kappa(s)^2 ds) +``` + +where `kappa(s)` is the curvature at arc-length parameter `s`. This is equivalent to the Euler elastica problem — connecting elements with smooth, low-curvature paths produces stronger perceived continuity. + +**Source:** Ullman, S. (1976). "Filling-in the gaps." *Biological Cybernetics*. + +## Constraint-Based Layout + +Inspired by the Penrose system (CMU), which provides a fully declarative, constraint-based diagram generation approach. + +**Source:** Penrose System (penrose.cs.cmu.edu); Ye et al. (2020), *SIGGRAPH 2020*. + +### Constraint Catalog + +Spatial relationship constraints an agent can declare and solve: + +| Category | Constraints | Purpose | +|---|---|---| +| Containment | `contains(outer, inner)`, `containsWithPadding(outer, inner, pad)` | Nesting, grouping | +| Overlap | `overlapping(a, b)`, `disjoint(a, b)` | Venn diagrams, exclusion | +| Proximity | `near(a, b, dist)`, `notTooClose(a, b, min)`, `touching(a, b)` | Gestalt grouping | +| Direction | `above(a, b)`, `below(a, b)`, `leftOf(a, b)`, `rightOf(a, b)` | Spatial metaphors | +| Distribution | `distributeHorizontally(elements, gap)`, `distributeVertically(elements, gap)` | Even spacing | +| Geometry | `perpendicular(l1, l2)`, `collinear(a, b, c)`, `isRegular(polygon)` | Structural alignment | +| Comparison | `equal(a.width, b.width)`, `lessThan(a.height, maxH)` | Size coordination | + +### Solving Approach + +Constraints are satisfied via numerical optimization (gradient descent on a differentiable energy function) or constraint propagation (Cassowary linear solver for layout constraints). + +**Cassowary constraint format** (for linear constraints): + +``` +a_1*x_1 + a_2*x_2 + ... + a_n*x_n {=, <=, >=} c [strength: required|strong|medium|weak] +``` + +**Source:** Badros, G. J., Borning, A. & Stuckey, P. J. (2001). "The Cassowary Linear Arithmetic Constraint Solving Algorithm." *ACM TOPLAS*, 23(4), 462–513. + +## Proportion Systems + +### Golden Ratio + +``` +phi = (1 + sqrt(5)) / 2 ~= 1.6180339887 +``` + +**Golden section division:** Place the primary focal element at approximately 61.8% / 38.2% of the composition frame, rather than at the midpoint. + +**Empirical caveat:** The claim that phi is universally preferred is contested. Fechner (1876) found slight preference for golden rectangles, but effect sizes were small and subsequent replications mixed. Modern meta-analyses suggest the preference is real but modest and culturally modulated. Treat as a default to deviate from intentionally, not a law. + +**Source:** Livio, M. (2002). *The Golden Ratio*. Broadway Books. + +### Rule of Thirds + +Divide the composition into a 3x3 grid. Place key elements at the four intersection points. + +**Formalization** (Bhattacharya et al. 2010): + +``` +F = (1 / (H * W)) * [||x_0 - s_1||, ||x_0 - s_2||, ||x_0 - s_3||, ||x_0 - s_4||] +``` + +where `x_0` is the saliency center of mass and `s_i` are the four rule-of-thirds points at `(W/3, H/3)`, `(2W/3, H/3)`, `(W/3, 2H/3)`, `(2W/3, 2H/3)`. + +**Relationship to golden ratio:** Golden ratio divides at ~38.2%/61.8%; rule of thirds at 33.3%/66.7%. Close but not identical. They emerged independently. + +## Layout Algorithms + +The agent selects algorithm based on data structure: + +| Algorithm | Input Structure | Output | Best For | +|---|---|---|---| +| Force-directed | Graph (nodes + edges) | Node positions | Relationship diagrams, networks | +| Treemap | Hierarchical (tree + values) | Nested rectangles | Space-filling, proportion visualization | +| Circle packing | Hierarchical (tree + values) | Nested circles | Organic hierarchy, set containment | +| Voronoi tessellation | Seed points | Cell partitions | Territory, proximity-based regions | +| Constraint solver | Constraints | Element positions | Structured diagrams, labeled layouts | +| Narrative diagram | Ordered act sequence | N static compositions | Scroll-driven reveals, causal sequence diagrams | + +### Force-Directed Layout + +**Sources:** Fruchterman, T. M. J. & Reingold, E. M. (1991), *Software: Practice and Experience*; Kamada, T. & Kawai, S. (1989), *Information Processing Letters*. + +**Fruchterman-Reingold model:** + +``` +f_rep(d) = -k^2 / d (repulsive: between all node pairs) +f_att(d) = d^2 / k (attractive: between connected nodes) + +where k = C * sqrt(area / |V|) (ideal edge length) +``` + +**Kamada-Kawai stress minimization:** + +``` +E = SUM_{i0) [log(N(epsilon)) / log(1/epsilon)] +``` + +Overlay the image with boxes of side length `epsilon`. Count boxes `N(epsilon)` containing part of the pattern. Plot `log(N)` vs. `log(1/epsilon)`; the slope is D. + +**Preferred fractal dimension:** + +| Pattern Type | Preferred D Range | Notes | +|---|---|---| +| Statistical fractals (nature) | **1.3–1.5** | Most prevalent in nature and species-rich habitats | +| Exact fractals (geometric) | Higher D preferred | Precise repetition shifts optimum upward | +| Human eye search pattern | D = 1.5 | Intrinsic eye movement traces fractal at D ~1.5 | + +**Stress reduction:** Viewing natural fractal patterns (D = 1.3–1.5) reduces physiological stress by up to 60% (Taylor, University of Oregon lab). + +**"Fractal fluency" hypothesis:** The visual system is tuned to efficiently process fractal complexities found in nature, producing both aesthetic pleasure and restorative outcomes when D matches the eye's intrinsic search pattern of approximately 1.5. + +**Validation threshold:** + +``` +For illustrations targeting aesthetic pleasure: + assert 1.2 <= D <= 1.6 +For geometric/technical diagrams: + D constraint relaxed (structure clarity takes priority over fractal fluency) +``` + +### Machado-Cardoso Aesthetic Measure + +**Source:** Machado, P. & Cardoso, A. (1998). "Computing aesthetics." *Proc. 14th Brazilian Symposium on AI*; Machado et al. (2015), *Acta Psychologica*. + +``` +AM = IC / PC +``` + +where: +- **IC** (Image Complexity) — estimated via JPEG compression ratio: `IC = compressed_size / raw_size`. Higher ratio = lower complexity. +- **PC** (Processing Complexity) — estimated via compression ratio of the edge-detected image. Edge detection → compression approximates structural processing complexity. + +Images with high IC but low PC (rich visual content that is easy to parse structurally) score highest — matching the Berlyne model: complex but comprehensible. + +### Shannon Entropy + +**Source:** Shannon, C. E. (1948). *Bell System Technical Journal*. + +``` +H = -SUM(p_i * log2(p_i)) +``` + +where `p_i` is the probability of intensity level `i` (for tonal complexity) or orientation bin `i` (for edge-orientation entropy). + +**Application:** Edge-orientation entropy — measuring how evenly luminance edges distribute across orientations — predicts aesthetic ratings for man-made images (Redies et al. 2018, *Frontiers in Neuroscience*). + +### Berlyne's Inverted-U Curve + +**Source:** Berlyne, D. E. (1971). *Aesthetics and Psychobiology*. Appleton-Century-Crofts. + +Aesthetic appreciation follows an inverted-U relationship with complexity: + +``` +Too simple (low D, low H) --> boring, trivial +Optimal middle range --> maximum aesthetic pleasure +Too complex (high D, high H) --> overwhelming, incomprehensible +``` + +This applies across visual art, music, and cinema. The optimal range corresponds to fractal dimension D = 1.3–1.5 and moderate entropy. + +## Balance Validation + +``` +For every composed illustration: + compute DCM + assert DCM < 10.0 (highly balanced) + warn if DCM > 15.0 (noticeable imbalance) + + For intentionally asymmetric compositions: + DCM threshold relaxed + but document the asymmetry rationale +``` + +## Diagram Effectiveness Checklist + +For every instructional diagram, verify these principles are satisfied: + +``` +Spatial Contiguity (d = 1.10): + Labels placed directly adjacent to elements they describe. + No separate legend when labels can be integrated. + +Coherence (d = 0.86): + Every visual element serves a communicative purpose. + No decorative elements that do not support the message. + +Signaling (d = 0.41): + Visual hierarchy guides attention through the diagram. + Key elements distinguished by size, weight, or semantic color. + +Redundant Encoding (WCAG 1.4.1): + Every color signal paired with a non-color indicator (shape, label, pattern). + See COLOR_GUIDE.md Phase 3 § Color-Only Information. +``` + +## Congruence Check + +Verify alignment with the color and spatial token systems: + +``` +For each illustration element: + color_PAD = PAD vector from color properties (from COLOR_GUIDE.md) + spatial_PAD = PAD vector from spatial properties (from SPATIAL_GUIDE.md) + shape_PAD = PAD vector from shape properties (this guide) + + congruence_check: + |color_PAD.pleasure - shape_PAD.pleasure| <= 1 tier + |spatial_PAD.dominance - shape_PAD.dominance| <= 1 tier + |color_PAD.arousal - shape_PAD.arousal| <= 1 tier + + Incongruence red flags: + Angular shapes + high-curvature spatial tokens → shape says "threat," space says "warm" + Organic noise + geometric spatial grid → shape says "natural," layout says "rigid" + High-complexity illustration + minimal color palette → visual density mismatch + Rounded shapes + error semantic color → shape says "safe," color says "danger" +``` + +## Extended Validation Checklist + +For every generated illustration, verify: + +**Shape Primitives:** +- [ ] All shapes generated from parametric formulas (superellipse, Bezier, superformula) +- [ ] Shape exponents/parameters documented for reproducibility +- [ ] Shapes snap to spatial grid (`--space-*` tokens from `SPATIAL_GUIDE.md`) +- [ ] Preferred angles used (15°, 30°, 45°, 60°, 75°, 90°) or deviations justified +- [ ] Minimum feature size ≥ 1 grid unit (FHWA symbol constraint) + +**Composition:** +- [ ] DCM < 10.0 (or intentional asymmetry documented) +- [ ] Gestalt proximity threshold met: within-group spacing < between-group spacing × 0.5 +- [ ] Layout algorithm appropriate for data structure (force-directed for graphs, treemap for hierarchies) +- [ ] Spatial contiguity: labels adjacent to elements (Mayer, d = 1.10) +- [ ] Coherence: no decorative elements without communicative purpose (Mayer, d = 0.86) + +**Aesthetic Quality:** +- [ ] Fractal dimension D in range 1.2–1.6 (for organic illustrations) +- [ ] Berlyne complexity in optimal range (not too simple, not too complex) +- [ ] Shannon entropy computed and within expected range for illustration type +- [ ] Machado-Cardoso AM > threshold (high image complexity, low processing complexity) + +**Color and Spatial Congruence:** +- [ ] All colors from semantic token system (`--visual-*` tokens from `DESIGN_SYSTEM.md`) +- [ ] Shape PAD vector within ±1 tier of color PAD vector on all dimensions +- [ ] Shape PAD vector within ±1 tier of spatial PAD vector on all dimensions +- [ ] No incongruence red flags (see Congruence Check above) + +**Accessibility:** +- [ ] Every color signal has redundant encoding (WCAG 1.4.1) +- [ ] Minimum contrast for text within illustrations meets APCA thresholds (see `COLOR_GUIDE.md` Phase 3) +- [ ] Illustrations function in both light and dark mode +- [ ] No seizure-risk content (no large-area saturated red transitions, no flashing > 3 Hz) +- [ ] `prefers-reduced-motion` respected for animated illustrations + +**Technical Output:** +- [ ] SVG output is valid and well-formed +- [ ] All path data uses standard commands (M, L, C, A, Z) +- [ ] No hardcoded color values — all colors reference CSS custom properties +- [ ] File size within budget (< 50 KB for icons, < 200 KB for full illustrations) + +--- + +## References + +### Shape Perception & Psychophysics +- **Bar, M. & Neta, M.** — "Humans prefer curved visual objects" (*Psychological Science*, 2006, 17(8), 645–648). Curvature preference at 84ms subliminal exposure. +- **Bar, M. & Neta, M.** — "Visual elements of subjective preference modulate amygdala activation" (*Neuropsychologia*, 2007, 45(10), 2191–2200). fMRI: sharp contours activate amygdala bilaterally. +- **Leder, H., Tinio, P. P. & Bar, M.** — "Emotional valence modulates the preference for curved objects" (*Perception*, 2011, 40(6), 649–655). Curvature preference only significant for positive/neutral valence. +- **Ramachandran, V. S. & Hubbard, E. M.** — "Synaesthesia — A window into perception, thought and language" (*J. Consciousness Studies*, 2001, 8(12), 3–34). Bouba/kiki effect formalization. +- **Kohler, W.** — *Gestalt Psychology* (Liveright, 1929). Original maluma/takete observation. +- **Fort, M., Schwartz, J.-L. & Boulle, G.** — "Resolving the bouba-kiki effect enigma by rooting iconic sound symbolism in physical properties of round and spiky objects" (*Scientific Reports*, 2022, 12, 19172). Balance x Continuity model, R² = 0.60. +- **Chuquichambi, E. G., Palumbo, L., Rey, G. D. & Munar, E.** — "How universal is preference for visual curvature?" (CBS working paper). Meta-analysis: Hedges' g = 0.39. +- **Dehaene, S. et al.** — "Neural mechanisms of geometric shape perception" (*eLife*, 2025, reviewed preprint 106464). Geometric intuition linked to mathematical cognition. +- **Lundholm, H.** — "The affective tone of lines" (*Psychological Review*, 1921). Curved = gentle/quiet; angular = agitating/hard/furious. +- **Aronoff, J., Barclay, A. M. & Stevenson, L. A.** — "The recognition of threatening facial stimuli" (*J. Personality and Social Psychology*, 1992). V-shapes → anger association. +- **Larson, C. L., Aronoff, J. & Stearns, J. J.** — "The shape of threat" (*Emotion*, 2007). Angular → anger; rounded → happiness. + +### Conceptual Metaphor & Spatial Cognition +- **Lakoff, G. & Johnson, M.** — *Metaphors We Live By* (University of Chicago Press, 1980). Conceptual Metaphor Theory and image schemas. +- **Casasanto, D. & Bottini, R.** — "Spatial metaphors in design and everyday objects" (*Frontiers in Psychology*, 2022). GOOD IS UP, GOOD IS RIGHT, mental number line. + +### Multimedia Learning +- **Mayer, R. E.** — *Multimedia Learning* (2nd ed., Cambridge University Press, 2009). Dual-channel, limited-capacity, active-processing model. +- **Mayer, R. E. & Fiorella, L.** — "Principles for reducing extraneous processing in multimedia learning" (*Cambridge Handbook of Multimedia Learning*, 2nd ed., 2014). Effect sizes: spatial contiguity d = 1.10, coherence d = 0.86. + +### Iconography & Symbol Design +- **FHWA-RD-03-065** — Chapter 4: Icon design guidelines (Federal Highway Administration, 2003). 20x20 grid, minimum feature size, Gestalt-based symbol principles. + +### Shape Grammars & Formal Systems +- **Stiny, G. & Gips, J.** — "Shape Grammars and the Generative Specification of Painting and Sculpture" (*Information Processing 71*, 1972, pp. 1460–1465). Foundational shape grammar paper. +- **Stiny, G.** — "Introduction to Shape and Shape Grammars" (*Environment and Planning B*, 1980, 7(3), 343–351). Shape algebra formalization. +- **Knight, T.** — "Computing with ambiguity" (*Environment and Planning B*, 2003, 30(2), 165–180). Emergence in shape computation. +- **Krishnamurti, R.** — "The construction of shapes" (*Environment and Planning B*, 1981, 8(1), 5–40). First interpreter with full embedding relation. + +### Parametric Curves & Procedural Generation +- **Lame, G.** — *Examen des differentes methodes employees pour resoudre les problemes de geometrie* (1818). Superellipse formulation. +- **Gielis, J.** — "A generic geometric transformation that unifies a wide range of natural and abstract shapes" (*American Journal of Botany*, 2003, 90(3), 333–338). Superformula. +- **Bezier, P.** — "Procede de definition numerique des courbes et surfaces non mathematiques" (French Patent 1,475,841, 1968). Bezier curves. +- **Perlin, K.** — "An Image Synthesizer" (*ACM SIGGRAPH*, 1985, 19(3), 287–296). Perlin noise. +- **Perlin, K.** — "Improving Noise" (*ACM Trans. Graphics*, 2002, 21(3), 681–682). Improved fade function. +- **Prusinkiewicz, P. & Lindenmayer, A.** — *The Algorithmic Beauty of Plants* (Springer-Verlag, 1990). L-systems. +- **Barnsley, M. F.** — *Fractals Everywhere* (Academic Press, 1988). IFS and Barnsley Fern. + +### Visual Balance & Composition +- **Hubner, R. & Fillinger, M. G.** — "Comparison of Objective Measures for Predicting Perceptual Balance and Visual Aesthetic Preference" (*Frontiers in Psychology*, 2016, 7, 335). DCM: r = -0.822, R² = 0.675. +- **Wilson, A. & Chatterjee, A.** — "The assessment of preference for balance" (*Empirical Studies of the Arts*, 2005, 23(2), 165–180). APB score. +- **Arnheim, R.** — *Art and Visual Perception: A Psychology of the Creative Eye* (revised ed., University of California Press, 1974). Structural skeleton, visual weight factors. +- **Bhattacharya, S., Sukthankar, R. & Shah, M.** — "A framework for photo-quality assessment and enhancement based on visual aesthetics" (*Proc. ACM Multimedia*, 2010, 271–280). Rule of thirds formalization. +- **Livio, M.** — *The Golden Ratio: The Story of Phi* (Broadway Books, 2002). + +### Gestalt Formalization +- **Kubovy, M. & Wagemans, J.** — "Grouping by proximity and multistability in dot lattices" (*Psychological Science*, 1995, 6(4), 225–234). Pure distance law. +- **Kubovy, M., Holcombe, A. O. & Wagemans, J.** — "On the lawfulness of grouping by proximity" (*Cognitive Psychology*, 1998, 35(1), 71–98). +- **Kubovy, M. & van den Berg, M.** — "The whole is equal to the sum of its parts" (*Psychological Review*, 2008, 115(1), 131–154). Proximity + similarity additivity. +- **Ullman, S.** — "Filling-in the gaps" (*Biological Cybernetics*, 1976). Continuity as curvature minimization. +- **Feldman, J.** — "Perceptual grouping by selection of a logically minimal model" (*Int. J. Computer Vision*, 2003, 55(1), 5–25). Closure as MDL. + +### Computational Aesthetics +- **Taylor, R. P., Micolich, A. P. & Jonas, D.** — "Fractal analysis of Pollock's drip paintings" (*Nature*, 1999, 399, 422). +- **Spehar, B., Clifford, C. W. G., Newell, B. R. & Taylor, R. P.** — "Universal aesthetic of fractals" (*Computers & Graphics*, 2003, 27(5), 813–820). +- **Taylor, R. P., Spehar, B., Van Donkelaar, P. & Hagerhall, C. M.** — "Perceptual and physiological responses to Jackson Pollock's fractals" (*Frontiers in Human Neuroscience*, 2011, 5, 60). D = 1.3–1.5 sweet spot. +- **Berlyne, D. E.** — *Aesthetics and Psychobiology* (Appleton-Century-Crofts, 1971). Inverted-U curve. +- **Machado, P. & Cardoso, A.** — "Computing aesthetics" (*Proc. 14th Brazilian Symposium on AI*, 1998, 219–228). AM = IC/PC. +- **Machado, P. et al.** — "Computerized measures of visual complexity" (*Acta Psychologica*, 2015, 160, 43–57). +- **Birkhoff, G. D.** — *Aesthetic Measure* (Harvard University Press, 1933). M = O/C. +- **Shannon, C. E.** — "A mathematical theory of communication" (*Bell System Technical Journal*, 1948, 27(3), 379–423). +- **Redies, C., Brachmann, A. & Hayn-Leichsenring, G. U.** — "Edge-orientation entropy predicts preference for diverse types of man-made images" (*Frontiers in Neuroscience*, 2018, 12, 678). +- **Hasler, D. & Suesstrunk, S. E.** — "Measuring colorfulness in natural images" (*Proc. SPIE*, 2003, 5007, 87–95). Colorfulness formula. + +### Layout Algorithms +- **Fruchterman, T. M. J. & Reingold, E. M.** — "Graph drawing by force-directed placement" (*Software: Practice and Experience*, 1991, 21(11), 1129–1164). +- **Kamada, T. & Kawai, S.** — "An algorithm for drawing general undirected graphs" (*Information Processing Letters*, 1989, 31(1), 7–15). +- **Badros, G. J., Borning, A. & Stuckey, P. J.** — "The Cassowary Linear Arithmetic Constraint Solving Algorithm" (*ACM TOPLAS*, 2001, 23(4), 462–513). +- **Bruls, M., Huizing, K. & van Wijk, J. J.** — "Squarified Treemaps" (*Joint Eurographics / IEEE TCVG Symposium on Visualization*, 2000, 33–42). +- **Wang, W. et al.** — "Visualization of large hierarchical data by circle packing" (*CHI 2006*, 517–520). + +### Industry Systems +- **IBM Design Language** — Illustration system (ibm.com/design/language/illustration). 8px grid, shape primitives, angle constraints, radii rules. +- **Penrose System** — Constraint-based mathematical diagram generation (penrose.cs.cmu.edu). Domain/Substance/Style DSL. Ye et al. (2020), *SIGGRAPH 2020*. +- **Bertin, J.** — *Semiologie Graphique* (1967). Visual encoding channels. +- **Munzner, T.** — *Visualization Analysis and Design* (CRC Press, 2014). Channel effectiveness ranking. + +### Data Visualization +- **Wilkinson, L.** — *The Grammar of Graphics* (Springer, 2005). Formal framework for data-to-visual mapping. +- **Okabe, M. & Ito, K.** — "Color Universal Design" (2002). CVD-safe categorical palette. (See `COLOR_GUIDE.md` Phase 3.) diff --git a/SPATIAL_GUIDE.md b/SPATIAL_GUIDE.md new file mode 100644 index 0000000..72a7940 --- /dev/null +++ b/SPATIAL_GUIDE.md @@ -0,0 +1,743 @@ +# Spatial Design System Generation Guide for AI Agents + +Production spatial system generation using psychophysical shape perception, Gestalt grouping thresholds, Fitts' law motor modeling, modular proportion scales, and PAD-mapped geometric emotion. Designed as agent-executable specification — every formula is code-ready. Domain-agnostic: works for any brand category. + +This guide is the spatial counterpart to `COLOR_GUIDE.md`. It takes three inputs — a validated color palette, a chosen typography stack, and a brand emotional profile (target PAD vector) — and produces a complete, validated spatial token system: spacing scale, border radii, line weights, surface hierarchy, proportion ratios, and target sizes. + +Two phases: **Spatial Strategy** (human-driven brand geometry decisions grounded in perception science) and **Spatial Engineering** (agent-executable math producing token values). The first phase establishes the geometric personality of the brand. The second phase computes every token from that personality. + +**Prerequisite:** Complete COLOR_GUIDE.md Phases 1–3 first. This guide references the PAD emotional model, font-color congruence framework, and three-tier token architecture defined there. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +--- + +## Phase 1: Spatial Strategy (Human Judgment) + +Before computing spatial tokens, ground the geometric decisions in perception science. The agent assists with research retrieval and PAD alignment checks; the human makes the judgment call on brand geometry. + +### Geometric Personality Framework + +Color triggers emotional response along Pleasure-Arousal-Dominance. So do spatial properties — but on partially different axes. The table below maps each spatial dimension to its primary PAD axis, based on converging evidence from psychophysics, neuroaesthetics, and HCI research. + +| Spatial Property | Primary PAD Axis | Direction | Evidence Strength | Key Source | +|---|---|---|---|---| +| Curvature (border-radius) | Pleasure | Curved ↑, Angular ↓ | Strong (fMRI, cross-cultural, cross-species) | Bar & Neta 2007; Gómez-Puerto et al. 2015 | +| Whitespace amount | Pleasure | More space ↑ | Moderate (+20% comprehension) | Mapletree Studio 2024; Fogg et al. 2003 | +| Border weight | Dominance | Thicker ↑ | Moderate (accessibility audits) | WordPress 5.3 audit 2019 | +| Element size | Dominance | Larger ↑ | Strong (Fitts' law, cross-domain) | Fitts 1954; ISO 9241-411 | +| Spacing density | Arousal | Tighter ↑ | Moderate (cognitive load studies) | NNG 2024 | +| Symmetry | Pleasure | Symmetric ↑ | Strong (EEG, eye-tracking) | Frontiers in Psychology 2016 | +| Asymmetry | Arousal | Asymmetric ↑ | Moderate | Frontiers in Human Neuroscience 2015 | +| Proportion (golden ratio) | Pleasure | Closer to φ ↑ | Moderate (aesthetic-usability effect) | Tractinsky et al. 2000 | +| Surface luminance stepping | Dominance | Greater step ↑ | Moderate | Material Design elevation studies | + +**Critical insight:** Color saturation is the dominant arousal lever (η² = .693, Wilms & Oberfeld 2018). Font weight is the dominant dominance lever (see COLOR_GUIDE.md). Curvature is the dominant *spatial* pleasure lever. These three axes are partially orthogonal — they combine multiplicatively, not additively. Bold text + high chroma + angular geometry = maximum arousal AND dominance AND low pleasure (urgent, commanding, aggressive). Light text + medium chroma + curved geometry = moderate arousal, low dominance, high pleasure (friendly, approachable, warm). + +### Curvature Preference: The Evidence Base + +The human preference for curved over angular contours is one of the most replicated findings in visual perception. It is not a design opinion — it is a measured neural response. + +**Source:** Gómez-Puerto, G., Munar, E., & Nadal, M. (2015). "Preference for curvature: A historical and conceptual framework." *Frontiers in Human Neuroscience*, 9:712. Comprehensive review spanning decades of curvature research. + +Key experimental findings, chronologically: + +| Study | Sample | Finding | +|---|---|---| +| Fantz & Miranda 1975 | 1-week-old neonates | Longer fixation on curved vs. sharp contour forms | +| Lundholm 1921 | Adults | Curved lines perceived as "gentle, quiet"; sharp lines as "agitating, hard, furious" | +| Poffenberger & Barrows 1924 | Adults | Confirmed Lundholm's affective associations | +| Bar & Neta 2006, *Psychological Science* | Adults | Curved stimuli liked more than angular even at 84ms exposure (subliminal threshold) | +| Bar & Neta 2007, *Neuropsychologia* | Adults (fMRI) | Sharp contours activate amygdala bilaterally; curved do not. Sharp = neural threat signal | +| Quinn et al. 1997 | 3–4 month infants | Curvature preference facilitates Gestalt organization | +| Leder et al. 2011, *Perception* | Adults | Curvature preference only significant when object has positive/neutral valence | +| Silvia & Barona 2009 | Adults | Non-experts prefer circles to hexagons; expertise moderates effect | +| Gómez-Puerto et al. 2013 | Rural Ghana | Curvature preference confirmed cross-culturally in non-Western population | +| Munar et al. 2015 | Non-human primates | Curvature preference observed across species | +| Vartanian et al. 2013 | Adults | Curved interior architectural spaces subjectively preferred | +| UXPA Journal 2024 | N=187 (between-subjects) | Rounded app corners → higher aesthetics (M=4.63/7, α=.89), warmth (M=5.25/7, α=.83), ease of use, satisfaction, and prosocial behavior | + +**Critical moderator (Leder et al. 2011):** Curvature preference disappears when the object carries negative semantic valence. This means curvature is not a universal "make it better" lever — it works when the content supports a positive or neutral emotional frame. Error states, destructive actions, and danger signals are semantically negative. Applying high curvature to negative-valence UI elements is incongruent and may reduce perceived severity. + +### Shape × Emotion Associations + +**Sources:** Lundholm (1921); Aronoff et al. (1992); Larson et al. (2007); Uher (1991). + +| Contour Type | Emotional Association | Neural Basis | +|---|---|---| +| Smooth curves | Gentle, quiet, safe, affiliative, happy | No amygdala activation; fluent processing | +| Wavy lines | Affiliative adjectives (Uher 1991) | Low spatial frequency, easy to parse | +| Sharp angles | Agitating, hard, furious, threatening | Bilateral amygdala activation (Bar & Neta 2007) | +| Zigzag lines | Antagonistic adjectives (Uher 1991) | High spatial frequency, effortful processing | +| V-shapes | Anger (Aronoff et al. 1992; Larson et al. 2007) | Threat geometry (eyebrow configuration) | +| Rounded shapes | Happiness (Larson et al. 2007) | Approach motivation | + +**Processing fluency explanation:** Curves are preferred partly because they are computationally cheaper for the visual system. Smooth contours have lower spatial frequency — the visual cortex encodes them more economically, leading to faster mental rotation and improved visual search performance (Frontiers in Computer Science, 2024). Fluent processing → positive affect (the "beauty-in-averageness" / processing fluency pathway). + +### Context-Dependent Shape Meaning + +Like color emotion (Elliot & Maier 2012, Color-in-Context Theory), shape emotion is context-dependent: + +| Context | Angular = | Curved = | Source | +|---|---|---|---| +| Service environment (crowded) | Competence → higher satisfaction | — | Ohio State, news.osu.edu | +| Service environment (uncrowded) | — | Friendliness → higher satisfaction | Ohio State, news.osu.edu | +| Consumer power state: high | Preferred (competence signal) | — | Frontiers in Psychology 2021 | +| Consumer power state: low | — | Preferred (warmth signal) | Frontiers in Psychology 2021 | +| Professional/institutional setting | Structure, efficiency | Too casual | Architizer 2024 | +| Consumer/personal setting | Cold, impersonal | Inviting, warm | Architizer 2024 | + +**Implication for agents:** The optimal curvature is not fixed — it depends on the brand's target PAD profile, the audience power state, and the interaction context. The agent should compute curvature values that match the brand's target emotional profile, not default to maximum curvature. + +### Flat vs. Elevated Surfaces + +**Sources:** Interaction Design Foundation, Material Design documentation; Smashing Magazine (2017), "Using shadows and blur effects in UI design"; Kota.co.uk (2024), "The texture of trust." + +Flat design (no shadows, no gradients) and material design (elevation via shadows) represent two strategies for the same goal: communicating interactive hierarchy. + +| Strategy | Depth Mechanism | Trust Signal | Risk | +|---|---|---|---| +| Flat | Spacing + border contrast + surface tone | Modernity, premium, clarity | Loss of affordance cues (NNG: unlabeled elements, hidden actions) | +| Flat 2.0 | Subtle tone stepping + border emphasis | Clean but navigable | Requires disciplined surface hierarchy | +| Material | Drop shadows at computed elevation | Physical metaphor → intuitive | Visual noise if overused; complexity budget | + +**For flat design systems:** Without shadows, perceived depth is created entirely by **luminance stepping** between surface tiers. This is computable: + +``` +perceived_elevation = |L_surface − L_page_background| +``` + +The agent must ensure each surface tier has sufficient luminance difference to be perceptible. The Weber fraction for luminance discrimination is approximately 1–3% for the adapted observer. Surface steps should exceed this threshold. + +### Decision Framework + +Score each spatial decision against the brand's target PAD vector: + +| Dimension | What to Evaluate | Alignment Check | +|---|---|---| +| Curvature profile | Border-radius range across component types | Does curvature match target Pleasure level? | +| Spacing density | Base unit × scale factor | Does density match target Arousal level? | +| Border weight range | Min/max stroke widths | Does weight match target Dominance level? | +| Proportion system | Type scale ratio, layout ratios | Does scale contrast match hierarchy needs? | +| Surface hierarchy | Number of tiers, luminance stepping | Does depth match information architecture? | +| Symmetry strategy | Default symmetric vs. intentional asymmetry | Does layout energy match target Arousal? | +| Target sizing | Touch targets, interactive areas | Do sizes meet accessibility thresholds? | + +The human operator evaluates these qualitatively against brand intent. The agent can compute PAD-congruent token values. Once the geometric personality is chosen, everything below is math. + +--- + +## Phase 2: Spatial Engineering (Agent Math) + +From this point forward, the agent generates the spatial token system autonomously. The human provides perceptual feedback during validation. + +**IMPORTANT: The agent MUST write code to run the math then execute it, NEVER attempt to compute values directly. Strict mathematical adherence!** + +## Input: Brand Geometry Table + +The only human judgment calls. Everything downstream is computable math. + +| Parameter | Value | Rationale | +|---|---|---| +| Target Pleasure | Low / Medium / High | From brand PAD profile | +| Target Arousal | Low / Medium / High | From brand PAD profile | +| Target Dominance | Low / Medium / High | From brand PAD profile | +| Base spacing unit | 4px or 8px | 8px default; 4px for dense data UIs | +| Type scale ratio | 1.125–1.618 | See Proportion System section | +| Base font size | 14–18px | From typography decisions | +| Primary surface strategy | Flat / Flat 2.0 / Elevated | From design philosophy | + +## Spacing Scale Generation + +### Base Unit Selection + +The 8px base unit is the industry-converged standard (Google Material, Apple HIG, IBM Carbon, Shopify Polaris). It works because: + +1. **Divisibility:** 8 divides cleanly into common viewport widths (320, 360, 375, 390, 414, 768, 1024, 1440) +2. **Sub-pixel avoidance:** Halving yields 4px → 2px → 1px — all integers on 1×, 2×, and 3× displays +3. **Perceptual stepping:** 8px increments produce visible but not jarring differences at UI scale + +Use 4px base for dense UIs (data tables, dashboards, IDE-like interfaces) where finer control is needed. The 4px unit is a half-step within the 8px grid, not a separate system. + +### Scale Formula + +A modified geometric progression — not pure arithmetic (too uniform at large values) or pure geometric (non-integer values, gaps too large at small end): + +``` +spacing(step) = base × multiplier[step] + +Step: 0 1 2 3 4 5 6 7 8 9 10 +Multiplier: 0 1 2 3 4 6 8 10 12 16 20 +Value (8px): 0 8 16 24 32 48 64 80 96 128 160 +Value (4px): 0 4 8 12 16 24 32 40 48 64 80 +``` + +The multiplier progression: ×1, ×2, ×1.5, ×1.33, ×1.5, ×1.33, ×1.25, ×1.2, ×1.33, ×1.25. This compresses at small values (where absolute pixel differences are perceptible) and expands at large values (where proportional differences matter more). + +**Why not pure geometric (e.g., ×1.5 throughout)?** A strict 1.5× scale from 8px yields: 8, 12, 18, 27, 40.5... — non-integer values that cause sub-pixel rendering artifacts. The hybrid approach preserves integer multiples of the base unit while maintaining roughly proportional steps. + +### Spacing Density by Brand Arousal + +The spacing scale is the same for all brands. What changes is which steps are used for which purposes: + +| Target Arousal | Component padding | Section gap | Page margin | Emotional Effect | +|---|---|---|---|---| +| Low (calm) | step 3–4 (24–32px) | step 6–7 (64–80px) | step 8–9 (96–128px) | Generous space → low arousal, high pleasure | +| Medium (balanced) | step 2–3 (16–24px) | step 5–6 (48–64px) | step 7–8 (80–96px) | Moderate density → neutral arousal | +| High (energetic) | step 1–2 (8–16px) | step 4–5 (32–48px) | step 5–6 (48–64px) | Tight density → high arousal, high dominance | + +**Quantitative basis:** Generous whitespace around paragraphs increases reading comprehension by up to 20% (cognitive load research synthesized by Mapletree Studio 2024). Grid-based alignment improves usability ratings (Parallel HQ 2026). Tight spacing increases cognitive load but also increases perceived information density and system capability — valued in data-heavy professional contexts (NNG; MASTERCAWEB UX density research). + +### Gestalt Proximity Threshold + +**Source:** Kubovy, M., & van den Berg, M. (2008). "The whole is equal to the sum of its parts: A probabilistic model of grouping by proximity and similarity in regular patterns." *Psychological Review*. Springer (2017) — Tilt Aftereffect measurement of grouping strength: proximity produces larger perceptual grouping effects than color similarity. + +The visual system groups elements by proximity. The critical threshold is a **ratio**, not an absolute value: + +``` +within_group_spacing < between_group_spacing × 0.5 +``` + +If spacing within a group is less than half the spacing between groups, the visual system reliably perceives grouping. When the ratio approaches 1.0, grouping dissolves; elements are perceived as equidistant and ungrouped. + +**Token mapping for proximity-based grouping:** + +| Relationship | Spacing Step | Ratio to Within-Group | Example | +|---|---|---|---| +| Tightly related (within component) | step 1 (8px) | 1.0× (baseline) | Label-to-input, icon-to-text | +| Related (within group) | step 2 (16px) | 2.0× | Items in a list, form fields | +| Loosely related (between groups) | step 4–5 (32–48px) | 4–6× | Section to section, card groups | +| Unrelated (between sections) | step 6–8 (64–96px) | 8–12× | Page sections, major content areas | + +The ratio between adjacent tiers should be ≥ 2× for the grouping boundary to be perceptible. Ratios below 1.5× create ambiguous grouping. + +### Vertical Rhythm + +**Source:** Robert Bringhurst, *The Elements of Typographic Style* (2004); Gamma UX (2023), "Types of grids: The evolution toward the 4-point grid system." + +All vertical spacing should snap to multiples of the base unit to create a consistent visual rhythm. This is the typographic equivalent of rhythmic regularity in music — predictable patterns reduce cognitive effort. + +``` +line_height = ceil(font_size × line_height_ratio / base_unit) × base_unit + +Example: font_size = 16px, line_height_ratio = 1.5, base_unit = 8px + raw_value = 16 × 1.5 = 24px + snapped = ceil(24 / 8) × 8 = 24px ✓ + +Example: font_size = 14px, line_height_ratio = 1.5, base_unit = 8px + raw_value = 14 × 1.5 = 21px + snapped = ceil(21 / 8) × 8 = 24px (rounds up to grid) +``` + +Snapping ensures that every text block, every component, and every section boundary aligns to the grid. When elements sit on the grid, vertical scanning is effortless. When they drift off-grid, the eye perceives "something is wrong" even without conscious awareness. + +**Line-height ratios by context** (these feed into the snapping formula): + +| Context | Ratio | Rationale | Source | +|---|---|---|---| +| Body text (Latin) | 1.4–1.6× | Saccade return accuracy | Bringhurst 2004 | +| Body text (CJK, Thai, Devanagari) | 1.6–1.8× | Stroke density + diacritical clearance | COLOR_GUIDE.md Phase 3 | +| Headings (display) | 1.1–1.2× | Tight for visual cohesion at large sizes | Convention | +| Code blocks | 1.3–1.5× | Monospace alignment | Convention | +| Captions, metadata | 1.3–1.4× | Compact but readable | Convention | + +## Border Radius System + +### Curvature as Computed Emotional Lever + +Border radius modulates the curved–angular continuum from Phase 1. The agent computes radius values from the brand's target Pleasure level, the element's semantic role, and the element's size. + +**Normalized curvature metric:** + +``` +curvature_ratio(r, width, height) = r / (min(width, height) / 2) + + 0.0 = sharp rectangle (maximum angularity) + 0.0–0.15 = subtle rounding + 0.15–0.35 = moderate rounding + 0.35–0.50 = strong rounding + 0.50 = pill/stadium shape (maximum curvature for non-square rectangle) + 1.0 = circle (square element with r = 50%) +``` + +### Curvature by Brand Pleasure Target + +| Target Pleasure | Default curvature_ratio | Pixel Range (for 40px-tall element) | Emotional Signal | +|---|---|---|---| +| Low (austere) | 0.0–0.05 | 0–1px | Sharp, institutional, authoritative | +| Medium (balanced) | 0.10–0.20 | 2–4px | Professional, approachable, structured | +| High (warm) | 0.25–0.40 | 5–8px | Friendly, inviting, consumer-facing | +| Maximum (playful) | 0.50 | pill | Casual, youthful, high warmth | + +### Curvature by Semantic Role + +Semantic role overrides brand default when congruence demands it. Error states should be sharper (threat congruence); success states can be rounder (positive valence congruence). + +**Source for congruence penalty:** Fox, D., Shaikh, A. D., & Chaparro, B. S. (2007). "Effect of typeface appropriateness on the perception of documents." Measured 22% credibility loss for incongruent typography. The same principle applies to shape-emotion congruence. + +| Element Role | Curvature Adjustment | Rationale | +|---|---|---| +| Error/danger states | −50% from default (sharper) | Angular = threat congruent (Bar & Neta 2007) | +| Warning states | −25% from default | Mild threat signal | +| Success/positive states | +25% from default (rounder) | Curved = positive valence congruent (Leder et al. 2011) | +| Neutral/informational | Brand default | No semantic override needed | +| Avatars, user photos | 50% (circle) | Maximum warmth for human representation | +| Input fields | −25% from default | Structure signals "fill this in" | + +**Formula:** + +``` +element_radius(element_role, brand_default_ratio, element_height) = + let adjustment = semantic_adjustment[element_role] // -0.5, -0.25, 0, +0.25 + let adjusted_ratio = clamp(brand_default_ratio × (1 + adjustment), 0, 0.5) + return round(adjusted_ratio × (element_height / 2)) +``` + +### Radius Scale (Token Output) + +Rather than computing per-element, generate a radius token scale and assign tokens semantically: + +``` +radius_scale(brand_pleasure, base_unit) = + let base_r = base_unit × pleasure_multiplier[brand_pleasure] + return [0, base_r × 0.5, base_r, base_r × 1.5, base_r × 2, base_r × 3, 9999] + +pleasure_multiplier: + low = 0.5 → base_r = 4px (with 8px base unit) + medium = 1.0 → base_r = 8px + high = 1.5 → base_r = 12px +``` + +**Token naming:** + +| Token | Formula | Low Pleasure | Medium Pleasure | High Pleasure | +|---|---|---|---|---| +| `--radius-none` | 0 | 0px | 0px | 0px | +| `--radius-sm` | base_r × 0.5 | 2px | 4px | 6px | +| `--radius-md` | base_r | 4px | 8px | 12px | +| `--radius-lg` | base_r × 1.5 | 6px | 12px | 18px | +| `--radius-xl` | base_r × 2 | 8px | 16px | 24px | +| `--radius-2xl` | base_r × 3 | 12px | 24px | 36px | +| `--radius-full` | 9999px | pill | pill | pill | + +## Line Weight System + +### Border Weight as Dominance Lever + +Line weight operates on the **Dominance** axis — analogous to font weight (see COLOR_GUIDE.md Phase 1 § Font Weight as Independent Emotional Lever). Thicker borders command attention and assert structural authority. Thinner borders recede and defer. + +**Source:** WordPress 5.3 accessibility audit (make.wordpress.org, 2019). Documented measurable improvements in element discoverability when borders shifted from `1px solid #ddd` (low contrast, thin) to `1px solid #7e8993` (adequate contrast). The variable is contrast × thickness jointly — a high-contrast 1px border can outweigh a low-contrast 2px border. + +| Border Weight | Dominance Level | Perceptual Role | Use | +|---|---|---|---| +| 0px | Lowest | Invisible boundary — relies entirely on spacing/color | Open cards, flush layouts, borderless surfaces | +| 1px | Low | Structural definition without commanding | Default borders, input fields, separators, table rules | +| 2px | Medium | Emphasis — draws attention to boundary | Active/selected states, section breaks, focused inputs | +| 3px | Medium-High | Accent — semantic signal carrier | Callout left-borders, active tab indicators, progress bars | +| 4px+ | High | Maximum structural dominance | Section dividers, hero element boundaries, decorative rules | + +### Border Weight × Font Weight Congruence + +Line weight and font weight should be congruent on the Dominance axis. Mismatched weight levels create the same perceptual confusion documented for font-color incongruence (Fox et al. 2007, ~22% credibility loss): + +| Target Dominance | Font Weight | Border Weight | Curvature | Combined Signal | +|---|---|---|---|---| +| Low (elegant, delicate) | 300–400 | 0–1px | High radius | Approachable, refined, airy | +| Medium (professional) | 400–500 | 1–2px | Medium radius | Balanced, structured, readable | +| High (authoritative) | 600–700 | 2–3px | Low radius | Commanding, institutional, assertive | +| Maximum (impact) | 800–900 | 3–4px | Minimal radius | Bold, urgent, declarative | + +### Border Density Budget + +**Observation from user research:** Excessive borders ("lines lines lines everywhere") increase cognitive load and decrease pleasure. This parallels the chroma budget concept in COLOR_GUIDE.md Phase 3 (body text C < 0.04) and the cognitive load limits (max 2–3 emphasis colors, max 6–8 categorical colors). + +``` +borders_per_viewport_section ≤ 4 visible structural lines +``` + +Beyond approximately 4 visible borders in a single viewport section, perceived density spikes and pleasure drops. Mitigation strategies: +- Use **spacing** (Gestalt proximity) instead of borders when possible +- Use **surface tone changes** (luminance stepping) instead of borders for container definition +- Reserve borders for elements that need explicit boundary definition (inputs, tables, separators) + +### Border Contrast Rule + +A border's visual weight is contrast × thickness. A 1px border at high contrast (neutral-200 on white = ΔL ~0.04) is less prominent than a 1px border at maximum contrast (neutral-900 on white = ΔL ~0.72) but both are "1px." + +``` +perceived_border_weight = thickness_px × |L_border − L_background| +``` + +The agent should compute perceived weight, not just pixel thickness. Token assignment should specify both thickness and color: + +| Token | Thickness | Color (Light) | Color (Dark) | Perceived Weight | +|---|---|---|---|---| +| `--border-subtle` | 1px | neutral-200 | neutral-800 | ~0.04 | +| `--border-default` | 1px | neutral-300 | neutral-700 | ~0.10 | +| `--border-emphasis` | 1px | neutral-500 | neutral-500 | ~0.20 | +| `--border-strong` | 2px | neutral-700 | neutral-300 | ~0.72 | +| `--border-accent` | 3px | semantic color | semantic color | Variable (semantic) | + +## Surface Hierarchy + +### Luminance-Stepped Elevation (Flat Systems) + +Without shadows, perceived depth is created exclusively by luminance differences between surface tiers. The agent computes surface luminance values that exceed the perceptual discrimination threshold. + +**Perceptual threshold:** The Weber fraction for luminance discrimination is approximately 1–3% for the adapted observer at typical screen luminance (100–300 cd/m²). Surface steps must exceed this threshold to be perceivable. + +``` +ΔL_minimum = 0.03 (3% Weber fraction — just noticeable) +ΔL_comfortable = 0.04–0.06 (clearly distinct without being jarring) +``` + +### Surface Tier Formula + +Three tiers minimum (page, raised, muted). Each tier's lightness is offset from the page background: + +``` +Light mode (page L = 1.0): + surface_raised = L_page − ΔL_step_1 // e.g., 1.0 − 0.04 = 0.96 + surface_muted = L_page − ΔL_step_1 − ΔL_step_2 // e.g., 0.96 − 0.05 = 0.91 + +Dark mode (page L = 0.11): + surface_raised = L_page + ΔL_step_1 // e.g., 0.11 + 0.02 = 0.13 + surface_muted = L_page + ΔL_step_1 + ΔL_step_2 // e.g., 0.13 + 0.11 = 0.24 +``` + +Dark mode uses smaller initial steps because the Weber fraction increases at low luminance (the visual system is less sensitive to absolute differences in dark ranges). Subsequent steps can be larger. + +### Surface Tiers by Brand Dominance + +Higher dominance brands use **more contrast between tiers** (bolder surface differentiation). Lower dominance brands use **subtler stepping** (less visual assertiveness): + +| Target Dominance | ΔL between tiers (light) | ΔL between tiers (dark) | Emotional Effect | +|---|---|---|---| +| Low | 0.02–0.03 | 0.01–0.02 | Subtle, minimal, premium | +| Medium | 0.04–0.05 | 0.02–0.04 | Clear but not commanding | +| High | 0.06–0.08 | 0.04–0.06 | Bold surface distinction | + +### The 60-30-10 Surface Distribution + +**Source:** Johannes Itten, *The Art of Color* (1961); applied to UI by Material Design. + +The proportion of each surface tier controls visual rhythm and accent impact: + +| Proportion | Surface Role | Emotional Effect | +|---|---|---| +| 60% | Page background (dominant surface) | Sets base arousal — low chroma, extreme L = calm | +| 30% | Raised/muted surfaces (secondary) | Creates structure, rhythm, moderate contrast | +| 10% | Accent (semantic color, borders, interactive) | Draws attention — high arousal and dominance | + +For content-heavy pages (documentation, articles), use 95/5 — nearly all page background with minimal accent. For diagram-heavy or interactive pages, use 60-30-10. The agent should compute based on content type. + +## Proportion System + +### Modular Type Scale + +**Sources:** Robert Bringhurst, *The Elements of Typographic Style* (2004); Spencer Mortensen's modular scale theory. + +A modular scale generates harmonious font sizes from a base size and a ratio. The ratio determines hierarchical contrast — how dramatically headings differ from body text. + +``` +font_size(step) = base_size × ratio^step +``` + +**Ratios and their emotional profiles:** + +| Ratio | Musical Name | Hierarchy Contrast | Emotional Profile | Best For | +|---|---|---|---|---| +| 1.067 | Minor Second | Minimal | Subtle, dense, bureaucratic | Dense data UIs | +| 1.125 | Major Second | Low | Reserved, professional, understated | Text-heavy apps, documentation | +| 1.200 | Minor Third | Moderate | Balanced, readable, workmanlike | Technical content, body-heavy sites | +| 1.250 | Major Third | Moderate+ | Confident, clear, structured | Docs + marketing hybrid | +| 1.333 | Perfect Fourth | Strong | Authoritative, editorial, dramatic | Magazine layouts, editorial sites | +| 1.414 | Augmented Fourth | Strong+ | Assertive, dynamic, high-contrast | Presentation slides, hero sections | +| 1.500 | Perfect Fifth | High | Bold, declarative, commanding | Marketing landing pages | +| 1.618 | Golden Ratio | Maximum | Monumental, dramatic, display | Display-only typography | + +### Scale Ratio × Brand Congruence + +The type scale ratio should align with the brand's target PAD profile: + +| Target Profile | Congruent Scale | Rationale | +|---|---|---| +| Calm, trust (low A, low D) | 1.125–1.200 | Minimal contrast = low arousal, low dominance | +| Balanced, professional (mid all) | 1.200–1.250 | Moderate contrast = balanced PAD | +| Bold, authoritative (high D) | 1.333–1.500 | Strong contrast = high dominance | +| Playful, energetic (high A, high P) | 1.250–1.333 | Moderate-strong contrast with curvature | + +### Font Size Output + +Given base_size and ratio, generate the scale: + +``` +for step in [-2, -1, 0, 1, 2, 3, 4, 5]: + raw = base_size × ratio^step + snapped = round(raw) // integer pixels for sub-pixel avoidance + line_height = ceil(snapped × lh_ratio / base_unit) × base_unit // grid snap +``` + +**Token naming convention:** + +| Step | Token | Role | +|---|---|---| +| -2 | `--text-xs` | Fine print, legal, captions | +| -1 | `--text-sm` | Secondary text, metadata | +| 0 | `--text-base` | Body text (base_size) | +| 1 | `--text-lg` | Lead paragraphs, emphasized body | +| 2 | `--text-xl` | h4, subheadings | +| 3 | `--text-2xl` | h3, section headings | +| 4 | `--text-3xl` | h2, major headings | +| 5 | `--text-4xl` | h1, page titles | + +### Layout Proportions + +**Source:** Interaction Design Foundation, Golden Ratio in design; Kurosu & Kashimura (1995), "Apparent usability vs. inherent usability." *CHI '95*. + +The golden ratio (φ = 1.618) and related proportions guide layout division: + +``` +content_width / sidebar_width ≈ φ (1.618:1 or ~62%:38%) +``` + +**The aesthetic-usability effect:** Tractinsky, Katz, & Ikar (2000) replicated Kurosu & Kashimura's finding across cultures — proportionally harmonious layouts are rated as more usable even when functionality is identical. Φ-proportioned layouts benefit from this effect. + +**Layout proportion tokens:** + +| Ratio | Division | Use | +|---|---|---| +| 1:1 | 50%:50% | Equal-weight comparison layouts | +| φ:1 (1.618:1) | ~62%:38% | Content + sidebar (golden section) | +| 2:1 | ~67%:33% | Content + narrow sidebar | +| 3:1 | 75%:25% | Content-dominant with metadata column | + +### Symmetry Strategy + +**Source:** Frontiers in Psychology (2016). Comparative eye-tracking study on symmetric pattern perception. Symmetric designs increase fixation duration (longer engagement) and score higher on pleasantness. Asymmetric layouts increase arousal (dynamic tension). + +| Target Arousal | Layout Strategy | Effect | +|---|---|---| +| Low (calm, orderly) | Symmetric (centered, equal columns) | High pleasure, low arousal | +| Medium (balanced) | Mostly symmetric with focal asymmetry | Moderate arousal, focused attention | +| High (dynamic, energetic) | Intentional asymmetry (off-center, unequal columns) | High arousal, tension | + +**Default:** Symmetric grid. Use asymmetry only for intentional emphasis — e.g., off-center hero text, asymmetric content:sidebar ratios, staggered card layouts. + +## Target Size & Interactive Geometry + +### Fitts' Law + +**Source:** Fitts, P. M. (1954). "The information capacity of the human motor system in controlling the amplitude of movement." *Journal of Experimental Psychology*, 47(6), 381–391. + +``` +MT = a + b × log₂(2D/W + 1) + +MT = movement time (ms) +D = distance from current pointer position to target center +W = target width along movement axis +a = intercept constant (device-dependent, ~50ms for mouse) +b = slope constant (device-dependent, ~150ms for mouse) +``` + +The logarithmic relationship means: doubling target size yields diminishing returns. The largest gains come from making small targets bigger. Once targets exceed ~48px, further size increases provide minimal speed improvement. + +### Minimum Target Sizes (Converged Standards) + +| Standard | Minimum Size | Physical Size | Context | +|---|---|---|---| +| WCAG 2.2 SC 2.5.8 (AA) | 24 × 24 CSS px | ~6.4mm | Absolute minimum for compliance | +| WCAG 2.1 SC 2.5.5 (AAA) | 44 × 44 CSS px | ~11.7mm | Recommended for accessible interfaces | +| Apple iOS HIG | 44 × 44 pt (~59px) | ~11.7mm | Apple ecosystem | +| Android Material | 48 × 48 dp | ~9mm (finger pad) | Android ecosystem | +| NNG recommendation | — | 10 × 10mm | Universal physical size | +| Apple Vision Pro | 60 × 60 pt (~80px) | — | Spatial computing (eye-tracking) | + +**Minimum spacing between adjacent targets:** 8px (Android guideline). This prevents adjacent-tap errors and provides visual separation per Gestalt proximity. + +### Size Hierarchy Rule + +Interactive elements should have a size hierarchy that matches their importance: + +``` +primary_action_height ≥ secondary_action_height × 1.25 +secondary_action_height ≥ tertiary_action_height × 1.15 +``` + +This creates a natural visual weight hierarchy driven by Fitts' law (larger = faster to acquire = more important) without relying on color differentiation. + +**Token output:** + +| Token | Height | Padding (horizontal) | Use | +|---|---|---|---| +| `--target-sm` | 32px | step 2 (16px) | Tertiary actions, inline buttons, tags | +| `--target-md` | 40px | step 3 (24px) | Secondary actions, form inputs | +| `--target-lg` | 48px | step 4 (32px) | Primary actions, main CTAs | +| `--target-xl` | 56px | step 5 (48px) | Hero CTAs, prominent actions | + +### Content Width & Line Length + +**Source:** Bringhurst (2004); WCAG SC 1.4.8. See also COLOR_GUIDE.md Phase 3 § Typography Layout and Color Fatigue. + +``` +Optimal line length: 45–75 characters (66 ideal for Latin script) +Implementation: max-width: 66ch on text containers +CJK: max-width: 40ch (WCAG SC 1.4.8) +``` + +The `ch` unit is relative to the width of the "0" glyph in the current font. This automatically adapts line length to font choice. + +**Color fatigue interaction (from COLOR_GUIDE.md):** High contrast + long lines (>75ch) increases saccade fatigue. If line length exceeds 75ch and cannot be reduced, reduce contrast target from |Lc| 90 to |Lc| 80 for body text. + +## Congruence Validation + +### Three-Axis Alignment Check + +Every design decision should be congruent across all three perceptual axes: color, typography, and spatial geometry. The agent should validate alignment after generating all tokens: + +``` +For each UI component: + color_PAD = PAD vector from color properties (hue, chroma, lightness) + type_PAD = PAD vector from typography properties (weight, classification, size) + spatial_PAD = PAD vector from spatial properties (radius, spacing, border weight, size) + + congruence_check: + |color_PAD.pleasure − spatial_PAD.pleasure| ≤ 1 tier // e.g., both "medium" or adjacent + |type_PAD.dominance − spatial_PAD.dominance| ≤ 1 tier + |color_PAD.arousal − spatial_PAD.arousal| ≤ 1 tier +``` + +### Congruence Examples + +| Target Emotion | Color | Typography | Spatial | Congruent? | +|---|---|---|---|---| +| Calm Trust | Low chroma, cool hue | 400 weight, humanist sans | 8–12px radius, generous spacing, 1px borders | Yes — all low A, low D, high P | +| Calm Trust | Low chroma, cool hue | 400 weight, humanist sans | 0px radius, tight spacing, 3px borders | **No** — spatial signals high D, high A | +| Urgent Authority | High chroma, warm hue | 700 weight, geometric sans | 0–4px radius, tight spacing, 2–3px borders | Yes — all high A, high D | +| Urgent Authority | High chroma, warm hue | 700 weight, geometric sans | 16px radius, generous spacing, 0px borders | **No** — spatial signals high P, low D | +| Friendly Innovation | Med chroma, cyan/green | 400–500, humanist sans | 12–16px radius, moderate spacing, 1px | Yes — balanced A, high P | +| Technical Precision | Low chroma, neutral | 400, monospace + geometric | 4–6px radius, grid-even spacing, 1px | Yes — low A, med D, med P | +| Premium Elegance | Low chroma, deep hue | 300 weight, didone serif | 0–2px radius, very generous spacing, 0–1px | Yes — low A, low D, high P | + +### Incongruence Red Flags + +Flag and report these mismatches to the human operator: + +- High curvature (pill buttons) + heavy font weight (800+) → spatial says "warm," type says "commanding" +- Generous whitespace + high chroma accent overuse → spatial says "calm," color says "urgent" +- Angular geometry (0px radius) + light font weight (300) → spatial says "institutional," type says "delicate" +- Tight spacing + low information density → arousal signal without content to justify it (wasted tension) +- Multiple surface tiers + minimal content per tier → dominance signal without hierarchy to communicate + +## Validation Checklist + +For every generated spatial system, verify: + +**Spacing:** +- [ ] Spacing scale generated with all steps as integer multiples of base unit +- [ ] All spacing values are whole pixels (no sub-pixel rendering) +- [ ] Within-group spacing < between-group spacing × 0.5 (Gestalt proximity threshold) +- [ ] Component padding and section gaps assigned from appropriate scale steps +- [ ] Vertical rhythm: all line-heights snap to base unit multiples + +**Border radius:** +- [ ] Radius scale generated from brand pleasure target +- [ ] Semantic role adjustments applied (error sharper, success rounder) +- [ ] No radius exceeds min(width, height) / 2 for any element +- [ ] Radius tokens named and mapped to component roles + +**Line weight:** +- [ ] Border weight range matches brand dominance target +- [ ] Border weight congruent with font weight selection (±1 dominance tier) +- [ ] Border density ≤ 4 structural lines per viewport section +- [ ] All borders have sufficient contrast: perceived_weight > 0.03 +- [ ] Border color tokens paired with thickness tokens + +**Surfaces:** +- [ ] Luminance step between adjacent tiers ≥ Weber fraction minimum (ΔL ≥ 0.03 light, ≥ 0.01 dark) +- [ ] Surface distribution follows 60-30-10 (or 95-5 for content pages) +- [ ] Dark mode surface values computed (not copied from light mode) +- [ ] No pure black (#000000) backgrounds; no pure white (#ffffff) body text on dark + +**Proportions:** +- [ ] Type scale ratio selected and congruent with brand PAD profile +- [ ] Font sizes generated, rounded to integers, with grid-snapped line-heights +- [ ] Layout proportions defined (content:sidebar ratios) +- [ ] Content containers have max-width set (66ch Latin, 40ch CJK) + +**Target sizes:** +- [ ] All interactive elements ≥ 24×24 CSS px (WCAG 2.2 AA minimum) +- [ ] Primary actions ≥ 44×44 CSS px (WCAG AAA target) +- [ ] Adjacent interactive elements have ≥ 8px gap +- [ ] Size hierarchy: primary > secondary × 1.25 + +**Congruence:** +- [ ] Spatial PAD vector within ±1 tier of color PAD vector on all dimensions +- [ ] Spatial PAD vector within ±1 tier of typography PAD vector on all dimensions +- [ ] No red flags from incongruence check +- [ ] Semantic role overrides documented and justified + +--- + +## References + +### Shape & Curvature Perception +- **Bar, M., & Neta, M.** — "Humans prefer curved visual objects" (*Psychological Science*, 2006, 17(8), 645–648). Curved stimuli preferred even at 84ms subliminal exposure. +- **Bar, M., & Neta, M.** — "Visual elements of subjective preference modulate amygdala activation" (*Neuropsychologia*, 2007, 45(10), 2191–2200). fMRI: sharp contours activate amygdala bilaterally; curved do not. +- **Gómez-Puerto, G., Munar, E., & Nadal, M.** — "Preference for curvature: A historical and conceptual framework" (*Frontiers in Human Neuroscience*, 2015, 9:712). Comprehensive review of curvature preference across ages, cultures, species. +- **Leder, H., Tinio, P. P., & Bar, M.** — "Emotional valence modulates the preference for curved objects" (*Perception*, 2011, 40(6), 649–655). Curvature preference only significant for positive/neutral valence objects. +- **Silvia, P. J., & Barona, C. M.** — "Do people prefer curved objects? Angularity, expertise, and aesthetic preference" (*Empirical Studies of the Arts*, 2009, 27(1), 25–42). Expertise moderates curvature preference. +- **Bertamini, M., Palumbo, L., Gheorghes, T. N., & Galatsidas, M.** — "Do observers like curvature or do they dislike angularity?" (*British Journal of Psychology*, 2015). Approach/avoidance behavioral measurement of curvature preference. +- **Fantz, R. L., & Miranda, S. B.** — "Newborn infant attention to form of contour" (*Child Development*, 1975). Neonates fixate longer on curved contours. +- **Lundholm, H.** — "The affective tone of lines" (*Psychological Review*, 1921). Curved = gentle/quiet; angular = agitating/hard/furious. +- **Aronoff, J., Barclay, A. M., & Stevenson, L. A.** — "The recognition of threatening facial stimuli" (*Journal of Personality and Social Psychology*, 1992). V-shapes → anger association. +- **Larson, C. L., Aronoff, J., & Stearns, J. J.** — "The shape of threat: Simple geometric forms evoke rapid and sustained capture of attention" (*Emotion*, 2007). Angular → anger; rounded → happiness. +- **Uher, J.** — (1991). On zigzag lines: antagonistic adjectives. On wavy lines: affiliative adjectives. +- **UXPA Journal** — "Rounded aesthetic beauty and warmth" (2024). N=187, between-subjects. Rounded corners → higher aesthetics, warmth, ease of use, satisfaction. +- **Ohio State University** — "Curves or angles: Shapes in businesses affect customer response" (news.osu.edu). Context-dependent shape preference: angular = competence in crowded settings; curved = warmth in uncrowded settings. +- **Frontiers in Psychology** — "The matching effect of consumer power state and shape preference" (2021). High-power consumers prefer angular; low-power prefer rounded. + +### Spacing, Grid & Proximity +- **Kubovy, M., & van den Berg, M.** — "The whole is equal to the sum of its parts: A probabilistic model of grouping by proximity and similarity in regular patterns" (*Psychological Review*, 2008). Mathematical model of proximity-based grouping. +- **Springer** — "Objective measurement of Gestalts using Tilt Aftereffect" (*Behavior Research Methods*, 2017). Proximity produces larger TAE (stronger grouping) than color similarity. +- **Wagemans, J., et al.** — "A century of Gestalt psychology in visual perception" (*Psychological Bulletin*, 2012, 138(6), 1172–1217). Comprehensive review of Gestalt principles. +- **Bringhurst, R.** — *The Elements of Typographic Style* (Hartley & Marks, 2004). Modular scales, vertical rhythm, line length (45–75 characters). +- **Gamma UX** — "Types of grids: The evolution toward the 4-point grid system" (2023). 4pt and 8pt grid systems, baseline alignment. +- **Mapletree Studio** — "The Psychology Behind Clean Website Design" (2024). Whitespace increases comprehension up to 20%. +- **Parallel HQ** — "Improving Visual Hierarchy" (2026). Grid-based alignment improves usability ratings. +- **MASTERCAWEB** — "Minimalism versus density in UI and UX" (Université de Strasbourg). Cultural factors: density signals capability in East Asian markets; minimalism signals premium in Western markets. +- **Nielsen Norman Group** — "4 Principles to Reduce Cognitive Load" (2024). Single-column layouts outperform multi-column for form completion. + +### Proportion & Visual Balance +- **Kurosu, M., & Kashimura, K.** — "Apparent usability vs. inherent usability" (*CHI '95 Conference Companion*, 1995). Aesthetically proportioned interfaces rated more usable. +- **Tractinsky, N., Katz, A. S., & Ikar, D.** — "What is beautiful is usable" (*Interacting with Computers*, 2000, 13(2), 127–145). Cross-cultural replication of aesthetic-usability effect. +- **Itten, J.** — *The Art of Color* (Reinhold Publishing, 1961). 60-30-10 proportion rule. +- **Frontiers in Psychology** — "Visual perception of symmetric patterns in humans and orangutans" (2016). Eye-tracking: symmetric patterns increase fixation duration. +- **Frontiers in Human Neuroscience** — "Visual saliency and pictorial balance in photographic cropping" (2015). Saliency center-of-mass closer to geometrical center in preferred compositions. + +### Target Size & Motor Performance +- **Fitts, P. M.** — "The information capacity of the human motor system in controlling the amplitude of movement" (*Journal of Experimental Psychology*, 1954, 47(6), 381–391). Foundational motor performance model. +- **ISO 9241-411** — Ergonomics of human-system interaction: Evaluation methods for the design of physical input devices. +- **WCAG 2.2 SC 2.5.8** — Target Size (Minimum): 24 × 24 CSS pixels (AA level). +- **WCAG 2.1 SC 2.5.5** — Target Size: 44 × 44 CSS pixels (AAA level). +- **Apple Human Interface Guidelines** — 44 × 44 pt minimum for iOS; 60 × 60 pt for visionOS. +- **Android Material Design** — 48 × 48 dp minimum touch target; 8dp spacing between targets. +- **Nielsen Norman Group** — "Touch targets on touchscreens" (2019). 10 × 10mm recommended physical size. +- **Lindgaard, G., Fernandes, G., Dudek, C., & Brown, J.** — "Attention web designers: You have 50 milliseconds to make a good first impression!" (*Behaviour & Information Technology*, 2006, 25(2), 115–126). First impressions formed in ~50ms, dominated by visual design. +- **Fogg, B. J., et al.** — Stanford Web Credibility Research (2003). N=2,684: design rated more important than any other feature for credibility. + +### Visual Weight & Hierarchy +- **Fox, D., Shaikh, A. D., & Chaparro, B. S.** — "Effect of typeface appropriateness on the perception of documents" (2007). 22% credibility loss for incongruent typography. Applicable to shape-emotion congruence. +- **WordPress Core** — "Noteworthy admin CSS changes in WordPress 5.3" (make.wordpress.org, 2019). Border contrast improvements for accessibility. +- **Smashing Magazine** — "Using shadows and blur effects in UI design" (2017). Elevation via shadows for interactive hierarchy. +- **Interaction Design Foundation** — Material Design documentation. Elevation, z-axis, and shadow computation. +- **Kota.co.uk** — "The texture of trust: How visual tactility sells online" (2024). Flat vs. elevated design trust perception. + +### Color Emotion (Cross-Referenced from COLOR_GUIDE.md) +- **Wilms, L., & Oberfeld, D.** — "Color and emotion" (*Psychological Research*, 2018). Saturation: dominant arousal lever (η² = .693). +- **Elliot, A. J., & Maier, M. A.** — "Color-in-context theory" (*Advances in Experimental Social Psychology*, 2012). Context modulates color emotion. +- **Valdez, P., & Mehrabian, A.** — "Effects of color on emotions" (*J. Experimental Psychology: General*, 1994). Brightness → pleasure; saturation → arousal. + +### Cognitive Load & Information Processing +- **SHIFT eLearning** — Color schemes amplify learning by 55–78%; cognitive overload from too many bright colors degrades comprehension. +- **Nature Scientific Reports** — "Optimizing waiting experience" (2025). Quantitative study: interface element density × animated indicators affect emotional experience and time perception. diff --git a/agentic-coding-brand-proposal.html b/agentic-coding-brand-proposal.html new file mode 100644 index 0000000..32f0892 --- /dev/null +++ b/agentic-coding-brand-proposal.html @@ -0,0 +1,2626 @@ + + + + + +Agentic Coding — Monochrome Brand System + + + + + +
+

Agentic Coding — Monochrome Brand System

+

Monochrome-first identity · 9 semantic accent colors · OKLCH-computed · WCAG AA validated · Geometry as brand

+
+ + +
+

Competitor Color Landscape

+
+ +
+
+
+
GitHub Copilot
+
270°
+
+
+ +
+
+
+
Cursor
+
280°
+
+
+ +
+
+
+
Windsurf/Codeium
+
170°
+
+
+ +
+
+
+
Replit
+
30°
+
+
+ +
+
+
+
OpenAI/ChatGPT
+
160°
+
+
+ +
+
+
+
Anthropic/Claude
+
30°
+
+
+ +
+
+
+
JetBrains AI
+
280°
+
+
+ +
+
+
+
Tabnine
+
260°
+
+
+ +
+
+
+
Sourcegraph Cody
+
280°
+
+
+ +
+
+
+
Pluralsight
+
330°
+
+
+ +
+
+
+
DeepLearning.AI
+
10°
+
+
+ +
+
+
+
Frontend Masters
+
+
+
+ +
+
+
+
Agentic Coding (Ours)
+
Monochrome — No primary hue
+
+
+
+

+ Every competitor owns a hue — we own the absence of one. Monochrome IS the most distinctive position on this map. + Per Sharp's framework, when every brand shouts color, restraint is the signal. Purple (260-290°) is particularly crowded — Copilot, Cursor, JetBrains, Tabnine, Sourcegraph all share it. Rather than compete for hue territory, we vacate it entirely. +

+
+ + +
+

Semantic Color Palette (OKLCH)

+

+ One achromatic neutral scale plus 9 chromatic hues at equal standing. No "primary" or "accent" hierarchy — each hue serves a semantic role in illustrations, diagrams, and callouts. All computed via OKLCH with WCAG AA validation. +

+ +
+
Neutral — Achromatic (C:0.000)
+
+
+
50
+
#f5f5f5
+
+
+
100
+
#e8e8e8
+
+
+
200
+
#d4d4d4
+
+
+
300
+
#b7b7b7
+
+
+
400
+
#9b9b9b
+
+
+
500
+
#808080
+
+
+
600
+
#666666
+
+
+
700
+
#505050
+
+
+
800
+
#3d3d3d
+
+
+
900
+
#2b2b2b
+
+
+
950
+
#222222
+
+
+
+ +
+
Error — H:25° C:0.16 · Danger / Critical / Destructive
+
+
+
50
+
#fff2f0
+
+
+
100
+
#ffdfdc
+
+
+
200
+
#ffc3bd
+
+
+
300
+
#ff958d
+
+
+
400
+
#ec7069
+
+
+
500
+
#ce514d
+
+
+
600
+
#ad3735
+
+
+
700
+
#8d2324
+
+
+
800
+
#701719
+
+
+
900
+
#520e10
+
+
+
950
+
#410b0c
+
+
+
+ +
+
Warning — H:70° C:0.13 · Caution / Attention / Deprecation
+
+
+
50
+
#fff3e6
+
+
+
100
+
#ffe3c3
+
+
+
200
+
#fcca91
+
+
+
300
+
#e6aa63
+
+
+
400
+
#cd8c37
+
+
+
500
+
#b17000
+
+
+
600
+
#8e5900
+
+
+
700
+
#704500
+
+
+
800
+
#573400
+
+
+
900
+
#402400
+
+
+
950
+
#331b00
+
+
+
+ +
+
Lime — H:110° C:0.14 · Progress / Growth / Output
+
+
+
50
+
#f7fac9
+
+
+
100
+
#eaedb0
+
+
+
200
+
#d8da8d
+
+
+
300
+
#bcbe5c
+
+
+
400
+
#a1a22b
+
+
+
500
+
#868600
+
+
+
600
+
#6b6b00
+
+
+
700
+
#535400
+
+
+
800
+
#404000
+
+
+
900
+
#2e2e00
+
+
+
950
+
#242400
+
+
+
+ +
+
Success — H:155° C:0.14 · Active / Validated / Complete
+
+
+
50
+
#ddffe8
+
+
+
100
+
#bef8d1
+
+
+
200
+
#9fe8b8
+
+
+
300
+
#72ce95
+
+
+
400
+
#48b475
+
+
+
500
+
#1c985a
+
+
+
600
+
#007a44
+
+
+
700
+
#006034
+
+
+
800
+
#004a27
+
+
+
900
+
#00361a
+
+
+
950
+
#002a13
+
+
+
+ +
+
Cyan — H:195° C:0.145 · Interactive / System / Code
+
+
+
50
+
#d4fffe
+
+
+
100
+
#a5faf9
+
+
+
200
+
#7aeae9
+
+
+
300
+
#2ad0d0
+
+
+
400
+
#00b2b2
+
+
+
500
+
#009393
+
+
+
600
+
#007576
+
+
+
700
+
#005c5c
+
+
+
800
+
#004747
+
+
+
900
+
#003333
+
+
+
950
+
#002828
+
+
+
+ +
+
Indigo — H:250° C:0.14 · Knowledge / Reference / Data
+
+
+
50
+
#eef6ff
+
+
+
100
+
#d7eaff
+
+
+
200
+
#b4d8ff
+
+
+
300
+
#7cbdff
+
+
+
400
+
#53a0ec
+
+
+
500
+
#3284d0
+
+
+
600
+
#1369b0
+
+
+
700
+
#005190
+
+
+
800
+
#003e71
+
+
+
900
+
#002c54
+
+
+
950
+
#002242
+
+
+
+ +
+
Violet — H:285° C:0.14 · AI / Transformation / Synthesis
+
+
+
50
+
#f4f4ff
+
+
+
100
+
#e5e5ff
+
+
+
200
+
#cfcfff
+
+
+
300
+
#b0adff
+
+
+
400
+
#938eeb
+
+
+
500
+
#7971d0
+
+
+
600
+
#6057af
+
+
+
700
+
#4b4290
+
+
+
800
+
#393172
+
+
+
900
+
#282254
+
+
+
950
+
#1f1b42
+
+
+
+ +
+
Magenta — H:320° C:0.14 · AI / Creative / Highlight
+
+
+
50
+
#fcf0ff
+
+
+
100
+
#f9ddff
+
+
+
200
+
#f2bffd
+
+
+
300
+
#da9de8
+
+
+
400
+
#c07ecf
+
+
+
500
+
#a462b4
+
+
+
600
+
#874895
+
+
+
700
+
#6d3579
+
+
+
800
+
#55265f
+
+
+
900
+
#3d1a45
+
+
+
950
+
#301436
+
+
+
+ +
+
Rose — H:355° C:0.13 · Emphasis / Priority / Human Actor
+
+
+
50
+
#fff1f6
+
+
+
100
+
#ffdde9
+
+
+
200
+
#ffbfd7
+
+
+
300
+
#f198bb
+
+
+
400
+
#d7799f
+
+
+
500
+
#bb5c84
+
+
+
600
+
#9b436a
+
+
+
700
+
#7e3053
+
+
+
800
+
#632240
+
+
+
900
+
#48172d
+
+
+
950
+
#391223
+
+
+
+ +

Hue Wheel Summary

+
+
+
+
Error
25° — Danger
+
+
+
+
Warning
70° — Caution
+
+
+
+
Lime
110° — Progress
+
+
+
+
Success
155° — Validation
+
+
+
+
Cyan
195° — System
+
+
+
+
Indigo
250° — Reference
+
+
+
+
Violet
285° — Synthesis
+
+
+
+
Magenta
320° — Creative
+
+
+
+
Rose
355° — Emphasis
+
+
+

+ Shade 600 = light mode semantic · Shade 400 = dark mode semantic · + Min pairwise distance: 30° (Rose↔Error) · Avg: 41° +

+
+ + +
+

Brand Identity Through Restraint

+

+ Most brands differentiate by choosing a color. We differentiate by choosing not to. The brand IS the geometry: the agent-loop logo mark, Space Grotesk + Inter + Monaspace Neon typography, disciplined whitespace, and flat construction. Color serves a single purpose: semantic meaning. +

+ + +
+
The Monochrome Principle in Practice
+
+
+
achromatic surface
+
Neutral container
+
+
+
color = semantic label
+
+ MODULE 1 — Hue carries meaning +
+
+
+
typographic interaction
+
Start Learning
+
Dark fill, no hue
+
+
+
+ + +
+
+
1
+
Achromatic Base
+
Surfaces, borders, text are pure gray (chroma 0). No tinted neutrals.
+
+
+
2
+
No Privileged Hue
+
All 9 chromatic colors have equal standing. None is "the brand color."
+
+
+
3
+
Color = Meaning
+
Color appears only in semantic callouts, diagrams, status indicators, and data viz. Every color use must answer: "what does this hue mean here?"
+
+
+
4
+
Typographic Interaction
+
Links use underlines + weight, buttons use dark/light fills. Interactive elements identified by typography and shape, not color.
+
+
+
5
+
Geometry as Brand
+
The logo mark, monospace type, whitespace rhythm, and flat construction are the Distinctive Brand Assets.
+
+
+
6
+
60-30-10 Color Budget
+
60% achromatic surfaces, 30% elevated gray, 10% semantic color. In practice most pages will be closer to 95/5.
+
+
+
+ + +
+

Brand Identity Assets

+

+ These are the Distinctive Brand Assets per Romaniuk's framework. They are geometry and typography, not color. +

+ +
+ +
+
</>
+
On white
+
+ +
+
</>
+
On dark
+
+ +
+
+ / +
+
Favicon 16px
+
+ +
+
</> Agentic Coding
+
Social card
+
+
+ + +
+
+
DISPLAY
+
Space Grotesk
+
The quick brown fox jumps over the lazy dog. ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789
+
+
+
BODY
+
Inter
+
The quick brown fox jumps over the lazy dog. ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789
+
+
+
CODE
+
Monaspace Neon
+
const agent = new Agent(); // 0O 1lI {[()]}
+
+
+ + +
+

Favicon Strategy

+
+
• SVG favicon with embedded @media (prefers-color-scheme: dark) for light/dark adaptation
+
• Pure monochrome mark — dark fill on light tab, light fill on dark tab
+
.ico fallback at 32×32
+
apple-touch-icon.png at 180×180 with solid background
+
+
+ +

+ Design action item: If the current logo geometry doesn't read at 16px in pure monochrome, the geometry needs simplification. Test and iterate. +

+
+ + +
+

Design System Elements

+
+
+

Buttons

+
+
Primary
+
Outline
+
Ghost Link
+
+
Dark fill, dark border, or underline — no color
+
+
+

9 Semantic Badges

+
+ Error + Warning + Lime + Success + Cyan + Indigo + Violet + Magenta + Rose +
+
All 9 hues at equal standing
+
+
+

Cards

+
+
Module 1: Fundamentals
+
LLM internals, context windows, and hallucination management.
+
+
+ TIP — Always verify agent output against source. +
+
Semantic color on callout border + label only
+
+
+

Inputs & Fields

+ + + + +
Focus → darker border (contrast shift, not color)
+
+
+

Neutral Interaction

+
+
Lesson 3: Grounding
+
File paths, line numbers, root cause analysis
+
Hover to see shift →
+
+
Neutral at rest. Border lightens on hover/focus/active.
+
+
+
+ + +
+

Homepage — Light & Dark Mode

+
+ +
+
+
+
+
+
agenticoding.ai
+
+
+ +
+
+ Open Source · MIT · 2.1k +
+

Master Agentic Coding

+

Structured methodology for enterprise codebases

+
+
Start Learning
+
Browse Prompts
+
+
+ + +
+
+
+
+
+
methodology.md
+
+
+ INVESTIGATE: Trace the code path
+ ANALYZE: Compare expected vs actual
+ EXPLAIN: File paths, line numbers, root cause + From the Prompt Library → +
+
+ +
+

What You'll Learn

+
+
+
Module 1
+

Fundamentals

+
    +
  • LLM internals & context
  • +
  • Hallucinations & drift
  • +
  • RAG integration
  • +
+
+
+
Module 2
+

Methodology

+
    +
  • Prompt structure
  • +
  • Grounding patterns
  • +
  • Plan → Execute → Verify
  • +
+
+
+
Module 3
+

Practical Techniques

+
    +
  • CI integration
  • +
  • Test generation
  • +
  • Debugging sessions
  • +
+
+
+
+
+

Learn Your Way

+
+
+ +

Reference Docs

+

Bookmark it. Jump back in.

+
+
+ +

Podcasts

+

Commute, gym, walking the dog.

+
+
+ +

Presentations

+

Share with your team.

+
+
+
+
+
+ + +
+
+
+
+
+
agenticoding.ai (dark)
+
+
+ +
+
+ Open Source · MIT · 2.1k +
+

Master Agentic Coding

+

Structured methodology for enterprise codebases

+
+
Start Learning
+
Browse Prompts
+
+
+ + +
+
+
+
+
+
methodology.md
+
+
+ INVESTIGATE: Trace the code path
+ ANALYZE: Compare expected vs actual
+ EXPLAIN: File paths, line numbers, root cause + From the Prompt Library → +
+
+ +
+

What You'll Learn

+
+
+
Module 1
+

Fundamentals

+
    +
  • LLM internals & context
  • +
  • Hallucinations & drift
  • +
  • RAG integration
  • +
+
+
+
Module 2
+

Methodology

+
    +
  • Prompt structure
  • +
  • Grounding patterns
  • +
  • Plan → Execute → Verify
  • +
+
+
+
Module 3
+

Practical Techniques

+
    +
  • CI integration
  • +
  • Test generation
  • +
  • Debugging sessions
  • +
+
+
+
+
+

Learn Your Way

+
+
+ +

Reference Docs

+

Bookmark it. Jump back in.

+
+
+ +

Podcasts

+

Commute, gym, walking the dog.

+
+
+ +

Presentations

+

Share with your team.

+
+
+
+
+
+
+
+ + +
+

Social Card & OG Image

+

+ Template: dark background (#111111), large white title, small monochrome logo mark, optional single semantic accent line chosen per content context. Variety without brand-color dependency. +

+
+ +
+
+
</> Agentic Coding
+
Lesson 5: Grounding & Validation
+
success accent — validated content
+
+ +
+
+
</> Agentic Coding
+
Lesson 9: AI Agent Patterns
+
violet accent — AI/transformation
+
+ +
+
+
</> Agentic Coding
+
Master Agentic Coding
+
neutral — default/homepage
+
+
+
+ + +
+

Illustration Examples — 9-Color System

+

+ The illustration system demonstrates monochrome-first at full capacity. These diagrams use all 9 semantic hues equally. No single color dominates. The achromatic base makes every accent equally prominent. +

+ +
+ + +
+

Architecture Diagram — 9 Entity Types

+
+ +
+
+
User
rose — human actor
+
+
+
API Gateway
indigo — reference
+
+
+
Auth
violet — transform
+
+
+ +
↓            ↓            ↓
+ +
+
+
LLM Agent
magenta — AI
+
+
+
Cache
lime — intermediate
+
+
+
Database
cyan — system
+
+
+ +
↓            ↓            ↓
+ +
+
+
Queue
success — active
+
+
+
Monitor
warning — caution
+
+
+
Error Handler
error — critical
+
+
+
+
+ + +
+

Data Visualization — 5-Series Bar Chart (dark mode)

+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
Indigo
+
Lime
+
Magenta
+
Violet
+
Rose
+
+
+ + +
+

Agent Pipeline — Multi-Phase Flow

+
+ +
+
1
+
User Input
+
rose
+
+
+ +
+
2
+
Context Retrieval
+
indigo
+
+
+ +
+
3
+
LLM Processing
+
magenta
+
+
+ +
+
4
+
Transformation
+
violet
+
+
+ +
+
5
+
Code Generation
+
cyan
+
+
+ +
+
6
+
Validation & Output
+
success
+
+
+
+ + +
+

Full 9-Color Badge System

+ + +
+
dark mode — solid fill
+
+ Error + Warning + Lime + Success + Cyan + Indigo + Violet + Magenta + Rose +
+
+ + +
+
light mode — tinted bg + colored text
+
+ Error + Warning + Lime + Success + Cyan + Indigo + Violet + Magenta + Rose +
+
+ + +
+
semantic application
+
+ Module 1 + AI Agent + Reference + Transform + User Input + In Progress + Complete + Caution + Breaking +
+
+
+
+ + +
+

Light Mode — Spec-to-Code Convergence Diagram

+
+ +
+
SPEC
+
Requirements
+
+ +
+
Generate
+
+
+
+ +
+
CODE
+
Implementation
+
+ +
+
Extract
+
+
+
+ +
+
TEST
+
Validation
+
+ +
+
Verify
+
+
+
+ +
+
PASS
+
Convergence
+
+
+ +
+
Spec (violet)
+
Generate (magenta)
+
Code (cyan)
+
Extract (success)
+
Test (indigo)
+
Verify (warning)
+
+
+ + +
+

Dark Mode — Knowledge Map (Concept Relationships)

+
+ +
+
Context Window
+
Token limits, attention
+
+
+
Transformer
+
Self-attention, layers
+
+
+
Prompt Engineering
+
System prompts, few-shot
+
+ +
+
RAG Pipeline
+
Retrieval, embeddings
+
+
+
Grounding
+
File paths, line numbers
+
+
+
Iteration Cycle
+
Plan → execute → verify
+
+ +
+
Developer Intent
+
Requirements, goals
+
+
+
Hallucination Risk
+
Drift, confabulation
+
+
+
Failure Modes
+
Context overflow, loops
+
+
+ +
+
↕ concepts connect by semantic color — same category = same hue family
+
+
+
+ + +
+

Semantic Token Mapping

+
+
+

Light Theme

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
TokenValueSource
Surfaces
--surface-page#ffffffwhite
--surface-raised#f5f5f5neutral-50
--surface-muted#e8e8e8neutral-100
Text
--text-heading#2b2b2bneutral-900
--text-body#505050neutral-700
--text-muted#808080neutral-500
Borders
--border-default#d4d4d4neutral-200
Semantic Colors
--visual-error#ad3735error-600
--visual-warning#8e5900warning-600
--visual-lime#6b6b00lime-600
--visual-success#007a44success-600
--visual-cyan#007576cyan-600
--visual-indigo#1369b0indigo-600
--visual-violet#6057afviolet-600
--visual-magenta#874895magenta-600
--visual-rose#9b436arose-600
+
+
+

Dark Theme

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
TokenValueSource
Surfaces
--surface-page#0d1117GitHub dark
--surface-raised#161b22elevated
--surface-muted#3d3d3dneutral-800
Text
--text-heading#e8e8e8neutral-100
--text-body#d4d4d4neutral-200
--text-muted#9b9b9bneutral-400
Borders
--border-default#505050neutral-700
Semantic Colors
--visual-error#ec7069error-400
--visual-warning#cd8c37warning-400
--visual-lime#a1a22blime-400
--visual-success#48b475success-400
--visual-cyan#00b2b2cyan-400
--visual-indigo#53a0ecindigo-400
--visual-violet#938eebviolet-400
--visual-magenta#c07ecfmagenta-400
--visual-rose#d7799frose-400
+
+
+
+ + +
+

Spatial System

+

+ Computed spatial token system for the Agentic Coding brand. All values derived programmatically from the brand PAD profile: + Pleasure: Medium, + Arousal: Low-Medium, + Dominance: Medium. + Base unit: 8px. Type scale ratio: 1.200 (Minor Third). Surface strategy: Flat 2.0. +

+ + +

Spacing Scale

+

Base unit: 8px. Hybrid geometric progression — compresses at small values, expands at large values.

+ + + + + + + + + + + + + + + + + +
TokenStepMultiplierValue
--space-0000px
--space-1118px
--space-22216px
--space-33324px
--space-44432px
--space-55648px
--space-66864px
--space-771080px
--space-881296px
--space-9916128px
--space-101020160px
+ +

Spacing Assignment (Low-Medium Arousal)

+ + + + + + + + + +
PurposeStep RangeValue Range
Component paddingstep 2–316–24px
Section gapstep 5–648–64px
Page marginstep 7–880–96px
+ + +

Type Scale

+

Base: 16px, ratio: 1.200 (Minor Third). All sizes rounded to integers. Line-heights grid-snapped to 8px multiples.

+ + + + + + + + + + + + + + +
TokenStepRoleSizeLH RatioLine-Height
--text-xs-2Fine print, captions11px1.5024px
--text-sm-1Secondary, metadata13px1.5024px
--text-base0Body text16px1.5024px
--text-lg1Lead paragraphs19px1.3032px
--text-xl2h4 subheadings23px1.3032px
--text-2xl3h3 section headings28px1.1540px
--text-3xl4h2 major headings33px1.1540px
--text-4xl5h1 page titles40px1.1548px
+ + +

Vertical Rhythm

+

Formula: ceil(fontSize * lhRatio / 8) * 8. All line-heights snap to the 8px grid.

+ + + + + + + + + + + + + + + +
ContextFont SizeRatioRawSnapped
Body16px1.5024.0px24px
Body (small)14px1.5021.0px24px
Heading (large)28px1.1532.2px40px
Heading (medium)23px1.1526.5px32px
Heading (small)19px1.1521.9px24px
Code16px1.4022.4px24px
Code (small)14px1.4019.6px24px
Caption14px1.3518.9px24px
Caption (small)12px1.3516.2px24px
+ + +

Border Radius

+

Medium pleasure: multiplier 1.0, base_r = 8px. Curvature ratio 0.10–0.20 (professional, approachable).

+
+
+

Token Scale

+ + + + + + + + + + + + + +
TokenFormulaValue
--radius-none00px
--radius-smbase_r × 0.54px
--radius-mdbase_r8px
--radius-lgbase_r × 1.512px
--radius-xlbase_r × 216px
--radius-2xlbase_r × 324px
--radius-full9999px9999px
+
+
+

Semantic Adjustments (40px ref)

+ + + + + + + + + + + + +
RoleAdjustmentRatioRadius
Error/danger-50%0.0752px
Warning-25%0.1122px
Success+25%0.1884px
Neutral/info0%0.1503px
Avatarcircle1.00050%
Input fields-25%0.1122px
+
+
+ + +

Line Weight System

+

Medium dominance: 1–2px default borders, 3px for semantic accents. Perceived weight = thickness × ΔL.

+ + + + + + + + + + + +
TokenThicknessLight ColorΔL (light)PBW (light)Dark ColorΔL (dark)PBW (dark)
--border-subtle1px#d4d4d4 0.3420.342#3d3d3d 0.0410.041
--border-default1px#b7b7b7 0.5270.527#505050 0.0750.075
--border-emphasis1px#808080 0.7840.784#808080 0.2100.210
--border-strong2px#505050 0.9201.840#b7b7b7 0.4680.936
--border-accent3pxsemantic colorvariablesemantic colorvariable
+ + +

Surface Hierarchy

+

Flat 2.0 with medium dominance. Depth via luminance stepping only (no shadows/gradients). Distribution: 95/5 for content pages, 60-30-10 for diagram-heavy pages.

+
+
+

Light Mode

+ + + + + + + + + +
TierHexLuminanceΔL
Page#ffffff 1.0000
Raised#f5f5f5 0.91310.087
Muted#e8e8e8 0.80700.106
+
+
+

Dark Mode

+ + + + + + + + + +
TierHexLuminanceΔL
Page#0d1117 0.0055
Raised#161b22 0.01070.005
Muted#3d3d3d 0.04670.036
+
+
+
+
Weber Fraction Compliance
+
+
Light page→raised: ΔL = 0.087 (min 0.03) PASS
+
Dark page→raised: ΔL = 0.005 (min 0.01) WARN
+
Light raised→muted: ΔL = 0.106 (min 0.03) PASS
+
Dark raised→muted: ΔL = 0.036 (min 0.01) PASS
+
+
Note: Dark page→raised ΔL (0.005) is below the 0.01 Weber minimum. The #0d1117/#161b22 pair relies on the tinted blue channel of #161b22 for perceptual separation, which sRGB luminance does not fully capture. Functionally acceptable but at the threshold.
+
+ + +

Target Sizes

+

Interactive element sizing per Fitts' law and WCAG 2.2. Size hierarchy: primary ≥ secondary × 1.25.

+ + + + + + + + + + +
TokenHeightH-PaddingWCAG AA (≥24px)WCAG AAA (≥44px)Use
--target-sm32px16pxPASSTertiary actions, inline buttons, tags
--target-md40px24pxPASSSecondary actions, form inputs
--target-lg48px32pxPASSPASSPrimary actions, main CTAs
--target-xl56px48pxPASSPASSHero CTAs, prominent actions
+
+ Hierarchy: + primary(48) / secondary(40) = 1.20 + WARN < 1.25 + + secondary(40) / tertiary(32) = 1.25 + PASS ≥ 1.15 +
+ + +

Gestalt Proximity Validation

+ + + + + + + + + + + +
RelationshipStepValueRatio to Within-GroupResult
Tightly related18px
Related (within-group)216px1.0×
Loosely related432px2.0×
Between-group548px3.0×0.333 < 0.5 PASS
Unrelated (between sections)664px4.0×0.250 < 0.5 PASS
+ + +

Congruence Validation

+

Three-axis PAD alignment check: spatial vs. color vs. typography. Tolerance: ±1 tier.

+
+
+
Spatial PAD
+
+ P: 2.0 (Medium)
+ A: 1.5 (Low-Medium)
+ D: 2.0 (Medium) +
+
+
+
Color PAD
+
+ P: 2.0 (Medium)
+ A: 1.0 (Low)
+ D: 2.0 (Medium) +
+
+
+
Typography PAD
+
+ P: 2.0 (Medium)
+ A: 1.5 (Low-Medium)
+ D: 2.0 (Medium) +
+
+
+ + + + + + + + + + + + + + +
CheckAxisSpatialComparedΔResult
Spatial vs Color
Pleasure2.02.00.0PASS
Arousal1.51.00.5PASS
Dominance2.02.00.0PASS
Spatial vs Typography
Pleasure2.02.00.0PASS
Arousal1.51.50.0PASS
Dominance2.02.00.0PASS
+
+ All congruence checks pass. + Spatial tokens align with the monochrome-first achromatic color system and the Inter/Space Grotesk geometric typography stack across all three PAD axes. No incongruence flags. +
+
+ + +
+

Design Rationale

+
+
+

Why Monochrome-First?

+
    +
  • Industry precedent: Vercel, Linear, Stripe, Notion — monochrome-first is the premium signal in dev tools. It communicates craft and confidence.
  • +
  • Sharp's distinctiveness: Every competitor claimed a hue. Achromatic is the unoccupied position. Maximum uniqueness by occupying the space no one else wants.
  • +
  • Color emotion science: Per Wilms & Oberfeld 2018, saturation drives arousal (η²=.693). High-chroma accents against achromatic backgrounds create maximum perceptual contrast. Pure gray amplifies every hue equally.
  • +
  • Cognitive load: Per SHIFT eLearning, cognitive overload from color is a real risk in educational content. Monochrome base prevents it by design.
  • +
+
+
+

Why Typographic Interaction?

+
    +
  • Color reserved for meaning: If links and buttons consume a brand color, that color can no longer serve as a pure semantic signal. Typographic interaction frees all 9 hues for content meaning.
  • +
  • Universal affordance: Underlines and dark fills are universally recognized interactive patterns. They work without color vision. No accessibility workaround needed.
  • +
  • Precedent: Vercel and Linear use minimal color for interactive chrome. Dark fill buttons, underlined links. The interaction pattern is shape and weight, not hue.
  • +
  • Simplicity: One fewer decision per component — no "which color should this button be?" Every interactive element follows the same neutral rule.
  • +
+
+
+

Why 9 Semantic Hues?

+
    +
  • Expressiveness: 9 hues prevent color reuse across unrelated concepts — architecture nodes, flow phases, and chart series each get distinct slots without collision.
  • +
  • Maximum separation: Hues placed at the largest gaps in the 360° wheel. Min pairwise distance: 30° (Rose↔Error). Average: 41°.
  • +
  • Semantic coherence: Each hue maps to a consistent meaning — Rose for human actors, Indigo for data/reference, Violet for AI/transformation, Magenta for creative/highlight, Lime for progress.
  • +
  • WCAG validation: Every hue passes AA (≥4.5:1) at shade 600 on white and shade 400 on dark backgrounds. No accessibility compromise.
  • +
+
+
+

Hue Placement Logic

+
    +
  • Indigo (250°): Full 11-shade scale for knowledge, documentation, and reference content.
  • +
  • Violet (285°): Exact midpoint of 250°–320° gap. AI/intelligence — distinct from competitor purple (260–280°) in the Cemetery quadrant.
  • +
  • Magenta (320°): Full scale for creative processes and highlights.
  • +
  • Rose (355°): 30° from error red (25°), perceptually distinct warm-pink. Human actors and emphasis.
  • +
  • Lime (110°): Fills the largest gap (85° between warning 70° and success 155°). ≥40° from both. Progress and intermediate states.
  • +
+
+
+
+ + +
+

Implementation Roadmap

+

+ Phased build-out of the monochrome-first system. Each phase is independently deployable. +

+
+
+
1
+
Neutral Scale
+
Configure neutral scale in custom.css at chroma 0.000 (pure achromatic)
+
+
+
2
+
Semantic Tokens
+
Define --visual-* tokens for all 9 semantic hues in CSS custom properties
+
+
+
3
+
Infima Primary
+
Set Infima primary to neutral dark. Style links as typographic (underline + weight).
+
+
+
4
+
Logo SVG
+
Create monochrome logo SVG variants for light and dark contexts.
+
+
+
5
+
Favicon Set
+
Regenerate favicon set from monochrome source SVG.
+
+
+
6
+
Color Guide
+
Write COLOR_GUIDE.md documenting neutral scale, 9 semantic hues, and usage rules.
+
+
+
7
+
Components
+
Wire all VisualElement components to --visual-* semantic tokens.
+
+
+
+ + + \ No newline at end of file diff --git a/icon-style-showcase.html b/icon-style-showcase.html new file mode 100644 index 0000000..4001b8b --- /dev/null +++ b/icon-style-showcase.html @@ -0,0 +1,805 @@ + + + + + +Agentic Coding — Icon & Illustration Style Showcase + + + + + + + + +
+

Icon & Illustration Style Showcase

+

Three parametric directions for the Agentic Coding design system

+
+
P: 0.55 Medium+
+
A: 0.50 Medium
+
D: 0.50 Medium
+
+
+ +
+ + + + +
+

Three Parametric Directions

+
+ + +
+ D1 +

Blueprint Grid

+

IBM-style construction from strict grid shapes. High regularity, symmetry, precision. Rectangles and circles on 8px grid with orthogonal routing.

+
n=2 (circle) / n=∞ (rect) · 90° angles · BK 0.56
+
PAD
0.748
+
Frac D
1.44
+
DCM
0.23%
+
+ + +
+ D2 +

Smooth Circuit

+

Superellipse-based shapes with smooth continuous curves. Squircles for containers, Bezier connectors. Apple-adjacent organic feel.

+
n=3–5 (squircle) · curves · BK 0.64 (bouba-lean)
+
PAD
0.817
+
Frac D
1.43
+
DCM
5.04%
+
+ + +
+ D3 +

Terminal Geometry

+

Code-native angular vocabulary. Diamonds, chevrons, bracket syntax. Line-segment based with sharp intersections on monospace grid.

+
n=1–1.5 (diamond) · 45° angles · BK 0.35 (kiki)
+
PAD
0.827
+
Frac D
1.48
+
DCM
0.16%
+
+
+
+ + + + +
+

Icon Comparison

+
+ +
+
D1 Blueprint Grid
+
D2 Smooth Circuit
+
D3 Terminal Geometry
+ + +
Node
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ + +
Loop
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ + +
Code
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + + + +
+ + +
Flow
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+ + +
Stack
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+ + +
Check
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ + +
Shield
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+ + +
Action
+
+ + + +
+
+ + + +
+
+ + + + +
+
+
+ + + + +
+

Illustration: Agent Workflow

+

+ Human → Agent → Output — each rendered in the three parametric styles. +

+
+ + +
+
Blueprint Grid
+ + + + + Human + + + + + + + + + + Agent + + + + + + + + + + + Output + +
+ + +
+
Smooth Circuit
+ + + + + Human + + + + + + + + + + Agent + + + + + + + + + + + Output + +
+ + +
+
Terminal Geometry
+ + + + + Human + + + + + + + + + + Agent + + + + + + + + + + + Output + +
+
+
+ + + + +
+

Semantic Color Application

+

+ Same icon form, colored by semantic role. Shown in D2 (Smooth Circuit) style. +

+
+
+ + + + + +
Error
+
+
+ + + + + +
Warning
+
+
+ + + + +
Success
+
+
+ + + + +
System
+
+
+ + + + + + +
Knowledge
+
+
+ + + + +
AI Transform
+
+
+ + + + +
AI Creative
+
+
+ + + + +
Human
+
+
+ + + + +
Progress
+
+
+ + + + + +
Info
+
+
+
+ + + + +
+

Computed Validation Metrics

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricThresholdD1 BlueprintD2 SmoothD3 Terminal
PAD Alignment> 0.800.7480.8170.827
Fractal Dimension D1.2 – 1.61.44 ✓1.43 ✓1.48 ✓
DCM (balance)< 10%0.23% ✓5.04% ✓0.16% ✓
Shannon Entropy H/Hmaxinfo0.8590.9100.877
Aesthetic Measure> 1.00.750.990.99
Berlyne Positionoptimalnear-optimaloptimal ✓optimal ✓
Bouba/Kiki0 = kiki, 1 = bouba0.56 (mixed)0.64 (bouba)0.35 (kiki)
Composite Score79.0100.4104.0
+
+
+ + + + +
+

Recommended Blend

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+

60% Smooth Circuit + 40% Terminal Geometry

+

+ Squircle containers (n=3–4) provide pleasure-axis warmth. Angular chevrons and diamond accents provide arousal-axis technical identity. + Curved forms for positive-valence elements (success, AI, system); angular forms for high-arousal states (error, warning, code). +

+
PAD alignment = 0.937 · P=0.55 A=0.51 D=0.52
+
+
+
+ + + + +
+

Parametric Reference

+
+
+

Superellipse

+
+ |x/a|n + |y/b|n = 1
+ n=1 → diamond
+ n=2 → ellipse
+ n=4 → squircle
+ n→∞ → rectangle +
+
+ + + + +
+
+
+

Angles

+
+ Preferred: 15° 30° 45° 60° 75° 90°
+ D1: 90° only (orthogonal)
+ D2: continuous (Bezier)
+ D3: 45° primary (diagonal grid) +
+
+ + + +
+
+
+

Grid

+
+ Base unit: 8px
+ Min feature: 8px (1 unit)
+ Min gap: 8px (1 unit)
+ Icon canvas: 48px (6 units)
+ All anchors grid-snapped +
+
+ + + + + +
+
+
+
+ +
+ + diff --git a/logo.svg b/logo.svg index 8f80d46..7c9d426 100644 --- a/logo.svg +++ b/logo.svg @@ -1,26 +1,18 @@ - AI Coding Course — Agent Loop Icon - Minimal agent loop with a node. Purple loop + fuchsia node, theme-aware. + Agentic Coding + Code glyph mark. Achromatic, theme-aware. - - - - + + + + - - - diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/scripts/compute-actor-coords.js b/scripts/compute-actor-coords.js new file mode 100644 index 0000000..6bc7db2 --- /dev/null +++ b/scripts/compute-actor-coords.js @@ -0,0 +1,317 @@ +#!/usr/bin/env node +// scripts/compute-actor-coords.js +// Source of truth for all actor primitive geometry. +// Run: node scripts/compute-actor-coords.js + +function round2(v) { return Math.round(v * 100) / 100; } + +// ── OperatorNode ────────────────────────────────────────────────────────────── +// Shape essence traced from 🧑 (U+1F9D1, gender-free Person emoji). +// Head: large circle (20% of BB). Body: smooth cubic Bezier bust silhouette +// converging at the neck point — no legs, Smooth Circuit throughout. +function computeOperator(S) { + // Head — emoji-proportioned (larger than stick-figure 15%) + const HEAD_R = round2(S * 0.200); + const HEAD_CX = round2(S * 0.500); + const HEAD_CY = round2(HEAD_R + S * 0.025); // 1px top margin at S=40 + + // Neck: bottom tangent of head circle + const NECK_Y = round2(HEAD_CY + HEAD_R); + + // Body bounding edges + const INSET = round2(S * 0.050); // 2px at S=40 + const BL = INSET; // body left x + const BR = round2(S - INSET); // body right x + const BOTY = S; // body bottom = full BB height + + // Cubic Bezier control points for the shoulder sweep + // CP1: keeps the tangent vertical at the bottom edge (stays on the side wall) + // CP2: arrives at the neck from the shoulder angle + const CP1Y = round2(NECK_Y + (BOTY - NECK_Y) * 0.55); + const CP2X = round2(HEAD_CX - S * 0.200); // 8px left of center at S=40 + + // Closed bust silhouette: M BL BOTY C BL CP1Y, CP2X NECK_Y, CX NECK_Y + // C (CX+CP2_offset) NECK_Y, BR CP1Y, BR BOTY Z + const CP2XR = round2(HEAD_CX + (HEAD_CX - CP2X)); // mirror + const bodyPath = + `M ${BL} ${BOTY}` + + ` C ${BL} ${CP1Y}, ${CP2X} ${NECK_Y}, ${HEAD_CX} ${NECK_Y}` + + ` C ${CP2XR} ${NECK_Y}, ${BR} ${CP1Y}, ${BR} ${BOTY}` + + ` Z`; + + return { S, HEAD_R, HEAD_CX, HEAD_CY, NECK_Y, BL, BR, BOTY, CP1Y, CP2X, CP2XR, bodyPath }; +} + +// ── AgentNode ───────────────────────────────────────────────────────────────── +function computeAgent(S) { + const HEAD_X = round2(S * 0.075); + const HEAD_Y = round2(S * 0.075); + const HEAD_W = round2(S * 0.85); + const HEAD_H = round2(S * 0.85); + const HEAD_RX = round2(HEAD_W * 0.25); + const EYE_R = round2(S * 0.075); + const EYE_Y = round2(HEAD_Y + HEAD_H * 0.38); + const EYE_LX = round2(HEAD_X + HEAD_W * 0.28); + const EYE_RX = round2(HEAD_X + HEAD_W * 0.72); + const MOUTH_X = round2(HEAD_X + HEAD_W * 0.12); + const MOUTH_Y = round2(HEAD_Y + HEAD_H * 0.65); + const MOUTH_W = round2(HEAD_W * 0.76); + const MOUTH_H = round2(HEAD_H * 0.16); + const DIVIDER_0 = round2(MOUTH_X + MOUTH_W * 0.33); + const DIVIDER_1 = round2(MOUTH_X + MOUTH_W * 0.66); + + return { + S, + HEAD_X, HEAD_Y, HEAD_W, HEAD_H, HEAD_RX, + EYE_R, EYE_Y, EYE_LX, EYE_RX, + MOUTH_X, MOUTH_Y, MOUTH_W, MOUTH_H, + DIVIDERS: [DIVIDER_0, DIVIDER_1], + }; +} + +// ── PromptCard ───────────────────────────────────────────────────────────────── +function computePromptCard() { + const W = 72, H = 40, bodyH = 34, rx = 8; + const STUBS = [ + { y: 8, w: 44 }, + { y: 16, w: 36 }, + { y: 24, w: 28 }, + ]; + const stubH = 2, stubX = 10, stubRx = 1; + const tailPoints = '8 34, 16 34, 8 40'; + + return { W, H, bodyH, rx, STUBS, stubH, stubX, stubRx, tailPoints }; +} + +// ── PromptBubble (40×18 body + cubic Bezier tail, total 40×26) ───────────────── +// Shape essence traced from 💬 (Speech Bubble emoji). +// Body: rounded rect W=40, H=18, rx=9. Tail: cubic Bezier at lower-left. +// Total height including tail: 26px. Cursor bar at x=23 signals active authoring. +function computePromptBubble() { + const W = 40, H = 18, rx = 9; + // Tail: cubic Bezier emerging from body bottom-left, tip at (0, 26) + const tailPath = 'M 8 18 C 4 20, 0 24, 0 26 C 4 20, 12 18, 14 18 Z'; + const stubs = [ + { x: 8, y: 6, w: 20, h: 2, rx: 1 }, + { x: 8, y: 11, w: 14, h: 2, rx: 1 }, + ]; + const cursor = { x: 23, y: 9, w: 2, h: 6, rx: 1 }; + const totalH = 26; // body H + tail depth + + return { W, H, rx, tailPath, stubs, cursor, totalH }; +} + +// ── TravelingPromptCard (36×20, centered at 0,0 for animateMotion) ──────────── +function computeTravelingCard() { + const W = 36, H = 20, rx = 8; + const bodyX = -W / 2, bodyY = -H / 2; // -18, -10 + const stub1W = Math.round(W * 0.50); // 18 + const stub2W = Math.round(W * 0.33); // 12 + const stub1X = -stub1W / 2; // -9 + const stub2X = -stub2W / 2; // -6 + const stub1Y = Math.round(-H / 2 + H * 0.30); // -4 + const stub2Y = Math.round(-H / 2 + H * 0.60); // 2 + return { W, H, rx, bodyX, bodyY, + stubs: [ + { x: stub1X, y: stub1Y, w: stub1W, h: 2, rx: 1 }, + { x: stub2X, y: stub2Y, w: stub2W, h: 2, rx: 1 }, + ], + }; +} + +// ── IntroHookDiagram arc (v2) ────────────────────────────────────────────────── +// New arc: operator right shoulder → agent left edge (no card intermediary). +// Operator BB at x=90/y=80, size=40 → right shoulder ≈ x=128, y=100 +// Agent BB at x=370/y=80 → left edge x=370, y=100 +// Control point Q(229, 42): bows 58px above chord midpoint +// Bubble placed at t=0.35 on arc, top-left = (point_x - 20, point_y - 13) +function computeIntroArc() { + const x0 = 128, y0 = 100; // operator right shoulder + const cpx = 229, cpy = 42; // control point: 58px above chord + const x1 = 370, y1 = 100; // agent BB left edge, mid + + // Bubble placement at t=0.35 + const t = 0.35, nt = 1 - t; + const bubblePtX = round2(nt * nt * x0 + 2 * nt * t * cpx + t * t * x1); + const bubblePtY = round2(nt * nt * y0 + 2 * nt * t * cpy + t * t * y1); + // top-left of 40×26 bubble centered on arc point + const bubbleX = round2(bubblePtX - 20); // W/2 = 20 + const bubbleY = round2(bubblePtY - 13); // totalH/2 = 13 + + // Numerically integrate quadratic bezier length + const N = 1000; + let len = 0; + let px = x0, py = y0; + for (let i = 1; i <= N; i++) { + const ti = i / N; + const nti = 1 - ti; + const qx = nti * nti * x0 + 2 * nti * ti * cpx + ti * ti * x1; + const qy = nti * nti * y0 + 2 * nti * ti * cpy + ti * ti * y1; + const dx = qx - px, dy = qy - py; + len += Math.sqrt(dx * dx + dy * dy); + px = qx; py = qy; + } + len = Math.ceil(len); + + return { + x0, y0, cpx, cpy, x1, y1, + d: `M ${x0} ${y0} Q ${cpx} ${cpy} ${x1} ${y1}`, + len, + bubbleX, bubbleY, bubblePtX, bubblePtY, + }; +} + +// ── AgentOrchestrationDiagram connectors ───────────────────────────────────── +function computeOrchestrationConnectors() { + // Operator center: (280, 36) BB: x=260 y=16 size=40 → bottom center y=56 + // Orchestrator BB: x=260 y=96 → top center y=96, bottom center y=136 + const vertD = 'M 280 56 L 280 96'; + const vertLen = 40; + + // Fan arcs from orchestrator bottom center (280, 136) to worker top centers + // Worker 1 center: (112, 220) BB top: 200 + // Worker 2 center: (280, 220) BB top: 200 + // Worker 3 center: (448, 220) BB top: 200 + + // Arc to Worker 1: Q with control point + const arc1 = { d: 'M 280 136 Q 196 168 112 200' }; + const arc2 = { d: 'M 280 136 L 280 200' }; + const arc3 = { d: 'M 280 136 Q 364 168 448 200' }; + + // Compute arc1 length + function quadBezierLen(x0, y0, cpx, cpy, x1, y1) { + const N = 1000; + let len = 0, px = x0, py = y0; + for (let i = 1; i <= N; i++) { + const t = i / N, nt = 1 - t; + const qx = nt*nt*x0 + 2*nt*t*cpx + t*t*x1; + const qy = nt*nt*y0 + 2*nt*t*cpy + t*t*y1; + len += Math.sqrt((qx-px)**2 + (qy-py)**2); + px=qx; py=qy; + } + return Math.ceil(len); + } + + arc1.len = quadBezierLen(280, 136, 196, 168, 112, 200); + arc2.len = 64; // straight line + arc3.len = quadBezierLen(280, 136, 364, 168, 448, 200); + + return { vertD, vertLen, arc1, arc2, arc3 }; +} + +// ── IdeaIcon ───────────────────────────────────────────────────────────────── +// Shape essence traced from 💡 (Lightbulb emoji). +// Globe: smooth circle (Smooth Circuit), positive valence. +// Base cap: slightly rounded rect (low Terminal Geometry for mechanical terminal). +function computeIdeaIcon(S) { + const GLOBE_R = round2(S * 0.375); // 12px at S=32 + const GLOBE_CX = round2(S * 0.500); // centered horizontally + const GLOBE_CY = round2(GLOBE_R + 2); // 14px at S=32 (2px top margin) + const CAP_W = round2(S * 0.375); // 12px at S=32 + const CAP_H = round2(S * 0.125); // 4px at S=32 + const CAP_X = round2(GLOBE_CX - CAP_W / 2); // 10px at S=32 + const CAP_Y = round2(GLOBE_CY + GLOBE_R + 1); // 27px at S=32 (1px gap below globe) + return { S, GLOBE_R, GLOBE_CX, GLOBE_CY, CAP_W, CAP_H, CAP_X, CAP_Y }; +} + +// ── Output ───────────────────────────────────────────────────────────────────── +const IDEA32 = computeIdeaIcon(32); +const OP40 = computeOperator(40); +const OP32 = computeOperator(32); +const AG40 = computeAgent(40); +const AG32 = computeAgent(32); +const AG14 = computeAgent(14); +const CARD = computePromptCard(); +const BUBBLE = computePromptBubble(); +const TCARD = computeTravelingCard(); +const ARC = computeIntroArc(); +const ORCH = computeOrchestrationConnectors(); + +console.log('// ── OperatorNode S=40 ──'); +console.log(`const OP_40 = {`); +console.log(` headCx: ${OP40.HEAD_CX}, headCy: ${OP40.HEAD_CY}, headR: ${OP40.HEAD_R},`); +console.log(` bodyPath: '${OP40.bodyPath}',`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── OperatorNode S=32 ──'); +console.log(`const OP_32 = {`); +console.log(` headCx: ${OP32.HEAD_CX}, headCy: ${OP32.HEAD_CY}, headR: ${OP32.HEAD_R},`); +console.log(` bodyPath: '${OP32.bodyPath}',`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── AgentNode S=40 ──'); +console.log(`const AGENT_40 = {`); +console.log(` headX: ${AG40.HEAD_X}, headY: ${AG40.HEAD_Y}, headW: ${AG40.HEAD_W}, headH: ${AG40.HEAD_H}, headRx: ${AG40.HEAD_RX},`); +console.log(` eyeR: ${AG40.EYE_R}, eyeY: ${AG40.EYE_Y}, eyeLx: ${AG40.EYE_LX}, eyeRx: ${AG40.EYE_RX},`); +console.log(` mouthX: ${AG40.MOUTH_X}, mouthY: ${AG40.MOUTH_Y}, mouthW: ${AG40.MOUTH_W}, mouthH: ${AG40.MOUTH_H},`); +console.log(` dividers: [${AG40.DIVIDERS.join(', ')}] as const,`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── AgentNode S=32 ──'); +console.log(`const AGENT_32 = {`); +console.log(` headX: ${AG32.HEAD_X}, headY: ${AG32.HEAD_Y}, headW: ${AG32.HEAD_W}, headH: ${AG32.HEAD_H}, headRx: ${AG32.HEAD_RX},`); +console.log(` eyeR: ${AG32.EYE_R}, eyeY: ${AG32.EYE_Y}, eyeLx: ${AG32.EYE_LX}, eyeRx: ${AG32.EYE_RX},`); +console.log(` mouthX: ${AG32.MOUTH_X}, mouthY: ${AG32.MOUTH_Y}, mouthW: ${AG32.MOUTH_W}, mouthH: ${AG32.MOUTH_H},`); +console.log(` dividers: [${AG32.DIVIDERS.join(', ')}] as const,`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── AgentNode S=14 ──'); +console.log(`const AGENT_14 = {`); +console.log(` headX: ${AG14.HEAD_X}, headY: ${AG14.HEAD_Y}, headW: ${AG14.HEAD_W}, headH: ${AG14.HEAD_H}, headRx: ${AG14.HEAD_RX},`); +console.log(` eyeR: ${AG14.EYE_R}, eyeY: ${AG14.EYE_Y}, eyeLx: ${AG14.EYE_LX}, eyeRx: ${AG14.EYE_RX},`); +console.log(` mouthX: ${AG14.MOUTH_X}, mouthY: ${AG14.MOUTH_Y}, mouthW: ${AG14.MOUTH_W}, mouthH: ${AG14.MOUTH_H},`); +console.log(` dividers: [${AG14.DIVIDERS.join(', ')}] as const,`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── PromptCard (72×40 fixed) — DEPRECATED, kept for reference ──'); +console.log(`const PROMPT_GEOM = {`); +console.log(` W: ${CARD.W}, H: ${CARD.H}, bodyH: ${CARD.bodyH}, rx: ${CARD.rx},`); +console.log(` stubs: ${JSON.stringify(CARD.STUBS)} as const,`); +console.log(` stubH: ${CARD.stubH}, stubX: ${CARD.stubX}, stubRx: ${CARD.stubRx},`); +console.log(` tailPoints: '${CARD.tailPoints}',`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── PromptBubble (40×18 body + 26px total with tail) ──'); +console.log(`const BUBBLE_GEOM = {`); +console.log(` W: ${BUBBLE.W}, H: ${BUBBLE.H}, rx: ${BUBBLE.rx},`); +console.log(` tailPath: '${BUBBLE.tailPath}',`); +console.log(` stubs: ${JSON.stringify(BUBBLE.stubs)} as const,`); +console.log(` cursor: ${JSON.stringify(BUBBLE.cursor)} as const,`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── IntroHookDiagram arc (v2: operator shoulder → agent left edge) ──'); +console.log(`// Operator BB x=90/y=80, right shoulder ≈ x=${ARC.x0}/y=${ARC.y0}`); +console.log(`// Control point Q(${ARC.cpx}, ${ARC.cpy}). Agent BB x=370/y=80, left edge x=${ARC.x1}/y=${ARC.y1}`); +console.log(`// Bubble at t=0.35: point=(${ARC.bubblePtX}, ${ARC.bubblePtY}), top-left=(${ARC.bubbleX}, ${ARC.bubbleY})`); +console.log(`const ARC_D = '${ARC.d}';`); +console.log(`const ARC_LEN = ${ARC.len}; // numerically integrated`); +console.log(`const BUBBLE_X = ${ARC.bubbleX}; // top-left x on arc at t=0.35`); +console.log(`const BUBBLE_Y = ${ARC.bubbleY}; // top-left y on arc at t=0.35`); +console.log(''); + +console.log('// ── AgentOrchestrationDiagram connectors ──'); +console.log(`const VERT_D = '${ORCH.vertD}'; // length: ${ORCH.vertLen}`); +console.log(`const FAN_ARC1 = { d: '${ORCH.arc1.d}', len: ${ORCH.arc1.len} } as const;`); +console.log(`const FAN_ARC2 = { d: '${ORCH.arc2.d}', len: ${ORCH.arc2.len} } as const;`); +console.log(`const FAN_ARC3 = { d: '${ORCH.arc3.d}', len: ${ORCH.arc3.len} } as const;`); +console.log(''); + +console.log('// ── IdeaIcon S=32 ──'); +console.log(`const IDEA_32 = {`); +console.log(` globeR: ${IDEA32.GLOBE_R}, globeCx: ${IDEA32.GLOBE_CX}, globeCy: ${IDEA32.GLOBE_CY},`); +console.log(` capX: ${IDEA32.CAP_X}, capY: ${IDEA32.CAP_Y}, capW: ${IDEA32.CAP_W}, capH: ${IDEA32.CAP_H}, capRx: 2,`); +console.log(`} as const;`); +console.log(''); + +console.log('// ── TravelingPromptCard (36×20, centered at 0,0 for animateMotion) ──'); +console.log(`const TCARD_GEOM = { W: ${TCARD.W}, H: ${TCARD.H}, rx: ${TCARD.rx}, bodyX: ${TCARD.bodyX}, bodyY: ${TCARD.bodyY}, stubs: ${JSON.stringify(TCARD.stubs)} as const } as const;`); +console.log('// IH opBubble: x=90, y=52 (bottom=78, gap=2px to op-top=80)'); +console.log('// AOD opBubble: x=260, y=60 (bottom=86, gap=10px to orch-top=96)'); +console.log('// AOD orchBubble: x=260, y=68 (bottom=94, gap=2px to orch-top=96)'); diff --git a/scripts/fonts/Inter-Variable.ttf b/scripts/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..4ab79e0 Binary files /dev/null and b/scripts/fonts/Inter-Variable.ttf differ diff --git a/scripts/fonts/SpaceGrotesk-Bold.ttf b/scripts/fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 0000000..f8eb245 Binary files /dev/null and b/scripts/fonts/SpaceGrotesk-Bold.ttf differ diff --git a/scripts/generate-favicons.js b/scripts/generate-favicons.js new file mode 100644 index 0000000..4f9171c --- /dev/null +++ b/scripts/generate-favicons.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +import sharp from 'sharp'; +import pngToIco from 'png-to-ico'; +import { readFile, writeFile } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const imgDir = join(__dirname, '..', 'website', 'static', 'img'); + +async function generateFavicons() { + console.log('Generating favicons...'); + + // Read the source SVG and strip dark-mode media query for light-mode raster export + const svgSource = await readFile(join(imgDir, 'favicon-source.svg'), 'utf8'); + const lightSvg = svgSource.replace( + /@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[^}]*\}/g, + '' + ); + const svgBuffer = Buffer.from(lightSvg); + + // Generate 32x32 PNG then convert to .ico + const png32 = await sharp(svgBuffer) + .resize(32, 32) + .png() + .toBuffer(); + + const ico = await pngToIco([png32]); + const icoPath = join(imgDir, 'favicon.ico'); + await writeFile(icoPath, ico); + console.log(` favicon.ico (32x32): ${(ico.length / 1024).toFixed(1)}KB`); + + // Generate 180x180 apple-touch-icon + const touchPath = join(imgDir, 'apple-touch-icon.png'); + await sharp(svgBuffer) + .resize(180, 180) + .png() + .toFile(touchPath); + + const { size } = await readFile(touchPath).then(b => ({ size: b.length })); + console.log(` apple-touch-icon.png (180x180): ${(size / 1024).toFixed(1)}KB`); + + console.log('Done.'); +} + +generateFavicons().catch(console.error); diff --git a/scripts/generate-social-card.js b/scripts/generate-social-card.js index 9d44ff6..43ca3a6 100644 --- a/scripts/generate-social-card.js +++ b/scripts/generate-social-card.js @@ -1,13 +1,23 @@ #!/usr/bin/env node -import { createCanvas, loadImage } from 'canvas'; -import { readFile, writeFile } from 'fs/promises'; +import { createCanvas, registerFont } from 'canvas'; +import { writeFile } from 'fs/promises'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Register design system fonts (TTF required by node-canvas) +registerFont(join(__dirname, 'fonts', 'SpaceGrotesk-Bold.ttf'), { + family: 'Space Grotesk', + weight: 'bold', +}); +registerFont(join(__dirname, 'fonts', 'Inter-Variable.ttf'), { + family: 'Inter', + weight: 'normal', +}); + // Open Graph standard dimensions const WIDTH = 1200; const HEIGHT = 630; @@ -15,79 +25,57 @@ const HEIGHT = 630; async function generateSocialCard() { console.log('Generating social card...'); - // Create canvas const canvas = createCanvas(WIDTH, HEIGHT); const ctx = canvas.getContext('2d'); - // Solid color background (professional minimalist approach) - ctx.fillStyle = '#7c3aed'; // Brand primary purple - ctx.fillRect(0, 0, WIDTH, HEIGHT); - - // Add subtle radial overlay for depth without over-saturation - const radialGradient = ctx.createRadialGradient( - WIDTH / 2, HEIGHT / 2, 0, - WIDTH / 2, HEIGHT / 2, WIDTH * 0.8 - ); - radialGradient.addColorStop(0, 'rgba(139, 92, 246, 0.3)'); // Lighter purple center - radialGradient.addColorStop(1, 'rgba(124, 58, 237, 0)'); // Transparent edges - - ctx.fillStyle = radialGradient; + // Solid dark background per design system spec + ctx.fillStyle = '#111111'; ctx.fillRect(0, 0, WIDTH, HEIGHT); - // Add a white/cream box in the center for the logo - const boxSize = 200; - const boxX = (WIDTH - boxSize) / 2; - const boxY = 135; - - ctx.fillStyle = '#f5f5f0'; - ctx.fillRect(boxX, boxY, boxSize, boxSize); - - // Load and draw the logo SVG - // For SVG, we need to use a workaround - load it as an image - // The logo SVG needs to be converted first, or we can embed a simplified version - - // Draw a simplified version of the logo directly - // Purple loop with fuchsia node + // Draw glyph — neutral-100 const centerX = WIDTH / 2; - const centerY = boxY + boxSize / 2; - const radius = 50; + const glyphY = 200; - // Draw the gapped circle (loop) - ctx.strokeStyle = '#7c3aed'; - ctx.lineWidth = 16; + ctx.strokeStyle = '#e8e8e8'; + ctx.lineWidth = 8; ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; - const startAngle = (Math.PI / 180) * 10; - const endAngle = (Math.PI / 180) * 280; - + // < bracket ctx.beginPath(); - ctx.arc(centerX, centerY, radius, startAngle, endAngle); + ctx.moveTo(centerX - 40, glyphY - 40); + ctx.lineTo(centerX - 80, glyphY); + ctx.lineTo(centerX - 40, glyphY + 40); ctx.stroke(); - // Draw the node (small square at top-right) - matches logo.svg positioning - const nodeSize = 22; - const nodeAngle = 36.9; // Match logo.svg (was 45°, causing 8.1° misalignment) - const nodeX = centerX + Math.cos((Math.PI / 180) * nodeAngle) * radius - nodeSize / 2; - const nodeY = centerY - Math.sin((Math.PI / 180) * nodeAngle) * radius - nodeSize / 2; + // / slash + ctx.beginPath(); + ctx.moveTo(centerX + 20, glyphY - 50); + ctx.lineTo(centerX - 20, glyphY + 50); + ctx.stroke(); - ctx.fillStyle = '#ec4899'; + // > bracket ctx.beginPath(); - ctx.roundRect(nodeX, nodeY, nodeSize, nodeSize, 5); - ctx.fill(); + ctx.moveTo(centerX + 40, glyphY - 40); + ctx.lineTo(centerX + 80, glyphY); + ctx.lineTo(centerX + 40, glyphY + 40); + ctx.stroke(); - // Add title text - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 90px sans-serif'; + // Title — Space Grotesk Bold (display font) + ctx.fillStyle = '#e8e8e8'; + ctx.font = 'bold 80px "Space Grotesk"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText('Agentic Coding', centerX, 380); - const titleY = boxY + boxSize + 100; - ctx.fillText('AI Coding Course', centerX, titleY); + // Subtitle — Inter (body font) + ctx.fillStyle = '#9b9b9b'; + ctx.font = '32px "Inter"'; + ctx.fillText('Master AI-assisted software engineering', centerX, 450); - // Add subtitle text - ctx.font = '32px sans-serif'; - const subtitleY = titleY + 70; - ctx.fillText('Master AI-assisted software engineering for experienced developers', centerX, subtitleY); + // 3px accent line — cyan-400 + ctx.fillStyle = '#00b2b2'; + ctx.fillRect(centerX - 150, 500, 300, 3); // Convert to buffer and save const buffer = canvas.toBuffer('image/png'); @@ -95,7 +83,7 @@ async function generateSocialCard() { await writeFile(outputPath, buffer); - console.log(`✓ Social card generated: ${outputPath}`); + console.log(`Social card generated: ${outputPath}`); console.log(` Dimensions: ${WIDTH}x${HEIGHT}px`); console.log(` File size: ${(buffer.length / 1024).toFixed(1)}KB`); } diff --git a/scripts/package-lock.json b/scripts/package-lock.json index e08d319..41507e1 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -9,7 +9,19 @@ "version": "1.0.0", "dependencies": { "@google/generative-ai": "^0.24.1", - "canvas": "^3.2.0" + "canvas": "^3.2.0", + "png-to-ico": "^2.1.8", + "sharp": "^0.33.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@google/generative-ai": { @@ -21,6 +33,373 @@ "node": ">=18.0.0" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -96,6 +475,47 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -191,6 +611,12 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -251,6 +677,32 @@ "wrappy": "1" } }, + "node_modules/png-to-ico": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/png-to-ico/-/png-to-ico-2.1.8.tgz", + "integrity": "sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.36", + "minimist": "^1.2.6", + "pngjs": "^6.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -348,6 +800,45 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -393,6 +884,15 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -439,6 +939,13 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/scripts/package.json b/scripts/package.json index 9448510..96656aa 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -8,10 +8,15 @@ "generate-podcast": "node generate-podcast.js", "generate-podcast-batch": "node generate-podcast.js --all", "generate-podcast-script-only": "node generate-podcast.js --script-only", - "generate-podcast-audio-only": "node generate-podcast.js --audio-only" + "generate-podcast-audio-only": "node generate-podcast.js --audio-only", + "generate-social-card": "node generate-social-card.js", + "generate-favicons": "node generate-favicons.js", + "generate-brand-assets": "node generate-social-card.js && node generate-favicons.js" }, "dependencies": { "@google/generative-ai": "^0.24.1", - "canvas": "^3.2.0" + "canvas": "^3.2.0", + "png-to-ico": "^2.1.8", + "sharp": "^0.33.0" } } diff --git a/website/developer-tools/cli-coding-agents.md b/website/developer-tools/cli-coding-agents.md index d6514c7..1b47739 100644 --- a/website/developer-tools/cli-coding-agents.md +++ b/website/developer-tools/cli-coding-agents.md @@ -15,7 +15,7 @@ This page covers agents that operate primarily from the command line—distinct **Key differentiators:** -- **Hierarchical CLAUDE.md:** Multi-level context files (global → project → subdirectory) that [merge automatically based on working directory](/docs/practical-techniques/lesson-6-project-onboarding)—define coding standards at project root, override per-module, and set personal preferences globally +- **Hierarchical CLAUDE.md:** Multi-level context files (global → project → subdirectory) that [merge automatically based on working directory](/practical-techniques/lesson-6-project-onboarding)—define coding standards at project root, override per-module, and set personal preferences globally - **Sub-agents via Task(...):** Spawn isolated agent instances for parallel research, code exploration, or specialized tasks without polluting main context - **Planning mode:** Explicit plan-before-execute workflow for complex changes—align on approach before any files are modified - **Hooks system:** Deterministic pre/post-execution rules for validation, formatting, or custom workflows triggered at specific points diff --git a/website/developer-tools/cli-tools.md b/website/developer-tools/cli-tools.md index 7b32e54..497c5de 100644 --- a/website/developer-tools/cli-tools.md +++ b/website/developer-tools/cli-tools.md @@ -412,6 +412,6 @@ agent-browser's ref-based approach (`@e1`, `@e2`) produces deterministic element **Related Course Content:** -- [Lesson 7: Planning & Execution](/docs/practical-techniques/lesson-7-planning-execution) - Multi-worktree workflows leveraging these CLI tools +- [Lesson 7: Planning & Execution](/practical-techniques/lesson-7-planning-execution) - Multi-worktree workflows leveraging these CLI tools - [Developer Tools: Terminals](/developer-tools/terminals) - Terminal recommendations for running these CLI tools efficiently - [Developer Tools: MCP Servers](/developer-tools/mcp-servers) - Extend CLI agents with code research and web grounding diff --git a/website/developer-tools/mcp-servers.md b/website/developer-tools/mcp-servers.md index c747764..53157ca 100644 --- a/website/developer-tools/mcp-servers.md +++ b/website/developer-tools/mcp-servers.md @@ -37,7 +37,7 @@ uv tool install chunkhound Requires Python 3.10+ and the uv package manager. See [ChunkHound on GitHub](https://github.com/chunkhound/chunkhound) for API key configuration and setup details. -**Learn more:** [Lesson 5: Grounding](/docs/methodology/lesson-5-grounding#deep-dive-chunkhound-architecture) covers ChunkHound's architecture, pipeline design, and scale guidance in detail. +**Learn more:** [Lesson 5: Grounding](/methodology/lesson-5-grounding#deep-dive-chunkhound-architecture) covers ChunkHound's architecture, pipeline design, and scale guidance in detail. ## Web Research @@ -69,7 +69,7 @@ brew install arguseek Requires Go 1.23+ and Google API credentials. See [ArguSeek on GitHub](https://github.com/ArguSeek/arguseek) for detailed setup instructions and configuration options. -**Learn more:** [Lesson 5: Grounding](/docs/methodology/lesson-5-grounding#deep-dive-arguseek-architecture) explains ArguSeek's architecture, semantic subtraction, and research patterns. +**Learn more:** [Lesson 5: Grounding](/methodology/lesson-5-grounding#deep-dive-arguseek-architecture) explains ArguSeek's architecture, semantic subtraction, and research patterns. ## Browser Automation @@ -91,5 +91,5 @@ Previous recommendations included Playwright MCP and Chrome DevTools MCP. These **Related Course Content:** -- [Lesson 5: Grounding](/docs/methodology/lesson-5-grounding) - Detailed architecture and use cases for ChunkHound and ArguSeek -- [Lesson 7: Planning & Execution](/docs/practical-techniques/lesson-7-planning-execution) - Multi-agent workflows that leverage MCP capabilities +- [Lesson 5: Grounding](/methodology/lesson-5-grounding) - Detailed architecture and use cases for ChunkHound and ArguSeek +- [Lesson 7: Planning & Execution](/practical-techniques/lesson-7-planning-execution) - Multi-agent workflows that leverage MCP capabilities diff --git a/website/developer-tools/terminals.md b/website/developer-tools/terminals.md index 5f182ed..bc4e80c 100644 --- a/website/developer-tools/terminals.md +++ b/website/developer-tools/terminals.md @@ -102,4 +102,4 @@ Use ArguSeek to research best practices for your chosen terminal—session manag **Related:** - [Developer Tools: Modern CLI Tools](/developer-tools/cli-tools) - The ecosystem that completes your terminal-based development environment -- [Lesson 7: Planning & Execution](/docs/practical-techniques/lesson-7-planning-execution) - Multi-worktree workflows leveraging modern terminals +- [Lesson 7: Planning & Execution](/practical-techniques/lesson-7-planning-execution) - Multi-worktree workflows leveraging modern terminals diff --git a/website/docs/CLAUDE.md b/website/docs/CLAUDE.md index 9cd61a9..6e0f1f0 100644 --- a/website/docs/CLAUDE.md +++ b/website/docs/CLAUDE.md @@ -1,4 +1,4 @@ -# Course Content - Writing Standards +# Content - Writing Standards ## Target Audience @@ -15,7 +15,7 @@ ### Voice -**Coworker-level communication** - Talk to peers, not students +**Coworker-level communication** - Talk to peers, not readers learning basics - Direct and concise - Professional but conversational diff --git a/website/docs/about.md b/website/docs/about.md new file mode 100644 index 0000000..30734be --- /dev/null +++ b/website/docs/about.md @@ -0,0 +1,17 @@ +--- +title: About +sidebar_label: About +hide_table_of_contents: true +--- + +## Author + +**Ofri Wolfus** is a software engineer and technical educator focused on helping experienced developers navigate the AI-assisted development landscape. You can connect with him on [LinkedIn](https://www.linkedin.com/in/ofriwolfus/). + +## Built With + +This site is built with [Docusaurus](https://docusaurus.io) — an open-source static site generator maintained by Meta. + +## Copyright + +© {new Date().getFullYear()} Ofri Wolfus diff --git a/website/docs/experience-engineering/lesson-14-design-tokens.md b/website/docs/experience-engineering/lesson-14-design-tokens.md new file mode 100644 index 0000000..a6d3a92 --- /dev/null +++ b/website/docs/experience-engineering/lesson-14-design-tokens.md @@ -0,0 +1,372 @@ +--- +sidebar_position: 1 +sidebar_label: 'Design Tokens & Visual Primitives' +sidebar_custom_props: + sectionNumber: 14 +title: 'Design Tokens & Visual Primitives' +--- + +[Lesson 12](/practical-techniques/lesson-12-spec-driven-development) established that specs are scaffolding — temporary thinking tools deleted after implementation. This section applies spec-driven development to user-facing interfaces. We call this **Experience Engineering** — specifying design tokens, component APIs, interaction flows, and accessibility architecture precisely enough for an agent to implement and browser automation to verify. For system-level specs, see [Lesson 13](/practical-techniques/lesson-13-systems-thinking-specs). + +The key insight: **the experience layer can be fully built and validated before any backend exists.** Design tokens, component layouts, interaction flows, accessibility constraints — none of these depend on API responses. An agent generates the UI with mocked data (behavior mocks via MSW), browser automation verifies it through the accessibility tree (`snapshot -ic`), and you iterate on the experience until it's right. Backend integration comes later, and by then the interface contract is locked. + +This lesson walks through building a production color palette using [Lesson 3](/methodology/lesson-3-high-level-methodology)'s four-phase workflow — **Research → Plan → Execute → Validate** — applied to experience engineering. The running example throughout these lessons: a **team dashboard for a SaaS billing product** — subscription plans, usage metrics, invoices, team management, and settings. + +### Why Agents Need You for UI + +**The review paradox.** UI code is the hardest to review because correctness is visual and behavioral, not just logical. You can't grep for "the button is in the wrong place" or "the modal doesn't feel right." This makes the spec → build → check loop even more critical for frontend work — the spec defines what "correct" means, browser automation checks it deterministically, and the accessibility tree makes it machine-readable. + +**The perceptual limitation.** Multimodal LLMs process screenshots through Vision Transformers that divide images into fixed patches — typically 16×16 pixels each. A 1920×1080 screenshot becomes roughly 1,100 tokens representing over two million pixels — each patch compressed into a single embedding vector. The model sees major structural features clearly: missing sections, wrong layout direction, a red error banner where a green success banner should be. But sub-patch details — a 3px spacing difference, a 1px border, the distinction between `gray-200` and `gray-300` — fall below the resolution floor. Vision is excellent for broad guidance during development. It cannot do exact verification or perceptual evaluation. + +This is why the four-phase workflow matters for experience engineering: the agent handles math and generation (Execute), but **you** provide perceptual judgment (Validate). The division of labor is driven by a known architectural limitation, not preference. + +## Three-Tier Token Architecture + +Design tokens are the foundational design decisions of the UI — colors, spacing, typography, shadows. When they change, the impact ripples through every component, layout, and flow. + +| Tier | Name | Example | Purpose | +|------|------|---------|---------| +| Primitive | Raw values | `blue-500: oklch(0.55 0.15 250)` | Color space, no semantics | +| Semantic | Intent | `color-bg-primary: {blue-500}` | Meaning, theme-switchable | +| Component | Scoped | `button-bg: {color-bg-primary}` | Component-specific | + +Components reference **semantic** tokens, never primitives. A theme swap changes only the primitive→semantic mapping. Components don't know or care. + +Brand colors are your judgment. Everything derivative — shade scales, harmony colors, dark mode variants, contrast validation — is computable math. + +## Research: Grounding Color Decisions + +[Lesson 3](/methodology/lesson-3-high-level-methodology)'s first phase: ground the agent in both your codebase and domain knowledge before planning changes. For color systems, that means understanding the color science, studying how competitors handle palettes, and confirming accessibility requirements — before you pick a single hue. + +### Domain Research with ArguSeek + +You wouldn't pick a database without researching the options. Same principle applies to color decisions. [ArguSeek](/methodology/lesson-5-grounding#arguseek-isolated-context--state)'s `research_iteratively` builds cumulative knowledge across queries through semantic subtraction — each follow-up skips already-covered content and advances the research: + +``` +Q1: "OKLCH color space best practices for design systems and token generation" + → 18 sources: perceptual uniformity, gamut mapping, CSS Color 4 spec + → Returns ~3,000 tokens + +Q2: "SaaS billing dashboard color palettes — Stripe, Linear, Vercel design approaches" + → 22 sources, skips OKLCH basics from Q1, focuses on competitor patterns + → Returns ~3,400 tokens (competitor-specific, no repeated theory) + +Q3: "WCAG 2.1 AA contrast requirements for design token systems" + → 15 sources, skips color theory and competitor patterns already covered + → Returns ~2,800 tokens (accessibility-specific) + +Total: 55 sources scanned, ~9,200 tokens to your orchestrator +``` + +After three queries you have grounded knowledge on the color space (OKLCH over HSL), competitor precedent (what works in production SaaS dashboards), and the accessibility constraints your palette must satisfy (WCAG AA ≥ 4.5:1). All without polluting your main context — ArguSeek runs in isolated context per [Lesson 5](/methodology/lesson-5-grounding)'s sub-agent pattern. + +### Visual Research with agent-browser + +Text research tells you *about* competitor palettes. Screenshots show you the actual result. Use agent-browser to capture competitor dashboards for **your** visual analysis: + +``` +open "https://stripe.com/billing" +screenshot /tmp/stripe-billing.png + +open "https://linear.app" +screenshot /tmp/linear-dashboard.png +``` + +The agent captures the screenshots. **You** evaluate them — which color temperature feels right for a billing product? How do competitors handle the neutral scale? Does Stripe's blue feel more trustworthy than Linear's purple? These are perceptual judgments the agent cannot make (the VT limitation above). But the agent saved you from manually navigating, waiting for page loads, and managing screenshot files. + +| Actor | Research Phase Responsibility | +|-------|-------------------------------| +| Human | Decide what to research, evaluate visual inspiration, form brand opinions | +| Agent | Retrieve and synthesize domain knowledge (ArguSeek), capture competitor screenshots (agent-browser) | + +## Plan: Source Hues and Token Spec + +Research complete. Now lock in the design decisions that drive everything downstream. This is [Lesson 3](/methodology/lesson-3-high-level-methodology)'s Plan phase — and for color palettes, it's "Exact Planning": the solution structure is known (OKLCH shade scales), so be directive with specificity and constraints. + +### Source Hue Selection (Human Judgment) + +The shade-scale algorithm works for any hue. A production palette applies it to **multiple independent source hues** — not computed harmonies, but convention-fixed colors that users already associate with specific meanings. + +**Source hues for the billing dashboard:** + +| Role | Hue | Peak Chroma | Rationale | +|------|-----|-------------|-----------| +| Primary | 250° | 0.15 | Brand blue — identity, CTAs, active states | +| Neutral | 250° | 0.015 | Brand-tinted gray — same hue, ~10% chroma | +| Error | 25° | 0.15 | Convention: red-family for danger | +| Warning | 70° | 0.12 | Convention: amber for caution | +| Success | 155° | 0.13 | Teal-green — colorblind-safe vs. error red | + +Error is red because users expect red for danger — not because red is a harmony of blue. Success uses teal-green rather than pure green because teal remains distinguishable from error-red under protanopia and deuteranopia (the two most common forms of color vision deficiency). Neutral is the brand hue with chroma stripped to ~0.015: this produces a brand-tinted gray that feels cohesive without being colorful. This project's own `custom.css` uses exactly this pattern: `--semantic-success: #06b6d4` (cyan), `--semantic-error: #e11d48` (rose-red) — independently chosen, not derived from `--brand-primary: #007576`. + +These five hue choices are **the** human judgment calls in this lesson. Everything downstream — shade generation, contrast checking, semantic mapping, dark mode derivation — is math the agent computes. + +### The Shade Scale Spec (Agent Math) + +HSL looks intuitive but isn't perceptually uniform — `hsl(60,100%,50%)` (yellow) appears far brighter than `hsl(240,100%,50%)` (blue) despite identical lightness values. OKLCH fixes this by design, which is why the primitive tokens above use it. From a single brand color, the agent writes and executes code to derive the entire primitive tier. This is [Lesson 4](/methodology/lesson-4-prompting-101)'s math-as-code principle in action: LLMs can't do arithmetic reliably, but they write code that does — color math is a perfect application. + +**What one brand color yields:** + +| Step | Output | Method | +|------|--------|--------| +| Shade scale (50–950) | 11 perceptually even shades | Non-linear lightness curve in OKLCH L channel | +| Hue compensation | Corrected hue per shade | Bezold-Brücke shift (colors drift toward yellow/purple with lightness) | +| Chroma curve | Balanced saturation | Parabolic — peak at midtones, taper at extremes | +| Harmony colors | Decorative accents, data-viz variety | Hue rotation in OKLCH H channel (not semantic roles — [see below](#color-harmony-for-decorative-variety)) | +| Dark mode | Inverted palette | Lightness inversion with chroma preservation | +| Contrast pairs | Valid text/bg combinations | WCAG AA ≥ 4.5:1 validation (A-010) | + +This table is the spec: it tells the agent exactly what to compute and how. Five hues × 11 shades = 55 primitive tokens, plus semantic mappings and contrast pairs. + +### Color Harmony for Decorative Variety + +Harmony rotation produces visually cohesive hue sets for data visualization, illustration palettes, and decorative accents — anywhere you need multiple distinct hues that feel related. It is **not** how semantic roles (error, warning, success) are assigned. Those are convention-fixed. + +| Harmony | Rotation | Best For | +|---------|----------|----------| +| Monochromatic | 0° (shade scale only) | Single-brand UIs, dashboards | +| Analogous | ±30° | Harmonious palettes, gradients | +| Complementary | 180° | CTAs, alerts, high-contrast accents | +| Split-complementary | 150° + 210° | Balanced accent pairs | +| Triadic | ±120° | Multi-brand, data visualization | +| Tetradic | 90° intervals | Complex UIs needing 4+ distinct hues | + +Which harmonies to include is design judgment. The calculation is the agent's job. + +### Token Assumptions Table + +| Token Assumption | Source | Drives | +|------------------|--------|--------| +| Spacing base = 8px, scale = 0.5x/1x/1.5x/2x/3x/4x | Design system | All component padding, margins, gaps | +| Type scale ratio = 1.25 (Major Third) | Typography standards | All font sizes: base × ratio^n | +| Min contrast = 4.5:1 (WCAG AA) | Accessibility requirement | A-010, all text/bg combinations | +| OKLCH gamut = sRGB with fallbacks | Browser support | All color tokens need sRGB fallback | +| Every semantic background has a paired foreground token | Production design systems (M3, shadcn) | A-010, all text/bg combinations across themes | + +The **Drives** column creates traceability from token decision to spec element. When a design decision changes (new brand colors, updated spacing scale, tighter accessibility requirements), the Drives column tells both you and the agent which constraints to re-verify. + +## Execute: Agent-Generated Palette + +Plan locked. The agent now writes and runs TypeScript to compute the entire palette — [Lesson 3](/methodology/lesson-3-high-level-methodology)'s Execute phase. The prompt below feeds the source hues and algorithm spec directly to the agent. This is [Lesson 4](/methodology/lesson-4-prompting-101)'s math-as-code principle: the agent can't do color arithmetic reliably, but it writes code that does. + +### The Prompt + +Demonstrates [Lesson 4](/methodology/lesson-4-prompting-101) principles — imperative commands, constraints as guardrails, chain-of-thought, structure, math-as-code: + +``` +Write a TypeScript script that generates a production color palette from +source hues. Use OKLCH for all calculations; output both OKLCH values +and sRGB hex fallbacks. + +## Input — Source Hues +| Role | Hue | Peak Chroma | Notes | +|---------|-----|-------------|------------------------------| +| primary | 250 | 0.15 | Brand blue | +| neutral | 250 | 0.015 | Brand-tinted gray | +| error | 25 | 0.15 | Red-family for danger | +| warning | 70 | 0.12 | Amber for caution | +| success | 155 | 0.13 | Teal-green, colorblind-safe | + +## Steps +1. For each source hue, generate shade scale (50–950, 11 stops): + - Map lightness non-linearly: L=0.97 at 50, L=0.25 at 950 + - Apply Bezold-Brücke hue compensation per shade + - Use parabolic chroma curve: peak at mid-lightness, taper at extremes + - Neutral scale uses same curve but capped at peak chroma 0.015 +2. Build semantic mapping for light and dark themes: + - Light: bg-page=neutral-50, text-primary=neutral-900, + primary=primary-600, error=error-600, etc. + - Dark: bg-page=neutral-900, text-primary=neutral-50, + primary=primary-400, error=error-400, etc. + - Use shade 600 for light-mode semantic colors (passes AA on white) + - Use shade 400 for dark-mode semantic colors (passes AA on dark bg) +3. Compute paired foreground tokens: + - For every background semantic token, find the shade with + highest contrast ≥ 4.5:1 (prefer white/black, then lightest/darkest) + - Output as --on-primary, --on-error, etc. +4. Validate every semantic text/background pair: + - WCAG AA ≥ 4.5:1 for normal text, ≥ 3:1 for large text + - Flag failing pairs with suggested fix (adjust shade until passing) +5. Output as CSS custom properties organized by tier: + - Primitives: --color-primary-500: oklch(...) + - Semantic (light): --color-primary: var(--color-primary-600) + - Semantic (dark): --color-primary: var(--color-primary-400) + - Include sRGB fallback for every primitive + +## Constraints +- All math in OKLCH — sRGB only for fallback output +- Gamut-map out-of-range values by clamping chroma, preserving hue +- Every generated value must include both OKLCH and hex +- 5 roles × 11 shades = 55 primitives + ~15 semantic tokens per theme +``` + +The prompt structure maps directly to [Lesson 4](/methodology/lesson-4-prompting-101) principles: imperative commands ("Write a script that..."), constraints as guardrails (WCAG thresholds, color space, gamut), chain-of-thought (numbered steps), markdown structure for information density, and math-as-code — the agent writes and runs the calculation rather than guessing color values. The input table separates designer judgment (which hues, which roles) from agent work (shade generation, contrast checking, semantic mapping). + +### What the Agent Produces + +The interactive component below demonstrates what the agent outputs from ~80 lines of TypeScript. Your job at this stage: run it, look at the result, and form a perceptual opinion before moving to the Validate phase. Try changing the hue from 250 to 30 (orange) — the entire palette recalculates. + +import ColorPaletteGenerator from '@site/src/components/VisualElements/ColorPaletteGenerator'; + + + +**Concrete example.** Here's what one brand color (`oklch(0.55 0.15 250)` — the blue-500 already in the token table) produces: + +export const shadeExample = [ + { shade: '50', oklch: '0.91 0.042 254', hex: '#cfe5ff', vsW: '1.3:1', vsB: '16.3:1', text: 'Black' }, + { shade: '100', oklch: '0.87 0.066 253', hex: '#b6d7ff', vsW: '1.5:1', vsB: '14.1:1', text: 'Black' }, + { shade: '200', oklch: '0.79 0.111 252', hex: '#85beff', vsW: '1.9:1', vsB: '10.8:1', text: 'Black' }, + { shade: '300', oklch: '0.71 0.150 252', hex: '#53a6fc', vsW: '2.6:1', vsB: '8.2:1', text: 'Black' }, + { shade: '400', oklch: '0.64 0.150 251', hex: '#398fe3', vsW: '3.4:1', vsB: '6.2:1', text: 'Black' }, + { shade: '500', oklch: '0.57 0.150 250', hex: '#1b7acb', vsW: '4.5:1', vsB: '4.7:1', text: 'Either' }, + { shade: '600', oklch: '0.50 0.140 249', hex: '#0067b0', vsW: '5.9:1', vsB: '3.6:1', text: 'White' }, + { shade: '700', oklch: '0.44 0.121 248', hex: '#005591', vsW: '7.8:1', vsB: '2.7:1', text: 'White' }, + { shade: '800', oklch: '0.37 0.084 248', hex: '#14446b', vsW: '10.2:1', vsB: '2.1:1', text: 'White' }, + { shade: '900', oklch: '0.31 0.031 247', hex: '#243240', vsW: '13.0:1', vsB: '1.6:1', text: 'White' }, + { shade: '950', oklch: '0.28 0.000 246', hex: '#292929', vsW: '14.6:1', vsB: '1.4:1', text: 'White' }, +]; + + + + +{shadeExample.map(s => ( + + + + + + + + +))} + +
ShadeOKLCHColorvs Whitevs BlackText
{s.shade}oklch({s.oklch}){s.hex}{s.vsW}{s.vsB}{s.text}
+ +Note the three algorithms at work: lightness drops non-linearly (0.91→0.28), chroma peaks at 300–400 then tapers (parabolic curve), and hue drifts subtly from 254→246 (Bezold-Brücke compensation). Every shade has a valid WCAG AA text color. The crossover at shade 500–600 is where text switches from black to white — this is computed, not chosen. + +## Validate: The Perceptual Loop + +[Lesson 3](/methodology/lesson-3-high-level-methodology)'s Validate phase closes the loop — and for experience engineering, this is where the human-agent division of labor matters most. The agent handles automated validation (contrast math, gamut checks). You handle perceptual validation (does it look right?). Neither can do the other's job. + +### Automated Validation (Agent) + +The agent validates every generated token against the spec constraints. Feed each source hue through the same shade-scale algorithm. Five hues × 11 shades = 55 primitive tokens. Here's a cross-section (shades 50, 500, 900): + +export const primitiveOverview = [ + { role: 'Primary', shade: '50', oklch: '0.97 0.068 250', hex: '#d3faff' }, + { role: 'Primary', shade: '500', oklch: '0.60 0.150 250', hex: '#2784d5' }, + { role: 'Primary', shade: '900', oklch: '0.29 0.092 250', hex: '#002c57' }, + { role: 'Neutral', shade: '50', oklch: '0.97 0.007 250', hex: '#f2f6fa' }, + { role: 'Neutral', shade: '500', oklch: '0.60 0.015 250', hex: '#7a8189' }, + { role: 'Neutral', shade: '900', oklch: '0.29 0.009 250', hex: '#282c30' }, + { role: 'Error', shade: '50', oklch: '0.97 0.068 25', hex: '#ffe4de' }, + { role: 'Error', shade: '500', oklch: '0.60 0.150 25', hex: '#ca5551' }, + { role: 'Error', shade: '900', oklch: '0.29 0.092 25', hex: '#501212' }, + { role: 'Warning', shade: '50', oklch: '0.97 0.054 70', hex: '#fff0ce' }, + { role: 'Warning', shade: '500', oklch: '0.60 0.120 70', hex: '#ad721c' }, + { role: 'Warning', shade: '900', oklch: '0.29 0.074 70', hex: '#422300' }, + { role: 'Success', shade: '50', oklch: '0.97 0.059 155', hex: '#d7ffe4' }, + { role: 'Success', shade: '500', oklch: '0.60 0.130 155', hex: '#2c965d' }, + { role: 'Success', shade: '900', oklch: '0.29 0.080 155', hex: '#003618' }, +]; + + + + +{primitiveOverview.map((s, i) => ( + + + + + + +))} + +
RoleShadeOKLCHColor
{s.role}{s.shade}oklch({s.oklch}){s.hex}
+ +Same algorithm, different inputs. The neutral scale's low chroma (~0.015 vs. 0.15) produces near-gray with a subtle blue undertone. Error, warning, and success scales are as saturated as primary — their 600 shades all pass WCAG AA on white, making them safe for text and icons in light mode. + +**Semantic mapping.** Primitives are raw values; semantic tokens assign meaning. The agent maps shades to roles for both light and dark themes: + +| Semantic Token | Light | Dark | Purpose | +|----------------|-------|------|---------| +| `--bg-page` | `neutral-50` | `neutral-900` | Page background | +| `--bg-surface` | `white` | `neutral-800` | Cards, popovers, modals | +| `--bg-muted` | `neutral-100` | `neutral-700` | Secondary surfaces, disabled | +| `--text-primary` | `neutral-900` | `neutral-50` | Body text | +| `--text-secondary` | `neutral-500` | `neutral-400` | Placeholder, muted text | +| `--border` | `neutral-200` | `neutral-700` | Borders, dividers | +| `--primary` | `primary-600` | `primary-400` | CTAs, links, active states | +| `--on-primary` | `white` | `primary-950` | Text on primary backgrounds | +| `--error` | `error-600` | `error-400` | Error text, icons | +| `--on-error` | `white` | `error-950` | Text on error background | +| `--error-subtle` | `error-50` | `error-950` | Error banner background | +| `--warning` | `warning-600` | `warning-400` | Warning text, icons | +| `--warning-subtle` | `warning-50` | `warning-950` | Warning banner background | +| `--success` | `success-600` | `success-400` | Success text, icons | +| `--success-subtle` | `success-50` | `success-950` | Success banner background | + +Note light mode uses shade 600 (not 500) for semantic text roles — 500 shades hover around 4:1 contrast on white, just below WCAG AA. Shade 600 clears the threshold consistently across all hues. Dark mode uses 400 shades for the same reason against dark backgrounds. This crossover is computed by the same contrast-checking algorithm from the shade scale, not guessed. + +**Paired foreground tokens.** Every background semantic token needs a paired foreground with guaranteed contrast. Material Design 3 calls these `on-primary`, `on-error`. shadcn/ui uses `--primary-foreground`, `--destructive-foreground`. The naming convention varies; the constraint is universal. Without explicit pairs, agents pick arbitrary text colors and contrast breaks silently. + +### Visual Validation (Human + agent-browser) + +Automated checks confirm the math is correct. They can't tell you whether the palette *feels* right. This is where you close the loop — and where the VT perceptual limitation from earlier becomes a workflow constraint. + +Have the agent render the generated tokens into a preview page, then capture it for your review: + +``` +open "http://localhost:3000/token-preview" +screenshot /tmp/palette-light.png + +# toggle theme +click @dark-mode-toggle +screenshot /tmp/palette-dark.png +``` + +You now have side-by-side screenshots of the full palette in both themes. The agent produced these artifacts, but it cannot evaluate them — it can verify "the page rendered without errors" via `snapshot -ic`, but not "the warning yellow is too muted" or "the neutral scale feels too warm." That's your judgment call. + +### The Iteration Loop + +When you spot a perceptual issue, feed it back to the agent in natural language: *"The warning yellow feels too muted at shades 400-600. Increase peak chroma from 0.12 to 0.14."* The agent regenerates the warning scale, re-validates WCAG contrast automatically, and screenshots the result. You review again. This loop continues until the palette satisfies both the math constraints (agent) and your visual standards (human). + +| Step | Human | Agent | +|------|:-----:|:-----:| +| Pick source hues and roles | ✓ | | +| Generate 55-token shade scales | | ✓ | +| Validate WCAG AA contrast pairs | | ✓ | +| Render and screenshot palette | | ✓ | +| Evaluate visual quality and harmony | ✓ | | +| Provide perceptual feedback | ✓ | | +| Regenerate from adjusted parameters | | ✓ | + +This is [Lesson 3](/methodology/lesson-3-high-level-methodology)'s "iterate back to research as needed" made concrete. The cycle isn't linear — a perceptual issue in Validate might send you back to Plan (different source hue) or even Research (what chroma range do competitors use for warnings?). The four phases are a cycle, not a waterfall. + +### Theming as Validation + +Theme switching is another validation axis. The palette must hold up under inversion. + +**Light/dark themes.** Same semantic token names, different primitive references — the Light/Dark columns in the semantic mapping table above. A theme is a complete primitive→semantic mapping. The agent generates all shade scales from the source hues, and components never change. + +**Custom themes / user-provided.** Token override layer. Feed a customer's brand color through the same derivation pipeline — the agent generates a full palette and validates it passes contrast checks (A-010). White-label skins and branded portals, once expensive custom design work, become a single prompt execution. + +**Platform adaptations.** Scale tokens per device class (mobile/tablet/desktop). Base unit changes; proportional relationships stay constant. + +## Key Takeaways + +- **The four-phase workflow applies to experience engineering** — Research grounds color decisions in competitors and standards, Plan captures human judgment (source hues), Execute generates the math (55 primitives + semantic mapping), Validate closes the perceptual loop. Same [Lesson 3](/methodology/lesson-3-high-level-methodology) cycle, different domain. + +- **Visual quality is the human's job; contrast math is the agent's** — the VT perceptual floor means agents can screenshot what they can't evaluate. You provide the feedback that drives iteration; the agent regenerates and re-validates instantly. + +- **Brand colors are design judgment; everything derivative is computable math** — shade scales, harmony colors, dark mode variants, and contrast validation are all derived by agent-written code from a handful of source hues. + +- **Three-tier architecture enables theme switching without component changes** — primitives hold raw values, semantic tokens assign meaning, component tokens scope to specific elements. A theme swap changes only the primitive→semantic mapping. + +- **OKLCH over HSL** — perceptually uniform color space produces accurate shade scales. HSL's non-uniform lightness makes "identical" lightness values appear drastically different across hues. + +- **Every semantic background needs a paired foreground token** — without explicit `on-primary`, `on-error` pairs with guaranteed contrast, agents pick arbitrary text colors and contrast breaks silently. + +- **Token assumptions table creates traceability** — the Drives column maps each design decision to the spec constraints it affects, telling you and the agent exactly what to re-verify when a token changes. + +- **Design tokens are architectural constraints** — they affect every component and must be specified upfront. Retrofitting tokens is an order of magnitude harder than building them in. [Lesson 15](/experience-engineering/lesson-15-ui-specs) builds components that consume these tokens. diff --git a/website/docs/experience-engineering/lesson-15-ui-specs.md b/website/docs/experience-engineering/lesson-15-ui-specs.md new file mode 100644 index 0000000..a1d3c92 --- /dev/null +++ b/website/docs/experience-engineering/lesson-15-ui-specs.md @@ -0,0 +1,195 @@ +--- +sidebar_position: 2 +sidebar_label: 'UI Specs' +sidebar_custom_props: + sectionNumber: 15 +title: 'UI Specs — Components, Flows, and State' +--- + +With design tokens established in [Lesson 14](/experience-engineering/lesson-14-design-tokens), this lesson defines the components, flows, and state transitions that consume them. These three sections are the minimum viable UI spec — enough to generate a first pass. The rest emerges as the code pulls depth from you. + +The running example: a **team dashboard for a SaaS billing product** — subscription plans, usage metrics, invoices, team management, and settings. Rich enough to exercise components, flows, state, responsive layouts, and view states. + +## User Story & Success Metrics + +**Why before what.** Every UI spec starts with the user story and measurable KPIs — the stopping condition for iteration. + +| Element | Example | +|---------|---------| +| Problem | "Team admins can't understand billing status at a glance" | +| Success metric | Task completion rate > 90%, time-to-insight < 5s | +| Failure metric | Rage clicks > 2%, abandonment rate > 15% | + +KPI categories: + +| Category | Examples | Measured By | +|----------|----------|-------------| +| **Efficiency** | Task completion time, clicks-to-goal | Analytics events, session recordings | +| **Effectiveness** | Success rate, error rate | Form submissions, retry counts | +| **Satisfaction** | Rage clicks, abandonment, NPS | Heatmaps, exit rate, surveys | + +Accessibility and internationalization KPIs (WCAG compliance, locale coverage, RTL correctness) live in their respective architecture sections in [Lesson 16](/experience-engineering/lesson-16-accessibility-i18n) — they're architectural constraints, not standalone metrics. + +Metrics are the completion test. When the agent's implementation meets these thresholds as measured by browser automation, iteration stops. + +## Components: Props, Responsibilities, and Rules + +Components are units of UI responsibility with explicit APIs. Props define what a component **accepts**. Rendered output defines what it **guarantees**. Accessibility attributes are **non-negotiable** — always present, never conditional. + +Throughout the Experience Engineering lessons and the [UI spec template](/prompts/specifications/experience-spec-template), you'll see IDs like `A-001` or `L-003`. These are constraint IDs — labels you put in the spec that the agent carries into code comments during implementation, making rules grep-able and verifiable across the codebase. For the code-level mechanics, see [Lesson 11](/practical-techniques/lesson-11-agent-friendly-code#comments-as-context-engineering-critical-sections-for-agents). + +### Component Table + +| Component | Responsibility | Boundary | API | +|-----------|---------------|----------|-----| +| `SubscriptionCard` | Display plan name, price, status | Never fetches data directly | Props: `plan: Plan`, emits: `onUpgrade(planId)` | +| `UsageChart` | Render usage over time | No business logic | Props: `data: TimeSeriesPoint[]`, `period: DateRange` | +| `TeamMemberList` | List members with roles | No direct API calls | Props: `members: Member[]`, emits: `onRoleChange`, `onRemove` | +| `InvoiceTable` | Display invoices with sort/filter | No direct API calls | Props: `invoices: Invoice[]`, emits: `onDownload(id)` | +| `SettingsForm` | Edit user/team settings | No direct persistence | Props: `settings: Settings`, emits: `onSave(settings)` | + +All text-bearing components accept translation keys, never hardcoded strings (L-001). + +### Component Interfaces + +| Parent | Child | Interface | +|--------|-------|-----------| +| `DashboardPage` | `SubscriptionCard` | `plan: Plan` — expects: plan object has `name`, `price`, `status` | +| `SubscriptionCard` | `DashboardPage` | `onUpgrade(planId: string)` — fires only when user clicks upgrade CTA | +| Any component | Screen reader | `role`, `aria-label`, `aria-live` — non-negotiable: always present on interactive elements (A-001) | + +### Component Responsibilities + +- `SubscriptionCard` — NEVER imports from `TeamMemberList` or `UsageChart` +- `SubscriptionCard` — NEVER calls APIs directly — receives data via props +- Shared kernel: `src/components/primitives/` — buttons, inputs, typography (design token consumers) + +Responsibility leaks are the first thing to check when an agent's implementation diverges from spec. If the agent wired `SubscriptionCard` directly to an API, the component responsibility split is wrong — fix the spec or make it more explicit. + +### Component Libraries as Agent Context + +When building on an established component library (shadcn/ui, Radix, Ant Design, Material UI), the library already defines primitive behavior — accessible button, modal, tooltip, form controls. The spec only needs to define **domain-specific** behavior on top. + +Example: `SubscriptionCard` uses `` from shadcn/ui. The agent already knows Card's API from library documentation. Your spec defines the domain props (`plan: Plan`, `onUpgrade`) and responsibility rules, not how a card renders a shadow. + +Include component library references in your project's CLAUDE.md / AGENTS.md ([Lesson 6](/practical-techniques/lesson-6-project-onboarding)): import conventions, preferred primitives, and any overrides. This dramatically reduces spec surface and improves agent output because the agent operates within a constrained, well-documented design system. + +## Flows: Interaction Traces + +Flows trace a user's path through the interface step by step — what happens on success, what happens on failure, and what the user sees at each transition. Each flow step maps to a **component state transition** and an **API call**. + +### Flow: Upgrade Subscription + +``` +1. User clicks "Upgrade" on SubscriptionCard + → Show plan comparison modal + → Focus moves to modal heading (A-006) + → On cancel: close modal, return focus to trigger button (A-007), no state change + +2. User selects new plan, clicks "Confirm" + → Show loading state (skeleton in confirmation area) + → Call billing API: POST /subscriptions/upgrade + → On success: show success toast (announced via aria-live="polite", A-008), + update SubscriptionCard, close modal, return focus to SubscriptionCard + → On API error (4xx): show inline error in modal, keep modal open + → On network error: show retry banner, keep modal open + +3. Dashboard refreshes subscription data + → On success: metrics update in real-time + → On stale data: show "Refreshing..." indicator +``` + +### Flow: Team Member Management + +``` +1. Admin clicks "Add Member" in TeamMemberList + → Show invite form (email, role selector) + → Focus moves to email input (A-006) + → On cancel: close form, return focus to "Add Member" button (A-007) + +2. Admin enters email, selects role, clicks "Send Invite" + → Validate email format (client-side) + → On invalid: show inline validation error, keep form open, + announce error via aria-live="assertive" (A-009) + → Call team API: POST /team/invites + → On success: show success toast, add pending member to list + → On 409 (already invited): show inline error "Already invited" + → On 403 (insufficient permissions): show error, + should not reach here (CTA hidden for non-admins) +``` + +All user-visible strings in flows use translation keys. Pluralization follows ICU MessageFormat (L-003). + +### Behavioral Scenarios + +Each flow step above is a behavioral scenario waiting to happen. The [UI spec template](/prompts/specifications/experience-spec-template) includes a Given/When/Then table for converting flow steps into concrete, automatable test cases. At minimum, cover five edge categories: boundary values (min/max inputs), null/empty states (no data loaded), error propagation (API failures), concurrency (rapid clicks, simultaneous updates), and temporal edge cases (slow network, stale data). + +Agents handle the happy path correctly and miss the error and edge branches. Annotating each flow step with all three outcomes (success, expected error, unexpected error) prevents the agent from generating optimistic-only implementations. + +## The UI Stack: Five View States[^1] + +Every data-bound view has exactly five states. Omitting any is a bug: + +| State | What User Sees | Screen Reader Announcement | Constraint | +|-------|---------------|---------------------------|------------| +| **Ideal** | Data rendered correctly | Content available via landmarks and labels | Must match design spec | +| **Loading** | Skeleton/spinner | Container has `aria-busy="true"` (A-002) | Must appear within 100ms of trigger; skeleton preferred for layout stability | +| **Error** | Error message + recovery action | Error announced via `aria-live="assertive"` (A-003) | Must have actionable CTA (retry, contact support); never show raw error | +| **Empty** | Helpful empty state | Guidance text perceivable, not just visual (A-004) | Must guide user to action ("No invoices yet. Create your first invoice.") | +| **Partial** | Some data loaded, some failed | Failed section announced, rest navigable (A-005) | Must not block entire view for one failed section | + +### UI Stack Constraints + +| ID | Rule | Verified By | +|----|------|-------------| +| V-001 | NEVER show a blank screen — always show one of the five states | Browser automation: disconnect network mid-load, verify error state renders | +| V-002 | NEVER show raw error messages to users | Browser automation: trigger 500 from mock, verify user-friendly message | +| V-003 | ALWAYS show loading state within 100ms of async action | Browser automation: throttle network, measure time-to-skeleton | + +Agents consistently miss the **Partial** and **Empty** states. If the spec doesn't enumerate all five explicitly with examples, the agent will implement only Ideal and Loading — then the first empty dataset or partial API failure renders a blank screen. + +**Choosing a state model.** Pick one model per view entity, not per app. Three options: **Declarative** — you define the desired state, a reconciler (React, for example) figures out how to get there. Best for data display (tables, charts, read-only views). **State machine** — you enumerate every legal state and transition. Best for multi-step flows (forms, wizards, modals with distinct phases). **Event-driven** — you react to incoming events as they arrive. Best for real-time data (WebSocket feeds, collaborative editing, live notifications). For the system-level perspective on these models (event sourcing, CQRS, entity lifecycles), see [Lesson 13: State Models](/practical-techniques/lesson-13-systems-thinking-specs#state-models). + +## Layouts and Responsiveness + +Layouts define **how components are arranged** — the spatial structure of the page. + +### Layout Table + +| Layout | Structure | Breakpoints | Components | +|--------|-----------|-------------|------------| +| `DashboardGrid` | 12-col grid, sidebar + main | Desktop: sidebar+main, Tablet: stacked, Mobile: single column | `Sidebar`, `MetricsRow`, `ContentArea` | +| `SettingsLayout` | 2-col: nav + panel | Desktop: side-by-side, Mobile: nav→panel drill-down | `SettingsNav`, `SettingsPanel` | + +RTL layouts mirror the entire grid — sidebar switches sides, reading order reverses. Use logical properties (`inline-start`/`block-start`) for all spatial tokens (L-002). See [Lesson 16: Accessibility & Internationalization](/experience-engineering/lesson-16-accessibility-i18n#internationalization-architecture) for the full RTL model. + +### Responsiveness as Constraint + +| Breakpoint | Viewport | Layout Adaptation | Token Scale | +|------------|----------|-------------------|-------------| +| Mobile | < 768px | Single column, bottom nav | spacing.base = 8px | +| Tablet | 768–1024px | Collapsed sidebar, 2-col content | spacing.base = 8px | +| Desktop | > 1024px | Full sidebar, 3-col dashboard | spacing.base = 8px | +| Large | > 1440px | Max-width container, increased spacing | spacing.base = 10px | + +**Accessibility constraints:** Touch targets ≥ 44px on mobile (WCAG 2.5.5, A-012). Tab order must follow visual reading order per breakpoint (A-013). Zoom to 200% must not cause horizontal scroll (A-014). Animation duration scales down on `prefers-reduced-motion` preference. + +Agents implement desktop-first by default. If your layout spec requires mobile-first breakpoints, state this explicitly: "implement mobile layout first, then add tablet and desktop as progressive enhancements." Without this constraint, the agent generates desktop CSS and retrofits mobile as an afterthought — producing brittle media queries. + +The [UI spec template](/prompts/specifications/experience-spec-template) includes a **Performance Budget** section with Core Web Vitals targets (FCP, LCP, CLS, INP) and bundle size limits. Include these when performance is a constraint — particularly on mobile where bundle size and INP thresholds are tighter. + +## Key Takeaways + +- **UI specs define component APIs, not screens** — components, flows, and state transitions are the specification units, not mockups or wireframes. + +- **Start with three sections** — Components, Flows, and State are the minimum viable spec. Everything else is pulled by the code. + +- **Every view has five states** — ideal, loading, error, empty, partial (the UI Stack[^1]). Agents consistently miss empty and partial — enumerate all five explicitly, or the first empty dataset renders a blank screen. + +- **Component libraries are agent context** — reference library primitives by name in the spec; define only domain-specific behavior on top. Include library docs and import conventions in CLAUDE.md. + +- **Agents build desktop-first, happy-path-only by default** — UI specs exist to constrain these failure modes through explicit breakpoint tables, error state enumeration, and non-negotiable accessibility rules. + +--- + +[^1]: Hurff, Scott (2015) — ["Why Your User Interface Is Awkward: You're Ignoring the UI Stack"](https://www.scotthurff.com/posts/why-your-user-interface-is-awkward-youre-ignoring-the-ui-stack) — Introduced the five UI states (Ideal, Empty, Error, Partial, Loading) as a framework for designing complete interfaces. diff --git a/website/docs/experience-engineering/lesson-16-accessibility-i18n.md b/website/docs/experience-engineering/lesson-16-accessibility-i18n.md new file mode 100644 index 0000000..b0b0c3b --- /dev/null +++ b/website/docs/experience-engineering/lesson-16-accessibility-i18n.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 3 +sidebar_label: 'Accessibility & i18n' +sidebar_custom_props: + sectionNumber: 16 +title: 'Accessibility & Internationalization' +--- + +Accessibility and internationalization are architectural constraints on the components from [Lesson 15](/experience-engineering/lesson-15-ui-specs) — structural decisions that affect every component, flow, and state transition. Both are specified up front, not bolted on after implementation, and both are verifiable through `snapshot -ic` — the same tool that validates agent output. + +## Accessibility Architecture + +Accessibility is architecture, not a checklist. Landmark structure, keyboard model, focus management, and live region strategy are structural decisions that affect every component, flow, and state transition. They are specified up front, not bolted on after implementation. + +Five architectural decisions for every UI spec: + +| Decision | Question | Example | +|----------|----------|---------| +| Semantic HTML | Can a native element do this? Use it before reaching for ARIA — incorrect ARIA creates *more* errors than no ARIA | `` over `
`, ` - -
- {formatTime(currentTime)} - / - {formatTime(duration)} -
- - - -
- - + + + {formatTime(currentTime)} / {formatTime(duration)} + + +
+ {([1, 1.25, 1.5, 2] as const).map(rate => ( +
-
- -
- 🎙️ Listen to this lesson as a podcast + {rate}x + + ))}
); diff --git a/website/src/components/PresentationMode/PresentationToggle.module.css b/website/src/components/PresentationMode/PresentationToggle.module.css index 580b180..c491881 100644 --- a/website/src/components/PresentationMode/PresentationToggle.module.css +++ b/website/src/components/PresentationMode/PresentationToggle.module.css @@ -1,56 +1,42 @@ .toggleButton { display: inline-flex; align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - background: var(--ifm-color-primary); - color: white; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 600; + gap: var(--space-1); + height: var(--target-sm); /* 32px — tertiary action */ + padding: 0 var(--space-2); /* 16px horizontal */ + background: transparent; + color: var(--text-body); + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 500; cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: border-color 0.15s ease, color 0.15s ease; } .toggleButton:hover:not(:disabled) { - background: var(--ifm-color-primary-dark); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + border-color: var(--border-emphasis); + color: var(--text-heading); } .toggleButton:active:not(:disabled) { - transform: translateY(0); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + background: var(--surface-muted); } .toggleButton:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; } -.icon { - font-size: 1.2em; - line-height: 1; -} - -.label { - font-family: var(--ifm-font-family-base); -} - .spinner { display: inline-block; - animation: spin 1s linear infinite; + animation: spin 0.8s linear infinite; } @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } .errorMessage { @@ -59,32 +45,35 @@ left: 50%; transform: translate(-50%, -50%); z-index: 9999; - background: var(--ifm-background-color); - border: 2px solid var(--ifm-color-danger); - border-radius: 8px; - padding: 2rem; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); + background: var(--surface-page); + border: 2px solid var(--visual-error); + border-radius: var(--radius-sm); + padding: var(--space-4); max-width: 400px; text-align: center; } .errorMessage p { - color: var(--ifm-color-danger); - margin-bottom: 1rem; + color: var(--visual-error); + margin-bottom: var(--space-2); } .errorMessage button { - padding: 0.5rem 1rem; - background: var(--ifm-color-danger); - color: white; + height: var(--target-sm); + padding: 0 var(--space-2); + background: var(--visual-error); + color: #ffffff; border: none; - border-radius: 4px; - cursor: pointer; + border-radius: var(--radius-sm); + font-family: var(--font-body); + font-size: var(--text-sm); font-weight: 600; + cursor: pointer; + transition: background 0.15s ease; } .errorMessage button:hover { - background: var(--ifm-color-danger-dark); + background: color-mix(in srgb, var(--visual-error) 80%, black); } .loadingOverlay { @@ -94,11 +83,11 @@ right: 0; bottom: 0; z-index: 10000; - background: #000; + background: var(--surface-page); display: flex; align-items: center; justify-content: center; - color: white; - font-size: 1.5rem; + color: var(--text-heading); + font-size: var(--text-lg); font-weight: 600; } diff --git a/website/src/components/PresentationMode/PresentationToggle.tsx b/website/src/components/PresentationMode/PresentationToggle.tsx index 0f85e9e..cc34543 100644 --- a/website/src/components/PresentationMode/PresentationToggle.tsx +++ b/website/src/components/PresentationMode/PresentationToggle.tsx @@ -118,9 +118,20 @@ export default function PresentationToggle({ title="Toggle presentation mode (Cmd/Ctrl + Shift + P)" > {loading ? ( - + ) : ( - 🎭 + )} {loading ? 'Loading...' : 'Present'} diff --git a/website/src/components/PresentationMode/RevealSlideshow.module.css b/website/src/components/PresentationMode/RevealSlideshow.module.css index 6a109d2..a007fba 100644 --- a/website/src/components/PresentationMode/RevealSlideshow.module.css +++ b/website/src/components/PresentationMode/RevealSlideshow.module.css @@ -17,32 +17,30 @@ .closeButton { position: fixed; - top: 20px; - right: 20px; + top: var(--space-3); + right: var(--space-3); z-index: 10001; - width: 40px; - height: 40px; - border: 2px solid rgba(255, 255, 255, 0.5); - border-radius: 50%; - background: rgba(0, 0, 0, 0.7); - color: white; - font-size: 24px; + width: var(--target-md); + height: var(--target-md); + border: 2px solid var(--neutral-500); + border-radius: var(--radius-full); + background: var(--neutral-950); + color: #ffffff; + font-size: var(--text-xl); cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: all 0.2s ease; + transition: border-color 0.15s ease; } .closeButton:hover { - background: rgba(255, 255, 255, 0.2); - border-color: white; - transform: scale(1.1); + border-color: #ffffff; } .subtitle { font-size: clamp(1rem, 2vw, 1.2em); - color: #999; + color: var(--text-subtle); margin-top: 0.5em; margin-bottom: 1em; max-width: 90%; @@ -82,7 +80,7 @@ content: '→'; position: absolute; left: 0; - color: #42b883; + color: var(--semantic-success); } .bulletList { @@ -113,7 +111,7 @@ margin: 0 auto; max-width: var(--pres-component-max-width); border-radius: 8px; - background: #1e1e1e; + background: var(--code-bg); padding: 1.2em; text-align: left; overflow-y: auto; @@ -132,7 +130,7 @@ line-height: 1.3; margin: 0.5em auto 0 auto; border-radius: 6px; - background: #1e1e1e; + background: var(--code-bg); padding: 0.75em; text-align: left; overflow-y: auto; @@ -148,7 +146,7 @@ .caption { font-size: clamp(0.8rem, 1.5vw, 0.95em); line-height: 1.4; - color: #e0e0e0; + color: var(--neutral-200); margin-top: 0.5em; font-style: italic; font-weight: 500; @@ -192,24 +190,24 @@ } .comparisonLeft { - background: rgba(255, 100, 100, 0.1); - border: 2px solid rgba(255, 100, 100, 0.3); + background: color-mix(in srgb, var(--semantic-error) 10%, transparent); + border: 2px solid color-mix(in srgb, var(--semantic-error) 30%, transparent); } .comparisonRight { - background: rgba(100, 255, 100, 0.1); - border: 2px solid rgba(100, 255, 100, 0.3); + background: color-mix(in srgb, var(--semantic-success) 10%, transparent); + border: 2px solid color-mix(in srgb, var(--semantic-success) 30%, transparent); } .ineffective { - color: #ff6b6b; + color: var(--semantic-error); font-size: clamp(0.9rem, 1.8vw, 1em); margin-bottom: 0.3em; font-weight: 600; } .effective { - color: #51cf66; + color: var(--semantic-success); font-size: clamp(0.9rem, 1.8vw, 1em); margin-bottom: 0.3em; font-weight: 600; @@ -233,14 +231,14 @@ content: '✗'; position: absolute; left: 0; - color: #ff6b6b; + color: var(--semantic-error); } .comparisonRight li::before { content: '✓'; position: absolute; left: 0; - color: #51cf66; + color: var(--semantic-success); } /* Neutral comparison variant - uses primary color for both sides */ @@ -250,13 +248,13 @@ padding: clamp(0.7em, 1.5vw, 1em); border-radius: 8px; overflow-y: auto; - background: var(--visual-bg-workflow); - border: 2px solid rgba(124, 58, 237, 0.3); + background: var(--visual-bg-cyan); + border: 2px solid color-mix(in srgb, var(--brand-primary) 30%, transparent); } .reveal .neutralLeft, .reveal .neutralRight { - border-color: rgba(167, 139, 250, 0.3); + border-color: color-mix(in srgb, var(--brand-primary) 30%, transparent); } .neutralHeading { @@ -295,24 +293,24 @@ } .metaphorColumn { - background: rgba(124, 58, 237, 0.1); /* --brand-primary with alpha */ - border: 2px solid rgba(124, 58, 237, 0.3); + background: var(--visual-bg-cyan); + border: 2px solid color-mix(in srgb, var(--brand-primary) 30%, transparent); } .realityColumn { - background: rgba(109, 40, 217, 0.12); /* --brand-primary-dark with alpha */ - border: 2px solid rgba(109, 40, 217, 0.35); + background: color-mix(in srgb, var(--brand-primary-dark) 12%, transparent); + border: 2px solid color-mix(in srgb, var(--brand-primary-dark) 35%, transparent); } .metaphorHeading { - color: #8b5cf6; /* --brand-primary-light */ + color: var(--brand-primary-light); font-size: clamp(0.9rem, 1.8vw, 1em); margin-bottom: 0.3em; font-weight: 600; } .realityHeading { - color: #7c3aed; /* --brand-primary */ + color: var(--brand-primary); font-size: clamp(0.9rem, 1.8vw, 1em); margin-bottom: 0.3em; font-weight: 600; @@ -337,7 +335,7 @@ content: '→'; position: absolute; left: 0; - color: #8b5cf6; /* --brand-primary-light */ + color: var(--brand-primary-light); font-weight: 600; } @@ -414,7 +412,6 @@ .executionStep { margin: 0.4em 0; padding: 0.6em 1em; - border-radius: 6px; border-left: 4px solid transparent; background: rgba(255, 255, 255, 0.05); transition: all 0.3s ease; @@ -443,37 +440,37 @@ box-shadow: 0 2px 12px rgba(255, 255, 255, 0.2); } -/* LLM Prediction: Purple (brand primary) */ +/* LLM Prediction: Cyan (brand primary) */ .executionPrediction { - border-left-color: #8b5cf6 !important; - background: rgba(139, 92, 246, 0.08); + border-left-color: var(--brand-primary-light) !important; + background: color-mix(in srgb, var(--brand-primary-light) 8%, transparent); } .executionPrediction.fragment.current-fragment { - background: rgba(139, 92, 246, 0.15); - box-shadow: 0 2px 12px rgba(139, 92, 246, 0.4); + background: color-mix(in srgb, var(--brand-primary-light) 15%, transparent); + box-shadow: 0 2px 12px color-mix(in srgb, var(--brand-primary-light) 40%, transparent); } /* Agent Execution: Green (effective/positive) */ .executionExecution { - border-left-color: #51cf66 !important; - background: rgba(81, 207, 102, 0.08); + border-left-color: var(--semantic-success) !important; + background: color-mix(in srgb, var(--semantic-success) 8%, transparent); } .executionExecution.fragment.current-fragment { - background: rgba(81, 207, 102, 0.15); - box-shadow: 0 2px 12px rgba(81, 207, 102, 0.4); + background: color-mix(in srgb, var(--semantic-success) 15%, transparent); + box-shadow: 0 2px 12px color-mix(in srgb, var(--semantic-success) 40%, transparent); } -/* Feedback: Purple lighter (LLM context update) */ +/* Feedback: Cyan lighter (LLM context update) */ .executionFeedback { - border-left-color: #8b5cf6 !important; - background: rgba(139, 92, 246, 0.06); + border-left-color: var(--brand-primary-light) !important; + background: color-mix(in srgb, var(--brand-primary-light) 6%, transparent); } .executionFeedback.fragment.current-fragment { - background: rgba(139, 92, 246, 0.12); - box-shadow: 0 2px 12px rgba(139, 92, 246, 0.3); + background: color-mix(in srgb, var(--brand-primary-light) 12%, transparent); + box-shadow: 0 2px 12px color-mix(in srgb, var(--brand-primary-light) 30%, transparent); } /* Summary: neutral/default */ @@ -536,7 +533,7 @@ } .error { - color: #ff6b6b; + color: var(--semantic-error); font-size: 1.2em; padding: 2em; } diff --git a/website/src/components/ResourcesSection/index.module.css b/website/src/components/ResourcesSection/index.module.css new file mode 100644 index 0000000..da319bd --- /dev/null +++ b/website/src/components/ResourcesSection/index.module.css @@ -0,0 +1,138 @@ +/* ======================================================================== + SHARED SECTION LAYOUT + ======================================================================== */ + +.sectionTitle { + font-family: var(--font-display); + text-align: center; + font-size: var(--text-3xl); + line-height: var(--lh-2xl); + font-weight: 700; + margin-bottom: var(--space-1); +} + +.sectionSubtitle { + text-align: center; + font-family: var(--font-body); + font-size: var(--text-base); + line-height: var(--lh-sm); + color: var(--text-muted); + max-width: 66ch; + margin-inline: auto; + margin-bottom: var(--space-5); +} + +/* ======================================================================== + RESOURCES + ======================================================================== */ + +.resources { + padding: var(--space-6) 0; +} + +.resourceGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-3); + max-width: 1100px; + margin: 0 auto; +} + +.resourceCard { + display: flex; + flex-direction: column; + padding: var(--space-3); + background: var(--surface-raised); + border: 1px solid var(--border-default); + border-top: 3px solid transparent; + border-radius: var(--radius-md); + text-decoration: none; + color: inherit; + transition: border-color 0.15s ease; +} + +.resourceCard:hover { + border-color: var(--neutral-400); + text-decoration: none; +} + +.resourceCard:focus { + outline: 3px solid var(--text-heading); + outline-offset: 2px; +} + +/* Semantic top accent borders */ +.resource-magenta { border-top-color: var(--visual-magenta); } +.resource-cyan { border-top-color: var(--visual-cyan); } +.resource-indigo { border-top-color: var(--visual-indigo); } +.resource-success { border-top-color: var(--visual-success); } + +.resourceIllustration { + width: 100%; + height: auto; + max-height: 80px; + margin-bottom: var(--space-2); +} + +.resourceTitle { + font-family: var(--font-display); + font-size: var(--text-lg); + line-height: var(--lh-lg); + font-weight: 600; + color: var(--text-heading); + margin: 0 0 var(--space-0h) 0; +} + +.resourceDesc { + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: var(--lh-sm); + color: var(--text-body); + margin: 0 0 var(--space-2) 0; +} + +.resourceFooter { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: auto; +} + +.resourceCount { + font-family: var(--font-mono-keyword); + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-muted); +} + +.resource-magenta .resourceCount { color: var(--visual-magenta); } +.resource-cyan .resourceCount { color: var(--visual-cyan); } +.resource-indigo .resourceCount { color: var(--visual-indigo); } +.resource-success .resourceCount { color: var(--visual-success); } + +.ghostLink { + color: var(--text-body); + font-weight: 500; + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} + +.ghostLink:hover { + color: var(--text-heading); + text-decoration-thickness: 2px; +} + +/* ======================================================================== + RESPONSIVE + ======================================================================== */ + +@media screen and (max-width: 768px) { + .resourceGrid { + grid-template-columns: 1fr; + } + + .sectionTitle { + font-size: var(--text-2xl); + } +} diff --git a/website/src/components/ResourcesSection/index.tsx b/website/src/components/ResourcesSection/index.tsx new file mode 100644 index 0000000..08cb9e3 --- /dev/null +++ b/website/src/components/ResourcesSection/index.tsx @@ -0,0 +1,595 @@ +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import Heading from '@theme/Heading'; +import promptsSidebars from '../../../sidebarsPrompts'; +import { superellipsePath } from '@site/src/utils/svgMath'; +import styles from './index.module.css'; + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +function countPrompts(): number { + let count = 0; + const traverse = (items: unknown[]) => { + for (const item of items) { + if (typeof item === 'string') { + count++; + } else if (item && typeof item === 'object' && 'type' in item) { + const obj = item as { type: string; items?: unknown[] }; + if (obj.type === 'category' && obj.items) { + count += obj.items.length; + } + } + } + }; + const sidebar = promptsSidebars.promptsSidebar as unknown[]; + traverse(sidebar.slice(1)); + return count; +} + +const PROMPT_COUNT = countPrompts(); + +// --------------------------------------------------------------------------- +// Illustrations +// --------------------------------------------------------------------------- + +/** + * Cascading squircle "cards" — Smooth Circuit family (n=3.5). + * Three overlapping squircle outlines fan rightward, front card has + * abstract text lines inside. Magenta accent on the front shape. + */ +function PromptIllustration() { + const n = 3.5; + const rx = 34, + ry = 24; + return ( + + + + + {/* Abstract text lines inside front card */} + + + + + ); +} + +/** + * Terminal window — Terminal Geometry family. + * Sharp-cornered frame with title-bar dots, chevron prompt, + * cursor line, and abstract output. Cyan accent on frame stroke. + */ +function ToolboxIllustration() { + return ( + + + + + + + {/* Chevron prompt */} + + + {/* Abstract output */} + + + + ); +} + +/** + * Code research — Smooth Circuit family. + * Layered code blocks with match highlights, dashed connectors + * leading to a magnifying glass. Indigo accent. + */ +function ChunkHoundIllustration() { + const n = 3.5; + return ( + + + + {/* Abstract text lines */} + + + + {/* Match highlights */} + + + {/* Dashed connectors to magnifying glass */} + + + {/* Magnifying glass */} + + + {/* Abstract text inside lens */} + + + + ); +} + +/** + * Converging research — Smooth Circuit family. + * Scattered source nodes with Bezier curves converging to a + * synthesis circle, then output arrow. Success accent. + */ +function ArguSeekIllustration() { + return ( + + {/* Search wave arcs */} + + + {/* Source node circles */} + + + + + + {/* Bezier curves to synthesis */} + + + + + + {/* Central synthesis circle */} + + {/* Abstract text inside synthesis */} + + + {/* Output arrow */} + + + {/* Result lines */} + + + + + ); +} + +// --------------------------------------------------------------------------- +// Section +// --------------------------------------------------------------------------- + +export default function ResourcesSection() { + return ( +
+
+ + Resources + +

+ {PROMPT_COUNT} prompts + tools + open source projects +

+
+ + + + Prompt Library + +

+ Production-ready prompts for testing, debugging, code review, and + specifications +

+
+ + {PROMPT_COUNT} prompts + + Browse Prompts → +
+ + + + + + Developer Toolbox + +

+ Curated CLI tools, coding agents, terminals, and MCP servers for + AI-first development +

+
+ 4 guides + Explore Tools → +
+ + + + + + ChunkHound + +

+ Semantic code research for large codebases. Don't search your + code — research it. +

+
+ 10K–1M+ LOC + Visit Site ↗ +
+
+ + + + + ArguSeek + +

+ Wide iterative research using search and LLM synthesis. Multiple + sources per query. +

+
+ + 12–100+ sources + + View Project ↗ +
+
+
+
+
+ ); +} diff --git a/website/src/components/SiteHero/index.module.css b/website/src/components/SiteHero/index.module.css new file mode 100644 index 0000000..468eef4 --- /dev/null +++ b/website/src/components/SiteHero/index.module.css @@ -0,0 +1,158 @@ +/* ======================================================================== + HERO + ======================================================================== */ + +.heroInner { + display: flex; + align-items: flex-start; + gap: var(--space-4); +} + +.heroContent { + flex: 1; + min-width: 0; +} + +/* ======================================================================== + ASIDE (testimonials) — flex sibling on desktop, block below content on mobile + ======================================================================== */ + +.heroAside { + width: 260px; + flex-shrink: 0; +} + +.testimonial { + margin: 0 0 var(--space-2); + padding: 0 0 0 var(--space-2); + border-left: 3px solid var(--border-subtle); + border-radius: 0; +} + +.testimonial:last-child { + margin-bottom: 0; +} + +.testimonialText { + font-family: var(--font-body); + font-style: italic; + color: var(--text-muted); + font-size: var(--text-sm); + line-height: var(--lh-sm); + margin: 0 0 var(--space-0h); +} + +.testimonialSource { + font-family: var(--font-body); + color: var(--text-muted); + font-size: var(--text-sm); + display: block; +} + +/* ======================================================================== + TITLE BLOCK + ======================================================================== */ + +.heroTitle { + font-family: var(--font-display); + font-size: var(--text-4xl); + font-weight: 700; + color: var(--text-heading); + margin-bottom: var(--space-2); + line-height: var(--lh-3xl); +} + +.heroSubtitle { + font-family: var(--font-body); + font-size: var(--text-lg); + color: var(--text-body); + margin-bottom: var(--space-1); + max-width: 66ch; + line-height: var(--lh-lg); +} + +/* ======================================================================== + METADATA LINE + ======================================================================== */ + +.heroMeta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-0h); + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--text-muted); + margin: 0; + margin-bottom: var(--space-3); +} + +.heroMetaLink { + color: var(--text-muted); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +.heroMetaLink:hover { + color: var(--text-body); + text-decoration-thickness: 2px; +} + +.metaSep { + color: var(--text-muted); +} + +/* Star uses warning/amber — the design system's H:70° rating color */ +.starIcon { + width: 12px; + height: 12px; + flex-shrink: 0; + color: var(--visual-warning); +} + +/* GitHub mark — filled silhouette, inherits text color via currentColor */ +.githubIcon { + width: 14px; + height: 14px; + flex-shrink: 0; + margin-right: var(--space-0h); +} + +/* ======================================================================== + RESPONSIVE + ======================================================================== */ + +@media screen and (max-width: 768px) { + .heroInner { + flex-direction: column; + gap: 0; + } + + .heroAside { + width: auto; + display: flex; + gap: var(--space-3); + } + + .testimonial { + flex: 1; + margin-bottom: 0; + } +} + +@media screen and (max-width: 576px) { + .hero { + padding: var(--space-4) 0 var(--space-3); + } + + .heroTitle { + font-size: var(--text-3xl); + line-height: var(--lh-2xl); + } + + .heroAside { + flex-direction: column; + gap: var(--space-2); + } +} diff --git a/website/src/components/SiteHero/index.tsx b/website/src/components/SiteHero/index.tsx new file mode 100644 index 0000000..5530384 --- /dev/null +++ b/website/src/components/SiteHero/index.tsx @@ -0,0 +1,178 @@ +import { type ReactNode, useEffect, useState } from 'react'; +import Heading from '@theme/Heading'; +import LessonAudioPlayer from '@site/src/components/LessonAudioPlayer'; +import styles from './index.module.css'; + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +function formatStarCount(count: number): string { + if (count < 1000) return count.toString(); + const formatted = (count / 1000).toFixed(1); + return formatted.replace(/\.0$/, '') + 'k'; +} + +function useGitHubStars(repo: string): number | null { + const [stars, setStars] = useState(() => { + if (typeof window === 'undefined') return null; + const cached = localStorage.getItem(`gh-stars-${repo}`); + if (cached) { + const { stars, timestamp } = JSON.parse(cached); + if (Date.now() - timestamp < 3600000) return stars; + } + return null; + }); + + useEffect(() => { + if (stars !== null) return; + fetch(`https://api.github.com/repos/${repo}`) + .then((res) => res.json()) + .then((data) => { + if (data.stargazers_count !== undefined) { + setStars(data.stargazers_count); + localStorage.setItem( + `gh-stars-${repo}`, + JSON.stringify({ stars: data.stargazers_count, timestamp: Date.now() }) + ); + } + }) + .catch(() => {}); + }, [repo, stars]); + + return stars; +} + +// --------------------------------------------------------------------------- +// Icons — Smooth Circuit style (fill: none, stroke: currentColor, round caps) +// --------------------------------------------------------------------------- + +function GitHubIcon() { + return ( + + ); +} + +// Star uses fill (rating convention); color comes from --visual-warning via CSS +function StarIcon() { + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- + +const testimonials = [ + '"I just finished studying this. Very useful and well organized"', + '"No CTO/startup-dev/tech-advisor chat has taken place in the past month without mentioning it"', + '"Thank you for not making another video course series"', + '"I was looking for something like a course or some sort of guidelines"', + '"Great work, this is amazing"', +]; + +// --------------------------------------------------------------------------- +// Hero +// --------------------------------------------------------------------------- + +export default function SiteHero(): ReactNode { + const courseStars = useGitHubStars('agenticoding/agenticoding.github.io'); + const chunkHoundStars = useGitHubStars('chunkhound/chunkhound'); + const arguSeekStars = useGitHubStars('ArguSeek/arguseek'); + + // Hydration-safe random pair: [0,1] on SSR, random distinct pair after mount + const [pair, setPair] = useState<[number, number]>([0, 1]); + useEffect(() => { + const first = Math.floor(Math.random() * testimonials.length); + let second = Math.floor(Math.random() * (testimonials.length - 1)); + if (second >= first) second++; + setPair([first, second]); + }, []); + + return ( +
+
+ + Master Agentic Coding + +

+ Structured methodology proven on enterprise mono-repos with millions + of lines of code +

+ +

+ + + Open Source + + · + MIT + {courseStars !== null && ( + <> + · + {formatStarCount(courseStars)} + + )} + {chunkHoundStars !== null && ( + <> + · + + ChunkHound + + {formatStarCount(chunkHoundStars)} + + )} + {arguSeekStars !== null && ( + <> + · + + ArguSeek + + {formatStarCount(arguSeekStars)} + + )} +

+ +
+ + +
+ ); +} diff --git a/website/src/components/VisualElements/AbstractShapesVisualization.module.css b/website/src/components/VisualElements/AbstractShapesVisualization.module.css index e96a815..ddbf8e0 100644 --- a/website/src/components/VisualElements/AbstractShapesVisualization.module.css +++ b/website/src/components/VisualElements/AbstractShapesVisualization.module.css @@ -2,8 +2,8 @@ margin: 2rem 0; padding: 2rem 1.5rem; border-radius: 8px; - background: var(--visual-bg-decision); - border: 1px solid var(--visual-decision); + background: var(--visual-bg-indigo); + border: 1px solid var(--visual-indigo); } .container.compact { @@ -52,14 +52,14 @@ font-size: 2.5rem; line-height: 1; letter-spacing: 0.3rem; - color: var(--visual-limitation); + color: var(--visual-warning); font-weight: 300; } .cleanShape { font-size: 4rem; line-height: 1; - color: var(--visual-capability); + color: var(--visual-success); font-weight: 300; } diff --git a/website/src/components/VisualElements/ActorNodes.tsx b/website/src/components/VisualElements/ActorNodes.tsx new file mode 100644 index 0000000..2d392a0 --- /dev/null +++ b/website/src/components/VisualElements/ActorNodes.tsx @@ -0,0 +1,215 @@ +import React from 'react'; + +// Computed via scripts/compute-actor-coords.js + +type ActorSize = 40 | 32 | 14; + +export interface OperatorNodeProps { x: number; y: number; size?: ActorSize; } +export interface AgentNodeProps { x: number; y: number; size?: ActorSize; } +export interface PromptBubbleProps { x: number; y: number; showCursor?: boolean; } + +// ── OperatorNode S=40 ── +const OP_40 = { + headCx: 20, headCy: 9, headR: 8, + bodyPath: 'M 2 40 C 2 29.65, 12 17, 20 17 C 28 17, 38 29.65, 38 40 Z', +} as const; + +// ── OperatorNode S=32 ── +const OP_32 = { + headCx: 16, headCy: 7.2, headR: 6.4, + bodyPath: 'M 1.6 32 C 1.6 23.72, 9.6 13.6, 16 13.6 C 22.4 13.6, 30.4 23.72, 30.4 32 Z', +} as const; + +// ── AgentNode S=40 ── +const AGENT_40 = { + headX: 3, headY: 3, headW: 34, headH: 34, headRx: 8.5, + eyeR: 3, eyeY: 15.92, eyeLx: 12.52, eyeRx: 27.48, + mouthX: 7.08, mouthY: 25.1, mouthW: 25.84, mouthH: 5.44, + dividers: [15.61, 24.13] as const, +} as const; + +// ── AgentNode S=32 ── +const AGENT_32 = { + headX: 2.4, headY: 2.4, headW: 27.2, headH: 27.2, headRx: 6.8, + eyeR: 2.4, eyeY: 12.74, eyeLx: 10.02, eyeRx: 21.98, + mouthX: 5.66, mouthY: 20.08, mouthW: 20.67, mouthH: 4.35, + dividers: [12.48, 19.3] as const, +} as const; + +// ── AgentNode S=14 ── +const AGENT_14 = { + headX: 1.05, headY: 1.05, headW: 11.9, headH: 11.9, headRx: 2.98, + eyeR: 1.05, eyeY: 5.57, eyeLx: 4.38, eyeRx: 9.62, + mouthX: 2.48, mouthY: 8.79, mouthW: 9.04, mouthH: 1.9, + dividers: [5.46, 8.45] as const, +} as const; + +// ── PromptBubble (40×18 body, same card shape as TravelingPromptCard) ── +// Cursor bar: active authoring signal. +// All coords computed via scripts/compute-actor-coords.js +const BUBBLE_GEOM = { + W: 40, H: 18, rx: 9, + stubs: [ + { x: 8, y: 6, w: 20, h: 2, rx: 1 }, + { x: 8, y: 11, w: 14, h: 2, rx: 1 }, + ] as const, + cursor: { x: 23, y: 9, w: 2, h: 6, rx: 1 }, +} as const; + +// ── IdeaIcon S=32 ── +// All coords computed via scripts/compute-actor-coords.js +const IDEA_32 = { + globeR: 12, globeCx: 16, globeCy: 14, + capX: 10, capY: 27, capW: 12, capH: 4, capRx: 2, +} as const; + +// ── TravelingPromptCard (36×20, centered at 0,0 for animateMotion origin) ── +// All coords computed via scripts/compute-actor-coords.js +const TCARD_GEOM = { + W: 36, H: 20, rx: 8, bodyX: -18, bodyY: -10, + stubs: [ + { x: -9, y: -4, w: 18, h: 2, rx: 1 }, + { x: -6, y: 2, w: 12, h: 2, rx: 1 }, + ] as const, +} as const; + +// ── IdeaIcon ───────────────────────────────────────────────────────────────── +// Shape essence traced from 💡 (Lightbulb emoji). +// Globe: smooth circle (Smooth Circuit). Base cap: slight rx (positive valence). +// Represents pre-prompt idea — uses indigo to signal proto-prompt continuity. +export function IdeaIcon({ x, y }: { x: number; y: number }) { + const g = IDEA_32; + return ( + + + + + ); +} + +// ── OperatorNode ───────────────────────────────────────────────────────────── +// Shape essence traced from 🧑 (gender-free Person emoji). +// Body: smooth cubic Bezier bust silhouette, Smooth Circuit throughout. +// Head: circle sits flush on the neck point; rendered last to overlay the body top. +export function OperatorNode({ x, y, size = 40 }: OperatorNodeProps) { + const g = size === 40 ? OP_40 : OP_32; + const sw = size === 40 ? 2 : 1.5; + return ( + + + + + ); +} + +// ── AgentNode ───────────────────────────────────────────────────────────────── +// Head squircle: Smooth Circuit. Eyes: solid dots. Mouth: Terminal Geometry. +export function AgentNode({ x, y, size = 40 }: AgentNodeProps) { + const g = size === 40 ? AGENT_40 : size === 32 ? AGENT_32 : AGENT_14; + const sw = size === 40 ? 2 : size === 32 ? 1.5 : 1; + const mouthSw = size === 14 ? 0.75 : 1; + const mouthRx = size === 14 ? 1 : 2; + return ( + + + + + + {g.dividers.map((dx, i) => ( + + ))} + + ); +} + +// ── PromptBubble ────────────────────────────────────────────────────────────── +// Body: Smooth Circuit (rx=9). Matches TravelingPromptCard shape (no tail). +// 2 text stubs + optional cursor bar signal message-in-composition. +export function PromptBubble({ x, y, showCursor = true }: PromptBubbleProps) { + const g = BUBBLE_GEOM; + return ( + + + {g.stubs.map((s, i) => ( + + ))} + {showCursor && ( + + )} + + ); +} + +// ── PromptIcon ──────────────────────────────────────────────────────────────── +// Unified prompt shape — same geometry as TravelingPromptCard, centered at 0,0. +// No embedded animateMotion; parent applies scroll-driven transform. +export function PromptIcon({ className }: { className?: string }) { + const g = TCARD_GEOM; + return ( + + + {g.stubs.map((s, i) => ( + + ))} + + ); +} + +// ── TravelingPromptCard ─────────────────────────────────────────────────────── +// (0,0) is the animateMotion anchor. Parent supplies in . +// begin defaults to "indefinite" — caller triggers via motionRef.current.beginElement(). +// rotate defaults to "0" — prompt cards are artifacts that don't tilt (Smooth Circuit). +export interface TravelingPromptCardProps { + pathId: string; + dur?: string; // default "0.4s" + motionRef?: React.RefObject; + className?: string; + style?: React.CSSProperties; + rotate?: string; // "0" | "auto-reverse" — default "0" +} + +export function TravelingPromptCard({ pathId, dur = '0.4s', motionRef, className, style, rotate }: TravelingPromptCardProps) { + const g = TCARD_GEOM; + return ( + + + + + + {g.stubs.map((s, i) => ( + + ))} + + ); +} diff --git a/website/src/components/VisualElements/AdoptionGapDiagram.tsx b/website/src/components/VisualElements/AdoptionGapDiagram.tsx new file mode 100644 index 0000000..5536033 --- /dev/null +++ b/website/src/components/VisualElements/AdoptionGapDiagram.tsx @@ -0,0 +1,146 @@ +import React from 'react'; + +interface Props { + compact?: boolean; +} + +export default function AdoptionGapDiagram({ compact = false }: Props) { + const viewBox = compact ? '0 0 560 200' : '0 0 560 210'; + + return ( + + {/* Baseline */} + + + {/* Adoption bar (77%) — x=100 y=20 width=48 height=150 */} + + + {/* Trust bar (3%) — x=400 y=162 width=48 height=8 */} + + + {/* Gap connector — polyline from adoption bar midpoint to trust bar midpoint */} + + + {/* Gap diamond marker on the vertical segment */} + + + {/* Gap label */} + + The Gap + + + {/* Adoption bar — percentage label above */} + + 77% + + + {/* Adoption bar — category label below baseline */} + + Adoption + + + {/* Trust bar — percentage label above */} + + 3% + + + {/* Trust bar — category label below baseline */} + + Trust + + + ); +} diff --git a/website/src/components/VisualElements/BulletIcons.module.css b/website/src/components/VisualElements/BulletIcons.module.css new file mode 100644 index 0000000..39534fc --- /dev/null +++ b/website/src/components/VisualElements/BulletIcons.module.css @@ -0,0 +1,48 @@ +.list { + display: flex; + flex-direction: column; + gap: var(--space-3); + margin: var(--space-3) 0; +} + +.item { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--space-2); + opacity: 0.4; + transform: translateY(6px); + will-change: opacity, transform; +} + +@media (prefers-reduced-motion: reduce) { + .item { + opacity: 1 !important; + transform: none !important; + will-change: auto; + } +} + +.iconWrapper { + flex-shrink: 0; + line-height: 0; +} + +.text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + font-weight: 600; + font-size: 1rem; + color: var(--text-heading); + line-height: 1.25; +} + +.description { + font-size: var(--text-sm); + color: var(--text-body); + line-height: 1.5; +} diff --git a/website/src/components/VisualElements/BulletIcons.tsx b/website/src/components/VisualElements/BulletIcons.tsx new file mode 100644 index 0000000..ab6f074 --- /dev/null +++ b/website/src/components/VisualElements/BulletIcons.tsx @@ -0,0 +1,97 @@ +import React, { useRef, useEffect } from 'react'; +import styles from './BulletIcons.module.css'; +import { useAnimationPhase } from '../animations/ScrollDrivenFigure'; +import ClockIcon from './icons/ClockIcon'; +import BugIcon from './icons/BugIcon'; +import ReviewLensIcon from './icons/ReviewLensIcon'; +import PlanExecuteIcon from './icons/PlanExecuteIcon'; +import ArchitectIcon from './icons/ArchitectIcon'; +import PencilIcon from './icons/PencilIcon'; + +interface Bullet { + icon: React.ComponentType<{ className?: string; size?: number }>; + hue: string; + label: string; + description: string; +} + +const BULLETS: Bullet[] = [ + { + icon: ClockIcon, + hue: 'var(--visual-indigo)', + label: 'Onboard to unfamiliar codebases', + description: '5–10× faster using systematic agentic research and grounding', + }, + { + icon: BugIcon, + hue: 'var(--visual-warning)', + label: 'Debug production issues', + description: 'by delegating log analysis, root cause investigation, and diagnostic scripts to agents', + }, + { + icon: ReviewLensIcon, + hue: 'var(--visual-success)', + label: 'Review code without confirmation bias', + description: '— fresh context, evidence-based validation, no line-by-line slog', + }, + { + icon: PlanExecuteIcon, + hue: 'var(--visual-violet)', + label: 'Plan and execute features', + description: 'with parallel sub-agents across isolated branches and contexts', + }, + { + icon: ArchitectIcon, + hue: 'var(--visual-cyan)', + label: 'Architect agent-friendly codebases', + description: 'where constraints are co-located with code and good patterns compound', + }, + { + icon: PencilIcon, + hue: 'var(--visual-magenta)', + label: 'Generate complete design systems', + description: 'from computed first principles — brand palettes, typography scales, spatial grids, and illustrations derived by code rather than guessed', + }, +]; + +const STAGGER = 0.08; +const ITEM_SPAN = 0.25; +const BASE_OPACITY = 0.4; +const OFFSET_Y = 6; + +export default function BulletIcons() { + const phase = useAnimationPhase(); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + BULLETS.forEach((_, i) => { + const el = itemRefs.current[i]; + if (!el) return; + const start = i * STAGGER; + const t = Math.min(Math.max((phase - start) / ITEM_SPAN, 0), 1); + el.style.opacity = String(BASE_OPACITY + t * (1 - BASE_OPACITY)); + el.style.transform = `translateY(${OFFSET_Y * (1 - t)}px)`; + }); + }, [phase]); + + return ( +
+ {BULLETS.map(({ icon: Icon, hue, label, description }, i) => ( +
{ itemRefs.current[i] = el; }} + > +
+
+
+ {label} + {description} +
+
+ ))} +
+ ); +} diff --git a/website/src/components/VisualElements/CapabilityMatrix.module.css b/website/src/components/VisualElements/CapabilityMatrix.module.css index 76d4acf..12ec3d1 100644 --- a/website/src/components/VisualElements/CapabilityMatrix.module.css +++ b/website/src/components/VisualElements/CapabilityMatrix.module.css @@ -2,8 +2,8 @@ margin: 2rem 0; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-capability); - border: 1px solid var(--visual-capability); + background: var(--visual-bg-success); + border: 1px solid var(--visual-success); } /* Compact mode for presentations - maximize table size */ @@ -36,11 +36,11 @@ } .compactBadge.high { - border-color: var(--visual-capability); + border-color: var(--visual-success); } .compactBadge.medium { - border-color: var(--visual-limitation); + border-color: var(--visual-warning); } .compactBadge.low { @@ -58,11 +58,11 @@ } .compactBadge.high .compactLabel { - color: var(--visual-capability); + color: var(--visual-success); } .compactBadge.medium .compactLabel { - color: var(--visual-limitation); + color: var(--visual-warning); } .compactBadge.low .compactLabel { diff --git a/website/src/components/VisualElements/CapabilityMatrix.tsx b/website/src/components/VisualElements/CapabilityMatrix.tsx index d4c0df2..422296b 100644 --- a/website/src/components/VisualElements/CapabilityMatrix.tsx +++ b/website/src/components/VisualElements/CapabilityMatrix.tsx @@ -52,10 +52,10 @@ function getTrustIndicator(level: 'high' | 'medium' | 'low'): { return { icon: '✅', label: 'Reliable', - color: 'var(--visual-capability)', + color: 'var(--visual-success)', }; case 'medium': - return { icon: '⚠️', label: 'Verify', color: 'var(--visual-limitation)' }; + return { icon: '⚠️', label: 'Verify', color: 'var(--visual-warning)' }; case 'low': return { icon: '❌', label: 'Check docs', color: 'var(--visual-error)' }; } diff --git a/website/src/components/VisualElements/ColorPaletteGenerator.module.css b/website/src/components/VisualElements/ColorPaletteGenerator.module.css new file mode 100644 index 0000000..176312b --- /dev/null +++ b/website/src/components/VisualElements/ColorPaletteGenerator.module.css @@ -0,0 +1,311 @@ +.container { + margin: 2rem 0; + padding: 1.5rem; + border-radius: 8px; + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); +} + +.container.compact { + margin: 0 auto; + padding: 0.5rem; + max-width: 95%; +} + +.header { + margin-bottom: 1rem; +} + +.title { + margin: 0 0 0.25rem; + font-size: 1.1rem; + font-weight: 600; + color: var(--ifm-font-color-base); +} + +.subtitle { + margin: 0; + font-size: 0.85rem; + font-family: var(--ifm-font-family-monospace); + color: var(--visual-neutral); +} + +/* Controls row */ +.controls { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: wrap; + margin-bottom: 1.25rem; +} + +.sliderGroup { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 200px; +} + +.sliderLabel { + font-size: 0.85rem; + font-weight: 500; + color: var(--ifm-font-color-base); + white-space: nowrap; +} + +.hueSlider { + -webkit-appearance: none; + appearance: none; + flex: 1; + height: 8px; + border-radius: 4px; + outline: none; + cursor: pointer; + background: linear-gradient( + to right, + oklch(0.6 0.15 0), + oklch(0.6 0.15 60), + oklch(0.6 0.15 120), + oklch(0.6 0.15 180), + oklch(0.6 0.15 240), + oklch(0.6 0.15 300), + oklch(0.6 0.15 360) + ); +} + +.hueSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--ifm-background-surface-color); + border: 2px solid var(--ifm-font-color-base); + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.hueSlider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--ifm-background-surface-color); + border: 2px solid var(--ifm-font-color-base); + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.hueValue { + font-size: 0.85rem; + font-weight: 600; + font-family: var(--ifm-font-family-monospace); + color: var(--visual-cyan); + min-width: 32px; + text-align: right; +} + +/* Button group for harmony selector */ +.buttonGroup { + display: flex; + gap: 0; + border-radius: var(--radius-sm); + overflow: hidden; + border: 1px solid var(--border-default); +} + +.harmonyButton { + height: var(--target-sm); + padding: 0 var(--space-2); + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 500; + border: none; + border-right: 1px solid var(--border-default); + background: transparent; + color: var(--text-heading); + cursor: pointer; + transition: background 0.15s ease; + white-space: nowrap; +} + +.harmonyButton:last-child { + border-right: none; +} + +.harmonyButton:hover:not(.harmonyButtonActive) { + background: var(--surface-muted); +} + +.harmonyButtonActive { + background: var(--text-heading); + color: var(--surface-page); +} + +/* Dark mode toggle */ +.toggleButton { + height: var(--target-sm); + padding: 0 var(--space-2); + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 500; + border: 1px solid var(--border-default); + background: transparent; + color: var(--text-heading); + border-radius: var(--radius-sm); + cursor: pointer; + transition: background 0.15s ease; + white-space: nowrap; +} + +.toggleButton:hover:not(.toggleButtonActive) { + background: var(--surface-muted); +} + +.toggleButtonActive { + background: var(--text-heading); + color: var(--surface-page); +} + +/* Section labels */ +.sectionLabel { + font-size: 0.85rem; + font-weight: 600; + color: var(--ifm-font-color-base); + margin: 1rem 0 0.5rem; +} + +/* Swatch row */ +.swatchRow { + display: flex; + gap: 2px; + margin-bottom: 0.75rem; +} + +.swatch { + flex: 1; + min-width: 0; + height: 80px; + border-radius: 4px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 4px 2px; + font-size: 0.65rem; + font-family: var(--ifm-font-family-monospace); + font-weight: 500; + line-height: 1.2; + overflow: hidden; + transition: background-color 0.2s ease; +} + +.swatchShade { + font-weight: 700; + font-size: 0.7rem; +} + +.swatchHex { + font-size: 0.6rem; + opacity: 0.9; +} + +.swatchContrast { + font-size: 0.55rem; + opacity: 0.85; +} + +.badge { + display: inline-block; + padding: 0px 3px; + border-radius: 2px; + font-size: 0.5rem; + font-weight: 700; + line-height: 1.4; +} + +.badgePass { + background: rgba(6, 182, 212, 0.25); +} + +.badgeFail { + background: rgba(225, 29, 72, 0.25); +} + +/* Harmony swatches row */ +.harmonyRow { + display: flex; + gap: 6px; + margin-bottom: 0.75rem; +} + +.harmonySwatch { + flex: 1; + height: 60px; + border-radius: 6px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + font-size: 0.75rem; + font-family: var(--ifm-font-family-monospace); + font-weight: 500; + transition: background-color 0.2s ease; +} + +.harmonyLabel { + font-size: 0.7rem; + font-weight: 700; +} + +.harmonyDegree { + font-size: 0.65rem; + opacity: 0.85; +} + +/* Responsive: stack swatches vertically on mobile */ +@media (max-width: 768px) { + .controls { + flex-direction: column; + align-items: stretch; + } + + .sliderGroup { + min-width: 100%; + } + + .buttonGroup { + width: 100%; + } + + .harmonyButton { + flex: 1; + text-align: center; + } + + .swatchRow { + flex-direction: column; + gap: 2px; + } + + .swatch { + flex-direction: row; + height: auto; + padding: 6px 10px; + justify-content: space-between; + gap: 8px; + } + + .harmonyRow { + flex-direction: column; + gap: 4px; + } + + .harmonySwatch { + flex-direction: row; + height: auto; + padding: 8px 12px; + gap: 8px; + } +} diff --git a/website/src/components/VisualElements/ColorPaletteGenerator.tsx b/website/src/components/VisualElements/ColorPaletteGenerator.tsx new file mode 100644 index 0000000..e5d2f1a --- /dev/null +++ b/website/src/components/VisualElements/ColorPaletteGenerator.tsx @@ -0,0 +1,257 @@ +import React, { useState } from 'react'; +import type { PresentationAwareProps } from '../PresentationMode/types'; +import styles from './ColorPaletteGenerator.module.css'; + +interface ColorPaletteGeneratorProps extends PresentationAwareProps {} + +type HarmonyMode = 'monochromatic' | 'analogous' | 'complementary' | 'triadic'; + +const SHADE_NAMES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const; + +// Non-linear lightness curve: L values for each shade stop +const LIGHTNESS_STOPS = [0.97, 0.93, 0.87, 0.78, 0.69, 0.60, 0.51, 0.43, 0.36, 0.29, 0.25]; + +const PEAK_CHROMA = 0.15; + +const HARMONY_OFFSETS: Record = { + monochromatic: [0], + analogous: [0, 30, -30], + complementary: [0, 180], + triadic: [0, 120, 240], +}; + +const HARMONY_LABELS: Record = { + monochromatic: 'Monochromatic', + analogous: 'Analogous', + complementary: 'Complementary', + triadic: 'Triadic', +}; + +// --- Color math utilities --- + +/** Compute chroma from lightness using a parabolic curve */ +function computeChroma(L: number): number { + const raw = PEAK_CHROMA * (1 - ((L - 0.6) / 0.5) ** 2); + return Math.max(0, Math.min(PEAK_CHROMA, raw)); +} + +/** OKLCH -> OKLab */ +function oklchToOklab(L: number, C: number, H: number): [number, number, number] { + const hRad = (H * Math.PI) / 180; + return [L, C * Math.cos(hRad), C * Math.sin(hRad)]; +} + +/** OKLab -> linear sRGB via LMS intermediate */ +function oklabToLinearSrgb(L: number, a: number, b: number): [number, number, number] { + // OKLab -> LMS (cube root space) + const l_ = L + 0.3963377774 * a + 0.2158037573 * b; + const m_ = L - 0.1055613458 * a - 0.0638541728 * b; + const s_ = L - 0.0894841775 * a - 1.2914855480 * b; + + // Cube to get LMS + const l = l_ * l_ * l_; + const m = m_ * m_ * m_; + const s = s_ * s_ * s_; + + // LMS -> linear sRGB + const r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; + const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; + const blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; + + return [r, g, blue]; +} + +/** Linear sRGB component -> sRGB gamma-corrected component */ +function linearToGamma(x: number): number { + if (x <= 0.0031308) return 12.92 * x; + return 1.055 * Math.pow(x, 1 / 2.4) - 0.055; +} + +/** Clamp a number to [0, 1] */ +function clamp01(x: number): number { + return Math.max(0, Math.min(1, x)); +} + +/** Convert a [0,1] float to 2-digit hex */ +function toHex2(x: number): string { + const val = Math.round(clamp01(x) * 255); + return val.toString(16).padStart(2, '0'); +} + +/** Full OKLCH -> hex conversion */ +function oklchToHex(L: number, C: number, H: number): string { + const [labL, labA, labB] = oklchToOklab(L, C, H); + const [lr, lg, lb] = oklabToLinearSrgb(labL, labA, labB); + const r = linearToGamma(clamp01(lr)); + const g = linearToGamma(clamp01(lg)); + const b = linearToGamma(clamp01(lb)); + return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`; +} + +/** Compute relative luminance from OKLCH (using linear RGB) */ +function relativeLuminance(L: number, C: number, H: number): number { + const [labL, labA, labB] = oklchToOklab(L, C, H); + const [lr, lg, lb] = oklabToLinearSrgb(labL, labA, labB); + return 0.2126 * clamp01(lr) + 0.7152 * clamp01(lg) + 0.0722 * clamp01(lb); +} + +/** WCAG contrast ratio between two relative luminance values */ +function contrastRatio(lum1: number, lum2: number): number { + const lighter = Math.max(lum1, lum2); + const darker = Math.min(lum1, lum2); + return (lighter + 0.05) / (darker + 0.05); +} + +/** Determine whether white or black text has higher contrast on the given background */ +function bestTextColor(bgLum: number): 'white' | 'black' { + const whiteContrast = contrastRatio(1.0, bgLum); + const blackContrast = contrastRatio(bgLum, 0.0); + return whiteContrast >= blackContrast ? 'white' : 'black'; +} + +/** Best contrast ratio (white or black) against a background luminance */ +function bestContrast(bgLum: number): number { + return Math.max(contrastRatio(1.0, bgLum), contrastRatio(bgLum, 0.0)); +} + +// --- Component --- + +export default function ColorPaletteGenerator({ + compact = false, +}: ColorPaletteGeneratorProps) { + const [hue, setHue] = useState(250); + const [harmony, setHarmony] = useState('monochromatic'); + const [darkMode, setDarkMode] = useState(false); + + const containerClassName = compact + ? `${styles.container} ${styles.compact}` + : styles.container; + + /** Get lightness for a shade index, respecting dark mode inversion */ + function getLightness(index: number): number { + if (darkMode) { + // Invert: shade 50 gets lightness of 950 and vice versa + return LIGHTNESS_STOPS[LIGHTNESS_STOPS.length - 1 - index]; + } + return LIGHTNESS_STOPS[index]; + } + + /** Build swatch data for a given hue */ + function buildShadeScale(h: number) { + return SHADE_NAMES.map((shade, i) => { + const L = getLightness(i); + const C = computeChroma(L); + const effectiveHue = ((h % 360) + 360) % 360; + const hex = oklchToHex(L, C, effectiveHue); + const lum = relativeLuminance(L, C, effectiveHue); + const textColor = bestTextColor(lum); + const ratio = bestContrast(lum); + const passAA = ratio >= 4.5; + return { shade, L, C, H: effectiveHue, hex, textColor, ratio, passAA }; + }); + } + + const shadeScale = buildShadeScale(hue); + + // Shade 500 lightness for harmony swatches (index 5) + const shade500L = getLightness(5); + const shade500C = computeChroma(shade500L); + + const harmonyOffsets = HARMONY_OFFSETS[harmony]; + const harmonySwatches = harmonyOffsets.map((offset) => { + const h = ((hue + offset) % 360 + 360) % 360; + const hex = oklchToHex(shade500L, shade500C, h); + const lum = relativeLuminance(shade500L, shade500C, h); + const textColor = bestTextColor(lum); + return { hue: h, offset, hex, textColor }; + }); + + const currentOklch = `oklch(${shade500L.toFixed(2)} ${shade500C.toFixed(3)} ${hue})`; + + return ( +
+
+

Color Palette Generator

+

{currentOklch}

+
+ +
+
+ Hue + setHue(Number(e.target.value))} + className={styles.hueSlider} + /> + {hue} +
+ +
+ {(Object.keys(HARMONY_LABELS) as HarmonyMode[]).map((mode) => ( + + ))} +
+ + +
+ +
+ Shade Scale {darkMode ? '(inverted)' : ''} +
+
+ {shadeScale.map(({ shade, hex, textColor, ratio, passAA }) => ( +
+ {shade} + {hex} + + {ratio.toFixed(1)}:1{' '} + + {passAA ? 'AA' : 'Fail'} + + +
+ ))} +
+ + {harmony !== 'monochromatic' && ( + <> +
+ {HARMONY_LABELS[harmony]} Harmony +
+
+ {harmonySwatches.map(({ hue: h, offset, hex, textColor }) => ( +
+ {h}° + + {offset === 0 ? 'Base' : `+${offset}°`} + +
+ ))} +
+ + )} +
+ ); +} diff --git a/website/src/components/VisualElements/CompoundQualityVisualization.module.css b/website/src/components/VisualElements/CompoundQualityVisualization.module.css index 6c7a3b4..12cdcf3 100644 --- a/website/src/components/VisualElements/CompoundQualityVisualization.module.css +++ b/website/src/components/VisualElements/CompoundQualityVisualization.module.css @@ -5,8 +5,8 @@ .container { margin: 2rem 0; border-radius: 8px; - background: var(--visual-bg-workflow); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); overflow: hidden; opacity: 0; animation: fadeInContainer 0.6s ease-out forwards; @@ -98,7 +98,7 @@ } .sideLabelPositive { - color: var(--visual-capability); + color: var(--visual-success); } .sideLabelNegative { @@ -156,7 +156,7 @@ /* Arrow markers */ .arrowMarkerPositive polygon { - fill: var(--visual-capability); + fill: var(--visual-success); } .arrowMarkerNegative polygon { @@ -196,7 +196,7 @@ } .iterationMarkerPositive { - fill: var(--visual-capability); + fill: var(--visual-success); stroke: var(--ifm-background-color); stroke-width: 2; } @@ -250,27 +250,26 @@ .circuitBreakerBox { margin: 0.5rem 1.5rem 1.5rem 1.5rem; padding: 1rem 1.25rem; - background: rgba(124, 58, 237, 0.05); - border-left: 4px solid var(--visual-workflow); - border-radius: 6px; + background: color-mix(in srgb, var(--visual-cyan) 5%, transparent); + border-left: 4px solid var(--visual-cyan); font-size: 0.9rem; line-height: 1.6; color: var(--ifm-color-emphasis-800); } .circuitBreakerBox strong { - color: var(--visual-workflow); + color: var(--visual-cyan); font-weight: 700; } .circuitBreakerBox a { - color: var(--visual-workflow); + color: var(--visual-cyan); text-decoration: underline; font-weight: 500; } .circuitBreakerBox a:hover { - color: var(--brand-primary-dark); + color: var(--visual-cyan); } /* ======================================================================== @@ -310,16 +309,16 @@ ======================================================================== */ [data-theme='dark'] .circuitBreakerBox { - background: rgba(167, 139, 250, 0.08); + background: color-mix(in srgb, var(--visual-cyan) 8%, transparent); color: var(--ifm-color-emphasis-700); } [data-theme='dark'] .circuitBreakerBox a { - color: var(--brand-primary-light); + color: var(--visual-cyan); } [data-theme='dark'] .circuitBreakerBox a:hover { - color: var(--brand-primary-lighter); + color: var(--visual-cyan); } /* ======================================================================== diff --git a/website/src/components/VisualElements/CompoundQualityVisualization.tsx b/website/src/components/VisualElements/CompoundQualityVisualization.tsx index c89dd4b..19e7a6d 100644 --- a/website/src/components/VisualElements/CompoundQualityVisualization.tsx +++ b/website/src/components/VisualElements/CompoundQualityVisualization.tsx @@ -119,12 +119,12 @@ export default function CompoundQualityVisualization({ >
@@ -149,12 +149,12 @@ export default function CompoundQualityVisualization({ @@ -180,7 +180,7 @@ export default function CompoundQualityVisualization({ > POSITIVE FEEDBACK @@ -318,7 +318,7 @@ export default function CompoundQualityVisualization({ cy={iter.y} r="6" className={styles.iterationMarkerPositive} - fill="var(--visual-capability)" + fill="var(--visual-success)" /> Your Role as Circuit Breaker: Code review ( - + Lesson 9 ) is where you intervene. Accepting bad patterns lets them enter the diff --git a/website/src/components/VisualElements/ContextWindowMeter.module.css b/website/src/components/VisualElements/ContextWindowMeter.module.css index 9b51c36..f9c3ab0 100644 --- a/website/src/components/VisualElements/ContextWindowMeter.module.css +++ b/website/src/components/VisualElements/ContextWindowMeter.module.css @@ -2,8 +2,8 @@ margin: 2rem 0; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-workflow); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); } /* Compact mode for presentations - maximize content area */ @@ -31,7 +31,7 @@ font-size: 0.9rem; font-weight: 500; font-family: var(--ifm-font-family-monospace); - color: var(--visual-workflow); + color: var(--visual-cyan); } .meterContainer { @@ -57,15 +57,15 @@ .meterFillNormal { background: linear-gradient( 90deg, - var(--visual-capability), - var(--visual-workflow) + var(--visual-success), + var(--visual-cyan) ); } .meterFillWarning { background: linear-gradient( 90deg, - var(--visual-limitation), + var(--visual-warning), var(--gradient-warning-end) ); } @@ -106,22 +106,24 @@ .button { flex: 1; min-width: 140px; - padding: 0.5rem 1rem; - font-size: 0.9rem; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--target-sm); + padding: 0 var(--space-2); + font-family: var(--font-body); + font-size: var(--text-sm); font-weight: 500; - border: 1px solid var(--visual-workflow); - background: var(--ifm-background-surface-color); - color: var(--visual-workflow); - border-radius: 6px; + border: 1px solid var(--border-default); + background: transparent; + color: var(--text-heading); + border-radius: var(--radius-sm); cursor: pointer; - transition: all 0.2s ease; + transition: border-color 0.15s ease; } .button:hover:not(:disabled) { - background: var(--visual-workflow); - color: white; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(124, 58, 237, 0.3); + border-color: var(--neutral-400); } .button:disabled { @@ -130,14 +132,12 @@ } .buttonReset { - border-color: var(--visual-neutral); - color: var(--visual-neutral); + border-color: var(--neutral-400); + color: var(--text-muted); } .buttonReset:hover:not(:disabled) { - background: var(--visual-neutral); - color: white; - box-shadow: 0 2px 8px rgba(100, 116, 139, 0.3); + border-color: var(--neutral-500); } .statusMessage { @@ -154,19 +154,19 @@ } .alertInfo { - background: var(--visual-bg-capability); - border: 1px solid var(--visual-capability); - color: var(--visual-capability); + background: var(--visual-bg-success); + border: 1px solid var(--visual-success); + color: var(--visual-success); } .alertWarning { - background: var(--visual-bg-limitation); - border: 1px solid var(--visual-limitation); - color: var(--visual-limitation); + background: var(--visual-bg-warning); + border: 1px solid var(--visual-warning); + color: var(--visual-warning); } .alertCritical { - background: var(--visual-bg-error, rgba(225, 29, 72, 0.1)); + background: var(--visual-bg-error); border: 1px solid var(--visual-error); color: var(--visual-error); } diff --git a/website/src/components/VisualElements/GroundingComparison.module.css b/website/src/components/VisualElements/GroundingComparison.module.css index 2cf20a0..02b0fdc 100644 --- a/website/src/components/VisualElements/GroundingComparison.module.css +++ b/website/src/components/VisualElements/GroundingComparison.module.css @@ -52,31 +52,32 @@ } .expandCollapseButton { - padding: 0.5rem 1rem; - font-size: 0.85rem; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--target-sm); + padding: 0 var(--space-2); + font-family: var(--font-body); + font-size: var(--text-sm); font-weight: 600; - color: var(--visual-workflow); - background: rgba(124, 58, 237, 0.1); - border: 1px solid var(--visual-workflow); - border-radius: 6px; + color: var(--text-heading); + background: transparent; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); cursor: pointer; - transition: all 0.2s ease; + transition: border-color 0.15s ease; } .expandCollapseButton:hover { - background: var(--visual-workflow); - color: white; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(124, 58, 237, 0.3); + border-color: var(--neutral-400); } .expandCollapseButton:active { - transform: translateY(0); - box-shadow: 0 1px 4px rgba(124, 58, 237, 0.2); + background: var(--surface-muted); } .expandCollapseButton:focus { - outline: 2px solid var(--visual-workflow); + outline: 2px solid var(--text-heading); outline-offset: 2px; } @@ -100,8 +101,8 @@ .sideWithout { background: linear-gradient( 180deg, - rgba(225, 29, 72, 0.03) 0%, - rgba(225, 29, 72, 0.01) 100% + color-mix(in srgb, var(--visual-error) 3%, transparent) 0%, + color-mix(in srgb, var(--visual-error) 1%, transparent) 100% ); border-right: 1px solid var(--ifm-color-emphasis-300); opacity: 0; @@ -112,8 +113,8 @@ .sideWith { background: linear-gradient( 180deg, - rgba(6, 182, 212, 0.03) 0%, - rgba(6, 182, 212, 0.01) 100% + color-mix(in srgb, var(--visual-success) 3%, transparent) 0%, + color-mix(in srgb, var(--visual-success) 1%, transparent) 100% ); opacity: 0; transform: translateX(20px); @@ -153,7 +154,7 @@ } .sideWith .sideLabel { - color: var(--visual-capability); + color: var(--visual-success); } .phases { @@ -206,18 +207,18 @@ } .phaseCardWithout:hover { - box-shadow: 0 4px 12px rgba(225, 29, 72, 0.25); - border-color: var(--visual-workflow); + box-shadow: 0 4px 12px color-mix(in srgb, var(--visual-error) 25%, transparent); + border-color: var(--visual-cyan); transform: translateY(-2px); } .phaseCardWith { - border-color: var(--visual-capability); + border-color: var(--visual-success); } .phaseCardWith:hover { - box-shadow: 0 2px 8px rgba(124, 58, 237, 0.15); - border-color: var(--visual-workflow); + box-shadow: 0 2px 8px color-mix(in srgb, var(--visual-cyan) 15%, transparent); + border-color: var(--visual-cyan); transform: translateY(-1px); } @@ -248,7 +249,7 @@ } .phaseCardWith .phaseIcon { - color: var(--visual-capability); + color: var(--visual-success); } .phaseTitle { @@ -323,10 +324,10 @@ padding: 4px 8px; font-size: 0.75rem; font-family: var(--ifm-font-family-monospace); - background: rgba(124, 58, 237, 0.1); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); border-radius: 4px; - color: var(--visual-workflow); + color: var(--visual-cyan); } /* ======================================================================== @@ -336,21 +337,20 @@ .tokenEfficiencyBox { margin: 0.5rem 1.5rem 1.5rem 1.5rem; padding: 1rem 1.25rem; - background: rgba(124, 58, 237, 0.05); - border-left: 4px solid var(--visual-workflow); - border-radius: 6px; + background: color-mix(in srgb, var(--visual-cyan) 5%, transparent); + border-left: 4px solid var(--visual-cyan); font-size: 0.9rem; line-height: 1.6; color: var(--ifm-color-emphasis-800); } .tokenEfficiencyBox strong { - color: var(--visual-workflow); + color: var(--visual-cyan); font-weight: 700; } [data-theme='dark'] .tokenEfficiencyBox { - background: rgba(167, 139, 250, 0.08); + background: color-mix(in srgb, var(--visual-cyan) 8%, transparent); color: var(--ifm-color-emphasis-700); } @@ -427,33 +427,33 @@ [data-theme='dark'] .sideWithout { background: linear-gradient( 180deg, - rgba(251, 113, 133, 0.08) 0%, - rgba(251, 113, 133, 0.02) 100% + color-mix(in srgb, var(--visual-error) 8%, transparent) 0%, + color-mix(in srgb, var(--visual-error) 2%, transparent) 100% ); } [data-theme='dark'] .sideWith { background: linear-gradient( 180deg, - rgba(34, 211, 238, 0.08) 0%, - rgba(34, 211, 238, 0.02) 100% + color-mix(in srgb, var(--visual-success) 8%, transparent) 0%, + color-mix(in srgb, var(--visual-success) 2%, transparent) 100% ); } [data-theme='dark'] .phaseCardWithout:hover { - box-shadow: 0 4px 12px rgba(251, 113, 133, 0.35); + box-shadow: 0 4px 12px color-mix(in srgb, var(--visual-error) 35%, transparent); } [data-theme='dark'] .phaseCardWith:hover { - box-shadow: 0 2px 8px rgba(167, 139, 250, 0.25); + box-shadow: 0 2px 8px color-mix(in srgb, var(--visual-cyan) 25%, transparent); } [data-theme='dark'] .toolBadge { - background: rgba(167, 139, 250, 0.15); + background: var(--visual-bg-cyan); } [data-theme='dark'] .footer { - background: rgba(22, 27, 34, 0.5); + background: color-mix(in srgb, var(--ifm-background-surface-color) 50%, transparent); } /* ======================================================================== diff --git a/website/src/components/VisualElements/IntroHookDiagram.module.css b/website/src/components/VisualElements/IntroHookDiagram.module.css new file mode 100644 index 0000000..21d935d --- /dev/null +++ b/website/src/components/VisualElements/IntroHookDiagram.module.css @@ -0,0 +1,111 @@ +/* ── IntroHookDiagram — Animation module ───────────────────────────────────── + Act-gated keyframes. Durations are intentionally longer than global tokens + to match the scroll-driven pacing of this specific diagram. + ─────────────────────────────────────────────────────────────────────────── */ + +@keyframes actEnter { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes drawPath { + to { stroke-dashoffset: 0; } +} + +/* ── Guide arc (operator → orchestrator) ── */ +.guideArc { + opacity: 0; +} +.guideArc.arcDraw { + opacity: 1; + animation: drawPath 500ms var(--ease-enter) both; +} + +/* ── Fan arcs (orchestrator → workers) ── */ +.fanArc { + opacity: 0; +} +.fanArc.fanDraw { + opacity: 1; +} + +/* ── Actor nodes (operator, orchestrator) ── */ +.actorNode { + opacity: 0; + transform: translateY(12px); +} +.actorNode.entered { + animation: actEnter 300ms var(--ease-enter) both; +} + +/* ── Worker nodes ── */ +.workerNode { + opacity: 0; + transform: translateY(8px); + transition: opacity 200ms var(--ease-exit), transform 200ms var(--ease-exit); +} +.workerNode.workerEntered { + opacity: 1; + transform: translateY(0); + transition: opacity 250ms var(--ease-enter), transform 250ms var(--ease-enter); +} + +/* ── Labels ── */ +.labels { + opacity: 0; + transform: translateY(12px); +} +.labels.entered { + animation: actEnter 300ms var(--ease-enter) both; +} + +/* ── Worker labels ── */ +.workerLabel { + opacity: 0; + transform: translateY(8px); + transition: opacity 200ms var(--ease-exit), transform 200ms var(--ease-exit); +} +.workerLabel.workerLabelEntered { + opacity: 1; + transform: translateY(0); + transition: opacity 250ms var(--ease-enter), transform 250ms var(--ease-enter); +} + +/* ── Prompt icon (unified: replaces PromptBubble + TravelingPromptCard) ── */ +.promptIcon { + animation: actEnter 300ms var(--ease-enter) both; +} + +/* ── Ghost worker placeholders (visible before dispatch, fade on dispatch) ── */ +.ghostWorker { opacity: 0; } +.ghostWorkerShown { opacity: 0.45; transition: opacity 300ms var(--ease-enter); } +.ghostWorkerHidden { opacity: 0; transition: opacity 200ms var(--ease-exit); } + +/* ── Static fallback card (reduced-motion only) ── */ +.staticCard { + display: none; +} + +/* ── Idea lightbulb (pre-prompt state) ── */ +.ideaBulb { opacity: 0; transition: opacity 150ms var(--ease-exit), transform 150ms var(--ease-exit); } +.ideaBulbVisible { opacity: 1; transform: translateY(0); } +.ideaBulbFadeOut { opacity: 0; transform: translateY(8px); pointer-events: none; } + +/* ── Reduced-motion overrides ── */ +@media (prefers-reduced-motion: reduce) { + .guideArc { opacity: 0.35; stroke-dashoffset: 0 !important; } + .guideArc.arcDraw { animation: none; } + .fanArc { opacity: 0.35; stroke-dashoffset: 0 !important; } + .fanArc.fanDraw { animation: none; } + .actorNode, + .workerNode, + .workerLabel, + .labels { opacity: 1; transform: none; animation: none; transition: none; } + .promptIcon { display: none; } + .staticCard { display: block; } + .ideaBulb, + .ideaBulbVisible, + .ideaBulbFadeOut { opacity: 0; } /* final settled state; static card serves as fallback */ + .ghostWorker, + .ghostWorkerShown, + .ghostWorkerHidden { opacity: 0 !important; } +} diff --git a/website/src/components/VisualElements/IntroHookDiagram.tsx b/website/src/components/VisualElements/IntroHookDiagram.tsx new file mode 100644 index 0000000..91996b4 --- /dev/null +++ b/website/src/components/VisualElements/IntroHookDiagram.tsx @@ -0,0 +1,339 @@ +import React, { useRef, useEffect, useState } from 'react'; +import clsx from 'clsx'; +import styles from './IntroHookDiagram.module.css'; +import { OperatorNode, AgentNode, PromptIcon } from './ActorNodes'; +import { useAnimationPhase } from '../animations/ScrollDrivenFigure'; +import { useActs } from '../../hooks/useActs'; + +// Layout — ViewBox 560×264 (fan: symmetric ±22.6° about orchestrator centre y=108) +// +// Operator: BB x=70 y=88 size=40 → right shoulder (108, 108) +// Orchestrator: BB x=250 y=88 size=40 → left shoulder (243, 108); right shoulder (288, 108) +// W1 (branch): BB x=435 y=24 size=32 → left edge (437, 40) +// W2 (tests): BB x=475 y=96 size=32 → left edge (477, 112) +// W3 (review): BB x=435 y=160 size=32 → left edge (437, 176) +// +// Main arc: M 108 108 Q 178 48 243 108 +// Fan W1: M 288 108 Q 345 60 437 40 +// Fan W2: M 288 108 Q 375 108 477 112 +// Fan W3: M 288 108 Q 345 156 437 176 +// +// Lightbulb: globe center (90, 55), r=13; cap x=82 y=68 w=16 h=5 rx=2 +// Ghost workers: AgentNode S=32 squircle outline at worker positions, opacity 0.28 → 0 on dispatch + +// 5 rays, −45° to +45° CW from North (top), 90° total arc. Globe center (90, 55), r=13. +// r_inner=16 (r+3), r_outer=21 (r_inner+5). All coords integer. All rays in sync. +const BULB_RAYS = [ + { x1: 79, y1: 44, x2: 75, y2: 40, dx: -1, dy: -1 }, // θ=−45° + { x1: 84, y1: 40, x2: 82, y2: 36, dx: -1, dy: -2 }, // θ=−22.5° + { x1: 90, y1: 39, x2: 90, y2: 34, dx: 0, dy: -2 }, // θ=0° + { x1: 96, y1: 40, x2: 98, y2: 36, dx: 1, dy: -2 }, // θ=+22.5° + { x1: 101, y1: 44, x2: 105, y2: 40, dx: 1, dy: -1 }, // θ=+45° +] as const; + +const ARC_D = 'M 108 108 Q 178 48 243 108'; +const TRAVEL_D = 'M 90 73 Q 178 40 243 108'; +const FAN1_D = 'M 288 108 Q 345 60 437 40'; +const FAN2_D = 'M 288 108 Q 375 108 477 112'; +const FAN3_D = 'M 288 108 Q 345 156 437 176'; + +const DISPATCH_START = 0.80; +const DISPATCH_END = 0.995; +const PHASE_STAGGER = 0.04; // ≈ 80 ms / 400 ms × 0.195 phase range + +const ACTS = [ + { id: 'arc', threshold: 0 }, + { id: 'operator', threshold: 0 }, + { id: 'orchestrator', threshold: 0 }, + { id: 'labels', threshold: 0 }, + { id: 'composing', threshold: 0.5 }, + { id: 'traveling', threshold: 0.6 }, + { id: 'dispatch', threshold: 0.80 }, +] as const; + +type FanSpec = { + d: string; + pathDefId: string; + workerX: number; workerY: number; + labelX: number; labelY: number; + label: string; + arcDelay: number; nodeDelay: number; +}; + +const FAN_ARCS: FanSpec[] = [ + { d: FAN1_D, pathDefId: 'ihFan1Path', workerX: 435, workerY: 24, labelX: 451, labelY: 72, label: 'explore', arcDelay: 0, nodeDelay: 200 }, + { d: FAN2_D, pathDefId: 'ihFan2Path', workerX: 475, workerY: 96, labelX: 491, labelY: 144, label: 'build', arcDelay: 80, nodeDelay: 280 }, + { d: FAN3_D, pathDefId: 'ihFan3Path', workerX: 435, workerY: 160, labelX: 451, labelY: 208, label: 'verify', arcDelay: 160, nodeDelay: 360 }, +]; + +export default function IntroHookDiagram() { + const phase = useAnimationPhase(); + const { wasReached, isCurrentAct } = useActs(ACTS as unknown as { id: string; threshold: number }[], phase); + + const arcPathRef = useRef(null); + const travelPathRef = useRef(null); + const fan1Ref = useRef(null); + const fan2Ref = useRef(null); + const fan3Ref = useRef(null); + const fanRefs = [fan1Ref, fan2Ref, fan3Ref]; + + // Keep lightbulb and ghost workers hidden until mount so the client has computed + // the correct phase. ideaBulb / ghostWorker default to opacity:0; the *Visible / + // *Shown classes make them explicit once safe. + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + + // Set stroke-dasharray/offset from computed path length on mount + useEffect(() => { + for (const p of [arcPathRef.current, fan1Ref.current, fan2Ref.current, fan3Ref.current]) { + if (!p) continue; + const len = p.getTotalLength(); + p.style.strokeDasharray = `${len}`; + p.style.strokeDashoffset = `${len}`; + } + }, []); + + const composingReached = wasReached('composing'); + const travelingReached = wasReached('traveling'); + const dispatchReached = wasReached('dispatch'); + const dispatched = dispatchReached; + + // Per-arc staggered t: maps phase into [0,1] with each arc offset by PHASE_STAGGER + const fanT = (i: number) => { + const start = DISPATCH_START + i * PHASE_STAGGER; + const range = DISPATCH_END - start; + return Math.min(Math.max((phase - start) / range, 0), 1); + }; + + // Drive fan arc strokeDashoffset directly from scroll phase (keeps tip in sync with prompt) + useEffect(() => { + fanRefs.forEach((ref, i) => { + const path = ref.current; + if (!path) return; + const len = path.getTotalLength(); + const t = fanT(i); + path.style.strokeDashoffset = `${len * (1 - t)}`; + }); + }, [phase]); + + // Main prompt position: idle at path start, then scroll-interpolated along arc + const mainPromptPos = (() => { + if (!composingReached) return null; + if (!travelingReached) return { x: 90, y: 73, opacity: 1 }; + const path = travelPathRef.current; + if (!path) return { x: 243, y: 108, opacity: 0 }; // fallback: arc end + const t = Math.min((phase - 0.6) / 0.20, 1); // 0.6→0.80 maps to 0→1 + const opacity = t < 0.7 ? 1 : 1 - (t - 0.7) / 0.3; + const pt = path.getPointAtLength(t * path.getTotalLength()); + return { x: pt.x, y: pt.y, opacity }; + })(); + + // Fan prompt positions: scroll-interpolated along each fan arc, staggered per arc + const fanPromptPositions = FAN_ARCS.map((_, i) => { + if (!dispatchReached) return null; + const path = fanRefs[i].current; + if (!path) return null; + const t = fanT(i); + if (t <= 0) return null; + const opacity = t < 0.7 ? 1 : 1 - (t - 0.7) / 0.3; + const pt = path.getPointAtLength(t * path.getTotalLength()); + return { x: pt.x, y: pt.y, opacity }; + }); + + return ( + + + + {FAN_ARCS.map(fan => )} + + + {/* Ghost worker placeholders — provide rightward visual mass in start state, + fading out as the real workers bloom in on dispatch. Shape matches the + AgentNode S=32 head squircle (headX=2.4 headY=2.4 headW=27.2 headH=27.2 rx=6.8). */} + {FAN_ARCS.map((fan, i) => ( + + ))} + + {/* Idea lightbulb — pre-prompt state; fades out when composing begins. */} + + {/* Rays — drawn first, globe+cap fill covers inner endpoints naturally */} + {BULB_RAYS.map((ray, i) => ( + + + + + + ))} + {/* Globe */} + + {/* Cap — x=82 y=68 w=16 h=5 rx=2 */} + + {/* Question mark — Radon (human-actor voice), fontSize=13 centered at globe (cx=90,cy=55,r=13). + 80%-fill rule: cap-h≈10px < 20.8px limit. fontWeight=400 (only weight Radon ships). */} + ? + + + {/* Guide arc — operator → orchestrator */} + + + {/* Fan arcs — orchestrator → workers, staggered on dispatch */} + {FAN_ARCS.map((fan, i) => ( + + ))} + + {/* Operator */} + + + + + {/* Main prompt: enters at operator, idles, then travels to orchestrator. + Outer handles SVG-attribute positioning only — no CSS class here, + because CSS `transform` (from actEnter) would override the SVG attribute. */} + {mainPromptPos && ( + + + + + + )} + + {/* Fan dispatch prompts — one per worker, scroll-driven */} + {fanPromptPositions.map((pos, i) => pos && ( + + + + + + ))} + + {/* Orchestrator */} + + + + + {/* Workers — bloom out on dispatch, staggered */} + {FAN_ARCS.map((fan, i) => ( + + + + ))} + + {/* Permanent orientation labels */} + + You + Orchestrator + + + {/* Worker labels — appear on dispatch, staggered with nodes */} + {FAN_ARCS.map((fan, i) => ( + {fan.label} + ))} + + {/* Reduced-motion fallback: static card at arc midpoint (177, 78) */} + + + + + + + + + ); +} diff --git a/website/src/components/VisualElements/KnowledgeExpansionDiamond.module.css b/website/src/components/VisualElements/KnowledgeExpansionDiamond.module.css index 8d41170..ce2e6ee 100644 --- a/website/src/components/VisualElements/KnowledgeExpansionDiamond.module.css +++ b/website/src/components/VisualElements/KnowledgeExpansionDiamond.module.css @@ -2,8 +2,8 @@ margin: 2.5rem auto; padding: 1.25rem; border-radius: 8px; - background: var(--visual-bg-decision); - border: 1px solid var(--visual-decision); + background: var(--visual-bg-indigo); + border: 1px solid var(--visual-indigo); max-width: 480px; animation: fadeIn 0.5s ease-out; } @@ -90,7 +90,7 @@ .layerBox { fill: var(--ifm-background-surface-color); - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 1.5; } @@ -126,7 +126,7 @@ } .codeSubtitle { - fill: var(--visual-capability); + fill: var(--visual-success); font-size: 9px; font-weight: 600; font-family: var(--ifm-font-family-base); @@ -141,7 +141,7 @@ } .arrowTraditional { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 120; @@ -156,7 +156,7 @@ } .arrowMarkerTraditional polygon { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } .annotation { @@ -173,7 +173,7 @@ } .arrowAI { - stroke: var(--visual-capability); + stroke: var(--visual-success); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 120; @@ -182,11 +182,11 @@ } .arrowMarkerAI polygon { - fill: var(--visual-capability); + fill: var(--visual-success); } .annotationAI { - fill: var(--visual-capability); + fill: var(--visual-success); font-size: 9px; font-style: italic; font-family: var(--ifm-font-family-base); @@ -200,11 +200,11 @@ } .legendDotTraditional { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } .legendDotAI { - fill: var(--visual-capability); + fill: var(--visual-success); } .legendText { @@ -246,11 +246,11 @@ :global(.reveal) .layerLabel, :global(.reveal) .codeLabel { - fill: #e5e7eb; + fill: var(--neutral-100); } :global(.reveal) .sizeLabel { - fill: #9ca3af; + fill: var(--neutral-400); } /* ===== RESPONSIVE ===== */ diff --git a/website/src/components/VisualElements/KnowledgeExpansionDiamond.tsx b/website/src/components/VisualElements/KnowledgeExpansionDiamond.tsx index 94a4d94..019cd70 100644 --- a/website/src/components/VisualElements/KnowledgeExpansionDiamond.tsx +++ b/website/src/components/VisualElements/KnowledgeExpansionDiamond.tsx @@ -111,17 +111,17 @@ export default function KnowledgeExpansionDiamond({ > @@ -132,7 +132,7 @@ export default function KnowledgeExpansionDiamond({ dx="0" dy="2" stdDeviation="4" - floodColor="var(--visual-workflow)" + floodColor="var(--visual-cyan)" floodOpacity="0.15" />
diff --git a/website/src/components/VisualElements/MentalModelComparison.tsx b/website/src/components/VisualElements/MentalModelComparison.tsx new file mode 100644 index 0000000..8d9f36d --- /dev/null +++ b/website/src/components/VisualElements/MentalModelComparison.tsx @@ -0,0 +1,268 @@ +import React from 'react'; +import { superellipsePath } from '@site/src/utils/svgMath'; + +interface Props { + compact?: boolean; +} + +// Precomputed superellipsePath(410, 148, 44, 30, 3.5) +const OPERATOR_PATH = + 'M454.0,148.0 L453.9,156.0 L453.5,159.8 L452.9,162.8 L452.1,165.3 L451.0,167.5 L449.6,169.4 L448.0,171.1 L446.1,172.6 L443.9,173.9 L441.4,175.0 L438.6,175.9 L435.4,176.7 L431.7,177.3 L427.3,177.7 L421.7,177.9 L410.0,178.0 L398.3,177.9 L392.7,177.7 L388.3,177.3 L384.6,176.7 L381.4,175.9 L378.6,175.0 L376.1,173.9 L373.9,172.6 L372.0,171.1 L370.4,169.4 L369.0,167.5 L367.9,165.3 L367.1,162.8 L366.5,159.8 L366.1,156.0 L366.0,148.0 L366.1,140.0 L366.5,136.2 L367.1,133.2 L367.9,130.7 L369.0,128.5 L370.4,126.6 L372.0,124.9 L373.9,123.4 L376.1,122.1 L378.6,121.0 L381.4,120.1 L384.6,119.3 L388.3,118.7 L392.7,118.3 L398.3,118.1 L410.0,118.0 L421.7,118.1 L427.3,118.3 L431.7,118.7 L435.4,119.3 L438.6,120.1 L441.4,121.0 L443.9,122.1 L446.1,123.4 L448.0,124.9 L449.6,126.6 L451.0,128.5 L452.1,130.7 L452.9,133.2 L453.5,136.2 L453.9,140.0 L454.0,148.0Z'; + +export default function MentalModelComparison({ compact = false }: Props) { + return ( + + + {/* Arrow marker for rose (Smooth Circuit) */} + + + + {/* Arrow marker for cyan (Terminal Geometry) */} + + + + + + {/* ---- DIVIDER ---- */} + + + {/* ================================================================ + LEFT PANEL — AI AS TEAMMATE + ================================================================ */} + + {/* Header background */} + + + {/* Header label */} + + ✗ AI AS TEAMMATE + + + {/* "You" circle */} + + + You + + + {/* "AI" circle — dashed border */} + + + AI + + + {/* Bidirectional arcs — You ↔ AI (Smooth Circuit: round linecaps) */} + {/* Upper arc: You → AI */} + + {/* Lower arc: AI → You */} + + + {/* Left description label */} + + Waiting. Fixing line-by-line. + + + {/* ================================================================ + RIGHT PANEL — AI AS POWER TOOL + ================================================================ */} + + {/* Header background */} + + + {/* Header label */} + + ✓ AI AS POWER TOOL + + + {/* Operator node — squircle (Smooth Circuit) */} + + + Operator + + + {/* Tool node — sharp rect (Terminal Geometry) */} + + + AI Tool + + + {/* Directional arrow — operator controls tool (Terminal Geometry: square caps) */} + + + {/* Right description label */} + + Direct. Systematic. Validate output. + + + ); +} diff --git a/website/src/components/VisualElements/OperatorCycleDiagram.module.css b/website/src/components/VisualElements/OperatorCycleDiagram.module.css new file mode 100644 index 0000000..4ba5f69 --- /dev/null +++ b/website/src/components/VisualElements/OperatorCycleDiagram.module.css @@ -0,0 +1,121 @@ +/* ── OperatorCycleDiagram — Animation module ────────────────────────────── + Phase-driven connector animation (scroll-reversible). Opacity toggled by + act class; strokeDashoffset driven directly from scroll phase in TSX. + ─────────────────────────────────────────────────────────────────────────── */ + +@keyframes actEnter { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Phase nodes (circles + icons + labels) ── */ +.phaseNode { + opacity: 0; + transform: translateY(12px); +} +.phaseNode.entered { + animation: actEnter 300ms var(--ease-enter) both; +} + +/* ── Forward connectors (top, right, bottom) ── */ +.connector { + opacity: 0; + transition: opacity 200ms var(--ease-exit); +} +.connector.drawing { + opacity: 1; + transition: opacity 250ms var(--ease-enter); +} + +/* ── Return connector (Validate → Research) ── */ +.returnPath { + opacity: 0; + transition: opacity 200ms var(--ease-exit); +} +.returnPath.drawing { + opacity: 1; + transition: opacity 250ms var(--ease-enter); +} + +/* ── Center iterate label ── */ +.cycleLabel { + opacity: 0; + transform: translateY(6px); +} +.cycleLabel.settled { + animation: actEnter 300ms var(--ease-enter) both; +} + +/* ── Description grid ── */ +.descGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); + margin: var(--space-3) 0; + opacity: 0; + transform: translateY(12px); + transition: opacity 300ms var(--ease-enter), transform 300ms var(--ease-enter); +} +.descGrid.descVisible { + opacity: 1; + transform: translateY(0); +} + +.descCell { + display: flex; + flex-direction: column; + gap: 4px; +} + +.descLabel { + font-weight: 600; + font-size: 1rem; + font-family: var(--font-mono); + font-feature-settings: var(--font-mono-features); + line-height: 1.25; +} + +.descText { + font-size: var(--text-sm); + color: var(--text-body); + line-height: 1.5; +} + +/* ── Mobile: single column ── */ +@media (max-width: 639px) { + .descGrid { + grid-template-columns: 1fr; + } +} + +/* ── Reduced-motion overrides ── + ScrollDrivenFigure sets phase=1 on mount, which fires all acts. + These rules prevent entrance animations from playing and ensure + a fully settled static appearance. Token is hidden (no travel metaphor). */ +@media (prefers-reduced-motion: reduce) { + .phaseNode { + opacity: 1; + transform: none; + animation: none; + } + .connector, + .connector.drawing { + opacity: 0.5; + stroke-dashoffset: 0 !important; + } + .returnPath, + .returnPath.drawing { + opacity: 0.5; + stroke-dashoffset: 0 !important; + } + .cycleLabel { + opacity: 1; + transform: none; + animation: none; + } + .descGrid { + opacity: 1; + transform: none; + transition: none; + } +} diff --git a/website/src/components/VisualElements/OperatorCycleDiagram.tsx b/website/src/components/VisualElements/OperatorCycleDiagram.tsx new file mode 100644 index 0000000..ff06ca5 --- /dev/null +++ b/website/src/components/VisualElements/OperatorCycleDiagram.tsx @@ -0,0 +1,334 @@ +import React, { useRef, useEffect } from 'react'; +import clsx from 'clsx'; +import styles from './OperatorCycleDiagram.module.css'; +import { useAnimationPhase } from '../animations/ScrollDrivenFigure'; +import { useActs } from '../../hooks/useActs'; + +// Layout — ViewBox 560×280 +// +// Research: center (160, 64) Plan: center (400, 64) +// Validate: center (160, 216) Execute: center (400, 216) +// +// Cycle direction (clockwise): Research → Plan → Execute → Validate → Research +// Node radius: R = 22px | Horizontal gap: 240px | Vertical gap: 152px (~1.58:1) +// +// Connector endpoints (at circle edges, R from center): +// Research right (182,64) Plan left (378,64) +// Plan bottom (400,86) Execute top (400,194) +// Execute left (378,216) Validate right(182,216) +// Validate top (160,194) Research bottom(160,86) + +const R = 22; + +const NODES = [ + { + id: 'research', + label: 'Research', + cx: 160, cy: 64, + labelAbove: true, + color: 'var(--visual-indigo)', + bgColor: 'var(--visual-bg-indigo)', + description: 'Ground agents in codebase patterns and domain knowledge before acting', + }, + { + id: 'plan', + label: 'Plan', + cx: 400, cy: 64, + labelAbove: true, + color: 'var(--visual-violet)', + bgColor: 'var(--visual-bg-violet)', + description: 'Design changes strategically — explore when uncertain, be directive when clear', + }, + { + id: 'execute', + label: 'Execute', + cx: 400, cy: 216, + labelAbove: false, + color: 'var(--visual-cyan)', + bgColor: 'var(--visual-bg-cyan)', + description: 'Run agents supervised or autonomous based on trust and task criticality', + }, + { + id: 'validate', + label: 'Validate', + cx: 160, cy: 216, + labelAbove: false, + color: 'var(--visual-success)', + bgColor: 'var(--visual-bg-success)', + description: 'Verify against your mental model, then iterate or regenerate', + }, +] as const; + +// Connector paths — straight lines for clear directed segments +const FWD_CONNECTORS = [ + { d: `M 182,64 L 378,64` }, // top: R → P + { d: `M 400,86 L 400,194` }, // right: P → E + { d: `M 378,216 L 182,216` }, // bottom: E → V +] as const; + +const RETURN_D = `M 160,194 L 160,86`; // left: V → R + +const ACTS = [ + { id: 'nodes', threshold: 0.00 }, + { id: 'forward', threshold: 0.15 }, + { id: 'return', threshold: 0.45 }, + { id: 'settle', threshold: 0.65 }, +] as const; + +const FWD_PHASE_START = 0.15; +const FWD_PHASE_END = 0.40; +const FWD_STAGGER = 0.04; +const RET_PHASE_START = 0.45; +const RET_PHASE_END = 0.62; + +// ── Inline icon content (path data from icon components, no foreignObject) ── + +function renderResearch(color: string) { + return ( + + + + + + + + ); +} + +function renderPlan(color: string) { + return ( + + {/* Row 1 — checked */} + + + {/* Row 2 — checked */} + + + {/* Row 3 — pending */} + + + + ); +} + +function renderExecute(color: string) { + return ( + // Terminal Geometry — retains square caps/miter joins per ExecuteIcon + + + + + ); +} + +function renderValidate(color: string) { + return ( + + + + + + + + + + + ); +} + +const ICON_RENDERERS = [renderResearch, renderPlan, renderExecute, renderValidate]; + +// ── Component ────────────────────────────────────────────────────────────── + +export default function OperatorCycleDiagram() { + const phase = useAnimationPhase(); + const { wasReached } = useActs( + ACTS as unknown as { id: string; threshold: number }[], + phase, + ); + + // Refs for connector paths (dasharray/dashoffset computed on mount) + const fwdRef0 = useRef(null); + const fwdRef1 = useRef(null); + const fwdRef2 = useRef(null); + const fwdRefs = [fwdRef0, fwdRef1, fwdRef2]; + const returnRef = useRef(null); + + // Refs for standalone arrowhead polygons + const arrowRef0 = useRef(null); + const arrowRef1 = useRef(null); + const arrowRef2 = useRef(null); + const arrowRefs = [arrowRef0, arrowRef1, arrowRef2]; + const arrowReturnRef = useRef(null); + + // Mount: compute strokeDasharray from path geometry + useEffect(() => { + for (const ref of [...fwdRefs, returnRef]) { + const p = ref.current; + if (!p) continue; + p.style.strokeDasharray = `${p.getTotalLength()}`; + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Phase-driven: drive strokeDashoffset and arrowhead opacity from scroll phase (reverses on scroll-up) + useEffect(() => { + fwdRefs.forEach((ref, i) => { + const p = ref.current; + if (!p) return; + const len = parseFloat(p.style.strokeDasharray || '0'); + if (!len) return; + const start = FWD_PHASE_START + i * FWD_STAGGER; + const t = Math.min(Math.max((phase - start) / (FWD_PHASE_END - start), 0), 1); + p.style.strokeDashoffset = `${len * (1 - t)}`; + const arrowG = arrowRefs[i].current; + if (arrowG) arrowG.style.opacity = `${Math.min(Math.max((t - 0.85) / 0.15, 0), 1)}`; + }); + const rp = returnRef.current; + if (rp) { + const len = parseFloat(rp.style.strokeDasharray || '0'); + if (len) { + const t = Math.min(Math.max((phase - RET_PHASE_START) / (RET_PHASE_END - RET_PHASE_START), 0), 1); + rp.style.strokeDashoffset = `${len * (1 - t)}`; + const arrowG = arrowReturnRef.current; + if (arrowG) arrowG.style.opacity = `${Math.min(Math.max((t - 0.85) / 0.15, 0), 1)}`; + } + } + }, [phase]); // eslint-disable-line react-hooks/exhaustive-deps + + const nodesReached = wasReached('nodes'); + const forwardReached = wasReached('forward'); + const returnReached = wasReached('return'); + const settleReached = wasReached('settle'); + + return ( +
+ {/* SVG Diagram */} + + {/* Forward connectors — top, right, bottom — staggered draw on `forward` act */} + {FWD_CONNECTORS.map((conn, i) => ( + + ))} + + {/* Return connector — Validate → Research — draws on `return` act */} + + + {/* Standalone arrowheads — phase-driven opacity so they appear when line arrives */} + {/* R→P: tip at (378,64), pointing right (rotate 0°) */} + + + + {/* P→E: tip at (400,194), pointing down (rotate 90°) */} + + + + {/* E→V: tip at (182,216), pointing left (rotate 180°) */} + + + + {/* V→R: tip at (160,86), pointing up (rotate 270°) */} + + + + + {/* Phase nodes — staggered entrance on `nodes` act */} + {NODES.map((node, i) => ( + + {/* Tinted circle with semantic stroke */} + + {/* Icon: nested SVG (viewBox 24×24) centered in circle — no foreignObject */} + + {/* Phase label — above top-row nodes, below bottom-row nodes */} + + {node.label} + + + ))} + + {/* Center "iterate" label — appears on `settle` act */} + + iterate + + + + + + {/* Description grid — fades in with nodes */} +
+ {NODES.map((node) => ( +
+ + {node.label} + + {node.description} +
+ ))} +
+
+ ); +} diff --git a/website/src/components/VisualElements/OperatorIllustration.module.css b/website/src/components/VisualElements/OperatorIllustration.module.css new file mode 100644 index 0000000..4c73d4d --- /dev/null +++ b/website/src/components/VisualElements/OperatorIllustration.module.css @@ -0,0 +1,22 @@ +@keyframes drawThread { + to { stroke-dashoffset: 0; } +} + +@keyframes fadeNode { + from { opacity: 0; } + to { opacity: 1; } +} + +.thread { + animation: drawThread 0.5s ease-out forwards; +} + +.node { + opacity: 0; + animation: fadeNode 0.3s ease-out forwards; +} + +@media (prefers-reduced-motion: reduce) { + .thread { animation: none; stroke-dashoffset: 0; } + .node { animation: none; opacity: 1; } +} diff --git a/website/src/components/VisualElements/OperatorIllustration.tsx b/website/src/components/VisualElements/OperatorIllustration.tsx new file mode 100644 index 0000000..283d7b2 --- /dev/null +++ b/website/src/components/VisualElements/OperatorIllustration.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import styles from './OperatorIllustration.module.css'; + +// Computed via scripts/compute-operator-coords.js +// viewBox="0 0 560 280", center=(280,140), R=120, cr=26, nw=80, nh=24 +const NODES = [ + { + label: 'Research', + // path lengths computed via numerical integration + pathLen: 85, + threadDelay: '0.00s', + nodeDelay: '0.50s', + // M tx0 ty0 Q cpx cpy ex ey + d: 'M 280 114 Q 262 73 280 32', + rx: 240, + ry: 8, + }, + { + label: 'Plan', + pathLen: 73, + threadDelay: '0.15s', + nodeDelay: '0.65s', + d: 'M 302.5 127 Q 323.8 93.9 363.1 92', + rx: 343.9, + ry: 68, + }, + { + label: 'Execute', + pathLen: 73, + threadDelay: '0.30s', + nodeDelay: '0.80s', + d: 'M 302.5 153 Q 341.8 154.9 363.1 188', + rx: 343.9, + ry: 188, + }, + { + label: 'Validate', + pathLen: 85, + threadDelay: '0.45s', + nodeDelay: '0.95s', + d: 'M 280 166 Q 298 207 280 248', + rx: 240, + ry: 248, + }, + { + label: 'Review', + pathLen: 73, + threadDelay: '0.60s', + nodeDelay: '1.10s', + d: 'M 257.5 153 Q 236.2 186.1 196.9 188', + rx: 136.1, + ry: 188, + }, + { + label: 'Debug', + pathLen: 73, + threadDelay: '0.75s', + nodeDelay: '1.25s', + d: 'M 257.5 127 Q 218.2 125.1 196.9 92', + rx: 136.1, + ry: 68, + }, +] as const; + +const NODE_W = 80; +const NODE_H = 24; + +export default function OperatorIllustration() { + return ( + + + + + + + + {/* Threads */} + {NODES.map((n) => ( + + ))} + + {/* Task nodes */} + {NODES.map((n) => ( + + + + {n.label} + + + ))} + + {/* Center node — human actor (Rose) */} + + + YOU + + + ); +} diff --git a/website/src/components/VisualElements/OutcomeIcons.module.css b/website/src/components/VisualElements/OutcomeIcons.module.css new file mode 100644 index 0000000..b0a52ab --- /dev/null +++ b/website/src/components/VisualElements/OutcomeIcons.module.css @@ -0,0 +1,34 @@ +.iconGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: var(--space-3); + margin: var(--space-4) 0; +} + +.iconItem { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-1); +} + +.iconLabel { + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + color: var(--text-muted); + text-align: center; +} + +@media (max-width: 768px) { + .iconGrid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--space-3); + } + + .iconItem { + flex: 0 0 calc((100% - 2 * var(--space-3)) / 3); + } +} diff --git a/website/src/components/VisualElements/OutcomeIcons.tsx b/website/src/components/VisualElements/OutcomeIcons.tsx new file mode 100644 index 0000000..aedc1f8 --- /dev/null +++ b/website/src/components/VisualElements/OutcomeIcons.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import styles from './OutcomeIcons.module.css'; + +interface OutcomeIconProps { + label: string; + color: string; + children: React.ReactNode; +} + +function OutcomeIcon({ label, color, children }: OutcomeIconProps) { + return ( +
+ + {label} +
+ ); +} + +/** Icon 1: Onboard codebases faster — open document with magnifying glass (Smooth Circuit) */ +function OnboardIcon() { + return ( + + + + + + + ); +} + +/** Icon 2: Refactor reliably — two rotating arrows (Smooth Circuit) */ +function RefactorIcon() { + return ( + + + + + + + ); +} + +/** Icon 3: Debug by delegating — terminal with search prompt (Terminal Geometry) */ +function DebugIcon() { + return ( + + + + + + + + ); +} + +/** Icon 4: Review systematically — shield with checkmark (Smooth Circuit) */ +function ReviewIcon() { + return ( + + + + + ); +} + +/** Icon 5: Plan & execute features — checklist (Smooth Circuit) */ +function PlanIcon() { + return ( + + {/* Row 1 — checked */} + + + {/* Row 2 — checked */} + + + {/* Row 3 — pending */} + + + + ); +} + +export default function OutcomeIcons() { + return ( +
+ + + + + +
+ ); +} diff --git a/website/src/components/VisualElements/PhaseIcons.module.css b/website/src/components/VisualElements/PhaseIcons.module.css new file mode 100644 index 0000000..0dd5a68 --- /dev/null +++ b/website/src/components/VisualElements/PhaseIcons.module.css @@ -0,0 +1,44 @@ +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); + margin: var(--space-3) 0; +} + +.card { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--space-2); +} + +.iconWrapper { + flex-shrink: 0; + color: var(--visual-cyan); + line-height: 0; +} + +.text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + font-weight: 600; + font-size: 1rem; + color: var(--text-heading); + line-height: 1.25; +} + +.description { + font-size: var(--text-sm); + color: var(--text-body); + line-height: 1.5; +} + +@media (max-width: 639px) { + .grid { + grid-template-columns: 1fr; + } +} diff --git a/website/src/components/VisualElements/PhaseIcons.tsx b/website/src/components/VisualElements/PhaseIcons.tsx new file mode 100644 index 0000000..7cc43b7 --- /dev/null +++ b/website/src/components/VisualElements/PhaseIcons.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import styles from './PhaseIcons.module.css'; +import ResearchIcon from './icons/ResearchIcon'; +import ChecklistIcon from './icons/ChecklistIcon'; +import ExecuteIcon from './icons/ExecuteIcon'; +import ValidateIcon from './icons/ValidateIcon'; + +interface Phase { + icon: React.ComponentType<{ className?: string; size?: number }>; + label: string; + description: string; +} + +const PHASES: Phase[] = [ + { + icon: ResearchIcon, + label: 'Research', + description: 'Ground agents in codebase patterns and domain knowledge before acting', + }, + { + icon: ChecklistIcon, + label: 'Plan', + description: 'Design changes strategically — explore when uncertain, be directive when clear', + }, + { + icon: ExecuteIcon, + label: 'Execute', + description: 'Run agents supervised or autonomous based on trust and task criticality', + }, + { + icon: ValidateIcon, + label: 'Validate', + description: 'Verify against your mental model, then iterate or regenerate', + }, +]; + +export default function PhaseIcons() { + return ( +
+ {PHASES.map(({ icon: Icon, label, description }) => ( +
+
+
+
+ {label} + {description} +
+
+ ))} +
+ ); +} diff --git a/website/src/components/VisualElements/PlanningStrategyComparison.module.css b/website/src/components/VisualElements/PlanningStrategyComparison.module.css index 1951ff7..881ccd4 100644 --- a/website/src/components/VisualElements/PlanningStrategyComparison.module.css +++ b/website/src/components/VisualElements/PlanningStrategyComparison.module.css @@ -2,8 +2,8 @@ margin: 3rem auto; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-decision); - border: 1px solid var(--visual-decision); + background: var(--visual-bg-indigo); + border: 1px solid var(--visual-indigo); max-width: 800px; animation: fadeIn 0.6s ease-in-out; } @@ -72,20 +72,20 @@ } .explorationStart { - fill: var(--visual-workflow); - stroke: var(--visual-workflow); + fill: var(--visual-cyan); + stroke: var(--visual-cyan); stroke-width: 2.5; } .explorationNode { fill: var(--ifm-background-surface-color); - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2; opacity: 0.85; } .explorationPathLight { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 1.5; fill: none; stroke-linecap: round; @@ -110,26 +110,26 @@ } .exactStart { - fill: var(--visual-capability); - stroke: var(--visual-capability); + fill: var(--visual-success); + stroke: var(--visual-success); stroke-width: 2.5; } .exactGoal { - fill: var(--visual-capability); - stroke: var(--visual-capability); + fill: var(--visual-success); + stroke: var(--visual-success); stroke-width: 2.5; } .exactPath { - stroke: var(--visual-capability); + stroke: var(--visual-success); stroke-width: 4; fill: none; stroke-linecap: round; } .arrowMarkerExact polygon { - fill: var(--visual-capability); + fill: var(--visual-success); } /* ===== CENTER DIVIDER ===== */ diff --git a/website/src/components/VisualElements/README.md b/website/src/components/VisualElements/README.md index cc2662e..0bc5a05 100644 --- a/website/src/components/VisualElements/README.md +++ b/website/src/components/VisualElements/README.md @@ -130,8 +130,8 @@ export default function MyNewComponent({ compact = false }: PresentationAwarePro margin: 2rem 0; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-workflow); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); } /* Compact mode for presentations - maximize content area */ @@ -147,14 +147,14 @@ export default function MyNewComponent({ compact = false }: PresentationAwarePro ```css /* ❌ BAD - Hardcoded colors won't adapt to light/dark mode */ .element { - color: #7c3aed; - background: rgba(124, 58, 237, 0.1); + color: #007576; + background: rgba(0, 117, 118, 0.1); } /* ✅ GOOD - CSS variables adapt automatically */ .element { - color: var(--visual-workflow); - background: var(--visual-bg-workflow); + color: var(--visual-cyan); + background: var(--visual-bg-cyan); } ``` @@ -232,10 +232,10 @@ All visual components must use CSS variables from `/website/src/css/custom.css`: #### Semantic Colors ```css ---visual-workflow /* Purple - AI workflows/agents */ ---visual-capability /* Cyan - capabilities/success */ ---visual-limitation /* Orange - limitations/warnings */ ---visual-decision /* Light purple - decision points */ +--visual-cyan /* Cyan - AI workflows/agents */ +--visual-success /* Cyan - capabilities/success */ +--visual-warning /* Orange - limitations/warnings */ +--visual-indigo /* Light cyan - decision points */ --visual-error /* Rose - errors/critical */ --visual-neutral /* Slate - neutral states */ ``` @@ -243,10 +243,10 @@ All visual components must use CSS variables from `/website/src/css/custom.css`: #### Transparent Backgrounds ```css ---visual-bg-workflow /* rgba(167, 139, 250, 0.15) */ ---visual-bg-capability /* rgba(34, 211, 238, 0.15) */ ---visual-bg-limitation /* rgba(251, 146, 60, 0.15) */ ---visual-bg-decision /* rgba(196, 181, 253, 0.15) */ +--visual-bg-cyan /* rgba(167, 139, 250, 0.15) */ +--visual-bg-success /* rgba(34, 211, 238, 0.15) */ +--visual-bg-warning /* rgba(251, 146, 60, 0.15) */ +--visual-bg-indigo /* rgba(196, 181, 253, 0.15) */ --visual-bg-error /* rgba(251, 113, 133, 0.15) */ ``` @@ -270,17 +270,17 @@ All visual components must use CSS variables from `/website/src/css/custom.css`: ```css /* Light mode */ :root { - --visual-workflow: #7c3aed; /* Medium purple */ + --visual-cyan: #007576; /* Cyan-600 */ } /* Dark mode */ [data-theme='dark'] { - --visual-workflow: #a78bfa; /* Brighter purple */ + --visual-cyan: #00b2b2; /* Brighter cyan */ } /* Presentations (always dark) */ :global(.reveal) { - --visual-workflow: #a78bfa; /* Same as dark mode */ + --visual-cyan: #00b2b2; /* Same as dark mode */ } ``` @@ -306,7 +306,7 @@ Components don't need mode-specific CSS - the variables handle it. ```css /* Use color-mix() to derive shadow from theme color */ -box-shadow: 0 2px 8px color-mix(in srgb, var(--brand-primary) 30%, transparent); +box-shadow: 0 2px 8px color-mix(in srgb, var(--visual-cyan) 30%, transparent); ``` --- diff --git a/website/src/components/VisualElements/SpecCodeZoomDiagram.module.css b/website/src/components/VisualElements/SpecCodeZoomDiagram.module.css index 7f3b001..4c6dd5e 100644 --- a/website/src/components/VisualElements/SpecCodeZoomDiagram.module.css +++ b/website/src/components/VisualElements/SpecCodeZoomDiagram.module.css @@ -2,8 +2,8 @@ margin: 2.5rem auto; padding: 1.25rem; border-radius: 8px; - background: var(--visual-bg-decision); - border: 1px solid var(--visual-decision); + background: var(--visual-bg-indigo); + border: 1px solid var(--visual-indigo); max-width: 500px; animation: fadeIn 0.5s ease-out; } @@ -94,7 +94,7 @@ .layerBox { fill: var(--ifm-background-surface-color); - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 1.5; } @@ -131,7 +131,7 @@ } .sourceLabel { - fill: var(--visual-capability); + fill: var(--visual-success); font-size: 10px; font-weight: 600; font-family: var(--ifm-font-family-base); @@ -146,7 +146,7 @@ } .arrowGenerate { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 180; @@ -161,11 +161,11 @@ } .arrowMarkerGenerate polygon { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } .arrowLabel { - fill: var(--visual-workflow); + fill: var(--visual-cyan); font-size: 12px; font-weight: 600; font-family: var(--ifm-font-family-base); @@ -178,7 +178,7 @@ } .arrowExtract { - stroke: var(--visual-capability); + stroke: var(--visual-success); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 180; @@ -187,11 +187,11 @@ } .arrowMarkerExtract polygon { - fill: var(--visual-capability); + fill: var(--visual-success); } .arrowLabelExtract { - fill: var(--visual-capability); + fill: var(--visual-success); font-size: 12px; font-weight: 600; font-family: var(--ifm-font-family-base); @@ -215,7 +215,7 @@ } .legendLineGenerate { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2; } @@ -259,11 +259,11 @@ :global(.reveal) .layerLabel, :global(.reveal) .codeLabel { - fill: #e5e7eb; + fill: var(--neutral-100); } :global(.reveal) .subtitleLabel { - fill: #9ca3af; + fill: var(--neutral-400); } :global(.reveal) .arrowLabel, @@ -272,7 +272,7 @@ } :global(.reveal) .iterateLabel { - fill: #d1d5db; + fill: var(--neutral-200); font-size: 14px; } diff --git a/website/src/components/VisualElements/SpecCodeZoomDiagram.tsx b/website/src/components/VisualElements/SpecCodeZoomDiagram.tsx index 9e1ecfc..53f2791 100644 --- a/website/src/components/VisualElements/SpecCodeZoomDiagram.tsx +++ b/website/src/components/VisualElements/SpecCodeZoomDiagram.tsx @@ -46,7 +46,7 @@ export default function SpecCodeZoomDiagram({ return `M ${startX} ${startY} Q ${curveX} ${midPointY} ${endX} ${endY}`; }; - // Left arrow (downward, purple) — Generate: spec→code + // Left arrow (downward, cyan) — Generate: spec→code const leftArrow = createArrowPath( centerX - specBoxWidth / 2 + 8, specY + specBoxHeight, @@ -93,17 +93,17 @@ export default function SpecCodeZoomDiagram({ > @@ -120,7 +120,7 @@ export default function SpecCodeZoomDiagram({ dx="0" dy="2" stdDeviation="4" - floodColor="var(--visual-workflow)" + floodColor="var(--visual-cyan)" floodOpacity="0.15" /> diff --git a/website/src/components/VisualElements/SystemBoundaryDiagram.module.css b/website/src/components/VisualElements/SystemBoundaryDiagram.module.css index 09b1463..0600d28 100644 --- a/website/src/components/VisualElements/SystemBoundaryDiagram.module.css +++ b/website/src/components/VisualElements/SystemBoundaryDiagram.module.css @@ -66,15 +66,15 @@ /* Internal module boxes (purple) */ .moduleBox { - fill: var(--visual-bg-workflow); - stroke: var(--visual-workflow); + fill: var(--visual-bg-cyan); + stroke: var(--visual-cyan); stroke-width: 2.5; transition: all 0.2s ease; } .moduleBox:hover { - fill: var(--visual-bg-decision); - stroke: var(--visual-decision); + fill: var(--visual-bg-indigo); + stroke: var(--visual-indigo); } .moduleLabel { @@ -86,15 +86,15 @@ /* External actor boxes (gray) */ .actorBox { - fill: var(--visual-bg-decision); + fill: var(--visual-bg-indigo); stroke: var(--visual-neutral); stroke-width: 2.5; transition: all 0.2s ease; } .actorBox:hover { - fill: var(--visual-bg-workflow); - stroke: var(--visual-workflow); + fill: var(--visual-bg-cyan); + stroke: var(--visual-cyan); } .actorLabel { @@ -117,38 +117,38 @@ /* Contract arrows (purple, internal) */ .contractArrow { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 3; fill: none; stroke-linecap: round; } .contractMarker { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } /* Input arrows (cyan, external → internal) */ .inputArrow { - stroke: var(--visual-capability); + stroke: var(--visual-success); stroke-width: 3; fill: none; stroke-linecap: round; } .inputMarker { - fill: var(--visual-capability); + fill: var(--visual-success); } /* Output arrows (orange, internal → external) */ .outputArrow { - stroke: var(--visual-limitation); + stroke: var(--visual-warning); stroke-width: 3; fill: none; stroke-linecap: round; } .outputMarker { - fill: var(--visual-limitation); + fill: var(--visual-warning); } /* Legend */ @@ -192,69 +192,69 @@ /* Presentation overrides */ :global(.reveal) .moduleBox { - fill: rgba(167, 139, 250, 0.2); - stroke: #a78bfa; + fill: color-mix(in srgb, var(--visual-cyan) 20%, transparent); + stroke: var(--visual-cyan); stroke-width: 3; } :global(.reveal) .actorBox { - fill: rgba(156, 163, 175, 0.15); - stroke: #9ca3af; + fill: color-mix(in srgb, var(--neutral-400) 15%, transparent); + stroke: var(--neutral-400); stroke-width: 3; } :global(.reveal) .boundary { - stroke: #6b7280; + stroke: var(--neutral-500); stroke-width: 3; } :global(.reveal) .boundaryLabel { - fill: #9ca3af; + fill: var(--neutral-400); font-size: 18px; } :global(.reveal) .moduleLabel { - fill: #e5e7eb; + fill: var(--neutral-100); font-size: 22px; } :global(.reveal) .actorLabel { - fill: #e5e7eb; + fill: var(--neutral-100); font-size: 20px; } :global(.reveal) .arrowLabel { - fill: #d1d5db; + fill: var(--neutral-200); font-size: 16px; - stroke: #000; + stroke: var(--ifm-background-color); stroke-width: 6px; } :global(.reveal) .contractArrow { - stroke: #a78bfa; + stroke: var(--visual-cyan); stroke-width: 4; } :global(.reveal) .contractMarker { - fill: #a78bfa; + fill: var(--visual-cyan); } :global(.reveal) .inputArrow { - stroke: #22d3ee; + stroke: var(--visual-success); stroke-width: 4; } :global(.reveal) .inputMarker { - fill: #22d3ee; + fill: var(--visual-success); } :global(.reveal) .outputArrow { - stroke: #fb923c; + stroke: var(--visual-warning); stroke-width: 4; } :global(.reveal) .outputMarker { - fill: #fb923c; + fill: var(--visual-warning); } :global(.reveal) .legendLine { @@ -262,7 +262,7 @@ } :global(.reveal) .legendLabel { - fill: #e5e7eb; + fill: var(--neutral-100); font-size: 16px; } diff --git a/website/src/components/VisualElements/SystemBoundaryDiagram.tsx b/website/src/components/VisualElements/SystemBoundaryDiagram.tsx index 1184ad8..5e2fa99 100644 --- a/website/src/components/VisualElements/SystemBoundaryDiagram.tsx +++ b/website/src/components/VisualElements/SystemBoundaryDiagram.tsx @@ -49,9 +49,9 @@ const DEFAULT_OUTPUTS: ArrowDef[] = [ const DEFAULT_LEGEND_Y = 440; const DEFAULT_LEGEND_ENTRIES = [ - { x1: 40, x2: 70, textX: 77, label: 'Contract (internal)', arrowId: 'arrow-contract', strokeVar: 'var(--visual-workflow)' }, - { x1: 210, x2: 240, textX: 247, label: 'Input (external)', arrowId: 'arrow-input', strokeVar: 'var(--visual-capability)' }, - { x1: 375, x2: 405, textX: 412, label: 'Output (external)', arrowId: 'arrow-output', strokeVar: 'var(--visual-limitation)' }, + { x1: 40, x2: 70, textX: 77, label: 'Contract (internal)', arrowId: 'arrow-contract', strokeVar: 'var(--visual-cyan)' }, + { x1: 210, x2: 240, textX: 247, label: 'Input (external)', arrowId: 'arrow-input', strokeVar: 'var(--visual-success)' }, + { x1: 375, x2: 405, textX: 412, label: 'Output (external)', arrowId: 'arrow-output', strokeVar: 'var(--visual-warning)' }, ]; const DEFAULT_VIEWBOX = { x: -47, y: -15, w: 587, h: 463 }; @@ -93,9 +93,9 @@ const COMPACT_OUTPUTS: ArrowDef[] = [ const COMPACT_LEGEND_Y = 272; const COMPACT_LEGEND_ENTRIES = [ - { x1: 240, x2: 270, textX: 277, label: 'Contract (internal)', arrowId: 'arrow-contract', strokeVar: 'var(--visual-workflow)' }, - { x1: 420, x2: 450, textX: 457, label: 'Input (external)', arrowId: 'arrow-input', strokeVar: 'var(--visual-capability)' }, - { x1: 600, x2: 630, textX: 637, label: 'Output (external)', arrowId: 'arrow-output', strokeVar: 'var(--visual-limitation)' }, + { x1: 240, x2: 270, textX: 277, label: 'Contract (internal)', arrowId: 'arrow-contract', strokeVar: 'var(--visual-cyan)' }, + { x1: 420, x2: 450, textX: 457, label: 'Input (external)', arrowId: 'arrow-input', strokeVar: 'var(--visual-success)' }, + { x1: 600, x2: 630, textX: 637, label: 'Output (external)', arrowId: 'arrow-output', strokeVar: 'var(--visual-warning)' }, ]; const COMPACT_VIEWBOX = { x: -25, y: -20, w: 970, h: 300 }; diff --git a/website/src/components/VisualElements/SystemFlowDiagram.module.css b/website/src/components/VisualElements/SystemFlowDiagram.module.css index a378ab5..afe8a3f 100644 --- a/website/src/components/VisualElements/SystemFlowDiagram.module.css +++ b/website/src/components/VisualElements/SystemFlowDiagram.module.css @@ -53,15 +53,15 @@ /* Main flow step boxes */ .stepBox { - fill: var(--visual-bg-workflow); - stroke: var(--visual-workflow); + fill: var(--visual-bg-cyan); + stroke: var(--visual-cyan); stroke-width: 2.5; transition: all 0.2s ease; } .stepBox:hover { - fill: var(--visual-bg-decision); - stroke: var(--visual-decision); + fill: var(--visual-bg-indigo); + stroke: var(--visual-indigo); } /* Step labels */ @@ -74,7 +74,7 @@ /* Main flow arrows */ .mainArrow { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 3; fill: none; stroke-linecap: round; @@ -83,19 +83,19 @@ /* Fork/Join paths for concurrent phases */ .forkPath, .joinPath { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 3; fill: none; stroke-linecap: round; } .arrowMarker { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } /* Concurrent phase bracket */ .concurrentBracket { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2; stroke-dasharray: 4 2; fill: none; @@ -103,13 +103,13 @@ /* Success terminal */ .successTerminal { - fill: var(--visual-bg-capability); - stroke: var(--visual-capability); + fill: var(--visual-bg-success); + stroke: var(--visual-success); stroke-width: 2.5; } .checkmark { - fill: var(--visual-capability); + fill: var(--visual-success); font-size: 22px; font-weight: bold; } @@ -131,8 +131,8 @@ } .exitMarkerSuccess { - fill: var(--visual-bg-capability); - stroke: var(--visual-capability); + fill: var(--visual-bg-success); + stroke: var(--visual-success); stroke-width: 1.5; } @@ -165,62 +165,62 @@ /* Dark mode specific adjustments for presentations */ :global(.reveal) .stepBox { - fill: rgba(167, 139, 250, 0.2); - stroke: #a78bfa; + fill: color-mix(in srgb, var(--visual-cyan) 20%, transparent); + stroke: var(--visual-cyan); stroke-width: 3; } :global(.reveal) .mainArrow, :global(.reveal) .forkPath, :global(.reveal) .joinPath { - stroke: #a78bfa; + stroke: var(--visual-cyan); stroke-width: 4; stroke-linecap: round; } :global(.reveal) .arrowMarker { - fill: #a78bfa; + fill: var(--visual-cyan); } :global(.reveal) .concurrentBracket { - stroke: #a78bfa; + stroke: var(--visual-cyan); stroke-width: 3; } :global(.reveal) .stepLabel { - fill: #e5e7eb; + fill: var(--neutral-100); font-size: 22px; } :global(.reveal) .successTerminal { - fill: rgba(34, 197, 94, 0.2); - stroke: #22c55e; + fill: color-mix(in srgb, var(--semantic-success) 20%, transparent); + stroke: var(--semantic-success); stroke-width: 3; } :global(.reveal) .checkmark { - fill: #22c55e; + fill: var(--semantic-success); font-size: 26px; } :global(.reveal) .exitPath { - stroke: #9ca3af; + stroke: var(--neutral-400); stroke-width: 2.5; } :global(.reveal) .exitMarkerError { - fill: rgba(239, 68, 68, 0.2); - stroke: #ef4444; + fill: color-mix(in srgb, var(--semantic-error) 20%, transparent); + stroke: var(--semantic-error); stroke-width: 2; } :global(.reveal) .exitMarkerSuccess { - fill: rgba(34, 197, 94, 0.2); - stroke: #22c55e; + fill: color-mix(in srgb, var(--semantic-success) 20%, transparent); + stroke: var(--semantic-success); stroke-width: 2; } :global(.reveal) .exitCode { - fill: #e5e7eb; + fill: var(--neutral-100); font-size: 16px; } diff --git a/website/src/components/VisualElements/ThreeContextWorkflow.module.css b/website/src/components/VisualElements/ThreeContextWorkflow.module.css index b0eaac6..4abb5d4 100644 --- a/website/src/components/VisualElements/ThreeContextWorkflow.module.css +++ b/website/src/components/VisualElements/ThreeContextWorkflow.module.css @@ -46,15 +46,15 @@ /* Context Boxes */ .contextBox { - fill: var(--visual-bg-workflow); - stroke: var(--visual-workflow); + fill: var(--visual-bg-cyan); + stroke: var(--visual-cyan); stroke-width: 3; transition: all 0.3s ease; } .contextBox:hover { - fill: var(--visual-bg-decision); - stroke: var(--visual-decision); + fill: var(--visual-bg-indigo); + stroke: var(--visual-indigo); } /* Text Styles */ @@ -80,18 +80,18 @@ /* Arrows */ .arrow { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 4; fill: none; } .arrowMarker { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } /* Separation Barriers */ .barrier { - stroke: var(--visual-decision); + stroke: var(--visual-indigo); stroke-width: 2; opacity: 0.3; } @@ -162,27 +162,27 @@ /* Dark mode specific adjustments for presentations */ :global(.reveal) .contextBox { - fill: rgba(167, 139, 250, 0.2); - stroke: #a78bfa; + fill: color-mix(in srgb, var(--visual-cyan) 20%, transparent); + stroke: var(--visual-cyan); stroke-width: 3; } :global(.reveal) .arrow { - stroke: #a78bfa; + stroke: var(--visual-cyan); stroke-width: 5; } :global(.reveal) .arrowMarker { - fill: #a78bfa; + fill: var(--visual-cyan); } :global(.reveal) .barrier { - stroke: #c4b5fd; + stroke: var(--visual-indigo); opacity: 0.4; } :global(.reveal) .contextTitle, :global(.reveal) .contextLabel, :global(.reveal) .contextDescription { - fill: #e5e7eb; + fill: var(--neutral-100); } diff --git a/website/src/components/VisualElements/UShapeAttentionCurve.module.css b/website/src/components/VisualElements/UShapeAttentionCurve.module.css index 9e6ac7f..21f60c9 100644 --- a/website/src/components/VisualElements/UShapeAttentionCurve.module.css +++ b/website/src/components/VisualElements/UShapeAttentionCurve.module.css @@ -2,8 +2,8 @@ margin: 2rem 0; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-workflow); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); } /* Compact mode for presentations - maximize diagram size */ @@ -160,41 +160,36 @@ } .button { - padding: 0.5rem 1rem; - font-size: 0.85rem; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--target-sm); + padding: 0 var(--space-2); + font-family: var(--font-body); + font-size: var(--text-sm); font-weight: 500; - border: 1px solid var(--visual-workflow); - background: var(--ifm-background-surface-color); - color: var(--visual-workflow); - border-radius: 6px; + border: 1px solid var(--border-default); + background: transparent; + color: var(--text-heading); + border-radius: var(--radius-sm); cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + transition: border-color 0.15s ease, background 0.15s ease; min-width: 70px; } .button:hover { - background: var(--visual-workflow); - color: white; - transform: translateY(-1px); - /* Shadow uses --brand-primary with alpha - updates automatically in dark mode */ - box-shadow: 0 2px 8px - color-mix(in srgb, var(--brand-primary) 30%, transparent); + border-color: var(--neutral-400); } .buttonActive { - background: var(--visual-workflow); - color: white; + background: var(--text-heading); + color: var(--surface-page); + border-color: var(--text-heading); font-weight: 600; - /* Shadow uses --brand-primary with alpha - updates automatically in dark mode */ - box-shadow: 0 2px 4px - color-mix(in srgb, var(--brand-primary) 20%, transparent); } .buttonActive:hover { - transform: translateY(0); - /* Shadow uses --brand-primary with alpha - updates automatically in dark mode */ - box-shadow: 0 2px 4px - color-mix(in srgb, var(--brand-primary) 20%, transparent); + border-color: var(--neutral-400); } /* Explanation text */ @@ -204,8 +199,7 @@ color: var(--ifm-color-emphasis-700); padding: 1rem; background: var(--ifm-background-surface-color); - border-radius: 6px; - border-left: 3px solid var(--visual-workflow); + border-left: 3px solid var(--visual-cyan); } .explanation strong { @@ -258,5 +252,5 @@ ); } -/* Note: button shadows now use color-mix with --brand-primary, +/* Button shadows use color-mix with --visual-cyan, which automatically adapts to light/dark mode */ diff --git a/website/src/components/VisualElements/UShapeAttentionCurve.tsx b/website/src/components/VisualElements/UShapeAttentionCurve.tsx index 31f5cb7..666db66 100644 --- a/website/src/components/VisualElements/UShapeAttentionCurve.tsx +++ b/website/src/components/VisualElements/UShapeAttentionCurve.tsx @@ -207,7 +207,7 @@ export default function UShapeAttentionCurve({ @@ -226,12 +226,12 @@ export default function UShapeAttentionCurve({ @@ -283,7 +283,7 @@ export default function UShapeAttentionCurve({ cx={startX} cy={startY} r="8" - fill="var(--visual-capability)" + fill="var(--visual-success)" className={styles.marker} /> @@ -301,7 +301,7 @@ export default function UShapeAttentionCurve({ cx={endX} cy={endY} r="8" - fill="var(--visual-capability)" + fill="var(--visual-success)" className={styles.marker} /> diff --git a/website/src/components/VisualElements/WorkflowCircle.module.css b/website/src/components/VisualElements/WorkflowCircle.module.css index 480c458..6157fb3 100644 --- a/website/src/components/VisualElements/WorkflowCircle.module.css +++ b/website/src/components/VisualElements/WorkflowCircle.module.css @@ -2,8 +2,8 @@ margin: 2rem auto; padding: 1.5rem; border-radius: 8px; - background: var(--visual-bg-workflow); - border: 1px solid var(--visual-workflow); + background: var(--visual-bg-cyan); + border: 1px solid var(--visual-cyan); max-width: 600px; animation: fadeIn 0.6s ease-in-out; } @@ -46,7 +46,7 @@ /* Phase circle nodes */ .phaseCircle { fill: var(--ifm-background-surface-color); - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 3; transition: all 0.3s ease; } @@ -96,7 +96,7 @@ /* Arrow paths - solid for forward flow */ .arrowPath { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2.5; fill: none; stroke-linecap: round; @@ -107,7 +107,7 @@ /* Dashed arrow for iteration (Validate -> Research) */ .arrowPathDashed { - stroke: var(--visual-workflow); + stroke: var(--visual-cyan); stroke-width: 2.5; fill: none; stroke-linecap: round; @@ -124,11 +124,11 @@ /* Arrow markers */ .arrowMarker polygon { - fill: var(--visual-workflow); + fill: var(--visual-cyan); } .arrowMarkerDashed polygon { - fill: var(--visual-workflow); + fill: var(--visual-cyan); opacity: 0.7; } diff --git a/website/src/components/VisualElements/icons/ArchitectIcon.tsx b/website/src/components/VisualElements/icons/ArchitectIcon.tsx new file mode 100644 index 0000000..1d4dd48 --- /dev/null +++ b/website/src/components/VisualElements/icons/ArchitectIcon.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ArchitectIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Set square body */} + + {/* Right-angle marker at bottom-left corner */} + + {/* Tick marks along hypotenuse */} + + + + ); +} diff --git a/website/src/components/VisualElements/icons/BugIcon.tsx b/website/src/components/VisualElements/icons/BugIcon.tsx new file mode 100644 index 0000000..edb7390 --- /dev/null +++ b/website/src/components/VisualElements/icons/BugIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function BugIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Head */} + + {/* Body */} + + {/* Left antenna */} + + {/* Right antenna */} + + {/* Left legs */} + + {/* Right legs */} + + + ); +} diff --git a/website/src/components/VisualElements/icons/ChecklistIcon.tsx b/website/src/components/VisualElements/icons/ChecklistIcon.tsx new file mode 100644 index 0000000..48fdf46 --- /dev/null +++ b/website/src/components/VisualElements/icons/ChecklistIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +// Checklist icon — 3 rows: 2 checked items + 1 pending circle. +// Communicates "structured plan in progress." Smooth Circuit throughout. +// Lines taper (11px → 8px → 6px) for visual progression. +export default function ChecklistIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Row 1 — checked */} + + + {/* Row 2 — checked */} + + + {/* Row 3 — pending (open circle, shorter line) */} + + + + ); +} diff --git a/website/src/components/VisualElements/icons/ClockIcon.tsx b/website/src/components/VisualElements/icons/ClockIcon.tsx new file mode 100644 index 0000000..a7b5cf2 --- /dev/null +++ b/website/src/components/VisualElements/icons/ClockIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ClockIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Clock face */} + + {/* Minute hand — pointing to 12 o'clock */} + + {/* Hour hand — pointing to ~2 o'clock */} + + {/* Center pivot */} + + + ); +} diff --git a/website/src/components/VisualElements/icons/ExecuteIcon.tsx b/website/src/components/VisualElements/icons/ExecuteIcon.tsx new file mode 100644 index 0000000..ed24ccd --- /dev/null +++ b/website/src/components/VisualElements/icons/ExecuteIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ExecuteIcon({ className, size = 24 }: IconProps) { + return ( + + {/* > prompt chevron */} + + {/* _ cursor */} + + + ); +} diff --git a/website/src/components/VisualElements/icons/PencilIcon.tsx b/website/src/components/VisualElements/icons/PencilIcon.tsx new file mode 100644 index 0000000..7941ddd --- /dev/null +++ b/website/src/components/VisualElements/icons/PencilIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function PencilIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Shaft parallelogram at 45° */} + + {/* Triangular nib */} + + {/* Collar — graphite/wood boundary */} + + {/* Center axis */} + + + ); +} diff --git a/website/src/components/VisualElements/icons/PlanExecuteIcon.tsx b/website/src/components/VisualElements/icons/PlanExecuteIcon.tsx new file mode 100644 index 0000000..4a3bb56 --- /dev/null +++ b/website/src/components/VisualElements/icons/PlanExecuteIcon.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { AgentNode } from '../ActorNodes'; + +interface IconProps { + className?: string; + size?: number; +} + +// Checklist (left column) + AgentNode badge (right column). +// Rows at y=4/10/16. Agent at x=10,y=7 — head top at y≈8.05, center at y≈14.5. +// Row 1 complete (check+line above agent). Rows 2 & 3 show marks only; agent +// fills the right zone beside them (no x overlap: marks ≤x8, head ≥x11.05). +export default function PlanExecuteIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Checklist strokes inherit currentColor */} + + {/* Row 1 — checked */} + + + {/* Row 2 — checked (no line; agent occupies right zone) */} + + {/* Row 3 — pending circle only (agent occupies right side) */} + + + {/* AgentNode S=14 — right column, raised to y=7 for tighter composition. + Head top lands at parent y≈8.05; center at y≈14.5 (between rows 2 & 3). + Row 1 check+line (y=4) sit cleanly above. Rows 2 & 3 marks are to the + left (x≤8) — no x overlap with head (x≥11.05). */} + + + + + ); +} diff --git a/website/src/components/VisualElements/icons/ResearchIcon.tsx b/website/src/components/VisualElements/icons/ResearchIcon.tsx new file mode 100644 index 0000000..8c9c813 --- /dev/null +++ b/website/src/components/VisualElements/icons/ResearchIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ResearchIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Lens circle */} + + {/* Handle */} + + {/* Scan lines — simulate circular aperture clipping */} + + + + + ); +} diff --git a/website/src/components/VisualElements/icons/ReviewLensIcon.tsx b/website/src/components/VisualElements/icons/ReviewLensIcon.tsx new file mode 100644 index 0000000..118a4c3 --- /dev/null +++ b/website/src/components/VisualElements/icons/ReviewLensIcon.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ReviewLensIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Lens circle */} + + {/* Handle */} + + {/* Checkmark inside lens */} + + + ); +} diff --git a/website/src/components/VisualElements/icons/ValidateIcon.tsx b/website/src/components/VisualElements/icons/ValidateIcon.tsx new file mode 100644 index 0000000..0fc940d --- /dev/null +++ b/website/src/components/VisualElements/icons/ValidateIcon.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface IconProps { + className?: string; + size?: number; +} + +export default function ValidateIcon({ className, size = 24 }: IconProps) { + return ( + + {/* Post */} + + {/* Beam */} + + {/* Base */} + + {/* Pivot pin */} + + {/* Left chain and pan */} + + + {/* Right chain and pan — 2px lower (weighing in progress) */} + + + + ); +} diff --git a/website/src/components/animations/NarrativeFigure.tsx b/website/src/components/animations/NarrativeFigure.tsx new file mode 100644 index 0000000..570dd11 --- /dev/null +++ b/website/src/components/animations/NarrativeFigure.tsx @@ -0,0 +1,99 @@ +import React, { type ReactNode } from 'react'; +import { useScrollNarrative, type NarrativeChapter } from '../../hooks/useScrollNarrative'; + +interface NarrativeChapterDef extends NarrativeChapter { + content: ReactNode; +} + +interface NarrativeFigureProps { + chapters: NarrativeChapterDef[]; + figure: (activeChapter: string, progress: number) => ReactNode; + caption: string; + className?: string; +} + +const ACCENT_ACTIVE = 'var(--visual-cyan)'; +const ACCENT_IDLE = 'var(--border-subtle)'; + +export default function NarrativeFigure({ + chapters, + figure, + caption, + className, +}: NarrativeFigureProps) { + const { containerRef, figureRef, chapterRefs, activeChapter, progress } = + useScrollNarrative({ chapters }); + + return ( +
} + className={className} + style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }} + > + + +
+ {/* Figure column */} +
} + className="narrative-figure-col" + style={{ position: 'static' }} + > +
+ {figure(activeChapter, progress)} +
+ {caption} +
+
+
+ + {/* Chapters column */} +
+ {chapters.map((chapter, i) => { + const isActive = chapter.id === activeChapter; + return ( +
} + style={{ + borderLeft: `3px solid ${isActive ? ACCENT_ACTIVE : ACCENT_IDLE}`, + paddingLeft: 'var(--space-3)', + opacity: isActive ? 1 : 0.6, + transition: 'border-color 0.3s ease, opacity 0.3s ease', + }} + > + {chapter.label && ( +
+ {chapter.label} +
+ )} + {chapter.content} +
+ ); + })} +
+
+
+ ); +} diff --git a/website/src/components/animations/ScrollDrivenFigure.module.css b/website/src/components/animations/ScrollDrivenFigure.module.css new file mode 100644 index 0000000..58f3439 --- /dev/null +++ b/website/src/components/animations/ScrollDrivenFigure.module.css @@ -0,0 +1,64 @@ +.figure { + position: relative; + width: 100%; + margin-block: var(--space-4); +} + +/* Base state: hidden before scroll drives it in */ +.inner { + opacity: 0; + transform: translateY(12px); + transition: opacity 0.4s ease, transform 0.4s ease; +} + +/* Fallback: IO has revealed it */ +.inner.revealed { + opacity: 1; + transform: translateY(0); +} + +/* When the feature is unsupported, show content immediately */ +.noScrollTimeline .inner { + opacity: 1; + transform: none; + transition: none; +} + +/* Scroll-driven path — only applied when CSS supports it */ +@supports (animation-timeline: scroll()) { + .inner { + animation-name: scrollReveal; + animation-fill-mode: both; + animation-timing-function: linear; + animation-timeline: view(); + animation-range: entry 0% cover 50%; + /* Override JS-transition fallback — CSS drives this entirely */ + transition: none; + } +} + +@keyframes scrollReveal { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + + +@media (prefers-reduced-motion: reduce) { + .inner { + animation: none; + opacity: 1; + transform: none; + transition: none; + } + + .inner.revealed { + opacity: 1; + transform: none; + } +} diff --git a/website/src/components/animations/ScrollDrivenFigure.tsx b/website/src/components/animations/ScrollDrivenFigure.tsx new file mode 100644 index 0000000..875129f --- /dev/null +++ b/website/src/components/animations/ScrollDrivenFigure.tsx @@ -0,0 +1,113 @@ +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; +import styles from './ScrollDrivenFigure.module.css'; + +// Context: 0.0 (not yet visible) → 1.0 (fully scrolled through) +const AnimationPhaseContext = createContext(0); +export const useAnimationPhase = () => useContext(AnimationPhaseContext); + +interface ScrollDrivenFigureProps { + children: ReactNode; + caption?: string; + className?: string; + phaseEnd?: number; +} + +function supportsScrollTimeline(): boolean { + if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return false; + return CSS.supports('animation-timeline', 'scroll()'); +} + +export default function ScrollDrivenFigure({ + children, + caption, + className, + phaseEnd = 0.5, +}: ScrollDrivenFigureProps) { + const innerRef = useRef(null); + const [phase, setPhase] = useState(0); + const [revealed, setRevealed] = useState(false); + const [noScrollTimeline, setNoScrollTimeline] = useState(false); + + useEffect(() => { + const el = innerRef.current; + if (!el) return; + + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reducedMotion) { + setPhase(1); + setRevealed(true); + return; + } + + const cssSupported = supportsScrollTimeline(); + + if (!cssSupported) { + setNoScrollTimeline(true); + + // Fallback: IntersectionObserver one-shot reveal + const io = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setRevealed(true); + setPhase(1); + io.disconnect(); + } + }, + { threshold: 0.15 }, + ); + io.observe(el); + return () => io.disconnect(); + } + + // CSS drives the visual animation; mirror scroll progress to JS context so child + // components can read animationPhase. document.scroll is the spec-correct target for + // the view() timeline scroller; window.resize handles viewport-dimension changes. + const computePhase = () => { + const rect = el.getBoundingClientRect(); + const vh = window.innerHeight; + // entry 0% = rect.bottom === vh, cover N% = rect.top === vh * phaseEnd + const start = vh; + const end = vh * phaseEnd; + const raw = (rect.bottom - start) / (end - start); + setPhase(Math.min(1, Math.max(0, raw))); + }; + + document.addEventListener('scroll', computePhase, { passive: true }); + window.addEventListener('resize', computePhase, { passive: true }); + + return () => { + document.removeEventListener('scroll', computePhase); + window.removeEventListener('resize', computePhase); + }; + }, []); + + const figureClass = [ + styles.figure, + noScrollTimeline ? styles.noScrollTimeline : '', + className ?? '', + ] + .filter(Boolean) + .join(' '); + + const innerClass = [styles.inner, revealed ? styles.revealed : ''] + .filter(Boolean) + .join(' '); + + return ( + +
+
+ {children} +
+ {caption &&
{caption}
} +
+
+ ); +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index db45273..1e30ec9 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -4,180 +4,437 @@ * work well for content-centric websites. */ +/* Self-hosted font faces */ +@import './fonts.css'; + /* Import unified presentation system */ @import '../styles/presentation-system.css'; +/* Motion tokens: durations, easings, stagger, reveal offsets */ +@import './motion-tokens.css'; + /* ======================================================================== - AI CODING COURSE - COLOR SYSTEM (Single Source of Truth) + AGENTIC CODING — DESIGN TOKEN SYSTEM (Single Source of Truth) ======================================================================== - All colors are defined here using a token-based system. - Components and content should ONLY reference these CSS variables, - never use hardcoded color values. - - Color System Structure: - 1. Brand Colors - Purple AI theme for primary/interactive elements - 2. Semantic Colors - Success, warning, error, info (readable with purple) - 3. Gradient Endpoints - For minimal accent gradients - 4. Infima Mapping - Maps brand colors to Docusaurus theme - 5. Visual Element Colors - For interactive components and diagrams - - WCAG Accessibility Compliance: - - All text colors meet WCAG AA minimum (4.5:1 contrast for normal text) - - Large text and UI components meet 3:1 minimum - - Purple brand color (4.84:1 on white) used only for accents, not body text - - Colorblind-safe: No critical information conveyed by color alone - - All interactive elements have non-color indicators (text, icons, patterns) - - Dark Mode: All colors have optimized variants in [data-theme='dark'] - Tested with: WebAIM Contrast Checker, Chrome DevTools colorblind simulation + Token Architecture (per DESIGN_SYSTEM.md): + 1. Primitive — Raw palette values (--neutral-*, brand hex) + 2. Semantic — UI surfaces, text, borders, illustration (--surface-*, --text-*, --visual-*) + 3. Component — Per-component overrides (in .module.css files) + + Color System: + - Achromatic base (C:0.000) — no tinted neutrals + - 9 chromatic hues with equal standing — no privileged brand hue + - Color = meaning — every hue use answers "what does this hue mean here?" + - 60-30-10 budget — 60% achromatic, 30% elevated gray, 10% semantic color + + Style constraint: Flat only — no gradients, no shadows, no glows. + Solid colors, clean borders, whitespace hierarchy. + + WCAG Accessibility: + - Shade-600 on white >= 4.5:1 (light mode text) + - Shade-400 on #0d1117 >= 4.5:1 (dark mode text) + - Redundant encoding: every color signal has a non-color indicator + ======================================================================== */ :root { - /* === BRAND COLORS - Purple AI Theme === */ - /* WCAG: #7c3aed on white = 4.84:1 (AA Pass for normal text) */ - /* Use only for accents, borders, badges - NOT body text */ - --brand-primary: #7c3aed; /* Rich violet - main brand color */ - --brand-primary-dark: #6d28d9; /* Deeper violet - hover states */ - --brand-primary-darker: #6020c0; /* Even deeper - active states */ - --brand-primary-darkest: #5b21b6; /* Maximum depth - pressed states */ - --brand-primary-light: #8b5cf6; /* Lighter violet - accents */ - --brand-primary-lighter: #a78bfa; /* Soft violet - highlights */ - --brand-primary-lightest: #c4b5fd; /* Pale violet - subtle backgrounds */ - - /* === SEMANTIC COLORS - Colorblind-Safe & WCAG Compliant === */ - /* Cyan/Orange/Rose are distinguishable for most types of colorblindness */ - /* Always paired with icons, text labels, or patterns for accessibility */ - --semantic-success: #06b6d4; /* Cyan - distinguishable from red/green for protanopia/deuteranopia */ - --semantic-warning: #f97316; /* Rose-orange - warm accent, distinguishable from blue */ - --semantic-error: #e11d48; /* Rose-red - distinct from green for colorblind users */ - --semantic-info: #3b82f6; /* Violet-blue - analogous to purple */ - --semantic-neutral: #64748b; /* Slate - cool gray with purple tint */ - - /* === GRADIENT ENDPOINTS - Harmonized Purple-Tinted === */ - --gradient-warm-end: #ec4899; /* Fuchsia - purple harmony */ - --gradient-warning-end: #fb923c; /* Rose-orange - matches warning */ - --gradient-critical-end: #fb7185; /* Rose-pink - matches error */ - - /* === MAP TO INFIMA (Docusaurus Framework) === */ - --ifm-color-primary: var(--brand-primary); - --ifm-color-primary-dark: var(--brand-primary-dark); - --ifm-color-primary-darker: var(--brand-primary-darker); - --ifm-color-primary-darkest: var(--brand-primary-darkest); - --ifm-color-primary-light: var(--brand-primary-light); - --ifm-color-primary-lighter: var(--brand-primary-lighter); - --ifm-color-primary-lightest: var(--brand-primary-lightest); - - /* === VISUAL ELEMENT COLORS - For Interactive Components === */ - --visual-workflow: var(--brand-primary); /* Purple for AI workflows/agents */ - --visual-capability: var( - --semantic-success - ); /* Green for capabilities/success */ - --visual-limitation: var( - --semantic-warning - ); /* Amber for limitations/warnings */ - --visual-decision: var( - --brand-primary-light - ); /* Light purple for decision points */ - --visual-error: var(--semantic-error); /* Red for errors/critical */ - --visual-neutral: var(--semantic-neutral); /* Gray for neutral states */ - - /* === VISUAL ELEMENT BACKGROUNDS - Transparent Variants === */ - --visual-bg-workflow: rgba(124, 58, 237, 0.1); /* Purple tint */ - --visual-bg-capability: rgba(6, 182, 212, 0.1); /* Cyan tint */ - --visual-bg-limitation: rgba(249, 115, 22, 0.1); /* Rose-orange tint */ - --visual-bg-decision: rgba(139, 92, 246, 0.1); /* Light purple tint */ - --visual-bg-error: rgba(225, 29, 72, 0.1); /* Rose-red tint */ + /* === NEUTRAL SCALE — Pure achromatic (C:0.000) === */ + --neutral-50: #f5f5f5; + --neutral-100: #e8e8e8; + --neutral-200: #d4d4d4; + --neutral-300: #b7b7b7; + --neutral-400: #9b9b9b; + --neutral-500: #808080; + --neutral-600: #666666; + --neutral-700: #505050; + --neutral-800: #3d3d3d; + --neutral-900: #2b2b2b; + --neutral-950: #222222; + + /* === TYPOGRAPHY === */ + --font-display: 'Space Grotesk', system-ui, sans-serif; + --font-body: 'Inter', system-ui, sans-serif; + --font-mono: 'Monaspace Neon', monospace; + --font-mono-ai: 'Monaspace Argon', var(--font-mono); + --font-mono-spec: 'Monaspace Xenon', var(--font-mono); + --font-mono-human: 'Monaspace Radon', var(--font-mono); + --font-mono-keyword: 'Monaspace Krypton', var(--font-mono); + --font-mono-features: 'calt' 1, 'liga' 0; + + /* === SPATIAL SYSTEM — 8px base grid === */ + --space-0: 0px; + --space-px: 1px; + --space-0h: 4px; + --space-1: 8px; + --space-2: 16px; + --space-3: 24px; + --space-4: 32px; + --space-5: 48px; + --space-6: 64px; + --space-7: 80px; + --space-8: 96px; + --space-9: 128px; + --space-10: 160px; + + /* === TYPE SCALE — Minor Third (1.200), base 16px === */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 16px; + --text-lg: 19px; + --text-xl: 23px; + --text-2xl: 28px; + --text-3xl: 33px; + --text-4xl: 40px; + --text-5xl: 48px; + + /* === LINE HEIGHTS — 8px-snapped === */ + --lh-tight: 16px; + --lh-sm: 24px; + --lh-lg: 32px; + --lh-2xl: 40px; + --lh-3xl: 48px; + --lh-4xl: 56px; + + /* === BORDER RADIUS === */ + --radius-none: 0px; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 24px; + --radius-full: 9999px; + + /* === STROKE WEIGHT SCALE — SVG illustration strokes === */ + --stroke-fine: 1px; + --stroke-light: 1.5px; + --stroke-default: 2px; + --stroke-medium: 2.5px; + --stroke-heavy: 3px; + --stroke-accent: 4px; + + /* === ICON CANVAS — Square viewBox sizes === */ + --icon-sm: 16px; + --icon-md: 24px; + --icon-lg: 32px; + --icon-xl: 48px; + --icon-2xl: 64px; + + /* === INTERACTIVE TARGET SIZES === */ + --target-sm: 32px; + --target-md: 40px; + --target-lg: 48px; + --target-xl: 56px; + + --ifm-font-family-base: var(--font-body); + --ifm-heading-font-family: var(--font-display); + --ifm-font-family-monospace: var(--font-mono); + --ifm-h1-font-size: var(--text-4xl); /* 40px — design system h1 token */ + + /* === NAVBAR — Flat, line-based (Design System rule #4) === */ + --ifm-navbar-background-color: var(--surface-page); + --ifm-navbar-shadow: none; + --ifm-navbar-height: var(--space-7); /* 80px — 8px grid */ + --ifm-navbar-padding-horizontal: var(--space-5); /* 48px */ + --ifm-navbar-padding-vertical: 0; + --ifm-navbar-item-padding-horizontal: var(--space-2); /* 16px */ + --ifm-navbar-item-padding-vertical: 0; + --ifm-navbar-link-color: var(--text-body); + --ifm-navbar-link-hover-color: var(--text-heading); + + /* === BREADCRUMBS — Metadata/wayfinding level === */ + --ifm-breadcrumb-color-active: var(--text-body); /* #505050 — current page item */ + --ifm-breadcrumb-item-background-active: var(--surface-raised); /* token-aligned pill bg */ + + /* === DOC SIDEBAR — Design system tokens === */ + --ifm-menu-color: var(--text-body); + --ifm-menu-color-active: var(--text-heading); + --ifm-menu-color-background-active: var(--surface-raised); + --ifm-menu-color-background-hover: var(--surface-muted); + --ifm-menu-link-padding-horizontal: var(--space-2); /* 16px, was 12px */ + --ifm-menu-link-padding-vertical: var(--space-1); /* 8px, was 6px → 40px touch targets */ + --ifm-menu-link-sublist-icon-filter: none; + --ifm-toc-border-color: var(--border-default); + --ifm-toc-link-color: var(--text-muted); /* #808080 light / #9b9b9b dark */ + --ifm-toc-padding-vertical: var(--space-1); /* 8px — 8px grid */ + --ifm-toc-padding-horizontal: var(--space-1); /* 8px — 8px grid */ + --docusaurus-collapse-button-bg: var(--surface-raised); + --docusaurus-collapse-button-bg-hover: var(--surface-muted); + --ifm-scrollbar-track-background-color: var(--surface-raised); + --ifm-scrollbar-thumb-background-color: var(--neutral-300); + --ifm-scrollbar-thumb-hover-background-color: var(--neutral-400); + + /* === SURFACE / TEXT / BORDER — Semantic layout tokens === */ + --surface-page: #ffffff; + --surface-raised: var(--neutral-50); /* #f5f5f5 */ + --surface-muted: var(--neutral-100); /* #e8e8e8 */ + --text-heading: var(--neutral-900); /* #2b2b2b */ + --text-body: var(--neutral-700); /* #505050 */ + --text-muted: var(--neutral-500); /* #808080 */ + --border-default: var(--neutral-200); /* #d4d4d4 */ + --border-subtle: var(--neutral-200); /* #d4d4d4 — hairline dividers */ + + /* Map to Infima layout variables */ + --ifm-background-color: var(--surface-page); + --ifm-background-surface-color: var(--surface-raised); + --ifm-container-width-xl: 960px; /* Reference-book prose width (~67ch at 16px) */ + + /* === UTILITY TOKENS === */ + --code-bg: var(--neutral-50); + --text-subtle: var(--neutral-500); + --icon-star: #e6aa63; /* warning-300 */ + + /* === CHROMATIC PRIMITIVES — Raw palette values === */ + --cyan-300: #2ad0d0; + --cyan-400: #00b2b2; + --cyan-600: #007576; + --cyan-700: #005c5c; + + /* === SEMANTIC COLORS === */ + --semantic-success: #00894d; /* Success optimal OKLCH(0.552 0.137 155°) */ + --semantic-warning: #a76900; /* Warning optimal OKLCH(0.575 0.125 70°) */ + --semantic-error: #ee0028; /* Error optimal OKLCH(0.598 0.242 25°) */ + --semantic-info: #0079d1; /* Indigo optimal OKLCH(0.568 0.161 250°) */ + --semantic-neutral: #666666; /* Neutral-600 — achromatic, unchanged */ + + /* === MAP TO INFIMA (Docusaurus Framework) — Neutral primary === */ + --ifm-color-primary: var(--neutral-900); + --ifm-color-primary-dark: var(--neutral-950); + --ifm-color-primary-darker: var(--neutral-950); + --ifm-color-primary-darkest: #000000; + --ifm-color-primary-light: var(--neutral-800); + --ifm-color-primary-lighter: var(--neutral-700); + --ifm-color-primary-lightest: var(--neutral-600); + + /* === VISUAL ELEMENT COLORS — Diagram & illustration palette === + Role names for universal UI concepts (error, warning, success). + Hue names for categorical diagram palette (cyan, indigo, etc.). + See DESIGN_SYSTEM.md "Semantic Meaning Map" for hue→meaning. */ + + /* Role-based (universal UI) */ + --visual-error: var(--semantic-error); /* #ee0028 — Danger, critical */ + --visual-warning: var(--semantic-warning); /* #a76900 — Caution, attention */ + --visual-success: var(--semantic-success); /* #00894d — Validated, complete */ + --visual-neutral: var(--semantic-neutral); /* #666666 — Neutral */ + + /* Hue-based (categorical) */ + --visual-lime: var(--visual-success); /* Alias → success; lime removed from spectrum */ + --visual-cyan: #008485; /* Cyan C=0.095 OKLCH(0.557 0.095 195°) — gamut limit */ + --visual-indigo: #307ac0; /* Indigo C=0.13 OKLCH(0.567 0.13 250°) */ + --visual-violet: #736cc3; /* Violet C=0.13 OKLCH(0.578 0.13 285°) */ + --visual-magenta: #9d5fab; /* Magenta C=0.13 OKLCH(0.583 0.13 320°) */ + --visual-rose: var(--visual-neutral); /* Alias → neutral; rose removed from spectrum */ + + /* === VISUAL ELEMENT BACKGROUNDS — 10% tint in light mode === */ + --visual-bg-error: color-mix(in srgb, var(--visual-error) 10%, transparent); + --visual-bg-warning: color-mix(in srgb, var(--visual-warning) 10%, transparent); + --visual-bg-lime: var(--visual-bg-success); /* Follows lime alias */ + --visual-bg-success: color-mix(in srgb, var(--visual-success) 10%, transparent); + --visual-bg-cyan: color-mix(in srgb, var(--visual-cyan) 10%, transparent); + --visual-bg-indigo: color-mix(in srgb, var(--visual-indigo) 10%, transparent); + --visual-bg-violet: color-mix(in srgb, var(--visual-violet) 10%, transparent); + --visual-bg-magenta: color-mix(in srgb, var(--visual-magenta) 10%, transparent); + --visual-bg-rose: var(--visual-bg-neutral); /* Follows rose alias */ + --visual-bg-neutral: color-mix(in srgb, var(--visual-neutral) 10%, transparent); + + /* === GRADIENT ENDPOINTS (used by ContextWindowMeter) === */ + --gradient-warning-end: #cd8c37; /* Warning-400 */ + --gradient-critical-end: #ec7069; /* Error-400 */ /* === TYPOGRAPHY & CODE === */ --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); /* === CODE BLOCK BACKGROUNDS === */ - --code-bg-light: #f9fafb; /* Subtle gray for code blocks */ - - /* === TEXT COLOR ACCESSIBILITY GUARANTEES === */ - /* Docusaurus default text colors (--ifm-color-emphasis-*) are WCAG compliant */ - /* Light mode: Dark text on light backgrounds = 7:1+ (AAA level) */ - /* Our usage: - - Body text: --ifm-color-emphasis-700 (dark gray, ~13:1 contrast) - - Secondary text: --ifm-color-emphasis-600 (medium gray, ~7:1 contrast) - - All meet or exceed WCAG AA 4.5:1 minimum for normal text - */ + --code-bg-light: #f5f5f5; /* neutral-50 */ + + /* === SEARCH PLUGIN — Design system overrides === + @easyops-cn/docusaurus-search-local CSS custom properties. + Flat construction: no shadows. Design tokens for all colors. + Contrast verified: active suggestion 14.16:1 light, 11.56:1 dark. */ + --search-local-modal-background: var(--surface-raised); + --search-local-modal-shadow: none; + --search-local-hit-background: var(--surface-page); + --search-local-hit-shadow: none; + --search-local-hit-color: var(--text-body); + --search-local-highlight-color: var(--neutral-900); + --search-local-hit-active-color: var(--surface-page); + --search-local-muted-color: var(--text-muted); + --search-local-input-active-border-color: transparent; + --search-local-spacing: var(--space-2); + --search-local-icon-stroke-width: 2; + + /* Search icon — stroke-based magnifying glass (light: #808080, 3.95:1 on white) */ + --ifm-navbar-search-input-icon: url('data:image/svg+xml;utf8,'); + + /* === ANNOUNCEMENT BAR === */ + --announcement-bg: #ffe3c3; /* warning-100 */ + --announcement-text: #402400; /* warning-900 */ } /* ======================================================================== - DARK MODE - Optimized Colors for Dark Backgrounds + DARK MODE — Optimized Colors for Dark Backgrounds ======================================================================== - Brighter, more vibrant versions of light mode colors to ensure - sufficient contrast and reduce eye strain on dark backgrounds. + Semantic colors shift from shade-600 to shade-400. + Visual bg tints shift from 10% to 15%. + Surface colors use GitHub-style dark backgrounds. ======================================================================== */ [data-theme='dark'] { - /* === BRAND COLORS - Brighter Purple for Dark Mode === */ - --brand-primary: #a78bfa; /* Brighter violet for dark backgrounds */ - --brand-primary-dark: #8b5cf6; - --brand-primary-darker: #7c3aed; - --brand-primary-darkest: #6d28d9; - --brand-primary-light: #c4b5fd; - --brand-primary-lighter: #ddd6fe; - --brand-primary-lightest: #ede9fe; - - /* === SEMANTIC COLORS - Harmonized for Dark Mode === */ - --semantic-success: #22d3ee; /* Cyan 400 - blue harmony with purple */ - --semantic-warning: #fb923c; /* Orange 400 - rose-orange for dark */ - --semantic-error: #fb7185; /* Rose 400 - magenta-red for dark */ - --semantic-info: #60a5fa; /* Blue 400 - violet-blue for dark */ - --semantic-neutral: #94a3b8; /* Slate 400 - purple-tinted gray */ - - /* === GRADIENT ENDPOINTS - Harmonized for Dark Mode === */ - --gradient-warm-end: #f472b6; /* Fuchsia 400 - purple harmony */ - --gradient-warning-end: #fdba74; /* Orange 300 - rose-orange bright */ - --gradient-critical-end: #fda4af; /* Rose 300 - rose-pink bright */ - - /* === MAP TO INFIMA (Docusaurus Framework) === */ - --ifm-color-primary: var(--brand-primary); - --ifm-color-primary-dark: var(--brand-primary-dark); - --ifm-color-primary-darker: var(--brand-primary-darker); - --ifm-color-primary-darkest: var(--brand-primary-darkest); - --ifm-color-primary-light: var(--brand-primary-light); - --ifm-color-primary-lighter: var(--brand-primary-lighter); - --ifm-color-primary-lightest: var(--brand-primary-lightest); - - /* === VISUAL ELEMENT COLORS - Adjusted for Dark Mode === */ - --visual-workflow: var(--brand-primary); /* Brighter purple */ - --visual-capability: var(--semantic-success); /* Brighter green */ - --visual-limitation: var(--semantic-warning); /* Brighter amber */ - --visual-decision: var(--brand-primary-light); /* Lighter purple */ - --visual-error: var(--semantic-error); /* Softer red */ - --visual-neutral: var(--semantic-neutral); /* Lighter gray */ - - /* === VISUAL ELEMENT BACKGROUNDS - Harmonized Transparency === */ - --visual-bg-workflow: rgba(167, 139, 250, 0.15); /* Purple tint */ - --visual-bg-capability: rgba(34, 211, 238, 0.15); /* Cyan tint */ - --visual-bg-limitation: rgba(251, 146, 60, 0.15); /* Rose-orange tint */ - --visual-bg-decision: rgba(196, 181, 253, 0.15); /* Light purple tint */ - --visual-bg-error: rgba(251, 113, 133, 0.15); /* Rose-red tint */ + /* === SEMANTIC COLORS — shade-400 for dark mode === */ + --semantic-success: #48b475; /* Success-400 */ + --semantic-warning: #cd8c37; /* Warning-400 */ + --semantic-error: #ec7069; /* Error-400 */ + --semantic-info: #53a0ec; /* Indigo-400 */ + --semantic-neutral: #9b9b9b; /* Neutral-400 */ + + /* === GRADIENT ENDPOINTS (used by ContextWindowMeter) === */ + --gradient-warning-end: #e6aa63; /* Warning-300 */ + --gradient-critical-end: #ff958d; /* Error-300 */ + + /* === MAP TO INFIMA — Neutral primary === */ + --ifm-color-primary: var(--neutral-100); + --ifm-color-primary-dark: var(--neutral-200); + --ifm-color-primary-darker: var(--neutral-300); + --ifm-color-primary-darkest: var(--neutral-400); + --ifm-color-primary-light: var(--neutral-50); + --ifm-color-primary-lighter: #ffffff; + --ifm-color-primary-lightest: #ffffff; + + /* === VISUAL ELEMENT COLORS — shade-400 for dark mode === */ + --visual-indigo: #53a0ec; /* Indigo-400 — override :root hardcode */ + --visual-cyan: var(--cyan-400); /* Cyan-400 */ + --visual-violet: #938eeb; /* Violet-400 */ + --visual-magenta: #c07ecf; /* Magenta-400 */ + /* --visual-lime and --visual-rose removed; they alias success and neutral */ + + /* === VISUAL ELEMENT BACKGROUNDS — 15% tint in dark mode === */ + --visual-bg-error: color-mix(in srgb, var(--visual-error) 15%, transparent); + --visual-bg-warning: color-mix(in srgb, var(--visual-warning) 15%, transparent); + --visual-bg-success: color-mix(in srgb, var(--visual-success) 15%, transparent); + --visual-bg-cyan: color-mix(in srgb, var(--visual-cyan) 15%, transparent); + --visual-bg-indigo: color-mix(in srgb, var(--visual-indigo) 15%, transparent); + --visual-bg-violet: color-mix(in srgb, var(--visual-violet) 15%, transparent); + --visual-bg-magenta: color-mix(in srgb, var(--visual-magenta) 15%, transparent); + --visual-bg-neutral: color-mix(in srgb, var(--visual-neutral) 15%, transparent); + /* --visual-bg-lime and --visual-bg-rose follow their aliases (success and neutral) */ + + /* === SURFACE / TEXT / BORDER — Dark mode === */ + --surface-page: #0d1117; + --surface-raised: #161b22; + --surface-muted: #3d3d3d; /* neutral-800 */ + --text-heading: #e8e8e8; /* neutral-100 */ + --text-body: #d4d4d4; /* neutral-200 */ + --text-muted: #9b9b9b; /* neutral-400 */ + --border-default: #505050; /* neutral-700 */ + --border-subtle: #3d3d3d; /* neutral-800 — hairline dividers */ + + /* === DARK MODE BACKGROUNDS === */ + --ifm-background-color: var(--surface-page); + --ifm-background-surface-color: var(--surface-raised); + --code-bg-dark: #222222; /* neutral-950 */ + --ifm-font-color-base: #d4d4d4; /* neutral-200 */ + --ifm-heading-color: #e8e8e8; /* neutral-100 */ + --code-bg: var(--neutral-950); + --text-subtle: var(--neutral-400); + --icon-star: #fcca91; /* warning-200 */ /* === TYPOGRAPHY & CODE === */ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); - /* === DARK MODE BACKGROUNDS === */ - --ifm-background-color: #0d1117; /* GitHub-style dark background */ - --ifm-background-surface-color: #161b22; /* Elevated surfaces (cards, etc.) */ - --code-bg-dark: #1e1e1e; /* VS Code dark background */ - --ifm-font-color-base: #c9d1d9; /* Soft white (less eye strain) */ - --ifm-heading-color: #e6edf3; /* Brighter for headings */ + /* === DOC SIDEBAR — Dark mode === */ + --ifm-menu-link-sublist-icon-filter: none; + --docusaurus-collapse-button-bg: var(--surface-raised); + --docusaurus-collapse-button-bg-hover: var(--surface-muted); + --ifm-scrollbar-track-background-color: var(--surface-raised); + --ifm-scrollbar-thumb-background-color: var(--neutral-700); + --ifm-scrollbar-thumb-hover-background-color: var(--neutral-600); + + /* === NAVBAR — Dark mode === */ + --ifm-navbar-background-color: var(--surface-page); + --ifm-navbar-link-color: var(--text-body); + --ifm-navbar-link-hover-color: var(--text-heading); + + /* === SEARCH PLUGIN — Dark mode overrides === + Active suggestion: light bg (#e8e8e8) with dark text (#2b2b2b), 11.56:1 contrast. */ + --search-local-highlight-color: var(--neutral-100); + --search-local-hit-active-color: var(--neutral-900); + + /* Search icon — stroke-based magnifying glass (dark: #9b9b9b, 6.81:1 on #0d1117) */ + --ifm-navbar-search-input-icon: url('data:image/svg+xml;utf8,'); + + /* === ANNOUNCEMENT BAR === */ + --announcement-bg: #8e5900; /* warning-600 */ + --announcement-text: #fff3e6; /* warning-50 */ +} - /* === DARK MODE TEXT COLOR ACCESSIBILITY === */ - /* Light text on dark backgrounds maintains 7:1+ contrast (AAA level) */ - /* #c9d1d9 on #0d1117 = ~12:1 contrast ratio */ - /* All emphasis colors desaturated for optimal dark mode readability */ +/* ======================================================================== + BREADCRUMBS — Inactive link color override + Infima uses `color: inherit` on breadcrumb links, bypassing the + --ifm-breadcrumb-color variable. Direct selector required. + ======================================================================== */ +.breadcrumbs__item:not(.breadcrumbs__item--active) .breadcrumbs__link { + color: var(--text-muted); /* #808080 light / #9b9b9b dark — metadata level */ +} + +/* ======================================================================== + PAGE TITLE — h1 size on design system scale + --ifm-h1-font-size is not applied to article h1 in Docusaurus's rendered + output; direct selector required. + ======================================================================== */ +article h1 { + font-size: var(--text-4xl); /* 40px */ + line-height: var(--lh-3xl); /* 48px */ } -/* Mermaid diagram styling - Centered horizontally - Docusaurus theme-mermaid uses CSS modules with hashed class names (e.g., container_lyt7) - Target using attribute selector that matches the Mermaid SVG ID pattern */ +/* ======================================================================== + TABLE OF CONTENTS — Right sidebar, design system alignment + ======================================================================== */ + +.table-of-contents { + font-size: var(--text-sm); /* 13px — secondary/metadata navigation */ + font-family: var(--font-body); +} + +.table-of-contents__link { + line-height: var(--lh-sm); /* 24px — 8px-snapped */ +} + +/* Active + hover: contrast shift within neutral palette, not a hue shift */ +.table-of-contents__link:hover, +.table-of-contents__link:hover code, +.table-of-contents__link--active, +.table-of-contents__link--active code { + color: var(--text-body); /* One step up from --text-muted in the hierarchy */ +} + +/* ======================================================================== + CHANNEL SWITCH — Main content entrance animation + ======================================================================== + Cross-channel navigation (/docs → /prompts) unmounts DocRoot entirely, + so a CSS animation on the container fires only on channel switches. + 50ms delay staggers after the sidebar entrance (150ms in styles.module.css). + ======================================================================== */ + +@keyframes channelEnter { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +[class*='docMainContainer'] > .container { + animation: channelEnter 200ms cubic-bezier(0.4, 0, 0.2, 1) 60ms both; +} + +@media (prefers-reduced-motion: reduce) { + [class*='docMainContainer'] > .container { + animation: none !important; + } +} + +/* ======================================================================== + MERMAID DIAGRAM STYLING + ======================================================================== */ + [class*="container_"]:has(> svg[id^="mermaid"]) { display: flex; justify-content: center; @@ -190,7 +447,6 @@ text-align: center; } -/* Mermaid SVG responsive behavior */ [class*="container_"]:has(> svg[id^="mermaid"]) > svg { max-width: 100%; height: auto; @@ -198,7 +454,10 @@ margin: 0 auto; } -/* Container for visual elements */ +/* ======================================================================== + VISUAL CONTAINER CLASSES — Hue-based naming per design system + ======================================================================== */ + .visual-container { margin: 2rem 0; padding: 1.5rem; @@ -206,27 +465,60 @@ border: 1px solid var(--ifm-color-emphasis-300); } -.visual-container--workflow { - background: var(--visual-bg-workflow); - border-color: var(--visual-workflow); +.visual-container--cyan { + background: var(--visual-bg-cyan); + border-color: var(--visual-cyan); +} + +.visual-container--success { + background: var(--visual-bg-success); + border-color: var(--visual-success); +} + +.visual-container--warning { + background: var(--visual-bg-warning); + border-color: var(--visual-warning); +} + +.visual-container--indigo { + background: var(--visual-bg-indigo); + border-color: var(--visual-indigo); +} + +.visual-container--error { + background: var(--visual-bg-error); + border-color: var(--visual-error); +} + +.visual-container--lime { + background: var(--visual-bg-lime); + border-color: var(--visual-lime); +} + +.visual-container--violet { + background: var(--visual-bg-violet); + border-color: var(--visual-violet); } -.visual-container--capability { - background: var(--visual-bg-capability); - border-color: var(--visual-capability); +.visual-container--magenta { + background: var(--visual-bg-magenta); + border-color: var(--visual-magenta); } -.visual-container--limitation { - background: var(--visual-bg-limitation); - border-color: var(--visual-limitation); +.visual-container--rose { + background: var(--visual-bg-rose); + border-color: var(--visual-rose); } -.visual-container--decision { - background: var(--visual-bg-decision); - border-color: var(--visual-decision); +.visual-container--neutral { + background: var(--visual-bg-neutral); + border-color: var(--visual-neutral); } -/* Visual checklist styling */ +/* ======================================================================== + VISUAL CHECKLIST STYLING + ======================================================================== */ + .visual-checklist { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -242,13 +534,11 @@ background: var(--ifm-background-surface-color); border-radius: 6px; border: 1px solid var(--ifm-color-emphasis-200); - transition: all 0.2s ease; + transition: border-color 0.2s ease; } .visual-checklist-item:hover { - border-color: var(--ifm-color-primary); - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-color: var(--neutral-400); } .visual-checklist-icon { @@ -262,95 +552,423 @@ } /* ======================================================================== - MINIMAL GRADIENT ACCENTS - Professional AI Aesthetic - ======================================================================== - Subtle gradient effects for key interactive elements. - Use sparingly - gradients only where they add meaning or visual hierarchy. + ACCENT BORDERS — Semantic callout + live code blocks ======================================================================== */ -/* Gradient border for AI-specific callouts and live code blocks */ .ai-gradient-border { - border-left: 4px solid transparent; - border-image: linear-gradient( - 180deg, - var(--brand-primary) 0%, - var(--gradient-warm-end) 100% - ) - 1; + border-left: 4px solid var(--neutral-400); } -/* Apply gradient border to live code blocks */ +/* Apply accent border to live code blocks */ .theme-live-codeblock { - border-left: 4px solid transparent; - border-image: linear-gradient( - 180deg, - var(--brand-primary) 0%, - var(--gradient-warm-end) 100% - ) - 1; + border-left: 4px solid var(--neutral-400); margin: 2rem 0; } -/* Subtle gradient divider for major sections */ +/* Subtle divider for major sections */ .gradient-divider { height: 2px; - background: linear-gradient( - 90deg, - transparent 0%, - var(--brand-primary) 50%, - transparent 100% - ); + background: var(--border-default); margin: 3rem 0; border: none; } -/* Navbar with subtle bottom border */ +/* ======================================================================== + NAVBAR — Hidden on desktop, minimal on mobile + ======================================================================== + All navigation is unified into the left sidebar on desktop (≥997px). + Mobile retains the navbar shell (brand + hamburger only). + ======================================================================== */ + +/* Hide navbar on desktop — sidebar-first layout */ +@media (min-width: 997px) { + .navbar { + display: none; + } + + :root { + --ifm-navbar-height: 0px; + } + + [data-theme='dark'] { + --ifm-navbar-height: 0px; + } +} + +/* Sidebar container — no structural borders */ +@media (min-width: 997px) { + .theme-doc-sidebar-container { + border-right: none; + border-bottom: none; + } +} + .navbar { - border-bottom: 1px solid var(--brand-primary); + border-bottom: 1px solid var(--neutral-300); +} + +[data-theme='dark'] .navbar { + border-bottom-color: var(--neutral-800); +} + +/* Vertical alignment: center all elements, nudge brand to match link baselines */ +.navbar__inner { + align-items: center; +} + +/* Brand: nudge up 1.5px to split the baseline gap between + 19px title (baseline at +3px) and 13px links (baseline at 0) */ +.navbar__brand { + transform: translateY(-1.5px); +} + +.navbar__title { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-lg); + line-height: var(--lh-lg); + letter-spacing: -0.01em; +} + +.navbar__logo { + height: var(--space-3); + margin-right: var(--space-1); +} + +/* Nav links: Inter, small caps feel via uppercase + tracking */ +.navbar__link { + font-family: var(--font-body); + font-weight: 400; + font-size: var(--text-sm); + line-height: var(--lh-sm); + text-transform: uppercase; + letter-spacing: 0.06em; + text-decoration: none; + padding-bottom: 2px; + border-bottom: 1px solid transparent; + transition: border-color 0.15s ease, font-weight 0.15s ease; +} + +/* Hover: underline appears */ +.navbar__link:hover { + text-decoration: none; + border-bottom: 1px solid var(--neutral-400); +} + +[data-theme='dark'] .navbar__link:hover { + border-bottom-color: var(--neutral-500); +} + +/* Active: persistent underline, slightly heavier */ +.navbar__link--active { + font-weight: 500; + text-decoration: none; + border-bottom: 2px solid var(--text-heading); +} + +/* Search input — flat, bordered */ +.navbar__search-input { + background: var(--surface-page); + border: 1px solid var(--neutral-300); + border-radius: var(--radius-sm); + font-family: var(--font-body); + font-size: var(--text-sm); + height: var(--target-sm); + color: var(--text-body); +} + +.navbar__search-input:focus { + border-color: var(--neutral-600); + outline: none; +} + +[data-theme='dark'] .navbar__search-input { + background: var(--surface-page); + border-color: var(--neutral-700); + color: var(--text-body); +} + +[data-theme='dark'] .navbar__search-input:focus { + border-color: var(--neutral-400); +} + +/* Color mode toggle — stable selector via swizzled Navbar/ColorModeToggle wrapper */ +.color-mode-toggle button { + border: 1px solid transparent; + border-radius: var(--radius-sm); /* 4px — input radius */ + width: var(--target-sm); /* 32px — tertiary action */ + height: var(--target-sm); + color: var(--text-muted); /* match footer link weight */ + transition: background 0.15s ease, color 0.15s ease; } -/* Enhanced code block styling with subtle shadow */ +.color-mode-toggle svg { + width: var(--icon-sm); /* 16px — match search icon */ + height: var(--icon-sm); +} + +.color-mode-toggle button:hover { + background: var(--surface-muted); + color: var(--text-heading); /* match footer link hover */ +} + +.color-mode-toggle button:focus-visible { + border-color: var(--neutral-600); + outline: none; +} + +[data-theme='dark'] .color-mode-toggle button:focus-visible { + border-color: var(--neutral-400); +} + +/* Search dropdown — flat border replaces shadow (design system: flat construction) */ +[class*='dropdownMenu'] { + border: 1px solid var(--border-default); + border-radius: var(--radius-md); /* 8px — container radius */ +} + +/* Search dropdown — sidebar context: align left, cap width to sidebar */ +.theme-doc-sidebar-container [class*='dropdownMenu'] { + left: 0; + right: auto; + max-width: calc(var(--doc-sidebar-width) - var(--space-4)); +} + + +/* Mobile hamburger toggle */ +@media (max-width: 996px) { + .navbar__toggle { + border: none; + color: var(--text-body); + } +} + +/* Mobile sidebar — flat, no shadow */ +.navbar-sidebar { + background-color: var(--surface-page); + box-shadow: none; + border-right: 1px solid var(--neutral-300); +} + +[data-theme='dark'] .navbar-sidebar { + border-right-color: var(--neutral-800); +} + +.navbar-sidebar__brand { + box-shadow: none; + border-bottom: 1px solid var(--neutral-300); +} + +[data-theme='dark'] .navbar-sidebar__brand { + border-bottom-color: var(--neutral-800); +} + +/* Figure — reset UA margins, apply design-system spacing consistently */ +figure { + margin-inline: 0; + margin-block: var(--space-4); /* 32px — matches ScrollDrivenFigure */ +} + +figure figcaption { + text-align: center; + font-size: var(--text-sm); /* 13px — caption scale per DESIGN_SYSTEM */ + line-height: var(--lh-sm); /* 24px — 8px-grid snap */ + color: var(--text-muted); + margin-top: var(--space-1); /* 8px */ +} + +/* Content area links — typographic interaction (Design System rule #4) */ +.markdown a, +article a { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +/* Code block styling */ .theme-code-block { - box-shadow: 0 2px 4px rgba(124, 58, 237, 0.1); margin: 2rem 0; } -[data-theme='dark'] .theme-code-block { - box-shadow: 0 2px 8px rgba(167, 139, 250, 0.2); +/* Font features for all monospace containers */ +code, pre, kbd, [class*="codeBlock"] { + font-feature-settings: var(--font-mono-features); } +/* Voice utility classes (per DESIGN_SYSTEM.md) */ +.mono-ai { font-family: var(--font-mono-ai); } +.mono-spec { font-family: var(--font-mono-spec); } +.mono-human { font-family: var(--font-mono-human); } +.mono-keyword { font-family: var(--font-mono-keyword); } + /* ======================================================================== - ANNOUNCEMENT BANNER - Under Construction Warning + DOC SIDEBAR — Design system conformance ======================================================================== - High-visibility warning banner for course development status. - Uses warm orange/amber colors for maximum attention. - Maintains WCAG AA compliance for accessibility. + Typography: Inter for items, Space Grotesk for category labels. + Spacing: 8px grid, 40px touch targets (--target-md). + Colors: achromatic — typographic interaction (weight/contrast, not hue). + Icons: mask-image replaces Infima's filter hack for dark mode. ======================================================================== */ -:root { - /* Light mode: Vibrant amber/orange background with dark text */ - /* WCAG: #78350f (dark brown) on #fed7aa (light peach) = 7.1:1 (AAA Pass) */ - --announcement-bg: #fed7aa; /* Light peach - softer than pure orange */ - --announcement-text: #78350f; /* Dark brown - high contrast, readable */ +/* Typography ------------------------------------------------------------ */ + +.menu { + font-weight: 400; /* was 500; active items get 600 below */ } -[data-theme='dark'] { - /* Dark mode: Deep orange background with bright text */ - /* WCAG: #fef3c7 (cream) on #ea580c (orange) = 7.8:1 (AAA Pass) */ - --announcement-bg: #ea580c; /* Vivid orange - warm and attention-grabbing */ - --announcement-text: #fef3c7; /* Cream - bright and readable */ +.menu__link { + font-size: var(--text-sm); /* 13px, was ~14px inherited */ + line-height: var(--lh-sm); /* 24px, was 1.25 (~17.5px) */ +} + +.menu__link--sublist { + font-family: var(--font-display); /* Space Grotesk for section headings */ + font-weight: 600; + font-size: var(--text-sm); + letter-spacing: -0.01em; +} + +.menu__link--active:not(.menu__link--sublist) { + font-weight: 600; /* active leaf item: weight bump */ +} + +/* Interactive states ---------------------------------------------------- */ + +.menu__link:hover { + color: var(--text-heading); /* contrast shift on hover */ } -/* Announcement bar custom styling */ +/* Spacing --------------------------------------------------------------- */ + +.menu > .menu__list > .menu__list-item:not(:first-child) { + margin-top: var(--space-2); /* 16px between category groups */ +} + +/* Icon column alignment: override Content module's 0px right padding + (scrollbar-gutter on .menu is inert — .sidebarScrollable is the scroll container) */ +.theme-doc-sidebar-container .menu { + padding-right: var(--space-2) !important; +} + +/* Caret icons — mask-image approach (no filter hack) -------------------- */ + +.menu__link--sublist-caret::after, +.menu__caret::before { + background: var(--text-muted); + filter: none; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z'/%3E%3C/svg%3E"); + mask-size: var(--icon-sm) var(--icon-sm); + -webkit-mask-size: var(--icon-sm) var(--icon-sm); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-position: center; + height: var(--icon-sm); /* 16px, was 20px */ + width: var(--icon-sm); +} + +.menu__link--sublist-caret::after { + min-width: var(--icon-sm); /* 16px, was 20px */ +} + +/* §N section marker — typographically secondary reference tag ----------- */ + +.sidebar-section-number { + display: inline-block; + font-family: var(--font-mono-keyword); /* Monaspace Krypton — technical/mechanical voice */ + font-size: var(--text-xs); /* 11px — visually subordinate */ + line-height: var(--lh-sm); /* 24px — stays on baseline */ + color: var(--text-muted); /* #808080 light / #9b9b9b dark */ + font-weight: 400; + letter-spacing: 0.02em; + margin-right: var(--space-0h); /* 4px gap before label text */ + min-width: 1.8em; /* prevents label shift as numbers grow: §1 vs §14 */ + font-feature-settings: var(--font-mono-features); +} + +.menu__link--active .sidebar-section-number { + color: var(--text-body); /* lift to body color when active — subtle emphasis */ +} + +/* ======================================================================== + ANNOUNCEMENT BANNER + ======================================================================== */ + div[class*='announcementBar'] { font-weight: 600; font-size: 0.95rem; text-align: center; border-bottom: 2px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } [data-theme='dark'] div[class*='announcementBar'] { border-bottom: 2px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 2px 12px rgba(234, 88, 12, 0.3); +} + +/* ======================================================================== + STAT CALLOUTS (intro.mdx) + ======================================================================== */ + +.stat-row { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); + margin: var(--space-4) 0; + align-items: flex-start; +} + +.stat-callout { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-0h); + min-width: 80px; +} + +.stat-callout__number { + font-family: var(--font-display); + font-size: var(--text-3xl); + font-weight: 700; + line-height: var(--lh-2xl); + color: var(--text-heading); +} + +.stat-callout__label { + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + color: var(--text-muted); + text-align: center; +} + +/* ── Idle micro-animations (DESIGN_SYSTEM.md §Motion) ─────────────────────── */ +@keyframes idleCursorBlink { + 0%, 100% { opacity: 1; } 50% { opacity: 0; } +} +@keyframes idleStatusPulse { + 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } +} +@keyframes idleFlowDrift { + 0%, 100% { transform: translateX(0); } 50% { transform: translateX(2px); } +} +@keyframes idleReadyBreathe { + 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } +} +@keyframes pingOnce { + 0% { transform: scale(1); opacity: 1; } + 100% { transform: scale(1.4); opacity: 0; } +} + +.idle-cursor-blink { animation: idleCursorBlink 1000ms step-end infinite; } +.idle-status-pulse { animation: idleStatusPulse 2000ms ease-in-out infinite; } +.idle-flow-drift { animation: idleFlowDrift 3000ms ease-in-out infinite; } +.idle-ready-breathe { animation: idleReadyBreathe 4000ms ease-in-out infinite; } +.ping-once { animation: pingOnce 400ms var(--ease-exit) 1 both; } + +@media (prefers-reduced-motion: reduce) { + .idle-cursor-blink, .idle-status-pulse, + .idle-flow-drift, .idle-ready-breathe, .ping-once { + animation: none !important; + } } diff --git a/website/src/css/fonts.css b/website/src/css/fonts.css new file mode 100644 index 0000000..abaa38e --- /dev/null +++ b/website/src/css/fonts.css @@ -0,0 +1,166 @@ +/* ======================================================================== + SELF-HOSTED FONT FACES + ======================================================================== + Space Grotesk — Display/Headings (600, 700) + Inter — Body text (400–800) + Monaspace Neon — Code/labels (400–700) + + All files served from /fonts/ as woff2. + font-display: swap ensures text is visible during load. + ======================================================================== */ + +/* --- Space Grotesk (variable: 600–700) --- */ + +@font-face { + font-family: 'Space Grotesk'; + src: url('/fonts/space-grotesk-latin-ext.woff2') format('woff2'); + font-weight: 600 700; + font-style: normal; + font-display: swap; + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, + U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, + U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} + +@font-face { + font-family: 'Space Grotesk'; + src: url('/fonts/space-grotesk-latin.woff2') format('woff2'); + font-weight: 600 700; + font-style: normal; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* --- Inter (variable: 400–800) --- */ + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin-ext.woff2') format('woff2'); + font-weight: 400 800; + font-style: normal; + font-display: swap; + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, + U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, + U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin.woff2') format('woff2'); + font-weight: 400 800; + font-style: normal; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* --- Monaspace Neon (static weights) --- */ + +@font-face { + font-family: 'Monaspace Neon'; + src: url('/fonts/monaspace-neon-400.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Neon'; + src: url('/fonts/monaspace-neon-500.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Neon'; + src: url('/fonts/monaspace-neon-600.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Neon'; + src: url('/fonts/monaspace-neon-700.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +/* --- Monaspace Argon (AI voice) --- */ + +@font-face { + font-family: 'Monaspace Argon'; + src: url('/fonts/monaspace-argon-400.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Argon'; + src: url('/fonts/monaspace-argon-500.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +/* --- Monaspace Xenon (spec/schema voice) --- */ + +@font-face { + font-family: 'Monaspace Xenon'; + src: url('/fonts/monaspace-xenon-400.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Xenon'; + src: url('/fonts/monaspace-xenon-500.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Xenon'; + src: url('/fonts/monaspace-xenon-600.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +/* --- Monaspace Radon (human/informal voice) --- */ + +@font-face { + font-family: 'Monaspace Radon'; + src: url('/fonts/monaspace-radon-400.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +/* --- Monaspace Krypton (keyword/mechanical voice) --- */ + +@font-face { + font-family: 'Monaspace Krypton'; + src: url('/fonts/monaspace-krypton-400.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Monaspace Krypton'; + src: url('/fonts/monaspace-krypton-500.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} diff --git a/website/src/css/motion-tokens.css b/website/src/css/motion-tokens.css new file mode 100644 index 0000000..dc5e803 --- /dev/null +++ b/website/src/css/motion-tokens.css @@ -0,0 +1,189 @@ +/* ========================================================================== + Motion Tokens + Productive style · Medium arousal · Medium pleasure + All durations and easings derived from ANIMATION_GUIDE.md formulas. + Only opacity and transform are animated — never layout properties. + ========================================================================== */ + +:root { + /* --- Duration tokens -------------------------------------------------- */ + --duration-instant: 70ms; + --duration-fast: 110ms; + --duration-subtle: 150ms; + --duration-moderate: 240ms; + --duration-deliberate: 400ms; + --duration-ambient: 700ms; + + /* Exit durations (shorter than enter — perception asymmetry) */ + --duration-fast-exit: 80ms; + --duration-subtle-exit: 110ms; + --duration-moderate-exit: 180ms; + --duration-deliberate-exit: 300ms; + + /* --- Easing tokens ----------------------------------------------------- */ + /* Enter: starts fast, decelerates into resting position */ + --ease-enter: cubic-bezier(0.00, 0.00, 0.38, 0.9); + /* Exit: starts slow, accelerates out of frame */ + --ease-exit: cubic-bezier(0.20, 0.00, 1.00, 0.9); + /* Standard: enters fast, eases to rest (for in-place state changes) */ + --ease-standard: cubic-bezier(0.20, 0.00, 0.38, 0.9); + /* Linear: constant velocity — only for continuous ambient motion */ + --ease-linear: linear; + + /* --- Stagger tokens ---------------------------------------------------- */ + --motion-stagger-sm: 60ms; /* 5–8 items */ + --motion-stagger-md: 80ms; /* 3–4 items */ + --motion-stagger-lg: 100ms; /* 2 items */ + + /* --- Reveal offset tokens ---------------------------------------------- */ + --motion-reveal-y-load: -8px; /* Page load: elements settle downward */ + --motion-reveal-y-scroll: 12px; /* Scroll reveal: elements rise upward */ + --motion-reveal-scale: 0.96; /* Modal only: slight scale expand */ + + /* --- Stroke tokens (SVG diagram animations) ---------------------------- */ + --stroke-fine: 1px; + --stroke-light: 1.5px; + --stroke-default: 2px; + --stroke-medium: 2.5px; + --stroke-heavy: 3px; + --stroke-accent: 4px; +} + +/* ========================================================================== + Keyframes + ========================================================================== */ + +/* Scroll reveal: element rises up into view from below */ +@keyframes reveal-from-bottom { + from { + opacity: 0; + transform: translateY(var(--motion-reveal-y-scroll)); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Page load: element settles downward into position */ +@keyframes reveal-from-top { + from { + opacity: 0; + transform: translateY(var(--motion-reveal-y-load)); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Modal reveal: element expands slightly as it fades in */ +@keyframes reveal-scale { + from { + opacity: 0; + transform: scale(var(--motion-reveal-scale)); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* SVG connector draw: path traces from start to end */ +@keyframes draw-path { + from { + stroke-dashoffset: var(--path-length); + } + to { + stroke-dashoffset: 0; + } +} + +/* SVG node entrance: fade + rise */ +@keyframes diagram-node-in { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ========================================================================== + Utility Classes + ========================================================================== */ + +/* Base reveal container — hidden until .reveal-is-visible is applied */ +.reveal { + opacity: 0; +} + +/* Scroll-triggered reveal (IntersectionObserver adds .reveal-is-visible) */ +.reveal-scroll { + opacity: 0; +} + +.reveal-scroll.reveal-is-visible { + animation: reveal-from-bottom var(--duration-deliberate) var(--ease-enter) both; +} + +/* Page-load reveal (applied immediately on mount, no observer needed) */ +.reveal-page-load { + opacity: 0; + animation: reveal-from-top var(--duration-moderate) var(--ease-enter) both; +} + +/* Triggered state — added by useScrollReveal when element enters viewport */ +.reveal-is-visible { + opacity: 1; +} + +/* Stagger: children receive --stagger-index via JS; delay compounds per item */ +.stagger-item { + animation-delay: calc(var(--stagger-index, 0) * var(--motion-stagger-md)); +} + +/* SVG diagram node */ +.diagram-node { + opacity: 0; + animation: diagram-node-in var(--duration-moderate) var(--ease-enter) both; + animation-delay: calc(var(--stagger-index, 0) * var(--motion-stagger-sm)); +} + +/* SVG diagram connector — requires --path-length set inline on the element */ +.diagram-connector { + stroke-dasharray: var(--path-length); + stroke-dashoffset: var(--path-length); + animation: draw-path var(--duration-deliberate) var(--ease-enter) both; +} + +/* ========================================================================== + Reduced Motion + Respects the OS-level "Reduce Motion" accessibility preference. + All durations collapse to near-zero; opacity/transform are reset. + ========================================================================== */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0.01ms !important; + transition-duration: 0.01ms !important; + transition-delay: 0.01ms !important; + } + + .reveal, + .reveal-scroll, + .reveal-page-load, + .diagram-node { + opacity: 1; + transform: none; + } + + .diagram-connector { + stroke-dashoffset: 0; + } +} diff --git a/website/src/hooks/useActs.ts b/website/src/hooks/useActs.ts new file mode 100644 index 0000000..e5bcf98 --- /dev/null +++ b/website/src/hooks/useActs.ts @@ -0,0 +1,32 @@ +import { useMemo, useCallback } from 'react'; + +export type ActDef = { id: string; threshold: number }; + +/** + * Phase-gated act hook (DESIGN_SYSTEM.md §Motion — Act System). + * phase: 0–1, sourced from useAnimationPhase() (ScrollDrivenFigure context). + * Acts are purely reactive to the current phase value — they re-evaluate on every + * render and can reverse when phase decreases. Enforce monotonicity at the call site + * with a useRef guard when one-way semantics are required. + */ +export function useActs(actDefs: ActDef[], phase: number) { + const sorted = useMemo( + () => [...actDefs].sort((a, b) => a.threshold - b.threshold), + [actDefs], + ); + + const reachedIds = useMemo( + () => new Set(sorted.filter((a) => phase >= a.threshold).map((a) => a.id)), + [sorted, phase], + ); + + const currentActId = useMemo(() => { + const r = sorted.filter((a) => phase >= a.threshold); + return r.length ? r[r.length - 1].id : null; + }, [sorted, phase]); + + const wasReached = useCallback((id: string) => reachedIds.has(id), [reachedIds]); + const isCurrentAct = useCallback((id: string) => currentActId === id, [currentActId]); + + return { wasReached, isCurrentAct }; +} diff --git a/website/src/hooks/useScrollNarrative.ts b/website/src/hooks/useScrollNarrative.ts new file mode 100644 index 0000000..770f2e1 --- /dev/null +++ b/website/src/hooks/useScrollNarrative.ts @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState, type RefObject } from 'react'; + +export interface NarrativeChapter { + id: string; + label?: string; +} + +export interface UseScrollNarrativeOptions { + chapters: NarrativeChapter[]; + threshold?: number; +} + +export interface UseScrollNarrativeResult { + containerRef: RefObject; + figureRef: RefObject; + chapterRefs: RefObject[]; + activeChapter: string; + progress: number; + isActive: boolean; +} + +export function useScrollNarrative({ + chapters, + threshold = 0.4, +}: UseScrollNarrativeOptions): UseScrollNarrativeResult { + const containerRef = useRef(null); + const figureRef = useRef(null); + // Stable array of refs — length must not change between renders + const chapterRefs = useRef[]>( + chapters.map(() => ({ current: null })), + ).current; + + const [activeChapter, setActiveChapter] = useState(chapters[0]?.id ?? ''); + const [progress, setProgress] = useState(0); + const [isActive, setIsActive] = useState(false); + + // Track which chapter index is active so scroll handler can reference it + const activeIndexRef = useRef(0); + + useEffect(() => { + if (typeof window === 'undefined') return; + + // --- IntersectionObserver: chapter entry detection --- + const computeProgress = () => { + const ref = chapterRefs[activeIndexRef.current]; + const el = ref?.current; + if (!el) return; + + const rect = el.getBoundingClientRect(); + const vh = window.innerHeight; + // progress goes 0→1 as the chapter top moves from vh*threshold to 0 + const raw = 1 - rect.top / (vh * threshold); + setProgress(Math.min(1, Math.max(0, raw))); + }; + + const onScroll = () => computeProgress(); + + const containerIO = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsActive(true); + window.addEventListener('scroll', onScroll, { passive: true }); + computeProgress(); + } else { + setIsActive(false); + window.removeEventListener('scroll', onScroll); + } + }, + { threshold: 0 }, + ); + + if (containerRef.current) containerIO.observe(containerRef.current); + + const chapterIO = new IntersectionObserver( + (entries) => { + // Pick the entry with the highest intersectionRatio + let best: IntersectionObserverEntry | null = null; + for (const entry of entries) { + if (!best || entry.intersectionRatio > best.intersectionRatio) { + best = entry; + } + } + if (!best || !best.isIntersecting) return; + + const idx = chapterRefs.findIndex((r) => r.current === best!.target); + if (idx === -1) return; + activeIndexRef.current = idx; + setActiveChapter(chapters[idx].id); + computeProgress(); + }, + { threshold: [0, 0.25, 0.5, 0.75, 1], rootMargin: '0px 0px -20% 0px' }, + ); + + chapterRefs.forEach((r) => { + if (r.current) chapterIO.observe(r.current); + }); + + return () => { + containerIO.disconnect(); + chapterIO.disconnect(); + window.removeEventListener('scroll', onScroll); + }; + }, [chapters, chapterRefs, threshold]); + + return { + containerRef, + figureRef, + chapterRefs, + activeChapter, + progress, + isActive, + }; +} diff --git a/website/src/hooks/useScrollReveal.ts b/website/src/hooks/useScrollReveal.ts new file mode 100644 index 0000000..4fe5590 --- /dev/null +++ b/website/src/hooks/useScrollReveal.ts @@ -0,0 +1,54 @@ +import { useEffect, useRef, useState } from 'react'; + +interface ScrollRevealOptions { + threshold?: number; + rootMargin?: string; + once?: boolean; + staggerChildren?: boolean; +} + +export function useScrollReveal(options?: ScrollRevealOptions): { + ref: React.RefObject; + isVisible: boolean; +} { + const { + threshold = 0.1, + rootMargin = '0px 0px -80px 0px', + once = true, + staggerChildren = false, + } = options ?? {}; + + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (!entry.isIntersecting) return; + + el.classList.add('reveal-is-visible'); + setIsVisible(true); + + if (staggerChildren) { + Array.from(el.children).forEach((child, index) => { + (child as HTMLElement).style.setProperty('--stagger-index', String(index)); + }); + } + + if (once) observer.disconnect(); + }, + { threshold, rootMargin }, + ); + + observer.observe(el); + + return () => observer.disconnect(); + }, [threshold, rootMargin, once, staggerChildren]); + + return { ref, isVisible }; +} diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css deleted file mode 100644 index 266b726..0000000 --- a/website/src/pages/index.module.css +++ /dev/null @@ -1,999 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -/* ======================================================================== - HERO SECTION - ======================================================================== */ - -.heroBanner { - padding: 4rem 0 3rem; - position: relative; - overflow: hidden; - text-align: center; -} - -.heroContent { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 3rem; - align-items: center; - max-width: 1200px; - margin: 0 auto; -} - -.heroText { - display: flex; - flex-direction: column; - align-items: flex-start; - text-align: left; -} - -.heroStats { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.5rem; - margin-top: 1.5rem; - font-size: 0.95rem; - color: rgba(255, 255, 255, 0.85); -} - -.statsSeparator { - color: rgba(255, 255, 255, 0.5); - margin: 0 0.25rem; -} - -.heroStatLink { - color: inherit; - text-decoration: none; - transition: opacity 0.15s ease; -} - -.heroStatLink:hover { - opacity: 0.8; - text-decoration: underline; -} - -.heroBadge { - display: inline-flex; - align-items: center; - gap: 0.35rem; - background: rgba(255, 255, 255, 0.12); - color: rgba(255, 255, 255, 0.95); - padding: 0.4rem 0.9rem; - border-radius: 6px; - font-size: 0.8rem; - font-weight: 500; - font-family: - ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; - margin-bottom: 1rem; - letter-spacing: 0; - text-decoration: none; - transition: - background 0.2s, - transform 0.2s; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.heroBadge:hover { - background: rgba(255, 255, 255, 0.2); - transform: translateY(-1px); - color: white; - text-decoration: none; - border-color: rgba(255, 255, 255, 0.3); -} - -.badgeSeparator { - color: rgba(255, 255, 255, 0.4); -} - -.heroBadge .starIcon { - width: 12px; - height: 12px; - color: #fbbf24; -} - -.githubIcon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.buttonOutline { - background: transparent; - border: 2px solid rgba(255, 255, 255, 0.8); - color: white; -} - -.buttonOutline:hover { - background: rgba(255, 255, 255, 0.1); - border-color: white; - color: white; -} - -.heroAudience { - font-size: 0.95rem; - margin: 1.5rem 0 0; - /* Accessibility: White text on purple background for sufficient contrast */ - /* Hero section uses purple primary background, requires light text */ - color: rgba(255, 255, 255, 0.75); - font-weight: 400; -} - -.heroMeta { - margin-top: 2rem; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.5rem 1rem; - font-size: 0.95rem; - /* Accessibility: Light text on purple hero background */ - color: rgba(255, 255, 255, 0.85); -} - -.metaItem { - display: inline-flex; - align-items: center; - gap: 0.25rem; -} - -.metaSeparator { - /* Accessibility: Light separator on purple hero background */ - color: rgba(255, 255, 255, 0.6); -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-top: 1.5rem; -} - -/* ======================================================================== - TERMINAL WINDOW - ======================================================================== */ - -.terminal { - background: #1e1e1e; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - text-align: left; -} - -.terminalHeader { - background: #323232; - padding: 0.6rem 1rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.terminalDot { - width: 12px; - height: 12px; - border-radius: 50%; -} - -.terminalDot[data-color='red'] { - background: #ff5f56; -} - -.terminalDot[data-color='yellow'] { - background: #ffbd2e; -} - -.terminalDot[data-color='green'] { - background: #27c93f; -} - -.terminalTitle { - margin-left: auto; - font-size: 0.75rem; - color: #888; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; -} - -.terminalBody { - padding: 1.25rem 1.5rem; - background: #1e1e1e; -} - -.terminalCode { - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; - font-size: 0.9rem; - color: #d4d4d4; - line-height: 1.7; - margin: 0; - white-space: pre-wrap; - background: transparent; - text-align: left; -} - -.terminalKeyword { - color: #c586c0; - font-weight: 600; -} - -.terminalLink { - display: inline-block; - margin-top: 1rem; - font-size: 0.85rem; - color: #569cd6; - text-decoration: none; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; -} - -.terminalLink:hover { - color: #9cdcfe; - text-decoration: underline; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem 1rem; - } - - .heroContent { - grid-template-columns: 1fr; - gap: 2rem; - } - - .heroText { - align-items: center; - text-align: center; - } - - .heroStats { - font-size: 0.85rem; - justify-content: center; - } - - .buttons { - flex-direction: column; - gap: 0.75rem; - } - - .terminal { - max-width: 400px; - margin: 0 auto; - } -} - -/* ======================================================================== - SECTION LAYOUT - ======================================================================== */ - -.modulesSection, -.prerequisitesSection, -.ctaSection { - padding: 3rem 0; -} - -.sectionTitle { - text-align: center; - font-size: 2rem; - margin-bottom: 2.5rem; - font-weight: 700; -} - -/* ======================================================================== - MODULE CARDS - ======================================================================== */ - -.modulesGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 2rem; - margin: 0 auto; - max-width: 1200px; -} - -.moduleCard { - display: block; - padding: 2rem; - background: var(--ifm-background-surface-color); - border: 2px solid var(--ifm-color-emphasis-200); - border-radius: 12px; - text-decoration: none; - color: inherit; - transition: all 0.3s ease; - position: relative; - /* Accessibility: Subtle shadow provides depth cue for colorblind users */ - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.moduleCard:hover { - border-color: var(--ifm-color-primary); - /* Accessibility: Larger transform (6px) provides motion-based feedback */ - /* This helps colorblind users who may not perceive border color change */ - transform: translateY(-6px); - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); - text-decoration: none; -} - -/* Accessibility: High-contrast focus indicator for keyboard navigation */ -.moduleCard:focus { - outline: 3px solid var(--ifm-color-primary); - outline-offset: 2px; -} - -.moduleNumber { - display: inline-block; - font-size: 0.85rem; - font-weight: 700; - color: var(--ifm-color-primary); - background: var(--visual-bg-workflow); - padding: 0.25rem 0.75rem; - border-radius: 6px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.moduleTitle { - font-size: 1.5rem; - margin-bottom: 0.5rem; - font-weight: 700; -} - -.moduleDuration { - font-size: 0.9rem; - color: var(--ifm-color-emphasis-600); - margin-bottom: 1rem; - font-weight: 500; -} - -.moduleTopics { - list-style: none; - padding: 0; - margin: 0; -} - -.moduleTopics li { - padding: 0.5rem 0; - font-size: 0.95rem; - color: var(--ifm-color-emphasis-700); - position: relative; - padding-left: 1.25rem; -} - -.moduleTopics li::before { - content: '→'; - position: absolute; - left: 0; - color: var(--ifm-color-primary); - font-weight: 700; -} - -/* ======================================================================== - PREREQUISITES SECTION - ======================================================================== */ - -.prerequisitesSection { - background: var(--ifm-background-surface-color); -} - -.prerequisitesContent { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 3rem; - max-width: 1000px; - margin: 0 auto; -} - -.prerequisiteColumn { - padding: 0 1rem; -} - -.prerequisiteSubtitle { - font-size: 1.25rem; - margin-bottom: 1.25rem; - font-weight: 700; -} - -.prerequisiteList { - list-style: none; - padding: 0; - margin: 0; -} - -.prerequisiteList li { - padding: 0.75rem 0; - font-size: 0.95rem; - line-height: 1.6; - color: var(--ifm-color-emphasis-700); - position: relative; - padding-left: 1.75rem; -} - -.prerequisiteList li::before { - content: '•'; - position: absolute; - left: 0.5rem; - color: var(--ifm-color-primary); - font-weight: 700; - font-size: 1.2rem; -} - -/* ======================================================================== - FINAL CTA SECTION - ======================================================================== */ - -.ctaSection { - text-align: center; - padding: 4rem 0; -} - -.ctaTitle { - font-size: 2rem; - margin-bottom: 1rem; - font-weight: 700; -} - -.ctaDescription { - font-size: 1.1rem; - color: var(--ifm-color-emphasis-700); - margin-bottom: 2rem; - max-width: 600px; - margin-left: auto; - margin-right: auto; -} - -.ctaLinks { - margin-top: 2rem; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.5rem 1rem; - font-size: 0.95rem; -} - -.ctaLinks a { - color: var(--ifm-color-primary); - text-decoration: none; - font-weight: 500; -} - -.ctaLinks a:hover { - text-decoration: underline; -} - -/* ======================================================================== - ACCESSIBILITY ENHANCEMENTS - ======================================================================== */ - -/* High-contrast focus indicators for all interactive elements */ -/* Ensures keyboard navigation is visible for all users */ - -.buttons a:focus, -.ctaLinks a:focus { - outline: 3px solid var(--ifm-color-primary); - outline-offset: 2px; -} - -/* Enhanced link underlines for colorblind users */ -/* Color + underline ensures links are identifiable without color perception */ -.ctaLinks a { - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 2px; -} - -.ctaLinks a:hover { - text-decoration-thickness: 2px; -} - -/* ======================================================================== - RESPONSIVE DESIGN - ======================================================================== */ - -@media screen and (max-width: 768px) { - .modulesGrid { - grid-template-columns: 1fr; - } - - .prerequisitesContent { - grid-template-columns: 1fr; - gap: 2rem; - } - - .sectionTitle { - font-size: 1.75rem; - } - - .moduleCard { - padding: 1.5rem; - } -} - -/* ======================================================================== - WHAT'S INCLUDED SECTION (Pillars) - ======================================================================== */ - -.whatsIncludedSection { - padding: 4rem 0; - background: var(--ifm-background-surface-color); -} - -.pillarsGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1.5rem; - max-width: 1000px; - margin: 0 auto; -} - -.pillarCard { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 1.75rem; - background: var(--ifm-background-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 12px; - text-decoration: none; - color: inherit; - transition: - transform 0.2s, - box-shadow 0.2s, - border-color 0.2s; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); -} - -.pillarCard:hover { - transform: translateY(-4px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); - border-color: var(--ifm-color-primary-light); - text-decoration: none; -} - -.pillarCard:focus { - outline: 3px solid var(--ifm-color-primary); - outline-offset: 2px; -} - -.pillarBlue .pillarIcon { - color: #3b82f6; -} - -.pillarIcon { - width: 32px; - height: 32px; -} - -.pillarTitle { - font-size: 1.15rem; - margin-bottom: 0.5rem; - font-weight: 600; -} - -.pillarDescription { - font-size: 0.9rem; - color: var(--ifm-color-emphasis-700); - line-height: 1.5; - margin: 0; -} - -/* ======================================================================== - COMPANION PROJECTS SECTION - ======================================================================== */ - -.companionSection { - padding: 4rem 0; -} - -.sectionSubtitle { - text-align: center; - font-size: 1.1rem; - color: var(--ifm-color-emphasis-600); - margin-top: -1.5rem; - margin-bottom: 2.5rem; -} - -.projectsGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.5rem; - max-width: 800px; - margin: 0 auto; -} - -.projectCard { - display: flex; - flex-direction: column; - padding: 1.75rem; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 12px; - text-decoration: none; - color: inherit; - transition: - transform 0.2s, - box-shadow 0.2s, - border-color 0.2s; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); -} - -.projectCard:hover { - transform: translateY(-4px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); - border-color: var(--ifm-color-primary); - text-decoration: none; -} - -.projectHeader { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.5rem; -} - -.projectTitle { - font-size: 1.25rem; - font-weight: 700; - margin: 0; -} - -.projectWordmark { - height: 24px; - width: auto; -} - -/* ChunkHound wordmark theme colors */ -.chunkHoundText { - fill: #1f2937; -} - -.chunkHoundAccent { - fill: #5b6fed; -} - -[data-theme='dark'] .chunkHoundText { - fill: #fafafa; -} - -[data-theme='dark'] .chunkHoundAccent { - fill: #7c8fff; -} - -.externalIcon { - width: 18px; - height: 18px; - color: var(--ifm-color-emphasis-500); -} - -.projectTagline { - font-size: 0.95rem; - color: var(--ifm-color-emphasis-700); - margin: 0 0 1rem 0; - line-height: 1.5; -} - -.projectScale { - display: inline-block; - align-self: flex-start; - background: var(--visual-bg-workflow); - color: var(--brand-primary); - padding: 0.35rem 0.75rem; - border-radius: 6px; - font-size: 0.8rem; - font-weight: 600; - margin-top: auto; -} - -.integrationNote { - text-align: center; - font-size: 0.9rem; - color: var(--ifm-color-emphasis-600); - margin-top: 2rem; - margin-bottom: 0; -} - -.ecosystemGrid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1.5rem; - max-width: 1000px; - margin: 0 auto; -} - -@media screen and (max-width: 996px) { - .ecosystemGrid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media screen and (max-width: 576px) { - .ecosystemGrid { - grid-template-columns: 1fr; - } -} - -.toolboxTitle { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1rem; - font-weight: 700; -} - -.toolboxTitle svg { - width: 24px; - height: 24px; - color: var(--ifm-color-primary); -} - -.disclosureNote { - text-align: center; - font-size: 0.85rem; - color: var(--ifm-color-emphasis-500); - margin-top: 0.75rem; - margin-bottom: 0; -} - -/* ======================================================================== - LEARNING FORMATS SECTION - ======================================================================== */ - -.formatsSection { - padding: 4rem 0; - background: var(--ifm-background-surface-color); -} - -.formatsGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 1.5rem; - max-width: 900px; - margin: 0 auto; -} - -.formatCard { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: 2rem 1.5rem; - background: var(--ifm-background-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); -} - -.formatInfo .formatIcon { - color: var(--semantic-info); -} - -.formatIcon { - width: 40px; - height: 40px; -} - -.formatTitle { - font-size: 1.1rem; - margin-bottom: 0.5rem; - font-weight: 600; -} - -.formatDescription { - font-size: 0.9rem; - color: var(--ifm-color-emphasis-700); - line-height: 1.5; - margin: 0; -} - -/* ======================================================================== - SOCIAL PROOF BAR - ======================================================================== */ - -.socialProofBar { - padding: 1.25rem 0; - background: var(--ifm-background-surface-color); - border-bottom: 1px solid var(--ifm-color-emphasis-200); -} - -.socialProofContent { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.75rem; - text-align: center; -} - -.starsRow { - display: flex; - align-items: center; - flex-wrap: wrap; - justify-content: center; -} - -.starIcon { - width: 14px; - height: 14px; - color: #f59e0b; -} - -.starsTotal { - font-weight: 600; - color: var(--ifm-color-emphasis-800); -} - -.starsSeparator { - color: var(--ifm-color-emphasis-400); - padding: 0 12px; -} - -.repoLink { - display: inline-flex; - align-items: center; - gap: 4px; - color: var(--ifm-color-emphasis-600); - text-decoration: none; - font-size: 0.9rem; -} - -.repoLink:hover { - color: var(--ifm-color-primary); - text-decoration: underline; -} - -.repoLink .githubIcon { - width: 14px; - height: 14px; -} - -.repoLink .starIcon { - width: 12px; - height: 12px; - color: #f59e0b; - margin-left: 2px; -} - -.testimonial { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - justify-content: center; -} - -.testimonialText { - font-style: italic; - color: var(--ifm-color-emphasis-600); -} - -.testimonialSource { - color: var(--ifm-color-emphasis-500); - font-size: 0.85rem; -} - -/* ======================================================================== - METHODOLOGY PREVIEW - ======================================================================== */ - -.methodologySection { - padding: 4rem 0; -} - -.phraseGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 1rem; - max-width: 900px; - margin: 0 auto; -} - -.phraseCard { - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 8px; - padding: 1rem 1.25rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.phraseText { - font-size: 0.95rem; - color: var(--ifm-color-emphasis-900); - background: none; - padding: 0; -} - -.phraseSource { - font-size: 0.8rem; - color: var(--ifm-color-emphasis-500); -} - -.promptCta { - text-align: center; - margin-top: 2rem; -} - -.promptCta a { - color: var(--ifm-color-primary); - text-decoration: none; - font-weight: 500; -} - -.promptCta a:hover { - text-decoration: underline; -} - -@media screen and (max-width: 576px) { - .phraseGrid { - grid-template-columns: 1fr; - } -} - -/* ======================================================================== - MODULE CARD ENHANCEMENTS - ======================================================================== */ - -.moduleHeader { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; -} - -.formatBadges { - display: flex; - gap: 0.5rem; -} - -.formatBadge { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: var(--visual-bg-workflow); - border-radius: 6px; -} - -.formatBadge svg { - width: 16px; - height: 16px; - color: var(--ifm-color-primary); -} - -.modulesCta { - text-align: center; - margin-top: 2.5rem; -} - -/* ======================================================================== - RESPONSIVE ADDITIONS - ======================================================================== */ - -@media screen and (max-width: 768px) { - .projectsGrid, - .ecosystemGrid { - grid-template-columns: 1fr; - } - - .heroBadge { - font-size: 0.75rem; - padding: 0.3rem 0.75rem; - } - - .sectionSubtitle { - font-size: 1rem; - } - - .starsRow { - gap: 0.35rem; - } - - .testimonial { - flex-direction: column; - gap: 0.25rem; - } -} diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx deleted file mode 100644 index 5aa168a..0000000 --- a/website/src/pages/index.tsx +++ /dev/null @@ -1,707 +0,0 @@ -import { type ReactNode, useState, useEffect } from 'react'; -import clsx from 'clsx'; -import Link from '@docusaurus/Link'; -import Layout from '@theme/Layout'; -import Heading from '@theme/Heading'; -import sidebars from '../../sidebars'; -import promptsSidebars from '../../sidebarsPrompts'; - -import styles from './index.module.css'; - -// Count lessons from sidebar config (filter for lesson-* items) -function countLessons(): number { - let count = 0; - const traverse = (items: unknown[]) => { - for (const item of items) { - if (typeof item === 'string' && item.includes('lesson-')) { - count++; - } else if (item && typeof item === 'object' && 'items' in item) { - traverse((item as { items: unknown[] }).items); - } - } - }; - traverse(sidebars.tutorialSidebar as unknown[]); - return count; -} - -// Count prompts from sidebar config (exclude index) -function countPrompts(): number { - let count = 0; - const traverse = (items: unknown[]) => { - for (const item of items) { - if (typeof item === 'string') { - count++; - } else if (item && typeof item === 'object' && 'type' in item) { - const obj = item as { type: string; items?: unknown[] }; - if (obj.type === 'category' && obj.items) { - count += obj.items.length; - } - } - } - }; - const sidebar = promptsSidebars.promptsSidebar as unknown[]; - // Skip first item (index/overview) - traverse(sidebar.slice(1)); - return count; -} - -const LESSON_COUNT = countLessons(); -const PROMPT_COUNT = countPrompts(); - -// Terminal window for developer aesthetic -function TerminalWindow() { - return ( -
-
- - - - methodology.md -
-
-
-          INVESTIGATE
-          {': Trace the code path\n'}
-          ANALYZE
-          {': Compare expected vs actual\n'}
-          EXPLAIN
-          {': File paths, line numbers, root cause'}
-        
- - From the Prompt Library → - -
-
- ); -} - -function HomepageHeader() { - const stars = useGitHubStars('agenticoding/agenticoding.github.io'); - - return ( -
-
-
-
- - - Open Source - · - MIT - {stars !== null && ( - <> - · - - {stars} - - )} - - - Master Agentic Coding - -

- Structured methodology proven on enterprise mono-repos with - millions of lines of code -

-
- - Start Learning - - - Browse Prompts - -
-
- - FAQ - - | - - {LESSON_COUNT} Lessons - - | - - {PROMPT_COUNT} Production Prompts - -
-
- -
-
-
- ); -} - -// GitHub icon (official Invertocat mark) -function GitHubIcon() { - return ( - - ); -} - -function HeadphonesIcon() { - return ( - - - - - ); -} - -function PresentationIcon() { - return ( - - - - - - ); -} - -function BookmarkIcon() { - return ( - - - - ); -} - -function ToolboxIcon() { - return ( - - - - - - ); -} - -function ExternalLinkIcon() { - return ( - - - - - - ); -} - -// Star icon for GitHub stars -function StarIcon() { - return ( - - - - ); -} - -function formatStarCount(count: number): string { - if (count < 1000) return count.toString(); - const formatted = (count / 1000).toFixed(1); - return formatted.replace(/\.0$/, '') + 'k'; -} - -// Hook to fetch GitHub stars with caching -function useGitHubStars(repo: string): number | null { - const [stars, setStars] = useState(() => { - if (typeof window === 'undefined') return null; - const cached = localStorage.getItem(`gh-stars-${repo}`); - if (cached) { - const { stars, timestamp } = JSON.parse(cached); - // Cache valid for 1 hour - if (Date.now() - timestamp < 3600000) return stars; - } - return null; - }); - - useEffect(() => { - if (stars !== null) return; - - fetch(`https://api.github.com/repos/${repo}`) - .then((res) => res.json()) - .then((data) => { - if (data.stargazers_count !== undefined) { - setStars(data.stargazers_count); - localStorage.setItem( - `gh-stars-${repo}`, - JSON.stringify({ - stars: data.stargazers_count, - timestamp: Date.now(), - }) - ); - } - }) - .catch(() => { - // Silently fail, stars will show as null - }); - }, [repo, stars]); - - return stars; -} - -// Social Proof Bar with GitHub stars and testimonial -function SocialProofBar() { - const courseStars = useGitHubStars('agenticoding/agenticoding.github.io'); - const chunkHoundStars = useGitHubStars('chunkhound/chunkhound'); - const arguSeekStars = useGitHubStars('ArguSeek/arguseek'); - - const hasStars = - courseStars !== null || chunkHoundStars !== null || arguSeekStars !== null; - - const testimonials = [ - '"I just finished studying this. Very useful and well organized"', - '"No CTO/startup-dev/tech-advisor chat has taken place in the past month without mentioning it"', - '"Thank you for not making another video course series"', - '"I was looking for something like a course or some sort of guidelines"', - '"Great work, this is amazing"', - ]; - const [testimonialIndex] = useState(() => - Math.floor(Math.random() * testimonials.length) - ); - - return ( -
- -
- ); -} - -// ChunkHound wordmark with theme support -function ChunkHoundWordmark({ className }: { className?: string }) { - return ( - - {/* Hound logo */} - - - - - {/* Text */} - - - Chunk - - - Hound - - - - ); -} - -// ArguSeek wordmark with magnifying glass icon -function ArguSeekWordmark({ className }: { className?: string }) { - return ( - - - - - - - - {/* Magnifying glass icon scaled to match text */} - - - - - - - - - {/* Wordmark text */} - - Argu - Seek - - - ); -} - -// Ecosystem Tools Section -function EcosystemTools() { - return ( -
-
- - Open Source Ecosystem - -

- Production-ready tools that apply course methodology -

-
- -
- - -
-

- Don't search your code. Research it. -

- 10K–1M+ LOC -
- - -
- - -
-

- Wide research, not deep reports -

- - 12–100+ sources per query - -
- - -
-
- - Curated Toolbox -
-
-

- Modern CLI tools for AI-first development -

- - ripgrep, fzf, lazygit... - - -
-

- The research layer that anchors your agents in reality -

-

- ChunkHound and ArguSeek are created by the course author. -

-
-
- ); -} - -interface ModuleCardProps { - number: number; - title: string; - topics: string[]; - link: string; -} - -function ModuleCard({ number, title, topics, link }: ModuleCardProps) { - return ( - -
-
Module {number}
-
- - - - - - -
-
- - {title} - -
    - {topics.map((topic, index) => ( -
  • {topic}
  • - ))} -
- - ); -} - -function CourseModules() { - const modules = [ - { - number: 1, - title: 'Fundamentals', - topics: [ - 'LLM internals: context, attention, token limits', - 'What breaks: hallucinations, code drift, refactoring', - 'Context management and RAG integration', - ], - link: '/docs/fundamentals/lesson-1-how-llms-work', - }, - { - number: 2, - title: 'Methodology', - topics: [ - 'Prompt structure: constraints, examples, chain-of-thought', - 'Grounding: embedding context that persists', - 'Iteration patterns: plan, execute, verify', - ], - link: '/docs/methodology/lesson-3-high-level-methodology', - }, - { - number: 3, - title: 'Practical Techniques', - topics: [ - 'CI integration and automated review patterns', - 'Test generation and coverage strategies', - 'Debugging sessions: when AI makes it worse', - ], - link: '/docs/practical-techniques/lesson-6-project-onboarding', - }, - ]; - - return ( -
-
- - What You'll Learn - -

- {LESSON_COUNT} lessons covering research, planning, execution, and - validation patterns -

-
- {modules.map((module) => ( - - ))} -
-
-
- ); -} - -// Learning Formats - Three ways to consume each lesson -function LearningFormats() { - return ( -
-
- - Learn Your Way - -

Every lesson, three formats

-
-
- - - Reference Docs - -

- Bookmark it. Jump back in when you need it. -

-
-
- - - Podcasts - -

- Commute, gym, walking the dog. -

-
-
- - - Presentations - -

Share with your team.

-
-
-
-
- ); -} - -export default function Home(): ReactNode { - return ( - - -
- - - - -
-
- ); -} diff --git a/website/src/styles/presentation-system.css b/website/src/styles/presentation-system.css index a21b5c7..b23b306 100644 --- a/website/src/styles/presentation-system.css +++ b/website/src/styles/presentation-system.css @@ -61,45 +61,52 @@ color: #fff; line-height: 1.3; - /* Typography colors - high contrast for readability */ - --ifm-font-color-base: #c9d1d9; - --ifm-heading-color: #e6edf3; - --ifm-color-emphasis-700: #8b949e; - --ifm-color-emphasis-600: #6e7681; - --ifm-color-emphasis-500: #545d68; - --ifm-color-emphasis-300: #3d444d; - --ifm-color-emphasis-200: #30363d; + /* Typography colors - achromatic, high contrast for readability */ + --ifm-font-color-base: #d4d4d4; /* neutral-200 */ + --ifm-heading-color: #e8e8e8; /* neutral-100 */ + --ifm-color-emphasis-700: #9b9b9b; /* neutral-400 */ + --ifm-color-emphasis-600: #808080; /* neutral-500 */ + --ifm-color-emphasis-500: #666666; /* neutral-600 */ + --ifm-color-emphasis-300: #505050; /* neutral-700 */ + --ifm-color-emphasis-200: #3d3d3d; /* neutral-800 */ /* Backgrounds - GitHub-style dark theme */ --ifm-background-color: #0d1117; --ifm-background-surface-color: #161b22; - /* Brand colors - brighter for dark backgrounds */ - --brand-primary: #a78bfa; - --brand-primary-light: #c4b5fd; - --brand-primary-dark: #8b5cf6; - - /* Semantic colors - ensures dark mode throughout presentation */ - --semantic-info: #60a5fa; - --semantic-success: #22d3ee; - --semantic-warning: #fb923c; - --semantic-error: #fb7185; - --semantic-neutral: #94a3b8; - - /* Visual element colors - adjusted for dark mode contrast */ - --visual-workflow: #a78bfa; /* Purple - AI workflows */ - --visual-capability: #22d3ee; /* Cyan - capabilities */ - --visual-limitation: #fb923c; /* Orange - limitations */ - --visual-decision: #c4b5fd; /* Light purple - decisions */ - --visual-error: #fb7185; /* Rose - errors */ - --visual-neutral: #94a3b8; /* Slate - neutral */ - - /* Visual element backgrounds - transparent overlays */ - --visual-bg-workflow: rgba(167, 139, 250, 0.15); - --visual-bg-capability: rgba(34, 211, 238, 0.15); - --visual-bg-limitation: rgba(251, 146, 60, 0.15); - --visual-bg-decision: rgba(196, 181, 253, 0.15); - --visual-bg-error: rgba(251, 113, 133, 0.15); + /* Brand colors - Cyan-400 for dark backgrounds */ + --brand-primary: #00b2b2; + --brand-primary-light: #2ad0d0; + --brand-primary-dark: #009393; + + /* Semantic colors - shade-400 for dark mode */ + --semantic-info: #53a0ec; /* Indigo-400 */ + --semantic-success: #48b475; /* Success-400 */ + --semantic-warning: #cd8c37; /* Warning-400 */ + --semantic-error: #ec7069; /* Error-400 */ + --semantic-neutral: #9b9b9b; /* Neutral-400 */ + + /* Visual element colors - hue-based naming */ + --visual-cyan: var(--brand-primary); + --visual-success: var(--semantic-success); + --visual-warning: var(--semantic-warning); + --visual-indigo: var(--semantic-info); + --visual-error: var(--semantic-error); + --visual-neutral: var(--semantic-neutral); + --visual-violet: #938eeb; /* Violet-400 */ + --visual-magenta: #c07ecf; /* Magenta-400 */ + /* --visual-lime and --visual-rose removed; they alias success and neutral */ + + /* Visual element backgrounds - 15% tint (dark mode) */ + --visual-bg-error: color-mix(in srgb, var(--visual-error) 15%, transparent); + --visual-bg-warning: color-mix(in srgb, var(--visual-warning) 15%, transparent); + --visual-bg-success: color-mix(in srgb, var(--visual-success) 15%, transparent); + --visual-bg-cyan: color-mix(in srgb, var(--visual-cyan) 15%, transparent); + --visual-bg-indigo: color-mix(in srgb, var(--visual-indigo) 15%, transparent); + --visual-bg-violet: color-mix(in srgb, var(--visual-violet) 15%, transparent); + --visual-bg-magenta: color-mix(in srgb, var(--visual-magenta) 15%, transparent); + --visual-bg-neutral: color-mix(in srgb, var(--visual-neutral) 15%, transparent); + /* --visual-bg-lime and --visual-bg-rose follow their aliases (success and neutral) */ } /* ======================================================================== @@ -160,7 +167,7 @@ /* Subtitle - below title, before content */ .reveal .subtitle { font-size: clamp(1rem, 2vw, 1.2em); - color: #999; + color: var(--text-subtle); margin-top: 0.5em; margin-bottom: 1em; max-width: var(--pres-component-max-width); @@ -259,13 +266,13 @@ /* Selection highlighting */ .reveal ::selection { color: #fff; - background: rgba(167, 139, 250, 0.4); + background: color-mix(in srgb, var(--brand-primary) 40%, transparent); text-shadow: none; } .reveal ::-moz-selection { color: #fff; - background: rgba(167, 139, 250, 0.4); + background: color-mix(in srgb, var(--brand-primary) 40%, transparent); text-shadow: none; } @@ -279,7 +286,6 @@ font-style: italic; background: rgba(255, 255, 255, 0.05); border-left: 4px solid var(--brand-primary); - border-radius: 4px; } /* ======================================================================== @@ -383,7 +389,7 @@ line-height: 1.4; margin: 0; border-radius: 8px; - background: #1e1e1e; + background: var(--code-bg); padding: 1.2em; text-align: left; overflow-y: auto; @@ -393,7 +399,7 @@ } .code-container code { - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-family: var(--ifm-font-family-monospace); } /* ======================================================================== diff --git a/website/src/theme/DocItem/Content/index.tsx b/website/src/theme/DocItem/Content/index.tsx index 3a92376..3ff4d98 100644 --- a/website/src/theme/DocItem/Content/index.tsx +++ b/website/src/theme/DocItem/Content/index.tsx @@ -6,6 +6,7 @@ import Heading from '@theme/Heading'; import MDXContent from '@theme/MDXContent'; import type { Props } from '@theme/DocItem/Content'; import LessonAudioPlayer from '@site/src/components/LessonAudioPlayer'; +import SiteHero from '@site/src/components/SiteHero'; /** Title can be declared inside md content or declared through @@ -29,14 +30,19 @@ function useSyntheticTitle(): string | null { export default function DocItemContent({ children }: Props): ReactNode { const syntheticTitle = useSyntheticTitle(); + const { metadata } = useDoc(); + const isIntroPage = metadata.id === 'intro'; + + const hasHeader = isIntroPage || !!syntheticTitle; + return (
- {syntheticTitle && ( + {hasHeader && (
- {syntheticTitle} + {isIntroPage ? : {syntheticTitle}} + {!isIntroPage && }
)} - {children}
); diff --git a/website/src/theme/DocItem/Layout/index.tsx b/website/src/theme/DocItem/Layout/index.tsx index 575df5c..b5d935c 100644 --- a/website/src/theme/DocItem/Layout/index.tsx +++ b/website/src/theme/DocItem/Layout/index.tsx @@ -1,13 +1,12 @@ import React, { type ReactNode } from 'react'; +import { publishTOC } from '../../tocStore'; import clsx from 'clsx'; -import { useWindowSize } from '@docusaurus/theme-common'; import { useDoc } from '@docusaurus/plugin-content-docs/client'; import DocItemPaginator from '@theme/DocItem/Paginator'; import DocVersionBanner from '@theme/DocVersionBanner'; import DocVersionBadge from '@theme/DocVersionBadge'; import DocItemFooter from '@theme/DocItem/Footer'; import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile'; -import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop'; import DocItemContent from '@theme/DocItem/Content'; import DocBreadcrumbs from '@theme/DocBreadcrumbs'; import ContentVisibility from '@theme/ContentVisibility'; @@ -22,26 +21,21 @@ interface CustomFrontMatter { } /** - * Decide if the toc should be rendered, on mobile or desktop viewports + * Decide if the toc should be rendered, and publish it to the sidebar TOC store. */ function useDocTOC() { const { frontMatter, toc } = useDoc(); - const windowSize = useWindowSize(); const hidden = frontMatter.hide_table_of_contents; const canRender = !hidden && toc.length > 0; - const mobile = canRender ? : undefined; - - const desktop = - canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? ( - - ) : undefined; + React.useEffect(() => { + publishTOC(hidden ? [] : toc); + }, [toc, hidden]); return { hidden, - mobile, - desktop, + mobile: canRender ? : undefined, }; } @@ -58,7 +52,7 @@ export default function DocItemLayout({ children }: Props): ReactNode {
-
+
@@ -76,7 +70,6 @@ export default function DocItemLayout({ children }: Props): ReactNode {
- {docTOC.desktop &&
{docTOC.desktop}
}
); } diff --git a/website/src/theme/DocItem/Layout/styles.module.css b/website/src/theme/DocItem/Layout/styles.module.css index ba63a50..50204c7 100644 --- a/website/src/theme/DocItem/Layout/styles.module.css +++ b/website/src/theme/DocItem/Layout/styles.module.css @@ -1,3 +1,12 @@ +@keyframes pageEnter { + from { opacity: 0; } + to { opacity: 1; } +} + +.docItemContainer { + animation: pageEnter var(--duration-subtle) var(--ease-enter) both; +} + .docItemContainer header + *, .docItemContainer article > *:first-child { margin-top: 0; @@ -7,9 +16,9 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + margin-bottom: var(--space-2); flex-wrap: wrap; - gap: 1rem; + gap: var(--space-2); } .presentationToggleWrapper { @@ -17,8 +26,3 @@ align-items: center; } -@media (min-width: 997px) { - .docItemCol { - max-width: 75% !important; - } -} diff --git a/website/src/theme/DocSidebar/Desktop/SidebarFooter.tsx b/website/src/theme/DocSidebar/Desktop/SidebarFooter.tsx new file mode 100644 index 0000000..4e8ccdd --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/SidebarFooter.tsx @@ -0,0 +1,21 @@ +import {type ReactNode} from 'react'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import styles from './styles.module.css'; + +export default function SidebarFooter(): ReactNode { + return ( + + ); +} diff --git a/website/src/theme/DocSidebar/Desktop/SidebarHeader.tsx b/website/src/theme/DocSidebar/Desktop/SidebarHeader.tsx new file mode 100644 index 0000000..4809fa9 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/SidebarHeader.tsx @@ -0,0 +1,43 @@ +import {type ReactNode} from 'react'; +import Link from '@docusaurus/Link'; +import {useLocation} from '@docusaurus/router'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import styles from './styles.module.css'; +import clsx from 'clsx'; +import SidebarSearch from './SidebarSearch'; +import {channels, getActiveIndex} from './channels'; + +export default function SidebarHeader(): ReactNode { + const location = useLocation(); + const logoUrl = useBaseUrl('/img/logo.svg'); + const activeIndex = getActiveIndex(location.pathname); + + return ( +
+
+ + Agentic Coding Logo + Agentic Coding + + +
+
+ {channels.map(({label, path, match}) => ( + + {label} + + ))} +
+
+ ); +} diff --git a/website/src/theme/DocSidebar/Desktop/SidebarSearch.tsx b/website/src/theme/DocSidebar/Desktop/SidebarSearch.tsx new file mode 100644 index 0000000..bc5f981 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/SidebarSearch.tsx @@ -0,0 +1,184 @@ +import {type ReactNode, useState, useRef, useEffect, useCallback} from 'react'; +import useIsBrowser from '@docusaurus/useIsBrowser'; +import SearchBar from '@theme/SearchBar'; +import styles from './styles.module.css'; + +type SearchPhase = 'idle' | 'mounting' | 'active' | 'collapsing'; + +export default function SidebarSearch(): ReactNode { + const [phase, setPhase] = useState('idle'); + const slotRef = useRef(null); + const buttonRef = useRef(null); + const hasBeenExpanded = useRef(false); + const rAFHandle = useRef(null); + const isBrowser = useIsBrowser(); + + const expand = useCallback(() => { + setPhase(prev => { + if (prev === 'active' || prev === 'mounting') return prev; + if (prev === 'collapsing') return 'active'; + return 'mounting'; // from idle + }); + }, []); + + const collapse = useCallback(() => { + if (rAFHandle.current) { + cancelAnimationFrame(rAFHandle.current); + rAFHandle.current = null; + } + setPhase(prev => { + if (prev === 'idle' || prev === 'collapsing') return prev; + if (prev === 'mounting') return 'idle'; + return 'collapsing'; // from active + }); + }, []); + + const handleTransitionEnd = useCallback((e: React.TransitionEvent) => { + if (e.target !== e.currentTarget) return; + setPhase(prev => (prev === 'collapsing' ? 'idle' : prev)); + }, []); + + // Double rAF: first frame commits opacity:0 paint, second triggers transition + useEffect(() => { + if (phase !== 'mounting') return; + const raf1 = requestAnimationFrame(() => { + const raf2 = requestAnimationFrame(() => { + rAFHandle.current = null; + setPhase(prev => (prev === 'mounting' ? 'active' : prev)); + }); + rAFHandle.current = raf2; + }); + rAFHandle.current = raf1; + return () => { + if (rAFHandle.current) { + cancelAnimationFrame(rAFHandle.current); + rAFHandle.current = null; + } + }; + }, [phase]); + + // Fallback: resolve collapsing if transitionend never fires (reduced motion, interrupted) + useEffect(() => { + if (phase !== 'collapsing') return; + const timer = setTimeout(() => { + setPhase(prev => (prev === 'collapsing' ? 'idle' : prev)); + }, 250); + return () => clearTimeout(timer); + }, [phase]); + + // Auto-focus input when active + useEffect(() => { + if (phase !== 'active' || !slotRef.current) return; + hasBeenExpanded.current = true; + requestAnimationFrame(() => { + slotRef.current?.querySelector('input')?.focus(); + }); + }, [phase]); + + // Return focus to button on close + useEffect(() => { + if (phase !== 'idle' || !hasBeenExpanded.current) return; + buttonRef.current?.focus(); + }, [phase]); + + // Cmd+K / Ctrl+K when not expanded + useEffect(() => { + if (!isBrowser || phase === 'active' || phase === 'mounting') return; + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + expand(); + } + } + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [isBrowser, phase, expand]); + + // Escape when not idle + useEffect(() => { + if (!isBrowser || phase === 'idle') return; + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') collapse(); + } + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [isBrowser, phase, collapse]); + + // Collapse on blur when input is empty + useEffect(() => { + if (phase !== 'active' || !slotRef.current) return; + function onFocusOut(e: FocusEvent) { + // Focus moving to close button — let its click handler decide + if (buttonRef.current?.contains(e.relatedTarget as Node)) return; + const slot = slotRef.current; + if (!slot) return; + requestAnimationFrame(() => { + if (slot.contains(document.activeElement)) return; + const input = slot.querySelector('input'); + if (input && input.value.length > 0) return; + collapse(); + }); + } + const slot = slotRef.current; + slot.addEventListener('focusout', onFocusOut); + return () => slot.removeEventListener('focusout', onFocusOut); + }, [phase, collapse]); + + const showSearchBar = phase !== 'idle'; + const isExpanded = phase === 'active'; + const showClose = phase === 'mounting' || phase === 'active'; + + return ( + <> +
+ {showSearchBar && } +
+ + + ); +} diff --git a/website/src/theme/DocSidebar/Desktop/SidebarTOC.tsx b/website/src/theme/DocSidebar/Desktop/SidebarTOC.tsx new file mode 100644 index 0000000..87c09a2 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/SidebarTOC.tsx @@ -0,0 +1,139 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import { useTreeifiedTOC, type TOCTreeNode } from '@docusaurus/theme-common/internal'; +import { useSidebarTOC } from '../../tocStore'; +import styles from './styles.module.css'; + +const STORAGE_KEY = 'sidebar-toc-open'; + +function getInitialOpen(): boolean { + if (typeof window === 'undefined') return false; + const stored = sessionStorage.getItem(STORAGE_KEY); + return stored === null ? false : stored === 'true'; +} + +function useScrollspy(ids: string[]): string { + const [activeId, setActiveId] = React.useState(''); + + React.useEffect(() => { + if (ids.length === 0) { setActiveId(''); return; } + + const observer = new IntersectionObserver( + entries => { + for (const entry of entries) { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + } + }, + { rootMargin: '0px 0px -80% 0px' }, + ); + + const elements = ids.flatMap(id => { + const el = document.getElementById(id); + return el ? [el] : []; + }); + + elements.forEach(el => observer.observe(el)); + return () => observer.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ids.join(',')]); + + return activeId; +} + +function flatIds(nodes: readonly TOCTreeNode[]): string[] { + return nodes.flatMap(h2 => [h2.id, ...h2.children.map(h3 => h3.id)]); +} + +export default function SidebarTOC(): ReactNode { + const flatToc = useSidebarTOC(); + const toc = useTreeifiedTOC(flatToc as Parameters[0]); + const [open, setOpen] = React.useState(false); + + // Hydrate from sessionStorage after mount (avoids SSR mismatch) + React.useEffect(() => { + setOpen(getInitialOpen()); + }, []); + + const allIds = React.useMemo(() => flatIds(toc), [toc]); + const activeId = useScrollspy(allIds); + + if (toc.length === 0) return null; + + const activeH2 = toc.find( + h2 => h2.id === activeId || h2.children.some(h3 => h3.id === activeId), + ); + + function handleToggle() { + const next = !open; + setOpen(next); + sessionStorage.setItem(STORAGE_KEY, String(next)); + } + + return ( +
+ + +
+ ); +} diff --git a/website/src/theme/DocSidebar/Desktop/channels.ts b/website/src/theme/DocSidebar/Desktop/channels.ts new file mode 100644 index 0000000..158b626 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/channels.ts @@ -0,0 +1,18 @@ +export const channels = [ + {label: 'Reference', path: '/', match: '/'}, + {label: 'Prompts', path: '/prompts', match: '/prompts'}, + {label: 'Toolbox', path: '/developer-tools/cli-coding-agents', match: '/developer-tools'}, +] as const; + +export function getActiveIndex(pathname: string): number { + // Find the most specific (longest matching prefix) channel + let bestIdx = 0; + let bestLen = 0; + channels.forEach((c, idx) => { + if (pathname.startsWith(c.match) && c.match.length > bestLen) { + bestIdx = idx; + bestLen = c.match.length; + } + }); + return bestIdx; +} diff --git a/website/src/theme/DocSidebar/Desktop/index.tsx b/website/src/theme/DocSidebar/Desktop/index.tsx new file mode 100644 index 0000000..6d65702 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/index.tsx @@ -0,0 +1,27 @@ +import {type ReactNode} from 'react'; +import Desktop from '@theme-original/DocSidebar/Desktop'; +import type DesktopType from '@theme/DocSidebar/Desktop'; +import type {WrapperProps} from '@docusaurus/types'; +import {useLocation} from '@docusaurus/router'; +import SidebarHeader from './SidebarHeader'; +import SidebarFooter from './SidebarFooter'; +import SidebarTOC from './SidebarTOC'; +import {getActiveIndex} from './channels'; +import styles from './styles.module.css'; + +type Props = WrapperProps; + +export default function DesktopWrapper(props: Props): ReactNode { + const activeIndex = getActiveIndex(useLocation().pathname); + + return ( +
+ +
+ +
+ + +
+ ); +} diff --git a/website/src/theme/DocSidebar/Desktop/styles.module.css b/website/src/theme/DocSidebar/Desktop/styles.module.css new file mode 100644 index 0000000..dc81084 --- /dev/null +++ b/website/src/theme/DocSidebar/Desktop/styles.module.css @@ -0,0 +1,442 @@ +/* DocSidebar/Desktop — Sidebar-first navigation layout + Design system: flat, achromatic, --space-* tokens, --target-* sizes */ + +.sidebarContainer { + display: flex; + flex-direction: column; + height: 100%; +} + +/* Sticky header: brand + channel switcher */ +.sidebarHeader { + flex-shrink: 0; + padding: var(--space-2) var(--space-2) 0; +} + +.brandRow { + display: flex; + align-items: center; + gap: var(--space-1); + min-height: var(--target-sm); +} + +/* Brand title — collapses when search active */ +.brandRow:has([data-search-active]) .brandTitle { + opacity: 0; + max-width: 0; +} + +/* Search button stays visible when active (becomes close toggle) */ + +.brandLink { + display: flex; + align-items: center; + gap: var(--space-1); + text-decoration: none; + color: var(--text-heading); +} + +.brandLink:hover { + text-decoration: none; + color: var(--text-heading); +} + +.brandLogo { + height: var(--space-3); + width: auto; +} + +.brandTitle { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-lg); + line-height: var(--lh-lg); + letter-spacing: -0.01em; + overflow: hidden; + white-space: nowrap; + max-width: 160px; + transition: opacity 200ms ease-out, max-width 200ms ease-out; +} + +/* Channel switcher — underline tabs with sliding indicator */ +.channelSwitcher { + position: relative; + display: flex; + margin-top: var(--space-1); + border-bottom: 1px solid var(--border-subtle); +} + +.channelSwitcher::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + height: 2px; + width: calc(100% / var(--channel-count, 3)); + background: var(--text-heading); + transform: translateX(calc(var(--active-index, 0) * 100%)); + transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.channelTab { + flex: 1; + min-width: 0; + height: var(--target-sm); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + letter-spacing: 0.04em; + text-transform: uppercase; + text-decoration: none; + cursor: pointer; + background: transparent; + color: var(--text-muted); + font-weight: 400; + padding: 0 var(--space-0h); + transition: color 0.15s ease; +} + +.channelTab:hover { + color: var(--text-body); + text-decoration: none; +} + +.channelTabActive { + font-weight: 600; + color: var(--text-heading); +} + +.channelTabActive:hover { + color: var(--text-heading); +} + +/* Search slot — grows when active */ +.searchSlot { + flex-grow: 0; + opacity: 0; + min-width: 0; + overflow: hidden; + transition: flex-grow 200ms ease-out, opacity 200ms ease-out; +} + +.searchSlot[data-search-active] { + flex-grow: 1; + opacity: 1; + overflow: visible; /* allow dropdown to escape */ +} + +/* Search plugin overrides — sidebar context. + Plugin targets full-width navbar; these constrain it to the sidebar flex row. + Specificity 0-2-0 beats plugin's module classes (0-1-0). */ + +.searchSlot :global(.navbar__search) { + margin-left: 0; /* plugin default: 16px */ +} + +.searchSlot :global(.navbar__search-input) { + width: 100%; /* Infima default: 12.5rem (200px) */ + padding-left: var(--space-2); /* Infima default: 2.25rem (36px) — icon gone, reduce to component padding */ +} + +.searchSlot :global([class*='searchBar_']) { + display: block !important; /* autocomplete.js inline style: inline-block */ + width: 100% !important; +} + +.searchSlot :global([class*='searchHintContainer']) { + gap: 0; + right: var(--space-1); /* plugin default: 10px (not grid-snapped) */ +} + +.searchSlot :global([class*='searchHint']) { + box-shadow: none; + border: none; + background: none; + color: var(--text-muted); + border-radius: 0; + font-family: var(--font-mono-keyword); + font-size: var(--text-xs); + padding: 0; +} + +/* Search button — 32×32 icon trigger in brand row */ +.searchButton { + margin-left: auto; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: var(--target-sm); + height: var(--target-sm); + max-width: var(--target-sm); + padding: 0; + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-muted); + cursor: pointer; + overflow: hidden; + transition: opacity 200ms ease-out, max-width 200ms ease-out, + background 0.15s ease, color 0.15s ease; +} + +.searchButton:hover { + background: var(--surface-muted); + color: var(--text-heading); +} + +.searchButton:focus-visible { + border-color: var(--neutral-600); + outline: none; +} + +:global([data-theme='dark']) .searchButton:focus-visible { + border-color: var(--neutral-400); +} + +/* Reduced motion — instant swap */ +@media (prefers-reduced-motion: reduce) { + .brandTitle, + .searchSlot, + .searchButton { + transition-duration: 0s !important; + } + .channelSwitcher::after { + transition-duration: 0s !important; + } + .sidebarScrollable { + animation: none !important; + } +} + +/* Channel entrance animation */ +@keyframes channelEnter { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Scrollable middle area: sidebar items */ +.sidebarScrollable { + flex: 1; + overflow-y: auto; + min-height: 0; + animation: channelEnter 200ms cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Sticky footer: GitHub + theme toggle */ +.sidebarFooter { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-0h) var(--space-2) var(--space-2); +} + +.footerLinks { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.footerLink { + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: var(--lh-sm); + color: var(--text-muted); + text-decoration: none; + transition: color 0.15s ease; +} + +.footerLink:hover { + color: var(--text-heading); + text-decoration: none; +} + +/* ── On This Page — sidebar TOC zone ─────────────────── */ + +.tocZone { + flex-shrink: 0; + padding: var(--space-3) var(--space-2) var(--space-0h); +} + +.tocToggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 0 var(--space-1); + background: transparent; + border: none; + cursor: pointer; + color: var(--text-muted); + gap: var(--space-1); +} + +.tocToggle:hover { + color: var(--text-body); +} + +/* Left side of the toggle row: label + active-section hint */ +.tocToggleRow { + display: flex; + align-items: baseline; + gap: var(--space-1); + min-width: 0; + overflow: hidden; +} + +.tocLabel { + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 400; + flex-shrink: 0; +} + +/* Active-section hint shown in collapsed state */ +.tocActiveLabel { + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + font-weight: 400; + letter-spacing: 0; + text-transform: none; + color: var(--text-body); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.tocChevron { + flex-shrink: 0; + width: var(--target-sm); /* 32px — centers 12px drawing at same x as search/toggle */ + transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.tocChevronOpen { + transform: rotate(180deg); +} + +/* Grid-based collapse — cross-browser safe (Safari, Firefox, Chrome). + max-height + overflow:hidden on flex children breaks in Safari. + grid-template-rows: 0fr → 1fr animates to intrinsic height without + needing overflow:hidden on the outer container. */ +.tocContent { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 200ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.tocContentOpen { + grid-template-rows: 1fr; +} + +/* Inner wrapper: overflow:hidden clips content at 0fr. + When open, cap height and enable scroll for long TOCs. + Re-keyed on page change → remount fires tocEnter animation. */ +@keyframes tocEnter { + from { opacity: 0; } + to { opacity: 1; } +} + +.tocContentInner { + min-height: 0; + overflow: hidden; + animation: tocEnter var(--duration-subtle) var(--ease-enter) both; +} + +.tocContentInnerOpen { + max-height: 20vh; + overflow-y: auto; +} + +/* H2-level link */ +.tocLink { + display: block; + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: var(--lh-sm); + color: var(--text-muted); + text-decoration: none; + padding: 3px var(--space-2); + border-left: 2px solid transparent; + margin-left: calc(-1 * var(--space-2) - 2px); + padding-left: calc(var(--space-2) + 2px); + transition: color 150ms ease; +} + +.tocLink:hover { + color: var(--text-body); + text-decoration: none; +} + +/* H3-level link — extra indent */ +.tocSublink { + display: block; + font-family: var(--font-body); + font-size: var(--text-xs); + line-height: var(--lh-sm); + color: var(--text-muted); + text-decoration: none; + padding: 2px var(--space-2); + padding-left: calc(var(--space-4) + 2px); + border-left: 2px solid transparent; + margin-left: calc(-1 * var(--space-2) - 2px); + transition: color 150ms ease; +} + +.tocSublink:hover { + color: var(--text-body); + text-decoration: none; +} + +/* Active state — position cursor, not semantic emphasis → use neutral */ +.tocLinkActive { + color: var(--text-body); + font-weight: 500; + border-left-color: var(--border-emphasis); +} + +.tocSublink.tocLinkActive { + font-weight: 400; +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .tocChevron, + .tocContent, + .tocActiveLabel { + transition-duration: 0s !important; + } + .tocContentInner { + animation: none !important; + } +} + +/* Mobile sidebar additions */ +.searchArea { + padding: var(--space-2); +} + +.mobileChannelArea { + padding: var(--space-2); +} + +.mobileFooterLinks { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-2) var(--space-2); +} diff --git a/website/src/theme/DocSidebar/Mobile/index.tsx b/website/src/theme/DocSidebar/Mobile/index.tsx new file mode 100644 index 0000000..bfac896 --- /dev/null +++ b/website/src/theme/DocSidebar/Mobile/index.tsx @@ -0,0 +1,55 @@ +import {type ReactNode} from 'react'; +import Mobile from '@theme-original/DocSidebar/Mobile'; +import type MobileType from '@theme/DocSidebar/Mobile'; +import type {WrapperProps} from '@docusaurus/types'; +import Link from '@docusaurus/Link'; +import {useLocation} from '@docusaurus/router'; +import SearchBar from '@theme/SearchBar'; +import clsx from 'clsx'; +import styles from '../Desktop/styles.module.css'; +import {channels, getActiveIndex} from '../Desktop/channels'; + +type Props = WrapperProps; + +export default function MobileWrapper(props: Props): ReactNode { + const location = useLocation(); + const activeIndex = getActiveIndex(location.pathname); + + return ( + <> +
+
+ {channels.map(({label, path, match}) => ( + + {label} + + ))} +
+
+
+ +
+ +
+ + ); +} diff --git a/website/src/theme/DocSidebarItem/Link/index.tsx b/website/src/theme/DocSidebarItem/Link/index.tsx new file mode 100644 index 0000000..2047758 --- /dev/null +++ b/website/src/theme/DocSidebarItem/Link/index.tsx @@ -0,0 +1,68 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import {isActiveSidebarItem} from '@docusaurus/plugin-content-docs/client'; +import Link from '@docusaurus/Link'; +import isInternalUrl from '@docusaurus/isInternalUrl'; +import IconExternalLink from '@theme/Icon/ExternalLink'; +import type {Props} from '@theme/DocSidebarItem/Link'; + +import styles from './styles.module.css'; + +function LinkLabel({label, sectionNumber}: {label: string; sectionNumber?: number}) { + return ( + <> + {sectionNumber != null && ( + + )} + + {label} + + + ); +} + +export default function DocSidebarItemLink({ + item, + onItemClick, + activePath, + level, + index, + ...props +}: Props): ReactNode { + const {href, label, className, autoAddBaseUrl, customProps} = item; + const sectionNumber = (customProps as {sectionNumber?: number} | undefined)?.sectionNumber; + const isActive = isActiveSidebarItem(item, activePath); + const isInternalLink = isInternalUrl(href); + return ( +
  • + onItemClick(item) : undefined, + })} + {...props}> + + {!isInternalLink && } + +
  • + ); +} diff --git a/website/src/theme/DocSidebarItem/Link/styles.module.css b/website/src/theme/DocSidebarItem/Link/styles.module.css new file mode 100644 index 0000000..214d958 --- /dev/null +++ b/website/src/theme/DocSidebarItem/Link/styles.module.css @@ -0,0 +1,11 @@ +.menuExternalLink { + align-items: center; +} + +.linkLabel { + overflow: hidden; + display: -webkit-box; + line-clamp: 2; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} diff --git a/website/src/theme/Icon/DarkMode/index.tsx b/website/src/theme/Icon/DarkMode/index.tsx new file mode 100644 index 0000000..6d2d5e5 --- /dev/null +++ b/website/src/theme/Icon/DarkMode/index.tsx @@ -0,0 +1,19 @@ +import {type ReactNode} from 'react'; +import type {Props} from '@theme/Icon/DarkMode'; + +// Stroked crescent moon — Smooth Circuit family (design system) +// Standard Lucide-style moon path at 24x24 viewBox. +export default function IconDarkMode(props: Props): ReactNode { + return ( + + + + ); +} diff --git a/website/src/theme/Icon/LightMode/index.tsx b/website/src/theme/Icon/LightMode/index.tsx new file mode 100644 index 0000000..847289d --- /dev/null +++ b/website/src/theme/Icon/LightMode/index.tsx @@ -0,0 +1,28 @@ +import {type ReactNode} from 'react'; +import type {Props} from '@theme/Icon/LightMode'; + +// Stroked sun icon — Smooth Circuit family (design system) +// Circle r=5 at center (12,12). 8 rays from r=8 to r=11. +// Diagonal coordinates computed: cos/sin at 45° intervals. +export default function IconLightMode(props: Props): ReactNode { + return ( + + + + + + + + + + + + + + ); +} diff --git a/website/src/theme/Icon/SystemColorMode/index.tsx b/website/src/theme/Icon/SystemColorMode/index.tsx new file mode 100644 index 0000000..7c06924 --- /dev/null +++ b/website/src/theme/Icon/SystemColorMode/index.tsx @@ -0,0 +1,21 @@ +import {type ReactNode} from 'react'; +import type {Props} from '@theme/Icon/SystemColorMode'; + +// Stroked circle with vertical line — Smooth Circuit family (design system) +// Circle r=9 at center (12,12). Vertical line from (12,3) to (12,21). +// Conveys "split between light and dark" without using fill. +export default function IconSystemColorMode(props: Props): ReactNode { + return ( + + + + + + + ); +} diff --git a/website/src/theme/Navbar/ColorModeToggle/index.tsx b/website/src/theme/Navbar/ColorModeToggle/index.tsx new file mode 100644 index 0000000..5720a98 --- /dev/null +++ b/website/src/theme/Navbar/ColorModeToggle/index.tsx @@ -0,0 +1,22 @@ +import {type ReactNode} from 'react'; +import {useColorMode, useThemeConfig} from '@docusaurus/theme-common'; +import ColorModeToggle from '@theme/ColorModeToggle'; +import type {Props} from '@theme/Navbar/ColorModeToggle'; + +export default function NavbarColorModeToggle({className}: Props): ReactNode { + const {disableSwitch, respectPrefersColorScheme} = useThemeConfig().colorMode; + const {colorModeChoice, setColorMode} = useColorMode(); + + if (disableSwitch) { + return null; + } + + return ( + + ); +} diff --git a/website/src/theme/tocStore.ts b/website/src/theme/tocStore.ts new file mode 100644 index 0000000..bb4bc48 --- /dev/null +++ b/website/src/theme/tocStore.ts @@ -0,0 +1,23 @@ +import React from 'react'; +import type { TOCItem } from '@docusaurus/mdx-loader'; + +type Listener = (toc: readonly TOCItem[]) => void; + +let _toc: readonly TOCItem[] = []; +const _listeners = new Set(); + +export function publishTOC(toc: readonly TOCItem[]): void { + _toc = toc; + _listeners.forEach(fn => fn(toc)); +} + +export function useSidebarTOC(): readonly TOCItem[] { + // useState + useEffect — NOT useState(initializer) to avoid SSR mismatch + const [toc, setToc] = React.useState([]); + React.useEffect(() => { + setToc(_toc); // sync on mount (catches in-flight nav) + _listeners.add(setToc); + return () => { _listeners.delete(setToc); }; + }, []); + return toc; +} diff --git a/website/src/utils/svgMath.ts b/website/src/utils/svgMath.ts new file mode 100644 index 0000000..b5be4e5 --- /dev/null +++ b/website/src/utils/svgMath.ts @@ -0,0 +1,20 @@ +/** Compute a closed superellipse SVG path via Lamé's formula. */ +export function superellipsePath( + cx: number, + cy: number, + rx: number, + ry: number, + n: number, + segments = 64 +): string { + const pts: string[] = []; + for (let i = 0; i <= segments; i++) { + const t = (i / segments) * 2 * Math.PI; + const c = Math.cos(t); + const s = Math.sin(t); + const x = cx + rx * Math.sign(c) * Math.abs(c) ** (2 / n); + const y = cy + ry * Math.sign(s) * Math.abs(s) ** (2 / n); + pts.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`); + } + return pts.join(' ') + 'Z'; +} diff --git a/website/static/audio/manifest.json b/website/static/audio/manifest.json index 22c8cbf..a05b6c2 100644 --- a/website/static/audio/manifest.json +++ b/website/static/audio/manifest.json @@ -1,12 +1,12 @@ { - "intro.md": { + "intro.mdx": { "audioUrl": "/audio/intro.mp3", "size": 19343176, "format": "audio/mp3", "tokenCount": 1318, "chunks": 2, "generatedAt": "2025-12-30T13:26:07.789Z", - "scriptSource": "intro.md" + "scriptSource": "intro.mdx" }, "practical-techniques/lesson-10-debugging.md": { "audioUrl": "/audio/practical-techniques/lesson-10-debugging.mp3", diff --git a/website/static/fonts/inter-latin-ext.woff2 b/website/static/fonts/inter-latin-ext.woff2 new file mode 100644 index 0000000..57da6f8 Binary files /dev/null and b/website/static/fonts/inter-latin-ext.woff2 differ diff --git a/website/static/fonts/inter-latin.woff2 b/website/static/fonts/inter-latin.woff2 new file mode 100644 index 0000000..91dc3e8 Binary files /dev/null and b/website/static/fonts/inter-latin.woff2 differ diff --git a/website/static/fonts/monaspace-argon-400.woff2 b/website/static/fonts/monaspace-argon-400.woff2 new file mode 100644 index 0000000..1d0207f Binary files /dev/null and b/website/static/fonts/monaspace-argon-400.woff2 differ diff --git a/website/static/fonts/monaspace-argon-500.woff2 b/website/static/fonts/monaspace-argon-500.woff2 new file mode 100644 index 0000000..5ce3dbc Binary files /dev/null and b/website/static/fonts/monaspace-argon-500.woff2 differ diff --git a/website/static/fonts/monaspace-krypton-400.woff2 b/website/static/fonts/monaspace-krypton-400.woff2 new file mode 100644 index 0000000..6e666e7 Binary files /dev/null and b/website/static/fonts/monaspace-krypton-400.woff2 differ diff --git a/website/static/fonts/monaspace-krypton-500.woff2 b/website/static/fonts/monaspace-krypton-500.woff2 new file mode 100644 index 0000000..56c6252 Binary files /dev/null and b/website/static/fonts/monaspace-krypton-500.woff2 differ diff --git a/website/static/fonts/monaspace-neon-400.woff2 b/website/static/fonts/monaspace-neon-400.woff2 new file mode 100644 index 0000000..005fa27 Binary files /dev/null and b/website/static/fonts/monaspace-neon-400.woff2 differ diff --git a/website/static/fonts/monaspace-neon-500.woff2 b/website/static/fonts/monaspace-neon-500.woff2 new file mode 100644 index 0000000..b897af5 Binary files /dev/null and b/website/static/fonts/monaspace-neon-500.woff2 differ diff --git a/website/static/fonts/monaspace-neon-600.woff2 b/website/static/fonts/monaspace-neon-600.woff2 new file mode 100644 index 0000000..0af2ff3 Binary files /dev/null and b/website/static/fonts/monaspace-neon-600.woff2 differ diff --git a/website/static/fonts/monaspace-neon-700.woff2 b/website/static/fonts/monaspace-neon-700.woff2 new file mode 100644 index 0000000..408bc6b Binary files /dev/null and b/website/static/fonts/monaspace-neon-700.woff2 differ diff --git a/website/static/fonts/monaspace-radon-400.woff2 b/website/static/fonts/monaspace-radon-400.woff2 new file mode 100644 index 0000000..5c86d3f Binary files /dev/null and b/website/static/fonts/monaspace-radon-400.woff2 differ diff --git a/website/static/fonts/monaspace-xenon-400.woff2 b/website/static/fonts/monaspace-xenon-400.woff2 new file mode 100644 index 0000000..f10788b Binary files /dev/null and b/website/static/fonts/monaspace-xenon-400.woff2 differ diff --git a/website/static/fonts/monaspace-xenon-500.woff2 b/website/static/fonts/monaspace-xenon-500.woff2 new file mode 100644 index 0000000..cf93384 Binary files /dev/null and b/website/static/fonts/monaspace-xenon-500.woff2 differ diff --git a/website/static/fonts/monaspace-xenon-600.woff2 b/website/static/fonts/monaspace-xenon-600.woff2 new file mode 100644 index 0000000..b0ec7eb Binary files /dev/null and b/website/static/fonts/monaspace-xenon-600.woff2 differ diff --git a/website/static/fonts/space-grotesk-latin-ext.woff2 b/website/static/fonts/space-grotesk-latin-ext.woff2 new file mode 100644 index 0000000..e3aa61c Binary files /dev/null and b/website/static/fonts/space-grotesk-latin-ext.woff2 differ diff --git a/website/static/fonts/space-grotesk-latin.woff2 b/website/static/fonts/space-grotesk-latin.woff2 new file mode 100644 index 0000000..7b0e76a Binary files /dev/null and b/website/static/fonts/space-grotesk-latin.woff2 differ diff --git a/website/static/img/apple-touch-icon.png b/website/static/img/apple-touch-icon.png index 1302a4d..dcc32d0 100644 Binary files a/website/static/img/apple-touch-icon.png and b/website/static/img/apple-touch-icon.png differ diff --git a/website/static/img/favicon-source.svg b/website/static/img/favicon-source.svg index 9c6c90e..f9193f2 100644 --- a/website/static/img/favicon-source.svg +++ b/website/static/img/favicon-source.svg @@ -1,20 +1,11 @@ - AI Coding Course + Agentic Coding - - - - - - - - - - + + + + + + diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico index fd15ba5..8d7bf60 100644 Binary files a/website/static/img/favicon.ico and b/website/static/img/favicon.ico differ diff --git a/website/static/img/icon.svg b/website/static/img/icon.svg index 8f80d46..7c9d426 100644 --- a/website/static/img/icon.svg +++ b/website/static/img/icon.svg @@ -1,26 +1,18 @@ - AI Coding Course — Agent Loop Icon - Minimal agent loop with a node. Purple loop + fuchsia node, theme-aware. + Agentic Coding + Code glyph mark. Achromatic, theme-aware. - - - - + + + + - - - diff --git a/website/static/img/logo.svg b/website/static/img/logo.svg index 8f80d46..7c9d426 100644 --- a/website/static/img/logo.svg +++ b/website/static/img/logo.svg @@ -1,26 +1,18 @@ - AI Coding Course — Agent Loop Icon - Minimal agent loop with a node. Purple loop + fuchsia node, theme-aware. + Agentic Coding + Code glyph mark. Achromatic, theme-aware. - - - - + + + + - - - diff --git a/website/static/img/social-card.png b/website/static/img/social-card.png index 1653e3f..2e195a3 100644 Binary files a/website/static/img/social-card.png and b/website/static/img/social-card.png differ diff --git a/website/static/presentations/manifest.json b/website/static/presentations/manifest.json index 824c9dd..9ed367c 100644 --- a/website/static/presentations/manifest.json +++ b/website/static/presentations/manifest.json @@ -1,5 +1,5 @@ { - "intro.md": { + "intro.mdx": { "presentationUrl": "/presentations/intro.json", "slideCount": 10, "estimatedDuration": "20-30 minutes",