@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.
@@ -62,18 +62,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
62
62
  label: string;
63
63
  description?: string;
64
64
  previewImageUrl?: string;
65
+ /**
66
+ * Editor hint: the block-picker category this block is shown under (e.g.
67
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
68
+ * this label; the package never acts on it. Free-form by design; for
69
+ * consistent, autocompleted group names across blocks, reference a shared
70
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
71
+ */
72
+ group?: string;
65
73
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
66
74
  events?: TEvents;
67
75
  } & ({
68
76
  allowChildren?: false;
69
77
  } | {
70
78
  allowChildren: true;
71
- allowedChildBlocks?: string[];
72
79
  });
73
80
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
74
81
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
75
82
  properties: TProps;
76
83
  };
84
+ /**
85
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
86
+ * — declares which child block types that parent (or the literal `'root'`) may
87
+ * contain. There are three mutually-exclusive modes, enforced by the type:
88
+ *
89
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
90
+ * entry at all; `'*'` is just an explicit, readable form.)
91
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
92
+ * a block added to the collection later is rejected until listed. `excludes`
93
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
94
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
95
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
96
+ *
97
+ * Whether a parent accepts children AT ALL is the separate, coarser
98
+ * `allowChildren` gate on the block (the root always accepts children); these
99
+ * rules only refine WHICH children an accepting parent may hold.
100
+ */
101
+ type BlockStructureEntry<TBlockName extends string> = {
102
+ /** `'*'` = open base (optional, for readability). */
103
+ accepts?: '*';
104
+ /** Holds anything except these. */
105
+ excludes?: readonly TBlockName[];
106
+ } | {
107
+ /** Holds ONLY these block types. */
108
+ accepts: readonly TBlockName[];
109
+ /**
110
+ * Forbidden alongside a concrete `accepts` list — the list already names
111
+ * exactly what is allowed, so `excludes` would be ignored.
112
+ */
113
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
114
+ };
115
+ /**
116
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
117
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
118
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
119
+ * the collection's block names and are checked at compile time by
120
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
121
+ *
122
+ * This is the single source of truth that the visual editor (drop-zone gating)
123
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
124
+ * alongside each block's `allowChildren` flag, so they can never diverge.
125
+ */
126
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
127
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
128
+ };
77
129
  type SlugConfig = {
78
130
  enabled: false;
79
131
  } | {
@@ -97,6 +149,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
97
149
  * regardless of this flag). Any collection can still be a reference target.
98
150
  */
99
151
  reusableBlock?: boolean;
152
+ /**
153
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
154
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
155
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
156
+ * together with each block's `allowChildren` flag. Open by default; block
157
+ * names are checked at compile time by the field type itself, so a typo is a
158
+ * compile error at the `defineCollection` call site.
159
+ */
160
+ structure?: CollectionStructure<TBlocks>;
100
161
  };
101
162
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
102
163
  type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
@@ -62,18 +62,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
62
62
  label: string;
63
63
  description?: string;
64
64
  previewImageUrl?: string;
65
+ /**
66
+ * Editor hint: the block-picker category this block is shown under (e.g.
67
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
68
+ * this label; the package never acts on it. Free-form by design; for
69
+ * consistent, autocompleted group names across blocks, reference a shared
70
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
71
+ */
72
+ group?: string;
65
73
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
66
74
  events?: TEvents;
67
75
  } & ({
68
76
  allowChildren?: false;
69
77
  } | {
70
78
  allowChildren: true;
71
- allowedChildBlocks?: string[];
72
79
  });
73
80
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
74
81
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
75
82
  properties: TProps;
76
83
  };
