Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/components/fragments/FragmentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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('')
Expand Down Expand Up @@ -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()
}
}}
Expand Down Expand Up @@ -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()
}
}}
Expand Down
6 changes: 4 additions & 2 deletions src/components/prose/ProseBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -693,8 +695,8 @@ export const ProseBlock = memo(function ProseBlock({
<button
className="p-1 rounded-md text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-all disabled:opacity-25"
disabled={deleteMutation.isPending}
onClick={() => {
if (window.confirm('Remove this passage? It will be archived.')) {
onClick={async () => {
if (await confirm({ title: 'Remove this passage?', description: 'It will be archived.', confirmText: 'Remove', destructive: true })) {
deleteMutation.mutate()
setShowActions(false)
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/prose/TimelineTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { GitBranch, Plus, MoreVertical, Pencil, Trash2, EyeOff } from 'lucide-react'
import { useConfirm } from '@/components/ui/confirm-dialog'

interface TimelineTabsProps {
storyId: string
Expand All @@ -19,6 +20,7 @@ interface TimelineTabsProps {

export function TimelineTabs({ storyId, branches, activeBranchId, onHide }: TimelineTabsProps) {
const queryClient = useQueryClient()
const confirm = useConfirm()
const [renamingId, setRenamingId] = useState<string | null>(null)
const [renameValue, setRenameValue] = useState('')
const [creatingTimeline, setCreatingTimeline] = useState(false)
Expand Down Expand Up @@ -126,8 +128,8 @@ export function TimelineTabs({ storyId, branches, activeBranchId, onHide }: Time
{!isMain && (
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => {
if (window.confirm(`Delete timeline "${branch.name}"? This cannot be undone.`)) {
onClick={async () => {
if (await confirm({ title: `Delete timeline "${branch.name}"?`, description: 'This cannot be undone.', confirmText: 'Delete', destructive: true })) {
deleteMutation.mutate(branch.id)
}
}}
Expand Down
6 changes: 4 additions & 2 deletions src/components/sidebar/ArchivePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Spinner, EmptyState } from '@/components/ui/async-view'
import { Undo2, Trash2, Archive } from 'lucide-react'
import { componentId } from '@/lib/dom-ids'
import { useConfirm } from '@/components/ui/confirm-dialog'

interface ArchivePanelProps {
storyId: string
Expand All @@ -16,6 +17,7 @@ interface ArchivePanelProps {

export function ArchivePanel({ storyId, onSelect }: ArchivePanelProps) {
const queryClient = useQueryClient()
const confirm = useConfirm()
const [search, setSearch] = useState('')

const { data: archivedFragments, isLoading } = useQuery({
Expand Down Expand Up @@ -118,9 +120,9 @@ export function ArchivePanel({ storyId, onSelect }: ArchivePanelProps) {
size="icon"
variant="ghost"
className="size-7 text-muted-foreground hover:text-destructive"
onClick={(e) => {
onClick={async (e) => {
e.stopPropagation()
if (confirm('Permanently delete this fragment? This cannot be undone.')) {
if (await confirm({ title: 'Permanently delete this fragment?', description: 'This cannot be undone.', confirmText: 'Delete', destructive: true })) {
deleteMutation.mutate(fragment.id)
}
}}
Expand Down
6 changes: 4 additions & 2 deletions src/components/sidebar/TimelineManagerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { GitBranch, Plus, Pencil, Trash2, Check, X } from 'lucide-react'
import { MetaLabel } from '@/components/ui/prose-text'
import { useConfirm } from '@/components/ui/confirm-dialog'

interface TimelineManagerPanelProps {
storyId: string
}

export function TimelineManagerPanel({ storyId }: TimelineManagerPanelProps) {
const queryClient = useQueryClient()
const confirm = useConfirm()
const [creatingTimeline, setCreatingTimeline] = useState(false)
const [newTimelineName, setNewTimelineName] = useState('')
const [renamingId, setRenamingId] = useState<string | null>(null)
Expand Down Expand Up @@ -206,8 +208,8 @@ export function TimelineManagerPanel({ storyId }: TimelineManagerPanelProps) {
{!isMain && (
<button
className="p-1 rounded text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all"
onClick={() => {
if (window.confirm(`Delete timeline "${branch.name}"?`)) {
onClick={async () => {
if (await confirm({ title: `Delete timeline "${branch.name}"?`, description: 'This cannot be undone.', confirmText: 'Delete', destructive: true })) {
deleteMutation.mutate(branch.id)
}
}}
Expand Down
80 changes: 80 additions & 0 deletions src/components/ui/confirm-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createContext, useCallback, useContext, useRef, useState } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'

export interface ConfirmOptions {
title: string
description?: string
confirmText?: string
cancelText?: string
/** Style the confirm button as a destructive action. */
destructive?: boolean
}

type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>

const ConfirmContext = createContext<ConfirmFn | null>(null)

/**
* Async confirm() resolving true/false — use instead of native window.confirm(), which
* on Electron can leave the window unfocused (blocking input) after it closes.
*/
export function useConfirm(): ConfirmFn {
const ctx = useContext(ConfirmContext)
if (!ctx) throw new Error('useConfirm must be used within a ConfirmProvider')
return ctx
}

export function ConfirmProvider({ children }: { children: React.ReactNode }) {
const [options, setOptions] = useState<ConfirmOptions | null>(null)
const resolverRef = useRef<((value: boolean) => void) | null>(null)

const confirm = useCallback<ConfirmFn>((opts) => {
setOptions(opts)
return new Promise<boolean>((resolve) => {
resolverRef.current = resolve
})
}, [])

const settle = useCallback((result: boolean) => {
resolverRef.current?.(result)
resolverRef.current = null
setOptions(null)
}, [])

return (
<ConfirmContext.Provider value={confirm}>
{children}
<Dialog open={options !== null} onOpenChange={(open) => { if (!open) settle(false) }}>
{options && (
<DialogContent showCloseButton={false} className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>{options.title}</DialogTitle>
{options.description && (
<DialogDescription>{options.description}</DialogDescription>
)}
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => settle(false)}>
{options.cancelText ?? 'Cancel'}
</Button>
<Button
variant={options.destructive ? 'destructive' : 'default'}
onClick={() => settle(true)}
>
{options.confirmText ?? 'Confirm'}
</Button>
</DialogFooter>
</DialogContent>
)}
</Dialog>
</ConfirmContext.Provider>
)
}
15 changes: 9 additions & 6 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -111,12 +112,14 @@ function RootComponent() {
<CustomCssProvider />
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<HelpProvider>
<Outlet />
<HelpPanel />
<TtsPlayerBar />
<DesktopUpdateBanner />
</HelpProvider>
<ConfirmProvider>
<HelpProvider>
<Outlet />
<HelpPanel />
<TtsPlayerBar />
<DesktopUpdateBanner />
</HelpProvider>
</ConfirmProvider>
</TooltipProvider>
</QueryClientProvider>
</ThemeProvider>
Expand Down
6 changes: 4 additions & 2 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}}
Expand Down