Spaces:
Running
Running
Download frontends/react/src/TimelineControls.tsx from nhamid289/autodiff-dev: direct link, hf CLI and curl.
- Browser
- Download file 6.3 kB
-
https://huggingface.co/spaces/nhamid289/autodiff-dev/resolve/main/frontends/react/src/TimelineControls.tsx
- Command line
-
hf download hf://spaces/nhamid289/autodiff-dev/frontends/react/src/TimelineControls.tsx
-
curl -L -o TimelineControls.tsx https://huggingface.co/spaces/nhamid289/autodiff-dev/resolve/main/frontends/react/src/TimelineControls.tsx
6.3 kB
| import { Pause, Play, RotateCcw, StepBack, StepForward } from "lucide-react"; | |
| import { useSyncExternalStore, type ReactNode } from "react"; | |
| import type { Timeline, TimelineSnapshot } from "./Timeline"; | |
| // Styled to match @elvis/ui's StepControls, which works in whole iterations | |
| const thumbClass = | |
| "[&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-ink " + | |
| "[&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-ink"; | |
| // How far an arrow key moves the playhead | |
| const KEY_MOVE_MS = 250; | |
| // The slider's thumb stops this far from each end, so marks on the track are inset to line up with it | |
| const THUMB_RADIUS = 8; | |
| const iconButtonClass = | |
| "flex size-11 shrink-0 items-center justify-center rounded border-[1.5px] border-ink bg-white text-ink enabled:cursor-pointer enabled:hover:bg-marker-soft disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink"; | |
| // The slider along the whole run, with a tick for each step and the two passes marked | |
| function TimelineBar({ timeline, snapshot }: { timeline: Timeline; snapshot: TimelineSnapshot }) { | |
| const time = useSyncExternalStore(timeline.subscribeTime, timeline.getTime); | |
| const { ticks, backwardAt, iteration, lastIteration } = snapshot; | |
| const length = ticks[ticks.length - 1]; | |
| const at = (t: number) => `${length > 0 ? (t / length) * 100 : 0}%`; | |
| return ( | |
| <div className="flex flex-col gap-1"> | |
| <div className="relative h-6"> | |
| {/* Inset so positions here match the thumb's centre */} | |
| <div className="pointer-events-none absolute inset-y-0" style={{ left: THUMB_RADIUS, right: THUMB_RADIUS }}> | |
| <div className="absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 bg-grid" /> | |
| <div className="absolute left-0 top-1/2 h-1 -translate-y-1/2 bg-ink" style={{ width: at(time) }} /> | |
| {ticks.map((tick) => ( | |
| <div key={tick} data-tick className="absolute top-1/2 h-2.5 w-px -translate-y-1/2 bg-ink-muted" style={{ left: at(tick) }} /> | |
| ))} | |
| {backwardAt !== null && ( | |
| <div data-pass-marker className="absolute top-0 h-full w-0.5 -translate-x-1/2 bg-ink" style={{ left: at(backwardAt) }} /> | |
| )} | |
| </div> | |
| <input | |
| type="range" | |
| aria-label="Position in the run" | |
| aria-valuetext={`iteration ${iteration}${lastIteration !== null ? ` of ${lastIteration}` : ""}`} | |
| min={0} | |
| max={length} | |
| step={1} | |
| value={time} | |
| disabled={length === 0} | |
| onChange={(e) => timeline.seek(Number(e.target.value))} | |
| onKeyDown={(e) => { | |
| if (e.key === "ArrowLeft" || e.key === "ArrowRight") { | |
| e.preventDefault(); | |
| timeline.seek(time + (e.key === "ArrowLeft" ? -KEY_MOVE_MS : KEY_MOVE_MS)); | |
| } | |
| }} | |
| className={`absolute inset-0 m-0 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink disabled:cursor-not-allowed ${thumbClass}`} | |
| /> | |
| </div> | |
| {backwardAt !== null && ( | |
| <div className="relative h-4 text-xs text-ink-muted" style={{ marginLeft: THUMB_RADIUS, marginRight: THUMB_RADIUS }}> | |
| <span className="absolute left-0">Forward pass</span> | |
| <span className="absolute" style={{ left: at(backwardAt) }}>Backward pass</span> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default function TimelineControls({ | |
| timeline, | |
| snapshot, | |
| animated, | |
| onAnimatedChange, | |
| }: { | |
| timeline: Timeline; | |
| snapshot: TimelineSnapshot; | |
| animated: boolean; | |
| onAnimatedChange: (animated: boolean) => void; | |
| }) { | |
| const { iteration, lastIteration, atStart, playing } = snapshot; | |
| const atEnd = lastIteration === null || iteration >= lastIteration; | |
| return ( | |
| <section className="flex flex-col gap-3"> | |
| <TimelineBar timeline={timeline} snapshot={snapshot} /> | |
| <div className="flex gap-2"> | |
| <IconButton label="Reset to iteration 0" onClick={timeline.reset} disabled={atStart}> | |
| <RotateCcw size={18} /> | |
| </IconButton> | |
| <IconButton label="Step back" onClick={timeline.stepBack} disabled={atStart || playing}> | |
| <StepBack size={18} /> | |
| </IconButton> | |
| <button | |
| type="button" | |
| onClick={playing ? timeline.pause : timeline.play} | |
| disabled={!playing && atEnd} | |
| className="flex h-11 flex-1 items-center justify-center gap-2 rounded border-2 border-ink bg-ink text-sm font-semibold text-white shadow-[3px_3px_0_var(--color-marker)] enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink" | |
| > | |
| <span aria-hidden>{playing ? <Pause size={16} /> : <Play size={16} />}</span> | |
| {playing ? "Pause" : "Play"} | |
| </button> | |
| <IconButton label="Step forward" onClick={timeline.stepForward} disabled={atEnd || playing}> | |
| <StepForward size={18} /> | |
| </IconButton> | |
| </div> | |
| <div className="flex items-center justify-between gap-2"> | |
| <p className="font-mono text-sm text-ink-muted" aria-live="polite"> | |
| iteration {iteration}{lastIteration !== null ? ` / ${lastIteration}` : ""} | |
| </p> | |
| <label className="flex cursor-pointer items-center gap-2 text-sm text-gray-700"> | |
| <input type="checkbox" checked={animated} onChange={(e) => onAnimatedChange(e.target.checked)} className="size-4 accent-ink" /> | |
| Animate steps | |
| </label> | |
| </div> | |
| </section> | |
| ); | |
| } | |
| function IconButton({ label, onClick, disabled, children }: { label: string; onClick: () => void; disabled: boolean; children: ReactNode }) { | |
| return ( | |
| <button type="button" aria-label={label} title={label} onClick={onClick} disabled={disabled} className={iconButtonClass}> | |
| <span aria-hidden>{children}</span> | |
| </button> | |
| ); | |
| } | |