@morphika/andami 0.2.26 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/app/admin/pages/[slug]/page.tsx +41 -47
  2. package/app/api/admin/assets/scan/route.ts +40 -13
  3. package/app/api/admin/custom-sections/[slug]/route.ts +4 -1
  4. package/app/api/admin/custom-sections/route.ts +4 -1
  5. package/app/api/admin/pages/[slug]/route.ts +7 -1
  6. package/app/api/admin/pages/route.ts +4 -1
  7. package/app/api/admin/r2/connect/route.ts +19 -1
  8. package/app/api/admin/r2/disconnect/route.ts +3 -0
  9. package/app/api/admin/r2/rename/route.ts +52 -13
  10. package/app/api/admin/r2/upload-url/route.ts +8 -1
  11. package/app/api/admin/settings/route.ts +4 -1
  12. package/app/api/admin/styles/route.ts +4 -1
  13. package/components/admin/styles/GridLayoutEditor.tsx +46 -46
  14. package/components/blocks/BlockRenderer.tsx +15 -2
  15. package/components/blocks/CoverSectionRenderer.tsx +75 -3
  16. package/components/blocks/ImageGridBlockRenderer.tsx +17 -11
  17. package/components/blocks/ParallaxGroupRenderer.tsx +45 -10
  18. package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
  19. package/components/blocks/ShaderCanvas.tsx +10 -6
  20. package/components/builder/BlockCardIcons.tsx +227 -0
  21. package/components/builder/BlockLivePreview.tsx +5 -0
  22. package/components/builder/BlockTypePicker.tsx +36 -63
  23. package/components/builder/BuilderCanvas.tsx +6 -2
  24. package/components/builder/ColumnDragOverlay.tsx +3 -3
  25. package/components/builder/CoverRowResizeHandle.tsx +5 -2
  26. package/components/builder/CoverSectionCanvas.tsx +45 -52
  27. package/components/builder/DndWrapper.tsx +1 -1
  28. package/components/builder/InsertionLines.tsx +1 -1
  29. package/components/builder/ParallaxGroupCanvas.tsx +12 -71
  30. package/components/builder/ReadOnlyFrame.tsx +4 -23
  31. package/components/builder/SectionCardIcons.tsx +320 -0
  32. package/components/builder/SectionEditorBar.tsx +17 -12
  33. package/components/builder/SectionTypePicker.tsx +34 -138
  34. package/components/builder/SectionV2Canvas.tsx +1 -1
  35. package/components/builder/SectionV2Column.tsx +19 -30
  36. package/components/builder/SettingsPanel.tsx +8 -32
  37. package/components/builder/SortableBlock.tsx +42 -50
  38. package/components/builder/SortableRow.tsx +207 -19
  39. package/components/builder/blockStyles.tsx +59 -180
  40. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
  41. package/components/builder/editors/index.ts +1 -0
  42. package/components/builder/iconPrimitives.tsx +78 -0
  43. package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
  44. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
  45. package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
  46. package/components/builder/live-preview/index.ts +1 -0
  47. package/components/builder/settings-panel/BlockSettings.tsx +7 -0
  48. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  49. package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
  50. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  51. package/lib/animation/enter-types.ts +1 -0
  52. package/lib/animation/hover-effect-types.ts +1 -0
  53. package/lib/assets.ts +17 -2
  54. package/lib/builder/block-registrations.ts +268 -0
  55. package/lib/builder/block-registry.ts +195 -0
  56. package/lib/builder/constants.ts +22 -15
  57. package/lib/builder/defaults.ts +21 -0
  58. package/lib/builder/format.ts +25 -0
  59. package/lib/builder/history.ts +0 -3
  60. package/lib/builder/index.ts +16 -0
  61. package/lib/builder/layout-styles.ts +1 -1
  62. package/lib/builder/registry.ts +44 -0
  63. package/lib/builder/section-visibility.ts +36 -0
  64. package/lib/builder/serializer/normalizers.ts +15 -6
  65. package/lib/builder/serializer/serializers.ts +3 -3
  66. package/lib/builder/store-blocks.ts +16 -9
  67. package/lib/builder/store-cover.ts +76 -8
  68. package/lib/builder/store-sections.ts +1 -1
  69. package/lib/builder/store.ts +0 -2
  70. package/lib/builder/types.ts +9 -5
  71. package/lib/csrf.ts +31 -0
  72. package/lib/sanity/types.ts +54 -2
  73. package/lib/security.ts +50 -0
  74. package/lib/version.ts +1 -1
  75. package/package.json +1 -1
  76. package/sanity/schemas/blocks/index.ts +2 -1
  77. package/sanity/schemas/blocks/projectCarouselBlock.ts +218 -0
  78. package/sanity/schemas/index.ts +4 -1
  79. package/sanity/schemas/objects/coverSection.ts +35 -3
  80. package/sanity/schemas/pageSectionV2.ts +1 -0
  81. package/components/builder/ParallaxSlideHeader.tsx +0 -113
