feat: add icon and color fields to VaultEntry
- Add icon (string|null) and color (string|null) to VaultEntry interface - Update Rust VaultEntry struct and frontmatter parsing to read icon/color - Add icon/color to SKIP_KEYS so they don't appear as relationships - Set mock data: Recipe type → cooking-pot/orange, Book type → book-open/green - Update all test files with the new required fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,10 @@ pub struct VaultEntry {
|
||||
/// Generic relationship fields: any frontmatter key whose value contains wikilinks.
|
||||
/// Key is the original frontmatter field name (e.g. "Has", "Topics", "Events").
|
||||
pub relationships: HashMap<String, Vec<String>>,
|
||||
/// Phosphor icon name (kebab-case) for Type entries, e.g. "cooking-pot".
|
||||
pub icon: Option<String>,
|
||||
/// Accent color key for Type entries: "red", "purple", "blue", "green", "yellow", "orange".
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// Intermediate struct to capture YAML frontmatter fields.
|
||||
@@ -57,6 +61,10 @@ struct Frontmatter {
|
||||
created_at: Option<String>,
|
||||
#[serde(rename = "Created time")]
|
||||
created_time: Option<String>,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
#[serde(default)]
|
||||
color: Option<String>,
|
||||
}
|
||||
|
||||
/// Handles YAML fields that can be either a single string or a list of strings.
|
||||
@@ -203,6 +211,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", "created at", "created time",
|
||||
"icon", "color",
|
||||
];
|
||||
|
||||
/// Check if a string contains a wikilink pattern `[[...]]`.
|
||||
@@ -352,6 +361,8 @@ pub fn parse_md_file(path: &Path) -> Result<VaultEntry, String> {
|
||||
owner: frontmatter.owner,
|
||||
cadence: frontmatter.cadence,
|
||||
modified_at, created_at, file_size,
|
||||
icon: frontmatter.icon,
|
||||
color: frontmatter.color,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ const mockEntry: VaultEntry = {
|
||||
fileSize: 1024,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
}
|
||||
|
||||
const mockContent = `---
|
||||
|
||||
@@ -19,6 +19,8 @@ const mockEntry: VaultEntry = {
|
||||
fileSize: 1024,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
}
|
||||
|
||||
const mockContent = `---
|
||||
@@ -54,6 +56,8 @@ const referrerEntry: VaultEntry = {
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
}
|
||||
|
||||
const allContent: Record<string, string> = {
|
||||
|
||||
@@ -25,6 +25,8 @@ const mockEntries: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Related to': ['[[topic/software-development]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/facebook-ads-strategy.md',
|
||||
@@ -45,6 +47,8 @@ const mockEntries: VaultEntry[] = [
|
||||
'Belongs to': ['[[project/26q1-laputa-app]]'],
|
||||
'Related to': ['[[topic/growth]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/person/matteo-cellini.md',
|
||||
@@ -62,6 +66,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 320,
|
||||
snippet: 'Sponsorship manager.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/event/2026-02-14-kickoff.md',
|
||||
@@ -79,6 +85,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 512,
|
||||
snippet: 'Project kickoff meeting notes.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/software-development.md',
|
||||
@@ -96,6 +104,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 256,
|
||||
snippet: 'Frontend, backend, and systems programming.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 1024,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/responsibility/grow-newsletter.md',
|
||||
@@ -37,6 +39,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 512,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/experiment/stock-screener.md',
|
||||
@@ -54,6 +58,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 256,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/procedure/weekly-essays.md',
|
||||
@@ -71,6 +77,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 128,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/topic/software-development.md',
|
||||
@@ -88,6 +96,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 256,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/topic/trading.md',
|
||||
@@ -105,6 +115,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 180,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/person/alice.md',
|
||||
@@ -122,6 +134,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 100,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/event/kickoff.md',
|
||||
@@ -139,6 +153,8 @@ const mockEntries: VaultEntry[] = [
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -283,6 +299,8 @@ describe('Sidebar', () => {
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/type/book.md',
|
||||
@@ -300,6 +318,8 @@ describe('Sidebar', () => {
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/vault/recipe/pasta.md',
|
||||
@@ -317,6 +337,8 @@ describe('Sidebar', () => {
|
||||
fileSize: 300,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -362,6 +384,8 @@ describe('Sidebar', () => {
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
}
|
||||
render(<Sidebar entries={[...mockEntries, projectTypeEntry]} selection={defaultSelection} onSelect={() => {}} />)
|
||||
// "Projects" should appear once (the built-in section), not twice
|
||||
|
||||
@@ -214,7 +214,7 @@ export function useNoteActions(
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: noStatusTypes.has(type) ? null : 'Active',
|
||||
owner: null, cadence: null, modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {},
|
||||
snippet: '', relationships: {}, icon: null, color: null,
|
||||
}
|
||||
|
||||
const frontmatter = [
|
||||
@@ -242,7 +242,7 @@ export function useNoteActions(
|
||||
aliases: [], belongsTo: [], relatedTo: [],
|
||||
status: null, owner: null, cadence: null,
|
||||
modifiedAt: now, createdAt: now, fileSize: 0,
|
||||
snippet: '', relationships: {},
|
||||
snippet: '', relationships: {}, icon: null, color: null,
|
||||
}
|
||||
|
||||
const content = `---\nIs A: Type\n---\n\n# ${typeName}\n\n`
|
||||
|
||||
@@ -552,6 +552,8 @@ A **general-purpose document** — research notes, meeting notes, strategy docs.
|
||||
// --- Custom type documents (user-created types) ---
|
||||
'/Users/luca/Laputa/type/recipe.md': `---
|
||||
Is A: Type
|
||||
icon: cooking-pot
|
||||
color: orange
|
||||
---
|
||||
|
||||
# Recipe
|
||||
@@ -565,6 +567,8 @@ A **recipe** for cooking or baking. Recipes have ingredients, steps, and serving
|
||||
`,
|
||||
'/Users/luca/Laputa/type/book.md': `---
|
||||
Is A: Type
|
||||
icon: book-open
|
||||
color: green
|
||||
---
|
||||
|
||||
# Book
|
||||
@@ -631,6 +635,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Related to': ['[[topic/software-development]]'],
|
||||
'Type': ['[[type/project]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/responsibility/grow-newsletter.md',
|
||||
@@ -657,6 +663,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Related to': ['[[topic/growth]]'],
|
||||
'Type': ['[[type/responsibility]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/responsibility/manage-sponsorships.md',
|
||||
@@ -677,6 +685,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Owner': ['[[person/matteo-cellini|Matteo Cellini]]'],
|
||||
'Type': ['[[type/responsibility]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/procedure/write-weekly-essays.md',
|
||||
@@ -697,6 +707,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Belongs to': ['[[responsibility/grow-newsletter]]'],
|
||||
'Type': ['[[type/procedure]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/procedure/run-sponsorships.md',
|
||||
@@ -717,6 +729,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Belongs to': ['[[responsibility/manage-sponsorships]]'],
|
||||
'Type': ['[[type/procedure]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/experiment/stock-screener.md',
|
||||
@@ -738,6 +752,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Has Data': ['[[data/ema200-backtest-results]]'],
|
||||
'Type': ['[[type/experiment]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/facebook-ads-strategy.md',
|
||||
@@ -759,6 +775,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Related to': ['[[topic/growth]]', '[[topic/ads]]'],
|
||||
'Type': ['[[type/note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/note/budget-allocation.md',
|
||||
@@ -779,6 +797,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Belongs to': ['[[project/26q1-laputa-app]]'],
|
||||
'Type': ['[[type/note]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/person/matteo-cellini.md',
|
||||
@@ -798,6 +818,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Type': ['[[type/person]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/event/2026-02-14-laputa-app-kickoff.md',
|
||||
@@ -818,6 +840,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Related to': ['[[project/26q1-laputa-app]]', '[[person/matteo-cellini]]'],
|
||||
'Type': ['[[type/event]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/software-development.md',
|
||||
@@ -838,6 +862,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Notes': ['[[note/facebook-ads-strategy]]', '[[note/budget-allocation]]'],
|
||||
'Type': ['[[type/topic]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/topic/trading.md',
|
||||
@@ -858,6 +884,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Notes': ['[[experiment/stock-screener]]'],
|
||||
'Type': ['[[type/topic]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/on-writing-well.md',
|
||||
@@ -878,6 +906,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Belongs to': ['[[responsibility/grow-newsletter]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/engineering-leadership-101.md',
|
||||
@@ -899,6 +929,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Related to': ['[[topic/software-development]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/essay/ai-agents-primer.md',
|
||||
@@ -919,6 +951,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
'Belongs to': ['[[responsibility/grow-newsletter]]'],
|
||||
'Type': ['[[type/essay]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
// --- Type documents ---
|
||||
{
|
||||
@@ -937,6 +971,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 320,
|
||||
snippet: 'A time-bound initiative that advances a Responsibility. Projects have a clear start, end, and deliverables.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/responsibility.md',
|
||||
@@ -954,6 +990,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 280,
|
||||
snippet: 'An ongoing area of ownership — something you\'re accountable for indefinitely.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/procedure.md',
|
||||
@@ -971,6 +1009,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 310,
|
||||
snippet: 'A recurring process tied to a Responsibility. Procedures have a cadence and describe how to do something.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/experiment.md',
|
||||
@@ -988,6 +1028,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 290,
|
||||
snippet: 'A hypothesis-driven investigation with a clear test and measurable outcome.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/person.md',
|
||||
@@ -1005,6 +1047,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 200,
|
||||
snippet: 'A person you interact with — team members, collaborators, contacts.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/event.md',
|
||||
@@ -1022,6 +1066,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 180,
|
||||
snippet: 'A point-in-time occurrence — meetings, launches, milestones.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/topic.md',
|
||||
@@ -1039,6 +1085,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 170,
|
||||
snippet: 'A subject area for categorization. Topics group related notes, projects, and resources by theme.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/essay.md',
|
||||
@@ -1056,6 +1104,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 200,
|
||||
snippet: 'A published piece of writing — newsletter essays, blog posts, articles.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/note.md',
|
||||
@@ -1073,6 +1123,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 190,
|
||||
snippet: 'A general-purpose document — research notes, meeting notes, strategy docs.',
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
// --- Custom type documents (user-created types) ---
|
||||
{
|
||||
@@ -1091,6 +1143,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 250,
|
||||
snippet: 'A recipe for cooking or baking. Recipes have ingredients, steps, and serving info.',
|
||||
relationships: {},
|
||||
icon: 'cooking-pot',
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/type/book.md',
|
||||
@@ -1108,6 +1162,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
fileSize: 220,
|
||||
snippet: 'A book you\'re reading or have read. Track reading progress, notes, and key takeaways.',
|
||||
relationships: {},
|
||||
icon: 'book-open',
|
||||
color: 'green',
|
||||
},
|
||||
// --- Instances of custom types ---
|
||||
{
|
||||
@@ -1128,6 +1184,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Type': ['[[type/recipe]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
{
|
||||
path: '/Users/luca/Laputa/book/designing-data-intensive-applications.md',
|
||||
@@ -1147,6 +1205,8 @@ const MOCK_ENTRIES: VaultEntry[] = [
|
||||
relationships: {
|
||||
'Type': ['[[type/book]]'],
|
||||
},
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ export interface VaultEntry {
|
||||
snippet: string
|
||||
/** Generic relationship fields: any frontmatter key whose value contains wikilinks. */
|
||||
relationships: Record<string, string[]>
|
||||
/** Phosphor icon name (kebab-case) for Type entries, e.g. "cooking-pot" */
|
||||
icon: string | null
|
||||
/** Accent color key for Type entries: "red" | "purple" | "blue" | "green" | "yellow" | "orange" */
|
||||
color: string | null
|
||||
}
|
||||
|
||||
export interface GitCommit {
|
||||
|
||||
Reference in New Issue
Block a user