@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.
@@ -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
+ }
@@ -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 _key = generateKey();
122
-
123
- switch (blockType) {
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
  }
@@ -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;
@@ -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>(["projectGridBlock"]);
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;
@@ -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
@@ -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.3.1";
9
+ export const ANDAMI_VERSION = "0.4.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -1,4 +1,4 @@
1
- // Block schemas (7)
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";