diff --git a/CHUNK_EXAMPLES_PLAN.md b/CHUNK_EXAMPLES_PLAN.md new file mode 100644 index 0000000..5e5e096 --- /dev/null +++ b/CHUNK_EXAMPLES_PLAN.md @@ -0,0 +1,51 @@ +# Chunk Examples Plan + +Following the Effect.succeed → Effect.all progression pattern, we should demonstrate Chunk basics before Stream examples. + +## Proposed Chunk Examples (in order) + +### 1. Chunk.make - Create a chunk +Simple construction from values +```ts +const chunk = Chunk.make(1, 2, 3) +``` + +### 2. Chunk.append - Add element +```ts +const chunk = Chunk.make(1, 2, 3) +const result = Chunk.append(chunk, 4) +``` + +### 3. Chunk.concat - Combine chunks +```ts +const chunk1 = Chunk.make(1, 2) +const chunk2 = Chunk.make(3, 4) +const result = Chunk.concat(chunk1, chunk2) +``` + +### 4. Chunk.map - Transform elements +```ts +const chunk = Chunk.make(1, 2, 3) +const result = Chunk.map(chunk, x => x * 2) +``` + +### 5. Chunk.filter - Select elements +```ts +const chunk = Chunk.make(1, 2, 3, 4, 5) +const result = Chunk.filter(chunk, x => x % 2 === 0) +``` + +### 6. Chunk.take/drop - Slice operations +```ts +const chunk = Chunk.make(1, 2, 3, 4, 5) +const taken = Chunk.take(chunk, 3) +const dropped = Chunk.drop(chunk, 2) +``` + +This mirrors the Effect progression: +- Effect.succeed (simple value) → Chunk.make (simple collection) +- Effect.map → Chunk.map +- Effect.filter → Chunk.filter +- etc. + +Then after Chunks are understood, we introduce Streams of Chunks. diff --git a/RUNNING_EFFECT_PATTERNS.md b/RUNNING_EFFECT_PATTERNS.md new file mode 100644 index 0000000..08c9277 --- /dev/null +++ b/RUNNING_EFFECT_PATTERNS.md @@ -0,0 +1,275 @@ +# Running Effect Visual Patterns + +This document describes the two distinct visual patterns that occur when an EffectNode is in the "running" state. + +## Pattern 1: Border Pulse + Glow Pulse + +**Location:** `src/components/effect/useEffectMotion.ts:154-166` + +**What it does:** Simultaneously animates border opacity and glow intensity in a pulsing pattern. + +### Visual Characteristics + +- **Border Opacity:** Pulses from 1 → 0.3 → 1 +- **Glow Intensity:** Pulses from 1 → 5 → 1 +- **Duration:** + - Border: 1.5s per cycle + - Glow: 0.5s per cycle +- **Easing:** `easeInOut` for both +- **Repeat:** Infinite loops +- **Colors:** + - Border: `rgba(100, 200, 255, 0.8)` (bright blue) + - Glow: `rgba(100, 200, 255, 0.2)` (softer blue) + +### Implementation Details + +```typescript +// Border pulse animation +animate(motionValues.borderOpacity, [1, 0.3, 1], { + duration: 1.5, + ease: "easeInOut", + repeat: Infinity, +}) + +// Glow pulse animation +animate(motionValues.glowIntensity, [1, 5, 1], { + duration: 0.5, + ease: "easeInOut", + repeat: Infinity, +}) +``` + +### Where it renders: + +1. **Border:** `EffectOverlay.tsx:48-60` - Inset box-shadow overlay +2. **Glow:** `EffectContainer.tsx:70-83` - Box-shadow on the container + +The glow uses `boxShadow: 0 0 ${cappedGlow}px rgba(100, 200, 255, 0.2)` where `cappedGlow` is capped at 8px max. + +--- + +## Pattern 2: Continuous Jitter (Rotation + Position) + +**Location:** `src/components/effect/useEffectMotion.ts:168-199` + +**What it does:** Randomly jitters the node's rotation and position in an endless loop. + +### Visual Characteristics + +- **Rotation:** Random angles between ±0.5° to ±4.5° +- **X Offset:** Random between ±0.5px to ±2px +- **Y Offset:** Random between ±0.1px to ±0.7px +- **Duration per jitter:** 0.1s to 0.2s (randomized) +- **Easing:** + - Rotation: `circInOut` + - Position: `easeInOut` +- **Pattern:** Infinite recursive loop via requestAnimationFrame + +### Implementation Details + +```typescript +// Random values computed each cycle +const angle = (Math.random() * 4 + 0.5) * (Math.random() < 0.5 ? 1 : -1) +const offsetX = (Math.random() * 1.5 + 0.5) * (Math.random() < 0.5 ? -1 : 1) +const offsetY = (Math.random() * 0.6 + 0.1) * (Math.random() < 0.5 ? -1 : 1) +const duration = 0.1 + Math.random() * 0.1 // 0.1 to 0.2 seconds + +// All three animate simultaneously +animate(motionValues.rotation, angle, { duration, ease: "circInOut" }) +animate(motionValues.shakeX, offsetX, { duration, ease: "easeInOut" }) +animate(motionValues.shakeY, offsetY, { duration, ease: "easeInOut" }) + +// When all three finish, schedule next jitter +Promise.all([rot.finished, x.finished, y.finished]).then(() => { + rafId = requestAnimationFrame(jitter) +}) +``` + +### Where it renders: + +`EffectContainer.tsx:51-53` - Applied as transform values on the main container: +- `rotate: motionValues.rotation` +- `x: motionValues.shakeX` +- `y: motionValues.shakeY` + +### Side Effect: Motion Blur + +The rotation velocity is tracked and converted to blur: + +- **Blur calculation:** `EffectContainer.tsx:61-68` +- Rotation velocity mapped from [-100, 0, 100] → [1px, 0px, 1px] blur +- Capped at 2px maximum +- Applied via CSS filter on the container + +--- + +## Pattern 3: Sweeping Light Overlay + +**Location:** `EffectOverlay.tsx:13-43` + +**What it does:** Multiple animated light sweeps move horizontally across the node. + +### Visual Characteristics + +- **Count:** 6 simultaneous sweeps with staggered delays +- **Delays:** 0s, 0.2s, 0.4s, 0.6s, 0.8s, 1.0s +- **Width:** 200% of container (allows off-screen starting position) +- **Animation:** Moves from `-66%` to `50%` horizontally +- **Duration:** 0.8s per sweep +- **Easing:** Custom cubic-bezier `[0.5, 0, 0.1, 1]` +- **Gradient:** Linear gradient with white center spike + - `transparent 0%` → `transparent 40%` → `rgba(255,255,255,0.1) 45%` → `rgba(255,255,255,0.5) 50%` → `rgba(255,255,255,0.1) 55%` → `transparent 60%` → `transparent 100%` +- **Effects:** + - `filter: blur(4px)` - Softens the light + - `mixBlendMode: lighten` - Blends naturally with background +- **Repeat:** Infinite + +### Implementation Details + +```typescript +{[0, 0.2, 0.4, 0.6, 0.8, 1].map((delay, i) => ( + +))} +``` + +### Where it renders: + +`EffectOverlay.tsx:62-63` - Rendered when `isRunning === true`, inside the node container after the border overlay. + +--- + +## How the Patterns Work Together + +### Timing Relationship + +1. **Border Pulse (1.5s cycle)** - Slow, steady breathing +2. **Glow Pulse (0.5s cycle)** - Fast heartbeat, 3x faster than border +3. **Jitter (0.1-0.2s per move)** - Erratic, nervous energy +4. **Light Sweeps (0.8s per sweep, 6 staggered)** - Constant flow of activity + +The different speeds create a complex, organic "working" feeling: +- Border provides slow rhythm +- Glow adds urgency +- Jitter gives instability/activity +- Sweeps show progress/motion + +### Layering (z-order from back to front) + +1. Base container with node background +2. Light sweeps overlay (inside container) +3. Border pulse overlay (inset box-shadow) +4. Glow (outer box-shadow on container) +5. Motion blur (filter on container from jitter) + +### Accessibility + +All patterns respect `prefers-reduced-motion`: +- Patterns stop completely when reduced motion is preferred +- Motion values reset to neutral (rotation: 0, shake: 0, opacity: 1, glow: 0) + +--- + +## Key Files Reference + +| Pattern | Primary Implementation | Rendering | +|---------|----------------------|-----------| +| Border Pulse | `useEffectMotion.ts:155-159` | `EffectOverlay.tsx:48-60` | +| Glow Pulse | `useEffectMotion.ts:162-166` | `EffectContainer.tsx:70-83` | +| Jitter | `useEffectMotion.ts:168-199` | `EffectContainer.tsx:51-53` | +| Light Sweeps | N/A (declarative) | `EffectOverlay.tsx:13-43` | +| Motion Blur | `useEffectMotion.ts:85` (derived) | `EffectContainer.tsx:61-68` | + +--- + +## Additional Running-State Changes + +### Shape Changes + +**Height reduction:** `useEffectMotion.ts:227-233` +- Height animates from 64px → 25.6px (64 * 0.4) +- Spring animation with 0.3 bounce +- 0.4s duration + +**Border radius change:** `useEffectMotion.ts:222-224` +- Border radius changes from 8px → 15px +- Makes the running node more pill-shaped + +**Width:** `useEffectMotion.ts:240` +- Set to fixed 64px (not animated) + +### Content Changes + +**Content opacity:** `useEffectMotion.ts:243` +- Content fades to 0 when running (icon disappears) +- Implemented in `EffectContent.tsx:120` via `opacity: motionValues.contentOpacity` + +--- + +## Animation Constants + +All timing/color values are defined in `src/animations.ts`: + +```typescript +// Border pulse timing +timing.borderPulse = { + duration: 1.5, + values: [1, 0.3, 1], +} + +// Glow pulse timing +timing.glowPulse = { + duration: 0.5, + values: [1, 5, 1], +} + +// Jitter ranges +shake.running = { + angleRange: 4, // rotation variance + angleBase: 0.5, // minimum rotation + offsetRange: 1.5, // X position variance + offsetBase: 0.5, // minimum X offset + offsetYRange: 0.6, // Y position variance + offsetYBase: 0.1, // minimum Y offset + durationMin: 0.1, // fastest jitter + durationMax: 0.2, // slowest jitter +} + +// Colors +colors.glow.running = "rgba(100, 200, 255, 0.2)" +colors.border.default = "rgba(255, 255, 255, 0.1)" +``` + +--- + +## Applying to Stream Emission Nodes + +To apply similar patterns to stream emission nodes: + +1. **Use the same hook structure:** Create `useStreamEmissionMotion()` based on `useEffectMotion()` +2. **Reuse motion values:** Same set of motion values (rotation, shakeX/Y, glowIntensity, etc.) +3. **Adjust constants:** Create separate config in `animations.ts` for emission-specific values +4. **Consider different colors:** Maybe green/cyan instead of blue to differentiate from effects +5. **Adjust intensity:** Emissions might be subtler (smaller glow, less jitter) +6. **Pattern variation:** Could use faster pulses or different light sweep patterns + +The architecture is already set up to support this - just need to: +1. Create emission-specific animation constants +2. Create `useEmissionMotion()` hook +3. Apply to emission node components diff --git a/STREAM_ADDITION.md b/STREAM_ADDITION.md new file mode 100644 index 0000000..7a139a0 --- /dev/null +++ b/STREAM_ADDITION.md @@ -0,0 +1,81 @@ +# Stream Section Addition - Revision 1 + +## Summary + +Added a new "streams" section to Visual Effect with `Stream.range` example showing sequential emission into a collector. + +## Visual Design + +**Stream.range Example Layout:** +- Five emission nodes (emit1-emit5) showing numbers 1-5 +- Each emission runs sequentially with 200-400ms jittered delay +- Final "result" node displays ChunkResult with all collected values +- Mirrors the Effect.all pattern: individual effects → combined result + +**Key Differences from First Draft:** +- Uses `resultEffect` pattern to separate emissions from collection +- All emissions highlight the same code: `Stream.range(1, 6)` +- Collector highlights: `Stream.runCollect(stream)` +- Sequential execution demonstrates pull-based lazy evaluation + +## Files Modified + +**src/examples/stream-range.tsx** +- Refactored to use `useVisualEffects` hook (plural) for batch creation +- Changed from 6 separate effects to 5 emissions + 1 collector +- Emissions run sequentially (no concurrency), showing stream's pull nature +- Collector waits for all emissions before displaying final Chunk + +**src/lib/examples-manifest.ts** +- Updated description: "Collect a finite range of integers into a Chunk" + +## Implementation Details + +**Emission Pattern:** +```typescript +const { emit1, emit2, emit3, emit4, emit5 } = useVisualEffects({ + emit1: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(1))), + // ... emit2-5 +}); +``` + +**Collector Pattern:** +```typescript +const collector = useMemo(() => { + const collectorEffect = Effect.all([ + emit1.effect, emit2.effect, emit3.effect, emit4.effect, emit5.effect + ]).pipe( + Effect.map(results => { + const numbers = results.map(r => r.value); + return new ChunkResult(numbers); + }) + ); + return new VisualEffect('result', collectorEffect); +}, [emit1, emit2, emit3, emit4, emit5]); +``` + +**ChunkResult Renderer:** +- Displays "Chunk(5)" header +- Shows individual values in pill-style badges +- Staggered animation (50ms delay per element) + +## Visual Flow + +1. User clicks Play +2. emit1 runs → shows "1" +3. emit2 runs → shows "2" +4. ... sequential through emit5 +5. All complete → collector activates +6. Result node shows: `Chunk(5)` with `[1, 2, 3, 4, 5]` pills + +This creates a **horizontal timeline metaphor**: values march left-to-right into the collector block, reinforcing the pull-based streaming model. + +## Next Steps + +When you show the screenshot, we'll adjust: +- Spacing/layout if emissions feel cramped +- ChunkResult styling if values don't read clearly +- Timing if the sequential flow is too fast/slow +- Labels if "emit1" naming is confusing + +Then proceed to Stream.map for transformation visualization. diff --git a/app/globals.css b/app/globals.css index de01936..a9bf108 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,9 @@ @import "tailwindcss"; +:root, body { + background: #0a0a0a; +} + /* Dark mode scrollbar for all elements */ * { scrollbar-width: thin; diff --git a/app/layout.tsx b/app/layout.tsx index 17a28d4..a1e5b7d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,34 +1,33 @@ /* eslint-disable react-refresh/only-export-components */ -import "./globals.css" -import { Analytics } from "@vercel/analytics/next" -import ClientAppContent from "./ClientAppContent" +import './globals.css'; +import { Analytics } from '@vercel/analytics/next'; export const metadata = { - title: "Visual Effect - Interactive Effect Playground", + title: 'Visual Effect - Interactive Effect Playground', description: - "An interactive visualization tool for the Effect library that demonstrates how Effect operations execute over time with animated visual representations and synchronized sound effects.", - metadataBase: new URL("https://effect.kitlangton.com"), + 'An interactive visualization tool for the Effect library that demonstrates how Effect operations execute over time with animated visual representations and synchronized sound effects.', + metadataBase: new URL('https://effect.kitlangton.com'), openGraph: { - title: "Visual Effect - Interactive Effect Playground", + title: 'Visual Effect - Interactive Effect Playground', description: "Interactive examples of TypeScript's beautiful Effect library", - url: "https://effect.kitlangton.com/", - siteName: "Visual Effect", + url: 'https://effect.kitlangton.com/', + siteName: 'Visual Effect', images: [ { - url: "/og-image.png", + url: '/og-image.png', width: 1200, height: 630, - alt: "Visual Effect - Interactive Effect Playground", + alt: 'Visual Effect - Interactive Effect Playground', }, ], - locale: "en_US", - type: "website", + locale: 'en_US', + type: 'website', }, twitter: { - card: "summary_large_image", - title: "Visual Effect - Interactive Effect Playground", + card: 'summary_large_image', + title: 'Visual Effect - Interactive Effect Playground', description: "Interactive examples of TypeScript's beautiful Effect library", - images: ["/og-image.png"], + images: ['/og-image.png'], }, robots: { index: true, @@ -36,25 +35,24 @@ export const metadata = { googleBot: { index: true, follow: true, - "max-video-preview": -1, - "max-image-preview": "large", - "max-snippet": -1, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, }, }, verification: { - google: "google-verification-code", // Add your Google verification code if needed + google: 'google-verification-code', // Add your Google verification code if needed }, -} +}; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {/* `vsc-initialized` is injected by some VS Code extensions after SSR; suppress hydration mismatch warnings */} - {children} - ) + ); } diff --git a/app/page.tsx b/app/page.tsx index c2a1c57..71cd357 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,5 @@ +import ClientAppContent from './ClientAppContent'; + export default function HomePage() { - return null + return ; } diff --git a/app/playground/page.tsx b/app/playground/page.tsx new file mode 100644 index 0000000..7565385 --- /dev/null +++ b/app/playground/page.tsx @@ -0,0 +1,789 @@ +'use client'; + +import { Chunk, Effect } from 'effect'; +import { useState } from 'react'; +import { EffectNode } from '../../src/components/effect'; +import { StreamPullPrototype } from '../../src/components/playground/StreamPullPrototype'; +import { StreamPullPrototypeV2 } from '../../src/components/playground/StreamPullPrototypeV2'; +import { StreamPushPullPrototype } from '../../src/components/playground/StreamPushPullPrototype'; +import { StreamPushPullPrototypeV2 } from '../../src/components/playground/StreamPushPullPrototypeV2'; +import { + BorderPulsePattern, + CardFaceBack, + CardFaceFront, + CardFlip, + CompletionCheckPattern, + DeathGlitchPattern, + FailureShakePattern, + FlashPattern, + GlowPulsePattern, + IdleStatePattern, + InteractiveRetryTimelineDemo, + JitterPattern, + LightSweepPattern, + RunningStatePattern, + ShapeMorphPattern, + StackedCard, + TimelineDotPattern, + TimelineSegmentPattern, + TimelineTicksPattern, +} from '../../src/components/patterns'; +import { + ChunkBadge, + ChunkResult, + EmojiResult, + NumberResult, + StringResult, + TemperatureResult, +} from '../../src/components/renderers'; +import { StreamTimeline } from '../../src/components/StreamTimeline'; +import { VisualEffect } from '../../src/VisualEffect'; + +export default function PlaygroundPage() { + // StreamTimeline state + const [dotCount, setDotCount] = useState(5); + const [activeIndex, setActiveIndex] = useState(0); + const [isComplete, setIsComplete] = useState(false); + + // Card component states + const [cardFlipped, setCardFlipped] = useState(false); + const [card1Flipped, setCard1Flipped] = useState(false); + const [card2Flipped, setCard2Flipped] = useState(false); + const [card3Flipped, setCard3Flipped] = useState(false); + const [fullStackFlipped, setFullStackFlipped] = useState([false, false, false, false, false]); + + // EffectNode states + const [simpleEffect] = useState(() => { + const effect = new VisualEffect('simpleEffect', Effect.succeed(new NumberResult(42))); + return effect; + }); + + const [customLabelEffect] = useState(() => { + const effect = new VisualEffect( + 'customLabelEffect', + Effect.succeed(new StringResult('Custom!')), + ); + return effect; + }); + + const [runningEffect] = useState(() => { + const effect = new VisualEffect( + 'runningEffect', + Effect.gen(function* () { + yield* Effect.sleep(10000); // long running + return new NumberResult(100); + }), + ); + return effect; + }); + + const [failedEffect] = useState(() => { + const effect = new VisualEffect( + 'failedEffect', + Effect.fail('Something went wrong'), + ); + return effect; + }); + + // Chunk examples + const smallChunk = Chunk.make(1, 2, 3); + const largeChunk = Chunk.make('A', 'B', 'C', 'D', 'E', 'F'); + + const handleNextDot = () => { + if (activeIndex < dotCount - 1) { + setActiveIndex(activeIndex + 1); + } + }; + + const handleComplete = () => { + setIsComplete(true); + }; + + const handleReset = () => { + setActiveIndex(0); + setIsComplete(false); + }; + + const handleRunSimple = () => { + simpleEffect.reset(); + simpleEffect.run(); + }; + + const handleRunCustom = () => { + customLabelEffect.reset(); + customLabelEffect.run(); + }; + + const handleRunLongRunning = () => { + runningEffect.reset(); + runningEffect.run(); + }; + + const handleRunFailed = () => { + failedEffect.reset(); + failedEffect.run(); + }; + + return ( +
+
+ {/* Header */} +
+

