@avocadostudio-ai/blocks 0.3.3 → 0.4.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/dist/blocks/_base.css +12 -0
- package/dist/blocks/_shared.d.ts +9 -0
- package/dist/blocks/_shared.js +20 -2
- package/dist/blocks/card/renderer.js +7 -1
- package/dist/blocks/card/styles.css +11 -0
- package/dist/blocks/card-grid/renderer.js +5 -2
- package/dist/blocks/card-grid/styles.css +15 -0
- package/dist/blocks/carousel/renderer.js +4 -1
- package/dist/blocks/carousel/styles.css +7 -0
- package/dist/blocks/feature-grid/renderer.js +1 -1
- package/dist/blocks/gallery/renderer.js +9 -1
- package/dist/blocks/gallery/styles.css +7 -0
- package/dist/blocks/quote/renderer.js +4 -1
- package/dist/blocks/quote/styles.css +9 -0
- package/dist/blocks/stats/renderer.js +1 -1
- package/dist/blocks/testimonials/renderer.js +11 -2
- package/dist/blocks/testimonials/styles.css +9 -0
- package/dist/blocks/two-column/renderer.js +53 -9
- package/dist/blocks/two-column/styles.css +36 -0
- package/dist/blocks/video/renderer.js +16 -5
- package/package.json +3 -3
package/dist/blocks/_base.css
CHANGED
|
@@ -106,3 +106,15 @@ section {
|
|
|
106
106
|
padding: 20px 16px;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/* The wrapper `renderBlockIcon` puts around an icon that is an image URL rather
|
|
111
|
+
than a glyph. It exists so the editable marker has an element that can hold
|
|
112
|
+
the image Change button — an `<img>` cannot contain anything. Sized entirely
|
|
113
|
+
by the icon inside it, so every block's own icon rules still decide the
|
|
114
|
+
layout. */
|
|
115
|
+
.block-icon-wrap {
|
|
116
|
+
display: block;
|
|
117
|
+
position: relative;
|
|
118
|
+
width: fit-content;
|
|
119
|
+
line-height: 0;
|
|
120
|
+
}
|
package/dist/blocks/_shared.d.ts
CHANGED
|
@@ -8,10 +8,19 @@ export { normalizeRichTextBody };
|
|
|
8
8
|
* image URL. Anything else (a bare icon-library name, descriptive text, an
|
|
9
9
|
* invented/relative URL) renders nothing instead of leaking raw text or a
|
|
10
10
|
* broken <img> into the page. Returns null when there's nothing safe to show.
|
|
11
|
+
*
|
|
12
|
+
* `editablePath` marks the icon for the overlay. Both branches end up on a
|
|
13
|
+
* `<span>`, including the image one — a marker on the `<img>` would be found by
|
|
14
|
+
* the editor and could never hold the Change button, which is appended *into*
|
|
15
|
+
* the marked element. The span also carries `data-editable-kind`, because the
|
|
16
|
+
* one thing the overlay cannot work out on its own is that a field named `icon`
|
|
17
|
+
* sometimes holds a picture: its fallback guesses from the prop name, and
|
|
18
|
+
* nothing about "icon" says image.
|
|
11
19
|
*/
|
|
12
20
|
export declare function renderBlockIcon(rawValue: unknown, opts: {
|
|
13
21
|
className: string;
|
|
14
22
|
size: number;
|
|
23
|
+
editablePath?: string;
|
|
15
24
|
}): JSX.Element | null;
|
|
16
25
|
export declare function decodeSoftHyphenEntities(input: string): string;
|
|
17
26
|
export declare function normalizeSoftHyphenEntities<T>(value: T, seen?: WeakSet<object>): T;
|
package/dist/blocks/_shared.js
CHANGED
|
@@ -21,16 +21,34 @@ function isIconGlyph(value) {
|
|
|
21
21
|
* image URL. Anything else (a bare icon-library name, descriptive text, an
|
|
22
22
|
* invented/relative URL) renders nothing instead of leaking raw text or a
|
|
23
23
|
* broken <img> into the page. Returns null when there's nothing safe to show.
|
|
24
|
+
*
|
|
25
|
+
* `editablePath` marks the icon for the overlay. Both branches end up on a
|
|
26
|
+
* `<span>`, including the image one — a marker on the `<img>` would be found by
|
|
27
|
+
* the editor and could never hold the Change button, which is appended *into*
|
|
28
|
+
* the marked element. The span also carries `data-editable-kind`, because the
|
|
29
|
+
* one thing the overlay cannot work out on its own is that a field named `icon`
|
|
30
|
+
* sometimes holds a picture: its fallback guesses from the prop name, and
|
|
31
|
+
* nothing about "icon" says image.
|
|
24
32
|
*/
|
|
25
33
|
export function renderBlockIcon(rawValue, opts) {
|
|
26
34
|
const value = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
27
35
|
if (value.length === 0)
|
|
28
36
|
return null;
|
|
37
|
+
const marker = opts.editablePath
|
|
38
|
+
? {
|
|
39
|
+
"data-editable-target": opts.editablePath,
|
|
40
|
+
"data-editable-target-label": opts.editablePath,
|
|
41
|
+
"data-editable-label": opts.editablePath,
|
|
42
|
+
// Selectable and, when it holds a URL, changeable — but never typed
|
|
43
|
+
// into. See `isInlineEditable` in preview-adapter, and `f.icon`.
|
|
44
|
+
"data-editable-inline": "false"
|
|
45
|
+
}
|
|
46
|
+
: {};
|
|
29
47
|
if (isIconUrl(value)) {
|
|
30
|
-
return (_jsx("img", { className: opts.className, src: value, alt: "", width: opts.size, height: opts.size, loading: "lazy" }));
|
|
48
|
+
return (_jsx("span", { className: `block-icon-wrap ${opts.className}-wrap`, ...marker, "data-editable-kind": "image", children: _jsx("img", { className: opts.className, src: value, alt: "", width: opts.size, height: opts.size, loading: "lazy" }) }));
|
|
31
49
|
}
|
|
32
50
|
if (isIconGlyph(value)) {
|
|
33
|
-
return (_jsx("span", { className: opts.className, "aria-hidden": "true", children: value }));
|
|
51
|
+
return (_jsx("span", { className: opts.className, "aria-hidden": "true", ...marker, children: value }));
|
|
34
52
|
}
|
|
35
53
|
return null;
|
|
36
54
|
}
|
|
@@ -7,5 +7,11 @@ export function Card(props) {
|
|
|
7
7
|
const variant = String(props.variant ?? "default");
|
|
8
8
|
const isFullBleed = variant === "full-bleed" && imageUrl.length > 0;
|
|
9
9
|
const HeadingTag = resolveHeadingTag("Card", props);
|
|
10
|
-
return (_jsx("section", { children: _jsx("div", { className: "section__inner", children: _jsxs("article", { className: `card${isFullBleed ? " card--full-bleed" : ""}`, children: [isFullBleed ? (
|
|
10
|
+
return (_jsx("section", { children: _jsx("div", { className: "section__inner", children: _jsxs("article", { className: `card${isFullBleed ? " card--full-bleed" : ""}`, children: [isFullBleed ? (
|
|
11
|
+
// The marker belongs on a wrapper here too. It sat on the
|
|
12
|
+
// `<BlockImage>` — an `<img>` — where the editor finds the field and
|
|
13
|
+
// the Change button, which is appended into the marked element, can
|
|
14
|
+
// never mount. The default variant below already got this right,
|
|
15
|
+
// which is why only the full-bleed card was missing its button.
|
|
16
|
+
_jsx("div", { className: "card__bg-image-wrap", "data-editable-target": "imageUrl", "data-editable-target-label": "image", "data-editable-label": "image", children: _jsx(BlockImage, { src: imageUrl, alt: imageAlt.length > 0 ? imageAlt : "Card image", className: "card__bg-image", width: 768, height: 512, sizes: "(max-width: 768px) 100vw, 50vw", loading: "lazy" }) })) : (imageUrl.length > 0 && (_jsx("div", { className: "card__image-wrap", "data-editable-target": "imageUrl", "data-editable-target-label": "image", children: _jsx(BlockImage, { src: imageUrl, alt: imageAlt.length > 0 ? imageAlt : "Card image", className: "card__image", width: 768, height: 512, sizes: "(max-width: 768px) 100vw, 50vw", loading: "lazy" }) }))), _jsxs("div", { className: isFullBleed ? "card__overlay-content" : undefined, children: [_jsx(HeadingTag, { "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: String(props.title ?? "") }), _jsx("p", { "data-editable-target": "description", "data-editable-target-label": "description", "data-editable-label": "description", children: String(props.description ?? "") }), _jsx(PrimaryButton, { href: String(props.ctaHref ?? "#"), newTab: props.ctaNewTab === true, "data-editable-target": "ctaText", "data-editable-target-label": "ctaText", "data-editable-label": "ctaText", children: String(props.ctaText ?? "") })] })] }) }) }));
|
|
11
17
|
}
|
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
border: none;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/* The element the full-bleed image marker sits on, and therefore the element
|
|
57
|
+
the editor's Change button mounts into. Deliberately no `z-index`: with
|
|
58
|
+
`z-index: auto` it does not open a stacking context, so the button's own
|
|
59
|
+
z-index competes with the gradient (`::after`, 1) and the overlay content (2)
|
|
60
|
+
in the card's context and wins. Give this rule a z-index and the button is
|
|
61
|
+
trapped underneath the gradient — present, hoverable, and invisible. */
|
|
62
|
+
.card--full-bleed .card__bg-image-wrap {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
.card--full-bleed .card__bg-image {
|
|
57
68
|
position: absolute;
|
|
58
69
|
inset: 0;
|
|
@@ -5,13 +5,16 @@ export function CardGrid(props) {
|
|
|
5
5
|
const cards = Array.isArray(props.cards) ? props.cards : [];
|
|
6
6
|
const columns = String(props.columns ?? "3");
|
|
7
7
|
const cardVariant = String(props.cardVariant ?? "default");
|
|
8
|
+
const subtitle = typeof props.subtitle === "string" ? props.subtitle.trim() : "";
|
|
8
9
|
const HeadingTag = resolveHeadingTag("CardGrid", props);
|
|
9
10
|
const ItemHeadingTag = resolveItemHeadingTag("CardGrid", props);
|
|
10
|
-
return (_jsx("section", { className: "card-grid-section", children: _jsxs("div", { className: "section__inner", children: [_jsx(HeadingTag, { "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: String(props.title ?? "") }), _jsx("div", { className: `card-grid${columns !== "3" ? ` card-grid--${columns}-col` : ""}`, children: cards.map((item, idx) => {
|
|
11
|
+
return (_jsx("section", { className: "card-grid-section", children: _jsxs("div", { className: "section__inner", children: [_jsx(HeadingTag, { "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: String(props.title ?? "") }), subtitle.length > 0 && (_jsx("p", { className: "card-grid-section__subtitle", "data-editable-target": "subtitle", "data-editable-target-label": "subtitle", "data-editable-label": "subtitle", children: subtitle })), _jsx("div", { className: `card-grid${columns !== "3" ? ` card-grid--${columns}-col` : ""}`, children: cards.map((item, idx) => {
|
|
11
12
|
const row = (item ?? {});
|
|
12
13
|
const imageUrl = typeof row.imageUrl === "string" ? row.imageUrl.trim() : "";
|
|
13
14
|
const imageAlt = typeof row.imageAlt === "string" ? row.imageAlt.trim() : "";
|
|
14
15
|
const isFullBleed = cardVariant === "full-bleed" && imageUrl.length > 0;
|
|
15
|
-
return (_jsxs("article", { className: `card${isFullBleed ? " card--full-bleed" : ""}`, "data-editable-target": `cards[${idx}]`, "data-editable-target-label": `cards[${idx}]`, "data-editable-label": `cards[${idx}]`, children: [isFullBleed ? (
|
|
16
|
+
return (_jsxs("article", { className: `card${isFullBleed ? " card--full-bleed" : ""}`, "data-editable-target": `cards[${idx}]`, "data-editable-target-label": `cards[${idx}]`, "data-editable-label": `cards[${idx}]`, children: [isFullBleed ? (
|
|
17
|
+
// On a wrapper, not the `<img>` — see the note in Card.
|
|
18
|
+
_jsx("div", { className: "card__bg-image-wrap", "data-editable-target": `cards[${idx}].imageUrl`, "data-editable-target-label": `cards[${idx}].imageUrl`, "data-editable-label": `cards[${idx}].imageUrl`, children: _jsx(BlockImage, { src: imageUrl, alt: imageAlt.length > 0 ? imageAlt : "Card image", className: "card__bg-image", width: 768, height: 512, sizes: "(max-width: 768px) 100vw, 33vw", loading: "lazy" }) })) : (imageUrl.length > 0 && (_jsx("div", { className: "card__image-wrap", "data-editable-target": `cards[${idx}].imageUrl`, "data-editable-target-label": `cards[${idx}].imageUrl`, "data-editable-label": `cards[${idx}].imageUrl`, children: _jsx(BlockImage, { src: imageUrl, alt: imageAlt.length > 0 ? imageAlt : "Card image", className: "card__image", width: 768, height: 512, sizes: "(max-width: 768px) 100vw, 33vw", loading: "lazy" }) }))), _jsxs("div", { className: isFullBleed ? "card__overlay-content" : undefined, children: [_jsx(ItemHeadingTag, { "data-editable-target": `cards[${idx}].title`, "data-editable-target-label": `cards[${idx}].title`, "data-editable-label": `cards[${idx}].title`, children: String(row.title ?? "") }), _jsx("p", { "data-editable-target": `cards[${idx}].description`, "data-editable-target-label": `cards[${idx}].description`, "data-editable-label": `cards[${idx}].description`, children: String(row.description ?? "") }), _jsx(PrimaryButton, { href: String(row.ctaHref ?? "#"), newTab: row.ctaNewTab === true, "data-editable-target": `cards[${idx}].ctaText`, "data-editable-target-label": `cards[${idx}].ctaText`, "data-editable-label": `cards[${idx}].ctaText`, children: String(row.ctaText ?? "") })] })] }, idx));
|
|
16
19
|
}) })] }) }));
|
|
17
20
|
}
|
|
@@ -16,6 +16,21 @@
|
|
|
16
16
|
margin-bottom: 16px;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/* With a subtitle the heading sits tighter against it, and the subtitle carries
|
|
20
|
+
the gap to the grid — otherwise the two stack with 16px between them and read
|
|
21
|
+
as unrelated. */
|
|
22
|
+
.card-grid-section:has(.card-grid-section__subtitle) :is(h1, h2, h3, h4, h5, h6) {
|
|
23
|
+
margin-bottom: 6px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.card-grid-section__subtitle {
|
|
27
|
+
margin: 0 0 16px;
|
|
28
|
+
max-width: 62ch;
|
|
29
|
+
font-size: 1.0625rem;
|
|
30
|
+
line-height: 1.6;
|
|
31
|
+
color: var(--body-secondary);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
.card-grid .card {
|
|
20
35
|
background-color: var(--surface);
|
|
21
36
|
background-image:
|
|
@@ -14,6 +14,9 @@ export function Carousel(props) {
|
|
|
14
14
|
const description = String(item.description ?? "");
|
|
15
15
|
const ctaText = String(item.ctaText ?? "");
|
|
16
16
|
const ctaHref = String(item.ctaHref ?? "");
|
|
17
|
-
return (_jsxs("div", { className: "carousel__slide", children: [imageUrl.length > 0 && (
|
|
17
|
+
return (_jsxs("div", { className: "carousel__slide", children: [imageUrl.length > 0 && (
|
|
18
|
+
// Marked on the wrapper: the Change button is appended into
|
|
19
|
+
// the marked element, and an `<img>` holds no children.
|
|
20
|
+
_jsx("div", { className: "carousel__media", "data-editable-target": `items[${idx}].imageUrl`, "data-editable-target-label": `items[${idx}].imageUrl`, "data-editable-label": `items[${idx}].imageUrl`, children: _jsx(BlockImage, { className: "carousel__image", src: imageUrl, alt: imageAlt, width: 1200, height: 600, sizes: "(max-width: 900px) 100vw, 80vw", priority: idx === 0, loading: idx === 0 ? "eager" : "lazy" }) })), (heading.length > 0 || description.length > 0 || ctaText.length > 0) && (_jsxs("div", { className: "carousel__caption", children: [heading.length > 0 && (_jsx(ItemHeadingTag, { "data-editable-target": `items[${idx}].heading`, "data-editable-target-label": `items[${idx}].heading`, "data-editable-label": `items[${idx}].heading`, children: renderInline(heading) })), description.length > 0 && (_jsx("p", { "data-editable-target": `items[${idx}].description`, "data-editable-target-label": `items[${idx}].description`, "data-editable-label": `items[${idx}].description`, children: renderInline(description) })), ctaText.length > 0 && ctaHref.length > 0 && (_jsx(PrimaryButton, { href: ctaHref, newTab: item.ctaNewTab === true, className: "carousel__cta", "data-editable-target": `items[${idx}].ctaText`, "data-editable-target-label": `items[${idx}].ctaText`, "data-editable-label": `items[${idx}].ctaText`, children: ctaText }))] }))] }, idx));
|
|
18
21
|
}) }), items.length > 1 && (_jsxs(_Fragment, { children: [_jsx("button", { className: "carousel__btn carousel__btn--prev", "aria-label": "Previous slide", type: "button", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "15 18 9 12 15 6" }) }) }), _jsx("button", { className: "carousel__btn carousel__btn--next", "aria-label": "Next slide", type: "button", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "9 18 15 12 9 6" }) }) }), _jsx("div", { className: "carousel__dots", children: items.map((_, idx) => (_jsx("button", { className: `carousel__dot${idx === 0 ? " carousel__dot--active" : ""}`, "aria-label": `Go to slide ${idx + 1}`, type: "button" }, idx))) })] }))] }) }));
|
|
19
22
|
}
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
border: 1px solid var(--border);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/* Wrapper the image marker sits on. `position: relative` only so the Change
|
|
37
|
+
button, which the overlay absolutely positions, lands on the image. */
|
|
38
|
+
.carousel__media {
|
|
39
|
+
display: block;
|
|
40
|
+
position: relative;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
.carousel__image {
|
|
37
44
|
width: 100%;
|
|
38
45
|
aspect-ratio: 16 / 9;
|
|
@@ -7,6 +7,6 @@ export function FeatureGrid(props) {
|
|
|
7
7
|
const HeadingTag = resolveHeadingTag("FeatureGrid", props);
|
|
8
8
|
return (_jsx("section", { children: _jsxs("div", { className: "section__inner", children: [_jsx(HeadingTag, { "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: String(props.title ?? "") }), _jsx("ul", { className: `feature-grid${columns !== "3" ? ` feature-grid--${columns}-col` : ""}`, children: items.map((item, idx) => {
|
|
9
9
|
const row = (item ?? {});
|
|
10
|
-
return (_jsxs("li", { className: "feature-card", children: [renderBlockIcon(row.icon, { className: "feature-card__icon", size: 40 }), _jsx("strong", { "data-editable-target": `features[${idx}].title`, "data-editable-target-label": `features[${idx}].title`, "data-editable-label": `features[${idx}].title`, children: String(row.title ?? "") }), _jsx("p", { "data-editable-target": `features[${idx}].description`, "data-editable-target-label": `features[${idx}].description`, "data-editable-label": `features[${idx}].description`, children: String(row.description ?? "") })] }, idx));
|
|
10
|
+
return (_jsxs("li", { className: "feature-card", children: [renderBlockIcon(row.icon, { className: "feature-card__icon", size: 40, editablePath: `features[${idx}].icon` }), _jsx("strong", { "data-editable-target": `features[${idx}].title`, "data-editable-target-label": `features[${idx}].title`, "data-editable-label": `features[${idx}].title`, children: String(row.title ?? "") }), _jsx("p", { "data-editable-target": `features[${idx}].description`, "data-editable-target-label": `features[${idx}].description`, "data-editable-label": `features[${idx}].description`, children: String(row.description ?? "") })] }, idx));
|
|
11
11
|
}) })] }) }));
|
|
12
12
|
}
|
|
@@ -16,6 +16,14 @@ export function Gallery(props) {
|
|
|
16
16
|
const imageUrl = String(img.imageUrl ?? "");
|
|
17
17
|
const alt = String(img.alt ?? "");
|
|
18
18
|
const caption = String(img.caption ?? "");
|
|
19
|
-
return (_jsxs("figure", { className: "gallery__item", children: [imageUrl.length > 0 && (
|
|
19
|
+
return (_jsxs("figure", { className: "gallery__item", children: [imageUrl.length > 0 && (
|
|
20
|
+
/*
|
|
21
|
+
* The marker goes on this wrapper, not on the image. The
|
|
22
|
+
* overlay mounts its Change button by appending it into the
|
|
23
|
+
* marked element, and an `<img>` cannot contain anything — so
|
|
24
|
+
* a marker there is found by the editor, counts as
|
|
25
|
+
* instrumented, and can never produce a button.
|
|
26
|
+
*/
|
|
27
|
+
_jsx("div", { className: "gallery__media", "data-editable-target": `images[${idx}].imageUrl`, "data-editable-target-label": `images[${idx}].imageUrl`, "data-editable-label": `images[${idx}].imageUrl`, children: _jsx(BlockImage, { className: "gallery__image", src: imageUrl, alt: alt, width: 800, height: 600, sizes: "(max-width: 768px) 100vw, 33vw", loading: idx < 4 ? "eager" : "lazy" }) })), caption.length > 0 && (_jsx("figcaption", { className: "gallery__caption", "data-editable-target": `images[${idx}].caption`, "data-editable-target-label": `images[${idx}].caption`, "data-editable-label": `images[${idx}].caption`, children: renderInline(caption) }))] }, idx));
|
|
20
28
|
}) })] }) }));
|
|
21
29
|
}
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
background: var(--bg-1);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* Wrapper the image marker sits on — see the note in the renderer. Sized by the
|
|
24
|
+
image inside it, so the grid cell is unchanged. */
|
|
25
|
+
.gallery__media {
|
|
26
|
+
display: block;
|
|
27
|
+
position: relative;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
.gallery__image {
|
|
24
31
|
width: 100%;
|
|
25
32
|
aspect-ratio: 4 / 3;
|
|
@@ -5,5 +5,8 @@ export function Quote(props) {
|
|
|
5
5
|
const author = String(props.author ?? "");
|
|
6
6
|
const role = String(props.role ?? "");
|
|
7
7
|
const imageUrl = String(props.imageUrl ?? "");
|
|
8
|
-
return (_jsx("section", { className: "quote-block", children: _jsxs("div", { className: "section__inner", children: [_jsxs("blockquote", { className: "quote-block__quote", children: [_jsx("span", { className: "quote-block__mark", "aria-hidden": "true", children: "\u201C" }), _jsx("div", { className: "quote-block__text", "data-editable-target": "quote", "data-editable-target-label": "quote", "data-editable-label": "quote", children: renderRichTextContent(quote) })] }), (author.length > 0 || role.length > 0) && (_jsxs("div", { className: "quote-block__attribution", children: [imageUrl.length > 0 && (
|
|
8
|
+
return (_jsx("section", { className: "quote-block", children: _jsxs("div", { className: "section__inner", children: [_jsxs("blockquote", { className: "quote-block__quote", children: [_jsx("span", { className: "quote-block__mark", "aria-hidden": "true", children: "\u201C" }), _jsx("div", { className: "quote-block__text", "data-editable-target": "quote", "data-editable-target-label": "quote", "data-editable-label": "quote", children: renderRichTextContent(quote) })] }), (author.length > 0 || role.length > 0) && (_jsxs("div", { className: "quote-block__attribution", children: [imageUrl.length > 0 && (
|
|
9
|
+
// On the wrapper, not the <img>: the Change button is appended
|
|
10
|
+
// into the marked element, which a void element cannot hold.
|
|
11
|
+
_jsx("span", { className: "quote-block__avatar-wrap", "data-editable-target": "imageUrl", "data-editable-target-label": "imageUrl", "data-editable-label": "imageUrl", children: _jsx(BlockImage, { className: "quote-block__avatar", src: imageUrl, alt: author || "Author", width: 48, height: 48 }) })), _jsxs("div", { className: "quote-block__author-info", children: [author.length > 0 && (_jsx("span", { className: "quote-block__author", "data-editable-target": "author", "data-editable-target-label": "author", "data-editable-label": "author", children: author })), role.length > 0 && (_jsx("span", { className: "quote-block__role", "data-editable-target": "role", "data-editable-target-label": "role", "data-editable-label": "role", children: role }))] })] }))] }) }));
|
|
9
12
|
}
|
|
@@ -51,6 +51,15 @@
|
|
|
51
51
|
text-align: left;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/* Wrapper the image marker sits on — see the note in the renderer. A flex item
|
|
55
|
+
of `.quote-block__attribution`, sized entirely by the avatar inside it. */
|
|
56
|
+
.quote-block__avatar-wrap {
|
|
57
|
+
display: block;
|
|
58
|
+
position: relative;
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
line-height: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
.quote-block__avatar {
|
|
55
64
|
width: 48px;
|
|
56
65
|
height: 48px;
|
|
@@ -8,6 +8,6 @@ export function Stats(props) {
|
|
|
8
8
|
return (_jsx("section", { className: "stats-section", children: _jsxs("div", { className: "section__inner", children: [title.length > 0 && (_jsx(HeadingTag, { "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: title })), _jsx("div", { className: "stats-grid", children: items.map((item, idx) => {
|
|
9
9
|
const row = (item ?? {});
|
|
10
10
|
const description = typeof row.description === "string" ? row.description.trim() : "";
|
|
11
|
-
return (_jsxs("div", { className: "stat-item", children: [renderBlockIcon(row.icon, { className: "stat-item__icon", size: 32 }), _jsx("span", { className: "stat-item__value", "data-editable-target": `stats[${idx}].value`, "data-editable-target-label": `stats[${idx}].value`, "data-editable-label": `stats[${idx}].value`, children: String(row.value ?? "") }), _jsx("span", { className: "stat-item__label", "data-editable-target": `stats[${idx}].label`, "data-editable-target-label": `stats[${idx}].label`, "data-editable-label": `stats[${idx}].label`, children: String(row.label ?? "") }), description.length > 0 && (_jsx("span", { className: "stat-item__description", "data-editable-target": `stats[${idx}].description`, "data-editable-target-label": `stats[${idx}].description`, "data-editable-label": `stats[${idx}].description`, children: description }))] }, idx));
|
|
11
|
+
return (_jsxs("div", { className: "stat-item", children: [renderBlockIcon(row.icon, { className: "stat-item__icon", size: 32, editablePath: `stats[${idx}].icon` }), _jsx("span", { className: "stat-item__value", "data-editable-target": `stats[${idx}].value`, "data-editable-target-label": `stats[${idx}].value`, "data-editable-label": `stats[${idx}].value`, children: String(row.value ?? "") }), _jsx("span", { className: "stat-item__label", "data-editable-target": `stats[${idx}].label`, "data-editable-target-label": `stats[${idx}].label`, "data-editable-label": `stats[${idx}].label`, children: String(row.label ?? "") }), description.length > 0 && (_jsx("span", { className: "stat-item__description", "data-editable-target": `stats[${idx}].description`, "data-editable-target-label": `stats[${idx}].description`, "data-editable-label": `stats[${idx}].description`, children: description }))] }, idx));
|
|
12
12
|
}) })] }) }));
|
|
13
13
|
}
|
|
@@ -9,7 +9,16 @@ export function Testimonials(props) {
|
|
|
9
9
|
const imageUrl = typeof row.imageUrl === "string" ? row.imageUrl.trim() : "";
|
|
10
10
|
const imageAlt = typeof row.imageAlt === "string" ? row.imageAlt.trim() : "";
|
|
11
11
|
const role = typeof row.role === "string" ? row.role.trim() : "";
|
|
12
|
-
const iconNode = renderBlockIcon(row.icon, { className: "testimonial-card__icon", size: 32 });
|
|
13
|
-
return (_jsxs("blockquote", { className: "testimonial-card", children: [iconNode ?? (_jsx("span", { className: "testimonial-card__mark", "aria-hidden": "true", children: "\u201C" })), _jsx("p", { className: "testimonial-card__quote", "data-editable-target": `items[${idx}].quote`, "data-editable-target-label": `items[${idx}].quote`, "data-editable-label": `items[${idx}].quote`, children: String(row.quote ?? "") }), _jsxs("footer", { className: "testimonial-card__footer", children: [imageUrl.length > 0 && (
|
|
12
|
+
const iconNode = renderBlockIcon(row.icon, { className: "testimonial-card__icon", size: 32, editablePath: `items[${idx}].icon` });
|
|
13
|
+
return (_jsxs("blockquote", { className: "testimonial-card", children: [iconNode ?? (_jsx("span", { className: "testimonial-card__mark", "aria-hidden": "true", children: "\u201C" })), _jsx("p", { className: "testimonial-card__quote", "data-editable-target": `items[${idx}].quote`, "data-editable-target-label": `items[${idx}].quote`, "data-editable-label": `items[${idx}].quote`, children: String(row.quote ?? "") }), _jsxs("footer", { className: "testimonial-card__footer", children: [imageUrl.length > 0 && (
|
|
14
|
+
/*
|
|
15
|
+
* The marker used to sit on the `<img>` itself, which reads
|
|
16
|
+
* as instrumented everywhere it is checked and is not: the
|
|
17
|
+
* overlay mounts the Change button by appending it into the
|
|
18
|
+
* marked element, and an `<img>` has no children. The
|
|
19
|
+
* attribute was in the HTML, the editor found the field, and
|
|
20
|
+
* no button could ever appear on an avatar.
|
|
21
|
+
*/
|
|
22
|
+
_jsx("span", { className: "testimonial-card__avatar-wrap", "data-editable-target": `items[${idx}].imageUrl`, "data-editable-target-label": `items[${idx}].imageUrl`, "data-editable-label": `items[${idx}].imageUrl`, children: _jsx(BlockImage, { className: "testimonial-card__avatar", src: imageUrl, alt: imageAlt.length > 0 ? imageAlt : String(row.author ?? ""), width: 48, height: 48, loading: "lazy" }) })), _jsxs("div", { className: "testimonial-card__attribution", children: [_jsxs("span", { className: "testimonial-card__author", "data-editable-target": `items[${idx}].author`, "data-editable-target-label": `items[${idx}].author`, "data-editable-label": `items[${idx}].author`, children: ["\u2014 ", String(row.author ?? "")] }), role.length > 0 && (_jsx("span", { className: "testimonial-card__role", "data-editable-target": `items[${idx}].role`, "data-editable-target-label": `items[${idx}].role`, "data-editable-label": `items[${idx}].role`, children: role }))] })] })] }, idx));
|
|
14
23
|
}) })] }) }));
|
|
15
24
|
}
|
|
@@ -52,6 +52,15 @@ img.testimonial-card__icon {
|
|
|
52
52
|
gap: 12px;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/* Wrapper the image marker sits on — see the note in the renderer. A flex item
|
|
56
|
+
of `.testimonial-card__footer`, sized by the avatar inside it. */
|
|
57
|
+
.testimonial-card__avatar-wrap {
|
|
58
|
+
display: block;
|
|
59
|
+
position: relative;
|
|
60
|
+
flex: 0 0 auto;
|
|
61
|
+
line-height: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
.testimonial-card__avatar {
|
|
56
65
|
width: 48px;
|
|
57
66
|
height: 48px;
|
|
@@ -1,31 +1,75 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { resolveHeadingTag } from "@avocadostudio-ai/shared";
|
|
3
3
|
import { PrimaryButton, renderRichTextContent, BlockImage } from "../_shared.js";
|
|
4
|
+
/**
|
|
5
|
+
* Mark an element for the field at `<prefix>.<key>`.
|
|
6
|
+
*
|
|
7
|
+
* The prefix matters more than it looks. These children used to be marked with
|
|
8
|
+
* flat names — `heading`, `body`, `ctaText` — which read like the other blocks'
|
|
9
|
+
* props and are nothing of the sort: a TwoColumn has no `heading` prop, only
|
|
10
|
+
* `left[]` and `right[]` of typed children. So every inline edit in this block
|
|
11
|
+
* committed a field draft addressed to a prop that does not exist, the schema
|
|
12
|
+
* dropped the unknown key on the way back through, and the edit disappeared on
|
|
13
|
+
* the next reconcile with no error anywhere. The coverage check saw the other
|
|
14
|
+
* half of the same fact: both lists reported "no marker for any item".
|
|
15
|
+
*/
|
|
16
|
+
function marker(pathPrefix, key, label) {
|
|
17
|
+
const path = `${pathPrefix}.${key}`;
|
|
18
|
+
return {
|
|
19
|
+
"data-editable-target": path,
|
|
20
|
+
"data-editable-target-label": label ?? path,
|
|
21
|
+
"data-editable-label": label ?? path,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
function TwoColumnChild({ item, headingTag, pathPrefix }) {
|
|
5
25
|
const childType = String(item.type ?? "");
|
|
6
26
|
const HeadingTag = headingTag;
|
|
7
|
-
if (childType === "heading") {
|
|
8
|
-
|
|
27
|
+
if (childType === "heading" || childType === "subheading") {
|
|
28
|
+
const Tag = childType === "heading" ? HeadingTag : "p";
|
|
29
|
+
return (_jsx(Tag, { className: childType === "subheading" ? "two-column__subheading" : undefined, ...marker(pathPrefix, "text"), children: String(item.text ?? "") }));
|
|
9
30
|
}
|
|
10
31
|
if (childType === "paragraph") {
|
|
11
32
|
const renderedBody = renderRichTextContent(String(item.text ?? ""));
|
|
12
|
-
return (_jsx("div", { className: "two-column__body",
|
|
33
|
+
return (_jsx("div", { className: "two-column__body", ...marker(pathPrefix, "text"), "data-editable-multiline": "true", children: renderedBody }));
|
|
34
|
+
}
|
|
35
|
+
if (childType === "list") {
|
|
36
|
+
const entries = Array.isArray(item.items) ? item.items : [];
|
|
37
|
+
const text = String(item.text ?? "");
|
|
38
|
+
if (entries.length === 0 && text.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
if (entries.length === 0) {
|
|
41
|
+
return (_jsx("div", { className: "two-column__body", ...marker(pathPrefix, "text"), "data-editable-multiline": "true", children: renderRichTextContent(text) }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsx("ul", { className: "two-column__list", children: entries.map((entry, i) => (_jsx("li", { children: String(entry ?? "") }, i))) }));
|
|
13
44
|
}
|
|
14
|
-
if (childType === "cta") {
|
|
15
|
-
|
|
16
|
-
|
|
45
|
+
if (childType === "cta" || childType === "ctas") {
|
|
46
|
+
/*
|
|
47
|
+
* `ctas` carries a `buttons` array; `cta` is the single-button spelling.
|
|
48
|
+
* Only the single one is marked: a button inside the array addresses
|
|
49
|
+
* `left[0].buttons[1].label`, two levels of nesting, which
|
|
50
|
+
* `supportsInlineEditablePath` rejects — so a marker there would offer an
|
|
51
|
+
* inline edit that cannot be committed.
|
|
52
|
+
*/
|
|
53
|
+
const buttons = childType === "ctas" && Array.isArray(item.buttons)
|
|
54
|
+
? item.buttons
|
|
55
|
+
: [{ label: item.label, href: item.href, newTab: item.newTab }];
|
|
56
|
+
const drawable = buttons.filter((button) => String(button?.label ?? "").length > 0);
|
|
57
|
+
if (drawable.length === 0)
|
|
17
58
|
return null;
|
|
18
|
-
return (_jsx("div", { className: "two-column__cta", children: _jsx(PrimaryButton, { href: String(
|
|
59
|
+
return (_jsx("div", { className: "two-column__cta", children: drawable.map((button, i) => (_jsx(PrimaryButton, { href: String(button.href ?? "#"), newTab: button.newTab === true, ...(childType === "cta" ? marker(pathPrefix, "label") : {}), children: String(button.label ?? "") }, i))) }));
|
|
19
60
|
}
|
|
20
61
|
if (childType === "video") {
|
|
21
62
|
const src = String(item.src ?? "");
|
|
22
63
|
const poster = item.poster ? String(item.poster) : undefined;
|
|
23
64
|
if (!src)
|
|
24
65
|
return null;
|
|
25
|
-
|
|
66
|
+
// The marker goes on the wrapper, never the <video>: the overlay mounts the
|
|
67
|
+
// Change button by appending it into the marked element, and a browser
|
|
68
|
+
// renders nothing a <video> contains.
|
|
69
|
+
return (_jsx("div", { className: "two-column__media", ...marker(pathPrefix, "poster", "Video poster"), "data-editable-kind": "image", children: _jsx("video", { className: "two-column__video", controls: true, playsInline: true, preload: "metadata", poster: poster, children: _jsx("source", { src: src, type: "video/mp4" }) }) }));
|
|
26
70
|
}
|
|
27
71
|
if (childType === "image") {
|
|
28
|
-
return (_jsx("div", { className: "two-column__media",
|
|
72
|
+
return (_jsx("div", { className: "two-column__media", ...marker(pathPrefix, "src", "Image"), children: _jsx(BlockImage, { className: "layout-grid__img", src: String(item.src ?? ""), alt: String(item.alt ?? ""), width: 768, height: 1024, sizes: "(max-width: 768px) 100vw, 50vw", "data-editable-label": "Image" }) }));
|
|
29
73
|
}
|
|
30
74
|
return null;
|
|
31
75
|
}
|
|
@@ -136,3 +136,39 @@
|
|
|
136
136
|
margin: 0.3em 0 0;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
|
|
140
|
+
/* A `subheading` child: a lead paragraph under the column title, not a heading
|
|
141
|
+
element — the block already resolves one heading level for the column and a
|
|
142
|
+
second one under it would misread as structure. */
|
|
143
|
+
.two-column__subheading {
|
|
144
|
+
margin: 0 0 12px;
|
|
145
|
+
font-size: 1.125rem;
|
|
146
|
+
line-height: 1.5;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
color: var(--body);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* A `list` child. The same look as a list written inside a `paragraph` child's
|
|
152
|
+
markdown, so the two spellings of the same content do not render differently. */
|
|
153
|
+
.two-column__list {
|
|
154
|
+
margin: 0 0 16px;
|
|
155
|
+
padding-left: 1.4em;
|
|
156
|
+
list-style: disc;
|
|
157
|
+
color: var(--body-secondary);
|
|
158
|
+
line-height: 1.75;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.two-column__list li {
|
|
162
|
+
display: list-item;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.two-column__list li + li {
|
|
166
|
+
margin-top: 0.3em;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* `ctas` draws more than one button; `cta` draws exactly one and is unaffected. */
|
|
170
|
+
.two-column__cta {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
gap: 10px;
|
|
174
|
+
}
|
|
@@ -31,14 +31,25 @@ function buildEmbedUrl(type, id, autoplay, loop) {
|
|
|
31
31
|
params.set("loop", "1");
|
|
32
32
|
return `https://player.vimeo.com/video/${id}?${params.toString()}`;
|
|
33
33
|
}
|
|
34
|
-
function EmbedFacade({ type, videoId, title, autoplay, loop }) {
|
|
34
|
+
function EmbedFacade({ type, videoId, title, autoplay, loop, posterUrl }) {
|
|
35
35
|
const [activated, setActivated] = useState(autoplay);
|
|
36
36
|
if (activated) {
|
|
37
37
|
return (_jsx("iframe", { src: buildEmbedUrl(type, videoId, true, loop), title: title || "Video", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture", allowFullScreen: true }));
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/*
|
|
40
|
+
* The poster the block was given wins over the one the platform derives.
|
|
41
|
+
*
|
|
42
|
+
* It used to be read only by the `<video>` branch, so setting a poster on a
|
|
43
|
+
* YouTube or Vimeo URL — which the property panel happily offers, with a
|
|
44
|
+
* picker and a Change button — changed nothing anybody could see. Vimeo was
|
|
45
|
+
* worse than a no-op: it has no `i.ytimg.com` equivalent, so its facade was a
|
|
46
|
+
* black rectangle with a play triangle whatever the block said.
|
|
47
|
+
*/
|
|
48
|
+
const thumbUrl = posterUrl.length > 0
|
|
49
|
+
? posterUrl
|
|
50
|
+
: type === "youtube"
|
|
51
|
+
? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
|
|
52
|
+
: undefined;
|
|
42
53
|
return (_jsxs("button", { type: "button", className: "video-block__facade", onClick: () => setActivated(true), "aria-label": `Play ${title || "video"}`, children: [thumbUrl && _jsx("img", { src: thumbUrl, alt: "", className: "video-block__facade-thumb", loading: "lazy" }), _jsxs("svg", { className: "video-block__facade-play", viewBox: "0 0 68 48", "aria-hidden": "true", children: [_jsx("path", { d: "M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z", fill: "currentColor" }), _jsx("path", { d: "M45 24L27 14v20", fill: "#fff" })] })] }));
|
|
43
54
|
}
|
|
44
55
|
export function Video(props) {
|
|
@@ -48,5 +59,5 @@ export function Video(props) {
|
|
|
48
59
|
const autoplay = String(props.autoplay) === "true";
|
|
49
60
|
const loop = String(props.loop) === "true";
|
|
50
61
|
const sourceType = detectSourceType(src);
|
|
51
|
-
return (_jsx("section", { className: "video-block", children: _jsxs("div", { className: "section__inner", children: [_jsxs("div", { className: "video-block__frame", children: [sourceType === "youtube" && (_jsx(EmbedFacade, { type: "youtube", videoId: extractYouTubeId(src), title: title, autoplay: autoplay, loop: loop })), sourceType === "vimeo" && (_jsx(EmbedFacade, { type: "vimeo", videoId: extractVimeoId(src), title: title, autoplay: autoplay, loop: loop })), sourceType === "direct" && (_jsx("video", { src: src, poster: posterUrl || undefined, controls: true, autoPlay: autoplay, loop: loop, playsInline: true, preload: "metadata" }))] }), title.length > 0 && (_jsx("p", { className: "video-block__caption", "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: renderInline(title) }))] }) }));
|
|
62
|
+
return (_jsx("section", { className: "video-block", children: _jsxs("div", { className: "section__inner", children: [_jsxs("div", { className: "video-block__frame", "data-editable-target": "posterUrl", "data-editable-target-label": "Poster image", "data-editable-kind": "image", children: [sourceType === "youtube" && (_jsx(EmbedFacade, { type: "youtube", videoId: extractYouTubeId(src), title: title, autoplay: autoplay, loop: loop, posterUrl: posterUrl })), sourceType === "vimeo" && (_jsx(EmbedFacade, { type: "vimeo", videoId: extractVimeoId(src), title: title, autoplay: autoplay, loop: loop, posterUrl: posterUrl })), sourceType === "direct" && (_jsx("video", { src: src, poster: posterUrl || undefined, controls: true, autoPlay: autoplay, loop: loop, playsInline: true, preload: "metadata" }))] }), title.length > 0 && (_jsx("p", { className: "video-block__caption", "data-editable-target": "title", "data-editable-target-label": "title", "data-editable-label": "title", children: renderInline(title) }))] }) }));
|
|
52
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/blocks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@avocadostudio-ai/shared": "^0.
|
|
30
|
-
"@avocadostudio-ai/richtext": "^0.
|
|
29
|
+
"@avocadostudio-ai/shared": "^0.4.0",
|
|
30
|
+
"@avocadostudio-ai/richtext": "^0.4.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"next": ">=15.0.0",
|