From 2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25 Mon Sep 17 00:00:00 2001 From: cashmere Date: Mon, 27 Jul 2026 01:21:02 +0200 Subject: feat: freeze-duration prompt Freeze frame now asks for the duration (decimal seconds, 0 < d <= 600) before capture, via a promise-based zustand store mounted as an editor-global dialog. Last-used duration persists and prefills. Cancel aborts before capture with no commands. --- apps/web/src/actions/freeze-frame-store.ts | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 apps/web/src/actions/freeze-frame-store.ts (limited to 'apps/web/src/actions/freeze-frame-store.ts') diff --git a/apps/web/src/actions/freeze-frame-store.ts b/apps/web/src/actions/freeze-frame-store.ts new file mode 100644 index 00000000..17102780 --- /dev/null +++ b/apps/web/src/actions/freeze-frame-store.ts @@ -0,0 +1,72 @@ +"use client"; + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export const DEFAULT_FREEZE_DURATION_SECONDS = 3; +export const MAX_FREEZE_DURATION_SECONDS = 600; + +export function isValidFreezeDuration({ + seconds, +}: { + seconds: number; +}): boolean { + return ( + Number.isFinite(seconds) && + seconds > 0 && + seconds <= MAX_FREEZE_DURATION_SECONDS + ); +} + +type DurationResolver = ({ seconds }: { seconds: number | null }) => void; + +interface FreezeFrameState { + isOpen: boolean; + lastDurationSeconds: number; + resolver: DurationResolver | null; + requestDuration: () => Promise; + confirmDuration: ({ seconds }: { seconds: number }) => void; + cancelDuration: () => void; +} + +export const useFreezeFrameStore = create()( + persist( + (set, get) => ({ + isOpen: false, + lastDurationSeconds: DEFAULT_FREEZE_DURATION_SECONDS, + resolver: null, + + requestDuration: () => + new Promise((resolve) => { + // A new invocation supersedes any pending one: resolve it as + // cancelled so its handler aborts instead of hanging. + get().resolver?.({ seconds: null }); + set({ + isOpen: true, + resolver: ({ seconds }) => resolve(seconds), + }); + }), + confirmDuration: ({ seconds }) => { + if (!isValidFreezeDuration({ seconds })) { + return; + } + get().resolver?.({ seconds }); + set({ + isOpen: false, + resolver: null, + lastDurationSeconds: seconds, + }); + }, + cancelDuration: () => { + get().resolver?.({ seconds: null }); + set({ isOpen: false, resolver: null }); + }, + }), + { + name: "freeze-frame-store", + partialize: (state) => ({ + lastDurationSeconds: state.lastDurationSeconds, + }), + }, + ), +); -- cgit v1.3.1