@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.
Files changed (88) hide show
  1. package/LICENSE +201 -0
  2. package/dist/block-error-boundary.d.ts +18 -0
  3. package/dist/block-error-boundary.js +36 -0
  4. package/dist/blocks/_base.css +108 -0
  5. package/dist/blocks/_layout.css +47 -0
  6. package/dist/blocks/_shared.d.ts +24 -0
  7. package/dist/blocks/_shared.js +153 -0
  8. package/dist/blocks/_tokens.css +127 -0
  9. package/dist/blocks/banner/renderer.d.ts +2 -0
  10. package/dist/blocks/banner/renderer.js +21 -0
  11. package/dist/blocks/banner/styles.css +72 -0
  12. package/dist/blocks/block-image.d.ts +14 -0
  13. package/dist/blocks/block-image.js +10 -0
  14. package/dist/blocks/block-types.d.ts +2 -0
  15. package/dist/blocks/block-types.js +22 -0
  16. package/dist/blocks/card/renderer.d.ts +1 -0
  17. package/dist/blocks/card/renderer.js +11 -0
  18. package/dist/blocks/card/styles.css +94 -0
  19. package/dist/blocks/card-grid/renderer.d.ts +1 -0
  20. package/dist/blocks/card-grid/renderer.js +17 -0
  21. package/dist/blocks/card-grid/styles.css +38 -0
  22. package/dist/blocks/carousel/init.d.ts +8 -0
  23. package/dist/blocks/carousel/init.js +35 -0
  24. package/dist/blocks/carousel/renderer.d.ts +2 -0
  25. package/dist/blocks/carousel/renderer.js +19 -0
  26. package/dist/blocks/carousel/styles.css +136 -0
  27. package/dist/blocks/cta/renderer.d.ts +1 -0
  28. package/dist/blocks/cta/renderer.js +7 -0
  29. package/dist/blocks/cta/styles.css +111 -0
  30. package/dist/blocks/embed/renderer.d.ts +2 -0
  31. package/dist/blocks/embed/renderer.js +16 -0
  32. package/dist/blocks/embed/styles.css +58 -0
  33. package/dist/blocks/faq-accordion/renderer.d.ts +1 -0
  34. package/dist/blocks/faq-accordion/renderer.js +11 -0
  35. package/dist/blocks/faq-accordion/styles.css +68 -0
  36. package/dist/blocks/feature-grid/renderer.d.ts +1 -0
  37. package/dist/blocks/feature-grid/renderer.js +12 -0
  38. package/dist/blocks/feature-grid/styles.css +62 -0
  39. package/dist/blocks/footer/renderer.d.ts +1 -0
  40. package/dist/blocks/footer/renderer.js +19 -0
  41. package/dist/blocks/footer/styles.css +64 -0
  42. package/dist/blocks/gallery/renderer.d.ts +2 -0
  43. package/dist/blocks/gallery/renderer.js +21 -0
  44. package/dist/blocks/gallery/styles.css +59 -0
  45. package/dist/blocks/hero/renderer.d.ts +1 -0
  46. package/dist/blocks/hero/renderer.js +20 -0
  47. package/dist/blocks/hero/styles.css +137 -0
  48. package/dist/blocks/index.d.ts +5 -0
  49. package/dist/blocks/index.js +47 -0
  50. package/dist/blocks/quote/renderer.d.ts +2 -0
  51. package/dist/blocks/quote/renderer.js +9 -0
  52. package/dist/blocks/quote/styles.css +97 -0
  53. package/dist/blocks/rich-text/renderer.d.ts +1 -0
  54. package/dist/blocks/rich-text/renderer.js +15 -0
  55. package/dist/blocks/rich-text/styles.css +64 -0
  56. package/dist/blocks/site-header/renderer.d.ts +1 -0
  57. package/dist/blocks/site-header/renderer.js +64 -0
  58. package/dist/blocks/site-header/styles.css +345 -0
  59. package/dist/blocks/stats/renderer.d.ts +1 -0
  60. package/dist/blocks/stats/renderer.js +13 -0
  61. package/dist/blocks/stats/styles.css +64 -0
  62. package/dist/blocks/styles.css +23 -0
  63. package/dist/blocks/table/renderer.d.ts +2 -0
  64. package/dist/blocks/table/renderer.js +14 -0
  65. package/dist/blocks/table/styles.css +87 -0
  66. package/dist/blocks/tabs/init.d.ts +5 -0
  67. package/dist/blocks/tabs/init.js +38 -0
  68. package/dist/blocks/tabs/renderer.d.ts +2 -0
  69. package/dist/blocks/tabs/renderer.js +30 -0
  70. package/dist/blocks/tabs/styles.css +147 -0
  71. package/dist/blocks/testimonials/renderer.d.ts +1 -0
  72. package/dist/blocks/testimonials/renderer.js +15 -0
  73. package/dist/blocks/testimonials/styles.css +92 -0
  74. package/dist/blocks/two-column/renderer.d.ts +1 -0
  75. package/dist/blocks/two-column/renderer.js +41 -0
  76. package/dist/blocks/two-column/styles.css +83 -0
  77. package/dist/blocks/video/renderer.d.ts +2 -0
  78. package/dist/blocks/video/renderer.js +52 -0
  79. package/dist/blocks/video/styles.css +84 -0
  80. package/dist/blocks-hydrator.d.ts +5 -0
  81. package/dist/blocks-hydrator.js +15 -0
  82. package/dist/catalogue.d.ts +1 -0
  83. package/dist/catalogue.js +741 -0
  84. package/dist/index.d.ts +7 -0
  85. package/dist/index.js +5 -0
  86. package/dist/renderer.d.ts +12 -0
  87. package/dist/renderer.js +35 -0
  88. 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,2 @@
1
+ import type { JSX } from "react";
2
+ export declare function Banner(props: Record<string, unknown>): JSX.Element;
@@ -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,2 @@
1
+ import type { JSX } from "react";
2
+ export declare function Carousel(props: Record<string, unknown>): JSX.Element;
@@ -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;