@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,206 @@
1
+ "use client";
2
+
3
+ import { useBuilderStore } from "../../../lib/builder/store";
4
+ import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
5
+ import type { ImageBlock, ContentBlock } from "../../../lib/sanity/types";
6
+ import {
7
+ SettingsField,
8
+ SettingsSection,
9
+ StyledCheckbox,
10
+ AssetPathInput,
11
+ ViewportBadge,
12
+ ResponsiveField,
13
+ useActiveViewport,
14
+ INPUT_CLASS,
15
+ SELECT_CLASS,
16
+ } from "./shared";
17
+
18
+ interface Props {
19
+ block: ImageBlock;
20
+ }
21
+
22
+ export default function ImageBlockEditor({ block }: Props) {
23
+ const store = useBuilderStore();
24
+ const viewport = useActiveViewport();
25
+
26
+ const snapshotOnFocus = () => store._pushSnapshot();
27
+
28
+ // Responsive-aware update for layout/appearance properties
29
+ const updateResponsive = (property: string, value: unknown) => {
30
+ if (viewport === "desktop") {
31
+ store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
32
+ } else {
33
+ const overrides = setResponsiveOverride(block, viewport, property, value);
34
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
35
+ }
36
+ };
37
+
38
+ const resetOverride = (property: string) => {
39
+ const overrides = setResponsiveOverride(block, viewport, property, undefined);
40
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
41
+ };
42
+
43
+ // Direct update (base block, not responsive)
44
+ const update = (updates: Partial<ImageBlock>) => {
45
+ store.updateBlock(block._key, updates as Partial<ContentBlock>);
46
+ };
47
+
48
+ const updateDebounced = (updates: Partial<ImageBlock>) => {
49
+ store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
50
+ };
51
+
52
+ // Effective values for the active viewport
53
+ const effectiveWidth = getEffectiveValue<string>(
54
+ block, viewport, "width", block.width || "full"
55
+ );
56
+ const effectiveAspect = getEffectiveValue<string>(
57
+ block, viewport, "aspect_ratio", block.aspect_ratio || "auto"
58
+ );
59
+
60
+ return (
61
+ <>
62
+ <ViewportBadge />
63
+
64
+ <SettingsSection title="Source" defaultOpen>
65
+ <SettingsField label="Asset Path" hint="Relative path from seed URL">
66
+ <AssetPathInput
67
+ value={block.asset_path || ""}
68
+ onFocus={snapshotOnFocus}
69
+ onChange={(v) => updateDebounced({ asset_path: v })}
70
+ placeholder="projects/slug/image.jpg"
71
+ filterType="image"
72
+ />
73
+ </SettingsField>
74
+
75
+ <SettingsField label="Alt Text">
76
+ <input
77
+ type="text"
78
+ value={block.alt || ""}
79
+ onFocus={snapshotOnFocus}
80
+ onChange={(e) => updateDebounced({ alt: e.target.value })}
81
+ className={INPUT_CLASS}
82
+ placeholder="Describe the image"
83
+ />
84
+ </SettingsField>
85
+
86
+ <SettingsField label="Caption">
87
+ <input
88
+ type="text"
89
+ value={block.caption || ""}
90
+ onFocus={snapshotOnFocus}
91
+ onChange={(e) => updateDebounced({ caption: e.target.value })}
92
+ className={INPUT_CLASS}
93
+ placeholder="Optional caption"
94
+ />
95
+ </SettingsField>
96
+ </SettingsSection>
97
+
98
+ <SettingsSection title="Layout">
99
+ <ResponsiveField
100
+ label="Width"
101
+ block={block}
102
+ property="width"
103
+ onReset={() => resetOverride("width")}
104
+ >
105
+ <div className="flex gap-1">
106
+ {(
107
+ [
108
+ { value: "full", label: "100%" },
109
+ { value: "contained", label: "75%" },
110
+ { value: "small", label: "50%" },
111
+ ] as const
112
+ ).map((opt) => (
113
+ <button
114
+ key={opt.value}
115
+ onClick={() => updateResponsive("width", opt.value)}
116
+ className={`flex-1 rounded border py-1 text-xs transition-colors ${
117
+ effectiveWidth === opt.value
118
+ ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
119
+ : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
120
+ }`}
121
+ >
122
+ {opt.label}
123
+ </button>
124
+ ))}
125
+ </div>
126
+ </ResponsiveField>
127
+
128
+ <ResponsiveField
129
+ label="Aspect Ratio"
130
+ block={block}
131
+ property="aspect_ratio"
132
+ onReset={() => resetOverride("aspect_ratio")}
133
+ >
134
+ <select
135
+ value={effectiveAspect}
136
+ onChange={(e) =>
137
+ updateResponsive("aspect_ratio", e.target.value)
138
+ }
139
+ className={SELECT_CLASS}
140
+ >
141
+ <option value="auto">Auto</option>
142
+ <option value="16:9">16:9</option>
143
+ <option value="4:3">4:3</option>
144
+ <option value="1:1">1:1</option>
145
+ <option value="21:9">21:9</option>
146
+ </select>
147
+ </ResponsiveField>
148
+ </SettingsSection>
149
+
150
+ <SettingsSection title="Appearance">
151
+ <ResponsiveField
152
+ label="Border Radius"
153
+ block={block}
154
+ property="border_radius"
155
+ onReset={() => resetOverride("border_radius")}
156
+ >
157
+ <div className="flex items-center gap-1.5">
158
+ <input
159
+ type="number"
160
+ value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
161
+ onFocus={snapshotOnFocus}
162
+ onChange={(e) => {
163
+ store._pushSnapshot();
164
+ updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
165
+ }}
166
+ className={INPUT_CLASS}
167
+ placeholder="0"
168
+ min={0}
169
+ />
170
+ <span className="text-[10px] text-neutral-400 shrink-0">px</span>
171
+ </div>
172
+ </ResponsiveField>
173
+
174
+ <ResponsiveField
175
+ label="Shadow"
176
+ block={block}
177
+ property="shadow"
178
+ onReset={() => resetOverride("shadow")}
179
+ >
180
+ <button
181
+ type="button"
182
+ onClick={() => {
183
+ const effectiveShadow = getEffectiveValue<boolean>(block, viewport, "shadow", block.shadow || false);
184
+ updateResponsive("shadow", !effectiveShadow);
185
+ }}
186
+ className={`relative w-8 h-[18px] rounded-full transition-colors ${
187
+ getEffectiveValue<boolean>(block, viewport, "shadow", block.shadow || false) ? "bg-[#076bff]" : "bg-neutral-200 hover:bg-neutral-300"
188
+ }`}
189
+ >
190
+ <span
191
+ className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow-sm transition-transform ${
192
+ getEffectiveValue<boolean>(block, viewport, "shadow", block.shadow || false) ? "left-[16px]" : "left-[2px]"
193
+ }`}
194
+ />
195
+ </button>
196
+ </ResponsiveField>
197
+
198
+ <StyledCheckbox
199
+ label="Lazy Loading"
200
+ checked={block.lazy !== false}
201
+ onChange={(checked) => update({ lazy: checked })}
202
+ />
203
+ </SettingsSection>
204
+ </>
205
+ );
206
+ }
@@ -0,0 +1,386 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useBuilderStore } from "../../../lib/builder/store";
5
+ import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
6
+ import type { ImageGridBlock, ContentBlock } from "../../../lib/sanity/types";
7
+ import AssetBrowser from "../AssetBrowser";
8
+ import {
9
+ SettingsField,
10
+ SettingsSection,
11
+ StyledInput,
12
+ ViewportBadge,
13
+ ResponsiveField,
14
+ useActiveViewport,
15
+ SELECT_CLASS,
16
+ } from "./shared";
17
+
18
+ interface Props {
19
+ block: ImageGridBlock;
20
+ }
21
+
22
+ // ============================================
23
+ // Thumbnail preview with delete-on-hover
24
+ // ============================================
25
+
26
+ function ImageThumb({
27
+ assetPath,
28
+ index,
29
+ onRemove,
30
+ }: {
31
+ assetPath: string;
32
+ index: number;
33
+ onRemove: () => void;
34
+ }) {
35
+ const [hovered, setHovered] = useState(false);
36
+ const src = assetPath
37
+ ? `/api/admin/assets/file?path=${encodeURIComponent(assetPath)}`
38
+ : "";
39
+
40
+ return (
41
+ <div
42
+ className="relative aspect-square rounded-lg overflow-hidden bg-neutral-100 cursor-default group"
43
+ onMouseEnter={() => setHovered(true)}
44
+ onMouseLeave={() => setHovered(false)}
45
+ >
46
+ {assetPath ? (
47
+ // eslint-disable-next-line @next/next/no-img-element
48
+ <img
49
+ src={src}
50
+ alt={`Image ${index + 1}`}
51
+ className="w-full h-full object-cover"
52
+ loading="lazy"
53
+ />
54
+ ) : (
55
+ <div className="w-full h-full flex items-center justify-center text-neutral-300">
56
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
57
+ <rect x="3" y="3" width="18" height="18" rx="2" />
58
+ <circle cx="8.5" cy="8.5" r="1.5" />
59
+ <polyline points="21 15 16 10 5 21" />
60
+ </svg>
61
+ </div>
62
+ )}
63
+
64
+ {/* Delete overlay */}
65
+ {hovered && (
66
+ <div className="absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity">
67
+ <button
68
+ onClick={(e) => {
69
+ e.stopPropagation();
70
+ onRemove();
71
+ }}
72
+ className="w-7 h-7 rounded-full bg-white/90 hover:bg-[var(--admin-error)] hover:text-white flex items-center justify-center transition-colors text-neutral-600"
73
+ title="Remove image"
74
+ >
75
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
76
+ <polyline points="3 6 5 6 21 6" />
77
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
78
+ </svg>
79
+ </button>
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ // ============================================
87
+ // Main Editor
88
+ // ============================================
89
+
90
+ export default function ImageGridBlockEditor({ block }: Props) {
91
+ const store = useBuilderStore();
92
+ const viewport = useActiveViewport();
93
+ const images = block.images || [];
94
+ const [browserOpen, setBrowserOpen] = useState(false);
95
+
96
+ const snapshotOnFocus = () => store._pushSnapshot();
97
+
98
+ // Responsive update helper
99
+ const updateResponsive = (property: string, value: unknown) => {
100
+ if (viewport === "desktop") {
101
+ store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
102
+ } else {
103
+ const overrides = setResponsiveOverride(block, viewport, property, value);
104
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
105
+ }
106
+ };
107
+
108
+ const resetOverride = (property: string) => {
109
+ const overrides = setResponsiveOverride(block, viewport, property, undefined);
110
+ store.updateBlock(block._key, overrides as Partial<ContentBlock>);
111
+ };
112
+
113
+ // Direct update (always base block — for images content)
114
+ const update = (updates: Partial<ImageGridBlock>) => {
115
+ store.updateBlock(block._key, updates as Partial<ContentBlock>);
116
+ };
117
+
118
+ const updateDebounced = (updates: Partial<ImageGridBlock>) => {
119
+ store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
120
+ };
121
+
122
+ // Add multiple images at once from browser
123
+ const addImages = (paths: string[]) => {
124
+ const newImages = paths.map((path) => ({ asset_path: path, alt: "" }));
125
+ update({ images: [...images, ...newImages] });
126
+ };
127
+
128
+ const removeImage = (index: number) => {
129
+ snapshotOnFocus();
130
+ update({ images: images.filter((_, i) => i !== index) });
131
+ };
132
+
133
+ // Effective values for current viewport
134
+ const effectiveHGutter = getEffectiveValue<number>(
135
+ block, viewport, "h_gutter", block.h_gutter ?? 10
136
+ );
137
+ const effectiveVGutter = getEffectiveValue<number>(
138
+ block, viewport, "v_gutter", block.v_gutter ?? 10
139
+ );
140
+ const effectiveImagesPerRow = getEffectiveValue<number>(
141
+ block, viewport, "images_per_row", block.images_per_row ?? 2
142
+ );
143
+ const effectiveRandomGrid = getEffectiveValue<string>(
144
+ block, viewport, "random_grid", block.random_grid ?? "disabled"
145
+ );
146
+ const effectiveRandomSeed = getEffectiveValue<number>(
147
+ block, viewport, "random_seed", block.random_seed ?? 1
148
+ );
149
+ const effectiveLightbox = getEffectiveValue<boolean>(
150
+ block, viewport, "lightbox", block.lightbox ?? false
151
+ );
152
+ const effectiveObjectFit = getEffectiveValue<string>(
153
+ block, viewport, "object_fit", block.object_fit ?? "cover"
154
+ );
155
+
156
+ return (
157
+ <>
158
+ <ViewportBadge />
159
+
160
+ {/* ====== Images Section ====== */}
161
+ <SettingsSection title="Images" defaultOpen>
162
+ {/* Thumbnail grid preview */}
163
+ {images.length > 0 && (
164
+ <div className="grid grid-cols-4 gap-1.5 mb-3">
165
+ {images.map((img, i) => (
166
+ <ImageThumb
167
+ key={i}
168
+ assetPath={img.asset_path}
169
+ index={i}
170
+ onRemove={() => removeImage(i)}
171
+ />
172
+ ))}
173
+ </div>
174
+ )}
175
+
176
+ {images.length === 0 && (
177
+ <p className="text-[11px] text-neutral-500 italic mb-2">
178
+ No images yet — add images to build your grid.
179
+ </p>
180
+ )}
181
+
182
+ {/* Add Images button — opens browser in multi-select mode */}
183
+ <button
184
+ onClick={() => setBrowserOpen(true)}
185
+ className="w-full rounded-lg border border-dashed border-neutral-300 py-3 text-xs text-neutral-500 hover:border-[#076bff] hover:text-neutral-900 transition-colors flex items-center justify-center gap-2"
186
+ >
187
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-neutral-400">
188
+ <rect x="3" y="3" width="18" height="18" rx="2" />
189
+ <circle cx="8.5" cy="8.5" r="1.5" />
190
+ <polyline points="21 15 16 10 5 21" />
191
+ </svg>
192
+ Add Images to Gallery
193
+ </button>
194
+
195
+ <AssetBrowser
196
+ open={browserOpen}
197
+ multiSelect
198
+ onSelect={(path) => {
199
+ addImages([path]);
200
+ setBrowserOpen(false);
201
+ }}
202
+ onSelectMultiple={(paths) => {
203
+ addImages(paths);
204
+ setBrowserOpen(false);
205
+ }}
206
+ onClose={() => setBrowserOpen(false)}
207
+ filterType="image"
208
+ />
209
+ </SettingsSection>
210
+
211
+ {/* ====== Settings Section ====== */}
212
+ <SettingsSection title="Settings" defaultOpen>
213
+ {/* H-Gutter & V-Gutter side by side */}
214
+ <div className="flex gap-3">
215
+ <ResponsiveField
216
+ label="H-Gutter"
217
+ block={block}
218
+ property="h_gutter"
219
+ onReset={() => resetOverride("h_gutter")}
220
+ >
221
+ <StyledInput
222
+ value={String(effectiveHGutter)}
223
+ onFocus={snapshotOnFocus}
224
+ onChange={(v) => {
225
+ const num = parseInt(v, 10);
226
+ if (!isNaN(num) && num >= 0) updateResponsive("h_gutter", Math.min(num, 200));
227
+ }}
228
+ placeholder="10"
229
+ />
230
+ </ResponsiveField>
231
+
232
+ <ResponsiveField
233
+ label="V-Gutter"
234
+ block={block}
235
+ property="v_gutter"
236
+ onReset={() => resetOverride("v_gutter")}
237
+ >
238
+ <StyledInput
239
+ value={String(effectiveVGutter)}
240
+ onFocus={snapshotOnFocus}
241
+ onChange={(v) => {
242
+ const num = parseInt(v, 10);
243
+ if (!isNaN(num) && num >= 0) updateResponsive("v_gutter", Math.min(num, 200));
244
+ }}
245
+ placeholder="10"
246
+ />
247
+ </ResponsiveField>
248
+ </div>
249
+
250
+ {/* Images per Row */}
251
+ <ResponsiveField
252
+ label="Images per Row"
253
+ block={block}
254
+ property="images_per_row"
255
+ onReset={() => resetOverride("images_per_row")}
256
+ >
257
+ <select
258
+ value={effectiveImagesPerRow}
259
+ onChange={(e) => updateResponsive("images_per_row", Number(e.target.value))}
260
+ onFocus={snapshotOnFocus}
261
+ className={SELECT_CLASS}
262
+ >
263
+ <option value={1}>1 Image</option>
264
+ <option value={2}>2 Images</option>
265
+ <option value={4}>4 Images</option>
266
+ <option value={6}>6 Images</option>
267
+ <option value={12}>12 Images</option>
268
+ </select>
269
+ </ResponsiveField>
270
+
271
+ {/* Random Grid */}
272
+ <ResponsiveField
273
+ label="Random Grid"
274
+ block={block}
275
+ property="random_grid"
276
+ onReset={() => resetOverride("random_grid")}
277
+ >
278
+ <select
279
+ value={effectiveRandomGrid}
280
+ onChange={(e) => updateResponsive("random_grid", e.target.value)}
281
+ onFocus={snapshotOnFocus}
282
+ className={SELECT_CLASS}
283
+ >
284
+ <option value="disabled">Disabled</option>
285
+ <option value="small2-big4">Small 2 Col, Big 4 Col</option>
286
+ <option value="small3-big6">Small 3 Col, Big 6 Col</option>
287
+ <option value="small4-big8">Small 4 Col, Big 8 Col</option>
288
+ </select>
289
+ </ResponsiveField>
290
+
291
+ {/* Refresh Grid button — only shown when random grid is enabled */}
292
+ {effectiveRandomGrid !== "disabled" && (
293
+ <button
294
+ onClick={() => {
295
+ snapshotOnFocus();
296
+ const newSeed = Math.floor(Math.random() * 999999) + 1;
297
+ updateResponsive("random_seed", newSeed);
298
+ }}
299
+ className="w-full rounded-lg bg-brand-accent-alt py-2 text-[11px] text-neutral-900 font-medium uppercase tracking-wider hover:bg-brand-accent transition-colors"
300
+ >
301
+ Refresh Grid
302
+ </button>
303
+ )}
304
+
305
+ {/* Lightbox toggle */}
306
+ <SettingsField label="Lightbox">
307
+ <div className="flex rounded-lg overflow-hidden border border-neutral-200">
308
+ <button
309
+ onClick={() => {
310
+ snapshotOnFocus();
311
+ updateResponsive("lightbox", true);
312
+ }}
313
+ className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
314
+ effectiveLightbox
315
+ ? "bg-[#076bff] text-white"
316
+ : "bg-white text-neutral-500 hover:bg-neutral-50"
317
+ }`}
318
+ >
319
+ Yes
320
+ </button>
321
+ <button
322
+ onClick={() => {
323
+ snapshotOnFocus();
324
+ updateResponsive("lightbox", false);
325
+ }}
326
+ className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
327
+ !effectiveLightbox
328
+ ? "bg-[#076bff] text-white"
329
+ : "bg-white text-neutral-500 hover:bg-neutral-50"
330
+ }`}
331
+ >
332
+ No
333
+ </button>
334
+ </div>
335
+ </SettingsField>
336
+ </SettingsSection>
337
+
338
+ {/* ====== Appearance Section ====== */}
339
+ <SettingsSection title="Appearance">
340
+ <ResponsiveField
341
+ label="Object Fit"
342
+ block={block}
343
+ property="object_fit"
344
+ onReset={() => resetOverride("object_fit")}
345
+ >
346
+ <div className="flex gap-1">
347
+ {(["cover", "contain"] as const).map((f) => (
348
+ <button
349
+ key={f}
350
+ onClick={() => updateResponsive("object_fit", f)}
351
+ className={`flex-1 rounded border py-1 text-xs transition-colors ${
352
+ effectiveObjectFit === f
353
+ ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
354
+ : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
355
+ }`}
356
+ >
357
+ {f}
358
+ </button>
359
+ ))}
360
+ </div>
361
+ </ResponsiveField>
362
+
363
+ <ResponsiveField
364
+ label="Border Radius"
365
+ block={block}
366
+ property="border_radius"
367
+ onReset={() => resetOverride("border_radius")}
368
+ >
369
+ <div className="flex items-center gap-1.5">
370
+ <StyledInput
371
+ value={String(getEffectiveValue<string>(block, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
372
+ onFocus={snapshotOnFocus}
373
+ onChange={(v) => {
374
+ store._pushSnapshot();
375
+ updateResponsive("border_radius", v.replace(/[^0-9]/g, ""));
376
+ }}
377
+ placeholder="0"
378
+ type="number"
379
+ />
380
+ <span className="text-[10px] text-neutral-400 shrink-0">px</span>
381
+ </div>
382
+ </ResponsiveField>
383
+ </SettingsSection>
384
+ </>
385
+ );
386
+ }