@morphika/webframe 0.1.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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +210 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,417 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ReadOnlyFrame — renders the page content as a read-only mirror.
5
+ *
6
+ * Used for inactive device frames (Tablet, Phone) in the multi-device preview.
7
+ * Reads rows directly from the Zustand store so it stays in sync in real-time.
8
+ * No dnd-kit, no selection, no toolbars — pure visual rendering.
9
+ *
10
+ * Accepts a `viewport` prop so blocks resolve their responsive overrides
11
+ * for the correct device size.
12
+ *
13
+ * Performance: Each row is memoized individually so that editing a block in
14
+ * row N only re-renders that single ReadOnlyRow — not the entire frame.
15
+ *
16
+ * Session 76: Updated to handle first-class PageSections alongside Rows.
17
+ */
18
+
19
+ import { memo, useMemo, useState, useEffect } from "react";
20
+ import { useBuilderStore } from "../../lib/builder/store";
21
+ import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
22
+ import type { DeviceViewport } from "../../lib/builder/types";
23
+ import type { ContentItem, PageSection, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
24
+ import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
25
+ import { DEVICE_HEIGHTS } from "../../lib/builder/types";
26
+ import { getEffectiveColumnsV2, getSectionV2SettingValue, getRowSettingValue } from "./settings-panel/responsive-helpers";
27
+ import BlockLivePreview from "./BlockLivePreview";
28
+ import { getRowLayoutStyles, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
29
+
30
+ // Shared list of layout keys that support responsive overrides (V1 + V2)
31
+ const OVERRIDABLE_KEYS = [
32
+ "spacing_top", "spacing_right", "spacing_bottom", "spacing_left",
33
+ "offset_top", "offset_right", "offset_bottom", "offset_left",
34
+ "background_color", "background_opacity",
35
+ "border_color", "border_width", "border_style", "border_sides", "border_radius",
36
+ ] as const;
37
+
38
+ // ============================================
39
+ // Memoized per-section renderer — renders PageSection block directly
40
+ // ============================================
41
+
42
+ interface ReadOnlySectionProps {
43
+ section: PageSection;
44
+ viewport: DeviceViewport;
45
+ }
46
+
47
+ const ReadOnlySection = memo(function ReadOnlySection({ section, viewport }: ReadOnlySectionProps) {
48
+ const block = section.block?.[0];
49
+ if (!block) return null;
50
+
51
+ const base = section.settings || {};
52
+ // Merge responsive overrides for V1 PageSection (same approach as V2)
53
+ const resolvedSettings = useMemo(() => {
54
+ const merged: Record<string, unknown> = { ...base };
55
+ for (const key of OVERRIDABLE_KEYS) {
56
+ merged[key] = getRowSettingValue(section, viewport, key, (base as Record<string, unknown>)[key]);
57
+ }
58
+ return merged;
59
+ }, [section, viewport, base]);
60
+ const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
61
+
62
+ const sectionStyle: React.CSSProperties = {
63
+ ...layoutStyles,
64
+ contentVisibility: "auto",
65
+ containIntrinsicSize: "auto 200px",
66
+ };
67
+
68
+ return (
69
+ <div style={sectionStyle}>
70
+ <BlockLivePreview block={block} viewport={viewport} />
71
+ </div>
72
+ );
73
+ });
74
+
75
+ // ============================================
76
+ // Memoized per-section V2 renderer — renders PageSectionV2 grid
77
+ // BUG-V2-005 fix: V2 sections were not rendered in inactive device frames.
78
+ // ============================================
79
+
80
+ interface ReadOnlySectionV2Props {
81
+ section: PageSectionV2;
82
+ viewport: DeviceViewport;
83
+ }
84
+
85
+ const ReadOnlySectionV2 = memo(function ReadOnlySectionV2({ section, viewport }: ReadOnlySectionV2Props) {
86
+ const base = section.settings || {} as PageSectionV2["settings"];
87
+ // Merge responsive overrides for layout properties at this viewport
88
+ const resolvedSettings = useMemo(() => {
89
+ const merged: Record<string, unknown> = { ...base };
90
+ for (const key of OVERRIDABLE_KEYS) {
91
+ merged[key] = getSectionV2SettingValue(section, viewport, key, (base as unknown as Record<string, unknown>)[key]);
92
+ }
93
+ return merged;
94
+ }, [section, viewport, base]);
95
+ const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
96
+
97
+ const gridColumns = base.grid_columns || 12;
98
+ const colGap = getSectionV2SettingValue(section, viewport, "col_gap", 20);
99
+ const rowGap = getSectionV2SettingValue(section, viewport, "row_gap", 20);
100
+ const effectiveCols = getEffectiveColumnsV2(section, viewport);
101
+
102
+ return (
103
+ <div style={{ ...layoutStyles, contentVisibility: "auto", containIntrinsicSize: "auto 200px" }}>
104
+ <div
105
+ style={{
106
+ display: "grid",
107
+ gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
108
+ columnGap: `${colGap}px`,
109
+ rowGap: `${rowGap}px`,
110
+ }}
111
+ >
112
+ {section.columns.map((col) => {
113
+ const eff = effectiveCols.find((ec) => ec._key === col._key);
114
+ const gridColumn = eff?.grid_column ?? col.grid_column;
115
+ const gridRow = eff?.grid_row ?? col.grid_row;
116
+ const span = eff?.span ?? col.span;
117
+ const colJustify = getColumnVerticalAlign(col.blocks || []);
118
+
119
+ return (
120
+ <div
121
+ key={col._key}
122
+ style={{
123
+ gridColumn: `${gridColumn} / span ${span}`,
124
+ gridRow,
125
+ display: "flex",
126
+ flexDirection: "column",
127
+ ...(colJustify ? { justifyContent: colJustify } : {}),
128
+ height: "100%",
129
+ minWidth: 0,
130
+ overflow: "hidden",
131
+ }}
132
+ >
133
+ {(col.blocks || []).map((block) => (
134
+ <div key={block._key} style={{ width: "100%", minWidth: 0 }}>
135
+ <BlockLivePreview block={block} viewport={viewport} />
136
+ </div>
137
+ ))}
138
+ </div>
139
+ );
140
+ })}
141
+ </div>
142
+ </div>
143
+ );
144
+ });
145
+
146
+ // ============================================
147
+ // Read-only parallax slide — renders a single slide's V2 grid content
148
+ // ============================================
149
+
150
+ interface ReadOnlyParallaxSlideProps {
151
+ slide: ParallaxSlideV2;
152
+ viewport: DeviceViewport;
153
+ deviceHeight: number;
154
+ }
155
+
156
+ const ReadOnlyParallaxSlide = memo(function ReadOnlyParallaxSlide({
157
+ slide,
158
+ viewport,
159
+ deviceHeight,
160
+ }: ReadOnlyParallaxSlideProps) {
161
+ const base = slide.section_settings || {} as ParallaxSlideV2["section_settings"];
162
+ const gridColumns = base.grid_columns || 12;
163
+ const colGap = base.col_gap ?? 20;
164
+ const rowGap = base.row_gap ?? 20;
165
+
166
+ return (
167
+ <div
168
+ className="relative"
169
+ style={{
170
+ minHeight: deviceHeight,
171
+ overflow: "hidden",
172
+ }}
173
+ >
174
+ {/* Background preview — image */}
175
+ {slide.background_type === "image" && slide.background_image && (
176
+ <div
177
+ className="absolute inset-0 bg-cover bg-center pointer-events-none"
178
+ style={{
179
+ backgroundImage: `url(/api/assets/${slide.background_image})`,
180
+ backgroundPosition: slide.background_position || "center center",
181
+ opacity: 0.15,
182
+ }}
183
+ />
184
+ )}
185
+ {/* Background preview — video (poster frame via <video> element) */}
186
+ {slide.background_type === "video" && slide.background_video && (
187
+ <video
188
+ src={`/api/assets/${slide.background_video}`}
189
+ muted
190
+ playsInline
191
+ autoPlay
192
+ loop
193
+ className="absolute inset-0 w-full h-full object-cover pointer-events-none"
194
+ style={{ opacity: 0.15 }}
195
+ />
196
+ )}
197
+ {/* Overlay — only when a background is set */}
198
+ {(slide.background_overlay_opacity ?? 0) > 0 && (slide.background_image || slide.background_video) && (
199
+ <div
200
+ className="absolute inset-0 pointer-events-none"
201
+ style={{
202
+ backgroundColor: slide.background_overlay_color || "#000000",
203
+ opacity: (slide.background_overlay_opacity ?? 0) / 100 * 0.3,
204
+ }}
205
+ />
206
+ )}
207
+ {/* V2 grid content — stretches to fill slide height (matches ParallaxGroupCanvas) */}
208
+ <div
209
+ className="relative"
210
+ style={{
211
+ minHeight: deviceHeight,
212
+ display: "flex",
213
+ flexDirection: "column",
214
+ }}
215
+ >
216
+ <div
217
+ style={{
218
+ display: "grid",
219
+ gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
220
+ gridTemplateRows: "1fr",
221
+ columnGap: `${colGap}px`,
222
+ rowGap: `${rowGap}px`,
223
+ flex: 1,
224
+ minHeight: 0,
225
+ }}
226
+ >
227
+ {slide.columns.map((col) => {
228
+ const colJustify = getColumnVerticalAlign(col.blocks || []);
229
+ return (
230
+ <div
231
+ key={col._key}
232
+ style={{
233
+ gridColumn: `${col.grid_column} / span ${col.span}`,
234
+ gridRow: col.grid_row,
235
+ display: "flex",
236
+ flexDirection: "column",
237
+ ...(colJustify ? { justifyContent: colJustify } : {}),
238
+ height: "100%",
239
+ minWidth: 0,
240
+ overflow: "hidden",
241
+ }}
242
+ >
243
+ {(col.blocks || []).map((block) => (
244
+ <div key={block._key} style={{ width: "100%", minWidth: 0 }}>
245
+ <BlockLivePreview block={block} viewport={viewport} />
246
+ </div>
247
+ ))}
248
+ </div>
249
+ );
250
+ })}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ );
255
+ });
256
+
257
+ // ============================================
258
+ // Read-only parallax group — renders all slides stacked
259
+ // ============================================
260
+
261
+ interface ReadOnlyParallaxGroupProps {
262
+ group: ParallaxGroup;
263
+ viewport: DeviceViewport;
264
+ }
265
+
266
+ const ReadOnlyParallaxGroup = memo(function ReadOnlyParallaxGroup({
267
+ group,
268
+ viewport,
269
+ }: ReadOnlyParallaxGroupProps) {
270
+ const deviceHeight = DEVICE_HEIGHTS[viewport];
271
+
272
+ return (
273
+ <div
274
+ style={{
275
+ borderRadius: 8,
276
+ border: "1px solid rgba(139, 92, 246, 0.15)",
277
+ overflow: "hidden",
278
+ }}
279
+ >
280
+ {/* Compact header */}
281
+ <div
282
+ style={{
283
+ background: "linear-gradient(135deg, #f3f0ff 0%, #ede5ff 100%)",
284
+ padding: "4px 8px",
285
+ borderBottom: "1px solid rgba(139, 92, 246, 0.1)",
286
+ }}
287
+ >
288
+ <span style={{ fontSize: 8, fontWeight: 600, color: "#8b5cf6" }}>
289
+ ▽ Parallax · {group.slides.length} slides
290
+ </span>
291
+ </div>
292
+ {/* Slides */}
293
+ {group.slides.map((slide, i) => (
294
+ <div
295
+ key={slide._key}
296
+ style={{
297
+ borderTop: i > 0 ? "1px solid rgba(139, 92, 246, 0.08)" : undefined,
298
+ }}
299
+ >
300
+ <ReadOnlyParallaxSlide
301
+ slide={slide}
302
+ viewport={viewport}
303
+ deviceHeight={deviceHeight}
304
+ />
305
+ </div>
306
+ ))}
307
+ </div>
308
+ );
309
+ });
310
+
311
+ // ============================================
312
+ // Read-only custom section instance — fetches and renders linked section data
313
+ // ============================================
314
+
315
+ interface ReadOnlyCustomSectionProps {
316
+ instance: CustomSectionInstance;
317
+ viewport: DeviceViewport;
318
+ }
319
+
320
+ const ReadOnlyCustomSection = memo(function ReadOnlyCustomSection({
321
+ instance,
322
+ viewport,
323
+ }: ReadOnlyCustomSectionProps) {
324
+ const cacheSettings = useBuilderStore((s) => s.cacheCustomSectionSettings);
325
+ const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
326
+
327
+ useEffect(() => {
328
+ let cancelled = false;
329
+ fetch(`/api/custom-sections/${instance.custom_section_id}`)
330
+ .then((res) => (res.ok ? res.json() : null))
331
+ .then((data) => {
332
+ if (!cancelled && data?.section) {
333
+ setSectionData(data.section);
334
+ // Cache base settings so SortableRow can merge with per-instance overrides
335
+ if (data.section.settings) {
336
+ cacheSettings(instance.custom_section_id, data.section.settings);
337
+ }
338
+ }
339
+ })
340
+ .catch(() => {});
341
+ return () => { cancelled = true; };
342
+ // eslint-disable-next-line react-hooks/exhaustive-deps
343
+ }, [instance.custom_section_id]);
344
+
345
+ if (!sectionData) {
346
+ return (
347
+ <div style={{ padding: "16px 0", textAlign: "center" }}>
348
+ <span style={{ fontSize: 9, color: "#bbb" }}>Loading section...</span>
349
+ </div>
350
+ );
351
+ }
352
+
353
+ // Merge per-instance overrides on top of base section settings
354
+ const mergedSection = instance.settings_overrides
355
+ ? { ...sectionData, settings: { ...sectionData.settings, ...instance.settings_overrides } }
356
+ : sectionData;
357
+
358
+ // Also merge responsive overrides for correct viewport-specific rendering
359
+ const withResponsive = instance.responsive_overrides
360
+ ? { ...mergedSection, responsive: { ...mergedSection.responsive, ...instance.responsive_overrides } }
361
+ : mergedSection;
362
+
363
+ return <ReadOnlySectionV2 section={withResponsive} viewport={viewport} />;
364
+ });
365
+
366
+ // ============================================
367
+ // ReadOnlyFrame — orchestrator
368
+ // ============================================
369
+
370
+ interface ReadOnlyFrameProps {
371
+ /** The device viewport this frame represents */
372
+ viewport: DeviceViewport;
373
+ }
374
+
375
+ export default function ReadOnlyFrame({ viewport }: ReadOnlyFrameProps) {
376
+ const rows = useBuilderStore((s) => s.rows);
377
+
378
+ if (rows.length === 0) {
379
+ return (
380
+ <div className="flex items-center justify-center min-h-[400px]">
381
+ <p className="text-[10px] text-neutral-300">No content</p>
382
+ </div>
383
+ );
384
+ }
385
+
386
+ return (
387
+ <div>
388
+ {rows.map((item) =>
389
+ isParallaxGroup(item) ? (
390
+ <ReadOnlyParallaxGroup
391
+ key={item._key}
392
+ group={item as ParallaxGroup}
393
+ viewport={viewport}
394
+ />
395
+ ) : isCustomSectionInstance(item) ? (
396
+ <ReadOnlyCustomSection
397
+ key={item._key}
398
+ instance={item as CustomSectionInstance}
399
+ viewport={viewport}
400
+ />
401
+ ) : isPageSectionV2(item) ? (
402
+ <ReadOnlySectionV2
403
+ key={item._key}
404
+ section={item as PageSectionV2}
405
+ viewport={viewport}
406
+ />
407
+ ) : isPageSection(item) ? (
408
+ <ReadOnlySection
409
+ key={item._key}
410
+ section={item as PageSection}
411
+ viewport={viewport}
412
+ />
413
+ ) : null
414
+ )}
415
+ </div>
416
+ );
417
+ }
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef, useEffect } from "react";
4
+ import { useBuilderStore } from "../../lib/builder/store";
5
+ import { getCsrfToken } from "../../lib/csrf-client";
6
+ import { ADMIN_ACCENT } from "../../lib/builder/constants";
7
+
8
+ // ============================================
9
+ // SectionEditorBar — Top bar for custom section editor mode
10
+ // ============================================
11
+ // Shows when editorMode === "customSection". Displays breadcrumbs,
12
+ // an editable section name, Cancel, Save & Exit, and Delete buttons.
13
+ // All text in English. Consistent with builder admin accent (ADMIN_ACCENT).
14
+
15
+ interface SectionEditorBarProps {
16
+ /** Called after save completes — receives the created/updated section info */
17
+ onSaveComplete?: (result: { id: string; slug: string; title: string }) => void;
18
+ }
19
+
20
+ export default function SectionEditorBar({ onSaveComplete }: SectionEditorBarProps) {
21
+ const customSectionSlug = useBuilderStore((s) => s.customSectionSlug);
22
+ const customSectionTitle = useBuilderStore((s) => s.customSectionTitle);
23
+ const isSaving = useBuilderStore((s) => s.isSaving);
24
+ const saveError = useBuilderStore((s) => s.saveError);
25
+ const isDirty = useBuilderStore((s) => s.isDirty);
26
+ const exitSectionEditor = useBuilderStore((s) => s.exitSectionEditor);
27
+ const saveSectionEditor = useBuilderStore((s) => s.saveSectionEditor);
28
+ const pageTitle = useBuilderStore((s) => s.pageTitle);
29
+ const pageType = useBuilderStore((s) => s.pageType);
30
+
31
+ const isEditing = !!customSectionSlug;
32
+ const [name, setName] = useState(customSectionTitle || "");
33
+ const [showCancelDialog, setShowCancelDialog] = useState(false);
34
+ const [showSaveDialog, setShowSaveDialog] = useState(false);
35
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
36
+ const [isDeleting, setIsDeleting] = useState(false);
37
+ const [deleteError, setDeleteError] = useState<string | null>(null);
38
+ const inputRef = useRef<HTMLInputElement>(null);
39
+ // Track whether the name was changed from its original value
40
+ const nameChanged = name.trim() !== (customSectionTitle || "").trim();
41
+
42
+ // Focus input on mount if creating new
43
+ useEffect(() => {
44
+ if (!isEditing && inputRef.current) {
45
+ inputRef.current.focus();
46
+ }
47
+ }, [isEditing]);
48
+
49
+ const handleCancel = useCallback(() => {
50
+ // m2 fix: only show warning dialog if there are actual unsaved changes (Session 110)
51
+ if (isDirty || nameChanged) {
52
+ setShowCancelDialog(true);
53
+ } else {
54
+ // No changes — exit immediately
55
+ exitSectionEditor();
56
+ }
57
+ }, [isDirty, nameChanged, exitSectionEditor]);
58
+
59
+ const confirmCancel = useCallback(() => {
60
+ setShowCancelDialog(false);
61
+ exitSectionEditor();
62
+ }, [exitSectionEditor]);
63
+
64
+ const handleSave = useCallback(() => {
65
+ if (!name.trim()) {
66
+ inputRef.current?.focus();
67
+ return;
68
+ }
69
+ if (isEditing) {
70
+ // Show warning dialog when editing existing section
71
+ setShowSaveDialog(true);
72
+ } else {
73
+ // Creating new — save directly
74
+ performSave();
75
+ }
76
+ }, [name, isEditing]); // eslint-disable-line react-hooks/exhaustive-deps
77
+
78
+ const performSave = useCallback(async () => {
79
+ setShowSaveDialog(false);
80
+ const result = await saveSectionEditor(name.trim());
81
+ if (result && onSaveComplete) {
82
+ onSaveComplete(result);
83
+ }
84
+ }, [name, saveSectionEditor, onSaveComplete]);
85
+
86
+ const handleDelete = useCallback(async () => {
87
+ if (!customSectionSlug) return;
88
+ setIsDeleting(true);
89
+ setDeleteError(null);
90
+
91
+ try {
92
+ const csrfToken = getCsrfToken();
93
+ const res = await fetch(`/api/admin/custom-sections/${customSectionSlug}`, {
94
+ method: "DELETE",
95
+ headers: {
96
+ ...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
97
+ },
98
+ });
99
+
100
+ if (!res.ok) {
101
+ const data = await res.json().catch(() => ({ error: "Delete failed" }));
102
+ throw new Error(data.error || `Delete failed (${res.status})`);
103
+ }
104
+
105
+ setShowDeleteDialog(false);
106
+ // Exit editor without marking page dirty
107
+ exitSectionEditor();
108
+ } catch (err) {
109
+ setDeleteError(err instanceof Error ? err.message : "Delete failed");
110
+ } finally {
111
+ setIsDeleting(false);
112
+ }
113
+ }, [customSectionSlug, exitSectionEditor]);
114
+
115
+ // Breadcrumb label for parent context
116
+ const parentLabel = pageType === "project" ? "Projects" : "Pages";
117
+ const parentListUrl = pageType === "project" ? "/admin/projects" : "/admin/pages";
118
+
119
+ return (
120
+ <>
121
+ <div
122
+ className="flex items-center justify-between px-4 py-2 border-b border-[#2a2a2a]"
123
+ style={{ backgroundColor: "#1a1a1a" }}
124
+ >
125
+ {/* Left: breadcrumbs + section name input */}
126
+ <div className="flex items-center gap-3">
127
+ {/* Breadcrumbs */}
128
+ <div className="flex items-center gap-1.5 text-xs mr-1">
129
+ <a
130
+ href={parentListUrl}
131
+ className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer"
132
+ >
133
+ {parentLabel}
134
+ </a>
135
+ <span className="text-[#444]">/</span>
136
+ <button
137
+ onClick={handleCancel}
138
+ className="text-[#666] hover:text-[#aaa] transition-colors cursor-pointer truncate max-w-[140px]"
139
+ title={`Back to ${pageTitle || "page"}`}
140
+ >
141
+ {pageTitle || "Page"}
142
+ </button>
143
+ <span className="text-[#444]">/</span>
144
+ <span className="text-[#999] font-medium">
145
+ {isEditing ? "Edit Section" : "New Section"}
146
+ </span>
147
+ </div>
148
+
149
+ <div className="w-px h-4 bg-[#3a3a3a]" />
150
+
151
+ <input
152
+ ref={inputRef}
153
+ type="text"
154
+ value={name}
155
+ onChange={(e) => setName(e.target.value)}
156
+ placeholder="Section name..."
157
+ className="bg-[#2a2a2a] text-white text-sm px-3 py-1.5 rounded border border-[#3a3a3a] focus:border-[#076bff] focus:outline-none w-64 transition-colors"
158
+ />
159
+ </div>
160
+
161
+ {/* Right: actions */}
162
+ <div className="flex items-center gap-2">
163
+ {saveError && (
164
+ <span className="text-xs text-red-400 mr-2">{saveError}</span>
165
+ )}
166
+ {/* Delete button — only for existing sections */}
167
+ {isEditing && (
168
+ <button
169
+ onClick={() => setShowDeleteDialog(true)}
170
+ disabled={isSaving}
171
+ className="px-3 py-1.5 text-sm text-red-400 hover:text-red-300 rounded border border-[#3a3a3a] hover:border-red-500/50 transition-colors disabled:opacity-50"
172
+ >
173
+ Delete
174
+ </button>
175
+ )}
176
+ <button
177
+ onClick={handleCancel}
178
+ disabled={isSaving}
179
+ className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors disabled:opacity-50"
180
+ >
181
+ Cancel
182
+ </button>
183
+ <button
184
+ onClick={handleSave}
185
+ disabled={isSaving || !name.trim()}
186
+ className="px-4 py-1.5 text-sm text-white rounded font-medium transition-colors disabled:opacity-50"
187
+ style={{
188
+ backgroundColor: isSaving ? "#555" : ADMIN_ACCENT,
189
+ }}
190
+ >
191
+ {isSaving ? "Saving..." : "Save & Exit"}
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ {/* Cancel confirmation dialog */}
197
+ {showCancelDialog && (
198
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
199
+ <div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
200
+ <h3 className="text-white text-sm font-medium mb-2">
201
+ Unsaved changes
202
+ </h3>
203
+ <p className="text-[#888] text-xs mb-4">
204
+ If you leave now, your changes will be lost.
205
+ </p>
206
+ <div className="flex justify-end gap-2">
207
+ <button
208
+ onClick={() => setShowCancelDialog(false)}
209
+ className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors"
210
+ >
211
+ Keep editing
212
+ </button>
213
+ <button
214
+ onClick={confirmCancel}
215
+ className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-500 rounded transition-colors"
216
+ >
217
+ Discard & exit
218
+ </button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ )}
223
+
224
+ {/* Save warning dialog (editing existing) */}
225
+ {showSaveDialog && (
226
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
227
+ <div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
228
+ <h3 className="text-white text-sm font-medium mb-2">
229
+ Save changes
230
+ </h3>
231
+ <p className="text-[#888] text-xs mb-4">
232
+ Changes will apply to all pages using this section.
233
+ </p>
234
+ <div className="flex justify-end gap-2">
235
+ <button
236
+ onClick={() => setShowSaveDialog(false)}
237
+ className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors"
238
+ >
239
+ Cancel
240
+ </button>
241
+ <button
242
+ onClick={performSave}
243
+ className="px-3 py-1.5 text-sm text-white rounded font-medium transition-colors"
244
+ style={{ backgroundColor: ADMIN_ACCENT }}
245
+ >
246
+ Save
247
+ </button>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ )}
252
+
253
+ {/* Delete confirmation dialog */}
254
+ {showDeleteDialog && (
255
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
256
+ <div className="bg-[#1a1a1a] rounded-lg border border-[#3a3a3a] p-6 max-w-sm shadow-xl">
257
+ <h3 className="text-white text-sm font-medium mb-2">
258
+ Delete section
259
+ </h3>
260
+ <p className="text-[#888] text-xs mb-4">
261
+ This will permanently delete &ldquo;{customSectionTitle || name}&rdquo;.
262
+ If any pages reference this section, deletion will be blocked.
263
+ </p>
264
+ {deleteError && (
265
+ <p className="text-xs text-red-400 mb-3">{deleteError}</p>
266
+ )}
267
+ <div className="flex justify-end gap-2">
268
+ <button
269
+ onClick={() => { setShowDeleteDialog(false); setDeleteError(null); }}
270
+ disabled={isDeleting}
271
+ className="px-3 py-1.5 text-sm text-[#aaa] hover:text-white rounded border border-[#3a3a3a] hover:border-[#555] transition-colors disabled:opacity-50"
272
+ >
273
+ Cancel
274
+ </button>
275
+ <button
276
+ onClick={handleDelete}
277
+ disabled={isDeleting}
278
+ className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-500 rounded transition-colors disabled:opacity-50"
279
+ >
280
+ {isDeleting ? "Deleting..." : "Delete"}
281
+ </button>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ )}
286
+ </>
287
+ );
288
+ }