84
+ /**
85
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
86
+ * — declares which child block types that parent (or the literal `'root'`) may
87
+ * contain. There are three mutually-exclusive modes, enforced by the type:
88
+ *
89
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
90
+ * entry at all; `'*'` is just an explicit, readable form.)
91
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
92
+ * a block added to the collection later is rejected until listed. `excludes`
93
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
94
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
95
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
96
+ *
97
+ * Whether a parent accepts children AT ALL is the separate, coarser
98
+ * `allowChildren` gate on the block (the root always accepts children); these
99
+ * rules only refine WHICH children an accepting parent may hold.
100
+ */
101
+ type BlockStructureEntry<TBlockName extends string> = {
102
+ /** `'*'` = open base (optional, for readability). */
103
+ accepts?: '*';
104
+ /** Holds anything except these. */
105
+ excludes?: readonly TBlockName[];
106
+ } | {
107
+ /** Holds ONLY these block types. */
108
+ accepts: readonly TBlockName[];
109
+ /**
110
+ * Forbidden alongside a concrete `accepts` list — the list already names
111
+ * exactly what is allowed, so `excludes` would be ignored.
112
+ */
113
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
114
+ };
115
+ /**
116
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
117
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
118
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
119
+ * the collection's block names and are checked at compile time by
120
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
121
+ *
122
+ * This is the single source of truth that the visual editor (drop-zone gating)
123
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
124
+ * alongside each block's `allowChildren` flag, so they can never diverge.
125
+ */
126
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
127
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
128
+ };
77
129
  type SlugConfig = {
78
130
  enabled: false;
79
131
  } | {
@@ -97,6 +149,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
97
149
  * regardless of this flag). Any collection can still be a reference target.
98
150
  */
99
151
  reusableBlock?: boolean;
152
+ /**
153
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
154
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
155
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
156
+ * together with each block's `allowChildren` flag. Open by default; block
157
+ * names are checked at compile time by the field type itself, so a typo is a
158
+ * compile error at the `defineCollection` call site.
159
+ */
160
+ structure?: CollectionStructure<TBlocks>;
100
161
  };
101
162
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
102
163
  type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
@@ -2455,6 +2455,10 @@ const CMS_ERRORS = {
2455
2455
  status: 404,
2456
2456
  message: 'Parent block not found'
2457
2457
  },
2458
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
2459
+ status: 400,
2460
+ message: 'This block type is not allowed inside the target parent'
2461
+ },
2458
2462
  ROOT_NOT_FOUND: {
2459
2463
  status: 404,
2460
2464
  message: 'Root block not found in snapshot'
@@ -323,18 +323,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
323
323
  label: string;
324
324
  description?: string;
325
325
  previewImageUrl?: string;
326
+ /**
327
+ * Editor hint: the block-picker category this block is shown under (e.g.
328
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
329
+ * this label; the package never acts on it. Free-form by design; for
330
+ * consistent, autocompleted group names across blocks, reference a shared
331
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
332
+ */
333
+ group?: string;
326
334
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
327
335
  events?: TEvents;
328
336
  } & ({
329
337
  allowChildren?: false;
330
338
  } | {
331
339
  allowChildren: true;
332
- allowedChildBlocks?: string[];
333
340
  });
334
341
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
335
342
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
336
343
  properties: TProps;
337
344
  };
345
+ /**
346
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
347
+ * — declares which child block types that parent (or the literal `'root'`) may
348
+ * contain. There are three mutually-exclusive modes, enforced by the type:
349
+ *
350
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
351
+ * entry at all; `'*'` is just an explicit, readable form.)
352
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
353
+ * a block added to the collection later is rejected until listed. `excludes`
354
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
355
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
356
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
357
+ *
358
+ * Whether a parent accepts children AT ALL is the separate, coarser
359
+ * `allowChildren` gate on the block (the root always accepts children); these
360
+ * rules only refine WHICH children an accepting parent may hold.
361
+ */
362
+ type BlockStructureEntry<TBlockName extends string> = {
363
+ /** `'*'` = open base (optional, for readability). */
364
+ accepts?: '*';
365
+ /** Holds anything except these. */
366
+ excludes?: readonly TBlockName[];
367
+ } | {
368
+ /** Holds ONLY these block types. */
369
+ accepts: readonly TBlockName[];
370
+ /**
371
+ * Forbidden alongside a concrete `accepts` list — the list already names
372
+ * exactly what is allowed, so `excludes` would be ignored.
373
+ */
374
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
375
+ };
376
+ /**
377
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
378
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
379
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
380
+ * the collection's block names and are checked at compile time by
381
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
382
+ *
383
+ * This is the single source of truth that the visual editor (drop-zone gating)
384
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
385
+ * alongside each block's `allowChildren` flag, so they can never diverge.
386
+ */
387
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
388
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
389
+ };
338
390
  type SlugConfig = {
339
391
  enabled: false;
340
392
  } | {
@@ -358,6 +410,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
358
410
  * regardless of this flag). Any collection can still be a reference target.
