@akinon/pz-theme 2.0.61-rc.0 → 2.0.62-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.61-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0cf9ea239]
8
- - Updated dependencies [324f97d55]
9
- - Updated dependencies [51ea06888]
10
- - Updated dependencies
11
- - Updated dependencies [2fea4143]
12
- - Updated dependencies [b55acb768]
13
- - Updated dependencies [760258c1c]
14
- - Updated dependencies [143be2b9d]
15
- - Updated dependencies [7889b08fe]
16
- - Updated dependencies [9f8cd3bc5]
17
- - Updated dependencies [bfafa3f49]
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 [e9598c71]
27
- - Updated dependencies [4998a9631]
28
- - Updated dependencies [804d2bd6]
29
- - Updated dependencies [3909d3224]
30
- - Updated dependencies [6a3d8a63]
31
- - Updated dependencies [e18836b20]
32
- - @akinon/next@2.0.61-rc.0
33
- - @akinon/theme-contract@2.0.61-rc.0
3
+ ## 2.0.62-beta.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [cbbbfd75]
8
+ - @akinon/next@2.0.62-beta.0
9
+ - @akinon/theme-contract@2.0.62-beta.0
10
+
11
+ ## 2.0.61
12
+
13
+ ### Patch Changes
14
+
15
+ - 760ea8b: ZERO-4799: Read slider arrow button geometry from its own block styles
16
+ - @akinon/next@2.0.61
17
+ - @akinon/theme-contract@2.0.61
34
18
 
