@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.
- package/app/admin/pages/[slug]/page.tsx +41 -47
- package/app/api/admin/assets/scan/route.ts +40 -13
- package/app/api/admin/custom-sections/[slug]/route.ts +4 -1
- package/app/api/admin/custom-sections/route.ts +4 -1
- package/app/api/admin/pages/[slug]/route.ts +7 -1
- package/app/api/admin/pages/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +19 -1
- package/app/api/admin/r2/disconnect/route.ts +3 -0
- package/app/api/admin/r2/rename/route.ts +52 -13
- package/app/api/admin/r2/upload-url/route.ts +8 -1
- package/app/api/admin/settings/route.ts +4 -1
- package/app/api/admin/styles/route.ts +4 -1
- package/components/admin/styles/GridLayoutEditor.tsx +46 -46
- package/components/blocks/BlockRenderer.tsx +15 -2
- package/components/blocks/CoverSectionRenderer.tsx +75 -3
- package/components/blocks/ImageGridBlockRenderer.tsx +17 -11
- package/components/blocks/ParallaxGroupRenderer.tsx +45 -10
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +527 -0
- package/components/blocks/ShaderCanvas.tsx +10 -6
- package/components/builder/BlockCardIcons.tsx +227 -0
- package/components/builder/BlockLivePreview.tsx +5 -0
- package/components/builder/BlockTypePicker.tsx +36 -63
- package/components/builder/BuilderCanvas.tsx +6 -2
- package/components/builder/ColumnDragOverlay.tsx +3 -3
- package/components/builder/CoverRowResizeHandle.tsx +5 -2
- package/components/builder/CoverSectionCanvas.tsx +45 -52
- package/components/builder/DndWrapper.tsx +1 -1
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/ParallaxGroupCanvas.tsx +12 -71
- package/components/builder/ReadOnlyFrame.tsx +4 -23
- package/components/builder/SectionCardIcons.tsx +320 -0
- package/components/builder/SectionEditorBar.tsx +17 -12
- package/components/builder/SectionTypePicker.tsx +34 -138
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +19 -30
- package/components/builder/SettingsPanel.tsx +8 -32
- package/components/builder/SortableBlock.tsx +42 -50
- package/components/builder/SortableRow.tsx +207 -19
- package/components/builder/blockStyles.tsx +59 -180
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -0
- package/components/builder/editors/index.ts +1 -0
- package/components/builder/iconPrimitives.tsx +78 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +16 -2
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +227 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +15 -2
- package/components/builder/live-preview/index.ts +1 -0
- package/components/builder/settings-panel/BlockSettings.tsx +7 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionSettings.tsx +28 -1
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/assets.ts +17 -2
- package/lib/builder/block-registrations.ts +268 -0
- package/lib/builder/block-registry.ts +195 -0
- package/lib/builder/constants.ts +22 -15
- package/lib/builder/defaults.ts +21 -0
- package/lib/builder/format.ts +25 -0
- package/lib/builder/history.ts +0 -3
- package/lib/builder/index.ts +16 -0
- package/lib/builder/layout-styles.ts +1 -1
- package/lib/builder/registry.ts +44 -0
- package/lib/builder/section-visibility.ts +36 -0
- package/lib/builder/serializer/normalizers.ts +15 -6
- package/lib/builder/serializer/serializers.ts +3 -3
- package/lib/builder/store-blocks.ts +16 -9
- package/lib/builder/store-cover.ts +76 -8
- package/lib/builder/store-sections.ts +1 -1
- package/lib/builder/store.ts +0 -2
- package/lib/builder/types.ts +9 -5
- package/lib/csrf.ts +31 -0
- package/lib/sanity/types.ts +54 -2
- package/lib/security.ts +50 -0
- 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/objects/coverSection.ts +35 -3
- package/sanity/schemas/pageSectionV2.ts +1 -0
- 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
|
+
}
|
package/lib/builder/constants.ts
CHANGED
|
@@ -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 (#
|
|
59
|
-
//
|
|
60
|
-
//
|
|
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
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
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
|
|
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 = "#
|
|
76
|
-
export const
|
|
77
|
-
export const
|
|
78
|
-
export const
|
|
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)
|
package/lib/builder/defaults.ts
CHANGED
|
@@ -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
|
+
}
|
package/lib/builder/history.ts
CHANGED
|
@@ -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
|
/**
|
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";
|
|
@@ -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";
|