@avocadostudio-ai/blocks 0.1.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/LICENSE +201 -0
- package/dist/block-error-boundary.d.ts +18 -0
- package/dist/block-error-boundary.js +36 -0
- package/dist/blocks/_base.css +108 -0
- package/dist/blocks/_layout.css +47 -0
- package/dist/blocks/_shared.d.ts +24 -0
- package/dist/blocks/_shared.js +153 -0
- package/dist/blocks/_tokens.css +127 -0
- package/dist/blocks/banner/renderer.d.ts +2 -0
- package/dist/blocks/banner/renderer.js +21 -0
- package/dist/blocks/banner/styles.css +72 -0
- package/dist/blocks/block-image.d.ts +14 -0
- package/dist/blocks/block-image.js +10 -0
- package/dist/blocks/block-types.d.ts +2 -0
- package/dist/blocks/block-types.js +22 -0
- package/dist/blocks/card/renderer.d.ts +1 -0
- package/dist/blocks/card/renderer.js +11 -0
- package/dist/blocks/card/styles.css +94 -0
- package/dist/blocks/card-grid/renderer.d.ts +1 -0
- package/dist/blocks/card-grid/renderer.js +17 -0
- package/dist/blocks/card-grid/styles.css +38 -0
- package/dist/blocks/carousel/init.d.ts +8 -0
- package/dist/blocks/carousel/init.js +35 -0
- package/dist/blocks/carousel/renderer.d.ts +2 -0
- package/dist/blocks/carousel/renderer.js +19 -0
- package/dist/blocks/carousel/styles.css +136 -0
- package/dist/blocks/cta/renderer.d.ts +1 -0
- package/dist/blocks/cta/renderer.js +7 -0
- package/dist/blocks/cta/styles.css +111 -0
- package/dist/blocks/embed/renderer.d.ts +2 -0
- package/dist/blocks/embed/renderer.js +16 -0
- package/dist/blocks/embed/styles.css +58 -0
- package/dist/blocks/faq-accordion/renderer.d.ts +1 -0
- package/dist/blocks/faq-accordion/renderer.js +11 -0
- package/dist/blocks/faq-accordion/styles.css +68 -0
- package/dist/blocks/feature-grid/renderer.d.ts +1 -0
- package/dist/blocks/feature-grid/renderer.js +12 -0
- package/dist/blocks/feature-grid/styles.css +62 -0
- package/dist/blocks/footer/renderer.d.ts +1 -0
- package/dist/blocks/footer/renderer.js +19 -0
- package/dist/blocks/footer/styles.css +64 -0
- package/dist/blocks/gallery/renderer.d.ts +2 -0
- package/dist/blocks/gallery/renderer.js +21 -0
- package/dist/blocks/gallery/styles.css +59 -0
- package/dist/blocks/hero/renderer.d.ts +1 -0
- package/dist/blocks/hero/renderer.js +20 -0
- package/dist/blocks/hero/styles.css +137 -0
- package/dist/blocks/index.d.ts +5 -0
- package/dist/blocks/index.js +47 -0
- package/dist/blocks/quote/renderer.d.ts +2 -0
- package/dist/blocks/quote/renderer.js +9 -0
- package/dist/blocks/quote/styles.css +97 -0
- package/dist/blocks/rich-text/renderer.d.ts +1 -0
- package/dist/blocks/rich-text/renderer.js +15 -0
- package/dist/blocks/rich-text/styles.css +64 -0
- package/dist/blocks/site-header/renderer.d.ts +1 -0
- package/dist/blocks/site-header/renderer.js +64 -0
- package/dist/blocks/site-header/styles.css +345 -0
- package/dist/blocks/stats/renderer.d.ts +1 -0
- package/dist/blocks/stats/renderer.js +13 -0
- package/dist/blocks/stats/styles.css +64 -0
- package/dist/blocks/styles.css +23 -0
- package/dist/blocks/table/renderer.d.ts +2 -0
- package/dist/blocks/table/renderer.js +14 -0
- package/dist/blocks/table/styles.css +87 -0
- package/dist/blocks/tabs/init.d.ts +5 -0
- package/dist/blocks/tabs/init.js +38 -0
- package/dist/blocks/tabs/renderer.d.ts +2 -0
- package/dist/blocks/tabs/renderer.js +30 -0
- package/dist/blocks/tabs/styles.css +147 -0
- package/dist/blocks/testimonials/renderer.d.ts +1 -0
- package/dist/blocks/testimonials/renderer.js +15 -0
- package/dist/blocks/testimonials/styles.css +92 -0
- package/dist/blocks/two-column/renderer.d.ts +1 -0
- package/dist/blocks/two-column/renderer.js +41 -0
- package/dist/blocks/two-column/styles.css +83 -0
- package/dist/blocks/video/renderer.d.ts +2 -0
- package/dist/blocks/video/renderer.js +52 -0
- package/dist/blocks/video/styles.css +84 -0
- package/dist/blocks-hydrator.d.ts +5 -0
- package/dist/blocks-hydrator.js +15 -0
- package/dist/catalogue.d.ts +1 -0
- package/dist/catalogue.js +741 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/renderer.d.ts +12 -0
- package/dist/renderer.js +35 -0
- package/package.json +54 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* Design tokens consumed by block renderers and the BlockCatalogue UI.
|
|
2
|
+
Ship with the package so consumers don't need to redeclare them. */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: light;
|
|
6
|
+
--bg-0: #faf9f5;
|
|
7
|
+
--bg-000: #faf9f5;
|
|
8
|
+
--bg-100: #ffffff;
|
|
9
|
+
--bg-200: #f0eee6;
|
|
10
|
+
--bg-300: #dddddd;
|
|
11
|
+
--text-100: #1f1e1d;
|
|
12
|
+
--text-200: #3d3d3a;
|
|
13
|
+
--text-300: #73726c;
|
|
14
|
+
--text-400: #888888;
|
|
15
|
+
--text-500: #999999;
|
|
16
|
+
--accent: #d97757;
|
|
17
|
+
--accent-hover: #c6613f;
|
|
18
|
+
--radius: 0.5rem;
|
|
19
|
+
--ease-silk: cubic-bezier(0.2, 0, 0, 1);
|
|
20
|
+
|
|
21
|
+
--brand: #0f766e;
|
|
22
|
+
--brand-hover: #10958a;
|
|
23
|
+
--brand-subtle: #ecfdf5;
|
|
24
|
+
--brand-fg: #fff;
|
|
25
|
+
|
|
26
|
+
--surface: #f8fafc;
|
|
27
|
+
--surface-border: #e2e8f0;
|
|
28
|
+
|
|
29
|
+
--heading: #0f172a;
|
|
30
|
+
--body: #1e293b;
|
|
31
|
+
--body-secondary: #334155;
|
|
32
|
+
--caption: #64748b;
|
|
33
|
+
--link: #0f766e;
|
|
34
|
+
|
|
35
|
+
--nav-border: #99d6d0;
|
|
36
|
+
--nav-bg: rgb(240 253 250 / 0.95);
|
|
37
|
+
|
|
38
|
+
--footer-bg: #0f172a;
|
|
39
|
+
--footer-text: #cbd5e1;
|
|
40
|
+
--footer-heading: #f1f5f9;
|
|
41
|
+
--footer-link: #94a3b8;
|
|
42
|
+
--footer-link-hover: #e2e8f0;
|
|
43
|
+
--footer-border: #1e293b;
|
|
44
|
+
|
|
45
|
+
--placeholder-img: #e2e8f0;
|
|
46
|
+
|
|
47
|
+
--font-body: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
48
|
+
--font-heading: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
49
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
50
|
+
|
|
51
|
+
--radius-btn: 6px;
|
|
52
|
+
--radius-card: 0;
|
|
53
|
+
--radius-feature: 8px;
|
|
54
|
+
--radius-logo: 8px;
|
|
55
|
+
--radius-input: 10px;
|
|
56
|
+
--radius-dropdown: 12px;
|
|
57
|
+
|
|
58
|
+
--bg-1: #ffffff;
|
|
59
|
+
--border: #e2e8f0;
|
|
60
|
+
--stripe-bg: #f5f5f0;
|
|
61
|
+
--table-header-bg: #eceee6;
|
|
62
|
+
|
|
63
|
+
--card-bg: #f8fafc;
|
|
64
|
+
--card-shadow: none;
|
|
65
|
+
--hero-bg: linear-gradient(120deg, #eef6ff 0%, #f0fdf4 55%, #fff7ed 100%);
|
|
66
|
+
--cta-bg:
|
|
67
|
+
radial-gradient(circle at 12% 18%, rgb(20 184 166 / 0.18), transparent 36%),
|
|
68
|
+
radial-gradient(circle at 82% 74%, rgb(59 130 246 / 0.14), transparent 40%),
|
|
69
|
+
linear-gradient(120deg, #f0fdfa 0%, #eff6ff 52%, #fefce8 100%);
|
|
70
|
+
--section-bg: var(--bg-100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.dark {
|
|
74
|
+
color-scheme: dark;
|
|
75
|
+
--bg-0: #212121;
|
|
76
|
+
--bg-000: #212121;
|
|
77
|
+
--bg-100: #262624;
|
|
78
|
+
--bg-200: #30302e;
|
|
79
|
+
--bg-300: #454540;
|
|
80
|
+
--text-100: #ececec;
|
|
81
|
+
--text-200: #e1e1e0;
|
|
82
|
+
--text-300: #b4b4b4;
|
|
83
|
+
--text-400: #8a8a88;
|
|
84
|
+
--text-500: #6b6b65;
|
|
85
|
+
--accent: #d2996e;
|
|
86
|
+
--accent-hover: #e5aa7f;
|
|
87
|
+
|
|
88
|
+
--brand: #0f766e;
|
|
89
|
+
--brand-hover: #10958a;
|
|
90
|
+
--brand-subtle: rgb(20 184 166 / 0.08);
|
|
91
|
+
--brand-fg: #fff;
|
|
92
|
+
|
|
93
|
+
--surface: #111827;
|
|
94
|
+
--surface-border: #2b3b53;
|
|
95
|
+
|
|
96
|
+
--heading: #f8fafc;
|
|
97
|
+
--body: #cbd5e1;
|
|
98
|
+
--body-secondary: #cbd5e1;
|
|
99
|
+
--caption: #93c5fd;
|
|
100
|
+
--link: #7dd3fc;
|
|
101
|
+
|
|
102
|
+
--nav-border: #334155;
|
|
103
|
+
--nav-bg: rgb(15 23 42 / 0.92);
|
|
104
|
+
|
|
105
|
+
--footer-bg: #0f172a;
|
|
106
|
+
--footer-text: #cbd5e1;
|
|
107
|
+
--footer-heading: #f1f5f9;
|
|
108
|
+
--footer-link: #94a3b8;
|
|
109
|
+
--footer-link-hover: #e2e8f0;
|
|
110
|
+
--footer-border: #1e293b;
|
|
111
|
+
|
|
112
|
+
--placeholder-img: #e2e8f0;
|
|
113
|
+
|
|
114
|
+
--bg-1: #1e293b;
|
|
115
|
+
--border: #334155;
|
|
116
|
+
--stripe-bg: #1c1c1a;
|
|
117
|
+
--table-header-bg: #2a2a27;
|
|
118
|
+
|
|
119
|
+
--card-bg: linear-gradient(160deg, #172033 0%, #111827 100%);
|
|
120
|
+
--card-shadow: 0 8px 22px rgb(2 6 23 / 0.35);
|
|
121
|
+
--hero-bg: linear-gradient(120deg, #172033 0%, #13272b 55%, #352817 100%);
|
|
122
|
+
--cta-bg:
|
|
123
|
+
radial-gradient(circle at 12% 18%, rgb(20 184 166 / 0.22), transparent 36%),
|
|
124
|
+
radial-gradient(circle at 82% 74%, rgb(59 130 246 / 0.2), transparent 40%),
|
|
125
|
+
linear-gradient(120deg, #0f172a 0%, #111827 52%, #1f2937 100%);
|
|
126
|
+
--section-bg: #111827;
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderInline, PrimaryButton } from "../_shared.js";
|
|
3
|
+
const variantClass = {
|
|
4
|
+
info: "banner--info",
|
|
5
|
+
success: "banner--success",
|
|
6
|
+
warning: "banner--warning",
|
|
7
|
+
};
|
|
8
|
+
export function Banner(props) {
|
|
9
|
+
const text = String(props.text ?? "");
|
|
10
|
+
const variant = String(props.variant ?? "info");
|
|
11
|
+
const ctaText = String(props.ctaText ?? "");
|
|
12
|
+
const ctaHref = String(props.ctaHref ?? "");
|
|
13
|
+
const backgroundColor = typeof props.backgroundColor === "string" ? props.backgroundColor.trim() : "";
|
|
14
|
+
const textColor = typeof props.textColor === "string" ? props.textColor.trim() : "";
|
|
15
|
+
const customStyle = {};
|
|
16
|
+
if (backgroundColor.length > 0)
|
|
17
|
+
customStyle.backgroundColor = backgroundColor;
|
|
18
|
+
if (textColor.length > 0)
|
|
19
|
+
customStyle.color = textColor;
|
|
20
|
+
return (_jsx("section", { className: `banner ${variantClass[variant] ?? variantClass.info}`, style: Object.keys(customStyle).length > 0 ? customStyle : undefined, children: _jsxs("div", { className: "banner__inner section__inner", children: [_jsx("p", { className: "banner__text", "data-editable-target": "text", "data-editable-target-label": "text", "data-editable-label": "text", children: renderInline(text) }), ctaText.length > 0 && ctaHref.length > 0 && (_jsx(PrimaryButton, { href: ctaHref, className: "banner__cta", "data-editable-target": "ctaText", "data-editable-target-label": "ctaText", "data-editable-label": "ctaText", children: ctaText }))] }) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.banner {
|
|
2
|
+
padding: 16px 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.banner__inner {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
gap: 16px;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.banner__text {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 1rem;
|
|
16
|
+
font-weight: 500;
|
|
17
|
+
line-height: 1.4;
|
|
18
|
+
text-align: center;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.banner__cta.btn-primary {
|
|
22
|
+
padding: 8px 20px;
|
|
23
|
+
font-size: 0.9rem;
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Variants — light mode */
|
|
28
|
+
.banner--info {
|
|
29
|
+
background: var(--brand-subtle, #e0f2fe);
|
|
30
|
+
color: var(--heading);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.banner--success {
|
|
34
|
+
background: var(--banner-success-bg, #d1fae5);
|
|
35
|
+
color: var(--banner-success-text, #065f46);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.banner--warning {
|
|
39
|
+
background: var(--banner-warning-bg, #fef3c7);
|
|
40
|
+
color: var(--banner-warning-text, #92400e);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Dark mode */
|
|
44
|
+
.dark .banner--info {
|
|
45
|
+
background: color-mix(in srgb, var(--brand) 15%, var(--bg-1));
|
|
46
|
+
color: var(--text-100);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.dark .banner--success {
|
|
50
|
+
background: var(--banner-success-bg, color-mix(in srgb, #10b981 15%, var(--bg-1)));
|
|
51
|
+
color: var(--banner-success-text, #6ee7b7);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.dark .banner--warning {
|
|
55
|
+
background: var(--banner-warning-bg, color-mix(in srgb, #f59e0b 15%, var(--bg-1)));
|
|
56
|
+
color: var(--banner-warning-text, #fcd34d);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 900px) {
|
|
60
|
+
.banner {
|
|
61
|
+
padding: 12px 12px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.banner__inner {
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: 10px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.banner__text {
|
|
70
|
+
font-size: 0.95rem;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type BlockImageProps = {
|
|
2
|
+
src: string;
|
|
3
|
+
alt: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
fill?: boolean;
|
|
7
|
+
sizes?: string;
|
|
8
|
+
loading?: "lazy" | "eager";
|
|
9
|
+
priority?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
"data-editable-label"?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare function BlockImage({ fill, sizes, priority, loading, ...rest }: BlockImageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import NextImage from "next/image";
|
|
4
|
+
export function BlockImage({ fill, sizes, priority, loading, ...rest }) {
|
|
5
|
+
if (fill) {
|
|
6
|
+
const { width, height, ...fillRest } = rest;
|
|
7
|
+
return _jsx(NextImage, { fill: true, sizes: sizes, priority: priority, loading: priority ? undefined : loading, ...fillRest });
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(NextImage, { width: rest.width ?? 0, height: rest.height ?? 0, sizes: sizes, priority: priority, loading: priority ? undefined : loading, ...rest }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const rendererBlockTypes: readonly ["Hero", "FeatureGrid", "Testimonials", "FAQAccordion", "CTA", "Card", "CardGrid", "RichText", "Stats", "TwoColumn", "Footer", "SiteHeader", "Embed", "Banner", "Carousel", "Gallery", "Tabs", "Table", "Quote", "Video"];
|
|
2
|
+
export type RendererBlockType = (typeof rendererBlockTypes)[number];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const rendererBlockTypes = [
|
|
2
|
+
"Hero",
|
|
3
|
+
"FeatureGrid",
|
|
4
|
+
"Testimonials",
|
|
5
|
+
"FAQAccordion",
|
|
6
|
+
"CTA",
|
|
7
|
+
"Card",
|
|
8
|
+
"CardGrid",
|
|
9
|
+
"RichText",
|
|
10
|
+
"Stats",
|
|
11
|
+
"TwoColumn",
|
|
12
|
+
"Footer",
|
|
13
|
+
"SiteHeader",
|
|
14
|
+
"Embed",
|
|
15
|
+
"Banner",
|
|
16
|
+
"Carousel",
|
|
17
|
+
"Gallery",
|
|
18
|
+
"Tabs",
|
|
19
|
+
"Table",
|
|
20
|
+
"Quote",
|
|
21
|
+
"Video"
|
|
22
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Card(props: Record<string, unknown>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveHeadingTag } from "@avocadostudio-ai/shared";
|
|
3
|
+
import { PrimaryButton, BlockImage } from "../_shared.js";
|
|
4
|
+
export function Card(props) {
|
|
5
|
+
const imageUrl = typeof props.imageUrl === "string" ? props.imageUrl.trim() : "";
|
|
6
|
+
const imageAlt = typeof props.imageAlt === "string" ? props.imageAlt.trim() : "";
|
|
7
|
+
const variant = String(props.variant ?? "default");
|
|
8
|
+
const isFullBleed = variant === "full-bleed" && imageUrl.length > 0;
|
|
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 ? (_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", "data-editable-target": "imageUrl", "data-editable-target-label": "image", "data-editable-label": "image" })) : (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 ?? "#"), "data-editable-target": "ctaText", "data-editable-target-label": "ctaText", "data-editable-label": "ctaText", children: String(props.ctaText ?? "") })] })] }) }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
background: var(--card-bg);
|
|
3
|
+
box-shadow: var(--card-shadow);
|
|
4
|
+
border-radius: var(--radius-card);
|
|
5
|
+
padding: 16px;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.card__image-wrap {
|
|
12
|
+
margin: -16px -16px 12px;
|
|
13
|
+
border-bottom: 1px solid var(--surface-border);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.card__image {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 180px;
|
|
19
|
+
object-fit: cover;
|
|
20
|
+
display: block;
|
|
21
|
+
background: var(--placeholder-img);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.card h3 {
|
|
25
|
+
margin: 0 0 10px;
|
|
26
|
+
font-size: 1.25rem;
|
|
27
|
+
line-height: 1.25;
|
|
28
|
+
letter-spacing: -0.01em;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
color: var(--heading);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.card p {
|
|
34
|
+
margin: 0 0 14px;
|
|
35
|
+
font-size: 1rem;
|
|
36
|
+
line-height: 1.6;
|
|
37
|
+
color: var(--body);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.card .btn-primary,
|
|
41
|
+
.card .btn-secondary {
|
|
42
|
+
margin-top: auto;
|
|
43
|
+
align-self: flex-start;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Full-bleed variant ─────────────────────────────── */
|
|
47
|
+
.card--full-bleed {
|
|
48
|
+
position: relative;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
min-height: 280px;
|
|
51
|
+
justify-content: flex-end;
|
|
52
|
+
padding: 0;
|
|
53
|
+
border: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.card--full-bleed .card__bg-image {
|
|
57
|
+
position: absolute;
|
|
58
|
+
inset: 0;
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
z-index: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.card--full-bleed::after {
|
|
66
|
+
content: "";
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.15) 100%);
|
|
70
|
+
z-index: 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.card__overlay-content {
|
|
74
|
+
position: relative;
|
|
75
|
+
z-index: 2;
|
|
76
|
+
padding: 24px;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
height: 100%;
|
|
80
|
+
justify-content: flex-end;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.card--full-bleed h1,
|
|
84
|
+
.card--full-bleed h2,
|
|
85
|
+
.card--full-bleed h3,
|
|
86
|
+
.card--full-bleed h4,
|
|
87
|
+
.card--full-bleed h5,
|
|
88
|
+
.card--full-bleed h6 {
|
|
89
|
+
color: #fff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.card--full-bleed p {
|
|
93
|
+
color: rgba(255, 255, 255, 0.9);
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CardGrid(props: Record<string, unknown>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveHeadingTag, resolveItemHeadingTag } from "@avocadostudio-ai/shared";
|
|
3
|
+
import { PrimaryButton, BlockImage } from "../_shared.js";
|
|
4
|
+
export function CardGrid(props) {
|
|
5
|
+
const cards = Array.isArray(props.cards) ? props.cards : [];
|
|
6
|
+
const columns = String(props.columns ?? "3");
|
|
7
|
+
const cardVariant = String(props.cardVariant ?? "default");
|
|
8
|
+
const HeadingTag = resolveHeadingTag("CardGrid", props);
|
|
9
|
+
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
|
+
const row = (item ?? {});
|
|
12
|
+
const imageUrl = typeof row.imageUrl === "string" ? row.imageUrl.trim() : "";
|
|
13
|
+
const imageAlt = typeof row.imageAlt === "string" ? row.imageAlt.trim() : "";
|
|
14
|
+
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 ? (_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", "data-editable-target": `cards[${idx}].imageUrl`, "data-editable-target-label": `cards[${idx}].imageUrl`, "data-editable-label": `cards[${idx}].imageUrl` })) : (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 ?? "#"), "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
|
+
}) })] }) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.card-grid {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
4
|
+
gap: 14px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.card-grid--2-col {
|
|
8
|
+
grid-template-columns: repeat(2, 1fr);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.card-grid--4-col {
|
|
12
|
+
grid-template-columns: repeat(4, 1fr);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.card-grid-section h2 {
|
|
16
|
+
margin-bottom: 16px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card-grid .card {
|
|
20
|
+
background-color: var(--surface);
|
|
21
|
+
background-image:
|
|
22
|
+
radial-gradient(circle at 100% 0, rgba(15, 118, 110, 0.1) 0, rgba(15, 118, 110, 0) 42%),
|
|
23
|
+
linear-gradient(145deg, rgba(148, 163, 184, 0.12), rgba(148, 163, 184, 0) 38%);
|
|
24
|
+
border: 1px solid var(--surface-border);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Dark */
|
|
28
|
+
.dark .card-grid .card {
|
|
29
|
+
background-image: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 900px) {
|
|
33
|
+
.card-grid,
|
|
34
|
+
.card-grid--2-col,
|
|
35
|
+
.card-grid--4-col {
|
|
36
|
+
grid-template-columns: 1fr;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire up carousel interactivity: prev/next buttons, dot navigation,
|
|
3
|
+
* scroll-based active dot tracking, and optional autoplay.
|
|
4
|
+
*
|
|
5
|
+
* Safe to call multiple times — uses a data-ready guard to skip
|
|
6
|
+
* already-initialized carousels.
|
|
7
|
+
*/
|
|
8
|
+
export declare function initCarousels(root?: Document | HTMLElement): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire up carousel interactivity: prev/next buttons, dot navigation,
|
|
3
|
+
* scroll-based active dot tracking, and optional autoplay.
|
|
4
|
+
*
|
|
5
|
+
* Safe to call multiple times — uses a data-ready guard to skip
|
|
6
|
+
* already-initialized carousels.
|
|
7
|
+
*/
|
|
8
|
+
export function initCarousels(root = document) {
|
|
9
|
+
root.querySelectorAll(".carousel:not([data-ready])").forEach((el) => {
|
|
10
|
+
el.setAttribute("data-ready", "1");
|
|
11
|
+
const track = el.querySelector(".carousel__track");
|
|
12
|
+
if (!track)
|
|
13
|
+
return;
|
|
14
|
+
const dots = el.querySelectorAll(".carousel__dot");
|
|
15
|
+
const prev = el.querySelector(".carousel__btn--prev");
|
|
16
|
+
const next = el.querySelector(".carousel__btn--next");
|
|
17
|
+
const currentIndex = () => Math.round(track.scrollLeft / track.offsetWidth);
|
|
18
|
+
const goTo = (i) => track.scrollTo({ left: i * track.offsetWidth, behavior: "smooth" });
|
|
19
|
+
const syncDots = () => {
|
|
20
|
+
const c = currentIndex();
|
|
21
|
+
dots.forEach((d, i) => d.classList.toggle("carousel__dot--active", i === c));
|
|
22
|
+
};
|
|
23
|
+
if (prev)
|
|
24
|
+
prev.onclick = () => { const i = currentIndex(); goTo(i > 0 ? i - 1 : dots.length - 1); };
|
|
25
|
+
if (next)
|
|
26
|
+
next.onclick = () => { const i = currentIndex(); goTo(i < dots.length - 1 ? i + 1 : 0); };
|
|
27
|
+
dots.forEach((d, i) => { d.onclick = () => goTo(i); });
|
|
28
|
+
track.addEventListener("scroll", syncDots, { passive: true });
|
|
29
|
+
if (el.dataset.autoplay === "true") {
|
|
30
|
+
const ms = parseInt(el.dataset.interval ?? "5000") || 5000;
|
|
31
|
+
const timer = setInterval(() => { const i = currentIndex(); goTo(i < dots.length - 1 ? i + 1 : 0); }, ms);
|
|
32
|
+
el.addEventListener("pointerdown", () => clearInterval(timer), { once: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { resolveItemHeadingTag } from "@avocadostudio-ai/shared";
|
|
3
|
+
import { renderInline, PrimaryButton, BlockImage } from "../_shared.js";
|
|
4
|
+
export function Carousel(props) {
|
|
5
|
+
const items = Array.isArray(props.items) ? props.items : [];
|
|
6
|
+
const autoplay = String(props.autoplay) === "true";
|
|
7
|
+
const interval = Number(props.interval) || 5000;
|
|
8
|
+
const ItemHeadingTag = resolveItemHeadingTag("Carousel", props);
|
|
9
|
+
return (_jsx("section", { className: "carousel", "data-autoplay": autoplay ? "true" : undefined, "data-interval": String(interval), children: _jsxs("div", { className: "carousel__inner section__inner", children: [_jsx("div", { className: "carousel__track", children: items.map((raw, idx) => {
|
|
10
|
+
const item = (raw ?? {});
|
|
11
|
+
const imageUrl = String(item.imageUrl ?? "");
|
|
12
|
+
const imageAlt = String(item.imageAlt ?? "");
|
|
13
|
+
const heading = String(item.heading ?? "");
|
|
14
|
+
const description = String(item.description ?? "");
|
|
15
|
+
const ctaText = String(item.ctaText ?? "");
|
|
16
|
+
const ctaHref = String(item.ctaHref ?? "");
|
|
17
|
+
return (_jsxs("div", { className: "carousel__slide", children: [imageUrl.length > 0 && (_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, 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
|
+
}) }), 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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
.carousel {
|
|
2
|
+
padding: 40px 20px;
|
|
3
|
+
background: var(--section-bg);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.carousel__inner {
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Track — scroll-snap container */
|
|
11
|
+
.carousel__track {
|
|
12
|
+
display: flex;
|
|
13
|
+
overflow-x: auto;
|
|
14
|
+
scroll-snap-type: x mandatory;
|
|
15
|
+
scroll-behavior: smooth;
|
|
16
|
+
-webkit-overflow-scrolling: touch;
|
|
17
|
+
gap: 0;
|
|
18
|
+
scrollbar-width: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.carousel__track::-webkit-scrollbar {
|
|
22
|
+
display: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Slide */
|
|
26
|
+
.carousel__slide {
|
|
27
|
+
flex: 0 0 100%;
|
|
28
|
+
scroll-snap-align: start;
|
|
29
|
+
position: relative;
|
|
30
|
+
border-radius: var(--radius-card, 12px);
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
background: var(--bg-1);
|
|
33
|
+
border: 1px solid var(--border);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.carousel__image {
|
|
37
|
+
width: 100%;
|
|
38
|
+
aspect-ratio: 16 / 9;
|
|
39
|
+
object-fit: cover;
|
|
40
|
+
display: block;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Caption area */
|
|
44
|
+
.carousel__caption {
|
|
45
|
+
padding: 20px 24px;
|
|
46
|
+
border-top: 1px solid var(--border);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.carousel__caption :is(h2, h3, h4, h5, h6) {
|
|
50
|
+
margin: 0 0 6px;
|
|
51
|
+
font-size: 1.25rem;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
color: var(--heading);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.carousel__caption p {
|
|
57
|
+
margin: 0;
|
|
58
|
+
font-size: 1rem;
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
color: var(--text-200);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.carousel__cta.btn-primary {
|
|
64
|
+
margin-top: 12px;
|
|
65
|
+
display: inline-block;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Prev / Next buttons */
|
|
69
|
+
.carousel__btn {
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 50%;
|
|
72
|
+
transform: translateY(-50%);
|
|
73
|
+
z-index: 2;
|
|
74
|
+
width: 40px;
|
|
75
|
+
height: 40px;
|
|
76
|
+
border: none;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
background: color-mix(in srgb, var(--bg-1) 85%, transparent);
|
|
79
|
+
color: var(--text-100);
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
85
|
+
transition: background 0.15s, opacity 0.15s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.carousel__btn:hover {
|
|
89
|
+
background: var(--bg-1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.carousel__btn--prev { left: 12px; }
|
|
93
|
+
.carousel__btn--next { right: 12px; }
|
|
94
|
+
|
|
95
|
+
/* Dot indicators */
|
|
96
|
+
.carousel__dots {
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
margin-top: 16px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.carousel__dot {
|
|
104
|
+
width: 10px;
|
|
105
|
+
height: 10px;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
border: 2px solid var(--text-200);
|
|
108
|
+
background: transparent;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
padding: 0;
|
|
111
|
+
transition: background 0.15s, border-color 0.15s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.carousel__dot--active {
|
|
115
|
+
background: var(--brand);
|
|
116
|
+
border-color: var(--brand);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Mobile */
|
|
120
|
+
@media (max-width: 900px) {
|
|
121
|
+
.carousel {
|
|
122
|
+
padding: 24px 12px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.carousel__btn {
|
|
126
|
+
width: 32px;
|
|
127
|
+
height: 32px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.carousel__btn--prev { left: 6px; }
|
|
131
|
+
.carousel__btn--next { right: 6px; }
|
|
132
|
+
|
|
133
|
+
.carousel__caption {
|
|
134
|
+
padding: 14px 16px;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CTA(props: Record<string, unknown>): import("react/jsx-runtime").JSX.Element;
|