mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
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
This commit is contained in:
@@ -31,6 +31,7 @@ interface ChatPanelProps {
|
||||
drawioUi: "min" | "sketch"
|
||||
onToggleDrawioUi: () => void
|
||||
isMobile?: boolean
|
||||
onCloseProtectionChange?: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
export default function ChatPanel({
|
||||
@@ -39,6 +40,7 @@ export default function ChatPanel({
|
||||
drawioUi,
|
||||
onToggleDrawioUi,
|
||||
isMobile = false,
|
||||
onCloseProtectionChange,
|
||||
}: ChatPanelProps) {
|
||||
const {
|
||||
loadDiagram: onDisplayChart,
|
||||
@@ -497,19 +499,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||
/>
|
||||
</a>
|
||||
{accessCodeRequired && (
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Settings"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSettingsDialog(true)}
|
||||
className="hover:bg-accent"
|
||||
>
|
||||
<Settings
|
||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||
/>
|
||||
</ButtonWithTooltip>
|
||||
)}
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Settings"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSettingsDialog(true)}
|
||||
className="hover:bg-accent"
|
||||
>
|
||||
<Settings
|
||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||
/>
|
||||
</ButtonWithTooltip>
|
||||
{!isMobile && (
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
||||
@@ -570,6 +570,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
<SettingsDialog
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
onCloseProtectionChange={onCloseProtectionChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,27 +11,47 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
interface SettingsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onCloseProtectionChange?: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
||||
|
||||
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||
export function SettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onCloseProtectionChange,
|
||||
}: SettingsDialogProps) {
|
||||
const [accessCode, setAccessCode] = useState("")
|
||||
const [closeProtection, setCloseProtection] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const storedCode =
|
||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||
setAccessCode(storedCode)
|
||||
|
||||
const storedCloseProtection = localStorage.getItem(
|
||||
STORAGE_CLOSE_PROTECTION_KEY,
|
||||
)
|
||||
// Default to true if not set
|
||||
setCloseProtection(storedCloseProtection !== "false")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||
localStorage.setItem(
|
||||
STORAGE_CLOSE_PROTECTION_KEY,
|
||||
closeProtection.toString(),
|
||||
)
|
||||
onCloseProtectionChange?.(closeProtection)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
@@ -68,6 +88,21 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||
Required if the server has enabled access control.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="close-protection">
|
||||
Close Protection
|
||||
</Label>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Show confirmation when leaving the page.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="close-protection"
|
||||
checked={closeProtection}
|
||||
onCheckedChange={setCloseProtection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
31
components/ui/switch.tsx
Normal file
31
components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
Reference in New Issue
Block a user