@morphika/webframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +210 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,532 @@
1
+ "use client";
2
+
3
+ /**
4
+ * BlockLayoutTab — Per-block layout styling.
5
+ *
6
+ * Section order: Alignment → Spacing → Offset → Background → Border.
7
+ * Each section has a small visual icon next to its title.
8
+ *
9
+ * Session 78: Added Alignment section with visual icon buttons.
10
+ * Reordered sections. Added section title icons.
11
+ * Session 82: Made viewport-aware with responsive override support.
12
+ * Every property is now editable per viewport (desktop/tablet/phone).
13
+ * Uses getBlockLayoutValue/setBlockLayoutOverride from responsive-helpers.
14
+ */
15
+
16
+ import { useBuilderStore } from "../../../lib/builder/store";
17
+ import type { ContentBlock, BlockLayout } from "../../../lib/sanity/types";
18
+ import {
19
+ SettingsField,
20
+ SettingsSection,
21
+ SELECT_CLASS,
22
+ AssetPathInput,
23
+ } from "../editors/shared";
24
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
25
+ import { TRBLInputs } from "./TRBLInputs";
26
+ import {
27
+ getBlockLayoutValue,
28
+ hasBlockLayoutOverride,
29
+ setBlockLayoutOverride,
30
+ } from "./responsive-helpers";
31
+
32
+ // ── Alignment visual icon buttons ──
33
+
34
+ type AlignH = "left" | "center" | "right";
35
+ type AlignV = "top" | "center" | "bottom";
36
+
37
+ function HAlignIcon({ value, size = 18 }: { value: AlignH; size?: number }) {
38
+ // Horizontal lines aligned left/center/right inside a box
39
+ const w = size;
40
+ const h = size;
41
+ const pad = 3;
42
+ const barH = 2.5;
43
+ const gap = 1.5;
44
+ const maxW = w - pad * 2;
45
+
46
+ const bars = [
47
+ { w: maxW * 0.9, y: pad + 1 },
48
+ { w: maxW * 0.55, y: pad + 1 + barH + gap },
49
+ { w: maxW * 0.75, y: pad + 1 + (barH + gap) * 2 },
50
+ ];
51
+
52
+ return (
53
+ <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} fill="none">
54
+ {bars.map((bar, i) => {
55
+ let x = pad;
56
+ if (value === "center") x = (w - bar.w) / 2;
57
+ else if (value === "right") x = w - pad - bar.w;
58
+ return (
59
+ <rect
60
+ key={i}
61
+ x={x}
62
+ y={bar.y}
63
+ width={bar.w}
64
+ height={barH}
65
+ rx={1}
66
+ fill="currentColor"
67
+ />
68
+ );
69
+ })}
70
+ </svg>
71
+ );
72
+ }
73
+
74
+ function VAlignIcon({ value, size = 18 }: { value: AlignV; size?: number }) {
75
+ const w = size;
76
+ const h = size;
77
+ const pad = 3;
78
+ const barW = w - pad * 2;
79
+ const barH = 3;
80
+
81
+ // Position of a single bar within the vertical space
82
+ let y = pad;
83
+ if (value === "center") y = (h - barH) / 2;
84
+ else if (value === "bottom") y = h - pad - barH;
85
+
86
+ return (
87
+ <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} fill="none">
88
+ {/* Container outline */}
89
+ <rect
90
+ x={pad - 0.5}
91
+ y={pad - 0.5}
92
+ width={barW + 1}
93
+ height={h - pad * 2 + 1}
94
+ rx={1.5}
95
+ stroke="currentColor"
96
+ strokeWidth={0.8}
97
+ opacity={0.3}
98
+ fill="none"
99
+ />
100
+ {/* Bar at position */}
101
+ <rect
102
+ x={pad + 1}
103
+ y={y}
104
+ width={barW - 2}
105
+ height={barH}
106
+ rx={1}
107
+ fill="currentColor"
108
+ />
109
+ </svg>
110
+ );
111
+ }
112
+
113
+ function AlignmentButtons<T extends string>({
114
+ options,
115
+ value,
116
+ onChange,
117
+ renderIcon,
118
+ }: {
119
+ options: { value: T; label: string }[];
120
+ value: T;
121
+ onChange: (v: T) => void;
122
+ renderIcon: (v: T) => React.ReactNode;
123
+ }) {
124
+ return (
125
+ <div className="flex gap-1">
126
+ {options.map((opt) => {
127
+ const isActive = value === opt.value;
128
+ return (
129
+ <button
130
+ key={opt.value}
131
+ title={opt.label}
132
+ onClick={() => onChange(opt.value)}
133
+ className={`flex items-center justify-center w-[34px] h-[30px] rounded-md border transition-all ${
134
+ isActive
135
+ ? "bg-[#076bff]/10 border-[#076bff]/30 text-[#076bff]"
136
+ : "bg-[#f5f5f5] border-transparent text-neutral-400 hover:bg-[#efefef] hover:text-neutral-600"
137
+ }`}
138
+ >
139
+ {renderIcon(opt.value)}
140
+ </button>
141
+ );
142
+ })}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ // ── Section title icons (small, inline) ──
148
+
149
+ function AlignmentSectionIcon() {
150
+ return (
151
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
152
+ <rect x="1" y="2" width="9" height="2" rx="0.5" fill="currentColor" opacity="0.7" />
153
+ <rect x="3" y="6" width="8" height="2" rx="0.5" fill="currentColor" opacity="0.5" />
154
+ <rect x="2" y="10" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.7" />
155
+ </svg>
156
+ );
157
+ }
158
+
159
+ function SpacingSectionIcon() {
160
+ return (
161
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
162
+ <rect x="4" y="4" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.5" />
163
+ <path d="M7 1 L7 3.5" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
164
+ <path d="M7 10.5 L7 13" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
165
+ <path d="M1 7 L3.5 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
166
+ <path d="M10.5 7 L13 7" stroke="currentColor" strokeWidth="0.8" opacity="0.7" />
167
+ </svg>
168
+ );
169
+ }
170
+
171
+ function OffsetSectionIcon() {
172
+ return (
173
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
174
+ <rect x="3" y="3" width="8" height="8" rx="1" stroke="currentColor" strokeWidth="0.8" strokeDasharray="2 1" fill="none" opacity="0.35" />
175
+ <rect x="5" y="5" width="6" height="6" rx="1" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.7" />
176
+ <path d="M4 4 L5 5" stroke="currentColor" strokeWidth="0.6" opacity="0.5" />
177
+ </svg>
178
+ );
179
+ }
180
+
181
+ function BackgroundSectionIcon() {
182
+ return (
183
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
184
+ <rect x="1.5" y="1.5" width="11" height="11" rx="2" fill="currentColor" opacity="0.15" />
185
+ <rect x="1.5" y="1.5" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="0.8" opacity="0.5" fill="none" />
186
+ <circle cx="5" cy="5" r="1.5" fill="currentColor" opacity="0.5" />
187
+ <path d="M1.5 10 L5 7 L8 9 L10.5 6.5 L12.5 8.5 L12.5 11 C12.5 11.8 11.8 12.5 11 12.5 L3 12.5 C2.2 12.5 1.5 11.8 1.5 11 Z" fill="currentColor" opacity="0.3" />
188
+ </svg>
189
+ );
190
+ }
191
+
192
+ function BorderSectionIcon() {
193
+ return (
194
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
195
+ <rect x="2" y="2" width="10" height="10" rx="2" stroke="currentColor" strokeWidth="1.2" fill="none" opacity="0.6" />
196
+ <rect x="2" y="2" width="10" height="1.2" rx="0.5" fill="currentColor" opacity="0.7" />
197
+ </svg>
198
+ );
199
+ }
200
+
201
+ // ── Override indicator badge ──
202
+
203
+ function OverrideBadge({
204
+ block,
205
+ viewport,
206
+ properties,
207
+ onReset,
208
+ }: {
209
+ block: ContentBlock;
210
+ viewport: string;
211
+ properties: (keyof BlockLayout)[];
212
+ onReset: () => void;
213
+ }) {
214
+ if (viewport === "desktop") return null;
215
+ const hasAny = properties.some((p) => hasBlockLayoutOverride(block, viewport as "tablet" | "phone", p));
216
+ if (hasAny) {
217
+ return (
218
+ <div className="flex items-center gap-2 mt-1">
219
+ <span className="text-[9px] text-[#076bff]">overridden</span>
220
+ <button
221
+ onClick={onReset}
222
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
223
+ >
224
+ Reset
225
+ </button>
226
+ </div>
227
+ );
228
+ }
229
+ return <p className="text-[9px] text-neutral-300 italic mt-1">inherited</p>;
230
+ }
231
+
232
+ // ── Main component ──
233
+
234
+ export function BlockLayoutTab({ block }: { block: ContentBlock }) {
235
+ const store = useBuilderStore();
236
+ const paletteSwatches = usePaletteSwatches();
237
+ const activeViewport = store.activeViewport;
238
+
239
+ /** Update a block layout property, viewport-aware */
240
+ const updateLayout = (property: keyof BlockLayout, value: unknown) => {
241
+ const updates = setBlockLayoutOverride(block, activeViewport, property, value);
242
+ store.updateBlock(block._key, updates);
243
+ };
244
+
245
+ /** Reset all layout overrides for a group of properties */
246
+ const resetGroup = (properties: (keyof BlockLayout)[]) => {
247
+ store._pushSnapshot();
248
+ // Accumulate resets by setting each property to undefined
249
+ let accumulated = block;
250
+ for (const prop of properties) {
251
+ const u = setBlockLayoutOverride(accumulated, activeViewport, prop, undefined);
252
+ accumulated = { ...accumulated, ...u } as ContentBlock;
253
+ }
254
+ const responsive = (accumulated as unknown as Record<string, unknown>).responsive;
255
+ store.updateBlock(block._key, { responsive } as unknown as Partial<ContentBlock>);
256
+ };
257
+
258
+ // Effective values per viewport
259
+ const bgOpacity = getBlockLayoutValue<number>(block, activeViewport, "background_opacity", 100);
260
+ const alignH: AlignH = getBlockLayoutValue<string>(block, activeViewport, "align_h", "left") as AlignH;
261
+ const alignV: AlignV = getBlockLayoutValue<string>(block, activeViewport, "align_v", "top") as AlignV;
262
+
263
+ const viewportLabel = activeViewport !== "desktop"
264
+ ? activeViewport === "tablet" ? "Tablet" : "Phone"
265
+ : null;
266
+
267
+ return (
268
+ <>
269
+ {viewportLabel && (
270
+ <div className="px-4 pt-3">
271
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
272
+ <span className="text-[11px] font-medium text-[#076bff]">
273
+ Editing {viewportLabel} overrides
274
+ </span>
275
+ </div>
276
+ </div>
277
+ )}
278
+
279
+ {/* Alignment */}
280
+ <SettingsSection title="Alignment" defaultOpen icon={<AlignmentSectionIcon />}>
281
+ <SettingsField label="Horizontal">
282
+ <AlignmentButtons<AlignH>
283
+ options={[
284
+ { value: "left", label: "Left" },
285
+ { value: "center", label: "Center" },
286
+ { value: "right", label: "Right" },
287
+ ]}
288
+ value={alignH}
289
+ onChange={(v) => {
290
+ store._pushSnapshot();
291
+ updateLayout("align_h", v);
292
+ }}
293
+ renderIcon={(v) => <HAlignIcon value={v} />}
294
+ />
295
+ </SettingsField>
296
+
297
+ <SettingsField label="Vertical">
298
+ <AlignmentButtons<AlignV>
299
+ options={[
300
+ { value: "top", label: "Top" },
301
+ { value: "center", label: "Center" },
302
+ { value: "bottom", label: "Bottom" },
303
+ ]}
304
+ value={alignV}
305
+ onChange={(v) => {
306
+ store._pushSnapshot();
307
+ updateLayout("align_v", v);
308
+ }}
309
+ renderIcon={(v) => <VAlignIcon value={v} />}
310
+ />
311
+ </SettingsField>
312
+
313
+ <OverrideBadge
314
+ block={block}
315
+ viewport={activeViewport}
316
+ properties={["align_h", "align_v"]}
317
+ onReset={() => resetGroup(["align_h", "align_v"])}
318
+ />
319
+ </SettingsSection>
320
+
321
+ {/* Spacing (Padding) */}
322
+ <SettingsSection title="Spacing" defaultOpen icon={<SpacingSectionIcon />}>
323
+ <TRBLInputs
324
+ top={getBlockLayoutValue<string>(block, activeViewport, "spacing_top", "0")}
325
+ right={getBlockLayoutValue<string>(block, activeViewport, "spacing_right", "0")}
326
+ bottom={getBlockLayoutValue<string>(block, activeViewport, "spacing_bottom", "0")}
327
+ left={getBlockLayoutValue<string>(block, activeViewport, "spacing_left", "0")}
328
+ onChange={(field, value) => {
329
+ updateLayout(`spacing_${field}` as keyof BlockLayout, value);
330
+ }}
331
+ />
332
+ <OverrideBadge
333
+ block={block}
334
+ viewport={activeViewport}
335
+ properties={["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"]}
336
+ onReset={() => resetGroup(["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"])}
337
+ />
338
+ </SettingsSection>
339
+
340
+ {/* Offset (Margin) */}
341
+ <SettingsSection title="Offset" icon={<OffsetSectionIcon />}>
342
+ <TRBLInputs
343
+ top={getBlockLayoutValue<string>(block, activeViewport, "offset_top", "0")}
344
+ right={getBlockLayoutValue<string>(block, activeViewport, "offset_right", "0")}
345
+ bottom={getBlockLayoutValue<string>(block, activeViewport, "offset_bottom", "0")}
346
+ left={getBlockLayoutValue<string>(block, activeViewport, "offset_left", "0")}
347
+ onChange={(field, value) => {
348
+ updateLayout(`offset_${field}` as keyof BlockLayout, value);
349
+ }}
350
+ />
351
+ <OverrideBadge
352
+ block={block}
353
+ viewport={activeViewport}
354
+ properties={["offset_top", "offset_right", "offset_bottom", "offset_left"]}
355
+ onReset={() => resetGroup(["offset_top", "offset_right", "offset_bottom", "offset_left"])}
356
+ />
357
+ </SettingsSection>
358
+
359
+ {/* Background */}
360
+ <SettingsSection title="Background" defaultOpen icon={<BackgroundSectionIcon />}>
361
+ <SettingsField label="Color">
362
+ <ColorSwatchPicker
363
+ value={getBlockLayoutValue<string>(block, activeViewport, "background_color", "")}
364
+ onChange={(hex) => {
365
+ store._pushSnapshot();
366
+ updateLayout("background_color", hex);
367
+ }}
368
+ swatches={paletteSwatches}
369
+ />
370
+ </SettingsField>
371
+
372
+ <SettingsField label="Opacity">
373
+ <div className="flex items-center gap-2">
374
+ <input
375
+ type="range"
376
+ min={0}
377
+ max={100}
378
+ value={bgOpacity}
379
+ onChange={(e) => updateLayout("background_opacity", parseInt(e.target.value))}
380
+ className="flex-1 accent-[#076bff]"
381
+ />
382
+ <span className="text-xs text-neutral-900 w-10 text-right">
383
+ {bgOpacity}%
384
+ </span>
385
+ </div>
386
+ </SettingsField>
387
+
388
+ <SettingsField label="Image">
389
+ <AssetPathInput
390
+ value={getBlockLayoutValue<string>(block, activeViewport, "background_image", "")}
391
+ onFocus={() => store._pushSnapshot()}
392
+ onChange={(v) => updateLayout("background_image", v)}
393
+ placeholder="path/to/image.jpg"
394
+ filterType="image"
395
+ />
396
+ </SettingsField>
397
+
398
+ {getBlockLayoutValue<string>(block, activeViewport, "background_image", "") && (
399
+ <>
400
+ <SettingsField label="Size">
401
+ <select
402
+ value={getBlockLayoutValue<string>(block, activeViewport, "background_size", "cover")}
403
+ onChange={(e) => updateLayout("background_size", e.target.value)}
404
+ className={SELECT_CLASS}
405
+ >
406
+ <option value="cover">Cover</option>
407
+ <option value="contain">Contain</option>
408
+ <option value="auto">Auto</option>
409
+ </select>
410
+ </SettingsField>
411
+
412
+ <SettingsField label="Position">
413
+ <select
414
+ value={getBlockLayoutValue<string>(block, activeViewport, "background_position", "center center")}
415
+ onChange={(e) => updateLayout("background_position", e.target.value)}
416
+ className={SELECT_CLASS}
417
+ >
418
+ <option value="center center">Center</option>
419
+ <option value="top center">Top</option>
420
+ <option value="bottom center">Bottom</option>
421
+ <option value="left center">Left</option>
422
+ <option value="right center">Right</option>
423
+ </select>
424
+ </SettingsField>
425
+
426
+ <SettingsField label="Repeat">
427
+ <select
428
+ value={getBlockLayoutValue<string>(block, activeViewport, "background_repeat", "no-repeat")}
429
+ onChange={(e) => updateLayout("background_repeat", e.target.value)}
430
+ className={SELECT_CLASS}
431
+ >
432
+ <option value="no-repeat">No Repeat</option>
433
+ <option value="repeat">Repeat</option>
434
+ <option value="repeat-x">Repeat X</option>
435
+ <option value="repeat-y">Repeat Y</option>
436
+ </select>
437
+ </SettingsField>
438
+ </>
439
+ )}
440
+
441
+ <OverrideBadge
442
+ block={block}
443
+ viewport={activeViewport}
444
+ properties={["background_color", "background_opacity", "background_image", "background_size", "background_position", "background_repeat"]}
445
+ onReset={() => resetGroup(["background_color", "background_opacity", "background_image", "background_size", "background_position", "background_repeat"])}
446
+ />
447
+ </SettingsSection>
448
+
449
+ {/* Border */}
450
+ <SettingsSection title="Border" icon={<BorderSectionIcon />}>
451
+ <SettingsField label="Color">
452
+ <ColorSwatchPicker
453
+ value={getBlockLayoutValue<string>(block, activeViewport, "border_color", "")}
454
+ onChange={(hex) => {
455
+ store._pushSnapshot();
456
+ updateLayout("border_color", hex);
457
+ }}
458
+ swatches={paletteSwatches}
459
+ />
460
+ </SettingsField>
461
+
462
+ <SettingsField label="Width">
463
+ <div className="flex items-center gap-2">
464
+ <input
465
+ type="range"
466
+ min={0}
467
+ max={20}
468
+ value={parseInt(getBlockLayoutValue<string>(block, activeViewport, "border_width", "0"))}
469
+ onChange={(e) => updateLayout("border_width", e.target.value)}
470
+ className="flex-1 accent-[#076bff]"
471
+ />
472
+ <span className="text-xs text-neutral-900 w-10 text-right">
473
+ {getBlockLayoutValue<string>(block, activeViewport, "border_width", "0")}px
474
+ </span>
475
+ </div>
476
+ </SettingsField>
477
+
478
+ <SettingsField label="Style">
479
+ <select
480
+ value={getBlockLayoutValue<string>(block, activeViewport, "border_style", "none")}
481
+ onChange={(e) => updateLayout("border_style", e.target.value)}
482
+ className={SELECT_CLASS}
483
+ >
484
+ <option value="none">None</option>
485
+ <option value="solid">Solid</option>
486
+ <option value="dashed">Dashed</option>
487
+ <option value="dotted">Dotted</option>
488
+ </select>
489
+ </SettingsField>
490
+
491
+ <SettingsField label="Sides">
492
+ <select
493
+ value={getBlockLayoutValue<string>(block, activeViewport, "border_sides", "all")}
494
+ onChange={(e) => updateLayout("border_sides", e.target.value)}
495
+ className={SELECT_CLASS}
496
+ >
497
+ <option value="all">All</option>
498
+ <option value="top">Top</option>
499
+ <option value="right">Right</option>
500
+ <option value="bottom">Bottom</option>
501
+ <option value="left">Left</option>
502
+ <option value="top-bottom">Top & Bottom</option>
503
+ <option value="left-right">Left & Right</option>
504
+ </select>
505
+ </SettingsField>
506
+
507
+ <SettingsField label="Radius">
508
+ <div className="flex items-center gap-2">
509
+ <input
510
+ type="range"
511
+ min={0}
512
+ max={50}
513
+ value={parseInt(getBlockLayoutValue<string>(block, activeViewport, "border_radius", "0"))}
514
+ onChange={(e) => updateLayout("border_radius", e.target.value)}
515
+ className="flex-1 accent-[#076bff]"
516
+ />
517
+ <span className="text-xs text-neutral-900 w-10 text-right">
518
+ {getBlockLayoutValue<string>(block, activeViewport, "border_radius", "0")}px
519
+ </span>
520
+ </div>
521
+ </SettingsField>
522
+
523
+ <OverrideBadge
524
+ block={block}
525
+ viewport={activeViewport}
526
+ properties={["border_color", "border_width", "border_style", "border_sides", "border_radius"]}
527
+ onReset={() => resetGroup(["border_color", "border_width", "border_style", "border_sides", "border_radius"])}
528
+ />
529
+ </SettingsSection>
530
+ </>
531
+ );
532
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ /**
4
+ * BlockSettings — Delegates to type-specific block editors.
5
+ *
6
+ * Session 64: Extracted from SettingsPanel.tsx.
7
+ */
8
+
9
+ import type { ContentBlock } from "../../../lib/sanity/types";
10
+ import {
11
+ TextBlockEditor,
12
+ ImageBlockEditor,
13
+ ImageGridBlockEditor,
14
+ VideoBlockEditor,
15
+ SpacerBlockEditor,
16
+ ButtonBlockEditor,
17
+ CoverBlockEditor,
18
+ ProjectGridEditor,
19
+ } from "../editors";
20
+
21
+ export default function BlockSettings({
22
+ block,
23
+ }: {
24
+ block: ContentBlock;
25
+ }) {
26
+ return <BlockTypeEditor block={block} />;
27
+ }
28
+
29
+ // ============================================
30
+ // Block Type Editor Router
31
+ // ============================================
32
+
33
+ function BlockTypeEditor({ block }: { block: ContentBlock }) {
34
+ switch (block._type) {
35
+ case "textBlock":
36
+ return (
37
+ <TextBlockEditor
38
+ block={block as import("@/lib/sanity/types").TextBlock}
39
+ />
40
+ );
41
+ case "imageBlock":
42
+ return (
43
+ <ImageBlockEditor
44
+ block={block as import("@/lib/sanity/types").ImageBlock}
45
+ />
46
+ );
47
+ case "imageGridBlock":
48
+ return (
49
+ <ImageGridBlockEditor
50
+ block={block as import("@/lib/sanity/types").ImageGridBlock}
51
+ />
52
+ );
53
+ case "videoBlock":
54
+ return (
55
+ <VideoBlockEditor
56
+ block={block as import("@/lib/sanity/types").VideoBlock}
57
+ />
58
+ );
59
+ case "spacerBlock":
60
+ return (
61
+ <SpacerBlockEditor
62
+ block={block as import("@/lib/sanity/types").SpacerBlock}
63
+ />
64
+ );
65
+ case "buttonBlock":
66
+ return (
67
+ <ButtonBlockEditor
68
+ block={block as import("@/lib/sanity/types").ButtonBlock}
69
+ />
70
+ );
71
+ case "coverBlock":
72
+ return (
73
+ <CoverBlockEditor
74
+ block={block as import("@/lib/sanity/types").CoverBlock}
75
+ />
76
+ );
77
+ case "projectGridBlock":
78
+ return (
79
+ <ProjectGridEditor
80
+ block={block as import("@/lib/sanity/types").ProjectGridBlock}
81
+ />
82
+ );
83
+ default:
84
+ return (
85
+ <div className="p-4">
86
+ <div className="rounded-lg bg-[#f5f5f5] p-3">
87
+ <p className="text-xs text-neutral-400">
88
+ No editor available for this block type.
89
+ </p>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+ }