@@ -0,0 +1,268 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Block registrations — fills the registry defined in `./block-registry.ts`
5
+ * with the 8 built-in block types shipped by the framework.
6
+ *
7
+ * Importing this module has a side effect: every `registerBlockType()`
8
+ * call below runs synchronously and populates the module-level registry.
9
+ * Most of the codebase currently does NOT import this — the existing
10
+ * dispatch tables in `components/blocks/BlockRenderer.tsx`,
11
+ * `components/builder/BlockLivePreview.tsx`, etc. are still the source
12
+ * of truth at runtime. This file is the parallel structure we'll migrate
13
+ * to in a later session, behind the existing public API so instances
14
+ * don't break.
15
+ *
16
+ * Session A (2026-04-19): all 8 blocks registered; nothing consumes
17
+ * the registry yet.
18
+ */
19
+
20
+ import { registerBlockType } from "./block-registry";
21
+ import { createDefaultBlock } from "./defaults";
22
+
23
+ import type {
24
+ ContentBlock,
25
+ TextBlock,
26
+ ImageBlock,
27
+ ImageGridBlock,
28
+ VideoBlock,
29
+ SpacerBlock,
30
+ ButtonBlock,
31
+ ProjectGridBlock,
32
+ ProjectCarouselBlock,
33
+ } from "../sanity/types";
34
+
35
+ // ── Sanity schemas ────────────────────────────────────────────────
36
+
37
+ import {
38
+ textBlock,
39
+ imageBlock,
40
+ imageGridBlock,
41
+ videoBlock,
42
+ spacerBlock,
43
+ buttonBlock,
44
+ projectGridBlock,
45
+ projectCarouselBlock,
46
+ } from "../../sanity/schemas/blocks";
47
+
48
+ // ── Public renderers ──────────────────────────────────────────────
49
+
50
+ import TextBlockRenderer from "../../components/blocks/TextBlockRenderer";
51
+ import ImageBlockRenderer from "../../components/blocks/ImageBlockRenderer";
52
+ import ImageGridBlockRenderer from "../../components/blocks/ImageGridBlockRenderer";
53
+ import VideoBlockRenderer from "../../components/blocks/VideoBlockRenderer";
54
+ import SpacerBlockRenderer from "../../components/blocks/SpacerBlockRenderer";
55
+ import ButtonBlockRenderer from "../../components/blocks/ButtonBlockRenderer";
56
+ import ProjectGridBlockRenderer from "../../components/blocks/ProjectGridBlockRenderer";
57
+ import ProjectCarouselBlockRenderer from "../../components/blocks/ProjectCarouselBlockRenderer";
58
+
59
+ // ── Builder live previews ─────────────────────────────────────────
60
+
61
+ import LiveTextEditor from "../../components/builder/live-preview/LiveTextEditor";
62
+ import LiveImagePreview from "../../components/builder/live-preview/LiveImagePreview";
63
+ import LiveImageGridPreview from "../../components/builder/live-preview/LiveImageGridPreview";
64
+ import LiveVideoPreview from "../../components/builder/live-preview/LiveVideoPreview";
65
+ import LiveSpacerPreview from "../../components/builder/live-preview/LiveSpacerPreview";
66
+ import LiveButtonPreview from "../../components/builder/live-preview/LiveButtonPreview";
67
+ import LiveProjectGridPreview from "../../components/builder/live-preview/LiveProjectGridPreview";
68
+ import LiveProjectCarouselPreview from "../../components/builder/live-preview/LiveProjectCarouselPreview";
69
+
70
+ // ── Settings-panel editors ────────────────────────────────────────
71
+
72
+ import TextBlockEditor from "../../components/builder/editors/TextBlockEditor";
73
+ import ImageBlockEditor from "../../components/builder/editors/ImageBlockEditor";
74
+ import ImageGridBlockEditor from "../../components/builder/editors/ImageGridBlockEditor";
75
+ import VideoBlockEditor from "../../components/builder/editors/VideoBlockEditor";
76
+ import SpacerBlockEditor from "../../components/builder/editors/SpacerBlockEditor";
77
+ import ButtonBlockEditor from "../../components/builder/editors/ButtonBlockEditor";
78
+ import ProjectGridEditor from "../../components/builder/editors/ProjectGridEditor";
79
+ import ProjectCarouselBlockEditor from "../../components/builder/editors/ProjectCarouselBlockEditor";
80
+
81
+ // ── Card icons (picker modal) ─────────────────────────────────────
82
+
83
+ import {
84
+ TextBlockCardIcon,
85
+ ImageBlockCardIcon,
86
+ ImageGridBlockCardIcon,
87
+ VideoBlockCardIcon,
88
+ SpacerBlockCardIcon,
89
+ ButtonBlockCardIcon,
90
+ } from "../../components/builder/BlockCardIcons";
91
+ import {
92
+ ProjectGridCardIcon,
93
+ ProjectCarouselCardIcon,
94
+ } from "../../components/builder/SectionCardIcons";
95
+
96
+ // ── Compact icons (settings panel header) ─────────────────────────
97
+
98
+ import {
99
+ TextBlockIcon,
100
+ ImageBlockIcon,
101
+ ImageGridBlockIcon,
102
+ VideoBlockIcon,
103
+ SpacerBlockIcon,
104
+ ButtonBlockIcon,
105
+ ProjectGridBlockIcon,
106
+ ProjectCarouselBlockIcon,
107
+ } from "../../components/builder/blockStyles";
108
+
109
+ // ────────────────────────────────────────────────────────────────────
110
+ // Helper: delegate to `createDefaultBlock` but override the generated
111
+ // `_key` so callers can supply deterministic keys (tests, fixtures).
112
+ // ────────────────────────────────────────────────────────────────────
113
+
114
+ function factory<T extends ContentBlock>(
115
+ type: T["_type"],
116
+ ): (key: string) => T {
117
+ return (key: string) => {
118
+ const block = createDefaultBlock(
119
+ type as Parameters<typeof createDefaultBlock>[0],
120
+ );
121
+ return { ...block, _key: key } as T;
122
+ };
123
+ }
124
+
125
+ // ────────────────────────────────────────────────────────────────────
126
+ // Registrations (order matches the existing BLOCK_TYPE_REGISTRY +
127
+ // section blocks appended at the end, so iteration order is stable).
128
+ // ────────────────────────────────────────────────────────────────────
129
+
130
+ // ── Content blocks ──
131
+
132
+ registerBlockType<TextBlock>({
133
+ type: "textBlock",
134
+ label: "Text",
135
+ description: "Rich text content",
136
+ category: "content",
137
+ iconGlyph: "T",
138
+ schema: textBlock,
139
+ defaultFactory: factory<TextBlock>("textBlock"),
140
+ renderer: TextBlockRenderer as React.ComponentType<{ block: TextBlock }>,
141
+ livePreview: LiveTextEditor as unknown as React.ComponentType<{ block: TextBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
142
+ editor: TextBlockEditor as React.ComponentType<{ block: TextBlock }>,
143
+ cardIcon: TextBlockCardIcon,
144
+ compactIcon: TextBlockIcon,
145
+ enterPresets: ["typewriter", "fade", "blur-in", "slide-up"],
146
+ hoverPresets: [],
147
+ });
148
+
149
+ registerBlockType<ImageBlock>({
150
+ type: "imageBlock",
151
+ label: "Image",
152
+ description: "Single image with caption",
153
+ category: "content",
154
+ iconGlyph: "🖼",
155
+ schema: imageBlock,
156
+ defaultFactory: factory<ImageBlock>("imageBlock"),
157
+ renderer: ImageBlockRenderer as React.ComponentType<{ block: ImageBlock }>,
158
+ livePreview: LiveImagePreview as unknown as React.ComponentType<{ block: ImageBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
159
+ editor: ImageBlockEditor as React.ComponentType<{ block: ImageBlock }>,
160
+ cardIcon: ImageBlockCardIcon,
161
+ compactIcon: ImageBlockIcon,
162
+ enterPresets: ["fade", "blur", "reveal", "scale", "slide-up"],
163
+ hoverPresets: ["scale-up", "lift", "tilt-3d", "ripple", "rgb-shift", "pixelate"],
164
+ });
165
+
166
+ registerBlockType<ImageGridBlock>({
167
+ type: "imageGridBlock",
168
+ label: "Image Grid",
169
+ description: "Multiple images in grid",
170
+ category: "content",
171
+ iconGlyph: "⊞",
172
+ schema: imageGridBlock,
173
+ defaultFactory: factory<ImageGridBlock>("imageGridBlock"),
174
+ renderer: ImageGridBlockRenderer as React.ComponentType<{ block: ImageGridBlock }>,
175
+ livePreview: LiveImageGridPreview as unknown as React.ComponentType<{ block: ImageGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
176
+ editor: ImageGridBlockEditor as React.ComponentType<{ block: ImageGridBlock }>,
177
+ cardIcon: ImageGridBlockCardIcon,
178
+ compactIcon: ImageGridBlockIcon,
179
+ enterPresets: ["fade", "scale", "slide-up"],
180
+ hoverPresets: ["tilt-3d"],
181
+ });
182
+
183
+ registerBlockType<VideoBlock>({
184
+ type: "videoBlock",
185
+ label: "Video",
186
+ description: "Vimeo, YouTube, or MP4",
187
+ category: "content",
188
+ iconGlyph: "▶",
189
+ schema: videoBlock,
190
+ defaultFactory: factory<VideoBlock>("videoBlock"),
191
+ renderer: VideoBlockRenderer as React.ComponentType<{ block: VideoBlock }>,
192
+ livePreview: LiveVideoPreview as unknown as React.ComponentType<{ block: VideoBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
193
+ editor: VideoBlockEditor as React.ComponentType<{ block: VideoBlock }>,
194
+ cardIcon: VideoBlockCardIcon,
195
+ compactIcon: VideoBlockIcon,
196
+ enterPresets: ["fade", "slide-up"],
197
+ hoverPresets: [],
198
+ });
199
+
200
+ registerBlockType<SpacerBlock>({
201
+ type: "spacerBlock",
202
+ label: "Spacer",
203
+ description: "Vertical spacing",
204
+ category: "content",
205
+ iconGlyph: "↕",
206
+ schema: spacerBlock,
207
+ defaultFactory: factory<SpacerBlock>("spacerBlock"),
208
+ renderer: SpacerBlockRenderer as React.ComponentType<{ block: SpacerBlock }>,
209
+ livePreview: LiveSpacerPreview as unknown as React.ComponentType<{ block: SpacerBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
210
+ editor: SpacerBlockEditor as React.ComponentType<{ block: SpacerBlock }>,
211
+ cardIcon: SpacerBlockCardIcon,
212
+ compactIcon: SpacerBlockIcon,
213
+ enterPresets: [],
214
+ hoverPresets: [],
215
+ });
216
+
217
+ registerBlockType<ButtonBlock>({
218
+ type: "buttonBlock",
219
+ label: "Button",
220
+ description: "Call-to-action button",
221
+ category: "content",
222
+ iconGlyph: "▣",
223
+ schema: buttonBlock,
224
+ defaultFactory: factory<ButtonBlock>("buttonBlock"),
225
+ renderer: ButtonBlockRenderer as React.ComponentType<{ block: ButtonBlock }>,
226
+ livePreview: LiveButtonPreview as unknown as React.ComponentType<{ block: ButtonBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
227
+ editor: ButtonBlockEditor as React.ComponentType<{ block: ButtonBlock }>,
228
+ cardIcon: ButtonBlockCardIcon,
229
+ compactIcon: ButtonBlockIcon,
230
+ enterPresets: ["fade", "slide-up", "scale"],
231
+ hoverPresets: ["scale-up", "lift", "border-glow"],
232
+ });
233
+
234
+ // ── Section-level blocks ──
235
+
236
+ registerBlockType<ProjectGridBlock>({
237
+ type: "projectGridBlock",
238
+ label: "Project Grid",
239
+ description: "Staggered project showcase grid",
240
+ category: "section",
241
+ iconGlyph: "⬡",
242
+ schema: projectGridBlock,
243
+ defaultFactory: factory<ProjectGridBlock>("projectGridBlock"),
244
+ renderer: ProjectGridBlockRenderer as React.ComponentType<{ block: ProjectGridBlock }>,
245
+ livePreview: LiveProjectGridPreview as unknown as React.ComponentType<{ block: ProjectGridBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
246
+ editor: ProjectGridEditor as React.ComponentType<{ block: ProjectGridBlock }>,
247
+ cardIcon: ProjectGridCardIcon,
248
+ compactIcon: ProjectGridBlockIcon,
249
+ enterPresets: [],
250
+ hoverPresets: ["scale-up", "lift"],
251
+ });
252
+
253
+ registerBlockType<ProjectCarouselBlock>({
254
+ type: "projectCarouselBlock",
255
+ label: "Project Carousel",
256
+ description: "Horizontal carousel of projects — great for end-of-page 'keep browsing'",
257
+ category: "section",
258
+ iconGlyph: "▸",
259
+ schema: projectCarouselBlock,
260
+ defaultFactory: factory<ProjectCarouselBlock>("projectCarouselBlock"),
261
+ renderer: ProjectCarouselBlockRenderer as React.ComponentType<{ block: ProjectCarouselBlock }>,
262
+ livePreview: LiveProjectCarouselPreview as unknown as React.ComponentType<{ block: ProjectCarouselBlock; viewport?: import("./types").DeviceViewport; editable?: boolean }>,
263
+ editor: ProjectCarouselBlockEditor as React.ComponentType<{ block: ProjectCarouselBlock }>,
264
+ cardIcon: ProjectCarouselCardIcon,
265
+ compactIcon: ProjectCarouselBlockIcon,
266
+ enterPresets: [],
267
+ hoverPresets: [],
268
+ });
@@ -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
+ }
@@ -55,27 +55,34 @@ export const ADMIN_ERROR_DARK = "#d42f1a";
55
55
  // visual differentiation at a glance. This is a design rule that
56
56
  // MUST be followed across all builder components.
57
57
  //
58
- // BLUE (#076bff) — Columns: outlines, resize handles, drag grip,
59
- // span badge, column selection/hover chrome.
60
- // GREEN-B (#0d9668) — Blocks: "+ Add Block" buttons, block toolbar
58
+ // BLUE (#4794e2) — Columns: outlines, resize handles, drag grip,
59
+ // column selection/hover chrome.
60
+ // BLUE (#4794e2) — Blocks: "+ Add Block" buttons, block toolbar
61
61
  // pill, block selection ring, block-level actions.
62
- // BLUE (#076bff) Drop zones: gap drop targets during drag,
63
- // insertion lines, "Drop Here" labels, swap target
64
- // highlight (blue border + tinted background).
65
- // (Merged with column color for coherence.)
66
- // VIOLET (#8b5cf6) — Custom sections: saved section cards, custom
67
- // section instance badges, section editor chrome,
68
- // "Create New" custom section button.
62
+ // (Same hue as columns a future pass may split
63
+ // these into distinct hues if the shared blue is
64
+ // causing ambiguity.)
65
+ // VIOLET (#7500d5) Sections: side pill, cover/parallax accent,
66
+ // section outlines, hover/selection chrome.
67
+ // Also used for Custom Sections card chrome.
69
68
  //
70
69
  // When adding new builder UI, pick the color that matches the entity
71
70
  // being represented, not the action being performed. For example, a
72
71
  // delete button on a column is BLUE (it belongs to the column chrome),
73
- // while a delete button on a block toolbar is ORANGE.
72
+ // while the delete inside a block toolbar is BLUE too (inside the block
73
+ // pill, on hover it flashes red as a destructive cue).
74
74
 
75
- export const BUILDER_BLUE = "#076bff"; // Columns
76
- export const BUILDER_ORANGE = "#0d9668"; // Blocks (emerald was orange #e28b00)
77
- export const BUILDER_GREEN = "#22c55e"; // Drop zones
78
- export const BUILDER_VIOLET = "#8b5cf6"; // Custom sections
75
+ export const BUILDER_BLUE = "#4794e2"; // Columns (softened — was #076bff)
76
+ export const BUILDER_BLOCK = "#4794e2"; // Blocks — same hue as columns for now
77
+ export const BUILDER_VIOLET = "#7500d5"; // Sections (incl. Custom)
78
+ export const BUILDER_GREEN = "#22c55e"; // Success / confirmation cues (e.g. R2 asset check)
79
+
80
+ /**
81
+ * @deprecated Use `BUILDER_BLOCK` instead. The legacy name survived multiple
82
+ * colour migrations (orange → emerald → blue) and no longer reflects reality.
83
+ * Kept as an alias so third-party code doesn't break at the import boundary.
84
+ */
85
+ export const BUILDER_ORANGE = BUILDER_BLOCK;
79
86
 
80
87
  /**
81
88
  * Padding map for Row settings (in pixels)
@@ -194,6 +194,27 @@ export function createDefaultBlock(blockType: BlockType): ContentBlock {
194
194
  video_mode: "off",
195
195
  projects: [],
196
196
  };
197
+ case "projectCarouselBlock":
198
+ return {
199
+ _type: "projectCarouselBlock",
200
+ _key,
201
+ source_mode: "auto_latest",
202
+ max_projects: 8,
203
+ exclude_current: true,
204
+ cards_per_view_desktop: 3.5,
205
+ cards_per_view_tablet: 2.2,
206
+ cards_per_view_phone: 1.2,
207
+ gap: 16,
208
+ aspect_ratio: "4/3",
209
+ show_title: true,
210
+ show_subtitle: false,
211
+ border_radius: 0,
212
+ hover_effect: "scale",
213
+ video_mode: "off",
214
+ show_arrows: true,
215
+ show_dots: false,
216
+ snap_scroll: true,
217
+ };
197
218
  default:
198
219
  return {
199
220
  _type: blockType,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Formatting helpers for builder UI.
3
+ *
4
+ * Kept as pure functions in their own module so they can be imported from
5
+ * any builder component and unit-tested in isolation.
6
+ */
7
+
8
+ /**
9
+ * Format a row height percentage with at most one decimal place, dropping
10
+ * the trailing `.0` when the rounded value is an integer. Used by the
11
+ * Cover Section row sub-pill in `SortableRow`.
12
+ *
13
+ * Examples:
14
+ * formatRowPercent(100) -> "100"
15
+ * formatRowPercent(50) -> "50"
16
+ * formatRowPercent(33.333) -> "33.3"
17
+ * formatRowPercent(66.6667) -> "66.7"
18
+ * formatRowPercent(0) -> "0"
19
+ * formatRowPercent(NaN) -> "0" (fallback — never expected in practice)
20
+ */
21
+ export function formatRowPercent(p: number): string {
22
+ if (!Number.isFinite(p)) return "0";
23
+ const rounded = Math.round(p * 10) / 10;
24
+ return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
25
+ }
@@ -23,14 +23,11 @@ export interface HistoryState {
23
23
  _history: HistorySnapshot[];
24
24
  /** Future snapshots (for redo). Last element = next redo. */
25
25
  _future: HistorySnapshot[];
26
- /** Whether we're currently applying undo/redo (skip snapshot). */
27
- _isTimeTraveling: boolean;
28
26
  }
29
27
 
30
28
  export const initialHistoryState: HistoryState = {
31
29
  _history: [],
32
30
  _future: [],
33
- _isTimeTraveling: false,
34
31
  };
35
32
 
36
33
  /**
@@ -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";
@@ -124,7 +124,7 @@ export function getBackgroundStyles(
124
124
  const imgUrl = assetBaseUrl
125
125
  ? `${assetBaseUrl.replace(/\/$/, "")}/${s.background_image}`
126
126
  : s.background_image;
127
- styles.backgroundImage = `url(${imgUrl})`;
127
+ styles.backgroundImage = `url("${imgUrl}")`;
128
128
  styles.backgroundSize = s.background_size || "cover";
129
129
  styles.backgroundPosition = s.background_position || "center center";
130
130
  styles.backgroundRepeat = s.background_repeat || "no-repeat";
@@ -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";