@morphika/andami 0.5.1 → 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 (117) hide show
  1. package/app/admin/assets/page.tsx +6 -6
  2. package/app/admin/database/page.tsx +302 -302
  3. package/app/admin/error.tsx +53 -53
  4. package/app/admin/layout.tsx +320 -320
  5. package/app/admin/navigation/page.tsx +255 -255
  6. package/app/admin/pages/[slug]/page.tsx +6 -6
  7. package/app/admin/pages/page.tsx +11 -11
  8. package/app/admin/projects/page.tsx +14 -14
  9. package/app/admin/setup/page.tsx +1 -1
  10. package/app/admin/styles/page.tsx +1 -1
  11. package/components/admin/MetadataEditor.tsx +6 -6
  12. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  13. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  14. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  15. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  16. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  17. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  18. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  19. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  20. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  22. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  23. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  24. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  25. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  26. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  27. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  28. package/components/admin/styles/ColorsEditor.tsx +2 -2
  29. package/components/admin/styles/FontsEditor.tsx +6 -6
  30. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  31. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  32. package/components/admin/styles/TypographyEditor.tsx +6 -6
  33. package/components/admin/styles/shared.tsx +68 -68
  34. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  35. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  36. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  37. package/components/builder/BlockCardIcons.tsx +316 -316
  38. package/components/builder/BlockTypePicker.tsx +1 -1
  39. package/components/builder/BubbleIcons.tsx +90 -0
  40. package/components/builder/BuilderCanvas.tsx +2 -0
  41. package/components/builder/CanvasMinimap.tsx +2 -2
  42. package/components/builder/CoverSectionCanvas.tsx +363 -363
  43. package/components/builder/DeviceFrame.tsx +1 -1
  44. package/components/builder/DndWrapper.tsx +3 -3
  45. package/components/builder/InsertionLines.tsx +1 -1
  46. package/components/builder/SectionCardIcons.tsx +421 -320
  47. package/components/builder/SectionEditorBar.tsx +1 -1
  48. package/components/builder/SectionTypePicker.tsx +4 -4
  49. package/components/builder/SectionV2Canvas.tsx +1 -1
  50. package/components/builder/SectionV2Column.tsx +69 -67
  51. package/components/builder/SortableBlock.tsx +93 -73
  52. package/components/builder/SortableRow.tsx +27 -26
  53. package/components/builder/VirtualAssetGrid.tsx +2 -2
  54. package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
  55. package/components/builder/blockStyles.tsx +192 -185
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  57. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  58. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  59. package/components/builder/color-picker/HueSlider.tsx +124 -124
  60. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  61. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  62. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  63. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  64. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  65. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  66. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  67. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  68. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  69. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  70. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  71. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  72. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  73. package/components/builder/editors/StaggerSettings.tsx +109 -109
  74. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  75. package/components/builder/editors/TextStylePicker.tsx +1 -1
  76. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  77. package/components/builder/editors/index.ts +11 -10
  78. package/components/builder/editors/shared.tsx +6 -6
  79. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  80. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  81. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  82. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  83. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  84. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  85. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  86. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  87. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  88. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  89. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  90. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  91. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  92. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  93. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  94. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  95. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  96. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  97. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  98. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  99. package/lib/animation/enter-types.ts +1 -0
  100. package/lib/animation/hover-effect-presets.ts +210 -210
  101. package/lib/animation/hover-effect-types.ts +1 -0
  102. package/lib/builder/block-registrations.ts +468 -417
  103. package/lib/builder/constants.ts +111 -111
  104. package/lib/builder/store-sections.ts +2 -2
  105. package/lib/builder/types-slices.ts +414 -414
  106. package/lib/builder/types.ts +4 -1
  107. package/lib/config/index.ts +27 -27
  108. package/lib/sanity/types.ts +98 -1
  109. package/lib/version.ts +1 -1
  110. package/package.json +1 -1
  111. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  112. package/sanity/schemas/blocks/index.ts +12 -11
  113. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  114. package/sanity/schemas/index.ts +120 -117
  115. package/styles/admin.css +85 -85
  116. package/styles/animations.css +237 -237
  117. 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
+ }