@akinon/pz-theme 2.0.31-beta.0 → 2.0.31

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 CHANGED
@@ -1,11 +1,15 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.31-beta.0
3
+ ## 2.0.31
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd75]
8
- - @akinon/next@2.0.31-beta.0
7
+ - 7717c4e: ZERO-4594: Make the render engine resilient to a single bad section. Every section is now wrapped in an error boundary, so a throwing custom section/block renderer is skipped (and dev-warned) instead of unmounting the whole placeholder. Unguarded `JSON.parse(JSON.stringify(...))` style/block deep-clones are replaced with a `safeDeepClone` helper that degrades gracefully instead of throwing on non-serializable values. No output change for healthy themes.
8
+ - 238fbdb: ZERO-4594: Add dev-only shape validation (zod) for the theme section tree. During the server render, each section and its nested blocks are validated against a loose structural schema; malformed shapes (missing id/type, non-array blocks, non-object styles) are surfaced via a single dev-only warning naming the offending section. Validation runs only outside production and never drops or mutates data, so render output is unchanged. Adds `zod` as an explicit dependency (it was previously present only transitively).
9
+ - d7f9621: ZERO-4594: Add dev-only render-engine warnings (devWarn) that surface previously silent failures — unknown block/section types (registry drift), malformed placeholder layout JSON, section data that cannot be resolved, missing/renamed data sources, iterator paths resolving to a non-array, and malformed visibility rules. Warnings fire only outside production (assertable in tests) and never change render output; all existing graceful fallbacks are preserved.
10
+ - cd7c590: ZERO-4594: Add dev-only warnings for responsive-value and CSS-unit mistakes. `getResponsiveValue` warns when handed an array (which it silently treats as an empty responsive map), and the emitted-CSS path warns when a unit-requiring property receives a unitless numeric string (e.g. `font-size: "16"`) — the browser drops it even though the inline-style path auto-pixels the same bare number, a silent parity trap. Dev-only (stripped in production); render output is unchanged.
11
+ - d2fe927: ZERO-4594: Harden theme CSS output against `</style>` tag breakout (security fix). Editor-controlled style values and section/block ids that flow into `<style dangerouslySetInnerHTML>` are now neutralized/escaped at the source — the generated CSS from `generateThemeCSS` (theme placeholder) and the shipping-threshold-progress section's dynamic CSS pass through a `</style>`-neutralizing step, and ids interpolated into attribute selectors are escaped — so injected markup can no longer close the style element and execute in a visitor's browser. No output change for legitimate themes.
12
+ - @akinon/next@2.0.31
9
13
 
10
14
  ## 2.0.30
11
15
 
