feat: draggable section groups in sidebar
This commit is contained in:
1
design/draggable-section-groups.pen
Normal file
1
design/draggable-section-groups.pen
Normal file
File diff suppressed because one or more lines are too long
@@ -19,6 +19,9 @@
|
||||
"@blocknote/core": "^0.46.2",
|
||||
"@blocknote/mantine": "^0.46.2",
|
||||
"@blocknote/react": "^0.46.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mantine/core": "^8.3.14",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -20,6 +20,15 @@ importers:
|
||||
'@blocknote/react':
|
||||
specifier: ^0.46.2
|
||||
version: 0.46.2(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.2.4)
|
||||
'@mantine/core':
|
||||
specifier: ^8.3.14
|
||||
version: 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -322,6 +331,28 @@ packages:
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/core@6.3.1':
|
||||
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/sortable@10.0.0':
|
||||
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.3.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@emoji-mart/data@1.2.1':
|
||||
resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
|
||||
|
||||
@@ -3811,6 +3842,31 @@ snapshots:
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.1(react@19.2.4)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.2.4)
|
||||
react: 19.2.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emoji-mart/data@1.2.1': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
|
||||
@@ -42,6 +42,8 @@ pub struct VaultEntry {
|
||||
pub icon: Option<String>,
|
||||
/// Accent color key for Type entries: "red", "purple", "blue", "green", "yellow", "orange".
|
||||
pub color: Option<String>,
|
||||
/// Display order for Type entries in sidebar (lower = higher). None = use default order.
|
||||
pub order: Option<i64>,
|
||||
}
|
||||
|
||||
/// Intermediate struct to capture YAML frontmatter fields.
|
||||
@@ -75,6 +77,8 @@ struct Frontmatter {
|
||||
icon: Option<String>,
|
||||
#[serde(default)]
|
||||
color: Option<String>,
|
||||
#[serde(default)]
|
||||
order: Option<i64>,
|
||||
}
|
||||
|
||||
/// Handles YAML fields that can be either a single string or a list of strings.
|
||||
@@ -221,7 +225,7 @@ fn parse_frontmatter(data: &HashMap<String, serde_json::Value>) -> Frontmatter {
|
||||
/// Only skip keys that can never contain wikilinks.
|
||||
const SKIP_KEYS: &[&str] = &[
|
||||
"is a", "aliases", "status", "cadence", "archived", "trashed", "trashed at",
|
||||
"created at", "created time", "icon", "color",
|
||||
"created at", "created time", "icon", "color", "order",
|
||||
];
|
||||
|
||||
/// Check if a string contains a wikilink pattern `[[...]]`.
|
||||
@@ -376,6 +380,7 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
modified_at, created_at, file_size,
|
||||
icon: frontmatter.icon,
|
||||
color: frontmatter.color,
|
||||
order: frontmatter.order,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -127,6 +127,15 @@ function App() {
|
||||
setToastMessage('Note restored from trash')
|
||||
}, [notes, vault, setToastMessage])
|
||||
|
||||
const handleReorderSections = useCallback((orderedTypes: { typeName: string; order: number }[]) => {
|
||||
for (const { typeName, order } of orderedTypes) {
|
||||
const typeEntry = vault.entries.find((e) => e.isA === 'Type' && e.title === typeName)
|
||||
if (!typeEntry) continue
|
||||
notes.handleUpdateFrontmatter(typeEntry.path, 'order', order)
|
||||
vault.updateEntry(typeEntry.path, { order })
|
||||
}
|
||||
}, [vault, notes])
|
||||
|
||||
useAppKeyboard({
|
||||
onQuickOpen: () => setShowQuickOpen(true),
|
||||
onCreateNote: openCreateDialog,
|
||||
@@ -177,7 +186,7 @@ function App() {
|
||||
<div className="app-shell">
|
||||
<div className="app">
|
||||
<div className="app__sidebar" style={{ width: sidebarWidth }}>
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={openCreateDialog} onCreateNewType={openCreateTypeDialog} onCustomizeType={handleCustomizeType} modifiedCount={vault.modifiedFiles.length} onCommitPush={() => setShowCommitDialog(true)} />
|
||||
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={openCreateDialog} onCreateNewType={openCreateTypeDialog} onCustomizeType={handleCustomizeType} onReorderSections={handleReorderSections} modifiedCount={vault.modifiedFiles.length} onCommitPush={() => setShowCommitDialog(true)} />
|
||||
</div>
|
||||
<ResizeHandle onResize={handleSidebarResize} />
|
||||
<div className="app__note-list" style={{ width: noteListWidth }}>
|
||||
|
||||
@@ -62,6 +62,7 @@ const mockEntry: VaultEntry = {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const mockContent = `---
|
||||
|
||||
@@ -24,6 +24,7 @@ const mockEntry: VaultEntry = {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const mockContent = `---
|
||||
@@ -65,6 +66,7 @@ const referrerEntry: VaultEntry = {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const allContent: Record<string, string> = {
|
||||
|
||||
@@ -30,6 +30,7 @@ const mockEntries: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/facebook-ads-strategy.md',
|
||||
@@ -55,6 +56,7 @@ const mockEntries: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/person/matteo-cellini.md',
|
||||
@@ -77,6 +79,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/event/2026-02-14-kickoff.md',
|
||||
@@ -99,6 +102,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/software-development.md',
|
||||
@@ -121,6 +125,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -278,6 +283,7 @@ describe('getSortComparator', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -353,6 +359,7 @@ describe('NoteList sort controls', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/responsibility/grow-newsletter.md',
|
||||
@@ -59,6 +60,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/experiment/stock-screener.md',
|
||||
@@ -81,6 +83,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/procedure/weekly-essays.md',
|
||||
@@ -103,6 +106,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/topic/software-development.md',
|
||||
@@ -125,6 +129,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/topic/trading.md',
|
||||
@@ -147,6 +152,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/person/alice.md',
|
||||
@@ -169,6 +175,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/event/kickoff.md',
|
||||
@@ -191,6 +198,7 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -215,34 +223,45 @@ describe('Sidebar', () => {
|
||||
expect(screen.getByText('Types')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows entity names under their section groups', () => {
|
||||
it('shows entity names under their section groups after expanding', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
// Sections start collapsed by default — expand them first
|
||||
fireEvent.click(screen.getByLabelText('Expand Projects'))
|
||||
fireEvent.click(screen.getByLabelText('Expand Responsibilities'))
|
||||
fireEvent.click(screen.getByLabelText('Expand Experiments'))
|
||||
fireEvent.click(screen.getByLabelText('Expand Procedures'))
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
expect(screen.getByText('Grow Newsletter')).toBeInTheDocument()
|
||||
expect(screen.getByText('Stock Screener')).toBeInTheDocument()
|
||||
expect(screen.getByText('Write Weekly Essays')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows People and Events as section groups', () => {
|
||||
it('shows People and Events items after expanding', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
fireEvent.click(screen.getByLabelText('Expand People'))
|
||||
fireEvent.click(screen.getByLabelText('Expand Events'))
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument()
|
||||
expect(screen.getByText('Kickoff Meeting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses and expands sections', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Collapse Projects'))
|
||||
// Start collapsed — items hidden
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
|
||||
// Expand
|
||||
fireEvent.click(screen.getByLabelText('Expand Projects'))
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
|
||||
// Collapse
|
||||
fireEvent.click(screen.getByLabelText('Collapse Projects'))
|
||||
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onSelect when clicking an entity', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={onSelect} />)
|
||||
fireEvent.click(screen.getByLabelText('Expand Projects'))
|
||||
fireEvent.click(screen.getByText('Build Laputa App'))
|
||||
expect(onSelect).toHaveBeenCalledWith({
|
||||
kind: 'entity',
|
||||
@@ -270,9 +289,10 @@ describe('Sidebar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('renders Topics section with topic entries', () => {
|
||||
it('renders Topics section with topic entries after expanding', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
expect(screen.getByText('Topics')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByLabelText('Expand Topics'))
|
||||
expect(screen.getByText('Software Development')).toBeInTheDocument()
|
||||
expect(screen.getByText('Trading')).toBeInTheDocument()
|
||||
})
|
||||
@@ -280,6 +300,7 @@ describe('Sidebar', () => {
|
||||
it('calls onSelect with topic kind when clicking a topic', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={onSelect} />)
|
||||
fireEvent.click(screen.getByLabelText('Expand Topics'))
|
||||
fireEvent.click(screen.getByText('Software Development'))
|
||||
expect(onSelect).toHaveBeenCalledWith({
|
||||
kind: 'topic',
|
||||
@@ -340,6 +361,7 @@ describe('Sidebar', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/type/book.md',
|
||||
@@ -362,6 +384,7 @@ describe('Sidebar', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/recipe/pasta.md',
|
||||
@@ -384,6 +407,7 @@ describe('Sidebar', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -393,8 +417,9 @@ describe('Sidebar', () => {
|
||||
expect(screen.getByText('Recipes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows instances of custom types under their section', () => {
|
||||
it('shows instances of custom types under their section after expanding', () => {
|
||||
render(<Sidebar entries={entriesWithCustomTypes} selection={defaultSelection} onSelect={() => {}} onCreateType={() => {}} />)
|
||||
fireEvent.click(screen.getByLabelText('Expand Recipes'))
|
||||
expect(screen.getByText('Pasta Carbonara')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -434,6 +459,7 @@ describe('Sidebar', () => {
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
render(<Sidebar entries={[...mockEntries, projectTypeEntry]} selection={defaultSelection} onSelect={() => {}} />)
|
||||
// "Projects" should appear once (the built-in section), not twice
|
||||
@@ -463,15 +489,17 @@ describe('Sidebar', () => {
|
||||
|
||||
it('hides a section when its toggle is clicked off', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
// People section should be visible initially
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument()
|
||||
// People section header should be visible initially
|
||||
expect(screen.getByText('People')).toBeInTheDocument()
|
||||
|
||||
// Open customize popover and toggle People off
|
||||
fireEvent.click(screen.getByTitle('Customize sections'))
|
||||
fireEvent.click(screen.getByLabelText('Toggle People'))
|
||||
|
||||
// People section and its items should be gone
|
||||
expect(screen.queryByText('Alice')).not.toBeInTheDocument()
|
||||
// People section header should be gone (use getAllByText to handle popover)
|
||||
const peopleElements = screen.queryAllByText('People')
|
||||
// Only the toggle label in the popover should remain, not the section header
|
||||
expect(peopleElements.length).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('re-shows a section when its toggle is clicked on again', () => {
|
||||
@@ -480,11 +508,12 @@ describe('Sidebar', () => {
|
||||
// Hide People
|
||||
fireEvent.click(screen.getByTitle('Customize sections'))
|
||||
fireEvent.click(screen.getByLabelText('Toggle People'))
|
||||
expect(screen.queryByText('Alice')).not.toBeInTheDocument()
|
||||
|
||||
// Show People again
|
||||
fireEvent.click(screen.getByLabelText('Toggle People'))
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument()
|
||||
// People section should be visible again — popover toggle + section header = 2 "People" texts
|
||||
const peopleElements = screen.getAllByText('People')
|
||||
expect(peopleElements.length).toBe(2)
|
||||
})
|
||||
|
||||
it('persists hidden sections in localStorage', () => {
|
||||
@@ -502,13 +531,13 @@ describe('Sidebar', () => {
|
||||
localStorage.setItem('laputa-hidden-sections', JSON.stringify(['Person', 'Event']))
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
|
||||
// People and Events sections should be hidden
|
||||
expect(screen.queryByText('Alice')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Kickoff Meeting')).not.toBeInTheDocument()
|
||||
// People and Events section headers should be hidden
|
||||
expect(screen.queryByText('People')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Events')).not.toBeInTheDocument()
|
||||
|
||||
// Other sections should still be visible
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
expect(screen.getByText('Software Development')).toBeInTheDocument()
|
||||
// Other section headers should still be visible
|
||||
expect(screen.getByText('Projects')).toBeInTheDocument()
|
||||
expect(screen.getByText('Topics')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not affect All Notes or other sidebar filters when sections are hidden', () => {
|
||||
@@ -534,4 +563,52 @@ describe('Sidebar', () => {
|
||||
expect(screen.queryByText('Show in sidebar')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('section ordering by type order property', () => {
|
||||
const entriesWithOrder: VaultEntry[] = [
|
||||
...mockEntries,
|
||||
// Type entries with order values — reversed from default
|
||||
{
|
||||
path: '/vault/type/project.md', filename: 'project.md', title: 'Project', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
relationships: {}, icon: null, color: null, order: 5,
|
||||
},
|
||||
{
|
||||
path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
relationships: {}, icon: null, color: null, order: 0,
|
||||
},
|
||||
{
|
||||
path: '/vault/type/person.md', filename: 'person.md', title: 'Person', isA: 'Type',
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null, owner: null, cadence: null,
|
||||
archived: false, modifiedAt: 1700000000, createdAt: null, fileSize: 200, snippet: '',
|
||||
relationships: {}, icon: null, color: null, order: 1,
|
||||
},
|
||||
]
|
||||
|
||||
it('sorts sections by order from Type entries', () => {
|
||||
render(<Sidebar entries={entriesWithOrder} selection={defaultSelection} onSelect={() => {}} />)
|
||||
// Get all section header labels
|
||||
const headers = screen.getAllByText(/^(Topics|People|Projects|Experiments|Responsibilities|Procedures|Events|Types)$/)
|
||||
const labels = headers.map((el) => el.textContent)
|
||||
|
||||
// Topics (order: 0) and People (order: 1) should come before Projects (order: 5)
|
||||
const topicsIdx = labels.indexOf('Topics')
|
||||
const peopleIdx = labels.indexOf('People')
|
||||
const projectsIdx = labels.indexOf('Projects')
|
||||
|
||||
expect(topicsIdx).toBeLessThan(projectsIdx)
|
||||
expect(peopleIdx).toBeLessThan(projectsIdx)
|
||||
expect(topicsIdx).toBeLessThan(peopleIdx)
|
||||
})
|
||||
|
||||
it('renders drag handle on section headers', () => {
|
||||
render(<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} />)
|
||||
const dragHandles = screen.getAllByLabelText(/^Drag to reorder/)
|
||||
// Should have one drag handle per visible section group
|
||||
expect(dragHandles.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { useState, useMemo, useRef, useEffect, useCallback, memo, type ComponentType } from 'react'
|
||||
import type { VaultEntry, SidebarSelection } from '../types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronRight, ChevronDown, GitCommitHorizontal, Plus, SlidersHorizontal } from 'lucide-react'
|
||||
import { ChevronRight, ChevronDown, GitCommitHorizontal, Plus, SlidersHorizontal, GripVertical } from 'lucide-react'
|
||||
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
||||
import { resolveIcon, TypeCustomizePopover } from './TypeCustomizePopover'
|
||||
import { useSectionVisibility } from '../hooks/useSectionVisibility'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
FileText,
|
||||
Star,
|
||||
@@ -30,6 +46,7 @@ interface SidebarProps {
|
||||
onCreateType?: (type: string) => void
|
||||
onCreateNewType?: () => void
|
||||
onCustomizeType?: (typeName: string, icon: string, color: string) => void
|
||||
onReorderSections?: (orderedTypes: { typeName: string; order: number }[]) => void
|
||||
modifiedCount?: number
|
||||
onCommitPush?: () => void
|
||||
}
|
||||
@@ -59,7 +76,7 @@ const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [
|
||||
|
||||
const BUILT_IN_TYPES = new Set(BUILT_IN_SECTION_GROUPS.map((s) => s.type))
|
||||
|
||||
export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType, onCustomizeType, modifiedCount = 0, onCommitPush }: SidebarProps) {
|
||||
export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType, onCustomizeType, onReorderSections, modifiedCount = 0, onCommitPush }: 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)
|
||||
@@ -83,7 +100,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
}, [showCustomize])
|
||||
|
||||
const toggleSection = (type: string) => {
|
||||
setCollapsed((prev) => ({ ...prev, [type]: !prev[type] }))
|
||||
setCollapsed((prev) => ({ ...prev, [type]: !(prev[type] ?? true) }))
|
||||
}
|
||||
|
||||
// Build a map of type name → type entry for quick lookup of icon/color
|
||||
@@ -130,11 +147,47 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
})
|
||||
}, [typeEntryMap])
|
||||
|
||||
const allSectionGroups = useMemo(
|
||||
() => [...builtInWithOverrides, ...customSectionGroups],
|
||||
[builtInWithOverrides, customSectionGroups],
|
||||
// Merge built-in and custom sections, then sort by order from type entries
|
||||
const allSectionGroups = useMemo(() => {
|
||||
const merged = [...builtInWithOverrides, ...customSectionGroups]
|
||||
return merged.sort((a, b) => {
|
||||
const orderA = typeEntryMap[a.type]?.order ?? Infinity
|
||||
const orderB = typeEntryMap[b.type]?.order ?? Infinity
|
||||
if (orderA !== orderB) return orderA - orderB
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
}, [builtInWithOverrides, customSectionGroups, typeEntryMap])
|
||||
|
||||
// DnD sensors with activation distance to avoid accidental drags on click
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
)
|
||||
|
||||
const visibleSections = useMemo(
|
||||
() => allSectionGroups.filter((g) => isSectionVisible(g.type)),
|
||||
[allSectionGroups, isSectionVisible],
|
||||
)
|
||||
|
||||
const sectionIds = useMemo(() => visibleSections.map((g) => g.type), [visibleSections])
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
|
||||
const oldIndex = sectionIds.indexOf(active.id as string)
|
||||
const newIndex = sectionIds.indexOf(over.id as string)
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
|
||||
// Compute new order: assign sequential order values to the reordered list
|
||||
const reordered = [...sectionIds]
|
||||
reordered.splice(oldIndex, 1)
|
||||
reordered.splice(newIndex, 0, active.id as string)
|
||||
|
||||
const updates = reordered.map((typeName, i) => ({ typeName, order: i }))
|
||||
onReorderSections?.(updates)
|
||||
}, [sectionIds, onReorderSections])
|
||||
|
||||
const archivedCount = useMemo(() => entries.filter((e) => e.archived).length, [entries])
|
||||
const trashedCount = useMemo(() => entries.filter((e) => e.trashed).length, [entries])
|
||||
|
||||
@@ -192,7 +245,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
}
|
||||
}, [customizeTarget, typeEntryMap, onCustomizeType])
|
||||
|
||||
const renderSection = ({ label, type, Icon, customColor }: SectionGroup) => {
|
||||
const renderSectionContent = ({ label, type, Icon, customColor }: SectionGroup, dragHandleProps?: Record<string, unknown>) => {
|
||||
const items = entries.filter((e) => e.isA === type && !e.archived && !e.trashed)
|
||||
const isCollapsed = collapsed[type] ?? true
|
||||
const isTopic = type === 'Topic'
|
||||
@@ -210,7 +263,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={type} style={{ padding: '4px 6px' }}>
|
||||
<>
|
||||
{/* Section header row */}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -219,13 +272,22 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
? "bg-secondary"
|
||||
: "hover:bg-accent"
|
||||
)}
|
||||
style={{ padding: '6px 16px', borderRadius: 4, gap: 8 }}
|
||||
style={{ padding: '6px 8px 6px 6px', borderRadius: 4, gap: 4 }}
|
||||
onClick={() => onSelect({ kind: 'sectionGroup', type })}
|
||||
onContextMenu={(e) => handleContextMenu(e, type)}
|
||||
>
|
||||
<div className="flex items-center" style={{ gap: 8 }}>
|
||||
<div className="flex items-center" style={{ gap: 4 }}>
|
||||
{/* Drag handle */}
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-center text-muted-foreground opacity-0 group-hover/section:opacity-50 hover:!opacity-100 cursor-grab"
|
||||
style={{ width: 16, height: 16 }}
|
||||
{...dragHandleProps}
|
||||
aria-label={`Drag to reorder ${label}`}
|
||||
>
|
||||
<GripVertical size={12} />
|
||||
</div>
|
||||
<Icon size={16} style={{ color: sectionColor }} />
|
||||
<span className="text-[13px] font-medium text-foreground">{label}</span>
|
||||
<span className="text-[13px] font-medium text-foreground" style={{ marginLeft: 4 }}>{label}</span>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ gap: 2 }}>
|
||||
{(onCreateType || (isTypeSection && onCreateNewType)) && (
|
||||
@@ -281,6 +343,30 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SortableSection({ group }: { group: SectionGroup }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: group.type })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
padding: '4px 6px',
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes}>
|
||||
{renderSectionContent(group, listeners)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -443,8 +529,14 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section Groups (built-in + custom), filtered by visibility */}
|
||||
{allSectionGroups.filter((g) => isSectionVisible(g.type)).map(renderSection)}
|
||||
{/* Section Groups (built-in + custom), filtered by visibility, sortable by drag */}
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sectionIds} strategy={verticalListSortingStrategy}>
|
||||
{visibleSections.map((g) => (
|
||||
<SortableSection key={g.type} group={g} />
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</nav>
|
||||
|
||||
{/* Commit button — always visible */}
|
||||
|
||||
@@ -232,7 +232,7 @@ export function useNoteActions(
|
||||
status: noStatusTypes.has(type) ? null : 'Active',
|
||||
owner: null, cadence: null, archived: false, trashed: false, trashedAt: null,
|
||||
modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {}, icon: null, color: null,
|
||||
snippet: '', relationships: {}, icon: null, color: null, order: null,
|
||||
}
|
||||
|
||||
const frontmatter = [
|
||||
@@ -260,7 +260,7 @@ export function useNoteActions(
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null,
|
||||
modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {}, icon: null, color: null,
|
||||
snippet: '', relationships: {}, icon: null, color: null, order: null,
|
||||
}
|
||||
|
||||
const content = `---\nIs A: Type\n---\n\n# ${typeName}\n\n`
|
||||
|
||||
@@ -449,6 +449,7 @@ AI agents are autonomous systems that can plan, execute, and adapt to achieve go
|
||||
// --- Type documents ---
|
||||
'/Users/luca/Laputa/type/project.md': `---
|
||||
Is A: Type
|
||||
order: 0
|
||||
---
|
||||
|
||||
# Project
|
||||
@@ -462,6 +463,7 @@ A **time-bound initiative** that advances a [[type/responsibility|Responsibility
|
||||
`,
|
||||
'/Users/luca/Laputa/type/responsibility.md': `---
|
||||
Is A: Type
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Responsibility
|
||||
@@ -474,6 +476,7 @@ An **ongoing area of ownership** — something you're accountable for indefinite
|
||||
`,
|
||||
'/Users/luca/Laputa/type/procedure.md': `---
|
||||
Is A: Type
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Procedure
|
||||
@@ -488,6 +491,7 @@ A **recurring process** tied to a [[type/responsibility|Responsibility]]. Proced
|
||||
`,
|
||||
'/Users/luca/Laputa/type/experiment.md': `---
|
||||
Is A: Type
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Experiment
|
||||
@@ -500,6 +504,7 @@ A **hypothesis-driven investigation** with a clear test and measurable outcome.
|
||||
`,
|
||||
'/Users/luca/Laputa/type/person.md': `---
|
||||
Is A: Type
|
||||
order: 4
|
||||
---
|
||||
|
||||
# Person
|
||||
@@ -511,6 +516,7 @@ A **person** you interact with — team members, collaborators, contacts. People
|
||||
`,
|
||||
'/Users/luca/Laputa/type/event.md': `---
|
||||
Is A: Type
|
||||
order: 5
|
||||
---
|
||||
|
||||
# Event
|
||||
@@ -522,6 +528,7 @@ A **point-in-time occurrence** — meetings, launches, milestones. Events are li
|
||||
`,
|
||||
'/Users/luca/Laputa/type/topic.md': `---
|
||||
Is A: Type
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Topic
|
||||
@@ -533,6 +540,7 @@ A **subject area** for categorization. Topics group related notes, projects, and
|
||||
`,
|
||||
'/Users/luca/Laputa/type/essay.md': `---
|
||||
Is A: Type
|
||||
order: 7
|
||||
---
|
||||
|
||||
# Essay
|
||||
@@ -544,6 +552,7 @@ A **published piece of writing** — newsletter essays, blog posts, articles. Es
|
||||
`,
|
||||
'/Users/luca/Laputa/type/note.md': `---
|
||||
Is A: Type
|
||||
order: 8
|
||||
---
|
||||
|
||||
# Note
|
||||
@@ -724,6 +733,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/responsibility/grow-newsletter.md',
|
||||
@@ -755,6 +765,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/responsibility/manage-sponsorships.md',
|
||||
@@ -780,6 +791,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/procedure/write-weekly-essays.md',
|
||||
@@ -805,6 +817,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/procedure/run-sponsorships.md',
|
||||
@@ -830,6 +843,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/experiment/stock-screener.md',
|
||||
@@ -856,6 +870,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/facebook-ads-strategy.md',
|
||||
@@ -882,6 +897,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/budget-allocation.md',
|
||||
@@ -907,6 +923,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/person/matteo-cellini.md',
|
||||
@@ -931,6 +948,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/event/2026-02-14-laputa-app-kickoff.md',
|
||||
@@ -956,6 +974,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/software-development.md',
|
||||
@@ -981,6 +1000,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/trading.md',
|
||||
@@ -1006,6 +1026,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/on-writing-well.md',
|
||||
@@ -1031,6 +1052,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/engineering-leadership-101.md',
|
||||
@@ -1057,6 +1079,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/ai-agents-primer.md',
|
||||
@@ -1082,6 +1105,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
// --- Type documents ---
|
||||
{
|
||||
@@ -1105,6 +1129,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/responsibility.md',
|
||||
@@ -1127,6 +1152,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/procedure.md',
|
||||
@@ -1149,6 +1175,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/experiment.md',
|
||||
@@ -1171,6 +1198,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/person.md',
|
||||
@@ -1193,6 +1221,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/event.md',
|
||||
@@ -1215,6 +1244,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/topic.md',
|
||||
@@ -1237,6 +1267,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/essay.md',
|
||||
@@ -1259,6 +1290,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/note.md',
|
||||
@@ -1281,6 +1313,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: 8,
|
||||
},
|
||||
// --- Custom type documents (user-created types) ---
|
||||
{
|
||||
@@ -1304,6 +1337,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: 'cooking-pot',
|
||||
color: 'orange',
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/book.md',
|
||||
@@ -1326,6 +1360,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {},
|
||||
icon: 'book-open',
|
||||
color: 'green',
|
||||
order: 10,
|
||||
},
|
||||
// --- Instances of custom types ---
|
||||
{
|
||||
@@ -1351,6 +1386,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/book/designing-data-intensive-applications.md',
|
||||
@@ -1375,6 +1411,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
},
|
||||
// --- Trashed entries ---
|
||||
{
|
||||
@@ -1468,6 +1505,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
trashedAt: null,
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
modifiedAt: Date.now() / 1000 - 86400 * 120,
|
||||
createdAt: Date.now() / 1000 - 86400 * 200,
|
||||
fileSize: 680,
|
||||
@@ -1493,6 +1531,7 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
trashedAt: null,
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
modifiedAt: Date.now() / 1000 - 86400 * 90,
|
||||
createdAt: Date.now() / 1000 - 86400 * 150,
|
||||
fileSize: 520,
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface VaultEntry {
|
||||
icon: string | null
|
||||
/** Accent color key for Type entries: "red" | "purple" | "blue" | "green" | "yellow" | "orange" */
|
||||
color: string | null
|
||||
/** Display order for Type entries in sidebar (lower = higher). null = use default order. */
|
||||
order: number | null
|
||||
}
|
||||
|
||||
export interface GitCommit {
|
||||
|
||||
Reference in New Issue
Block a user