@morphika/andami 0.1.3 → 0.1.5
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 +2 -2
- package/app/(site)/layout.tsx +1 -0
- package/app/(site)/page.tsx +2 -2
- package/app/(site)/preview/page.tsx +4 -4
- package/app/(site)/work/[slug]/page.tsx +2 -2
- package/app/admin/layout.tsx +2 -2
- package/app/admin/login/page.tsx +5 -5
- package/app/admin/navigation/page.tsx +255 -157
- package/app/api/admin/assets/relink/confirm/route.ts +1 -1
- package/app/api/admin/pages/[slug]/route.ts +1 -1
- package/app/api/admin/settings/route.ts +40 -15
- package/app/api/admin/setup/complete/route.ts +1 -1
- package/app/api/admin/setup/route.ts +6 -3
- 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 +95 -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/queries.ts +4 -3
- package/lib/sanity/types.ts +76 -1
- package/lib/setup/detect.ts +1 -1
- 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,28 @@ 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 */}
|
|
497
540
|
<div
|
|
498
|
-
className=
|
|
541
|
+
className={`flex lg:hidden items-center justify-between ${mobileBarColorClass}`}
|
|
499
542
|
style={{
|
|
500
543
|
maxWidth: "var(--grid-width, 1445px)",
|
|
501
544
|
marginLeft: "auto",
|
|
502
545
|
marginRight: "auto",
|
|
503
|
-
paddingLeft: `${
|
|
504
|
-
paddingRight: `${
|
|
505
|
-
paddingTop: `${
|
|
506
|
-
paddingBottom: `${
|
|
546
|
+
paddingLeft: `${mobilePaddingH}px`,
|
|
547
|
+
paddingRight: `${mobilePaddingH}px`,
|
|
548
|
+
paddingTop: `${mobilePaddingV}px`,
|
|
549
|
+
paddingBottom: `${mobilePaddingV}px`,
|
|
550
|
+
...mobileNavBgStyle,
|
|
551
|
+
...mobileBarColorStyle,
|
|
507
552
|
}}
|
|
508
553
|
>
|
|
509
554
|
<Link
|
|
510
555
|
href="/"
|
|
511
|
-
className={
|
|
512
|
-
style={
|
|
556
|
+
className={`tracking-normal ${mobileBarColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
557
|
+
style={{
|
|
558
|
+
...linkStyle,
|
|
559
|
+
...(mobileBarIsHex ? { color: "inherit" } : {}),
|
|
560
|
+
}}
|
|
513
561
|
>
|
|
514
562
|
{logoLabel}
|
|
515
563
|
</Link>
|
|
@@ -517,10 +565,11 @@ export default function Navbar({
|
|
|
517
565
|
<button
|
|
518
566
|
ref={hamburgerButtonRef}
|
|
519
567
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
520
|
-
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${
|
|
568
|
+
className={`flex flex-col justify-center items-center gap-[5px] w-8 h-8 ${mobileBarColorClass} focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
|
|
521
569
|
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
|
522
570
|
aria-expanded={isMenuOpen}
|
|
523
571
|
aria-controls="mobile-nav-menu"
|
|
572
|
+
style={mobileBarIsHex ? { color: mobileBarColor } : undefined}
|
|
524
573
|
>
|
|
525
574
|
<span
|
|
526
575
|
className={`block w-5 h-[1.5px] bg-current transition-all duration-300 ${
|
|
@@ -541,30 +590,51 @@ export default function Navbar({
|
|
|
541
590
|
</div>
|
|
542
591
|
</nav>
|
|
543
592
|
|
|
544
|
-
{/* Mobile overlay menu */}
|
|
593
|
+
{/* Mobile overlay menu — uses independent mobile styles (Session 158) */}
|
|
545
594
|
<div
|
|
546
595
|
ref={mobileMenuRef}
|
|
547
596
|
id="mobile-nav-menu"
|
|
548
597
|
role="dialog"
|
|
549
598
|
aria-modal={isMenuOpen}
|
|
550
599
|
aria-label="Mobile navigation menu"
|
|
551
|
-
className={`fixed inset-0 z-40
|
|
600
|
+
className={`fixed inset-0 z-40 transition-opacity duration-300 lg:hidden ${
|
|
552
601
|
isMenuOpen
|
|
553
602
|
? "opacity-100 pointer-events-auto"
|
|
554
603
|
: "opacity-0 pointer-events-none"
|
|
555
604
|
}`}
|
|
605
|
+
style={{
|
|
606
|
+
backgroundColor: mobileOverlayBg || "var(--color-brand-dark, #0a0a0a)",
|
|
607
|
+
}}
|
|
556
608
|
>
|
|
557
|
-
<div
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
609
|
+
<div
|
|
610
|
+
className="flex flex-col justify-center h-full"
|
|
611
|
+
style={{
|
|
612
|
+
alignItems: mobileItemsAlign === "right" ? "flex-end" : mobileItemsAlign === "left" ? "flex-start" : "center",
|
|
613
|
+
gap: `${mobileItemsGap}px`,
|
|
614
|
+
paddingLeft: `${mobilePaddingH}px`,
|
|
615
|
+
paddingRight: `${mobilePaddingH}px`,
|
|
616
|
+
}}
|
|
617
|
+
>
|
|
618
|
+
{[...menuItems].sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0)).map((item) => {
|
|
619
|
+
// Mobile text color: explicit mobile setting > desktop design.color (NOT context)
|
|
620
|
+
const mobileItemIsHex = mobileTextColor.startsWith("#");
|
|
621
|
+
const mobileItemColorClass = mobileItemIsHex ? "" : (colorMap[design?.color || colorVariant] || colorMap["yellow-lime"]);
|
|
622
|
+
return (
|
|
623
|
+
<NavLink
|
|
624
|
+
key={item._key}
|
|
625
|
+
item={item}
|
|
626
|
+
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`}
|
|
627
|
+
style={{
|
|
628
|
+
fontSize: `${mobileFontSize}px`,
|
|
629
|
+
textTransform: mobileTextTransform as React.CSSProperties["textTransform"],
|
|
630
|
+
...(fontFamily ? { fontFamily } : {}),
|
|
631
|
+
...(mobileItemIsHex ? { color: mobileTextColor } : {}),
|
|
632
|
+
}}
|
|
633
|
+
currentPath={pathname}
|
|
634
|
+
onContentClick={(clickedItem) => { setLightboxItem(clickedItem); setIsMenuOpen(false); }}
|
|
635
|
+
/>
|
|
636
|
+
);
|
|
637
|
+
})}
|
|
568
638
|
</div>
|
|
569
639
|
</div>
|
|
570
640
|
|
|
@@ -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 };
|