simple-module-pagebuilder 0.0.1__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 (208) hide show
  1. simple_module_pagebuilder-0.0.1/.gitignore +11 -0
  2. simple_module_pagebuilder-0.0.1/PKG-INFO +152 -0
  3. simple_module_pagebuilder-0.0.1/README.md +130 -0
  4. simple_module_pagebuilder-0.0.1/package.json +18 -0
  5. simple_module_pagebuilder-0.0.1/pagebuilder/__init__.py +0 -0
  6. simple_module_pagebuilder-0.0.1/pagebuilder/components/DiffSummary.tsx +107 -0
  7. simple_module_pagebuilder-0.0.1/pagebuilder/components/ScheduledBadge.tsx +34 -0
  8. simple_module_pagebuilder-0.0.1/pagebuilder/components/StatusBadge.tsx +21 -0
  9. simple_module_pagebuilder-0.0.1/pagebuilder/components/blockRegistry.test.ts +56 -0
  10. simple_module_pagebuilder-0.0.1/pagebuilder/components/blockRegistry.ts +80 -0
  11. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Button.tsx +142 -0
  12. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Columns.tsx +64 -0
  13. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Heading.tsx +92 -0
  14. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Image.tsx +125 -0
  15. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/MediaPicker.tsx +178 -0
  16. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Spacer.tsx +31 -0
  17. simple_module_pagebuilder-0.0.1/pagebuilder/components/blocks/Text.tsx +80 -0
  18. simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
  19. simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
  20. simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/SchedulePanel.tsx +85 -0
  21. simple_module_pagebuilder-0.0.1/pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
  22. simple_module_pagebuilder-0.0.1/pagebuilder/components/layoutPuckConfig.tsx +109 -0
  23. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaFilters.tsx +83 -0
  24. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
  25. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaGrid.tsx +76 -0
  26. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaHeader.tsx +37 -0
  27. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
  28. simple_module_pagebuilder-0.0.1/pagebuilder/components/media/types.ts +20 -0
  29. simple_module_pagebuilder-0.0.1/pagebuilder/components/migrateContent.test.ts +146 -0
  30. simple_module_pagebuilder-0.0.1/pagebuilder/components/migrateContent.ts +135 -0
  31. simple_module_pagebuilder-0.0.1/pagebuilder/components/puckConfig.test.tsx +118 -0
  32. simple_module_pagebuilder-0.0.1/pagebuilder/components/puckConfig.tsx +121 -0
  33. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgetCatalog.test.tsx +123 -0
  34. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgetCatalog.ts +208 -0
  35. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
  36. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
  37. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
  38. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
  39. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
  40. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
  41. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
  42. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
  43. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
  44. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
  45. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
  46. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/heading.tsx +88 -0
  47. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/index.ts +39 -0
  48. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/mask.ts +54 -0
  49. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/section.tsx +60 -0
  50. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
  51. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
  52. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/accordion-widget.tsx +64 -0
  53. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/alert-widget.tsx +47 -0
  54. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
  55. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/article-cards-render.tsx +132 -0
  56. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
  57. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
  58. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
  59. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
  60. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/carousel-widget.tsx +97 -0
  61. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
  62. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
  63. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/column-widget.tsx +63 -0
  64. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
  65. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-form-render.tsx +293 -0
  66. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
  67. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/container-widget.tsx +93 -0
  68. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
  69. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/divider-widget.tsx +56 -0
  70. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
  71. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/faq-render.tsx +268 -0
  72. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/faq-widget.tsx +64 -0
  73. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
  74. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
  75. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
  76. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
  77. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
  78. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
  79. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/gallery-widget.tsx +139 -0
  80. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/grid-widget.tsx +59 -0
  81. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/hero-widget.tsx +292 -0
  82. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/html-widget.tsx +42 -0
  83. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/iframe-widget.tsx +44 -0
  84. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
  85. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/list-widget.tsx +56 -0
  86. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
  87. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/markdown-widget.tsx +148 -0
  88. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-layout.ts +156 -0
  89. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-render.tsx +284 -0
  90. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-text.tsx +202 -0
  91. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/media-object-widget.tsx +176 -0
  92. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
  93. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/page-header-widget.tsx +118 -0
  94. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
  95. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/quote-widget.tsx +51 -0
  96. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/row-widget.tsx +74 -0
  97. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
  98. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
  99. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
  100. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
  101. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-header-nav.tsx +153 -0
  102. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/site-header-widget.tsx +182 -0
  103. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
  104. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/stats-widget.tsx +200 -0
  105. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/steps-widget.tsx +211 -0
  106. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
  107. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/table-widget.tsx +86 -0
  108. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/tabs-widget.tsx +95 -0
  109. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/tags-widget.tsx +85 -0
  110. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
  111. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-layout.ts +73 -0
  112. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-render.tsx +294 -0
  113. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/timeline-widget.tsx +106 -0
  114. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
  115. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/video-widget.tsx +103 -0
  116. simple_module_pagebuilder-0.0.1/pagebuilder/components/widgets/welcome-widget.tsx +38 -0
  117. simple_module_pagebuilder-0.0.1/pagebuilder/contracts/__init__.py +57 -0
  118. simple_module_pagebuilder-0.0.1/pagebuilder/contracts/events.py +25 -0
  119. simple_module_pagebuilder-0.0.1/pagebuilder/contracts/schemas.py +239 -0
  120. simple_module_pagebuilder-0.0.1/pagebuilder/conversion/blocks-to-html.test.ts +105 -0
  121. simple_module_pagebuilder-0.0.1/pagebuilder/conversion/blocks-to-html.ts +102 -0
  122. simple_module_pagebuilder-0.0.1/pagebuilder/conversion/index.ts +2 -0
  123. simple_module_pagebuilder-0.0.1/pagebuilder/conversion/types.ts +5 -0
  124. simple_module_pagebuilder-0.0.1/pagebuilder/deps.py +26 -0
  125. simple_module_pagebuilder-0.0.1/pagebuilder/diff.py +129 -0
  126. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/__init__.py +0 -0
  127. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/__init__.py +21 -0
  128. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/_deps.py +21 -0
  129. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/layout.py +78 -0
  130. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/pages.py +85 -0
  131. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/revisions.py +79 -0
  132. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/uploads.py +92 -0
  133. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/api/workflow.py +124 -0
  134. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/seo.py +95 -0
  135. simple_module_pagebuilder-0.0.1/pagebuilder/endpoints/views.py +228 -0
  136. simple_module_pagebuilder-0.0.1/pagebuilder/fields/checkbox-field.tsx +22 -0
  137. simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
  138. simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
  139. simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/index.ts +6 -0
  140. simple_module_pagebuilder-0.0.1/pagebuilder/fields/file-picker/types.ts +15 -0
  141. simple_module_pagebuilder-0.0.1/pagebuilder/fields/index.ts +9 -0
  142. simple_module_pagebuilder-0.0.1/pagebuilder/fields/media-adapter.ts +47 -0
  143. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useAutosave.ts +134 -0
  144. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useEditorForm.ts +193 -0
  145. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/useMediaUploads.ts +96 -0
  146. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageRevisions.ts +59 -0
  147. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageSchedule.ts +72 -0
  148. simple_module_pagebuilder-0.0.1/pagebuilder/hooks/usePageWorkflow.ts +173 -0
  149. simple_module_pagebuilder-0.0.1/pagebuilder/layout_service.py +101 -0
  150. simple_module_pagebuilder-0.0.1/pagebuilder/media_images.py +185 -0
  151. simple_module_pagebuilder-0.0.1/pagebuilder/media_queries.py +92 -0
  152. simple_module_pagebuilder-0.0.1/pagebuilder/media_service.py +222 -0
  153. simple_module_pagebuilder-0.0.1/pagebuilder/models.py +209 -0
  154. simple_module_pagebuilder-0.0.1/pagebuilder/module.py +279 -0
  155. simple_module_pagebuilder-0.0.1/pagebuilder/pages/LayoutEditor.tsx +183 -0
  156. simple_module_pagebuilder-0.0.1/pagebuilder/pages/MediaLibrary.tsx +249 -0
  157. simple_module_pagebuilder-0.0.1/pagebuilder/pages/PageEditor.tsx +181 -0
  158. simple_module_pagebuilder-0.0.1/pagebuilder/pages/PageList.tsx +129 -0
  159. simple_module_pagebuilder-0.0.1/pagebuilder/pages/PendingReview.tsx +128 -0
  160. simple_module_pagebuilder-0.0.1/pagebuilder/pages/PublicPage.tsx +126 -0
  161. simple_module_pagebuilder-0.0.1/pagebuilder/permissions.py +44 -0
  162. simple_module_pagebuilder-0.0.1/pagebuilder/py.typed +0 -0
  163. simple_module_pagebuilder-0.0.1/pagebuilder/security.py +230 -0
  164. simple_module_pagebuilder-0.0.1/pagebuilder/service/__init__.py +141 -0
  165. simple_module_pagebuilder-0.0.1/pagebuilder/service/_common.py +24 -0
  166. simple_module_pagebuilder-0.0.1/pagebuilder/service/_revisions.py +92 -0
  167. simple_module_pagebuilder-0.0.1/pagebuilder/service/_workflow.py +207 -0
  168. simple_module_pagebuilder-0.0.1/pagebuilder/services.py +19 -0
  169. simple_module_pagebuilder-0.0.1/pagebuilder/settings.py +161 -0
  170. simple_module_pagebuilder-0.0.1/pagebuilder/static/widgets-base.css +57 -0
  171. simple_module_pagebuilder-0.0.1/pagebuilder/translation/apply.ts +54 -0
  172. simple_module_pagebuilder-0.0.1/pagebuilder/translation/extract.ts +141 -0
  173. simple_module_pagebuilder-0.0.1/pagebuilder/translation/index.ts +20 -0
  174. simple_module_pagebuilder-0.0.1/pagebuilder/translation/translatable-fields.ts +65 -0
  175. simple_module_pagebuilder-0.0.1/pagebuilder/translation/translation.test.ts +167 -0
  176. simple_module_pagebuilder-0.0.1/pagebuilder/utils/api.ts +12 -0
  177. simple_module_pagebuilder-0.0.1/pagebuilder/utils/datetime.ts +25 -0
  178. simple_module_pagebuilder-0.0.1/pagebuilder/utils/decorative-image.ts +15 -0
  179. simple_module_pagebuilder-0.0.1/pagebuilder/utils/editorSnapshot.ts +39 -0
  180. simple_module_pagebuilder-0.0.1/pagebuilder/utils/markdown.ts +160 -0
  181. simple_module_pagebuilder-0.0.1/pagebuilder/utils/mediaApi.ts +112 -0
  182. simple_module_pagebuilder-0.0.1/pagebuilder/utils/mediaFormat.ts +19 -0
  183. simple_module_pagebuilder-0.0.1/pagebuilder/utils/pagesApi.ts +111 -0
  184. simple_module_pagebuilder-0.0.1/pagebuilder/utils/parse-list.ts +7 -0
  185. simple_module_pagebuilder-0.0.1/pagebuilder/utils/request.ts +52 -0
  186. simple_module_pagebuilder-0.0.1/pagebuilder/utils/slugify.ts +9 -0
  187. simple_module_pagebuilder-0.0.1/pagebuilder/utils/types.ts +171 -0
  188. simple_module_pagebuilder-0.0.1/pagebuilder/utils/widgetUtils.ts +11 -0
  189. simple_module_pagebuilder-0.0.1/pyproject.toml +54 -0
  190. simple_module_pagebuilder-0.0.1/tests/conftest.py +300 -0
  191. simple_module_pagebuilder-0.0.1/tests/test_approval_workflow.py +251 -0
  192. simple_module_pagebuilder-0.0.1/tests/test_auth_gating.py +53 -0
  193. simple_module_pagebuilder-0.0.1/tests/test_csrf.py +73 -0
  194. simple_module_pagebuilder-0.0.1/tests/test_image_processing.py +183 -0
  195. simple_module_pagebuilder-0.0.1/tests/test_media_security.py +118 -0
  196. simple_module_pagebuilder-0.0.1/tests/test_module.py +99 -0
  197. simple_module_pagebuilder-0.0.1/tests/test_pages_api.py +101 -0
  198. simple_module_pagebuilder-0.0.1/tests/test_public_routes.py +84 -0
  199. simple_module_pagebuilder-0.0.1/tests/test_public_view_headers.py +46 -0
  200. simple_module_pagebuilder-0.0.1/tests/test_public_viewer.py +79 -0
  201. simple_module_pagebuilder-0.0.1/tests/test_revision_notes_and_diff.py +265 -0
  202. simple_module_pagebuilder-0.0.1/tests/test_revisions_api.py +101 -0
  203. simple_module_pagebuilder-0.0.1/tests/test_scheduled_publish.py +283 -0
  204. simple_module_pagebuilder-0.0.1/tests/test_seo_meta.py +97 -0
  205. simple_module_pagebuilder-0.0.1/tests/test_site_layout.py +264 -0
  206. simple_module_pagebuilder-0.0.1/tests/test_sitemap_robots.py +94 -0
  207. simple_module_pagebuilder-0.0.1/tests/test_uploads_api.py +221 -0
  208. simple_module_pagebuilder-0.0.1/tsconfig.json +10 -0
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.db
8
+ *.sqlite3
9
+ .env
10
+
11
+ */static/dist/
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_module_pagebuilder
3
+ Version: 0.0.1
4
+ Summary: Drag-and-drop visual page builder for simple_module apps
5
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
6
+ Author-email: Anto Subash <antosubash@live.com>
7
+ License-Expression: MIT
8
+ Keywords: cms,pagebuilder,puck,simple-module
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: pillow>=10.0
11
+ Requires-Dist: pydantic-settings>=2.0
12
+ Requires-Dist: simple-module-core<0.1,>=0.0.25
13
+ Requires-Dist: simple-module-db<0.1,>=0.0.25
14
+ Requires-Dist: simple-module-hosting<0.1,>=0.0.25
15
+ Requires-Dist: sqlalchemy>=2.0
16
+ Requires-Dist: sqlmodel>=0.0.21
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Requires-Dist: simple-module-test<0.1,>=0.0.25; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # simple_module_pagebuilder
24
+
25
+ Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
26
+ apps. Pages are composed from a reusable block library using
27
+ [Puck](https://puckeditor.com) and stored as JSON; published pages are served
28
+ at `/p/{slug}`.
29
+
30
+ ## Features
31
+
32
+ - **Visual editor** — Puck-based drag-and-drop with a block library (heading,
33
+ text, image, button, columns, spacer) and a media picker.
34
+ - **Site layout** — a shared header/footer layout, edited the same way, wrapped
35
+ around every public page.
36
+ - **Media library** — uploads with magic-byte content sniffing, folder
37
+ organisation, and server-generated WebP thumbnails.
38
+ - **Revisions** — every status transition snapshots the page, with a diff view
39
+ and restore-as-draft.
40
+ - **Approval workflow** — edit → submit for review → approve / reject, gated by
41
+ three separate permissions.
42
+ - **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
43
+ poller flips status at the due time.
44
+ - **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
45
+ `/sitemap.xml` and `/robots.txt`.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ uv add simple_module_pagebuilder
51
+ ```
52
+
53
+ Then add it to your host's `pyproject.toml` dependencies. The module is
54
+ discovered at boot through the `simple_module` entry point — no registration
55
+ code required.
56
+
57
+ Its frontend ships inside the wheel. Pull its JS dependencies into your host's
58
+ client app and regenerate the page manifest with:
59
+
60
+ ```bash
61
+ smpy host sync-js-deps --host-client-app=host/client_app
62
+ smpy host gen-pages --host-dir=host/client_app
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ Once installed and migrated, sign in and open `/pagebuilder`:
68
+
69
+ 1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
70
+ from it and stays editable.
71
+ 2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
72
+ media library, which uploads and thumbnails them for you.
73
+ 3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
74
+ review if your role only carries `pagebuilder.edit`. Reviewers work through
75
+ `/pagebuilder/pending`.
76
+ 4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
77
+ now, and the poller flips the status at the due time.
78
+ 5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
79
+ from `/pagebuilder/layout`.
80
+
81
+ Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
82
+ two revisions and restore either one as a draft.
83
+
84
+ ## Migrations
85
+
86
+ This module ships **no** migrations — that is the framework convention. Its
87
+ SQLModel tables are picked up by your host's `build_module_metadata()`, so
88
+ after installing it run:
89
+
90
+ ```bash
91
+ make migration msg="add pagebuilder"
92
+ make migrate
93
+ ```
94
+
95
+ Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
96
+ module can later be removed on its own with
97
+ `alembic downgrade pagebuilder@base`.
98
+
99
+ ## Permissions
100
+
101
+ | Permission | Grants |
102
+ |---|---|
103
+ | `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
104
+ | `pagebuilder.publish` | Publish and unpublish directly |
105
+ | `pagebuilder.approve` | Approve or reject a submitted page |
106
+
107
+ `DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
108
+ `pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
109
+ cumulatively mapped to those permissions.
110
+
111
+ ## Settings
112
+
113
+ All settings use the `SM_PAGEBUILDER_` env prefix.
114
+
115
+ | Setting | Default | Purpose |
116
+ |---|---|---|
117
+ | `public_route_prefix` | `/p` | Where published pages are served |
118
+ | `requires_auth` | `true` | Gate the admin surface behind authentication |
119
+ | `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
120
+ | `media_root` | `var/pagebuilder/media` | Upload storage directory |
121
+ | `media_url_prefix` | `/media/pagebuilder` | Public URL prefix for uploads |
122
+ | `media_max_bytes` | `10485760` | Per-upload size ceiling |
123
+ | `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
124
+ | `media_webp_quality` | `82` | WebP encoder quality |
125
+ | `public_csp` | see `settings.py` | CSP header on public pages |
126
+ | `public_cache_max_age` | `60` | `max-age` on public pages |
127
+ | `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
128
+ | `site_name` | `None` | OpenGraph site name |
129
+ | `sitemap_enabled` | `true` | Serve `/sitemap.xml` |
130
+ | `robots_enabled` | `true` | Serve `/robots.txt` |
131
+ | `scheduler_enabled` | `true` | Run the scheduled publish/unpublish poller |
132
+ | `scheduler_interval_seconds` | `30` | Poll interval |
133
+
134
+ ## Routes
135
+
136
+ **Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
137
+ `/pending`, `/layout`, `/media`.
138
+
139
+ **Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
140
+ revisions and diffs, layout and its revisions, uploads.
141
+
142
+ **Public**: `/p/{slug}`, `/sitemap.xml`, `/robots.txt`.
143
+
144
+ ## Contracts
145
+
146
+ DTOs other modules may depend on are exported from `pagebuilder.contracts`.
147
+ Everything else in the package is internal and may change without a major
148
+ version bump.
149
+
150
+ ## Licence
151
+
152
+ MIT
@@ -0,0 +1,130 @@
1
+ # simple_module_pagebuilder
2
+
3
+ Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
4
+ apps. Pages are composed from a reusable block library using
5
+ [Puck](https://puckeditor.com) and stored as JSON; published pages are served
6
+ at `/p/{slug}`.
7
+
8
+ ## Features
9
+
10
+ - **Visual editor** — Puck-based drag-and-drop with a block library (heading,
11
+ text, image, button, columns, spacer) and a media picker.
12
+ - **Site layout** — a shared header/footer layout, edited the same way, wrapped
13
+ around every public page.
14
+ - **Media library** — uploads with magic-byte content sniffing, folder
15
+ organisation, and server-generated WebP thumbnails.
16
+ - **Revisions** — every status transition snapshots the page, with a diff view
17
+ and restore-as-draft.
18
+ - **Approval workflow** — edit → submit for review → approve / reject, gated by
19
+ three separate permissions.
20
+ - **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
21
+ poller flips status at the due time.
22
+ - **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
23
+ `/sitemap.xml` and `/robots.txt`.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ uv add simple_module_pagebuilder
29
+ ```
30
+
31
+ Then add it to your host's `pyproject.toml` dependencies. The module is
32
+ discovered at boot through the `simple_module` entry point — no registration
33
+ code required.
34
+
35
+ Its frontend ships inside the wheel. Pull its JS dependencies into your host's
36
+ client app and regenerate the page manifest with:
37
+
38
+ ```bash
39
+ smpy host sync-js-deps --host-client-app=host/client_app
40
+ smpy host gen-pages --host-dir=host/client_app
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Once installed and migrated, sign in and open `/pagebuilder`:
46
+
47
+ 1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
48
+ from it and stays editable.
49
+ 2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
50
+ media library, which uploads and thumbnails them for you.
51
+ 3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
52
+ review if your role only carries `pagebuilder.edit`. Reviewers work through
53
+ `/pagebuilder/pending`.
54
+ 4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
55
+ now, and the poller flips the status at the due time.
56
+ 5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
57
+ from `/pagebuilder/layout`.
58
+
59
+ Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
60
+ two revisions and restore either one as a draft.
61
+
62
+ ## Migrations
63
+
64
+ This module ships **no** migrations — that is the framework convention. Its
65
+ SQLModel tables are picked up by your host's `build_module_metadata()`, so
66
+ after installing it run:
67
+
68
+ ```bash
69
+ make migration msg="add pagebuilder"
70
+ make migrate
71
+ ```
72
+
73
+ Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
74
+ module can later be removed on its own with
75
+ `alembic downgrade pagebuilder@base`.
76
+
77
+ ## Permissions
78
+
79
+ | Permission | Grants |
80
+ |---|---|
81
+ | `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
82
+ | `pagebuilder.publish` | Publish and unpublish directly |
83
+ | `pagebuilder.approve` | Approve or reject a submitted page |
84
+
85
+ `DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
86
+ `pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
87
+ cumulatively mapped to those permissions.
88
+
89
+ ## Settings
90
+
91
+ All settings use the `SM_PAGEBUILDER_` env prefix.
92
+
93
+ | Setting | Default | Purpose |
94
+ |---|---|---|
95
+ | `public_route_prefix` | `/p` | Where published pages are served |
96
+ | `requires_auth` | `true` | Gate the admin surface behind authentication |
97
+ | `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
98
+ | `media_root` | `var/pagebuilder/media` | Upload storage directory |
99
+ | `media_url_prefix` | `/media/pagebuilder` | Public URL prefix for uploads |
100
+ | `media_max_bytes` | `10485760` | Per-upload size ceiling |
101
+ | `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
102
+ | `media_webp_quality` | `82` | WebP encoder quality |
103
+ | `public_csp` | see `settings.py` | CSP header on public pages |
104
+ | `public_cache_max_age` | `60` | `max-age` on public pages |
105
+ | `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
106
+ | `site_name` | `None` | OpenGraph site name |
107
+ | `sitemap_enabled` | `true` | Serve `/sitemap.xml` |
108
+ | `robots_enabled` | `true` | Serve `/robots.txt` |
109
+ | `scheduler_enabled` | `true` | Run the scheduled publish/unpublish poller |
110
+ | `scheduler_interval_seconds` | `30` | Poll interval |
111
+
112
+ ## Routes
113
+
114
+ **Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
115
+ `/pending`, `/layout`, `/media`.
116
+
117
+ **Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
118
+ revisions and diffs, layout and its revisions, uploads.
119
+
120
+ **Public**: `/p/{slug}`, `/sitemap.xml`, `/robots.txt`.
121
+
122
+ ## Contracts
123
+
124
+ DTOs other modules may depend on are exported from `pagebuilder.contracts`.
125
+ Everything else in the package is internal and may change without a major
126
+ version bump.
127
+
128
+ ## Licence
129
+
130
+ MIT
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@simple-module-py/pagebuilder",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "description": "Frontend assets for the PageBuilder module",
6
+ "peerDependencies": {
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "@inertiajs/react": "^2.0.0",
10
+ "@simple-module-py/ui": "*"
11
+ },
12
+ "devDependencies": {
13
+ "@simple-module-py/tsconfig": "*"
14
+ },
15
+ "dependencies": {
16
+ "@puckeditor/core": "^0.22.4"
17
+ }
18
+ }
@@ -0,0 +1,107 @@
1
+ import type React from 'react';
2
+ import type { RevisionDiff } from '../utils/api';
3
+
4
+ interface Props {
5
+ diff: RevisionDiff;
6
+ }
7
+
8
+ function renderValue(value: unknown): string {
9
+ if (value === null || value === undefined) return '∅';
10
+ if (typeof value === 'string') return value || '""';
11
+ return JSON.stringify(value);
12
+ }
13
+
14
+ /**
15
+ * Compact, scannable diff: metadata table (only changed fields) plus
16
+ * three lists of blocks (added / removed / changed). Designed to fit
17
+ * inside the editor's history panel without a separate page.
18
+ */
19
+ export function DiffSummary({ diff }: Props): React.JSX.Element {
20
+ const { metadata, blocks } = diff;
21
+ const metaKeys = Object.keys(metadata);
22
+ const totalChanges =
23
+ metaKeys.length + blocks.added.length + blocks.removed.length + blocks.changed.length;
24
+
25
+ if (totalChanges === 0) {
26
+ return (
27
+ <div
28
+ className="mt-3 rounded border border-gray-200 bg-white p-3 text-xs text-gray-500"
29
+ data-testid="diff-summary"
30
+ >
31
+ No changes between these revisions.
32
+ </div>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <div
38
+ className="mt-3 rounded border border-gray-200 bg-white p-3 text-xs space-y-2"
39
+ data-testid="diff-summary"
40
+ >
41
+ {metaKeys.length > 0 && (
42
+ <div>
43
+ <div className="font-semibold text-gray-700">Metadata</div>
44
+ <ul className="ml-3 list-disc">
45
+ {metaKeys.map((key) => (
46
+ <li key={key}>
47
+ <span className="font-mono">{key}</span>:{' '}
48
+ <span className="text-red-600 line-through">
49
+ {renderValue(metadata[key].before)}
50
+ </span>{' '}
51
+ → <span className="text-green-700">{renderValue(metadata[key].after)}</span>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ </div>
56
+ )}
57
+ {blocks.added.length > 0 && (
58
+ <div>
59
+ <div className="font-semibold text-green-700">Added ({blocks.added.length})</div>
60
+ <ul className="ml-3 list-disc">
61
+ {blocks.added.map((b) => (
62
+ <li key={b.id}>
63
+ <span className="font-mono">{b.type ?? '?'}</span>{' '}
64
+ <span className="text-gray-500">({b.id})</span>
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ </div>
69
+ )}
70
+ {blocks.removed.length > 0 && (
71
+ <div>
72
+ <div className="font-semibold text-red-700">Removed ({blocks.removed.length})</div>
73
+ <ul className="ml-3 list-disc">
74
+ {blocks.removed.map((b) => (
75
+ <li key={b.id}>
76
+ <span className="font-mono">{b.type ?? '?'}</span>{' '}
77
+ <span className="text-gray-500">({b.id})</span>
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ </div>
82
+ )}
83
+ {blocks.changed.length > 0 && (
84
+ <div>
85
+ <div className="font-semibold text-amber-700">Changed ({blocks.changed.length})</div>
86
+ <ul className="ml-3 list-disc">
87
+ {blocks.changed.map((b) => (
88
+ <li key={b.id}>
89
+ <span className="font-mono">{b.type ?? '?'}</span>{' '}
90
+ <span className="text-gray-500">({b.id})</span>
91
+ {b.type_before && (
92
+ <span className="text-gray-500">
93
+ {' '}
94
+ — was <span className="font-mono">{b.type_before}</span>
95
+ </span>
96
+ )}
97
+ {b.fields.length > 0 && (
98
+ <span className="text-gray-700">: {b.fields.join(', ')}</span>
99
+ )}
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,34 @@
1
+ import type { PageStatus } from '../utils/api';
2
+
3
+ interface Props {
4
+ status: PageStatus;
5
+ publishAt: string | null;
6
+ unpublishAt: string | null;
7
+ }
8
+
9
+ /**
10
+ * Inline pill rendered next to ``StatusBadge`` when a page has a
11
+ * pending scheduler-driven flip. Drafts surface the upcoming publish
12
+ * time; published pages surface the upcoming unpublish time. Pages
13
+ * with no schedule for their current state render nothing — so callers
14
+ * can drop this in unconditionally.
15
+ */
16
+ export function ScheduledBadge({ status, publishAt, unpublishAt }: Props) {
17
+ const target =
18
+ status === 'draft' && publishAt
19
+ ? { kind: 'publish' as const, at: publishAt }
20
+ : status === 'published' && unpublishAt
21
+ ? { kind: 'unpublish' as const, at: unpublishAt }
22
+ : null;
23
+ if (!target) return null;
24
+ const label = target.kind === 'publish' ? 'Scheduled to publish' : 'Scheduled to unpublish';
25
+ return (
26
+ <span
27
+ className="ml-2 inline-block text-xs font-medium px-2 py-1 rounded bg-blue-100 text-blue-800"
28
+ title={new Date(target.at).toLocaleString()}
29
+ data-testid="scheduled-badge"
30
+ >
31
+ {label} {new Date(target.at).toLocaleString()}
32
+ </span>
33
+ );
34
+ }
@@ -0,0 +1,21 @@
1
+ import { Badge } from '@simple-module-py/ui/components/ui/badge';
2
+
3
+ import type { PageStatus } from '../utils/api';
4
+
5
+ // The e2e suite asserts on this text verbatim — getByText('published',
6
+ // { exact: true }) — so these labels stay lowercase and unabbreviated.
7
+ const LABELS: Record<PageStatus, string> = {
8
+ published: 'published',
9
+ submitted_for_review: 'pending review',
10
+ draft: 'draft',
11
+ };
12
+
13
+ const VARIANTS: Record<PageStatus, 'default' | 'secondary' | 'outline'> = {
14
+ published: 'default',
15
+ submitted_for_review: 'secondary',
16
+ draft: 'outline',
17
+ };
18
+
19
+ export function StatusBadge({ status }: { status: PageStatus }) {
20
+ return <Badge variant={VARIANTS[status]}>{LABELS[status]}</Badge>;
21
+ }
@@ -0,0 +1,56 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ type AnyBlock,
5
+ declareBuiltInBlocks,
6
+ registeredBlocks,
7
+ registerPuckBlocks,
8
+ registryVersion,
9
+ resetBlockRegistry,
10
+ } from './blockRegistry';
11
+
12
+ const stub = { render: () => null } as unknown as AnyBlock;
13
+
14
+ describe('block registry', () => {
15
+ beforeEach(() => {
16
+ declareBuiltInBlocks(['Heading', 'Text']);
17
+ resetBlockRegistry();
18
+ });
19
+
20
+ it('collects a registration', () => {
21
+ registerPuckBlocks({ blocks: { Widget: stub } });
22
+ expect(registeredBlocks().flatMap((r) => Object.keys(r.blocks))).toContain('Widget');
23
+ });
24
+
25
+ it('bumps the version on each registration', () => {
26
+ const before = registryVersion();
27
+ registerPuckBlocks({ blocks: { Widget: stub } });
28
+ expect(registryVersion()).toBeGreaterThan(before);
29
+ });
30
+
31
+ it('refuses to shadow a block pagebuilder already ships', () => {
32
+ // Block names are the `type` stored in page content, so a duplicate would
33
+ // make two blocks each render the other's saved data.
34
+ expect(() => registerPuckBlocks({ blocks: { Heading: stub } })).toThrow(/Heading/);
35
+ });
36
+
37
+ it('refuses to register the same name twice', () => {
38
+ registerPuckBlocks({ blocks: { Widget: stub } });
39
+ expect(() => registerPuckBlocks({ blocks: { Widget: stub } })).toThrow(/Widget/);
40
+ });
41
+
42
+ it('keeps the built-ins reserved across a reset', () => {
43
+ resetBlockRegistry();
44
+ expect(() => registerPuckBlocks({ blocks: { Text: stub } })).toThrow(/Text/);
45
+ });
46
+
47
+ it('carries the category through', () => {
48
+ registerPuckBlocks({ blocks: { Widget: stub }, category: { key: 'feeds', title: 'Feeds' } });
49
+ expect(registeredBlocks()[0].category).toEqual({ key: 'feeds', title: 'Feeds' });
50
+ });
51
+
52
+ it('defaults to page-only, not layout', () => {
53
+ registerPuckBlocks({ blocks: { Widget: stub } });
54
+ expect(registeredBlocks()[0].layout).toBeUndefined();
55
+ });
56
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Lets another module contribute Puck blocks without pagebuilder importing it.
3
+ *
4
+ * The framework's registries — menu, permissions, public routes, design packs —
5
+ * are all backend. This is the frontend equivalent, and the reason it exists is
6
+ * dependency direction: the news module knows about pagebuilder, so pagebuilder
7
+ * must not know about news.
8
+ *
9
+ * Registration is a side effect of importing a module's `puck-blocks.ts`. The
10
+ * host imports them all eagerly at app start (see `host/client_app/blocks.ts`),
11
+ * which is what guarantees the registry is populated before the first render.
12
+ */
13
+
14
+ import type { ComponentConfig } from '@puckeditor/core';
15
+
16
+ // Puck's ComponentConfig is invariant in its props, so a registry that holds
17
+ // configs for unrelated prop shapes cannot be typed precisely. The runtime
18
+ // shape is what matters here; each module keeps its own widget types honest.
19
+ // biome-ignore lint/suspicious/noExplicitAny: a heterogeneous registry has no single prop type
20
+ export type AnyBlock = ComponentConfig<any>;
21
+
22
+ export interface BlockRegistration {
23
+ /** Block name → config. The name is the `type` stored in page content. */
24
+ blocks: Record<string, AnyBlock>;
25
+ /** Palette group. Blocks with no category land in Puck's "Other". */
26
+ category?: { key: string; title: string };
27
+ /** Also offer these in the site-layout palette. Default false. */
28
+ layout?: boolean;
29
+ }
30
+
31
+ let registrations: BlockRegistration[] = [];
32
+ let takenNames = new Set<string>();
33
+ let builtIns: readonly string[] = [];
34
+ let version = 0;
35
+
36
+ /**
37
+ * Declare the names pagebuilder itself ships, so a module cannot shadow one.
38
+ *
39
+ * Called once by puckConfig at import time rather than hardcoded here, which
40
+ * would be a second list to keep in sync.
41
+ */
42
+ export function declareBuiltInBlocks(names: readonly string[]): void {
43
+ builtIns = names;
44
+ for (const name of names) takenNames.add(name);
45
+ }
46
+
47
+ export function registerPuckBlocks(registration: BlockRegistration): void {
48
+ for (const name of Object.keys(registration.blocks)) {
49
+ if (takenNames.has(name)) {
50
+ throw new Error(
51
+ `Puck block "${name}" is already registered. Block names are the \`type\` ` +
52
+ 'stored in page content, so two blocks sharing one would each render ' +
53
+ "the other's saved data.",
54
+ );
55
+ }
56
+ takenNames.add(name);
57
+ }
58
+ registrations = [...registrations, registration];
59
+ version += 1;
60
+ }
61
+
62
+ export function registeredBlocks(): readonly BlockRegistration[] {
63
+ return registrations;
64
+ }
65
+
66
+ /**
67
+ * Bumped on every registration. The config builders memoise on this rather
68
+ * than caching unconditionally, so a registration that lands after the first
69
+ * read is not silently dropped.
70
+ */
71
+ export function registryVersion(): number {
72
+ return version;
73
+ }
74
+
75
+ /** Test-only: drop every registration, keeping the declared built-ins. */
76
+ export function resetBlockRegistry(): void {
77
+ registrations = [];
78
+ takenNames = new Set(builtIns);
79
+ version += 1;
80
+ }