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,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke coverage for the whole block catalogue.
|
|
3
|
+
*
|
|
4
|
+
* The e2e suite proves a handful of widgets reach the DOM; it can't afford a
|
|
5
|
+
* page per block. This renders every entry with its own `defaultProps`, which
|
|
6
|
+
* is what an author gets the moment they drag one in — so a widget that throws
|
|
7
|
+
* on first paint fails here rather than as a blank section on someone's page.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ComponentType } from 'react';
|
|
11
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
|
|
14
|
+
import { catalogCategories, catalogComponents } from './widgetCatalog';
|
|
15
|
+
|
|
16
|
+
type AnyConfig = {
|
|
17
|
+
label?: string;
|
|
18
|
+
fields?: Record<string, { type?: string }>;
|
|
19
|
+
defaultProps?: Record<string, unknown>;
|
|
20
|
+
render: ComponentType<Record<string, unknown>>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const entries = Object.entries(catalogComponents) as [string, AnyConfig][];
|
|
24
|
+
|
|
25
|
+
/** A slot arrives at `render` as a component; stored data holds an array. */
|
|
26
|
+
const SlotStub = () => null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Swap every slot field's stored array for the component Puck would inject.
|
|
30
|
+
* Without this the layout blocks throw on `<Content />`, which would be this
|
|
31
|
+
* test failing on its own setup rather than on the widget.
|
|
32
|
+
*/
|
|
33
|
+
function withSlotsResolved(config: AnyConfig): Record<string, unknown> {
|
|
34
|
+
const props: Record<string, unknown> = { ...(config.defaultProps ?? {}) };
|
|
35
|
+
for (const [name, field] of Object.entries(config.fields ?? {})) {
|
|
36
|
+
if (field?.type === 'slot') props[name] = SlotStub;
|
|
37
|
+
}
|
|
38
|
+
// Slots nested in array items (Columns keeps one per column) need the same
|
|
39
|
+
// treatment a level down.
|
|
40
|
+
for (const [name, field] of Object.entries(config.fields ?? {})) {
|
|
41
|
+
const arrayFields = (field as { arrayFields?: Record<string, { type?: string }> }).arrayFields;
|
|
42
|
+
if (!arrayFields || !Array.isArray(props[name])) continue;
|
|
43
|
+
const slotKeys = Object.entries(arrayFields)
|
|
44
|
+
.filter(([, f]) => f?.type === 'slot')
|
|
45
|
+
.map(([k]) => k);
|
|
46
|
+
if (slotKeys.length === 0) continue;
|
|
47
|
+
props[name] = (props[name] as Record<string, unknown>[]).map((item) => ({
|
|
48
|
+
...item,
|
|
49
|
+
...Object.fromEntries(slotKeys.map((k) => [k, SlotStub])),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
return props;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('catalogue structure', () => {
|
|
56
|
+
it('files every block under exactly one category', () => {
|
|
57
|
+
const filed = Object.values(catalogCategories).flatMap((c) => c.components);
|
|
58
|
+
const duplicated = filed.filter((name, i) => filed.indexOf(name) !== i);
|
|
59
|
+
expect(duplicated).toEqual([]);
|
|
60
|
+
expect([...filed].sort()).toEqual(Object.keys(catalogComponents).sort());
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('gives every block a label', () => {
|
|
64
|
+
// Puck falls back to the raw key, so a missing label is not a crash — it
|
|
65
|
+
// just shows the author "ColoredActionList" instead of "Action list".
|
|
66
|
+
const unlabelled = entries.filter(([, c]) => !c.label).map(([name]) => name);
|
|
67
|
+
expect(unlabelled).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('gives every field a default', () => {
|
|
71
|
+
// A field with no default renders as `undefined` on first drop, which is
|
|
72
|
+
// how a widget ends up blank until the author touches every input.
|
|
73
|
+
const gaps = entries.flatMap(([name, c]) =>
|
|
74
|
+
Object.keys(c.fields ?? {})
|
|
75
|
+
.filter((f) => !(f in (c.defaultProps ?? {})))
|
|
76
|
+
.map((f) => `${name}.${f}`),
|
|
77
|
+
);
|
|
78
|
+
expect(gaps).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('every block renders with its defaults', () => {
|
|
83
|
+
it.each(entries.map(([name, config]) => ({ name, config })))('$name', ({ config }) => {
|
|
84
|
+
const Render = config.render;
|
|
85
|
+
// The real assertion is that this line doesn't throw. `toBe('string')`
|
|
86
|
+
// could never fail — renderToStaticMarkup always returns one — so it said
|
|
87
|
+
// nothing about the widget. Requiring output also catches a block that
|
|
88
|
+
// renders to nothing at all with its own defaults.
|
|
89
|
+
const html = renderToStaticMarkup(<Render {...withSlotsResolved(config)} />);
|
|
90
|
+
expect(html).not.toBe('');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('blocks tolerate data stored before a field existed', () => {
|
|
95
|
+
// `defaultProps` only covers a block dropped *now*. A Button saved before
|
|
96
|
+
// `size` existed arrives without it, and `sizeClass[undefined]` yields
|
|
97
|
+
// `undefined` — which `cn()` silently drops, so the button renders with no
|
|
98
|
+
// size classes at all rather than crashing. Nothing in a normal suite
|
|
99
|
+
// notices, because every other fixture is built from the current defaults.
|
|
100
|
+
//
|
|
101
|
+
// So the assertion is that the *expected class* is present. Checking the
|
|
102
|
+
// output merely doesn't say "undefined" passes even with the default
|
|
103
|
+
// removed, which is how the first version of this test was useless.
|
|
104
|
+
const cases: { name: string; props: Record<string, unknown>; expected: string[] }[] = [
|
|
105
|
+
{
|
|
106
|
+
name: 'Button',
|
|
107
|
+
props: { label: 'Old', href: '/x', variant: 'primary', target: '_self' },
|
|
108
|
+
expected: ['px-5', 'bg-primary-700'],
|
|
109
|
+
},
|
|
110
|
+
// Every asserted class must come from a prop the case OMITS, otherwise the
|
|
111
|
+
// assertion is satisfied by the explicit prop and the default it is meant
|
|
112
|
+
// to guard can be deleted with the test still green.
|
|
113
|
+
{ name: 'Text', props: { text: 'Old copy.' }, expected: ['text-base', 'text-left'] },
|
|
114
|
+
{ name: 'Heading', props: { text: 'Old' }, expected: ['text-3xl', 'text-left'] },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
it.each(cases)('$name', ({ name, props, expected }) => {
|
|
118
|
+
const config = catalogComponents[name as keyof typeof catalogComponents] as AnyConfig;
|
|
119
|
+
const Render = config.render;
|
|
120
|
+
const html = renderToStaticMarkup(<Render {...props} />);
|
|
121
|
+
for (const cls of expected) expect(html).toContain(cls);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every block pagebuilder ships, and where it sits in the editor palette.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `puckConfig.tsx` because the catalogue is the part that grows:
|
|
5
|
+
* the config file keeps the root definition and the registry merge, which are
|
|
6
|
+
* a fixed size, while this list gains an entry per widget.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
10
|
+
|
|
11
|
+
import { ButtonBlock } from './blocks/Button';
|
|
12
|
+
import { ColumnsBlock } from './blocks/Columns';
|
|
13
|
+
import { HeadingBlock } from './blocks/Heading';
|
|
14
|
+
import { ImageBlock } from './blocks/Image';
|
|
15
|
+
import { SpacerBlock } from './blocks/Spacer';
|
|
16
|
+
import { TextBlock } from './blocks/Text';
|
|
17
|
+
import { AccordionWidget } from './widgets/accordion-widget';
|
|
18
|
+
import { AlertWidget } from './widgets/alert-widget';
|
|
19
|
+
import { AppStoreBadgesWidget } from './widgets/app-store-badges-widget';
|
|
20
|
+
import { ArticleCardsWidget } from './widgets/article-cards-widget';
|
|
21
|
+
import { CallToActionWidget } from './widgets/call-to-action-widget';
|
|
22
|
+
import { CarouselWidget } from './widgets/carousel-widget';
|
|
23
|
+
import { CenteredHeroWidget } from './widgets/centered-hero-widget';
|
|
24
|
+
import { ColoredActionListWidget } from './widgets/colored-action-list-widget';
|
|
25
|
+
import { ColumnWidget } from './widgets/column-widget';
|
|
26
|
+
import { ContactCardsWidget } from './widgets/contact-cards-widget';
|
|
27
|
+
import { ContactFormWidget } from './widgets/contact-form-widget';
|
|
28
|
+
import { ContainerWidget } from './widgets/container-widget';
|
|
29
|
+
import { DefinitionListWidget } from './widgets/definition-list-widget';
|
|
30
|
+
import { DividerWidget } from './widgets/divider-widget';
|
|
31
|
+
import { EyebrowSectionWidget } from './widgets/eyebrow-section-widget';
|
|
32
|
+
import { FaqWidget } from './widgets/faq-widget';
|
|
33
|
+
import { FeatureCardWidget } from './widgets/feature-card-widget';
|
|
34
|
+
import { FeatureCardsWidget } from './widgets/feature-cards-widget';
|
|
35
|
+
import { FeatureGridWidget } from './widgets/feature-grid-widget';
|
|
36
|
+
import { FeatureShowcaseWidget } from './widgets/feature-showcase-widget';
|
|
37
|
+
import { GalleryWidget } from './widgets/gallery-widget';
|
|
38
|
+
import { GridWidget } from './widgets/grid-widget';
|
|
39
|
+
import { HeroWidget } from './widgets/hero-widget';
|
|
40
|
+
import { HtmlWidget } from './widgets/html-widget';
|
|
41
|
+
import { IframeWidget } from './widgets/iframe-widget';
|
|
42
|
+
import { LeaderboardWidget } from './widgets/leaderboard-widget';
|
|
43
|
+
import { ListWidget } from './widgets/list-widget';
|
|
44
|
+
import { LogoCloudWidget } from './widgets/logo-cloud-widget';
|
|
45
|
+
import { MarkdownWidget } from './widgets/markdown-widget';
|
|
46
|
+
import { MediaObjectWidget } from './widgets/media-object-widget';
|
|
47
|
+
import { NewsletterWidget } from './widgets/newsletter-widget';
|
|
48
|
+
import { PageHeaderWidget } from './widgets/page-header-widget';
|
|
49
|
+
import { ProseSectionWidget } from './widgets/prose-section-widget';
|
|
50
|
+
import { QuoteWidget } from './widgets/quote-widget';
|
|
51
|
+
import { RowWidget } from './widgets/row-widget';
|
|
52
|
+
import { SectionIntroWidget } from './widgets/section-intro-widget';
|
|
53
|
+
import { SignupBannerWidget } from './widgets/signup-banner-widget';
|
|
54
|
+
import { SimpleActionListWidget } from './widgets/simple-action-list-widget';
|
|
55
|
+
import { SocialBannerWidget } from './widgets/social-banner-widget';
|
|
56
|
+
import { StatsWidget } from './widgets/stats-widget';
|
|
57
|
+
import { StepsWidget } from './widgets/steps-widget';
|
|
58
|
+
import { StoryCardsWidget } from './widgets/story-cards-widget';
|
|
59
|
+
import { TableWidget } from './widgets/table-widget';
|
|
60
|
+
import { TabsWidget } from './widgets/tabs-widget';
|
|
61
|
+
import { TagsWidget } from './widgets/tags-widget';
|
|
62
|
+
import { TestimonialWidget } from './widgets/testimonial-widget';
|
|
63
|
+
import { TimelineWidget } from './widgets/timeline-widget';
|
|
64
|
+
import { UnderConstructionWidget } from './widgets/under-construction-widget';
|
|
65
|
+
import { VideoWidget } from './widgets/video-widget';
|
|
66
|
+
import { WelcomeWidget } from './widgets/welcome-widget';
|
|
67
|
+
|
|
68
|
+
export const catalogComponents = {
|
|
69
|
+
Accordion: AccordionWidget,
|
|
70
|
+
Alert: AlertWidget,
|
|
71
|
+
AppStoreBadges: AppStoreBadgesWidget,
|
|
72
|
+
ArticleCards: ArticleCardsWidget,
|
|
73
|
+
Button: ButtonBlock,
|
|
74
|
+
CallToAction: CallToActionWidget,
|
|
75
|
+
Carousel: CarouselWidget,
|
|
76
|
+
CenteredHero: CenteredHeroWidget,
|
|
77
|
+
ColoredActionList: ColoredActionListWidget,
|
|
78
|
+
Column: ColumnWidget,
|
|
79
|
+
Columns: ColumnsBlock,
|
|
80
|
+
ContactCards: ContactCardsWidget,
|
|
81
|
+
ContactForm: ContactFormWidget,
|
|
82
|
+
Container: ContainerWidget,
|
|
83
|
+
DefinitionList: DefinitionListWidget,
|
|
84
|
+
Divider: DividerWidget,
|
|
85
|
+
EyebrowSection: EyebrowSectionWidget,
|
|
86
|
+
Faq: FaqWidget,
|
|
87
|
+
FeatureCard: FeatureCardWidget,
|
|
88
|
+
FeatureCards: FeatureCardsWidget,
|
|
89
|
+
FeatureGrid: FeatureGridWidget,
|
|
90
|
+
FeatureShowcase: FeatureShowcaseWidget,
|
|
91
|
+
Gallery: GalleryWidget,
|
|
92
|
+
Grid: GridWidget,
|
|
93
|
+
Heading: HeadingBlock,
|
|
94
|
+
Hero: HeroWidget,
|
|
95
|
+
Html: HtmlWidget,
|
|
96
|
+
Iframe: IframeWidget,
|
|
97
|
+
Image: ImageBlock,
|
|
98
|
+
Leaderboard: LeaderboardWidget,
|
|
99
|
+
List: ListWidget,
|
|
100
|
+
LogoCloud: LogoCloudWidget,
|
|
101
|
+
Markdown: MarkdownWidget,
|
|
102
|
+
MediaObject: MediaObjectWidget,
|
|
103
|
+
Newsletter: NewsletterWidget,
|
|
104
|
+
PageHeader: PageHeaderWidget,
|
|
105
|
+
ProseSection: ProseSectionWidget,
|
|
106
|
+
Quote: QuoteWidget,
|
|
107
|
+
Row: RowWidget,
|
|
108
|
+
SectionIntro: SectionIntroWidget,
|
|
109
|
+
SignupBanner: SignupBannerWidget,
|
|
110
|
+
SimpleActionList: SimpleActionListWidget,
|
|
111
|
+
SocialBanner: SocialBannerWidget,
|
|
112
|
+
Spacer: SpacerBlock,
|
|
113
|
+
Stats: StatsWidget,
|
|
114
|
+
Steps: StepsWidget,
|
|
115
|
+
StoryCards: StoryCardsWidget,
|
|
116
|
+
Table: TableWidget,
|
|
117
|
+
Tabs: TabsWidget,
|
|
118
|
+
Tags: TagsWidget,
|
|
119
|
+
Testimonial: TestimonialWidget,
|
|
120
|
+
Text: TextBlock,
|
|
121
|
+
Timeline: TimelineWidget,
|
|
122
|
+
UnderConstruction: UnderConstructionWidget,
|
|
123
|
+
Video: VideoWidget,
|
|
124
|
+
Welcome: WelcomeWidget,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Each block's own props, recovered from its `ComponentConfig`.
|
|
129
|
+
*
|
|
130
|
+
* Derived rather than hand-listed: a parallel type map would be a second
|
|
131
|
+
* 56-entry list to keep in step, and `ComponentConfig<P>` already carries `P`
|
|
132
|
+
* in an inferable position. This also handles slot-bearing blocks correctly,
|
|
133
|
+
* which a map keyed off the *render* signature cannot — Puck rewrites `Slot`
|
|
134
|
+
* to `SlotComponent` on the way into `render`, and that mapping is one-way.
|
|
135
|
+
*/
|
|
136
|
+
type PropsOf<T> = T extends ComponentConfig<infer P> ? P : never;
|
|
137
|
+
|
|
138
|
+
export type CatalogProps = {
|
|
139
|
+
[K in keyof typeof catalogComponents]: PropsOf<(typeof catalogComponents)[K]>;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Palette grouping. `_hidden` is Puck's convention for a block that stays
|
|
144
|
+
* usable in stored data but is not offered in the palette — `CenteredHero` is
|
|
145
|
+
* a Hero variant reached through Hero's own fields, so offering both would
|
|
146
|
+
* just be two doors to one room.
|
|
147
|
+
*
|
|
148
|
+
* Typed against the catalogue's own keys rather than `string`, so a category
|
|
149
|
+
* naming a block that doesn't exist — or a block renamed out from under a
|
|
150
|
+
* category — is a compile error instead of an entry that silently never
|
|
151
|
+
* appears in the palette.
|
|
152
|
+
*/
|
|
153
|
+
type CatalogCategory = {
|
|
154
|
+
title?: string;
|
|
155
|
+
visible?: boolean;
|
|
156
|
+
components: (keyof typeof catalogComponents)[];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const catalogCategories: Record<string, CatalogCategory> = {
|
|
160
|
+
typography: {
|
|
161
|
+
components: ['Heading', 'Text', 'Markdown', 'ProseSection', 'Quote', 'List', 'DefinitionList'],
|
|
162
|
+
},
|
|
163
|
+
media: { components: ['Image', 'Gallery', 'Carousel', 'Video', 'Iframe'] },
|
|
164
|
+
actions: {
|
|
165
|
+
components: ['Button', 'SimpleActionList', 'ColoredActionList', 'AppStoreBadges'],
|
|
166
|
+
},
|
|
167
|
+
layout: {
|
|
168
|
+
components: ['Columns', 'Container', 'Row', 'Column', 'Grid', 'Spacer', 'Divider'],
|
|
169
|
+
},
|
|
170
|
+
sections: {
|
|
171
|
+
title: 'Sections',
|
|
172
|
+
components: [
|
|
173
|
+
'PageHeader',
|
|
174
|
+
'Hero',
|
|
175
|
+
'EyebrowSection',
|
|
176
|
+
'MediaObject',
|
|
177
|
+
'CallToAction',
|
|
178
|
+
'SectionIntro',
|
|
179
|
+
'FeatureShowcase',
|
|
180
|
+
'Steps',
|
|
181
|
+
'Timeline',
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
collections: {
|
|
185
|
+
title: 'Collections',
|
|
186
|
+
components: [
|
|
187
|
+
'FeatureCards',
|
|
188
|
+
'FeatureCard',
|
|
189
|
+
'FeatureGrid',
|
|
190
|
+
'Faq',
|
|
191
|
+
'Stats',
|
|
192
|
+
'ArticleCards',
|
|
193
|
+
'ContactCards',
|
|
194
|
+
'LogoCloud',
|
|
195
|
+
'StoryCards',
|
|
196
|
+
'Testimonial',
|
|
197
|
+
'Leaderboard',
|
|
198
|
+
'Table',
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
forms: { title: 'Forms', components: ['ContactForm', 'Tags', 'Newsletter', 'SignupBanner'] },
|
|
202
|
+
interactive: { title: 'Interactive', components: ['Accordion', 'Tabs'] },
|
|
203
|
+
utility: {
|
|
204
|
+
title: 'Utility',
|
|
205
|
+
components: ['Html', 'Alert', 'SocialBanner', 'UnderConstruction', 'Welcome'],
|
|
206
|
+
},
|
|
207
|
+
_hidden: { components: ['CenteredHero'], visible: false },
|
|
208
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode, useMemo } from 'react';
|
|
2
|
+
import { renderRichText } from './rich-text';
|
|
3
|
+
|
|
4
|
+
// Accent word styling is tenant-themeable: Recodo/GCA keep the italic semibold
|
|
5
|
+
// default; tenants like Mowing render an upright bold word via these tokens.
|
|
6
|
+
const ACCENT_WORD_STYLE: CSSProperties = {
|
|
7
|
+
fontStyle: 'var(--pb-accent-word-style, italic)',
|
|
8
|
+
fontWeight: 'var(--pb-accent-word-weight, 600)' as CSSProperties['fontWeight'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// `<strong>`'s default `font-weight: bolder` is RELATIVE — inside a
|
|
12
|
+
// `font-light` (300) display heading it computes to 400 and looks unchanged,
|
|
13
|
+
// so heading bolds carry an explicit tenant-tunable weight.
|
|
14
|
+
const STRONG_STYLE: CSSProperties = {
|
|
15
|
+
fontWeight: 'var(--pb-strong-weight, 600)' as CSSProperties['fontWeight'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders a heading-style string as light markdown. `*word*` segments keep
|
|
20
|
+
* the signature accent treatment — italic semibold by default ("*Clusters*",
|
|
21
|
+
* "*Finder*", "*biodiversity*") — and `**bold**`, `` `code` `` and
|
|
22
|
+
* `[label](href)` render like every other rich-text site.
|
|
23
|
+
*/
|
|
24
|
+
export function AccentText({
|
|
25
|
+
text,
|
|
26
|
+
color,
|
|
27
|
+
allowLinks,
|
|
28
|
+
}: {
|
|
29
|
+
text: string;
|
|
30
|
+
/**
|
|
31
|
+
* Colour for the emphasised segments. Omitted, they inherit — which is what
|
|
32
|
+
* headings want. A section can opt into a contrasting accent (BioGarden's
|
|
33
|
+
* "Offene Fragen" highlights phrases in the brand red) without turning every
|
|
34
|
+
* accent word on the page that colour.
|
|
35
|
+
*/
|
|
36
|
+
color?: string;
|
|
37
|
+
/** Pass `false` when the accent text sits inside an `<a>`/`<button>`. */
|
|
38
|
+
allowLinks?: boolean;
|
|
39
|
+
}): ReactNode {
|
|
40
|
+
return useMemo(() => {
|
|
41
|
+
if (!text) return null;
|
|
42
|
+
return renderRichText(text, {
|
|
43
|
+
allowLinks,
|
|
44
|
+
renderEm: (content, key) => (
|
|
45
|
+
<em
|
|
46
|
+
key={key}
|
|
47
|
+
className="font-semibold italic"
|
|
48
|
+
style={color ? { ...ACCENT_WORD_STYLE, color } : ACCENT_WORD_STYLE}
|
|
49
|
+
>
|
|
50
|
+
{content}
|
|
51
|
+
</em>
|
|
52
|
+
),
|
|
53
|
+
renderStrong: (content, key) => (
|
|
54
|
+
<strong key={key} style={STRONG_STYLE}>
|
|
55
|
+
{content}
|
|
56
|
+
</strong>
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
}, [text, color, allowLinks]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function DisclosureChevron() {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
className="size-5 transition-transform group-open:rotate-180"
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
aria-hidden="true"
|
|
9
|
+
>
|
|
10
|
+
<title>Toggle</title>
|
|
11
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../../utils/widgetUtils';
|
|
3
|
+
import { eyebrowTextStyle } from '../_shared';
|
|
4
|
+
import { AccentText } from './accent-text';
|
|
5
|
+
import { renderRichText } from './rich-text';
|
|
6
|
+
|
|
7
|
+
const HEADING_STYLE: CSSProperties = {
|
|
8
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
9
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
10
|
+
fontFamily: 'var(--pb-display-font)',
|
|
11
|
+
color: 'var(--pb-heading-color)',
|
|
12
|
+
fontSize: 'var(--pb-heading-md, clamp(1.875rem, 1.3rem + 2.2vw, 3rem))',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type EyebrowSplitSectionProps = {
|
|
16
|
+
eyebrow?: string;
|
|
17
|
+
heading?: string;
|
|
18
|
+
/** Wraps the grid in the Mowing muted rounded card. */
|
|
19
|
+
muted?: boolean;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Eyebrow-in-left-margin + big heading + content, on the tenant-tunable
|
|
25
|
+
* `--pb-split-cols` grid (Mowing aligns content to the 4th page-grid column).
|
|
26
|
+
* Shared by ProseSection, DefinitionList, and the LogoCloud grid variant, and
|
|
27
|
+
* matches the existing Mowing FAQ 2-column layout so all Mowing sections align.
|
|
28
|
+
*/
|
|
29
|
+
export function EyebrowSplitSection({
|
|
30
|
+
eyebrow,
|
|
31
|
+
heading,
|
|
32
|
+
muted = false,
|
|
33
|
+
children,
|
|
34
|
+
}: EyebrowSplitSectionProps) {
|
|
35
|
+
return (
|
|
36
|
+
<section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
'grid gap-8 lg:grid-cols-[var(--pb-split-cols,1fr_2fr)] lg:gap-[var(--pb-split-gap,3rem)]',
|
|
40
|
+
muted &&
|
|
41
|
+
'rounded-[12px] bg-[var(--pb-surface-muted,#f3f3f4)] px-8 py-12 lg:px-0 lg:py-20',
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{eyebrow && (
|
|
45
|
+
<p
|
|
46
|
+
className={cn(muted && 'lg:pl-12')}
|
|
47
|
+
style={{
|
|
48
|
+
// Eyebrow size/weight/case/tracking are theme-driven; the
|
|
49
|
+
// fallbacks keep today's look for tenants that don't set them.
|
|
50
|
+
...eyebrowTextStyle('1.125rem', '400'),
|
|
51
|
+
color: 'var(--pb-heading-color,#161728)',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{renderRichText(eyebrow)}
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
<div className="max-w-[var(--pb-split-content-max,none)]">
|
|
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
|
+
{heading && (
|
|
62
|
+
<h2 className="mb-8 leading-tight" style={HEADING_STYLE}>
|
|
63
|
+
<AccentText text={heading} />
|
|
64
|
+
</h2>
|
|
65
|
+
)}
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cn } from '../../../utils/widgetUtils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Right-arrow icon for inline text links ("Learn more", "View all", …).
|
|
5
|
+
* The fill reads `--pb-link-arrow-color` so a tenant can tint every link
|
|
6
|
+
* arrow (Mowing → orange #fa6e25); the `currentColor` fallback keeps the
|
|
7
|
+
* arrow matching the link text for tenants that don't set the token.
|
|
8
|
+
*/
|
|
9
|
+
export function LinkArrow({ className }: { className?: string }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
className={cn('size-6 shrink-0 fill-[var(--pb-link-arrow-color,currentColor)]', className)}
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
>
|
|
18
|
+
{/* No <title>: the svg is aria-hidden (decorative) and a title pops a
|
|
19
|
+
stray native "Arrow" tooltip on every link hover. */}
|
|
20
|
+
<path d="M21.7061 12.7083L14.707 19.7073C14.512 19.9023 14.256 20.0003 14 20.0003C13.744 20.0003 13.488 19.9023 13.293 19.7073C12.902 19.3163 12.902 18.6842 13.293 18.2933L18.5859 13.0003H3C2.447 13.0003 2 12.5523 2 12.0003C2 11.4483 2.447 11.0003 3 11.0003H18.5859L13.293 5.70731C12.902 5.31631 12.902 4.68425 13.293 4.29325C13.684 3.90225 14.316 3.90225 14.707 4.29325L21.7061 11.2923C21.7991 11.3853 21.8721 11.4952 21.9231 11.6182C22.0241 11.8622 22.0241 12.1384 21.9231 12.3824C21.8721 12.5054 21.7991 12.6153 21.7061 12.7083Z" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|