diff --git a/corpus/frontend/design-tokens/index.yaml b/corpus/frontend/design-tokens/index.yaml
new file mode 100644
index 0000000..1f41659
--- /dev/null
+++ b/corpus/frontend/design-tokens/index.yaml
@@ -0,0 +1,18 @@
+namespace: frontend.design-tokens
+procedures:
+ step-1:
+ file: step-1.yaml
+ step-2:
+ file: step-2.yaml
+ step-3:
+ file: step-3.yaml
+ step-4:
+ file: step-4.yaml
+ step-5:
+ file: step-5.yaml
+ step-6:
+ file: step-6.yaml
+ step-7:
+ file: step-7.yaml
+ step-8:
+ file: step-8.yaml
diff --git a/corpus/frontend/design-tokens/step-1.yaml b/corpus/frontend/design-tokens/step-1.yaml
new file mode 100644
index 0000000..02352d5
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-1.yaml
@@ -0,0 +1,51 @@
+step: 1
+title: Define color ramp primitives in @theme
+description: >-
+ Create 11-stop OKLCH ramps for brand, neutral, and pop colors. These are
+ static compile-time values.
+code: |
+ @theme {
+ --color-brand-50: oklch(0.98 0.01 291);
+ --color-brand-100: oklch(0.96 0.02 291);
+ --color-brand-200: oklch(0.90 0.04 291);
+ --color-brand-300: oklch(0.82 0.07 291);
+ --color-brand-400: oklch(0.72 0.11 291);
+ --color-brand-500: oklch(0.62 0.14 291);
+ --color-brand-600: oklch(0.54 0.14 291);
+ --color-brand-700: oklch(0.46 0.13 291);
+ --color-brand-800: oklch(0.38 0.11 291);
+ --color-brand-900: oklch(0.28 0.08 291);
+ --color-brand-950: oklch(0.18 0.05 291);
+
+ --color-neutral-50: oklch(0.98 0.008 60);
+ --color-neutral-100: oklch(0.96 0.008 60);
+ --color-neutral-200: oklch(0.92 0.008 60);
+ --color-neutral-300: oklch(0.84 0.008 60);
+ --color-neutral-400: oklch(0.72 0.008 60);
+ --color-neutral-500: oklch(0.58 0.010 60);
+ --color-neutral-600: oklch(0.46 0.010 60);
+ --color-neutral-700: oklch(0.35 0.010 60);
+ --color-neutral-800: oklch(0.26 0.010 60);
+ --color-neutral-900: oklch(0.18 0.008 60);
+ --color-neutral-950: oklch(0.12 0.005 60);
+
+ --color-pop-50: oklch(0.97 0.025 50);
+ --color-pop-100: oklch(0.94 0.045 50);
+ --color-pop-200: oklch(0.88 0.075 50);
+ --color-pop-300: oklch(0.80 0.110 50);
+ --color-pop-400: oklch(0.72 0.145 50);
+ --color-pop-500: oklch(0.65 0.165 50);
+ --color-pop-600: oklch(0.57 0.155 50);
+ --color-pop-700: oklch(0.48 0.140 50);
+ --color-pop-800: oklch(0.38 0.110 50);
+ --color-pop-900: oklch(0.28 0.070 50);
+ --color-pop-950: oklch(0.18 0.040 50);
+ }
+rules:
+ - Use @theme (not :root) for primitive ramps - they become Tailwind static utilities
+ - Hue angle (H) must be consistent across all stops in a ramp
+ - Chroma (C) peaks around 400-600, decreases toward 50 and 950
+ - Lightness (L) must be monotonically decreasing from 50 to 950
+gotchas:
+ - "@theme values are static - they cannot reference CSS custom properties or be overridden at runtime by JavaScript."
+ - "If you change the H value after building components, all color utilities change across the app. Lock the hue angle early."
diff --git a/corpus/frontend/design-tokens/step-2.yaml b/corpus/frontend/design-tokens/step-2.yaml
new file mode 100644
index 0000000..048795e
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-2.yaml
@@ -0,0 +1,66 @@
+step: 2
+title: Map semantic tokens in :root and .dark
+description: >-
+ Create role-based semantic tokens that reference primitives. These are the
+ tokens your components actually use.
+code: |
+ :root {
+ /* Backgrounds */
+ --color-bg: var(--color-neutral-50);
+ --color-bg-subtle: var(--color-neutral-100);
+ --color-surface: #ffffff;
+ --color-surface-raised: var(--color-neutral-50);
+
+ /* Borders */
+ --color-border: var(--color-neutral-200);
+ --color-border-strong: var(--color-neutral-300);
+ --color-border-focus: var(--color-brand-500);
+
+ /* Text */
+ --color-text: var(--color-neutral-900);
+ --color-text-muted: var(--color-neutral-500);
+ --color-text-subtle: var(--color-neutral-400);
+ --color-text-inverted: #ffffff;
+
+ /* Interactive */
+ --color-primary: var(--color-brand-500);
+ --color-primary-hover: var(--color-brand-600);
+ --color-primary-fg: #ffffff;
+ --color-secondary: var(--color-neutral-200);
+ --color-secondary-hover: var(--color-neutral-300);
+ --color-secondary-fg: var(--color-neutral-900);
+
+ /* Status */
+ --color-success: oklch(0.55 0.14 145);
+ --color-success-bg: oklch(0.96 0.04 145);
+ --color-success-fg: #ffffff;
+ --color-error: oklch(0.55 0.20 25);
+ --color-error-bg: oklch(0.97 0.04 25);
+ --color-error-fg: #ffffff;
+ --color-warning: oklch(0.65 0.18 75);
+ --color-warning-bg: oklch(0.97 0.04 75);
+ --color-warning-fg: oklch(0.20 0.05 75);
+ }
+
+ .dark {
+ --color-bg: oklch(0.15 0.008 265);
+ --color-bg-subtle: oklch(0.18 0.008 265);
+ --color-surface: oklch(0.21 0.008 265);
+ --color-surface-raised: oklch(0.24 0.008 265);
+ --color-border: oklch(0.28 0.008 265);
+ --color-border-strong: oklch(0.35 0.008 265);
+ --color-text: oklch(0.95 0.008 265);
+ --color-text-muted: oklch(0.65 0.008 265);
+ --color-text-subtle: oklch(0.50 0.008 265);
+ --color-primary: var(--color-brand-400);
+ --color-primary-hover: var(--color-brand-300);
+ --color-primary-fg: oklch(0.15 0.005 291);
+ }
+rules:
+ - Components must only reference semantic tokens, never primitive ramp values directly
+ - Dark mode requires bg-color elevation (lighter bg per level) since shadows are invisible
+ - Status color L should be ~0.55 for 4.5:1 contrast with white foreground
+ - Dark mode primary shifts from brand-500 (light) to brand-400 (dark) for perceptual brightness parity
+gotchas:
+ - "Status colors at L=0.63 (typical green/red) fail 4.5:1 AA with white. Always run contrast check after defining status colors."
+ - "Do not use hex values for semantic tokens - use oklch primitives so the color stays in the defined color space."
diff --git a/corpus/frontend/design-tokens/step-3.yaml b/corpus/frontend/design-tokens/step-3.yaml
new file mode 100644
index 0000000..8016fb5
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-3.yaml
@@ -0,0 +1,41 @@
+step: 3
+title: Bridge to Tailwind v4 utilities with @theme inline
+description: >-
+ Use @theme inline to expose runtime-swappable CSS custom properties as
+ Tailwind utility classes.
+code: |
+ /* @theme inline READS from CSS vars at runtime */
+ /* This means dark mode changes propagate to Tailwind utilities automatically */
+ @theme inline {
+ /* Map semantic tokens to Tailwind utility names */
+ --color-background: var(--color-bg);
+ --color-foreground: var(--color-text);
+ --color-muted: var(--color-text-muted);
+ --color-border: var(--color-border);
+ --color-surface: var(--color-surface);
+
+ --color-primary: var(--color-primary);
+ --color-primary-foreground: var(--color-primary-fg);
+ --color-secondary: var(--color-secondary);
+ --color-secondary-foreground: var(--color-secondary-fg);
+
+ --color-success: var(--color-success);
+ --color-success-foreground: var(--color-success-fg);
+ --color-error: var(--color-error);
+ --color-error-foreground: var(--color-error-fg);
+ --color-warning: var(--color-warning);
+ --color-warning-foreground: var(--color-warning-fg);
+ }
+
+ /* Now these work as Tailwind utilities: */
+ /* bg-background, text-foreground, bg-primary, text-primary-foreground */
+ /* border-border, text-muted, bg-surface, bg-success, text-error... */
+rules:
+ - "@theme inline is read at runtime - it reflects CSS custom property values dynamically"
+ - Plain @theme is static - values are baked in at build time
+ - Use @theme inline ONLY for semantic tokens; use plain @theme for primitive ramps
+ - "Name mapping: --color-X in @theme inline → bg-X, text-X, border-X Tailwind classes"
+gotchas:
+ - "If you put runtime-swappable vars in plain @theme (not inline), dark mode will NOT work - the values won't update when .dark class is toggled."
+ - "@theme inline does not accept hardcoded values - it should only map CSS var references."
+ - "@theme generates Tailwind utility classes but does NOT emit CSS custom properties on :root. If :root uses var(--color-brand-400) and --color-brand-400 is only defined in @theme, the var() resolves to undefined at runtime - making all text and backgrounds white. Correct architecture: raw OKLCH values on :root and .dark (runtime CSS vars), @theme inline bridges them to utilities, @theme separately generates primitive ramp utilities."
diff --git a/corpus/frontend/design-tokens/step-4.yaml b/corpus/frontend/design-tokens/step-4.yaml
new file mode 100644
index 0000000..0d7ec16
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-4.yaml
@@ -0,0 +1,31 @@
+step: 4
+title: Define spacing system
+description: Set the 4px base multiplier and create named semantic spacing tokens.
+code: |
+ @theme {
+ /* 4px base - sets the multiplier for p-1, p-2, p-4, etc. */
+ --spacing: 0.25rem;
+ }
+
+ /* Named semantic tokens in :root */
+ :root {
+ --spacing-section-y: 6rem; /* 96px */
+ --spacing-section-x: 1.5rem; /* 24px mobile, override at md */
+ --spacing-card: 1.75rem; /* 28px */
+ --spacing-card-sm: 1.25rem; /* 20px */
+ --spacing-grid-cards: 1.5rem; /* 24px */
+ --spacing-inline: 0.5rem; /* 8px */
+ --spacing-form-gap: 1.25rem; /* 20px */
+ }
+
+ @media (min-width: 768px) {
+ :root {
+ --spacing-section-x: 3rem; /* 48px desktop */
+ }
+ }
+rules:
+ - "--spacing is the global multiplier - changing it scales ALL numeric spacing utilities"
+ - "Named tokens auto-generate Tailwind utilities: p-card, py-section-y, gap-grid-cards"
+ - "Always follow 4px grid: 0.25rem, 0.5rem, 0.75rem, 1rem, 1.25rem, 1.5rem, 1.75rem..."
+gotchas:
+ - "--spacing is NOT an individual token for a specific spacing value. It is the base MULTIPLIER that affects p-1, p-2, p-4, etc. Overriding it changes every numeric spacing utility in the project."
diff --git a/corpus/frontend/design-tokens/step-5.yaml b/corpus/frontend/design-tokens/step-5.yaml
new file mode 100644
index 0000000..7591d60
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-5.yaml
@@ -0,0 +1,34 @@
+step: 5
+title: Set up typography scale
+description: >-
+ Define fluid heading sizes with clamp(), fixed body sizes, and line-height
+ and tracking tokens.
+code: |
+ :root {
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
+ --font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
+
+ --text-display: clamp(3rem, 5vw + 1rem, 4.5rem);
+ --text-h1: clamp(2.25rem, 3.5vw + 0.5rem, 3rem);
+ --text-h2: clamp(1.75rem, 2.5vw + 0.25rem, 2.25rem);
+ --text-h3: clamp(1.375rem, 1.5vw + 0.25rem, 1.5rem);
+ --text-body: 1rem;
+ --text-body-sm: 0.875rem;
+ --text-caption: 0.75rem;
+ --text-overline: 0.6875rem;
+
+ --leading-display: 1.05;
+ --leading-heading: 1.2;
+ --leading-body: 1.7;
+
+ --tracking-tight: -0.03em;
+ --tracking-heading: -0.02em;
+ --tracking-normal: 0em;
+ --tracking-wide: 0.10em;
+ }
+rules:
+ - Body text (16px) is NEVER fluid - use fixed rem values
+ - Headings MUST have tight line-height (1.05-1.2), not body line-height (1.6+)
+ - Negative tracking on body text is a readability error
+gotchas:
+ - Applying clamp() to body text causes font-size to change on window resize, causing reflow and jarring user experience.
diff --git a/corpus/frontend/design-tokens/step-6.yaml b/corpus/frontend/design-tokens/step-6.yaml
new file mode 100644
index 0000000..159f382
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-6.yaml
@@ -0,0 +1,33 @@
+step: 6
+title: Define shadows and elevation
+description: >-
+ Build the 5-level elevation shadow system using warm oklch-tinted shadows.
+code: |
+ :root {
+ --shadow-xs: 0 1px 2px 0 oklch(0.30 0.02 60 / 0.08);
+ --shadow-sm:
+ 0 1px 3px 0 oklch(0.30 0.02 60 / 0.10),
+ 0 1px 2px -1px oklch(0.30 0.02 60 / 0.06);
+ --shadow-md:
+ 0 4px 6px -1px oklch(0.30 0.02 60 / 0.10),
+ 0 2px 4px -2px oklch(0.30 0.02 60 / 0.06);
+ --shadow-lg:
+ 0 10px 15px -3px oklch(0.30 0.02 60 / 0.10),
+ 0 4px 6px -4px oklch(0.30 0.02 60 / 0.05);
+ --shadow-xl:
+ 0 20px 25px -5px oklch(0.30 0.02 60 / 0.10),
+ 0 8px 10px -6px oklch(0.30 0.02 60 / 0.04);
+ }
+
+ .dark {
+ --shadow-xs: none; --shadow-sm: none;
+ --shadow-md: none; --shadow-lg: none; --shadow-xl: none;
+ --color-surface-1: oklch(0.22 0.008 265);
+ --color-surface-2: oklch(0.26 0.008 265);
+ --color-surface-3: oklch(0.30 0.008 265);
+ }
+rules:
+ - Use oklch-tinted shadows, never rgba(0,0,0)
+ - Dark mode disables all shadows, uses bg-color elevation instead
+gotchas:
+ - rgba(0,0,0) shadows look cold on warm backgrounds. The warm hue tint (H=60) makes shadows feel natural.
diff --git a/corpus/frontend/design-tokens/step-7.yaml b/corpus/frontend/design-tokens/step-7.yaml
new file mode 100644
index 0000000..be8b7f3
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-7.yaml
@@ -0,0 +1,32 @@
+step: 7
+title: Define motion and z-index tokens
+description: Add duration, easing, and z-index scale with prefers-reduced-motion.
+code: |
+ @theme {
+ --duration-fast: 100ms;
+ --duration-normal: 200ms;
+ --duration-slow: 300ms;
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
+ --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
+
+ --z-dropdown: 1000;
+ --z-sticky: 1010;
+ --z-modal: 1050;
+ --z-tooltip: 1070;
+ --z-toast: 1080;
+ }
+
+ @layer base {
+ @media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+ }
+ }
+rules:
+ - prefers-reduced-motion MUST be in @layer base with !important
+ - Never use arbitrary z-index values
+gotchas:
+ - Forgetting prefers-reduced-motion is a WCAG 2.3.3 violation.
diff --git a/corpus/frontend/design-tokens/step-8.yaml b/corpus/frontend/design-tokens/step-8.yaml
new file mode 100644
index 0000000..8ad66b2
--- /dev/null
+++ b/corpus/frontend/design-tokens/step-8.yaml
@@ -0,0 +1,27 @@
+step: 8
+title: Run contrast audit and verify token usage
+description: >-
+ After building the full token system, run a contrast audit on all color
+ token pairs.
+code: |
+ /* Run contrast checks on these pairs: */
+ /* --color-text on --color-bg → target 7:1 (AAA) */
+ /* --color-text-muted on --color-bg → target 4.5:1 (AA) */
+ /* --color-primary-fg on --color-primary → target 4.5:1 (AA) */
+ /* --color-success-fg on --color-success → target 4.5:1 (AA) */
+ /* --color-error-fg on --color-error → target 4.5:1 (AA) */
+ /* --color-border on --color-bg → target 1.1:1+ (perceptible) */
+
+ /* Tools: */
+ /* - https://oklch.com - build and preview oklch colors */
+ /* - https://www.myndex.com/APCA/ - APCA contrast checker */
+ /* - Storybook a11y addon - automated component-level checks */
+
+ /* Check for stale hue references: */
+ /* grep -r "violet-\|purple-\|blue-" src/ (old hardcoded Tailwind hue names) */
+rules:
+ - Run contrast audit AFTER finalizing the palette - before building components
+ - Status colors often need L adjusted to ~0.55 for AA compliance with white foreground
+ - Grep for stale hue-name references after renaming ramps
+gotchas:
+ - "WCAG contrast is calculated on final rendered colors. If CSS vars are not resolving correctly in dark mode, contrast tools won't catch it - test with a browser extension on the live page."
diff --git a/corpus/frontend/designer/dashboard.yaml b/corpus/frontend/designer/dashboard.yaml
new file mode 100644
index 0000000..b616096
--- /dev/null
+++ b/corpus/frontend/designer/dashboard.yaml
@@ -0,0 +1,90 @@
+type: dashboard
+description: >-
+ Data-forward workspace with a fixed shell, clear hierarchy, and enough density
+ for power users without sacrificing readability.
+layout: >-
+ App shell: fixed sidebar (240-280px) + header (56-64px) + scrollable content
+ area
+sections:
+ - name: Sidebar Navigation
+ required: true
+ components:
+ - Logo/brand mark
+ - Primary nav items (5-7 max)
+ - Workspace switcher
+ - Collapsed state for mobile
+ - User avatar + settings at bottom
+ notes: >-
+ Persistent, keyboard navigable, and clearly indicates the active route.
+ - name: Header Bar
+ required: true
+ components:
+ - Page title / breadcrumbs
+ - Search (Cmd+K)
+ - Notifications
+ - User menu
+ notes: >-
+ Fixed or sticky. Keep power-user actions in the header so they are always
+ reachable.
+ - name: Metrics Row
+ required: false
+ components:
+ - 3-5 KPI cards
+ - Trend indicator (up/down arrow + %)
+ - Sparkline or mini chart
+ notes: >-
+ Group related metrics and keep visible count within working-memory limits.
+ - name: Primary Content
+ required: true
+ components:
+ - Data table or card grid
+ - Filters + sort controls
+ - Pagination or virtual scroll
+ - Bulk actions toolbar
+ notes: >-
+ Tables should right-align numbers, use monospace for values, and keep
+ header rows sticky.
+ - name: Detail Panel
+ required: false
+ components:
+ - Side drawer (not modal)
+ - Record details
+ - Edit-in-place or form
+ - Action buttons
+ notes: >-
+ Keep list context visible while the user inspects or edits a record.
+ - name: Empty States
+ required: true
+ components:
+ - Illustration (optional)
+ - Headline: what's missing
+ - Body: what to do next
+ - Single CTA
+ notes: >-
+ Every data container needs an empty state that helps the user recover or
+ create the first item.
+cognitiveApply:
+ - miller
+ - hick
+ - gestalt
+ - fitts
+ - doherty
+compositionApply:
+ - grid-theory
+ - visual-hierarchy
+interactionApply:
+ - navigation
+ - skeleton-vs-spinner
+ - progressive-disclosure
+ - empty-states
+ - feedback
+writingApply:
+ - headlines
+ - empty-states-copy
+ - loading-states
+keyRules:
+ - Command palette (Cmd+K) for 50+ feature apps
+ - Keyboard shortcuts for all frequent actions
+ - Skeleton loaders for data panels, not spinners
+ - Right-align quantitative data in monospace font
+ - Compact density: 14px body, 48px section gaps, 20px card padding
diff --git a/corpus/frontend/designer/index.yaml b/corpus/frontend/designer/index.yaml
new file mode 100644
index 0000000..de0c3c5
--- /dev/null
+++ b/corpus/frontend/designer/index.yaml
@@ -0,0 +1,4 @@
+namespace: frontend.designer
+pageTemplates:
+ dashboard:
+ file: dashboard.yaml
diff --git a/corpus/frontend/lenis/gsap-integration.yaml b/corpus/frontend/lenis/gsap-integration.yaml
new file mode 100644
index 0000000..04cdac8
--- /dev/null
+++ b/corpus/frontend/lenis/gsap-integration.yaml
@@ -0,0 +1,52 @@
+name: gsap-integration
+description: >-
+ Integrate Lenis with GSAP ScrollTrigger by disabling autoRaf and driving Lenis
+ from GSAP's ticker.
+code: |
+ "use client";
+
+ import { useEffect } from "react";
+ import { ReactLenis, useLenis } from "lenis/react";
+ import "lenis/dist/lenis.css";
+ import gsap from "gsap";
+ import ScrollTrigger from "gsap/ScrollTrigger";
+
+ gsap.registerPlugin(ScrollTrigger);
+
+ function GSAPSyncEffect() {
+ const lenis = useLenis();
+
+ useEffect(() => {
+ if (!lenis) return;
+
+ // Sync Lenis with GSAP ticker
+ gsap.ticker.add((time) => {
+ lenis.raf(time * 1000);
+ });
+
+ // Disable GSAP's lagSmoothing to prevent conflicts
+ gsap.ticker.lagSmoothing(0);
+
+ // Update ScrollTrigger on each scroll
+ lenis.on("scroll", ScrollTrigger.update);
+
+ return () => {
+ lenis.off("scroll", ScrollTrigger.update);
+ };
+ }, [lenis]);
+
+ return null;
+ }
+
+ export function GSAPLenisProvider({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+ }
+tips:
+ - autoRaf: false is required - if GSAP and Lenis both run their own RAF loops, scroll updates fire twice per frame causing desync.
+ - gsap.ticker.lagSmoothing(0) prevents GSAP from adjusting delta time which would cause Lenis to stutter after tab switches.
+ - lenis.raf() expects milliseconds - multiply GSAP time (seconds) by 1000.
diff --git a/corpus/frontend/lenis/index.yaml b/corpus/frontend/lenis/index.yaml
new file mode 100644
index 0000000..a9f0f74
--- /dev/null
+++ b/corpus/frontend/lenis/index.yaml
@@ -0,0 +1,4 @@
+namespace: frontend.lenis
+patterns:
+ gsap-integration:
+ file: gsap-integration.yaml
diff --git a/corpus/frontend/motion/examples/scroll.yaml b/corpus/frontend/motion/examples/scroll.yaml
new file mode 100644
index 0000000..a192acc
--- /dev/null
+++ b/corpus/frontend/motion/examples/scroll.yaml
@@ -0,0 +1,54 @@
+category: scroll
+examples:
+ - title: Scroll progress rail
+ description: Fixed progress indicator driven by the page scroll position.
+ code: |
+ import { motion, useScroll, useSpring } from "motion/react"
+
+ export function ScrollProgressRail() {
+ const { scrollYProgress } = useScroll()
+ const scaleX = useSpring(scrollYProgress, { stiffness: 120, damping: 30 })
+
+ return (
+
+ )
+ }
+ - title: Section reveal on enter
+ description: Fade and lift content as it enters the viewport without wiring the effect to a global progress bar.
+ code: |
+ import { motion } from "motion/react"
+
+ export function RevealSection({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+ - title: Horizontal story strip
+ description: Drive a horizontal motion lane from vertical scroll for long-form storytelling or product tours.
+ code: |
+ import { motion, useScroll, useTransform } from "motion/react"
+ import { useRef } from "react"
+
+ export function HorizontalStoryStrip() {
+ const ref = useRef(null)
+ const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
+ const x = useTransform(scrollYProgress, [0, 1], ["0%", "-75%"])
+
+ return (
+
+ )
+ }
diff --git a/corpus/frontend/motion/index.yaml b/corpus/frontend/motion/index.yaml
new file mode 100644
index 0000000..ff0d4b8
--- /dev/null
+++ b/corpus/frontend/motion/index.yaml
@@ -0,0 +1,7 @@
+namespace: frontend.motion
+apis:
+ motion:
+ file: motion.yaml
+examples:
+ scroll:
+ file: examples/scroll.yaml
diff --git a/corpus/frontend/motion/motion.yaml b/corpus/frontend/motion/motion.yaml
new file mode 100644
index 0000000..d05f122
--- /dev/null
+++ b/corpus/frontend/motion/motion.yaml
@@ -0,0 +1,66 @@
+name: motion
+kind: component
+description: >-
+ The core building block. Every HTML and SVG element has a motion counterpart
+ (motion.div, motion.span, motion.circle, etc.). Accepts all standard props
+ plus animation-specific props.
+importPath: import { motion } from "motion/react"
+usage: |
+ import { motion } from "motion/react"
+
+ export function HeroCard() {
+ return (
+
+ Motion content
+
+ )
+ }
+props:
+ - name: initial
+ type: Target | VariantLabels | false
+ description: Initial visual state on mount. Set false to disable enter animation.
+ - name: animate
+ type: Target | VariantLabels
+ description: Target animation values on enter and update.
+ - name: exit
+ type: Target | VariantLabels
+ description: Target animation when removed (requires AnimatePresence parent).
+ - name: transition
+ type: Transition
+ description: Default transition config for this component.
+ - name: variants
+ type: Variants
+ description: "Named animation states. Object of { [name]: target }."
+ - name: style
+ type: MotionStyle
+ description: Extended style prop supporting motion values and independent transforms.
+examples:
+ - title: Basic fade in
+ category: animation
+ code: |
+ import { motion } from "motion/react"
+
+ export function FadeInCard() {
+ return (
+
+ Hello
+
+ )
+ }
+tips:
+ - Use motion/react-client for React Server Components (Next.js app dir).
+ - motion.create(Component) wraps custom components. In React 18 the component must use forwardRef. In React 19 ref is passed via props automatically.
+ - Independent transforms: x, y, z, scale, scaleX, scaleY, rotate, rotateX, rotateY, rotateZ, skewX, skewY, transformPerspective.
+relatedApis:
+ - AnimatePresence
+ - useAnimate
+ - useMotionValue
+ - MotionConfig
diff --git a/corpus/frontend/reactflow/index.yaml b/corpus/frontend/reactflow/index.yaml
new file mode 100644
index 0000000..b005965
--- /dev/null
+++ b/corpus/frontend/reactflow/index.yaml
@@ -0,0 +1,4 @@
+namespace: frontend.reactflow
+apis:
+ reactflow:
+ file: reactflow.yaml
diff --git a/corpus/frontend/reactflow/reactflow.yaml b/corpus/frontend/reactflow/reactflow.yaml
new file mode 100644
index 0000000..d2e1fb3
--- /dev/null
+++ b/corpus/frontend/reactflow/reactflow.yaml
@@ -0,0 +1,84 @@
+name: ReactFlow
+kind: component
+description: >-
+ Core canvas component for rendering nodes and edges, wiring connection
+ interactions, and managing controlled or uncontrolled flow state.
+importPath: "import { ReactFlow } from '@xyflow/react'"
+props:
+ - name: nodes
+ type: Node[]
+ description: Array of nodes for controlled flows.
+ default: "[]"
+ - name: edges
+ type: Edge[]
+ description: Array of edges for controlled flows.
+ default: "[]"
+ - name: defaultNodes
+ type: Node[]
+ description: Initial nodes for uncontrolled flows.
+ - name: defaultEdges
+ type: Edge[]
+ description: Initial edges for uncontrolled flows.
+ - name: onConnect
+ type: OnConnect
+ description: Fires when a new connection is completed.
+ - name: onNodesChange
+ type: OnNodesChange
+ description: Called for drag, select, and position changes in controlled flows.
+ - name: onEdgesChange
+ type: OnEdgesChange
+ description: Called when edges change in controlled flows.
+ - name: nodeTypes
+ type: NodeTypes
+ description: Custom node components keyed by node type.
+ - name: edgeTypes
+ type: EdgeTypes
+ description: Custom edge components keyed by edge type.
+ - name: isValidConnection
+ type: IsValidConnection
+ description: Validate candidate connections before an edge is created.
+ - name: fitView
+ type: boolean
+ description: Fit the viewport to all nodes on initial render.
+ default: "false"
+ - name: defaultViewport
+ type: Viewport
+ description: Initial viewport position and zoom.
+ default: "{ x: 0, y: 0, zoom: 1 }"
+usage: |
+ import { ReactFlow } from '@xyflow/react';
+
+ export function FlowCanvas() {
+ return (
+
+
+
+ );
+ }
+examples:
+ - title: Controlled flow
+ category: state-management
+ code: |
+ import { ReactFlow } from '@xyflow/react';
+
+ export function ControlledFlow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
+ return (
+
+ );
+ }
+tips:
+ - Import '@xyflow/react/dist/style.css' once at the app root.
+ - The parent container must have an explicit width and height.
+ - Define nodeTypes and edgeTypes outside the component or memoize them.
+relatedApis:
+ - Background
+ - Controls
+ - MiniMap
+ - useReactFlow
diff --git a/corpus/index.yaml b/corpus/index.yaml
new file mode 100644
index 0000000..888611c
--- /dev/null
+++ b/corpus/index.yaml
@@ -0,0 +1,11 @@
+namespaces:
+ frontend.motion:
+ index: frontend/motion/index.yaml
+ frontend.reactflow:
+ index: frontend/reactflow/index.yaml
+ frontend.lenis:
+ index: frontend/lenis/index.yaml
+ frontend.designer:
+ index: frontend/designer/index.yaml
+ frontend.design-tokens:
+ index: frontend/design-tokens/index.yaml
diff --git a/src/plugins/design-tokens/tools/get-procedure.ts b/src/plugins/design-tokens/tools/get-procedure.ts
index e24b745..69468eb 100644
--- a/src/plugins/design-tokens/tools/get-procedure.ts
+++ b/src/plugins/design-tokens/tools/get-procedure.ts
@@ -1,7 +1,150 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { TOKEN_PROCEDURES, getProcedureByStep } from "../data.js";
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.design-tokens"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ procedures?: Record;
+};
+
+type CorpusProcedureEntry = {
+ step?: number;
+ title?: string;
+ description?: string;
+ code?: string;
+ rules?: string[];
+ gotchas?: string[];
+};
+
+type LoadedCorpusProcedure = {
+ step: number;
+ title: string;
+ description: string;
+ code: string;
+ rules: string[];
+ gotchas: string[];
+ source: string;
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.design-tokens";
+
+let cachedCorpusProcedures: Map | null | undefined;
+
+function loadCorpusProcedures(): Map | null {
+ if (cachedCorpusProcedures !== undefined) {
+ return cachedCorpusProcedures;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ cachedCorpusProcedures = null;
+ return cachedCorpusProcedures;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const procedureEntries = namespaceIndex?.procedures ?? {};
+ const loaded = new Map();
+
+ for (const [key, meta] of Object.entries(procedureEntries)) {
+ const match = key.match(/^step-(\d+)$/i);
+ const step = match ? Number(match[1]) : Number.NaN;
+ const file = meta?.file;
+ if (!Number.isInteger(step) || !file) {
+ continue;
+ }
+
+ const raw = readFileSync(join(corpusRoot, "frontend/design-tokens", file), "utf8");
+ const entry = YAML.parse(raw) as CorpusProcedureEntry | null;
+ if (
+ !entry ||
+ entry.step !== step ||
+ !entry.title ||
+ !entry.description ||
+ !entry.code ||
+ !Array.isArray(entry.rules) ||
+ !Array.isArray(entry.gotchas)
+ ) {
+ continue;
+ }
+
+ loaded.set(step, {
+ step,
+ title: entry.title,
+ description: entry.description,
+ code: entry.code,
+ rules: entry.rules,
+ gotchas: entry.gotchas,
+ source: corpusNamespace,
+ });
+ }
+
+ cachedCorpusProcedures = loaded.size > 0 ? loaded : null;
+ return cachedCorpusProcedures;
+ } catch {
+ cachedCorpusProcedures = null;
+ return cachedCorpusProcedures;
+ }
+}
+
+function getProcedure(step: number) {
+ const corpusEntry = loadCorpusProcedures()?.get(step);
+ if (corpusEntry) {
+ return corpusEntry;
+ }
+
+ return getProcedureByStep(step);
+}
+
+function renderProcedure(proc: {
+ step: number;
+ title: string;
+ description: string;
+ code?: string;
+ rules?: string[];
+ gotchas?: string[];
+ source?: string;
+}): string {
+ let text = `# Step ${proc.step}: ${proc.title}\n\n${proc.description}\n\n`;
+
+ if (proc.code) {
+ text += `## Code\n\`\`\`css\n${proc.code}\n\`\`\`\n\n`;
+ }
+
+ if (proc.rules && proc.rules.length > 0) {
+ text += `## Rules\n`;
+ for (const rule of proc.rules) text += `- ${rule}\n`;
+ }
+
+ if (proc.gotchas && proc.gotchas.length > 0) {
+ text += `\n## Gotchas\n`;
+ for (const gotcha of proc.gotchas) text += `- ${gotcha}\n`;
+ }
+
+ if (proc.source) {
+ text += `\n**Corpus Source:** ${proc.source}`;
+ }
+
+ return text;
+}
+
export function register(server: McpServer): void {
server.tool(
"design_tokens_get_procedure",
@@ -12,26 +155,24 @@ export function register(server: McpServer): void {
},
async ({ step }) => {
if (step !== undefined) {
- const proc = getProcedureByStep(step);
+ const proc = getProcedure(step);
if (!proc) {
return {
content: [{ type: "text", text: `Step ${step} not found. Valid steps: 1-${TOKEN_PROCEDURES.length}` }],
isError: true,
};
}
- let text = `# Step ${proc.step}: ${proc.title}\n\n${proc.description}\n\n`;
- text += `## Code\n\`\`\`css\n${proc.code}\n\`\`\`\n\n`;
- text += `## Rules\n`;
- for (const rule of proc.rules) text += `- ${rule}\n`;
- text += `\n## Gotchas\n`;
- for (const gotcha of proc.gotchas) text += `- ${gotcha}\n`;
- return { content: [{ type: "text", text }] };
+ return { content: [{ type: "text", text: renderProcedure(proc) }] };
}
let text = "# Design Token Build Procedure\n\n";
text += "A complete token system = 10 categories. Follow in order.\n\n";
for (const proc of TOKEN_PROCEDURES) {
- text += `## Step ${proc.step}: ${proc.title}\n${proc.description}\n\n`;
+ const corpusProc = loadCorpusProcedures()?.get(proc.step);
+ text += `## Step ${proc.step}: ${corpusProc?.title ?? proc.title}\n${corpusProc?.description ?? proc.description}\n\n`;
+ }
+ if (loadCorpusProcedures()) {
+ text += `**Corpus Source:** ${corpusNamespace}`;
}
text += "\nCall `design_tokens_get_procedure` with a step number for full code + rules.";
return { content: [{ type: "text", text }] };
diff --git a/src/plugins/designer/tools/get-page-template.ts b/src/plugins/designer/tools/get-page-template.ts
index d83ca9f..bb6b9a0 100644
--- a/src/plugins/designer/tools/get-page-template.ts
+++ b/src/plugins/designer/tools/get-page-template.ts
@@ -1,7 +1,116 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { PAGE_TYPES, getPageTemplate } from "../data.js";
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.designer"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ pageTemplates?: {
+ dashboard?: {
+ file?: string;
+ };
+ };
+};
+
+type CorpusPageSection = {
+ name?: string;
+ required?: boolean;
+ components?: string[];
+ notes?: string;
+};
+
+type CorpusPageTemplate = {
+ type?: string;
+ description?: string;
+ layout?: string;
+ sections?: CorpusPageSection[];
+ cognitiveApply?: string[];
+ compositionApply?: string[];
+ interactionApply?: string[];
+ writingApply?: string[];
+ keyRules?: string[];
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.designer";
+
+let cachedCorpusDashboard: { template: CorpusPageTemplate; source: string } | null | undefined;
+
+function loadCorpusDashboardTemplate(): { template: CorpusPageTemplate; source: string } | null {
+ if (cachedCorpusDashboard !== undefined) {
+ return cachedCorpusDashboard;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ cachedCorpusDashboard = null;
+ return null;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const pageTemplatePath = namespaceIndex?.pageTemplates?.dashboard?.file;
+ if (!pageTemplatePath) {
+ cachedCorpusDashboard = null;
+ return null;
+ }
+
+ const templateRaw = readFileSync(join(corpusRoot, "frontend/designer", pageTemplatePath), "utf8");
+ const template = YAML.parse(templateRaw) as CorpusPageTemplate | null;
+ const sections = template?.sections;
+ const cognitiveApply = template?.cognitiveApply;
+ const compositionApply = template?.compositionApply;
+ const interactionApply = template?.interactionApply;
+ const writingApply = template?.writingApply;
+ const keyRules = template?.keyRules;
+ if (
+ !template ||
+ template.type?.toLowerCase() !== "dashboard" ||
+ !template.description ||
+ !template.layout ||
+ !Array.isArray(sections) ||
+ sections.length === 0 ||
+ !Array.isArray(cognitiveApply) ||
+ !Array.isArray(compositionApply) ||
+ !Array.isArray(interactionApply) ||
+ !Array.isArray(writingApply) ||
+ !Array.isArray(keyRules)
+ ) {
+ cachedCorpusDashboard = null;
+ return null;
+ }
+
+ cachedCorpusDashboard = { template, source: corpusNamespace };
+ return cachedCorpusDashboard;
+ } catch {
+ cachedCorpusDashboard = null;
+ return null;
+ }
+}
+
+function getCorpusAwarePageTemplate(type: (typeof PAGE_TYPES)[number]) {
+ if (type === "dashboard") {
+ return loadCorpusDashboardTemplate()?.template ?? getPageTemplate(type);
+ }
+
+ return getPageTemplate(type);
+}
+
export function register(server: McpServer): void {
server.tool(
"designer_get_page_template",
@@ -10,7 +119,8 @@ export function register(server: McpServer): void {
type: z.enum(PAGE_TYPES).describe("Page type to get template for"),
},
async ({ type }) => {
- const template = getPageTemplate(type);
+ const corpusEntry = type === "dashboard" ? loadCorpusDashboardTemplate() : null;
+ const template = getCorpusAwarePageTemplate(type);
if (!template) {
return {
content: [{ type: "text" as const, text: `Page type "${type}" not found. Available: ${PAGE_TYPES.join(", ")}` }],
@@ -23,27 +133,31 @@ export function register(server: McpServer): void {
text += `**Layout:** ${template.layout}\n\n`;
text += `## Sections\n\n`;
- for (const section of template.sections) {
- text += `### ${section.name}${section.required ? "" : " (optional)"}\n\n`;
+ for (const section of template.sections ?? []) {
+ text += `### ${section.name ?? "Untitled Section"}${section.required ? "" : " (optional)"}\n\n`;
text += `**Components:**\n`;
- for (const comp of section.components) {
+ for (const comp of section.components ?? []) {
text += `- ${comp}\n`;
}
- text += `\n**Notes:** ${section.notes}\n\n`;
+ text += `\n**Notes:** ${section.notes ?? ""}\n\n`;
}
text += `## Apply These\n\n`;
- text += `**Cognitive Laws:** ${template.cognitiveApply.join(", ")}\n`;
- text += `**Composition:** ${template.compositionApply.join(", ")}\n`;
- text += `**Interaction Patterns:** ${template.interactionApply.join(", ")}\n`;
- text += `**UX Writing:** ${template.writingApply.join(", ")}\n\n`;
+ text += `**Cognitive Laws:** ${(template.cognitiveApply ?? []).join(", ")}\n`;
+ text += `**Composition:** ${(template.compositionApply ?? []).join(", ")}\n`;
+ text += `**Interaction Patterns:** ${(template.interactionApply ?? []).join(", ")}\n`;
+ text += `**UX Writing:** ${(template.writingApply ?? []).join(", ")}\n\n`;
text += `## Key Rules\n\n`;
- for (const rule of template.keyRules) {
+ for (const rule of template.keyRules ?? []) {
text += `- ${rule}\n`;
}
+ if (corpusEntry) {
+ text += `\n**Corpus Source:** ${corpusEntry.source}`;
+ }
+
return { content: [{ type: "text" as const, text }] };
- }
+ },
);
}
diff --git a/src/plugins/lenis/tools/get-pattern.ts b/src/plugins/lenis/tools/get-pattern.ts
index 4c706f5..e01dd34 100644
--- a/src/plugins/lenis/tools/get-pattern.ts
+++ b/src/plugins/lenis/tools/get-pattern.ts
@@ -1,9 +1,76 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { PATTERNS } from "../data.js";
const PATTERN_NAMES = Object.keys(PATTERNS) as [string, ...string[]];
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.lenis"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ patterns?: {
+ [name: string]: {
+ file?: string;
+ };
+ };
+};
+
+type CorpusPatternEntry = {
+ name?: string;
+ description?: string;
+ code?: string;
+ tips?: string[];
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.lenis";
+
+let cachedCorpusPattern: { name: string; pattern: CorpusPatternEntry; source: string } | undefined;
+
+function loadCorpusPattern(name: string): { name: string; pattern: CorpusPatternEntry; source: string } | null {
+ if (cachedCorpusPattern?.name === name) {
+ return cachedCorpusPattern;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ return null;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const patternPath = namespaceIndex?.patterns?.[name]?.file;
+ if (!patternPath) {
+ return null;
+ }
+
+ const patternRaw = readFileSync(join(corpusRoot, "frontend/lenis", patternPath), "utf8");
+ const pattern = YAML.parse(patternRaw) as CorpusPatternEntry | null;
+ if (!pattern || pattern.name?.toLowerCase() !== name.toLowerCase() || !pattern.description || !pattern.code) {
+ return null;
+ }
+
+ cachedCorpusPattern = { name, pattern, source: corpusNamespace };
+ return cachedCorpusPattern;
+ } catch {
+ return null;
+ }
+}
+
export function register(server: McpServer): void {
server.tool(
"lenis_get_pattern",
@@ -16,7 +83,8 @@ export function register(server: McpServer): void {
),
},
async ({ name }) => {
- const pattern = PATTERNS[name];
+ const corpusEntry = loadCorpusPattern(name);
+ const pattern = corpusEntry?.pattern ?? PATTERNS[name];
if (!pattern) {
return {
content: [
@@ -40,6 +108,10 @@ export function register(server: McpServer): void {
for (const tip of pattern.tips) text += `- ${tip}\n`;
}
+ if (corpusEntry) {
+ text += `\n**Corpus Source:** ${corpusEntry.source}`;
+ }
+
return { content: [{ type: "text", text }] };
},
);
diff --git a/src/plugins/motion/tools/get-api.ts b/src/plugins/motion/tools/get-api.ts
index a806038..82f0d7f 100644
--- a/src/plugins/motion/tools/get-api.ts
+++ b/src/plugins/motion/tools/get-api.ts
@@ -1,7 +1,98 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data.js";
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.motion"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ apis?: {
+ motion?: {
+ file?: string;
+ };
+ };
+};
+
+type CorpusApiEntry = {
+ name?: string;
+ kind?: string;
+ description?: string;
+ importPath?: string;
+ props?: Array<{ name: string; type: string; description: string; default?: string }>;
+ returns?: string;
+ usage?: string;
+ examples?: Array<{ title: string; code: string; description?: string; category: string }>;
+ tips?: string[];
+ relatedApis?: string[];
+ source?: string;
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.motion";
+
+let cachedCorpusMotionApi: { api: CorpusApiEntry; source: string } | null | undefined;
+
+function loadCorpusMotionApi(): { api: CorpusApiEntry; source: string } | null {
+ if (cachedCorpusMotionApi !== undefined) {
+ return cachedCorpusMotionApi;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ cachedCorpusMotionApi = null;
+ return cachedCorpusMotionApi;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const apiPath = namespaceIndex?.apis?.motion?.file;
+ if (!apiPath) {
+ cachedCorpusMotionApi = null;
+ return cachedCorpusMotionApi;
+ }
+
+ const apiRaw = readFileSync(join(corpusRoot, "frontend/motion", apiPath), "utf8");
+ const api = YAML.parse(apiRaw) as CorpusApiEntry | null;
+ if (!api || api.name?.toLowerCase() !== "motion" || !api.usage || !api.importPath || !api.description || !api.kind) {
+ cachedCorpusMotionApi = null;
+ return cachedCorpusMotionApi;
+ }
+
+ cachedCorpusMotionApi = { api, source: corpusNamespace };
+ return cachedCorpusMotionApi;
+ } catch {
+ cachedCorpusMotionApi = null;
+ return cachedCorpusMotionApi;
+ }
+}
+
+function getMotionApi(name: string) {
+ if (name.toLowerCase() !== "motion") {
+ return getApiByName(name);
+ }
+
+ const corpusEntry = loadCorpusMotionApi();
+ if (corpusEntry) {
+ return corpusEntry.api as Parameters[0];
+ }
+
+ return getApiByName(name);
+}
+
export function register(server: McpServer): void {
server.tool(
"motion_get_api",
@@ -10,7 +101,8 @@ export function register(server: McpServer): void {
name: z.string().describe("API name (e.g., 'motion', 'AnimatePresence', 'useAnimate', 'useScroll', 'stagger', 'Reorder.Group')"),
},
async ({ name }) => {
- const api = getApiByName(name);
+ const corpusEntry = name.toLowerCase() === "motion" ? loadCorpusMotionApi() : null;
+ const api = getMotionApi(name);
if (!api) {
const suggestions = searchApis(name).map((r) => r.api.name);
return {
@@ -18,7 +110,15 @@ export function register(server: McpServer): void {
isError: true,
};
}
- return { content: [{ type: "text", text: formatApiReference(api) }] };
+ const rendered = formatApiReference(api);
+ return {
+ content: [
+ {
+ type: "text",
+ text: corpusEntry ? `${rendered}\n**Corpus Source:** ${corpusEntry.source}` : rendered,
+ },
+ ],
+ };
},
);
}
diff --git a/src/plugins/motion/tools/get-examples.ts b/src/plugins/motion/tools/get-examples.ts
index 3846138..34fe8c5 100644
--- a/src/plugins/motion/tools/get-examples.ts
+++ b/src/plugins/motion/tools/get-examples.ts
@@ -1,6 +1,97 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { CATEGORIES, getExamplesByCategory, formatExample, capitalize } from "../data.js";
+import type { Example } from "../data.js";
+
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.motion"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ examples?: Record;
+};
+
+type CorpusExampleFile = {
+ category?: string;
+ examples?: Array<{
+ title?: string;
+ description?: string;
+ code?: string;
+ category?: string;
+ }>;
+};
+
+type ParsedCorpusExample = {
+ title: string;
+ description?: string;
+ code: string;
+ category?: string;
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.motion";
+
+let cachedCorpusExamples: Map | undefined;
+
+function loadCorpusExamples(category: string): { examples: Example[]; source: string } | null {
+ const key = category.toLowerCase().trim();
+ cachedCorpusExamples ??= new Map();
+ const cached = cachedCorpusExamples.get(key);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ cachedCorpusExamples.set(key, null);
+ return null;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const examplePath = namespaceIndex?.examples?.[key]?.file;
+ if (!examplePath) {
+ cachedCorpusExamples.set(key, null);
+ return null;
+ }
+
+ const fileRaw = readFileSync(join(corpusRoot, "frontend/motion", examplePath), "utf8");
+ const file = YAML.parse(fileRaw) as CorpusExampleFile | null;
+ const examples = (file?.examples ?? [])
+ .filter((ex): ex is ParsedCorpusExample => Boolean(ex?.title && ex?.code))
+ .map((ex) => ({
+ title: ex.title,
+ code: ex.code,
+ description: ex.description,
+ category: (ex.category ?? key) as Example["category"],
+ }));
+
+ if (examples.length === 0) {
+ cachedCorpusExamples.set(key, null);
+ return null;
+ }
+
+ const result = { examples, source: corpusNamespace };
+ cachedCorpusExamples.set(key, result);
+ return result;
+ } catch {
+ cachedCorpusExamples.set(key, null);
+ return null;
+ }
+}
export function register(server: McpServer): void {
server.tool(
@@ -8,12 +99,16 @@ export function register(server: McpServer): void {
"Get code examples for a specific animation category",
{ category: z.string().describe(`Category: ${CATEGORIES.join(", ")}`) },
async ({ category }) => {
- const examples = getExamplesByCategory(category);
+ const corpusEntry = loadCorpusExamples(category);
+ const examples = corpusEntry?.examples ?? getExamplesByCategory(category);
if (examples.length === 0) {
return { content: [{ type: "text", text: `No examples for category "${category}". Available: ${CATEGORIES.join(", ")}` }] };
}
let text = `# ${capitalize(category)} Examples\n\n`;
for (const ex of examples) text += formatExample(ex, 2);
+ if (corpusEntry) {
+ text += `**Corpus Source:** ${corpusEntry.source}\n`;
+ }
return { content: [{ type: "text", text }] };
},
);
diff --git a/src/plugins/reactflow/tools/get-api.ts b/src/plugins/reactflow/tools/get-api.ts
index ff3d6bd..0bc15ec 100644
--- a/src/plugins/reactflow/tools/get-api.ts
+++ b/src/plugins/reactflow/tools/get-api.ts
@@ -1,7 +1,133 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import YAML from "yaml";
import { z } from "zod";
import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data/index.js";
+type CorpusIndex = {
+ namespaces?: {
+ "frontend.reactflow"?: {
+ index?: string;
+ };
+ };
+};
+
+type CorpusNamespaceIndex = {
+ namespace?: string;
+ apis?: {
+ reactflow?: {
+ file?: string;
+ };
+ };
+};
+
+type CorpusApiEntry = {
+ name?: string;
+ kind?: string;
+ description?: string;
+ importPath?: string;
+ props?: Array<{ name: string; type: string; description: string; default?: string }>;
+ returns?: string;
+ usage?: string;
+ examples?: Array<{ title?: string; code?: string; description?: string; category?: string }>;
+ tips?: string[];
+ relatedApis?: string[];
+};
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const corpusRoot = join(moduleDir, "../../../../corpus");
+const corpusNamespace = "frontend.reactflow";
+const corpusApiName = "ReactFlow";
+type ReactFlowExample = Parameters[0]["examples"][number];
+
+let cachedCorpusReactFlowApi: { api: Parameters[0]; source: string } | null | undefined;
+
+function normalizeApiName(name: string): string {
+ return name.toLowerCase().replace(/[<>/()]/g, "").trim();
+}
+
+function loadCorpusReactFlowApi(): { api: Parameters[0]; source: string } | null {
+ if (cachedCorpusReactFlowApi !== undefined) {
+ return cachedCorpusReactFlowApi;
+ }
+
+ try {
+ const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
+ const index = YAML.parse(indexRaw) as CorpusIndex | null;
+ const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
+ if (!namespaceIndexPath) {
+ cachedCorpusReactFlowApi = null;
+ return cachedCorpusReactFlowApi;
+ }
+
+ const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
+ const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
+ const apiPath = namespaceIndex?.apis?.reactflow?.file;
+ if (!apiPath) {
+ cachedCorpusReactFlowApi = null;
+ return cachedCorpusReactFlowApi;
+ }
+
+ const apiRaw = readFileSync(join(corpusRoot, "frontend/reactflow", apiPath), "utf8");
+ const api = YAML.parse(apiRaw) as CorpusApiEntry | null;
+ if (
+ !api ||
+ normalizeApiName(api.name ?? "") !== normalizeApiName(corpusApiName) ||
+ !api.usage ||
+ !api.importPath ||
+ !api.description ||
+ !api.kind
+ ) {
+ cachedCorpusReactFlowApi = null;
+ return cachedCorpusReactFlowApi;
+ }
+
+ cachedCorpusReactFlowApi = {
+ api: {
+ name: api.name ?? corpusApiName,
+ kind: api.kind as Parameters[0]["kind"],
+ description: api.description,
+ importPath: api.importPath,
+ props: api.props,
+ returns: api.returns,
+ usage: api.usage,
+ examples: (api.examples ?? [])
+ .filter((ex): ex is { title: string; code: string; description?: string; category?: ReactFlowExample["category"] } =>
+ Boolean(ex?.title && ex?.code),
+ )
+ .map((ex) => ({
+ title: ex.title,
+ code: ex.code,
+ description: ex.description,
+ category: ex.category ?? "quickstart",
+ })),
+ tips: api.tips ?? [],
+ relatedApis: api.relatedApis ?? [],
+ },
+ source: corpusNamespace,
+ };
+ return cachedCorpusReactFlowApi ?? null;
+ } catch {
+ cachedCorpusReactFlowApi = null;
+ return null;
+ }
+}
+
+function getReactFlowApi(name: string) {
+ if (normalizeApiName(name) !== normalizeApiName(corpusApiName)) {
+ return getApiByName(name, ALL_APIS);
+ }
+
+ const corpusEntry = loadCorpusReactFlowApi();
+ if (corpusEntry) {
+ return corpusEntry.api;
+ }
+
+ return getApiByName(name, ALL_APIS);
+}
+
export function register(server: McpServer): void {
server.tool(
"reactflow_get_api",
@@ -14,7 +140,8 @@ export function register(server: McpServer): void {
),
},
async ({ name }) => {
- const api = getApiByName(name, ALL_APIS);
+ const corpusEntry = normalizeApiName(name) === normalizeApiName(corpusApiName) ? loadCorpusReactFlowApi() : null;
+ const api = getReactFlowApi(name);
if (!api) {
const suggestions = searchApis(name, ALL_APIS)
.slice(0, 5)
@@ -29,7 +156,15 @@ export function register(server: McpServer): void {
isError: true,
};
}
- return { content: [{ type: "text", text: formatApiReference(api) }] };
+ const rendered = formatApiReference(api);
+ return {
+ content: [
+ {
+ type: "text",
+ text: corpusEntry ? `${rendered}\n**Corpus Source:** ${corpusEntry.source}` : rendered,
+ },
+ ],
+ };
},
);
}
diff --git a/tests/design-tokens-procedure-corpus-backed-tools-behaviour.test.ts b/tests/design-tokens-procedure-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..82ff33e
--- /dev/null
+++ b/tests/design-tokens-procedure-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,86 @@
+import { expect, test } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerDesignTokensGetProcedure } from "../src/plugins/design-tokens/tools/get-procedure.ts";
+
+const designTokensGetProcedure = captureTool(registerDesignTokensGetProcedure);
+
+test("design_tokens_get_procedure prefers corpus metadata for step 1", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 1 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 1: Define color ramp primitives in @theme");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--color-brand-50: oklch(0.98 0.01 291);");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 2", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 2 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 2: Map semantic tokens in :root and .dark");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--color-bg: var(--color-neutral-50);");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 3", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 3 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 3: Bridge to Tailwind v4 utilities with @theme inline");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--color-background: var(--color-bg);");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 4", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 4 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 4: Define spacing system");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--spacing: 0.25rem;");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 5", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 5 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 5: Set up typography scale");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--font-sans:");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 6", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 6 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 6: Define shadows and elevation");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--shadow-xs: 0 1px 2px 0 oklch(0.30 0.02 60 / 0.08);");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 7", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 7 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 7: Define motion and z-index tokens");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--duration-fast: 100ms;");
+});
+
+test("design_tokens_get_procedure prefers corpus metadata for step 8", async () => {
+ const result = await designTokensGetProcedure.invoke({ step: 8 });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Step 8: Run contrast audit and verify token usage");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("--color-text on --color-bg");
+});
+
+test("design_tokens_get_procedure summary includes corpus source when corpus metadata is present", async () => {
+ const result = await designTokensGetProcedure.invoke({});
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Design Token Build Procedure");
+ expect(text).toContain("**Corpus Source:** frontend.design-tokens");
+ expect(text).toContain("## Step 1: Define color ramp primitives in @theme");
+});
diff --git a/tests/designer-page-template-corpus-backed-tools-behaviour.test.ts b/tests/designer-page-template-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..3524c32
--- /dev/null
+++ b/tests/designer-page-template-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,25 @@
+import { expect, test } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerDesignerPageTemplate } from "../src/plugins/designer/tools/get-page-template.ts";
+
+const designerGetPageTemplate = captureTool(registerDesignerPageTemplate);
+
+test("designer_get_page_template prefers corpus metadata for dashboard", async () => {
+ const result = await designerGetPageTemplate.invoke({ type: "dashboard" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Page Template: dashboard");
+ expect(text).toContain("**Corpus Source:** frontend.designer");
+ expect(text).toContain("## Sections");
+ expect(text).toContain("Sidebar Navigation");
+});
+
+test("designer_get_page_template falls back to in-file data for non-corpus templates", async () => {
+ const result = await designerGetPageTemplate.invoke({ type: "auth" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Page Template: auth");
+ expect(text).toContain("## Sections");
+ expect(text).toContain("Login");
+ expect(text).not.toContain("**Corpus Source:** frontend.designer");
+});
diff --git a/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..a039f55
--- /dev/null
+++ b/tests/lenis-pattern-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerLenisGetPattern } from "../src/plugins/lenis/tools/get-pattern.ts";
+
+const lenisGetPattern = captureTool(registerLenisGetPattern);
+
+test("lenis_get_pattern prefers corpus metadata for gsap-integration", async () => {
+ const result = await lenisGetPattern.invoke({ name: "gsap-integration" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: gsap-integration");
+ expect(text).toContain("**Corpus Source:** frontend.lenis");
+ expect(text).toContain('import { ReactLenis, useLenis } from "lenis/react"');
+});
+
+test("lenis_get_pattern falls back to in-file data for non-corpus patterns", async () => {
+ const result = await lenisGetPattern.invoke({ name: "accessibility" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Lenis Pattern: accessibility");
+ expect(text).toContain("## Key Notes");
+ expect(text).not.toContain("**Corpus Source:** frontend.lenis");
+});
diff --git a/tests/motion-corpus-backed-tools-behaviour.test.ts b/tests/motion-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..a48baec
--- /dev/null
+++ b/tests/motion-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,23 @@
+import { test, expect } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerMotionApi } from "../src/plugins/motion/tools/get-api.ts";
+
+const motionGetApi = captureTool(registerMotionApi);
+
+test("motion_get_api prefers corpus metadata for motion", async () => {
+ const result = await motionGetApi.invoke({ name: "motion" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# motion");
+ expect(text).toContain("**Corpus Source:** frontend.motion");
+ expect(text).toContain('import { motion } from "motion/react"');
+});
+
+test("motion_get_api falls back to in-file data for non-corpus motion APIs", async () => {
+ const result = await motionGetApi.invoke({ name: "AnimatePresence" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# AnimatePresence");
+ expect(text).toContain("**Kind:** component");
+ expect(text).not.toContain("**Corpus Source:** frontend.motion");
+});
diff --git a/tests/motion-examples-corpus-backed-tools-behaviour.test.ts b/tests/motion-examples-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..5f0aa75
--- /dev/null
+++ b/tests/motion-examples-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,24 @@
+import { expect, test } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerMotionExamples } from "../src/plugins/motion/tools/get-examples.ts";
+
+const motionGetExamples = captureTool(registerMotionExamples);
+
+test("motion_get_examples prefers corpus metadata for scroll", async () => {
+ const result = await motionGetExamples.invoke({ category: "scroll" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Scroll Examples");
+ expect(text).toContain("Scroll progress rail");
+ expect(text).toContain("**Corpus Source:** frontend.motion");
+ expect(text).toContain('import { motion, useScroll, useSpring } from "motion/react"');
+});
+
+test("motion_get_examples falls back to in-file data for layout", async () => {
+ const result = await motionGetExamples.invoke({ category: "layout" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Layout Examples");
+ expect(text).toContain("Layout animation");
+ expect(text).not.toContain("**Corpus Source:** frontend.motion");
+});
diff --git a/tests/reactflow-corpus-backed-tools-behaviour.test.ts b/tests/reactflow-corpus-backed-tools-behaviour.test.ts
new file mode 100644
index 0000000..491d417
--- /dev/null
+++ b/tests/reactflow-corpus-backed-tools-behaviour.test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from "bun:test";
+import { captureTool, extractTextContent } from "./helpers";
+import { register as registerReactFlowApi } from "../src/plugins/reactflow/tools/get-api.ts";
+
+const reactFlowGetApi = captureTool(registerReactFlowApi);
+
+test("reactflow_get_api prefers corpus metadata for ReactFlow", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "ReactFlow" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# ReactFlow (component)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
+ expect(text).toContain("import { ReactFlow } from '@xyflow/react'");
+});
+
+test("reactflow_get_api falls back to in-file data for non-corpus APIs", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "Handle" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Handle (component)");
+ expect(text).toContain("import { Handle, Position } from '@xyflow/react'");
+ expect(text).not.toContain("**Corpus Source:** frontend.reactflow");
+});