@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
@@ -3,7 +3,9 @@
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';
6
7
  import { useThemePageContext } from '../theme-page-context';
8
+ import { useThemeSettingsContext } from '../theme-settings-context';
7
9
  import { BlockRendererProps } from './block-renderer-registry';
8
10
  import {
9
11
  DesignerPlaceholder,
@@ -70,13 +72,19 @@ const ProductBreadcrumbBlock = ({
70
72
  }: BlockRendererProps) => {
71
73
  const product = useThemeProduct();
72
74
  const pageContext = useThemePageContext();
75
+ const themeSettings = useThemeSettingsContext();
73
76
 
74
77
  const prop = (key: string, fallback: string) =>
75
78
  String(
76
79
  getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
77
80
  );
78
81
 
79
- const separator = prop('separator', '/');
82
+ // An unset separator follows the theme's breadcrumb setting; a
83
+ // stored one (including '/') keeps winning, so saved blocks render as before.
84
+ const separator = prop(
85
+ 'separator',
86
+ readThemeBreadcrumbSeparator(themeSettings) ?? '/'
87
+ );
80
88
  const ariaLabel = prop('ariaLabel', 'Breadcrumb');
81
89
  const homeLabel = prop('homeLabel', 'Home');
82
90
  const homeHref = prop('homeHref', '/');
@@ -119,8 +119,39 @@ export const SOCIAL_GLYPHS: Record<string, React.ReactNode> = {
119
119
  )
120
120
  };
121
121
 
122
+ /**
123
+ * Keys that draw another key's mark. The block pickers store `twitter`; the
124
+ * settings page's social networks store X as `x`. Kept out of
125
+ * `SOCIAL_GLYPHS` itself so that table stays the list of what the picker
126
+ * offers, with one path per mark.
127
+ */
128
+ export const SOCIAL_GLYPH_ALIASES: Record<string, string> = {
129
+ x: 'twitter'
130
+ };
131
+
132
+ /**
133
+ * The glyph for a platform key, following aliases; `undefined` when the
134
+ * platform has no mark (callers choose their own fallback, usually `globe`).
135
+ */
136
+ export const resolveSocialGlyph = (
137
+ platform: string
138
+ ): React.ReactNode | undefined => {
139
+ // Own keys only: a platform read from settings could be `constructor`.
140
+ const own = (table: Record<string, unknown>, key: string | undefined) =>
141
+ key !== undefined && Object.prototype.hasOwnProperty.call(table, key);
142
+
143
+ if (own(SOCIAL_GLYPHS, platform)) return SOCIAL_GLYPHS[platform];
144
+
145
+ const alias = own(SOCIAL_GLYPH_ALIASES, platform)
146
+ ? SOCIAL_GLYPH_ALIASES[platform]
147
+ : undefined;
148
+ return own(SOCIAL_GLYPHS, alias) ? SOCIAL_GLYPHS[alias as string] : undefined;
149
+ };
150
+
122
151
  export const PLATFORM_LABELS: Record<string, string> = {
123
152
  twitter: 'Twitter / X',
153
+ // The settings page's key for X.
154
+ x: 'X',
124
155
  globe: 'Website',
125
156
  email: 'Email',
126
157
  phone: 'Phone'
@@ -3,11 +3,17 @@
3
3
  import React from 'react';
4
4
  import { ThemeLink as NextLink } from '../components/theme-link';
5
5
  import { partAttrs } from '../utils';
6
+ import { readThemeSocialNetworks } from '../utils/theme-settings-readers';
7
+ import { useThemeSettingsContext } from '../theme-settings-context';
6
8
  import { BlockRendererProps } from './block-renderer-registry';
7
- import { DesignerPlaceholder } from './product-block-utils';
9
+ import { DesignerPlaceholder, readToggle } from './product-block-utils';
8
10
  import { readResponsiveArray } from './responsive-array';
9
11
  import { IconGlyph } from './icon-source';
10
- import { SOCIAL_GLYPHS, platformLabel } from './social-glyphs';
12
+ import {
13
+ SOCIAL_GLYPHS,
14
+ platformLabel,
15
+ resolveSocialGlyph
16
+ } from './social-glyphs';
11
17
 
12
18
  /**
13
19
  * What the editor's social icon panel writes per entry
@@ -37,12 +43,29 @@ const clampSize = (value: unknown, fallback: number) => {
37
43
  */
38
44
  const isExternalHref = (href: string) => /^(https?:)?\/\//i.test(href);
39
45
 
46
+ interface SocialIconEntry {
47
+ key: string;
48
+ platform: string;
49
+ href: string;
50
+ label: string;
51
+ size: number;
52
+ color: string;
53
+ /** A brand-uploaded icon as a safe image source; null draws the glyph. */
54
+ iconSrc: string | null;
55
+ }
56
+
40
57
  /**
41
58
  * Functional chrome block: a row of social platform marks, each one a link.
42
59
  * Entries are authored through the `icons` property (platform, url, size,
43
60
  * colour); this block has no children, no captions and no layout properties —
44
61
  * it is `social-links` reduced to the icons.
45
62
  *
63
+ * With `useThemeSocialNetworks` on and no entries of its own, the
64
+ * block draws the theme's `socialNetworks` (settings page) instead, in their
65
+ * stored order. Opt-in because the block saves `icons: []` by default and the
66
+ * old settings page already stored networks: an automatic fallback would make
67
+ * blocks that render nothing today start drawing icons on existing sites.
68
+ *
46
69
  * NO BOX OF ITS OWN, deliberately. The entries are returned as siblings so
47
70
  * they are the direct children of the `[data-block-id]` wrapper the generated
48
71
  * stylesheet writes onto. The descriptor advertises `gap` as a block-level
@@ -50,7 +73,7 @@ const isExternalHref = (href: string) => /^(https?:)?\/\//i.test(href);
50
73
  * on the wrapper only reaches the icons if nothing of ours sits in between —
51
74
  * a list element here would collect the whole row into one flex item.
52
75
  *
53
- * Properties: icons.
76
+ * Properties: icons, useThemeSocialNetworks.
54
77
  * Parts: link, icon.
55
78
  */
56
79
  const SocialIconsListBlock = ({
@@ -64,7 +87,14 @@ const SocialIconsListBlock = ({
64
87
  currentBreakpoint
65
88
  );
66
89
 
67
- const entries = items
90
+ const themeSettings = useThemeSettingsContext();
91
+ const themeNetworksOn = readToggle(
92
+ block.properties?.useThemeSocialNetworks,
93
+ currentBreakpoint,
94
+ false
95
+ );
96
+
97
+ const ownEntries: SocialIconEntry[] = items
68
98
  .filter((item) => item && (item.icon || item.link))
69
99
  .map((item, index) => {
70
100
  const platform = String(item.icon || 'globe').toLowerCase();
@@ -78,14 +108,34 @@ const SocialIconsListBlock = ({
78
108
  // 24 is what the panel seeds a new entry with, so an entry saved
79
109
  // before the size field existed matches the ones saved after it.
80
110
  size: clampSize(item.size, 24),
81
- color: String(item.color || '')
111
+ color: String(item.color || ''),
112
+ iconSrc: null
82
113
  };
83
114
  });
84
115
 
116
+ // The block's own entries always win; the theme list only fills an empty
117
+ // block that asked for it. Hrefs and icons are validated by the reader.
118
+ const entries: SocialIconEntry[] =
119
+ ownEntries.length === 0 && themeNetworksOn
120
+ ? readThemeSocialNetworks(themeSettings).map((network, index) => ({
121
+ key: `${network.network}-${index}`,
122
+ platform: network.network,
123
+ href: network.href,
124
+ label: platformLabel(network.network),
125
+ size: 24,
126
+ color: '',
127
+ iconSrc: network.iconSrc
128
+ }))
129
+ : ownEntries;
130
+
85
131
  if (entries.length === 0) {
86
132
  return isDesigner ? (
87
133
  <DesignerPlaceholder
88
- label="Social icons (no entries)"
134
+ label={
135
+ themeNetworksOn
136
+ ? 'Social icons (no theme social networks)'
137
+ : 'Social icons (no entries)'
138
+ }
89
139
  attribute="data-social-icons-list"
90
140
  />
91
141
  ) : null;
@@ -96,9 +146,23 @@ const SocialIconsListBlock = ({
96
146
  {entries.map((entry) => {
97
147
  // An unknown platform still draws something recognisable rather than
98
148
  // an empty box; the picker and this table are separate lists.
99
- const glyph = SOCIAL_GLYPHS[entry.platform] || SOCIAL_GLYPHS.globe;
149
+ const glyph = resolveSocialGlyph(entry.platform) || SOCIAL_GLYPHS.globe;
100
150
 
101
- const icon = (
151
+ // A brand's own icon is an <img>, never injected markup: SVG loaded
152
+ // as an image runs no script (see utils/svg-image-source.ts). It
153
+ // cannot follow `currentColor`, which is the price of that.
154
+ const icon = entry.iconSrc ? (
155
+ // eslint-disable-next-line @next/next/no-img-element
156
+ <img
157
+ src={entry.iconSrc}
158
+ width={entry.size}
159
+ height={entry.size}
160
+ alt=""
161
+ aria-hidden="true"
162
+ data-social-icon-image=""
163
+ {...partAttrs('icon')}
164
+ />
165
+ ) : (
102
166
  <IconGlyph
103
167
  glyph={glyph}
104
168
  size={entry.size}
@@ -7,7 +7,11 @@ 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';
10
+ // Rich-text element defaults from the theme settings.
11
+ import {
12
+ THEME_RICH_TEXT_ATTRIBUTE,
13
+ hasThemeTextDefaults
14
+ } from '../utils/theme-settings-css';
11
15
 
12
16
  const TextBlock = ({
13
17
  block,
@@ -15,7 +19,7 @@ const TextBlock = ({
15
19
  }: BlockRendererProps) => {
16
20
  const { locale } = useLocalization();
17
21
  const localize = useLocalizeHref();
18
- const { defaultLocale } = useValueLocale();
22
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
19
23
  const themeSettings = useThemeSettingsContext();
20
24
 
21
25
  const getLocalizedContent = (): string => {
@@ -31,10 +35,7 @@ const TextBlock = ({
31
35
 
32
36
  if (typeof value === 'object' && value !== null) {
33
37
  return (
34
- value[locale] ||
35
- (defaultLocale && value[defaultLocale]) ||
36
- Object.values(value)[0] ||
37
- ''
38
+ value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
38
39
  );
39
40
  }
40
41
 
@@ -66,9 +67,17 @@ const TextBlock = ({
66
67
  );
67
68
  }, [block.styles, block.rowStyleKeys, themeSettings, currentBreakpoint]);
68
69
 
70
+ // Marks this root as the rich text the theme's element defaults
71
+ // (link/heading/text colours, managed typography) may style. Only while the
72
+ // theme sets one of those keys, so an untouched site's markup is unchanged.
73
+ const richTextAttrs = hasThemeTextDefaults(themeSettings)
74
+ ? { [THEME_RICH_TEXT_ATTRIBUTE]: '' }
75
+ : null;
76
+
69
77
  return (
70
78
  <div
71
79
  {...partAttrs('root')}
80
+ {...richTextAttrs}
72
81
  style={rootStyles}
73
82
  dangerouslySetInnerHTML={{ __html: html }}
74
83
  />