Rune packages provide their own layouts
Context
Layouts are currently defined in packages/transform/src/layouts.ts and exported from @refrakt-md/transform. There are four: defaultLayout, docsLayout, planLayout, and blogArticleLayout.
Two of these — docsLayout and planLayout — are domain-specific. The docs layout references computed slots like breadcrumb and version-switcher that only make sense in a documentation context. The plan layout is tightly coupled to the plan package's pipeline hooks and rune set. Yet both live in transform, a low-level package with no dependency on runes or any community package.
This creates an inverted dependency: transform contains domain knowledge that logically belongs to runes/docs/ and runes/plan/. The plan package already has to import { planLayout } from '@refrakt-md/transform' to use its own layout — reaching across packages for something it should own.
Meanwhile, RunePackage already supports contributing theme.runes (engine config), behaviors, and pipeline hooks. Layouts are the same kind of contribution — a declarative config that a package provides to the system — but there is no field for them.
Decision
Option 2: Packages provide layouts via RunePackage.
docsLayoutmoves torunes/docs/— it is owned by@refrakt-md/docsplanLayoutmoves torunes/plan/— it is owned by@refrakt-md/plandefaultLayoutandblogArticleLayoutremain in@refrakt-md/transform— they are generic and have no domain-specific semantics (blog article layout uses standard frontmatter fields, not blog-package-specific concepts)- Shared chrome building blocks (menu button, close button, search button, hamburger SVGs) are exported from
@refrakt-md/transformso packages can compose them without duplication
Rationale
RunePackage is already the unit of distribution for everything a rune domain needs: schemas, engine config, behaviors, and pipeline hooks. Layouts complete the picture. A docs package that provides doc-specific runes, doc-specific computed slots (breadcrumb, version-switcher), and doc-specific pipeline hooks should also provide the doc-specific layout that ties them all together.
Option 1 keeps the status quo but the dependency direction is wrong — transform shouldn't encode docs or plan domain knowledge. Option 3 adds an unnecessary package and still doesn't solve the community package story.
The blog article layout stays in transform because it uses only generic frontmatter fields (title, date, author, tags) and doesn't depend on any rune package concepts. There is no need for a separate blog rune package.
Consequences
RunePackagegains alayoutsfield. Either directly onRunePackageor nested undertheme, it maps layout names toLayoutConfigobjects. The exact placement should follow the existing pattern —theme.layoutsis a natural fit alongsidetheme.runes.@refrakt-md/transformexports chrome building blocks. The shared SVG icons and chrome structure entries (menuButton,closeButton,searchButton,hamburger) become public exports that packages can import and compose into their layouts.mergePackages()collects layouts. The existing package merging logic inpackages/runes/src/packages.tsextends to aggregate layouts from all loaded packages, detecting name collisions the same way it does for rune names.Lumina's theme entry points change. Instead of importing
docsLayoutandplanLayoutfrom@refrakt-md/transform, the theme gets them from the merged package config.defaultLayoutandblogArticleLayoutcontinue to be imported fromtransform.Plan package simplifies.
runes/plan/src/commands/render-pipeline.tscurrently importsplanLayoutfrom@refrakt-md/transform. After the move, it imports from its own package — the correct dependency direction.Community packages can ship layouts. A future package (e.g., a wiki package, a knowledge-base package) can provide its own layout alongside its runes without needing changes to core.