@createcms/core 0.1.1 → 0.2.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.
@@ -227,18 +227,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
227
227
  label: string;
228
228
  description?: string;
229
229
  previewImageUrl?: string;
230
+ /**
231
+ * Editor hint: the block-picker category this block is shown under (e.g.
232
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
233
+ * this label; the package never acts on it. Free-form by design; for
234
+ * consistent, autocompleted group names across blocks, reference a shared
235
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
236
+ */
237
+ group?: string;
230
238
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
231
239
  events?: TEvents;
232
240
  } & ({
233
241
  allowChildren?: false;
234
242
  } | {
235
243
  allowChildren: true;
236
- allowedChildBlocks?: string[];
237
244
  });
238
245
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
239
246
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
240
247
  properties: TProps;
241
248
  };
249
+ /**
250
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
251
+ * — declares which child block types that parent (or the literal `'root'`) may
252
+ * contain. There are three mutually-exclusive modes, enforced by the type:
253
+ *
254
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
255
+ * entry at all; `'*'` is just an explicit, readable form.)
256
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
257
+ * a block added to the collection later is rejected until listed. `excludes`
258
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
259
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
260
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
261
+ *
262
+ * Whether a parent accepts children AT ALL is the separate, coarser
263
+ * `allowChildren` gate on the block (the root always accepts children); these
264
+ * rules only refine WHICH children an accepting parent may hold.
265
+ */
266
+ type BlockStructureEntry<TBlockName extends string> = {
267
+ /** `'*'` = open base (optional, for readability). */
268
+ accepts?: '*';
269
+ /** Holds anything except these. */
270
+ excludes?: readonly TBlockName[];
271
+ } | {
272
+ /** Holds ONLY these block types. */
273
+ accepts: readonly TBlockName[];
274
+ /**
275
+ * Forbidden alongside a concrete `accepts` list — the list already names
276
+ * exactly what is allowed, so `excludes` would be ignored.
277
+ */
278
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
279
+ };
280
+ /**
281
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
282
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
283
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
284
+ * the collection's block names and are checked at compile time by
285
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
286
+ *
287
+ * This is the single source of truth that the visual editor (drop-zone gating)
288
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
289
+ * alongside each block's `allowChildren` flag, so they can never diverge.
290
+ */
291
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
292
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
293
+ };
242
294
  type SlugConfig = {
243
295
  enabled: false;
244
296
  } | {
@@ -262,6 +314,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
262
314
  * regardless of this flag). Any collection can still be a reference target.
263
315
  */
264
316
  reusableBlock?: boolean;
317
+ /**
318
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
319
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
320
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
321
+ * together with each block's `allowChildren` flag. Open by default; block
322
+ * names are checked at compile time by the field type itself, so a typo is a
323
+ * compile error at the `defineCollection` call site.
324
+ */
325
+ structure?: CollectionStructure<TBlocks>;
265
326
  };
266
327
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
267
328
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -925,6 +925,10 @@ const CMS_ERRORS = {
925
925
  status: 404,
926
926
  message: 'Parent block not found'
927
927
  },
928
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
929
+ status: 400,
930
+ message: 'This block type is not allowed inside the target parent'
931
+ },
928
932
  ROOT_NOT_FOUND: {
929
933
  status: 404,
930
934
  message: 'Root block not found in snapshot'
@@ -225,18 +225,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
225
225
  label: string;
226
226
  description?: string;
227
227
  previewImageUrl?: string;
228
+ /**
229
+ * Editor hint: the block-picker category this block is shown under (e.g.
230
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
231
+ * this label; the package never acts on it. Free-form by design; for
232
+ * consistent, autocompleted group names across blocks, reference a shared
233
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
234
+ */
235
+ group?: string;
228
236
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
229
237
  events?: TEvents;
230
238
  } & ({
231
239
  allowChildren?: false;
232
240
  } | {
233
241
  allowChildren: true;
234
- allowedChildBlocks?: string[];
235
242
  });
236
243
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
237
244
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
238
245
  properties: TProps;
239
246
  };
