@morphika/andami 0.1.8 → 0.1.10
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/README.md +3 -0
- package/components/admin/nav-builder/NavBuilder.tsx +90 -14
- package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
- package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
- package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
- package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/SettingsPanel.tsx +29 -543
- package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
- package/components/builder/editors/CoverBlockEditor.tsx +14 -6
- package/components/builder/editors/ImageBlockEditor.tsx +8 -3
- package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
- package/components/builder/editors/ProjectGridEditor.tsx +7 -46
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
- package/components/builder/editors/StaggerSettings.tsx +2 -1
- package/components/builder/editors/TextBlockEditor.tsx +8 -3
- package/components/builder/editors/VideoBlockEditor.tsx +10 -4
- package/components/builder/editors/section-icons.tsx +492 -0
- package/components/builder/editors/shared.tsx +23 -4
- package/components/builder/live-preview/GhostCard.tsx +84 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +294 -1010
- package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -0
- package/components/builder/live-preview/drag-utils.tsx +89 -0
- package/components/builder/live-preview/useDragReorder.ts +370 -0
- package/components/builder/settings-panel/AnimationTab.tsx +152 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -0
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +32 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- package/components/builder/settings-panel/CustomSectionSettings.tsx +150 -0
- package/components/builder/settings-panel/LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/PageSettings.tsx +10 -4
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
- package/components/builder/settings-panel/index.ts +6 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +184 -0
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/serializer/migrations.ts +107 -0
- package/lib/builder/serializer/normalizers.ts +278 -0
- package/lib/builder/serializer/serializers.ts +393 -0
- package/lib/builder/serializer/shared.ts +102 -0
- package/lib/builder/serializer.ts +11 -846
- package/lib/sanity/types.ts +22 -0
- package/package.json +13 -10
- package/styles/base.css +7 -3
|
@@ -17,3 +17,9 @@ export { SectionV2AnimationTab } from "./SectionV2AnimationTab";
|
|
|
17
17
|
export { default as ColumnV2Settings } from "./ColumnV2Settings";
|
|
18
18
|
export { default as ParallaxSlideSettings } from "./ParallaxSlideSettings";
|
|
19
19
|
export { default as ParallaxGroupSettings } from "./ParallaxGroupSettings";
|
|
20
|
+
export { useSettingsPanelSelection } from "./useSettingsPanelSelection";
|
|
21
|
+
export type { SelectedBlockInfo, SelectedParallaxSlideInfo } from "./useSettingsPanelSelection";
|
|
22
|
+
export { AnimationTab, getBlockHoverEffect } from "./AnimationTab";
|
|
23
|
+
export { ColumnV2AnimationTab } from "./ColumnV2AnimationTab";
|
|
24
|
+
export { CardEntranceSection, ENTRANCE_PRESETS, CARD_ENTRANCE_SELECT_CLASS, CARD_ENTRANCE_SLIDER_CLASS } from "./CardEntranceSection";
|
|
25
|
+
export { CustomSectionSettings } from "./CustomSectionSettings";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSettingsPanelSelection — Custom hook that resolves the current builder
|
|
3
|
+
* selection into strongly-typed values for SettingsPanel routing.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from SettingsPanel.tsx in Session C (refactor split).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
9
|
+
import { ALL_BLOCK_INFO } from "../../../lib/builder/types";
|
|
10
|
+
import { BLOCK_GRADIENTS, BLOCK_ICON_COMPONENTS } from "../blockStyles";
|
|
11
|
+
import type {
|
|
12
|
+
ContentBlock,
|
|
13
|
+
ContentItem,
|
|
14
|
+
PageSection,
|
|
15
|
+
PageSectionV2,
|
|
16
|
+
CustomSectionInstance,
|
|
17
|
+
ParallaxGroup,
|
|
18
|
+
ParallaxSlideV2,
|
|
19
|
+
SectionColumn,
|
|
20
|
+
} from "../../../lib/sanity/types";
|
|
21
|
+
import {
|
|
22
|
+
isPageSection,
|
|
23
|
+
isPageSectionV2,
|
|
24
|
+
isCustomSectionInstance,
|
|
25
|
+
isParallaxGroup,
|
|
26
|
+
} from "../../../lib/sanity/types";
|
|
27
|
+
|
|
28
|
+
export interface SelectedBlockInfo {
|
|
29
|
+
block: ContentBlock;
|
|
30
|
+
rowKey: string;
|
|
31
|
+
colKey: string;
|
|
32
|
+
isSection: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SelectedParallaxSlideInfo {
|
|
36
|
+
group: ParallaxGroup;
|
|
37
|
+
slide: ParallaxSlideV2;
|
|
38
|
+
virtualSection: PageSectionV2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useSettingsPanelSelection() {
|
|
42
|
+
const store = useBuilderStore();
|
|
43
|
+
|
|
44
|
+
// Find selected elements — handle page sections, V2 sections, and parallax groups/slides
|
|
45
|
+
const selectedItem: ContentItem | undefined = store.rows.find((r) => r._key === store.selectedRowKey);
|
|
46
|
+
const selectedSection: PageSection | null = selectedItem && isPageSection(selectedItem) ? selectedItem : null;
|
|
47
|
+
const selectedSectionV2: PageSectionV2 | null = selectedItem && isPageSectionV2(selectedItem) ? selectedItem : null;
|
|
48
|
+
const selectedCustomSectionInstance: CustomSectionInstance | null = selectedItem && isCustomSectionInstance(selectedItem) ? selectedItem as CustomSectionInstance : null;
|
|
49
|
+
|
|
50
|
+
// Parallax detection: group selected directly, or slide selected (search inside groups)
|
|
51
|
+
const selectedParallaxGroup: ParallaxGroup | null = selectedItem && isParallaxGroup(selectedItem) ? selectedItem as ParallaxGroup : null;
|
|
52
|
+
const selectedParallaxSlide: SelectedParallaxSlideInfo | null = (() => {
|
|
53
|
+
if (!store.selectedRowKey) return null;
|
|
54
|
+
for (const item of store.rows) {
|
|
55
|
+
if (!isParallaxGroup(item)) continue;
|
|
56
|
+
const group = item as ParallaxGroup;
|
|
57
|
+
const slide = group.slides.find((s) => s._key === store.selectedRowKey);
|
|
58
|
+
if (slide) {
|
|
59
|
+
// Create a virtual PageSectionV2 for the slide so we can delegate to SectionV2Settings etc.
|
|
60
|
+
const virtualSection: PageSectionV2 = {
|
|
61
|
+
_type: "pageSectionV2",
|
|
62
|
+
_key: slide._key,
|
|
63
|
+
section_type: "empty-v2",
|
|
64
|
+
columns: slide.columns,
|
|
65
|
+
settings: slide.section_settings,
|
|
66
|
+
};
|
|
67
|
+
return { group, slide, virtualSection };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
// V2 column: when a V2 section (or parallax slide) is selected and a column key is set
|
|
74
|
+
const effectiveSectionV2 = selectedSectionV2 || selectedParallaxSlide?.virtualSection || null;
|
|
75
|
+
const selectedColumnV2: SectionColumn | null = effectiveSectionV2 && store.selectedColumnKey
|
|
76
|
+
? effectiveSectionV2.columns.find((c) => c._key === store.selectedColumnKey) || null
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
// For PageSections, the "block" is section.block[0] — selected automatically
|
|
80
|
+
const selectedBlock: SelectedBlockInfo | null = (() => {
|
|
81
|
+
// If a PageSection is selected, its block is the section block
|
|
82
|
+
if (selectedSection) {
|
|
83
|
+
const block = selectedSection.block[0];
|
|
84
|
+
if (block) return { block, rowKey: selectedSection._key, colKey: "", isSection: true };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Regular block search inside rows, V2 sections, and parallax slides
|
|
88
|
+
if (!store.selectedBlockKey) return null;
|
|
89
|
+
for (const item of store.rows) {
|
|
90
|
+
// V2 sections: search inside columns
|
|
91
|
+
if (isPageSectionV2(item)) {
|
|
92
|
+
for (const col of (item as PageSectionV2).columns || []) {
|
|
93
|
+
const block = (col.blocks || []).find(
|
|
94
|
+
(b) => b._key === store.selectedBlockKey
|
|
95
|
+
);
|
|
96
|
+
if (block) return { block, rowKey: item._key, colKey: col._key, isSection: false };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Parallax groups: search inside slide columns
|
|
100
|
+
if (isParallaxGroup(item)) {
|
|
101
|
+
const group = item as ParallaxGroup;
|
|
102
|
+
for (const slide of group.slides) {
|
|
103
|
+
for (const col of slide.columns || []) {
|
|
104
|
+
const block = (col.blocks || []).find(
|
|
105
|
+
(b) => b._key === store.selectedBlockKey
|
|
106
|
+
);
|
|
107
|
+
if (block) return { block, rowKey: slide._key, colKey: col._key, isSection: false };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
// Derive the panel title + icon from what's selected
|
|
116
|
+
const blockInfo = selectedBlock
|
|
117
|
+
? ALL_BLOCK_INFO.find((b) => b.type === selectedBlock.block._type)
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
// BUG-V2-003 fix: Block selection takes priority over V2 column/section
|
|
121
|
+
const panelTitle = selectedBlock
|
|
122
|
+
? blockInfo?.label || selectedBlock.block._type
|
|
123
|
+
: selectedColumnV2
|
|
124
|
+
? "Column"
|
|
125
|
+
: selectedParallaxSlide
|
|
126
|
+
? `Slide ${selectedParallaxSlide.group.slides.findIndex((s) => s._key === selectedParallaxSlide.slide._key) + 1}`
|
|
127
|
+
: selectedParallaxGroup
|
|
128
|
+
? "Parallax Showcase"
|
|
129
|
+
: selectedCustomSectionInstance
|
|
130
|
+
? (selectedCustomSectionInstance.custom_section_title || "Saved Section")
|
|
131
|
+
: selectedSectionV2
|
|
132
|
+
? "Section"
|
|
133
|
+
: selectedSection
|
|
134
|
+
? (selectedSection.section_type === "projectGrid" ? "Project Grid" : "Parallax Section")
|
|
135
|
+
: "Page";
|
|
136
|
+
|
|
137
|
+
// Resolve gradient + icon component for the header
|
|
138
|
+
const headerStyleKey = selectedBlock
|
|
139
|
+
? selectedBlock.block._type
|
|
140
|
+
: selectedColumnV2
|
|
141
|
+
? "column"
|
|
142
|
+
: (selectedParallaxSlide || selectedParallaxGroup)
|
|
143
|
+
? "parallaxGroup"
|
|
144
|
+
: selectedCustomSectionInstance
|
|
145
|
+
? "customSectionInstance"
|
|
146
|
+
: selectedSectionV2
|
|
147
|
+
? "row"
|
|
148
|
+
: selectedSection
|
|
149
|
+
? (selectedSection.block[0]?._type || "row")
|
|
150
|
+
: "page";
|
|
151
|
+
const headerGradient = BLOCK_GRADIENTS[headerStyleKey] || BLOCK_GRADIENTS.page;
|
|
152
|
+
const HeaderIconComponent = BLOCK_ICON_COMPONENTS[headerStyleKey];
|
|
153
|
+
|
|
154
|
+
const hasSelection = !!(store.selectedRowKey || store.selectedColumnKey || store.selectedBlockKey);
|
|
155
|
+
// V2 columns: show Settings + Animation tabs (not Layout) — but NOT when a block inside the column is selected
|
|
156
|
+
const isColumnOnly = !!(selectedColumnV2 && !selectedBlock);
|
|
157
|
+
// Parallax group header: show Settings + Animation (no Layout)
|
|
158
|
+
const isParallaxGroupOnly = !!(selectedParallaxGroup && !selectedParallaxSlide && !selectedBlock);
|
|
159
|
+
// Custom section instance: show all 3 tabs (Settings with Edit/Detach, Layout, Animation)
|
|
160
|
+
const isCustomSectionOnly = !!(selectedCustomSectionInstance && !selectedBlock);
|
|
161
|
+
// Page level: nothing selected — show Settings + SEO + Animation (no Layout)
|
|
162
|
+
const isPageLevel = !hasSelection;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
selectedItem,
|
|
166
|
+
selectedSection,
|
|
167
|
+
selectedSectionV2,
|
|
168
|
+
selectedCustomSectionInstance,
|
|
169
|
+
selectedParallaxGroup,
|
|
170
|
+
selectedParallaxSlide,
|
|
171
|
+
effectiveSectionV2,
|
|
172
|
+
selectedColumnV2,
|
|
173
|
+
selectedBlock,
|
|
174
|
+
panelTitle,
|
|
175
|
+
headerStyleKey,
|
|
176
|
+
headerGradient,
|
|
177
|
+
HeaderIconComponent,
|
|
178
|
+
hasSelection,
|
|
179
|
+
isColumnOnly,
|
|
180
|
+
isParallaxGroupOnly,
|
|
181
|
+
isCustomSectionOnly,
|
|
182
|
+
isPageLevel,
|
|
183
|
+
};
|
|
184
|
+
}
|
package/components/ui/Navbar.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import type { NavItem, NavDesign, NavEntrancePreset, MobileNavDesign } from "../../lib/sanity/types";
|
|
6
|
+
import type { NavItem, NavDesign, NavEntrancePreset, NavDesignResponsiveOverride, MobileNavDesign } from "../../lib/sanity/types";
|
|
7
7
|
import { useNavColor } from "../../lib/contexts/NavColorContext";
|
|
8
8
|
import { useNavAnimation } from "../../lib/contexts/NavAnimationContext";
|
|
9
9
|
import { usePageExit } from "../../lib/contexts/PageExitContext";
|
|
@@ -23,6 +23,79 @@ const colorMap: Record<string, string> = {
|
|
|
23
23
|
white: "text-brand-text",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// ============================================
|
|
27
|
+
// Responsive CSS custom properties generator
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
/** Breakpoints matching the builder's responsive system */
|
|
31
|
+
const NAV_BREAKPOINTS = { tablet: 1024, phone: 640 } as const;
|
|
32
|
+
|
|
33
|
+
/** Generates a <style> block with CSS custom properties for responsive nav overrides.
|
|
34
|
+
* Desktop values are set on the scoping selector; tablet/phone overrides via media queries.
|
|
35
|
+
* Returns empty string if no responsive overrides exist. */
|
|
36
|
+
function buildResponsiveNavCSS(design: NavDesign | undefined, scopeSelector: string): string {
|
|
37
|
+
if (!design) return "";
|
|
38
|
+
|
|
39
|
+
// Desktop (base) custom properties
|
|
40
|
+
const vars: Record<string, string> = {
|
|
41
|
+
"--nav-font-size": `${design.font_size ?? 14}px`,
|
|
42
|
+
"--nav-font-weight": design.font_weight || "400",
|
|
43
|
+
"--nav-text-transform": design.text_transform || "uppercase",
|
|
44
|
+
"--nav-text-align": design.text_align || "left",
|
|
45
|
+
"--nav-vertical-align": (() => {
|
|
46
|
+
const v = design.vertical_align || "top";
|
|
47
|
+
return v === "bottom" ? "end" : v === "middle" ? "center" : "start";
|
|
48
|
+
})(),
|
|
49
|
+
"--nav-items-justify": (() => {
|
|
50
|
+
const a = design.text_align || "left";
|
|
51
|
+
return a === "center" ? "center" : a === "right" ? "flex-end" : "flex-start";
|
|
52
|
+
})(),
|
|
53
|
+
"--nav-padding-h": `${design.padding_h ?? 24}px`,
|
|
54
|
+
"--nav-padding-v": `${design.padding_v ?? 27}px`,
|
|
55
|
+
"--nav-margin-h": `${design.margin_h ?? 0}px`,
|
|
56
|
+
"--nav-margin-v": `${design.margin_v ?? 0}px`,
|
|
57
|
+
"--nav-items-gap": `${design.items_gap ?? 32}px`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const responsive = design.responsive;
|
|
61
|
+
if (!responsive) {
|
|
62
|
+
// No responsive overrides — still emit base vars for consistency
|
|
63
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
64
|
+
return `${scopeSelector}{${baseLines}}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build media query blocks for tablet and phone
|
|
68
|
+
const mqBlocks: string[] = [];
|
|
69
|
+
for (const [viewport, bp] of [["tablet", NAV_BREAKPOINTS.tablet], ["phone", NAV_BREAKPOINTS.phone]] as const) {
|
|
70
|
+
const overrides = responsive[viewport] as NavDesignResponsiveOverride | undefined;
|
|
71
|
+
if (!overrides) continue;
|
|
72
|
+
const ovLines: string[] = [];
|
|
73
|
+
if (overrides.font_size != null) ovLines.push(`--nav-font-size:${overrides.font_size}px`);
|
|
74
|
+
if (overrides.font_weight != null) ovLines.push(`--nav-font-weight:${overrides.font_weight}`);
|
|
75
|
+
if (overrides.text_transform != null) ovLines.push(`--nav-text-transform:${overrides.text_transform}`);
|
|
76
|
+
if (overrides.text_align != null) {
|
|
77
|
+
ovLines.push(`--nav-text-align:${overrides.text_align}`);
|
|
78
|
+
const j = overrides.text_align === "center" ? "center" : overrides.text_align === "right" ? "flex-end" : "flex-start";
|
|
79
|
+
ovLines.push(`--nav-items-justify:${j}`);
|
|
80
|
+
}
|
|
81
|
+
if (overrides.vertical_align != null) {
|
|
82
|
+
const v = overrides.vertical_align === "bottom" ? "end" : overrides.vertical_align === "middle" ? "center" : "start";
|
|
83
|
+
ovLines.push(`--nav-vertical-align:${v}`);
|
|
84
|
+
}
|
|
85
|
+
if (overrides.padding_h != null) ovLines.push(`--nav-padding-h:${overrides.padding_h}px`);
|
|
86
|
+
if (overrides.padding_v != null) ovLines.push(`--nav-padding-v:${overrides.padding_v}px`);
|
|
87
|
+
if (overrides.margin_h != null) ovLines.push(`--nav-margin-h:${overrides.margin_h}px`);
|
|
88
|
+
if (overrides.margin_v != null) ovLines.push(`--nav-margin-v:${overrides.margin_v}px`);
|
|
89
|
+
if (overrides.items_gap != null) ovLines.push(`--nav-items-gap:${overrides.items_gap}px`);
|
|
90
|
+
if (ovLines.length > 0) {
|
|
91
|
+
mqBlocks.push(`@media(max-width:${bp}px){${scopeSelector}{${ovLines.join(";")}}}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const baseLines = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
|
|
96
|
+
return `${scopeSelector}{${baseLines}}${mqBlocks.join("")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
// ============================================
|
|
27
100
|
// NavLink — shared link component (eliminates duplication)
|
|
28
101
|
// ============================================
|
|
@@ -181,6 +254,18 @@ export default function Navbar({
|
|
|
181
254
|
// Map vertical_align to CSS align-items value for the grid container
|
|
182
255
|
const gridAlignItems = verticalAlign === "bottom" ? "end" : verticalAlign === "middle" ? "center" : "start";
|
|
183
256
|
|
|
257
|
+
// ── Responsive CSS custom properties (Session 164) ──
|
|
258
|
+
// Uses a stable ID scoped to the nav element. CSS vars + media queries
|
|
259
|
+
// ensure zero-flash SSR: the correct values apply before hydration.
|
|
260
|
+
const hasResponsive = !!(design?.responsive?.tablet || design?.responsive?.phone);
|
|
261
|
+
const reactId = useId();
|
|
262
|
+
const navScopeId = `nav-${reactId.replace(/:/g, "")}`;
|
|
263
|
+
const responsiveCSS = useMemo(
|
|
264
|
+
() => hasResponsive ? buildResponsiveNavCSS(design, `[data-nav-scope="${navScopeId}"]`) : "",
|
|
265
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
266
|
+
[hasResponsive, design?.responsive, navScopeId, fontSize, fontWeight, textTransformVal, textAlignVal, paddingH, paddingV, marginH, marginV, itemsGap, verticalAlign],
|
|
267
|
+
);
|
|
268
|
+
|
|
184
269
|
// ── Mobile menu resolved values (Session 158) ──
|
|
185
270
|
// Mobile styles are independent from page-level NavColorContext overrides.
|
|
186
271
|
// They fall back to desktop design values when not explicitly set.
|
|
@@ -374,13 +459,23 @@ export default function Navbar({
|
|
|
374
459
|
// When using hex color, we must set `color: inherit` on links because browser
|
|
375
460
|
// user-agent stylesheet sets `a { color: ... }` which overrides CSS inheritance.
|
|
376
461
|
const linkClassName = `tracking-normal ${textColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
462
|
+
// When responsive overrides exist, link styles reference CSS custom properties
|
|
463
|
+
// so they respond to media queries without JS. Otherwise use direct values.
|
|
464
|
+
const linkStyle: React.CSSProperties = hasResponsive
|
|
465
|
+
? {
|
|
466
|
+
fontSize: "var(--nav-font-size)",
|
|
467
|
+
fontWeight: "var(--nav-font-weight)" as React.CSSProperties["fontWeight"],
|
|
468
|
+
textTransform: "var(--nav-text-transform)" as React.CSSProperties["textTransform"],
|
|
469
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
470
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
471
|
+
}
|
|
472
|
+
: {
|
|
473
|
+
fontSize: `${fontSize}px`,
|
|
474
|
+
fontWeight: fontWeight as React.CSSProperties["fontWeight"],
|
|
475
|
+
textTransform: textTransformVal as React.CSSProperties["textTransform"],
|
|
476
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
477
|
+
...(isHexColor ? { color: "inherit" } : {}),
|
|
478
|
+
};
|
|
384
479
|
|
|
385
480
|
return (
|
|
386
481
|
<>
|
|
@@ -388,6 +483,7 @@ export default function Navbar({
|
|
|
388
483
|
<nav
|
|
389
484
|
role="navigation"
|
|
390
485
|
aria-label="Main navigation"
|
|
486
|
+
data-nav-scope={navScopeId}
|
|
391
487
|
className={`${positionClass} top-0 left-0 right-0 z-50 transition-transform duration-300 ease-in-out ${
|
|
392
488
|
shouldHide ? "-translate-y-full" : "translate-y-0"
|
|
393
489
|
}`}
|
|
@@ -397,14 +493,21 @@ export default function Navbar({
|
|
|
397
493
|
style={{
|
|
398
494
|
...navBgStyle,
|
|
399
495
|
...textColorStyle,
|
|
400
|
-
...(
|
|
496
|
+
...(hasResponsive
|
|
401
497
|
? {
|
|
402
|
-
left:
|
|
403
|
-
right:
|
|
404
|
-
top:
|
|
405
|
-
borderRadius: "8px",
|
|
498
|
+
left: "var(--nav-margin-h)",
|
|
499
|
+
right: "var(--nav-margin-h)",
|
|
500
|
+
top: "var(--nav-margin-v)",
|
|
501
|
+
...(marginH > 0 || marginV > 0 ? { borderRadius: "8px" } : {}),
|
|
406
502
|
}
|
|
407
|
-
:
|
|
503
|
+
: marginH > 0 || marginV > 0
|
|
504
|
+
? {
|
|
505
|
+
left: `${marginH}px`,
|
|
506
|
+
right: `${marginH}px`,
|
|
507
|
+
top: `${marginV}px`,
|
|
508
|
+
borderRadius: "8px",
|
|
509
|
+
}
|
|
510
|
+
: {}),
|
|
408
511
|
// CSS custom properties for animation duration
|
|
409
512
|
...(entrancePreset && !entranceStagger ? {
|
|
410
513
|
"--nav-entrance-duration": `${entranceDuration}ms`,
|
|
@@ -412,21 +515,39 @@ export default function Navbar({
|
|
|
412
515
|
} as React.CSSProperties : {}),
|
|
413
516
|
}}
|
|
414
517
|
>
|
|
518
|
+
{/* Responsive nav CSS custom properties + media queries */}
|
|
519
|
+
{responsiveCSS && (
|
|
520
|
+
<style dangerouslySetInnerHTML={{ __html: responsiveCSS }} />
|
|
521
|
+
)}
|
|
415
522
|
{/* Desktop: 12-column grid constrained to --grid-width */}
|
|
416
523
|
<div
|
|
417
524
|
className="hidden lg:grid"
|
|
418
|
-
style={
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
525
|
+
style={hasResponsive
|
|
526
|
+
? {
|
|
527
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
528
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
529
|
+
marginLeft: "auto",
|
|
530
|
+
marginRight: "auto",
|
|
531
|
+
paddingLeft: "var(--nav-padding-h)",
|
|
532
|
+
paddingRight: "var(--nav-padding-h)",
|
|
533
|
+
paddingTop: "var(--nav-padding-v)",
|
|
534
|
+
paddingBottom: "var(--nav-padding-v)",
|
|
535
|
+
alignItems: "var(--nav-vertical-align)" as React.CSSProperties["alignItems"],
|
|
536
|
+
columnGap: "var(--nav-items-gap)",
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
gridTemplateColumns: "repeat(12, 1fr)",
|
|
540
|
+
maxWidth: "var(--grid-width, 1445px)",
|
|
541
|
+
marginLeft: "auto",
|
|
542
|
+
marginRight: "auto",
|
|
543
|
+
paddingLeft: `${paddingH}px`,
|
|
544
|
+
paddingRight: `${paddingH}px`,
|
|
545
|
+
paddingTop: `${paddingV}px`,
|
|
546
|
+
paddingBottom: `${paddingV}px`,
|
|
547
|
+
alignItems: gridAlignItems,
|
|
548
|
+
columnGap: `${itemsGap}px`,
|
|
549
|
+
}
|
|
550
|
+
}
|
|
430
551
|
>
|
|
431
552
|
{/* Logo */}
|
|
432
553
|
<div
|
|
@@ -480,7 +601,7 @@ export default function Navbar({
|
|
|
480
601
|
gridColumn: `${item.grid_column} / span ${item.column_span || 1}`,
|
|
481
602
|
gridRow: 1,
|
|
482
603
|
display: "flex",
|
|
483
|
-
justifyContent: itemsJustify,
|
|
604
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
484
605
|
alignItems: "center",
|
|
485
606
|
minWidth: 0,
|
|
486
607
|
overflow: "hidden",
|
|
@@ -518,9 +639,9 @@ export default function Navbar({
|
|
|
518
639
|
style={{
|
|
519
640
|
gridColumn: `${logoCols + 1} / -1`,
|
|
520
641
|
display: "flex",
|
|
521
|
-
justifyContent: itemsJustify,
|
|
642
|
+
justifyContent: hasResponsive ? "var(--nav-items-justify)" as React.CSSProperties["justifyContent"] : itemsJustify,
|
|
522
643
|
alignItems: "center",
|
|
523
|
-
gap: `${design?.items_gap ?? 32}px`,
|
|
644
|
+
gap: hasResponsive ? "var(--nav-items-gap)" : `${design?.items_gap ?? 32}px`,
|
|
524
645
|
}}
|
|
525
646
|
>
|
|
526
647
|
{unplacedItems.map((item) => (
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V1 → V2 migration utilities for content blocks.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from serializer.ts in Session 162.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Project Grid v1 → v2 Migration (Session 105)
|
|
9
|
+
// ============================================
|
|
10
|
+
|
|
11
|
+
/** Map v1 string gap values to pixel numbers */
|
|
12
|
+
export const GAP_V1_MAP: Record<string, number> = {
|
|
13
|
+
small: 8,
|
|
14
|
+
medium: 16,
|
|
15
|
+
large: 32,
|
|
16
|
+
xlarge: 48,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect and migrate a v1 projectGridBlock to v2 format.
|
|
21
|
+
* v1 is identified by the presence of `grid_layout` (string field).
|
|
22
|
+
* v2 is identified by the presence of `columns` (number field).
|
|
23
|
+
* If already v2 or not a projectGridBlock, returns as-is.
|
|
24
|
+
*/
|
|
25
|
+
export function migrateProjectGridV1ToV2(block: Record<string, unknown>): void {
|
|
26
|
+
if (block._type !== "projectGridBlock") return;
|
|
27
|
+
|
|
28
|
+
// Already v2: has `columns` as a number
|
|
29
|
+
if (typeof block.columns === "number") return;
|
|
30
|
+
|
|
31
|
+
// Not v1 either (fresh block without grid_layout) — apply defaults
|
|
32
|
+
if (!block.grid_layout && typeof block.columns !== "number") {
|
|
33
|
+
block.columns = 3;
|
|
34
|
+
block.aspect_ratios = ["16/9"];
|
|
35
|
+
block.gap_v = 16;
|
|
36
|
+
block.gap_h = 16;
|
|
37
|
+
block.hover_effect = block.hover_effect || "scale";
|
|
38
|
+
block.show_subtitle = block.show_subtitle ?? true;
|
|
39
|
+
block.border_radius = 0;
|
|
40
|
+
block.video_mode = "off";
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── v1 → v2 migration ───
|
|
45
|
+
|
|
46
|
+
// columns: columns_desktop → columns (clamp 1–6)
|
|
47
|
+
const colDesktop = typeof block.columns_desktop === "number" ? block.columns_desktop : 2;
|
|
48
|
+
block.columns = Math.max(1, Math.min(6, colDesktop));
|
|
49
|
+
|
|
50
|
+
// gap: string → gap_v + gap_h (both same value)
|
|
51
|
+
const gapStr = typeof block.gap === "string" ? block.gap : "large";
|
|
52
|
+
const gapPx = GAP_V1_MAP[gapStr] ?? 32;
|
|
53
|
+
block.gap_v = gapPx;
|
|
54
|
+
block.gap_h = gapPx;
|
|
55
|
+
|
|
56
|
+
// card_aspect_ratio → aspect_ratios
|
|
57
|
+
const oldRatio = typeof block.card_aspect_ratio === "string" ? block.card_aspect_ratio : "16/9";
|
|
58
|
+
if (oldRatio === "random") {
|
|
59
|
+
block.aspect_ratios = ["16/9", "1/1", "9/16"];
|
|
60
|
+
} else {
|
|
61
|
+
block.aspect_ratios = [oldRatio];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// hover_effect: "overlay" → "scale", rest 1:1
|
|
65
|
+
const oldHover = typeof block.hover_effect === "string" ? block.hover_effect : "overlay";
|
|
66
|
+
if (oldHover === "overlay") {
|
|
67
|
+
block.hover_effect = "scale";
|
|
68
|
+
}
|
|
69
|
+
// "scale" and "none" pass through
|
|
70
|
+
|
|
71
|
+
// border_radius: string → number
|
|
72
|
+
const oldRadius = block.card_border_radius;
|
|
73
|
+
block.border_radius = typeof oldRadius === "string" ? (parseInt(oldRadius, 10) || 0) : 0;
|
|
74
|
+
|
|
75
|
+
// video: video_autoloop + video_hover → video_mode
|
|
76
|
+
if (block.video_autoloop === true) {
|
|
77
|
+
block.video_mode = "autoloop";
|
|
78
|
+
} else if (block.video_hover === true) {
|
|
79
|
+
block.video_mode = "hover";
|
|
80
|
+
} else {
|
|
81
|
+
block.video_mode = "off";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// show_subtitle: direct copy (default true)
|
|
85
|
+
block.show_subtitle = block.show_subtitle ?? true;
|
|
86
|
+
|
|
87
|
+
// ─── Clean up v1 fields (not needed in builder state) ───
|
|
88
|
+
delete block.grid_layout;
|
|
89
|
+
delete block.gap;
|
|
90
|
+
delete block.auto_columns;
|
|
91
|
+
delete block.card_size;
|
|
92
|
+
delete block.columns_desktop;
|
|
93
|
+
delete block.card_aspect_ratio;
|
|
94
|
+
delete block.card_border_radius;
|
|
95
|
+
delete block.video_hover;
|
|
96
|
+
delete block.video_autoloop;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Normalize animation fields on a block during read.
|
|
101
|
+
* Post-migration (Session 122): only new fields exist in Sanity.
|
|
102
|
+
* This is a no-op now but kept as a hook point for future needs.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeBlockAnimationFields(_block: Record<string, unknown>): void {
|
|
105
|
+
// No-op: migration completed in Session 122.
|
|
106
|
+
// All documents now use enter_animation / hover_effect directly.
|
|
107
|
+
}
|