@akinon/pz-theme 2.0.47-rc.0 → 2.0.48-beta.0

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,36 +1,20 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.47-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0cf9ea239]
8
- - Updated dependencies [324f97d55]
9
- - Updated dependencies [51ea0688]
10
- - Updated dependencies
11
- - Updated dependencies [2fea414]
12
- - Updated dependencies [b55acb768]
13
- - Updated dependencies [760258c1]
14
- - Updated dependencies [143be2b9d]
15
- - Updated dependencies [7889b08fe]
16
- - Updated dependencies [9f8cd3bc5]
17
- - Updated dependencies [bfafa3f4]
18
- - Updated dependencies [57d7eb305]
19
- - Updated dependencies [d99a6a7d5]
20
- - Updated dependencies [9db81a714]
21
- - Updated dependencies [591e345e1]
22
- - Updated dependencies [4de5303c5]
23
- - Updated dependencies [95b139dc1]
24
- - Updated dependencies [1d00f2d06]
25
- - Updated dependencies [4ac7b2a1e]
26
- - Updated dependencies [e9598c7]
27
- - Updated dependencies [4998a9631]
28
- - Updated dependencies [804d2bd6]
29
- - Updated dependencies [3909d3224]
30
- - Updated dependencies [6a3d8a63]
31
- - Updated dependencies [e18836b2]
32
- - @akinon/next@2.0.47-rc.0
33
- - @akinon/theme-contract@2.0.47-rc.0
3
+ ## 2.0.48-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [cbbbfd75]
8
+ - @akinon/next@2.0.48-beta.0
9
+ - @akinon/theme-contract@2.0.48-beta.0
10
+
11
+ ## 2.0.47
12
+
13
+ ### Patch Changes
14
+
15
+ - b9a50e8: ZERO-4740: Promote the size guide renderer into a generic Panel block with drawer, modal, sheet and anchored presentations
16
+ - @akinon/next@2.0.47
17
+ - @akinon/theme-contract@2.0.47
34
18
 
35
19
  ## 2.0.46
36
20
 
package/package.json CHANGED
@@ -1,26 +1,26 @@
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.47-rc.0",
4
+ "version": "2.0.48-beta.0",
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.47-rc.0",
11
+ "@akinon/next": "2.0.48-beta.0",
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.47-rc.0",
16
+ "@akinon/theme-contract": "2.0.48-beta.0",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "tailwind-merge": "^2.5.4",
20
20
  "zod": "4.4.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@akinon/next": "2.0.47-rc.0",
23
+ "@akinon/next": "2.0.48-beta.0",
24
24
  "@testing-library/jest-dom": "5.16.4",
25
25
  "@testing-library/react": "16.3.2",
26
26
  "@testing-library/user-event": "14.1.1",
@@ -77,6 +77,7 @@ const EXPECTED_BLOCK_TYPES = [
77
77
  'order-list',
78
78
  'order-success',
79
79
  'order-tracking',
80
+ 'panel',
80
81
  'payment-badges',
81
82
  'product-attributes',
82
83
  'product-breadcrumb',
@@ -54,6 +54,7 @@ import HamburgerMenuBlock from '../hamburger-menu-block';
54
54
  import MegaMenuBlock from '../mega-menu-block';
55
55
  import NavMenuBlock from '../nav-menu-block';
56
56
  import HotspotBlock from '../hotspot-block';
57
+ import PanelBlock from '../panel-block';
57
58
  import { PartStatePreview } from '../../components/part-state-preview';
58
59
 
59
60
  const mkBlock = (
@@ -376,3 +377,94 @@ describe('HotspotBlock under force-open', () => {
376
377
  expect(screen.queryByText('Hot Title')).not.toBeInTheDocument();
377
378
  });
378
379
  });