247
+ /**
248
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
249
+ * — declares which child block types that parent (or the literal `'root'`) may
250
+ * contain. There are three mutually-exclusive modes, enforced by the type:
251
+ *
252
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
253
+ * entry at all; `'*'` is just an explicit, readable form.)
254
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
255
+ * a block added to the collection later is rejected until listed. `excludes`
256
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
257
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
258
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
259
+ *
260
+ * Whether a parent accepts children AT ALL is the separate, coarser
261
+ * `allowChildren` gate on the block (the root always accepts children); these
262
+ * rules only refine WHICH children an accepting parent may hold.
263
+ */
264
+ type BlockStructureEntry<TBlockName extends string> = {
265
+ /** `'*'` = open base (optional, for readability). */
266
+ accepts?: '*';
267
+ /** Holds anything except these. */
268
+ excludes?: readonly TBlockName[];
269
+ } | {
270
+ /** Holds ONLY these block types. */
271
+ accepts: readonly TBlockName[];
272
+ /**
273
+ * Forbidden alongside a concrete `accepts` list — the list already names
274
+ * exactly what is allowed, so `excludes` would be ignored.
275
+ */
276
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
277
+ };
278
+ /**
279
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
280
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
281
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
282
+ * the collection's block names and are checked at compile time by
283
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
284
+ *
285
+ * This is the single source of truth that the visual editor (drop-zone gating)
286
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
287
+ * alongside each block's `allowChildren` flag, so they can never diverge.
288
+ */
289
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
290
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
291
+ };
240
292
  type SlugConfig = {
241
293
  enabled: false;
242
294
  } | {
@@ -260,6 +312,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
260
312
  * regardless of this flag). Any collection can still be a reference target.
261
313
  */
262
314
  reusableBlock?: boolean;
315
+ /**
316
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
317
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
318
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
319
+ * together with each block's `allowChildren` flag. Open by default; block
320
+ * names are checked at compile time by the field type itself, so a typo is a
321
+ * compile error at the `defineCollection` call site.
322
+ */
323
+ structure?: CollectionStructure<TBlocks>;
263
324
  };
264
325
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
265
326
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -225,18 +225,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
225
225
  label: string;
226
226
  description?: string;
227
227
  previewImageUrl?: string;
228
+ /**
229
+ * Editor hint: the block-picker category this block is shown under (e.g.
230
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
231
+ * this label; the package never acts on it. Free-form by design; for
232
+ * consistent, autocompleted group names across blocks, reference a shared
233
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
234
+ */
235
+ group?: string;
228
236
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
229
237
  events?: TEvents;
230
238
  } & ({
231
239
  allowChildren?: false;
232
240
  } | {
233
241
  allowChildren: true;
234
- allowedChildBlocks?: string[];
235
242
  });
236
243
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
237
244
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
238
245
  properties: TProps;
239
246
  };
247
+ /**
248
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
249
+ * — declares which child block types that parent (or the literal `'root'`) may
250
+ * contain. There are three mutually-exclusive modes, enforced by the type:
251
+ *
252
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
253
+ * entry at all; `'*'` is just an explicit, readable form.)
254
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
255
+ * a block added to the collection later is rejected until listed. `excludes`
256
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
257
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
258
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
259
+ *
260
+ * Whether a parent accepts children AT ALL is the separate, coarser
261
+ * `allowChildren` gate on the block (the root always accepts children); these
262
+ * rules only refine WHICH children an accepting parent may hold.
263
+ */
264
+ type BlockStructureEntry<TBlockName extends string> = {
265
+ /** `'*'` = open base (optional, for readability). */
266
+ accepts?: '*';
267
+ /** Holds anything except these. */
268
+ excludes?: readonly TBlockName[];
269
+ } | {
270
+ /** Holds ONLY these block types. */
271
+ accepts: readonly TBlockName[];
272
+ /**
273
+ * Forbidden alongside a concrete `accepts` list — the list already names
274
+ * exactly what is allowed, so `excludes` would be ignored.
275
+ */
276
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
277
+ };
278
+ /**
279
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
280
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
281
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
282
+ * the collection's block names and are checked at compile time by
283
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
284
+ *
285
+ * This is the single source of truth that the visual editor (drop-zone gating)
286
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
287
+ * alongside each block's `allowChildren` flag, so they can never diverge.
288
+ */
289
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
290
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
291
+ };
240
292
  type SlugConfig = {
241
293
  enabled: false;
242
294
  } | {
@@ -260,6 +312,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
260
312
  * regardless of this flag). Any collection can still be a reference target.
261
313
  */
262
314
  reusableBlock?: boolean;
315
+ /**
316
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
317
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
318
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
319
+ * together with each block's `allowChildren` flag. Open by default; block
320
+ * names are checked at compile time by the field type itself, so a typo is a
321
+ * compile error at the `defineCollection` call site.
322
+ */
323
+ structure?: CollectionStructure<TBlocks>;
263
324
  };
