@phis/ui 0.4.2 → 0.4.3

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.
Files changed (241) hide show
  1. package/components/cms/phi-cms-layout-renderer.js +4 -3
  2. package/components/cms/phi-cms-page-render-scope.d.ts +4 -5
  3. package/components/cms/phi-cms-page-render-scope.js +43 -22
  4. package/components/cms/phi-cms-render-diagnostic-client.js +7 -3
  5. package/components/cms/phi-cms-render-error-boundary.js +2 -1
  6. package/components/controls/phi-bullet-list-control.js +31 -2
  7. package/components/controls/phi-ribbon-control.d.ts +29 -0
  8. package/components/controls/phi-ribbon-control.js +53 -0
  9. package/components/controls/phi-table-control.d.ts +3 -0
  10. package/components/controls/phi-table-control.js +13 -1
  11. package/components/controls/phi-tree-control.js +3 -3
  12. package/components/forms/shared-form-provider-registry.js +1 -4
  13. package/components/media/asset-collection-runtime.js +6 -16
  14. package/components/media/media-font-metrics.d.ts +3 -0
  15. package/components/media/media-font-metrics.js +43 -0
  16. package/components/media/phi-area-upload-widget.js +53 -97
  17. package/components/media/phi-image-preview-data.d.ts +13 -0
  18. package/components/media/phi-image-preview-data.js +184 -9
  19. package/components/media/phi-media-picker-binding.js +16 -13
  20. package/components/media/phi-media-upload.d.ts +15 -8
  21. package/components/media/phi-media-upload.js +18 -9
  22. package/components/media/phi-site-font-assets.js +7 -8
  23. package/components/overlays/phi-overlay-container-client.js +16 -1
  24. package/components/regions/presets/phi-settings-page-shell-tree.js +48 -1
  25. package/components/runtime/runtime-controller-materialization.js +12 -1
  26. package/components/state/scoped-state-store.js +20 -3
  27. package/components/tables/client/phi-table-binding.d.ts +2 -1
  28. package/components/tables/client/phi-table-binding.js +45 -21
  29. package/components/trees/client/phi-tree-binding.js +19 -11
  30. package/components/widgets/client/bullet-list-entries-editor.d.ts +26 -0
  31. package/components/widgets/client/bullet-list-entries-editor.js +103 -0
  32. package/components/widgets/config/choice-shared.js +12 -13
  33. package/components/widgets/config/command-buttons.d.ts +5 -1
  34. package/components/widgets/config/command-buttons.js +38 -81
  35. package/components/widgets/config/control-signal-config.d.ts +3 -1
  36. package/components/widgets/config/control-signal-config.js +19 -17
  37. package/components/widgets/config/image-source-parser.d.ts +1 -0
  38. package/components/widgets/config/image-source-parser.js +7 -8
  39. package/components/widgets/config/mask.d.ts +6 -1
  40. package/components/widgets/config/mask.js +24 -1
  41. package/components/widgets/config/parser-primitives.js +72 -0
  42. package/components/widgets/config/stack-tabs.d.ts +1 -1
  43. package/components/widgets/config/stack-tabs.js +2 -3
  44. package/components/widgets/config/strict-config-reader.d.ts +40 -0
  45. package/components/widgets/config/strict-config-reader.js +174 -0
  46. package/components/widgets/label-sets/effects.js +2 -0
  47. package/components/widgets/label-types/effects.d.ts +1 -0
  48. package/components/widgets/label-types/effects.js +1 -0
  49. package/components/widgets/navigation-descriptor-resolver.server.js +20 -8
  50. package/components/widgets/shared/bullet-list-vocabulary.d.ts +17 -1
  51. package/components/widgets/shared/bullet-list-vocabulary.js +19 -2
  52. package/components/widgets/shared/card-body-client.d.ts +16 -4
  53. package/components/widgets/shared/card-body-client.js +81 -24
  54. package/components/widgets/shared/card-vocabulary.d.ts +21 -4
  55. package/components/widgets/shared/card-vocabulary.js +28 -2
  56. package/components/widgets/signals/control-signal-capabilities.d.ts +0 -18
  57. package/components/widgets/signals/control-signal-capabilities.js +10 -22
  58. package/components/workspace/catalog-store.d.ts +4 -3
  59. package/components/workspace/catalog-store.js +6 -5
  60. package/gateway/dashboard-cards-route.js +1 -0
  61. package/gateway/internal-references.d.ts +6 -1
  62. package/gateway/internal-references.js +48 -23
  63. package/gateway/site-cms-write-validation.d.ts +9 -0
  64. package/gateway/site-cms-write-validation.js +78 -0
  65. package/gateway/site-proxy.js +19 -3
  66. package/helpers/renderable-block-serialization.js +1 -1
  67. package/next/config.d.mts +3 -0
  68. package/next/config.mjs +28 -1
  69. package/package.json +2 -2
  70. package/plugins/runtime/phi-slot-child-viewport-effects-observer.js +5 -1
  71. package/plugins/runtime-modules/accounting/presets.d.ts +9 -0
  72. package/plugins/runtime-modules/accounting/presets.js +37 -0
  73. package/plugins/runtime-modules/accounting/server.d.ts +1 -4
  74. package/plugins/runtime-modules/accounting/server.js +4 -7
  75. package/plugins/runtime-modules/app/presets.d.ts +18 -0
  76. package/plugins/runtime-modules/app/presets.js +67 -0
  77. package/plugins/runtime-modules/app/server.d.ts +1 -4
  78. package/plugins/runtime-modules/app/server.js +4 -7
  79. package/plugins/runtime-modules/asset/controller/runtime.js +2 -2
  80. package/plugins/runtime-modules/asset/services/collection.js +9 -1
  81. package/plugins/runtime-modules/asset/services/options.js +2 -2
  82. package/plugins/runtime-modules/builder/area-settings-answer.js +2 -1
  83. package/plugins/runtime-modules/builder/authoring-canvas.js +6 -6
  84. package/plugins/runtime-modules/builder/cards.js +3 -5
  85. package/plugins/runtime-modules/builder/clients/builder-region-overrides.js +3 -3
  86. package/plugins/runtime-modules/builder/clients/canvas-widget.js +15 -54
  87. package/plugins/runtime-modules/builder/clients/inspector-config-field.d.ts +4 -1
  88. package/plugins/runtime-modules/builder/clients/inspector-config-field.js +5 -34
  89. package/plugins/runtime-modules/builder/clients/inspector-link-target-field.js +1 -1
  90. package/plugins/runtime-modules/builder/clients/inspector-section-widget.js +8 -8
  91. package/plugins/runtime-modules/builder/clients/inspector-title.js +6 -6
  92. package/plugins/runtime-modules/builder/clients/layout-inspector.d.ts +1 -1
  93. package/plugins/runtime-modules/builder/clients/layout-inspector.js +11 -25
  94. package/plugins/runtime-modules/builder/clients/mode-switch.js +1 -1
  95. package/plugins/runtime-modules/builder/clients/pages-header.js +11 -11
  96. package/plugins/runtime-modules/builder/clients/pages-workspace.js +1 -1
  97. package/plugins/runtime-modules/builder/clients/region-visibility-fields.js +1 -1
  98. package/plugins/runtime-modules/builder/clients/runtime-module-authoring-boundary.js +1 -1
  99. package/plugins/runtime-modules/builder/clients/shells-workspace.js +15 -31
  100. package/plugins/runtime-modules/builder/clients/structure-canvas.js +1 -1
  101. package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.d.ts +26 -0
  102. package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.js +36 -0
  103. package/plugins/runtime-modules/builder/clients/widget-effects-editor.js +1 -1
  104. package/plugins/runtime-modules/builder/clients/widget-inspector.js +1 -0
  105. package/plugins/runtime-modules/builder/clients/workspace-scope-boundary.js +2 -2
  106. package/plugins/runtime-modules/builder/cms-instance-allocator.js +8 -28
  107. package/plugins/runtime-modules/builder/controller/client.d.ts +0 -15
  108. package/plugins/runtime-modules/builder/controller/definition.d.ts +0 -15
  109. package/plugins/runtime-modules/builder/controller/definition.js +0 -8
  110. package/plugins/runtime-modules/builder/controller/draft-command-controller.d.ts +1 -2
  111. package/plugins/runtime-modules/builder/controller/draft-command-controller.js +11 -30
  112. package/plugins/runtime-modules/builder/controller/draft-status-controller.js +15 -2
  113. package/plugins/runtime-modules/builder/controller/inspector-controller.d.ts +1 -2
  114. package/plugins/runtime-modules/builder/controller/inspector-controller.js +4 -7
  115. package/plugins/runtime-modules/builder/controller/page-controller.d.ts +1 -2
  116. package/plugins/runtime-modules/builder/controller/page-controller.js +16 -68
  117. package/plugins/runtime-modules/builder/controller/server-definition.d.ts +0 -15
  118. package/plugins/runtime-modules/builder/controller/signal-wiring-controller.d.ts +2 -5
  119. package/plugins/runtime-modules/builder/controller/signal-wiring-controller.js +6 -50
  120. package/plugins/runtime-modules/builder/controller/workspace-controller.d.ts +2 -3
  121. package/plugins/runtime-modules/builder/controller/workspace-controller.js +89 -153
  122. package/plugins/runtime-modules/builder/data-providers.d.ts +1 -0
  123. package/plugins/runtime-modules/builder/data-providers.js +1 -0
  124. package/plugins/runtime-modules/builder/definition.d.ts +1 -0
  125. package/plugins/runtime-modules/builder/developer-workspace-store.d.ts +64 -30
  126. package/plugins/runtime-modules/builder/developer-workspace-store.js +255 -61
  127. package/plugins/runtime-modules/builder/edit-scaffold-drawer.js +1 -1
  128. package/plugins/runtime-modules/builder/history.d.ts +3 -1
  129. package/plugins/runtime-modules/builder/options-providers.js +3 -2
  130. package/plugins/runtime-modules/builder/page-catalog-client.js +19 -7
  131. package/plugins/runtime-modules/builder/page-meta-form.js +1 -0
  132. package/plugins/runtime-modules/builder/page-references-provider.js +1 -1
  133. package/plugins/runtime-modules/builder/persistence.js +33 -69
  134. package/plugins/runtime-modules/builder/public-route-collisions.d.ts +2 -2
  135. package/plugins/runtime-modules/builder/public-route-collisions.js +6 -19
  136. package/plugins/runtime-modules/builder/render-root-node-preview.server.js +20 -3
  137. package/plugins/runtime-modules/builder/render-root-node-scaffold.js +3 -3
  138. package/plugins/runtime-modules/builder/runtime-module-selection.d.ts +2 -12
  139. package/plugins/runtime-modules/builder/runtime-module-selection.js +16 -42
  140. package/plugins/runtime-modules/builder/services/navigation-table.js +9 -15
  141. package/plugins/runtime-modules/builder/services/page-source-tree.js +8 -8
  142. package/plugins/runtime-modules/builder/services/runtime-modules-table.js +10 -10
  143. package/plugins/runtime-modules/builder/services/signal-routes-table.js +3 -3
  144. package/plugins/runtime-modules/builder/signal-route-selection.d.ts +1 -2
  145. package/plugins/runtime-modules/builder/signal-route-selection.js +2 -2
  146. package/plugins/runtime-modules/builder/signal-wiring-options.js +3 -2
  147. package/plugins/runtime-modules/builder/structure-dnd.js +8 -61
  148. package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +13 -10
  149. package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +12 -18
  150. package/plugins/runtime-modules/builder/workspace-catalog-actions.d.ts +55 -0
  151. package/plugins/runtime-modules/builder/workspace-catalog-actions.js +142 -0
  152. package/plugins/runtime-modules/client-manifests/common.js +1 -1
  153. package/plugins/runtime-modules/{runtime-calendar-adapters.js → core/calendar-adapters.js} +2 -2
  154. package/plugins/runtime-modules/{client-calendar-adapters/core.js → core/client-calendar-adapters.js} +1 -1
  155. package/plugins/runtime-modules/core/content-entries/table.js +1 -1
  156. package/plugins/runtime-modules/core/data-providers.d.ts +3 -0
  157. package/plugins/runtime-modules/core/data-providers.js +2 -1
  158. package/plugins/runtime-modules/core/definition.d.ts +3 -0
  159. package/plugins/runtime-modules/core/definition.js +1 -1
  160. package/plugins/runtime-modules/core/messaging/data-providers.d.ts +1 -0
  161. package/plugins/runtime-modules/core/messaging/data-providers.js +1 -0
  162. package/plugins/runtime-modules/core/messaging/thread-inbox-table.js +1 -1
  163. package/plugins/runtime-modules/core/server.js +1 -1
  164. package/plugins/runtime-modules/core/themes.d.ts +44 -0
  165. package/plugins/runtime-modules/core/themes.js +12 -0
  166. package/plugins/runtime-modules/core/widgets/card/collection-view-binding.js +25 -9
  167. package/plugins/runtime-modules/core/widgets/card/config.d.ts +296 -254
  168. package/plugins/runtime-modules/core/widgets/card/config.js +52 -3
  169. package/plugins/runtime-modules/core/widgets/card/editor.js +30 -3
  170. package/plugins/runtime-modules/core/widgets/card/server.d.ts +3 -0
  171. package/plugins/runtime-modules/core/widgets/card/server.js +31 -3
  172. package/plugins/runtime-modules/core/widgets/collection-view/asset-binding.js +7 -6
  173. package/plugins/runtime-modules/core/widgets/collection-view/config.js +9 -2
  174. package/plugins/runtime-modules/core/widgets/description/config.d.ts +1 -25
  175. package/plugins/runtime-modules/core/widgets/description/editor.js +4 -61
  176. package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +1 -25
  177. package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +1 -25
  178. package/plugins/runtime-modules/core/widgets/image/config.js +2 -2
  179. package/plugins/runtime-modules/core/widgets/markdown/editor.js +2 -3
  180. package/plugins/runtime-modules/core/widgets/result/config.d.ts +4 -25
  181. package/plugins/runtime-modules/core/widgets/result/config.js +2 -0
  182. package/plugins/runtime-modules/core/widgets/result/server.js +25 -19
  183. package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +1 -25
  184. package/plugins/runtime-modules/core/widgets/slot-upload/client.js +11 -2
  185. package/plugins/runtime-modules/core/widgets/table/client.js +8 -4
  186. package/plugins/runtime-modules/core/widgets/table/config.d.ts +3 -83
  187. package/plugins/runtime-modules/core/widgets/table/config.js +302 -452
  188. package/plugins/runtime-modules/core/widgets/tree/config.js +5 -2
  189. package/plugins/runtime-modules/dashboard/card-rows.d.ts +5 -0
  190. package/plugins/runtime-modules/dashboard/card-rows.js +59 -0
  191. package/plugins/runtime-modules/dashboard/clients/dashboard-card-view.js +4 -6
  192. package/plugins/runtime-modules/dashboard/services/collection.js +17 -7
  193. package/plugins/runtime-modules/groups/data-providers.d.ts +2 -0
  194. package/plugins/runtime-modules/groups/data-providers.js +3 -3
  195. package/plugins/runtime-modules/groups/definition.d.ts +2 -0
  196. package/plugins/runtime-modules/localization/data-providers.d.ts +3 -0
  197. package/plugins/runtime-modules/localization/data-providers.js +2 -0
  198. package/plugins/runtime-modules/localization/definition.d.ts +3 -0
  199. package/plugins/runtime-modules/localization/services/table.js +3 -7
  200. package/plugins/runtime-modules/observability/data-providers.d.ts +1 -0
  201. package/plugins/runtime-modules/observability/data-providers.js +1 -0
  202. package/plugins/runtime-modules/observability/definition.d.ts +1 -0
  203. package/plugins/runtime-modules/observability/module.d.ts +1 -0
  204. package/plugins/runtime-modules/observability/services/table.js +1 -1
  205. package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +2 -105
  206. package/plugins/runtime-modules/public/presets.d.ts +115 -0
  207. package/plugins/runtime-modules/{area-base-presets.js → public/presets.js} +19 -119
  208. package/plugins/runtime-modules/public/server.d.ts +1 -4
  209. package/plugins/runtime-modules/public/server.js +4 -7
  210. package/plugins/runtime-modules/public/trees/phi-default-pub-error-page-tree.js +7 -8
  211. package/plugins/runtime-modules/public-base-page-references.d.ts +1 -1
  212. package/plugins/runtime-modules/public-base-page-references.js +1 -1
  213. package/plugins/runtime-modules/revisions/controller/client.js +11 -11
  214. package/plugins/runtime-modules/revisions/services/table.js +3 -3
  215. package/plugins/runtime-modules/theme/trees/phi-default-builder-theme-page-tree.js +22 -0
  216. package/plugins/runtime-modules/theme/widgets/brand-controls/client.d.ts +2 -1
  217. package/plugins/runtime-modules/theme/widgets/brand-controls/client.js +93 -58
  218. package/plugins/runtime-modules/theme/widgets/brand-controls/config.d.ts +2 -1
  219. package/plugins/runtime-modules/theme/widgets/brand-controls/config.js +23 -22
  220. package/plugins/runtime-modules/theme/widgets/brand-controls/plugin.js +1 -1
  221. package/plugins/runtime-modules/theme-descriptors.js +1 -1
  222. package/plugins/runtime-modules/user-management/data-providers.d.ts +2 -0
  223. package/plugins/runtime-modules/user-management/data-providers.js +1 -0
  224. package/plugins/runtime-modules/user-management/definition.d.ts +2 -0
  225. package/styles/control-shape.css +11 -11
  226. package/styles/controls.css +3 -3
  227. package/styles/layout.css +16 -7
  228. package/types/cms-config.js +142 -185
  229. package/types/cms-plugins.d.ts +4 -11
  230. package/types/core-widget-placements.d.ts +14 -3
  231. package/types/dashboard-cards.d.ts +9 -0
  232. package/types/media.d.ts +0 -3
  233. package/types/renderable-block.d.ts +6 -1
  234. package/types/table-widget.d.ts +9 -0
  235. package/types/table-widget.js +20 -0
  236. package/types.d.ts +1 -1
  237. package/widget-config.d.ts +2 -0
  238. package/widget-config.js +6 -0
  239. package/plugins/runtime-modules/area-base-presets.d.ts +0 -139
  240. /package/plugins/runtime-modules/{runtime-calendar-adapters.d.ts → core/calendar-adapters.d.ts} +0 -0
  241. /package/plugins/runtime-modules/{client-calendar-adapters/core.d.ts → core/client-calendar-adapters.d.ts} +0 -0
