simple-module-pagebuilder 0.0.1__py3-none-any.whl

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 (189) hide show
  1. pagebuilder/__init__.py +0 -0
  2. pagebuilder/components/DiffSummary.tsx +107 -0
  3. pagebuilder/components/ScheduledBadge.tsx +34 -0
  4. pagebuilder/components/StatusBadge.tsx +21 -0
  5. pagebuilder/components/blockRegistry.test.ts +56 -0
  6. pagebuilder/components/blockRegistry.ts +80 -0
  7. pagebuilder/components/blocks/Button.tsx +142 -0
  8. pagebuilder/components/blocks/Columns.tsx +64 -0
  9. pagebuilder/components/blocks/Heading.tsx +92 -0
  10. pagebuilder/components/blocks/Image.tsx +125 -0
  11. pagebuilder/components/blocks/MediaPicker.tsx +178 -0
  12. pagebuilder/components/blocks/Spacer.tsx +31 -0
  13. pagebuilder/components/blocks/Text.tsx +80 -0
  14. pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
  15. pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
  16. pagebuilder/components/editor/SchedulePanel.tsx +85 -0
  17. pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
  18. pagebuilder/components/layoutPuckConfig.tsx +109 -0
  19. pagebuilder/components/media/MediaFilters.tsx +83 -0
  20. pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
  21. pagebuilder/components/media/MediaGrid.tsx +76 -0
  22. pagebuilder/components/media/MediaHeader.tsx +37 -0
  23. pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
  24. pagebuilder/components/media/types.ts +20 -0
  25. pagebuilder/components/migrateContent.test.ts +146 -0
  26. pagebuilder/components/migrateContent.ts +135 -0
  27. pagebuilder/components/puckConfig.test.tsx +118 -0
  28. pagebuilder/components/puckConfig.tsx +121 -0
  29. pagebuilder/components/widgetCatalog.test.tsx +123 -0
  30. pagebuilder/components/widgetCatalog.ts +208 -0
  31. pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
  32. pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
  33. pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
  34. pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
  35. pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
  36. pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
  37. pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
  38. pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
  39. pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
  40. pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
  41. pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
  42. pagebuilder/components/widgets/_shared/heading.tsx +88 -0
  43. pagebuilder/components/widgets/_shared/index.ts +39 -0
  44. pagebuilder/components/widgets/_shared/mask.ts +54 -0
  45. pagebuilder/components/widgets/_shared/section.tsx +60 -0
  46. pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
  47. pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
  48. pagebuilder/components/widgets/accordion-widget.tsx +64 -0
  49. pagebuilder/components/widgets/alert-widget.tsx +47 -0
  50. pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
  51. pagebuilder/components/widgets/article-cards-render.tsx +132 -0
  52. pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
  53. pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
  54. pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
  55. pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
  56. pagebuilder/components/widgets/carousel-widget.tsx +97 -0
  57. pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
  58. pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
  59. pagebuilder/components/widgets/column-widget.tsx +63 -0
  60. pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
  61. pagebuilder/components/widgets/contact-form-render.tsx +293 -0
  62. pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
  63. pagebuilder/components/widgets/container-widget.tsx +93 -0
  64. pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
  65. pagebuilder/components/widgets/divider-widget.tsx +56 -0
  66. pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
  67. pagebuilder/components/widgets/faq-render.tsx +268 -0
  68. pagebuilder/components/widgets/faq-widget.tsx +64 -0
  69. pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
  70. pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
  71. pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
  72. pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
  73. pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
  74. pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
  75. pagebuilder/components/widgets/gallery-widget.tsx +139 -0
  76. pagebuilder/components/widgets/grid-widget.tsx +59 -0
  77. pagebuilder/components/widgets/hero-widget.tsx +292 -0
  78. pagebuilder/components/widgets/html-widget.tsx +42 -0
  79. pagebuilder/components/widgets/iframe-widget.tsx +44 -0
  80. pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
  81. pagebuilder/components/widgets/list-widget.tsx +56 -0
  82. pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
  83. pagebuilder/components/widgets/markdown-widget.tsx +148 -0
  84. pagebuilder/components/widgets/media-object-layout.ts +156 -0
  85. pagebuilder/components/widgets/media-object-render.tsx +284 -0
  86. pagebuilder/components/widgets/media-object-text.tsx +202 -0
  87. pagebuilder/components/widgets/media-object-widget.tsx +176 -0
  88. pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
  89. pagebuilder/components/widgets/page-header-widget.tsx +118 -0
  90. pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
  91. pagebuilder/components/widgets/quote-widget.tsx +51 -0
  92. pagebuilder/components/widgets/row-widget.tsx +74 -0
  93. pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
  94. pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
  95. pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
  96. pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
  97. pagebuilder/components/widgets/site-header-nav.tsx +153 -0
  98. pagebuilder/components/widgets/site-header-widget.tsx +182 -0
  99. pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
  100. pagebuilder/components/widgets/stats-widget.tsx +200 -0
  101. pagebuilder/components/widgets/steps-widget.tsx +211 -0
  102. pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
  103. pagebuilder/components/widgets/table-widget.tsx +86 -0
  104. pagebuilder/components/widgets/tabs-widget.tsx +95 -0
  105. pagebuilder/components/widgets/tags-widget.tsx +85 -0
  106. pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
  107. pagebuilder/components/widgets/timeline-layout.ts +73 -0
  108. pagebuilder/components/widgets/timeline-render.tsx +294 -0
  109. pagebuilder/components/widgets/timeline-widget.tsx +106 -0
  110. pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
  111. pagebuilder/components/widgets/video-widget.tsx +103 -0
  112. pagebuilder/components/widgets/welcome-widget.tsx +38 -0
  113. pagebuilder/contracts/__init__.py +57 -0
  114. pagebuilder/contracts/events.py +25 -0
  115. pagebuilder/contracts/schemas.py +239 -0
  116. pagebuilder/conversion/blocks-to-html.test.ts +105 -0
  117. pagebuilder/conversion/blocks-to-html.ts +102 -0
  118. pagebuilder/conversion/index.ts +2 -0
  119. pagebuilder/conversion/types.ts +5 -0
  120. pagebuilder/deps.py +26 -0
  121. pagebuilder/diff.py +129 -0
  122. pagebuilder/endpoints/__init__.py +0 -0
  123. pagebuilder/endpoints/api/__init__.py +21 -0
  124. pagebuilder/endpoints/api/_deps.py +21 -0
  125. pagebuilder/endpoints/api/layout.py +78 -0
  126. pagebuilder/endpoints/api/pages.py +85 -0
  127. pagebuilder/endpoints/api/revisions.py +79 -0
  128. pagebuilder/endpoints/api/uploads.py +92 -0
  129. pagebuilder/endpoints/api/workflow.py +124 -0
  130. pagebuilder/endpoints/seo.py +95 -0
  131. pagebuilder/endpoints/views.py +228 -0
  132. pagebuilder/fields/checkbox-field.tsx +22 -0
  133. pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
  134. pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
  135. pagebuilder/fields/file-picker/index.ts +6 -0
  136. pagebuilder/fields/file-picker/types.ts +15 -0
  137. pagebuilder/fields/index.ts +9 -0
  138. pagebuilder/fields/media-adapter.ts +47 -0
  139. pagebuilder/hooks/useAutosave.ts +134 -0
  140. pagebuilder/hooks/useEditorForm.ts +193 -0
  141. pagebuilder/hooks/useMediaUploads.ts +96 -0
  142. pagebuilder/hooks/usePageRevisions.ts +59 -0
  143. pagebuilder/hooks/usePageSchedule.ts +72 -0
  144. pagebuilder/hooks/usePageWorkflow.ts +173 -0
  145. pagebuilder/layout_service.py +101 -0
  146. pagebuilder/media_images.py +185 -0
  147. pagebuilder/media_queries.py +92 -0
  148. pagebuilder/media_service.py +222 -0
  149. pagebuilder/models.py +209 -0
  150. pagebuilder/module.py +279 -0
  151. pagebuilder/package.json +18 -0
  152. pagebuilder/pages/LayoutEditor.tsx +183 -0
  153. pagebuilder/pages/MediaLibrary.tsx +249 -0
  154. pagebuilder/pages/PageEditor.tsx +181 -0
  155. pagebuilder/pages/PageList.tsx +129 -0
  156. pagebuilder/pages/PendingReview.tsx +128 -0
  157. pagebuilder/pages/PublicPage.tsx +126 -0
  158. pagebuilder/permissions.py +44 -0
  159. pagebuilder/py.typed +0 -0
  160. pagebuilder/security.py +230 -0
  161. pagebuilder/service/__init__.py +141 -0
  162. pagebuilder/service/_common.py +24 -0
  163. pagebuilder/service/_revisions.py +92 -0
  164. pagebuilder/service/_workflow.py +207 -0
  165. pagebuilder/services.py +19 -0
  166. pagebuilder/settings.py +161 -0
  167. pagebuilder/static/widgets-base.css +57 -0
  168. pagebuilder/translation/apply.ts +54 -0
  169. pagebuilder/translation/extract.ts +141 -0
  170. pagebuilder/translation/index.ts +20 -0
  171. pagebuilder/translation/translatable-fields.ts +65 -0
  172. pagebuilder/translation/translation.test.ts +167 -0
  173. pagebuilder/utils/api.ts +12 -0
  174. pagebuilder/utils/datetime.ts +25 -0
  175. pagebuilder/utils/decorative-image.ts +15 -0
  176. pagebuilder/utils/editorSnapshot.ts +39 -0
  177. pagebuilder/utils/markdown.ts +160 -0
  178. pagebuilder/utils/mediaApi.ts +112 -0
  179. pagebuilder/utils/mediaFormat.ts +19 -0
  180. pagebuilder/utils/pagesApi.ts +111 -0
  181. pagebuilder/utils/parse-list.ts +7 -0
  182. pagebuilder/utils/request.ts +52 -0
  183. pagebuilder/utils/slugify.ts +9 -0
  184. pagebuilder/utils/types.ts +171 -0
  185. pagebuilder/utils/widgetUtils.ts +11 -0
  186. simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
  187. simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
  188. simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
  189. simple_module_pagebuilder-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,285 @@
