@morphika/andami 0.5.0 → 0.5.2

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 (122) hide show
  1. package/README.md +151 -36
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. package/styles/base.css +114 -114
@@ -1,102 +1,102 @@
1
- "use client";
2
-
3
- import { useEffect, useRef } from "react";
4
-
5
- interface NavItemTypePickerProps {
6
- column: number;
7
- onSelect: (type: "logo" | "menu-item", defaultSpan: number) => void;
8
- onClose: () => void;
9
- hasLogo: boolean; // prevent adding multiple logos
10
- }
11
-
12
- const ITEM_TYPES = [
13
- {
14
- type: "logo" as const,
15
- label: "Logo",
16
- description: "Brand logo (text or image)",
17
- defaultSpan: 3,
18
- icon: (
19
- <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
20
- <rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
21
- <path d="M6 10h8M6 13h4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
22
- <circle cx="10" cy="7.5" r="1.5" stroke="currentColor" strokeWidth="1.2" />
23
- </svg>
24
- ),
25
- },
26
- {
27
- type: "menu-item" as const,
28
- label: "Menu Item",
29
- description: "Navigation link",
30
- defaultSpan: 1,
31
- icon: (
32
- <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
33
- <path d="M4 7h12M4 10h8M4 13h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
34
- </svg>
35
- ),
36
- },
37
- ];
38
-
39
- export default function NavItemTypePicker({
40
- column,
41
- onSelect,
42
- onClose,
43
- hasLogo,
44
- }: NavItemTypePickerProps) {
45
- const ref = useRef<HTMLDivElement>(null);
46
-
47
- // Close on Escape
48
- useEffect(() => {
49
- const handleKey = (e: KeyboardEvent) => {
50
- if (e.key === "Escape") onClose();
51
- };
52
- window.addEventListener("keydown", handleKey);
53
- return () => window.removeEventListener("keydown", handleKey);
54
- }, [onClose]);
55
-
56
- // Focus trap
57
- useEffect(() => {
58
- ref.current?.focus();
59
- }, []);
60
-
61
- const types = hasLogo
62
- ? ITEM_TYPES.filter((t) => t.type !== "logo")
63
- : ITEM_TYPES;
64
-
65
- return (
66
- <div
67
- className="fixed inset-0 z-50 flex items-center justify-center"
68
- style={{ background: "rgba(0,0,0,0.2)", backdropFilter: "blur(4px)" }}
69
- onClick={onClose}
70
- >
71
- <div
72
- ref={ref}
73
- tabIndex={-1}
74
- className="outline-none bg-white border border-neutral-200 rounded-2xl p-6 shadow-2xl"
75
- style={{ width: 320 }}
76
- onClick={(e) => e.stopPropagation()}
77
- >
78
- <div className="text-[13px] font-semibold text-neutral-900 mb-1">Add Nav Item</div>
79
- <div className="text-[11px] text-neutral-500 mb-4">Column {column}</div>
80
- <div className="flex flex-col gap-2">
81
- {types.map((t) => (
82
- <button
83
- key={t.type}
84
- onClick={() => onSelect(t.type, t.defaultSpan)}
85
- className="flex items-center gap-3 p-3 rounded-xl border border-neutral-200 bg-white text-left transition-colors hover:border-[#076bff] hover:bg-[#076bff]/[0.02]"
86
- >
87
- <div className="w-10 h-10 rounded-lg bg-neutral-50 border border-neutral-100 flex items-center justify-center text-[#076bff] shrink-0">
88
- {t.icon}
89
- </div>
90
- <div>
91
- <div className="text-[13px] font-medium text-neutral-900">{t.label}</div>
92
- <div className="text-[11px] text-neutral-500 mt-0.5">
93
- {t.description}
94
- </div>
95
- </div>
96
- </button>
97
- ))}
98
- </div>
99
- </div>
100
- </div>
101
- );
102
- }
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ interface NavItemTypePickerProps {
6
+ column: number;
7
+ onSelect: (type: "logo" | "menu-item", defaultSpan: number) => void;
8
+ onClose: () => void;
9
+ hasLogo: boolean; // prevent adding multiple logos
10
+ }
11
+
12
+ const ITEM_TYPES = [
13
+ {
14
+ type: "logo" as const,
15
+ label: "Logo",
16
+ description: "Brand logo (text or image)",
17
+ defaultSpan: 3,
18
+ icon: (
19
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
20
+ <rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
21
+ <path d="M6 10h8M6 13h4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
22
+ <circle cx="10" cy="7.5" r="1.5" stroke="currentColor" strokeWidth="1.2" />
23
+ </svg>
24
+ ),
25
+ },
26
+ {
27
+ type: "menu-item" as const,
28
+ label: "Menu Item",
29
+ description: "Navigation link",
30
+ defaultSpan: 1,
31
+ icon: (
32
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
33
+ <path d="M4 7h12M4 10h8M4 13h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
34
+ </svg>
35
+ ),
36
+ },
37
+ ];
38
+
39
+ export default function NavItemTypePicker({
40
+ column,
41
+ onSelect,
42
+ onClose,
43
+ hasLogo,
44
+ }: NavItemTypePickerProps) {
45
+ const ref = useRef<HTMLDivElement>(null);
46
+
47
+ // Close on Escape
48
+ useEffect(() => {
49
+ const handleKey = (e: KeyboardEvent) => {
50
+ if (e.key === "Escape") onClose();
51
+ };
52
+ window.addEventListener("keydown", handleKey);
53
+ return () => window.removeEventListener("keydown", handleKey);
54
+ }, [onClose]);
55
+
56
+ // Focus trap
57
+ useEffect(() => {
58
+ ref.current?.focus();
59
+ }, []);
60
+
61
+ const types = hasLogo
62
+ ? ITEM_TYPES.filter((t) => t.type !== "logo")
63
+ : ITEM_TYPES;
64
+
65
+ return (
66
+ <div
67
+ className="fixed inset-0 z-50 flex items-center justify-center"
68
+ style={{ background: "rgba(0,0,0,0.2)", backdropFilter: "blur(4px)" }}
69
+ onClick={onClose}
70
+ >
71
+ <div
72
+ ref={ref}
73
+ tabIndex={-1}
74
+ className="outline-none bg-white border border-neutral-200 rounded-2xl p-6 shadow-2xl"
75
+ style={{ width: 320 }}
76
+ onClick={(e) => e.stopPropagation()}
77
+ >
78
+ <div className="text-[13px] font-semibold text-neutral-900 mb-1">Add Nav Item</div>
79
+ <div className="text-[11px] text-neutral-500 mb-4">Column {column}</div>
80
+ <div className="flex flex-col gap-2">
81
+ {types.map((t) => (
82
+ <button
83
+ key={t.type}
84
+ onClick={() => onSelect(t.type, t.defaultSpan)}
85
+ className="flex items-center gap-3 p-3 rounded-xl border border-neutral-200 bg-white text-left transition-colors hover:border-[#3580f9] hover:bg-[#3580f9]/[0.02]"
86
+ >
87
+ <div className="w-10 h-10 rounded-lg bg-neutral-50 border border-neutral-100 flex items-center justify-center text-[#3580f9] shrink-0">
88
+ {t.icon}
89
+ </div>
90
+ <div>
91
+ <div className="text-[13px] font-medium text-neutral-900">{t.label}</div>
92
+ <div className="text-[11px] text-neutral-500 mt-0.5">
93
+ {t.description}
94
+ </div>
95
+ </div>
96
+ </button>
97
+ ))}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -10,7 +10,7 @@ const NAV_COLOR_HEX: Record<NavColorVariant, string> = {
10
10
  "yellow-lime": _cfg.palette.accent,
11
11
  yellow: _cfg.palette.accent,
12
12
  "red-coral": _cfg.palette.secondary,
13
- blue: "#076bff",
13
+ blue: "#3580f9",
14
14
  green: _cfg.palette.accent,
15
15
  white: _cfg.palette.text,
16
16
  };
@@ -1,226 +1,226 @@
1
- "use client";
2
-
3
- import type { NavItem, NavDesign, MobileNavDesign, NavColorVariant } from "../../../lib/sanity/types";
4
- import { hexToRgba } from "./nav-builder-utils";
5
- import { getSiteConfig } from "../../../lib/config";
6
-
7
- const _cfg = getSiteConfig();
8
-
9
- const NAV_COLOR_HEX: Record<NavColorVariant, string> = {
10
- "yellow-lime": _cfg.palette.accent,
11
- yellow: _cfg.palette.accent,
12
- "red-coral": _cfg.palette.secondary,
13
- blue: "#076bff",
14
- green: _cfg.palette.accent,
15
- white: _cfg.palette.text,
16
- };
17
-
18
- // ============================================
19
- // NavMobileLivePreview — phone-sized preview of the mobile menu
20
- // Session 158: Mirrors NavLivePreview visual language (dark bg, footer label)
21
- // but renders the mobile hamburger overlay layout inside a phone frame.
22
- // Designed to sit on the right side of NavMobileSettings.
23
- // ============================================
24
-
25
- interface NavMobileLivePreviewProps {
26
- items: NavItem[];
27
- design: NavDesign;
28
- mobileDesign: MobileNavDesign;
29
- }
30
-
31
- export default function NavMobileLivePreview({
32
- items,
33
- design,
34
- mobileDesign,
35
- }: NavMobileLivePreviewProps) {
36
- // Desktop fallback values
37
- const desktopColorKey = design.color || "yellow-lime";
38
- const desktopColor = /^#[0-9a-fA-F]{6}$/.test(desktopColorKey)
39
- ? desktopColorKey
40
- : NAV_COLOR_HEX[desktopColorKey as keyof typeof NAV_COLOR_HEX] || NAV_COLOR_HEX["yellow-lime"];
41
-
42
- // ── Navbar bar (logo + hamburger) ──
43
- const barPaddingH = mobileDesign.padding_h ?? design.padding_h ?? 24;
44
- const barPaddingV = mobileDesign.padding_v ?? design.padding_v ?? 27;
45
- const barBg = mobileDesign.navbar_bg || "";
46
- const barBgOpacity = mobileDesign.navbar_bg_opacity ?? 0;
47
- const barBgColor =
48
- barBg && barBgOpacity > 0 ? hexToRgba(barBg, barBgOpacity / 100) : "transparent";
49
- const hamburgerColor = mobileDesign.hamburger_color || desktopColor;
50
-
51
- // ── Overlay (expanded menu) ──
52
- const overlayBg = mobileDesign.overlay_bg || "#0a0a0a";
53
- const textColor = mobileDesign.text_color || desktopColor;
54
- const fontSize = mobileDesign.font_size ?? 24;
55
- const textTransform = (mobileDesign.text_transform || design.text_transform || "uppercase") as React.CSSProperties["textTransform"];
56
- const itemsGap = mobileDesign.items_gap ?? 32;
57
- const itemsAlign = mobileDesign.items_align || "center";
58
- const alignItems =
59
- itemsAlign === "right" ? "flex-end" : itemsAlign === "left" ? "flex-start" : "center";
60
-
61
- const fontFamily =
62
- design.font_family || `var(--font-family, '${_cfg.typography.defaultFont}', ${_cfg.typography.monoFallback})`;
63
-
64
- const logoLabel =
65
- items.find((i) => i.type === "logo")?.label ||
66
- design.logo_text ||
67
- _cfg.defaults.logoText;
68
- const menuItems = items
69
- .filter((i) => i.type !== "logo" && i.visible !== false)
70
- .sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0));
71
-
72
- return (
73
- <div
74
- style={{
75
- background: "#020202",
76
- height: "100%",
77
- display: "flex",
78
- flexDirection: "column",
79
- }}
80
- >
81
- {/* Phone frame — centered */}
82
- <div
83
- style={{
84
- flex: 1,
85
- display: "flex",
86
- alignItems: "center",
87
- justifyContent: "center",
88
- padding: "20px 24px 12px",
89
- }}
90
- >
91
- <div
92
- style={{
93
- width: "100%",
94
- maxWidth: 280,
95
- borderRadius: 20,
96
- overflow: "hidden",
97
- border: "1px solid rgba(255,255,255,0.08)",
98
- background: overlayBg,
99
- }}
100
- >
101
- {/* Navbar bar — logo + hamburger icon */}
102
- <div
103
- style={{
104
- display: "flex",
105
- alignItems: "center",
106
- justifyContent: "space-between",
107
- paddingLeft: `${Math.min(barPaddingH, 24)}px`,
108
- paddingRight: `${Math.min(barPaddingH, 24)}px`,
109
- paddingTop: `${Math.min(barPaddingV, 20)}px`,
110
- paddingBottom: `${Math.min(barPaddingV, 20)}px`,
111
- background: barBgColor,
112
- }}
113
- >
114
- {/* Logo label */}
115
- <span
116
- style={{
117
- color: hamburgerColor,
118
- fontSize: Math.min(design.font_size ?? 14, 14),
119
- fontWeight: design.font_weight || "400",
120
- fontFamily,
121
- textTransform,
122
- letterSpacing: "0.05em",
123
- overflow: "hidden",
124
- textOverflow: "ellipsis",
125
- whiteSpace: "nowrap",
126
- }}
127
- >
128
- {logoLabel}
129
- </span>
130
-
131
- {/* Hamburger icon */}
132
- <div
133
- style={{
134
- display: "flex",
135
- flexDirection: "column",
136
- gap: 3.5,
137
- flexShrink: 0,
138
- }}
139
- >
140
- <span
141
- style={{
142
- display: "block",
143
- width: 16,
144
- height: 1.5,
145
- borderRadius: 1,
146
- background: hamburgerColor,
147
- }}
148
- />
149
- <span
150
- style={{
151
- display: "block",
152
- width: 16,
153
- height: 1.5,
154
- borderRadius: 1,
155
- background: hamburgerColor,
156
- }}
157
- />
158
- <span
159
- style={{
160
- display: "block",
161
- width: 16,
162
- height: 1.5,
163
- borderRadius: 1,
164
- background: hamburgerColor,
165
- }}
166
- />
167
- </div>
168
- </div>
169
-
170
- {/* Overlay — menu items */}
171
- <div
172
- style={{
173
- display: "flex",
174
- flexDirection: "column",
175
- alignItems,
176
- justifyContent: "center",
177
- gap: `${Math.min(itemsGap, 40)}px`,
178
- paddingLeft: `${Math.min(barPaddingH, 24)}px`,
179
- paddingRight: `${Math.min(barPaddingH, 24)}px`,
180
- paddingTop: 24,
181
- paddingBottom: 32,
182
- minHeight: 180,
183
- }}
184
- >
185
- {menuItems.map((item) => (
186
- <span
187
- key={item._key}
188
- style={{
189
- color: textColor,
190
- fontSize: Math.min(fontSize, 28),
191
- fontWeight: design.font_weight || "400",
192
- fontFamily,
193
- textTransform,
194
- letterSpacing: "0.05em",
195
- whiteSpace: "nowrap",
196
- overflow: "hidden",
197
- textOverflow: "ellipsis",
198
- maxWidth: "100%",
199
- }}
200
- >
201
- {item.label || "Untitled"}
202
- </span>
203
- ))}
204
- {menuItems.length === 0 && (
205
- <span
206
- style={{
207
- color: "rgba(255,255,255,0.2)",
208
- fontSize: 12,
209
- fontStyle: "italic",
210
- }}
211
- >
212
- No menu items
213
- </span>
214
- )}
215
- </div>
216
- </div>
217
- </div>
218
-
219
- {/* Footer — matches NavLivePreview style */}
220
- <div className="flex items-center justify-between px-3 py-1.5 text-[9px] text-neutral-600">
221
- <span>Mobile Preview</span>
222
- <span>{menuItems.length} menu items</span>
223
- </div>
224
- </div>
225
- );
226
- }
1
+ "use client";
2
+
3
+ import type { NavItem, NavDesign, MobileNavDesign, NavColorVariant } from "../../../lib/sanity/types";
4
+ import { hexToRgba } from "./nav-builder-utils";
5
+ import { getSiteConfig } from "../../../lib/config";
6
+
7
+ const _cfg = getSiteConfig();
8
+
9
+ const NAV_COLOR_HEX: Record<NavColorVariant, string> = {
10
+ "yellow-lime": _cfg.palette.accent,
11
+ yellow: _cfg.palette.accent,
12
+ "red-coral": _cfg.palette.secondary,
13
+ blue: "#3580f9",
14
+ green: _cfg.palette.accent,
15
+ white: _cfg.palette.text,
16
+ };
17
+
18
+ // ============================================
19
+ // NavMobileLivePreview — phone-sized preview of the mobile menu
20
+ // Session 158: Mirrors NavLivePreview visual language (dark bg, footer label)
21
+ // but renders the mobile hamburger overlay layout inside a phone frame.
22
+ // Designed to sit on the right side of NavMobileSettings.
23
+ // ============================================
24
+
25
+ interface NavMobileLivePreviewProps {
26
+ items: NavItem[];
27
+ design: NavDesign;
28
+ mobileDesign: MobileNavDesign;
29
+ }
30
+
31
+ export default function NavMobileLivePreview({
32
+ items,
33
+ design,
34
+ mobileDesign,
35
+ }: NavMobileLivePreviewProps) {
36
+ // Desktop fallback values
37
+ const desktopColorKey = design.color || "yellow-lime";
38
+ const desktopColor = /^#[0-9a-fA-F]{6}$/.test(desktopColorKey)
39
+ ? desktopColorKey
40
+ : NAV_COLOR_HEX[desktopColorKey as keyof typeof NAV_COLOR_HEX] || NAV_COLOR_HEX["yellow-lime"];
41
+
42
+ // ── Navbar bar (logo + hamburger) ──
43
+ const barPaddingH = mobileDesign.padding_h ?? design.padding_h ?? 24;
44
+ const barPaddingV = mobileDesign.padding_v ?? design.padding_v ?? 27;
45
+ const barBg = mobileDesign.navbar_bg || "";
46
+ const barBgOpacity = mobileDesign.navbar_bg_opacity ?? 0;
47
+ const barBgColor =
48
+ barBg && barBgOpacity > 0 ? hexToRgba(barBg, barBgOpacity / 100) : "transparent";
49
+ const hamburgerColor = mobileDesign.hamburger_color || desktopColor;
50
+
51
+ // ── Overlay (expanded menu) ──
52
+ const overlayBg = mobileDesign.overlay_bg || "#0a0a0a";
53
+ const textColor = mobileDesign.text_color || desktopColor;
54
+ const fontSize = mobileDesign.font_size ?? 24;
55
+ const textTransform = (mobileDesign.text_transform || design.text_transform || "uppercase") as React.CSSProperties["textTransform"];
56
+ const itemsGap = mobileDesign.items_gap ?? 32;
57
+ const itemsAlign = mobileDesign.items_align || "center";
58
+ const alignItems =
59
+ itemsAlign === "right" ? "flex-end" : itemsAlign === "left" ? "flex-start" : "center";
60
+
61
+ const fontFamily =
62
+ design.font_family || `var(--font-family, '${_cfg.typography.defaultFont}', ${_cfg.typography.monoFallback})`;
63
+
64
+ const logoLabel =
65
+ items.find((i) => i.type === "logo")?.label ||
66
+ design.logo_text ||
67
+ _cfg.defaults.logoText;
68
+ const menuItems = items
69
+ .filter((i) => i.type !== "logo" && i.visible !== false)
70
+ .sort((a, b) => (a.grid_column || 0) - (b.grid_column || 0));
71
+
72
+ return (
73
+ <div
74
+ style={{
75
+ background: "#020202",
76
+ height: "100%",
77
+ display: "flex",
78
+ flexDirection: "column",
79
+ }}
80
+ >
81
+ {/* Phone frame — centered */}
82
+ <div
83
+ style={{
84
+ flex: 1,
85
+ display: "flex",
86
+ alignItems: "center",
87
+ justifyContent: "center",
88
+ padding: "20px 24px 12px",
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ width: "100%",
94
+ maxWidth: 280,
95
+ borderRadius: 20,
96
+ overflow: "hidden",
97
+ border: "1px solid rgba(255,255,255,0.08)",
98
+ background: overlayBg,
99
+ }}
100
+ >
101
+ {/* Navbar bar — logo + hamburger icon */}
102
+ <div
103
+ style={{
104
+ display: "flex",
105
+ alignItems: "center",
106
+ justifyContent: "space-between",
107
+ paddingLeft: `${Math.min(barPaddingH, 24)}px`,
108
+ paddingRight: `${Math.min(barPaddingH, 24)}px`,
109
+ paddingTop: `${Math.min(barPaddingV, 20)}px`,
110
+ paddingBottom: `${Math.min(barPaddingV, 20)}px`,
111
+ background: barBgColor,
112
+ }}
113
+ >
114
+ {/* Logo label */}
115
+ <span
116
+ style={{
117
+ color: hamburgerColor,
118
+ fontSize: Math.min(design.font_size ?? 14, 14),
119
+ fontWeight: design.font_weight || "400",
120
+ fontFamily,
121
+ textTransform,
122
+ letterSpacing: "0.05em",
123
+ overflow: "hidden",
124
+ textOverflow: "ellipsis",
125
+ whiteSpace: "nowrap",
126
+ }}
127
+ >
128
+ {logoLabel}
129
+ </span>
130
+
131
+ {/* Hamburger icon */}
132
+ <div
133
+ style={{
134
+ display: "flex",
135
+ flexDirection: "column",
136
+ gap: 3.5,
137
+ flexShrink: 0,
138
+ }}
139
+ >
140
+ <span
141
+ style={{
142
+ display: "block",
143
+ width: 16,
144
+ height: 1.5,
145
+ borderRadius: 1,
146
+ background: hamburgerColor,
147
+ }}
148
+ />
149
+ <span
150
+ style={{
151
+ display: "block",
152
+ width: 16,
153
+ height: 1.5,
154
+ borderRadius: 1,
155
+ background: hamburgerColor,
156
+ }}
157
+ />
158
+ <span
159
+ style={{
160
+ display: "block",
161
+ width: 16,
162
+ height: 1.5,
163
+ borderRadius: 1,
164
+ background: hamburgerColor,
165
+ }}
166
+ />
167
+ </div>
168
+ </div>
169
+
170
+ {/* Overlay — menu items */}
171
+ <div
172
+ style={{
173
+ display: "flex",
174
+ flexDirection: "column",
175
+ alignItems,
176
+ justifyContent: "center",
177
+ gap: `${Math.min(itemsGap, 40)}px`,
178
+ paddingLeft: `${Math.min(barPaddingH, 24)}px`,
179
+ paddingRight: `${Math.min(barPaddingH, 24)}px`,
180
+ paddingTop: 24,
181
+ paddingBottom: 32,
182
+ minHeight: 180,
183
+ }}
184
+ >
185
+ {menuItems.map((item) => (
186
+ <span
187
+ key={item._key}
188
+ style={{
189
+ color: textColor,
190
+ fontSize: Math.min(fontSize, 28),
191
+ fontWeight: design.font_weight || "400",
192
+ fontFamily,
193
+ textTransform,
194
+ letterSpacing: "0.05em",
195
+ whiteSpace: "nowrap",
196
+ overflow: "hidden",
197
+ textOverflow: "ellipsis",
198
+ maxWidth: "100%",
199
+ }}
200
+ >
201
+ {item.label || "Untitled"}
202
+ </span>
203
+ ))}
204
+ {menuItems.length === 0 && (
205
+ <span
206
+ style={{
207
+ color: "rgba(255,255,255,0.2)",
208
+ fontSize: 12,
209
+ fontStyle: "italic",
210
+ }}
211
+ >
212
+ No menu items
213
+ </span>
214
+ )}
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Footer — matches NavLivePreview style */}
220
+ <div className="flex items-center justify-between px-3 py-1.5 text-[9px] text-neutral-600">
221
+ <span>Mobile Preview</span>
222
+ <span>{menuItems.length} menu items</span>
223
+ </div>
224
+ </div>
225
+ );
226
+ }