@morphika/andami 0.3.1 → 0.4.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.
- package/app/admin/pages/[slug]/page.tsx +2 -2
- package/components/blocks/BlockRenderer.tsx +21 -42
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
- package/components/builder/BlockLivePreview.tsx +18 -42
- package/components/builder/ReadOnlyFrame.tsx +4 -23
- package/components/builder/SectionCardIcons.tsx +54 -0
- package/components/builder/SectionTypePicker.tsx +1 -1
- package/components/builder/blockStyles.tsx +6 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
- package/components/builder/editors/index.ts +1 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
- package/components/builder/live-preview/index.ts +1 -0
- package/components/builder/settings-panel/BlockSettings.tsx +18 -63
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +335 -0
- package/lib/builder/block-registry.ts +195 -0
- package/lib/builder/defaults.ts +22 -81
- package/lib/builder/index.ts +16 -0
- package/lib/builder/registry.ts +44 -0
- package/lib/builder/store-sections.ts +1 -1
- package/lib/builder/types.ts +8 -3
- package/lib/sanity/types.ts +50 -1
- 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/pageSectionV2.ts +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Block Registry — central source of truth for block-type metadata.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by the Puck pattern (puckeditor.com). Each block type declares its
|
|
7
|
+
* schema, renderer, live-preview, editor, icons, and animation-preset lists
|
|
8
|
+
* in ONE place instead of scattering across 8 files.
|
|
9
|
+
*
|
|
10
|
+
* Currently this file contains only the infrastructure (types + helpers).
|
|
11
|
+
* Population of the registry happens in `./block-registrations.ts` via
|
|
12
|
+
* side-effect imports. Dispatch tables (`ALL_BLOCK_INFO`, `BLOCK_CARD_ICONS`,
|
|
13
|
+
* renderer/editor switches) still exist in their original locations and are
|
|
14
|
+
* NOT derived from this registry yet — that migration happens in later
|
|
15
|
+
* sessions, behind the existing public API so it stays backward-compatible.
|
|
16
|
+
*
|
|
17
|
+
* ## Stability contract
|
|
18
|
+
*
|
|
19
|
+
* The registry is an INTERNAL consolidation helper. Consumers (instances
|
|
20
|
+
* using `@morphika/andami`) should not import from this module directly.
|
|
21
|
+
* The existing public exports (`ALL_BLOCK_INFO`, `BLOCK_TYPE_REGISTRY`,
|
|
22
|
+
* `BLOCK_CARD_ICONS`, etc.) remain the supported API.
|
|
23
|
+
*
|
|
24
|
+
* Session A (2026-04-19): infrastructure only — no existing exports change.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type React from "react";
|
|
28
|
+
import type { DeviceViewport } from "./types";
|
|
29
|
+
import type { ContentBlock } from "../sanity/types";
|
|
30
|
+
|
|
31
|
+
// ────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Types
|
|
33
|
+
// ────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Full registration for a single block type. Generic over the specific
|
|
37
|
+
* block union member so renderers / editors / factories can stay strongly
|
|
38
|
+
* typed even when pulled out of the registry map.
|
|
39
|
+
*/
|
|
40
|
+
export interface BlockRegistrationInput<T extends ContentBlock = ContentBlock> {
|
|
41
|
+
// ── Identity ──
|
|
42
|
+
/**
|
|
43
|
+
* Block type string — MUST match the `_type` literal stored in Sanity
|
|
44
|
+
* documents. Renaming this string is a breaking data migration.
|
|
45
|
+
*/
|
|
46
|
+
type: T["_type"];
|
|
47
|
+
/** Human-readable label shown in the picker modal and settings header. */
|
|
48
|
+
label: string;
|
|
49
|
+
/** Short description shown under the label in the picker modal card. */
|
|
50
|
+
description: string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the block appears.
|
|
53
|
+
* - "content": rendered inside a column, picked via the "+ Add Block" modal
|
|
54
|
+
* - "section": rendered as a full-width section-level block, picked via
|
|
55
|
+
* the "+ Add Section" modal (same pattern as projectGridBlock /
|
|
56
|
+
* projectCarouselBlock)
|
|
57
|
+
*/
|
|
58
|
+
category: "content" | "section";
|
|
59
|
+
/**
|
|
60
|
+
* Legacy single-char / emoji icon kept for backward compatibility with
|
|
61
|
+
* `ALL_BLOCK_INFO` entries and debug UIs. Prefer `cardIcon` for rich
|
|
62
|
+
* visuals.
|
|
63
|
+
*/
|
|
64
|
+
iconGlyph?: string;
|
|
65
|
+
|
|
66
|
+
// ── Sanity ──
|
|
67
|
+
/**
|
|
68
|
+
* Sanity schema (result of `defineType(...)`). Kept as `unknown` because
|
|
69
|
+
* Sanity's internal types are not worth pulling through the generic.
|
|
70
|
+
* Studio consumers cast at registration time.
|
|
71
|
+
*/
|
|
72
|
+
schema: unknown;
|
|
73
|
+
|
|
74
|
+
// ── Factory ──
|
|
75
|
+
/**
|
|
76
|
+
* Creates a default instance of this block type. Receives a pre-generated
|
|
77
|
+
* `_key` so the caller controls key generation (e.g. for deterministic
|
|
78
|
+
* snapshots in tests).
|
|
79
|
+
*/
|
|
80
|
+
defaultFactory: (key: string) => T;
|
|
81
|
+
|
|
82
|
+
// ── Runtime components ──
|
|
83
|
+
/** Public-site renderer (used inside `BlockRenderer`). */
|
|
84
|
+
renderer: React.ComponentType<{ block: T }>;
|
|
85
|
+
/**
|
|
86
|
+
* Builder canvas live preview. Signature is intentionally permissive —
|
|
87
|
+
* some previews consume `viewport`, some `editable`, most neither. At the
|
|
88
|
+
* callsite, props are forwarded from `BlockLivePreview`.
|
|
89
|
+
*/
|
|
90
|
+
livePreview: React.ComponentType<{
|
|
91
|
+
block: T;
|
|
92
|
+
viewport?: DeviceViewport;
|
|
93
|
+
editable?: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
/** Settings-panel editor for this block type. */
|
|
96
|
+
editor: React.ComponentType<{ block: T }>;
|
|
97
|
+
|
|
98
|
+
// ── Icons ──
|
|
99
|
+
/**
|
|
100
|
+
* Full-size card icon (220×120) used in the "+ Add Block" or "+ Add
|
|
101
|
+
* Section" picker modal. For section-level blocks, this is the violet
|
|
102
|
+
* variant from `SectionCardIcons.tsx`; for content blocks, the blue
|
|
103
|
+
* variant from `BlockCardIcons.tsx`.
|
|
104
|
+
*/
|
|
105
|
+
cardIcon: React.FC;
|
|
106
|
+
/**
|
|
107
|
+
* Compact icon used in the settings panel header, auto-scaled via
|
|
108
|
+
* `blockStyles.scaleToHeight()` to whatever `size` the caller requests.
|
|
109
|
+
*/
|
|
110
|
+
compactIcon: React.FC<{ size?: number }>;
|
|
111
|
+
|
|
112
|
+
// ── Animation preset allowlists ──
|
|
113
|
+
/**
|
|
114
|
+
* Which enter-animation presets apply to this block. Used by
|
|
115
|
+
* `BLOCK_ENTER_PRESETS` in `lib/animation/enter-types.ts`.
|
|
116
|
+
*/
|
|
117
|
+
enterPresets: readonly string[];
|
|
118
|
+
/**
|
|
119
|
+
* Which hover-effect presets apply to this block. Used by
|
|
120
|
+
* `BLOCK_HOVER_PRESETS` in `lib/animation/hover-effect-types.ts`.
|
|
121
|
+
*/
|
|
122
|
+
hoverPresets: readonly string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Concrete registration stored in the map. Erases the block-type generic
|
|
127
|
+
* so heterogeneous registrations can coexist in the same `Map`.
|
|
128
|
+
*/
|
|
129
|
+
export type BlockRegistration = BlockRegistrationInput;
|
|
130
|
+
|
|
131
|
+
// ────────────────────────────────────────────────────────────────────
|
|
132
|
+
// Registry state (module-level singleton)
|
|
133
|
+
// ────────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const REGISTRY: Map<string, BlockRegistration> = new Map();
|
|
136
|
+
|
|
137
|
+
// ────────────────────────────────────────────────────────────────────
|
|
138
|
+
// Public API
|
|
139
|
+
// ────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register a block type. Called once per block type from
|
|
143
|
+
* `./block-registrations.ts` during module initialization. In dev mode,
|
|
144
|
+
* warns on duplicate type strings (which usually indicate a bug).
|
|
145
|
+
*/
|
|
146
|
+
export function registerBlockType<T extends ContentBlock>(
|
|
147
|
+
registration: BlockRegistrationInput<T>,
|
|
148
|
+
): void {
|
|
149
|
+
if (REGISTRY.has(registration.type)) {
|
|
150
|
+
if (process.env.NODE_ENV !== "production") {
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.warn(
|
|
153
|
+
`[block-registry] Duplicate registration for "${registration.type}" — overwriting. ` +
|
|
154
|
+
`This usually indicates registrations.ts was imported twice or a block type was registered from multiple places.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
REGISTRY.set(
|
|
159
|
+
registration.type,
|
|
160
|
+
registration as unknown as BlockRegistration,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Look up a registration by block type. Returns `undefined` for unknown
|
|
166
|
+
* types (including when the registrations module hasn't been imported yet).
|
|
167
|
+
*/
|
|
168
|
+
export function getBlockRegistration<T extends ContentBlock = ContentBlock>(
|
|
169
|
+
type: string,
|
|
170
|
+
): BlockRegistrationInput<T> | undefined {
|
|
171
|
+
return REGISTRY.get(type) as BlockRegistrationInput<T> | undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* All currently-registered blocks in insertion order. Safe to iterate —
|
|
176
|
+
* the returned array is a snapshot, not a live view of the registry.
|
|
177
|
+
*/
|
|
178
|
+
export function getAllBlockRegistrations(): readonly BlockRegistration[] {
|
|
179
|
+
return Array.from(REGISTRY.values());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Cheap existence check for dispatch-site assertions. */
|
|
183
|
+
export function hasBlockRegistration(type: string): boolean {
|
|
184
|
+
return REGISTRY.has(type);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reset the registry. Only used by tests that want a clean slate between
|
|
189
|
+
* suites — production code should never call this.
|
|
190
|
+
*
|
|
191
|
+
* @internal
|
|
192
|
+
*/
|
|
193
|
+
export function __clearBlockRegistryForTests(): void {
|
|
194
|
+
REGISTRY.clear();
|
|
195
|
+
}
|
package/lib/builder/defaults.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { generateKey } from "./utils";
|
|
|
4
4
|
import type { EnterAnimationConfig, TypewriterConfig } from "../../lib/animation/enter-types";
|
|
5
5
|
import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
|
|
6
6
|
|
|
7
|
+
// Side-effect import: populates the block registry with all built-in block
|
|
8
|
+
// types so `createDefaultBlock` can delegate to each registration's
|
|
9
|
+
// `defaultFactory`. See `./block-registry.ts` for the stability contract.
|
|
10
|
+
import "./block-registrations";
|
|
11
|
+
import { getBlockRegistration } from "./block-registry";
|
|
12
|
+
|
|
7
13
|
// ============================================
|
|
8
14
|
// Parallax slide defaults (Session 128)
|
|
9
15
|
// ============================================
|
|
@@ -116,88 +122,23 @@ export const DEFAULT_TYPEWRITER_CONFIG: Required<TypewriterConfig> = {
|
|
|
116
122
|
|
|
117
123
|
/**
|
|
118
124
|
* Create a new block with sensible defaults for the given block type.
|
|
125
|
+
*
|
|
126
|
+
* Session 181 (C): switched from a giant switch-case to a registry lookup.
|
|
127
|
+
* Each block type now declares its default shape in
|
|
128
|
+
* `./block-registrations.ts` via the `defaultFactory` field. The
|
|
129
|
+
* registration module is loaded eagerly here (side-effect import) so the
|
|
130
|
+
* registry is populated before any `createDefaultBlock` call.
|
|
131
|
+
*
|
|
132
|
+
* Unknown block types fall back to the minimal `{ _type, _key }` shape —
|
|
133
|
+
* same behavior as the previous switch-case's `default` branch.
|
|
119
134
|
*/
|
|
120
135
|
export function createDefaultBlock(blockType: BlockType): ContentBlock {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
case "textBlock":
|
|
125
|
-
return {
|
|
126
|
-
_type: "textBlock",
|
|
127
|
-
_key,
|
|
128
|
-
text: [],
|
|
129
|
-
style: { fontSize: 14, alignment: "left", fontWeight: "400" },
|
|
130
|
-
};
|
|
131
|
-
case "imageBlock":
|
|
132
|
-
return {
|
|
133
|
-
_type: "imageBlock",
|
|
134
|
-
_key,
|
|
135
|
-
asset_path: "",
|
|
136
|
-
alt: "",
|
|
137
|
-
width: "full",
|
|
138
|
-
aspect_ratio: "auto",
|
|
139
|
-
lazy: true,
|
|
140
|
-
shadow: false,
|
|
141
|
-
border_radius: "",
|
|
142
|
-
};
|
|
143
|
-
case "imageGridBlock":
|
|
144
|
-
return {
|
|
145
|
-
_type: "imageGridBlock",
|
|
146
|
-
_key,
|
|
147
|
-
images: [],
|
|
148
|
-
h_gutter: 10,
|
|
149
|
-
v_gutter: 10,
|
|
150
|
-
images_per_row: 2,
|
|
151
|
-
random_grid: "disabled",
|
|
152
|
-
random_seed: 1,
|
|
153
|
-
lightbox: false,
|
|
154
|
-
object_fit: "cover",
|
|
155
|
-
};
|
|
156
|
-
case "videoBlock":
|
|
157
|
-
return {
|
|
158
|
-
_type: "videoBlock",
|
|
159
|
-
_key,
|
|
160
|
-
video_type: "vimeo",
|
|
161
|
-
url_or_path: "",
|
|
162
|
-
autoplay: false,
|
|
163
|
-
loop: false,
|
|
164
|
-
muted: true,
|
|
165
|
-
controls: true,
|
|
166
|
-
aspect_ratio: "16:9",
|
|
167
|
-
};
|
|
168
|
-
case "spacerBlock":
|
|
169
|
-
return {
|
|
170
|
-
_type: "spacerBlock",
|
|
171
|
-
_key,
|
|
172
|
-
height: "medium",
|
|
173
|
-
};
|
|
174
|
-
case "buttonBlock":
|
|
175
|
-
return {
|
|
176
|
-
_type: "buttonBlock",
|
|
177
|
-
_key,
|
|
178
|
-
text: "Button",
|
|
179
|
-
url: "#",
|
|
180
|
-
style: "primary",
|
|
181
|
-
size: "medium",
|
|
182
|
-
};
|
|
183
|
-
case "projectGridBlock":
|
|
184
|
-
return {
|
|
185
|
-
_type: "projectGridBlock",
|
|
186
|
-
_key,
|
|
187
|
-
columns: 3,
|
|
188
|
-
aspect_ratios: ["16/9"],
|
|
189
|
-
gap_v: 16,
|
|
190
|
-
gap_h: 16,
|
|
191
|
-
hover_effect: "scale",
|
|
192
|
-
show_subtitle: true,
|
|
193
|
-
border_radius: 0,
|
|
194
|
-
video_mode: "off",
|
|
195
|
-
projects: [],
|
|
196
|
-
};
|
|
197
|
-
default:
|
|
198
|
-
return {
|
|
199
|
-
_type: blockType,
|
|
200
|
-
_key,
|
|
201
|
-
} as ContentBlock;
|
|
136
|
+
const registration = getBlockRegistration(blockType);
|
|
137
|
+
if (registration) {
|
|
138
|
+
return registration.defaultFactory(generateKey());
|
|
202
139
|
}
|
|
140
|
+
return {
|
|
141
|
+
_type: blockType,
|
|
142
|
+
_key: generateKey(),
|
|
143
|
+
} as ContentBlock;
|
|
203
144
|
}
|
package/lib/builder/index.ts
CHANGED
|
@@ -19,3 +19,19 @@ export type {
|
|
|
19
19
|
SectionTypeInfo,
|
|
20
20
|
SectionBlockType,
|
|
21
21
|
} from "./types";
|
|
22
|
+
|
|
23
|
+
// ── Block registry (Session A + B: infrastructure + consistency validation) ──
|
|
24
|
+
//
|
|
25
|
+
// Registry API re-exports for consumers that want to register custom block
|
|
26
|
+
// types or introspect the built-in ones. See `./registry` for the batteries-
|
|
27
|
+
// included entry point (triggers `block-registrations` as a side effect).
|
|
28
|
+
export type {
|
|
29
|
+
BlockRegistration,
|
|
30
|
+
BlockRegistrationInput,
|
|
31
|
+
} from "./block-registry";
|
|
32
|
+
export {
|
|
33
|
+
registerBlockType,
|
|
34
|
+
getBlockRegistration,
|
|
35
|
+
getAllBlockRegistrations,
|
|
36
|
+
hasBlockRegistration,
|
|
37
|
+
} from "./block-registry";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block registry — batteries-included entry point.
|
|
3
|
+
*
|
|
4
|
+
* Importing this module triggers the side-effect load of
|
|
5
|
+
* `./block-registrations`, populating the registry with all 8 built-in
|
|
6
|
+
* block types (text, image, imageGrid, video, spacer, button, projectGrid,
|
|
7
|
+
* projectCarousel). Then re-exports the registry query API so consumers
|
|
8
|
+
* can introspect registrations with one import.
|
|
9
|
+
*
|
|
10
|
+
* ## When to use this vs `./block-registry`
|
|
11
|
+
*
|
|
12
|
+
* - **`./block-registry`** — the bare API (types + `registerBlockType` +
|
|
13
|
+
* query helpers). Use this if you're registering custom blocks in an
|
|
14
|
+
* instance and want to control population order.
|
|
15
|
+
* - **`./registry`** (this file) — convenience wrapper. Use this if you
|
|
16
|
+
* just want to read the built-in registrations from an instance or
|
|
17
|
+
* documentation script.
|
|
18
|
+
*
|
|
19
|
+
* ## Example
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { getAllBlockRegistrations } from "@morphika/andami/lib/builder/registry";
|
|
23
|
+
*
|
|
24
|
+
* // Registry is already populated — no side-effect import needed.
|
|
25
|
+
* const allBlocks = getAllBlockRegistrations();
|
|
26
|
+
* console.log(allBlocks.map(r => r.type));
|
|
27
|
+
* // → ["textBlock", "imageBlock", ..., "projectCarouselBlock"]
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Side-effect: fill the registry with all built-in block types.
|
|
32
|
+
import "./block-registrations";
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
BlockRegistration,
|
|
36
|
+
BlockRegistrationInput,
|
|
37
|
+
} from "./block-registry";
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
registerBlockType,
|
|
41
|
+
getBlockRegistration,
|
|
42
|
+
getAllBlockRegistrations,
|
|
43
|
+
hasBlockRegistration,
|
|
44
|
+
} from "./block-registry";
|
|
@@ -57,7 +57,7 @@ export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
|
57
57
|
* Existing V1 pages still work — V1 update/delete actions are kept until
|
|
58
58
|
* the data migration in Session 165.
|
|
59
59
|
*/
|
|
60
|
-
addSection: (blockType: "projectGridBlock", afterRowKey?: string | null): void => {
|
|
60
|
+
addSection: (blockType: "projectGridBlock" | "projectCarouselBlock", afterRowKey?: string | null): void => {
|
|
61
61
|
get()._pushSnapshot();
|
|
62
62
|
const block = createDefaultBlock(blockType);
|
|
63
63
|
const gridColumns = 12;
|
package/lib/builder/types.ts
CHANGED
|
@@ -62,6 +62,7 @@ export const ALL_BLOCK_INFO: BlockTypeInfo[] = [
|
|
|
62
62
|
...BLOCK_TYPE_REGISTRY,
|
|
63
63
|
// Section blocks — not in the content picker but still need label/icon lookup
|
|
64
64
|
{ type: "projectGridBlock", label: "Project Grid", description: "Staggered project showcase grid", group: "generic", icon: "⬡", category: "section" },
|
|
65
|
+
{ type: "projectCarouselBlock", label: "Project Carousel", description: "Horizontal carousel of projects — great for end-of-page 'keep browsing'", group: "generic", icon: "▸", category: "section" },
|
|
65
66
|
];
|
|
66
67
|
|
|
67
68
|
// Parallax group info — used by BuilderCanvas/SortableRow for label/icon lookup (not a block)
|
|
@@ -72,10 +73,13 @@ export const PARALLAX_GROUP_INFO = { label: "Parallax Showcase", icon: "▽" };
|
|
|
72
73
|
// ============================================
|
|
73
74
|
|
|
74
75
|
/** Section block types that create a full-width row with a pre-populated block */
|
|
75
|
-
export type SectionBlockType = "projectGridBlock";
|
|
76
|
+
export type SectionBlockType = "projectGridBlock" | "projectCarouselBlock";
|
|
76
77
|
|
|
77
78
|
/** Set for fast lookup — used by SortableBlock, ColumnDropZone, SortableRow to suppress inner chrome */
|
|
78
|
-
const SECTION_BLOCK_TYPES: ReadonlySet<string> = new Set<string>([
|
|
79
|
+
const SECTION_BLOCK_TYPES: ReadonlySet<string> = new Set<string>([
|
|
80
|
+
"projectGridBlock",
|
|
81
|
+
"projectCarouselBlock",
|
|
82
|
+
]);
|
|
79
83
|
|
|
80
84
|
/** Check if a block type is a section-level block (should render without block/column chrome) */
|
|
81
85
|
export function isSectionBlockType(type: string): boolean {
|
|
@@ -107,6 +111,7 @@ export const SECTION_TYPE_REGISTRY: SectionTypeInfo[] = [
|
|
|
107
111
|
{ type: "empty-v2", label: "Empty Section", description: "Grid section with flexible columns", icon: "⊞" },
|
|
108
112
|
{ type: "coverSection", label: "Cover Section", description: "Full-viewport section with background and proportional rows", icon: "◆" },
|
|
109
113
|
{ type: "projectGridBlock", label: "Project Grid", description: "Staggered project showcase grid", icon: "⬡", blockType: "projectGridBlock" },
|
|
114
|
+
{ type: "projectCarouselBlock", label: "Project Carousel", description: "Horizontal 'keep browsing' carousel of projects", icon: "▸", blockType: "projectCarouselBlock" },
|
|
110
115
|
{ type: "parallaxGroup", label: "Parallax Section", description: "Full-screen parallax showcase with V2 slides", icon: "▽" },
|
|
111
116
|
];
|
|
112
117
|
|
|
@@ -266,7 +271,7 @@ export interface BuilderActions {
|
|
|
266
271
|
|
|
267
272
|
// Section operations
|
|
268
273
|
/** Add a Project Grid section as a V2 section with a full-width column (Session 164) */
|
|
269
|
-
addSection: (blockType: "projectGridBlock", afterRowKey?: string | null) => void;
|
|
274
|
+
addSection: (blockType: "projectGridBlock" | "projectCarouselBlock", afterRowKey?: string | null) => void;
|
|
270
275
|
reorderRows: (fromIndex: number, toIndex: number) => void;
|
|
271
276
|
/** Delete a section by key */
|
|
272
277
|
deleteSection: (sectionKey: string) => void;
|
package/lib/sanity/types.ts
CHANGED
|
@@ -240,6 +240,54 @@ export interface CardEntranceConfig {
|
|
|
240
240
|
duration?: number; // ms, default 500
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Project Carousel Block — horizontal "keep browsing" carousel of projects.
|
|
245
|
+
*
|
|
246
|
+
* Section-level block (lives in a full-width column). Unlike `projectGridBlock`
|
|
247
|
+
* there is no manual selection list — projects are pulled automatically (latest
|
|
248
|
+
* or random), optionally excluding the project currently being viewed when the
|
|
249
|
+
* page matches `/work/[slug]`.
|
|
250
|
+
*
|
|
251
|
+
* Intentionally independent from `ProjectGridBlock` — they share visual
|
|
252
|
+
* primitives and the same `/api/projects` endpoint but no coupled code.
|
|
253
|
+
*/
|
|
254
|
+
export interface ProjectCarouselBlock {
|
|
255
|
+
_type: "projectCarouselBlock";
|
|
256
|
+
_key: string;
|
|
257
|
+
|
|
258
|
+
// ─── Source ───
|
|
259
|
+
source_mode?: "auto_latest" | "auto_random";
|
|
260
|
+
max_projects?: number; // 2–20, default 8
|
|
261
|
+
exclude_current?: boolean; // default true
|
|
262
|
+
|
|
263
|
+
// ─── Layout ───
|
|
264
|
+
cards_per_view_desktop?: number; // default 3.5 (fractional → peek next card)
|
|
265
|
+
cards_per_view_tablet?: number; // default 2.2
|
|
266
|
+
cards_per_view_phone?: number; // default 1.2
|
|
267
|
+
gap?: number; // px, default 16
|
|
268
|
+
|
|
269
|
+
// ─── Card display ───
|
|
270
|
+
aspect_ratio?: "16/9" | "4/3" | "1/1" | "3/4" | "9/16";
|
|
271
|
+
show_title?: boolean; // default true
|
|
272
|
+
show_subtitle?: boolean; // default false
|
|
273
|
+
border_radius?: number; // px, default 0
|
|
274
|
+
hover_effect?: "scale" | "none"; // default "scale"
|
|
275
|
+
video_mode?: "off" | "hover" | "autoloop"; // default "off"
|
|
276
|
+
|
|
277
|
+
// ─── Controls ───
|
|
278
|
+
show_arrows?: boolean; // default true
|
|
279
|
+
show_dots?: boolean; // default false
|
|
280
|
+
snap_scroll?: boolean; // default true
|
|
281
|
+
|
|
282
|
+
// ─── Card entrance animation ───
|
|
283
|
+
card_entrance?: CardEntranceConfig;
|
|
284
|
+
|
|
285
|
+
// ─── Standard block fields ───
|
|
286
|
+
enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
|
|
287
|
+
layout?: BlockLayout;
|
|
288
|
+
responsive?: ResponsiveOverrides<ProjectCarouselBlock>;
|
|
289
|
+
}
|
|
290
|
+
|
|
243
291
|
export interface ProjectGridBlock {
|
|
244
292
|
_type: "projectGridBlock";
|
|
245
293
|
_key: string;
|
|
@@ -570,7 +618,8 @@ export type ContentBlock =
|
|
|
570
618
|
| VideoBlock
|
|
571
619
|
| SpacerBlock
|
|
572
620
|
| ButtonBlock
|
|
573
|
-
| ProjectGridBlock
|
|
621
|
+
| ProjectGridBlock
|
|
622
|
+
| ProjectCarouselBlock;
|
|
574
623
|
|
|
575
624
|
// ============================================
|
|
576
625
|
// Structural types
|
package/lib/version.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Block schemas (
|
|
1
|
+
// Block schemas (8)
|
|
2
2
|
export { textBlock } from "./textBlock";
|
|
3
3
|
export { imageBlock } from "./imageBlock";
|
|
4
4
|
export { imageGridBlock } from "./imageGridBlock";
|
|
@@ -6,3 +6,4 @@ export { videoBlock } from "./videoBlock";
|
|
|
6
6
|
export { spacerBlock } from "./spacerBlock";
|
|
7
7
|
export { buttonBlock } from "./buttonBlock";
|
|
8
8
|
export { projectGridBlock } from "./projectGridBlock";
|
|
9
|
+
export { projectCarouselBlock } from "./projectCarouselBlock";
|