359
411
  */
360
412
  reusableBlock?: boolean;
413
+ /**
414
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
415
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
416
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
417
+ * together with each block's `allowChildren` flag. Open by default; block
418
+ * names are checked at compile time by the field type itself, so a typo is a
419
+ * compile error at the `defineCollection` call site.
420
+ */
421
+ structure?: CollectionStructure<TBlocks>;
361
422
  };
362
423
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
363
424
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -323,18 +323,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
323
323
  label: string;
324
324
  description?: string;
325
325
  previewImageUrl?: string;
326
+ /**
327
+ * Editor hint: the block-picker category this block is shown under (e.g.
328
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
329
+ * this label; the package never acts on it. Free-form by design; for
330
+ * consistent, autocompleted group names across blocks, reference a shared
331
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
332
+ */
333
+ group?: string;
326
334
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
327
335
  events?: TEvents;
328
336
  } & ({
329
337
  allowChildren?: false;
330
338
  } | {
331
339
  allowChildren: true;
332
- allowedChildBlocks?: string[];
333
340
  });
334
341
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
335
342
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
336
343
  properties: TProps;
337
344
  };
345
+ /**
346
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
347
+ * — declares which child block types that parent (or the literal `'root'`) may
348
+ * contain. There are three mutually-exclusive modes, enforced by the type:
349
+ *
350
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
351
+ * entry at all; `'*'` is just an explicit, readable form.)
352
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
353
+ * a block added to the collection later is rejected until listed. `excludes`
354
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
355
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
356
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
357
+ *
358
+ * Whether a parent accepts children AT ALL is the separate, coarser
359
+ * `allowChildren` gate on the block (the root always accepts children); these
360
+ * rules only refine WHICH children an accepting parent may hold.
361
+ */
362
+ type BlockStructureEntry<TBlockName extends string> = {
363
+ /** `'*'` = open base (optional, for readability). */
364
+ accepts?: '*';
365
+ /** Holds anything except these. */
366
+ excludes?: readonly TBlockName[];
367
+ } | {
368
+ /** Holds ONLY these block types. */
369
+ accepts: readonly TBlockName[];
370
+ /**
371
+ * Forbidden alongside a concrete `accepts` list — the list already names
372
+ * exactly what is allowed, so `excludes` would be ignored.
373
+ */
374
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
375
+ };
376
+ /**
377
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
378
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
379
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
380
+ * the collection's block names and are checked at compile time by
381
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
382
+ *
383
+ * This is the single source of truth that the visual editor (drop-zone gating)
384
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
385
+ * alongside each block's `allowChildren` flag, so they can never diverge.
386
+ */
387
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
388
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
389
+ };
338
390
  type SlugConfig = {
339
391
  enabled: false;
340
392
  } | {
@@ -358,6 +410,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
358
410
  * regardless of this flag). Any collection can still be a reference target.
359
411
  */
360
412
  reusableBlock?: boolean;
413
+ /**
414
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
415
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
416
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
417
+ * together with each block's `allowChildren` flag. Open by default; block
418
+ * names are checked at compile time by the field type itself, so a typo is a
419
+ * compile error at the `defineCollection` call site.
420
+ */
421
+ structure?: CollectionStructure<TBlocks>;
361
422
  };
362
423
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
363
424
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -2430,6 +2430,10 @@ const CMS_ERRORS = {
2430
2430
  status: 404,
2431
2431
  message: 'Parent block not found'
2432
2432
  },
