simple-module-pagebuilder 0.0.3__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.3 → simple_module_pagebuilder-0.0.4}/PKG-INFO +33 -1
  2. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/README.md +32 -0
  3. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/package.json +1 -1
  4. simple_module_pagebuilder-0.0.4/pagebuilder/components/lazyBlock.tsx +58 -0
  5. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/background-media.tsx +5 -0
  6. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/content-media.tsx +13 -1
  7. simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/_shared/image-srcset.ts +55 -0
  8. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/index.ts +7 -0
  9. simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/hero-widget-props.ts +33 -0
  10. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/hero-widget.tsx +21 -29
  11. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-layout.ts +2 -0
  12. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-render.tsx +5 -1
  13. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-widget.tsx +4 -0
  14. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/seo.py +4 -1
  15. simple_module_pagebuilder-0.0.4/pagebuilder/media_files.py +128 -0
  16. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_service.py +4 -1
  17. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/module.py +8 -3
  18. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/__init__.py +32 -2
  19. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/settings.py +5 -2
  20. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pyproject.toml +1 -1
  21. simple_module_pagebuilder-0.0.4/tests/test_media_files.py +137 -0
  22. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/.gitignore +0 -0
  23. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/__init__.py +0 -0
  24. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/DiffSummary.tsx +0 -0
  25. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/ScheduledBadge.tsx +0 -0
  26. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/StatusBadge.tsx +0 -0
  27. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.test.ts +0 -0
  28. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.ts +0 -0
  29. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Button.tsx +0 -0
  30. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Columns.tsx +0 -0
  31. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Heading.tsx +0 -0
  32. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Image.test.tsx +0 -0
  33. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Image.tsx +0 -0
  34. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
  35. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Spacer.tsx +0 -0
  36. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Text.tsx +0 -0
  37. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/PageEditorToolbar.tsx +0 -0
  38. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +0 -0
  39. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SchedulePanel.tsx +0 -0
  40. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -0
  41. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
  42. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFilters.tsx +0 -0
  43. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFolderSidebar.tsx +0 -0
  44. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaGrid.tsx +0 -0
  45. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaHeader.tsx +0 -0
  46. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaUploadQueue.tsx +0 -0
  47. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/types.ts +0 -0
  48. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.test.ts +0 -0
  49. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.ts +0 -0
  50. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.test.tsx +0 -0
  51. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.tsx +0 -0
  52. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
  53. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.ts +0 -0
  54. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
  55. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
  56. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
  57. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
  58. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
  59. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
  60. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
  61. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
  62. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
  63. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
  64. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
  65. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
  66. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
  67. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
  68. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
  69. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
  70. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
  71. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
  72. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
  73. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
  74. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
  75. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
  76. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
  77. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
  78. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
  79. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/column-widget.tsx +0 -0
  80. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
  81. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
  82. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
  83. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/container-widget.tsx +0 -0
  84. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
  85. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/divider-widget.tsx +0 -0
  86. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
  87. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-render.tsx +0 -0
  88. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
  89. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
  90. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
  91. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
  92. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
  93. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
  94. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
  95. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
  96. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
  97. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/html-widget.tsx +0 -0
  98. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
  99. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
  100. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/list-widget.tsx +0 -0
  101. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
  102. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
  103. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
  104. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
  105. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
  106. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
  107. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
  108. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/row-widget.tsx +0 -0
  109. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
  110. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
  111. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
  112. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
  113. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
  114. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
  115. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
  116. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
  117. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
  118. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
  119. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/table-widget.tsx +0 -0
  120. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
  121. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
  122. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
  123. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
  124. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
  125. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
  126. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
  127. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/video-widget.tsx +0 -0
  128. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
  129. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/__init__.py +0 -0
  130. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/events.py +0 -0
  131. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/schemas.py +0 -0
  132. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.test.ts +0 -0
  133. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.ts +0 -0
  134. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/index.ts +0 -0
  135. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/types.ts +0 -0
  136. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/deps.py +0 -0
  137. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/diff.py +0 -0
  138. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/__init__.py +0 -0
  139. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/__init__.py +0 -0
  140. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/_deps.py +0 -0
  141. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/layout.py +0 -0
  142. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/pages.py +0 -0
  143. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/revisions.py +0 -0
  144. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/uploads.py +0 -0
  145. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/workflow.py +0 -0
  146. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/views.py +0 -0
  147. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/checkbox-field.tsx +0 -0
  148. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
  149. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
  150. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/index.ts +0 -0
  151. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/types.ts +0 -0
  152. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/index.ts +0 -0
  153. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/media-adapter.ts +0 -0
  154. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useAutosave.ts +0 -0
  155. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useEditorForm.ts +0 -0
  156. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useMediaUploads.ts +0 -0
  157. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageRevisions.ts +0 -0
  158. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageSchedule.ts +0 -0
  159. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageWorkflow.ts +0 -0
  160. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/layout_service.py +0 -0
  161. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_images.py +0 -0
  162. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_queries.py +0 -0
  163. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/models.py +0 -0
  164. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/LayoutEditor.tsx +0 -0
  165. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/MediaLibrary.tsx +0 -0
  166. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageEditor.tsx +0 -0
  167. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageList.tsx +0 -0
  168. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PendingReview.tsx +0 -0
  169. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PublicPage.tsx +0 -0
  170. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/permissions.py +0 -0
  171. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/py.typed +0 -0
  172. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/security.py +0 -0
  173. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_common.py +0 -0
  174. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_revisions.py +0 -0
  175. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_workflow.py +0 -0
  176. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/services.py +0 -0
  177. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/static/widgets-base.css +0 -0
  178. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/apply.ts +0 -0
  179. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/extract.ts +0 -0
  180. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/index.ts +0 -0
  181. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translatable-fields.ts +0 -0
  182. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translation.test.ts +0 -0
  183. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/api.ts +0 -0
  184. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/datetime.ts +0 -0
  185. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/decorative-image.ts +0 -0
  186. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/editorSnapshot.ts +0 -0
  187. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/markdown.ts +0 -0
  188. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaApi.ts +0 -0
  189. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaFormat.ts +0 -0
  190. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/pagesApi.ts +0 -0
  191. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/parse-list.ts +0 -0
  192. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/request.ts +0 -0
  193. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/slugify.ts +0 -0
  194. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/types.ts +0 -0
  195. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/widgetUtils.ts +0 -0
  196. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/conftest.py +0 -0
  197. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_approval_workflow.py +0 -0
  198. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_auth_gating.py +0 -0
  199. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_csrf.py +0 -0
  200. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_image_processing.py +0 -0
  201. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_media_security.py +0 -0
  202. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_module.py +0 -0
  203. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_pages_api.py +0 -0
  204. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_public_routes.py +0 -0
  205. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_public_view_headers.py +0 -0
  206. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_public_viewer.py +0 -0
  207. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_revision_notes_and_diff.py +0 -0
  208. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_revisions_api.py +0 -0
  209. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_scheduled_publish.py +0 -0
  210. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_seo_meta.py +0 -0
  211. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_site_layout.py +0 -0
  212. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_sitemap_robots.py +0 -0
  213. {simple_module_pagebuilder-0.0.3 → simple_module_pagebuilder-0.0.4}/tests/test_uploads_api.py +0 -0
  214. {simple_module_pagebuilder-0.0.3 → 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.3
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.3",
3
+ "version": "0.0.4",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the PageBuilder module",
6
6
  "peerDependencies": {
@@ -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"
@@ -5,6 +5,7 @@ import type { ComponentConfig } from '@puckeditor/core';
5
5
 
6
6
  import { createImageField, mediaLibraryAdapter } from '../../fields';
7
7
 
8
+ import { imageSrcsetField, resolveImageSrcset } from './_shared';
8
9
  import { type MediaObjectWidgetProps, MediaObjectWidgetRender } from './media-object-render';
9
10
 
10
11
  export type * from './media-object-render';
@@ -14,6 +15,7 @@ export const MediaObjectWidget: ComponentConfig<MediaObjectWidgetProps> = {
14
15
  fields: {
15
16
  imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
16
17
  imageAlt: { type: 'text', label: 'Image alt text' },
18
+ imageSrcset: imageSrcsetField,
17
19
  imageMaskUrl: {
18
20
  type: 'text',
19
21
  label: 'Image mask URL (organic shape — optional)',
@@ -145,6 +147,7 @@ export const MediaObjectWidget: ComponentConfig<MediaObjectWidgetProps> = {
145
147
  defaultProps: {
146
148
  imageUrl: '',
147
149
  imageAlt: '',
150
+ imageSrcset: '',
148
151
  imageMaskUrl: '',
149
152
  imageShape: 'rounded',
150
153
  imageTag: '',
@@ -172,5 +175,6 @@ export const MediaObjectWidget: ComponentConfig<MediaObjectWidgetProps> = {
172
175
  surface: 'default',
173
176
  surfaceColor: '',
174
177
  },
178
+ resolveData: resolveImageSrcset,
175
179
  render: (props) => <MediaObjectWidgetRender {...props} />,
176
180
  };
@@ -42,7 +42,10 @@ async def sitemap(
42
42
  if not settings.sitemap_enabled:
43
43
  return Response(status_code=404)
44
44
 
45
- pages = await PagesService(db).list_indexable_published()
45
+ # Column-only query: at a few thousand pages, loading full entities
46
+ # (with both block-JSON columns) made every crawl a multi-second,
47
+ # tens-of-MB request (issue #11).
48
+ pages = await PagesService(db).list_sitemap_entries()
46
49
  origin = _public_origin(request, settings)
47
50
  prefix = settings.public_route_prefix
48
51
 
@@ -0,0 +1,128 @@
1
+ """Media file serving: root resolution, cache headers, integrity check.
2
+
3
+ Extracted from ``module.py`` (which sits near the size cap) and shared with
4
+ :class:`~pagebuilder.media_service.MediaService` so the mount that *serves*
5
+ files and the service that *writes* them can never disagree about where the
6
+ media library lives (issue #14).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from fastapi.staticfiles import StaticFiles
17
+ from starlette.exceptions import HTTPException
18
+ from starlette.responses import PlainTextResponse, Response
19
+ from starlette.types import Scope
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+ from sqlalchemy.ext.asyncio import AsyncSession
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Same override + sentinels the hosting package uses to find the project
29
+ # root, so a host and this module always agree on the anchor. Duplicated
30
+ # rather than imported: modules depend on core, not hosting.
31
+ _ENV_PROJECT_ROOT = "SM_PROJECT_ROOT"
32
+ _PROJECT_ROOT_SENTINELS = ("pyproject.toml", ".env", "alembic.ini")
33
+
34
+
35
+ def _project_root() -> Path:
36
+ override = os.environ.get(_ENV_PROJECT_ROOT)
37
+ if override:
38
+ return Path(override)
39
+ cwd = Path.cwd().resolve()
40
+ for candidate in (cwd, *cwd.parents):
41
+ if any((candidate / s).exists() for s in _PROJECT_ROOT_SENTINELS):
42
+ return candidate
43
+ return cwd
44
+
45
+
46
+ def resolve_media_root(configured: Path | str) -> Path:
47
+ """Anchor a relative ``media_root`` to the project root, not the cwd.
48
+
49
+ A cwd-relative default meant every process with a different working
50
+ directory read and wrote a *different* media directory while sharing one
51
+ database — rows silently outlived their files (issue #14). Absolute
52
+ paths pass through untouched.
53
+ """
54
+ root = Path(configured)
55
+ if not root.is_absolute():
56
+ root = _project_root() / root
57
+ return root.resolve()
58
+
59
+
60
+ class MediaFiles(StaticFiles):
61
+ """StaticFiles with immutable caching and an honest 404.
62
+
63
+ Media filenames are content-addressed at upload (a fresh UUID hex per
64
+ file, variants included), so a URL never serves different bytes —
65
+ long-lived immutable caching is as safe here as for hashed Vite assets
66
+ (issue #13). Missing files answer with a plain-text 404 instead of
67
+ falling through to the HTML app shell, which hid broken images behind
68
+ ``text/html`` responses (issue #14).
69
+ """
70
+
71
+ _CACHE_CONTROL = "public, max-age=31536000, immutable"
72
+
73
+ async def get_response(self, path: str, scope: Scope) -> Response:
74
+ try:
75
+ response = await super().get_response(path, scope)
76
+ except HTTPException as exc:
77
+ if exc.status_code == 404:
78
+ return PlainTextResponse("Not Found", status_code=404)
79
+ raise
80
+ if response.status_code in (200, 304):
81
+ response.headers.setdefault("Cache-Control", self._CACHE_CONTROL)
82
+ return response
83
+
84
+
85
+ async def count_missing_media_files(session: AsyncSession, media_root: Path) -> int:
86
+ """Rows in the media library whose file (or a variant) is gone from disk."""
87
+ from sqlmodel import select
88
+
89
+ from pagebuilder.models import MediaAsset
90
+
91
+ result = await session.execute(select(MediaAsset.filename, MediaAsset.variants))
92
+ missing = 0
93
+ for filename, variants in result.all():
94
+ names = [filename] + [
95
+ meta["filename"] for meta in (variants or {}).values() if meta.get("filename")
96
+ ]
97
+ if any(not (media_root / name).is_file() for name in names):
98
+ missing += 1
99
+ return missing
100
+
101
+
102
+ async def warn_on_orphaned_media(
103
+ session_factory: Callable[[], AsyncSession], media_root: Path
104
+ ) -> int:
105
+ """Log a warning when media rows point at files that don't exist.
106
+
107
+ Runs once at startup. This drift is otherwise invisible until a visitor
108
+ sees broken images: the DB says the asset exists, the picker lists it,
109
+ and only the file request fails. Failures of the check itself (e.g. the
110
+ table doesn't exist yet on a fresh database) are logged at debug and
111
+ never block startup.
112
+ """
113
+ try:
114
+ async with session_factory() as session:
115
+ missing = await count_missing_media_files(session, media_root)
116
+ except Exception: # pragma: no cover - defensive: never break boot
117
+ logger.debug("pagebuilder.media integrity check skipped", exc_info=True)
118
+ return 0
119
+ if missing:
120
+ logger.warning(
121
+ "pagebuilder.media: %d media row(s) have no file under %s — "
122
+ "uploads from a different working directory or a missing volume. "
123
+ "Re-upload the assets or point SM_PAGEBUILDER_MEDIA_ROOT at the "
124
+ "directory that holds them.",
125
+ missing,
126
+ media_root,
127
+ )
128
+ return missing