@phis/ui 0.4.1 → 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 (246) 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/shared/markdown-body-client.d.ts +2 -1
  57. package/components/widgets/shared/markdown-body-client.js +3 -1
  58. package/components/widgets/shared/markdown-render.js +8 -1
  59. package/components/widgets/signals/control-signal-capabilities.d.ts +0 -18
  60. package/components/widgets/signals/control-signal-capabilities.js +10 -22
  61. package/components/workspace/catalog-store.d.ts +4 -3
  62. package/components/workspace/catalog-store.js +6 -5
  63. package/gateway/dashboard-cards-route.js +1 -0
  64. package/gateway/internal-references.d.ts +6 -1
  65. package/gateway/internal-references.js +48 -23
  66. package/gateway/site-cms-write-validation.d.ts +9 -0
  67. package/gateway/site-cms-write-validation.js +78 -0
  68. package/gateway/site-proxy.js +19 -3
  69. package/helpers/renderable-block-serialization.js +1 -1
  70. package/next/config.d.mts +3 -0
  71. package/next/config.mjs +28 -1
  72. package/package.json +2 -2
  73. package/plugins/runtime/phi-slot-child-viewport-effects-observer.js +5 -1
  74. package/plugins/runtime-modules/accounting/presets.d.ts +9 -0
  75. package/plugins/runtime-modules/accounting/presets.js +37 -0
  76. package/plugins/runtime-modules/accounting/server.d.ts +1 -4
  77. package/plugins/runtime-modules/accounting/server.js +4 -7
  78. package/plugins/runtime-modules/app/presets.d.ts +18 -0
  79. package/plugins/runtime-modules/app/presets.js +67 -0
  80. package/plugins/runtime-modules/app/server.d.ts +1 -4
  81. package/plugins/runtime-modules/app/server.js +4 -7
  82. package/plugins/runtime-modules/asset/controller/runtime.js +2 -2
  83. package/plugins/runtime-modules/asset/services/collection.js +9 -1
  84. package/plugins/runtime-modules/asset/services/options.js +2 -2
  85. package/plugins/runtime-modules/builder/area-settings-answer.js +2 -1
  86. package/plugins/runtime-modules/builder/authoring-canvas.js +6 -6
  87. package/plugins/runtime-modules/builder/cards.js +3 -5
  88. package/plugins/runtime-modules/builder/clients/builder-region-overrides.js +3 -3
  89. package/plugins/runtime-modules/builder/clients/canvas-widget.js +15 -54
  90. package/plugins/runtime-modules/builder/clients/inspector-config-field.d.ts +4 -1
  91. package/plugins/runtime-modules/builder/clients/inspector-config-field.js +5 -34
  92. package/plugins/runtime-modules/builder/clients/inspector-link-target-field.js +1 -1
  93. package/plugins/runtime-modules/builder/clients/inspector-section-widget.js +8 -8
  94. package/plugins/runtime-modules/builder/clients/inspector-title.js +6 -6
  95. package/plugins/runtime-modules/builder/clients/layout-inspector.d.ts +1 -1
  96. package/plugins/runtime-modules/builder/clients/layout-inspector.js +11 -25
  97. package/plugins/runtime-modules/builder/clients/mode-switch.js +1 -1
  98. package/plugins/runtime-modules/builder/clients/pages-header.js +11 -11
  99. package/plugins/runtime-modules/builder/clients/pages-workspace.js +1 -1
  100. package/plugins/runtime-modules/builder/clients/region-visibility-fields.js +1 -1
  101. package/plugins/runtime-modules/builder/clients/runtime-module-authoring-boundary.js +1 -1
  102. package/plugins/runtime-modules/builder/clients/shells-workspace.js +15 -31
  103. package/plugins/runtime-modules/builder/clients/structure-canvas.js +1 -1
  104. package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.d.ts +26 -0
  105. package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.js +36 -0
  106. package/plugins/runtime-modules/builder/clients/widget-effects-editor.js +1 -1
  107. package/plugins/runtime-modules/builder/clients/widget-inspector.js +1 -0
  108. package/plugins/runtime-modules/builder/clients/workspace-scope-boundary.js +2 -2
  109. package/plugins/runtime-modules/builder/cms-instance-allocator.js +8 -28
  110. package/plugins/runtime-modules/builder/controller/client.d.ts +0 -15
  111. package/plugins/runtime-modules/builder/controller/definition.d.ts +0 -15
  112. package/plugins/runtime-modules/builder/controller/definition.js +0 -8
  113. package/plugins/runtime-modules/builder/controller/draft-command-controller.d.ts +1 -2
  114. package/plugins/runtime-modules/builder/controller/draft-command-controller.js +11 -30
  115. package/plugins/runtime-modules/builder/controller/draft-status-controller.js +15 -2
  116. package/plugins/runtime-modules/builder/controller/inspector-controller.d.ts +1 -2
  117. package/plugins/runtime-modules/builder/controller/inspector-controller.js +4 -7
  118. package/plugins/runtime-modules/builder/controller/page-controller.d.ts +1 -2
  119. package/plugins/runtime-modules/builder/controller/page-controller.js +16 -68
  120. package/plugins/runtime-modules/builder/controller/server-definition.d.ts +0 -15
  121. package/plugins/runtime-modules/builder/controller/signal-wiring-controller.d.ts +2 -5
  122. package/plugins/runtime-modules/builder/controller/signal-wiring-controller.js +6 -50
  123. package/plugins/runtime-modules/builder/controller/workspace-controller.d.ts +2 -3
  124. package/plugins/runtime-modules/builder/controller/workspace-controller.js +89 -153
  125. package/plugins/runtime-modules/builder/data-providers.d.ts +1 -0
  126. package/plugins/runtime-modules/builder/data-providers.js +1 -0
  127. package/plugins/runtime-modules/builder/definition.d.ts +1 -0
  128. package/plugins/runtime-modules/builder/developer-workspace-store.d.ts +64 -30
  129. package/plugins/runtime-modules/builder/developer-workspace-store.js +255 -61
  130. package/plugins/runtime-modules/builder/edit-scaffold-drawer.js +1 -1
  131. package/plugins/runtime-modules/builder/history.d.ts +3 -1
  132. package/plugins/runtime-modules/builder/options-providers.js +3 -2
  133. package/plugins/runtime-modules/builder/page-catalog-client.js +19 -7
  134. package/plugins/runtime-modules/builder/page-meta-form.js +1 -0
  135. package/plugins/runtime-modules/builder/page-references-provider.js +1 -1
  136. package/plugins/runtime-modules/builder/persistence.js +33 -69
  137. package/plugins/runtime-modules/builder/public-route-collisions.d.ts +2 -2
  138. package/plugins/runtime-modules/builder/public-route-collisions.js +6 -19
  139. package/plugins/runtime-modules/builder/render-root-node-preview.server.js +20 -3
  140. package/plugins/runtime-modules/builder/render-root-node-scaffold.js +3 -3
  141. package/plugins/runtime-modules/builder/runtime-module-selection.d.ts +2 -12
  142. package/plugins/runtime-modules/builder/runtime-module-selection.js +16 -42
  143. package/plugins/runtime-modules/builder/services/navigation-table.js +9 -15
  144. package/plugins/runtime-modules/builder/services/page-source-tree.js +8 -8
  145. package/plugins/runtime-modules/builder/services/runtime-modules-table.js +10 -10
  146. package/plugins/runtime-modules/builder/services/signal-routes-table.js +3 -3
  147. package/plugins/runtime-modules/builder/signal-route-selection.d.ts +1 -2
  148. package/plugins/runtime-modules/builder/signal-route-selection.js +2 -2
  149. package/plugins/runtime-modules/builder/signal-wiring-options.js +3 -2
  150. package/plugins/runtime-modules/builder/structure-dnd.js +8 -61
  151. package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +13 -10
  152. package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +12 -18
  153. package/plugins/runtime-modules/builder/workspace-catalog-actions.d.ts +55 -0
  154. package/plugins/runtime-modules/builder/workspace-catalog-actions.js +142 -0
  155. package/plugins/runtime-modules/client-manifests/common.js +1 -1
  156. package/plugins/runtime-modules/{runtime-calendar-adapters.js → core/calendar-adapters.js} +2 -2
  157. package/plugins/runtime-modules/{client-calendar-adapters/core.js → core/client-calendar-adapters.js} +1 -1
  158. package/plugins/runtime-modules/core/content-entries/table.js +1 -1
  159. package/plugins/runtime-modules/core/data-providers.d.ts +3 -0
  160. package/plugins/runtime-modules/core/data-providers.js +2 -1
  161. package/plugins/runtime-modules/core/definition.d.ts +3 -0
  162. package/plugins/runtime-modules/core/definition.js +1 -1
  163. package/plugins/runtime-modules/core/messaging/data-providers.d.ts +1 -0
  164. package/plugins/runtime-modules/core/messaging/data-providers.js +1 -0
  165. package/plugins/runtime-modules/core/messaging/thread-inbox-table.js +1 -1
  166. package/plugins/runtime-modules/core/server.js +1 -1
  167. package/plugins/runtime-modules/core/themes.d.ts +44 -0
  168. package/plugins/runtime-modules/core/themes.js +12 -0
  169. package/plugins/runtime-modules/core/widgets/card/collection-view-binding.js +25 -9
  170. package/plugins/runtime-modules/core/widgets/card/config.d.ts +296 -254
  171. package/plugins/runtime-modules/core/widgets/card/config.js +52 -3
  172. package/plugins/runtime-modules/core/widgets/card/editor.js +30 -3
  173. package/plugins/runtime-modules/core/widgets/card/server.d.ts +3 -0
  174. package/plugins/runtime-modules/core/widgets/card/server.js +31 -3
  175. package/plugins/runtime-modules/core/widgets/collection-view/asset-binding.js +7 -6
  176. package/plugins/runtime-modules/core/widgets/collection-view/config.js +9 -2
  177. package/plugins/runtime-modules/core/widgets/description/config.d.ts +1 -25
  178. package/plugins/runtime-modules/core/widgets/description/editor.js +4 -61
  179. package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +1 -25
  180. package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +1 -25
  181. package/plugins/runtime-modules/core/widgets/image/config.js +2 -2
  182. package/plugins/runtime-modules/core/widgets/markdown/config.d.ts +3 -1
  183. package/plugins/runtime-modules/core/widgets/markdown/config.js +14 -0
  184. package/plugins/runtime-modules/core/widgets/markdown/editor.js +2 -3
  185. package/plugins/runtime-modules/core/widgets/result/config.d.ts +4 -25
  186. package/plugins/runtime-modules/core/widgets/result/config.js +2 -0
  187. package/plugins/runtime-modules/core/widgets/result/server.js +25 -19
  188. package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +1 -25
  189. package/plugins/runtime-modules/core/widgets/slot-upload/client.js +11 -2
  190. package/plugins/runtime-modules/core/widgets/table/client.js +8 -4
  191. package/plugins/runtime-modules/core/widgets/table/config.d.ts +3 -83
  192. package/plugins/runtime-modules/core/widgets/table/config.js +302 -452
  193. package/plugins/runtime-modules/core/widgets/tree/config.js +5 -2
  194. package/plugins/runtime-modules/dashboard/card-rows.d.ts +5 -0
  195. package/plugins/runtime-modules/dashboard/card-rows.js +59 -0
  196. package/plugins/runtime-modules/dashboard/clients/dashboard-card-view.js +4 -6
  197. package/plugins/runtime-modules/dashboard/services/collection.js +17 -7
  198. package/plugins/runtime-modules/groups/data-providers.d.ts +2 -0
  199. package/plugins/runtime-modules/groups/data-providers.js +3 -3
  200. package/plugins/runtime-modules/groups/definition.d.ts +2 -0
  201. package/plugins/runtime-modules/localization/data-providers.d.ts +3 -0
  202. package/plugins/runtime-modules/localization/data-providers.js +2 -0
  203. package/plugins/runtime-modules/localization/definition.d.ts +3 -0
  204. package/plugins/runtime-modules/localization/services/table.js +3 -7
  205. package/plugins/runtime-modules/observability/data-providers.d.ts +1 -0
  206. package/plugins/runtime-modules/observability/data-providers.js +1 -0
  207. package/plugins/runtime-modules/observability/definition.d.ts +1 -0
  208. package/plugins/runtime-modules/observability/module.d.ts +1 -0
  209. package/plugins/runtime-modules/observability/services/table.js +1 -1
  210. package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +2 -105
  211. package/plugins/runtime-modules/public/presets.d.ts +115 -0
  212. package/plugins/runtime-modules/{area-base-presets.js → public/presets.js} +19 -119
  213. package/plugins/runtime-modules/public/server.d.ts +1 -4
  214. package/plugins/runtime-modules/public/server.js +4 -7
  215. package/plugins/runtime-modules/public/trees/phi-default-pub-error-page-tree.js +7 -8
  216. package/plugins/runtime-modules/public-base-page-references.d.ts +1 -1
  217. package/plugins/runtime-modules/public-base-page-references.js +1 -1
  218. package/plugins/runtime-modules/revisions/controller/client.js +11 -11
  219. package/plugins/runtime-modules/revisions/services/table.js +3 -3
  220. package/plugins/runtime-modules/theme/trees/phi-default-builder-theme-page-tree.js +22 -0
  221. package/plugins/runtime-modules/theme/widgets/brand-controls/client.d.ts +2 -1
  222. package/plugins/runtime-modules/theme/widgets/brand-controls/client.js +93 -58
  223. package/plugins/runtime-modules/theme/widgets/brand-controls/config.d.ts +2 -1
  224. package/plugins/runtime-modules/theme/widgets/brand-controls/config.js +23 -22
  225. package/plugins/runtime-modules/theme/widgets/brand-controls/plugin.js +1 -1
  226. package/plugins/runtime-modules/theme-descriptors.js +1 -1
  227. package/plugins/runtime-modules/user-management/data-providers.d.ts +2 -0
  228. package/plugins/runtime-modules/user-management/data-providers.js +1 -0
  229. package/plugins/runtime-modules/user-management/definition.d.ts +2 -0
  230. package/styles/control-shape.css +11 -11
  231. package/styles/controls.css +29 -3
  232. package/styles/layout.css +16 -7
  233. package/types/cms-config.js +142 -185
  234. package/types/cms-plugins.d.ts +4 -11
  235. package/types/core-widget-placements.d.ts +17 -3
  236. package/types/dashboard-cards.d.ts +9 -0
  237. package/types/media.d.ts +0 -3
  238. package/types/renderable-block.d.ts +6 -1
  239. package/types/table-widget.d.ts +9 -0
  240. package/types/table-widget.js +20 -0
  241. package/types.d.ts +1 -1
  242. package/widget-config.d.ts +2 -0
  243. package/widget-config.js +6 -0
  244. package/plugins/runtime-modules/area-base-presets.d.ts +0 -139
  245. /package/plugins/runtime-modules/{runtime-calendar-adapters.d.ts → core/calendar-adapters.d.ts} +0 -0
  246. /package/plugins/runtime-modules/{client-calendar-adapters/core.d.ts → core/client-calendar-adapters.d.ts} +0 -0
