simple-module-pagebuilder 0.0.2__tar.gz → 0.0.4__tar.gz

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 (214) hide show
  1. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/PKG-INFO +33 -1
  2. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/README.md +32 -0
  3. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/package.json +1 -1
  4. simple_module_pagebuilder-0.0.4/pagebuilder/components/blocks/Image.test.tsx +94 -0
  5. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Image.tsx +37 -3
  6. simple_module_pagebuilder-0.0.4/pagebuilder/components/lazyBlock.tsx +58 -0
  7. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/background-media.tsx +5 -0
  8. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/content-media.tsx +13 -1
  9. simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/_shared/image-srcset.ts +55 -0
  10. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/index.ts +7 -0
  11. simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/hero-widget-props.ts +33 -0
  12. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/hero-widget.tsx +21 -29
  13. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-layout.ts +2 -0
  14. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-render.tsx +5 -1
  15. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-widget.tsx +4 -0
  16. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.test.ts +24 -0
  17. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.ts +10 -2
  18. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/seo.py +4 -1
  19. simple_module_pagebuilder-0.0.4/pagebuilder/media_files.py +128 -0
  20. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_service.py +4 -1
  21. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/module.py +8 -3
  22. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/__init__.py +32 -2
  23. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/settings.py +5 -2
  24. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/static/widgets-base.css +7 -0
  25. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/markdown.ts +8 -1
  26. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pyproject.toml +1 -1
  27. simple_module_pagebuilder-0.0.4/tests/test_media_files.py +137 -0
  28. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/.gitignore +0 -0
  29. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/__init__.py +0 -0
  30. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/DiffSummary.tsx +0 -0
  31. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/ScheduledBadge.tsx +0 -0
  32. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/StatusBadge.tsx +0 -0
  33. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.test.ts +0 -0
  34. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.ts +0 -0
  35. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Button.tsx +0 -0
  36. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Columns.tsx +0 -0
  37. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Heading.tsx +0 -0
  38. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
  39. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Spacer.tsx +0 -0
  40. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Text.tsx +0 -0
  41. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/PageEditorToolbar.tsx +0 -0
  42. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +0 -0
  43. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SchedulePanel.tsx +0 -0
  44. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -0
  45. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
  46. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFilters.tsx +0 -0
  47. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFolderSidebar.tsx +0 -0
  48. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaGrid.tsx +0 -0
  49. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaHeader.tsx +0 -0
  50. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaUploadQueue.tsx +0 -0
  51. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/types.ts +0 -0
  52. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.test.ts +0 -0
  53. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.ts +0 -0
  54. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.test.tsx +0 -0
  55. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.tsx +0 -0
  56. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
  57. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.ts +0 -0
  58. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
  59. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
  60. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
  61. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
  62. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
  63. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
  64. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
  65. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
  66. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
  67. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
  68. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
  69. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
  70. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
  71. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
  72. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
  73. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
  74. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
  75. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
  76. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
  77. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
  78. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
  79. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
  80. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
  81. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
  82. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
  83. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/column-widget.tsx +0 -0
  84. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
  85. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
  86. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
  87. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/container-widget.tsx +0 -0
  88. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
  89. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/divider-widget.tsx +0 -0
  90. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
  91. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-render.tsx +0 -0
  92. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
  93. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
  94. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
  95. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
  96. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
  97. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
  98. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
  99. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
  100. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
  101. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/html-widget.tsx +0 -0
  102. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
  103. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
  104. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/list-widget.tsx +0 -0
  105. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
  106. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
  107. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
  108. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
  109. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
  110. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
  111. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
  112. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/row-widget.tsx +0 -0
  113. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
  114. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
  115. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
  116. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
  117. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
  118. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
  119. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
  120. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
  121. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
  122. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
  123. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/table-widget.tsx +0 -0
  124. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
  125. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
  126. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
  127. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
  128. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
  129. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
  130. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
  131. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/video-widget.tsx +0 -0
  132. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
  133. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/__init__.py +0 -0
  134. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/events.py +0 -0
  135. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/schemas.py +0 -0
  136. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/index.ts +0 -0
  137. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/types.ts +0 -0
  138. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/deps.py +0 -0
  139. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/diff.py +0 -0
  140. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/__init__.py +0 -0
  141. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/__init__.py +0 -0
  142. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/_deps.py +0 -0
  143. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/layout.py +0 -0
  144. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/pages.py +0 -0
  145. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/revisions.py +0 -0
  146. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/uploads.py +0 -0
  147. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/workflow.py +0 -0
  148. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/views.py +0 -0
  149. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/checkbox-field.tsx +0 -0
  150. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
  151. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
  152. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/index.ts +0 -0
  153. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/types.ts +0 -0
  154. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/index.ts +0 -0
  155. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/media-adapter.ts +0 -0
  156. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useAutosave.ts +0 -0
  157. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useEditorForm.ts +0 -0
  158. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useMediaUploads.ts +0 -0
  159. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageRevisions.ts +0 -0
  160. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageSchedule.ts +0 -0
  161. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageWorkflow.ts +0 -0
  162. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/layout_service.py +0 -0
  163. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_images.py +0 -0
  164. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_queries.py +0 -0
  165. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/models.py +0 -0
  166. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/LayoutEditor.tsx +0 -0
  167. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/MediaLibrary.tsx +0 -0
  168. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageEditor.tsx +0 -0
  169. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageList.tsx +0 -0
  170. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PendingReview.tsx +0 -0
  171. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PublicPage.tsx +0 -0
  172. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/permissions.py +0 -0
  173. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/py.typed +0 -0
  174. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/security.py +0 -0
  175. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_common.py +0 -0
  176. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_revisions.py +0 -0
  177. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_workflow.py +0 -0
  178. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/services.py +0 -0
  179. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/apply.ts +0 -0
  180. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/extract.ts +0 -0
  181. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/index.ts +0 -0
  182. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translatable-fields.ts +0 -0
  183. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translation.test.ts +0 -0
  184. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/api.ts +0 -0
  185. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/datetime.ts +0 -0
  186. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/decorative-image.ts +0 -0
  187. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/editorSnapshot.ts +0 -0
  188. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaApi.ts +0 -0
  189. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaFormat.ts +0 -0
  190. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/pagesApi.ts +0 -0
  191. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/parse-list.ts +0 -0
  192. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/request.ts +0 -0
  193. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/slugify.ts +0 -0
  194. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/types.ts +0 -0
  195. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/widgetUtils.ts +0 -0
  196. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/conftest.py +0 -0
  197. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_approval_workflow.py +0 -0
  198. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_auth_gating.py +0 -0
  199. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_csrf.py +0 -0
  200. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_image_processing.py +0 -0
  201. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_media_security.py +0 -0
  202. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_module.py +0 -0
  203. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_pages_api.py +0 -0
  204. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_routes.py +0 -0
  205. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_view_headers.py +0 -0
  206. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_viewer.py +0 -0
  207. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_revision_notes_and_diff.py +0 -0
  208. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_revisions_api.py +0 -0
  209. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_scheduled_publish.py +0 -0
  210. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_seo_meta.py +0 -0
  211. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_site_layout.py +0 -0
  212. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_sitemap_robots.py +0 -0
  213. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_uploads_api.py +0 -0
  214. {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_module_pagebuilder
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Drag-and-drop visual page builder for simple_module apps
5
5
  Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
6
6
  Author-email: Anto Subash <antosubash@live.com>
@@ -81,6 +81,38 @@ Once installed and migrated, sign in and open `/pagebuilder`:
81
81
  Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
82
82
  two revisions and restore either one as a draft.
83
83
 
84
+ ## Contributing blocks from another module
85
+
86
+ A module ships Puck blocks by calling `registerPuckBlocks` from its
87
+ `puck-blocks.ts`; the host imports every module's registration eagerly at app
88
+ start, so the registry is populated before the first render.
89
+
90
+ Because registration is eager, a block that statically imports a heavy render
91
+ component puts that component's whole dependency graph in the entry chunk —
92
+ on every page of the site, for every visitor. Measured on a consuming site, a
93
+ map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
94
+ in front of visitors who never opened the map. Register heavy blocks with
95
+ `lazyBlock` instead, which keeps fields and defaults eager but loads the
96
+ component the first time a page actually renders the block:
97
+
98
+ ```tsx
99
+ import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
100
+ import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
101
+
102
+ registerPuckBlocks({
103
+ blocks: {
104
+ AtlasMap: lazyBlock(
105
+ () => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
106
+ { label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
107
+ (props) => <div className={heightClass[props.height]} aria-busy="true" />,
108
+ ),
109
+ },
110
+ });
111
+ ```
112
+
113
+ Light blocks (text, cards, lists) can keep their static imports — the split
114
+ only pays for itself when the component drags in something big.
115
+
84
116
  ## Migrations
85
117
 
86
118
  This module ships **no** migrations — that is the framework convention. Its
@@ -59,6 +59,38 @@ Once installed and migrated, sign in and open `/pagebuilder`:
59
59
  Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
60
60
  two revisions and restore either one as a draft.
61
61
 
62
+ ## Contributing blocks from another module
63
+
64
+ A module ships Puck blocks by calling `registerPuckBlocks` from its
65
+ `puck-blocks.ts`; the host imports every module's registration eagerly at app
66
+ start, so the registry is populated before the first render.
67
+
68
+ Because registration is eager, a block that statically imports a heavy render
69
+ component puts that component's whole dependency graph in the entry chunk —
70
+ on every page of the site, for every visitor. Measured on a consuming site, a
71
+ map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
72
+ in front of visitors who never opened the map. Register heavy blocks with
73
+ `lazyBlock` instead, which keeps fields and defaults eager but loads the
74
+ component the first time a page actually renders the block:
75
+
76
+ ```tsx
77
+ import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
78
+ import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
79
+
80
+ registerPuckBlocks({
81
+ blocks: {
82
+ AtlasMap: lazyBlock(
83
+ () => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
84
+ { label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
85
+ (props) => <div className={heightClass[props.height]} aria-busy="true" />,
86
+ ),
87
+ },
88
+ });
89
+ ```
90
+
91
+ Light blocks (text, cards, lists) can keep their static imports — the split
92
+ only pays for itself when the component drags in something big.
93
+
62
94
  ## Migrations
63
95
 
64
96
  This module ships **no** migrations — that is the framework convention. Its
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-module-py/pagebuilder",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the PageBuilder module",
6
6
  "peerDependencies": {
@@ -0,0 +1,94 @@
1
+ import type { ReactElement } from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { ImageBlock } from './Image';
6
+
7
+ // Puck passes its own `id`/`puck` props alongside the authored ones; the block
8
+ // reads none of them, so these tests supply the authored props alone.
9
+ const Image = ImageBlock.render as unknown as (props: Record<string, unknown>) => ReactElement;
10
+
11
+ const BASE = {
12
+ ...ImageBlock.defaultProps,
13
+ src: '/media/pagebuilder/atlas-map.jpg',
14
+ alt: 'The Global Canopy Atlas interactive map',
15
+ };
16
+
17
+ function html(props: Record<string, unknown> = {}): string {
18
+ return renderToStaticMarkup(<Image {...BASE} {...props} />);
19
+ }
20
+
21
+ describe('ImageBlock caption', () => {
22
+ it('offers a Caption field in the inspector', () => {
23
+ expect(ImageBlock.fields?.caption).toEqual({ type: 'text', label: 'Caption' });
24
+ });
25
+
26
+ it('defaults to no caption', () => {
27
+ expect(ImageBlock.defaultProps?.caption).toBe('');
28
+ });
29
+
30
+ it('renders the caption in a figcaption inside a figure', () => {
31
+ const markup = html({ caption: 'Image caption goes here, if CMS field is populated' });
32
+ expect(markup).toContain('<figure');
33
+ expect(markup).toContain('<figcaption');
34
+ expect(markup).toContain('Image caption goes here, if CMS field is populated');
35
+ });
36
+
37
+ it('takes the caption’s colour and metrics from the design pack', () => {
38
+ // A tenant restyles captions by overriding tokens, not by patching this
39
+ // block — same contract the widget set gives the rest of the page.
40
+ const markup = html({ caption: 'A caption' });
41
+ expect(markup).toContain('--pb-caption-color');
42
+ expect(markup).toContain('--pb-body-color');
43
+ expect(markup).toContain('--pb-caption-size');
44
+ });
45
+
46
+ it('keeps alt and caption separate', () => {
47
+ // alt is the accessible description, caption is visible prose; the two are
48
+ // rarely the same sentence, so neither may stand in for the other.
49
+ const markup = html({ alt: 'A map of the world', caption: 'Figure 1' });
50
+ expect(markup).toContain('alt="A map of the world"');
51
+ expect(markup).toContain('>Figure 1</figcaption>');
52
+ });
53
+
54
+ it('renders the caption as light markdown, like every other copy field', () => {
55
+ // Table, Carousel and CallToAction captions all go through renderRichText,
56
+ // so `**bold**` cannot mean one thing in one caption and another here.
57
+ expect(html({ caption: '**Figure 1** — the map' })).toContain('<strong');
58
+ });
59
+
60
+ it('escapes caption text rather than emitting it as markup', () => {
61
+ expect(html({ caption: '<b>bold</b>' })).not.toContain('<b>bold</b>');
62
+ });
63
+
64
+ it('emits a bare <img> when no caption is set', () => {
65
+ // Existing pages carry no caption prop at all — `undefined`, not `''`, is
66
+ // what Puck hands the block for them — and must render exactly as they did
67
+ // before the field existed.
68
+ const markup = html({ caption: undefined });
69
+ expect(markup).not.toContain('figure');
70
+ expect(markup.startsWith('<img')).toBe(true);
71
+ expect(markup).toContain('class="my-3"');
72
+ });
73
+
74
+ it('treats an empty or blank caption as no caption', () => {
75
+ // Whitespace-only would otherwise be truthy and render an empty caption
76
+ // box under the image.
77
+ const bare = html({ caption: undefined });
78
+ expect(html({ caption: '' })).toBe(bare);
79
+ expect(html({ caption: ' ' })).toBe(bare);
80
+ });
81
+
82
+ it('leaves the empty-src placeholder alone', () => {
83
+ const markup = html({ src: '', caption: 'A caption' });
84
+ expect(markup).toContain('No image selected');
85
+ expect(markup).not.toContain('figcaption');
86
+ });
87
+
88
+ it('still hides a decorative image from screen readers', () => {
89
+ const markup = html({ altKind: 'decorative', caption: 'A caption' });
90
+ expect(markup).toContain('alt=""');
91
+ expect(markup).toContain('aria-hidden="true"');
92
+ expect(markup).toContain('<figcaption');
93
+ });
94
+ });
@@ -6,6 +6,7 @@ import {
6
6
  lookupAsset,
7
7
  type MediaAssetRead,
8
8
  } from '../../utils/api';
9
+ import { renderRichText } from '../widgets/_internal/rich-text';
9
10
  import { MediaPicker } from './MediaPicker';
10
11
 
11
12
  type AltKind = 'meaningful' | 'decorative';
@@ -14,6 +15,10 @@ interface ImageProps {
14
15
  src: string;
15
16
  alt: string;
16
17
  altKind: AltKind;
18
+ /** Visible prose under the image, as light markdown. Distinct from `alt`,
19
+ * which describes the image to a screen reader — the two are rarely the
20
+ * same sentence. */
21
+ caption?: string;
17
22
  width: number | null;
18
23
  height: number | null;
19
24
  objectFit: 'cover' | 'contain' | 'fill';
@@ -21,6 +26,14 @@ interface ImageProps {
21
26
  sizes: string;
22
27
  }
23
28
 
29
+ // Captions read their look from the design pack the way the widget set does,
30
+ // so a tenant restyles them by overriding tokens instead of patching this
31
+ // block. `--pb-caption-color` falls through to the pack's body colour, which
32
+ // is what an unstyled pack (and widgets-base) already defines.
33
+ const CAPTION_CLASS =
34
+ 'mt-2 text-[length:var(--pb-caption-size,0.875rem)] leading-[var(--pb-body-leading,1.625)]';
35
+ const CAPTION_STYLE = { color: 'var(--pb-caption-color, var(--pb-body-color))' };
36
+
24
37
  function resolveSizes(srcset: string, sizes: string): string | undefined {
25
38
  if (sizes) return sizes;
26
39
  // Browsers assume 100vw when sizes is omitted but srcset is present;
@@ -50,6 +63,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
50
63
  ],
51
64
  },
52
65
  alt: { type: 'text' },
66
+ caption: { type: 'text', label: 'Caption' },
53
67
  width: { type: 'number' },
54
68
  height: { type: 'number' },
55
69
  objectFit: {
@@ -67,6 +81,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
67
81
  src: '',
68
82
  alt: '',
69
83
  altKind: 'meaningful',
84
+ caption: '',
70
85
  width: null,
71
86
  height: null,
72
87
  objectFit: 'cover',
@@ -96,7 +111,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
96
111
  },
97
112
  };
98
113
  },
