@akinon/pz-theme 2.0.162-rc.0 → 2.0.162

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +7 -50
  2. package/package.json +4 -4
  3. package/readme.md +22 -0
  4. package/src/__tests__/custom-catalog-announce.test.ts +110 -0
  5. package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
  6. package/src/blocks/__tests__/text-block.test.tsx +6 -34
  7. package/src/blocks/order-detail-block.tsx +95 -113
  8. package/src/blocks/text-block.tsx +2 -6
  9. package/src/chrome/composition-source.ts +7 -2
  10. package/src/chrome/preview-snapshot-source.ts +5 -1
  11. package/src/content-capabilities.ts +41 -0
  12. package/src/custom/README.md +219 -100
  13. package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
  14. package/src/custom/collect.ts +9 -4
  15. package/src/custom/property-types.ts +15 -15
  16. package/src/custom/use-theme-setting.ts +5 -21
  17. package/src/editor-locale-bridge.tsx +11 -39
  18. package/src/load-placeholder-sections.ts +13 -3
  19. package/src/placeholder-registry.ts +42 -1
  20. package/src/region/__tests__/editor-handshake.test.ts +144 -0
  21. package/src/region/__tests__/region-contract.test.ts +500 -0
  22. package/src/region/__tests__/region-placeholder-readers.test.ts +431 -0
  23. package/src/region/__tests__/region-source.test.ts +621 -0
  24. package/src/region/editor-handshake.ts +60 -0
  25. package/src/region/region-contract.ts +184 -0
  26. package/src/region/region-source.ts +360 -0
  27. package/src/theme-placeholder-wrapper.tsx +0 -3
  28. package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
  29. package/src/blocks/order-line-actions.ts +0 -45
  30. package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
  31. package/src/utils/sanitize-rich-text.ts +0 -215
package/CHANGELOG.md CHANGED
@@ -1,55 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.162-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - 8e040731e: ZERO-5235: let brands hang actions on order detail lines
8
- - c761b9fd: ZERO-5296: fall back to the storefront default language in the text block
9
- - 17a598da: ZERO-5296: report the storefront default language to the editor
10
- - 3b05a407: ZERO-5232: allow richtext settings on custom blocks
11
- - Updated dependencies [86187b1c2]
12
- - Updated dependencies [08390a87]
13
- - Updated dependencies [0cf9ea239]
14
- - Updated dependencies [324f97d55]
15
- - Updated dependencies [4c3465ae]
16
- - Updated dependencies [51ea06888]
17
- - Updated dependencies
18
- - Updated dependencies [549da545]
19
- - Updated dependencies [2fea41430]
20
- - Updated dependencies [b55acb768]
21
- - Updated dependencies [760258c1c]
22
- - Updated dependencies [143be2b9d]
23
- - Updated dependencies [7889b08fe]
24
- - Updated dependencies [9f8cd3bc5]
25
- - Updated dependencies [d635d345]
26
- - Updated dependencies [bfafa3f49]
27
- - Updated dependencies [d860c917]
28
- - Updated dependencies [ac6cada1]
29
- - Updated dependencies [e90c0e02]
30
- - Updated dependencies [dc7f9ca5]
31
- - Updated dependencies [57d7eb305]
32
- - Updated dependencies [0145dc3f]
33
- - Updated dependencies [d99a6a7d5]
34
- - Updated dependencies [92086b29]
35
- - Updated dependencies [227b7408]
36
- - Updated dependencies [9db81a714]
37
- - Updated dependencies [591e345e1]
38
- - Updated dependencies [4de5303c5]
39
- - Updated dependencies [95b139dc1]
40
- - Updated dependencies [1d00f2d06]
41
- - Updated dependencies [4ac7b2a1e]
42
- - Updated dependencies [4998a9631]
43
- - Updated dependencies [804d2bd6c]
44
- - Updated dependencies [3909d3224]
45
- - Updated dependencies [6a3d8a631]
46
- - Updated dependencies [e18836b20]
47
- - Updated dependencies [b1111d7b1]
48
- - Updated dependencies [1595d458e]
49
- - Updated dependencies [dca128112]
50
- - Updated dependencies [9a7b0937]
51
- - @akinon/next@2.0.162-rc.0
52
- - @akinon/theme-contract@2.0.162-rc.0
3
+ ## 2.0.162
4
+
5
+ ### Patch Changes
6
+
7
+ - 8758f3a3: ZERO-5391: Per-country placeholder content. When the request locale names a country (`en-ae` → `ae`) and the `theme-regions` registry lists a placeholder for it, the placeholder renders from that country's copy (`<slug>-region-<cc>`; in draft mode its `-preview-<id>` blob first) instead of the shared default, with the data-source records the copy carries (`theme_region_dataSources`) filling in any id theme-config lacks (theme-config's record wins when it has the id). Unlisted placeholders, language-only locales and a missing or unreadable registry keep today's reads; a server process that has read the registry keeps using it for up to 15 minutes while reads fail. `PLACEHOLDERS_AVAILABLE` now carries `contentCapabilities`, `localeUrlStrategy` and `defaultLocale`, and placeholders always report their canonical slug as `placeholderId`.
8
+ - @akinon/next@2.0.162
9
+ - @akinon/theme-contract@2.0.162
53
10
 