1
+ /** CallToActionWidget types, helpers and render — split from call-to-action-widget.tsx,
2
+ * which keeps the field definitions, so both stay under the 300-line cap. */
3
+
4
+ import type { CSSProperties } from 'react';
5
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
6
+ import { CTAButton, Heading, Section } from './_shared';
7
+ import {
8
+ BAND_SHELL,
9
+ DISPLAY_STYLE,
10
+ LIME_HEADING_STYLE,
11
+ SOFT_HEADING_STYLE,
12
+ } from './call-to-action-styles';
13
+
14
+ export type CtaLink = { label: string; href: string };
15
+ export type CtaImage = { src: string; alt: string; caption: string };
16
+
17
+ export type CallToActionWidgetProps = {
18
+ surface: 'default' | 'lime' | 'soft' | 'related';
19
+ /**
20
+ * Optional CSS color for the band background (e.g. a tenant's program
21
+ * color). When set, renders the centered band on this color with white
22
+ * text, overriding `surface`.
23
+ */
24
+ surfaceColor?: string;
25
+ align: 'left' | 'center';
26
+ eyebrow: string;
27
+ heading: string;
28
+ subheading: string;
29
+ primaryLabel: string;
30
+ primaryHref: string;
31
+ secondaryLabel: string;
32
+ secondaryHref: string;
33
+ links: CtaLink[];
34
+ images: CtaImage[];
35
+ };
36
+
37
+ export const CallToActionWidgetRender = ({
38
+ surface,
39
+ align,
40
+ eyebrow,
41
+ heading,
42
+ subheading,
43
+ primaryLabel,
44
+ primaryHref,
45
+ secondaryLabel,
46
+ secondaryHref,
47
+ links,
48
+ images,
49
+ surfaceColor = '',
50
+ }: CallToActionWidgetProps) => {
51
+ // Custom-colored band (tenant program colors): centered white text on
52
+ // the given background, with the links rendered as white text links.
53
+ if (surfaceColor) {
54
+ return (
55
+ <section className={BAND_SHELL}>
56
+ <div
57
+ className="rounded-3xl px-6 py-12 text-center lg:py-16"
58
+ style={{ backgroundColor: surfaceColor }}
59
+ >
60
+ {eyebrow && (
61
+ <p
62
+ className="text-sm font-medium opacity-80"
63
+ style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
64
+ >
65
+ {renderRichText(eyebrow)}
66
+ </p>
67
+ )}
68
+ {heading && (
69
+ <h2
70
+ className="mx-auto max-w-3xl text-2xl sm:text-3xl lg:text-4xl"
71
+ style={{
72
+ ...DISPLAY_STYLE,
73
+ color: 'var(--pb-surface-contrast, #ffffff)',
74
+ }}
75
+ >
76
+ {renderRichText(heading)}
77
+ </h2>
78
+ )}
79
+ {subheading && (
80
+ <RichTextBlock
81
+ text={subheading}
82
+ className="mx-auto mt-3 max-w-2xl opacity-85"
83
+ style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
84
+ />
85
+ )}
86
+ {links && links.length > 0 && (
87
+ <div className="mt-6 flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
88
+ {links.map((link, idx) => (
89
+ <a
90
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
91
+ key={idx}
92
+ href={link.href || '#'}
93
+ className="text-sm font-semibold underline-offset-4 hover:underline"
94
+ style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
95
+ >
96
+ {renderRichText(link.label || '', { allowLinks: false })}
97
+ </a>
98
+ ))}
99
+ </div>
100
+ )}
101
+ {primaryLabel && (
102
+ <a
103
+ href={primaryHref || '#'}
104
+ className="mt-6 inline-flex items-center justify-center rounded-full border px-6 py-2.5 text-sm font-semibold transition-colors hover:bg-white/10"
105
+ style={{
106
+ color: 'var(--pb-surface-contrast, #ffffff)',
107
+ borderColor: 'var(--pb-surface-contrast, #ffffff)',
108
+ }}
109
+ >
110
+ {renderRichText(primaryLabel, { allowLinks: false })}
111
+ </a>
112
+ )}
113
+ </div>
114
+ </section>
115
+ );
116
+ }
117
+
118
+ // Lavender "related links" band — left heading column, right stacked
119
+ // link rows with arrows + dividers (the recurring "you might also be
120
+ // interested" / "keep exploring" section).
121
+ if (surface === 'related') {
122
+ return (
123
+ <section className={BAND_SHELL}>
124
+ <div className="grid gap-8 rounded-3xl bg-[#eeedff] px-6 py-12 lg:grid-cols-[1fr_1.5fr] lg:gap-12 lg:px-12 lg:py-14">
125
+ <div>
126
+ {eyebrow && (
127
+ <p className="text-sm font-medium text-[#475c61]">{renderRichText(eyebrow)}</p>
128
+ )}
129
+ {heading && (
130
+ <h2
131
+ className="mt-2 text-2xl sm:text-3xl lg:text-4xl"
132
+ style={{ ...DISPLAY_STYLE, color: '#1a353e' }}
133
+ >
134
+ {renderRichText(heading)}
135
+ </h2>
136
+ )}
137
+ {subheading && (
138
+ <RichTextBlock text={subheading} className="mt-3 max-w-md text-[#475c61]" />
139
+ )}
140
+ </div>
141
+ {links && links.length > 0 && (
142
+ <ul className="self-center">
143
+ {links.map((link, idx) => (
144
+ <li
145
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
146
+ key={idx}
147
+ className="border-t border-[#1a353e]/15 last:border-b"
148
+ >
149
+ <a
150
+ href={link.href || '#'}
151
+ className="flex items-center justify-between gap-4 py-4 text-[#1a353e] transition-opacity hover:opacity-70"
152
+ >
153
+ <span className="font-medium">
154
+ {renderRichText(link.label || '', {
155
+ allowLinks: false,
156
+ })}
157
+ </span>
158
+ <span aria-hidden="true">→</span>
159
+ </a>
160
+ </li>
161
+ ))}
162
+ </ul>
163
+ )}
164
+ </div>
165
+ </section>
166
+ );
167
+ }
168
+
169
+ // Lavender "soft" band — centered heading with a row of text links.
170
+ // Sized to the GCA design system (H2 44 Light, 18px SemiBold links —
171
+ // SNAG_012).
172
+ if (surface === 'soft') {
173
+ return (
174
+ <section className={BAND_SHELL}>
175
+ <div className="rounded-3xl bg-[#eeedff] px-6 py-14 text-center lg:py-20">
176
+ {heading && (
177
+ <h2
178
+ className="mx-auto max-w-4xl leading-[var(--pb-heading-leading,1.15)]"
179
+ style={SOFT_HEADING_STYLE}
180
+ >
181
+ {renderRichText(heading)}
182
+ </h2>
183
+ )}
184
+ {subheading && (
185
+ <RichTextBlock
186
+ text={subheading}
187
+ className="mx-auto mt-4 max-w-2xl text-[17px] text-[#475c61]"
188
+ />
189
+ )}
190
+ {links && links.length > 0 && (
191
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-x-10 gap-y-3">
192
+ {links.map((link, idx) => (
193
+ <a
194
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
195
+ key={idx}
196
+ href={link.href || '#'}
197
+ className="text-[17px] font-semibold text-[#1a353e] transition-colors hover:text-[#468f8c]"
198
+ >
199
+ {renderRichText(link.label || '', { allowLinks: false })}
200
+ </a>
201
+ ))}
202
+ </div>
203
+ )}
204
+ </div>
205
+ </section>
206
+ );
207
+ }
208
+
209
+ // Lime "Get involved" band — text + button left, image collage right.
210
+ // The main call to action: display-size heading, 20px body, white pill
211
+ // button that inverts to ink on hover (design-system "Secondary" button —
212
+ // SNAG_013).
213
+ if (surface === 'lime') {
214
+ return (
215
+ <section className={BAND_SHELL}>
216
+ <div className="grid items-center gap-10 rounded-3xl bg-[#d8ec29] px-6 py-12 lg:grid-cols-2 lg:gap-16 lg:px-14 lg:py-16">
217
+ <div>
218
+ {heading && (
219
+ <h2 className="leading-[var(--pb-heading-leading,1.15)]" style={LIME_HEADING_STYLE}>
220
+ {renderRichText(heading)}
221
+ </h2>
222
+ )}
223
+ {subheading && (
224
+ <RichTextBlock
225
+ text={subheading}
226
+ className="mt-6 max-w-md text-[19px] leading-relaxed text-[#1a353e]/85"
227
+ />
228
+ )}
229
+ {primaryLabel && (
230
+ <a
231
+ href={primaryHref || '#'}
232
+ className="mt-8 inline-flex items-center justify-center rounded-full bg-white px-7 py-3 text-[17px] font-semibold text-[#1a353e] transition-colors hover:bg-[#1a353e] hover:text-white"
233
+ >
234
+ {renderRichText(primaryLabel, { allowLinks: false })}
235
+ </a>
236
+ )}
237
+ </div>
238
+ {images && images.length > 0 && (
239
+ <div className="grid grid-cols-3 gap-4">
240
+ {images.map((img, idx) => (
241
+ <div
242
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
243
+ key={idx}
244
+ className="relative aspect-square overflow-hidden rounded-lg bg-[#1a353e]/10"
245
+ >
246
+ {img.src && (
247
+ <img
248
+ src={img.src}
249
+ alt={img.alt}
250
+ loading="lazy"
251
+ className="size-full object-cover"
252
+ />
253
+ )}
254
+ {img.caption && (
255
+ <span className="absolute bottom-1.5 right-1.5 rounded bg-[#1a353e]/70 px-2 py-0.5 text-[11px] font-medium text-white">
256
+ {renderRichText(img.caption)}
257
+ </span>
258
+ )}
259
+ </div>
260
+ ))}
261
+ </div>
262
+ )}
263
+ </div>
264
+ </section>
265
+ );
266
+ }
267
+
268
+ // Default accent CTA (unchanged — used by other tenants).
269
+ return (
270
+ <Section variant="accent" spacing="default" align={align}>
271
+ <div className="lg:flex lg:items-center lg:justify-between gap-6">
272
+ <div className="flex-1">
273
+ <Heading as="h2" size="md" tone="inverse" text={heading} />
274
+ {subheading && (
275
+ <RichTextBlock text={subheading} className="mt-2 text-xl text-primary-100" />
276
+ )}
277
+ </div>
278
+ <div className="mt-6 lg:mt-0 flex flex-wrap gap-3">
279
+ <CTAButton label={primaryLabel} href={primaryHref} variant="inverse-primary" />
280
+ <CTAButton label={secondaryLabel} href={secondaryHref} variant="inverse-outline" />
281
+ </div>
282
+ </div>
283
+ </Section>
284
+ );
285
+ };
@@ -0,0 +1,28 @@
1
+ /** Heading styles and band shell for the call-to-action widget. */
2
+
3
+ import type { CSSProperties } from 'react';
4
+
5
+ export const DISPLAY_STYLE: CSSProperties = {
6
+ fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
7
+ letterSpacing: 'var(--pb-display-tracking)',
8
+ fontFamily: 'var(--pb-display-font)',
9
+ };
10
+
11
+ // Hoisted band-heading styles (constant tokens only). The fallback clamps
12
+ // reproduce the prior `text-2xl sm:text-3xl lg:text-4xl` responsive steps:
13
+ // 24px on phones, 30px at 640px, 36px at 1024px.
14
+ export const SOFT_HEADING_STYLE: CSSProperties = {
15
+ ...DISPLAY_STYLE,
16
+ color: '#1a353e',
17
+ fontSize: 'var(--pb-heading-lg, clamp(1.5rem, 1.25rem + 1.5625vw, 2.25rem))',
18
+ };
19
+
20
+ export const LIME_HEADING_STYLE: CSSProperties = {
21
+ ...DISPLAY_STYLE,
22
+ color: '#1a353e',
23
+ fontSize: 'var(--pb-heading-xl, clamp(1.5rem, 1.25rem + 1.5625vw, 2.25rem))',
24
+ };
25
+
26
+ // Inset bands (lime / soft) share one shell so they line up with every other
27
+ // contained section at the same content width across breakpoints.
28
+ export const BAND_SHELL = 'container mx-auto px-4 py-12 sm:px-6 lg:px-8';
@@ -0,0 +1,83 @@
1
+ /** CallToActionWidget: field definitions and config assembly.
2
+ * Types, helpers and render live in call-to-action-render.tsx. */
3
+
4
+ import type { ComponentConfig } from '@puckeditor/core';
5
+
6
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
7
+
8
+ import { type CallToActionWidgetProps, CallToActionWidgetRender } from './call-to-action-render';
9
+
10
+ export type * from './call-to-action-render';
11
+
12
+ export const CallToActionWidget: ComponentConfig<CallToActionWidgetProps> = {
13
+ label: 'Call to action',
14
+ fields: {
15
+ surface: {
16
+ type: 'select',
17
+ label: 'Surface',
18
+ options: [
19
+ { label: 'Default (accent)', value: 'default' },
20
+ { label: 'Lime band', value: 'lime' },
21
+ { label: 'Soft band (links)', value: 'soft' },
22
+ { label: 'Related links band', value: 'related' },
23
+ ],
24
+ },
25
+ surfaceColor: {
26
+ type: 'text',
27
+ label: 'Surface color (CSS, white text — optional)',
28
+ },
29
+ eyebrow: { type: 'text', label: 'Eyebrow' },
30
+ align: {
31
+ type: 'select',
32
+ label: 'Alignment',
33
+ options: [
34
+ { label: 'Left', value: 'left' },
35
+ { label: 'Center', value: 'center' },
36
+ ],
37
+ },
38
+ heading: { type: 'text', label: 'Heading' },
39
+ subheading: { type: 'textarea', label: 'Subheading' },
40
+ primaryLabel: { type: 'text', label: 'Primary label' },
41
+ primaryHref: { type: 'text', label: 'Primary link' },
42
+ secondaryLabel: { type: 'text', label: 'Secondary label' },
43
+ secondaryHref: { type: 'text', label: 'Secondary link' },
44
+ links: {
45
+ type: 'array',
46
+ label: 'Links (soft band)',
47
+ arrayFields: {
48
+ label: { type: 'text', label: 'Label' },
49
+ href: { type: 'text', label: 'URL' },
50
+ },
51
+ defaultItemProps: { label: 'Link', href: '#' },
52
+ min: 1,
53
+ max: 6,
54
+ },
55
+ images: {
56
+ type: 'array',
57
+ label: 'Images (lime band)',
58
+ arrayFields: {
59
+ src: createImageField(mediaLibraryAdapter, 'Image'),
60
+ alt: { type: 'text', label: 'Alt text' },
61
+ caption: { type: 'text', label: 'Caption' },
62
+ },
63
+ defaultItemProps: { src: '', alt: '', caption: '' },
64
+ min: 1,
65
+ max: 8,
66
+ },
67
+ },
68
+ defaultProps: {
69
+ surface: 'default',
70
+ surfaceColor: '',
71
+ align: 'left',
72
+ eyebrow: '',
73
+ heading: 'Start contributing today.',
74
+ subheading: 'Add a place, a photo, or a story — every contribution counts.',
75
+ primaryLabel: 'Get started',
76
+ primaryHref: '#',
77
+ secondaryLabel: 'Learn more',
78
+ secondaryHref: '#',
79
+ links: [],
80
+ images: [],
81
+ },
82
+ render: (props) => <CallToActionWidgetRender {...props} />,
83
+ };
@@ -0,0 +1,97 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { useState } from 'react';
3
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
4
+ import { renderRichText } from './_internal/rich-text';
5
+ import { EmptyPlaceholder } from './_shared';
6
+
7
+ export type CarouselSlide = {
8
+ imageUrl: string;
9
+ caption: string;
10
+ };
11
+
12
+ export type CarouselWidgetProps = {
13
+ slides: CarouselSlide[];
14
+ };
15
+
16
+ function CarouselRenderer({ slides }: CarouselWidgetProps) {
17
+ const [index, setIndex] = useState(0);
18
+ if (!slides || slides.length === 0) {
19
+ return <EmptyPlaceholder label="Carousel (no slides)" />;
20
+ }
21
+ const slide = slides[Math.min(index, slides.length - 1)];
22
+ return (
23
+ <div className="container mx-auto py-6">
24
+ <div className="relative aspect-video w-full overflow-hidden rounded-xl">
25
+ {slide.imageUrl ? (
26
+ <img
27
+ src={slide.imageUrl}
28
+ alt={slide.caption || ''}
29
+ className="w-full h-full object-cover"
30
+ />
31
+ ) : (
32
+ <div className="w-full h-full bg-gradient-to-br from-gray-200 to-gray-300" />
33
+ )}
34
+ {slide.caption && (
35
+ <div className="absolute inset-x-0 bottom-0 bg-black/60 text-white p-4 text-center">
36
+ {renderRichText(slide.caption)}
37
+ </div>
38
+ )}
39
+ <button
40
+ type="button"
41
+ onClick={() => setIndex((i) => (i === 0 ? slides.length - 1 : i - 1))}
42
+ className="absolute left-3 top-1/2 -translate-y-1/2 size-10 rounded-full bg-white/80 hover:bg-white text-gray-900 grid place-items-center"
43
+ aria-label="Previous slide"
44
+ >
45
+ ‹
46
+ </button>
47
+ <button
48
+ type="button"
49
+ onClick={() => setIndex((i) => (i === slides.length - 1 ? 0 : i + 1))}
50
+ className="absolute right-3 top-1/2 -translate-y-1/2 size-10 rounded-full bg-white/80 hover:bg-white text-gray-900 grid place-items-center"
51
+ aria-label="Next slide"
52
+ >
53
+ ›
54
+ </button>
55
+ <div className="absolute inset-x-0 bottom-2 flex justify-center gap-2">
56
+ {slides.map((_, i) => (
57
+ <button
58
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
59
+ key={i}
60
+ type="button"
61
+ onClick={() => {
62
+ if (i !== index) setIndex(i);
63
+ }}
64
+ className={`size-2 rounded-full ${i === index ? 'bg-white' : 'bg-white/40'}`}
65
+ aria-label={`Go to slide ${i + 1}`}
66
+ />
67
+ ))}
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export const CarouselWidget: ComponentConfig<CarouselWidgetProps> = {
75
+ label: 'Carousel',
76
+ fields: {
77
+ slides: {
78
+ type: 'array',
79
+ label: 'Slides',
80
+ arrayFields: {
81
+ imageUrl: createImageField(mediaLibraryAdapter, 'Image URL'),
82
+ caption: { type: 'text', label: 'Caption' },
83
+ },
84
+ defaultItemProps: { imageUrl: '', caption: '' },
85
+ min: 1,
86
+ max: 12,
87
+ },
88
+ },
89
+ defaultProps: {
90
+ slides: [
91
+ { imageUrl: '', caption: 'Slide one' },
92
+ { imageUrl: '', caption: 'Slide two' },
93
+ { imageUrl: '', caption: 'Slide three' },
94
+ ],
95
+ },
96
+ render: ({ slides }) => <CarouselRenderer slides={slides || []} />,
97
+ };
@@ -0,0 +1,54 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { renderHero } from './hero-widget';
4
+
5
+ export type CenteredHeroWidgetProps = {
6
+ title: string;
7
+ subtitle: string;
8
+ imageUrl: string;
9
+ imageAlt: string;
10
+ primaryLabel: string;
11
+ primaryHref: string;
12
+ };
13
+
14
+ /**
15
+ * @deprecated Use HeroWidget with imagePosition="background", align="center",
16
+ * surface="card". Kept for backwards compatibility with saved pages.
17
+ */
18
+ export const CenteredHeroWidget: ComponentConfig<CenteredHeroWidgetProps> = {
19
+ label: 'Centered hero (deprecated)',
20
+ fields: {
21
+ title: { type: 'text', label: 'Title' },
22
+ subtitle: { type: 'textarea', label: 'Subtitle' },
23
+ imageUrl: createImageField(mediaLibraryAdapter, 'Background image URL'),
24
+ imageAlt: { type: 'text', label: 'Image alt text' },
25
+ primaryLabel: { type: 'text', label: 'Button label' },
26
+ primaryHref: { type: 'text', label: 'Button link' },
27
+ },
28
+ defaultProps: {
29
+ title: 'Welcome to GeoWiki',
30
+ subtitle: 'Discover and contribute to global knowledge.',
31
+ imageUrl: '',
32
+ imageAlt: '',
33
+ primaryLabel: 'Get started',
34
+ primaryHref: '#',
35
+ },
36
+ render: (props) =>
37
+ renderHero({
38
+ eyebrow: '',
39
+ title: props.title,
40
+ subtitle: props.subtitle,
41
+ logoUrl: '',
42
+ logoAlt: '',
43
+ imageUrl: props.imageUrl,
44
+ imageAlt: props.imageAlt,
45
+ imagePosition: 'background',
46
+ primaryLabel: props.primaryLabel,
47
+ primaryHref: props.primaryHref,
48
+ secondaryLabel: '',
49
+ secondaryHref: '',
50
+ overlay: false,
51
+ align: 'center',
52
+ surface: 'card',
53
+ }),
54
+ };
@@ -0,0 +1,117 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { cn } from '../../utils/widgetUtils';
3
+ import { renderRichText } from './_internal/rich-text';
4
+
5
+ export type ColoredActionListItem = {
6
+ title: string;
7
+ href: string;
8
+ };
9
+
10
+ export type ColoredActionListWidgetProps = {
11
+ eyebrow: string;
12
+ heading: string;
13
+ tone: 'green' | 'purple' | 'neutral';
14
+ items: ColoredActionListItem[];
15
+ };
16
+
17
+ const TONE_CLASS: Record<ColoredActionListWidgetProps['tone'], string> = {
18
+ green: 'bg-primary-700 text-white',
19
+ purple: 'bg-[#eeedff] text-gray-900',
20
+ neutral: 'bg-gray-100 text-gray-900',
21
+ };
22
+
23
+ const TONE_DIVIDER: Record<ColoredActionListWidgetProps['tone'], string> = {
24
+ green: 'border-white/20',
25
+ purple: 'border-black/10',
26
+ neutral: 'border-black/10',
27
+ };
28
+
29
+ export const ColoredActionListWidget: ComponentConfig<ColoredActionListWidgetProps> = {
30
+ label: 'Colored action list (tinted block)',
31
+ fields: {
32
+ eyebrow: { type: 'text', label: 'Eyebrow' },
33
+ heading: { type: 'text', label: 'Heading' },
34
+ tone: {
35
+ type: 'select',
36
+ label: 'Tone',
37
+ options: [
38
+ { label: 'Green', value: 'green' },
39
+ { label: 'Purple', value: 'purple' },
40
+ { label: 'Neutral', value: 'neutral' },
41
+ ],
42
+ },
43
+ items: {
44
+ type: 'array',
45
+ label: 'Items',
46
+ arrayFields: {
47
+ title: { type: 'text', label: 'Title' },
48
+ href: { type: 'text', label: 'Link' },
49
+ },
50
+ defaultItemProps: { title: 'Action', href: '#' },
51
+ min: 1,
52
+ max: 20,
53
+ },
54
+ },
55
+ defaultProps: {
56
+ eyebrow: 'Get Started',
57
+ heading: 'Get started with our Farmer Cluster guidelines',
58
+ tone: 'green',
59
+ items: [
60
+ { title: 'Farmer Cluster: An Overview', href: '#' },
61
+ { title: 'Starting a Farmer Cluster', href: '#' },
62
+ { title: 'Managing a Farmer Cluster', href: '#' },
63
+ { title: 'Farmer Cluster Communication Guidelines', href: '#' },
64
+ { title: 'Stakeholder Engagement Guidelines', href: '#' },
65
+ { title: 'Monitoring Biodiversity', href: '#' },
66
+ ],
67
+ },
68
+ render: ({ eyebrow, heading, tone, items }) => (
69
+ <section className="container mx-auto px-4 py-6">
70
+ <div className={cn('rounded-xl p-6 sm:p-10', TONE_CLASS[tone])}>
71
+ {eyebrow && (
72
+ <p className="text-xs font-semibold uppercase tracking-wider opacity-80">
73
+ {renderRichText(eyebrow)}
74
+ </p>
75
+ )}
76
+ {heading && (
77
+ <h2 className="mt-2 text-2xl sm:text-3xl font-light leading-tight">
78
+ {renderRichText(heading)}
79
+ </h2>
80
+ )}
81
+ {items?.length > 0 && (
82
+ <ul className={cn('mt-6 divide-y rounded-lg overflow-hidden border', TONE_DIVIDER[tone])}>
83
+ {items.map((item, idx) => (
84
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
85
+ <li key={idx}>
86
+ <a
87
+ href={item.href || '#'}
88
+ className={cn(
89
+ 'flex items-center justify-between px-4 py-3 text-base font-medium transition-colors',
90
+ tone === 'green' ? 'hover:bg-white/10' : 'hover:bg-black/5',
91
+ )}
92
+ >
93
+ {renderRichText(item.title || '', { allowLinks: false })}
94
+ <svg
95
+ className="size-4 opacity-80"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ viewBox="0 0 24 24"
99
+ aria-hidden="true"
100
+ >
101
+ <title>Open</title>
102
+ <path
103
+ strokeLinecap="round"
104
+ strokeLinejoin="round"
105
+ strokeWidth={2}
106
+ d="M9 5l7 7-7 7"
107
+ />
108
+ </svg>
109
+ </a>
110
+ </li>
111
+ ))}
112
+ </ul>
113
+ )}
114
+ </div>
115
+ </section>
116
+ ),
117
+ };