@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.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +210 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- 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
|
+
}
|