@akinon/pz-theme 2.0.164-rc.0 → 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 -50
- 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
|
@@ -33,20 +33,19 @@ export type PropertyType = (typeof CUSTOM_PROPERTY_TYPES)[number];
|
|
|
33
33
|
* The subset whose value provably survives the full round trip — panel write,
|
|
34
34
|
* persistence, and resolution back into a prop.
|
|
35
35
|
*
|
|
36
|
-
* The
|
|
37
|
-
* guessed:
|
|
38
|
-
* (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
|
|
39
|
-
* through without the per-breakpoint wrapper). `getResponsiveValue` then
|
|
40
|
-
* rejects an array with a devWarn and returns the fallback, and a locale
|
|
41
|
-
* object has no `desktop` key so it does too. They can never deliver a
|
|
42
|
-
* value until a per-type resolver exists.
|
|
36
|
+
* The three excluded members are excluded for reasons that were verified, not
|
|
37
|
+
* guessed:
|
|
43
38
|
*
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
39
|
+
* - `multiselect` and `localetext` are stored UNBUCKETED by the editor
|
|
40
|
+
* (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
|
|
41
|
+
* through without the per-breakpoint wrapper). `getResponsiveValue` then
|
|
42
|
+
* rejects an array with a devWarn and returns the fallback, and a locale
|
|
43
|
+
* object has no `desktop` key so it does too. They can never deliver a
|
|
44
|
+
* value until a per-type resolver exists.
|
|
45
|
+
* - `richtext` has no safe renderer here. The allowlist sanitizer this repo
|
|
46
|
+
* already owns parses by assigning to `innerHTML` on a live element, so the
|
|
47
|
+
* sink is its first act — and pz-theme renders server-side, where there is
|
|
48
|
+
* no `document` at all.
|
|
50
49
|
*
|
|
51
50
|
* Declaring one of these throws at define time rather than shipping a control
|
|
52
51
|
* that silently shows the default forever.
|
|
@@ -60,11 +59,12 @@ export const SUPPORTED_PROPERTY_TYPES: readonly PropertyType[] = [
|
|
|
60
59
|
'color',
|
|
61
60
|
'image',
|
|
62
61
|
'file',
|
|
63
|
-
'richtext',
|
|
64
62
|
'typography',
|
|
65
63
|
'icon'
|
|
66
64
|
];
|
|
67
65
|
|
|
68
|
-
export const isSupportedPropertyType = (
|
|
66
|
+
export const isSupportedPropertyType = (
|
|
67
|
+
type: unknown
|
|
68
|
+
): type is PropertyType =>
|
|
69
69
|
typeof type === 'string' &&
|
|
70
70
|
(SUPPORTED_PROPERTY_TYPES as readonly string[]).includes(type);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { getResponsiveValue } from '../utils';
|
|
4
|
-
import { sanitizeRichText } from '../utils/sanitize-rich-text';
|
|
5
4
|
import { devWarn } from '../utils/dev-warn';
|
|
6
5
|
import {
|
|
7
6
|
isLocalizedValue,
|
|
@@ -239,11 +238,6 @@ export function useThemeSetting<
|
|
|
239
238
|
|
|
240
239
|
const defaults = splitDefault(declaration, valueLocale);
|
|
241
240
|
|
|
242
|
-
const finish = (value: unknown): TValue =>
|
|
243
|
-
(declaration.type === 'richtext'
|
|
244
|
-
? sanitizeRichText(typeof value === 'string' ? value : '')
|
|
245
|
-
: value) as TValue;
|
|
246
|
-
|
|
247
241
|
if (!context) {
|
|
248
242
|
// A missing block context is never an error on its own: a developer
|
|
249
243
|
// previewing the component in a storybook or a unit test should see the
|
|
@@ -260,7 +254,7 @@ export function useThemeSetting<
|
|
|
260
254
|
`unreachable and the declared default is used instead. Render this ` +
|
|
261
255
|
`component through a block registered with registerCustomBlocks().`
|
|
262
256
|
);
|
|
263
|
-
return
|
|
257
|
+
return defaults.page as TValue;
|
|
264
258
|
}
|
|
265
259
|
|
|
266
260
|
// Reported on every render rather than once from an effect: the collection
|
|
@@ -277,17 +271,7 @@ export function useThemeSetting<
|
|
|
277
271
|
// attribute keys that were never declared. `node` rather than `block` so a
|
|
278
272
|
// setting declared inside a custom SECTION reads the section's own
|
|
279
273
|
// properties, which is where the editor writes it.
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
// The rich text control writes per language — `{ tr: '<p>…</p>', en: … }` —
|
|
283
|
-
// and the editor stores that map as it is, with no breakpoint key: the same
|
|
284
|
-
// shape that keeps `localetext` out (see `property-types.ts`). Read the
|
|
285
|
-
// page's language out of it first; what is left is a string or a breakpoint
|
|
286
|
-
// map, which the responsive read below already understands.
|
|
287
|
-
const stored =
|
|
288
|
-
declaration.type === 'richtext'
|
|
289
|
-
? resolveLocalizedValue(authored, valueLocale)
|
|
290
|
-
: authored;
|
|
274
|
+
const stored = context.node.properties?.[key];
|
|
291
275
|
|
|
292
276
|
// The sentinel again, for the same reason: handing `getResponsiveValue` an
|
|
293
277
|
// undefined fallback would resolve an undeclared default to `''`, so a
|
|
@@ -296,15 +280,15 @@ export function useThemeSetting<
|
|
|
296
280
|
const resolved = getResponsiveValue(stored, breakpoint, NOT_AUTHORED);
|
|
297
281
|
|
|
298
282
|
if (resolved === NOT_AUTHORED) {
|
|
299
|
-
return
|
|
283
|
+
return resolveAuthoredDefault(defaults.page, breakpoint) as TValue;
|
|
300
284
|
}
|
|
301
285
|
|
|
302
286
|
// The default-language text as the editor seeded it when the block was
|
|
303
287
|
// placed, never changed since: still the author's copy, so it follows the
|
|
304
288
|
// page's language. Anything the merchant typed is theirs and shows as typed.
|
|
305
289
|
if (defaults.perLocale && resolved === defaults.base) {
|
|
306
|
-
return
|
|
290
|
+
return defaults.page as TValue;
|
|
307
291
|
}
|
|
308
292
|
|
|
309
|
-
return
|
|
293
|
+
return resolved as TValue;
|
|
310
294
|
}
|
|
@@ -4,28 +4,14 @@ import { useEffect } from 'react';
|
|
|
4
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
5
|
|
|
6
6
|
import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
|
|
7
|
-
import { useValueLocale } from './utils/localized-value';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* The storefront half of the editor's Language dropdown.
|
|
11
10
|
*
|
|
12
11
|
* The editor starts that dropdown with English only and fills it from the
|
|
13
|
-
* `LOCALE_UPDATE` frame the preview posts (`{ locale, locales
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The frame carries the storefront's default language as well. The editor's
|
|
18
|
-
* per-language text controls store a plain string in that language and a map
|
|
19
|
-
* in every other, so an editor that never heard it assumed English and gave
|
|
20
|
-
* every Turkish-default store a stray `en` key (ZERO-5296).
|
|
21
|
-
*
|
|
22
|
-
* It is posted BEFORE the document reports ready (`IFRAME_READY`): the bridge
|
|
23
|
-
* is a child of the placeholder wrapper, whose own effect posts that frame,
|
|
24
|
-
* and React runs a child's effects first. The editor relies on the order — it
|
|
25
|
-
* holds every per-language write until the preview is ready, and takes
|
|
26
|
-
* "ready without languages" to mean a storefront that will never send them.
|
|
27
|
-
*
|
|
28
|
-
* For a long time the only code answering either direction
|
|
12
|
+
* `LOCALE_UPDATE` frame the preview posts (`{ locale, locales }`); when the
|
|
13
|
+
* merchant picks another language it posts `LOCALE_UPDATE` back and expects the
|
|
14
|
+
* preview to follow. For a long time the only code answering either direction
|
|
29
15
|
* was the app-template's `LanguageSelect` — brand code. A brand whose header no
|
|
30
16
|
* longer renders that component (a header composed through ChromeSlot, a
|
|
31
17
|
* custom header section, a project older than the template) left the editor
|
|
@@ -68,7 +54,6 @@ export interface EditorLocaleOption {
|
|
|
68
54
|
interface LocaleState {
|
|
69
55
|
locale: string;
|
|
70
56
|
locales: EditorLocaleOption[];
|
|
71
|
-
defaultLocale?: string;
|
|
72
57
|
setLocale: (locale: string) => void;
|
|
73
58
|
}
|
|
74
59
|
|
|
@@ -101,22 +86,15 @@ const toEditorLocales = (locales: unknown): EditorLocaleOption[] =>
|
|
|
101
86
|
: entry.value
|
|
102
87
|
}));
|
|
103
88
|
|
|
104
|
-
const announcementKey = (
|
|
105
|
-
locale,
|
|
106
|
-
locales,
|
|
107
|
-
defaultLocale
|
|
108
|
-
}: Omit<LocaleState, 'setLocale'>) =>
|
|
109
|
-
JSON.stringify([locale, locales, defaultLocale ?? null]);
|
|
89
|
+
const announcementKey = (locale: string, locales: EditorLocaleOption[]) =>
|
|
90
|
+
JSON.stringify([locale, locales]);
|
|
110
91
|
|
|
111
|
-
const announce = ({ locale, locales
|
|
112
|
-
const key = announcementKey(
|
|
92
|
+
const announce = ({ locale, locales }: LocaleState) => {
|
|
93
|
+
const key = announcementKey(locale, locales);
|
|
113
94
|
if (key === lastAnnounced) return;
|
|
114
95
|
lastAnnounced = key;
|
|
115
96
|
|
|
116
|
-
postToEditor({
|
|
117
|
-
type: 'LOCALE_UPDATE',
|
|
118
|
-
data: { locale, locales, defaultLocale }
|
|
119
|
-
});
|
|
97
|
+
postToEditor({ type: 'LOCALE_UPDATE', data: { locale, locales } });
|
|
120
98
|
};
|
|
121
99
|
|
|
122
100
|
const handleEditorMessage = (event: MessageEvent) => {
|
|
@@ -137,7 +115,7 @@ const handleEditorMessage = (event: MessageEvent) => {
|
|
|
137
115
|
|
|
138
116
|
// The editor is already showing `requested`. Recording it as announced keeps
|
|
139
117
|
// the switch from being echoed back when the new locale reaches the page.
|
|
140
|
-
lastAnnounced = announcementKey(
|
|
118
|
+
lastAnnounced = announcementKey(requested, current.locales);
|
|
141
119
|
current.setLocale(requested);
|
|
142
120
|
};
|
|
143
121
|
|
|
@@ -148,7 +126,6 @@ const handleEditorMessage = (event: MessageEvent) => {
|
|
|
148
126
|
*/
|
|
149
127
|
export function EditorLocaleBridge(): null {
|
|
150
128
|
const { locale, locales, setLocale } = useLocalization();
|
|
151
|
-
const { defaultLocale } = useValueLocale();
|
|
152
129
|
|
|
153
130
|
useEffect(() => {
|
|
154
131
|
if (!isFramed()) return;
|
|
@@ -170,14 +147,9 @@ export function EditorLocaleBridge(): null {
|
|
|
170
147
|
useEffect(() => {
|
|
171
148
|
if (!isFramed() || !locale) return;
|
|
172
149
|
|
|
173
|
-
current = {
|
|
174
|
-
locale,
|
|
175
|
-
locales: toEditorLocales(locales),
|
|
176
|
-
defaultLocale,
|
|
177
|
-
setLocale
|
|
178
|
-
};
|
|
150
|
+
current = { locale, locales: toEditorLocales(locales), setLocale };
|
|
179
151
|
announce(current);
|
|
180
|
-
}, [locale, locales,
|
|
152
|
+
}, [locale, locales, setLocale]);
|
|
181
153
|
|
|
182
154
|
return null;
|
|
183
155
|
}
|
package/src/index.ts
CHANGED
|
@@ -164,6 +164,21 @@ export type {
|
|
|
164
164
|
} from './overlays/modal-registry';
|
|
165
165
|
|
|
166
166
|
export { default as ThemeModals } from './overlays/theme-modals';
|
|
167
|
+
// Also mounted by ThemeModals; exported for brands without it.
|
|
168
|
+
export { ThemePageUp } from './chrome/theme-page-up';
|
|
169
|
+
export type { ThemePageUpProps } from './chrome/theme-page-up';
|
|
170
|
+
export {
|
|
171
|
+
PAGE_UP_OFFSET_VARIABLE,
|
|
172
|
+
readThemeBreadcrumbSeparator,
|
|
173
|
+
readThemePageUpSettings,
|
|
174
|
+
readThemeSocialNetworks,
|
|
175
|
+
toSafeSocialHref
|
|
176
|
+
} from './utils/theme-settings-readers';
|
|
177
|
+
export type {
|
|
178
|
+
ThemePageUpSettings,
|
|
179
|
+
ThemeSocialNetworkEntry
|
|
180
|
+
} from './utils/theme-settings-readers';
|
|
181
|
+
export { toSafeImageSource } from './utils/svg-image-source';
|
|
167
182
|
export { default as ThemeModalMount } from './overlays/theme-modal-mount';
|
|
168
183
|
|
|
169
184
|
export {
|
|
@@ -5,6 +5,9 @@ import { getThemeConfig } from '../chrome/theme-config-source';
|
|
|
5
5
|
import ThemeModalMount from './theme-modal-mount';
|
|
6
6
|
import { ModalHostBeacon } from './modal-host-beacon';
|
|
7
7
|
import ModalPreviewHost from './modal-preview-host';
|
|
8
|
+
// The back-to-top button rides on this global mount.
|
|
9
|
+
import ThemePageUp from '../chrome/theme-page-up';
|
|
10
|
+
import { readThemePageUpSettings } from '../utils/theme-settings-readers';
|
|
8
11
|
|
|
9
12
|
interface ThemeModalsProps {
|
|
10
13
|
/**
|
|
@@ -77,6 +80,13 @@ export default async function ThemeModals({
|
|
|
77
80
|
themeSettings={settings}
|
|
78
81
|
pageContext={pageContext}
|
|
79
82
|
/>
|
|
83
|
+
{/*
|
|
84
|
+
The settings page's "Page up" button. Mounted here because
|
|
85
|
+
this is the one global pz-theme mount every brand layout already
|
|
86
|
+
renders; it renders null unless `pageUpEnabled` is set (or the editor
|
|
87
|
+
turns it on live). Only the page-up keys cross to the client.
|
|
88
|
+
*/}
|
|
89
|
+
<ThemePageUp settings={readThemePageUpSettings(settings)} />
|
|
80
90
|
</>
|
|
81
91
|
);
|
|
82
92
|
}
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import SectionWrapper from '../section-wrapper';
|
|
4
4
|
import type { Section } from '../../theme-section';
|
|
5
|
+
import { ThemeSettingsProvider } from '../../theme-settings-context';
|
|
5
6
|
|
|
6
7
|
jest.mock('../../components/action-toolbar', () => ({
|
|
7
8
|
__esModule: true,
|
|
@@ -161,4 +162,79 @@ describe('SectionWrapper', () => {
|
|
|
161
162
|
expect(screen.queryByTestId('action-toolbar')).not.toBeInTheDocument();
|
|
162
163
|
});
|
|
163
164
|
});
|
|
165
|
+
|
|
166
|
+
// Registered and custom sections render through this wrapper,
|
|
167
|
+
// so it carries the same own-colour / own-type markers as the generic
|
|
168
|
+
// <section>: their rich text keeps inheriting the section's authored values.
|
|
169
|
+
describe('theme rich-text default markers', () => {
|
|
170
|
+
const renderWithSettings = (
|
|
171
|
+
settings: Record<string, unknown> | null,
|
|
172
|
+
styles: Record<string, unknown>
|
|
173
|
+
) =>
|
|
174
|
+
render(
|
|
175
|
+
<ThemeSettingsProvider value={settings}>
|
|
176
|
+
<SectionWrapper
|
|
177
|
+
section={makeSection({ styles })}
|
|
178
|
+
placeholderId="ph-1"
|
|
179
|
+
>
|
|
180
|
+
<span>child content</span>
|
|
181
|
+
</SectionWrapper>
|
|
182
|
+
</ThemeSettingsProvider>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const authored = {
|
|
186
|
+
color: { desktop: '#ffffff' },
|
|
187
|
+
'font-size': { mobile: '18px' }
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
it('stamps both markers when the theme has text defaults', () => {
|
|
191
|
+
const { container } = renderWithSettings(
|
|
192
|
+
{ headingColor: '#222222' },
|
|
193
|
+
authored
|
|
194
|
+
);
|
|
195
|
+
const el = getSection(container);
|
|
196
|
+
expect(el).toHaveAttribute('data-pz-own-color', '');
|
|
197
|
+
expect(el).toHaveAttribute('data-pz-own-type', '');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('stamps the colour marker for a colour set only in a state', () => {
|
|
201
|
+
// A header band whose text only turns white once the page scrolls.
|
|
202
|
+
const { container } = renderWithSettings(
|
|
203
|
+
{ textColor: '#C8C8C8' },
|
|
204
|
+
{ ':scrolled/color': { desktop: '#ffffff' } }
|
|
205
|
+
);
|
|
206
|
+
const el = getSection(container);
|
|
207
|
+
expect(el).toHaveAttribute('data-pz-own-color', '');
|
|
208
|
+
expect(el).not.toHaveAttribute('data-pz-own-type');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('stamps nothing for a section without authored colour or type', () => {
|
|
212
|
+
const { container } = renderWithSettings(
|
|
213
|
+
{ headingColor: '#222222' },
|
|
214
|
+
{ 'background-color': { desktop: '#000000' }, 'label/color': '#fff' }
|
|
215
|
+
);
|
|
216
|
+
const el = getSection(container);
|
|
217
|
+
expect(el).not.toHaveAttribute('data-pz-own-color');
|
|
218
|
+
expect(el).not.toHaveAttribute('data-pz-own-type');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('stamps nothing while the theme sets no text default', () => {
|
|
222
|
+
const { container } = renderWithSettings(
|
|
223
|
+
{ primaryColor: '#000000', footerBackgroundColor: '#F4F4F4' },
|
|
224
|
+
authored
|
|
225
|
+
);
|
|
226
|
+
const el = getSection(container);
|
|
227
|
+
expect(el).not.toHaveAttribute('data-pz-own-color');
|
|
228
|
+
expect(el).not.toHaveAttribute('data-pz-own-type');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('stamps nothing without a settings provider', () => {
|
|
232
|
+
const { container } = renderWrapper({
|
|
233
|
+
section: makeSection({ styles: authored })
|
|
234
|
+
});
|
|
235
|
+
const el = getSection(container);
|
|
236
|
+
expect(el).not.toHaveAttribute('data-pz-own-color');
|
|
237
|
+
expect(el).not.toHaveAttribute('data-pz-own-type');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
164
240
|
});
|
|
@@ -7,6 +7,10 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
import ThemeBlock, { Block } from '../theme-block';
|
|
8
8
|
import { WithDesignerFeatures } from '../components/with-designer-features';
|
|
9
9
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
10
|
+
import {
|
|
11
|
+
type ThemeOwnTextStyle,
|
|
12
|
+
themeOwnStyleAttributes
|
|
13
|
+
} from '../utils/theme-settings-css';
|
|
10
14
|
import {
|
|
11
15
|
applyAlphaToColor,
|
|
12
16
|
getCSSStyles,
|
|
@@ -530,6 +534,7 @@ const CouponBannerSection: React.FC<CouponBannerSectionProps> = ({
|
|
|
530
534
|
className?: string;
|
|
531
535
|
style?: React.CSSProperties;
|
|
532
536
|
keyOverride?: string;
|
|
537
|
+
ownTextStyle?: ThemeOwnTextStyle;
|
|
533
538
|
} = {}
|
|
534
539
|
) => {
|
|
535
540
|
if (!block) {
|
|
@@ -538,6 +543,11 @@ const CouponBannerSection: React.FC<CouponBannerSectionProps> = ({
|
|
|
538
543
|
key={options.keyOverride}
|
|
539
544
|
className={options.className}
|
|
540
545
|
style={options.style}
|
|
546
|
+
{...themeOwnStyleAttributes(
|
|
547
|
+
options.style,
|
|
548
|
+
themeSettings,
|
|
549
|
+
options.ownTextStyle
|
|
550
|
+
)}
|
|
541
551
|
>
|
|
542
552
|
{children}
|
|
543
553
|
</div>
|
|
@@ -558,6 +568,7 @@ const CouponBannerSection: React.FC<CouponBannerSectionProps> = ({
|
|
|
558
568
|
...(options.style || {}),
|
|
559
569
|
...getCSSStyles(block.styles || {}, themeSettings, currentBreakpoint)
|
|
560
570
|
}}
|
|
571
|
+
ownTextStyle={options.ownTextStyle}
|
|
561
572
|
onMoveUp={() =>
|
|
562
573
|
postBlockAction('MOVE_BLOCK_UP', block.styleSourceId || block.id)
|
|
563
574
|
}
|
|
@@ -777,7 +788,10 @@ const CouponBannerSection: React.FC<CouponBannerSectionProps> = ({
|
|
|
777
788
|
className:
|
|
778
789
|
'inline-flex min-h-[42px] flex-1 items-center justify-center px-4 py-2 text-sm font-semibold uppercase tracking-[0.12em]',
|
|
779
790
|
style: couponCodeBoxStyle,
|
|
780
|
-
keyOverride: 'coupon-code-box'
|
|
791
|
+
keyOverride: 'coupon-code-box',
|
|
792
|
+
// The code's type is the class list above, and its colour
|
|
793
|
+
// the box style: the theme's text defaults must not reach it.
|
|
794
|
+
ownTextStyle: { type: true }
|
|
781
795
|
}
|
|
782
796
|
)}
|
|
783
797
|
|
|
@@ -12,6 +12,10 @@ import { resolveSectionPartName } from '../hooks/use-designer-features';
|
|
|
12
12
|
import { useSectionSeen } from '../hooks/use-section-seen';
|
|
13
13
|
import { useListImpression } from '../hooks/use-list-impression';
|
|
14
14
|
import { ThemeSectionListProvider } from '../theme-section-data-context';
|
|
15
|
+
// Own-colour / own-typography markers for the theme's rich-text
|
|
16
|
+
// element defaults (same as the generic <section> in theme-section.tsx).
|
|
17
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
18
|
+
import { themeOwnStyleAttributes } from '../utils/theme-settings-css';
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
interface SectionWrapperProps {
|
|
@@ -82,6 +86,7 @@ export default function SectionWrapper({
|
|
|
82
86
|
).trim();
|
|
83
87
|
|
|
84
88
|
const sectionRef = useRef<HTMLElement>(null);
|
|
89
|
+
const themeSettings = useThemeSettingsContext();
|
|
85
90
|
|
|
86
91
|
// Report this section becoming visible (ZERO-5164). Inert in the editor:
|
|
87
92
|
// the emitter reads the editor handshake flag itself, so there is no
|
|
@@ -152,6 +157,8 @@ export default function SectionWrapper({
|
|
|
152
157
|
<section
|
|
153
158
|
ref={sectionRef}
|
|
154
159
|
data-section-id={section.id}
|
|
160
|
+
// Keeps the section's authored colour/typography inherited.
|
|
161
|
+
{...themeOwnStyleAttributes(section.styles, themeSettings)}
|
|
155
162
|
data-newsletter-signup={isNewsletterSection ? 'true' : undefined}
|
|
156
163
|
data-newsletter-endpoint={
|
|
157
164
|
isNewsletterSection ? newsletterEndpoint : undefined
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { twMerge } from 'tailwind-merge';
|
|
12
12
|
import clsx from 'clsx';
|
|
13
13
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
14
|
+
import { themeOwnStyleAttributes } from '../utils/theme-settings-css';
|
|
14
15
|
|
|
15
16
|
import { postToEditor } from '../editor-bridge';
|
|
16
17
|
interface TabsSectionProps {
|
|
@@ -438,6 +439,16 @@ const TabsSection: React.FC<TabsSectionProps> = ({
|
|
|
438
439
|
)}
|
|
439
440
|
|
|
440
441
|
<div
|
|
442
|
+
// The tab paints its title (the active colour, or the header
|
|
443
|
+
// block's own) and passes the header block's typography; the
|
|
444
|
+
// theme's rich-text defaults must leave both to the title.
|
|
445
|
+
{...themeOwnStyleAttributes(
|
|
446
|
+
{
|
|
447
|
+
...cleanHeaderStyles,
|
|
448
|
+
color: isActive ? activeTabColor : headerTextColor
|
|
449
|
+
},
|
|
450
|
+
themeSettings
|
|
451
|
+
)}
|
|
441
452
|
style={{
|
|
442
453
|
...cleanHeaderStyles,
|
|
443
454
|
cursor: 'pointer',
|
|
@@ -525,6 +536,8 @@ const TabsSection: React.FC<TabsSectionProps> = ({
|
|
|
525
536
|
<div
|
|
526
537
|
key={tabBlock.id || `tab-content-${index}`}
|
|
527
538
|
style={contentStyles}
|
|
539
|
+
// The Tab Content block's own styles are painted here.
|
|
540
|
+
{...themeOwnStyleAttributes(contentBlock.styles, themeSettings)}
|
|
528
541
|
>
|
|
529
542
|
{contentBlock.blocks
|
|
530
543
|
?.filter((childBlock) =>
|
|
@@ -50,6 +50,15 @@ import { parseEditorMessage, postToEditor } from './editor-bridge';
|
|
|
50
50
|
import { EditorLocaleBridge } from './editor-locale-bridge';
|
|
51
51
|
import { useBrowseMode } from './browse-mode';
|
|
52
52
|
import { neutralizeStyleTagBreakout } from './utils/css-safety';
|
|
53
|
+
// The redesigned Web Theme Settings' colour roles, custom colours
|
|
54
|
+
// and typography metrics, plus their rich-text element defaults.
|
|
55
|
+
import {
|
|
56
|
+
type ThemeRootVariable,
|
|
57
|
+
buildThemeElementDefaultsCss,
|
|
58
|
+
buildThemeRoleVariablesCss,
|
|
59
|
+
collectThemeRootVariables,
|
|
60
|
+
syncThemeRootVariables
|
|
61
|
+
} from './utils/theme-settings-css';
|
|
53
62
|
import { buildCustomFontFaceCss } from './utils/custom-fonts';
|
|
54
63
|
import {
|
|
55
64
|
collectGoogleFontFamilies,
|
|
@@ -60,6 +69,8 @@ import {
|
|
|
60
69
|
// lives in ./utils/custom-fonts because a server route has to call it too and
|
|
61
70
|
// this module is client-only.
|
|
62
71
|
export { buildCustomFontFaceCss };
|
|
72
|
+
// Re-exported for the tests, like the builders above.
|
|
73
|
+
export { buildThemeElementDefaultsCss, buildThemeRoleVariablesCss };
|
|
63
74
|
export {
|
|
64
75
|
armHistoryRestoreGuard,
|
|
65
76
|
buildRestoreGuardScript,
|
|
@@ -78,6 +89,32 @@ const themeFontsLinked = () =>
|
|
|
78
89
|
// One pageshow listener per page, not per placeholder — see the effect below.
|
|
79
90
|
let bfcacheReloadListenerArmed = false;
|
|
80
91
|
|
|
92
|
+
// The :root variables `injectThemeColors` wrote inline last time,
|
|
93
|
+
// so the next call can remove the ones whose key is gone (a hidden colour
|
|
94
|
+
// role, a deleted custom colour or typography style). One set per document,
|
|
95
|
+
// like the inline style it mirrors.
|
|
96
|
+
let writtenThemeVars: ReadonlySet<string> = new Set();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every variable the settings own on :root, for the inline sync:
|
|
100
|
+
* the family of each variant (what the loop in `injectThemeColors` writes, so
|
|
101
|
+
* a deleted variant's family is removed too) followed by the new variables.
|
|
102
|
+
*/
|
|
103
|
+
const collectInlineThemeVariables = (
|
|
104
|
+
settings: Record<string, unknown>
|
|
105
|
+
): ThemeRootVariable[] => {
|
|
106
|
+
const families: ThemeRootVariable[] = [];
|
|
107
|
+
if (Array.isArray(settings.typography)) {
|
|
108
|
+
settings.typography.forEach((variant) => {
|
|
109
|
+
const key = (variant as { key?: unknown })?.key;
|
|
110
|
+
if (typeof key !== 'string' || !/^[a-z0-9-]+$/i.test(key)) return;
|
|
111
|
+
const family = readThemeTypographyFont(settings, key);
|
|
112
|
+
if (family) families.push([`--theme-font-${key}`, family]);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return [...families, ...collectThemeRootVariables(settings)];
|
|
116
|
+
};
|
|
117
|
+
|
|
81
118
|
// Helper function to inject theme colors as CSS custom properties
|
|
82
119
|
const injectThemeColors = (settings: Record<string, unknown>) => {
|
|
83
120
|
if (!settings) return;
|
|
@@ -161,6 +198,15 @@ const injectThemeColors = (settings: Record<string, unknown>) => {
|
|
|
161
198
|
root.style.removeProperty('--theme-breadcrumb-icon-display');
|
|
162
199
|
}
|
|
163
200
|
|
|
201
|
+
// The new roles, custom colours and typography metrics: set
|
|
202
|
+
// what the settings hold now, remove what they no longer hold. The five
|
|
203
|
+
// colour variables above are never removed.
|
|
204
|
+
writtenThemeVars = syncThemeRootVariables(
|
|
205
|
+
root.style,
|
|
206
|
+
collectInlineThemeVariables(settings),
|
|
207
|
+
writtenThemeVars
|
|
208
|
+
);
|
|
209
|
+
|
|
164
210
|
// `mainMenuSticky` is deliberately not handled here: it used to write three
|
|
165
211
|
// --theme-header-* variables for a brand's own header CSS to read, which
|
|
166
212
|
// nothing ever did, and a composed header is not the brand's markup anyway.
|
|
@@ -1125,6 +1171,10 @@ export default function ThemePlaceholderWrapper({
|
|
|
1125
1171
|
[
|
|
1126
1172
|
buildThemeBackgroundCss(themeSettings),
|
|
1127
1173
|
buildThemeTypographyCss(themeSettings),
|
|
1174
|
+
// Both '' until the new settings keys are set, so a theme
|
|
1175
|
+
// that never used them gets the same bytes as before.
|
|
1176
|
+
buildThemeRoleVariablesCss(themeSettings),
|
|
1177
|
+
buildThemeElementDefaultsCss(themeSettings),
|
|
1128
1178
|
// Static, and tiny: the rules for blocks a property switch reveals on
|
|
1129
1179
|
// hover (utils/property-switches.ts). Emitted with the theme CSS so
|
|
1130
1180
|
// the storefront and the canvas hover the same way.
|
|
@@ -1166,9 +1216,6 @@ export default function ThemePlaceholderWrapper({
|
|
|
1166
1216
|
Tells the editor which languages this storefront has and follows its
|
|
1167
1217
|
language switch. Every placeholder renders one; they share a single
|
|
1168
1218
|
listener and a single announcement (see editor-locale-bridge.tsx).
|
|
1169
|
-
A child of this wrapper on purpose: its effect, and so its frame,
|
|
1170
|
-
runs before the effect below that posts IFRAME_READY, and the editor
|
|
1171
|
-
counts on hearing the languages before the ready report.
|
|
1172
1219
|
*/}
|
|
1173
1220
|
<EditorLocaleBridge />
|
|
1174
1221
|
{themeBackgroundCSS && (
|
package/src/theme-section.tsx
CHANGED
|
@@ -35,6 +35,10 @@ import { useListImpression } from './hooks/use-list-impression';
|
|
|
35
35
|
import { reportsOwnImpressions } from './utils/section-impressions';
|
|
36
36
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
37
37
|
import { useSectionSeen } from './hooks/use-section-seen';
|
|
38
|
+
// Own-colour / own-typography markers for the theme's rich-text
|
|
39
|
+
// element defaults.
|
|
40
|
+
import { useThemeSettingsContext } from './theme-settings-context';
|
|
41
|
+
import { themeOwnStyleAttributes } from './utils/theme-settings-css';
|
|
38
42
|
export interface Section {
|
|
39
43
|
id: string;
|
|
40
44
|
type: string;
|
|
@@ -180,6 +184,9 @@ export default function ThemeSection({
|
|
|
180
184
|
const { locale, currency: storefrontCurrency } = useLocalization();
|
|
181
185
|
setBindingLocale(locale);
|
|
182
186
|
|
|
187
|
+
// With the hooks, above the early returns below.
|
|
188
|
+
const themeSettings = useThemeSettingsContext();
|
|
189
|
+
|
|
183
190
|
useEffect(() => {
|
|
184
191
|
if (!isDesigner) return;
|
|
185
192
|
|
|
@@ -1195,12 +1202,20 @@ export default function ThemeSection({
|
|
|
1195
1202
|
};
|
|
1196
1203
|
}
|
|
1197
1204
|
|
|
1205
|
+
// Data-pz-own-color / data-pz-own-type, so the theme's rich-text
|
|
1206
|
+
// defaults leave the colour and typography this section authored to inherit.
|
|
1207
|
+
const ownStyleAttributes = themeOwnStyleAttributes(
|
|
1208
|
+
effectiveSection.styles,
|
|
1209
|
+
themeSettings
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1198
1212
|
return (
|
|
1199
1213
|
<ThemeSectionDataProvider items={sectionDataItems}>
|
|
1200
1214
|
<ThemeSectionListProvider listId={sectionListId} listName={sectionListName}>
|
|
1201
1215
|
<section
|
|
1202
1216
|
ref={sectionRef}
|
|
1203
1217
|
data-section-id={effectiveSection.id}
|
|
1218
|
+
{...ownStyleAttributes}
|
|
1204
1219
|
data-newsletter-signup={isNewsletterSection ? 'true' : undefined}
|
|
1205
1220
|
data-newsletter-endpoint={
|
|
1206
1221
|
isNewsletterSection ? newsletterEndpoint : undefined
|