@akinon/pz-theme 2.0.164-rc.1 → 2.0.164
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/CHANGELOG.md +8 -58
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/__tests__/theme-settings-defaults.test.tsx +395 -0
- package/src/__tests__/theme-text-defaults-code-owned.test.tsx +340 -0
- package/src/blocks/__tests__/breadcrumb-theme-separator.test.tsx +148 -0
- package/src/blocks/__tests__/social-icons-theme-fallback.test.tsx +268 -0
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/accordion-block.tsx +10 -0
- package/src/blocks/hotspot-block.tsx +6 -1
- package/src/blocks/listing-breadcrumb-block.tsx +10 -1
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/product-breadcrumb-block.tsx +9 -1
- package/src/blocks/social-glyphs.tsx +31 -0
- package/src/blocks/social-icons-list-block.tsx +72 -8
- package/src/blocks/text-block.tsx +15 -6
- package/src/chrome/__tests__/theme-page-up.test.tsx +522 -0
- package/src/chrome/theme-page-up.tsx +303 -0
- package/src/chrome/use-editor-theme-settings.ts +104 -0
- package/src/components/with-designer-features.tsx +27 -0
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/index.ts +15 -0
- package/src/overlays/theme-modals.tsx +10 -0
- package/src/sections/__tests__/section-wrapper.test.tsx +76 -0
- package/src/sections/coupon-banner-section.tsx +15 -1
- package/src/sections/section-wrapper.tsx +7 -0
- package/src/sections/tabs-section.tsx +13 -0
- package/src/theme-placeholder-wrapper.tsx +50 -3
- package/src/theme-section.tsx +15 -0
- package/src/utils/__tests__/svg-image-source.test.ts +123 -0
- package/src/utils/__tests__/theme-settings-css.test.ts +801 -0
- package/src/utils/__tests__/theme-settings-readers.test.ts +213 -0
- package/src/utils/__tests__/theme-settings-resolver.test.ts +232 -0
- package/src/utils/index.ts +36 -1
- package/src/utils/svg-image-source.ts +84 -0
- package/src/utils/theme-settings-css.ts +717 -0
- package/src/utils/theme-settings-readers.ts +205 -0
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { readThemeBreadcrumbSeparator } from '../utils/theme-settings-readers';
|
|
6
7
|
import { useThemePageContext } from '../theme-page-context';
|
|
8
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
7
9
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
10
|
import {
|
|
9
11
|
DesignerPlaceholder,
|
|
@@ -70,13 +72,19 @@ const ProductBreadcrumbBlock = ({
|
|
|
70
72
|
}: BlockRendererProps) => {
|
|
71
73
|
const product = useThemeProduct();
|
|
72
74
|
const pageContext = useThemePageContext();
|
|
75
|
+
const themeSettings = useThemeSettingsContext();
|
|
73
76
|
|
|
74
77
|
const prop = (key: string, fallback: string) =>
|
|
75
78
|
String(
|
|
76
79
|
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
77
80
|
);
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
// An unset separator follows the theme's breadcrumb setting; a
|
|
83
|
+
// stored one (including '/') keeps winning, so saved blocks render as before.
|
|
84
|
+
const separator = prop(
|
|
85
|
+
'separator',
|
|
86
|
+
readThemeBreadcrumbSeparator(themeSettings) ?? '/'
|
|
87
|
+
);
|
|
80
88
|
const ariaLabel = prop('ariaLabel', 'Breadcrumb');
|
|
81
89
|
const homeLabel = prop('homeLabel', 'Home');
|
|
82
90
|
const homeHref = prop('homeHref', '/');
|
|
@@ -119,8 +119,39 @@ export const SOCIAL_GLYPHS: Record<string, React.ReactNode> = {
|
|
|
119
119
|
)
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Keys that draw another key's mark. The block pickers store `twitter`; the
|
|
124
|
+
* settings page's social networks store X as `x`. Kept out of
|
|
125
|
+
* `SOCIAL_GLYPHS` itself so that table stays the list of what the picker
|
|
126
|
+
* offers, with one path per mark.
|
|
127
|
+
*/
|
|
128
|
+
export const SOCIAL_GLYPH_ALIASES: Record<string, string> = {
|
|
129
|
+
x: 'twitter'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The glyph for a platform key, following aliases; `undefined` when the
|
|
134
|
+
* platform has no mark (callers choose their own fallback, usually `globe`).
|
|
135
|
+
*/
|
|
136
|
+
export const resolveSocialGlyph = (
|
|
137
|
+
platform: string
|
|
138
|
+
): React.ReactNode | undefined => {
|
|
139
|
+
// Own keys only: a platform read from settings could be `constructor`.
|
|
140
|
+
const own = (table: Record<string, unknown>, key: string | undefined) =>
|
|
141
|
+
key !== undefined && Object.prototype.hasOwnProperty.call(table, key);
|
|
142
|
+
|
|
143
|
+
if (own(SOCIAL_GLYPHS, platform)) return SOCIAL_GLYPHS[platform];
|
|
144
|
+
|
|
145
|
+
const alias = own(SOCIAL_GLYPH_ALIASES, platform)
|
|
146
|
+
? SOCIAL_GLYPH_ALIASES[platform]
|
|
147
|
+
: undefined;
|
|
148
|
+
return own(SOCIAL_GLYPHS, alias) ? SOCIAL_GLYPHS[alias as string] : undefined;
|
|
149
|
+
};
|
|
150
|
+
|
|
122
151
|
export const PLATFORM_LABELS: Record<string, string> = {
|
|
123
152
|
twitter: 'Twitter / X',
|
|
153
|
+
// The settings page's key for X.
|
|
154
|
+
x: 'X',
|
|
124
155
|
globe: 'Website',
|
|
125
156
|
email: 'Email',
|
|
126
157
|
phone: 'Phone'
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { partAttrs } from '../utils';
|
|
6
|
+
import { readThemeSocialNetworks } from '../utils/theme-settings-readers';
|
|
7
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
6
8
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
-
import { DesignerPlaceholder } from './product-block-utils';
|
|
9
|
+
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
8
10
|
import { readResponsiveArray } from './responsive-array';
|
|
9
11
|
import { IconGlyph } from './icon-source';
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
SOCIAL_GLYPHS,
|
|
14
|
+
platformLabel,
|
|
15
|
+
resolveSocialGlyph
|
|
16
|
+
} from './social-glyphs';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* What the editor's social icon panel writes per entry
|
|
@@ -37,12 +43,29 @@ const clampSize = (value: unknown, fallback: number) => {
|
|
|
37
43
|
*/
|
|
38
44
|
const isExternalHref = (href: string) => /^(https?:)?\/\//i.test(href);
|
|
39
45
|
|
|
46
|
+
interface SocialIconEntry {
|
|
47
|
+
key: string;
|
|
48
|
+
platform: string;
|
|
49
|
+
href: string;
|
|
50
|
+
label: string;
|
|
51
|
+
size: number;
|
|
52
|
+
color: string;
|
|
53
|
+
/** A brand-uploaded icon as a safe image source; null draws the glyph. */
|
|
54
|
+
iconSrc: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
/**
|
|
41
58
|
* Functional chrome block: a row of social platform marks, each one a link.
|
|
42
59
|
* Entries are authored through the `icons` property (platform, url, size,
|
|
43
60
|
* colour); this block has no children, no captions and no layout properties —
|
|
44
61
|
* it is `social-links` reduced to the icons.
|
|
45
62
|
*
|
|
63
|
+
* With `useThemeSocialNetworks` on and no entries of its own, the
|
|
64
|
+
* block draws the theme's `socialNetworks` (settings page) instead, in their
|
|
65
|
+
* stored order. Opt-in because the block saves `icons: []` by default and the
|
|
66
|
+
* old settings page already stored networks: an automatic fallback would make
|
|
67
|
+
* blocks that render nothing today start drawing icons on existing sites.
|
|
68
|
+
*
|
|
46
69
|
* NO BOX OF ITS OWN, deliberately. The entries are returned as siblings so
|
|
47
70
|
* they are the direct children of the `[data-block-id]` wrapper the generated
|
|
48
71
|
* stylesheet writes onto. The descriptor advertises `gap` as a block-level
|
|
@@ -50,7 +73,7 @@ const isExternalHref = (href: string) => /^(https?:)?\/\//i.test(href);
|
|
|
50
73
|
* on the wrapper only reaches the icons if nothing of ours sits in between —
|
|
51
74
|
* a list element here would collect the whole row into one flex item.
|
|
52
75
|
*
|
|
53
|
-
* Properties: icons.
|
|
76
|
+
* Properties: icons, useThemeSocialNetworks.
|
|
54
77
|
* Parts: link, icon.
|
|
55
78
|
*/
|
|
56
79
|
const SocialIconsListBlock = ({
|
|
@@ -64,7 +87,14 @@ const SocialIconsListBlock = ({
|
|
|
64
87
|
currentBreakpoint
|
|
65
88
|
);
|
|
66
89
|
|
|
67
|
-
const
|
|
90
|
+
const themeSettings = useThemeSettingsContext();
|
|
91
|
+
const themeNetworksOn = readToggle(
|
|
92
|
+
block.properties?.useThemeSocialNetworks,
|
|
93
|
+
currentBreakpoint,
|
|
94
|
+
false
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const ownEntries: SocialIconEntry[] = items
|
|
68
98
|
.filter((item) => item && (item.icon || item.link))
|
|
69
99
|
.map((item, index) => {
|
|
70
100
|
const platform = String(item.icon || 'globe').toLowerCase();
|
|
@@ -78,14 +108,34 @@ const SocialIconsListBlock = ({
|
|
|
78
108
|
// 24 is what the panel seeds a new entry with, so an entry saved
|
|
79
109
|
// before the size field existed matches the ones saved after it.
|
|
80
110
|
size: clampSize(item.size, 24),
|
|
81
|
-
color: String(item.color || '')
|
|
111
|
+
color: String(item.color || ''),
|
|
112
|
+
iconSrc: null
|
|
82
113
|
};
|
|
83
114
|
});
|
|
84
115
|
|
|
116
|
+
// The block's own entries always win; the theme list only fills an empty
|
|
117
|
+
// block that asked for it. Hrefs and icons are validated by the reader.
|
|
118
|
+
const entries: SocialIconEntry[] =
|
|
119
|
+
ownEntries.length === 0 && themeNetworksOn
|
|
120
|
+
? readThemeSocialNetworks(themeSettings).map((network, index) => ({
|
|
121
|
+
key: `${network.network}-${index}`,
|
|
122
|
+
platform: network.network,
|
|
123
|
+
href: network.href,
|
|
124
|
+
label: platformLabel(network.network),
|
|
125
|
+
size: 24,
|
|
126
|
+
color: '',
|
|
127
|
+
iconSrc: network.iconSrc
|
|
128
|
+
}))
|
|
129
|
+
: ownEntries;
|
|
130
|
+
|
|
85
131
|
if (entries.length === 0) {
|
|
86
132
|
return isDesigner ? (
|
|
87
133
|
<DesignerPlaceholder
|
|
88
|
-
label=
|
|
134
|
+
label={
|
|
135
|
+
themeNetworksOn
|
|
136
|
+
? 'Social icons (no theme social networks)'
|
|
137
|
+
: 'Social icons (no entries)'
|
|
138
|
+
}
|
|
89
139
|
attribute="data-social-icons-list"
|
|
90
140
|
/>
|
|
91
141
|
) : null;
|
|
@@ -96,9 +146,23 @@ const SocialIconsListBlock = ({
|
|
|
96
146
|
{entries.map((entry) => {
|
|
97
147
|
// An unknown platform still draws something recognisable rather than
|
|
98
148
|
// an empty box; the picker and this table are separate lists.
|
|
99
|
-
const glyph =
|
|
149
|
+
const glyph = resolveSocialGlyph(entry.platform) || SOCIAL_GLYPHS.globe;
|
|
100
150
|
|
|
101
|
-
|
|
151
|
+
// A brand's own icon is an <img>, never injected markup: SVG loaded
|
|
152
|
+
// as an image runs no script (see utils/svg-image-source.ts). It
|
|
153
|
+
// cannot follow `currentColor`, which is the price of that.
|
|
154
|
+
const icon = entry.iconSrc ? (
|
|
155
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
156
|
+
<img
|
|
157
|
+
src={entry.iconSrc}
|
|
158
|
+
width={entry.size}
|
|
159
|
+
height={entry.size}
|
|
160
|
+
alt=""
|
|
161
|
+
aria-hidden="true"
|
|
162
|
+
data-social-icon-image=""
|
|
163
|
+
{...partAttrs('icon')}
|
|
164
|
+
/>
|
|
165
|
+
) : (
|
|
102
166
|
<IconGlyph
|
|
103
167
|
glyph={glyph}
|
|
104
168
|
size={entry.size}
|
|
@@ -7,7 +7,11 @@ import { getInnerRootInlineStyles, partAttrs } from '../utils';
|
|
|
7
7
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
8
8
|
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
9
9
|
import { localizeHtmlLinks } from '../utils/localize-href';
|
|
10
|
-
|
|
10
|
+
// Rich-text element defaults from the theme settings.
|
|
11
|
+
import {
|
|
12
|
+
THEME_RICH_TEXT_ATTRIBUTE,
|
|
13
|
+
hasThemeTextDefaults
|
|
14
|
+
} from '../utils/theme-settings-css';
|
|
11
15
|
|
|
12
16
|
const TextBlock = ({
|
|
13
17
|
block,
|
|
@@ -15,7 +19,7 @@ const TextBlock = ({
|
|
|
15
19
|
}: BlockRendererProps) => {
|
|
16
20
|
const { locale } = useLocalization();
|
|
17
21
|
const localize = useLocalizeHref();
|
|
18
|
-
const
|
|
22
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
19
23
|
const themeSettings = useThemeSettingsContext();
|
|
20
24
|
|
|
21
25
|
const getLocalizedContent = (): string => {
|
|
@@ -31,10 +35,7 @@ const TextBlock = ({
|
|
|
31
35
|
|
|
32
36
|
if (typeof value === 'object' && value !== null) {
|
|
33
37
|
return (
|
|
34
|
-
value[locale] ||
|
|
35
|
-
(defaultLocale && value[defaultLocale]) ||
|
|
36
|
-
Object.values(value)[0] ||
|
|
37
|
-
''
|
|
38
|
+
value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
|
|
38
39
|
);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -66,9 +67,17 @@ const TextBlock = ({
|
|
|
66
67
|
);
|
|
67
68
|
}, [block.styles, block.rowStyleKeys, themeSettings, currentBreakpoint]);
|
|
68
69
|
|
|
70
|
+
// Marks this root as the rich text the theme's element defaults
|
|
71
|
+
// (link/heading/text colours, managed typography) may style. Only while the
|
|
72
|
+
// theme sets one of those keys, so an untouched site's markup is unchanged.
|
|
73
|
+
const richTextAttrs = hasThemeTextDefaults(themeSettings)
|
|
74
|
+
? { [THEME_RICH_TEXT_ATTRIBUTE]: '' }
|
|
75
|
+
: null;
|
|
76
|
+
|
|
69
77
|
return (
|
|
70
78
|
<div
|
|
71
79
|
{...partAttrs('root')}
|
|
80
|
+
{...richTextAttrs}
|
|
72
81
|
style={rootStyles}
|
|
73
82
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
74
83
|
/>
|