simple-module-pagebuilder 0.0.4__tar.gz → 0.0.6__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 (270) hide show
  1. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/PKG-INFO +2 -2
  2. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/package.json +1 -1
  3. simple_module_pagebuilder-0.0.6/pagebuilder/board.py +137 -0
  4. simple_module_pagebuilder-0.0.6/pagebuilder/components/ConfirmDialog.tsx +138 -0
  5. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/DiffSummary.tsx +14 -13
  6. simple_module_pagebuilder-0.0.6/pagebuilder/components/NewPageDialog.tsx +218 -0
  7. simple_module_pagebuilder-0.0.6/pagebuilder/components/NoteDialog.tsx +116 -0
  8. simple_module_pagebuilder-0.0.6/pagebuilder/components/PageBoard.tsx +123 -0
  9. simple_module_pagebuilder-0.0.6/pagebuilder/components/PageBoardCard.tsx +143 -0
  10. simple_module_pagebuilder-0.0.6/pagebuilder/components/PageListFilters.tsx +104 -0
  11. simple_module_pagebuilder-0.0.6/pagebuilder/components/blocks/Image.test.tsx +171 -0
  12. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Image.tsx +74 -9
  13. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/BlockOutline.tsx +76 -0
  14. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/NarrowCanvas.tsx +59 -0
  15. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageActions.tsx +103 -0
  16. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/PageEditorToolbar.tsx +54 -16
  17. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageInspectorDrawer.tsx +136 -0
  18. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/PageSettingsPanel.tsx +147 -0
  19. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +36 -19
  20. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/editor/SchedulePanel.tsx +38 -21
  21. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/SeoPreview.tsx +93 -0
  22. simple_module_pagebuilder-0.0.6/pagebuilder/components/editor/SeoSettingsPanel.tsx +188 -0
  23. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaFilters.tsx +4 -4
  24. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaFolderSidebar.tsx +4 -4
  25. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaGrid.tsx +22 -9
  26. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaUploadQueue.tsx +10 -7
  27. simple_module_pagebuilder-0.0.6/pagebuilder/components/media/MediaUsageList.tsx +58 -0
  28. simple_module_pagebuilder-0.0.6/pagebuilder/components/widgets/divider-widget.test.tsx +48 -0
  29. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/divider-widget.tsx +9 -2
  30. simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/__init__.py +74 -0
  31. simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_layout.py +52 -0
  32. simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_media.py +95 -0
  33. simple_module_pagebuilder-0.0.4/pagebuilder/contracts/schemas.py → simple_module_pagebuilder-0.0.6/pagebuilder/contracts/schemas/_pages.py +43 -92
  34. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/__init__.py +10 -1
  35. simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/api/media_detail.py +102 -0
  36. simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/api/pages.py +149 -0
  37. simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/public_views.py +162 -0
  38. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/seo.py +9 -1
  39. simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/views.py +251 -0
  40. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useAutosave.ts +14 -1
  41. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useEditorForm.ts +44 -0
  42. simple_module_pagebuilder-0.0.6/pagebuilder/hooks/useIsNarrow.ts +45 -0
  43. simple_module_pagebuilder-0.0.6/pagebuilder/hooks/useMediaDetail.ts +78 -0
  44. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageRevisions.ts +2 -3
  45. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageWorkflow.ts +19 -23
  46. simple_module_pagebuilder-0.0.6/pagebuilder/hooks/usePendingDialog.ts +43 -0
  47. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_service.py +3 -0
  48. simple_module_pagebuilder-0.0.6/pagebuilder/media_usage.py +101 -0
  49. simple_module_pagebuilder-0.0.6/pagebuilder/models/__init__.py +35 -0
  50. simple_module_pagebuilder-0.0.6/pagebuilder/models/_base.py +14 -0
  51. simple_module_pagebuilder-0.0.6/pagebuilder/models/_layout.py +45 -0
  52. simple_module_pagebuilder-0.0.6/pagebuilder/models/_media.py +58 -0
  53. simple_module_pagebuilder-0.0.4/pagebuilder/models.py → simple_module_pagebuilder-0.0.6/pagebuilder/models/_page.py +60 -71
  54. simple_module_pagebuilder-0.0.6/pagebuilder/models/_redirect.py +28 -0
  55. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/module.py +13 -7
  56. simple_module_pagebuilder-0.0.6/pagebuilder/pages/LayoutEditor.tsx +225 -0
  57. simple_module_pagebuilder-0.0.6/pagebuilder/pages/MediaDetail.tsx +185 -0
  58. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/MediaLibrary.tsx +17 -13
  59. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PageEditor.tsx +59 -52
  60. simple_module_pagebuilder-0.0.6/pagebuilder/pages/PageList.tsx +291 -0
  61. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PendingReview.tsx +37 -43
  62. simple_module_pagebuilder-0.0.6/pagebuilder/pages/Trash.tsx +150 -0
  63. simple_module_pagebuilder-0.0.6/pagebuilder/public_claims.py +73 -0
  64. simple_module_pagebuilder-0.0.6/pagebuilder/redirects.py +60 -0
  65. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/__init__.py +94 -30
  66. simple_module_pagebuilder-0.0.6/pagebuilder/service/_trash.py +166 -0
  67. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_workflow.py +5 -1
  68. simple_module_pagebuilder-0.0.6/pagebuilder/utils/blockOutline.test.ts +139 -0
  69. simple_module_pagebuilder-0.0.6/pagebuilder/utils/blockOutline.ts +130 -0
  70. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/editorSnapshot.ts +13 -0
  71. simple_module_pagebuilder-0.0.6/pagebuilder/utils/measure.ts +54 -0
  72. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/mediaApi.ts +17 -0
  73. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/pagesApi.ts +18 -20
  74. simple_module_pagebuilder-0.0.6/pagebuilder/utils/request.test.ts +106 -0
  75. simple_module_pagebuilder-0.0.6/pagebuilder/utils/request.ts +120 -0
  76. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/types.ts +37 -0
  77. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pyproject.toml +5 -1
  78. simple_module_pagebuilder-0.0.6/tests/db_fixture.py +37 -0
  79. simple_module_pagebuilder-0.0.6/tests/test_admin_list_view.py +82 -0
  80. simple_module_pagebuilder-0.0.6/tests/test_draft_preview.py +92 -0
  81. simple_module_pagebuilder-0.0.6/tests/test_media_detail.py +175 -0
  82. simple_module_pagebuilder-0.0.6/tests/test_media_usage_escaping.py +58 -0
  83. simple_module_pagebuilder-0.0.6/tests/test_page_templates.py +164 -0
  84. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_pages_api.py +1 -1
  85. simple_module_pagebuilder-0.0.6/tests/test_pages_search.py +98 -0
  86. simple_module_pagebuilder-0.0.6/tests/test_public_claims.py +158 -0
  87. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_view_headers.py +1 -1
  88. simple_module_pagebuilder-0.0.6/tests/test_redirects.py +144 -0
  89. simple_module_pagebuilder-0.0.6/tests/test_trash.py +177 -0
  90. simple_module_pagebuilder-0.0.6/tests/test_trash_preconditions.py +93 -0
  91. simple_module_pagebuilder-0.0.4/pagebuilder/components/blocks/Image.test.tsx +0 -94
  92. simple_module_pagebuilder-0.0.4/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -119
  93. simple_module_pagebuilder-0.0.4/pagebuilder/endpoints/api/pages.py +0 -85
  94. simple_module_pagebuilder-0.0.4/pagebuilder/endpoints/views.py +0 -228
  95. simple_module_pagebuilder-0.0.4/pagebuilder/pages/LayoutEditor.tsx +0 -183
  96. simple_module_pagebuilder-0.0.4/pagebuilder/pages/PageList.tsx +0 -129
  97. simple_module_pagebuilder-0.0.4/pagebuilder/utils/request.ts +0 -52
  98. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/.gitignore +0 -0
  99. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/README.md +0 -0
  100. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/__init__.py +0 -0
  101. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/ScheduledBadge.tsx +0 -0
  102. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/StatusBadge.tsx +0 -0
  103. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blockRegistry.test.ts +0 -0
  104. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blockRegistry.ts +0 -0
  105. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Button.tsx +0 -0
  106. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Columns.tsx +0 -0
  107. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Heading.tsx +0 -0
  108. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
  109. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Spacer.tsx +0 -0
  110. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/blocks/Text.tsx +0 -0
  111. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
  112. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/lazyBlock.tsx +0 -0
  113. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/MediaHeader.tsx +0 -0
  114. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/media/types.ts +0 -0
  115. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/migrateContent.test.ts +0 -0
  116. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/migrateContent.ts +0 -0
  117. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/puckConfig.test.tsx +0 -0
  118. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/puckConfig.tsx +0 -0
  119. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
  120. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgetCatalog.ts +0 -0
  121. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
  122. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
  123. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
  124. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
  125. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
  126. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
  127. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/background-media.tsx +0 -0
  128. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/content-media.tsx +0 -0
  129. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
  130. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
  131. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
  132. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
  133. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/image-srcset.ts +0 -0
  134. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/index.ts +0 -0
  135. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
  136. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
  137. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
  138. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
  139. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
  140. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
  141. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
  142. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
  143. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
  144. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
  145. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
  146. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
  147. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
  148. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
  149. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
  150. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/column-widget.tsx +0 -0
  151. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
  152. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
  153. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
  154. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/container-widget.tsx +0 -0
  155. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
  156. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
  157. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/faq-render.tsx +0 -0
  158. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
  159. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
  160. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
  161. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
  162. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
  163. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
  164. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
  165. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
  166. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
  167. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/hero-widget-props.ts +0 -0
  168. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/hero-widget.tsx +0 -0
  169. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/html-widget.tsx +0 -0
  170. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
  171. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
  172. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/list-widget.tsx +0 -0
  173. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
  174. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
  175. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-layout.ts +0 -0
  176. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-render.tsx +0 -0
  177. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
  178. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/media-object-widget.tsx +0 -0
  179. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
  180. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
  181. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
  182. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
  183. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/row-widget.tsx +0 -0
  184. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
  185. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
  186. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
  187. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
  188. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
  189. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
  190. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
  191. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
  192. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
  193. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
  194. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/table-widget.tsx +0 -0
  195. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
  196. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
  197. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
  198. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
  199. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
  200. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
  201. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
  202. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/video-widget.tsx +0 -0
  203. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
  204. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/contracts/__init__.py +0 -0
  205. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/contracts/events.py +0 -0
  206. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/blocks-to-html.test.ts +0 -0
  207. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/blocks-to-html.ts +0 -0
  208. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/index.ts +0 -0
  209. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/conversion/types.ts +0 -0
  210. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/deps.py +0 -0
  211. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/diff.py +0 -0
  212. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/__init__.py +0 -0
  213. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/_deps.py +0 -0
  214. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/layout.py +0 -0
  215. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/revisions.py +0 -0
  216. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/uploads.py +0 -0
  217. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/endpoints/api/workflow.py +0 -0
  218. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/checkbox-field.tsx +0 -0
  219. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
  220. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
  221. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/index.ts +0 -0
  222. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/file-picker/types.ts +0 -0
  223. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/index.ts +0 -0
  224. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/fields/media-adapter.ts +0 -0
  225. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/useMediaUploads.ts +0 -0
  226. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/hooks/usePageSchedule.ts +0 -0
  227. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/layout_service.py +0 -0
  228. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_files.py +0 -0
  229. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_images.py +0 -0
  230. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/media_queries.py +0 -0
  231. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/pages/PublicPage.tsx +0 -0
  232. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/permissions.py +0 -0
  233. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/py.typed +0 -0
  234. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/security.py +0 -0
  235. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_common.py +0 -0
  236. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/service/_revisions.py +0 -0
  237. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/services.py +0 -0
  238. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/settings.py +0 -0
  239. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/static/widgets-base.css +0 -0
  240. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/apply.ts +0 -0
  241. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/extract.ts +0 -0
  242. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/index.ts +0 -0
  243. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/translatable-fields.ts +0 -0
  244. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/translation/translation.test.ts +0 -0
  245. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/api.ts +0 -0
  246. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/datetime.ts +0 -0
  247. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/decorative-image.ts +0 -0
  248. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/markdown.ts +0 -0
  249. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/mediaFormat.ts +0 -0
  250. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/parse-list.ts +0 -0
  251. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/slugify.ts +0 -0
  252. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/pagebuilder/utils/widgetUtils.ts +0 -0
  253. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/conftest.py +0 -0
  254. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_approval_workflow.py +0 -0
  255. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_auth_gating.py +0 -0
  256. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_csrf.py +0 -0
  257. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_image_processing.py +0 -0
  258. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_media_files.py +0 -0
  259. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_media_security.py +0 -0
  260. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_module.py +0 -0
  261. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_routes.py +0 -0
  262. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_public_viewer.py +0 -0
  263. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_revision_notes_and_diff.py +0 -0
  264. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_revisions_api.py +0 -0
  265. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_scheduled_publish.py +0 -0
  266. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_seo_meta.py +0 -0
  267. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_site_layout.py +0 -0
  268. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_sitemap_robots.py +0 -0
  269. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tests/test_uploads_api.py +0 -0
  270. {simple_module_pagebuilder-0.0.4 → simple_module_pagebuilder-0.0.6}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simple_module_pagebuilder
