@akinon/pz-theme 2.0.130-rc.0 → 2.0.130
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 +10 -35
- package/package.json +4 -4
- package/src/chrome/__tests__/theme-config-source.test.ts +73 -8
- package/src/chrome/error-composition-source.ts +5 -1
- package/src/chrome/preview-snapshot-source.ts +2 -0
- package/src/chrome/theme-config-source.ts +31 -2
- package/src/load-placeholder-sections.ts +15 -3
- package/src/overlays/use-overlay-breakpoint.ts +4 -2
- package/src/theme-placeholder-wrapper.tsx +49 -7
- package/src/theme-placeholder.tsx +9 -2
- package/src/utils/__tests__/custom-breakpoints.test.ts +326 -0
- package/src/utils/breakpoints.ts +194 -2
- package/src/utils/index.ts +174 -42
- package/src/utils/property-switches.ts +2 -2
- package/src/utils/section-content-width.ts +2 -2
- package/src/utils/visibility-rules.ts +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.130
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Updated dependencies [b55acb768]
|
|
14
|
-
- Updated dependencies [760258c1c]
|
|
15
|
-
- Updated dependencies [143be2b9d]
|
|
16
|
-
- Updated dependencies [7889b08fe]
|
|
17
|
-
- Updated dependencies [9f8cd3bc5]
|
|
18
|
-
- Updated dependencies [bfafa3f49]
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b1]
|
|
34
|
-
- Updated dependencies [1595d458]
|
|
35
|
-
- Updated dependencies [dca12811]
|
|
36
|
-
- @akinon/next@2.0.130-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.130-rc.0
|
|
3
|
+
## 2.0.130
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ab5e529: ZERO-5152: Render the breakpoints a designer adds next to desktop, tablet and mobile
|
|
8
|
+
|
|
9
|
+
The theme editor saves the breakpoints a designer defines (`theme_breakpoints` on the `theme-config` widget), each one a name and a pixel range, and stores the values authored for it under its own key in every per-breakpoint object. The renderer read none of that, so a value set for such a breakpoint showed in the canvas at best and never on the live site. The theme config reader now parses the list, the placeholder passes it to the stylesheet generator, and the generator writes the values a custom breakpoint sets inside its own media query, after the built-in ones so it wins inside its range. Anything a custom breakpoint does not set falls back to the built-in breakpoint its range starts in, in the stylesheet, in the canvas and in the value readers alike, and a visibility list saved before the breakpoint existed answers for it the way it answers for that built-in breakpoint. On the live site the wrapper reports the custom breakpoint whose range holds the viewport, so property values resolve the same way. A theme without custom breakpoints emits exactly the stylesheet it emitted before.
|
|
10
|
+
|
|
11
|
+
- @akinon/next@2.0.130
|
|
12
|
+
- @akinon/theme-contract@2.0.130
|
|
38
13
|
|
|
39
14
|
## 2.0.129
|
|
40
15
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.130
|
|
4
|
+
"version": "2.0.130",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.130
|
|
11
|
+
"@akinon/next": "2.0.130",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.130
|
|
16
|
+
"@akinon/theme-contract": "2.0.130",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.130
|
|
24
|
+
"@akinon/next": "2.0.130",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -25,7 +25,8 @@ describe('readThemeConfig', () => {
|
|
|
25
25
|
).toEqual({
|
|
26
26
|
settings: { mainMenuSticky: true },
|
|
27
27
|
dataSources: [{ id: 'ds-1' }],
|
|
28
|
-
modals: []
|
|
28
|
+
modals: [],
|
|
29
|
+
breakpoints: []
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -40,7 +41,8 @@ describe('readThemeConfig', () => {
|
|
|
40
41
|
).toEqual({
|
|
41
42
|
settings: { mainMenuSticky: true },
|
|
42
43
|
dataSources: [{ id: 'ds-1' }],
|
|
43
|
-
modals: []
|
|
44
|
+
modals: [],
|
|
45
|
+
breakpoints: []
|
|
44
46
|
});
|
|
45
47
|
});
|
|
46
48
|
|
|
@@ -55,7 +57,8 @@ describe('readThemeConfig', () => {
|
|
|
55
57
|
).toEqual({
|
|
56
58
|
settings: { mainMenuSticky: false },
|
|
57
59
|
dataSources: [{ id: 'ds-1' }],
|
|
58
|
-
modals: []
|
|
60
|
+
modals: [],
|
|
61
|
+
breakpoints: []
|
|
59
62
|
});
|
|
60
63
|
});
|
|
61
64
|
|
|
@@ -99,7 +102,8 @@ describe('readThemeConfig', () => {
|
|
|
99
102
|
autoTrigger: undefined,
|
|
100
103
|
visibilityRules: undefined
|
|
101
104
|
}
|
|
102
|
-
]
|
|
105
|
+
],
|
|
106
|
+
breakpoints: []
|
|
103
107
|
});
|
|
104
108
|
});
|
|
105
109
|
|
|
@@ -113,7 +117,65 @@ describe('readThemeConfig', () => {
|
|
|
113
117
|
theme_dataSources: JSON.stringify([{ id: 'ds-1' }])
|
|
114
118
|
}
|
|
115
119
|
})
|
|
116
|
-
).toEqual({
|
|
120
|
+
).toEqual({
|
|
121
|
+
settings: null,
|
|
122
|
+
dataSources: [{ id: 'ds-1' }],
|
|
123
|
+
modals: [],
|
|
124
|
+
breakpoints: []
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
error.mockRestore();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reads the custom breakpoints the editor saves next to the settings', () => {
|
|
131
|
+
// The built-in names only ever set a canvas width in the editor, and an
|
|
132
|
+
// entry the reader cannot make sense of is dropped on its own.
|
|
133
|
+
expect(
|
|
134
|
+
readThemeConfig({
|
|
135
|
+
attributes: {
|
|
136
|
+
theme_breakpoints: {
|
|
137
|
+
value: JSON.stringify([
|
|
138
|
+
{ name: 'desktop', width: 1024 },
|
|
139
|
+
{ name: 'mobile', width: 390 },
|
|
140
|
+
{
|
|
141
|
+
name: 'custom-wide',
|
|
142
|
+
label: 'Wide desktop',
|
|
143
|
+
width: 1440,
|
|
144
|
+
minWidth: 1440
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'custom-small',
|
|
148
|
+
label: 'Small phone',
|
|
149
|
+
width: 374,
|
|
150
|
+
maxWidth: 374
|
|
151
|
+
},
|
|
152
|
+
{ name: 'custom-broken', label: 'Broken', minWidth: 900, maxWidth: 600 }
|
|
153
|
+
])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}).breakpoints
|
|
157
|
+
).toEqual([
|
|
158
|
+
{ name: 'custom-wide', label: 'Wide desktop', width: 1440, minWidth: 1440 },
|
|
159
|
+
{ name: 'custom-small', label: 'Small phone', width: 374, maxWidth: 374 }
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('keeps the rest of the config when the breakpoints are malformed', () => {
|
|
164
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
165
|
+
|
|
166
|
+
expect(
|
|
167
|
+
readThemeConfig({
|
|
168
|
+
attributes: {
|
|
169
|
+
theme_breakpoints: '[{not json',
|
|
170
|
+
theme_settings: JSON.stringify({ mainMenuSticky: true })
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
).toEqual({
|
|
174
|
+
settings: { mainMenuSticky: true },
|
|
175
|
+
dataSources: [],
|
|
176
|
+
modals: [],
|
|
177
|
+
breakpoints: []
|
|
178
|
+
});
|
|
117
179
|
|
|
118
180
|
error.mockRestore();
|
|
119
181
|
});
|
|
@@ -122,12 +184,14 @@ describe('readThemeConfig', () => {
|
|
|
122
184
|
expect(readThemeConfig(null)).toEqual({
|
|
123
185
|
settings: null,
|
|
124
186
|
dataSources: [],
|
|
125
|
-
modals: []
|
|
187
|
+
modals: [],
|
|
188
|
+
breakpoints: []
|
|
126
189
|
});
|
|
127
190
|
expect(readThemeConfig({ attributes: {} })).toEqual({
|
|
128
191
|
settings: null,
|
|
129
192
|
dataSources: [],
|
|
130
|
-
modals: []
|
|
193
|
+
modals: [],
|
|
194
|
+
breakpoints: []
|
|
131
195
|
});
|
|
132
196
|
});
|
|
133
197
|
});
|
|
@@ -143,7 +207,8 @@ describe('getThemeConfig', () => {
|
|
|
143
207
|
await expect(getThemeConfig()).resolves.toEqual({
|
|
144
208
|
settings: null,
|
|
145
209
|
dataSources: [],
|
|
146
|
-
modals: []
|
|
210
|
+
modals: [],
|
|
211
|
+
breakpoints: []
|
|
147
212
|
});
|
|
148
213
|
});
|
|
149
214
|
});
|
|
@@ -30,7 +30,11 @@ export async function loadErrorComposition(): Promise<ErrorComposition | null> {
|
|
|
30
30
|
slug,
|
|
31
31
|
placeholderId: loaded.placeholderId,
|
|
32
32
|
sections: loaded.sections,
|
|
33
|
-
themeCSS: generateThemeCSS(
|
|
33
|
+
themeCSS: generateThemeCSS(
|
|
34
|
+
loaded.sections,
|
|
35
|
+
undefined,
|
|
36
|
+
loaded.customBreakpoints
|
|
37
|
+
),
|
|
34
38
|
themeSettings: loaded.themeSettings,
|
|
35
39
|
pageContext: buildErrorPageContext()
|
|
36
40
|
};
|
|
@@ -11,6 +11,8 @@ export interface PreviewSnapshot {
|
|
|
11
11
|
savedAt?: string;
|
|
12
12
|
themeSettings?: Record<string, unknown> | null;
|
|
13
13
|
dataSources?: unknown[];
|
|
14
|
+
/** `theme.breakpoints` as the working copy was saved with it. */
|
|
15
|
+
breakpoints?: unknown[];
|
|
14
16
|
sections: unknown[];
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -2,6 +2,10 @@ import * as React from 'react';
|
|
|
2
2
|
import { getWidgetDataWithRetry } from './retrying-widget-data';
|
|
3
3
|
|
|
4
4
|
import { readThemeModals, ThemeModal } from '../overlays/modal-registry';
|
|
5
|
+
import {
|
|
6
|
+
type CustomBreakpoint,
|
|
7
|
+
readCustomBreakpoints
|
|
8
|
+
} from '../utils/breakpoints';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* The `theme-config` widget, read and parsed once per request.
|
|
@@ -23,6 +27,12 @@ export interface ThemeConfig {
|
|
|
23
27
|
* every page already fetches once.
|
|
24
28
|
*/
|
|
25
29
|
modals: ThemeModal[];
|
|
30
|
+
/**
|
|
31
|
+
* The breakpoints the designer added next to desktop, tablet and mobile.
|
|
32
|
+
* The editor has always saved `theme_breakpoints` on this widget; the live
|
|
33
|
+
* stylesheet needs them to emit the rules authored for them.
|
|
34
|
+
*/
|
|
35
|
+
breakpoints: CustomBreakpoint[];
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
type ThemeConfigWidget = {
|
|
@@ -42,11 +52,30 @@ const unwrapAttribute = (raw: unknown): unknown => {
|
|
|
42
52
|
};
|
|
43
53
|
|
|
44
54
|
export const readThemeConfig = (data: ThemeConfigWidget): ThemeConfig => {
|
|
45
|
-
const config: ThemeConfig = {
|
|
55
|
+
const config: ThemeConfig = {
|
|
56
|
+
settings: null,
|
|
57
|
+
dataSources: [],
|
|
58
|
+
modals: [],
|
|
59
|
+
breakpoints: []
|
|
60
|
+
};
|
|
46
61
|
const attributes = data?.attributes;
|
|
47
62
|
|
|
48
63
|
if (!attributes) return config;
|
|
49
64
|
|
|
65
|
+
if (attributes.theme_breakpoints) {
|
|
66
|
+
try {
|
|
67
|
+
const resolved = unwrapAttribute(attributes.theme_breakpoints);
|
|
68
|
+
|
|
69
|
+
// `readCustomBreakpoints` drops a bad entry on its own, so a malformed
|
|
70
|
+
// one never costs the others.
|
|
71
|
+
config.breakpoints = readCustomBreakpoints(
|
|
72
|
+
typeof resolved === 'string' ? JSON.parse(resolved) : resolved
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Failed to parse theme_breakpoints:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
if (attributes.theme_modals) {
|
|
51
80
|
// `readThemeModals` already survives a malformed blob one entry at a time,
|
|
52
81
|
// so it needs no try/catch of its own here.
|
|
@@ -95,7 +124,7 @@ const load = async (): Promise<ThemeConfig> => {
|
|
|
95
124
|
return readThemeConfig(data as ThemeConfigWidget);
|
|
96
125
|
} catch {
|
|
97
126
|
// A brand with no published theme still renders: no settings, no sources.
|
|
98
|
-
return { settings: null, dataSources: [], modals: [] };
|
|
127
|
+
return { settings: null, dataSources: [], modals: [], breakpoints: [] };
|
|
99
128
|
}
|
|
100
129
|
};
|
|
101
130
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
PreviewSnapshot
|
|
14
14
|
} from './chrome/preview-snapshot-source';
|
|
15
15
|
import { getThemeConfig, ThemeConfig } from './chrome/theme-config-source';
|
|
16
|
+
import { readCustomBreakpoints } from './utils/breakpoints';
|
|
16
17
|
// One acceptance rule for this loader and the designer canvas's client fetch
|
|
17
18
|
// (utils/live-rows.ts), so the two cannot take different answers.
|
|
18
19
|
import { carriesProducts } from './utils/live-rows';
|
|
@@ -226,7 +227,12 @@ async function loadFromPreviewSnapshot(
|
|
|
226
227
|
placeholderId: slug,
|
|
227
228
|
dataSources,
|
|
228
229
|
themeSettings:
|
|
229
|
-
(snapshot.themeSettings as Record<string, unknown> | null) ?? null
|
|
230
|
+
(snapshot.themeSettings as Record<string, unknown> | null) ?? null,
|
|
231
|
+
// The working copy's own list; a blob written before it carried one
|
|
232
|
+
// renders with the live theme's.
|
|
233
|
+
customBreakpoints: Array.isArray(snapshot.breakpoints)
|
|
234
|
+
? readCustomBreakpoints(snapshot.breakpoints)
|
|
235
|
+
: (await getThemeConfig()).breakpoints
|
|
230
236
|
};
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -235,6 +241,7 @@ export interface LoadedPlaceholder {
|
|
|
235
241
|
placeholderId: string;
|
|
236
242
|
dataSources: ThemeConfig['dataSources'];
|
|
237
243
|
themeSettings: ThemeConfig['settings'];
|
|
244
|
+
customBreakpoints: ThemeConfig['breakpoints'];
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/**
|
|
@@ -262,7 +269,11 @@ export async function loadPlaceholderSections(
|
|
|
262
269
|
// theme-config is optional for placeholder rendering, and the reader is
|
|
263
270
|
// memoized per request — a page with several placeholders, or a ChromeSlot
|
|
264
271
|
// asking the same question to decide whether it is pinned, reads it once.
|
|
265
|
-
const {
|
|
272
|
+
const {
|
|
273
|
+
dataSources,
|
|
274
|
+
settings: themeSettings,
|
|
275
|
+
breakpoints: customBreakpoints
|
|
276
|
+
} = await getThemeConfig();
|
|
266
277
|
|
|
267
278
|
let data: {
|
|
268
279
|
slug?: string;
|
|
@@ -493,7 +504,8 @@ export async function loadPlaceholderSections(
|
|
|
493
504
|
sections,
|
|
494
505
|
placeholderId: data?.slug?.toString() || slug,
|
|
495
506
|
dataSources,
|
|
496
|
-
themeSettings
|
|
507
|
+
themeSettings,
|
|
508
|
+
customBreakpoints
|
|
497
509
|
};
|
|
498
510
|
} catch (error) {
|
|
499
511
|
console.error(`Error fetching theme placeholder data for ${slug}:`, error);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { parseEditorMessage } from '../editor-bridge';
|
|
6
|
-
import { breakpointForWidth } from '../utils/breakpoints';
|
|
6
|
+
import { breakpointForWidth, builtInBreakpointOf } from '../utils/breakpoints';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Which breakpoint the surrounding page is at.
|
|
@@ -39,7 +39,9 @@ export const useOverlayBreakpoint = (): string => {
|
|
|
39
39
|
if (message?.type !== 'CHANGE_BREAKPOINT') return;
|
|
40
40
|
|
|
41
41
|
const next = (message.data as { breakpoint?: string })?.breakpoint;
|
|
42
|
-
|
|
42
|
+
// Modal chrome knows the three built-in breakpoints only; a custom one
|
|
43
|
+
// shows the chrome of the band it stands on, as its range does live.
|
|
44
|
+
if (next) setBreakpoint(builtInBreakpointOf(next));
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
window.addEventListener('message', handleMessage);
|
|
@@ -19,7 +19,12 @@ import { ThemeSettingsProvider } from './theme-settings-context';
|
|
|
19
19
|
import { MenuSeedProvider } from './menu-seed-context';
|
|
20
20
|
import type { MenuSeeds } from './blocks/menu-query-args';
|
|
21
21
|
import { generateThemeCSS, readThemeTypographyFont } from './utils';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
type CustomBreakpoint,
|
|
24
|
+
breakpointForViewport,
|
|
25
|
+
readCustomBreakpoints,
|
|
26
|
+
registerCustomBreakpoints
|
|
27
|
+
} from './utils/breakpoints';
|
|
23
28
|
import { PartStatePreview } from './components/part-state-preview';
|
|
24
29
|
import { useVisibilityContext } from './hooks/use-visibility-context';
|
|
25
30
|
import { createPageContextDiscovery } from './utils/page-context-discovery';
|
|
@@ -222,6 +227,12 @@ interface ThemePlaceholderWrapperProps {
|
|
|
222
227
|
isDesignMode: boolean;
|
|
223
228
|
dataSources?: any[];
|
|
224
229
|
initialThemeSettings?: Record<string, unknown> | null;
|
|
230
|
+
/**
|
|
231
|
+
* The theme's custom breakpoints (`theme_breakpoints`), read with the theme
|
|
232
|
+
* settings. Absent for a caller that has none to give; the editor canvas
|
|
233
|
+
* gets them with the theme it sends.
|
|
234
|
+
*/
|
|
235
|
+
initialCustomBreakpoints?: CustomBreakpoint[] | null;
|
|
225
236
|
initialPageContext?: Record<string, unknown> | null;
|
|
226
237
|
/**
|
|
227
238
|
* Menus the server fetched for this placeholder's nav-menu blocks
|
|
@@ -366,6 +377,7 @@ export default function ThemePlaceholderWrapper({
|
|
|
366
377
|
isDesignMode,
|
|
367
378
|
dataSources: initialDataSources = [],
|
|
368
379
|
initialThemeSettings = null,
|
|
380
|
+
initialCustomBreakpoints = null,
|
|
369
381
|
initialPageContext = null,
|
|
370
382
|
initialMenuSeeds = null
|
|
371
383
|
}: ThemePlaceholderWrapperProps) {
|
|
@@ -378,6 +390,17 @@ export default function ThemePlaceholderWrapper({
|
|
|
378
390
|
);
|
|
379
391
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
|
380
392
|
const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
|
|
393
|
+
// Registered for the readers that only see a value and a breakpoint name
|
|
394
|
+
// (see utils/breakpoints). Only when this caller has a list to give: a
|
|
395
|
+
// wrapper without one must not clear what another placeholder registered.
|
|
396
|
+
const [customBreakpoints, setCustomBreakpoints] = useState<
|
|
397
|
+
CustomBreakpoint[]
|
|
398
|
+
>(() => {
|
|
399
|
+
if (initialCustomBreakpoints) {
|
|
400
|
+
registerCustomBreakpoints(initialCustomBreakpoints);
|
|
401
|
+
}
|
|
402
|
+
return initialCustomBreakpoints ?? [];
|
|
403
|
+
});
|
|
381
404
|
// Designer flag survives remounts via the window global; the editor sends
|
|
382
405
|
// SET_THEME_EDITOR_COOKIE only once per document (IFRAME_READY guard).
|
|
383
406
|
const [isDesigner, setIsDesigner] = useState(
|
|
@@ -728,8 +751,9 @@ export default function ThemePlaceholderWrapper({
|
|
|
728
751
|
const width = window.innerWidth;
|
|
729
752
|
// The cutoffs live in utils/breakpoints, alongside the media queries
|
|
730
753
|
// the generated stylesheet is built from, so the viewport this reports
|
|
731
|
-
// and the query that is matching it can never drift apart.
|
|
732
|
-
|
|
754
|
+
// and the query that is matching it can never drift apart. A custom
|
|
755
|
+
// breakpoint whose range holds the width is the one being read.
|
|
756
|
+
const newBreakpoint = breakpointForViewport(width, customBreakpoints);
|
|
733
757
|
|
|
734
758
|
setCurrentBreakpoint((prev) => {
|
|
735
759
|
if (prev !== newBreakpoint) {
|
|
@@ -754,7 +778,7 @@ export default function ThemePlaceholderWrapper({
|
|
|
754
778
|
clearTimeout(timeoutId);
|
|
755
779
|
};
|
|
756
780
|
}
|
|
757
|
-
}, []);
|
|
781
|
+
}, [customBreakpoints]);
|
|
758
782
|
|
|
759
783
|
// Re-hangs the editor's data-source records over the sections, and again
|
|
760
784
|
// whenever live rows arrive (`liveRowsRevision`). Waits for the editor's
|
|
@@ -865,6 +889,18 @@ export default function ThemePlaceholderWrapper({
|
|
|
865
889
|
case 'UPDATE_THEME': {
|
|
866
890
|
const theme = messageData?.theme;
|
|
867
891
|
|
|
892
|
+
// Before any state is set, so the render this message causes already
|
|
893
|
+
// resolves custom breakpoints against the list it carries.
|
|
894
|
+
if (Array.isArray(theme?.breakpoints)) {
|
|
895
|
+
const incoming = readCustomBreakpoints(theme.breakpoints);
|
|
896
|
+
registerCustomBreakpoints(incoming);
|
|
897
|
+
setCustomBreakpoints((previous) =>
|
|
898
|
+
JSON.stringify(previous) === JSON.stringify(incoming)
|
|
899
|
+
? previous
|
|
900
|
+
: incoming
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
868
904
|
// Inject theme colors as CSS custom properties
|
|
869
905
|
if (theme?.settings) {
|
|
870
906
|
injectThemeColors(theme.settings);
|
|
@@ -1032,8 +1068,12 @@ export default function ThemePlaceholderWrapper({
|
|
|
1032
1068
|
// mobile-width frame — a layout jump on every toggle.
|
|
1033
1069
|
const dynamicCSS = useMemo(
|
|
1034
1070
|
() =>
|
|
1035
|
-
generateThemeCSS(
|
|
1036
|
-
|
|
1071
|
+
generateThemeCSS(
|
|
1072
|
+
sections,
|
|
1073
|
+
isDesigner ? currentBreakpoint : undefined,
|
|
1074
|
+
customBreakpoints
|
|
1075
|
+
),
|
|
1076
|
+
[sections, isDesigner, currentBreakpoint, customBreakpoints]
|
|
1037
1077
|
);
|
|
1038
1078
|
const themeBackgroundCSS = useMemo(
|
|
1039
1079
|
() =>
|
|
@@ -1059,7 +1099,9 @@ export default function ThemePlaceholderWrapper({
|
|
|
1059
1099
|
applyVisibilityRulesToSections(sections, visibilityContext),
|
|
1060
1100
|
currentBreakpoint
|
|
1061
1101
|
),
|
|
1062
|
-
|
|
1102
|
+
// `customBreakpoints` too: the chain a custom breakpoint reads through is
|
|
1103
|
+
// the registered list, which changes without the breakpoint changing.
|
|
1104
|
+
[sections, visibilityContext, currentBreakpoint, customBreakpoints]
|
|
1063
1105
|
);
|
|
1064
1106
|
|
|
1065
1107
|
return (
|
|
@@ -22,8 +22,14 @@ export default async function ThemePlaceholder({
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const {
|
|
26
|
-
|
|
25
|
+
const {
|
|
26
|
+
sections,
|
|
27
|
+
placeholderId,
|
|
28
|
+
dataSources,
|
|
29
|
+
themeSettings,
|
|
30
|
+
customBreakpoints
|
|
31
|
+
} = loaded;
|
|
32
|
+
const themeCSS = generateThemeCSS(sections, undefined, customBreakpoints);
|
|
27
33
|
// The live and the preview-snapshot loads both arrive here, so both get the
|
|
28
34
|
// menus their nav-menu blocks would otherwise fetch after hydration.
|
|
29
35
|
const menuSeeds = await loadMenuSeeds(sections);
|
|
@@ -40,6 +46,7 @@ export default async function ThemePlaceholder({
|
|
|
40
46
|
isDesignMode={false}
|
|
41
47
|
dataSources={dataSources}
|
|
42
48
|
initialThemeSettings={themeSettings}
|
|
49
|
+
initialCustomBreakpoints={customBreakpoints}
|
|
43
50
|
initialPageContext={pageContext}
|
|
44
51
|
initialMenuSeeds={menuSeeds}
|
|
45
52
|
/>
|