@fuaran-ui/renderer
v0.5.0
Published
React renderer + reference CSS + per-instance custom-renderer registry for the Fuaran UI typed Node tree — a conformant TypeScript host of the language-neutral contract in fuaran-dotnet/docs/WIRE_FORMAT.md. Renders any tree @fuaran-ui/ui authors or @fuara
Downloads
840
Maintainers
Readme
@fuaran-ui/renderer
The React renderer for the Fuaran UI typed Node tree — the user-visible
surface of the TypeScript reference implementation. A conformant host of the
language-neutral wire-format contract (fuaran-dotnet/docs/WIRE_FORMAT.md): it renders
any tree @fuaran-ui/ui authors or @fuaran-ui/ops decodes, with class-name
and ARIA parity to the F# reference renderer (the packaged reference CSS keys
off those exact class names).
npm install @fuaran-ui/ui @fuaran-ui/ops @fuaran-ui/renderer react react-domimport { FuaranRenderer } from '@fuaran-ui/renderer';
import '@fuaran-ui/renderer/css';
import { fuaran, node } from '@fuaran-ui/ui';
const tree = node('root', fuaran.heading({ level: 1, text: { kind: 'Literal', value: 'Hello' } }));
export const App = () => <FuaranRenderer tree={tree} />;Custom-renderer registry
NodeKind.Custom is the bounded escape hatch. Registries are per-instance
(never module-global) — the host threads one in via the runtime prop:
import {
createCustomRendererRegistry,
registerCustomRenderer,
FuaranRenderer,
} from '@fuaran-ui/renderer';
const registry = createCustomRendererRegistry();
registerCustomRenderer(registry, 'charts', 'sparkline', ({ props }) => (
<MySparkline data={props.points} />
));
<FuaranRenderer tree={tree} runtime={{ registry }} />;Theme bridge
import { FuaranRenderer, defaultTheme } from '@fuaran-ui/renderer';
<FuaranRenderer tree={tree} theme={defaultTheme} />; // injects --fuaran-* vars at the rootIn-page introspection (window.__fuaran)
Pass debug to register a DEBUG-only console global that exposes the running
UI's typed layer — so the browser DevTools console answers "which node is
this, what did this binding resolve to, where is it on screen?" instead of
handing back an untyped post-projection DOM node.
// Gate on the bundler's dev flag so it never registers in a production build.
<FuaranRenderer tree={tree} sources={sources} debug={import.meta.env.DEV} />Then, in the DevTools console of the running app:
__fuaran.help(); // method reference
__fuaran.inspectTree(); // recursive structural snapshot (every node id)
__fuaran.getNodeState('submit-btn'); // kind + bound binding slots + child ids
__fuaran.getBindingValue('counter-kpi', 'Source'); // resolved value vs the live sources
__fuaran.getRenderedDom('counter-kpi'); // live geometry (x/y/size + overflow/hidden)
__fuaran.findNodes('Button'); // ids of every node of a kind
__fuaran.apply(op); // policy-gated TreeOp mutation (default-deny)
__fuaran.getBindingState('counter-kpi', 'Source'); // as getBindingValue, tagged with the binding's identity
__fuaran.treeRevision(); // opaque token identifying the current tree state
__fuaran.subscribe((c) => console.log(c)); // committed-tree-change signal → unsubscribe fnThe global tracks the live tree + sources (it re-registers on each render), so
a value read after a state change reflects the new state. Its shape is
DEBUG-only and unstable (excluded from semver), and it is undefined unless
debug is set — leave it on import.meta.env.DEV and a production build never
registers it. buildDebugGlobal(tree, sources, options?) /
registerDebugGlobal(global) are exported for hosts that want to wire the
global on their own terms.
Policy-gated apply(op)
apply decodes a canonical TreeOp and applies it to the live tree — but only
when the host's policy gate permits, so an in-page mutation obeys the same
default-deny contract as every other dispatch path. Wire it by passing
onApply (the host's "re-render with this tree" callback, typically a
setState) alongside debug; the apply consults runtime.canDispatch first:
<FuaranRenderer
tree={tree}
runtime={runtime}
debug={import.meta.env.DEV}
onApply={setTree}
validate={(candidate) => {
const r = preEmitValidate(candidate); // @fuaran-ui/ui — optional
return r.ok ? [] : r.error;
}}
/>The op may be a JSON string (paste it into the console) or a structured object — the same edit, and the host serialises the object canonically rather than asking the caller to.
apply returns a structured envelope, never a silent no-op: { ok: true,
status: 'applied', treeRevision } on success; { ok: false, status: 'denied',
denied: true, error } when the gate refuses (the tree is unchanged);
'decodeFailed' (carrying the codec's DecodeError) / 'rejected' (carrying
the diagnostic code) for a malformed or inapplicable op; and 'unwired' when
no onApply was supplied (a read-only surface). A host that omits runtime (or
its canDispatch) allows by default, matching every other dispatch path.
The optional validate prop runs the host's tree validator on the candidate
tree and folds the edit only when it introduces no defect the tree did not
already carry — a pre-existing defect is not the edit's fault and does not block
it.
Change subscription
__fuaran.subscribe(cb) reports committed tree changes and returns an
unsubscribe handle. It pushes — there is nothing to poll — and rapid changes
coalesce into one notification carrying the latest treeRevision, because a
change is a staleness signal rather than a change log: re-read what you need.
The subscription lives on a page-wide hub, so it survives the surface rebuild
that every tree change causes.
const off = __fuaran.subscribe(({ treeRevision, cause }) => refresh(treeRevision, cause));
off(); // stoptreeRevision is opaque: compare it for equality to notice a cached read
has gone stale; never parse or order it.
DevTools relay (relay) — off by default
relay installs a same-origin postMessage endpoint that carries the surface
above across the page/extension boundary, so a browser extension (or any other
same-page script) can inspect — and, where onApply is wired, edit — the live
tree. It implements the [email protected] contract (DEVTOOLS_RELAY.md in the
specification repository) and is pinned against that contract's fixture family.
<FuaranRenderer
tree={tree}
debug={import.meta.env.DEV}
relay={import.meta.env.DEV}
onApply={setTree}
/>Three properties are deliberate and worth knowing before you enable it:
- Off by default, and absent rather than inert. Without
relay, no listener is installed at all, so a probe gets no answer whatsoever. Gate it on the same dev flag asdebug; it is a debugging affordance, not a feature flag. - No side door. Every relayed mutation crosses this host's own decode → validate → policy path, in the page. The relay contributes no apply engine, no validator, and no policy — a relay client's reach is exactly the set of legal, permitted ops, and no message can widen it.
- Origin discipline. Messages are accepted only from this same window at this
same origin, replies are posted at
window.origin(never"*"), and anything failing those checks is ignored in silence — a refusal would itself disclose that a Fuaran host is present.
createRelayPeer / installRelayPeer are exported for hosts wiring the peer on
their own terms; both are not opted in unless told to be.
Sanitisation
Every string→DOM seam (href, dangerouslySetInnerHTML, custom attributes)
routes through @fuaran-ui/renderer/sanitize. Custom-registered React
components are a host trust boundary — they are expected to do their own
escaping.
Apache-2.0. Part of the @fuaran-ui/* package set.
