Projections
Schema, validators, and a markdown-subset parser for the Syncropel Rendering Protocol (SRP) — the declarative document format for query-driven and AI-generated UI.
What this is
@syncropel/projections is a types-only TypeScript package that ships the canonical schema for SRP v0.1 — a constrained JSON document format for rendering user interfaces declaratively. Think of an SRP document as a portable, validated blueprint for a UI view: a query result, a dashboard, a form, a report.
It pairs with:
@syncropel/sdk— emit SRP documents as record bodies@syncropel/react— render SRP documents into live React components
On its own, this package is useful anywhere you need to validate an SRP document, parse inline markdown in a text node, or share SRP types across services without pulling in a UI framework.
Install
npm install @syncropel/projectionsZero dependencies. Works in every JavaScript runtime.
Document shape
An SRP document has three top-level fields:
interface SRPDocument {
srp: "0.1"; // version tag
meta: { // optional metadata
name?: string;
description?: string;
};
root: SRPNode; // the root node (usually a Column or Grid)
}A SRPNode is a discriminated union of 19 node types:
Layout: column, row, grid, card, divider
Content: heading, text, stat, keyValue, chip, code
Records: recordLine, recordLineList
Interactive: button, iconButton, copyButton, select
State: emptyState, errorState, skeletonEach node has a type discriminator and a props object with type-checked fields. Example:
const doc: SRPDocument = {
srp: "0.1",
meta: { name: "Dashboard" },
root: {
type: "column",
props: { gap: "md" },
children: [
{ type: "heading", props: { level: 2, text: "Summary" } },
{ type: "stat", props: { label: "Records", value: 42, delta: 3 } },
{ type: "chip", props: { label: "live", tone: "info" } },
],
},
};Validation
import { validateSRP, isSRPDocument } from "@syncropel/projections";
const result = validateSRP(doc);
if (result.valid) {
// Safe to render
} else {
result.errors.forEach((e) => {
console.error(`${e.path}: ${e.message}`);
});
}
// Type guard
if (isSRPDocument(unknown)) {
// unknown is now narrowed to SRPDocument
}validateSRP walks every node, checks required fields, and reports structured errors with path, code, and message. Use it on any input that claims to be SRP — records you received from a daemon, documents an AI generated, user-submitted JSON.
Per-node validation is also exposed:
import { validateNode, isSRPNode } from "@syncropel/projections";
validateNode(someNode, "root.children[0]");Markdown-subset parser
Every text node and recordLine supports a constrained inline-markdown subset: bold, italic, code, links, strikethrough. The package ships a parser for this subset so renderers can turn strings into structured AST:
import { parseInline, stripFormatting } from "@syncropel/projections";
const ast = parseInline("This is **bold** and `code`, see [the docs](https://docs.syncropel.com).");
// → [{ kind: "text", text: "This is " },
// { kind: "bold", children: [{ kind: "text", text: "bold" }] },
// { kind: "text", text: " and " },
// { kind: "code", text: "code" },
// ...]
stripFormatting(ast);
// → "This is bold and code, see the docs."Node shape:
type InlineNode =
| { kind: "text"; text: string }
| { kind: "bold"; children: InlineNode[] }
| { kind: "italic"; children: InlineNode[] }
| { kind: "code"; text: string }
| { kind: "link"; url: string; children: InlineNode[] }
| { kind: "strike"; children: InlineNode[] };Block-level markdown (headings, lists, tables) is not in the subset — use dedicated SRP nodes (heading, column with item children, grid) for those.
Exported types
Every SRP node type is exported as an interface for typed code:
import type {
SRPDocument, SRPNode, SRPMeta,
ColumnNode, RowNode, GridNode, CardNode, DividerNode,
HeadingNode, TextNode, StatNode, KeyValueNode,
ChipNode, CodeNode,
RecordLineNode, RecordLineListNode,
ButtonNode, IconButtonNode, CopyButtonNode, SelectNode,
EmptyStateNode, ErrorStateNode, SkeletonNode,
} from "@syncropel/projections";Design tokens (gap scale, column counts, tones, typography sizes, etc.) are exported as string literal unions:
import type {
Gap, Padding, DividerSpacing, Cols,
ColumnAlign, RowAlign, Justify,
ChipTone, TextSize, TextWeight, TextTone,
ButtonVariant, ButtonSize, SkeletonShape, GlyphKind,
} from "@syncropel/projections";These are the same tokens consumed by @syncropel/react — your type-checked props in @syncropel/projections automatically match what the React components accept.
Using alongside the record SDK
Projections are most useful when they travel as record bodies — e.g., a daemon emits a KNOW record whose body is a projection document, and the UI queries that thread and renders the documents.
import { Client, Identity } from "@syncropel/sdk";
import { validateSRP } from "@syncropel/projections";
const client = new Client({
endpoint: "http://localhost:9100",
identity: Identity.static("did:example:dashboard"),
});
const doc = {
srp: "0.1",
meta: { name: "Session recap" },
root: { type: "column", props: { gap: "md" }, children: [/* ... */] },
};
if (validateSRP(doc).valid) {
await client.emit({
act: "KNOW",
kind: "ui.view.summary",
body: doc,
thread: "th_dashboards",
});
}Downstream readers do the symmetric query + validate + render.
What's next
- TypeScript SDK — emit and query records containing projection documents
- React components — drop-in components that render SRP documents
- Extensions SDK — embed UIs that produce + consume projections inside a Syncropel host