@@ -92,6 +92,7 @@ export type PhiEffectsWidgetLabels = {
92
92
  viewport: {
93
93
  x: string;
94
94
  y: string;
95
+ z: string;
95
96
  translate: string;
96
97
  opacity: string;
97
98
  rotate: string;
@@ -91,6 +91,7 @@ export const PHI_EFFECTS_WIDGET_DEFAULT_LABELS = {
91
91
  viewport: {
92
92
  x: "X",
93
93
  y: "Y",
94
+ z: "Z",
94
95
  translate: "Translate",
95
96
  opacity: "Opacity",
96
97
  rotate: "Rotate",
@@ -3,6 +3,7 @@ import { isPhiCmsNavigationOverlayTarget, readPhiCmsNavigationTargetPath, } from
3
3
  import { createPhiPresetCmsInstanceId } from "../../types/cms-instance-id";
4
4
  import { createGlobalTranslator, createSiteTranslator, PHI_TR_CTX_WEB_UI_LABEL, } from "../../gateway/tr";
5
5
  import { resolvePhiCmsNavigationOverlay, resolvePhiCmsActiveNavigationSurfaces, resolvePhiCmsRoutePresetByPageId, } from "../../plugins/runtime-modules/descriptor-compiler";
6
+ import { cache } from "react";
6
7
  import { logRuntimeEvent } from "../../net/log";
7
8
  import { phiRuntime } from "../../server-helpers/phi-runtime";
8
9
  import { getPhiRequestNavigationContext } from "../../server-helpers/request-runtime";
@@ -58,19 +59,30 @@ function mapResolvedNavigationItem(item, translatedLabels) {
58
59
  : {}),
59
60
  };
60
61
  }
62
+ /*
63
+ * Every active navigation surface of an Area, resolved once per request (audit K15).
64
+ *
65
+ * A page carries a navigation Widget per surface -- header, footer, sider -- and each one resolved all of
66
+ * the Area's surfaces to pick its own, hashing an instance id per item each time. The request's navigation
67
+ * context and its viewer are one object each per request, so React's `cache` keyed by them keeps the
68
+ * answer for exactly that request and nothing longer.
69
+ */
70
+ const resolveActiveNavigationSurfaces = cache((context, area, viewer) => resolvePhiCmsActiveNavigationSurfaces({
71
+ catalog: context.catalog,
72
+ area,
73
+ activeModuleIds: context.activeModuleIds,
74
+ routeTable: context.routeTable,
75
+ viewer,
76
+ }));
61
77
  export async function resolvePhiDescriptorNavigationItems(runtime, navKey, overlay = null) {
62
78
  const normalizedNavKey = navKey.trim().toLowerCase();
63
79
  if (!normalizedNavKey.startsWith(`${runtime.area}:`)) {
64
80
  return null;
65
81
  }
66
- const { catalog, activeModuleIds, routeTable } = getPhiRequestNavigationContext(runtime.area);
67
- const descriptorSurface = resolvePhiCmsActiveNavigationSurfaces({
68
- catalog,
69
- area: runtime.area,
70
- activeModuleIds,
71
- routeTable,
72
- viewer: runtime.viewer,
73
- }).find((candidate) => candidate.navKey === normalizedNavKey);
82
+ const navigationContext = getPhiRequestNavigationContext(runtime.area);
83
+ const { routeTable } = navigationContext;
84
+ const descriptorSurface = resolveActiveNavigationSurfaces(navigationContext, runtime.area, runtime.viewer)
85
+ .find((candidate) => candidate.navKey === normalizedNavKey);
74
86
  if (!descriptorSurface && !overlay) {
75
87
  return null;
76
88
  }
@@ -46,6 +46,15 @@ export declare function readPhiBulletListStyle(value: unknown): PhiBulletListSty
46
46
  export declare function resolvePhiBulletListStyle(style: PhiBulletListStyle | null | undefined): PhiResolvedBulletListStyle;
47
47
  /** The level an entry at `depth` (1 for the outermost) is drawn at. */
48
48
  export declare function resolvePhiBulletListLevel(depth: number): PhiBulletListLevel;
49
+ /**
50
+ * What an entry says about the thing the list belongs to, where the list is a list of what is in it -- a
51
+ * plan's features. `included` draws a tick, `excluded` a dash and the entry's words quieter; `neutral`
52
+ * is the list's own marker, and is what an entry without a state is. Stored only where it is not neutral.
53
+ */
54
+ export declare const PHI_BULLET_LIST_ENTRY_STATES: readonly ["included", "excluded", "neutral"];
55
+ export type PhiBulletListEntryState = (typeof PHI_BULLET_LIST_ENTRY_STATES)[number];
56
+ /** The state after `state`, in the order a click on a marker steps through them. */
57
+ export declare function nextPhiBulletListEntryState(state: PhiBulletListEntryState | undefined): PhiBulletListEntryState;
49
58
  /**
50
59
  * One entry of a list written as a list: its text, how deep it stands, and the values its text names.
51
60
  *
@@ -58,11 +67,17 @@ export type PhiBulletListEntry = {
58
67
  /** 1 for the outermost level, which is what an entry without one stands at. */
59
68
  level?: number;
60
69
  values?: string[];
70
+ /** Absent is `neutral`. */
71
+ state?: Exclude<PhiBulletListEntryState, "neutral">;
61
72
  };
62
73
  export declare function readPhiBulletListEntries(value: unknown): PhiBulletListEntry[];
63
74
  /** An entry of the tree the Control draws: what it shows, and the entries one level below it. */
64
75
  export type PhiBulletListNode<Content> = {
65
76
  content: Content;
77
+ /** Absent is `neutral`: the marker of the entry's level. */
78
+ state?: PhiBulletListEntryState;
79
+ /** Set by an editor: the marker becomes a button that steps the entry's state on. */
80
+ onMarkerClick?: () => void;
66
81
  children?: PhiBulletListNode<Content>[];
67
82
  };
68
83
  /**
@@ -72,7 +87,8 @@ export type PhiBulletListNode<Content> = {
72
87
  */
73
88
  export declare function buildPhiBulletListTree<Entry extends {
74
89
  level?: number;
75
- }, Content>(entries: readonly Entry[], content: (entry: Entry, index: number) => Content): PhiBulletListNode<Content>[];
90
+ state?: PhiBulletListEntryState;
91
+ }, Content>(entries: readonly Entry[], content: (entry: Entry, index: number) => Content, onMarkerClick?: (entry: Entry, index: number) => void): PhiBulletListNode<Content>[];
76
92
  /**
77
93
  * The Inspector fields for a list style stored under `key`, under a divider titled `heading`. Every Widget
78
94
  * with a list offers these, so a list is set the same way wherever it stands.
@@ -69,6 +69,17 @@ export function resolvePhiBulletListLevel(depth) {
69
69
  const deepest = PHI_BULLET_LIST_LEVELS[PHI_BULLET_LIST_LEVELS.length - 1];
70
70
  return Math.min(Math.max(1, Math.trunc(depth) || 1), deepest);
71
71
  }
72
+ /**
73
+ * What an entry says about the thing the list belongs to, where the list is a list of what is in it -- a
74
+ * plan's features. `included` draws a tick, `excluded` a dash and the entry's words quieter; `neutral`
75
+ * is the list's own marker, and is what an entry without a state is. Stored only where it is not neutral.
76
+ */
77
+ export const PHI_BULLET_LIST_ENTRY_STATES = ["included", "excluded", "neutral"];
78
+ /** The state after `state`, in the order a click on a marker steps through them. */
79
+ export function nextPhiBulletListEntryState(state) {
80
+ const index = PHI_BULLET_LIST_ENTRY_STATES.indexOf(state ?? "neutral");
81
+ return PHI_BULLET_LIST_ENTRY_STATES[(index + 1) % PHI_BULLET_LIST_ENTRY_STATES.length];
82
+ }
72
83
  export function readPhiBulletListEntries(value) {
73
84
  if (!Array.isArray(value)) {
74
85
  return [];
@@ -82,10 +93,12 @@ export function readPhiBulletListEntries(value) {
82
93
  const values = Array.isArray(record.values)
83
94
  ? record.values.map((item) => (typeof item === "string" || typeof item === "number" ? String(item) : ""))
84
95
  : [];
96
+ const state = record.state === "included" || record.state === "excluded" ? record.state : undefined;
85
97
  return [{
86
98
  text: record.text,
87
99
  ...(level > 1 ? { level } : {}),
88
100
  ...(values.length > 0 ? { values } : {}),
101
+ ...(state ? { state } : {}),
89
102
  }];
90
103
  });
91
104
  }
@@ -94,14 +107,18 @@ export function readPhiBulletListEntries(value) {
94
107
  * more than a level hangs one level below it all the same: a level cannot be skipped in a nested list,
95
108
  * and an entry that tried stays with the entry it follows rather than jumping.
96
109
  */
97
- export function buildPhiBulletListTree(entries, content) {
110
+ export function buildPhiBulletListTree(entries, content, onMarkerClick) {
98
111
  const root = [];
99
112
  const open = [root];
100
113
  entries.forEach((entry, index) => {
101
114
  const wanted = resolvePhiBulletListLevel(entry.level ?? 1);
102
115
  const level = Math.min(wanted, open.length);
103
116
  open.length = level;
104
- const node = { content: content(entry, index) };
117
+ const node = {
118
+ content: content(entry, index),
119
+ ...(entry.state && entry.state !== "neutral" ? { state: entry.state } : {}),
120
+ ...(onMarkerClick ? { onMarkerClick: () => onMarkerClick(entry, index) } : {}),
121
+ };
105
122
  open[level - 1].push(node);
106
123
  node.children = [];
107
124
  open.push(node.children);
@@ -4,8 +4,8 @@ import type { PhiImagePresentation } from "../../media/image-presentation";
4
4
  import type { PhiClientBlockBaseProps } from "../../../types";
5
5
  import type { PhiSurface } from "../../../types/surface";
6
6
  import type { PhiSignalRouteSet } from "../../../types/signals";
7
- import type { PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody } from "./card-vocabulary";
8
- export type { PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody, } from "./card-vocabulary";
7
+ import type { PhiCardBillingPeriod, PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody } from "./card-vocabulary";
8
+ export type { PhiCardBillingPeriod, PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody, } from "./card-vocabulary";
9
9
  export type PhiCardWidgetClientLabels = {
10
10
  eyebrow?: string;
11
11
  /**
@@ -19,6 +19,13 @@ export type PhiCardWidgetClientLabels = {
19
19
  actionLabel?: string;
20
20
  /** The figure a `stat` body draws. Data rather than copy, and never translated. */
21
21
  value?: string;
22
+ /** The band at the card's end edge. */
23
+ badge?: string;
24
+ /** A `plan` body's prices, data and never translated, and their periods, which are words. */
25
+ price?: string;
26
+ pricePeriod?: string;
27
+ priceYearly?: string;
28
+ pricePeriodYearly?: string;
22
29
  };
23
30
  /**
24
31
  * What is true about a card's content right now, as opposed to what the card says.
@@ -63,6 +70,10 @@ export type PhiCardWidgetClientConfig = {
63
70
  variant?: PhiCardVariant;
64
71
  body?: PhiCardWidgetBody;
65
72
  highlight?: boolean;
73
+ /** The badge's ground; absent, the Theme's primary. */
74
+ badgeColor?: string;
75
+ /** Which price a `plan` shows until a `billingPeriod` signal says otherwise. Absent is `monthly`. */
76
+ billingPeriod?: PhiCardBillingPeriod;
66
77
  hoverEffect?: PhiCardHoverEffect;
67
78
  /** The card's wired signals (`PHI_CARD_SIGNALS`); absent where nothing is wired, as on the canvas. */
68
79
  signalRoutes?: PhiSignalRouteSet | null;
@@ -78,10 +89,11 @@ export type PhiCardWidgetAction = {
78
89
  export type PhiCardWidgetClientProps = PhiClientBlockBaseProps<PhiCardWidgetClientLabels, PhiCardWidgetClientConfig> & {
79
90
  binding?: PhiCardWidgetClientBinding;
80
91
  /**
81
- * What a `list` body draws, already drawn.
92
+ * What a `list` body draws, and a `plan` body's features, already drawn.
82
93
  *
83
94
  * Handed in rather than drawn here: the Card is on Public pages, and a list Control imported by it
84
- * would load on every page with a Card. A Dashboard draws its entries and passes them.
95
+ * would load on every page with a Card. A Dashboard draws its entries and passes them; a plan card's
96
+ * server half draws its features on the server, and its editor draws them as fields.
85
97
  */
86
98
  listContent?: ReactNode;
87
99
  /**
@@ -4,6 +4,7 @@ import { useState } from "react";
4
4
  import { PhiButtonControl } from "../../controls/phi-button-control";
5
5
  import { PhiFlexControl } from "../../controls/phi-flex-control";
6
6
  import { PhiImageControl } from "../../controls/phi-image-control";
7
+ import { PhiRibbonControl } from "../../controls/phi-ribbon-control";
7
8
  import { PhiSkeletonControl } from "../../controls/phi-skeleton-control";
8
9
  import { PhiStatisticControl } from "../../controls/phi-statistic-control";
9
10
  import { PhiTypographyControl } from "../../controls/phi-typography-control";
@@ -14,8 +15,9 @@ import { PhiSurfaceGroundLayer } from "../../surface/phi-surface-ground";
14
15
  import { PhiSurfaceTone } from "../../surface/phi-surface-tone";
15
16
  import { resolvePhiSurfaceStyle } from "../../../helpers/surface-style";
16
17
  import { usePhiControlSignalController } from "../client/shared/phi-control-signals";
17
- import { PHI_SPACE } from "../../../theme/antd-css-var-contract";
18
- const PHI_CARD_SIGNAL_TEXT_SLOTS = new Set(["eyebrow", "title", "description", "meta", "value"]);
18
+ import { PHI_COLOR, PHI_SPACE } from "../../../theme/antd-css-var-contract";
19
+ import { PHI_CARD_TEXT_SLOTS, readPhiCardBillingPeriod } from "./card-vocabulary";
20
+ const PHI_CARD_SIGNAL_TEXT_SLOTS = new Set(PHI_CARD_TEXT_SLOTS);
19
21
  /*
20
22
  * What each variant sets, in one row per variant. A further variant -- a featured card -- is a row here
21
23
  * and an entry in `PHI_CARD_VARIANTS`.
@@ -71,13 +73,19 @@ const PHI_CARD_HIDDEN_TEXT = {
71
73
  whiteSpace: "nowrap",
72
74
  border: 0,
73
75
  };
74
- const PHI_CARD_THEME_CORNER = "var(--phi-surface-radius, var(--ant-border-radius-lg))";
76
+ const PHI_CARD_THEME_CORNER = "var(--phi-surface-radius)";
75
77
  /** The box a picture is shown in takes the picture's own proportion; 3:2 where nothing is known. */
76
78
  function resolveMediaAspectRatio(presentation) {
77
79
  return presentation.width && presentation.height
78
80
  ? `${presentation.width} / ${presentation.height}`
79
81
  : "3 / 2";
80
82
  }
83
+ /*
84
+ * Every colour the card paints is the Theme's variable (`PHI_COLOR`), never a token's resolved value: a
85
+ * Surface with a `tone` writes the other mode's variables on the card's box, and only a variable reaches
86
+ * the words inside it. Resolved values were the page's mode written in by hand, and dark words stayed
87
+ * dark on a photograph darkened for light ones.
88
+ */
81
89
  export function PhiCardWidgetClient({ labels: placedLabels, config, binding, listContent, renderText, }) {
82
90
  const { token } = usePhiConfig();
83
91
  const [overrides, setOverrides] = useState({});
@@ -94,6 +102,11 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
94
102
  else if (capabilityId === "loading" || capabilityId === "highlight") {
95
103
  setOverrides((current) => ({ ...current, [capabilityId]: signal.value === true }));
96
104
  }
105
+ else if (capabilityId === "billingPeriod") {
106
+ const billingPeriod = readPhiCardBillingPeriod(signal.value);
107
+ if (billingPeriod)
108
+ setOverrides((current) => ({ ...current, billingPeriod }));
109
+ }
97
110
  return true;
98
111
  },
99
112
  });
@@ -157,15 +170,28 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
157
170
  const title = textOf("title");
158
171
  const description = textOf("description");
159
172
  const meta = textOf("meta");
173
+ const badge = textOf("badge");
160
174
  const eyebrowType = { fontSize: token.fontSizeSM, letterSpacing: "0.04em", textTransform: "uppercase" };
161
175
  /* On the picture's corner where the variant puts it there and there is a picture to put it on. */
162
176
  const eyebrowOnMedia = metrics.eyebrowOnMedia === true && image != null;
163
- const heading = title != null ? (_jsx(PhiTypographyControl, { presentation: "title", level: Number(headingLevel.slice(1)), style: { margin: 0, ...headingType[headingLevel], color: token.colorTextHeading, ...slotWidth }, children: title })) : null;
177
+ const heading = title != null ? (_jsx(PhiTypographyControl, { presentation: "title", level: Number(headingLevel.slice(1)), style: { margin: 0, ...headingType[headingLevel], color: PHI_COLOR.textHeading, ...slotWidth }, children: title })) : null;
164
178
  /*
165
179
  * The icon's colours as custom properties, which `.phi-card__icon` paints (`styles/layout.css`): the
166
180
  * `icon` hover effect turns them round, and a colour stated inline could not be turned by a rule.
167
181
  */
168
- const iconColours = (color, background) => ({ "--phi-card-icon-color": color, "--phi-card-icon-background": background });
182
+ const iconColours = (color, background) => ({
183
+ "--phi-card-icon-color": color,
184
+ "--phi-card-icon-background": background,
185
+ /* The mark's colour once the `icon` effect turns them round: a ground it can be read on. */
186
+ "--phi-card-icon-hover-color": config?.iconBackground ?? PHI_COLOR.bgContainer,
187
+ });
188
+ /*
189
+ * The corner of the icon's ground: the Theme's Control shape at the large size, scaled from a large
190
+ * Control's height to the ground's side. So `square` draws a square, `pill` a circle -- half the large
191
+ * height scaled to the side is half the side -- and the steps between keep their proportion, as a
192
+ * Segmented's items follow their track. The Theme's plain corner did not: a pill Site drew square marks.
193
+ */
194
+ const iconCorner = (side) => `calc(var(--phi-control-radius-lg, ${token.borderRadiusLG}px) * ${side / token.controlHeightLG})`;
169
195
  const iconMark = (size, framed) => iconName ? (_jsx("span", { "aria-hidden": "true", className: "phi-card__icon", style: {
170
196
  display: "inline-flex",
171
197
  flex: "none",
@@ -173,21 +199,37 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
173
199
  justifyContent: "center",
174
200
  width: framed,
175
201
  height: framed,
176
- borderRadius: token.borderRadius,
177
- ...iconColours(config?.iconColor ?? (highlight ? token.colorPrimary : token.colorTextSecondary), config?.iconBackground ?? token.colorFillQuaternary),
202
+ borderRadius: iconCorner(framed),
203
+ ...iconColours(config?.iconColor ?? (highlight ? PHI_COLOR.primary : PHI_COLOR.textSecondary), config?.iconBackground ?? PHI_COLOR.fillQuaternary),
178
204
  }, children: _jsx(PhiIcon, { name: iconName, size: size }) })) : null;
179
205
  /*
180
206
  * A failure stands where the figure would, at body size, because a sentence in figure type is
181
207
  * unreadable. A card whose figure is the only thing it has makes the figure the link.
182
208
  */
183
209
  const valueText = renderText ? renderText("value", labels.value) : null;
184
- const figure = valueText != null ? (_jsx("div", { style: { fontSize: token.fontSizeHeading3, color: highlight ? token.colorPrimary : token.colorTextHeading, ...slotWidth }, children: valueText })) : (_jsx(PhiStatisticControl, { value: binding?.error ?? labels.value ?? "", loading: binding?.loading === true || overrides.loading === true, styles: {
210
+ const figure = valueText != null ? (_jsx("div", { style: { fontSize: token.fontSizeHeading3, color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading, ...slotWidth }, children: valueText })) : (_jsx(PhiStatisticControl, { value: binding?.error ?? labels.value ?? "", loading: binding?.loading === true || overrides.loading === true, styles: {
185
211
  content: binding?.error
186
- ? { color: token.colorError, fontSize: token.fontSize }
187
- : { color: highlight ? token.colorPrimary : token.colorTextHeading },
212
+ ? { color: PHI_COLOR.error, fontSize: token.fontSize }
213
+ : { color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading },
188
214
  } }));
189
215
  /* A failure stands where the lines would, as it does for a figure; a list still coming is three lines. */
190
- const list = binding?.error ? (_jsx(PhiTypographyControl, { style: { color: token.colorError, ...slotWidth }, children: binding.error })) : binding?.loading === true || overrides.loading === true ? (_jsx(PhiSkeletonControl, { lines: 3 })) : listContent ?? null;
216
+ const list = binding?.error ? (_jsx(PhiTypographyControl, { style: { color: PHI_COLOR.error, ...slotWidth }, children: binding.error })) : binding?.loading === true || overrides.loading === true ? (_jsx(PhiSkeletonControl, { lines: 3 })) : listContent ?? null;
217
+ /*
218
+ * A plan's price: the period's own where it states one, the monthly one where it does not, so a switch
219
+ * to yearly never leaves a card without a price. An editor is asked for the shown period's fields.
220
+ */
221
+ const yearly = (overrides.billingPeriod ?? config?.billingPeriod) === "yearly";
222
+ const showsYearly = yearly && (renderText != null || Boolean(labels.priceYearly));
223
+ const priceText = body === "plan" ? textOf(showsYearly ? "priceYearly" : "price") : null;
224
+ const periodText = body === "plan" ? textOf(showsYearly ? "pricePeriodYearly" : "pricePeriod") : null;
225
+ const plan = body === "plan" ? (_jsxs("div", { style: { display: "grid", gap, justifySelf: "stretch", justifyItems, minWidth: 0 }, children: [priceText != null || periodText != null ? (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "baseline", justifyContent: justify, columnGap: PHI_SPACE.xs, ...slotWidth }, children: [priceText != null ? (_jsx("span", { className: "phi-card__price", style: {
226
+ fontSize: token.fontSizeHeading2,
227
+ lineHeight: token.lineHeightHeading2,
228
+ fontWeight: 600,
229
+ fontVariantNumeric: "tabular-nums",
230
+ color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading,
231
+ ...slotWidth,
232
+ }, children: priceText })) : null, periodText != null ? (_jsx("span", { style: { color: PHI_COLOR.textSecondary, ...slotWidth }, children: periodText })) : null] })) : null, list != null ? _jsx("div", { style: { justifySelf: "stretch", minWidth: 0, textAlign: "start" }, children: list }) : null] })) : null;
191
233
  const headingRow = iconPlacement === "inline" && iconName ? (_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: justify, gap, ...slotWidth }, children: [iconMark(iconSize, iconSize + 16), heading] })) : heading;
192
234
  const media = image ? (_jsxs("div", { className: "phi-card__media", style: {
193
235
  position: "relative",
@@ -208,9 +250,9 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
208
250
  justifyContent: "center",
209
251
  width: topIconSize + 24,
210
252
  height: topIconSize + 24,
211
- borderRadius: "50%",
253
+ borderRadius: iconCorner(topIconSize + 24),
212
254
  // A ground of its own, so the mark reads on a bright picture and on a dark one alike.
213
- ...iconColours(config?.iconColor ?? (highlight ? token.colorPrimary : token.colorText), config?.iconBackground ?? `color-mix(in srgb, ${token.colorBgContainer} 88%, transparent)`),
255
+ ...iconColours(config?.iconColor ?? (highlight ? PHI_COLOR.primary : PHI_COLOR.text), config?.iconBackground ?? `color-mix(in srgb, ${PHI_COLOR.bgContainer} 88%, transparent)`),
214
256
  boxShadow: token.boxShadowTertiary,
215
257
  }, children: _jsx(PhiIcon, { name: iconName, size: topIconSize }) })) : null, eyebrowOnMedia && eyebrow != null ? (_jsx(PhiTypographyControl, { type: "secondary", style: {
216
258
  ...eyebrowType,
@@ -222,8 +264,8 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
222
264
  paddingInline: token.paddingXS,
223
265
  borderRadius: token.borderRadiusSM,
224
266
  // A ground of its own, as the icon has, so the words read on any picture.
225
- background: `color-mix(in srgb, ${token.colorBgContainer} 88%, transparent)`,
226
- color: highlight ? token.colorPrimary : token.colorText,
267
+ background: `color-mix(in srgb, ${PHI_COLOR.bgContainer} 88%, transparent)`,
268
+ color: highlight ? PHI_COLOR.primary : PHI_COLOR.text,
227
269
  }, children: eyebrow })) : null] })) : null;