@@ -4,42 +4,28 @@ import { readPhiCmsMountPolicy } from "./cms-mount-policy";
4
4
  import { clampPhiSequenceTransitionMs, readPhiMotionEasing, PHI_SEQUENCE_ANCHORS, PHI_SEQUENCE_TRANSITIONS, } from "../helpers/motion";
5
5
  import { readPhiLengthValue } from "./length";
6
6
  import { isPhiAnchorWidgetPlacement, resolvePhiRenderableBlockAnchor, } from "../components/controls/phi-anchor-control-contract";
7
- import { readBoolean, readCssSize, readNumber, readRenderableBlockConfig, readString, } from "../components/widgets/config/parser-primitives";
7
+ import { readCssSize, readRenderableBlockConfig, } from "../components/widgets/config/parser-primitives";
8
8
  import { applyPhiLayoutDefaults } from "../helpers/cms-layout-defaults";
9
9
  import { resolvePhiLayoutDefaults } from "../helpers/cms-layout-defaults";
10
10
  import { normalizeRenderableBlockAnchor } from "../helpers/renderable-block-anchor";
11
11
  import { isPhiRecord } from "../helpers/is-record";
12
+ import { createPhiConfigReader } from "../components/widgets/config/strict-config-reader";
12
13
  import { PHI_SPACING_SCALE_KEYS } from "../components/widgets/config/spacing-options";
