@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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
|
|
6
|
+
import { IconGlyph, resolveIconSource } from '../blocks/icon-source';
|
|
7
|
+
import { toSafeImageSource } from '../utils/svg-image-source';
|
|
8
|
+
import {
|
|
9
|
+
PAGE_UP_OFFSET_VARIABLE,
|
|
10
|
+
readThemePageUpSettings,
|
|
11
|
+
type ThemePageUpSettings
|
|
12
|
+
} from '../utils/theme-settings-readers';
|
|
13
|
+
import { useEditorThemeSettings } from './use-editor-theme-settings';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The "back to top" button the settings page's "Page up oku" switch turns on
|
|
17
|
+
* (`pageUpEnabled`, icon `pageUpIcon`).
|
|
18
|
+
*
|
|
19
|
+
* Mounted once per page by `ThemeModals`, the one global pz-theme mount every
|
|
20
|
+
* brand layout already renders, so a brand gets it with a version bump and no
|
|
21
|
+
* layout edit. Exported for a brand that does not mount `ThemeModals`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ICON_SIZE = 24;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The lift the button measured for itself, as a private variable on the
|
|
28
|
+
* button. It is only the fallback of the brand's offset variable, so a brand
|
|
29
|
+
* that sets that variable always wins, and the `bottom` declaration itself
|
|
30
|
+
* never changes after the server render.
|
|
31
|
+
*/
|
|
32
|
+
const MEASURED_LIFT_VARIABLE = '--pz-page-up-lift';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Height of a fixed or sticky bar covering the bottom edge under the button,
|
|
36
|
+
* in px; 0 when there is none, when the brand sets the offset variable itself,
|
|
37
|
+
* or when the browser cannot hit-test.
|
|
38
|
+
*
|
|
39
|
+
* Only bars count: an element reaching above the middle of the viewport is an
|
|
40
|
+
* overlay (a drawer, a modal backdrop) that the button should not climb.
|
|
41
|
+
*/
|
|
42
|
+
export const measureBottomOverlay = (button: HTMLElement | null): number => {
|
|
43
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (typeof document.elementsFromPoint !== 'function') return 0;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
button &&
|
|
50
|
+
window
|
|
51
|
+
.getComputedStyle(button)
|
|
52
|
+
.getPropertyValue(PAGE_UP_OFFSET_VARIABLE)
|
|
53
|
+
.trim()
|
|
54
|
+
) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { innerWidth, innerHeight } = window;
|
|
59
|
+
const rect = button?.getBoundingClientRect();
|
|
60
|
+
// The button's own column (it may sit on the left in a right-to-left page).
|
|
61
|
+
const x =
|
|
62
|
+
rect && rect.width > 0 ? rect.left + rect.width / 2 : innerWidth - 40;
|
|
63
|
+
const size = rect && rect.height > 0 ? rect.height : 44;
|
|
64
|
+
// Two probes. The bottom edge finds a bar (an add-to-cart strip,
|
|
65
|
+
// an install prompt); the button's resting spot, 16px above that edge, finds
|
|
66
|
+
// a floating button parked in the same corner (a chat or WhatsApp launcher)
|
|
67
|
+
// that would otherwise sit on top of it.
|
|
68
|
+
const probes = [innerHeight - 8, innerHeight - 16 - size / 2];
|
|
69
|
+
|
|
70
|
+
let offset = 0;
|
|
71
|
+
|
|
72
|
+
probes.forEach((y, index) => {
|
|
73
|
+
for (const element of document.elementsFromPoint(x, y)) {
|
|
74
|
+
if (button && (element === button || button.contains(element))) continue;
|
|
75
|
+
|
|
76
|
+
const { position } = window.getComputedStyle(element);
|
|
77
|
+
if (position !== 'fixed' && position !== 'sticky') continue;
|
|
78
|
+
|
|
79
|
+
const box = element.getBoundingClientRect();
|
|
80
|
+
if (index === 0 && box.bottom < innerHeight - 1) continue;
|
|
81
|
+
if (box.top <= innerHeight / 2) continue;
|
|
82
|
+
|
|
83
|
+
offset = Math.max(offset, innerHeight - box.top);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return Math.ceil(Math.min(offset, innerHeight / 2));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const prefersReducedMotion = (): boolean =>
|
|
91
|
+
typeof window !== 'undefined' &&
|
|
92
|
+
typeof window.matchMedia === 'function' &&
|
|
93
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* After the jump, keyboard focus goes to the top of the content rather than
|
|
97
|
+
* staying on a button that is about to hide. A temporary `tabindex` makes the
|
|
98
|
+
* target focusable and is removed again once focus leaves it.
|
|
99
|
+
*/
|
|
100
|
+
const focusPageTop = () => {
|
|
101
|
+
const target = document.querySelector<HTMLElement>('main') ?? document.body;
|
|
102
|
+
|
|
103
|
+
if (!target.hasAttribute('tabindex')) {
|
|
104
|
+
target.setAttribute('tabindex', '-1');
|
|
105
|
+
target.addEventListener('blur', () => target.removeAttribute('tabindex'), {
|
|
106
|
+
once: true
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
target.focus({ preventScroll: true });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const PageUpIcon = ({ settings }: { settings: ThemePageUpSettings }) => {
|
|
114
|
+
// `up-arrow` resolves the brand's uploaded override of that icon before the
|
|
115
|
+
// catalogue chevron, exactly as an icon block asking for it would.
|
|
116
|
+
const source = resolveIconSource(settings.pageUpIcon || 'up-arrow', settings);
|
|
117
|
+
|
|
118
|
+
if (source.kind === 'svg' || source.kind === 'url') {
|
|
119
|
+
// Never injected as markup: the icon is shared settings data and an SVG
|
|
120
|
+
// loaded as an image runs no script (utils/svg-image-source.ts).
|
|
121
|
+
const src = toSafeImageSource(
|
|
122
|
+
source.kind === 'svg' ? source.markup : source.url
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (src) {
|
|
126
|
+
return (
|
|
127
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
128
|
+
<img
|
|
129
|
+
src={src}
|
|
130
|
+
width={ICON_SIZE}
|
|
131
|
+
height={ICON_SIZE}
|
|
132
|
+
alt=""
|
|
133
|
+
aria-hidden="true"
|
|
134
|
+
data-page-up-icon="image"
|
|
135
|
+
style={{ display: 'block' }}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<IconGlyph
|
|
143
|
+
{...(source.kind === 'glyph'
|
|
144
|
+
? { glyph: source.glyph }
|
|
145
|
+
: { name: 'up-arrow' })}
|
|
146
|
+
size={ICON_SIZE}
|
|
147
|
+
data-page-up-icon="glyph"
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const PageUpButton = ({ settings }: { settings: ThemePageUpSettings }) => {
|
|
153
|
+
const { locale } = useLocalization();
|
|
154
|
+
const label = String(locale || '')
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.startsWith('tr')
|
|
157
|
+
? 'Sayfanın başına dön'
|
|
158
|
+
: 'Back to top';
|
|
159
|
+
|
|
160
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
161
|
+
const [visible, setVisible] = useState(false);
|
|
162
|
+
const [measuredOffset, setMeasuredOffset] = useState(0);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Visible once the page has scrolled a full viewport. An observed sentinel
|
|
166
|
+
* rather than a scroll handler, as in chrome/scroll-state.tsx: the observer
|
|
167
|
+
* costs nothing until the sentinel crosses the viewport edge. Zero-sized so
|
|
168
|
+
* it can never add a scrollbar to a page shorter than 100vh.
|
|
169
|
+
*/
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (typeof IntersectionObserver === 'undefined') return undefined;
|
|
172
|
+
|
|
173
|
+
const sentinel = document.createElement('div');
|
|
174
|
+
sentinel.setAttribute('data-page-up-sentinel', '');
|
|
175
|
+
sentinel.setAttribute('aria-hidden', 'true');
|
|
176
|
+
sentinel.style.position = 'absolute';
|
|
177
|
+
sentinel.style.top = '100vh';
|
|
178
|
+
sentinel.style.left = '0px';
|
|
179
|
+
sentinel.style.width = '0px';
|
|
180
|
+
sentinel.style.height = '0px';
|
|
181
|
+
sentinel.style.pointerEvents = 'none';
|
|
182
|
+
document.body.appendChild(sentinel);
|
|
183
|
+
|
|
184
|
+
const observer = new IntersectionObserver((entries) => {
|
|
185
|
+
const entry = entries[entries.length - 1];
|
|
186
|
+
if (!entry) return;
|
|
187
|
+
// Out of view AND above it: below the fold (a fresh page) is not
|
|
188
|
+
// "scrolled past".
|
|
189
|
+
setVisible(!entry.isIntersecting && entry.boundingClientRect.top < 0);
|
|
190
|
+
});
|
|
191
|
+
observer.observe(sentinel);
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
observer.disconnect();
|
|
195
|
+
sentinel.remove();
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
/** Re-measured when the button shows and on resize, one frame at most. */
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!visible) return undefined;
|
|
202
|
+
|
|
203
|
+
let frame = 0;
|
|
204
|
+
const measure = () => {
|
|
205
|
+
frame = 0;
|
|
206
|
+
setMeasuredOffset(measureBottomOverlay(buttonRef.current));
|
|
207
|
+
};
|
|
208
|
+
const schedule = () => {
|
|
209
|
+
if (typeof window.requestAnimationFrame !== 'function') {
|
|
210
|
+
measure();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!frame) frame = window.requestAnimationFrame(measure);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
schedule();
|
|
217
|
+
window.addEventListener('resize', schedule);
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
window.removeEventListener('resize', schedule);
|
|
221
|
+
if (frame && typeof window.cancelAnimationFrame === 'function') {
|
|
222
|
+
window.cancelAnimationFrame(frame);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}, [visible]);
|
|
226
|
+
|
|
227
|
+
const onClick = () => {
|
|
228
|
+
window.scrollTo({
|
|
229
|
+
top: 0,
|
|
230
|
+
behavior: prefersReducedMotion() ? 'auto' : 'smooth'
|
|
231
|
+
});
|
|
232
|
+
focusPageTop();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const style = {
|
|
236
|
+
[MEASURED_LIFT_VARIABLE]: `${measuredOffset}px`,
|
|
237
|
+
position: 'fixed',
|
|
238
|
+
// Logical, so a right-to-left page gets it on its trailing side too.
|
|
239
|
+
insetInlineEnd:
|
|
240
|
+
'max(16px, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))',
|
|
241
|
+
bottom: `calc(16px + var(${PAGE_UP_OFFSET_VARIABLE}, var(${MEASURED_LIFT_VARIABLE}, 0px)) + env(safe-area-inset-bottom, 0px))`,
|
|
242
|
+
zIndex: 30,
|
|
243
|
+
width: 44,
|
|
244
|
+
height: 44,
|
|
245
|
+
margin: 0,
|
|
246
|
+
padding: 0,
|
|
247
|
+
display: 'inline-flex',
|
|
248
|
+
alignItems: 'center',
|
|
249
|
+
justifyContent: 'center',
|
|
250
|
+
boxSizing: 'border-box',
|
|
251
|
+
borderRadius: '50%',
|
|
252
|
+
border: '1px solid var(--theme-border, #e2e2e2)',
|
|
253
|
+
background: 'var(--theme-background, #fff)',
|
|
254
|
+
color: 'var(--theme-heading, #111)',
|
|
255
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
|
|
256
|
+
cursor: 'pointer',
|
|
257
|
+
opacity: visible ? 1 : 0,
|
|
258
|
+
visibility: visible ? 'visible' : 'hidden',
|
|
259
|
+
pointerEvents: visible ? 'auto' : 'none',
|
|
260
|
+
// A fade, not a movement, so it stays under reduced motion.
|
|
261
|
+
transition: 'opacity 150ms ease, visibility 150ms ease'
|
|
262
|
+
} as React.CSSProperties;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<button
|
|
266
|
+
ref={buttonRef}
|
|
267
|
+
type="button"
|
|
268
|
+
data-theme-page-up=""
|
|
269
|
+
aria-label={label}
|
|
270
|
+
title={label}
|
|
271
|
+
onClick={onClick}
|
|
272
|
+
// Hidden, it must not be reachable either: no tab stop, nothing read.
|
|
273
|
+
aria-hidden={visible ? undefined : true}
|
|
274
|
+
tabIndex={visible ? undefined : -1}
|
|
275
|
+
style={style}
|
|
276
|
+
>
|
|
277
|
+
<PageUpIcon settings={settings} />
|
|
278
|
+
</button>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export interface ThemePageUpProps {
|
|
283
|
+
/**
|
|
284
|
+
* The theme settings, whole or already narrowed with
|
|
285
|
+
* `readThemePageUpSettings`; only the page-up keys are read.
|
|
286
|
+
*/
|
|
287
|
+
settings?: Record<string, unknown> | null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Renders nothing unless `pageUpEnabled` is true. In the designer the
|
|
292
|
+
* editor's live settings replace the saved ones as soon as they arrive.
|
|
293
|
+
*/
|
|
294
|
+
export function ThemePageUp({ settings = null }: ThemePageUpProps) {
|
|
295
|
+
const saved = useMemo(() => readThemePageUpSettings(settings), [settings]);
|
|
296
|
+
const effective = useEditorThemeSettings(saved);
|
|
297
|
+
|
|
298
|
+
if (effective.pageUpEnabled !== true) return null;
|
|
299
|
+
|
|
300
|
+
return <PageUpButton settings={effective} />;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default ThemePageUp;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { parseEditorMessage } from '../editor-bridge';
|
|
6
|
+
import {
|
|
7
|
+
readThemePageUpSettings,
|
|
8
|
+
type ThemePageUpSettings
|
|
9
|
+
} from '../utils/theme-settings-readers';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The page-up settings as the EDITOR currently has them.
|
|
13
|
+
*
|
|
14
|
+
* The back-to-top button is mounted by `ThemeModals`, a server component
|
|
15
|
+
* outside every placeholder, so it never sees the `LOAD_THEME`/`UPDATE_THEME`
|
|
16
|
+
* frames a placeholder wrapper turns into live settings. Without this the
|
|
17
|
+
* designer would flip "Page up oku" and see nothing until the theme was saved
|
|
18
|
+
* and the canvas reloaded.
|
|
19
|
+
*
|
|
20
|
+
* Module state, like `overlays/use-editor-modals.ts` and for the same reason:
|
|
21
|
+
* every mount, in whichever React root, must agree on one answer.
|
|
22
|
+
*/
|
|
23
|
+
let editorSettings: ThemePageUpSettings | null = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Serialized form of the slice. `UPDATE_THEME` arrives on every keystroke
|
|
27
|
+
* anywhere in the theme and almost none of those touch these keys, so the
|
|
28
|
+
* button only re-renders when the slice actually changes.
|
|
29
|
+
*/
|
|
30
|
+
let signature: string | null = null;
|
|
31
|
+
|
|
32
|
+
const listeners = new Set<() => void>();
|
|
33
|
+
|
|
34
|
+
const setEditorSettings = (next: ThemePageUpSettings) => {
|
|
35
|
+
const nextSignature = JSON.stringify(next);
|
|
36
|
+
if (nextSignature === signature) return;
|
|
37
|
+
|
|
38
|
+
signature = nextSignature;
|
|
39
|
+
editorSettings = next;
|
|
40
|
+
listeners.forEach((listener) => listener());
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let listening = false;
|
|
44
|
+
|
|
45
|
+
const handleMessage = (event: MessageEvent) => {
|
|
46
|
+
const message = parseEditorMessage(event);
|
|
47
|
+
if (!message) return;
|
|
48
|
+
if (message.type !== 'LOAD_THEME' && message.type !== 'UPDATE_THEME') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const settings = (
|
|
53
|
+
message.data as { theme?: { settings?: unknown } } | undefined
|
|
54
|
+
)?.theme?.settings;
|
|
55
|
+
// A frame without settings says nothing about them; the placeholder
|
|
56
|
+
// wrapper ignores it the same way.
|
|
57
|
+
if (!settings || typeof settings !== 'object') return;
|
|
58
|
+
|
|
59
|
+
setEditorSettings(
|
|
60
|
+
readThemePageUpSettings(settings as Record<string, unknown>)
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const startListening = () => {
|
|
65
|
+
if (listening || typeof window === 'undefined') return;
|
|
66
|
+
listening = true;
|
|
67
|
+
window.addEventListener('message', handleMessage);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The editor's page-up slice once one has been heard, otherwise `initial` —
|
|
72
|
+
* which is what the server rendered, so the first client render matches it.
|
|
73
|
+
*/
|
|
74
|
+
export const useEditorThemeSettings = (
|
|
75
|
+
initial: ThemePageUpSettings
|
|
76
|
+
): ThemePageUpSettings => {
|
|
77
|
+
const [heard, setHeard] = useState<ThemePageUpSettings | null>(null);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
startListening();
|
|
81
|
+
|
|
82
|
+
const listener = () => setHeard(editorSettings);
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
// A frame may have landed between render and this effect.
|
|
85
|
+
if (editorSettings) listener();
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
listeners.delete(listener);
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
return heard ?? initial;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Test-only: drop what was heard and the listener. */
|
|
96
|
+
export const __resetEditorThemeSettingsForTests = (): void => {
|
|
97
|
+
editorSettings = null;
|
|
98
|
+
signature = null;
|
|
99
|
+
listeners.clear();
|
|
100
|
+
if (listening && typeof window !== 'undefined') {
|
|
101
|
+
window.removeEventListener('message', handleMessage);
|
|
102
|
+
}
|
|
103
|
+
listening = false;
|
|
104
|
+
};
|
|
@@ -19,6 +19,13 @@ import {
|
|
|
19
19
|
HIDE_IN_CARD_VIEW_ATTRIBUTE,
|
|
20
20
|
HIDE_IN_CARD_VIEW_PROPERTY
|
|
21
21
|
} from '../utils/card-media';
|
|
22
|
+
// Own-colour / own-typography markers for the theme's rich-text
|
|
23
|
+
// element defaults.
|
|
24
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
25
|
+
import {
|
|
26
|
+
type ThemeOwnTextStyle,
|
|
27
|
+
themeOwnStyleAttributes
|
|
28
|
+
} from '../utils/theme-settings-css';
|
|
22
29
|
|
|
23
30
|
/** Elements that already act on Enter/Space themselves. */
|
|
24
31
|
export const NATIVE_KEYBOARD_CONTROLS =
|
|
@@ -39,6 +46,11 @@ interface WithDesignerFeaturesProps {
|
|
|
39
46
|
onToggleVisibility?: () => void;
|
|
40
47
|
onDelete?: () => void;
|
|
41
48
|
onRename?: (newLabel: string) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Text colour or typography the caller paints on this wrapper with classes,
|
|
51
|
+
* which neither the block's styles nor `style` can show.
|
|
52
|
+
*/
|
|
53
|
+
ownTextStyle?: ThemeOwnTextStyle;
|
|
42
54
|
// Note: repo has mixed React type versions; keep this permissive to avoid ReactNode incompatibilities.
|
|
43
55
|
children: any;
|
|
44
56
|
}
|
|
@@ -58,9 +70,14 @@ export function WithDesignerFeatures({
|
|
|
58
70
|
onToggleVisibility,
|
|
59
71
|
onDelete,
|
|
60
72
|
onRename,
|
|
73
|
+
ownTextStyle,
|
|
61
74
|
children
|
|
62
75
|
}: WithDesignerFeaturesProps) {
|
|
63
76
|
const blockRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
// Read here, not in the renderers: this wrapper encloses every
|
|
78
|
+
// element the block's root styles reach (its own box, or the inner root of
|
|
79
|
+
// a text/link/button), so a marker on it covers the authored values.
|
|
80
|
+
const themeSettings = useThemeSettingsContext();
|
|
64
81
|
const actionBlockId = block.styleSourceId || block.id;
|
|
65
82
|
const { handleClick } = useDesignerFeatures({
|
|
66
83
|
blockId: actionBlockId,
|
|
@@ -182,6 +199,15 @@ export function WithDesignerFeatures({
|
|
|
182
199
|
...(ownsAction && !isDesigner ? { cursor: 'pointer' } : null)
|
|
183
200
|
};
|
|
184
201
|
|
|
202
|
+
// Data-pz-own-color / data-pz-own-type, so the theme's rich-text
|
|
203
|
+
// defaults leave the colour and typography this block authored to inherit.
|
|
204
|
+
// `style` counts too: a section paints a code-owned box's text colour there.
|
|
205
|
+
const ownStyleAttributes = themeOwnStyleAttributes(
|
|
206
|
+
{ ...block.styles, ...style },
|
|
207
|
+
themeSettings,
|
|
208
|
+
ownTextStyle
|
|
209
|
+
);
|
|
210
|
+
|
|
185
211
|
return (
|
|
186
212
|
<div
|
|
187
213
|
ref={blockRef}
|
|
@@ -191,6 +217,7 @@ export function WithDesignerFeatures({
|
|
|
191
217
|
{...(hideInCardView
|
|
192
218
|
? { [HIDE_IN_CARD_VIEW_ATTRIBUTE]: hideInCardView }
|
|
193
219
|
: null)}
|
|
220
|
+
{...ownStyleAttributes}
|
|
194
221
|
className={clsx(
|
|
195
222
|
'relative',
|
|
196
223
|
className,
|