54
11
  ## 2.0.161
55
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.162-rc.0",
4
+ "version": "2.0.162",
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.162-rc.0",
11
+ "@akinon/next": "2.0.162",
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.162-rc.0",
16
+ "@akinon/theme-contract": "2.0.162",
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.162-rc.0",
24
+ "@akinon/next": "2.0.162",
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",
package/readme.md CHANGED
@@ -26,6 +26,28 @@ export default async function Page() {
26
26
 
27
27
  Developer-authored sections and blocks are written against `@akinon/pz-theme/src/custom` — see [`src/custom/README.md`](./src/custom/README.md) and [`src/custom/README-sections.md`](./src/custom/README-sections.md). Reference examples live in `examples/header/` at the root of the pz-next repository; they are not shipped in the npm package.
28
28
 
29
+ ## Per-country content (ZERO-5391)
30
+
31
+ The theme editor can give one country its own copy of a placeholder (a page body, the header or footer, a custom page, a modal) while every other visitor keeps the shared content. pz-theme serves it without any change in the brand's code:
32
+
33
+ - The country ("region") is the last part of the request locale when that part is a two-letter code: `en-ae` and `ar-ae` both read `ae`, `zh-hant-tw` reads `tw`. A language-only locale (`en`, `tr`) has no region, and its requests make no new reads.
34
+ - The editor lists each country's customised placeholders in the `theme-regions` widget. A listed placeholder renders from `<slug>-region-<cc>`, with the data-source records that copy carries (`theme_region_dataSources`) filling in any id theme-config lacks; theme-config's record wins when it has the id. An unlisted placeholder is never looked up under a country slug.
35
+ - Country content that cannot be read is replaced by the shared content, which is what the country's visitors saw before the area was customised. That covers a listed copy that does not answer or has no section order, and a registry that is missing or unreadable (a server process that has read the registry before keeps using that one for a while; see below).
36
+ - Placeholders report their canonical slug (`header`, never `header-region-ae`) as `placeholderId`.
37
+ - Every `PLACEHOLDERS_AVAILABLE` frame carries `contentCapabilities: ['region-content']`, `localeUrlStrategy` and `defaultLocale` (from `settings.localization`). The editor offers countries only to a storefront that announces the capability, does not use `LocaleUrlStrategy.Subdomain` (switching the preview's locale would change its origin) and has at least two audiences: its distinct countries, plus one if any locale is language-only.
38
+
39
+ There is no on-demand revalidation, so changes reach visitors as caches expire:
40
+
41
+ - Customising or reverting an area: up to about two minutes (the registry is cached for 60 s in Redis and kept for 60 s per server process), plus the page's ISR window. An area reverted and customised again inside the widget cache window can briefly show its previous country content.
42
+ - When a registry read fails, is unreadable or finds no registry, a server process that has read one before keeps serving that one for up to 15 minutes after its last successful read, trying again every 10 seconds, so an outage does not flip countries back to the shared content. A registry that is deleted on purpose stays in effect just as long.
43
+ - Publishing a country's edits: the same caching as any widget (Redis 900 s, then ISR).
44
+
45
+ Requirements and known limits:
46
+
47
+ - Live country content works on every core this package supports.
48
+ - Country previews (the editor's draft, "View on site" and signed draft links in a country locale) need `@akinon/next` 1.126.15 or later and the `[pz]` route segment, as named previews do. Without them a country's draft cannot be previewed on the storefront, and its pages show live content.
49
+ - A signed draft link opened in a country locale shows the shared DRAFT in the areas that country has not customised, while the editor's canvas shows the shared LIVE content there. Customised areas show the country's own draft in both.
50
+
29
51
  ## Using pz-theme on the v1 line
30
52
 
31
53
  `@akinon/pz-theme` and `@akinon/theme-contract` have a single release line. They are developed on `main` and published with the 2.x packages; the `v1` branch has no copy of them and publishes no 1.x version. A brand still on v1 (Next 14.2, React 18, `@akinon/next` 1.x) installs the same releases as a v2 brand.
@@ -460,4 +460,114 @@ describe('PLACEHOLDERS_AVAILABLE announce', () => {
460
460
  ]);
461
461
  expect(second.placeholders).toEqual(['header', 'footer']);
462
462
  });
463
+
464
+ // ZERO-5391 — per-country content.
465
+
466
+ it('announces per-country content and the locale settings before the editor proves itself', () => {
467
+ const { registry } = load();
468
+
469
+ registry.registerPlaceholder('header');
470
+ jest.advanceTimersByTime(100);
471
+
472
+ // On the gated frame, like the action list: for a build with no custom
473
+ // catalog it is the only frame the editor ever gets, and a frame without
474
+ // the capability keeps the editor's country picker hidden.
475
+ //
476
+ // Literals rather than the constants, for the same reason as the action
477
+ // list: these pin the wire values. The locale values are the ones
478
+ // jest/__mocks__/settings.js carries.
479
+ const [payload] = frames();
480
+ expect(payload.contentCapabilities).toEqual(['region-content']);
481
+ expect(payload.localeUrlStrategy).toBe('hide-default-locale');
482
+ expect(payload.defaultLocale).toBe('en-us');
483
+ });
484
+
485
+ it('keeps the per-country fields on both frames of the designer re-announce', () => {
486
+ const { registry, register } = load();
487
+
488
+ register.registerCustomBlocks('acme', { 'promo-strip': Promo });
489
+ registry.registerPlaceholder('header');
490
+ jest.advanceTimersByTime(100);
491
+
492
+ (window as { __themeEditorDesigner?: boolean }).__themeEditorDesigner =
493
+ true;
494
+ window.dispatchEvent(
495
+ new MessageEvent('message', {
496
+ origin: EDITOR_ORIGIN,
497
+ source: window.parent as Window,
498
+ data: { type: 'SET_THEME_EDITOR_COOKIE', data: true }
499
+ })
500
+ );
501
+ jest.advanceTimersByTime(100);
502
+
503
+ // Every frame, not just the first. The editor replaces what it holds with
504
+ // each announce, so a later frame without the capability would take the
505
+ // country picker away halfway through a session.
506
+ expect(postMessage).toHaveBeenCalledTimes(2);
507
+ expect(
508
+ frames().map(
509
+ ({ contentCapabilities, localeUrlStrategy, defaultLocale }) => ({
510
+ contentCapabilities,
511
+ localeUrlStrategy,
512
+ defaultLocale
513
+ })
514
+ )
515
+ ).toEqual([
516
+ {
517
+ contentCapabilities: ['region-content'],
518
+ localeUrlStrategy: 'hide-default-locale',
519
+ defaultLocale: 'en-us'
520
+ },
521
+ {
522
+ contentCapabilities: ['region-content'],
523
+ localeUrlStrategy: 'hide-default-locale',
524
+ defaultLocale: 'en-us'
525
+ }
526
+ ]);
527
+ });
528
+
529
+ it('announces the locale settings the build actually routes by', () => {
530
+ // The stub's strategy is also the fallback, so only settings of another
531
+ // shape prove the frame reads them rather than a constant.
532
+ jest.doMock('settings', () => ({
533
+ localization: { localeUrlStrategy: 'subdomain', defaultLocaleValue: 'tr' }
534
+ }));
535
+ try {
536
+ const { registry } = load();
537
+
538
+ registry.registerPlaceholder('header');
539
+ jest.advanceTimersByTime(100);
540
+
541
+ const [payload] = frames();
542
+ expect(payload.localeUrlStrategy).toBe('subdomain');
543
+ expect(payload.defaultLocale).toBe('tr');
544
+ expect(payload.contentCapabilities).toEqual(['region-content']);
545
+ } finally {
546
+ jest.dontMock('settings');
547
+ }
548
+ });
549
+
550
+ it('still announces when the settings cannot be read', () => {
551
+ jest.doMock('settings', () => ({
552
+ get localization() {
553
+ throw new Error('settings are not readable');
554
+ }
555
+ }));
556
+ try {
557
+ const { registry } = load();
558
+
559
+ registry.registerPlaceholder('header');
560
+ jest.advanceTimersByTime(100);
561
+
562
+ // The frame is what every editor load waits for. Losing it to a settings
563
+ // read would cost the whole canvas, not just the country picker.
564
+ expect(postMessage).toHaveBeenCalledTimes(1);
565
+ const [payload] = frames();
566
+ expect(payload.placeholders).toEqual(['header']);
567
+ expect(payload.localeUrlStrategy).toBe('hide-default-locale');
568
+ expect(payload.defaultLocale).toBe('');
569
+ } finally {
570
+ jest.dontMock('settings');
571
+ }
572
+ });
463
573
  });