228
270
  const boxStyle = {
229
271
  ...(config?.surface ? {} : { borderRadius: PHI_CARD_THEME_CORNER }),
@@ -235,21 +277,36 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
235
277
  minWidth: 0,
236
278
  overflow: "hidden",
237
279
  textAlign,
238
- color: token.colorText,
280
+ color: PHI_COLOR.text,
239
281
  /* The ring's colour; `.phi-card--highlight` draws it (`styles/layout.css`). */
240
- ...(highlight ? { "--phi-card-highlight-color": token.colorPrimary } : {}),
282
+ ...(highlight ? { "--phi-card-highlight-color": PHI_COLOR.primary } : {}),
241
283
  };
242
- const content = (_jsxs(_Fragment, { children: [media, _jsxs("div", { style: { display: "grid", gap, padding: inset, justifyItems }, children: [iconAtTop && iconName && !image ? (_jsx("div", { style: { justifySelf: markSide === "center" ? "center" : markSide === "end" ? "end" : "start" }, children: iconMark(topIconSize, topIconSize + 24) })) : null, eyebrow != null && !eyebrowOnMedia && !labels.eyebrowEnd ? (_jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: highlight ? token.colorPrimary : token.colorTextTertiary, ...slotWidth }, children: eyebrow })) : null, labels.eyebrowEnd && !eyebrowOnMedia ? (_jsxs(PhiFlexControl, { justify: "space-between", align: "baseline", wrap: true, gap: PHI_SPACE.xs, style: { justifySelf: "stretch", minWidth: 0 }, children: [_jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: highlight ? token.colorPrimary : token.colorTextTertiary }, children: eyebrow ?? "" }), _jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: token.colorTextTertiary }, children: labels.eyebrowEnd })] })) : null, headingRow, body === "stat" ? figure : null, body === "list" && list != null ? (_jsx("div", { style: { justifySelf: "stretch", minWidth: 0 }, children: list })) : null, description != null ? (_jsx(PhiTypographyControl, { presentation: "paragraph", style: {
243
- marginBottom: 0,
244
- color: token.colorTextSecondary,
245
- fontSize: descriptionSize,
246
- ...slotWidth,
247
- }, children: description })) : null, meta != null ? (_jsx(PhiTypographyControl, { type: "secondary", style: { fontSize: token.fontSizeSM, color: token.colorTextTertiary, ...slotWidth }, children: meta })) : null, action ? (_jsx("div", { className: "phi-card__action", children: _jsx(PhiButtonControl, { type: highlight ? "primary" : "default", size: buttonSize, ...(action.href ? { href: action.href, newTab: action.newTab, external: action.external } : {}), label: labels.actionLabel || undefined, ...(labels.actionLabel ? {} : { ariaLabel: action.ariaLabel }), ...(action.icon ? { icon: _jsx(PhiIcon, { name: action.icon, size: "1em" }) } : {}), ...(activates ? { onClick: () => controlSignals.emitCapability("activate", null) } : {}) }) })) : null] }), cardLink] }));
284
+ /*
285
+ * A plan's button stands at the card's foot: plans side by side in a row have lists of different
286
+ * lengths, and their buttons belong on one line. The words keep their grid; the grid and the button
287
+ * stand in a column that fills the card, and the button takes what is left above it.
288
+ */
289
+ const footAction = body === "plan" && action != null;
290
+ const actionButton = action ? (_jsx("div", { className: "phi-card__action",
291
+ /* At the foot, it stands where the words' alignment puts it, as it does among them. */
292
+ style: footAction ? { marginTop: "auto", alignSelf: textAlign === "center" ? "center" : textAlign === "end" ? "flex-end" : "flex-start" } : undefined, children: _jsx(PhiButtonControl, { type: highlight ? "primary" : "default", size: buttonSize, ...(action.href ? { href: action.href, newTab: action.newTab, external: action.external } : {}), label: labels.actionLabel || undefined, ...(labels.actionLabel ? {} : { ariaLabel: action.ariaLabel }), ...(action.icon ? { icon: _jsx(PhiIcon, { name: action.icon, size: "1em" }) } : {}), ...(activates ? { onClick: () => controlSignals.emitCapability("activate", null) } : {}) }) })) : null;
293
+ /*
294
+ * The band in the card's top end corner, floating (`PhiRibbonControl`): the words below it start where
295
+ * they start in a card without one, so plans side by side keep their headings on one line.
296
+ */
297
+ const ribbon = badge != null ? (_jsx(PhiRibbonControl, { color: config?.badgeColor, placement: "corner", ...(renderText ? { style: { pointerEvents: "auto", minWidth: "6em" } } : {}), children: badge })) : null;
298
+ const words = (_jsxs("div", { style: { display: "grid", gap, padding: footAction ? 0 : inset, justifyItems }, children: [iconAtTop && iconName && !image ? (_jsx("div", { style: { justifySelf: markSide === "center" ? "center" : markSide === "end" ? "end" : "start" }, children: iconMark(topIconSize, topIconSize + 24) })) : null, eyebrow != null && !eyebrowOnMedia && !labels.eyebrowEnd ? (_jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: highlight ? PHI_COLOR.primary : PHI_COLOR.textTertiary, ...slotWidth }, children: eyebrow })) : null, labels.eyebrowEnd && !eyebrowOnMedia ? (_jsxs(PhiFlexControl, { justify: "space-between", align: "baseline", wrap: true, gap: PHI_SPACE.xs, style: { justifySelf: "stretch", minWidth: 0 }, children: [_jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: highlight ? PHI_COLOR.primary : PHI_COLOR.textTertiary }, children: eyebrow ?? "" }), _jsx(PhiTypographyControl, { type: "secondary", style: { ...eyebrowType, color: PHI_COLOR.textTertiary }, children: labels.eyebrowEnd })] })) : null, headingRow, body === "stat" ? figure : null, body === "list" && list != null ? (_jsx("div", { style: { justifySelf: "stretch", minWidth: 0 }, children: list })) : null, description != null ? (_jsx(PhiTypographyControl, { presentation: "paragraph", style: {
299
+ marginBottom: 0,
300
+ color: PHI_COLOR.textSecondary,
301
+ fontSize: descriptionSize,
302
+ ...slotWidth,
303
+ }, children: description })) : null, plan, meta != null ? (_jsx(PhiTypographyControl, { type: "secondary", style: { fontSize: token.fontSizeSM, color: PHI_COLOR.textTertiary, ...slotWidth }, children: meta })) : null, footAction ? null : actionButton] }));
304
+ const content = (_jsxs(_Fragment, { children: [media, footAction ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap, padding: inset, flex: "1 1 auto", minWidth: 0 }, children: [words, actionButton] })) : words, cardLink] }));
248
305
  return (_jsxs("article", { className: [
249
306
  "phi-card",
250
307
  href ? "phi-card--link" : null,
251
308
  highlight ? "phi-card--highlight" : null,
252
309
  hoverEffect !== "none" ? `phi-card--hover-${hoverEffect}` : null,
253
310
  surface.className,
254
- ].filter(Boolean).join(" "), "data-phi-card-variant": variant, style: boxStyle, children: [_jsx(PhiSurfaceGroundLayer, { ground: surface.ground, className: "phi-card__ground" }), surface.className ? _jsx(PhiSurfaceTone, { tone: config?.surface?.tone, children: content }) : content] }));
311
+ ].filter(Boolean).join(" "), "data-phi-card-variant": variant, style: boxStyle, children: [_jsx(PhiSurfaceGroundLayer, { ground: surface.ground, className: "phi-card__ground" }), ribbon, surface.className ? _jsx(PhiSurfaceTone, { tone: config?.surface?.tone, children: content }) : content] }));
255
312
  }