package/jest.config.js CHANGED
@@ -36,10 +36,13 @@ module.exports = {
36
36
  '^.+\\.(ts|tsx)$': [
37
37
  'ts-jest',
38
38
  {
39
+ // Transpile-only: pz-theme has no tsconfig and is consumed by brands
40
+ // via SWC transpilation (never type-checked), so tests verify runtime
41
+ // behavior, not types. `diagnostics: false` keeps the runner
42
+ // deterministic (no cache-dependent type-check surprises) and matches
43
+ // how the package is actually built downstream.
44
+ diagnostics: false,
39
45
  tsconfig: {
40
- // Transpile-only: avoids whole-program type-checking (pz-theme has
41
- // no tsconfig and relies on consumer transpilation) while still
42
- // eliding type-only imports such as `import { Block }`.
43
46
  isolatedModules: true,
44
47
  jsx: 'react-jsx',
45
48
  esModuleInterop: true,
package/package.json CHANGED
@@ -1,24 +1,25 @@
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.31-beta.0",
4
+ "version": "2.0.31",
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.31-beta.0",
11
+ "@akinon/next": "2.0.31",
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
16
  "clsx": "2.1.1",
17
17
  "lottie-web": "^5.13.0",
18
- "tailwind-merge": "^2.5.4"
18
+ "tailwind-merge": "^2.5.4",
19
+ "zod": "4.4.1"
19
20
  },
20
21
  "devDependencies": {
21
- "@akinon/next": "2.0.31-beta.0",
22
+ "@akinon/next": "2.0.31",
22
23
  "@testing-library/jest-dom": "5.16.4",
23
24
  "@testing-library/react": "16.3.2",
24
25
  "@testing-library/user-event": "14.1.1",
@@ -12,6 +12,7 @@ jest.mock('../components/with-designer-features', () => ({
12
12
  }));
13
13
 
14
14
  import ThemeBlock from '../theme-block';
15
+ import { resetDevWarnCacheForTests } from '../utils/dev-warn';
15
16
 
16
17
  const props = (over: Record<string, unknown> = {}) =>
17
18
  ({
@@ -31,6 +32,17 @@ const props = (over: Record<string, unknown> = {}) =>
31
32
  } as any);
32
33
 
33
34
  describe('ThemeBlock — unknown block type', () => {
35
+ let warnSpy: jest.SpyInstance;
36
+
37
+ beforeEach(() => {
38
+ resetDevWarnCacheForTests();
39
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
40
+ });
41
+
42
+ afterEach(() => {
43
+ warnSpy.mockRestore();
44
+ });
45
+
34
46
  it('renders nothing in production (no debug text leaks to end users)', () => {
35
47
  const { container } = render(<ThemeBlock {...props({ isDesigner: false })} />);
36
48
  expect(container.textContent).not.toMatch(/Unknown block type/);
@@ -43,4 +55,11 @@ describe('ThemeBlock — unknown block type', () => {
43
55
  'Unknown block type: nonexistent-xyz'
44
56
  );
45
57
  });
58
+
59
+ it('dev-warns naming the missing renderer (catches registry drift)', () => {
60
+ render(<ThemeBlock {...props({ isDesigner: false })} />);
61
+ expect(warnSpy).toHaveBeenCalledWith(
62
+ '[pz-theme:ThemeBlock] No renderer registered for block type: "nonexistent-xyz"'
63
+ );
64
+ });
46
65
  });
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import SectionErrorBoundary from '../section-error-boundary';
4
+ import { resetDevWarnCacheForTests } from '../../utils/dev-warn';
5
+
6
+ const Boom = (): React.ReactElement => {
7
+ throw new Error('section blew up');
8
+ };
9
+
10
+ describe('SectionErrorBoundary', () => {
11
+ let warnSpy: jest.SpyInstance;
12
+ let errorSpy: jest.SpyInstance;
13
+
14
+ beforeEach(() => {
15
+ resetDevWarnCacheForTests();
16
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
17
+ // React logs caught render errors to console.error; silence it so the test
18
+ // output stays clean.
19
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
20
+ });
21
+
22
+ afterEach(() => {
23
+ warnSpy.mockRestore();
24
+ errorSpy.mockRestore();
25
+ });
26
+
27
+ it('renders its children when nothing throws', () => {
28
+ render(
29
+ <SectionErrorBoundary sectionId="sec-ok">
30
+ <div data-testid="child">ok</div>
31
+ </SectionErrorBoundary>
32
+ );
33
+
34
+ expect(screen.getByTestId('child')).toBeInTheDocument();
35
+ });
36
+
37
+ it('renders nothing and dev-warns (naming the section) when a child throws', () => {
38
+ const { container } = render(
39
+ <SectionErrorBoundary sectionId="sec-boom">
40
+ <Boom />
41
+ </SectionErrorBoundary>
42
+ );
43
+
44
+ // The broken section is skipped — no markup leaks to end users.
45
+ expect(container).toBeEmptyDOMElement();
46
+ expect(warnSpy).toHaveBeenCalledWith(
47
+ '[pz-theme:SectionErrorBoundary] Section "sec-boom" failed to render and was skipped',
48
+ expect.any(Error)
49
+ );
50
+ });
51
+
52
+ it('falls back to "unknown" when no sectionId is provided', () => {
53
+ render(
54
+ <SectionErrorBoundary>
55
+ <Boom />
56
+ </SectionErrorBoundary>
57
+ );
58
+
59
+ expect(warnSpy).toHaveBeenCalledWith(
60
+ '[pz-theme:SectionErrorBoundary] Section "unknown" failed to render and was skipped',
61
+ expect.any(Error)
62
+ );
63
+ });
64
+ });
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { devWarn } from '../utils/dev-warn';
5
+
6
+ interface SectionErrorBoundaryProps {
7
+ sectionId?: string;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ interface SectionErrorBoundaryState {
12
+ hasError: boolean;
13
+ }
14
+
15
+ /**
16
+ * Isolates a single section's render. A throw in one custom section/block
17
+ * renderer skips only that section instead of unmounting the whole placeholder.
18
+ * Renders nothing on failure (end users never see a broken section); the
19
+ * dev-only warning names the offending section so authors/devs notice it.
20
+ */
21
+ export default class SectionErrorBoundary extends React.Component<
22
+ SectionErrorBoundaryProps,
23
+ SectionErrorBoundaryState
24
+ > {
25
+ state: SectionErrorBoundaryState = { hasError: false };
26
+
27
+ static getDerivedStateFromError(): SectionErrorBoundaryState {
28
+ return { hasError: true };
29
+ }
30
+
31
+ componentDidCatch(error: unknown): void {
32
+ devWarn(
33
+ 'SectionErrorBoundary',
34
+ `Section "${
35
+ this.props.sectionId ?? 'unknown'
36
+ }" failed to render and was skipped`,
37
+ error
38
+ );
39
+ }
40
+
41
+ render(): React.ReactNode {
42
+ if (this.state.hasError) {
43
+ return null;
44
+ }
45
+ return this.props.children;
46
+ }
47
+ }
@@ -0,0 +1,93 @@
1
+ import { validateThemeSectionsInDev } from '../theme-schema';
2
+ import { resetDevWarnCacheForTests } from '../../utils/dev-warn';
3
+
4
+ const setNodeEnv = (value: string | undefined): void => {
5
+ (process.env as Record<string, string | undefined>).NODE_ENV = value;
6
+ };
7
+
8
+ const validSection = (over: Record<string, unknown> = {}) => ({
9
+ id: 'sec-1',
10
+ type: 'default',
11
+ order: 0,
12
+ hidden: false,
13
+ styles: { color: 'red' },
14
+ properties: {},
15
+ blocks: [
16
+ {
17
+ id: 'blk-1',
18
+ type: 'text',
19
+ order: 0,
20
+ hidden: false,
21
+ styles: {},
22
+ properties: {},
23
+ blocks: []
24
+ }
25
+ ],
26
+ ...over
27
+ });
28
+
29
+ describe('validateThemeSectionsInDev', () => {
30
+ let warnSpy: jest.SpyInstance;
31
+ const originalNodeEnv = process.env.NODE_ENV;
32
+
33
+ beforeEach(() => {
34
+ resetDevWarnCacheForTests();
35
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
36
+ });
37
+
38
+ afterEach(() => {
39
+ warnSpy.mockRestore();
40
+ setNodeEnv(originalNodeEnv);
41
+ });
42
+
43
+ it('stays silent for a valid section tree (incl. nested blocks)', () => {
44
+ validateThemeSectionsInDev([validSection()]);
45
+ expect(warnSpy).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it('accepts null styles/properties (render tolerates them)', () => {
49
+ validateThemeSectionsInDev([validSection({ styles: null, properties: null })]);
50
+ expect(warnSpy).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('warns when a section is missing its id', () => {
54
+ validateThemeSectionsInDev([validSection({ id: undefined })]);
55
+ expect(warnSpy).toHaveBeenCalledTimes(1);
56
+ expect(warnSpy.mock.calls[0][0]).toContain('[pz-theme:themeSchema]');
57
+ expect(warnSpy.mock.calls[0][0]).toContain('index 0');
58
+ expect(warnSpy.mock.calls[0][0]).toContain('id');
59
+ });
60
+
61
+ it('warns when section type is not a string', () => {
62
+ validateThemeSectionsInDev([validSection({ type: 5 })]);
63
+ expect(warnSpy).toHaveBeenCalledTimes(1);
64
+ expect(warnSpy.mock.calls[0][0]).toContain('sec-1');
65
+ });
66
+
67
+ it('warns when styles is a string instead of an object', () => {
68
+ validateThemeSectionsInDev([validSection({ styles: 'color:red' })]);
69
+ expect(warnSpy).toHaveBeenCalledTimes(1);
70
+ expect(warnSpy.mock.calls[0][0]).toContain('styles');
71
+ });
72
+
73
+ it('warns about a malformed nested block (path points at the block)', () => {
74
+ validateThemeSectionsInDev([
75
+ validSection({ blocks: [{ id: 'blk-bad', type: 5 }] })
76
+ ]);
77
+ expect(warnSpy).toHaveBeenCalledTimes(1);
78
+ expect(warnSpy.mock.calls[0][0]).toContain('blocks.0.type');
79
+ });
80
+
81
+ it('is a no-op in production (zero behavior change)', () => {
82
+ setNodeEnv('production');
83
+ validateThemeSectionsInDev([validSection({ id: undefined })]);
84
+ expect(warnSpy).not.toHaveBeenCalled();
85
+ });
86
+
87
+ it('is a no-op for non-array input', () => {
88
+ validateThemeSectionsInDev(null);
89
+ validateThemeSectionsInDev(undefined);
90
+ validateThemeSectionsInDev('nope');
91
+ expect(warnSpy).not.toHaveBeenCalled();
92
+ });
93
+ });
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ import { devWarn } from '../utils/dev-warn';
3
+
4
+ // Loose, structural-only schemas. They assert just the invariants the render
5
+ // engine relies on — id/type are strings, styles/properties are plain objects
6
+ // (when present), blocks is an array of blocks — and intentionally ignore value
7
+ // contents. Extra keys are allowed, so legitimate data with additional fields
8
+ // validates cleanly and we avoid false-positive warnings.
9
+ const blockSchema: z.ZodType<unknown> = z.lazy(() =>
10
+ z.object({
11
+ id: z.string(),
12
+ type: z.string(),
13
+ order: z.number().optional(),
14
+ hidden: z.boolean().optional(),
15
+ styles: z.object({}).nullable().optional(),
16
+ properties: z.object({}).nullable().optional(),
17
+ blocks: z.array(blockSchema).optional()
18
+ })
19
+ );
20
+
21
+ const sectionSchema = z.object({
22
+ id: z.string(),
23
+ type: z.string(),
24
+ order: z.number().optional(),
25
+ hidden: z.boolean().optional(),
26
+ styles: z.object({}).nullable().optional(),
27
+ properties: z.object({}).nullable().optional(),
28
+ blocks: z.array(blockSchema).optional()
29
+ });
30
+
31
+ const describeIssues = (error: z.ZodError): string =>
32
+ error.issues
33
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
34
+ .join('; ');
35
+
36
+ /**
37
+ * Dev-only, warn-only shape check for the section tree. It runs nothing in
38
+ * production (zero cost, zero behavior change) and never mutates or drops data —
39
+ * it only surfaces structurally malformed sections/blocks so developers notice
40
+ * shape drift between the editor/backend and the render engine. Intended to be
41
+ * called from the server entry point so zod stays out of the client bundle.
42
+ */
43
+ export const validateThemeSectionsInDev = (sections: unknown): void => {
44
+ if (process.env.NODE_ENV === 'production') return;
45
+ if (!Array.isArray(sections)) return;
46
+
47
+ sections.forEach((section, index) => {
48
+ const result = sectionSchema.safeParse(section);
49
+ if (result.success) return;
50
+
51
+ const rawId =
52
+ section && typeof section === 'object'
53
+ ? (section as { id?: unknown }).id
54
+ : undefined;
55
+ const label =
56
+ typeof rawId === 'string' && rawId ? rawId : `at index ${index}`;
57
+
58
+ devWarn(
59
+ 'themeSchema',
60
+ `Section "${label}" has an unexpected shape: ${describeIssues(
61
+ result.error
62
+ )}`
63
+ );
64
+ });
65
+ };
@@ -6,6 +6,7 @@ import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
6
6
  import ThemeBlock, { Block } from '../theme-block';
7
7
  import { useThemeSettingsContext } from '../theme-settings-context';
8
8
  import { Section } from '../theme-section';
9
+ import { safeDeepClone } from '../utils/safe-clone';
9
10
  import { getCSSStyles, getResponsiveValue } from '../utils';
10
11
 
11
12
  interface FeaturedProductSpotlightSectionProps {
@@ -335,7 +336,7 @@ export default function FeaturedProductSpotlightSection({
335
336
  ...block,
336
337
  properties: block.properties ? { ...block.properties } : block.properties,
337
338
  styles: block.styles
338
- ? JSON.parse(JSON.stringify(block.styles))
339
+ ? safeDeepClone(block.styles)
339
340
  : block.styles,
340
341
  blocks: undefined
341
342
  };
@@ -6,6 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
 
7
7
  import ThemeBlock from '../theme-block';
8
8
  import { useThemeSettingsContext } from '../theme-settings-context';
9
+ import { safeDeepClone } from '../utils/safe-clone';
9
10
  import {
10
11
  applyFirstSlideImageLoading,
11
12
  getCSSStyles,
@@ -55,7 +56,7 @@ const cloneBlock = (block: any): any => ({
55
56
  ...block,
56
57
  properties: block.properties ? { ...block.properties } : block.properties,
57
58
  styles: block.styles
58
- ? JSON.parse(JSON.stringify(block.styles))
59
+ ? safeDeepClone(block.styles)
59
60
  : block.styles,
60
61
  blocks: block.blocks
61
62
  ? block.blocks.map((child: any) => cloneBlock(child))
@@ -6,6 +6,7 @@ import { useLocalization } from '@akinon/next/hooks';
6
6
 
7
7
  import ThemeBlock, { Block } from '../theme-block';
8
8
  import { useThemeSettingsContext } from '../theme-settings-context';
9
+ import { safeDeepClone } from '../utils/safe-clone';
9
10
  import { Section } from '../theme-section';
10
11
  import { getCSSStyles, getResponsiveValue } from '../utils';
11
12
 
@@ -497,7 +498,7 @@ export default function PreOrderLaunchBannerSection({
497
498
  ? { ...block.properties }
498
499
  : block.properties,
499
500
  styles: block.styles
500
- ? JSON.parse(JSON.stringify(block.styles))
501
+ ? safeDeepClone(block.styles)
501
502
  : block.styles,
502
503
  blocks: undefined
503
504
  };
@@ -13,6 +13,10 @@ import {
13
13
  getResponsiveValue,
14
14
  resolveThemeCssVariables
15
15
  } from '../utils';
16
+ import {
17
+ neutralizeStyleTagBreakout,
18
+ escapeCssAttributeValue
19
+ } from '../utils/css-safety';
16
20
 
17
21
  interface ShippingThresholdProgressSectionProps {
18
22
  section: Section;
@@ -227,10 +231,6 @@ const findProgressBlockIds = (
227
231
  return { fillBlockId, markerBlockId, wrapperBlockId, startLabelBlockId };
228
232
  };
229
233
 
230
- const escapeCssAttributeValue = (value: string): string => {
231
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
232
- };
233
-
234
234
  const injectDynamicState = (
235
235
  blocks: Block[],
236
236
  tokens: Record<string, string>,
@@ -485,7 +485,9 @@ const ShippingThresholdProgressSection: React.FC<
485
485
  rules.push(`[data-block-id="${escapedStartLabelId}"]{display:none !important;}`);
486
486
  }
487
487
 
488
- return rules.join('');
488
+ // Neutralize any </style> breakout from editor-controlled color/radius
489
+ // values before this string is inlined into a <style> tag (stored XSS fix).
490
+ return neutralizeStyleTagBreakout(rules.join(''));
489
491
  }, [
490
492
  fillBlockId,
491
493
  markerBlockId,
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import blockRendererRegistry from './blocks/block-renderer-registry';
5
5
  import { WithDesignerFeatures } from './components/with-designer-features';
6
6
  import { isPublishWindowVisible } from './utils';
7
+ import { devWarn } from './utils/dev-warn';
7
8
 
8
9
  export interface Block {
9
10
  id: string;
@@ -58,7 +59,13 @@ export default function ThemeBlock({
58
59
 
59
60
  if (!BlockRenderer) {
60
61
  // Surface unknown types to authors in the designer, but never leak debug
61
- // text to end users in production.
62
+ // text to end users in production. The dev-only warning catches registry
63
+ // drift (a type saved in the editor but missing from the runtime registry)
64
+ // which otherwise vanishes silently on the live site.
65
+ devWarn(
66
+ 'ThemeBlock',
67
+ `No renderer registered for block type: "${block.type}"`
68
+ );
62
69
  return isDesigner ? <div>Unknown block type: {block.type}</div> : null;
63
70
  }
64
71
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from 'react';
4
4
  import ThemeSection, { Section } from './theme-section';
5
+ import SectionErrorBoundary from './components/section-error-boundary';
5
6
  import {
6
7
  registerPlaceholder,
7
8
  unregisterPlaceholder
@@ -163,62 +164,64 @@ export default function ThemePlaceholderClient({
163
164
  };
164
165
 
165
166
  return (
166
- <div data-placeholder={slug} className="theme-placeholder">
167
+ <div data-placeholder={slug} className="theme-placeholder">
167
168
  {renderedSections
168
169
  .sort((a, b) => a.order - b.order)
169
- .filter((section) =>
170
- !section.hidden &&
171
- (isDesigner ||
172
- isPublishWindowVisible(section.properties?.['publish-window']))
170
+ .filter(
171
+ (section) =>
172
+ !section.hidden &&
173
+ (isDesigner ||
174
+ isPublishWindowVisible(section.properties?.['publish-window']))
173
175
  )
174
176
  .map((section) => (
175
- <ThemeSection
176
- key={section.id}
177
- section={section}
178
- placeholderId={placeholderSlug}
179
- isDesigner={isDesigner}
180
- isSelected={selectedSectionId === section.id}
181
- selectedBlockId={selectedBlockId}
182
- currentBreakpoint={currentBreakpoint}
183
- onSelect={setSelectedSectionId}
184
- onMoveUp={() =>
185
- sendAction('MOVE_SECTION_UP', {
186
- placeholderId: placeholderSlug,
187
- sectionId: section.id
188
- })
189
- }
190
- onMoveDown={() =>
191
- sendAction('MOVE_SECTION_DOWN', {
192
- placeholderId: placeholderSlug,
193
- sectionId: section.id
194
- })
195
- }
196
- onDuplicate={() =>
197
- sendAction('DUPLICATE_SECTION', {
198
- placeholderId: placeholderSlug,
199
- sectionId: section.id
200
- })
201
- }
202
- onToggleVisibility={() =>
203
- sendAction('TOGGLE_SECTION_VISIBILITY', {
204
- placeholderId: placeholderSlug,
205
- sectionId: section.id
206
- })
207
- }
208
- onDelete={() =>
209
- sendAction('DELETE_SECTION', {
210
- placeholderId: placeholderSlug,
211
- sectionId: section.id
212
- })
213
- }
214
- onRename={(newLabel) =>
215
- sendAction('RENAME_SECTION', {
216
- placeholderId: placeholderSlug,
217
- sectionId: section.id,
218
- label: newLabel
219
- })
220
- }
221
- />
177
+ <SectionErrorBoundary key={section.id} sectionId={section.id}>
178
+ <ThemeSection
179
+ section={section}
180
+ placeholderId={placeholderSlug}
181
+ isDesigner={isDesigner}
182
+ isSelected={selectedSectionId === section.id}
183
+ selectedBlockId={selectedBlockId}
184
+ currentBreakpoint={currentBreakpoint}
185
+ onSelect={setSelectedSectionId}
186
+ onMoveUp={() =>
187
+ sendAction('MOVE_SECTION_UP', {
188
+ placeholderId: placeholderSlug,
189
+ sectionId: section.id
190
+ })
191
+ }
192
+ onMoveDown={() =>
193
+ sendAction('MOVE_SECTION_DOWN', {
194
+ placeholderId: placeholderSlug,
195
+ sectionId: section.id
196
+ })
197
+ }
198
+ onDuplicate={() =>
199
+ sendAction('DUPLICATE_SECTION', {
200
+ placeholderId: placeholderSlug,
201
+ sectionId: section.id
202
+ })
203
+ }
204
+ onToggleVisibility={() =>
205
+ sendAction('TOGGLE_SECTION_VISIBILITY', {
206
+ placeholderId: placeholderSlug,
207
+ sectionId: section.id
208
+ })
209
+ }
210
+ onDelete={() =>
211
+ sendAction('DELETE_SECTION', {
212
+ placeholderId: placeholderSlug,
213
+ sectionId: section.id
214
+ })
215
+ }
216
+ onRename={(newLabel) =>
217
+ sendAction('RENAME_SECTION', {
218
+ placeholderId: placeholderSlug,
219
+ sectionId: section.id,
220
+ label: newLabel
221
+ })
222
+ }
223
+ />
224
+ </SectionErrorBoundary>
222
225
  ))}
223
226
  </div>
224
227
  );