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

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 (46) hide show
  1. package/CHANGELOG.md +8 -50
  2. package/package.json +4 -4
  3. package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
  4. package/src/__tests__/theme-settings-defaults.test.tsx +395 -0
  5. package/src/__tests__/theme-text-defaults-code-owned.test.tsx +340 -0
  6. package/src/blocks/__tests__/breadcrumb-theme-separator.test.tsx +148 -0
  7. package/src/blocks/__tests__/social-icons-theme-fallback.test.tsx +268 -0
  8. package/src/blocks/__tests__/text-block.test.tsx +6 -34
  9. package/src/blocks/accordion-block.tsx +10 -0
  10. package/src/blocks/hotspot-block.tsx +6 -1
  11. package/src/blocks/listing-breadcrumb-block.tsx +10 -1
  12. package/src/blocks/order-detail-block.tsx +95 -113
  13. package/src/blocks/product-breadcrumb-block.tsx +9 -1
  14. package/src/blocks/social-glyphs.tsx +31 -0
  15. package/src/blocks/social-icons-list-block.tsx +72 -8
  16. package/src/blocks/text-block.tsx +15 -6
  17. package/src/chrome/__tests__/theme-page-up.test.tsx +522 -0
  18. package/src/chrome/theme-page-up.tsx +303 -0
  19. package/src/chrome/use-editor-theme-settings.ts +104 -0
  20. package/src/components/with-designer-features.tsx +27 -0
  21. package/src/custom/README.md +219 -100
  22. package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
  23. package/src/custom/collect.ts +9 -4
  24. package/src/custom/property-types.ts +15 -15
  25. package/src/custom/use-theme-setting.ts +5 -21
  26. package/src/editor-locale-bridge.tsx +11 -39
  27. package/src/index.ts +15 -0
  28. package/src/overlays/theme-modals.tsx +10 -0
  29. package/src/sections/__tests__/section-wrapper.test.tsx +76 -0
  30. package/src/sections/coupon-banner-section.tsx +15 -1
  31. package/src/sections/section-wrapper.tsx +7 -0
  32. package/src/sections/tabs-section.tsx +13 -0
  33. package/src/theme-placeholder-wrapper.tsx +50 -3
  34. package/src/theme-section.tsx +15 -0
  35. package/src/utils/__tests__/svg-image-source.test.ts +123 -0
  36. package/src/utils/__tests__/theme-settings-css.test.ts +801 -0
  37. package/src/utils/__tests__/theme-settings-readers.test.ts +213 -0
  38. package/src/utils/__tests__/theme-settings-resolver.test.ts +232 -0
  39. package/src/utils/index.ts +36 -1
  40. package/src/utils/svg-image-source.ts +84 -0
  41. package/src/utils/theme-settings-css.ts +717 -0
  42. package/src/utils/theme-settings-readers.ts +205 -0
  43. package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
  44. package/src/blocks/order-line-actions.ts +0 -45
  45. package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
  46. package/src/utils/sanitize-rich-text.ts +0 -215
@@ -0,0 +1,268 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ import type { BlockRendererProps } from '../block-renderer-registry';
5
+ import SocialIconsListBlock from '../social-icons-list-block';
6
+ import { ThemeSettingsProvider } from '../../theme-settings-context';
7
+ import {
8
+ SOCIAL_GLYPHS,
9
+ platformLabel,
10
+ resolveSocialGlyph
11
+ } from '../social-glyphs';
12
+
13
+ /**
14
+ * The settings page's "Sosyal Ağlar" (`socialNetworks`) can feed a
15
+ * social-icons-list block — but only one that opts in with
16
+ * `useThemeSocialNetworks` and has no entries of its own. The block saves
17
+ * `icons: []` by default and the old settings page already stored networks,
18
+ * so an automatic fallback would make blocks that render nothing today start
19
+ * drawing icons on live sites.
20
+ */
21
+
22
+ const props = (
23
+ properties: Record<string, unknown>,
24
+ extra: Partial<BlockRendererProps> = {}
25
+ ): BlockRendererProps =>
26
+ ({
27
+ block: {
28
+ id: 'b1',
29
+ type: 'social-icons-list',
30
+ label: 'Social Icons',
31
+ value: '',
32
+ properties,
33
+ styles: {},
34
+ order: 0,
35
+ hidden: false
36
+ },
37
+ placeholderId: 'footer',
38
+ sectionId: 's1',
39
+ isDesigner: false,
40
+ currentBreakpoint: 'desktop',
41
+ ...extra
42
+ } as unknown as BlockRendererProps);
43
+
44
+ const NETWORKS = {
45
+ instagram: { url: 'https://instagram.com/lulu', icon: '' },
46
+ x: { url: 'https://x.com/lulu', icon: '' },
47
+ facebook: { url: 'https://facebook.com/lulu', icon: '' }
48
+ };
49
+
50
+ const renderWith = (
51
+ properties: Record<string, unknown>,
52
+ settings: Record<string, unknown> | null,
53
+ extra: Partial<BlockRendererProps> = {}
54
+ ) =>
55
+ render(
56
+ <ThemeSettingsProvider value={settings}>
57
+ <SocialIconsListBlock {...props(properties, extra)} />
58
+ </ThemeSettingsProvider>
59
+ );
60
+
61
+ const platforms = (container: HTMLElement) =>
62
+ Array.from(container.querySelectorAll('[data-social-icon]')).map((node) =>
63
+ node.getAttribute('data-social-icon')
64
+ );
65
+
66
+ describe('social-icons-list — theme social networks fallback', () => {
67
+ it('ignores the theme networks without the toggle', () => {
68
+ const { container } = renderWith(
69
+ { icons: [] },
70
+ { socialNetworks: NETWORKS }
71
+ );
72
+
73
+ expect(container.querySelector('[data-social-icon]')).toBeNull();
74
+ });
75
+
76
+ it('draws the theme networks, in stored order, when the toggle is on', () => {
77
+ const { container } = renderWith(
78
+ { icons: [], useThemeSocialNetworks: true },
79
+ { socialNetworks: NETWORKS }
80
+ );
81
+
82
+ expect(platforms(container)).toEqual(['instagram', 'x', 'facebook']);
83
+
84
+ const links = container.querySelectorAll('[data-social-icon]');
85
+ expect(links[0].getAttribute('href')).toBe('https://instagram.com/lulu');
86
+ expect(links[0].getAttribute('aria-label')).toBe('Instagram');
87
+ expect(links[0].getAttribute('target')).toBe('_blank');
88
+ });
89
+
90
+ it('reads the toggle per breakpoint and as a stored string', () => {
91
+ const settings = { socialNetworks: NETWORKS };
92
+
93
+ const mobileOff = renderWith(
94
+ { useThemeSocialNetworks: { desktop: true, mobile: false } },
95
+ settings,
96
+ { currentBreakpoint: 'mobile' }
97
+ );
98
+ expect(mobileOff.container.querySelector('[data-social-icon]')).toBeNull();
99
+
100
+ const stringOn = renderWith({ useThemeSocialNetworks: 'true' }, settings);
101
+ expect(platforms(stringOn.container)).toHaveLength(3);
102
+ });
103
+
104
+ it("keeps the block's own entries when it has any", () => {
105
+ const { container } = renderWith(
106
+ {
107
+ icons: [{ icon: 'youtube', link: 'https://youtube.com/@lulu' }],
108
+ useThemeSocialNetworks: true
109
+ },
110
+ { socialNetworks: NETWORKS }
111
+ );
112
+
113
+ expect(platforms(container)).toEqual(['youtube']);
114
+ });
115
+
116
+ it('draws the X mark for the settings key `x`', () => {
117
+ const { container } = renderWith(
118
+ { useThemeSocialNetworks: true },
119
+ { socialNetworks: { x: { url: 'https://x.com/lulu' } } }
120
+ );
121
+
122
+ const link = container.querySelector('[data-social-icon="x"]')!;
123
+ expect(link.getAttribute('aria-label')).toBe('X');
124
+ // The twitter mark is one path; the globe it used to fall back to is
125
+ // three shapes.
126
+ const glyph = link.querySelector('svg')!;
127
+ expect(glyph.children.length).toBe(1);
128
+ expect(glyph.querySelector('path')!.getAttribute('d')).toBe(
129
+ 'M4 4l16 16M20 4L4 20'
130
+ );
131
+ });
132
+
133
+ it('treats legacy `twitter` and `x` as one network', () => {
134
+ const { container } = renderWith(
135
+ { useThemeSocialNetworks: true },
136
+ {
137
+ socialNetworks: {
138
+ twitter: { url: 'https://twitter.com/lulu' },
139
+ x: { url: 'https://x.com/lulu' }
140
+ }
141
+ }
142
+ );
143
+
144
+ expect(platforms(container)).toEqual(['twitter']);
145
+ });
146
+
147
+ it('drops a network whose link is not a safe href', () => {
148
+ const { container } = renderWith(
149
+ { useThemeSocialNetworks: true },
150
+ {
151
+ socialNetworks: {
152
+ facebook: { url: 'javascript:alert(1)' },
153
+ instagram: { url: '//evil.example/lulu' },
154
+ youtube: { url: '' },
155
+ tiktok: { url: '/\t/evil.example' },
156
+ linkedin: { url: 'https://linkedin.com/company/lulu' }
157
+ }
158
+ }
159
+ );
160
+
161
+ expect(platforms(container)).toEqual(['linkedin']);
162
+ expect(container.innerHTML).not.toContain('javascript:');
163
+ expect(container.innerHTML).not.toContain('evil.example');
164
+ });
165
+
166
+ it('gives a bare domain https and keeps mailto/tel links', () => {
167
+ const { container } = renderWith(
168
+ { useThemeSocialNetworks: true },
169
+ {
170
+ socialNetworks: {
171
+ instagram: { url: 'www.instagram.com/lulu' },
172
+ email: { url: 'mailto:hello@lulu.example' },
173
+ phone: { url: 'tel:+905550000000' }
174
+ }
175
+ }
176
+ );
177
+
178
+ const hrefs = Array.from(
179
+ container.querySelectorAll('[data-social-icon]')
180
+ ).map((node) => node.getAttribute('href'));
181
+ expect(hrefs).toEqual([
182
+ 'https://www.instagram.com/lulu',
183
+ 'mailto:hello@lulu.example',
184
+ 'tel:+905550000000'
185
+ ]);
186
+ });
187
+
188
+ it('renders an uploaded SVG only as an image, never as markup', () => {
189
+ const markup =
190
+ '<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><script>alert(2)</script><path d="M0 0h24v24H0z"/></svg>';
191
+ const { container } = renderWith(
192
+ { useThemeSocialNetworks: true },
193
+ {
194
+ socialNetworks: {
195
+ facebook: { url: 'https://facebook.com/lulu', icon: markup }
196
+ }
197
+ }
198
+ );
199
+
200
+ const link = container.querySelector('[data-social-icon="facebook"]')!;
201
+ const image = link.querySelector('img')!;
202
+
203
+ expect(image.getAttribute('src')).toMatch(
204
+ /^data:image\/svg\+xml;charset=utf-8,/
205
+ );
206
+ expect(decodeURIComponent(image.getAttribute('src')!.split(',')[1])).toBe(
207
+ markup
208
+ );
209
+ expect(image.getAttribute('alt')).toBe('');
210
+ expect(image.getAttribute('aria-hidden')).toBe('true');
211
+ expect(image.getAttribute('data-part')).toBe('icon');
212
+ expect(link.querySelector('svg')).toBeNull();
213
+ expect(container.querySelector('script')).toBeNull();
214
+ });
215
+
216
+ it('falls back to the platform glyph when the uploaded icon is unusable', () => {
217
+ const { container } = renderWith(
218
+ { useThemeSocialNetworks: true },
219
+ {
220
+ socialNetworks: {
221
+ facebook: {
222
+ url: 'https://facebook.com/lulu',
223
+ icon: 'javascript:alert(1)'
224
+ }
225
+ }
226
+ }
227
+ );
228
+
229
+ const link = container.querySelector('[data-social-icon="facebook"]')!;
230
+ expect(link.querySelector('img')).toBeNull();
231
+ expect(link.querySelector('svg[data-part="icon"]')).not.toBeNull();
232
+ });
233
+
234
+ it('says why it is empty in the designer', () => {
235
+ const { container } = renderWith(
236
+ { useThemeSocialNetworks: true },
237
+ { socialNetworks: {} },
238
+ { isDesigner: true }
239
+ );
240
+
241
+ expect(container.textContent).toContain(
242
+ 'Social icons (no theme social networks)'
243
+ );
244
+ });
245
+
246
+ it('renders nothing on the storefront when the theme has no usable network', () => {
247
+ const { container } = renderWith({ useThemeSocialNetworks: true }, null);
248
+
249
+ expect(container.innerHTML).toBe('');
250
+ });
251
+ });
252
+
253
+ describe('social glyph aliases', () => {
254
+ it('resolves `x` to the twitter mark and keeps the table itself unchanged', () => {
255
+ expect(resolveSocialGlyph('x')).toBe(SOCIAL_GLYPHS.twitter);
256
+ expect(resolveSocialGlyph('facebook')).toBe(SOCIAL_GLYPHS.facebook);
257
+ expect(Object.prototype.hasOwnProperty.call(SOCIAL_GLYPHS, 'x')).toBe(
258
+ false
259
+ );
260
+ expect(platformLabel('x')).toBe('X');
261
+ });
262
+
263
+ it('never resolves an inherited property as a glyph', () => {
264
+ expect(resolveSocialGlyph('constructor')).toBeUndefined();
265
+ expect(resolveSocialGlyph('toString')).toBeUndefined();
266
+ expect(resolveSocialGlyph('myspace')).toBeUndefined();
267
+ });
268
+ });
@@ -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
 
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
2
2
  import { BlockRendererProps } from './block-renderer-registry';
3
3
  import ThemeBlock from '../theme-block';
4
4
  import { partAttrs } from '../utils';
5
+ import { useThemeSettingsContext } from '../theme-settings-context';
6
+ import { themeOwnPartStyleAttributes } from '../utils/theme-settings-css';
5
7
 
6
8
  /**
7
9
  * Accordion: a heading row that toggles a content panel.
@@ -30,6 +32,11 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
30
32
  currentBreakpoint = 'desktop'
31
33
  }) => {
32
34
  const childBlocks = block.blocks || [];
35
+ const themeSettings = useThemeSettingsContext();
36
+ // The header, panel and content parts hold the child blocks, so a colour
37
+ // or type authored on one of them has to keep reaching the text inside.
38
+ const ownPartStyle = (part: string) =>
39
+ themeOwnPartStyleAttributes(block.styles, part, themeSettings);
33
40
 
34
41
  const defaultOpen = block.properties?.defaultOpen === true;
35
42
  const [isOpen, setIsOpen] = useState(defaultOpen);
@@ -140,6 +147,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
140
147
  <>
141
148
  <div
142
149
  {...partAttrs('header', { open: isOpen })}
150
+ {...ownPartStyle('header')}
143
151
  style={{ cursor: 'pointer' }}
144
152
  onClick={toggleAccordion}
145
153
  >
@@ -148,6 +156,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
148
156
 
149
157
  <div
150
158
  {...partAttrs('panel', { open: isOpen })}
159
+ {...ownPartStyle('panel')}
151
160
  style={{
152
161
  display: 'grid',
153
162
  gridTemplateRows: isOpen ? '1fr' : '0fr',
@@ -156,6 +165,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
156
165
  >
157
166
  <div
158
167
  {...partAttrs('content', { open: isOpen })}
168
+ {...ownPartStyle('content')}
159
169
  style={{ overflow: 'hidden' }}
160
170
  >
161
171
  {renderContent()}
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
4
4
  import { BlockRendererProps } from './block-renderer-registry';
5
5
  import { getResponsiveValue, getCSSStyles, resolveForceOpen } from '../utils';
6
6
  import { useThemeSettingsContext } from '../theme-settings-context';
7
+ import { themeOwnStyleAttributes } from '../utils/theme-settings-css';
7
8
  import { useLocalization } from '@akinon/next/hooks';
8
9
  import { useLocalizeHref } from '../hooks/use-localize-href';
9
10
  import ThemeBlock from '../theme-block';
@@ -109,7 +110,11 @@ const HotspotBlock = ({
109
110
  </button>
110
111
 
111
112
  {(isOpen || forcedOpen) && (
112
- <div className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 w-64 bg-white p-4 rounded-lg shadow-xl z-20 text-left text-sm pointer-events-auto">
113
+ <div
114
+ className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 w-64 bg-white p-4 rounded-lg shadow-xl z-20 text-left text-sm pointer-events-auto"
115
+ // `text-sm` sets the size of the blocks inside the tooltip.
116
+ {...themeOwnStyleAttributes(null, themeSettings, { type: true })}
117
+ >
113
118
  <div className="flex justify-between items-start mb-2">
114
119
  {title && <h3 className="font-bold text-gray-900">{title}</h3>}
115
120
  <button
@@ -3,6 +3,8 @@
3
3
  import React from 'react';
4
4
  import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { getResponsiveValue, partAttrs } from '../utils';
6
+ import { readThemeBreadcrumbSeparator } from '../utils/theme-settings-readers';
7
+ import { useThemeSettingsContext } from '../theme-settings-context';
6
8
  import { BlockRendererProps } from './block-renderer-registry';
7
9
  import {
8
10
  listingPlaceholderStyle,
@@ -33,9 +35,16 @@ const ListingBreadcrumbBlock = ({
33
35
  isDesigner = false
34
36
  }: BlockRendererProps) => {
35
37
  const listing = useListingContext();
38
+ const themeSettings = useThemeSettingsContext();
36
39
 
40
+ // An unset separator follows the theme's breadcrumb setting; a
41
+ // stored one (including '/') keeps winning, so saved blocks render as before.
37
42
  const separator = String(
38
- getResponsiveValue(block.properties?.separator, currentBreakpoint, '/')
43
+ getResponsiveValue(
44
+ block.properties?.separator,
45
+ currentBreakpoint,
46
+ readThemeBreadcrumbSeparator(themeSettings) ?? '/'
47
+ )
39
48
  );
40
49
  const showHome = readToggle(
41
50
  block.properties?.showHome,
@@ -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
  );