v0.1.1: 多Agent代码质量审查+优化
- fix: useCodeMirror onChange闭包过期导致编辑器内容丢失同步(P0) - feat: 全部组件添加displayName(React DevTools调试支持) - feat: 全部组件添加ARIA属性(WCAG无障碍支持) - fix: App异步操作添加try-catch错误边界 - fix: tabStore空catch块添加错误日志 - fix: sidebarStore Set→string[](Zustand可序列化) - fix: useDragDrop readFile错误处理+空文件修复 - fix: useSettings/useTheme load()添加错误处理 - fix: useTheme toggleDarkMode引用优化 - chore: 版本号升级至0.1.1
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# MarkLite v0.1.0 — 架构设计文档
|
# MarkLite v0.1.1 — 架构设计文档
|
||||||
|
|
||||||
## 1. 项目概述
|
## 1. 项目概述
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
|
<img src="https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript" alt="TypeScript">
|
||||||
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
<img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react" alt="React">
|
||||||
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
||||||
<img src="https://img.shields.io/badge/Version-v0.1.0-orange?style=flat-square" alt="Version">
|
<img src="https://img.shields.io/badge/Version-v0.1.1-orange?style=flat-square" alt="Version">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "marklite",
|
"name": "marklite",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "Lightweight Markdown Editor for Windows",
|
"description": "Lightweight Markdown Editor for Windows",
|
||||||
"main": "./dist/main/index.js",
|
"main": "./dist/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+43
-27
@@ -21,9 +21,9 @@ import { ModifiedBanner } from './components/ModifiedBanner/ModifiedBanner'
|
|||||||
import { DropOverlay } from './components/DropOverlay/DropOverlay'
|
import { DropOverlay } from './components/DropOverlay/DropOverlay'
|
||||||
import { AppIcon, Gitee } from './components/Icons'
|
import { AppIcon, Gitee } from './components/Icons'
|
||||||
|
|
||||||
const APP_VERSION = 'v0.1.0'
|
const APP_VERSION = 'v0.1.1'
|
||||||
|
|
||||||
export default function App() {
|
export function App() {
|
||||||
const tabs = useTabStore(s => s.tabs)
|
const tabs = useTabStore(s => s.tabs)
|
||||||
const activeTabId = useTabStore(s => s.activeTabId)
|
const activeTabId = useTabStore(s => s.activeTabId)
|
||||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||||
@@ -89,42 +89,54 @@ export default function App() {
|
|||||||
|
|
||||||
// 打开文件
|
// 打开文件
|
||||||
const handleOpenFile = useCallback(async () => {
|
const handleOpenFile = useCallback(async () => {
|
||||||
if (window.electronAPI) {
|
try {
|
||||||
const result = await window.electronAPI.openFile()
|
if (window.electronAPI) {
|
||||||
if (result && 'filePath' in result) {
|
const result = await window.electronAPI.openFile()
|
||||||
createTab(result.filePath, result.content)
|
if (result && 'filePath' in result) {
|
||||||
if (result.filePath) recentFilesRepository.add(result.filePath)
|
createTab(result.filePath, result.content)
|
||||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
if (result.filePath) recentFilesRepository.add(result.filePath)
|
||||||
|
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('操作失败')
|
||||||
}
|
}
|
||||||
}, [createTab])
|
}, [createTab, showToast])
|
||||||
|
|
||||||
// 保存文件
|
// 保存文件
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
const tab = getActiveTab()
|
try {
|
||||||
if (!tab) return
|
const tab = getActiveTab()
|
||||||
if (window.electronAPI) {
|
if (!tab) return
|
||||||
const result = await window.electronAPI.saveFile({
|
if (window.electronAPI) {
|
||||||
filePath: tab.filePath,
|
const result = await window.electronAPI.saveFile({
|
||||||
content: tab.content
|
filePath: tab.filePath,
|
||||||
})
|
content: tab.content
|
||||||
if (result.success) {
|
})
|
||||||
useTabStore.getState().setModified(tab.id, false)
|
if (result.success) {
|
||||||
showToast('已保存')
|
useTabStore.getState().setModified(tab.id, false)
|
||||||
|
showToast('已保存')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('操作失败')
|
||||||
}
|
}
|
||||||
}, [getActiveTab, showToast])
|
}, [getActiveTab, showToast])
|
||||||
|
|
||||||
// 另存为
|
// 另存为
|
||||||
const handleSaveAs = useCallback(async () => {
|
const handleSaveAs = useCallback(async () => {
|
||||||
const tab = getActiveTab()
|
try {
|
||||||
if (!tab) return
|
const tab = getActiveTab()
|
||||||
if (window.electronAPI) {
|
if (!tab) return
|
||||||
const result = await window.electronAPI.saveFileAs({ content: tab.content })
|
if (window.electronAPI) {
|
||||||
if (result.success) {
|
const result = await window.electronAPI.saveFileAs({ content: tab.content })
|
||||||
useTabStore.getState().setModified(tab.id, false)
|
if (result.success) {
|
||||||
showToast('已保存')
|
useTabStore.getState().setModified(tab.id, false)
|
||||||
|
showToast('已保存')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
showToast('操作失败')
|
||||||
}
|
}
|
||||||
}, [getActiveTab, showToast])
|
}, [getActiveTab, showToast])
|
||||||
|
|
||||||
@@ -247,7 +259,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* 关于弹框 */}
|
{/* 关于弹框 */}
|
||||||
{showAbout && (
|
{showAbout && (
|
||||||
<div className="about-overlay" onClick={() => setShowAbout(false)}>
|
<div className="about-overlay" onClick={() => setShowAbout(false)} role="dialog" aria-modal="true" aria-label="关于 MarkLite">
|
||||||
<div className="about-dialog" onClick={e => e.stopPropagation()}>
|
<div className="about-dialog" onClick={e => e.stopPropagation()}>
|
||||||
<div className="about-header">
|
<div className="about-header">
|
||||||
<AppIcon size={64} />
|
<AppIcon size={64} />
|
||||||
@@ -289,3 +301,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.displayName = 'App'
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|||||||
@@ -87,9 +87,11 @@ export function Editor({ darkMode }: EditorProps) {
|
|||||||
}, [viewRef])
|
}, [viewRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container">
|
<div className="editor-container" role="region" aria-label="Markdown编辑器">
|
||||||
<EditorToolbar viewRef={viewRef} />
|
<EditorToolbar viewRef={viewRef} />
|
||||||
<div ref={containerRef} className="codemirror-wrapper" />
|
<div ref={containerRef} className="codemirror-wrapper" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Editor.displayName = 'Editor'
|
||||||
@@ -17,6 +17,12 @@ export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOpti
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const viewRef = useRef<EditorView | null>(null)
|
const viewRef = useRef<EditorView | null>(null)
|
||||||
const isExternalUpdate = useRef(false)
|
const isExternalUpdate = useRef(false)
|
||||||
|
const onChangeRef = useRef(onChange)
|
||||||
|
|
||||||
|
// 始终保持 onChangeRef 为最新回调
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeRef.current = onChange
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
// 初始化编辑器
|
// 初始化编辑器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,7 +67,7 @@ export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOpti
|
|||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of(update => {
|
EditorView.updateListener.of(update => {
|
||||||
if (update.docChanged && !isExternalUpdate.current) {
|
if (update.docChanged && !isExternalUpdate.current) {
|
||||||
onChange(update.state.doc.toString())
|
onChangeRef.current(update.state.doc.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -56,8 +56,13 @@ export function Preview() {
|
|||||||
ref={previewRef}
|
ref={previewRef}
|
||||||
id="preview"
|
id="preview"
|
||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
|
role="region"
|
||||||
|
aria-label="Markdown预览"
|
||||||
|
aria-live="polite"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Preview.displayName = 'Preview'
|
||||||
@@ -128,7 +128,7 @@ export function Sidebar() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sidebar-tree">
|
<div id="sidebar-tree" role="tree" aria-label="文件树">
|
||||||
{/* 独立文件区 */}
|
{/* 独立文件区 */}
|
||||||
{independentFiles.length > 0 && (
|
{independentFiles.length > 0 && (
|
||||||
<div className="independent-files-section">
|
<div className="independent-files-section">
|
||||||
@@ -138,6 +138,7 @@ export function Sidebar() {
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`tree-item independent-file-item ${tab.id === activeTabId ? 'active' : ''}`}
|
className={`tree-item independent-file-item ${tab.id === activeTabId ? 'active' : ''}`}
|
||||||
style={{ paddingLeft: '8px' }}
|
style={{ paddingLeft: '8px' }}
|
||||||
|
role="treeitem"
|
||||||
onClick={() => switchToTab(tab.id)}
|
onClick={() => switchToTab(tab.id)}
|
||||||
>
|
>
|
||||||
<span className="tree-icon">
|
<span className="tree-icon">
|
||||||
@@ -179,7 +180,7 @@ export function Sidebar() {
|
|||||||
function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFilePath, onFileClick }: {
|
function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFilePath, onFileClick }: {
|
||||||
nodes: FileNode[]
|
nodes: FileNode[]
|
||||||
depth: number
|
depth: number
|
||||||
expandedDirs: Set<string>
|
expandedDirs: string[]
|
||||||
toggleDir: (path: string) => void
|
toggleDir: (path: string) => void
|
||||||
activeTabId: string | null
|
activeTabId: string | null
|
||||||
activeFilePath: string | null
|
activeFilePath: string | null
|
||||||
@@ -192,6 +193,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
|
|||||||
<div
|
<div
|
||||||
className={`tree-item ${node.type === 'file' && node.path === activeFilePath ? 'active' : ''}`}
|
className={`tree-item ${node.type === 'file' && node.path === activeFilePath ? 'active' : ''}`}
|
||||||
style={{ paddingLeft: (8 + depth * 16) + 'px' }}
|
style={{ paddingLeft: (8 + depth * 16) + 'px' }}
|
||||||
|
role="treeitem"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (node.type === 'dir') {
|
if (node.type === 'dir') {
|
||||||
toggleDir(node.path)
|
toggleDir(node.path)
|
||||||
@@ -202,7 +204,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
|
|||||||
>
|
>
|
||||||
{node.type === 'dir' ? (
|
{node.type === 'dir' ? (
|
||||||
<>
|
<>
|
||||||
<span className={`tree-arrow ${expandedDirs.has(node.path) ? 'expanded' : ''}`}>
|
<span className={`tree-arrow ${expandedDirs.includes(node.path) ? 'expanded' : ''}`}>
|
||||||
<ChevronRight size={10} />
|
<ChevronRight size={10} />
|
||||||
</span>
|
</span>
|
||||||
<span className="tree-icon">
|
<span className="tree-icon">
|
||||||
@@ -219,7 +221,7 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
|
|||||||
)}
|
)}
|
||||||
<span className="tree-name">{node.name}</span>
|
<span className="tree-name">{node.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{node.type === 'dir' && expandedDirs.has(node.path) && node.children && (
|
{node.type === 'dir' && expandedDirs.includes(node.path) && node.children && (
|
||||||
<FileTree
|
<FileTree
|
||||||
nodes={node.children}
|
nodes={node.children}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
@@ -235,3 +237,6 @@ function FileTree({ nodes, depth, expandedDirs, toggleDir, activeTabId, activeFi
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sidebar.displayName = 'Sidebar'
|
||||||
|
FileTree.displayName = 'FileTree'
|
||||||
@@ -9,7 +9,7 @@ export function StatusBar() {
|
|||||||
const tab = tabs.find(t => t.id === activeTabId) ?? null
|
const tab = tabs.find(t => t.id === activeTabId) ?? null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="statusbar">
|
<div id="statusbar" role="status" aria-live="polite">
|
||||||
<div className="status-left">
|
<div className="status-left">
|
||||||
<span id="status-text">
|
<span id="status-text">
|
||||||
{tab ? (tab.filePath ? getFileName(tab.filePath) : '未命名') : '就绪'}
|
{tab ? (tab.filePath ? getFileName(tab.filePath) : '未命名') : '就绪'}
|
||||||
@@ -23,3 +23,5 @@ export function StatusBar() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusBar.displayName = 'StatusBar'
|
||||||
@@ -149,11 +149,13 @@ export function TabBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="tab-bar">
|
<div id="tab-bar">
|
||||||
<div id="tab-list" ref={tabListRef}>
|
<div id="tab-list" ref={tabListRef} role="tablist" aria-label="标签页">
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
|
className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tab.id === activeTabId}
|
||||||
onClick={() => switchToTab(tab.id)}
|
onClick={() => switchToTab(tab.id)}
|
||||||
onContextMenu={(e) => handleContextMenu(e, tab.id)}
|
onContextMenu={(e) => handleContextMenu(e, tab.id)}
|
||||||
>
|
>
|
||||||
@@ -206,3 +208,5 @@ export function TabBar() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TabBar.displayName = 'TabBar'
|
||||||
@@ -13,7 +13,7 @@ interface ToolbarProps {
|
|||||||
|
|
||||||
export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode, onToggleDark, onShowAbout }: ToolbarProps) {
|
export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode, onToggleDark, onShowAbout }: ToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div id="toolbar">
|
<div id="toolbar" role="toolbar" aria-label="工具栏">
|
||||||
<div className="toolbar-left">
|
<div className="toolbar-left">
|
||||||
<button className="toolbar-btn" onClick={onOpen} title="打开文件 (Ctrl+O)">
|
<button className="toolbar-btn" onClick={onOpen} title="打开文件 (Ctrl+O)">
|
||||||
<FolderOpen size={18} />
|
<FolderOpen size={18} />
|
||||||
@@ -44,3 +44,5 @@ export function Toolbar({ onOpen, onSave, viewMode, onViewModeChange, darkMode,
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toolbar.displayName = 'Toolbar'
|
||||||
@@ -26,9 +26,13 @@ export function useDragDrop(showToast: (msg: string) => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronAPI) {
|
if (window.electronAPI) {
|
||||||
const result = await window.electronAPI.readFile(filePath)
|
try {
|
||||||
if (result.success && result.content) {
|
const result = await window.electronAPI.readFile(filePath)
|
||||||
createTab(filePath, result.content)
|
if (result.success && result.content !== undefined) {
|
||||||
|
createTab(filePath, result.content)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read file via electronAPI:', err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function useSettings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settingsRepository.load().then(settings => {
|
settingsRepository.load().then(settings => {
|
||||||
setViewMode(settings.viewMode ?? 'editor')
|
setViewMode(settings.viewMode ?? 'editor')
|
||||||
})
|
}).catch(err => console.error('Failed to load settings:', err))
|
||||||
}, [setViewMode])
|
}, [setViewMode])
|
||||||
|
|
||||||
// M-10: 使用 useCallback 稳定函数引用
|
// M-10: 使用 useCallback 稳定函数引用
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function useTheme() {
|
|||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
setDarkMode(settings.darkMode !== undefined ? settings.darkMode : prefersDark)
|
setDarkMode(settings.darkMode !== undefined ? settings.darkMode : prefersDark)
|
||||||
isInitialized.current = true
|
isInitialized.current = true
|
||||||
})
|
}).catch(err => console.error('Failed to load theme settings:', err))
|
||||||
}, [setDarkMode])
|
}, [setDarkMode])
|
||||||
|
|
||||||
// B-05: 首次加载完成后才保存主题设置
|
// B-05: 首次加载完成后才保存主题设置
|
||||||
@@ -23,5 +23,7 @@ export function useTheme() {
|
|||||||
settingsRepository.save({ darkMode })
|
settingsRepository.save({ darkMode })
|
||||||
}, [darkMode])
|
}, [darkMode])
|
||||||
|
|
||||||
return { darkMode, toggleDarkMode: useEditorStore(s => s.toggleDarkMode) }
|
const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
|
||||||
|
|
||||||
|
return { darkMode, toggleDarkMode }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface SidebarState {
|
|||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
rootPath: string | null
|
rootPath: string | null
|
||||||
tree: FileNode[]
|
tree: FileNode[]
|
||||||
expandedDirs: Set<string>
|
expandedDirs: string[]
|
||||||
sidebarWidth: number
|
sidebarWidth: number
|
||||||
_loaded: boolean
|
_loaded: boolean
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
rootPath: null,
|
rootPath: null,
|
||||||
tree: [],
|
tree: [],
|
||||||
expandedDirs: new Set<string>(),
|
expandedDirs: [],
|
||||||
sidebarWidth: 240,
|
sidebarWidth: 240,
|
||||||
_loaded: false,
|
_loaded: false,
|
||||||
|
|
||||||
@@ -50,26 +50,17 @@ export const useSidebarStore = create<SidebarState>((set, get) => ({
|
|||||||
setRootPath: (path) => set({ rootPath: path }),
|
setRootPath: (path) => set({ rootPath: path }),
|
||||||
setTree: (tree) => set({ tree }),
|
setTree: (tree) => set({ tree }),
|
||||||
toggleDir: (path) =>
|
toggleDir: (path) =>
|
||||||
set(state => {
|
set(state => ({
|
||||||
const newSet = new Set(state.expandedDirs)
|
expandedDirs: state.expandedDirs.includes(path)
|
||||||
if (newSet.has(path)) {
|
? state.expandedDirs.filter(p => p !== path)
|
||||||
newSet.delete(path)
|
: [...state.expandedDirs, path]
|
||||||
} else {
|
})),
|
||||||
newSet.add(path)
|
|
||||||
}
|
|
||||||
return { expandedDirs: newSet }
|
|
||||||
}),
|
|
||||||
expandDirs: (paths) =>
|
expandDirs: (paths) =>
|
||||||
set(state => {
|
set(state => {
|
||||||
const newSet = new Set(state.expandedDirs)
|
const newDirs = paths.filter(p => !state.expandedDirs.includes(p))
|
||||||
let changed = false
|
return newDirs.length > 0
|
||||||
for (const p of paths) {
|
? { expandedDirs: [...state.expandedDirs, ...newDirs] }
|
||||||
if (!newSet.has(p)) {
|
: state
|
||||||
newSet.add(p)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed ? { expandedDirs: newSet } : state
|
|
||||||
}),
|
}),
|
||||||
setSidebarWidth: (width) => {
|
setSidebarWidth: (width) => {
|
||||||
set({ sidebarWidth: width })
|
set({ sidebarWidth: width })
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export const useTabStore = create<TabState>((set, get) => ({
|
|||||||
} else {
|
} else {
|
||||||
set({ _loaded: true })
|
set({ _loaded: true })
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('Failed to load tabs from DB:', err)
|
||||||
set({ _loaded: true })
|
set({ _loaded: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user