@@ -28,7 +28,6 @@ const SETTINGS_LOCALES = [
28
28
  const mockLocalization = {
29
29
  locale: 'tr',
30
30
  locales: SETTINGS_LOCALES as Array<Record<string, unknown>>,
31
- defaultLocaleValue: 'tr',
32
31
  setLocale: jest.fn()
33
32
  };
34
33
 
@@ -107,7 +106,6 @@ beforeEach(() => {
107
106
  postMessage.mockClear();
108
107
  mockLocalization.locale = 'tr';
109
108
  mockLocalization.locales = SETTINGS_LOCALES;
110
- mockLocalization.defaultLocaleValue = 'tr';
111
109
  mockLocalization.setLocale = jest.fn();
112
110
  delete (window as { __iframeReadySent?: boolean }).__iframeReadySent;
113
111
  });
@@ -146,8 +144,7 @@ describe('inside the editor', () => {
146
144
  { value: 'en', label: 'English' },
147
145
  { value: 'tr', label: 'Türkçe' },
148
146
  { value: 'de', label: 'de' }
149
- ],
150
- defaultLocale: 'tr'
147
+ ]
151
148
  }
152
149
  })
153
150
  ]);
@@ -157,34 +154,6 @@ describe('inside the editor', () => {
157
154
  expect(targetOrigin).toBe(EDITOR_ORIGIN);
158
155
  });
