simple-module-pagebuilder 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. pagebuilder/__init__.py +0 -0
  2. pagebuilder/components/DiffSummary.tsx +107 -0
  3. pagebuilder/components/ScheduledBadge.tsx +34 -0
  4. pagebuilder/components/StatusBadge.tsx +21 -0
  5. pagebuilder/components/blockRegistry.test.ts +56 -0
  6. pagebuilder/components/blockRegistry.ts +80 -0
  7. pagebuilder/components/blocks/Button.tsx +142 -0
  8. pagebuilder/components/blocks/Columns.tsx +64 -0
  9. pagebuilder/components/blocks/Heading.tsx +92 -0
  10. pagebuilder/components/blocks/Image.tsx +125 -0
  11. pagebuilder/components/blocks/MediaPicker.tsx +178 -0
  12. pagebuilder/components/blocks/Spacer.tsx +31 -0
  13. pagebuilder/components/blocks/Text.tsx +80 -0
  14. pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
  15. pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
  16. pagebuilder/components/editor/SchedulePanel.tsx +85 -0
  17. pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
  18. pagebuilder/components/layoutPuckConfig.tsx +109 -0
  19. pagebuilder/components/media/MediaFilters.tsx +83 -0
  20. pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
  21. pagebuilder/components/media/MediaGrid.tsx +76 -0
  22. pagebuilder/components/media/MediaHeader.tsx +37 -0
  23. pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
  24. pagebuilder/components/media/types.ts +20 -0
  25. pagebuilder/components/migrateContent.test.ts +146 -0
  26. pagebuilder/components/migrateContent.ts +135 -0
  27. pagebuilder/components/puckConfig.test.tsx +118 -0
  28. pagebuilder/components/puckConfig.tsx +121 -0
  29. pagebuilder/components/widgetCatalog.test.tsx +123 -0
  30. pagebuilder/components/widgetCatalog.ts +208 -0
  31. pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
  32. pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
  33. pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
  34. pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
  35. pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
  36. pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
  37. pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
  38. pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
  39. pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
  40. pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
  41. pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
  42. pagebuilder/components/widgets/_shared/heading.tsx +88 -0
  43. pagebuilder/components/widgets/_shared/index.ts +39 -0
  44. pagebuilder/components/widgets/_shared/mask.ts +54 -0
  45. pagebuilder/components/widgets/_shared/section.tsx +60 -0
  46. pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
  47. pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
  48. pagebuilder/components/widgets/accordion-widget.tsx +64 -0
  49. pagebuilder/components/widgets/alert-widget.tsx +47 -0
  50. pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
  51. pagebuilder/components/widgets/article-cards-render.tsx +132 -0
  52. pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
  53. pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
  54. pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
  55. pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
  56. pagebuilder/components/widgets/carousel-widget.tsx +97 -0
  57. pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
  58. pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
  59. pagebuilder/components/widgets/column-widget.tsx +63 -0
  60. pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
  61. pagebuilder/components/widgets/contact-form-render.tsx +293 -0
  62. pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
  63. pagebuilder/components/widgets/container-widget.tsx +93 -0
  64. pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
  65. pagebuilder/components/widgets/divider-widget.tsx +56 -0
  66. pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
  67. pagebuilder/components/widgets/faq-render.tsx +268 -0
  68. pagebuilder/components/widgets/faq-widget.tsx +64 -0
  69. pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
  70. pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
  71. pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
  72. pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
  73. pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
  74. pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
  75. pagebuilder/components/widgets/gallery-widget.tsx +139 -0
  76. pagebuilder/components/widgets/grid-widget.tsx +59 -0
  77. pagebuilder/components/widgets/hero-widget.tsx +292 -0
  78. pagebuilder/components/widgets/html-widget.tsx +42 -0
  79. pagebuilder/components/widgets/iframe-widget.tsx +44 -0
  80. pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
  81. pagebuilder/components/widgets/list-widget.tsx +56 -0
  82. pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
  83. pagebuilder/components/widgets/markdown-widget.tsx +148 -0
  84. pagebuilder/components/widgets/media-object-layout.ts +156 -0
  85. pagebuilder/components/widgets/media-object-render.tsx +284 -0
  86. pagebuilder/components/widgets/media-object-text.tsx +202 -0
  87. pagebuilder/components/widgets/media-object-widget.tsx +176 -0
  88. pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
  89. pagebuilder/components/widgets/page-header-widget.tsx +118 -0
  90. pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
  91. pagebuilder/components/widgets/quote-widget.tsx +51 -0
  92. pagebuilder/components/widgets/row-widget.tsx +74 -0
  93. pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
  94. pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
  95. pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
  96. pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
  97. pagebuilder/components/widgets/site-header-nav.tsx +153 -0
  98. pagebuilder/components/widgets/site-header-widget.tsx +182 -0
  99. pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
  100. pagebuilder/components/widgets/stats-widget.tsx +200 -0
  101. pagebuilder/components/widgets/steps-widget.tsx +211 -0
  102. pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
  103. pagebuilder/components/widgets/table-widget.tsx +86 -0
  104. pagebuilder/components/widgets/tabs-widget.tsx +95 -0
  105. pagebuilder/components/widgets/tags-widget.tsx +85 -0
  106. pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
  107. pagebuilder/components/widgets/timeline-layout.ts +73 -0
  108. pagebuilder/components/widgets/timeline-render.tsx +294 -0
  109. pagebuilder/components/widgets/timeline-widget.tsx +106 -0
  110. pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
  111. pagebuilder/components/widgets/video-widget.tsx +103 -0
  112. pagebuilder/components/widgets/welcome-widget.tsx +38 -0
  113. pagebuilder/contracts/__init__.py +57 -0
  114. pagebuilder/contracts/events.py +25 -0
  115. pagebuilder/contracts/schemas.py +239 -0
  116. pagebuilder/conversion/blocks-to-html.test.ts +105 -0
  117. pagebuilder/conversion/blocks-to-html.ts +102 -0
  118. pagebuilder/conversion/index.ts +2 -0
  119. pagebuilder/conversion/types.ts +5 -0
  120. pagebuilder/deps.py +26 -0
  121. pagebuilder/diff.py +129 -0
  122. pagebuilder/endpoints/__init__.py +0 -0
  123. pagebuilder/endpoints/api/__init__.py +21 -0
  124. pagebuilder/endpoints/api/_deps.py +21 -0
  125. pagebuilder/endpoints/api/layout.py +78 -0
  126. pagebuilder/endpoints/api/pages.py +85 -0
  127. pagebuilder/endpoints/api/revisions.py +79 -0
  128. pagebuilder/endpoints/api/uploads.py +92 -0
  129. pagebuilder/endpoints/api/workflow.py +124 -0
  130. pagebuilder/endpoints/seo.py +95 -0
  131. pagebuilder/endpoints/views.py +228 -0
  132. pagebuilder/fields/checkbox-field.tsx +22 -0
  133. pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
  134. pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
  135. pagebuilder/fields/file-picker/index.ts +6 -0
  136. pagebuilder/fields/file-picker/types.ts +15 -0
  137. pagebuilder/fields/index.ts +9 -0
  138. pagebuilder/fields/media-adapter.ts +47 -0
  139. pagebuilder/hooks/useAutosave.ts +134 -0
  140. pagebuilder/hooks/useEditorForm.ts +193 -0
  141. pagebuilder/hooks/useMediaUploads.ts +96 -0
  142. pagebuilder/hooks/usePageRevisions.ts +59 -0
  143. pagebuilder/hooks/usePageSchedule.ts +72 -0
  144. pagebuilder/hooks/usePageWorkflow.ts +173 -0
  145. pagebuilder/layout_service.py +101 -0
  146. pagebuilder/media_images.py +185 -0
  147. pagebuilder/media_queries.py +92 -0
  148. pagebuilder/media_service.py +222 -0
  149. pagebuilder/models.py +209 -0
  150. pagebuilder/module.py +279 -0
  151. pagebuilder/package.json +18 -0
  152. pagebuilder/pages/LayoutEditor.tsx +183 -0
  153. pagebuilder/pages/MediaLibrary.tsx +249 -0
  154. pagebuilder/pages/PageEditor.tsx +181 -0
  155. pagebuilder/pages/PageList.tsx +129 -0
  156. pagebuilder/pages/PendingReview.tsx +128 -0
  157. pagebuilder/pages/PublicPage.tsx +126 -0
  158. pagebuilder/permissions.py +44 -0
  159. pagebuilder/py.typed +0 -0
  160. pagebuilder/security.py +230 -0
  161. pagebuilder/service/__init__.py +141 -0
  162. pagebuilder/service/_common.py +24 -0
  163. pagebuilder/service/_revisions.py +92 -0
  164. pagebuilder/service/_workflow.py +207 -0
  165. pagebuilder/services.py +19 -0
  166. pagebuilder/settings.py +161 -0
  167. pagebuilder/static/widgets-base.css +57 -0
  168. pagebuilder/translation/apply.ts +54 -0
  169. pagebuilder/translation/extract.ts +141 -0
  170. pagebuilder/translation/index.ts +20 -0
  171. pagebuilder/translation/translatable-fields.ts +65 -0
  172. pagebuilder/translation/translation.test.ts +167 -0
  173. pagebuilder/utils/api.ts +12 -0
  174. pagebuilder/utils/datetime.ts +25 -0
  175. pagebuilder/utils/decorative-image.ts +15 -0
  176. pagebuilder/utils/editorSnapshot.ts +39 -0
  177. pagebuilder/utils/markdown.ts +160 -0
  178. pagebuilder/utils/mediaApi.ts +112 -0
  179. pagebuilder/utils/mediaFormat.ts +19 -0
  180. pagebuilder/utils/pagesApi.ts +111 -0
  181. pagebuilder/utils/parse-list.ts +7 -0
  182. pagebuilder/utils/request.ts +52 -0
  183. pagebuilder/utils/slugify.ts +9 -0
  184. pagebuilder/utils/types.ts +171 -0
  185. pagebuilder/utils/widgetUtils.ts +11 -0
  186. simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
  187. simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
  188. simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
  189. simple_module_pagebuilder-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,111 @@
