Merge pull request #25 from refactoringhq/task/relazioni-bidirezionali
feat: bidirectional relationships (Referenced By panel)
This commit is contained in:
775
design/relazioni-bidirezionali.pen
Normal file
775
design/relazioni-bidirezionali.pen
Normal file
@@ -0,0 +1,775 @@
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"name": "Inspector — Referenced By (Multiple)",
|
||||
"theme": { "Mode": "Light" },
|
||||
"clip": true,
|
||||
"width": 320,
|
||||
"height": 700,
|
||||
"fill": "$--background",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"padding": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01h",
|
||||
"name": "Inspector Header",
|
||||
"width": "fill_container",
|
||||
"height": 36,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01hL",
|
||||
"name": "leftGroup",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01hI",
|
||||
"name": "propIcon",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "sliders-horizontal",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01hT",
|
||||
"name": "inspTitle",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "Properties",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01hX",
|
||||
"name": "closeIcon",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "x",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01rel",
|
||||
"name": "Relationships Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01relT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "BELONGS TO",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01relB",
|
||||
"name": "relBtn",
|
||||
"width": "fill_container",
|
||||
"fill": "$--accent-blue-light",
|
||||
"cornerRadius": 6,
|
||||
"padding": [6, 10],
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01relBT",
|
||||
"fill": "$--accent-blue",
|
||||
"content": "Grow Newsletter",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01relBI",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "target",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-blue"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01ref",
|
||||
"name": "Referenced By Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refH",
|
||||
"name": "refByHeader",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refHT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "REFERENCED BY",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refHC",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "5",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10,
|
||||
"fontWeight": "normal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG1",
|
||||
"name": "refByGroup — via Belongs to",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG1T",
|
||||
"fill": "$--muted-foreground",
|
||||
"opacity": 0.7,
|
||||
"content": "VIA BELONGS TO",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG1B1",
|
||||
"name": "refItem1",
|
||||
"width": "fill_container",
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"padding": [4, 0],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG1B1T",
|
||||
"fill": "$--accent-orange",
|
||||
"content": "Write Weekly Essays",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01refG1B1I",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "arrows-clockwise",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-orange"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG1B2",
|
||||
"name": "refItem2",
|
||||
"width": "fill_container",
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"padding": [4, 0],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG1B2T",
|
||||
"fill": "$--accent-purple",
|
||||
"content": "On Writing Well",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01refG1B2I",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "file-text",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-purple"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG1B3",
|
||||
"name": "refItem3",
|
||||
"width": "fill_container",
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"padding": [4, 0],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG1B3T",
|
||||
"fill": "$--accent-purple",
|
||||
"content": "AI Agents Primer",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01refG1B3I",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "file-text",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-purple"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG2",
|
||||
"name": "refByGroup — via Related to",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG2T",
|
||||
"fill": "$--muted-foreground",
|
||||
"opacity": 0.7,
|
||||
"content": "VIA RELATED TO",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG2B1",
|
||||
"name": "refItem4",
|
||||
"width": "fill_container",
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"padding": [4, 0],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG2B1T",
|
||||
"fill": "$--accent-red",
|
||||
"content": "Failed SEO Experiment",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01refG2B1I",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "flask",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-red"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01refG2B2",
|
||||
"name": "refItem5",
|
||||
"width": "fill_container",
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"padding": [4, 0],
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01refG2B2T",
|
||||
"fill": "$--accent-red",
|
||||
"content": "Twitter Thread Experiment",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF01refG2B2I",
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"opacity": 0.5,
|
||||
"iconFontName": "flask",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--accent-red"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF01bl",
|
||||
"name": "Backlinks Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01blT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "BACKLINKS",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF01blI1",
|
||||
"fill": "$--primary",
|
||||
"content": "Write Weekly Essays",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02",
|
||||
"x": 360,
|
||||
"y": 0,
|
||||
"name": "Inspector — Referenced By (Empty)",
|
||||
"theme": { "Mode": "Light" },
|
||||
"clip": true,
|
||||
"width": 320,
|
||||
"height": 500,
|
||||
"fill": "$--background",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"padding": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02h",
|
||||
"name": "Inspector Header",
|
||||
"width": "fill_container",
|
||||
"height": 36,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02hL",
|
||||
"name": "leftGroup",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF02hI",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "sliders-horizontal",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02hT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "Properties",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF02hX",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "x",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02rel",
|
||||
"name": "Relationships Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02relT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "No relationships",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02ref",
|
||||
"name": "Referenced By Section (Empty)",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02refT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "REFERENCED BY",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02refE",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "No references",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF02bl",
|
||||
"name": "Backlinks Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02blT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "BACKLINKS",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF02blE",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "No backlinks",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03",
|
||||
"x": 720,
|
||||
"y": 0,
|
||||
"name": "Inspector — Referenced By (Many Backlinks)",
|
||||
"theme": { "Mode": "Light" },
|
||||
"clip": true,
|
||||
"width": 320,
|
||||
"height": 800,
|
||||
"fill": "$--background",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"padding": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03h",
|
||||
"name": "Inspector Header",
|
||||
"width": "fill_container",
|
||||
"height": 36,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03hL",
|
||||
"name": "leftGroup",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF03hI",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "sliders-horizontal",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF03hT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "Properties",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "rbF03hX",
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"iconFontName": "x",
|
||||
"iconFontFamily": "phosphor",
|
||||
"fill": "$--muted-foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03rel",
|
||||
"name": "Relationships Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03relG1",
|
||||
"name": "relGroup — Has",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF03relG1T",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "HAS",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03relG1B1",
|
||||
"width": "fill_container",
|
||||
"fill": "$--accent-purple-light",
|
||||
"cornerRadius": 6,
|
||||
"padding": [6, 10],
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03relG1B1T", "fill": "$--accent-purple", "content": "On Writing Well", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03relG1B1I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "file-text", "iconFontFamily": "phosphor", "fill": "$--accent-purple" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03relG1B2",
|
||||
"width": "fill_container",
|
||||
"fill": "$--accent-purple-light",
|
||||
"cornerRadius": 6,
|
||||
"padding": [6, 10],
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03relG1B2T", "fill": "$--accent-purple", "content": "Engineering Leadership 101", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03relG1B2I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "file-text", "iconFontFamily": "phosphor", "fill": "$--accent-purple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03ref",
|
||||
"name": "Referenced By Section (Many)",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 10,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03refH",
|
||||
"name": "refByHeader",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF03refHT",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "REFERENCED BY",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "rbF03refHC",
|
||||
"fill": "$--muted-foreground",
|
||||
"content": "8",
|
||||
"fontFamily": "IBM Plex Mono",
|
||||
"fontSize": 10,
|
||||
"fontWeight": "normal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03refG1",
|
||||
"name": "refByGroup — via Belongs to",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG1T", "fill": "$--muted-foreground", "opacity": 0.7, "content": "VIA BELONGS TO", "fontFamily": "IBM Plex Mono", "fontSize": 9 },
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG1B1", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG1B1T", "fill": "$--accent-orange", "content": "Write Weekly Essays", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG1B1I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "arrows-clockwise", "iconFontFamily": "phosphor", "fill": "$--accent-orange" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG1B2", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG1B2T", "fill": "$--accent-purple", "content": "On Writing Well", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG1B2I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "file-text", "iconFontFamily": "phosphor", "fill": "$--accent-purple" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG1B3", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG1B3T", "fill": "$--accent-purple", "content": "Engineering Leadership 101", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG1B3I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "file-text", "iconFontFamily": "phosphor", "fill": "$--accent-purple" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG1B4", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG1B4T", "fill": "$--accent-purple", "content": "AI Agents Primer", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG1B4I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "file-text", "iconFontFamily": "phosphor", "fill": "$--accent-purple" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03refG2",
|
||||
"name": "refByGroup — via Related to",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG2T", "fill": "$--muted-foreground", "opacity": 0.7, "content": "VIA RELATED TO", "fontFamily": "IBM Plex Mono", "fontSize": 9 },
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG2B1", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG2B1T", "fill": "$--accent-red", "content": "Failed SEO Experiment", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG2B1I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "flask", "iconFontFamily": "phosphor", "fill": "$--accent-red" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG2B2", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG2B2T", "fill": "$--accent-red", "content": "Twitter Thread Experiment", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG2B2I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "flask", "iconFontFamily": "phosphor", "fill": "$--accent-red" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03refG3",
|
||||
"name": "refByGroup — via Topics",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 4,
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG3T", "fill": "$--muted-foreground", "opacity": 0.7, "content": "VIA TOPICS", "fontFamily": "IBM Plex Mono", "fontSize": 9 },
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG3B1", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG3B1T", "fill": "$--accent-blue", "content": "Build Laputa App", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG3B1I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "wrench", "iconFontFamily": "phosphor", "fill": "$--accent-blue" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "rbF03refG3B2", "width": "fill_container", "justifyContent": "space_between", "alignItems": "center", "padding": [4, 0],
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03refG3B2T", "fill": "$--accent-blue", "content": "Newsletter Strategy 2026", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "500" },
|
||||
{ "type": "icon_font", "id": "rbF03refG3B2I", "width": 14, "height": 14, "opacity": 0.5, "iconFontName": "wrench", "iconFontFamily": "phosphor", "fill": "$--accent-blue" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03bl",
|
||||
"name": "Backlinks Section",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "rbF03blH",
|
||||
"gap": 6,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{ "type": "text", "id": "rbF03blT", "fill": "$--muted-foreground", "content": "BACKLINKS", "fontFamily": "IBM Plex Mono", "fontSize": 10 },
|
||||
{ "type": "text", "id": "rbF03blC", "fill": "$--muted-foreground", "content": "3", "fontFamily": "IBM Plex Mono", "fontSize": 10, "fontWeight": "normal" }
|
||||
]
|
||||
},
|
||||
{ "type": "text", "id": "rbF03blI1", "fill": "$--primary", "content": "Write Weekly Essays", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "normal" },
|
||||
{ "type": "text", "id": "rbF03blI2", "fill": "$--primary", "content": "Monthly Review Template", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "normal" },
|
||||
{ "type": "text", "id": "rbF03blI3", "fill": "$--primary", "content": "Q1 Planning", "fontFamily": "Inter", "fontSize": 12, "fontWeight": "normal" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": {}
|
||||
}
|
||||
@@ -324,4 +324,250 @@ This is a test note with some words to count.
|
||||
expect(row.className).not.toContain('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Referenced By (bidirectional relationships)', () => {
|
||||
const targetEntry: VaultEntry = {
|
||||
path: '/Users/luca/Laputa/responsibility/grow-newsletter.md',
|
||||
filename: 'grow-newsletter.md',
|
||||
title: 'Grow Newsletter',
|
||||
isA: 'Responsibility',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: 'Active',
|
||||
owner: null,
|
||||
cadence: null,
|
||||
archived: false,
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
modifiedAt: 1707900000,
|
||||
createdAt: null,
|
||||
fileSize: 500,
|
||||
snippet: '',
|
||||
relationships: { 'Type': ['[[type/responsibility]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const essayEntry: VaultEntry = {
|
||||
path: '/Users/luca/Laputa/essay/on-writing.md',
|
||||
filename: 'on-writing.md',
|
||||
title: 'On Writing Well',
|
||||
isA: 'Essay',
|
||||
aliases: [],
|
||||
belongsTo: ['[[responsibility/grow-newsletter]]'],
|
||||
relatedTo: [],
|
||||
status: null,
|
||||
owner: null,
|
||||
cadence: null,
|
||||
archived: false,
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
modifiedAt: 1707900000,
|
||||
createdAt: null,
|
||||
fileSize: 300,
|
||||
snippet: '',
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/essay]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const procedureEntry: VaultEntry = {
|
||||
path: '/Users/luca/Laputa/procedure/write-essays.md',
|
||||
filename: 'write-essays.md',
|
||||
title: 'Write Weekly Essays',
|
||||
isA: 'Procedure',
|
||||
aliases: [],
|
||||
belongsTo: ['[[responsibility/grow-newsletter]]'],
|
||||
relatedTo: [],
|
||||
status: null,
|
||||
owner: null,
|
||||
cadence: null,
|
||||
archived: false,
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
modifiedAt: 1707900000,
|
||||
createdAt: null,
|
||||
fileSize: 400,
|
||||
snippet: '',
|
||||
relationships: { 'Belongs to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/procedure]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const experimentEntry: VaultEntry = {
|
||||
path: '/Users/luca/Laputa/experiment/seo.md',
|
||||
filename: 'seo.md',
|
||||
title: 'SEO Experiment',
|
||||
isA: 'Experiment',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: ['[[responsibility/grow-newsletter]]'],
|
||||
status: null,
|
||||
owner: null,
|
||||
cadence: null,
|
||||
archived: false,
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
modifiedAt: 1707900000,
|
||||
createdAt: null,
|
||||
fileSize: 200,
|
||||
snippet: '',
|
||||
relationships: { 'Related to': ['[[responsibility/grow-newsletter]]'], 'Type': ['[[type/experiment]]'] },
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
const targetContent = `---
|
||||
title: Grow Newsletter
|
||||
is_a: Responsibility
|
||||
Status: Active
|
||||
---
|
||||
|
||||
# Grow Newsletter
|
||||
`
|
||||
|
||||
it('shows entries that reference the current note via frontmatter relationships', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={targetEntry}
|
||||
content={targetContent}
|
||||
entries={[targetEntry, essayEntry, procedureEntry, experimentEntry]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('On Writing Well')).toBeInTheDocument()
|
||||
expect(screen.getByText('Write Weekly Essays')).toBeInTheDocument()
|
||||
expect(screen.getByText('SEO Experiment')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('groups referenced-by entries by relationship key', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={targetEntry}
|
||||
content={targetContent}
|
||||
entries={[targetEntry, essayEntry, experimentEntry]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText(/via Belongs to/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/via Related to/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows count badge for referenced-by entries', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={targetEntry}
|
||||
content={targetContent}
|
||||
entries={[targetEntry, essayEntry, procedureEntry]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
// 2 entries reference via Belongs to
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows "No references" when no entries reference the current note', () => {
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={targetEntry}
|
||||
content={targetContent}
|
||||
entries={[targetEntry]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('No references')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('navigates when clicking a referenced-by entry', () => {
|
||||
const onNavigate = vi.fn()
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={targetEntry}
|
||||
content={targetContent}
|
||||
entries={[targetEntry, essayEntry]}
|
||||
allContent={{}}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('On Writing Well'))
|
||||
expect(onNavigate).toHaveBeenCalledWith('On Writing Well')
|
||||
})
|
||||
|
||||
it('skips Type relationships in referenced-by computation', () => {
|
||||
const typeEntry: VaultEntry = {
|
||||
...targetEntry,
|
||||
path: '/Users/luca/Laputa/type/responsibility.md',
|
||||
filename: 'responsibility.md',
|
||||
title: 'Responsibility',
|
||||
isA: 'Type',
|
||||
relationships: {},
|
||||
}
|
||||
// essayEntry has Type: [[type/responsibility]] — should NOT show as referenced-by
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={typeEntry}
|
||||
content="---\nIs A: Type\n---\n# Responsibility\n"
|
||||
entries={[typeEntry, essayEntry]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
// On Writing Well references responsibility via "Belongs to" (path match), not via "Type"
|
||||
// But the Type entry is at type/responsibility.md, so wikilinks to
|
||||
// responsibility/grow-newsletter won't match. Should show "No references"
|
||||
expect(screen.getByText('No references')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resolves references via aliased wikilinks', () => {
|
||||
const aliasedTarget: VaultEntry = {
|
||||
...targetEntry,
|
||||
aliases: ['Newsletter'],
|
||||
}
|
||||
const referrer: VaultEntry = {
|
||||
...essayEntry,
|
||||
relationships: { 'Topics': ['[[Newsletter]]'], 'Type': ['[[type/essay]]'] },
|
||||
}
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={aliasedTarget}
|
||||
content={targetContent}
|
||||
entries={[aliasedTarget, referrer]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('On Writing Well')).toBeInTheDocument()
|
||||
expect(screen.getByText(/via Topics/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show self-references', () => {
|
||||
const selfRef: VaultEntry = {
|
||||
...targetEntry,
|
||||
relationships: {
|
||||
...targetEntry.relationships,
|
||||
'Notes': ['[[responsibility/grow-newsletter]]'],
|
||||
},
|
||||
}
|
||||
render(
|
||||
<Inspector
|
||||
{...defaultProps}
|
||||
entry={selfRef}
|
||||
content={targetContent}
|
||||
entries={[selfRef]}
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('No references')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ import { cn } from '@/lib/utils'
|
||||
import { SlidersHorizontal, X } from '@phosphor-icons/react'
|
||||
import { parseFrontmatter } from '../utils/frontmatter'
|
||||
import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
|
||||
import { DynamicRelationshipsPanel, BacklinksPanel, GitHistoryPanel } from './InspectorPanels'
|
||||
import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, wikilinkTarget } from './InspectorPanels'
|
||||
import type { ReferencedByItem } from './InspectorPanels'
|
||||
|
||||
export type FrontmatterValue = string | number | boolean | string[] | null
|
||||
|
||||
@@ -40,6 +41,35 @@ function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[], allConten
|
||||
}, [entry, entries, allContent])
|
||||
}
|
||||
|
||||
function useReferencedBy(entry: VaultEntry | null, entries: VaultEntry[]): ReferencedByItem[] {
|
||||
return useMemo(() => {
|
||||
if (!entry) return []
|
||||
|
||||
const pathStem = entry.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, '')
|
||||
const filenameStem = entry.filename.replace(/\.md$/, '')
|
||||
const matchTargets = new Set([pathStem, filenameStem, entry.title, ...entry.aliases])
|
||||
|
||||
const results: ReferencedByItem[] = []
|
||||
|
||||
for (const other of entries) {
|
||||
if (other.path === entry.path) continue
|
||||
|
||||
for (const [key, refs] of Object.entries(other.relationships)) {
|
||||
if (key === 'Type') continue
|
||||
for (const ref of refs) {
|
||||
const target = wikilinkTarget(ref)
|
||||
if (matchTargets.has(target) || matchTargets.has(target.split('/').pop() ?? '')) {
|
||||
results.push({ entry: other, viaKey: key })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}, [entry, entries])
|
||||
}
|
||||
|
||||
function InspectorHeader({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<div className="flex items-center border-b border-border" style={{ height: 45, padding: '0 12px', gap: 8 }} data-tauri-drag-region>
|
||||
@@ -65,6 +95,10 @@ function EmptyInspector() {
|
||||
<>
|
||||
<div><p className="m-0 text-[13px] text-muted-foreground">No note selected</p></div>
|
||||
<div><p className="m-0 text-[13px] text-muted-foreground">No relationships</p></div>
|
||||
<div>
|
||||
<h4 className="font-mono-overline mb-2 text-muted-foreground">Referenced by</h4>
|
||||
<p className="m-0 text-[13px] text-muted-foreground">No references</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-mono-overline mb-2 text-muted-foreground">Backlinks</h4>
|
||||
<p className="m-0 text-[13px] text-muted-foreground">No backlinks</p>
|
||||
@@ -82,6 +116,7 @@ export function Inspector({
|
||||
onViewCommitDiff, onUpdateFrontmatter, onDeleteProperty, onAddProperty,
|
||||
}: InspectorProps) {
|
||||
const backlinks = useBacklinks(entry, entries, allContent)
|
||||
const referencedBy = useReferencedBy(entry, entries)
|
||||
const frontmatter = useMemo(() => parseFrontmatter(content), [content])
|
||||
|
||||
const handleUpdateProperty = useCallback((key: string, value: FrontmatterValue) => {
|
||||
@@ -111,6 +146,7 @@ export function Inspector({
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
<DynamicRelationshipsPanel frontmatter={frontmatter} entries={entries} onNavigate={onNavigate} onAddProperty={onAddProperty ? handleAddProperty : undefined} />
|
||||
<ReferencedByPanel items={referencedBy} onNavigate={onNavigate} />
|
||||
<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />
|
||||
<GitHistoryPanel commits={gitHistory} onViewCommitDiff={onViewCommitDiff} />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { DynamicRelationshipsPanel, BacklinksPanel, GitHistoryPanel } from './InspectorPanels'
|
||||
import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel } from './InspectorPanels'
|
||||
import type { ReferencedByItem } from './InspectorPanels'
|
||||
import type { VaultEntry, GitCommit } from '../types'
|
||||
|
||||
const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
|
||||
@@ -225,6 +226,69 @@ describe('BacklinksPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ReferencedByPanel', () => {
|
||||
const onNavigate = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('shows "No references" when items is empty', () => {
|
||||
render(<ReferencedByPanel items={[]} onNavigate={onNavigate} />)
|
||||
expect(screen.getByText('No references')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders referenced-by entries grouped by relationship key', () => {
|
||||
const items: ReferencedByItem[] = [
|
||||
{ entry: makeEntry({ path: '/vault/proc/a.md', title: 'Write Essays', isA: 'Procedure' }), viaKey: 'Belongs to' },
|
||||
{ entry: makeEntry({ path: '/vault/essay/b.md', title: 'On Writing Well', isA: 'Essay' }), viaKey: 'Belongs to' },
|
||||
{ entry: makeEntry({ path: '/vault/exp/c.md', title: 'SEO Experiment', isA: 'Experiment' }), viaKey: 'Related to' },
|
||||
]
|
||||
render(<ReferencedByPanel items={items} onNavigate={onNavigate} />)
|
||||
|
||||
expect(screen.getByText('Write Essays')).toBeInTheDocument()
|
||||
expect(screen.getByText('On Writing Well')).toBeInTheDocument()
|
||||
expect(screen.getByText('SEO Experiment')).toBeInTheDocument()
|
||||
expect(screen.getByText(/via Belongs to/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/via Related to/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows count badge when items exist', () => {
|
||||
const items: ReferencedByItem[] = [
|
||||
{ entry: makeEntry({ path: '/vault/a.md', title: 'A' }), viaKey: 'Has' },
|
||||
{ entry: makeEntry({ path: '/vault/b.md', title: 'B' }), viaKey: 'Has' },
|
||||
{ entry: makeEntry({ path: '/vault/c.md', title: 'C' }), viaKey: 'Topics' },
|
||||
]
|
||||
render(<ReferencedByPanel items={items} onNavigate={onNavigate} />)
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('navigates when clicking a referenced-by entry', () => {
|
||||
const items: ReferencedByItem[] = [
|
||||
{ entry: makeEntry({ path: '/vault/a.md', title: 'My Note' }), viaKey: 'Belongs to' },
|
||||
]
|
||||
render(<ReferencedByPanel items={items} onNavigate={onNavigate} />)
|
||||
fireEvent.click(screen.getByText('My Note'))
|
||||
expect(onNavigate).toHaveBeenCalledWith('My Note')
|
||||
})
|
||||
|
||||
it('dims archived entries in referenced-by', () => {
|
||||
const items: ReferencedByItem[] = [
|
||||
{ entry: makeEntry({ path: '/vault/a.md', title: 'Old Note', archived: true }), viaKey: 'Has' },
|
||||
]
|
||||
render(<ReferencedByPanel items={items} onNavigate={onNavigate} />)
|
||||
expect(screen.getByTitle('Archived')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows trashed indicator for trashed entries', () => {
|
||||
const items: ReferencedByItem[] = [
|
||||
{ entry: makeEntry({ path: '/vault/a.md', title: 'Trash Note', trashed: true }), viaKey: 'Has' },
|
||||
]
|
||||
render(<ReferencedByPanel items={items} onNavigate={onNavigate} />)
|
||||
expect(screen.getByTitle('Trashed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GitHistoryPanel', () => {
|
||||
const onViewCommitDiff = vi.fn()
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ function wikilinkDisplay(ref: string): string {
|
||||
return last.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
function wikilinkTarget(ref: string): string {
|
||||
export function wikilinkTarget(ref: string): string {
|
||||
const inner = ref.replace(/^\[\[|\]\]$/g, '')
|
||||
const pipeIdx = inner.indexOf('|')
|
||||
return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner
|
||||
@@ -217,6 +217,62 @@ export function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntr
|
||||
)
|
||||
}
|
||||
|
||||
export interface ReferencedByItem {
|
||||
entry: VaultEntry
|
||||
viaKey: string
|
||||
}
|
||||
|
||||
export function ReferencedByPanel({ items, onNavigate }: {
|
||||
items: ReferencedByItem[]
|
||||
onNavigate: (target: string) => void
|
||||
}) {
|
||||
const grouped = useMemo(() => {
|
||||
const map = new Map<string, VaultEntry[]>()
|
||||
for (const item of items) {
|
||||
const existing = map.get(item.viaKey)
|
||||
if (existing) existing.push(item.entry)
|
||||
else map.set(item.viaKey, [item.entry])
|
||||
}
|
||||
return Array.from(map.entries())
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="font-mono-overline mb-2 text-muted-foreground">
|
||||
Referenced by {items.length > 0 && <span className="ml-1" style={{ fontWeight: 400 }}>{items.length}</span>}
|
||||
</h4>
|
||||
{items.length === 0
|
||||
? <p className="m-0 text-[13px] text-muted-foreground">No references</p>
|
||||
: (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{grouped.map(([viaKey, groupEntries]) => (
|
||||
<div key={viaKey}>
|
||||
<span className="mb-1 block font-mono text-muted-foreground" style={{ fontSize: 9, fontWeight: 600, letterSpacing: '1.2px', textTransform: 'uppercase', opacity: 0.7 }}>
|
||||
via {viaKey}
|
||||
</span>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{groupEntries.map((e) => (
|
||||
<LinkButton
|
||||
key={e.path}
|
||||
label={e.title}
|
||||
typeColor={e.isA ? getTypeColor(e.isA) : 'var(--accent-blue)'}
|
||||
isArchived={e.archived}
|
||||
isTrashed={e.trashed}
|
||||
onClick={() => onNavigate(e.title)}
|
||||
title={e.trashed ? 'Trashed' : e.archived ? 'Archived' : undefined}
|
||||
TypeIcon={getTypeIcon(e.isA ?? undefined)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatRelativeDate(timestamp: number): string {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const days = Math.floor((now - timestamp) / 86400)
|
||||
|
||||
Reference in New Issue
Block a user