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,144 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { EyebrowSplitSection } from './_internal/eyebrow-split-section';
|
|
3
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
4
|
+
|
|
5
|
+
export type DefinitionSection = {
|
|
6
|
+
subheading: string;
|
|
7
|
+
body: string;
|
|
8
|
+
image1: string;
|
|
9
|
+
image1Alt: string;
|
|
10
|
+
image2: string;
|
|
11
|
+
image2Alt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DefinitionItem = {
|
|
15
|
+
title: string;
|
|
16
|
+
sections: DefinitionSection[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DefinitionListWidgetProps = {
|
|
20
|
+
eyebrow: string;
|
|
21
|
+
heading: string;
|
|
22
|
+
items: DefinitionItem[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function SectionImages({ section }: { section: DefinitionSection }) {
|
|
26
|
+
const imgs = [
|
|
27
|
+
section.image1 ? { src: section.image1, alt: section.image1Alt } : null,
|
|
28
|
+
section.image2 ? { src: section.image2, alt: section.image2Alt } : null,
|
|
29
|
+
].filter((x): x is { src: string; alt: string } => x !== null);
|
|
30
|
+
if (imgs.length === 0) return null;
|
|
31
|
+
return (
|
|
32
|
+
<div className={imgs.length === 2 ? 'grid grid-cols-2 gap-4' : ''}>
|
|
33
|
+
{imgs.map((img, i) => (
|
|
34
|
+
<img
|
|
35
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
36
|
+
key={i}
|
|
37
|
+
src={img.src}
|
|
38
|
+
alt={img.alt}
|
|
39
|
+
loading="lazy"
|
|
40
|
+
className="aspect-[3/2] w-full rounded-lg object-cover"
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DefinitionListWidget: ComponentConfig<DefinitionListWidgetProps> = {
|
|
48
|
+
label: 'Definition list (rich accordion)',
|
|
49
|
+
fields: {
|
|
50
|
+
eyebrow: { type: 'text', label: 'Eyebrow' },
|
|
51
|
+
heading: { type: 'text', label: 'Heading' },
|
|
52
|
+
items: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
label: 'Definitions',
|
|
55
|
+
arrayFields: {
|
|
56
|
+
title: { type: 'text', label: 'Title' },
|
|
57
|
+
sections: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
label: 'Sections',
|
|
60
|
+
arrayFields: {
|
|
61
|
+
subheading: { type: 'text', label: 'Sub-heading' },
|
|
62
|
+
body: { type: 'textarea', label: 'Body' },
|
|
63
|
+
image1: { type: 'text', label: 'Image 1 URL' },
|
|
64
|
+
image1Alt: { type: 'text', label: 'Image 1 alt' },
|
|
65
|
+
image2: { type: 'text', label: 'Image 2 URL (optional)' },
|
|
66
|
+
image2Alt: { type: 'text', label: 'Image 2 alt' },
|
|
67
|
+
},
|
|
68
|
+
defaultItemProps: {
|
|
69
|
+
subheading: '',
|
|
70
|
+
body: '',
|
|
71
|
+
image1: '',
|
|
72
|
+
image1Alt: '',
|
|
73
|
+
image2: '',
|
|
74
|
+
image2Alt: '',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultItemProps: { title: 'New definition', sections: [] },
|
|
79
|
+
min: 1,
|
|
80
|
+
max: 20,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
defaultProps: {
|
|
84
|
+
eyebrow: 'Definitions',
|
|
85
|
+
heading: 'Definitions',
|
|
86
|
+
items: [{ title: 'Definition', sections: [] }],
|
|
87
|
+
},
|
|
88
|
+
render: ({ eyebrow, heading, items }) => (
|
|
89
|
+
<EyebrowSplitSection eyebrow={eyebrow} heading={heading}>
|
|
90
|
+
<div className="border-t border-[#1a353e]/15">
|
|
91
|
+
{items?.map((item, idx) => (
|
|
92
|
+
<details
|
|
93
|
+
// Index key: items have no stable id, and keying by title would
|
|
94
|
+
// collide on duplicate titles (defaultItemProps.title is fixed).
|
|
95
|
+
// Reordering in the editor can rebind open-state to a slot — an
|
|
96
|
+
// accepted editor-only edge until items carry an id.
|
|
97
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
98
|
+
key={idx}
|
|
99
|
+
className="group border-b border-[#1a353e]/15"
|
|
100
|
+
// `open` set at mount only (widget has no state → no re-render
|
|
101
|
+
// re-asserts it), so the first row starts open but stays toggleable.
|
|
102
|
+
{...(idx === 0 ? { open: true } : {})}
|
|
103
|
+
>
|
|
104
|
+
<summary
|
|
105
|
+
className="flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg font-semibold"
|
|
106
|
+
style={{ color: 'var(--pb-heading-color)' }}
|
|
107
|
+
>
|
|
108
|
+
{/* Inside <summary>: emphasis only — a nested anchor here
|
|
109
|
+
would be invalid HTML and hijack the toggle. */}
|
|
110
|
+
{renderRichText(item.title || '', { allowLinks: false })}
|
|
111
|
+
<span
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
className="shrink-0 text-2xl font-light leading-none text-[#1a353e]"
|
|
114
|
+
>
|
|
115
|
+
<span className="group-open:hidden">+</span>
|
|
116
|
+
<span className="hidden group-open:inline">−</span>
|
|
117
|
+
</span>
|
|
118
|
+
</summary>
|
|
119
|
+
<div className="flex flex-col gap-5 pb-6">
|
|
120
|
+
{item.sections?.map((section, si) => (
|
|
121
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
122
|
+
<div key={si} className="flex flex-col gap-3">
|
|
123
|
+
{section.subheading && (
|
|
124
|
+
<p className="font-semibold" style={{ color: 'var(--pb-heading-color)' }}>
|
|
125
|
+
{renderRichText(section.subheading)}
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
{section.body && (
|
|
129
|
+
<RichTextBlock
|
|
130
|
+
text={section.body}
|
|
131
|
+
className="leading-[var(--pb-body-leading,1.625)]"
|
|
132
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
<SectionImages section={section} />
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
</details>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</EyebrowSplitSection>
|
|
143
|
+
),
|
|
144
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { cn } from '../../utils/widgetUtils';
|
|
3
|
+
|
|
4
|
+
export type DividerWidgetProps = {
|
|
5
|
+
thickness: 'thin' | 'medium' | 'thick';
|
|
6
|
+
style: 'solid' | 'dashed' | 'dotted';
|
|
7
|
+
color: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const THICKNESS_CLASS: Record<DividerWidgetProps['thickness'], string> = {
|
|
11
|
+
thin: 'border-t',
|
|
12
|
+
medium: 'border-t-2',
|
|
13
|
+
thick: 'border-t-4',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STYLE_CLASS: Record<DividerWidgetProps['style'], string> = {
|
|
17
|
+
solid: 'border-solid',
|
|
18
|
+
dashed: 'border-dashed',
|
|
19
|
+
dotted: 'border-dotted',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const DividerWidget: ComponentConfig<DividerWidgetProps> = {
|
|
23
|
+
label: 'Divider',
|
|
24
|
+
fields: {
|
|
25
|
+
thickness: {
|
|
26
|
+
type: 'select',
|
|
27
|
+
label: 'Thickness',
|
|
28
|
+
options: [
|
|
29
|
+
{ label: 'Thin', value: 'thin' },
|
|
30
|
+
{ label: 'Medium', value: 'medium' },
|
|
31
|
+
{ label: 'Thick', value: 'thick' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
style: {
|
|
35
|
+
type: 'select',
|
|
36
|
+
label: 'Style',
|
|
37
|
+
options: [
|
|
38
|
+
{ label: 'Solid', value: 'solid' },
|
|
39
|
+
{ label: 'Dashed', value: 'dashed' },
|
|
40
|
+
{ label: 'Dotted', value: 'dotted' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
color: { type: 'text', label: 'Color (CSS value)' },
|
|
44
|
+
},
|
|
45
|
+
defaultProps: {
|
|
46
|
+
thickness: 'thin',
|
|
47
|
+
style: 'solid',
|
|
48
|
+
color: '#e5e7eb',
|
|
49
|
+
},
|
|
50
|
+
render: ({ thickness, style, color }) => (
|
|
51
|
+
<hr
|
|
52
|
+
className={cn('my-6', THICKNESS_CLASS[thickness], STYLE_CLASS[style])}
|
|
53
|
+
style={{ borderColor: color }}
|
|
54
|
+
/>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import { AccentText } from './_internal/accent-text';
|
|
4
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
5
|
+
|
|
6
|
+
export type EyebrowSectionWidgetProps = {
|
|
7
|
+
eyebrow: string;
|
|
8
|
+
heading: string;
|
|
9
|
+
body: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const EyebrowSectionWidget: ComponentConfig<EyebrowSectionWidgetProps> = {
|
|
13
|
+
label: 'Eyebrow + heading + body',
|
|
14
|
+
fields: {
|
|
15
|
+
eyebrow: { type: 'text', label: 'Eyebrow' },
|
|
16
|
+
heading: { type: 'text', label: 'Heading' },
|
|
17
|
+
body: { type: 'textarea', label: 'Body' },
|
|
18
|
+
},
|
|
19
|
+
defaultProps: {
|
|
20
|
+
eyebrow: 'Our story',
|
|
21
|
+
heading: 'How we got here',
|
|
22
|
+
body: 'Body text describing the section in more detail.',
|
|
23
|
+
},
|
|
24
|
+
render: ({ eyebrow, heading, body }) => (
|
|
25
|
+
<section className="container mx-auto px-4 py-12 max-w-3xl">
|
|
26
|
+
{eyebrow && (
|
|
27
|
+
<p className="text-sm font-semibold uppercase tracking-wider text-primary-700">
|
|
28
|
+
{renderRichText(eyebrow)}
|
|
29
|
+
</p>
|
|
30
|
+
)}
|
|
31
|
+
{/* AccentText, not bare renderRichText: this is a display heading on
|
|
32
|
+
`--pb-display-weight`, where a plain <strong>'s relative `bolder`
|
|
33
|
+
can compute to no visible change. */}
|
|
34
|
+
{heading && (
|
|
35
|
+
<h2
|
|
36
|
+
className="mt-2 text-2xl sm:text-3xl lg:text-4xl"
|
|
37
|
+
style={{
|
|
38
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
39
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
40
|
+
fontFamily: 'var(--pb-display-font)',
|
|
41
|
+
color: 'var(--pb-heading-color)',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<AccentText text={heading} />
|
|
45
|
+
</h2>
|
|
46
|
+
)}
|
|
47
|
+
{body && (
|
|
48
|
+
<RichTextBlock
|
|
49
|
+
text={body}
|
|
50
|
+
className="mt-4 text-lg leading-relaxed"
|
|
51
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</section>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/** FAQ types, styles and render — split from faq-widget.tsx, which keeps the
|
|
2
|
+
* field definitions, so both stay under the 300-line cap. */
|
|
3
|
+
|
|
4
|
+
import type { CSSProperties } from 'react';
|
|
5
|
+
|
|
6
|
+
import { parseMarkdownBlocks } from '../../utils/markdown';
|
|
7
|
+
import { cn } from '../../utils/widgetUtils';
|
|
8
|
+
import { AccentText } from './_internal/accent-text';
|
|
9
|
+
import { DisclosureChevron } from './_internal/disclosure-chevron';
|
|
10
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
11
|
+
import { eyebrowTextStyle } from './_shared';
|
|
12
|
+
|
|
13
|
+
export type FaqItem = {
|
|
14
|
+
question: string;
|
|
15
|
+
answer: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FaqWidgetProps = {
|
|
19
|
+
eyebrow: string;
|
|
20
|
+
title: string;
|
|
21
|
+
/** Optional paragraph under the title (2-column layout only). */
|
|
22
|
+
lead?: string;
|
|
23
|
+
variant: 'cards' | 'divider';
|
|
24
|
+
/** "muted" wraps the 2-column layout in a soft rounded card (Mowing FAQs). */
|
|
25
|
+
surface?: 'default' | 'muted';
|
|
26
|
+
/**
|
|
27
|
+
* Optional CSS color for the 2-column band background (e.g. a tenant's
|
|
28
|
+
* program color). When set, the band renders with white text and rules.
|
|
29
|
+
*/
|
|
30
|
+
surfaceColor?: string;
|
|
31
|
+
items: FaqItem[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const DISPLAY_STYLE: CSSProperties = {
|
|
35
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
36
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
37
|
+
fontFamily: 'var(--pb-display-font)',
|
|
38
|
+
color: 'var(--pb-heading-color)',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Section heading size: reads `--pb-heading-md` (Mowing → 40px to match the
|
|
42
|
+
// Figma) and falls back to the prior responsive scale (~30→48px) so tenants
|
|
43
|
+
// that don't set the token (GCA, Recodo) render unchanged.
|
|
44
|
+
const HEADING_STYLE: CSSProperties = {
|
|
45
|
+
...DISPLAY_STYLE,
|
|
46
|
+
fontSize: 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 2.2vw, 3rem))',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** An answer is light markdown: links, `- ` bullets and `**bold**`. */
|
|
50
|
+
function Answer({ text, inverse }: { text: string; inverse?: boolean }) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className="pb-5 leading-[var(--pb-body-leading,1.625)]"
|
|
54
|
+
style={
|
|
55
|
+
inverse
|
|
56
|
+
? { color: 'var(--pb-surface-contrast, #ffffff)', opacity: 0.85 }
|
|
57
|
+
: { color: 'var(--pb-body-color)' }
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{parseMarkdownBlocks(text || '').map((block, i) =>
|
|
61
|
+
block.type === 'ul' ? (
|
|
62
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional slices of one string, re-derived each render
|
|
63
|
+
<ul key={i} className="list-disc pl-5 [&:not(:first-child)]:mt-3">
|
|
64
|
+
{block.items.map((li, j) => (
|
|
65
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: list items are positional slices too
|
|
66
|
+
<li key={j}>{renderRichText(li)}</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
) : (
|
|
70
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional slices of one string, re-derived each render
|
|
71
|
+
<p key={i} className="[&:not(:first-child)]:mt-3">
|
|
72
|
+
{renderRichText(block.text)}
|
|
73
|
+
</p>
|
|
74
|
+
),
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Divider-row accordion (thin top/bottom rules, +/- toggle) — the GCA style,
|
|
81
|
+
// shared by the 2-column (our-process) and single-column (FAQs / statements)
|
|
82
|
+
// layouts.
|
|
83
|
+
function DividerItems({ items, inverse = false }: { items: FaqItem[]; inverse?: boolean }) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={cn(
|
|
87
|
+
'border-t-[length:var(--pb-accordion-top-rule,1px)]',
|
|
88
|
+
inverse ? 'border-white/30' : 'border-[#1a353e]/15',
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{items?.map((item, idx) => (
|
|
92
|
+
// Keyed on the question so reordering in the editor carries each row's
|
|
93
|
+
// open/closed state with it; the index is only a fallback for a blank one.
|
|
94
|
+
<details
|
|
95
|
+
key={item.question || `faq-${idx}`}
|
|
96
|
+
className={cn('group border-b', inverse ? 'border-white/30' : 'border-[#1a353e]/15')}
|
|
97
|
+
>
|
|
98
|
+
<summary
|
|
99
|
+
className="flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg font-semibold"
|
|
100
|
+
style={{
|
|
101
|
+
color: inverse ? 'var(--pb-surface-contrast, #ffffff)' : 'var(--pb-heading-color)',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{/* Inside <summary>: emphasis only — a nested anchor here would
|
|
105
|
+
be invalid HTML and hijack the toggle. */}
|
|
106
|
+
{renderRichText(item.question || '', { allowLinks: false })}
|
|
107
|
+
<span
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
className={cn(
|
|
110
|
+
'shrink-0 text-2xl font-light leading-none',
|
|
111
|
+
!inverse && 'text-[#1a353e]',
|
|
112
|
+
)}
|
|
113
|
+
style={inverse ? { color: 'var(--pb-surface-contrast, #ffffff)' } : undefined}
|
|
114
|
+
>
|
|
115
|
+
<span className="group-open:hidden">+</span>
|
|
116
|
+
<span className="hidden group-open:inline">−</span>
|
|
117
|
+
</span>
|
|
118
|
+
</summary>
|
|
119
|
+
<Answer text={item.answer} inverse={inverse} />
|
|
120
|
+
</details>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Figma 2-column layout: eyebrow left, heading + divider accordion right. */
|
|
127
|
+
function SplitFaq({
|
|
128
|
+
eyebrow,
|
|
129
|
+
title,
|
|
130
|
+
lead,
|
|
131
|
+
surface,
|
|
132
|
+
surfaceColor,
|
|
133
|
+
items,
|
|
134
|
+
}: Required<Pick<FaqWidgetProps, 'eyebrow' | 'title' | 'items'>> &
|
|
135
|
+
Pick<FaqWidgetProps, 'lead' | 'surface' | 'surfaceColor'>) {
|
|
136
|
+
const muted = surface === 'muted';
|
|
137
|
+
const inverse = !!surfaceColor;
|
|
138
|
+
return (
|
|
139
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
'grid gap-8 lg:grid-cols-[var(--pb-split-cols,1fr_2fr)] lg:gap-[var(--pb-split-gap,3rem)]',
|
|
143
|
+
muted &&
|
|
144
|
+
!inverse &&
|
|
145
|
+
'rounded-[12px] bg-[var(--pb-surface-muted,#f3f3f4)] px-8 py-12 lg:px-0 lg:py-20',
|
|
146
|
+
inverse && 'rounded-[12px] px-8 py-12 lg:px-0 lg:py-20',
|
|
147
|
+
)}
|
|
148
|
+
style={inverse ? { backgroundColor: surfaceColor } : undefined}
|
|
149
|
+
>
|
|
150
|
+
<p
|
|
151
|
+
className={cn((muted || inverse) && 'lg:pl-12')}
|
|
152
|
+
style={{
|
|
153
|
+
// Eyebrow size/weight/case/tracking are theme-driven; the
|
|
154
|
+
// fallbacks keep today's look for tenants that don't set them.
|
|
155
|
+
...eyebrowTextStyle('1.125rem', '400'),
|
|
156
|
+
color: inverse
|
|
157
|
+
? 'var(--pb-surface-contrast, #ffffff)'
|
|
158
|
+
: 'var(--pb-heading-color,#161728)',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{renderRichText(eyebrow)}
|
|
162
|
+
</p>
|
|
163
|
+
<div className="max-w-[var(--pb-split-content-max,none)]">
|
|
164
|
+
<div className="max-w-[var(--pb-faq-intro-max,none)]">
|
|
165
|
+
{/* AccentText, not bare renderRichText: these are display headings
|
|
166
|
+
on `--pb-display-weight`, where a plain <strong>'s relative
|
|
167
|
+
`bolder` can compute to no visible change. */}
|
|
168
|
+
{title && (
|
|
169
|
+
<h2
|
|
170
|
+
className="mb-8 leading-tight"
|
|
171
|
+
style={
|
|
172
|
+
inverse
|
|
173
|
+
? { ...HEADING_STYLE, color: 'var(--pb-surface-contrast, #ffffff)' }
|
|
174
|
+
: HEADING_STYLE
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
<AccentText text={title} />
|
|
178
|
+
</h2>
|
|
179
|
+
)}
|
|
180
|
+
{lead && (
|
|
181
|
+
<RichTextBlock
|
|
182
|
+
text={lead}
|
|
183
|
+
className="-mt-4 mb-8 text-lg leading-[var(--pb-body-leading,1.625)]"
|
|
184
|
+
style={
|
|
185
|
+
inverse
|
|
186
|
+
? { color: 'var(--pb-surface-contrast, #ffffff)', opacity: 0.85 }
|
|
187
|
+
: { color: 'var(--pb-body-color)' }
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
<DividerItems items={items} inverse={inverse} />
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</section>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function FaqRender({
|
|
200
|
+
eyebrow,
|
|
201
|
+
title,
|
|
202
|
+
lead = '',
|
|
203
|
+
variant = 'cards',
|
|
204
|
+
surface = 'default',
|
|
205
|
+
surfaceColor = '',
|
|
206
|
+
items,
|
|
207
|
+
}: FaqWidgetProps) {
|
|
208
|
+
// The 2-column layout is opt-in via `eyebrow`.
|
|
209
|
+
if (eyebrow) {
|
|
210
|
+
return (
|
|
211
|
+
<SplitFaq
|
|
212
|
+
eyebrow={eyebrow}
|
|
213
|
+
title={title}
|
|
214
|
+
lead={lead}
|
|
215
|
+
surface={surface}
|
|
216
|
+
surfaceColor={surfaceColor}
|
|
217
|
+
items={items}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Single narrow-column divider accordion (FAQs / statements pages — the page
|
|
223
|
+
// hero already supplies the title, so this is usually just the list).
|
|
224
|
+
if (variant === 'divider') {
|
|
225
|
+
return (
|
|
226
|
+
<section className="container mx-auto max-w-3xl px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
227
|
+
{title && (
|
|
228
|
+
<h2 className="mb-8 leading-tight" style={HEADING_STYLE}>
|
|
229
|
+
<AccentText text={title} />
|
|
230
|
+
</h2>
|
|
231
|
+
)}
|
|
232
|
+
<DividerItems items={items} />
|
|
233
|
+
</section>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] max-w-3xl">
|
|
239
|
+
{title && (
|
|
240
|
+
<h2 className="text-3xl sm:text-4xl lg:text-5xl text-center mb-10" style={DISPLAY_STYLE}>
|
|
241
|
+
<AccentText text={title} />
|
|
242
|
+
</h2>
|
|
243
|
+
)}
|
|
244
|
+
<div className="space-y-3">
|
|
245
|
+
{items?.map((item, idx) => (
|
|
246
|
+
<details
|
|
247
|
+
key={item.question || `faq-card-${idx}`}
|
|
248
|
+
className="group rounded-lg border bg-white dark:bg-gray-900 p-4"
|
|
249
|
+
>
|
|
250
|
+
<summary
|
|
251
|
+
className="cursor-pointer flex items-center justify-between text-lg font-semibold list-none"
|
|
252
|
+
style={{ color: 'var(--pb-heading-color)' }}
|
|
253
|
+
>
|
|
254
|
+
{renderRichText(item.question || '', { allowLinks: false })}
|
|
255
|
+
<DisclosureChevron />
|
|
256
|
+
</summary>
|
|
257
|
+
{/* Same light markdown as the divider variant's answers. */}
|
|
258
|
+
<RichTextBlock
|
|
259
|
+
text={item.answer}
|
|
260
|
+
className="mt-3 leading-[var(--pb-body-leading,1.625)]"
|
|
261
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
262
|
+
/>
|
|
263
|
+
</details>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** FAQ widget: field definitions and config assembly.
|
|
2
|
+
* Types, styles and render live in faq-render.tsx. */
|
|
3
|
+
|
|
4
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
5
|
+
|
|
6
|
+
import { FaqRender, type FaqWidgetProps } from './faq-render';
|
|
7
|
+
|
|
8
|
+
export type { FaqItem, FaqWidgetProps } from './faq-render';
|
|
9
|
+
|
|
10
|
+
export const FaqWidget: ComponentConfig<FaqWidgetProps> = {
|
|
11
|
+
label: 'FAQ',
|
|
12
|
+
fields: {
|
|
13
|
+
eyebrow: { type: 'text', label: 'Eyebrow (enables 2-column layout)' },
|
|
14
|
+
title: { type: 'text', label: 'Title' },
|
|
15
|
+
lead: { type: 'textarea', label: 'Lead paragraph (2-column layout)' },
|
|
16
|
+
variant: {
|
|
17
|
+
type: 'select',
|
|
18
|
+
label: 'Style',
|
|
19
|
+
options: [
|
|
20
|
+
{ label: 'Cards', value: 'cards' },
|
|
21
|
+
{ label: 'Divider rows', value: 'divider' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
surface: {
|
|
25
|
+
type: 'select',
|
|
26
|
+
label: 'Surface (2-column layout)',
|
|
27
|
+
options: [
|
|
28
|
+
{ label: 'Default', value: 'default' },
|
|
29
|
+
{ label: 'Muted (soft card)', value: 'muted' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
surfaceColor: {
|
|
33
|
+
type: 'text',
|
|
34
|
+
label: 'Surface color (CSS, white text — optional)',
|
|
35
|
+
},
|
|
36
|
+
items: {
|
|
37
|
+
type: 'array',
|
|
38
|
+
label: 'Questions',
|
|
39
|
+
arrayFields: {
|
|
40
|
+
question: { type: 'text', label: 'Question' },
|
|
41
|
+
answer: { type: 'textarea', label: 'Answer' },
|
|
42
|
+
},
|
|
43
|
+
defaultItemProps: {
|
|
44
|
+
question: 'Your question?',
|
|
45
|
+
answer: 'The answer goes here.',
|
|
46
|
+
},
|
|
47
|
+
min: 1,
|
|
48
|
+
max: 30,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultProps: {
|
|
52
|
+
eyebrow: '',
|
|
53
|
+
title: 'Frequently asked questions',
|
|
54
|
+
lead: '',
|
|
55
|
+
variant: 'cards',
|
|
56
|
+
surface: 'default',
|
|
57
|
+
surfaceColor: '',
|
|
58
|
+
items: [
|
|
59
|
+
{ question: 'How does this work?', answer: 'It just does.' },
|
|
60
|
+
{ question: 'Is it free?', answer: "There's a free plan." },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
render: (props) => <FaqRender {...props} />,
|
|
64
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
4
|
+
import { AccentText } from './_internal/accent-text';
|
|
5
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
6
|
+
|
|
7
|
+
export type FeatureCardWidgetProps = {
|
|
8
|
+
eyebrow: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
imageAlt: string;
|
|
13
|
+
linkLabel: string;
|
|
14
|
+
linkHref: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const FeatureCardWidget: ComponentConfig<FeatureCardWidgetProps> = {
|
|
18
|
+
label: 'Feature card (image + text)',
|
|
19
|
+
fields: {
|
|
20
|
+
eyebrow: { type: 'text', label: 'Eyebrow tag' },
|
|
21
|
+
title: { type: 'text', label: 'Title' },
|
|
22
|
+
description: { type: 'textarea', label: 'Description' },
|
|
23
|
+
imageUrl: createImageField(mediaLibraryAdapter, 'Image URL'),
|
|
24
|
+
imageAlt: { type: 'text', label: 'Image alt text' },
|
|
25
|
+
linkLabel: { type: 'text', label: 'Link label' },
|
|
26
|
+
linkHref: { type: 'text', label: 'Link URL' },
|
|
27
|
+
},
|
|
28
|
+
defaultProps: {
|
|
29
|
+
eyebrow: 'Featured',
|
|
30
|
+
title: 'A great looking card',
|
|
31
|
+
description: 'Pair an image with text and a clear call to action.',
|
|
32
|
+
imageUrl: '',
|
|
33
|
+
imageAlt: '',
|
|
34
|
+
linkLabel: 'Read more',
|
|
35
|
+
linkHref: '#',
|
|
36
|
+
},
|
|
37
|
+
render: ({ eyebrow, title, description, imageUrl, imageAlt, linkLabel, linkHref }) => (
|
|
38
|
+
<article className="container mx-auto px-4 py-16 lg:py-20 sm:max-w-xl md:max-w-full lg:max-w-screen-xl md:px-24 lg:px-8">
|
|
39
|
+
<div className="flex flex-col max-w-screen-lg overflow-hidden border rounded shadow-sm lg:flex-row sm:mx-auto">
|
|
40
|
+
<div className="relative lg:w-1/2">
|
|
41
|
+
{imageUrl ? (
|
|
42
|
+
<img
|
|
43
|
+
src={imageUrl}
|
|
44
|
+
alt={imageAlt || title}
|
|
45
|
+
loading="lazy"
|
|
46
|
+
className="object-cover w-full lg:absolute h-80 lg:h-full"
|
|
47
|
+
/>
|
|
48
|
+
) : (
|
|
49
|
+
<div className="w-full lg:absolute h-80 lg:h-full bg-gradient-to-br from-gray-200 to-gray-300" />
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex flex-col justify-center p-8 lg:p-16 lg:pl-10 lg:w-1/2">
|
|
53
|
+
{eyebrow && (
|
|
54
|
+
<p className="inline-block px-3 py-px mb-4 text-xs font-semibold tracking-wider uppercase rounded-full bg-teal-100 text-teal-800 w-fit">
|
|
55
|
+
{renderRichText(eyebrow)}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
{/* AccentText, not bare renderRichText: this is a display heading on
|
|
59
|
+
`--pb-display-weight`, where a plain <strong>'s relative `bolder`
|
|
60
|
+
can compute to no visible change. */}
|
|
61
|
+
{title && (
|
|
62
|
+
<h3
|
|
63
|
+
className="mb-3 text-3xl leading-none sm:text-4xl"
|
|
64
|
+
style={{
|
|
65
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
66
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
67
|
+
fontFamily: 'var(--pb-display-font)',
|
|
68
|
+
color: 'var(--pb-heading-color)',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<AccentText text={title} />
|
|
72
|
+
</h3>
|
|
73
|
+
)}
|
|
74
|
+
{description && <RichTextBlock text={description} className="mb-5" />}
|
|
75
|
+
{linkLabel && (
|
|
76
|
+
<div className="flex items-center">
|
|
77
|
+
<a
|
|
78
|
+
href={linkHref || '#'}
|
|
79
|
+
className="inline-flex items-center font-semibold transition-colors duration-200 text-primary-700 hover:text-primary-900"
|
|
80
|
+
>
|
|
81
|
+
{renderRichText(linkLabel, { allowLinks: false })}
|
|
82
|
+
<svg
|
|
83
|
+
className="inline-block w-3 ml-2"
|
|
84
|
+
fill="currentColor"
|
|
85
|
+
viewBox="0 0 12 12"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
>
|
|
88
|
+
<title>Arrow right</title>
|
|
89
|
+
<path d="M9.707,5.293l-5-5A1,1,0,0,0,3.293,1.707L7.586,6,3.293,10.293a1,1,0,1,0,1.414,1.414l5-5A1,1,0,0,0,9.707,5.293Z" />
|
|
90
|
+
</svg>
|
|
91
|
+
</a>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</article>
|
|
97
|
+
),
|
|
98
|
+
};
|