@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
@@ -1,10 +1,12 @@
1
1
  "use client";
2
- import { isPhiRecord } from "../../helpers/is-record";
3
2
  import { useEffect, useMemo, useState } from "react";
4
3
  import { PhiMediaKind } from "../../constants/media";
5
4
  import { createPhiAssetUri } from "../../types/references";
6
5
  import { usePhiCollectionProvider } from "../widgets/client/shared/phi-collection-provider";
7
6
  import { PHI_ASSET_COLLECTION_DATA_SOURCE } from "./asset-collection-runtime";
7
+ import { readPhiFontMetricsFields } from "./media-font-metrics";
8
+ import { readPhiMediaAssetTiles } from "./phi-image-preview-data";
9
+ import { createPhiResponseReader } from "../widgets/config/strict-config-reader";
8
10
  /**
9
11
  * The typefaces a Site owns, as something a font slot can be set to.
10
12
  *
@@ -19,12 +21,9 @@ import { PHI_ASSET_COLLECTION_DATA_SOURCE } from "./asset-collection-runtime";
19
21
  /** Enough for any Site's own lettering; a ceiling rather than a page size. */
20
22
  const PHI_SITE_FONT_ASSET_LIMIT = 100;
21
23
  function readFontAssetMetrics(asset) {
22
- const meta = asset.meta?.font;
23
- if (!isPhiRecord(meta))
24
- return null;
25
- const record = meta;
26
- // The management tile carries `meta` as stored; only a shape with its unit grid is worth drawing from.
27
- return typeof record.unitsPerEm === "number" && record.unitsPerEm > 0 ? record : null;
24
+ // The management tile carries `meta` as stored, and the server stored every field of the metrics.
25
+ return createPhiResponseReader(asset.meta ?? {}, `The stored metrics of Asset ${asset.id}`, "meta")
26
+ .object("font", readPhiFontMetricsFields) ?? null;
28
27
  }
29
28
  function readFontAssetLabel(asset) {
30
29
  const family = readFontAssetMetrics(asset)?.familyName;
@@ -68,7 +67,7 @@ export function usePhiSiteFontAssets(enabled = true) {
68
67
  setError(data.error);
69
68
  return;
70
69
  }
71
- setAssets(data.items.filter((item) => Boolean(item)));
70
+ setAssets(readPhiMediaAssetTiles(data.items));
72
71
  }).catch((cause) => {
73
72
  if (abortController.signal.aborted)
74
73
  return;
@@ -60,6 +60,13 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
60
60
  const [runtimeControlSizeOverride, setRuntimeControlSizeOverride] = useState(null);
61
61
  const [runtimeSizeOverride, setRuntimeSizeOverride] = useState(null);
62
62
  const [runtimeTitleOverride, setRuntimeTitleOverride] = useState(null);
63
+ /*
64
+ * Whether work the Overlay holds is under way -- a Form in the Body saving. While it is, a reader's
65
+ * dismissal (close button, mask, Escape) is not taken: closing would abandon the save's answer. A
66
+ * `close` sent on purpose still closes, which is how a Form that went through ends its dialog. Told
67
+ * through the `busy` input by whoever knows, so a dialog needs no Controller to refuse a close.
68
+ */
69
+ const [busy, setBusy] = useState(false);
63
70
  const runtimeControlSize = runtimeControlSizeOverride?.config === config
64
71
  ? runtimeControlSizeOverride.value
65
72
  : null;
@@ -106,6 +113,9 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
106
113
  // A failed load is forgotten on close, so the next open asks the server again.
107
114
  if (!nextOpen)
108
115
  setLoadedZones((current) => current && "failure" in current ? null : current);
116
+ // A closed Overlay holds no work; a busy state left behind would refuse the next dismissal.
117
+ if (!nextOpen)
118
+ setBusy(false);
109
119
  setOpen(nextOpen);
110
120
  }, []);
