@morphika/andami 0.5.1 → 0.5.3

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