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,39 @@
1
+ export type {
2
+ BackgroundMediaFallback,
3
+ BackgroundMediaOverlay,
4
+ BackgroundMediaProps,
5
+ } from './background-media';
6
+ export { BackgroundMedia } from './background-media';
7
+ export type {
8
+ ContentMediaAspect,
9
+ ContentMediaProps,
10
+ } from './content-media';
11
+ export { ContentMedia } from './content-media';
12
+ export type {
13
+ CTAButtonProps,
14
+ CTAButtonSize,
15
+ CTAButtonVariant,
16
+ } from './cta-button';
17
+ export { CTAButton } from './cta-button';
18
+ export type { EmptyPlaceholderProps } from './empty-placeholder';
19
+ export { EmptyPlaceholder } from './empty-placeholder';
20
+ export type { EyebrowTextProps, EyebrowTone } from './eyebrow-text';
21
+ export { EyebrowText } from './eyebrow-text';
22
+ export type {
23
+ HeadingLevel,
24
+ HeadingProps,
25
+ HeadingSize,
26
+ HeadingTone,
27
+ } from './heading';
28
+ export { Heading } from './heading';
29
+ export {
30
+ EYEBROW_CASE_STYLE,
31
+ eyebrowTextStyle,
32
+ maskStyle,
33
+ SURFACE_CONTRAST,
34
+ } from './mask';
35
+ export type { SectionProps, SectionSpacing, SectionVariant } from './section';
36
+ export { Section } from './section';
37
+ export type { SocialIconName } from './social-icon';
38
+ export { SOCIAL_ICON_NAMES, SOCIAL_ICON_OPTIONS, SocialIcon } from './social-icon';
39
+ export { TextLink } from './text-link';
@@ -0,0 +1,54 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ /**
4
+ * An organic mask clips an image to a content-supplied SVG shape (e.g. the
5
+ * BioGarden blob/egg shapes). The -webkit- prefixed keys keep Safari/iOS in
6
+ * sync with the standard properties. Call once per widget render (not per
7
+ * item) and share the returned object across mapped items.
8
+ */
9
+ export function maskStyle(url: string): CSSProperties {
10
+ return {
11
+ maskImage: `url(${url})`,
12
+ maskSize: '100% 100%',
13
+ maskRepeat: 'no-repeat',
14
+ WebkitMaskImage: `url(${url})`,
15
+ WebkitMaskSize: '100% 100%',
16
+ WebkitMaskRepeat: 'no-repeat',
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Text color for content sitting on a colored band/card surface
22
+ * (`surfaceColor`/`cardBg`). Tenants override `--pb-surface-contrast` when
23
+ * their brand colors need dark-on-light contrast instead of white.
24
+ */
25
+ export const SURFACE_CONTRAST = 'var(--pb-surface-contrast, #ffffff)';
26
+
27
+ /**
28
+ * Theme-driven eyebrow case/tracking (`--pb-eyebrow-transform` /
29
+ * `--pb-eyebrow-tracking`). Defaults keep eyebrows sentence-case; a tenant
30
+ * sets `uppercase` + a tracking value for the small-caps eyebrow look.
31
+ */
32
+ export const EYEBROW_CASE_STYLE: CSSProperties = {
33
+ textTransform: 'var(--pb-eyebrow-transform, none)' as CSSProperties['textTransform'],
34
+ letterSpacing: 'var(--pb-eyebrow-tracking, normal)',
35
+ };
36
+
37
+ /**
38
+ * The ONE eyebrow ("subtitle") type spec, shared by every widget that renders
39
+ * one — MediaObject (inline and banner), EyebrowSplitSection, Timeline (both
40
+ * variants) and FeatureCards. Each of those used to hardcode its own size and
41
+ * weight, so a single tenant showed four different eyebrows across one page.
42
+ *
43
+ * `size`/`weight` are the caller's PREVIOUS literal values and are used only
44
+ * as the `var()` fallback, so a tenant that sets neither token renders exactly
45
+ * as before; a tenant that sets `--pb-eyebrow-size`/`--pb-eyebrow-weight`
46
+ * (BioGarden: 16px Medium) gets one consistent eyebrow everywhere.
47
+ */
48
+ export function eyebrowTextStyle(size = '0.875rem', weight = '500'): CSSProperties {
49
+ return {
50
+ ...EYEBROW_CASE_STYLE,
51
+ fontSize: `var(--pb-eyebrow-size, ${size})`,
52
+ fontWeight: `var(--pb-eyebrow-weight, ${weight})` as CSSProperties['fontWeight'],
53
+ };
54
+ }
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../../utils/widgetUtils';
3
+
4
+ export type SectionVariant = 'default' | 'muted' | 'accent' | 'image-overlay';
5
+ export type SectionSpacing = 'compact' | 'default' | 'loose';
6
+
7
+ export type SectionProps = {
8
+ variant: SectionVariant;
9
+ spacing: SectionSpacing;
10
+ align?: 'left' | 'center';
11
+ as?: 'section' | 'header';
12
+ className?: string;
13
+ children: ReactNode;
14
+ } & Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'children'>;
15
+
16
+ const VARIANT_CLASSES: Record<SectionVariant, string> = {
17
+ default: '',
18
+ muted: 'bg-gray-50 dark:bg-gray-900/40',
19
+ accent: 'bg-primary-800 text-white rounded-xl',
20
+ 'image-overlay': 'relative isolate overflow-hidden rounded-xl bg-slate-900 text-white',
21
+ };
22
+
23
+ // Vertical section rhythm is a component-level, tenant-tunable token. A tenant
24
+ // (e.g. Mowing) sets `--pb-section-py` to one value so every section shares a
25
+ // consistent rhythm; tenants that don't set it keep these per-variant fallbacks
26
+ // (the original py-8/12 · py-12/16 · py-16/24 scale), so GCA/Recodo are unchanged.
27
+ const SPACING_CLASSES: Record<SectionSpacing, string> = {
28
+ compact: 'py-[var(--pb-section-py,2rem)] lg:py-[var(--pb-section-py,3rem)]',
29
+ default: 'py-[var(--pb-section-py,3rem)] lg:py-[var(--pb-section-py,4rem)]',
30
+ loose: 'py-[var(--pb-section-py,4rem)] lg:py-[var(--pb-section-py,6rem)]',
31
+ };
32
+
33
+ export function Section({
34
+ variant,
35
+ spacing,
36
+ align,
37
+ as = 'section',
38
+ className,
39
+ children,
40
+ ...rest
41
+ }: SectionProps) {
42
+ const Tag = as;
43
+ return (
44
+ <Tag
45
+ {...rest}
46
+ className={cn(
47
+ // Same responsive gutters as every other widget container so all
48
+ // sections share one grid edge (px-4 alone left Section content
49
+ // 16px wider than its siblings at lg).
50
+ 'container mx-auto px-4 sm:px-6 lg:px-8',
51
+ VARIANT_CLASSES[variant],
52
+ SPACING_CLASSES[spacing],
53
+ align === 'center' && 'text-center',
54
+ className,
55
+ )}
56
+ >
57
+ {children}
58
+ </Tag>
59
+ );
60
+ }
@@ -0,0 +1,71 @@
1
+ import { Mail } from 'lucide-react';
2
+
3
+ /**
4
+ * Icons the site header's utility bar can show in place of a link label.
5
+ *
6
+ * Deliberately small: lucide 1.x removed its brand glyphs, so anything here
7
+ * has to be vendored below with its licence. Adding one means finding a
8
+ * correctly-licensed path, not approximating it.
9
+ */
10
+ export const SOCIAL_ICON_NAMES = ['bluesky', 'linkedin', 'email'] as const;
11
+
12
+ export type SocialIconName = (typeof SOCIAL_ICON_NAMES)[number];
13
+
14
+ export const SOCIAL_ICON_OPTIONS = [
15
+ { label: 'BlueSky', value: 'bluesky' },
16
+ { label: 'LinkedIn', value: 'linkedin' },
17
+ { label: 'Email', value: 'email' },
18
+ ];
19
+
20
+ /**
21
+ * BlueSky butterfly. Glyph: Font Awesome Free 6.7.2 `brands/bluesky`
22
+ * (https://fontawesome.com — CC BY 4.0), path data unmodified.
23
+ */
24
+ function BlueskyIcon({ className }: { className?: string }) {
25
+ return (
26
+ <svg
27
+ viewBox="0 0 512 512"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ fill="currentColor"
30
+ className={className}
31
+ aria-hidden="true"
32
+ >
33
+ {/* No <title>: the svg is aria-hidden (the link carries the label), and a
34
+ title would pop a stray native tooltip on hover. */}
35
+ <path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z" />
36
+ </svg>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * LinkedIn mark. Geometry: lucide `linkedin` (ISC), which lucide 1.x dropped
42
+ * along with the rest of its brand set — vendored rather than pinned back, so
43
+ * the module keeps up with lucide. Filled rather than stroked, matching the
44
+ * design system's solid social strip.
45
+ */
46
+ function LinkedinIcon({ className }: { className?: string }) {
47
+ return (
48
+ <svg
49
+ viewBox="0 0 24 24"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ fill="currentColor"
52
+ className={className}
53
+ aria-hidden="true"
54
+ >
55
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
56
+ <rect width="4" height="12" x="2" y="9" />
57
+ <circle cx="4" cy="4" r="2" />
58
+ </svg>
59
+ );
60
+ }
61
+
62
+ export function SocialIcon({ name, className }: { name: SocialIconName; className?: string }) {
63
+ switch (name) {
64
+ case 'bluesky':
65
+ return <BlueskyIcon className={className} />;
66
+ case 'linkedin':
67
+ return <LinkedinIcon className={className} />;
68
+ default:
69
+ return <Mail className={className} aria-hidden="true" />;
70
+ }
71
+ }
@@ -0,0 +1,47 @@
1
+ import type { CSSProperties } from 'react';
2
+ import { cn } from '../../../utils/widgetUtils';
3
+ import { LinkArrow } from '../_internal/link-arrow';
4
+ import { renderRichText } from '../_internal/rich-text';
5
+
6
+ /**
7
+ * Inline text link with a trailing arrow ("View all", "Learn more", …), themed
8
+ * via the tenant link tokens: `--pb-link-color` / `--pb-link-hover-color` /
9
+ * `--pb-link-hover-decoration` / `--pb-link-size` / `--pb-link-weight`
10
+ * (GCA: ink 18px SemiBold, teal hover, no underline). The fallbacks reproduce
11
+ * the prior widget styling (accent-coloured 14px medium, underline on hover)
12
+ * so tenants that don't set the tokens render unchanged.
13
+ */
14
+
15
+ // Hoisted: a fresh style object per render would defeat React's prop-equality
16
+ // fast-path (see the same pattern on MESH_STYLE in _shared/background-media).
17
+ const LINK_TEXT_STYLE: CSSProperties = {
18
+ fontSize: 'var(--pb-link-size, 0.875rem)',
19
+ fontWeight: 'var(--pb-link-weight, 500)' as CSSProperties['fontWeight'],
20
+ };
21
+
22
+ export function TextLink({
23
+ label,
24
+ href,
25
+ className,
26
+ }: {
27
+ label: string;
28
+ href: string;
29
+ className?: string;
30
+ }) {
31
+ if (!label || !href) return null;
32
+ return (
33
+ <a
34
+ href={href}
35
+ className={cn(
36
+ 'inline-flex shrink-0 items-center gap-2 whitespace-nowrap text-[color:var(--pb-link-color,var(--pb-accent))] underline-offset-4 hover:text-[color:var(--pb-link-hover-color,var(--pb-link-color,var(--pb-accent)))] hover:[text-decoration-line:var(--pb-link-hover-decoration,underline)]',
37
+ className,
38
+ )}
39
+ style={LINK_TEXT_STYLE}
40
+ >
41
+ {/* Emphasis only: the label already sits inside this <a>, and a nested
42
+ anchor is invalid HTML that browsers split into two links. */}
43
+ {renderRichText(label, { allowLinks: false })}
44
+ <LinkArrow className="size-5" />
45
+ </a>
46
+ );
47
+ }
@@ -0,0 +1,64 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { DisclosureChevron } from './_internal/disclosure-chevron';
3
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
4
+
5
+ export type AccordionItem = {
6
+ title: string;
7
+ content: string;
8
+ };
9
+
10
+ export type AccordionWidgetProps = {
11
+ items: AccordionItem[];
12
+ };
13
+
14
+ export const AccordionWidget: ComponentConfig<AccordionWidgetProps> = {
15
+ label: 'Accordion',
16
+ fields: {
17
+ items: {
18
+ type: 'array',
19
+ label: 'Items',
20
+ arrayFields: {
21
+ title: { type: 'text', label: 'Title' },
22
+ content: { type: 'textarea', label: 'Content' },
23
+ },
24
+ defaultItemProps: {
25
+ title: 'Section title',
26
+ content: 'Section content...',
27
+ },
28
+ min: 1,
29
+ max: 20,
30
+ },
31
+ },
32
+ defaultProps: {
33
+ items: [
34
+ { title: 'Section 1', content: 'Content for section 1...' },
35
+ { title: 'Section 2', content: 'Content for section 2...' },
36
+ ],
37
+ },
38
+ render: ({ items }) => {
39
+ if (!items || items.length === 0) {
40
+ return (
41
+ <div className="text-gray-500 text-center py-4">Add accordion items in the editor.</div>
42
+ );
43
+ }
44
+ return (
45
+ <div className="container mx-auto max-w-3xl py-8 space-y-2">
46
+ {items.map((item, idx) => (
47
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
48
+ <details key={idx} className="group rounded-lg border bg-white dark:bg-gray-900 p-4">
49
+ <summary className="cursor-pointer flex items-center justify-between font-semibold list-none">
50
+ {renderRichText(item.title || `Item ${idx + 1}`, {
51
+ allowLinks: false,
52
+ })}
53
+ <DisclosureChevron />
54
+ </summary>
55
+ <RichTextBlock
56
+ text={item.content}
57
+ className="mt-3 text-gray-700 dark:text-gray-300 leading-relaxed"
58
+ />
59
+ </details>
60
+ ))}
61
+ </div>
62
+ );
63
+ },
64
+ };
@@ -0,0 +1,47 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { RichTextBlock, renderRichText } from './_internal/rich-text';
3
+
4
+ export type AlertWidgetProps = {
5
+ title: string;
6
+ message: string;
7
+ variant: 'info' | 'success' | 'warning' | 'error';
8
+ };
9
+
10
+ const VARIANT_CLASS: Record<AlertWidgetProps['variant'], string> = {
11
+ info: 'bg-primary-50 border-primary-200 text-primary-900',
12
+ success: 'bg-green-50 border-green-200 text-green-900',
13
+ warning: 'bg-yellow-50 border-yellow-200 text-yellow-900',
14
+ error: 'bg-red-50 border-red-200 text-red-900',
15
+ };
16
+
17
+ export const AlertWidget: ComponentConfig<AlertWidgetProps> = {
18
+ label: 'Alert',
19
+ fields: {
20
+ title: { type: 'text', label: 'Title' },
21
+ message: { type: 'textarea', label: 'Message' },
22
+ variant: {
23
+ type: 'select',
24
+ label: 'Variant',
25
+ options: [
26
+ { label: 'Info', value: 'info' },
27
+ { label: 'Success', value: 'success' },
28
+ { label: 'Warning', value: 'warning' },
29
+ { label: 'Error', value: 'error' },
30
+ ],
31
+ },
32
+ },
33
+ defaultProps: {
34
+ title: 'Heads up!',
35
+ message: 'Something important to share.',
36
+ variant: 'info',
37
+ },
38
+ render: ({ title, message, variant }) => (
39
+ <div
40
+ className={`max-w-3xl mx-auto rounded-lg border px-4 py-3 ${VARIANT_CLASS[variant]}`}
41
+ role="alert"
42
+ >
43
+ <div className="font-semibold">{renderRichText(title || '')}</div>
44
+ {message && <RichTextBlock text={message} className="mt-1 text-sm" />}
45
+ </div>
46
+ ),
47
+ };
@@ -0,0 +1,91 @@
1
+ import type { ComponentConfig } from '@puckeditor/core';
2
+ import { cn } from '../../utils/widgetUtils';
3
+
4
+ export type AppStoreBadgesWidgetProps = {
5
+ googlePlayHref: string;
6
+ appStoreHref: string;
7
+ googlePlaySrc: string;
8
+ appStoreSrc: string;
9
+ align: 'left' | 'center';
10
+ /**
11
+ * Start the badges at a page-grid column so they line up with an indented
12
+ * page header above (Mowing landing hero: 2nd column). "none" keeps the
13
+ * legacy container-edge alignment.
14
+ */
15
+ indent?: 'none' | '2' | '4';
16
+ };
17
+
18
+ // 12-col grid placement (gap 24px) matching the Figma page grid.
19
+ const INDENT_CLASSES: Record<'2' | '4', string> = {
20
+ '2': 'lg:col-start-2 lg:col-span-10',
21
+ '4': 'lg:col-start-4 lg:col-span-9',
22
+ };
23
+
24
+ export const AppStoreBadgesWidget: ComponentConfig<AppStoreBadgesWidgetProps> = {
25
+ label: 'App store badges',
26
+ fields: {
27
+ googlePlayHref: { type: 'text', label: 'Google Play link' },
28
+ appStoreHref: { type: 'text', label: 'App Store link' },
29
+ googlePlaySrc: { type: 'text', label: 'Google Play badge image' },
30
+ appStoreSrc: { type: 'text', label: 'App Store badge image' },
31
+ align: {
32
+ type: 'select',
33
+ label: 'Align',
34
+ options: [
35
+ { label: 'Left', value: 'left' },
36
+ { label: 'Center', value: 'center' },
37
+ ],
38
+ },
39
+ indent: {
40
+ type: 'select',
41
+ label: 'Grid indent',
42
+ options: [
43
+ { label: 'None', value: 'none' },
44
+ { label: '2nd column', value: '2' },
45
+ { label: '4th column', value: '4' },
46
+ ],
47
+ },
48
+ },
49
+ defaultProps: {
50
+ googlePlayHref: '#',
51
+ appStoreHref: '#',
52
+ googlePlaySrc: '',
53
+ appStoreSrc: '',
54
+ align: 'left',
55
+ indent: 'none',
56
+ },
57
+ render: ({
58
+ googlePlayHref,
59
+ appStoreHref,
60
+ googlePlaySrc,
61
+ appStoreSrc,
62
+ align,
63
+ indent = 'none',
64
+ }) => (
65
+ <div
66
+ className={cn(
67
+ 'container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8',
68
+ indent !== 'none' && 'lg:grid lg:grid-cols-12 lg:gap-6',
69
+ )}
70
+ >
71
+ <div
72
+ className={cn(
73
+ 'flex flex-wrap items-center gap-4',
74
+ align === 'center' ? 'justify-center' : 'justify-start',
75
+ indent !== 'none' && INDENT_CLASSES[indent],
76
+ )}
77
+ >
78
+ {googlePlaySrc && (
79
+ <a href={googlePlayHref} target="_blank" rel="noopener noreferrer">
80
+ <img src={googlePlaySrc} alt="Get it on Google Play" className="h-16 w-auto" />
81
+ </a>
82
+ )}
83
+ {appStoreSrc && (
84
+ <a href={appStoreHref} target="_blank" rel="noopener noreferrer">
85
+ <img src={appStoreSrc} alt="Download on the App Store" className="h-16 w-auto" />
86
+ </a>
87
+ )}
88
+ </div>
89
+ </div>
90
+ ),
91
+ };
@@ -0,0 +1,132 @@
1
+ /** ArticleCards presentation, split from its Puck config so another module
2
+ * can render the same grid from live data — the news feed does.
3
+ * The config lives in article-cards-widget.tsx. */
4
+
5
+ import type { CSSProperties } from 'react';
6
+ import { cn } from '../../utils/widgetUtils';
7
+ import { renderRichText } from './_internal/rich-text';
8
+ import { TextLink } from './_shared/text-link';
9
+
10
+ export type ArticleCardItem = {
11
+ imageUrl: string;
12
+ imageAlt: string;
13
+ eyebrow: string;
14
+ date: string;
15
+ title: string;
16
+ body: string;
17
+ href: string;
18
+ };
19
+
20
+ export type ArticleCardsWidgetProps = {
21
+ title: string;
22
+ viewAllLabel: string;
23
+ viewAllHref: string;
24
+ columns: '2' | '3' | '4';
25
+ items: ArticleCardItem[];
26
+ };
27
+
28
+ const COLS_CLASS: Record<ArticleCardsWidgetProps['columns'], string> = {
29
+ '2': 'sm:grid-cols-2',
30
+ '3': 'sm:grid-cols-2 lg:grid-cols-3',
31
+ '4': 'sm:grid-cols-2 lg:grid-cols-4',
32
+ };
33
+
34
+ // Display tokens come from the per-tenant page-builder theme (see styles.css
35
+ // `:root` for Recodo defaults and `.gca-root` for the GCA overrides). The
36
+ // fallback clamp reproduces the prior `text-2xl sm:text-3xl lg:text-4xl`
37
+ // steps: 24px on phones, 30px at 640px, 36px at 1024px. All style objects are
38
+ // hoisted — constant tokens only, no per-render allocation.
39
+ const HEADING_STYLE: CSSProperties = {
40
+ fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
41
+ letterSpacing: 'var(--pb-display-tracking)',
42
+ fontFamily: 'var(--pb-display-font)',
43
+ color: 'var(--pb-heading-color)',
44
+ fontSize: 'var(--pb-heading-lg, clamp(1.5rem, 1.25rem + 1.5625vw, 2.25rem))',
45
+ };
46
+
47
+ const CARD_META_STYLE: CSSProperties = {
48
+ color: 'var(--pb-body-color)',
49
+ fontSize: 'var(--pb-card-meta-size, 0.75rem)',
50
+ };
51
+
52
+ const CARD_TITLE_STYLE: CSSProperties = {
53
+ fontFamily: 'var(--pb-display-font)',
54
+ fontWeight:
55
+ 'var(--pb-card-title-weight, var(--pb-display-weight))' as CSSProperties['fontWeight'],
56
+ fontSize: 'var(--pb-card-title-size, 1.125rem)',
57
+ color: 'var(--pb-heading-color)',
58
+ };
59
+
60
+ const CARD_BODY_STYLE: CSSProperties = {
61
+ color: 'var(--pb-body-color)',
62
+ fontSize: 'var(--pb-card-body-size, 0.875rem)',
63
+ };
64
+
65
+ export function ArticleCardsGrid({
66
+ title,
67
+ viewAllLabel,
68
+ viewAllHref,
69
+ columns,
70
+ items,
71
+ }: ArticleCardsWidgetProps) {
72
+ return (
73
+ <section className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
74
+ {(title || (viewAllLabel && viewAllHref)) && (
75
+ <div className="mb-8 flex items-end justify-between gap-4">
76
+ {title && (
77
+ <h2 className="leading-[var(--pb-heading-leading,1.15)]" style={HEADING_STYLE}>
78
+ {renderRichText(title)}
79
+ </h2>
80
+ )}
81
+ {viewAllLabel && viewAllHref && <TextLink label={viewAllLabel} href={viewAllHref} />}
82
+ </div>
83
+ )}
84
+ <div className={cn('grid gap-6 grid-cols-1', COLS_CLASS[columns])}>
85
+ {items?.map((item, idx) => (
86
+ // The whole card is one anchor, so every string inside it renders
87
+ // with `allowLinks: false` — a nested <a> is invalid HTML and
88
+ // browsers split the card link in two.
89
+ <a
90
+ // biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
91
+ key={idx}
92
+ href={item.href || '#'}
93
+ className="group block"
94
+ >
95
+ <div className="relative aspect-[16/10] overflow-hidden rounded-xl bg-[var(--pb-surface-muted)]">
96
+ {item.imageUrl && (
97
+ <img
98
+ src={item.imageUrl}
99
+ alt={item.imageAlt || item.title}
100
+ loading="lazy"
101
+ className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
102
+ />
103
+ )}
104
+ {item.eyebrow && (
105
+ <span className="absolute right-3 top-3 inline-flex w-fit items-center rounded-full bg-[var(--pb-card-tag-bg,rgb(255_255_255/0.9))] px-2.5 py-0.5 text-xs font-medium text-[var(--pb-card-tag-color,#1a353e)] backdrop-blur-sm">
106
+ {renderRichText(item.eyebrow, { allowLinks: false })}
107
+ </span>
108
+ )}
109
+ </div>
110
+ <div className="mt-4">
111
+ {item.date && (
112
+ <span style={CARD_META_STYLE}>
113
+ {renderRichText(item.date, { allowLinks: false })}
114
+ </span>
115
+ )}
116
+ {item.title && (
117
+ <h3 className="mt-3 leading-snug" style={CARD_TITLE_STYLE}>
118
+ {renderRichText(item.title, { allowLinks: false })}
119
+ </h3>
120
+ )}
121
+ {item.body && (
122
+ <p className="mt-2 leading-relaxed" style={CARD_BODY_STYLE}>
123
+ {renderRichText(item.body, { allowLinks: false })}
124
+ </p>
125
+ )}
126
+ </div>
127
+ </a>
128
+ ))}
129
+ </div>
130
+ </section>
131
+ );
132
+ }
@@ -0,0 +1,72 @@
1
+ /** ArticleCards widget: field definitions and config assembly.
2
+ * Types and the grid itself live in article-cards-render.tsx. */
3
+
4
+ import type { ComponentConfig } from '@puckeditor/core';
5
+
6
+ import { createImageField, mediaLibraryAdapter } from '../../fields';
7
+ import { ArticleCardsGrid, type ArticleCardsWidgetProps } from './article-cards-render';
8
+
9
+ export type { ArticleCardItem, ArticleCardsWidgetProps } from './article-cards-render';
10
+ export { ArticleCardsGrid } from './article-cards-render';
11
+
12
+ export const ArticleCardsWidget: ComponentConfig<ArticleCardsWidgetProps> = {
13
+ label: 'Article cards (image-top, with category & date)',
14
+ fields: {
15
+ title: { type: 'text', label: 'Title' },
16
+ viewAllLabel: { type: 'text', label: 'View-all link label' },
17
+ viewAllHref: { type: 'text', label: 'View-all link URL' },
18
+ columns: {
19
+ type: 'select',
20
+ label: 'Columns (desktop)',
21
+ options: [
22
+ { label: '2', value: '2' },
23
+ { label: '3', value: '3' },
24
+ { label: '4', value: '4' },
25
+ ],
26
+ },
27
+ items: {
28
+ type: 'array',
29
+ label: 'Articles',
30
+ arrayFields: {
31
+ imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
32
+ imageAlt: { type: 'text', label: 'Image alt text' },
33
+ eyebrow: { type: 'text', label: 'Category tag' },
34
+ date: { type: 'text', label: 'Date' },
35
+ title: { type: 'text', label: 'Title' },
36
+ // The whole card is a link, so markdown links in this copy render
37
+ // as their label text only — say so where the editor can see it.
38
+ body: { type: 'textarea', label: 'Body (links not supported here)' },
39
+ href: { type: 'text', label: 'Link' },
40
+ },
41
+ defaultItemProps: {
42
+ imageUrl: '',
43
+ imageAlt: '',
44
+ eyebrow: 'Category tag',
45
+ date: 'MMM DD, YYYY',
46
+ title: 'Article title goes here and may be on two lines',
47
+ body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
48
+ href: '#',
49
+ },
50
+ min: 1,
51
+ max: 12,
52
+ },
53
+ },
54
+ defaultProps: {
55
+ title: 'Latest news, updates & events',
56
+ viewAllLabel: 'View all',
57
+ viewAllHref: '#',
58
+ columns: '4',
59
+ items: [
60
+ {
61
+ imageUrl: '',
62
+ imageAlt: '',
63
+ eyebrow: 'Category tag',
64
+ date: 'MMM DD, YYYY',
65
+ title: 'Article title goes here and may be on two lines',
66
+ body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
67
+ href: '#',
68
+ },
69
+ ],
70
+ },
71
+ render: ArticleCardsGrid,
72
+ };