3
- Version: 0.0.4
3
+ Version: 0.0.6
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>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-module-py/pagebuilder",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the PageBuilder module",
6
6
  "peerDependencies": {
@@ -0,0 +1,137 @@
1
+ """Stage-grouped view of the page list — the board behind ``?view=board``.
2
+
3
+ The pipeline is two-state: a page is a draft or it is published. "Scheduled" is
4
+ not a third state, it is a draft carrying a future ``publish_at`` that will flip
5
+ itself; splitting it into its own column is a *display* decision, made here
6
+ rather than in the model precisely so nothing downstream has to learn a status
7
+ that does not exist.
8
+
9
+ Each column is capped and reports its own total, so a site with 400 published
10
+ pages renders three short columns and says how much it is not showing, instead
11
+ of paginating a board — which nobody can read anyway.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from datetime import UTC, datetime
18
+
19
+ from sqlalchemy import func, select
20
+ from sqlalchemy.ext.asyncio import AsyncSession
21
+
22
+ from pagebuilder.models import NOT_TRASHED, Page, PageStatus
23
+
24
+ DRAFT = "draft"
25
+ SCHEDULED = "scheduled"
26
+ IN_REVIEW = "in_review"
27
+ PUBLISHED = "published"
28
+
29
+ #: Rows shown per column before the "N more" line takes over.
30
+ PER_STAGE = 8
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Stage:
35
+ key: str
36
+ label: str
37
+ items: list[Page]
38
+ total: int
39
+
40
+
41
+ def _search_filter(search: str | None):
42
+ """Reuses the escaping rule from ``list_pages`` — a title containing ``%``
43
+ is searched for literally rather than matching everything."""
44
+ if not search or not search.strip():
45
+ return None
46
+ term = search.strip().translate(str.maketrans({"%": r"\%", "_": r"\_", "\\": "\\\\"}))
47
+ pattern = f"%{term}%"
48
+ return Page.title.ilike(pattern, escape="\\") | Page.slug.ilike(pattern, escape="\\")
49
+
50
+
51
+ def _conditions(stage: str, now: datetime) -> list:
52
+ """What puts a page in this column.
53
+
54
+ Templates are excluded from every column. They are ordinary pages, but they
55
+ are not *in* the pipeline — nobody publishes a template — so listing them
56
+ under Draft would put permanent residents in a column that is meant to read
57
+ as a queue. They stay reachable in the list view, and in the New page
58
+ dialog, which is where they are actually used.
59
+
60
+ Draft and scheduled are deliberately complementary halves of
61
+ ``status == DRAFT``: a page with no ``publish_at``, or one whose date has
62
+ already passed, is simply a draft. Without the second half a scheduled page
63
+ whose moment arrived but whose job has not yet run would vanish from the
64
+ board entirely.
65
+ """
66
+ not_template = Page.is_template.is_(False)
67
+ if stage == DRAFT:
68
+ return [
69
+ NOT_TRASHED,
70
+ not_template,
71
+ Page.status == PageStatus.DRAFT,
72
+ (Page.publish_at.is_(None)) | (Page.publish_at <= now),
73
+ ]
74
+ if stage == SCHEDULED:
75
+ return [NOT_TRASHED, not_template, Page.status == PageStatus.DRAFT, Page.publish_at > now]
76
+ if stage == IN_REVIEW:
77
+ return [NOT_TRASHED, not_template, Page.status == PageStatus.SUBMITTED_FOR_REVIEW]
78
+ return [NOT_TRASHED, not_template, Page.status == PageStatus.PUBLISHED]
79
+
80
+
81
+ _ORDER = [
82
+ (DRAFT, "Draft"),
83
+ (SCHEDULED, "Scheduled"),
84
+ (IN_REVIEW, "In review"),
85
+ (PUBLISHED, "Published"),
86
+ ]
87
+
88
+
89
+ async def load(
90
+ db: AsyncSession, *, search: str | None = None, per_stage: int = PER_STAGE
91
+ ) -> list[Stage]:
92
+ """Every column, in pipeline order.
93
+
94
+ ``In review`` is returned even when empty; the caller drops it, because
95
+ whether the approval workflow is in use is a question about the site rather
96
+ than about this query.
97
+ """
98
+ now = datetime.now(UTC)
99
+ search_clause = _search_filter(search)
100
+
101
+ stages: list[Stage] = []
102
+ for key, label in _ORDER:
103
+ clauses = _conditions(key, now)
104
+ if search_clause is not None:
105
+ clauses = [*clauses, search_clause]
106
+
107
+ total = await db.scalar(select(func.count()).select_from(Page).where(*clauses))
108
+ # Scheduled sorts by when it fires — the next one to go live is the one
109
+ # worth seeing first. Every other column is newest-touched first.
110
+ order = Page.publish_at.asc() if key == SCHEDULED else Page.id.desc()
111
+ rows = await db.execute(select(Page).where(*clauses).order_by(order).limit(per_stage))
112
+ stages.append(
113
+ Stage(key=key, label=label, items=list(rows.scalars().all()), total=int(total or 0))
114
+ )
115
+ return stages
116
+
117
+
118
+ def to_payload(stages: list[Stage], serialize) -> list[dict]:
119
+ """Stages as JSON for the Inertia page.
120
+
121
+ ``serialize`` turns one ``Page`` into a dict — passed in rather than
122
+ imported so this module keeps knowing nothing about the schemas.
123
+
124
+ ``In review`` is dropped when empty: an empty column would advertise an
125
+ approval workflow the site may not use, while a non-empty one must never be
126
+ hidden or its pages are stranded with no route to them.
127
+ """
128
+ return [
129
+ {
130
+ "key": stage.key,
131
+ "label": stage.label,
132
+ "total": stage.total,
133
+ "items": [serialize(item) for item in stage.items],
134
+ }
135
+ for stage in stages
136
+ if stage.key != IN_REVIEW or stage.total
137
+ ]
@@ -0,0 +1,138 @@
1
+ import {
2
+ AlertDialog,
3
+ AlertDialogAction,
4
+ AlertDialogCancel,
5
+ AlertDialogContent,
6
+ AlertDialogDescription,
7
+ AlertDialogFooter,
8
+ AlertDialogHeader,
9
+ AlertDialogTitle,
10
+ AlertDialogTrigger,
11
+ } from '@simple-module-py/ui/components/ui/alert-dialog';
12
+ import { Input } from '@simple-module-py/ui/components/ui/input';
13
+ import { Label } from '@simple-module-py/ui/components/ui/label';
14
+ import { type ReactNode, useState } from 'react';
15
+
16
+ import { usePendingDialog } from '../hooks/usePendingDialog';
17
+
18
+ /** How much a confirmation is allowed to cost the person clicking it.
19
+ *
20
+ * A deliberate scale rather than a style choice: friction has to track blast
21
+ * radius, or people learn to click through the cheap confirmations and then
22
+ * click through the expensive one exactly the same way.
23
+ *
24
+ * - `low` — reversible; nothing is public. A plain button.
25
+ * - `medium` — this screen cannot undo it, but nothing live is lost: either it
26
+ * changes the public site while the content survives (unpublish), or it
27
+ * removes something for good that nothing references (an unused asset).
28
+ * - `high` — irreversible past the retention window. Requires typing a phrase.
29
+ *
30
+ * The axis is how hard the result is to walk back, not whether the public site
31
+ * changes. An irreversible delete does not get `low` because it happens to be
32
+ * invisible from outside.
33
+ */
34
+ export type ConfirmLevel = 'low' | 'medium' | 'high';
35
+
36
+ const CONFIRM_PHRASE_INPUT = 'confirm-dialog-phrase';
37
+
38
+ /**
39
+ * Confirm a consequential action without handing the page to the browser.
40
+ *
41
+ * `confirm()` blocks the event loop, cannot say which row it is asking about
42
+ * beyond a bare sentence, is unstyleable, and — the reason it matters here —
43
+ * gives the failure nowhere to go: the call site learns the user said yes and
44
+ * then swallows whatever the request answered. This keeps the dialog open
45
+ * while the work runs and shows the error inside it, so a delete that comes
46
+ * back 403 is visible instead of looking like it worked.
47
+ */
48
+ export function ConfirmDialog({
49
+ trigger,
50
+ title,
51
+ description,
52
+ confirmLabel = 'Confirm',
53
+ destructive = false,
54
+ level,
55
+ confirmPhrase,
56
+ children,
57
+ onConfirm,
58
+ }: {
59
+ trigger: ReactNode;
60
+ title: string;
61
+ description: ReactNode;
62
+ confirmLabel?: string;
63
+ destructive?: boolean;
64
+ /** Blast radius. Defaults from `destructive` so existing call sites keep
65
+ * the behaviour they had before the scale existed. */
66
+ level?: ConfirmLevel;
67
+ /** At `high`, the phrase that must be typed before the action unlocks.
68
+ * Ignored at the other levels. */
69
+ confirmPhrase?: string;
70
+ /** Extra controls — a reassignment target, a checkbox — above the buttons. */
71
+ children?: ReactNode;
72
+ /** Rejecting keeps the dialog open and surfaces the message. */
73
+ onConfirm: () => Promise<unknown>;
74
+ }) {
75
+ const [typed, setTyped] = useState('');
76
+
77
+ const resolvedLevel: ConfirmLevel = level ?? (destructive ? 'medium' : 'low');
78
+ // Compared exactly, case included. A forgiving match would defeat the point:
79
+ // the delay is the safeguard, and it only works while the phrase is actually
80
+ // being read off the screen.
81
+ const needsPhrase = resolvedLevel === 'high' && !!confirmPhrase;
82
+ const unlocked = !needsPhrase || typed === confirmPhrase;
83
+
84
+ // The open/pending/error lifecycle is shared with NoteDialog — see the hook
85
+ // for the invariants (in-flight request owns the dialog, failure keeps it
86
+ // open, closing clears the error). The `onClose` callback clears the typed
87
+ // phrase, because reopening must not inherit the one typed last time: that
88
+ // would put a second delete one click from confirmed, which is the exact
89
+ // property this level exists to remove.
90
+ const { open, pending, error, change, run } = usePendingDialog(`${confirmLabel} failed`, () =>
91
+ setTyped(''),
92
+ );
93
+
94
+ return (
95
+ <AlertDialog open={open} onOpenChange={change}>
96
+ <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
97
+ <AlertDialogContent>
98
+ <AlertDialogHeader>
99
+ <AlertDialogTitle>{title}</AlertDialogTitle>
100
+ <AlertDialogDescription>{description}</AlertDialogDescription>
101
+ </AlertDialogHeader>
102
+ {children && <div className="grid gap-2 py-2">{children}</div>}
103
+ {needsPhrase && (
104
+ <div className="grid gap-2 py-2">
105
+ <Label htmlFor={CONFIRM_PHRASE_INPUT}>
106
+ Type <code className="font-mono font-semibold">{confirmPhrase}</code> to confirm
107
+ </Label>
108
+ <Input
109
+ id={CONFIRM_PHRASE_INPUT}
110
+ value={typed}
111
+ autoComplete="off"
112
+ disabled={pending}
113
+ onChange={(e) => setTyped(e.target.value)}
114
+ />
115
+ </div>
116
+ )}
117
+ {error && <p className="text-sm text-destructive">{error}</p>}
118
+ <AlertDialogFooter>
119
+ <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
120
+ <AlertDialogAction
121
+ disabled={pending || !unlocked}
122
+ className={
123
+ resolvedLevel === 'low' ? '' : 'bg-destructive text-white hover:bg-destructive/90'
124
+ }
125
+ // Radix closes on click; the close has to wait for the request so
126
+ // a failure stays on screen.
127
+ onClick={(e) => {
128
+ e.preventDefault();
129
+ void run(onConfirm);
130
+ }}
131
+ >
132
+ {pending ? 'Working…' : confirmLabel}
133
+ </AlertDialogAction>
134
+ </AlertDialogFooter>
135
+ </AlertDialogContent>
136
+ </AlertDialog>
137
+ );
138
+ }
@@ -15,6 +15,12 @@ function renderValue(value: unknown): string {
15
15
  * Compact, scannable diff: metadata table (only changed fields) plus
16
16
  * three lists of blocks (added / removed / changed). Designed to fit
17
17
  * inside the editor's history panel without a separate page.
18
+ *
19
+ * The added/removed greens and reds below are deliberately literal rather than
20
+ * theme tokens. They are a matched pair carrying meaning, and the token set has
21
+ * `--destructive` but no success counterpart — converting only the red half
22
+ * would leave the pair mismatched. Give the design system a success token and
23
+ * both sides can move together.
18
24
  */
19
25
  export function DiffSummary({ diff }: Props): React.JSX.Element {
20
26
  const { metadata, blocks } = diff;
@@ -25,7 +31,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
25
31
  if (totalChanges === 0) {
26
32
  return (
27
33
  <div
28
- className="mt-3 rounded border border-gray-200 bg-white p-3 text-xs text-gray-500"
34
+ className="mt-3 rounded border bg-card p-3 text-xs text-muted-foreground"
29
35
  data-testid="diff-summary"
30
36
  >
31
37
  No changes between these revisions.
@@ -34,13 +40,10 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
34
40
  }
35
41
 
36
42
  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
- >
43
+ <div className="mt-3 space-y-2 rounded border bg-card p-3 text-xs" data-testid="diff-summary">
41
44
  {metaKeys.length > 0 && (
42
45
  <div>
43
- <div className="font-semibold text-gray-700">Metadata</div>
46
+ <div className="font-semibold">Metadata</div>
44
47
  <ul className="ml-3 list-disc">
45
48
  {metaKeys.map((key) => (
46
49
  <li key={key}>
@@ -61,7 +64,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
61
64
  {blocks.added.map((b) => (
62
65
  <li key={b.id}>
63
66
  <span className="font-mono">{b.type ?? '?'}</span>{' '}
64
- <span className="text-gray-500">({b.id})</span>
67
+ <span className="text-muted-foreground">({b.id})</span>
65
68
  </li>
66
69
  ))}
67
70
  </ul>
@@ -74,7 +77,7 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
74
77
  {blocks.removed.map((b) => (
75
78
  <li key={b.id}>
76
79
  <span className="font-mono">{b.type ?? '?'}</span>{' '}
77
- <span className="text-gray-500">({b.id})</span>
80
+ <span className="text-muted-foreground">({b.id})</span>
78
81
  </li>
79
82
  ))}
80
83
  </ul>
@@ -87,16 +90,14 @@ export function DiffSummary({ diff }: Props): React.JSX.Element {
87
90
  {blocks.changed.map((b) => (
88
91
  <li key={b.id}>
89
92
  <span className="font-mono">{b.type ?? '?'}</span>{' '}
90
- <span className="text-gray-500">({b.id})</span>
93
+ <span className="text-muted-foreground">({b.id})</span>
91
94
  {b.type_before && (
92
- <span className="text-gray-500">
95
+ <span className="text-muted-foreground">
93
96
  {' '}
94
97
  — was <span className="font-mono">{b.type_before}</span>
95
98
  </span>
96
99
  )}
97
- {b.fields.length > 0 && (
98
- <span className="text-gray-700">: {b.fields.join(', ')}</span>
99
- )}
100
+ {b.fields.length > 0 && <span>: {b.fields.join(', ')}</span>}
100
101
  </li>
101
102
  ))}
102
103
  </ul>
@@ -0,0 +1,218 @@
1
+ import { router } from '@inertiajs/react';
2
+ import { Button } from '@simple-module-py/ui/components/ui/button';
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from '@simple-module-py/ui/components/ui/dialog';
12
+ import { Input } from '@simple-module-py/ui/components/ui/input';
13
+ import { Label } from '@simple-module-py/ui/components/ui/label';
14
+ import {
15
+ NativeSelect,
16
+ NativeSelectOptGroup,
17
+ } from '@simple-module-py/ui/components/ui/native-select';
18
+ import { useEffect, useState } from 'react';
19
+
20
+ import type { PageRead } from '../utils/api';
21
+ import { createPage, listPages, listTemplates } from '../utils/pagesApi';
22
+ import { slugify } from '../utils/slugify';
23
+
24
+ const TITLE_ID = 'new-page-title';
25
+ const SLUG_ID = 'new-page-slug';
26
+ const START_ID = 'new-page-start';
27
+ const PARENT_ID = 'new-page-parent';
28
+
29
+ const BLANK = '';
30
+
31
+ /** An empty Puck document — what "Blank" starts from. */
32
+ const EMPTY_DRAFT = { root: { props: { title: '', width: 'full' } }, content: [], zones: {} };
33
+
34
+ /** "New page" — title, URL, a starting point, and an optional breadcrumb parent.
35
+ *
36
+ * Four fields, all changeable later. The dialog exists because two of them are
37
+ * awkward to change *after* the fact: the URL leaves a stale link behind, and
38
+ * the starting point cannot be applied to a page that already has content.
39
+ */
40
+ export function NewPageDialog({ publicPrefix = '/p' }: { publicPrefix?: string }) {
41
+ const [open, setOpen] = useState(false);
42
+ const [title, setTitle] = useState('');
43
+ // Null until the author edits it: while it is null the slug tracks the title,
44
+ // and the moment they type their own it stops being overwritten.
45
+ const [slugOverride, setSlugOverride] = useState<string | null>(null);
46
+ const [startFrom, setStartFrom] = useState(BLANK);
47
+ const [parentId, setParentId] = useState('');
48
+ const [templates, setTemplates] = useState<PageRead[]>([]);
49
+ const [pages, setPages] = useState<PageRead[]>([]);
50
+ const [pending, setPending] = useState(false);
51
+ const [error, setError] = useState<string | null>(null);
52
+
53
+ const slug = slugOverride ?? slugify(title);
54
+
55
+ useEffect(() => {
56
+ if (!open) return;
57
+ const controller = new AbortController();
58
+ // The signal is threaded into the requests, not just checked afterwards:
59
+ // without it, closing the dialog left both fetches running to completion.
60
+ void Promise.all([listTemplates(controller.signal), listPages(controller.signal)])
61
+ .then(([t, p]) => {
62
+ if (controller.signal.aborted) return;
63
+ setTemplates(t.items);
64
+ setPages(p.items);
65
+ })
66
+ // Not worth a banner: without these the dialog still creates a blank
67
+ // page, which is the common case anyway.
68
+ .catch(() => {});
69
+ return () => controller.abort();
70
+ }, [open]);
71
+
72
+ const reset = (next: boolean) => {
73
+ if (pending) return;
74
+ setOpen(next);
75
+ if (!next) {
76
+ setTitle('');
77
+ setSlugOverride(null);
78
+ setStartFrom(BLANK);
79
+ setParentId('');
80
+ setError(null);
81
+ }
82
+ };
83
+
84
+ const create = async () => {
85
+ setPending(true);
86
+ setError(null);
87
+ try {
88
+ const page = await createPage({
89
+ title: title.trim(),
90
+ slug,
91
+ draft_data: EMPTY_DRAFT,
92
+ ...(startFrom ? { copy_from_page_id: Number(startFrom) } : {}),
93
+ ...(parentId ? { parent_id: Number(parentId) } : {}),
94
+ });
95
+ router.visit(`/pagebuilder/${page.id}/edit`);
96
+ } catch (e) {
97
+ setError((e as Error).message);
98
+ setPending(false);
99
+ }
100
+ };
101
+
102
+ const copyable = pages.filter((p) => !p.is_template);
103
+
104
+ return (
105
+ <Dialog open={open} onOpenChange={reset}>
106
+ <DialogTrigger asChild>
107
+ <Button type="button">New page</Button>
108
+ </DialogTrigger>
109
+ <DialogContent>
110
+ <form
111
+ onSubmit={(e) => {
112
+ e.preventDefault();
113
+ if (title.trim() && slug && !pending) void create();
114
+ }}
115
+ >
116
+ <DialogHeader>
117
+ <DialogTitle>New page</DialogTitle>
118
+ <DialogDescription>
119
+ Four fields, all changeable later. Creating opens the editor.
120
+ </DialogDescription>
121
+ </DialogHeader>
122
+
123
+ <div className="grid gap-4 py-4">
124
+ <div className="grid gap-2">
125
+ <Label htmlFor={TITLE_ID}>Title</Label>
126
+ <Input
127
+ id={TITLE_ID}
128
+ value={title}
129
+ autoFocus
130
+ disabled={pending}
131
+ placeholder="Field methods"
132
+ onChange={(e) => setTitle(e.target.value)}
133
+ />
134
+ </div>
135
+
136
+ <div className="grid gap-2">
137
+ <Label htmlFor={SLUG_ID}>URL</Label>
138
+ <div className="flex items-center gap-1">
139
+ <span className="text-sm text-muted-foreground">{publicPrefix}/</span>
140
+ <Input
141
+ id={SLUG_ID}
142
+ value={slug}
143
+ disabled={pending}
144
+ onChange={(e) => setSlugOverride(e.target.value)}
145
+ />
146
+ </div>
147
+ </div>
148
+
149
+ <div className="grid gap-2">
150
+ <Label htmlFor={START_ID}>Start from</Label>
151
+ <NativeSelect
152
+ id={START_ID}
153
+ value={startFrom}
154
+ disabled={pending}
155
+ onChange={(e) => setStartFrom(e.target.value)}
156
+ >
157
+ <option value={BLANK}>Blank</option>
158
+ {templates.length > 0 && (
159
+ <NativeSelectOptGroup label="Templates">
160
+ {templates.map((t) => (
161
+ <option key={t.id} value={String(t.id)}>
162
+ {t.title}
163
+ </option>
164
+ ))}
165
+ </NativeSelectOptGroup>
166
+ )}
167
+ {copyable.length > 0 && (
168
+ <NativeSelectOptGroup label="Copy a page">
169
+ {copyable.map((p) => (
170
+ <option key={p.id} value={String(p.id)}>
171
+ {p.title}
172
+ </option>
173
+ ))}
174
+ </NativeSelectOptGroup>
175
+ )}
176
+ </NativeSelect>
177
+ <p className="text-xs text-muted-foreground">
178
+ Templates are ordinary pages flagged as templates, so the set grows without a
179
+ developer.
180
+ </p>
181
+ </div>
182
+
183
+ <div className="grid gap-2">
184
+ <Label htmlFor={PARENT_ID}>Parent</Label>
185
+ <NativeSelect
186
+ id={PARENT_ID}
187
+ value={parentId}
188
+ disabled={pending}
189
+ onChange={(e) => setParentId(e.target.value)}
190
+ >
191
+ <option value="">None</option>
192
+ {pages.map((p) => (
193
+ <option key={p.id} value={String(p.id)}>
194
+ {p.title}
195
+ </option>
196
+ ))}
197
+ </NativeSelect>
198
+ <p className="text-xs text-muted-foreground">
199
+ Optional. Affects the breadcrumb, not the URL.
200
+ </p>
201
+ </div>
202
+
203
+ {error && <p className="text-sm text-destructive">{error}</p>}
204
+ </div>
205
+
206
+ <DialogFooter>
207
+ <Button type="button" variant="outline" disabled={pending} onClick={() => reset(false)}>
208
+ Cancel
209
+ </Button>
210
+ <Button type="submit" disabled={pending || !title.trim() || !slug}>
211
+ {pending ? 'Creating…' : 'Create and open editor'}
212
+ </Button>
213
+ </DialogFooter>
214
+ </form>
215
+ </DialogContent>
216
+ </Dialog>
217
+ );
218
+ }