2026-02-22 10:37:33 +01:00
import { useState , useMemo , useRef , useEffect , useCallback , memo } from 'react'
2026-02-14 19:35:10 +01:00
import type { VaultEntry , SidebarSelection } from '../types'
2026-02-24 15:16:26 +01:00
import { resolveIcon } from '../utils/iconRegistry'
2026-02-26 04:05:22 +01:00
import { buildTypeEntryMap } from '../utils/typeColors'
2026-02-28 20:44:35 +01:00
import { pluralizeType } from '../hooks/useCommandRegistry'
2026-02-24 15:16:26 +01:00
import { TypeCustomizePopover } from './TypeCustomizePopover'
2026-02-21 17:29:42 +01:00
import {
2026-02-22 10:37:33 +01:00
DndContext , closestCenter , KeyboardSensor , PointerSensor ,
useSensor , useSensors , type DragEndEvent ,
2026-02-21 17:29:42 +01:00
} from '@dnd-kit/core'
import {
2026-02-22 10:37:33 +01:00
SortableContext , sortableKeyboardCoordinates , useSortable , verticalListSortingStrategy ,
2026-02-21 17:29:42 +01:00
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
2026-02-17 11:03:23 +01:00
import {
2026-03-06 23:16:23 +01:00
FileText , Wrench , Flask , Target , ArrowsClockwise ,
Users , CalendarBlank , Tag , Trash , StackSimple , Archive , CaretLeft , GitDiff , Pulse ,
2026-02-17 11:03:23 +01:00
} from '@phosphor-icons/react'
2026-02-22 10:37:33 +01:00
import { GitCommitHorizontal , SlidersHorizontal } from 'lucide-react'
import {
type SectionGroup , isSelectionActive ,
NavItem , SectionContent , type SectionContentProps , VisibilityPopover ,
} from './SidebarParts'
2026-02-25 21:43:16 +01:00
import { useDragRegion } from '../hooks/useDragRegion'
2026-02-14 18:22:42 +01:00
2026-02-14 19:35:10 +01:00
interface SidebarProps {
entries : VaultEntry [ ]
selection : SidebarSelection
onSelect : ( selection : SidebarSelection ) = > void
2026-02-15 10:00:29 +01:00
onSelectNote ? : ( entry : VaultEntry ) = > void
2026-02-20 23:49:30 +01:00
onCreateType ? : ( type : string ) = > void
2026-02-21 09:41:46 +01:00
onCreateNewType ? : ( ) = > void
2026-02-21 13:27:33 +01:00
onCustomizeType ? : ( typeName : string , icon : string , color : string ) = > void
2026-03-02 08:37:06 +01:00
onUpdateTypeTemplate ? : ( typeName : string , template : string ) = > void
2026-02-21 17:29:42 +01:00
onReorderSections ? : ( orderedTypes : { typeName : string ; order : number } [ ] ) = > void
2026-03-02 21:32:41 +01:00
onRenameSection ? : ( typeName : string , label : string ) = > void
2026-03-06 21:33:47 +01:00
onToggleTypeVisibility ? : ( typeName : string ) = > void
2026-02-15 12:56:28 +01:00
modifiedCount? : number
2026-02-15 13:00:10 +01:00
onCommitPush ? : ( ) = > void
2026-02-22 18:42:33 +01:00
onCollapse ? : ( ) = > void
2026-03-05 16:27:29 +01:00
isGitVault? : boolean
2026-02-14 19:35:10 +01:00
}
2026-02-21 09:41:46 +01:00
const BUILT_IN_SECTION_GROUPS : SectionGroup [ ] = [
2026-02-17 18:18:41 +01:00
{ label : 'Projects' , type : 'Project' , Icon : Wrench } ,
{ label : 'Experiments' , type : 'Experiment' , Icon : Flask } ,
{ label : 'Responsibilities' , type : 'Responsibility' , Icon : Target } ,
{ label : 'Procedures' , type : 'Procedure' , Icon : ArrowsClockwise } ,
{ label : 'People' , type : 'Person' , Icon : Users } ,
{ label : 'Events' , type : 'Event' , Icon : CalendarBlank } ,
{ label : 'Topics' , type : 'Topic' , Icon : Tag } ,
2026-02-20 21:45:54 +01:00
{ label : 'Types' , type : 'Type' , Icon : StackSimple } ,
2026-02-17 11:03:23 +01:00
]
2026-02-14 19:35:10 +01:00
2026-02-28 20:44:35 +01:00
/** Metadata lookup for well-known types (icon/label only — NOT used to determine which sections to show) */
const BUILT_IN_TYPE_MAP = new Map ( BUILT_IN_SECTION_GROUPS . map ( ( sg ) = > [ sg . type , sg ] ) )
2026-02-21 09:41:46 +01:00
2026-02-22 10:37:33 +01:00
// --- Hooks ---
2026-02-21 16:37:00 +01:00
2026-02-22 10:37:33 +01:00
function useOutsideClick ( ref : React.RefObject < HTMLElement | null > , isOpen : boolean , onClose : ( ) = > void ) {
2026-02-21 16:37:00 +01:00
useEffect ( ( ) = > {
2026-02-22 10:37:33 +01:00
if ( ! isOpen ) return
const handler = ( e : MouseEvent ) = > {
if ( ref . current && ! ref . current . contains ( e . target as Node ) ) onClose ( )
2026-02-21 16:37:00 +01:00
}
2026-02-22 10:37:33 +01:00
document . addEventListener ( 'mousedown' , handler )
return ( ) = > document . removeEventListener ( 'mousedown' , handler )
fix: resolve React hooks/compiler ESLint errors
- Wrap ref assignments in useEffect to fix refs-during-render in
useTabManagement, useKeyboardNavigation, and Editor
- Rewrite useAppKeyboard to define keyMap inside useEffect, eliminating
ref access during render
- Add eslint-disable for react-hooks/set-state-in-effect on legitimate
dialog reset patterns (CommitDialog, CreateNoteDialog, CreateTypeDialog,
QuickOpenPalette, AIChatPanel, useVaultLoader)
- Add eslint-disable for react-hooks/static-components on icon lookups
(NoteItem, NoteList PinnedCard) — stateless icon components from a
static map
- Add eslint-disable for react-hooks/purity on Date.now() in
NoteItem TrashDateLine — intentionally memoized on trashedAt
- Remove unused `model` param from buildSystemPrompt (ai-chat.ts) and
update callers
- Fix exhaustive-deps: suppress vault object dep in App.tsx git history
effect (vault object is unstable, loadGitHistory is the real dep)
- Remove unused `modifiedFiles` from useNoteListData params and caller
- Add missing `ref` to Sidebar useOutsideClick deps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:06:51 +01:00
} , [ ref , isOpen , onClose ] )
2026-02-22 10:37:33 +01:00
}
2026-02-21 13:27:33 +01:00
2026-02-28 20:44:35 +01:00
/** Collect unique isA values from active (non-trashed, non-archived) entries, excluding generic Note */
function collectActiveTypes ( entries : VaultEntry [ ] ) : Set < string > {
const types = new Set < string > ( )
for ( const e of entries ) {
if ( e . isA && e . isA !== 'Note' && ! e . trashed && ! e . archived ) types . add ( e . isA )
}
return types
}
/** Build a single SectionGroup for a type, using built-in metadata or Type entry for icon/label */
function buildSectionGroup ( type : string , typeEntryMap : Record < string , VaultEntry > ) : SectionGroup {
const builtIn = BUILT_IN_TYPE_MAP . get ( type )
const typeEntry = typeEntryMap [ type ]
const customColor = typeEntry ? . color ? ? null
2026-03-02 02:01:21 +01:00
const label = typeEntry ? . sidebarLabel || ( builtIn ? . label ? ? pluralizeType ( type ) )
2026-02-28 20:44:35 +01:00
if ( builtIn ) {
const Icon = typeEntry ? . icon ? resolveIcon ( typeEntry . icon ) : builtIn . Icon
2026-03-02 02:01:21 +01:00
return { . . . builtIn , label , Icon , customColor }
2026-02-28 20:44:35 +01:00
}
2026-03-02 02:01:21 +01:00
return { label , type , Icon : resolveIcon ( typeEntry ? . icon ? ? null ) , customColor }
2026-02-22 10:37:33 +01:00
}
2026-02-28 20:44:35 +01:00
/** Build sections dynamically from actual vault entries — only types with ≥1 active note appear */
function buildDynamicSections ( entries : VaultEntry [ ] , typeEntryMap : Record < string , VaultEntry > ) : SectionGroup [ ] {
const activeTypes = collectActiveTypes ( entries )
return Array . from ( activeTypes , ( type ) = > buildSectionGroup ( type , typeEntryMap ) )
2026-02-22 10:37:33 +01:00
}
function sortSections ( groups : SectionGroup [ ] , typeEntryMap : Record < string , VaultEntry > ) : SectionGroup [ ] {
return [ . . . groups ] . sort ( ( a , b ) = > {
const orderA = typeEntryMap [ a . type ] ? . order ? ? Infinity
const orderB = typeEntryMap [ b . type ] ? . order ? ? Infinity
return orderA !== orderB ? orderA - orderB : a.label.localeCompare ( b . label )
} )
}
2026-03-06 21:33:47 +01:00
function useSidebarSections ( entries : VaultEntry [ ] ) {
2026-02-22 10:37:33 +01:00
const typeEntryMap = useMemo ( ( ) = > buildTypeEntryMap ( entries ) , [ entries ] )
const allSectionGroups = useMemo ( ( ) = > {
2026-02-28 20:44:35 +01:00
const sections = buildDynamicSections ( entries , typeEntryMap )
return sortSections ( sections , typeEntryMap )
2026-02-22 10:37:33 +01:00
} , [ entries , typeEntryMap ] )
2026-03-06 21:33:47 +01:00
const visibleSections = useMemo ( ( ) = > allSectionGroups . filter ( ( g ) = > typeEntryMap [ g . type ] ? . visible !== false ) , [ allSectionGroups , typeEntryMap ] )
2026-02-22 10:37:33 +01:00
const sectionIds = useMemo ( ( ) = > visibleSections . map ( ( g ) = > g . type ) , [ visibleSections ] )
return { typeEntryMap , allSectionGroups , visibleSections , sectionIds }
}
2026-02-14 19:35:10 +01:00
2026-02-22 10:37:33 +01:00
function useEntryCounts ( entries : VaultEntry [ ] ) {
return useMemo ( ( ) = > {
let active = 0 , archived = 0 , trashed = 0
2026-02-21 13:27:33 +01:00
for ( const e of entries ) {
2026-02-22 10:37:33 +01:00
if ( e . trashed ) trashed ++
else if ( e . archived ) archived ++
else active ++
2026-02-21 13:27:33 +01:00
}
2026-02-22 10:37:33 +01:00
return { activeCount : active , archivedCount : archived , trashedCount : trashed }
2026-02-21 13:27:33 +01:00
} , [ entries ] )
2026-02-22 10:37:33 +01:00
}
2026-02-21 13:27:33 +01:00
2026-02-22 10:37:33 +01:00
function computeReorder ( sectionIds : string [ ] , activeId : string , overId : string ) : string [ ] | null {
const oldIndex = sectionIds . indexOf ( activeId )
const newIndex = sectionIds . indexOf ( overId )
if ( oldIndex === - 1 || newIndex === - 1 ) return null
const reordered = [ . . . sectionIds ]
reordered . splice ( oldIndex , 1 )
reordered . splice ( newIndex , 0 , activeId )
return reordered
}
2026-02-14 19:35:10 +01:00
2026-02-22 10:37:33 +01:00
function buildCustomizeArgs ( typeEntry : VaultEntry , prop : 'icon' | 'color' , value : string ) : [ string , string ] {
return [
prop === 'icon' ? value : ( typeEntry . icon ? ? 'file-text' ) ,
prop === 'color' ? value : ( typeEntry . color ? ? 'blue' ) ,
]
}
2026-02-21 09:41:46 +01:00
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
function applyCustomization (
target : string | null ,
typeEntryMap : Record < string , VaultEntry > ,
onCustomizeType : ( ( typeName : string , icon : string , color : string ) = > void ) | undefined ,
prop : 'icon' | 'color' ,
value : string ,
) : void {
if ( ! target || ! onCustomizeType ) return
const te = typeEntryMap [ target ]
2026-03-02 11:22:03 +01:00
const [ icon , color ] = te
? buildCustomizeArgs ( te , prop , value )
: [ prop === 'icon' ? value : 'file-text' , prop === 'color' ? value : 'blue' ]
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
onCustomizeType ( target , icon , color )
}
2026-02-22 10:37:33 +01:00
// --- Sub-components ---
2026-02-21 17:29:42 +01:00
2026-02-22 10:37:33 +01:00
function SortableSection ( { group , sectionProps } : {
group : SectionGroup
2026-03-02 23:20:49 +01:00
sectionProps : Omit < SectionContentProps , 'group' | 'items' | 'isCollapsed' | 'onToggle' | 'isRenaming' | 'renameInitialValue' >
2026-03-02 21:32:41 +01:00
& { entries : VaultEntry [ ] ; collapsed : Record < string , boolean > ; onToggle : ( type : string ) = > void ; renamingType : string | null ; renameInitialValue : string }
2026-02-22 10:37:33 +01:00
} ) {
2026-03-04 11:07:56 +01:00
const { attributes , listeners , setNodeRef , transform , transition , isDragging } = useSortable ( { id : group.type } )
2026-02-22 10:37:33 +01:00
const items = sectionProps . entries . filter ( ( e ) = > e . isA === group . type && ! e . archived && ! e . trashed )
const isCollapsed = sectionProps . collapsed [ group . type ] ? ? true
2026-03-02 21:32:41 +01:00
const isRenaming = sectionProps . renamingType === group . type
2026-02-22 10:37:33 +01:00
return (
< div ref = { setNodeRef } style = { { transform : CSS.Transform.toString ( transform ) , transition , opacity : isDragging ? 0.5 : 1 , padding : '4px 6px' } } { ...attributes } >
< SectionContent
group = { group } items = { items } isCollapsed = { isCollapsed }
selection = { sectionProps . selection } onSelect = { sectionProps . onSelect }
onSelectNote = { sectionProps . onSelectNote } onCreateType = { sectionProps . onCreateType }
onCreateNewType = { sectionProps . onCreateNewType } onContextMenu = { sectionProps . onContextMenu }
onToggle = { ( ) = > sectionProps . onToggle ( group . type ) }
2026-03-04 11:07:56 +01:00
dragHandleProps = { listeners }
2026-03-02 21:32:41 +01:00
isRenaming = { isRenaming }
renameInitialValue = { isRenaming ? sectionProps.renameInitialValue : undefined }
onRenameSubmit = { sectionProps . onRenameSubmit }
onRenameCancel = { sectionProps . onRenameCancel }
2026-02-22 10:37:33 +01:00
/ >
< / div >
2026-02-21 09:41:46 +01:00
)
2026-02-22 10:37:33 +01:00
}
2026-02-21 09:41:46 +01:00
2026-02-22 10:37:33 +01:00
function CommitButton ( { modifiedCount , onClick } : { modifiedCount : number ; onClick ? : ( ) = > void } ) {
if ( ! onClick ) return null
return (
< div className = "shrink-0 border-t border-border" style = { { padding : 12 } } >
< button className = "flex w-full items-center justify-center bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" style = { { borderRadius : 6 , gap : 6 , padding : '8px 16px' , border : 'none' , cursor : 'pointer' } } onClick = { onClick } >
< GitCommitHorizontal size = { 14 } / >
< span className = "text-[13px] font-medium" > Commit & Push < / span >
{ modifiedCount > 0 && (
< span className = "text-white font-semibold" style = { { background : '#ffffff40' , borderRadius : 9 , padding : '0 6px' , fontSize : 10 } } > { modifiedCount } < / span >
) }
< / button >
< / div >
)
}
2026-02-21 17:29:42 +01:00
2026-02-22 18:42:33 +01:00
function SidebarTitleBar ( { onCollapse } : { onCollapse ? : ( ) = > void } ) {
2026-02-25 21:43:16 +01:00
const { onMouseDown } = useDragRegion ( )
2026-02-22 18:42:33 +01:00
return (
2026-02-26 20:14:41 +01:00
< div className = "shrink-0 flex items-center justify-end border-b border-border" style = { { height : 52 , padding : '0 8px' , paddingLeft : 80 , cursor : 'default' } as React . CSSProperties } onMouseDown = { onMouseDown } >
2026-02-22 18:42:33 +01:00
{ onCollapse && (
< button
className = "flex shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
2026-02-25 21:43:16 +01:00
style = { { width : 24 , height : 24 } }
2026-02-22 18:42:33 +01:00
onClick = { onCollapse }
aria - label = "Collapse sidebar"
title = "Collapse sidebar"
>
< CaretLeft size = { 14 } weight = "bold" / >
< / button >
) }
< / div >
)
}
2026-03-02 21:32:41 +01:00
function ContextMenuOverlay ( { pos , type , innerRef , onOpenCustomize , onStartRename } : {
2026-02-22 10:37:33 +01:00
pos : { x : number ; y : number } | null ; type : string | null
innerRef : React.Ref < HTMLDivElement >
onOpenCustomize : ( type : string ) = > void
2026-03-02 21:32:41 +01:00
onStartRename : ( type : string ) = > void
2026-02-22 10:37:33 +01:00
} ) {
if ( ! pos || ! type ) return null
2026-03-02 21:32:41 +01:00
const btnClass = "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-default hover:bg-accent hover:text-accent-foreground transition-colors border-none bg-transparent text-left"
2026-02-22 10:37:33 +01:00
return (
< div ref = { innerRef } className = "fixed z-50 rounded-md border bg-popover p-1 shadow-md" style = { { left : pos.x , top : pos.y , minWidth : 180 } } >
2026-03-02 21:32:41 +01:00
< button className = { btnClass } onClick = { ( ) = > onStartRename ( type ) } >
Rename section …
< / button >
< button className = { btnClass } onClick = { ( ) = > onOpenCustomize ( type ) } >
Customize icon & amp ; color …
2026-02-22 10:37:33 +01:00
< / button >
< / div >
)
}
2026-02-21 17:29:42 +01:00
2026-03-02 21:54:32 +01:00
function CustomizeOverlay ( { target , typeEntryMap , innerRef , onCustomize , onChangeTemplate , onClose } : {
target : string | null ; typeEntryMap : Record < string , VaultEntry >
2026-02-22 10:37:33 +01:00
innerRef : React.Ref < HTMLDivElement >
onCustomize : ( prop : 'icon' | 'color' , value : string ) = > void
2026-03-02 08:37:06 +01:00
onChangeTemplate : ( template : string ) = > void
2026-02-22 10:37:33 +01:00
onClose : ( ) = > void
} ) {
if ( ! target ) return null
return (
2026-03-02 21:54:32 +01:00
< div ref = { innerRef } className = "fixed z-50" style = { { left : 20 , top : 100 } } >
2026-02-22 10:37:33 +01:00
< TypeCustomizePopover
currentIcon = { typeEntryMap [ target ] ? . icon ? ? null }
currentColor = { typeEntryMap [ target ] ? . color ? ? null }
2026-03-02 08:37:06 +01:00
currentTemplate = { typeEntryMap [ target ] ? . template ? ? null }
2026-02-22 10:37:33 +01:00
onChangeIcon = { ( icon ) = > onCustomize ( 'icon' , icon ) }
onChangeColor = { ( color ) = > onCustomize ( 'color' , color ) }
2026-03-02 08:37:06 +01:00
onChangeTemplate = { onChangeTemplate }
2026-02-22 10:37:33 +01:00
onClose = { onClose }
/ >
< / div >
)
}
2026-02-21 17:29:42 +01:00
2026-02-22 10:37:33 +01:00
// --- Main Sidebar ---
2026-02-21 17:29:42 +01:00
2026-02-22 10:37:33 +01:00
export const Sidebar = memo ( function Sidebar ( {
entries , selection , onSelect , onSelectNote , onCreateType , onCreateNewType ,
2026-03-02 21:32:41 +01:00
onCustomizeType , onUpdateTypeTemplate , onReorderSections , onRenameSection ,
2026-03-06 21:33:47 +01:00
onToggleTypeVisibility ,
2026-03-05 16:27:29 +01:00
modifiedCount = 0 , onCommitPush , onCollapse , isGitVault = false ,
2026-02-22 10:37:33 +01:00
} : SidebarProps ) {
const [ collapsed , setCollapsed ] = useState < Record < string , boolean > > ( { } )
const [ customizeTarget , setCustomizeTarget ] = useState < string | null > ( null )
const [ contextMenuPos , setContextMenuPos ] = useState < { x : number ; y : number } | null > ( null )
2026-03-02 21:32:41 +01:00
const [ renamingType , setRenamingType ] = useState < string | null > ( null )
const [ renameInitialValue , setRenameInitialValue ] = useState ( '' )
2026-03-02 23:20:49 +01:00
const [ contextMenuType , setContextMenuType ] = useState < string | null > ( null )
const [ showCustomize , setShowCustomize ] = useState ( false )
2026-02-21 17:29:42 +01:00
2026-02-22 10:37:33 +01:00
const contextMenuRef = useRef < HTMLDivElement > ( null )
const popoverRef = useRef < HTMLDivElement > ( null )
const customizeRef = useRef < HTMLDivElement > ( null )
2026-02-21 16:36:14 +01:00
2026-03-06 21:33:47 +01:00
const { typeEntryMap , allSectionGroups , visibleSections , sectionIds } = useSidebarSections ( entries )
const isSectionVisible = useCallback ( ( type : string ) = > typeEntryMap [ type ] ? . visible !== false , [ typeEntryMap ] )
const toggleVisibility = useCallback ( ( type : string ) = > onToggleTypeVisibility ? . ( type ) , [ onToggleTypeVisibility ] )
2026-02-22 10:37:33 +01:00
const { activeCount , archivedCount , trashedCount } = useEntryCounts ( entries )
2026-02-21 13:27:33 +01:00
2026-02-22 10:37:33 +01:00
const closeContextMenu = useCallback ( ( ) = > { setContextMenuPos ( null ) ; setContextMenuType ( null ) } , [ ] )
const closeCustomize = useCallback ( ( ) = > setShowCustomize ( false ) , [ ] )
const closeCustomizeTarget = useCallback ( ( ) = > setCustomizeTarget ( null ) , [ ] )
2026-02-21 13:27:33 +01:00
2026-02-22 10:37:33 +01:00
useOutsideClick ( customizeRef , showCustomize , closeCustomize )
useOutsideClick ( contextMenuRef , ! ! contextMenuPos , closeContextMenu )
useOutsideClick ( popoverRef , ! ! customizeTarget , closeCustomizeTarget )
2026-02-21 13:27:33 +01:00
2026-02-22 10:37:33 +01:00
const toggleSection = useCallback ( ( type : string ) = > {
setCollapsed ( ( prev ) = > ( { . . . prev , [ type ] : ! ( prev [ type ] ? ? true ) } ) )
2026-02-21 13:27:33 +01:00
} , [ ] )
2026-02-22 10:37:33 +01:00
const sensors = useSensors (
useSensor ( PointerSensor , { activationConstraint : { distance : 5 } } ) ,
useSensor ( KeyboardSensor , { coordinateGetter : sortableKeyboardCoordinates } ) ,
)
2026-02-21 09:41:46 +01:00
2026-02-22 10:37:33 +01:00
const handleDragEnd = useCallback ( ( event : DragEndEvent ) = > {
const { active , over } = event
if ( ! over || active . id === over . id ) return
const reordered = computeReorder ( sectionIds , active . id as string , over . id as string )
if ( reordered ) onReorderSections ? . ( reordered . map ( ( typeName , i ) = > ( { typeName , order : i } ) ) )
} , [ sectionIds , onReorderSections ] )
2026-02-21 09:41:46 +01:00
2026-02-22 10:37:33 +01:00
const handleContextMenu = useCallback ( ( e : React.MouseEvent , type : string ) = > {
e . preventDefault ( ) ; e . stopPropagation ( )
2026-03-02 21:54:32 +01:00
setContextMenuPos ( { x : e.clientX , y : e.clientY } ) ; setContextMenuType ( type )
2026-02-22 10:37:33 +01:00
} , [ ] )
2026-02-21 17:29:42 +01:00
2026-03-02 23:20:49 +01:00
const cancelRename = useCallback ( ( ) = > setRenamingType ( null ) , [ ] )
2026-03-02 21:32:41 +01:00
const handleStartRename = useCallback ( ( type : string ) = > {
closeContextMenu ( )
const group = allSectionGroups . find ( ( g ) = > g . type === type )
setRenameInitialValue ( group ? . label ? ? type )
setRenamingType ( type )
} , [ closeContextMenu , allSectionGroups ] )
const handleRenameSubmit = useCallback ( ( value : string ) = > {
if ( renamingType ) onRenameSection ? . ( renamingType , value )
setRenamingType ( null )
} , [ renamingType , onRenameSection ] )
2026-02-22 10:37:33 +01:00
const handleCustomize = useCallback ( ( prop : 'icon' | 'color' , value : string ) = > {
refactor: decompose Editor.tsx, NoteList.tsx, Sidebar.tsx — CodeScene hotspots (#126)
* refactor: decompose Editor.tsx into focused subcomponents and hooks
Extract from the monolithic Editor component (cc=35, 213 LoC, score 7.82):
- useDiffMode hook: diff state + toggle/commit-diff handlers
- useEditorFocus hook: new-note focus event listener
- editorSchema: WikiLink spec + BlockNote schema (module-level)
- SingleEditorView: BlockNote editor + suggestion menus
- EditorContent: breadcrumb bar + diff/editor/loading views
- EditorRightPanel: AI chat / Inspector panel switching
- suggestionEnrichment util: shared enrichment logic (eliminates duplication)
Editor.tsx is now 10.0 code health (was 7.82). All 25 existing tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract standalone functions from NoteList and Sidebar to reduce cc
NoteList: extract createNoteStatusResolver and toggleSetMember from
NoteListInner (cc 13→8, score 9.04→9.38).
Sidebar: extract applyCustomization from Sidebar (cc 10→7, score 9.09→10.0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add tests for useDiffMode, useEditorFocus, and suggestionEnrichment
Cover extracted hook/utility logic: diff toggle/reset, editor focus event
listener with adaptive timing, and suggestion item enrichment pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update architecture for editor decomposition and write .claude-done
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 15:27:25 +01:00
applyCustomization ( customizeTarget , typeEntryMap , onCustomizeType , prop , value )
2026-02-22 10:37:33 +01:00
} , [ customizeTarget , typeEntryMap , onCustomizeType ] )
2026-02-21 17:29:42 +01:00
2026-03-02 08:37:06 +01:00
const handleChangeTemplate = useCallback ( ( template : string ) = > {
if ( customizeTarget ) onUpdateTypeTemplate ? . ( customizeTarget , template )
} , [ customizeTarget , onUpdateTypeTemplate ] )
2026-02-22 10:37:33 +01:00
const sectionProps = {
entries , collapsed , selection , onSelect , onSelectNote , onCreateType , onCreateNewType ,
onContextMenu : handleContextMenu , onToggle : toggleSection ,
2026-03-02 21:32:41 +01:00
renamingType , renameInitialValue , onRenameSubmit : handleRenameSubmit , onRenameCancel : cancelRename ,
2026-02-21 09:41:46 +01:00
}
2026-02-14 18:22:42 +01:00
return (
2026-02-22 12:11:39 +01:00
< aside className = "flex h-full flex-col overflow-hidden border-r border-[var(--sidebar-border)] bg-sidebar text-sidebar-foreground" >
2026-02-22 18:42:33 +01:00
< SidebarTitleBar onCollapse = { onCollapse } / >
2026-02-17 11:03:23 +01:00
< nav className = "flex-1 overflow-y-auto" >
2026-02-22 10:37:33 +01:00
{ /* Top nav */ }
2026-02-17 11:03:23 +01:00
< div className = "border-b border-border" style = { { padding : '4px 6px' } } >
2026-02-22 10:37:33 +01:00
< NavItem icon = { FileText } label = "All Notes" count = { activeCount } isActive = { isSelectionActive ( selection , { kind : 'filter' , filter : 'all' } ) } badgeClassName = "bg-primary text-primary-foreground" onClick = { ( ) = > onSelect ( { kind : 'filter' , filter : 'all' } ) } / >
< NavItem icon = { Archive } label = "Archive" count = { archivedCount } isActive = { isSelectionActive ( selection , { kind : 'filter' , filter : 'archived' } ) } badgeClassName = "text-muted-foreground" badgeStyle = { { background : 'var(--muted)' } } onClick = { ( ) = > onSelect ( { kind : 'filter' , filter : 'archived' } ) } / >
< NavItem icon = { Trash } label = "Trash" count = { trashedCount } isActive = { isSelectionActive ( selection , { kind : 'filter' , filter : 'trash' } ) } activeClassName = "bg-destructive/10 text-destructive" badgeClassName = "text-muted-foreground" badgeStyle = { { background : 'var(--muted)' } } onClick = { ( ) = > onSelect ( { kind : 'filter' , filter : 'trash' } ) } / >
2026-02-24 14:42:06 +01:00
{ modifiedCount > 0 && (
< NavItem icon = { GitDiff } label = "Changes" count = { modifiedCount } isActive = { isSelectionActive ( selection , { kind : 'filter' , filter : 'changes' } ) } activeClassName = "bg-[color:var(--accent-orange)]/10 text-[var(--accent-orange)]" badgeClassName = "text-white" badgeStyle = { { background : 'var(--accent-orange)' } } onClick = { ( ) = > onSelect ( { kind : 'filter' , filter : 'changes' } ) } / >
) }
2026-03-05 16:27:29 +01:00
< NavItem icon = { Pulse } label = "Pulse" isActive = { isSelectionActive ( selection , { kind : 'filter' , filter : 'pulse' } ) } disabled = { ! isGitVault } disabledTooltip = "Pulse is only available for git-enabled vaults" onClick = { isGitVault ? ( ) = > onSelect ( { kind : 'filter' , filter : 'pulse' } ) : undefined } / >
2026-02-14 19:38:38 +01:00
< / div >
2026-02-22 10:37:33 +01:00
{ /* Sections header + visibility popover */ }
2026-02-21 16:37:00 +01:00
< div ref = { customizeRef } style = { { position : 'relative' , padding : '4px 6px 0' } } >
2026-02-22 10:37:33 +01:00
< div className = "flex w-full select-none items-center justify-between" style = { { padding : '4px 16px' } } >
2026-02-21 18:40:06 +01:00
< span className = "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground" > Sections < / span >
2026-02-22 10:37:33 +01:00
< button className = "flex shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-foreground" style = { { width : 20 , height : 20 } } onClick = { ( ) = > setShowCustomize ( ( v ) = > ! v ) } aria-label = "Customize sections" title = "Customize sections" >
2026-02-21 18:40:06 +01:00
< SlidersHorizontal size = { 14 } / >
< / button >
< / div >
2026-02-22 10:37:33 +01:00
{ showCustomize && < VisibilityPopover sections = { allSectionGroups } isSectionVisible = { isSectionVisible } onToggle = { toggleVisibility } / > }
2026-02-21 16:37:00 +01:00
< / div >
2026-02-22 10:37:33 +01:00
{ /* Sortable section groups */ }
2026-02-21 17:29:42 +01:00
< DndContext sensors = { sensors } collisionDetection = { closestCenter } onDragEnd = { handleDragEnd } >
< SortableContext items = { sectionIds } strategy = { verticalListSortingStrategy } >
{ visibleSections . map ( ( g ) = > (
2026-02-22 10:37:33 +01:00
< SortableSection key = { g . type } group = { g } sectionProps = { sectionProps } / >
2026-02-21 17:29:42 +01:00
) ) }
< / SortableContext >
< / DndContext >
2026-02-14 18:22:42 +01:00
< / nav >
2026-02-16 16:56:44 +01:00
2026-02-22 10:37:33 +01:00
< CommitButton modifiedCount = { modifiedCount } onClick = { onCommitPush } / >
2026-03-02 21:32:41 +01:00
< ContextMenuOverlay pos = { contextMenuPos } type = { contextMenuType } innerRef = { contextMenuRef } onOpenCustomize = { ( type ) = > { closeContextMenu ( ) ; setCustomizeTarget ( type ) } } onStartRename = { handleStartRename } / >
2026-03-02 21:54:32 +01:00
< CustomizeOverlay target = { customizeTarget } typeEntryMap = { typeEntryMap } innerRef = { popoverRef } onCustomize = { handleCustomize } onChangeTemplate = { handleChangeTemplate } onClose = { closeCustomizeTarget } / >
2026-02-14 18:22:42 +01:00
< / aside >
)
2026-02-17 17:14:50 +01:00
} )