@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.
- package/README.md +151 -36
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -327
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -227
- package/components/builder/BlockTypePicker.tsx +3 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -275
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +20 -4
- package/components/builder/SectionV2Column.tsx +74 -68
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +192 -173
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +7 -7
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +3 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +3 -0
- package/lib/builder/block-registrations.ts +468 -335
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +6 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +156 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +12 -9
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -111
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -72,7 +72,7 @@ function SegmentedControl<T extends string>({
|
|
|
72
72
|
onClick={() => onChange(opt.value)}
|
|
73
73
|
className={`flex-1 px-2 py-1.5 text-xs rounded transition-colors ${
|
|
74
74
|
active
|
|
75
|
-
? "bg-[#
|
|
75
|
+
? "bg-[#3580f9] text-white"
|
|
76
76
|
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
|
|
77
77
|
}`}
|
|
78
78
|
>
|
|
@@ -112,7 +112,7 @@ function RangeSlider({
|
|
|
112
112
|
step={step}
|
|
113
113
|
value={value}
|
|
114
114
|
onChange={(e) => onChange(Number(e.target.value))}
|
|
115
|
-
className="flex-1 h-1 accent-[#
|
|
115
|
+
className="flex-1 h-1 accent-[#3580f9] cursor-pointer"
|
|
116
116
|
/>
|
|
117
117
|
<span className="text-[11px] text-neutral-500 w-8 text-right tabular-nums shrink-0">
|
|
118
118
|
{value}{suffix}
|
|
@@ -154,7 +154,7 @@ function RatioChips({
|
|
|
154
154
|
onClick={() => toggle(opt.value)}
|
|
155
155
|
className={`flex-1 flex flex-col items-center gap-1 px-1.5 py-2 rounded-lg text-[10px] transition-colors ${
|
|
156
156
|
active
|
|
157
|
-
? "bg-[#
|
|
157
|
+
? "bg-[#3580f9] text-white"
|
|
158
158
|
: "bg-neutral-100 text-neutral-500 hover:bg-neutral-200"
|
|
159
159
|
}`}
|
|
160
160
|
>
|
|
@@ -206,7 +206,7 @@ function CardRatioChips({
|
|
|
206
206
|
onClick={() => onChange(opt.value as "16/9" | "1/1" | "9/16" | null)}
|
|
207
207
|
className={`flex flex-col items-center gap-1 px-1.5 py-2 rounded-lg text-[10px] transition-colors ${
|
|
208
208
|
active
|
|
209
|
-
? "bg-[#
|
|
209
|
+
? "bg-[#3580f9] text-white"
|
|
210
210
|
: "bg-neutral-100 text-neutral-500 hover:bg-neutral-200"
|
|
211
211
|
}`}
|
|
212
212
|
>
|
|
@@ -511,7 +511,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
511
511
|
<React.Fragment key={item._key}>
|
|
512
512
|
<div
|
|
513
513
|
className={`flex items-center gap-2 px-2 py-1.5 group cursor-pointer transition-colors ${
|
|
514
|
-
isCardSelected ? "bg-[#
|
|
514
|
+
isCardSelected ? "bg-[#3580f9]/10 ring-1 ring-[#3580f9]/30 rounded-t-lg" : "bg-[#f5f5f5] hover:bg-[#efefef] rounded-lg"
|
|
515
515
|
}`}
|
|
516
516
|
onClick={() => selectProjectCard(isCardSelected ? null : item._key)}
|
|
517
517
|
>
|
|
@@ -576,10 +576,10 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
576
576
|
|
|
577
577
|
{/* Per-card settings — expanded below this project row */}
|
|
578
578
|
{isCardSelected && (
|
|
579
|
-
<div className="px-2 pb-2 pt-1 -mt-1 rounded-b-lg bg-[#
|
|
579
|
+
<div className="px-2 pb-2 pt-1 -mt-1 rounded-b-lg bg-[#3580f9]/5 border-x border-b border-[#3580f9]/15">
|
|
580
580
|
{isResponsive && (
|
|
581
|
-
<div className="flex items-center gap-1.5 px-2 py-1 mb-1 rounded bg-[#
|
|
582
|
-
<span className="text-[10px] font-medium text-[#
|
|
581
|
+
<div className="flex items-center gap-1.5 px-2 py-1 mb-1 rounded bg-[#3580f9]/8">
|
|
582
|
+
<span className="text-[10px] font-medium text-[#3580f9]">
|
|
583
583
|
{activeViewport === "tablet" ? "Tablet" : "Phone"} override
|
|
584
584
|
</span>
|
|
585
585
|
</div>
|
|
@@ -614,7 +614,7 @@ export default function ProjectGridEditor({ block }: ProjectGridEditorProps) {
|
|
|
614
614
|
placeholder="Search projects..."
|
|
615
615
|
value={search}
|
|
616
616
|
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-[#
|
|
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-[#3580f9]/20"
|
|
618
618
|
autoFocus
|
|
619
619
|
/>
|
|
620
620
|
</div>
|
|
@@ -85,7 +85,7 @@ export default function SpacerBlockEditor({ block }: Props) {
|
|
|
85
85
|
onClick={() => updateResponsive("height", preset.value)}
|
|
86
86
|
className={`flex-1 rounded border py-1.5 text-xs transition-colors flex flex-col items-center gap-0.5 ${
|
|
87
87
|
effectiveHeight === preset.value
|
|
88
|
-
? "border-[#
|
|
88
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
89
89
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
90
90
|
}`}
|
|
91
91
|
title={
|
|
@@ -122,7 +122,7 @@ export default function SpacerBlockEditor({ block }: Props) {
|
|
|
122
122
|
onChange={(e) =>
|
|
123
123
|
updateResponsive("custom_height", parseInt(e.target.value))
|
|
124
124
|
}
|
|
125
|
-
className="flex-1 accent-[#
|
|
125
|
+
className="flex-1 accent-[#3580f9]"
|
|
126
126
|
/>
|
|
127
127
|
<input
|
|
128
128
|
type="number"
|
|
@@ -146,9 +146,9 @@ export default function SpacerBlockEditor({ block }: Props) {
|
|
|
146
146
|
style={{ height: `${Math.min(currentPx, 200)}px` }}
|
|
147
147
|
>
|
|
148
148
|
<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-[#
|
|
150
|
-
<div className="absolute left-2 bottom-0 w-px h-2 bg-[#
|
|
151
|
-
<div className="absolute left-2 top-0 bottom-0 w-px bg-[#
|
|
149
|
+
<div className="absolute left-2 top-0 w-px h-2 bg-[#3580f9]" />
|
|
150
|
+
<div className="absolute left-2 bottom-0 w-px h-2 bg-[#3580f9]" />
|
|
151
|
+
<div className="absolute left-2 top-0 bottom-0 w-px bg-[#3580f9]/20" />
|
|
152
152
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
153
153
|
<span className="text-xs text-neutral-500 bg-white/80 px-1.5 py-0.5 rounded">
|
|
154
154
|
{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-[#
|
|
20
|
-
|
|
21
|
-
const TOGGLE_CLASS_ON =
|
|
22
|
-
"relative w-8 h-[18px] rounded-full bg-[#
|
|
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-[#
|
|
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-[#
|
|
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
|
+
}
|
|
@@ -62,7 +62,7 @@ function ResponsiveStyleField({
|
|
|
62
62
|
<span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
|
|
63
63
|
)}
|
|
64
64
|
{isOverridden && (
|
|
65
|
-
<span className="block text-[9px] text-[#
|
|
65
|
+
<span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
|
|
66
66
|
)}
|
|
67
67
|
</label>
|
|
68
68
|
<div className="flex-1 min-w-0">
|
|
@@ -310,7 +310,7 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
|
310
310
|
</ResponsiveStyleField>
|
|
311
311
|
|
|
312
312
|
<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-[#
|
|
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-[#3580f9] focus-within:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]">
|
|
314
314
|
<input
|
|
315
315
|
type="number"
|
|
316
316
|
min={1}
|
|
@@ -450,7 +450,7 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
|
450
450
|
onChange={(e) =>
|
|
451
451
|
updateStyleResponsive("opacity", parseInt(e.target.value) / 100)
|
|
452
452
|
}
|
|
453
|
-
className="flex-1 accent-[#
|
|
453
|
+
className="flex-1 accent-[#3580f9]"
|
|
454
454
|
/>
|
|
455
455
|
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
456
456
|
{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-[#
|
|
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-[#
|
|
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-[#
|
|
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 {
|
|
10
|
-
export {
|
|
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";
|
|
@@ -13,11 +13,11 @@ import AssetBrowser from "../AssetBrowser";
|
|
|
13
13
|
|
|
14
14
|
/** Base input class: gray bg, no border, border on focus */
|
|
15
15
|
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-[#
|
|
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-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]";
|
|
17
17
|
|
|
18
18
|
/** Select class — same as input */
|
|
19
19
|
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-[#
|
|
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-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]";
|
|
21
21
|
|
|
22
22
|
// ============================================
|
|
23
23
|
// Hooks
|
|
@@ -47,8 +47,8 @@ export function ViewportBadge() {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
|
-
<div className="flex items-center gap-1.5 px-3 py-1.5 mb-2 rounded-lg bg-[#
|
|
51
|
-
<span className="text-[11px] font-medium text-[#
|
|
50
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 mb-2 rounded-lg bg-[#3580f9]/8 border border-[#3580f9]/15">
|
|
51
|
+
<span className="text-[11px] font-medium text-[#3580f9]">
|
|
52
52
|
Editing {labels[viewport]} overrides
|
|
53
53
|
</span>
|
|
54
54
|
</div>
|
|
@@ -88,7 +88,7 @@ export function ResponsiveField({
|
|
|
88
88
|
<span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
|
|
89
89
|
)}
|
|
90
90
|
{isOverridden && (
|
|
91
|
-
<span className="block text-[9px] text-[#
|
|
91
|
+
<span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
|
|
92
92
|
)}
|
|
93
93
|
</label>
|
|
94
94
|
<div className="flex-1 min-w-0">
|
|
@@ -291,7 +291,7 @@ export function StyledCheckbox({
|
|
|
291
291
|
type="button"
|
|
292
292
|
onClick={() => onChange(!checked)}
|
|
293
293
|
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
294
|
-
checked ? "bg-[#
|
|
294
|
+
checked ? "bg-[#3580f9]" : "bg-neutral-200 group-hover:bg-neutral-300"
|
|
295
295
|
}`}
|
|
296
296
|
>
|
|
297
297
|
<span
|
|
@@ -320,7 +320,7 @@ export function AssetPathInput({
|
|
|
320
320
|
onChange: (value: string) => void;
|
|
321
321
|
onFocus?: () => void;
|
|
322
322
|
placeholder?: string;
|
|
323
|
-
filterType?: "image" | "video" | "all";
|
|
323
|
+
filterType?: "image" | "video" | "audio" | "all";
|
|
324
324
|
}) {
|
|
325
325
|
const [browserOpen, setBrowserOpen] = useState(false);
|
|
326
326
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
|
|
4
|
+
import type { AudioBlock } from "../../../lib/sanity/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* LiveAudioPreview — Static preview for builder canvas.
|
|
8
|
+
*
|
|
9
|
+
* Same layout as the runtime renderer but no audio element / no playback —
|
|
10
|
+
* a frozen snapshot with a 0% progress bar, a play glyph, and a dummy
|
|
11
|
+
* `0:00 / 0:00` time label. Metadata (title / artist) and cover art
|
|
12
|
+
* render when present.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const widthStyleMap: Record<string, { width: string; margin?: string }> = {
|
|
16
|
+
full: { width: "100%" },
|
|
17
|
+
contained: { width: "75%", margin: "0 auto" },
|
|
18
|
+
small: { width: "50%", margin: "0 auto" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function LiveAudioPreview({ block }: { block: AudioBlock }) {
|
|
22
|
+
const accent = block.accent_color || "#3580f9";
|
|
23
|
+
const coverSrc = block.cover_path ? (adminThumbUrl(block.cover_path) || adminAssetUrl(block.cover_path)) : null;
|
|
24
|
+
|
|
25
|
+
const isFill = block.width === "fill";
|
|
26
|
+
const widthStyle = isFill ? { width: "100%" } : (widthStyleMap[block.width ?? "contained"] || widthStyleMap.contained);
|
|
27
|
+
|
|
28
|
+
const rawRadius = block.border_radius ? String(block.border_radius).replace(/[a-z%]+$/i, "") : "";
|
|
29
|
+
const borderRadius = rawRadius && !isNaN(Number(rawRadius)) ? `${rawRadius}px` : "12px";
|
|
30
|
+
|
|
31
|
+
const hasMetaText = !!(block.title || block.artist);
|
|
32
|
+
const hasAsset = !!block.asset_path;
|
|
33
|
+
|
|
34
|
+
const containerStyle: React.CSSProperties = {
|
|
35
|
+
...widthStyle,
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: 14,
|
|
39
|
+
padding: "12px 16px",
|
|
40
|
+
background: "#fafafa",
|
|
41
|
+
border: "1px solid #ececec",
|
|
42
|
+
borderRadius,
|
|
43
|
+
boxShadow: block.shadow ? "0 8px 24px -12px rgba(0,0,0,0.25)" : undefined,
|
|
44
|
+
overflow: "hidden",
|
|
45
|
+
opacity: hasAsset ? 1 : 0.75,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div style={containerStyle}>
|
|
50
|
+
{coverSrc ? (
|
|
51
|
+
<div style={{ width: 52, height: 52, flexShrink: 0, borderRadius: 8, overflow: "hidden", background: "#eee" }}>
|
|
52
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
53
|
+
<img src={coverSrc} alt={block.alt || block.title || ""} style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
|
|
54
|
+
</div>
|
|
55
|
+
) : null}
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
aria-hidden
|
|
59
|
+
style={{
|
|
60
|
+
width: 40,
|
|
61
|
+
height: 40,
|
|
62
|
+
flexShrink: 0,
|
|
63
|
+
borderRadius: "50%",
|
|
64
|
+
background: accent,
|
|
65
|
+
color: "#fff",
|
|
66
|
+
display: "flex",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginLeft: 2 }}>
|
|
72
|
+
<path d="M8 5v14l11-7z" />
|
|
73
|
+
</svg>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 4 }}>
|
|
77
|
+
{hasMetaText ? (
|
|
78
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 6, minWidth: 0 }}>
|
|
79
|
+
{block.title && (
|
|
80
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: "#111", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
81
|
+
{block.title}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
{block.artist && (
|
|
85
|
+
<span style={{ fontSize: 12, color: "#777", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
86
|
+
{block.artist}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
!hasAsset && (
|
|
92
|
+
<span style={{ fontSize: 11, color: "#8a8f98" }}>Audio — pick a file</span>
|
|
93
|
+
)
|
|
94
|
+
)}
|
|
95
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
96
|
+
<div style={{ flex: 1, height: 4, background: "#e5e5e5", borderRadius: 999, position: "relative" }}>
|
|
97
|
+
<div style={{ position: "absolute", inset: 0, width: "0%", background: accent, borderRadius: 999 }} />
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
position: "absolute",
|
|
101
|
+
top: "50%",
|
|
102
|
+
left: "0%",
|
|
103
|
+
width: 10,
|
|
104
|
+
height: 10,
|
|
105
|
+
marginTop: -5,
|
|
106
|
+
marginLeft: -5,
|
|
107
|
+
borderRadius: "50%",
|
|
108
|
+
background: "#fff",
|
|
109
|
+
boxShadow: `0 0 0 2px ${accent}`,
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<span style={{ fontSize: 11, color: "#777", fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>
|
|
114
|
+
0:00 / 0:00
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|