@parametron/ui is the reference UI for Parametron models, built on Domphy (@domphy/core + @domphy/theme + @domphy/ui). It ships as plain TypeScript source (main: src/index.ts, no build step) and is organized as one shared chrome — router, app state, brand theme, the host bridge contract, shared blocks — plus panel clusters merged into folders one repo-split ago: builder/, configurator/, catalog/, font/, material/, i18n/. This page covers the chrome. Panels are documented on their own pages, linked at the bottom.
Nothing here is required to use Parametron — it is one reference implementation of the contracts described in /parametric/rendering and /parametric/reference. Swap it for your own UI on any framework by consuming the same ModelJSON / node API surface.
Host-pure doctrine
Every panel is host-pure: it receives data through a host interface — BuilderHost, CatalogHost, FontPickerHost, MaterialEditorHost / MaterialsPanelHost — passed in by the caller. A panel never calls fetch, never touches localStorage, and never reads global app/router state directly. Persistence, viewport highlighting, and catalog data are all callbacks or injected objects, not window globals:
// types.ts — BuilderHost is the contract, not an implementation
export interface BuilderHost {
placeBlock(modelId: string): Promise<PlacedBlock>
updateBlock(placedId: string, params: Record<string, unknown>): Promise<void>
importEntities?: (file: File) => Promise<{ entities: unknown[]; materials?: { name: string }[] }>
viewport?: ViewportBridge
translations?: Record<string, string>
}This is what lets the same panel code run inside a browser web app (host backed by Three.js + a REST API) and inside an offline SketchUp HtmlDialog (host backed by a Ruby message bridge) with zero branching in the panel itself. The pattern repeats for every cluster — see the per-panel pages for each cluster's specific host interface.
Router
createRouter (router.ts) is a minimal hash-based SPA router. Routes live in location.hash rather than pathname so the same bundle works loaded over file:// (the offline .rbz fallback has no server to resolve a path against). Each host supplies a RouterConfig and the router mounts into the #app element:
import { createRouter } from "@parametron/ui"
const router = createRouter({
resolve: (path) => (path === "/catalog" ? CatalogView() : NotFoundView()),
title: (path) => `Parashape — ${path}`,
})
router.start()Router exposes navigate(path), current(), start(), and refresh() — refresh re-resolves and re-renders the current path without changing it, used when external state (e.g. auth resolving) flips what the current path should render.
App state
app (states.ts) is a singleton AppState (extends Notifier from @parametron/parametric) holding local-only UI state — it is not a model store. Models are injected from the host app, never fetched by the package itself:
class AppState extends Notifier {
activeScope?: ModelScope
activeModel?: any
loadedModels: Map<string, ModelScope>
setModels(metas: ModelMetaJSON[]): void
getMetasSync(listener?: Listener): ModelMetaJSON[]
getMetaSync(id: string, listener?: Listener): ModelMeta | undefined
setActive(modelScope: ModelScope): void
getActive(listener?: Listener): ModelScope | undefined
}A host app hard-codes or fetches its own model list and calls app.setModels(metas); panels read it back reactively via app.getMetasSync(listener) / app.getMetaSync(id, listener). Passing a Listener subscribes the caller to future setModels/setActive calls — the same reactivity pattern used throughout @parametron/parametric's Notifier-based classes.
ModelMeta
ModelMeta (ModelMeta.ts) wraps the metadata record for a model — id, name, origin?, description, categories, tags, dependencies, status ("draft" | "published" | "archived" | "private") — as a reactive Notifier subclass:
ModelMeta.fromJSON(json: ModelMetaJSON): ModelMeta
meta.get(key, listener) // reactive read, key restricted to name/description/categories/tags/status
meta.set(key, value) // reactive write, same restricted key set
meta.clone(): ModelMeta // copy with a fresh id (arrays are shared refs, not deep-cloned)
meta.toJSON(): ModelMetaJSONget/set only allow the five display-mutable fields — id, origin, user, and dependencies are set once at construction and never edited through this API.
Brand theme
brandTheme.ts applies the 3dshouse/ParaShape brand accent to Domphy's theme in one call, so every surface (buttons, links, docs, catalog) shares it:
import { applyBrandTheme } from "@parametron/ui"
applyBrandTheme() // setBrandTheme() + @domphy/theme's themeApply(), in one callsetBrandTheme() alone (no themeApply()) is for SSR/prerender paths that call @domphy/theme's themeCSS() themselves. PARASHAPE_LOGO_SVG is the inlined brand mark (isometric box, navy/gold) used wherever a real logo is needed instead of a generic icon — it has explicit fills, not currentColor, so it renders the same regardless of theme.
Host bridge contract (types.ts)
Beyond BuilderHost (above), types.ts defines the preview protocol — how a panel describes hover/highlight feedback as data instead of draw calls, so every host renders it through its own canvas consistently:
type PreviewItem =
| { kind: "point"; position: PreviewPoint }
| { kind: "points"; positions: PreviewPoint[]; color?: string }
| { kind: "segments"; segments: [PreviewPoint, PreviewPoint][]; color?: string }
| { kind: "vector"; direction: PreviewPoint; anchorKey?: string }
| { kind: "axis"; origin: PreviewPoint; direction: PreviewPoint }
| { kind: "plane"; origin: PreviewPoint; normal: PreviewPoint }
| { kind: "entities"; entities: EntityJSON[]; color?: string } // escape hatch: real engine entities as a transient ghostViewportBridge ({ preview(preview: PreviewJSON): void; clearPreview(): void }) is how a host receives these payloads — injected via BuilderHost.viewport, threaded down explicitly to the blocks that need it rather than read off a window global. A host with no canvas overlay just leaves viewport undefined and the panels no-op. PlacedBlock ({ id, modelId, name, params,
paramTypes, nodes: NodeCollection<ChildNode> }) is the per-instance record hosts carry for every placed model, passed back opaquely through BuilderHost calls — but BuilderPanel itself reads into it directly: .nodes to render and evaluate the parameter/operation sections, .name/.id/.modelId for display, list keys, and re-placement.
Shared blocks
blocks/ holds small building blocks reused across panel clusters rather than a full component:
railIcon(content, label, active, onClick)— one full-width icon button styled for the brand-navy left rail.railFlyout(build)/railFlyoutItem(iconElement, fly)— a hover-triggered flyout menu (title + link rows) anchored to a rail icon, pure CSS:hover, no JS state:import { railFlyout, railFlyoutItem, railIcon } from "@parametron/ui" const accountFly = railFlyout((l) => ({ title: "Account", links: [{ text: "Sign out", onClick: () => signOut() }], })) const item = railFlyoutItem(railIcon(avatarIcon, "Account", false, () => {}), accountFly)typeColors/ThemeColorFamily— re-exported from@parametron/parametric(the parameter-type → theme-color-family mapping is defined once in the engine's type registry; the UI resolves it through Domphy'sthemeColor()).
i18n
The barrel also re-exports ./i18n/index.js in full: a package-scoped @domphy/i18n instance (initI18n, setLocale, getLocale, t, …) holding panel/chrome strings only — app-level and auth strings live with their own packages. A host app composes this instance alongside its own. Full catalog reference: /ui/i18n.
Panels
Each cluster is a folder under src/, re-exported from the barrel (export * from "./builder/index.js", etc.) and documented on its own page:
| Folder | Page | Contents |
|---|---|---|
builder/ | /ui/builder | BuilderPanel — the model-editing form (params, operations, node icons) |
configurator/ | /ui/configurator | ConfiguratorPanel — end-user parameter form for a placed model |
catalog/ | /ui/catalog | CatalogPanel, card blocks, CatalogHost/CatalogMaterial/CatalogTerm/CatalogFavorites |
font/ | /ui/font | FontPicker, webfont catalog loading, FontPickerHost/FontRecord |
material/ | /ui/material | MaterialEditor, MaterialsPanel, texture helpers |
i18n/ | /ui/i18n | The string catalog itself (keys, locales) |
The reference renderer that these panels' viewport/host callbacks typically drive is @parametron/three — see /three/viewport for the SceneHighlight consumer of PreviewJSON.