380
+
381
+ describe('PanelBlock under force-open', () => {
382
+ // A panel with nothing in it disappears on the storefront, so the byte-for-
383
+ // byte comparison below would otherwise compare two empty strings.
384
+ const child = {
385
+ id: 'row-1',
386
+ type: 'text',
387
+ label: 'Row',
388
+ order: 0,
389
+ hidden: false,
390
+ value: 'S / M / L',
391
+ properties: {},
392
+ styles: {},
393
+ blocks: []
394
+ };
395
+
396
+ const renderPanel = (
397
+ isDesigner: boolean,
398
+ styles: Record<string, unknown>,
399
+ properties: Record<string, unknown> = {}
400
+ ) =>
401
+ render(
402
+ <PanelBlock
403
+ block={
404
+ {
405
+ ...(mkBlock('size-guide', properties, styles) as object),
406
+ blocks: [child]
407
+ } as never
408
+ }
409
+ isDesigner={isDesigner}
410
+ {...blockProps}
411
+ />
412
+ );
413
+
414
+ it('mounts the backdrop and the panel with no interaction', () => {
415
+ renderPanel(true, { forceOpen: true });
416
+
417
+ // The backdrop is the point: it cannot be coloured or blurred in a canvas
418
+ // that never draws it.
419
+ expect(
420
+ document.querySelector('[data-part="overlay"][data-state="open"]')
421
+ ).toBeInTheDocument();
422
+ expect(
423
+ document.querySelector('[data-part="modal"][data-state="open"]')
424
+ ).toBeInTheDocument();
425
+ expect(document.querySelector('[data-part="trigger"]')).toHaveAttribute(
426
+ 'data-state',
427
+ 'open'
428
+ );
429
+ });
430
+
431
+ it('leaves the panel mounted when the backdrop is clicked', () => {
432
+ renderPanel(true, { forceOpen: true });
433
+
434
+ fireEvent.click(document.querySelector('[data-part="overlay"]') as Element);
435
+
436
+ expect(
437
+ document.querySelector('[data-part="modal"][data-state="open"]')
438
+ ).toBeInTheDocument();
439
+ });
440
+
441
+ it('leaves the panel mounted when the close button is clicked', () => {
442
+ renderPanel(true, { forceOpen: true });
443
+
444
+ fireEvent.click(document.querySelector('[data-size-guide-close]') as Element);
445
+
446
+ expect(
447
+ document.querySelector('[data-part="modal"][data-state="open"]')
448
+ ).toBeInTheDocument();
449
+ });
450
+
451
+ it('holds the drawer open too, backdrop and all', () => {
452
+ renderPanel(true, { forceOpen: true }, { presentation: 'drawer' });
453
+
454
+ expect(
455
+ document.querySelector('[data-panel-presentation="drawer"]')
456
+ ).toBeInTheDocument();
457
+ expect(document.querySelector('[data-part="overlay"]')).toBeInTheDocument();
458
+ });
459
+
460
+ it('ignores forceOpen on the storefront, byte for byte', () => {
461
+ const forced = renderPanel(false, { forceOpen: true });
462
+ const forcedHtml = forced.container.innerHTML;
463
+ forced.unmount();
464
+
465
+ const plain = renderPanel(false, {});
466
+
467
+ expect(document.querySelector('[data-part="modal"]')).toBeNull();
468
+ expect(forcedHtml).toBe(plain.container.innerHTML);
469
+ });
470
+ });
@@ -23,7 +23,7 @@ jest.mock('@akinon/next/data/client/misc', () => ({
23
23
  }));
24
24
 
25
25
  import FacetFiltersBlock from '../facet-filters-block';
26
- import SizeGuideBlock from '../size-guide-block';
26
+ import PanelBlock from '../panel-block';
27
27
  import HamburgerMenuBlock from '../hamburger-menu-block';
28
28
  import MegaMenuBlock from '../mega-menu-block';
29
29
  import SortSelectBlock from '../sort-select-block';
