Skip to content

Commit 9f77199

Browse files
authored
feat: add configurable close protection setting (#123)
- Add Close Protection toggle to Settings dialog - Save setting to localStorage (default: enabled) - Make beforeunload confirmation conditional - Settings button now always visible in header - Add shadcn Switch and Label components
1 parent 77f2569 commit 9f77199

File tree

7 files changed

+358
-15
lines changed

7 files changed

+358
-15
lines changed

app/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react"
33
import { DrawIoEmbed } from "react-drawio"
44
import type { ImperativePanelHandle } from "react-resizable-panels"
55
import ChatPanel from "@/components/chat-panel"
6+
import { STORAGE_CLOSE_PROTECTION_KEY } from "@/components/settings-dialog"
67
import {
78
ResizableHandle,
89
ResizablePanel,
@@ -21,6 +22,13 @@ export default function Home() {
2122
}
2223
return "min"
2324
})
25+
const [closeProtection, setCloseProtection] = useState(() => {
26+
if (typeof window !== "undefined") {
27+
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
28+
return saved !== "false" // Default to true
29+
}
30+
return true
31+
})
2432
const chatPanelRef = useRef<ImperativePanelHandle>(null)
2533

2634
useEffect(() => {
@@ -61,6 +69,8 @@ export default function Home() {
6169
// Show confirmation dialog when user tries to leave the page
6270
// This helps prevent accidental navigation from browser back gestures
6371
useEffect(() => {
72+
if (!closeProtection) return
73+
6474
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
6575
event.preventDefault()
6676
return ""
@@ -69,7 +79,7 @@ export default function Home() {
6979
window.addEventListener("beforeunload", handleBeforeUnload)
7080
return () =>
7181
window.removeEventListener("beforeunload", handleBeforeUnload)
72-
}, [])
82+
}, [closeProtection])
7383

7484
return (
7585
<div className="h-screen bg-background relative overflow-hidden">
@@ -127,6 +137,7 @@ export default function Home() {
127137
setDrawioUi(newTheme)
128138
}}
129139
isMobile={isMobile}
140+
onCloseProtectionChange={setCloseProtection}
130141
/>
131142
</div>
132143
</ResizablePanel>

components/chat-panel.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface ChatPanelProps {
3131
drawioUi: "min" | "sketch"
3232
onToggleDrawioUi: () => void
3333
isMobile?: boolean
34+
onCloseProtectionChange?: (enabled: boolean) => void
3435
}
3536

3637
export default function ChatPanel({
@@ -39,6 +40,7 @@ export default function ChatPanel({
3940
drawioUi,
4041
onToggleDrawioUi,
4142
isMobile = false,
43+
onCloseProtectionChange,
4244
}: ChatPanelProps) {
4345
const {
4446
loadDiagram: onDisplayChart,
@@ -497,19 +499,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
497499
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
498500
/>
499501
</a>
500-
{accessCodeRequired && (
501-
<ButtonWithTooltip
502-
tooltipContent="Settings"
503-
variant="ghost"
504-
size="icon"
505-
onClick={() => setShowSettingsDialog(true)}
506-
className="hover:bg-accent"
507-
>
508-
<Settings
509-
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
510-
/>
511-
</ButtonWithTooltip>
512-
)}
502+
<ButtonWithTooltip
503+
tooltipContent="Settings"
504+
variant="ghost"
505+
size="icon"
506+
onClick={() => setShowSettingsDialog(true)}
507+
className="hover:bg-accent"
508+
>
509+
<Settings
510+
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
511+
/>
512+
</ButtonWithTooltip>
513513
{!isMobile && (
514514
<ButtonWithTooltip
515515
tooltipContent="Hide chat panel (Ctrl+B)"
@@ -570,6 +570,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
570570
<SettingsDialog
571571
open={showSettingsDialog}
572572
onOpenChange={setShowSettingsDialog}
573+
onCloseProtectionChange={onCloseProtectionChange}
573574
/>
574575
</div>
575576
)

components/settings-dialog.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,47 @@ import {
1111
DialogTitle,
1212
} from "@/components/ui/dialog"
1313
import { Input } from "@/components/ui/input"
14+
import { Label } from "@/components/ui/label"
15+
import { Switch } from "@/components/ui/switch"
1416

1517
interface SettingsDialogProps {
1618
open: boolean
1719
onOpenChange: (open: boolean) => void
20+
onCloseProtectionChange?: (enabled: boolean) => void
1821
}
1922

2023
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
24+
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
2125

22-
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
26+
export function SettingsDialog({
27+
open,
28+
onOpenChange,
29+
onCloseProtectionChange,
30+
}: SettingsDialogProps) {
2331
const [accessCode, setAccessCode] = useState("")
32+
const [closeProtection, setCloseProtection] = useState(true)
2433

2534
useEffect(() => {
2635
if (open) {
2736
const storedCode =
2837
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
2938
setAccessCode(storedCode)
39+
40+
const storedCloseProtection = localStorage.getItem(
41+
STORAGE_CLOSE_PROTECTION_KEY,
42+
)
43+
// Default to true if not set
44+
setCloseProtection(storedCloseProtection !== "false")
3045
}
3146
}, [open])
3247

3348
const handleSave = () => {
3449
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
50+
localStorage.setItem(
51+
STORAGE_CLOSE_PROTECTION_KEY,
52+
closeProtection.toString(),
53+
)
54+
onCloseProtectionChange?.(closeProtection)
3555
onOpenChange(false)
3656
}
3757

@@ -68,6 +88,21 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
6888
Required if the server has enabled access control.
6989
</p>
7090
</div>
91+
<div className="flex items-center justify-between">
92+
<div className="space-y-0.5">
93+
<Label htmlFor="close-protection">
94+
Close Protection
95+
</Label>
96+
<p className="text-[0.8rem] text-muted-foreground">
97+
Show confirmation when leaving the page.
98+
</p>
99+
</div>
100+
<Switch
101+
id="close-protection"
102+
checked={closeProtection}
103+
onCheckedChange={setCloseProtection}
104+
/>
105+
</div>
71106
</div>
72107
<DialogFooter>
73108
<Button

components/ui/label.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as LabelPrimitive from "@radix-ui/react-label"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Label({
9+
className,
10+
...props
11+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
12+
return (
13+
<LabelPrimitive.Root
14+
data-slot="label"
15+
className={cn(
16+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17+
className
18+
)}
19+
{...props}
20+
/>
21+
)
22+
}
23+
24+
export { Label }

components/ui/switch.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as SwitchPrimitive from "@radix-ui/react-switch"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Switch({
9+
className,
10+
...props
11+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
12+
return (
13+
<SwitchPrimitive.Root
14+
data-slot="switch"
15+
className={cn(
16+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
17+
className
18+
)}
19+
{...props}
20+
>
21+
<SwitchPrimitive.Thumb
22+
data-slot="switch-thumb"
23+
className={cn(
24+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
25+
)}
26+
/>
27+
</SwitchPrimitive.Root>
28+
)
29+
}
30+
31+
export { Switch }

0 commit comments

Comments
 (0)