2433
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
2434
+ status: 400,
2435
+ message: 'This block type is not allowed inside the target parent'
2436
+ },
2433
2437
  ROOT_NOT_FOUND: {
2434
2438
  status: 404,
2435
2439
  message: 'Root block not found in snapshot'
@@ -226,18 +226,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
226
226
  label: string;
227
227
  description?: string;
228
228
  previewImageUrl?: string;
229
+ /**
230
+ * Editor hint: the block-picker category this block is shown under (e.g.
231
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
232
+ * this label; the package never acts on it. Free-form by design; for
233
+ * consistent, autocompleted group names across blocks, reference a shared
234
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
235
+ */
236
+ group?: string;
229
237
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
230
238
  events?: TEvents;
231
239
  } & ({
232
240
  allowChildren?: false;
233
241
  } | {
234
242
  allowChildren: true;
235
- allowedChildBlocks?: string[];
236
243
  });
237
244
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
238
245
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
239
246
  properties: TProps;
240
247
  };
248
+ /**
249
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
250
+ * — declares which child block types that parent (or the literal `'root'`) may
251
+ * contain. There are three mutually-exclusive modes, enforced by the type:
252
+ *
253
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
254
+ * entry at all; `'*'` is just an explicit, readable form.)
255
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
256
+ * a block added to the collection later is rejected until listed. `excludes`
257
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
258
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
259
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
260
+ *
261
+ * Whether a parent accepts children AT ALL is the separate, coarser
262
+ * `allowChildren` gate on the block (the root always accepts children); these
263
+ * rules only refine WHICH children an accepting parent may hold.
264
+ */
265
+ type BlockStructureEntry<TBlockName extends string> = {
266
+ /** `'*'` = open base (optional, for readability). */
267
+ accepts?: '*';
268
+ /** Holds anything except these. */
269
+ excludes?: readonly TBlockName[];
270
+ } | {
271
+ /** Holds ONLY these block types. */
272
+ accepts: readonly TBlockName[];
273
+ /**
274
+ * Forbidden alongside a concrete `accepts` list — the list already names
275
+ * exactly what is allowed, so `excludes` would be ignored.
276
+ */
277
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
278
+ };
279
+ /**
280
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
281
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
282
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
283
+ * the collection's block names and are checked at compile time by
284
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
285
+ *
286
+ * This is the single source of truth that the visual editor (drop-zone gating)
287
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
288
+ * alongside each block's `allowChildren` flag, so they can never diverge.
289
+ */
290
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
291
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
292
+ };
241
293
  type SlugConfig = {
242
294
  enabled: false;
243
295
  } | {
@@ -261,6 +313,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
261
313
  * regardless of this flag). Any collection can still be a reference target.
262
314
  */
263
315
  reusableBlock?: boolean;
316
+ /**
317
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
318
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
319
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
320
+ * together with each block's `allowChildren` flag. Open by default; block
321
+ * names are checked at compile time by the field type itself, so a typo is a
322
+ * compile error at the `defineCollection` call site.
323
+ */
324
+ structure?: CollectionStructure<TBlocks>;
264
325
  };
265
326
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
266
327
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -226,18 +226,70 @@ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<strin
226
226
  label: string;
227
227
  description?: string;
228
228
  previewImageUrl?: string;
229
+ /**
230
+ * Editor hint: the block-picker category this block is shown under (e.g.
231
+ * `'Forms'`, `'Layout'`). Purely presentational — the editor groups blocks by
232
+ * this label; the package never acts on it. Free-form by design; for
233
+ * consistent, autocompleted group names across blocks, reference a shared
234
+ * `as const` object (e.g. `group: BLOCK_GROUPS.forms`).
235
+ */
236
+ group?: string;
229
237
  /** Events this (functional) block can emit — see {@link EventDeclaration}. */
230
238
  events?: TEvents;
231
239
  } & ({
232
240
  allowChildren?: false;
233
241
  } | {
234
242
  allowChildren: true;
235
- allowedChildBlocks?: string[];
236
243
  });
237
244
  type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
238
245
  type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
239
246
  properties: TProps;
240
247
  };
