@morphika/andami 0.1.2

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 +50 -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 +212 -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,9 @@
1
+ "use client";
2
+
3
+ import { getSpacerPx } from "../editors/SpacerBlockEditor";
4
+ import type { SpacerBlock } from "../../../lib/sanity/types";
5
+
6
+ export default function LiveSpacerPreview({ block }: { block: SpacerBlock }) {
7
+ const height = getSpacerPx(block);
8
+ return <div style={{ height: `${height}px` }} />;
9
+ }
@@ -0,0 +1,198 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useCallback, useEffect } from "react";
4
+ import { useBuilderStore } from "../../../lib/builder/store";
5
+ import type { TextBlock, ContentBlock } from "../../../lib/sanity/types";
6
+
7
+ /** Hook to get page-level text color for default block text rendering */
8
+ export function usePageTextColor(): string {
9
+ return useBuilderStore((s) => s.pageSettings.text_color) || "#0a0a0a";
10
+ }
11
+
12
+ /** Resolve fontSize: supports new numeric px and legacy string enum */
13
+ function resolveTextFontSize(fontSize?: number | string): string {
14
+ if (typeof fontSize === "number") return `${fontSize}px`;
15
+ // Legacy string enum fallback
16
+ const legacyMap: Record<string, string> = {
17
+ small: "12px", base: "14px", large: "20px",
18
+ xl: "24px", "2xl": "32px", "3xl": "48px",
19
+ };
20
+ return legacyMap[fontSize || "base"] || "14px";
21
+ }
22
+
23
+ /** Resolve fontWeight: supports new string numbers and legacy names */
24
+ function resolveTextFontWeight(fw?: string): number {
25
+ if (!fw) return 400;
26
+ const num = parseInt(fw, 10);
27
+ if (!isNaN(num)) return num;
28
+ // Legacy name fallback
29
+ if (fw === "bold") return 700;
30
+ if (fw === "medium") return 500;
31
+ return 400;
32
+ }
33
+
34
+ export default function LiveTextEditor({ block, editable = false }: { block: TextBlock; editable?: boolean }) {
35
+ const pageTextColor = usePageTextColor();
36
+ const store = useBuilderStore();
37
+ const previewMode = useBuilderStore((s) => s.previewMode);
38
+ const isSelected = useBuilderStore((s) => s.selectedBlockKey) === block._key;
39
+ const editableRef = useRef<HTMLDivElement>(null);
40
+ const isComposingRef = useRef(false);
41
+ const snapshotPushedRef = useRef(false);
42
+
43
+ const plainText = Array.isArray(block.text)
44
+ ? block.text
45
+ .map(
46
+ (blk) =>
47
+ blk?.children
48
+ ?.map((c: { text?: string }) => c.text || "")
49
+ .join("") || ""
50
+ )
51
+ .join("\n")
52
+ : "";
53
+
54
+ const style = block.style || {};
55
+
56
+ // Debounce timer ref — commits text 500ms after last keystroke (not on every input)
57
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
58
+
59
+ // Cleanup debounce timer on unmount
60
+ useEffect(() => {
61
+ return () => {
62
+ if (debounceRef.current) clearTimeout(debounceRef.current);
63
+ };
64
+ }, []);
65
+
66
+ // Sync content when block text changes externally (undo/redo)
67
+ useEffect(() => {
68
+ if (!editableRef.current || document.activeElement === editableRef.current) return;
69
+ const currentHtml = editableRef.current.innerText || "";
70
+ if (currentHtml !== plainText) {
71
+ editableRef.current.innerText = plainText || "";
72
+ }
73
+ }, [plainText]);
74
+
75
+ const commitText = useCallback(() => {
76
+ if (!editableRef.current) return;
77
+ const raw = editableRef.current.innerText || "";
78
+ const lines = raw.split("\n");
79
+ const portableText = lines.map((line, i) => ({
80
+ _type: "block" as const,
81
+ _key: `pt_${i}`,
82
+ style: "normal" as const,
83
+ markDefs: [],
84
+ children: [
85
+ {
86
+ _type: "span" as const,
87
+ _key: `sp_${i}`,
88
+ text: line,
89
+ marks: [],
90
+ },
91
+ ],
92
+ }));
93
+ store.updateBlockDebounced(block._key, {
94
+ text: portableText,
95
+ } as Partial<ContentBlock>);
96
+ }, [block._key, store]);
97
+
98
+ const handleFocus = useCallback(() => {
99
+ if (!snapshotPushedRef.current) {
100
+ store._pushSnapshot();
101
+ snapshotPushedRef.current = true;
102
+ }
103
+ }, [store]);
104
+
105
+ const handleBlur = useCallback(() => {
106
+ // Flush any pending debounced commit immediately on blur
107
+ if (debounceRef.current) {
108
+ clearTimeout(debounceRef.current);
109
+ debounceRef.current = null;
110
+ }
111
+ snapshotPushedRef.current = false;
112
+ commitText();
113
+ }, [commitText]);
114
+
115
+ const handleInput = useCallback(() => {
116
+ if (isComposingRef.current) return;
117
+ // Clear any pending debounce and schedule a new one
118
+ if (debounceRef.current) clearTimeout(debounceRef.current);
119
+ debounceRef.current = setTimeout(() => {
120
+ commitText();
121
+ debounceRef.current = null;
122
+ }, 500);
123
+ }, [commitText]);
124
+
125
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
126
+ // Stop propagation so builder shortcuts don't fire while typing
127
+ if (!e.metaKey && !e.ctrlKey) {
128
+ e.stopPropagation();
129
+ }
130
+ // Allow Ctrl+S, Ctrl+Z, Ctrl+Shift+Z to pass through
131
+ if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "z" || e.key === "Z" || e.key === "y")) {
132
+ // Don't stop propagation for these
133
+ return;
134
+ }
135
+ }, []);
136
+
137
+ const cols = block.columns && block.columns > 1 ? block.columns : undefined;
138
+
139
+ const computedStyle: React.CSSProperties = {
140
+ fontSize: resolveTextFontSize(style.fontSize),
141
+ fontWeight: resolveTextFontWeight(style.fontWeight),
142
+ textAlign: style.alignment || "left",
143
+ color: style.color || pageTextColor,
144
+ opacity: style.opacity ?? 1,
145
+ lineHeight: style.lineHeight || "1.6",
146
+ letterSpacing: style.letterSpacing || "normal",
147
+ maxWidth: style.maxWidth || "none",
148
+ textTransform: style.textTransform || "none",
149
+ fontFamily: "inherit",
150
+ outline: "none",
151
+ whiteSpace: "pre-wrap",
152
+ wordBreak: "break-word",
153
+ minHeight: "1em",
154
+ // Multi-column layout: gap inherits global grid gutter
155
+ ...(cols ? {
156
+ columnCount: cols,
157
+ columnGap: "var(--grid-gutter, 24px)",
158
+ } : {}),
159
+ };
160
+
161
+ // Preview mode or read-only frame: static render
162
+ if (previewMode || !editable) {
163
+ if (!plainText) {
164
+ return (
165
+ <p className="text-neutral-400 italic text-sm py-4 text-center">
166
+ Empty text block
167
+ </p>
168
+ );
169
+ }
170
+ return (
171
+ <div style={computedStyle}>
172
+ {plainText.split("\n").map((line, i) => (
173
+ <p key={i} className={i > 0 ? "mt-3" : ""}>
174
+ {line || "\u00A0"}
175
+ </p>
176
+ ))}
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // Design mode: contentEditable inline editing
182
+ return (
183
+ <div
184
+ ref={editableRef}
185
+ contentEditable
186
+ suppressContentEditableWarning
187
+ onFocus={handleFocus}
188
+ onBlur={handleBlur}
189
+ onInput={handleInput}
190
+ onKeyDown={handleKeyDown}
191
+ onCompositionStart={() => { isComposingRef.current = true; }}
192
+ onCompositionEnd={() => { isComposingRef.current = false; commitText(); }}
193
+ style={computedStyle}
194
+ data-placeholder="Type something..."
195
+ className={`${!plainText && !isSelected ? "before:content-[attr(data-placeholder)] before:text-neutral-400 before:italic before:pointer-events-none" : ""}`}
196
+ />
197
+ );
198
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { adminAssetUrl } from "../../../lib/assets";
4
+ import type { VideoBlock } from "../../../lib/sanity/types";
5
+
6
+ /**
7
+ * LiveVideoPreview — Static placeholder for builder canvas.
8
+ *
9
+ * Shows the correct aspect ratio + a lightweight thumbnail instead of
10
+ * loading real Vimeo/YouTube iframes or streaming MP4 files.
11
+ * This eliminates all video bandwidth in the builder.
12
+ *
13
+ * Thumbnail sources:
14
+ * - Vimeo: vumbnail.com (lightweight, no API key needed)
15
+ * - YouTube: ytimg.com (standard Google CDN)
16
+ * - MP4/URL: poster image if set, otherwise generic placeholder
17
+ */
18
+ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
19
+ if (!block.url_or_path) {
20
+ return (
21
+ <div className="border border-dashed border-neutral-700 rounded bg-neutral-900/50 flex items-center justify-center py-12">
22
+ <span className="text-neutral-600 text-xs">No video set</span>
23
+ </div>
24
+ );
25
+ }
26
+
27
+ const aspectMap: Record<string, string> = {
28
+ "16:9": "56.25%",
29
+ "21:9": "42.86%",
30
+ "4:3": "75%",
31
+ auto: "56.25%",
32
+ };
33
+ const paddingBottom = aspectMap[block.aspect_ratio || "16:9"] || "56.25%";
34
+ const widthStyle = block.width === "contained" ? "75%" : "100%";
35
+
36
+ // Resolve thumbnail URL based on video type (no iframes, no streaming)
37
+ let thumbnailUrl: string | null = null;
38
+ let videoLabel = (block.video_type || "video").toUpperCase();
39
+
40
+ if (block.video_type === "vimeo") {
41
+ const match = block.url_or_path.match(/vimeo\.com\/(\d+)/);
42
+ if (match) {
43
+ thumbnailUrl = `https://vumbnail.com/${match[1]}.jpg`;
44
+ videoLabel = "VIMEO";
45
+ }
46
+ } else if (block.video_type === "youtube") {
47
+ const match = block.url_or_path.match(
48
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/
49
+ );
50
+ if (match) {
51
+ thumbnailUrl = `https://i.ytimg.com/vi/${match[1]}/hqdefault.jpg`;
52
+ videoLabel = "YOUTUBE";
53
+ }
54
+ } else if (block.poster) {
55
+ // MP4/URL with poster — use poster image via admin proxy
56
+ thumbnailUrl = adminAssetUrl(block.poster);
57
+ videoLabel = block.video_type === "mp4" ? "MP4" : "VIDEO";
58
+ }
59
+
60
+ const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
61
+
62
+ return (
63
+ <div style={{ width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
64
+ <div style={{ position: "relative", paddingBottom, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
65
+ {thumbnailUrl ? (
66
+ <>
67
+ {/* eslint-disable-next-line @next/next/no-img-element */}
68
+ <img
69
+ src={thumbnailUrl}
70
+ alt="Video thumbnail"
71
+ loading="lazy"
72
+ className="absolute inset-0 w-full h-full object-cover"
73
+ />
74
+ {/* Play icon + type badge overlay */}
75
+ <div className="absolute inset-0 flex items-center justify-center">
76
+ <div className="w-14 h-14 rounded-full bg-black/50 flex items-center justify-center">
77
+ <span className="text-white text-xl ml-0.5">&#9654;</span>
78
+ </div>
79
+ </div>
80
+ <div className="absolute bottom-2 left-2 px-1.5 py-0.5 rounded bg-black/60 text-[10px] text-neutral-300 font-mono">
81
+ {videoLabel}
82
+ </div>
83
+ </>
84
+ ) : (
85
+ /* Generic placeholder — no thumbnail available */
86
+ <div className="absolute inset-0 bg-neutral-900 flex items-center justify-center">
87
+ <div className="text-center">
88
+ <span className="text-neutral-600 text-3xl block mb-2">&#9654;</span>
89
+ <span className="text-neutral-600 text-xs font-mono">
90
+ {videoLabel}
91
+ </span>
92
+ </div>
93
+ </div>
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,10 @@
1
+ export { default as LiveTextEditor } from "./LiveTextEditor";
2
+ export { usePageTextColor } from "./LiveTextEditor";
3
+ export { default as LiveImagePreview } from "./LiveImagePreview";
4
+ export { default as LiveImageGridPreview } from "./LiveImageGridPreview";
5
+ export { default as LiveVideoPreview } from "./LiveVideoPreview";
6
+ export { default as LiveSpacerPreview } from "./LiveSpacerPreview";
7
+ export { default as LiveButtonPreview } from "./LiveButtonPreview";
8
+ export { default as LiveCoverPreview } from "./LiveCoverPreview";
9
+ export { default as LiveProjectGridPreview } from "./LiveProjectGridPreview";
10
+ export { ThumbBadge, LivePlaceholder, useProjectThumbnails, ProjectGridCard } from "./shared";
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useThumbStatus } from "../../../lib/contexts/ThumbStatusContext";
5
+ import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
6
+
7
+ // ============================================
8
+ // Thumbnail status badge (builder-only)
9
+ // ============================================
10
+
11
+ /** Tiny green/amber dot showing thumbnail availability on image blocks. */
12
+ export function ThumbBadge({ assetPath }: { assetPath: string }) {
13
+ const { hasThumb } = useThumbStatus();
14
+ const status = hasThumb(assetPath);
15
+ // undefined = not raster or unknown — don't show badge
16
+ if (status === undefined) return null;
17
+ return (
18
+ <span
19
+ title={status ? "Thumbnail available" : "No thumbnail — full resolution"}
20
+ className="absolute bottom-1.5 right-1.5 z-10 flex items-center justify-center"
21
+ style={{
22
+ width: 16,
23
+ height: 16,
24
+ borderRadius: "50%",
25
+ backgroundColor: status ? "rgba(34,197,94,0.8)" : "rgba(245,158,11,0.8)",
26
+ pointerEvents: "auto",
27
+ }}
28
+ >
29
+ {status ? (
30
+ // Checkmark
31
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
32
+ <path d="M2 5.5L4 7.5L8 3" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
33
+ </svg>
34
+ ) : (
35
+ // Info dot
36
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
37
+ <circle cx="5" cy="3.5" r="1" fill="white" />
38
+ <rect x="4.25" y="5" width="1.5" height="3" rx="0.5" fill="white" />
39
+ </svg>
40
+ )}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ // ============================================
46
+ // Shared project thumbnail hook
47
+ // ============================================
48
+
49
+ /** Shared hook: fetch project thumbnails from /api/projects for builder previews */
50
+ export function useProjectThumbnails(slugs: string[]) {
51
+ const [thumbMap, setThumbMap] = useState<Map<string, string | undefined>>(new Map());
52
+
53
+ useEffect(() => {
54
+ if (!slugs.length) return;
55
+
56
+ fetch("/api/projects")
57
+ .then((r) => (r.ok ? r.json() : { projects: [] }))
58
+ .then((data) => {
59
+ const map = new Map<string, string | undefined>();
60
+ for (const proj of data.projects || []) {
61
+ const s = proj.slug?.current || proj.slug;
62
+ if (s && slugs.includes(s)) {
63
+ map.set(s, proj.thumbnail_path || undefined);
64
+ }
65
+ }
66
+ setThumbMap(map);
67
+ })
68
+ .catch(() => { /* silent */ });
69
+ }, [slugs.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
70
+
71
+ return thumbMap;
72
+ }
73
+
74
+ // ============================================
75
+ // Shared project grid card
76
+ // ============================================
77
+
78
+ /**
79
+ * Project card with optional thumbnail background.
80
+ * Supports two sizing modes:
81
+ * - `aspectRatio` (preferred): CSS aspect-ratio for responsive sizing that matches public site
82
+ * - `height` (legacy fallback): fixed px height
83
+ */
84
+ export function ProjectGridCard({
85
+ slug,
86
+ thumbPath,
87
+ customThumb,
88
+ height,
89
+ aspectRatio,
90
+ borderRadius,
91
+ style,
92
+ className,
93
+ }: {
94
+ slug: string;
95
+ thumbPath?: string;
96
+ customThumb?: string;
97
+ height?: number;
98
+ aspectRatio?: string;
99
+ borderRadius?: string;
100
+ style?: React.CSSProperties;
101
+ className?: string;
102
+ }) {
103
+ const imgPath = customThumb || thumbPath;
104
+ const src = imgPath ? adminThumbUrl(imgPath) : undefined;
105
+ const [fallback, setFallback] = useState(false);
106
+ const fullSrc = imgPath ? adminAssetUrl(imgPath) : undefined;
107
+
108
+ return (
109
+ <div
110
+ className={`relative overflow-hidden flex items-end ${className || ""}`}
111
+ style={{
112
+ ...(aspectRatio ? { aspectRatio } : { height: height || 160 }),
113
+ borderRadius: borderRadius ? `${borderRadius}px` : undefined,
114
+ backgroundColor: "#e5e5e5",
115
+ ...style,
116
+ }}
117
+ >
118
+ {src && (
119
+ // eslint-disable-next-line @next/next/no-img-element
120
+ <img
121
+ src={fallback && fullSrc ? fullSrc : src}
122
+ alt=""
123
+ className="absolute inset-0 w-full h-full object-cover"
124
+ onError={() => { if (!fallback) setFallback(true); }}
125
+ draggable={false}
126
+ />
127
+ )}
128
+ <span
129
+ className="relative z-[1] text-[10px] truncate px-3 pb-2"
130
+ style={{ color: src ? "rgba(255,255,255,0.8)" : "#a3a3a3", textShadow: src ? "0 1px 3px rgba(0,0,0,0.5)" : "none" }}
131
+ >
132
+ {slug}
133
+ </span>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ // ============================================
139
+ // Placeholder for unknown block types
140
+ // ============================================
141
+
142
+ export function LivePlaceholder({ type }: { type: string }) {
143
+ const label = type
144
+ .replace("Block", "")
145
+ .replace(/([A-Z])/g, " $1")
146
+ .trim();
147
+
148
+ return (
149
+ <div className="border border-dashed border-neutral-700 rounded bg-neutral-900/30 p-6 flex items-center justify-center">
150
+ <span className="text-xs text-neutral-500">{label}</span>
151
+ </div>
152
+ );
153
+ }