@@ -4,12 +4,24 @@
4
4
  * `text` is a heading. `stat` is a labelled figure: the label is the heading, so a card that counts
5
5
  * something says what it counted in the same place and type a text card says what it is about, and the
6
6
  * figure is drawn by the house's one statistic Control. `list` is a heading over a few lines the caller
7
- * draws and hands in (`listContent`), which only a Dashboard does today.
7
+ * draws and hands in (`listContent`), which only a Dashboard does today. `plan` is an offer: a price
8
+ * and what it is paid per, a monthly and a yearly one of each, over a list of what the plan includes and
9
+ * what it does not (`PhiBulletListEntry.state`). Several plan cards side by side are a Layout's row, and
10
+ * the recommended one is the card that is `highlight`ed.
8
11
  *
9
12
  * The body is chosen, never inferred from whether a value happens to have arrived. A card that decided
10
13
  * by presence would draw as text and reflow into a statistic the moment its figure landed.
11
14
  */
12
- export type PhiCardWidgetBody = "text" | "stat" | "list";
15
+ export type PhiCardWidgetBody = "text" | "stat" | "list" | "plan";
16
+ /**
17
+ * Which of a plan card's two prices it shows. A plan states one, the other, or both; one that states
18
+ * only the monthly price shows it under `yearly` as well, so a period switch on the page never leaves a
19
+ * card without a price. The page switches every plan at once by sending `billingPeriod`.
20
+ */
21
+ export declare const PHI_CARD_BILLING_PERIODS: readonly ["monthly", "yearly"];
22
+ export type PhiCardBillingPeriod = (typeof PHI_CARD_BILLING_PERIODS)[number];
23
+ /** A sent period as one of the two; anything else is not a period and changes nothing. */
24
+ export declare function readPhiCardBillingPeriod(value: unknown): PhiCardBillingPeriod | undefined;
13
25
  /**
14
26
  * The tag of the card's heading, and the step it is drawn at.
15
27
  *
@@ -48,6 +60,11 @@ export type PhiCardHoverEffect = (typeof PHI_CARD_HOVER_EFFECTS)[number];
48
60
  */
