@morphika/andami 0.1.2 → 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.
Files changed (85) hide show
  1. package/app/(site)/[slug]/page.tsx +2 -2
  2. package/app/(site)/layout.tsx +1 -0
  3. package/app/(site)/page.tsx +2 -2
  4. package/app/(site)/preview/page.tsx +4 -4
  5. package/app/(site)/work/[slug]/page.tsx +2 -2
  6. package/app/admin/layout.tsx +2 -2
  7. package/app/admin/login/page.tsx +5 -5
  8. package/app/admin/navigation/page.tsx +255 -157
  9. package/app/api/admin/assets/relink/confirm/route.ts +1 -1
  10. package/app/api/admin/pages/[slug]/route.ts +1 -1
  11. package/app/api/admin/settings/route.ts +40 -15
  12. package/app/api/admin/setup/complete/route.ts +1 -1
  13. package/app/api/admin/setup/route.ts +6 -3
  14. package/components/admin/index.ts +7 -0
  15. package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
  16. package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
  17. package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
  18. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
  19. package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
  20. package/components/admin/nav-builder/index.ts +2 -0
  21. package/components/blocks/BlockRenderer.tsx +65 -13
  22. package/components/blocks/ButtonBlockRenderer.tsx +29 -6
  23. package/components/blocks/CoverBlockRenderer.tsx +36 -14
  24. package/components/blocks/ImageBlockRenderer.tsx +5 -3
  25. package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
  26. package/components/blocks/PageRenderer.tsx +4 -2
  27. package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
  28. package/components/blocks/SectionRenderer.tsx +9 -8
  29. package/components/blocks/SectionV2Renderer.tsx +8 -8
  30. package/components/blocks/SpacerBlockRenderer.tsx +4 -2
  31. package/components/blocks/TextBlockRenderer.tsx +9 -4
  32. package/components/builder/BuilderCanvas.tsx +10 -4
  33. package/components/builder/ColorPicker.tsx +51 -243
  34. package/components/builder/ColorSwatchPicker.tsx +214 -274
  35. package/components/builder/DndWrapper.tsx +5 -2
  36. package/components/builder/SectionV2Canvas.tsx +15 -4
  37. package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
  38. package/components/builder/color-picker/AlphaSlider.tsx +141 -0
  39. package/components/builder/color-picker/AngleControl.tsx +138 -0
  40. package/components/builder/color-picker/ColorInputs.tsx +105 -0
  41. package/components/builder/color-picker/EyedropperButton.tsx +74 -0
  42. package/components/builder/color-picker/GradientBar.tsx +222 -0
  43. package/components/builder/color-picker/GradientPreview.tsx +53 -0
  44. package/components/builder/color-picker/HueSlider.tsx +124 -0
  45. package/components/builder/color-picker/MeshCanvas.tsx +172 -0
  46. package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
  47. package/components/builder/color-picker/MeshPointList.tsx +200 -0
  48. package/components/builder/color-picker/PositionControl.tsx +158 -0
  49. package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
  50. package/components/builder/color-picker/StopEditor.tsx +178 -0
  51. package/components/builder/color-picker/SwatchBar.tsx +93 -0
  52. package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
  53. package/components/builder/color-picker/index.ts +62 -0
  54. package/components/builder/color-picker/types.ts +115 -0
  55. package/components/builder/color-picker/utils.ts +138 -0
  56. package/components/builder/editors/CoverBlockEditor.tsx +86 -32
  57. package/components/builder/editors/ProjectGridEditor.tsx +51 -4
  58. package/components/builder/hooks/useColumnDrag.ts +25 -27
  59. package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
  60. package/components/builder/settings-panel/LayoutTab.tsx +382 -310
  61. package/components/builder/settings-panel/PageSettings.tsx +6 -4
  62. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  63. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
  64. package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
  65. package/components/ui/Navbar.tsx +95 -25
  66. package/components/ui/PortfolioTracker.tsx +3 -3
  67. package/lib/assets.ts +1 -1
  68. package/lib/auth.ts +1 -1
  69. package/lib/builder/gradient-presets.ts +128 -0
  70. package/lib/builder/layout-styles.ts +16 -10
  71. package/lib/builder/serializer.ts +1 -0
  72. package/lib/builder/store-blocks.ts +48 -61
  73. package/lib/builder/store-helpers.ts +31 -14
  74. package/lib/builder/store.ts +59 -41
  75. package/lib/builder/types.ts +14 -0
  76. package/lib/color-utils.ts +200 -0
  77. package/lib/config/index.ts +14 -43
  78. package/lib/revalidate.ts +2 -2
  79. package/lib/sanity/queries.ts +4 -3
  80. package/lib/sanity/types.ts +76 -1
  81. package/lib/setup/detect.ts +1 -1
  82. package/package.json +8 -12
  83. package/sanity/schemas/siteSettings.ts +34 -0
  84. package/styles/base.css +7 -51
  85. 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, appearance (background color/opacity/image)
