@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
@@ -0,0 +1,621 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MarqueeBlockEditor — Settings editor for the marqueeBlock.
5
+ *
6
+ * Sections:
7
+ * - Content (items list with add / reorder / remove, inline edit)
8
+ * - Motion (direction, speed, pause on hover)
9
+ * - Typography (font size, weight, color, text style, letter spacing, transform)
10
+ * - Layout (gap, row height, vertical padding, background color)
11
+ *
12
+ * Items reorder uses native HTML5 drag & drop — no dnd-kit dependency. Kept
13
+ * intentionally lightweight because the item list rarely exceeds ~20 entries.
14
+ *
15
+ * Typography controls apply to both text and separator items — separators
16
+ * inherit everything except the per-item character.
17
+ */
18
+
19
+ import React, { useCallback, useState } from "react";
20
+ import { useBuilderStore } from "../../../lib/builder/store";
21
+ import type {
22
+ MarqueeBlock,
23
+ MarqueeItem,
24
+ MarqueeTextItem,
25
+ MarqueeImageItem,
26
+ MarqueeSeparatorItem,
27
+ MarqueeFontSize,
28
+ } from "../../../lib/sanity/types";
29
+ import {
30
+ SettingsField,
31
+ SettingsSection,
32
+ StyledCheckbox,
33
+ } from "./shared";
34
+ import {
35
+ ContentIcon,
36
+ AnimationIcon,
37
+ TypographyIcon,
38
+ LayoutIcon,
39
+ } from "./section-icons";
40
+
41
+ // ============================================
42
+ // Constants
43
+ // ============================================
44
+
45
+ const DIRECTION_OPTIONS = [
46
+ { value: "left", label: "← Left" },
47
+ { value: "right", label: "Right →" },
48
+ ] as const;
49
+
50
+ const FONT_SIZE_OPTIONS: { value: MarqueeFontSize; label: string }[] = [
51
+ { value: "s", label: "S" },
52
+ { value: "base", label: "Base" },
53
+ { value: "l", label: "L" },
54
+ { value: "xl", label: "XL" },
55
+ { value: "2xl", label: "2XL" },
56
+ { value: "3xl", label: "3XL" },
57
+ { value: "4xl", label: "4XL" },
58
+ { value: "5xl", label: "5XL" },
59
+ { value: "6xl", label: "6XL" },
60
+ ];
61
+
62
+ const FONT_WEIGHT_OPTIONS = [
63
+ { value: "400", label: "Normal" },
64
+ { value: "500", label: "Medium" },
65
+ { value: "700", label: "Bold" },
66
+ { value: "900", label: "Black" },
67
+ ] as const;
68
+
69
+ const TEXT_STYLE_OPTIONS = [
70
+ { value: "solid", label: "Solid" },
71
+ { value: "outline", label: "Outline" },
72
+ { value: "italic-outline", label: "Italic outline" },
73
+ ] as const;
74
+
75
+ const TEXT_TRANSFORM_OPTIONS = [
76
+ { value: "none", label: "Aa" },
77
+ { value: "uppercase", label: "AA" },
78
+ { value: "lowercase", label: "aa" },
79
+ ] as const;
80
+
81
+ const ITEM_TYPE_LABEL: Record<MarqueeItem["_type"], string> = {
82
+ marqueeText: "Text",
83
+ marqueeImage: "Image",
84
+ marqueeSeparator: "Separator",
85
+ };
86
+
87
+ // ============================================
88
+ // Shared mini-components
89
+ // ============================================
90
+
91
+ function SegmentedControl<T extends string>({
92
+ options,
93
+ value,
94
+ onChange,
95
+ }: {
96
+ options: readonly { value: T; label: string }[];
97
+ value: T;
98
+ onChange: (v: T) => void;
99
+ }) {
100
+ return (
101
+ <div className="flex gap-1">
102
+ {options.map((opt) => {
103
+ const active = value === opt.value;
104
+ return (
105
+ <button
106
+ key={opt.value}
107
+ type="button"
108
+ onClick={() => onChange(opt.value)}
109
+ className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
110
+ active
111
+ ? "bg-[#3580f9] text-white"
112
+ : "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
113
+ }`}
114
+ >
115
+ {opt.label}
116
+ </button>
117
+ );
118
+ })}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function RangeSlider({
124
+ value,
125
+ onChange,
126
+ min,
127
+ max,
128
+ step = 1,
129
+ suffix = "",
130
+ decimals = 0,
131
+ }: {
132
+ value: number;
133
+ onChange: (v: number) => void;
134
+ min: number;
135
+ max: number;
136
+ step?: number;
137
+ suffix?: string;
138
+ decimals?: number;
139
+ }) {
140
+ return (
141
+ <div className="flex items-center gap-2">
142
+ <input
143
+ type="range"
144
+ min={min}
145
+ max={max}
146
+ step={step}
147
+ value={value}
148
+ onChange={(e) => onChange(Number(e.target.value))}
149
+ className="flex-1 h-1 accent-[#3580f9] cursor-pointer"
150
+ />
151
+ <span className="text-[11px] text-neutral-500 w-14 text-right tabular-nums shrink-0">
152
+ {value.toFixed(decimals)}{suffix}
153
+ </span>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ function Dropdown<T extends string>({
159
+ options,
160
+ value,
161
+ onChange,
162
+ }: {
163
+ options: readonly { value: T; label: string }[];
164
+ value: T;
165
+ onChange: (v: T) => void;
166
+ }) {
167
+ return (
168
+ <select
169
+ value={value}
170
+ onChange={(e) => onChange(e.target.value as T)}
171
+ className="w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]"
172
+ >
173
+ {options.map((opt) => (
174
+ <option key={opt.value} value={opt.value}>
175
+ {opt.label}
176
+ </option>
177
+ ))}
178
+ </select>
179
+ );
180
+ }
181
+
182
+ function TextInput({
183
+ value,
184
+ onChange,
185
+ placeholder,
186
+ }: {
187
+ value: string;
188
+ onChange: (v: string) => void;
189
+ placeholder?: string;
190
+ }) {
191
+ return (
192
+ <input
193
+ type="text"
194
+ value={value}
195
+ onChange={(e) => onChange(e.target.value)}
196
+ placeholder={placeholder}
197
+ className="w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]"
198
+ />
199
+ );
200
+ }
201
+
202
+ function ColorInput({
203
+ value,
204
+ onChange,
205
+ }: {
206
+ value: string;
207
+ onChange: (v: string) => void;
208
+ }) {
209
+ return (
210
+ <div className="flex gap-2 items-center">
211
+ <input
212
+ type="color"
213
+ value={value || "#000000"}
214
+ onChange={(e) => onChange(e.target.value)}
215
+ className="w-8 h-8 rounded cursor-pointer border border-neutral-200 bg-transparent p-0 shrink-0"
216
+ />
217
+ <input
218
+ type="text"
219
+ value={value}
220
+ onChange={(e) => onChange(e.target.value)}
221
+ placeholder="#000000"
222
+ className="flex-1 rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none hover:bg-[#efefef] focus:bg-white focus:border-[#3580f9]"
223
+ />
224
+ </div>
225
+ );
226
+ }
227
+
228
+ // ============================================
229
+ // Item row — inline editor per item
230
+ // ============================================
231
+
232
+ interface ItemRowProps {
233
+ item: MarqueeItem;
234
+ index: number;
235
+ isDragging: boolean;
236
+ isDragOver: boolean;
237
+ onUpdate: (updates: Partial<MarqueeItem>) => void;
238
+ onRemove: () => void;
239
+ onDragStart: () => void;
240
+ onDragOver: (e: React.DragEvent) => void;
241
+ onDrop: () => void;
242
+ onDragEnd: () => void;
243
+ }
244
+
245
+ function ItemRow({
246
+ item,
247
+ index,
248
+ isDragging,
249
+ isDragOver,
250
+ onUpdate,
251
+ onRemove,
252
+ onDragStart,
253
+ onDragOver,
254
+ onDrop,
255
+ onDragEnd,
256
+ }: ItemRowProps) {
257
+ return (
258
+ <div
259
+ draggable
260
+ onDragStart={onDragStart}
261
+ onDragOver={onDragOver}
262
+ onDrop={onDrop}
263
+ onDragEnd={onDragEnd}
264
+ className={`rounded-lg border p-2 transition-colors ${
265
+ isDragging
266
+ ? "opacity-40 border-[#7500d5]"
267
+ : isDragOver
268
+ ? "border-[#7500d5] bg-[#7500d5]/5"
269
+ : "border-neutral-200 bg-white hover:border-neutral-300"
270
+ }`}
271
+ >
272
+ <div className="flex items-center gap-2 mb-1.5">
273
+ <span
274
+ className="text-neutral-300 cursor-grab active:cursor-grabbing select-none"
275
+ title="Drag to reorder"
276
+ aria-label="Drag handle"
277
+ >
278
+ ⋮⋮
279
+ </span>
280
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-neutral-400">
281
+ {ITEM_TYPE_LABEL[item._type]} · #{index + 1}
282
+ </span>
283
+ <button
284
+ type="button"
285
+ onClick={onRemove}
286
+ className="ml-auto text-[11px] text-neutral-400 hover:text-red-500 transition-colors"
287
+ aria-label="Remove item"
288
+ >
289
+
290
+ </button>
291
+ </div>
292
+
293
+ {item._type === "marqueeText" && (
294
+ <TextInput
295
+ value={(item as MarqueeTextItem).text}
296
+ onChange={(v) => onUpdate({ text: v } as Partial<MarqueeTextItem>)}
297
+ placeholder="Enter text…"
298
+ />
299
+ )}
300
+
301
+ {item._type === "marqueeImage" && (
302
+ <div className="space-y-1.5">
303
+ <TextInput
304
+ value={(item as MarqueeImageItem).asset_path}
305
+ onChange={(v) =>
306
+ onUpdate({ asset_path: v } as Partial<MarqueeImageItem>)
307
+ }
308
+ placeholder="Asset path (e.g. logos/client.svg)"
309
+ />
310
+ <TextInput
311
+ value={(item as MarqueeImageItem).alt ?? ""}
312
+ onChange={(v) =>
313
+ onUpdate({ alt: v } as Partial<MarqueeImageItem>)
314
+ }
315
+ placeholder="Alt text (accessibility)"
316
+ />
317
+ </div>
318
+ )}
319
+
320
+ {item._type === "marqueeSeparator" && (
321
+ <TextInput
322
+ value={(item as MarqueeSeparatorItem).character}
323
+ onChange={(v) =>
324
+ onUpdate({
325
+ character: v.slice(0, 4),
326
+ } as Partial<MarqueeSeparatorItem>)
327
+ }
328
+ placeholder="• · — / ▸ ★"
329
+ />
330
+ )}
331
+ </div>
332
+ );
333
+ }
334
+
335
+ // ============================================
336
+ // Main editor
337
+ // ============================================
338
+
339
+ interface MarqueeBlockEditorProps {
340
+ block: MarqueeBlock;
341
+ }
342
+
343
+ /** Small random _key generator — matches the format used elsewhere in the builder. */
344
+ function randomKey(): string {
345
+ return Math.random().toString(36).slice(2, 12);
346
+ }
347
+
348
+ export default function MarqueeBlockEditor({ block }: MarqueeBlockEditorProps) {
349
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
350
+
351
+ const update = useCallback(
352
+ (updates: Partial<MarqueeBlock>) => {
353
+ updateBlock(block._key, updates as Partial<MarqueeBlock>);
354
+ },
355
+ [updateBlock, block._key],
356
+ );
357
+
358
+ const items = block.items ?? [];
359
+
360
+ // ─── Item mutations ──────────────────────────────
361
+ const addItem = useCallback(
362
+ (type: MarqueeItem["_type"]) => {
363
+ const base = { _key: randomKey(), _type: type };
364
+ let newItem: MarqueeItem;
365
+ if (type === "marqueeText") {
366
+ newItem = { ...base, _type: "marqueeText", text: "New text" };
367
+ } else if (type === "marqueeImage") {
368
+ newItem = {
369
+ ...base,
370
+ _type: "marqueeImage",
371
+ asset_path: "",
372
+ alt: "",
373
+ border_radius: 0,
374
+ };
375
+ } else {
376
+ newItem = { ...base, _type: "marqueeSeparator", character: "•" };
377
+ }
378
+ update({ items: [...items, newItem] });
379
+ },
380
+ [items, update],
381
+ );
382
+
383
+ const updateItem = useCallback(
384
+ (index: number, updates: Partial<MarqueeItem>) => {
385
+ const next = items.map((it, i) =>
386
+ i === index ? ({ ...it, ...updates } as MarqueeItem) : it,
387
+ );
388
+ update({ items: next });
389
+ },
390
+ [items, update],
391
+ );
392
+
393
+ const removeItem = useCallback(
394
+ (index: number) => {
395
+ update({ items: items.filter((_, i) => i !== index) });
396
+ },
397
+ [items, update],
398
+ );
399
+
400
+ // ─── Drag-reorder state ──────────────────────────
401
+ const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
402
+ const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
403
+
404
+ const handleDragStart = (index: number) => {
405
+ setDraggingIndex(index);
406
+ };
407
+ const handleDragOver = (e: React.DragEvent, index: number) => {
408
+ e.preventDefault();
409
+ setDragOverIndex(index);
410
+ };
411
+ const handleDrop = (targetIndex: number) => {
412
+ if (draggingIndex === null || draggingIndex === targetIndex) return;
413
+ const next = [...items];
414
+ const [moved] = next.splice(draggingIndex, 1);
415
+ next.splice(targetIndex, 0, moved);
416
+ update({ items: next });
417
+ };
418
+ const handleDragEnd = () => {
419
+ setDraggingIndex(null);
420
+ setDragOverIndex(null);
421
+ };
422
+
423
+ // ─── Resolved values (with defaults) ────────────
424
+ const direction = block.direction ?? "left";
425
+ const speed = block.speed ?? 60;
426
+ const pauseOnHover = block.pause_on_hover !== false;
427
+
428
+ const fontSize = block.font_size ?? "3xl";
429
+ const fontWeight = block.font_weight ?? "700";
430
+ const color = block.color ?? "#111111";
431
+ const textStyle = block.text_style ?? "solid";
432
+ const letterSpacing = block.letter_spacing ?? 0;
433
+ const textTransform = block.text_transform ?? "uppercase";
434
+
435
+ const gap = block.gap ?? 48;
436
+ const rowHeight = block.row_height ?? 120;
437
+ const paddingY = block.padding_y ?? 16;
438
+ const backgroundColor = block.background_color ?? "";
439
+
440
+ return (
441
+ <>
442
+ {/* ─── Content ─────────────────────────────────── */}
443
+ <SettingsSection title="Content" defaultOpen icon={<ContentIcon />}>
444
+ <div className="space-y-1.5 mb-2">
445
+ {items.length === 0 && (
446
+ <p className="text-[11px] text-neutral-400 italic py-2 text-center">
447
+ No items yet. Add text, images or separators below.
448
+ </p>
449
+ )}
450
+ {items.map((item, i) => (
451
+ <ItemRow
452
+ key={item._key}
453
+ item={item}
454
+ index={i}
455
+ isDragging={draggingIndex === i}
456
+ isDragOver={dragOverIndex === i && draggingIndex !== i}
457
+ onUpdate={(updates) => updateItem(i, updates)}
458
+ onRemove={() => removeItem(i)}
459
+ onDragStart={() => handleDragStart(i)}
460
+ onDragOver={(e) => handleDragOver(e, i)}
461
+ onDrop={() => handleDrop(i)}
462
+ onDragEnd={handleDragEnd}
463
+ />
464
+ ))}
465
+ </div>
466
+
467
+ {/* Add-item buttons */}
468
+ <div className="grid grid-cols-3 gap-1.5">
469
+ <button
470
+ type="button"
471
+ onClick={() => addItem("marqueeText")}
472
+ className="px-2 py-1.5 text-[11px] font-medium rounded-lg bg-neutral-100 text-neutral-700 hover:bg-[#3580f9]/10 hover:text-[#3580f9] transition-colors"
473
+ >
474
+ + Text
475
+ </button>
476
+ <button
477
+ type="button"
478
+ onClick={() => addItem("marqueeImage")}
479
+ className="px-2 py-1.5 text-[11px] font-medium rounded-lg bg-neutral-100 text-neutral-700 hover:bg-[#3580f9]/10 hover:text-[#3580f9] transition-colors"
480
+ >
481
+ + Image
482
+ </button>
483
+ <button
484
+ type="button"
485
+ onClick={() => addItem("marqueeSeparator")}
486
+ className="px-2 py-1.5 text-[11px] font-medium rounded-lg bg-neutral-100 text-neutral-700 hover:bg-[#3580f9]/10 hover:text-[#3580f9] transition-colors"
487
+ >
488
+ + Sep.
489
+ </button>
490
+ </div>
491
+ </SettingsSection>
492
+
493
+ {/* ─── Motion ───────────────────────────────────── */}
494
+ <SettingsSection title="Motion" defaultOpen icon={<AnimationIcon />}>
495
+ <SettingsField label="Direction">
496
+ <SegmentedControl
497
+ options={DIRECTION_OPTIONS}
498
+ value={direction}
499
+ onChange={(v) => update({ direction: v })}
500
+ />
501
+ </SettingsField>
502
+
503
+ <SettingsField label="Speed">
504
+ <RangeSlider
505
+ value={speed}
506
+ onChange={(v) => update({ speed: Math.round(v) })}
507
+ min={5}
508
+ max={600}
509
+ step={5}
510
+ suffix=" px/s"
511
+ />
512
+ </SettingsField>
513
+
514
+ <SettingsField label="Hover">
515
+ <StyledCheckbox
516
+ checked={pauseOnHover}
517
+ onChange={(checked) => update({ pause_on_hover: checked })}
518
+ label="Pause on hover"
519
+ />
520
+ </SettingsField>
521
+ </SettingsSection>
522
+
523
+ {/* ─── Typography ──────────────────────────────── */}
524
+ <SettingsSection title="Typography" defaultOpen icon={<TypographyIcon />}>
525
+ <SettingsField label="Size">
526
+ <Dropdown
527
+ options={FONT_SIZE_OPTIONS}
528
+ value={fontSize}
529
+ onChange={(v) => update({ font_size: v })}
530
+ />
531
+ </SettingsField>
532
+
533
+ <SettingsField label="Weight">
534
+ <SegmentedControl
535
+ options={FONT_WEIGHT_OPTIONS}
536
+ value={fontWeight}
537
+ onChange={(v) => update({ font_weight: v })}
538
+ />
539
+ </SettingsField>
540
+
541
+ <SettingsField label="Color">
542
+ <ColorInput
543
+ value={color}
544
+ onChange={(v) => update({ color: v })}
545
+ />
546
+ </SettingsField>
547
+
548
+ <SettingsField label="Style">
549
+ <SegmentedControl
550
+ options={TEXT_STYLE_OPTIONS}
551
+ value={textStyle}
552
+ onChange={(v) => update({ text_style: v })}
553
+ />
554
+ </SettingsField>
555
+
556
+ <SettingsField label="Tracking">
557
+ <RangeSlider
558
+ value={letterSpacing}
559
+ onChange={(v) => update({ letter_spacing: v })}
560
+ min={-0.1}
561
+ max={0.5}
562
+ step={0.01}
563
+ decimals={2}
564
+ suffix="em"
565
+ />
566
+ </SettingsField>
567
+
568
+ <SettingsField label="Case">
569
+ <SegmentedControl
570
+ options={TEXT_TRANSFORM_OPTIONS}
571
+ value={textTransform}
572
+ onChange={(v) => update({ text_transform: v })}
573
+ />
574
+ </SettingsField>
575
+ </SettingsSection>
576
+
577
+ {/* ─── Layout ──────────────────────────────────── */}
578
+ <SettingsSection title="Layout" defaultOpen icon={<LayoutIcon />}>
579
+ <SettingsField label="Gap">
580
+ <RangeSlider
581
+ value={gap}
582
+ onChange={(v) => update({ gap: Math.round(v) })}
583
+ min={0}
584
+ max={200}
585
+ step={2}
586
+ suffix="px"
587
+ />
588
+ </SettingsField>
589
+
590
+ <SettingsField label="Row height">
591
+ <RangeSlider
592
+ value={rowHeight}
593
+ onChange={(v) => update({ row_height: Math.round(v) })}
594
+ min={24}
595
+ max={600}
596
+ step={4}
597
+ suffix="px"
598
+ />
599
+ </SettingsField>
600
+
601
+ <SettingsField label="Padding Y">
602
+ <RangeSlider
603
+ value={paddingY}
604
+ onChange={(v) => update({ padding_y: Math.round(v) })}
605
+ min={0}
606
+ max={200}
607
+ step={2}
608
+ suffix="px"
609
+ />
610
+ </SettingsField>
611
+
612
+ <SettingsField label="Background">
613
+ <ColorInput
614
+ value={backgroundColor}
615
+ onChange={(v) => update({ background_color: v })}
616
+ />
617
+ </SettingsField>
618
+ </SettingsSection>
619
+ </>
620
+ );
621
+ }