49
61
  export declare const PHI_CARD_VARIANTS: readonly ["default", "compact", "center"];
50
62
  export type PhiCardVariant = (typeof PHI_CARD_VARIANTS)[number];
51
- /** The words a card shows, each in its own place; what an editor can put a field in. */
52
- export declare const PHI_CARD_TEXT_SLOTS: readonly ["eyebrow", "title", "description", "meta", "value"];
63
+ /**
64
+ * The words a card shows, each in its own place; what an editor can put a field in, and what a signal can
65
+ * write. `badge` is the band at the card's end edge, any body may carry one. The prices are figures like
66
+ * `value` and are never translated: a price that differs by country is sent by a Controller or comes
67
+ * from a Provider's row, not from a translator. The periods (`/ month`) are words and are.
68
+ */
69
+ export declare const PHI_CARD_TEXT_SLOTS: readonly ["eyebrow", "title", "description", "meta", "value", "badge", "price", "pricePeriod", "priceYearly", "pricePeriodYearly"];
53
70
  export type PhiCardTextSlot = (typeof PHI_CARD_TEXT_SLOTS)[number];
@@ -2,6 +2,16 @@
2
2
  * The Card Widget's words, apart from its Client half: the Widget's definition is read on the server, and
3
3
  * a value imported from the Client file would make that file a client reference of every route.
