@morphika/andami 0.5.0 → 0.5.1

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 (29) hide show
  1. package/README.md +151 -36
  2. package/app/admin/layout.tsx +145 -152
  3. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  4. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  5. package/components/builder/BlockCardIcons.tsx +89 -0
  6. package/components/builder/BlockTypePicker.tsx +2 -0
  7. package/components/builder/CoverSectionCanvas.tsx +90 -2
  8. package/components/builder/SectionV2Canvas.tsx +19 -3
  9. package/components/builder/SectionV2Column.tsx +5 -1
  10. package/components/builder/asset-browser/R2BrowserContent.tsx +23 -6
  11. package/components/builder/asset-browser/helpers.ts +4 -0
  12. package/components/builder/asset-browser/types.ts +2 -1
  13. package/components/builder/blockStyles.tsx +12 -0
  14. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  15. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  16. package/components/builder/editors/shared.tsx +1 -1
  17. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  18. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  19. package/lib/animation/enter-types.ts +2 -0
  20. package/lib/animation/hover-effect-types.ts +2 -0
  21. package/lib/builder/block-registrations.ts +83 -1
  22. package/lib/builder/types.ts +2 -0
  23. package/lib/sanity/types.ts +58 -0
  24. package/lib/version.ts +1 -1
  25. package/package.json +1 -1
  26. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  27. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  28. package/sanity/schemas/blocks/index.ts +3 -1
  29. package/sanity/schemas/index.ts +7 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Block registrations — fills the registry defined in `./block-registry.ts`
5
- * with the 8 built-in block types shipped by the framework.
5
+ * with the 10 built-in block types shipped by the framework.
6
6
  *
7
7
  * Importing this module has a side effect: every `registerBlockType()`
8
8
  * call below runs synchronously and populates the module-level registry.
@@ -26,6 +26,8 @@ import type {
26
26
  VideoBlock,
27
27
  SpacerBlock,
28
28
  ButtonBlock,
29
+ BeforeAfterBlock,
30
+ AudioBlock,
29
31
  ProjectGridBlock,
30
32
  ProjectCarouselBlock,
31
33
  } from "../sanity/types";
@@ -39,6 +41,8 @@ import {
39
41
  videoBlock,
40
42
  spacerBlock,
41
43
  buttonBlock,
44
+ beforeAfterBlock,
45
+ audioBlock,
42
46
  projectGridBlock,
43
47
  projectCarouselBlock,
44
48
  } from "../../sanity/schemas/blocks";
@@ -51,6 +55,8 @@ import ImageGridBlockRenderer from "../../components/blocks/ImageGridBlockRender
51
55
  import VideoBlockRenderer from "../../components/blocks/VideoBlockRenderer";
52
56
  import SpacerBlockRenderer from "../../components/blocks/SpacerBlockRenderer";
53
57
  import ButtonBlockRenderer from "../../components/blocks/ButtonBlockRenderer";
58
+ import BeforeAfterBlockRenderer from "../../components/blocks/BeforeAfterBlockRenderer";
59
+ import AudioBlockRenderer from "../../components/blocks/AudioBlockRenderer";
54
60
  import ProjectGridBlockRenderer from "../../components/blocks/ProjectGridBlockRenderer";
55
61
  import ProjectCarouselBlockRenderer from "../../components/blocks/ProjectCarouselBlockRenderer";
56
62
 
@@ -62,6 +68,8 @@ import LiveImageGridPreview from "../../components/builder/live-preview/LiveImag
62
68
  import LiveVideoPreview from "../../components/builder/live-preview/LiveVideoPreview";
63
69
  import LiveSpacerPreview from "../../components/builder/live-preview/LiveSpacerPreview";
64
70
  import LiveButtonPreview from "../../components/builder/live-preview/LiveButtonPreview";
71
+ import LiveBeforeAfterPreview from "../../components/builder/live-preview/LiveBeforeAfterPreview";
72
+ import LiveAudioPreview from "../../components/builder/live-preview/LiveAudioPreview";
65
73
  import LiveProjectGridPreview from "../../components/builder/live-preview/LiveProjectGridPreview";
66
74
  import LiveProjectCarouselPreview from "../../components/builder/live-preview/LiveProjectCarouselPreview";
67
75
 
@@ -73,6 +81,8 @@ import ImageGridBlockEditor from "../../components/builder/editors/ImageGridBloc
73
81
  import VideoBlockEditor from "../../components/builder/editors/VideoBlockEditor";
