|
1 | | -import { useGlowEffect, useGradientBorderEffect, useNoiseEffect } from '@protohiro/effects'; |
| 1 | +import { |
| 2 | + useGlowEffect, |
| 3 | + useGradientBorderEffect, |
| 4 | + useNoiseEffect, |
| 5 | + useSpotlightEffect, |
| 6 | +} from '@protohiro/effects'; |
2 | 7 | import { useMemo, useState, type CSSProperties, type RefCallback } from 'react'; |
| 8 | +import revealImageAsset from './assets/reveal.jpeg'; |
3 | 9 |
|
4 | 10 | type DemoOption = string | number | boolean; |
5 | 11 | type DemoOptions = Record<string, DemoOption>; |
6 | | -type ControlType = 'text' | 'number' | 'range' | 'checkbox' | 'color'; |
| 12 | +type ControlType = 'text' | 'number' | 'range' | 'checkbox' | 'color' | 'select'; |
7 | 13 | type PreviewElement = 'button' | 'div' | 'article'; |
8 | 14 | type EffectHook = (options: DemoOptions) => RefCallback<HTMLElement>; |
9 | 15 |
|
10 | 16 | type DemoControl = { |
11 | 17 | key: string; |
12 | 18 | label: string; |
13 | 19 | type: ControlType; |
| 20 | + selectOptions?: string[]; |
14 | 21 | min?: number; |
15 | 22 | max?: number; |
16 | 23 | step?: number; |
@@ -42,6 +49,10 @@ function useNoiseStoryHook(options: DemoOptions): RefCallback<HTMLElement> { |
42 | 49 | return useNoiseEffect(options); |
43 | 50 | } |
44 | 51 |
|
| 52 | +function useSpotlightStoryHook(options: DemoOptions): RefCallback<HTMLElement> { |
| 53 | + return useSpotlightEffect(options); |
| 54 | +} |
| 55 | + |
45 | 56 | const STORIES: EffectStory[] = [ |
46 | 57 | { |
47 | 58 | id: 'gradient-border', |
@@ -116,6 +127,52 @@ const STORIES: EffectStory[] = [ |
116 | 127 | { key: 'disabled', label: 'Disabled', type: 'checkbox' }, |
117 | 128 | ], |
118 | 129 | }, |
| 130 | + { |
| 131 | + id: 'spotlight', |
| 132 | + title: 'Spotlight', |
| 133 | + description: 'Interactive light cone with cinematic falloff and pointer tracking.', |
| 134 | + hookName: 'useSpotlightEffect', |
| 135 | + componentName: 'SpotlightCard', |
| 136 | + previewText: 'Move pointer across the card', |
| 137 | + previewElement: 'article', |
| 138 | + previewStyle: { |
| 139 | + background: |
| 140 | + 'radial-gradient(circle at 8% 12%, rgba(99, 102, 241, 0.26), transparent 34%), radial-gradient(circle at 90% 82%, rgba(45, 212, 191, 0.12), transparent 42%), linear-gradient(150deg, #090617 0%, #0a1023 48%, #070b1a 100%)', |
| 141 | + color: '#f5f3ff', |
| 142 | + border: '1px solid rgba(129, 140, 248, 0.32)', |
| 143 | + boxShadow: |
| 144 | + 'inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 -24px 45px rgba(12, 8, 35, 0.42), 0 24px 70px rgba(3, 6, 19, 0.58)', |
| 145 | + }, |
| 146 | + hook: useSpotlightStoryHook, |
| 147 | + defaults: { |
| 148 | + mode: 'reveal', |
| 149 | + color: '#2a2079', |
| 150 | + size: 50, |
| 151 | + intensity: 0.3, |
| 152 | + softness: 0.9, |
| 153 | + coreIntensity: 0.35, |
| 154 | + x: 36, |
| 155 | + y: 32, |
| 156 | + followPointer: true, |
| 157 | + revealColor: '#38bdf8', |
| 158 | + revealImage: revealImageAsset, |
| 159 | + revealOpacity: 0.9, |
| 160 | + }, |
| 161 | + controls: [ |
| 162 | + { key: 'mode', label: 'Mode', type: 'select', selectOptions: ['reveal', 'glow'] }, |
| 163 | + { key: 'color', label: 'Color', type: 'color' }, |
| 164 | + { key: 'size', label: 'Size', type: 'number', min: 0, max: 280, step: 1 }, |
| 165 | + { key: 'intensity', label: 'Intensity', type: 'range', min: 0, max: 1, step: 0.01 }, |
| 166 | + { key: 'softness', label: 'Softness', type: 'range', min: 0, max: 1, step: 0.01 }, |
| 167 | + { key: 'coreIntensity', label: 'Core intensity', type: 'range', min: 0, max: 1, step: 0.01 }, |
| 168 | + { key: 'revealColor', label: 'Reveal color', type: 'color' }, |
| 169 | + { key: 'revealOpacity', label: 'Reveal opacity', type: 'range', min: 0, max: 1, step: 0.01 }, |
| 170 | + { key: 'x', label: 'X', type: 'range', min: 0, max: 100, step: 1 }, |
| 171 | + { key: 'y', label: 'Y', type: 'range', min: 0, max: 100, step: 1 }, |
| 172 | + { key: 'followPointer', label: 'Follow pointer', type: 'checkbox' }, |
| 173 | + { key: 'disabled', label: 'Disabled', type: 'checkbox' }, |
| 174 | + ], |
| 175 | + }, |
119 | 176 | ]; |
120 | 177 |
|
121 | 178 | function formatOptionValue(value: DemoOption): string { |
@@ -150,6 +207,7 @@ function EffectPlayground({ story }: { story: EffectStory }) { |
150 | 207 | const useEffectHook = story.hook; |
151 | 208 | const ref = useEffectHook(options); |
152 | 209 | const PreviewTag = story.previewElement; |
| 210 | + const previewClassName = story.id === 'spotlight' ? 'demo-card demo-card-spotlight' : 'demo-card'; |
153 | 211 | const hookSnippet = useMemo(() => createHookSnippet(story, options), [options, story]); |
154 | 212 |
|
155 | 213 | const controls = useMemo( |
@@ -177,6 +235,30 @@ function EffectPlayground({ story }: { story: EffectStory }) { |
177 | 235 | ); |
178 | 236 | } |
179 | 237 |
|
| 238 | + if (control.type === 'select') { |
| 239 | + return ( |
| 240 | + <label key={control.key} className="demo-control" htmlFor={inputId}> |
| 241 | + <span>{control.label}</span> |
| 242 | + <select |
| 243 | + id={inputId} |
| 244 | + value={String(value ?? '')} |
| 245 | + onChange={(event) => { |
| 246 | + setOptions((current) => ({ |
| 247 | + ...current, |
| 248 | + [control.key]: event.target.value, |
| 249 | + })); |
| 250 | + }} |
| 251 | + > |
| 252 | + {(control.selectOptions ?? []).map((optionValue) => ( |
| 253 | + <option key={optionValue} value={optionValue}> |
| 254 | + {optionValue} |
| 255 | + </option> |
| 256 | + ))} |
| 257 | + </select> |
| 258 | + </label> |
| 259 | + ); |
| 260 | + } |
| 261 | + |
180 | 262 | return ( |
181 | 263 | <label key={control.key} className="demo-control" htmlFor={inputId}> |
182 | 264 | <span>{control.label}</span> |
@@ -216,7 +298,7 @@ function EffectPlayground({ story }: { story: EffectStory }) { |
216 | 298 | <p>{story.description}</p> |
217 | 299 | </header> |
218 | 300 | <div className="demo-preview"> |
219 | | - <PreviewTag ref={ref} className="demo-card" style={story.previewStyle}> |
| 301 | + <PreviewTag ref={ref} className={previewClassName} style={story.previewStyle}> |
220 | 302 | {story.previewText} |
221 | 303 | </PreviewTag> |
222 | 304 | </div> |
|
0 commit comments