From 267e44f83fd774e633502d8e8b7c6a355ba0024c Mon Sep 17 00:00:00 2001
From: KyleTryon
Date: Thu, 10 Sep 2026 11:50:36 -0400
Subject: [PATCH 1/2] feat(frontend): add bouncy filename settings accordion
---
.../components/editor/EditorExportDialog.tsx | 35 ++--
.../src/components/ui/bouncy-accordion.tsx | 179 ++++++++++++++++++
2 files changed, 199 insertions(+), 15 deletions(-)
create mode 100644 apps/frontend/src/components/ui/bouncy-accordion.tsx
diff --git a/apps/frontend/src/components/editor/EditorExportDialog.tsx b/apps/frontend/src/components/editor/EditorExportDialog.tsx
index 12ec80be..00925f8d 100644
--- a/apps/frontend/src/components/editor/EditorExportDialog.tsx
+++ b/apps/frontend/src/components/editor/EditorExportDialog.tsx
@@ -1,4 +1,5 @@
-import { Download } from "lucide-react";
+import { Download, FileText } from "lucide-react";
+import { BouncyAccordion } from "@/components/ui/bouncy-accordion";
import type {
ExportFormat,
ExportResolution,
@@ -185,20 +186,24 @@ export function EditorExportDialog({
hlsSourceLabel={hlsSourceLabel}
/>
-
-
- Advanced filename settings
-
-
-
-
-
+ ,
+ description: (
+
+ ),
+ },
+ ]}
+ />
diff --git a/apps/frontend/src/components/ui/bouncy-accordion.tsx b/apps/frontend/src/components/ui/bouncy-accordion.tsx
new file mode 100644
index 00000000..77804935
--- /dev/null
+++ b/apps/frontend/src/components/ui/bouncy-accordion.tsx
@@ -0,0 +1,179 @@
+/*
+ * Bouncy Accordion adapted from https://beui.dev/r/bouncy-accordion/raw
+ * MIT License — Copyright (c) 2026 Saurabh Chauhan
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import { ChevronDown } from "lucide-react";
+import { motion, useReducedMotion, type Transition } from "motion/react";
+import { useId, useState, type ReactNode } from "react";
+import { cliparrMotionTransitions } from "@/lib/motionPresets";
+import { cn } from "@/lib/utilities";
+
+interface BouncyAccordionItem {
+ id: string;
+ title: ReactNode;
+ description: ReactNode;
+ icon?: ReactNode;
+ disabled?: boolean;
+}
+
+interface BouncyAccordionProperties {
+ items: readonly BouncyAccordionItem[];
+ value?: string | null;
+ defaultValue?: string | null;
+ onValueChange?: (value: string | null) => void;
+ collapsible?: boolean;
+ className?: string;
+}
+
+const rowTransition = {
+ type: "spring",
+ duration: 0.55,
+ bounce: 0.38,
+} satisfies Transition;
+
+const contentOpenTransition = {
+ type: "spring",
+ duration: 0.58,
+ bounce: 0.32,
+} satisfies Transition;
+
+const contentCloseTransition = {
+ type: "spring",
+ duration: 0.46,
+ bounce: 0.26,
+} satisfies Transition;
+
+const chevronTransition = {
+ type: "spring",
+ duration: 0.42,
+ bounce: 0.28,
+} satisfies Transition;
+
+export function BouncyAccordion({
+ items,
+ value,
+ defaultValue = null,
+ onValueChange,
+ collapsible = true,
+ className,
+}: BouncyAccordionProperties) {
+ const reducedMotion = useReducedMotion();
+ const baseId = useId();
+ const [internalValue, setInternalValue] = useState(defaultValue);
+ const activeValue = value === undefined ? internalValue : value;
+ const activeIndex = items.findIndex((item) => item.id === activeValue);
+
+ function toggleItem(id: string) {
+ if (activeValue === id && !collapsible) {
+ return;
+ }
+ const nextValue = activeValue === id ? null : id;
+ if (value === undefined) {
+ setInternalValue(nextValue);
+ }
+ onValueChange?.(nextValue);
+ }
+
+ return (
+
+ {items.map((item, index) => {
+ const open = item.id === activeValue;
+ const contentTransition = open
+ ? contentOpenTransition
+ : contentCloseTransition;
+ const previousIsOpen = activeIndex === index - 1;
+ const startsGroup = open || index === 0 || previousIsOpen;
+ const endsGroup =
+ open || index === items.length - 1 || activeIndex === index + 1;
+ const contentId = `${baseId}-${item.id}-content`;
+ const triggerId = `${baseId}-${item.id}-trigger`;
+
+ return (
+ 0 && (open || previousIsOpen) && "mt-3",
+ item.disabled && "opacity-60",
+ )}
+ >
+
+
+
+ {item.description}
+
+
+
+ );
+ })}
+
+ );
+}
From 0a01f132a5b778cc23e30b3dc046eab0ab648db1 Mon Sep 17 00:00:00 2001
From: KyleTryon
Date: Thu, 10 Sep 2026 12:16:49 -0400
Subject: [PATCH 2/2] feat(frontend): replace native controls with styled
components
---
.../src/components/DashboardScreen.tsx | 173 +++++++++---------
.../src/components/MobilePwaInstallNudge.tsx | 20 +-
.../src/components/editor/EditorControls.tsx | 38 +---
.../components/editor/EditorMediaRange.tsx | 56 +++---
.../src/components/editor/EditorScreen.tsx | 16 ++
.../components/editor/EditorSubtitlePanel.tsx | 22 ++-
.../src/components/editor/EditorTimeline.tsx | 59 +++---
.../editor/EditorViewportScrollbar.tsx | 166 +++++++++--------
.../components/editor/useEditorSubtitles.ts | 52 ++++--
.../src/components/frontendWorkflow.test.ts | 112 +++++++-----
.../src/components/sources/SourcesDialog.tsx | 16 +-
.../sources/SourcesDialogSections.tsx | 57 ++----
.../src/components/sources/useSourcesState.ts | 18 +-
.../src/components/ui/confirmation-dialog.tsx | 56 ++++++
apps/frontend/src/components/ui/tooltip.tsx | 60 ++++++
apps/www/src/components/BaseLayout.astro | 4 +-
apps/www/src/components/CommandBlock.astro | 12 +-
apps/www/src/components/Header.astro | 4 +-
apps/www/src/components/SiteTooltips.astro | 151 +++++++++++++++
.../convert/ConvertPwaInstallPrompt.tsx | 4 +-
apps/www/src/pages/blog/index.astro | 2 +-
21 files changed, 714 insertions(+), 384 deletions(-)
create mode 100644 apps/frontend/src/components/ui/confirmation-dialog.tsx
create mode 100644 apps/www/src/components/SiteTooltips.astro
diff --git a/apps/frontend/src/components/DashboardScreen.tsx b/apps/frontend/src/components/DashboardScreen.tsx
index 17c1be26..7090cd99 100644
--- a/apps/frontend/src/components/DashboardScreen.tsx
+++ b/apps/frontend/src/components/DashboardScreen.tsx
@@ -1,3 +1,4 @@
+import { ControlTooltip } from "@/components/ui/tooltip";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
@@ -546,16 +547,18 @@ export function DashboardVersionBadge({
if (!latestRelease) {
return (
-
- {versionLabel}
-
+
+
+ {versionLabel}
+
+
);
}
@@ -573,7 +576,6 @@ export function DashboardVersionBadge({
"gap-1.5 border-primary/40 bg-primary/10 text-primary transition-colors hover:bg-primary/15 focus-visible:ring-2 focus-visible:ring-primary/35 focus-visible:outline-none",
)}
aria-label={`${updateLabel}. View release notes.`}
- title={updateLabel}
data-dashboard-version-badge
data-dashboard-update-available
>
@@ -824,17 +826,18 @@ export default function DashboardScreen({
Sources
{renderViewerFilterPicker()}
-
+
+
+
@@ -855,67 +858,71 @@ export default function DashboardScreen({
Sources
{renderViewerFilterPicker()}
-
-
-
-
-
-
-
-
-
- {!showViewerFilterControl && "Disconnect"}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!showViewerFilterControl && "Disconnect"}
+
+
diff --git a/apps/frontend/src/components/MobilePwaInstallNudge.tsx b/apps/frontend/src/components/MobilePwaInstallNudge.tsx
index 9559ac54..665ef72b 100644
--- a/apps/frontend/src/components/MobilePwaInstallNudge.tsx
+++ b/apps/frontend/src/components/MobilePwaInstallNudge.tsx
@@ -1,3 +1,4 @@
+import { ControlTooltip } from "@/components/ui/tooltip";
import { Download, Share, Smartphone, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
@@ -99,15 +100,16 @@ export function MobilePwaInstallNudgeCard({
: "Open it like an app with a faster, full-screen experience."}
-
+
+
+
diff --git a/apps/frontend/src/components/editor/EditorControls.tsx b/apps/frontend/src/components/editor/EditorControls.tsx
index dae877a4..2c2d4d9a 100644
--- a/apps/frontend/src/components/editor/EditorControls.tsx
+++ b/apps/frontend/src/components/editor/EditorControls.tsx
@@ -1,9 +1,4 @@
-import {
- useMemo,
- type CSSProperties,
- type ReactElement,
- type ReactNode,
-} from "react";
+import { useMemo, type CSSProperties, type ReactNode } from "react";
import {
Camera,
Pause,
@@ -15,11 +10,7 @@ import {
ZoomIn,
ZoomOut,
} from "lucide-react";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
+import { ControlTooltip } from "@/components/ui/tooltip";
import {
Drawer,
DrawerContent,
@@ -67,31 +58,6 @@ interface EditorControlsProperties {
onFitSelection: () => void;
}
-function ControlTooltip({
- label,
- disabled = false,
- children,
-}: {
- label: string;
- disabled?: boolean;
- children: ReactElement;
-}) {
- return (
-
-
- {disabled ? (
-
- {children}
-
- ) : (
- children
- )}
-
- {label}
-
- );
-}
-
export function EditorControls({
variant = "desktop",
playbackSourcePanel,
diff --git a/apps/frontend/src/components/editor/EditorMediaRange.tsx b/apps/frontend/src/components/editor/EditorMediaRange.tsx
index a7d39c13..ce7857dd 100644
--- a/apps/frontend/src/components/editor/EditorMediaRange.tsx
+++ b/apps/frontend/src/components/editor/EditorMediaRange.tsx
@@ -1,3 +1,4 @@
+import { ControlTooltip } from "@/components/ui/tooltip";
import {
RangeScrollbar,
Timeline,
@@ -39,11 +40,12 @@ export function EditorMediaRange({ engine }: { engine: TimelineEngine }) {
className="editor-media-range-row"
style={{ top: rect.y, height: rect.height }}
>
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
diff --git a/apps/frontend/src/components/editor/EditorScreen.tsx b/apps/frontend/src/components/editor/EditorScreen.tsx
index c4efebd2..c45b80a1 100644
--- a/apps/frontend/src/components/editor/EditorScreen.tsx
+++ b/apps/frontend/src/components/editor/EditorScreen.tsx
@@ -1,3 +1,4 @@
+import { ConfirmationDialog } from "@/components/ui/confirmation-dialog";
import {
lazy,
Suspense,
@@ -152,6 +153,7 @@ function EditorScreenContent({
const [editorPropertiesOpenSections, setEditorPropertiesOpenSections] =
useState(loadEditorPropertiesOpenSections);
const [exportDialogMounted, setExportDialogMounted] = useState(false);
+ const subtitleTrackTriggerReference = useRef(null);
const {
subtitleTracks,
selectedSubtitleTrack,
@@ -170,6 +172,9 @@ function EditorScreenContent({
clippedSubtitleCues,
subtitleExportSummary,
handleSelectedSubtitleTrackChange,
+ subtitleTrackChangePending,
+ confirmSubtitleTrackChange,
+ cancelSubtitleTrackChange,
selectedSubtitleCue,
handleSelectedSubtitleTextCommit,
handleSelectedSubtitleStartCommit,
@@ -604,6 +609,8 @@ function EditorScreenContent({
providerId={session.source.providerId}
subtitleTracks={subtitleTracks}
selectedSubtitleTrackKey={selectedSubtitleTrackKey}
+ subtitleTrackChangePending={subtitleTrackChangePending}
+ subtitleTrackTriggerRef={subtitleTrackTriggerReference}
onSelectedSubtitleTrackKeyChange={handleSelectedSubtitleTrackChange}
subtitlesEnabled={subtitleEnabled}
onSubtitlesEnabledChange={setSubtitleEnabled}
@@ -649,6 +656,15 @@ function EditorScreenContent({
return (
+
;
onSelectedSubtitleTrackKeyChange: (value: string) => void;
subtitlesEnabled: boolean;
onSubtitlesEnabledChange: (value: boolean) => void;
@@ -85,6 +93,8 @@ export function EditorSubtitlePanel({
providerId,
subtitleTracks,
selectedSubtitleTrackKey,
+ subtitleTrackChangePending,
+ subtitleTrackTriggerRef,
onSelectedSubtitleTrackKeyChange,
subtitlesEnabled,
onSubtitlesEnabledChange,
@@ -207,13 +217,21 @@ export function EditorSubtitlePanel({
onValueChange={onSelectedSubtitleTrackKeyChange}
>
-
+ {
+ // The confirmation owns focus while it is open.
+ if (subtitleTrackChangePending) {
+ event.preventDefault();
+ }
+ }}
+ >
Subtitle Tracks
No subtitles
diff --git a/apps/frontend/src/components/editor/EditorTimeline.tsx b/apps/frontend/src/components/editor/EditorTimeline.tsx
index 7d4ed3f2..58ded085 100644
--- a/apps/frontend/src/components/editor/EditorTimeline.tsx
+++ b/apps/frontend/src/components/editor/EditorTimeline.tsx
@@ -1,3 +1,4 @@
+import { ControlTooltip } from "@/components/ui/tooltip";
import {
CanvasRenderer,
Timeline,
@@ -43,35 +44,41 @@ function TrackHeaderColumn({ muted, onMutedChange }: EditorTimelineProperties) {
{(header: UseTimelineTrackHeaderResult) => (
{track.id === EDITOR_MEDIA_TRACK_ID ? (
-
+
+
) : (
-
+
+
)}
{header.label}
diff --git a/apps/frontend/src/components/editor/EditorViewportScrollbar.tsx b/apps/frontend/src/components/editor/EditorViewportScrollbar.tsx
index 35e5ab2b..43d733f0 100644
--- a/apps/frontend/src/components/editor/EditorViewportScrollbar.tsx
+++ b/apps/frontend/src/components/editor/EditorViewportScrollbar.tsx
@@ -1,3 +1,4 @@
+import { ControlTooltip } from "@/components/ui/tooltip";
import {
RangeScrollbar,
Timeline,
@@ -37,92 +38,95 @@ function ZoomHandle({
};
return (
- ) => {
- const delta = {
- ArrowLeft: -10,
- ArrowRight: 10,
- PageUp: -120,
- PageDown: 120,
- }[event.key];
- if (delta === undefined) {
- return;
+
+ ) => {
+ const delta = {
+ ArrowLeft: -10,
+ ArrowRight: 10,
+ PageUp: -120,
+ PageDown: 120,
+ }[event.key];
+ if (delta === undefined) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ control.onValueChange(
+ viewportRangeAfterZoomDrag({
+ range: {
+ start: control.viewStartSeconds,
+ end: control.viewEndSeconds,
+ },
+ side,
+ deltaPixels: delta,
+ minSpan: control.range.minSpan,
+ duration: control.totalDurationSeconds,
+ }),
+ { reason: "handle-keyboard", side },
+ );
+ }}
+ onPointerDown={(event: PointerEvent) => {
+ // Preserve the library's accessible handle while replacing its
+ // full-duration linear pointer sensitivity with proportional zoom.
+ event.preventDefault();
+ event.stopPropagation();
+ if (
+ (event.pointerType !== "touch" && event.button !== 0) ||
+ drag.current
+ ) {
+ return;
+ }
+ event.currentTarget.focus();
+ drag.current = {
+ pointerId: event.pointerId,
+ clientX: event.clientX,
range: {
start: control.viewStartSeconds,
end: control.viewEndSeconds,
},
- side,
- deltaPixels: delta,
- minSpan: control.range.minSpan,
- duration: control.totalDurationSeconds,
- }),
- { reason: "handle-keyboard", side },
- );
- }}
- onPointerDown={(event: PointerEvent) => {
- // Preserve the library's accessible handle while replacing its
- // full-duration linear pointer sensitivity with proportional zoom.
- event.preventDefault();
- event.stopPropagation();
- if (
- (event.pointerType !== "touch" && event.button !== 0) ||
- drag.current
- ) {
- return;
- }
- event.currentTarget.focus();
- drag.current = {
- pointerId: event.pointerId,
- clientX: event.clientX,
- range: {
- start: control.viewStartSeconds,
- end: control.viewEndSeconds,
- },
- };
- event.currentTarget.setPointerCapture(event.pointerId);
- }}
- onPointerMove={(event: PointerEvent) => {
- const active = drag.current;
- if (!active || active.pointerId !== event.pointerId) {
- return;
- }
- control.onValueChange(
- viewportRangeAfterZoomDrag({
- range: active.range,
- side,
- deltaPixels: event.clientX - active.clientX,
- minSpan: control.range.minSpan,
- duration: control.totalDurationSeconds,
- }),
- { reason: "handle-drag", side },
- );
- }}
- onPointerUp={finishDrag}
- onPointerCancel={finishDrag}
- onLostPointerCapture={finishDrag}
- />
+ };
+ event.currentTarget.setPointerCapture(event.pointerId);
+ }}
+ onPointerMove={(event: PointerEvent) => {
+ const active = drag.current;
+ if (!active || active.pointerId !== event.pointerId) {
+ return;
+ }
+ control.onValueChange(
+ viewportRangeAfterZoomDrag({
+ range: active.range,
+ side,
+ deltaPixels: event.clientX - active.clientX,
+ minSpan: control.range.minSpan,
+ duration: control.totalDurationSeconds,
+ }),
+ { reason: "handle-drag", side },
+ );
+ }}
+ onPointerUp={finishDrag}
+ onPointerCancel={finishDrag}
+ onLostPointerCapture={finishDrag}
+ />
+
);
}
diff --git a/apps/frontend/src/components/editor/useEditorSubtitles.ts b/apps/frontend/src/components/editor/useEditorSubtitles.ts
index 12c7b43e..b9968c2d 100644
--- a/apps/frontend/src/components/editor/useEditorSubtitles.ts
+++ b/apps/frontend/src/components/editor/useEditorSubtitles.ts
@@ -90,6 +90,9 @@ export function useEditorSubtitles({
const [selectedSubtitleTrackKey, setSelectedSubtitleTrackKey] = useState(
initialSubtitleSelection.key,
);
+ const [pendingSubtitleTrackKey, setPendingSubtitleTrackKey] = useState<
+ string | null
+ >(null);
const [importedSubtitleTrackKey, setImportedSubtitleTrackKey] = useState<
string | null
>(initialDraft?.subtitles.importedTrackKey ?? null);
@@ -241,20 +244,8 @@ export function useEditorSubtitles({
setSubtitleEnabled(subtitleTrackSupportsBurnIn(preferredSubtitleTrack));
}, [session.selectedSubtitleTrack, subtitleTracks]);
- const handleSelectedSubtitleTrackChange = useCallback(
+ const applySubtitleTrackChange = useCallback(
(value: string) => {
- if (
- value !== "none" &&
- value !== importedSubtitleTrackKey &&
- subtitleCues.length > 0 &&
- globalThis.window !== undefined &&
- !globalThis.confirm(
- "Changing subtitle tracks will replace your customized subtitle cues. Continue?",
- )
- ) {
- return;
- }
-
subtitleTrackSelectionChangedByUserReference.current = true;
setSelectedSubtitleTrackKey(value);
clearSubtitleError();
@@ -272,15 +263,33 @@ export function useEditorSubtitles({
Boolean(nextTrack && subtitleTrackSupportsBurnIn(nextTrack)),
);
},
- [
- clearSubtitleError,
- importedSubtitleTrackKey,
- resetSubtitleCues,
- subtitleCues.length,
- subtitleTracks,
- ],
+ [clearSubtitleError, resetSubtitleCues, subtitleTracks],
);
+ const handleSelectedSubtitleTrackChange = useCallback(
+ (value: string) => {
+ if (
+ value !== "none" &&
+ value !== importedSubtitleTrackKey &&
+ subtitleCues.length > 0
+ ) {
+ setPendingSubtitleTrackKey(value);
+ return;
+ }
+
+ applySubtitleTrackChange(value);
+ },
+ [applySubtitleTrackChange, importedSubtitleTrackKey, subtitleCues.length],
+ );
+
+ function confirmSubtitleTrackChange() {
+ if (pendingSubtitleTrackKey === null) {
+ return;
+ }
+ applySubtitleTrackChange(pendingSubtitleTrackKey);
+ setPendingSubtitleTrackKey(null);
+ }
+
const handleSelectedSubtitleTextCommit = useCallback(
(text: string) => {
if (!selectedSubtitleClip) {
@@ -385,6 +394,9 @@ export function useEditorSubtitles({
clippedSubtitleCues,
subtitleExportSummary,
handleSelectedSubtitleTrackChange,
+ subtitleTrackChangePending: pendingSubtitleTrackKey !== null,
+ confirmSubtitleTrackChange,
+ cancelSubtitleTrackChange: () => setPendingSubtitleTrackKey(null),
selectedSubtitleCue,
handleSelectedSubtitleTextCommit,
handleSelectedSubtitleStartCommit,
diff --git a/apps/frontend/src/components/frontendWorkflow.test.ts b/apps/frontend/src/components/frontendWorkflow.test.ts
index 8aec3a2f..c3bba962 100644
--- a/apps/frontend/src/components/frontendWorkflow.test.ts
+++ b/apps/frontend/src/components/frontendWorkflow.test.ts
@@ -412,30 +412,36 @@ void test("renders dashboard version badge as a release link when an update is a
void test("renders dashboard dev version badge with disabled update checks", () => {
const markup = renderToStaticMarkup(
- createElement(DashboardVersionBadge, {
- versionLabel: "dev",
- latestRelease: null,
- releaseChecksDisabledReason:
- "Local development build; release update checks are disabled",
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(DashboardVersionBadge, {
+ versionLabel: "dev",
+ latestRelease: null,
+ releaseChecksDisabledReason:
+ "Local development build; release update checks are disabled",
+ }),
+ ),
);
assert.match(markup, /data-dashboard-version-badge/);
assert.match(markup, /data-dashboard-release-check-disabled="true"/);
assert.match(markup, />dev);
- assert.match(
- markup,
- /Local development build; release update checks are disabled/,
- );
+ assert.match(markup, /tabindex="0"/);
+ assert.doesNotMatch(markup, / title=/);
});
void test("renders mobile PWA install nudge for native install state", () => {
const markup = renderToStaticMarkup(
- createElement(MobilePwaInstallNudgeCard, {
- mode: "native",
- onDismiss: () => {},
- onInstall: () => {},
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(MobilePwaInstallNudgeCard, {
+ mode: "native",
+ onDismiss: () => {},
+ onInstall: () => {},
+ }),
+ ),
);
assert.match(markup, /Add Cliparr to your home screen/);
@@ -445,11 +451,15 @@ void test("renders mobile PWA install nudge for native install state", () => {
void test("hides mobile PWA install nudge by default", () => {
const markup = renderToStaticMarkup(
- createElement(MobilePwaInstallNudgeCard, {
- mode: "hidden",
- onDismiss: () => {},
- onInstall: () => {},
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(MobilePwaInstallNudgeCard, {
+ mode: "hidden",
+ onDismiss: () => {},
+ onInstall: () => {},
+ }),
+ ),
);
assert.equal(markup, "");
@@ -457,13 +467,17 @@ void test("hides mobile PWA install nudge by default", () => {
void test("does not render dashboard PWA nudge in default server markup", () => {
const markup = renderToStaticMarkup(
- createElement(DashboardScreen, {
- activeViewTransitionSessionId: null,
- onSelectSession: () => {},
- onOpenLocalVideo: () => {},
- onOpenSources: () => {},
- onDisconnect: () => {},
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(DashboardScreen, {
+ activeViewTransitionSessionId: null,
+ onSelectSession: () => {},
+ onOpenLocalVideo: () => {},
+ onOpenSources: () => {},
+ onDisconnect: () => {},
+ }),
+ ),
);
assert.doesNotMatch(markup, /Add Cliparr to your home screen/);
@@ -471,13 +485,17 @@ void test("does not render dashboard PWA nudge in default server markup", () =>
void test("reserves dashboard playback card space before sessions load", () => {
const markup = renderToStaticMarkup(
- createElement(DashboardScreen, {
- activeViewTransitionSessionId: null,
- onSelectSession: () => {},
- onOpenLocalVideo: () => {},
- onOpenSources: () => {},
- onDisconnect: () => {},
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(DashboardScreen, {
+ activeViewTransitionSessionId: null,
+ onSelectSession: () => {},
+ onOpenLocalVideo: () => {},
+ onOpenSources: () => {},
+ onDisconnect: () => {},
+ }),
+ ),
);
assert.match(markup, /data-dashboard-loading-grid/);
@@ -492,13 +510,17 @@ void test("reserves dashboard playback card space before sessions load", () => {
void test("reserves dashboard version badge space before health loads", () => {
const markup = renderToStaticMarkup(
- createElement(DashboardScreen, {
- activeViewTransitionSessionId: null,
- onSelectSession: () => {},
- onOpenLocalVideo: () => {},
- onOpenSources: () => {},
- onDisconnect: () => {},
- }),
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(DashboardScreen, {
+ activeViewTransitionSessionId: null,
+ onSelectSession: () => {},
+ onOpenLocalVideo: () => {},
+ onOpenSources: () => {},
+ onDisconnect: () => {},
+ }),
+ ),
);
assert.match(markup, /data-dashboard-version-badge/);
@@ -507,7 +529,13 @@ void test("reserves dashboard version badge space before health loads", () => {
void test("renders mobile PWA install nudge on the initial eligible browser pass", () => {
withMobilePwaBrowserEnvironment(() => {
- const markup = renderToStaticMarkup(createElement(MobilePwaInstallNudge));
+ const markup = renderToStaticMarkup(
+ createElement(
+ TooltipProvider,
+ null,
+ createElement(MobilePwaInstallNudge),
+ ),
+ );
assert.match(markup, /Add Cliparr to your home screen/);
assert.match(markup, /data-pwa-install-mode="ios"/);
diff --git a/apps/frontend/src/components/sources/SourcesDialog.tsx b/apps/frontend/src/components/sources/SourcesDialog.tsx
index a74a8c4c..554594ce 100644
--- a/apps/frontend/src/components/sources/SourcesDialog.tsx
+++ b/apps/frontend/src/components/sources/SourcesDialog.tsx
@@ -1,6 +1,7 @@
import { useRef } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { DialogWindow } from "@/components/ui/dialog";
+import { ConfirmationDialog } from "@/components/ui/confirmation-dialog";
import {
SourceCard,
SourcesConnectSection,
@@ -70,7 +71,10 @@ export default function SourcesDialog({
saveSourceEdits,
toggleSourceEnabled,
checkSource,
- deleteSource,
+ sourceToRemove,
+ requestSourceRemoval,
+ cancelSourceRemoval,
+ confirmSourceRemoval,
updateDraftName,
updateDraftBaseUrl,
} = useSourcesState({
@@ -195,7 +199,7 @@ export default function SourcesDialog({
onSave={() => saveSourceEdits(source)}
onToggleEnabled={() => toggleSourceEnabled(source)}
onRefresh={() => checkSource(source)}
- onRemove={() => deleteSource(source)}
+ onRemove={() => requestSourceRemoval(source)}
/>
))}
@@ -212,6 +216,14 @@ export default function SourcesDialog({
portalClassName="p-4 sm:p-6"
popupClassName="h-full max-w-6xl rounded-lg"
>
+ void confirmSourceRemoval()}
+ />
-
-
- {children}
-
-
- {message}
-
- );
-}
-
interface SourceCounts {
all: number;
enabled: number;
@@ -274,7 +247,7 @@ export function SourcesDialogHeader({
{showConnectPanel ? "Hide" : "Add Source"}
)}
-
+
Reload
-
-
+
+
Refresh All
-
+