create-nextblock 0.16.3 → 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 (201) 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 +10388 -9315
  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 -40
  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/ProductFormClientShell.tsx +19 -0
  61. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
  62. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  64. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  65. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  66. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  67. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  68. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  69. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  70. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  71. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  72. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  73. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  74. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  75. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  76. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  77. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  78. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  79. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  80. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  81. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  82. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  83. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  84. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  85. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  86. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  87. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  89. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  90. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  91. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  92. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  93. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  94. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  95. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  96. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  97. package/templates/nextblock-template/components/Header.tsx +38 -38
  98. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  99. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  100. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  101. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  102. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  103. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  104. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  105. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  106. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  107. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  108. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  109. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  110. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  111. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  112. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  113. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  114. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  115. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  116. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  117. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  118. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  119. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  120. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  121. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  122. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  123. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
  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 +19 -0
  126. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  127. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  128. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  129. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  130. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  131. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  132. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  133. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  134. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  135. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  136. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  137. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  138. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  139. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  140. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  141. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  142. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  143. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  144. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  145. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  146. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  147. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  148. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  149. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  150. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  151. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  152. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  153. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  154. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  155. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  156. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  157. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  158. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  159. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  160. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  161. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  162. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  163. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  164. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +3 -3
  165. package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
  166. package/templates/nextblock-template/lib/seo/page-document.ts +3 -462
  167. package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
  168. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  169. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  170. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  171. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +50 -10
  172. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  173. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  174. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  175. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  176. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  177. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  178. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  179. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  180. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  181. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  182. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  183. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  184. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  185. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  186. package/templates/nextblock-template/next-env.d.ts +1 -0
  187. package/templates/nextblock-template/package.json +1 -1
  188. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  189. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  190. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  191. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  192. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  193. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  194. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  195. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  196. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  197. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  198. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  199. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  200. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  201. 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>