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,44 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createCheckboxField } from '../../fields';
3
+ import { EmptyPlaceholder } from './_shared';
4
+
5
+ export type IframeWidgetProps = {
6
+ src: string;
7
+ title: string;
8
+ height: string;
9
+ sandboxed: boolean;
10
+ };
11
+
12
+ export const IframeWidget: ComponentConfig<IframeWidgetProps> = {
13
+ label: 'Iframe',
14
+ fields: {
15
+ src: { type: 'text', label: 'Source URL' },
16
+ title: { type: 'text', label: 'Title (accessibility)' },
17
+ height: { type: 'text', label: 'Height (e.g. 600px)' },
18
+ sandboxed: createCheckboxField('Sandbox (block popups & navigation)'),
19
+ },
20
+ defaultProps: {
21
+ src: '',
22
+ title: 'Embedded content',
23
+ height: '600px',
24
+ sandboxed: true,
25
+ },
26
+ render: ({ src, title, height, sandboxed }) =>
27
+ src ? (
28
+ <div className="container mx-auto py-6">
29
+ <iframe
30
+ src={src}
31
+ title={title || 'Embedded content'}
32
+ className="w-full rounded-lg border"
33
+ style={{ height }}
34
+ // NOTE: allow-scripts + allow-same-origin lets a SAME-ORIGIN embedded
35
+ // document remove its own sandbox; the protection is meaningful for
36
+ // cross-origin embeds (the primary use). Do not "simplify" this set
37
+ // or the label without revisiting that trade-off.
38
+ sandbox={sandboxed ? 'allow-scripts allow-same-origin allow-forms' : undefined}
39
+ />
40
+ </div>
41
+ ) : (
42
+ <EmptyPlaceholder label="Iframe (no source)" />
43
+ ),
44
+ };
@@ -0,0 +1,203 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { cn } from '../../utils/widgetUtils';
4
+ import { AccentText } from './_internal/accent-text';
5
+ import { renderRichText } from './_internal/rich-text';
6
+
7
+ export type LeaderboardRow = {
8
+ rank: string;
9
+ name: string;
10
+ score: string;
11
+ avatarUrl?: string;
12
+ };
13
+
14
+ export type LeaderboardWidgetProps = {
15
+ eyebrow: string;
16
+ title: string;
17
+ surface: 'default' | 'navy';
18
+ top: { name: string; score: string; avatarUrl: string }[];
19
+ rows: LeaderboardRow[];
20
+ };
21
+
22
+ // Inline icons keep the shared widget dependency-free.
23
+ function CrownIcon() {
24
+ return (
25
+ <svg viewBox="0 0 24 24" fill="#f5b40a" className="size-6 shrink-0" aria-hidden="true">
26
+ <title>Top rank</title>
27
+ <path d="M5 16 3 6l5 4 4-6 4 6 5-4-2 10H5Zm0 2h14v2H5v-2Z" />
28
+ </svg>
29
+ );
30
+ }
31
+
32
+ function UserGlyph() {
33
+ return (
34
+ <svg viewBox="0 0 24 24" fill="#ffffff" className="size-1/2" aria-hidden="true">
35
+ <title>User</title>
36
+ <path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4 0-7 2.2-7 5v1h14v-1c0-2.8-3-5-7-5Z" />
37
+ </svg>
38
+ );
39
+ }
40
+
41
+ // Avatar: photo when supplied, else a pink disc with a white user glyph
42
+ // (matches the Figma's #f1708a fallback avatars).
43
+ function Avatar({ src, size }: { src?: string; size: string }) {
44
+ if (src) {
45
+ return <img src={src} alt="" className={cn('shrink-0 rounded-full object-cover', size)} />;
46
+ }
47
+ return (
48
+ <span
49
+ className={cn('flex shrink-0 items-center justify-center rounded-full bg-[#f1708a]', size)}
50
+ >
51
+ <UserGlyph />
52
+ </span>
53
+ );
54
+ }
55
+
56
+ export const LeaderboardWidget: ComponentConfig<LeaderboardWidgetProps> = {
57
+ label: 'Leaderboard',
58
+ fields: {
59
+ eyebrow: { type: 'text', label: 'Eyebrow' },
60
+ title: { type: 'text', label: 'Title' },
61
+ surface: {
62
+ type: 'select',
63
+ label: 'Surface',
64
+ options: [
65
+ { label: 'Default', value: 'default' },
66
+ { label: 'Navy band', value: 'navy' },
67
+ ],
68
+ },
69
+ top: {
70
+ type: 'array',
71
+ label: 'Podium (top 3)',
72
+ arrayFields: {
73
+ name: { type: 'text', label: 'Name' },
74
+ score: { type: 'text', label: 'Score' },
75
+ avatarUrl: createImageField(mediaLibraryAdapter, 'Avatar image'),
76
+ },
77
+ defaultItemProps: { name: 'Player', score: '0', avatarUrl: '' },
78
+ min: 1,
79
+ max: 3,
80
+ },
81
+ rows: {
82
+ type: 'array',
83
+ label: 'Ranked rows',
84
+ arrayFields: {
85
+ rank: { type: 'text', label: 'Rank' },
86
+ name: { type: 'text', label: 'Name' },
87
+ score: { type: 'text', label: 'Score' },
88
+ avatarUrl: createImageField(mediaLibraryAdapter, 'Avatar image'),
89
+ },
90
+ defaultItemProps: {
91
+ rank: '1',
92
+ name: 'Player',
93
+ score: '0',
94
+ avatarUrl: '',
95
+ },
96
+ min: 1,
97
+ max: 20,
98
+ },
99
+ },
100
+ defaultProps: {
101
+ eyebrow: '',
102
+ title: 'Leaderboard',
103
+ surface: 'navy',
104
+ top: [
105
+ { name: 'username', score: '1737', avatarUrl: '' },
106
+ { name: 'username', score: '1706', avatarUrl: '' },
107
+ { name: 'username', score: '924', avatarUrl: '' },
108
+ ],
109
+ rows: Array.from({ length: 7 }, (_, i) => ({
110
+ rank: String(i + 4),
111
+ name: 'username',
112
+ score: '—',
113
+ })),
114
+ },
115
+ render: (props) => <LeaderboardView {...props} />,
116
+ };
117
+
118
+ // Presentational leaderboard — shared by the static base widget (above) and the
119
+ // app's live, data-driven override, so both render pixel-for-pixel identically.
120
+ export function LeaderboardView({ eyebrow, title, surface, top, rows }: LeaderboardWidgetProps) {
121
+ const navy = surface === 'navy';
122
+ return (
123
+ <section className="py-[var(--pb-section-py,3rem)]">
124
+ <div
125
+ className={cn(
126
+ 'container mx-auto rounded-[12px] px-6 py-10 sm:px-10 sm:py-12',
127
+ navy
128
+ ? 'bg-[var(--secondary,#42547e)] text-white'
129
+ : 'bg-[var(--pb-surface-muted,#f3f3f4)]',
130
+ )}
131
+ >
132
+ {eyebrow && (
133
+ <p className="text-sm font-semibold uppercase tracking-wider opacity-80">
134
+ {renderRichText(eyebrow)}
135
+ </p>
136
+ )}
137
+ {/* AccentText, not bare renderRichText: this is a display heading on
138
+ `--pb-display-weight`, where a plain <strong>'s relative `bolder`
139
+ can compute to no visible change. */}
140
+ {title && (
141
+ <h2
142
+ className="leading-tight"
143
+ style={{
144
+ fontWeight: 'var(--pb-display-weight)',
145
+ fontFamily: 'var(--pb-display-font)',
146
+ fontSize: 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 1.4vw, 2.5rem))',
147
+ color: navy ? '#ffffff' : 'var(--pb-heading-color)',
148
+ }}
149
+ >
150
+ <AccentText text={title} />
151
+ </h2>
152
+ )}
153
+ <div className="mt-8 grid items-start gap-6 lg:grid-cols-[minmax(0,5fr)_7fr]">
154
+ {/* Podium — three stacked cards */}
155
+ <ol className="flex flex-col gap-4">
156
+ {top.map((p, idx) => (
157
+ <li
158
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
159
+ key={idx}
160
+ className="flex items-center gap-4 rounded-[12px] border border-[#dcdcdf]/40 px-4 py-3"
161
+ >
162
+ <Avatar src={p.avatarUrl || undefined} size="size-16" />
163
+ <div className="min-w-0 flex-1">
164
+ <p className="truncate text-sm font-semibold">{p.name}</p>
165
+ <p className="flex items-center gap-2 text-3xl font-semibold leading-tight">
166
+ {p.score}
167
+ {idx === 0 && <CrownIcon />}
168
+ </p>
169
+ </div>
170
+ <span className="grid size-12 shrink-0 place-items-center rounded-full border-2 border-white text-xl font-semibold">
171
+ {idx + 1}
172
+ </span>
173
+ </li>
174
+ ))}
175
+ </ol>
176
+
177
+ {/* Ranked table */}
178
+ <div className="overflow-hidden rounded-[12px]">
179
+ <div className="grid grid-cols-[1fr_auto_auto] gap-x-6 bg-white px-6 py-4 text-sm text-[#161728] sm:gap-x-10">
180
+ <span>User</span>
181
+ <span className="w-16 text-center">Rank</span>
182
+ <span className="w-16 text-right">Score</span>
183
+ </div>
184
+ {rows.map((r, i) => (
185
+ <div
186
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
187
+ key={i}
188
+ className="grid grid-cols-[1fr_auto_auto] items-center gap-x-6 bg-[#f3f3f4] px-6 py-3 text-[#161728] sm:gap-x-10"
189
+ >
190
+ <div className="flex min-w-0 items-center gap-3">
191
+ <Avatar src={r.avatarUrl || undefined} size="size-10" />
192
+ <span className="truncate text-sm font-medium">{r.name}</span>
193
+ </div>
194
+ <span className="w-16 text-center text-sm font-medium">{r.rank}</span>
195
+ <span className="w-16 text-right text-sm font-semibold">{r.score}</span>
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </section>
202
+ );
203
+ }
@@ -0,0 +1,56 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { renderRichText } from './_internal/rich-text';
3
+
4
+ export type ListWidgetProps = {
5
+ type: 'ul' | 'ol';
6
+ items: string;
7
+ };
8
+
9
+ export const ListWidget: ComponentConfig<ListWidgetProps> = {
10
+ label: 'List',
11
+ fields: {
12
+ type: {
13
+ type: 'select',
14
+ label: 'List type',
15
+ options: [
16
+ { label: 'Unordered', value: 'ul' },
17
+ { label: 'Ordered', value: 'ol' },
18
+ ],
19
+ },
20
+ items: {
21
+ type: 'textarea',
22
+ label: 'Items (one per line)',
23
+ },
24
+ },
25
+ defaultProps: {
26
+ type: 'ul',
27
+ items: 'First item\nSecond item\nThird item',
28
+ },
29
+ render: ({ type, items }) => {
30
+ const list = items
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .filter(Boolean);
34
+ const className =
35
+ type === 'ol' ? 'list-decimal list-inside space-y-1' : 'list-disc list-inside space-y-1';
36
+ return (
37
+ <div className="container mx-auto px-4 py-2">
38
+ {type === 'ol' ? (
39
+ <ol className={`${className} max-w-3xl mx-auto`}>
40
+ {list.map((item, idx) => (
41
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
42
+ <li key={idx}>{renderRichText(item)}</li>
43
+ ))}
44
+ </ol>
45
+ ) : (
46
+ <ul className={`${className} max-w-3xl mx-auto`}>
47
+ {list.map((item, idx) => (
48
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
49
+ <li key={idx}>{renderRichText(item)}</li>
50
+ ))}
51
+ </ul>
52
+ )}
53
+ </div>
54
+ );
55
+ },
56
+ };
@@ -0,0 +1,127 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+
3
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
4
+ import { EyebrowSplitSection } from './_internal/eyebrow-split-section';
5
+ import { renderRichText } from './_internal/rich-text';
6
+
7
+ export type LogoCloudItem = {
8
+ src: string;
9
+ alt: string;
10
+ href: string;
11
+ };
12
+
13
+ export type LogoCloudWidgetProps = {
14
+ title: string;
15
+ eyebrow: string;
16
+ heading: string;
17
+ variant: 'inline' | 'grid';
18
+ items: LogoCloudItem[];
19
+ };
20
+
21
+ export const LogoCloudWidget: ComponentConfig<LogoCloudWidgetProps> = {
22
+ label: 'Logo cloud (partners / funders)',
23
+ fields: {
24
+ title: { type: 'text', label: 'Title' },
25
+ eyebrow: { type: 'text', label: 'Eyebrow (enables section layout)' },
26
+ heading: { type: 'text', label: 'Heading (section layout)' },
27
+ variant: {
28
+ type: 'select',
29
+ label: 'Layout',
30
+ options: [
31
+ { label: 'Inline strip', value: 'inline' },
32
+ { label: 'Grid', value: 'grid' },
33
+ ],
34
+ },
35
+ items: {
36
+ type: 'array',
37
+ label: 'Logos',
38
+ arrayFields: {
39
+ src: createImageField(mediaLibraryAdapter, 'Image'),
40
+ alt: { type: 'text', label: 'Alt text' },
41
+ href: { type: 'text', label: 'Link (optional)' },
42
+ },
43
+ defaultItemProps: { src: '', alt: 'Partner logo', href: '' },
44
+ min: 1,
45
+ max: 24,
46
+ },
47
+ },
48
+ defaultProps: {
49
+ title: 'Funded by',
50
+ eyebrow: '',
51
+ heading: '',
52
+ variant: 'inline',
53
+ items: [
54
+ { src: '', alt: 'Partner 1', href: '' },
55
+ { src: '', alt: 'Partner 2', href: '' },
56
+ { src: '', alt: 'Partner 3', href: '' },
57
+ { src: '', alt: 'Partner 4', href: '' },
58
+ ],
59
+ },
60
+ render: ({ title, eyebrow = '', heading = '', variant = 'inline', items }) => {
61
+ if (eyebrow || heading) {
62
+ return (
63
+ <EyebrowSplitSection eyebrow={eyebrow} heading={heading}>
64
+ <div
65
+ className={
66
+ variant === 'grid'
67
+ ? 'grid grid-cols-2 items-center gap-x-10 gap-y-8 sm:grid-cols-3 lg:grid-cols-4'
68
+ : 'flex flex-wrap items-center gap-x-10 gap-y-6'
69
+ }
70
+ >
71
+ {items?.map((logo, idx) => (
72
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
73
+ <LogoCloudItem key={idx} logo={logo} />
74
+ ))}
75
+ </div>
76
+ </EyebrowSplitSection>
77
+ );
78
+ }
79
+ return (
80
+ // data-pb-logo-* hooks let a tenant restyle the strip from its theme
81
+ // stylesheet (GCA: greyscale, spread edge-to-edge, bottom keyline).
82
+ <section
83
+ data-pb-logo-strip=""
84
+ className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] text-center sm:px-6 lg:px-8"
85
+ >
86
+ {title && (
87
+ <p
88
+ data-pb-logo-label=""
89
+ className="text-xs font-semibold uppercase tracking-[0.2em] mb-6"
90
+ style={{ color: 'var(--pb-body-color)' }}
91
+ >
92
+ {renderRichText(title)}
93
+ </p>
94
+ )}
95
+ <div
96
+ data-pb-logo-row=""
97
+ className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6"
98
+ >
99
+ {items?.map((logo, idx) => (
100
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
101
+ <LogoCloudItem key={idx} logo={logo} />
102
+ ))}
103
+ </div>
104
+ </section>
105
+ );
106
+ },
107
+ };
108
+
109
+ function LogoCloudItem({ logo }: { logo: LogoCloudItem }) {
110
+ const img = logo.src ? (
111
+ <img
112
+ src={logo.src}
113
+ alt={logo.alt}
114
+ loading="lazy"
115
+ className="h-10 w-auto opacity-70 hover:opacity-100 transition-opacity"
116
+ />
117
+ ) : (
118
+ <div className="h-10 w-32 rounded bg-gray-100" aria-hidden="true" />
119
+ );
120
+ return logo.href ? (
121
+ <a href={logo.href} className="block" aria-label={logo.alt}>
122
+ {img}
123
+ </a>
124
+ ) : (
125
+ <div>{img}</div>
126
+ );
127
+ }
@@ -0,0 +1,148 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { useMemo } from 'react';
3
+ import { parseMarkdownBlocks as renderMarkdown } from '../../utils/markdown';
4
+ import { renderRichText } from './_internal/rich-text';
5
+
6
+ export type MarkdownWidgetProps = {
7
+ content: string;
8
+ /**
9
+ * Text column width. "auto" is the legacy centred `max-w-4xl`; "660" is the
10
+ * Figma's measure for the legal and event pages.
11
+ */
12
+ width?: 'auto' | '660';
13
+ /**
14
+ * Left placement on the 12-column page grid. "none" keeps the centred
15
+ * column; "4" starts the text at the 4th column, as the Figma does.
16
+ */
17
+ indent?: 'none' | '4';
18
+ };
19
+
20
+ // Emphasis tokenization is shared with the markdown→HTML converter; links are
21
+ // layered on top of it by renderRichText, which is also what the FAQ answers
22
+ // use — before that, `[label](href)` rendered here as literal text.
23
+ const renderInline = renderRichText;
24
+
25
+ export const MarkdownWidget: ComponentConfig<MarkdownWidgetProps> = {
26
+ label: 'Markdown',
27
+ fields: {
28
+ width: {
29
+ type: 'select',
30
+ label: 'Text width',
31
+ options: [
32
+ { label: 'Auto (centred)', value: 'auto' },
33
+ { label: '660px', value: '660' },
34
+ ],
35
+ },
36
+ indent: {
37
+ type: 'select',
38
+ label: 'Grid indent',
39
+ options: [
40
+ { label: 'None', value: 'none' },
41
+ { label: '4th column', value: '4' },
42
+ ],
43
+ },
44
+ content: {
45
+ type: 'textarea',
46
+ label: 'Markdown content',
47
+ },
48
+ },
49
+ defaultProps: {
50
+ width: 'auto',
51
+ indent: 'none',
52
+ content: '# Heading\n\nBody paragraph text.\n\n## Subheading\n\n- First item\n- Second item',
53
+ },
54
+ render: (props) => <MarkdownRender {...props} />,
55
+ };
56
+
57
+ function MarkdownRender({ content, width = 'auto', indent = 'none' }: MarkdownWidgetProps) {
58
+ const blocks = useMemo(() => renderMarkdown(content || ''), [content]);
59
+ const indented = indent === '4';
60
+ return (
61
+ <div
62
+ className={`container mx-auto px-4 py-8${indented ? ' lg:grid lg:grid-cols-12 lg:gap-6' : ''}`}
63
+ >
64
+ <div
65
+ className={[
66
+ // `--pb-prose-link-color` lets a tenant colour links (BioGarden's
67
+ // Figma uses the brand purple); unset, links keep the prose default.
68
+ 'prose prose-lg dark:prose-invert break-words [&_a]:text-[color:var(--pb-prose-link-color,inherit)]',
69
+ // The last block's own `mb-4` sits BELOW the text and stacks on top
70
+ // of this section's bottom padding, so the visible gap to whatever
71
+ // follows is 16px wider than the padding says. A tenant measuring
72
+ // exact gaps off a Figma (BioGarden: 48px text→image, 40px
73
+ // text→CTA) sets `--pb-prose-trailing-mb: 0` to make the section
74
+ // padding the whole gap; the 1rem fallback matches every block
75
+ // type's own mb-4 (h3's mb-3 is the one exception — content ending
76
+ // on a bare h3 gains 4px).
77
+ '[&>*:last-child]:mb-[var(--pb-prose-trailing-mb,1rem)]',
78
+ width === '660' ? 'max-w-[660px]' : 'max-w-4xl',
79
+ // Indented text starts at the 4th grid column instead of centring.
80
+ indented ? 'lg:col-start-4 lg:col-span-9' : 'mx-auto',
81
+ ].join(' ')}
82
+ >
83
+ {blocks.map((block, idx) => {
84
+ if (block.type === 'h1')
85
+ return (
86
+ <h1
87
+ // biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional slices of one string, re-derived each render
88
+ key={idx}
89
+ // `**key words**` must read as semibold against the light
90
+ // display weight. `<strong>`'s default `bolder` is RELATIVE,
91
+ // so on a 300-weight heading it resolves to 400 and the
92
+ // emphasis is invisible — state the weight explicitly.
93
+ // `leading-[calc(2.5/2.25)]` restores what `text-4xl` bundles:
94
+ // Tailwind's font-size utilities also set a line-height, and
95
+ // `text-[length:…]` sets ONLY the size. The `prose` classes
96
+ // can't cover for it — @tailwindcss/typography is not
97
+ // installed, so they compile to nothing. Unitless keeps the
98
+ // old 2.25rem/2.5rem ratio exactly while scaling with a
99
+ // tenant's larger heading. Size first, leading second (the
100
+ // reverse order is silently dropped by tailwind-merge).
101
+ className="text-[length:var(--pb-prose-h1-size,2.25rem)] leading-[calc(2.5/2.25)] font-light mb-4 [&_strong]:font-semibold"
102
+ >
103
+ {renderInline(block.text)}
104
+ </h1>
105
+ );
106
+ if (block.type === 'h2')
107
+ return (
108
+ // Token-sized like the H1 above: the fallback reproduces the
109
+ // previous `text-3xl` (size AND its bundled 1.2 line-height),
110
+ // while a pack can scale prose H2s per breakpoint — without
111
+ // this, a mobile pack that shrinks the H1 leaves section H2s
112
+ // LARGER than the page title (BioGarden QA, SUB-36).
113
+ <h2
114
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
115
+ key={idx}
116
+ className="text-[length:var(--pb-prose-h2-size,1.875rem)] leading-[1.2] font-light mt-8 mb-4"
117
+ >
118
+ {renderInline(block.text)}
119
+ </h2>
120
+ );
121
+ if (block.type === 'h3')
122
+ return (
123
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
124
+ <h3 key={idx} className="text-2xl font-semibold mt-6 mb-3">
125
+ {renderInline(block.text)}
126
+ </h3>
127
+ );
128
+ if (block.type === 'ul')
129
+ return (
130
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
131
+ <ul key={idx} className="list-disc list-inside space-y-1 mb-4">
132
+ {block.items.map((item, i) => (
133
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
134
+ <li key={i}>{renderInline(item)}</li>
135
+ ))}
136
+ </ul>
137
+ );
138
+ return (
139
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
140
+ <p key={idx} className="leading-relaxed mb-4">
141
+ {renderInline(block.text)}
142
+ </p>
143
+ );
144
+ })}
145
+ </div>
146
+ </div>
147
+ );
148
+ }