@peripheries/renderer 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,11 @@ const runtime = await loadPageData(page, source, { params, baseProps, viewer, co
51
51
  - `frameProps(cell, contract)`: extra attributes and style on a cell's frame,
52
52
  for what the host doesn't apply yet (a print policy, say).
53
53
  - `onDiagnostic(d)`: a page configuration problem.
54
+ - `showHiddenCells`: where a page is being built or reviewed (an editor, a
55
+ harness, a preview), a cell its `hide_when_empty` leaves out shows as a
56
+ small "hidden (empty)" placeholder instead of nothing.
57
+ - `showProblemCodes`: a cell's problem notice adds its diagnostic code, for
58
+ staff and editors; without it a viewer reads only the plain wording.
54
59
 
55
60
  A consumer with resolved values instead of a graph (the component repos'
56
61
  harness) builds its runtime with `pageRuntimeFromValues(page, { values,
@@ -70,6 +75,7 @@ names: 0.8.0 dropped the old client-prefixed twins and container names.
70
75
  | A cell's buckets | `data-hide-when`, `data-show-when`, `data-stack-when`, `data-truncate-when` |
71
76
  | A structure section | `data-section`, `data-section-level`, `data-section-kind`, `data-derived-ordinal` |
72
77
  | A missing component's notice | `data-missing-component` |
78
+ | A cell `hide_when_empty` left out, shown by `showHiddenCells` | `.cell-hidden`, `data-cell-state="hidden-empty"` |
73
79
 
74
80
  The store specifier and runtime global are in `@peripheries/runtime`'s README.
75
81
  The theme reaches a page as CSS, in the `theme` layer; the renderer ignores a
@@ -77,6 +83,30 @@ bundle's `ThemeProvider` export.
77
83
 
78
84
  ## Versions
79
85
 
86
+ - **0.25.0** (2026-10-24): `hide_when_empty`, a cell-level list of prop names.
87
+ - **A cell with `hide_when_empty: ["title", "items"]`** renders nothing, and leaves no gap,
88
+ when every prop it names is empty once bound: null, undefined, text that is blank once
89
+ trimmed, `[]` or `{}`. `0` and `false` are content. A slot or `children` filled by child
90
+ cells is content. Print follows the screen. The check runs before the bundles load, so a
91
+ server render and the browser agree.
92
+ - **A refused, failed or unresolved read is not empty.** A named prop that reads a label whose
93
+ read was refused or failed keeps the cell's error notice. One whose query never ran because
94
+ its own reference resolved to nothing (`QUERY_UNRESOLVED`) shows the same notice instead of
95
+ hiding (`hideWhenEmptyProblem`). A label still loading isn't decided yet (the cell renders).
96
+ - **`RendererEnvironment.showProblemCodes`**: a problem notice adds its diagnostic code
97
+ (`data-cell-code`, `.cell-notice__code`) for staff and editors; a client reads only "This
98
+ part of the page couldn't load." The host sets it in the platform environment and in a
99
+ proposal preview.
100
+ - **`RendererEnvironment.showHiddenCells`**: an editor, a harness or a preview shows a hidden
101
+ cell as a small "hidden (empty)" placeholder (`CellHidden`, `.cell-hidden`).
102
+ - **Validation:** `validatePage` reports `PROP_UNKNOWN` (warning) at
103
+ `content.cells[i].hide_when_empty[j]` for a name the component's `props_schema` doesn't
104
+ declare, and `HIDE_WHEN_EMPTY_INVALID` (error) for anything but a non-empty list of names.
105
+ - A ref cell's node owns `hide_when_empty` (`CONTENT_FIELDS`, via `RENDER_FIELDS`). `/core`
106
+ exports `hiddenWhenEmpty`, `hideWhenEmptyProblem` and `isEmptyPropValue`. `PAGE_RULES` names the key.
107
+ - The renderer's `PageCell` type adds `hide_when_empty?: string[]`; `@peripheries/runtime`'s
108
+ `PageCell` gains it at its next release.
109
+
80
110
  - **0.24.0** (2026-10-20): one renderer batch (CX).
81
111
  - **`layerName(system)` and `LAYER_ROLES`** in `/core`: a system's state name (its layer's role,
82
112
  else a registered layer's own type, `question`, `answer`, …) from the layer catalog, which is
package/dist/catalog.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { x as isEach } from "./chunks/query-D6xQbTFK.js";
2
- import { i as eachFormProblems, s as validatePage } from "./chunks/validate-BcB_PA1k.js";
2
+ import { i as eachFormProblems, s as validatePage } from "./chunks/validate-CrHGlwey.js";
3
3
  import { KNOWLEDGE_BUDGETS, KNOWLEDGE_KEY } from "@peripheries/runtime";
4
4
  import Ajv2020 from "ajv/dist/2020.js";
5
5
  var ref = (kind, key) => `${kind}:${key}`;
@@ -16,7 +16,12 @@ var STORE_FIELDS = [
16
16
  "viewState",
17
17
  "live"
18
18
  ];
19
- var CONTENT_FIELDS = [...HYDRATED_CELL_FIELDS.filter((f) => f !== "layout"), ...STORE_FIELDS];
19
+ var RENDER_FIELDS = ["hide_when_empty"];
20
+ var CONTENT_FIELDS = [
21
+ ...HYDRATED_CELL_FIELDS.filter((f) => f !== "layout"),
22
+ ...STORE_FIELDS,
23
+ ...RENDER_FIELDS
24
+ ];
20
25
  function normalizeCell(cell, path = "cell") {
21
26
  const diagnostics = [];
22
27
  if (cell.ref) return {
@@ -114,6 +119,39 @@ function failedLabels(cell, data) {
114
119
  return Boolean(entry?.error) && !entry?.unresolved?.length && !entry?.isLoading && (entry?.data === void 0 || entry?.data === null);
115
120
  });
116
121
  }
122
+ function isEmptyPropValue(value) {
123
+ if (value === null || value === void 0) return true;
124
+ if (typeof value === "string") return value.trim() === "";
125
+ if (Array.isArray(value)) return value.length === 0;
126
+ if (typeof value === "object") {
127
+ const proto = Object.getPrototypeOf(value);
128
+ return (proto === Object.prototype || proto === null) && Object.keys(value).length === 0;
129
+ }
130
+ return false;
131
+ }
132
+ var propLabels = (value) => [...JSON.stringify(value ?? null).matchAll(LABEL_REF)].map((m) => m[1]);
133
+ function hideWhenEmptyProblem(cell, data) {
134
+ const names = cell.hide_when_empty;
135
+ if (!Array.isArray(names)) return null;
136
+ for (const name of names) {
137
+ if (typeof name !== "string") continue;
138
+ for (const label of propLabels(cell.props?.[name])) if (data[label]?.unresolved?.length && !data[label]?.isLoading) return "QUERY_UNRESOLVED";
139
+ }
140
+ return null;
141
+ }
142
+ function hiddenWhenEmpty(cell, props, data) {
143
+ const names = cell.hide_when_empty;
144
+ if (!Array.isArray(names) || names.length === 0) return false;
145
+ for (const name of names) {
146
+ if (typeof name !== "string") return false;
147
+ for (const label of propLabels(cell.props?.[name])) {
148
+ const entry = data[label];
149
+ if (entry?.isLoading || entry?.error) return false;
150
+ }
151
+ if (!isEmptyPropValue(props[name])) return false;
152
+ }
153
+ return true;
154
+ }
117
155
  function assembleCellProps(i) {
118
156
  const { style: _fromConfig, ...resolved } = i.resolvedProps;
119
157
  const props = { ...resolved };
@@ -304,4 +342,4 @@ function collectScopeIds(ctx, selections, viewState) {
304
342
  }
305
343
  return [...ids];
306
344
  }
307
- export { normalizeCell as C, hasRef as S, rootCells as T, failedLabels as _, projectSelections as a, HYDRATED_CELL_FIELDS as b, pageScopeKey as c, resolveComponent as d, cellAttributes as f, assembleCellProps as g, pageContainerStyle as h, projectLive as i, projectScopeKey as l, documentProfile as m, attachViewStateSetters as n, projectViewState as o, cellPlacementStyle as p, collectScopeIds as r, shallowEqualBundle as s, attachSelectionSetters as t, resolveScopeId as u, projectCellData as v, resolveCell as w, STORE_FIELDS as x, CONTENT_FIELDS as y };
345
+ export { HYDRATED_CELL_FIELDS as C, normalizeCell as D, hasRef as E, resolveCell as O, CONTENT_FIELDS as S, STORE_FIELDS as T, failedLabels as _, projectSelections as a, isEmptyPropValue as b, pageScopeKey as c, resolveComponent as d, cellAttributes as f, assembleCellProps as g, pageContainerStyle as h, projectLive as i, rootCells as k, projectScopeKey as l, documentProfile as m, attachViewStateSetters as n, projectViewState as o, cellPlacementStyle as p, collectScopeIds as r, shallowEqualBundle as s, attachSelectionSetters as t, resolveScopeId as u, hiddenWhenEmpty as v, RENDER_FIELDS as w, projectCellData as x, hideWhenEmptyProblem as y };
@@ -641,6 +641,16 @@ function validateCell(cell, catalog, path = "cell", ids, options = {}) {
641
641
  message: `${cell.component} requires ${prop}`
642
642
  });
643
643
  }
644
+ const listed = schema?.properties;
645
+ if (listed && Array.isArray(cell.hide_when_empty)) cell.hide_when_empty.forEach((prop, i) => {
646
+ if (typeof prop === "string" && prop !== "children" && !(prop in listed)) push({
647
+ severity: "warning",
648
+ code: "PROP_UNKNOWN",
649
+ cellId,
650
+ path: `${path}.hide_when_empty[${i}]`,
651
+ message: `hide_when_empty names ${prop}, which ${cell.component} doesn’t declare`
652
+ });
653
+ });
644
654
  const declared = schema?.properties;
645
655
  if (declared && schema?.additionalProperties !== true && typeof schema?.additionalProperties !== "object") {
646
656
  for (const prop of Object.keys(cell.props ?? {})) if (prop !== "children" && prop !== "style" && !(prop in declared)) push({
@@ -674,6 +684,14 @@ function validateCell(cell, catalog, path = "cell", ids, options = {}) {
674
684
  path: `${path}.children`,
675
685
  message: `child cell "${child}" is not on the page`
676
686
  });
687
+ const hide = cell.hide_when_empty;
688
+ if (hide !== void 0 && (!Array.isArray(hide) || hide.length === 0 || hide.some((p) => typeof p !== "string" || !p))) push({
689
+ severity: "error",
690
+ code: "HIDE_WHEN_EMPTY_INVALID",
691
+ cellId,
692
+ path: `${path}.hide_when_empty`,
693
+ message: "hide_when_empty takes a non-empty list of prop names"
694
+ });
677
695
  checkBindings(cell.data?.queries, `${path}.data.queries`, push, cellId, options.template);
678
696
  checkActions(cell, path, push);
679
697
  checkProjections(cell.props, `${path}.props`, push, cellId);
package/dist/core.js CHANGED
@@ -1,6 +1,6 @@
1
- import { C as normalizeCell, S as hasRef, T as rootCells, a as projectSelections, b as HYDRATED_CELL_FIELDS, c as pageScopeKey, d as resolveComponent, f as cellAttributes, g as assembleCellProps, h as pageContainerStyle, i as projectLive, l as projectScopeKey, n as attachViewStateSetters, o as projectViewState, p as cellPlacementStyle, r as collectScopeIds, s as shallowEqualBundle, t as attachSelectionSetters, u as resolveScopeId, v as projectCellData, w as resolveCell, x as STORE_FIELDS, y as CONTENT_FIELDS } from "./chunks/projection-zhu32_KJ.js";
1
+ import { C as HYDRATED_CELL_FIELDS, D as normalizeCell, E as hasRef, O as resolveCell, S as CONTENT_FIELDS, T as STORE_FIELDS, a as projectSelections, b as isEmptyPropValue, c as pageScopeKey, d as resolveComponent, f as cellAttributes, g as assembleCellProps, h as pageContainerStyle, i as projectLive, k as rootCells, l as projectScopeKey, n as attachViewStateSetters, o as projectViewState, p as cellPlacementStyle, r as collectScopeIds, s as shallowEqualBundle, t as attachSelectionSetters, u as resolveScopeId, v as hiddenWhenEmpty, w as RENDER_FIELDS, x as projectCellData, y as hideWhenEmptyProblem } from "./chunks/projection-e8MHn6Bt.js";
2
2
  import { C as passesConditions, S as pageContext, T as settled, _ as EACH, a as VARIABLE_NAMESPACES, b as cellResolutionContext, c as pageVariables, d as resolveDescriptorVariables, f as selectionShape, g as variableReferences, h as variableLabels, i as UnsafeDocumentError, l as prepareDocument, m as validateDescriptor, n as RENDER_FUNCTIONS, o as descriptorShape, p as shapeAt, r as RENDER_FUNCTIONS_AHEAD, s as orderByVariable, t as PAGING_VARIABLES, u as queryKey, v as EACH_DEFAULT_AS, w as resolveReferences, x as isEach, y as cellContext } from "./chunks/query-D6xQbTFK.js";
3
- import { _ as stateValue, a as nearestNames, b as walkKey, c as selectionIssues, d as needsWalk, f as readField, g as stateNames, h as resolveWalkStart, i as eachFormProblems, l as RESERVED_LABELS, m as referencedLabels, n as STORE_SCOPES, o as validateCell, p as readWalk, r as WALK_DIRECTIONS, s as validatePage, t as RESPONSIVE_BUCKETS, u as mergeStates, v as systemsFromGraphs, x as walkRequest, y as systemsFromProcess } from "./chunks/validate-BcB_PA1k.js";
3
+ import { _ as stateValue, a as nearestNames, b as walkKey, c as selectionIssues, d as needsWalk, f as readField, g as stateNames, h as resolveWalkStart, i as eachFormProblems, l as RESERVED_LABELS, m as referencedLabels, n as STORE_SCOPES, o as validateCell, p as readWalk, r as WALK_DIRECTIONS, s as validatePage, t as RESPONSIVE_BUCKETS, u as mergeStates, v as systemsFromGraphs, x as walkRequest, y as systemsFromProcess } from "./chunks/validate-CrHGlwey.js";
4
4
  import { C as resolveAction, S as projectCellActions, _ as actionBindingKey, a as deriveSectionReferences, b as declaredActions, c as nodePrintStyle, d as referencesFor, f as sectionAnchor, g as usesStructureLayout, h as tableOfContents, i as deriveSectionNumbering, l as normalizeStructure, m as structureIssues, n as SECTION_KINDS, r as deriveElementReferences, s as mergeSectionReferences, t as PAGE_DOCUMENT, u as numberingFor, v as actionProblems, x as normalizeActionDeclaration, y as actionReferences } from "./chunks/structure-CKBtQHJq.js";
5
5
  import { a as loadBundlesSettled, i as loadBundles, n as isComponentExport, o as projectModule, r as loadBundle, s as runtimeReady, t as _clearBundleCache } from "./chunks/load-b38vUiZM.js";
6
6
  import { a as normalizeRef, i as bundleScope, n as MAIN, o as refKey, r as PUBLIC, t as CORE_SPECIFIER } from "./chunks/refs-Csgh6rXq.js";
@@ -801,6 +801,10 @@ var DIAGNOSTICS = {
801
801
  meaning: "A literal prop value fails its schema.",
802
802
  fix: "Write the value in the shape `props_schema` gives, or bind it with a whole-string `@label.path` reference."
803
803
  },
804
+ HIDE_WHEN_EMPTY_INVALID: {
805
+ meaning: "A cell’s `hide_when_empty` isn’t a list of prop names.",
806
+ fix: "Write `hide_when_empty: [\"title\", \"items\"]`: the props that, all empty once bound (null, blank text, `[]`, `{}`), leave the cell out."
807
+ },
804
808
  CELL_UNKNOWN_CHILD: {
805
809
  meaning: "`children` names a cell that is not on the page.",
806
810
  fix: "List only ids of cells on this page in `children`."
@@ -1054,7 +1058,7 @@ The host's rules for every page. Components, kits and patterns are in the catalo
1054
1058
  - Every cell has a unique \`id\`. Inline: \`{ id, component, props, layout? }\`. Managed by a cell node: \`{ id, ref: { graph_node_id }, layout? }\`; its \`component\` and \`props\` are the fallback.
1055
1059
  - \`children\` lists child cell ids, rendered inside the cell in place of \`props.children\`.
1056
1060
  - A child with \`slot\` fills that \`x-slot\` of its parent; others are \`children\`.
1057
- - \`conditions.showIf\` / \`hideIf\` test a value, usually a reference, for truthiness.
1061
+ - \`conditions.showIf\` / \`hideIf\` test a value for truthiness; \`hide_when_empty: [props]\` hides the cell if all are empty.
1058
1062
  - An embed cell \`{ id, embed: { page_id } | { page_type, project_id?, process_id? } }\` renders another published page, one level deep.
1059
1063
 
1060
1064
  ## Data and references
@@ -1132,4 +1136,4 @@ function manifestPageFrom(graphNodeId, data) {
1132
1136
  }
1133
1137
  };
1134
1138
  }
1135
- export { CONTENT_FIELDS, CORE_SPECIFIER, DIAGNOSTICS, EACH, EACH_DEFAULT_AS, HYDRATED_CELL_FIELDS, LAYER_ROLES, LAYOUT_TYPES, MAIN, PAGE_DOCUMENT, PAGE_RULES, PAGING_VARIABLES, PUBLIC, RENDER_FUNCTIONS, RENDER_FUNCTIONS_AHEAD, RESERVED_LABELS, RESPONSIVE_BUCKETS, SECTION_KINDS, STORE_FIELDS, STORE_SCOPES, STRUCTURE_FIELDS, TOOL_OWNED_KEYS, UnsafeDocumentError, VARIABLE_NAMESPACES, WALK_DIRECTIONS, _clearBundleCache, actionBindingKey, actionProblems, actionReferences, assembleCellProps, attachSelectionSetters, attachViewStateSetters, buildDataContext, bundleScope, cellAttributes, cellContext, cellPlacementStyle, cellPropSources, cellResolutionContext, clearScope, collectDeclarations, collectDescriptors, collectScopeIds, collectStatic, createStackStore, declaredActions, deriveElementReferences, deriveSectionNumbering, deriveSectionReferences, descriptorShape, eachFormProblems, editTarget, hasRef, isComponentExport, isEach, layerName, loadBundle, loadBundles, loadBundlesSettled, loadPageData, loadScope, manifestPageFrom, mergeSectionReferences, mergeStates, nearestNames, needsWalk, nodePrintStyle, normalizeActionDeclaration, normalizeCell, normalizeRef, normalizeStructure, numberingFor, orderByVariable, pageBundleRefs, pageBundles, pageContainerStyle, pageContext, pageRuntimeFromValues, pageScopeKey, pageVariables, passesConditions, patchAt, prepareDocument, projectCellActions, projectCellData, projectLive, projectModule, projectScopeKey, projectSelections, projectViewState, queryKey, readField, readWalk, refKey, referencedLabels, referencesFor, resolveAction, resolveCell, resolveComponent, resolveDataSystem, resolveDeclarations, resolveDescriptorVariables, resolveReferences, resolveScopeId, resolveSystems, resolveWalkStart, rootCells, runtimeReady, saveScope, sectionAnchor, selectionIssues, selectionShape, settled, shallowEqualBundle, shapeAt, stateNames, stateValue, structureIssues, systemsFromGraphs, systemsFromProcess, tableOfContents, usesStructureLayout, validateCell, validateDescriptor, validateManifest, validatePage, variableLabels, variableReferences, walkKey, walkRequest };
1139
+ export { CONTENT_FIELDS, CORE_SPECIFIER, DIAGNOSTICS, EACH, EACH_DEFAULT_AS, HYDRATED_CELL_FIELDS, LAYER_ROLES, LAYOUT_TYPES, MAIN, PAGE_DOCUMENT, PAGE_RULES, PAGING_VARIABLES, PUBLIC, RENDER_FIELDS, RENDER_FUNCTIONS, RENDER_FUNCTIONS_AHEAD, RESERVED_LABELS, RESPONSIVE_BUCKETS, SECTION_KINDS, STORE_FIELDS, STORE_SCOPES, STRUCTURE_FIELDS, TOOL_OWNED_KEYS, UnsafeDocumentError, VARIABLE_NAMESPACES, WALK_DIRECTIONS, _clearBundleCache, actionBindingKey, actionProblems, actionReferences, assembleCellProps, attachSelectionSetters, attachViewStateSetters, buildDataContext, bundleScope, cellAttributes, cellContext, cellPlacementStyle, cellPropSources, cellResolutionContext, clearScope, collectDeclarations, collectDescriptors, collectScopeIds, collectStatic, createStackStore, declaredActions, deriveElementReferences, deriveSectionNumbering, deriveSectionReferences, descriptorShape, eachFormProblems, editTarget, hasRef, hiddenWhenEmpty, hideWhenEmptyProblem, isComponentExport, isEach, isEmptyPropValue, layerName, loadBundle, loadBundles, loadBundlesSettled, loadPageData, loadScope, manifestPageFrom, mergeSectionReferences, mergeStates, nearestNames, needsWalk, nodePrintStyle, normalizeActionDeclaration, normalizeCell, normalizeRef, normalizeStructure, numberingFor, orderByVariable, pageBundleRefs, pageBundles, pageContainerStyle, pageContext, pageRuntimeFromValues, pageScopeKey, pageVariables, passesConditions, patchAt, prepareDocument, projectCellActions, projectCellData, projectLive, projectModule, projectScopeKey, projectSelections, projectViewState, queryKey, readField, readWalk, refKey, referencedLabels, referencesFor, resolveAction, resolveCell, resolveComponent, resolveDataSystem, resolveDeclarations, resolveDescriptorVariables, resolveReferences, resolveScopeId, resolveSystems, resolveWalkStart, rootCells, runtimeReady, saveScope, sectionAnchor, selectionIssues, selectionShape, settled, shallowEqualBundle, shapeAt, stateNames, stateValue, structureIssues, systemsFromGraphs, systemsFromProcess, tableOfContents, usesStructureLayout, validateCell, validateDescriptor, validateManifest, validatePage, variableLabels, variableReferences, walkKey, walkRequest };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { T as rootCells, _ as failedLabels, a as projectSelections, d as resolveComponent, f as cellAttributes, g as assembleCellProps, h as pageContainerStyle, i as projectLive, m as documentProfile, n as attachViewStateSetters, o as projectViewState, p as cellPlacementStyle, s as shallowEqualBundle, t as attachSelectionSetters, u as resolveScopeId, v as projectCellData } from "./chunks/projection-zhu32_KJ.js";
1
+ import { _ as failedLabels, a as projectSelections, d as resolveComponent, f as cellAttributes, g as assembleCellProps, h as pageContainerStyle, i as projectLive, k as rootCells, m as documentProfile, n as attachViewStateSetters, o as projectViewState, p as cellPlacementStyle, s as shallowEqualBundle, t as attachSelectionSetters, u as resolveScopeId, v as hiddenWhenEmpty, x as projectCellData, y as hideWhenEmptyProblem } from "./chunks/projection-e8MHn6Bt.js";
2
2
  import { C as passesConditions, S as pageContext, T as settled, b as cellResolutionContext, c as pageVariables, v as EACH_DEFAULT_AS, w as resolveReferences, y as cellContext } from "./chunks/query-D6xQbTFK.js";
3
3
  import { a as deriveSectionReferences, b as declaredActions, c as nodePrintStyle, d as referencesFor, f as sectionAnchor, g as usesStructureLayout, i as deriveSectionNumbering, l as normalizeStructure, m as structureIssues, o as isSectionNode, p as sortByOrder, r as deriveElementReferences, s as mergeSectionReferences, t as PAGE_DOCUMENT, u as numberingFor } from "./chunks/structure-CKBtQHJq.js";
4
4
  import { a as goToQuestion, c as missing, f as progress, g as visibleSteps, h as stepKind, i as goTo, l as move, m as settle, n as enterAdvances, o as isReadOnlyStatus, p as questionsOf, s as linkTo, t as defaultMode, u as positionFromLink } from "./chunks/forms-DxA9WzCt.js";
@@ -12,18 +12,34 @@ function CellPlaceholder({ state }) {
12
12
  "aria-busy": state === "loading"
13
13
  });
14
14
  }
15
- function CellNotice({ state, component, onRetry, children }) {
15
+ function CellHidden() {
16
+ return jsx("div", {
17
+ className: "cell-hidden",
18
+ "data-cell-state": "hidden-empty",
19
+ role: "note",
20
+ children: "hidden (empty)"
21
+ });
22
+ }
23
+ function CellNotice({ state, component, onRetry, code, showCode, children }) {
16
24
  return jsxs("div", {
17
25
  className: "cell-notice",
18
26
  "data-cell-state": state,
19
27
  role: state === "error" ? "alert" : "note",
20
28
  ...state === "not-found" && component ? { "data-missing-component": component } : {},
21
- children: [children, onRetry && jsxs(Fragment$1, { children: [" ", jsx("button", {
22
- type: "button",
23
- className: "cell-notice__retry",
24
- onClick: onRetry,
25
- children: "Try again"
26
- })] })]
29
+ children: [
30
+ children,
31
+ code && showCode && jsxs(Fragment$1, { children: [" ", jsx("code", {
32
+ className: "cell-notice__code",
33
+ "data-cell-code": code,
34
+ children: code
35
+ })] }),
36
+ onRetry && jsxs(Fragment$1, { children: [" ", jsx("button", {
37
+ type: "button",
38
+ className: "cell-notice__retry",
39
+ onClick: onRetry,
40
+ children: "Try again"
41
+ })] })
42
+ ]
27
43
  });
28
44
  }
29
45
  var CellErrorBoundary = class extends Component {
@@ -219,6 +235,23 @@ function mergeActions(a, b) {
219
235
  ...b
220
236
  } : void 0;
221
237
  }
238
+ function boundProps(cell, ctx, env, injectedProps) {
239
+ const out = { ...resolveReferences(cell.props ?? {}, ctx) };
240
+ for (const id of cell.children ?? []) {
241
+ const child = env.cellsById.get(id);
242
+ if (!child) continue;
243
+ const slot = typeof child.slot === "string" && child.slot ? child.slot : "children";
244
+ if (slot === "item" && cell.props?.each !== void 0) {
245
+ const each = out.each;
246
+ out[slot] = Array.isArray(each) ? each : out[slot];
247
+ } else out[slot] = [...Array.isArray(out[slot]) ? out[slot] : [], id];
248
+ }
249
+ return {
250
+ ...out,
251
+ ...injectedProps,
252
+ ...env.cellProps?.(cell)
253
+ };
254
+ }
222
255
  function CellView({ cell, env, nested = false, injectedProps }) {
223
256
  const frame = useRef(null);
224
257
  const store = useCellStore(cell, env.scope, env.store);
@@ -305,8 +338,19 @@ function CellView({ cell, env, nested = false, injectedProps }) {
305
338
  context
306
339
  ]);
307
340
  if (!passesConditions(cell.conditions, ctx)) return null;
341
+ const deciding = Boolean(cell.hide_when_empty?.length && cell.component && !cell.embed && !failedLabels(cell, ctx.data).length);
342
+ const problem = deciding ? hideWhenEmptyProblem(cell, ctx.data) : null;
343
+ const hidden = deciding && !problem && hiddenWhenEmpty(cell, boundProps(cell, ctx, env, injectedProps), ctx.data);
344
+ if (hidden && !env.showHiddenCells) return null;
308
345
  let body;
309
- if (cell.embed) body = env.renderEmbed?.(cell.id) ?? null;
346
+ if (hidden) body = jsx(CellHidden, {});
347
+ else if (problem) body = jsx(CellNotice, {
348
+ state: "error",
349
+ code: problem,
350
+ showCode: env.showProblemCodes,
351
+ children: "This part of the page couldn't load."
352
+ });
353
+ else if (cell.embed) body = env.renderEmbed?.(cell.id) ?? null;
310
354
  else if (!cell.component) body = jsx(CellPlaceholder, { state: "pending" });
311
355
  else if (failedLabels(cell, ctx.data).length) body = jsx(CellNotice, {
312
356
  state: "error",
@@ -528,7 +572,7 @@ var warn = (d) => console.warn(`[render] ${d.code}${d.cellId ? ` (cell ${d.cellI
528
572
  function PageRenderer({ runtime, bundles, params, baseProps, context, viewer, environment }) {
529
573
  const { page, cells } = runtime;
530
574
  const declared = useMemo(() => new Set(runtime.declared), [runtime.declared]);
531
- const { core, navigate, store, onDiagnostic = warn, actionsFor, runAction, loadPage, onCellError, onUnresolvedReference, onPropSources, frameProps, cellProps, retry } = environment;
575
+ const { core, navigate, store, onDiagnostic = warn, actionsFor, runAction, loadPage, onCellError, onUnresolvedReference, onPropSources, frameProps, cellProps, retry, showHiddenCells, showProblemCodes } = environment;
532
576
  useEffect(() => {
533
577
  for (const d of runtime.diagnostics) onDiagnostic(d);
534
578
  }, [runtime.diagnostics, onDiagnostic]);
@@ -556,6 +600,8 @@ function PageRenderer({ runtime, bundles, params, baseProps, context, viewer, en
556
600
  onDiagnostic,
557
601
  frameProps,
558
602
  cellProps,
603
+ showHiddenCells,
604
+ showProblemCodes,
559
605
  renderEmbed: (cellId) => {
560
606
  const embedded = runtime.embeds?.[cellId];
561
607
  if (!embedded) return jsx(CellNotice, {
@@ -607,6 +653,8 @@ function PageRenderer({ runtime, bundles, params, baseProps, context, viewer, en
607
653
  frameProps,
608
654
  cellProps,
609
655
  retry,
656
+ showHiddenCells,
657
+ showProblemCodes,
610
658
  environment
611
659
  ]);
612
660
  const structured = usesStructureLayout(page.layout);
@@ -980,4 +1028,4 @@ function bindActions(v, path, latest) {
980
1028
  if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, bindActions(x, [...path, k], latest)]));
981
1029
  return v;
982
1030
  }
983
- export { CellErrorBoundary, CellNotice, CellPlaceholder, CellView, FORM_SAVE_DEBOUNCE_MS, PageRenderer, StructureView, answerInputId, cellLabels, useCellStore, useContainerSize, useFormsRuntime, usePaging, useStructureFacts };
1031
+ export { CellErrorBoundary, CellHidden, CellNotice, CellPlaceholder, CellView, FORM_SAVE_DEBOUNCE_MS, PageRenderer, StructureView, answerInputId, cellLabels, useCellStore, useContainerSize, useFormsRuntime, usePaging, useStructureFacts };
package/dist/styles.css CHANGED
@@ -6,6 +6,8 @@
6
6
  --cell-placeholder-bg: var(--color-surface-sunken, #f1f1f1);
7
7
  --cell-notice-bg: var(--color-warning-surface, #fff4d6);
8
8
  --cell-notice-fg: var(--color-warning, #6b4e00);
9
+ --cell-hidden-border: var(--color-border, #c8c8c8);
10
+ --cell-hidden-fg: var(--color-text-muted, #6b6b6b);
9
11
  }
10
12
 
11
13
  [data-page-container] { font-family: var(--font-body, system-ui, sans-serif); color: var(--color-text, inherit); }
@@ -17,7 +19,9 @@
17
19
  .cell-placeholder[aria-busy='true'] { animation: cell-pulse 1.5s ease-in-out infinite; }
18
20
  @keyframes cell-pulse { 50% { opacity: 0.55; } }
19
21
  @media (prefers-reduced-motion: reduce) { .cell-placeholder[aria-busy='true'] { animation: none; } }
22
+ .cell-hidden { padding: 0.125rem 0.5rem; border: 1px dashed var(--cell-hidden-border); border-radius: 0.375rem; color: var(--cell-hidden-fg); font-size: 0.75rem; line-height: 1.5; }
20
23
  .cell-notice { padding: 0.75rem 1rem; border-radius: 0.5rem; background: var(--cell-notice-bg); color: var(--cell-notice-fg); }
24
+ .cell-notice__code { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; }
21
25
 
22
26
 
23
27
  [data-show-when] { display: none; }
@@ -45,6 +49,8 @@
45
49
  --cell-placeholder-bg: #262626;
46
50
  --cell-notice-bg: #3a300f;
47
51
  --cell-notice-fg: #f5dc9a;
52
+ --cell-hidden-border: #4a4a4a;
53
+ --cell-hidden-fg: #a3a3a3;
48
54
  }
49
55
  }
50
56
 
@@ -9,6 +9,9 @@ export declare function projectCellData(cell: PageCell, ctx: DataContext): {
9
9
  status: CellStatus;
10
10
  } | null;
11
11
  export declare function failedLabels(cell: PageCell, data: DataContext): string[];
12
+ export declare function isEmptyPropValue(value: unknown): boolean;
13
+ export declare function hideWhenEmptyProblem(cell: Pick<PageCell, 'hide_when_empty' | 'props'>, data: DataContext): 'QUERY_UNRESOLVED' | null;
14
+ export declare function hiddenWhenEmpty(cell: Pick<PageCell, 'hide_when_empty' | 'props'>, props: Record<string, unknown>, data: DataContext): boolean;
12
15
  export type AssembledInputs = {
13
16
  resolvedProps: Record<string, unknown>;
14
17
  children: unknown;
@@ -2,7 +2,8 @@ import type { Diagnostic } from './graph.js';
2
2
  import type { PageCell } from './types.js';
3
3
  export declare const HYDRATED_CELL_FIELDS: readonly ["component", "props", "layout", "conditions", "children", "traversal", "data", "bundle_ref", "actions", "embed"];
4
4
  export declare const STORE_FIELDS: readonly ["selections", "viewState", "live"];
5
- export declare const CONTENT_FIELDS: readonly [...("data" | "component" | "props" | "conditions" | "children" | "traversal" | "bundle_ref" | "actions" | "embed")[], "selections", "viewState", "live"];
5
+ export declare const RENDER_FIELDS: readonly ["hide_when_empty"];
6
+ export declare const CONTENT_FIELDS: readonly [...("component" | "props" | "conditions" | "children" | "traversal" | "data" | "bundle_ref" | "actions" | "embed")[], "selections", "viewState", "live", "hide_when_empty"];
6
7
  export type CellSource = 'inline' | 'node' | 'fallback' | 'pending';
7
8
  export type ResolvedCell = PageCell & {
8
9
  source: CellSource;
@@ -51,6 +51,10 @@ export declare const DIAGNOSTICS: {
51
51
  readonly meaning: "A literal prop value fails its schema.";
52
52
  readonly fix: "Write the value in the shape `props_schema` gives, or bind it with a whole-string `@label.path` reference.";
53
53
  };
54
+ readonly HIDE_WHEN_EMPTY_INVALID: {
55
+ readonly meaning: "A cell’s `hide_when_empty` isn’t a list of prop names.";
56
+ readonly fix: "Write `hide_when_empty: [\"title\", \"items\"]`: the props that, all empty once bound (null, blank text, `[]`, `{}`), leave the cell out.";
57
+ };
54
58
  readonly CELL_UNKNOWN_CHILD: {
55
59
  readonly meaning: "`children` names a cell that is not on the page.";
56
60
  readonly fix: "List only ids of cells on this page in `children`.";
@@ -1,4 +1,4 @@
1
- export { CONTENT_FIELDS, hasRef, HYDRATED_CELL_FIELDS, normalizeCell, resolveCell, rootCells, STORE_FIELDS, type CellSource, type ResolvedCell } from './cells.js';
1
+ export { CONTENT_FIELDS, hasRef, HYDRATED_CELL_FIELDS, normalizeCell, resolveCell, RENDER_FIELDS, rootCells, STORE_FIELDS, type CellSource, type ResolvedCell } from './cells.js';
2
2
  export { buildDataContext, collectDeclarations, collectDescriptors, collectStatic, readField, resolveDeclarations, type ResolvedDeclaration, type StateRead } from './data-context.js';
3
3
  export { mergeStates, needsWalk, readWalk, referencedLabels, RESERVED_LABELS, resolveWalkStart, stateNames, stateValue, systemsFromGraphs, systemsFromProcess, walkKey, walkRequest, type Diagnostic, type GraphSystem, type ProcessSystems, type RawWalk, type WalkBinding, type WalkRequest, type WalkStart, } from './graph.js';
4
4
  export { LAYER_ROLES, layerName, type LayerType } from './layers.js';
@@ -14,7 +14,7 @@ export { pageRuntimeFromValues } from './values.js';
14
14
  export { actionBindingKey, actionProblems, actionReferences, declaredActions, normalizeActionDeclaration, projectCellActions, resolveAction, type RunAction } from './actions.js';
15
15
  export type { ActionDeclaration } from '@peripheries/runtime';
16
16
  export type * from './types.js';
17
- export { assembleCellProps, projectCellData, type CellStatus } from './cell-props.js';
17
+ export { assembleCellProps, hiddenWhenEmpty, hideWhenEmptyProblem, isEmptyPropValue, projectCellData, type CellStatus } from './cell-props.js';
18
18
  export { cellAttributes, cellPlacementStyle, pageContainerStyle } from './layout.js';
19
19
  export { deriveElementReferences, deriveSectionNumbering, deriveSectionReferences, mergeSectionReferences, nodePrintStyle, normalizeStructure, numberingFor, PAGE_DOCUMENT, referencesFor, SECTION_KINDS, sectionAnchor, structureIssues, tableOfContents, usesStructureLayout, type ContentsEntry, type LayoutNode, type SectionNumbering, type SectionReference, type SectionReferences, } from './structure.js';
20
20
  export { _clearBundleCache, isComponentExport, loadBundle, loadBundles, loadBundlesSettled, projectModule, runtimeReady, type BundleFiles, type BundleRow, type LoadedBundle, type LoadedBundles, } from './bundles/load.js';
@@ -1,4 +1,4 @@
1
- import type { DeclaredBinding, PageCell, QueryDescriptor } from '@peripheries/runtime';
1
+ import type { DeclaredBinding, PageCell as RuntimePageCell, QueryDescriptor } from '@peripheries/runtime';
2
2
  export type DataPage = {
3
3
  hasMore: boolean;
4
4
  endCursor: string | null;
@@ -37,7 +37,10 @@ export type DataOrigin = {
37
37
  kind: 'reserved';
38
38
  };
39
39
  export type DataContext = Record<string, DataEntry>;
40
- export type { CellData, CellLayout, DeclaredBinding, PageCell, ResponsiveBucket } from '@peripheries/runtime';
40
+ export type { CellData, CellLayout, DeclaredBinding, ResponsiveBucket } from '@peripheries/runtime';
41
+ export type PageCell = RuntimePageCell & {
42
+ hide_when_empty?: string[];
43
+ };
41
44
  export type PageContent = {
42
45
  cells?: PageCell[];
43
46
  data?: {
@@ -2,10 +2,13 @@ import { Component, type ReactNode } from 'react';
2
2
  export declare function CellPlaceholder({ state }: {
3
3
  state: 'loading' | 'pending';
4
4
  }): import("react").JSX.Element;
5
- export declare function CellNotice({ state, component, onRetry, children }: {
5
+ export declare function CellHidden(): import("react").JSX.Element;
6
+ export declare function CellNotice({ state, component, onRetry, code, showCode, children }: {
6
7
  state: 'not-found' | 'error';
7
8
  component?: string;
8
9
  onRetry?: () => void;
10
+ code?: string;
11
+ showCode?: boolean;
9
12
  children: ReactNode;
10
13
  }): import("react").JSX.Element;
11
14
  export declare class CellErrorBoundary extends Component<{
@@ -38,6 +38,8 @@ export type CellEnvironment = {
38
38
  onDiagnostic?: (d: Diagnostic) => void;
39
39
  frameProps?: (cell: PageCell, contract: ComponentContract | undefined) => FrameProps | undefined;
40
40
  cellProps?: (cell: PageCell) => Record<string, unknown> | undefined;
41
+ showHiddenCells?: boolean;
42
+ showProblemCodes?: boolean;
41
43
  };
42
44
  export type FrameProps = {
43
45
  attributes?: Record<string, string>;
@@ -1,4 +1,4 @@
1
- export { CellErrorBoundary, CellNotice, CellPlaceholder } from './cell-states.js';
1
+ export { CellErrorBoundary, CellHidden, CellNotice, CellPlaceholder } from './cell-states.js';
2
2
  export { CellView, type CellEnvironment, type FrameProps } from './cell-view.js';
3
3
  export { PageRenderer, type PageRendererProps, type RendererEnvironment } from './page-renderer.js';
4
4
  export { StructureView, useStructureFacts } from './structure-view.js';
@@ -23,6 +23,8 @@ export type RendererEnvironment = {
23
23
  retry?: () => void;
24
24
  frameProps?: (cell: PageCell, contract: ComponentContract | undefined) => FrameProps | undefined;
25
25
  cellProps?: (cell: PageCell) => Record<string, unknown> | undefined;
26
+ showHiddenCells?: boolean;
27
+ showProblemCodes?: boolean;
26
28
  };
27
29
  export type PageRendererProps = {
28
30
  runtime: PageRuntime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peripheries/renderer",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "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.",
5
5
  "type": "module",
6
6
  "sideEffects": [