@morphika/andami 0.5.1 → 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/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 -320
- 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 -286
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -316
- package/components/builder/BlockTypePicker.tsx +1 -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 -363
- 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 +1 -1
- package/components/builder/SectionV2Column.tsx +69 -67
- 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 +11 -11
- package/components/builder/blockStyles.tsx +192 -185
- 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 -242
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
- 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 +6 -6
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
- 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 +1 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +468 -417
- 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 +4 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +98 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -69
- package/sanity/schemas/blocks/index.ts +12 -11
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -117
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
package/lib/builder/types.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const ALL_BLOCK_INFO: BlockTypeInfo[] = [
|
|
|
65
65
|
// Section blocks — not in the content picker but still need label/icon lookup
|
|
66
66
|
{ type: "projectGridBlock", label: "Project Grid", description: "Staggered project showcase grid", group: "generic", icon: "⬡", category: "section" },
|
|
67
67
|
{ type: "projectCarouselBlock", label: "Project Carousel", description: "Horizontal carousel of projects — great for end-of-page 'keep browsing'", group: "generic", icon: "▸", category: "section" },
|
|
68
|
+
{ type: "marqueeBlock", label: "Marquee", description: "Horizontal scrolling ticker of text and images", group: "generic", icon: "⇄", category: "section" },
|
|
68
69
|
];
|
|
69
70
|
|
|
70
71
|
// Parallax group info — used by BuilderCanvas/SortableRow for label/icon lookup (not a block)
|
|
@@ -75,12 +76,13 @@ export const PARALLAX_GROUP_INFO = { label: "Parallax Showcase", icon: "▽" };
|
|
|
75
76
|
// ============================================
|
|
76
77
|
|
|
77
78
|
/** Section block types that create a full-width row with a pre-populated block */
|
|
78
|
-
export type SectionBlockType = "projectGridBlock" | "projectCarouselBlock";
|
|
79
|
+
export type SectionBlockType = "projectGridBlock" | "projectCarouselBlock" | "marqueeBlock";
|
|
79
80
|
|
|
80
81
|
/** Set for fast lookup — used by SortableBlock, ColumnDropZone, SortableRow to suppress inner chrome */
|
|
81
82
|
const SECTION_BLOCK_TYPES: ReadonlySet<string> = new Set<string>([
|
|
82
83
|
"projectGridBlock",
|
|
83
84
|
"projectCarouselBlock",
|
|
85
|
+
"marqueeBlock",
|
|
84
86
|
]);
|
|
85
87
|
|
|
86
88
|
/** Check if a block type is a section-level block (should render without block/column chrome) */
|
|
@@ -114,6 +116,7 @@ export const SECTION_TYPE_REGISTRY: SectionTypeInfo[] = [
|
|
|
114
116
|
{ type: "coverSection", label: "Cover Section", description: "Full-viewport section with background and proportional rows", icon: "◆" },
|
|
115
117
|
{ type: "projectGridBlock", label: "Project Grid", description: "Staggered project showcase grid", icon: "⬡", blockType: "projectGridBlock" },
|
|
116
118
|
{ type: "projectCarouselBlock", label: "Project Carousel", description: "Horizontal 'keep browsing' carousel of projects", icon: "▸", blockType: "projectCarouselBlock" },
|
|
119
|
+
{ type: "marqueeBlock", label: "Marquee", description: "Horizontal scrolling ticker of text and images", icon: "⇄", blockType: "marqueeBlock" },
|
|
117
120
|
{ type: "parallaxGroup", label: "Parallax Section", description: "Full-screen parallax showcase with V2 slides", icon: "▽" },
|
|
118
121
|
];
|
|
119
122
|
|
package/lib/config/index.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Site configuration accessor.
|
|
3
|
-
*
|
|
4
|
-
* Uses globalThis + Symbol.for to guarantee a true singleton even when
|
|
5
|
-
* the bundler creates multiple module instances of this file.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { SiteConfig } from "./types";
|
|
9
|
-
|
|
10
|
-
const CONFIG_KEY = Symbol.for("@morphika/andami/siteConfig");
|
|
11
|
-
const g = globalThis as unknown as Record<symbol, SiteConfig | undefined>;
|
|
12
|
-
|
|
13
|
-
export function registerConfig(config: SiteConfig): void {
|
|
14
|
-
g[CONFIG_KEY] = config;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getSiteConfig(): SiteConfig {
|
|
18
|
-
const cfg = g[CONFIG_KEY];
|
|
19
|
-
if (!cfg) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
"SiteConfig not registered. Call registerConfig(config) in your root layout before using getSiteConfig().\n" +
|
|
22
|
-
"See: https://github.com/MorphikaStudio/Morphika_Andami#quick-start",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return cfg;
|
|
26
|
-
}
|
|
27
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Site configuration accessor.
|
|
3
|
+
*
|
|
4
|
+
* Uses globalThis + Symbol.for to guarantee a true singleton even when
|
|
5
|
+
* the bundler creates multiple module instances of this file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SiteConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
const CONFIG_KEY = Symbol.for("@morphika/andami/siteConfig");
|
|
11
|
+
const g = globalThis as unknown as Record<symbol, SiteConfig | undefined>;
|
|
12
|
+
|
|
13
|
+
export function registerConfig(config: SiteConfig): void {
|
|
14
|
+
g[CONFIG_KEY] = config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getSiteConfig(): SiteConfig {
|
|
18
|
+
const cfg = g[CONFIG_KEY];
|
|
19
|
+
if (!cfg) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"SiteConfig not registered. Call registerConfig(config) in your root layout before using getSiteConfig().\n" +
|
|
22
|
+
"See: https://github.com/MorphikaStudio/Morphika_Andami#quick-start",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return cfg;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
28
|
export type { SiteConfig } from "./types";
|
package/lib/sanity/types.ts
CHANGED
|
@@ -368,6 +368,102 @@ export interface ProjectGridBlock {
|
|
|
368
368
|
responsive?: ResponsiveOverrides<ProjectGridBlock>;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// ============================================
|
|
372
|
+
// Marquee Block — horizontal infinite-scroll ticker (Session 184)
|
|
373
|
+
// ============================================
|
|
374
|
+
|
|
375
|
+
/** Plain-text item in a marquee. */
|
|
376
|
+
export interface MarqueeTextItem {
|
|
377
|
+
_key: string;
|
|
378
|
+
_type: "marqueeText";
|
|
379
|
+
text: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Image item in a marquee — asset_path is relative (resolved by the asset proxy). */
|
|
383
|
+
export interface MarqueeImageItem {
|
|
384
|
+
_key: string;
|
|
385
|
+
_type: "marqueeImage";
|
|
386
|
+
asset_path: string;
|
|
387
|
+
alt?: string;
|
|
388
|
+
/** Per-item override, 0–200 px. */
|
|
389
|
+
border_radius?: number;
|
|
390
|
+
/** Fixed width in px. When absent, derived from row_height × natural aspect ratio. */
|
|
391
|
+
width?: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Separator item — inherits block-level typography (size/color/weight/style). */
|
|
395
|
+
export interface MarqueeSeparatorItem {
|
|
396
|
+
_key: string;
|
|
397
|
+
_type: "marqueeSeparator";
|
|
398
|
+
/** 1–4 chars — glyph or emoji. Common: • · — / ▸ ★ */
|
|
399
|
+
character: string;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export type MarqueeItem =
|
|
403
|
+
| MarqueeTextItem
|
|
404
|
+
| MarqueeImageItem
|
|
405
|
+
| MarqueeSeparatorItem;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Font-size scale for text/separator items in a marquee.
|
|
409
|
+
* Mapped to concrete rem/px values by the renderer.
|
|
410
|
+
*/
|
|
411
|
+
export type MarqueeFontSize =
|
|
412
|
+
| "s"
|
|
413
|
+
| "base"
|
|
414
|
+
| "l"
|
|
415
|
+
| "xl"
|
|
416
|
+
| "2xl"
|
|
417
|
+
| "3xl"
|
|
418
|
+
| "4xl"
|
|
419
|
+
| "5xl"
|
|
420
|
+
| "6xl";
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Marquee Block — horizontal infinite-scroll ticker.
|
|
424
|
+
*
|
|
425
|
+
* Section-level block: lives inside a full-width column of a `PageSectionV2`.
|
|
426
|
+
* Available on both pages and projects (unlike projectGrid/projectCarousel
|
|
427
|
+
* which are pages-only) — makes sense as an end-of-project "keep browsing"
|
|
428
|
+
* style band.
|
|
429
|
+
*
|
|
430
|
+
* Motion is driven by CSS `@keyframes` with an IntersectionObserver pause
|
|
431
|
+
* when off-screen. Respects `prefers-reduced-motion`. Hover effects are
|
|
432
|
+
* deliberately not supported — a block that is already animating fights
|
|
433
|
+
* scale/tilt hover presets.
|
|
434
|
+
*/
|
|
435
|
+
export interface MarqueeBlock {
|
|
436
|
+
_type: "marqueeBlock";
|
|
437
|
+
_key: string;
|
|
438
|
+
|
|
439
|
+
// ─── Content ───
|
|
440
|
+
items: MarqueeItem[];
|
|
441
|
+
|
|
442
|
+
// ─── Motion ───
|
|
443
|
+
direction?: "left" | "right"; // default "left"
|
|
444
|
+
speed?: number; // px/s, 5–600, default 60
|
|
445
|
+
pause_on_hover?: boolean; // default true
|
|
446
|
+
|
|
447
|
+
// ─── Typography (applies to text + separator items) ───
|
|
448
|
+
font_size?: MarqueeFontSize; // default "3xl"
|
|
449
|
+
font_weight?: "400" | "500" | "700" | "900"; // default "700"
|
|
450
|
+
color?: string; // hex or palette token, default "#111111"
|
|
451
|
+
text_style?: "solid" | "outline" | "italic-outline"; // default "solid"
|
|
452
|
+
letter_spacing?: number; // em, default 0
|
|
453
|
+
text_transform?: "none" | "uppercase" | "lowercase"; // default "uppercase"
|
|
454
|
+
|
|
455
|
+
// ─── Layout ───
|
|
456
|
+
gap?: number; // px between items, default 48
|
|
457
|
+
row_height?: number; // px, default 120 (controls image height)
|
|
458
|
+
padding_y?: number; // px, default 16
|
|
459
|
+
background_color?: string; // hex or palette; empty = transparent
|
|
460
|
+
|
|
461
|
+
// ─── Standard block fields ───
|
|
462
|
+
enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
|
|
463
|
+
layout?: BlockLayout;
|
|
464
|
+
responsive?: ResponsiveOverrides<MarqueeBlock>;
|
|
465
|
+
}
|
|
466
|
+
|
|
371
467
|
// ============================================
|
|
372
468
|
// Parallax V2 — Group + Slide types (Session 123)
|
|
373
469
|
// ============================================
|
|
@@ -694,7 +790,8 @@ export type ContentBlock =
|
|
|
694
790
|
| BeforeAfterBlock
|
|
695
791
|
| AudioBlock
|
|
696
792
|
| ProjectGridBlock
|
|
697
|
-
| ProjectCarouselBlock
|
|
793
|
+
| ProjectCarouselBlock
|
|
794
|
+
| MarqueeBlock;
|
|
698
795
|
|
|
699
796
|
// ============================================
|
|
700
797
|
// Structural types
|
package/lib/version.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,69 +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: "#
|
|
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
|
-
});
|
|
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: "#3580f9",
|
|
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
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// Block schemas (
|
|
2
|
-
export { textBlock } from "./textBlock";
|
|
3
|
-
export { imageBlock } from "./imageBlock";
|
|
4
|
-
export { imageGridBlock } from "./imageGridBlock";
|
|
5
|
-
export { videoBlock } from "./videoBlock";
|
|
6
|
-
export { spacerBlock } from "./spacerBlock";
|
|
7
|
-
export { buttonBlock } from "./buttonBlock";
|
|
8
|
-
export { beforeAfterBlock } from "./beforeAfterBlock";
|
|
9
|
-
export { audioBlock } from "./audioBlock";
|
|
10
|
-
export { projectGridBlock } from "./projectGridBlock";
|
|
11
|
-
export { projectCarouselBlock } from "./projectCarouselBlock";
|
|
1
|
+
// Block schemas (11)
|
|
2
|
+
export { textBlock } from "./textBlock";
|
|
3
|
+
export { imageBlock } from "./imageBlock";
|
|
4
|
+
export { imageGridBlock } from "./imageGridBlock";
|
|
5
|
+
export { videoBlock } from "./videoBlock";
|
|
6
|
+
export { spacerBlock } from "./spacerBlock";
|
|
7
|
+
export { buttonBlock } from "./buttonBlock";
|
|
8
|
+
export { beforeAfterBlock } from "./beforeAfterBlock";
|
|
9
|
+
export { audioBlock } from "./audioBlock";
|
|
10
|
+
export { projectGridBlock } from "./projectGridBlock";
|
|
11
|
+
export { projectCarouselBlock } from "./projectCarouselBlock";
|
|
12
|
+
export { marqueeBlock } from "./marqueeBlock";
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
import { blockLayoutField, blockAnimationFields } from "./blockLayout";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* marqueeBlock — horizontal infinite-scroll ticker.
|
|
6
|
+
*
|
|
7
|
+
* Signature portfolio element. Supports mixed content (text, images,
|
|
8
|
+
* separators) scrolling horizontally at a configurable speed, with
|
|
9
|
+
* pause-on-hover and a hollow-text "outline" typography option for the
|
|
10
|
+
* classic portfolio look.
|
|
11
|
+
*
|
|
12
|
+
* Treated as a **section-level block** (same semantics as
|
|
13
|
+
* projectGridBlock / projectCarouselBlock): lives inside a full-width
|
|
14
|
+
* column of a pageSectionV2 and is added via the "+ Add Section" modal,
|
|
15
|
+
* not the "+ Add Block" picker. Unlike the two project-related section
|
|
16
|
+
* blocks, Marquee is available on **both pages and projects** — the
|
|
17
|
+
* "AVAILABLE FOR WORK" band fits naturally on `/work/[slug]` too.
|
|
18
|
+
*
|
|
19
|
+
* Performance: the renderer uses pure CSS @keyframes + IntersectionObserver
|
|
20
|
+
* to pause the animation when off-screen. Zero JS in the hot path, zero
|
|
21
|
+
* CPU off-screen. Respects `prefers-reduced-motion`.
|
|
22
|
+
*
|
|
23
|
+
* Hover effects deliberately disabled (enterPresets only): a block that
|
|
24
|
+
* is already moving doesn't benefit from scale/tilt hover effects — would
|
|
25
|
+
* fight the motion.
|
|
26
|
+
*/
|
|
27
|
+
export const marqueeBlock = defineType({
|
|
28
|
+
name: "marqueeBlock",
|
|
29
|
+
title: "Marquee",
|
|
30
|
+
type: "object",
|
|
31
|
+
fields: [
|
|
32
|
+
// ─── Content (items array) ────────────────────────────────
|
|
33
|
+
defineField({
|
|
34
|
+
name: "items",
|
|
35
|
+
title: "Items",
|
|
36
|
+
type: "array",
|
|
37
|
+
description:
|
|
38
|
+
"Ordered list of text, images and separators. They repeat in order indefinitely as the marquee scrolls.",
|
|
39
|
+
of: [
|
|
40
|
+
{
|
|
41
|
+
type: "object",
|
|
42
|
+
name: "marqueeText",
|
|
43
|
+
title: "Text",
|
|
44
|
+
fields: [
|
|
45
|
+
defineField({
|
|
46
|
+
name: "text",
|
|
47
|
+
title: "Text",
|
|
48
|
+
type: "string",
|
|
49
|
+
validation: (Rule) => Rule.required().min(1),
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
preview: {
|
|
53
|
+
select: { title: "text" },
|
|
54
|
+
prepare: ({ title }: { title?: string }) => ({
|
|
55
|
+
title: title || "(empty text)",
|
|
56
|
+
subtitle: "Text",
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: "object",
|
|
62
|
+
name: "marqueeImage",
|
|
63
|
+
title: "Image",
|
|
64
|
+
fields: [
|
|
65
|
+
defineField({
|
|
66
|
+
name: "asset_path",
|
|
67
|
+
title: "Asset path",
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Relative path to the image file",
|
|
70
|
+
validation: (Rule) => Rule.required(),
|
|
71
|
+
}),
|
|
72
|
+
defineField({
|
|
73
|
+
name: "alt",
|
|
74
|
+
title: "Alt text",
|
|
75
|
+
type: "string",
|
|
76
|
+
}),
|
|
77
|
+
defineField({
|
|
78
|
+
name: "border_radius",
|
|
79
|
+
title: "Border radius (px)",
|
|
80
|
+
type: "number",
|
|
81
|
+
initialValue: 0,
|
|
82
|
+
validation: (Rule) => Rule.min(0).max(200),
|
|
83
|
+
}),
|
|
84
|
+
defineField({
|
|
85
|
+
name: "width",
|
|
86
|
+
title: "Width (px)",
|
|
87
|
+
type: "number",
|
|
88
|
+
description:
|
|
89
|
+
"Fixed width. Leave empty to derive from the row height and image aspect ratio.",
|
|
90
|
+
validation: (Rule) => Rule.min(16).max(1200),
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
preview: {
|
|
94
|
+
select: { title: "alt", subtitle: "asset_path" },
|
|
95
|
+
prepare: ({ title, subtitle }: { title?: string; subtitle?: string }) => ({
|
|
96
|
+
title: title || "(no alt)",
|
|
97
|
+
subtitle: `Image · ${subtitle || "no asset"}`,
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "object",
|
|
103
|
+
name: "marqueeSeparator",
|
|
104
|
+
title: "Separator",
|
|
105
|
+
fields: [
|
|
106
|
+
defineField({
|
|
107
|
+
name: "character",
|
|
108
|
+
title: "Character",
|
|
109
|
+
type: "string",
|
|
110
|
+
description:
|
|
111
|
+
"Any character (or emoji) used as a divider. Common: • · — / ▸ ★",
|
|
112
|
+
initialValue: "•",
|
|
113
|
+
validation: (Rule) => Rule.required().min(1).max(4),
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
preview: {
|
|
117
|
+
select: { title: "character" },
|
|
118
|
+
prepare: ({ title }: { title?: string }) => ({
|
|
119
|
+
title: title || "•",
|
|
120
|
+
subtitle: "Separator",
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
|
|
127
|
+
// ─── Motion ───────────────────────────────────────────────
|
|
128
|
+
defineField({
|
|
129
|
+
name: "direction",
|
|
130
|
+
title: "Direction",
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Horizontal scroll direction",
|
|
133
|
+
options: {
|
|
134
|
+
list: [
|
|
135
|
+
{ title: "Left →", value: "left" },
|
|
136
|
+
{ title: "← Right", value: "right" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
initialValue: "left",
|
|
140
|
+
}),
|
|
141
|
+
defineField({
|
|
142
|
+
name: "speed",
|
|
143
|
+
title: "Speed (px/s)",
|
|
144
|
+
type: "number",
|
|
145
|
+
description:
|
|
146
|
+
"Scroll speed in pixels per second. Decouples from content length (a 20s loop feels radically different with 10 items vs 100).",
|
|
147
|
+
initialValue: 60,
|
|
148
|
+
validation: (Rule) => Rule.min(5).max(600),
|
|
149
|
+
}),
|
|
150
|
+
defineField({
|
|
151
|
+
name: "pause_on_hover",
|
|
152
|
+
title: "Pause on hover",
|
|
153
|
+
type: "boolean",
|
|
154
|
+
initialValue: true,
|
|
155
|
+
}),
|
|
156
|
+
|
|
157
|
+
// ─── Typography (text + separator items) ──────────────────
|
|
158
|
+
defineField({
|
|
159
|
+
name: "font_size",
|
|
160
|
+
title: "Font size",
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Size scale for text and separator items",
|
|
163
|
+
options: {
|
|
164
|
+
list: [
|
|
165
|
+
{ title: "S", value: "s" },
|
|
166
|
+
{ title: "Base", value: "base" },
|
|
167
|
+
{ title: "L", value: "l" },
|
|
168
|
+
{ title: "XL", value: "xl" },
|
|
169
|
+
{ title: "2XL", value: "2xl" },
|
|
170
|
+
{ title: "3XL", value: "3xl" },
|
|
171
|
+
{ title: "4XL", value: "4xl" },
|
|
172
|
+
{ title: "5XL", value: "5xl" },
|
|
173
|
+
{ title: "6XL", value: "6xl" },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
initialValue: "3xl",
|
|
177
|
+
}),
|
|
178
|
+
defineField({
|
|
179
|
+
name: "font_weight",
|
|
180
|
+
title: "Font weight",
|
|
181
|
+
type: "string",
|
|
182
|
+
options: {
|
|
183
|
+
list: [
|
|
184
|
+
{ title: "Normal", value: "400" },
|
|
185
|
+
{ title: "Medium", value: "500" },
|
|
186
|
+
{ title: "Bold", value: "700" },
|
|
187
|
+
{ title: "Black", value: "900" },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
initialValue: "700",
|
|
191
|
+
}),
|
|
192
|
+
defineField({
|
|
193
|
+
name: "color",
|
|
194
|
+
title: "Text color",
|
|
195
|
+
type: "string",
|
|
196
|
+
description: "Hex or palette token",
|
|
197
|
+
initialValue: "#111111",
|
|
198
|
+
}),
|
|
199
|
+
defineField({
|
|
200
|
+
name: "text_style",
|
|
201
|
+
title: "Text style",
|
|
202
|
+
type: "string",
|
|
203
|
+
description:
|
|
204
|
+
"Outline renders hollow text via -webkit-text-stroke — the classic portfolio signature look.",
|
|
205
|
+
options: {
|
|
206
|
+
list: [
|
|
207
|
+
{ title: "Solid", value: "solid" },
|
|
208
|
+
{ title: "Outline (hollow)", value: "outline" },
|
|
209
|
+
{ title: "Italic outline", value: "italic-outline" },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
initialValue: "solid",
|
|
213
|
+
}),
|
|
214
|
+
defineField({
|
|
215
|
+
name: "letter_spacing",
|
|
216
|
+
title: "Letter spacing (em)",
|
|
217
|
+
type: "number",
|
|
218
|
+
description: "Extra tracking between letters — in em units",
|
|
219
|
+
initialValue: 0,
|
|
220
|
+
validation: (Rule) => Rule.min(-0.1).max(0.5),
|
|
221
|
+
}),
|
|
222
|
+
defineField({
|
|
223
|
+
name: "text_transform",
|
|
224
|
+
title: "Text transform",
|
|
225
|
+
type: "string",
|
|
226
|
+
options: {
|
|
227
|
+
list: [
|
|
228
|
+
{ title: "None", value: "none" },
|
|
229
|
+
{ title: "UPPERCASE", value: "uppercase" },
|
|
230
|
+
{ title: "lowercase", value: "lowercase" },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
initialValue: "uppercase",
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
// ─── Layout ───────────────────────────────────────────────
|
|
237
|
+
defineField({
|
|
238
|
+
name: "gap",
|
|
239
|
+
title: "Gap between items (px)",
|
|
240
|
+
type: "number",
|
|
241
|
+
initialValue: 48,
|
|
242
|
+
validation: (Rule) => Rule.min(0).max(200),
|
|
243
|
+
}),
|
|
244
|
+
defineField({
|
|
245
|
+
name: "row_height",
|
|
246
|
+
title: "Row height (px)",
|
|
247
|
+
type: "number",
|
|
248
|
+
description: "Height of the marquee band. Determines image item height.",
|
|
249
|
+
initialValue: 120,
|
|
250
|
+
validation: (Rule) => Rule.min(24).max(600),
|
|
251
|
+
}),
|
|
252
|
+
defineField({
|
|
253
|
+
name: "padding_y",
|
|
254
|
+
title: "Vertical padding (px)",
|
|
255
|
+
type: "number",
|
|
256
|
+
initialValue: 16,
|
|
257
|
+
validation: (Rule) => Rule.min(0).max(200),
|
|
258
|
+
}),
|
|
259
|
+
defineField({
|
|
260
|
+
name: "background_color",
|
|
261
|
+
title: "Background color",
|
|
262
|
+
type: "string",
|
|
263
|
+
description: "Hex or palette token. Leave empty for transparent.",
|
|
264
|
+
}),
|
|
265
|
+
|
|
266
|
+
// ─── Standard block fields ────────────────────────────────
|
|
267
|
+
...blockAnimationFields,
|
|
268
|
+
blockLayoutField,
|
|
269
|
+
],
|
|
270
|
+
preview: {
|
|
271
|
+
select: {
|
|
272
|
+
direction: "direction",
|
|
273
|
+
speed: "speed",
|
|
274
|
+
items: "items",
|
|
275
|
+
},
|
|
276
|
+
prepare: ({
|
|
277
|
+
direction,
|
|
278
|
+
speed,
|
|
279
|
+
items,
|
|
280
|
+
}: {
|
|
281
|
+
direction?: string;
|
|
282
|
+
speed?: number;
|
|
283
|
+
items?: unknown[];
|
|
284
|
+
}) => {
|
|
285
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
286
|
+
const arrow = direction === "right" ? "←" : "→";
|
|
287
|
+
return {
|
|
288
|
+
title: `Marquee (${count} items ${arrow} ${speed ?? 60}px/s)`,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|