@akinon/pz-theme 2.0.153-rc.4 → 2.0.153

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,43 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.153-rc.4
4
-
5
- ### Patch Changes
6
-
7
- - ad5240c4: ZERO-5254: add custom render and theme editor support to pz-rate-review
8
- - Updated dependencies [219164a3]
9
- - Updated dependencies [86187b1c2]
10
- - Updated dependencies [0cf9ea239]
11
- - Updated dependencies [97d29893]
12
- - Updated dependencies [324f97d55]
13
- - Updated dependencies [51ea06888]
14
- - Updated dependencies
15
- - Updated dependencies [2fea41430]
16
- - Updated dependencies [b55acb768]
17
- - Updated dependencies [760258c1c]
18
- - Updated dependencies [143be2b9d]
19
- - Updated dependencies [7889b08fe]
20
- - Updated dependencies [9f8cd3bc5]
21
- - Updated dependencies [bfafa3f49]
22
- - Updated dependencies [57d7eb305]
23
- - Updated dependencies [d99a6a7d5]
24
- - Updated dependencies [9db81a714]
25
- - Updated dependencies [591e345e1]
26
- - Updated dependencies [4de5303c5]
27
- - Updated dependencies [95b139dc1]
28
- - Updated dependencies [1d00f2d06]
29
- - Updated dependencies [4ac7b2a1e]
30
- - Updated dependencies [4998a9631]
31
- - Updated dependencies [804d2bd6c]
32
- - Updated dependencies [3909d3224]
33
- - Updated dependencies [6a3d8a631]
34
- - Updated dependencies [e18836b20]
35
- - Updated dependencies [b1111d7b1]
36
- - Updated dependencies [1595d458e]
37
- - Updated dependencies [dca128112]
38
- - Updated dependencies [9c778307]
39
- - @akinon/next@2.0.153-rc.4
40
- - @akinon/theme-contract@2.0.153-rc.4
3
+ ## 2.0.153
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3e0d8af7]
8
+ - @akinon/next@2.0.153
9
+ - @akinon/theme-contract@2.0.153
41
10
 
42
11
  ## 2.0.152
43
12
 
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.153-rc.4",
4
+ "version": "2.0.153",
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.153-rc.4",
11
+ "@akinon/next": "2.0.153",
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.153-rc.4",
16
+ "@akinon/theme-contract": "2.0.153",
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.153-rc.4",
24
+ "@akinon/next": "2.0.153",
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",
@@ -15,7 +15,6 @@ import {
15
15
  resolveItemProduct
16
16
  } from './item-template-utils';
17
17
  import { ThemeItemScopeProvider } from './product-block-utils';
18
- import { ThemeRowScopeProvider } from './row-scope';
19
18
 