111
121
  /*
@@ -200,6 +210,9 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
200
210
  else if (route.capabilityId === "close") {
201
211
  updateOpen(false);
202
212
  }
213
+ else if (route.capabilityId === "busy" && signal.valueType === "boolean") {
214
+ setBusy(signal.value === true);
215
+ }
203
216
  else if (route.capabilityId === "toggle") {
204
217
  updateOpen(typeof signal.value === "boolean" ? signal.value : !open);
205
218
  }
@@ -229,12 +242,14 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
229
242
  receiver,
230
243
  }), [receiver, signalScope]), receiver);
231
244
  const requestClose = useCallback((source) => {
245
+ if (busy)
246
+ return;
232
247
  if (config.closeMode === "request") {
233
248
  emitCapability("closeRequest", { source });
234
249
  return;
235
250
  }
236
251
  updateOpen(false);
237
- }, [config.closeMode, emitCapability, updateOpen]);
252
+ }, [busy, config.closeMode, emitCapability, updateOpen]);
238
253
  // An Overlay's window is simply "open", which is why it can share the rule with a Carousel whose
239
254
  // window is several slots wide.
240
255
  const shouldRenderContent = shouldPhiCmsContentStayMounted({
@@ -4,7 +4,7 @@ import { createPhiCmsPresetNodes } from "../../../helpers/cms-preset-nodes";
4
4
  import { PHI_SPACE } from "../../../theme/antd-css-var-contract";
5
5
  import { PHI_LAYOUT } from "../../../theme/phi-tokens";
6
6
  import { createPhiPresetCmsInstanceIdMap } from "../../../types/cms-instance-id";
7
- import { PHI_SIGNAL_VALUE_SCHEMAS, createPhiSignalAddress } from "../../../types/signals";
7
+ import { PHI_SIGNAL_VALUE_SCHEMAS, createPhiSignalAddress, createPhiSignalSubcontrolAddress, } from "../../../types/signals";
8
8
  import { buildPhiBasePageContentScaffold, PHI_BASE_PAGE_LAYOUT_NODE_ID } from "./phi-base-page-layout";
9
9
  import { PHI_DIALOG_BODY_CONFIG, PHI_DIALOG_COMMANDS_CONFIG } from "../../../helpers/cms-dialog";
10
10
  /**
@@ -252,6 +252,29 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
252
252
  valueType: "none",
253
253
  receiver: overlayAddress,
254
254
  },
255
+ /*
256
+ * While the save is under way the dialog refuses a reader's dismissal, and the Save
257
+ * button shows that it is working -- the two things a Controller would otherwise be
258
+ * mounted for (OVERLAYS.md, "Runtime and signaling").
259
+ */
260
+ {
261
+ routeKey: `${overlay.nodeKey}-busy`,
262
+ capabilityId: "submitting",
263
+ scope: "page",
264
+ channel: "busy",
265
+ action: "change",
266
+ valueType: "boolean",
267
+ receiver: overlayAddress,
268
+ },
269
+ {
270
+ routeKey: `${overlay.nodeKey}-submit-loading`,
271
+ capabilityId: "submitting",
272
+ scope: "page",
273
+ channel: "loading",
274
+ action: "change",
275
+ valueType: "boolean",
276
+ receiver: createPhiSignalSubcontrolAddress("cms", widgets[`${overlay.nodeKey}Commands`], "submit"),
277
+ },
255
278
  ...(extraSignalRoutes?.emits ?? []),
256
279
  ],
257
280
  listens: [
@@ -315,6 +338,17 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
315
338
  receiver: formAddress,
316
339
  },
317
340
  ],
341
+ listens: [
342
+ {
343
+ routeKey: `${overlay.nodeKey}-commands-submit-loading`,
344
+ capabilityId: "loading",
345
+ scope: "page",
346
+ channel: "loading",
347
+ action: "change",
348
+ valueType: "boolean",
349
+ receiver: createPhiSignalSubcontrolAddress("cms", widgets[`${overlay.nodeKey}Commands`], "submit"),
350
+ },
351
+ ],
318
352
  },
319
353
  },
320
354
  }),
@@ -344,6 +378,10 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
344
378
  * the provider somebody looked at before.
345
379
  */
346
380
  mountPolicy: "remount",
381
+ /*
382
+ * `immediate`, because nothing is mounted to answer a close request; while the Form saves, its
383
+ * `submitting` route sets this Overlay `busy`, and a busy Overlay does not take a dismissal.
384
+ */
347
385
  closeMode: "immediate",
348
386
  openActionKey: overlay.openActionKey,
