@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,274 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ColorSwatchPicker — Dropdown color picker used in block editors.
5
+ * Shows the user's palette swatches + common colors + custom picker.
6
+ *
7
+ * Used in: SettingsPanel (row/block bg, border), TextBlockEditor (text color),
8
+ * CoverBlockEditor (text color), and any future color field.
9
+ *
10
+ * The dropdown uses a portal + fixed positioning so it is never clipped
11
+ * by ancestor `overflow: auto/hidden` containers (e.g. SettingsPanel).
12
+ */
13
+
14
+ import { useState, useRef, useEffect, useCallback } from "react";
15
+ import { createPortal } from "react-dom";
16
+ import ColorPicker, { isValidHex } from "./ColorPicker";
17
+ import type { ColorSwatch } from "../../lib/sanity/types";
18
+
19
+ // Common neutral colors always available
20
+ const COMMON_COLORS = [
21
+ "#ffffff", "#f5f5f5", "#e5e5e5", "#a3a3a3",
22
+ "#525252", "#262626", "#171717", "#000000",
23
+ ];
24
+
25
+ interface ColorSwatchPickerProps {
26
+ /** Current color value (hex string or empty) */
27
+ value: string;
28
+ /** Callback when color changes */
29
+ onChange: (hex: string) => void;
30
+ /** Palette swatches from global styles */
31
+ swatches?: ColorSwatch[];
32
+ /** Optional label */
33
+ label?: string;
34
+ /** Allow clearing the color */
35
+ allowClear?: boolean;
36
+ /** Compact inline mode (no popover, just the swatch row) */
37
+ inline?: boolean;
38
+ }
39
+
40
+ export default function ColorSwatchPicker({
41
+ value,
42
+ onChange,
43
+ swatches = [],
44
+ label,
45
+ allowClear = true,
46
+ inline = false,
47
+ }: ColorSwatchPickerProps) {
48
+ const [open, setOpen] = useState(false);
49
+ const [customOpen, setCustomOpen] = useState(false);
50
+ const containerRef = useRef<HTMLDivElement>(null);
51
+ const portalRef = useRef<HTMLDivElement>(null);
52
+ const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
53
+
54
+ // Compute dropdown position from trigger button's bounding rect
55
+ useEffect(() => {
56
+ if (!open || !containerRef.current) return;
57
+ const rect = containerRef.current.getBoundingClientRect();
58
+ // Position below the trigger, right-aligned
59
+ const panelWidth = 244; // min-w-[220px] + padding
60
+ let left = rect.right - panelWidth;
61
+ // Clamp to viewport left edge
62
+ if (left < 8) left = 8;
63
+ setDropdownPos({ top: rect.bottom + 4, left });
64
+ }, [open]);
65
+
66
+ // Close on outside click — check both container and portal
67
+ useEffect(() => {
68
+ if (!open) return;
69
+ const handler = (e: MouseEvent) => {
70
+ const target = e.target as Node;
71
+ const inContainer = containerRef.current?.contains(target);
72
+ const inPortal = portalRef.current?.contains(target);
73
+ if (!inContainer && !inPortal) {
74
+ setOpen(false);
75
+ setCustomOpen(false);
76
+ }
77
+ };
78
+ document.addEventListener("mousedown", handler);
79
+ return () => document.removeEventListener("mousedown", handler);
80
+ }, [open]);
81
+
82
+ // Close on scroll of any ancestor (reposition would be complex; just close)
83
+ useEffect(() => {
84
+ if (!open) return;
85
+ const handler = () => {
86
+ if (!containerRef.current) return;
87
+ // Recompute position on scroll instead of closing
88
+ const rect = containerRef.current.getBoundingClientRect();
89
+ const panelWidth = 244;
90
+ let left = rect.right - panelWidth;
91
+ if (left < 8) left = 8;
92
+ setDropdownPos({ top: rect.bottom + 4, left });
93
+ };
94
+ // Listen on capture phase to catch scrolls on any ancestor
95
+ window.addEventListener("scroll", handler, { capture: true, passive: true });
96
+ return () => window.removeEventListener("scroll", handler, { capture: true });
97
+ }, [open]);
98
+
99
+ const handleSelect = useCallback((hex: string) => {
100
+ onChange(hex);
101
+ if (!inline) {
102
+ setOpen(false);
103
+ setCustomOpen(false);
104
+ }
105
+ }, [onChange, inline]);
106
+
107
+ const handleClear = useCallback(() => {
108
+ onChange("");
109
+ setOpen(false);
110
+ }, [onChange]);
111
+
112
+ // ─── Trigger button (the colored swatch + hex label) ───
113
+ const trigger = (
114
+ <button
115
+ type="button"
116
+ onClick={() => setOpen(!open)}
117
+ className="flex items-center gap-2 px-1.5 py-1 rounded-lg border border-neutral-200 bg-white hover:border-neutral-300 transition-colors cursor-pointer w-full"
118
+ >
119
+ <div
120
+ className="w-6 h-6 rounded-md border border-neutral-200 shrink-0"
121
+ style={{
122
+ background: value && isValidHex(value) ? value : "transparent",
123
+ backgroundImage: !value ? "linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%), linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%)" : undefined,
124
+ backgroundSize: !value ? "6px 6px" : undefined,
125
+ backgroundPosition: !value ? "0 0, 3px 3px" : undefined,
126
+ }}
127
+ />
128
+ <span className="text-[11px] text-neutral-500 font-mono truncate">
129
+ {value ? value.toUpperCase() : "None"}
130
+ </span>
131
+ </button>
132
+ );
133
+
134
+ // ─── Dropdown panel ───
135
+ const panel = (
136
+ <div className="bg-white rounded-xl border border-neutral-200 p-3 shadow-xl min-w-[220px]">
137
+
138
+ {/* User palette swatches */}
139
+ {swatches.length > 0 && (
140
+ <div className="mb-3">
141
+ <div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-1.5">Palette</div>
142
+ <div className="flex flex-wrap gap-1.5">
143
+ {swatches.map((s, i) => (
144
+ <button
145
+ key={s._key || i}
146
+ onClick={() => handleSelect(s.hex)}
147
+ title={`${s.name}: ${s.hex}`}
148
+ className={`w-7 h-7 rounded-lg cursor-pointer transition-all ${
149
+ value === s.hex
150
+ ? "ring-2 ring-[#076bff] ring-offset-1 ring-offset-white"
151
+ : "border border-neutral-200 hover:border-neutral-400"
152
+ }`}
153
+ style={{ background: s.hex }}
154
+ />
155
+ ))}
156
+ </div>
157
+ </div>
158
+ )}
159
+
160
+ {/* Common colors */}
161
+ <div className="mb-3">
162
+ <div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-1.5">Common</div>
163
+ <div className="flex flex-wrap gap-1.5">
164
+ {COMMON_COLORS.map((c) => (
165
+ <button
166
+ key={c}
167
+ onClick={() => handleSelect(c)}
168
+ title={c}
169
+ className={`w-7 h-7 rounded-lg cursor-pointer transition-all ${
170
+ value === c
171
+ ? "ring-2 ring-[#076bff] ring-offset-1 ring-offset-white"
172
+ : "border border-neutral-200 hover:border-neutral-400"
173
+ }`}
174
+ style={{ background: c }}
175
+ />
176
+ ))}
177
+ </div>
178
+ </div>
179
+
180
+ {/* Custom color toggle */}
181
+ {!customOpen ? (
182
+ <button
183
+ onClick={() => setCustomOpen(true)}
184
+ className="w-full py-1.5 rounded-lg border border-dashed border-neutral-300 text-neutral-400 text-[10px] uppercase tracking-widest cursor-pointer hover:border-neutral-400 hover:text-neutral-600 transition-colors"
185
+ >
186
+ Custom color
187
+ </button>
188
+ ) : (
189
+ <div className="mt-1">
190
+ <ColorPicker
191
+ color={value || "#ffffff"}
192
+ onChange={(hex) => handleSelect(hex)}
193
+ onClose={() => setCustomOpen(false)}
194
+ confirmLabel="Apply"
195
+ />
196
+ </div>
197
+ )}
198
+
199
+ {/* Clear button */}
200
+ {allowClear && value && (
201
+ <button
202
+ onClick={handleClear}
203
+ className="w-full mt-2 py-1.5 rounded-lg border border-neutral-200 text-neutral-400 text-[10px] uppercase tracking-widest cursor-pointer hover:border-red-300 hover:text-red-500 transition-colors"
204
+ >
205
+ Clear color
206
+ </button>
207
+ )}
208
+ </div>
209
+ );
210
+
211
+ return (
212
+ <div ref={containerRef} className="relative">
213
+ {label && (
214
+ <label className="text-[10px] text-neutral-500 uppercase tracking-wider block mb-1">
215
+ {label}
216
+ </label>
217
+ )}
218
+ {trigger}
219
+ {open && typeof document !== "undefined" &&
220
+ createPortal(
221
+ <div
222
+ ref={portalRef}
223
+ className="fixed z-[9999]"
224
+ style={{ top: dropdownPos.top, left: dropdownPos.left }}
225
+ >
226
+ {panel}
227
+ </div>,
228
+ document.body
229
+ )
230
+ }
231
+ </div>
232
+ );
233
+ }
234
+
235
+ // ─── Hook: fetch palette swatches from admin styles API ───
236
+
237
+ let cachedSwatches: ColorSwatch[] | null = null;
238
+ let cachePromise: Promise<ColorSwatch[]> | null = null;
239
+
240
+ export function usePaletteSwatches(): ColorSwatch[] {
241
+ const [swatches, setSwatches] = useState<ColorSwatch[]>(cachedSwatches || []);
242
+
243
+ useEffect(() => {
244
+ if (cachedSwatches) {
245
+ setSwatches(cachedSwatches);
246
+ return;
247
+ }
248
+
249
+ if (!cachePromise) {
250
+ cachePromise = fetch("/api/admin/styles")
251
+ .then((res) => (res.ok ? res.json() : { styles: { colors: { swatches: [] } } }))
252
+ .then((data) => {
253
+ const s = data?.styles?.colors?.swatches || [];
254
+ cachedSwatches = s;
255
+ return s;
256
+ })
257
+ .catch(() => {
258
+ /* Color palette unavailable — use empty swatches */
259
+ cachedSwatches = [];
260
+ return [];
261
+ });
262
+ }
263
+
264
+ cachePromise.then((s) => setSwatches(s));
265
+ }, []);
266
+
267
+ return swatches;
268
+ }
269
+
270
+ /** Invalidate the cached swatches (call after saving palette) */
271
+ export function invalidatePaletteCache() {
272
+ cachedSwatches = null;
273
+ cachePromise = null;
274
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, type ReactNode } from "react";
4
+ import { useColumnDrag, type UseColumnDragReturn } from "./hooks/useColumnDrag";
5
+ import ColumnDragOverlay from "./ColumnDragOverlay";
6
+
7
+ // ============================================
8
+ // Context
9
+ // ============================================
10
+
11
+ const ColumnDragContext = createContext<UseColumnDragReturn | null>(null);
12
+
13
+ // ============================================
14
+ // Provider
15
+ // ============================================
16
+
17
+ interface ColumnDragProviderProps {
18
+ children: ReactNode;
19
+ }
20
+
21
+ export function ColumnDragProvider({ children }: ColumnDragProviderProps) {
22
+ const columnDrag = useColumnDrag();
23
+
24
+ return (
25
+ <ColumnDragContext.Provider value={columnDrag}>
26
+ {children}
27
+ {columnDrag.isDragging &&
28
+ columnDrag.overlayPosition &&
29
+ columnDrag.draggedSectionKey &&
30
+ columnDrag.draggedColumnKey && (
31
+ <ColumnDragOverlay
32
+ sectionKey={columnDrag.draggedSectionKey}
33
+ columnKey={columnDrag.draggedColumnKey}
34
+ position={columnDrag.overlayPosition}
35
+ />
36
+ )}
37
+ </ColumnDragContext.Provider>
38
+ );
39
+ }
40
+
41
+ // ============================================
42
+ // Consumer Hook
43
+ // ============================================
44
+
45
+ export function useColumnDragContext(): UseColumnDragReturn {
46
+ const context = useContext(ColumnDragContext);
47
+ if (!context) {
48
+ throw new Error("useColumnDragContext must be used within a ColumnDragProvider");
49
+ }
50
+ return context;
51
+ }
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { useBuilderStore } from "../../lib/builder/store";
6
+ import type { PageSectionV2 } from "../../lib/sanity/types";
7
+ import { isPageSectionV2 } from "../../lib/sanity/types";
8
+ import { BUILDER_BLUE } from "../../lib/builder/constants";
9
+
10
+ interface ColumnDragOverlayProps {
11
+ sectionKey: string;
12
+ columnKey: string;
13
+ position: { x: number; y: number };
14
+ }
15
+
16
+ const ColumnDragOverlay = memo(function ColumnDragOverlay({
17
+ sectionKey,
18
+ columnKey,
19
+ position,
20
+ }: ColumnDragOverlayProps) {
21
+ const rows = useBuilderStore((s) => s.rows);
22
+ const item = rows.find((r) => r._key === sectionKey);
23
+ if (!item || !isPageSectionV2(item)) return null;
24
+
25
+ const v2Section = item as PageSectionV2;
26
+ const col = v2Section.columns?.find((c) => c._key === columnKey);
27
+ if (!col) return null;
28
+
29
+ const blockCount = (col.blocks || []).length;
30
+ const gridColumns = v2Section.settings.grid_columns || 12;
31
+
32
+ const overlay = (
33
+ <div
34
+ style={{
35
+ position: "fixed",
36
+ left: position.x,
37
+ top: position.y,
38
+ transform: "translate(-50%, -50%)",
39
+ pointerEvents: "none",
40
+ zIndex: 99999,
41
+ }}
42
+ >
43
+ <div
44
+ style={{
45
+ width: 180,
46
+ minHeight: 80,
47
+ background: "rgba(7, 107, 255, 0.08)",
48
+ backdropFilter: "blur(8px)",
49
+ opacity: 0.85,
50
+ borderRadius: 8,
51
+ border: `2px solid ${BUILDER_BLUE}`,
52
+ boxShadow: "0 8px 32px rgba(7, 107, 255, 0.3)",
53
+ }}
54
+ >
55
+ {/* Column header badge */}
56
+ <div
57
+ style={{
58
+ display: "flex",
59
+ alignItems: "center",
60
+ gap: 8,
61
+ padding: "8px 12px",
62
+ borderBottom: "1px solid rgba(7, 107, 255, 0.2)",
63
+ }}
64
+ >
65
+ <svg width="10" height="10" viewBox="0 0 10 10" fill={BUILDER_BLUE}>
66
+ <circle cx="3" cy="3" r="1" />
67
+ <circle cx="7" cy="3" r="1" />
68
+ <circle cx="3" cy="7" r="1" />
69
+ <circle cx="7" cy="7" r="1" />
70
+ </svg>
71
+ <span style={{ fontSize: 12, color: "white", fontWeight: 500 }}>
72
+ Column {col.span}/{gridColumns}
73
+ </span>
74
+ </div>
75
+ {/* Block count indicators */}
76
+ <div style={{ padding: "8px 12px" }}>
77
+ {blockCount > 0 ? (
78
+ <>
79
+ {Array.from({ length: Math.min(blockCount, 3) }).map((_, i) => (
80
+ <div
81
+ key={i}
82
+ style={{
83
+ height: 12,
84
+ borderRadius: 4,
85
+ background: "rgba(255,255,255,0.1)",
86
+ marginBottom: i < Math.min(blockCount, 3) - 1 ? 6 : 0,
87
+ width: `${90 - i * 15}%`,
88
+ }}
89
+ />
90
+ ))}
91
+ {blockCount > 3 && (
92
+ <span style={{ fontSize: 10, color: "rgba(255,255,255,0.4)", marginTop: 4, display: "block" }}>
93
+ +{blockCount - 3} more
94
+ </span>
95
+ )}
96
+ </>
97
+ ) : (
98
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "8px 0" }}>
99
+ <span style={{ fontSize: 10, color: "rgba(255,255,255,0.3)" }}>Empty column</span>
100
+ </div>
101
+ )}
102
+ </div>
103
+ </div>
104
+ </div>
105
+ );
106
+
107
+ return createPortal(overlay, document.body);
108
+ });
109
+
110
+ export default ColumnDragOverlay;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CustomSectionInstanceCard — Builder canvas card for custom section instances.
5
+ *
6
+ * Renders a live preview of the linked section content in the builder canvas.
7
+ * Edit/Detach actions are in the SettingsPanel (not here).
8
+ *
9
+ * Session 108: Created as part of Custom Sections Phase 2.
10
+ * Session 110: M1 fix — auto-sync stale cached titles.
11
+ * Session 130: Removed violet header bar — Edit/Detach moved to SettingsPanel.
12
+ * Exposed sectionData via callback for SettingsPanel to use.
13
+ */
14
+
15
+ import { useState, useEffect } from "react";
16
+ import type { CustomSectionInstance, PageSectionV2 } from "../../lib/sanity/types";
17
+ import { useBuilderStore } from "../../lib/builder/store";
18
+ import SectionV2Canvas from "./SectionV2Canvas";
19
+
20
+ interface CustomSectionInstanceCardProps {
21
+ instance: CustomSectionInstance;
22
+ onAddBlockTarget?: (sectionKey: string, colKey: string, insertIndex?: number) => void;
23
+ /** Callback to expose fetched section data to parent (for SettingsPanel actions) */
24
+ onSectionDataLoaded?: (data: PageSectionV2 | null) => void;
25
+ }
26
+
27
+ export default function CustomSectionInstanceCard({
28
+ instance,
29
+ onAddBlockTarget,
30
+ onSectionDataLoaded,
31
+ }: CustomSectionInstanceCardProps) {
32
+ const cacheSettings = useBuilderStore((s) => s.cacheCustomSectionSettings);
33
+ const [sectionData, setSectionData] = useState<PageSectionV2 | null>(null);
34
+ const [loading, setLoading] = useState(true);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ // Fetch the section data for preview
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+ setLoading(true);
41
+ setError(null);
42
+
43
+ fetch(`/api/custom-sections/${instance.custom_section_id}`)
44
+ .then((res) => {
45
+ if (!res.ok) throw new Error("Section not found");
46
+ return res.json();
47
+ })
48
+ .then((data) => {
49
+ if (!cancelled && data.section) {
50
+ setSectionData(data.section);
51
+ onSectionDataLoaded?.(data.section);
52
+ // Cache base settings so SortableRow can merge with per-instance overrides
53
+ if (data.section.settings) {
54
+ cacheSettings(instance.custom_section_id, data.section.settings);
55
+ }
56
+ }
57
+ })
58
+ .catch(() => {
59
+ if (!cancelled) setError("Could not load section");
60
+ })
61
+ .finally(() => {
62
+ if (!cancelled) setLoading(false);
63
+ });
64
+
65
+ return () => { cancelled = true; };
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, [instance.custom_section_id]);
68
+
69
+ return (
70
+ <div className="relative">
71
+ {loading ? (
72
+ <div className="flex items-center justify-center py-12">
73
+ <p className="text-xs text-neutral-400 animate-pulse">Loading section...</p>
74
+ </div>
75
+ ) : error ? (
76
+ <div className="flex items-center justify-center py-12">
77
+ <p className="text-xs text-red-400">{error}</p>
78
+ </div>
79
+ ) : sectionData ? (
80
+ <div className="pointer-events-none opacity-90">
81
+ <SectionV2Canvas
82
+ section={(() => {
83
+ let merged = instance.settings_overrides
84
+ ? { ...sectionData, settings: { ...sectionData.settings, ...instance.settings_overrides } }
85
+ : sectionData;
86
+ if (instance.responsive_overrides) {
87
+ merged = { ...merged, responsive: { ...merged.responsive, ...instance.responsive_overrides } };
88
+ }
89
+ return merged;
90
+ })()}
91
+ onAddBlockTarget={onAddBlockTarget || (() => {})}
92
+ />
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type { DeviceViewport } from "../../lib/builder/types";
5
+ import { DEVICE_WIDTHS } from "../../lib/builder/types";
6
+ import { ADMIN_ACCENT } from "../../lib/builder/constants";
7
+
8
+ // ============================================
9
+ // DeviceFrame — Responsive preview frame
10
+ // ============================================
11
+ // Wraps content in a device-sized frame with header label.
12
+ // Active frame is editable; inactive frames are read-only mirrors.
13
+
14
+ const DEVICE_LABELS: Record<DeviceViewport, string> = {
15
+ desktop: "Desktop",
16
+ tablet: "Tablet",
17
+ phone: "Phone",
18
+ };
19
+
20
+ const DEVICE_ICONS: Record<DeviceViewport, ReactNode> = {
21
+ desktop: (
22
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
23
+ <rect x="1" y="1.5" width="10" height="7" rx="1" stroke="currentColor" strokeWidth="1.2" />
24
+ <path d="M4 10.5H8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
25
+ <path d="M6 8.5V10.5" stroke="currentColor" strokeWidth="1.2" />
26
+ </svg>
27
+ ),
28
+ tablet: (
29
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
30
+ <rect x="2" y="0.5" width="8" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
31
+ <circle cx="6" cy="9.5" r="0.6" fill="currentColor" />
32
+ </svg>
33
+ ),
34
+ phone: (
35
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
36
+ <rect x="3" y="0.5" width="6" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
37
+ <path d="M5 1.5H7" stroke="currentColor" strokeWidth="0.8" strokeLinecap="round" />
38
+ <circle cx="6" cy="9.5" r="0.5" fill="currentColor" />
39
+ </svg>
40
+ ),
41
+ };
42
+
43
+ interface DeviceFrameProps {
44
+ device: DeviceViewport;
45
+ isActive: boolean;
46
+ onActivate: () => void;
47
+ onDoubleClick?: () => void;
48
+ backgroundColor: string;
49
+ children: ReactNode;
50
+ }
51
+
52
+ export default function DeviceFrame({
53
+ device,
54
+ isActive,
55
+ onActivate,
56
+ onDoubleClick,
57
+ backgroundColor,
58
+ children,
59
+ }: DeviceFrameProps) {
60
+ const width = DEVICE_WIDTHS[device];
61
+ const label = DEVICE_LABELS[device];
62
+
63
+ return (
64
+ <div
65
+ className="relative flex-shrink-0"
66
+ style={{ width }}
67
+ >
68
+ {/* Frame header — click to activate */}
69
+ <button
70
+ type="button"
71
+ onClick={(e) => {
72
+ e.stopPropagation();
73
+ onActivate();
74
+ }}
75
+ onDoubleClick={(e) => {
76
+ e.stopPropagation();
77
+ onDoubleClick?.();
78
+ }}
79
+ className="flex items-center gap-2 w-full px-3 py-1.5 rounded-t-lg border border-b-0 text-left transition-colors"
80
+ style={{
81
+ backgroundColor: isActive ? "#ffffff" : "#fafafa",
82
+ borderColor: isActive ? ADMIN_ACCENT : "#d4d4d4",
83
+ cursor: isActive ? "default" : "pointer",
84
+ }}
85
+ >
86
+ <span
87
+ style={{ color: isActive ? ADMIN_ACCENT : "#a3a3a3" }}
88
+ className="transition-colors"
89
+ >
90
+ {DEVICE_ICONS[device]}
91
+ </span>
92
+ <span
93
+ className="text-[10px] font-medium transition-colors"
94
+ style={{ color: isActive ? ADMIN_ACCENT : "#737373" }}
95
+ >
96
+ {label}
97
+ </span>
98
+ <span className="text-[10px] text-neutral-400 ml-auto">
99
+ {width}px
100
+ </span>
101
+ </button>
102
+
103
+ {/* Frame content area — contain: layout style paint limits browser work to this subtree */}
104
+ <div
105
+ className="border rounded-b-lg transition-[border-color,box-shadow,opacity]"
106
+ style={{
107
+ width,
108
+ backgroundColor,
109
+ borderColor: isActive ? ADMIN_ACCENT : "#d4d4d4",
110
+ borderTopWidth: 0,
111
+ minHeight: 400,
112
+ boxShadow: isActive
113
+ ? "0 4px 24px rgba(7, 107, 255, 0.08), 0 1px 4px rgba(0,0,0,0.05)"
114
+ : "0 1px 4px rgba(0,0,0,0.04)",
115
+ opacity: isActive ? 1 : 0.75,
116
+ contain: "layout style",
117
+ }}
118
+ >
119
+ {children}
120
+ </div>
121
+ </div>
122
+ );
123
+ }