@phis/ui 0.4.3 → 0.4.4

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 (132) hide show
  1. package/components/controls/phi-anchor-nav-control.js +4 -1
  2. package/components/controls/phi-background-control.js +21 -6
  3. package/components/controls/phi-bullet-list-control.d.ts +7 -2
  4. package/components/controls/phi-bullet-list-control.js +4 -3
  5. package/components/controls/phi-color-control.js +4 -2
  6. package/components/controls/phi-drawer-control-adapter.js +3 -2
  7. package/components/controls/phi-inline-text-editor.js +7 -1
  8. package/components/controls/phi-length-control.js +1 -1
  9. package/components/controls/phi-ribbon-control.d.ts +13 -4
  10. package/components/controls/phi-ribbon-control.js +10 -6
  11. package/components/controls/phi-ribbon-wrap-control.d.ts +26 -0
  12. package/components/controls/phi-ribbon-wrap-control.js +25 -0
  13. package/components/controls/phi-select-control.js +5 -2
  14. package/components/controls/phi-series-chart-control.d.ts +48 -0
  15. package/components/controls/phi-series-chart-control.js +82 -0
  16. package/components/controls/phi-text-control.js +57 -3
  17. package/components/controls/phi-tree-control.js +1 -1
  18. package/components/controls/phi-truncated-hover-text.d.ts +15 -0
  19. package/components/controls/phi-truncated-hover-text.js +25 -0
  20. package/components/forms/link-target-form-control.d.ts +1 -1
  21. package/components/forms/link-target-form-control.js +2 -45
  22. package/components/forms/phi-form-widget-frame.js +4 -2
  23. package/components/layouts/clients/phi-three-column-layout-client.js +10 -1
  24. package/components/navigation/phi-link.js +6 -1
  25. package/components/regions/clients/cms-region-container-client.js +6 -1
  26. package/components/regions/phi-cms-region-container.js +3 -0
  27. package/components/regions/phi-cms-region-shell.d.ts +12 -1
  28. package/components/regions/phi-cms-region-shell.js +81 -19
  29. package/components/root/phi-config-provider.d.ts +13 -0
  30. package/components/root/phi-config-provider.js +12 -1
  31. package/components/root/phi-root-background.d.ts +7 -0
  32. package/components/root/phi-root-background.js +20 -1
  33. package/components/root/phi-root-theme-resolver.js +3 -0
  34. package/components/root/phi-shell-chrome-overlay.d.ts +12 -1
  35. package/components/root/phi-shell-chrome-overlay.js +19 -3
  36. package/components/shell/phi-antd-icon.d.ts +1 -0
  37. package/components/shell/phi-antd-icon.js +3 -0
  38. package/components/shell/use-phi-shell-sticky-top.js +61 -16
  39. package/components/surface/phi-surface-tone.d.ts +13 -0
  40. package/components/surface/phi-surface-tone.js +21 -0
  41. package/components/widgets/client/bullet-list-entries-editor.js +7 -1
  42. package/components/widgets/client/html-editor.js +1 -1
  43. package/components/widgets/client/shared/use-page-reference-options.d.ts +17 -0
  44. package/components/widgets/client/shared/use-page-reference-options.js +57 -0
  45. package/components/widgets/config/background.d.ts +32 -1
  46. package/components/widgets/config/background.js +41 -2
  47. package/components/widgets/label-sets/background.js +2 -0
  48. package/components/widgets/label-types/background.d.ts +1 -0
  49. package/components/widgets/label-types/background.js +1 -0
  50. package/components/widgets/shared/bullet-list-vocabulary.d.ts +10 -0
  51. package/components/widgets/shared/bullet-list-vocabulary.js +5 -2
  52. package/components/widgets/shared/card-body-client.d.ts +5 -3
  53. package/components/widgets/shared/card-body-client.js +49 -6
  54. package/components/widgets/shared/card-vocabulary.d.ts +12 -2
  55. package/components/widgets/shared/card-vocabulary.js +7 -0
  56. package/components/widgets/shared/markdown-body-client.js +39 -10
  57. package/helpers/cms-container-chrome.js +6 -1
  58. package/helpers/locale.d.ts +8 -0
  59. package/helpers/locale.js +18 -0
  60. package/helpers/shell-region-style.js +2 -1
  61. package/helpers/surface-style.d.ts +12 -1
  62. package/helpers/surface-style.js +28 -2
  63. package/module-dashboard-cards.d.ts +1 -1
  64. package/module-dashboard-cards.js +1 -1
  65. package/package.json +1 -1
  66. package/plugins/runtime-modules/area-definitions.d.ts +0 -42
  67. package/plugins/runtime-modules/area-definitions.js +3 -8
  68. package/plugins/runtime-modules/builder/clients/inspector-config-field.js +20 -1
  69. package/plugins/runtime-modules/builder/clients/layout-scaffold-overlays.js +10 -5
  70. package/plugins/runtime-modules/builder/edit-scaffold-drawer.js +3 -2
  71. package/plugins/runtime-modules/builder/render-root-node-scaffold.js +25 -15
  72. package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +4 -0
  73. package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +15 -4
  74. package/plugins/runtime-modules/client-authoring-module.d.ts +8 -1
  75. package/plugins/runtime-modules/client-authoring-module.js +13 -3
  76. package/plugins/runtime-modules/core/widgets/brand/client.d.ts +12 -3
  77. package/plugins/runtime-modules/core/widgets/brand/client.js +3 -11
  78. package/plugins/runtime-modules/core/widgets/brand/home.d.ts +15 -0
  79. package/plugins/runtime-modules/core/widgets/brand/home.js +18 -0
  80. package/plugins/runtime-modules/core/widgets/brand/plugin.js +3 -2
  81. package/plugins/runtime-modules/core/widgets/brand/server.d.ts +2 -2
  82. package/plugins/runtime-modules/core/widgets/brand/server.js +2 -2
  83. package/plugins/runtime-modules/core/widgets/card/config.d.ts +6 -1
  84. package/plugins/runtime-modules/core/widgets/card/config.js +19 -3
  85. package/plugins/runtime-modules/core/widgets/card/editor.js +13 -3
  86. package/plugins/runtime-modules/core/widgets/card/server.js +1 -0
  87. package/plugins/runtime-modules/core/widgets/description/client.js +3 -3
  88. package/plugins/runtime-modules/core/widgets/description/config.d.ts +1 -0
  89. package/plugins/runtime-modules/core/widgets/description/editor.js +3 -2
  90. package/plugins/runtime-modules/core/widgets/description/server.js +3 -2
  91. package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +1 -0
  92. package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +1 -0
  93. package/plugins/runtime-modules/core/widgets/header-navigation/client.js +2 -2
  94. package/plugins/runtime-modules/core/widgets/html/client.js +1 -1
  95. package/plugins/runtime-modules/core/widgets/markdown/config.d.ts +320 -2
  96. package/plugins/runtime-modules/core/widgets/markdown/config.js +18 -4
  97. package/plugins/runtime-modules/core/widgets/markdown/editor.js +9 -1
  98. package/plugins/runtime-modules/core/widgets/markdown-toc/client.js +6 -1
  99. package/plugins/runtime-modules/core/widgets/markdown-toc/config.d.ts +14 -0
  100. package/plugins/runtime-modules/core/widgets/markdown-toc/config.js +14 -2
  101. package/plugins/runtime-modules/core/widgets/quick-links/authoring.js +7 -2
  102. package/plugins/runtime-modules/core/widgets/quick-links/client.js +2 -2
  103. package/plugins/runtime-modules/core/widgets/quick-links/items.d.ts +0 -2
  104. package/plugins/runtime-modules/core/widgets/quick-links/items.js +0 -15
  105. package/plugins/runtime-modules/core/widgets/result/config.d.ts +1 -0
  106. package/plugins/runtime-modules/core/widgets/sidebar-navigation/authoring.js +1 -1
  107. package/plugins/runtime-modules/core/widgets/sidebar-navigation/client.js +1 -1
  108. package/plugins/runtime-modules/core/widgets/simple-text/client.js +8 -3
  109. package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +1 -0
  110. package/plugins/runtime-modules/core/widgets/simple-text/editor.js +2 -2
  111. package/plugins/runtime-modules/core/widgets/table/config.d.ts +3 -0
  112. package/plugins/runtime-modules/dashboard/card-rows.d.ts +3 -1
  113. package/plugins/runtime-modules/dashboard/card-rows.js +30 -2
  114. package/plugins/runtime-modules/dashboard/clients/dashboard-card-view.js +29 -7
  115. package/plugins/runtime-modules/dashboard/trees/phi-default-builder-dashboard-page-tree.js +5 -40
  116. package/plugins/runtime-modules/theme/widgets/brand-controls/client.js +19 -15
  117. package/plugins/runtime-modules/theme/widgets/brand-controls/config.js +5 -9
  118. package/server-helpers/cms-link-targets.js +6 -0
  119. package/styles/controls.css +64 -2
  120. package/styles/layout.css +45 -2
  121. package/styles/shell.css +73 -0
  122. package/theme/phi-antd-token-resolver.js +20 -1
  123. package/theme/phi-theme-tokens.d.ts +1 -1
  124. package/theme/phi-theme-tokens.js +1 -0
  125. package/theme/phi-theme.d.ts +5 -0
  126. package/theme/phi-theme.js +21 -0
  127. package/theme/phi-tokens.d.ts +16 -15
  128. package/theme/phi-tokens.js +16 -15
  129. package/types/cms-plugins.d.ts +2 -0
  130. package/types/dashboard-cards.d.ts +39 -4
  131. package/types/dashboard-cards.js +6 -0
  132. package/types/site-theme.d.ts +10 -2
