@phis/ui 0.4.0 → 0.4.1
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.
- package/components/controls/phi-table-control.d.ts +6 -1
- package/components/controls/phi-table-control.js +16 -4
- package/components/layouts/layout-definitions.d.ts +2 -2
- package/components/layouts/layout-definitions.js +6 -1
- package/components/shell/use-phi-shell-sticky-top.d.ts +5 -0
- package/components/shell/use-phi-shell-sticky-top.js +78 -0
- package/components/widgets/helpers/semantic-html-translation.d.ts +3 -1
- package/components/widgets/helpers/semantic-html-translation.js +8 -3
- package/components/widgets/navigation-descriptor-resolver.server.js +6 -1
- package/components/widgets/shared/markdown-render.js +15 -5
- package/helpers/text-source.d.ts +19 -0
- package/helpers/text-source.js +20 -0
- package/next/config.mjs +1 -1
- package/package.json +1 -1
- package/plugins/runtime-modules/auth/trees/phi-default-pub-confirm-page-tree.js +3 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-login-page-tree.js +2 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-registration-page-tree.js +3 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-reset-password-page-tree.js +3 -0
- package/plugins/runtime-modules/builder/clients/widget-inspector.js +9 -0
- package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +4 -0
- package/plugins/runtime-modules/core/widgets/card/server.js +4 -2
- package/plugins/runtime-modules/core/widgets/description/config.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/description/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/description/server.js +10 -12
- package/plugins/runtime-modules/core/widgets/entry-list/server.js +2 -2
- package/plugins/runtime-modules/core/widgets/html/server.js +1 -1
- package/plugins/runtime-modules/core/widgets/markdown-toc/server.js +7 -2
- package/plugins/runtime-modules/core/widgets/quick-links/config.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/quick-links/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/quick-links/plugin.js +4 -2
- package/plugins/runtime-modules/core/widgets/quick-links/server.d.ts +5 -1
- package/plugins/runtime-modules/core/widgets/quick-links/server.js +4 -2
- package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/simple-text/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/simple-text/plugin.js +1 -1
- package/plugins/runtime-modules/core/widgets/simple-text/server.d.ts +4 -1
- package/plugins/runtime-modules/core/widgets/simple-text/server.js +3 -3
- package/plugins/runtime-modules/core/widgets/table/client.js +1 -1
- package/plugins/runtime-modules/core/widgets/table/config.d.ts +1 -0
- package/plugins/runtime-modules/core/widgets/table/config.js +3 -0
- package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +12 -4
- package/plugins/runtime-modules/public/trees/phi-default-pub-contact-page-tree.js +2 -0
- package/plugins/runtime-modules/public/trees/phi-default-pub-unsubscribe-page-tree.js +3 -0
- package/plugins/runtime-modules/public/trees/phi-default-pub-welcome-page-tree.js +2 -1
- package/plugins/runtime-modules/revisions/trees/phi-default-builder-revisions-page-tree.js +4 -0
- package/server-helpers/translate.d.ts +9 -0
- package/server-helpers/translate.js +14 -0
- package/styles/controls.css +15 -1
- package/styles/root.css +20 -19
- package/types/core-widget-placements.d.ts +5 -0
- package/types/table-widget.d.ts +2 -0
|
@@ -122,6 +122,11 @@ export type PhiTableControlProps<TRow extends Record<string, unknown>> = {
|
|
|
122
122
|
bordered?: boolean;
|
|
123
123
|
striped?: boolean;
|
|
124
124
|
showHeader?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* The header row stays at the top while the rows scroll under it: under the Shell's Header on a page,
|
|
127
|
+
* at the top edge of a Modal's or Drawer's body. `styles/controls.css` places it.
|
|
128
|
+
*/
|
|
129
|
+
stickyHeader?: boolean;
|
|
125
130
|
footer?: PhiTableControlFooter;
|
|
126
131
|
summary?: PhiTableControlSummary;
|
|
127
132
|
size?: PhiControlSize;
|
|
@@ -141,4 +146,4 @@ export type PhiTableControlProps<TRow extends Record<string, unknown>> = {
|
|
|
141
146
|
onExternalDrop?: (event: DragEvent<HTMLDivElement>) => void;
|
|
142
147
|
};
|
|
143
148
|
export declare function readPhiTableControlValue(row: Record<string, unknown>, path: string): unknown;
|
|
144
|
-
export declare function PhiTableControl<TRow extends Record<string, unknown>>({ rows, rowIdentityPath, columns, columnOrder, sortingMode, sorts, onSortsChange, columnReordering, onColumnOrderChange, rowSelection, pagination, tree, rowReordering, editing, loading, bordered, striped, showHeader, footer, summary, size, emptyText, layout, rowStyle, onRowActivate, onExternalRowDragOver, onExternalRowDrop, onExternalDragOver, onExternalDrop, }: PhiTableControlProps<TRow>): import("react").JSX.Element;
|
|
149
|
+
export declare function PhiTableControl<TRow extends Record<string, unknown>>({ rows, rowIdentityPath, columns, columnOrder, sortingMode, sorts, onSortsChange, columnReordering, onColumnOrderChange, rowSelection, pagination, tree, rowReordering, editing, loading, bordered, striped, stickyHeader, showHeader, footer, summary, size, emptyText, layout, rowStyle, onRowActivate, onExternalRowDragOver, onExternalRowDrop, onExternalDragOver, onExternalDrop, }: PhiTableControlProps<TRow>): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { usePhiShellStickyTop } from "../shell/use-phi-shell-sticky-top";
|
|
3
4
|
import { Button, Flex, Table } from "antd";
|
|
4
5
|
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
5
6
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable";
|
|
@@ -354,7 +355,7 @@ function buildRowMove(rows, rowIdentityPath, parentRowIdentityPath, movedRowIden
|
|
|
354
355
|
afterRowIdentity: placement === "after" ? targetRowIdentity : null,
|
|
355
356
|
};
|
|
356
357
|
}
|
|
357
|
-
export function PhiTableControl({ rows, rowIdentityPath, columns, columnOrder, sortingMode, sorts, onSortsChange, columnReordering = false, onColumnOrderChange, rowSelection, pagination = false, tree, rowReordering, editing, loading = false, bordered, striped = false, showHeader, footer, summary, size, emptyText, layout, rowStyle, onRowActivate, onExternalRowDragOver, onExternalRowDrop, onExternalDragOver, onExternalDrop, }) {
|
|
358
|
+
export function PhiTableControl({ rows, rowIdentityPath, columns, columnOrder, sortingMode, sorts, onSortsChange, columnReordering = false, onColumnOrderChange, rowSelection, pagination = false, tree, rowReordering, editing, loading = false, bordered, striped = false, stickyHeader = false, showHeader, footer, summary, size, emptyText, layout, rowStyle, onRowActivate, onExternalRowDragOver, onExternalRowDrop, onExternalDragOver, onExternalDrop, }) {
|
|
358
359
|
const { token } = usePhiConfig();
|
|
359
360
|
const [editingRowIdentityState, setEditingRowIdentity] = useState(null);
|
|
360
361
|
const [rowEditPatch, setRowEditPatch] = useState({});
|
|
@@ -988,6 +989,8 @@ export function PhiTableControl({ rows, rowIdentityPath, columns, columnOrder, s
|
|
|
988
989
|
const hasClosingBlock = !loading &&
|
|
989
990
|
(Boolean(footer) || (summary != null && summary.placement !== "sticky-top"));
|
|
990
991
|
const frameRef = useRef(null);
|
|
992
|
+
// Where the Shell's Header ends, which is where a sticky header row stops (`styles/controls.css`).
|
|
993
|
+
usePhiShellStickyTop({ active: stickyHeader && showHeader !== false, elementRef: frameRef });
|
|
991
994
|
const [headerBox, setHeaderBox] = useState(null);
|
|
992
995
|
useEffect(() => {
|
|
993
996
|
const frame = frameRef.current;
|
|
@@ -1007,11 +1010,15 @@ export function PhiTableControl({ rows, rowIdentityPath, columns, columnOrder, s
|
|
|
1007
1010
|
observer.observe(frame);
|
|
1008
1011
|
return () => observer.disconnect();
|
|
1009
1012
|
}, [showHeader, loading]);
|
|
1010
|
-
const table = (_jsx(Table, { className: [
|
|
1013
|
+
const table = (_jsx(Table, { className: [
|
|
1014
|
+
"phi-table-control",
|
|
1015
|
+
hasClosingBlock ? "phi-table-control--closing-block" : null,
|
|
1016
|
+
stickyHeader && showHeader !== false ? "phi-table-control--sticky-header" : null,
|
|
1017
|
+
]
|
|
1011
1018
|
.filter(Boolean)
|
|
1012
1019
|
.join(" "), style: { "--phi-table-striped-row-background": token.colorFillAlter }, rowKey: (row) => loading
|
|
1013
1020
|
? `__phi_table_loading_${String(row[PHI_TABLE_LOADING_ROW])}`
|
|
1014
|
-
: String(readIdentity(row, rowIdentityPath)), columns: renderedColumns, dataSource: loading ? loadingRows : dataSource, bordered: bordered, showHeader: showHeader, tableLayout: layout.mode, size: size, rowClassName: (_row, index) => striped && index % 2 === 1 ? "phi-table-control__row--striped" : "", rowSelection: loading ? undefined : antdRowSelection, components: rowReordering?.enabled && !loading ? { body: { row: PhiTableSortableRow } } : undefined, scroll: layout.overflowX === "auto" ? { x: true } : undefined, pagination: pagination === false ? false : {
|
|
1021
|
+
: String(readIdentity(row, rowIdentityPath)), columns: renderedColumns, dataSource: loading ? loadingRows : dataSource, bordered: bordered, showHeader: showHeader, tableLayout: layout.mode, size: size, rowClassName: (_row, index) => striped && index % 2 === 1 ? "phi-table-control__row--striped" : "", rowSelection: loading ? undefined : antdRowSelection, components: rowReordering?.enabled && !loading ? { body: { row: PhiTableSortableRow } } : undefined, scroll: layout.overflowX === "auto" ? { x: true } : undefined, sticky: stickyHeader && showHeader !== false ? true : undefined, pagination: pagination === false ? false : {
|
|
1015
1022
|
...{ [PHI_VISUAL_ANCHOR_PART_ATTRIBUTE]: "pagination" },
|
|
1016
1023
|
current: pagination.page,
|
|
1017
1024
|
pageSize: pagination.pageSize,
|
|
@@ -1097,7 +1104,12 @@ export function PhiTableControl({ rows, rowIdentityPath, columns, columnOrder, s
|
|
|
1097
1104
|
insertionGapIndex,
|
|
1098
1105
|
insertionGapHeight: activeDragRowHeight,
|
|
1099
1106
|
}, children: table }) }) })) : table;
|
|
1100
|
-
|
|
1107
|
+
/*
|
|
1108
|
+
* Clipped, not hidden: `hidden` makes the frame a scroll container, and a sticky header row then sticks
|
|
1109
|
+
* to the frame's own top -- pushed down inside the Table by the Shell's Header height -- instead of to
|
|
1110
|
+
* the page. `clip` cuts off the same overflow and leaves the page the one that scrolls.
|
|
1111
|
+
*/
|
|
1112
|
+
const constrainedTable = (_jsxs("div", { ref: frameRef, style: { position: "relative", width: "100%", maxWidth: "100%", minWidth: 0, overflow: "clip" }, children: [renderedTable, headerBox ? (_jsx("div", { "aria-hidden": true, [PHI_VISUAL_ANCHOR_PART_ATTRIBUTE]: "header", style: { position: "absolute", left: 0, right: 0, top: headerBox.top, height: headerBox.height, pointerEvents: "none" } })) : null] }));
|
|
1101
1113
|
return onExternalDragOver || onExternalDrop ? (_jsx("div", { onDragOver: (event) => {
|
|
1102
1114
|
if (event.defaultPrevented) {
|
|
1103
1115
|
setExternalAppendActive(false);
|
|
@@ -483,7 +483,7 @@ export declare const PHI_GRID_LAYOUT_DEFINITION: {
|
|
|
483
483
|
kind: "layout";
|
|
484
484
|
pluginKey: string;
|
|
485
485
|
typeKey: string;
|
|
486
|
-
slotSizePolicy: "fill";
|
|
486
|
+
slotSizePolicy: "fill-inline";
|
|
487
487
|
title: string;
|
|
488
488
|
description: string;
|
|
489
489
|
category: "structure";
|
|
@@ -1176,7 +1176,7 @@ export declare const PHI_CORE_LAYOUT_DEFINITIONS: readonly [{
|
|
|
1176
1176
|
kind: "layout";
|
|
1177
1177
|
pluginKey: string;
|
|
1178
1178
|
typeKey: string;
|
|
1179
|
-
slotSizePolicy: "fill";
|
|
1179
|
+
slotSizePolicy: "fill-inline";
|
|
1180
1180
|
title: string;
|
|
1181
1181
|
description: string;
|
|
1182
1182
|
category: "structure";
|
|
@@ -415,7 +415,12 @@ export const PHI_GRID_LAYOUT_DEFINITION = {
|
|
|
415
415
|
kind: "layout",
|
|
416
416
|
pluginKey: resolvePhiCmsLayoutPluginKey("grid"),
|
|
417
417
|
typeKey: "grid",
|
|
418
|
-
|
|
418
|
+
/*
|
|
419
|
+
* As wide as its slot, as tall as its rows. A Grid is content in flow: filling the block axis as well
|
|
420
|
+
* let a vertical stack share its height out among the Grids in it, and each one, held to its share,
|
|
421
|
+
* spilled its rows over the next section.
|
|
422
|
+
*/
|
|
423
|
+
slotSizePolicy: "fill-inline",
|
|
419
424
|
title: "Grid",
|
|
420
425
|
description: "Slots in rows, as many to a row as the Grid's own width allows.",
|
|
421
426
|
category: "structure",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* How far down the viewport the Shell's Header holds right now, published on the Shell.
|
|
5
|
+
*
|
|
6
|
+
* Something that sticks to the top of the page -- a Table's header row -- has to stick under the Header
|
|
7
|
+
* rather than behind it, and where the Header ends is not a constant: each band sticks or scrolls by its
|
|
8
|
+
* own config, and a revealing `header_top` slides away while the visitor reads down and comes back when
|
|
9
|
+
* they scroll up. So the edge is measured, not configured: the lowest bottom of every Header band that is
|
|
10
|
+
* stuck at this moment, written as `--phi-shell-sticky-top` on the Shell element. Unset, a reader falls
|
|
11
|
+
* back to `0px`, which is right for a Shell without a sticky Header.
|
|
12
|
+
*
|
|
13
|
+
* Run by whatever sticks under it -- every sticky Table does -- so a page without one loads nothing for
|
|
14
|
+
* it. Each runner measures all the bands and they all write the same value. Not inside an Overlay: a
|
|
15
|
+
* Modal's or Drawer's body scrolls on its own, and its top edge is the line.
|
|
16
|
+
*/
|
|
17
|
+
const PHI_SHELL_STICKY_TOP_PROPERTY = "--phi-shell-sticky-top";
|
|
18
|
+
const HEADER_BANDS = '[data-phi-region-key^="header_"]:not([hidden])';
|
|
19
|
+
function measurePhiShellStickyTop(shell) {
|
|
20
|
+
let bottom = 0;
|
|
21
|
+
for (const band of shell.querySelectorAll(HEADER_BANDS)) {
|
|
22
|
+
const style = window.getComputedStyle(band);
|
|
23
|
+
if (style.position !== "sticky" && style.position !== "fixed") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const rect = band.getBoundingClientRect();
|
|
27
|
+
const stuckAt = Number.parseFloat(style.top) || 0;
|
|
28
|
+
// Below its sticking line it is still in flow; above the viewport it has slid away.
|
|
29
|
+
if (rect.height === 0 || rect.top > stuckAt + 1 || rect.bottom <= 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
33
|
+
}
|
|
34
|
+
return `${Math.round(bottom)}px`;
|
|
35
|
+
}
|
|
36
|
+
const OVERLAY_BODIES = "[data-phi-modal-body-content], [data-phi-drawer-body-content]";
|
|
37
|
+
export function usePhiShellStickyTop({ active, elementRef, }) {
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const element = elementRef.current;
|
|
40
|
+
const shell = active && element && !element.closest(OVERLAY_BODIES)
|
|
41
|
+
? element.closest("[data-phi-shell-layout]")
|
|
42
|
+
: null;
|
|
43
|
+
if (!element || !shell) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let frame = 0;
|
|
47
|
+
const publish = () => {
|
|
48
|
+
frame = 0;
|
|
49
|
+
const next = measurePhiShellStickyTop(shell);
|
|
50
|
+
if (shell.style.getPropertyValue(PHI_SHELL_STICKY_TOP_PROPERTY) !== next) {
|
|
51
|
+
shell.style.setProperty(PHI_SHELL_STICKY_TOP_PROPERTY, next);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const schedule = () => {
|
|
55
|
+
if (frame === 0) {
|
|
56
|
+
frame = window.requestAnimationFrame(publish);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
publish();
|
|
60
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
61
|
+
window.addEventListener("resize", schedule);
|
|
62
|
+
// A revealing band moves by a transition; its end is where it settled.
|
|
63
|
+
shell.addEventListener("transitionend", schedule);
|
|
64
|
+
const observer = new ResizeObserver(schedule);
|
|
65
|
+
for (const band of shell.querySelectorAll(HEADER_BANDS)) {
|
|
66
|
+
observer.observe(band);
|
|
67
|
+
}
|
|
68
|
+
return () => {
|
|
69
|
+
if (frame !== 0) {
|
|
70
|
+
window.cancelAnimationFrame(frame);
|
|
71
|
+
}
|
|
72
|
+
window.removeEventListener("scroll", schedule);
|
|
73
|
+
window.removeEventListener("resize", schedule);
|
|
74
|
+
shell.removeEventListener("transitionend", schedule);
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
};
|
|
77
|
+
}, [active, elementRef]);
|
|
78
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
-
export declare function translateSemanticHtml(sourceHtml: string, sourceLocale?: string
|
|
2
|
+
export declare function translateSemanticHtml(sourceHtml: string, sourceLocale?: string,
|
|
3
|
+
/** The render's locale. Without it the request's is asked, and a static render has no request. */
|
|
4
|
+
locale?: string): Promise<string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import renderHtml from "dom-serializer";
|
|
3
3
|
import { parseDocument } from "htmlparser2";
|
|
4
|
-
import { trBulk } from "../../../server-helpers/translate";
|
|
4
|
+
import { trBulk, trBulkForLocale } from "../../../server-helpers/translate";
|
|
5
5
|
import { sanitizePhiHtmlWidgetMarkup } from "./html-content";
|
|
6
6
|
const SEMANTIC_HTML_BLOCKS = new Set([
|
|
7
7
|
"p",
|
|
@@ -49,7 +49,9 @@ function renderNodes(nodes) {
|
|
|
49
49
|
function hasVisibleText(html) {
|
|
50
50
|
return html.replace(/<[^>]+>/g, "").replace(/ /g, " ").trim().length > 0;
|
|
51
51
|
}
|
|
52
|
-
export async function translateSemanticHtml(sourceHtml, sourceLocale
|
|
52
|
+
export async function translateSemanticHtml(sourceHtml, sourceLocale,
|
|
53
|
+
/** The render's locale. Without it the request's is asked, and a static render has no request. */
|
|
54
|
+
locale) {
|
|
53
55
|
const sanitized = sanitizePhiHtmlWidgetMarkup(sourceHtml);
|
|
54
56
|
if (!sanitized) {
|
|
55
57
|
return "";
|
|
@@ -64,7 +66,10 @@ export async function translateSemanticHtml(sourceHtml, sourceLocale) {
|
|
|
64
66
|
if (units.length === 0) {
|
|
65
67
|
return sanitized;
|
|
66
68
|
}
|
|
67
|
-
const
|
|
69
|
+
const msgs = units.map((unit) => unit.html);
|
|
70
|
+
const translated = locale
|
|
71
|
+
? await trBulkForLocale(locale, msgs, 0, "html", sourceLocale)
|
|
72
|
+
: await trBulk(msgs, 0, "html", sourceLocale);
|
|
68
73
|
units.forEach((unit, index) => {
|
|
69
74
|
const fragment = parseDocument(translated[index] ?? unit.html);
|
|
70
75
|
unit.node.children = fragment.children ?? [];
|
|
@@ -127,6 +127,11 @@ export async function resolvePhiDescriptorNavigationItems(runtime, navKey, overl
|
|
|
127
127
|
/*
|
|
128
128
|
* One batch per source language. A Module's label names its language and is translated from it; an
|
|
129
129
|
* operator's names none and is read in the Site's source language, which the server supplies.
|
|
130
|
+
*
|
|
131
|
+
* The batch also decides the store. A Module's label is the platform's text, the same on every Site,
|
|
132
|
+
* so it is translated once in the global store, which only the operator edits. A Site that wants
|
|
133
|
+
* other words renames the entry, the label loses its `sourceLocale`, and from then on it is the
|
|
134
|
+
* Site's own text in the Site's store. The Builder's surfaces carry Module labels only.
|
|
130
135
|
*/
|
|
131
136
|
await Promise.all([...collectNavigationLabels(surface.items)].map(async ([sourceLocale, group]) => {
|
|
132
137
|
const labels = [...group];
|
|
@@ -136,7 +141,7 @@ export async function resolvePhiDescriptorNavigationItems(runtime, navKey, overl
|
|
|
136
141
|
locale: runtime.locale.current,
|
|
137
142
|
...(sourceLocale ? { sourceLocale } : {}),
|
|
138
143
|
};
|
|
139
|
-
const translator = runtime.area === "builder"
|
|
144
|
+
const translator = sourceLocale || runtime.area === "builder"
|
|
140
145
|
? createGlobalTranslator(options)
|
|
141
146
|
: createSiteTranslator({ ...options, siteKey: rt.siteKey });
|
|
142
147
|
/*
|
|
@@ -2,7 +2,7 @@ import { cache } from "react";
|
|
|
2
2
|
import { unified } from "unified";
|
|
3
3
|
import remarkParse from "remark-parse";
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
|
-
import { trBulk } from "../../../server-helpers/translate";
|
|
5
|
+
import { trBulk, trBulkForLocale } from "../../../server-helpers/translate";
|
|
6
6
|
import { resolvePhiCodeLanguage, tokenizePhiCode } from "./markdown-code-tokens";
|
|
7
7
|
import { readPhiInternalReference } from "../../../types/references";
|
|
8
8
|
import { resolvePhiWidgetInternalReferences } from "../helpers/internal-reference-resolver.server";
|
|
@@ -294,11 +294,21 @@ function parseTranslatedMarkdownInlines(translatedHtml, metadata) {
|
|
|
294
294
|
appendText(translatedHtml.slice(cursor));
|
|
295
295
|
return stack.length === 1 ? stack[0]?.children ?? [] : null;
|
|
296
296
|
}
|
|
297
|
-
|
|
297
|
+
/*
|
|
298
|
+
* The render's own locale where there is one. A static render has no request to ask: `trBulk` reads the
|
|
299
|
+
* locale from the request headers, finds none, and answers in the Site's default -- which left every
|
|
300
|
+
* static Public page's body in its source language under a translated header.
|
|
301
|
+
*/
|
|
302
|
+
function translateUnits(locale, msgs, format, sourceLocale) {
|
|
303
|
+
return locale
|
|
304
|
+
? trBulkForLocale(locale, msgs, 0, format, sourceLocale)
|
|
305
|
+
: trBulk(msgs, 0, format, sourceLocale);
|
|
306
|
+
}
|
|
307
|
+
async function buildMarkdownInlineMap(tree, translate, sourceLocale, locale) {
|
|
298
308
|
const units = [];
|
|
299
309
|
collectMarkdownTranslationUnits(tree, units);
|
|
300
310
|
const translated = translate && units.length > 0
|
|
301
|
-
? await
|
|
311
|
+
? await translateUnits(locale, units.map((unit) => unit.html), "html", sourceLocale)
|
|
302
312
|
: units.map((unit) => unit.html);
|
|
303
313
|
/*
|
|
304
314
|
* Image titles are prose and translate as plain text, each on its own: they are separate sentences
|
|
@@ -307,7 +317,7 @@ async function buildMarkdownInlineMap(tree, translate, sourceLocale) {
|
|
|
307
317
|
*/
|
|
308
318
|
const titleSlots = units.flatMap((unit) => unit.metadata.titles.flatMap((title, index) => title ? [{ metadata: unit.metadata, index, title }] : []));
|
|
309
319
|
if (translate && titleSlots.length > 0) {
|
|
310
|
-
const translatedTitles = await
|
|
320
|
+
const translatedTitles = await translateUnits(locale, titleSlots.map((slot) => slot.title), "text", sourceLocale);
|
|
311
321
|
titleSlots.forEach((slot, index) => {
|
|
312
322
|
slot.metadata.titles[slot.index] = translatedTitles[index] || slot.title;
|
|
313
323
|
});
|
|
@@ -508,7 +518,7 @@ async function resolveMarkdownRenderDataFromSource(markdown, translate, headingI
|
|
|
508
518
|
headingIdPrefix,
|
|
509
519
|
headingOffset,
|
|
510
520
|
};
|
|
511
|
-
const inlineMap = await buildMarkdownInlineMap(tree, translate, sourceLocale);
|
|
521
|
+
const inlineMap = await buildMarkdownInlineMap(tree, translate, sourceLocale, runtime?.locale.current);
|
|
512
522
|
const blocks = mapBlockNodes(tree.children, context, inlineMap);
|
|
513
523
|
return { blocks, headings: context.headings };
|
|
514
524
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose words a text Widget holds, which decides the store that translates them.
|
|
3
|
+
*
|
|
4
|
+
* A Preset's text is the platform's: the same sentence on every installation, translated once in the
|
|
5
|
+
* global store, which only the operator edits. Text an author wrote on a Site is the Site's and goes to
|
|
6
|
+
* the Site's store. The Preset says so by writing `textSource: "preset"` beside its words; the Builder
|
|
7
|
+
* drops the marker the moment an author changes one of the Widget's texts, and from then on all of them
|
|
8
|
+
* are the Site's.
|
|
9
|
+
*
|
|
10
|
+
* A marker rather than a translation done by the Preset itself: adopting a Preset stores the tree it
|
|
11
|
+
* built, so a Preset that translated its words would freeze one locale into the Site's copy.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PHI_TEXT_SOURCE_PRESET: "preset";
|
|
14
|
+
export type PhiTextSource = typeof PHI_TEXT_SOURCE_PRESET;
|
|
15
|
+
export declare function readPhiTextSource(value: unknown): PhiTextSource | undefined;
|
|
16
|
+
/** Whether a Builder edit of this field ends the Preset's ownership of the Widget's words. */
|
|
17
|
+
export declare function isPhiAuthoredTextField(field: {
|
|
18
|
+
type?: string;
|
|
19
|
+
}): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose words a text Widget holds, which decides the store that translates them.
|
|
3
|
+
*
|
|
4
|
+
* A Preset's text is the platform's: the same sentence on every installation, translated once in the
|
|
5
|
+
* global store, which only the operator edits. Text an author wrote on a Site is the Site's and goes to
|
|
6
|
+
* the Site's store. The Preset says so by writing `textSource: "preset"` beside its words; the Builder
|
|
7
|
+
* drops the marker the moment an author changes one of the Widget's texts, and from then on all of them
|
|
8
|
+
* are the Site's.
|
|
9
|
+
*
|
|
10
|
+
* A marker rather than a translation done by the Preset itself: adopting a Preset stores the tree it
|
|
11
|
+
* built, so a Preset that translated its words would freeze one locale into the Site's copy.
|
|
12
|
+
*/
|
|
13
|
+
export const PHI_TEXT_SOURCE_PRESET = "preset";
|
|
14
|
+
export function readPhiTextSource(value) {
|
|
15
|
+
return value === PHI_TEXT_SOURCE_PRESET ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Whether a Builder edit of this field ends the Preset's ownership of the Widget's words. */
|
|
18
|
+
export function isPhiAuthoredTextField(field) {
|
|
19
|
+
return field.type === "string";
|
|
20
|
+
}
|
package/next/config.mjs
CHANGED
|
@@ -57,7 +57,7 @@ function customRenders(antd, h, Fragment) {
|
|
|
57
57
|
Form: (Form) => h(Form, null, h(Form.Item, { label: "Label" }, h(antd.Input))),
|
|
58
58
|
Dropdown: (Dropdown) => h(Dropdown, { menu: { items: [] } }, h("div")),
|
|
59
59
|
Menu: (Menu) => h(Menu, { items: [] }),
|
|
60
|
-
QRCode: (QRCode) => h(QRCode, { value: "https://
|
|
60
|
+
QRCode: (QRCode) => h(QRCode, { value: "https://phis.dev" }),
|
|
61
61
|
Tree: (Tree) => h(Tree, { treeData: [] }),
|
|
62
62
|
Tag: (Tag) => h(Fragment, null, h(Tag, { color: "blue" }, "Tag"), h(Tag, { color: "success" }, "Tag")),
|
|
63
63
|
Badge: (Badge) => h(Fragment, null, h(Badge), h(Badge.Ribbon)),
|
package/package.json
CHANGED
|
@@ -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_AUTH_RUNTIME_MODULE_ID } from "../ids";
|
|
@@ -97,6 +98,7 @@ export async function buildPhiDefaultPubConfirmPageTree({ page, presetKey, }) {
|
|
|
97
98
|
sortOrder: 0,
|
|
98
99
|
label: "pub confirmation description widget",
|
|
99
100
|
config: {
|
|
101
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
100
102
|
eyebrow: "Confirm Email",
|
|
101
103
|
title: "Activate your account",
|
|
102
104
|
description: "Review your registration details and confirm your email address to activate access.",
|
|
@@ -160,6 +162,7 @@ export async function buildPhiDefaultPubConfirmPageTree({ page, presetKey, }) {
|
|
|
160
162
|
slotIndex: 2,
|
|
161
163
|
label: "pub confirmation missing token",
|
|
162
164
|
config: {
|
|
165
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
163
166
|
text: "No confirmation token was provided. Open the confirmation link from your email.",
|
|
164
167
|
tone: "secondary",
|
|
165
168
|
visibleWhen: withoutToken,
|
|
@@ -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_LAYOUT } from "../../../../theme/phi-tokens";
|
|
@@ -98,6 +99,7 @@ export async function buildPhiDefaultPubLoginPageTree({ page, presetKey, runtime
|
|
|
98
99
|
sortOrder: 0,
|
|
99
100
|
label: "pub login description widget",
|
|
100
101
|
config: {
|
|
102
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
101
103
|
eyebrow: "Login",
|
|
102
104
|
title: "Sign in to your account",
|
|
103
105
|
description: "Access your customer workspace, orders, and account details.",
|
|
@@ -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_AUTH_RUNTIME_MODULE_ID } from "../ids";
|
|
@@ -68,6 +69,7 @@ export async function buildPhiDefaultPubRegistrationPageTree({ page, presetKey,
|
|
|
68
69
|
sortOrder: 0,
|
|
69
70
|
label: "pub registration description widget",
|
|
70
71
|
config: {
|
|
72
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
71
73
|
eyebrow: "Register",
|
|
72
74
|
title: "Create account",
|
|
73
75
|
description: "Create your account to access your personalized workspace.",
|
|
@@ -113,6 +115,7 @@ export async function buildPhiDefaultPubRegistrationPageTree({ page, presetKey,
|
|
|
113
115
|
slotIndex: 1,
|
|
114
116
|
label: "pub registration notice",
|
|
115
117
|
config: {
|
|
118
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
116
119
|
text: "We only use your details to create and support your account.",
|
|
117
120
|
tone: "secondary",
|
|
118
121
|
},
|
|
@@ -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_AUTH_RUNTIME_MODULE_ID } from "../ids";
|
|
@@ -89,6 +90,7 @@ export async function buildPhiDefaultPubResetPasswordPageTree({ page, presetKey,
|
|
|
89
90
|
sortOrder: 0,
|
|
90
91
|
label: "pub reset password description widget",
|
|
91
92
|
config: {
|
|
93
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
92
94
|
eyebrow: "Reset password",
|
|
93
95
|
title: "Choose a new password",
|
|
94
96
|
description: "Use the reset link from your email to choose a new secure password.",
|
|
@@ -113,6 +115,7 @@ export async function buildPhiDefaultPubResetPasswordPageTree({ page, presetKey,
|
|
|
113
115
|
slotIndex: 0,
|
|
114
116
|
label: "pub reset password intro",
|
|
115
117
|
config: {
|
|
118
|
+
textSource: PHI_TEXT_SOURCE_PRESET,
|
|
116
119
|
text: "If the account exists, a reset email is on its way. Open the link in that email to choose a new password.",
|
|
117
120
|
tone: "secondary",
|
|
118
121
|
visibleWhen: withoutToken,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isPhiAuthoredTextField, readPhiTextSource } from "../../../../helpers/text-source";
|
|
3
4
|
import { useEffect } from "react";
|
|
4
5
|
import { PhiGeometryControl } from "../../../../components/controls/phi-geometry-control";
|
|
5
6
|
import { PhiSurfaceControl } from "../../../../components/controls/phi-surface-control";
|
|
@@ -81,6 +82,14 @@ export function PhiDeveloperBuilderWidgetInspectorWidgetClient({ section, builde
|
|
|
81
82
|
videoProviderDescriptors,
|
|
82
83
|
onChange: (next) => {
|
|
83
84
|
const patch = buildPhiInspectorConfigPathPatch(currentWidgetSettingsConfigRecord, next);
|
|
85
|
+
/*
|
|
86
|
+
* An author writing into a Preset's text makes the Widget's words the Site's: from here on the
|
|
87
|
+
* Site's store translates them, not the global one (`helpers/text-source.ts`).
|
|
88
|
+
*/
|
|
89
|
+
if (readPhiTextSource(currentWidgetSettingsConfigRecord.textSource) != null
|
|
90
|
+
&& isPhiAuthoredTextField(field)) {
|
|
91
|
+
patch.textSource = undefined;
|
|
92
|
+
}
|
|
84
93
|
onConfigChange?.(patch);
|
|
85
94
|
},
|
|
86
95
|
});
|
|
@@ -1663,6 +1663,8 @@ async function buildPhiDefaultBuilderPagePresetTemplateTree({ page, runtime, pre
|
|
|
1663
1663
|
}),
|
|
1664
1664
|
nodes.widget({
|
|
1665
1665
|
typeKey: "simple-text",
|
|
1666
|
+
/* Label set text, translated already; a second pass would hand the translator a non-source text. */
|
|
1667
|
+
flags: PhiCmsFlags.NoTranslate,
|
|
1666
1668
|
id: PHI_BUILDER_MODULE_USAGE_WIDGET_IDS.moduleUsageIntro,
|
|
1667
1669
|
parentLayoutNodeId: PHI_BUILDER_MODULE_USAGE_LAYOUT_IDS.moduleUsageBody,
|
|
1668
1670
|
slotIndex: PHI_CMS_DEFAULT_SLOT_INDEX,
|
|
@@ -1737,6 +1739,8 @@ async function buildPhiDefaultBuilderPagePresetTemplateTree({ page, runtime, pre
|
|
|
1737
1739
|
}),
|
|
1738
1740
|
nodes.widget({
|
|
1739
1741
|
typeKey: "simple-text",
|
|
1742
|
+
/* Label set text, translated already; a second pass would hand the translator a non-source text. */
|
|
1743
|
+
flags: PhiCmsFlags.NoTranslate,
|
|
1740
1744
|
id: PHI_BUILDER_PUBLIC_ROUTES_WIDGET_IDS.publicRoutesIntro,
|
|
1741
1745
|
parentLayoutNodeId: PHI_BUILDER_PUBLIC_ROUTES_LAYOUT_IDS.publicRoutesBody,
|
|
1742
1746
|
slotIndex: PHI_CMS_DEFAULT_SLOT_INDEX,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { trBulk } from "../../../../../server-helpers/translate";
|
|
2
|
+
import { trBulk, trBulkForLocale } from "../../../../../server-helpers/translate";
|
|
3
3
|
import { isPhiMediaSvgContentType } from "../../../../../constants/media";
|
|
4
4
|
import { resolvePhiImagePresentation } from "../../../../../components/media/image-presentation";
|
|
5
5
|
import { resolvePhiPublicAssetReference } from "../../../../../components/widgets/helpers/internal-reference-resolver.server";
|
|
@@ -23,7 +23,9 @@ export async function PhiCardWidget({ labels, config, runtime, links, translate,
|
|
|
23
23
|
["actionLabel", actionEnabled ? ownActionLabel : undefined],
|
|
24
24
|
];
|
|
25
25
|
const translatedTexts = translate
|
|
26
|
-
? await
|
|
26
|
+
? await (runtime?.locale.current
|
|
27
|
+
? trBulkForLocale(runtime.locale.current, textEntries.map(([, value]) => value ?? ""))
|
|
28
|
+
: trBulk(textEntries.map(([, value]) => value ?? "")))
|
|
27
29
|
: textEntries.map(([, value]) => value ?? "");
|
|
28
30
|
const translatedByKey = new Map(textEntries.map(([key], index) => [key, translatedTexts[index] || undefined]));
|
|
29
31
|
/*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PhiTextSource } from "../../../../../helpers/text-source";
|
|
1
2
|
import { type PhiCmsWidgetConfigBase } from "../../../../../components/widgets/config/parser-primitives";
|
|
2
3
|
import { type PhiTextTone } from "../../../../../components/widgets/config/text-tone";
|
|
3
4
|
import { type PhiBulletListEntry, type PhiBulletListStyle } from "../../../../../components/widgets/shared/bullet-list-vocabulary";
|
|
@@ -13,6 +14,8 @@ export type PhiCmsDescriptionWidgetConfig = PhiCmsWidgetConfigBase & {
|
|
|
13
14
|
footer?: string;
|
|
14
15
|
/** The tone of the description text; absent is the default secondary text. */
|
|
15
16
|
tone?: PhiTextTone;
|
|
17
|
+
/** `"preset"` while the words are a Preset's and translated globally (`helpers/text-source.ts`). */
|
|
18
|
+
textSource?: PhiTextSource;
|
|
16
19
|
};
|
|
17
20
|
declare function parsePhiCmsDescriptionWidgetConfig(config: Record<string, unknown>): PhiCmsDescriptionWidgetConfig;
|
|
18
21
|
export declare const PHI_DESCRIPTION_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 { readRenderableBlockConfig, readString } from "../../../../../components/widgets/config/parser-primitives";
|
|
4
5
|
import { PHI_TEXT_TONE_FIELD_OPTIONS, readPhiTextTone, } from "../../../../../components/widgets/config/text-tone";
|
|
@@ -15,6 +16,7 @@ function parsePhiCmsDescriptionWidgetConfig(config) {
|
|
|
15
16
|
asideList: readPhiBulletListStyle(config.asideList),
|
|
16
17
|
footer: readString(config.footer),
|
|
17
18
|
tone: readPhiTextTone(config.tone),
|
|
19
|
+
textSource: readPhiTextSource(config.textSource),
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
export const PHI_DESCRIPTION_WIDGET_DEFINITION = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
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 =
|
|
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 {
|
|
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
|
|
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,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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
?
|
|
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;
|
|
@@ -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 {
|
|
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
|
-
|
|
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:
|
|
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.
|
package/styles/controls.css
CHANGED
|
@@ -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
|
-
|
|
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
|
/*
|
package/styles/root.css
CHANGED
|
@@ -114,29 +114,30 @@ samp {
|
|
|
114
114
|
animation-timing-function: var(--phi-effects-animation-easing, ease-out);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
:where([data-phi-renderable-block="true"][data-phi-effects-transition="composite"][data-phi-effects-trigger="on_visible"]) {
|
|
118
|
-
animation-composition: var(--phi-effects-animation-composition, replace);
|
|
119
|
-
animation-delay: var(--phi-effects-animation-delay, 0ms);
|
|
120
|
-
animation-duration: var(--phi-effects-animation-duration, 1000ms);
|
|
121
|
-
animation-name: var(--phi-effects-animation-name);
|
|
122
|
-
animation-play-state: paused;
|
|
123
|
-
animation-timing-function: var(--phi-effects-animation-easing, ease-out);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
:where([data-phi-renderable-block="true"][data-phi-effects-transition="composite"][data-phi-effects-trigger="on_visible"][data-phi-effects-state="running"]) {
|
|
127
|
-
animation-play-state: running;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
117
|
/*
|
|
131
|
-
* On ready, the whole rule inside the query that asks whether anything can release it.
|
|
118
|
+
* On visible and on ready, the whole rule inside the query that asks whether anything can release it.
|
|
132
119
|
*
|
|
133
|
-
* The animation is held at its first frame until the
|
|
134
|
-
* content the server streams in a segment of its own can land,
|
|
135
|
-
* behind an entrance that has not started. Only scripting sets the
|
|
136
|
-
* is none the Block would stand at that first frame for good, which
|
|
137
|
-
* without scripting there is no animation at all and the Block is
|
|
120
|
+
* The animation is held at its first frame -- until the Block is scrolled into view, or until the page
|
|
121
|
+
* has finished arriving, so a Widget whose content the server streams in a segment of its own can land,
|
|
122
|
+
* and everything below it can settle, behind an entrance that has not started. Only scripting sets the
|
|
123
|
+
* state that releases it; where there is none the Block would stand at that first frame for good, which
|
|
124
|
+
* for a fade in means invisible. So without scripting there is no animation at all and the Block is
|
|
125
|
+
* simply there.
|
|
138
126
|
*/
|
|
139
127
|
@media (scripting: enabled) {
|
|
128
|
+
:where([data-phi-renderable-block="true"][data-phi-effects-transition="composite"][data-phi-effects-trigger="on_visible"]) {
|
|
129
|
+
animation-composition: var(--phi-effects-animation-composition, replace);
|
|
130
|
+
animation-delay: var(--phi-effects-animation-delay, 0ms);
|
|
131
|
+
animation-duration: var(--phi-effects-animation-duration, 1000ms);
|
|
132
|
+
animation-name: var(--phi-effects-animation-name);
|
|
133
|
+
animation-play-state: paused;
|
|
134
|
+
animation-timing-function: var(--phi-effects-animation-easing, ease-out);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
:where([data-phi-renderable-block="true"][data-phi-effects-transition="composite"][data-phi-effects-trigger="on_visible"][data-phi-effects-state="running"]) {
|
|
138
|
+
animation-play-state: running;
|
|
139
|
+
}
|
|
140
|
+
|
|
140
141
|
:where([data-phi-renderable-block="true"][data-phi-effects-transition="composite"][data-phi-effects-trigger="on_ready"]) {
|
|
141
142
|
animation-composition: var(--phi-effects-animation-composition, replace);
|
|
142
143
|
animation-delay: var(--phi-effects-animation-delay, 0ms);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PhiBulletListEntry, PhiBulletListStyle } from "../components/widgets/shared/bullet-list-vocabulary";
|
|
2
2
|
import type { PhiRenderableBlockBase } from "./renderable-block";
|
|
3
|
+
import type { PhiTextSource } from "../helpers/text-source";
|
|
3
4
|
import type { PhiRuntimeConditionExpression } from "./runtime-condition";
|
|
4
5
|
import type { PhiSignalRoute, PhiSignalValue } from "./signals";
|
|
5
6
|
import type { PhiCssLength } from "./length";
|
|
@@ -71,6 +72,8 @@ export type PhiSimpleTextWidgetPlacement = PhiWidgetPlacementBase & {
|
|
|
71
72
|
tone?: PhiTextTone;
|
|
72
73
|
marks?: PhiSimpleTextMark[];
|
|
73
74
|
disabled?: boolean;
|
|
75
|
+
/** `"preset"` while the words are a Preset's (`helpers/text-source.ts`). */
|
|
76
|
+
textSource?: PhiTextSource;
|
|
74
77
|
};
|
|
75
78
|
export type PhiDescriptionWidgetPlacement = PhiWidgetPlacementBase & {
|
|
76
79
|
eyebrow?: string;
|
|
@@ -81,6 +84,8 @@ export type PhiDescriptionWidgetPlacement = PhiWidgetPlacementBase & {
|
|
|
81
84
|
asideList?: PhiBulletListStyle;
|
|
82
85
|
footer?: string;
|
|
83
86
|
tone?: PhiTextTone;
|
|
87
|
+
/** `"preset"` while the words are a Preset's (`helpers/text-source.ts`). */
|
|
88
|
+
textSource?: PhiTextSource;
|
|
84
89
|
};
|
|
85
90
|
export type PhiButtonWidgetPlacement = PhiWidgetPlacementBase & PhiControlPlacement & PhiControlBadgeConfig & {
|
|
86
91
|
action?: PhiCommonControlActionKey;
|
package/types/table-widget.d.ts
CHANGED
|
@@ -306,6 +306,8 @@ export type PhiTableWidgetPresentation = PhiControlPresentationConfig & {
|
|
|
306
306
|
borders?: boolean;
|
|
307
307
|
/** The header row; shown unless this says `false`. */
|
|
308
308
|
header?: boolean;
|
|
309
|
+
/** Whether the header row stays at the top while the rows scroll under it; on unless this says `false`. */
|
|
310
|
+
stickyHeader?: boolean;
|
|
309
311
|
footer?: PhiTableFooterConfig;
|
|
310
312
|
summary?: PhiTableSummaryConfig;
|
|
311
313
|
row?: {
|