@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,19 @@
1
+ /**
2
+ * Settings panel sub-components barrel export.
3
+ *
4
+ * Session 64: SettingsPanel split into focused modules.
5
+ * Session 65: LayoutTab split into LayoutTab, BlockLayoutTab,
6
+ * RowLayoutPresetPicker, TRBLInputs.
7
+ */
8
+
9
+ export { LayoutTab } from "./LayoutTab";
10
+ export { BlockLayoutTab } from "./BlockLayoutTab";
11
+ export { TRBLInputs } from "./TRBLInputs";
12
+ export { default as PageSettings, PageSeoSettings } from "./PageSettings";
13
+ export { default as BlockSettings } from "./BlockSettings";
14
+ export { default as SectionV2Settings } from "./SectionV2Settings";
15
+ export { SectionV2LayoutTab } from "./SectionV2LayoutTab";
16
+ export { SectionV2AnimationTab } from "./SectionV2AnimationTab";
17
+ export { default as ColumnV2Settings } from "./ColumnV2Settings";
18
+ export { default as ParallaxSlideSettings } from "./ParallaxSlideSettings";
19
+ export { default as ParallaxGroupSettings } from "./ParallaxGroupSettings";
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Responsive helpers for section setting resolution.
3
+ *
4
+ * Session 64: Extracted from SettingsPanel.tsx.
5
+ * Session 86: Removed V1 Row/Column types — now uses generic interface
6
+ * compatible with PageSection (settings + responsive shape).
7
+ */
8
+
9
+ import type { PageSection } from "../../../lib/sanity/types";
10
+ import type { DeviceViewport } from "../../../lib/builder/types";
11
+
12
+ /**
13
+ * Generic shape for items with settings + responsive overrides.
14
+ * Compatible with PageSection (and formerly Row).
15
+ * Uses `unknown` to accept any settings interface without index signature issues.
16
+ */
17
+ type SettingsItem = {
18
+ settings?: unknown;
19
+ responsive?: Record<string, Record<string, unknown>>;
20
+ };
21
+
22
+ /** Get effective value for a section setting, checking viewport overrides */
23
+ export function getRowSettingValue<T>(item: SettingsItem, viewport: DeviceViewport, property: string, fallback: T): T {
24
+ if (viewport === "desktop") {
25
+ const val = (item.settings as Record<string, unknown> || {})[property];
26
+ return (val !== undefined ? val : fallback) as T;
27
+ }
28
+ const override = item.responsive?.[viewport]?.[property];
29
+ if (override !== undefined) return override as T;
30
+ const base = (item.settings as Record<string, unknown> || {})[property];
31
+ return (base !== undefined ? base : fallback) as T;
32
+ }
33
+
34
+ /** Check if a section setting has a responsive override */
35
+ export function hasRowSettingOverride(item: SettingsItem, viewport: DeviceViewport, property: string): boolean {
36
+ if (viewport === "desktop") return false;
37
+ return item.responsive?.[viewport]?.[property] !== undefined;
38
+ }
39
+
40
+ /** Set a responsive override on a section setting, returning the updated fields */
41
+ export function setRowResponsiveOverride(item: SettingsItem, viewport: DeviceViewport, property: string, value: unknown): Partial<PageSection> {
42
+ if (viewport === "desktop") {
43
+ return { settings: { ...(item.settings as Record<string, unknown> || {}), [property]: value } } as Partial<PageSection>;
44
+ }
45
+ const existing = item.responsive || {};
46
+ const vp = viewport as "tablet" | "phone";
47
+ const vpOverrides = { ...(existing[vp] || {}), [property]: value };
48
+ if (value === undefined) {
49
+ delete (vpOverrides as Record<string, unknown>)[property];
50
+ }
51
+ const responsive = { ...existing, [vp]: vpOverrides };
52
+ if (Object.keys(vpOverrides).length === 0) {
53
+ delete responsive[vp];
54
+ }
55
+ return { responsive: Object.keys(responsive).length ? responsive : undefined } as Partial<PageSection>;
56
+ }
57
+
58
+ // ============================================
59
+ // Block Layout responsive helpers
60
+ // ============================================
61
+ // Block layout properties (spacing, background, offset, border, alignment)
62
+ // are stored in block.responsive.tablet.layout / block.responsive.phone.layout.
63
+ // The resolveBlock() function deep-merges these automatically.
64
+
65
+ import type { ContentBlock, BlockLayout } from "../../../lib/sanity/types";
66
+
67
+ /** Get effective value for a block layout property, checking viewport overrides */
68
+ export function getBlockLayoutValue<T>(
69
+ block: ContentBlock,
70
+ viewport: DeviceViewport,
71
+ property: keyof BlockLayout,
72
+ fallback: T
73
+ ): T {
74
+ const layout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
75
+ const base = layout?.[property];
76
+
77
+ if (viewport === "desktop") {
78
+ return (base !== undefined ? base : fallback) as T;
79
+ }
80
+
81
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
82
+ | Record<string, Record<string, unknown>>
83
+ | undefined;
84
+ const vpLayout = responsive?.[viewport]?.layout as Partial<BlockLayout> | undefined;
85
+ if (vpLayout && property in vpLayout) {
86
+ return vpLayout[property] as T;
87
+ }
88
+
89
+ return (base !== undefined ? base : fallback) as T;
90
+ }
91
+
92
+ /** Check if a block layout property has a responsive override for the given viewport */
93
+ export function hasBlockLayoutOverride(
94
+ block: ContentBlock,
95
+ viewport: DeviceViewport,
96
+ property: keyof BlockLayout
97
+ ): boolean {
98
+ if (viewport === "desktop") return false;
99
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
100
+ | Record<string, Record<string, unknown>>
101
+ | undefined;
102
+ const vpLayout = responsive?.[viewport]?.layout as Partial<BlockLayout> | undefined;
103
+ return vpLayout !== undefined && property in vpLayout;
104
+ }
105
+
106
+ /**
107
+ * Set a block layout responsive override. Returns the updates to pass to store.updateBlock().
108
+ *
109
+ * - Desktop: returns `{ layout: { ...existing, [property]: value } }`
110
+ * - Tablet/Phone: returns `{ responsive: { ..., [viewport]: { ..., layout: { ..., [property]: value } } } }`
111
+ */
112
+ export function setBlockLayoutOverride(
113
+ block: ContentBlock,
114
+ viewport: DeviceViewport,
115
+ property: keyof BlockLayout,
116
+ value: unknown
117
+ ): Partial<ContentBlock> {
118
+ const layout = (block as unknown as Record<string, unknown>).layout as BlockLayout | undefined;
119
+
120
+ if (viewport === "desktop") {
121
+ return {
122
+ layout: { ...(layout || {}), [property]: value },
123
+ } as Partial<ContentBlock>;
124
+ }
125
+
126
+ const existing = (block as unknown as Record<string, unknown>).responsive as
127
+ | Record<string, Record<string, unknown>>
128
+ | undefined || {};
129
+ const vpOverrides = { ...(existing[viewport] || {}) };
130
+ const layoutOverrides = { ...((vpOverrides.layout as Record<string, unknown>) || {}), [property]: value };
131
+
132
+ // Remove the field if value is undefined (reset to inherit)
133
+ if (value === undefined) {
134
+ delete layoutOverrides[property];
135
+ }
136
+
137
+ // Clean up empty layout object
138
+ if (Object.keys(layoutOverrides).length > 0) {
139
+ vpOverrides.layout = layoutOverrides;
140
+ } else {
141
+ delete vpOverrides.layout;
142
+ }
143
+
144
+ // Clean up empty viewport object
145
+ const responsive = { ...existing };
146
+ if (Object.keys(vpOverrides).length > 0) {
147
+ responsive[viewport] = vpOverrides;
148
+ } else {
149
+ delete responsive[viewport];
150
+ }
151
+
152
+ return {
153
+ responsive: Object.keys(responsive).length > 0 ? responsive : undefined,
154
+ } as unknown as Partial<ContentBlock>;
155
+ }
156
+
157
+ /**
158
+ * Reset all block layout overrides for a given viewport.
159
+ * Returns the updated responsive object to pass to store.updateBlock().
160
+ */
161
+ export function resetBlockLayoutOverrides(
162
+ block: ContentBlock,
163
+ viewport: DeviceViewport
164
+ ): Partial<ContentBlock> {
165
+ if (viewport === "desktop") return {};
166
+
167
+ const existing = (block as unknown as Record<string, unknown>).responsive as
168
+ | Record<string, Record<string, unknown>>
169
+ | undefined;
170
+ if (!existing?.[viewport]?.layout) return {};
171
+
172
+ const vpOverrides = { ...(existing[viewport] || {}) };
173
+ delete vpOverrides.layout;
174
+
175
+ const responsive = { ...existing };
176
+ if (Object.keys(vpOverrides).length > 0) {
177
+ responsive[viewport] = vpOverrides;
178
+ } else {
179
+ delete responsive[viewport];
180
+ }
181
+
182
+ return {
183
+ responsive: Object.keys(responsive).length > 0 ? responsive : undefined,
184
+ } as unknown as Partial<ContentBlock>;
185
+ }
186
+
187
+ // ============================================
188
+ // Block animation responsive helpers
189
+ // ============================================
190
+ // Block animation properties (enter_animation, hover_effect) are stored
191
+ // at the block root level for desktop. Per-viewport overrides live in
192
+ // block.responsive.tablet / block.responsive.phone (same level, not nested
193
+ // under layout).
194
+
195
+ type BlockAnimationProperty = "enter_animation" | "hover_effect";
196
+
197
+ /** Get effective value for a block animation property, checking viewport overrides */
198
+ export function getBlockAnimationValue<T>(
199
+ block: ContentBlock,
200
+ viewport: DeviceViewport,
201
+ property: BlockAnimationProperty,
202
+ fallback: T
203
+ ): T {
204
+ const base = (block as unknown as Record<string, unknown>)[property];
205
+
206
+ if (viewport === "desktop") {
207
+ return (base !== undefined ? base : fallback) as T;
208
+ }
209
+
210
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
211
+ | Record<string, Record<string, unknown>>
212
+ | undefined;
213
+ const vpOverride = responsive?.[viewport]?.[property];
214
+ if (vpOverride !== undefined) return vpOverride as T;
215
+
216
+ return (base !== undefined ? base : fallback) as T;
217
+ }
218
+
219
+ /** Check if a block animation property has a responsive override */
220
+ export function hasBlockAnimationOverride(
221
+ block: ContentBlock,
222
+ viewport: DeviceViewport,
223
+ property: BlockAnimationProperty
224
+ ): boolean {
225
+ if (viewport === "desktop") return false;
226
+ const responsive = (block as unknown as Record<string, unknown>).responsive as
227
+ | Record<string, Record<string, unknown>>
228
+ | undefined;
229
+ const vpData = responsive?.[viewport];
230
+ return vpData !== undefined && property in vpData;
231
+ }
232
+
233
+ /**
234
+ * Set a block animation responsive override. Returns updates for store.updateBlock().
235
+ *
236
+ * - Desktop: returns `{ [property]: value }`
237
+ * - Tablet/Phone: returns `{ responsive: { ..., [viewport]: { ..., [property]: value } } }`
238
+ */
239
+ export function setBlockAnimationOverride(
240
+ block: ContentBlock,
241
+ viewport: DeviceViewport,
242
+ property: BlockAnimationProperty,
243
+ value: unknown
244
+ ): Partial<ContentBlock> {
245
+ if (viewport === "desktop") {
246
+ return { [property]: value } as Partial<ContentBlock>;
247
+ }
248
+
249
+ const existing = (block as unknown as Record<string, unknown>).responsive as
250
+ | Record<string, Record<string, unknown>>
251
+ | undefined || {};
252
+ const vpOverrides = { ...(existing[viewport] || {}), [property]: value };
253
+
254
+ // Remove the field if value is undefined (reset to inherit)
255
+ if (value === undefined) {
256
+ delete vpOverrides[property];
257
+ }
258
+
259
+ // Clean up empty viewport object
260
+ const responsive = { ...existing };
261
+ if (Object.keys(vpOverrides).length > 0) {
262
+ responsive[viewport] = vpOverrides;
263
+ } else {
264
+ delete responsive[viewport];
265
+ }
266
+
267
+ return {
268
+ responsive: Object.keys(responsive).length > 0 ? responsive : undefined,
269
+ } as unknown as Partial<ContentBlock>;
270
+ }
271
+
272
+ // ============================================
273
+ // V2 Section responsive helpers
274
+ // ============================================
275
+ // V2 sections store responsive overrides at two levels:
276
+ // 1. Section settings (col_gap, row_gap, spacing, border, etc.)
277
+ // → section.responsive[viewport].settings
278
+ // 2. Column position/span overrides
279
+ // → section.responsive[viewport].columns[]
280
+ //
281
+ // On desktop, edits go to the base section.settings / column properties.
282
+ // On tablet/phone, edits write to the responsive overrides.
283
+
284
+ import type { PageSectionV2, SectionV2Settings, SectionV2SettingsOverridable, ColumnOverride, SectionColumn } from "../../../lib/sanity/types";
285
+
286
+ /**
287
+ * Get the effective value for a V2 section setting, checking viewport overrides.
288
+ */
289
+ export function getSectionV2SettingValue<T>(
290
+ section: PageSectionV2,
291
+ viewport: DeviceViewport,
292
+ property: keyof SectionV2SettingsOverridable,
293
+ fallback: T
294
+ ): T {
295
+ if (viewport !== "desktop") {
296
+ const vp = viewport as "tablet" | "phone";
297
+ const vpSettings = section.responsive?.[vp]?.settings;
298
+ if (vpSettings && property in vpSettings) {
299
+ return (vpSettings as Record<string, unknown>)[property] as T;
300
+ }
301
+ }
302
+ const val = (section.settings as unknown as Record<string, unknown>)[property];
303
+ return (val !== undefined ? val : fallback) as T;
304
+ }
305
+
306
+ /**
307
+ * Check if a V2 section setting has a responsive override for the given viewport.
308
+ */
309
+ export function hasSectionV2SettingOverride(
310
+ section: PageSectionV2,
311
+ viewport: DeviceViewport,
312
+ property: keyof SectionV2SettingsOverridable
313
+ ): boolean {
314
+ if (viewport === "desktop") return false;
315
+ const vp = viewport as "tablet" | "phone";
316
+ const vpSettings = section.responsive?.[vp]?.settings;
317
+ return vpSettings !== undefined && property in vpSettings;
318
+ }
319
+
320
+ /**
321
+ * Build the updated responsive object after setting a V2 section setting override.
322
+ * Desktop writes to settings directly (returns null — caller should use updateSectionV2Settings).
323
+ * Tablet/Phone writes to responsive overrides.
324
+ */
325
+ export function buildSectionV2SettingOverride(
326
+ section: PageSectionV2,
327
+ viewport: DeviceViewport,
328
+ property: keyof SectionV2SettingsOverridable,
329
+ value: unknown
330
+ ): PageSectionV2["responsive"] | null {
331
+ if (viewport === "desktop") return null; // Caller uses updateSectionV2Settings directly
332
+
333
+ const existing = section.responsive || {};
334
+ const vp = viewport as "tablet" | "phone";
335
+ const vpSettings = { ...(existing[vp]?.settings || {}), [property]: value } as Record<string, unknown>;
336
+
337
+ if (value === undefined) delete vpSettings[property];
338
+
339
+ const vpOverride = {
340
+ ...(existing[vp] || {}),
341
+ settings: Object.keys(vpSettings).length ? vpSettings : undefined,
342
+ } as NonNullable<PageSectionV2["responsive"]>[typeof vp];
343
+
344
+ // Clean up viewport override if empty
345
+ const hasColumns = Array.isArray((existing[vp] as Record<string, unknown>)?.columns) &&
346
+ ((existing[vp] as Record<string, unknown>)?.columns as unknown[]).length > 0;
347
+ if (!vpOverride!.settings && !hasColumns) {
348
+ const responsive = { ...existing };
349
+ delete responsive[vp];
350
+ return Object.keys(responsive).length ? responsive : undefined;
351
+ }
352
+
353
+ const responsive = { ...existing, [vp]: vpOverride };
354
+ return Object.keys(responsive).length ? responsive : undefined;
355
+ }
356
+
357
+ // ============================================
358
+ // V2 Column position/span responsive helpers
359
+ // ============================================
360
+
361
+ /**
362
+ * Get effective column position/span for a V2 column, checking viewport overrides.
363
+ * Returns the override value if set, otherwise the base column value.
364
+ */
365
+ export function getColumnV2Override(
366
+ section: PageSectionV2,
367
+ columnKey: string,
368
+ viewport: DeviceViewport,
369
+ property: "grid_column" | "grid_row" | "span"
370
+ ): number | undefined {
371
+ if (viewport === "desktop") return undefined;
372
+ const vp = viewport as "tablet" | "phone";
373
+ const colOverride = section.responsive?.[vp]?.columns?.find((c) => c._key === columnKey);
374
+ return colOverride?.[property];
375
+ }
376
+
377
+ /**
378
+ * Get all effective column positions for a V2 section at a given viewport.
379
+ * Merges responsive overrides on top of base column positions.
380
+ */
381
+ export function getEffectiveColumnsV2(
382
+ section: PageSectionV2,
383
+ viewport: DeviceViewport
384
+ ): Array<{ _key: string; grid_column: number; grid_row: number; span: number }> {
385
+ if (viewport === "desktop") {
386
+ return section.columns.map((c) => ({
387
+ _key: c._key,
388
+ grid_column: c.grid_column,
389
+ grid_row: c.grid_row,
390
+ span: c.span,
391
+ }));
392
+ }
393
+
394
+ const vp = viewport as "tablet" | "phone";
395
+ const overrides = section.responsive?.[vp]?.columns || [];
396
+ const overrideMap = new Map(overrides.map((o) => [o._key, o]));
397
+
398
+ return section.columns.map((c) => {
399
+ const override = overrideMap.get(c._key);
400
+ return {
401
+ _key: c._key,
402
+ grid_column: override?.grid_column ?? c.grid_column,
403
+ grid_row: override?.grid_row ?? c.grid_row,
404
+ span: override?.span ?? c.span,
405
+ };
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Check if any column has responsive overrides for a given viewport.
411
+ */
412
+ export function hasAnyColumnV2Overrides(
413
+ section: PageSectionV2,
414
+ viewport: DeviceViewport
415
+ ): boolean {
416
+ if (viewport === "desktop") return false;
417
+ const vp = viewport as "tablet" | "phone";
418
+ const overrides = section.responsive?.[vp]?.columns;
419
+ return Array.isArray(overrides) && overrides.length > 0;
420
+ }
421
+
422
+ /**
423
+ * Check if any settings have overrides for a given viewport.
424
+ */
425
+ export function hasAnySectionV2SettingOverrides(
426
+ section: PageSectionV2,
427
+ viewport: DeviceViewport
428
+ ): boolean {
429
+ if (viewport === "desktop") return false;
430
+ const vp = viewport as "tablet" | "phone";
431
+ const settings = section.responsive?.[vp]?.settings;
432
+ return settings !== undefined && Object.keys(settings).length > 0;
433
+ }
434
+
435
+ /**
436
+ * Build a "stack" layout override — all columns get full width, sequential rows.
437
+ * Used for one-click mobile stacking.
438
+ */
439
+ export function buildStackOverride(
440
+ section: PageSectionV2
441
+ ): ColumnOverride[] {
442
+ const gridColumns = section.settings.grid_columns || 12;
443
+ return section.columns.map((col, index) => ({
444
+ _key: col._key,
445
+ grid_column: 1,
446
+ grid_row: index + 1,
447
+ span: gridColumns,
448
+ }));
449
+ }
450
+
451
+ /**
452
+ * Build the updated responsive object after setting column overrides for a viewport.
453
+ * Merges with existing settings overrides to preserve them.
454
+ */
455
+ export function buildColumnV2Overrides(
456
+ section: PageSectionV2,
457
+ viewport: DeviceViewport,
458
+ columnOverrides: ColumnOverride[] | undefined
459
+ ): PageSectionV2["responsive"] | undefined {
460
+ if (viewport === "desktop") return section.responsive;
461
+
462
+ const existing = section.responsive || {};
463
+ const vp = viewport as "tablet" | "phone";
464
+
465
+ const vpOverride = {
466
+ ...(existing[vp] || {}),
467
+ columns: columnOverrides && columnOverrides.length > 0 ? columnOverrides : undefined,
468
+ } as NonNullable<PageSectionV2["responsive"]>[typeof vp];
469
+
470
+ // Clean up if both columns and settings are empty
471
+ if (!vpOverride!.columns?.length && !vpOverride!.settings) {
472
+ const responsive = { ...existing };
473
+ delete responsive[vp];
474
+ return Object.keys(responsive).length ? responsive : undefined;
475
+ }
476
+
477
+ const responsive = { ...existing, [vp]: vpOverride };
478
+ return Object.keys(responsive).length ? responsive : undefined;
479
+ }
480
+
481
+ /**
482
+ * Build a single-column position override within existing overrides.
483
+ * Updates one column's grid_column, grid_row, or span without affecting others.
484
+ */
485
+ export function buildSingleColumnV2Override(
486
+ section: PageSectionV2,
487
+ viewport: DeviceViewport,
488
+ columnKey: string,
489
+ updates: Partial<Pick<ColumnOverride, "grid_column" | "grid_row" | "span">>
490
+ ): PageSectionV2["responsive"] | undefined {
491
+ if (viewport === "desktop") return section.responsive;
492
+
493
+ const vp = viewport as "tablet" | "phone";
494
+ const existingOverrides = section.responsive?.[vp]?.columns || [];
495
+
496
+ // Find existing override for this column or create a new one
497
+ const overrideIndex = existingOverrides.findIndex((o) => o._key === columnKey);
498
+ const existingColOverride = overrideIndex >= 0 ? existingOverrides[overrideIndex] : { _key: columnKey };
499
+
500
+ const updatedColOverride: ColumnOverride = {
501
+ ...existingColOverride,
502
+ ...updates,
503
+ };
504
+
505
+ // Check if the override is actually different from base
506
+ const baseCol = section.columns.find((c) => c._key === columnKey);
507
+ const isIdenticalToBase = baseCol &&
508
+ (updatedColOverride.grid_column === undefined || updatedColOverride.grid_column === baseCol.grid_column) &&
509
+ (updatedColOverride.grid_row === undefined || updatedColOverride.grid_row === baseCol.grid_row) &&
510
+ (updatedColOverride.span === undefined || updatedColOverride.span === baseCol.span);
511
+
512
+ let updatedOverrides: ColumnOverride[];
513
+ if (isIdenticalToBase) {
514
+ // Remove this column's override if it matches the base
515
+ updatedOverrides = existingOverrides.filter((o) => o._key !== columnKey);
516
+ } else if (overrideIndex >= 0) {
517
+ updatedOverrides = [...existingOverrides];
518
+ updatedOverrides[overrideIndex] = updatedColOverride;
519
+ } else {
520
+ updatedOverrides = [...existingOverrides, updatedColOverride];
521
+ }
522
+
523
+ return buildColumnV2Overrides(section, viewport, updatedOverrides);
524
+ }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+
5
+ export default function CustomCursor() {
6
+ const [position, setPosition] = useState({ x: -100, y: -100 });
7
+ const [isHovering, setIsHovering] = useState(false);
8
+ const [isVisible, setIsVisible] = useState(false);
9
+ const [isMobile, setIsMobile] = useState(true);
10
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
11
+ const rafId = useRef<number>(0);
12
+
13
+ // Check if device supports hover (desktop)
14
+ useEffect(() => {
15
+ const mql = window.matchMedia("(hover: hover) and (pointer: fine)");
16
+ setIsMobile(!mql.matches);
17
+
18
+ const handler = (e: MediaQueryListEvent) => setIsMobile(!e.matches);
19
+ mql.addEventListener("change", handler);
20
+ return () => mql.removeEventListener("change", handler);
21
+ }, []);
22
+
23
+ // Respect prefers-reduced-motion — disable custom cursor entirely
24
+ useEffect(() => {
25
+ const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
26
+ setPrefersReducedMotion(mql.matches);
27
+
28
+ const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
29
+ mql.addEventListener("change", handler);
30
+ return () => mql.removeEventListener("change", handler);
31
+ }, []);
32
+
33
+ const handleMouseMove = useCallback((e: MouseEvent) => {
34
+ if (rafId.current) cancelAnimationFrame(rafId.current);
35
+ rafId.current = requestAnimationFrame(() => {
36
+ setPosition({ x: e.clientX, y: e.clientY });
37
+ setIsVisible(true);
38
+ });
39
+ }, []);
40
+
41
+ const handleMouseLeave = useCallback(() => {
42
+ setIsVisible(false);
43
+ }, []);
44
+
45
+ // Track hover state on interactive elements
46
+ useEffect(() => {
47
+ if (isMobile) return;
48
+
49
+ const handleMouseOver = (e: MouseEvent) => {
50
+ const target = e.target as HTMLElement;
51
+ if (
52
+ target.closest("a, button, [role='button'], input, textarea, select, [data-cursor-hover]")
53
+ ) {
54
+ setIsHovering(true);
55
+ }
56
+ };
57
+
58
+ const handleMouseOut = (e: MouseEvent) => {
59
+ const target = e.target as HTMLElement;
60
+ if (
61
+ target.closest("a, button, [role='button'], input, textarea, select, [data-cursor-hover]")
62
+ ) {
63
+ setIsHovering(false);
64
+ }
65
+ };
66
+
67
+ document.addEventListener("mousemove", handleMouseMove);
68
+ document.addEventListener("mouseleave", handleMouseLeave);
69
+ document.addEventListener("mouseover", handleMouseOver);
70
+ document.addEventListener("mouseout", handleMouseOut);
71
+
72
+ return () => {
73
+ document.removeEventListener("mousemove", handleMouseMove);
74
+ document.removeEventListener("mouseleave", handleMouseLeave);
75
+ document.removeEventListener("mouseover", handleMouseOver);
76
+ document.removeEventListener("mouseout", handleMouseOut);
77
+ if (rafId.current) cancelAnimationFrame(rafId.current);
78
+ };
79
+ }, [isMobile, handleMouseMove, handleMouseLeave]);
80
+
81
+ // Don't render on mobile/touch devices or when reduced motion is preferred
82
+ if (isMobile || prefersReducedMotion) return null;
83
+
84
+ const size = isHovering ? 38 : 20;
85
+ const innerSize = isHovering ? 10 : 4;
86
+
87
+ return (
88
+ <div
89
+ className="pointer-events-none fixed inset-0 z-[9999]"
90
+ aria-hidden="true"
91
+ >
92
+ {/* Outer circle — primary brand color */}
93
+ <div
94
+ className="absolute rounded-full border-[1.5px] border-brand-primary"
95
+ style={{
96
+ width: size,
97
+ height: size,
98
+ left: position.x - size / 2,
99
+ top: position.y - size / 2,
100
+ opacity: isVisible ? 1 : 0,
101
+ transition: "width 0.2s ease, height 0.2s ease, opacity 0.15s ease",
102
+ }}
103
+ />
104
+ {/* Inner circle — secondary brand color */}
105
+ <div
106
+ className="absolute rounded-full bg-brand-secondary"
107
+ style={{
108
+ width: innerSize,
109
+ height: innerSize,
110
+ left: position.x - innerSize / 2,
111
+ top: position.y - innerSize / 2,
112
+ opacity: isVisible ? 1 : 0,
113
+ transition: "width 0.2s ease, height 0.2s ease, opacity 0.15s ease",
114
+ }}
115
+ />
116
+ </div>
117
+ );
118
+ }