159
156
 
160
- it('posts the languages before the document reports ready', () => {
161
- // The editor holds every per-language write until IFRAME_READY and takes
162
- // "ready without languages" to mean they are never coming, so the order
163
- // of the two frames is part of the contract.
164
- render(placeholder('header'));
165
-
166
- const types = postMessage.mock.calls.map(([message]) => message?.type);
167
- expect(types).toContain('LOCALE_UPDATE');
168
- expect(types).toContain('IFRAME_READY');
169
- expect(types.indexOf('LOCALE_UPDATE')).toBeLessThan(
170
- types.indexOf('IFRAME_READY')
171
- );
172
- });
173
-
174
- it("posts the brand's default language, whichever it is", () => {
175
- // The editor's per-language text controls take the plain-string form to
176
- // mean this language; left to guess, the editor assumed English.
177
- mockLocalization.defaultLocaleValue = 'de';
178
-
179
- render(<EditorLocaleBridge />);
180
-
181
- expect(localeFrames()).toEqual([
182
- expect.objectContaining({
183
- data: expect.objectContaining({ locale: 'tr', defaultLocale: 'de' })
184
- })
185
- ]);
186
- });
187
-
188
157
  it("follows the editor's switch exactly once, with several placeholders mounted", () => {
189
158
  render(
190
159
  <>
@@ -2,31 +2,9 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import TextBlock from '../text-block';
4
4
 
5
- // The page's language and the storefront's default, as the localization
6
- // provider reports them. Most cases run on an English-default store showing
7
- // English; the fallback case moves both.
8
- const mockLocalization = { locale: 'en-us', defaultLocaleValue: 'en-us' };
9
-
10
- jest.mock('@akinon/next/hooks', () => ({
11
- __esModule: true,
12
- useLocalization: () => ({
13
- t: (path: string) => path,
14
- setLocale: () => {},
15
- setCurrency: () => {},
16
- locale: mockLocalization.locale,
17
- currency: 'USD',
18
- locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
19
- currencies: ['USD'],
20
- defaultLocaleValue: mockLocalization.defaultLocaleValue,
21
- defaultCurrencyCode: 'USD',
22
- localeUrlStrategy: 'hide-default-locale'
23
- })
24
- }));
25
-
26
- beforeEach(() => {
27
- mockLocalization.locale = 'en-us';
28
- mockLocalization.defaultLocaleValue = 'en-us';
29
- });
5
+ // The mocked useLocalization (jest/__mocks__/akinon-next-hooks.js) returns
6
+ // locale: 'en-us'. process.env.NEXT_PUBLIC_DEFAULT_LOCALE is unset in the
7
+ // jest env, so the component's defaultLocale falls back to 'en'.
30
8
 
31
9
  const renderBlock = (value: unknown, styles?: Record<string, unknown>) => {
32
10
  const block: any = { id: 'b1', type: 'text', value, styles };
@@ -56,16 +34,10 @@ describe('TextBlock value-shape resolution', () => {
56
34
  );
57
35
  });
58
36
 
59
- it("falls back to the storefront's default language when the active one is absent", () => {
60
- // A Turkish-default store showing German. The entry under `en` is what
61
- // the block used to show: its default was read from an env variable
62
- // that falls back to English, not from the storefront (ZERO-5296).
63
- mockLocalization.locale = 'de-de';
64
- mockLocalization.defaultLocaleValue = 'tr-tr';
65
-
66
- const { container } = renderBlock({ en: 'EN', 'tr-tr': 'TR' });
37
+ it('falls back to defaultLocale ("en") when active locale is absent', () => {
38
+ const { container } = renderBlock({ en: 'EN', fr: 'FR' });
67
39
  expect((container.querySelector('div') as HTMLDivElement).innerHTML).toBe(
68
- 'TR'
40
+ 'EN'
69
41
  );
70
42
  });
71
43
 
@@ -9,7 +9,6 @@ import { getResponsiveValue, partAttrs } from '../utils';
9
9
  import { formatDate, formatMoney } from './commerce-format';
10
10
  import { readMoneyStyle } from './money-style';
11
11
  import { hasItemTemplate, ItemTemplateList } from './item-template';
12
- import { renderOrderLineActions } from './order-line-actions';
13
12
  import { BlockRendererProps } from './block-renderer-registry';
14
13
  import {
15
14
  DesignerPlaceholder,
@@ -25,7 +24,7 @@ import {
25
24
  useOrderActionRules
26
25
  } from '../utils/order-action-rules';
27
26
 
28
- type OrderItemLike = {
27
+ interface OrderItemLike {
29
28
  id?: number;
30
29
  price?: string;
31
30
  quantity?: string | number;
@@ -44,7 +43,7 @@ type OrderItemLike = {
44
43
  absolute_url?: string;
45
44
  attributes?: Record<string, unknown>;
46
45
  } | null;
47
- };
46
+ }
48
47
 
49
48
  interface AddressLike {
50
49
  first_name?: string;
@@ -409,7 +408,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
409
408
  * currencyText, currencyPosition.
410
409
  * Parts: detail, header, number, date, header-total, status,
411
410
  * invoice-link, cancel-link, return-link, list, item, image, name, quantity,
412
- * color, size, item-status, item-tracking, item-return-code, item-actions,
411
+ * color, size, item-status, item-tracking, item-return-code,
413
412
  * item-return-method, item-return-carrier, item-pickup-date, price, shipment,
414
413
  * delivery, tracking, addresses, address, address-label, totals, total-row,
415
414
  * total-label, total-value, grand-total, payment-info, payment-info-label.
@@ -956,21 +955,6 @@ const OrderDetailBlock = ({
956
955
  showPickupDate && returnDetails.pickupDate
957
956
  ? formatPickupDate(returnDetails.pickupDate, locale)
958
957
  : '';
959
- const lineActions = renderOrderLineActions({
960
- order,
961
- line,
962
- isDesigner
963
- });
964
- const showLineMeta =
965
- showShipmentStatus &&
966
- Boolean(
967
- item.status?.label ||
968
- trackingValue ||
969
- returnCodeValue ||
970
- returnMethodValue ||
971
- returnCarrierValue ||
972
- pickupDateValue
973
- );
974
958
 
975
959
  return (
976
960
  <li
@@ -1086,100 +1070,98 @@ const OrderDetailBlock = ({
1086
1070
  </div>
1087
1071
  </div>
1088
1072
 
1089
- {(showLineMeta || lineActions) && (
1090
- <div
1091
- style={{
1092
- display: 'flex',
1093
- flexDirection: 'column',
1094
- alignItems: isMobile ? 'flex-start' : 'flex-end',
1095
- textAlign: isMobile ? 'left' : 'right',
1096
- gap: '4px',
1097
- flex: isMobile ? '1 1 100%' : '0 0 auto'
1098
- }}
1099
- >
1100
- {lineActions && (
1101
- <div
1102
- data-order-detail-item-actions=""
1103
- {...partAttrs('item-actions')}
1104
- >
1105
- {lineActions}
1106
- </div>
1107
- )}
1108
- {showLineMeta && item.status?.label && (
1109
- <span
1110
- data-order-detail-item-status=""
1111
- data-order-status-value={item.status.value}
1112
- {...partAttrs('item-status')}
1113
- style={{ display: 'inline-block' }}
1114
- >
1115
- {item.status.label}
1116
- </span>
1117
- )}
1118
- {/* Label and value in their own spans, so a
1119
- composition can colour them apart. */}
1120
- {showLineMeta && returnMethodValue && (
1121
- <span
1122
- data-order-detail-item-return-method=""
1123
- {...partAttrs('item-return-method')}
1124
- style={{ opacity: 0.8 }}
1125
- >
1126
- <span data-label="">{returnMethodLabel}</span>{' '}
1127
- <span data-value="">{returnMethodValue}</span>
1128
- </span>
1129
- )}
1130
- {showLineMeta && returnCarrierValue && (
1131
- <span
1132
- data-order-detail-item-return-carrier=""
1133
- {...partAttrs('item-return-carrier')}
1134
- style={{ opacity: 0.8 }}
1135
- >
1136
- <span data-label="">{returnCarrierLabel}</span>{' '}
1137
- <span data-value="">{returnCarrierValue}</span>
1138
- </span>
1139
- )}
1140
- {showLineMeta && pickupDateValue && (
1141
- <span
1142
- data-order-detail-item-pickup-date=""
1143
- {...partAttrs('item-pickup-date')}
1144
- style={{ opacity: 0.8 }}
1145
- >
1146
- <span data-label="">{pickupDateLabel}</span>{' '}
1147
- <span data-value="">{pickupDateValue}</span>
1148
- </span>
1149
- )}
1150
- {showLineMeta && trackingValue && (
1151
- <span
1152
- data-order-detail-item-tracking=""
1153
- {...partAttrs('item-tracking')}
1154
- style={{ opacity: 0.8 }}
1155
- >
1156
- {lineTrackingLabel}{' '}
1157
- {item.tracking_url ? (
1158
- <a
1159
- href={isDesigner ? '#' : item.tracking_url}
1160
- onClick={guard}
1161
- target={isDesigner ? undefined : '_blank'}
1162
- rel="noreferrer"
1163
- style={{ color: 'inherit' }}
1164
- >
1165
- {item.tracking_number || item.tracking_url}
1166
- </a>
1167
- ) : (
1168
- trackingValue
1169
- )}
1170
- </span>
1171
- )}
1172
- {showLineMeta && returnCodeValue && (
1173
- <span
1174
- data-order-detail-item-return-code=""
1175
- {...partAttrs('item-return-code')}
1176
- style={{ opacity: 0.8 }}
1177
- >
1178
- {returnCodeLabel} {returnCodeValue}
1179
- </span>
1180
- )}
1181
- </div>
1182
- )}
1073
+ {showShipmentStatus &&
1074
+ (item.status?.label ||
1075
+ trackingValue ||
1076
+ returnCodeValue ||
1077
+ returnMethodValue ||
1078
+ returnCarrierValue ||
1079
+ pickupDateValue) && (
1080
+ <div
1081
+ style={{
1082
+ display: 'flex',
1083
+ flexDirection: 'column',
1084
+ alignItems: isMobile ? 'flex-start' : 'flex-end',
1085
+ textAlign: isMobile ? 'left' : 'right',
1086
+ gap: '4px',
1087
+ flex: isMobile ? '1 1 100%' : '0 0 auto'
1088
+ }}
1089
+ >
1090
+ {item.status?.label && (
1091
+ <span
1092
+ data-order-detail-item-status=""
1093
+ data-order-status-value={item.status.value}
1094
+ {...partAttrs('item-status')}
1095
+ style={{ display: 'inline-block' }}
1096
+ >
1097
+ {item.status.label}
1098
+ </span>
1099
+ )}
1100
+ {/* Label and value in their own spans, so a
1101
+ composition can colour them apart. */}
1102
+ {returnMethodValue && (
1103
+ <span
1104
+ data-order-detail-item-return-method=""
1105
+ {...partAttrs('item-return-method')}
1106
+ style={{ opacity: 0.8 }}
1107
+ >
1108
+ <span data-label="">{returnMethodLabel}</span>{' '}
1109
+ <span data-value="">{returnMethodValue}</span>
1110
+ </span>
1111
+ )}
1112
+ {returnCarrierValue && (
1113
+ <span
1114
+ data-order-detail-item-return-carrier=""
1115
+ {...partAttrs('item-return-carrier')}
1116
+ style={{ opacity: 0.8 }}
1117
+ >
1118
+ <span data-label="">{returnCarrierLabel}</span>{' '}
1119
+ <span data-value="">{returnCarrierValue}</span>
1120
+ </span>
1121
+ )}
1122
+ {pickupDateValue && (
1123
+ <span
1124
+ data-order-detail-item-pickup-date=""
1125
+ {...partAttrs('item-pickup-date')}
1126
+ style={{ opacity: 0.8 }}
1127
+ >
1128
+ <span data-label="">{pickupDateLabel}</span>{' '}
1129
+ <span data-value="">{pickupDateValue}</span>
1130
+ </span>
1131
+ )}
1132
+ {trackingValue && (
1133
+ <span
1134
+ data-order-detail-item-tracking=""
1135
+ {...partAttrs('item-tracking')}
1136
+ style={{ opacity: 0.8 }}
1137
+ >
1138
+ {lineTrackingLabel}{' '}
1139
+ {item.tracking_url ? (
1140
+ <a
1141
+ href={isDesigner ? '#' : item.tracking_url}
1142
+ onClick={guard}
1143
+ target={isDesigner ? undefined : '_blank'}
1144
+ rel="noreferrer"
1145
+ style={{ color: 'inherit' }}
1146
+ >
1147
+ {item.tracking_number || item.tracking_url}
1148
+ </a>
1149
+ ) : (
1150
+ trackingValue
1151
+ )}
1152
+ </span>
1153
+ )}
1154
+ {returnCodeValue && (
1155
+ <span
1156
+ data-order-detail-item-return-code=""
1157
+ {...partAttrs('item-return-code')}
1158
+ style={{ opacity: 0.8 }}
1159
+ >
1160
+ {returnCodeLabel} {returnCodeValue}
1161
+ </span>
1162
+ )}
1163
+ </div>
1164
+ )}
1183
1165
  </div>
1184
1166
  </li>
1185
1167
  );
@@ -7,7 +7,6 @@ import { getInnerRootInlineStyles, partAttrs } from '../utils';
7
7
  import { useThemeSettingsContext } from '../theme-settings-context';
8
8
  import { useLocalizeHref } from '../hooks/use-localize-href';
9
9
  import { localizeHtmlLinks } from '../utils/localize-href';
10
- import { useValueLocale } from '../utils/localized-value';
11
10
 
12
11
  const TextBlock = ({
13
12
  block,
@@ -15,7 +14,7 @@ const TextBlock = ({
15
14
  }: BlockRendererProps) => {
16
15
  const { locale } = useLocalization();
17
16
  const localize = useLocalizeHref();
18
- const { defaultLocale } = useValueLocale();
17
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
19
18
  const themeSettings = useThemeSettingsContext();
20
19
 
21
20
  const getLocalizedContent = (): string => {
@@ -31,10 +30,7 @@ const TextBlock = ({
31
30
 
32
31
  if (typeof value === 'object' && value !== null) {
33
32
  return (
34
- value[locale] ||
35
- (defaultLocale && value[defaultLocale]) ||
36
- Object.values(value)[0] ||
37
- ''
33
+ value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
38
34
  );
39
35
  }
40
36