264
325
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
265
326
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -52,7 +52,8 @@ function isResolvedReference(value) {
52
52
  return {
53
53
  __brand: 'BlocksMap',
54
54
  _components: components,
55
- _events: extractBlockEvents(collection.blocks)
55
+ _events: extractBlockEvents(collection.blocks),
56
+ _collection: collection
56
57
  };
57
58
  }
58
59
  // ============================================================================
@@ -133,18 +133,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
133
133
  label: string;
134
134
  description?: string;
135
135
  previewImageUrl?: string;
136
+ /**
137
+ * Editor hint: the block-picker category this block is shown under (e.g.
138
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
139
+ * this label; the package never acts on it. Free-form by design; for
140
+ * consistent, autocompleted group names across blocks, reference a shared
141
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
142
+ */
143
+ group?: string;
136
144
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
137
145
  events?: TEvents;
138
146
  } & ({
139
147
  allowChildren?: false;
140
148
  } | {
141
149
  allowChildren: true;
142
- allowedChildBlocks?: string[];
143
150
  });
144
151
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
145
152
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
146
153
  properties: TProps;
147
154
  };
155
+ /**
156
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
157
+ * — declares which child block types that parent (or the literal `'root'`) may
158
+ * contain. There are three mutually-exclusive modes, enforced by the type:
159
+ *
160
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
161
+ * entry at all; `'*'` is just an explicit, readable form.)
162
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
163
+ * a block added to the collection later is rejected until listed. `excludes`
164
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
165
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
166
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
167
+ *
168
+ * Whether a parent accepts children AT ALL is the separate, coarser
169
+ * `allowChildren` gate on the block (the root always accepts children); these
170
+ * rules only refine WHICH children an accepting parent may hold.
171
+ */
172
+ type BlockStructureEntry<TBlockName extends string> = {
173
+ /** `'*'` = open base (optional, for readability). */
174
+ accepts?: '*';
175
+ /** Holds anything except these. */
176
+ excludes?: readonly TBlockName[];
177
+ } | {
178
+ /** Holds ONLY these block types. */
179
+ accepts: readonly TBlockName[];
180
+ /**
181
+ * Forbidden alongside a concrete `accepts` list — the list already names
182
+ * exactly what is allowed, so `excludes` would be ignored.
183
+ */
184
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
185
+ };
186
+ /**
187
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
188
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
189
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
190
+ * the collection's block names and are checked at compile time by
191
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
192
+ *
193
+ * This is the single source of truth that the visual editor (drop-zone gating)
194
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
195
+ * alongside each block's `allowChildren` flag, so they can never diverge.
196
+ */
197
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
198
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
199
+ };
148
200
  type SlugConfig = {
149
201
  enabled: false;
150
202
  } | {
@@ -168,6 +220,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
168
220
  * regardless of this flag). Any collection can still be a reference target.
169
221
  */
170
222
  reusableBlock?: boolean;
223
+ /**
224
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
225
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
226
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
227
+ * together with each block's `allowChildren` flag. Open by default; block
228
+ * names are checked at compile time by the field type itself, so a typo is a
229
+ * compile error at the `defineCollection` call site.
230
+ */
231
+ structure?: CollectionStructure<TBlocks>;
171
232
  };
172
233
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
173
234
 
@@ -180,24 +241,33 @@ type BlockComponentProps<TProps extends Record<string, BlockProperty> = Record<s
180
241
  blockId: string;
181
242
  node: BlockTreeNode;
182
243
  };
183
- /** Shorthand to derive block component props from a collection definition. */
244
+ /** Shorthand to derive block component props from a collection definition.
245
+ * `blocks` is optional on `CollectionDefinition`, so the constraint accepts the
246
+ * optional shape and `NonNullable` resolves it — passing `typeof myCollection`
247
+ * directly works, and `TBlock` autocompletes the collection's block names. */
184
248
  type BlockProps<TCollection extends {
185
- blocks: Record<string, AnyBlockDefinition>;
186
- }, TBlock extends keyof TCollection['blocks'] & string> = BlockComponentProps<TCollection['blocks'][TBlock]['properties']>;
249
+ blocks?: Record<string, AnyBlockDefinition>;
250
+ }, TBlock extends keyof NonNullable<TCollection['blocks']> & string> = BlockComponentProps<NonNullable<TCollection['blocks']>[TBlock]['properties']>;
187
251
  type BlockComponentMap<TBlocks extends Record<string, AnyBlockDefinition>> = {
188
252
  [K in keyof TBlocks & string]: (props: BlockComponentProps<TBlocks[K]['properties']>) => ReactNode;
189
253
  };
