@akinon/pz-theme 2.0.130-beta.0 → 2.0.130

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.
@@ -23,9 +23,12 @@ import {
23
23
  import { hasBindingToken } from './iterator-utils';
24
24
  import { isCustomThemeType } from '../custom/types';
25
25
  import {
26
- BREAKPOINT_FALLBACKS,
26
+ type CustomBreakpoint,
27
27
  MOBILE_MEDIA_QUERY,
28
- TABLET_MEDIA_QUERY
28
+ TABLET_MEDIA_QUERY,
29
+ baseBreakpointOf,
30
+ customBreakpointMediaQuery,
31
+ fallbackChainOf
29
32
  } from './breakpoints';
30
33
  import {
31
34
  isSectionMaxWidthToken,
@@ -329,13 +332,11 @@ export const getResponsiveValue = (
329
332
  // a tablet reads the mobile value before it reads the desktop one, because
330
333
  // the mobile rule is the one that covers 768-1023 when no tablet rule was
331
334
  // emitted. `desktop` and `mobile` have no intermediate step, so this loop
332
- // does not run for them and they resolve exactly as they always have.
333
- const chain = BREAKPOINT_FALLBACKS[breakpoint];
334
- if (chain) {
335
- for (const next of chain) {
336
- const inherited = responsiveValue[next];
337
- if (inherited !== undefined) return inherited;
338
- }
335
+ // does not run for them and they resolve exactly as they always have. A
336
+ // custom breakpoint reads the built-in one it stands on.
337
+ for (const next of fallbackChainOf(breakpoint)) {
338
+ const inherited = responsiveValue[next];
339
+ if (inherited !== undefined) return inherited;
339
340
  }
340
341
 
341
342
  if (responsiveValue.desktop !== undefined) return responsiveValue.desktop;
@@ -851,17 +852,15 @@ const isAuthored = (value: unknown): value is string | number =>
851
852
  */
852
853
  const resolveEntryValue = (
853
854
  entry: ResponsiveEntry,
854
- breakpoint: string
855
+ breakpoint: string,
856
+ customBreakpoints?: CustomBreakpoint[]
855
857
  ): string | number | undefined => {
856
858
  const own = entry[breakpoint as keyof ResponsiveEntry];
857
859
  if (own !== undefined) return own;
858
860
 
859
- const chain = BREAKPOINT_FALLBACKS[breakpoint];
860
- if (chain) {
861
- for (const next of chain) {
862
- const inherited = entry[next as keyof ResponsiveEntry];
863
- if (inherited !== undefined) return inherited;
864
- }
861
+ for (const next of fallbackChainOf(breakpoint, customBreakpoints)) {
862
+ const inherited = entry[next as keyof ResponsiveEntry];
863
+ if (inherited !== undefined) return inherited;
865
864
  }
866
865
 
867
866
  return entry.desktop;
@@ -974,14 +973,31 @@ const formatCSSValue = (property: string, value: string | number): string => {
974
973
  return String(value);
975
974
  };
976
975
 
976
+ /** The helper keys the hover rules are built from. */
977
+ const HOVER_HELPER_KEYS = [
978
+ 'hover-color',
979
+ 'hover-opacity',
980
+ 'hover-flex',
981
+ 'hover-child-flex'
982
+ ];
983
+
977
984
  const generateElementCSS = (
978
985
  selector: string,
979
986
  rawStyles: ResponsiveStyles,
980
987
  targetBreakpoint?: string,
981
- options?: { treatHelperKeysAsCss?: boolean }
988
+ options?: {
989
+ treatHelperKeysAsCss?: boolean;
990
+ /**
991
+ * The theme's custom breakpoints. Passed in rather than read from the
992
+ * browser registry because the live stylesheet is built on the server.
993
+ */
994
+ customBreakpoints?: CustomBreakpoint[];
995
+ }
982
996
  ): string => {
983
997
  if (!rawStyles || Object.keys(rawStyles).length === 0) return '';
984
998
 
999
+ const customBreakpoints = options?.customBreakpoints ?? [];
1000
+
985
1001
  // Bound values belong to the per-row resolution, not to the shared rule.
986
1002
  const styles = stripBoundStyleValues(
987
1003
  rawStyles as Record<string, unknown>
@@ -997,22 +1013,41 @@ const generateElementCSS = (
997
1013
  return styles[kebabKey] ?? styles[camelKey];
998
1014
  };
999
1015
 
1016
+ // `getResponsiveValue` with this theme's custom breakpoints in view: their own
1017
+ // value, and otherwise whatever the built-in breakpoint they stand on reads.
1018
+ const valueAt = (
1019
+ entry: unknown,
1020
+ breakpoint: string,
1021
+ fallback: unknown
1022
+ ): unknown => {
1023
+ const custom = customBreakpoints.find((bp) => bp.name === breakpoint);
1024
+
1025
+ if (custom && entry && typeof entry === 'object' && !Array.isArray(entry)) {
1026
+ const own = (entry as Record<string, unknown>)[breakpoint];
1027
+ if (own !== undefined) return own;
1028
+
1029
+ return getResponsiveValue(entry, baseBreakpointOf(custom), fallback);
1030
+ }
1031
+
1032
+ return getResponsiveValue(entry, breakpoint, fallback);
1033
+ };
1034
+
1000
1035
  const getBackgroundAlpha = (breakpoint: string): number | null => {
1001
1036
  const entry = getStyleEntry('background-opacity');
1002
- const value = entry ? getResponsiveValue(entry, breakpoint, '100') : '100';
1037
+ const value = entry ? valueAt(entry, breakpoint, '100') : '100';
1003
1038
  return clampAlpha(value, 100);
1004
1039
  };
1005
1040
 
1006
1041
  const getHoverAlpha = (breakpoint: string): number | null => {
1007
1042
  const entry = getStyleEntry('hover-opacity');
1008
- const value = entry ? getResponsiveValue(entry, breakpoint, '100') : '100';
1043
+ const value = entry ? valueAt(entry, breakpoint, '100') : '100';
1009
1044
  return clampAlpha(value, 100);
1010
1045
  };
1011
1046
 
1012
1047
  const getHoverColor = (breakpoint: string): string | null => {
1013
1048
  const entry = getStyleEntry('hover-color');
1014
1049
  const value = entry
1015
- ? getResponsiveValue(entry, breakpoint, 'var(--theme-secondary)')
1050
+ ? valueAt(entry, breakpoint, 'var(--theme-secondary)')
1016
1051
  : undefined;
1017
1052
  if (value === undefined || value === null || value === '') return null;
1018
1053
  return String(value);
@@ -1021,7 +1056,7 @@ const generateElementCSS = (
1021
1056
  const getHoverFlex = (breakpoint: string): string | null => {
1022
1057
  const entry = getStyleEntry('hover-flex');
1023
1058
  const value = entry
1024
- ? getResponsiveValue(entry, breakpoint, undefined)
1059
+ ? valueAt(entry, breakpoint, undefined)
1025
1060
  : undefined;
1026
1061
  if (value === undefined || value === null || value === '') return null;
1027
1062
  return String(value);
@@ -1030,7 +1065,7 @@ const generateElementCSS = (
1030
1065
  const getHoverChildFlex = (breakpoint: string): string | null => {
1031
1066
  const entry = getStyleEntry('hover-child-flex');
1032
1067
  const value = entry
1033
- ? getResponsiveValue(entry, breakpoint, undefined)
1068
+ ? valueAt(entry, breakpoint, undefined)
1034
1069
  : undefined;
1035
1070
  if (value === undefined || value === null || value === '') return null;
1036
1071
  return String(value);
@@ -1120,7 +1155,8 @@ const generateElementCSS = (
1120
1155
  // canvas show a tablet the device never renders.
1121
1156
  const breakpointValue = resolveEntryValue(
1122
1157
  responsiveValue,
1123
- targetBreakpoint
1158
+ targetBreakpoint,
1159
+ customBreakpoints
1124
1160
  );
1125
1161
  if (
1126
1162
  breakpointValue !== undefined &&
@@ -1230,6 +1266,9 @@ const generateElementCSS = (
1230
1266
  const desktopStyles: string[] = [];
1231
1267
  const mobileStyles: string[] = [];
1232
1268
  const tabletStyles: string[] = [];
1269
+ const customStyles = new Map<string, string[]>(
1270
+ customBreakpoints.map((bp) => [bp.name, []])
1271
+ );
1233
1272
 
1234
1273
  /**
1235
1274
  * The declaration value one breakpoint would produce for one property, by
@@ -1405,6 +1444,23 @@ const generateElementCSS = (
1405
1444
  tabletStyles.push(` ${cssProperty}: ${formattedTablet}`);
1406
1445
  }
1407
1446
  }
1447
+
1448
+ // A custom breakpoint writes only what it sets itself; everything else
1449
+ // in its range is already painted by the rules above.
1450
+ customBreakpoints.forEach((bp) => {
1451
+ const own = (responsiveValue as Record<string, unknown>)[bp.name];
1452
+ if (!isAuthored(own)) return;
1453
+
1454
+ const formatted = declarationValue(
1455
+ property,
1456
+ cssProperty,
1457
+ bp.name,
1458
+ own
1459
+ );
1460
+ if (formatted) {
1461
+ customStyles.get(bp.name)?.push(` ${cssProperty}: ${formatted}`);
1462
+ }
1463
+ });
1408
1464
  } else {
1409
1465
  if (cssProperty === 'background-color' && typeof value === 'string') {
1410
1466
  const backgroundAlpha = getBackgroundAlpha('desktop');
@@ -1463,6 +1519,32 @@ const generateElementCSS = (
1463
1519
  css += `${TABLET_MEDIA_QUERY} {\n${tabletDeclarations}${tabletHoverCss}}\n`;
1464
1520
  }
1465
1521
 
1522
+ // Last of all: a custom range overrides whichever built-in rules match inside
1523
+ // it, and at equal specificity it is source order that lets it. Nothing is
1524
+ // written for a theme without custom breakpoints, so its stylesheet is the
1525
+ // one it always was.
1526
+ customBreakpoints.forEach((bp) => {
1527
+ const declarations = customStyles.get(bp.name) ?? [];
1528
+ const setsHover = HOVER_HELPER_KEYS.some((key) => {
1529
+ const entry = getStyleEntry(key);
1530
+ return (
1531
+ !!entry &&
1532
+ typeof entry === 'object' &&
1533
+ isAuthored((entry as Record<string, unknown>)[bp.name])
1534
+ );
1535
+ });
1536
+ const hoverCss = setsHover ? buildHoverCss(bp.name, ' ') : '';
1537
+
1538
+ if (declarations.length === 0 && !hoverCss) return;
1539
+
1540
+ const rule =
1541
+ declarations.length > 0
1542
+ ? ` ${selector} {\n ${declarations.join(';\n ')};\n }\n`
1543
+ : '';
1544
+
1545
+ css += `${customBreakpointMediaQuery(bp)} {\n${rule}${hoverCss}}\n`;
1546
+ });
1547
+
1466
1548
  return css;
1467
1549
  };
1468
1550
 
@@ -1477,7 +1559,8 @@ const generateElementCSS = (
1477
1559
  export const generatePartCSS = (
1478
1560
  blockSelector: string,
1479
1561
  styles: Record<string, unknown>,
1480
- targetBreakpoint?: string
1562
+ targetBreakpoint?: string,
1563
+ customBreakpoints?: CustomBreakpoint[]
1481
1564
  ): string => {
1482
1565
  const { parts } = splitPartStyles(styles);
1483
1566
  if (parts.length === 0) return '';
@@ -1492,7 +1575,7 @@ export const generatePartCSS = (
1492
1575
  selectors.join(',\n'),
1493
1576
  bucket.styles as ResponsiveStyles,
1494
1577
  targetBreakpoint,
1495
- { treatHelperKeysAsCss: true }
1578
+ { treatHelperKeysAsCss: true, customBreakpoints }
1496
1579
  );
1497
1580
 
1498
1581
  if (!rules) return;
@@ -1540,7 +1623,8 @@ const generateRootStateCSS = (
1540
1623
  * stateless rule does — and the guard has to be placed per state expression,
1541
1624
  * which is why the flag travels here instead of a finished selector.
1542
1625
  */
1543
- targetsInnerRoot = false
1626
+ targetsInnerRoot = false,
1627
+ customBreakpoints?: CustomBreakpoint[]
1544
1628
  ): string => {
1545
1629
  const { rootStates } = splitPartStyles(styles);
1546
1630
  if (rootStates.length === 0) return '';
@@ -1562,7 +1646,7 @@ const generateRootStateCSS = (
1562
1646
  // `:<state>/opacity` the editor offers is dropped on the floor. A state
1563
1647
  // BUCKET has no such helper: there it is a real CSS property, exactly as
1564
1648
  // it is on a part, which is why `generatePartCSS` has always passed this.
1565
- { treatHelperKeysAsCss: true }
1649
+ { treatHelperKeysAsCss: true, customBreakpoints }
1566
1650
  );
1567
1651
 
1568
1652
  if (!rules) return;
@@ -1581,11 +1665,28 @@ const generateRootStateCSS = (
1581
1665
  return css;
1582
1666
  };
1583
1667
 
1668
+ /**
1669
+ * The canvas target as one of the three built-in breakpoints, for the emitters
1670
+ * that know only those: a custom breakpoint becomes the band it stands on.
1671
+ */
1672
+ const builtInTargetOf = (
1673
+ targetBreakpoint: string | undefined,
1674
+ customBreakpoints: CustomBreakpoint[]
1675
+ ): string | undefined => {
1676
+ const custom = targetBreakpoint
1677
+ ? customBreakpoints.find((bp) => bp.name === targetBreakpoint)
1678
+ : undefined;
1679
+
1680
+ return custom ? baseBreakpointOf(custom) : targetBreakpoint;
1681
+ };
1682
+
1584
1683
  const collectBlockCSS = (
1585
1684
  blocks: Block[],
1586
- targetBreakpoint?: string
1685
+ targetBreakpoint?: string,
1686
+ customBreakpoints: CustomBreakpoint[] = []
1587
1687
  ): string => {
1588
1688
  let css = '';
1689
+ const hatchBreakpoint = builtInTargetOf(targetBreakpoint, customBreakpoints);
1589
1690
 
1590
1691
  blocks.forEach((block) => {
1591
1692
  // A block that IS or CONTAINS the checkout flow outlet has the real
@@ -1630,7 +1731,8 @@ const collectBlockCSS = (
1630
1731
  css += generateElementCSS(
1631
1732
  rootSelectors.join(',\n'),
1632
1733
  root,
1633
- targetBreakpoint
1734
+ targetBreakpoint,
1735
+ { customBreakpoints }
1634
1736
  );
1635
1737
  }
1636
1738
 
@@ -1640,10 +1742,16 @@ const collectBlockCSS = (
1640
1742
  selector,
1641
1743
  blockStyles,
1642
1744
  targetBreakpoint,
1643
- targetsInnerRoot
1745
+ targetsInnerRoot,
1746
+ customBreakpoints
1644
1747
  );
1645
1748
 
1646
- css += generatePartCSS(selector, blockStyles, targetBreakpoint);
1749
+ css += generatePartCSS(
1750
+ selector,
1751
+ blockStyles,
1752
+ targetBreakpoint,
1753
+ customBreakpoints
1754
+ );
1647
1755
  }
1648
1756
 
1649
1757
  if (block.properties) {
@@ -1653,7 +1761,7 @@ const collectBlockCSS = (
1653
1761
  const customCss = buildCustomCss(
1654
1762
  block.properties as Record<string, unknown>,
1655
1763
  selector,
1656
- targetBreakpoint
1764
+ hatchBreakpoint
1657
1765
  );
1658
1766
 
1659
1767
  css += guardsCheckoutFlow ? stripHidingCustomCss(customCss) : customCss;
@@ -1664,7 +1772,7 @@ const collectBlockCSS = (
1664
1772
  const partCustomCss = buildPartCustomCss(
1665
1773
  block.properties as Record<string, unknown>,
1666
1774
  selector,
1667
- targetBreakpoint
1775
+ hatchBreakpoint
1668
1776
  );
1669
1777
 
1670
1778
  css += guardsCheckoutFlow
@@ -1673,7 +1781,11 @@ const collectBlockCSS = (
1673
1781
  }
1674
1782
 
1675
1783
  if (block.blocks && block.blocks.length > 0) {
1676
- css += collectBlockCSS(block.blocks, targetBreakpoint);
1784
+ css += collectBlockCSS(
1785
+ block.blocks,
1786
+ targetBreakpoint,
1787
+ customBreakpoints
1788
+ );
1677
1789
  }
1678
1790
  });
1679
1791
 
@@ -1682,15 +1794,22 @@ const collectBlockCSS = (
1682
1794
 
1683
1795
  export const generateThemeCSS = (
1684
1796
  sections: Section[],
1685
- targetBreakpoint?: string
1797
+ targetBreakpoint?: string,
1798
+ /** The theme's custom breakpoints (`readCustomBreakpoints`). */
1799
+ customBreakpoints: CustomBreakpoint[] = []
1686
1800
  ): string => {
1801
+ // The custom-CSS hatch and the scoped line breaks know the three built-in
1802
+ // bands only. On the canvas a custom breakpoint shows them as the band it
1803
+ // stands on does, which is also what its range shows on the live site.
1804
+ const hatchBreakpoint = builtInTargetOf(targetBreakpoint, customBreakpoints);
1805
+
1687
1806
  // First, and outside every section selector: the rules are global by nature
1688
1807
  // (a `<br>` scoped to the phone means the same thing wherever it was typed),
1689
1808
  // and they must lose a specificity tie to anything a section or block emits
1690
1809
  // for itself, which source order settles. Emitted only when the composition
1691
1810
  // actually carries a scoped break — see utils/line-breaks.ts.
1692
1811
  let css = compositionUsesScopedLineBreaks(sections)
1693
- ? buildLineBreakCss(targetBreakpoint)
1812
+ ? buildLineBreakCss(hatchBreakpoint)
1694
1813
  : '';
1695
1814
 
1696
1815
  sections.forEach((section) => {
@@ -1745,7 +1864,8 @@ export const generateThemeCSS = (
1745
1864
  css += generateElementCSS(
1746
1865
  sectionSelector,
1747
1866
  resolvesOwnMaxWidth ? band : resolveSectionMaxWidthTokens(band),
1748
- targetBreakpoint
1867
+ targetBreakpoint,
1868
+ { customBreakpoints }
1749
1869
  );
1750
1870
  }
1751
1871
 
@@ -1757,7 +1877,8 @@ export const generateThemeCSS = (
1757
1877
  css += generateElementCSS(
1758
1878
  `${sectionSelector} > [${SECTION_CONTENT_ATTRIBUTE}]`,
1759
1879
  content as ResponsiveStyles,
1760
- targetBreakpoint
1880
+ targetBreakpoint,
1881
+ { customBreakpoints }
1761
1882
  );
1762
1883
  }
1763
1884
  }
@@ -1765,10 +1886,17 @@ export const generateThemeCSS = (
1765
1886
  css += generateRootStateCSS(
1766
1887
  sectionSelector,
1767
1888
  sectionStyles,
1768
- targetBreakpoint
1889
+ targetBreakpoint,
1890
+ false,
1891
+ customBreakpoints
1769
1892
  );
1770
1893
 
1771
- css += generatePartCSS(sectionSelector, sectionStyles, targetBreakpoint);
1894
+ css += generatePartCSS(
1895
+ sectionSelector,
1896
+ sectionStyles,
1897
+ targetBreakpoint,
1898
+ customBreakpoints
1899
+ );
1772
1900
  }
1773
1901
 
1774
1902
  if (section.properties) {
@@ -1778,14 +1906,18 @@ export const generateThemeCSS = (
1778
1906
  const customCss = buildCustomCss(
1779
1907
  section.properties as Record<string, unknown>,
1780
1908
  sectionSelector,
1781
- targetBreakpoint
1909
+ hatchBreakpoint
1782
1910
  );
1783
1911
 
1784
1912
  css += guardsCheckoutFlow ? stripHidingCustomCss(customCss) : customCss;
1785
1913
  }
1786
1914
 
1787
1915
  if (section.blocks && section.blocks.length > 0) {
1788
- css += collectBlockCSS(section.blocks, targetBreakpoint);
1916
+ css += collectBlockCSS(
1917
+ section.blocks,
1918
+ targetBreakpoint,
1919
+ customBreakpoints
1920
+ );
1789
1921
  }
1790
1922
  });
1791
1923
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  import type { Block } from '../theme-block';
43
43
  import type { Section } from '../theme-section';
44
- import { BREAKPOINT_FALLBACKS } from './breakpoints';
44
+ import { fallbackChainOf } from './breakpoints';
45
45
 
46
46
  export const VISIBLE_WHEN_PROPERTY = 'visibleWhenProperty';
47
47
  export const HOVER_REVEAL_WHEN_PROPERTY = 'hoverRevealWhenProperty';
@@ -95,7 +95,7 @@ const readResponsive = (value: unknown, breakpoint: string): unknown => {
95
95
  }
96
96
 
97
97
  const record = value as Properties;
98
- const chain = [breakpoint, ...(BREAKPOINT_FALLBACKS[breakpoint] ?? []), 'desktop'];
98
+ const chain = [breakpoint, ...fallbackChainOf(breakpoint), 'desktop'];
99
99
 
100
100
  for (const key of chain) {
101
101
  if (record[key] !== undefined) return record[key];
@@ -1,4 +1,4 @@
1
- import { BREAKPOINTS, BREAKPOINT_FALLBACKS } from './breakpoints';
1
+ import { BREAKPOINTS, fallbackChainOf } from './breakpoints';
2
2
 
3
3
  /**
4
4
  * How wide the CONTENT of a band is allowed to get, as opposed to how wide the
@@ -73,7 +73,7 @@ const at = (value: unknown, breakpoint: string): unknown => {
73
73
  const own = value[breakpoint];
74
74
  if (own !== undefined) return own;
75
75
 
76
- for (const next of BREAKPOINT_FALLBACKS[breakpoint] ?? []) {
76
+ for (const next of fallbackChainOf(breakpoint)) {
77
77
  if (value[next] !== undefined) return value[next];
78
78
  }
79
79
 
@@ -1,6 +1,6 @@
1
1
  import { Block } from '../theme-block';
2
2
  import { Section } from '../theme-section';
3
- import { BREAKPOINT_FALLBACKS } from './breakpoints';
3
+ import { fallbackChainOf } from './breakpoints';
4
4
  import { containsCheckoutOutlet } from './checkout-safety';
5
5
  import { devWarn } from './dev-warn';
6
6
 
@@ -87,10 +87,10 @@ export const isBreakpointAllowed = (
87
87
  ): boolean => {
88
88
  if (allowed.includes(breakpoint)) return true;
89
89
 
90
- const chain = BREAKPOINT_FALLBACKS[breakpoint];
91
- if (!chain) return false;
92
-
93
- return chain.some((next) => allowed.includes(next));
90
+ // A custom breakpoint the list does not name answers as the built-in one it
91
+ // stands on, the way an unticked-because-it-did-not-exist tablet answers as
92
+ // mobile: adding a breakpoint must not hide what was already showing there.
93
+ return fallbackChainOf(breakpoint).some((next) => allowed.includes(next));
94
94
  };
95
95
 
96
96
  export interface VisibilityRuleContext {
@@ -548,7 +548,7 @@ const readResponsive = (value: unknown, breakpoint?: string): unknown => {
548
548
  const record = value as Record<string, unknown>;
549
549
  const chain = [
550
550
  breakpoint,
551
- ...(breakpoint ? BREAKPOINT_FALLBACKS[breakpoint] ?? [] : []),
551
+ ...(breakpoint ? fallbackChainOf(breakpoint) : []),
552
552
  'desktop'
553
553
  ];
554
554