@syncropel/renderer
v0.2.0
Published
React renderer for Syncropel workspaces. Mount `core.workspace.v1` manifests in any React app — Next.js, Vite, Astro, Remix, Electron, Tauri, or your own.
Maintainers
Readme
@syncropel/renderer
React renderer for Syncropel workspaces. Mount
core.workspace.v1manifests in any React app — Next.js, Vite, Astro, Remix, Electron, Tauri, or your own.
Workspaces are portable: your app, your records, your renderer.
npm install @syncropel/renderer @tanstack/react-queryQuick start
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
RendererProvider,
WorkspaceRenderer,
WorkspaceBootstrap,
} from "@syncropel/renderer";
const queryClient = new QueryClient();
export function App() {
return (
<QueryClientProvider client={queryClient}>
<RendererProvider
fetcher={authedFetch}
apiBase="https://api.example.com"
catalogUrl="https://catalog.syncropel.com"
onAction={(action, record) => console.log("action:", action, record)}
>
<WorkspaceBootstrap />
<WorkspaceRenderer
did="did:sync:syncropel-team/dashboard"
fallback={<p>Workspace unavailable</p>}
/>
</RendererProvider>
</QueryClientProvider>
);
}<RendererProvider> is optional — every prop has a sensible default for browser hosts. Drop it entirely if you don't need authenticated requests, custom catalogs, or typed action handlers; the renderer falls back to plain fetch, window.location.origin, and a window.CustomEvent action dispatch.
That's it. The renderer:
- Resolves
did:sync:syncropel-team/dashboardagainst the public catalog (https://catalog.syncropel.com). - Falls back to your local Syncropel daemon if the catalog misses.
- Validates the manifest.
- Mounts each declared component, fetching records via the registered projection (
table,feed, ormetric). - Renders your
fallbackif the manifest can't be resolved.
What this package is
A Syncropel workspace is a core.workspace.v1 record that declares an interactive UI — pages, views, projection names, action labels — over your Syncropel records. This package mounts that record as React.
Every workspace — published by Syncropel or by anyone else — is the same kind of artifact, rendered the same way. One renderer; mountable anywhere a React tree exists.
What it isn't
- Not the data client. Use
@syncropel/sdkto emit and query records. - Not the design system. Use
@syncropel/reactfor tokens, atoms, and molecules. This package consumes CSS variables but ships no styling decisions of its own. - Not the schema authority.
@syncropel/configships the canonical JSON Schemas. This package re-uses the renderer-relevant types. - Not the iframe extension SDK. Use
@syncropel/extensionsfor sandbox-isolated extensions. This package mounts in-process React components.
Concepts in 60 seconds
- Workspace — a
core.workspace.v1record withname,publisher_did,lifecycle, andcomponents[]. Identified by a DID of the formdid:sync:<publisher>/<slug>. - Component — one entry in
components[]. Has anid, akind(viewis fully supported in this release), and projection wiring. - Projection — a registered React component that takes records, options, and className, and renders UI. Three ship out of the box:
table,feed,metric. Hosts can register more viaProjectionRegistry.register(...). - Manifest fetcher — resolves a DID by querying the catalog (
POST /v1/records/query) and falling back to the local daemon. Cached in an in-memory LRU (32 entries, 5-minute TTL). - Action — a row-level button declared in a manifest's
projection_options. Clicks dispatch a typedWorkspaceActionEventDetailviawindow.dispatchEvent. (A future release adds a typed React context as the canonical mechanism.)
API tour
<WorkspaceRenderer>
<WorkspaceRenderer
did="did:sync:syncropel-team/dashboard"
fallback={<DashboardFallback />}
catalogUrl="https://catalog.example.com" // optional override
hideHeader // hide the workspace metadata header
manifest={preResolvedManifest} // skip the catalog hop entirely
recordsByComponent={{ "live-feed": [...] }} // skip the daemon hop too (test mode)
/>useWorkspaceManifest(did, options)
import { useWorkspaceManifest } from "@syncropel/renderer/hooks";
function Custom() {
const { manifest, isLoading, isError, error } = useWorkspaceManifest(
"did:sync:alice/timetracker-pro",
);
// ...render manually
}Registering a custom projection
import { ProjectionRegistry, type Projection } from "@syncropel/renderer";
const VibeGraph: Projection = {
name: "cmv-vibe-graph",
label: "Vibe graph",
Component: ({ records, options, className }) => {
/* your D3 / xyflow / canvas surface */
},
};
ProjectionRegistry.register(VibeGraph);A workspace manifest can now reference cmv-vibe-graph in projection_options.default and the renderer will mount it instead of one of the built-in projections.
Listening for workspace actions
The recommended path: pass an onAction callback to <RendererProvider>.
<RendererProvider
onAction={(action, record) => {
if (action === "edit") openEditModal(record);
if (action === "delete") confirmDelete(record);
}}
>
{/* ... */}
</RendererProvider>The legacy window.CustomEvent surface still works for back-compat:
import { WORKSPACE_ACTION_EVENT, type WorkspaceActionEventDetail } from "@syncropel/renderer";
useEffect(() => {
function onAction(ev: Event) {
const { action, record } = (ev as CustomEvent<WorkspaceActionEventDetail>).detail;
if (action === "edit") openEditModal(record);
if (action === "delete") confirmDelete(record);
}
window.addEventListener(WORKSPACE_ACTION_EVENT, onAction);
return () => window.removeEventListener(WORKSPACE_ACTION_EVENT, onAction);
}, []);Both surfaces fire on every action — pick the one that fits your host. Inside a custom projection you've registered, use useWorkspaceActions() to emit:
import { useWorkspaceActions } from "@syncropel/renderer";
function MyProjection({ records }) {
const { emit } = useWorkspaceActions();
return <button onClick={() => emit("custom-action", records[0])}>Do thing</button>;
}Sub-path exports
// Main entry — everything
import { WorkspaceRenderer, RendererProvider, ProjectionRegistry } from "@syncropel/renderer";
// Just the projections
import { TableProjection, FeedProjection } from "@syncropel/renderer/projections";
// Just the manifest types and utilities (no React)
import { parseWorkspaceDid, validateManifest, fetchManifest } from "@syncropel/renderer/manifest";
// Just the React hooks
import { useWorkspaceManifest } from "@syncropel/renderer/hooks";
// Just the context layer (provider + hooks)
import { RendererProvider, useRendererContext, useWorkspaceActions } from "@syncropel/renderer/context";Tree-shakers (esbuild, Rollup, Webpack 5) drop unused exports automatically.
Theming
The renderer is theme-agnostic. It uses CSS custom properties (var(--color-text-primary), var(--color-border-subtle), etc.) that the host defines. Two sane choices:
Option A — bring your own tokens. Define the variables on :root (or any ancestor of the renderer):
:root {
--color-text-primary: #111;
--color-text-secondary: #444;
--color-text-muted: #888;
--color-border-subtle: #ddd;
--color-bg-secondary: #fafafa;
--color-bg-hover: #f0f0f0;
--color-act-intend: #f97316;
--color-act-do: #38bdf8;
--color-act-know: #34d399;
--color-act-learn: #c084fc;
}Option B — use the canonical Syncropel design system. Install @syncropel/react and import its tokens:
import "@syncropel/react/tokens.css"; // or styles.css for tokens + atom CSSEither way, the renderer uses CSS variables and never bakes specific colors into its source. Customize at the variable level; no rebuild required.
Status: 0.2.0
The public API is stable and additive over 0.1.0. New in this release:
<RendererProvider>typed React context — the canonical mechanism for app-wide configuration. Inject your authenticatedfetch, daemon base URL, default catalog URL, error reporter, and action callback in one place. Optional — defaults work for standard browser hosts.useWorkspaceActions()hook — callemit(action, record)from inside a custom projection. Fires both the provider'sonActioncallback (when set) and the legacywindow.CustomEvent(always, for back-compat)./contextsub-path —import { RendererProvider, useRendererContext } from "@syncropel/renderer/context"for hosts that want only the context layer.
Carried over from 0.1.0:
- The manifest schema defines five component kinds; this release renders one.
viewis fully supported.page,thread_view,workspace, andexternalrender a placeholder pending the runtime extension layer. - Module-level host setters (
_setHostFetch,_setApiBaseGetter,_setErrorReporter) still work as a fallback configuration mechanism for hosts that haven't adopted the provider.
Migrating from 0.1.0 requires no code changes. The provider is purely additive; existing code keeps working unchanged. See CHANGELOG.md for the full roadmap.
Why this package
Workspaces should be portable in practice — mountable in your own app, not only on syncropel.com. That requires the renderer itself to be a public package. This is it.
Your workspace runs against your records, in your daemon, in your app. End-to-end ownership.
Related packages
| Package | Role |
|---|---|
| @syncropel/sdk | Record client (emit, query, search, infer) |
| @syncropel/projections | SRP v0.1 schema for declarative UI documents |
| @syncropel/react | Design tokens + 21 React atoms/molecules |
| @syncropel/extensions | Iframe extension SDK (SAP v0.2) |
| @syncropel/config | Body-kind JSON Schemas |
| @syncropel/workspace-templates | Starter workspace manifests |
License
Apache-2.0. Copyright Syncropic, Inc. See LICENSE.