99
- render: ({ src, alt, altKind, width, height, objectFit, srcset, sizes }) => {
114
+ render: ({ src, alt, altKind, caption, width, height, objectFit, srcset, sizes }) => {
100
115
  if (!src) {
101
116
  return (
102
117
  <div className="my-3 p-6 border-2 border-dashed border-gray-300 rounded text-center text-gray-500 text-sm">
@@ -105,7 +120,11 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
105
120
  );
106
121
  }
107
122
  const decorative = altKind === 'decorative';
108
- return (
123
+ // Trimmed, so a caption of nothing but spaces is no caption: it would
124
+ // otherwise render an empty <figcaption> box under the image. Same rule
125
+ // the translation extractor applies to every other copy field.
126
+ const hasCaption = Boolean(caption?.trim());
127
+ const image = (
109
128
  <img
110
129
  src={src}
111
130
  srcSet={srcset || undefined}
@@ -118,8 +137,23 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
118
137
  loading="lazy"
119
138
  decoding="async"
120
139
  style={{ objectFit, maxWidth: '100%' }}
121
- className="my-3"
140
+ // Inside a <figure> the margin belongs to the figure, and `block`
141
+ // drops the inline descender gap above the caption. Uncaptioned
142
+ // images keep the exact markup they had before the field existed.
143
+ className={hasCaption ? 'block' : 'my-3'}
122
144
  />
123
145
  );
146
+ if (!hasCaption) return image;
147
+ return (
148
+ <figure className="my-3">
149
+ {image}
150
+ <figcaption className={CAPTION_CLASS} style={CAPTION_STYLE}>
151
+ {/* Light markdown, like every other copy field in the module — the
152
+ Table/Carousel/CallToAction captions all render this way, so
153
+ `**Figure 1**` can't work in one caption and not another. */}
154
+ {renderRichText(caption)}
155
+ </figcaption>
156
+ </figure>
157
+ );
124
158
  },
125
159
  };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Register a Puck block whose render component loads on demand (issue #16).
3
+ *
4
+ * Block registration is eager by design — the host imports every module's
5
+ * `puck-blocks.ts` before first render so the registry is populated when a
6
+ * page reads the Puck config. But a registration that statically imports its
7
+ * render component drags the component's whole dependency graph into the
8
+ * entry chunk, on every page of the site. Measured on a consuming site, a
9
+ * map block put maplibre-gl — 1 MB minified, 62% of the bundle — in front
10
+ * of every visitor.
11
+ *
12
+ * `lazyBlock` keeps the cheap parts eager (fields, defaults, labels) and
13
+ * loads the component the first time a page actually renders the block:
14
+ *
15
+ * ```tsx
16
+ * registerPuckBlocks({
17
+ * blocks: {
18
+ * CanopyAtlasMap: lazyBlock(
19
+ * () => import('./components/CanopyAtlasMap').then((m) => m.CanopyAtlasMapEmbed),
20
+ * {
21
+ * label: 'Canopy Atlas map',
22
+ * fields: { height: { type: 'select', options: HEIGHTS } },
23
+ * defaultProps: { height: 'screen' },
24
+ * },
25
+ * (props) => <div className={heightClass[props.height]} aria-busy="true" />,
26
+ * ),
27
+ * },
28
+ * });
29
+ * ```
30
+ */
31
+
32
+ import { type ComponentType, lazy, type ReactNode, Suspense } from 'react';
33
+
34
+ import type { AnyBlock } from './blockRegistry';
35
+
36
+ // Typed via AnyBlock rather than a precise ComponentConfig<P>: Puck's config
37
+ // generic carries constraints a pass-through helper cannot restate, and the
38
+ // registry these blocks land in erases the props type anyway.
39
+ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous block props, same as AnyBlock
40
+ type AnyProps = Record<string, any>;
41
+
42
+ export function lazyBlock<P extends AnyProps>(
43
+ load: () => Promise<ComponentType<P>>,
44
+ config: Omit<AnyBlock, 'render'>,
45
+ /** Rendered while the chunk downloads — size it like the block to avoid
46
+ * layout shift. Defaults to nothing. */
47
+ fallback?: (props: P) => ReactNode,
48
+ ): AnyBlock {
49
+ const Lazy = lazy(async () => ({ default: await load() }));
50
+ return {
51
+ ...config,
52
+ render: (props: AnyProps) => (
53
+ <Suspense fallback={fallback ? fallback(props as P) : null}>
54
+ <Lazy {...(props as P)} />
55
+ </Suspense>
56
+ ),
57
+ };
58
+ }
@@ -1,4 +1,5 @@
1
1
  import { cn, decorativeAriaProps } from '../../../utils/widgetUtils';
2
+ import { SIZES_FULL, srcsetAttrs } from './image-srcset';
2
3
 
3
4
  export type BackgroundMediaOverlay = 'none' | 'subtle' | 'dark';
4
5
  export type BackgroundMediaFallback = 'gradient-mesh' | 'solid';
@@ -15,6 +16,8 @@ export type BackgroundMediaProps = {
15
16
  * anchors to the top; centered, the sky is cut away). Omitted = centered.
16
17
  */
17
18
  objectPosition?: string;
19
+ /** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
20
+ imageSrcset?: string;
18
21
  };
19
22
 
20
23
  // The "dark" scrim strength is tenant-tunable: GCA's Figma hero is nearly
@@ -41,6 +44,7 @@ export function BackgroundMedia({
41
44
  overlay = 'none',
42
45
  fallback = 'gradient-mesh',
43
46
  objectPosition,
47
+ imageSrcset,
44
48
  }: BackgroundMediaProps) {
45
49
  // data-pb-* attributes are stable selectors for tests and CMS-side analytics.
46
50
  return (
@@ -48,6 +52,7 @@ export function BackgroundMedia({
48
52
  {imageUrl ? (
49
53
  <img
50
54
  src={imageUrl}
55
+ {...srcsetAttrs(imageSrcset, SIZES_FULL)}
51
56
  alt={imageAlt ?? ''}
52
57
  {...decorativeAriaProps(imageAlt)}
53
58
  loading="lazy"
@@ -1,4 +1,5 @@
1
1
  import { cn, decorativeAriaProps } from '../../../utils/widgetUtils';
2
+ import { srcsetAttrs } from './image-srcset';
2
3
 
3
4
  export type ContentMediaAspect = 'square' | 'video' | 'portrait';
4
5
 
@@ -6,6 +7,10 @@ export type ContentMediaProps = {
6
7
  imageUrl: string;
7
8
  imageAlt: string;
8
9
  aspect?: ContentMediaAspect;
10
+ /** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
11
+ imageSrcset?: string;
12
+ /** `sizes` for the srcset; callers in split layouts pass SIZES_HALF. */
13
+ sizes?: string;
9
14
  };
10
15
 
11
16
  const ASPECT_CLASSES: Record<ContentMediaAspect, string> = {
@@ -23,13 +28,20 @@ const DOTTED_STYLE = {
23
28
  backgroundSize: '16px 16px, 100% 100%',
24
29
  } as const;
25
30
 
26
- export function ContentMedia({ imageUrl, imageAlt, aspect = 'video' }: ContentMediaProps) {
31
+ export function ContentMedia({
32
+ imageUrl,
33
+ imageAlt,
34
+ aspect = 'video',
35
+ imageSrcset,
36
+ sizes = '100vw',
37
+ }: ContentMediaProps) {
27
38
  const wrapperCls = cn('w-full rounded-xl overflow-hidden', ASPECT_CLASSES[aspect], 'shadow-xl');
28
39
  if (imageUrl) {
29
40
  return (
30
41
  <div className={wrapperCls}>
31
42
  <img
32
43
  src={imageUrl}
44
+ {...srcsetAttrs(imageSrcset, sizes)}
33
45
  alt={imageAlt ?? ''}
34
46
  {...decorativeAriaProps(imageAlt)}
35
47
  loading="lazy"
@@ -0,0 +1,55 @@
1
+ /** Responsive-image support for picker-backed widgets (issue #15).
2
+ *
3
+ * The media library generates WebP variants at upload, but widgets that
4
+ * store a bare `imageUrl` string shipped the full-size original to every
5
+ * viewport. These helpers let a widget persist the variant srcset next to
6
+ * the URL the same way the Image block does: `resolveImageSrcset` runs as
7
+ * Puck's `resolveData` when the picked URL changes, and the value lands in
8
+ * block data so the public render needs no lookup at all. Old content
9
+ * without the prop renders exactly as before.
10
+ */
11
+
12
+ import { buildSrcset, lookupAsset } from '../../../utils/mediaApi';
13
+
14
+ /** Full-bleed imagery (background heroes, banners). */
15
+ export const SIZES_FULL = '100vw';
16
+
17
+ /** Half-width split layouts (side hero, media object) — full width stacked. */
18
+ export const SIZES_HALF = '(min-width: 1024px) 50vw, 100vw';
19
+
20
+ /** Field for the persisted srcset — visible so an author can override it. */
21
+ export const imageSrcsetField = {
22
+ type: 'textarea',
23
+ label: 'Image srcset (auto-filled from the media library)',
24
+ } as const;
25
+
26
+ /** Spreadable `<img>` attributes; nothing when no srcset is recorded. */
27
+ export function srcsetAttrs(
28
+ srcset: string | undefined,
29
+ sizes: string,
30
+ ): { srcSet: string; sizes: string } | Record<string, never> {
31
+ return srcset ? { srcSet: srcset, sizes } : {};
32
+ }
33
+
34
+ type SrcsetProps = { imageUrl: string; imageSrcset?: string };
35
+
36
+ /** `resolveData` for any widget with `imageUrl` + `imageSrcset` props.
37
+ * `changed` is typed to just the key read here — Puck's own param type
38
+ * (which also carries `id: string`) stays assignable. */
39
+ export async function resolveImageSrcset<P extends SrcsetProps>(
40
+ { props }: { props: P },
41
+ { changed }: { changed: { imageUrl?: unknown } },
42
+ ): Promise<{ props: P }> {
43
+ if (!changed.imageUrl) return { props };
44
+ if (!props.imageUrl) return { props: { ...props, imageSrcset: '' } };
45
+ let asset: Awaited<ReturnType<typeof lookupAsset>>;
46
+ try {
47
+ asset = await lookupAsset(props.imageUrl);
48
+ } catch {
49
+ // Network failure mid-edit shouldn't break the editor — keep the picked
50
+ // URL and let the image serve without variants.
51
+ return { props };
52
+ }
53
+ if (!asset) return { props };
54
+ return { props: { ...props, imageSrcset: buildSrcset(asset) } };
55
+ }
@@ -26,6 +26,13 @@ export type {
26
26
  HeadingTone,
27
27
  } from './heading';
28
28
  export { Heading } from './heading';
29
+ export {
30
+ imageSrcsetField,
31
+ resolveImageSrcset,
32
+ SIZES_FULL,
33
+ SIZES_HALF,
34
+ srcsetAttrs,
35
+ } from './image-srcset';
29
36
  export {
30
37
  EYEBROW_CASE_STYLE,
31
38
  eyebrowTextStyle,
@@ -0,0 +1,33 @@
1
+ /** Prop shape for HeroWidget — extracted so hero-widget.tsx stays under the
2
+ * repo's 300-line cap. Re-exported from there, so importers are unaffected. */
3
+
4
+ export type HeroWidgetProps = {
5
+ eyebrow: string;
6
+ title: string;
7
+ subtitle: string;
8
+ logoUrl: string;
9
+ logoAlt: string;
10
+ imageUrl: string;
11
+ imageAlt: string;
12
+ /** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
13
+ imageSrcset?: string;
14
+ /**
15
+ * Organic mask (SVG url, from the tenant's content) applied to the side
16
+ * image in the right/left layouts. Empty = the standard rounded media.
17
+ */
18
+ imageMaskUrl?: string;
19
+ /**
20
+ * "native" renders supplied transparent artwork exactly as-is — no mask, no
21
+ * crop — for a photo already cut to its final organic shape. Preferred over
22
+ * `imageMaskUrl`, which re-derives the silhouette from a hand-kept SVG.
23
+ */
24
+ imageShape?: 'rounded' | 'native';
25
+ imagePosition: 'background' | 'right' | 'left';
26
+ primaryLabel: string;
27
+ primaryHref: string;
28
+ secondaryLabel: string;
29
+ secondaryHref: string;
30
+ overlay: boolean;
31
+ align: 'left' | 'center';
32
+ surface: 'plain' | 'card';
33
+ };
@@ -8,38 +8,17 @@ import {
8
8
  CTAButton,
9
9
  EyebrowText,
10
10
  Heading,
11
+ imageSrcsetField,
11
12
  maskStyle,
13
+ resolveImageSrcset,
12
14
  Section,
15
+ SIZES_HALF,
16
+ srcsetAttrs,
13
17
  } from './_shared';
14
18
 
15
- export type HeroWidgetProps = {
16
- eyebrow: string;
17
- title: string;
18
- subtitle: string;
19
- logoUrl: string;
20
- logoAlt: string;
21
- imageUrl: string;
22
- imageAlt: string;
23
- /**
24
- * Organic mask (SVG url, from the tenant's content) applied to the side
25
- * image in the right/left layouts. Empty = the standard rounded media.
26
- */
27
- imageMaskUrl?: string;
28
- /**
29
- * "native" renders supplied transparent artwork exactly as-is — no mask, no
30
- * crop — for a photo already cut to its final organic shape. Preferred over
31
- * `imageMaskUrl`, which re-derives the silhouette from a hand-kept SVG.
32
- */
33
- imageShape?: 'rounded' | 'native';
34
- imagePosition: 'background' | 'right' | 'left';
35
- primaryLabel: string;
36
- primaryHref: string;
37
- secondaryLabel: string;
38
- secondaryHref: string;
39
- overlay: boolean;
40
- align: 'left' | 'center';
41
- surface: 'plain' | 'card';
42
- };
19
+ import type { HeroWidgetProps } from './hero-widget-props';
20
+
21
+ export type { HeroWidgetProps } from './hero-widget-props';
43
22
 
44
23
  // Exported so CenteredHeroWidget can render through it without smuggling
45
24
  // Puck's ComponentConfig.render signature past the type system.
@@ -51,6 +30,7 @@ export function renderHero({
51
30
  logoAlt,
52
31
  imageUrl,
53
32
  imageAlt,
33
+ imageSrcset = '',
54
34
  imageMaskUrl = '',
55
35
  imageShape = 'rounded',
56
36
  imagePosition,
@@ -120,6 +100,7 @@ export function renderHero({
120
100
  <BackgroundMedia
121
101
  imageUrl={imageUrl}
122
102
  imageAlt={imageAlt}
103
+ imageSrcset={imageSrcset}
123
104
  overlay={overlay ? 'dark' : 'none'}
124
105
  fallback="gradient-mesh"
125
106
  objectPosition="var(--pb-hero-image-position, 50% 50%)"
@@ -189,6 +170,7 @@ export function renderHero({
189
170
  // Figma's box width. No mask, no crop — the file IS the shape.
190
171
  <img
191
172
  src={imageUrl}
173
+ {...srcsetAttrs(imageSrcset, SIZES_HALF)}
192
174
  alt={imageAlt || title}
193
175
  loading="lazy"
194
176
  className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)]"
@@ -199,6 +181,7 @@ export function renderHero({
199
181
  // unset, the image keeps its previous free-flowing size.
200
182
  <img
201
183
  src={imageUrl}
184
+ {...srcsetAttrs(imageSrcset, SIZES_HALF)}
202
185
  alt={imageAlt || title}
203
186
  loading="lazy"
204
187
  className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)] object-cover"
@@ -208,7 +191,13 @@ export function renderHero({
208
191
  }}
209
192
  />
210
193
  ) : (
211
- <ContentMedia imageUrl={imageUrl} imageAlt={imageAlt} aspect="square" />
194
+ <ContentMedia
195
+ imageUrl={imageUrl}
196
+ imageAlt={imageAlt}
197
+ imageSrcset={imageSrcset}
198
+ sizes={SIZES_HALF}
199
+ aspect="square"
200
+ />
212
201
  )}
213
202
  </div>
214
203
  </div>
@@ -226,6 +215,7 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
226
215
  logoAlt: { type: 'text', label: 'Logo alt text' },
227
216
  imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
228
217
  imageAlt: { type: 'text', label: 'Image alt text' },
218
+ imageSrcset: imageSrcsetField,
229
219
  imageMaskUrl: {
230
220
  type: 'text',
231
221
  label: 'Image mask URL (organic shape, side layouts — optional)',
@@ -277,6 +267,7 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
277
267
  logoAlt: '',
278
268
  imageUrl: '',
279
269
  imageAlt: '',
270
+ imageSrcset: '',
280
271
  imageMaskUrl: '',
281
272
  imageShape: 'rounded',
282
273
  imagePosition: 'background',
@@ -288,5 +279,6 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
288
279
  align: 'center',
289
280
  surface: 'plain',
290
281
  },
282
+ resolveData: resolveImageSrcset,
291
283
  render: renderHero,
292
284
  };
@@ -5,6 +5,8 @@ import type { CSSProperties } from 'react';
5
5
  export type MediaObjectWidgetProps = {
6
6
  imageUrl: string;
7
7
  imageAlt: string;
8
+ /** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
9
+ imageSrcset?: string;
8
10
  imagePosition: 'left' | 'right';
9
11
  heading: string;
10
12
  body: string;
@@ -9,7 +9,7 @@ import { cn } from '../../utils/widgetUtils';
9
9
  import { AccentText } from './_internal/accent-text';
10
10
  import { LinkArrow } from './_internal/link-arrow';
11
11
  import { RichTextBlock, renderRichText } from './_internal/rich-text';
12
- import { CTAButton, eyebrowTextStyle, maskStyle } from './_shared';
12
+ import { CTAButton, eyebrowTextStyle, maskStyle, SIZES_HALF, srcsetAttrs } from './_shared';
13
13
 
14
14
  import { GRID_IMAGE, GRID_TEXT, type MediaObjectWidgetProps } from './media-object-layout';
15
15
  import { MediaObjectText } from './media-object-text';
@@ -19,6 +19,7 @@ export type { MediaObjectWidgetProps } from './media-object-layout';
19
19
  export const MediaObjectWidgetRender = ({
20
20
  imageUrl,
21
21
  imageAlt,
22
+ imageSrcset = '',
22
23
  imageMaskUrl = '',
23
24
  imageShape = 'rounded',
24
25
  imageTag = '',
@@ -86,6 +87,7 @@ export const MediaObjectWidgetRender = ({
86
87
  const imageEl = imageUrl ? (
87
88
  <img
88
89
  src={imageUrl}
90
+ {...srcsetAttrs(imageSrcset, SIZES_HALF)}
89
91
  alt={imageAlt || heading}
90
92
  loading="lazy"
91
93
  className={cn('w-full h-auto', !masked && !native && 'rounded-lg')}
@@ -196,6 +198,8 @@ export const MediaObjectWidgetRender = ({
196
198
  {imageUrl && (
197
199
  <img
198
200
  src={imageUrl}
201
+ // Rendered 68px tall — the smallest variant is plenty.
202
+ {...srcsetAttrs(imageSrcset, '320px')}
199
203
  alt={imageAlt || heading}
200
204
  loading="lazy"
201
205
  className="mt-8 h-[68px] w-auto max-w-full object-contain object-left"