190
254
  declare function isResolvedReference(value: unknown): value is ResolvedReference;
191
255
  /**
192
256
  * Opaque handle returned by `createBlocksMap`. Pass it to `<BlocksRenderer>`.
193
- * Carries the React component map AND the per-block-type event declarations
194
- * (the runtime half of the M2a typed-events seam) so the renderer can tell a
195
- * functional block (one that declared `events`) from a presentational one.
257
+ * Carries the React component map, the per-block-type event declarations (the
258
+ * runtime half of the M2a typed-events seam, so the renderer can tell a
259
+ * functional block from a presentational one), AND the collection definition
260
+ * itself. Bundling the collection means an editor can consume a single object
261
+ * for both rendering (`_components`) and schema/placement/grouping
262
+ * (`_collection`) — no separate `collection` handoff. The type parameter is
263
+ * preserved so that consumption stays typed; it defaults to the erased
264
+ * `AnyCollectionDefinition` for plain `BlocksMap` annotations.
196
265
  */
197
- type BlocksMap = {
266
+ type BlocksMap<TCollection = AnyCollectionDefinition> = {
198
267
  readonly __brand: 'BlocksMap';
199
268
  readonly _components: Record<string, (props: any) => ReactNode>;
200
269
  readonly _events: Record<string, Record<string, EventDeclaration>>;
270
+ readonly _collection: TCollection;
201
271
  };
202
272
  /**
203
273
  * Extracts the per-block-type event declarations from a collection definition —
@@ -229,7 +299,7 @@ declare function extractBlockEvents(blocks: Record<string, AnyBlockDefinition> |
229
299
  * });
230
300
  * ```
231
301
  */
232
- declare function createBlocksMap<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): BlocksMap;
302
+ declare function createBlocksMap<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): BlocksMap<CollectionDefinition<TProps, TBlocks>>;
233
303
  /**
234
304
  * Renders a `BlockTreeNode` tree using a block component map.
235
305
  *
@@ -133,18 +133,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
133
133
  label: string;
134
134
  description?: string;
135
135
  previewImageUrl?: string;
136
+ /**
137
+ * Editor hint: the block-picker category this block is shown under (e.g.
138
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
139
+ * this label; the package never acts on it. Free-form by design; for
140
+ * consistent, autocompleted group names across blocks, reference a shared
141
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
142
+ */
143
+ group?: string;
136
144
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
137
145
  events?: TEvents;
138
146
  } & ({
139
147
  allowChildren?: false;
140
148
  } | {
141
149
  allowChildren: true;
142
- allowedChildBlocks?: string[];
143
150
  });
144
151
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
145
152
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
146
153
  properties: TProps;
147
154
  };
155
+ /**
156
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
157
+ * — declares which child block types that parent (or the literal `'root'`) may
158
+ * contain. There are three mutually-exclusive modes, enforced by the type:
159
+ *
160
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
161
+ * entry at all; `'*'` is just an explicit, readable form.)
162
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
163
+ * a block added to the collection later is rejected until listed. `excludes`
164
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
165
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
166
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
167
+ *
168
+ * Whether a parent accepts children AT ALL is the separate, coarser
169
+ * `allowChildren` gate on the block (the root always accepts children); these
170
+ * rules only refine WHICH children an accepting parent may hold.
171
+ */
172
+ type BlockStructureEntry<TBlockName extends string> = {
173
+ /** `'*'` = open base (optional, for readability). */
174
+ accepts?: '*';
175
+ /** Holds anything except these. */
176
+ excludes?: readonly TBlockName[];
177
+ } | {
178
+ /** Holds ONLY these block types. */
179
+ accepts: readonly TBlockName[];
180
+ /**
181
+ * Forbidden alongside a concrete `accepts` list — the list already names
182
+ * exactly what is allowed, so `excludes` would be ignored.
183
+ */
184
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
185
+ };
186
+ /**
187
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
188
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
189
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
190
+ * the collection's block names and are checked at compile time by
191
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
192
+ *
193
+ * This is the single source of truth that the visual editor (drop-zone gating)
194
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
195
+ * alongside each block's `allowChildren` flag, so they can never diverge.
196
+ */
197
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
198
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
199
+ };
148
200
  type SlugConfig = {
149
201
  enabled: false;
150
202
  } | {
@@ -168,6 +220,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
168
220
  * regardless of this flag). Any collection can still be a reference target.
169
221
  */