349
387
  signalRoutes: {
@@ -371,6 +409,15 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
371
409
  valueType: "none",
372
410
  receiver: createPhiSignalAddress("cms", overlayIds[overlay.nodeKey]),
373
411
  },
412
+ {
413
+ routeKey: `${overlay.nodeKey}-busy`,
414
+ capabilityId: "busy",
415
+ scope: "page",
416
+ channel: "busy",
417
+ action: "change",
418
+ valueType: "boolean",
419
+ receiver: createPhiSignalAddress("cms", overlayIds[overlay.nodeKey]),
420
+ },
374
421
  ],
375
422
  },
376
423
  },
@@ -39,7 +39,18 @@ export function materializePhiWidgetRuntimeControllerSettings({ widget, tree, ow
39
39
  if (!plugin.requiredRuntimeControllers) {
40
40
  return [];
41
41
  }
42
- const config = plugin.parseConfig(widget.config);
42
+ /*
43
+ * A Widget whose config cannot be read asks for no Controller. It is not rendered either: the renderer
44
+ * meets the same error in the Widget's own place, leaves it out of a live page and shows it in the
45
+ * Builder. Thrown here, outside any block, it took the whole page with it.
46
+ */
47
+ let config;
48
+ try {
49
+ config = plugin.parseConfig(widget.config);
50
+ }
51
+ catch {
52
+ return [];
53
+ }
43
54
  const settingsByKey = new Map();
44
55
  for (const requirement of plugin.requiredRuntimeControllers({ widget, tree, config })) {
45
56
  const setting = {
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { useRef, useSyncExternalStore } from "react";
2
+ import { useCallback, useRef, useSyncExternalStore } from "react";
3
3
  const scopedStateStoreKey = "__phiScopedStateStores__";
4
4
  function normalizeScopedStateKey(scopeKey) {
5
5
  return scopeKey.trim() || "default";
@@ -92,7 +92,12 @@ export function createScopedStateStore(storeId, createDefaultState) {
92
92
  }
93
93
  function useStore(scopeKey) {
94
94
  const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
95
- return useSyncExternalStore((listener) => subscribe(normalizedScopeKey, listener), () => getSnapshot(normalizedScopeKey), () => getHydrationSnapshot(normalizedScopeKey));
95
+ /*
96
+ * Held per scope. An inline `subscribe` is a new function every render, and `useSyncExternalStore`
97
+ * unsubscribes and subscribes again for each new one.
98
+ */
99
+ const subscribeToScope = useCallback((listener) => subscribe(normalizedScopeKey, listener), [normalizedScopeKey]);
100
+ return useSyncExternalStore(subscribeToScope, () => getSnapshot(normalizedScopeKey), () => getHydrationSnapshot(normalizedScopeKey));
96
101
  }
97
102
  function useStoreSelector(scopeKey, selector) {
98
103
  const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
@@ -124,14 +129,26 @@ export function createScopedStateStore(storeId, createDefaultState) {
124
129
  selectedHydrationSnapshotRef.current = { source, selector, selected };
125
130
  return selected;
126
131
  };
127
- return useSyncExternalStore((listener) => subscribe(normalizedScopeKey, listener), getSelectedSnapshot, getSelectedHydrationSnapshot);
132
+ const subscribeToScope = useCallback((listener) => subscribe(normalizedScopeKey, listener), [normalizedScopeKey]);
133
+ return useSyncExternalStore(subscribeToScope, getSelectedSnapshot, getSelectedHydrationSnapshot);
128
134
  }
129
135
  function reset(scopeKey) {
130
136
  patch(scopeKey, () => createDefaultState(normalizeScopedStateKey(scopeKey)));
131
137
  }
138
+ /*
139
+ * Forgets a scope's state. A component still mounted on it keeps its subscription on the entry it
140
+ * subscribed to, so dropping that entry left it listening to nothing: the next write went into a new
141
+ * entry nobody was told about. With listeners, the entry stays and starts over from the default
142
+ * instead, and they hear it.
143
+ */
132
144
  function deleteScope(scopeKey) {
133
145
  const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
134
146
  const bucket = getScopedBucket();
147
+ const entry = bucket.get(normalizedScopeKey);
148
+ if (entry && entry.listeners.size > 0) {
149
+ replace(normalizedScopeKey, createDefaultState(normalizedScopeKey));
150
+ return;
151
+ }
135
152
  bucket.delete(normalizedScopeKey);
136
153
  }
137
154
  return {
@@ -22,7 +22,8 @@ export declare function usePhiTableBinding({ source, initialQuery, externalQuery
22
22
  resource: PhiTableProviderResourceDescriptor | null;
23
23
  bindingError: string | null;
24
24
  rows: readonly TableRow[];
25
- total: number;
25
+ total: number | null;
26
+ hasMore: boolean;
26
27
  summary: Readonly<Record<string, PhiTableSummaryValue>>;
27
28
  contractError: string | null;
28
29
  loading: boolean;
@@ -76,7 +76,14 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
76
76
  expandedRowIdentities: initialQuery.expandedRowIdentities,
77
77
  }));
78
78
  const [rows, setRows] = useState([]);
79
- const [total, setTotal] = useState(0);
79
+ /*
80
+ * The number of rows the whole query has, where the Provider counts them (`query.total` on its
81
+ * resource), and `null` where it does not. Counting is a query of its own on a large table, so a
82
+ * Provider answers it only where a Table shows it; the rows on this page are never put in its place.
83
+ */
84
+ const [total, setTotal] = useState(null);
85
+ /** Whether a page follows this one, for a Table that pages without a count. */
86
+ const [hasMore, setHasMore] = useState(false);
80
87
  const [summary, setSummary] = useState({});
81
88
  const [queryLoading, setQueryLoading] = useState(Boolean(source));
82
89
  const [pendingMutationTargets, setPendingMutationTargets] = useState(() => new Set());
@@ -102,6 +109,30 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
102
109
  controller.abort();
103
110
  mutationControllers.current.clear();
104
111
  }, []);
112
+ /*
113
+ * A new source starts from nothing, and it does so in the render that brought it.
114
+ *
115
+ * The reset used to run in a microtask after the effects, so the query effect below had already
116
+ * fetched the new source with the old page and cursor -- one request thrown away on every source or
117
+ * `bindingParamsChange`. Adjusting state while rendering is React's way to follow a prop: the effects
118
+ * of this render see page 1 already. It also leaves the first render alone, so an `initialQuery` page
119
+ * and `initialExpandedRowIdentities` are no longer reset away on mount.
120
+ */
121
+ const [renderedSourceKey, setRenderedSourceKey] = useState(sourceKey);
122
+ if (renderedSourceKey !== sourceKey) {
123
+ setRenderedSourceKey(sourceKey);
124
+ setPendingMutationTargets(new Set());
125
+ setRows([]);
126
+ setTotal(null);
127
+ setHasMore(false);
128
+ setSummary({});
129
+ setSelectedRowIdentities([]);
130
+ setExpandedRowIdentities([]);
131
+ setQuery((current) => ({ ...current, page: 1, cursor: null }));
132
+ setQueryLoading(sourceKey !== "null");
133
+ setError(null);
134
+ }
135
+ // What a render cannot reset: requests in flight and the bookkeeping kept beside state.
105
136
  useEffect(() => {
106
137
  for (const controller of mutationControllers.current)
107
138
  controller.abort();
@@ -109,21 +140,6 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
109
140
  latestMutationByTarget.current.clear();
110
141
  pendingMutationCountsByTarget.current.clear();
111
142
  rowsRef.current = [];
112
- let cancelled = false;
113
- queueMicrotask(() => {
114
- if (cancelled)
115
- return;
116
- setPendingMutationTargets(new Set());
117
- setRows([]);
118
- setTotal(0);
119
- setSummary({});
120
- setSelectedRowIdentities([]);
121
- setExpandedRowIdentities([]);
122
- setQuery((current) => ({ ...current, page: 1, cursor: null }));
123
- setQueryLoading(sourceKey !== "null");
124
- setError(null);
125
- });
126
- return () => { cancelled = true; };
127
143
  }, [sourceKey]);
128
144
  /*
129
145
  * Held by content, not identity: a caller that writes the object inline hands in a new one every
@@ -158,7 +174,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
158
174
  if (data.rows.some((row) => readPhiTableRowIdentity(row, resource.rowIdentityPath) == null)) {
159
175
  setRows([]);
160
176
  rowsRef.current = [];
161
- setTotal(0);
177
+ setTotal(null);
178
+ setHasMore(false);
162
179
  setSummary({});
163
180
  setQueryLoading(false);
164
181
  setError(new PhiTableProviderError("missing-row-identity", `Table Provider row is missing identity field "${resource.rowIdentityPath}".`));
@@ -168,7 +185,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
168
185
  if (summaryError) {
169
186
  setRows([]);
170
187
  rowsRef.current = [];
171
- setTotal(0);
188
+ setTotal(null);
189
+ setHasMore(false);
172
190
  setSummary({});
173
191
  setQueryLoading(false);
174
192
  setError(new PhiTableProviderError("invalid-summary", summaryError));
@@ -176,7 +194,11 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
176
194
  }
177
195
  rowsRef.current = data.rows;
178
196
  setRows(data.rows);
179
- setTotal(data.total ?? data.rows.length);
197
+ // A resource that does not page answers every row at once, so its rows are its count.
198
+ const pages = resource.query.pagination === "offset" || resource.query.pagination === "cursor";
199
+ setTotal(data.total ?? (pages ? null : data.rows.length));
200
+ const pageSize = data.pageSize ?? resolvedQuery.pageSize;
201
+ setHasMore(data.nextCursor != null || (data.total == null && pageSize != null && data.rows.length >= pageSize));
180
202
  setSummary(data.summary ?? {});
181
203
  setQueryLoading(false);
182
204
  setError(null);
@@ -188,7 +210,7 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
188
210
  setSelectedRowIdentities((current) => current.filter((identity) => available.has(String(identity))));
189
211
  }
190
212
  onDataRef.current?.(data);
191
- }, [preserveSelectedRowIdentities, resource]);
213
+ }, [preserveSelectedRowIdentities, resolvedQuery.pageSize, resource]);
192
214
  useEffect(() => {
193
215
  if (!source || !provider || !resource || resourceError)
194
216
  return;
@@ -254,7 +276,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
254
276
  if (!background) {
255
277
  setRows([]);
256
278
  rowsRef.current = [];
257
- setTotal(0);
279
+ setTotal(null);
280
+ setHasMore(false);
258
281
  setSummary({});
259
282
  }
260
283
  setQueryLoading(false);
@@ -505,6 +528,7 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
505
528
  bindingError,
506
529
  rows,
507
530
  total,
531
+ hasMore,
508
532
  summary,
509
533
  contractError: resourceError,
510
534
  loading: queryLoading && !bindingError && !resourceError,
@@ -104,19 +104,27 @@ export function usePhiTreeBinding({ source, initialQuery = PHI_TREE_DEFAULT_INIT
104
104
  controllers.clear();
105
105
  };
106
106
  }, []);
107
+ /*
108
+ * A new source, or new defaults, start the tree over -- in the render that brought them, so the query
109
+ * effect below asks once, with the reset query, instead of first with the old one.
110
+ *
111
+ * The search a reader typed stays. Resetting it to the initial query showed the unfiltered tree for a
112
+ * moment and fetched it, and the Widget's debounce put the search back 250 ms later for a third request:
113
+ * a new binding parameter narrows what the tree is about, not what the reader is looking for.
114
+ */
115
+ const [renderedResetInputs, setRenderedResetInputs] = useState({ sourceKey, initialQuery, defaultExpandedNodeIdentities });
116
+ if (renderedResetInputs.sourceKey !== sourceKey ||
117
+ renderedResetInputs.initialQuery !== initialQuery ||
118
+ renderedResetInputs.defaultExpandedNodeIdentities !== defaultExpandedNodeIdentities) {
119
+ setRenderedResetInputs({ sourceKey, initialQuery, defaultExpandedNodeIdentities });
120
+ setNodes([]);
121
+ setSelectedNodeIdentities([]);
122
+ setCheckedNodeIdentities([]);
123
+ setExpandedNodeIdentities(defaultExpandedNodeIdentities);
124
+ setQuery((current) => (current.search ? { ...initialQuery, search: current.search } : initialQuery));
125
+ }
107
126
  useEffect(() => {
108
127
  expandedInitialSnapshot.current = false;
109
- let cancelled = false;
110
- queueMicrotask(() => {
111
- if (cancelled)
112
- return;
113
- setNodes([]);
114
- setSelectedNodeIdentities([]);
115
- setCheckedNodeIdentities([]);
116
- setExpandedNodeIdentities(defaultExpandedNodeIdentities);
117
- setQuery(initialQuery);
118
- });
119
- return () => { cancelled = true; };
120
128
  }, [defaultExpandedNodeIdentities, initialQuery, sourceKey]);
121
129
  useEffect(() => {
122
130
  if (!source || !provider || !resource)
@@ -0,0 +1,26 @@
1
+ import { type CSSProperties } from "react";
2
+ import { type PhiBulletListEntry, type PhiResolvedBulletListStyle } from "../shared/bullet-list-vocabulary";
3
+ export type PhiBulletListEntriesEditorProps = {
4
+ entries: readonly PhiBulletListEntry[];
5
+ listStyle: PhiResolvedBulletListStyle;
6
+ /** Absent, the entries are only shown: a read-only canvas. */
7
+ onCommit?: (entries: PhiBulletListEntry[]) => void;
8
+ /** Whether a click on an entry's marker steps its state on (`PHI_BULLET_LIST_ENTRY_STATES`). */
9
+ states?: boolean;
10
+ /** Each entry's field, apart from what every field here is: borderless, its text's own type. */
11
+ fieldStyle?: CSSProperties;
12
+ };
13
+ /**
14
+ * A list written in place, entry by entry, as the Widgets that carry one edit it.
15
+ *
16
+ * Every entry is a text field standing where the entry stands, so the list is set as it will be read.
17
+ * What is typed stays here until the field is left, and the Builder's history records one change per
18
+ * entry rather than one per keystroke. What changes the list's shape is written at once, with what was
19
+ * typed so far: Enter opens an entry after this one, Backspace in an empty entry removes it, Tab and
20
+ * Shift+Tab move an entry a level down and up, and -- where the list has `states` -- a click on the
21
+ * marker steps the entry from included to excluded to neither. Waiting for the blur would let the entry
22
+ * jump back while the tree is drawn again.
23
+ *
24
+ * A list with no entries still offers one empty field, so there is something to type into.
25
+ */
26
+ export declare function PhiBulletListEntriesEditor({ entries, listStyle, onCommit, states, fieldStyle, }: PhiBulletListEntriesEditorProps): import("react").JSX.Element;
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRef, useState } from "react";
4
+ import { PhiBulletListControl } from "../../controls/phi-bullet-list-control";
5
+ import { PhiTextControl } from "../../controls/phi-text-control";
6
+ import { buildPhiBulletListTree, nextPhiBulletListEntryState, resolvePhiBulletListLevel, } from "../shared/bullet-list-vocabulary";
7
+ /**
8
+ * A list written in place, entry by entry, as the Widgets that carry one edit it.
9
+ *
10
+ * Every entry is a text field standing where the entry stands, so the list is set as it will be read.
11
+ * What is typed stays here until the field is left, and the Builder's history records one change per
12
+ * entry rather than one per keystroke. What changes the list's shape is written at once, with what was
13
+ * typed so far: Enter opens an entry after this one, Backspace in an empty entry removes it, Tab and
14
+ * Shift+Tab move an entry a level down and up, and -- where the list has `states` -- a click on the
15
+ * marker steps the entry from included to excluded to neither. Waiting for the blur would let the entry
16
+ * jump back while the tree is drawn again.
17
+ *
18
+ * A list with no entries still offers one empty field, so there is something to type into.
19
+ */
20
+ export function PhiBulletListEntriesEditor({ entries, listStyle, onCommit, states = false, fieldStyle, }) {
21
+ const [draftState, setDraftState] = useState(() => ({ source: entries, value: [...entries] }));
22
+ const draft = draftState.source === entries ? draftState.value : [...entries];
23
+ const shown = draft.length > 0 || !onCommit ? draft : [{ text: "" }];
24
+ const listRef = useRef(null);
25
+ function focusEntry(index) {
26
+ requestAnimationFrame(() => {
27
+ const field = listRef.current?.querySelectorAll("textarea")[index];
28
+ field?.focus();
29
+ field?.setSelectionRange(field.value.length, field.value.length);
30
+ });
31
+ }
32
+ function commit(next) {
33
+ // The empty field an empty list offers is not an entry until something is typed into it.
34
+ if (entries.length === 0 && next.length === 1 && next[0].text === "" && !next[0].state)
35
+ return;
36
+ setDraftState({ source: entries, value: next });
37
+ if (JSON.stringify(next) !== JSON.stringify(entries))
38
+ onCommit?.(next);
39
+ }
40
+ function updateText(index, text) {
41
+ const next = [...shown];
42
+ next[index] = { ...next[index], text };
43
+ setDraftState({ source: entries, value: next });
44
+ }
45
+ function shiftLevel(index, delta) {
46
+ const entry = shown[index];
47
+ if (!entry)
48
+ return;
49
+ const level = resolvePhiBulletListLevel((entry.level ?? 1) + delta);
50
+ if (level === (entry.level ?? 1))
51
+ return;
52
+ const { level: _level, ...rest } = entry;
53
+ void _level;
54
+ commit(shown.map((item, itemIndex) => (itemIndex === index ? (level > 1 ? { ...rest, level } : rest) : item)));
55
+ focusEntry(index);
56
+ }
57
+ function insertAfter(index) {
58
+ const entry = shown[index];
59
+ const level = entry?.level;
60
+ commit([...shown.slice(0, index + 1), { text: "", ...(level ? { level } : {}) }, ...shown.slice(index + 1)]);
61
+ focusEntry(index + 1);
62
+ }
63
+ function remove(index) {
64
+ commit(shown.filter((_, itemIndex) => itemIndex !== index));
65
+ focusEntry(Math.max(0, index - 1));
66
+ }
67
+ function stepState(index) {
68
+ const entry = shown[index];
69
+ if (!entry)
70
+ return;
71
+ const { state: _state, ...rest } = entry;
72
+ void _state;
73
+ const state = nextPhiBulletListEntryState(entry.state);
74
+ commit(shown.map((item, itemIndex) => (itemIndex === index ? (state === "neutral" ? rest : { ...rest, state }) : item)));
75
+ }
76
+ return (_jsx("div", { ref: listRef, style: { width: "100%", minWidth: 0 }, children: _jsx(PhiBulletListControl, { listStyle: listStyle,
77
+ // Each entry is a borderless text field, whose first line stands at the field's height.
78
+ markerLineHeight: "var(--ant-control-height)", items: buildPhiBulletListTree(shown, (item, index) => (_jsx(PhiTextControl, { presentation: "textarea", value: item.text, readOnly: !onCommit, variant: "borderless", placeholder: `Item ${index + 1}`, autoSize: { minRows: 1, maxRows: 6 }, onChange: (value) => updateText(index, value ?? ""), onBlur: () => commit(shown), onKeyDown: (event) => {
79
+ if (!onCommit)
80
+ return;
81
+ if (event.key === "Tab") {
82
+ event.preventDefault();
83
+ shiftLevel(index, event.shiftKey ? -1 : 1);
84
+ }
85
+ else if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
86
+ event.preventDefault();
87
+ insertAfter(index);
88
+ }
89
+ else if (event.key === "Backspace" && item.text === "" && shown.length > 1) {
90
+ event.preventDefault();
91
+ remove(index);
92
+ }
93
+ }, style: {
94
+ paddingInline: 0,
95
+ paddingBlock: 0,
96
+ borderRadius: 0,
97
+ borderWidth: 0,
98
+ backgroundColor: "transparent",
99
+ resize: "none",
100
+ color: "inherit",
101
+ ...fieldStyle,
102
+ } })), states && onCommit ? (_entry, index) => stepState(index) : undefined) }) }));
103
+ }
@@ -1,5 +1,5 @@
1
1
  import { parsePhiControlOptionsProviderConfig, readPhiControlOptions, } from "../../controls/phi-control-options";
2
- import { readBoolean, readString } from "./parser-primitives";
2
+ import { createPhiConfigReader } from "./strict-config-reader";
3
3
  import { PHI_CONTROL_PRESENTATION_FIELDS, PHI_CONTROL_STATE_FIELDS, parsePhiControlConfig, } from "./control-signal-config";
4
4
  export function buildPhiChoiceControlOptionFields(editorPlacement = "inspector") {
5
5
  return [{
@@ -28,9 +28,10 @@ export function buildPhiChoiceControlOptionFields(editorPlacement = "inspector")
28
28
  }
29
29
  export const PHI_CHOICE_CONTROL_OPTION_FIELDS = buildPhiChoiceControlOptionFields();
30
30
  export function parsePhiChoiceOptionsConfig(config) {
31
+ const reader = createPhiConfigReader(config);
31
32
  return {
32
- optionsProvider: parsePhiControlOptionsProviderConfig(config.optionsProvider),
33
- options: readPhiControlOptions(config.options),
33
+ optionsProvider: reader.value("optionsProvider", "an options provider", (value) => parsePhiControlOptionsProviderConfig(value) ?? undefined) ?? null,
34
+ options: reader.value("options", "a list of options", (value) => (Array.isArray(value) ? readPhiControlOptions(value) : undefined)) ?? readPhiControlOptions(undefined),
34
35
  };
35
36
  }
36
37
  export const PHI_CHOICE_CONTROL_CONFIG_FIELDS = [
@@ -46,27 +47,25 @@ export const PHI_CHOICE_CONTROL_VALUE_MODE_FIELD = {
46
47
  { value: "stack-slot-index", label: "Stack Slot Index" },
47
48
  ],
48
49
  };
49
- function readPhiChoiceValueMode(value) {
50
- return readString(value) === "stack-slot-index" ? "stack-slot-index" : "raw";
51
- }
52
50
  export function parsePhiChoiceControlConfig(config) {
53
51
  const controlState = parsePhiControlConfig(config, {
54
52
  key: "choice",
55
53
  });
54
+ const reader = createPhiConfigReader(config);
56
55
  return {
57
56
  ...controlState,
58
- label: readString(config.label),
59
- description: readString(config.description),
60
- value: readString(config.value),
61
- placeholder: readString(config.placeholder),
62
- presentation: readString(config.presentation) === "autocomplete" ? "autocomplete" : "select",
63
- allowCustom: readBoolean(config.allowCustom),
57
+ label: reader.string("label"),
58
+ description: reader.string("description"),
59
+ value: reader.string("value"),
60
+ placeholder: reader.string("placeholder"),
61
+ presentation: reader.oneOf("presentation", ["select", "autocomplete"]) ?? "select",
62
+ allowCustom: reader.boolean("allowCustom"),
64
63
  ...parsePhiChoiceOptionsConfig(config),
65
64
  };
66
65
  }
67
66
  export function parsePhiStackChoiceControlConfig(config) {
68
67
  return {
69
68
  ...parsePhiChoiceControlConfig(config),
70
- valueMode: readPhiChoiceValueMode(config.valueMode),
69
+ valueMode: createPhiConfigReader(config).oneOf("valueMode", ["raw", "stack-slot-index"]) ?? "raw",
71
70
  };
72
71
  }
@@ -2,7 +2,11 @@ import type { PhiCommandButtonConfig } from "../../../types/core-widget-placemen
2
2
  import type { PhiCmsConfigField } from "../../../types";
3
3
  import type { PhiCmsConfigFieldVisibilityRule } from "../../../types/cms-plugins";
4
4
  import { type PhiAccessViewer, type PhiRoleProviderId } from "../../../types/access";
5
- export declare function readPhiCommandButtons(value: unknown): PhiCommandButtonConfig[];
5
+ /**
6
+ * A Widget's buttons. Strict (`strict-config-reader.ts`): a button without a key or without an emit is
7
+ * an error naming it, not a button left out.
8
+ */
9
+ export declare function readPhiCommandButtons(value: unknown, path?: string): PhiCommandButtonConfig[];
6
10
  export declare function filterPhiCommandButtonsForViewer(buttons: readonly PhiCommandButtonConfig[], viewer: PhiAccessViewer, ownerProviderId?: PhiRoleProviderId | null): PhiCommandButtonConfig[];
7
11
  /**
8
12
  * A new button: the first free `buttonN` key, labelled after it, sending its key as the command. The