1
+ /** Page, layout, and revision calls against the pagebuilder admin API. */
2
+
3
+ import { request } from './request';
4
+ import type {
5
+ LayoutDetail,
6
+ LayoutRevisionRead,
7
+ LayoutUpdate,
8
+ PageDetail,
9
+ PageRead,
10
+ PageRevisionRead,
11
+ PageWritePayload,
12
+ RevisionDiff,
13
+ ScheduleRequest,
14
+ } from './types';
15
+
16
+ export const createPage = (data: PageWritePayload & { title: string; slug: string }) =>
17
+ request<PageDetail>('/pages', { method: 'POST', body: JSON.stringify(data) });
18
+
19
+ export const savePage = (id: number, data: PageWritePayload) =>
20
+ request<PageDetail>(`/pages/${id}`, { method: 'PUT', body: JSON.stringify(data) });
21
+
22
+ export const deletePage = (id: number) => request<void>(`/pages/${id}`, { method: 'DELETE' });
23
+
24
+ function noteBody(note?: string | null): BodyInit | undefined {
25
+ return note ? JSON.stringify({ note }) : undefined;
26
+ }
27
+
28
+ export const publishPage = (id: number, note?: string | null) =>
29
+ request<PageRead>(`/pages/${id}/publish`, {
30
+ method: 'POST',
31
+ body: noteBody(note),
32
+ });
33
+
34
+ export const unpublishPage = (id: number, note?: string | null) =>
35
+ request<PageRead>(`/pages/${id}/unpublish`, {
36
+ method: 'POST',
37
+ body: noteBody(note),
38
+ });
39
+
40
+ export const submitPage = (id: number, note?: string | null) =>
41
+ request<PageRead>(`/pages/${id}/submit`, {
42
+ method: 'POST',
43
+ body: noteBody(note),
44
+ });
45
+
46
+ export const approvePage = (id: number, note?: string | null) =>
47
+ request<PageRead>(`/pages/${id}/approve`, {
48
+ method: 'POST',
49
+ body: noteBody(note),
50
+ });
51
+
52
+ export const rejectPage = (id: number, note: string) =>
53
+ request<PageRead>(`/pages/${id}/reject`, {
54
+ method: 'POST',
55
+ body: JSON.stringify({ note }),
56
+ });
57
+
58
+ export const listPendingPages = () => request<{ items: PageRead[] }>('/pages/pending');
59
+
60
+ export const schedulePage = (id: number, body: ScheduleRequest) =>
61
+ request<PageRead>(`/pages/${id}/schedule`, {
62
+ method: 'POST',
63
+ body: JSON.stringify(body),
64
+ });
65
+
66
+ /**
67
+ * Drive a rejection prompt → reject API round-trip. Returns the
68
+ * updated page on success, or a string describing why the call was
69
+ * skipped (cancelled / empty note / API error).
70
+ *
71
+ * Shared by the editor and the pending-review queue so both surfaces
72
+ * stay in sync on prompt copy + empty-note handling.
73
+ */
74
+ export async function promptAndReject(id: number): Promise<PageRead | { skipped: string }> {
75
+ const note = window.prompt('Reason for rejection (shown to the editor):');
76
+ if (note === null) return { skipped: 'cancelled' };
77
+ const trimmed = note.trim();
78
+ if (!trimmed) return { skipped: 'A rejection note is required.' };
79
+ try {
80
+ return await rejectPage(id, trimmed);
81
+ } catch (e) {
82
+ return { skipped: e instanceof Error ? e.message : 'Reject failed' };
83
+ }
84
+ }
85
+
86
+ export const listRevisions = (id: number) =>
87
+ request<{ items: PageRevisionRead[] }>(`/pages/${id}/revisions`);
88
+
89
+ export const restoreRevision = (pageId: number, revisionId: number) =>
90
+ request<PageDetail>(`/pages/${pageId}/revisions/${revisionId}/restore`, {
91
+ method: 'POST',
92
+ });
93
+
94
+ export const diffRevisions = (pageId: number, beforeId: number, afterId: number) =>
95
+ request<RevisionDiff>(`/pages/${pageId}/revisions/${beforeId}/diff/${afterId}`);
96
+
97
+ export const getLayout = () => request<LayoutDetail>('/layout');
98
+
99
+ export const saveLayout = (body: LayoutUpdate) =>
100
+ request<LayoutDetail>('/layout', {
101
+ method: 'PUT',
102
+ body: JSON.stringify(body),
103
+ });
104
+
105
+ export const listLayoutRevisions = () =>
106
+ request<{ items: LayoutRevisionRead[] }>('/layout/revisions');
107
+
108
+ export const restoreLayoutRevision = (revisionId: number) =>
109
+ request<LayoutDetail>(`/layout/revisions/${revisionId}/restore`, {
110
+ method: 'POST',
111
+ });
@@ -0,0 +1,7 @@
1
+ export function parseList(value: string | undefined | null): string[] {
2
+ if (!value) return [];
3
+ return value
4
+ .split(/\r?\n/)
5
+ .map((line) => line.trim())
6
+ .filter(Boolean);
7
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared fetch plumbing for the pagebuilder JSON admin API.
3
+ *
4
+ * All endpoints live under `/api/pagebuilder` and return JSON. We use
5
+ * plain fetch + same-origin cookies for auth, plus an `X-CSRF-Token`
6
+ * header on every mutating request (token is read from the
7
+ * `pagebuilder_csrf` cookie set by the view layer).
8
+ */
9
+
10
+ export const BASE = '/api/pagebuilder';
11
+
12
+ export const CSRF_COOKIE = 'pagebuilder_csrf';
13
+
14
+ const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
15
+
16
+ export function readCookie(name: string): string | null {
17
+ if (typeof document === 'undefined') return null;
18
+ const target = `${name}=`;
19
+ for (const piece of document.cookie.split(';')) {
20
+ const trimmed = piece.trim();
21
+ if (trimmed.startsWith(target)) {
22
+ return decodeURIComponent(trimmed.slice(target.length));
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
29
+ const isFormData = init.body instanceof FormData;
30
+ const method = (init.method ?? 'GET').toUpperCase();
31
+ const csrfHeaders: Record<string, string> = {};
32
+ if (UNSAFE_METHODS.has(method)) {
33
+ const token = readCookie(CSRF_COOKIE);
34
+ if (token) csrfHeaders['X-CSRF-Token'] = token;
35
+ }
36
+ const response = await fetch(`${BASE}${path}`, {
37
+ credentials: 'same-origin',
38
+ headers: {
39
+ ...(isFormData ? {} : { 'Content-Type': 'application/json' }),
40
+ Accept: 'application/json',
41
+ ...csrfHeaders,
42
+ ...(init.headers || {}),
43
+ },
44
+ ...init,
45
+ });
46
+ if (!response.ok) {
47
+ const text = await response.text();
48
+ throw new Error(`Request failed (${response.status}): ${text}`);
49
+ }
50
+ if (response.status === 204) return undefined as T;
51
+ return (await response.json()) as T;
52
+ }
@@ -0,0 +1,9 @@
1
+ export function slugify(value: string): string {
2
+ return value
3
+ .toLowerCase()
4
+ .normalize('NFKD')
5
+ .replace(/[̀-ͯ]/g, '')
6
+ .replace(/[^a-z0-9]+/g, '-')
7
+ .replace(/^-+|-+$/g, '')
8
+ .slice(0, 200);
9
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Wire types for the pagebuilder JSON admin API.
3
+ *
4
+ * These mirror the Pydantic DTOs in `pagebuilder.contracts.schemas`. Keep
5
+ * the two in sync when either changes.
6
+ */
7
+
8
+ export type PageStatus = 'draft' | 'submitted_for_review' | 'published';
9
+
10
+ export type RevisionEvent = 'publish' | 'unpublish' | 'submit' | 'approve' | 'reject';
11
+
12
+ export interface PageRead {
13
+ id: number;
14
+ slug: string;
15
+ title: string;
16
+ status: PageStatus;
17
+ has_published: boolean;
18
+ meta_description: string | null;
19
+ og_image: string | null;
20
+ canonical_url: string | null;
21
+ index_in_search: boolean;
22
+ rejection_note: string | null;
23
+ publish_at: string | null;
24
+ unpublish_at: string | null;
25
+ created_at: string;
26
+ updated_at: string | null;
27
+ }
28
+
29
+ export interface PageDetail extends PageRead {
30
+ draft_data: Record<string, unknown>;
31
+ published_data: Record<string, unknown> | null;
32
+ json_ld: Record<string, unknown> | null;
33
+ }
34
+
35
+ export interface LayoutDetail {
36
+ id: number;
37
+ header_data: Record<string, unknown>;
38
+ footer_data: Record<string, unknown>;
39
+ created_at: string;
40
+ updated_at: string | null;
41
+ }
42
+
43
+ export interface LayoutRevisionRead {
44
+ id: number;
45
+ layout_id: number;
46
+ note: string | null;
47
+ created_at: string;
48
+ created_by: string | null;
49
+ }
50
+
51
+ export interface LayoutUpdate {
52
+ header_data?: Record<string, unknown> | null;
53
+ footer_data?: Record<string, unknown> | null;
54
+ note?: string | null;
55
+ }
56
+
57
+ export interface PageRevisionRead {
58
+ id: number;
59
+ page_id: number;
60
+ title: string;
61
+ meta_description: string | null;
62
+ og_image: string | null;
63
+ event: RevisionEvent;
64
+ note: string | null;
65
+ created_at: string;
66
+ created_by: string | null;
67
+ }
68
+
69
+ export interface MediaAssetVariant {
70
+ filename: string;
71
+ url: string;
72
+ content_type: string;
73
+ width: number;
74
+ height: number | null;
75
+ size_bytes: number;
76
+ }
77
+
78
+ export interface MediaAssetRead {
79
+ id: number;
80
+ filename: string;
81
+ original_filename: string;
82
+ content_type: string;
83
+ size_bytes: number;
84
+ url: string;
85
+ width: number | null;
86
+ height: number | null;
87
+ folder: string | null;
88
+ variants: Record<string, MediaAssetVariant>;
89
+ created_at: string;
90
+ }
91
+
92
+ export interface MediaListResponse {
93
+ items: MediaAssetRead[];
94
+ next_cursor: number | null;
95
+ folders: string[];
96
+ /** Only present when the request used offset paging. */
97
+ total?: number | null;
98
+ }
99
+
100
+ export interface MediaListQuery {
101
+ search?: string;
102
+ content_type?: string;
103
+ /**
104
+ * Folder filter. Omit for "any folder", pass an empty string for
105
+ * "Unfiled" (rows where folder IS NULL), or pass a path like
106
+ * "marketing/heros" to match an exact folder.
107
+ */
108
+ folder?: string;
109
+ min_size_bytes?: number;
110
+ max_size_bytes?: number;
111
+ cursor?: number;
112
+ /** Offset paging, used by the image-picker gallery. Makes the response
113
+ * carry `total`; takes precedence over `cursor`. */
114
+ offset?: number;
115
+ limit?: number;
116
+ }
117
+
118
+ export interface PageWritePayload {
119
+ title?: string;
120
+ slug?: string;
121
+ meta_description?: string | null;
122
+ og_image?: string | null;
123
+ canonical_url?: string | null;
124
+ index_in_search?: boolean;
125
+ json_ld?: Record<string, unknown> | null;
126
+ draft_data?: Record<string, unknown>;
127
+ publish_at?: string | null;
128
+ unpublish_at?: string | null;
129
+ }
130
+
131
+ /**
132
+ * Body for ``POST /pages/{id}/schedule``.
133
+ *
134
+ * A field's *presence* tells the server "act on me" — omit a field to
135
+ * leave the stored value alone, send ``null`` to clear it, send an ISO
136
+ * timestamp to set it. So ``{publish_at: "2026-…"}`` schedules a publish
137
+ * without touching ``unpublish_at``.
138
+ */
139
+ export interface ScheduleRequest {
140
+ publish_at?: string | null;
141
+ unpublish_at?: string | null;
142
+ }
143
+
144
+ export interface BlockChange {
145
+ id: string;
146
+ type?: string;
147
+ fields: string[];
148
+ type_before?: string;
149
+ }
150
+
151
+ export interface MetadataChange {
152
+ before: unknown;
153
+ after: unknown;
154
+ }
155
+
156
+ export interface RevisionDiff {
157
+ before_id: number;
158
+ after_id: number;
159
+ metadata: Record<string, MetadataChange>;
160
+ blocks: {
161
+ added: BlockChange[];
162
+ removed: BlockChange[];
163
+ changed: BlockChange[];
164
+ };
165
+ }
166
+
167
+ export interface UploadMediaOptions {
168
+ folder?: string | null;
169
+ onProgress?: (loaded: number, total: number) => void;
170
+ signal?: AbortSignal;
171
+ }
@@ -0,0 +1,11 @@
1
+ /** Barrel for helpers the ported GCA widgets import as a group.
2
+ *
3
+ * Mirrors the source package's `utils` barrel. `cn` came from @geowiki/shared
4
+ * there; here it comes from the UI package we already depend on, so there's no
5
+ * second clsx/tailwind-merge implementation in the tree.
6
+ */
7
+
8
+ export { cn } from '@simple-module-py/ui/lib/utils';
9
+
10
+ export { decorativeAriaProps } from './decorative-image';
11
+ export { parseList } from './parse-list';
@@ -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