create-nextblock 0.16.4 → 0.17.1

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 (203) hide show
  1. package/CLAUDE.md +22 -0
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +272 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  62. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  63. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  64. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  65. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  66. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  67. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  68. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  69. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  70. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  71. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  72. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  73. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  74. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  75. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  76. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  77. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  78. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  79. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  80. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  81. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  82. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  83. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  84. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  85. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  86. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  87. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  89. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  90. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  91. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  92. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  93. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  94. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  95. package/templates/nextblock-template/components/Header.tsx +38 -38
  96. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  97. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  98. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  99. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  100. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  101. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  102. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  103. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  104. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  105. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  106. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  107. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  108. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  109. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  110. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  111. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  112. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  113. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  114. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  115. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  116. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  117. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  118. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  119. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  120. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  121. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
  122. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
  123. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
  124. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  125. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
  126. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  127. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
  128. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
  129. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
  130. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
  131. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  132. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  133. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  134. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  135. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  136. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  137. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  138. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  139. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  140. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  141. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  142. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  143. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  144. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  145. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  146. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  147. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  148. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  149. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  150. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  151. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  152. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  153. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  154. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  155. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  156. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  157. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  158. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  159. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  160. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  161. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  162. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  163. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  164. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  165. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  166. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  167. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  168. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  169. package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
  170. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  171. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  172. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  173. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -175
  174. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  175. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  176. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  177. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  178. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  179. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  180. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  181. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  182. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  183. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  184. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  185. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  186. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  187. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  188. package/templates/nextblock-template/next-env.d.ts +1 -0
  189. package/templates/nextblock-template/package.json +1 -1
  190. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  191. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  192. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  193. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  194. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  195. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  196. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  197. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  198. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  199. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  200. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  201. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  202. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  203. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -14,6 +14,8 @@ type PublicPostData = PostType & {
14
14
  language_id: number;
15
15
  translation_group_id: string;
16
16
  feature_image_url?: string | null;
17
+ /** The media row's description, used as the hero image alt text when present. */
18
+ feature_image_alt?: string | null;
17
19
  feature_image_blur_data_url?: string | null;
18
20
  feature_image_width?: number | null;
19
21
  feature_image_height?: number | null;
@@ -249,7 +251,7 @@ export async function getPostDataBySlug(slug: string): Promise<PublicPostData |
249
251
  *,
250
252
  languages!inner (id, code),
251
253
  blocks (*),
252
- media ( object_key, blur_data_url, width, height )
254
+ media ( object_key, blur_data_url, width, height, description )
253
255
  `)
254
256
  .eq("slug", slug) // Find the post by its unique slug for this language
255
257
  .order('order', { foreignTable: 'blocks', ascending: true });
@@ -320,6 +322,7 @@ export async function getPostDataBySlug(slug: string): Promise<PublicPostData |
320
322
  language_id: langInfo.id,
321
323
  translation_group_id: postData.translation_group_id,
322
324
  feature_image_url: resolveMediaUrl(postData.media?.object_key),
325
+ feature_image_alt: postData.media?.description ?? null,
323
326
  feature_image_blur_data_url: postData.media?.blur_data_url,
324
327
  feature_image_width: postData.media?.width ?? null,
325
328
  feature_image_height: postData.media?.height ?? null,
@@ -1,20 +1,20 @@
1
- 'use client';
2
-
3
- import { useEffect } from 'react';
4
- import { useCartStore, type CartItem } from '@nextblock-cms/ecommerce';
5
-
6
- interface UcpCartHydratorProps {
7
- items: CartItem[];
8
- }
9
-
10
- export function UcpCartHydrator({ items }: UcpCartHydratorProps) {
11
- useEffect(() => {
12
- if (items.length === 0) {
13
- return;
14
- }
15
-
16
- useCartStore.getState().setItems(items);
17
- }, [items]);
18
-
19
- return null;
20
- }
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useCartStore, type CartItem } from '@nextblock-cms/ecommerce';
5
+
6
+ interface UcpCartHydratorProps {
7
+ items: CartItem[];
8
+ }
9
+
10
+ export function UcpCartHydrator({ items }: UcpCartHydratorProps) {
11
+ useEffect(() => {
12
+ if (items.length === 0) {
13
+ return;
14
+ }
15
+
16
+ useCartStore.getState().setItems(items);
17
+ }, [items]);
18
+
19
+ return null;
20
+ }
@@ -1,5 +1,5 @@
1
- import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent, useCallback, type CSSProperties } from "react";
2
- import { cn } from "@nextblock-cms/utils";
1
+ import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent, useCallback, type CSSProperties } from "react";
2
+ import { cn } from "@nextblock-cms/utils";
3
3
  import {
4
4
  Dialog,
5
5
  DialogContent,
@@ -9,7 +9,7 @@ import {
9
9
  DialogDescription,
10
10
  } from "@nextblock-cms/ui";
11
11
  import { Button } from "@nextblock-cms/ui";
12
- import { blockRegistry, BlockType } from '../../../../lib/blocks/blockRegistry';
12
+ import { blockRegistry, BlockType } from '../../../../lib/blocks/blockRegistry';
13
13
 
14
14
  // A generic representation of a block object.
15
15
  // The modal primarily needs `type` to get the label and `content` for editing.
@@ -20,171 +20,171 @@ export type Block<T = unknown> = {
20
20
  };
21
21
 
22
22
  // Props that every block editor component must accept.
23
- export type BlockEditorProps<T = unknown> = {
24
- block: Block<T>;
25
- content: T;
26
- onChange: (newContent: T) => void;
27
- className?: string; // Added for editor component styling
28
- sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
29
- };
30
-
31
- export type EditorSurfaceContext = {
32
- isDark: boolean;
33
- style: CSSProperties;
34
- };
35
-
36
- export type BlockEditorSaveMode = "manual" | "autosave";
37
- export type BlockEditorSaveStatus = "idle" | "dirty" | "saving" | "saved" | "error";
38
-
39
- type BlockEditorModalProps = {
40
- block: Block;
41
- isOpen: boolean;
42
- onClose: () => void;
43
- onSave: (updatedContent: unknown) => void;
44
- EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
45
- sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
46
- editorSurfaceContext?: EditorSurfaceContext | null;
47
- titleOverride?: string;
48
- useContextualSurface?: boolean;
49
- saveMode?: BlockEditorSaveMode;
50
- saveStatus?: BlockEditorSaveStatus;
51
- saveStatusText?: string;
52
- onAutoChange?: (content: unknown) => void;
53
- onFlushBeforeClose?: () => Promise<boolean>;
54
- };
55
-
56
- const autosaveStatusLabels: Record<BlockEditorSaveStatus, string> = {
57
- idle: "Saved",
58
- dirty: "Unsaved",
59
- saving: "Saving...",
60
- saved: "Saved",
61
- error: "Save failed",
62
- };
63
-
64
- export function BlockEditorModal({
65
- block,
66
- isOpen,
67
- onClose,
68
- onSave,
69
- EditorComponent,
70
- sectionBackground,
71
- editorSurfaceContext,
72
- titleOverride,
73
- useContextualSurface,
74
- saveMode = "manual",
75
- saveStatus = "idle",
76
- saveStatusText,
77
- onAutoChange,
78
- onFlushBeforeClose,
79
- }: BlockEditorModalProps) {
80
- const [tempContent, setTempContent] = useState(block.content);
81
- const [showConfirmClose, setShowConfirmClose] = useState(false);
82
- const [isFlushingBeforeClose, setIsFlushingBeforeClose] = useState(false);
83
- const isValid = true; // Placeholder for future validation logic
84
- const isAutosaveMode = saveMode === "autosave";
85
- const [saveShortcutLabel, setSaveShortcutLabel] = useState("CMD+S");
86
-
87
- useEffect(() => {
88
- if (typeof window !== "undefined") {
89
- const isMac = navigator.userAgent.indexOf("Mac") !== -1;
90
- setSaveShortcutLabel(isMac ? "CMD+S" : "Ctrl+S");
91
- }
92
- }, []);
93
-
94
- useEffect(() => {
95
- // When the modal is opened with a new block, reset the temp content
96
- if (isOpen) {
97
- setTempContent(block.content);
98
- setShowConfirmClose(false);
99
- setIsFlushingBeforeClose(false);
100
- }
101
- }, [isOpen, block.content]);
102
-
103
- const handleSave = useCallback(() => {
104
- onSave(tempContent);
105
- }, [onSave, tempContent]);
106
-
107
- const hasUnsavedChanges = useCallback(() => {
108
- return JSON.stringify(block.content) !== JSON.stringify(tempContent);
109
- }, [block.content, tempContent]);
110
-
111
- const flushAutosave = useCallback(async () => {
112
- if (!onFlushBeforeClose) {
113
- return true;
114
- }
115
-
116
- setIsFlushingBeforeClose(true);
117
- try {
118
- return await onFlushBeforeClose();
119
- } finally {
120
- setIsFlushingBeforeClose(false);
121
- }
122
- }, [onFlushBeforeClose]);
123
-
124
- const handleCloseAttempt = useCallback(() => {
125
- if (isAutosaveMode) {
126
- void (async () => {
127
- const didFlush = await flushAutosave();
128
- if (didFlush) {
129
- onClose();
130
- }
131
- })();
132
- return;
133
- }
134
-
135
- if (hasUnsavedChanges()) {
136
- setShowConfirmClose(true);
137
- } else {
138
- onClose();
139
- }
140
- }, [flushAutosave, hasUnsavedChanges, isAutosaveMode, onClose]);
141
-
142
- useEffect(() => {
143
- if (!isOpen) {
144
- return;
145
- }
146
-
147
- const handleKeyDown = (event: KeyboardEvent) => {
148
- if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "s") {
149
- return;
150
- }
151
-
152
- event.preventDefault();
153
- if (isAutosaveMode) {
154
- handleCloseAttempt();
155
- } else {
156
- handleSave();
157
- }
158
- };
159
-
160
- window.addEventListener("keydown", handleKeyDown, true);
161
- return () => {
162
- window.removeEventListener("keydown", handleKeyDown, true);
163
- };
164
- }, [handleCloseAttempt, handleSave, isAutosaveMode, isOpen]);
165
-
166
- const handleContentChange = (newContent: unknown) => {
167
- setTempContent(newContent);
168
- if (isAutosaveMode) {
169
- onAutoChange?.(newContent);
170
- }
171
- // Potentially add validation here and set isValid
172
- };
173
-
174
- const blockInfo = blockRegistry[block.type];
175
- const displayText = blockInfo?.label || "Block";
176
- const shouldUseContextualSurface =
177
- (useContextualSurface ?? true) && block.type === 'text';
178
- const contextualSurfaceStyle = shouldUseContextualSurface ? editorSurfaceContext?.style : undefined;
179
- const editorClassName = cn(
180
- "bg-transparent text-foreground border-none shadow-none focus-within:ring-0 min-h-[60vh]",
181
- shouldUseContextualSurface &&
182
- editorSurfaceContext?.isDark &&
183
- "[&_.ProseMirror]:text-white [&_.ProseMirror.prose]:prose-invert"
184
- );
185
- const resolvedSaveStatusText = saveStatusText ?? autosaveStatusLabels[saveStatus];
186
-
187
- return (
23
+ export type BlockEditorProps<T = unknown> = {
24
+ block: Block<T>;
25
+ content: T;
26
+ onChange: (newContent: T) => void;
27
+ className?: string; // Added for editor component styling
28
+ sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
29
+ };
30
+
31
+ export type EditorSurfaceContext = {
32
+ isDark: boolean;
33
+ style: CSSProperties;
34
+ };
35
+
36
+ export type BlockEditorSaveMode = "manual" | "autosave";
37
+ export type BlockEditorSaveStatus = "idle" | "dirty" | "saving" | "saved" | "error";
38
+
39
+ type BlockEditorModalProps = {
40
+ block: Block;
41
+ isOpen: boolean;
42
+ onClose: () => void;
43
+ onSave: (updatedContent: unknown) => void;
44
+ EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
45
+ sectionBackground?: import("@/lib/blocks/blockRegistry").SectionBlockContent['background'];
46
+ editorSurfaceContext?: EditorSurfaceContext | null;
47
+ titleOverride?: string;
48
+ useContextualSurface?: boolean;
49
+ saveMode?: BlockEditorSaveMode;
50
+ saveStatus?: BlockEditorSaveStatus;
51
+ saveStatusText?: string;
52
+ onAutoChange?: (content: unknown) => void;
53
+ onFlushBeforeClose?: () => Promise<boolean>;
54
+ };
55
+
56
+ const autosaveStatusLabels: Record<BlockEditorSaveStatus, string> = {
57
+ idle: "Saved",
58
+ dirty: "Unsaved",
59
+ saving: "Saving...",
60
+ saved: "Saved",
61
+ error: "Save failed",
62
+ };
63
+
64
+ export function BlockEditorModal({
65
+ block,
66
+ isOpen,
67
+ onClose,
68
+ onSave,
69
+ EditorComponent,
70
+ sectionBackground,
71
+ editorSurfaceContext,
72
+ titleOverride,
73
+ useContextualSurface,
74
+ saveMode = "manual",
75
+ saveStatus = "idle",
76
+ saveStatusText,
77
+ onAutoChange,
78
+ onFlushBeforeClose,
79
+ }: BlockEditorModalProps) {
80
+ const [tempContent, setTempContent] = useState(block.content);
81
+ const [showConfirmClose, setShowConfirmClose] = useState(false);
82
+ const [isFlushingBeforeClose, setIsFlushingBeforeClose] = useState(false);
83
+ const isValid = true; // Placeholder for future validation logic
84
+ const isAutosaveMode = saveMode === "autosave";
85
+ const [saveShortcutLabel, setSaveShortcutLabel] = useState("CMD+S");
86
+
87
+ useEffect(() => {
88
+ if (typeof window !== "undefined") {
89
+ const isMac = navigator.userAgent.indexOf("Mac") !== -1;
90
+ setSaveShortcutLabel(isMac ? "CMD+S" : "Ctrl+S");
91
+ }
92
+ }, []);
93
+
94
+ useEffect(() => {
95
+ // When the modal is opened with a new block, reset the temp content
96
+ if (isOpen) {
97
+ setTempContent(block.content);
98
+ setShowConfirmClose(false);
99
+ setIsFlushingBeforeClose(false);
100
+ }
101
+ }, [isOpen, block.content]);
102
+
103
+ const handleSave = useCallback(() => {
104
+ onSave(tempContent);
105
+ }, [onSave, tempContent]);
106
+
107
+ const hasUnsavedChanges = useCallback(() => {
108
+ return JSON.stringify(block.content) !== JSON.stringify(tempContent);
109
+ }, [block.content, tempContent]);
110
+
111
+ const flushAutosave = useCallback(async () => {
112
+ if (!onFlushBeforeClose) {
113
+ return true;
114
+ }
115
+
116
+ setIsFlushingBeforeClose(true);
117
+ try {
118
+ return await onFlushBeforeClose();
119
+ } finally {
120
+ setIsFlushingBeforeClose(false);
121
+ }
122
+ }, [onFlushBeforeClose]);
123
+
124
+ const handleCloseAttempt = useCallback(() => {
125
+ if (isAutosaveMode) {
126
+ void (async () => {
127
+ const didFlush = await flushAutosave();
128
+ if (didFlush) {
129
+ onClose();
130
+ }
131
+ })();
132
+ return;
133
+ }
134
+
135
+ if (hasUnsavedChanges()) {
136
+ setShowConfirmClose(true);
137
+ } else {
138
+ onClose();
139
+ }
140
+ }, [flushAutosave, hasUnsavedChanges, isAutosaveMode, onClose]);
141
+
142
+ useEffect(() => {
143
+ if (!isOpen) {
144
+ return;
145
+ }
146
+
147
+ const handleKeyDown = (event: KeyboardEvent) => {
148
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "s") {
149
+ return;
150
+ }
151
+
152
+ event.preventDefault();
153
+ if (isAutosaveMode) {
154
+ handleCloseAttempt();
155
+ } else {
156
+ handleSave();
157
+ }
158
+ };
159
+
160
+ window.addEventListener("keydown", handleKeyDown, true);
161
+ return () => {
162
+ window.removeEventListener("keydown", handleKeyDown, true);
163
+ };
164
+ }, [handleCloseAttempt, handleSave, isAutosaveMode, isOpen]);
165
+
166
+ const handleContentChange = (newContent: unknown) => {
167
+ setTempContent(newContent);
168
+ if (isAutosaveMode) {
169
+ onAutoChange?.(newContent);
170
+ }
171
+ // Potentially add validation here and set isValid
172
+ };
173
+
174
+ const blockInfo = blockRegistry[block.type];
175
+ const displayText = blockInfo?.label || "Block";
176
+ const shouldUseContextualSurface =
177
+ (useContextualSurface ?? true) && block.type === 'text';
178
+ const contextualSurfaceStyle = shouldUseContextualSurface ? editorSurfaceContext?.style : undefined;
179
+ const editorClassName = cn(
180
+ "bg-transparent text-foreground border-none shadow-none focus-within:ring-0 min-h-[60vh]",
181
+ shouldUseContextualSurface &&
182
+ editorSurfaceContext?.isDark &&
183
+ "[&_.ProseMirror]:text-white [&_.ProseMirror.prose]:prose-invert"
184
+ );
185
+ const resolvedSaveStatusText = saveStatusText ?? autosaveStatusLabels[saveStatus];
186
+
187
+ return (
188
188
  <>
189
189
  <Dialog open={isOpen} onOpenChange={(open) => {
190
190
  if (!open) {
@@ -192,56 +192,56 @@ export function BlockEditorModal({
192
192
  }
193
193
  }}>
194
194
  <DialogContent
195
- className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
196
- onInteractOutside={(e) => {
197
- e.preventDefault();
198
- handleCloseAttempt();
199
- }}
195
+ className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
196
+ onInteractOutside={(e) => {
197
+ e.preventDefault();
198
+ handleCloseAttempt();
199
+ }}
200
200
  onEscapeKeyDown={(e) => {
201
201
  e.preventDefault();
202
202
  handleCloseAttempt();
203
203
  }}
204
204
  >
205
205
  {/* Header */}
206
- <div className="flex items-center justify-between p-4 border-b bg-background/95 text-foreground backdrop-blur z-10">
207
- <div className="flex items-center gap-2">
208
- <DialogTitle className="text-lg font-semibold">
209
- {titleOverride ?? `Edit ${displayText}`}
210
- </DialogTitle>
211
- </div>
212
- <div className="flex items-center gap-2">
213
- {isAutosaveMode && (
214
- <span
215
- aria-live="polite"
216
- className={cn(
217
- "rounded-md px-2 py-1 text-xs font-medium",
218
- saveStatus === "error"
219
- ? "bg-destructive/10 text-destructive"
220
- : saveStatus === "dirty"
221
- ? "bg-amber-500/10 text-amber-700 dark:text-amber-300"
222
- : "bg-muted text-muted-foreground",
223
- "max-w-[360px] truncate"
224
- )}
225
- title={resolvedSaveStatusText}
226
- >
227
- {resolvedSaveStatusText}
228
- </span>
229
- )}
230
- <Button
231
- variant={isAutosaveMode ? "default" : "ghost"}
232
- size="sm"
233
- onClick={handleCloseAttempt}
234
- disabled={isAutosaveMode && isFlushingBeforeClose}
235
- >
236
- {isAutosaveMode ? "Done" : "Cancel"}
237
- </Button>
238
- {!isAutosaveMode && (
239
- <Button onClick={handleSave} disabled={!isValid} size="sm">
240
- Save ({saveShortcutLabel})
241
- </Button>
242
- )}
243
- </div>
244
- </div>
206
+ <div className="flex items-center justify-between p-4 border-b bg-background/95 text-foreground backdrop-blur z-10">
207
+ <div className="flex items-center gap-2">
208
+ <DialogTitle className="text-lg font-semibold">
209
+ {titleOverride ?? `Edit ${displayText}`}
210
+ </DialogTitle>
211
+ </div>
212
+ <div className="flex items-center gap-2">
213
+ {isAutosaveMode && (
214
+ <span
215
+ aria-live="polite"
216
+ className={cn(
217
+ "rounded-md px-2 py-1 text-xs font-medium",
218
+ saveStatus === "error"
219
+ ? "bg-destructive/10 text-destructive"
220
+ : saveStatus === "dirty"
221
+ ? "bg-amber-500/10 text-amber-700 dark:text-amber-300"
222
+ : "bg-muted text-muted-foreground",
223
+ "max-w-[360px] truncate"
224
+ )}
225
+ title={resolvedSaveStatusText}
226
+ >
227
+ {resolvedSaveStatusText}
228
+ </span>
229
+ )}
230
+ <Button
231
+ variant={isAutosaveMode ? "default" : "ghost"}
232
+ size="sm"
233
+ onClick={handleCloseAttempt}
234
+ disabled={isAutosaveMode && isFlushingBeforeClose}
235
+ >
236
+ {isAutosaveMode ? "Done" : "Cancel"}
237
+ </Button>
238
+ {!isAutosaveMode && (
239
+ <Button onClick={handleSave} disabled={!isValid} size="sm">
240
+ Save ({saveShortcutLabel})
241
+ </Button>
242
+ )}
243
+ </div>
244
+ </div>
245
245
 
246
246
  {/* Editor Area with Contextual Background */}
247
247
  <div
@@ -251,38 +251,38 @@ export function BlockEditorModal({
251
251
  // Conditional Background Logic:
252
252
  // Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
253
253
  // For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
254
- shouldUseContextualSurface ? (
255
- // If no specific background, use white/dark default
256
- (!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
257
- ) : "bg-muted/10", // Default for non-text blocks
258
-
259
- // Apply theme classes if present (ONLY for text/heading)
260
- shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
261
- shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
262
- shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
263
-
264
- // Dark mode prose invert if dark background (approximate check for solid color)
265
- shouldUseContextualSurface && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert",
266
- shouldUseContextualSurface && editorSurfaceContext?.isDark && "[&_.prose]:prose-invert [&_.ProseMirror]:text-white"
267
- )}
268
- style={{
269
- // Only apply custom color/gradient styles for text/heading
270
- backgroundColor: shouldUseContextualSurface && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
271
- backgroundImage: shouldUseContextualSurface && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
272
- `${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
273
- : undefined,
274
- ...contextualSurfaceStyle,
275
- }}
276
- >
254
+ shouldUseContextualSurface ? (
255
+ // If no specific background, use white/dark default
256
+ (!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
257
+ ) : "bg-muted/10", // Default for non-text blocks
258
+
259
+ // Apply theme classes if present (ONLY for text/heading)
260
+ shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
261
+ shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
262
+ shouldUseContextualSurface && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
263
+
264
+ // Dark mode prose invert if dark background (approximate check for solid color)
265
+ shouldUseContextualSurface && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert",
266
+ shouldUseContextualSurface && editorSurfaceContext?.isDark && "[&_.prose]:prose-invert [&_.ProseMirror]:text-white"
267
+ )}
268
+ style={{
269
+ // Only apply custom color/gradient styles for text/heading
270
+ backgroundColor: shouldUseContextualSurface && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
271
+ backgroundImage: shouldUseContextualSurface && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
272
+ `${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
273
+ : undefined,
274
+ ...contextualSurfaceStyle,
275
+ }}
276
+ >
277
277
  <div className="max-w-6xl mx-auto">
278
278
  <Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
279
279
  <EditorComponent
280
- block={block}
281
- content={tempContent}
282
- onChange={handleContentChange}
283
- className={editorClassName}
284
- sectionBackground={sectionBackground} // Pass down if editor supports it
285
- />
280
+ block={block}
281
+ content={tempContent}
282
+ onChange={handleContentChange}
283
+ className={editorClassName}
284
+ sectionBackground={sectionBackground} // Pass down if editor supports it
285
+ />
286
286
  </Suspense>
287
287
  </div>
288
288
  </div>