SuperAPI AI Directory

obsidian-arrow-composition

Coding & development

View repository

Updated 10-15 23:48

About this skill

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 all .ts files in the area
find src/components/<area> src/components/shared -name "*.ts" | sort

# For each component, check who imports it
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 {
  // ... exact fields, exact types
}
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:

  1. 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?"

  2. 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?"

  3. 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?"

  4. 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?"

  5. 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?"

  6. 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?"

  7. 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:

  1. Layer table — primitive name, layer number, what it composes, what composes it
  2. Exact TypeScript interfaces for every new primitive
  3. Call mapping table — if replacing old functions, the exact old-call → new-call mapping
  4. Proposed file trees — final confirmed version for src/views/, src/components/, stories/
  5. 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