Tesserae Basics
Tesserae is the UI toolkit Curiosity workspaces (and the desktop app) are built with. You author components as C# classes that implement IComponent; the h5 compiler turns the code into JavaScript that runs in the browser. For the full reference see the Tesserae documentation.
This page is the minimum you need to be productive inside a custom workspace front-end.
A component
Every view implements IComponent. The Render() method returns an HTMLElement.
using H5.Core;
using Tesserae;
using static Tesserae.UI;
public class HelloView : IComponent
{
public dom.HTMLElement Render() =>
VStack().Children(
TextBlock("Hello, workspace").Size(24).SemiBold(),
Button("Click me").OnClick(() => window.alert("Hi!"))
).Render();
}
static Tesserae.UI is the trick that makes the fluent API readable — VStack, HStack, TextBlock, Button are all top-level factories.
Composition
Components are first-class values; pass them around like any other.
IComponent Header(string title) =>
HStack().AlignItemsCenter().Padding(8.px()).Children(
Icon(UIcons.Box).Color("#346eeb"),
TextBlock(title).SemiBold()
);
IComponent Card(string title, IComponent body) =>
VStack().Background("white").Border().Padding(12.px()).Children(
Header(title),
body
);
Layout primitives
| Primitive | Purpose |
|---|---|
VStack |
Vertical flexbox column. |
HStack |
Horizontal flexbox row. |
Grid |
CSS grid with explicit column/row sizing. |
Stack |
Z-axis stacking (use sparingly). |
Defer |
Render an async result without blocking the tree. |
Use .Grow(n) / .Width(...) / .Padding(...) etc. for sizing.
Async rendering
Most workspace data comes from endpoints. Don't await in Render() — return a Defer and let Tesserae stream in the result:
public dom.HTMLElement Render() =>
VStack().Children(
TextBlock("Top cases").SemiBold(),
Defer(async () =>
{
var cases = await Mosaik.API.Endpoints.CallAsync<CaseList>("top-cases");
return VStack().Children(cases.Items.Select(c => TextBlock(c.Summary)));
})
).Render();
Defer renders a placeholder while the task runs, then swaps in the result. If you want the placeholder to be a spinner, pass it explicitly: Defer(loader, async () => ...).
Reactive content
For values that change over time, use observables (see State management). Defer understands Observable<T> and re-renders on change.
var query = new Observable<string>("");
VStack().Children(
TextBox().Placeholder("Search...").Bind(query),
Defer(query, q => string.IsNullOrEmpty(q)
? TextBlock("Type to search.")
: Defer(async () =>
{
var hits = await Mosaik.API.Endpoints.CallAsync<Hit[]>("search", new { q });
return VStack().Children(hits.Select(h => TextBlock(h.Title)));
}))
);
Calling endpoints
Mosaik.API.Endpoints.CallAsync<T>(path, body?) is the typed wrapper that handles auth, serialization, and error mapping.
var result = await Mosaik.API.Endpoints.CallAsync<SimilarProductsResponse>(
"similar-products",
new { ProductKey = "P-12345", Limit = 3 });
For raw HTTP (rare), use REQ.New("api", "endpoints", "raw").PostJsonAsync(...).
Where to go next
- Curiosity components — pre-built search/graph widgets.
- Routing and navigation — register routes and hook into the sidebar.
- Node renderers — customize per-type node views.
- Tesserae components reference.