35
19
  ## 2.0.60
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.61-rc.0",
4
+ "version": "2.0.62-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.61-rc.0",
11
+ "@akinon/next": "2.0.62-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.61-rc.0",
16
+ "@akinon/theme-contract": "2.0.62-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.61-rc.0",
23
+ "@akinon/next": "2.0.62-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",
@@ -22,10 +22,7 @@ jest.mock('../../theme-block', () => ({
22
22
 
23
23
  import SliderBlock from '../slider-block';
24
24
 
25
- const child = (
26
- id: string,
27
- over: Record<string, unknown> = {}
28
- ): any => ({
25
+ const child = (id: string, over: Record<string, unknown> = {}): any => ({
29
26
  id,
30
27
  type: 'text',
31
28
  label: id,
@@ -146,3 +143,120 @@ describe('SliderBlock — child filtering, ordering and empty state', () => {
146
143
  expect(markers(container)).toEqual(['a', 'b', 'c']);
147
144
  });
148
145
  });
146
+
147
+ /**
148
+ * The arrow button's geometry used to be hardcoded, so the only way to reach it
149
+ * was to edit this component. These cover the contract the panel now relies on:
150
+ * the legacy circle when nothing is authored, and the block's own styles when
151
+ * something is.
152
+ */
153
+ describe('SliderBlock — arrow button geometry', () => {
154
+ const arrow = (label: string, styles: Record<string, unknown>) =>
155
+ child(label === 'Left Arrow Icon' ? 'left' : 'right', {
156
+ type: 'icon',
157
+ label,
158
+ styles
159
+ });
160
+
161
+ const renderArrows = (
162
+ styles: Record<string, unknown>,
163
+ over: Record<string, unknown> = {}
164
+ ) =>
165
+ render(
166
+ <SliderBlock
167
+ {...props({
168
+ isDesigner: true,
169
+ block: {
170
+ ...props().block,
171
+ blocks: [
172
+ arrow('Left Arrow Icon', styles),
173
+ child('s1'),
174
+ arrow('Right Arrow Icon', styles)
175
+ ]
176
+ },
177
+ ...over
178
+ })}
179
+ />
180
+ );
181
+
182
+ const previous = (container: HTMLElement) =>
183
+ container.querySelector('button[aria-label="Previous"]') as HTMLElement;
184
+
185
+ it('keeps the 40px circle when no geometry is authored', () => {
186
+ const { container } = renderArrows({});
187
+ const style = previous(container).style;
188
+
189
+ expect(style.width).toBe('40px');
190
+ expect(style.height).toBe('40px');
191
+ expect(style.borderRadius).toBe('50%');
192
+ });
193
+
194
+ it('takes width, height and radius from the arrow block', () => {
195
+ const { container } = renderArrows({
196
+ width: '32px',
197
+ height: '64px',
198
+ 'border-radius': '8px'
199
+ });
200
+ const style = previous(container).style;
201
+
202
+ expect(style.width).toBe('32px');
203
+ expect(style.height).toBe('64px');
204
+ expect(style.borderRadius).toBe('8px');
205
+ });
206
+
207
+ it('rounds one side only, which is what a half-round tab needs', () => {
208
+ const { container } = renderArrows({
209
+ width: '32px',
210
+ height: '64px',
211
+ 'border-top-left-radius': '32px',
212
+ 'border-bottom-left-radius': '32px'
213
+ });
214
+ const style = previous(container).style;
215
+
216
+ expect(style.borderTopLeftRadius).toBe('32px');
217
+ expect(style.borderBottomLeftRadius).toBe('32px');
218
+ // The legacy 50% must not stand in here, or the flat side would be round.
219
+ expect(style.borderTopRightRadius).toBe('');
220
+ expect(style.borderBottomRightRadius).toBe('');
221
+ });
222
+
223
+ it('writes the shorthand before a corner, so the corner overrides it', () => {
224
+ const { container } = renderArrows({
225
+ 'border-radius': '20px',
226
+ 'border-top-left-radius': '0px'
227
+ });
228
+ const button = previous(container);
229
+
230
+ expect(button.style.borderRadius).toBe('20px');
231
+ expect(button.style.borderTopLeftRadius).toBe('0px');
232
+
233
+ // Order is the whole mechanism: the shorthand has to be declared first or
234
+ // it would reset the corner. jsdom does not expand the shorthand into
235
+ // longhands, so the declaration order is what there is to assert.
236
+ const declarations = button.getAttribute('style') ?? '';
237
+ expect(declarations.indexOf('border-radius')).toBeLessThan(
238
+ declarations.indexOf('border-top-left-radius')
239
+ );
240
+ });
241
+
242
+ it('appends px to the bare numbers the numeric controls store', () => {
243
+ const { container } = renderArrows({ width: 48, height: 48 });
244
+ const style = previous(container).style;
245
+
246
+ expect(style.width).toBe('48px');
247
+ expect(style.height).toBe('48px');
248
+ });
249
+
250
+ it('reads the breakpoint being previewed, not always desktop', () => {
251
+ const styles = {
252
+ width: { desktop: '40px', mobile: '28px' },
253
+ height: { desktop: '40px', mobile: '28px' }
254
+ };
255
+
256
+ expect(previous(renderArrows(styles).container).style.width).toBe('40px');
257
+ expect(
258
+ previous(renderArrows(styles, { currentBreakpoint: 'mobile' }).container)
259
+ .style.width
260
+ ).toBe('28px');
261
+ });
262
+ });
@@ -118,8 +118,9 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
118
118
  }
119
119
  };
120
120
 
121
- const currentSlidesToSlide =
122
- isNarrowBreakpoint(currentBreakpoint) ? mobileSlides : desktopSlides;
121
+ const currentSlidesToSlide = isNarrowBreakpoint(currentBreakpoint)
122
+ ? mobileSlides
123
+ : desktopSlides;
123
124
 
124
125
  const showArrows = getResponsiveValue(
125
126
  getConfigEntry('show-arrows'),
@@ -263,32 +264,60 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
263
264
  const rightArrowIconUrl = getIconUrl(rightArrowIconBlock);
264
265
 
265
266
  const leftArrowIconSize = String(
266
- getResponsiveValue(leftArrowIconBlock?.styles?.size, '24')
267
+ getResponsiveValue(
268
+ leftArrowIconBlock?.styles?.size,
269
+ currentBreakpoint,
270
+ '24'
271
+ )
267
272
  );
268
273
  const rightArrowIconSize = String(
269
- getResponsiveValue(rightArrowIconBlock?.styles?.size, '24')
274
+ getResponsiveValue(
275
+ rightArrowIconBlock?.styles?.size,
276
+ currentBreakpoint,
277
+ '24'
278
+ )
270
279
  );
271
280
 
272
281
  const leftArrowIconColor = resolveThemeCssVariables(
273
282
  String(
274
- getResponsiveValue(leftArrowIconBlock?.styles?.['color'], '#ffffff')
283
+ getResponsiveValue(
284
+ leftArrowIconBlock?.styles?.['color'],
285
+ currentBreakpoint,
286
+ '#ffffff'
287
+ )
275
288
  ),
276
289
  themeSettings
277
290
  );
278
291
  const rightArrowIconColor = resolveThemeCssVariables(
279
292
  String(
280
- getResponsiveValue(rightArrowIconBlock?.styles?.['color'], '#ffffff')
293
+ getResponsiveValue(
294
+ rightArrowIconBlock?.styles?.['color'],
295
+ currentBreakpoint,
296
+ '#ffffff'
297
+ )
281
298
  ),
282
299
  themeSettings
283
300
  );
284
301
 
285
302
  const leftArrowIconOpacity =
286
303
  parseFloat(
287
- String(getResponsiveValue(leftArrowIconBlock?.styles?.['opacity'], '1'))
304
+ String(
305
+ getResponsiveValue(
306
+ leftArrowIconBlock?.styles?.['opacity'],
307
+ currentBreakpoint,
308
+ '1'
309
+ )
310
+ )
288
311
  ) || 1;
289
312
  const rightArrowIconOpacity =
290
313
  parseFloat(
291
- String(getResponsiveValue(rightArrowIconBlock?.styles?.['opacity'], '1'))
314
+ String(
315
+ getResponsiveValue(
316
+ rightArrowIconBlock?.styles?.['opacity'],
317
+ currentBreakpoint,
318
+ '1'
319
+ )
320
+ )
292
321
  ) || 1;
293
322
 
294
323
  // Process SVGs with color
@@ -328,6 +357,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
328
357
  getResponsiveValue(
329
358
  leftArrowIconBlock?.styles?.['background'] ??
330
359
  leftArrowIconBlock?.styles?.['backgroundColor'],
360
+ currentBreakpoint,
331
361
  'rgba(0, 0, 0, 0.5)'
332
362
  )
333
363
  ),
@@ -339,12 +369,80 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
339
369
  getResponsiveValue(
340
370
  rightArrowIconBlock?.styles?.['background'] ??
341
371
  rightArrowIconBlock?.styles?.['backgroundColor'],
372
+ currentBreakpoint,
342
373
  'rgba(0, 0, 0, 0.5)'
343
374
  )
344
375
  ),
345
376
  themeSettings
346
377
  );
347
378
 
379
+ // The arrow button's geometry used to live here and nowhere else — a 40px
380
+ // circle — so the panel could set the button's background and nothing else: a
381
+ // half-round tab, a square, a wider pill were all unreachable, and a Corner
382
+ // Radius typed in the editor was stored and then ignored. Geometry now comes
383
+ // from the arrow icon block's own styles, with the old circle as the default
384
+ // so a slider that authored none of it renders exactly as before.
385
+ //
386
+ // Only geometry is read. `color` and `opacity` stay bound to the icon image
387
+ // below; reading them here as well would apply each of them twice.
388
+ const arrowLength = (value: unknown): string | undefined => {
389
+ const text = String(value ?? '').trim();
390
+ if (!text) return undefined;
391
+ // A bare number is what the editor's numeric controls store; its free-text
392
+ // corner fields carry their own unit (`32px`, `50%`).
393
+ return /^-?\d*\.?\d+$/.test(text) ? `${text}px` : text;
394
+ };
395
+
396
+ const arrowGeometry = (iconBlock?: {
397
+ styles?: Record<string, unknown>;
398
+ }): React.CSSProperties => {
399
+ const styles = iconBlock?.styles ?? {};
400
+ const read = (key: string) => {
401
+ // Published JSON may carry either spelling of a two-word key.
402
+ const camelKey = key.replace(/-([a-z])/g, (_, letter: string) =>
403
+ letter.toUpperCase()
404
+ );
405
+ return arrowLength(
406
+ getResponsiveValue(
407
+ styles[key] ?? styles[camelKey],
408
+ currentBreakpoint,
409
+ ''
410
+ )
411
+ );
412
+ };
413
+
414
+ const corners: Record<string, string> = {};
415
+ (
416
+ [
417
+ ['border-top-left-radius', 'borderTopLeftRadius'],
418
+ ['border-top-right-radius', 'borderTopRightRadius'],
419
+ ['border-bottom-right-radius', 'borderBottomRightRadius'],
420
+ ['border-bottom-left-radius', 'borderBottomLeftRadius']
421
+ ] as const
422
+ ).forEach(([styleKey, cssProperty]) => {
423
+ const value = read(styleKey);
424
+ if (value) corners[cssProperty] = value;
425
+ });
426
+
427
+ const geometry: React.CSSProperties = {
428
+ width: read('width') ?? '40px',
429
+ height: read('height') ?? '40px'
430
+ };
431
+
432
+ // The shorthand is written before the corners so an authored corner
433
+ // overrides just that corner, the way the cascade does. The 50% default
434
+ // only stands in when no radius was authored at all — otherwise rounding
435
+ // one corner would leave the other three on the legacy circle.
436
+ const radius = read('border-radius');
437
+ if (radius) geometry.borderRadius = radius;
438
+ else if (Object.keys(corners).length === 0) geometry.borderRadius = '50%';
439
+
440
+ return { ...geometry, ...corners };
441
+ };
442
+
443
+ const leftArrowButtonStyle = arrowGeometry(leftArrowIconBlock);
444
+ const rightArrowButtonStyle = arrowGeometry(rightArrowIconBlock);
445
+
348
446
  const handleLeftArrowClick = (e: React.MouseEvent) => {
349
447
  e.preventDefault();
350
448
  e.stopPropagation();
@@ -377,9 +475,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
377
475
  style={{
378
476
  backgroundColor: leftArrowBackground,
379
477
  border: 'none',
380
- borderRadius: '50%',
381
- width: '40px',
382
- height: '40px',
478
+ ...leftArrowButtonStyle,
383
479
  display: 'flex',
384
480
  alignItems: 'center',
385
481
  justifyContent: 'center',
@@ -419,9 +515,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
419
515
  style={{
420
516
  backgroundColor: rightArrowBackground,
421
517
  border: 'none',
422
- borderRadius: '50%',
423
- width: '40px',
424
- height: '40px',
518
+ ...rightArrowButtonStyle,
425
519
  display: 'flex',
426
520
  alignItems: 'center',
427
521
  justifyContent: 'center',
@@ -544,9 +638,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
544
638
  style={{
545
639
  backgroundColor: leftArrowBackground,
546
640
  border: 'none',
547
- borderRadius: '50%',
548
- width: '40px',
549
- height: '40px',
641
+ ...leftArrowButtonStyle,
550
642
  display: 'flex',
551
643
  alignItems: 'center',
552
644
  justifyContent: 'center',
@@ -577,9 +669,7 @@ const SliderBlock: React.FC<BlockRendererProps> = ({
577
669
  style={{
578
670
  backgroundColor: rightArrowBackground,
579
671
  border: 'none',
580
- borderRadius: '50%',
581
- width: '40px',
582
- height: '40px',
672
+ ...rightArrowButtonStyle,
583
673
  display: 'flex',
584
674
  alignItems: 'center',
585
675
  justifyContent: 'center',