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