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.
- pagebuilder/__init__.py +0 -0
- pagebuilder/components/DiffSummary.tsx +107 -0
- pagebuilder/components/ScheduledBadge.tsx +34 -0
- pagebuilder/components/StatusBadge.tsx +21 -0
- pagebuilder/components/blockRegistry.test.ts +56 -0
- pagebuilder/components/blockRegistry.ts +80 -0
- pagebuilder/components/blocks/Button.tsx +142 -0
- pagebuilder/components/blocks/Columns.tsx +64 -0
- pagebuilder/components/blocks/Heading.tsx +92 -0
- pagebuilder/components/blocks/Image.tsx +125 -0
- pagebuilder/components/blocks/MediaPicker.tsx +178 -0
- pagebuilder/components/blocks/Spacer.tsx +31 -0
- pagebuilder/components/blocks/Text.tsx +80 -0
- pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
- pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
- pagebuilder/components/editor/SchedulePanel.tsx +85 -0
- pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
- pagebuilder/components/layoutPuckConfig.tsx +109 -0
- pagebuilder/components/media/MediaFilters.tsx +83 -0
- pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
- pagebuilder/components/media/MediaGrid.tsx +76 -0
- pagebuilder/components/media/MediaHeader.tsx +37 -0
- pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
- pagebuilder/components/media/types.ts +20 -0
- pagebuilder/components/migrateContent.test.ts +146 -0
- pagebuilder/components/migrateContent.ts +135 -0
- pagebuilder/components/puckConfig.test.tsx +118 -0
- pagebuilder/components/puckConfig.tsx +121 -0
- pagebuilder/components/widgetCatalog.test.tsx +123 -0
- pagebuilder/components/widgetCatalog.ts +208 -0
- pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
- pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
- pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
- pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
- pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
- pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
- pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
- pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
- pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
- pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
- pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
- pagebuilder/components/widgets/_shared/heading.tsx +88 -0
- pagebuilder/components/widgets/_shared/index.ts +39 -0
- pagebuilder/components/widgets/_shared/mask.ts +54 -0
- pagebuilder/components/widgets/_shared/section.tsx +60 -0
- pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
- pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
- pagebuilder/components/widgets/accordion-widget.tsx +64 -0
- pagebuilder/components/widgets/alert-widget.tsx +47 -0
- pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
- pagebuilder/components/widgets/article-cards-render.tsx +132 -0
- pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
- pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
- pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
- pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
- pagebuilder/components/widgets/carousel-widget.tsx +97 -0
- pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
- pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
- pagebuilder/components/widgets/column-widget.tsx +63 -0
- pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
- pagebuilder/components/widgets/contact-form-render.tsx +293 -0
- pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
- pagebuilder/components/widgets/container-widget.tsx +93 -0
- pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
- pagebuilder/components/widgets/divider-widget.tsx +56 -0
- pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
- pagebuilder/components/widgets/faq-render.tsx +268 -0
- pagebuilder/components/widgets/faq-widget.tsx +64 -0
- pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
- pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
- pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
- pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
- pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
- pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
- pagebuilder/components/widgets/gallery-widget.tsx +139 -0
- pagebuilder/components/widgets/grid-widget.tsx +59 -0
- pagebuilder/components/widgets/hero-widget.tsx +292 -0
- pagebuilder/components/widgets/html-widget.tsx +42 -0
- pagebuilder/components/widgets/iframe-widget.tsx +44 -0
- pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
- pagebuilder/components/widgets/list-widget.tsx +56 -0
- pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
- pagebuilder/components/widgets/markdown-widget.tsx +148 -0
- pagebuilder/components/widgets/media-object-layout.ts +156 -0
- pagebuilder/components/widgets/media-object-render.tsx +284 -0
- pagebuilder/components/widgets/media-object-text.tsx +202 -0
- pagebuilder/components/widgets/media-object-widget.tsx +176 -0
- pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
- pagebuilder/components/widgets/page-header-widget.tsx +118 -0
- pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
- pagebuilder/components/widgets/quote-widget.tsx +51 -0
- pagebuilder/components/widgets/row-widget.tsx +74 -0
- pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
- pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
- pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
- pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
- pagebuilder/components/widgets/site-header-nav.tsx +153 -0
- pagebuilder/components/widgets/site-header-widget.tsx +182 -0
- pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
- pagebuilder/components/widgets/stats-widget.tsx +200 -0
- pagebuilder/components/widgets/steps-widget.tsx +211 -0
- pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
- pagebuilder/components/widgets/table-widget.tsx +86 -0
- pagebuilder/components/widgets/tabs-widget.tsx +95 -0
- pagebuilder/components/widgets/tags-widget.tsx +85 -0
- pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
- pagebuilder/components/widgets/timeline-layout.ts +73 -0
- pagebuilder/components/widgets/timeline-render.tsx +294 -0
- pagebuilder/components/widgets/timeline-widget.tsx +106 -0
- pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
- pagebuilder/components/widgets/video-widget.tsx +103 -0
- pagebuilder/components/widgets/welcome-widget.tsx +38 -0
- pagebuilder/contracts/__init__.py +57 -0
- pagebuilder/contracts/events.py +25 -0
- pagebuilder/contracts/schemas.py +239 -0
- pagebuilder/conversion/blocks-to-html.test.ts +105 -0
- pagebuilder/conversion/blocks-to-html.ts +102 -0
- pagebuilder/conversion/index.ts +2 -0
- pagebuilder/conversion/types.ts +5 -0
- pagebuilder/deps.py +26 -0
- pagebuilder/diff.py +129 -0
- pagebuilder/endpoints/__init__.py +0 -0
- pagebuilder/endpoints/api/__init__.py +21 -0
- pagebuilder/endpoints/api/_deps.py +21 -0
- pagebuilder/endpoints/api/layout.py +78 -0
- pagebuilder/endpoints/api/pages.py +85 -0
- pagebuilder/endpoints/api/revisions.py +79 -0
- pagebuilder/endpoints/api/uploads.py +92 -0
- pagebuilder/endpoints/api/workflow.py +124 -0
- pagebuilder/endpoints/seo.py +95 -0
- pagebuilder/endpoints/views.py +228 -0
- pagebuilder/fields/checkbox-field.tsx +22 -0
- pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
- pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
- pagebuilder/fields/file-picker/index.ts +6 -0
- pagebuilder/fields/file-picker/types.ts +15 -0
- pagebuilder/fields/index.ts +9 -0
- pagebuilder/fields/media-adapter.ts +47 -0
- pagebuilder/hooks/useAutosave.ts +134 -0
- pagebuilder/hooks/useEditorForm.ts +193 -0
- pagebuilder/hooks/useMediaUploads.ts +96 -0
- pagebuilder/hooks/usePageRevisions.ts +59 -0
- pagebuilder/hooks/usePageSchedule.ts +72 -0
- pagebuilder/hooks/usePageWorkflow.ts +173 -0
- pagebuilder/layout_service.py +101 -0
- pagebuilder/media_images.py +185 -0
- pagebuilder/media_queries.py +92 -0
- pagebuilder/media_service.py +222 -0
- pagebuilder/models.py +209 -0
- pagebuilder/module.py +279 -0
- pagebuilder/package.json +18 -0
- pagebuilder/pages/LayoutEditor.tsx +183 -0
- pagebuilder/pages/MediaLibrary.tsx +249 -0
- pagebuilder/pages/PageEditor.tsx +181 -0
- pagebuilder/pages/PageList.tsx +129 -0
- pagebuilder/pages/PendingReview.tsx +128 -0
- pagebuilder/pages/PublicPage.tsx +126 -0
- pagebuilder/permissions.py +44 -0
- pagebuilder/py.typed +0 -0
- pagebuilder/security.py +230 -0
- pagebuilder/service/__init__.py +141 -0
- pagebuilder/service/_common.py +24 -0
- pagebuilder/service/_revisions.py +92 -0
- pagebuilder/service/_workflow.py +207 -0
- pagebuilder/services.py +19 -0
- pagebuilder/settings.py +161 -0
- pagebuilder/static/widgets-base.css +57 -0
- pagebuilder/translation/apply.ts +54 -0
- pagebuilder/translation/extract.ts +141 -0
- pagebuilder/translation/index.ts +20 -0
- pagebuilder/translation/translatable-fields.ts +65 -0
- pagebuilder/translation/translation.test.ts +167 -0
- pagebuilder/utils/api.ts +12 -0
- pagebuilder/utils/datetime.ts +25 -0
- pagebuilder/utils/decorative-image.ts +15 -0
- pagebuilder/utils/editorSnapshot.ts +39 -0
- pagebuilder/utils/markdown.ts +160 -0
- pagebuilder/utils/mediaApi.ts +112 -0
- pagebuilder/utils/mediaFormat.ts +19 -0
- pagebuilder/utils/pagesApi.ts +111 -0
- pagebuilder/utils/parse-list.ts +7 -0
- pagebuilder/utils/request.ts +52 -0
- pagebuilder/utils/slugify.ts +9 -0
- pagebuilder/utils/types.ts +171 -0
- pagebuilder/utils/widgetUtils.ts +11 -0
- simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
- simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
- simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
- 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;
|