170
222
  reusableBlock?: boolean;
223
+ /**
224
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
225
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
226
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
227
+ * together with each block's `allowChildren` flag. Open by default; block
228
+ * names are checked at compile time by the field type itself, so a typo is a
229
+ * compile error at the `defineCollection` call site.
230
+ */
231
+ structure?: CollectionStructure<TBlocks>;
171
232
  };
172
233
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
173
234
 
@@ -180,24 +241,33 @@ type BlockComponentProps<TProps extends Record<string, BlockProperty> = Record<s
180
241
  blockId: string;
181
242
  node: BlockTreeNode;
182
243
  };
183
- /** Shorthand to derive block component props from a collection definition. */
244
+ /** Shorthand to derive block component props from a collection definition.
245
+ * `blocks` is optional on `CollectionDefinition`, so the constraint accepts the
246
+ * optional shape and `NonNullable` resolves it — passing `typeof myCollection`
247
+ * directly works, and `TBlock` autocompletes the collection's block names. */
184
248
  type BlockProps<TCollection extends {
185
- blocks: Record<string, AnyBlockDefinition>;
186
- }, TBlock extends keyof TCollection['blocks'] & string> = BlockComponentProps<TCollection['blocks'][TBlock]['properties']>;
249
+ blocks?: Record<string, AnyBlockDefinition>;
250
+ }, TBlock extends keyof NonNullable<TCollection['blocks']> & string> = BlockComponentProps<NonNullable<TCollection['blocks']>[TBlock]['properties']>;
187
251
  type BlockComponentMap<TBlocks extends Record<string, AnyBlockDefinition>> = {
188
252
  [K in keyof TBlocks & string]: (props: BlockComponentProps<TBlocks[K]['properties']>) => ReactNode;
189
253
  };
190
254
  declare function isResolvedReference(value: unknown): value is ResolvedReference;
191
255
  /**
192
256
  * Opaque handle returned by `createBlocksMap`. Pass it to `<BlocksRenderer>`.
193
- * Carries the React component map AND the per-block-type event declarations
194
- * (the runtime half of the M2a typed-events seam) so the renderer can tell a
195
- * functional block (one that declared `events`) from a presentational one.
257
+ * Carries the React component map, the per-block-type event declarations (the
258
+ * runtime half of the M2a typed-events seam, so the renderer can tell a
259
+ * functional block from a presentational one), AND the collection definition
260
+ * itself. Bundling the collection means an editor can consume a single object
261
+ * for both rendering (`_components`) and schema/placement/grouping
262
+ * (`_collection`) — no separate `collection` handoff. The type parameter is
263
+ * preserved so that consumption stays typed; it defaults to the erased
264
+ * `AnyCollectionDefinition` for plain `BlocksMap` annotations.
196
265
  */
197
- type BlocksMap = {
266
+ type BlocksMap<TCollection = AnyCollectionDefinition> = {
198
267
  readonly __brand: 'BlocksMap';
199
268
  readonly _components: Record<string, (props: any) => ReactNode>;
200
269
  readonly _events: Record<string, Record<string, EventDeclaration>>;
270
+ readonly _collection: TCollection;
201
271
  };
202
272
  /**
203
273
  * Extracts the per-block-type event declarations from a collection definition —
@@ -229,7 +299,7 @@ declare function extractBlockEvents(blocks: Record<string, AnyBlockDefinition> |
229
299
  * });
230
300
  * ```
231
301
  */
232
- declare function createBlocksMap<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): BlocksMap;
302
+ declare function createBlocksMap<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): BlocksMap<CollectionDefinition<TProps, TBlocks>>;
233
303
  /**
234
304
  * Renders a `BlockTreeNode` tree using a block component map.
235
305
  *
@@ -50,7 +50,8 @@ function isResolvedReference(value) {
50
50
  return {
51
51
  __brand: 'BlocksMap',
52
52
  _components: components,
53
- _events: extractBlockEvents(collection.blocks)
53
+ _events: extractBlockEvents(collection.blocks),
54
+ _collection: collection
54
55
  };
55
56
  }
56
57
  // ============================================================================
@@ -150,6 +150,10 @@ const CMS_ERRORS = {
150
150
  status: 404,
151
151
  message: 'Parent block not found'
152
152
  },
153
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
154
+ status: 400,
155
+ message: 'This block type is not allowed inside the target parent'
156
+ },
153
157
  ROOT_NOT_FOUND: {
154
158
  status: 404,
155
159
  message: 'Root block not found in snapshot'