@akinon/pz-theme 2.0.36-beta.0 → 2.0.36
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 +5 -4
- package/jest/__mocks__/hookform-resolvers.js +5 -0
- package/jest/__mocks__/next-auth-react.js +5 -0
- package/jest.config.js +3 -0
- package/package.json +4 -4
- package/src/blocks/accordion-block.tsx +35 -3
- package/src/blocks/commerce-format.ts +220 -0
- package/src/blocks/icon-block.tsx +6 -1
- package/src/blocks/image-gallery-block.tsx +1 -1
- package/src/blocks/link-block.tsx +9 -1
- package/src/blocks/slider-block.tsx +2 -1
- package/src/blocks/tab-block.tsx +15 -2
- package/src/blocks/video-block.tsx +6 -0
- package/src/components/__tests__/part-state-preview-scope.test.tsx +148 -0
- package/src/components/action-toolbar.tsx +182 -3
- package/src/components/part-state-preview.tsx +92 -0
- package/src/hooks/__tests__/use-overlay.test.tsx +503 -0
- package/src/hooks/use-designer-features.tsx +16 -1
- package/src/hooks/use-overlay.ts +538 -0
- package/src/hooks/use-visibility-context.ts +14 -2
- package/src/sections/__tests__/divider-section.test.tsx +28 -1
- package/src/sections/contact-form-section.tsx +4 -1
- package/src/sections/coupon-banner-section.tsx +2 -1
- package/src/sections/featured-product-spotlight-section.tsx +4 -4
- package/src/sections/find-in-store-section.tsx +2 -1
- package/src/sections/hover-showcase-section.tsx +2 -1
- package/src/sections/installment-options-section.tsx +2 -1
- package/src/sections/order-tracking-lookup-section.tsx +6 -1
- package/src/sections/posts-slider-section.tsx +8 -3
- package/src/sections/pre-order-launch-banner-section.tsx +2 -1
- package/src/sections/tabs-section.tsx +7 -13
- package/src/theme-block.tsx +81 -16
- package/src/theme-placeholder-client.tsx +8 -1
- package/src/theme-placeholder-wrapper.tsx +17 -3
- package/src/theme-placeholder.tsx +51 -5
- package/src/theme-section.tsx +20 -4
- package/src/utils/__tests__/breakpoint-cascade.test.ts +351 -0
- package/src/utils/__tests__/group-box-chrome.test.ts +83 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +1178 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +298 -0
- package/src/utils/breakpoints.ts +116 -0
- package/src/utils/custom-css.ts +233 -0
- package/src/utils/index.ts +353 -42
- package/src/utils/iterator-utils.ts +675 -62
- package/src/utils/page-context-discovery.test.ts +188 -0
- package/src/utils/page-context-discovery.ts +48 -4
- package/src/utils/part-styles.ts +229 -0
- package/src/utils/row-visibility.ts +273 -0
- package/src/utils/visibility-rules.test.ts +579 -0
- package/src/utils/visibility-rules.ts +346 -3
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getResponsiveValue,
|
|
10
10
|
resolveThemeCssVariables
|
|
11
11
|
} from '../utils';
|
|
12
|
+
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
12
13
|
|
|
13
14
|
import { Section } from '../theme-section';
|
|
14
15
|
import { WithDesignerFeatures } from '../components/with-designer-features';
|
|
@@ -115,7 +116,7 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
|
|
|
115
116
|
themeSettings
|
|
116
117
|
);
|
|
117
118
|
|
|
118
|
-
const isMobile = currentBreakpoint
|
|
119
|
+
const isMobile = isNarrowBreakpoint(currentBreakpoint);
|
|
119
120
|
|
|
120
121
|
const items = (section.blocks || []).filter(
|
|
121
122
|
(b: Block) => b.type === 'hover-showcase-item'
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getResponsiveValue,
|
|
21
21
|
resolveThemeCssVariables
|
|
22
22
|
} from '../utils';
|
|
23
|
+
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
23
24
|
|
|
24
25
|
import { postToEditor } from '../editor-bridge';
|
|
25
26
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -417,7 +418,7 @@ export default function InstallmentOptionsSection({
|
|
|
417
418
|
}: InstallmentOptionsSectionProps) {
|
|
418
419
|
const themeSettings = useThemeSettingsContext();
|
|
419
420
|
const { t } = useLocalization();
|
|
420
|
-
const isMobile = currentBreakpoint
|
|
421
|
+
const isMobile = isNarrowBreakpoint(currentBreakpoint);
|
|
421
422
|
|
|
422
423
|
const maxWidth = getResponsiveValue(
|
|
423
424
|
section.styles?.['max-width'],
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getResponsiveValue,
|
|
23
23
|
resolveThemeCssVariables
|
|
24
24
|
} from '../utils';
|
|
25
|
+
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
25
26
|
|
|
26
27
|
import { postToEditor } from '../editor-bridge';
|
|
27
28
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -55,8 +56,12 @@ const readProperty = (
|
|
|
55
56
|
if (typeof raw === 'string') return raw;
|
|
56
57
|
if (typeof raw === 'object') {
|
|
57
58
|
const responsive = raw as Record<string, unknown>;
|
|
59
|
+
// Same order as the emitted cascade: a tablet inherits mobile before it
|
|
60
|
+
// inherits desktop. Reading desktop first made 768-1023 show one thing and
|
|
61
|
+
// the stylesheet paint another.
|
|
58
62
|
const picked =
|
|
59
63
|
responsive[breakpoint] ??
|
|
64
|
+
(breakpoint === 'tablet' ? responsive.mobile : undefined) ??
|
|
60
65
|
responsive.desktop ??
|
|
61
66
|
responsive.mobile ??
|
|
62
67
|
Object.values(responsive)[0];
|
|
@@ -1051,7 +1056,7 @@ export default function OrderTrackingLookupSection({
|
|
|
1051
1056
|
const trackingUrl = resolveTrackingUrl(lookupOrder);
|
|
1052
1057
|
const trackingNumber = resolveTrackingNumber(lookupOrder);
|
|
1053
1058
|
const localeCode = locale || 'en';
|
|
1054
|
-
const isMobile = currentBreakpoint
|
|
1059
|
+
const isMobile = isNarrowBreakpoint(currentBreakpoint);
|
|
1055
1060
|
|
|
1056
1061
|
return (
|
|
1057
1062
|
<div
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getCSSStyles,
|
|
13
13
|
getResponsiveValue
|
|
14
14
|
} from '../utils';
|
|
15
|
+
import { DESKTOP_MIN_WIDTH, TABLET_MIN_WIDTH } from '../utils/breakpoints';
|
|
15
16
|
|
|
16
17
|
import { postToEditor } from '../editor-bridge';
|
|
17
18
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -275,19 +276,23 @@ const PostsSliderSection: React.FC<PostsSliderSectionProps> = ({
|
|
|
275
276
|
? tabletItems
|
|
276
277
|
: desktopItems;
|
|
277
278
|
|
|
279
|
+
// This section had the three bands before the system did, and it picked the
|
|
280
|
+
// same two lines by hand. They come from the shared constants now, so the
|
|
281
|
+
// carousel switches at the width the stylesheet switches at rather than at a
|
|
282
|
+
// number that merely used to agree.
|
|
278
283
|
const responsive = {
|
|
279
284
|
desktop: {
|
|
280
|
-
breakpoint: { max: 3000, min:
|
|
285
|
+
breakpoint: { max: 3000, min: DESKTOP_MIN_WIDTH },
|
|
281
286
|
items: desktopItems,
|
|
282
287
|
slidesToSlide: desktopSlides
|
|
283
288
|
},
|
|
284
289
|
tablet: {
|
|
285
|
-
breakpoint: { max:
|
|
290
|
+
breakpoint: { max: DESKTOP_MIN_WIDTH - 1, min: TABLET_MIN_WIDTH },
|
|
286
291
|
items: tabletItems,
|
|
287
292
|
slidesToSlide: tabletSlides
|
|
288
293
|
},
|
|
289
294
|
mobile: {
|
|
290
|
-
breakpoint: { max:
|
|
295
|
+
breakpoint: { max: TABLET_MIN_WIDTH - 1, min: 0 },
|
|
291
296
|
items: mobileItems,
|
|
292
297
|
slidesToSlide: mobileSlides
|
|
293
298
|
}
|
|
@@ -9,6 +9,7 @@ import { useThemeSettingsContext } from '../theme-settings-context';
|
|
|
9
9
|
import { safeDeepClone } from '../utils/safe-clone';
|
|
10
10
|
import { Section } from '../theme-section';
|
|
11
11
|
import { getCSSStyles, getResponsiveValue } from '../utils';
|
|
12
|
+
import { isNarrowBreakpoint } from '../utils/breakpoints';
|
|
12
13
|
|
|
13
14
|
import { postToEditor } from '../editor-bridge';
|
|
14
15
|
interface PreOrderLaunchBannerSectionProps {
|
|
@@ -352,7 +353,7 @@ export default function PreOrderLaunchBannerSection({
|
|
|
352
353
|
return {
|
|
353
354
|
...baseStyles,
|
|
354
355
|
flexDirection:
|
|
355
|
-
currentBreakpoint
|
|
356
|
+
isNarrowBreakpoint(currentBreakpoint) ? 'column-reverse' : 'row-reverse'
|
|
356
357
|
};
|
|
357
358
|
}, [
|
|
358
359
|
currentBreakpoint,
|
|
@@ -3,6 +3,7 @@ import { Section } from '../theme-section';
|
|
|
3
3
|
import ThemeBlock from '../theme-block';
|
|
4
4
|
import {
|
|
5
5
|
getCSSStyles,
|
|
6
|
+
getResponsiveValue as readResponsiveValue,
|
|
6
7
|
resolveThemeCssVariables,
|
|
7
8
|
resolveThemeStyleObject
|
|
8
9
|
} from '../utils';
|
|
@@ -42,20 +43,13 @@ const TabsSection: React.FC<TabsSectionProps> = ({
|
|
|
42
43
|
)
|
|
43
44
|
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
44
45
|
|
|
46
|
+
// Reads through the shared chain rather than a local desktop fallback: a
|
|
47
|
+
// tablet inherits the mobile value, which is what the emitted stylesheet
|
|
48
|
+
// does at 768-1023. A private reader here made the markup disagree with the
|
|
49
|
+
// CSS on compositions nobody had touched.
|
|
45
50
|
const getResponsiveValue = useCallback(
|
|
46
|
-
<T,>(value: unknown, fallback: T): T =>
|
|
47
|
-
|
|
48
|
-
return fallback;
|
|
49
|
-
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
50
|
-
const responsiveValue = value as Record<string, T | undefined> & {
|
|
51
|
-
desktop?: T;
|
|
52
|
-
};
|
|
53
|
-
const matchedValue =
|
|
54
|
-
responsiveValue[currentBreakpoint] ?? responsiveValue.desktop;
|
|
55
|
-
return matchedValue !== undefined ? (matchedValue as T) : fallback;
|
|
56
|
-
}
|
|
57
|
-
return value as T;
|
|
58
|
-
},
|
|
51
|
+
<T,>(value: unknown, fallback: T): T =>
|
|
52
|
+
readResponsiveValue(value, currentBreakpoint, fallback) as T,
|
|
59
53
|
[currentBreakpoint]
|
|
60
54
|
);
|
|
61
55
|
|
package/src/theme-block.tsx
CHANGED
|
@@ -3,9 +3,35 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import blockRendererRegistry from './blocks/block-renderer-registry';
|
|
5
5
|
import { WithDesignerFeatures } from './components/with-designer-features';
|
|
6
|
-
import { isPublishWindowVisible } from './utils';
|
|
6
|
+
import { getResponsiveValue, isPublishWindowVisible } from './utils';
|
|
7
7
|
import { devWarn } from './utils/dev-warn';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Narrows what a designer typed into something usable as a document-wide id.
|
|
11
|
+
*
|
|
12
|
+
* The value ends up in three places at once — an `#fragment` in a link, an
|
|
13
|
+
* `id` attribute, and any `#id` selector written later — so it is reduced to
|
|
14
|
+
* the intersection all three accept. Stripped: whitespace (runs become a
|
|
15
|
+
* single hyphen first, so "Size guide" stays readable as "Size-guide"), and
|
|
16
|
+
* every character outside `[A-Za-z0-9_-]`. That drops the duplicate-hostile
|
|
17
|
+
* ones specifically: `#` and `.` and `:` and `[` `]` would split a selector,
|
|
18
|
+
* `/` and `?` would end a URL fragment, and quotes would close an attribute.
|
|
19
|
+
* Leading characters that are not a letter or underscore go too, because an id
|
|
20
|
+
* must not start with a digit. Nothing left means no id at all.
|
|
21
|
+
*
|
|
22
|
+
* Two blocks given the same id is the designer's problem, not a crash: the
|
|
23
|
+
* markup is still valid HTML and the browser simply jumps to the first one.
|
|
24
|
+
*/
|
|
25
|
+
export const sanitizeHtmlId = (value: unknown): string => {
|
|
26
|
+
if (typeof value !== 'string') return '';
|
|
27
|
+
|
|
28
|
+
return value
|
|
29
|
+
.trim()
|
|
30
|
+
.replace(/\s+/g, '-')
|
|
31
|
+
.replace(/[^A-Za-z0-9_-]/g, '')
|
|
32
|
+
.replace(/^[^A-Za-z_]+/, '');
|
|
33
|
+
};
|
|
34
|
+
|
|
9
35
|
export interface Block {
|
|
10
36
|
id: string;
|
|
11
37
|
styleSourceId?: string;
|
|
@@ -76,6 +102,28 @@ export default function ThemeBlock({
|
|
|
76
102
|
return null;
|
|
77
103
|
}
|
|
78
104
|
|
|
105
|
+
const htmlId = sanitizeHtmlId(
|
|
106
|
+
getResponsiveValue(block.properties?.htmlId, currentBreakpoint, '')
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const renderedBlock = (
|
|
110
|
+
<BlockRenderer
|
|
111
|
+
block={block}
|
|
112
|
+
placeholderId={placeholderId}
|
|
113
|
+
sectionId={sectionId}
|
|
114
|
+
isDesigner={isDesigner}
|
|
115
|
+
isSelected={isSelected}
|
|
116
|
+
selectedBlockId={selectedBlockId}
|
|
117
|
+
currentBreakpoint={currentBreakpoint}
|
|
118
|
+
onMoveUp={onMoveUp}
|
|
119
|
+
onMoveDown={onMoveDown}
|
|
120
|
+
onDuplicate={onDuplicate}
|
|
121
|
+
onToggleVisibility={onToggleVisibility}
|
|
122
|
+
onDelete={onDelete}
|
|
123
|
+
onRename={onRename}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
|
|
79
127
|
return (
|
|
80
128
|
<WithDesignerFeatures
|
|
81
129
|
block={block}
|
|
@@ -91,21 +139,38 @@ export default function ThemeBlock({
|
|
|
91
139
|
onDelete={onDelete}
|
|
92
140
|
onRename={onRename}
|
|
93
141
|
>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
{htmlId ? (
|
|
143
|
+
<>
|
|
144
|
+
{/*
|
|
145
|
+
The id rides a zero-size marker rather than the block's own
|
|
146
|
+
element. The wrapper this renders inside is the block's box, but it
|
|
147
|
+
belongs to WithDesignerFeatures, and every alternative that owns an
|
|
148
|
+
element changes the layout of a block that asked only for a name:
|
|
149
|
+
an extra flex item adds a gap, and `display: contents` has no box
|
|
150
|
+
at all, which is precisely what a browser needs to scroll to a
|
|
151
|
+
fragment. Absolute positioning keeps the marker out of flow (no
|
|
152
|
+
flex item, no grid cell, no space) while still generating the box
|
|
153
|
+
that makes the jump land — at the block's top-left corner, since
|
|
154
|
+
that wrapper is always `relative`.
|
|
155
|
+
*/}
|
|
156
|
+
<span
|
|
157
|
+
id={htmlId}
|
|
158
|
+
data-theme-anchor=""
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
style={{
|
|
161
|
+
position: 'absolute',
|
|
162
|
+
top: 0,
|
|
163
|
+
left: 0,
|
|
164
|
+
width: 0,
|
|
165
|
+
height: 0,
|
|
166
|
+
overflow: 'hidden'
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
{renderedBlock}
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
renderedBlock
|
|
173
|
+
)}
|
|
109
174
|
</WithDesignerFeatures>
|
|
110
175
|
);
|
|
111
176
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { useVisibilityContext } from './hooks/use-visibility-context';
|
|
11
11
|
import { applyVisibilityRulesToSections } from './utils/visibility-rules';
|
|
12
12
|
import { isPublishWindowVisible } from './utils/publish-window';
|
|
13
|
+
import { useThemePageContext } from './theme-page-context';
|
|
13
14
|
|
|
14
15
|
import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
|
|
15
16
|
import { PreviewToEditorMessageType } from '@akinon/theme-contract';
|
|
@@ -46,7 +47,13 @@ export default function ThemePlaceholderClient({
|
|
|
46
47
|
const [isDesigner, setIsDesigner] = useState(
|
|
47
48
|
() => typeof window !== 'undefined' && window.__themeEditorDesigner === true
|
|
48
49
|
);
|
|
49
|
-
|
|
50
|
+
// Null unless a ThemePageContextProvider sits above this placeholder; data
|
|
51
|
+
// conditions then have nothing to read and resolve the same way on both sides.
|
|
52
|
+
const pageContext = useThemePageContext();
|
|
53
|
+
const visibilityContext = useVisibilityContext(currentBreakpoint, {
|
|
54
|
+
pageContext,
|
|
55
|
+
isDesigner
|
|
56
|
+
});
|
|
50
57
|
const renderedSections = applyVisibilityRulesToSections(
|
|
51
58
|
sections,
|
|
52
59
|
visibilityContext
|
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
unregisterPlaceholder
|
|
10
10
|
} from './placeholder-registry';
|
|
11
11
|
import { useThemePageContext } from './theme-page-context';
|
|
12
|
+
import { ThemePageContextProvider } from './theme-page-context';
|
|
12
13
|
import { ThemeSettingsProvider } from './theme-settings-context';
|
|
13
14
|
import { generateThemeCSS } from './utils';
|
|
15
|
+
import { breakpointForWidth } from './utils/breakpoints';
|
|
16
|
+
import { PartStatePreview } from './components/part-state-preview';
|
|
14
17
|
import { useVisibilityContext } from './hooks/use-visibility-context';
|
|
15
18
|
import { createPageContextDiscovery } from './utils/page-context-discovery';
|
|
16
19
|
import { applyVisibilityRulesToSections } from './utils/visibility-rules';
|
|
@@ -427,8 +430,10 @@ export default function ThemePlaceholderWrapper({
|
|
|
427
430
|
if (!checkIsInIframe()) {
|
|
428
431
|
const checkBreakpoint = () => {
|
|
429
432
|
const width = window.innerWidth;
|
|
430
|
-
//
|
|
431
|
-
|
|
433
|
+
// The cutoffs live in utils/breakpoints, alongside the media queries
|
|
434
|
+
// the generated stylesheet is built from, so the viewport this reports
|
|
435
|
+
// and the query that is matching it can never drift apart.
|
|
436
|
+
const newBreakpoint = breakpointForWidth(width);
|
|
432
437
|
|
|
433
438
|
setCurrentBreakpoint((prev) => {
|
|
434
439
|
if (prev !== newBreakpoint) {
|
|
@@ -708,14 +713,22 @@ export default function ThemePlaceholderWrapper({
|
|
|
708
713
|
generateThemeCSS(sections, isDesigner ? currentBreakpoint : undefined),
|
|
709
714
|
[sections, isDesigner, currentBreakpoint]
|
|
710
715
|
);
|
|
711
|
-
const visibilityContext = useVisibilityContext(currentBreakpoint
|
|
716
|
+
const visibilityContext = useVisibilityContext(currentBreakpoint, {
|
|
717
|
+
pageContext: resolvedPageContext,
|
|
718
|
+
isDesigner
|
|
719
|
+
});
|
|
712
720
|
const renderedSections = useMemo(
|
|
713
721
|
() => applyVisibilityRulesToSections(sections, visibilityContext),
|
|
714
722
|
[sections, visibilityContext]
|
|
715
723
|
);
|
|
716
724
|
|
|
717
725
|
return (
|
|
726
|
+
// Blocks read commerce data through useThemePageContext(); the provider
|
|
727
|
+
// has to sit above the rendered sections or every data-driven block
|
|
728
|
+
// falls back to its empty state.
|
|
729
|
+
<ThemePageContextProvider value={resolvedPageContext}>
|
|
718
730
|
<ThemeSettingsProvider value={themeSettings}>
|
|
731
|
+
<PartStatePreview isDesigner={isDesigner} />
|
|
719
732
|
{dynamicCSS && <style dangerouslySetInnerHTML={{ __html: dynamicCSS }} />}
|
|
720
733
|
<div
|
|
721
734
|
data-placeholder={slug}
|
|
@@ -783,5 +796,6 @@ export default function ThemePlaceholderWrapper({
|
|
|
783
796
|
))}
|
|
784
797
|
</div>
|
|
785
798
|
</ThemeSettingsProvider>
|
|
799
|
+
</ThemePageContextProvider>
|
|
786
800
|
);
|
|
787
801
|
}
|
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
3
|
+
import * as serverData from '@akinon/next/data/server';
|
|
4
|
+
import appFetch from '@akinon/next/utils/app-fetch';
|
|
5
|
+
import { ServerVariables } from '@akinon/next/utils/server-variables';
|
|
6
|
+
|
|
7
|
+
const { getWidgetData } = serverData;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* v1 cores ship a smaller server data layer: only `getWidgetData` exists,
|
|
11
|
+
* `getWidgetSchemaData` and `getCollectionWidgetData` arrived with v2.
|
|
12
|
+
*
|
|
13
|
+
* The endpoints are identical on both backends — v1 just never wrapped them.
|
|
14
|
+
* Degrading to "no data" would render every published section *without its
|
|
15
|
+
* blocks* (a section's block tree lives in the widget schema, not in the
|
|
16
|
+
* widget), so on v1 we call the REST endpoints directly. This is what keeps
|
|
17
|
+
* composed pages rendering on the brands still running v1.
|
|
18
|
+
*/
|
|
19
|
+
type ServerDataFn = (<T>(args: never) => Promise<T | null>) | undefined;
|
|
20
|
+
|
|
21
|
+
const optionalServerFn = (name: string): ServerDataFn =>
|
|
22
|
+
(serverData as unknown as Record<string, ServerDataFn>)[name];
|
|
23
|
+
|
|
24
|
+
const directFetch = async <T,>(url: string): Promise<T | null> => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await appFetch<T>({
|
|
27
|
+
url,
|
|
28
|
+
locale: ServerVariables.locale,
|
|
29
|
+
currency: ServerVariables.currency
|
|
30
|
+
});
|
|
31
|
+
return result ?? null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getCollectionWidgetData = async <T,>(args: {
|
|
38
|
+
slug: string;
|
|
39
|
+
}): Promise<T | null> => {
|
|
40
|
+
const fn = optionalServerFn('getCollectionWidgetData');
|
|
41
|
+
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
42
|
+
if (!args?.slug) return null;
|
|
43
|
+
return directFetch<T>(`/product_collection_widgets/${args.slug}/`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getWidgetSchemaData = async <T,>(args: {
|
|
47
|
+
widgetSlug: string;
|
|
48
|
+
}): Promise<T | null> => {
|
|
49
|
+
const fn = optionalServerFn('getWidgetSchemaData');
|
|
50
|
+
if (fn) return (await fn<T>(args as never)) ?? null;
|
|
51
|
+
if (!args?.widgetSlug) return null;
|
|
52
|
+
return directFetch<T>(`/widgets/${args.widgetSlug}/schema/`);
|
|
53
|
+
};
|
|
8
54
|
import ThemePlaceholderWrapper from './theme-placeholder-wrapper';
|
|
9
55
|
import { Section } from './theme-section';
|
|
10
56
|
import { generateThemeCSS } from './utils';
|
package/src/theme-section.tsx
CHANGED
|
@@ -9,9 +9,12 @@ import SectionWrapper from './sections/section-wrapper';
|
|
|
9
9
|
import sectionRendererRegistry from './sections/section-renderer-registry';
|
|
10
10
|
import {
|
|
11
11
|
buildIteratorBlock,
|
|
12
|
-
resolveBlockBindings
|
|
12
|
+
resolveBlockBindings,
|
|
13
|
+
setBindingLocale
|
|
13
14
|
} from './utils/iterator-utils';
|
|
15
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
14
16
|
import { devWarn } from './utils/dev-warn';
|
|
17
|
+
import { isNarrowBreakpoint } from './utils/breakpoints';
|
|
15
18
|
import { safeDeepClone } from './utils/safe-clone';
|
|
16
19
|
|
|
17
20
|
import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
|
|
@@ -117,6 +120,13 @@ export default function ThemeSection({
|
|
|
117
120
|
|
|
118
121
|
const sectionRef = useRef<HTMLElement>(null);
|
|
119
122
|
|
|
123
|
+
// Bound prices are formatted deep inside the binding walk, which has no
|
|
124
|
+
// React context to read. Publishing the storefront's locale before the
|
|
125
|
+
// bindings resolve is what stops an en-GB brand rendering Turkish number
|
|
126
|
+
// grouping in every repeated row.
|
|
127
|
+
const { locale } = useLocalization();
|
|
128
|
+
setBindingLocale(locale);
|
|
129
|
+
|
|
120
130
|
useEffect(() => {
|
|
121
131
|
if (!isDesigner) return;
|
|
122
132
|
|
|
@@ -615,9 +625,15 @@ export default function ThemeSection({
|
|
|
615
625
|
)
|
|
616
626
|
)
|
|
617
627
|
);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
628
|
+
// This section offers a desktop count and a mobile count and no tablet
|
|
629
|
+
// control, so a tablet inherits the mobile count the same way every
|
|
630
|
+
// other unauthored value does. Synthesising `min(desktopCount, 2)` here
|
|
631
|
+
// put FEWER columns at 900px than the phone shows whenever the mobile
|
|
632
|
+
// count was above two — a regression on compositions nobody had edited.
|
|
633
|
+
const tabletCount = mobileCount;
|
|
634
|
+
const activeCount = isNarrowBreakpoint(currentBreakpoint)
|
|
635
|
+
? mobileCount
|
|
636
|
+
: desktopCount;
|
|
621
637
|
return {
|
|
622
638
|
desktop: desktopCount,
|
|
623
639
|
tablet: tabletCount,
|