4
4
  */
5
+ /**
6
+ * Which of a plan card's two prices it shows. A plan states one, the other, or both; one that states
7
+ * only the monthly price shows it under `yearly` as well, so a period switch on the page never leaves a
8
+ * card without a price. The page switches every plan at once by sending `billingPeriod`.
9
+ */
10
+ export const PHI_CARD_BILLING_PERIODS = ["monthly", "yearly"];
11
+ /** A sent period as one of the two; anything else is not a period and changes nothing. */
12
+ export function readPhiCardBillingPeriod(value) {
13
+ return PHI_CARD_BILLING_PERIODS.includes(value) ? value : undefined;
14
+ }
5
15
  /**
6
16
  * The tag of the card's heading, and the step it is drawn at.
7
17
  *
@@ -35,5 +45,21 @@ export const PHI_CARD_HOVER_EFFECTS = ["none", "lift", "zoom", "icon"];
35
45
  * not another branch in every size the card draws. `default` is what a card without one is.
36
46
  */
37
47
  export const PHI_CARD_VARIANTS = ["default", "compact", "center"];
38
- /** The words a card shows, each in its own place; what an editor can put a field in. */
39
- export const PHI_CARD_TEXT_SLOTS = ["eyebrow", "title", "description", "meta", "value"];
48
+ /**
49
+ * The words a card shows, each in its own place; what an editor can put a field in, and what a signal can
50
+ * write. `badge` is the band at the card's end edge, any body may carry one. The prices are figures like
51
+ * `value` and are never translated: a price that differs by country is sent by a Controller or comes
52
+ * from a Provider's row, not from a translator. The periods (`/ month`) are words and are.
53
+ */
54
+ export const PHI_CARD_TEXT_SLOTS = [
55
+ "eyebrow",
56
+ "title",
57
+ "description",
58
+ "meta",
59
+ "value",
60
+ "badge",
61
+ "price",
62
+ "pricePeriod",
63
+ "priceYearly",
64
+ "pricePeriodYearly",
65
+ ];
@@ -1,6 +1,6 @@
1
1
  import type { PhiClientBlockBaseProps, PhiNoLabels } from "../../../types";
