@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,160 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ColumnV2Settings — Settings panel for a selected column within a V2 section.
5
+ *
6
+ * Settings tab only — no Layout or Animation sub-tabs.
7
+ * - Size: numeric input (1 to grid_columns), triggers cascade on change
8
+ * - Desktop: modifies base column data via cascade engine
9
+ * - Tablet/Phone: writes to responsive column overrides
10
+ * - Block count info
11
+ * - Delete column button
12
+ *
13
+ * Session 83: Phase 4 of V2 Grid System.
14
+ * Session 84: Phase 7 — viewport-aware column editing.
15
+ */
16
+
17
+ import { useBuilderStore } from "../../../lib/builder/store";
18
+ import type { PageSectionV2, SectionColumn } from "../../../lib/sanity/types";
19
+ import {
20
+ SettingsField,
21
+ SettingsSection,
22
+ } from "../editors/shared";
23
+ import {
24
+ getColumnV2Override,
25
+ buildSingleColumnV2Override,
26
+ hasAnyColumnV2Overrides,
27
+ } from "./responsive-helpers";
28
+
29
+ export default function ColumnV2Settings({
30
+ section,
31
+ column,
32
+ }: {
33
+ section: PageSectionV2;
34
+ column: SectionColumn;
35
+ }) {
36
+ const store = useBuilderStore();
37
+ const activeViewport = store.activeViewport;
38
+ const isResponsive = activeViewport !== "desktop";
39
+ const gridColumns = section.settings.grid_columns;
40
+
41
+ // Effective values: responsive override or base
42
+ const effectiveSpan = (isResponsive
43
+ ? getColumnV2Override(section, column._key, activeViewport, "span")
44
+ : undefined) ?? column.span;
45
+
46
+ const effectiveGridColumn = (isResponsive
47
+ ? getColumnV2Override(section, column._key, activeViewport, "grid_column")
48
+ : undefined) ?? column.grid_column;
49
+
50
+ const effectiveGridRow = (isResponsive
51
+ ? getColumnV2Override(section, column._key, activeViewport, "grid_row")
52
+ : undefined) ?? column.grid_row;
53
+
54
+ const hasSpanOverride = isResponsive &&
55
+ getColumnV2Override(section, column._key, activeViewport, "span") !== undefined;
56
+
57
+ const handleSpanChange = (newSpan: number) => {
58
+ const clamped = Math.max(1, Math.min(gridColumns, newSpan));
59
+ if (clamped === effectiveSpan) return;
60
+
61
+ if (isResponsive) {
62
+ // Write span override to responsive
63
+ const responsive = buildSingleColumnV2Override(section, activeViewport, column._key, {
64
+ span: clamped,
65
+ });
66
+ store.updateSectionV2Responsive(section._key, responsive ?? undefined);
67
+ } else {
68
+ // Desktop: use cascade engine
69
+ store.resizeColumnV2(section._key, column._key, clamped);
70
+ }
71
+ };
72
+
73
+ const handleResetSpan = () => {
74
+ // Remove span override for this column
75
+ const responsive = buildSingleColumnV2Override(section, activeViewport, column._key, {
76
+ span: undefined,
77
+ });
78
+ store.updateSectionV2Responsive(section._key, responsive ?? undefined);
79
+ };
80
+
81
+ return (
82
+ <>
83
+ {isResponsive && (
84
+ <div className="px-4 pt-3">
85
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
86
+ <span className="text-[11px] font-medium text-[#076bff]">
87
+ Editing {activeViewport === "tablet" ? "Tablet" : "Phone"} overrides
88
+ </span>
89
+ </div>
90
+ </div>
91
+ )}
92
+
93
+ <SettingsSection title="Column Size" defaultOpen>
94
+ <SettingsField label={
95
+ <span>
96
+ Span
97
+ {hasSpanOverride && (
98
+ <span className="ml-1 text-[9px] text-[#076bff]">overridden</span>
99
+ )}
100
+ </span>
101
+ }>
102
+ <div className="flex items-center gap-2">
103
+ <input
104
+ type="range"
105
+ min={1}
106
+ max={gridColumns}
107
+ value={effectiveSpan}
108
+ onChange={(e) => handleSpanChange(parseInt(e.target.value))}
109
+ className="flex-1 accent-[#076bff]"
110
+ />
111
+ <span className="text-xs text-neutral-900 w-12 text-right font-medium">
112
+ {effectiveSpan}/{gridColumns}
113
+ </span>
114
+ {hasSpanOverride && (
115
+ <button
116
+ onClick={handleResetSpan}
117
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
118
+ >
119
+ Reset
120
+ </button>
121
+ )}
122
+ </div>
123
+ {/* Visual grid indicator */}
124
+ <div className="flex gap-0.5 mt-1.5">
125
+ {Array.from({ length: gridColumns }).map((_, i) => {
126
+ const colStart = effectiveGridColumn - 1; // 0-based for display
127
+ const colEnd = colStart + effectiveSpan;
128
+ const isActive = i >= colStart && i < colEnd;
129
+ return (
130
+ <div
131
+ key={i}
132
+ className={`h-1.5 flex-1 rounded-full transition-colors ${
133
+ isActive ? "bg-[#076bff]" : "bg-neutral-200"
134
+ }`}
135
+ />
136
+ );
137
+ })}
138
+ </div>
139
+ </SettingsField>
140
+
141
+ <SettingsField label="Position">
142
+ <p className="text-xs text-neutral-900 py-[7px]">
143
+ Column {effectiveGridColumn}, Row {effectiveGridRow}
144
+ {isResponsive && (
145
+ <span className="text-neutral-400"> (base: {column.grid_column}, {column.grid_row})</span>
146
+ )}
147
+ </p>
148
+ </SettingsField>
149
+
150
+ <SettingsField label="Blocks">
151
+ <p className="text-xs text-neutral-900 py-[7px]">
152
+ {(column.blocks || []).length} block
153
+ {(column.blocks || []).length !== 1 ? "s" : ""}
154
+ </p>
155
+ </SettingsField>
156
+ </SettingsSection>
157
+
158
+ </>
159
+ );
160
+ }
@@ -0,0 +1,310 @@
1
+ "use client";
2
+
3
+ /**
4
+ * LayoutTab — Page Section styling (spacing, background, offset, border).
5
+ * Viewport-aware with responsive override support.
6
+ *
7
+ * Session 64: Extracted from SettingsPanel.tsx.
8
+ * Session 65: Split out RowLayoutPresetPicker, TRBLInputs, BlockLayoutTab
9
+ * into separate modules. This file now contains only LayoutTab.
10
+ */
11
+
12
+ import { useBuilderStore } from "../../../lib/builder/store";
13
+ import { resolveEffectiveSpacing } from "../../../lib/builder/layout-styles";
14
+ import type { PageSection } from "../../../lib/sanity/types";
15
+ import {
16
+ SettingsField,
17
+ SettingsSection,
18
+ SELECT_CLASS,
19
+ AssetPathInput,
20
+ } from "../editors/shared";
21
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
22
+ import {
23
+ getRowSettingValue,
24
+ hasRowSettingOverride,
25
+ setRowResponsiveOverride,
26
+ } from "./responsive-helpers";
27
+ import { TRBLInputs } from "./TRBLInputs";
28
+
29
+ /**
30
+ * BUG-007 fix: LayoutTab now handles PageSection styling (spacing, background, border).
31
+ * BUG-013 fix: Sections support responsive overrides (tablet/phone).
32
+ */
33
+ export function LayoutTab({ section, sectionKey }: { section: PageSection; sectionKey: string }) {
34
+ const store = useBuilderStore();
35
+ const paletteSwatches = usePaletteSwatches();
36
+ const settings = section.settings || {};
37
+ const activeViewport = store.activeViewport;
38
+
39
+ const updateSetting = (updates: Partial<NonNullable<PageSection["settings"]>>) => {
40
+ store.updateSectionSettings(sectionKey, updates);
41
+ };
42
+
43
+ /** Update a setting, viewport-aware. Supports sections with responsive overrides. */
44
+ const updateSettingResponsive = (property: string, value: unknown) => {
45
+ // BUG-013 fix: Sections now support responsive overrides
46
+ if (activeViewport === "desktop") {
47
+ store.updateSectionSettings(sectionKey, { [property]: value });
48
+ } else {
49
+ // Build responsive override for section
50
+ const existing = section.responsive || {};
51
+ const vp = activeViewport as "tablet" | "phone";
52
+ const vpOverrides = { ...(existing[vp] || {}), [property]: value };
53
+ if (value === undefined) delete (vpOverrides as Record<string, unknown>)[property];
54
+ const responsive = { ...existing, [vp]: vpOverrides };
55
+ if (Object.keys(vpOverrides).length === 0) delete responsive[vp];
56
+ store.updateSectionResponsive(sectionKey, Object.keys(responsive).length ? responsive : undefined);
57
+ }
58
+ };
59
+
60
+ // Resolve effective spacing — shows real values even when legacy enum is active
61
+ const effective = resolveEffectiveSpacing(settings);
62
+
63
+ // Viewport-aware spacing values
64
+ const effectiveSpacingTop = getRowSettingValue<string>(section, activeViewport, "spacing_top", effective.top);
65
+ const effectiveSpacingRight = getRowSettingValue<string>(section, activeViewport, "spacing_right", effective.right);
66
+ const effectiveSpacingBottom = getRowSettingValue<string>(section, activeViewport, "spacing_bottom", effective.bottom);
67
+ const effectiveSpacingLeft = getRowSettingValue<string>(section, activeViewport, "spacing_left", effective.left);
68
+
69
+ // Parse background color + opacity to display
70
+ const bgOpacity = getRowSettingValue<number>(section, activeViewport, "background_opacity", settings.background_opacity ?? 100);
71
+
72
+ const viewportLabel = activeViewport !== "desktop"
73
+ ? activeViewport === "tablet" ? "Tablet" : "Phone"
74
+ : null;
75
+
76
+ return (
77
+ <>
78
+ {viewportLabel && (
79
+ <div className="px-4 pt-3">
80
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
81
+ <span className="text-[11px] font-medium text-[#076bff]">
82
+ Editing {viewportLabel} overrides
83
+ </span>
84
+ </div>
85
+ </div>
86
+ )}
87
+
88
+ {/* Spacing (Padding) */}
89
+ <SettingsSection title="Spacing" defaultOpen>
90
+ <TRBLInputs
91
+ top={effectiveSpacingTop}
92
+ right={effectiveSpacingRight}
93
+ bottom={effectiveSpacingBottom}
94
+ left={effectiveSpacingLeft}
95
+ onChange={(field, value) => {
96
+ if (activeViewport === "desktop") {
97
+ // When user edits TRBL, set explicit TRBL values
98
+ const base = resolveEffectiveSpacing(settings);
99
+ updateSetting({
100
+ spacing_top: field === "top" ? value : (settings.spacing_top ?? base.top),
101
+ spacing_right: field === "right" ? value : (settings.spacing_right ?? base.right),
102
+ spacing_bottom: field === "bottom" ? value : (settings.spacing_bottom ?? base.bottom),
103
+ spacing_left: field === "left" ? value : (settings.spacing_left ?? base.left),
104
+ });
105
+ } else {
106
+ updateSettingResponsive(`spacing_${field}`, value);
107
+ }
108
+ }}
109
+ />
110
+ {activeViewport !== "desktop" && (
111
+ ["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"].some(
112
+ (p) => hasRowSettingOverride(section, activeViewport, p)
113
+ ) ? (
114
+ <div className="flex items-center gap-2 mt-1">
115
+ <span className="text-[9px] text-[#076bff]">overridden</span>
116
+ <button
117
+ onClick={() => {
118
+ // BUG-021 fix: use proper store action for responsive reset
119
+ let updated = section;
120
+ ["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"].forEach((p) => {
121
+ const updates = setRowResponsiveOverride(updated, activeViewport, p, undefined);
122
+ if (updates.responsive !== undefined) {
123
+ updated = { ...updated, responsive: updates.responsive };
124
+ }
125
+ });
126
+ store.updateSectionResponsive(sectionKey, (updated as PageSection).responsive);
127
+ }}
128
+ className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
129
+ >
130
+ Reset
131
+ </button>
132
+ </div>
133
+ ) : (
134
+ <p className="text-[9px] text-neutral-300 italic mt-1">inherited</p>
135
+ )
136
+ )}
137
+ </SettingsSection>
138
+
139
+ {/* Background */}
140
+ <SettingsSection title="Background" defaultOpen>
141
+ <SettingsField label="Color">
142
+ <ColorSwatchPicker
143
+ value={getRowSettingValue<string>(section, activeViewport, "background_color", "")}
144
+ onChange={(hex) => updateSettingResponsive("background_color", hex)}
145
+ swatches={paletteSwatches}
146
+ />
147
+ </SettingsField>
148
+
149
+ <SettingsField label="Opacity">
150
+ <div className="flex items-center gap-2">
151
+ <input
152
+ type="range"
153
+ min={0}
154
+ max={100}
155
+ value={bgOpacity}
156
+ onChange={(e) => updateSettingResponsive("background_opacity", parseInt(e.target.value))}
157
+ className="flex-1 accent-[#076bff]"
158
+ />
159
+ <span className="text-xs text-neutral-900 w-10 text-right">
160
+ {bgOpacity}%
161
+ </span>
162
+ </div>
163
+ </SettingsField>
164
+
165
+ <SettingsField label="Image">
166
+ <AssetPathInput
167
+ value={getRowSettingValue<string>(section, activeViewport, "background_image", "")}
168
+ onFocus={() => store._pushSnapshot()}
169
+ onChange={(v) => updateSettingResponsive("background_image", v)}
170
+ placeholder="path/to/image.jpg"
171
+ filterType="image"
172
+ />
173
+ </SettingsField>
174
+
175
+ {getRowSettingValue<string>(section, activeViewport, "background_image", "") && (
176
+ <>
177
+ <SettingsField label="Size">
178
+ <select
179
+ value={getRowSettingValue<string>(section, activeViewport, "background_size", "cover")}
180
+ onChange={(e) => updateSettingResponsive("background_size", e.target.value)}
181
+ className={SELECT_CLASS}
182
+ >
183
+ <option value="cover">Cover</option>
184
+ <option value="contain">Contain</option>
185
+ <option value="auto">Auto</option>
186
+ </select>
187
+ </SettingsField>
188
+
189
+ <SettingsField label="Position">
190
+ <select
191
+ value={getRowSettingValue<string>(section, activeViewport, "background_position", "center center")}
192
+ onFocus={() => store._pushSnapshot()}
193
+ onChange={(e) => updateSettingResponsive("background_position", e.target.value)}
194
+ className={SELECT_CLASS}
195
+ >
196
+ <option value="center center">Center</option>
197
+ <option value="top center">Top</option>
198
+ <option value="bottom center">Bottom</option>
199
+ <option value="left center">Left</option>
200
+ <option value="right center">Right</option>
201
+ <option value="top left">Top Left</option>
202
+ <option value="top right">Top Right</option>
203
+ <option value="bottom left">Bottom Left</option>
204
+ <option value="bottom right">Bottom Right</option>
205
+ </select>
206
+ </SettingsField>
207
+
208
+ <SettingsField label="Repeat">
209
+ <select
210
+ value={getRowSettingValue<string>(section, activeViewport, "background_repeat", "no-repeat")}
211
+ onChange={(e) => updateSettingResponsive("background_repeat", e.target.value)}
212
+ className={SELECT_CLASS}
213
+ >
214
+ <option value="no-repeat">No Repeat</option>
215
+ <option value="repeat">Repeat</option>
216
+ <option value="repeat-x">Repeat X</option>
217
+ <option value="repeat-y">Repeat Y</option>
218
+ </select>
219
+ </SettingsField>
220
+ </>
221
+ )}
222
+ </SettingsSection>
223
+
224
+ {/* Offset (Margin) */}
225
+ <SettingsSection title="Offset">
226
+ <TRBLInputs
227
+ top={getRowSettingValue<string>(section, activeViewport, "offset_top", "0")}
228
+ right={getRowSettingValue<string>(section, activeViewport, "offset_right", "0")}
229
+ bottom={getRowSettingValue<string>(section, activeViewport, "offset_bottom", "0")}
230
+ left={getRowSettingValue<string>(section, activeViewport, "offset_left", "0")}
231
+ onChange={(field, value) => {
232
+ updateSettingResponsive(`offset_${field}`, value);
233
+ }}
234
+ />
235
+ </SettingsSection>
236
+
237
+ {/* Border */}
238
+ <SettingsSection title="Border">
239
+ <SettingsField label="Color">
240
+ <ColorSwatchPicker
241
+ value={getRowSettingValue<string>(section, activeViewport, "border_color", "")}
242
+ onChange={(hex) => updateSettingResponsive("border_color", hex)}
243
+ swatches={paletteSwatches}
244
+ />
245
+ </SettingsField>
246
+
247
+ <SettingsField label="Width">
248
+ <div className="flex items-center gap-2">
249
+ <input
250
+ type="range"
251
+ min={0}
252
+ max={20}
253
+ value={parseInt(getRowSettingValue<string>(section, activeViewport, "border_width", "0"))}
254
+ onChange={(e) => updateSettingResponsive("border_width", e.target.value)}
255
+ className="flex-1 accent-[#076bff]"
256
+ />
257
+ <span className="text-xs text-neutral-900 w-10 text-right">
258
+ {getRowSettingValue<string>(section, activeViewport, "border_width", "0")}px
259
+ </span>
260
+ </div>
261
+ </SettingsField>
262
+
263
+ <SettingsField label="Style">
264
+ <select
265
+ value={getRowSettingValue<string>(section, activeViewport, "border_style", "none")}
266
+ onChange={(e) => updateSettingResponsive("border_style", e.target.value)}
267
+ className={SELECT_CLASS}
268
+ >
269
+ <option value="none">None</option>
270
+ <option value="solid">Solid</option>
271
+ <option value="dashed">Dashed</option>
272
+ <option value="dotted">Dotted</option>
273
+ </select>
274
+ </SettingsField>
275
+
276
+ <SettingsField label="Sides">
277
+ <select
278
+ value={getRowSettingValue<string>(section, activeViewport, "border_sides", "all")}
279
+ onChange={(e) => updateSettingResponsive("border_sides", e.target.value)}
280
+ className={SELECT_CLASS}
281
+ >
282
+ <option value="all">All</option>
283
+ <option value="top">Top</option>
284
+ <option value="right">Right</option>
285
+ <option value="bottom">Bottom</option>
286
+ <option value="left">Left</option>
287
+ <option value="top-bottom">Top & Bottom</option>
288
+ <option value="left-right">Left & Right</option>
289
+ </select>
290
+ </SettingsField>
291
+
292
+ <SettingsField label="Radius">
293
+ <div className="flex items-center gap-2">
294
+ <input
295
+ type="range"
296
+ min={0}
297
+ max={50}
298
+ value={parseInt(getRowSettingValue<string>(section, activeViewport, "border_radius", "0"))}
299
+ onChange={(e) => updateSettingResponsive("border_radius", e.target.value)}
300
+ className="flex-1 accent-[#076bff]"
301
+ />
302
+ <span className="text-xs text-neutral-900 w-10 text-right">
303
+ {getRowSettingValue<string>(section, activeViewport, "border_radius", "0")}px
304
+ </span>
305
+ </div>
306
+ </SettingsField>
307
+ </SettingsSection>
308
+ </>
309
+ );
310
+ }
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ /**
4
+ * PageSettings — General, Appearance, and Navigation settings (Settings tab).
5
+ * PageSeoSettings — SEO fields (dedicated SEO tab at page level).
6
+ *
7
+ * Session 64: Extracted from SettingsPanel.tsx.
8
+ * Session 135: Reorganized — Layout tab replaced with SEO tab at page level,
9
+ * Nav Color moved from Appearance to Navigation, Nav Animation renamed to Navigation.
10
+ */
11
+
12
+ import { useState } from "react";
13
+ import { useBuilderStore } from "../../../lib/builder/store";
14
+ import {
15
+ SettingsField,
16
+ SettingsSection,
17
+ INPUT_CLASS,
18
+ } from "../editors/shared";
19
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
20
+
21
+ /** Convert a title to a URL-safe slug. Handles unicode (é, ñ, ü, etc.). */
22
+ function slugify(text: string): string {
23
+ return text
24
+ .normalize("NFD") // decompose accented chars (é → e + combining accent)
25
+ .replace(/[\u0300-\u036f]/g, "") // strip combining diacritical marks
26
+ .toLowerCase()
27
+ .trim()
28
+ .replace(/[^\w\s-]/g, "") // remove non-word chars
29
+ .replace(/[\s_]+/g, "-") // spaces/underscores → hyphens
30
+ .replace(/-+/g, "-") // collapse multiple hyphens
31
+ .replace(/^-|-$/g, ""); // trim leading/trailing hyphens
32
+ }
33
+
34
+ export default function PageSettings() {
35
+ const store = useBuilderStore();
36
+ const paletteSwatches = usePaletteSwatches();
37
+ const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
38
+
39
+ return (
40
+ <>
41
+ <SettingsSection title="General" defaultOpen>
42
+ <SettingsField label="Title">
43
+ <input
44
+ type="text"
45
+ value={store.pageTitle}
46
+ onChange={(e) => {
47
+ const newTitle = e.target.value;
48
+ store.setPageTitle(newTitle);
49
+ // Auto-update slug from title unless user manually edited the slug
50
+ if (!slugManuallyEdited) {
51
+ store.setPageSlug(slugify(newTitle));
52
+ }
53
+ }}
54
+ className={INPUT_CLASS}
55
+ />
56
+ </SettingsField>
57
+ <SettingsField label="Slug">
58
+ <input
59
+ type="text"
60
+ value={store.pageSlug}
61
+ onChange={(e) => {
62
+ setSlugManuallyEdited(true);
63
+ store.setPageSlug(e.target.value);
64
+ }}
65
+ className={INPUT_CLASS}
66
+ />
67
+ </SettingsField>
68
+ <SettingsField label="Type">
69
+ <p className="text-xs text-neutral-900 py-[7px]">
70
+ {store.pageType}
71
+ </p>
72
+ </SettingsField>
73
+ </SettingsSection>
74
+
75
+ <SettingsSection title="Appearance" defaultOpen>
76
+ <SettingsField label="Background">
77
+ <ColorSwatchPicker
78
+ value={store.pageSettings.background_color || ""}
79
+ onChange={(hex) => store.updatePageSettings({ background_color: hex || "transparent" })}
80
+ swatches={paletteSwatches}
81
+ />
82
+ </SettingsField>
83
+ <SettingsField label="Text Color">
84
+ <ColorSwatchPicker
85
+ value={store.pageSettings.text_color || ""}
86
+ onChange={(hex) => store.updatePageSettings({ text_color: hex })}
87
+ swatches={paletteSwatches}
88
+ />
89
+ </SettingsField>
90
+ </SettingsSection>
91
+
92
+ <SettingsSection title="Navigation">
93
+ <SettingsField label="Nav Color">
94
+ <ColorSwatchPicker
95
+ value={store.pageSettings.nav_color || ""}
96
+ onChange={(hex) => store.updatePageSettings({ nav_color: hex })}
97
+ swatches={paletteSwatches}
98
+ />
99
+ </SettingsField>
100
+ <SettingsField label="Animation Override" hint="Override global nav entrance for this page">
101
+ <select
102
+ value={store.pageSettings.nav_entrance_animation || ""}
103
+ onChange={(e) => store.updatePageSettings({ nav_entrance_animation: e.target.value as "" | "fade-in" | "slide-down" | "blur-in" })}
104
+ className={INPUT_CLASS}
105
+ >
106
+ <option value="">Use Global</option>
107
+ <option value="fade-in">Fade In</option>
108
+ <option value="slide-down">Slide Down</option>
109
+ <option value="blur-in">Blur In</option>
110
+ </select>
111
+ </SettingsField>
112
+ {store.pageSettings.nav_entrance_animation && (
113
+ <>
114
+ <SettingsField label={`Duration: ${store.pageSettings.nav_entrance_duration || 600}ms`}>
115
+ <input
116
+ type="range"
117
+ min={200}
118
+ max={5000}
119
+ step={50}
120
+ value={store.pageSettings.nav_entrance_duration || 600}
121
+ onChange={(e) => store.updatePageSettings({ nav_entrance_duration: Number(e.target.value) })}
122
+ className="w-full accent-[#076bff]"
123
+ />
124
+ </SettingsField>
125
+ <SettingsField label={`Delay: ${store.pageSettings.nav_entrance_delay || 0}ms`}>
126
+ <input
127
+ type="range"
128
+ min={0}
129
+ max={5000}
130
+ step={50}
131
+ value={store.pageSettings.nav_entrance_delay || 0}
132
+ onChange={(e) => store.updatePageSettings({ nav_entrance_delay: Number(e.target.value) })}
133
+ className="w-full accent-[#076bff]"
134
+ />
135
+ </SettingsField>
136
+ </>
137
+ )}
138
+ <SettingsField label="Disable Animation" hint="No nav animation on this page">
139
+ <button
140
+ type="button"
141
+ onClick={() => store.updatePageSettings({ nav_entrance_disabled: !store.pageSettings.nav_entrance_disabled })}
142
+ className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
143
+ store.pageSettings.nav_entrance_disabled ? "bg-[#076bff]" : "bg-neutral-300"
144
+ }`}
145
+ >
146
+ <span
147
+ className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${
148
+ store.pageSettings.nav_entrance_disabled ? "translate-x-[18px]" : "translate-x-[3px]"
149
+ }`}
150
+ />
151
+ </button>
152
+ </SettingsField>
153
+ </SettingsSection>
154
+
155
+ </>
156
+ );
157
+ }
158
+
159
+ /** SEO settings — rendered in the dedicated SEO tab at page level. */
160
+ export function PageSeoSettings() {
161
+ const store = useBuilderStore();
162
+
163
+ return (
164
+ <>
165
+ <SettingsSection title="SEO" defaultOpen>
166
+ <SettingsField label="SEO Title">
167
+ <input
168
+ type="text"
169
+ value={store.metadata.seo_title || ""}
170
+ onChange={(e) => store.setMetadata({ seo_title: e.target.value })}
171
+ className={INPUT_CLASS}
172
+ placeholder="Page title for search engines"
173
+ />
174
+ </SettingsField>
175
+ <SettingsField label="Description">
176
+ <textarea
177
+ value={store.metadata.seo_description || ""}
178
+ onChange={(e) =>
179
+ store.setMetadata({ seo_description: e.target.value })
180
+ }
181
+ rows={2}
182
+ className={`${INPUT_CLASS} resize-y`}
183
+ placeholder="Brief description for search results"
184
+ />
185
+ </SettingsField>
186
+ <SettingsField label="OG Image" hint="Social sharing image path">
187
+ <input
188
+ type="text"
189
+ value={store.metadata.og_image_path || ""}
190
+ onChange={(e) =>
191
+ store.setMetadata({ og_image_path: e.target.value })
192
+ }
193
+ className={INPUT_CLASS}
194
+ placeholder="og/page-image.jpg"
195
+ />
196
+ </SettingsField>
197
+ </SettingsSection>
198
+ </>
199
+ );
200
+ }