13
14
  import { PHI_GRID_COLUMN_COUNTS, isPhiGridColumnCount } from "../constants/grid-columns";
14
- function readInitialSlotStates(value) {
15
- if (Array.isArray(value)) {
16
- const normalized = value.map((candidate) => candidate === "expanded" || candidate === "collapsed" || candidate === "hidden" ? candidate : undefined);
17
- return normalized.some((candidate) => candidate !== undefined) ? normalized : undefined;
18
- }
19
- if (!value || typeof value !== "object") {
20
- return undefined;
21
- }
22
- const raw = value;
23
- const next = {};
24
- for (const [key, candidate] of Object.entries(raw)) {
25
- const slotIndex = Number(key);
26
- if (!Number.isInteger(slotIndex) || slotIndex < 0) {
27
- continue;
28
- }
29
- if (candidate === "expanded" || candidate === "collapsed" || candidate === "hidden") {
30
- next[slotIndex] = candidate;
15
+ const PHI_LAYOUT_SLOT_STATES = ["expanded", "collapsed", "hidden"];
16
+ /** A Layout's starting slot states: a list by slot, gaps allowed, or a record keyed by slot index. */
17
+ function readInitialSlotStates(reader) {
18
+ return reader.value("initialSlotStates", "slot states by slot index", (value) => {
19
+ const isState = (candidate) => candidate == null || PHI_LAYOUT_SLOT_STATES.includes(candidate);
20
+ if (Array.isArray(value)) {
21
+ return value.every(isState) ? value : undefined;
31
22
  }
32
- }
33
- return Object.keys(next).length > 0 ? next : undefined;
34
- }
35
- function readStringArray(value) {
36
- if (!Array.isArray(value)) {
37
- return undefined;
38
- }
39
- const next = value
40
- .map((candidate) => (typeof candidate === "string" ? candidate.trim() : ""))
41
- .filter((candidate) => candidate.length > 0);
42
- return [...new Set(next)];
23
+ if (!isPhiRecord(value))
24
+ return undefined;
25
+ return Object.entries(value).every(([key, candidate]) => Number.isInteger(Number(key)) && Number(key) >= 0 && isState(candidate))
26
+ ? value
27
+ : undefined;
28
+ });
43
29
  }
44
30
  export function normalizePhiPaddingWidgetConfig(config) {
45
31
  if (!isPhiRecord(config)) {
@@ -153,18 +139,14 @@ function readRenderableBlockAnchorOrPlacement(value) {
153
139
  ? resolvePhiRenderableBlockAnchor(value)
154
140
  : undefined;
155
141
  }
156
- function readGridResponsiveCount(value, accepts) {
157
- if (!isPhiRecord(value))
158
- return undefined;
159
- const record = value;
160
- const read = (key) => {
161
- const candidate = readNumber(record[key]);
162
- return candidate != null && Number.isInteger(candidate) && accepts(candidate) ? candidate : undefined;
163
- };
164
- const responsive = { compact: read("compact"), medium: read("medium"), wide: read("wide") };
165
- return responsive.compact != null || responsive.medium != null || responsive.wide != null
166
- ? responsive
167
- : undefined;
142
+ function readLayoutAnchor(reader) {
143
+ return reader.value("anchor", "an anchor or a placement", readRenderableBlockAnchorOrPlacement);
144
+ }
145
+ function readGridResponsiveCount(reader, key, expected, accepts) {
146
+ return reader.object(key, (countReader) => {
147
+ const read = (width) => countReader.value(width, expected, (candidate) => typeof candidate === "number" && Number.isInteger(candidate) && accepts(candidate) ? candidate : undefined);
148
+ return { compact: read("compact"), medium: read("medium"), wide: read("wide") };
149
+ });
168
150
  }
169
151
  const PHI_GRID_MAX_COLUMNS = Math.max(...PHI_GRID_COLUMN_COUNTS);
170
152
  /*
@@ -172,32 +154,43 @@ const PHI_GRID_MAX_COLUMNS = Math.max(...PHI_GRID_COLUMN_COUNTS);
172
154
  * they fit the row at a given width is answered where the row is known, by clamping
173
155
  * (`resolvePhiGridSlotPlacement`): a Grid whose columns were reduced keeps its stated placements.
174
156
  */
175
- function readGridSlotPlacement(value, slotIndexFromArray) {
176
- if (!isPhiRecord(value)) {
177
- return null;
178
- }
179
- const slot = value;
180
- const slotIndex = readNumber(slot.slotIndex) ?? slotIndexFromArray;
181
- if (!Number.isInteger(slotIndex) || slotIndex == null || slotIndex < 0) {
182
- return null;
183
- }
184
- const span = readGridResponsiveCount(slot.span, (candidate) => candidate >= 1 && candidate <= PHI_GRID_MAX_COLUMNS);
185
- const offset = readGridResponsiveCount(slot.offset, (candidate) => candidate >= 0 && candidate < PHI_GRID_MAX_COLUMNS);
157
+ function readGridSlotPlacement(reader, slotIndexFromArray) {
158
+ const slotIndex = reader.value("slotIndex", "a slot index", (candidate) => typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 0 ? candidate : undefined) ?? slotIndexFromArray;
159
+ const span = readGridResponsiveCount(reader, "span", `a whole number from 1 to ${PHI_GRID_MAX_COLUMNS}`, (candidate) => candidate >= 1 && candidate <= PHI_GRID_MAX_COLUMNS);
160
+ const offset = readGridResponsiveCount(reader, "offset", `a whole number from 0 to ${PHI_GRID_MAX_COLUMNS - 1}`, (candidate) => candidate >= 0 && candidate < PHI_GRID_MAX_COLUMNS);
186
161
  if (!span && !offset)
187
- return null;
162
+ reader.fail("span", "a span or an offset", undefined);
188
163
  return { slotIndex, span, offset };
189
164
  }
190
165
  /**
191
- * A shared Layout field, read the same way wherever it appears: a line on the 24-track form grid, so
192
- * anything outside 2..24 is not a narrower column but a mistake, and is dropped rather than clamped.
166
+ * The fields many Layouts share, read the same way wherever they appear. A label end is a line on the
167
+ * 24-track form grid, so anything outside 2..24 is not a narrower column but a mistake.
193
168
  */
194
- function readPhiLayoutLabelEnd(value) {
195
- const labelEnd = readNumber(value);
196
- return labelEnd !== undefined && Number.isInteger(labelEnd) && labelEnd >= 2 && labelEnd <= 24
197
- ? labelEnd
198
- : undefined;
169
+ function readCssSizeField(reader, key) {
170
+ return reader.value(key, "a CSS size", (value) => (typeof value === "number" && Number.isFinite(value)) || typeof value === "string" ? value : undefined);
171
+ }
172
+ function readLengthField(reader, key) {
173
+ return reader.value(key, "a CSS length", (value) => readPhiLengthValue(value) ?? undefined);
174
+ }
175
+ function readLayoutLabelEnd(reader) {
176
+ return reader.value("labelEnd", "a whole number from 2 to 24", (value) => typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 24 ? value : undefined);
177
+ }
178
+ function readLayoutPadding(reader) {
179
+ return {
180
+ padding: readCssSizeField(reader, "padding"),
181
+ paddingLeft: readCssSizeField(reader, "paddingLeft"),
182
+ paddingRight: readCssSizeField(reader, "paddingRight"),
183
+ paddingTop: readCssSizeField(reader, "paddingTop"),
184
+ paddingBottom: readCssSizeField(reader, "paddingBottom"),
185
+ };
186
+ }
187
+ function readFlexWrap(reader) {
188
+ return reader.value("wrap", "true, false or a flex wrap", (value) => typeof value === "boolean" || value === "nowrap" || value === "wrap" || value === "wrap-reverse"
189
+ ? value
190
+ : undefined) ?? false;
199
191
  }
200
192
  export function parsePhiCmsContentLayoutConfig(config) {
193
+ const reader = createPhiConfigReader(config);
201
194
  return applyPhiLayoutDefaults({
202
195
  ...readRenderableBlockConfig(config),
203
196
  /*
@@ -207,82 +200,65 @@ export function parsePhiCmsContentLayoutConfig(config) {
207
200
  * constraints never had the second way at all -- nothing has ever read a flat `maxWidth`.
208
201
  */
209
202
  size: normalizePhiRenderableBlockResponsiveSize(config.size),
210
- margin: readCssSize(config.margin),
211
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
212
- padding: readCssSize(config.padding),
213
- paddingLeft: readCssSize(config.paddingLeft),
214
- paddingRight: readCssSize(config.paddingRight),
215
- paddingTop: readCssSize(config.paddingTop),
216
- paddingBottom: readCssSize(config.paddingBottom),
203
+ margin: readCssSizeField(reader, "margin"),
204
+ labelEnd: readLayoutLabelEnd(reader),
205
+ ...readLayoutPadding(reader),
217
206
  /*
218
207
  * Handed on as the block anchor, like every other Layout's. This one used to be folded into a
219
208
  * placement name here, and the renderer then asked the name for its `horizontal` -- a string has
220
209
  * none, so a Content Layout anchored left drew centred wherever the parsed config reached it, in
221
210
  * preview and live, while the Builder, which hands the object straight through, drew it left.
222
211
  */
223
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
212
+ anchor: readLayoutAnchor(reader),
224
213
  }, resolvePhiLayoutDefaults("content"));
225
214
  }
226
215
  export function parsePhiCmsFlexLayoutConfig(config) {
227
- const distribution = readString(config.distribution);
216
+ const reader = createPhiConfigReader(config);
228
217
  return applyPhiLayoutDefaults({
229
218
  ...readRenderableBlockConfig(config),
230
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
231
- initialSlotStates: readInitialSlotStates(config.initialSlotStates),
232
- padding: readCssSize(config.padding),
233
- paddingLeft: readCssSize(config.paddingLeft),
234
- paddingRight: readCssSize(config.paddingRight),
235
- paddingTop: readCssSize(config.paddingTop),
236
- paddingBottom: readCssSize(config.paddingBottom),
237
- gap: readCssSize(config.gap),
238
- verticalSeparators: readBoolean(config.verticalSeparators) ?? false,
239
- separatorBeforeFirst: readBoolean(config.separatorBeforeFirst) ?? false,
240
- separatorSpan: readPhiLengthValue(config.separatorSpan) ?? undefined,
241
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
242
- distribution: distribution === "between" || distribution === "around" || distribution === "evenly"
243
- ? distribution
244
- : "anchor",
245
- wrap: readBoolean(config.wrap) ??
246
- readString(config.wrap) ??
247
- false,
219
+ labelEnd: readLayoutLabelEnd(reader),
220
+ initialSlotStates: readInitialSlotStates(reader),
221
+ ...readLayoutPadding(reader),
222
+ gap: readCssSizeField(reader, "gap"),
223
+ verticalSeparators: reader.boolean("verticalSeparators") ?? false,
224
+ separatorBeforeFirst: reader.boolean("separatorBeforeFirst") ?? false,
225
+ separatorSpan: readLengthField(reader, "separatorSpan"),
226
+ anchor: readLayoutAnchor(reader),
227
+ distribution: reader.oneOf("distribution", ["anchor", "between", "around", "evenly"]) ?? "anchor",
228
+ wrap: readFlexWrap(reader),
248
229
  }, resolvePhiLayoutDefaults("flex"));
249
230
  }
250
231
  export function parsePhiCmsFlexVerticalLayoutConfig(config) {
232
+ const reader = createPhiConfigReader(config);
251
233
  return applyPhiLayoutDefaults({
252
234
  ...readRenderableBlockConfig(config),
253
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
254
- initialSlotStates: readInitialSlotStates(config.initialSlotStates),
255
- padding: readCssSize(config.padding),
256
- paddingLeft: readCssSize(config.paddingLeft),
257
- paddingRight: readCssSize(config.paddingRight),
258
- paddingTop: readCssSize(config.paddingTop),
259
- paddingBottom: readCssSize(config.paddingBottom),
260
- gap: readCssSize(config.gap),
261
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
235
+ labelEnd: readLayoutLabelEnd(reader),
236
+ initialSlotStates: readInitialSlotStates(reader),
237
+ ...readLayoutPadding(reader),
238
+ gap: readCssSizeField(reader, "gap"),
239
+ anchor: readLayoutAnchor(reader),
262
240
  }, resolvePhiLayoutDefaults("verticalflex"));
263
241
  }
264
242
  export function parsePhiCmsMasonryLayoutConfig(config) {
243
+ const reader = createPhiConfigReader(config);
265
244
  return applyPhiLayoutDefaults({
266
245
  ...readRenderableBlockConfig(config),
267
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
268
- padding: readCssSize(config.padding),
269
- paddingLeft: readCssSize(config.paddingLeft),
270
- paddingRight: readCssSize(config.paddingRight),
271
- paddingTop: readCssSize(config.paddingTop),
272
- paddingBottom: readCssSize(config.paddingBottom),
273
- columns: readGridResponsiveCount(config.columns, isPhiGridColumnCount),
274
- gap: readCssSize(config.gap),
246
+ labelEnd: readLayoutLabelEnd(reader),
247
+ ...readLayoutPadding(reader),
248
+ columns: readGridResponsiveCount(reader, "columns", "a Grid column count", isPhiGridColumnCount),
249
+ gap: readCssSizeField(reader, "gap"),
275
250
  }, resolvePhiLayoutDefaults("masonry"));
276
251
  }
277
252
  export function parsePhiCmsStackLayoutConfig(config) {
253
+ const reader = createPhiConfigReader(config);
278
254
  return applyPhiLayoutDefaults({
279
255
  ...readRenderableBlockConfig(config),
280
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
281
- padding: readCssSize(config.padding),
282
- defaultActiveSlotKey: readString(config.defaultActiveSlotKey),
283
- slotDisplay: config.slotDisplay === "stacked" ? "stacked" : "single",
256
+ labelEnd: readLayoutLabelEnd(reader),
257
+ padding: readCssSizeField(reader, "padding"),
258
+ defaultActiveSlotKey: reader.string("defaultActiveSlotKey"),
259
+ slotDisplay: reader.oneOf("slotDisplay", ["single", "stacked"]) ?? "single",
284
260
  mountPolicy: readPhiCmsMountPolicy(config.mountPolicy, "remount"),
285
- slotTransition: config.slotTransition === "fade-over" ? "fade-over" : "none",
261
+ slotTransition: reader.oneOf("slotTransition", ["none", "fade-over"]) ?? "none",
286
262
  // Absent means "whatever the theme does", which is why the fallback is read from the token
287
263
  // rather than written here as a number.
288
264
  ...(config.slotTransitionDurationMs === undefined ? {} : {
@@ -338,9 +314,9 @@ export function parsePhiCmsCarouselLayoutConfig(config) {
338
314
  const slotGap = readCarouselMember(config.slotGap, PHI_SPACING_SCALE_KEYS, "slotGap");
339
315
  return applyPhiLayoutDefaults({
340
316
  ...readRenderableBlockConfig(config),
341
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
342
- padding: readCssSize(config.padding),
343
- defaultActiveSlotKey: readString(config.defaultActiveSlotKey),
317
+ labelEnd: readLayoutLabelEnd(createPhiConfigReader(config)),
318
+ padding: readCssSizeField(createPhiConfigReader(config), "padding"),
319
+ defaultActiveSlotKey: createPhiConfigReader(config).string("defaultActiveSlotKey"),
344
320
  ...(visibleSlots === undefined ? {} : { visibleSlots }),
345
321
  ...(windowAnchor === undefined ? {} : { windowAnchor }),
346
322
  ...(transition === undefined ? {} : { transition }),
@@ -361,100 +337,81 @@ export function parsePhiCmsCarouselLayoutConfig(config) {
361
337
  ...(lookahead === undefined ? {} : { lookahead }),
362
338
  }, resolvePhiLayoutDefaults("carousel"));
363
339
  }
364
- function readCollapsibleTrigger(value) {
365
- return value === "header" || value === "icon" || value === "disabled" ? value : undefined;
366
- }
367
- function readCollapsibleSize(value) {
368
- return readPhiControlSize(value);
369
- }
370
- function readExpandIconPlacement(value) {
371
- return value === "start" || value === "end" ? value : undefined;
372
- }
373
- function readSlotTexts(value) {
374
- if (!Array.isArray(value)) {
340
+ /** Panel titles or descriptions by slot: texts, an empty one for a slot that has none; trailing empties trimmed. */
341
+ function readSlotTexts(reader, key) {
342
+ const texts = reader.value(key, "a list of texts", (value) => Array.isArray(value) && value.every((candidate) => typeof candidate === "string")
343
+ ? value.map((candidate) => candidate.trim())
344
+ : undefined);
345
+ if (!texts)
375
346
  return undefined;
376
- }
377
- const texts = value.map((candidate) => (typeof candidate === "string" ? candidate.trim() : ""));
378
- while (texts.length > 0 && !texts[texts.length - 1]) {
347
+ while (texts.length > 0 && !texts[texts.length - 1])
379
348
  texts.pop();
380
- }
381
349
  return texts.length > 0 ? texts : undefined;
382
350
  }
383
351
  export function parsePhiCmsCollapsibleLayoutConfig(config) {
352
+ const reader = createPhiConfigReader(config);
353
+ const defaultOpenSlotKeys = reader.stringList("defaultOpenSlotKeys");
384
354
  return applyPhiLayoutDefaults({
385
355
  ...readRenderableBlockConfig(config),
386
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
387
- initialSlotStates: readInitialSlotStates(config.initialSlotStates),
388
- padding: readCssSize(config.padding),
389
- paddingLeft: readCssSize(config.paddingLeft),
390
- paddingRight: readCssSize(config.paddingRight),
391
- paddingTop: readCssSize(config.paddingTop),
392
- paddingBottom: readCssSize(config.paddingBottom),
393
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
394
- panelMinHeight: readPhiLengthValue(config.panelMinHeight) ?? undefined,
395
- accordion: readBoolean(config.accordion),
396
- slotTitles: readSlotTexts(config.slotTitles),
397
- slotDescriptions: readSlotTexts(config.slotDescriptions),
398
- defaultOpenSlotKeys: readStringArray(config.defaultOpenSlotKeys),
399
- collapsible: readCollapsibleTrigger(config.collapsible),
400
- ghost: readBoolean(config.ghost),
401
- expandIconPlacement: readExpandIconPlacement(config.expandIconPlacement),
402
- collapseSize: readCollapsibleSize(config.collapseSize),
403
- titleStrong: readBoolean(config.titleStrong),
404
- headerPadding: readCssSize(config.headerPadding),
405
- innerPadding: readCssSize(config.innerPadding),
356
+ labelEnd: readLayoutLabelEnd(reader),
357
+ initialSlotStates: readInitialSlotStates(reader),
358
+ ...readLayoutPadding(reader),
359
+ anchor: readLayoutAnchor(reader),
360
+ panelMinHeight: readLengthField(reader, "panelMinHeight"),
361
+ accordion: reader.boolean("accordion"),
362
+ slotTitles: readSlotTexts(reader, "slotTitles"),
363
+ slotDescriptions: readSlotTexts(reader, "slotDescriptions"),
364
+ defaultOpenSlotKeys: defaultOpenSlotKeys
365
+ ? [...new Set(defaultOpenSlotKeys.map((key) => key.trim()).filter(Boolean))]
366
+ : undefined,
367
+ collapsible: reader.oneOf("collapsible", ["header", "icon", "disabled"]),
368
+ ghost: reader.boolean("ghost"),
369
+ expandIconPlacement: reader.oneOf("expandIconPlacement", ["start", "end"]),
370
+ collapseSize: reader.value("collapseSize", "a control size", readPhiControlSize),
371
+ titleStrong: reader.boolean("titleStrong"),
372
+ headerPadding: readCssSizeField(reader, "headerPadding"),
373
+ innerPadding: readCssSizeField(reader, "innerPadding"),
406
374
  }, resolvePhiLayoutDefaults("collapsible"));
407
375
  }
408
376
  export function parsePhiCmsGridLayoutConfig(config) {
377
+ const reader = createPhiConfigReader(config);
409
378
  return applyPhiLayoutDefaults({
410
379
  ...readRenderableBlockConfig(config),
411
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
412
- padding: readCssSize(config.padding),
413
- paddingLeft: readCssSize(config.paddingLeft),
414
- paddingRight: readCssSize(config.paddingRight),
415
- paddingTop: readCssSize(config.paddingTop),
416
- paddingBottom: readCssSize(config.paddingBottom),
417
- gap: readCssSize(config.gap),
418
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
419
- columns: readGridResponsiveCount(config.columns, isPhiGridColumnCount),
420
- slotPlacements: Array.isArray(config.slotPlacements)
421
- ? config.slotPlacements
422
- .map((slot, slotIndex) => readGridSlotPlacement(slot, slotIndex))
423
- .filter((slot) => slot !== null)
424
- : undefined,
380
+ labelEnd: readLayoutLabelEnd(reader),
381
+ ...readLayoutPadding(reader),
382
+ gap: readCssSizeField(reader, "gap"),
383
+ anchor: readLayoutAnchor(reader),
384
+ columns: readGridResponsiveCount(reader, "columns", "a Grid column count", isPhiGridColumnCount),
385
+ slotPlacements: reader.list("slotPlacements", readGridSlotPlacement),
425
386
  }, resolvePhiLayoutDefaults("grid"));
426
387
  }
427
388
  export function parsePhiCmsThreeColumnLayoutConfig(config) {
389
+ const reader = createPhiConfigReader(config);
428
390
  return applyPhiLayoutDefaults({
429
391
  ...readRenderableBlockConfig(config),
430
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
431
- padding: readCssSize(config.padding),
432
- paddingLeft: readCssSize(config.paddingLeft),
433
- paddingRight: readCssSize(config.paddingRight),
434
- paddingTop: readCssSize(config.paddingTop),
435
- paddingBottom: readCssSize(config.paddingBottom),
436
- balancedSides: readBoolean(config.balancedSides) ?? true,
437
- gap: readCssSize(config.gap),
438
- anchor: readRenderableBlockAnchorOrPlacement(config.anchor),
439
- align: readString(config.align),
440
- justify: readString(config.justify),
441
- wrap: readBoolean(config.wrap) ??
442
- readString(config.wrap) ??
443
- false,
444
- leftWidth: readPhiLengthValue(config.leftWidth) ?? undefined,
445
- middleWidth: readPhiLengthValue(config.middleWidth) ?? undefined,
446
- rightWidth: readPhiLengthValue(config.rightWidth) ?? undefined,
447
- contentAlign: readString(config.contentAlign),
392
+ labelEnd: readLayoutLabelEnd(reader),
393
+ ...readLayoutPadding(reader),
394
+ balancedSides: reader.boolean("balancedSides") ?? true,
395
+ gap: readCssSizeField(reader, "gap"),
396
+ anchor: readLayoutAnchor(reader),
397
+ align: reader.string("align"),
398
+ justify: reader.string("justify"),
399
+ wrap: readFlexWrap(reader),
400
+ leftWidth: readLengthField(reader, "leftWidth"),
401
+ middleWidth: readLengthField(reader, "middleWidth"),
402
+ rightWidth: readLengthField(reader, "rightWidth"),
403
+ contentAlign: reader.string("contentAlign"),
448
404
  }, resolvePhiLayoutDefaults("threecol"));
449
405
  }
450
406
  export function parsePhiCmsSplitCardLayoutConfig(config) {
407
+ const reader = createPhiConfigReader(config);
451
408
  return applyPhiLayoutDefaults({
452
409
  ...readRenderableBlockConfig(config),
453
- labelEnd: readPhiLayoutLabelEnd(config.labelEnd),
454
- gap: readCssSize(config.gap),
455
- swapRatio: readBoolean(config.swapRatio),
456
- padding: readCssSize(config.padding),
457
- paddingTop: readCssSize(config.paddingTop),
458
- paddingBottom: readCssSize(config.paddingBottom),
410
+ labelEnd: readLayoutLabelEnd(reader),
411
+ gap: readCssSizeField(reader, "gap"),
412
+ swapRatio: reader.boolean("swapRatio"),
413
+ padding: readCssSizeField(reader, "padding"),
414
+ paddingTop: readCssSizeField(reader, "paddingTop"),
415
+ paddingBottom: readCssSizeField(reader, "paddingBottom"),
459
416
  }, resolvePhiLayoutDefaults("split"));
460
417
  }
@@ -101,7 +101,6 @@ export type PhiCmsConfigFieldChoiceMode = "single" | "multiple";
101
101
  export type PhiCmsConfigFieldChoiceValueType = "string" | "string[]" | "number";
102
102
  export type PhiCmsConfigFieldChoiceCreateBehavior = "none" | "accept-custom" | "create-record";
103
103
  export type PhiCmsConfigFieldColorMode = "single" | "gradient" | "both";
104
- export type PhiCmsConfigFieldValueStorage = "field" | "self";
105
104
  export type PhiCmsConfigFieldChoiceFilter = {
106
105
  widgetType?: string;
107
106
  group?: string;
@@ -290,14 +289,6 @@ export type PhiCmsConfigField = (PhiCmsConfigFieldBase & {
290
289
  presentation?: PhiCmsConfigFieldCollectionPresentation;
291
290
  /** What the button that opens the Overlay says. */
292
291
  editLabel?: string;
293
- }) | (PhiCmsConfigFieldBase & {
294
- type: "background";
295
- section?: string;
296
- storage?: PhiCmsConfigFieldValueStorage;
297
- }) | (PhiCmsConfigFieldBase & {
298
- type: "border";
299
- section?: string;
300
- storage?: PhiCmsConfigFieldValueStorage;
301
292
  }) | (PhiCmsConfigFieldBase & {
302
293
  /**
303
294
  * A Surface (`types/surface.ts`) of something the Widget draws inside itself -- each entry of a
@@ -308,7 +299,7 @@ export type PhiCmsConfigField = (PhiCmsConfigFieldBase & {
308
299
  /** Whether the mode is offered; left out, it is. */
309
300
  tone?: false;
310
301
  }) | (PhiCmsConfigFieldBase & {
311
- type: "shadow" | "grid-placement" | "column-count";
302
+ type: "grid-placement" | "column-count";
312
303
  section?: string;
313
304
  });
314
305
  export type PhiCmsLayoutSlotDefinition = {
@@ -575,7 +566,9 @@ export type PhiCmsRuntimeRenderRegistry = {
575
566
  dataProviderDescriptorsByKey: ReadonlyMap<PhiRuntimeDataProviderKey, PhiRuntimeModuleDataProviderDescriptor>;
576
567
  formDefinitionsById: ReadonlyMap<string, PhiRuntimeModuleFormDefinition>;
577
568
  };
578
- export type PhiCmsRenderIssueCode = "missing-module" | "missing-renderer" | "renderer-load-failed" | "render-failed" | "invalid-render-output"
569
+ export type PhiCmsRenderIssueCode = "missing-module" | "missing-renderer" | "renderer-load-failed" | "render-failed"
570
+ /** The stored config holds a value the block cannot read (`PhiConfigError`); the detail names the field. */
571
+ | "invalid-config" | "invalid-render-output"
579
572
  /** A node whose `visibleWhen` reads a feature namespace whose Module could not be asked this render. */
580
573
  | "feature-unavailable"
581
574
  /** A Region or Overlay zone names a root layout node the tree does not contain. */
@@ -18,7 +18,7 @@ import type { PhiTextTone } from "../components/widgets/config/text-tone";
18
18
  import type { PhiCommonControlActionKey } from "../components/widgets/label-types/common-controls";
19
19
  import type { PhiMaskConfig } from "../components/widgets/config/mask";
20
20
  import type { PhiSpacingScaleKey } from "../components/widgets/config/spacing-options";
21
- import type { PhiCardVariant } from "../components/widgets/shared/card-vocabulary";
21
+ import type { PhiCardBillingPeriod, PhiCardVariant } from "../components/widgets/shared/card-vocabulary";
22
22
  import type { PhiControlBadgeConfig, PhiControlConfig } from "../components/widgets/config/control-signal-config";
23
23
  /**
24
24
  * The part of a placement every Widget shares, whatever it is.
@@ -285,10 +285,21 @@ export type PhiCmsCollectionCardPresentation = {
285
285
  actionLabel?: string;
286
286
  actionHref?: string;
287
287
  variant?: PhiCardVariant;
288
- /** Every card in the View draws this body; `stat` needs `value`. */
289
- body?: "text" | "stat";
288
+ /** Every card in the View draws this body; `stat` needs `value`, `plan` a `price`. */
289
+ body?: "text" | "stat" | "plan";
290
290
  /** A field whose truth marks the card as highlighted. */
291
291
  highlight?: string;
292
+ /** The words on the card's band. */
293
+ badge?: string;
294
+ /** A `plan`'s prices and their periods, as a provider answers them -- per country, if it knows one. */
295
+ price?: string;
296
+ pricePeriod?: string;
297
+ priceYearly?: string;
298
+ pricePeriodYearly?: string;
299
+ /** A field holding the plan's features, each `{ text, state? }` (`PhiBulletListEntry`). */
300
+ features?: string;
301
+ /** Which price every plan card of the View shows. Not a path: the View's own choice. */
302
+ billingPeriod?: PhiCardBillingPeriod;
292
303
  };
293
304
  export type PhiCollectionViewWidgetPlacement = PhiWidgetPlacementBase & {
294
305
  presentation?: {
@@ -1,3 +1,4 @@
1
+ import type { PhiRuntimeModuleCatalog } from "./cms-plugins";
1
2
  import type { PhiCmsAreaKey } from "../constants/cms-areas";
2
3
  import type { PhiTimelineEntry } from "@phis/contracts/timeline";
3
4
  import type { PhiAccessViewer, PhiViewerAccessPolicy } from "./access";
@@ -133,6 +134,14 @@ export type PhiDashboardCardContext = {
133
134
  locale: string;
134
135
  area: PhiCmsAreaKey;
135
136
  viewer: PhiAccessViewer;
137
+ /**
138
+ * The Module catalog of the Area the Dashboard stands in.
139
+ *
140
+ * A card about structure -- which Area has an open draft -- has to know the Area shells, and the
141
+ * only catalog it may know is the one it is running in. Importing the first-party catalog instead
142
+ * would compile every Module into the provider (MODULES.md, "The dependency rule").
143
+ */
144
+ runtimeModuleCatalog: PhiRuntimeModuleCatalog;
136
145
  };
137
146
  /**
138
147
  * One Module's answer, loaded on the server and never reached from the browser.
package/types/media.d.ts CHANGED
@@ -69,7 +69,6 @@ export type PhiMediaAssetVariant = {
69
69
  deliveryUrl: string;
70
70
  contentType: string;
71
71
  bytes: number | null;
72
- checksumSha256: string | null;
73
72
  width: number;
74
73
  height: number;
75
74
  blurDataUrl: string | null;
@@ -90,7 +89,6 @@ export type PhiMediaAsset = {
90
89
  contentType: string;
91
90
  kind: PhiMediaKindValue;
92
91
  bytes: number | null;
93
- checksumSha256: string | null;
94
92
  width: number | null;
95
93
  height: number | null;
96
94
  blurDataUrl: string | null;
@@ -127,7 +125,6 @@ export type PhiMediaAssetTile = {
127
125
  width: number | null;
128
126
  height: number | null;
129
127
  bytes: number | null;
130
- checksumSha256: string | null;
131
128
  lifecycleStatus: number;
132
129
  deliveryPolicy: number;
133
130
  deliveryRevision: number;
@@ -48,7 +48,12 @@ export type PhiRenderableBlockTransitionEasing = PhiMotionEasing;
48
48
  export type PhiRenderableBlockTransitionAxis = "x" | "y" | "z";
49
49
  export type PhiRenderableBlockTransitionOrigin = "top left" | "top center" | "top right" | "center left" | "center" | "center right" | "bottom left" | "bottom center" | "bottom right";
50
50
  export type PhiRenderableBlockTransitionTrigger = "on_mount" | "on_ready" | "on_visible" | "on_hover" | "on_focus" | "manual";
51
- export type PhiRenderableBlockViewportEffectAxis = "x" | "y";
51
+ /**
52
+ * Along which axis a viewport effect moves, or about which one it turns. `z` turns flat in the page plane
53
+ * and only means something for `rotate`; a translate along `z` is refused when the config is read. Progress
54
+ * is measured along `x` for `x`, and along the page's own scroll direction for `y` and `z`.
55
+ */
56
+ export type PhiRenderableBlockViewportEffectAxis = "x" | "y" | "z";
52
57
  export type PhiRenderableBlockViewportEffectProperty = "translate" | "opacity" | "rotate" | "scale";
53
58
  export type PhiRenderableBlockViewportEffectRangePoint = "enter" | "center" | "exit";
54
59
  export type PhiRenderableBlockViewportEffectUnit = "px" | "%" | "deg" | "";
@@ -426,6 +426,15 @@ export type PhiTableProviderQueryCapabilities = {
426
426
  filterFields?: readonly string[];
427
427
  sorting?: "none" | "single" | "multiple";
428
428
  pagination?: "none" | "offset" | "cursor";
429
+ /**
430
+ * Whether a query of this resource answers `total`, the rows the whole query has.
431
+ *
432
+ * Counting is a query of its own on a large table, so a paging resource answers it only where it
433
+ * says so here. A Table that shows a count -- `totalRows` or `pageCount` in its footer or summary --
434
+ * needs a resource that counts or does not page; one that pages without a count is paged back and
435
+ * forward. A resource that does not page answers every row at once, and its rows are its count.
436
+ */
437
+ total?: boolean;
429
438
  facets?: readonly string[];
430
439
  expansion?: boolean;
431
440
  };
@@ -260,6 +260,26 @@ export function validatePhiTableProviderFieldValue(field, value, row) {
260
260
  }
261
261
  export function validatePhiTableWidgetBinding(config, resource) {
262
262
  const errors = [];
263
+ /*
264
+ * A count shown needs a count given. The rows of one page are not the query's, so a Table that says
265
+ * how many there are, or how many pages, is bound to a resource that counts -- or one that does not
266
+ * page and so answers every row at once (`query.total`).
267
+ */
268
+ const resourceCounts = resource.query.total === true ||
269
+ (resource.query.pagination !== "offset" && resource.query.pagination !== "cursor");
270
+ if (!resourceCounts) {
271
+ const footer = config.presentation.footer;
272
+ const shownSources = [
273
+ ...(footer?.values ?? []).map((item) => item.value),
274
+ ...(footer?.variants ?? []).flatMap((variant) => [variant.when.value, ...(variant.values ?? []).map((item) => item.value)]),
275
+ ...(config.presentation.summary?.rows ?? []).flatMap((row) => row.cells.map((cell) => cell.item.value)),
276
+ ];
277
+ const counted = shownSources.find((source) => source.source === "core" && (source.fieldKey === "totalRows" || source.fieldKey === "pageCount"));
278
+ if (counted) {
279
+ errors.push(`Table shows "${counted.fieldKey}", but resource "${resource.resourceKey}" pages without a count ` +
280
+ "(declare query.total on a resource that answers total).");
281
+ }
282
+ }
263
283
  const fields = new Set(resource.fields.map((field) => field.key));
264
284
  const fieldsByKey = new Map(resource.fields.map((field) => [field.key, field]));
265
285
  const columns = config.presentation.columns;
package/types.d.ts CHANGED
@@ -18,7 +18,7 @@ export * from "./types/spacing";
18
18
  export * from "./types/runtime-condition";
19
19
  export * from "./types/state-machine";
20
20
  export * from "./types/access";
21
- export type { PhiAuthoringRenderPolicy, PhiCmsBuilderWidgetEditorInteraction, PhiCmsBuilderWidgetPlugin, PhiCmsBuilderWidgetRenderArgs, PhiCmsConfigField, PhiCmsConfigFieldChoiceCreateBehavior, PhiCmsConfigFieldChoiceFilter, PhiCmsConfigFieldChoiceMode, PhiCmsConfigFieldChoicePresentation, PhiCmsConfigFieldChoiceValueType, PhiCmsConfigFieldCollectionPresentation, PhiCmsConfigFieldColorMode, PhiCmsConfigFieldValueStorage, PhiCmsConfigFieldVisibilityRule, PhiCmsLayoutPlugin, PhiCmsLayoutPluginDefinition, PhiCmsLayoutPluginRenderArgs, PhiCmsLayoutSlotDefinition, PhiCmsPluginCommercialMeta, PhiCmsPluginCommercialPlan, PhiCmsPluginLicenseState, PhiCmsPreviewWidgetPlugin, PhiCmsResolvedRequestLoaderArgs, PhiCmsRuntimeWidgetPlugin, PhiCmsServerWidgetPlugin, PhiCmsSiteBridge, PhiCmsSiteRuntime, PhiCmsWidgetAuthoringCanvas, PhiCmsWidgetAuthoringContext, PhiCmsWidgetContentBinding, PhiCmsWidgetPlugin, PhiCmsWidgetPluginDefinition, PhiCmsWidgetPluginRenderArgs, PhiCmsWidgetRuntimeControllerRequirementArgs, PhiCmsWidgetRuntimeControllerRequirementResolver, PhiCmsWidgetSignalSubcontrolCollection, PhiPreviewRenderPolicy, PhiResolvedCmsRequest, PhiRuntimeControllerDefinition, PhiRuntimeControllerFlag, PhiRuntimeControllerMountScope, PhiRuntimeControllerPlugin, PhiRuntimeControllerPreloadMap, PhiRuntimeControllerRenderArgs, PhiRuntimeControllerRequirement, PhiRuntimeControllerServerPreloadArgs, PhiRuntimeControllerSetting, PhiRuntimeModule, PhiRuntimeModuleAuthoringClientProps, PhiRuntimeModuleCalendarAdapterClientDefinition, PhiRuntimeModuleCatalog, PhiRuntimeModuleCatalogEntry, PhiRuntimeModuleClientWidgetDefinition, PhiRuntimeModuleControllerClientProps, PhiRuntimeModuleControllerDescriptor, PhiRuntimeModuleControllerMountPolicy, PhiRuntimeModuleDataProviderClientDefinition, PhiRuntimeModuleDataProviderClientProps, PhiRuntimeModuleDataProviderDescriptor, PhiRuntimeModuleDefinition, PhiRuntimeModuleFeatureContext, PhiRuntimeModuleFeatureResolver, PhiRuntimeModuleFormDefinition, PhiRuntimeModuleLayoutDefinition, PhiRuntimeModuleLoader, PhiRuntimeModuleRenderPolicies, PhiRuntimeModuleUiProvider, PhiRuntimeModuleWidgetDefinition, PhiRuntimeRenderPolicy, } from "./types/cms-plugins";
21
+ export type { PhiAuthoringRenderPolicy, PhiCmsBuilderWidgetEditorInteraction, PhiCmsBuilderWidgetPlugin, PhiCmsBuilderWidgetRenderArgs, PhiCmsConfigField, PhiCmsConfigFieldChoiceCreateBehavior, PhiCmsConfigFieldChoiceFilter, PhiCmsConfigFieldChoiceMode, PhiCmsConfigFieldChoicePresentation, PhiCmsConfigFieldChoiceValueType, PhiCmsConfigFieldCollectionPresentation, PhiCmsConfigFieldColorMode, PhiCmsConfigFieldVisibilityRule, PhiCmsLayoutPlugin, PhiCmsLayoutPluginDefinition, PhiCmsLayoutPluginRenderArgs, PhiCmsLayoutSlotDefinition, PhiCmsPluginCommercialMeta, PhiCmsPluginCommercialPlan, PhiCmsPluginLicenseState, PhiCmsPreviewWidgetPlugin, PhiCmsResolvedRequestLoaderArgs, PhiCmsRuntimeWidgetPlugin, PhiCmsServerWidgetPlugin, PhiCmsSiteBridge, PhiCmsSiteRuntime, PhiCmsWidgetAuthoringCanvas, PhiCmsWidgetAuthoringContext, PhiCmsWidgetContentBinding, PhiCmsWidgetPlugin, PhiCmsWidgetPluginDefinition, PhiCmsWidgetPluginRenderArgs, PhiCmsWidgetRuntimeControllerRequirementArgs, PhiCmsWidgetRuntimeControllerRequirementResolver, PhiCmsWidgetSignalSubcontrolCollection, PhiPreviewRenderPolicy, PhiResolvedCmsRequest, PhiRuntimeControllerDefinition, PhiRuntimeControllerFlag, PhiRuntimeControllerMountScope, PhiRuntimeControllerPlugin, PhiRuntimeControllerPreloadMap, PhiRuntimeControllerRenderArgs, PhiRuntimeControllerRequirement, PhiRuntimeControllerServerPreloadArgs, PhiRuntimeControllerSetting, PhiRuntimeModule, PhiRuntimeModuleAuthoringClientProps, PhiRuntimeModuleCalendarAdapterClientDefinition, PhiRuntimeModuleCatalog, PhiRuntimeModuleCatalogEntry, PhiRuntimeModuleClientWidgetDefinition, PhiRuntimeModuleControllerClientProps, PhiRuntimeModuleControllerDescriptor, PhiRuntimeModuleControllerMountPolicy, PhiRuntimeModuleDataProviderClientDefinition, PhiRuntimeModuleDataProviderClientProps, PhiRuntimeModuleDataProviderDescriptor, PhiRuntimeModuleDefinition, PhiRuntimeModuleFeatureContext, PhiRuntimeModuleFeatureResolver, PhiRuntimeModuleFormDefinition, PhiRuntimeModuleLayoutDefinition, PhiRuntimeModuleLoader, PhiRuntimeModuleRenderPolicies, PhiRuntimeModuleUiProvider, PhiRuntimeModuleWidgetDefinition, PhiRuntimeRenderPolicy, } from "./types/cms-plugins";
22
22
  export type * from "./types/cms-presets";
23
23
  export type * from "./types/slot-size-policy";
24
24
  export type * from "./types/runtime-data-provider";