Component Playground

+

Visual showcase of atomic components in isolation

+
+ + {/* Card Atomic Components */} +
+

Card Atomic Components

+

+ Playing card-style chunk cards with flip animations and stacking +

+ + {/* Individual Card Faces */} +
+

Individual Card Faces

+
+
+
+ CardFaceFront (42) +
+
+ +
+
+
+
+ CardFaceFront (7) +
+
+ +
+
+
+
+ CardFaceBack (Locked) +
+
+ +
+
+
+
+ + {/* Interactive Card Flip */} +
+

Interactive Card Flip

+
+
+ } + backFace={} + /> +
+
+ +
+
+
+ + {/* Mini Stack Demo */} +
+

StackedCard (3 cards)

+
+
+
+ + + +
+
+
+ + + + +
+
+
+ + {/* Full Card Stack */} +
+

+ Full Card Stack (5 cards) +

+
+
+
+ {[5, 4, 3, 2, 1].map((value, index) => ( + + ))} +
+
+
+ + + +
+
+ Bottom card (index 0) has pulsing highlight +
+
+
+
+ + {/* StreamTimeline Section */} +
+

StreamTimeline

+
+
+ +
+
+
+ + + + + +
+
+ State: {dotCount} dots, active: {activeIndex}, complete: {isComplete ? 'yes' : 'no'} +
+
+ + {/* Basic Result Renderers */} +
+

Basic Result Renderers

+
+
+
NumberResult
+ {new NumberResult(42).render()} +
+
+
StringResult
+ {new StringResult('Hello').render()} +
+
+
EmojiResult
+ {new EmojiResult('🚀').render()} +
+
+
TemperatureResult
+ {new TemperatureResult(72, 'San Francisco').render()} +
+
+
+ + {/* Chunk Renderers */} +
+

Chunk Renderers

+
+
+
ChunkBadge (small)
+ {new ChunkBadge(smallChunk).render()} +
+
+
ChunkBadge (large)
+ {new ChunkBadge(largeChunk).render()} +
+
+
ChunkResult (small)
+ {new ChunkResult(smallChunk).render()} +
+
+
ChunkResult (large)
+ {new ChunkResult(largeChunk).render()} +
+
+
+ + {/* Timeline Patterns */} +
+

Timeline Patterns

+

+ Visual patterns for showing time-based Effect operations (retry, repeat, scheduling) +

+ + {/* Atomic Timeline Components */} +
+

+ Atomic Timeline Components +

+
+ {/* Timeline Ticks */} +
+
Timeline Ticks (Grid)
+
+ +
+
+ + {/* Timeline Dots */} +
+
Timeline Dots (States)
+
+
+ + + +
+
+
+ Idle (neutral) + Active (blue, larger) + Complete (blue) +
+
+ + {/* Timeline Segments */} +
+
Timeline Segments
+
+
+ + + +
+
+
+ Complete segment + Active segment + Gap with duration label +
+
+
+
+ + {/* Interactive Retry Timeline */} +
+

Interactive Retry Timeline

+
+
+ Complete retry visualization with exponential backoff, matching Effect.retry + behavior +
+ +
+
+
+ + {/* Atomic Effect Patterns */} +
+

Atomic Effect Patterns

+

+ Individual visual patterns decomposed from EffectNode states +

+ + {/* Full State Patterns */} +
+

Full State Compositions

+
+
+
Idle State
+
+ +
+
+
+
+ Running (All Patterns) +
+
+ +
+
+
+
+ + {/* Individual Running Patterns */} +
+

+ Individual Running Patterns +

+
+
+
Border Pulse
+
+ +
+
+
+
Glow Pulse
+
+ +
+
+
+
+ Jitter + Motion Blur +
+
+ +
+
+
+
Light Sweeps
+
+ +
+
+
+
Shape Morph
+
+ +
+
+
+
+ + {/* Other State Patterns */} +
+

Other State Patterns

+
+
+
+ Completion Check +
+
+ +
+
+
+
Failure Shake
+
+ +
+
+
+
Death Glitch
+
+ +
+
+
+
Flash Effect
+
+ +
+
+
+
+
+ + {/* EffectNode Components */} +
+

EffectNode Variations

+ +
+ {/* Basic EffectNode */} +
+
Basic EffectNode
+
+ +
+ + +
+
+
+ + {/* EffectNode with customLabel */} +
+
EffectNode with customLabel
+
+ +
+ + +
+
+
+ + {/* Running EffectNode */} +
+
Running EffectNode (long)
+
+ +
+ + +
+
+
+ + {/* Failed EffectNode */} +
+
Failed EffectNode
+
+ +
+ + +
+
+
+
+
+ + {/* Stream Pull Prototype V2 - With Card Flips */} +
+

Stream Pull Prototype V2 🃏

+

+ Cards start face-down (mystery), flip to reveal value when pulled +

+ +
+ + {/* Stream Pull Prototype (Original) */} +
+

Stream Pull Prototype (Original)

+

+ Manual pull-based stream - click button to consume each chunk +

+ +
+ + {/* Stream Push + Pull Prototype V2 - With Card Flips */} +
+

+ Stream Push + Pull Prototype V2 🃏☁️ +

+

+ Push new face-down cards from cloud, pull and flip from bottom to reveal values +

+ +
+ + {/* Stream Push + Pull Prototype (Original) */} +
+

+ Stream Push + Pull Prototype (Original) +

+

+ Push new chunks from the cloud, pull from the bottom - simulates async stream sources +

