@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,238 @@
1
+ import type { NavItem, NavDesign } from "../../../lib/sanity/types";
2
+
3
+ export const TOTAL_COLUMNS = 12;
4
+
5
+ // ── Occupancy helpers ──────────────────────────────────────
6
+
7
+ /** Get all column numbers occupied by items, optionally excluding one item */
8
+ export function getOccupiedColumns(items: NavItem[], excludeKey?: string): Set<number> {
9
+ const occupied = new Set<number>();
10
+ for (const item of items) {
11
+ if (item._key === excludeKey) continue;
12
+ for (let c = item.grid_column; c < item.grid_column + item.column_span; c++) {
13
+ if (c >= 1 && c <= TOTAL_COLUMNS) occupied.add(c);
14
+ }
15
+ }
16
+ return occupied;
17
+ }
18
+
19
+ /** Check if an item can be placed at column with given span */
20
+ export function canPlace(
21
+ items: NavItem[],
22
+ column: number,
23
+ span: number,
24
+ excludeKey?: string
25
+ ): boolean {
26
+ if (column < 1 || column + span - 1 > TOTAL_COLUMNS) return false;
27
+ const occupied = getOccupiedColumns(items, excludeKey);
28
+ for (let c = column; c < column + span; c++) {
29
+ if (occupied.has(c)) return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ /** Get maximum span from a column before hitting another item or grid edge */
35
+ export function getMaxSpan(items: NavItem[], column: number, currentKey?: string): number {
36
+ const occupied = getOccupiedColumns(items, currentKey);
37
+ let maxSpan = 0;
38
+ for (let c = column; c <= TOTAL_COLUMNS; c++) {
39
+ if (occupied.has(c)) break;
40
+ maxSpan++;
41
+ }
42
+ return maxSpan;
43
+ }
44
+
45
+ /** Find the next free column from a starting point */
46
+ export function findNextFreeColumn(items: NavItem[], from = 1): number | null {
47
+ const occupied = getOccupiedColumns(items);
48
+ for (let c = from; c <= TOTAL_COLUMNS; c++) {
49
+ if (!occupied.has(c)) return c;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** Validate all items don't overlap and are in range */
55
+ export function validateLayout(items: NavItem[]): { valid: boolean; conflicts: string[] } {
56
+ const conflicts: string[] = [];
57
+ const columnOwner = new Map<number, string>();
58
+
59
+ for (const item of items) {
60
+ for (let c = item.grid_column; c < item.grid_column + item.column_span; c++) {
61
+ if (c < 1 || c > TOTAL_COLUMNS) {
62
+ conflicts.push(`"${item.label}" extends beyond grid (column ${c})`);
63
+ continue;
64
+ }
65
+ const existing = columnOwner.get(c);
66
+ if (existing) {
67
+ conflicts.push(`Column ${c} conflict: "${item.label}" overlaps with "${existing}"`);
68
+ } else {
69
+ columnOwner.set(c, item.label);
70
+ }
71
+ }
72
+ }
73
+
74
+ return { valid: conflicts.length === 0, conflicts };
75
+ }
76
+
77
+ /** Get list of free (unoccupied) column numbers */
78
+ export function getFreeColumns(items: NavItem[]): number[] {
79
+ const occupied = getOccupiedColumns(items);
80
+ const free: number[] = [];
81
+ for (let c = 1; c <= TOTAL_COLUMNS; c++) {
82
+ if (!occupied.has(c)) free.push(c);
83
+ }
84
+ return free;
85
+ }
86
+
87
+ /** Convert old format (logo in NavDesign + basic items) to new format (all items with type) */
88
+ export function migrateNavItems(items: NavItem[], design: NavDesign): NavItem[] {
89
+ // Empty array = no items yet (fresh start or user deleted all). Don't auto-create anything.
90
+ if (items.length === 0) return [];
91
+
92
+ // Check if items already have the new format
93
+ const hasTypedItems = items.some((i) => i.type === "logo" || i.type === "menu-item");
94
+
95
+ if (hasTypedItems) {
96
+ // Already migrated — but still run cleanup to fix common data issues
97
+ return cleanupNavItems(items);
98
+ }
99
+
100
+ const migrated: NavItem[] = [];
101
+ const logoCols = design.logo_columns ?? 3;
102
+
103
+ // Create logo item from design
104
+ migrated.push({
105
+ _key: "logo-migrated",
106
+ type: "logo",
107
+ label: design.logo_text || "Logo",
108
+ link_type: "internal",
109
+ internal_page: undefined,
110
+ external_url: "",
111
+ visible: true,
112
+ grid_column: 1,
113
+ column_span: logoCols,
114
+ });
115
+
116
+ // Migrate existing items as menu-items, starting AFTER the logo span
117
+ let nextFreeCol = 1 + logoCols;
118
+ for (const item of items) {
119
+ const col = item.grid_column && item.grid_column > logoCols ? item.grid_column : nextFreeCol;
120
+ const span = item.column_span || 1;
121
+ migrated.push({
122
+ ...item,
123
+ type: "menu-item",
124
+ column_span: span,
125
+ grid_column: Math.min(col, TOTAL_COLUMNS),
126
+ });
127
+ nextFreeCol = col + span;
128
+ }
129
+
130
+ return migrated;
131
+ }
132
+
133
+ /**
134
+ * Clean up common data issues in nav items:
135
+ * - Ensure all items have a type
136
+ * - Ensure only ONE logo item exists (keep first, convert duplicates to menu-item)
137
+ * - Ensure no two items occupy the same column (resolve overlaps)
138
+ * - Ensure all items have valid grid_column and column_span
139
+ */
140
+ function cleanupNavItems(items: NavItem[]): NavItem[] {
141
+ let logoFound = false;
142
+ const cleaned: NavItem[] = [];
143
+
144
+ for (const item of items) {
145
+ const patched = { ...item };
146
+
147
+ // Ensure type is set
148
+ if (!patched.type) {
149
+ patched.type = "menu-item";
150
+ }
151
+
152
+ // Ensure only one logo
153
+ if (patched.type === "logo") {
154
+ if (logoFound) {
155
+ // Duplicate logo — convert to menu-item
156
+ patched.type = "menu-item";
157
+ } else {
158
+ logoFound = true;
159
+ }
160
+ }
161
+
162
+ // Ensure valid grid position
163
+ if (!patched.grid_column || patched.grid_column < 1 || patched.grid_column > TOTAL_COLUMNS) {
164
+ patched.grid_column = 1;
165
+ }
166
+ if (!patched.column_span || patched.column_span < 1) {
167
+ patched.column_span = 1;
168
+ }
169
+ // Clamp span to not exceed grid
170
+ if (patched.grid_column + patched.column_span - 1 > TOTAL_COLUMNS) {
171
+ patched.column_span = TOTAL_COLUMNS - patched.grid_column + 1;
172
+ }
173
+
174
+ cleaned.push(patched);
175
+ }
176
+
177
+ // Resolve overlapping items — nudge overlapping items to the next free column
178
+ const resolved: NavItem[] = [];
179
+ const occupiedCols = new Set<number>();
180
+
181
+ for (const item of cleaned) {
182
+ // Check if this item's columns are already occupied
183
+ let hasOverlap = false;
184
+ for (let c = item.grid_column; c < item.grid_column + item.column_span; c++) {
185
+ if (occupiedCols.has(c)) {
186
+ hasOverlap = true;
187
+ break;
188
+ }
189
+ }
190
+
191
+ if (hasOverlap) {
192
+ // Find next free position that can fit this item
193
+ let placed = false;
194
+ for (let startCol = 1; startCol <= TOTAL_COLUMNS - item.column_span + 1; startCol++) {
195
+ let fits = true;
196
+ for (let c = startCol; c < startCol + item.column_span; c++) {
197
+ if (occupiedCols.has(c)) {
198
+ fits = false;
199
+ break;
200
+ }
201
+ }
202
+ if (fits) {
203
+ const relocated = { ...item, grid_column: startCol };
204
+ resolved.push(relocated);
205
+ for (let c = startCol; c < startCol + relocated.column_span; c++) {
206
+ occupiedCols.add(c);
207
+ }
208
+ placed = true;
209
+ break;
210
+ }
211
+ }
212
+ // If no space at all, shrink to span 1 and try again
213
+ if (!placed) {
214
+ for (let startCol = 1; startCol <= TOTAL_COLUMNS; startCol++) {
215
+ if (!occupiedCols.has(startCol)) {
216
+ resolved.push({ ...item, grid_column: startCol, column_span: 1 });
217
+ occupiedCols.add(startCol);
218
+ placed = true;
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ // If still not placed (grid is full), skip this item
224
+ if (!placed) continue;
225
+ } else {
226
+ // No overlap — place as-is
227
+ resolved.push(item);
228
+ for (let c = item.grid_column; c < item.grid_column + item.column_span; c++) {
229
+ occupiedCols.add(c);
230
+ }
231
+ }
232
+ }
233
+
234
+ return resolved;
235
+ }
236
+
237
+ // Re-export from centralized color-utils
238
+ export { hexToRgba } from "../../../lib/color-utils";
@@ -0,0 +1,218 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useEffect } from "react";
4
+ import { csrfHeaders } from "../../../lib/csrf-client";
5
+ import { getSiteConfig } from "../../../lib/config";
6
+ import type { WizardStepProps } from "./SetupWizard";
7
+
8
+ // ── Icons ──
9
+
10
+ function CheckCircle() {
11
+ return (
12
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
13
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
14
+ <polyline points="22 4 12 14.01 9 11.01" />
15
+ </svg>
16
+ );
17
+ }
18
+
19
+ function Spinner() {
20
+ return (
21
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
22
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
23
+ </svg>
24
+ );
25
+ }
26
+
27
+ // ── Component ──
28
+
29
+ /**
30
+ * Step 4 — Branding (minimal)
31
+ *
32
+ * Sets the site title via the existing settings API.
33
+ * Full branding (fonts, colors, typography) is available in /admin/styles.
34
+ */
35
+ export function BrandingStep({ onNext, onBack }: WizardStepProps) {
36
+ const config = getSiteConfig();
37
+
38
+ const [siteTitle, setSiteTitle] = useState("");
39
+ const [loading, setLoading] = useState(true);
40
+ const [saving, setSaving] = useState(false);
41
+ const [saved, setSaved] = useState(false);
42
+ const [error, setError] = useState<string | null>(null);
43
+
44
+ // Load current settings on mount
45
+ useEffect(() => {
46
+ fetch("/api/admin/settings")
47
+ .then((res) => (res.ok ? res.json() : null))
48
+ .then((data) => {
49
+ if (data?.settings) {
50
+ // Use existing title or fall back to config default
51
+ setSiteTitle(data.settings.default_title || "");
52
+ }
53
+ setLoading(false);
54
+ })
55
+ .catch(() => setLoading(false));
56
+ }, []);
57
+
58
+ const handleSave = useCallback(async () => {
59
+ if (!siteTitle.trim()) {
60
+ setError("Please enter a site title");
61
+ return;
62
+ }
63
+
64
+ setSaving(true);
65
+ setError(null);
66
+
67
+ try {
68
+ const res = await fetch("/api/admin/settings", {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ ...csrfHeaders(),
73
+ },
74
+ body: JSON.stringify({
75
+ section: "metadata",
76
+ data: {
77
+ default_title: siteTitle.trim(),
78
+ default_description: "",
79
+ default_og_image: "",
80
+ favicon_path: "",
81
+ analytics_id: "",
82
+ },
83
+ }),
84
+ });
85
+
86
+ if (!res.ok) {
87
+ const data = await res.json();
88
+ setError(data.error || "Save failed");
89
+ return;
90
+ }
91
+
92
+ setSaved(true);
93
+ // Auto-advance after a brief pause
94
+ setTimeout(() => onNext(), 600);
95
+ } catch (err) {
96
+ setError(err instanceof Error ? err.message : "Save failed");
97
+ } finally {
98
+ setSaving(false);
99
+ }
100
+ }, [siteTitle, onNext]);
101
+
102
+ const inputClass =
103
+ "w-full rounded-lg border border-black/[0.08] bg-white px-3 py-2.5 text-sm text-[#333] placeholder:text-[#bbb] focus:outline-none focus:ring-2 focus:ring-[#076bff]/20 focus:border-[#076bff]/40";
104
+
105
+ if (loading) {
106
+ return (
107
+ <div className="flex flex-col items-center justify-center py-16 text-center">
108
+ <Spinner />
109
+ <p className="text-[#999] text-xs mt-3">Loading settings...</p>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div className="pt-8">
116
+ <h2 className="text-lg font-semibold text-[#111] mb-1">Branding</h2>
117
+ <p className="text-sm text-[#666] mb-8">
118
+ Set your site title. You can customize fonts, colors, and typography later in{" "}
119
+ <span className="font-medium text-[#333]">Customize</span>.
120
+ </p>
121
+
122
+ <div className="bg-white rounded-xl border border-black/[0.06] p-5 mb-6">
123
+ {/* Site Title */}
124
+ <div className="mb-4">
125
+ <label className="block text-xs font-medium text-[#666] mb-1.5">
126
+ Site Title
127
+ </label>
128
+ <input
129
+ type="text"
130
+ value={siteTitle}
131
+ onChange={(e) => {
132
+ setSiteTitle(e.target.value);
133
+ setSaved(false);
134
+ setError(null);
135
+ }}
136
+ placeholder={config.defaults.metaTitle || "My Website"}
137
+ className={inputClass}
138
+ maxLength={200}
139
+ disabled={saving}
140
+ />
141
+ <p className="text-[11px] text-[#bbb] mt-1.5">
142
+ Used in the browser tab, search results, and social sharing.
143
+ </p>
144
+ </div>
145
+
146
+ {/* Saved indicator */}
147
+ {saved && (
148
+ <div className="flex items-center gap-2 mt-3">
149
+ <CheckCircle />
150
+ <span className="text-xs text-green-600 font-medium">
151
+ Title saved
152
+ </span>
153
+ </div>
154
+ )}
155
+
156
+ {/* Info about further customization */}
157
+ <div className="mt-4 p-3 rounded-lg bg-[#f8f8f8] border border-black/[0.04]">
158
+ <p className="text-xs text-[#999] leading-relaxed">
159
+ Full branding options (logo, fonts, color palette, typography hierarchy)
160
+ are available in the <span className="font-medium text-[#666]">Customize</span> section
161
+ after setup is complete.
162
+ </p>
163
+ </div>
164
+ </div>
165
+
166
+ {error && (
167
+ <div className="p-3 rounded-lg bg-red-50 border border-red-200 mb-6">
168
+ <p className="text-xs text-red-700">{error}</p>
169
+ </div>
170
+ )}
171
+
172
+ {/* Actions */}
173
+ <div className="flex items-center justify-between">
174
+ <div>
175
+ {onBack && (
176
+ <button
177
+ onClick={onBack}
178
+ className="px-4 py-2 text-sm text-[#666] hover:text-[#333] transition-colors"
179
+ >
180
+ Back
181
+ </button>
182
+ )}
183
+ </div>
184
+
185
+ <div className="flex items-center gap-3">
186
+ {/* Skip */}
187
+ <button
188
+ onClick={onNext}
189
+ className="px-4 py-2 text-sm text-[#999] hover:text-[#333] transition-colors"
190
+ >
191
+ Skip for now
192
+ </button>
193
+
194
+ {/* Save & Continue */}
195
+ {!saved && (
196
+ <button
197
+ onClick={handleSave}
198
+ disabled={saving || !siteTitle.trim()}
199
+ className="px-5 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors disabled:opacity-50"
200
+ >
201
+ {saving ? "Saving..." : "Save & Continue"}
202
+ </button>
203
+ )}
204
+
205
+ {/* Next (after saved) */}
206
+ {saved && (
207
+ <button
208
+ onClick={onNext}
209
+ className="px-5 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
210
+ >
211
+ Next
212
+ </button>
213
+ )}
214
+ </div>
215
+ </div>
216
+ </div>
217
+ );
218
+ }