# Rule: componentization ## When to make a component Extract when the same markup appears **three times**, or when a block has a name a person would use out loud ("the eyebrow", "the route card", "the phase panel"). Do not extract on the second occurrence. Two similar blocks often diverge; the premature abstraction costs more than the duplication. ## Sizes - A component that exceeds ~120 lines of markup is doing two jobs. Split it. - A page that is a bare list of components with no markup of its own has been over-split. Pages are allowed to contain layout. ## Boundaries ``` src/components/ primitives/ Eyebrow, Rule, Callout, CodeBlock — no domain knowledge blocks/ RouteCard, PhasePanel, HandoffTable, SkillPackage — composed, page-agnostic islands/ interactive only; each one justified per rules/astro.md ``` - Primitives never import blocks. - Blocks never import page-specific data; they take props. - Islands are leaves. An island must not wrap static children that could have been server-rendered. ## Props - Typed `interface Props`, every field. No `any`, no untyped rest spread. - Required by default. Optional props need a default and a reason. - Pass data, not markup. If you find yourself passing an HTML string, you want a ``. ## Named exports, no barrels Import the file you need. Barrel `index.ts` files break tree-shaking and create import cycles; bulletproof-react advises against them and so do we. ## The catalog is data, not components The 24 review-desk entries are content, not 24 components. One `SkillReviewCard.astro` iterating a typed collection. If you are writing the 25th near-identical component, stop and model the data. ## Do not componentize `hands-on/starter/` and `hands-on/rules/` are lab fixtures. Their whole value is being flat, dependency-free files an attendee hands to an agent. They ship from `public/` unchanged.