@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,514 +1,514 @@
1
- "use client";
2
-
3
- import { useState, type ReactNode } from "react";
4
- import {
5
- PositionIcon,
6
- TypographyIcon,
7
- AnimationIcon,
8
- SpacingIcon,
9
- ContentIcon,
10
- LinkIcon,
11
- GridIcon,
12
- StyleIcon,
13
- BackgroundIcon,
14
- } from "../../builder/editors/section-icons";
15
-
16
- // ── Nav viewport type for responsive overrides ──
17
- export type NavViewport = "desktop" | "tablet" | "phone";
18
-
19
- // ── Shared field components for NavBuilder settings panel ──
20
- // v2: Card-based sections, colored icons, builder-aligned styling
21
-
22
- // ============================================
23
- // Field wrapper
24
- // ============================================
25
-
26
- export function Field({ label, children }: { label: string; children: ReactNode }) {
27
- return (
28
- <div className="flex items-center gap-2.5 py-[5px]">
29
- <label className="text-[11px] text-neutral-400 w-[56px] min-w-[56px] shrink-0">
30
- {label}
31
- </label>
32
- <div className="flex-1 min-w-0">{children}</div>
33
- </div>
34
- );
35
- }
36
-
37
- // ============================================
38
- // Text input
39
- // ============================================
40
-
41
- export function TextInput({
42
- value,
43
- onChange,
44
- placeholder,
45
- type = "text",
46
- }: {
47
- value: string | number;
48
- onChange: (value: string) => void;
49
- placeholder?: string;
50
- type?: "text" | "number";
51
- }) {
52
- return (
53
- <input
54
- type={type}
55
- value={value}
56
- onChange={(e) => onChange(e.target.value)}
57
- placeholder={placeholder}
58
- className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)] font-[inherit] placeholder:text-neutral-400"
59
- />
60
- );
61
- }
62
-
63
- // ============================================
64
- // Select input
65
- // ============================================
66
-
67
- export function SelectInput({
68
- value,
69
- onChange,
70
- options,
71
- }: {
72
- value: string;
73
- onChange: (value: string) => void;
74
- options: { value: string; label: string }[];
75
- }) {
76
- return (
77
- <div className="relative">
78
- <select
79
- value={value}
80
- onChange={(e) => onChange(e.target.value)}
81
- className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none appearance-none cursor-pointer pr-7 transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)] font-[inherit]"
82
- >
83
- {options.map((opt) => (
84
- <option key={opt.value} value={opt.value}>
85
- {opt.label}
86
- </option>
87
- ))}
88
- </select>
89
- <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-neutral-400">
90
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
91
- <path
92
- d="M3 4.5l3 3 3-3"
93
- stroke="currentColor"
94
- strokeWidth="1.5"
95
- strokeLinecap="round"
96
- strokeLinejoin="round"
97
- />
98
- </svg>
99
- </div>
100
- </div>
101
- );
102
- }
103
-
104
- // ============================================
105
- // Segmented control
106
- // ============================================
107
-
108
- export function SegmentedControl({
109
- value,
110
- onChange,
111
- options,
112
- }: {
113
- value: string;
114
- onChange: (value: string) => void;
115
- options: { value: string; label: string | ReactNode }[];
116
- }) {
117
- return (
118
- <div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0]">
119
- {options.map((opt) => (
120
- <button
121
- key={String(opt.value)}
122
- onClick={() => onChange(opt.value)}
123
- className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center ${
124
- value === opt.value
125
- ? "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
126
- : "text-neutral-400 hover:text-neutral-600"
127
- }`}
128
- >
129
- {opt.label}
130
- </button>
131
- ))}
132
- </div>
133
- );
134
- }
135
-
136
- // ============================================
137
- // Toggle switch
138
- // ============================================
139
-
140
- export function Toggle({
141
- value,
142
- onChange,
143
- }: {
144
- value: boolean;
145
- onChange: (value: boolean) => void;
146
- }) {
147
- return (
148
- <button
149
- onClick={() => onChange(!value)}
150
- className="w-9 h-5 rounded-full relative transition-all cursor-pointer"
151
- style={{
152
- background: value ? "#076bff" : "#d4d4d4",
153
- }}
154
- >
155
- <div
156
- className="w-3.5 h-3.5 rounded-full bg-white absolute top-[2.5px] transition-all shadow-sm"
157
- style={{ left: value ? 19 : 2 }}
158
- />
159
- </button>
160
- );
161
- }
162
-
163
- // ============================================
164
- // Range slider
165
- // ============================================
166
-
167
- export function RangeSlider({
168
- value,
169
- onChange,
170
- min = 0,
171
- max = 100,
172
- suffix,
173
- }: {
174
- value: number;
175
- onChange: (value: number) => void;
176
- min?: number;
177
- max?: number;
178
- suffix?: string;
179
- }) {
180
- return (
181
- <div className="flex items-center gap-2">
182
- <input
183
- type="range"
184
- min={min}
185
- max={max}
186
- value={value}
187
- onChange={(e) => onChange(Number(e.target.value))}
188
- className="flex-1"
189
- style={{ accentColor: "#076bff" }}
190
- />
191
- <span className="text-[10px] text-neutral-400 w-9 text-right tabular-nums">
192
- {value}{suffix || ""}
193
- </span>
194
- </div>
195
- );
196
- }
197
-
198
- // ============================================
199
- // Card section — collapsible with colored icon
200
- // ============================================
201
-
202
- export function CardSection({
203
- title,
204
- icon,
205
- iconBg,
206
- defaultOpen = true,
207
- children,
208
- }: {
209
- title: string;
210
- icon: ReactNode;
211
- iconBg: string;
212
- defaultOpen?: boolean;
213
- children: ReactNode;
214
- }) {
215
- const [open, setOpen] = useState(defaultOpen);
216
-
217
- return (
218
- <div
219
- className="mx-2.5 my-1 rounded-[10px] bg-[#fafafa] border border-transparent transition-colors hover:border-[#f0f0f0]"
220
- >
221
- <button
222
- onClick={() => setOpen(!open)}
223
- className="flex items-center gap-2 w-full px-3 py-2.5 cursor-pointer select-none hover:bg-black/[0.01] rounded-[10px] transition-colors"
224
- >
225
- <div
226
- className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center shrink-0"
227
- style={{ background: iconBg }}
228
- >
229
- {icon}
230
- </div>
231
- <span className="text-[11px] font-semibold text-neutral-600 tracking-[0.2px]">
232
- {title}
233
- </span>
234
- <span
235
- className="ml-auto text-neutral-400 transition-transform"
236
- style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
237
- >
238
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
239
- <path
240
- d="M3 4.5l3 3 3-3"
241
- stroke="currentColor"
242
- strokeWidth="1.5"
243
- strokeLinecap="round"
244
- strokeLinejoin="round"
245
- />
246
- </svg>
247
- </span>
248
- </button>
249
- {open && <div className="px-3 pb-3 pt-0.5">{children}</div>}
250
- </div>
251
- );
252
- }
253
-
254
- // ============================================
255
- // Legacy Section — kept for NavItemSettings compatibility
256
- // ============================================
257
-
258
- export function Section({
259
- title,
260
- defaultOpen = true,
261
- children,
262
- }: {
263
- title: string;
264
- defaultOpen?: boolean;
265
- children: ReactNode;
266
- }) {
267
- const [open, setOpen] = useState(defaultOpen);
268
-
269
- return (
270
- <div>
271
- <button
272
- onClick={() => setOpen(!open)}
273
- className="flex items-center justify-between w-full py-2 border-t border-neutral-200 mt-1 text-[11px] font-semibold text-neutral-900 tracking-wide cursor-pointer uppercase"
274
- >
275
- {title}
276
- <span
277
- className="text-neutral-400 transition-transform"
278
- style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
279
- >
280
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
281
- <path
282
- d="M3 4.5l3 3 3-3"
283
- stroke="currentColor"
284
- strokeWidth="1.5"
285
- strokeLinecap="round"
286
- strokeLinejoin="round"
287
- />
288
- </svg>
289
- </span>
290
- </button>
291
- {open && <div className="pb-2">{children}</div>}
292
- </div>
293
- );
294
- }
295
-
296
- // ============================================
297
- // Thin divider line
298
- // ============================================
299
-
300
- export function Divider() {
301
- return <div className="h-px bg-[#f0f0f0] my-1.5" />;
302
- }
303
-
304
- // ============================================
305
- // Color input (hex) with swatch preview
306
- // ============================================
307
-
308
- export function ColorInput({
309
- value,
310
- onChange,
311
- placeholder = "Transparent",
312
- }: {
313
- value: string;
314
- onChange: (value: string) => void;
315
- placeholder?: string;
316
- }) {
317
- return (
318
- <div className="flex gap-1.5 items-center">
319
- <div
320
- className="w-7 h-7 rounded-lg border border-neutral-200 shrink-0 cursor-pointer"
321
- style={{
322
- background: value || "transparent",
323
- backgroundImage: !value
324
- ? "repeating-conic-gradient(#e5e5e5 0% 25%, transparent 0% 50%) 0 0 / 8px 8px"
325
- : "none",
326
- }}
327
- />
328
- <TextInput value={value} onChange={onChange} placeholder={placeholder} />
329
- {value && (
330
- <button
331
- onClick={() => onChange("")}
332
- className="text-neutral-400 hover:text-neutral-600 shrink-0"
333
- title="Clear"
334
- >
335
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
336
- <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
337
- </svg>
338
- </button>
339
- )}
340
- </div>
341
- );
342
- }
343
-
344
- // ============================================
345
- // Card section icon presets — re-exported from centralized section-icons.tsx
346
- // Session 163: Unified icon system. All icons now live in section-icons.tsx.
347
- // ============================================
348
- export {
349
- PositionIcon,
350
- TypographyIcon,
351
- AnimationIcon,
352
- SpacingIcon,
353
- ContentIcon,
354
- LinkIcon,
355
- GridIcon,
356
- StyleIcon,
357
- BackgroundIcon,
358
- };
359
-
360
- // ============================================
361
- // Viewport Switcher — segmented control for Desktop / Tablet / Phone
362
- // ============================================
363
-
364
- const viewportOptions: { value: NavViewport; icon: ReactNode; label: string }[] = [
365
- {
366
- value: "desktop",
367
- label: "Desktop",
368
- icon: (
369
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
370
- <rect x="2" y="3" width="20" height="14" rx="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
371
- </svg>
372
- ),
373
- },
374
- {
375
- value: "tablet",
376
- label: "Tablet",
377
- icon: (
378
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
379
- <rect x="4" y="2" width="16" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
380
- </svg>
381
- ),
382
- },
383
- {
384
- value: "phone",
385
- label: "Phone",
386
- icon: (
387
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
388
- <rect x="5" y="2" width="14" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
389
- </svg>
390
- ),
391
- },
392
- ];
393
-
394
- export function ViewportSwitcher({
395
- value,
396
- onChange,
397
- }: {
398
- value: NavViewport;
399
- onChange: (v: NavViewport) => void;
400
- }) {
401
- return (
402
- <div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0] mb-2">
403
- {viewportOptions.map((opt) => {
404
- const isActive = value === opt.value;
405
- const isNonDesktop = opt.value !== "desktop";
406
- return (
407
- <button
408
- key={opt.value}
409
- onClick={() => onChange(opt.value)}
410
- title={opt.label}
411
- className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
412
- isActive
413
- ? isNonDesktop
414
- ? "text-[#076bff] bg-blue-50 shadow-[0_1px_2px_rgba(7,107,255,0.08)]"
415
- : "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
416
- : "text-neutral-400 hover:text-neutral-600"
417
- }`}
418
- >
419
- {opt.icon}
420
- </button>
421
- );
422
- })}
423
- </div>
424
- );
425
- }
426
-
427
- // ============================================
428
- // Viewport Badge — shows which viewport is being edited
429
- // ============================================
430
-
431
- export function NavViewportBadge({ viewport }: { viewport: NavViewport }) {
432
- if (viewport === "desktop") return null;
433
-
434
- const color = viewport === "tablet" ? "#076bff" : "#7c3aed";
435
- const bgColor = viewport === "tablet" ? "rgba(7, 107, 255, 0.06)" : "rgba(124, 58, 237, 0.06)";
436
- const label = viewport === "tablet" ? "Tablet" : "Phone";
437
-
438
- return (
439
- <div
440
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md mb-2 text-[10px]"
441
- style={{ background: bgColor, color }}
442
- >
443
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
444
- <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
445
- </svg>
446
- <span>
447
- Editing <strong>{label}</strong> overrides &middot; empty fields inherit desktop
448
- </span>
449
- </div>
450
- );
451
- }
452
-
453
- // ============================================
454
- // Responsive Field — wraps a field with inherited/overridden state
455
- // ============================================
456
-
457
- export function ResponsiveField({
458
- label,
459
- viewport,
460
- isOverridden,
461
- onReset,
462
- children,
463
- }: {
464
- label: string;
465
- viewport: NavViewport;
466
- isOverridden: boolean;
467
- onReset?: () => void;
468
- children: ReactNode;
469
- }) {
470
- // Desktop mode — render as a normal field
471
- if (viewport === "desktop") {
472
- return <Field label={label}>{children}</Field>;
473
- }
474
-
475
- return (
476
- <div className="flex items-center gap-2.5 py-[5px]">
477
- <div className="w-[56px] min-w-[56px] shrink-0">
478
- <label
479
- className={`text-[11px] block ${
480
- isOverridden ? "text-[#076bff] font-medium" : "text-neutral-400"
481
- }`}
482
- >
483
- {label}
484
- </label>
485
- {isOverridden ? (
486
- <span className="text-[8px] text-[#076bff]/60 uppercase tracking-[0.5px]">
487
- override
488
- </span>
489
- ) : (
490
- <span className="text-[8px] text-neutral-300 uppercase tracking-[0.5px]">
491
- inherited
492
- </span>
493
- )}
494
- </div>
495
- <div
496
- className="flex-1 min-w-0 transition-opacity"
497
- style={{ opacity: isOverridden ? 1 : 0.5 }}
498
- >
499
- {children}
500
- </div>
501
- {isOverridden && onReset && (
502
- <button
503
- onClick={onReset}
504
- className="shrink-0 text-[#076bff]/60 hover:text-[#076bff] transition-colors"
505
- title="Reset to desktop value"
506
- >
507
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
508
- <polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
509
- </svg>
510
- </button>
511
- )}
512
- </div>
513
- );
514
- }
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import {
5
+ PositionIcon,
6
+ TypographyIcon,
7
+ AnimationIcon,
8
+ SpacingIcon,
9
+ ContentIcon,
10
+ LinkIcon,
11
+ GridIcon,
12
+ StyleIcon,
13
+ BackgroundIcon,
14
+ } from "../../builder/editors/section-icons";
15
+
16
+ // ── Nav viewport type for responsive overrides ──
17
+ export type NavViewport = "desktop" | "tablet" | "phone";
18
+
19
+ // ── Shared field components for NavBuilder settings panel ──
20
+ // v2: Card-based sections, colored icons, builder-aligned styling
21
+
22
+ // ============================================
23
+ // Field wrapper
24
+ // ============================================
25
+
26
+ export function Field({ label, children }: { label: string; children: ReactNode }) {
27
+ return (
28
+ <div className="flex items-center gap-2.5 py-[5px]">
29
+ <label className="text-[11px] text-neutral-400 w-[56px] min-w-[56px] shrink-0">
30
+ {label}
31
+ </label>
32
+ <div className="flex-1 min-w-0">{children}</div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // ============================================
38
+ // Text input
39
+ // ============================================
40
+
41
+ export function TextInput({
42
+ value,
43
+ onChange,
44
+ placeholder,
45
+ type = "text",
46
+ }: {
47
+ value: string | number;
48
+ onChange: (value: string) => void;
49
+ placeholder?: string;
50
+ type?: "text" | "number";
51
+ }) {
52
+ return (
53
+ <input
54
+ type={type}
55
+ value={value}
56
+ onChange={(e) => onChange(e.target.value)}
57
+ placeholder={placeholder}
58
+ className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)] font-[inherit] placeholder:text-neutral-400"
59
+ />
60
+ );
61
+ }
62
+
63
+ // ============================================
64
+ // Select input
65
+ // ============================================
66
+
67
+ export function SelectInput({
68
+ value,
69
+ onChange,
70
+ options,
71
+ }: {
72
+ value: string;
73
+ onChange: (value: string) => void;
74
+ options: { value: string; label: string }[];
75
+ }) {
76
+ return (
77
+ <div className="relative">
78
+ <select
79
+ value={value}
80
+ onChange={(e) => onChange(e.target.value)}
81
+ className="w-full rounded-lg border border-transparent bg-white px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none appearance-none cursor-pointer pr-7 transition-all hover:bg-neutral-50 focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)] font-[inherit]"
82
+ >
83
+ {options.map((opt) => (
84
+ <option key={opt.value} value={opt.value}>
85
+ {opt.label}
86
+ </option>
87
+ ))}
88
+ </select>
89
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-neutral-400">
90
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
91
+ <path
92
+ d="M3 4.5l3 3 3-3"
93
+ stroke="currentColor"
94
+ strokeWidth="1.5"
95
+ strokeLinecap="round"
96
+ strokeLinejoin="round"
97
+ />
98
+ </svg>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ // ============================================
105
+ // Segmented control
106
+ // ============================================
107
+
108
+ export function SegmentedControl({
109
+ value,
110
+ onChange,
111
+ options,
112
+ }: {
113
+ value: string;
114
+ onChange: (value: string) => void;
115
+ options: { value: string; label: string | ReactNode }[];
116
+ }) {
117
+ return (
118
+ <div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0]">
119
+ {options.map((opt) => (
120
+ <button
121
+ key={String(opt.value)}
122
+ onClick={() => onChange(opt.value)}
123
+ className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center ${
124
+ value === opt.value
125
+ ? "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
126
+ : "text-neutral-400 hover:text-neutral-600"
127
+ }`}
128
+ >
129
+ {opt.label}
130
+ </button>
131
+ ))}
132
+ </div>
133
+ );
134
+ }
135
+
136
+ // ============================================
137
+ // Toggle switch
138
+ // ============================================
139
+
140
+ export function Toggle({
141
+ value,
142
+ onChange,
143
+ }: {
144
+ value: boolean;
145
+ onChange: (value: boolean) => void;
146
+ }) {
147
+ return (
148
+ <button
149
+ onClick={() => onChange(!value)}
150
+ className="w-9 h-5 rounded-full relative transition-all cursor-pointer"
151
+ style={{
152
+ background: value ? "#3580f9" : "#d4d4d4",
153
+ }}
154
+ >
155
+ <div
156
+ className="w-3.5 h-3.5 rounded-full bg-white absolute top-[2.5px] transition-all shadow-sm"
157
+ style={{ left: value ? 19 : 2 }}
158
+ />
159
+ </button>
160
+ );
161
+ }
162
+
163
+ // ============================================
164
+ // Range slider
165
+ // ============================================
166
+
167
+ export function RangeSlider({
168
+ value,
169
+ onChange,
170
+ min = 0,
171
+ max = 100,
172
+ suffix,
173
+ }: {
174
+ value: number;
175
+ onChange: (value: number) => void;
176
+ min?: number;
177
+ max?: number;
178
+ suffix?: string;
179
+ }) {
180
+ return (
181
+ <div className="flex items-center gap-2">
182
+ <input
183
+ type="range"
184
+ min={min}
185
+ max={max}
186
+ value={value}
187
+ onChange={(e) => onChange(Number(e.target.value))}
188
+ className="flex-1"
189
+ style={{ accentColor: "#3580f9" }}
190
+ />
191
+ <span className="text-[10px] text-neutral-400 w-9 text-right tabular-nums">
192
+ {value}{suffix || ""}
193
+ </span>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ // ============================================
199
+ // Card section — collapsible with colored icon
200
+ // ============================================
201
+
202
+ export function CardSection({
203
+ title,
204
+ icon,
205
+ iconBg,
206
+ defaultOpen = true,
207
+ children,
208
+ }: {
209
+ title: string;
210
+ icon: ReactNode;
211
+ iconBg: string;
212
+ defaultOpen?: boolean;
213
+ children: ReactNode;
214
+ }) {
215
+ const [open, setOpen] = useState(defaultOpen);
216
+
217
+ return (
218
+ <div
219
+ className="mx-2.5 my-1 rounded-[10px] bg-[#fafafa] border border-transparent transition-colors hover:border-[#f0f0f0]"
220
+ >
221
+ <button
222
+ onClick={() => setOpen(!open)}
223
+ className="flex items-center gap-2 w-full px-3 py-2.5 cursor-pointer select-none hover:bg-black/[0.01] rounded-[10px] transition-colors"
224
+ >
225
+ <div
226
+ className="w-[26px] h-[26px] rounded-[7px] flex items-center justify-center shrink-0"
227
+ style={{ background: iconBg }}
228
+ >
229
+ {icon}
230
+ </div>
231
+ <span className="text-[11px] font-semibold text-neutral-600 tracking-[0.2px]">
232
+ {title}
233
+ </span>
234
+ <span
235
+ className="ml-auto text-neutral-400 transition-transform"
236
+ style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
237
+ >
238
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
239
+ <path
240
+ d="M3 4.5l3 3 3-3"
241
+ stroke="currentColor"
242
+ strokeWidth="1.5"
243
+ strokeLinecap="round"
244
+ strokeLinejoin="round"
245
+ />
246
+ </svg>
247
+ </span>
248
+ </button>
249
+ {open && <div className="px-3 pb-3 pt-0.5">{children}</div>}
250
+ </div>
251
+ );
252
+ }
253
+
254
+ // ============================================
255
+ // Legacy Section — kept for NavItemSettings compatibility
256
+ // ============================================
257
+
258
+ export function Section({
259
+ title,
260
+ defaultOpen = true,
261
+ children,
262
+ }: {
263
+ title: string;
264
+ defaultOpen?: boolean;
265
+ children: ReactNode;
266
+ }) {
267
+ const [open, setOpen] = useState(defaultOpen);
268
+
269
+ return (
270
+ <div>
271
+ <button
272
+ onClick={() => setOpen(!open)}
273
+ className="flex items-center justify-between w-full py-2 border-t border-neutral-200 mt-1 text-[11px] font-semibold text-neutral-900 tracking-wide cursor-pointer uppercase"
274
+ >
275
+ {title}
276
+ <span
277
+ className="text-neutral-400 transition-transform"
278
+ style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
279
+ >
280
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
281
+ <path
282
+ d="M3 4.5l3 3 3-3"
283
+ stroke="currentColor"
284
+ strokeWidth="1.5"
285
+ strokeLinecap="round"
286
+ strokeLinejoin="round"
287
+ />
288
+ </svg>
289
+ </span>
290
+ </button>
291
+ {open && <div className="pb-2">{children}</div>}
292
+ </div>
293
+ );
294
+ }
295
+
296
+ // ============================================
297
+ // Thin divider line
298
+ // ============================================
299
+
300
+ export function Divider() {
301
+ return <div className="h-px bg-[#f0f0f0] my-1.5" />;
302
+ }
303
+
304
+ // ============================================
305
+ // Color input (hex) with swatch preview
306
+ // ============================================
307
+
308
+ export function ColorInput({
309
+ value,
310
+ onChange,
311
+ placeholder = "Transparent",
312
+ }: {
313
+ value: string;
314
+ onChange: (value: string) => void;
315
+ placeholder?: string;
316
+ }) {
317
+ return (
318
+ <div className="flex gap-1.5 items-center">
319
+ <div
320
+ className="w-7 h-7 rounded-lg border border-neutral-200 shrink-0 cursor-pointer"
321
+ style={{
322
+ background: value || "transparent",
323
+ backgroundImage: !value
324
+ ? "repeating-conic-gradient(#e5e5e5 0% 25%, transparent 0% 50%) 0 0 / 8px 8px"
325
+ : "none",
326
+ }}
327
+ />
328
+ <TextInput value={value} onChange={onChange} placeholder={placeholder} />
329
+ {value && (
330
+ <button
331
+ onClick={() => onChange("")}
332
+ className="text-neutral-400 hover:text-neutral-600 shrink-0"
333
+ title="Clear"
334
+ >
335
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
336
+ <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
337
+ </svg>
338
+ </button>
339
+ )}
340
+ </div>
341
+ );
342
+ }
343
+
344
+ // ============================================
345
+ // Card section icon presets — re-exported from centralized section-icons.tsx
346
+ // Session 163: Unified icon system. All icons now live in section-icons.tsx.
347
+ // ============================================
348
+ export {
349
+ PositionIcon,
350
+ TypographyIcon,
351
+ AnimationIcon,
352
+ SpacingIcon,
353
+ ContentIcon,
354
+ LinkIcon,
355
+ GridIcon,
356
+ StyleIcon,
357
+ BackgroundIcon,
358
+ };
359
+
360
+ // ============================================
361
+ // Viewport Switcher — segmented control for Desktop / Tablet / Phone
362
+ // ============================================
363
+
364
+ const viewportOptions: { value: NavViewport; icon: ReactNode; label: string }[] = [
365
+ {
366
+ value: "desktop",
367
+ label: "Desktop",
368
+ icon: (
369
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
370
+ <rect x="2" y="3" width="20" height="14" rx="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
371
+ </svg>
372
+ ),
373
+ },
374
+ {
375
+ value: "tablet",
376
+ label: "Tablet",
377
+ icon: (
378
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
379
+ <rect x="4" y="2" width="16" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
380
+ </svg>
381
+ ),
382
+ },
383
+ {
384
+ value: "phone",
385
+ label: "Phone",
386
+ icon: (
387
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
388
+ <rect x="5" y="2" width="14" height="20" rx="2" /><line x1="12" y1="18" x2="12" y2="18" />
389
+ </svg>
390
+ ),
391
+ },
392
+ ];
393
+
394
+ export function ViewportSwitcher({
395
+ value,
396
+ onChange,
397
+ }: {
398
+ value: NavViewport;
399
+ onChange: (v: NavViewport) => void;
400
+ }) {
401
+ return (
402
+ <div className="flex bg-white rounded-lg p-0.5 border border-[#f0f0f0] mb-2">
403
+ {viewportOptions.map((opt) => {
404
+ const isActive = value === opt.value;
405
+ const isNonDesktop = opt.value !== "desktop";
406
+ return (
407
+ <button
408
+ key={opt.value}
409
+ onClick={() => onChange(opt.value)}
410
+ title={opt.label}
411
+ className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
412
+ isActive
413
+ ? isNonDesktop
414
+ ? "text-[#3580f9] bg-blue-50 shadow-[0_1px_2px_rgba(53, 128, 249,0.08)]"
415
+ : "text-neutral-900 bg-neutral-100 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
416
+ : "text-neutral-400 hover:text-neutral-600"
417
+ }`}
418
+ >
419
+ {opt.icon}
420
+ </button>
421
+ );
422
+ })}
423
+ </div>
424
+ );
425
+ }
426
+
427
+ // ============================================
428
+ // Viewport Badge — shows which viewport is being edited
429
+ // ============================================
430
+
431
+ export function NavViewportBadge({ viewport }: { viewport: NavViewport }) {
432
+ if (viewport === "desktop") return null;
433
+
434
+ const color = viewport === "tablet" ? "#3580f9" : "#7c3aed";
435
+ const bgColor = viewport === "tablet" ? "rgba(53, 128, 249, 0.06)" : "rgba(124, 58, 237, 0.06)";
436
+ const label = viewport === "tablet" ? "Tablet" : "Phone";
437
+
438
+ return (
439
+ <div
440
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md mb-2 text-[10px]"
441
+ style={{ background: bgColor, color }}
442
+ >
443
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
444
+ <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
445
+ </svg>
446
+ <span>
447
+ Editing <strong>{label}</strong> overrides &middot; empty fields inherit desktop
448
+ </span>
449
+ </div>
450
+ );
451
+ }
452
+
453
+ // ============================================
454
+ // Responsive Field — wraps a field with inherited/overridden state
455
+ // ============================================
456
+
457
+ export function ResponsiveField({
458
+ label,
459
+ viewport,
460
+ isOverridden,
461
+ onReset,
462
+ children,
463
+ }: {
464
+ label: string;
465
+ viewport: NavViewport;
466
+ isOverridden: boolean;
467
+ onReset?: () => void;
468
+ children: ReactNode;
469
+ }) {
470
+ // Desktop mode — render as a normal field
471
+ if (viewport === "desktop") {
472
+ return <Field label={label}>{children}</Field>;
473
+ }
474
+
475
+ return (
476
+ <div className="flex items-center gap-2.5 py-[5px]">
477
+ <div className="w-[56px] min-w-[56px] shrink-0">
478
+ <label
479
+ className={`text-[11px] block ${
480
+ isOverridden ? "text-[#3580f9] font-medium" : "text-neutral-400"
481
+ }`}
482
+ >
483
+ {label}
484
+ </label>
485
+ {isOverridden ? (
486
+ <span className="text-[8px] text-[#3580f9]/60 uppercase tracking-[0.5px]">
487
+ override
488
+ </span>
489
+ ) : (
490
+ <span className="text-[8px] text-neutral-300 uppercase tracking-[0.5px]">
491
+ inherited
492
+ </span>
493
+ )}
494
+ </div>
495
+ <div
496
+ className="flex-1 min-w-0 transition-opacity"
497
+ style={{ opacity: isOverridden ? 1 : 0.5 }}
498
+ >
499
+ {children}
500
+ </div>
501
+ {isOverridden && onReset && (
502
+ <button
503
+ onClick={onReset}
504
+ className="shrink-0 text-[#3580f9]/60 hover:text-[#3580f9] transition-colors"
505
+ title="Reset to desktop value"
506
+ >
507
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
508
+ <polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
509
+ </svg>
510
+ </button>
511
+ )}
512
+ </div>
513
+ );
514
+ }