2
2
  import type { PhiCodeToken } from "./markdown-code-tokens";
3
- import type { PhiMarkdownTextAlign } from "../../../types/core-widget-placements";
3
+ import type { PhiMarkdownImageFloat, PhiMarkdownTextAlign } from "../../../types/core-widget-placements";
4
4
  import { type PhiSpacingScaleKey } from "../config/spacing-options";
5
5
  import type { PhiMarkdownTocHeading } from "../../../types/markdown";
6
6
  import { type PhiBulletListStyle } from "./bullet-list-vocabulary";
@@ -75,6 +75,7 @@ export type PhiMarkdownWidgetClientProps = PhiClientBlockBaseProps<PhiNoLabels,
75
75
  tocKey?: string;
76
76
  headings?: PhiMarkdownTocHeading[];
77
77
  textAlign?: PhiMarkdownTextAlign;
78
+ imageFloat?: PhiMarkdownImageFloat;
78
79
  textBlockSpacingBefore?: PhiSpacingScaleKey;
79
80
  textBlockSpacingAfter?: PhiSpacingScaleKey;
80
81
  headingBlockSpacingBefore?: PhiSpacingScaleKey;
@@ -162,7 +162,9 @@ export function PhiMarkdownWidgetClient({ config }) {
162
162
  marginBlockStart: resolvePhiSpacingScaleValue(config?.headingBlockSpacingBefore ?? "none"),
163
163
  marginBlockEnd: resolvePhiSpacingScaleValue(config?.headingBlockSpacingAfter ?? "sm"),
164
164
  };
