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,118 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { cn, parseList } from '../../utils/widgetUtils';
4
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
5
+ import { ContentMedia, CTAButton, EyebrowText, Heading, Section } from './_shared';
6
+
7
+ export type FeatureShowcaseWidgetProps = {
8
+ eyebrow: string;
9
+ title: string;
10
+ description: string;
11
+ imageUrl: string;
12
+ imageAlt: string;
13
+ imagePosition: 'left' | 'right';
14
+ bulletList: string;
15
+ ctaLabel: string;
16
+ ctaHref: string;
17
+ };
18
+
19
+ export const FeatureShowcaseWidget: ComponentConfig<FeatureShowcaseWidgetProps> = {
20
+ label: 'Feature showcase (image + bullets)',
21
+ fields: {
22
+ eyebrow: { type: 'text', label: 'Eyebrow (optional)' },
23
+ title: { type: 'text', label: 'Title' },
24
+ description: { type: 'textarea', label: 'Description' },
25
+ imageUrl: createImageField(mediaLibraryAdapter, 'Image URL'),
26
+ imageAlt: { type: 'text', label: 'Image alt text' },
27
+ imagePosition: {
28
+ type: 'select',
29
+ label: 'Image position',
30
+ options: [
31
+ { label: 'Left', value: 'left' },
32
+ { label: 'Right', value: 'right' },
33
+ ],
34
+ },
35
+ bulletList: { type: 'textarea', label: 'Bullet list (one per line)' },
36
+ ctaLabel: { type: 'text', label: 'CTA label (optional)' },
37
+ ctaHref: { type: 'text', label: 'CTA link (optional)' },
38
+ },
39
+ defaultProps: {
40
+ eyebrow: '',
41
+ title: 'Tools built for contributors',
42
+ description: 'Compose your pages from reusable blocks.',
43
+ imageUrl: '',
44
+ imageAlt: '',
45
+ imagePosition: 'right',
46
+ bulletList: 'Drag and drop\nLive preview\nNo code required',
47
+ ctaLabel: '',
48
+ ctaHref: '',
49
+ },
50
+ render: ({
51
+ eyebrow,
52
+ title,
53
+ description,
54
+ imageUrl,
55
+ imageAlt,
56
+ imagePosition,
57
+ bulletList,
58
+ ctaLabel,
59
+ ctaHref,
60
+ }) => {
61
+ const bullets = parseList(bulletList);
62
+ return (
63
+ <Section variant="default" spacing="loose">
64
+ <div
65
+ className={cn(
66
+ 'flex flex-col lg:flex-row items-center gap-12',
67
+ imagePosition === 'left' ? 'lg:flex-row-reverse' : '',
68
+ )}
69
+ >
70
+ <div className="flex-1">
71
+ <EyebrowText>{eyebrow}</EyebrowText>
72
+ <Heading as="h2" size="lg" text={title} />
73
+ {description && (
74
+ <RichTextBlock
75
+ text={description}
76
+ className="mt-4 text-lg"
77
+ style={{ color: 'var(--pb-body-color)' }}
78
+ />
79
+ )}
80
+ {bullets.length > 0 && (
81
+ <ul className="mt-6 space-y-3">
82
+ {bullets.map((bullet, idx) => (
83
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
84
+ <li key={`${idx}-${bullet}`} className="flex items-start gap-3">
85
+ <svg
86
+ className="size-5 text-primary-700 flex-none mt-0.5"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ viewBox="0 0 24 24"
90
+ aria-hidden="true"
91
+ >
92
+ <title>Check</title>
93
+ <path
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ strokeWidth={2}
97
+ d="M5 13l4 4L19 7"
98
+ />
99
+ </svg>
100
+ <span>{renderRichText(bullet)}</span>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ )}
105
+ {ctaLabel && (
106
+ <div className="mt-6">
107
+ <CTAButton label={ctaLabel} href={ctaHref} variant="primary" />
108
+ </div>
109
+ )}
110
+ </div>
111
+ <div className="flex-1">
112
+ <ContentMedia imageUrl={imageUrl} imageAlt={imageAlt} aspect="video" />
113
+ </div>
114
+ </div>
115
+ </Section>
116
+ );
117
+ },
118
+ };
@@ -0,0 +1,139 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { cn } from '../../utils/widgetUtils';
4
+ import { renderRichText } from './_internal/rich-text';
5
+ import { maskStyle } from './_shared';
6
+
7
+ export type GalleryItem = {
8
+ src: string;
9
+ alt: string;
10
+ };
11
+
12
+ export type GalleryWidgetProps = {
13
+ title: string;
14
+ columns: '2' | '3' | '4' | '5';
15
+ /**
16
+ * Organic mask (SVG url, from the tenant's content) clipped over every item
17
+ * image — e.g. the team "egg" grid. When set, items switch to the egg aspect
18
+ * (309/382) and lose their square rounding. Empty = the standard square grid.
19
+ */
20
+ maskUrl?: string;
21
+ /**
22
+ * How each tile is framed. "square" is the default crop. "image" keeps the
23
+ * source's own portrait proportions and does not crop or round it — for
24
+ * artwork that already carries its shape (BioGarden's team portraits ship
25
+ * pre-cut to organic blobs, each one different, so a single CSS mask can't
26
+ * reproduce them).
27
+ */
28
+ itemShape?: 'square' | 'image';
29
+ items: GalleryItem[];
30
+ };
31
+
32
+ const COLS_CLASS: Record<GalleryWidgetProps['columns'], string> = {
33
+ '2': 'grid-cols-2',
34
+ '3': 'grid-cols-2 sm:grid-cols-3',
35
+ '4': 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4',
36
+ '5': 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
37
+ };
38
+
39
+ export const GalleryWidget: ComponentConfig<GalleryWidgetProps> = {
40
+ label: 'Image gallery',
41
+ fields: {
42
+ title: { type: 'text', label: 'Title' },
43
+ columns: {
44
+ type: 'select',
45
+ label: 'Columns',
46
+ options: [
47
+ { label: '2', value: '2' },
48
+ { label: '3', value: '3' },
49
+ { label: '4', value: '4' },
50
+ { label: '5', value: '5' },
51
+ ],
52
+ },
53
+ itemShape: {
54
+ type: 'select',
55
+ label: 'Item shape',
56
+ options: [
57
+ { label: 'Square crop', value: 'square' },
58
+ { label: "Image's own shape", value: 'image' },
59
+ ],
60
+ },
61
+ maskUrl: {
62
+ type: 'text',
63
+ label: 'Item mask URL (organic shape — optional)',
64
+ },
65
+ items: {
66
+ type: 'array',
67
+ label: 'Items',
68
+ arrayFields: {
69
+ src: createImageField(mediaLibraryAdapter, 'Image URL'),
70
+ alt: { type: 'text', label: 'Alt text' },
71
+ },
72
+ defaultItemProps: { src: '', alt: '' },
73
+ min: 1,
74
+ max: 24,
75
+ },
76
+ },
77
+ defaultProps: {
78
+ title: 'Gallery',
79
+ columns: '3',
80
+ maskUrl: '',
81
+ itemShape: 'square',
82
+ items: [
83
+ { src: '', alt: '' },
84
+ { src: '', alt: '' },
85
+ { src: '', alt: '' },
86
+ ],
87
+ },
88
+ render: ({ title, columns, maskUrl = '', itemShape = 'square', items }) => {
89
+ const masked = Boolean(maskUrl);
90
+ const maskCss = masked ? maskStyle(maskUrl) : undefined;
91
+ // A mask still wins (unchanged behaviour); otherwise "image" tiles keep
92
+ // their own proportions and are contained rather than cropped.
93
+ const ownShape = !masked && itemShape === 'image';
94
+ return (
95
+ <section className="container mx-auto my-4">
96
+ {title && (
97
+ <h2 className="my-8 text-center text-4xl tracking-tight font-light sm:text-5xl lg:text-6xl leading-[1.1]">
98
+ {renderRichText(title)}
99
+ </h2>
100
+ )}
101
+ {items && items.length > 0 ? (
102
+ <div className={cn('grid gap-2 sm:gap-4 py-2', COLS_CLASS[columns])}>
103
+ {items.map((item, idx) => (
104
+ <div
105
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
106
+ key={idx}
107
+ className={cn(
108
+ 'overflow-hidden',
109
+ masked && 'aspect-[309/382]',
110
+ ownShape && 'aspect-[250/309]',
111
+ !masked && !ownShape && 'aspect-square rounded',
112
+ )}
113
+ >
114
+ {item.src ? (
115
+ <img
116
+ src={item.src}
117
+ alt={item.alt || ''}
118
+ loading="lazy"
119
+ className={cn('w-full h-full', ownShape ? 'object-contain' : 'object-cover')}
120
+ style={maskCss}
121
+ />
122
+ ) : (
123
+ <div
124
+ className="w-full h-full bg-gradient-to-br from-gray-200 to-gray-300"
125
+ style={maskCss}
126
+ />
127
+ )}
128
+ </div>
129
+ ))}
130
+ </div>
131
+ ) : (
132
+ <div className="text-gray-500 text-center py-4">
133
+ Add images in the editor to populate the gallery.
134
+ </div>
135
+ )}
136
+ </section>
137
+ );
138
+ },
139
+ };
@@ -0,0 +1,59 @@
1
+ import type { ComponentConfig, Slot } from '@puckeditor/core';
2
+ import { cn } from '../../utils/widgetUtils';
3
+
4
+ export type GridWidgetProps = {
5
+ columns: '1' | '2' | '3' | '4';
6
+ gap: 'none' | 'sm' | 'md' | 'lg';
7
+ grid: Slot;
8
+ };
9
+
10
+ const COLS_CLASS: Record<GridWidgetProps['columns'], string> = {
11
+ '1': 'grid-cols-1',
12
+ '2': 'grid-cols-1 sm:grid-cols-2',
13
+ '3': 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
14
+ '4': 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
15
+ };
16
+
17
+ const GAP_CLASS: Record<GridWidgetProps['gap'], string> = {
18
+ none: 'gap-0',
19
+ sm: 'gap-2',
20
+ md: 'gap-4',
21
+ lg: 'gap-8',
22
+ };
23
+
24
+ export const GridWidget: ComponentConfig<GridWidgetProps> = {
25
+ label: 'Grid',
26
+ fields: {
27
+ columns: {
28
+ type: 'select',
29
+ label: 'Columns',
30
+ options: [
31
+ { label: '1', value: '1' },
32
+ { label: '2', value: '2' },
33
+ { label: '3', value: '3' },
34
+ { label: '4', value: '4' },
35
+ ],
36
+ },
37
+ gap: {
38
+ type: 'select',
39
+ label: 'Gap',
40
+ options: [
41
+ { label: 'None', value: 'none' },
42
+ { label: 'Small', value: 'sm' },
43
+ { label: 'Medium', value: 'md' },
44
+ { label: 'Large', value: 'lg' },
45
+ ],
46
+ },
47
+ grid: { type: 'slot' },
48
+ },
49
+ defaultProps: {
50
+ columns: '3',
51
+ gap: 'md',
52
+ grid: [],
53
+ },
54
+ render: ({ columns, gap, grid: Grid }) => (
55
+ <div className={cn('grid', COLS_CLASS[columns], GAP_CLASS[gap])}>
56
+ <Grid />
57
+ </div>
58
+ ),
59
+ };
@@ -0,0 +1,292 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createCheckboxField, createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { cn } from '../../utils/widgetUtils';
4
+ import { RichTextBlock } from './_internal/rich-text';
5
+ import {
6
+ BackgroundMedia,
7
+ ContentMedia,
8
+ CTAButton,
9
+ EyebrowText,
10
+ Heading,
11
+ maskStyle,
12
+ Section,
13
+ } from './_shared';
14
+
15
+ export type HeroWidgetProps = {
16
+ eyebrow: string;
17
+ title: string;
18
+ subtitle: string;
19
+ logoUrl: string;
20
+ logoAlt: string;
21
+ imageUrl: string;
22
+ imageAlt: string;
23
+ /**
24
+ * Organic mask (SVG url, from the tenant's content) applied to the side
25
+ * image in the right/left layouts. Empty = the standard rounded media.
26
+ */
27
+ imageMaskUrl?: string;
28
+ /**
29
+ * "native" renders supplied transparent artwork exactly as-is — no mask, no
30
+ * crop — for a photo already cut to its final organic shape. Preferred over
31
+ * `imageMaskUrl`, which re-derives the silhouette from a hand-kept SVG.
32
+ */
33
+ imageShape?: 'rounded' | 'native';
34
+ imagePosition: 'background' | 'right' | 'left';
35
+ primaryLabel: string;
36
+ primaryHref: string;
37
+ secondaryLabel: string;
38
+ secondaryHref: string;
39
+ overlay: boolean;
40
+ align: 'left' | 'center';
41
+ surface: 'plain' | 'card';
42
+ };
43
+
44
+ // Exported so CenteredHeroWidget can render through it without smuggling
45
+ // Puck's ComponentConfig.render signature past the type system.
46
+ export function renderHero({
47
+ eyebrow,
48
+ title,
49
+ subtitle,
50
+ logoUrl,
51
+ logoAlt,
52
+ imageUrl,
53
+ imageAlt,
54
+ imageMaskUrl = '',
55
+ imageShape = 'rounded',
56
+ imagePosition,
57
+ primaryLabel,
58
+ primaryHref,
59
+ secondaryLabel,
60
+ secondaryHref,
61
+ overlay,
62
+ align,
63
+ surface,
64
+ }: HeroWidgetProps) {
65
+ // A brand-logo lockup (e.g. a script wordmark) can stand in for the plain
66
+ // text title; the title then only feeds screen readers.
67
+ // Both axes are capped so a tenant can pin the lockup to its Figma box
68
+ // (BioGarden: 371x268 on the background hero, 445x214 on the side-image
69
+ // one). `--pb-hero-logo-max-w` defaults to 100%, i.e. the previous
70
+ // `max-w-full`; the side layout's tokens fall back to the background ones.
71
+ const logoImg = (side: boolean) =>
72
+ logoUrl ? (
73
+ <h1 className={align === 'center' ? 'flex justify-center' : 'flex'}>
74
+ <img
75
+ src={logoUrl}
76
+ alt={logoAlt || title}
77
+ className={
78
+ side
79
+ ? 'max-h-[var(--pb-hero-side-logo-max-h,var(--pb-hero-logo-max-h,14rem))] w-auto max-w-[var(--pb-hero-side-logo-max-w,var(--pb-hero-logo-max-w,100%))] object-contain'
80
+ : 'max-h-[var(--pb-hero-logo-max-h,14rem)] w-auto max-w-[var(--pb-hero-logo-max-w,100%)] object-contain'
81
+ }
82
+ />
83
+ </h1>
84
+ ) : null;
85
+ const heading = logoImg(false);
86
+
87
+ if (imagePosition === 'background') {
88
+ const content = (
89
+ <>
90
+ <EyebrowText tone="inverse">{eyebrow}</EyebrowText>
91
+ {heading ?? <Heading as="h1" size="xl" tone="inverse" text={title} align={align} accent />}
92
+ {/* Size/weight/opacity are themeable: BioGarden's Figma sets the hero
93
+ lede in Body 20M (Medium, full white); the fallbacks keep the
94
+ previous regular-weight 90%-white 20px for other tenants. */}
95
+ {subtitle && (
96
+ <RichTextBlock
97
+ text={subtitle}
98
+ className="mt-6 text-[length:var(--pb-hero-subtitle-size,1.25rem)] font-[number:var(--pb-hero-subtitle-weight,400)] leading-relaxed text-white opacity-[var(--pb-hero-subtitle-opacity,0.9)]"
99
+ />
100
+ )}
101
+ <div
102
+ className={cn(
103
+ 'flex flex-wrap gap-4 mt-8',
104
+ align === 'center' ? 'justify-center' : 'justify-start',
105
+ )}
106
+ >
107
+ <CTAButton label={primaryLabel} href={primaryHref} variant="inverse-primary" />
108
+ <CTAButton label={secondaryLabel} href={secondaryHref} variant="inverse-outline" />
109
+ </div>
110
+ </>
111
+ );
112
+ return (
113
+ <section
114
+ className="relative isolate flex min-h-[var(--pb-hero-min-h,560px)] items-center overflow-hidden bg-slate-900 text-white"
115
+ // `--pb-hero-pull` (default 0) lets a full-bleed hero slide up under
116
+ // a normal-flow floating header (the pill nav) so the photo shows
117
+ // through behind it, as in the BioGarden Figma.
118
+ style={{ marginTop: 'var(--pb-hero-pull, 0px)' }}
119
+ >
120
+ <BackgroundMedia
121
+ imageUrl={imageUrl}
122
+ imageAlt={imageAlt}
123
+ overlay={overlay ? 'dark' : 'none'}
124
+ fallback="gradient-mesh"
125
+ objectPosition="var(--pb-hero-image-position, 50% 50%)"
126
+ />
127
+ <div className="container mx-auto px-4 py-20 sm:px-6 lg:px-8 lg:py-28">
128
+ <div
129
+ className={cn(
130
+ 'relative max-w-3xl text-white',
131
+ align === 'center' ? 'mx-auto text-center' : '',
132
+ )}
133
+ >
134
+ {surface === 'card' ? (
135
+ <div className="bg-black/60 p-8 sm:p-12 rounded-lg">{content}</div>
136
+ ) : (
137
+ content
138
+ )}
139
+ </div>
140
+ </div>
141
+ </section>
142
+ );
143
+ }
144
+
145
+ return (
146
+ // The side-image hero's top/bottom rhythm is its own: the Figma sets the
147
+ // artwork 152px below the page top (i.e. 52px under the 100px-tall header)
148
+ // and 100px above the next section, which the symmetric section padding
149
+ // can't express. Section emits BOTH `py-*` and `lg:py-*`, and the `lg:`
150
+ // rule is emitted after the unprefixed one, so an unprefixed `pt-*` alone
151
+ // silently loses at desktop — the override has to be stated for both
152
+ // breakpoints. Unset, every token resolves to Section's own padding, so
153
+ // tenants without them are untouched.
154
+ <Section
155
+ variant="default"
156
+ spacing="loose"
157
+ className="pt-[var(--pb-hero-side-pt,var(--pb-section-py,4rem))] pb-[var(--pb-hero-side-pb,var(--pb-section-py,4rem))] lg:pt-[var(--pb-hero-side-pt,var(--pb-section-py,6rem))] lg:pb-[var(--pb-hero-side-pb,var(--pb-section-py,6rem))]"
158
+ >
159
+ <div
160
+ className={cn(
161
+ 'flex flex-col lg:flex-row items-center gap-12',
162
+ imagePosition === 'left' ? 'lg:flex-row-reverse' : '',
163
+ )}
164
+ >
165
+ <div className="flex-1">
166
+ <EyebrowText>{eyebrow}</EyebrowText>
167
+ {logoImg(true) ?? <Heading as="h1" size="xl" text={title} accent />}
168
+ {subtitle && (
169
+ <RichTextBlock
170
+ text={subtitle}
171
+ className="mt-6 text-xl leading-relaxed"
172
+ style={{ color: 'var(--pb-body-color)' }}
173
+ />
174
+ )}
175
+ {/* Both CTAButtons render null when their label is blank, but the
176
+ row itself still contributed its `mt-8`, pushing the column's
177
+ content up by half that — enough to visibly break the Figma's
178
+ "logo vertically centred on the image" in the side layout. */}
179
+ {(primaryLabel || secondaryLabel) && (
180
+ <div className="flex flex-wrap gap-4 mt-8">
181
+ <CTAButton label={primaryLabel} href={primaryHref} variant="primary" />
182
+ <CTAButton label={secondaryLabel} href={secondaryHref} variant="secondary" />
183
+ </div>
184
+ )}
185
+ </div>
186
+ <div className="flex-1">
187
+ {imageShape === 'native' && imageUrl ? (
188
+ // Supplied cut-out: rendered exactly as delivered, capped to the
189
+ // Figma's box width. No mask, no crop — the file IS the shape.
190
+ <img
191
+ src={imageUrl}
192
+ alt={imageAlt || title}
193
+ loading="lazy"
194
+ className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)]"
195
+ />
196
+ ) : imageMaskUrl && imageUrl ? (
197
+ // `--pb-hero-media-max-w` / `--pb-hero-media-aspect` pin the
198
+ // organic cut-out to the Figma's box (BioGarden: 545x481);
199
+ // unset, the image keeps its previous free-flowing size.
200
+ <img
201
+ src={imageUrl}
202
+ alt={imageAlt || title}
203
+ loading="lazy"
204
+ className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)] object-cover"
205
+ style={{
206
+ ...maskStyle(imageMaskUrl),
207
+ aspectRatio: 'var(--pb-hero-media-aspect, auto)',
208
+ }}
209
+ />
210
+ ) : (
211
+ <ContentMedia imageUrl={imageUrl} imageAlt={imageAlt} aspect="square" />
212
+ )}
213
+ </div>
214
+ </div>
215
+ </Section>
216
+ );
217
+ }
218
+
219
+ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
220
+ label: 'Hero',
221
+ fields: {
222
+ eyebrow: { type: 'text', label: 'Eyebrow (optional)' },
223
+ title: { type: 'text', label: 'Title' },
224
+ subtitle: { type: 'textarea', label: 'Subtitle' },
225
+ logoUrl: createImageField(mediaLibraryAdapter, 'Logo image (replaces title, optional)'),
226
+ logoAlt: { type: 'text', label: 'Logo alt text' },
227
+ imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
228
+ imageAlt: { type: 'text', label: 'Image alt text' },
229
+ imageMaskUrl: {
230
+ type: 'text',
231
+ label: 'Image mask URL (organic shape, side layouts — optional)',
232
+ },
233
+ imageShape: {
234
+ type: 'select',
235
+ label: 'Image shape (side layouts)',
236
+ options: [
237
+ { label: 'Rounded', value: 'rounded' },
238
+ { label: "Image's own shape (transparent artwork)", value: 'native' },
239
+ ],
240
+ },
241
+ imagePosition: {
242
+ type: 'select',
243
+ label: 'Image position',
244
+ options: [
245
+ { label: 'Background', value: 'background' },
246
+ { label: 'Right', value: 'right' },
247
+ { label: 'Left', value: 'left' },
248
+ ],
249
+ },
250
+ primaryLabel: { type: 'text', label: 'Primary button text' },
251
+ primaryHref: { type: 'text', label: 'Primary button link' },
252
+ secondaryLabel: { type: 'text', label: 'Secondary button text' },
253
+ secondaryHref: { type: 'text', label: 'Secondary button link' },
254
+ overlay: createCheckboxField('Dark overlay (background only)'),
255
+ align: {
256
+ type: 'select',
257
+ label: 'Alignment',
258
+ options: [
259
+ { label: 'Left', value: 'left' },
260
+ { label: 'Center', value: 'center' },
261
+ ],
262
+ },
263
+ surface: {
264
+ type: 'select',
265
+ label: 'Surface (background variant)',
266
+ options: [
267
+ { label: 'Plain', value: 'plain' },
268
+ { label: 'Card overlay', value: 'card' },
269
+ ],
270
+ },
271
+ },
272
+ defaultProps: {
273
+ eyebrow: '',
274
+ title: 'Open knowledge, *mapped*.',
275
+ subtitle: "Discover and contribute to the world's largest community-maintained gazetteer.",
276
+ logoUrl: '',
277
+ logoAlt: '',
278
+ imageUrl: '',
279
+ imageAlt: '',
280
+ imageMaskUrl: '',
281
+ imageShape: 'rounded',
282
+ imagePosition: 'background',
283
+ primaryLabel: 'Get started',
284
+ primaryHref: '#',
285
+ secondaryLabel: 'Learn more',
286
+ secondaryHref: '#',
287
+ overlay: true,
288
+ align: 'center',
289
+ surface: 'plain',
290
+ },
291
+ render: renderHero,
292
+ };
@@ -0,0 +1,42 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { EmptyPlaceholder } from './_shared';
3
+
4
+ export type HtmlWidgetProps = {
5
+ html: string;
6
+ height: string;
7
+ };
8
+
9
+ export const HtmlWidget: ComponentConfig<HtmlWidgetProps> = {
10
+ label: 'Raw HTML',
11
+ fields: {
12
+ html: {
13
+ type: 'textarea',
14
+ label: 'HTML (rendered in sandboxed iframe)',
15
+ },
16
+ height: {
17
+ type: 'text',
18
+ label: 'Height (e.g. 400px)',
19
+ },
20
+ },
21
+ defaultProps: {
22
+ html: '<p>Edit me…</p>',
23
+ height: '400px',
24
+ },
25
+ render: ({ html, height }) =>
26
+ html?.trim() ? (
27
+ <div className="container mx-auto py-6">
28
+ <iframe
29
+ title="Embedded HTML"
30
+ className="w-full rounded-lg border bg-white"
31
+ // Stored blocks may omit height (Puck's public Render does not
32
+ // merge defaultProps) — fall back to the default instead of
33
+ // letting the iframe collapse to the UA default ~150px.
34
+ style={{ height: height || '400px' }}
35
+ sandbox="allow-same-origin"
36
+ srcDoc={html}
37
+ />
38
+ </div>
39
+ ) : (
40
+ <EmptyPlaceholder label="Html (no content)" />
41
+ ),
42
+ };