MeshWeaver UI is server-driven: a layout area computes a tree of typed controls in a per-node hub, and every client renders that tree — Blazor Server, the Next.js shell (portal-next), the React Native app, and MAUI. Because the tree is the contract, most UI work inherits into every client automatically. This page is the author's guide to keeping it that way: what you get for free, the rules that preserve it, and the exact checklist when you add a new control.
The architecture of the extension lanes is UI Extensibility; the layout-area procedure itself is the /layout-area skill.
What auto-inherits — and why
The JS shells are not ports of the Blazor portal. They subscribe to the same live streams the
Blazor renderer uses: a SubscribeRequest with a LayoutAreaReference yields versioned frames
(full, then JSON patches), and interactions flow back as the same small event set. Everything that
lives in the control tree therefore renders everywhere with zero client work:
- layout areas, containers, grids, tabs, markdown, DataGrids, forms and their two-way bindings;
- dialogs and click actions (
WithClickAction,DialogControl); - menus — the node/mesh/AI menus are
$Menu:*areas inside the same stream, i.e. protocol data, not client code. This is the model case: chrome expressed as controls needs no parity work, ever.
What does not inherit is anything outside the tree: shell-level access gates, and any semantics a client must implement per control (see the parity section).
The five rules
Controls only, never markup.
Controls.Htmlwith hand-built markup renders as an opaque blob on the JS shells at best. Structured data goes throughControls.DataGrid, composition throughControls.Stack/Controls.LayoutGrid— the same rule the /layout-area skill states for Blazor, with double force here.Geometry and derivation live in the control, not the view. If a visual needs computed layout, compute it server-side into the control record so every renderer draws the result. The analysis controls are the precedent:
TowerControl.Layout()resolves the band geometry inMeshWeaver.Layout, and the Blazor, React, and MAUI views all render the same resolved rows. A view that derives geometry client-side has to be written four times — and will drift.Check semantic parity before relying on advanced control behaviour. The parity ratchets (below) guarantee every control type has a renderer in every pack — they cannot guarantee every behaviour. Real example:
MarkdownEditorControl.AutoSaveAddresswas implemented in Blazor and silently ignored by the JS packs — edits in auto-save views did not persist there, and nothing failed. When your area depends on a control's write-path or side-effect semantics, verify the JS pack implements it (or file the gap) before shipping.Localize through the catalog — and mirror the keys. Server-rendered text uses
host.Localize("key")with the key in bothstrings.en.jsonandstrings.de.json(src/MeshWeaver.Messaging.Hub/Localization/). 🚨 Every new key has a second home:clients/react/src/i18n/bundles a byte-identical copy so the JS shells resolve synchronously, andlocalize.test.tsis a drift guard asserting key-and-value equality — add a key server-side only and theClientsworkflow goes red while a JS client would render the raw key. Prefer glyphs over words where possible, and see Localization for the[Translation]attribute path for declaration-bound text.Stay inside the interaction surface. The events every client speaks are: click, blur, close-dialog, and field edits (a JSON patch against the bound pointer). An interaction that needs more than these — drag, keyboard chords, scroll observation — is a framework/pack feature, not something a layout area can express portably.
Known per-renderer gaps (as of 2026-08)
Be honest with yourself about these when designing a view:
| Capability | Blazor | portal-next / RN |
|---|---|---|
| Monaco completions + live diagnostics | yes | value binding only — no LSP yet |
| CollaborativeMarkdown annotations (accept/reject, threads) | yes | read-only render |
AutoSaveAddress editors |
yes | gap — tracked, verify before relying on it |
Content bytes (/api/content) behind auth in <img> |
cookie session | needs the signed-URL work |
Every control type renders on every shell — the packs pass a zero-missing, zero-placeholder ratchet — so the gaps above are semantic, not structural, and each is on the universal-protocol work list.
Adding a new control: the checklist
A new control is not done when the Blazor view renders. The definition of done:
- The control record in
MeshWeaver.Layout(or your view pack): an immutable record deriving fromUiControl, with any visual derivation as a server-side method on the record (rule 2). Add aControls.X(...)factory only if the control is framework-level. - Serialization registration:
config.WithType(typeof(XControl))wherever the views register — an unregistered$typedegrades to an untypedJsonElementand renders empty. - The Blazor view: via the pack seam
(
AddViews(l => l.WithView<XControl, XView>())) for packs, or the core registry for framework controls. - The React renderer: a component in
clients/react/src/controls/wired into the render registry. The Blazor-parity ratchet (clients/react/src/render/parity.test.ts) scrapes the core registry's control list and fails on any missing or placeholder entry — a new core control turns theClientsworkflow red until the React side exists. That is deliberate: the ratchet is what keeps "renders everywhere" true. React-side patterns: React · React Custom Controls. - The React Native pack: the
rnPackentry plus its own parity test (MeshWeaver.Plugins/app/react-native/src/parity.test.ts). RN consumes the shared renderer core, so most controls are a thin mapping; native-feeling leaves (HTML, editors) have RN-specific views. - Localization of any user-visible strings per rule 4 — both catalogs, both homes.
- A gallery/doc page under
Doc/GUIwith an executable--rendercell, so the control is discoverable and its example is compiled and rendered by the doc gate on every PR.
For steps 4–5 the wire shape matters: the client sees your control as camelCase JSON with a
$type discriminator, patches arrive as RFC 6902 against the area document, and collection keys
arrive JSON-encoded. The protocol reference lives in the repo at
clients/react/docs/live-protocol.md.
Testing across renderers
- Server-side: a layout-area render test (the
Testsarea pattern thatmw-plugin-testexecutes, or anAreaProbe-based test) proves the control tree materializes — this covers every renderer's input. - JS shells: the two parity ratchets prove coverage; component-level tests live next to the
React controls (vitest). The shells' own suites (portal-next, RN) run in the
Clientsworkflow on every PR. - What a green Blazor test does not prove: any behaviour in rule 3's category. If the JS pack lacks the semantic, no existing test fails — which is exactly why the checklist puts the React renderer in the definition of done rather than in a follow-up.
Related: UI Extensibility · Layout Areas · GUI Data Binding · React · React Custom Controls · Localization