@phis/ui 0.4.0 → 0.4.2

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 (55) hide show
  1. package/components/controls/phi-table-control.d.ts +6 -1
  2. package/components/controls/phi-table-control.js +16 -4
  3. package/components/layouts/layout-definitions.d.ts +2 -2
  4. package/components/layouts/layout-definitions.js +6 -1
  5. package/components/shell/use-phi-shell-sticky-top.d.ts +5 -0
  6. package/components/shell/use-phi-shell-sticky-top.js +78 -0
  7. package/components/widgets/helpers/semantic-html-translation.d.ts +3 -1
  8. package/components/widgets/helpers/semantic-html-translation.js +8 -3
  9. package/components/widgets/navigation-descriptor-resolver.server.js +6 -1
  10. package/components/widgets/shared/markdown-body-client.d.ts +2 -1
  11. package/components/widgets/shared/markdown-body-client.js +3 -1
  12. package/components/widgets/shared/markdown-render.js +23 -6
  13. package/helpers/text-source.d.ts +19 -0
  14. package/helpers/text-source.js +20 -0
  15. package/next/config.mjs +1 -1
  16. package/package.json +1 -1
  17. package/plugins/runtime-modules/auth/trees/phi-default-pub-confirm-page-tree.js +3 -0
  18. package/plugins/runtime-modules/auth/trees/phi-default-pub-login-page-tree.js +2 -0
  19. package/plugins/runtime-modules/auth/trees/phi-default-pub-registration-page-tree.js +3 -0
  20. package/plugins/runtime-modules/auth/trees/phi-default-pub-reset-password-page-tree.js +3 -0
  21. package/plugins/runtime-modules/builder/clients/widget-inspector.js +9 -0
  22. package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +4 -0
  23. package/plugins/runtime-modules/core/widgets/card/server.js +4 -2
  24. package/plugins/runtime-modules/core/widgets/description/config.d.ts +3 -0
  25. package/plugins/runtime-modules/core/widgets/description/config.js +2 -0
  26. package/plugins/runtime-modules/core/widgets/description/server.js +10 -12
  27. package/plugins/runtime-modules/core/widgets/entry-list/server.js +2 -2
  28. package/plugins/runtime-modules/core/widgets/html/server.js +1 -1
  29. package/plugins/runtime-modules/core/widgets/markdown/config.d.ts +3 -1
  30. package/plugins/runtime-modules/core/widgets/markdown/config.js +14 -0
  31. package/plugins/runtime-modules/core/widgets/markdown-toc/server.js +7 -2
  32. package/plugins/runtime-modules/core/widgets/quick-links/config.d.ts +3 -0
  33. package/plugins/runtime-modules/core/widgets/quick-links/config.js +2 -0
  34. package/plugins/runtime-modules/core/widgets/quick-links/plugin.js +4 -2
  35. package/plugins/runtime-modules/core/widgets/quick-links/server.d.ts +5 -1
  36. package/plugins/runtime-modules/core/widgets/quick-links/server.js +4 -2
  37. package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +3 -0
  38. package/plugins/runtime-modules/core/widgets/simple-text/config.js +2 -0
  39. package/plugins/runtime-modules/core/widgets/simple-text/plugin.js +1 -1
  40. package/plugins/runtime-modules/core/widgets/simple-text/server.d.ts +4 -1
  41. package/plugins/runtime-modules/core/widgets/simple-text/server.js +3 -3
  42. package/plugins/runtime-modules/core/widgets/table/client.js +1 -1
  43. package/plugins/runtime-modules/core/widgets/table/config.d.ts +1 -0
  44. package/plugins/runtime-modules/core/widgets/table/config.js +3 -0
  45. package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +12 -4
  46. package/plugins/runtime-modules/public/trees/phi-default-pub-contact-page-tree.js +2 -0
  47. package/plugins/runtime-modules/public/trees/phi-default-pub-unsubscribe-page-tree.js +3 -0
  48. package/plugins/runtime-modules/public/trees/phi-default-pub-welcome-page-tree.js +2 -1
  49. package/plugins/runtime-modules/revisions/trees/phi-default-builder-revisions-page-tree.js +4 -0
  50. package/server-helpers/translate.d.ts +9 -0
  51. package/server-helpers/translate.js +14 -0
  52. package/styles/controls.css +41 -1
  53. package/styles/root.css +20 -19
  54. package/types/core-widget-placements.d.ts +8 -0
  55. package/types/table-widget.d.ts +2 -0
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { trGlobal, trGlobalForLocale, } from "../../../../../server-helpers/translate";
2
+ import { trWidgetText } from "../../../../../server-helpers/translate";
3
3
  import { maskPhiTextPlaceholders, resolvePhiTextPlaceholders } from "../../../../../helpers/text-placeholders";
4
4
  import { PhiBulletListControl } from "../../../../../components/controls/phi-bullet-list-control";
5
5
  import { buildPhiBulletListTree, resolvePhiBulletListStyle, } from "../../../../../components/widgets/shared/bullet-list-vocabulary";
@@ -12,17 +12,15 @@ import { PHI_COLOR, PHI_FONT_SIZE, PHI_LINE_HEIGHT, PHI_SPACE, } from "../../../
12
12
  * `{year}` out of the translator's hands, which would otherwise hand back `{Jahr}`.
13
13
  */
14
14
  async function translateLabel(locale, value, runtime, translate,
15
+ /** Whose words these are, which decides the store (`helpers/text-source.ts`). */
16
+ textSource,
15
17
  /** The text's own values, which stand in it as `%1` onwards and are never translated. */
16
18
  values = []) {
17
19
  if (!value) {
18
20
  return undefined;
19
21
  }
20
22
  const masked = maskPhiTextPlaceholders(value, values.length);
21
- const translated = !translate
22
- ? masked.text
23
- : locale
24
- ? await trGlobalForLocale(locale, masked.text)
25
- : await trGlobal(masked.text);
23
+ const translated = translate ? await trWidgetText(locale, masked.text, textSource) : masked.text;
26
24
  return resolvePhiTextPlaceholders(translated, masked.names, runtime, values);
27
25
  }