@@ -38,6 +38,7 @@ const PHI_ANTD_ICON_LOADERS = {
38
38
  CalendarOutlined: () => import("@ant-design/icons-svg/es/asn/CalendarOutlined"),
39
39
  CheckOutlined: () => import("@ant-design/icons-svg/es/asn/CheckOutlined"),
40
40
  CloseOutlined: () => import("@ant-design/icons-svg/es/asn/CloseOutlined"),
41
+ CloudDownloadOutlined: () => import("@ant-design/icons-svg/es/asn/CloudDownloadOutlined"),
41
42
  CloudUploadOutlined: () => import("@ant-design/icons-svg/es/asn/CloudUploadOutlined"),
42
43
  ClusterOutlined: () => import("@ant-design/icons-svg/es/asn/ClusterOutlined"),
43
44
  CodeOutlined: () => import("@ant-design/icons-svg/es/asn/CodeOutlined"),
@@ -327,6 +328,8 @@ export const PHI_ANTD_ICON_NAMES = {
327
328
  "video-camera-two-tone": "VideoCameraTwoTone",
328
329
  warning: "WarningOutlined",
329
330
  "warning-outlined": "WarningOutlined",
331
+ "cloud-download": "CloudDownloadOutlined",
332
+ "cloud-download-outlined": "CloudDownloadOutlined",
330
333
  "cloud-upload": "CloudUploadOutlined",
331
334
  "cloud-upload-outlined": "CloudUploadOutlined",
332
335
  "eye-invisible": "EyeInvisibleOutlined",
@@ -1,32 +1,35 @@
1
1
  "use client";
2
2
  import { useEffect } from "react";
3
+ import { PHI_SHELL_STICKY_TOP_PROPERTY } from "../root/phi-shell-chrome-overlay";
3
4
  /**
4
5
  * How far down the viewport the Shell's Header holds right now, published on the Shell.
5
6
  *
6
7
  * Something that sticks to the top of the page -- a Table's header row -- has to stick under the Header
7
8
  * rather than behind it, and where the Header ends is not a constant: each band sticks or scrolls by its
8
9
  * own config, and a revealing `header_top` slides away while the visitor reads down and comes back when
9
- * they scroll up. So the edge is measured, not configured: the lowest bottom of every Header band that is
10
- * stuck at this moment, written as `--phi-shell-sticky-top` on the Shell element. Unset, a reader falls
10
+ * they scroll up. So the edge is measured, not configured: the lowest visible bottom of the Header bands,
11
+ * stuck or in the flow, written as `--phi-shell-sticky-top` on the Shell element. Unset, a reader falls
11
12
  * back to `0px`, which is right for a Shell without a sticky Header.
12
13
  *
13
- * Run by whatever sticks under it -- every sticky Table does -- so a page without one loads nothing for
14
- * it. Each runner measures all the bands and they all write the same value. Not inside an Overlay: a
15
- * Modal's or Drawer's body scrolls on its own, and its top edge is the line.
14
+ * Run by whatever sticks under it -- every sticky Table does, and the Region renderer for a stuck Sider
15
+ * (`phiCmsRegionSiderSticks`) -- so a page without one loads nothing for it. Each runner measures all the
16
+ * bands and they all write the same value. Not inside an Overlay: a Modal's or Drawer's body scrolls on
17
+ * its own, and its top edge is the line.
16
18
  */
17
- const PHI_SHELL_STICKY_TOP_PROPERTY = "--phi-shell-sticky-top";
18
19
  const HEADER_BANDS = '[data-phi-region-key^="header_"]:not([hidden])';
19
20
  function measurePhiShellStickyTop(shell) {
21
+ /*
22
+ * The lowest visible bottom of the Header bands, stuck or not. Counting only the bands stuck at this
23
+ * moment made the edge jump: a band leaving its sticking line for its place in the flow -- the main
24
+ * band while the top band scrolls back in above it -- dropped out of the count at once, and whatever
25
+ * stuck under it leapt up by the band's height. Counted while it is in view, the edge moves with the
26
+ * scroll instead. A band in the flow stands above the content anyway, so nothing sticks too low for it.
27
+ */
20
28
  let bottom = 0;
21
29
  for (const band of shell.querySelectorAll(HEADER_BANDS)) {
22
- const style = window.getComputedStyle(band);
23
- if (style.position !== "sticky" && style.position !== "fixed") {
24
- continue;
25
- }
26
30
  const rect = band.getBoundingClientRect();
27
- const stuckAt = Number.parseFloat(style.top) || 0;
28
- // Below its sticking line it is still in flow; above the viewport it has slid away.
29
- if (rect.height === 0 || rect.top > stuckAt + 1 || rect.bottom <= 0) {
31
+ // Empty, or scrolled or slid out of the viewport.
32
+ if (rect.height === 0 || rect.bottom <= 0) {
30
33
  continue;
31
34
  }
32
35
  bottom = Math.max(bottom, rect.bottom);
@@ -57,10 +60,46 @@ export function usePhiShellStickyTop({ active, elementRef, }) {
57
60
  }
58
61
  };
59
62
  publish();
63
+ /*
64
+ * A revealing band moves by a transition. Measured only where it settled, whatever sticks under it
65
+ * would stand still while the band slides and then jump; so while a transition runs in the Shell the
66
+ * edge is measured every frame, and once more where it ends.
67
+ */
68
+ let following = 0;
69
+ let followFrame = 0;
70
+ const follow = () => {
71
+ publish();
72
+ followFrame = following > 0 ? window.requestAnimationFrame(follow) : 0;
73
+ };
74
+ // Only a Header band's own transitions move the edge; a button's hover elsewhere in the Shell does not.
75
+ const isBandTransition = (event) => event.target instanceof Element && event.target.closest(HEADER_BANDS) !== null;
76
+ // A transition whose element leaves the page never ends; the follow stops on its own regardless.
77
+ let followTimeout = 0;
78
+ const stopFollowing = () => {
79
+ following = 0;
80
+ followTimeout = 0;
81
+ };
82
+ const transitionStarted = (event) => {
83
+ if (!isBandTransition(event))
84
+ return;
85
+ following += 1;
86
+ window.clearTimeout(followTimeout);
87
+ followTimeout = window.setTimeout(stopFollowing, 2000);
88
+ if (followFrame === 0) {
89
+ followFrame = window.requestAnimationFrame(follow);
90
+ }
91
+ };
92
+ const transitionStopped = (event) => {
93
+ if (!isBandTransition(event))
94
+ return;
95
+ following = Math.max(0, following - 1);
96
+ schedule();
97
+ };
60
98
  window.addEventListener("scroll", schedule, { passive: true });
61
99
  window.addEventListener("resize", schedule);
62
- // A revealing band moves by a transition; its end is where it settled.
63
- shell.addEventListener("transitionend", schedule);
100
+ shell.addEventListener("transitionrun", transitionStarted);
101
+ shell.addEventListener("transitionend", transitionStopped);
102
+ shell.addEventListener("transitioncancel", transitionStopped);
64
103
  const observer = new ResizeObserver(schedule);
65
104
  for (const band of shell.querySelectorAll(HEADER_BANDS)) {
66
105
  observer.observe(band);
@@ -69,9 +108,15 @@ export function usePhiShellStickyTop({ active, elementRef, }) {
69
108
  if (frame !== 0) {
70
109
  window.cancelAnimationFrame(frame);
71
110
  }
111
+ if (followFrame !== 0) {
112
+ window.cancelAnimationFrame(followFrame);
113
+ }
114
+ window.clearTimeout(followTimeout);
72
115
  window.removeEventListener("scroll", schedule);
73
116
  window.removeEventListener("resize", schedule);
74
- shell.removeEventListener("transitionend", schedule);
117
+ shell.removeEventListener("transitionrun", transitionStarted);
118
+ shell.removeEventListener("transitionend", transitionStopped);
119
+ shell.removeEventListener("transitioncancel", transitionStopped);
75
120
  observer.disconnect();
76
121
  };
77
122
  }, [active, elementRef]);
@@ -16,3 +16,16 @@ export declare function PhiSurfaceTone({ tone, children, }: {
16
16
  tone: PhiSurfaceToneValue | null | undefined;
17
17
  children: ReactNode;
18
18
  }): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
19
+ /**
20
+ * The page's own mode around something that belongs to the page's tools rather than to its content.
21
+ *
22
+ * The Builder draws its scaffolds -- tool bars, drag handles, the effects preview -- inside the Surfaces
23
+ * they edit, and a toned Surface would turn them with its content. This turns them back: inside a tone
24
+ * scope it mounts the page's mode again (its Ant Design components take it by themselves), outside one it
25
+ * renders the children as they are. `className` is the class an element of the caller's that reads the
26
+ * Theme's variables itself must carry, so they resolve in the page's mode there too; empty outside a tone.
27
+ */
28
+ export declare function usePhiPageModeTone(): {
29
+ className: string | undefined;
30
+ wrap: (children: ReactNode) => ReactNode;
31
+ };
@@ -34,3 +34,24 @@ export function PhiSurfaceTone({ tone, children, }) {
34
34
  }
35
35
  return (_jsx(Suspense, { fallback: null, children: _jsx(PhiSurfaceToneScope, { mode: target, cssVarKey: cssVarKey, source: source, children: children }) }));
36
36
  }
37
+ /**
38
+ * The page's own mode around something that belongs to the page's tools rather than to its content.
39
+ *
40
+ * The Builder draws its scaffolds -- tool bars, drag handles, the effects preview -- inside the Surfaces
41
+ * they edit, and a toned Surface would turn them with its content. This turns them back: inside a tone
42
+ * scope it mounts the page's mode again (its Ant Design components take it by themselves), outside one it
43
+ * renders the children as they are. `className` is the class an element of the caller's that reads the
44
+ * Theme's variables itself must carry, so they resolve in the page's mode there too; empty outside a tone.
45
+ */
46
+ export function usePhiPageModeTone() {
47
+ const { mode } = usePhiConfig();
48
+ const source = usePhiThemeToneSource();
49
+ const pageMode = source?.pageMode ?? mode;
50
+ if (pageMode === mode) {
51
+ return { className: undefined, wrap: (children) => children };
52
+ }
53
+ return {
54
+ className: resolvePhiSurfaceToneClassName(pageMode),
55
+ wrap: (children) => _jsx(PhiSurfaceTone, { tone: pageMode, children: children }),
56
+ };
57
+ }
@@ -99,5 +99,11 @@ export function PhiBulletListEntriesEditor({ entries, listStyle, onCommit, state
99
99
  resize: "none",
100
100
  color: "inherit",
101
101
  ...fieldStyle,
102
- } })), states && onCommit ? (_entry, index) => stepState(index) : undefined) }) }));
102
+ },
103
+ /*
104
+ * The text's own padding, stated: half of what the line leaves of the field's height, so the
105
+ * first line's middle is the marker's (`markerLineHeight` above). That is the padding the
106
+ * field had from Ant Design already; stated, the clear button can find the line too.
107
+ */
108
+ inputStyle: { paddingBlock: "calc((var(--ant-control-height) - 1lh) / 2)" } })), states && onCommit ? (_entry, index) => stepState(index) : undefined) }) }));
103
109
  }