165
- return (_jsx("div", { style: { display: "grid", gap: 0, width: "100%", textAlign: config?.textAlign }, children: renderBlocks(blocks, token, paragraphBlockStyle, headingBlockStyle, {
165
+ return (_jsx("div", { className: config?.imageFloat ? `phi-markdown-body phi-markdown-body--float-${config.imageFloat}` : undefined, style: config?.imageFloat
166
+ ? { display: "flow-root", width: "100%", textAlign: config.textAlign, containerType: "inline-size" }
167
+ : { display: "grid", gap: 0, width: "100%", textAlign: config?.textAlign }, children: renderBlocks(blocks, token, paragraphBlockStyle, headingBlockStyle, {
166
168
  style: resolvePhiBulletListStyle(config?.list),
167
169
  depth: 1,
168
170
  }) }));
@@ -56,6 +56,13 @@ function normalizePotentialPhiScheme(value) {
56
56
  * accepted let `javascript:` and `data:` through here while the inline path refused them.
57
57
  */
58
58
  const PHI_EXTERNAL_DOCUMENT_URL_SCHEMES = /^(?:https?:|mailto:|tel:)$/iu;
59
+ /*
60
+ * A picture written into the text itself may also be a data URL of an image type -- the one way a Module
61
+ * brings a drawing with its copy, since it cannot put a file into the Site's `public/`. An `<img>` runs
62
+ * nothing it loads, an SVG's scripts included, so the type list is what keeps this a picture. Links
63
+ * stay on the scheme list above, and a fetched document gets no data URLs at all.
64
+ */
65
+ const PHI_INLINE_IMAGE_DATA_URL = /^data:image\/(?:png|jpeg|gif|webp|svg\+xml)[;,]/iu;
59
66
  function resolveExternalDocumentUrl(value, sourceUrl) {
60
67
  const normalized = value.trim();
61
68
  if (!normalized || normalizePotentialPhiScheme(normalized).startsWith("phis:"))
@@ -119,7 +126,7 @@ async function projectInternalMarkdownNodes(tree, runtime) {
119
126
  const src = resolved.assetUrls.get(reference.assetId);
120
127
  return src ? [{ ...node, url: src, children }] : [];
121
128
  }
122
- if (!/^https?:/iu.test(node.url ?? ""))
129
+ if (!/^https?:/iu.test(node.url ?? "") && !PHI_INLINE_IMAGE_DATA_URL.test(node.url ?? ""))
123
130
  return [];
124
131
  return [{ ...node, children }];
125
132
  }
@@ -328,21 +328,3 @@ export declare const PHI_MULTI_SELECT_CONTROL_SIGNALS: {
328
328
  valueType: "none";
329
329
  })[];
330
330
  };
331
- export declare const PHI_DRAG_SOURCE_CONTROL_SIGNALS: {
332
- emits: ({
333
- id: string;
334
- action: "start";
335
- valueType: "json";
336
- valueSchema: `${string}/signals/${string}`;
337
- } | {
338
- id: string;
339
- action: "change";
340
- valueType: "json";
341
- valueSchema: `${string}/signals/${string}`;
342
- } | {
343
- id: string;
344
- action: "stop";
345
- valueType: "json";
346
- valueSchema: `${string}/signals/${string}`;
347
- })[];
348
- };
@@ -89,6 +89,16 @@ export const PHI_CARD_SIGNALS = {
89
89
  { id: "description", channel: "description", action: "change", valueType: "string" },
90
90
  { id: "meta", channel: "meta", action: "change", valueType: "string" },
91
91
  { id: "highlight", channel: "highlight", action: "change", valueType: "boolean" },
92
+ { id: "badge", channel: "badge", action: "change", valueType: "string" },
93
+ /*
94
+ * A plan's prices, for a Controller that knows them better than the page does -- by the reader's
95
+ * country, say -- and the period a switch on the page shows (`monthly` or `yearly`).
96
+ */
97
+ { id: "price", channel: "price", action: "change", valueType: "string" },
98
+ { id: "pricePeriod", channel: "pricePeriod", action: "change", valueType: "string" },
99
+ { id: "priceYearly", channel: "priceYearly", action: "change", valueType: "string" },
100
+ { id: "pricePeriodYearly", channel: "pricePeriodYearly", action: "change", valueType: "string" },
101
+ { id: "billingPeriod", channel: "billingPeriod", action: "change", valueType: "string" },
92
102
  ],
93
103
  };
94
104
  export const PHI_TEXT_CONTROL_SIGNALS = {
@@ -227,25 +237,3 @@ export const PHI_MULTI_SELECT_CONTROL_SIGNALS = {
227
237
  { ...PHI_CONTROL_CLEAR_SIGNAL, channel: "selection" },
228
238
  ],
229
239
  };
230
- export const PHI_DRAG_SOURCE_CONTROL_SIGNALS = {
231
- emits: [
232
- {
233
- id: "dragStart",
234
- action: "start",
235
- valueType: "json",
236
- valueSchema: PHI_SIGNAL_VALUE_SCHEMAS.dragDrop,
237
- },
238
- {
239
- id: "dragChange",
240
- action: "change",
241
- valueType: "json",
242
- valueSchema: PHI_SIGNAL_VALUE_SCHEMAS.dragDrop,
243
- },
244
- {
245
- id: "dragEnd",
246
- action: "stop",
247
- valueType: "json",
248
- valueSchema: PHI_SIGNAL_VALUE_SCHEMAS.dragDrop,
249
- },
250
- ],
251
- };