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,44 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { createCheckboxField } from '../../fields';
|
|
3
|
+
import { EmptyPlaceholder } from './_shared';
|
|
4
|
+
|
|
5
|
+
export type IframeWidgetProps = {
|
|
6
|
+
src: string;
|
|
7
|
+
title: string;
|
|
8
|
+
height: string;
|
|
9
|
+
sandboxed: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const IframeWidget: ComponentConfig<IframeWidgetProps> = {
|
|
13
|
+
label: 'Iframe',
|
|
14
|
+
fields: {
|
|
15
|
+
src: { type: 'text', label: 'Source URL' },
|
|
16
|
+
title: { type: 'text', label: 'Title (accessibility)' },
|
|
17
|
+
height: { type: 'text', label: 'Height (e.g. 600px)' },
|
|
18
|
+
sandboxed: createCheckboxField('Sandbox (block popups & navigation)'),
|
|
19
|
+
},
|
|
20
|
+
defaultProps: {
|
|
21
|
+
src: '',
|
|
22
|
+
title: 'Embedded content',
|
|
23
|
+
height: '600px',
|
|
24
|
+
sandboxed: true,
|
|
25
|
+
},
|
|
26
|
+
render: ({ src, title, height, sandboxed }) =>
|
|
27
|
+
src ? (
|
|
28
|
+
<div className="container mx-auto py-6">
|
|
29
|
+
<iframe
|
|
30
|
+
src={src}
|
|
31
|
+
title={title || 'Embedded content'}
|
|
32
|
+
className="w-full rounded-lg border"
|
|
33
|
+
style={{ height }}
|
|
34
|
+
// NOTE: allow-scripts + allow-same-origin lets a SAME-ORIGIN embedded
|
|
35
|
+
// document remove its own sandbox; the protection is meaningful for
|
|
36
|
+
// cross-origin embeds (the primary use). Do not "simplify" this set
|
|
37
|
+
// or the label without revisiting that trade-off.
|
|
38
|
+
sandbox={sandboxed ? 'allow-scripts allow-same-origin allow-forms' : undefined}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
) : (
|
|
42
|
+
<EmptyPlaceholder label="Iframe (no source)" />
|
|
43
|
+
),
|
|
44
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
3
|
+
import { cn } from '../../utils/widgetUtils';
|
|
4
|
+
import { AccentText } from './_internal/accent-text';
|
|
5
|
+
import { renderRichText } from './_internal/rich-text';
|
|
6
|
+
|
|
7
|
+
export type LeaderboardRow = {
|
|
8
|
+
rank: string;
|
|
9
|
+
name: string;
|
|
10
|
+
score: string;
|
|
11
|
+
avatarUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LeaderboardWidgetProps = {
|
|
15
|
+
eyebrow: string;
|
|
16
|
+
title: string;
|
|
17
|
+
surface: 'default' | 'navy';
|
|
18
|
+
top: { name: string; score: string; avatarUrl: string }[];
|
|
19
|
+
rows: LeaderboardRow[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Inline icons keep the shared widget dependency-free.
|
|
23
|
+
function CrownIcon() {
|
|
24
|
+
return (
|
|
25
|
+
<svg viewBox="0 0 24 24" fill="#f5b40a" className="size-6 shrink-0" aria-hidden="true">
|
|
26
|
+
<title>Top rank</title>
|
|
27
|
+
<path d="M5 16 3 6l5 4 4-6 4 6 5-4-2 10H5Zm0 2h14v2H5v-2Z" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function UserGlyph() {
|
|
33
|
+
return (
|
|
34
|
+
<svg viewBox="0 0 24 24" fill="#ffffff" className="size-1/2" aria-hidden="true">
|
|
35
|
+
<title>User</title>
|
|
36
|
+
<path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4 0-7 2.2-7 5v1h14v-1c0-2.8-3-5-7-5Z" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Avatar: photo when supplied, else a pink disc with a white user glyph
|
|
42
|
+
// (matches the Figma's #f1708a fallback avatars).
|
|
43
|
+
function Avatar({ src, size }: { src?: string; size: string }) {
|
|
44
|
+
if (src) {
|
|
45
|
+
return <img src={src} alt="" className={cn('shrink-0 rounded-full object-cover', size)} />;
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
className={cn('flex shrink-0 items-center justify-center rounded-full bg-[#f1708a]', size)}
|
|
50
|
+
>
|
|
51
|
+
<UserGlyph />
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const LeaderboardWidget: ComponentConfig<LeaderboardWidgetProps> = {
|
|
57
|
+
label: 'Leaderboard',
|
|
58
|
+
fields: {
|
|
59
|
+
eyebrow: { type: 'text', label: 'Eyebrow' },
|
|
60
|
+
title: { type: 'text', label: 'Title' },
|
|
61
|
+
surface: {
|
|
62
|
+
type: 'select',
|
|
63
|
+
label: 'Surface',
|
|
64
|
+
options: [
|
|
65
|
+
{ label: 'Default', value: 'default' },
|
|
66
|
+
{ label: 'Navy band', value: 'navy' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
top: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
label: 'Podium (top 3)',
|
|
72
|
+
arrayFields: {
|
|
73
|
+
name: { type: 'text', label: 'Name' },
|
|
74
|
+
score: { type: 'text', label: 'Score' },
|
|
75
|
+
avatarUrl: createImageField(mediaLibraryAdapter, 'Avatar image'),
|
|
76
|
+
},
|
|
77
|
+
defaultItemProps: { name: 'Player', score: '0', avatarUrl: '' },
|
|
78
|
+
min: 1,
|
|
79
|
+
max: 3,
|
|
80
|
+
},
|
|
81
|
+
rows: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
label: 'Ranked rows',
|
|
84
|
+
arrayFields: {
|
|
85
|
+
rank: { type: 'text', label: 'Rank' },
|
|
86
|
+
name: { type: 'text', label: 'Name' },
|
|
87
|
+
score: { type: 'text', label: 'Score' },
|
|
88
|
+
avatarUrl: createImageField(mediaLibraryAdapter, 'Avatar image'),
|
|
89
|
+
},
|
|
90
|
+
defaultItemProps: {
|
|
91
|
+
rank: '1',
|
|
92
|
+
name: 'Player',
|
|
93
|
+
score: '0',
|
|
94
|
+
avatarUrl: '',
|
|
95
|
+
},
|
|
96
|
+
min: 1,
|
|
97
|
+
max: 20,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
defaultProps: {
|
|
101
|
+
eyebrow: '',
|
|
102
|
+
title: 'Leaderboard',
|
|
103
|
+
surface: 'navy',
|
|
104
|
+
top: [
|
|
105
|
+
{ name: 'username', score: '1737', avatarUrl: '' },
|
|
106
|
+
{ name: 'username', score: '1706', avatarUrl: '' },
|
|
107
|
+
{ name: 'username', score: '924', avatarUrl: '' },
|
|
108
|
+
],
|
|
109
|
+
rows: Array.from({ length: 7 }, (_, i) => ({
|
|
110
|
+
rank: String(i + 4),
|
|
111
|
+
name: 'username',
|
|
112
|
+
score: '—',
|
|
113
|
+
})),
|
|
114
|
+
},
|
|
115
|
+
render: (props) => <LeaderboardView {...props} />,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Presentational leaderboard — shared by the static base widget (above) and the
|
|
119
|
+
// app's live, data-driven override, so both render pixel-for-pixel identically.
|
|
120
|
+
export function LeaderboardView({ eyebrow, title, surface, top, rows }: LeaderboardWidgetProps) {
|
|
121
|
+
const navy = surface === 'navy';
|
|
122
|
+
return (
|
|
123
|
+
<section className="py-[var(--pb-section-py,3rem)]">
|
|
124
|
+
<div
|
|
125
|
+
className={cn(
|
|
126
|
+
'container mx-auto rounded-[12px] px-6 py-10 sm:px-10 sm:py-12',
|
|
127
|
+
navy
|
|
128
|
+
? 'bg-[var(--secondary,#42547e)] text-white'
|
|
129
|
+
: 'bg-[var(--pb-surface-muted,#f3f3f4)]',
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{eyebrow && (
|
|
133
|
+
<p className="text-sm font-semibold uppercase tracking-wider opacity-80">
|
|
134
|
+
{renderRichText(eyebrow)}
|
|
135
|
+
</p>
|
|
136
|
+
)}
|
|
137
|
+
{/* AccentText, not bare renderRichText: this is a display heading on
|
|
138
|
+
`--pb-display-weight`, where a plain <strong>'s relative `bolder`
|
|
139
|
+
can compute to no visible change. */}
|
|
140
|
+
{title && (
|
|
141
|
+
<h2
|
|
142
|
+
className="leading-tight"
|
|
143
|
+
style={{
|
|
144
|
+
fontWeight: 'var(--pb-display-weight)',
|
|
145
|
+
fontFamily: 'var(--pb-display-font)',
|
|
146
|
+
fontSize: 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 1.4vw, 2.5rem))',
|
|
147
|
+
color: navy ? '#ffffff' : 'var(--pb-heading-color)',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<AccentText text={title} />
|
|
151
|
+
</h2>
|
|
152
|
+
)}
|
|
153
|
+
<div className="mt-8 grid items-start gap-6 lg:grid-cols-[minmax(0,5fr)_7fr]">
|
|
154
|
+
{/* Podium — three stacked cards */}
|
|
155
|
+
<ol className="flex flex-col gap-4">
|
|
156
|
+
{top.map((p, idx) => (
|
|
157
|
+
<li
|
|
158
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
159
|
+
key={idx}
|
|
160
|
+
className="flex items-center gap-4 rounded-[12px] border border-[#dcdcdf]/40 px-4 py-3"
|
|
161
|
+
>
|
|
162
|
+
<Avatar src={p.avatarUrl || undefined} size="size-16" />
|
|
163
|
+
<div className="min-w-0 flex-1">
|
|
164
|
+
<p className="truncate text-sm font-semibold">{p.name}</p>
|
|
165
|
+
<p className="flex items-center gap-2 text-3xl font-semibold leading-tight">
|
|
166
|
+
{p.score}
|
|
167
|
+
{idx === 0 && <CrownIcon />}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
<span className="grid size-12 shrink-0 place-items-center rounded-full border-2 border-white text-xl font-semibold">
|
|
171
|
+
{idx + 1}
|
|
172
|
+
</span>
|
|
173
|
+
</li>
|
|
174
|
+
))}
|
|
175
|
+
</ol>
|
|
176
|
+
|
|
177
|
+
{/* Ranked table */}
|
|
178
|
+
<div className="overflow-hidden rounded-[12px]">
|
|
179
|
+
<div className="grid grid-cols-[1fr_auto_auto] gap-x-6 bg-white px-6 py-4 text-sm text-[#161728] sm:gap-x-10">
|
|
180
|
+
<span>User</span>
|
|
181
|
+
<span className="w-16 text-center">Rank</span>
|
|
182
|
+
<span className="w-16 text-right">Score</span>
|
|
183
|
+
</div>
|
|
184
|
+
{rows.map((r, i) => (
|
|
185
|
+
<div
|
|
186
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
187
|
+
key={i}
|
|
188
|
+
className="grid grid-cols-[1fr_auto_auto] items-center gap-x-6 bg-[#f3f3f4] px-6 py-3 text-[#161728] sm:gap-x-10"
|
|
189
|
+
>
|
|
190
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
191
|
+
<Avatar src={r.avatarUrl || undefined} size="size-10" />
|
|
192
|
+
<span className="truncate text-sm font-medium">{r.name}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<span className="w-16 text-center text-sm font-medium">{r.rank}</span>
|
|
195
|
+
<span className="w-16 text-right text-sm font-semibold">{r.score}</span>
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { renderRichText } from './_internal/rich-text';
|
|
3
|
+
|
|
4
|
+
export type ListWidgetProps = {
|
|
5
|
+
type: 'ul' | 'ol';
|
|
6
|
+
items: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ListWidget: ComponentConfig<ListWidgetProps> = {
|
|
10
|
+
label: 'List',
|
|
11
|
+
fields: {
|
|
12
|
+
type: {
|
|
13
|
+
type: 'select',
|
|
14
|
+
label: 'List type',
|
|
15
|
+
options: [
|
|
16
|
+
{ label: 'Unordered', value: 'ul' },
|
|
17
|
+
{ label: 'Ordered', value: 'ol' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
items: {
|
|
21
|
+
type: 'textarea',
|
|
22
|
+
label: 'Items (one per line)',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultProps: {
|
|
26
|
+
type: 'ul',
|
|
27
|
+
items: 'First item\nSecond item\nThird item',
|
|
28
|
+
},
|
|
29
|
+
render: ({ type, items }) => {
|
|
30
|
+
const list = items
|
|
31
|
+
.split(/\r?\n/)
|
|
32
|
+
.map((line) => line.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
const className =
|
|
35
|
+
type === 'ol' ? 'list-decimal list-inside space-y-1' : 'list-disc list-inside space-y-1';
|
|
36
|
+
return (
|
|
37
|
+
<div className="container mx-auto px-4 py-2">
|
|
38
|
+
{type === 'ol' ? (
|
|
39
|
+
<ol className={`${className} max-w-3xl mx-auto`}>
|
|
40
|
+
{list.map((item, idx) => (
|
|
41
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
42
|
+
<li key={idx}>{renderRichText(item)}</li>
|
|
43
|
+
))}
|
|
44
|
+
</ol>
|
|
45
|
+
) : (
|
|
46
|
+
<ul className={`${className} max-w-3xl mx-auto`}>
|
|
47
|
+
{list.map((item, idx) => (
|
|
48
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
49
|
+
<li key={idx}>{renderRichText(item)}</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
|
|
3
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
4
|
+
import { EyebrowSplitSection } from './_internal/eyebrow-split-section';
|
|
5
|
+
import { renderRichText } from './_internal/rich-text';
|
|
6
|
+
|
|
7
|
+
export type LogoCloudItem = {
|
|
8
|
+
src: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
href: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type LogoCloudWidgetProps = {
|
|
14
|
+
title: string;
|
|
15
|
+
eyebrow: string;
|
|
16
|
+
heading: string;
|
|
17
|
+
variant: 'inline' | 'grid';
|
|
18
|
+
items: LogoCloudItem[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const LogoCloudWidget: ComponentConfig<LogoCloudWidgetProps> = {
|
|
22
|
+
label: 'Logo cloud (partners / funders)',
|
|
23
|
+
fields: {
|
|
24
|
+
title: { type: 'text', label: 'Title' },
|
|
25
|
+
eyebrow: { type: 'text', label: 'Eyebrow (enables section layout)' },
|
|
26
|
+
heading: { type: 'text', label: 'Heading (section layout)' },
|
|
27
|
+
variant: {
|
|
28
|
+
type: 'select',
|
|
29
|
+
label: 'Layout',
|
|
30
|
+
options: [
|
|
31
|
+
{ label: 'Inline strip', value: 'inline' },
|
|
32
|
+
{ label: 'Grid', value: 'grid' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
items: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
label: 'Logos',
|
|
38
|
+
arrayFields: {
|
|
39
|
+
src: createImageField(mediaLibraryAdapter, 'Image'),
|
|
40
|
+
alt: { type: 'text', label: 'Alt text' },
|
|
41
|
+
href: { type: 'text', label: 'Link (optional)' },
|
|
42
|
+
},
|
|
43
|
+
defaultItemProps: { src: '', alt: 'Partner logo', href: '' },
|
|
44
|
+
min: 1,
|
|
45
|
+
max: 24,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultProps: {
|
|
49
|
+
title: 'Funded by',
|
|
50
|
+
eyebrow: '',
|
|
51
|
+
heading: '',
|
|
52
|
+
variant: 'inline',
|
|
53
|
+
items: [
|
|
54
|
+
{ src: '', alt: 'Partner 1', href: '' },
|
|
55
|
+
{ src: '', alt: 'Partner 2', href: '' },
|
|
56
|
+
{ src: '', alt: 'Partner 3', href: '' },
|
|
57
|
+
{ src: '', alt: 'Partner 4', href: '' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
render: ({ title, eyebrow = '', heading = '', variant = 'inline', items }) => {
|
|
61
|
+
if (eyebrow || heading) {
|
|
62
|
+
return (
|
|
63
|
+
<EyebrowSplitSection eyebrow={eyebrow} heading={heading}>
|
|
64
|
+
<div
|
|
65
|
+
className={
|
|
66
|
+
variant === 'grid'
|
|
67
|
+
? 'grid grid-cols-2 items-center gap-x-10 gap-y-8 sm:grid-cols-3 lg:grid-cols-4'
|
|
68
|
+
: 'flex flex-wrap items-center gap-x-10 gap-y-6'
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{items?.map((logo, idx) => (
|
|
72
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
73
|
+
<LogoCloudItem key={idx} logo={logo} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</EyebrowSplitSection>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
// data-pb-logo-* hooks let a tenant restyle the strip from its theme
|
|
81
|
+
// stylesheet (GCA: greyscale, spread edge-to-edge, bottom keyline).
|
|
82
|
+
<section
|
|
83
|
+
data-pb-logo-strip=""
|
|
84
|
+
className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] text-center sm:px-6 lg:px-8"
|
|
85
|
+
>
|
|
86
|
+
{title && (
|
|
87
|
+
<p
|
|
88
|
+
data-pb-logo-label=""
|
|
89
|
+
className="text-xs font-semibold uppercase tracking-[0.2em] mb-6"
|
|
90
|
+
style={{ color: 'var(--pb-body-color)' }}
|
|
91
|
+
>
|
|
92
|
+
{renderRichText(title)}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
<div
|
|
96
|
+
data-pb-logo-row=""
|
|
97
|
+
className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6"
|
|
98
|
+
>
|
|
99
|
+
{items?.map((logo, idx) => (
|
|
100
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
101
|
+
<LogoCloudItem key={idx} logo={logo} />
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function LogoCloudItem({ logo }: { logo: LogoCloudItem }) {
|
|
110
|
+
const img = logo.src ? (
|
|
111
|
+
<img
|
|
112
|
+
src={logo.src}
|
|
113
|
+
alt={logo.alt}
|
|
114
|
+
loading="lazy"
|
|
115
|
+
className="h-10 w-auto opacity-70 hover:opacity-100 transition-opacity"
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="h-10 w-32 rounded bg-gray-100" aria-hidden="true" />
|
|
119
|
+
);
|
|
120
|
+
return logo.href ? (
|
|
121
|
+
<a href={logo.href} className="block" aria-label={logo.alt}>
|
|
122
|
+
{img}
|
|
123
|
+
</a>
|
|
124
|
+
) : (
|
|
125
|
+
<div>{img}</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { parseMarkdownBlocks as renderMarkdown } from '../../utils/markdown';
|
|
4
|
+
import { renderRichText } from './_internal/rich-text';
|
|
5
|
+
|
|
6
|
+
export type MarkdownWidgetProps = {
|
|
7
|
+
content: string;
|
|
8
|
+
/**
|
|
9
|
+
* Text column width. "auto" is the legacy centred `max-w-4xl`; "660" is the
|
|
10
|
+
* Figma's measure for the legal and event pages.
|
|
11
|
+
*/
|
|
12
|
+
width?: 'auto' | '660';
|
|
13
|
+
/**
|
|
14
|
+
* Left placement on the 12-column page grid. "none" keeps the centred
|
|
15
|
+
* column; "4" starts the text at the 4th column, as the Figma does.
|
|
16
|
+
*/
|
|
17
|
+
indent?: 'none' | '4';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Emphasis tokenization is shared with the markdown→HTML converter; links are
|
|
21
|
+
// layered on top of it by renderRichText, which is also what the FAQ answers
|
|
22
|
+
// use — before that, `[label](href)` rendered here as literal text.
|
|
23
|
+
const renderInline = renderRichText;
|
|
24
|
+
|
|
25
|
+
export const MarkdownWidget: ComponentConfig<MarkdownWidgetProps> = {
|
|
26
|
+
label: 'Markdown',
|
|
27
|
+
fields: {
|
|
28
|
+
width: {
|
|
29
|
+
type: 'select',
|
|
30
|
+
label: 'Text width',
|
|
31
|
+
options: [
|
|
32
|
+
{ label: 'Auto (centred)', value: 'auto' },
|
|
33
|
+
{ label: '660px', value: '660' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
indent: {
|
|
37
|
+
type: 'select',
|
|
38
|
+
label: 'Grid indent',
|
|
39
|
+
options: [
|
|
40
|
+
{ label: 'None', value: 'none' },
|
|
41
|
+
{ label: '4th column', value: '4' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
content: {
|
|
45
|
+
type: 'textarea',
|
|
46
|
+
label: 'Markdown content',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultProps: {
|
|
50
|
+
width: 'auto',
|
|
51
|
+
indent: 'none',
|
|
52
|
+
content: '# Heading\n\nBody paragraph text.\n\n## Subheading\n\n- First item\n- Second item',
|
|
53
|
+
},
|
|
54
|
+
render: (props) => <MarkdownRender {...props} />,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function MarkdownRender({ content, width = 'auto', indent = 'none' }: MarkdownWidgetProps) {
|
|
58
|
+
const blocks = useMemo(() => renderMarkdown(content || ''), [content]);
|
|
59
|
+
const indented = indent === '4';
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={`container mx-auto px-4 py-8${indented ? ' lg:grid lg:grid-cols-12 lg:gap-6' : ''}`}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
className={[
|
|
66
|
+
// `--pb-prose-link-color` lets a tenant colour links (BioGarden's
|
|
67
|
+
// Figma uses the brand purple); unset, links keep the prose default.
|
|
68
|
+
'prose prose-lg dark:prose-invert break-words [&_a]:text-[color:var(--pb-prose-link-color,inherit)]',
|
|
69
|
+
// The last block's own `mb-4` sits BELOW the text and stacks on top
|
|
70
|
+
// of this section's bottom padding, so the visible gap to whatever
|
|
71
|
+
// follows is 16px wider than the padding says. A tenant measuring
|
|
72
|
+
// exact gaps off a Figma (BioGarden: 48px text→image, 40px
|
|
73
|
+
// text→CTA) sets `--pb-prose-trailing-mb: 0` to make the section
|
|
74
|
+
// padding the whole gap; the 1rem fallback matches every block
|
|
75
|
+
// type's own mb-4 (h3's mb-3 is the one exception — content ending
|
|
76
|
+
// on a bare h3 gains 4px).
|
|
77
|
+
'[&>*:last-child]:mb-[var(--pb-prose-trailing-mb,1rem)]',
|
|
78
|
+
width === '660' ? 'max-w-[660px]' : 'max-w-4xl',
|
|
79
|
+
// Indented text starts at the 4th grid column instead of centring.
|
|
80
|
+
indented ? 'lg:col-start-4 lg:col-span-9' : 'mx-auto',
|
|
81
|
+
].join(' ')}
|
|
82
|
+
>
|
|
83
|
+
{blocks.map((block, idx) => {
|
|
84
|
+
if (block.type === 'h1')
|
|
85
|
+
return (
|
|
86
|
+
<h1
|
|
87
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional slices of one string, re-derived each render
|
|
88
|
+
key={idx}
|
|
89
|
+
// `**key words**` must read as semibold against the light
|
|
90
|
+
// display weight. `<strong>`'s default `bolder` is RELATIVE,
|
|
91
|
+
// so on a 300-weight heading it resolves to 400 and the
|
|
92
|
+
// emphasis is invisible — state the weight explicitly.
|
|
93
|
+
// `leading-[calc(2.5/2.25)]` restores what `text-4xl` bundles:
|
|
94
|
+
// Tailwind's font-size utilities also set a line-height, and
|
|
95
|
+
// `text-[length:…]` sets ONLY the size. The `prose` classes
|
|
96
|
+
// can't cover for it — @tailwindcss/typography is not
|
|
97
|
+
// installed, so they compile to nothing. Unitless keeps the
|
|
98
|
+
// old 2.25rem/2.5rem ratio exactly while scaling with a
|
|
99
|
+
// tenant's larger heading. Size first, leading second (the
|
|
100
|
+
// reverse order is silently dropped by tailwind-merge).
|
|
101
|
+
className="text-[length:var(--pb-prose-h1-size,2.25rem)] leading-[calc(2.5/2.25)] font-light mb-4 [&_strong]:font-semibold"
|
|
102
|
+
>
|
|
103
|
+
{renderInline(block.text)}
|
|
104
|
+
</h1>
|
|
105
|
+
);
|
|
106
|
+
if (block.type === 'h2')
|
|
107
|
+
return (
|
|
108
|
+
// Token-sized like the H1 above: the fallback reproduces the
|
|
109
|
+
// previous `text-3xl` (size AND its bundled 1.2 line-height),
|
|
110
|
+
// while a pack can scale prose H2s per breakpoint — without
|
|
111
|
+
// this, a mobile pack that shrinks the H1 leaves section H2s
|
|
112
|
+
// LARGER than the page title (BioGarden QA, SUB-36).
|
|
113
|
+
<h2
|
|
114
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
115
|
+
key={idx}
|
|
116
|
+
className="text-[length:var(--pb-prose-h2-size,1.875rem)] leading-[1.2] font-light mt-8 mb-4"
|
|
117
|
+
>
|
|
118
|
+
{renderInline(block.text)}
|
|
119
|
+
</h2>
|
|
120
|
+
);
|
|
121
|
+
if (block.type === 'h3')
|
|
122
|
+
return (
|
|
123
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
124
|
+
<h3 key={idx} className="text-2xl font-semibold mt-6 mb-3">
|
|
125
|
+
{renderInline(block.text)}
|
|
126
|
+
</h3>
|
|
127
|
+
);
|
|
128
|
+
if (block.type === 'ul')
|
|
129
|
+
return (
|
|
130
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
131
|
+
<ul key={idx} className="list-disc list-inside space-y-1 mb-4">
|
|
132
|
+
{block.items.map((item, i) => (
|
|
133
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
134
|
+
<li key={i}>{renderInline(item)}</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
);
|
|
138
|
+
return (
|
|
139
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
140
|
+
<p key={idx} className="leading-relaxed mb-4">
|
|
141
|
+
{renderInline(block.text)}
|
|
142
|
+
</p>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|