@@ -388,7 +388,7 @@ export function PhiHtmlWidgetEditor({ blockId, html, fontFamily, fontSize, onCha
388
388
  const initialHtmlRef = useRef(normalizedHtml);
389
389
  const latestHtmlRef = useRef(normalizedHtml);
390
390
  const resolvedFontFamily = resolvePhiWidgetFontFamily(fontFamily, fonts, token);
391
- const resolvedFontSize = resolvePhiWidgetFontSize(fontSize, token, "lg");
391
+ const resolvedFontSize = resolvePhiWidgetFontSize(fontSize, token, "base");
392
392
  const editable = onChange != null;
393
393
  useEffect(() => {
394
394
  latestHtmlRef.current = normalizedHtml;
@@ -0,0 +1,17 @@
1
+ import type { PhiCmsAreaKey } from "../../../../constants/cms-areas";
2
+ import type { PhiTreeOption } from "../../../../types/tree";
3
+ import type { PhiPageReferenceSelection } from "./phi-page-reference-picker";
4
+ type PageReferenceOptions = PhiTreeOption<PhiPageReferenceSelection>[];
5
+ /**
6
+ * The Pages of one Area a link may point at, as the tree `PhiLinkTargetControl` offers, for a surface
7
+ * that stands outside the Builder's own catalog: a Form's `link-target` field, the Theme's Brand.
8
+ *
9
+ * Answered by `/api/site/page-references` (`gateway/page-references-route.ts`), which merges the Area's
10
+ * Module Pages and Core's the way the Builder's picker does. `failed` is set when the list could not be
11
+ * read, so the surface can say so instead of showing an empty list that looks like a Site without Pages.
12
+ */
13
+ export declare function usePhiPageReferenceOptions(area: PhiCmsAreaKey, logScope: string): {
14
+ options: PageReferenceOptions;
15
+ failed: boolean;
16
+ };
17
+ export {};
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ /*
4
+ * One request per Area for as long as the page lives: the create and the edit dialog offer the same
5
+ * Pages, and a list that changed meanwhile is a Page somebody published while this one was being written.
6
+ * A failed request is forgotten, so the next dialog asks again.
7
+ */
8
+ const pageOptionsByArea = new Map();
9
+ function loadPageOptions(area) {
10
+ const cached = pageOptionsByArea.get(area);
11
+ if (cached)
12
+ return cached;
13
+ const request = (async () => {
14
+ const response = await fetch(`/api/site/page-references?area=${encodeURIComponent(area)}`, {
15
+ cache: "no-store",
16
+ credentials: "same-origin",
17
+ headers: { accept: "application/json" },
18
+ });
19
+ const payload = (await response.json().catch(() => null));
20
+ if (!response.ok || !Array.isArray(payload?.options)) {
21
+ throw new Error(`Page references for "${area}" answered ${response.status}.`);
22
+ }
23
+ return payload.options;
24
+ })();
25
+ pageOptionsByArea.set(area, request);
26
+ request.catch(() => pageOptionsByArea.delete(area));
27
+ return request;
28
+ }
29
+ /**
30
+ * The Pages of one Area a link may point at, as the tree `PhiLinkTargetControl` offers, for a surface
31
+ * that stands outside the Builder's own catalog: a Form's `link-target` field, the Theme's Brand.
32
+ *
33
+ * Answered by `/api/site/page-references` (`gateway/page-references-route.ts`), which merges the Area's
34
+ * Module Pages and Core's the way the Builder's picker does. `failed` is set when the list could not be
35
+ * read, so the surface can say so instead of showing an empty list that looks like a Site without Pages.
36
+ */
37
+ export function usePhiPageReferenceOptions(area, logScope) {
38
+ const [options, setOptions] = useState([]);
39
+ const [failed, setFailed] = useState(false);
40
+ useEffect(() => {
41
+ let current = true;
42
+ loadPageOptions(area).then((next) => {
43
+ if (current) {
44
+ setOptions(next);
45
+ setFailed(false);
46
+ }
47
+ }, (error) => {
48
+ console.warn(`[${logScope}] Page references could not be loaded.`, { area, error });
49
+ if (current)
50
+ setFailed(true);
51
+ });
52
+ return () => {
53
+ current = false;
54
+ };
55
+ }, [area, logScope]);
56
+ return { options, failed };
57
+ }
@@ -18,6 +18,19 @@ export type PhiBackgroundBaseColor = {
18
18
  export type PhiBackgroundBaseNone = {
19
19
  kind: "none";
20
20
  };
21
+ /**
22
+ * The Theme's Chrome Overlay as the ground (LAYOUTING.md "Surface tone", SHELL.md "Root Background and
23
+ * Shell Backdrop Layers").
24
+ *
25
+ * The box paints what the Theme workspace calls the Chrome Overlay -- its colour or picture and its glass
26
+ * pane -- instead of a ground of its own. It carries nothing else: the Overlay is the Theme's, per mode
27
+ * (`root.chrome[mode]`), and the box reads the mode it stands in. A Surface `tone` is that mode, so an
28
+ * `inverse` Footer is the dark Overlay on a light page and the light one on a dark page. It is a stated
29
+ * ground: a tone does not paint its own container ground beneath it.
30
+ */
31
+ export type PhiBackgroundBaseTheme = {
32
+ kind: "theme";
33
+ };
21
34
  export type PhiBackgroundBaseGradient = {
22
35
  kind: "gradient";
23
36
  direction: PhiBackgroundDirection;
@@ -50,6 +63,24 @@ export type PhiBackgroundImageSourceKind = "asset" | "url";
50
63
  */
51
64
  export declare const PHI_BACKGROUND_IMAGE_SOURCE_KINDS: readonly PhiBackgroundImageSourceKind[];
52
65
  export declare const PHI_BACKGROUND_BASE_KINDS: readonly PhiCmsBackgroundWidgetConfig["base"]["kind"][];
66
+ /**
67
+ * The container ground of the mode a box stands in. What a toned Surface paints where it states no
68
+ * ground (`PHI_SURFACE_TONE_GROUND`), and what a `theme` ground falls back to where the Theme's Overlay
69
+ * is not published.
70
+ */
71
+ export declare const PHI_BACKGROUND_CONTAINER_GROUND = "var(--ant-color-bg-container)";
72
+ /**
73
+ * What a `theme` Base paints: the Shell Chrome Overlay's custom properties, the same ones the Shell's
74
+ * chrome panes paint from (components/shell/phi-cms-shell.tsx).
75
+ *
76
+ * The Root Layout publishes them for the page's mode, and `styles/shell.css` publishes them again under a
77
+ * tone's class for the tone's mode, so the box needs no colour of its own and turns with the mode switch.
78
+ * Outside the Root Layout -- a Modal or Drawer portalled to `<body>` -- nothing publishes them, and the box
79
+ * takes the container ground of its mode instead: the tone's ground, or the Overlay's own where it has no
80
+ * tone. Fixed attachment for the reason the Chrome Regions have it: every box is a window onto one
81
+ * viewport-sized painting.
82
+ */
83
+ export declare const PHI_BACKGROUND_THEME_STYLE: Readonly<CSSProperties>;
53
84
  export declare const PHI_BACKGROUND_MOTION_MODES: readonly PhiBackgroundMotionMode[];
54
85
  export type PhiBackgroundMotionDirection = "natural" | "reverse";
55
86
  /**
@@ -103,7 +134,7 @@ export type PhiBackgroundOverlay = PhiBackgroundColorOverlay | PhiBackgroundPatt
103
134
  /** What an Overlay that states no opacity is painted at, shared by every kind. */
104
135
  export declare const PHI_BACKGROUND_OVERLAY_DEFAULT_OPACITY = 0.14;
105
136
  export type PhiCmsBackgroundWidgetConfig = {
106
- base: PhiBackgroundBaseColor | PhiBackgroundBaseGradient | PhiBackgroundBaseImage | PhiBackgroundBaseNone;
137
+ base: PhiBackgroundBaseColor | PhiBackgroundBaseGradient | PhiBackgroundBaseImage | PhiBackgroundBaseNone | PhiBackgroundBaseTheme;
107
138
  overlay?: PhiBackgroundOverlay | null;
108
139
  filter?: PhiBackgroundFilter | null;
109
140
  motion?: PhiBackgroundMotion | null;
@@ -30,7 +30,35 @@ export const PHI_BACKGROUND_BASE_KINDS = [
30
30
  "color",
31
31
  "gradient",
32
32
  "image",
33
+ "theme",
33
34
  ];
35
+ /**
36
+ * The container ground of the mode a box stands in. What a toned Surface paints where it states no
37
+ * ground (`PHI_SURFACE_TONE_GROUND`), and what a `theme` ground falls back to where the Theme's Overlay
38
+ * is not published.
39
+ */
40
+ export const PHI_BACKGROUND_CONTAINER_GROUND = "var(--ant-color-bg-container)";
41
+ /**
42
+ * What a `theme` Base paints: the Shell Chrome Overlay's custom properties, the same ones the Shell's
43
+ * chrome panes paint from (components/shell/phi-cms-shell.tsx).
44
+ *
45
+ * The Root Layout publishes them for the page's mode, and `styles/shell.css` publishes them again under a
46
+ * tone's class for the tone's mode, so the box needs no colour of its own and turns with the mode switch.
47
+ * Outside the Root Layout -- a Modal or Drawer portalled to `<body>` -- nothing publishes them, and the box
48
+ * takes the container ground of its mode instead: the tone's ground, or the Overlay's own where it has no
49
+ * tone. Fixed attachment for the reason the Chrome Regions have it: every box is a window onto one
50
+ * viewport-sized painting.
51
+ */
52
+ export const PHI_BACKGROUND_THEME_STYLE = {
53
+ backgroundColor: `var(--phi-shell-chrome-color, ${PHI_BACKGROUND_CONTAINER_GROUND})`,
54
+ backgroundImage: "var(--phi-shell-chrome-image, none)",
55
+ backgroundSize: "var(--phi-shell-chrome-size, auto)",
56
+ backgroundPosition: "var(--phi-shell-chrome-position, 0 0)",
57
+ backgroundRepeat: "var(--phi-shell-chrome-repeat, repeat)",
58
+ backgroundAttachment: "fixed",
59
+ backdropFilter: "var(--phi-shell-chrome-filter, none)",
60
+ WebkitBackdropFilter: "var(--phi-shell-chrome-filter, none)",
61
+ };
34
62
  export const PHI_BACKGROUND_MOTION_MODES = [
35
63
  "static",
36
64
  "fixed",
@@ -145,7 +173,7 @@ function normalizePhiBackgroundBase(value) {
145
173
  ? { ...normalized, ...source }
146
174
  : { ...normalized, ...source };
147
175
  }
148
- if (kind === "none")
176
+ if (kind === "none" || kind === "theme")
149
177
  return { kind };
150
178
  if (kind === "color") {
151
179
  const color = readString(raw.color);
@@ -253,6 +281,10 @@ export function normalizePhiBackgroundWidgetConfig(config) {
253
281
  }
254
282
  const raw = config;
255
283
  const base = normalizePhiBackgroundBase(raw.base) ?? normalizePhiBackgroundBase(raw) ?? { kind: "none" };
284
+ // The Theme's Overlay is the whole ground: it carries its own Overlay and pane, and takes none of the box's.
285
+ if (base.kind === "theme") {
286
+ return { base, overlay: null, filter: null, motion: null };
287
+ }
256
288
  return {
257
289
  base,
258
290
  overlay: readBackgroundOverlay(raw.overlay),
@@ -278,6 +310,9 @@ export function readPhiBackgroundPatternInkFromCss(css) {
278
310
  export function serializePhiBackgroundBaseCss(base) {
279
311
  if (base.kind === "none")
280
312
  return "none";
313
+ // Not a value a Base can be read back from: the Overlay's colour, as far as one CSS value can say it.
314
+ if (base.kind === "theme")
315
+ return String(PHI_BACKGROUND_THEME_STYLE.backgroundColor);
281
316
  if (base.kind === "color")
282
317
  return base.color;
283
318
  if (base.kind === "gradient") {
@@ -320,6 +355,9 @@ function resolvePhiBackgroundOverlayLayer(overlay) {
320
355
  export function resolvePhiBackgroundWidgetStyle(config) {
321
356
  const normalized = normalizePhiBackgroundWidgetConfig(config);
322
357
  const base = normalized.base;
358
+ if (base.kind === "theme") {
359
+ return { ...PHI_BACKGROUND_THEME_STYLE };
360
+ }
323
361
  const style = {};
324
362
  if (base.kind === "color")
325
363
  style.backgroundColor = base.color;
@@ -397,7 +435,8 @@ export function phiBackgroundWidgetConfigPaintsGround(config) {
397
435
  * nor honoured there.
398
436
  */
399
437
  export function phiBackgroundBaseSupportsGlassEffect(base) {
400
- return base.kind !== "image" && base.kind !== "gradient";
438
+ // The Theme's Overlay brings its own pane, and a second one over it is nothing anybody asked for.
439
+ return base.kind !== "image" && base.kind !== "gradient" && base.kind !== "theme";
401
440
  }
402
441
  /**
403
442
  * Whether `blur` has anything to soften: a picture, a gradient, or a patterned Overlay.
@@ -18,6 +18,7 @@ const PHI_BACKGROUND_WIDGET_LABEL_SET = definePhiLabelSet({
18
18
  base_color: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.base.color,
19
19
  base_gradient: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.base.gradient,
20
20
  base_image: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.base.image,
21
+ base_theme: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.base.theme,
21
22
  image_url: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.image.url,
22
23
  image_alt: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.image.alt,
23
24
  image_position: PHI_BACKGROUND_WIDGET_DEFAULT_LABELS.image.position,
@@ -104,6 +105,7 @@ export async function getPhiBackgroundWidgetLabels(options) {
104
105
  color: labels.base_color,
105
106
  gradient: labels.base_gradient,
106
107
  image: labels.base_image,
108
+ theme: labels.base_theme,
107
109
  },
108
110
  image: {
109
111
  url: labels.image_url,
@@ -14,6 +14,7 @@ export type PhiBackgroundWidgetLabels = {
14
14
  color: string;
15
15
  gradient: string;
16
16
  image: string;
17
+ theme: string;
17
18
  };
18
19
  image: {
19
20
  url: string;
@@ -14,6 +14,7 @@ export const PHI_BACKGROUND_WIDGET_DEFAULT_LABELS = {
14
14
  color: "Color",
15
15
  gradient: "Gradient",
16
16
  image: "Image",
17
+ theme: "Theme",
17
18
  },
18
19
  image: {
19
20
  url: "Image URL",
@@ -41,6 +41,16 @@ export type PhiResolvedBulletListStyle = {
41
41
  gap: PhiSpacingScaleKey;
42
42
  flags: number;
43
43
  };
44
+ /**
45
+ * The markers a browser cycles through, and a tight gap. Exported so a Widget can state them as its
46
+ * defaults, which is what an Inspector shows for a list nobody has set yet.
47
+ */
48
+ export declare const PHI_BULLET_LIST_DEFAULT_STYLE: {
49
+ readonly level1: "dot";
50
+ readonly level2: "circle";
51
+ readonly level3: "dash";
52
+ readonly gap: "xs";
53
+ };
44
54
  /** A stored list style, part by part; whatever is not one of its values is left out. */
45
55
  export declare function readPhiBulletListStyle(value: unknown): PhiBulletListStyle | undefined;
46
56
  export declare function resolvePhiBulletListStyle(style: PhiBulletListStyle | null | undefined): PhiResolvedBulletListStyle;
@@ -20,8 +20,11 @@ export const PhiBulletListFlag = {
20
20
  /** A line between entries, for entries without a ground of their own. */
21
21
  Dividers: 1 << 0,
22
22
  };
23
- /** The markers a browser cycles through, and a tight gap. */
24
- const PHI_BULLET_LIST_DEFAULT_STYLE = {
23
+ /**
24
+ * The markers a browser cycles through, and a tight gap. Exported so a Widget can state them as its
25
+ * defaults, which is what an Inspector shows for a list nobody has set yet.
26
+ */
27
+ export const PHI_BULLET_LIST_DEFAULT_STYLE = {
25
28
  level1: "dot",
26
29
  level2: "circle",
27
30
  level3: "dash",
@@ -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 { 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";
7
+ import type { PhiCardBadgeStyle, PhiCardBillingPeriod, PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody } from "./card-vocabulary";
8
+ export type { PhiCardBadgeStyle, PhiCardBillingPeriod, PhiCardHeadingLevel, PhiCardHoverEffect, PhiCardIconPlacement, PhiCardTextAlign, PhiCardTextSlot, PhiCardVariant, PhiCardWidgetBody, } from "./card-vocabulary";
9
9
  export type PhiCardWidgetClientLabels = {
10
10
  eyebrow?: string;
11
11
  /**
@@ -72,6 +72,8 @@ export type PhiCardWidgetClientConfig = {
72
72
  highlight?: boolean;
73
73
  /** The badge's ground; absent, the Theme's primary. */
74
74
  badgeColor?: string;
75
+ /** How the badge is drawn; absent, `none`. */
76
+ badgeStyle?: PhiCardBadgeStyle;
75
77
  /** Which price a `plan` shows until a `billingPeriod` signal says otherwise. Absent is `monthly`. */
76
78
  billingPeriod?: PhiCardBillingPeriod;
77
79
  hoverEffect?: PhiCardHoverEffect;
@@ -89,7 +91,7 @@ export type PhiCardWidgetAction = {
89
91
  export type PhiCardWidgetClientProps = PhiClientBlockBaseProps<PhiCardWidgetClientLabels, PhiCardWidgetClientConfig> & {
90
92
  binding?: PhiCardWidgetClientBinding;
91
93
  /**
92
- * What a `list` body draws, and a `plan` body's features, already drawn.
94
+ * What a `list` body draws, a `series` body's chart, and a `plan` body's features, already drawn.
93
95
  *
94
96
  * Handed in rather than drawn here: the Card is on Public pages, and a list Control imported by it
95
97
  * would load on every page with a Card. A Dashboard draws its entries and passes them; a plan card's
@@ -5,6 +5,7 @@ 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
7
  import { PhiRibbonControl } from "../../controls/phi-ribbon-control";
8
+ import { PhiRibbonWrapControl } from "../../controls/phi-ribbon-wrap-control";
8
9
  import { PhiSkeletonControl } from "../../controls/phi-skeleton-control";
9
10
  import { PhiStatisticControl } from "../../controls/phi-statistic-control";
10
11
  import { PhiTypographyControl } from "../../controls/phi-typography-control";
@@ -16,6 +17,7 @@ import { PhiSurfaceTone } from "../../surface/phi-surface-tone";
16
17
  import { resolvePhiSurfaceStyle } from "../../../helpers/surface-style";
17
18
  import { usePhiControlSignalController } from "../client/shared/phi-control-signals";
18
19
  import { PHI_COLOR, PHI_SPACE } from "../../../theme/antd-css-var-contract";
20
+ import { PHI_THEME_HEADING_FONT_VARIABLE } from "../../../theme/phi-theme-typography";
19
21
  import { PHI_CARD_TEXT_SLOTS, readPhiCardBillingPeriod } from "./card-vocabulary";
20
22
  const PHI_CARD_SIGNAL_TEXT_SLOTS = new Set(PHI_CARD_TEXT_SLOTS);
21
23
  /*
@@ -192,6 +194,8 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
192
194
  * Segmented's items follow their track. The Theme's plain corner did not: a pill Site drew square marks.
193
195
  */
194
196
  const iconCorner = (side) => `calc(var(--phi-control-radius-lg, ${token.borderRadiusLG}px) * ${side / token.controlHeightLG})`;
197
+ /* The framed box an icon before the title stands in. */
198
+ const inlineIconFrame = iconSize + 16;
195
199
  const iconMark = (size, framed) => iconName ? (_jsx("span", { "aria-hidden": "true", className: "phi-card__icon", style: {
196
200
  display: "inline-flex",
197
201
  flex: "none",
@@ -207,10 +211,34 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
207
211
  * unreadable. A card whose figure is the only thing it has makes the figure the link.
208
212
  */
209
213
  const valueText = renderText ? renderText("value", labels.value) : null;
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: {
214
+ /*
215
+ * Beside an icon before the title, the figure starts where the title's words do, under them rather
216
+ * than under the icon: the number belongs to the heading, the icon only marks it. On a line set to its
217
+ * start only -- centred or at the end, the figure already stands with the words.
218
+ */
219
+ const figureIndent = iconPlacement === "inline" && iconName && (textAlign === "start" || textAlign === "justify")
220
+ ? { paddingInlineStart: inlineIconFrame + gap }
221
+ : {};
222
+ /*
223
+ * The figure is set like the title it belongs to: in the font the Theme gives headings, and with digits
224
+ * of one width, so a figure that changes does not shift as it counts. No fallback in the `var()`, as for
225
+ * the headings themselves (`styles/layout.css`): a Theme that keeps headings on the body font sets no
226
+ * variable, and the figure inherits the body font.
227
+ */
228
+ const figureType = {
229
+ fontFamily: `var(${PHI_THEME_HEADING_FONT_VARIABLE})`,
230
+ fontVariantNumeric: "tabular-nums",
231
+ };
232
+ const figure = valueText != null ? (_jsx("div", { style: {
233
+ ...figureType,
234
+ fontSize: token.fontSizeHeading3,
235
+ color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading,
236
+ ...slotWidth,
237
+ ...figureIndent,
238
+ }, children: valueText })) : (_jsx(PhiStatisticControl, { value: binding?.error ?? labels.value ?? "", loading: binding?.loading === true || overrides.loading === true, style: figureIndent, styles: {
211
239
  content: binding?.error
212
240
  ? { color: PHI_COLOR.error, fontSize: token.fontSize }
213
- : { color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading },
241
+ : { ...figureType, color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading },
214
242
  } }));
215
243
  /* A failure stands where the lines would, as it does for a figure; a list still coming is three lines. */
216
244
  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;
@@ -230,7 +258,7 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
230
258
  color: highlight ? PHI_COLOR.primary : PHI_COLOR.textHeading,
231
259
  ...slotWidth,
232
260
  }, 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;
233
- const headingRow = iconPlacement === "inline" && iconName ? (_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: justify, gap, ...slotWidth }, children: [iconMark(iconSize, iconSize + 16), heading] })) : heading;
261
+ const headingRow = iconPlacement === "inline" && iconName ? (_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: justify, gap, ...slotWidth }, children: [iconMark(iconSize, inlineIconFrame), heading] })) : heading;
234
262
  const media = image ? (_jsxs("div", { className: "phi-card__media", style: {
235
263
  position: "relative",
236
264
  aspectRatio: metrics.mediaAspectRatio ?? resolveMediaAspectRatio(image.presentation),
@@ -294,19 +322,34 @@ export function PhiCardWidgetClient({ labels: placedLabels, config, binding, lis
294
322
  * The band in the card's top end corner, floating (`PhiRibbonControl`): the words below it start where
295
323
  * they start in a card without one, so plans side by side keep their headings on one line.
296
324
  */
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: {
325
+ const badgeStyle = config?.badgeStyle ?? "none";
326
+ const ribbon = badge != null && (badgeStyle === "corner" || badgeStyle === "tag") ? (_jsx(PhiRibbonControl, { color: config?.badgeColor, placement: badgeStyle === "tag" ? "edge" : "corner", ...(!renderText && labels.badge
327
+ ? {
328
+ fullText: labels.badge,
329
+ ...(href
330
+ ? {
331
+ onClick: (event) => event.currentTarget.closest(".phi-card")?.querySelector(".phi-card__link")?.click(),
332
+ }
333
+ : {}),
334
+ }
335
+ : {}), ...(renderText ? { style: { pointerEvents: "auto", minWidth: "6em" } } : {}), children: badge })) : null;
336
+ 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 === "series" ? (_jsxs(_Fragment, { children: [labels.value || binding?.error || binding?.loading === true || overrides.loading === true ? figure : null, list != null && !binding?.error && binding?.loading !== true && overrides.loading !== true ? (_jsx("div", { style: { justifySelf: "stretch", minWidth: 0 }, children: list })) : null] })) : null, body === "list" && list != null ? (_jsx("div", { style: { justifySelf: "stretch", minWidth: 0 }, children: list })) : null, description != null ? (_jsx(PhiTypographyControl, { presentation: "paragraph", style: {
299
337
  marginBottom: 0,
300
338
  color: PHI_COLOR.textSecondary,
301
339
  fontSize: descriptionSize,
302
340
  ...slotWidth,
303
341
  }, 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
342
  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] }));
305
- return (_jsxs("article", { className: [
343
+ const card = (_jsxs("article", { className: [
306
344
  "phi-card",
307
345
  href ? "phi-card--link" : null,
308
346
  highlight ? "phi-card--highlight" : null,
309
347
  hoverEffect !== "none" ? `phi-card--hover-${hoverEffect}` : null,
310
348
  surface.className,
311
349
  ].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] }));
350
+ /*
351
+ * A ribbon hangs over the card's edge, which the card -- clipping for its picture and its zoom -- cannot
352
+ * give a child of its own; it wraps the card instead (`PhiRibbonWrapControl`). An editor writes in it.
353
+ */
354
+ return badge != null && badgeStyle === "ribbon" ? (_jsx(PhiRibbonWrapControl, { text: badge, ...(config?.badgeColor ? { color: config.badgeColor } : {}), interactive: renderText != null, className: hoverEffect === "lift" ? "phi-card-ribbon phi-card-ribbon--lift" : "phi-card-ribbon", children: card })) : card;
312
355
  }