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,156 @@
|
|
|
1
|
+
/** Props type and grid/heading tables for the media-object widget. */
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from 'react';
|
|
4
|
+
|
|
5
|
+
export type MediaObjectWidgetProps = {
|
|
6
|
+
imageUrl: string;
|
|
7
|
+
imageAlt: string;
|
|
8
|
+
imagePosition: 'left' | 'right';
|
|
9
|
+
heading: string;
|
|
10
|
+
body: string;
|
|
11
|
+
linkLabel: string;
|
|
12
|
+
linkHref: string;
|
|
13
|
+
logos?: { src: string; alt: string }[];
|
|
14
|
+
/** "narrow" constrains the heading so a short title wraps to two lines. */
|
|
15
|
+
headingWidth?: 'auto' | 'narrow';
|
|
16
|
+
/**
|
|
17
|
+
* Organic mask (SVG url, supplied by the tenant's content) applied to the
|
|
18
|
+
* side image. When set the image is clipped to the shape and loses its
|
|
19
|
+
* rounded corners. Empty = the plain rounded image (unchanged).
|
|
20
|
+
*/
|
|
21
|
+
imageMaskUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* How the side image is framed. "rounded" is the default rounded rectangle.
|
|
24
|
+
* "native" renders the file exactly as supplied — no crop, no rounding, no
|
|
25
|
+
* mask — for transparent artwork that is already cut to its final shape.
|
|
26
|
+
* Preferred over `imageMaskUrl`: a supplied cut-out is pixel-accurate, where
|
|
27
|
+
* a mask re-derives the silhouette and has to be kept in sync by hand.
|
|
28
|
+
*/
|
|
29
|
+
imageShape?: 'rounded' | 'native';
|
|
30
|
+
/** Chip overlaid top-left on the image (e.g. a location tag). */
|
|
31
|
+
imageTag?: string;
|
|
32
|
+
/** CSS color for the image tag chip (text stays surface-contrast). */
|
|
33
|
+
imageTagColor?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Organic mask for the image tag. Set it and the tag renders as the Figma's
|
|
36
|
+
* tilted blob overhanging the photo's top-left instead of a rounded pill.
|
|
37
|
+
*/
|
|
38
|
+
imageTagMaskUrl?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Transparent artwork for the image tag, used INSTEAD of `imageTagMaskUrl` +
|
|
41
|
+
* `imageTagColor`: the file carries the tag's shape and colour, and only the
|
|
42
|
+
* upright label is drawn over it.
|
|
43
|
+
*/
|
|
44
|
+
imageTagImageUrl?: string;
|
|
45
|
+
/** Small pill rendered above the heading (e.g. a date/time or a count). */
|
|
46
|
+
datePill?: string;
|
|
47
|
+
/** CSS color for the date pill (text stays surface-contrast). */
|
|
48
|
+
datePillColor?: string;
|
|
49
|
+
surface: 'default' | 'dark' | 'muted';
|
|
50
|
+
/**
|
|
51
|
+
* Optional CSS color for the band background (e.g. a tenant's program
|
|
52
|
+
* color). When set, the band renders like the dark surface (white text)
|
|
53
|
+
* but on this color instead of `--secondary`.
|
|
54
|
+
*/
|
|
55
|
+
surfaceColor?: string;
|
|
56
|
+
/** Small label in the first grid column (grid/banner layouts only). */
|
|
57
|
+
eyebrow?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Structured points rendered under `body` as a marker list — the Figma's
|
|
60
|
+
* "Warum mitmachen?" (title + explanation per point) and "Netzwerk" (bare
|
|
61
|
+
* names). Kept separate from `body`, which is one text block and can only
|
|
62
|
+
* flatten such a list into indistinguishable lines.
|
|
63
|
+
*/
|
|
64
|
+
bullets?: { title: string; body?: string }[];
|
|
65
|
+
/**
|
|
66
|
+
* Optional CSS max-width for the body paragraph alone (the Figma caps the
|
|
67
|
+
* event-card copy at 478px inside a wider text column). Empty = unconstrained.
|
|
68
|
+
*/
|
|
69
|
+
bodyMaxWidth?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Trailing credit line under the body, set semibold italic — the Figma's
|
|
72
|
+
* "In Zusammenarbeit mit …" on each event card.
|
|
73
|
+
*/
|
|
74
|
+
footnote?: string;
|
|
75
|
+
/** Point-title size: `md` = 20px, `lg` = 24px (both from the Figma). */
|
|
76
|
+
bulletSize?: 'md' | 'lg';
|
|
77
|
+
/**
|
|
78
|
+
* Render `linkLabel` as the tenant's outlined CTA button rather than an
|
|
79
|
+
* inline text link — the Figma draws the "Projekt-Flyer herunterladen"
|
|
80
|
+
* action on the green band as a bordered pill, not a link.
|
|
81
|
+
*/
|
|
82
|
+
linkVariant?: 'link' | 'button';
|
|
83
|
+
/** Show the design's small square marker before each point. */
|
|
84
|
+
bulletMarker?: 'square' | 'none';
|
|
85
|
+
/**
|
|
86
|
+
* "half" is the legacy 50/50 flex split. "grid" aligns image and text to the
|
|
87
|
+
* 12-column page grid (Mowing Figma: image spans 6 columns, text sits at the
|
|
88
|
+
* 2nd column for image-right / ends at the 12th for image-left). "banner"
|
|
89
|
+
* drops the side image: eyebrow left, text at the 4th column, with the image
|
|
90
|
+
* (e.g. a partner-logo strip) rendered underneath the text.
|
|
91
|
+
*/
|
|
92
|
+
layout?: 'half' | 'grid' | 'banner';
|
|
93
|
+
/** Text column width on the page grid: xs ≈ 315px, sm ≈ 432px, md ≈ 546px. */
|
|
94
|
+
textWidth?: 'auto' | 'xs' | 'sm' | 'md';
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Display tokens for the heading — weight/tracking/font come from the per-tenant
|
|
98
|
+
// page-builder theme (see styles.css `:root` for Recodo defaults and `.gca-root`
|
|
99
|
+
// for the GCA overrides). Color is overridden to white in the dark surface. The
|
|
100
|
+
// font-size reads `--pb-heading-lg` but falls back to the prior size, so tenants
|
|
101
|
+
// that don't set the token (GCA, Recodo) render unchanged.
|
|
102
|
+
|
|
103
|
+
export function headingStyle(
|
|
104
|
+
inverse: boolean,
|
|
105
|
+
layout: MediaObjectWidgetProps['layout'] = 'half',
|
|
106
|
+
): CSSProperties {
|
|
107
|
+
return {
|
|
108
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
109
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
110
|
+
fontFamily: 'var(--pb-display-font)',
|
|
111
|
+
// The banner reads the section-heading size (`--pb-heading-md`, Mowing →
|
|
112
|
+
// 40px) so its title matches the other eyebrow sections on the page.
|
|
113
|
+
fontSize:
|
|
114
|
+
layout === 'banner'
|
|
115
|
+
? 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 1.4vw, 2.5rem))'
|
|
116
|
+
: 'var(--pb-heading-lg, clamp(1.875rem, 1.3rem + 1.8vw, 2.25rem))',
|
|
117
|
+
color: inverse ? 'var(--pb-surface-contrast, #ffffff)' : 'var(--pb-heading-color)',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 12-col grid placement (gap 24px): the text column spans 3/4/5 columns and the
|
|
122
|
+
// image spans the design's 6 columns for image-right, or fills the columns left
|
|
123
|
+
// of the text for image-left — both text placements end flush with the Figma
|
|
124
|
+
// grid (image-right text starts at col 2; image-left text ends before col 12).
|
|
125
|
+
export const GRID_TEXT: Record<'xs' | 'sm' | 'md', { right: string; left: string; max: string }> = {
|
|
126
|
+
xs: {
|
|
127
|
+
right: 'lg:col-start-2 lg:col-span-3',
|
|
128
|
+
left: 'lg:col-start-9 lg:col-span-3',
|
|
129
|
+
max: 'max-w-[315px]',
|
|
130
|
+
},
|
|
131
|
+
sm: {
|
|
132
|
+
right: 'lg:col-start-2 lg:col-span-4',
|
|
133
|
+
left: 'lg:col-start-8 lg:col-span-4',
|
|
134
|
+
max: 'max-w-[432px]',
|
|
135
|
+
},
|
|
136
|
+
md: {
|
|
137
|
+
right: 'lg:col-start-2 lg:col-span-5',
|
|
138
|
+
left: 'lg:col-start-7 lg:col-span-5',
|
|
139
|
+
max: 'max-w-[546px]',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const GRID_IMAGE: Record<'xs' | 'sm' | 'md', { right: string; left: string }> = {
|
|
144
|
+
xs: {
|
|
145
|
+
right: 'lg:col-start-7 lg:col-span-6',
|
|
146
|
+
left: 'lg:col-start-1 lg:col-span-7',
|
|
147
|
+
},
|
|
148
|
+
sm: {
|
|
149
|
+
right: 'lg:col-start-7 lg:col-span-6',
|
|
150
|
+
left: 'lg:col-start-1 lg:col-span-6',
|
|
151
|
+
},
|
|
152
|
+
md: {
|
|
153
|
+
right: 'lg:col-start-7 lg:col-span-6',
|
|
154
|
+
left: 'lg:col-start-1 lg:col-span-5',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/** MediaObject render. The props type and layout tables live in
|
|
2
|
+
* media-object-layout.ts so this file stays under the 300-line cap. */
|
|
3
|
+
|
|
4
|
+
/** MediaObjectWidget types, helpers and render — split from media-object-widget.tsx,
|
|
5
|
+
* which keeps the field definitions, so both stay under the 300-line cap. */
|
|
6
|
+
|
|
7
|
+
import type { CSSProperties } from 'react';
|
|
8
|
+
import { cn } from '../../utils/widgetUtils';
|
|
9
|
+
import { AccentText } from './_internal/accent-text';
|
|
10
|
+
import { LinkArrow } from './_internal/link-arrow';
|
|
11
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
12
|
+
import { CTAButton, eyebrowTextStyle, maskStyle } from './_shared';
|
|
13
|
+
|
|
14
|
+
import { GRID_IMAGE, GRID_TEXT, type MediaObjectWidgetProps } from './media-object-layout';
|
|
15
|
+
import { MediaObjectText } from './media-object-text';
|
|
16
|
+
|
|
17
|
+
export type { MediaObjectWidgetProps } from './media-object-layout';
|
|
18
|
+
|
|
19
|
+
export const MediaObjectWidgetRender = ({
|
|
20
|
+
imageUrl,
|
|
21
|
+
imageAlt,
|
|
22
|
+
imageMaskUrl = '',
|
|
23
|
+
imageShape = 'rounded',
|
|
24
|
+
imageTag = '',
|
|
25
|
+
imageTagColor = '',
|
|
26
|
+
imageTagMaskUrl = '',
|
|
27
|
+
imageTagImageUrl = '',
|
|
28
|
+
imagePosition,
|
|
29
|
+
eyebrow = '',
|
|
30
|
+
datePill = '',
|
|
31
|
+
datePillColor = '',
|
|
32
|
+
bodyMaxWidth = '',
|
|
33
|
+
footnote = '',
|
|
34
|
+
bullets = [],
|
|
35
|
+
bulletSize = 'md',
|
|
36
|
+
bulletMarker = 'square',
|
|
37
|
+
linkVariant = 'link',
|
|
38
|
+
heading,
|
|
39
|
+
body,
|
|
40
|
+
linkLabel,
|
|
41
|
+
linkHref,
|
|
42
|
+
logos,
|
|
43
|
+
headingWidth = 'auto',
|
|
44
|
+
layout = 'half',
|
|
45
|
+
textWidth = 'auto',
|
|
46
|
+
surface,
|
|
47
|
+
surfaceColor = '',
|
|
48
|
+
}: MediaObjectWidgetProps) => {
|
|
49
|
+
const isDark = surface === 'dark' || !!surfaceColor;
|
|
50
|
+
const grid = layout === 'grid';
|
|
51
|
+
// "auto" keeps the legacy unconstrained text; the grid layout needs a
|
|
52
|
+
// concrete column span, so it falls back to the 4-column (~432px) text.
|
|
53
|
+
const widthKey = textWidth === 'auto' ? 'sm' : textWidth;
|
|
54
|
+
const textMax = textWidth === 'auto' ? '' : GRID_TEXT[widthKey].max;
|
|
55
|
+
|
|
56
|
+
const textBlock = (
|
|
57
|
+
<MediaObjectText
|
|
58
|
+
{...({
|
|
59
|
+
eyebrow,
|
|
60
|
+
datePill,
|
|
61
|
+
datePillColor,
|
|
62
|
+
heading,
|
|
63
|
+
body,
|
|
64
|
+
bodyMaxWidth,
|
|
65
|
+
bullets,
|
|
66
|
+
bulletSize,
|
|
67
|
+
bulletMarker,
|
|
68
|
+
footnote,
|
|
69
|
+
linkLabel,
|
|
70
|
+
linkHref,
|
|
71
|
+
linkVariant,
|
|
72
|
+
logos,
|
|
73
|
+
headingWidth,
|
|
74
|
+
layout,
|
|
75
|
+
} as MediaObjectWidgetProps)}
|
|
76
|
+
isDark={isDark}
|
|
77
|
+
grid={grid}
|
|
78
|
+
textMax={textMax}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Supplied cut-out wins over a mask: the artwork already has the shape, so
|
|
83
|
+
// it renders untouched. A mask is the fallback for square source images.
|
|
84
|
+
const native = imageShape === 'native';
|
|
85
|
+
const masked = !native && Boolean(imageMaskUrl);
|
|
86
|
+
const imageEl = imageUrl ? (
|
|
87
|
+
<img
|
|
88
|
+
src={imageUrl}
|
|
89
|
+
alt={imageAlt || heading}
|
|
90
|
+
loading="lazy"
|
|
91
|
+
className={cn('w-full h-auto', !masked && !native && 'rounded-lg')}
|
|
92
|
+
style={masked ? maskStyle(imageMaskUrl) : undefined}
|
|
93
|
+
/>
|
|
94
|
+
) : (
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
'w-full aspect-square bg-gradient-to-br from-gray-200 to-gray-300',
|
|
98
|
+
!masked && !native && 'rounded-lg',
|
|
99
|
+
)}
|
|
100
|
+
style={masked ? maskStyle(imageMaskUrl) : undefined}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
const image = imageTag ? (
|
|
104
|
+
<div className="relative">
|
|
105
|
+
{imageEl}
|
|
106
|
+
{imageTagImageUrl ? (
|
|
107
|
+
// Supplied tag artwork: the file carries the shape AND the colour,
|
|
108
|
+
// so only the upright label is drawn over it. No colour behind it
|
|
109
|
+
// for the same reason as the timeline blob — the artwork is a
|
|
110
|
+
// transparent cut-out, and a backing colour would square off the
|
|
111
|
+
// tilted tag shape.
|
|
112
|
+
<span className="pointer-events-none absolute -top-4 -left-3 block w-[34%] max-w-[188px]">
|
|
113
|
+
<img
|
|
114
|
+
src={imageTagImageUrl}
|
|
115
|
+
alt=""
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
loading="lazy"
|
|
118
|
+
className="block h-auto w-full"
|
|
119
|
+
/>
|
|
120
|
+
<span
|
|
121
|
+
className="absolute inset-0 flex items-center justify-center px-2 text-center text-[20px] font-bold leading-tight"
|
|
122
|
+
style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
|
|
123
|
+
>
|
|
124
|
+
{renderRichText(imageTag)}
|
|
125
|
+
</span>
|
|
126
|
+
</span>
|
|
127
|
+
) : imageTagMaskUrl ? (
|
|
128
|
+
// Figma tag: a tilted blob straddling the photo's top-left corner
|
|
129
|
+
// with an upright label over it (same two-layer construction the
|
|
130
|
+
// timeline marker uses — the art rotates, the text does not).
|
|
131
|
+
<span
|
|
132
|
+
className="pointer-events-none absolute -top-4 -left-3 block w-[34%] max-w-[188px]"
|
|
133
|
+
style={{ aspectRatio: '188 / 161' }}
|
|
134
|
+
>
|
|
135
|
+
<span
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className="absolute inset-0"
|
|
138
|
+
style={{
|
|
139
|
+
backgroundColor: imageTagColor || 'var(--pb-accent, #16a34a)',
|
|
140
|
+
transform: 'rotate(-18.2deg)',
|
|
141
|
+
...maskStyle(imageTagMaskUrl),
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
<span
|
|
145
|
+
className="absolute inset-0 flex items-center justify-center px-2 text-center text-[20px] font-bold leading-tight"
|
|
146
|
+
style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
|
|
147
|
+
>
|
|
148
|
+
{renderRichText(imageTag)}
|
|
149
|
+
</span>
|
|
150
|
+
</span>
|
|
151
|
+
) : (
|
|
152
|
+
<span
|
|
153
|
+
className="absolute left-4 top-6 inline-flex items-center rounded-full px-4 py-1.5 text-sm font-bold"
|
|
154
|
+
style={{
|
|
155
|
+
backgroundColor: imageTagColor || 'var(--pb-accent, #16a34a)',
|
|
156
|
+
color: 'var(--pb-surface-contrast, #ffffff)',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{renderRichText(imageTag)}
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
imageEl
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Banner: eyebrow in the first grid column, text at the 4th column
|
|
168
|
+
// (Figma "Project Partners": 652px text, logo strip underneath).
|
|
169
|
+
if (layout === 'banner') {
|
|
170
|
+
return (
|
|
171
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
'rounded-[12px] px-8 py-12 lg:grid lg:grid-cols-12 lg:gap-6 lg:px-0 lg:py-20',
|
|
175
|
+
isDark && !surfaceColor
|
|
176
|
+
? 'bg-[var(--secondary,#1a353e)]'
|
|
177
|
+
: surface === 'muted' && !surfaceColor
|
|
178
|
+
? 'bg-[var(--pb-surface-muted,#f3f3f4)]'
|
|
179
|
+
: '',
|
|
180
|
+
)}
|
|
181
|
+
style={surfaceColor ? { backgroundColor: surfaceColor } : undefined}
|
|
182
|
+
>
|
|
183
|
+
{eyebrow && (
|
|
184
|
+
<p
|
|
185
|
+
className="mb-6 lg:col-span-3 lg:mb-0 lg:pl-12"
|
|
186
|
+
style={{
|
|
187
|
+
...eyebrowTextStyle('1.125rem', '400'),
|
|
188
|
+
color: isDark ? 'var(--pb-surface-contrast, #ffffff)' : 'var(--pb-heading-color)',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{renderRichText(eyebrow)}
|
|
192
|
+
</p>
|
|
193
|
+
)}
|
|
194
|
+
<div className="max-w-[652px] space-y-4 lg:col-start-4 lg:col-span-7">
|
|
195
|
+
{textBlock}
|
|
196
|
+
{imageUrl && (
|
|
197
|
+
<img
|
|
198
|
+
src={imageUrl}
|
|
199
|
+
alt={imageAlt || heading}
|
|
200
|
+
loading="lazy"
|
|
201
|
+
className="mt-8 h-[68px] w-auto max-w-full object-contain object-left"
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</section>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const layoutBlock = grid ? (
|
|
211
|
+
// 12-col page grid: text and image cells sit on the same columns as the
|
|
212
|
+
// Figma layout, so section text lines up across the whole page. Both
|
|
213
|
+
// cells pin to row 1: the image cell renders first in the DOM, so for
|
|
214
|
+
// image-right (image at col 7, text at col 2) grid auto-placement would
|
|
215
|
+
// otherwise wrap the text cell to a second row instead of beside it.
|
|
216
|
+
<div className="flex flex-col gap-8 lg:grid lg:grid-cols-12 lg:items-center lg:gap-6">
|
|
217
|
+
<div
|
|
218
|
+
className={cn(
|
|
219
|
+
'w-full lg:row-start-1',
|
|
220
|
+
imagePosition === 'right' ? GRID_IMAGE[widthKey].right : GRID_IMAGE[widthKey].left,
|
|
221
|
+
)}
|
|
222
|
+
>
|
|
223
|
+
{image}
|
|
224
|
+
</div>
|
|
225
|
+
<div
|
|
226
|
+
className={cn(
|
|
227
|
+
'space-y-4 lg:row-start-1',
|
|
228
|
+
textMax,
|
|
229
|
+
imagePosition === 'right' ? GRID_TEXT[widthKey].right : GRID_TEXT[widthKey].left,
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{textBlock}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
<div
|
|
237
|
+
className={cn(
|
|
238
|
+
'flex flex-col md:flex-row gap-8 lg:gap-12 items-center',
|
|
239
|
+
imagePosition === 'right' ? 'md:flex-row-reverse' : '',
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<div className="md:w-1/2">{image}</div>
|
|
243
|
+
<div className={cn('md:w-1/2 space-y-4', textMax)}>{textBlock}</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (isDark) {
|
|
248
|
+
return (
|
|
249
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
250
|
+
<div
|
|
251
|
+
className={cn(
|
|
252
|
+
'rounded-[12px]',
|
|
253
|
+
!surfaceColor && 'bg-[var(--secondary,#1a353e)]',
|
|
254
|
+
grid ? 'px-8 py-12 lg:px-0' : 'p-8 lg:p-12',
|
|
255
|
+
)}
|
|
256
|
+
style={surfaceColor ? { backgroundColor: surfaceColor } : undefined}
|
|
257
|
+
>
|
|
258
|
+
{layoutBlock}
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (surface === 'muted') {
|
|
265
|
+
return (
|
|
266
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
267
|
+
<div
|
|
268
|
+
className={cn(
|
|
269
|
+
'rounded-[12px] bg-[var(--pb-surface-muted,#f3f3f4)]',
|
|
270
|
+
grid ? 'px-8 py-12 lg:px-0' : 'p-8 sm:p-10 lg:p-14',
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
{layoutBlock}
|
|
274
|
+
</div>
|
|
275
|
+
</section>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
281
|
+
{layoutBlock}
|
|
282
|
+
</section>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** The text column of the media-object widget — eyebrow, heading, body,
|
|
2
|
+
* bullets, footnote and link. Split out so the render file stays under
|
|
3
|
+
* the 300-line cap. */
|
|
4
|
+
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/widgetUtils';
|
|
8
|
+
import { AccentText } from './_internal/accent-text';
|
|
9
|
+
import { LinkArrow } from './_internal/link-arrow';
|
|
10
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
11
|
+
import { CTAButton, eyebrowTextStyle, maskStyle } from './_shared';
|
|
12
|
+
import { GRID_TEXT, headingStyle, type MediaObjectWidgetProps } from './media-object-layout';
|
|
13
|
+
|
|
14
|
+
type TextBlockProps = MediaObjectWidgetProps & {
|
|
15
|
+
isDark: boolean;
|
|
16
|
+
grid: boolean;
|
|
17
|
+
textMax: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function MediaObjectText({
|
|
21
|
+
eyebrow = '',
|
|
22
|
+
datePill = '',
|
|
23
|
+
datePillColor = '',
|
|
24
|
+
heading,
|
|
25
|
+
body,
|
|
26
|
+
bodyMaxWidth = '',
|
|
27
|
+
bullets = [],
|
|
28
|
+
bulletSize = 'md',
|
|
29
|
+
bulletMarker = 'square',
|
|
30
|
+
footnote = '',
|
|
31
|
+
linkLabel,
|
|
32
|
+
linkHref,
|
|
33
|
+
linkVariant = 'link',
|
|
34
|
+
logos,
|
|
35
|
+
headingWidth = 'auto',
|
|
36
|
+
layout = 'half',
|
|
37
|
+
isDark,
|
|
38
|
+
grid,
|
|
39
|
+
textMax,
|
|
40
|
+
}: TextBlockProps) {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{/* The banner layout renders the eyebrow in its own grid column, so
|
|
44
|
+
only the other layouts carry it inline above the heading (the
|
|
45
|
+
BioGarden "VIELFÄLTIGE GÄRTEN" kicker). */}
|
|
46
|
+
{eyebrow && layout !== 'banner' && (
|
|
47
|
+
<p
|
|
48
|
+
style={{
|
|
49
|
+
...eyebrowTextStyle(),
|
|
50
|
+
color: isDark
|
|
51
|
+
? 'var(--pb-surface-contrast, #ffffff)'
|
|
52
|
+
: 'var(--pb-heading-color, #161728)',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{renderRichText(eyebrow)}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
{datePill && (
|
|
59
|
+
<span
|
|
60
|
+
className="inline-flex w-fit items-center rounded-full px-3 py-1 text-sm font-bold"
|
|
61
|
+
style={{
|
|
62
|
+
backgroundColor: datePillColor || 'var(--pb-accent, #16a34a)',
|
|
63
|
+
color: 'var(--pb-surface-contrast, #ffffff)',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{renderRichText(datePill)}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
{heading && (
|
|
70
|
+
<h3
|
|
71
|
+
className={cn('leading-tight', headingWidth === 'narrow' && 'max-w-[400px]')}
|
|
72
|
+
style={headingStyle(isDark, layout)}
|
|
73
|
+
>
|
|
74
|
+
<AccentText text={heading} />
|
|
75
|
+
</h3>
|
|
76
|
+
)}
|
|
77
|
+
{body && (
|
|
78
|
+
<RichTextBlock
|
|
79
|
+
text={body}
|
|
80
|
+
className={cn(
|
|
81
|
+
'whitespace-pre-line text-lg leading-[var(--pb-body-leading,1.625)]',
|
|
82
|
+
isDark && 'opacity-80',
|
|
83
|
+
)}
|
|
84
|
+
style={{
|
|
85
|
+
color: isDark ? 'var(--pb-surface-contrast, #ffffff)' : 'var(--pb-body-color)',
|
|
86
|
+
maxWidth: bodyMaxWidth || undefined,
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
{footnote && (
|
|
91
|
+
<p
|
|
92
|
+
className={cn(
|
|
93
|
+
'font-semibold italic leading-[var(--pb-body-leading,1.625)]',
|
|
94
|
+
isDark && 'opacity-80',
|
|
95
|
+
)}
|
|
96
|
+
style={{
|
|
97
|
+
color: isDark ? 'var(--pb-surface-contrast, #ffffff)' : 'var(--pb-body-color)',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{renderRichText(footnote)}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
{bullets.filter((b) => b.title || b.body).length > 0 && (
|
|
104
|
+
// The design's point list: a small square marker, a Medium point
|
|
105
|
+
// title, and an optional explanation line beneath it.
|
|
106
|
+
<ul className="flex list-none flex-col gap-6 pt-2">
|
|
107
|
+
{bullets
|
|
108
|
+
.filter((b) => b.title || b.body)
|
|
109
|
+
.map((point, i) => (
|
|
110
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
111
|
+
<li key={i} className="flex gap-3">
|
|
112
|
+
{bulletMarker === 'square' && (
|
|
113
|
+
<span
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
className="mt-[0.55em] size-[7px] shrink-0 bg-current"
|
|
116
|
+
style={{
|
|
117
|
+
color: isDark
|
|
118
|
+
? 'var(--pb-surface-contrast, #ffffff)'
|
|
119
|
+
: 'var(--pb-heading-color, #161728)',
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<div className="flex flex-col gap-2">
|
|
124
|
+
{point.title && (
|
|
125
|
+
<p
|
|
126
|
+
className={cn(
|
|
127
|
+
'font-medium leading-tight',
|
|
128
|
+
bulletSize === 'lg' ? 'text-[24px]' : 'text-[20px]',
|
|
129
|
+
)}
|
|
130
|
+
style={{
|
|
131
|
+
color: isDark
|
|
132
|
+
? 'var(--pb-surface-contrast, #ffffff)'
|
|
133
|
+
: 'var(--pb-heading-color, #161728)',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{renderRichText(point.title)}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
{point.body && (
|
|
140
|
+
<RichTextBlock
|
|
141
|
+
text={point.body}
|
|
142
|
+
className="whitespace-pre-line leading-[var(--pb-body-leading,1.625)]"
|
|
143
|
+
style={{
|
|
144
|
+
color: isDark
|
|
145
|
+
? 'var(--pb-surface-contrast, #ffffff)'
|
|
146
|
+
: 'var(--pb-body-color)',
|
|
147
|
+
opacity: isDark ? 0.8 : 1,
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</li>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
)}
|
|
156
|
+
{linkLabel && linkVariant === 'button' && (
|
|
157
|
+
<div className="pt-2">
|
|
158
|
+
<CTAButton
|
|
159
|
+
label={linkLabel}
|
|
160
|
+
href={linkHref || '#'}
|
|
161
|
+
variant={isDark ? 'inverse-outline' : 'secondary'}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{linkLabel &&
|
|
166
|
+
linkVariant === 'link' &&
|
|
167
|
+
(isDark ? (
|
|
168
|
+
<a
|
|
169
|
+
href={linkHref || '#'}
|
|
170
|
+
className="inline-flex items-center gap-1 font-medium hover:opacity-80"
|
|
171
|
+
style={{ color: 'var(--pb-surface-contrast, #ffffff)' }}
|
|
172
|
+
>
|
|
173
|
+
{renderRichText(linkLabel, { allowLinks: false })}
|
|
174
|
+
<LinkArrow className="size-5" />
|
|
175
|
+
</a>
|
|
176
|
+
) : (
|
|
177
|
+
<a
|
|
178
|
+
href={linkHref || '#'}
|
|
179
|
+
className="inline-flex items-center gap-1 font-medium text-[color:var(--pb-link-color,var(--pb-accent,#2f7d32))] hover:text-[color:var(--pb-link-hover-color,var(--pb-link-color,var(--pb-accent,#2f7d32)))] hover:[text-decoration-line:var(--pb-link-hover-decoration,underline)]"
|
|
180
|
+
>
|
|
181
|
+
{renderRichText(linkLabel, { allowLinks: false })}
|
|
182
|
+
<LinkArrow className="size-5" />
|
|
183
|
+
</a>
|
|
184
|
+
))}
|
|
185
|
+
{logos && logos.filter((l) => l.src).length > 0 && (
|
|
186
|
+
<div className="flex flex-wrap items-center gap-6 pt-2">
|
|
187
|
+
{logos
|
|
188
|
+
.filter((l) => l.src)
|
|
189
|
+
.map((logo, i) => (
|
|
190
|
+
<img
|
|
191
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
192
|
+
key={i}
|
|
193
|
+
src={logo.src}
|
|
194
|
+
alt={logo.alt}
|
|
195
|
+
className="h-12 w-auto object-contain"
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|