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,182 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import type { CSSProperties } from 'react';
3
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
4
+ import { cn } from '../../utils/widgetUtils';
5
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
6
+ import { TextLink } from './_shared/text-link';
7
+
8
+ // Hoisted heading style — constant tokens only. The fallback clamp reproduces
9
+ // the prior `text-2xl sm:text-3xl lg:text-4xl` steps: 24px on phones, 30px at
10
+ // the 640px breakpoint, 36px at 1024px.
11
+ const HEADING_STYLE: CSSProperties = {
12
+ fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
13
+ letterSpacing: 'var(--pb-display-tracking)',
14
+ fontFamily: 'var(--pb-display-font)',
15
+ color: 'var(--pb-heading-color)',
16
+ fontSize: 'var(--pb-heading-lg, clamp(1.5rem, 1.25rem + 1.5625vw, 2.25rem))',
17
+ };
18
+
19
+ export type StoryCardItem = {
20
+ imageUrl: string;
21
+ imageAlt: string;
22
+ eyebrow: string;
23
+ date: string;
24
+ title: string;
25
+ href: string;
26
+ };
27
+
28
+ export type StoryCardsWidgetProps = {
29
+ title: string;
30
+ subtitle: string;
31
+ viewAllLabel: string;
32
+ viewAllHref: string;
33
+ columns: '2' | '3' | '4';
34
+ items: StoryCardItem[];
35
+ };
36
+
37
+ const COLS_CLASS: Record<StoryCardsWidgetProps['columns'], string> = {
38
+ '2': 'sm:grid-cols-2',
39
+ '3': 'sm:grid-cols-2 lg:grid-cols-3',
40
+ '4': 'sm:grid-cols-2 lg:grid-cols-4',
41
+ };
42
+
43
+ export const StoryCardsWidget: ComponentConfig<StoryCardsWidgetProps> = {
44
+ label: 'Story cards (image with overlay text)',
45
+ fields: {
46
+ title: { type: 'text', label: 'Title' },
47
+ subtitle: { type: 'textarea', label: 'Subtitle' },
48
+ viewAllLabel: { type: 'text', label: 'View-all link label' },
49
+ viewAllHref: { type: 'text', label: 'View-all link URL' },
50
+ columns: {
51
+ type: 'select',
52
+ label: 'Columns (desktop)',
53
+ options: [
54
+ { label: '2', value: '2' },
55
+ { label: '3', value: '3' },
56
+ { label: '4', value: '4' },
57
+ ],
58
+ },
59
+ items: {
60
+ type: 'array',
61
+ label: 'Stories',
62
+ arrayFields: {
63
+ imageUrl: createImageField(mediaLibraryAdapter, 'Image URL'),
64
+ imageAlt: { type: 'text', label: 'Image alt text' },
65
+ eyebrow: { type: 'text', label: 'Eyebrow / tag' },
66
+ date: { type: 'text', label: 'Date' },
67
+ title: { type: 'text', label: 'Title' },
68
+ href: { type: 'text', label: 'Link' },
69
+ },
70
+ defaultItemProps: {
71
+ imageUrl: '',
72
+ imageAlt: '',
73
+ eyebrow: 'Story',
74
+ date: '',
75
+ title: 'Story title',
76
+ href: '#',
77
+ },
78
+ min: 1,
79
+ max: 12,
80
+ },
81
+ },
82
+ defaultProps: {
83
+ title: 'Get inspired',
84
+ subtitle: '',
85
+ viewAllLabel: '',
86
+ viewAllHref: '',
87
+ columns: '3',
88
+ items: [
89
+ {
90
+ imageUrl: '',
91
+ imageAlt: '',
92
+ eyebrow: 'Story',
93
+ date: '',
94
+ title: 'Hear first-hand from a Farmer Cluster',
95
+ href: '#',
96
+ },
97
+ {
98
+ imageUrl: '',
99
+ imageAlt: '',
100
+ eyebrow: 'Story',
101
+ date: '',
102
+ title: "Inside Europe's biggest cluster network",
103
+ href: '#',
104
+ },
105
+ {
106
+ imageUrl: '',
107
+ imageAlt: '',
108
+ eyebrow: 'Story',
109
+ date: '',
110
+ title: 'From farm to landscape: a journey',
111
+ href: '#',
112
+ },
113
+ ],
114
+ },
115
+ render: ({ title, subtitle, viewAllLabel, viewAllHref, columns, items }) => (
116
+ <section className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
117
+ {(title || subtitle || (viewAllLabel && viewAllHref)) && (
118
+ <div className="mb-8 flex items-end justify-between gap-4">
119
+ <div>
120
+ {title && (
121
+ <h2 className="leading-[var(--pb-heading-leading,1.15)]" style={HEADING_STYLE}>
122
+ {renderRichText(title)}
123
+ </h2>
124
+ )}
125
+ {subtitle && (
126
+ <RichTextBlock
127
+ text={subtitle}
128
+ className="mt-3 max-w-3xl text-lg"
129
+ style={{ color: 'var(--pb-body-color)' }}
130
+ />
131
+ )}
132
+ </div>
133
+ {viewAllLabel && viewAllHref && <TextLink label={viewAllLabel} href={viewAllHref} />}
134
+ </div>
135
+ )}
136
+ <div className={cn('grid gap-4 grid-cols-1', COLS_CLASS[columns])}>
137
+ {items?.map((item, idx) => (
138
+ <a
139
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
140
+ key={idx}
141
+ href={item.href || '#'}
142
+ className="group relative block aspect-[4/5] overflow-hidden rounded-xl"
143
+ >
144
+ {item.imageUrl ? (
145
+ <img
146
+ src={item.imageUrl}
147
+ alt={item.imageAlt || item.title}
148
+ loading="lazy"
149
+ className="absolute inset-0 size-full object-cover transition-transform duration-300 group-hover:scale-105"
150
+ />
151
+ ) : (
152
+ <div className="absolute inset-0 bg-gradient-to-br from-primary-700 to-primary-900" />
153
+ )}
154
+ <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
155
+ {item.eyebrow && (
156
+ <span
157
+ data-pb-story-tag=""
158
+ className="absolute left-4 top-4 inline-flex w-fit items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
159
+ style={{
160
+ backgroundColor: 'var(--pb-card-tag-bg, var(--pb-accent))',
161
+ color: 'var(--pb-card-tag-color, var(--pb-accent-foreground))',
162
+ }}
163
+ >
164
+ {renderRichText(item.eyebrow, { allowLinks: false })}
165
+ </span>
166
+ )}
167
+ <div className="absolute inset-0 flex flex-col justify-end p-5 text-white">
168
+ {item.date && (
169
+ <span className="text-xs text-white/80">
170
+ {renderRichText(item.date, { allowLinks: false })}
171
+ </span>
172
+ )}
173
+ <h3 className="mt-1 text-lg font-medium leading-snug">
174
+ {renderRichText(item.title, { allowLinks: false })}
175
+ </h3>
176
+ </div>
177
+ </a>
178
+ ))}
179
+ </div>
180
+ </section>
181
+ ),
182
+ };
@@ -0,0 +1,86 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { renderRichText } from './_internal/rich-text';
3
+
4
+ export type TableWidgetProps = {
5
+ caption: string;
6
+ headers: string;
7
+ rows: string;
8
+ };
9
+
10
+ function parseDelimited(value: string): string[] {
11
+ return value
12
+ .split('|')
13
+ .map((cell) => cell.trim())
14
+ .filter((cell) => cell.length > 0);
15
+ }
16
+
17
+ export const TableWidget: ComponentConfig<TableWidgetProps> = {
18
+ label: 'Table',
19
+ fields: {
20
+ caption: { type: 'text', label: 'Caption (accessibility)' },
21
+ headers: { type: 'text', label: 'Headers (pipe-separated)' },
22
+ rows: {
23
+ type: 'textarea',
24
+ label: 'Rows (one per line, columns pipe-separated)',
25
+ },
26
+ },
27
+ defaultProps: {
28
+ caption: '',
29
+ headers: 'Name | Role | Email',
30
+ rows: 'Jane Doe | Director | jane@example.com\nJohn Smith | Manager | john@example.com',
31
+ },
32
+ render: ({ caption, headers, rows }) => {
33
+ const headerList = parseDelimited(headers);
34
+ const rowList = rows
35
+ .split(/\r?\n/)
36
+ .map((line) => line.trim())
37
+ .filter(Boolean)
38
+ .map(parseDelimited);
39
+
40
+ if (headerList.length === 0 && rowList.length === 0) {
41
+ return (
42
+ <div className="text-gray-500 text-center py-4">Add headers and rows in the editor.</div>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <div className="container mx-auto py-6 overflow-x-auto">
48
+ <table className="w-full border-collapse text-sm">
49
+ {caption && (
50
+ <caption className="text-sm text-gray-600 dark:text-gray-400 pb-3">
51
+ {renderRichText(caption)}
52
+ </caption>
53
+ )}
54
+ {headerList.length > 0 && (
55
+ <thead>
56
+ <tr className="bg-gray-50 dark:bg-gray-800">
57
+ {headerList.map((header, idx) => (
58
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
59
+ <th key={idx} scope="col" className="text-left font-semibold px-4 py-3 border-b">
60
+ {renderRichText(header)}
61
+ </th>
62
+ ))}
63
+ </tr>
64
+ </thead>
65
+ )}
66
+ <tbody>
67
+ {rowList.map((row, ridx) => (
68
+ <tr
69
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
70
+ key={ridx}
71
+ className="border-b last:border-b-0 hover:bg-gray-50 dark:hover:bg-gray-900"
72
+ >
73
+ {row.map((cell, cidx) => (
74
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
75
+ <td key={cidx} className="px-4 py-3">
76
+ {renderRichText(cell)}
77
+ </td>
78
+ ))}
79
+ </tr>
80
+ ))}
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ );
85
+ },
86
+ };
@@ -0,0 +1,95 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { useId, useState } from 'react';
3
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
4
+
5
+ export type TabItem = {
6
+ label: string;
7
+ content: string;
8
+ };
9
+
10
+ export type TabsWidgetProps = {
11
+ items: TabItem[];
12
+ };
13
+
14
+ function TabsRenderer({ items }: TabsWidgetProps) {
15
+ const [active, setActive] = useState(0);
16
+ const baseId = useId();
17
+ if (!items || items.length === 0) {
18
+ return (
19
+ <div className="text-gray-500 text-center py-4">
20
+ Add tabs in the editor to populate this section.
21
+ </div>
22
+ );
23
+ }
24
+ const safeIndex = Math.min(active, items.length - 1);
25
+ return (
26
+ <div className="container mx-auto max-w-3xl py-8">
27
+ <div className="border-b flex flex-wrap gap-2" role="tablist" aria-label="Tabs">
28
+ {items.map((item, idx) => {
29
+ const isActive = idx === safeIndex;
30
+ const tabId = `${baseId}-tab-${idx}`;
31
+ const panelId = `${baseId}-panel-${idx}`;
32
+ return (
33
+ <button
34
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
35
+ key={idx}
36
+ id={tabId}
37
+ type="button"
38
+ role="tab"
39
+ aria-selected={isActive}
40
+ aria-controls={panelId}
41
+ tabIndex={isActive ? 0 : -1}
42
+ onClick={() => {
43
+ if (idx !== safeIndex) setActive(idx);
44
+ }}
45
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
46
+ isActive
47
+ ? 'border-primary-700 text-primary-700 dark:text-primary-400 dark:border-primary-300'
48
+ : 'border-transparent text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
49
+ }`}
50
+ >
51
+ {item.label ? renderRichText(item.label, { allowLinks: false }) : `Tab ${idx + 1}`}
52
+ </button>
53
+ );
54
+ })}
55
+ </div>
56
+ <div
57
+ id={`${baseId}-panel-${safeIndex}`}
58
+ role="tabpanel"
59
+ aria-labelledby={`${baseId}-tab-${safeIndex}`}
60
+ className="pt-4"
61
+ >
62
+ {items[safeIndex]?.content && (
63
+ <RichTextBlock
64
+ text={items[safeIndex].content}
65
+ className="leading-relaxed whitespace-pre-line"
66
+ />
67
+ )}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export const TabsWidget: ComponentConfig<TabsWidgetProps> = {
74
+ label: 'Tabs',
75
+ fields: {
76
+ items: {
77
+ type: 'array',
78
+ label: 'Tabs',
79
+ arrayFields: {
80
+ label: { type: 'text', label: 'Label' },
81
+ content: { type: 'textarea', label: 'Content' },
82
+ },
83
+ defaultItemProps: { label: 'Tab', content: 'Tab content...' },
84
+ min: 1,
85
+ max: 12,
86
+ },
87
+ },
88
+ defaultProps: {
89
+ items: [
90
+ { label: 'Overview', content: 'Overview content...' },
91
+ { label: 'Details', content: 'Details content...' },
92
+ ],
93
+ },
94
+ render: ({ items }) => <TabsRenderer items={items || []} />,
95
+ };
@@ -0,0 +1,85 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { cn } from '../../utils/widgetUtils';
3
+ import { renderRichText } from './_internal/rich-text';
4
+
5
+ export type TagItem = {
6
+ label: string;
7
+ href: string;
8
+ active: boolean;
9
+ };
10
+
11
+ export type TagsWidgetProps = {
12
+ label: string;
13
+ items: TagItem[];
14
+ };
15
+
16
+ export const TagsWidget: ComponentConfig<TagsWidgetProps> = {
17
+ label: 'Tags (pill row)',
18
+ fields: {
19
+ label: { type: 'text', label: 'Aria label' },
20
+ items: {
21
+ type: 'array',
22
+ label: 'Tags',
23
+ arrayFields: {
24
+ label: { type: 'text', label: 'Label' },
25
+ href: { type: 'text', label: 'Link' },
26
+ active: {
27
+ type: 'radio',
28
+ label: 'Active',
29
+ options: [
30
+ { label: 'No', value: false },
31
+ { label: 'Yes', value: true },
32
+ ],
33
+ },
34
+ },
35
+ defaultItemProps: { label: 'Tag', href: '#', active: false },
36
+ min: 1,
37
+ max: 50,
38
+ },
39
+ },
40
+ defaultProps: {
41
+ label: 'Filters',
42
+ items: [
43
+ { label: 'All', href: '#', active: true },
44
+ { label: 'English', href: '#', active: false },
45
+ { label: 'Français', href: '#', active: false },
46
+ ],
47
+ },
48
+ render: ({ label, items }) =>
49
+ !items || items.length === 0 ? (
50
+ <div className="text-gray-500 text-center py-4">Add tags in the editor.</div>
51
+ ) : (
52
+ <nav aria-label={label || 'Tags'} className="container mx-auto px-4 py-4 sm:px-6 lg:px-8">
53
+ <ul className="flex flex-wrap gap-2">
54
+ {items.map((tag, idx) => (
55
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
56
+ <li key={idx}>
57
+ <a
58
+ href={tag.href || '#'}
59
+ aria-current={tag.active ? 'page' : undefined}
60
+ className={cn(
61
+ 'inline-flex items-center gap-2 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors',
62
+ tag.active
63
+ ? 'border-primary-700 bg-primary-50 text-primary-800'
64
+ : 'border-gray-200 text-gray-700 hover:border-primary-300 hover:bg-primary-50',
65
+ )}
66
+ >
67
+ {/* Radio-style indicator (filled when active) — matches the
68
+ GCA filter pills. */}
69
+ <span
70
+ aria-hidden="true"
71
+ className={cn(
72
+ 'flex size-3.5 shrink-0 items-center justify-center rounded-full border',
73
+ tag.active ? 'border-primary-700' : 'border-gray-300',
74
+ )}
75
+ >
76
+ {tag.active && <span className="size-1.5 rounded-full bg-primary-700" />}
77
+ </span>
78
+ {renderRichText(tag.label, { allowLinks: false })}
79
+ </a>
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ </nav>
84
+ ),
85
+ };
@@ -0,0 +1,102 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
3
+ import { renderRichText } from './_internal/rich-text';
4
+
5
+ export type TestimonialItem = {
6
+ quote: string;
7
+ name: string;
8
+ role: string;
9
+ avatarUrl: string;
10
+ };
11
+
12
+ export type TestimonialWidgetProps = {
13
+ title: string;
14
+ items: TestimonialItem[];
15
+ };
16
+
17
+ export const TestimonialWidget: ComponentConfig<TestimonialWidgetProps> = {
18
+ label: 'Testimonials',
19
+ fields: {
20
+ title: { type: 'text', label: 'Title' },
21
+ items: {
22
+ type: 'array',
23
+ label: 'Testimonials',
24
+ arrayFields: {
25
+ quote: { type: 'textarea', label: 'Quote' },
26
+ name: { type: 'text', label: 'Name' },
27
+ role: { type: 'text', label: 'Role' },
28
+ avatarUrl: createImageField(mediaLibraryAdapter, 'Avatar URL'),
29
+ },
30
+ defaultItemProps: {
31
+ quote: 'Great product!',
32
+ name: 'Jane Doe',
33
+ role: 'CEO at Acme',
34
+ avatarUrl: '',
35
+ },
36
+ min: 1,
37
+ max: 12,
38
+ },
39
+ },
40
+ defaultProps: {
41
+ title: 'What our customers say',
42
+ items: [
43
+ {
44
+ quote: 'This changed how we work.',
45
+ name: 'Jane Doe',
46
+ role: 'CEO at Acme',
47
+ avatarUrl: '',
48
+ },
49
+ {
50
+ quote: 'Highly recommended.',
51
+ name: 'John Smith',
52
+ role: 'Founder at Beta',
53
+ avatarUrl: '',
54
+ },
55
+ ],
56
+ },
57
+ render: ({ title, items }) => (
58
+ <div className="container mx-auto px-4 py-16">
59
+ {title && (
60
+ <h2 className="text-3xl sm:text-4xl lg:text-5xl font-light text-center mb-12">
61
+ {renderRichText(title)}
62
+ </h2>
63
+ )}
64
+ <div className="grid gap-6 lg:grid-cols-2">
65
+ {items?.map((item, idx) => (
66
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
67
+ <figure key={idx} className="rounded-2xl border bg-white dark:bg-gray-900 p-8 shadow-sm">
68
+ <svg
69
+ className="size-8 text-primary-400"
70
+ fill="currentColor"
71
+ viewBox="0 0 32 32"
72
+ aria-hidden="true"
73
+ >
74
+ <title>Quote</title>
75
+ <path d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z" />
76
+ </svg>
77
+ <blockquote className="mt-4 text-lg leading-relaxed">
78
+ {renderRichText(item.quote)}
79
+ </blockquote>
80
+ <figcaption className="mt-6 flex items-center gap-4">
81
+ {item.avatarUrl ? (
82
+ <img
83
+ src={item.avatarUrl}
84
+ alt={item.name}
85
+ className="size-12 rounded-full object-cover"
86
+ />
87
+ ) : (
88
+ <div className="size-12 rounded-full bg-gradient-to-br from-gray-200 to-gray-300" />
89
+ )}
90
+ <div>
91
+ <div className="font-semibold">{renderRichText(item.name)}</div>
92
+ <div className="text-sm text-gray-500 dark:text-gray-400">
93
+ {renderRichText(item.role)}
94
+ </div>
95
+ </div>
96
+ </figcaption>
97
+ </figure>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ ),
102
+ };
@@ -0,0 +1,73 @@
1
+ /** Timeline types and marker geometry. Split out so the widget's field
2
+ * definitions and its render can import them without a cycle, and so all
3
+ * three stay under the 300-line cap. */
4
+
5
+ export type TimelineItem = {
6
+ marker: string;
7
+ title: string;
8
+ body: string;
9
+ /**
10
+ * Trailing emphasis line under the body (the design's bold-italic date, e.g.
11
+ * "September & Oktober 2026"). Kept separate from `body` so it can sit on its
12
+ * own line rather than running into the sentence before it.
13
+ */
14
+ meta?: string;
15
+ /** Blobs variant only: CSS color filling the organic blob. */
16
+ color?: string;
17
+ /**
18
+ * Blobs variant only: per-entry organic mask (SVG url), overriding the
19
+ * section-wide `blobMaskUrl`. The BioGarden Figma draws each milestone as a
20
+ * DIFFERENT shape, so one mask for the whole row was visibly wrong.
21
+ */
22
+ shapeMaskUrl?: string;
23
+ /**
24
+ * Supplied transparent artwork for the blob, used INSTEAD of
25
+ * `shapeMaskUrl` + `color`: the file carries the shape AND its fill, and the
26
+ * entry's copy is laid out over it. Preferred — a mask re-derives the
27
+ * silhouette from a hand-kept SVG and drifts from the design.
28
+ */
29
+ shapeImageUrl?: string;
30
+ };
31
+ export type TimelineWidgetProps = {
32
+ eyebrow: string;
33
+ title: string;
34
+ /** "muted" wraps the section in a soft rounded card (Mowing "Incentives"). */
35
+ surface?: 'default' | 'muted';
36
+ /**
37
+ * "rows" is the default vertical-list rendering. "blobs" lays the entries
38
+ * out as a row of organic coloured shapes (BioGarden "Projekt-Fahrplan").
39
+ */
40
+ variant?: 'rows' | 'blobs';
41
+ /** Blobs variant: organic mask (SVG url) clipped over each blob. */
42
+ blobMaskUrl?: string;
43
+ /**
44
+ * Blobs variant: organic mask for the marker tag. Set it and the marker
45
+ * renders as a tilted blob overhanging the shape's top-left (the BioGarden
46
+ * Figma); leave it blank and the marker stays the inline centered pill.
47
+ */
48
+ markerMaskUrl?: string;
49
+ /** Supplied transparent artwork for the marker tag (replaces mask + colour). */
50
+ markerImageUrl?: string;
51
+ /** Blobs variant: CSS color filling the marker blob (Figma: brand yellow). */
52
+ markerColor?: string;
53
+ items: TimelineItem[];
54
+ };
55
+
56
+ /**
57
+ * Marker-blob geometry, as fractions of the blob box, re-measured off the
58
+ * BioGarden Figma (blob 415x343 at 59,4097; marker AABB 133.26x117.6 at
59
+ * 48.88,**4094** — so the tag straddles the blob's upper-left edge, well clear
60
+ * of the copy). The previous 27.4% top came from a mis-read y and dropped the
61
+ * tag onto the entry title. The tag art is drawn tilted in the design and its
62
+ * label stays upright, so the mask and the text are separate layers over one
63
+ * shared centering box.
64
+ */
65
+ export const MARKER_BLOB = {
66
+ /** Center of the marker, relative to the blob box. */
67
+ left: '13.6%',
68
+ top: '16.3%',
69
+ /** Unrotated width; the box keeps the tag vector's own aspect (no squash). */
70
+ width: '27%',
71
+ aspect: '159.547 / 116.348',
72
+ rotation: '-20.12deg',
73
+ } as const;