@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.
- 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-body-client.d.ts +2 -1
- package/components/widgets/shared/markdown-body-client.js +3 -1
- package/components/widgets/shared/markdown-render.js +23 -6
- 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/config.d.ts +3 -1
- package/plugins/runtime-modules/core/widgets/markdown/config.js +14 -0
- 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 +41 -1
- package/styles/root.css +20 -19
- package/types/core-widget-placements.d.ts +8 -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
|
/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PhiClientBlockBaseProps, PhiNoLabels } from "../../../types";
|
|
2
2
|
import type { PhiCodeToken } from "./markdown-code-tokens";
|
|
3
|
-
import type { PhiMarkdownTextAlign } from "../../../types/core-widget-placements";
|
|
3
|
+
import type { PhiMarkdownImageFloat, PhiMarkdownTextAlign } from "../../../types/core-widget-placements";
|
|
4
4
|
import { type PhiSpacingScaleKey } from "../config/spacing-options";
|
|
5
5
|
import type { PhiMarkdownTocHeading } from "../../../types/markdown";
|
|
6
6
|
import { type PhiBulletListStyle } from "./bullet-list-vocabulary";
|
|
@@ -75,6 +75,7 @@ export type PhiMarkdownWidgetClientProps = PhiClientBlockBaseProps<PhiNoLabels,
|
|
|
75
75
|
tocKey?: string;
|
|
76
76
|
headings?: PhiMarkdownTocHeading[];
|
|
77
77
|
textAlign?: PhiMarkdownTextAlign;
|
|
78
|
+
imageFloat?: PhiMarkdownImageFloat;
|
|
78
79
|
textBlockSpacingBefore?: PhiSpacingScaleKey;
|
|
79
80
|
textBlockSpacingAfter?: PhiSpacingScaleKey;
|
|
80
81
|
headingBlockSpacingBefore?: PhiSpacingScaleKey;
|
|
@@ -162,7 +162,9 @@ export function PhiMarkdownWidgetClient({ config }) {
|
|
|
162
162
|
marginBlockStart: resolvePhiSpacingScaleValue(config?.headingBlockSpacingBefore ?? "none"),
|
|
163
163
|
marginBlockEnd: resolvePhiSpacingScaleValue(config?.headingBlockSpacingAfter ?? "sm"),
|
|
164
164
|
};
|
|
165
|
-
return (_jsx("div", {
|
|
165
|
+
return (_jsx("div", { className: config?.imageFloat ? `phi-markdown-body phi-markdown-body--float-${config.imageFloat}` : undefined, style: config?.imageFloat
|
|
166
|
+
? { display: "flow-root", width: "100%", textAlign: config.textAlign, containerType: "inline-size" }
|
|
167
|
+
: { display: "grid", gap: 0, width: "100%", textAlign: config?.textAlign }, children: renderBlocks(blocks, token, paragraphBlockStyle, headingBlockStyle, {
|
|
166
168
|
style: resolvePhiBulletListStyle(config?.list),
|
|
167
169
|
depth: 1,
|
|
168
170
|
}) }));
|
|
@@ -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";
|
|
@@ -56,6 +56,13 @@ function normalizePotentialPhiScheme(value) {
|
|
|
56
56
|
* accepted let `javascript:` and `data:` through here while the inline path refused them.
|
|
57
57
|
*/
|
|
58
58
|
const PHI_EXTERNAL_DOCUMENT_URL_SCHEMES = /^(?:https?:|mailto:|tel:)$/iu;
|
|
59
|
+
/*
|
|
60
|
+
* A picture written into the text itself may also be a data URL of an image type -- the one way a Module
|
|
61
|
+
* brings a drawing with its copy, since it cannot put a file into the Site's `public/`. An `<img>` runs
|
|
62
|
+
* nothing it loads, an SVG's scripts included, so the type list is what keeps this a picture. Links
|
|
63
|
+
* stay on the scheme list above, and a fetched document gets no data URLs at all.
|
|
64
|
+
*/
|
|
65
|
+
const PHI_INLINE_IMAGE_DATA_URL = /^data:image\/(?:png|jpeg|gif|webp|svg\+xml)[;,]/iu;
|
|
59
66
|
function resolveExternalDocumentUrl(value, sourceUrl) {
|
|
60
67
|
const normalized = value.trim();
|
|
61
68
|
if (!normalized || normalizePotentialPhiScheme(normalized).startsWith("phis:"))
|
|
@@ -119,7 +126,7 @@ async function projectInternalMarkdownNodes(tree, runtime) {
|
|
|
119
126
|
const src = resolved.assetUrls.get(reference.assetId);
|
|
120
127
|
return src ? [{ ...node, url: src, children }] : [];
|
|
121
128
|
}
|
|
122
|
-
if (!/^https?:/iu.test(node.url ?? ""))
|
|
129
|
+
if (!/^https?:/iu.test(node.url ?? "") && !PHI_INLINE_IMAGE_DATA_URL.test(node.url ?? ""))
|
|
123
130
|
return [];
|
|
124
131
|
return [{ ...node, children }];
|
|
125
132
|
}
|
|
@@ -294,11 +301,21 @@ function parseTranslatedMarkdownInlines(translatedHtml, metadata) {
|
|
|
294
301
|
appendText(translatedHtml.slice(cursor));
|
|
295
302
|
return stack.length === 1 ? stack[0]?.children ?? [] : null;
|
|
296
303
|
}
|
|
297
|
-
|
|
304
|
+
/*
|
|
305
|
+
* The render's own locale where there is one. A static render has no request to ask: `trBulk` reads the
|
|
306
|
+
* locale from the request headers, finds none, and answers in the Site's default -- which left every
|
|
307
|
+
* static Public page's body in its source language under a translated header.
|
|
308
|
+
*/
|
|
309
|
+
function translateUnits(locale, msgs, format, sourceLocale) {
|
|
310
|
+
return locale
|
|
311
|
+
? trBulkForLocale(locale, msgs, 0, format, sourceLocale)
|
|
312
|
+
: trBulk(msgs, 0, format, sourceLocale);
|
|
313
|
+
}
|
|
314
|
+
async function buildMarkdownInlineMap(tree, translate, sourceLocale, locale) {
|
|
298
315
|
const units = [];
|
|
299
316
|
collectMarkdownTranslationUnits(tree, units);
|
|
300
317
|
const translated = translate && units.length > 0
|
|
301
|
-
? await
|
|
318
|
+
? await translateUnits(locale, units.map((unit) => unit.html), "html", sourceLocale)
|
|
302
319
|
: units.map((unit) => unit.html);
|
|
303
320
|
/*
|
|
304
321
|
* Image titles are prose and translate as plain text, each on its own: they are separate sentences
|
|
@@ -307,7 +324,7 @@ async function buildMarkdownInlineMap(tree, translate, sourceLocale) {
|
|
|
307
324
|
*/
|
|
308
325
|
const titleSlots = units.flatMap((unit) => unit.metadata.titles.flatMap((title, index) => title ? [{ metadata: unit.metadata, index, title }] : []));
|
|
309
326
|
if (translate && titleSlots.length > 0) {
|
|
310
|
-
const translatedTitles = await
|
|
327
|
+
const translatedTitles = await translateUnits(locale, titleSlots.map((slot) => slot.title), "text", sourceLocale);
|
|
311
328
|
titleSlots.forEach((slot, index) => {
|
|
312
329
|
slot.metadata.titles[slot.index] = translatedTitles[index] || slot.title;
|
|
313
330
|
});
|
|
@@ -508,7 +525,7 @@ async function resolveMarkdownRenderDataFromSource(markdown, translate, headingI
|
|
|
508
525
|
headingIdPrefix,
|
|
509
526
|
headingOffset,
|
|
510
527
|
};
|
|
511
|
-
const inlineMap = await buildMarkdownInlineMap(tree, translate, sourceLocale);
|
|
528
|
+
const inlineMap = await buildMarkdownInlineMap(tree, translate, sourceLocale, runtime?.locale.current);
|
|
512
529
|
const blocks = mapBlockNodes(tree.children, context, inlineMap);
|
|
513
530
|
return { blocks, headings: context.headings };
|
|
514
531
|
}
|
|
@@ -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 = {
|