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:
Michael Chihlas
2026-02-10 18:41:46 -05:00
parent 0c9fbdc90b
commit d155c83ef0
14 changed files with 81 additions and 28 deletions

View File

@@ -10,7 +10,7 @@ import { TreeEditorLayout } from '@/components/tree-editor/TreeEditorLayout'
import { ValidationSummary } from '@/components/tree-editor/ValidationSummary'
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
import { usePermissions } from '@/hooks/usePermissions'
import { cn } from '@/lib/utils'
import { cn, safeGetItem } from '@/lib/utils'
import { toast } from '@/lib/toast'
export function TreeEditorPage() {
@@ -156,7 +156,7 @@ export function TreeEditorPage() {
initNewTree()
setTreeStatus('draft') // New trees start as draft
// Check for draft after initializing
const draftExists = localStorage.getItem('tree-editor-draft') !== null
const draftExists = safeGetItem('tree-editor-draft') !== null
if (draftExists) {
setShowDraftPrompt(true)
}

View File

@@ -58,8 +58,16 @@ export function TreeLibraryPage() {
}
}, [])
// Load categories once on mount (they rarely change)
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])
// Load folders on mount and listen for changes
@@ -70,21 +78,17 @@ export function TreeLibraryPage() {
return () => window.removeEventListener('folder-changed', handleFolderChange)
}, [loadFolders])
const loadData = async () => {
const loadTrees = async () => {
setIsLoading(true)
try {
const [treesData, categoriesData] = await Promise.all([
treesApi.list({
category_id: selectedCategoryId || undefined,
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
folder_id: selectedFolderId || undefined,
sort_by: treeLibrarySortBy,
include_drafts: showDrafts || undefined,
}),
categoriesApi.list(),
])
const treesData = await treesApi.list({
category_id: selectedCategoryId || undefined,
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
folder_id: selectedFolderId || undefined,
sort_by: treeLibrarySortBy,
include_drafts: showDrafts || undefined,
})
setTrees(treesData)
setCategories(categoriesData)
} catch (err) {
toast.error('Failed to load trees')
console.error(err)
@@ -95,7 +99,7 @@ export function TreeLibraryPage() {
const handleSearch = async () => {
if (!searchQuery.trim()) {
loadData()
loadTrees()
return
}
setIsLoading(true)
@@ -413,7 +417,7 @@ export function TreeLibraryPage() {
setFolderModalOpen(false)
setNewFolderParentId(null)
}}
onSave={loadData}
onSave={loadTrees}
/>
{/* Delete Confirmation */}

View File

@@ -4,7 +4,7 @@ import { treesApi, sessionsApi } from '@/api'
import { useTreeNavigationShortcuts } from '@/hooks/useKeyboardShortcuts'
import { useCustomStepFlow } from '@/hooks/useCustomStepFlow'
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 { CustomStepModal } from '@/components/step-library/CustomStepModal'
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar } from '@/components/session'
@@ -37,7 +37,7 @@ export function TreeNavigationPage() {
// Scratchpad state
const [scratchpadOpen, setScratchpadOpen] = useState(() => {
return localStorage.getItem('scratchpad-collapsed') === 'false'
return safeGetItem('scratchpad-collapsed') === 'false'
})
const findNode = (nodeId: string, structure?: TreeStructure): TreeStructure | null => {