From 53050cbdd349e5f554501417adf9c781eb2d25a7 Mon Sep 17 00:00:00 2001 From: Viscerous Date: Tue, 23 Jun 2026 01:44:04 +0200 Subject: [PATCH] fix(ui): replace native confirm() with an in-app dialog to avoid window focus loss --- src/components/fragments/FragmentEditor.tsx | 10 ++- src/components/prose/ProseBlock.tsx | 6 +- src/components/prose/TimelineTabs.tsx | 6 +- src/components/sidebar/ArchivePanel.tsx | 6 +- .../sidebar/TimelineManagerPanel.tsx | 6 +- src/components/ui/confirm-dialog.tsx | 80 +++++++++++++++++++ src/routes/__root.tsx | 15 ++-- src/routes/index.tsx | 6 +- 8 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 src/components/ui/confirm-dialog.tsx diff --git a/src/components/fragments/FragmentEditor.tsx b/src/components/fragments/FragmentEditor.tsx index 2acf5932..469fe7bf 100644 --- a/src/components/fragments/FragmentEditor.tsx +++ b/src/components/fragments/FragmentEditor.tsx @@ -13,6 +13,7 @@ import type { FrozenSection } from '@/lib/api/types' import { RefinementPanel } from '@/components/refinement/RefinementPanel' import { copyFragmentToClipboard } from '@/lib/fragment-clipboard' import { CropDialog } from '@/components/fragments/CropDialog' +import { useConfirm } from '@/components/ui/confirm-dialog' import { Hint, EmptyHint, MetaLabel } from '@/components/ui/prose-text' export interface FragmentPrefill { @@ -37,6 +38,7 @@ export function FragmentEditor({ onSaved, }: FragmentEditorProps) { const queryClient = useQueryClient() + const confirm = useConfirm() const [name, setName] = useState('') const [description, setDescription] = useState('') const [content, setContent] = useState('') @@ -541,8 +543,8 @@ export function FragmentEditor({ size="sm" variant="ghost" className="h-7 text-xs gap-1 text-muted-foreground hover:text-foreground" - onClick={() => { - if (confirm('Archive this fragment?')) { + onClick={async () => { + if (await confirm({ title: 'Archive this fragment?', confirmText: 'Archive' })) { archiveMutation.mutate() } }} @@ -578,8 +580,8 @@ export function FragmentEditor({ size="sm" variant="ghost" className="h-7 text-xs gap-1 text-destructive/70 hover:text-destructive" - onClick={() => { - if (confirm('Permanently delete this fragment? This cannot be undone.')) { + onClick={async () => { + if (await confirm({ title: 'Permanently delete this fragment?', description: 'This cannot be undone.', confirmText: 'Delete', destructive: true })) { deleteMutation.mutate() } }} diff --git a/src/components/prose/ProseBlock.tsx b/src/components/prose/ProseBlock.tsx index 2d108c10..17caaf8e 100644 --- a/src/components/prose/ProseBlock.tsx +++ b/src/components/prose/ProseBlock.tsx @@ -11,6 +11,7 @@ import { type ThoughtStep } from './InlineGenerationInput' import { buildAnnotationHighlighter, formatDialogue, composeTextTransforms, stripEmphasisInDialogue, type Annotation } from '@/lib/character-mentions' import { RefreshCw, Undo2, PenLine, Bug, Trash2, GitBranch, MessageSquare, ChevronLeft, ChevronRight, Info, BookOpen, Volume2, Square } from 'lucide-react' import { Caption } from '@/components/ui/prose-text' +import { useConfirm } from '@/components/ui/confirm-dialog' import { useTtsSettings, useIsReadingFragment, playFragment, stopTts } from '@/lib/tts' interface ProseBlockProps { @@ -119,6 +120,7 @@ export const ProseBlock = memo(function ProseBlock({ void isFirst void isLast const queryClient = useQueryClient() + const confirm = useConfirm() const [actionMode, setActionMode] = useState<'regenerate' | null>(null) const [showUndo, setShowUndo] = useState(false) const [isStreamingAction, setIsStreamingAction] = useState(false) @@ -693,8 +695,8 @@ export const ProseBlock = memo(function ProseBlock({ + + + + )} + + + ) +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ea0c9e8d..4c6d16d4 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -6,6 +6,7 @@ import { } from '@tanstack/react-router' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { TooltipProvider } from '@/components/ui/tooltip' +import { ConfirmProvider } from '@/components/ui/confirm-dialog' import { ThemeProvider } from '@/lib/theme' import { HelpProvider } from '@/hooks/use-help' import { HelpPanel } from '@/components/help/HelpPanel' @@ -111,12 +112,14 @@ function RootComponent() { - - - - - - + + + + + + + + diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d8219949..42f906e1 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -38,6 +38,7 @@ import { ProviderList, ProviderPanel } from '@/components/settings/ProviderManag import { AboutSection } from '@/components/settings/AboutPanel' import { DesktopUpdatesControls } from '@/components/settings/DesktopUpdatesPanel' import { SectionHeading } from '@/components/settings/primitives' +import { useConfirm } from '@/components/ui/confirm-dialog' const THEME_OPTIONS = [ { value: 'light' as const, label: 'Light', Icon: Sun }, @@ -49,6 +50,7 @@ export const Route = createFileRoute('/')({ component: StoryListPage }) function StoryListPage() { const queryClient = useQueryClient() + const confirm = useConfirm() const navigate = useNavigate() const { theme, setTheme } = useTheme() const [open, setOpen] = useState(false) @@ -661,8 +663,8 @@ function StoryListPage() { key={story.id} story={story} isRecent={i === 0 && sortedStories.length > 1} - onDelete={() => { - if (confirm(`Delete "${story.name}"?`)) { + onDelete={async () => { + if (await confirm({ title: `Delete "${story.name}"?`, description: 'This permanently deletes the story and all its fragments.', confirmText: 'Delete', destructive: true })) { deleteMutation.mutate(story.id) } }}