simple-module-pagebuilder 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pagebuilder/__init__.py +0 -0
- pagebuilder/components/DiffSummary.tsx +107 -0
- pagebuilder/components/ScheduledBadge.tsx +34 -0
- pagebuilder/components/StatusBadge.tsx +21 -0
- pagebuilder/components/blockRegistry.test.ts +56 -0
- pagebuilder/components/blockRegistry.ts +80 -0
- pagebuilder/components/blocks/Button.tsx +142 -0
- pagebuilder/components/blocks/Columns.tsx +64 -0
- pagebuilder/components/blocks/Heading.tsx +92 -0
- pagebuilder/components/blocks/Image.tsx +125 -0
- pagebuilder/components/blocks/MediaPicker.tsx +178 -0
- pagebuilder/components/blocks/Spacer.tsx +31 -0
- pagebuilder/components/blocks/Text.tsx +80 -0
- pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
- pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
- pagebuilder/components/editor/SchedulePanel.tsx +85 -0
- pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
- pagebuilder/components/layoutPuckConfig.tsx +109 -0
- pagebuilder/components/media/MediaFilters.tsx +83 -0
- pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
- pagebuilder/components/media/MediaGrid.tsx +76 -0
- pagebuilder/components/media/MediaHeader.tsx +37 -0
- pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
- pagebuilder/components/media/types.ts +20 -0
- pagebuilder/components/migrateContent.test.ts +146 -0
- pagebuilder/components/migrateContent.ts +135 -0
- pagebuilder/components/puckConfig.test.tsx +118 -0
- pagebuilder/components/puckConfig.tsx +121 -0
- pagebuilder/components/widgetCatalog.test.tsx +123 -0
- pagebuilder/components/widgetCatalog.ts +208 -0
- pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
- pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
- pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
- pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
- pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
- pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
- pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
- pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
- pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
- pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
- pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
- pagebuilder/components/widgets/_shared/heading.tsx +88 -0
- pagebuilder/components/widgets/_shared/index.ts +39 -0
- pagebuilder/components/widgets/_shared/mask.ts +54 -0
- pagebuilder/components/widgets/_shared/section.tsx +60 -0
- pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
- pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
- pagebuilder/components/widgets/accordion-widget.tsx +64 -0
- pagebuilder/components/widgets/alert-widget.tsx +47 -0
- pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
- pagebuilder/components/widgets/article-cards-render.tsx +132 -0
- pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
- pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
- pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
- pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
- pagebuilder/components/widgets/carousel-widget.tsx +97 -0
- pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
- pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
- pagebuilder/components/widgets/column-widget.tsx +63 -0
- pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
- pagebuilder/components/widgets/contact-form-render.tsx +293 -0
- pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
- pagebuilder/components/widgets/container-widget.tsx +93 -0
- pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
- pagebuilder/components/widgets/divider-widget.tsx +56 -0
- pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
- pagebuilder/components/widgets/faq-render.tsx +268 -0
- pagebuilder/components/widgets/faq-widget.tsx +64 -0
- pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
- pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
- pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
- pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
- pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
- pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
- pagebuilder/components/widgets/gallery-widget.tsx +139 -0
- pagebuilder/components/widgets/grid-widget.tsx +59 -0
- pagebuilder/components/widgets/hero-widget.tsx +292 -0
- pagebuilder/components/widgets/html-widget.tsx +42 -0
- pagebuilder/components/widgets/iframe-widget.tsx +44 -0
- pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
- pagebuilder/components/widgets/list-widget.tsx +56 -0
- pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
- pagebuilder/components/widgets/markdown-widget.tsx +148 -0
- pagebuilder/components/widgets/media-object-layout.ts +156 -0
- pagebuilder/components/widgets/media-object-render.tsx +284 -0
- pagebuilder/components/widgets/media-object-text.tsx +202 -0
- pagebuilder/components/widgets/media-object-widget.tsx +176 -0
- pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
- pagebuilder/components/widgets/page-header-widget.tsx +118 -0
- pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
- pagebuilder/components/widgets/quote-widget.tsx +51 -0
- pagebuilder/components/widgets/row-widget.tsx +74 -0
- pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
- pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
- pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
- pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
- pagebuilder/components/widgets/site-header-nav.tsx +153 -0
- pagebuilder/components/widgets/site-header-widget.tsx +182 -0
- pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
- pagebuilder/components/widgets/stats-widget.tsx +200 -0
- pagebuilder/components/widgets/steps-widget.tsx +211 -0
- pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
- pagebuilder/components/widgets/table-widget.tsx +86 -0
- pagebuilder/components/widgets/tabs-widget.tsx +95 -0
- pagebuilder/components/widgets/tags-widget.tsx +85 -0
- pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
- pagebuilder/components/widgets/timeline-layout.ts +73 -0
- pagebuilder/components/widgets/timeline-render.tsx +294 -0
- pagebuilder/components/widgets/timeline-widget.tsx +106 -0
- pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
- pagebuilder/components/widgets/video-widget.tsx +103 -0
- pagebuilder/components/widgets/welcome-widget.tsx +38 -0
- pagebuilder/contracts/__init__.py +57 -0
- pagebuilder/contracts/events.py +25 -0
- pagebuilder/contracts/schemas.py +239 -0
- pagebuilder/conversion/blocks-to-html.test.ts +105 -0
- pagebuilder/conversion/blocks-to-html.ts +102 -0
- pagebuilder/conversion/index.ts +2 -0
- pagebuilder/conversion/types.ts +5 -0
- pagebuilder/deps.py +26 -0
- pagebuilder/diff.py +129 -0
- pagebuilder/endpoints/__init__.py +0 -0
- pagebuilder/endpoints/api/__init__.py +21 -0
- pagebuilder/endpoints/api/_deps.py +21 -0
- pagebuilder/endpoints/api/layout.py +78 -0
- pagebuilder/endpoints/api/pages.py +85 -0
- pagebuilder/endpoints/api/revisions.py +79 -0
- pagebuilder/endpoints/api/uploads.py +92 -0
- pagebuilder/endpoints/api/workflow.py +124 -0
- pagebuilder/endpoints/seo.py +95 -0
- pagebuilder/endpoints/views.py +228 -0
- pagebuilder/fields/checkbox-field.tsx +22 -0
- pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
- pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
- pagebuilder/fields/file-picker/index.ts +6 -0
- pagebuilder/fields/file-picker/types.ts +15 -0
- pagebuilder/fields/index.ts +9 -0
- pagebuilder/fields/media-adapter.ts +47 -0
- pagebuilder/hooks/useAutosave.ts +134 -0
- pagebuilder/hooks/useEditorForm.ts +193 -0
- pagebuilder/hooks/useMediaUploads.ts +96 -0
- pagebuilder/hooks/usePageRevisions.ts +59 -0
- pagebuilder/hooks/usePageSchedule.ts +72 -0
- pagebuilder/hooks/usePageWorkflow.ts +173 -0
- pagebuilder/layout_service.py +101 -0
- pagebuilder/media_images.py +185 -0
- pagebuilder/media_queries.py +92 -0
- pagebuilder/media_service.py +222 -0
- pagebuilder/models.py +209 -0
- pagebuilder/module.py +279 -0
- pagebuilder/package.json +18 -0
- pagebuilder/pages/LayoutEditor.tsx +183 -0
- pagebuilder/pages/MediaLibrary.tsx +249 -0
- pagebuilder/pages/PageEditor.tsx +181 -0
- pagebuilder/pages/PageList.tsx +129 -0
- pagebuilder/pages/PendingReview.tsx +128 -0
- pagebuilder/pages/PublicPage.tsx +126 -0
- pagebuilder/permissions.py +44 -0
- pagebuilder/py.typed +0 -0
- pagebuilder/security.py +230 -0
- pagebuilder/service/__init__.py +141 -0
- pagebuilder/service/_common.py +24 -0
- pagebuilder/service/_revisions.py +92 -0
- pagebuilder/service/_workflow.py +207 -0
- pagebuilder/services.py +19 -0
- pagebuilder/settings.py +161 -0
- pagebuilder/static/widgets-base.css +57 -0
- pagebuilder/translation/apply.ts +54 -0
- pagebuilder/translation/extract.ts +141 -0
- pagebuilder/translation/index.ts +20 -0
- pagebuilder/translation/translatable-fields.ts +65 -0
- pagebuilder/translation/translation.test.ts +167 -0
- pagebuilder/utils/api.ts +12 -0
- pagebuilder/utils/datetime.ts +25 -0
- pagebuilder/utils/decorative-image.ts +15 -0
- pagebuilder/utils/editorSnapshot.ts +39 -0
- pagebuilder/utils/markdown.ts +160 -0
- pagebuilder/utils/mediaApi.ts +112 -0
- pagebuilder/utils/mediaFormat.ts +19 -0
- pagebuilder/utils/pagesApi.ts +111 -0
- pagebuilder/utils/parse-list.ts +7 -0
- pagebuilder/utils/request.ts +52 -0
- pagebuilder/utils/slugify.ts +9 -0
- pagebuilder/utils/types.ts +171 -0
- pagebuilder/utils/widgetUtils.ts +11 -0
- simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
- simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
- simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
- simple_module_pagebuilder-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Site header for the layout's header slot: utility bar + primary nav bar. */
|
|
2
|
+
|
|
3
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
4
|
+
import { Menu, X } from 'lucide-react';
|
|
5
|
+
import { useId, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { createCheckboxField, createImageField, mediaLibraryAdapter } from '../../fields';
|
|
8
|
+
import { SOCIAL_ICON_OPTIONS, SocialIcon, type SocialIconName } from './_shared';
|
|
9
|
+
import { DesktopNav, MobileNav, type SiteNavItem } from './site-header-nav';
|
|
10
|
+
|
|
11
|
+
export interface UtilityLink {
|
|
12
|
+
label: string;
|
|
13
|
+
href: string;
|
|
14
|
+
/** Empty renders the label as text; otherwise the icon replaces it. */
|
|
15
|
+
icon?: SocialIconName | '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SiteHeaderWidgetProps {
|
|
19
|
+
logoUrl: string;
|
|
20
|
+
logoAlt: string;
|
|
21
|
+
homeHref: string;
|
|
22
|
+
utilityLinks: UtilityLink[];
|
|
23
|
+
navItems: SiteNavItem[];
|
|
24
|
+
ctaLabel: string;
|
|
25
|
+
ctaHref: string;
|
|
26
|
+
sticky: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function UtilityBar({ links }: { links: UtilityLink[] }) {
|
|
30
|
+
if (links.length === 0) return null;
|
|
31
|
+
return (
|
|
32
|
+
<div className="bg-primary-800 text-white">
|
|
33
|
+
<div className="mx-auto flex h-10 max-w-[1440px] items-center justify-end gap-4 px-4 text-[13px] sm:gap-6 sm:px-6 lg:px-12">
|
|
34
|
+
{links.map((link, index) => (
|
|
35
|
+
<a
|
|
36
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
|
|
37
|
+
key={index}
|
|
38
|
+
href={link.href || '#'}
|
|
39
|
+
aria-label={link.icon ? link.label : undefined}
|
|
40
|
+
className="opacity-90 transition-opacity hover:opacity-100"
|
|
41
|
+
>
|
|
42
|
+
{link.icon ? <SocialIcon name={link.icon} className="size-4" /> : link.label}
|
|
43
|
+
</a>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function SiteHeaderRender({
|
|
51
|
+
logoUrl,
|
|
52
|
+
logoAlt,
|
|
53
|
+
homeHref,
|
|
54
|
+
utilityLinks,
|
|
55
|
+
navItems,
|
|
56
|
+
ctaLabel,
|
|
57
|
+
ctaHref,
|
|
58
|
+
sticky,
|
|
59
|
+
}: SiteHeaderWidgetProps) {
|
|
60
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
61
|
+
const mobileMenuId = useId();
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
// The public page wraps this slot in its own <header>, which is exactly as
|
|
65
|
+
// tall as this block — so `position: sticky` here would have nothing to
|
|
66
|
+
// travel within. The attribute lets widgets-base.css make that *wrapper*
|
|
67
|
+
// sticky instead; see the `:has()` rule there.
|
|
68
|
+
<div data-pb-site-header={sticky ? 'sticky' : 'static'} className="gca-display">
|
|
69
|
+
<UtilityBar links={utilityLinks} />
|
|
70
|
+
|
|
71
|
+
<div className="border-b border-[color:var(--border,#e4e6e7)] bg-[var(--pb-surface,#fff)]">
|
|
72
|
+
<div className="mx-auto flex h-[72px] max-w-[1440px] items-center justify-between px-4 sm:px-6 lg:h-[112px] lg:px-12">
|
|
73
|
+
<a href={homeHref || '/'} aria-label={logoAlt || 'Home'}>
|
|
74
|
+
{logoUrl ? (
|
|
75
|
+
<img src={logoUrl} alt={logoAlt} className="h-10 w-auto lg:h-16" />
|
|
76
|
+
) : (
|
|
77
|
+
<span className="text-sm font-bold uppercase leading-tight tracking-wide text-primary-800">
|
|
78
|
+
{logoAlt || 'Home'}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
</a>
|
|
82
|
+
|
|
83
|
+
<DesktopNav items={navItems} />
|
|
84
|
+
|
|
85
|
+
{ctaLabel && (
|
|
86
|
+
<a
|
|
87
|
+
href={ctaHref || '#'}
|
|
88
|
+
className="hidden rounded-[var(--pb-button-radius,9999px)] bg-primary-800 px-6 py-2.5 text-[17px] font-semibold text-white transition-colors hover:bg-primary-900 lg:inline-block"
|
|
89
|
+
>
|
|
90
|
+
{ctaLabel}
|
|
91
|
+
</a>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => setMobileOpen((open) => !open)}
|
|
97
|
+
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
|
98
|
+
aria-expanded={mobileOpen}
|
|
99
|
+
aria-controls={mobileMenuId}
|
|
100
|
+
className="-mr-2 inline-flex size-10 items-center justify-center rounded-md text-primary-800 transition-colors hover:text-primary-900 lg:hidden"
|
|
101
|
+
>
|
|
102
|
+
{mobileOpen ? <X className="size-6" /> : <Menu className="size-6" />}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{mobileOpen && (
|
|
108
|
+
<MobileNav
|
|
109
|
+
id={mobileMenuId}
|
|
110
|
+
items={navItems}
|
|
111
|
+
ctaLabel={ctaLabel}
|
|
112
|
+
ctaHref={ctaHref}
|
|
113
|
+
onNavigate={() => setMobileOpen(false)}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const SiteHeaderWidget: ComponentConfig<SiteHeaderWidgetProps> = {
|
|
121
|
+
label: 'Site header (logo + navigation)',
|
|
122
|
+
fields: {
|
|
123
|
+
logoUrl: createImageField(mediaLibraryAdapter, 'Logo'),
|
|
124
|
+
logoAlt: { type: 'text', label: 'Logo alt text' },
|
|
125
|
+
homeHref: { type: 'text', label: 'Logo link' },
|
|
126
|
+
utilityLinks: {
|
|
127
|
+
type: 'array',
|
|
128
|
+
label: 'Utility bar (top strip)',
|
|
129
|
+
arrayFields: {
|
|
130
|
+
label: { type: 'text', label: 'Label' },
|
|
131
|
+
href: { type: 'text', label: 'Link' },
|
|
132
|
+
icon: {
|
|
133
|
+
type: 'select',
|
|
134
|
+
label: 'Show as icon (optional)',
|
|
135
|
+
options: [{ label: 'Text label', value: '' }, ...SOCIAL_ICON_OPTIONS],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
defaultItemProps: { label: 'Link', href: '#', icon: '' },
|
|
139
|
+
getItemSummary: (item) => item.label || 'Link',
|
|
140
|
+
max: 8,
|
|
141
|
+
},
|
|
142
|
+
navItems: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
label: 'Navigation',
|
|
145
|
+
arrayFields: {
|
|
146
|
+
label: { type: 'text', label: 'Label' },
|
|
147
|
+
href: { type: 'text', label: 'Link (ignored when sub-links are set)' },
|
|
148
|
+
children: {
|
|
149
|
+
type: 'array',
|
|
150
|
+
label: 'Sub-links (makes this a dropdown)',
|
|
151
|
+
arrayFields: {
|
|
152
|
+
label: { type: 'text', label: 'Label' },
|
|
153
|
+
href: { type: 'text', label: 'Link' },
|
|
154
|
+
},
|
|
155
|
+
defaultItemProps: { label: 'Sub-link', href: '#' },
|
|
156
|
+
getItemSummary: (child) => child.label || 'Sub-link',
|
|
157
|
+
max: 10,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
defaultItemProps: { label: 'Section', href: '#', children: [] },
|
|
161
|
+
getItemSummary: (item) => item.label || 'Section',
|
|
162
|
+
max: 10,
|
|
163
|
+
},
|
|
164
|
+
ctaLabel: { type: 'text', label: 'Call-to-action label' },
|
|
165
|
+
ctaHref: { type: 'text', label: 'Call-to-action link' },
|
|
166
|
+
sticky: createCheckboxField('Stick to the top of the page on scroll'),
|
|
167
|
+
},
|
|
168
|
+
defaultProps: {
|
|
169
|
+
logoUrl: '',
|
|
170
|
+
logoAlt: 'Home',
|
|
171
|
+
homeHref: '/',
|
|
172
|
+
utilityLinks: [],
|
|
173
|
+
navItems: [
|
|
174
|
+
{ label: 'Home', href: '/', children: [] },
|
|
175
|
+
{ label: 'Contact', href: '#', children: [] },
|
|
176
|
+
],
|
|
177
|
+
ctaLabel: '',
|
|
178
|
+
ctaHref: '#',
|
|
179
|
+
sticky: true,
|
|
180
|
+
},
|
|
181
|
+
render: SiteHeaderRender,
|
|
182
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
3
|
+
import { renderRichText } from './_internal/rich-text';
|
|
4
|
+
|
|
5
|
+
export type SocialBannerLink = {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
iconUrl: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SocialBannerWidgetProps = {
|
|
12
|
+
heading: string;
|
|
13
|
+
subheading: string;
|
|
14
|
+
links: SocialBannerLink[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const SocialBannerWidget: ComponentConfig<SocialBannerWidgetProps> = {
|
|
18
|
+
label: 'Social banner (follow links)',
|
|
19
|
+
fields: {
|
|
20
|
+
heading: { type: 'text', label: 'Heading' },
|
|
21
|
+
subheading: { type: 'text', label: 'Subheading' },
|
|
22
|
+
links: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
label: 'Links',
|
|
25
|
+
arrayFields: {
|
|
26
|
+
label: { type: 'text', label: 'Label' },
|
|
27
|
+
href: { type: 'text', label: 'URL' },
|
|
28
|
+
iconUrl: createImageField(mediaLibraryAdapter, 'Icon URL'),
|
|
29
|
+
},
|
|
30
|
+
defaultItemProps: {
|
|
31
|
+
label: 'Twitter',
|
|
32
|
+
href: '#',
|
|
33
|
+
iconUrl: '',
|
|
34
|
+
},
|
|
35
|
+
min: 1,
|
|
36
|
+
max: 10,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultProps: {
|
|
40
|
+
heading: 'Follow us',
|
|
41
|
+
subheading: 'Stay up to date with the latest news.',
|
|
42
|
+
links: [
|
|
43
|
+
{ label: 'Twitter', href: '#', iconUrl: '' },
|
|
44
|
+
{ label: 'GitHub', href: '#', iconUrl: '' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
render: ({ heading, subheading, links }) => (
|
|
48
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
49
|
+
<div className="rounded-2xl bg-[var(--secondary,#1a353e)] px-6 py-14 text-center text-white sm:py-16">
|
|
50
|
+
{heading && (
|
|
51
|
+
<h2
|
|
52
|
+
className="mx-auto max-w-3xl leading-tight"
|
|
53
|
+
style={{
|
|
54
|
+
fontFamily: 'var(--pb-display-font)',
|
|
55
|
+
fontWeight: 'var(--pb-display-weight)',
|
|
56
|
+
fontSize: 'var(--pb-heading-lg, clamp(1.875rem, 1.3rem + 2.5vw, 3rem))',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{renderRichText(heading)}
|
|
60
|
+
</h2>
|
|
61
|
+
)}
|
|
62
|
+
{subheading && <p className="mt-3 text-lg text-white/80">{renderRichText(subheading)}</p>}
|
|
63
|
+
{links && links.length > 0 && (
|
|
64
|
+
<div className="mt-6 flex flex-wrap items-center justify-center gap-8">
|
|
65
|
+
{links.map((link, idx) => (
|
|
66
|
+
<a
|
|
67
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
68
|
+
key={idx}
|
|
69
|
+
href={link.href || '#'}
|
|
70
|
+
className="inline-flex items-center gap-2 text-xl font-semibold transition-opacity hover:opacity-80"
|
|
71
|
+
>
|
|
72
|
+
{link.iconUrl && (
|
|
73
|
+
<img src={link.iconUrl} alt="" loading="lazy" className="size-5 object-contain" />
|
|
74
|
+
)}
|
|
75
|
+
{renderRichText(link.label, { allowLinks: false })}
|
|
76
|
+
</a>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
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 StatItem = {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
iconUrl?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StatsWidgetProps = {
|
|
14
|
+
title: string;
|
|
15
|
+
subtitle: string;
|
|
16
|
+
items: StatItem[];
|
|
17
|
+
surface: 'default' | 'lime' | 'cards';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Display tokens for the title/numbers — weight/tracking/font come from the
|
|
21
|
+
// per-tenant page-builder theme (see styles.css `:root` for Recodo defaults and
|
|
22
|
+
// `.gca-root` for the GCA overrides). Color is overridden in the lime surface.
|
|
23
|
+
const DISPLAY_STYLE: CSSProperties = {
|
|
24
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
25
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
26
|
+
fontFamily: 'var(--pb-display-font)',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const StatsWidget: ComponentConfig<StatsWidgetProps> = {
|
|
30
|
+
label: 'Statistics',
|
|
31
|
+
fields: {
|
|
32
|
+
title: { type: 'text', label: 'Title' },
|
|
33
|
+
subtitle: { type: 'textarea', label: 'Subtitle' },
|
|
34
|
+
items: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
label: 'Statistics',
|
|
37
|
+
arrayFields: {
|
|
38
|
+
value: { type: 'text', label: 'Value' },
|
|
39
|
+
label: { type: 'text', label: 'Label' },
|
|
40
|
+
iconUrl: createImageField(mediaLibraryAdapter, 'Icon image (optional)'),
|
|
41
|
+
},
|
|
42
|
+
defaultItemProps: { value: '100+', label: 'Item' },
|
|
43
|
+
min: 1,
|
|
44
|
+
max: 12,
|
|
45
|
+
},
|
|
46
|
+
surface: {
|
|
47
|
+
type: 'select',
|
|
48
|
+
label: 'Surface',
|
|
49
|
+
options: [
|
|
50
|
+
{ label: 'Default', value: 'default' },
|
|
51
|
+
{ label: 'Lime', value: 'lime' },
|
|
52
|
+
{ label: 'Cards', value: 'cards' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultProps: {
|
|
57
|
+
title: "We're trusted by thousands",
|
|
58
|
+
subtitle: 'Numbers that speak for themselves',
|
|
59
|
+
items: [
|
|
60
|
+
{ value: '10K+', label: 'Users' },
|
|
61
|
+
{ value: '99.9%', label: 'Uptime' },
|
|
62
|
+
{ value: '24/7', label: 'Support' },
|
|
63
|
+
],
|
|
64
|
+
surface: 'default',
|
|
65
|
+
},
|
|
66
|
+
render: (props) => <StatsView {...props} />,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Presentational stats — shared by the static base widget (above) and the app's
|
|
70
|
+
// live, data-driven override, so both render identically.
|
|
71
|
+
export function StatsView({ title, subtitle, items, surface }: StatsWidgetProps) {
|
|
72
|
+
const count = items?.length ?? 0;
|
|
73
|
+
|
|
74
|
+
if (surface === 'cards') {
|
|
75
|
+
return (
|
|
76
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
77
|
+
{count > 0 && (
|
|
78
|
+
<dl className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
79
|
+
{items.map((stat, idx) => (
|
|
80
|
+
<div
|
|
81
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
82
|
+
key={idx}
|
|
83
|
+
className="relative flex min-h-[142px] flex-col justify-center rounded-[12px] border border-[var(--border,#dcdcdf)] bg-[var(--pb-surface,#ffffff)] px-6 py-5"
|
|
84
|
+
>
|
|
85
|
+
{stat.iconUrl && (
|
|
86
|
+
<span className="absolute right-6 top-6 flex size-14 items-center justify-center rounded-lg bg-[var(--secondary,#42547e)]">
|
|
87
|
+
<img src={stat.iconUrl} alt="" className="size-8 object-contain" />
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<dd
|
|
91
|
+
className="text-[2.75rem] sm:text-[3.5rem] leading-none font-semibold"
|
|
92
|
+
style={{
|
|
93
|
+
fontFamily: 'var(--pb-display-font)',
|
|
94
|
+
color: 'var(--pb-heading-color,#161728)',
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{renderRichText(stat.value)}
|
|
98
|
+
</dd>
|
|
99
|
+
<dt className="mt-3 text-base font-semibold text-[var(--pb-accent,#fa6e25)]">
|
|
100
|
+
{renderRichText(stat.label)}
|
|
101
|
+
</dt>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</dl>
|
|
105
|
+
)}
|
|
106
|
+
</section>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (surface === 'lime') {
|
|
111
|
+
// GCA "Stats item-5 column": left-aligned values and labels on the lime
|
|
112
|
+
// band, one line per value, thin ink dividers (SNAG_007).
|
|
113
|
+
return (
|
|
114
|
+
<section className="container mx-auto py-[var(--pb-section-py,3rem)] px-4 sm:px-6 lg:px-8">
|
|
115
|
+
{count > 0 && (
|
|
116
|
+
<div className="rounded-xl bg-[#d8ec29] px-6 py-8 lg:px-8 lg:py-10">
|
|
117
|
+
<dl className="flex flex-col justify-center sm:flex-row sm:flex-wrap lg:flex-nowrap">
|
|
118
|
+
{items.map((stat, idx) => (
|
|
119
|
+
<div
|
|
120
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
121
|
+
key={idx}
|
|
122
|
+
className={cn(
|
|
123
|
+
'flex flex-1 flex-col px-4 py-4 sm:px-6',
|
|
124
|
+
idx > 0 ? 'sm:border-l sm:border-[#1a353e]/20' : '',
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<dd
|
|
128
|
+
className="order-1 text-[1.875rem] leading-[1.2] text-[#1a353e] sm:text-[2.125rem] xl:whitespace-nowrap"
|
|
129
|
+
style={DISPLAY_STYLE}
|
|
130
|
+
>
|
|
131
|
+
{renderRichText(stat.value)}
|
|
132
|
+
</dd>
|
|
133
|
+
<dt className="order-2 mt-2 text-[18px] text-[#1a353e]">
|
|
134
|
+
{renderRichText(stat.label)}
|
|
135
|
+
</dt>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</dl>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<section className="container mx-auto py-[var(--pb-section-py,3rem)] px-4 sm:px-6 lg:px-8 font-semibold">
|
|
147
|
+
<div className="max-w-4xl mx-auto text-center">
|
|
148
|
+
{title && (
|
|
149
|
+
<h2
|
|
150
|
+
className="text-3xl sm:text-4xl"
|
|
151
|
+
style={{
|
|
152
|
+
...DISPLAY_STYLE,
|
|
153
|
+
color: 'var(--pb-heading-color)',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{renderRichText(title)}
|
|
157
|
+
</h2>
|
|
158
|
+
)}
|
|
159
|
+
{subtitle && <RichTextBlock text={subtitle} className="mt-3 text-xl sm:mt-4" />}
|
|
160
|
+
</div>
|
|
161
|
+
{count > 0 && (
|
|
162
|
+
<dl
|
|
163
|
+
className="mt-10 text-center max-w-3xl mx-auto grid gap-8 grid-cols-1 stats-grid"
|
|
164
|
+
style={
|
|
165
|
+
{
|
|
166
|
+
'--stats-cols': count,
|
|
167
|
+
} as React.CSSProperties
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
{items.map((stat, idx) => (
|
|
171
|
+
<div
|
|
172
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
173
|
+
key={idx}
|
|
174
|
+
className="flex flex-col items-center"
|
|
175
|
+
>
|
|
176
|
+
{stat.iconUrl && (
|
|
177
|
+
<img src={stat.iconUrl} alt="" className="mx-auto mb-3 h-10 w-10 object-contain" />
|
|
178
|
+
)}
|
|
179
|
+
<dt
|
|
180
|
+
className="order-2 mt-2 text-lg leading-6 font-medium"
|
|
181
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
182
|
+
>
|
|
183
|
+
{renderRichText(stat.label)}
|
|
184
|
+
</dt>
|
|
185
|
+
<dd
|
|
186
|
+
className="order-1 text-5xl"
|
|
187
|
+
style={{
|
|
188
|
+
...DISPLAY_STYLE,
|
|
189
|
+
color: 'var(--pb-heading-color)',
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{renderRichText(stat.value)}
|
|
193
|
+
</dd>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</dl>
|
|
197
|
+
)}
|
|
198
|
+
</section>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { cn } from '../../utils/widgetUtils';
|
|
3
|
+
import { AccentText } from './_internal/accent-text';
|
|
4
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
5
|
+
import { eyebrowTextStyle } from './_shared';
|
|
6
|
+
|
|
7
|
+
export type StepItem = {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
links: { label: string; href: string }[];
|
|
11
|
+
showAppBadges: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StepsWidgetProps = {
|
|
15
|
+
eyebrow: string;
|
|
16
|
+
title: string;
|
|
17
|
+
intro: string;
|
|
18
|
+
surface: 'default' | 'muted';
|
|
19
|
+
googlePlaySrc: string;
|
|
20
|
+
appStoreSrc: string;
|
|
21
|
+
items: StepItem[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function ArrowUpRight() {
|
|
25
|
+
return (
|
|
26
|
+
<svg
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth={2}
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
className="size-6 shrink-0 text-[color:var(--pb-link-arrow-color,currentColor)]"
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
>
|
|
36
|
+
<title>Open</title>
|
|
37
|
+
<path d="M7 17 17 7M9 7h8v8" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const StepsWidget: ComponentConfig<StepsWidgetProps> = {
|
|
43
|
+
label: 'Steps (how to)',
|
|
44
|
+
fields: {
|
|
45
|
+
eyebrow: { type: 'text', label: 'Eyebrow' },
|
|
46
|
+
title: { type: 'text', label: 'Title' },
|
|
47
|
+
intro: { type: 'textarea', label: 'Intro text' },
|
|
48
|
+
surface: {
|
|
49
|
+
type: 'select',
|
|
50
|
+
label: 'Surface',
|
|
51
|
+
options: [
|
|
52
|
+
{ label: 'Default', value: 'default' },
|
|
53
|
+
{ label: 'Muted', value: 'muted' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
googlePlaySrc: { type: 'text', label: 'Google Play badge image' },
|
|
57
|
+
appStoreSrc: { type: 'text', label: 'App Store badge image' },
|
|
58
|
+
items: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
label: 'Steps',
|
|
61
|
+
arrayFields: {
|
|
62
|
+
title: { type: 'text', label: 'Step title' },
|
|
63
|
+
description: { type: 'textarea', label: 'Description' },
|
|
64
|
+
links: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
label: 'Links',
|
|
67
|
+
arrayFields: {
|
|
68
|
+
label: { type: 'text', label: 'Label' },
|
|
69
|
+
href: { type: 'text', label: 'Link' },
|
|
70
|
+
},
|
|
71
|
+
defaultItemProps: { label: 'Link', href: '#' },
|
|
72
|
+
},
|
|
73
|
+
showAppBadges: {
|
|
74
|
+
type: 'radio',
|
|
75
|
+
label: 'Show app store badges',
|
|
76
|
+
options: [
|
|
77
|
+
{ label: 'Yes', value: true },
|
|
78
|
+
{ label: 'No', value: false },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultItemProps: {
|
|
83
|
+
title: 'Step',
|
|
84
|
+
description: '',
|
|
85
|
+
links: [],
|
|
86
|
+
showAppBadges: false,
|
|
87
|
+
},
|
|
88
|
+
min: 1,
|
|
89
|
+
max: 12,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
defaultProps: {
|
|
93
|
+
eyebrow: '',
|
|
94
|
+
title: 'How to Contribute Data',
|
|
95
|
+
intro: '',
|
|
96
|
+
surface: 'muted',
|
|
97
|
+
googlePlaySrc: '',
|
|
98
|
+
appStoreSrc: '',
|
|
99
|
+
items: [
|
|
100
|
+
{
|
|
101
|
+
title: '1. Download Geo-Quest',
|
|
102
|
+
description: 'Get the app from the Google Play or Apple App Store.',
|
|
103
|
+
links: [],
|
|
104
|
+
showAppBadges: true,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
// Muted surface renders as a rounded card spanning the container (Mowing
|
|
109
|
+
// Figma "How to Contribute Data": 80px vertical padding, eyebrow inset 48px).
|
|
110
|
+
// Column geometry is tenant-tunable via `--pb-split-cols`/`--pb-split-gap`
|
|
111
|
+
// (Mowing aligns the content to the 4th page-grid column); the 1fr/2fr
|
|
112
|
+
// fallback keeps tenants without the tokens unchanged.
|
|
113
|
+
render: ({ eyebrow, title, intro, surface, googlePlaySrc, appStoreSrc, items }) => (
|
|
114
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
115
|
+
<div
|
|
116
|
+
className={cn(
|
|
117
|
+
'grid gap-y-6 md:grid-cols-[var(--pb-split-cols,1fr_2fr)] md:gap-x-[var(--pb-split-gap,2rem)]',
|
|
118
|
+
surface === 'muted' &&
|
|
119
|
+
'rounded-[12px] bg-[var(--pb-surface-muted,#f3f3f4)] px-8 py-12 lg:px-0 lg:py-20',
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{eyebrow && (
|
|
123
|
+
<p
|
|
124
|
+
className={cn(surface === 'muted' && 'lg:pl-12')}
|
|
125
|
+
style={{
|
|
126
|
+
// Eyebrow size/weight/case/tracking are theme-driven; the
|
|
127
|
+
// fallbacks keep today's look for tenants that don't set them.
|
|
128
|
+
...eyebrowTextStyle('1.125rem', '400'),
|
|
129
|
+
color: 'var(--pb-heading-color,#161728)',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{renderRichText(eyebrow)}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
<div className={cn('max-w-2xl', !eyebrow && 'md:col-span-2')}>
|
|
136
|
+
{title && (
|
|
137
|
+
<h2
|
|
138
|
+
className="leading-tight"
|
|
139
|
+
style={{
|
|
140
|
+
color: 'var(--pb-heading-color)',
|
|
141
|
+
fontWeight: 'var(--pb-display-weight)',
|
|
142
|
+
fontFamily: 'var(--pb-display-font)',
|
|
143
|
+
fontSize: 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 1.4vw, 2.5rem))',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{renderRichText(title)}
|
|
147
|
+
</h2>
|
|
148
|
+
)}
|
|
149
|
+
{intro && (
|
|
150
|
+
<RichTextBlock
|
|
151
|
+
text={intro}
|
|
152
|
+
className="mt-5 text-lg leading-[var(--pb-body-leading,1.625)] text-[var(--pb-body-color,#686873)]"
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
<div className="mt-8 flex flex-col gap-10">
|
|
156
|
+
{items.map((step, idx) => (
|
|
157
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
158
|
+
<div key={idx}>
|
|
159
|
+
<h3 className="text-xl font-semibold" style={{ color: 'var(--pb-heading-color)' }}>
|
|
160
|
+
{renderRichText(step.title)}
|
|
161
|
+
</h3>
|
|
162
|
+
{step.description && (
|
|
163
|
+
// Inline AccentText, not RichTextBlock: step copy uses the
|
|
164
|
+
// tenant accent treatment for `*word*` (Mowing's Figma), and
|
|
165
|
+
// `whitespace-pre-line` keeps single newlines. Bullet lists
|
|
166
|
+
// are deliberately not supported here.
|
|
167
|
+
<p className="mt-2 whitespace-pre-line text-lg leading-[var(--pb-body-leading,1.625)] text-[var(--pb-body-color,#686873)]">
|
|
168
|
+
<AccentText text={step.description} />
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
{step.showAppBadges && (googlePlaySrc || appStoreSrc) && (
|
|
172
|
+
<div className="mt-5 flex flex-wrap gap-4">
|
|
173
|
+
{googlePlaySrc && (
|
|
174
|
+
<img
|
|
175
|
+
src={googlePlaySrc}
|
|
176
|
+
alt="Get it on Google Play"
|
|
177
|
+
className="h-16 w-auto"
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
{appStoreSrc && (
|
|
181
|
+
<img
|
|
182
|
+
src={appStoreSrc}
|
|
183
|
+
alt="Download on the App Store"
|
|
184
|
+
className="h-16 w-auto"
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
{step.links?.length > 0 && (
|
|
190
|
+
<div className="mt-5">
|
|
191
|
+
{step.links.map((l, i) => (
|
|
192
|
+
<a
|
|
193
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
194
|
+
key={i}
|
|
195
|
+
href={l.href}
|
|
196
|
+
className="flex items-center justify-between gap-4 border-b border-[var(--border,#dcdcdf)] py-4 text-lg font-medium text-[color:var(--pb-heading-color,#161728)] transition-colors hover:text-[color:var(--pb-link-hover-color,var(--pb-heading-color,#161728))]"
|
|
197
|
+
>
|
|
198
|
+
{renderRichText(l.label, { allowLinks: false })}
|
|
199
|
+
<ArrowUpRight />
|
|
200
|
+
</a>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</section>
|
|
210
|
+
),
|
|
211
|
+
};
|