Node Renderers
A node renderer tells the workspace how to display nodes of a given type — the card in search results, the preview in side-panels, the full page when a user opens the node. Implement INodeRenderer, register it once, and every built-in component (search, neighbors, graph viewer) picks it up.
The contract
public interface INodeRenderer
{
string NodeType { get; } // The type this renderer applies to (e.g. N.Device.Type).
UIcons Icon { get; } // Icon used in lists and breadcrumbs.
string Color { get; } // Hex color used in graph viewer and badges.
CardContent CompactView(Node node); // Used in search results.
Task<IComponent> PreviewAsync(Node node); // Used in side panels.
Task<IComponent> ViewAsync(Node node, Parameters state); // Full page view.
}
CompactView is synchronous because it's rendered for every search hit — it must be fast. PreviewAsync and ViewAsync are async because they typically fetch related data.
Implementation example
public class DeviceRenderer : INodeRenderer
{
public string NodeType => N.Device.Type;
public UIcons Icon => UIcons.Box;
public string Color => "#346eeb";
public CardContent CompactView(Node node) =>
CardContent(
Header: Header(this, node),
Body: TextBlock(node.GetString(N.Device.Name)));
public Task<IComponent> PreviewAsync(Node node)
{
IComponent panel = VStack().Children(
TextBlock(node.GetString(N.Device.Name)).Size(18).SemiBold(),
Neighbors(node.UID, N.Part.Type, E.HasPart).WithLimit(5));
return Task.FromResult(panel);
}
public Task<IComponent> ViewAsync(Node node, Parameters state)
{
IComponent page = VStack().Children(
TextBlock(node.GetString(N.Device.Name)).Size(28).SemiBold(),
HStack().Children(
Card("Parts", Neighbors(node.UID, N.Part.Type, E.HasPart)),
Card("Open cases", Neighbors(node.UID, N.SupportCase.Type, E.HasSupportCase)
.WithFacets())));
return Task.FromResult(page);
}
}
Registering
Register the renderer once at startup, before the first route fires:
NodeRendererRegistry.Register(new DeviceRenderer());
NodeRendererRegistry.Register(new SupportCaseRenderer());
NodeRendererRegistry.Register(new ManufacturerRenderer());
Tip: keep one registration call per renderer so you can grep for Register(new and audit the set.
What overrides what
| Surface | Picks up renderer through |
|---|---|
SearchArea results |
CompactView |
Neighbors items |
CompactView |
| Side-panel preview | PreviewAsync |
Default node URL (#/node?uid=…) |
ViewAsync |
| Graph explorer node tooltip | CompactView (with Icon and Color) |
If you register a renderer for a node type that already has a default, your renderer takes precedence — there's no chaining.
Common patterns
- Header helper. Most renderers share
Header(this, node)to render an icon + key consistently. Factor it once. - Lazy fetch. In
ViewAsync, preferNeighbors(...)over anawait Q().Emit()—Neighborspaginates and caches. - Compact-only renderers. If you only want to customize search-result rendering and keep the default page view, return
nullfromViewAsync— the workspace falls back to the default.
Cross-links
- Curiosity components —
Neighbors,SearchArea,NodeViewer. - Routing and navigation — wiring a custom URL to
ViewAsync. - Tesserae basics — building blocks for
Render.