summaryrefslogtreecommitdiff
path: root/apps/web/src/actions/components
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 01:21:02 +0200
committercashmere <cashmere@autistici.org>2026-07-27 01:21:02 +0200
commit2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25 (patch)
tree2fdd617e9d51f7293e56b0e9b947f00643aeb8cd /apps/web/src/actions/components
parentf8fdff7f0c1b8efdef9093c932e28e427799d6ef (diff)
downloadopencut-2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25.tar.gz
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.
Diffstat (limited to 'apps/web/src/actions/components')
-rw-r--r--apps/web/src/actions/components/freeze-frame-dialog.tsx109
1 files changed, 109 insertions, 0 deletions
diff --git a/apps/web/src/actions/components/freeze-frame-dialog.tsx b/apps/web/src/actions/components/freeze-frame-dialog.tsx
new file mode 100644
index 00000000..a6a7e0ae
--- /dev/null
+++ b/apps/web/src/actions/components/freeze-frame-dialog.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ isValidFreezeDuration,
+ MAX_FREEZE_DURATION_SECONDS,
+ useFreezeFrameStore,
+} from "@/actions/freeze-frame-store";
+
+export function FreezeFrameDialog() {
+ const isOpen = useFreezeFrameStore((state) => state.isOpen);
+ const cancelDuration = useFreezeFrameStore((state) => state.cancelDuration);
+
+ return (
+ <Dialog
+ open={isOpen}
+ onOpenChange={(open) => {
+ if (!open) {
+ cancelDuration();
+ }
+ }}
+ >
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>Freeze frame</DialogTitle>
+ <DialogDescription>
+ How long should the frozen frame stay on screen?
+ </DialogDescription>
+ </DialogHeader>
+ {isOpen && <FreezeFrameForm />}
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+function FreezeFrameForm() {
+ const lastDurationSeconds = useFreezeFrameStore(
+ (state) => state.lastDurationSeconds,
+ );
+ const confirmDuration = useFreezeFrameStore((state) => state.confirmDuration);
+ const cancelDuration = useFreezeFrameStore((state) => state.cancelDuration);
+ const [value, setValue] = useState(String(lastDurationSeconds));
+
+ const seconds = Number(value);
+ const showError = value.trim() !== "" && !isValidFreezeDuration({ seconds });
+ const canConfirm = isValidFreezeDuration({ seconds });
+
+ const handleConfirm = () => {
+ if (canConfirm) {
+ confirmDuration({ seconds });
+ }
+ };
+
+ return (
+ <>
+ <DialogBody className="gap-3">
+ <Label htmlFor="freeze-frame-duration">Duration (seconds)</Label>
+ <Input
+ id="freeze-frame-duration"
+ value={value}
+ inputMode="decimal"
+ aria-invalid={showError}
+ onChange={(e) => setValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleConfirm();
+ }
+ }}
+ placeholder="e.g. 2.5"
+ />
+ {showError && (
+ <p className="text-destructive text-xs">
+ Enter a duration greater than 0 and at most{" "}
+ {MAX_FREEZE_DURATION_SECONDS} seconds.
+ </p>
+ )}
+ </DialogBody>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ cancelDuration();
+ }}
+ >
+ Cancel
+ </Button>
+ <Button onClick={handleConfirm} disabled={!canConfirm}>
+ Freeze
+ </Button>
+ </DialogFooter>
+ </>
+ );
+}