20
19
  export {
21
20
  findItemTemplate,
@@ -121,15 +120,20 @@ export const ItemTemplateList = ({
121
120
  const scopesProducts = publishesProductItems(block.type);
122
121
  // The same slice the iterator applied, so clone N is handed the row it
123
122
  // was built from rather than row N.
124
- const slicedRows = sliceIteratorItems(block, rows).items;
123
+ const scopedRows = scopesProducts
124
+ ? sliceIteratorItems(block, rows).items
125
+ : rows;
125
126
 
126
127
  return (
127
128
  <>
128
129
  {clones.map((clone, index) => {
129
130
  const key = clone.id || `${template.id}-item-${index}`;
130
- const rowData = slicedRows[index] ?? {};
131
131
  const row = (
132
132
  <ThemeBlock
133
+ // Keyed here so the non-scoped branch is the element it always
134
+ // was: wrapping it in anything, even a fragment, would change the
135
+ // element type at this position and remount every existing row.
136
+ key={key}
133
137
  block={clone}
134
138
  placeholderId={placeholderId}
135
139
  sectionId={sectionId}
@@ -139,32 +143,22 @@ export const ItemTemplateList = ({
139
143
  currentBreakpoint={currentBreakpoint}
140
144
  />
141
145
  );
142
- const scoped = scopesProducts
143
- ? resolveItemProduct(block.type, rowData)
144
- : null;
145
146
 
146
- // Context only — the providers render no element, so the markup a
147
- // published row produces is unchanged. Every list opens the row
148
- // scope; only product lists open the product scope on top of it.
147
+ if (!scopesProducts) return row;
148
+
149
+ const scoped = resolveItemProduct(block.type, scopedRows[index]);
150
+
151
+ // Context only — the provider renders no element, so the markup a
152
+ // published row produces is unchanged.
149
153
  return (
150
- <ThemeRowScopeProvider
154
+ <ThemeItemScopeProvider
151
155
  key={key}
152
- listType={block.type}
153
- row={rowData}
154
- index={index}
156
+ product={
157
+ scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
158
+ }
155
159
  >
156
- {scopesProducts ? (
157
- <ThemeItemScopeProvider
158
- product={
159
- scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
160
- }
161
- >
162
- {row}
163
- </ThemeItemScopeProvider>
164
- ) : (
165
- row
166
- )}
167
- </ThemeRowScopeProvider>
160
+ {row}
161
+ </ThemeItemScopeProvider>
168
162
  );
169
163
  })}
170
164
  </>
@@ -13,14 +13,7 @@ import { readToggle } from './product-block-utils';
13
13
  import { BlockRendererProps } from './block-renderer-registry';
14
14
 
15
15
  interface OrderItemLike {
16
- id?: number;
17
- status?: { value?: string | number; label?: string };
18
- product?: {
19
- image?: string;
20
- name?: string;
21
- sku?: string;
22
- base_code?: string;
23
- } | null;
16
+ product?: { image?: string; name?: string } | null;
24
17
  }
25
18
 
26
19
  interface OrderLike {
@@ -255,23 +248,7 @@ const OrderListBlock = ({
255
248
  maxImages
256
249
  ),
257
250
  url: hrefFor(order),
258
- href: hrefFor(order),
259
- createdDate: order.created_date || '',
260
- items: (Array.isArray(order.orderitem_set) ? order.orderitem_set : []).map(
261
- (item, itemIndex) => ({
262
- id: item?.id ?? itemIndex,
263
- name: item?.product?.name || '',
264
- image: item?.product?.image || '',
265
- status: item?.status?.label || '',
266
- statusValue: String(item?.status?.value ?? ''),
267
- product: {
268
- sku: item?.product?.sku || '',
269
- base_code: item?.product?.base_code || '',
270
- name: item?.product?.name || '',
271
- image: item?.product?.image || ''
272
- }
273
- })
274
- )
251
+ href: hrefFor(order)
275
252
  }));
276
253
 
277
254
  // A designer who marked a child as the row layout gets that layout repeated
@@ -41,7 +41,7 @@ const orderPayload = {
41
41
  orderitem_set: [
42
42
  {
43
43
  price: '649.95',
44
- status: { value: '500', label: 'Shipped' },
44
+ status: { label: 'Shipped' },
45
45
  product: {
46
46
  name: 'Spring Sneaker',
47
47
  image: '/i.jpg',
@@ -63,19 +63,6 @@ describe('buildOrderPageContext', () => {
63
63
  expect(context.order.currency).toBe('TRY');
64
64
  expect(context.order.items).toHaveLength(1);
65
65
  expect(context.order.items[0].title).toBe('Spring Sneaker');
66
- expect(context.order.items[0].status).toBe('Shipped');
67
- expect(context.order.items[0].statusValue).toBe('500');
68
- });
69
-
70
- it('stringifies a numeric order item status code', () => {
71
- const context = buildOrderPageContext({
72
- order: {
73
- ...orderPayload,
74
- orderitem_set: [{ status: { value: 550, label: 'Delivered' } }]
75
- }
76
- });
77
-
78
- expect(context.order.items[0].statusValue).toBe('550');
79
66
  });
80
67
 
81
68
  it('does not publish the identity fields the raw payload carries', () => {
@@ -59,9 +59,6 @@ const toRecord = (value: unknown): UnknownRecord =>
59
59
  const toStringValue = (value: unknown, fallback = ''): string =>
60
60
  typeof value === 'string' ? value : fallback;
61
61
 
62
- const toCodeValue = (value: unknown): string =>
63
- typeof value === 'number' ? String(value) : toStringValue(value);
64
-
65
62
  const humanizeSlug = (value: string) =>
66
63
  value
67
64
  .split('-')
@@ -482,7 +479,6 @@ export const buildOrderPageContext = ({
482
479
  unitPrice: toStringValue(item.price),
483
480
  quantity: 1,
484
481
  status: toStringValue(toRecord(item.status).label),
485
- statusValue: toCodeValue(toRecord(item.status).value),
486
482
  currency
487
483
  };
488
484
  });
@@ -303,26 +303,6 @@ attached". The editor's **Use preview** toggle turns it off while the editor
303
303
  stays open, so the canvas behaves like the live site: whatever you held open
304
304
  closes and links navigate again.
305
305
 
306
- ## Reading the row a block is placed in
307
-
308
- A block dropped into a list's row layout (the child a designer marked as the
309
- row template of an order list, a product grid, an address list, …) renders once
310
- per row. The product lists open the product scope `useThemeProduct()` reads;
311
- every list also opens a row scope that carries the row itself:
312
-
313
- ```ts
314
- import { useThemeRowScope } from '@akinon/pz-theme/src/blocks/row-scope';
315
-
316
- const scope = useThemeRowScope();
317
- ```
318
-
319
- Inside a row it returns `{ listType, row, index }`, for example
320
- `listType: 'order-list'` with the order's `number` and `items` on `row`; anywhere
321
- else it returns `null`. `row` is the same record the row's bindings resolve
322
- against, so whatever a text block can bind to (`item.number`), code can read.
323
- Import it from that deep path for the same reason as `useThemePageContext`: the
324
- package root is not importable from a client module.
325
-
326
306
  ## Reporting what happened
327
307
 
328
308
  A custom unit can report a moment — a strip was seen, a lookbook was opened, a
@@ -1,118 +0,0 @@
1
- import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
- import { ItemTemplateList } from '../blocks/item-template';
4
- import { useThemeProduct } from '../blocks/product-block-utils';
5
- import { useThemeRowScope } from '../blocks/row-scope';
6
- import blockRendererRegistry from '../blocks/block-renderer-registry';
7
- import { resetCustomCatalog } from '../custom/catalog';
8
- import { resetCustomCollection } from '../custom/collect';
9
- import { isCustomThemeType, registerCustomBlocks } from '../custom';
10
- import type { Block } from '../theme-block';
11
-
12
- const READER = 'custom:acme:row-reader';
13
-
14
- function RowReader() {
15
- const scope = useThemeRowScope();
16
- const product = useThemeProduct();
17
-
18
- return (
19
- <span
20
- data-testid="reader"
21
- data-list={scope?.listType ?? ''}
22
- data-index={scope?.index ?? ''}
23
- data-number={String(scope?.row.number ?? '')}
24
- data-product={String(product?.pk ?? '')}
25
- />
26
- );
27
- }
28
-
29
- const template = (): Block =>
30
- ({
31
- id: 'tpl',
32
- type: 'group',
33
- label: 'Row',
34
- order: 0,
35
- hidden: false,
36
- value: '',
37
- properties: { role: { desktop: 'item-template' } },
38
- styles: {},
39
- blocks: [
40
- {
41
- id: 'tpl-reader',
42
- type: READER,
43
- label: 'Reader',
44
- order: 0,
45
- hidden: false,
46
- value: '',
47
- properties: {},
48
- styles: {},
49
- blocks: []
50
- }
51
- ]
52
- } as Block);
53
-
54
- const list = (type: string): Block =>
55
- ({
56
- id: 'list',
57
- type,
58
- label: 'List',
59
- order: 0,
60
- hidden: false,
61
- value: '',
62
- properties: {},
63
- styles: {},
64
- blocks: [template()]
65
- } as Block);
66
-
67
- const renderRows = (type: string, items: Record<string, unknown>[]) =>
68
- render(
69
- <ItemTemplateList
70
- block={list(type)}
71
- items={items}
72
- placeholderId="p1"
73
- sectionId="s1"
74
- />
75
- );
76
-
77
- beforeAll(() => {
78
- registerCustomBlocks('acme', { 'row-reader': RowReader });
79
- });
80
-
81
- afterAll(() => {
82
- const map = (
83
- blockRendererRegistry as unknown as { renderers: Map<string, unknown> }
84
- ).renderers;
85
- Array.from(map.keys())
86
- .filter(isCustomThemeType)
87
- .forEach((key) => map.delete(key));
88
- resetCustomCatalog();
89
- resetCustomCollection();
90
- });
91
-
92
- describe('the row scope', () => {
93
- it('hands every repeated row to the blocks inside it, on any list', () => {
94
- renderRows('order-list', [{ number: 'ORD-1' }, { number: 'ORD-2' }]);
95
-
96
- const readers = screen.getAllByTestId('reader');
97
- expect(readers).toHaveLength(2);
98
- expect(readers[0]).toHaveAttribute('data-list', 'order-list');
99
- expect(readers[0]).toHaveAttribute('data-index', '0');
100
- expect(readers[0]).toHaveAttribute('data-number', 'ORD-1');
101
- expect(readers[1]).toHaveAttribute('data-number', 'ORD-2');
102
- expect(readers[0]).toHaveAttribute('data-product', '');
103
- });
104
-
105
- it('keeps the product scope on product lists next to the row scope', () => {
106
- renderRows('product-grid', [{ pk: 7, name: 'Sneaker' }]);
107
-
108
- const reader = screen.getByTestId('reader');
109
- expect(reader).toHaveAttribute('data-list', 'product-grid');
110
- expect(reader).toHaveAttribute('data-product', '7');
111
- });
112
-
113
- it('is closed outside a repeated row', () => {
114
- render(<RowReader />);
115
-
116
- expect(screen.getByTestId('reader')).toHaveAttribute('data-list', '');
117
- });
118
- });
@@ -1,112 +0,0 @@
1
- import React from 'react';
2
- import { render } from '@testing-library/react';
3
- import OrderListBlock from '../order-list-block';
4
-
5
- const captured: { items: Record<string, unknown>[] } = { items: [] };
6
-
7
- jest.mock('../item-template', () => ({
8
- __esModule: true,
9
- hasItemTemplate: () => true,
10
- ItemTemplateList: ({ items }: { items: Record<string, unknown>[] }) => {
11
- captured.items = items;
12
- return <div data-testid="item-template-list" />;
13
- }
14
- }));
15
-
16
- jest.mock('next-auth/react', () => ({
17
- useSession: () => ({ data: null, status: 'authenticated' })
18
- }));
19
-
20
- jest.mock('@akinon/next/hooks', () => ({
21
- __esModule: true,
22
- useLocalization: () => ({ locale: 'tr', currency: 'try' })
23
- }));
24
-
25
- jest.mock('@akinon/next/redux/hooks', () => ({
26
- __esModule: true,
27
- useAppSelector: () => undefined,
28
- useAppDispatch: () => jest.fn()
29
- }));
30
-
31
- jest.mock('@akinon/next/data/client/account', () => ({
32
- __esModule: true,
33
- useGetOrdersQuery: () => ({
34
- data: {
35
- count: 1,
36
- results: [
37
- {
38
- id: 42,
39
- number: 'ORD-42',
40
- amount: '100.00',
41
- created_date: '2026-09-01T10:00:00Z',
42
- status: { value: '550', label: 'Delivered' },
43
- orderitem_set: [
44
- {
45
- id: 7,
46
- status: { value: 550, label: 'Delivered' },
47
- product: {
48
- sku: 'SKU-1',
49
- base_code: 'BASE-1',
50
- name: 'Sneaker',
51
- image: '/s.jpg'
52
- }
53
- },
54
- {
55
- status: { value: '400', label: 'Approved' },
56
- product: { name: 'Boot' }
57
- }
58
- ]
59
- }
60
- ]
61
- },
62
- isLoading: false
63
- })
64
- }));
65
-
66
- describe('order list rows', () => {
67
- it('publishes the order items with their product codes and status code', () => {
68
- render(
69
- <OrderListBlock
70
- block={{
71
- id: 'order-list-1',
72
- type: 'order-list',
73
- properties: {},
74
- styles: {},
75
- blocks: []
76
- }}
77
- placeholderId="p1"
78
- sectionId="s1"
79
- />
80
- );
81
-
82
- expect(captured.items).toHaveLength(1);
83
- expect(captured.items[0]).toMatchObject({
84
- id: 42,
85
- number: 'ORD-42',
86
- createdDate: '2026-09-01T10:00:00Z'
87
- });
88
- expect(captured.items[0].items).toEqual([
89
- {
90
- id: 7,
91
- name: 'Sneaker',
92
- image: '/s.jpg',
93
- status: 'Delivered',
94
- statusValue: '550',
95
- product: {
96
- sku: 'SKU-1',
97
- base_code: 'BASE-1',
98
- name: 'Sneaker',
99
- image: '/s.jpg'
100
- }
101
- },
102
- {
103
- id: 1,
104
- name: 'Boot',
105
- image: '',
106
- status: 'Approved',
107
- statusValue: '400',
108
- product: { sku: '', base_code: '', name: 'Boot', image: '' }
109
- }
110
- ]);
111
- });
112
- });
@@ -1,35 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- export interface ThemeRowScopeValue {
6
- listType: string;
7
- row: Record<string, unknown>;
8
- index: number;
9
- }
10
-
11
- const ThemeRowScopeContext = React.createContext<ThemeRowScopeValue | null>(
12
- null
13
- );
14
-
15
- export const ThemeRowScopeProvider = ({
16
- listType,
17
- row,
18
- index,
19
- children
20
- }: ThemeRowScopeValue & { children: React.ReactNode }) => {
21
- const value = React.useMemo(
22
- () => ({ listType, row, index }),
23
- [listType, row, index]
24
- );
25
-
26
- return (
27
- <ThemeRowScopeContext.Provider value={value}>
28
- {children}
29
- </ThemeRowScopeContext.Provider>
30
- );
31
- };
32
-
33
- /** Non-null while a repeated row of any list renders; null everywhere else. */
34
- export const useThemeRowScope = (): ThemeRowScopeValue | null =>
35
- React.useContext(ThemeRowScopeContext);