@peripheries/renderer
v0.25.0
Published
The one renderer for pages of federated component bundles: a framework-free core, a React renderer, the runtime a page boots with, and golden pages for testing.
Readme
@peripheries/renderer
The one renderer for pages built from federated component bundles. Hosts, component repos' harnesses and previews all render through it, so a page looks the same in each.
| Entry | Holds |
|---|---|
| @peripheries/renderer/core | Framework-free: cells and thin-ref hydration, references and conditions, the data context, walks and state names (layerName, the layer catalog LAYER_ROLES), cell props, layout, document structure, bundle refs and resolution, the shared store, loadPageData(page, source), validation, DIAGNOSTICS and PAGE_RULES |
| @peripheries/renderer/catalog | checkCatalog: what a component repo runs on the knowledge it emits. The only entry with a JSON Schema validator (ajv) |
| @peripheries/renderer/react | PageRenderer, CellView, StructureView, the cell states, useContainerSize, useFormsRuntime and its FormSlotProps and FormAnswer |
| @peripheries/renderer/runtime | What a page boots with: the import map and boot script, the shims, installRuntime, the store bundles import |
| @peripheries/renderer/theme | The org theme pipeline: parseTheme (version 1 or 2 in, version 2 out, with diagnostics), themeTokens (semantic overrides derived in OKLCH), themeCss (the theme layer), checkTheme (WCAG contrast) |
| @peripheries/renderer/print | The print pipeline, framework-free: collectBlocks (the page map), paginate, buildPrintLayout (the packer's decision as CSS), buildPageStylesheet (@page geometry and margin boxes), running headers, @printPolicy parsing, print lint, resolveDocumentProfile, region bindings, map staleness, and the layout.structure[].print writer |
| @peripheries/renderer/forms | The forms runtime, framework-free: the steps overlay and its model, conditions, modes, navigation, progress, deep links, and the persistence port (FormsPort), and the form-section contract's type (FormSection, FormContentBlock); the React hook is useFormsRuntime in /react |
| @peripheries/renderer/testing | The golden pages, synthetic fixtures, and sources that serve them |
| @peripheries/renderer/styles.css | Page and cell containers, the responsive buckets, placeholders and notices, in the renderer layer |
A consumer supplies:
- a
GraphSourcefor the page's reads (the host's runs GraphQL under the viewer's JWT; fixtures in/testing); - its bundles, as
PageBundles(pageBundles(scope, rows, loaded, orgId)); - a
RendererEnvironment: core components,navigate, the store, and where diagnostics go.
const runtime = await loadPageData(page, source, { params, baseProps, viewer, context })
<PageRenderer runtime={runtime} bundles={bundles} params={params} baseProps={baseProps} context={context} viewer={viewer} environment={environment} />What a consumer can hook in
RendererEnvironment takes, besides core, navigate and store:
actionsFor(cell): the actions a cell gets;projectCellActions(cell.actions, pageActions)applies the lookup rule (binding key, then registry key, then the cell's own name).runAction: runs a cell's declared action (the host's write path, or a harness's mock); without it a declared action reportsFAILED.loadPage: loads a paged label's next page.onPropSources(cell, sources): each rendered cell's props, leaf by leaf, with where each value came from (for an edit layer, witheditTarget).retry(): reads the page again, for a failed cell's "Try again".cellProps(cell): props from the page's runtime, merged last; on a form page,useFormsRuntime'scellPropsgives each cellform,answersandanswer.onCellError(cellId, error): a component threw. The cell shows an error notice either way.onUnresolvedReference(cellId, ref): a prop's@refresolved to nothing (not reported while its data is loading).frameProps(cell, contract): extra attributes and style on a cell's frame, for what the host doesn't apply yet (a print policy, say).onDiagnostic(d): a page configuration problem.showHiddenCells: where a page is being built or reviewed (an editor, a harness, a preview), a cell itshide_when_emptyleaves out shows as a small "hidden (empty)" placeholder instead of nothing.showProblemCodes: a cell's problem notice adds its diagnostic code, for staff and editors; without it a viewer reads only the plain wording.
A consumer with resolved values instead of a graph (the component repos'
harness) builds its runtime with pageRuntimeFromValues(page, { values,
traversal }), and wraps the page in whatever its head would give it (the
org's theme is CSS, not a provider). A declared store default seeds its channel, so every cell
sharing the channel reads it.
The names a bundle may rely on
What the renderer writes, for a bundle's CSS and checks. These are the only names: 0.8.0 dropped the old client-prefixed twins and container names.
| Where | Name |
|---|---|
| The page container | data-page-container, data-page-grid, data-page-id, data-profile; container page |
| Each cell | .cell (container cell), data-page-cell, data-cell-id, data-component |
| A cell's buckets | data-hide-when, data-show-when, data-stack-when, data-truncate-when |
| A structure section | data-section, data-section-level, data-section-kind, data-derived-ordinal |
| A missing component's notice | data-missing-component |
| A cell hide_when_empty left out, shown by showHiddenCells | .cell-hidden, data-cell-state="hidden-empty" |
The store specifier and runtime global are in @peripheries/runtime's README.
The theme reaches a page as CSS, in the theme layer; the renderer ignores a
bundle's ThemeProvider export.
Versions
0.25.0 (2026-10-24):
hide_when_empty, a cell-level list of prop names.- A cell with
hide_when_empty: ["title", "items"]renders nothing, and leaves no gap, when every prop it names is empty once bound: null, undefined, text that is blank once trimmed,[]or{}.0andfalseare content. A slot orchildrenfilled by child cells is content. Print follows the screen. The check runs before the bundles load, so a server render and the browser agree. - A refused, failed or unresolved read is not empty. A named prop that reads a label whose
read was refused or failed keeps the cell's error notice. One whose query never ran because
its own reference resolved to nothing (
QUERY_UNRESOLVED) shows the same notice instead of hiding (hideWhenEmptyProblem). A label still loading isn't decided yet (the cell renders). RendererEnvironment.showProblemCodes: a problem notice adds its diagnostic code (data-cell-code,.cell-notice__code) for staff and editors; a client reads only "This part of the page couldn't load." The host sets it in the platform environment and in a proposal preview.RendererEnvironment.showHiddenCells: an editor, a harness or a preview shows a hidden cell as a small "hidden (empty)" placeholder (CellHidden,.cell-hidden).- Validation:
validatePagereportsPROP_UNKNOWN(warning) atcontent.cells[i].hide_when_empty[j]for a name the component'sprops_schemadoesn't declare, andHIDE_WHEN_EMPTY_INVALID(error) for anything but a non-empty list of names. - A ref cell's node owns
hide_when_empty(CONTENT_FIELDS, viaRENDER_FIELDS)./coreexportshiddenWhenEmpty,hideWhenEmptyProblemandisEmptyPropValue.PAGE_RULESnames the key. - The renderer's
PageCelltype addshide_when_empty?: string[];@peripheries/runtime'sPageCellgains it at its next release.
- A cell with
0.24.0 (2026-10-20): one renderer batch (CX).
layerName(system)andLAYER_ROLESin/core: a system's state name (its layer's role, else a registered layer's own type,question,answer, …) from the layer catalog, which is generated frompublic.system_layer(npm run gen:layers; CI checks it is current). A host and BEA's preview resolve a page's state names alike. An unregistered type has no name.FormSectionandFormContentBlockin/forms: a form section cell'sprops.section, contract v1 (contracts/form-section.schema.json): the section node'sid, its authoredsectionId,title,description?,sectionType?, itsquestions({ key, label, required, definition }, the definition the form-definition contract's question or null) and itscontentblocks (the form-definition contract's content block). Static: visibility, answers and totals stay runtime (forms.*,FormAnswer, unchanged). v1 is the target shape: the component engine doesn't emitprops.sectionyet; the cutover is in the stack'scontracts/README.md.FormQuestionDefinition'soptions,validation,descriptionandhelpTextmay be null, as the form-definition contract's question has them.
0.23.1 (2026-10-15): the published files carry no comments and no source maps (declarations included); this README is the documentation. No API change.
0.23.0 (2026-10-15): the versions and forms release, in one minor.
- Main only:
prepareDocumentreads main systems and placements only, addingversion: { is: NULL }tosystemCollection,graph_nodeCollectionandgraph_edgeCollectionat any depth. New diagnostics:SYSTEM_NOT_MAIN(a warning: a page names a system that isn't main, read as absent;GraphSource.mainSystemssays which are),DESCRIPTOR_VERSION_FILTER(a document filters onversionitself) andDESCRIPTOR_STATE_UNSCOPED(a document reads states by node or unfiltered, so a branch's would be read too); both are refused before a document is sent. - The version context:
PageScope.version(VersionContext { versionId, versionKind }, from@peripheries/runtime) reaches everyGraphSourceread as an optional trailingReadContext, and@context.version/@context.versionKindname it to a query. At main the reads are 0.22's. The runtime freezes the read-side types (ResolvedVersionContext,StateAsOf,NodeStateView,Via,ReleaseProblem,RecordedEmbed). - Edits at a version: a release or a channel refuses an edit before
it is sent (
VERSION_IS_RELEASE); a branch's edit carriesp_version_id. - Embeds and print at a version:
PageScope.recordedEmbeds(a release'smetadata.embeds) makes an embed render the release its host recorded, else the viewer's context;PageRuntime.embedVersionsays which (recorded|context), andPageRuntime.readVersionIdsthe versions the states were read from, for print's and an embed's "Release N". A state row may carryversionId,unversionedandoutOfScope. - Render-time functions:
RENDER_FUNCTIONSadds the version reads (node_states_as_of,node_state_view,version_context_for,version_scope_members,versions_for,version_diff,auto_release_status), each with the tables it reads for invalidation. - Declared ahead:
form_answers(p_structure_nodes: [UUID!]!)is a render function now, listed inRENDER_FUNCTIONS_AHEADuntil the database has it, so descriptors can read a workbook's answers without a renderer release. Its JSON is a leaf:result.pathreads into it and nothing in it is flattened. An alias on a render function's JSON root is refused (DESCRIPTOR_ALIAS_JSON_ROOT), since its JSON is decoded by field name. - Systems from the graph:
GraphSource.graphSystems(processId)(the main, keyless systems on the process's graphs, with each graph's type and layer's role) is howresolveSystemsfinds a page's systems;processSystemsis optional and read only without it, andmetadata.pageProcessis no longer read. A state name is<tier>_<role>for any registered layer's role (eng_answer), not only the four; whether the page's process has it isSTATE_UNKNOWN_SYSTEMat load. - Releases:
validateManifest(pages)returnsReleaseProblem[], advisory before a release;manifestPageFrom(graphNodeId, data)reads a page artifact's state data as a manifest page. The runtime addsPageViewandSubmissionData. - The forms runtime (
@peripheries/renderer/forms): the steps overlay, modes, navigation, saving, progress and deep links behindFormsPort { save, submit, visit, reopen }(and, optional,uploadandopenFilefor file answers), withuseFormsRuntimeandFormSlotPropsin/react. Refusals are codes, never throws (FormsRefusal:CONFLICT,ANSWER_*,FORM_INCOMPLETE,FORM_NOT_OPEN,FORM_COMPLETED,FORM_STEP_UNKNOWN,NO_CAPABILITY,NETWORK).reopenis the "Edit answers" slot on a submitted form. A phone opens in question mode; the slot props carry the question shown (question, its place and the next),goToQuestion,progress.totalbeside the required count, a step's authoredlabel, and stay one object while what they show is unchanged. A step with only optional questions is complete once seen. The slot props name every chapter, the submission'sstatus,readOnly, and the newest staffreview(its note, status, time and writer). The runtime addsFileAnswerandAnswerValue;SubmissionDatacarriesreopensand each note'sstatus. - Form cells: on a form page each cell gets, through
RendererEnvironment.cellProps(useFormsRuntime'scellProps),form(the slot props),answers(its questions the conditions show, by key) and, with one question,answer: aFormAnswer { key, label, question, value, change, readOnly, required, missing, refusal, inputId, upload?, openFile? }, in the slot props' words.questionis the question's definition (FormQuestion.definition);inputIdis wheregoToQuestionmoves focus. In question modeFormSlotProps.questioncarries the sameanswer. - For consumers (nothing is required to keep 0.22's behaviour):
- a
GraphSourceaddsgraphSystems(processId)to find systems by graph (elseprocessSystemsis read),mainSystems(ids)to read main only (else every system is read), and takes the trailingReadContextto read at a version (else it reads main); - a host passes
PageScope.version(andrecordedEmbedsat a release) to read at a version, and refuses nothing else itself; - a form host implements
FormsPort(uploadandopenFileonly if it takes files) and passescellPropsfromuseFormsRuntimeasRendererEnvironment.cellProps;review.byis a display name or null, never an email or a user id; - a component reads
form,answersandanswerfrom its props on a form page, and putsanswer.inputIdon its control. - page ids (an embed's
page_id,PageScope's page) become anchor ids; the renderer treats them as opaque, so nothing changes.
- a
- Main only:
0.10.0: actions run, documents edit, pages embed.
- Declared actions run through the environment's
runAction; the runtime'sACTION_FIELDStakessubjectsandaudienceoncreate,editandappend, andcreateneedsedge_typeonly under a parent. - Cells get
renderComponent(key, props): another component by registry key, resolved in the cell's scope; an unknown key reportsCOMPONENT_UNKNOWN(throughonDiagnostic, now passed to cells) and returns null. - Embed cells:
PageCell.embed({ page_id }or{ page_type, project_id?, process_id? }) renders another page, loaded byloadPageDatathrough the optionalGraphSource.page(selector), one level deep (PageRuntime.embeds;EMBED_INVALID,EMBED_NOT_FOUND,EMBED_NESTED).embedis inHYDRATED_CELL_FIELDS. - A nullable query variable whose whole reference reads
@params,@contextor@viewerand resolves to nothing is sent asnull. - PAGE_RULES is shorter (3780 characters) and describes embed cells.
- Declared actions run through the environment's
0.9.0: queries, realtime's inputs, the host contract retired, and the renderer half of actions.
- A label can be
{ descriptor: QueryDescriptor }, or a node binding to a livequery_datanode, whose descriptor runs instead of returning its state.GraphSourcegainsquery(descriptor, variables), which returns the shaped result; the golden source answers fromqueries, by operation name. /coreexportsvalidateDescriptor,prepareDocument(the render-time rule, withRENDER_FUNCTIONS),resolveDescriptorVariables(the props resolver, for variables),selectionShapeandselectionIssues.loadPageData(page, source, { params, baseProps, viewer, context })runs queries in rounds: those whose variables read no label with the states and walks, then those that read a label, and live nodes' queries. A failed or unresolved query gives its own label an error (QUERY_FAILED,QUERY_UNRESOLVED); the rest of the page renders.@viewer.*and@context.*resolve in props (PageRenderertakesviewer).viewer,context,inputandscopeare reserved labels (LABEL_RESERVED).- New diagnostics:
DESCRIPTOR_*,REF_NOT_SELECTED,QUERY_READS_QUERY,QUERY_UNRESOLVED,QUERY_FAILED,LABEL_RESERVED. graphqlis a dependency.PageRuntime.graphNodeIds: every graph node a page reads, whose graphs are its Realtime topics.{ registry: "x.y" }query bindings are retired: an error (UNSUPPORTED_REGISTRY_BINDING) that aliases nothing.- Actions: a cell declares
{ action, … }with@inputfor what the component passes;RendererEnvironment.runActionruns it, a call never throws, andWriteResult.codesays why one failed (ACTION_INVALID; the registry form isACTION_REGISTRY, legacy).
- A label can be
0.8.1:
BasePropscarries the org'slocaleandcurrency(from its org template's metadata), which@stack/chartsformats with.0.8.0: the old names go. The renderer writes only the attributes and container names above, the runtime installs only
__STACK_RUNTIME__and maps only@stack/store, and the store has no second hook name. A bundle'sThemeProvideris no longer lifted or wrapped around the page (LoadedBundle.ThemeProviderandnearestThemeProviderare gone).@peripheries/runtime0.8.0 dropsLEGACY_RUNTIME_GLOBALandSPECIFIER_ALIASES.0.7.3: the page container also carries
data-page-grid, the canonical name of the old page-grid attribute, so every attribute the renderer writes has its canonical name (above) before the old ones go.0.7.2: the package ships its LICENSE: proprietary to Peripheries Studio Inc., all rights reserved.
0.7.1:
bundleScopeputs the advisor org'smainafter the shared lineages, so an advisor org's older copy of a core component never shadows core on a client org's page.0.7.0: shared lineages and the theme.
bundleScopeappends the platform's shared lineages, and the loader imports them by specifier;@core/resolves fromstack/core(host:is retired);importMapForandimportMapJsonput shared lineages in the import map. The new/themeentry derives an org's theme into semantic tokens and checks its contrast.styles.cssis in therendererlayer and reads the theme's body font and ink. A document page's container carriesdata-profilefromlayout.profile.@peripheries/runtimeaddsSEMANTIC_TOKENS,LAYER_ORDER, the version 2 theme type, andcsson a shared import.0.6.2: a malformed projection is reported once.
validatePagePropsjudges only a well-formed projection'smap; before, a malformed one's map was judged too, so one mistake gaveEACH_INVALIDand two or three map findings.eachFormProblems(from/core) is the shared judgement.0.6.1:
validatePagePropsjudges an@eachprojection by itsmap: its keys against the prop'sitemsschema (a required key missing isPROP_INVALID, an undeclared onePROP_UNKNOWN), and its literal values against theirs, nested projections too. A malformed projection is no longer also reported asPROP_INVALID;validatePage'sEACH_INVALIDsays it once.0.6.0:
@eachprojections. A prop value{ "@each": "@label.path", "as": "item", "map": … }resolves to the list withmapresolved once per element (@item.…reads it), so a list in its own shape fills a prop that wants another without the data or the component changing.EACH_INVALIDchecks the form;PAGE_RULESteaches it;referencedLabelsdoesn't count the element label as the page's.0.5.0: one judgement of a page at patch and publish.
CELL_UNKNOWN_COMPONENTcarriessuggestions, the nearest names in scope (nearestNames, also exported).PROP_UNKNOWNwarns of a prop the schema doesn't declare./catalogaddsvalidatePageProps, which reportsPROP_INVALIDfor a literal value that fails its prop's schema; run it besidevalidatePage.checkCatalog's pack estimate counts only the cards of linked components, asknowledge_packnow returns them.0.4.1: a card no longer carries a snippet; a component's first snippet is its
example(budget 1,000), shown beside the card in a pattern's pack. The card budget drops to 400.0.4.0:
DIAGNOSTICSandPAGE_RULES;/catalogwithcheckCatalog; the knowledge types in@peripheries/runtime.0.3.0:
@peripheries/runtime/rank, sibling order ongraph_node.rank(compareRank,sortByRank,rankBetweenand the rest), so no one vendors it; theWALK_NODE_TYPES_EMPTYwarning, becausenodeTypes: []filters nothing (it doesn't mean "enter nothing").0.2.0: the hooks above,
pageRuntimeFromValues, action projection,data-missing-componenton a missing component's notice, the theme passed toThemeProvider, store defaults seeded.0.1.0: the first release.
Validation
validatePage(page, catalog?) and validateCell(cell, catalog?) return
diagnostics, { severity, code, message, path?, cellId?, label? }, with
severity one of error, warning or legacy (a form still read, on its way
out). loadPageData runs validatePage and adds what loading finds. Given a
catalog ({ components: { [name]: { props_schema } } }) it also checks that
components exist and required props are given (schema.required only). For a
node-managed cell those are warnings, since the page's copy is only a fallback.
Every code is in DIAGNOSTICS (from /core), with what it means and how
to fix it; Diagnostic.code is typed from that table. Tools show the fix with
the diagnostic, so a writer learns a rule when they break it.
PAGE_RULES (from /core) is the host's rules for composing a page, the
short text a writer reads first. It is built from the
constants the renderer and validator use, so it changes when they do.
core imports no React at runtime and never reaches the network, and the
package never imports the host (npm run lint:deps). Tests sit next to the
code; the golden pages run over fixture sources in react/golden-pages.test.tsx.
Publishing: npm run publish:packages (and --dry-run) builds and
publishes every package whose version isn't on npm yet, built output only;
.github/workflows/publish-packages.yml stages each new version from
develop, and a maintainer approves it on npmjs.com. Bump the version here to
release.
