@morphika/andami 0.2.26 → 0.4.0
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/app/admin/pages/[slug]/page.tsx +41 -47
- package/app/api/admin/assets/scan/route.ts +40 -13
- package/app/api/admin/custom-sections/[slug]/route.ts +4 -1
- package/app/api/admin/custom-sections/route.ts +4 -1
- package/app/api/admin/pages/[slug]/route.ts +7 -1
- package/app/api/admin/pages/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +19 -1
- package/app/api/admin/r2/disconnect/route.ts +3 -0
- package/app/api/admin/r2/rename/route.ts +52 -13
- package/app/api/admin/r2/upload-url/route.ts +8 -1
- package/app/api/admin/settings/route.ts +4 -1
- package/app/api/admin/styles/route.ts +4 -1
- package/components/admin/styles/GridLayoutEditor.tsx +46 -46
- package/components/blocks/BlockRenderer.tsx +15 -2
- package/components/blocks/CoverSectionRenderer.tsx +75 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +17 -11
- package/components/blocks/ParallaxGroupRenderer.tsx +45 -10
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
- package/components/blocks/ShaderCanvas.tsx +10 -6
- package/components/builder/BlockCardIcons.tsx +227 -0
- package/components/builder/BlockLivePreview.tsx +5 -0
- package/components/builder/BlockTypePicker.tsx +36 -63
- package/components/builder/BuilderCanvas.tsx +6 -2
- package/components/builder/ColumnDragOverlay.tsx +3 -3
- package/components/builder/CoverRowResizeHandle.tsx +5 -2
- package/components/builder/CoverSectionCanvas.tsx +45 -52
- package/components/builder/DndWrapper.tsx +1 -1
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/ParallaxGroupCanvas.tsx +12 -71
- package/components/builder/ReadOnlyFrame.tsx +4 -23
- package/components/builder/SectionCardIcons.tsx +320 -0
- package/components/builder/SectionEditorBar.tsx +17 -12
- package/components/builder/SectionTypePicker.tsx +34 -138
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +19 -30
- package/components/builder/SettingsPanel.tsx +8 -32
- package/components/builder/SortableBlock.tsx +42 -50
- package/components/builder/SortableRow.tsx +207 -19
- package/components/builder/blockStyles.tsx +59 -180
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
- package/components/builder/editors/index.ts +1 -0
- package/components/builder/iconPrimitives.tsx +78 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
- package/components/builder/live-preview/index.ts +1 -0
- package/components/builder/settings-panel/BlockSettings.tsx +7 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/assets.ts +17 -2
- package/lib/builder/block-registrations.ts +268 -0
- package/lib/builder/block-registry.ts +195 -0
- package/lib/builder/constants.ts +22 -15
- package/lib/builder/defaults.ts +21 -0
- package/lib/builder/format.ts +25 -0
- package/lib/builder/history.ts +0 -3
- package/lib/builder/index.ts +16 -0
- package/lib/builder/layout-styles.ts +1 -1
- package/lib/builder/registry.ts +44 -0
- package/lib/builder/section-visibility.ts +36 -0
- package/lib/builder/serializer/normalizers.ts +15 -6
- package/lib/builder/serializer/serializers.ts +3 -3
- package/lib/builder/store-blocks.ts +16 -9
- package/lib/builder/store-cover.ts +76 -8
- package/lib/builder/store-sections.ts +1 -1
- package/lib/builder/store.ts +0 -2
- package/lib/builder/types.ts +9 -5
- package/lib/csrf.ts +31 -0
- package/lib/sanity/types.ts +54 -2
- package/lib/security.ts +50 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/index.ts +2 -1
- package/sanity/schemas/blocks/projectCarouselBlock.ts +218 -0
- package/sanity/schemas/index.ts +4 -1
- package/sanity/schemas/objects/coverSection.ts +35 -3
- package/sanity/schemas/pageSectionV2.ts +1 -0
- package/components/builder/ParallaxSlideHeader.tsx +0 -113
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
import { blockLayoutField, blockAnimationFields } from "./blockLayout";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* projectCarouselBlock — horizontal carousel of projects.
|
|
6
|
+
*
|
|
7
|
+
* Intended use: "keep browsing" row at the end of a project page, but usable
|
|
8
|
+
* on any page. Projects are pulled automatically (latest / random) — no
|
|
9
|
+
* manual selection list. When placed on /work/[slug], the current project
|
|
10
|
+
* can be filtered out automatically via `exclude_current`.
|
|
11
|
+
*
|
|
12
|
+
* Treated as a **section-level block** (same semantics as projectGridBlock):
|
|
13
|
+
* it lives in a full-width column of a pageSectionV2 and is added via the
|
|
14
|
+
* "+ Add Section" modal, not the "+ Add Block" picker.
|
|
15
|
+
*
|
|
16
|
+
* Intentionally independent from projectGridBlock — the two share visual
|
|
17
|
+
* primitives (thumbnail resolution, project data shape) but no coupled
|
|
18
|
+
* code, so changes on one side never break the other.
|
|
19
|
+
*/
|
|
20
|
+
export const projectCarouselBlock = defineType({
|
|
21
|
+
name: "projectCarouselBlock",
|
|
22
|
+
title: "Project Carousel",
|
|
23
|
+
type: "object",
|
|
24
|
+
fields: [
|
|
25
|
+
// ─── Source ───
|
|
26
|
+
defineField({
|
|
27
|
+
name: "source_mode",
|
|
28
|
+
title: "Source",
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "How projects are selected for the carousel",
|
|
31
|
+
options: {
|
|
32
|
+
list: [
|
|
33
|
+
{ title: "Latest projects", value: "auto_latest" },
|
|
34
|
+
{ title: "Random projects", value: "auto_random" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
initialValue: "auto_latest",
|
|
38
|
+
}),
|
|
39
|
+
defineField({
|
|
40
|
+
name: "max_projects",
|
|
41
|
+
title: "Number of projects",
|
|
42
|
+
type: "number",
|
|
43
|
+
description: "How many projects to show (2–20)",
|
|
44
|
+
initialValue: 8,
|
|
45
|
+
validation: (Rule) => Rule.min(2).max(20).integer(),
|
|
46
|
+
}),
|
|
47
|
+
defineField({
|
|
48
|
+
name: "exclude_current",
|
|
49
|
+
title: "Exclude current project",
|
|
50
|
+
type: "boolean",
|
|
51
|
+
description:
|
|
52
|
+
"When the carousel is on a /work/[slug] page, automatically filter out the project currently being viewed",
|
|
53
|
+
initialValue: true,
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
// ─── Layout ───
|
|
57
|
+
defineField({
|
|
58
|
+
name: "cards_per_view_desktop",
|
|
59
|
+
title: "Cards per view (desktop)",
|
|
60
|
+
type: "number",
|
|
61
|
+
description: "Supports fractional values — e.g. 3.5 shows half of the next card to hint 'more content'",
|
|
62
|
+
initialValue: 3.5,
|
|
63
|
+
validation: (Rule) => Rule.min(1).max(6),
|
|
64
|
+
}),
|
|
65
|
+
defineField({
|
|
66
|
+
name: "cards_per_view_tablet",
|
|
67
|
+
title: "Cards per view (tablet)",
|
|
68
|
+
type: "number",
|
|
69
|
+
initialValue: 2.2,
|
|
70
|
+
validation: (Rule) => Rule.min(1).max(4),
|
|
71
|
+
}),
|
|
72
|
+
defineField({
|
|
73
|
+
name: "cards_per_view_phone",
|
|
74
|
+
title: "Cards per view (phone)",
|
|
75
|
+
type: "number",
|
|
76
|
+
initialValue: 1.2,
|
|
77
|
+
validation: (Rule) => Rule.min(1).max(3),
|
|
78
|
+
}),
|
|
79
|
+
defineField({
|
|
80
|
+
name: "gap",
|
|
81
|
+
title: "Gap between cards (px)",
|
|
82
|
+
type: "number",
|
|
83
|
+
initialValue: 16,
|
|
84
|
+
validation: (Rule) => Rule.min(0).max(80),
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
// ─── Card Display ───
|
|
88
|
+
defineField({
|
|
89
|
+
name: "aspect_ratio",
|
|
90
|
+
title: "Card aspect ratio",
|
|
91
|
+
type: "string",
|
|
92
|
+
options: {
|
|
93
|
+
list: [
|
|
94
|
+
{ title: "Landscape (16:9)", value: "16/9" },
|
|
95
|
+
{ title: "Standard (4:3)", value: "4/3" },
|
|
96
|
+
{ title: "Square (1:1)", value: "1/1" },
|
|
97
|
+
{ title: "Portrait (3:4)", value: "3/4" },
|
|
98
|
+
{ title: "Tall (9:16)", value: "9/16" },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
initialValue: "4/3",
|
|
102
|
+
}),
|
|
103
|
+
defineField({
|
|
104
|
+
name: "show_title",
|
|
105
|
+
title: "Show title",
|
|
106
|
+
type: "boolean",
|
|
107
|
+
initialValue: true,
|
|
108
|
+
}),
|
|
109
|
+
defineField({
|
|
110
|
+
name: "show_subtitle",
|
|
111
|
+
title: "Show subtitle",
|
|
112
|
+
type: "boolean",
|
|
113
|
+
initialValue: false,
|
|
114
|
+
}),
|
|
115
|
+
defineField({
|
|
116
|
+
name: "border_radius",
|
|
117
|
+
title: "Border radius (px)",
|
|
118
|
+
type: "number",
|
|
119
|
+
initialValue: 0,
|
|
120
|
+
}),
|
|
121
|
+
defineField({
|
|
122
|
+
name: "hover_effect",
|
|
123
|
+
title: "Hover effect",
|
|
124
|
+
type: "string",
|
|
125
|
+
options: {
|
|
126
|
+
list: [
|
|
127
|
+
{ title: "Scale", value: "scale" },
|
|
128
|
+
{ title: "None", value: "none" },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
initialValue: "scale",
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
// ─── Video ───
|
|
135
|
+
defineField({
|
|
136
|
+
name: "video_mode",
|
|
137
|
+
title: "Video mode",
|
|
138
|
+
type: "string",
|
|
139
|
+
options: {
|
|
140
|
+
list: [
|
|
141
|
+
{ title: "Off", value: "off" },
|
|
142
|
+
{ title: "Hover", value: "hover" },
|
|
143
|
+
{ title: "Autoloop", value: "autoloop" },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
initialValue: "off",
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
// ─── Controls ───
|
|
150
|
+
defineField({
|
|
151
|
+
name: "show_arrows",
|
|
152
|
+
title: "Show prev / next arrows",
|
|
153
|
+
type: "boolean",
|
|
154
|
+
initialValue: true,
|
|
155
|
+
}),
|
|
156
|
+
defineField({
|
|
157
|
+
name: "show_dots",
|
|
158
|
+
title: "Show pagination dots",
|
|
159
|
+
type: "boolean",
|
|
160
|
+
initialValue: false,
|
|
161
|
+
}),
|
|
162
|
+
defineField({
|
|
163
|
+
name: "snap_scroll",
|
|
164
|
+
title: "Snap scrolling",
|
|
165
|
+
type: "boolean",
|
|
166
|
+
description: "Cards snap into place as the user scrolls / swipes",
|
|
167
|
+
initialValue: true,
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
// ─── Card entrance animation ───
|
|
171
|
+
defineField({
|
|
172
|
+
name: "card_entrance",
|
|
173
|
+
title: "Card Entrance Animation",
|
|
174
|
+
type: "object",
|
|
175
|
+
fields: [
|
|
176
|
+
defineField({ name: "enabled", title: "Enabled", type: "boolean", initialValue: false }),
|
|
177
|
+
defineField({
|
|
178
|
+
name: "preset",
|
|
179
|
+
title: "Preset",
|
|
180
|
+
type: "string",
|
|
181
|
+
options: {
|
|
182
|
+
list: [
|
|
183
|
+
{ title: "Fade", value: "fade" },
|
|
184
|
+
{ title: "Slide Up", value: "slide-up" },
|
|
185
|
+
{ title: "Scale", value: "scale" },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
initialValue: "slide-up",
|
|
189
|
+
}),
|
|
190
|
+
defineField({
|
|
191
|
+
name: "stagger_delay",
|
|
192
|
+
title: "Stagger Delay",
|
|
193
|
+
type: "number",
|
|
194
|
+
description: "Delay between cards (ms)",
|
|
195
|
+
initialValue: 80,
|
|
196
|
+
}),
|
|
197
|
+
defineField({
|
|
198
|
+
name: "duration",
|
|
199
|
+
title: "Duration",
|
|
200
|
+
type: "number",
|
|
201
|
+
description: "Animation duration (ms)",
|
|
202
|
+
initialValue: 500,
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
// ─── Standard block fields ───
|
|
208
|
+
...blockAnimationFields,
|
|
209
|
+
blockLayoutField,
|
|
210
|
+
],
|
|
211
|
+
preview: {
|
|
212
|
+
select: { mode: "source_mode", n: "max_projects" },
|
|
213
|
+
prepare({ mode, n }: { mode?: string; n?: number }) {
|
|
214
|
+
const modeLabel = mode === "auto_random" ? "Random" : "Latest";
|
|
215
|
+
return { title: `Project Carousel (${modeLabel} · ${n ?? 8})` };
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
package/sanity/schemas/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
spacerBlock,
|
|
20
20
|
buttonBlock,
|
|
21
21
|
projectGridBlock,
|
|
22
|
+
projectCarouselBlock,
|
|
22
23
|
} from "./blocks";
|
|
23
24
|
|
|
24
25
|
// Re-export individual schemas for granular use by instances
|
|
@@ -69,6 +70,7 @@ export {
|
|
|
69
70
|
spacerBlock,
|
|
70
71
|
buttonBlock,
|
|
71
72
|
projectGridBlock,
|
|
73
|
+
projectCarouselBlock,
|
|
72
74
|
} from "./blocks";
|
|
73
75
|
|
|
74
76
|
export const schemaTypes = [
|
|
@@ -91,7 +93,7 @@ export const schemaTypes = [
|
|
|
91
93
|
parallaxGroup, // Parallax V2 group (Session 123)
|
|
92
94
|
coverSection, // Cover Section — proportional rows (Session 176)
|
|
93
95
|
|
|
94
|
-
// Blocks (
|
|
96
|
+
// Blocks (9)
|
|
95
97
|
textBlock,
|
|
96
98
|
imageBlock,
|
|
97
99
|
imageGridBlock,
|
|
@@ -99,6 +101,7 @@ export const schemaTypes = [
|
|
|
99
101
|
spacerBlock,
|
|
100
102
|
buttonBlock,
|
|
101
103
|
projectGridBlock,
|
|
104
|
+
projectCarouselBlock,
|
|
102
105
|
];
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -81,10 +81,31 @@ const responsiveRowOverrideFields = [
|
|
|
81
81
|
type: "object",
|
|
82
82
|
name: "coverRowOverride",
|
|
83
83
|
fields: [
|
|
84
|
-
defineField({
|
|
84
|
+
defineField({
|
|
85
|
+
name: "height_percent",
|
|
86
|
+
title: "Height %",
|
|
87
|
+
type: "number",
|
|
88
|
+
// [10, 95] matches the resize handle's MIN_ROW_PERCENT in
|
|
89
|
+
// store-cover.ts. Letting the schema accept values below 10
|
|
90
|
+
// created rows that the UI resizer could not edit without
|
|
91
|
+
// an unexpected normalization jump.
|
|
92
|
+
validation: (Rule) => Rule.min(10).max(95),
|
|
93
|
+
}),
|
|
85
94
|
],
|
|
86
95
|
},
|
|
87
96
|
],
|
|
97
|
+
// Sum-to-100 invariant — applied across the array of overrides.
|
|
98
|
+
validation: (Rule) =>
|
|
99
|
+
Rule.custom((rows: unknown) => {
|
|
100
|
+
if (!Array.isArray(rows) || rows.length === 0) return true; // nothing to check
|
|
101
|
+
const total = rows.reduce((acc: number, row: unknown) => {
|
|
102
|
+
const pct = (row as { height_percent?: number } | null)?.height_percent;
|
|
103
|
+
return typeof pct === "number" ? acc + pct : acc;
|
|
104
|
+
}, 0);
|
|
105
|
+
// Allow ±1% drift for rounding artefacts from the resize handle.
|
|
106
|
+
if (Math.abs(total - 100) <= 1) return true;
|
|
107
|
+
return `Row heights must sum to 100% (got ${total.toFixed(1)}%)`;
|
|
108
|
+
}),
|
|
88
109
|
}),
|
|
89
110
|
];
|
|
90
111
|
|
|
@@ -174,6 +195,16 @@ export default defineType({
|
|
|
174
195
|
validation: (Rule) => Rule.min(0).max(100),
|
|
175
196
|
}),
|
|
176
197
|
|
|
198
|
+
// ── Navbar Color Override ──
|
|
199
|
+
defineField({
|
|
200
|
+
name: "nav_color",
|
|
201
|
+
title: "Navbar Color",
|
|
202
|
+
type: "string",
|
|
203
|
+
description:
|
|
204
|
+
"Hex color applied to navbar text while this cover section is on screen. " +
|
|
205
|
+
"Clears when the next section takes over.",
|
|
206
|
+
}),
|
|
207
|
+
|
|
177
208
|
// ── Section Height ──
|
|
178
209
|
defineField({
|
|
179
210
|
name: "height",
|
|
@@ -184,6 +215,7 @@ export default defineType({
|
|
|
184
215
|
{ title: "Full Viewport (100vh)", value: "100vh" },
|
|
185
216
|
{ title: "80% Viewport (80vh)", value: "80vh" },
|
|
186
217
|
{ title: "50% Viewport (50vh)", value: "50vh" },
|
|
218
|
+
{ title: "20% Viewport (20vh)", value: "20vh" },
|
|
187
219
|
],
|
|
188
220
|
},
|
|
189
221
|
initialValue: "100vh",
|
|
@@ -204,8 +236,8 @@ export default defineType({
|
|
|
204
236
|
name: "height_percent",
|
|
205
237
|
title: "Height (%)",
|
|
206
238
|
type: "number",
|
|
207
|
-
description: "Row height as percentage of section (
|
|
208
|
-
validation: (Rule) => Rule.required().min(
|
|
239
|
+
description: "Row height as percentage of section (10–95)",
|
|
240
|
+
validation: (Rule) => Rule.required().min(10).max(95),
|
|
209
241
|
}),
|
|
210
242
|
defineField({
|
|
211
243
|
name: "vertical_align",
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useBuilderStore } from "../../lib/builder/store";
|
|
4
|
-
import type { ParallaxSlideV2 } from "../../lib/sanity/types";
|
|
5
|
-
import { BUILDER_GREEN } from "../../lib/builder/constants";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* ParallaxSlideHeader — thin header bar for each slide in a parallax group.
|
|
9
|
-
* Shows slide index, background preview thumbnail, settings icon,
|
|
10
|
-
* reorder arrows, and delete button.
|
|
11
|
-
*
|
|
12
|
-
* Session 123: Parallax V2 Phase 2
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
interface ParallaxSlideHeaderProps {
|
|
16
|
-
slide: ParallaxSlideV2;
|
|
17
|
-
slideIndex: number;
|
|
18
|
-
totalSlides: number;
|
|
19
|
-
groupKey: string;
|
|
20
|
-
isSelected: boolean;
|
|
21
|
-
onSelect: () => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default function ParallaxSlideHeader({
|
|
25
|
-
slide,
|
|
26
|
-
slideIndex,
|
|
27
|
-
totalSlides,
|
|
28
|
-
groupKey,
|
|
29
|
-
isSelected,
|
|
30
|
-
onSelect,
|
|
31
|
-
}: ParallaxSlideHeaderProps) {
|
|
32
|
-
const store = useBuilderStore();
|
|
33
|
-
const hasBg = slide.background_type === "image"
|
|
34
|
-
? !!slide.background_image
|
|
35
|
-
: !!slide.background_video;
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div
|
|
39
|
-
className="flex items-center gap-2 px-3 py-1.5 rounded-t-lg cursor-pointer select-none transition-colors"
|
|
40
|
-
style={{
|
|
41
|
-
background: isSelected
|
|
42
|
-
? "linear-gradient(135deg, #c8a8ff 0%, #d8b8ff 100%)"
|
|
43
|
-
: "#f5f0ff",
|
|
44
|
-
borderBottom: "1px solid rgba(139, 92, 246, 0.15)",
|
|
45
|
-
}}
|
|
46
|
-
onClick={(e) => {
|
|
47
|
-
e.stopPropagation();
|
|
48
|
-
onSelect();
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{/* Slide number */}
|
|
52
|
-
<span
|
|
53
|
-
className="flex items-center justify-center rounded-md text-[10px] font-bold text-white min-w-[22px] h-[22px] px-1"
|
|
54
|
-
style={{ background: "#8b5cf6" }}
|
|
55
|
-
>
|
|
56
|
-
{slideIndex + 1}
|
|
57
|
-
</span>
|
|
58
|
-
|
|
59
|
-
{/* Background type indicator */}
|
|
60
|
-
<span className="text-[10px] text-neutral-500 uppercase tracking-wider font-medium">
|
|
61
|
-
{slide.background_type === "video" ? "Video BG" : "Image BG"}
|
|
62
|
-
{hasBg && <span className="ml-1 text-green-500">●</span>}
|
|
63
|
-
</span>
|
|
64
|
-
|
|
65
|
-
{/* Spacer */}
|
|
66
|
-
<div className="flex-1" />
|
|
67
|
-
|
|
68
|
-
{/* Reorder arrows */}
|
|
69
|
-
<button
|
|
70
|
-
className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-30 transition-colors"
|
|
71
|
-
disabled={slideIndex === 0}
|
|
72
|
-
onClick={(e) => {
|
|
73
|
-
e.stopPropagation();
|
|
74
|
-
store.moveParallaxSlide(groupKey, slide._key, "up");
|
|
75
|
-
}}
|
|
76
|
-
title="Move slide up"
|
|
77
|
-
>
|
|
78
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
79
|
-
<path d="M6 3L9 7H3L6 3Z" fill="currentColor" />
|
|
80
|
-
</svg>
|
|
81
|
-
</button>
|
|
82
|
-
<button
|
|
83
|
-
className="p-0.5 text-neutral-400 hover:text-neutral-700 disabled:opacity-30 transition-colors"
|
|
84
|
-
disabled={slideIndex === totalSlides - 1}
|
|
85
|
-
onClick={(e) => {
|
|
86
|
-
e.stopPropagation();
|
|
87
|
-
store.moveParallaxSlide(groupKey, slide._key, "down");
|
|
88
|
-
}}
|
|
89
|
-
title="Move slide down"
|
|
90
|
-
>
|
|
91
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
92
|
-
<path d="M6 9L3 5H9L6 9Z" fill="currentColor" />
|
|
93
|
-
</svg>
|
|
94
|
-
</button>
|
|
95
|
-
|
|
96
|
-
{/* Delete slide */}
|
|
97
|
-
{totalSlides > 1 && (
|
|
98
|
-
<button
|
|
99
|
-
className="p-0.5 text-neutral-400 hover:text-red-500 transition-colors"
|
|
100
|
-
onClick={(e) => {
|
|
101
|
-
e.stopPropagation();
|
|
102
|
-
store.removeParallaxSlide(groupKey, slide._key);
|
|
103
|
-
}}
|
|
104
|
-
title="Delete slide"
|
|
105
|
-
>
|
|
106
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
107
|
-
<path d="M3 3L9 9M9 3L3 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
108
|
-
</svg>
|
|
109
|
-
</button>
|
|
110
|
-
)}
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|