@@ -356,7 +356,7 @@ describe('the filter drawer next to an overlay already on the primitive', () =>
356
356
  describe('the size guide next to an overlay already on the primitive', () => {
357
357
  const SizeGuidePage = ({ migratedOpen = false }: { migratedOpen?: boolean }) => (
358
358
  <>
359
- <SizeGuideBlock
359
+ <PanelBlock
360
360
  block={mkBlock('size-guide', {}, [
361
361
  {
362
362
  id: 'row-1',
@@ -421,7 +421,7 @@ describe('the size guide next to an overlay already on the primitive', () => {
421
421
 
422
422
  it('honours a composition that turned Escape off', () => {
423
423
  render(
424
- <SizeGuideBlock
424
+ <PanelBlock
425
425
  block={mkBlock('size-guide', { closeOnEscape: false }, [
426
426
  {
427
427
  id: 'row-1',
@@ -449,6 +449,42 @@ describe('the size guide next to an overlay already on the primitive', () => {
449
449
  screen.getByRole('dialog', { name: 'Size guide' })
450
450
  ).toBeInTheDocument();
451
451
  });
452
+
453
+ // The same renderer under its current type id. The cases above are now the
454
+ // legacy-alias regression too, so both keys are exercised against the
455
+ // primitive rather than only the one the block used to be called.
456
+ it('behaves the same way under the `panel` type', () => {
457
+ render(
458
+ <PanelBlock
459
+ block={mkBlock('panel', {}, [
460
+ {
461
+ id: 'row-1',
462
+ type: 'text',
463
+ label: 'Row',
464
+ order: 0,
465
+ hidden: false,
466
+ value: 'S / M / L',
467
+ properties: {},
468
+ styles: {},
469
+ blocks: []
470
+ }
471
+ ])}
472
+ placeholderId="p"
473
+ sectionId="s"
474
+ isDesigner={false}
475
+ currentBreakpoint="desktop"
476
+ />
477
+ );
478
+ openGuide();
479
+
480
+ expect(
481
+ screen.getByRole('dialog', { name: 'Size guide' })
482
+ ).toHaveAttribute('aria-modal', 'true');
483
+
484
+ fireEvent.keyDown(document, { key: 'Escape' });
485
+
486
+ expect(screen.queryByRole('dialog', { name: 'Size guide' })).toBeNull();
487
+ });
452
488
  });
453
489
 
454
490
  describe('the hamburger drawer next to an overlay already on the primitive', () => {
@@ -0,0 +1,329 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+
4
+ // Stub the recursive ThemeBlock (default export) with a countable marker, so
5
+ // the golden captures this block's own markup and nothing downstream of it.
6
+ jest.mock('../../theme-block', () => ({
7
+ __esModule: true,
8
+ default: ({ block }: { block: { id: string } }) => (
9
+ <div data-testid="tb" data-id={block.id} />
10
+ )
11
+ }));
12
+
13
+ import PanelBlock from '../panel-block';
14
+ import blockRendererRegistry from '../block-renderer-registry';
15
+ import GOLDEN from './panel-legacy-golden.json';
16
+
17
+ const CHILD = {
18
+ id: 'child-1',
19
+ type: 'text',
20
+ label: 'row',
21
+ order: 0,
22
+ hidden: false,
23
+ value: 'row',
24
+ properties: {},
25
+ styles: {},
26
+ blocks: []
27
+ };
28
+
29
+ const mkBlock = (
30
+ type: string,
31
+ properties: Record<string, unknown> = {},
32
+ styles: Record<string, unknown> = {},
33
+ blocks: unknown[] = [CHILD]
34
+ ) =>
35
+ ({
36
+ id: `${type}-1`,
37
+ type,
38
+ label: type,
39
+ order: 0,
40
+ hidden: false,
41
+ value: '',
42
+ properties,
43
+ styles,
44
+ blocks
45
+ } as never);
46
+
47
+ const renderPanel = (
48
+ properties: Record<string, unknown>,
49
+ {
50
+ isDesigner = false,
51
+ open = false,
52
+ type = 'size-guide',
53
+ currentBreakpoint = 'desktop',
54
+ styles = {},
55
+ blocks = [CHILD]
56
+ }: {
57
+ isDesigner?: boolean;
58
+ open?: boolean;
59
+ type?: string;
60
+ currentBreakpoint?: string;
61
+ styles?: Record<string, unknown>;
62
+ blocks?: unknown[];
63
+ } = {}
64
+ ) => {
65
+ const view = render(
66
+ <PanelBlock
67
+ block={mkBlock(type, properties, styles, blocks)}
68
+ isDesigner={isDesigner}
69
+ placeholderId="p"
70
+ sectionId="s"
71
+ currentBreakpoint={currentBreakpoint}
72
+ />
73
+ );
74
+
75
+ if (open) {
76
+ fireEvent.click(
77
+ view.container.querySelector('[data-size-guide-trigger]') as Element
78
+ );
79
+ }
80
+
81
+ return view;
82
+ };
83
+
84
+ /* ------------------------------------------------------------------ */
85
+ /* Back-compat: a published `size-guide` renders exactly as before. */
86
+ /* ------------------------------------------------------------------ */
87
+
88
+ describe('the legacy size-guide markup, byte for byte', () => {
89
+ // Captured from the unmodified size-guide renderer BEFORE this block was
90
+ // generalised. Every one of these is markup a live storefront is already
91
+ // styling, so nothing here may move: the centred modal keeps its combined
92
+ // root/backdrop node, its attribute order and its missing `tabIndex`.
93
+ const CASES: Array<[string, Record<string, unknown>, boolean, boolean]> = [
94
+ ['legacy-closed', {}, false, false],
95
+ ['legacy-open', {}, false, true],
96
+ [
97
+ 'legacy-open-titled',
98
+ { modalTitle: 'Size chart', modalWidth: 900 },
99
+ false,
100
+ true
101
+ ],
102
+ ['legacy-open-noclose', { showClose: false }, false, true],
103
+ ['legacy-designer', {}, true, false]
104
+ ];
105
+
106
+ it.each(CASES)('%s matches the pre-Panel golden', (name, properties, isDesigner, open) => {
107
+ const { container } = renderPanel(properties as Record<string, unknown>, {
108
+ isDesigner: isDesigner as boolean,
109
+ open: open as boolean
110
+ });
111
+
112
+ expect(container.innerHTML).toBe(
113
+ (GOLDEN as Record<string, string>)[name as string]
114
+ );
115
+ });
116
+
117
+ it('covers every captured case', () => {
118
+ expect(CASES.map(([n]) => n).sort()).toEqual(Object.keys(GOLDEN).sort());
119
+ });
120
+
121
+ it('renders the same markup whether or not `modal` was chosen explicitly', () => {
122
+ const implicit = renderPanel({}, { open: true });
123
+ const implicitHtml = implicit.container.innerHTML;
124
+ implicit.unmount();
125
+
126
+ const explicit = renderPanel({ presentation: 'modal' }, { open: true });
127
+
128
+ expect(explicit.container.innerHTML).toBe(implicitHtml);
129
+ });
130
+
131
+ it('renders the same markup under the `panel` type as under `size-guide`', () => {
132
+ const legacy = renderPanel({}, { open: true, type: 'size-guide' });
133
+ const legacyHtml = legacy.container.innerHTML;
134
+ legacy.unmount();
135
+
136
+ const current = renderPanel({}, { open: true, type: 'panel' });
137
+
138
+ expect(current.container.innerHTML).toBe(legacyHtml);
139
+ });
140
+ });
141
+
142
+ describe('the renderer registry', () => {
143
+ it('resolves the legacy type to the very same renderer', () => {
144
+ expect(blockRendererRegistry.getRenderer('panel')).toBe(PanelBlock);
145
+ expect(blockRendererRegistry.getRenderer('size-guide')).toBe(
146
+ blockRendererRegistry.getRenderer('panel')
147
+ );
148
+ });
149
+ });
150
+
151
+ /* ------------------------------------------------------------------ */
152
+ /* Presentations. */
153
+ /* ------------------------------------------------------------------ */
154
+
155
+ describe('presentation modes', () => {
156
+ it('draws the drawer against the side the composition picked', () => {
157
+ const { container } = renderPanel(
158
+ { presentation: 'drawer', panelSide: 'left' },
159
+ { open: true }
160
+ );
161
+
162
+ const panel = container.querySelector('[data-part="modal"]');
163
+ expect(panel).toHaveAttribute('data-panel-presentation', 'drawer');
164
+ expect(panel).toHaveAttribute('data-state', 'open');
165
+ expect((panel as HTMLElement).style.left).toBe('0px');
166
+ expect((panel as HTMLElement).style.right).toBe('');
167
+ expect(container.querySelector('[data-part="overlay"]')).toHaveAttribute(
168
+ 'data-state',
169
+ 'open'
170
+ );
171
+ // Only the sheet grows a grab bar.
172
+ expect(container.querySelector('[data-part="handle"]')).toBeNull();
173
+ });
174
+
175
+ it('draws the sheet from the bottom with a grab bar', () => {
176
+ const { container } = renderPanel(
177
+ { presentation: 'sheet', sheetHeight: 40 },
178
+ { open: true }
179
+ );
180
+
181
+ const panel = container.querySelector('[data-part="modal"]');
182
+ expect(panel).toHaveAttribute('data-panel-presentation', 'sheet');
183
+ expect((panel as HTMLElement).style.maxHeight).toBe('40vh');
184
+ expect(container.querySelector('[data-part="handle"]')).toBeInTheDocument();
185
+ });
186
+
187
+ it('hangs the anchored panel off the block root rather than a fixed layer', () => {
188
+ const { container } = renderPanel(
189
+ { presentation: 'anchored', anchorOffset: 12, anchorAlign: 'end' },
190
+ { open: true }
191
+ );
192
+
193
+ const root = container.querySelector('[data-size-guide]') as HTMLElement;
194
+ expect(root.style.position).toBe('relative');
195
+
196
+ const panel = container.querySelector('[data-part="modal"]') as HTMLElement;
197
+ expect(panel).toHaveAttribute('data-panel-presentation', 'anchored');
198
+ expect(panel.style.top).toBe('calc(100% + 12px)');
199
+ expect(panel.style.right).toBe('0px');
200
+ // The panel is a child of the block root, not of the fixed backdrop layer.
201
+ expect(panel.closest('[data-panel-root]')).toBeNull();
202
+ });
203
+
204
+ it('keeps the bare root every other presentation shipped with', () => {
205
+ const { container } = renderPanel(
206
+ { presentation: 'drawer' },
207
+ { open: true }
208
+ );
209
+
210
+ expect(
211
+ (container.querySelector('[data-size-guide]') as HTMLElement).getAttribute(
212
+ 'style'
213
+ )
214
+ ).toBeNull();
215
+ });
216
+
217
+ it('resolves the presentation per breakpoint', () => {
218
+ const { container } = renderPanel(
219
+ { presentation: { desktop: 'modal', mobile: 'sheet' } },
220
+ { open: true, currentBreakpoint: 'mobile' }
221
+ );
222
+
223
+ expect(container.querySelector('[data-part="modal"]')).toHaveAttribute(
224
+ 'data-panel-presentation',
225
+ 'sheet'
226
+ );
227
+ expect(container.querySelector('[data-part="handle"]')).toBeInTheDocument();
228
+ });
229
+
230
+ it('falls back to the centred modal for a value it does not know', () => {
231
+ const { container } = renderPanel(
232
+ { presentation: 'popover' },
233
+ { open: true }
234
+ );
235
+
236
+ expect(container.querySelector('[data-size-guide-mode="modal"]')).toBeInTheDocument();
237
+ });
238
+ });
239
+
240
+ /* ------------------------------------------------------------------ */
241
+ /* The backdrop. */
242
+ /* ------------------------------------------------------------------ */
243
+
244
+ describe('the backdrop', () => {
245
+ it('is drawn by default, because every published composition had one', () => {
246
+ const { container } = renderPanel(
247
+ { presentation: 'drawer' },
248
+ { open: true }
249
+ );
250
+
251
+ expect(container.querySelector('[data-part="overlay"]')).toBeInTheDocument();
252
+ });
253
+
254
+ it('lets the page through when it is turned off', () => {
255
+ const { container } = renderPanel(
256
+ { presentation: 'drawer', showBackdrop: false },
257
+ { open: true }
258
+ );
259
+
260
+ expect(container.querySelector('[data-part="overlay"]')).toBeNull();
261
+ expect(
262
+ (container.querySelector('[data-panel-root]') as HTMLElement).style
263
+ .pointerEvents
264
+ ).toBe('none');
265
+ expect(
266
+ (container.querySelector('[data-part="modal"]') as HTMLElement).style
267
+ .pointerEvents
268
+ ).toBe('auto');
269
+ });
270
+
271
+ it('makes no `aria-modal` claim with the page behind still clickable', () => {
272
+ const { container } = renderPanel(
273
+ { presentation: 'anchored', showBackdrop: false },
274
+ { open: true }
275
+ );
276
+
277
+ expect(container.querySelector('[data-part="modal"]')).not.toHaveAttribute(
278
+ 'aria-modal'
279
+ );
280
+ });
281
+
282
+ it('claims the page once a backdrop really covers it', () => {
283
+ const { container } = renderPanel(
284
+ { presentation: 'anchored' },
285
+ { open: true }
286
+ );
287
+
288
+ expect(container.querySelector('[data-part="modal"]')).toHaveAttribute(
289
+ 'aria-modal',
290
+ 'true'
291
+ );
292
+ });
293
+
294
+ it('honours a composition that turned the backdrop dismissal off', () => {
295
+ const { container } = renderPanel(
296
+ { presentation: 'drawer', closeOnOverlay: false },
297
+ { open: true }
298
+ );
299
+
300
+ fireEvent.click(container.querySelector('[data-part="overlay"]') as Element);
301
+
302
+ expect(container.querySelector('[data-part="modal"]')).toBeInTheDocument();
303
+ });
304
+
305
+ it('closes on a backdrop click when it was left on', () => {
306
+ const { container } = renderPanel(
307
+ { presentation: 'drawer' },
308
+ { open: true }
309
+ );
310
+
311
+ fireEvent.click(container.querySelector('[data-part="overlay"]') as Element);
312
+
313
+ expect(container.querySelector('[data-part="modal"]')).toBeNull();
314
+ });
315
+ });
316
+
317
+ describe('an empty panel', () => {
318
+ it('disappears on the storefront rather than offering a dead trigger', () => {
319
+ const { container } = renderPanel({}, { blocks: [] });
320
+
321
+ expect(container.innerHTML).toBe('');
322
+ });
323
+
324
+ it('still shows its drop target in the designer', () => {
325
+ const { container } = renderPanel({}, { isDesigner: true, blocks: [] });
326
+
327
+ expect(container.querySelector('[data-size-guide]')).toBeInTheDocument();
328
+ });
329
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "legacy-closed": "<div data-size-guide=\"\" data-size-guide-state=\"closed\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"false\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\">Size guide</button></div>",
3
+ "legacy-open": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size guide\" style=\"position: relative; width: 100%; max-width: 640px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><button type=\"button\" data-size-guide-close=\"\" data-part=\"close\" aria-label=\"Close\" style=\"position: absolute; top: 12px; right: 12px; background: transparent; cursor: pointer; padding: 0px;\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"></path></svg></button><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
4
+ "legacy-open-titled": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size chart\" style=\"position: relative; width: 100%; max-width: 900px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><button type=\"button\" data-size-guide-close=\"\" data-part=\"close\" aria-label=\"Close\" style=\"position: absolute; top: 12px; right: 12px; background: transparent; cursor: pointer; padding: 0px;\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"></path></svg></button><div data-size-guide-title=\"\" data-part=\"title\" style=\"margin-bottom: 12px; font-weight: 600;\">Size chart</div><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
5
+ "legacy-open-noclose": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size guide\" style=\"position: relative; width: 100%; max-width: 640px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
6
+ "legacy-designer": "<div data-size-guide=\"\" data-size-guide-state=\"inline\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"false\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\">Size guide</button><div data-size-guide-content=\"\" data-size-guide-mode=\"inline\" data-part=\"modal\" style=\"margin-top: 8px; padding: 12px; border: 1px dashed #cbd5e1; border-radius: 4px;\"><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div>"
7
+ }
@@ -66,7 +66,7 @@ import OrderTrackingBlock from './order-tracking-block';
66
66
  import ProductBreadcrumbBlock from './product-breadcrumb-block';
67
67
  import ProductAttributesBlock from './product-attributes-block';
68
68
  import ProductRecommendationsBlock from './product-recommendations-block';
69
- import SizeGuideBlock from './size-guide-block';
69
+ import PanelBlock from './panel-block';
70
70
  import AppliedFiltersBlock from './applied-filters-block';
71
71
  import ListingResultCountBlock from './listing-result-count-block';
72
72
  import CountdownBlock from './countdown-block';
@@ -187,7 +187,10 @@ blockRendererRegistry.register('checkout-summary-outlet', CheckoutSummaryOutletB
187
187
  blockRendererRegistry.register('product-breadcrumb', ProductBreadcrumbBlock);
188
188
  blockRendererRegistry.register('product-attributes', ProductAttributesBlock);
189
189
  blockRendererRegistry.register('product-recommendations', ProductRecommendationsBlock);
190
- blockRendererRegistry.register('size-guide', SizeGuideBlock);
190
+ blockRendererRegistry.register('panel', PanelBlock);
191
+ // Legacy alias: published widget JSON still carries `size-guide`, and the AI
192
+ // generator manifest still emits it. Same renderer, same parts, same markup.
193
+ blockRendererRegistry.register('size-guide', PanelBlock);
191
194
  blockRendererRegistry.register('applied-filters', AppliedFiltersBlock);
192
195
  blockRendererRegistry.register('listing-result-count', ListingResultCountBlock);
193
196
  blockRendererRegistry.register('countdown', CountdownBlock);