| name | obsidian-arrow-composition |
| description | Use when designing the component structure for a new view area or refactoring an existing one. Surveys the codebase for existing components and shared patterns, proposes a src/views/ + src/components/ hierarchy with file trees, asks targeted questions to confirm decisions, checks for DRY violations and improper primitive use, and produces a locked hierarchy doc the migration agent can execute. Run this BEFORE writing any component code or migration plan. |
Component Composition Analysis
An interactive, iterative workflow for figuring out the right component structure before writing any code. The output is a locked hierarchy document โ file trees, layer assignments, primitive interfaces, and call mappings โ that a migration or implementation agent can execute without further design decisions.
Announce at start: "I'm using the obsidian-arrow-composition skill to analyse the component structure."
Hard gate: Do NOT produce a hierarchy document until the user has confirmed the proposed structure. Show file trees, ask questions, iterate. The locked plan is the output of a confirmed conversation, not a first-pass guess.
Process
digraph composition {
"Survey codebase" [shape=box];
"Show file tree of current state" [shape=box];
"Map shared template shapes" [shape=box];
"Propose layer hierarchy" [shape=box];
"Show proposed file trees (views + components)" [shape=box];
"Ask targeted questions" [shape=box];
"User confirms?" [shape=diamond];
"Revise proposal" [shape=box];
"Output locked hierarchy doc" [shape=box style=filled fillcolor=lightgreen];
"Survey codebase" -> "Show file tree of current state";
"Show file tree of current state" -> "Map shared template shapes";
"Map shared template shapes" -> "Propose layer hierarchy";
"Propose layer hierarchy" -> "Show proposed file trees (views + components)";
"Show proposed file trees (views + components)" -> "Ask targeted questions";
"Ask targeted questions" -> "User confirms?" [label="answered"];
"User confirms?" -> "Revise proposal" [label="no / changes"];
"User confirms?" -> "Output locked hierarchy doc" [label="yes"];
"Revise proposal" -> "Show proposed file trees (views + components)";
}
Step 1: Survey the codebase
Read every component file in the area under analysis. Do not skip any. For each file, record:
- What it exports โ function names, interfaces, reactive state objects
- What it renders โ the root DOM element and key class names in the
html\`` template
- What it imports โ which other components, shared primitives, or utilities it depends on
- Who imports it โ which other files use this component
find src/components/<area> src/components/shared -name "*.ts" | sort
grep -rn "from.*<ComponentName>" src/ --include="*.ts"
Also check the existing src/components/ for already-extracted shared primitives โ do not re-extract something that already exists.
Step 2: Show the current file tree
Display the current structure clearly so the user can see the starting point:
Current state:
src/components/
<area>/
ComponentA.ts โ exports: ComponentA()
ComponentA.css
ComponentB.ts โ exports: ComponentB(), helperFn()
ComponentB.css
...
shared/
PrimitiveX.ts โ exports: PrimitiveX()
PrimitiveY.ts โ exports: PrimitiveY()
...
stories/
<area>/
ComponentA.stories.ts
...
Ask: "Does this match your understanding of the current state? Anything missing or wrong?"
Wait for confirmation before proceeding.
Step 3: Map shared template shapes
Survey the actual template output of each component โ not what they conceptually do, but what DOM structure and CSS class patterns they produce. Look for:
- Identical or near-identical HTML structure โ same element hierarchy, same class names
- Repeated prop shapes โ multiple components accepting
items: () => T[], onSelect: (item: T) => void, renderItem: (item: T) => ArrowTemplate
- Repeated CSS patterns โ same class names applied to the same structural role across multiple components
What to look for โ real signals:
- Two components that both produce a
<div class="some-list"> containing rows with click handlers โ possible shared ItemList primitive
- Two components that both produce a positioned box with a header and content slot โ possible shared Popover primitive
- Three components that each declare their own
ICON_MAP = { ... } โ should import from one shared icons.ts
- Two components that each implement the same input+filter+list shape โ possible FilterableList primitive
What to ignore โ false signals:
- "These both open when something is clicked" โ interaction pattern, not a shared template shape
- "These are both popovers" โ label, not a shared invariant
- Components that are similar in purpose but produce different DOM
Report the actual shared template fragments found โ quote the real code, don't summarize.
Step 4: Propose a layer hierarchy
Based on the shared shapes found, propose a bottom-up layer hierarchy:
Layer 0 โ atomic building blocks (no imports from other layers)
Layer 1 โ composed from Layer 0 only
Layer 2 โ composed from Layer 0 + 1 only
Layer 3 โ thin callers of Layer 2 (trigger + content wrapper)
Rules:
- A primitive at Layer N never imports a peer at Layer N (no sideways imports)
- A view-specific component at any layer stays inside its view folder
- If a pattern appears in only one place, it is NOT a primitive โ leave it in place
- If a pattern appears in 2+ places with identical template structure, it is a candidate โ propose extracting it
For each proposed primitive, specify the exact interface (TypeScript):
export interface PrimitiveXOptions {
}
export function PrimitiveX(options: PrimitiveXOptions): ArrowTemplate
Step 5: Show proposed file trees
Show BOTH the proposed src/views/ structure and the proposed src/components/ structure side by side:
Proposed: src/views/
ChatView/
ChatView.ts โ view (kind: "view")
ChatView.css
state.ts
Composer.ts โ view-specific (belongs to this view)
Composer.css
ToolCard.ts โ view-specific (belongs to this view)
ToolCard.css
...
Proposed: src/components/
icons.ts โ flat (no CSS)
ItemList/
ItemList.ts
(no CSS โ callers supply their own list container)
Popover/
Popover.ts
Popover.css
FilterableList/
FilterableList.ts
(no CSS โ uses Popover's CSS)
...
Proposed: stories/
views/
ChatView/
ChatView.stories.ts (kind: "view")
Composer.stories.ts (kind: "component")
ToolCard.stories.ts (kind: "component")
components/
ItemList.stories.ts (kind: "component")
Popover.stories.ts (kind: "component")
...
Apply the folder-when-needed rule: a component gets its own directory only when it has more than one file (CSS, types.ts, state.ts). A single .ts file with no companions stays flat.
Step 6: Ask targeted questions
After showing the proposed trees, ask the specific questions needed to confirm or correct design decisions. One question per message where possible.
Standard questions to work through:
-
View vs component boundary: "I've put Composer inside ChatView/ because it's only used there. Is it view-specific by nature, or does it belong in src/components/ as a shared component?"
-
Primitive scope: "I've proposed extracting ItemList as a shared primitive. Does the list-of-clickable-rows pattern actually recur with identical structure, or should some of these stay hand-rolled?"
-
Layer assignment: "I've put FilterableList at Layer 2 (depends on PopoverInput + ItemList but not on Popover). Does that feel right, or should FilterableList compose Popover instead?"
-
Folder-when-needed check: "I've shown Popover as a folder since it has CSS. Should it be flat? Does it actually need its own CSS or will callers supply all class names?"
-
DRY check: "I found this pattern duplicated in ContextSuggest and AutocompletePopover:" [quote actual code] "Should this be extracted as a shared primitive or are the differences significant enough to keep them separate?"
-
CSS ownership: "Who owns the CSS for Suggest's positioned box โ does Suggest have its own .css file, or does each consumer supply the class via rootClass?"
-
Stories: "For ToolCard, which states actually look meaningfully different? I see running, done, error in the component โ should there be variants for all three, or just running and done (error being the same visual as done with different color)?"
Step 7: DRY and primitive-use audit
Before finalizing, explicitly check:
Icons: Is there a single src/components/icons.ts? Does every component that uses an icon import from it, or are there local ICON_MAP copies?
Shared CSS: Is there a src/components/primitives.css (or similar) for genuinely cross-component chrome? Is it imported directly (not via a barrel) so orphaned-CSS lint catches it if it loses its importer?
Primitive adoption: For each proposed primitive, confirm it has at least 2 real consumers with identical template structure ready to adopt it now โ not anticipated future consumers.
Barrel exports: Is there a shared/index.ts barrel? If yes, recommend dissolving it and using direct imports (barrels hide unused exports from the check:imports lint).
View-specific components leaking: Run:
grep -rn "from.*views/<ViewName>" src/ --include="*.ts"
If anything outside src/views/<ViewName>/ imports from inside it, flag it โ either it should be in src/components/ or the importer is wrong.
Step 8: Output the locked hierarchy document
Only after the user has confirmed the structure, produce the locked hierarchy document. It must contain:
- Layer table โ primitive name, layer number, what it composes, what composes it
- Exact TypeScript interfaces for every new primitive
- Call mapping table โ if replacing old functions, the exact old-call โ new-call mapping
- Proposed file trees โ final confirmed version for
src/views/, src/components/, stories/
- Verification steps โ what to cold-load check after each step, including specific computed-style assertions
Save to: docs/plans/<area>-composition-<date>.md
Worked example: the Popover primitive was designed with this exact process โ
see docs/popover-consolidation-index.md
(the survey/index step) and docs/plans/popover-composition-2026-07-06.md
(the locked hierarchy). It landed a primitive shell (Popover) + structural
primitives (Button, ListItem, SearchField, SectionLabel) โ a clean
primitive/composed split. If the area involves floating UI, also read
docs/arrow-notes.md before designing.
Red flags โ stop and ask
- A proposed primitive has only one consumer โ don't extract it
- A proposed primitive's interface has more parameters than the originals had lines โ the abstraction is wrong, go back to survey
- A component is proposed for
src/components/ but imports from state.ts or types.ts in a specific view area โ it's view-specific
- "These do similar things" as the justification for shared primitive โ not sufficient; require identical template fragments
- Any
as unknown as ArrowTemplate cast in proposed interfaces โ red flag, requires explicit discussion