248
+ /**
249
+ * One PARENT's placement rule inside a collection's {@link CollectionStructure}
250
+ * — declares which child block types that parent (or the literal `'root'`) may
251
+ * contain. There are three mutually-exclusive modes, enforced by the type:
252
+ *
253
+ * - **open** — `{}` or `{ accepts: '*' }`: holds any block. (Same as having no
254
+ * entry at all; `'*'` is just an explicit, readable form.)
255
+ * - **whitelist** — `{ accepts: ['a', 'b'] }`: holds ONLY `a`/`b`. Fail-closed —
256
+ * a block added to the collection later is rejected until listed. `excludes`
257
+ * is forbidden here (a concrete `accepts` already says exactly what's allowed).
258
+ * - **blacklist** — `{ excludes: ['z'] }` (or `{ accepts: '*', excludes: ['z'] }`):
259
+ * holds anything EXCEPT `z`. Fail-open — a block added later is accepted.
260
+ *
261
+ * Whether a parent accepts children AT ALL is the separate, coarser
262
+ * `allowChildren` gate on the block (the root always accepts children); these
263
+ * rules only refine WHICH children an accepting parent may hold.
264
+ */
265
+ type BlockStructureEntry<TBlockName extends string> = {
266
+ /** `'*'` = open base (optional, for readability). */
267
+ accepts?: '*';
268
+ /** Holds anything except these. */
269
+ excludes?: readonly TBlockName[];
270
+ } | {
271
+ /** Holds ONLY these block types. */
272
+ accepts: readonly TBlockName[];
273
+ /**
274
+ * Forbidden alongside a concrete `accepts` list — the list already names
275
+ * exactly what is allowed, so `excludes` would be ignored.
276
+ */
277
+ excludes?: "Remove 'excludes': a concrete 'accepts' list already defines exactly which blocks are allowed. Use accepts: '*' with excludes for an all-except list.";
278
+ };
279
+ /**
280
+ * Placement rules for a collection, keyed by PARENT block name (or the literal
281
+ * `'root'` for the top level). Open by default: a parent with no entry holds any
282
+ * block. The keys and the `accepts` / `excludes` block names autocomplete against
283
+ * the collection's block names and are checked at compile time by
284
+ * {@link defineCollection} (the field type alone enforces this — no extra step).
285
+ *
286
+ * This is the single source of truth that the visual editor (drop-zone gating)
287
+ * and the server guard (createBlock / moveBlock / duplicateBlock) both read,
288
+ * alongside each block's `allowChildren` flag, so they can never diverge.
289
+ */
290
+ type CollectionStructure<TBlocks extends Record<string, AnyBlockDefinition>> = {
291
+ [K in keyof TBlocks | 'root']?: BlockStructureEntry<keyof TBlocks & string>;
292
+ };
241
293
  type SlugConfig = {
242
294
  enabled: false;
243
295
  } | {
@@ -261,6 +313,15 @@ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<
261
313
  * regardless of this flag). Any collection can still be a reference target.
262
314
  */
263
315
  reusableBlock?: boolean;
316
+ /**
317
+ * Placement rules keyed by PARENT block name (or `'root'`) — which children
318
+ * each container may hold, via `accepts` (whitelist) / `excludes` (blacklist)
319
+ * (see {@link CollectionStructure}). Read by the editor and the server guard
320
+ * together with each block's `allowChildren` flag. Open by default; block
321
+ * names are checked at compile time by the field type itself, so a typo is a
322
+ * compile error at the `defineCollection` call site.
323
+ */
324
+ structure?: CollectionStructure<TBlocks>;
264
325
  };
265
326
  type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
266
327
  type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
@@ -950,6 +950,10 @@ const CMS_ERRORS = {
950
950
  status: 404,
951
951
  message: 'Parent block not found'
952
952
  },
953
+ BLOCK_NOT_ALLOWED_IN_PARENT: {
954
+ status: 400,
955
+ message: 'This block type is not allowed inside the target parent'
956
+ },
953
957
  ROOT_NOT_FOUND: {
954
958
  status: 404,
955
959
  message: 'Root block not found in snapshot'
@@ -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'> & {