Parametron

@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(): ModelMetaJSON

get/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 call

setBrandTheme() 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 ghost

ViewportBridge ({ 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's themeColor()).

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:

FolderPageContents
builder//ui/builderBuilderPanel — the model-editing form (params, operations, node icons)
configurator//ui/configuratorConfiguratorPanel — end-user parameter form for a placed model
catalog//ui/catalogCatalogPanel, card blocks, CatalogHost/CatalogMaterial/CatalogTerm/CatalogFavorites
font//ui/fontFontPicker, webfont catalog loading, FontPickerHost/FontRecord
material//ui/materialMaterialEditor, MaterialsPanel, texture helpers
i18n//ui/i18nThe 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.

📖 5 min read