8
- * - Layout: spacing TRBL, offset TRBL, border, background image settings (reuses LayoutTab patterns)
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 ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
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
  }
@@ -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 visibleItems = navItems.filter((item) => item.visible !== false);
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-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace)",
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="flex lg:hidden items-center justify-between"
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: `${paddingH}px`,
504
- paddingRight: `${paddingH}px`,
505
- paddingTop: `${paddingV}px`,
506
- paddingBottom: `${paddingV}px`,
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={linkClassName}
512
- style={linkStyle}
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 ${textColorClass} focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
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 bg-brand-dark transition-opacity duration-300 lg:hidden ${
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 className="flex flex-col items-center justify-center h-full gap-8">
558
- {[...menuItems].sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0)).map((item) => (
559
- <NavLink
560
- key={item._key}
561
- item={item}
562
- className={`font-mono text-2xl tracking-wide ${textColorClass} transition-colors duration-200 hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current`}
563
- style={{ textTransform: textTransformVal as React.CSSProperties["textTransform"], ...(fontFamily ? { fontFamily } : {}), ...(isHexColor ? { color: "inherit" } : {}) }}
564
- currentPath={pathname}
565
- onContentClick={(clickedItem) => { setLightboxItem(clickedItem); setIsMenuOpen(false); }}
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.knockApiUrl;
23
- const REF_KEY = `${cfg.tracking.sessionPrefix}_ref`;
24
- const CHANNEL_KEY = `${cfg.tracking.sessionPrefix}_channel`;
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
@@ -21,7 +21,7 @@
21
21
  * // Or proxy: "/api/assets/projects/house-of-delights/cover.jpg"
22
22
  */
23
23
 
24
- import { logger } from "../lib/logger";
24
+ import { logger } from "./logger";
25
25
 
26
26
  /**
27
27
  * Resolve a relative asset path to a full URL (public site).
package/lib/auth.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cookies } from "next/headers";
2
- import { validateAdminToken } from "../lib/auth-token";
2
+ import { validateAdminToken } from "./auth-token";
3
3
 
4
4
  /**
5
5
  * Verify that the current request is authenticated as admin.
@@ -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
- if (opacity < 100) {
117
- styles.backgroundColor = hexToRgba(s.background_color, opacity / 100);
118
- } else {
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
- const borderValue = `${bw}px ${bs} ${bc}`;
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 @/lib/color-utils)
349
+ // Re-export hexToRgba for backward compatibility (imported at top from ../color-utils)
344
350
  export { hexToRgba };
@@ -502,6 +502,7 @@ export function documentToState(doc: Page): Omit<BuilderState, "isDirty" | "isSa
502
502
  gutter_phone: "16",
503
503
  },
504
504
  selectedProjectCardKey: null,
505
+ colorPickerPreview: null,
505
506
  };
506
507
  }
507
508