+ +
+
+
+ ); +} diff --git a/bun.lock b/bun.lock index ffae326..f1212b7 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "visual-effect", @@ -8,27 +9,27 @@ "@vercel/analytics": "^1.5.0", "effect": "^3.18.4", "motion": "^12.23.24", - "next": "^15.5.4", + "next": "^16.0.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.0", "react-dom": "^19.2.0", "tone": "^15.1.22", }, "devDependencies": { - "@biomejs/biome": "2.2.5", - "@effect/language-service": "^0.44.0", + "@biomejs/biome": "2.3.1", + "@effect/language-service": "^0.48.0", "@resvg/resvg-js": "^2.6.2", - "@tailwindcss/postcss": "^4.1.14", - "@types/node": "^24.7.2", + "@tailwindcss/postcss": "^4.1.16", + "@types/node": "^24.9.1", "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.1", - "@vitest/ui": "^3.2.4", - "jsdom": "^27.0.0", + "@types/react-dom": "^19.2.2", + "@vitest/ui": "^4.0.4", + "jsdom": "^27.0.1", "satori": "^0.18.3", - "tailwindcss": "^4.1.14", + "tailwindcss": "^4.1.16", "tsx": "^4.20.6", "typescript": "~5.9.3", - "vitest": "^3.2.4", + "vitest": "^4.0.4", }, }, }, @@ -46,23 +47,23 @@ "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@biomejs/biome": ["@biomejs/biome@2.2.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.5", "@biomejs/cli-darwin-x64": "2.2.5", "@biomejs/cli-linux-arm64": "2.2.5", "@biomejs/cli-linux-arm64-musl": "2.2.5", "@biomejs/cli-linux-x64": "2.2.5", "@biomejs/cli-linux-x64-musl": "2.2.5", "@biomejs/cli-win32-arm64": "2.2.5", "@biomejs/cli-win32-x64": "2.2.5" }, "bin": { "biome": "bin/biome" } }, "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw=="], + "@biomejs/biome": ["@biomejs/biome@2.3.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.1", "@biomejs/cli-darwin-x64": "2.3.1", "@biomejs/cli-linux-arm64": "2.3.1", "@biomejs/cli-linux-arm64-musl": "2.3.1", "@biomejs/cli-linux-x64": "2.3.1", "@biomejs/cli-linux-x64-musl": "2.3.1", "@biomejs/cli-win32-arm64": "2.3.1", "@biomejs/cli-win32-x64": "2.3.1" }, "bin": { "biome": "bin/biome" } }, "sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], @@ -76,7 +77,7 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], - "@effect/language-service": ["@effect/language-service@0.44.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-BH5F8B1CFbkL2iaX8Ly5OFO7wHfWVc+FJ7xxF+XF2tVOW1gaWxxyBdvyPpZSZDbL8wp+Fii7NH5lXh+Q9W7Tqg=="], + "@effect/language-service": ["@effect/language-service@0.48.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-u7DTPoGFFeDGSdomjY5C2nCGNWSisxpYSqHp3dlSG8kCZh5cay+166bveHRYvuJSJS5yomdkPTJwjwrqMmT7Og=="], "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], @@ -188,23 +189,23 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@next/env": ["@next/env@15.5.6", "", {}, "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q=="], + "@next/env": ["@next/env@16.0.2", "", {}, "sha512-V2e9ITU6Ts9kxtTBX60qtWlKV+AeBNlz/hgAt0gkGA8aPgX27cRLjp7OEUMzYq4cY0QzOkOQ4CI/8IJh6kW/iw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E6rxUdkZX5sZjLduXphiMuRJAmvsxWi5IivD0kRLLX5cjNLOs2PjlSyda+dtT3iqE6vxaRGV3oQMnQiJU8F+Ig=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-QNXdjXVFtb35vImDJtXqYlhq8A2mHLroqD8q4WCwO+IVnVoQshhcEVWJlP9UB/dOC6Wh782BbTHqGzKQwlCSkQ=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dM9yEB35GZAW3r+w88iGEz7OkJjSYSd4pKyl4KwSXx8cLWMpWaX1WW42dCAKXCWWQhVUXUZAEx38yfpEZ1/IJg=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hiNysPK1VeK5MGNmuKLnj3Y4lkaffvAlXin404QpxYkNCBms/Bk0msZHey5lUNq8FV50PY6I9CgY+c/NK+xeLg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-hAhhobw4tHOCzZ5sm5W/EsQPxS3NbZl6rqzmA0GTV9etE8sPHmsV6OopP12TeeoXA/NjXKD2mcz8hcVWLe4jkg=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-s0LUsoeRky95aTS6IfYnJOn6F5kbs+gjiVUQK0JmsJ/ZCXaply20kDoJ8/zHwMz5cyOVg7GrQJdMvyO9FLD9Bw=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-TMWE1h44d0WRyq0yQI/0W5A7nZUoiwE2Sdg43wt2Q1IoadU5Ky00G3cJ2mSnbetwL7+eFyM7BQgx+Fonpz6T8w=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+8SqzDhau/PNsWdcagnoz6ltOM9IcsqagdTFsEELNOty0+lNh5hwO5oUFForPOywTbM+d3tPLo5m20VdEBDf3Q=="], "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], @@ -318,14 +319,10 @@ "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], @@ -336,41 +333,35 @@ "@vercel/analytics": ["@vercel/analytics@1.5.0", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + "@vitest/expect": ["@vitest/expect@4.0.8", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.8", "@vitest/utils": "4.0.8", "chai": "^6.2.0", "tinyrainbow": "^3.0.3" } }, "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA=="], - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + "@vitest/mocker": ["@vitest/mocker@4.0.8", "", { "dependencies": { "@vitest/spy": "4.0.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg=="], - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.8", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg=="], - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + "@vitest/runner": ["@vitest/runner@4.0.8", "", { "dependencies": { "@vitest/utils": "4.0.8", "pathe": "^2.0.3" } }, "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ=="], - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.8", "", { "dependencies": { "@vitest/pretty-format": "4.0.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/spy": ["@vitest/spy@4.0.8", "", {}, "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA=="], - "@vitest/ui": ["@vitest/ui@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.1", "tinyglobby": "^0.2.14", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.2.4" } }, "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA=="], + "@vitest/ui": ["@vitest/ui@4.0.8", "", { "dependencies": { "@vitest/utils": "4.0.8", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.0.8" } }, "sha512-F9jI5rSstNknPlTlPN2gcc4gpbaagowuRzw/OJzl368dvPun668Q182S8Q8P9PITgGCl5LAKXpzuue106eM4wA=="], - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "@vitest/utils": ["@vitest/utils@4.0.8", "", { "dependencies": { "@vitest/pretty-format": "4.0.8", "tinyrainbow": "^3.0.3" } }, "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "automation-events": ["automation-events@7.1.13", "", { "dependencies": { "@babel/runtime": "^7.28.4", "tslib": "^2.8.1" } }, "sha512-1Hay5TQPzxsskSqPTH3YXyzE9Iirz82zZDse2vr3+kOR7Sc7om17qIEPsESchlNX0EgKxANwR40i2g/O3GM1Tw=="], "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -400,8 +391,6 @@ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], @@ -452,8 +441,6 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "jsdom": ["jsdom@27.0.1", "", { "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -482,11 +469,9 @@ "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], - "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], @@ -502,7 +487,7 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "next": ["next@15.5.6", "", { "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.6", "@next/swc-darwin-x64": "15.5.6", "@next/swc-linux-arm64-gnu": "15.5.6", "@next/swc-linux-arm64-musl": "15.5.6", "@next/swc-linux-x64-gnu": "15.5.6", "@next/swc-linux-x64-musl": "15.5.6", "@next/swc-win32-arm64-msvc": "15.5.6", "@next/swc-win32-x64-msvc": "15.5.6", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ=="], + "next": ["next@16.0.2", "", { "dependencies": { "@next/env": "16.0.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.2", "@next/swc-darwin-x64": "16.0.2", "@next/swc-linux-arm64-gnu": "16.0.2", "@next/swc-linux-arm64-musl": "16.0.2", "@next/swc-linux-x64-gnu": "16.0.2", "@next/swc-linux-x64-musl": "16.0.2", "@next/swc-win32-arm64-msvc": "16.0.2", "@next/swc-win32-x64-msvc": "16.0.2", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zL8+UBf+xUIm8zF0vYGJYJMYDqwaBrRRe7S0Kob6zo9Kf+BdqFLEECMI+B6cNIcoQ+el9XM2fvUExwhdDnXjtw=="], "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], @@ -512,8 +497,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -562,12 +545,10 @@ "standardized-audio-context": ["standardized-audio-context@25.3.77", "", { "dependencies": { "@babel/runtime": "^7.25.6", "automation-events": "^7.0.9", "tslib": "^2.7.0" } }, "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A=="], - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="], - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], @@ -584,11 +565,7 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], "tldts": ["tldts@7.0.17", "", { "dependencies": { "tldts-core": "^7.0.17" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ=="], @@ -614,9 +591,7 @@ "vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "vitest": ["vitest@4.0.8", "", { "dependencies": { "@vitest/expect": "4.0.8", "@vitest/mocker": "4.0.8", "@vitest/pretty-format": "4.0.8", "@vitest/runner": "4.0.8", "@vitest/snapshot": "4.0.8", "@vitest/spy": "4.0.8", "@vitest/utils": "4.0.8", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.8", "@vitest/browser-preview": "4.0.8", "@vitest/browser-webdriverio": "4.0.8", "@vitest/ui": "4.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], @@ -640,6 +615,8 @@ "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + "@tailwindcss/node/magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], diff --git a/next-env.d.ts b/next-env.d.ts index d39ca30..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js index cc43a8c..15f334d 100644 --- a/next.config.js +++ b/next.config.js @@ -1,18 +1,19 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: "export", + output: 'export', trailingSlash: true, images: { unoptimized: true, }, - distDir: "out", + distDir: 'out', reactStrictMode: true, experimental: { esmExternals: true, }, + turbopack: {}, webpack: (config, { isServer }) => { // Disable module concatenation to fix "Unexpected end of JSON input" errors - config.optimization.concatenateModules = false + config.optimization.concatenateModules = false; if (!isServer) { config.resolve.fallback = { @@ -20,12 +21,12 @@ const nextConfig = { fs: false, path: false, crypto: false, - } + }; } - return config + return config; }, - transpilePackages: ["motion"], -} + transpilePackages: ['motion'], +}; -export default nextConfig +export default nextConfig; diff --git a/src/AppContent.tsx b/src/AppContent.tsx index 7630967..231c4d7 100644 --- a/src/AppContent.tsx +++ b/src/AppContent.tsx @@ -1,4 +1,4 @@ -"use client" +'use client'; import { ArrowClockwiseIcon, @@ -7,150 +7,155 @@ import { PlayIcon, SkullIcon, StopIcon, -} from "@phosphor-icons/react" -import { MotionConfig } from "motion/react" -import { usePathname } from "next/navigation" -import { Fragment, useCallback, useEffect, useMemo, useState } from "react" +} from '@phosphor-icons/react'; +import { MotionConfig } from 'motion/react'; +import { usePathname } from 'next/navigation'; +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; // Examples -import EffectAcquireRelease from "@/examples/effect-acquire-release" -import EffectAddFinalizer from "@/examples/effect-add-finalizer" -import EffectAll from "@/examples/effect-all" -import EffectAllShortCircuit from "@/examples/effect-all-short-circuit" -import EffectDie from "@/examples/effect-die" -import EffectEventually from "@/examples/effect-eventually" -import EffectFail from "@/examples/effect-fail" -import EffectForEach from "@/examples/effect-foreach" -import EffectOrElse from "@/examples/effect-orelse" -import EffectPartition from "@/examples/effect-partition" -import EffectPromise from "@/examples/effect-promise" -import EffectRace from "@/examples/effect-race" -import EffectRaceAll from "@/examples/effect-raceall" -import EffectRepeatSpaced from "@/examples/effect-repeat-spaced" -import EffectRepeatWhileOutput from "@/examples/effect-repeat-while-output" -import EffectRetryExponential from "@/examples/effect-retry-exponential" -import EffectRetryRecurs from "@/examples/effect-retry-recurs" -import EffectSleep from "@/examples/effect-sleep" -import EffectSucceed from "@/examples/effect-succeed" -import EffectSync from "@/examples/effect-sync" -import EffectTimeout from "@/examples/effect-timeout" -import EffectValidate from "@/examples/effect-validate" +import EffectAcquireRelease from '@/examples/effect-acquire-release'; +import EffectAddFinalizer from '@/examples/effect-add-finalizer'; +import EffectAll from '@/examples/effect-all'; +import EffectAllShortCircuit from '@/examples/effect-all-short-circuit'; +import EffectDie from '@/examples/effect-die'; +import EffectEventually from '@/examples/effect-eventually'; +import EffectFail from '@/examples/effect-fail'; +import EffectForEach from '@/examples/effect-foreach'; +import EffectOrElse from '@/examples/effect-orelse'; +import EffectPartition from '@/examples/effect-partition'; +import EffectPromise from '@/examples/effect-promise'; +import EffectRace from '@/examples/effect-race'; +import EffectRaceAll from '@/examples/effect-raceall'; +import EffectRepeatSpaced from '@/examples/effect-repeat-spaced'; +import EffectRepeatWhileOutput from '@/examples/effect-repeat-while-output'; +import EffectRetryExponential from '@/examples/effect-retry-exponential'; +import EffectRetryRecurs from '@/examples/effect-retry-recurs'; +import EffectSleep from '@/examples/effect-sleep'; +import EffectSucceed from '@/examples/effect-succeed'; +import EffectSync from '@/examples/effect-sync'; +import EffectTimeout from '@/examples/effect-timeout'; +import EffectValidate from '@/examples/effect-validate'; -import RefMake from "@/examples/ref-make" -import RefUpdateAndGet from "@/examples/ref-update-and-get" -import type { ExampleMeta } from "@/lib/example-types" +import RefMake from '@/examples/ref-make'; +import RefUpdateAndGet from '@/examples/ref-update-and-get'; +import StreamRange from '@/examples/stream-range'; +import type { ExampleMeta } from '@/lib/example-types'; type ExampleComponent = React.ComponentType<{ - index: number - metadata: ExampleMeta - exampleId: string -}> + index: number; + metadata: ExampleMeta; + exampleId: string; +}>; const exampleComponentById: Record = { - "effect-acquire-release": EffectAcquireRelease, - "effect-add-finalizer": EffectAddFinalizer, - "effect-all-short-circuit": EffectAllShortCircuit, - "effect-all": EffectAll, - "effect-die": EffectDie, - "effect-eventually": EffectEventually, - "effect-fail": EffectFail, - "effect-foreach": EffectForEach, - "effect-orelse": EffectOrElse, - "effect-partition": EffectPartition, - "effect-promise": EffectPromise, - "effect-race": EffectRace, - "effect-raceall": EffectRaceAll, - "effect-repeat-spaced": EffectRepeatSpaced, - "effect-repeat-while-output": EffectRepeatWhileOutput, - "effect-retry-exponential": EffectRetryExponential, - "effect-retry-recurs": EffectRetryRecurs, - "effect-sleep": EffectSleep, - "effect-succeed": EffectSucceed, - "effect-sync": EffectSync, - "effect-timeout": EffectTimeout, - "effect-validate": EffectValidate, - "ref-make": RefMake, - "ref-update-and-get": RefUpdateAndGet, -} + 'effect-acquire-release': EffectAcquireRelease, + 'effect-add-finalizer': EffectAddFinalizer, + 'effect-all-short-circuit': EffectAllShortCircuit, + 'effect-all': EffectAll, + 'effect-die': EffectDie, + 'effect-eventually': EffectEventually, + 'effect-fail': EffectFail, + 'effect-foreach': EffectForEach, + 'effect-orelse': EffectOrElse, + 'effect-partition': EffectPartition, + 'effect-promise': EffectPromise, + 'effect-race': EffectRace, + 'effect-raceall': EffectRaceAll, + 'effect-repeat-spaced': EffectRepeatSpaced, + 'effect-repeat-while-output': EffectRepeatWhileOutput, + 'effect-retry-exponential': EffectRetryExponential, + 'effect-retry-recurs': EffectRetryRecurs, + 'effect-sleep': EffectSleep, + 'effect-succeed': EffectSucceed, + 'effect-sync': EffectSync, + 'effect-timeout': EffectTimeout, + 'effect-validate': EffectValidate, + 'ref-make': RefMake, + 'ref-update-and-get': RefUpdateAndGet, + 'stream-range': StreamRange, +}; -import { defaultSpring } from "@/animations" -import { EffectLogo } from "@/components/feedback" -import { NavigationSidebar } from "@/components/layout/NavigationSidebar" -import { PageHeader } from "@/components/layout/PageHeader" -import { QuickOpen } from "@/components/ui" -import type { AppItem } from "@/lib/example-types" -import { appItems, createExampleId } from "@/shared/appItems" -import { taskSounds } from "@/sounds/TaskSounds" +import { defaultSpring } from '@/animations'; +import { EffectLogo } from '@/components/feedback'; +import { NavigationSidebar } from '@/components/layout/NavigationSidebar'; +import { PageHeader } from '@/components/layout/PageHeader'; +import { QuickOpen } from '@/components/ui'; +import type { AppItem } from '@/lib/example-types'; +import { appItems, createExampleId } from '@/shared/appItems'; +import { taskSounds } from '@/sounds/TaskSounds'; // Helper function to get item section function getItemSection(item: AppItem): string { - return item.metadata.section + return item.metadata.section; } function AppContentInner() { - const [isMuted, setIsMuted] = useState(false) + const [isMuted, setIsMuted] = useState(false); // Update sound system when mute changes useEffect(() => { - taskSounds.setMuted(isMuted) - }, [isMuted]) + taskSounds.setMuted(isMuted); + }, [isMuted]); - const [currentExampleId, setCurrentExampleId] = useState() + const [currentExampleId, setCurrentExampleId] = useState(); // Handle example selection from sidebar const handleExampleSelect = useCallback((id: string) => { - setCurrentExampleId(id) + setCurrentExampleId(id); // Scroll to the element - const element = document.getElementById(id) + const element = document.getElementById(id); if (element) { - element.scrollIntoView({ behavior: "smooth", block: "center" }) + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } - }, []) + }, []); // Prepare example metadata once for both navigation and quick-open const exampleDisplayItems = useMemo( () => - appItems.map(item => ({ + appItems.map((item) => ({ id: createExampleId(item.metadata.name, item.metadata.variant), name: item.metadata.name, ...(item.metadata.variant ? { variant: item.metadata.variant } : {}), section: item.metadata.section, })), [], - ) + ); const exampleIdSet = useMemo( - () => new Set(exampleDisplayItems.map(example => example.id)), + () => new Set(exampleDisplayItems.map((example) => example.id)), [exampleDisplayItems], - ) + ); - const pathname = usePathname() + const pathname = usePathname(); useEffect(() => { - if (!pathname) return - const segments = pathname.split("/").filter(Boolean) - const rawTarget = segments.at(-1) + if (!pathname) return; + const segments = pathname.split('/').filter(Boolean); + const rawTarget = segments.at(-1); if (!rawTarget) { - setCurrentExampleId(undefined) - return + setCurrentExampleId(undefined); + return; } - const targetId = decodeURIComponent(rawTarget) - if (!exampleIdSet.has(targetId)) return + const targetId = decodeURIComponent(rawTarget); + if (!exampleIdSet.has(targetId)) return; window.requestAnimationFrame(() => { // RequestAnimationFrame ensures the DOM is ready before attempting to scroll. - handleExampleSelect(targetId) - }) - }, [pathname, exampleIdSet, handleExampleSelect]) + handleExampleSelect(targetId); + }); + }, [pathname, exampleIdSet, handleExampleSelect]); return ( -
+
{/* Command-K quick-open modal */} - + -
+
{/* Navigation Sidebar */} -
-
- setIsMuted(!isMuted)} /> +
+
+ setIsMuted(!isMuted)} + /> {/* Introduction section */} -
-
-

- Here are some interactive examples of TypeScript's beautiful{" "} - +

+
+

+ Here are some interactive examples of TypeScript's beautiful{' '} + Effect - {" "} - library. Tap the following effects to{" "} - - run,{" "} - - interrupt, or{" "} - - reset them. + {' '} + library. Tap the following effects to{' '} + + run,{' '} + + interrupt, or{' '} + + reset them.

{/* Multiple effect examples and callouts */} -
+
{appItems.map((item, index) => { - const prevItem: AppItem | undefined = index > 0 ? appItems[index - 1] : undefined + const prevItem: AppItem | undefined = index > 0 ? appItems[index - 1] : undefined; const showSectionHeader = index === 0 || - (prevItem !== undefined && getItemSection(prevItem) !== getItemSection(item)) + (prevItem !== undefined && getItemSection(prevItem) !== getItemSection(item)); return ( {showSectionHeader && ( -
-

-
-
+
+

+
+
- + {getItemSection(item).toUpperCase()}
@@ -216,55 +236,59 @@ function AppContentInner() {

)}
{(() => { - const Component = exampleComponentById[item.metadata.id] - if (!Component) return null + const Component = exampleComponentById[item.metadata.id]; + if (!Component) return null; return ( - ) + ); })()}
- ) + ); })}
{/* Footer */} -
+
{/* Left side */} -
+
EFFECT OR - +
{/* Right side */} KIT @@ -273,7 +297,7 @@ function AppContentInner() {

- ) + ); } export function AppContent() { @@ -281,5 +305,5 @@ export function AppContent() { - ) + ); } diff --git a/src/VisualEffect.ts b/src/VisualEffect.ts index 3db7717..288290b 100644 --- a/src/VisualEffect.ts +++ b/src/VisualEffect.ts @@ -1,73 +1,73 @@ -"use client" +'use client'; -import { Context, Effect, Fiber, Option } from "effect" -import { useSyncExternalStore } from "react" -import { taskSounds } from "./sounds/TaskSounds" +import { Context, Effect, Fiber, Option } from 'effect'; +import { useSyncExternalStore } from 'react'; +import { taskSounds } from './sounds/TaskSounds'; export type EffectState = - | { type: "idle" } - | { type: "running" } - | { type: "completed"; result: A } - | { type: "failed"; error: E } - | { type: "interrupted" } - | { type: "death"; error: unknown } + | { type: 'idle' } + | { type: 'running' } + | { type: 'completed'; result: A } + | { type: 'failed'; error: E } + | { type: 'interrupted' } + | { type: 'death'; error: unknown }; // Pattern matching helper for EffectState (internal use only) const matchEffectState = ( state: EffectState, cases: { - idle: () => T - running: () => T - completed: (result: A) => T - failed: (error: E) => T - interrupted: () => T - death: (error: unknown) => T + idle: () => T; + running: () => T; + completed: (result: A) => T; + failed: (error: E) => T; + interrupted: () => T; + death: (error: unknown) => T; }, ): T => { switch (state.type) { - case "idle": - return cases.idle() - case "running": - return cases.running() - case "completed": - return cases.completed(state.result) - case "failed": - return cases.failed(state.error) - case "interrupted": - return cases.interrupted() - case "death": - return cases.death(state.error) + case 'idle': + return cases.idle(); + case 'running': + return cases.running(); + case 'completed': + return cases.completed(state.result); + case 'failed': + return cases.failed(state.error); + case 'interrupted': + return cases.interrupted(); + case 'death': + return cases.death(state.error); } -} +}; export interface Notification { - id: string - message: string - timestamp: number - duration?: number // auto-dismiss after this many ms - icon?: string // emoji or icon + id: string; + message: string; + timestamp: number; + duration?: number; // auto-dismiss after this many ms + icon?: string; // emoji or icon } // Valid state transitions for the state machine const VALID_TRANSITIONS: Record> = { - idle: new Set(["running", "idle"]), - running: new Set(["completed", "failed", "interrupted", "death", "idle", "running"]), - completed: new Set(["idle", "running", "completed"]), - failed: new Set(["failed", "idle", "running"]), - interrupted: new Set(["interrupted", "idle", "running"]), - death: new Set(["death", "idle", "running"]), -} + idle: new Set(['running', 'idle']), + running: new Set(['completed', 'failed', 'interrupted', 'death', 'idle', 'running']), + completed: new Set(['idle', 'running', 'completed']), + failed: new Set(['failed', 'idle', 'running']), + interrupted: new Set(['interrupted', 'idle', 'running']), + death: new Set(['death', 'idle', 'running']), +}; // Service interface for parent-child VisualEffect communication export interface VisualEffectService { - readonly addChild: (child: VisualEffect) => Effect.Effect + readonly addChild: (child: VisualEffect) => Effect.Effect; readonly notify: ( message: string, options?: { duration?: number; icon?: string }, - ) => Effect.Effect + ) => Effect.Effect; } -const VisualEffectService = Context.GenericTag("VisualEffectService") +const VisualEffectService = Context.GenericTag('VisualEffectService'); // Service implementation class VisualEffectServiceImpl implements VisualEffectService { @@ -75,32 +75,32 @@ class VisualEffectServiceImpl implements VisualEffectService { addChild = (child: VisualEffect): Effect.Effect => Effect.sync(() => { - this.parent.addChildEffect(child) - }) + this.parent.addChildEffect(child); + }); notify = (message: string, options?: { duration?: number; icon?: string }): Effect.Effect => Effect.sync(() => { - this.parent.notify(message, options) - }) + this.parent.notify(message, options); + }); } export class VisualEffect { - private listeners = new Set<() => void>() - private notificationListeners = new Set<() => void>() - private currentNotification: Notification | null = null - private fiber: Fiber.RuntimeFiber | null = null - private timeouts = new Set>() - private isResetting = false - private children = new Set>() + private listeners = new Set<() => void>(); + private notificationListeners = new Set<() => void>(); + private currentNotification: Notification | null = null; + private fiber: Fiber.RuntimeFiber | null = null; + private timeouts = new Set>(); + private isResetting = false; + private children = new Set>(); - state: EffectState = { type: "idle" } + state: EffectState = { type: 'idle' }; addChildEffect(child: VisualEffect): void { - this.children.add(child) + this.children.add(child); } - startTime: number | null = null - endTime: number | null = null + startTime: number | null = null; + endTime: number | null = null; constructor( public name: string, @@ -114,91 +114,91 @@ export class VisualEffect { const quickReturn = matchEffectState(this.state, { idle: () => null, running: () => null, - completed: result => Effect.succeed(result), - failed: error => Effect.fail(error) as Effect.Effect, + completed: (result) => Effect.succeed(result), + failed: (error) => Effect.fail(error) as Effect.Effect, interrupted: () => null, - death: error => Effect.die(error), - }) + death: (error) => Effect.die(error), + }); - if (quickReturn) return quickReturn + if (quickReturn) return quickReturn; // Create the effect return Effect.gen( function* (this: VisualEffect) { // Register with parent service if available - const maybeParentService = yield* Effect.serviceOption(VisualEffectService) + const maybeParentService = yield* Effect.serviceOption(VisualEffectService); yield* Option.match(maybeParentService, { onNone: () => Effect.void, - onSome: service => service.addChild(this), - }) + onSome: (service) => service.addChild(this), + }); // Mark as running - this.setState({ type: "running" }) + this.setState({ type: 'running' }); // Execute the wrapped effect with appropriate service provided to all nested effects const effectWithRootService = this._effect.pipe( Effect.provideService(VisualEffectService, new VisualEffectServiceImpl(this)), - ) + ); const wrappedEffect = Option.isSome(maybeParentService) ? this._effect - : effectWithRootService + : effectWithRootService; - return yield* wrappedEffect + return yield* wrappedEffect; }.bind(this), ).pipe( // Clear notifications on any non-success exit Effect.tapErrorCause(() => Effect.sync(() => this.clearNotifications())), // Handle success - Effect.tap(result => + Effect.tap((result) => Effect.sync(() => { - this.setState({ type: "completed", result }) + this.setState({ type: 'completed', result }); }), ), // Handle errors Effect.tapError((error: E) => Effect.sync(() => { - this.setState({ type: "failed", error }) + this.setState({ type: 'failed', error }); }), ), // Handle interruption Effect.onInterrupt(() => Effect.sync(() => { if (!this.isResetting) { - this.setState({ type: "interrupted" }) + this.setState({ type: 'interrupted' }); } }), ), // Handle defects Effect.tapDefect((defect: unknown) => Effect.sync(() => { - if (process.env.NODE_ENV === "development") { - console.error(`Effect "${this.name}" died with defect:`, defect) + if (process.env.NODE_ENV === 'development') { + console.error(`Effect "${this.name}" died with defect:`, defect); } - this.setState({ type: "death", error: defect }) + this.setState({ type: 'death', error: defect }); }), ), - ) as Effect.Effect + ) as Effect.Effect; } // Observable pattern methods subscribe(listener: () => void) { - this.listeners.add(listener) + this.listeners.add(listener); return () => { - this.listeners.delete(listener) - } + this.listeners.delete(listener); + }; } subscribeToNotifications(listener: () => void) { - this.notificationListeners.add(listener) + this.notificationListeners.add(listener); return () => { - this.notificationListeners.delete(listener) - } + this.notificationListeners.delete(listener); + }; } notify(message: string, options?: { duration?: number; icon?: string }): void { // Clear any existing notification and its timeout - this.clearNotifications() + this.clearNotifications(); const notification: Notification = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, @@ -206,66 +206,66 @@ export class VisualEffect { timestamp: Date.now(), duration: options?.duration ?? 2000, // default 2 seconds ...(options?.icon && { icon: options.icon }), - } + }; - this.currentNotification = notification - this.notifyNotificationListeners() + this.currentNotification = notification; + this.notifyNotificationListeners(); // Auto-remove after duration if (notification.duration) { const timeoutId = setTimeout(() => { - this.clearNotifications() - }, notification.duration) - this.timeouts.add(timeoutId) + this.clearNotifications(); + }, notification.duration); + this.timeouts.add(timeoutId); } } getCurrentNotification(): Notification | null { - return this.currentNotification + return this.currentNotification; } private clearNotifications(): void { - this.currentNotification = null - this.clearTimeouts() - this.notifyNotificationListeners() + this.currentNotification = null; + this.clearTimeouts(); + this.notifyNotificationListeners(); } private notifyStateListeners() { - this.listeners.forEach(listener => { - listener() - }) + this.listeners.forEach((listener) => { + listener(); + }); } private notifyNotificationListeners() { - this.notificationListeners.forEach(listener => { - listener() - }) + this.notificationListeners.forEach((listener) => { + listener(); + }); } private setState(newState: EffectState) { - if (this.isResetting) return + if (this.isResetting) return; - const previousState = this.state - const validTransitions = VALID_TRANSITIONS[previousState.type] + const previousState = this.state; + const validTransitions = VALID_TRANSITIONS[previousState.type]; if (!validTransitions?.has(newState.type)) { - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === 'development') { console.warn( `Invalid state transition from ${previousState.type} to ${newState.type} for task ${this.name}`, - ) + ); } - return + return; } - this.state = newState + this.state = newState; // Track timing if (this.showTimer) { - if (newState.type === "running" && previousState.type !== "running") { - this.startTime = Date.now() - this.endTime = null - } else if (previousState.type === "running" && newState.type !== "running") { - this.endTime = Date.now() + if (newState.type === 'running' && previousState.type !== 'running') { + this.startTime = Date.now(); + this.endTime = null; + } else if (previousState.type === 'running' && newState.type !== 'running') { + this.endTime = Date.now(); } } @@ -278,70 +278,70 @@ export class VisualEffect { failed: () => taskSounds.playFailure().catch(() => {}), interrupted: () => taskSounds.playInterrupted().catch(() => {}), death: () => taskSounds.playDeath().catch(() => {}), - }) + }); } - this.notifyStateListeners() + this.notifyStateListeners(); } private clearTimeouts() { - this.timeouts.forEach(clearTimeout) - this.timeouts.clear() + this.timeouts.forEach(clearTimeout); + this.timeouts.clear(); } reset() { - this.isResetting = true + this.isResetting = true; try { // Reset all children first so their state transitions obey the reset flag - this.children.forEach(child => { - child.reset() - }) + this.children.forEach((child) => { + child.reset(); + }); // Clear the children collection since they're no longer relevant - this.children.clear() + this.children.clear(); // Interrupt our own fiber if it's still running if (this.fiber) { - Effect.runFork(Fiber.interrupt(this.fiber)) - this.fiber = null + Effect.runFork(Fiber.interrupt(this.fiber)); + this.fiber = null; } // Clean up any scheduled work / caches - this.clearTimeouts() - this.clearNotifications() // Clear notifications on reset - this.startTime = null - this.endTime = null + this.clearTimeouts(); + this.clearNotifications(); // Clear notifications on reset + this.startTime = null; + this.endTime = null; } finally { // Allow subsequent state transitions - this.isResetting = false + this.isResetting = false; } // Now that the reset flag is cleared, transition ourselves to idle - this.setState({ type: "idle" }) + this.setState({ type: 'idle' }); } async run() { try { - this.fiber = Effect.runFork(this.effect) - await Effect.runPromise(Fiber.await(this.fiber)) + this.fiber = Effect.runFork(this.effect); + await Effect.runPromise(Fiber.await(this.fiber)); } catch { // Error handling is done within the effect } finally { - this.fiber = null + this.fiber = null; } } interrupt() { - if (this.state.type === "running") { - const fiberToInterrupt = this.fiber - this.fiber = null + if (this.state.type === 'running') { + const fiberToInterrupt = this.fiber; + this.fiber = null; // Optimistically mark as interrupted so observers update immediately. // The onInterrupt handler inside the effect will confirm this later. - this.setState({ type: "interrupted" }) + this.setState({ type: 'interrupted' }); if (fiberToInterrupt) { - Effect.runFork(Fiber.interrupt(fiberToInterrupt)) + Effect.runFork(Fiber.interrupt(fiberToInterrupt)); } } } @@ -353,28 +353,34 @@ export const notify = ( options?: { duration?: number; icon?: string }, ): Effect.Effect => Effect.serviceOption(VisualEffectService).pipe( - Effect.flatMap(option => + Effect.flatMap((option) => Option.isSome(option) ? option.value.notify(message, options) : Effect.void, ), - ) + ); // Granular React hooks for better performance // Subscribe only to state changes export function useVisualEffectState(effect: VisualEffect) { - return useSyncExternalStore(effect.subscribe.bind(effect), () => effect.state) + return useSyncExternalStore(effect.subscribe.bind(effect), () => effect.state); } // Subscribe only to notification changes export function useVisualEffectNotification(effect: VisualEffect) { - return useSyncExternalStore(effect.subscribeToNotifications.bind(effect), () => - effect.getCurrentNotification(), - ) + return useSyncExternalStore( + effect.subscribeToNotifications.bind(effect), + () => effect.getCurrentNotification(), + () => effect.getCurrentNotification(), // getServerSnapshot + ); } // Subscribe for re-renders only (no return value) export function useVisualEffectSubscription(effect: VisualEffect) { - useSyncExternalStore(effect.subscribe.bind(effect), () => effect.state) + useSyncExternalStore( + effect.subscribe.bind(effect), + () => effect.state, + () => effect.state, // getServerSnapshot + ); } // Factory function - compatible with V1 signature @@ -382,4 +388,4 @@ export const visualEffect = ( name: string, effect: Effect.Effect, showTimer: boolean = false, -) => new VisualEffect(name, effect, showTimer) +) => new VisualEffect(name, effect, showTimer); diff --git a/src/components/StreamTimeline.tsx b/src/components/StreamTimeline.tsx new file mode 100644 index 0000000..b870621 --- /dev/null +++ b/src/components/StreamTimeline.tsx @@ -0,0 +1,98 @@ +import { motion } from 'motion/react'; + +export interface StreamTimelineProps { + dotCount: number; + activeIndex: number; + isComplete: boolean; + className?: string; +} + +const DOT_SIZE = 12; +const LINE_HEIGHT = 3; +const DOT_SPACING = 32; + +const COLORS = { + line: 'var(--color-neutral-700)', + dotInactive: 'var(--color-neutral-600)', + dotActive: 'var(--color-blue-400)', + dotComplete: 'var(--color-green-500)', +}; + +const ANIMATION = { + dot: { + duration: 0.3, + ease: 'easeOut', + }, + scale: { + type: 'spring', + visualDuration: 0.4, + bounce: 0.3, + }, +} as const; + +export function StreamTimeline({ + dotCount, + activeIndex, + isComplete, + className = '', +}: StreamTimelineProps) { + const timelineWidth = Math.max(0, (dotCount - 1) * DOT_SPACING); + + return ( +
+ {/* Horizontal line */} +
+ + {/* Dots */} + {Array.from({ length: dotCount }, (_, i) => { + const isActive = i === activeIndex; + const isPast = isComplete || i < activeIndex; + + let dotColor = COLORS.dotInactive; + if (isComplete) { + dotColor = COLORS.dotComplete; + } else if (isActive) { + dotColor = COLORS.dotActive; + } else if (isPast) { + dotColor = COLORS.dotComplete; + } + + return ( + + ); + })} +
+ ); +} diff --git a/src/components/Timer.tsx b/src/components/Timer.tsx index d2d7439..a0e7ed0 100644 --- a/src/components/Timer.tsx +++ b/src/components/Timer.tsx @@ -1,60 +1,66 @@ -import { useEffect, useRef, useState } from "react" -import type { VisualEffect } from "@/VisualEffect" +import { useEffect, useRef, useState } from 'react'; +import type { VisualEffect } from '@/VisualEffect'; function useTimer(task: VisualEffect) { - const [elapsedTime, setElapsedTime] = useState(0) - const intervalRef = useRef(null) + const [elapsedTime, setElapsedTime] = useState(0); + const intervalRef = useRef(null); useEffect(() => { - if (task.showTimer && task.state.type === "running" && task.startTime) { - const start = task.startTime + if (task.showTimer && task.state.type === 'running' && task.startTime) { + const start = task.startTime; const updateTimer = () => { - const now = Date.now() - setElapsedTime(now - (start ?? now)) - } + const now = Date.now(); + setElapsedTime(now - (start ?? now)); + }; - updateTimer() + updateTimer(); - intervalRef.current = window.setInterval(updateTimer, 10) + intervalRef.current = window.setInterval(updateTimer, 10); } else if (task.showTimer && task.endTime && task.startTime) { - setElapsedTime(task.endTime - task.startTime) - } else if (task.state.type === "idle") { - setElapsedTime(0) + setElapsedTime(task.endTime - task.startTime); + } else if (task.state.type === 'idle') { + setElapsedTime(0); } return () => { if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null + clearInterval(intervalRef.current); + intervalRef.current = null; } - } - }, [task.showTimer, task.state.type, task.startTime, task.endTime]) + }; + }, [task.showTimer, task.state.type, task.startTime, task.endTime]); - return elapsedTime + return elapsedTime; } -export function Timer({ effect: task }: { effect: VisualEffect }) { - const elapsedTime = useTimer(task) +export function Timer({ + effect: task, + customLabel, +}: { + effect: VisualEffect; + customLabel?: string; +}) { + const elapsedTime = useTimer(task); const shouldShowTimer = task.showTimer && - (task.state.type === "running" || - task.state.type === "completed" || - task.state.type === "failed" || - task.state.type === "interrupted" || - task.state.type === "death") + (task.state.type === 'running' || + task.state.type === 'completed' || + task.state.type === 'failed' || + task.state.type === 'interrupted' || + task.state.type === 'death'); const formatTime = (ms: number) => { if (ms < 1000) { - return `${ms}ms` + return `${ms}ms`; } else { - return `${(ms / 1000).toFixed(1)}s` + return `${(ms / 1000).toFixed(1)}s`; } - } + }; if (shouldShowTimer) { - return {formatTime(elapsedTime)} + return {formatTime(elapsedTime)}; } - return {task.name} + return {customLabel ?? task.name}; } diff --git a/src/components/effect/EffectLabel.tsx b/src/components/effect/EffectLabel.tsx index f4df0bf..7ad9b2a 100644 --- a/src/components/effect/EffectLabel.tsx +++ b/src/components/effect/EffectLabel.tsx @@ -1,31 +1,35 @@ -import { motion } from "motion/react" -import { Timer } from "@/components/Timer" -import { theme } from "@/theme" -import type { VisualEffect } from "@/VisualEffect" -import { useVisualEffectState } from "@/VisualEffect" +import { motion } from 'motion/react'; +import { Timer } from '@/components/Timer'; +import { theme } from '@/theme'; +import type { VisualEffect } from '@/VisualEffect'; +import { useVisualEffectState } from '@/VisualEffect'; interface EffectLabelProps { - effect: VisualEffect + effect: VisualEffect; + customLabel?: string; } -export function EffectLabel({ effect }: EffectLabelProps) { - const state = useVisualEffectState(effect) +export function EffectLabel({ effect, customLabel }: EffectLabelProps) { + const state = useVisualEffectState(effect); return ( - + - ) + ); } diff --git a/src/components/effect/EffectNode.tsx b/src/components/effect/EffectNode.tsx index d82fa67..6dcf07f 100644 --- a/src/components/effect/EffectNode.tsx +++ b/src/components/effect/EffectNode.tsx @@ -1,65 +1,67 @@ -import { AnimatePresence, motion } from "motion/react" -import { memo, useCallback, useState } from "react" +import { AnimatePresence, motion } from 'motion/react'; +import { memo, useCallback, useState } from 'react'; import { useVisualEffectNotification, useVisualEffectState, type VisualEffect, -} from "@/VisualEffect" -import { DeathBubble } from "../feedback/DeathBubble" -import { FailureBubble } from "../feedback/FailureBubble" -import { NotificationBubble } from "../feedback/NotificationBubble" -import { EffectContainer } from "./EffectContainer" -import { EffectContent } from "./EffectContent" -import { EffectLabel } from "./EffectLabel" -import { EffectOverlay } from "./EffectOverlay" +} from '@/VisualEffect'; +import { DeathBubble } from '../feedback/DeathBubble'; +import { FailureBubble } from '../feedback/FailureBubble'; +import { NotificationBubble } from '../feedback/NotificationBubble'; +import { EffectContainer } from './EffectContainer'; +import { EffectContent } from './EffectContent'; +import { EffectLabel } from './EffectLabel'; +import { EffectOverlay } from './EffectOverlay'; import { useEffectAnimations, useEffectMotion, useRunningAnimation, useStateAnimations, -} from "./useEffectMotion" +} from './useEffectMotion'; function EffectNodeComponent({ style = {}, effect, labelEffect, + customLabel, }: { - effect: VisualEffect - style?: React.CSSProperties - labelEffect?: VisualEffect + effect: VisualEffect; + style?: React.CSSProperties; + labelEffect?: VisualEffect; + customLabel?: string; }) { - const notification = useVisualEffectNotification(effect) - const state = useVisualEffectState(effect) - const effectMotion = useEffectMotion() - const isRunning = state.type === "running" - const isFailedOrDeath = state.type === "failed" || state.type === "death" + const notification = useVisualEffectNotification(effect); + const state = useVisualEffectState(effect); + const effectMotion = useEffectMotion(); + const isRunning = state.type === 'running'; + const isFailedOrDeath = state.type === 'failed' || state.type === 'death'; // State for error bubble visibility - const [showErrorBubble, setShowErrorBubble] = useState(false) - const [isHovering, setIsHovering] = useState(false) + const [showErrorBubble, setShowErrorBubble] = useState(false); + const [isHovering, setIsHovering] = useState(false); // Apply all animations - useRunningAnimation(isRunning, effectMotion) - useStateAnimations(state, effectMotion) - useEffectAnimations(state, effectMotion, isHovering, setShowErrorBubble) + useRunningAnimation(isRunning, effectMotion); + useStateAnimations(state, effectMotion); + useEffectAnimations(state, effectMotion, isHovering, setShowErrorBubble); // Stable mouse handlers to avoid creating new functions on every render const handleMouseEnter = useCallback(() => { - setIsHovering(true) - if (isFailedOrDeath) setShowErrorBubble(true) - }, [isFailedOrDeath]) + setIsHovering(true); + if (isFailedOrDeath) setShowErrorBubble(true); + }, [isFailedOrDeath]); const handleMouseLeave = useCallback(() => { - setIsHovering(false) - }, []) + setIsHovering(false); + }, []); return ( -
+
{/* Error bubble positioned outside container */} {isFailedOrDeath && showErrorBubble && - (state.type === "failed" ? ( + (state.type === 'failed' ? ( ) : ( @@ -69,7 +71,10 @@ function EffectNodeComponent({ {/* Notification bubbles - hidden when error bubbles are shown */} {!isFailedOrDeath && notification && ( - + )} @@ -77,10 +82,10 @@ function EffectNodeComponent({ style={{ width: effectMotion.nodeWidth, height: 64, - display: "flex", - alignItems: "center", - justifyContent: "center", - position: "relative", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + position: 'relative', }} > ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > - - + + - +
- ) + ); } -export const EffectNode = memo(EffectNodeComponent) as typeof EffectNodeComponent +export const EffectNode = memo(EffectNodeComponent) as typeof EffectNodeComponent; diff --git a/src/components/index.ts b/src/components/index.ts index 941979b..4b706fc 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,13 +1,14 @@ // Content components -export * from "./CodeBlock" +export * from './CodeBlock'; // Display components -export * from "./display" +export * from './display'; // Effect visualization -export * from "./effect" +export * from './effect'; // Feedback components -export * from "./feedback" -export * from "./HeaderView" +export * from './feedback'; +export * from './HeaderView'; // Renderers -export * from "./renderers" +export * from './renderers'; +export * from './StreamTimeline'; // UI components -export * from "./ui" +export * from './ui'; diff --git a/src/components/patterns/BorderPulsePattern.tsx b/src/components/patterns/BorderPulsePattern.tsx new file mode 100644 index 0000000..40ccc61 --- /dev/null +++ b/src/components/patterns/BorderPulsePattern.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { motion, useSpring, animate } from 'motion/react'; +import { useEffect } from 'react'; +import { timing } from '../../animations'; + +interface BorderPulsePatternProps { + size?: number; + color?: string; +} + +export function BorderPulsePattern({ + size = 64, + color = 'rgba(100, 200, 255, 0.8)', +}: BorderPulsePatternProps) { + const borderOpacity = useSpring(1, { stiffness: 180, damping: 25 }); + + useEffect(() => { + const anim = animate(borderOpacity, [...timing.borderPulse.values], { + duration: timing.borderPulse.duration, + ease: 'easeInOut', + repeat: Infinity, + }); + + return () => { + anim.stop(); + }; + }, [borderOpacity]); + + return ( +
+
+ +
+ ); +} diff --git a/src/components/patterns/CardFaceBack.tsx b/src/components/patterns/CardFaceBack.tsx new file mode 100644 index 0000000..bb4bc92 --- /dev/null +++ b/src/components/patterns/CardFaceBack.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { motion } from 'motion/react'; + +// CardFaceBack: Face-down card showing mystery/locked state +// Used for unrevealed chunks in the stack (face-down state) +export interface CardFaceBackProps { + size?: { width: number; height: number }; +} + +export function CardFaceBack({ size = { width: 64, height: 88 } }: CardFaceBackProps) { + return ( + + {/* Diagonal lines pattern overlay */} + + + + + + + + + + {/* Large question mark watermark */} +
+ ? +
+ + {/* Lock icon in center */} +
🔒
+
+ ); +} diff --git a/src/components/patterns/CardFaceFront.tsx b/src/components/patterns/CardFaceFront.tsx new file mode 100644 index 0000000..adf877f --- /dev/null +++ b/src/components/patterns/CardFaceFront.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { motion } from 'motion/react'; + +// CardFaceFront: Playing card aspect ratio showing the chunk value +// Used for revealed chunks in the stack (face-up state) +export interface CardFaceFrontProps { + value: number; + size?: { width: number; height: number }; +} + +export function CardFaceFront({ value, size = { width: 64, height: 88 } }: CardFaceFrontProps) { + return ( + + {/* Optional small value label in top-left */} +
+ {value} +
+ + {/* Main centered number */} +
{value}
+
+ ); +} diff --git a/src/components/patterns/CardFlip.tsx b/src/components/patterns/CardFlip.tsx new file mode 100644 index 0000000..8eae980 --- /dev/null +++ b/src/components/patterns/CardFlip.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { motion } from 'motion/react'; +import type { ReactNode } from 'react'; +import { useEffect, useState } from 'react'; + +// CardFlip: Two-sided card that flips on Y-axis +// Handles the flip animation and visibility swap at 90deg +export interface CardFlipProps { + isFlipped: boolean; + frontFace: ReactNode; + backFace: ReactNode; + size?: { width: number; height: number }; + onFlipComplete?: () => void; + showPulsingBorder?: boolean; +} + +export function CardFlip({ + isFlipped, + frontFace, + backFace, + size = { width: 64, height: 88 }, + onFlipComplete, + showPulsingBorder = false, +}: CardFlipProps) { + const [showFront, setShowFront] = useState(!isFlipped); + + useEffect(() => { + // At 90deg (halfway through flip), swap which face is visible + const timeout = setTimeout( + () => { + setShowFront(isFlipped); + }, + 250, // Half of 500ms animation + ); + + return () => clearTimeout(timeout); + }, [isFlipped]); + + return ( +
+ { + if (onFlipComplete) { + onFlipComplete(); + } + }} + > + {/* Pulsing border - inside the rotating container */} + {showPulsingBorder && ( + + )} + + {/* Back face (shown when not flipped) */} + + {backFace} + + + {/* Front face (shown when flipped) */} + + {frontFace} + + + {/* Glow burst on flip completion - inside rotating container */} + {isFlipped && ( + + )} + +
+ ); +} diff --git a/src/components/patterns/ChunkCardStack.tsx b/src/components/patterns/ChunkCardStack.tsx new file mode 100644 index 0000000..eddbbff --- /dev/null +++ b/src/components/patterns/ChunkCardStack.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { motion } from 'motion/react'; + +interface ChunkCardStackProps { + items: number[]; + pulling?: boolean; + pushing?: boolean; + label?: string; +} + +export function ChunkCardStack({ + items, + pulling = false, + pushing = false, + label = 'Source Queue', +}: ChunkCardStackProps) { + return ( +
+
{label}
+
+ {items.length > 0 ? ( +
+ {items.map((num, idx) => { + const isBottom = idx === items.length - 1; + const isTop = idx === 0; + // Stack from bottom up with overlap + const bottomOffset = idx * 12; // 12px offset per card + return ( + +
+ {num} + {isBottom && ( + + )} +
+
+ ); + })} +
+ ) : ( +
+ Empty +
+ )} +
+
+ ); +} diff --git a/src/components/patterns/CompletionCheckPattern.tsx b/src/components/patterns/CompletionCheckPattern.tsx new file mode 100644 index 0000000..dbbd358 --- /dev/null +++ b/src/components/patterns/CompletionCheckPattern.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { StarFourIcon } from '@phosphor-icons/react'; +import { motion, useSpring, animate } from 'motion/react'; +import { useLayoutEffect } from 'react'; +import { springs } from '../../animations'; + +interface CompletionCheckPatternProps { + size?: number; +} + +export function CompletionCheckPattern({ size = 64 }: CompletionCheckPatternProps) { + const contentScale = useSpring(1, springs.default); + + useLayoutEffect(() => { + contentScale.set(0); + animate(contentScale, [1.3, 1], springs.contentScale); + }, [contentScale]); + + return ( + + + + ); +} diff --git a/src/components/patterns/DeathGlitchPattern.tsx b/src/components/patterns/DeathGlitchPattern.tsx new file mode 100644 index 0000000..4eabdcd --- /dev/null +++ b/src/components/patterns/DeathGlitchPattern.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { SkullIcon } from '@phosphor-icons/react'; +import { motion, useSpring, useMotionValue } from 'motion/react'; +import { useEffect } from 'react'; +import { timing, effects } from '../../animations'; + +interface DeathGlitchPatternProps { + size?: number; +} + +export function DeathGlitchPattern({ size = 64 }: DeathGlitchPatternProps) { + const contentScale = useSpring(1, { stiffness: 180, damping: 25 }); + const glowIntensity = useSpring(0, { stiffness: 180, damping: 25 }); + + useEffect(() => { + let cancelled = false; + let timeoutId: ReturnType | null = null; + + const scheduleIdle = (cb: () => void, delay: number) => { + timeoutId = setTimeout(() => { + const win = window as Window & { requestIdleCallback?: (cb: () => void) => number }; + if (typeof win.requestIdleCallback === 'function') { + win.requestIdleCallback(cb); + } else { + cb(); + } + }, delay); + }; + + const glitchSequence = async () => { + const t = timing.glitch; + const e = effects.glitch; + + // initial pulses + for (let i = 0; i < t.initialCount && !cancelled; i++) { + contentScale.set(1 + Math.random() * e.scaleRange); + glowIntensity.set(Math.random() * e.intensePulseMax); + + await new Promise((resolve) => { + scheduleIdle( + resolve, + t.initialDelayMin + Math.random() * Math.max(0, t.initialDelayMax - t.initialDelayMin), + ); + }); + + if (cancelled) break; + contentScale.set(1); + glowIntensity.set(e.glowMax); + + await new Promise((resolve) => { + scheduleIdle(resolve, t.pauseMin + Math.random() * Math.max(0, t.pauseMax - t.pauseMin)); + }); + } + + // subtle loop + const subtle = () => { + if (cancelled) return; + glowIntensity.set(e.glowMin + Math.random() * (e.glowMax - e.glowMin)); + scheduleIdle( + subtle, + t.subtleDelayMin + Math.random() * Math.max(0, t.subtleDelayMax - t.subtleDelayMin), + ); + }; + subtle(); + }; + + glitchSequence(); + + return () => { + cancelled = true; + if (timeoutId) clearTimeout(timeoutId); + glowIntensity.set(0); + }; + }, [contentScale, glowIntensity]); + + return ( + + + + ); +} diff --git a/src/components/patterns/FailureShakePattern.tsx b/src/components/patterns/FailureShakePattern.tsx new file mode 100644 index 0000000..04b1f2a --- /dev/null +++ b/src/components/patterns/FailureShakePattern.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { SkullIcon } from '@phosphor-icons/react'; +import { motion, useMotionValue, animate } from 'motion/react'; +import { useEffect } from 'react'; +import { shake } from '../../animations'; + +interface FailureShakePatternProps { + size?: number; +} + +export function FailureShakePattern({ size = 64 }: FailureShakePatternProps) { + const shakeX = useMotionValue(0); + const shakeY = useMotionValue(0); + const rotation = useMotionValue(0); + + useEffect(() => { + let cancelled = false; + + const shakeSequence = async () => { + const { intensity, duration, count, rotationRange, returnDuration } = shake.failure; + + for (let i = 0; i < count && !cancelled; i++) { + const xOffset = (Math.random() - 0.5) * intensity; + const yOffset = (Math.random() - 0.5) * intensity; + const rotOffset = (Math.random() - 0.5) * rotationRange; + + const anims = [ + animate(shakeX, xOffset, { duration, ease: 'easeInOut' }), + animate(shakeY, yOffset, { duration, ease: 'easeInOut' }), + animate(rotation, rotOffset, { duration, ease: 'easeInOut' }), + ]; + await Promise.all(anims.map((a) => a.finished)); + } + + if (!cancelled) { + await Promise.all([ + animate(shakeX, 0, { duration: returnDuration, ease: 'easeOut' }).finished, + animate(shakeY, 0, { duration: returnDuration, ease: 'easeOut' }).finished, + animate(rotation, 0, { duration: returnDuration, ease: 'easeOut' }).finished, + ]); + } + }; + + shakeSequence(); + + return () => { + cancelled = true; + }; + }, [shakeX, shakeY, rotation]); + + return ( + + + + ); +} diff --git a/src/components/patterns/FlashPattern.tsx b/src/components/patterns/FlashPattern.tsx new file mode 100644 index 0000000..43b3342 --- /dev/null +++ b/src/components/patterns/FlashPattern.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { motion, useMotionValue, animate } from 'motion/react'; +import { useEffect } from 'react'; +import { timing, colors } from '../../animations'; + +interface FlashPatternProps { + size?: number; +} + +export function FlashPattern({ size = 64 }: FlashPatternProps) { + const flashOpacity = useMotionValue(0); + + useEffect(() => { + const up = animate(flashOpacity, 0.6, { + duration: 0.02, + ease: 'circOut', + }); + up.finished.then(() => { + animate(flashOpacity, 0, { + duration: timing.flash.duration, + ease: timing.flash.ease, + }); + }); + }, [flashOpacity]); + + return ( +
+
+ +
+ ); +} diff --git a/src/components/patterns/GlowPulsePattern.tsx b/src/components/patterns/GlowPulsePattern.tsx new file mode 100644 index 0000000..9e3455a --- /dev/null +++ b/src/components/patterns/GlowPulsePattern.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { motion, useSpring, animate, useTransform } from 'motion/react'; +import { useEffect } from 'react'; +import { timing, colors } from '../../animations'; + +interface GlowPulsePatternProps { + size?: number; + color?: string; +} + +export function GlowPulsePattern({ + size = 64, + color = colors.glow.running, +}: GlowPulsePatternProps) { + const glowIntensity = useSpring(0, { stiffness: 180, damping: 25 }); + + useEffect(() => { + const anim = animate(glowIntensity, [...timing.glowPulse.values], { + duration: timing.glowPulse.duration, + ease: 'easeInOut', + repeat: Infinity, + }); + + return () => { + anim.stop(); + }; + }, [glowIntensity]); + + const boxShadow = useTransform([glowIntensity], ([glow = 0]: Array) => { + const cappedGlow = Math.min(glow, 8); + return cappedGlow > 0 ? `0 0 ${cappedGlow}px ${color}` : 'none'; + }); + + return ( + + ); +} diff --git a/src/components/patterns/IdleStatePattern.tsx b/src/components/patterns/IdleStatePattern.tsx new file mode 100644 index 0000000..eac8486 --- /dev/null +++ b/src/components/patterns/IdleStatePattern.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { StarFourIcon } from '@phosphor-icons/react'; +import { motion } from 'motion/react'; +import { TASK_COLORS } from '../../constants/colors'; + +interface IdleStatePatternProps { + size?: number; +} + +export function IdleStatePattern({ size = 64 }: IdleStatePatternProps) { + return ( + + + + ); +} diff --git a/src/components/patterns/JitterPattern.tsx b/src/components/patterns/JitterPattern.tsx new file mode 100644 index 0000000..1acd37e --- /dev/null +++ b/src/components/patterns/JitterPattern.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { motion, useMotionValue, useTransform, useVelocity, animate } from 'motion/react'; +import { useEffect } from 'react'; +import { shake } from '../../animations'; + +interface JitterPatternProps { + size?: number; +} + +export function JitterPattern({ size = 64 }: JitterPatternProps) { + const rotation = useMotionValue(0); + const shakeX = useMotionValue(0); + const shakeY = useMotionValue(0); + + const rotationVelocity = useVelocity(rotation); + const blurAmount = useTransform(rotationVelocity, [-100, 0, 100], [1, 0, 1], { clamp: true }); + + useEffect(() => { + let cancelled = false; + let rafId: number | null = null; + + const jitter = () => { + if (cancelled) return; + + const angle = + (Math.random() * shake.running.angleRange + shake.running.angleBase) * + (Math.random() < 0.5 ? 1 : -1); + + const offset = + (Math.random() * shake.running.offsetRange + shake.running.offsetBase) * + (Math.random() < 0.5 ? -1 : 1); + + const offsetY = + (Math.random() * shake.running.offsetYRange + shake.running.offsetYBase) * + (Math.random() < 0.5 ? -1 : 1); + + const min = shake.running.durationMin; + const max = shake.running.durationMax ?? min * 2; + const duration = min + Math.random() * Math.max(0.001, max - min); + + const rot = animate(rotation, angle, { duration, ease: 'circInOut' }); + const x = animate(shakeX, offset, { duration, ease: 'easeInOut' }); + const y = animate(shakeY, offsetY, { duration, ease: 'easeInOut' }); + + Promise.all([rot.finished, x.finished, y.finished]).then(() => { + if (cancelled) return; + rafId = requestAnimationFrame(jitter); + }); + }; + + rafId = requestAnimationFrame(jitter); + + return () => { + cancelled = true; + if (rafId !== null) cancelAnimationFrame(rafId); + }; + }, [rotation, shakeX, shakeY]); + + return ( + ) => { + const cappedBlur = Math.min(blur, 2); + return `blur(${cappedBlur}px)`; + }), + }} + > + Jitter + + ); +} diff --git a/src/components/patterns/LightSweepPattern.tsx b/src/components/patterns/LightSweepPattern.tsx new file mode 100644 index 0000000..e5520b5 --- /dev/null +++ b/src/components/patterns/LightSweepPattern.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { motion } from 'motion/react'; + +interface LightSweepPatternProps { + size?: number; +} + +export function LightSweepPattern({ size = 64 }: LightSweepPatternProps) { + return ( +
+ {[0, 0.2, 0.4, 0.6, 0.8, 1].map((delay, i) => ( + + ))} +
+ ); +} diff --git a/src/components/patterns/RetryTimelinePattern.tsx b/src/components/patterns/RetryTimelinePattern.tsx new file mode 100644 index 0000000..1848dd2 --- /dev/null +++ b/src/components/patterns/RetryTimelinePattern.tsx @@ -0,0 +1,292 @@ +/** + * RetryTimelinePattern - Complete retry visualization + * Shows multiple retry attempts with increasing gaps (exponential backoff) + */ + +import { motion } from "motion/react" +import { useState } from "react" +import { TimelineSegmentPattern } from "./TimelineSegmentPattern" +import { TimelineTicksPattern } from "./TimelineTicksPattern" + +export interface RetryAttempt { + attemptDuration: number // How long the attempt ran + delayAfter?: number // Delay before next attempt + failed?: boolean +} + +export interface RetryTimelinePatternProps { + attempts: RetryAttempt[] + pixelsPerSecond?: number + height?: number + showTicks?: boolean + className?: string +} + +const DEFAULT_PIXELS_PER_SECOND = 100 +const DEFAULT_HEIGHT = 80 +const SEGMENT_Y = "50%" +const LINE_HEIGHT = 3 +const DOT_SIZE = 12 + +const COLORS = { + attempt: "var(--color-blue-500)", + gap: "var(--color-neutral-500)", + failed: "var(--color-red-500)", +} + +export function RetryTimelinePattern({ + attempts, + pixelsPerSecond = DEFAULT_PIXELS_PER_SECOND, + height = DEFAULT_HEIGHT, + showTicks = true, + className = "", +}: RetryTimelinePatternProps) { + // Calculate segment positions + let currentX = 50 // Start offset + const segments: Array<{ + type: "attempt" | "gap" + startX: number + endX: number + duration: number + failed?: boolean + }> = [] + type SegmentKey = `${number}-${"attempt" | "gap"}` + + for (const attempt of attempts) { + // Attempt segment + const attemptWidth = (attempt.attemptDuration / 1000) * pixelsPerSecond + segments.push({ + type: "attempt", + startX: currentX, + endX: currentX + attemptWidth, + duration: attempt.attemptDuration, + failed: attempt.failed, + }) + currentX += attemptWidth + + // Gap segment (if there's a delay after this attempt) + if (attempt.delayAfter !== undefined) { + const gapWidth = (attempt.delayAfter / 1000) * pixelsPerSecond + segments.push({ + type: "gap", + startX: currentX, + endX: currentX + gapWidth, + duration: attempt.delayAfter, + }) + currentX += gapWidth + } + } + + const totalWidth = currentX + 100 // Add padding at the end + + return ( +
+
+ {/* Background ticks */} + {showTicks && } + + {/* Background line */} +
+ + {/* Segments */} + {segments.map((segment, i) => { + const key: SegmentKey = `${i}-${segment.type}` + if (segment.type === "attempt") { + const color = segment.failed ? COLORS.failed : COLORS.attempt + return ( + + ) + } else { + // Gap segment + return ( + + ) + } + })} + + {/* Terminator bar at the end */} + +
+
+ ) +} + +/** + * Interactive retry timeline demo with controls + */ +export interface InteractiveRetryTimelineDemoProps { + className?: string +} + +export function InteractiveRetryTimelineDemo({ + className = "", +}: InteractiveRetryTimelineDemoProps) { + const [retryCount, setRetryCount] = useState(3) + const [delayType, setDelayType] = useState<"exponential" | "fixed">("exponential") + const [baseDelay, setBaseDelay] = useState(500) + const [isRunning, setIsRunning] = useState(false) + const [currentAttempts, setCurrentAttempts] = useState([]) + + const handleStart = () => { + setIsRunning(true) + const attempts: RetryAttempt[] = [] + + // Initial failed attempt + attempts.push({ + attemptDuration: 200, + delayAfter: baseDelay, + failed: true, + }) + + // Retry attempts + for (let i = 0; i < retryCount; i++) { + const delay = delayType === "exponential" ? baseDelay * 2 ** i : baseDelay + + const isLastAttempt = i === retryCount - 1 + attempts.push({ + attemptDuration: 200, + delayAfter: isLastAttempt ? undefined : delay, + failed: !isLastAttempt, // Last attempt succeeds + }) + } + + setCurrentAttempts(attempts) + } + + const handleReset = () => { + setIsRunning(false) + setCurrentAttempts([]) + } + + return ( +
+ {/* Controls */} +
+
+ + +
+ +
+ + setRetryCount(Number.parseInt(e.target.value, 10))} + disabled={isRunning} + className="w-16 px-2 py-1 bg-neutral-700 border border-neutral-600 rounded text-sm disabled:opacity-50" + /> +
+ +
+ + +
+ +
+ + setBaseDelay(Number.parseInt(e.target.value, 10))} + disabled={isRunning} + className="w-20 px-2 py-1 bg-neutral-700 border border-neutral-600 rounded text-sm disabled:opacity-50" + /> +
+
+ + {/* Timeline */} + {currentAttempts.length > 0 && ( +
+ +
+ )} + + {/* Status */} +
+ {isRunning ? `Running ${retryCount} retries with ${delayType} backoff` : "Ready to start"} +
+
+ ) +} diff --git a/src/components/patterns/RunningStatePattern.tsx b/src/components/patterns/RunningStatePattern.tsx new file mode 100644 index 0000000..21cdd1e --- /dev/null +++ b/src/components/patterns/RunningStatePattern.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { + motion, + useSpring, + useMotionValue, + useTransform, + useVelocity, + animate, +} from 'motion/react'; +import { useEffect } from 'react'; +import { timing, colors, shake } from '../../animations'; +import { TASK_COLORS } from '../../constants/colors'; + +interface RunningStatePatternProps { + size?: number; +} + +export function RunningStatePattern({ size = 64 }: RunningStatePatternProps) { + // All motion values + const borderOpacity = useSpring(1, { stiffness: 180, damping: 25 }); + const glowIntensity = useSpring(0, { stiffness: 180, damping: 25 }); + const rotation = useMotionValue(0); + const shakeX = useMotionValue(0); + const shakeY = useMotionValue(0); + const nodeHeight = useMotionValue(size); + const borderRadius = useSpring(8, { stiffness: 180, damping: 25 }); + + const rotationVelocity = useVelocity(rotation); + const blurAmount = useTransform(rotationVelocity, [-100, 0, 100], [1, 0, 1], { clamp: true }); + + // Shape morph + useEffect(() => { + animate(nodeHeight, size * 0.4, { + duration: 0.4, + bounce: 0.3, + type: 'spring', + }); + borderRadius.set(15); + }, [nodeHeight, borderRadius, size]); + + // Border + glow pulses + useEffect(() => { + const borderAnim = animate(borderOpacity, [...timing.borderPulse.values], { + duration: timing.borderPulse.duration, + ease: 'easeInOut', + repeat: Infinity, + }); + + const glowAnim = animate(glowIntensity, [...timing.glowPulse.values], { + duration: timing.glowPulse.duration, + ease: 'easeInOut', + repeat: Infinity, + }); + + return () => { + borderAnim.stop(); + glowAnim.stop(); + }; + }, [borderOpacity, glowIntensity]); + + // Jitter + useEffect(() => { + let cancelled = false; + let rafId: number | null = null; + + const jitter = () => { + if (cancelled) return; + + const angle = + (Math.random() * shake.running.angleRange + shake.running.angleBase) * + (Math.random() < 0.5 ? 1 : -1); + + const offset = + (Math.random() * shake.running.offsetRange + shake.running.offsetBase) * + (Math.random() < 0.5 ? -1 : 1); + + const offsetY = + (Math.random() * shake.running.offsetYRange + shake.running.offsetYBase) * + (Math.random() < 0.5 ? -1 : 1); + + const min = shake.running.durationMin; + const max = shake.running.durationMax ?? min * 2; + const duration = min + Math.random() * Math.max(0.001, max - min); + + const rot = animate(rotation, angle, { duration, ease: 'circInOut' }); + const x = animate(shakeX, offset, { duration, ease: 'easeInOut' }); + const y = animate(shakeY, offsetY, { duration, ease: 'easeInOut' }); + + Promise.all([rot.finished, x.finished, y.finished]).then(() => { + if (cancelled) return; + rafId = requestAnimationFrame(jitter); + }); + }; + + rafId = requestAnimationFrame(jitter); + + return () => { + cancelled = true; + if (rafId !== null) cancelAnimationFrame(rafId); + }; + }, [rotation, shakeX, shakeY]); + + const boxShadow = useTransform([glowIntensity], ([glow = 0]: Array) => { + const cappedGlow = Math.min(glow, 8); + return cappedGlow > 0 ? `0 0 ${cappedGlow}px ${colors.glow.running}` : 'none'; + }); + + return ( + ) => { + const cappedBlur = Math.min(blur, 2); + return `blur(${cappedBlur}px)`; + }), + }} + > + {/* Border overlay */} + + + {/* Light sweeps */} + {[0, 0.2, 0.4, 0.6, 0.8, 1].map((delay, i) => ( + + ))} + + ); +} diff --git a/src/components/patterns/ShapeMorphPattern.tsx b/src/components/patterns/ShapeMorphPattern.tsx new file mode 100644 index 0000000..fa8ac05 --- /dev/null +++ b/src/components/patterns/ShapeMorphPattern.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { motion } from 'motion/react'; + +interface ShapeMorphPatternProps { + size?: number; +} + +export function ShapeMorphPattern({ size = 64 }: ShapeMorphPatternProps) { + return ( +
+ +
+ ); +} diff --git a/src/components/patterns/StackedCard.tsx b/src/components/patterns/StackedCard.tsx new file mode 100644 index 0000000..2d9697f --- /dev/null +++ b/src/components/patterns/StackedCard.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { motion } from 'motion/react'; +import { CardFaceBack } from './CardFaceBack'; +import { CardFaceFront } from './CardFaceFront'; +import { CardFlip } from './CardFlip'; + +// StackedCard: Card positioned within a stack with offset and rotation +// Combines CardFlip with stack positioning logic +export interface StackedCardProps { + value: number; + stackIndex: number; + totalInStack: number; + isFlipped: boolean; + pulling?: boolean; + pushing?: boolean; + size?: { width: number; height: number }; +} + +export function StackedCard({ + value, + stackIndex, + totalInStack, + isFlipped, + pulling = false, + pushing = false, + size = { width: 64, height: 88 }, +}: StackedCardProps) { + // Calculate offset - each card is slightly offset from the one below + const offsetY = stackIndex * 10; + + // Small random tilt based on index for natural stack feel + // Use deterministic rotation based on value to keep consistent + const rotation = ((value * 37) % 7) - 3; // Range: -3° to +3° + + // Bottom card (stackIndex === 0) gets pulsing highlight + const isBottom = stackIndex === 0; + + // Pulling animation: slide right before flip + const pullingX = pulling ? 40 : 0; + + // Pushing animation: slide in from above with rotation + const pushingY = pushing ? -80 : 0; + const pushingRotation = pushing ? -15 : 0; + + return ( + + + } + backFace={} + size={size} + showPulsingBorder={isBottom} + /> + + ); +} diff --git a/src/components/patterns/TimelineDotPattern.tsx b/src/components/patterns/TimelineDotPattern.tsx new file mode 100644 index 0000000..64f164d --- /dev/null +++ b/src/components/patterns/TimelineDotPattern.tsx @@ -0,0 +1,65 @@ +/** + * TimelineDotPattern - Individual dot marker on timeline + * Can be start, middle, or end markers with different states + */ + +import { motion } from "motion/react" + +export interface TimelineDotPatternProps { + x: number + y?: string + size?: number + color?: string + state?: "idle" | "active" | "complete" + className?: string +} + +const DEFAULT_SIZE = 12 +const DEFAULT_Y = "50%" +const DEFAULT_COLORS = { + idle: "var(--color-neutral-500)", + active: "var(--color-blue-400)", + complete: "var(--color-blue-500)", +} + +const SPRING_CONFIG = { + type: "spring" as const, + visualDuration: 0.5, + bounce: 0.4, +} + +export function TimelineDotPattern({ + x, + y = DEFAULT_Y, + size = DEFAULT_SIZE, + color, + state = "idle", + className = "", +}: TimelineDotPatternProps) { + const dotColor = color || DEFAULT_COLORS[state] + const scale = state === "active" ? 1.2 : 1 + + return ( + + ) +} diff --git a/src/components/patterns/TimelineSegmentPattern.tsx b/src/components/patterns/TimelineSegmentPattern.tsx new file mode 100644 index 0000000..fbee7eb --- /dev/null +++ b/src/components/patterns/TimelineSegmentPattern.tsx @@ -0,0 +1,107 @@ +/** + * TimelineSegmentPattern - Single timeline segment (line with start/end dots) + * Shows duration label above the segment + */ + +import { motion } from "motion/react" +import { TimelineDotPattern } from "./TimelineDotPattern" + +export interface TimelineSegmentPatternProps { + startX: number + endX: number + y?: string + lineHeight?: number + color?: string + dotSize?: number + showDots?: boolean + showDuration?: boolean + duration?: number + state?: "idle" | "active" | "complete" + className?: string +} + +const DEFAULT_Y = "50%" +const DEFAULT_LINE_HEIGHT = 3 +const DEFAULT_DOT_SIZE = 12 +const DEFAULT_COLORS = { + idle: "var(--color-neutral-500)", + active: "var(--color-blue-400)", + complete: "var(--color-blue-500)", +} + +function formatTime(ms: number): string { + return `${ms}ms` +} + +export function TimelineSegmentPattern({ + startX, + endX, + y = DEFAULT_Y, + lineHeight = DEFAULT_LINE_HEIGHT, + color, + dotSize = DEFAULT_DOT_SIZE, + showDots = true, + showDuration = false, + duration, + state = "idle", + className = "", +}: TimelineSegmentPatternProps) { + const width = endX - startX + const segmentColor = color || DEFAULT_COLORS[state] + const midX = startX + width / 2 + + return ( +
+ {/* Line */} + + + {/* Start dot */} + {showDots && ( + + )} + + {/* End dot */} + {showDots && ( + + )} + + {/* Duration label */} + {showDuration && duration !== undefined && width > 50 && ( +
+ + {formatTime(duration)} + +
+ )} +
+ ) +} diff --git a/src/components/patterns/TimelineTicksPattern.tsx b/src/components/patterns/TimelineTicksPattern.tsx new file mode 100644 index 0000000..2d0723c --- /dev/null +++ b/src/components/patterns/TimelineTicksPattern.tsx @@ -0,0 +1,53 @@ +/** + * TimelineTicksPattern - Background tick marks for timeline visualization + * Shows vertical lines at regular intervals to mark time progression + */ + +export interface TimelineTicksPatternProps { + width: number + height?: number + spacing?: number + tickWidth?: number + color?: string + className?: string +} + +const DEFAULT_HEIGHT = 50 +const DEFAULT_SPACING = 50 +const DEFAULT_TICK_WIDTH = 1 +const DEFAULT_COLOR = "var(--color-neutral-800)" + +export function TimelineTicksPattern({ + width, + height = DEFAULT_HEIGHT, + spacing = DEFAULT_SPACING, + tickWidth = DEFAULT_TICK_WIDTH, + color = DEFAULT_COLOR, + className = "", +}: TimelineTicksPatternProps) { + const tickCount = Math.ceil(width / spacing) + const ticks: React.ReactElement[] = [] + + for (let i = 1; i <= tickCount; i++) { + const x = i * spacing + ticks.push( +
, + ) + } + + return ( +
+ {ticks} +
+ ) +} diff --git a/src/components/patterns/index.ts b/src/components/patterns/index.ts new file mode 100644 index 0000000..5a3dc8e --- /dev/null +++ b/src/components/patterns/index.ts @@ -0,0 +1,20 @@ +export { BorderPulsePattern } from './BorderPulsePattern'; +export { ChunkCardStack } from './ChunkCardStack'; +export { GlowPulsePattern } from './GlowPulsePattern'; +export { JitterPattern } from './JitterPattern'; +export { LightSweepPattern } from './LightSweepPattern'; +export { ShapeMorphPattern } from './ShapeMorphPattern'; +export { CompletionCheckPattern } from './CompletionCheckPattern'; +export { FailureShakePattern } from './FailureShakePattern'; +export { DeathGlitchPattern } from './DeathGlitchPattern'; +export { FlashPattern } from './FlashPattern'; +export { IdleStatePattern } from './IdleStatePattern'; +export { RunningStatePattern } from './RunningStatePattern'; +export { TimelineTicksPattern } from './TimelineTicksPattern'; +export { TimelineDotPattern } from './TimelineDotPattern'; +export { TimelineSegmentPattern } from './TimelineSegmentPattern'; +export { RetryTimelinePattern, InteractiveRetryTimelineDemo } from './RetryTimelinePattern'; +export { CardFaceFront } from './CardFaceFront'; +export { CardFaceBack } from './CardFaceBack'; +export { CardFlip } from './CardFlip'; +export { StackedCard } from './StackedCard'; diff --git a/src/components/playground/StreamPullPrototype.tsx b/src/components/playground/StreamPullPrototype.tsx new file mode 100644 index 0000000..0481235 --- /dev/null +++ b/src/components/playground/StreamPullPrototype.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { motion } from 'motion/react'; +import { useEffect, useState } from 'react'; +import { ChunkCardStack } from '../patterns/ChunkCardStack'; + +export function StreamPullPrototype() { + const [queue, setQueue] = useState([]); + const [consumed, setConsumed] = useState([]); + const [pulling, setPulling] = useState(false); + const [isInitializing, setIsInitializing] = useState(true); + + // Initialize queue by adding items one at a time + // Array order: [newest/top=5, ..., oldest/bottom=1] + // We add from oldest to newest, so 1 goes in first (will be at bottom) + useEffect(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); // Add to front (top of stack) + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + + return () => clearInterval(interval); + }, []); + + const handlePull = () => { + if (queue.length === 0 || pulling) return; + + setPulling(true); + + // Animate the pull - pull from bottom (last item) + setTimeout(() => { + const next = queue[queue.length - 1]; + setConsumed((prev) => [...prev, next]); + setQueue(queue.slice(0, -1)); + setPulling(false); + }, 400); + }; + + const handleReset = () => { + setQueue([]); + setConsumed([]); + setPulling(false); + setIsInitializing(true); + + // Re-initialize + setTimeout(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + }, 100); + }; + + const isExhausted = queue.length === 0 && !isInitializing; + + return ( +
+ {/* Visual Layout */} +
+ + + + {pulling && } +
+ + {/* Controls */} +
+ +
+
+ Queue: {queue.length} | Consumed: {consumed.length} +
+
+
+ ); +} + +function PullLever({ + onPull, + active, + pulling, +}: { + onPull: () => void; + active: boolean; + pulling: boolean; +}) { + return ( +
+ + {pulling ? '⚡' : '⬇'} + {active && !pulling && ( + + )} + +
PULL
+
+ {active ? 'Active' : 'Exhausted'} +
+
+ ); +} + +function ConsumedRail({ items }: { items: number[] }) { + return ( +
+
Consumed
+
+ {items.length > 0 ? ( + items.map((num, idx) => ( + +
+ {num} +
+
+ )) + ) : ( +
+ ∅ +
+ )} +
+
+ ); +} + +function TransferBeam() { + return ( + + ); +} diff --git a/src/components/playground/StreamPullPrototypeV2.tsx b/src/components/playground/StreamPullPrototypeV2.tsx new file mode 100644 index 0000000..eb1bf84 --- /dev/null +++ b/src/components/playground/StreamPullPrototypeV2.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { motion } from 'motion/react'; +import { useEffect, useState } from 'react'; +import { CardFaceFront } from '../patterns/CardFaceFront'; +import { CardFaceBack } from '../patterns/CardFaceBack'; +import { StackedCard } from '../patterns/StackedCard'; + +export function StreamPullPrototypeV2() { + const [queue, setQueue] = useState([]); + const [consumed, setConsumed] = useState([]); + const [flippedCards, setFlippedCards] = useState>(new Set()); + const [pulling, setPulling] = useState(false); + const [isInitializing, setIsInitializing] = useState(true); + + // Initialize queue by adding items one at a time + useEffect(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); // Add to front (top of stack) + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + + return () => clearInterval(interval); + }, []); + + const handlePull = () => { + if (queue.length === 0 || pulling) return; + + setPulling(true); + const bottomCard = queue[queue.length - 1]; + + // Remove from queue immediately to prevent double-pull + setQueue(queue.slice(0, -1)); + + // First flip the card + setFlippedCards((prev) => new Set([...prev, bottomCard])); + + // After flip animation, add to consumed + setTimeout(() => { + setConsumed((prev) => [...prev, bottomCard]); + setPulling(false); + }, 800); // 600ms flip + 200ms buffer + }; + + const handleReset = () => { + setQueue([]); + setConsumed([]); + setFlippedCards(new Set()); + setPulling(false); + setIsInitializing(true); + + // Re-initialize + setTimeout(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + }, 100); + }; + + const isExhausted = queue.length === 0 && !isInitializing; + + return ( +
+ {/* Visual Layout */} +
+ {/* Card Stack */} +
+
Source Queue
+
+ {queue.length > 0 ? ( +
+ {queue.map((value, idx) => ( + + ))} +
+ ) : ( +
+ Empty +
+ )} +
+
+ + + + {pulling && } +
+ + {/* Controls */} +
+ +
+
+ Queue: {queue.length} | Consumed: {consumed.length} +
+
+
+ ); +} + +function PullLever({ + onPull, + active, + pulling, +}: { + onPull: () => void; + active: boolean; + pulling: boolean; +}) { + return ( +
+ + {pulling ? '⚡' : '⬇'} + {active && !pulling && ( + + )} + +
PULL
+
+ {active ? 'Active' : 'Exhausted'} +
+
+ ); +} + +function ConsumedRail({ items }: { items: number[] }) { + return ( +
+
Consumed
+
+ {items.length > 0 ? ( + items.map((num, idx) => ( + + + + )) + ) : ( +
+ ∅ +
+ )} +
+
+ ); +} + +function TransferBeam() { + return ( + + ); +} diff --git a/src/components/playground/StreamPushPullPrototype.tsx b/src/components/playground/StreamPushPullPrototype.tsx new file mode 100644 index 0000000..ee3eeaa --- /dev/null +++ b/src/components/playground/StreamPushPullPrototype.tsx @@ -0,0 +1,322 @@ +'use client'; + +import { motion } from 'motion/react'; +import { useEffect, useState } from 'react'; +import { ChunkCardStack } from '../patterns/ChunkCardStack'; + +export function StreamPushPullPrototype() { + const [queue, setQueue] = useState([]); + const [consumed, setConsumed] = useState([]); + const [pulling, setPulling] = useState(false); + const [pushing, setPushing] = useState(false); + const [nextValue, setNextValue] = useState(6); + const [isInitializing, setIsInitializing] = useState(true); + + // Initialize queue by adding items one at a time + // Array order: [newest/top=5, ..., oldest/bottom=1] + // We add from oldest to newest, so 1 goes in first (will be at bottom) + useEffect(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); // Add to front (top of stack) + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + + return () => clearInterval(interval); + }, []); + + const handlePull = () => { + if (queue.length === 0 || pulling) return; + + setPulling(true); + + // Animate the pull - pull from bottom (last item) + setTimeout(() => { + const next = queue[queue.length - 1]; + setConsumed((prev) => [...prev, next]); + setQueue(queue.slice(0, -1)); + setPulling(false); + }, 400); + }; + + const handlePush = () => { + if (pushing) return; + + setPushing(true); + + // Animate the push - add to top + setTimeout(() => { + setQueue((prev) => [nextValue, ...prev]); + setNextValue((prev) => prev + 1); + setPushing(false); + }, 400); + }; + + const handleReset = () => { + setQueue([]); + setConsumed([]); + setPulling(false); + setPushing(false); + setNextValue(6); + setIsInitializing(true); + + // Re-initialize + setTimeout(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + }, 100); + }; + + const isExhausted = queue.length === 0 && !isInitializing; + + return ( +
+ {/* Visual Layout */} +
+ {/* Cloud Push Source */} +
+ + {pushing ? '💫' : '☁️'} + {!pushing && ( + + )} + +
PUSH
+
+ Next: {nextValue} +
+
+ + {pushing && } + + + + + {pulling && } +
+ + {/* Controls */} +
+ +
+
+ Queue: {queue.length} | Consumed: {consumed.length} +
+
+
+ ); +} + +function PullLever({ + onPull, + active, + pulling, +}: { + onPull: () => void; + active: boolean; + pulling: boolean; +}) { + return ( +
+ + {pulling ? '⚡' : '⬇'} + {active && !pulling && ( + + )} + +
PULL
+
+ {active ? 'Active' : 'Exhausted'} +
+
+ ); +} + +function ConsumedRail({ items }: { items: number[] }) { + return ( +
+
Consumed
+
+ {items.length > 0 ? ( + items.map((num, idx) => ( + +
+ {num} +
+
+ )) + ) : ( +
+ ∅ +
+ )} +
+
+ ); +} + +function TransferBeam() { + return ( + + ); +} + +function PushBeam() { + return ( + + ); +} diff --git a/src/components/playground/StreamPushPullPrototypeV2.tsx b/src/components/playground/StreamPushPullPrototypeV2.tsx new file mode 100644 index 0000000..a83b5c3 --- /dev/null +++ b/src/components/playground/StreamPushPullPrototypeV2.tsx @@ -0,0 +1,341 @@ +'use client'; + +import { motion } from 'motion/react'; +import { useEffect, useState } from 'react'; +import { CardFaceFront } from '../patterns/CardFaceFront'; +import { StackedCard } from '../patterns/StackedCard'; + +export function StreamPushPullPrototypeV2() { + const [queue, setQueue] = useState([]); + const [consumed, setConsumed] = useState([]); + const [flippedCards, setFlippedCards] = useState>(new Set()); + const [pulling, setPulling] = useState(false); + const [pushing, setPushing] = useState(false); + const [nextValue, setNextValue] = useState(6); + const [isInitializing, setIsInitializing] = useState(true); + + // Initialize queue by adding items one at a time + useEffect(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); // Add to front (top of stack) + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + + return () => clearInterval(interval); + }, []); + + const handlePull = () => { + if (queue.length === 0 || pulling) return; + + setPulling(true); + const bottomCard = queue[queue.length - 1]; + + // First flip the card + setFlippedCards((prev) => new Set([...prev, bottomCard])); + + // After flip animation, move to consumed + setTimeout(() => { + setConsumed((prev) => [...prev, bottomCard]); + setQueue(queue.slice(0, -1)); + setPulling(false); + }, 800); // 600ms flip + 200ms buffer + }; + + const handlePush = () => { + if (pushing) return; + + setPushing(true); + + // Animate the push - add to top + setTimeout(() => { + setQueue((prev) => [nextValue, ...prev]); + setNextValue((prev) => prev + 1); + setPushing(false); + }, 400); + }; + + const handleReset = () => { + setQueue([]); + setConsumed([]); + setFlippedCards(new Set()); + setPulling(false); + setPushing(false); + setNextValue(6); + setIsInitializing(true); + + // Re-initialize + setTimeout(() => { + let currentIndex = 1; + + const interval = setInterval(() => { + if (currentIndex <= 5) { + setQueue((prev) => [currentIndex, ...prev]); + currentIndex++; + } else { + clearInterval(interval); + setIsInitializing(false); + } + }, 150); + }, 100); + }; + + const isExhausted = queue.length === 0 && !isInitializing; + + return ( +
+ {/* Visual Layout */} +
+ {/* Cloud Push Source */} +
+ + {pushing ? '💫' : '☁️'} + {!pushing && ( + + )} + +
PUSH
+
+ Next: {nextValue} +
+
+ + {pushing && } + + {/* Card Stack */} +
+
Source Queue
+
+ {queue.length > 0 ? ( +
+ {queue.map((value, idx) => ( + + ))} +
+ ) : ( +
+ Empty +
+ )} +
+
+ + + + {pulling && } +
+ + {/* Controls */} +
+ +
+
+ Queue: {queue.length} | Consumed: {consumed.length} +
+
+
+ ); +} + +function PullLever({ + onPull, + active, + pulling, +}: { + onPull: () => void; + active: boolean; + pulling: boolean; +}) { + return ( +
+ + {pulling ? '⚡' : '⬇'} + {active && !pulling && ( + + )} + +
PULL
+
+ {active ? 'Active' : 'Exhausted'} +
+
+ ); +} + +function ConsumedRail({ items }: { items: number[] }) { + return ( +
+
Consumed
+
+ {items.length > 0 ? ( + items.map((num, idx) => ( + + + + )) + ) : ( +
+ ∅ +
+ )} +
+
+ ); +} + +function TransferBeam() { + return ( + + ); +} + +function PushBeam() { + return ( + + ); +} diff --git a/src/components/renderers/ChunkBadge.tsx b/src/components/renderers/ChunkBadge.tsx new file mode 100644 index 0000000..34a0438 --- /dev/null +++ b/src/components/renderers/ChunkBadge.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Chunk } from 'effect'; +import { motion } from 'motion/react'; +import type { RenderableResult } from './RenderableResult'; + +export class ChunkBadge implements RenderableResult { + constructor(public chunk: Chunk.Chunk) {} + + render() { + const values = Chunk.toReadonlyArray(this.chunk); + return ( + +
+ Chunk({values.length}) +
+
+ {values.map((val, i) => ( + + {String(val)} + + ))} +
+
+ ); + } +} diff --git a/src/components/renderers/ChunkResult.tsx b/src/components/renderers/ChunkResult.tsx new file mode 100644 index 0000000..8cd099d --- /dev/null +++ b/src/components/renderers/ChunkResult.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { Chunk } from 'effect'; +import { motion } from 'motion/react'; +import type { RenderableResult } from './RenderableResult'; + +export class ChunkResult implements RenderableResult { + constructor(public chunk: Chunk.Chunk) {} + + render() { + const values = Chunk.toReadonlyArray(this.chunk); + return ( + +
Chunk({values.length})
+
+ {values.map((val, i) => ( + + {String(val)} + + ))} +
+
+ ); + } +} diff --git a/src/components/renderers/index.ts b/src/components/renderers/index.ts index 74bc919..18b9bdb 100644 --- a/src/components/renderers/index.ts +++ b/src/components/renderers/index.ts @@ -1,5 +1,7 @@ -export * from "./ArrayResult" -export * from "./BasicRenderers" -export * from "./EmojiResult" -export * from "./RenderableResult" -export * from "./TemperatureResult" +export * from './ArrayResult'; +export * from './BasicRenderers'; +export * from './ChunkBadge'; +export * from './ChunkResult'; +export * from './EmojiResult'; +export * from './RenderableResult'; +export * from './TemperatureResult'; diff --git a/src/examples/chunk-make.tsx b/src/examples/chunk-make.tsx new file mode 100644 index 0000000..f8e22bb --- /dev/null +++ b/src/examples/chunk-make.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { Chunk } from 'effect'; +import { useMemo } from 'react'; +import { EffectExample } from '@/components/display'; +import { ChunkBadge } from '@/components/renderers'; +import type { ExampleComponentProps } from '@/lib/example-types'; + +export function ChunkMakeExample({ exampleId, index, metadata }: ExampleComponentProps) { + // Just display the chunk - no effects needed, it's a pure value + const chunk = useMemo(() => Chunk.make(1, 2, 3, 4, 5), []); + + const codeSnippet = `const chunk = Chunk.make(1, 2, 3, 4, 5) +// Chunk(5) [1, 2, 3, 4, 5]`; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

{metadata.name}

+

{metadata.description}

+
+
+
+ + {/* Visual */} +
+
+ +
+
+ + {/* Code */} +
+
+          
+            const chunk{' '}
+            = Chunk
+            .
+            make
+            (
+            1
+            , 
+            2
+            , 
+            3
+            , 
+            4
+            , 
+            5
+            )
+            {'\n'}
+            // Chunk(5) [1, 2, 3, 4, 5]
+          
+        
+
+
+ ); +} + +export default ChunkMakeExample; diff --git a/src/examples/stream-range.tsx b/src/examples/stream-range.tsx new file mode 100644 index 0000000..cbdc600 --- /dev/null +++ b/src/examples/stream-range.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { Chunk, Effect, Stream } from 'effect'; +import { useMemo } from 'react'; +import { EffectExample } from '@/components/display'; +import { ChunkResult, NumberResult } from '@/components/renderers'; +import { useVisualEffects } from '@/hooks/useVisualEffects'; +import type { ExampleComponentProps } from '@/lib/example-types'; +import { VisualEffect } from '@/VisualEffect'; +import { getDelay } from './helpers'; + +export function StreamRangeExample({ exampleId, index, metadata }: ExampleComponentProps) { + // Individual stream elements being emitted sequentially + const { emit1, emit2, emit3, emit4, emit5 } = useVisualEffects({ + emit1: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(1))), + emit2: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(2))), + emit3: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(3))), + emit4: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(4))), + emit5: () => Effect.sleep(getDelay(200, 400)).pipe(Effect.as(new NumberResult(5))), + }); + + // The final collected result - waits for all emissions to complete + const collector = useMemo(() => { + const collectorEffect = Effect.all([ + emit1.effect, + emit2.effect, + emit3.effect, + emit4.effect, + emit5.effect, + ]).pipe( + Effect.map((results) => { + // Extract raw values from NumberResult objects and convert to Chunk + const numbers = results.map((r) => r.value); + return new ChunkResult(Chunk.fromIterable(numbers)); + }), + ); + + return new VisualEffect('result', collectorEffect); + }, [emit1, emit2, emit3, emit4, emit5]); + + const codeSnippet = `const stream = Stream.range(1, 6) + +const result = Stream.runCollect(stream) +// Chunk(1, 2, 3, 4, 5)`; + + const taskHighlightMap = useMemo( + () => ({ + emit1: { text: 'Stream.range(1, 6)' }, + emit2: { text: 'Stream.range(1, 6)' }, + emit3: { text: 'Stream.range(1, 6)' }, + emit4: { text: 'Stream.range(1, 6)' }, + emit5: { text: 'Stream.range(1, 6)' }, + result: { text: 'Stream.runCollect(stream)' }, + }), + [], + ); + + return ( + [emit1, emit2, emit3, emit4, emit5], + [emit1, emit2, emit3, emit4, emit5], + )} + resultEffect={collector} + effectHighlightMap={taskHighlightMap} + {...(index !== undefined && { index })} + exampleId={exampleId} + /> + ); +} + +export default StreamRangeExample; diff --git a/src/lib/example-types.ts b/src/lib/example-types.ts index 1c146b4..0d0224d 100644 --- a/src/lib/example-types.ts +++ b/src/lib/example-types.ts @@ -1,21 +1,29 @@ export interface ExampleMeta { - id: string - name: string - variant?: string - description: string - section: "constructors" | "concurrency" | "error handling" | "schedule" | "ref" | "scope" - order?: number + id: string; + name: string; + variant?: string; + description: string; + section: + | 'constructors' + | 'concurrency' + | 'error handling' + | 'schedule' + | 'ref' + | 'scope' + | 'chunks' + | 'streams'; + order?: number; } export interface ExampleComponentProps { - index?: number - metadata: ExampleMeta - exampleId: string + index?: number; + metadata: ExampleMeta; + exampleId: string; } export interface ExampleItem { - type: "example" - metadata: ExampleMeta + type: 'example'; + metadata: ExampleMeta; } -export type AppItem = ExampleItem +export type AppItem = ExampleItem; diff --git a/src/lib/examples-manifest.ts b/src/lib/examples-manifest.ts index 76dc964..124a3bd 100644 --- a/src/lib/examples-manifest.ts +++ b/src/lib/examples-manifest.ts @@ -1,70 +1,70 @@ -import type { ExampleMeta } from "./example-types" +import type { ExampleMeta } from './example-types'; // This is the single source of truth for all examples // Examples are listed in the exact order they should appear export const examplesManifest: Array = [ // Constructors { - id: "effect-succeed", - name: "Effect.succeed", - description: "Create an effect that always succeeds with a given value", - section: "constructors", + id: 'effect-succeed', + name: 'Effect.succeed', + description: 'Create an effect that always succeeds with a given value', + section: 'constructors', }, { - id: "effect-fail", - name: "Effect.fail", - description: "Create an effect that represents a recoverable error", - section: "constructors", + id: 'effect-fail', + name: 'Effect.fail', + description: 'Create an effect that represents a recoverable error', + section: 'constructors', }, { - id: "effect-die", - name: "Effect.die", - description: "Create an effect that terminates with an unrecoverable defect", - section: "constructors", + id: 'effect-die', + name: 'Effect.die', + description: 'Create an effect that terminates with an unrecoverable defect', + section: 'constructors', }, { - id: "effect-sync", - name: "Effect.sync", - description: "Create an effect from a synchronous side-effectful computation", - section: "constructors", + id: 'effect-sync', + name: 'Effect.sync', + description: 'Create an effect from a synchronous side-effectful computation', + section: 'constructors', }, { - id: "effect-promise", - name: "Effect.promise", - description: "Create an effect from an asynchronous computation guaranteed to succeed", - section: "constructors", + id: 'effect-promise', + name: 'Effect.promise', + description: 'Create an effect from an asynchronous computation guaranteed to succeed', + section: 'constructors', }, { - id: "effect-sleep", - name: "Effect.sleep", - description: "Create an effect that suspends execution for a given duration", - section: "constructors", + id: 'effect-sleep', + name: 'Effect.sleep', + description: 'Create an effect that suspends execution for a given duration', + section: 'constructors', }, // Concurrency { - id: "effect-all", - name: "Effect.all", - description: "Combine multiple effects into one, returning results based on input structure", - section: "concurrency", + id: 'effect-all', + name: 'Effect.all', + description: 'Combine multiple effects into one, returning results based on input structure', + section: 'concurrency', }, { - id: "effect-race", - name: "Effect.race", - description: "Race two effects and return the result of the first successful one", - section: "concurrency", + id: 'effect-race', + name: 'Effect.race', + description: 'Race two effects and return the result of the first successful one', + section: 'concurrency', }, { - id: "effect-raceall", - name: "Effect.raceAll", - description: "Race multiple effects and return the first successful result", - section: "concurrency", + id: 'effect-raceall', + name: 'Effect.raceAll', + description: 'Race multiple effects and return the first successful result', + section: 'concurrency', }, { - id: "effect-foreach", - name: "Effect.forEach", - description: "Execute an effectful operation for each element in an iterable", - section: "concurrency", + id: 'effect-foreach', + name: 'Effect.forEach', + description: 'Execute an effectful operation for each element in an iterable', + section: 'concurrency', }, // { // id: "effect-semaphore", @@ -81,103 +81,131 @@ export const examplesManifest: Array = [ // Error Handling { - id: "effect-all-short-circuit", - name: "Effect.all", - variant: "short circuit", - description: "Stop execution on the first error encountered", - section: "error handling", + id: 'effect-all-short-circuit', + name: 'Effect.all', + variant: 'short circuit', + description: 'Stop execution on the first error encountered', + section: 'error handling', }, { - id: "effect-orelse", - name: "Effect.orElse", - description: "Try one effect, and if it fails, fall back to another effect", - section: "error handling", + id: 'effect-orelse', + name: 'Effect.orElse', + description: 'Try one effect, and if it fails, fall back to another effect', + section: 'error handling', }, { - id: "effect-timeout", - name: "Effect.timeout", - description: "Add a time limit to an effect, failing with timeout if exceeded", - section: "error handling", + id: 'effect-timeout', + name: 'Effect.timeout', + description: 'Add a time limit to an effect, failing with timeout if exceeded', + section: 'error handling', }, { - id: "effect-eventually", - name: "Effect.eventually", - description: "Run an effect repeatedly until it succeeds, ignoring errors", - section: "error handling", + id: 'effect-eventually', + name: 'Effect.eventually', + description: 'Run an effect repeatedly until it succeeds, ignoring errors', + section: 'error handling', }, { - id: "effect-partition", - name: "Effect.partition", - description: "Execute effects and partition results into successes and failures", - section: "error handling", + id: 'effect-partition', + name: 'Effect.partition', + description: 'Execute effects and partition results into successes and failures', + section: 'error handling', }, { - id: "effect-validate", - name: "Effect.validate", - description: "Accumulate validation errors instead of short-circuiting", - section: "error handling", + id: 'effect-validate', + name: 'Effect.validate', + description: 'Accumulate validation errors instead of short-circuiting', + section: 'error handling', }, // Schedule { - id: "effect-repeat-spaced", - name: "Effect.repeat", - description: "Repeat an effect with a fixed delay between each execution", - section: "schedule", - variant: "spaced", + id: 'effect-repeat-spaced', + name: 'Effect.repeat', + description: 'Repeat an effect with a fixed delay between each execution', + section: 'schedule', + variant: 'spaced', }, { - id: "effect-repeat-while-output", - name: "Effect.repeat", - variant: "whileOutput", - description: "Repeat while output matches a condition", - section: "schedule", + id: 'effect-repeat-while-output', + name: 'Effect.repeat', + variant: 'whileOutput', + description: 'Repeat while output matches a condition', + section: 'schedule', }, { - id: "effect-retry-recurs", - name: "Effect.retry", - description: "Retry an effect a fixed number of times", - section: "schedule", - variant: "recurs", + id: 'effect-retry-recurs', + name: 'Effect.retry', + description: 'Retry an effect a fixed number of times', + section: 'schedule', + variant: 'recurs', }, { - id: "effect-retry-exponential", - name: "Effect.retry", - variant: "exponential", - description: "Retry with exponential backoff", - section: "schedule", + id: 'effect-retry-exponential', + name: 'Effect.retry', + variant: 'exponential', + description: 'Retry with exponential backoff', + section: 'schedule', }, // Ref { - id: "ref-make", - name: "Ref.make", - description: "Create a concurrency-safe mutable reference", - section: "ref", + id: 'ref-make', + name: 'Ref.make', + description: 'Create a concurrency-safe mutable reference', + section: 'ref', }, { - id: "ref-update-and-get", - name: "Ref.updateAndGet", - description: "Update a ref and return the new value", - section: "ref", + id: 'ref-update-and-get', + name: 'Ref.updateAndGet', + description: 'Update a ref and return the new value', + section: 'ref', }, // Scope { - id: "effect-add-finalizer", - name: "Effect.addFinalizer", - description: "Register cleanup actions in a scope", - section: "scope", + id: 'effect-add-finalizer', + name: 'Effect.addFinalizer', + description: 'Register cleanup actions in a scope', + section: 'scope', }, { - id: "effect-acquire-release", - name: "Effect.acquireRelease", - description: "Acquire resources with guaranteed cleanup", - section: "scope", + id: 'effect-acquire-release', + name: 'Effect.acquireRelease', + description: 'Acquire resources with guaranteed cleanup', + section: 'scope', }, -] + + // Chunks + { + id: 'chunk-make', + name: 'Chunk.make', + description: 'Create a chunk from individual values', + section: 'chunks', + }, + { + id: 'chunk-append', + name: 'Chunk.append', + description: 'Add an element to the end of a chunk', + section: 'chunks', + }, + { + id: 'chunk-concat', + name: 'Chunk.concat', + description: 'Combine two chunks into one', + section: 'chunks', + }, + + // Streams + { + id: 'stream-range', + name: 'Stream.range', + description: 'Collect a finite range of integers into a Chunk', + section: 'streams', + }, +]; // Helper function to get metadata by ID export function getExampleMeta(id: string): ExampleMeta | undefined { - return examplesManifest.find(meta => meta.id === id) + return examplesManifest.find((meta) => meta.id === id); } diff --git a/tsconfig.json b/tsconfig.json index 01cbc13..24111e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2022", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": false, "skipLibCheck": true, "strict": true, @@ -17,7 +21,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "noErrorTruncation": true, "plugins": [ { @@ -26,15 +30,30 @@ { "name": "@effect/language-service", "transform": "@effect/language-service/transform", - "namespaceImportPackages": ["effect", "@effect/*"] + "namespaceImportPackages": [ + "effect", + "@effect/*" + ] } ], /* Path mapping */ "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": [ + "src/*" + ] } }, - "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next-env.d.ts", "out/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "next-env.d.ts", + "out/types/**/*.ts", + "out/dev/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }