@akinon/pz-theme 2.0.74-rc.1 → 2.0.74
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,44 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.74
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.74
|
|
9
|
-
- @akinon/theme-contract@2.0.74
|
|
10
|
-
|
|
11
|
-
## 2.0.74-rc.0
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- Updated dependencies [0cf9ea239]
|
|
16
|
-
- Updated dependencies [324f97d55]
|
|
17
|
-
- Updated dependencies [51ea06888]
|
|
18
|
-
- Updated dependencies
|
|
19
|
-
- Updated dependencies [2fea4143]
|
|
20
|
-
- Updated dependencies [b55acb768]
|
|
21
|
-
- Updated dependencies [760258c1c]
|
|
22
|
-
- Updated dependencies [143be2b9d]
|
|
23
|
-
- Updated dependencies [7889b08fe]
|
|
24
|
-
- Updated dependencies [9f8cd3bc5]
|
|
25
|
-
- Updated dependencies [bfafa3f49]
|
|
26
|
-
- Updated dependencies [57d7eb305]
|
|
27
|
-
- Updated dependencies [d99a6a7d5]
|
|
28
|
-
- Updated dependencies [9db81a714]
|
|
29
|
-
- Updated dependencies [591e345e1]
|
|
30
|
-
- Updated dependencies [4de5303c5]
|
|
31
|
-
- Updated dependencies [95b139dc1]
|
|
32
|
-
- Updated dependencies [1d00f2d06]
|
|
33
|
-
- Updated dependencies [4ac7b2a1e]
|
|
34
|
-
- Updated dependencies [e9598c71]
|
|
35
|
-
- Updated dependencies [4998a9631]
|
|
36
|
-
- Updated dependencies [804d2bd6]
|
|
37
|
-
- Updated dependencies [3909d3224]
|
|
38
|
-
- Updated dependencies [6a3d8a63]
|
|
39
|
-
- Updated dependencies [e18836b20]
|
|
40
|
-
- @akinon/next@2.0.74-rc.0
|
|
41
|
-
- @akinon/theme-contract@2.0.74-rc.0
|
|
3
|
+
## 2.0.74
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e03dbf7c: Add coupon-wallet block: a two-column (active/used) coupon list for the account coupons page, selectable from the theme editor as an alternative to the default page — same takeover mechanism as the order detail page.
|
|
8
|
+
- @akinon/next@2.0.74
|
|
9
|
+
- @akinon/theme-contract@2.0.74
|
|
42
10
|
|
|
43
11
|
## 2.0.73
|
|
44
12
|
|
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.74
|
|
4
|
+
"version": "2.0.74",
|
|
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.74
|
|
11
|
+
"@akinon/next": "2.0.74",
|
|
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.74
|
|
16
|
+
"@akinon/theme-contract": "2.0.74",
|
|
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.74
|
|
23
|
+
"@akinon/next": "2.0.74",
|
|
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",
|
|
@@ -49,6 +49,7 @@ import ProfileFormBlock from './profile-form-block';
|
|
|
49
49
|
import ChangePasswordFormBlock from './change-password-form-block';
|
|
50
50
|
import OrderDetailBlock from './order-detail-block';
|
|
51
51
|
import CouponListBlock from './coupon-list-block';
|
|
52
|
+
import CouponWalletBlock from './coupon-wallet-block';
|
|
52
53
|
import FavouritesGridBlock from './favourites-grid-block';
|
|
53
54
|
import MegaMenuBlock from './mega-menu-block';
|
|
54
55
|
import CurrencySelectorBlock from './currency-selector-block';
|
|
@@ -164,6 +165,7 @@ blockRendererRegistry.register('profile-form', ProfileFormBlock);
|
|
|
164
165
|
blockRendererRegistry.register('change-password-form', ChangePasswordFormBlock);
|
|
165
166
|
blockRendererRegistry.register('order-detail', OrderDetailBlock);
|
|
166
167
|
blockRendererRegistry.register('coupon-list', CouponListBlock);
|
|
168
|
+
blockRendererRegistry.register('coupon-wallet', CouponWalletBlock);
|
|
167
169
|
blockRendererRegistry.register('favourites-grid', FavouritesGridBlock);
|
|
168
170
|
|
|
169
171
|
// Chrome extras
|
|
@@ -180,13 +182,22 @@ blockRendererRegistry.register('checkout-steps', CheckoutStepsBlock);
|
|
|
180
182
|
blockRendererRegistry.register('order-success', OrderSuccessBlock);
|
|
181
183
|
blockRendererRegistry.register('order-tracking', OrderTrackingBlock);
|
|
182
184
|
blockRendererRegistry.register('checkout-flow-outlet', CheckoutFlowOutletBlock);
|
|
183
|
-
blockRendererRegistry.register(
|
|
184
|
-
|
|
185
|
+
blockRendererRegistry.register(
|
|
186
|
+
'checkout-steps-outlet',
|
|
187
|
+
CheckoutStepsOutletBlock
|
|
188
|
+
);
|
|
189
|
+
blockRendererRegistry.register(
|
|
190
|
+
'checkout-summary-outlet',
|
|
191
|
+
CheckoutSummaryOutletBlock
|
|
192
|
+
);
|
|
185
193
|
|
|
186
194
|
// Catalog extras
|
|
187
195
|
blockRendererRegistry.register('product-breadcrumb', ProductBreadcrumbBlock);
|
|
188
196
|
blockRendererRegistry.register('product-attributes', ProductAttributesBlock);
|
|
189
|
-
blockRendererRegistry.register(
|
|
197
|
+
blockRendererRegistry.register(
|
|
198
|
+
'product-recommendations',
|
|
199
|
+
ProductRecommendationsBlock
|
|
200
|
+
);
|
|
190
201
|
blockRendererRegistry.register('panel', PanelBlock);
|
|
191
202
|
// Legacy alias: published widget JSON still carries `size-guide`, and the AI
|
|
192
203
|
// generator manifest still emits it. Same renderer, same parts, same markup.
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useSession } from 'next-auth/react';
|
|
5
|
+
import {
|
|
6
|
+
useGetBasketOffersQuery,
|
|
7
|
+
useGetDiscountItemsQuery
|
|
8
|
+
} from '@akinon/next/data/client/account';
|
|
9
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
10
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
11
|
+
import { formatDate, formatMoney } from './commerce-format';
|
|
12
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
13
|
+
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
14
|
+
|
|
15
|
+
interface CouponLike {
|
|
16
|
+
pk?: number;
|
|
17
|
+
label?: string;
|
|
18
|
+
voucher_code?: string;
|
|
19
|
+
amount?: string;
|
|
20
|
+
percentage?: string | number;
|
|
21
|
+
// 'fixed' -> amount is a currency value; 'percentage' -> percentage is the
|
|
22
|
+
// discount rate. Missing/unknown falls back to "whichever field is set".
|
|
23
|
+
benefit_type?: string;
|
|
24
|
+
start_datetime?: string;
|
|
25
|
+
end_datetime?: string;
|
|
26
|
+
created_date?: string;
|
|
27
|
+
order_number?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The active-coupon endpoint answers with `{ discounts: [...] }` while some
|
|
32
|
+
* payloads answer with the paginated `{ results: [...] }` shape. Both are
|
|
33
|
+
* probed (plus a bare array) so one reader covers every response — same
|
|
34
|
+
* approach as coupon-list-block.
|
|
35
|
+
*/
|
|
36
|
+
const readCoupons = (payload: unknown): CouponLike[] => {
|
|
37
|
+
if (Array.isArray(payload)) return payload as CouponLike[];
|
|
38
|
+
|
|
39
|
+
const record = (payload ?? {}) as Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(record.discounts)) return record.discounts as CouponLike[];
|
|
42
|
+
if (Array.isArray(record.results)) return record.results as CouponLike[];
|
|
43
|
+
|
|
44
|
+
return [];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Functional account block: the shopper's coupon wallet split into two
|
|
49
|
+
* columns — active and used — on a CSS grid with two equal `1fr` tracks
|
|
50
|
+
* (collapsing to one track when only one side is shown), so the split stays
|
|
51
|
+
* exactly 50/50 regardless of which side has more rows, fewer rows, or none
|
|
52
|
+
* at all — a grid track's size is set by the track definition, not by the
|
|
53
|
+
* content inside it, which is what a flex `flex-basis` split can't guarantee
|
|
54
|
+
* once one side is empty. Stacked full-width on mobile. "Active" reads the
|
|
55
|
+
* basket-offers endpoint; "used" reads the discount-items endpoint (a coupon
|
|
56
|
+
* that was actually redeemed on an order), which is a different fact than
|
|
57
|
+
* "expired" (coupon-list-block's distinction).
|
|
58
|
+
* The column title's HTML tag is fixed (h2) — its visual style is a
|
|
59
|
+
* `column-title` typography preset (font, size, weight in one step), applied
|
|
60
|
+
* from the editor, not a per-block property.
|
|
61
|
+
* Properties: activeTitle, showActive, activeEmptyLabel, usedTitle, showUsed,
|
|
62
|
+
* usedEmptyLabel, limit, showDate, showCode, showAmount, percentagePrefix,
|
|
63
|
+
* termsLinkLabel, termsHref, termsLinkTarget.
|
|
64
|
+
* Parts: wrapper, column (active/used via data-coupon-wallet-column),
|
|
65
|
+
* column-title, list, item (state: disabled, for a used/redeemed row), date,
|
|
66
|
+
* title, code, amount, terms-link, empty.
|
|
67
|
+
*/
|
|
68
|
+
const CouponWalletBlock = ({
|
|
69
|
+
block,
|
|
70
|
+
currentBreakpoint = 'desktop',
|
|
71
|
+
isDesigner = false
|
|
72
|
+
}: BlockRendererProps) => {
|
|
73
|
+
const showActive = readToggle(
|
|
74
|
+
block.properties?.showActive,
|
|
75
|
+
currentBreakpoint,
|
|
76
|
+
true
|
|
77
|
+
);
|
|
78
|
+
const showUsed = readToggle(
|
|
79
|
+
block.properties?.showUsed,
|
|
80
|
+
currentBreakpoint,
|
|
81
|
+
true
|
|
82
|
+
);
|
|
83
|
+
const showCode = readToggle(
|
|
84
|
+
block.properties?.showCode,
|
|
85
|
+
currentBreakpoint,
|
|
86
|
+
true
|
|
87
|
+
);
|
|
88
|
+
const showAmount = readToggle(
|
|
89
|
+
block.properties?.showAmount,
|
|
90
|
+
currentBreakpoint,
|
|
91
|
+
true
|
|
92
|
+
);
|
|
93
|
+
const showDate = readToggle(
|
|
94
|
+
block.properties?.showDate,
|
|
95
|
+
currentBreakpoint,
|
|
96
|
+
true
|
|
97
|
+
);
|
|
98
|
+
const text = (key: string, fallback: string) =>
|
|
99
|
+
String(
|
|
100
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
101
|
+
);
|
|
102
|
+
const activeTitle = text('activeTitle', 'Active Coupons');
|
|
103
|
+
const usedTitle = text('usedTitle', 'Used Coupons');
|
|
104
|
+
const percentagePrefix = text('percentagePrefix', '%');
|
|
105
|
+
const activeEmptyLabel = text('activeEmptyLabel', '');
|
|
106
|
+
const usedEmptyLabel = text('usedEmptyLabel', '');
|
|
107
|
+
const termsLinkLabel = text('termsLinkLabel', '');
|
|
108
|
+
const termsHref = text('termsHref', '');
|
|
109
|
+
const termsLinkTarget = text('termsLinkTarget', '_self');
|
|
110
|
+
const rawLimit = Math.round(
|
|
111
|
+
Number(getResponsiveValue(block.properties?.limit, currentBreakpoint, 0))
|
|
112
|
+
);
|
|
113
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 0;
|
|
114
|
+
|
|
115
|
+
const isMobile = currentBreakpoint === 'mobile';
|
|
116
|
+
const { locale, currency } = useLocalization();
|
|
117
|
+
const { status } = useSession();
|
|
118
|
+
const isAuthenticated = status === 'authenticated';
|
|
119
|
+
|
|
120
|
+
// Guests have no coupon wallet; skipping avoids a guaranteed 403 on every
|
|
121
|
+
// page that hosts this block.
|
|
122
|
+
const { data: activeData, isLoading: activeLoading } =
|
|
123
|
+
useGetBasketOffersQuery(undefined, {
|
|
124
|
+
skip: !isAuthenticated || !showActive
|
|
125
|
+
});
|
|
126
|
+
const { data: usedData, isLoading: usedLoading } = useGetDiscountItemsQuery(
|
|
127
|
+
undefined,
|
|
128
|
+
{ skip: !isAuthenticated || !showUsed }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const activeCoupons = readCoupons(activeData);
|
|
132
|
+
const usedCoupons = readCoupons(usedData);
|
|
133
|
+
const visibleActive =
|
|
134
|
+
limit > 0 ? activeCoupons.slice(0, limit) : activeCoupons;
|
|
135
|
+
const visibleUsed = limit > 0 ? usedCoupons.slice(0, limit) : usedCoupons;
|
|
136
|
+
|
|
137
|
+
/** The one value a row actually prices at, driven by `benefit_type`. */
|
|
138
|
+
const describeValue = (coupon: CouponLike): string => {
|
|
139
|
+
const amount = formatMoney(coupon.amount, { locale, currency });
|
|
140
|
+
const percentage =
|
|
141
|
+
coupon.percentage !== undefined &&
|
|
142
|
+
coupon.percentage !== null &&
|
|
143
|
+
String(coupon.percentage).trim()
|
|
144
|
+
? `${percentagePrefix}${coupon.percentage}`
|
|
145
|
+
: '';
|
|
146
|
+
const benefitType = String(coupon.benefit_type || '').toLowerCase();
|
|
147
|
+
|
|
148
|
+
if (benefitType === 'fixed') return amount;
|
|
149
|
+
if (benefitType === 'percentage') return percentage;
|
|
150
|
+
|
|
151
|
+
// Payload omitted benefit_type: fall back to whichever field is set.
|
|
152
|
+
return percentage || amount;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const guard = (event: React.MouseEvent) => {
|
|
156
|
+
if (isDesigner) event.preventDefault();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const renderTermsLink = () =>
|
|
160
|
+
termsLinkLabel ? (
|
|
161
|
+
<a
|
|
162
|
+
href={isDesigner ? '#' : termsHref || '#'}
|
|
163
|
+
target={isDesigner ? undefined : termsLinkTarget}
|
|
164
|
+
rel={termsLinkTarget === '_blank' ? 'noreferrer' : undefined}
|
|
165
|
+
onClick={guard}
|
|
166
|
+
data-coupon-wallet-terms-link=""
|
|
167
|
+
{...partAttrs('terms-link')}
|
|
168
|
+
style={{
|
|
169
|
+
display: 'inline-block',
|
|
170
|
+
marginTop: '8px',
|
|
171
|
+
color: 'inherit',
|
|
172
|
+
textDecoration: 'none'
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
→ {termsLinkLabel}
|
|
176
|
+
</a>
|
|
177
|
+
) : null;
|
|
178
|
+
|
|
179
|
+
const renderColumn = (
|
|
180
|
+
title: string,
|
|
181
|
+
coupons: CouponLike[],
|
|
182
|
+
isUsed: boolean,
|
|
183
|
+
emptyLabel: string,
|
|
184
|
+
loading: boolean
|
|
185
|
+
) => (
|
|
186
|
+
// A column with nothing to show still renders its shell (title + empty
|
|
187
|
+
// state) so the grid keeps both tracks — see the block doc comment above
|
|
188
|
+
// for why the split is a grid track, not a flex-basis split.
|
|
189
|
+
<div
|
|
190
|
+
data-coupon-wallet-column={isUsed ? 'used' : 'active'}
|
|
191
|
+
{...partAttrs('column')}
|
|
192
|
+
style={{ minWidth: 0 }}
|
|
193
|
+
>
|
|
194
|
+
<h2
|
|
195
|
+
data-coupon-wallet-column-title=""
|
|
196
|
+
{...partAttrs('column-title')}
|
|
197
|
+
style={{ marginBottom: '24px' }}
|
|
198
|
+
>
|
|
199
|
+
{title}
|
|
200
|
+
</h2>
|
|
201
|
+
|
|
202
|
+
{coupons.length > 0 ? (
|
|
203
|
+
<ul
|
|
204
|
+
data-coupon-wallet-list=""
|
|
205
|
+
{...partAttrs('list')}
|
|
206
|
+
style={{
|
|
207
|
+
display: 'flex',
|
|
208
|
+
flexDirection: 'column',
|
|
209
|
+
gap: 'var(--coupon-wallet-item-gap, 24px)',
|
|
210
|
+
listStyle: 'none',
|
|
211
|
+
margin: 0,
|
|
212
|
+
padding: 0
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{coupons.map((coupon, index) => {
|
|
216
|
+
const value = describeValue(coupon);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<li
|
|
220
|
+
key={coupon.pk ?? `${coupon.voucher_code}-${index}`}
|
|
221
|
+
data-coupon-wallet-item=""
|
|
222
|
+
data-coupon-wallet-item-used={isUsed ? 'true' : 'false'}
|
|
223
|
+
{...partAttrs('item', { disabled: isUsed })}
|
|
224
|
+
style={{ opacity: isUsed ? 0.5 : 1 }}
|
|
225
|
+
>
|
|
226
|
+
{showDate && (
|
|
227
|
+
<span
|
|
228
|
+
data-coupon-wallet-date=""
|
|
229
|
+
{...partAttrs('date')}
|
|
230
|
+
style={{
|
|
231
|
+
display: 'inline-block',
|
|
232
|
+
padding: '4px 10px',
|
|
233
|
+
marginBottom: '12px',
|
|
234
|
+
background: 'var(--coupon-wallet-date-bg, #efece3)',
|
|
235
|
+
fontSize: '12px'
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{formatDate(
|
|
239
|
+
isUsed ? coupon.created_date : coupon.end_datetime,
|
|
240
|
+
locale
|
|
241
|
+
)}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
<div
|
|
245
|
+
style={{
|
|
246
|
+
display: 'flex',
|
|
247
|
+
alignItems: 'baseline',
|
|
248
|
+
flexWrap: 'wrap',
|
|
249
|
+
gap: '8px'
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<span
|
|
253
|
+
data-coupon-wallet-title=""
|
|
254
|
+
{...partAttrs('title')}
|
|
255
|
+
style={{ fontWeight: 600, textTransform: 'uppercase' }}
|
|
256
|
+
>
|
|
257
|
+
{coupon.label}
|
|
258
|
+
</span>
|
|
259
|
+
{showCode && coupon.voucher_code && (
|
|
260
|
+
<span
|
|
261
|
+
data-coupon-wallet-code=""
|
|
262
|
+
{...partAttrs('code')}
|
|
263
|
+
style={{ letterSpacing: '0.05em' }}
|
|
264
|
+
>
|
|
265
|
+
{coupon.voucher_code}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
{showAmount && value && (
|
|
269
|
+
<span
|
|
270
|
+
data-coupon-wallet-amount=""
|
|
271
|
+
{...partAttrs('amount')}
|
|
272
|
+
style={{ opacity: 0.7 }}
|
|
273
|
+
>
|
|
274
|
+
{value}
|
|
275
|
+
</span>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
{renderTermsLink()}
|
|
279
|
+
</li>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</ul>
|
|
283
|
+
) : isDesigner ? (
|
|
284
|
+
<DesignerPlaceholder
|
|
285
|
+
label={loading ? 'Coupons (loading)' : 'Coupons (none)'}
|
|
286
|
+
attribute="data-coupon-wallet-list"
|
|
287
|
+
/>
|
|
288
|
+
) : (
|
|
289
|
+
!loading &&
|
|
290
|
+
emptyLabel && (
|
|
291
|
+
<div
|
|
292
|
+
data-coupon-wallet-empty=""
|
|
293
|
+
{...partAttrs('empty')}
|
|
294
|
+
style={{ opacity: 0.6 }}
|
|
295
|
+
>
|
|
296
|
+
{emptyLabel}
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (!showActive && !showUsed) return null;
|
|
304
|
+
|
|
305
|
+
const bothShown = showActive && showUsed;
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
data-coupon-wallet=""
|
|
310
|
+
{...partAttrs('wrapper')}
|
|
311
|
+
style={
|
|
312
|
+
isMobile
|
|
313
|
+
? {
|
|
314
|
+
display: 'flex',
|
|
315
|
+
flexDirection: 'column',
|
|
316
|
+
gap: '48px',
|
|
317
|
+
width: '100%'
|
|
318
|
+
}
|
|
319
|
+
: {
|
|
320
|
+
display: 'grid',
|
|
321
|
+
// Two equal tracks only when both columns render — a single
|
|
322
|
+
// shown column gets the one track it needs, not half the row.
|
|
323
|
+
gridTemplateColumns: bothShown ? '1fr 1fr' : '1fr',
|
|
324
|
+
gap: '64px',
|
|
325
|
+
// Without an explicit width, a grid box's auto-width can shrink
|
|
326
|
+
// to its content's min-content instead of filling the row (the
|
|
327
|
+
// theme editor's own root "width" style targets the block's
|
|
328
|
+
// outer wrapper, not this element), so the 1fr/1fr split never
|
|
329
|
+
// gets a full row to divide.
|
|
330
|
+
width: '100%'
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
>
|
|
334
|
+
{showActive &&
|
|
335
|
+
renderColumn(
|
|
336
|
+
activeTitle,
|
|
337
|
+
visibleActive,
|
|
338
|
+
false,
|
|
339
|
+
activeEmptyLabel,
|
|
340
|
+
activeLoading
|
|
341
|
+
)}
|
|
342
|
+
{showUsed &&
|
|
343
|
+
renderColumn(usedTitle, visibleUsed, true, usedEmptyLabel, usedLoading)}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export default CouponWalletBlock;
|