@akinon/pz-theme 2.0.96-rc.0 → 2.0.96
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 +7 -33
- package/package.json +4 -4
- package/src/__tests__/registry-parity.test.ts +11 -11
- package/src/blocks/__tests__/carousel-item-block.test.tsx +346 -0
- package/src/blocks/__tests__/container-block.test.tsx +149 -0
- package/src/blocks/__tests__/hover-showcase-item-block.test.tsx +315 -0
- package/src/blocks/__tests__/icon-button-block.test.tsx +231 -0
- package/src/blocks/__tests__/icon-group-block.test.tsx +186 -0
- package/src/blocks/__tests__/icon-source.test.tsx +226 -0
- package/src/blocks/__tests__/responsive-array.test.ts +77 -0
- package/src/blocks/__tests__/social-glyphs.test.tsx +108 -0
- package/src/blocks/__tests__/social-icons-list-block.test.tsx +243 -0
- package/src/blocks/__tests__/value-props-list-block.test.tsx +231 -0
- package/src/blocks/block-renderer-registry.tsx +21 -0
- package/src/blocks/carousel-item-block.tsx +168 -0
- package/src/blocks/container-block.tsx +80 -0
- package/src/blocks/hover-showcase-item-block.tsx +232 -0
- package/src/blocks/icon-block.tsx +82 -121
- package/src/blocks/icon-button-block.tsx +180 -0
- package/src/blocks/icon-group-block.tsx +119 -0
- package/src/blocks/icon-group-colored-block.tsx +47 -0
- package/src/blocks/icon-source.tsx +311 -0
- package/src/blocks/responsive-array.ts +54 -0
- package/src/blocks/social-glyphs.tsx +131 -0
- package/src/blocks/social-icons-list-block.tsx +159 -0
- package/src/blocks/social-links-block.tsx +1 -125
- package/src/blocks/value-props-list-block.tsx +152 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
5
|
+
import { DesignerPlaceholder } from './product-block-utils';
|
|
6
|
+
import { renderComposedChildren } from './render-composed-children';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A plain band a designer fills with other blocks: the header's top row, a
|
|
10
|
+
* free-shipping strip above the basket, the card a campaign sits in.
|
|
11
|
+
*
|
|
12
|
+
* Everything a container LOOKS like is authored as style — the descriptor
|
|
13
|
+
* offers background, the four paddings, border width/colour and radius, and
|
|
14
|
+
* the advanced controls reach it like any other block. Everything a container
|
|
15
|
+
* CONTAINS is authored as child blocks. That leaves this component with one
|
|
16
|
+
* job, rendering those children, and nothing to read: the descriptor declares
|
|
17
|
+
* no properties and no parts, and every style key it declares is real CSS the
|
|
18
|
+
* generated sheet already applies, so `block.styles` is never touched here.
|
|
19
|
+
*
|
|
20
|
+
* Properties: none.
|
|
21
|
+
* Parts: none.
|
|
22
|
+
*/
|
|
23
|
+
const ContainerBlock = ({
|
|
24
|
+
block,
|
|
25
|
+
placeholderId,
|
|
26
|
+
sectionId,
|
|
27
|
+
isDesigner = false,
|
|
28
|
+
selectedBlockId,
|
|
29
|
+
currentBreakpoint = 'desktop'
|
|
30
|
+
}: BlockRendererProps) => {
|
|
31
|
+
/*
|
|
32
|
+
* Rendered with NO element of its own — the children go straight into the
|
|
33
|
+
* block wrapper.
|
|
34
|
+
*
|
|
35
|
+
* The box the merchant paints already exists. `collectBlockCSS` routes a
|
|
36
|
+
* block's root styles to `[data-block-id]`, which is the WithDesignerFeatures
|
|
37
|
+
* div around this component, present on the live storefront exactly as it is
|
|
38
|
+
* on the canvas and always a real `position: relative` box. So the
|
|
39
|
+
* background, paddings, border and radius have something to paint on before
|
|
40
|
+
* this component renders anything at all.
|
|
41
|
+
*
|
|
42
|
+
* A box drawn here would be a SECOND box inside the painted one, and the
|
|
43
|
+
* damage is not cosmetic: an authored `display: flex` / `grid` — with its
|
|
44
|
+
* `gap` and `grid-template-columns` — also lands on the wrapper, so all the
|
|
45
|
+
* children would sit inside one single flex item and the layout controls
|
|
46
|
+
* would look inert.
|
|
47
|
+
*
|
|
48
|
+
* `group` avoids that with `display: contents` because it still needs an
|
|
49
|
+
* element: its `tag` property can make it an `<a>` or a `<form>`. A container
|
|
50
|
+
* has no properties at all, so there is nothing for an element to carry,
|
|
51
|
+
* while the element itself would still be counted by `:nth-child()` and by
|
|
52
|
+
* any `[data-block-id] > *` rule written against the wrapper — box or no box.
|
|
53
|
+
* Adding none is the honest version of the same trick.
|
|
54
|
+
*/
|
|
55
|
+
const children = renderComposedChildren({
|
|
56
|
+
blocks: block.blocks,
|
|
57
|
+
placeholderId,
|
|
58
|
+
sectionId,
|
|
59
|
+
isDesigner,
|
|
60
|
+
selectedBlockId,
|
|
61
|
+
currentBreakpoint
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Empty is a legitimate published state — a container with only a background
|
|
65
|
+
// and padding is a band or a spacer, and the wrapper paints it whatever this
|
|
66
|
+
// returns. On the canvas that same band is invisible until a padding is set,
|
|
67
|
+
// so adding one has to say what it is waiting for.
|
|
68
|
+
if (children.length === 0) {
|
|
69
|
+
return isDesigner ? (
|
|
70
|
+
<DesignerPlaceholder
|
|
71
|
+
label="Container (drop blocks here)"
|
|
72
|
+
attribute="data-container-placeholder"
|
|
73
|
+
/>
|
|
74
|
+
) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return <>{children}</>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default ContainerBlock;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import { DesignerPlaceholder } from './product-block-utils';
|
|
8
|
+
import { renderComposedChildren } from './render-composed-children';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One authored string, read through both shapes the editor can store it in.
|
|
12
|
+
*
|
|
13
|
+
* A `localetext` control writes a LOCALE map (`{ 'en-us': …, 'tr-tr': … }`)
|
|
14
|
+
* while `getDefaultValue` seeds the same key as a BREAKPOINT bucket
|
|
15
|
+
* (`{ desktop: …, mobile: … }`) — so a hover showcase item that was never
|
|
16
|
+
* edited and one that was carry the same key in two different shapes. Locale
|
|
17
|
+
* wins when the map has one, otherwise it resolves as an ordinary responsive
|
|
18
|
+
* value.
|
|
19
|
+
*
|
|
20
|
+
* Exported so `hover-showcase-section.tsx` can be repointed at it: the section
|
|
21
|
+
* carries a byte-identical private copy today (:53), and a third copy of a
|
|
22
|
+
* reader is a third place for the two shapes to be resolved differently.
|
|
23
|
+
*/
|
|
24
|
+
export const resolveLocalizedBlockText = (
|
|
25
|
+
value: unknown,
|
|
26
|
+
locale: string,
|
|
27
|
+
defaultLocale: string,
|
|
28
|
+
currentBreakpoint: string,
|
|
29
|
+
fallback = ''
|
|
30
|
+
): string => {
|
|
31
|
+
if (value == null) return fallback;
|
|
32
|
+
|
|
33
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
34
|
+
const localized = value as Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
if (localized[locale] != null) return String(localized[locale]);
|
|
37
|
+
if (localized[defaultLocale] != null)
|
|
38
|
+
return String(localized[defaultLocale]);
|
|
39
|
+
|
|
40
|
+
const responsive = getResponsiveValue(value, currentBreakpoint);
|
|
41
|
+
if (responsive != null && typeof responsive !== 'object') {
|
|
42
|
+
return String(responsive);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const firstValue = Object.values(localized).find((item) => item != null);
|
|
46
|
+
if (firstValue != null) return String(firstValue);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const responsive = getResponsiveValue(value, currentBreakpoint, fallback);
|
|
50
|
+
return String(responsive ?? fallback);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One entry of a hover showcase: a title, an optional subtitle, and whatever
|
|
55
|
+
* media the author dropped inside it.
|
|
56
|
+
*
|
|
57
|
+
* `hover-showcase-section.tsx` draws these itself, by type, never through
|
|
58
|
+
* ThemeBlock (:122). This renderer exists so the same stored block also draws
|
|
59
|
+
* when it sits anywhere else, and it draws the item's WHOLE content stacked —
|
|
60
|
+
* which is what the section's mobile arm shows. The desktop split pane and the
|
|
61
|
+
* cross-fade between entries are the SECTION's, not the block's: both are
|
|
62
|
+
* coordination across siblings (which entry is active), and a single block has
|
|
63
|
+
* no siblings to coordinate with.
|
|
64
|
+
*
|
|
65
|
+
* Parts: title, link, subtitle.
|
|
66
|
+
*/
|
|
67
|
+
const HoverShowcaseItemBlock = ({
|
|
68
|
+
block,
|
|
69
|
+
placeholderId,
|
|
70
|
+
sectionId,
|
|
71
|
+
selectedBlockId = null,
|
|
72
|
+
currentBreakpoint = 'desktop',
|
|
73
|
+
isDesigner = false
|
|
74
|
+
}: BlockRendererProps) => {
|
|
75
|
+
const { locale } = useLocalization();
|
|
76
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Properties first, styles second — for one key, in two places.
|
|
80
|
+
*
|
|
81
|
+
* The descriptor declares title/subtitle/link under `properties` but without
|
|
82
|
+
* `target: 'property'`, and the panel defaults an untargeted field to
|
|
83
|
+
* `'style'` (generic-properties-panel.tsx:471). So `getDefaultValue` SEEDS
|
|
84
|
+
* these into `block.properties` while every edit the merchant makes LANDS in
|
|
85
|
+
* `block.styles`. Both locations therefore hold real authored values today.
|
|
86
|
+
*
|
|
87
|
+
* Properties still wins, and the asymmetry is deliberate. Preferring styles
|
|
88
|
+
* would make today's edits visible — and would then let a value typed BEFORE
|
|
89
|
+
* the descriptor is fixed shadow every edit made after it, permanently, with
|
|
90
|
+
* no control left that could clear it. Preferring properties costs only what
|
|
91
|
+
* the section already costs (an edit that landed in styles stays ignored
|
|
92
|
+
* while a seed sits in properties) and self-heals the moment the panel starts
|
|
93
|
+
* writing to the right place.
|
|
94
|
+
*
|
|
95
|
+
* The seeded value is also why this cannot be a `??` chain: the seed writes
|
|
96
|
+
* `link: ''`, and an empty string is not nullish, so a nullish fallback would
|
|
97
|
+
* let the untouched seed shadow the URL the merchant actually typed. Emptiness
|
|
98
|
+
* is what makes a location absent here, not nullishness.
|
|
99
|
+
*/
|
|
100
|
+
const authored = (key: string): string => {
|
|
101
|
+
const fromProperties = resolveLocalizedBlockText(
|
|
102
|
+
block.properties?.[key],
|
|
103
|
+
locale,
|
|
104
|
+
defaultLocale,
|
|
105
|
+
currentBreakpoint
|
|
106
|
+
);
|
|
107
|
+
if (fromProperties) return fromProperties;
|
|
108
|
+
|
|
109
|
+
return resolveLocalizedBlockText(
|
|
110
|
+
block.styles?.[key],
|
|
111
|
+
locale,
|
|
112
|
+
defaultLocale,
|
|
113
|
+
currentBreakpoint
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const title = authored('title');
|
|
118
|
+
const subtitle = authored('subtitle');
|
|
119
|
+
const link = authored('link');
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The two style keys this renderer is allowed to read, because they are the
|
|
123
|
+
* only two it CAN read: `title-font-size` and `subtitle-font-size` are not
|
|
124
|
+
* CSS properties, so the generated sheet emits them verbatim as declarations
|
|
125
|
+
* the browser drops. Nothing else from `block.styles` is touched — every real
|
|
126
|
+
* property, `padding-top`/`padding-bottom` included, already lands on the
|
|
127
|
+
* `[data-block-id]` wrapper as a rule.
|
|
128
|
+
*
|
|
129
|
+
* Returns undefined rather than a default size on purpose. The panel also
|
|
130
|
+
* offers the advanced typography family, so a merchant can set a real
|
|
131
|
+
* `font-size` on this block; a default inlined here would outrank that rule
|
|
132
|
+
* (and its media queries) for everyone who never touched these two controls.
|
|
133
|
+
*/
|
|
134
|
+
const fontSizeOverride = (key: string): string | undefined => {
|
|
135
|
+
const raw = getResponsiveValue(block.styles?.[key], currentBreakpoint, '');
|
|
136
|
+
const text = String(raw ?? '').trim();
|
|
137
|
+
|
|
138
|
+
return text || undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const titleFontSize = fontSizeOverride('title-font-size');
|
|
142
|
+
const subtitleFontSize = fontSizeOverride('subtitle-font-size');
|
|
143
|
+
|
|
144
|
+
const children = renderComposedChildren({
|
|
145
|
+
blocks: block.blocks,
|
|
146
|
+
placeholderId,
|
|
147
|
+
sectionId,
|
|
148
|
+
isDesigner,
|
|
149
|
+
selectedBlockId,
|
|
150
|
+
currentBreakpoint
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* No invented title. The section falls back to `Item ${index + 1}`, which it
|
|
155
|
+
* can only do because it is looping — a standalone block has no index, and
|
|
156
|
+
* the alternatives are worse than nothing: `block.label` is the layer-tree
|
|
157
|
+
* name ("Hover Showcase Item") and would ship that to a shopper as a heading.
|
|
158
|
+
* So an item with no authored text and nothing inside draws the labelled
|
|
159
|
+
* designer band and stays invisible on the storefront.
|
|
160
|
+
*/
|
|
161
|
+
if (!title && !subtitle && children.length === 0) {
|
|
162
|
+
return isDesigner ? (
|
|
163
|
+
<DesignerPlaceholder
|
|
164
|
+
label="Hover showcase item (set a title, or add a block inside)"
|
|
165
|
+
attribute="data-hover-showcase-item"
|
|
166
|
+
/>
|
|
167
|
+
) : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A click in the designer selects the block; following the href would
|
|
171
|
+
// navigate the preview iframe away from the page being edited.
|
|
172
|
+
const handleLinkClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
173
|
+
if (isDesigner) event.preventDefault();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const heading = title ? (
|
|
177
|
+
<h3
|
|
178
|
+
data-hover-showcase-title=""
|
|
179
|
+
{...partAttrs('title')}
|
|
180
|
+
style={{
|
|
181
|
+
margin: 0,
|
|
182
|
+
...(titleFontSize ? { fontSize: titleFontSize } : null)
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{title}
|
|
186
|
+
</h3>
|
|
187
|
+
) : null;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A fragment, not a box. Every layout property the merchant authors on this
|
|
191
|
+
* item (`display`, `gap`, `grid-template-columns`) is emitted against the
|
|
192
|
+
* `[data-block-id]` wrapper around this component — a box of my own would
|
|
193
|
+
* make the title, the subtitle and the media a single flex item inside it,
|
|
194
|
+
* and the authored layout would have exactly one thing to lay out.
|
|
195
|
+
*/
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
{heading && link ? (
|
|
199
|
+
// Only the title is linked, matching the section. With no title there
|
|
200
|
+
// is nothing to hang the anchor on, so an authored link is simply not
|
|
201
|
+
// reachable — the same as in the section today.
|
|
202
|
+
<a
|
|
203
|
+
href={link}
|
|
204
|
+
data-hover-showcase-link=""
|
|
205
|
+
{...partAttrs('link')}
|
|
206
|
+
onClick={handleLinkClick}
|
|
207
|
+
>
|
|
208
|
+
{heading}
|
|
209
|
+
</a>
|
|
210
|
+
) : (
|
|
211
|
+
heading
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{subtitle ? (
|
|
215
|
+
<p
|
|
216
|
+
data-hover-showcase-subtitle=""
|
|
217
|
+
{...partAttrs('subtitle')}
|
|
218
|
+
style={{
|
|
219
|
+
margin: 0,
|
|
220
|
+
...(subtitleFontSize ? { fontSize: subtitleFontSize } : null)
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{subtitle}
|
|
224
|
+
</p>
|
|
225
|
+
) : null}
|
|
226
|
+
|
|
227
|
+
{children}
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export default HoverShowcaseItemBlock;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import React from 'react';
|
|
2
4
|
import { getResponsiveValue } from '../utils';
|
|
3
5
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
4
6
|
import { Image } from '@akinon/next/components/image';
|
|
7
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
8
|
+
import { IconGlyph, resolveIconSource } from './icon-source';
|
|
5
9
|
|
|
6
10
|
const isValidUrlValue = (value: string): boolean => {
|
|
7
11
|
if (!value) return false;
|
|
@@ -19,6 +23,7 @@ const IconBlock = ({
|
|
|
19
23
|
block,
|
|
20
24
|
currentBreakpoint = 'desktop'
|
|
21
25
|
}: BlockRendererProps) => {
|
|
26
|
+
const themeSettings = useThemeSettingsContext();
|
|
22
27
|
const iconValue = getResponsiveValue(block.value, currentBreakpoint, '');
|
|
23
28
|
// An uploaded brand mark announced itself to screen readers as "Icon"
|
|
24
29
|
// because the alt the panel offers was never read.
|
|
@@ -44,13 +49,6 @@ const IconBlock = ({
|
|
|
44
49
|
justifyContent: 'center'
|
|
45
50
|
} as React.CSSProperties;
|
|
46
51
|
|
|
47
|
-
const isInlineSvg = (value: string): boolean => {
|
|
48
|
-
return (
|
|
49
|
-
typeof value === 'string' &&
|
|
50
|
-
(value.includes('<svg') || value.includes('<?xml'))
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
52
|
const modifySVGColor = (svgContent: string, color: string) => {
|
|
55
53
|
if (color === 'currentColor' || !color) return svgContent;
|
|
56
54
|
|
|
@@ -90,120 +88,21 @@ const IconBlock = ({
|
|
|
90
88
|
}
|
|
91
89
|
};
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Try to find a string value in the object (e.g. localized)
|
|
106
|
-
const values = Object.values(iconValue);
|
|
107
|
-
const stringVal = values.find((v) => typeof v === 'string');
|
|
108
|
-
if (stringVal) {
|
|
109
|
-
svgSrc = (stringVal as string).trim();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const isBase64 =
|
|
115
|
-
typeof svgSrc === 'string' && svgSrc.startsWith('data:image');
|
|
116
|
-
const isSvg = isInlineSvg(svgSrc);
|
|
117
|
-
const isAbsolutePath = svgSrc.startsWith('/');
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
!isBase64 &&
|
|
121
|
-
!isSvg &&
|
|
122
|
-
svgSrc &&
|
|
123
|
-
!isAbsolutePath &&
|
|
124
|
-
!svgSrc.startsWith('http') &&
|
|
125
|
-
!svgSrc.startsWith('//')
|
|
126
|
-
) {
|
|
127
|
-
const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
|
|
128
|
-
svgSrc = cloudName
|
|
129
|
-
? `https://${cloudName}/${svgSrc.replace(/^\/+/, '')}`
|
|
130
|
-
: '';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
isBase64 &&
|
|
135
|
-
svgSrc.includes('data:image/svg+xml;base64,') &&
|
|
136
|
-
iconColor !== 'currentColor'
|
|
137
|
-
) {
|
|
138
|
-
svgSrc = processBase64SVG(svgSrc, iconColor);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (svgSrc && svgSrc.trim() !== '') {
|
|
142
|
-
// Check if it's inline SVG content
|
|
143
|
-
if (isInlineSvg(svgSrc)) {
|
|
144
|
-
// Apply color modification to inline SVG
|
|
145
|
-
const coloredSvg =
|
|
146
|
-
iconColor !== 'currentColor'
|
|
147
|
-
? modifySVGColor(svgSrc, iconColor)
|
|
148
|
-
: svgSrc;
|
|
149
|
-
|
|
150
|
-
iconContent = (
|
|
151
|
-
<div
|
|
152
|
-
style={{
|
|
153
|
-
...iconStyles,
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
-
['--svg-size' as any]: iconSize + 'px'
|
|
156
|
-
}}
|
|
157
|
-
className="[&>svg]:w-[var(--svg-size)] [&>svg]:h-[var(--svg-size)]"
|
|
158
|
-
dangerouslySetInnerHTML={{ __html: coloredSvg }}
|
|
159
|
-
/>
|
|
160
|
-
);
|
|
161
|
-
} else {
|
|
162
|
-
// It's a URL or base64, use img tag
|
|
163
|
-
if (isValidUrlValue(svgSrc)) {
|
|
164
|
-
iconContent = (
|
|
165
|
-
<div style={iconStyles}>
|
|
166
|
-
<Image
|
|
167
|
-
src={svgSrc}
|
|
168
|
-
width={Number(iconSize)}
|
|
169
|
-
height={Number(iconSize)}
|
|
170
|
-
alt={iconAlt}
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
iconContent = (
|
|
178
|
-
<div
|
|
179
|
-
style={{
|
|
180
|
-
...iconStyles,
|
|
181
|
-
border: '1px dashed #ccc',
|
|
182
|
-
borderRadius: '4px',
|
|
183
|
-
backgroundColor: '#f9f9f9'
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
186
|
-
<span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
|
|
187
|
-
</div>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
91
|
+
const noIcon = (
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
...iconStyles,
|
|
95
|
+
border: '1px dashed #ccc',
|
|
96
|
+
borderRadius: '4px',
|
|
97
|
+
backgroundColor: '#f9f9f9'
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
190
103
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<div
|
|
194
|
-
style={{
|
|
195
|
-
...iconStyles,
|
|
196
|
-
border: '1px dashed #ccc',
|
|
197
|
-
borderRadius: '4px',
|
|
198
|
-
backgroundColor: '#f9f9f9'
|
|
199
|
-
}}
|
|
200
|
-
>
|
|
201
|
-
<span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
iconContent = (
|
|
104
|
+
if (!iconValue) {
|
|
105
|
+
return (
|
|
207
106
|
<div
|
|
208
107
|
style={{
|
|
209
108
|
...iconStyles,
|
|
@@ -234,7 +133,69 @@ const IconBlock = ({
|
|
|
234
133
|
);
|
|
235
134
|
}
|
|
236
135
|
|
|
237
|
-
|
|
136
|
+
// What the stored value actually is — markup, a URL, a catalogue key, or
|
|
137
|
+
// nothing. The CDN prefix used to be applied to every bare value, which is
|
|
138
|
+
// why a picked catalogue key such as `basket` shipped as a broken image.
|
|
139
|
+
const source = resolveIconSource(iconValue, themeSettings);
|
|
140
|
+
|
|
141
|
+
if (source.kind === 'svg') {
|
|
142
|
+
const coloredSvg =
|
|
143
|
+
iconColor !== 'currentColor'
|
|
144
|
+
? modifySVGColor(source.markup, iconColor)
|
|
145
|
+
: source.markup;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
style={{
|
|
150
|
+
...iconStyles,
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
['--svg-size' as any]: iconSize + 'px'
|
|
153
|
+
}}
|
|
154
|
+
className="[&>svg]:w-[var(--svg-size)] [&>svg]:h-[var(--svg-size)]"
|
|
155
|
+
dangerouslySetInnerHTML={{ __html: coloredSvg }}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (source.kind === 'glyph') {
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
style={{
|
|
164
|
+
...iconStyles,
|
|
165
|
+
// The glyphs are drawn with `currentColor`, so the colour the panel
|
|
166
|
+
// stores is inherited rather than rewritten into the markup.
|
|
167
|
+
...(iconColor && iconColor !== 'currentColor'
|
|
168
|
+
? { color: iconColor }
|
|
169
|
+
: null)
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<IconGlyph glyph={source.glyph} size={Number(iconSize) || 24} />
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (source.kind === 'url') {
|
|
178
|
+
const src =
|
|
179
|
+
source.url.includes('data:image/svg+xml;base64,') &&
|
|
180
|
+
iconColor !== 'currentColor'
|
|
181
|
+
? processBase64SVG(source.url, iconColor)
|
|
182
|
+
: source.url;
|
|
183
|
+
|
|
184
|
+
if (isValidUrlValue(src)) {
|
|
185
|
+
return (
|
|
186
|
+
<div style={iconStyles}>
|
|
187
|
+
<Image
|
|
188
|
+
src={src}
|
|
189
|
+
width={Number(iconSize)}
|
|
190
|
+
height={Number(iconSize)}
|
|
191
|
+
alt={iconAlt}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return noIcon;
|
|
238
199
|
};
|
|
239
200
|
|
|
240
201
|
export default IconBlock;
|