28
26
  function resolveDescriptionToneColor(tone) {
@@ -39,15 +37,15 @@ function resolveDescriptionToneColor(tone) {
39
37
  }
40
38
  export async function PhiDescriptionWidget({ labels, config, runtime, translate, }) {
41
39
  const locale = runtime?.locale.current;
42
- const eyebrow = await translateLabel(locale, labels.eyebrow ?? config?.eyebrow, runtime, translate);
43
- const title = await translateLabel(locale, labels.title ?? config?.title, runtime, translate);
44
- const description = await translateLabel(locale, labels.description ?? config?.description, runtime, translate);
45
- const asideTitle = await translateLabel(locale, labels.asideTitle ?? config?.asideTitle, runtime, translate);
40
+ const eyebrow = await translateLabel(locale, labels.eyebrow ?? config?.eyebrow, runtime, translate, config?.textSource);
41
+ const title = await translateLabel(locale, labels.title ?? config?.title, runtime, translate, config?.textSource);
42
+ const description = await translateLabel(locale, labels.description ?? config?.description, runtime, translate, config?.textSource);
43
+ const asideTitle = await translateLabel(locale, labels.asideTitle ?? config?.asideTitle, runtime, translate, config?.textSource);
46
44
  const asideEntries = await Promise.all((labels.asideItems ?? config?.asideItems ?? []).map(async (entry) => ({
47
45
  ...entry,
48
- text: await translateLabel(locale, entry.text, runtime, translate, entry.values),
46
+ text: await translateLabel(locale, entry.text, runtime, translate, config?.textSource, entry.values),
49
47
  })));
50
- const footer = await translateLabel(locale, labels.footer ?? config?.footer, runtime, translate);
48
+ const footer = await translateLabel(locale, labels.footer ?? config?.footer, runtime, translate, config?.textSource);
51
49
  const resolvedAsideItems = buildPhiBulletListTree(asideEntries.filter((entry) => Boolean(entry.text)), (entry) => entry.text);
52
50
  const stackStyle = {
53
51
  display: "grid",
@@ -5,7 +5,7 @@ import { getPhiContentEntryReaderLabels } from "../../../../../components/widget
5
5
  import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
6
6
  import { fetchPhiSiteContentEntries } from "../../../../../gateway/content-entries";
7
7
  import { phiRuntime } from "../../../../../server-helpers/phi-runtime";
8
- import { trBulk } from "../../../../../server-helpers/translate";
8
+ import { trBulkForLocale } from "../../../../../server-helpers/translate";
9
9
  import { readPhiBoundContentEntry } from "../../content-entries/record-source.server";
10
10
  import { buildPhiEntryViews } from "./view.server";
11
11
  import { normalizePhiEntryListWidgetConfig } from "./config";
@@ -74,7 +74,7 @@ export async function PhiEntryListWidget({ config, runtime, registry, links, tra
74
74
  // A stacked entry's title is an `h2`; its text's headings start under it.
75
75
  headingOffset: 2,
76
76
  });
77
- const [title] = settings.title && translate ? await trBulk([settings.title]) : [settings.title];
77
+ const [title] = settings.title && translate ? await trBulkForLocale(locale, [settings.title]) : [settings.title];
78
78
  return (_jsx(PhiRuntimeModuleRenderClientHost, { type: PhiCmsWidgetType.EntryList, componentProps: {
79
79
  entries: views,
80
80
  labels,
@@ -52,7 +52,7 @@ export async function PhiHtmlWidget({ blockId, config, runtime, }) {
52
52
  }
53
53
  const translatedHtml = (sourceMode !== "url" && config?.resolvedContent != null) || config?.preferSource === true
54
54
  ? sourceHtml
55
- : await translateSemanticHtml(sourceHtml, sourceMode === "url" ? config?.sourceLocale?.trim() : undefined);
55
+ : await translateSemanticHtml(sourceHtml, sourceMode === "url" ? config?.sourceLocale?.trim() : undefined, runtime.locale.current);
56
56
  if (!translatedHtml) {
57
57
  return null;
58
58
  }
@@ -1,4 +1,4 @@
1
- import type { PhiMarkdownTextAlign } from "../../../../../types/core-widget-placements";
1
+ import type { PhiMarkdownImageFloat, PhiMarkdownTextAlign } from "../../../../../types/core-widget-placements";
2
2
  import { type PhiSpacingScaleKey } from "../../../../../components/widgets/config/spacing-options";
3
3
  import type { PhiCmsResolvedContent } from "../../../../../types/cms";
4
4
  import { type PhiBulletListStyle } from "../../../../../components/widgets/shared/bullet-list-vocabulary";
@@ -13,6 +13,8 @@ export type PhiCmsMarkdownWidgetConfig = PhiCmsWidgetConfigBase & {
13
13
  /** Set at render from the node's `NoTranslate` flag, or by a caller that knows; never authored. */
14
14
  translate?: boolean;
15
15
  textAlign?: PhiMarkdownTextAlign;
16
+ /** Pictures stand at this edge and the text flows past them; absent, a picture is a line of its own. */
17
+ imageFloat?: PhiMarkdownImageFloat;
16
18
  textBlockSpacingBefore?: PhiSpacingScaleKey;
17
19
  textBlockSpacingAfter?: PhiSpacingScaleKey;
18
20
  headingBlockSpacingBefore?: PhiSpacingScaleKey;
@@ -9,6 +9,13 @@ const PHI_MARKDOWN_TEXT_ALIGN_OPTIONS = [
9
9
  { value: "end", label: "End" },
10
10
  { value: "justify", label: "Justify" },
11
11
  ];
12
+ const PHI_MARKDOWN_IMAGE_FLOAT_OPTIONS = [
13
+ { value: "start", label: "Start" },
14
+ { value: "end", label: "End" },
15
+ ];
16
+ function readMarkdownImageFloat(value) {
17
+ return value === "start" || value === "end" ? value : undefined;
18
+ }
12
19
  // The one spacing scale every padding and gap speaks (`theme/phi-tokens.ts`), stored by its key.
13
20
  const PHI_MARKDOWN_SPACING_OPTIONS = PHI_SPACING_SCALE_KEYS.map((key) => ({ value: key, label: key }));
14
21
  function readMarkdownSourceMode(value) {
@@ -41,6 +48,7 @@ export function parsePhiCmsMarkdownWidgetConfig(config) {
41
48
  sourceLocale: readString(config.sourceLocale),
42
49
  revalidateSeconds: readNumber(config.revalidateSeconds),
43
50
  textAlign: readMarkdownTextAlign(config.textAlign),
51
+ imageFloat: readMarkdownImageFloat(config.imageFloat),
44
52
  textBlockSpacingBefore: readMarkdownSpacingKey(config.textBlockSpacingBefore),
45
53
  textBlockSpacingAfter: readMarkdownSpacingKey(config.textBlockSpacingAfter),
46
54
  headingBlockSpacingBefore: readMarkdownSpacingKey(config.headingBlockSpacingBefore),
@@ -115,6 +123,12 @@ export const PHI_MARKDOWN_WIDGET_DEFINITION = {
115
123
  label: "Align",
116
124
  options: PHI_MARKDOWN_TEXT_ALIGN_OPTIONS,
117
125
  },
126
+ {
127
+ key: "imageFloat",
128
+ type: "choice",
129
+ label: "Image Float",
130
+ options: PHI_MARKDOWN_IMAGE_FLOAT_OPTIONS,
131
+ },
118
132
  {
119
133
  key: "textBlockSpacingBefore",
120
134
  type: "choice",
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PhiCmsFlags } from "../../../../../constants/phi-cms";
3
3
  import { hasPhiFlag } from "../../../../../helpers/flags";
4
- import { tr } from "../../../../../server-helpers/translate";
4
+ import { trForLocale } from "../../../../../server-helpers/translate";
5
5
  import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
6
6
  import { parsePhiCmsMarkdownWidgetConfig } from "../markdown/config";
7
7
  import { PhiRuntimeModuleRenderClientHost } from "../../../../../components/runtime/runtime-module-render-client-manifest";
@@ -25,7 +25,12 @@ function filterHeadings(headings, config) {
25
25
  return headings.filter((heading) => heading.level >= minLevel && heading.level <= maxLevel);
26
26
  }
27
27
  export async function PhiMarkdownTocWidget({ config, defaultLabels, tree, runtime, }) {
28
- const title = await tr(config?.title?.trim() || defaultLabels.title);
28
+ /*
29
+ * The author's title in the render's locale; the default is a label set's and arrives translated, so
30
+ * asking for it again would hand a translator text that is no longer in the source language.
31
+ */
32
+ const ownTitle = config?.title?.trim();
33
+ const title = ownTitle ? await trForLocale(runtime.locale.current, ownTitle) : defaultLabels.title;
29
34
  const targetWidget = resolveTargetMarkdownWidget(tree, config);
30
35
  if (!targetWidget) {
31
36
  return (_jsx(PhiRuntimeModuleRenderClientHost, { type: PhiCmsWidgetType.MarkdownToc, componentProps: { config: { ...config, title, headings: [] } } }));
@@ -1,9 +1,12 @@
1
+ import { type PhiTextSource } from "../../../../../helpers/text-source";
1
2
  import { type PhiCmsWidgetConfigBase } from "../../../../../components/widgets/config/parser-primitives";
2
3
  export type PhiCmsQuickLinksWidgetConfig = PhiCmsWidgetConfigBase & {
3
4
  navKey?: string;
4
5
  title?: string;
5
6
  columns?: 1 | 2 | 3;
6
7
  separator?: boolean;
8
+ /** `"preset"` while the words are a Preset's and translated globally (`helpers/text-source.ts`). */
9
+ textSource?: PhiTextSource;
7
10
  };
8
11
  declare function parsePhiCmsQuickLinksWidgetConfig(config: Record<string, unknown>): PhiCmsQuickLinksWidgetConfig;
9
12
  export declare const PHI_QUICK_LINKS_WIDGET_DEFINITION: {
@@ -1,4 +1,5 @@
1
1
  import { resolvePhiCmsWidgetPluginKey } from "../../../../../constants/cms-widget-types";
2
+ import { readPhiTextSource } from "../../../../../helpers/text-source";
2
3
  import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
3
4
  import { PHI_AUTHORING_OPTION_PROVIDER_KEYS } from "../../../../../constants/authoring-option-provider-keys";
4
5
  import { readBoolean, readNumber, readRenderableBlockConfig, readString, } from "../../../../../components/widgets/config/parser-primitives";
@@ -11,6 +12,7 @@ function parsePhiCmsQuickLinksWidgetConfig(config) {
11
12
  title: readString(config.title),
12
13
  columns: columns === 1 || columns === 3 ? columns : 2,
13
14
  separator: readBoolean(config.separator) ?? true,
15
+ textSource: readPhiTextSource(config.textSource),
14
16
  };
15
17
  }
16
18
  export const PHI_QUICK_LINKS_WIDGET_DEFINITION = {
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PhiCmsFlags } from "../../../../../constants/phi-cms";
3
+ import { hasPhiFlag } from "../../../../../helpers/flags";
2
4
  import { PhiQuickLinksWidget } from "./server";
3
5
  import { PHI_QUICK_LINKS_WIDGET_DEFINITION, PHI_QUICK_LINKS_WIDGET_PLUGIN_TYPE, } from "./config";
4
6
  export const PHI_QUICK_LINKS_WIDGET_PLUGIN = {
5
7
  ...PHI_QUICK_LINKS_WIDGET_DEFINITION,
6
- render: ({ widget, runtime, config }) => (_jsx(PhiQuickLinksWidget, { runtime: runtime, navKey: config.navKey, title: config.title, columns: config.columns, separator: config.separator }, `widget-${widget.id}`)),
7
- renderPreview: ({ widget, runtime, config }) => (_jsx(PhiQuickLinksWidget, { runtime: runtime, navKey: config.navKey, title: config.title, columns: config.columns, separator: config.separator, interactive: false }, `widget-${widget.id}`)),
8
+ render: ({ widget, runtime, config }) => (_jsx(PhiQuickLinksWidget, { runtime: runtime, navKey: config.navKey, title: config.title, textSource: config.textSource, translate: !hasPhiFlag(widget.flags, PhiCmsFlags.NoTranslate), columns: config.columns, separator: config.separator }, `widget-${widget.id}`)),
9
+ renderPreview: ({ widget, runtime, config }) => (_jsx(PhiQuickLinksWidget, { runtime: runtime, navKey: config.navKey, title: config.title, textSource: config.textSource, translate: !hasPhiFlag(widget.flags, PhiCmsFlags.NoTranslate), columns: config.columns, separator: config.separator, interactive: false }, `widget-${widget.id}`)),
8
10
  };
9
11
  export { PHI_QUICK_LINKS_WIDGET_PLUGIN_TYPE };
@@ -1,10 +1,14 @@
1
1
  import type { PhiBlockRuntime } from "../../../../../types";
2
+ import type { PhiTextSource } from "../../../../../helpers/text-source";
2
3
  export type PhiQuickLinksWidgetProps = {
3
4
  runtime: Pick<PhiBlockRuntime, "site" | "locale" | "area" | "viewer" | "request">;
4
5
  navKey?: string;
5
6
  title?: string;
7
+ /** Whose words the title is, which decides the store that translates it. */
8
+ textSource?: PhiTextSource;
9
+ translate?: boolean;
6
10
  columns?: 1 | 2 | 3;
7
11
  separator?: boolean;
8
12
  interactive?: boolean;
9
13
  };
10
- export declare function PhiQuickLinksWidget({ runtime, navKey, title, columns, separator, interactive, }: PhiQuickLinksWidgetProps): Promise<import("react").JSX.Element | null>;
14
+ export declare function PhiQuickLinksWidget({ runtime, navKey, title, textSource, translate, columns, separator, interactive, }: PhiQuickLinksWidgetProps): Promise<import("react").JSX.Element | null>;
@@ -3,15 +3,17 @@ import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
3
3
  import { PhiRuntimeModuleRenderClientHost } from "../../../../../components/runtime/runtime-module-render-client-manifest";
4
4
  import { resolvePhiNavigationItems } from "../../../../../components/widgets/server/navigation-request";
5
5
  import { mapPhiNavItemsToQuickLinksItems } from "./items";
6
- export async function PhiQuickLinksWidget({ runtime, navKey, title, columns = 2, separator = true, interactive = true, }) {
6
+ import { trWidgetText } from "../../../../../server-helpers/translate";
7
+ export async function PhiQuickLinksWidget({ runtime, navKey, title, textSource, translate = true, columns = 2, separator = true, interactive = true, }) {
7
8
  const resolvedNavKey = navKey?.trim() ||
8
9
  `${runtime.area}:footer`;
9
10
  const resolvedNavigationItems = (await resolvePhiNavigationItems(runtime, resolvedNavKey)) ?? [];
10
11
  if (resolvedNavigationItems.length === 0) {
11
12
  return null;
12
13
  }
14
+ const ownTitle = title?.trim();
13
15
  const labels = {
14
- title,
16
+ title: ownTitle && translate ? await trWidgetText(runtime.locale.current, ownTitle, textSource) : title,
15
17
  };
16
18
  const config = {
17
19
  columns,
@@ -1,3 +1,4 @@
1
+ import { type PhiTextSource } from "../../../../../helpers/text-source";
1
2
  import { type PhiSimpleTextMark } from "../../../../../types/core-widget-placements";
2
3
  import type { PhiCmsResolvedContent } from "../../../../../types";
3
4
  import type { PhiWidgetFontFamilyKey, PhiWidgetFontSizeKey } from "../../../../../types/site-theme";
@@ -15,6 +16,8 @@ export type PhiCmsSimpleTextWidgetConfig = PhiCmsWidgetConfigBase & {
15
16
  tone?: PhiTextTone;
16
17
  marks?: PhiSimpleTextMark[];
17
18
  disabled?: boolean;
19
+ /** `"preset"` while the words are a Preset's and translated globally (`helpers/text-source.ts`). */
20
+ textSource?: PhiTextSource;
18
21
  };
19
22
  declare function parsePhiCmsSimpleTextWidgetConfig(config: Record<string, unknown>): PhiCmsSimpleTextWidgetConfig;
20
23
  export type PhiSimpleTextWidgetRenderableConfig = PhiCmsSimpleTextWidgetConfig & {
@@ -1,4 +1,5 @@
1
1
  import { resolvePhiCmsWidgetPluginKey } from "../../../../../constants/cms-widget-types";
2
+ import { readPhiTextSource } from "../../../../../helpers/text-source";
2
3
  import { PHI_SIMPLE_TEXT_MARKS } from "../../../../../types/core-widget-placements";
3
4
  import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
4
5
  import { PHI_RENDERABLE_BLOCK_GEOMETRY_FIELDS } from "../../../../../helpers/renderable-block-plugin-fields";
@@ -27,6 +28,7 @@ function parsePhiCmsSimpleTextWidgetConfig(config) {
27
28
  tone: readPhiTextTone(config.tone),
28
29
  marks: readSimpleTextMarks(config.marks),
29
30
  disabled: readBoolean(config.disabled),
31
+ textSource: readPhiTextSource(config.textSource),
30
32
  };
31
33
  }
32
34
  export const PHI_SIMPLE_TEXT_WIDGET_DEFINITION = {
@@ -9,7 +9,7 @@ export const PHI_SIMPLE_TEXT_WIDGET_PLUGIN = {
9
9
  const noTranslate = hasPhiFlag(widget.flags, PhiCmsFlags.NoTranslate);
10
10
  // A record's text is already translated; only the Widget's own still has to be.
11
11
  const hasResolvedText = Boolean(widget.resolvedContent?.textFields.text);
12
- return (_jsx(PhiSimpleTextWidget, { blockId: widget.id, translate: !noTranslate && !hasResolvedText, labels: {
12
+ return (_jsx(PhiSimpleTextWidget, { blockId: widget.id, translate: !noTranslate && !hasResolvedText, textSource: config.textSource, labels: {
13
13
  text: resolvePhiSimpleTextWidgetText({
14
14
  ...config,
15
15
  label: widget.label ?? undefined,
@@ -1,3 +1,4 @@
1
+ import type { PhiTextSource } from "../../../../../helpers/text-source";
1
2
  import type { PhiCmsInstanceId, PhiRenderableBlockBase, PhiServerBlockBaseProps } from "../../../../../types";
2
3
  import type { PhiSimpleTextWidgetClientConfig } from "./client";
3
4
  export type PhiSimpleTextWidgetLabels = {
@@ -6,6 +7,8 @@ export type PhiSimpleTextWidgetLabels = {
6
7
  export type PhiSimpleTextWidgetConfig = PhiSimpleTextWidgetClientConfig & PhiRenderableBlockBase;
7
8
  export type PhiSimpleTextWidgetProps = PhiServerBlockBaseProps<PhiSimpleTextWidgetLabels, PhiSimpleTextWidgetConfig> & {
8
9
  blockId: PhiCmsInstanceId;
10
+ /** Whose words the text is, which decides the store that translates it (`helpers/text-source.ts`). */
11
+ textSource?: PhiTextSource;
9
12
  /**
10
13
  * Whether the text still has to be translated, which is true only where it is the Widget's own.
11
14
  *
@@ -16,4 +19,4 @@ export type PhiSimpleTextWidgetProps = PhiServerBlockBaseProps<PhiSimpleTextWidg
16
19
  */
17
20
  translate?: boolean;
18
21
  };
19
- export declare function PhiSimpleTextWidget({ blockId, labels, config, translate, runtime, }: PhiSimpleTextWidgetProps): Promise<import("react").JSX.Element>;
22
+ export declare function PhiSimpleTextWidget({ blockId, labels, config, translate, textSource, runtime, }: PhiSimpleTextWidgetProps): Promise<import("react").JSX.Element>;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { trGlobal, trGlobalForLocale, } from "../../../../../server-helpers/translate";
2
+ import { trWidgetText } from "../../../../../server-helpers/translate";
3
3
  import { maskPhiTextPlaceholders, resolvePhiTextPlaceholders } from "../../../../../helpers/text-placeholders";
4
4
  import { PhiCmsWidgetType } from "../../../../../constants/cms-widget-types";
5
5
  import { PhiRuntimeModuleRenderClientHost } from "../../../../../components/runtime/runtime-module-render-client-manifest";
6
- export async function PhiSimpleTextWidget({ blockId, labels, config, translate, runtime, }) {
6
+ export async function PhiSimpleTextWidget({ blockId, labels, config, translate, textSource, runtime, }) {
7
7
  const locale = runtime?.locale.current;
8
8
  /*
9
9
  * Masked before the translation and filled after it.
@@ -14,7 +14,7 @@ export async function PhiSimpleTextWidget({ blockId, labels, config, translate,
14
14
  */
15
15
  const masked = maskPhiTextPlaceholders(labels.text);
16
16
  const translated = translate && masked.text
17
- ? locale ? await trGlobalForLocale(locale, masked.text) : await trGlobal(masked.text)
17
+ ? await trWidgetText(locale, masked.text, textSource)
18
18
  : masked.text;
19
19
  const text = resolvePhiTextPlaceholders(translated, masked.names, runtime);
20
20
  return (_jsx(PhiRuntimeModuleRenderClientHost, { type: PhiCmsWidgetType.SimpleText, componentProps: {
@@ -913,7 +913,7 @@ export function PhiTableWidgetClient({ config: inputConfig, externalSearch, exte
913
913
  afterRowIdentity: null,
914
914
  }).catch(() => undefined).finally(clearPhiDataDragPayload);
915
915
  }
916
- : undefined, loading: effectiveLoading, bordered: presentation.borders, striped: presentation.row?.striped, showHeader: presentation.header, size: resolvedControlSize, emptyText: (_jsxs(PhiFlexControl, { vertical: true, gap: "small", children: [_jsx(PhiTypographyControl, { strong: true, children: presentation.emptyState?.title ?? labels.emptyTitle }), _jsx(PhiTypographyControl, { type: "secondary", children: presentation.emptyState?.description ?? labels.emptyDescription })] })), layout: presentation.layout, footer: footer, summary: tableSummary })] }));
916
+ : undefined, loading: effectiveLoading, bordered: presentation.borders, striped: presentation.row?.striped, showHeader: presentation.header, stickyHeader: presentation.stickyHeader !== false, size: resolvedControlSize, emptyText: (_jsxs(PhiFlexControl, { vertical: true, gap: "small", children: [_jsx(PhiTypographyControl, { strong: true, children: presentation.emptyState?.title ?? labels.emptyTitle }), _jsx(PhiTypographyControl, { type: "secondary", children: presentation.emptyState?.description ?? labels.emptyDescription })] })), layout: presentation.layout, footer: footer, summary: tableSummary })] }));
917
917
  }
918
918
  function TableAction({ action, disabled = false, disabledReason, templateValue, size, onActivate }) {
919
919
  const icon = action.icon ? resolvePhiButtonIcon(action.icon) : null;
@@ -1217,6 +1217,7 @@ export declare const PHI_TABLE_WIDGET_DEFINITION: {
1217
1217
  };
1218
1218
  borders: false;
1219
1219
  header: true;
1220
+ stickyHeader: true;
1220
1221
  row: {
1221
1222
  striped: false;
1222
1223
  };
@@ -537,6 +537,7 @@ export function parsePhiTableWidgetConfig(config) {
537
537
  },
538
538
  borders: readBoolean(presentation.borders),
539
539
  header: readBoolean(presentation.header),
540
+ stickyHeader: readBoolean(presentation.stickyHeader),
540
541
  footer: readTableFooter(presentation.footer),
541
542
  summary: readTableSummary(presentation.summary),
542
543
  row: {
@@ -1153,6 +1154,7 @@ export const PHI_TABLE_WIDGET_DEFINITION = {
1153
1154
  { ...PHI_CONTROL_SIZE_FIELD, key: "presentation.controlSize" },
1154
1155
  { key: "presentation.borders", type: "boolean", label: "Borders" },
1155
1156
  { key: "presentation.header", type: "boolean", label: "Header" },
1157
+ { key: "presentation.stickyHeader", type: "boolean", label: "Sticky Header" },
1156
1158
  { key: "presentation.row.striped", type: "boolean", label: "Striped Rows" },
1157
1159
  ...buildPhiTableConditionExpressionConfigFields("presentation.row.mutedWhen", "Muted Row Conditions", "any", "No muted rows"),
1158
1160
  ],
@@ -1162,6 +1164,7 @@ export const PHI_TABLE_WIDGET_DEFINITION = {
1162
1164
  layout: { mode: "auto", overflowX: "auto" },
1163
1165
  borders: false,
1164
1166
  header: true,
1167
+ stickyHeader: true,
1165
1168
  row: { striped: false },
1166
1169
  },
1167
1170
  features: {
@@ -4,8 +4,7 @@ import { PhiCmsFlags, PhiCmsRegionType } from "../../constants/phi-cms";
4
4
  import { resolvePhiCmsWidgetPluginKey } from "../../constants/cms-widget-types";
5
5
  import { buildPhiCmsLayoutNamespacedTypeKey } from "../../constants/cms-layout-types";
6
6
  import { resolvePhiCmsLayoutPluginKey } from "../../constants/cms-layout-type-keys";
7
- import { trGlobal } from "../../server-helpers/translate";
8
- import { PHI_TR_CTX_WEB_UI_LABEL } from "../../gateway/tr";
7
+ import { PHI_TEXT_SOURCE_PRESET } from "../../helpers/text-source";
9
8
  import { PHI_SPACE } from "../../theme/antd-css-var-contract";
10
9
  import { buildPhiCmsWidgetNamespacedTypeKey } from "../../constants/cms-widget-types";
11
10
  import { createPhiCmsPresetNodes } from "../../helpers/cms-preset-nodes";
@@ -44,7 +43,12 @@ export async function buildPhiDefaultSiteAreaPresetTree({ page, runtime, presetK
44
43
  const footerTopRegion = PHI_DEFAULT_PUB_AREA_PRESET.regions.find((region) => region.key === "footer_top");
45
44
  const footerBottomRegion = PHI_DEFAULT_PUB_AREA_PRESET.regions.find((region) => region.key === "footer_bottom");
46
45
  const { label: contactLabel, href: contactHref, icon: contactIcon, } = resolvePhiBrandContact(runtime);
47
- const defaultQuickLinksTitle = await trGlobal("Quick Links", 0, PHI_TR_CTX_WEB_UI_LABEL);
46
+ /*
47
+ * The Preset's words stay in the source language and say they are the Preset's; the Widgets translate
48
+ * them through the global store (`helpers/text-source.ts`). The contact label is the Preset's only
49
+ * while the Site's theme names none.
50
+ */
51
+ const contactLabelIsPreset = !runtime.site.theme?.contact?.label?.trim();
48
52
  /*
49
53
  * A template rather than a finished sentence, and left untranslated here.
50
54
  *
@@ -477,6 +481,7 @@ export async function buildPhiDefaultSiteAreaPresetTree({ page, runtime, presetK
477
481
  label: "pub header top right",
478
482
  config: {
479
483
  text: contactLabel,
484
+ ...(contactLabelIsPreset ? { textSource: PHI_TEXT_SOURCE_PRESET } : {}),
480
485
  href: contactHref,
481
486
  icon: contactIcon,
482
487
  },
@@ -581,7 +586,8 @@ export async function buildPhiDefaultSiteAreaPresetTree({ page, runtime, presetK
581
586
  visibilityMask: page.visibilityMask,
582
587
  label: "pub footer main quick links",
583
588
  config: {
584
- title: defaultQuickLinksTitle,
589
+ title: "Quick Links",
590
+ textSource: PHI_TEXT_SOURCE_PRESET,
585
591
  navKey: `${navigationArea}:footer`,
586
592
  columns: 2,
587
593
  separator: true,
@@ -601,6 +607,7 @@ export async function buildPhiDefaultSiteAreaPresetTree({ page, runtime, presetK
601
607
  label: "pub footer main right",
602
608
  config: {
603
609
  text: contactLabel,
610
+ ...(contactLabelIsPreset ? { textSource: PHI_TEXT_SOURCE_PRESET } : {}),
604
611
  href: contactHref,
605
612
  icon: contactIcon,
606
613
  },
@@ -623,6 +630,7 @@ export async function buildPhiDefaultSiteAreaPresetTree({ page, runtime, presetK
623
630
  label: "pub footer bottom text",
624
631
  config: {
625
632
  text: footerBottomText,
633
+ textSource: PHI_TEXT_SOURCE_PRESET,
626
634
  tone: "secondary",
627
635
  },
628
636
  contentVersionId: null,
@@ -1,3 +1,4 @@
1
+ import { PHI_TEXT_SOURCE_PRESET } from "../../../../helpers/text-source";
1
2
  import { createPhiPresetCmsInstanceIdMap } from "../../../../types/cms-instance-id";
2
3
  import { PHI_SPACE } from "../../../../theme/antd-css-var-contract";
3
4
  import { PHI_PUBLIC_RUNTIME_MODULE_ID } from "../ids";
@@ -73,6 +74,7 @@ export async function buildPhiDefaultPubContactPageTree({ page, }) {
73
74
  sortOrder: 0,
74
75
  label: "pub contact description widget",
75
76
  config: {
77
+ textSource: PHI_TEXT_SOURCE_PRESET,
76
78
  eyebrow: "Contact",
77
79
  title: "Get in touch",
78
80
  description: "Send us your question, project request, or product inquiry and we will get back to you.",
@@ -1,3 +1,4 @@
1
+ import { PHI_TEXT_SOURCE_PRESET } from "../../../../helpers/text-source";
1
2
  import { createPhiPresetCmsInstanceIdMap } from "../../../../types/cms-instance-id";
2
3
  import { PHI_PUBLIC_RUNTIME_MODULE_ID } from "../ids";
3
4
  import { PhiCmsPageType } from "../../../../constants/phi-cms";
@@ -58,6 +59,7 @@ export async function buildPhiDefaultPubUnsubscribePageTree({ page, presetKey, }
58
59
  sortOrder: 0,
59
60
  label: "pub unsubscribe intro widget",
60
61
  config: {
62
+ textSource: PHI_TEXT_SOURCE_PRESET,
61
63
  title: "Stop receiving updates",
62
64
  description: "Press the button below and this site will send you no further updates.",
63
65
  visibleWhen: withToken,
@@ -75,6 +77,7 @@ export async function buildPhiDefaultPubUnsubscribePageTree({ page, presetKey, }
75
77
  sortOrder: 0,
76
78
  label: "pub unsubscribe incomplete widget",
77
79
  config: {
80
+ textSource: PHI_TEXT_SOURCE_PRESET,
78
81
  title: "This link is incomplete",
79
82
  description: "Open the unsubscribe link from the message itself, or use the most recent one you received.",
80
83
  tone: "secondary",
@@ -1,3 +1,4 @@
1
+ import { PHI_TEXT_SOURCE_PRESET } from "../../../../helpers/text-source";
1
2
  import { createPhiPresetCmsInstanceIdMap } from "../../../../types/cms-instance-id";
2
3
  import { PHI_PUBLIC_RUNTIME_MODULE_ID } from "../ids";
3
4
  import { PHI_PUBLIC_BASE_PAGE_REFERENCES } from "../../public-base-page-references";
@@ -125,7 +126,7 @@ export async function buildPhiDefaultPubWelcomePageTree({ page, runtime, }) {
125
126
  sortOrder: 0,
126
127
  label: "pub welcome greeting",
127
128
  // English source, like every Preset sentence: the Site translator makes it "Herzlich willkommen".
128
- config: { text: "A warm welcome" },
129
+ config: { textSource: PHI_TEXT_SOURCE_PRESET, text: "A warm welcome" },
129
130
  }),
130
131
  nodes.widget({
131
132
  typeKey: "markdown",
@@ -265,6 +265,8 @@ export async function buildPhiDefaultBuilderRevisionsPageTree({ page, runtime, }
265
265
  */
266
266
  nodes.widget({
267
267
  typeKey: "simple-text",
268
+ /* Label set text, translated already; a second pass would hand the translator a non-source text. */
269
+ flags: PhiCmsFlags.NoTranslate,
268
270
  id: PHI_REVISIONS_DELETE_AREA_WIDGET_IDS.deleteAreaWarning,
269
271
  parentLayoutNodeId: PHI_REVISIONS_DELETE_AREA_LAYOUT_IDS.deleteAreaBody,
270
272
  slotIndex: PHI_CMS_DEFAULT_SLOT_INDEX,
@@ -279,6 +281,8 @@ export async function buildPhiDefaultBuilderRevisionsPageTree({ page, runtime, }
279
281
  }),
280
282
  nodes.widget({
281
283
  typeKey: "simple-text",
284
+ /* Label set text, translated already; a second pass would hand the translator a non-source text. */
285
+ flags: PhiCmsFlags.NoTranslate,
282
286
  id: PHI_REVISIONS_DELETE_AREA_WIDGET_IDS.deleteAreaSurvives,
283
287
  parentLayoutNodeId: PHI_REVISIONS_DELETE_AREA_LAYOUT_IDS.deleteAreaBody,
284
288
  slotIndex: PHI_CMS_SEQUENTIAL_LAYOUT_SLOTS[1].slotIndex,
@@ -1,10 +1,19 @@
1
1
  import "server-only";
2
2
  import { type PhiTranslationParams } from "../helpers/translation-format";
3
3
  import { PHI_TR_CTX_WEB_UI_LABEL, type PhiTranslationFormat } from "../gateway/tr";
4
+ import { type PhiTextSource } from "../helpers/text-source";
4
5
  export { PHI_TR_CTX_WEB_UI_LABEL };
5
6
  export type { PhiTranslationParams } from "../helpers/translation-format";
6
7
  export declare function tr(msg: string, params?: PhiTranslationParams, ctx?: string | number, format?: PhiTranslationFormat): Promise<string>;
7
8
  export declare function trGlobal(msg: string, params?: PhiTranslationParams, ctx?: string | number, format?: PhiTranslationFormat): Promise<string>;
9
+ /**
10
+ * A text Widget's own words in the render's locale, from the store their source names.
11
+ *
12
+ * `textSource: "preset"` is a Preset's sentence and the platform's, so the global store answers it;
13
+ * anything else an author wrote and the Site's store does (`helpers/text-source.ts`). The locale is the
14
+ * render's where there is one: a static render has no request to read it from.
15
+ */
16
+ export declare function trWidgetText(locale: string | undefined, msg: string, textSource: PhiTextSource | undefined): Promise<string>;
8
17
  export declare function trForLocale(localeInput: string, msg: string, params?: PhiTranslationParams, ctx?: string | number, format?: PhiTranslationFormat,
9
18
  /** The language `msg` is written in, when it is not the Site's own -- a Module's text. */
10
19
  sourceLocale?: string): Promise<string>;
@@ -6,6 +6,7 @@ import { PHI_TR_CTX_WEB_UI_LABEL, trBulk as requestInternalTranslationBulk, tr a
6
6
  import { getPhiRequestRuntime } from "./request-runtime";
7
7
  import { resolvePhiRequestLocale } from "./request-locale";
8
8
  import { readPhiServerApiCredentials } from "../helpers/phis-server-credentials";
9
+ import { PHI_TEXT_SOURCE_PRESET } from "../helpers/text-source";
9
10
  export { PHI_TR_CTX_WEB_UI_LABEL };
10
11
  function buildTranslatorOptions(locale, sourceLocale) {
11
12
  const requestRuntime = getPhiRequestRuntime();
@@ -46,6 +47,19 @@ export async function trGlobal(msg, params = 0, ctx = 0, format = "text") {
46
47
  const locale = await resolvePhiRequestLocale();
47
48
  return trGlobalForLocale(locale, msg, params, ctx, format);
48
49
  }
50
+ /**
51
+ * A text Widget's own words in the render's locale, from the store their source names.
52
+ *
53
+ * `textSource: "preset"` is a Preset's sentence and the platform's, so the global store answers it;
54
+ * anything else an author wrote and the Site's store does (`helpers/text-source.ts`). The locale is the
55
+ * render's where there is one: a static render has no request to read it from.
56
+ */
57
+ export async function trWidgetText(locale, msg, textSource) {
58
+ if (textSource === PHI_TEXT_SOURCE_PRESET) {
59
+ return locale ? trGlobalForLocale(locale, msg) : trGlobal(msg);
60
+ }
61
+ return locale ? trForLocale(locale, msg) : tr(msg);
62
+ }
49
63
  /**
50
64
  * Trimmed, one per message and in order: an empty message stays in its place, and `trBulk` answers it
51
65
  * with an empty text, so every answer lines up with the message it belongs to.
@@ -492,10 +492,24 @@ h5.phi-typography, .phi-typography h5 {
492
492
  * as a Table closed with one instead of with a footer. Clipping the Table is what both cases want, which
493
493
  * is why the rule names neither.
494
494
  */
495
+ /*
496
+ * A sticky header row sticks under the Shell's Header, which publishes where it ends right now
497
+ * (`components/shell/use-phi-shell-sticky-top.ts`), and at the top edge of an Overlay's body,
498
+ * which scrolls on its own. Ant Design writes the offset inline, so the position is stated over it.
499
+ */
500
+ .phi-table-control.phi-table-control--sticky-header [class~="ant-table-sticky-holder"] {
501
+ top: var(--phi-table-sticky-top, var(--phi-shell-sticky-top, 0px)) !important;
502
+ }
503
+
504
+ :is([data-phi-modal-body-content], [data-phi-drawer-body-content]) {
505
+ --phi-table-sticky-top: 0px;
506
+ }
507
+
495
508
  .phi-table-control.phi-table-control--closing-block [class~="ant-table"] {
496
509
  border-end-start-radius: var(--phi-surface-radius, var(--ant-border-radius-lg));
497
510
  border-end-end-radius: var(--phi-surface-radius, var(--ant-border-radius-lg));
498
- overflow: hidden;
511
+ /* `clip`, not `hidden`, so the corners are cut without making this the sticky header's scroller. */
512
+ overflow: clip;
499
513
  }
500
514
 
501
515
  /*
@@ -584,3 +598,29 @@ h5.phi-typography, .phi-typography h5 {
584
598
  .phi-plain-table tbody tr:last-child td {
585
599
  border-block-end: none;
586
600
  }
601
+
602
+ /*
603
+ * Markdown pictures the text flows past (`imageFloat` on the Markdown Widget).
604
+ *
605
+ * Only where there is room for both: below 560px a picture beside the words leaves each of them a column
606
+ * nobody reads, so there the picture stands on its own line at full width, as an unfloated one does. The
607
+ * width is a share of the text with a ceiling, so a wide column does not grow the picture past a glance.
608
+ */
609
+ .phi-markdown-body :is(img) {
610
+ display: block;
611
+ margin-block-end: var(--ant-margin, 16px);
612
+ }
613
+
614
+ @container (min-width: 560px) {
615
+ .phi-markdown-body--float-start :is(img) {
616
+ float: inline-start;
617
+ width: min(45%, 440px);
618
+ margin-inline-end: var(--ant-margin-lg, 24px);
619
+ }
620
+
621
+ .phi-markdown-body--float-end :is(img) {
622
+ float: inline-end;
623
+ width: min(45%, 440px);
624
+ margin-inline-start: var(--ant-margin-lg, 24px);
625
+ }
626
+ }