@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,7 +4,7 @@ import { PHI_COMMON_CONTROL_DEFAULT_LABELS, readPhiCommonControlActionKey, } fro
4
4
  import { PHI_BUTTON_VARIANT_FIELD_OPTIONS, readPhiButtonVariant } from "./button-variant";
5
5
  import { createPhiCommonActionField, readPhiControlLabel } from "./common-action-field";
6
6
  import { PHI_CONTROL_BADGE_FIELDS, parsePhiControlBadgeConfig } from "./control-signal-config";
7
- import { readBoolean, readNumber, readString } from "./parser-primitives";
7
+ import { createPhiConfigReader } from "./strict-config-reader";
8
8
  /*
9
9
  * The buttons of a Widget that holds several: the Command Toolbar's row, and the Float Button's corner.
10
10
  *
@@ -12,102 +12,59 @@ import { readBoolean, readNumber, readString } from "./parser-primitives";
12
12
  * emit, and the Signals panel wires both. The two Widgets differ in how the buttons stand, never in what
13
13
  * a button is, so the parser, the Inspector fields, the viewer filter and the scaffold's count are one.
14
14
  */
15
+ /** What a button sends: a text, a number, true/false, a list of texts or of numbers, or an object. */
15
16
  function readButtonEmitValue(value) {
16
- const stringValue = readString(value);
17
- if (stringValue !== undefined) {
18
- return stringValue;
19
- }
20
- const numberValue = readNumber(value);
21
- if (numberValue !== undefined) {
22
- return numberValue;
23
- }
24
- const booleanValue = readBoolean(value);
25
- if (booleanValue !== undefined) {
26
- return booleanValue;
27
- }
28
- if (value == null) {
29
- return undefined;
30
- }
17
+ if (typeof value === "string" || typeof value === "boolean")
18
+ return value;
19
+ if (typeof value === "number")
20
+ return Number.isFinite(value) ? value : undefined;
31
21
  if (Array.isArray(value)) {
32
- const stringItems = value.filter((item) => typeof item === "string");
33
- if (stringItems.length === value.length) {
34
- return stringItems;
35
- }
36
- const numberItems = value.filter((item) => typeof item === "number" && Number.isFinite(item));
37
- if (numberItems.length === value.length) {
38
- return numberItems;
39
- }
22
+ if (value.every((item) => typeof item === "string"))
23
+ return value;
24
+ if (value.every((item) => typeof item === "number" && Number.isFinite(item)))
25
+ return value;
40
26
  return undefined;
41
27
  }
42
- if (typeof value === "object") {
43
- return value;
44
- }
45
- return undefined;
28
+ return isPhiRecord(value) ? value : undefined;
46
29
  }
