From 1a87036087ff15b47f335241548cabf70a470c0a Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 24 Feb 2026 13:47:30 +0100 Subject: [PATCH 1/2] feat: make relations editable with add/remove controls Add remove (X) button on hover for each related note in a relationship group, and inline add control with autocomplete to add new notes to existing relations. Changes update frontmatter via the existing update/delete property pipeline. Co-Authored-By: Claude Opus 4.6 --- src/components/Inspector.tsx | 7 +- src/components/InspectorPanels.test.tsx | 156 ++++++++++++++++++++++ src/components/InspectorPanels.tsx | 169 ++++++++++++++++++++---- 3 files changed, 307 insertions(+), 25 deletions(-) diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index a1b5d942..16aa95f0 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -146,7 +146,12 @@ export function Inspector({ onAddProperty={onAddProperty ? handleAddProperty : undefined} onNavigate={onNavigate} /> - + diff --git a/src/components/InspectorPanels.test.tsx b/src/components/InspectorPanels.test.tsx index 18d6cd4b..76f12049 100644 --- a/src/components/InspectorPanels.test.tsx +++ b/src/components/InspectorPanels.test.tsx @@ -188,6 +188,162 @@ describe('DynamicRelationshipsPanel', () => { ) expect(screen.getByText('My Cool Project')).toBeInTheDocument() }) + + describe('relation editing', () => { + const onUpdateProperty = vi.fn() + const onDeleteProperty = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('shows remove buttons on relation refs when editing is enabled', () => { + render( + + ) + expect(screen.getByTestId('remove-relation-ref')).toBeInTheDocument() + }) + + it('does not show remove buttons when editing is disabled', () => { + render( + + ) + expect(screen.queryByTestId('remove-relation-ref')).not.toBeInTheDocument() + }) + + it('calls onDeleteProperty when removing the last ref in a group', () => { + render( + + ) + fireEvent.click(screen.getByTestId('remove-relation-ref')) + expect(onDeleteProperty).toHaveBeenCalledWith('Belongs to') + }) + + it('calls onUpdateProperty with remaining refs when removing one of many', () => { + render( + + ) + const removeButtons = screen.getAllByTestId('remove-relation-ref') + fireEvent.click(removeButtons[0]) // Remove first ref + expect(onUpdateProperty).toHaveBeenCalledWith('Related to', '[[topic/ai]]') + }) + + it('calls onUpdateProperty with string when two refs become one', () => { + render( + + ) + const removeButtons = screen.getAllByTestId('remove-relation-ref') + fireEvent.click(removeButtons[0]) + // Should pass a single string, not an array of one + expect(onUpdateProperty).toHaveBeenCalledWith('Has', '[[topic/ai]]') + }) + + it('shows inline add button for each relationship group when editing is enabled', () => { + render( + + ) + expect(screen.getByTestId('add-relation-ref')).toBeInTheDocument() + }) + + it('opens inline add input when add button clicked', () => { + render( + + ) + fireEvent.click(screen.getByTestId('add-relation-ref')) + expect(screen.getByTestId('add-relation-ref-input')).toBeInTheDocument() + }) + + it('adds a note to an existing relationship via inline add', () => { + render( + + ) + fireEvent.click(screen.getByTestId('add-relation-ref')) + const input = screen.getByTestId('add-relation-ref-input') + fireEvent.change(input, { target: { value: 'AI' } }) + fireEvent.keyDown(input, { key: 'Enter' }) + expect(onUpdateProperty).toHaveBeenCalledWith('Belongs to', ['[[project/my-project]]', '[[AI]]']) + }) + + it('does not add duplicate refs', () => { + render( + + ) + fireEvent.click(screen.getByTestId('add-relation-ref')) + const input = screen.getByTestId('add-relation-ref-input') + fireEvent.change(input, { target: { value: 'AI' } }) + fireEvent.keyDown(input, { key: 'Enter' }) + expect(onUpdateProperty).not.toHaveBeenCalled() + }) + + it('closes inline add on Escape', () => { + render( + + ) + fireEvent.click(screen.getByTestId('add-relation-ref')) + const input = screen.getByTestId('add-relation-ref-input') + fireEvent.keyDown(input, { key: 'Escape' }) + expect(screen.queryByTestId('add-relation-ref-input')).not.toBeInTheDocument() + expect(screen.getByTestId('add-relation-ref')).toBeInTheDocument() + }) + }) }) describe('BacklinksPanel', () => { diff --git a/src/components/InspectorPanels.tsx b/src/components/InspectorPanels.tsx index e32b9a44..5ce14a64 100644 --- a/src/components/InspectorPanels.tsx +++ b/src/components/InspectorPanels.tsx @@ -4,7 +4,7 @@ import { wikilinkTarget, wikilinkDisplay } from '../utils/wikilink' import type { VaultEntry, GitCommit } from '../types' import { Wrench, Flask, Target, ArrowsClockwise, - Users, CalendarBlank, Tag, FileText, StackSimple, Trash, + Users, CalendarBlank, Tag, FileText, StackSimple, Trash, X, Plus, } from '@phosphor-icons/react' import type { ParsedFrontmatter } from '../utils/frontmatter' import { RELATIONSHIP_KEYS, containsWikilinks } from './DynamicPropertiesPanel' @@ -40,36 +40,49 @@ function StatusSuffix({ isArchived, isTrashed }: { isArchived: boolean; isTrashe return null } -function LinkButton({ label, typeColor, bgColor, isArchived, isTrashed, onClick, title, TypeIcon }: { +function LinkButton({ label, typeColor, bgColor, isArchived, isTrashed, onClick, onRemove, title, TypeIcon }: { label: string typeColor: string bgColor?: string isArchived: boolean isTrashed: boolean onClick: () => void + onRemove?: () => void title?: string TypeIcon: ComponentType> }) { const isDimmed = isArchived || isTrashed const color = isDimmed ? 'var(--muted-foreground)' : typeColor return ( - +
+ + {onRemove && ( + + )} +
) } @@ -94,8 +107,75 @@ function resolveRefProps(ref: string, entries: VaultEntry[]) { } } -function RelationshipGroup({ label, refs, entries, onNavigate }: { +function InlineAddNote({ entries, onAdd }: { + entries: VaultEntry[] + onAdd: (noteTitle: string) => void +}) { + const [active, setActive] = useState(false) + const [query, setQuery] = useState('') + const inputRef = useRef(null) + const noteTitles = useMemo(() => entries.map(e => e.title), [entries]) + + const handleSubmit = useCallback(() => { + const trimmed = query.trim() + if (!trimmed) return + onAdd(trimmed) + setQuery('') + setActive(false) + }, [query, onAdd]) + + if (!active) { + return ( + + ) + } + + return ( +
+ {noteTitles.map(t => + setQuery(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') handleSubmit() + else if (e.key === 'Escape') { setQuery(''); setActive(false) } + }} + data-testid="add-relation-ref-input" + /> + + +
+ ) +} + +function RelationshipGroup({ label, refs, entries, onNavigate, onRemoveRef, onAddRef }: { label: string; refs: string[]; entries: VaultEntry[]; onNavigate: (target: string) => void + onRemoveRef?: (ref: string) => void; onAddRef?: (noteTitle: string) => void }) { if (refs.length === 0) return null return ( @@ -104,9 +184,17 @@ function RelationshipGroup({ label, refs, entries, onNavigate }: {
{refs.map((ref, idx) => { const props = resolveRefProps(ref, entries) - return onNavigate(props.target)} /> + return ( + onNavigate(props.target)} + onRemove={onRemoveRef ? () => onRemoveRef(ref) : undefined} + /> + ) })}
+ {onAddRef && } ) } @@ -166,15 +254,48 @@ function AddRelationshipForm({ entries, onAddProperty }: { ) } -export function DynamicRelationshipsPanel({ frontmatter, entries, onNavigate, onAddProperty }: { - frontmatter: ParsedFrontmatter; entries: VaultEntry[]; onNavigate: (target: string) => void; onAddProperty?: (key: string, value: FrontmatterValue) => void +export function DynamicRelationshipsPanel({ frontmatter, entries, onNavigate, onAddProperty, onUpdateProperty, onDeleteProperty }: { + frontmatter: ParsedFrontmatter; entries: VaultEntry[]; onNavigate: (target: string) => void + onAddProperty?: (key: string, value: FrontmatterValue) => void + onUpdateProperty?: (key: string, value: FrontmatterValue) => void + onDeleteProperty?: (key: string) => void }) { const relationshipEntries = useMemo(() => extractRelationshipRefs(frontmatter), [frontmatter]) + + const handleRemoveRef = useCallback((key: string, refToRemove: string) => { + if (!onUpdateProperty || !onDeleteProperty) return + const group = relationshipEntries.find(g => g.key === key) + if (!group) return + const remaining = group.refs.filter(r => r !== refToRemove) + if (remaining.length === 0) onDeleteProperty(key) + else if (remaining.length === 1) onUpdateProperty(key, remaining[0]) + else onUpdateProperty(key, remaining) + }, [relationshipEntries, onUpdateProperty, onDeleteProperty]) + + const handleAddRef = useCallback((key: string, noteTitle: string) => { + if (!onUpdateProperty) return + const group = relationshipEntries.find(g => g.key === key) + const existing = group?.refs ?? [] + const newRef = `[[${noteTitle}]]` + if (existing.includes(newRef)) return + const updated = [...existing, newRef] + if (updated.length === 1) onUpdateProperty(key, updated[0]) + else onUpdateProperty(key, updated) + }, [relationshipEntries, onUpdateProperty]) + + const canEdit = !!onUpdateProperty && !!onDeleteProperty + return (
{relationshipEntries.length === 0 ?

No relationships

- : relationshipEntries.map(({ key, refs }) => ) + : relationshipEntries.map(({ key, refs }) => ( + handleRemoveRef(key, ref) : undefined} + onAddRef={canEdit ? (noteTitle) => handleAddRef(key, noteTitle) : undefined} + /> + )) } {onAddProperty ? From fe55f9253dc39a25bd3a086ab7026382c29c8b1d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 24 Feb 2026 14:02:05 +0100 Subject: [PATCH 2/2] design: relations-edit wireframes (3 frames) --- design/relations-edit.pen | 1 + 1 file changed, 1 insertion(+) create mode 100644 design/relations-edit.pen diff --git a/design/relations-edit.pen b/design/relations-edit.pen new file mode 100644 index 00000000..a0bb9413 --- /dev/null +++ b/design/relations-edit.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"reF01","x":0,"y":0,"name":"Relations Edit — Default (hover to reveal remove)","theme":{"Mode":"Light"},"clip":true,"width":320,"height":520,"fill":"$--background","layout":"vertical","gap":16,"padding":16,"children":[{"type":"frame","id":"reF01h","name":"Inspector Header","width":"fill_container","height":36,"justifyContent":"space_between","alignItems":"center","children":[{"type":"frame","id":"reF01hL","name":"leftGroup","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF01hI","width":16,"height":16,"iconFontName":"sliders-horizontal","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF01hT","fill":"$--muted-foreground","content":"Properties","fontFamily":"Inter","fontSize":13,"fontWeight":"600"}]},{"type":"icon_font","id":"reF01hX","width":16,"height":16,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--muted-foreground"}]},{"type":"frame","id":"reF01rel","name":"Relationships Section","width":"fill_container","layout":"vertical","gap":16,"children":[{"type":"frame","id":"reF01relG1","name":"relGroup — Belongs to","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF01relG1T","fill":"$--muted-foreground","content":"BELONGS TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF01relG1B1","name":"relPill1","width":"fill_container","fill":"$--accent-blue-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF01relG1B1T","fill":"$--accent-blue","content":"Grow Newsletter","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF01relG1B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF01relG1B1X","width":14,"height":14,"opacity":0.5,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--accent-blue","annotation":"visible on hover"},{"type":"icon_font","id":"reF01relG1B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"target","iconFontFamily":"phosphor","fill":"$--accent-blue"}]}]},{"type":"frame","id":"reF01relG1Add","name":"addBtn","gap":4,"alignItems":"center","padding":[4,0],"children":[{"type":"icon_font","id":"reF01relG1AddI","width":14,"height":14,"iconFontName":"plus","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF01relG1AddT","fill":"$--muted-foreground","content":"Add","fontFamily":"Inter","fontSize":12}]}]},{"type":"frame","id":"reF01relG2","name":"relGroup — Related to","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF01relG2T","fill":"$--muted-foreground","content":"RELATED TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF01relG2B1","name":"relPill2","width":"fill_container","fill":"$--accent-purple-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF01relG2B1T","fill":"$--accent-purple","content":"On Writing Well","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF01relG2B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF01relG2B1X","width":14,"height":14,"opacity":0.5,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--accent-purple","annotation":"visible on hover"},{"type":"icon_font","id":"reF01relG2B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"file-text","iconFontFamily":"phosphor","fill":"$--accent-purple"}]}]},{"type":"frame","id":"reF01relG2B2","name":"relPill3","width":"fill_container","fill":"$--accent-red-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF01relG2B2T","fill":"$--accent-red","content":"Failed SEO Experiment","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF01relG2B2R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF01relG2B2X","width":14,"height":14,"opacity":0.5,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--accent-red","annotation":"visible on hover"},{"type":"icon_font","id":"reF01relG2B2I","width":14,"height":14,"opacity":0.5,"iconFontName":"flask","iconFontFamily":"phosphor","fill":"$--accent-red"}]}]},{"type":"frame","id":"reF01relG2Add","name":"addBtn","gap":4,"alignItems":"center","padding":[4,0],"children":[{"type":"icon_font","id":"reF01relG2AddI","width":14,"height":14,"iconFontName":"plus","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF01relG2AddT","fill":"$--muted-foreground","content":"Add","fontFamily":"Inter","fontSize":12}]}]}]}]},{"type":"frame","id":"reF02","x":360,"y":0,"name":"Relations Edit — Hover on pill (X remove visible)","theme":{"Mode":"Light"},"clip":true,"width":320,"height":520,"fill":"$--background","layout":"vertical","gap":16,"padding":16,"children":[{"type":"frame","id":"reF02h","name":"Inspector Header","width":"fill_container","height":36,"justifyContent":"space_between","alignItems":"center","children":[{"type":"frame","id":"reF02hL","name":"leftGroup","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF02hI","width":16,"height":16,"iconFontName":"sliders-horizontal","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF02hT","fill":"$--muted-foreground","content":"Properties","fontFamily":"Inter","fontSize":13,"fontWeight":"600"}]},{"type":"icon_font","id":"reF02hX","width":16,"height":16,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--muted-foreground"}]},{"type":"frame","id":"reF02rel","name":"Relationships Section","width":"fill_container","layout":"vertical","gap":16,"children":[{"type":"frame","id":"reF02relG1","name":"relGroup — Belongs to","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF02relG1T","fill":"$--muted-foreground","content":"BELONGS TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF02relG1B1","name":"relPill1 — hovered","width":"fill_container","fill":"$--accent-blue-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","stroke":{"align":"inside","thickness":1,"fill":"$--accent-blue","opacity":0.3},"annotation":"pill is hovered — X button fully visible","children":[{"type":"text","id":"reF02relG1B1T","fill":"$--accent-blue","content":"Grow Newsletter","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF02relG1B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF02relG1B1X","width":14,"height":14,"opacity":1,"iconFontName":"x-circle","iconFontFamily":"phosphor","fill":"$--accent-blue","annotation":"X remove — fully visible on hover"},{"type":"icon_font","id":"reF02relG1B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"target","iconFontFamily":"phosphor","fill":"$--accent-blue"}]}]},{"type":"frame","id":"reF02relG1Add","name":"addBtn","gap":4,"alignItems":"center","padding":[4,0],"children":[{"type":"icon_font","id":"reF02relG1AddI","width":14,"height":14,"iconFontName":"plus","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF02relG1AddT","fill":"$--muted-foreground","content":"Add","fontFamily":"Inter","fontSize":12}]}]},{"type":"frame","id":"reF02relG2","name":"relGroup — Related to","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF02relG2T","fill":"$--muted-foreground","content":"RELATED TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF02relG2B1","name":"relPill2","width":"fill_container","fill":"$--accent-purple-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF02relG2B1T","fill":"$--accent-purple","content":"On Writing Well","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF02relG2B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF02relG2B1X","width":14,"height":14,"opacity":0,"iconFontName":"x-circle","iconFontFamily":"phosphor","fill":"$--accent-purple","annotation":"hidden — not hovered"},{"type":"icon_font","id":"reF02relG2B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"file-text","iconFontFamily":"phosphor","fill":"$--accent-purple"}]}]},{"type":"frame","id":"reF02relG2Add","name":"addBtn","gap":4,"alignItems":"center","padding":[4,0],"children":[{"type":"icon_font","id":"reF02relG2AddI","width":14,"height":14,"iconFontName":"plus","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF02relG2AddT","fill":"$--muted-foreground","content":"Add","fontFamily":"Inter","fontSize":12}]}]}]}]},{"type":"frame","id":"reF03","x":720,"y":0,"name":"Relations Edit — Add expanded with autocomplete","theme":{"Mode":"Light"},"clip":true,"width":320,"height":620,"fill":"$--background","layout":"vertical","gap":16,"padding":16,"children":[{"type":"frame","id":"reF03h","name":"Inspector Header","width":"fill_container","height":36,"justifyContent":"space_between","alignItems":"center","children":[{"type":"frame","id":"reF03hL","name":"leftGroup","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF03hI","width":16,"height":16,"iconFontName":"sliders-horizontal","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF03hT","fill":"$--muted-foreground","content":"Properties","fontFamily":"Inter","fontSize":13,"fontWeight":"600"}]},{"type":"icon_font","id":"reF03hX","width":16,"height":16,"iconFontName":"x","iconFontFamily":"phosphor","fill":"$--muted-foreground"}]},{"type":"frame","id":"reF03rel","name":"Relationships Section","width":"fill_container","layout":"vertical","gap":16,"children":[{"type":"frame","id":"reF03relG1","name":"relGroup — Belongs to (add expanded)","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF03relG1T","fill":"$--muted-foreground","content":"BELONGS TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF03relG1B1","name":"relPill1","width":"fill_container","fill":"$--accent-blue-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF03relG1B1T","fill":"$--accent-blue","content":"Grow Newsletter","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF03relG1B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG1B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"target","iconFontFamily":"phosphor","fill":"$--accent-blue"}]}]},{"type":"frame","id":"reF03relG1Input","name":"addInputWrapper","width":"fill_container","layout":"vertical","gap":0,"children":[{"type":"frame","id":"reF03relG1InputF","name":"textInputField","width":"fill_container","height":32,"fill":"$--background","cornerRadius":6,"stroke":{"align":"inside","thickness":1,"fill":"$--primary"},"padding":[6,10],"alignItems":"center","gap":4,"children":[{"type":"icon_font","id":"reF03relG1InputSI","width":14,"height":14,"iconFontName":"magnifying-glass","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF03relG1InputTV","fill":"$--foreground","content":"Eng","fontFamily":"Inter","fontSize":12},{"type":"frame","id":"reF03relG1InputCursor","name":"textCursor","width":1,"height":14,"fill":"$--primary"}]},{"type":"frame","id":"reF03relG1Drop","name":"autocompleteDropdown","width":"fill_container","fill":"$--popover","cornerRadius":6,"stroke":{"align":"outside","thickness":1,"fill":"$--border"},"layout":"vertical","padding":[4,0],"children":[{"type":"frame","id":"reF03relG1DropI1","name":"suggestion1 — highlighted","width":"fill_container","fill":"$--accent","cornerRadius":4,"padding":[6,10],"gap":8,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG1DropI1I","width":14,"height":14,"iconFontName":"file-text","iconFontFamily":"phosphor","fill":"$--accent-purple"},{"type":"text","id":"reF03relG1DropI1T","fill":"$--foreground","content":"Engineering Leadership 101","fontFamily":"Inter","fontSize":12,"fontWeight":"500"}]},{"type":"frame","id":"reF03relG1DropI2","name":"suggestion2","width":"fill_container","padding":[6,10],"gap":8,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG1DropI2I","width":14,"height":14,"iconFontName":"file-text","iconFontFamily":"phosphor","fill":"$--accent-purple"},{"type":"text","id":"reF03relG1DropI2T","fill":"$--foreground","content":"Engineering Metrics","fontFamily":"Inter","fontSize":12}]},{"type":"frame","id":"reF03relG1DropI3","name":"suggestion3","width":"fill_container","padding":[6,10],"gap":8,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG1DropI3I","width":14,"height":14,"iconFontName":"target","iconFontFamily":"phosphor","fill":"$--accent-blue"},{"type":"text","id":"reF03relG1DropI3T","fill":"$--foreground","content":"English Writing Course","fontFamily":"Inter","fontSize":12}]},{"type":"frame","id":"reF03relG1DropI4","name":"suggestion4","width":"fill_container","padding":[6,10],"gap":8,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG1DropI4I","width":14,"height":14,"iconFontName":"flask","iconFontFamily":"phosphor","fill":"$--accent-red"},{"type":"text","id":"reF03relG1DropI4T","fill":"$--foreground","content":"Engine Performance Tests","fontFamily":"Inter","fontSize":12}]}]}]}]},{"type":"frame","id":"reF03relG2","name":"relGroup — Related to","width":"fill_container","layout":"vertical","gap":4,"children":[{"type":"text","id":"reF03relG2T","fill":"$--muted-foreground","content":"RELATED TO","fontFamily":"IBM Plex Mono","fontSize":10},{"type":"frame","id":"reF03relG2B1","name":"relPill2","width":"fill_container","fill":"$--accent-purple-light","cornerRadius":6,"padding":[6,10],"justifyContent":"space_between","alignItems":"center","children":[{"type":"text","id":"reF03relG2B1T","fill":"$--accent-purple","content":"On Writing Well","fontFamily":"Inter","fontSize":12,"fontWeight":"500"},{"type":"frame","id":"reF03relG2B1R","name":"rightIcons","gap":6,"alignItems":"center","children":[{"type":"icon_font","id":"reF03relG2B1I","width":14,"height":14,"opacity":0.5,"iconFontName":"file-text","iconFontFamily":"phosphor","fill":"$--accent-purple"}]}]},{"type":"frame","id":"reF03relG2Add","name":"addBtn","gap":4,"alignItems":"center","padding":[4,0],"children":[{"type":"icon_font","id":"reF03relG2AddI","width":14,"height":14,"iconFontName":"plus","iconFontFamily":"phosphor","fill":"$--muted-foreground"},{"type":"text","id":"reF03relG2AddT","fill":"$--muted-foreground","content":"Add","fontFamily":"Inter","fontSize":12}]}]}]}]}],"variables":{}}