@morphika/andami 0.1.3 → 0.1.6
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/app/(site)/[slug]/page.tsx +7 -4
- package/app/(site)/layout.tsx +5 -2
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +7 -4
- package/app/admin/layout.tsx +3 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/health/route.ts +1 -1
- package/app/api/admin/assets/register/route.ts +1 -1
- package/app/api/admin/assets/registry/route.ts +1 -1
- package/app/api/admin/assets/relink/confirm/route.ts +2 -2
- package/app/api/admin/assets/relink/route.ts +1 -1
- package/app/api/admin/assets/scan/route.ts +1 -1
- package/app/api/admin/custom-sections/[slug]/route.ts +1 -1
- package/app/api/admin/custom-sections/route.ts +1 -1
- package/app/api/admin/database/route.ts +1 -1
- package/app/api/admin/pages/[slug]/duplicate/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +2 -2
- package/app/api/admin/pages/[slug]/set-home/route.ts +1 -1
- package/app/api/admin/pages/route.ts +1 -1
- package/app/api/admin/preview/route.ts +1 -1
- package/app/api/admin/r2/delete/route.ts +1 -1
- package/app/api/admin/r2/rename/route.ts +1 -1
- package/app/api/admin/r2/status/route.ts +1 -1
- package/app/api/admin/r2/upload-url/route.ts +1 -1
- package/app/api/admin/settings/route.ts +41 -16
- package/app/api/admin/setup/complete/route.ts +2 -2
- package/app/api/admin/setup/route.ts +7 -4
- package/app/api/admin/storage/switch/route.ts +1 -1
- package/app/api/admin/styles/route.ts +1 -1
- package/components/admin/index.ts +7 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
- package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
- package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
- package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
- package/components/admin/nav-builder/index.ts +2 -0
- package/components/blocks/BlockRenderer.tsx +65 -13
- package/components/blocks/ButtonBlockRenderer.tsx +29 -6
- package/components/blocks/CoverBlockRenderer.tsx +36 -14
- package/components/blocks/ImageBlockRenderer.tsx +5 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
- package/components/blocks/PageRenderer.tsx +4 -2
- package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
- package/components/blocks/SectionRenderer.tsx +9 -8
- package/components/blocks/SectionV2Renderer.tsx +8 -8
- package/components/blocks/SpacerBlockRenderer.tsx +4 -2
- package/components/blocks/TextBlockRenderer.tsx +9 -4
- package/components/builder/BuilderCanvas.tsx +10 -4
- package/components/builder/ColorPicker.tsx +51 -243
- package/components/builder/ColorSwatchPicker.tsx +214 -274
- package/components/builder/DndWrapper.tsx +5 -2
- package/components/builder/SectionV2Canvas.tsx +15 -4
- package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
- package/components/builder/color-picker/AlphaSlider.tsx +141 -0
- package/components/builder/color-picker/AngleControl.tsx +138 -0
- package/components/builder/color-picker/ColorInputs.tsx +105 -0
- package/components/builder/color-picker/EyedropperButton.tsx +74 -0
- package/components/builder/color-picker/GradientBar.tsx +222 -0
- package/components/builder/color-picker/GradientPreview.tsx +53 -0
- package/components/builder/color-picker/HueSlider.tsx +124 -0
- package/components/builder/color-picker/MeshCanvas.tsx +172 -0
- package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
- package/components/builder/color-picker/MeshPointList.tsx +200 -0
- package/components/builder/color-picker/PositionControl.tsx +158 -0
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
- package/components/builder/color-picker/StopEditor.tsx +178 -0
- package/components/builder/color-picker/SwatchBar.tsx +93 -0
- package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
- package/components/builder/color-picker/index.ts +62 -0
- package/components/builder/color-picker/types.ts +115 -0
- package/components/builder/color-picker/utils.ts +138 -0
- package/components/builder/editors/CoverBlockEditor.tsx +86 -32
- package/components/builder/editors/ProjectGridEditor.tsx +51 -4
- package/components/builder/hooks/useColumnDrag.ts +25 -27
- package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
- package/components/builder/settings-panel/LayoutTab.tsx +382 -310
- package/components/builder/settings-panel/PageSettings.tsx +6 -4
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
- package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
- package/components/ui/Navbar.tsx +97 -25
- package/components/ui/PortfolioTracker.tsx +3 -3
- package/lib/assets.ts +1 -1
- package/lib/auth.ts +1 -1
- package/lib/builder/gradient-presets.ts +128 -0
- package/lib/builder/layout-styles.ts +16 -10
- package/lib/builder/serializer.ts +1 -0
- package/lib/builder/store-blocks.ts +48 -61
- package/lib/builder/store-helpers.ts +31 -14
- package/lib/builder/store.ts +59 -41
- package/lib/builder/types.ts +14 -0
- package/lib/color-utils.ts +200 -0
- package/lib/revalidate.ts +2 -2
- package/lib/sanity/client.ts +16 -0
- package/lib/sanity/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- package/lib/storage/index.ts +22 -4
- package/lib/version.ts +6 -0
- package/package.json +8 -2
- package/sanity/schemas/siteSettings.ts +34 -0
- package/styles/base.css +3 -3
- package/app/globals.css +0 -7
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* SectionV2Settings — Settings panel for V2 grid sections.
|
|
5
5
|
*
|
|
6
6
|
* Three tabs managed by parent SettingsPanel:
|
|
7
|
-
* - Settings: presets grid, col_gap, row_gap
|
|
8
|
-
* - Layout: spacing TRBL, offset TRBL, border
|
|
7
|
+
* - Settings: presets grid, col_gap, row_gap
|
|
8
|
+
* - Layout: spacing TRBL, background (color/opacity/image), offset TRBL, border
|
|
9
9
|
* - Animation: scroll animation picker, hover animation picker, stagger settings
|
|
10
10
|
*
|
|
11
11
|
* Session 83: Phase 4 of V2 Grid System.
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
SettingsField,
|
|
18
18
|
SettingsSection,
|
|
19
19
|
} from "../editors/shared";
|
|
20
|
-
import
|
|
20
|
+
import { findGaps } from "../../../lib/builder/cascade";
|
|
21
21
|
import {
|
|
22
22
|
getSectionV2SettingValue,
|
|
23
23
|
hasSectionV2SettingOverride,
|
|
@@ -50,18 +50,62 @@ const PRESETS: PresetOption[] = [
|
|
|
50
50
|
|
|
51
51
|
const CUSTOM_PRESET: PresetOption = { id: "custom", label: "Custom", cols: [], readonly: true };
|
|
52
52
|
|
|
53
|
+
// ============================================
|
|
54
|
+
// Section title icons (small, inline)
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
function LayoutPresetIcon() {
|
|
58
|
+
return (
|
|
59
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
60
|
+
<rect x="1.5" y="1.5" width="11" height="11" rx="1.5" stroke="currentColor" strokeWidth="0.8" fill="none" opacity="0.4" />
|
|
61
|
+
<line x1="6" y1="1.5" x2="6" y2="12.5" stroke="currentColor" strokeWidth="0.8" opacity="0.6" />
|
|
62
|
+
<rect x="1.5" y="1.5" width="4.5" height="11" rx="0" fill="currentColor" opacity="0.12" />
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function GridGapIcon() {
|
|
68
|
+
return (
|
|
69
|
+
<svg width={14} height={14} viewBox="0 0 14 14" fill="none">
|
|
70
|
+
<rect x="1" y="1" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
71
|
+
<rect x="8" y="1" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
72
|
+
<rect x="1" y="8" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
73
|
+
<rect x="8" y="8" width="5" height="5" rx="1" fill="currentColor" opacity="0.25" />
|
|
74
|
+
<line x1="7" y1="1" x2="7" y2="13" stroke="currentColor" strokeWidth="0.8" strokeDasharray="1.5 1" opacity="0.5" />
|
|
75
|
+
<line x1="1" y1="7" x2="13" y2="7" stroke="currentColor" strokeWidth="0.8" strokeDasharray="1.5 1" opacity="0.5" />
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
// ============================================
|
|
54
81
|
// Preset Grid Component
|
|
55
82
|
// ============================================
|
|
56
83
|
|
|
57
84
|
function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
58
85
|
const applyPresetV2 = useBuilderStore((s) => s.applyPresetV2);
|
|
86
|
+
const addColumnV2 = useBuilderStore((s) => s.addColumnV2);
|
|
59
87
|
const currentPreset = section.settings.preset;
|
|
60
88
|
|
|
61
89
|
const allPresets = currentPreset === "custom"
|
|
62
90
|
? [...PRESETS, CUSTOM_PRESET]
|
|
63
91
|
: PRESETS;
|
|
64
92
|
|
|
93
|
+
const handleAddColumn = () => {
|
|
94
|
+
const gridCols = section.settings.grid_columns || 12;
|
|
95
|
+
const cascadeCols = section.columns.map((c) => ({
|
|
96
|
+
_key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
|
|
97
|
+
}));
|
|
98
|
+
const gapList = findGaps(cascadeCols, gridCols);
|
|
99
|
+
if (gapList.length > 0) {
|
|
100
|
+
// Fill first available gap
|
|
101
|
+
addColumnV2(section._key, gapList[0].grid_row, gapList[0].grid_column, gapList[0].span);
|
|
102
|
+
} else {
|
|
103
|
+
// No space — add a new full-width row below
|
|
104
|
+
const maxRow = cascadeCols.reduce((max, c) => Math.max(max, c.grid_row), 1);
|
|
105
|
+
addColumnV2(section._key, maxRow + 1, 1, gridCols);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
65
109
|
return (
|
|
66
110
|
<div className="grid grid-cols-3 gap-1.5">
|
|
67
111
|
{allPresets.map((preset) => {
|
|
@@ -109,6 +153,22 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
109
153
|
</button>
|
|
110
154
|
);
|
|
111
155
|
})}
|
|
156
|
+
|
|
157
|
+
{/* + Add Column button */}
|
|
158
|
+
<button
|
|
159
|
+
onClick={handleAddColumn}
|
|
160
|
+
className="flex flex-col items-center gap-1 p-2 rounded-lg border border-dashed border-neutral-300 transition-all hover:border-[#076bff] hover:bg-[#076bff]/5 group"
|
|
161
|
+
title="Add a column (fills first gap, or adds new row below)"
|
|
162
|
+
>
|
|
163
|
+
<div className="flex items-center justify-center w-full h-4">
|
|
164
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="text-neutral-400 group-hover:text-[#076bff] transition-colors">
|
|
165
|
+
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
166
|
+
</svg>
|
|
167
|
+
</div>
|
|
168
|
+
<span className="text-[9px] font-medium text-neutral-400 group-hover:text-[#076bff] transition-colors">
|
|
169
|
+
Add Col
|
|
170
|
+
</span>
|
|
171
|
+
</button>
|
|
112
172
|
</div>
|
|
113
173
|
);
|
|
114
174
|
}
|
|
@@ -119,7 +179,6 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
119
179
|
|
|
120
180
|
export default function SectionV2Settings({ section }: { section: PageSectionV2 }) {
|
|
121
181
|
const store = useBuilderStore();
|
|
122
|
-
const paletteSwatches = usePaletteSwatches();
|
|
123
182
|
const settings = section.settings;
|
|
124
183
|
const activeViewport = store.activeViewport;
|
|
125
184
|
const isResponsive = activeViewport !== "desktop";
|
|
@@ -207,7 +266,7 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
207
266
|
|
|
208
267
|
{/* Presets — desktop only (changing structure from responsive would be confusing) */}
|
|
209
268
|
{!isResponsive && (
|
|
210
|
-
<SettingsSection title="Layout Preset" defaultOpen>
|
|
269
|
+
<SettingsSection title="Layout Preset" defaultOpen icon={<LayoutPresetIcon />}>
|
|
211
270
|
<PresetGrid section={section} />
|
|
212
271
|
<p className="text-[10px] text-neutral-400 mt-1.5">
|
|
213
272
|
{section.columns.length} column{section.columns.length !== 1 ? "s" : ""}
|
|
@@ -219,7 +278,7 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
219
278
|
)}
|
|
220
279
|
|
|
221
280
|
{/* Gaps */}
|
|
222
|
-
<SettingsSection title="Grid Gaps" defaultOpen>
|
|
281
|
+
<SettingsSection title="Grid Gaps" defaultOpen icon={<GridGapIcon />}>
|
|
223
282
|
<SettingsField label={
|
|
224
283
|
<span>
|
|
225
284
|
Col Gap
|
|
@@ -285,35 +344,6 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
285
344
|
</SettingsField>
|
|
286
345
|
</SettingsSection>
|
|
287
346
|
|
|
288
|
-
{/* Appearance */}
|
|
289
|
-
<SettingsSection title="Appearance">
|
|
290
|
-
<SettingsField label="Background">
|
|
291
|
-
<ColorSwatchPicker
|
|
292
|
-
value={getSectionV2SettingValue(section, activeViewport, "background_color", "")}
|
|
293
|
-
onChange={(hex) => updateSettingResponsive("background_color", hex)}
|
|
294
|
-
swatches={paletteSwatches}
|
|
295
|
-
/>
|
|
296
|
-
</SettingsField>
|
|
297
|
-
|
|
298
|
-
{getSectionV2SettingValue(section, activeViewport, "background_color", "") && (
|
|
299
|
-
<SettingsField label="Opacity">
|
|
300
|
-
<div className="flex items-center gap-2">
|
|
301
|
-
<input
|
|
302
|
-
type="range"
|
|
303
|
-
min={0}
|
|
304
|
-
max={100}
|
|
305
|
-
value={getSectionV2SettingValue(section, activeViewport, "background_opacity", 100)}
|
|
306
|
-
onChange={(e) => updateSettingResponsive("background_opacity", parseInt(e.target.value))}
|
|
307
|
-
className="flex-1 accent-[#076bff]"
|
|
308
|
-
/>
|
|
309
|
-
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
310
|
-
{getSectionV2SettingValue(section, activeViewport, "background_opacity", 100)}%
|
|
311
|
-
</span>
|
|
312
|
-
</div>
|
|
313
|
-
</SettingsField>
|
|
314
|
-
)}
|
|
315
|
-
</SettingsSection>
|
|
316
|
-
|
|
317
347
|
</>
|
|
318
348
|
);
|
|
319
349
|
}
|
package/components/ui/Navbar.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import type { NavItem, NavDesign, NavEntrancePreset } from "../../lib/sanity/types";
|
|
6
|
+
import type { NavItem, NavDesign, NavEntrancePreset, 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";
|
|
@@ -115,6 +115,8 @@ function NavLink({
|
|
|
115
115
|
interface NavbarProps {
|
|
116
116
|
navItems?: NavItem[];
|
|
117
117
|
design?: NavDesign;
|
|
118
|
+
/** Mobile menu design — independent styles for hamburger menu */
|
|
119
|
+
mobileDesign?: MobileNavDesign;
|
|
118
120
|
/** @deprecated Use design.color instead */
|
|
119
121
|
colorVariant?: string;
|
|
120
122
|
}
|
|
@@ -126,6 +128,7 @@ interface NavbarProps {
|
|
|
126
128
|
export default function Navbar({
|
|
127
129
|
navItems = [],
|
|
128
130
|
design,
|
|
131
|
+
mobileDesign,
|
|
129
132
|
colorVariant = "yellow-lime",
|
|
130
133
|
}: NavbarProps) {
|
|
131
134
|
// Resolve color: context > design > prop
|
|
@@ -178,6 +181,45 @@ export default function Navbar({
|
|
|
178
181
|
// Map vertical_align to CSS align-items value for the grid container
|
|
179
182
|
const gridAlignItems = verticalAlign === "bottom" ? "end" : verticalAlign === "middle" ? "center" : "start";
|
|
180
183
|
|
|
184
|
+
// ── Mobile menu resolved values (Session 158) ──
|
|
185
|
+
// Mobile styles are independent from page-level NavColorContext overrides.
|
|
186
|
+
// They fall back to desktop design values when not explicitly set.
|
|
187
|
+
const mobilePaddingH = mobileDesign?.padding_h ?? paddingH;
|
|
188
|
+
const mobilePaddingV = mobileDesign?.padding_v ?? paddingV;
|
|
189
|
+
const mobileOverlayBg = mobileDesign?.overlay_bg || "";
|
|
190
|
+
const mobileFontSize = mobileDesign?.font_size ?? 24;
|
|
191
|
+
const mobileItemsGap = mobileDesign?.items_gap ?? 32;
|
|
192
|
+
const mobileItemsAlign = mobileDesign?.items_align || "center";
|
|
193
|
+
const mobileTextTransform = mobileDesign?.text_transform || textTransformVal;
|
|
194
|
+
// Mobile text color: explicit mobile setting > desktop design.color (NOT context)
|
|
195
|
+
const mobileTextColor = mobileDesign?.text_color || "";
|
|
196
|
+
// Hamburger/logo color: explicit mobile setting > desktop design.color (NOT context)
|
|
197
|
+
const mobileHamburgerColor = mobileDesign?.hamburger_color || "";
|
|
198
|
+
// Mobile navbar bar background
|
|
199
|
+
const mobileNavbarBg = mobileDesign?.navbar_bg || "";
|
|
200
|
+
const mobileNavbarBgOpacity = mobileDesign?.navbar_bg_opacity ?? 0;
|
|
201
|
+
|
|
202
|
+
// Resolve mobile bar background style
|
|
203
|
+
const mobileNavBgStyle: React.CSSProperties = {};
|
|
204
|
+
if (mobileNavbarBg && mobileNavbarBgOpacity > 0) {
|
|
205
|
+
const hex = mobileNavbarBg.replace("#", "");
|
|
206
|
+
if (hex.length >= 6) {
|
|
207
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
208
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
209
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
210
|
+
mobileNavBgStyle.backgroundColor = `rgba(${r}, ${g}, ${b}, ${mobileNavbarBgOpacity / 100})`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Resolve mobile hamburger/logo color — does NOT use contextColor (page overrides).
|
|
215
|
+
// Falls back to desktop design.color (the non-context-overridden value).
|
|
216
|
+
const desktopDesignColor = design?.color || colorVariant;
|
|
217
|
+
const desktopDesignIsHex = desktopDesignColor.startsWith("#");
|
|
218
|
+
const mobileBarColor = mobileHamburgerColor || (desktopDesignIsHex ? desktopDesignColor : "");
|
|
219
|
+
const mobileBarIsHex = mobileBarColor.startsWith("#");
|
|
220
|
+
const mobileBarColorClass = mobileBarIsHex ? "" : (colorMap[desktopDesignColor] || colorMap["yellow-lime"]);
|
|
221
|
+
const mobileBarColorStyle: React.CSSProperties | undefined = mobileBarIsHex ? { color: mobileBarColor } : undefined;
|
|
222
|
+
|
|
181
223
|
const [isVisible, setIsVisible] = useState(true);
|
|
182
224
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
183
225
|
const [lightboxItem, setLightboxItem] = useState<NavItem | null>(null);
|
|
@@ -312,7 +354,8 @@ export default function Navbar({
|
|
|
312
354
|
const shouldHide = position !== "static" && hideOnScroll && !isVisible;
|
|
313
355
|
|
|
314
356
|
// Grid layout: detect new format (items with type field) vs legacy (logo in design)
|
|
315
|
-
const
|
|
357
|
+
const safeNavItems = navItems ?? [];
|
|
358
|
+
const visibleItems = safeNavItems.filter((item) => item.visible !== false);
|
|
316
359
|
const hasNewFormat = visibleItems.some((i) => i.type === "logo" || i.type === "menu-item");
|
|
317
360
|
|
|
318
361
|
// New format: logo is a nav item; legacy: logo from design
|
|
@@ -335,7 +378,7 @@ export default function Navbar({
|
|
|
335
378
|
fontSize: `${fontSize}px`,
|
|
336
379
|
fontWeight: fontWeight as React.CSSProperties["fontWeight"],
|
|
337
380
|
textTransform: textTransformVal as React.CSSProperties["textTransform"],
|
|
338
|
-
fontFamily: fontFamily || "var(--font-
|
|
381
|
+
fontFamily: fontFamily || "var(--font-sans, Inter, system-ui, sans-serif)",
|
|
339
382
|
...(isHexColor ? { color: "inherit" } : {}),
|
|
340
383
|
};
|
|
341
384
|
|
|
@@ -493,23 +536,30 @@ export default function Navbar({
|
|
|
493
536
|
)}
|
|
494
537
|
</div>
|
|
495
538
|
|
|
496
|
-
{/* Mobile: simple flex layout with hamburger */}
|
|
539
|
+
{/* Mobile: simple flex layout with hamburger — uses independent mobile styles */}
|
|
540
|
+
{/* BG color and icon color only apply when menu is open (overlay visible). */}
|
|
541
|
+
{/* When closed, the bar inherits desktop navbar colors for visual continuity. */}
|
|
497
542
|
<div
|
|
498
|
-
className=
|
|
543
|
+
className={`flex lg:hidden items-center justify-between ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300`}
|
|
499
544
|
style={{
|
|
500
545
|
maxWidth: "var(--grid-width, 1445px)",
|
|
501
546
|
marginLeft: "auto",
|
|
502
547
|
marginRight: "auto",
|
|
503
|
-
paddingLeft: `${
|
|
504
|
-
paddingRight: `${
|
|
505
|
-
paddingTop: `${
|
|
506
|
-
paddingBottom: `${
|
|
548
|
+
paddingLeft: `${mobilePaddingH}px`,
|
|
549
|
+
paddingRight: `${mobilePaddingH}px`,
|
|
550
|
+
paddingTop: `${mobilePaddingV}px`,
|
|
551
|
+
paddingBottom: `${mobilePaddingV}px`,
|
|
552
|
+
...(isMenuOpen ? mobileNavBgStyle : {}),
|
|
553
|
+
...(isMenuOpen ? mobileBarColorStyle : {}),
|
|
507
554
|
}}
|
|
508
555
|
>
|
|
509
556
|
<Link
|
|
510
557
|
href="/"
|
|
511
|
-
className={
|
|
512
|
-
style={
|
|
558
|
+
className={`tracking-normal ${isMenuOpen && mobileBarColorClass ? mobileBarColorClass : ""} transition-colors duration-300 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
559
|
+
style={{
|
|
560
|
+
...linkStyle,
|
|
561
|
+
...(isMenuOpen && mobileBarIsHex ? { color: "inherit" } : {}),
|
|
562
|
+
}}
|
|
513
563
|
>
|
|
514
564
|
{logoLabel}
|
|
515
565
|
</Link>
|
|
@@ -517,10 +567,11 @@ export default function Navbar({
|
|
|
517
567
|
<button
|
|
518
568
|
ref={hamburgerButtonRef}
|
|
519
569
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
520
|
-
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${
|
|
570
|
+
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${isMenuOpen ? mobileBarColorClass : ""} transition-colors duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
521
571
|
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
|
522
572
|
aria-expanded={isMenuOpen}
|
|
523
573
|
aria-controls="mobile-nav-menu"
|
|
574
|
+
style={isMenuOpen && mobileBarIsHex ? { color: mobileBarColor } : undefined}
|
|
524
575
|
>
|
|
525
576
|
<span
|
|
526
577
|
className={`block w-5 h-[1.5px] bg-current transition-all duration-300 ${
|
|
@@ -541,30 +592,51 @@ export default function Navbar({
|
|
|
541
592
|
</div>
|
|
542
593
|
</nav>
|
|
543
594
|
|
|
544
|
-
{/* Mobile overlay menu */}
|
|
595
|
+
{/* Mobile overlay menu — uses independent mobile styles (Session 158) */}
|
|
545
596
|
<div
|
|
546
597
|
ref={mobileMenuRef}
|
|
547
598
|
id="mobile-nav-menu"
|
|
548
599
|
role="dialog"
|
|
549
600
|
aria-modal={isMenuOpen}
|
|
550
601
|
aria-label="Mobile navigation menu"
|
|
551
|
-
className={`fixed inset-0 z-40
|
|
602
|
+
className={`fixed inset-0 z-40 transition-opacity duration-300 lg:hidden ${
|
|
552
603
|
isMenuOpen
|
|
553
604
|
? "opacity-100 pointer-events-auto"
|
|
554
605
|
: "opacity-0 pointer-events-none"
|
|
555
606
|
}`}
|
|
607
|
+
style={{
|
|
608
|
+
backgroundColor: mobileOverlayBg || "var(--color-brand-dark, #0a0a0a)",
|
|
609
|
+
}}
|
|
556
610
|
>
|
|
557
|
-
<div
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
611
|
+
<div
|
|
612
|
+
className="flex flex-col justify-center h-full"
|
|
613
|
+
style={{
|
|
614
|
+
alignItems: mobileItemsAlign === "right" ? "flex-end" : mobileItemsAlign === "left" ? "flex-start" : "center",
|
|
615
|
+
gap: `${mobileItemsGap}px`,
|
|
616
|
+
paddingLeft: `${mobilePaddingH}px`,
|
|
617
|
+
paddingRight: `${mobilePaddingH}px`,
|
|
618
|
+
}}
|
|
619
|
+
>
|
|
620
|
+
{[...menuItems].sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0)).map((item) => {
|
|
621
|
+
// Mobile text color: explicit mobile setting > desktop design.color (NOT context)
|
|
622
|
+
const mobileItemIsHex = mobileTextColor.startsWith("#");
|
|
623
|
+
const mobileItemColorClass = mobileItemIsHex ? "" : (colorMap[design?.color || colorVariant] || colorMap["yellow-lime"]);
|
|
624
|
+
return (
|
|
625
|
+
<NavLink
|
|
626
|
+
key={item._key}
|
|
627
|
+
item={item}
|
|
628
|
+
className={`font-sans tracking-wide ${mobileItemColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
629
|
+
style={{
|
|
630
|
+
fontSize: `${mobileFontSize}px`,
|
|
631
|
+
textTransform: mobileTextTransform as React.CSSProperties["textTransform"],
|
|
632
|
+
...(fontFamily ? { fontFamily } : {}),
|
|
633
|
+
...(mobileItemIsHex ? { color: mobileTextColor } : {}),
|
|
634
|
+
}}
|
|
635
|
+
currentPath={pathname}
|
|
636
|
+
onContentClick={(clickedItem) => { setLightboxItem(clickedItem); setIsMenuOpen(false); }}
|
|
637
|
+
/>
|
|
638
|
+
);
|
|
639
|
+
})}
|
|
568
640
|
</div>
|
|
569
641
|
</div>
|
|
570
642
|
|
|
@@ -19,9 +19,9 @@ import { usePathname } from "next/navigation";
|
|
|
19
19
|
import { getSiteConfig } from "../../lib/config";
|
|
20
20
|
|
|
21
21
|
const cfg = getSiteConfig();
|
|
22
|
-
const KNOCK_API_URL = cfg.tracking
|
|
23
|
-
const REF_KEY = `${cfg.tracking
|
|
24
|
-
const CHANNEL_KEY = `${cfg.tracking
|
|
22
|
+
const KNOCK_API_URL = cfg.tracking?.knockApiUrl ?? "";
|
|
23
|
+
const REF_KEY = `${cfg.tracking?.sessionPrefix ?? "site"}_ref`;
|
|
24
|
+
const CHANNEL_KEY = `${cfg.tracking?.sessionPrefix ?? "site"}_channel`;
|
|
25
25
|
|
|
26
26
|
function sendVisit(
|
|
27
27
|
page: string,
|
package/lib/assets.ts
CHANGED
package/lib/auth.ts
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradient Presets
|
|
3
|
+
*
|
|
4
|
+
* Predefined gradient templates for quick selection in the color picker.
|
|
5
|
+
* Pattern follows hover-effect-presets.ts.
|
|
6
|
+
*
|
|
7
|
+
* Phase 4 — Color Picker v2.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { GradientValue, LinearGradient, RadialGradient, MeshGradient } from "../sanity/types";
|
|
11
|
+
|
|
12
|
+
export type GradientPresetCategory = "linear" | "radial" | "mesh";
|
|
13
|
+
|
|
14
|
+
export interface GradientPresetInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
category: GradientPresetCategory;
|
|
18
|
+
template: GradientValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const GRADIENT_PRESETS: GradientPresetInfo[] = [
|
|
22
|
+
// ── Linear presets ──
|
|
23
|
+
{
|
|
24
|
+
id: "sunset",
|
|
25
|
+
label: "Sunset",
|
|
26
|
+
category: "linear",
|
|
27
|
+
template: {
|
|
28
|
+
type: "linear",
|
|
29
|
+
stops: [
|
|
30
|
+
{ color: "#ff6b35", alpha: 1, position: 0 },
|
|
31
|
+
{ color: "#d63384", alpha: 1, position: 100 },
|
|
32
|
+
],
|
|
33
|
+
angle: 135,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "ocean",
|
|
38
|
+
label: "Ocean",
|
|
39
|
+
category: "linear",
|
|
40
|
+
template: {
|
|
41
|
+
type: "linear",
|
|
42
|
+
stops: [
|
|
43
|
+
{ color: "#0c2d48", alpha: 1, position: 0 },
|
|
44
|
+
{ color: "#2e8bc0", alpha: 1, position: 100 },
|
|
45
|
+
],
|
|
46
|
+
angle: 135,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "midnight",
|
|
51
|
+
label: "Midnight",
|
|
52
|
+
category: "linear",
|
|
53
|
+
template: {
|
|
54
|
+
type: "linear",
|
|
55
|
+
stops: [
|
|
56
|
+
{ color: "#0a0a0a", alpha: 1, position: 0 },
|
|
57
|
+
{ color: "#3d1c56", alpha: 1, position: 100 },
|
|
58
|
+
],
|
|
59
|
+
angle: 135,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "fire",
|
|
64
|
+
label: "Fire",
|
|
65
|
+
category: "linear",
|
|
66
|
+
template: {
|
|
67
|
+
type: "linear",
|
|
68
|
+
stops: [
|
|
69
|
+
{ color: "#c62828", alpha: 1, position: 0 },
|
|
70
|
+
{ color: "#f9a825", alpha: 1, position: 100 },
|
|
71
|
+
],
|
|
72
|
+
angle: 135,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
// ── Mesh presets ──
|
|
76
|
+
{
|
|
77
|
+
id: "nebula",
|
|
78
|
+
label: "Nebula",
|
|
79
|
+
category: "mesh",
|
|
80
|
+
template: {
|
|
81
|
+
type: "mesh",
|
|
82
|
+
points: [
|
|
83
|
+
{ color: "#6a0dad", x: 20, y: 25 },
|
|
84
|
+
{ color: "#3a0ca3", x: 75, y: 35 },
|
|
85
|
+
{ color: "#4cc9f0", x: 50, y: 80 },
|
|
86
|
+
],
|
|
87
|
+
background: "#0b0b1a",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "pastel",
|
|
92
|
+
label: "Pastel",
|
|
93
|
+
category: "mesh",
|
|
94
|
+
template: {
|
|
95
|
+
type: "mesh",
|
|
96
|
+
points: [
|
|
97
|
+
{ color: "#f8c8dc", x: 25, y: 30 },
|
|
98
|
+
{ color: "#fef3e2", x: 70, y: 25 },
|
|
99
|
+
{ color: "#dcd6f7", x: 50, y: 75 },
|
|
100
|
+
],
|
|
101
|
+
background: "#faf5ff",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "aurora",
|
|
106
|
+
label: "Aurora",
|
|
107
|
+
category: "mesh",
|
|
108
|
+
template: {
|
|
109
|
+
type: "mesh",
|
|
110
|
+
points: [
|
|
111
|
+
{ color: "#00b894", x: 15, y: 40 },
|
|
112
|
+
{ color: "#6c5ce7", x: 80, y: 30 },
|
|
113
|
+
{ color: "#0984e3", x: 50, y: 80 },
|
|
114
|
+
],
|
|
115
|
+
background: "#0a0a2a",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/** Filter presets by category */
|
|
121
|
+
export function getPresetsForCategory(category: GradientPresetCategory): GradientPresetInfo[] {
|
|
122
|
+
return GRADIENT_PRESETS.filter((p) => p.category === category);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Quick lookup by ID */
|
|
126
|
+
export const GRADIENT_PRESET_MAP: Record<string, GradientPresetInfo> = Object.fromEntries(
|
|
127
|
+
GRADIENT_PRESETS.map((p) => [p.id, p])
|
|
128
|
+
) as Record<string, GradientPresetInfo>;
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { BlockLayout } from "../../lib/sanity/types";
|
|
14
|
-
import { hexToRgba } from "../../lib/color-utils";
|
|
14
|
+
import { hexToRgba, colorToCSSProperty, borderColorToCSS, parseColorField } from "../../lib/color-utils";
|
|
15
|
+
import type { ColorField } from "../../lib/sanity/types";
|
|
15
16
|
|
|
16
17
|
/** Row-level settings shape (section settings, V2 settings, etc.) */
|
|
17
18
|
type RowSettings = LayoutProps & { padding?: string };
|
|
@@ -110,14 +111,12 @@ export function getBackgroundStyles(
|
|
|
110
111
|
): React.CSSProperties {
|
|
111
112
|
const styles: React.CSSProperties = {};
|
|
112
113
|
|
|
113
|
-
// Background color + opacity
|
|
114
|
+
// Background color + opacity (supports solid hex and gradients via ColorField)
|
|
114
115
|
if (s.background_color && s.background_color !== "transparent") {
|
|
115
116
|
const opacity = s.background_opacity ?? 100;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
styles.backgroundColor = s.background_color;
|
|
120
|
-
}
|
|
117
|
+
const colorField = parseColorField(s.background_color);
|
|
118
|
+
const colorProps = colorToCSSProperty(colorField, opacity);
|
|
119
|
+
Object.assign(styles, colorProps);
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
// Background image
|
|
@@ -166,9 +165,11 @@ export function getBorderStyles(s: LayoutProps): React.CSSProperties {
|
|
|
166
165
|
const bs = s.border_style || "none";
|
|
167
166
|
|
|
168
167
|
if (bw > 0 && bs !== "none") {
|
|
169
|
-
const bc = s.border_color || "#000000";
|
|
168
|
+
const bc: ColorField = parseColorField(s.border_color || "#000000");
|
|
170
169
|
const sides = s.border_sides || "all";
|
|
171
|
-
|
|
170
|
+
// Use bridge function for gradient-safe border rendering
|
|
171
|
+
const borderProps = borderColorToCSS(bc, bw, bs);
|
|
172
|
+
const borderValue = borderProps.border;
|
|
172
173
|
|
|
173
174
|
switch (sides) {
|
|
174
175
|
case "top":
|
|
@@ -195,6 +196,11 @@ export function getBorderStyles(s: LayoutProps): React.CSSProperties {
|
|
|
195
196
|
styles.border = borderValue;
|
|
196
197
|
break;
|
|
197
198
|
}
|
|
199
|
+
// For gradient borders, apply borderImage regardless of sides
|
|
200
|
+
// (CSS border-image applies to all existing borders)
|
|
201
|
+
if (borderProps.borderImage) {
|
|
202
|
+
styles.borderImage = borderProps.borderImage;
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
const br = parseInt(s.border_radius || "0");
|
|
@@ -340,5 +346,5 @@ export function resolveEffectiveSpacing(s: RowSettings): {
|
|
|
340
346
|
|
|
341
347
|
// ---- Helpers ----
|
|
342
348
|
|
|
343
|
-
// Re-export hexToRgba for backward compatibility (imported at top from
|
|
349
|
+
// Re-export hexToRgba for backward compatibility (imported at top from ../color-utils)
|
|
344
350
|
export { hexToRgba };
|