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,74 @@
1
+ import type { ComponentConfig, Slot } from '@puckeditor/core';
2
+ import { cn } from '../../utils/widgetUtils';
3
+
4
+ export type RowWidgetProps = {
5
+ gap: 'none' | 'sm' | 'md' | 'lg';
6
+ align: 'start' | 'center' | 'end';
7
+ wrap: 'wrap' | 'nowrap';
8
+ row: Slot;
9
+ };
10
+
11
+ const GAP_CLASS: Record<RowWidgetProps['gap'], string> = {
12
+ none: 'gap-0',
13
+ sm: 'gap-2',
14
+ md: 'gap-4',
15
+ lg: 'gap-8',
16
+ };
17
+
18
+ const ALIGN_CLASS: Record<RowWidgetProps['align'], string> = {
19
+ start: 'items-start',
20
+ center: 'items-center',
21
+ end: 'items-end',
22
+ };
23
+
24
+ export const RowWidget: ComponentConfig<RowWidgetProps> = {
25
+ label: 'Row',
26
+ fields: {
27
+ gap: {
28
+ type: 'select',
29
+ label: 'Gap',
30
+ options: [
31
+ { label: 'None', value: 'none' },
32
+ { label: 'Small', value: 'sm' },
33
+ { label: 'Medium', value: 'md' },
34
+ { label: 'Large', value: 'lg' },
35
+ ],
36
+ },
37
+ align: {
38
+ type: 'select',
39
+ label: 'Vertical alignment',
40
+ options: [
41
+ { label: 'Start', value: 'start' },
42
+ { label: 'Center', value: 'center' },
43
+ { label: 'End', value: 'end' },
44
+ ],
45
+ },
46
+ wrap: {
47
+ type: 'select',
48
+ label: 'Wrap',
49
+ options: [
50
+ { label: 'Wrap', value: 'wrap' },
51
+ { label: 'No wrap', value: 'nowrap' },
52
+ ],
53
+ },
54
+ row: { type: 'slot' },
55
+ },
56
+ defaultProps: {
57
+ gap: 'md',
58
+ align: 'start',
59
+ wrap: 'wrap',
60
+ row: [],
61
+ },
62
+ render: ({ gap, align, wrap, row: Row }) => (
63
+ <div
64
+ className={cn(
65
+ 'flex w-full',
66
+ GAP_CLASS[gap],
67
+ ALIGN_CLASS[align],
68
+ wrap === 'wrap' ? 'flex-wrap' : 'flex-nowrap',
69
+ )}
70
+ >
71
+ <Row />
72
+ </div>
73
+ ),
74
+ };
@@ -0,0 +1,143 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import type { CSSProperties } from 'react';
3
+ import { cn } from '../../utils/widgetUtils';
4
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
5
+ import { eyebrowTextStyle } from './_shared';
6
+
7
+ export type SectionIntroWidgetProps = {
8
+ eyebrow: string;
9
+ heading: string;
10
+ body: string;
11
+ align: 'left' | 'center';
12
+ layout: 'stacked' | 'split';
13
+ ctaLabel: string;
14
+ ctaHref: string;
15
+ };
16
+
17
+ const headingStyle: CSSProperties = {
18
+ fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
19
+ letterSpacing: 'var(--pb-display-tracking)',
20
+ fontFamily: 'var(--pb-display-font)',
21
+ fontSize: 'var(--pb-heading-lg, clamp(1.875rem, 1.3rem + 2.5vw, 3rem))',
22
+ color: 'var(--pb-heading-color)',
23
+ };
24
+
25
+ export const SectionIntroWidget: ComponentConfig<SectionIntroWidgetProps> = {
26
+ label: 'Section intro',
27
+ fields: {
28
+ eyebrow: { type: 'text', label: 'Eyebrow (optional)' },
29
+ heading: { type: 'text', label: 'Heading' },
30
+ body: { type: 'textarea', label: 'Body text' },
31
+ align: {
32
+ type: 'select',
33
+ label: 'Alignment',
34
+ options: [
35
+ { label: 'Left', value: 'left' },
36
+ { label: 'Center', value: 'center' },
37
+ ],
38
+ },
39
+ layout: {
40
+ type: 'select',
41
+ label: 'Layout',
42
+ options: [
43
+ { label: 'Stacked', value: 'stacked' },
44
+ { label: 'Split (eyebrow left)', value: 'split' },
45
+ ],
46
+ },
47
+ ctaLabel: { type: 'text', label: 'Button label (optional)' },
48
+ ctaHref: { type: 'text', label: 'Button link (optional)' },
49
+ },
50
+ defaultProps: {
51
+ eyebrow: '',
52
+ heading: 'Our mission',
53
+ body: 'We help people build great things.',
54
+ align: 'left',
55
+ layout: 'stacked',
56
+ ctaLabel: '',
57
+ ctaHref: '',
58
+ },
59
+ render: ({ eyebrow, heading, body, align, layout, ctaLabel, ctaHref }) => {
60
+ const content = (
61
+ <div
62
+ className={cn(
63
+ 'flex flex-col gap-6',
64
+ align === 'center' ? 'items-center text-center' : 'items-start',
65
+ )}
66
+ >
67
+ {heading && (
68
+ <h2 className="leading-[var(--pb-heading-leading,1.1)]" style={headingStyle}>
69
+ {renderRichText(heading)}
70
+ </h2>
71
+ )}
72
+ {body && (
73
+ <RichTextBlock
74
+ text={body}
75
+ className={cn(
76
+ 'text-lg leading-[var(--pb-body-leading,1.625)]',
77
+ // Mowing Figma: the split intro's body wraps at 432px.
78
+ layout === 'split' && 'max-w-[432px]',
79
+ )}
80
+ style={{ color: 'var(--pb-body-color)' }}
81
+ />
82
+ )}
83
+ {ctaLabel && (
84
+ <a
85
+ href={ctaHref || '#'}
86
+ className="inline-flex items-center rounded-full bg-[var(--primary,#000000)] px-7 py-4 font-medium text-[var(--primary-foreground,#ffffff)] transition-opacity hover:opacity-90"
87
+ >
88
+ {renderRichText(ctaLabel, { allowLinks: false })}
89
+ </a>
90
+ )}
91
+ </div>
92
+ );
93
+
94
+ // Split column geometry is tenant-tunable via `--pb-split-cols`/
95
+ // `--pb-split-gap` (Mowing aligns the content to the 4th page-grid
96
+ // column); the 1fr/2fr fallback keeps tenants without the tokens on the
97
+ // prior layout.
98
+ if (layout === 'split') {
99
+ return (
100
+ <section className="container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8">
101
+ <div className="grid gap-6 md:grid-cols-[var(--pb-split-cols,1fr_2fr)] md:gap-x-[var(--pb-split-gap,1.5rem)]">
102
+ {eyebrow && (
103
+ <p
104
+ style={{
105
+ // Eyebrow size/weight/case/tracking are theme-driven; the
106
+ // fallbacks keep today's look for tenants that don't set them.
107
+ ...eyebrowTextStyle('1.125rem', '400'),
108
+ color: 'var(--pb-heading-color,#161728)',
109
+ }}
110
+ >
111
+ {renderRichText(eyebrow)}
112
+ </p>
113
+ )}
114
+ <div className={eyebrow ? undefined : 'md:col-span-2'}>{content}</div>
115
+ </div>
116
+ </section>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <section
122
+ className={cn(
123
+ 'container mx-auto px-4 py-[var(--pb-section-py,3rem)] max-w-4xl flex flex-col gap-4',
124
+ align === 'center' ? 'text-center' : 'text-left',
125
+ )}
126
+ >
127
+ {eyebrow && (
128
+ <p
129
+ style={{
130
+ // Eyebrow size/weight/case/tracking are theme-driven; the
131
+ // fallbacks keep today's look for tenants that don't set them.
132
+ ...eyebrowTextStyle('1.125rem', '400'),
133
+ color: 'var(--pb-heading-color,#161728)',
134
+ }}
135
+ >
136
+ {renderRichText(eyebrow)}
137
+ </p>
138
+ )}
139
+ {content}
140
+ </section>
141
+ );
142
+ },
143
+ };
@@ -0,0 +1,50 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
3
+
4
+ export type SignupBannerWidgetProps = {
5
+ heading: string;
6
+ subheading: string;
7
+ primaryLabel: string;
8
+ primaryHref: string;
9
+ };
10
+
11
+ export const SignupBannerWidget: ComponentConfig<SignupBannerWidgetProps> = {
12
+ label: 'Signup banner',
13
+ fields: {
14
+ heading: { type: 'text', label: 'Heading' },
15
+ subheading: { type: 'textarea', label: 'Subheading' },
16
+ primaryLabel: { type: 'text', label: 'Button label' },
17
+ primaryHref: { type: 'text', label: 'Button link' },
18
+ },
19
+ defaultProps: {
20
+ heading: 'Join us today',
21
+ subheading: 'Create a free account in seconds.',
22
+ primaryLabel: 'Sign up',
23
+ primaryHref: '#',
24
+ },
25
+ render: ({ heading, subheading, primaryLabel, primaryHref }) => (
26
+ <section className="rounded-xl bg-gray-900 text-white">
27
+ <div className="container mx-auto py-12 px-6 sm:px-10 text-center">
28
+ {heading && (
29
+ <h2 className="text-3xl sm:text-4xl lg:text-5xl font-light tracking-tight">
30
+ {renderRichText(heading)}
31
+ </h2>
32
+ )}
33
+ {subheading && (
34
+ <RichTextBlock
35
+ text={subheading}
36
+ className="mt-3 text-lg text-gray-300 max-w-2xl mx-auto"
37
+ />
38
+ )}
39
+ {primaryLabel && (
40
+ <a
41
+ href={primaryHref || '#'}
42
+ className="mt-8 inline-flex items-center justify-center px-6 py-3 rounded-md bg-primary-700 hover:bg-primary-800 text-white font-medium transition-colors"
43
+ >
44
+ {renderRichText(primaryLabel, { allowLinks: false })}
45
+ </a>
46
+ )}
47
+ </div>
48
+ </section>
49
+ ),
50
+ };
@@ -0,0 +1,91 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { renderRichText } from './_internal/rich-text';
3
+
4
+ export type SimpleActionItem = {
5
+ title: string;
6
+ description: string;
7
+ href: string;
8
+ };
9
+
10
+ export type SimpleActionListWidgetProps = {
11
+ title: string;
12
+ items: SimpleActionItem[];
13
+ };
14
+
15
+ export const SimpleActionListWidget: ComponentConfig<SimpleActionListWidgetProps> = {
16
+ label: 'Action List',
17
+ fields: {
18
+ title: { type: 'text', label: 'Title' },
19
+ items: {
20
+ type: 'array',
21
+ label: 'Actions',
22
+ arrayFields: {
23
+ title: { type: 'text', label: 'Title' },
24
+ // Each row is a link, so markdown links here render as label text.
25
+ description: {
26
+ type: 'textarea',
27
+ label: 'Description (links not supported here)',
28
+ },
29
+ href: { type: 'text', label: 'Link URL' },
30
+ },
31
+ defaultItemProps: {
32
+ title: 'Action',
33
+ description: 'Description...',
34
+ href: '#',
35
+ },
36
+ min: 1,
37
+ max: 20,
38
+ },
39
+ },
40
+ defaultProps: {
41
+ title: 'Quick links',
42
+ items: [
43
+ {
44
+ title: 'Documentation',
45
+ description: 'Read the docs',
46
+ href: '#',
47
+ },
48
+ {
49
+ title: 'API reference',
50
+ description: 'Browse the API',
51
+ href: '#',
52
+ },
53
+ ],
54
+ },
55
+ render: ({ title, items }) => (
56
+ <div className="container mx-auto px-4 py-12 max-w-3xl">
57
+ {title && <h2 className="text-2xl font-light mb-6">{renderRichText(title)}</h2>}
58
+ <div className="divide-y border rounded-lg overflow-hidden">
59
+ {items?.map((item, idx) => (
60
+ <a
61
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
62
+ key={idx}
63
+ href={item.href || '#'}
64
+ className="flex items-center justify-between p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
65
+ >
66
+ <div>
67
+ <div className="font-semibold">
68
+ {renderRichText(item.title, { allowLinks: false })}
69
+ </div>
70
+ {item.description && (
71
+ <div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
72
+ {renderRichText(item.description, { allowLinks: false })}
73
+ </div>
74
+ )}
75
+ </div>
76
+ <svg
77
+ className="size-5 text-gray-400"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ viewBox="0 0 24 24"
81
+ aria-hidden="true"
82
+ >
83
+ <title>Arrow</title>
84
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
85
+ </svg>
86
+ </a>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ ),
91
+ };
@@ -0,0 +1,97 @@
1
+ /** Site footer for the layout's footer slot: dark block with logo + nav.
2
+ *
3
+ * The "supported and funded by" partner strip that sits under it on the GCA
4
+ * site is the existing LogoCloud widget — the footer slot takes a list of
5
+ * blocks, so the strip is composed in rather than duplicated here.
6
+ */
7
+
8
+ import type { ComponentConfig } from '@puckeditor/core';
9
+
10
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
11
+
12
+ export interface SiteFooterLink {
13
+ label: string;
14
+ href: string;
15
+ }
16
+
17
+ export interface SiteFooterWidgetProps {
18
+ logoUrl: string;
19
+ logoAlt: string;
20
+ homeHref: string;
21
+ links: SiteFooterLink[];
22
+ note: string;
23
+ }
24
+
25
+ export function SiteFooterRender({
26
+ logoUrl,
27
+ logoAlt,
28
+ homeHref,
29
+ links,
30
+ note,
31
+ }: SiteFooterWidgetProps) {
32
+ return (
33
+ <div className="gca-display bg-primary-800 px-6 py-16 text-white lg:py-20">
34
+ <div className="mx-auto flex max-w-[1440px] flex-col items-center gap-10 lg:gap-16">
35
+ <a href={homeHref || '/'} aria-label={logoAlt || 'Home'}>
36
+ {logoUrl ? (
37
+ <img src={logoUrl} alt={logoAlt} className="h-16 w-auto lg:h-20" />
38
+ ) : (
39
+ <span className="text-lg font-bold uppercase tracking-wide">{logoAlt || 'Home'}</span>
40
+ )}
41
+ </a>
42
+
43
+ {links.length > 0 && (
44
+ <nav
45
+ aria-label="Footer"
46
+ className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3"
47
+ >
48
+ {links.map((link, index) => (
49
+ <a
50
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
51
+ key={index}
52
+ href={link.href || '#'}
53
+ // The hover tint is a design-pack detail, not the brand action
54
+ // colour — it has to read against the dark block, which the
55
+ // configured brand colour is not guaranteed to do.
56
+ className="text-[18px] font-semibold text-white/90 transition-colors hover:text-[color:var(--pb-footer-link-hover,#ffffff)]"
57
+ >
58
+ {link.label}
59
+ </a>
60
+ ))}
61
+ </nav>
62
+ )}
63
+
64
+ {note && <p className="max-w-3xl text-center text-sm text-white/70">{note}</p>}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export const SiteFooterWidget: ComponentConfig<SiteFooterWidgetProps> = {
71
+ label: 'Site footer (logo + links)',
72
+ fields: {
73
+ logoUrl: createImageField(mediaLibraryAdapter, 'Logo (reversed / light)'),
74
+ logoAlt: { type: 'text', label: 'Logo alt text' },
75
+ homeHref: { type: 'text', label: 'Logo link' },
76
+ links: {
77
+ type: 'array',
78
+ label: 'Links',
79
+ arrayFields: {
80
+ label: { type: 'text', label: 'Label' },
81
+ href: { type: 'text', label: 'Link' },
82
+ },
83
+ defaultItemProps: { label: 'Link', href: '#' },
84
+ getItemSummary: (item) => item.label || 'Link',
85
+ max: 12,
86
+ },
87
+ note: { type: 'textarea', label: 'Small print (optional)' },
88
+ },
89
+ defaultProps: {
90
+ logoUrl: '',
91
+ logoAlt: 'Home',
92
+ homeHref: '/',
93
+ links: [{ label: 'Contact us', href: '#' }],
94
+ note: '',
95
+ },
96
+ render: SiteFooterRender,
97
+ };
@@ -0,0 +1,153 @@
1
+ /** Desktop and mobile navigation for the SiteHeader widget. */
2
+
3
+ import { ChevronDown } from 'lucide-react';
4
+
5
+ export interface SiteNavChild {
6
+ label: string;
7
+ href: string;
8
+ }
9
+
10
+ export interface SiteNavItem extends SiteNavChild {
11
+ /**
12
+ * Sub-links. A non-empty list turns the entry into a dropdown and its own
13
+ * `href` becomes decorative — Puck's array editor has no "either/or" field,
14
+ * so the shape carries both and the render picks.
15
+ */
16
+ children?: SiteNavChild[];
17
+ }
18
+
19
+ export function isNavGroup(item: SiteNavItem): boolean {
20
+ return (item.children?.length ?? 0) > 0;
21
+ }
22
+
23
+ const LINK_CLASS =
24
+ 'text-[length:var(--pb-nav-link-size,1.125rem)] font-semibold text-primary-800 underline-offset-[10px] hover:underline';
25
+
26
+ function Dropdown({ item }: { item: SiteNavItem }) {
27
+ return (
28
+ <div className="group relative">
29
+ {/* A group entry is a disclosure, not a destination: rendering it as a
30
+ button keeps the dropdown reachable by keyboard without advertising a
31
+ link that goes nowhere. */}
32
+ <button
33
+ type="button"
34
+ aria-haspopup="true"
35
+ className={`flex items-center gap-1 ${LINK_CLASS}`}
36
+ >
37
+ {item.label}
38
+ <ChevronDown className="size-4 transition-transform group-focus-within:rotate-180 group-hover:rotate-180" />
39
+ </button>
40
+ {/* Opens on hover AND on keyboard focus, so the child links stay
41
+ reachable without a pointer. */}
42
+ <div className="invisible absolute left-0 top-full z-50 min-w-56 pt-3 opacity-0 transition-all group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
43
+ <ul className="overflow-hidden rounded-lg bg-[var(--pb-surface,#fff)] py-2 shadow-lg ring-1 ring-black/5">
44
+ {item.children?.map((child) => (
45
+ <li key={child.href || child.label}>
46
+ <a
47
+ href={child.href || '#'}
48
+ // Blur on click: a click-focused link would otherwise hold
49
+ // :focus-within and pin the panel open over the destination.
50
+ onClick={(e) => e.currentTarget.blur()}
51
+ className="block px-5 py-2.5 text-base text-primary-800 transition-colors hover:bg-[var(--pb-surface-muted,#f3f3f4)]"
52
+ >
53
+ {child.label}
54
+ </a>
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function DesktopNav({ items }: { items: SiteNavItem[] }) {
64
+ return (
65
+ <nav aria-label="Main" className="hidden items-center gap-8 lg:flex">
66
+ {items.map((item, index) =>
67
+ isNavGroup(item) ? (
68
+ // Index keys: nav entries are author-ordered and an author may well
69
+ // create two blank rows before filling them in, so neither the label
70
+ // nor the href is unique while the row is being typed.
71
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
72
+ <Dropdown key={index} item={item} />
73
+ ) : (
74
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
75
+ <a key={index} href={item.href || '#'} className={LINK_CLASS}>
76
+ {item.label}
77
+ </a>
78
+ ),
79
+ )}
80
+ </nav>
81
+ );
82
+ }
83
+
84
+ export function MobileNav({
85
+ id,
86
+ items,
87
+ ctaLabel,
88
+ ctaHref,
89
+ onNavigate,
90
+ }: {
91
+ id: string;
92
+ items: SiteNavItem[];
93
+ ctaLabel: string;
94
+ ctaHref: string;
95
+ onNavigate: () => void;
96
+ }) {
97
+ return (
98
+ <nav
99
+ id={id}
100
+ aria-label="Main"
101
+ className="border-b border-[color:var(--border,#e4e6e7)] bg-[var(--pb-surface,#fff)] lg:hidden"
102
+ >
103
+ <ul className="mx-auto max-w-[1440px] divide-y divide-[color:var(--border,#e4e6e7)] px-4 sm:px-6">
104
+ {items.map((item, index) =>
105
+ isNavGroup(item) ? (
106
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
107
+ <li key={index} className="py-3">
108
+ <p className="px-1 py-1 text-[13px] font-semibold uppercase tracking-wide text-primary-800 opacity-80">
109
+ {item.label}
110
+ </p>
111
+ <ul className="mt-1">
112
+ {item.children?.map((child, childIndex) => (
113
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
114
+ <li key={childIndex}>
115
+ <a
116
+ href={child.href || '#'}
117
+ onClick={onNavigate}
118
+ className="block rounded-md px-3 py-2.5 text-[15px] text-primary-800 transition-colors hover:bg-[var(--pb-surface-muted,#f3f3f4)]"
119
+ >
120
+ {child.label}
121
+ </a>
122
+ </li>
123
+ ))}
124
+ </ul>
125
+ </li>
126
+ ) : (
127
+ // biome-ignore lint/suspicious/noArrayIndexKey: author rows are not uniquely identified until filled in
128
+ <li key={index} className="py-1">
129
+ <a
130
+ href={item.href || '#'}
131
+ onClick={onNavigate}
132
+ className="block rounded-md px-3 py-3 text-[15px] font-medium text-primary-800 transition-colors hover:bg-[var(--pb-surface-muted,#f3f3f4)]"
133
+ >
134
+ {item.label}
135
+ </a>
136
+ </li>
137
+ ),
138
+ )}
139
+ {ctaLabel && (
140
+ <li className="py-4">
141
+ <a
142
+ href={ctaHref || '#'}
143
+ onClick={onNavigate}
144
+ className="block rounded-[var(--pb-button-radius,9999px)] bg-primary-800 px-6 py-3 text-center text-[15px] font-semibold text-white transition-colors hover:bg-primary-900"
145
+ >
146
+ {ctaLabel}
147
+ </a>
148
+ </li>
149
+ )}
150
+ </ul>
151
+ </nav>
152
+ );
153
+ }