fix: split category fetch, safe localStorage, aria-labels on icon buttons
- TreeLibraryPage: split categories into a mount-only fetch so filter changes only re-fetch trees (not categories every time) - Add safeGetItem/safeSetItem/safeRemoveItem helpers in utils.ts to prevent crashes in private browsing or when storage is unavailable - Replace raw localStorage calls in ScratchpadSidebar, TreeNavigationPage, TreeEditorPage, and treeEditorStore with safe wrappers - Add aria-label to 20 icon-only buttons across 8 component files for screen reader accessibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,7 @@ export function CategoryRow({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Edit category"
|
title="Edit category"
|
||||||
|
aria-label="Edit category"
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -94,6 +95,7 @@ export function CategoryRow({
|
|||||||
'hover:bg-accent hover:text-accent-foreground'
|
'hover:bg-accent hover:text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
title="Archive category"
|
title="Archive category"
|
||||||
|
aria-label="Archive category"
|
||||||
>
|
>
|
||||||
<Archive className="h-4 w-4" />
|
<Archive className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -106,6 +108,7 @@ export function CategoryRow({
|
|||||||
'hover:bg-accent hover:text-accent-foreground'
|
'hover:bg-accent hover:text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
title="Restore category"
|
title="Restore category"
|
||||||
|
aria-label="Restore category"
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-4 w-4" />
|
<RotateCcw className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export function AddToFolderMenu({ treeId, onFolderCreated }: AddToFolderMenuProp
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Add to folder"
|
title="Add to folder"
|
||||||
|
aria-label="Add to folder"
|
||||||
>
|
>
|
||||||
<FolderPlus className="h-4 w-4" />
|
<FolderPlus className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export function TreeGridView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Fork tree"
|
title="Fork tree"
|
||||||
|
aria-label="Fork tree"
|
||||||
>
|
>
|
||||||
<GitBranch className="h-4 w-4" />
|
<GitBranch className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -97,6 +98,7 @@ export function TreeGridView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Edit tree"
|
title="Edit tree"
|
||||||
|
aria-label="Edit tree"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -110,6 +112,7 @@ export function TreeGridView({
|
|||||||
'hover:bg-red-400/10 hover:text-red-400'
|
'hover:bg-red-400/10 hover:text-red-400'
|
||||||
)}
|
)}
|
||||||
title="Delete tree"
|
title="Delete tree"
|
||||||
|
aria-label="Delete tree"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export function TreeListView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Fork tree"
|
title="Fork tree"
|
||||||
|
aria-label="Fork tree"
|
||||||
>
|
>
|
||||||
<GitBranch className="h-4 w-4" />
|
<GitBranch className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -100,6 +101,7 @@ export function TreeListView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Edit tree"
|
title="Edit tree"
|
||||||
|
aria-label="Edit tree"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export function TreeTableView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Fork tree"
|
title="Fork tree"
|
||||||
|
aria-label="Fork tree"
|
||||||
>
|
>
|
||||||
<GitBranch className="h-3.5 w-3.5" />
|
<GitBranch className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -204,6 +205,7 @@ export function TreeTableView({
|
|||||||
'hover:bg-white/10 hover:text-white'
|
'hover:bg-white/10 hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Edit tree"
|
title="Edit tree"
|
||||||
|
aria-label="Edit tree"
|
||||||
>
|
>
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
|||||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Grid view"
|
title="Grid view"
|
||||||
|
aria-label="Grid view"
|
||||||
>
|
>
|
||||||
<LayoutGrid className="h-4 w-4" />
|
<LayoutGrid className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -35,6 +36,7 @@ export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
|||||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="List view"
|
title="List view"
|
||||||
|
aria-label="List view"
|
||||||
>
|
>
|
||||||
<List className="h-4 w-4" />
|
<List className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -48,6 +50,7 @@ export function ViewToggle({ view, onChange, className }: ViewToggleProps) {
|
|||||||
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
|
||||||
)}
|
)}
|
||||||
title="Table view"
|
title="Table view"
|
||||||
|
aria-label="Table view"
|
||||||
>
|
>
|
||||||
<Table className="h-4 w-4" />
|
<Table className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, safeGetItem, safeSetItem } from '@/lib/utils'
|
||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
import { StickyNote, X, Eye, Pencil, Loader2 } from 'lucide-react'
|
import { StickyNote, X, Eye, Pencil, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
|||||||
const [content, setContent] = useState(initialContent)
|
const [content, setContent] = useState(initialContent)
|
||||||
const [lastSaved, setLastSaved] = useState(initialContent)
|
const [lastSaved, setLastSaved] = useState(initialContent)
|
||||||
const [isCollapsed, setIsCollapsed] = useState(() => {
|
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||||
return localStorage.getItem('scratchpad-collapsed') !== 'false'
|
return safeGetItem('scratchpad-collapsed') !== 'false'
|
||||||
})
|
})
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [showPreview, setShowPreview] = useState(false)
|
const [showPreview, setShowPreview] = useState(false)
|
||||||
@@ -43,7 +43,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
|||||||
|
|
||||||
// Persist collapse state and notify parent
|
// Persist collapse state and notify parent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('scratchpad-collapsed', String(isCollapsed))
|
safeSetItem('scratchpad-collapsed', String(isCollapsed))
|
||||||
onOpenChange?.(!isCollapsed)
|
onOpenChange?.(!isCollapsed)
|
||||||
}, [isCollapsed, onOpenChange])
|
}, [isCollapsed, onOpenChange])
|
||||||
|
|
||||||
@@ -130,6 +130,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
|||||||
isCollapsed ? 'opacity-100' : 'pointer-events-none opacity-0'
|
isCollapsed ? 'opacity-100' : 'pointer-events-none opacity-0'
|
||||||
)}
|
)}
|
||||||
title="Open scratchpad (Ctrl+/)"
|
title="Open scratchpad (Ctrl+/)"
|
||||||
|
aria-label="Open scratchpad (Ctrl+/)"
|
||||||
>
|
>
|
||||||
<StickyNote className="h-5 w-5" />
|
<StickyNote className="h-5 w-5" />
|
||||||
{hasUnsavedChanges && (
|
{hasUnsavedChanges && (
|
||||||
@@ -176,6 +177,7 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
|||||||
onClick={() => setIsCollapsed(true)}
|
onClick={() => setIsCollapsed(true)}
|
||||||
className="rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
className="rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
||||||
title="Close scratchpad"
|
title="Close scratchpad"
|
||||||
|
aria-label="Close scratchpad"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function DynamicArrayField<T>({
|
|||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
||||||
title="Move up"
|
title="Move up"
|
||||||
|
aria-label="Move up"
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-3 w-3" />
|
<ChevronUp className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -62,6 +63,7 @@ export function DynamicArrayField<T>({
|
|||||||
disabled={index === items.length - 1}
|
disabled={index === items.length - 1}
|
||||||
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
className="rounded p-0.5 text-white/50 hover:bg-white/[0.06] hover:text-white disabled:opacity-30"
|
||||||
title="Move down"
|
title="Move down"
|
||||||
|
aria-label="Move down"
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -78,6 +80,7 @@ export function DynamicArrayField<T>({
|
|||||||
onClick={() => onRemove(index)}
|
onClick={() => onRemove(index)}
|
||||||
className="mt-1 rounded p-1 text-white/50 hover:bg-red-400/20 hover:text-red-400"
|
className="mt-1 rounded p-1 text-white/50 hover:bg-red-400/20 hover:text-red-400"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
|
aria-label="Remove"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ function NodeListItem({
|
|||||||
onAddChild(node.id)
|
onAddChild(node.id)
|
||||||
}}
|
}}
|
||||||
title="Add child node"
|
title="Add child node"
|
||||||
|
aria-label="Add child node"
|
||||||
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
@@ -298,6 +299,7 @@ function NodeListItem({
|
|||||||
onEdit(node)
|
onEdit(node)
|
||||||
}}
|
}}
|
||||||
title="Edit node"
|
title="Edit node"
|
||||||
|
aria-label="Edit node"
|
||||||
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
@@ -311,6 +313,7 @@ function NodeListItem({
|
|||||||
onDuplicate(node.id)
|
onDuplicate(node.id)
|
||||||
}}
|
}}
|
||||||
title="Duplicate node"
|
title="Duplicate node"
|
||||||
|
aria-label="Duplicate node"
|
||||||
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="h-3 w-3" />
|
||||||
@@ -322,6 +325,7 @@ function NodeListItem({
|
|||||||
onDelete(node.id)
|
onDelete(node.id)
|
||||||
}}
|
}}
|
||||||
title="Delete node"
|
title="Delete node"
|
||||||
|
aria-label="Delete node"
|
||||||
className="rounded p-1 text-muted-foreground hover:bg-destructive/20 hover:text-destructive"
|
className="rounded p-1 text-muted-foreground hover:bg-destructive/20 hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
|
|||||||
@@ -4,3 +4,28 @@ import { twMerge } from 'tailwind-merge'
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Safe localStorage access — returns null on error (e.g. private browsing) */
|
||||||
|
export function safeGetItem(key: string): string | null {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeSetItem(key: string, value: string): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
} catch {
|
||||||
|
// Storage full or unavailable — silently fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeRemoveItem(key: string): void {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
} catch {
|
||||||
|
// Storage unavailable — silently fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TreeEditorLayout } from '@/components/tree-editor/TreeEditorLayout'
|
|||||||
import { ValidationSummary } from '@/components/tree-editor/ValidationSummary'
|
import { ValidationSummary } from '@/components/tree-editor/ValidationSummary'
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, safeGetItem } from '@/lib/utils'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
|
|
||||||
export function TreeEditorPage() {
|
export function TreeEditorPage() {
|
||||||
@@ -156,7 +156,7 @@ export function TreeEditorPage() {
|
|||||||
initNewTree()
|
initNewTree()
|
||||||
setTreeStatus('draft') // New trees start as draft
|
setTreeStatus('draft') // New trees start as draft
|
||||||
// Check for draft after initializing
|
// Check for draft after initializing
|
||||||
const draftExists = localStorage.getItem('tree-editor-draft') !== null
|
const draftExists = safeGetItem('tree-editor-draft') !== null
|
||||||
if (draftExists) {
|
if (draftExists) {
|
||||||
setShowDraftPrompt(true)
|
setShowDraftPrompt(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,16 @@ export function TreeLibraryPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Load categories once on mount (they rarely change)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
categoriesApi.list()
|
||||||
|
.then(setCategories)
|
||||||
|
.catch((err) => console.error('Failed to load categories:', err))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load trees when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
loadTrees()
|
||||||
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, showDrafts])
|
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, showDrafts])
|
||||||
|
|
||||||
// Load folders on mount and listen for changes
|
// Load folders on mount and listen for changes
|
||||||
@@ -70,21 +78,17 @@ export function TreeLibraryPage() {
|
|||||||
return () => window.removeEventListener('folder-changed', handleFolderChange)
|
return () => window.removeEventListener('folder-changed', handleFolderChange)
|
||||||
}, [loadFolders])
|
}, [loadFolders])
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadTrees = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const [treesData, categoriesData] = await Promise.all([
|
const treesData = await treesApi.list({
|
||||||
treesApi.list({
|
category_id: selectedCategoryId || undefined,
|
||||||
category_id: selectedCategoryId || undefined,
|
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
|
||||||
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
|
folder_id: selectedFolderId || undefined,
|
||||||
folder_id: selectedFolderId || undefined,
|
sort_by: treeLibrarySortBy,
|
||||||
sort_by: treeLibrarySortBy,
|
include_drafts: showDrafts || undefined,
|
||||||
include_drafts: showDrafts || undefined,
|
})
|
||||||
}),
|
|
||||||
categoriesApi.list(),
|
|
||||||
])
|
|
||||||
setTrees(treesData)
|
setTrees(treesData)
|
||||||
setCategories(categoriesData)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to load trees')
|
toast.error('Failed to load trees')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -95,7 +99,7 @@ export function TreeLibraryPage() {
|
|||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
loadData()
|
loadTrees()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@@ -413,7 +417,7 @@ export function TreeLibraryPage() {
|
|||||||
setFolderModalOpen(false)
|
setFolderModalOpen(false)
|
||||||
setNewFolderParentId(null)
|
setNewFolderParentId(null)
|
||||||
}}
|
}}
|
||||||
onSave={loadData}
|
onSave={loadTrees}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation */}
|
{/* Delete Confirmation */}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { treesApi, sessionsApi } from '@/api'
|
|||||||
import { useTreeNavigationShortcuts } from '@/hooks/useKeyboardShortcuts'
|
import { useTreeNavigationShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||||
import { useCustomStepFlow } from '@/hooks/useCustomStepFlow'
|
import { useCustomStepFlow } from '@/hooks/useCustomStepFlow'
|
||||||
import type { Tree, Session, DecisionRecord, TreeStructure } from '@/types'
|
import type { Tree, Session, DecisionRecord, TreeStructure } from '@/types'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, safeGetItem } from '@/lib/utils'
|
||||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||||
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
||||||
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar } from '@/components/session'
|
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar } from '@/components/session'
|
||||||
@@ -37,7 +37,7 @@ export function TreeNavigationPage() {
|
|||||||
|
|
||||||
// Scratchpad state
|
// Scratchpad state
|
||||||
const [scratchpadOpen, setScratchpadOpen] = useState(() => {
|
const [scratchpadOpen, setScratchpadOpen] = useState(() => {
|
||||||
return localStorage.getItem('scratchpad-collapsed') === 'false'
|
return safeGetItem('scratchpad-collapsed') === 'false'
|
||||||
})
|
})
|
||||||
|
|
||||||
const findNode = (nodeId: string, structure?: TreeStructure): TreeStructure | null => {
|
const findNode = (nodeId: string, structure?: TreeStructure): TreeStructure | null => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { shallow } from 'zustand/shallow'
|
|||||||
import { immer } from 'zustand/middleware/immer'
|
import { immer } from 'zustand/middleware/immer'
|
||||||
import type { Tree, TreeStructure, TreeCreate, TreeUpdate, NodeType, TreeMarkdownValidationError, TreeMarkdownValidation } from '@/types'
|
import type { Tree, TreeStructure, TreeCreate, TreeUpdate, NodeType, TreeMarkdownValidationError, TreeMarkdownValidation } from '@/types'
|
||||||
import { treeStructureToMarkdownPreview } from '@/lib/treeMarkdownSync'
|
import { treeStructureToMarkdownPreview } from '@/lib/treeMarkdownSync'
|
||||||
|
import { safeGetItem, safeSetItem, safeRemoveItem } from '@/lib/utils'
|
||||||
|
|
||||||
// Throttle helper: captures first call immediately, then throttles subsequent calls
|
// Throttle helper: captures first call immediately, then throttles subsequent calls
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -304,7 +305,7 @@ export const useTreeEditorStore = create<TreeEditorState>()(
|
|||||||
|
|
||||||
// Check for existing draft on init
|
// Check for existing draft on init
|
||||||
initNewTree: () => {
|
initNewTree: () => {
|
||||||
const hasDraft = localStorage.getItem(DRAFT_STORAGE_KEY) !== null
|
const hasDraft = safeGetItem(DRAFT_STORAGE_KEY) !== null
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.treeId = null
|
state.treeId = null
|
||||||
state.name = ''
|
state.name = ''
|
||||||
@@ -360,7 +361,7 @@ export const useTreeEditorStore = create<TreeEditorState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadDraft: () => {
|
loadDraft: () => {
|
||||||
const draftJson = localStorage.getItem(DRAFT_STORAGE_KEY)
|
const draftJson = safeGetItem(DRAFT_STORAGE_KEY)
|
||||||
if (!draftJson) return false
|
if (!draftJson) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -380,13 +381,13 @@ export const useTreeEditorStore = create<TreeEditorState>()(
|
|||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
safeRemoveItem(DRAFT_STORAGE_KEY)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
discardDraft: () => {
|
discardDraft: () => {
|
||||||
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
safeRemoveItem(DRAFT_STORAGE_KEY)
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.hasDraft = false
|
state.hasDraft = false
|
||||||
})
|
})
|
||||||
@@ -868,12 +869,12 @@ export const useTreeEditorStore = create<TreeEditorState>()(
|
|||||||
treeStructure: state.treeStructure,
|
treeStructure: state.treeStructure,
|
||||||
savedAt: new Date().toISOString()
|
savedAt: new Date().toISOString()
|
||||||
}
|
}
|
||||||
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft))
|
safeSetItem(DRAFT_STORAGE_KEY, JSON.stringify(draft))
|
||||||
set((s) => { s.draftSavedAt = new Date() })
|
set((s) => { s.draftSavedAt = new Date() })
|
||||||
},
|
},
|
||||||
|
|
||||||
markSaved: () => {
|
markSaved: () => {
|
||||||
localStorage.removeItem(DRAFT_STORAGE_KEY)
|
safeRemoveItem(DRAFT_STORAGE_KEY)
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.isDirty = false
|
state.isDirty = false
|
||||||
state.lastSavedAt = new Date()
|
state.lastSavedAt = new Date()
|
||||||
|
|||||||
Reference in New Issue
Block a user