@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
@@ -24,6 +24,7 @@ import {
24
24
  ViewportBadge,
25
25
  StyledCheckbox,
26
26
  } from "./shared";
27
+ import { BubbleTooltip } from "../BubbleIcons";
27
28
 
28
29
  // ============================================
29
30
  // Constants
@@ -72,7 +73,7 @@ function SegmentedControl<T extends string>({
72
73
  onClick={() => onChange(opt.value)}
73
74
  className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
74
75
  active
75
- ? "bg-[#076bff] text-white"
76
+ ? "bg-[#3580f9] text-white"
76
77
  : "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
77
78
  }`}
78
79
  >
@@ -112,7 +113,7 @@ function RangeSlider({
112
113
  step={step}
113
114
  value={value}
114
115
  onChange={(e) => onChange(Number(e.target.value))}
115
- className="flex-1 h-1 accent-[#076bff] cursor-pointer"
116
+ className="flex-1 h-1 accent-[#3580f9] cursor-pointer"
116
117
  />
117
118
  <span className="text-[11px] text-neutral-500 w-8 text-right tabular-nums shrink-0">
118
119
  {value}{suffix}
@@ -154,7 +155,7 @@ function RatioChips({
154
155
  onClick={() => toggle(opt.value)}
155
156
  className={`flex-1 flex flex-col items-center gap-1 px-1.5 py-2 rounded-lg text-[10px] transition-colors ${
156
157
  active
157
- ? "bg-[#076bff] text-white"
158
+ ? "bg-[#3580f9] text-white"
158
159
  : "bg-neutral-100 text-neutral-500 hover:bg-neutral-200"
159
160
  }`}
160
161
  >
@@ -206,7 +207,7 @@ function CardRatioChips({
206
207
  onClick={() => onChange(opt.value as "16/9" | "1/1" | "9/16" | null)}
207
208
  className={`flex flex-col items-center gap-1 px-1.5 py-2 rounded-lg text-[10px] transition-colors ${
208
209
  active
209
- ? "bg-[#076bff] text-white"
210
+ ? "bg-[#3580f9] text-white"
210
211
  : "bg-neutral-100 text-neutral-500 hover:bg-neutral-200"
211
212
  }`}
212
213
  >
@@ -511,12 +512,12 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
511
512
  <React.Fragment key={item._key}>
512
513
  <div
513
514
  className={`flex items-center gap-2 px-2 py-1.5 group cursor-pointer transition-colors ${
514
- isCardSelected ? "bg-[#076bff]/10 ring-1 ring-[#076bff]/30 rounded-t-lg" : "bg-[#f5f5f5] hover:bg-[#efefef] rounded-lg"
515
+ isCardSelected ? "bg-[#3580f9]/10 ring-1 ring-[#3580f9]/30 rounded-t-lg" : "bg-[#f5f5f5] hover:bg-[#efefef] rounded-lg"
515
516
  }`}
516
517
  onClick={() => selectProjectCard(isCardSelected ? null : item._key)}
517
518
  >
518
519
  {/* Drag grip */}
519
- <span className="text-neutral-300 shrink-0 cursor-grab" title="Reorder">
520
+ <span className="group/bb relative text-neutral-300 shrink-0 cursor-grab" aria-label="Reorder">
520
521
  <svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
521
522
  <circle cx="3" cy="2" r="1" />
522
523
  <circle cx="7" cy="2" r="1" />
@@ -525,6 +526,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
525
526
  <circle cx="3" cy="8" r="1" />
526
527
  <circle cx="7" cy="8" r="1" />
527
528
  </svg>
529
+ <BubbleTooltip>Reorder</BubbleTooltip>
528
530
  </span>
529
531
 
530
532
  {/* Project name */}
@@ -543,43 +545,46 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
543
545
  <button
544
546
  onClick={(e) => { e.stopPropagation(); moveProject(i, -1); }}
545
547
  disabled={i === 0}
546
- className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
547
- title="Move up"
548
+ className="group/bb relative p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
549
+ aria-label="Move up"
548
550
  >
549
551
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
550
552
  <polyline points="18 15 12 9 6 15" />
551
553
  </svg>
554
+ <BubbleTooltip>Move up</BubbleTooltip>
552
555
  </button>
553
556
  <button
554
557
  onClick={(e) => { e.stopPropagation(); moveProject(i, 1); }}
555
558
  disabled={i === (block.projects || []).length - 1}
556
- className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
557
- title="Move down"
559
+ className="group/bb relative p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-20 transition-colors"
560
+ aria-label="Move down"
558
561
  >
559
562
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
560
563
  <polyline points="6 9 12 15 18 9" />
561
564
  </svg>
565
+ <BubbleTooltip>Move down</BubbleTooltip>
562
566
  </button>
563
567
 
564
568
  {/* Remove */}
565
569
  <button
566
570
  onClick={(e) => { e.stopPropagation(); removeProject(item._key); }}
567
- className="p-0.5 text-neutral-400 hover:text-red-500 transition-colors"
568
- title="Remove"
571
+ className="group/bb relative p-0.5 text-neutral-400 hover:text-red-500 transition-colors"
572
+ aria-label="Remove"
569
573
  >
570
574
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
571
575
  <line x1="18" y1="6" x2="6" y2="18" />
572
576
  <line x1="6" y1="6" x2="18" y2="18" />
573
577
  </svg>
578
+ <BubbleTooltip>Remove</BubbleTooltip>
574
579
  </button>
575
580
  </div>
576
581
 
577
582
  {/* Per-card settings — expanded below this project row */}
578
583
  {isCardSelected && (
579
- <div className="px-2 pb-2 pt-1 -mt-1 rounded-b-lg bg-[#076bff]/5 border-x border-b border-[#076bff]/15">
584
+ <div className="px-2 pb-2 pt-1 -mt-1 rounded-b-lg bg-[#3580f9]/5 border-x border-b border-[#3580f9]/15">
580
585
  {isResponsive && (
581
- <div className="flex items-center gap-1.5 px-2 py-1 mb-1 rounded bg-[#076bff]/8">
582
- <span className="text-[10px] font-medium text-[#076bff]">
586
+ <div className="flex items-center gap-1.5 px-2 py-1 mb-1 rounded bg-[#3580f9]/8">
587
+ <span className="text-[10px] font-medium text-[#3580f9]">
583
588
  {activeViewport === "tablet" ? "Tablet" : "Phone"} override
584
589
  </span>
585
590
  </div>
@@ -614,7 +619,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
614
619
  placeholder="Search projects..."
615
620
  value={search}
616
621
  onChange={(e) => setSearch(e.target.value)}
617
- className="w-full rounded-md bg-[#f5f5f5] px-2.5 py-1.5 text-xs text-neutral-900 placeholder:text-neutral-400 outline-none focus:bg-white focus:ring-1 focus:ring-[#076bff]/20"
622
+ className="w-full rounded-md bg-[#f5f5f5] px-2.5 py-1.5 text-xs text-neutral-900 placeholder:text-neutral-400 outline-none focus:bg-white focus:ring-1 focus:ring-[#3580f9]/20"
618
623
  autoFocus
619
624
  />
620
625
  </div>
@@ -14,6 +14,7 @@ import {
14
14
  useActiveViewport,
15
15
  INPUT_CLASS,
16
16
  } from "./shared";
17
+ import { BubbleTooltip } from "../BubbleIcons";
17
18
 
18
19
  interface Props {
19
20
  block: SpacerBlock;
@@ -79,29 +80,30 @@ export default function SpacerBlockEditor({ block }: Props) {
79
80
  onReset={() => resetOverride("height")}
80
81
  >
81
82
  <div className="flex gap-1">
82
- {HEIGHT_PRESETS.map((preset) => (
83
- <button
84
- key={preset.value}
85
- onClick={() => updateResponsive("height", preset.value)}
86
- className={`flex-1 rounded border py-1.5 text-xs transition-colors flex flex-col items-center gap-0.5 ${
87
- effectiveHeight === preset.value
88
- ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
89
- : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
90
- }`}
91
- title={
92
- preset.value === "custom"
93
- ? "Custom height"
94
- : `${preset.px}px`
95
- }
96
- >
97
- <span>{preset.label}</span>
98
- {preset.value !== "custom" && (
99
- <span className="text-[8px] text-neutral-600">
100
- {preset.px}px
101
- </span>
102
- )}
103
- </button>
104
- ))}
83
+ {HEIGHT_PRESETS.map((preset) => {
84
+ const tooltip =
85
+ preset.value === "custom" ? "Custom height" : `${preset.px}px`;
86
+ return (
87
+ <button
88
+ key={preset.value}
89
+ onClick={() => updateResponsive("height", preset.value)}
90
+ className={`group/bb relative flex-1 rounded border py-1.5 text-xs transition-colors flex flex-col items-center gap-0.5 ${
91
+ effectiveHeight === preset.value
92
+ ? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
93
+ : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
94
+ }`}
95
+ aria-label={tooltip}
96
+ >
97
+ <span>{preset.label}</span>
98
+ {preset.value !== "custom" && (
99
+ <span className="text-[8px] text-neutral-600">
100
+ {preset.px}px
101
+ </span>
102
+ )}
103
+ <BubbleTooltip>{tooltip}</BubbleTooltip>
104
+ </button>
105
+ );
106
+ })}
105
107
  </div>
106
108
  </ResponsiveField>
107
109
 
@@ -122,7 +124,7 @@ export default function SpacerBlockEditor({ block }: Props) {
122
124
  onChange={(e) =>
123
125
  updateResponsive("custom_height", parseInt(e.target.value))
124
126
  }
125
- className="flex-1 accent-[#076bff]"
127
+ className="flex-1 accent-[#3580f9]"
126
128
  />
127
129
  <input
128
130
  type="number"
@@ -146,9 +148,9 @@ export default function SpacerBlockEditor({ block }: Props) {
146
148
  style={{ height: `${Math.min(currentPx, 200)}px` }}
147
149
  >
148
150
  <div className="absolute left-0 right-0 top-1/2 border-t border-dashed border-neutral-200" />
149
- <div className="absolute left-2 top-0 w-px h-2 bg-[#076bff]" />
150
- <div className="absolute left-2 bottom-0 w-px h-2 bg-[#076bff]" />
151
- <div className="absolute left-2 top-0 bottom-0 w-px bg-[#076bff]/20" />
151
+ <div className="absolute left-2 top-0 w-px h-2 bg-[#3580f9]" />
152
+ <div className="absolute left-2 bottom-0 w-px h-2 bg-[#3580f9]" />
153
+ <div className="absolute left-2 top-0 bottom-0 w-px bg-[#3580f9]/20" />
152
154
  <div className="absolute inset-0 flex items-center justify-center">
153
155
  <span className="text-xs text-neutral-500 bg-white/80 px-1.5 py-0.5 rounded">
154
156
  {currentPx}px
@@ -1,109 +1,109 @@
1
- import { StaggerIcon } from "./section-icons";
2
- import { SettingsSection, SettingsField } from "./shared";
3
-
4
- // ============================================
5
- // Types
6
- // ============================================
7
-
8
- export interface StaggerConfig {
9
- enabled?: boolean;
10
- delayPerChild?: number;
11
- direction?: "left-to-right" | "right-to-left";
12
- }
13
-
14
- // ============================================
15
- // CSS constants
16
- // ============================================
17
-
18
- const SLIDER_CLASS =
19
- "w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#076bff] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#076bff] [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm";
20
-
21
- const TOGGLE_CLASS_ON =
22
- "relative w-8 h-[18px] rounded-full bg-[#076bff] transition-colors cursor-pointer after:content-[''] after:absolute after:top-[2px] after:left-[15px] after:w-[14px] after:h-[14px] after:rounded-full after:bg-white after:shadow-sm after:transition-all";
23
-
24
- const TOGGLE_CLASS_OFF =
25
- "relative w-8 h-[18px] rounded-full bg-[#d4d4d4] transition-colors cursor-pointer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:w-[14px] after:h-[14px] after:rounded-full after:bg-white after:shadow-sm after:transition-all";
26
-
27
- // ============================================
28
- // Stagger Section (row mode only)
29
- // ============================================
30
-
31
- export default function StaggerSettings({
32
- stagger,
33
- onChange,
34
- }: {
35
- stagger?: StaggerConfig;
36
- onChange: (s: StaggerConfig) => void;
37
- }) {
38
- const enabled = stagger?.enabled ?? false;
39
- const delay = stagger?.delayPerChild ?? 100;
40
- const direction = stagger?.direction ?? "left-to-right";
41
-
42
- return (
43
- <SettingsSection title="Stagger Children" icon={<StaggerIcon />}>
44
- <div className="space-y-3">
45
- {/* Enable toggle */}
46
- <div className="flex items-center justify-between">
47
- <span className="text-[11px] text-neutral-500">
48
- {enabled ? "Enabled" : "Disabled"}
49
- </span>
50
- <button
51
- type="button"
52
- onClick={() => onChange({ ...stagger, enabled: !enabled })}
53
- className={enabled ? TOGGLE_CLASS_ON : TOGGLE_CLASS_OFF}
54
- aria-label={enabled ? "Stagger enabled" : "Stagger disabled"}
55
- />
56
- </div>
57
-
58
- {enabled && (
59
- <>
60
- {/* Delay slider */}
61
- <SettingsField label={`Delay — ${delay}ms`}>
62
- <input
63
- type="range"
64
- min={50}
65
- max={5000}
66
- step={10}
67
- value={delay}
68
- onChange={(e) =>
69
- onChange({ ...stagger, enabled: true, delayPerChild: Number(e.target.value) })
70
- }
71
- className={SLIDER_CLASS}
72
- />
73
- <div className="flex justify-between mt-0.5">
74
- <span className="text-[10px] text-neutral-400">50ms</span>
75
- <span className="text-[10px] text-neutral-400">5000ms</span>
76
- </div>
77
- </SettingsField>
78
-
79
- {/* Direction */}
80
- <SettingsField label="Direction">
81
- <div className="flex gap-1">
82
- <button
83
- onClick={() => onChange({ ...stagger, enabled: true, direction: "left-to-right" })}
84
- className={`flex-1 py-1.5 rounded-md text-[10px] font-medium transition-all ${
85
- direction === "left-to-right"
86
- ? "bg-[#076bff] text-white"
87
- : "bg-[#f5f5f5] text-neutral-500 hover:bg-[#ebebeb]"
88
- }`}
89
- >
90
- L → R
91
- </button>
92
- <button
93
- onClick={() => onChange({ ...stagger, enabled: true, direction: "right-to-left" })}
94
- className={`flex-1 py-1.5 rounded-md text-[10px] font-medium transition-all ${
95
- direction === "right-to-left"
96
- ? "bg-[#076bff] text-white"
97
- : "bg-[#f5f5f5] text-neutral-500 hover:bg-[#ebebeb]"
98
- }`}
99
- >
100
- R → L
101
- </button>
102
- </div>
103
- </SettingsField>
104
- </>
105
- )}
106
- </div>
107
- </SettingsSection>
108
- );
109
- }
1
+ import { StaggerIcon } from "./section-icons";
2
+ import { SettingsSection, SettingsField } from "./shared";
3
+
4
+ // ============================================
5
+ // Types
6
+ // ============================================
7
+
8
+ export interface StaggerConfig {
9
+ enabled?: boolean;
10
+ delayPerChild?: number;
11
+ direction?: "left-to-right" | "right-to-left";
12
+ }
13
+
14
+ // ============================================
15
+ // CSS constants
16
+ // ============================================
17
+
18
+ const SLIDER_CLASS =
19
+ "w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#3580f9] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#3580f9] [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm";
20
+
21
+ const TOGGLE_CLASS_ON =
22
+ "relative w-8 h-[18px] rounded-full bg-[#3580f9] transition-colors cursor-pointer after:content-[''] after:absolute after:top-[2px] after:left-[15px] after:w-[14px] after:h-[14px] after:rounded-full after:bg-white after:shadow-sm after:transition-all";
23
+
24
+ const TOGGLE_CLASS_OFF =
25
+ "relative w-8 h-[18px] rounded-full bg-[#d4d4d4] transition-colors cursor-pointer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:w-[14px] after:h-[14px] after:rounded-full after:bg-white after:shadow-sm after:transition-all";
26
+
27
+ // ============================================
28
+ // Stagger Section (row mode only)
29
+ // ============================================
30
+
31
+ export default function StaggerSettings({
32
+ stagger,
33
+ onChange,
34
+ }: {
35
+ stagger?: StaggerConfig;
36
+ onChange: (s: StaggerConfig) => void;
37
+ }) {
38
+ const enabled = stagger?.enabled ?? false;
39
+ const delay = stagger?.delayPerChild ?? 100;
40
+ const direction = stagger?.direction ?? "left-to-right";
41
+
42
+ return (
43
+ <SettingsSection title="Stagger Children" icon={<StaggerIcon />}>
44
+ <div className="space-y-3">
45
+ {/* Enable toggle */}
46
+ <div className="flex items-center justify-between">
47
+ <span className="text-[11px] text-neutral-500">
48
+ {enabled ? "Enabled" : "Disabled"}
49
+ </span>
50
+ <button
51
+ type="button"
52
+ onClick={() => onChange({ ...stagger, enabled: !enabled })}
53
+ className={enabled ? TOGGLE_CLASS_ON : TOGGLE_CLASS_OFF}
54
+ aria-label={enabled ? "Stagger enabled" : "Stagger disabled"}
55
+ />
56
+ </div>
57
+
58
+ {enabled && (
59
+ <>
60
+ {/* Delay slider */}
61
+ <SettingsField label={`Delay — ${delay}ms`}>
62
+ <input
63
+ type="range"
64
+ min={50}
65
+ max={5000}
66
+ step={10}
67
+ value={delay}
68
+ onChange={(e) =>
69
+ onChange({ ...stagger, enabled: true, delayPerChild: Number(e.target.value) })
70
+ }
71
+ className={SLIDER_CLASS}
72
+ />
73
+ <div className="flex justify-between mt-0.5">
74
+ <span className="text-[10px] text-neutral-400">50ms</span>
75
+ <span className="text-[10px] text-neutral-400">5000ms</span>
76
+ </div>
77
+ </SettingsField>
78
+
79
+ {/* Direction */}
80
+ <SettingsField label="Direction">
81
+ <div className="flex gap-1">
82
+ <button
83
+ onClick={() => onChange({ ...stagger, enabled: true, direction: "left-to-right" })}
84
+ className={`flex-1 py-1.5 rounded-md text-[10px] font-medium transition-all ${
85
+ direction === "left-to-right"
86
+ ? "bg-[#3580f9] text-white"
87
+ : "bg-[#f5f5f5] text-neutral-500 hover:bg-[#ebebeb]"
88
+ }`}
89
+ >
90
+ L → R
91
+ </button>
92
+ <button
93
+ onClick={() => onChange({ ...stagger, enabled: true, direction: "right-to-left" })}
94
+ className={`flex-1 py-1.5 rounded-md text-[10px] font-medium transition-all ${
95
+ direction === "right-to-left"
96
+ ? "bg-[#3580f9] text-white"
97
+ : "bg-[#f5f5f5] text-neutral-500 hover:bg-[#ebebeb]"
98
+ }`}
99
+ >
100
+ R → L
101
+ </button>
102
+ </div>
103
+ </SettingsField>
104
+ </>
105
+ )}
106
+ </div>
107
+ </SettingsSection>
108
+ );
109
+ }
@@ -30,6 +30,7 @@ import {
30
30
  AlignRightIcon,
31
31
  AlignJustifyIcon,
32
32
  } from "./TextAlignmentIcons";
33
+ import { BubbleTooltip } from "../BubbleIcons";
33
34
 
34
35
  // ============================================
35
36
  // Responsive style field — MUST be defined outside the editor component
@@ -62,7 +63,7 @@ function ResponsiveStyleField({
62
63
  <span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
63
64
  )}
64
65
  {isOverridden && (
65
- <span className="block text-[9px] text-[#076bff] mt-0.5">overridden</span>
66
+ <span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
66
67
  )}
67
68
  </label>
68
69
  <div className="flex-1 min-w-0">
@@ -292,25 +293,29 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
292
293
 
293
294
  <ResponsiveStyleField label="Align" subProp="alignment" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("alignment")} onReset={resetStyleOverride}>
294
295
  <div className="flex gap-0.5 bg-[#f5f5f5] rounded-lg p-0.5">
295
- {alignments.map(({ value, icon }) => (
296
- <button
297
- key={value}
298
- onClick={() => updateStyleResponsive("alignment", value)}
299
- className={`flex-1 flex items-center justify-center py-[5px] rounded-md transition-all ${
300
- currentAlignment === value
301
- ? "bg-white text-neutral-900 shadow-[0_1px_3px_rgba(0,0,0,0.08)]"
302
- : "text-neutral-300 hover:text-neutral-500"
303
- }`}
304
- title={value.charAt(0).toUpperCase() + value.slice(1)}
305
- >
306
- {icon}
307
- </button>
308
- ))}
296
+ {alignments.map(({ value, icon }) => {
297
+ const label = value.charAt(0).toUpperCase() + value.slice(1);
298
+ return (
299
+ <button
300
+ key={value}
301
+ onClick={() => updateStyleResponsive("alignment", value)}
302
+ className={`group/bb relative flex-1 flex items-center justify-center py-[5px] rounded-md transition-all ${
303
+ currentAlignment === value
304
+ ? "bg-white text-neutral-900 shadow-[0_1px_3px_rgba(0,0,0,0.08)]"
305
+ : "text-neutral-300 hover:text-neutral-500"
306
+ }`}
307
+ aria-label={label}
308
+ >
309
+ {icon}
310
+ <BubbleTooltip>{label}</BubbleTooltip>
311
+ </button>
312
+ );
313
+ })}
309
314
  </div>
310
315
  </ResponsiveStyleField>
311
316
 
312
317
  <ResponsiveStyleField label="Size" subProp="fontSize" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("fontSize")} onReset={resetStyleOverride}>
313
- <div className="flex items-center gap-0 bg-[#f5f5f5] rounded-lg overflow-hidden transition-all border border-transparent focus-within:bg-white focus-within:border-[#076bff] focus-within:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]">
318
+ <div className="flex items-center gap-0 bg-[#f5f5f5] rounded-lg overflow-hidden transition-all border border-transparent focus-within:bg-white focus-within:border-[#3580f9] focus-within:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]">
314
319
  <input
315
320
  type="number"
316
321
  min={1}
@@ -450,7 +455,7 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
450
455
  onChange={(e) =>
451
456
  updateStyleResponsive("opacity", parseInt(e.target.value) / 100)
452
457
  }
453
- className="flex-1 accent-[#076bff]"
458
+ className="flex-1 accent-[#3580f9]"
454
459
  />
455
460
  <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
456
461
  {Math.round((getEffectiveStyleValue<number>("opacity", style.opacity ?? 1)) * 100)}%
@@ -134,7 +134,7 @@ export default function TextStylePicker({
134
134
  onChange={(e) => setSearch(e.target.value)}
135
135
  placeholder="Search..."
136
136
  autoFocus
137
- className="w-full rounded-md bg-[#f5f5f5] px-2 py-1.5 text-xs text-neutral-900 border-none outline-none focus:bg-white focus:ring-1 focus:ring-[#076bff]/30"
137
+ className="w-full rounded-md bg-[#f5f5f5] px-2 py-1.5 text-xs text-neutral-900 border-none outline-none focus:bg-white focus:ring-1 focus:ring-[#3580f9]/30"
138
138
  />
139
139
  </div>
140
140
  <div className="max-h-[220px] overflow-y-auto py-1">
@@ -111,7 +111,7 @@ export default function VideoBlockEditor({ block }: Props) {
111
111
  onClick={() => update({ video_type: opt.value })}
112
112
  className={`flex-1 rounded border py-1 text-xs transition-colors ${
113
113
  (block.video_type || "vimeo") === opt.value
114
- ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
114
+ ? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
115
115
  : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
116
116
  }`}
117
117
  >
@@ -200,7 +200,7 @@ export default function VideoBlockEditor({ block }: Props) {
200
200
  onClick={() => updateResponsive("width", opt.value)}
201
201
  className={`flex-1 rounded border py-1 text-xs transition-colors ${
202
202
  effectiveWidth === opt.value
203
- ? "border-[#076bff] bg-[#076bff]/20 text-neutral-900"
203
+ ? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
204
204
  : "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
205
205
  }`}
206
206
  >
@@ -1,10 +1,11 @@
1
- export { default as TextBlockEditor } from "./TextBlockEditor";
2
- export { default as ImageBlockEditor } from "./ImageBlockEditor";
3
- export { default as ImageGridBlockEditor } from "./ImageGridBlockEditor";
4
- export { default as VideoBlockEditor } from "./VideoBlockEditor";
5
- export { default as SpacerBlockEditor } from "./SpacerBlockEditor";
6
- export { default as ButtonBlockEditor } from "./ButtonBlockEditor";
7
- export { default as ProjectGridEditor } from "./ProjectGridEditor";
8
- export { default as ProjectCarouselBlockEditor } from "./ProjectCarouselBlockEditor";
9
- export { SettingsField, SettingsSection, StyledSelect, StyledInput, StyledCheckbox } from "./shared";
10
- export { getSpacerPx } from "./SpacerBlockEditor";
1
+ export { default as TextBlockEditor } from "./TextBlockEditor";
2
+ export { default as ImageBlockEditor } from "./ImageBlockEditor";
3
+ export { default as ImageGridBlockEditor } from "./ImageGridBlockEditor";
4
+ export { default as VideoBlockEditor } from "./VideoBlockEditor";
5
+ export { default as SpacerBlockEditor } from "./SpacerBlockEditor";
6
+ export { default as ButtonBlockEditor } from "./ButtonBlockEditor";
7
+ export { default as ProjectGridEditor } from "./ProjectGridEditor";
8
+ export { default as ProjectCarouselBlockEditor } from "./ProjectCarouselBlockEditor";
9
+ export { default as MarqueeBlockEditor } from "./MarqueeBlockEditor";
10
+ export { SettingsField, SettingsSection, StyledSelect, StyledInput, StyledCheckbox } from "./shared";
11
+ export { getSpacerPx } from "./SpacerBlockEditor";
@@ -6,6 +6,7 @@ import { hasOverride } from "../../../lib/builder/responsive";
6
6
  import type { DeviceViewport } from "../../../lib/builder/types";
7
7
  import type { ContentBlock } from "../../../lib/sanity/types";
8
8
  import AssetBrowser from "../AssetBrowser";
9
+ import { BubbleTooltip } from "../BubbleIcons";
9
10
 
10
11
  // ============================================
11
12
  // Shared CSS classes — Framer-style design system
@@ -13,11 +14,11 @@ import AssetBrowser from "../AssetBrowser";
13
14
 
14
15
  /** Base input class: gray bg, no border, border on focus */
15
16
  const INPUT_CLASS =
16
- "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-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
17
+ "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)]";
17
18
 
18
19
  /** Select class — same as input */
19
20
  const SELECT_CLASS =
20
- "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-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
21
+ "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)]";
21
22
 
22
23
  // ============================================
23
24
  // Hooks
@@ -47,8 +48,8 @@ export function ViewportBadge() {
47
48
  };
48
49
 
49
50
  return (
50
- <div className="flex items-center gap-1.5 px-3 py-1.5 mb-2 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
51
- <span className="text-[11px] font-medium text-[#076bff]">
51
+ <div className="flex items-center gap-1.5 px-3 py-1.5 mb-2 rounded-lg bg-[#3580f9]/8 border border-[#3580f9]/15">
52
+ <span className="text-[11px] font-medium text-[#3580f9]">
52
53
  Editing {labels[viewport]} overrides
53
54
  </span>
54
55
  </div>
@@ -88,7 +89,7 @@ export function ResponsiveField({
88
89
  <span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
89
90
  )}
90
91
  {isOverridden && (
91
- <span className="block text-[9px] text-[#076bff] mt-0.5">overridden</span>
92
+ <span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
92
93
  )}
93
94
  </label>
94
95
  <div className="flex-1 min-w-0">
@@ -291,7 +292,7 @@ export function StyledCheckbox({
291
292
  type="button"
292
293
  onClick={() => onChange(!checked)}
293
294
  className={`relative w-8 h-[18px] rounded-full transition-colors ${
294
- checked ? "bg-[#076bff]" : "bg-neutral-200 group-hover:bg-neutral-300"
295
+ checked ? "bg-[#3580f9]" : "bg-neutral-200 group-hover:bg-neutral-300"
295
296
  }`}
296
297
  >
297
298
  <span
@@ -338,10 +339,11 @@ export function AssetPathInput({
338
339
  <button
339
340
  type="button"
340
341
  onClick={() => setBrowserOpen(true)}
341
- className="shrink-0 rounded-lg bg-[#f5f5f5] px-2.5 py-[7px] text-[11px] text-neutral-500 hover:text-neutral-900 hover:bg-[#efefef] transition-colors"
342
- title="Browse assets"
342
+ className="group/bb relative shrink-0 rounded-lg bg-[#f5f5f5] px-2.5 py-[7px] text-[11px] text-neutral-500 hover:text-neutral-900 hover:bg-[#efefef] transition-colors"
343
+ aria-label="Browse assets"
343
344
  >
344
345
  Browse
346
+ <BubbleTooltip>Browse assets</BubbleTooltip>
345
347
  </button>
346
348
  </div>
347
349
  <AssetBrowser