47
- function readCommandButtonEmitConfig(value) {
48
- if (!isPhiRecord(value)) {
49
- return null;
50
- }
51
- const record = value;
52
- const capabilityId = readString(record.capabilityId);
53
- if (!capabilityId) {
54
- return null;
55
- }
30
+ function readCommandButtonEmitConfig(reader) {
56
31
  return {
57
- capabilityId,
58
- value: readButtonEmitValue(record.value),
32
+ capabilityId: reader.string("capabilityId") ?? reader.fail("capabilityId", "a capability id"),
33
+ value: reader.value("value", "a text, number, true/false, list or object", readButtonEmitValue),
59
34
  };
60
35
  }
61
- function readCommandButtonEmits(value) {
62
- if (!Array.isArray(value)) {
63
- return [];
64
- }
65
- return value
66
- .map(readCommandButtonEmitConfig)
67
- .filter((item) => item != null);
68
- }
69
- function readCommandButtonConfig(value) {
70
- if (!isPhiRecord(value)) {
71
- return null;
72
- }
73
- const record = value;
74
- const key = readString(record.key);
75
- const emits = readCommandButtonEmits(record.emits);
76
- if (!key || emits.length === 0) {
77
- return null;
78
- }
79
- const hasAccessPolicy = Object.prototype.hasOwnProperty.call(record, "accessPolicy");
80
- const accessPolicy = hasAccessPolicy
81
- ? readPhiViewerAccessPolicy(record.accessPolicy)
36
+ function readCommandButtonConfig(reader) {
37
+ const key = reader.string("key") ?? reader.fail("key", "a text");
38
+ const emits = reader.list("emits", readCommandButtonEmitConfig) ?? [];
39
+ if (emits.length === 0)
40
+ reader.fail("emits", "at least one emit");
41
+ const accessPolicy = Object.prototype.hasOwnProperty.call(reader.record, "accessPolicy")
42
+ ? readPhiViewerAccessPolicy(reader.record.accessPolicy) ?? reader.fail("accessPolicy", "an access policy")
82
43
  : null;
83
- if (hasAccessPolicy && !accessPolicy) {
84
- return null;
85
- }
86
44
  return {
87
45
  key,
88
46
  emits,
89
47
  ...(accessPolicy ? { accessPolicy } : {}),
90
- ...parsePhiControlBadgeConfig(record),
91
- action: readPhiCommonControlActionKey(readString(record.action)) ?? undefined,
92
- label: readPhiControlLabel(record.label),
93
- tooltip: readString(record.tooltip),
94
- icon: readString(record.icon),
95
- display: record.display === "icon" || record.display === "label" || record.display === "icon-label"
96
- ? record.display
97
- : undefined,
98
- danger: readBoolean(record.danger),
99
- disabled: readBoolean(record.disabled),
100
- readOnly: readBoolean(record.readOnly),
101
- variant: readPhiButtonVariant(record.variant),
48
+ ...parsePhiControlBadgeConfig(reader.record, reader.path),
49
+ action: reader.value("action", "a common action", (value) => typeof value === "string" ? readPhiCommonControlActionKey(value) ?? undefined : undefined),
50
+ label: readPhiControlLabel(reader.record.label),
51
+ tooltip: reader.string("tooltip"),
52
+ icon: reader.string("icon"),
53
+ display: reader.oneOf("display", ["icon", "label", "icon-label"]),
54
+ danger: reader.boolean("danger"),
55
+ disabled: reader.boolean("disabled"),
56
+ readOnly: reader.boolean("readOnly"),
57
+ variant: reader.value("variant", "a button variant", readPhiButtonVariant),
102
58
  };
103
59
  }
104
- export function readPhiCommandButtons(value) {
105
- if (!Array.isArray(value)) {
60
+ /**
61
+ * A Widget's buttons. Strict (`strict-config-reader.ts`): a button without a key or without an emit is
62
+ * an error naming it, not a button left out.
63
+ */
64
+ export function readPhiCommandButtons(value, path = "buttons") {
65
+ if (value === undefined || value === null)
106
66
  return [];
107
- }
108
- return value
109
- .map(readCommandButtonConfig)
110
- .filter((item) => item != null);
67
+ return createPhiConfigReader({ [path]: value }).list(path, readCommandButtonConfig) ?? [];
111
68
  }
112
69
  export function filterPhiCommandButtonsForViewer(buttons, viewer, ownerProviderId) {
113
70
  return buttons.filter((button) => canPhiViewerAccessOwnedPolicy(viewer, button.accessPolicy, ownerProviderId));
@@ -143,4 +143,6 @@ export declare function parsePhiControlSignalConfig(config: Record<string, unkno
143
143
  export declare function parsePhiControlStateConfig(config: Record<string, unknown>, defaults: PhiControlSignalConfigDefaults): Required<Pick<PhiControlStateConfig, "key">> & Pick<PhiControlStateConfig, "signalRoutes" | "readOnly" | "disabled">;
144
144
  export declare function parsePhiControlPresentationConfig<TSize extends PhiControlSize = PhiControlSize>(config: Record<string, unknown>, allowedSizes?: readonly TSize[]): PhiControlPresentationConfig<TSize>;
145
145
  export declare function parsePhiControlConfig<TSize extends PhiControlSize = PhiControlSize>(config: Record<string, unknown>, defaults: PhiControlSignalConfigDefaults, allowedSizes?: readonly TSize[]): Required<Pick<PhiControlConfig<TSize>, "key">> & Pick<PhiControlConfig<TSize>, "signalRoutes" | "readOnly" | "disabled" | "controlSize">;
146
- export declare function parsePhiControlBadgeConfig(config: Record<string, unknown>): PhiControlBadgeConfig;
146
+ export declare function parsePhiControlBadgeConfig(config: Record<string, unknown>,
147
+ /** Where the config stands, when it is an entry of a list -- a button of a toolbar. */
148
+ path?: string): PhiControlBadgeConfig;
@@ -1,6 +1,6 @@
1
1
  import { readPhiSignalRouteSet, } from "../../../types/signals";
2
- import { PHI_CONTROL_SIZES, readPhiControlSize, } from "../../../types/control";
3
- import { readBoolean, readNumber, readString } from "./parser-primitives";
2
+ import { PHI_CONTROL_SIZES, } from "../../../types/control";
3
+ import { createPhiConfigReader } from "./strict-config-reader";
4
4
  export const PHI_CONTROL_READ_ONLY_FIELD = {
5
5
  key: "readOnly",
6
6
  type: "boolean",
@@ -83,23 +83,22 @@ export const PHI_CONTROL_BADGE_FIELDS = [
83
83
  export function parsePhiControlSignalConfig(config, defaults) {
84
84
  return {
85
85
  signalRoutes: readPhiSignalRouteSet(config.signalRoutes),
86
- key: readString(config.key) ?? defaults.key,
86
+ key: createPhiConfigReader(config).string("key") ?? defaults.key,
87
87
  };
88
88
  }
89
89
  export function parsePhiControlStateConfig(config, defaults) {
90
+ const reader = createPhiConfigReader(config);
90
91
  return {
91
92
  ...parsePhiControlSignalConfig(config, defaults),
92
- readOnly: readBoolean(config.readOnly),
93
- disabled: readBoolean(config.disabled),
93
+ readOnly: reader.boolean("readOnly"),
94
+ disabled: reader.boolean("disabled"),
94
95
  };
95
96
  }
96
97
  export function parsePhiControlPresentationConfig(config, allowedSizes) {
97
- const controlSize = readPhiControlSize(config.controlSize);
98
- const resolvedAllowedSizes = allowedSizes ?? PHI_CONTROL_SIZES;
98
+ // A size this Control does not offer is an error, not the default in its place.
99
+ const resolvedAllowedSizes = (allowedSizes ?? PHI_CONTROL_SIZES);
99
100
  return {
100
- controlSize: controlSize != null && resolvedAllowedSizes.includes(controlSize)
101
- ? controlSize
102
- : undefined,
101
+ controlSize: createPhiConfigReader(config).oneOf("controlSize", resolvedAllowedSizes),
103
102
  };
104
103
  }
105
104
  export function parsePhiControlConfig(config, defaults, allowedSizes) {
@@ -113,13 +112,16 @@ export function parsePhiControlConfig(config, defaults, allowedSizes) {
113
112
  controlSize: controlSize ?? ((allowedSizes ?? PHI_CONTROL_SIZES).includes("medium") ? "medium" : undefined),
114
113
  };
115
114
  }
116
- export function parsePhiControlBadgeConfig(config) {
115
+ export function parsePhiControlBadgeConfig(config,
116
+ /** Where the config stands, when it is an entry of a list -- a button of a toolbar. */
117
+ path = "") {
118
+ const reader = createPhiConfigReader(config, path);
117
119
  return {
118
- badgeEnabled: readBoolean(config.badgeEnabled),
119
- badgeText: readString(config.badgeText),
120
- badgeCount: readNumber(config.badgeCount),
121
- badgeShowZero: readBoolean(config.badgeShowZero),
122
- badgeOverflowCount: readNumber(config.badgeOverflowCount),
123
- badgeColor: readString(config.badgeColor),
120
+ badgeEnabled: reader.boolean("badgeEnabled"),
121
+ badgeText: reader.string("badgeText"),
122
+ badgeCount: reader.number("badgeCount"),
123
+ badgeShowZero: reader.boolean("badgeShowZero"),
124
+ badgeOverflowCount: reader.number("badgeOverflowCount"),
125
+ badgeColor: reader.string("badgeColor"),
124
126
  };
125
127
  }
@@ -1,2 +1,3 @@
1
1
  import type { PhiMediaImageSourceConfig } from "../../../types/media";
2
+ /** Where an image comes from: an Asset of the Media library, or a URL. Strict: an unknown kind is an error. */
2
3
  export declare function readPhiMediaImageSourceConfig(value: Record<string, unknown>): PhiMediaImageSourceConfig;
@@ -1,19 +1,18 @@
1
1
  import { normalizePhiImageAssetVariantKey } from "../../../constants/media";
2
- import { readNumber, readString } from "./parser-primitives";
3
- function readPhiImageSourceKind(value) {
4
- return readString(value) === "asset" ? "asset" : "url";
5
- }
2
+ import { createPhiConfigReader } from "./strict-config-reader";
3
+ /** Where an image comes from: an Asset of the Media library, or a URL. Strict: an unknown kind is an error. */
6
4
  export function readPhiMediaImageSourceConfig(value) {
7
- if (readPhiImageSourceKind(value.sourceKind) === "asset") {
5
+ const reader = createPhiConfigReader(value);
6
+ if ((reader.oneOf("sourceKind", ["url", "asset"]) ?? "url") === "asset") {
8
7
  return {
9
8
  sourceKind: "asset",
10
- assetId: readNumber(value.assetId),
9
+ assetId: reader.number("assetId"),
11
10
  variantKey: normalizePhiImageAssetVariantKey(value.variantKey),
12
- variantVersion: readNumber(value.variantVersion),
11
+ variantVersion: reader.number("variantVersion"),
13
12
  };
14
13
  }
15
14
  return {
16
15
  sourceKind: "url",
17
- sourceUrl: readString(value.sourceUrl),
16
+ sourceUrl: reader.string("sourceUrl"),
18
17
  };
19
18
  }
@@ -24,7 +24,12 @@ export declare const PHI_MASK_PRESET_OPTIONS: ReadonlyArray<{
24
24
  label: string;
25
25
  }>;
26
26
  export declare function resolvePhiMaskStyle(mask: PhiMaskConfig | null | undefined): CSSProperties | undefined;
27
- export declare function normalizePhiMaskConfig(value: unknown): PhiMaskConfig | undefined;
27
+ /**
28
+ * A stored mask, read strictly (`strict-config-reader.ts`) before it is normalized: a field that is
29
+ * there and of the wrong kind is an error naming it. The normalization below stays lenient for the
30
+ * editor, which normalizes drafts while an author is still choosing.
31
+ */
32
+ export declare function readPhiMaskConfig(value: unknown, path?: string): PhiMaskConfig | undefined;
28
33
  export declare function mergePhiMaskConfigDefaults(mask: PhiMaskConfig | null | undefined): PhiMaskConfig;
29
34
  /** A picker's change on top of the current mask: the defaults filled, the patch applied, the mask enabled. */
30
35
  export declare function mergePhiMaskConfigPatch(mask: PhiMaskConfig | null | undefined, patch: PhiMaskConfig): PhiMaskConfig;
@@ -1,5 +1,6 @@
1
1
  import { isPhiRecord } from "../../../helpers/is-record";
2
2
  import { readBoolean, readCssSize, readNumber, readString } from "./parser-primitives";
3
+ import { createPhiConfigReader } from "./strict-config-reader";
3
4
  /** The preset shapes and their names, in the order a picker lists them. */
4
5
  const PHI_MASK_PRESET_ENTRIES = [
5
6
  ["circle", "Circle"],
@@ -129,7 +130,29 @@ function readMaskRotation(value) {
129
130
  }
130
131
  return Math.min(180, Math.max(-180, rotation));
131
132
  }
132
- export function normalizePhiMaskConfig(value) {
133
+ /**
134
+ * A stored mask, read strictly (`strict-config-reader.ts`) before it is normalized: a field that is
135
+ * there and of the wrong kind is an error naming it. The normalization below stays lenient for the
136
+ * editor, which normalizes drafts while an author is still choosing.
137
+ */
138
+ export function readPhiMaskConfig(value, path = "mask") {
139
+ createPhiConfigReader({ [path]: value }).object(path, (reader) => {
140
+ reader.boolean("enabled");
141
+ reader.oneOf("source", ["asset", "preset"]);
142
+ reader.oneOf("preset", [...PHI_MASK_PRESETS]);
143
+ reader.number("assetId");
144
+ reader.string("assetUrl");
145
+ reader.number("scale");
146
+ reader.number("offsetX");
147
+ reader.number("offsetY");
148
+ reader.number("rotationDeg");
149
+ reader.value("position", "a CSS position", (position) => typeof position === "string" || (typeof position === "number" && Number.isFinite(position)) ? position : undefined);
150
+ reader.oneOf("repeat", ["repeat", "no-repeat"]);
151
+ reader.oneOf("mode", ["luminance", "alpha"]);
152
+ });
153
+ return normalizePhiMaskConfig(value);
154
+ }
155
+ function normalizePhiMaskConfig(value) {
133
156
  if (!isPhiRecord(value)) {
134
157
  return undefined;
135
158
  }
@@ -1,5 +1,10 @@
1
1
  import { isPhiRecord } from "../../../helpers/is-record";
2
2
  import { mergePhiCmsRenderableBlockConfigDefaults } from "../../../helpers/cms-config-serialization";
3
+ import { readPhiViewerAccessPolicy } from "../../../types/access";
4
+ import { createPhiConfigReader } from "./strict-config-reader";
5
+ import { PHI_BACKGROUND_FILTERS, readPhiShadow } from "../../../types/layout-style";
6
+ import { PHI_CMS_BORDER_SOURCES } from "../../../types/cms-border-source";
7
+ import { PHI_SURFACE_TONES } from "../../../types/surface";
3
8
  export function readString(value) {
4
9
  return typeof value === "string" && value.trim() ? value : undefined;
5
10
  }
@@ -22,7 +27,74 @@ export function readNumberList(value) {
22
27
  export function readCssSize(value) {
23
28
  return readNumber(value) ?? readString(value);
24
29
  }
30
+ /*
31
+ * The fields every block carries, checked strictly before they are normalized
32
+ * (`strict-config-reader.ts`): a render mode, visibility or size of the wrong kind is an error naming the
33
+ * field, not the default put in its place. The check is one level deep -- a surface, a size or the
34
+ * effects must be what they claim to be; what is inside them is still normalized as before.
35
+ *
36
+ * The normalization itself stays lenient, because the Builder runs it on drafts while an author edits.
37
+ * This reader is what a stored config is read with: every Widget's `parseConfig` and the Layouts'.
38
+ */
39
+ /*
40
+ * A Surface, one level into each of its parts: a background's base, overlay, filter and motion are what
41
+ * they claim to be, a border is an object of the border's fields, a shadow is one the Theme knows. What is
42
+ * inside a gradient's stops or an overlay's pattern is still normalized as before.
43
+ */
44
+ function assertPhiSurfaceConfig(surface) {
45
+ const cssSize = (value) => (typeof value === "number" && Number.isFinite(value)) || typeof value === "string" ? value : undefined;
46
+ surface.object("background", (background) => {
47
+ background.value("base", "a background base: a CSS value or an object with a kind", (value) => typeof value === "string" ||
48
+ (isPhiRecord(value) && ["color", "gradient", "image", "none"].includes(String(value.kind)))
49
+ ? value
50
+ : undefined);
51
+ background.value("overlay", "a background overlay", (value) => (isPhiRecord(value) ? value : undefined));
52
+ background.oneOf("filter", PHI_BACKGROUND_FILTERS);
53
+ background.value("motion", "a background motion", (value) => (isPhiRecord(value) ? value : undefined));
54
+ });
55
+ surface.oneOf("borderSource", PHI_CMS_BORDER_SOURCES);
56
+ surface.object("border", (border) => {
57
+ border.number("borderWidth");
58
+ border.string("borderStyle");
59
+ border.string("borderColor");
60
+ for (const corner of ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]) {
61
+ border.value(corner, "a CSS size", cssSize);
62
+ }
63
+ });
64
+ surface.value("shadow", "a shadow", readPhiShadow);
65
+ surface.oneOf("tone", PHI_SURFACE_TONES);
66
+ }
67
+ function assertPhiRenderableBlockBaseConfig(config) {
68
+ const reader = createPhiConfigReader(config);
69
+ const sizeLike = (value) => isPhiRecord(value) || typeof value === "string" || (typeof value === "number" && Number.isFinite(value))
70
+ ? value
71
+ : undefined;
72
+ reader.oneOf("renderMode", ["live", "preview", "editor"]);
73
+ reader.oneOf("visibility", ["visible", "hidden", "collapsed"]);
74
+ reader.value("accessPolicy", "an access policy", (value) => readPhiViewerAccessPolicy(value) ?? undefined);
75
+ reader.value("viewportFlags", "a whole number from 0 to 6", (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 6 ? value : undefined);
76
+ reader.boolean("enabled");
77
+ reader.boolean("debugMode");
78
+ reader.value("anchor", "an anchor", (value) => (isPhiRecord(value) || typeof value === "string" ? value : undefined));
79
+ reader.integer("zIndex");
80
+ reader.number("opacity");
81
+ reader.object("surface", assertPhiSurfaceConfig);
82
+ reader.string("className");
83
+ reader.value("size", "a size", sizeLike);
84
+ reader.value("minSize", "a size", sizeLike);
85
+ reader.value("maxSize", "a size", sizeLike);
86
+ reader.value("collapsedSizeHint", "a size", sizeLike);
87
+ reader.object("effects", (effects) => {
88
+ effects.list("viewportEffects", (effect) => {
89
+ // `z` turns flat in the page plane; nothing moves or fades along it.
90
+ if (effect.record.axis === "z" && effect.record.property !== "rotate") {
91
+ effect.fail("axis", '"x" or "y" ("z" only turns a "rotate")');
92
+ }
93
+ });
94
+ });
95
+ }
25
96
  export function readRenderableBlockConfig(config) {
97
+ assertPhiRenderableBlockBaseConfig(config);
26
98
  return mergePhiCmsRenderableBlockConfigDefaults(config);
27
99
  }
28
100
  export function readNavItems(value) {
@@ -1,4 +1,4 @@
1
- import { type PhiCmsWidgetConfigBase } from "./parser-primitives";
1
+ import type { PhiCmsWidgetConfigBase } from "./parser-primitives";
2
2
  import { type PhiStackChoiceControlConfig } from "./choice-shared";
3
3
  export type PhiCmsTabBarWidgetConfig = PhiCmsWidgetConfigBase & PhiStackChoiceControlConfig & {
4
4
  key?: string;
@@ -1,12 +1,11 @@
1
- import { readString } from "./parser-primitives";
1
+ import { createPhiConfigReader } from "./strict-config-reader";
2
2
  import { PHI_CHOICE_CONTROL_OPTION_FIELDS, PHI_CHOICE_CONTROL_CONFIG_FIELDS, PHI_CHOICE_CONTROL_VALUE_MODE_FIELD, parsePhiStackChoiceControlConfig, } from "./choice-shared";
3
3
  import { createPhiCmsWidgetDefinition } from "./helpers";
4
4
  import { PHI_SELECT_CONTROL_SIGNALS } from "../signals/control-signal-capabilities";
5
5
  function parsePhiCmsTabBarWidgetConfig(config) {
6
- const placement = readString(config.placement);
7
6
  return {
8
7
  ...parsePhiStackChoiceControlConfig(config),
9
- placement: placement === "bottom" || placement === "left" || placement === "right" ? placement : "top",
8
+ placement: createPhiConfigReader(config).oneOf("placement", ["top", "bottom", "left", "right"]) ?? "top",
10
9
  };
11
10
  }
12
11
  export const PHI_TAB_BAR_WIDGET_DEFINITION = createPhiCmsWidgetDefinition({
@@ -0,0 +1,40 @@
1
+ export declare class PhiConfigError extends Error {
2
+ readonly path: string;
3
+ readonly expected: string;
4
+ readonly received: unknown;
5
+ constructor(path: string, expected: string, received: unknown);
6
+ }
7
+ export declare function isPhiConfigError(error: unknown): error is PhiConfigError;
8
+ /** The render issue code for an error: a config nobody can read is said as such, not as a crash. */
9
+ export declare function resolvePhiRenderFailureCode(error: unknown): "invalid-config" | "render-failed";
10
+ export type PhiConfigReader = {
11
+ /** Where this reader stands, for nested readers and errors. */
12
+ readonly path: string;
13
+ readonly record: Readonly<Record<string, unknown>>;
14
+ has(key: string): boolean;
15
+ string(key: string): string | undefined;
16
+ number(key: string): number | undefined;
17
+ integer(key: string): number | undefined;
18
+ boolean(key: string): boolean | undefined;
19
+ /** One of a closed list of values. */
20
+ oneOf<const T extends string | number>(key: string, values: readonly T[]): T | undefined;
21
+ /** A value a reader of the caller's checks; `read` answers `undefined` for a value it does not accept. */
22
+ value<T>(key: string, expected: string, read: (value: unknown) => T | undefined): T | undefined;
23
+ /** A nested object, read by a reader of its own. */
24
+ object<T>(key: string, read: (reader: PhiConfigReader) => T): T | undefined;
25
+ /** A list of objects, each read by a reader of its own. */
26
+ list<T>(key: string, read: (reader: PhiConfigReader, index: number) => T): T[] | undefined;
27
+ /** A list of texts. */
28
+ stringList(key: string): string[] | undefined;
29
+ /** An error at a field of this reader, for a rule the field types cannot state. */
30
+ fail(key: string, expected: string, received?: unknown): never;
31
+ };
32
+ export declare function createPhiConfigReader(record: Readonly<Record<string, unknown>>, path?: string): PhiConfigReader;
33
+ /**
34
+ * The same strict reader over a server's answer. `source` names who answered (`"The media listing"`),
35
+ * so the error says whose shape it was. A value is read as in a config: absent is allowed and reads as
36
+ * absent, present and wrong throws. What the answer must carry, the caller asks for with `required`.
37
+ */
38
+ export declare function createPhiResponseReader(value: unknown, source: string, path?: string): PhiConfigReader;
39
+ /** A field the record must carry: what `read` answers, or the reader's error for the field. */
40
+ export declare function requirePhiField<T>(reader: PhiConfigReader, key: string, expected: string, read: (key: string) => T | undefined): T;
@@ -0,0 +1,174 @@
1
+ import { isPhiRecord } from "../../../helpers/is-record";
2
+ /*
3
+ * Reading a Widget's or Layout's stored config strictly.
4
+ *
5
+ * A value that is absent -- `undefined`, `null`, or an empty text -- is allowed and reads as absent; the
6
+ * parser decides what absent means. A value that is present and wrong is an error, never a silent
7
+ * `undefined` or a default put in its place: a config that says something the Widget cannot read is a
8
+ * config somebody has to correct, and they can only do that if it is said.
9
+ *
10
+ * The error names where it is (`columns[2].sizing.width`), what was expected and what was found. It is
11
+ * thrown from `parseConfig`; the renderer catches it per block (`phi-cms-layout-renderer.tsx`), so a live
12
+ * page leaves the one Widget out and logs it, while the Builder and the preview show it in the Widget's
13
+ * place, where the field can be corrected.
14
+ */
15
+ export class PhiConfigError extends Error {
16
+ path;
17
+ expected;
18
+ received;
19
+ constructor(path, expected, received) {
20
+ super(`Config field "${path}" expects ${expected}, found ${describePhiConfigValue(received)}.`);
21
+ this.name = "PhiConfigError";
22
+ this.path = path;
23
+ this.expected = expected;
24
+ this.received = received;
25
+ }
26
+ }
27
+ export function isPhiConfigError(error) {
28
+ return error instanceof PhiConfigError;
29
+ }
30
+ /** The render issue code for an error: a config nobody can read is said as such, not as a crash. */
31
+ export function resolvePhiRenderFailureCode(error) {
32
+ return error instanceof PhiConfigError || (error instanceof Error && error.name === "PhiConfigError")
33
+ ? "invalid-config"
34
+ : "render-failed";
35
+ }
36
+ function describePhiConfigValue(value) {
37
+ if (value === undefined)
38
+ return "nothing";
39
+ if (value === null)
40
+ return "null";
41
+ if (Array.isArray(value))
42
+ return "a list";
43
+ if (isPhiRecord(value))
44
+ return "an object";
45
+ if (typeof value === "string")
46
+ return `the text ${JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value)}`;
47
+ return `${typeof value} ${String(value)}`;
48
+ }
49
+ function isAbsent(value) {
50
+ return value === undefined || value === null || (typeof value === "string" && value.trim() === "");
51
+ }
52
+ /**
53
+ * A field a reader found wrong, in a record that did not come from a stored config: a server's answer.
54
+ *
55
+ * The same reading, said as what it is. A response the client cannot read is the server's or the
56
+ * contract's fault, not something an author can correct, so it is not a `PhiConfigError` and no
57
+ * Builder offers to fix it.
58
+ */
59
+ class PhiResponseShapeError extends Error {
60
+ source;
61
+ path;
62
+ expected;
63
+ received;
64
+ constructor(source, path, expected, received) {
65
+ super(`${source} answered "${path}" with ${describePhiConfigValue(received)}, expected ${expected}.`);
66
+ this.name = "PhiResponseShapeError";
67
+ this.source = source;
68
+ this.path = path;
69
+ this.expected = expected;
70
+ this.received = received;
71
+ }
72
+ }
73
+ export function createPhiConfigReader(record, path = "") {
74
+ return createPhiStrictReader(record, path, (at, expected, received) => new PhiConfigError(at, expected, received));
75
+ }
76
+ /**
77
+ * The same strict reader over a server's answer. `source` names who answered (`"The media listing"`),
78
+ * so the error says whose shape it was. A value is read as in a config: absent is allowed and reads as
79
+ * absent, present and wrong throws. What the answer must carry, the caller asks for with `required`.
80
+ */
81
+ export function createPhiResponseReader(value, source, path = "") {
82
+ if (!isPhiRecord(value)) {
83
+ throw new PhiResponseShapeError(source, path || "(answer)", "an object", value);
84
+ }
85
+ return createPhiStrictReader(value, path, (at, expected, received) => new PhiResponseShapeError(source, at, expected, received));
86
+ }
87
+ /** A field the record must carry: what `read` answers, or the reader's error for the field. */
88
+ export function requirePhiField(reader, key, expected, read) {
89
+ const value = read(key);
90
+ return value === undefined ? reader.fail(key, expected) : value;
91
+ }
92
+ function createPhiStrictReader(record, path, makeError) {
93
+ const at = (key) => (path ? `${path}.${key}` : key);
94
+ const fail = (key, expected, received = record[key]) => {
95
+ throw makeError(at(key), expected, received);
96
+ };
97
+ const reader = {
98
+ path,
99
+ record,
100
+ has: (key) => !isAbsent(record[key]),
101
+ string(key) {
102
+ const value = record[key];
103
+ if (isAbsent(value))
104
+ return undefined;
105
+ return typeof value === "string" ? value : fail(key, "a text");
106
+ },
107
+ number(key) {
108
+ const value = record[key];
109
+ if (isAbsent(value))
110
+ return undefined;
111
+ return typeof value === "number" && Number.isFinite(value) ? value : fail(key, "a number");
112
+ },
113
+ integer(key) {
114
+ const value = record[key];
115
+ if (isAbsent(value))
116
+ return undefined;
117
+ return typeof value === "number" && Number.isInteger(value) ? value : fail(key, "a whole number");
118
+ },
119
+ boolean(key) {
120
+ const value = record[key];
121
+ if (isAbsent(value))
122
+ return undefined;
123
+ return typeof value === "boolean" ? value : fail(key, "true or false");
124
+ },
125
+ oneOf(key, values) {
126
+ const value = record[key];
127
+ if (isAbsent(value))
128
+ return undefined;
129
+ return values.includes(value)
130
+ ? value
131
+ : fail(key, `one of ${values.map((entry) => JSON.stringify(entry)).join(", ")}`);
132
+ },
133
+ value(key, expected, read) {
134
+ const value = record[key];
135
+ if (isAbsent(value))
136
+ return undefined;
137
+ const result = read(value);
138
+ return result === undefined ? fail(key, expected) : result;
139
+ },
140
+ object(key, read) {
141
+ const value = record[key];
142
+ if (isAbsent(value))
143
+ return undefined;
144
+ return isPhiRecord(value) ? read(createPhiStrictReader(value, at(key), makeError)) : fail(key, "an object");
145
+ },
146
+ list(key, read) {
147
+ const value = record[key];
148
+ if (isAbsent(value))
149
+ return undefined;
150
+ if (!Array.isArray(value))
151
+ return fail(key, "a list");
152
+ return value.map((entry, index) => {
153
+ const entryPath = `${at(key)}[${index}]`;
154
+ if (!isPhiRecord(entry))
155
+ throw makeError(entryPath, "an object", entry);
156
+ return read(createPhiStrictReader(entry, entryPath, makeError), index);
157
+ });
158
+ },
159
+ stringList(key) {
160
+ const value = record[key];
161
+ if (isAbsent(value))
162
+ return undefined;
163
+ if (!Array.isArray(value))
164
+ return fail(key, "a list of texts");
165
+ return value.map((entry, index) => {
166
+ if (typeof entry !== "string")
167
+ throw makeError(`${at(key)}[${index}]`, "a text", entry);
168
+ return entry;
169
+ });
170
+ },
171
+ fail,
172
+ };
173
+ return reader;
174
+ }
@@ -81,6 +81,7 @@ const PHI_EFFECTS_WIDGET_LABEL_SET = definePhiLabelSet({
81
81
  origin_bottom_right: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.origins.bottomRight,
82
82
  viewport_x: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.x,
83
83
  viewport_y: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.y,
84
+ viewport_z: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.z,
84
85
  viewport_translate: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.translate,
85
86
  viewport_opacity: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.opacity,
86
87
  viewport_rotate: PHI_EFFECTS_WIDGET_DEFAULT_LABELS.viewport.rotate,
@@ -191,6 +192,7 @@ export async function getPhiEffectsWidgetLabels(options) {
191
192
  viewport: {
192
193
  x: labels.viewport_x,
193
194
  y: labels.viewport_y,
195
+ z: labels.viewport_z,
194
196
  translate: labels.viewport_translate,
195
197
  opacity: labels.viewport_opacity,
196
198
  rotate: labels.viewport_rotate,