create-nextblock 0.16.4 → 0.17.0

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 (196) 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 +270 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
  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/04-DATABASE-AND-AUTH.md +11 -0
  122. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  123. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
  124. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  125. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  126. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  127. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  128. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  129. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  130. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  131. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  132. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  133. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  134. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  135. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  136. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  137. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  138. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  139. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  140. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  141. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  142. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  143. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  144. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  145. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  146. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  147. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  148. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  149. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  150. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  151. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  152. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  153. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  154. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  155. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  156. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  157. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  158. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  159. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  160. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  161. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  162. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  163. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  164. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  165. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  166. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +42 -12
  167. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  168. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  169. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  170. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  171. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  172. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  173. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  174. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  175. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  176. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  177. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  178. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  179. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  180. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  181. package/templates/nextblock-template/next-env.d.ts +1 -0
  182. package/templates/nextblock-template/package.json +1 -1
  183. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  184. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  185. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  186. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  187. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  188. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  189. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  190. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  191. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  192. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  193. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  194. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  195. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  196. 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>