← Split overviewComponent specsPattern 3 of spec splitting, in detail. When a custom UI shows up on two or more screens, extract its contract into `docs/components/json/<name>.component.json`. Screens import it through `structure.customComponents`; the generator emits a platform-native component per target. This site ships five of these today — CodeBlock, Sidebar, TableOfContents, TopBar, and DocSamplePreview — and the same pattern scales to dozens.~10 min read
When to extractThe trigger is repetition across screens, not size. A 50-line inline View that only appears on one screen stays inline. A 30-line chart shape that shows up on three dashboards belongs in a component_spec. The break-even rule of thumb: two reuses pay for the spec + converter pair; three reuses make it obviously worth it. If it's still a one-off, stay with inline.
The spec shape`type: 'component_spec'`, a `metadata` block (name, category, description), a `props` map that declares the public API (type + required / default / enum / description per prop), an optional `slots` map for named children, and a `platformMapping` block that tells each platform's converter where to emit the implementation. Event props (functions) live in `props` too — their `type` carries the signature.
component_spec shape
// docs/components/json/chart.component.json{ "type": "component_spec", "version": "1.0", "metadata": { "name": "Chart", "category": "display", // one of: display, input, container, overlay "description": "Line / bar chart for time-series metrics." }, "props": { "data": { "type": "[ChartPoint]", "required": true, "description": "Points sorted by x." }, "kind": { "type": "String", "default": "line", "enum": ["line", "bar", "area"] }, "height": { "type": "Int", "default": 240 }, "onPointTap": { "type": "(ChartPoint) -> Void" } // event prop }, "slots": { // optional — named children "legend": { "type": "View?", "description": "Rendered above the chart." } }, "platformMapping": { // generator hints "web": { "emit": "extensions/Chart.tsx", "tagName": "Chart" }, "ios": { "emit": "Views/Chart.swift", "tagName": "Chart" }, "android": { "emit": "ui/Chart.kt", "tagName": "Chart" } }}Where things live on diskThe spec goes in `docs/components/json/<name>.component.json` — authored, committed, the single source of truth. The emitted implementation lives in each platform root's `extensions/` directory (`jsonui-doc-web/src/components/extensions/Chart.tsx` on web, for example). After the one-time scaffold the implementation file is hand-edited — the converter is what turns the Layout JSON reference into a call site against it.
on-disk placement
docs/└── components/ └── json/ ← authored component specs live here ├── chart.component.json ├── codeblock.component.json ← ships today ├── doc-sample-preview.component.json ├── search.component.json ├── sidebar.component.json ├── tableofcontents.component.json └── topbar.component.json jsonui-doc-web/└── src/ └── components/ └── extensions/ ← hand-authored component implementations ├── CodeBlock.tsx ← emitted once, edited by hand thereafter ├── DocSamplePreview.tsx └── Chart.tsx ← (would live here after `jui g converter`)Referencing from a screenTwo steps. (1) Add the component to `structure.customComponents` in the screen spec with `{ name, ref }` — the `ref` is the path to the component_spec.json. (2) Use it in the Layout JSON as if it were a built-in View type: the prop names come straight from the component_spec's `props` map. Bind values with `@{…}`, pass static props inline, and wire events with `@{handler}` just like a built-in.
screen spec + Layout usage
// Any screen spec imports a component by adding it to// structure.customComponents and then using it in the Layout JSON. // analytics.spec.json — references the Chart component.{ "metadata": { "name": "Analytics", "layoutFile": "analytics" }, "structure": { "customComponents": [ { "name": "Chart", "ref": "docs/components/json/chart.component.json" } ] }} // docs/screens/layouts/analytics.json — uses the component like any other View type.{ "type": "View", "orientation": "vertical", "child": [ { "type": "Chart", "width": "matchParent", "height": 260, "data": "@{metricsPoints}", // prop binding "kind": "area", // static prop "onPointTap": "@{onSelectPoint}" // event binding } ]}Scaffolding the implementation`rjui g converter <Name>` (web) / `sjui g converter <Name>` (iOS) / `kjui g converter <Name>` (Android) creates two files per platform: a converter `.rb` file under `rjui_tools/lib/react/converters/extensions/` (or the Swift / Kotlin equivalent) that translates the Layout JSON reference into a platform call site, and a stub implementation file under the platform's `extensions/` directory. The stub is the starting point — you edit it by hand; the converter is the machine-readable glue.
one-time scaffold
# One-time: scaffold the platform-native component skeleton from the spec.# Generates one converter + one component impl per target platform. # Web — emits jsonui-doc-web/src/components/extensions/Chart.tsx# and jsonui-doc-web/rjui_tools/lib/react/converters/extensions/chart_converter.rbrjui g converter Chart # iOS — equivalent for Swiftsjui g converter Chart # Androidkjui g converter Chart # After scaffolding you edit the component body by hand (the converter is# the machine-readable translator from Layout JSON → platform source).# The component_spec.json remains the single source of truth for props,# slots, and events that the converter compiles against.Live examples on this siteFive component specs ship today: `codeblock.component.json` (the syntax-highlighted code snippet you're reading this next to), `doc-sample-preview.component.json` (the iframe wrapper on `/tools/doc`), `search.component.json`, `sidebar.component.json`, `tableofcontents.component.json`, and `topbar.component.json`. `codeblock` is the richest — copy button, line numbers, highlight lines, theme — and a good read if you want a real, non-trivial example to start from.
When NOT to extractTwo anti-patterns. (1) Extracting a one-off View just because it's long — readers have to jump to another file for something they'll only ever see once. (2) Extracting a composition of built-ins that doesn't need platform-native code — if you can express it in Layout JSON alone, that's Pattern 1 (a shared Layout JSON) or Pattern 5 (a reusable cell), not a component_spec. component_spec is for things that need *code*, not *structure*, to be reusable.
Keep goingThree patterns down, two to go — customTypes and cellClasses.
Parent + sub specsPattern 2 — split one screen into multiple spec files by region of state./spec/parent-sub-spec
Custom typesPattern 4 — declare shared row / entry shapes once in dataFlow.customTypes, reference by name across specs./spec/custom-types