74
82
  import SpacerBlockEditor from "../../components/builder/editors/SpacerBlockEditor";
75
83
  import ButtonBlockEditor from "../../components/builder/editors/ButtonBlockEditor";
84
+ import BeforeAfterBlockEditor from "../../components/builder/editors/BeforeAfterBlockEditor";
85
+ import AudioBlockEditor from "../../components/builder/editors/AudioBlockEditor";
76
86
  import ProjectGridEditor from "../../components/builder/editors/ProjectGridEditor";
77
87
  import ProjectCarouselBlockEditor from "../../components/builder/editors/ProjectCarouselBlockEditor";
78
88
 
@@ -85,6 +95,8 @@ import {
85
95
  VideoBlockCardIcon,
86
96
  SpacerBlockCardIcon,
87
97
  ButtonBlockCardIcon,
98
+ BeforeAfterBlockCardIcon,
99
+ AudioBlockCardIcon,
88
100
  } from "../../components/builder/BlockCardIcons";
89
101
  import {
90
102
  ProjectGridCardIcon,
@@ -100,6 +112,8 @@ import {
100
112
  VideoBlockIcon,
101
113
  SpacerBlockIcon,
102
114
  ButtonBlockIcon,
115
+ BeforeAfterBlockIcon,
116
+ AudioBlockIcon,
103
117
  ProjectGridBlockIcon,
104
118
  ProjectCarouselBlockIcon,
105
119
  } from "../../components/builder/blockStyles";
@@ -267,6 +281,74 @@ registerBlockType<ButtonBlock>({
267
281
  hoverPresets: ["scale-up", "lift", "border-glow"],
268
282
  });
269
283
 
284
+ registerBlockType<BeforeAfterBlock>({
285
+ type: "beforeAfterBlock",
286
+ label: "Before / After",
287
+ description: "Drag-slider comparison between two images or videos",
288
+ category: "content",
289
+ iconGlyph: "◫",
290
+ schema: beforeAfterBlock,
291
+ defaultFactory: (key) => ({
292
+ _type: "beforeAfterBlock",
293
+ _key: key,
294
+ before_media_type: "image",
295
+ before_asset_path: "",
296
+ before_alt: "",
297
+ after_media_type: "image",
298
+ after_asset_path: "",
299
+ after_alt: "",
300
+ orientation: "horizontal",
301
+ initial_position: 50,
302
+ handle_color: "#FFFFFF",
303
+ width: "full",
304
+ aspect_ratio: "16:9",
305
+ video_autoplay: true,
306
+ video_loop: true,
307
+ video_muted: true,
308
+ border_radius: "",
309
+ shadow: false,
310
+ }),
311
+ renderer: BeforeAfterBlockRenderer as React.ComponentType<{ block: BeforeAfterBlock }>,
312
+ livePreview: LiveBeforeAfterPreview as unknown as React.ComponentType<{ block: BeforeAfterBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
313
+ editor: BeforeAfterBlockEditor as React.ComponentType<{ block: BeforeAfterBlock }>,
314
+ cardIcon: BeforeAfterBlockCardIcon,
315
+ compactIcon: BeforeAfterBlockIcon,
316
+ enterPresets: ["fade", "slide-up", "scale"],
317
+ hoverPresets: [],
318
+ });
319
+
320
+ registerBlockType<AudioBlock>({
321
+ type: "audioBlock",
322
+ label: "Audio",
323
+ description: "Minimal audio player with cover art and metadata",
324
+ category: "content",
325
+ iconGlyph: "♪",
326
+ schema: audioBlock,
327
+ defaultFactory: (key) => ({
328
+ _type: "audioBlock",
329
+ _key: key,
330
+ asset_path: "",
331
+ alt: "",
332
+ title: "",
333
+ artist: "",
334
+ cover_path: "",
335
+ accent_color: "#4794E2",
336
+ width: "contained",
337
+ border_radius: "",
338
+ shadow: false,
339
+ autoplay: false,
340
+ loop: false,
341
+ muted: false,
342
+ }),
343
+ renderer: AudioBlockRenderer as React.ComponentType<{ block: AudioBlock }>,
344
+ livePreview: LiveAudioPreview as unknown as React.ComponentType<{ block: AudioBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
345
+ editor: AudioBlockEditor as React.ComponentType<{ block: AudioBlock }>,
346
+ cardIcon: AudioBlockCardIcon,
347
+ compactIcon: AudioBlockIcon,
348
+ enterPresets: ["fade", "slide-up", "scale"],
349
+ hoverPresets: [],
350
+ });
351
+
270
352
  // ── Section-level blocks ──
271
353
 
272
354
  registerBlockType<ProjectGridBlock>({
@@ -51,6 +51,8 @@ export const BLOCK_TYPE_REGISTRY: BlockTypeInfo[] = [
51
51
  { type: "videoBlock", label: "Video", description: "Vimeo, YouTube, or MP4", group: "generic", icon: "▶", category: "content" },
52
52
  { type: "spacerBlock", label: "Spacer", description: "Vertical spacing", group: "generic", icon: "↕", category: "content" },
53
53
  { type: "buttonBlock", label: "Button", description: "Call-to-action button", group: "generic", icon: "▣", category: "content" },
54
+ { type: "beforeAfterBlock", label: "Before / After", description: "Drag-slider comparison between two images or videos", group: "generic", icon: "◫", category: "content" },
55
+ { type: "audioBlock", label: "Audio", description: "Minimal audio player with cover art and metadata", group: "generic", icon: "♪", category: "content" },
54
56
  ];
55
57
 
56
58
  /**
@@ -210,6 +210,62 @@ export interface ButtonBlock {
210
210
  responsive?: ResponsiveOverrides<ButtonBlock>;
211
211
  }
212
212
 
213
+ export interface BeforeAfterBlock {
214
+ _type: "beforeAfterBlock";
215
+ _key: string;
216
+ // Before side
217
+ before_media_type?: "image" | "video";
218
+ before_asset_path: string;
219
+ before_alt?: string;
220
+ // After side
221
+ after_media_type?: "image" | "video";
222
+ after_asset_path: string;
223
+ after_alt?: string;
224
+ // Slider
225
+ orientation?: "horizontal" | "vertical";
226
+ initial_position?: number; // 0–100
227
+ handle_color?: string; // hex
228
+ // Layout
229
+ width?: "full" | "contained" | "small" | "fill";
230
+ aspect_ratio?: "auto" | "16:9" | "4:3" | "1:1" | "21:9";
231
+ // Video playback (applies when either side is video)
232
+ video_autoplay?: boolean;
233
+ video_loop?: boolean;
234
+ video_muted?: boolean;
235
+ // Appearance
236
+ border_radius?: string;
237
+ shadow?: boolean;
238
+ enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
239
+ hover_effect?: import("../../lib/animation/hover-effect-types").HoverEffectConfig;
240
+ layout?: BlockLayout;
241
+ responsive?: ResponsiveOverrides<BeforeAfterBlock>;
242
+ }
243
+
244
+ export interface AudioBlock {
245
+ _type: "audioBlock";
246
+ _key: string;
247
+ // Source
248
+ asset_path: string;
249
+ alt?: string;
250
+ // Metadata
251
+ title?: string;
252
+ artist?: string;
253
+ cover_path?: string;
254
+ // Appearance
255
+ accent_color?: string;
256
+ width?: "full" | "contained" | "small" | "fill";
257
+ border_radius?: string;
258
+ shadow?: boolean;
259
+ // Playback
260
+ autoplay?: boolean;
261
+ loop?: boolean;
262
+ muted?: boolean;
263
+ enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
264
+ hover_effect?: import("../../lib/animation/hover-effect-types").HoverEffectConfig;
265
+ layout?: BlockLayout;
266
+ responsive?: ResponsiveOverrides<AudioBlock>;
267
+ }
268
+
213
269
  // ============================================
214
270
  // Project Grid Block v2 (template-only, Session 105)
215
271
  // ============================================
@@ -635,6 +691,8 @@ export type ContentBlock =
635
691
  | VideoBlock
636
692
  | SpacerBlock
637
693
  | ButtonBlock
694
+ | BeforeAfterBlock
695
+ | AudioBlock
638
696
  | ProjectGridBlock
639
697
  | ProjectCarouselBlock;
640
698
 
package/lib/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * Exposed as a plain constant so it can be imported without reading
7
7
  * package.json at runtime.
8
8
  */
9
- export const ANDAMI_VERSION = "0.5.0";
9
+ export const ANDAMI_VERSION = "0.5.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,69 @@
1
+ import { defineField, defineType } from "sanity";
2
+ import { blockLayoutField, blockAnimationFields } from "./blockLayout";
3
+
4
+ export const audioBlock = defineType({
5
+ name: "audioBlock",
6
+ title: "Audio Block",
7
+ type: "object",
8
+ fields: [
9
+ // ── Source ──
10
+ defineField({
11
+ name: "asset_path",
12
+ title: "Audio File",
13
+ type: "string",
14
+ description: "Relative path to the audio file (mp3, wav, ogg, m4a, aac, flac)",
15
+ validation: (Rule) => Rule.required(),
16
+ }),
17
+ defineField({ name: "alt", title: "Alt Text", type: "string" }),
18
+
19
+ // ── Metadata ──
20
+ defineField({ name: "title", title: "Title", type: "string" }),
21
+ defineField({ name: "artist", title: "Artist", type: "string" }),
22
+ defineField({
23
+ name: "cover_path",
24
+ title: "Cover Art",
25
+ type: "string",
26
+ description: "Optional relative path to a cover image",
27
+ }),
28
+
29
+ // ── Appearance ──
30
+ defineField({
31
+ name: "accent_color",
32
+ title: "Accent Color",
33
+ type: "string",
34
+ description: "Hex color for the play button + progress fill",
35
+ initialValue: "#4794E2",
36
+ }),
37
+ defineField({
38
+ name: "width",
39
+ title: "Width",
40
+ type: "string",
41
+ options: {
42
+ list: [
43
+ { title: "Full", value: "full" },
44
+ { title: "Contained", value: "contained" },
45
+ { title: "Small", value: "small" },
46
+ { title: "Fill", value: "fill" },
47
+ ],
48
+ },
49
+ initialValue: "contained",
50
+ }),
51
+ defineField({ name: "border_radius", title: "Border Radius", type: "string" }),
52
+ defineField({ name: "shadow", title: "Shadow", type: "boolean", initialValue: false }),
53
+
54
+ // ── Playback ──
55
+ defineField({ name: "autoplay", title: "Autoplay", type: "boolean", initialValue: false }),
56
+ defineField({ name: "loop", title: "Loop", type: "boolean", initialValue: false }),
57
+ defineField({ name: "muted", title: "Muted", type: "boolean", initialValue: false }),
58
+
59
+ ...blockAnimationFields,
60
+ blockLayoutField,
61
+ ],
62
+ preview: {
63
+ select: { path: "asset_path", title: "title", artist: "artist" },
64
+ prepare({ path, title, artist }) {
65
+ const label = title ? (artist ? `${title} — ${artist}` : title) : path || "Audio block";
66
+ return { title: label };
67
+ },
68
+ },
69
+ });
@@ -0,0 +1,121 @@
1
+ import { defineField, defineType } from "sanity";
2
+ import { blockLayoutField, blockAnimationFields } from "./blockLayout";
3
+
4
+ export const beforeAfterBlock = defineType({
5
+ name: "beforeAfterBlock",
6
+ title: "Before / After Block",
7
+ type: "object",
8
+ fields: [
9
+ // ── Before side ──
10
+ defineField({
11
+ name: "before_media_type",
12
+ title: "Before Media Type",
13
+ type: "string",
14
+ options: { list: [{ title: "Image", value: "image" }, { title: "Video", value: "video" }] },
15
+ initialValue: "image",
16
+ }),
17
+ defineField({
18
+ name: "before_asset_path",
19
+ title: "Before Asset Path",
20
+ type: "string",
21
+ description: "Relative path to the image or video file shown before the slider",
22
+ validation: (Rule) => Rule.required(),
23
+ }),
24
+ defineField({ name: "before_alt", title: "Before Alt Text", type: "string" }),
25
+
26
+ // ── After side ──
27
+ defineField({
28
+ name: "after_media_type",
29
+ title: "After Media Type",
30
+ type: "string",
31
+ options: { list: [{ title: "Image", value: "image" }, { title: "Video", value: "video" }] },
32
+ initialValue: "image",
33
+ }),
34
+ defineField({
35
+ name: "after_asset_path",
36
+ title: "After Asset Path",
37
+ type: "string",
38
+ description: "Relative path to the image or video file shown after the slider",
39
+ validation: (Rule) => Rule.required(),
40
+ }),
41
+ defineField({ name: "after_alt", title: "After Alt Text", type: "string" }),
42
+
43
+ // ── Slider behavior ──
44
+ defineField({
45
+ name: "orientation",
46
+ title: "Orientation",
47
+ type: "string",
48
+ options: {
49
+ list: [
50
+ { title: "Horizontal (slider left-right)", value: "horizontal" },
51
+ { title: "Vertical (slider top-bottom)", value: "vertical" },
52
+ ],
53
+ },
54
+ initialValue: "horizontal",
55
+ }),
56
+ defineField({
57
+ name: "initial_position",
58
+ title: "Initial Position",
59
+ type: "number",
60
+ description: "Starting split position (0–100%)",
61
+ initialValue: 50,
62
+ validation: (Rule) => Rule.min(0).max(100),
63
+ }),
64
+ defineField({
65
+ name: "handle_color",
66
+ title: "Handle Color",
67
+ type: "string",
68
+ description: "Hex color for the slider line + handle",
69
+ initialValue: "#FFFFFF",
70
+ }),
71
+
72
+ // ── Layout ──
73
+ defineField({
74
+ name: "width",
75
+ title: "Width",
76
+ type: "string",
77
+ options: {
78
+ list: [
79
+ { title: "Full", value: "full" },
80
+ { title: "Contained", value: "contained" },
81
+ { title: "Small", value: "small" },
82
+ { title: "Fill", value: "fill" },
83
+ ],
84
+ },
85
+ initialValue: "full",
86
+ }),
87
+ defineField({
88
+ name: "aspect_ratio",
89
+ title: "Aspect Ratio",
90
+ type: "string",
91
+ options: {
92
+ list: [
93
+ { title: "Auto", value: "auto" },
94
+ { title: "16:9", value: "16:9" },
95
+ { title: "4:3", value: "4:3" },
96
+ { title: "1:1", value: "1:1" },
97
+ { title: "21:9", value: "21:9" },
98
+ ],
99
+ },
100
+ initialValue: "16:9",
101
+ }),
102
+
103
+ // ── Video playback (applies when either side is video) ──
104
+ defineField({ name: "video_autoplay", title: "Video Autoplay", type: "boolean", initialValue: true }),
105
+ defineField({ name: "video_loop", title: "Video Loop", type: "boolean", initialValue: true }),
106
+ defineField({ name: "video_muted", title: "Video Muted", type: "boolean", initialValue: true }),
107
+
108
+ // ── Appearance ──
109
+ defineField({ name: "border_radius", title: "Border Radius", type: "string" }),
110
+ defineField({ name: "shadow", title: "Shadow", type: "boolean", initialValue: false }),
111
+
112
+ ...blockAnimationFields,
113
+ blockLayoutField,
114
+ ],
115
+ preview: {
116
+ select: { before: "before_asset_path", after: "after_asset_path" },
117
+ prepare({ before, after }) {
118
+ return { title: `Before/After: ${before || "—"} ↔ ${after || "—"}` };
119
+ },
120
+ },
121
+ });
@@ -1,9 +1,11 @@
1
- // Block schemas (8)
1
+ // Block schemas (10)
2
2
  export { textBlock } from "./textBlock";
3
3
  export { imageBlock } from "./imageBlock";
4
4
  export { imageGridBlock } from "./imageGridBlock";
5
5
  export { videoBlock } from "./videoBlock";
6
6
  export { spacerBlock } from "./spacerBlock";
7
7
  export { buttonBlock } from "./buttonBlock";
8
+ export { beforeAfterBlock } from "./beforeAfterBlock";
9
+ export { audioBlock } from "./audioBlock";
8
10
  export { projectGridBlock } from "./projectGridBlock";
9
11
  export { projectCarouselBlock } from "./projectCarouselBlock";
@@ -18,6 +18,8 @@ import {
18
18
  videoBlock,
19
19
  spacerBlock,
20
20
  buttonBlock,
21
+ beforeAfterBlock,
22
+ audioBlock,
21
23
  projectGridBlock,
22
24
  projectCarouselBlock,
23
25
  } from "./blocks";
@@ -69,6 +71,8 @@ export {
69
71
  videoBlock,
70
72
  spacerBlock,
71
73
  buttonBlock,
74
+ beforeAfterBlock,
75
+ audioBlock,
72
76
  projectGridBlock,
73
77
  projectCarouselBlock,
74
78
  } from "./blocks";
@@ -93,13 +97,15 @@ export const schemaTypes = [
93
97
  parallaxGroup, // Parallax V2 group (Session 123)
94
98
  coverSection, // Cover Section — proportional rows (Session 176)
95
99
 
96
- // Blocks (9)
100
+ // Blocks (10)
97
101
  textBlock,
98
102
  imageBlock,
99
103
  imageGridBlock,
100
104
  videoBlock,
101
105
  spacerBlock,
102
106
  buttonBlock,
107
+ beforeAfterBlock,
108
+ audioBlock,
103
109
  projectGridBlock,
104
110
  projectCarouselBlock,
105
111
  ];