@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
@@ -50,7 +50,7 @@ export function PhiSimpleTextWidgetClient({ blockId, labels, config, }) {
50
50
  const resolvedFontFamily = styleValue.code
51
51
  ? token.fontFamilyCode
52
52
  : resolvePhiWidgetFontFamily(fontFamilyValue, fonts, token);
53
- const resolvedFontSize = resolvePhiWidgetFontSize(fontSizeValue, token, "lg");
53
+ const resolvedFontSize = resolvePhiWidgetFontSize(fontSizeValue, token, "base");
54
54
  const receiver = createPhiRenderableBlockReceiver("widget", blockId);
55
55
  usePhiRenderableBlockSignalListener(receiver, (signal) => {
56
56
  if (signal.channel === "text") {
@@ -105,16 +105,21 @@ export function PhiSimpleTextWidgetClient({ blockId, labels, config, }) {
105
105
  code: typeof stylePayload.code === "boolean" ? stylePayload.code : current.code,
106
106
  }));
107
107
  });
108
+ /*
109
+ * A linked text's colour is the link's (`PhiLink`), which answers the pointer; the words inside inherit
110
+ * it. Stated on them as well, the colour stood over the link's and the hover never showed.
111
+ */
112
+ const contentColor = hasHref ? undefined : textColor;
108
113
  const contentNode = (_jsxs(PhiFlexControl, { align: "center", gap: 8, style: {
109
114
  width: "fit-content",
110
115
  maxWidth: "100%",
111
116
  minWidth: 0,
112
117
  fontSize: resolvedFontSize ?? "inherit",
113
118
  lineHeight: resolvedFontSize ? PHI_LINE_HEIGHT_LG : "inherit",
114
- color: textColor ?? undefined,
119
+ color: contentColor ?? undefined,
115
120
  fontFamily: resolvedFontFamily,
116
121
  }, children: [iconValue ? _jsx(PhiIcon, { name: iconValue, size: "1.25em" }) : null, _jsx(PhiTypographyControl, { type: config?.tone, strong: styleValue.strong, italic: styleValue.italic, underline: styleValue.underline, delete: styleValue.delete, disabled: styleValue.disabled, code: styleValue.code, style: {
117
- color: textColor ?? "inherit",
122
+ color: contentColor ?? "inherit",
118
123
  fontSize: resolvedFontSize ?? "inherit",
119
124
  lineHeight: resolvedFontSize ? PHI_LINE_HEIGHT_LG : "inherit",
120
125
  ...(resolvedFontFamily ? { fontFamily: resolvedFontFamily } : {}),
@@ -50,6 +50,7 @@ export declare const PHI_SIMPLE_TEXT_WIDGET_DEFINITION: {
50
50
  } & {
51
51
  type: "string" | "url" | "icon";
52
52
  emptyValue?: "";
53
+ placeholder?: string;
53
54
  }) | ({
54
55
  key: string;
55
56
  label: string;
@@ -43,7 +43,7 @@ export function PhiSimpleTextWidgetColorButton({ config, onChange, }) {
43
43
  }
44
44
  export function PhiSimpleTextWidgetTypographyButton({ config, onChange, }) {
45
45
  const labels = usePhiAuthoringToolsLabels();
46
- return (_jsx(PhiWidgetTypographyToolButton, { fontFamily: config?.fontFamily, fontSize: config?.fontSize, defaultFontSize: "lg", ariaLabel: labels.text.typography, onChange: ({ fontFamily, fontSize }) => onChange({
46
+ return (_jsx(PhiWidgetTypographyToolButton, { fontFamily: config?.fontFamily, fontSize: config?.fontSize, defaultFontSize: "base", ariaLabel: labels.text.typography, onChange: ({ fontFamily, fontSize }) => onChange({
47
47
  ...(fontFamily !== undefined ? { fontFamily: fontFamily ?? undefined } : {}),
48
48
  ...(fontSize !== undefined ? { fontSize: fontSize ?? undefined } : {}),
49
49
  }) }));
@@ -71,7 +71,7 @@ export function PhiSimpleTextWidgetEditor({ text, config, onChangeText, }) {
71
71
  const resolvedFontFamily = hasPhiSimpleTextMark(config, "code")
72
72
  ? token.fontFamilyCode
73
73
  : resolvePhiWidgetFontFamily(config?.fontFamily, fonts, token);
74
- const resolvedFontSize = resolvePhiWidgetFontSize(config?.fontSize, token, "lg");
74
+ const resolvedFontSize = resolvePhiWidgetFontSize(config?.fontSize, token, "base");
75
75
  return (_jsxs(PhiFlexControl, { align: "center", gap: 8, style: {
76
76
  width: "fit-content",
77
77
  maxWidth: "100%",
@@ -73,6 +73,7 @@ export declare const PHI_TABLE_WIDGET_DEFINITION: {
73
73
  } & {
74
74
  type: "string" | "url" | "icon";
75
75
  emptyValue?: "";
76
+ placeholder?: string;
76
77
  }) | ({
77
78
  key: string;
78
79
  label: string;
@@ -373,6 +374,7 @@ export declare const PHI_TABLE_WIDGET_DEFINITION: {
373
374
  heading?: string;
374
375
  type: "string" | "url" | "icon";
375
376
  emptyValue?: "";
377
+ placeholder?: string;
376
378
  } | {
377
379
  visibleWhen: {
378
380
  field: string;
@@ -738,6 +740,7 @@ export declare const PHI_TABLE_WIDGET_DEFINITION: {
738
740
  heading?: string;
739
741
  type: "string" | "url" | "icon";
740
742
  emptyValue?: "";
743
+ placeholder?: string;
741
744
  } | {
742
745
  visibleWhen: {
743
746
  field: string;
@@ -1,5 +1,7 @@
1
- import { type PhiDashboardCardPayload, type PhiDashboardCardRow } from "../../../types/dashboard-cards";
1
+ import { type PhiDashboardCardPayload, type PhiDashboardCardSeries, type PhiDashboardCardRow } from "../../../types/dashboard-cards";
2
2
  /** The card rows a list answer carries, or a Collection holds. */
3
3
  export declare function readPhiDashboardCardRows(value: unknown, source?: string): PhiDashboardCardRow[];
4
4
  /** One card's refreshed payload. */
5
5
  export declare function readPhiDashboardCardPayload(value: unknown): PhiDashboardCardPayload;
6
+ /** The latest figure of the first series: the headline of a `series` card whose provider states none. */
7
+ export declare function readPhiDashboardSeriesHeadline(series: PhiDashboardCardSeries, locale: string): string | undefined;
@@ -9,16 +9,38 @@ import { createPhiResponseReader, requirePhiField, } from "../../../components/w
9
9
  * shape rendered as an empty card. Read field by field now, a wrong answer is an error naming the field,
10
10
  * which the Collection shows as the card list's error.
11
11
  */
12
- const PHI_DASHBOARD_CARD_FORMS = ["stat", "list"];
12
+ const PHI_DASHBOARD_CARD_FORMS = ["stat", "list", "series"];
13
13
  function readCardId(reader) {
14
14
  return requirePhiField(reader, "cardId", "a Dashboard card id", (key) => reader.value(key, "a Dashboard card id", (value) => (isPhiDashboardCardId(value) ? value : undefined)));
15
15
  }
16
16
  function readTimelineEntry(reader, index) {
17
17
  return readPhiTimelineEntry(reader.record) ?? reader.fail(String(index), "a timeline entry with a key", reader.record);
18
18
  }
19
+ /*
20
+ * A series is read whole: a point with one value too few would draw a line that drops to nothing on the
21
+ * day it lost, which looks like data. So each point states a finite number for every label, or the payload
22
+ * is an error naming the point.
23
+ */
24
+ function readSeries(reader) {
25
+ const labels = requirePhiField(reader, "labels", "a list of series labels", reader.stringList);
26
+ if (labels.length === 0) {
27
+ reader.fail("labels", "at least one series label", labels);
28
+ }
29
+ const points = requirePhiField(reader, "points", "a list of points", (key) => reader.list(key, (point) => ({
30
+ at: requirePhiField(point, "at", "an ISO date or instant", point.string),
31
+ values: requirePhiField(point, "values", `${labels.length} finite numbers, one per label`, (valuesKey) => point.value(valuesKey, `${labels.length} finite numbers, one per label`, (value) => Array.isArray(value)
32
+ && value.length === labels.length
33
+ && value.every((entry) => typeof entry === "number" && Number.isFinite(entry))
34
+ ? value
35
+ : undefined)),
36
+ })));
37
+ const unit = reader.string("unit");
38
+ return { labels, points, ...(unit === undefined ? {} : { unit }) };
39
+ }
19
40
  function readPayloadFields(reader) {
20
41
  const value = reader.string("value");
21
42
  const entries = reader.list("entries", readTimelineEntry);
43
+ const series = reader.object("series", readSeries);
22
44
  const description = reader.string("description");
23
45
  const meta = reader.string("meta");
24
46
  const error = reader.string("error");
@@ -26,6 +48,7 @@ function readPayloadFields(reader) {
26
48
  cardId: readCardId(reader),
27
49
  ...(value === undefined ? {} : { value }),
28
50
  ...(entries === undefined ? {} : { entries }),
51
+ ...(series === undefined ? {} : { series }),
29
52
  ...(description === undefined ? {} : { description }),
30
53
  ...(meta === undefined ? {} : { meta }),
31
54
  ...(error === undefined ? {} : { error }),
@@ -40,7 +63,7 @@ function readRowFields(reader) {
40
63
  const payload = reader.object("payload", readPayloadFields);
41
64
  return {
42
65
  cardId: readCardId(reader),
43
- form: requirePhiField(reader, "form", '"stat" or "list"', (key) => reader.oneOf(key, PHI_DASHBOARD_CARD_FORMS)),
66
+ form: requirePhiField(reader, "form", '"stat", "list" or "series"', (key) => reader.oneOf(key, PHI_DASHBOARD_CARD_FORMS)),
44
67
  title: requirePhiField(reader, "title", "a text", reader.string),
45
68
  ...(eyebrow === undefined ? {} : { eyebrow }),
46
69
  ...(description === undefined ? {} : { description }),
@@ -57,3 +80,8 @@ export function readPhiDashboardCardRows(value, source = "The Dashboard card lis
57
80
  export function readPhiDashboardCardPayload(value) {
58
81
  return readPayloadFields(createPhiResponseReader(value, "The Dashboard card"));
59
82
  }
83
+ /** The latest figure of the first series: the headline of a `series` card whose provider states none. */
84
+ export function readPhiDashboardSeriesHeadline(series, locale) {
85
+ const latest = series.points[series.points.length - 1]?.values[0];
86
+ return latest === undefined ? undefined : new Intl.NumberFormat(locale).format(latest);
87
+ }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { readPhiTimelineEntries } from "@phis/contracts/timeline";
5
- import { PHI_DASHBOARD_CARD_LIST_LIMIT, } from "../../../../types/dashboard-cards";
5
+ import { PHI_DASHBOARD_CARD_LIST_LIMIT, PHI_DASHBOARD_CARD_SERIES_LIMIT, } from "../../../../types/dashboard-cards";
6
6
  import { PhiCardWidgetClient } from "../../../../components/widgets/shared/card-body-client";
7
7
  import {} from "../../../../components/widgets/shared/card-vocabulary";
8
8
  import { PHI_SURFACE_CARD } from "../../../../helpers/surface-presets";
@@ -10,10 +10,11 @@ import { PhiCollectionViewControl } from "../../../../components/controls/phi-co
10
10
  import { PhiAlertControl } from "../../../../components/controls/phi-alert-control";
11
11
  import { PhiEmptyControl } from "../../../../components/controls/phi-empty-control";
12
12
  import { usePhiPathname } from "../../../../components/runtime/use-phi-pathname";
13
- import { readPhiDashboardCardPayload, readPhiDashboardCardRows } from "../card-rows";
13
+ import { readPhiDashboardCardPayload, readPhiDashboardCardRows, readPhiDashboardSeriesHeadline, } from "../card-rows";
14
14
  import { PhiTimelineControl } from "../../../../components/controls/phi-timeline-control";
15
+ import { PhiSeriesChartControl } from "../../../../components/controls/phi-series-chart-control";
15
16
  import { usePhiIntlLocale } from "../../../../components/root/phi-intl-locale";
16
- import { formatPhiRelativeTime } from "../../../../helpers/format-date-time";
17
+ import { formatPhiDate, formatPhiRelativeTime } from "../../../../helpers/format-date-time";
17
18
  /**
18
19
  * A Dashboard, drawn.
19
20
  *
@@ -33,8 +34,7 @@ const CARD_MIN_COLUMN_WIDTH = 260;
33
34
  * A Module's form, and the Core body that draws it.
34
35
  *
35
36
  * Two vocabularies that agree today, and deliberately not one type. `form` is what a Module
36
- * declares about its contribution; `body` is what Core draws. They agree now and are allowed to stop:
37
- * a `series` form would reasonably draw in whatever body Core has for a graphic, under another name.
37
+ * declares about its contribution; `body` is what Core draws. They agree now and are allowed to stop.
38
38
  *
39
39
  * A total map rather than a condition, so a form that joins the vocabulary stops this file compiling
40
40
  * instead of quietly falling back to a heading.
@@ -42,6 +42,7 @@ const CARD_MIN_COLUMN_WIDTH = 260;
42
42
  const CARD_BODY_BY_FORM = {
43
43
  stat: "stat",
44
44
  list: "list",
45
+ series: "series",
45
46
  };
46
47
  /**
47
48
  * A `list` card's entries as the Timeline draws them: the first few, each time in the page's locale.
@@ -62,6 +63,24 @@ function PhiDashboardCardList({ entries }) {
62
63
  ...(entry.pending ? { pending: true } : {}),
63
64
  })) }));
64
65
  }
66
+ /*
67
+ * A point's day as the calendar day it names. An ISO date read by `Date` is midnight in UTC, which is
68
+ * the day before west of Greenwich; an instant is formatted where the reader is.
69
+ */
70
+ function formatSeriesKey(at, locale) {
71
+ return /^\d{4}-\d{2}-\d{2}$/.test(at)
72
+ ? new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(new Date(`${at}T00:00:00Z`))
73
+ : formatPhiDate(at, locale);
74
+ }
75
+ /**
76
+ * A `series` card's chart: its first few series over every point, each day in the page's locale. Drawn
77
+ * here and handed to the card, as a list is, so a Card on a Public page never loads the chart Control.
78
+ */
79
+ function PhiDashboardCardSeriesChart({ series, title }) {
80
+ const locale = usePhiIntlLocale();
81
+ const labels = series.labels.slice(0, PHI_DASHBOARD_CARD_SERIES_LIMIT);
82
+ return (_jsx(PhiSeriesChartControl, { locale: locale, title: title, keys: series.points.map((point) => formatSeriesKey(point.at, locale)), series: labels.map((label, index) => ({ label, values: series.points.map((point) => point.values[index]) })), ...(series.unit ? { unit: series.unit } : {}) }));
83
+ }
65
84
  function readRows(binding) {
66
85
  // A Collection holds items of no particular type; the rows are read again, not cast.
67
86
  return readPhiDashboardCardRows(binding.data?.items ?? [], "The Dashboard Collection");
@@ -84,6 +103,7 @@ function PhiDashboardCard({ row, area, pathname, }) {
84
103
  * says the same thing by comparison: an answer to a question no longer being asked is simply not this
85
104
  * card's answer.
86
105
  */
106
+ const locale = usePhiIntlLocale();
87
107
  const requestKey = `${area}\u0000${pathname}\u0000${row.cardId}`;
88
108
  const delivered = row.payload ?? null;
89
109
  const [resolved, setResolved] = useState(null);
@@ -125,6 +145,8 @@ function PhiDashboardCard({ row, area, pathname, }) {
125
145
  const payload = current?.payload ?? null;
126
146
  const error = current?.failure ?? payload?.error ?? null;
127
147
  const entries = row.form === "list" ? readPhiTimelineEntries(payload?.entries) : [];
148
+ const series = row.form === "series" ? payload?.series ?? null : null;
149
+ const value = payload?.value ?? (series ? readPhiDashboardSeriesHeadline(series, locale) : undefined);
128
150
  return (_jsx(PhiCardWidgetClient, { labels: {
129
151
  ...(row.eyebrow ? { eyebrow: row.eyebrow } : {}),
130
152
  /*
@@ -135,7 +157,7 @@ function PhiDashboardCard({ row, area, pathname, }) {
135
157
  * screen together and the card reads the same before and after it resolves.
136
158
  */
137
159
  title: row.title,
138
- ...(payload?.value ? { value: payload.value } : {}),
160
+ ...(value ? { value } : {}),
139
161
  ...(payload?.description ?? row.description
140
162
  ? { description: payload?.description ?? row.description }
141
163
  : {}),
@@ -152,7 +174,7 @@ function PhiDashboardCard({ row, area, pathname, }) {
152
174
  * this; that this file still does is what will move to a Controller when the Dashboard gets a
153
175
  * clock (DASHBOARD.md section 4).
154
176
  */
155
- binding: { loading: current === null, error }, listContent: entries.length > 0 ? _jsx(PhiDashboardCardList, { entries: entries }) : null }));
177
+ binding: { loading: current === null, error }, listContent: series ? (_jsx(PhiDashboardCardSeriesChart, { series: series, title: row.title })) : entries.length > 0 ? _jsx(PhiDashboardCardList, { entries: entries }) : null }));
156
178
  }
157
179
  export function PhiDashboardCardCollectionViewBinding({ config, binding, widgetId, }) {
158
180
  void widgetId;
@@ -1,25 +1,19 @@
1
1
  import { createPhiPresetCmsInstanceIdMap } from "../../../../types/cms-instance-id";
2
2
  import { PHI_DASHBOARD_RUNTIME_MODULE_ID } from "../ids";
3
- import { PHI_CMS_DEFAULT_SLOT_INDEX } from "../../../../constants/cms-layout-types";
4
- import { PhiCmsFlags, PhiCmsPageType, PhiCmsRegionType } from "../../../../constants/phi-cms";
3
+ import { PhiCmsPageType } from "../../../../constants/phi-cms";
5
4
  import { createPhiCmsPresetNodes } from "../../../../helpers/cms-preset-nodes";
6
- import { resolvePhiShellHeaderHeight } from "../../../../helpers/shell-region-style";
7
5
  import { buildPhiBasePageContentScaffold } from "../../../../components/regions/presets/phi-base-page-layout";
8
6
  import { getPhiBuilderChromeWidgetLabels } from "../../../../components/widgets/label-sets/builder-chrome";
9
7
  import { readPhiServerApiCredentials } from "../../../../helpers/phis-server-credentials";
10
8
  import { buildPhiDashboardCardsWidget } from "./dashboard-cards-page-tree";
11
9
  import { getPhiDashboardCardsEmptyDescription } from "./area-dashboard-label-set";
10
+ /*
11
+ * No Header bottom band: the Dashboard has no commands -- nothing to save, preview or publish -- and the
12
+ * band stood empty under the title.
13
+ */
12
14
  const SYNTHETIC_BUILDER_DASHBOARD_REGION_IDS = {
13
- regionHeaderBottom: -580,
14
15
  regionContent: -581,
15
16
  };
16
- const SYNTHETIC_BUILDER_DASHBOARD_LAYOUT_IDS = createPhiPresetCmsInstanceIdMap({
17
- domain: "page",
18
- ownerModuleId: PHI_DASHBOARD_RUNTIME_MODULE_ID,
19
- presetKey: "builder-dashboard-page",
20
- }, [
21
- "layoutHeaderBottom",
22
- ]);
23
17
  const SYNTHETIC_BUILDER_DASHBOARD_WIDGET_IDS = createPhiPresetCmsInstanceIdMap({
24
18
  domain: "page",
25
19
  ownerModuleId: PHI_DASHBOARD_RUNTIME_MODULE_ID,
@@ -56,38 +50,9 @@ export async function buildPhiDefaultBuilderDashboardPageTree({ page, runtime, }
56
50
  },
57
51
  overlays: [],
58
52
  regions: [
59
- nodes.region({
60
- id: SYNTHETIC_BUILDER_DASHBOARD_REGION_IDS.regionHeaderBottom,
61
- regionType: PhiCmsRegionType.HeaderBottom,
62
- rootLayoutNodeId: SYNTHETIC_BUILDER_DASHBOARD_LAYOUT_IDS.layoutHeaderBottom,
63
- sortOrder: 25,
64
- config: {
65
- flags: PhiCmsFlags.Sticky,
66
- /*
67
- * No Effect or Shadow of its own, like every other Builder Header. A `glass` authored here
68
- * frosts this one Region against the Site's Shell Chrome Overlay, so the Dashboard's Header
69
- * stopped matching the Sider and Footer beside it. What the frame looks like belongs to the Theme.
70
- */
71
- size: { height: `${resolvePhiShellHeaderHeight(runtime.site.theme?.shell, "bottom")}px` },
72
- offsetTop: resolvePhiShellHeaderHeight(runtime.site.theme?.shell, "main"),
73
- },
74
- }),
75
53
  scaffold.region,
76
54
  ],
77
55
  layoutNodes: [
78
- nodes.layout({
79
- creationPreset: { layoutKind: "threecol", preset: "panel" },
80
- typeKey: "three-column",
81
- id: SYNTHETIC_BUILDER_DASHBOARD_LAYOUT_IDS.layoutHeaderBottom,
82
- parentLayoutNodeId: null,
83
- slotIndex: PHI_CMS_DEFAULT_SLOT_INDEX,
84
- sortOrder: 0,
85
- label: "builder dashboard header bottom",
86
- config: {
87
- balancedSides: true,
88
- contentAlign: "center",
89
- },
90
- }),
91
56
  scaffold.layoutNode,
92
57
  ],
93
58
  contentWidgets: [
@@ -24,7 +24,7 @@ import { PHI_SPACING_TOKEN_KEYS } from "../../../../../components/widgets/config
24
24
  import { PhiColorFieldControl } from "../../../../../components/controls/phi-color-field-control";
25
25
  import { PhiBrandControl, resolvePhiBrandLogoUrl } from "../../../../../components/controls/phi-brand-control";
26
26
  import { PhiBackgroundControl } from "../../../../../components/controls/phi-background-control";
27
- import { PHI_ROOT_BACKGROUND_IMAGE_SOURCE_KINDS, PHI_ROOT_BACKGROUND_FILTERS, PHI_ROOT_BACKGROUND_MOTION_MODES, resolvePhiRootBackgroundPaintStyle, } from "../../../../../components/root/phi-root-background";
27
+ import { PHI_ROOT_BACKGROUND_BASE_KINDS, PHI_ROOT_BACKGROUND_IMAGE_SOURCE_KINDS, PHI_ROOT_BACKGROUND_FILTERS, PHI_ROOT_BACKGROUND_MOTION_MODES, resolvePhiRootBackgroundPaintStyle, } from "../../../../../components/root/phi-root-background";
28
28
  import { PHI_SHELL_CHROME_OVERLAY_BASE_KINDS, PHI_SHELL_CHROME_OVERLAY_FILTERS, PHI_SHELL_CHROME_OVERLAY_IMAGE_SOURCE_KINDS, PHI_SHELL_CHROME_OVERLAY_MOTION_MODES, resolvePhiShellChromeOverlayConfig, resolvePhiShellChromePaneShadow, resolvePhiShellChromePaneShadows, resolvePhiShellChromeOverlayStyle, } from "../../../../../components/root/phi-shell-chrome-overlay";
29
29
  import { normalizePhiBackgroundWidgetConfig } from "../../../../../components/widgets/config/background";
30
30
  import { PhiMediaPickerBinding } from "../../../../../components/media/phi-media-picker-binding";
@@ -34,13 +34,12 @@ import { buildPhiMediaAssetContentDeliveryUrl, PhiMediaAssetFlags, PhiMediaKind,
34
34
  import { createPhiMediaPickerAssetControllerRoutes } from "../../../../../components/media/asset-controller-routes";
35
35
  import { PhiPresetSizeControl } from "../../../../../components/controls/phi-preset-size-control";
36
36
  import { PhiButtonControl } from "../../../../../components/controls/phi-button-control";
37
- import { PhiCascaderControl } from "../../../../../components/controls/phi-cascader-control";
38
37
  import { PhiCheckboxControl } from "../../../../../components/controls/phi-checkbox-control";
39
38
  import { PhiLabeledControl } from "../../../../../components/controls/phi-labeled-control";
40
39
  import { PhiNumberControl } from "../../../../../components/controls/phi-number-control";
41
40
  import { PhiColorControl } from "../../../../../components/controls/phi-color-control";
42
- import { usePhiControlOptionsProvider } from "../../../../../components/controls/phi-options-provider";
43
- import { PHI_AUTHORING_OPTION_PROVIDER_KEYS } from "../../../../../constants/authoring-option-provider-keys";
41
+ import { PhiLinkTargetControl } from "../../../../../components/widgets/client/shared/phi-link-target-control";
42
+ import { usePhiPageReferenceOptions } from "../../../../../components/widgets/client/shared/use-page-reference-options";
44
43
  import { PhiShadowControl } from "../../../../../components/controls/phi-shadow-control";
45
44
  import { PhiIcon } from "../../../../../components/shell/phi-icon";
46
45
  import { PhiIconPickerControl } from "../../../../../components/controls/phi-icon-picker-control";
@@ -1476,13 +1475,11 @@ export function PhiBuilderBrandIdentityControlsWidgetClient({ runtime, config, }
1476
1475
  */
1477
1476
  const fallbackWordmark = runtime.site.name ?? runtime.site.key;
1478
1477
  /*
1479
- * The Pages of the public Area, from the same provider the Builder's own Page Cascader reads. The
1480
- * Widget declares the provider so the Page mounts it; without that the list is simply empty, which
1481
- * would look like a Site with one Page.
1478
+ * The Pages of the public Area as references, from the list every link picker outside the Builder's
1479
+ * own catalog reads (`/api/site/page-references`). The Brand leads into the public Area wherever it
1480
+ * is drawn, so that is the Area written into the target.
1482
1481
  */
1483
- const { options: homePathOptions } = usePhiControlOptionsProvider({
1484
- optionsProvider: { providerKey: PHI_AUTHORING_OPTION_PROVIDER_KEYS.pages },
1485
- });
1482
+ const { options: homePageOptions, failed: homePagesFailed } = usePhiPageReferenceOptions("public", "phi-theme-brand");
1486
1483
  function updateWordmarkPart(index, patch) {
1487
1484
  publishDraft(mergeThemeWordmarkParts(state.draft, editableWordmarkParts.map((part, at) => (at === index ? { ...part, ...patch } : part))));
1488
1485
  }
@@ -1506,7 +1503,7 @@ export function PhiBuilderBrandIdentityControlsWidgetClient({ runtime, config, }
1506
1503
  backgroundSize: "contain",
1507
1504
  backgroundRepeat: "no-repeat",
1508
1505
  backgroundPosition: "left center",
1509
- } })) : (_jsx(PhiTypographyControl, { type: "secondary", style: { color: readAliasTokenString(logoGroundToken, "colorTextTertiary") }, children: "No Logo" })) }), _jsxs(PhiFlexControl, { align: "center", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: brand.logo?.[mode] == null, blockTitle: markSet.title, onReset: () => publishDraft(mergeThemeBrandLogo(state.draft, mode, undefined)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: JSON.stringify(logo) === JSON.stringify(logos[otherMode] ?? null), onCopy: () => publishDraft(mergeThemeBrandLogo(state.draft, otherMode, logo ?? { sourceKind: "none" })) })] })] }), _jsx(PhiMediaPickerBinding, { config: {
1506
+ } })) : (_jsx(PhiTypographyControl, { type: "secondary", style: { color: readAliasTokenString(logoGroundToken, "colorTextTertiary") }, children: "No Logo" })) }), _jsxs(PhiFlexControl, { vertical: true, align: "stretch", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: brand.logo?.[mode] == null, blockTitle: markSet.title, onReset: () => publishDraft(mergeThemeBrandLogo(state.draft, mode, undefined)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: JSON.stringify(logo) === JSON.stringify(logos[otherMode] ?? null), onCopy: () => publishDraft(mergeThemeBrandLogo(state.draft, otherMode, logo ?? { sourceKind: "none" })) })] })] }), _jsx(PhiMediaPickerBinding, { config: {
1510
1507
  mediaType: PhiMediaKind.Image,
1511
1508
  pageSize: 12,
1512
1509
  showPagination: true,
@@ -1530,8 +1527,15 @@ export function PhiBuilderBrandIdentityControlsWidgetClient({ runtime, config, }
1530
1527
  })) }) }), _jsx(PhiLabeledControl, { label: "Offset", fill: true, children: _jsx(PhiNumberControl, { prefix: "px", style: { width: "100%" }, value: brand.logoYOffset ?? 0, step: 1, min: -16, max: 16, onChange: (next) => publishDraft(mergeThemeBrand(state.draft, {
1531
1528
  // No offset is no key, the way `Forward` spells its default away below.
1532
1529
  logoYOffset: next ? next : null,
1533
- })) }) }), _jsx(PhiLabeledControl, { label: "Forward", fill: true, children: _jsx(PhiCascaderControl, { value: brand.homeHref ?? "/", options: homePathOptions, placeholder: "Where the Brand leads", onChange: (next) => publishDraft(mergeThemeBrand(state.draft, {
1534
- homeHref: next === "/" ? null : next,
1530
+ })) }) }), _jsx(PhiLabeledControl, { label: "Forward", fill: true, children: _jsx(PhiLinkTargetControl, { value: brand.homeLinkTarget, options: homePageOptions, area: "public", labels: {
1531
+ none: "Site root",
1532
+ page: "Page",
1533
+ external: "Address",
1534
+ pagePlaceholder: homePagesFailed ? "The Pages could not be loaded." : "Select a Page",
1535
+ externalPlaceholder: "https://example.com",
1536
+ newTab: "Open in a new tab",
1537
+ }, onChange: (next) => publishDraft(mergeThemeBrand(state.draft, {
1538
+ homeLinkTarget: next ?? null,
1535
1539
  })) }) })] })] })),
1536
1540
  },
1537
1541
  {
@@ -1611,12 +1615,12 @@ export function PhiBuilderBrandBackgroundControlsWidgetClient({ runtime, config,
1611
1615
  {
1612
1616
  key: "root",
1613
1617
  label: _jsx(PhiTypographyControl, { strong: true, children: "Root Background" }),
1614
- children: (_jsxs(PhiFlexControl, { vertical: true, gap: clientToken.paddingXS, children: [_jsxs(PhiFlexControl, { align: "center", justify: "space-between", gap: clientToken.paddingXS, children: [_jsxs(PhiTypographyControl, { type: "secondary", children: [modeLabel, ". One layer behind the whole Site, fixed to the viewport."] }), _jsxs(PhiFlexControl, { align: "center", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: state.draft.root?.background?.[mode] == null, blockTitle: composition.ground.title, onReset: () => publishDraft(clearThemeRootSurface(state.draft, "background", mode)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: isSamePhiBackgroundConfig(rootBackground, otherRootBackground), onCopy: () => publishDraft(mergeThemeRootSurface(state.draft, "background", otherMode, rootBackground)) })] })] }), _jsx(PhiBackgroundControl, { value: rootBackground, motionModes: PHI_ROOT_BACKGROUND_MOTION_MODES, filters: PHI_ROOT_BACKGROUND_FILTERS, imageSourceKinds: PHI_ROOT_BACKGROUND_IMAGE_SOURCE_KINDS, renderMediaPicker: renderPhiThemeRootBackgroundMediaPicker, editTransaction: editTransaction, onChange: (value) => publishDraft(mergeThemeRootSurface(state.draft, "background", mode, value)) }, mode)] })),
1618
+ children: (_jsxs(PhiFlexControl, { vertical: true, gap: clientToken.paddingXS, children: [_jsxs(PhiFlexControl, { align: "center", justify: "space-between", gap: clientToken.paddingXS, children: [_jsxs(PhiTypographyControl, { type: "secondary", children: [modeLabel, ". One layer behind the whole Site, fixed to the viewport."] }), _jsxs(PhiFlexControl, { vertical: true, align: "stretch", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: state.draft.root?.background?.[mode] == null, blockTitle: composition.ground.title, onReset: () => publishDraft(clearThemeRootSurface(state.draft, "background", mode)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: isSamePhiBackgroundConfig(rootBackground, otherRootBackground), onCopy: () => publishDraft(mergeThemeRootSurface(state.draft, "background", otherMode, rootBackground)) })] })] }), _jsx(PhiBackgroundControl, { value: rootBackground, motionModes: PHI_ROOT_BACKGROUND_MOTION_MODES, filters: PHI_ROOT_BACKGROUND_FILTERS, baseKinds: PHI_ROOT_BACKGROUND_BASE_KINDS, imageSourceKinds: PHI_ROOT_BACKGROUND_IMAGE_SOURCE_KINDS, renderMediaPicker: renderPhiThemeRootBackgroundMediaPicker, editTransaction: editTransaction, onChange: (value) => publishDraft(mergeThemeRootSurface(state.draft, "background", mode, value)) }, mode)] })),
1615
1619
  },
1616
1620
  {
1617
1621
  key: "chrome",
1618
1622
  label: _jsx(PhiTypographyControl, { strong: true, children: "Chrome Overlay" }),
1619
- children: (_jsxs(PhiFlexControl, { vertical: true, gap: clientToken.paddingXS, children: [_jsxs(PhiFlexControl, { align: "center", justify: "space-between", gap: clientToken.paddingXS, children: [_jsxs(PhiTypographyControl, { type: "secondary", children: [modeLabel, ". Shared by the Header, Sider and Footer Regions. Content and Hero never take it, and a Region that authors its own Background or Effect paints over it."] }), _jsxs(PhiFlexControl, { align: "center", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: state.draft.root?.chrome?.[mode] == null, blockTitle: composition.ground.title, onReset: () => publishDraft(clearThemeRootSurface(state.draft, "chrome", mode)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: isSamePhiBackgroundConfig(chromeOverlay, otherChromeOverlay), onCopy: () => publishDraft(mergeThemeRootSurface(state.draft, "chrome", otherMode, chromeOverlay)) })] })] }), _jsx(PhiBackgroundControl, { value: chromeOverlay, motionModes: PHI_SHELL_CHROME_OVERLAY_MOTION_MODES, filters: PHI_SHELL_CHROME_OVERLAY_FILTERS, baseKinds: PHI_SHELL_CHROME_OVERLAY_BASE_KINDS, imageSourceKinds: PHI_SHELL_CHROME_OVERLAY_IMAGE_SOURCE_KINDS, renderMediaPicker: renderPhiThemeRootBackgroundMediaPicker, editTransaction: editTransaction, onChange: (value) => publishDraft(mergeThemeRootSurface(state.draft, "chrome", mode, value)) }, `chrome-${mode}`)] })),
1623
+ children: (_jsxs(PhiFlexControl, { vertical: true, gap: clientToken.paddingXS, children: [_jsxs(PhiFlexControl, { align: "center", justify: "space-between", gap: clientToken.paddingXS, children: [_jsxs(PhiTypographyControl, { type: "secondary", children: [modeLabel, ". Shared by the Header, Sider and Footer Regions. Content and Hero never take it, and a Region that authors its own Background or Effect paints over it."] }), _jsxs(PhiFlexControl, { vertical: true, align: "stretch", gap: clientToken.paddingXXS, style: { flexShrink: 0 }, children: [_jsx(PhiBrandBlockResetButton, { disabled: state.draft.root?.chrome?.[mode] == null, blockTitle: composition.ground.title, onReset: () => publishDraft(clearThemeRootSurface(state.draft, "chrome", mode)) }), _jsx(PhiBrandCopyModeButton, { mode: mode, disabled: isSamePhiBackgroundConfig(chromeOverlay, otherChromeOverlay), onCopy: () => publishDraft(mergeThemeRootSurface(state.draft, "chrome", otherMode, chromeOverlay)) })] })] }), _jsx(PhiBackgroundControl, { value: chromeOverlay, motionModes: PHI_SHELL_CHROME_OVERLAY_MOTION_MODES, filters: PHI_SHELL_CHROME_OVERLAY_FILTERS, baseKinds: PHI_SHELL_CHROME_OVERLAY_BASE_KINDS, imageSourceKinds: PHI_SHELL_CHROME_OVERLAY_IMAGE_SOURCE_KINDS, renderMediaPicker: renderPhiThemeRootBackgroundMediaPicker, editTransaction: editTransaction, onChange: (value) => publishDraft(mergeThemeRootSurface(state.draft, "chrome", mode, value)) }, `chrome-${mode}`)] })),
1620
1624
  },
1621
1625
  {
1622
1626
  key: "shadow",
@@ -1,6 +1,5 @@
1
1
  import { isPhiCmsAreaKey, } from "../../../../../constants/cms-areas";
2
2
  import { PHI_MEDIA_LIBRARY_DATA_PROVIDER_KEYS } from "../../../../../constants/media-library-provider-keys";
3
- import { PHI_AUTHORING_OPTION_PROVIDER_KEYS } from "../../../../../constants/authoring-option-provider-keys";
4
3
  import { PHI_THEME_SIGNAL_CHANNELS } from "../../../../../plugins/runtime-modules/theme/controller/signals";
5
4
  import { createPhiCmsWidgetDefinition } from "../../../../../components/widgets/config/helpers";
6
5
  import { readString } from "../../../../../components/widgets/config/parser-primitives";
@@ -52,15 +51,12 @@ export const PHI_BUILDER_BRAND_IDENTITY_CONTROLS_WIDGET_DEFINITION = createPhiCm
52
51
  iconFamily: "theme",
53
52
  slotSizePolicy: "fill-inline",
54
53
  /*
55
- * The Logo is picked from the Media library and the Brand's link from the Page catalogue, and a Page
56
- * mounts only the Data Providers its tree asks for. The scan reads this declaration; a Provider a
57
- * Widget reaches for from inside its client code is invisible to it. Same reason the Background
58
- * panel declares its own.
54
+ * The Logo is picked from the Media library, and a Page mounts only the Data Providers its tree asks
55
+ * for. The scan reads this declaration; a Provider a Widget reaches for from inside its client code is
56
+ * invisible to it. Same reason the Background panel declares its own. The Brand's link needs none: its
57
+ * Pages come from `/api/site/page-references`, as every link picker's outside the Builder catalog do.
59
58
  */
60
- requiredDataProviders: [
61
- PHI_MEDIA_LIBRARY_DATA_PROVIDER_KEYS.collection,
62
- PHI_AUTHORING_OPTION_PROVIDER_KEYS.pages,
63
- ],
59
+ requiredDataProviders: [PHI_MEDIA_LIBRARY_DATA_PROVIDER_KEYS.collection],
64
60
  runtimeSignals: PHI_BUILDER_BRAND_PREVIEW_MODE_SIGNALS,
65
61
  fields: [
66
62
  { key: "themeKey", type: "string", label: "Theme Key" },
@@ -21,6 +21,12 @@ export async function resolvePhiCmsTreeLinkTargets(tree, runtime) {
21
21
  for (const node of [...tree.regions, ...tree.overlays, ...tree.layoutNodes, ...tree.contentWidgets]) {
22
22
  collectPhiLinkTargetReferences(node.config, references);
23
23
  }
24
+ /*
25
+ * And the Brand's, which is the Site's and not any node's (THEME.md "Brand"): the Brand Widget leads
26
+ * where the Theme says, and that target resolves with the rest of the page rather than in a round
27
+ * trip of its own per Brand drawn -- a header and a footer would be two.
28
+ */
29
+ collectPhiLinkTargetReferences(runtime.site.theme?.brand, references);
24
30
  return resolvePhiLinkTargetReferences(references, runtime);
25
31
  }
26
32
  /**
@@ -501,6 +501,18 @@ h5.phi-typography, .phi-typography h5 {
501
501
  top: var(--phi-table-sticky-top, var(--phi-shell-sticky-top, 0px)) !important;
502
502
  }
503
503
 
504
+ /*
505
+ * A sticky header row is a table of its own, and its columns are the body's: Ant Design measures the
506
+ * body's columns and writes their widths into the header's `<colgroup>`. In `auto` layout those widths
507
+ * are only a floor, and a `content` column's header cell carries `width: 1%` -- so each header cell shrank
508
+ * to its title and a `fill` column took the rest, out of line with the rows under it. Fixed layout makes
509
+ * the `<colgroup>` the header's widths. The body keeps the configured `layout.mode`; Ant Design writes
510
+ * that inline on both tables, so the header's is stated over it.
511
+ */
512
+ .phi-table-control.phi-table-control--sticky-header [class~="ant-table-sticky-holder"] > table {
513
+ table-layout: fixed !important;
514
+ }
515
+
504
516
  :is([data-phi-modal-body-content], [data-phi-drawer-body-content]) {
505
517
  --phi-table-sticky-top: 0px;
506
518
  }
@@ -562,6 +574,55 @@ h5.phi-typography, .phi-typography h5 {
562
574
  align-self: center;
563
575
  }
564
576
 
577
+ /*
578
+ * Its arrow on the switcher's own centre. antd sets the switcher's line height to the row height and lets
579
+ * the arrow stand on that line's baseline, which leaves a small icon above the middle of the box. The box
580
+ * keeps a compact control's height, so its hover ground is the square it was.
581
+ */
582
+ .phi-tree-control.phi-tree-control [class~="ant-tree-switcher"] {
583
+ display: inline-flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ min-block-size: var(--ant-control-height-sm);
587
+ }
588
+
589
+ /* --- Compact Select ------------------------------------------------------------------------------ */
590
+
591
+ /*
592
+ * A compact control writes small: Input, InputNumber, DatePicker and Button take the scale's `sm` through
593
+ * their component tokens (theme/phi-theme.ts). Select has no such token -- it sizes its words by variables
594
+ * of its own and gives `-sm` the body's -- so its compact form is told here. Both variables move together:
595
+ * Select reckons its padding as (height - font height) / 2, so a smaller font size with the body's font
596
+ * height would leave the box short of `controlHeightSM` by the difference.
597
+ */
598
+ [class~="ant-select"][class~="ant-select-sm"] {
599
+ --ant-select-font-size: var(--ant-font-size-sm);
600
+ --ant-select-font-height: calc(var(--ant-font-size-sm) * var(--ant-select-line-height));
601
+ }
602
+
603
+ /* The list a compact Select opens writes as the field does; Ant Design gives every Select's list the body size. */
604
+ .phi-select-popup--sm [class~="ant-select-item"] {
605
+ font-size: var(--ant-font-size-sm);
606
+ }
607
+
608
+ /* --- Running text --------------------------------------------------------------------------------- */
609
+
610
+ /*
611
+ * Markdown and HTML text breaks long words at their syllables, by the rules of the page's language
612
+ * (`<html lang>`), rather than leaving a ragged edge or running a word past its column. Code keeps its
613
+ * characters as written.
614
+ */
615
+ .phi-markdown-text,
616
+ .phi-html-widget__content {
617
+ hyphens: auto;
618
+ overflow-wrap: break-word;
619
+ }
620
+
621
+ .phi-markdown-text :is(pre, code),
622
+ .phi-html-widget__content :is(pre, code) {
623
+ hyphens: manual;
624
+ }
625
+
565
626
  /* --- Plain table --------------------------------------------------------------------------------- */
566
627
 
567
628
  /* Wide tables scroll inside their own box instead of widening the Page. */
@@ -570,11 +631,12 @@ h5.phi-typography, .phi-typography h5 {
570
631
  overflow-x: auto;
571
632
  }
572
633
 
634
+ /* A table is read across, not down: its cells take the small size, as the Table Control's do. */
573
635
  .phi-plain-table {
574
636
  width: 100%;
575
637
  border-collapse: collapse;
576
- font-size: var(--ant-font-size);
577
- line-height: var(--ant-line-height);
638
+ font-size: var(--ant-font-size-sm);
639
+ line-height: var(--ant-line-height-sm);
578
640
  color: var(--ant-color-text);
579
641
  }
580
642
 
package/styles/layout.css CHANGED
@@ -493,8 +493,13 @@
493
493
  * COLOUR the page already computed from them, not the variable -- so the box states its text colour again
494
494
  * and its content reads it from the variables in force there. When the content is already in that mode,
495
495
  * the class has no variables of its own and this resolves to what the page has anyway.
496
+ *
497
+ * The box only, not an Ant Design component inside it: the tone scope keys its variables to the same
498
+ * class, and Ant Design puts that key on every component it draws there. Stated on those too, the colour
499
+ * stopped inheritance at each of them -- a Quick Link's words kept the text colour while the link turned
500
+ * to its hover colour around them.
496
501
  */
497
- :where(.phi-tone-light, .phi-tone-dark, .phi-tone-inverse) {
502
+ :where(.phi-tone-light, .phi-tone-dark, .phi-tone-inverse):where(:not([class^="ant-"], [class*=" ant-"])) {
498
503
  color: var(--ant-color-text);
499
504
  }
500
505
 
@@ -576,6 +581,43 @@
576
581
  transition: scale 300ms ease;
577
582
  }
578
583
 
584
+ /*
585
+ * A card with a ribbon stands in the ribbon's wrapper (`PhiRibbonWrapControl`): a column that takes the
586
+ * slot's size and hands it to the card. A card that lifts carries its ribbon with it -- the ribbon is the
587
+ * card's sibling, so the wrapper moves it when the card moves.
588
+ */
589
+ :where(.phi-card-ribbon) {
590
+ display: flex;
591
+ flex-direction: column;
592
+ min-width: 0;
593
+ }
594
+
595
+ :where(.phi-card-ribbon) > :where(.phi-card) {
596
+ flex: 1 1 auto;
597
+ }
598
+
599
+ /*
600
+ * A little lower than Ant Design sets it, so the ribbon stands centred on the eyebrow's line. Outside
601
+ * `:where`: Ant Design's own `.ant-ribbon` states `top`, and this has to outrank it.
602
+ */
603
+ .phi-card-ribbon > .ant-ribbon {
604
+ top: calc(var(--ant-margin-xs) + 3px);
605
+ }
606
+
607
+ :where(.phi-card-ribbon--lift) > :where(.ant-ribbon) {
608
+ transition: translate 200ms ease;
609
+ }
610
+
611
+ @media (hover: hover) {
612
+ :where(.phi-card-ribbon--lift):has(> .phi-card:hover) > :where(.ant-ribbon) {
613
+ translate: 0 -4px;
614
+ }
615
+ }
616
+
617
+ :where(.phi-card-ribbon--lift):has(> .phi-card:focus-within) > :where(.ant-ribbon) {
618
+ translate: 0 -4px;
619
+ }
620
+
579
621
  @media (hover: hover) {
580
622
  :where(.phi-card--hover-lift):hover {
581
623
  translate: 0 -4px;
@@ -629,7 +671,8 @@
629
671
  transition: none;
630
672
  }
631
673
 
632
- .phi-card--hover-lift:is(:hover, :focus-within) {
674
+ .phi-card--hover-lift:is(:hover, :focus-within),
675
+ .phi-card-ribbon--lift > .ant-ribbon {
633
676
  translate: none;
634
677
  }
635
678