@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,443 +1,443 @@
1
- "use client";
2
-
3
- /**
4
- * ProjectCarouselBlockEditor — Settings editor for the projectCarouselBlock.
5
- *
6
- * Sections:
7
- * - Source (mode, number of projects, exclude-current toggle)
8
- * - Layout (cards per view — desktop / tablet / phone — and gap)
9
- * - Card Display (aspect ratio, show title / subtitle, border radius,
10
- * hover effect)
11
- * - Video (video mode)
12
- * - Controls (show arrows / dots, snap scrolling)
13
- * - Card Entrance Animation (enabled, preset, stagger, duration)
14
- *
15
- * Intentionally independent from ProjectGridEditor — they follow the same
16
- * visual style but share no code so changes to one never break the other.
17
- */
18
-
19
- import React, { useCallback } from "react";
20
- import { useBuilderStore } from "../../../lib/builder/store";
21
- import type { ProjectCarouselBlock } from "../../../lib/sanity/types";
22
- import {
23
- SettingsField,
24
- SettingsSection,
25
- StyledCheckbox,
26
- } from "./shared";
27
- import {
28
- SourceIcon,
29
- LayoutIcon,
30
- AppearanceIcon,
31
- VideoIcon,
32
- NavigationIcon,
33
- AnimationIcon,
34
- } from "./section-icons";
35
-
36
- // ============================================
37
- // Constants
38
- // ============================================
39
-
40
- const SOURCE_OPTIONS = [
41
- { value: "auto_latest", label: "Latest" },
42
- { value: "auto_random", label: "Random" },
43
- ] as const;
44
-
45
- const ASPECT_RATIO_OPTIONS = [
46
- { value: "16/9", label: "16:9" },
47
- { value: "4/3", label: "4:3" },
48
- { value: "1/1", label: "1:1" },
49
- { value: "3/4", label: "3:4" },
50
- { value: "9/16", label: "9:16" },
51
- ] as const;
52
-
53
- const HOVER_EFFECT_OPTIONS = [
54
- { value: "scale", label: "Scale" },
55
- { value: "none", label: "None" },
56
- ] as const;
57
-
58
- const VIDEO_MODE_OPTIONS = [
59
- { value: "off", label: "Off" },
60
- { value: "hover", label: "Hover" },
61
- { value: "autoloop", label: "Auto" },
62
- ] as const;
63
-
64
- const ANIMATION_PRESET_OPTIONS = [
65
- { value: "fade", label: "Fade" },
66
- { value: "slide-up", label: "Slide Up" },
67
- { value: "scale", label: "Scale" },
68
- ] as const;
69
-
70
- // ============================================
71
- // Shared mini-components (inline copies — not imported from ProjectGridEditor
72
- // to keep the two blocks decoupled, per user's explicit request).
73
- // ============================================
74
-
75
- function SegmentedControl<T extends string>({
76
- options,
77
- value,
78
- onChange,
79
- }: {
80
- options: readonly { value: T; label: string }[];
81
- value: T;
82
- onChange: (v: T) => void;
83
- }) {
84
- return (
85
- <div className="flex gap-1">
86
- {options.map((opt) => {
87
- const active = value === opt.value;
88
- return (
89
- <button
90
- key={opt.value}
91
- type="button"
92
- onClick={() => onChange(opt.value)}
93
- className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
94
- active
95
- ? "bg-[#4794e2] text-white"
96
- : "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
97
- }`}
98
- >
99
- {opt.label}
100
- </button>
101
- );
102
- })}
103
- </div>
104
- );
105
- }
106
-
107
- function RangeSlider({
108
- value,
109
- onChange,
110
- min,
111
- max,
112
- step = 1,
113
- suffix = "",
114
- decimals = 0,
115
- }: {
116
- value: number;
117
- onChange: (v: number) => void;
118
- min: number;
119
- max: number;
120
- step?: number;
121
- suffix?: string;
122
- decimals?: number;
123
- }) {
124
- return (
125
- <div className="flex items-center gap-2">
126
- <input
127
- type="range"
128
- min={min}
129
- max={max}
130
- step={step}
131
- value={value}
132
- onChange={(e) => onChange(Number(e.target.value))}
133
- className="flex-1 h-1 accent-[#4794e2] cursor-pointer"
134
- />
135
- <span className="text-[11px] text-neutral-500 w-10 text-right tabular-nums shrink-0">
136
- {value.toFixed(decimals)}{suffix}
137
- </span>
138
- </div>
139
- );
140
- }
141
-
142
- function Dropdown<T extends string>({
143
- options,
144
- value,
145
- onChange,
146
- }: {
147
- options: readonly { value: T; label: string }[];
148
- value: T;
149
- onChange: (v: T) => void;
150
- }) {
151
- return (
152
- <select
153
- value={value}
154
- onChange={(e) => onChange(e.target.value as T)}
155
- 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-[#4794e2] focus:shadow-[0_0_0_3px_rgba(71,148,226,0.06)]"
156
- >
157
- {options.map((opt) => (
158
- <option key={opt.value} value={opt.value}>
159
- {opt.label}
160
- </option>
161
- ))}
162
- </select>
163
- );
164
- }
165
-
166
- // ============================================
167
- // Main editor
168
- // ============================================
169
-
170
- interface ProjectCarouselBlockEditorProps {
171
- block: ProjectCarouselBlock;
172
- }
173
-
174
- export default function ProjectCarouselBlockEditor({
175
- block,
176
- }: ProjectCarouselBlockEditorProps) {
177
- const updateBlock = useBuilderStore((s) => s.updateBlock);
178
-
179
- const update = useCallback(
180
- (updates: Partial<ProjectCarouselBlock>) => {
181
- updateBlock(block._key, updates as Partial<ProjectCarouselBlock>);
182
- },
183
- [updateBlock, block._key],
184
- );
185
-
186
- const updateCardEntrance = useCallback(
187
- (updates: Partial<NonNullable<ProjectCarouselBlock["card_entrance"]>>) => {
188
- update({
189
- card_entrance: {
190
- enabled: false,
191
- preset: "slide-up",
192
- stagger_delay: 80,
193
- duration: 500,
194
- ...block.card_entrance,
195
- ...updates,
196
- },
197
- });
198
- },
199
- [update, block.card_entrance],
200
- );
201
-
202
- // ─── Resolved values (with defaults) ───
203
- const sourceMode = block.source_mode ?? "auto_latest";
204
- const maxProjects = block.max_projects ?? 8;
205
- const excludeCurrent = block.exclude_current !== false;
206
-
207
- const cpvDesktop = block.cards_per_view_desktop ?? 3.5;
208
- const cpvTablet = block.cards_per_view_tablet ?? 2.2;
209
- const cpvPhone = block.cards_per_view_phone ?? 1.2;
210
- const gap = block.gap ?? 16;
211
-
212
- const aspectRatio = block.aspect_ratio ?? "4/3";
213
- const showTitle = block.show_title !== false;
214
- const showSubtitle = block.show_subtitle === true;
215
- const borderRadius = block.border_radius ?? 0;
216
- const hoverEffect = block.hover_effect ?? "scale";
217
-
218
- const videoMode = block.video_mode ?? "off";
219
-
220
- const showArrows = block.show_arrows !== false;
221
- const showDots = block.show_dots === true;
222
- const snapScroll = block.snap_scroll !== false;
223
-
224
- const entranceEnabled = block.card_entrance?.enabled === true;
225
- const entrancePreset = block.card_entrance?.preset ?? "slide-up";
226
- const entranceStagger = block.card_entrance?.stagger_delay ?? 80;
227
- const entranceDuration = block.card_entrance?.duration ?? 500;
228
-
229
- return (
230
- <>
231
- {/* ─── Source ──────────────────────────────────────────── */}
232
- <SettingsSection title="Source" defaultOpen icon={<SourceIcon />}>
233
- <SettingsField label="Mode">
234
- <SegmentedControl
235
- options={SOURCE_OPTIONS}
236
- value={sourceMode}
237
- onChange={(v) => update({ source_mode: v })}
238
- />
239
- </SettingsField>
240
-
241
- <SettingsField label="Projects">
242
- <RangeSlider
243
- value={maxProjects}
244
- onChange={(v) => update({ max_projects: Math.round(v) })}
245
- min={2}
246
- max={20}
247
- step={1}
248
- />
249
- </SettingsField>
250
-
251
- <SettingsField label="Exclude current">
252
- <StyledCheckbox
253
- checked={excludeCurrent}
254
- onChange={(checked) => update({ exclude_current: checked })}
255
- label="Hide the project currently being viewed"
256
- />
257
- </SettingsField>
258
- </SettingsSection>
259
-
260
- {/* ─── Layout ──────────────────────────────────────────── */}
261
- <SettingsSection title="Layout" defaultOpen icon={<LayoutIcon />}>
262
- <SettingsField label="Desktop">
263
- <RangeSlider
264
- value={cpvDesktop}
265
- onChange={(v) => update({ cards_per_view_desktop: v })}
266
- min={1}
267
- max={6}
268
- step={0.5}
269
- decimals={1}
270
- suffix=" cards"
271
- />
272
- </SettingsField>
273
-
274
- <SettingsField label="Tablet">
275
- <RangeSlider
276
- value={cpvTablet}
277
- onChange={(v) => update({ cards_per_view_tablet: v })}
278
- min={1}
279
- max={4}
280
- step={0.5}
281
- decimals={1}
282
- suffix=" cards"
283
- />
284
- </SettingsField>
285
-
286
- <SettingsField label="Phone">
287
- <RangeSlider
288
- value={cpvPhone}
289
- onChange={(v) => update({ cards_per_view_phone: v })}
290
- min={1}
291
- max={3}
292
- step={0.1}
293
- decimals={1}
294
- suffix=" cards"
295
- />
296
- </SettingsField>
297
-
298
- <SettingsField label="Gap">
299
- <RangeSlider
300
- value={gap}
301
- onChange={(v) => update({ gap: Math.round(v) })}
302
- min={0}
303
- max={80}
304
- step={2}
305
- suffix="px"
306
- />
307
- </SettingsField>
308
- </SettingsSection>
309
-
310
- {/* ─── Card Display ────────────────────────────────────── */}
311
- <SettingsSection title="Card Display" defaultOpen={false} icon={<AppearanceIcon />}>
312
- <SettingsField label="Aspect">
313
- <Dropdown
314
- options={ASPECT_RATIO_OPTIONS}
315
- value={aspectRatio}
316
- onChange={(v) => update({ aspect_ratio: v as ProjectCarouselBlock["aspect_ratio"] })}
317
- />
318
- </SettingsField>
319
-
320
- <SettingsField label="Title">
321
- <StyledCheckbox
322
- checked={showTitle}
323
- onChange={(checked) => update({ show_title: checked })}
324
- label="Show project title"
325
- />
326
- </SettingsField>
327
-
328
- <SettingsField label="Subtitle">
329
- <StyledCheckbox
330
- checked={showSubtitle}
331
- onChange={(checked) => update({ show_subtitle: checked })}
332
- label="Show project subtitle"
333
- />
334
- </SettingsField>
335
-
336
- <SettingsField label="Border radius">
337
- <RangeSlider
338
- value={borderRadius}
339
- onChange={(v) => update({ border_radius: Math.round(v) })}
340
- min={0}
341
- max={40}
342
- step={1}
343
- suffix="px"
344
- />
345
- </SettingsField>
346
-
347
- <SettingsField label="Hover">
348
- <SegmentedControl
349
- options={HOVER_EFFECT_OPTIONS}
350
- value={hoverEffect}
351
- onChange={(v) => update({ hover_effect: v })}
352
- />
353
- </SettingsField>
354
- </SettingsSection>
355
-
356
- {/* ─── Video ───────────────────────────────────────────── */}
357
- <SettingsSection title="Video" defaultOpen={false} icon={<VideoIcon />}>
358
- <SettingsField label="Mode">
359
- <SegmentedControl
360
- options={VIDEO_MODE_OPTIONS}
361
- value={videoMode}
362
- onChange={(v) => update({ video_mode: v })}
363
- />
364
- </SettingsField>
365
- <p className="text-[10px] text-neutral-400 leading-snug px-0.5">
366
- How project cover videos play in the carousel.
367
- </p>
368
- </SettingsSection>
369
-
370
- {/* ─── Controls ────────────────────────────────────────── */}
371
- <SettingsSection title="Controls" defaultOpen={false} icon={<NavigationIcon />}>
372
- <SettingsField label="Arrows">
373
- <StyledCheckbox
374
- checked={showArrows}
375
- onChange={(checked) => update({ show_arrows: checked })}
376
- label="Show prev / next arrows on hover (desktop)"
377
- />
378
- </SettingsField>
379
-
380
- <SettingsField label="Dots">
381
- <StyledCheckbox
382
- checked={showDots}
383
- onChange={(checked) => update({ show_dots: checked })}
384
- label="Show pagination dots below the carousel"
385
- />
386
- </SettingsField>
387
-
388
- <SettingsField label="Snap">
389
- <StyledCheckbox
390
- checked={snapScroll}
391
- onChange={(checked) => update({ snap_scroll: checked })}
392
- label="Cards snap into place when scrolling"
393
- />
394
- </SettingsField>
395
- </SettingsSection>
396
-
397
- {/* ─── Card Entrance Animation ─────────────────────────── */}
398
- <SettingsSection title="Card Entrance" defaultOpen={false} icon={<AnimationIcon />}>
399
- <SettingsField label="Enabled">
400
- <StyledCheckbox
401
- checked={entranceEnabled}
402
- onChange={(checked) => updateCardEntrance({ enabled: checked })}
403
- label="Animate cards as they appear"
404
- />
405
- </SettingsField>
406
-
407
- {entranceEnabled && (
408
- <>
409
- <SettingsField label="Preset">
410
- <SegmentedControl
411
- options={ANIMATION_PRESET_OPTIONS}
412
- value={entrancePreset}
413
- onChange={(v) => updateCardEntrance({ preset: v })}
414
- />
415
- </SettingsField>
416
-
417
- <SettingsField label="Stagger">
418
- <RangeSlider
419
- value={entranceStagger}
420
- onChange={(v) => updateCardEntrance({ stagger_delay: Math.round(v) })}
421
- min={0}
422
- max={300}
423
- step={10}
424
- suffix="ms"
425
- />
426
- </SettingsField>
427
-
428
- <SettingsField label="Duration">
429
- <RangeSlider
430
- value={entranceDuration}
431
- onChange={(v) => updateCardEntrance({ duration: Math.round(v) })}
432
- min={100}
433
- max={1500}
434
- step={50}
435
- suffix="ms"
436
- />
437
- </SettingsField>
438
- </>
439
- )}
440
- </SettingsSection>
441
- </>
442
- );
443
- }
1
+ "use client";
2
+
3
+ /**
4
+ * ProjectCarouselBlockEditor — Settings editor for the projectCarouselBlock.
5
+ *
6
+ * Sections:
7
+ * - Source (mode, number of projects, exclude-current toggle)
8
+ * - Layout (cards per view — desktop / tablet / phone — and gap)
9
+ * - Card Display (aspect ratio, show title / subtitle, border radius,
10
+ * hover effect)
11
+ * - Video (video mode)
12
+ * - Controls (show arrows / dots, snap scrolling)
13
+ * - Card Entrance Animation (enabled, preset, stagger, duration)
14
+ *
15
+ * Intentionally independent from ProjectGridEditor — they follow the same
16
+ * visual style but share no code so changes to one never break the other.
17
+ */
18
+
19
+ import React, { useCallback } from "react";
20
+ import { useBuilderStore } from "../../../lib/builder/store";
21
+ import type { ProjectCarouselBlock } from "../../../lib/sanity/types";
22
+ import {
23
+ SettingsField,
24
+ SettingsSection,
25
+ StyledCheckbox,
26
+ } from "./shared";
27
+ import {
28
+ SourceIcon,
29
+ LayoutIcon,
30
+ AppearanceIcon,
31
+ VideoIcon,
32
+ NavigationIcon,
33
+ AnimationIcon,
34
+ } from "./section-icons";
35
+
36
+ // ============================================
37
+ // Constants
38
+ // ============================================
39
+
40
+ const SOURCE_OPTIONS = [
41
+ { value: "auto_latest", label: "Latest" },
42
+ { value: "auto_random", label: "Random" },
43
+ ] as const;
44
+
45
+ const ASPECT_RATIO_OPTIONS = [
46
+ { value: "16/9", label: "16:9" },
47
+ { value: "4/3", label: "4:3" },
48
+ { value: "1/1", label: "1:1" },
49
+ { value: "3/4", label: "3:4" },
50
+ { value: "9/16", label: "9:16" },
51
+ ] as const;
52
+
53
+ const HOVER_EFFECT_OPTIONS = [
54
+ { value: "scale", label: "Scale" },
55
+ { value: "none", label: "None" },
56
+ ] as const;
57
+
58
+ const VIDEO_MODE_OPTIONS = [
59
+ { value: "off", label: "Off" },
60
+ { value: "hover", label: "Hover" },
61
+ { value: "autoloop", label: "Auto" },
62
+ ] as const;
63
+
64
+ const ANIMATION_PRESET_OPTIONS = [
65
+ { value: "fade", label: "Fade" },
66
+ { value: "slide-up", label: "Slide Up" },
67
+ { value: "scale", label: "Scale" },
68
+ ] as const;
69
+
70
+ // ============================================
71
+ // Shared mini-components (inline copies — not imported from ProjectGridEditor
72
+ // to keep the two blocks decoupled, per user's explicit request).
73
+ // ============================================
74
+
75
+ function SegmentedControl<T extends string>({
76
+ options,
77
+ value,
78
+ onChange,
79
+ }: {
80
+ options: readonly { value: T; label: string }[];
81
+ value: T;
82
+ onChange: (v: T) => void;
83
+ }) {
84
+ return (
85
+ <div className="flex gap-1">
86
+ {options.map((opt) => {
87
+ const active = value === opt.value;
88
+ return (
89
+ <button
90
+ key={opt.value}
91
+ type="button"
92
+ onClick={() => onChange(opt.value)}
93
+ className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
94
+ active
95
+ ? "bg-[#3580f9] text-white"
96
+ : "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
97
+ }`}
98
+ >
99
+ {opt.label}
100
+ </button>
101
+ );
102
+ })}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ function RangeSlider({
108
+ value,
109
+ onChange,
110
+ min,
111
+ max,
112
+ step = 1,
113
+ suffix = "",
114
+ decimals = 0,
115
+ }: {
116
+ value: number;
117
+ onChange: (v: number) => void;
118
+ min: number;
119
+ max: number;
120
+ step?: number;
121
+ suffix?: string;
122
+ decimals?: number;
123
+ }) {
124
+ return (
125
+ <div className="flex items-center gap-2">
126
+ <input
127
+ type="range"
128
+ min={min}
129
+ max={max}
130
+ step={step}
131
+ value={value}
132
+ onChange={(e) => onChange(Number(e.target.value))}
133
+ className="flex-1 h-1 accent-[#3580f9] cursor-pointer"
134
+ />
135
+ <span className="text-[11px] text-neutral-500 w-10 text-right tabular-nums shrink-0">
136
+ {value.toFixed(decimals)}{suffix}
137
+ </span>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ function Dropdown<T extends string>({
143
+ options,
144
+ value,
145
+ onChange,
146
+ }: {
147
+ options: readonly { value: T; label: string }[];
148
+ value: T;
149
+ onChange: (v: T) => void;
150
+ }) {
151
+ return (
152
+ <select
153
+ value={value}
154
+ onChange={(e) => onChange(e.target.value as T)}
155
+ 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)]"
156
+ >
157
+ {options.map((opt) => (
158
+ <option key={opt.value} value={opt.value}>
159
+ {opt.label}
160
+ </option>
161
+ ))}
162
+ </select>
163
+ );
164
+ }
165
+
166
+ // ============================================
167
+ // Main editor
168
+ // ============================================
169
+
170
+ interface ProjectCarouselBlockEditorProps {
171
+ block: ProjectCarouselBlock;
172
+ }
173
+
174
+ export default function ProjectCarouselBlockEditor({
175
+ block,
176
+ }: ProjectCarouselBlockEditorProps) {
177
+ const updateBlock = useBuilderStore((s) => s.updateBlock);
178
+
179
+ const update = useCallback(
180
+ (updates: Partial<ProjectCarouselBlock>) => {
181
+ updateBlock(block._key, updates as Partial<ProjectCarouselBlock>);
182
+ },
183
+ [updateBlock, block._key],
184
+ );
185
+
186
+ const updateCardEntrance = useCallback(
187
+ (updates: Partial<NonNullable<ProjectCarouselBlock["card_entrance"]>>) => {
188
+ update({
189
+ card_entrance: {
190
+ enabled: false,
191
+ preset: "slide-up",
192
+ stagger_delay: 80,
193
+ duration: 500,
194
+ ...block.card_entrance,
195
+ ...updates,
196
+ },
197
+ });
198
+ },
199
+ [update, block.card_entrance],
200
+ );
201
+
202
+ // ─── Resolved values (with defaults) ───
203
+ const sourceMode = block.source_mode ?? "auto_latest";
204
+ const maxProjects = block.max_projects ?? 8;
205
+ const excludeCurrent = block.exclude_current !== false;
206
+
207
+ const cpvDesktop = block.cards_per_view_desktop ?? 3.5;
208
+ const cpvTablet = block.cards_per_view_tablet ?? 2.2;
209
+ const cpvPhone = block.cards_per_view_phone ?? 1.2;
210
+ const gap = block.gap ?? 16;
211
+
212
+ const aspectRatio = block.aspect_ratio ?? "4/3";
213
+ const showTitle = block.show_title !== false;
214
+ const showSubtitle = block.show_subtitle === true;
215
+ const borderRadius = block.border_radius ?? 0;
216
+ const hoverEffect = block.hover_effect ?? "scale";
217
+
218
+ const videoMode = block.video_mode ?? "off";
219
+
220
+ const showArrows = block.show_arrows !== false;
221
+ const showDots = block.show_dots === true;
222
+ const snapScroll = block.snap_scroll !== false;
223
+
224
+ const entranceEnabled = block.card_entrance?.enabled === true;
225
+ const entrancePreset = block.card_entrance?.preset ?? "slide-up";
226
+ const entranceStagger = block.card_entrance?.stagger_delay ?? 80;
227
+ const entranceDuration = block.card_entrance?.duration ?? 500;
228
+
229
+ return (
230
+ <>
231
+ {/* ─── Source ──────────────────────────────────────────── */}
232
+ <SettingsSection title="Source" defaultOpen icon={<SourceIcon />}>
233
+ <SettingsField label="Mode">
234
+ <SegmentedControl
235
+ options={SOURCE_OPTIONS}
236
+ value={sourceMode}
237
+ onChange={(v) => update({ source_mode: v })}
238
+ />
239
+ </SettingsField>
240
+
241
+ <SettingsField label="Projects">
242
+ <RangeSlider
243
+ value={maxProjects}
244
+ onChange={(v) => update({ max_projects: Math.round(v) })}
245
+ min={2}
246
+ max={20}
247
+ step={1}
248
+ />
249
+ </SettingsField>
250
+
251
+ <SettingsField label="Exclude current">
252
+ <StyledCheckbox
253
+ checked={excludeCurrent}
254
+ onChange={(checked) => update({ exclude_current: checked })}
255
+ label="Hide the project currently being viewed"
256
+ />
257
+ </SettingsField>
258
+ </SettingsSection>
259
+
260
+ {/* ─── Layout ──────────────────────────────────────────── */}
261
+ <SettingsSection title="Layout" defaultOpen icon={<LayoutIcon />}>
262
+ <SettingsField label="Desktop">
263
+ <RangeSlider
264
+ value={cpvDesktop}
265
+ onChange={(v) => update({ cards_per_view_desktop: v })}
266
+ min={1}
267
+ max={6}
268
+ step={0.5}
269
+ decimals={1}
270
+ suffix=" cards"
271
+ />
272
+ </SettingsField>
273
+
274
+ <SettingsField label="Tablet">
275
+ <RangeSlider
276
+ value={cpvTablet}
277
+ onChange={(v) => update({ cards_per_view_tablet: v })}
278
+ min={1}
279
+ max={4}
280
+ step={0.5}
281
+ decimals={1}
282
+ suffix=" cards"
283
+ />
284
+ </SettingsField>
285
+
286
+ <SettingsField label="Phone">
287
+ <RangeSlider
288
+ value={cpvPhone}
289
+ onChange={(v) => update({ cards_per_view_phone: v })}
290
+ min={1}
291
+ max={3}
292
+ step={0.1}
293
+ decimals={1}
294
+ suffix=" cards"
295
+ />
296
+ </SettingsField>
297
+
298
+ <SettingsField label="Gap">
299
+ <RangeSlider
300
+ value={gap}
301
+ onChange={(v) => update({ gap: Math.round(v) })}
302
+ min={0}
303
+ max={80}
304
+ step={2}
305
+ suffix="px"
306
+ />
307
+ </SettingsField>
308
+ </SettingsSection>
309
+
310
+ {/* ─── Card Display ────────────────────────────────────── */}
311
+ <SettingsSection title="Card Display" defaultOpen={false} icon={<AppearanceIcon />}>
312
+ <SettingsField label="Aspect">
313
+ <Dropdown
314
+ options={ASPECT_RATIO_OPTIONS}
315
+ value={aspectRatio}
316
+ onChange={(v) => update({ aspect_ratio: v as ProjectCarouselBlock["aspect_ratio"] })}
317
+ />
318
+ </SettingsField>
319
+
320
+ <SettingsField label="Title">
321
+ <StyledCheckbox
322
+ checked={showTitle}
323
+ onChange={(checked) => update({ show_title: checked })}
324
+ label="Show project title"
325
+ />
326
+ </SettingsField>
327
+
328
+ <SettingsField label="Subtitle">
329
+ <StyledCheckbox
330
+ checked={showSubtitle}
331
+ onChange={(checked) => update({ show_subtitle: checked })}
332
+ label="Show project subtitle"
333
+ />
334
+ </SettingsField>
335
+
336
+ <SettingsField label="Border radius">
337
+ <RangeSlider
338
+ value={borderRadius}
339
+ onChange={(v) => update({ border_radius: Math.round(v) })}
340
+ min={0}
341
+ max={40}
342
+ step={1}
343
+ suffix="px"
344
+ />
345
+ </SettingsField>
346
+
347
+ <SettingsField label="Hover">
348
+ <SegmentedControl
349
+ options={HOVER_EFFECT_OPTIONS}
350
+ value={hoverEffect}
351
+ onChange={(v) => update({ hover_effect: v })}
352
+ />
353
+ </SettingsField>
354
+ </SettingsSection>
355
+
356
+ {/* ─── Video ───────────────────────────────────────────── */}
357
+ <SettingsSection title="Video" defaultOpen={false} icon={<VideoIcon />}>
358
+ <SettingsField label="Mode">
359
+ <SegmentedControl
360
+ options={VIDEO_MODE_OPTIONS}
361
+ value={videoMode}
362
+ onChange={(v) => update({ video_mode: v })}
363
+ />
364
+ </SettingsField>
365
+ <p className="text-[10px] text-neutral-400 leading-snug px-0.5">
366
+ How project cover videos play in the carousel.
367
+ </p>
368
+ </SettingsSection>
369
+
370
+ {/* ─── Controls ────────────────────────────────────────── */}
371
+ <SettingsSection title="Controls" defaultOpen={false} icon={<NavigationIcon />}>
372
+ <SettingsField label="Arrows">
373
+ <StyledCheckbox
374
+ checked={showArrows}
375
+ onChange={(checked) => update({ show_arrows: checked })}
376
+ label="Show prev / next arrows on hover (desktop)"
377
+ />
378
+ </SettingsField>
379
+
380
+ <SettingsField label="Dots">
381
+ <StyledCheckbox
382
+ checked={showDots}
383
+ onChange={(checked) => update({ show_dots: checked })}
384
+ label="Show pagination dots below the carousel"
385
+ />
386
+ </SettingsField>
387
+
388
+ <SettingsField label="Snap">
389
+ <StyledCheckbox
390
+ checked={snapScroll}
391
+ onChange={(checked) => update({ snap_scroll: checked })}
392
+ label="Cards snap into place when scrolling"
393
+ />
394
+ </SettingsField>
395
+ </SettingsSection>
396
+
397
+ {/* ─── Card Entrance Animation ─────────────────────────── */}
398
+ <SettingsSection title="Card Entrance" defaultOpen={false} icon={<AnimationIcon />}>
399
+ <SettingsField label="Enabled">
400
+ <StyledCheckbox
401
+ checked={entranceEnabled}
402
+ onChange={(checked) => updateCardEntrance({ enabled: checked })}
403
+ label="Animate cards as they appear"
404
+ />
405
+ </SettingsField>
406
+
407
+ {entranceEnabled && (
408
+ <>
409
+ <SettingsField label="Preset">
410
+ <SegmentedControl
411
+ options={ANIMATION_PRESET_OPTIONS}
412
+ value={entrancePreset}
413
+ onChange={(v) => updateCardEntrance({ preset: v })}
414
+ />
415
+ </SettingsField>
416
+
417
+ <SettingsField label="Stagger">
418
+ <RangeSlider
419
+ value={entranceStagger}
420
+ onChange={(v) => updateCardEntrance({ stagger_delay: Math.round(v) })}
421
+ min={0}
422
+ max={300}
423
+ step={10}
424
+ suffix="ms"
425
+ />
426
+ </SettingsField>
427
+
428
+ <SettingsField label="Duration">
429
+ <RangeSlider
430
+ value={entranceDuration}
431
+ onChange={(v) => updateCardEntrance({ duration: Math.round(v) })}
432
+ min={100}
433
+ max={1500}
434
+ step={50}
435
+ suffix="ms"
436
+ />
437
+ </SettingsField>
438
+ </>
439
+ )}
440
+ </SettingsSection>
441
+ </>
442
+ );
443
+ }