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,258 @@
|
|
|
1
|
+
/** FeatureCards render. Icon map, types and style tables live in
|
|
2
|
+
* feature-cards-styles.ts so this file stays under the 300-line cap. */
|
|
3
|
+
|
|
4
|
+
/** FeatureCardsWidget types, helpers and render — split from feature-cards-widget.tsx,
|
|
5
|
+
* which keeps the field definitions, so both stay under the 300-line cap. */
|
|
6
|
+
|
|
7
|
+
import { ArrowRight, ArrowUpRight } from 'lucide-react';
|
|
8
|
+
import type { CSSProperties } from 'react';
|
|
9
|
+
import { cn } from '../../utils/widgetUtils';
|
|
10
|
+
import { AccentText } from './_internal/accent-text';
|
|
11
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
12
|
+
import {
|
|
13
|
+
ChartAreaSolid,
|
|
14
|
+
MagnifyingGlassChartSolid,
|
|
15
|
+
PawSolid,
|
|
16
|
+
type SolidIconProps,
|
|
17
|
+
TemperatureSolid,
|
|
18
|
+
TreeSolid,
|
|
19
|
+
WaterSolid,
|
|
20
|
+
} from './_internal/solid-icons';
|
|
21
|
+
import { TextLink } from './_shared/text-link';
|
|
22
|
+
|
|
23
|
+
// Named glyphs selectable per card — solid icons matching the GCA design
|
|
24
|
+
// system "Icons" page (SNAG_009/SNAG_014). Keep the set small and meaningful;
|
|
25
|
+
// unknown names fall back to a plain coloured badge.
|
|
26
|
+
|
|
27
|
+
export type { FeatureCardItem, FeatureCardsWidgetProps } from './feature-cards-styles';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
CARD_BODY_STYLE,
|
|
31
|
+
CARD_TAG_STYLE,
|
|
32
|
+
CARD_TITLE_STYLE,
|
|
33
|
+
COLS_CLASS,
|
|
34
|
+
DISPLAY_STYLE,
|
|
35
|
+
EYEBROW_STYLE,
|
|
36
|
+
type FeatureCardsWidgetProps,
|
|
37
|
+
HEADING_LG_STYLE,
|
|
38
|
+
ICON_MAP,
|
|
39
|
+
} from './feature-cards-styles';
|
|
40
|
+
|
|
41
|
+
export const FeatureCardsWidgetRender = ({
|
|
42
|
+
eyebrow,
|
|
43
|
+
title,
|
|
44
|
+
subtitle,
|
|
45
|
+
linkLabel,
|
|
46
|
+
linkHref,
|
|
47
|
+
cardLinkLabel = 'Learn more',
|
|
48
|
+
surface = 'default',
|
|
49
|
+
cardSurface = 'default',
|
|
50
|
+
columns,
|
|
51
|
+
items,
|
|
52
|
+
}: FeatureCardsWidgetProps) => {
|
|
53
|
+
const hasHeaderLink = Boolean(linkLabel && linkHref);
|
|
54
|
+
const hasSide = Boolean(eyebrow) || hasHeaderLink;
|
|
55
|
+
const panelClass =
|
|
56
|
+
surface === 'muted' ? 'rounded-3xl bg-[#f3f3f4] px-6 py-12 sm:px-10 lg:px-12' : '';
|
|
57
|
+
const cardClass =
|
|
58
|
+
cardSurface === 'muted'
|
|
59
|
+
? 'group flex flex-col rounded-xl bg-[#f3f3f4] p-6 transition-shadow hover:shadow-md'
|
|
60
|
+
: 'group flex flex-col rounded-xl border border-[var(--border,#e4e6e7)] bg-white p-6 transition-shadow hover:shadow-lg dark:bg-gray-900';
|
|
61
|
+
const headerLink = hasHeaderLink ? <TextLink label={linkLabel} href={linkHref} /> : null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section className="container mx-auto px-4 py-16 sm:px-6 lg:px-8">
|
|
65
|
+
<div className={panelClass}>
|
|
66
|
+
{(title || subtitle || hasSide) &&
|
|
67
|
+
(hasSide ? (
|
|
68
|
+
<div className="mb-10 grid items-center gap-4 sm:grid-cols-[1fr_auto_1fr]">
|
|
69
|
+
<span className="font-medium" style={EYEBROW_STYLE}>
|
|
70
|
+
{renderRichText(eyebrow)}
|
|
71
|
+
</span>
|
|
72
|
+
<div className="mx-auto max-w-3xl text-center">
|
|
73
|
+
{title && (
|
|
74
|
+
<h2 className="leading-[var(--pb-heading-leading,1.1)]" style={HEADING_LG_STYLE}>
|
|
75
|
+
{renderRichText(title)}
|
|
76
|
+
</h2>
|
|
77
|
+
)}
|
|
78
|
+
{subtitle && (
|
|
79
|
+
<RichTextBlock
|
|
80
|
+
text={subtitle}
|
|
81
|
+
className="mt-3 text-lg"
|
|
82
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="sm:justify-self-end">{headerLink}</div>
|
|
87
|
+
</div>
|
|
88
|
+
) : (
|
|
89
|
+
<div className="text-center mb-10 max-w-3xl mx-auto">
|
|
90
|
+
{title && (
|
|
91
|
+
<h2 className="text-3xl sm:text-4xl lg:text-5xl" style={DISPLAY_STYLE}>
|
|
92
|
+
{renderRichText(title)}
|
|
93
|
+
</h2>
|
|
94
|
+
)}
|
|
95
|
+
{subtitle && (
|
|
96
|
+
<RichTextBlock
|
|
97
|
+
text={subtitle}
|
|
98
|
+
className="mt-3 text-lg"
|
|
99
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
{items && items.length > 0 ? (
|
|
105
|
+
<div className={cn('grid gap-6 grid-cols-1', COLS_CLASS[columns])}>
|
|
106
|
+
{items.map((item, idx) => {
|
|
107
|
+
const Icon = item.icon ? ICON_MAP[item.icon] : undefined;
|
|
108
|
+
const linked = Boolean(item.href);
|
|
109
|
+
const colored = Boolean(item.cardBg);
|
|
110
|
+
return (
|
|
111
|
+
<a
|
|
112
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
113
|
+
key={idx}
|
|
114
|
+
href={item.href || '#'}
|
|
115
|
+
className={
|
|
116
|
+
colored
|
|
117
|
+
? 'group flex min-h-[var(--pb-card-min-h,0)] flex-col rounded-xl p-6 transition-shadow hover:shadow-lg'
|
|
118
|
+
: cardClass
|
|
119
|
+
}
|
|
120
|
+
style={colored ? { backgroundColor: item.cardBg } : undefined}
|
|
121
|
+
>
|
|
122
|
+
<div className="mb-4 flex items-start justify-between gap-2">
|
|
123
|
+
{item.iconUrl ? (
|
|
124
|
+
<img
|
|
125
|
+
src={item.iconUrl}
|
|
126
|
+
alt={item.iconAlt || ''}
|
|
127
|
+
loading="lazy"
|
|
128
|
+
className={
|
|
129
|
+
colored
|
|
130
|
+
? 'h-36 w-auto max-w-[70%] object-contain object-left'
|
|
131
|
+
: 'size-12 object-contain'
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<div
|
|
136
|
+
className="flex size-12 items-center justify-center rounded-lg"
|
|
137
|
+
style={{
|
|
138
|
+
backgroundColor:
|
|
139
|
+
item.iconBg || (Icon ? 'var(--pb-accent)' : 'var(--pb-surface-muted)'),
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{Icon && <Icon className="size-6 text-white" />}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
{/* On colored cards the tag is a bold lead-in line above the
|
|
146
|
+
description (BioGarden's "Jetzt anmelden"), so it is
|
|
147
|
+
rendered down there rather than as a pill up here. */}
|
|
148
|
+
{item.tag && !colored && (
|
|
149
|
+
<span
|
|
150
|
+
className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
151
|
+
style={{
|
|
152
|
+
backgroundColor: 'var(--pb-surface-muted)',
|
|
153
|
+
color: 'var(--pb-heading-color)',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{renderRichText(item.tag, { allowLinks: false })}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
<h3
|
|
161
|
+
className="transition-colors"
|
|
162
|
+
style={
|
|
163
|
+
colored
|
|
164
|
+
? {
|
|
165
|
+
...CARD_TITLE_STYLE,
|
|
166
|
+
color: 'var(--pb-surface-contrast, #ffffff)',
|
|
167
|
+
}
|
|
168
|
+
: CARD_TITLE_STYLE
|
|
169
|
+
}
|
|
170
|
+
>
|
|
171
|
+
{renderRichText(item.title, { allowLinks: false })}
|
|
172
|
+
</h3>
|
|
173
|
+
{/* Colored cards put the tag, description and arrow button in
|
|
174
|
+
one bottom group — tag as a bold lead-in, copy on the left
|
|
175
|
+
(~2/3 of the card), circle bottom-right — as the BioGarden
|
|
176
|
+
Figma does. `mt-auto` sits on the GROUP, not on either
|
|
177
|
+
child: on the tag it would break tagless cards, on the row
|
|
178
|
+
it would push the copy away from the tag leading into it. */}
|
|
179
|
+
{colored ? (
|
|
180
|
+
<div className="mt-auto">
|
|
181
|
+
{item.tag && (
|
|
182
|
+
<p
|
|
183
|
+
className="pt-6 font-bold"
|
|
184
|
+
style={{
|
|
185
|
+
...CARD_TAG_STYLE,
|
|
186
|
+
color: 'var(--pb-surface-contrast, #ffffff)',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{renderRichText(item.tag, { allowLinks: false })}
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
<div className="flex items-end justify-between gap-4">
|
|
193
|
+
{item.description && (
|
|
194
|
+
<p
|
|
195
|
+
className="mt-2 max-w-[66%] leading-relaxed"
|
|
196
|
+
style={{
|
|
197
|
+
...CARD_BODY_STYLE,
|
|
198
|
+
color: 'var(--pb-surface-contrast, #ffffff)',
|
|
199
|
+
opacity: 0.85,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{/* `*word*` marks the semibold key phrase the
|
|
203
|
+
Figma uses to lead each description. Links off:
|
|
204
|
+
this sits inside the card-wide <a>. */}
|
|
205
|
+
<AccentText text={item.description} allowLinks={false} />
|
|
206
|
+
</p>
|
|
207
|
+
)}
|
|
208
|
+
{linked && (
|
|
209
|
+
// The wrapping <a> already names the link (card
|
|
210
|
+
// title), so the arrow is decorative.
|
|
211
|
+
<span
|
|
212
|
+
className="flex size-16 shrink-0 items-center justify-center rounded-full bg-[var(--pb-surface,#ffffff)]"
|
|
213
|
+
style={{
|
|
214
|
+
color: 'var(--pb-heading-color, #161728)',
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<ArrowRight className="size-6" />
|
|
218
|
+
</span>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
item.description && (
|
|
224
|
+
<p className="mt-2 leading-relaxed" style={CARD_BODY_STYLE}>
|
|
225
|
+
{/* Inline (not RichTextBlock): the whole card is one
|
|
226
|
+
<a>, so a `[label](href)` here would nest anchors. */}
|
|
227
|
+
{renderRichText(item.description, {
|
|
228
|
+
allowLinks: false,
|
|
229
|
+
})}
|
|
230
|
+
</p>
|
|
231
|
+
)
|
|
232
|
+
)}
|
|
233
|
+
{linked &&
|
|
234
|
+
(colored ? null : (
|
|
235
|
+
<span className="mt-auto inline-flex items-center gap-1 pt-6 text-sm font-medium text-[color:var(--pb-link-color,var(--pb-accent))] group-hover:text-[color:var(--pb-link-hover-color,var(--pb-link-color,var(--pb-accent)))]">
|
|
236
|
+
{renderRichText(cardLinkLabel, { allowLinks: false })}
|
|
237
|
+
<ArrowUpRight className="size-4" />
|
|
238
|
+
</span>
|
|
239
|
+
))}
|
|
240
|
+
</a>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
) : (
|
|
245
|
+
<EmptyState />
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</section>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function EmptyState() {
|
|
253
|
+
return (
|
|
254
|
+
<div className="text-center text-gray-500 py-6">
|
|
255
|
+
Add cards in the editor to populate this section.
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Icon map and style tables for the feature-cards widget. */
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { SolidIconProps } from './_internal/solid-icons';
|
|
6
|
+
import {
|
|
7
|
+
ChartAreaSolid,
|
|
8
|
+
MagnifyingGlassChartSolid,
|
|
9
|
+
PawSolid,
|
|
10
|
+
TemperatureSolid,
|
|
11
|
+
TreeSolid,
|
|
12
|
+
WaterSolid,
|
|
13
|
+
} from './_internal/solid-icons';
|
|
14
|
+
|
|
15
|
+
export const ICON_MAP: Record<string, (props: SolidIconProps) => React.JSX.Element> = {
|
|
16
|
+
chart: ChartAreaSolid,
|
|
17
|
+
search: MagnifyingGlassChartSolid,
|
|
18
|
+
thermometer: TemperatureSolid,
|
|
19
|
+
tree: TreeSolid,
|
|
20
|
+
paw: PawSolid,
|
|
21
|
+
waves: WaterSolid,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type FeatureCardItem = {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
icon: string;
|
|
28
|
+
iconBg: string;
|
|
29
|
+
iconUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Alt text for `iconUrl`. Needed when the image IS the card's visible title
|
|
32
|
+
* (BioGarden's wordmark cards leave `title` empty) — otherwise the card link
|
|
33
|
+
* is announced without its name. Empty = decorative.
|
|
34
|
+
*/
|
|
35
|
+
iconAlt?: string;
|
|
36
|
+
href: string;
|
|
37
|
+
/** Optional category label shown top-right of the card (e.g. outputs type). */
|
|
38
|
+
tag?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Optional CSS color filling the whole card (e.g. a tenant's program
|
|
41
|
+
* color). When set, the card renders with white text and the icon image
|
|
42
|
+
* becomes a larger wordmark-style lockup.
|
|
43
|
+
*/
|
|
44
|
+
cardBg?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type FeatureCardsWidgetProps = {
|
|
48
|
+
eyebrow: string;
|
|
49
|
+
title: string;
|
|
50
|
+
subtitle: string;
|
|
51
|
+
linkLabel: string;
|
|
52
|
+
linkHref: string;
|
|
53
|
+
/** Per-card link label (defaults to "Learn more"; e.g. "View output"). */
|
|
54
|
+
cardLinkLabel: string;
|
|
55
|
+
/** Section surface: plain (default) or a soft grey rounded panel. */
|
|
56
|
+
surface: 'default' | 'muted';
|
|
57
|
+
/** Card surface: white with a border (default) or a soft grey panel. */
|
|
58
|
+
cardSurface: 'default' | 'muted';
|
|
59
|
+
columns: '2' | '3' | '4';
|
|
60
|
+
items: FeatureCardItem[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const COLS_CLASS: Record<FeatureCardsWidgetProps['columns'], string> = {
|
|
64
|
+
'2': 'sm:grid-cols-2',
|
|
65
|
+
'3': 'sm:grid-cols-2 lg:grid-cols-3',
|
|
66
|
+
'4': 'sm:grid-cols-2 lg:grid-cols-4',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const DISPLAY_STYLE: CSSProperties = {
|
|
70
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
71
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
72
|
+
fontFamily: 'var(--pb-display-font)',
|
|
73
|
+
color: 'var(--pb-heading-color)',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Hoisted (not per-render) style objects — all values are constant tokens.
|
|
77
|
+
// The heading fallback clamp reproduces the prior `text-3xl sm:text-4xl`
|
|
78
|
+
// steps: 30px below 640px, 36px from the 640px breakpoint up.
|
|
79
|
+
export const HEADING_LG_STYLE: CSSProperties = {
|
|
80
|
+
...DISPLAY_STYLE,
|
|
81
|
+
fontSize: 'var(--pb-heading-lg, clamp(1.875rem, 1.3125rem + 2.34375vw, 2.25rem))',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const EYEBROW_STYLE: CSSProperties = {
|
|
85
|
+
color: 'var(--pb-body-color)',
|
|
86
|
+
fontSize: 'var(--pb-eyebrow-size, 0.875rem)',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const CARD_TITLE_STYLE: CSSProperties = {
|
|
90
|
+
fontFamily: 'var(--pb-display-font)',
|
|
91
|
+
fontWeight:
|
|
92
|
+
'var(--pb-card-title-weight, var(--pb-display-weight))' as CSSProperties['fontWeight'],
|
|
93
|
+
fontSize: 'var(--pb-card-title-size, 1.125rem)',
|
|
94
|
+
color: 'var(--pb-heading-color)',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const CARD_BODY_STYLE: CSSProperties = {
|
|
98
|
+
color: 'var(--pb-body-color)',
|
|
99
|
+
fontSize: 'var(--pb-card-body-size, 0.875rem)',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// The bold lead-in line above a coloured card's description (BioGarden's
|
|
103
|
+
// "Jetzt anmelden"). Sized by its own token so it can be the Figma's 22px
|
|
104
|
+
// without dragging the body copy with it; the fallback is the previous 1rem.
|
|
105
|
+
export const CARD_TAG_STYLE: CSSProperties = {
|
|
106
|
+
fontSize: 'var(--pb-card-tag-size, 1rem)',
|
|
107
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** FeatureCardsWidget: field definitions and config assembly.
|
|
2
|
+
* Types, helpers and render live in feature-cards-render.tsx. */
|
|
3
|
+
|
|
4
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
5
|
+
|
|
6
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
7
|
+
|
|
8
|
+
import { type FeatureCardsWidgetProps, FeatureCardsWidgetRender } from './feature-cards-render';
|
|
9
|
+
|
|
10
|
+
export type * from './feature-cards-render';
|
|
11
|
+
|
|
12
|
+
export const FeatureCardsWidget: ComponentConfig<FeatureCardsWidgetProps> = {
|
|
13
|
+
label: 'Feature cards (linked grid)',
|
|
14
|
+
fields: {
|
|
15
|
+
eyebrow: { type: 'text', label: 'Eyebrow (left of title)' },
|
|
16
|
+
title: { type: 'text', label: 'Section title' },
|
|
17
|
+
subtitle: { type: 'textarea', label: 'Section subtitle' },
|
|
18
|
+
linkLabel: { type: 'text', label: 'Header link label' },
|
|
19
|
+
linkHref: { type: 'text', label: 'Header link URL' },
|
|
20
|
+
cardLinkLabel: { type: 'text', label: 'Card link label' },
|
|
21
|
+
surface: {
|
|
22
|
+
type: 'select',
|
|
23
|
+
label: 'Surface',
|
|
24
|
+
options: [
|
|
25
|
+
{ label: 'Default', value: 'default' },
|
|
26
|
+
{ label: 'Soft grey panel', value: 'muted' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
cardSurface: {
|
|
30
|
+
type: 'select',
|
|
31
|
+
label: 'Card surface',
|
|
32
|
+
options: [
|
|
33
|
+
{ label: 'White + border', value: 'default' },
|
|
34
|
+
{ label: 'Soft grey', value: 'muted' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
columns: {
|
|
38
|
+
type: 'select',
|
|
39
|
+
label: 'Columns',
|
|
40
|
+
options: [
|
|
41
|
+
{ label: '2', value: '2' },
|
|
42
|
+
{ label: '3', value: '3' },
|
|
43
|
+
{ label: '4', value: '4' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
items: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
label: 'Cards',
|
|
49
|
+
arrayFields: {
|
|
50
|
+
title: { type: 'text', label: 'Title' },
|
|
51
|
+
// The whole card is a link, so markdown links in this copy render
|
|
52
|
+
// as their label text only — say so where the editor can see it.
|
|
53
|
+
description: {
|
|
54
|
+
type: 'textarea',
|
|
55
|
+
label: 'Description (links not supported here)',
|
|
56
|
+
},
|
|
57
|
+
icon: {
|
|
58
|
+
type: 'select',
|
|
59
|
+
label: 'Icon',
|
|
60
|
+
options: [
|
|
61
|
+
{ label: 'None', value: '' },
|
|
62
|
+
{ label: 'Chart', value: 'chart' },
|
|
63
|
+
{ label: 'Search', value: 'search' },
|
|
64
|
+
{ label: 'Thermometer', value: 'thermometer' },
|
|
65
|
+
{ label: 'Tree', value: 'tree' },
|
|
66
|
+
{ label: 'Paw', value: 'paw' },
|
|
67
|
+
{ label: 'Waves', value: 'waves' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
iconBg: { type: 'text', label: 'Icon badge colour' },
|
|
71
|
+
iconUrl: createImageField(mediaLibraryAdapter, 'Icon image (overrides icon)'),
|
|
72
|
+
iconAlt: { type: 'text', label: 'Icon image alt text' },
|
|
73
|
+
href: { type: 'text', label: 'Link URL' },
|
|
74
|
+
tag: { type: 'text', label: 'Tag (top-right)' },
|
|
75
|
+
cardBg: {
|
|
76
|
+
type: 'text',
|
|
77
|
+
label: 'Card colour (CSS, white text — optional)',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
defaultItemProps: {
|
|
81
|
+
title: 'Card title',
|
|
82
|
+
description: 'Short description.',
|
|
83
|
+
icon: '',
|
|
84
|
+
iconBg: '',
|
|
85
|
+
iconUrl: '',
|
|
86
|
+
iconAlt: '',
|
|
87
|
+
href: '#',
|
|
88
|
+
tag: '',
|
|
89
|
+
cardBg: '',
|
|
90
|
+
},
|
|
91
|
+
min: 1,
|
|
92
|
+
max: 12,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
defaultProps: {
|
|
96
|
+
eyebrow: '',
|
|
97
|
+
title: 'Featured topics',
|
|
98
|
+
subtitle: '',
|
|
99
|
+
linkLabel: '',
|
|
100
|
+
linkHref: '',
|
|
101
|
+
cardLinkLabel: 'Learn more',
|
|
102
|
+
surface: 'default',
|
|
103
|
+
cardSurface: 'default',
|
|
104
|
+
columns: '3',
|
|
105
|
+
items: [
|
|
106
|
+
{
|
|
107
|
+
title: 'First topic',
|
|
108
|
+
description: 'Short description of the topic.',
|
|
109
|
+
icon: '',
|
|
110
|
+
iconBg: '',
|
|
111
|
+
iconUrl: '',
|
|
112
|
+
href: '#',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
title: 'Second topic',
|
|
116
|
+
description: 'Short description of the topic.',
|
|
117
|
+
icon: '',
|
|
118
|
+
iconBg: '',
|
|
119
|
+
iconUrl: '',
|
|
120
|
+
href: '#',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: 'Third topic',
|
|
124
|
+
description: 'Short description of the topic.',
|
|
125
|
+
icon: '',
|
|
126
|
+
iconBg: '',
|
|
127
|
+
iconUrl: '',
|
|
128
|
+
href: '#',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
render: (props) => <FeatureCardsWidgetRender {...props} />,
|
|
133
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
|
|
7
|
+
export type FeatureGridItem = {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
iconUrl: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type FeatureGridWidgetProps = {
|
|
14
|
+
heading: string;
|
|
15
|
+
subtext: string;
|
|
16
|
+
columns: '2' | '3' | '4';
|
|
17
|
+
items: FeatureGridItem[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const COLS_CLASS: Record<FeatureGridWidgetProps['columns'], string> = {
|
|
21
|
+
'2': 'sm:grid-cols-2',
|
|
22
|
+
'3': 'sm:grid-cols-2 lg:grid-cols-3',
|
|
23
|
+
'4': 'sm:grid-cols-2 lg:grid-cols-4',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const FeatureGridWidget: ComponentConfig<FeatureGridWidgetProps> = {
|
|
27
|
+
label: 'Feature grid (marketing)',
|
|
28
|
+
fields: {
|
|
29
|
+
heading: { type: 'text', label: 'Heading' },
|
|
30
|
+
subtext: { type: 'textarea', label: 'Subtext' },
|
|
31
|
+
columns: {
|
|
32
|
+
type: 'select',
|
|
33
|
+
label: 'Columns',
|
|
34
|
+
options: [
|
|
35
|
+
{ label: '2', value: '2' },
|
|
36
|
+
{ label: '3', value: '3' },
|
|
37
|
+
{ label: '4', value: '4' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
items: {
|
|
41
|
+
type: 'array',
|
|
42
|
+
label: 'Features',
|
|
43
|
+
arrayFields: {
|
|
44
|
+
title: { type: 'text', label: 'Title' },
|
|
45
|
+
description: { type: 'textarea', label: 'Description' },
|
|
46
|
+
iconUrl: createImageField(mediaLibraryAdapter, 'Icon URL'),
|
|
47
|
+
},
|
|
48
|
+
defaultItemProps: {
|
|
49
|
+
title: 'Feature',
|
|
50
|
+
description: 'Feature description.',
|
|
51
|
+
iconUrl: '',
|
|
52
|
+
},
|
|
53
|
+
min: 1,
|
|
54
|
+
max: 12,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultProps: {
|
|
58
|
+
heading: 'Powerful features',
|
|
59
|
+
subtext: 'Everything you need to build amazing pages.',
|
|
60
|
+
columns: '3',
|
|
61
|
+
items: [
|
|
62
|
+
{ title: 'Fast', description: 'Loads in milliseconds.', iconUrl: '' },
|
|
63
|
+
{ title: 'Flexible', description: 'Compose any layout.', iconUrl: '' },
|
|
64
|
+
{
|
|
65
|
+
title: 'Reliable',
|
|
66
|
+
description: 'Battle-tested in production.',
|
|
67
|
+
iconUrl: '',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
render: ({ heading, subtext, columns, items }) => (
|
|
72
|
+
<section className="container px-4 py-16 mx-auto sm:max-w-xl md:max-w-full lg:max-w-screen-xl md:px-24 lg:px-8 lg:py-20">
|
|
73
|
+
{(heading || subtext) && (
|
|
74
|
+
<div className="max-w-xl mb-10 md:mx-auto sm:text-center lg:max-w-2xl md:mb-12">
|
|
75
|
+
{heading && (
|
|
76
|
+
<h2
|
|
77
|
+
className="max-w-lg mb-6 text-3xl leading-tight sm:text-4xl md:mx-auto"
|
|
78
|
+
style={{
|
|
79
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
80
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
81
|
+
fontFamily: 'var(--pb-display-font)',
|
|
82
|
+
color: 'var(--pb-heading-color)',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{renderRichText(heading)}
|
|
86
|
+
</h2>
|
|
87
|
+
)}
|
|
88
|
+
{subtext && <RichTextBlock text={subtext} className="text-base md:text-lg" />}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
<div className={cn('grid gap-8 grid-cols-1', COLS_CLASS[columns])}>
|
|
92
|
+
{items?.map((item, idx) => (
|
|
93
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
94
|
+
<div key={idx} className="text-center">
|
|
95
|
+
{item.iconUrl ? (
|
|
96
|
+
<img
|
|
97
|
+
src={item.iconUrl}
|
|
98
|
+
alt=""
|
|
99
|
+
loading="lazy"
|
|
100
|
+
className="size-12 mx-auto mb-4 object-contain"
|
|
101
|
+
/>
|
|
102
|
+
) : (
|
|
103
|
+
<div className="size-12 rounded-full bg-primary-100 mx-auto mb-4" />
|
|
104
|
+
)}
|
|
105
|
+
<h3 className="mb-3 text-xl font-medium leading-snug">{renderRichText(item.title)}</h3>
|
|
106
|
+
{item.description && (
|
|
107
|
+
<RichTextBlock
|
|
108
|
+
text={item.description}
|
|
109
|
+
className="text-sm"
|
|
110
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
),
|
|
118
|
+
};
|