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.
Files changed (189) hide show
  1. pagebuilder/__init__.py +0 -0
  2. pagebuilder/components/DiffSummary.tsx +107 -0
  3. pagebuilder/components/ScheduledBadge.tsx +34 -0
  4. pagebuilder/components/StatusBadge.tsx +21 -0
  5. pagebuilder/components/blockRegistry.test.ts +56 -0
  6. pagebuilder/components/blockRegistry.ts +80 -0
  7. pagebuilder/components/blocks/Button.tsx +142 -0
  8. pagebuilder/components/blocks/Columns.tsx +64 -0
  9. pagebuilder/components/blocks/Heading.tsx +92 -0
  10. pagebuilder/components/blocks/Image.tsx +125 -0
  11. pagebuilder/components/blocks/MediaPicker.tsx +178 -0
  12. pagebuilder/components/blocks/Spacer.tsx +31 -0
  13. pagebuilder/components/blocks/Text.tsx +80 -0
  14. pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
  15. pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
  16. pagebuilder/components/editor/SchedulePanel.tsx +85 -0
  17. pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
  18. pagebuilder/components/layoutPuckConfig.tsx +109 -0
  19. pagebuilder/components/media/MediaFilters.tsx +83 -0
  20. pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
  21. pagebuilder/components/media/MediaGrid.tsx +76 -0
  22. pagebuilder/components/media/MediaHeader.tsx +37 -0
  23. pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
  24. pagebuilder/components/media/types.ts +20 -0
  25. pagebuilder/components/migrateContent.test.ts +146 -0
  26. pagebuilder/components/migrateContent.ts +135 -0
  27. pagebuilder/components/puckConfig.test.tsx +118 -0
  28. pagebuilder/components/puckConfig.tsx +121 -0
  29. pagebuilder/components/widgetCatalog.test.tsx +123 -0
  30. pagebuilder/components/widgetCatalog.ts +208 -0
  31. pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
  32. pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
  33. pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
  34. pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
  35. pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
  36. pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
  37. pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
  38. pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
  39. pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
  40. pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
  41. pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
  42. pagebuilder/components/widgets/_shared/heading.tsx +88 -0
  43. pagebuilder/components/widgets/_shared/index.ts +39 -0
  44. pagebuilder/components/widgets/_shared/mask.ts +54 -0
  45. pagebuilder/components/widgets/_shared/section.tsx +60 -0
  46. pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
  47. pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
  48. pagebuilder/components/widgets/accordion-widget.tsx +64 -0
  49. pagebuilder/components/widgets/alert-widget.tsx +47 -0
  50. pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
  51. pagebuilder/components/widgets/article-cards-render.tsx +132 -0
  52. pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
  53. pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
  54. pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
  55. pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
  56. pagebuilder/components/widgets/carousel-widget.tsx +97 -0
  57. pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
  58. pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
  59. pagebuilder/components/widgets/column-widget.tsx +63 -0
  60. pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
  61. pagebuilder/components/widgets/contact-form-render.tsx +293 -0
  62. pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
  63. pagebuilder/components/widgets/container-widget.tsx +93 -0
  64. pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
  65. pagebuilder/components/widgets/divider-widget.tsx +56 -0
  66. pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
  67. pagebuilder/components/widgets/faq-render.tsx +268 -0
  68. pagebuilder/components/widgets/faq-widget.tsx +64 -0
  69. pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
  70. pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
  71. pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
  72. pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
  73. pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
  74. pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
  75. pagebuilder/components/widgets/gallery-widget.tsx +139 -0
  76. pagebuilder/components/widgets/grid-widget.tsx +59 -0
  77. pagebuilder/components/widgets/hero-widget.tsx +292 -0
  78. pagebuilder/components/widgets/html-widget.tsx +42 -0
  79. pagebuilder/components/widgets/iframe-widget.tsx +44 -0
  80. pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
  81. pagebuilder/components/widgets/list-widget.tsx +56 -0
  82. pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
  83. pagebuilder/components/widgets/markdown-widget.tsx +148 -0
  84. pagebuilder/components/widgets/media-object-layout.ts +156 -0
  85. pagebuilder/components/widgets/media-object-render.tsx +284 -0
  86. pagebuilder/components/widgets/media-object-text.tsx +202 -0
  87. pagebuilder/components/widgets/media-object-widget.tsx +176 -0
  88. pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
  89. pagebuilder/components/widgets/page-header-widget.tsx +118 -0
  90. pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
  91. pagebuilder/components/widgets/quote-widget.tsx +51 -0
  92. pagebuilder/components/widgets/row-widget.tsx +74 -0
  93. pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
  94. pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
  95. pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
  96. pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
  97. pagebuilder/components/widgets/site-header-nav.tsx +153 -0
  98. pagebuilder/components/widgets/site-header-widget.tsx +182 -0
  99. pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
  100. pagebuilder/components/widgets/stats-widget.tsx +200 -0
  101. pagebuilder/components/widgets/steps-widget.tsx +211 -0
  102. pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
  103. pagebuilder/components/widgets/table-widget.tsx +86 -0
  104. pagebuilder/components/widgets/tabs-widget.tsx +95 -0
  105. pagebuilder/components/widgets/tags-widget.tsx +85 -0
  106. pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
  107. pagebuilder/components/widgets/timeline-layout.ts +73 -0
  108. pagebuilder/components/widgets/timeline-render.tsx +294 -0
  109. pagebuilder/components/widgets/timeline-widget.tsx +106 -0
  110. pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
  111. pagebuilder/components/widgets/video-widget.tsx +103 -0
  112. pagebuilder/components/widgets/welcome-widget.tsx +38 -0
  113. pagebuilder/contracts/__init__.py +57 -0
  114. pagebuilder/contracts/events.py +25 -0
  115. pagebuilder/contracts/schemas.py +239 -0
  116. pagebuilder/conversion/blocks-to-html.test.ts +105 -0
  117. pagebuilder/conversion/blocks-to-html.ts +102 -0
  118. pagebuilder/conversion/index.ts +2 -0
  119. pagebuilder/conversion/types.ts +5 -0
  120. pagebuilder/deps.py +26 -0
  121. pagebuilder/diff.py +129 -0
  122. pagebuilder/endpoints/__init__.py +0 -0
  123. pagebuilder/endpoints/api/__init__.py +21 -0
  124. pagebuilder/endpoints/api/_deps.py +21 -0
  125. pagebuilder/endpoints/api/layout.py +78 -0
  126. pagebuilder/endpoints/api/pages.py +85 -0
  127. pagebuilder/endpoints/api/revisions.py +79 -0
  128. pagebuilder/endpoints/api/uploads.py +92 -0
  129. pagebuilder/endpoints/api/workflow.py +124 -0
  130. pagebuilder/endpoints/seo.py +95 -0
  131. pagebuilder/endpoints/views.py +228 -0
  132. pagebuilder/fields/checkbox-field.tsx +22 -0
  133. pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
  134. pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
  135. pagebuilder/fields/file-picker/index.ts +6 -0
  136. pagebuilder/fields/file-picker/types.ts +15 -0
  137. pagebuilder/fields/index.ts +9 -0
  138. pagebuilder/fields/media-adapter.ts +47 -0
  139. pagebuilder/hooks/useAutosave.ts +134 -0
  140. pagebuilder/hooks/useEditorForm.ts +193 -0
  141. pagebuilder/hooks/useMediaUploads.ts +96 -0
  142. pagebuilder/hooks/usePageRevisions.ts +59 -0
  143. pagebuilder/hooks/usePageSchedule.ts +72 -0
  144. pagebuilder/hooks/usePageWorkflow.ts +173 -0
  145. pagebuilder/layout_service.py +101 -0
  146. pagebuilder/media_images.py +185 -0
  147. pagebuilder/media_queries.py +92 -0
  148. pagebuilder/media_service.py +222 -0
  149. pagebuilder/models.py +209 -0
  150. pagebuilder/module.py +279 -0
  151. pagebuilder/package.json +18 -0
  152. pagebuilder/pages/LayoutEditor.tsx +183 -0
  153. pagebuilder/pages/MediaLibrary.tsx +249 -0
  154. pagebuilder/pages/PageEditor.tsx +181 -0
  155. pagebuilder/pages/PageList.tsx +129 -0
  156. pagebuilder/pages/PendingReview.tsx +128 -0
  157. pagebuilder/pages/PublicPage.tsx +126 -0
  158. pagebuilder/permissions.py +44 -0
  159. pagebuilder/py.typed +0 -0
  160. pagebuilder/security.py +230 -0
  161. pagebuilder/service/__init__.py +141 -0
  162. pagebuilder/service/_common.py +24 -0
  163. pagebuilder/service/_revisions.py +92 -0
  164. pagebuilder/service/_workflow.py +207 -0
  165. pagebuilder/services.py +19 -0
  166. pagebuilder/settings.py +161 -0
  167. pagebuilder/static/widgets-base.css +57 -0
  168. pagebuilder/translation/apply.ts +54 -0
  169. pagebuilder/translation/extract.ts +141 -0
  170. pagebuilder/translation/index.ts +20 -0
  171. pagebuilder/translation/translatable-fields.ts +65 -0
  172. pagebuilder/translation/translation.test.ts +167 -0
  173. pagebuilder/utils/api.ts +12 -0
  174. pagebuilder/utils/datetime.ts +25 -0
  175. pagebuilder/utils/decorative-image.ts +15 -0
  176. pagebuilder/utils/editorSnapshot.ts +39 -0
  177. pagebuilder/utils/markdown.ts +160 -0
  178. pagebuilder/utils/mediaApi.ts +112 -0
  179. pagebuilder/utils/mediaFormat.ts +19 -0
  180. pagebuilder/utils/pagesApi.ts +111 -0
  181. pagebuilder/utils/parse-list.ts +7 -0
  182. pagebuilder/utils/request.ts +52 -0
  183. pagebuilder/utils/slugify.ts +9 -0
  184. pagebuilder/utils/types.ts +171 -0
  185. pagebuilder/utils/widgetUtils.ts +11 -0
  186. simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
  187. simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
  188. simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
  189. 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
+ }