@jackuait/blok 0.10.8 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-ClCrnWuI.mjs → blok-DbRn9adY.mjs} +2454 -2057
  3. package/dist/chunks/{constants-BoE5frJm.mjs → constants-C9lsSOXl.mjs} +4 -3
  4. package/dist/chunks/{tools-HQPJLj5m.mjs → tools-D0W3_dlA.mjs} +502 -497
  5. package/dist/full.mjs +3 -3
  6. package/dist/react.mjs +2 -2
  7. package/dist/tools.mjs +2 -2
  8. package/package.json +3 -6
  9. package/src/components/block/index.ts +36 -0
  10. package/src/components/blocks.ts +191 -5
  11. package/src/components/modules/api/blocks.ts +6 -4
  12. package/src/components/modules/blockEvents/composers/keyboardNavigation.ts +17 -6
  13. package/src/components/modules/blockManager/blockManager.ts +364 -23
  14. package/src/components/modules/blockManager/hierarchy.ts +164 -8
  15. package/src/components/modules/blockManager/operations.ts +223 -26
  16. package/src/components/modules/blockManager/types.ts +13 -1
  17. package/src/components/modules/blockManager/yjs-sync.ts +48 -3
  18. package/src/components/modules/drag/DragController.ts +209 -8
  19. package/src/components/modules/drag/operations/DragOperations.ts +153 -20
  20. package/src/components/modules/paste/handlers/base.ts +48 -20
  21. package/src/components/modules/paste/handlers/blok-data-handler.ts +93 -45
  22. package/src/components/modules/paste/index.ts +20 -0
  23. package/src/components/modules/saver.ts +75 -5
  24. package/src/components/modules/toolbar/index.ts +41 -60
  25. package/src/components/modules/uiControllers/controllers/keyboard.ts +20 -0
  26. package/src/components/modules/yjs/block-observer.ts +87 -23
  27. package/src/components/modules/yjs/document-store.ts +37 -11
  28. package/src/components/modules/yjs/index.ts +83 -7
  29. package/src/components/modules/yjs/types.ts +35 -2
  30. package/src/components/modules/yjs/undo-history.ts +116 -5
  31. package/src/components/utils/data-model-transform.ts +81 -7
  32. package/src/components/utils/hierarchy-invariant.ts +137 -0
  33. package/src/styles/main.css +5 -0
  34. package/src/tools/callout/constants.ts +0 -1
  35. package/src/tools/callout/dom-builder.ts +1 -11
  36. package/src/tools/callout/index.ts +0 -6
  37. package/src/tools/header/index.ts +14 -1
  38. package/src/tools/toggle/constants.ts +2 -1
  39. package/src/tools/toggle/dom-builder.ts +7 -0
  40. package/src/tools/toggle/index.ts +14 -1
  41. package/src/tools/toggle/toggle-lifecycle.ts +24 -0
package/dist/full.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { n as e, t } from "./chunks/blok-ClCrnWuI.mjs";
2
- import { ur as n } from "./chunks/constants-BoE5frJm.mjs";
1
+ import { n as e, t } from "./chunks/blok-DbRn9adY.mjs";
2
+ import { ur as n } from "./chunks/constants-C9lsSOXl.mjs";
3
3
  import { t as r } from "./chunks/objectSpread2-CWwMYL_U.mjs";
4
- import { a as i, b as a, c as o, g as s, i as c, l, n as u, o as d, s as f, t as p, v as m, y as h } from "./chunks/tools-HQPJLj5m.mjs";
4
+ import { a as i, b as a, c as o, g as s, i as c, l, n as u, o as d, s as f, t as p, v as m, y as h } from "./chunks/tools-D0W3_dlA.mjs";
5
5
  //#region src/full.ts
6
6
  var g = {
7
7
  paragraph: {
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as e } from "./chunks/blok-ClCrnWuI.mjs";
2
- import "./chunks/constants-BoE5frJm.mjs";
1
+ import { t as e } from "./chunks/blok-DbRn9adY.mjs";
2
+ import "./chunks/constants-C9lsSOXl.mjs";
3
3
  import { t } from "./chunks/objectSpread2-CWwMYL_U.mjs";
4
4
  import { t as n } from "./chunks/objectWithoutProperties-Dci1-l7D.mjs";
5
5
  import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
package/dist/tools.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { m as e } from "./chunks/constants-BoE5frJm.mjs";
2
- import { _ as t, a as n, b as r, c as i, d as a, f as o, g as s, h as c, i as l, l as u, m as d, n as f, o as p, p as m, r as h, s as g, t as _, u as v, v as y, y as b } from "./chunks/tools-HQPJLj5m.mjs";
1
+ import { m as e } from "./chunks/constants-C9lsSOXl.mjs";
2
+ import { _ as t, a as n, b as r, c as i, d as a, f as o, g as s, h as c, i as l, l as u, m as d, n as f, o as p, p as m, r as h, s as g, t as _, u as v, v as y, y as b } from "./chunks/tools-D0W3_dlA.mjs";
3
3
  export { u as Bold, c as Callout, v as Code, e as Convert, d as Database, m as DatabaseRow, o as Divider, b as Header, h as InlineCode, i as Italic, g as Link, y as List, p as Marker, r as Paragraph, a as Quote, l as Strikethrough, t as Table, s as Toggle, n as Underline, _ as defaultBlockTools, f as defaultInlineTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackuait/blok",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
5
5
  "module": "dist/blok.mjs",
6
6
  "types": "./types/index.d.ts",
@@ -116,8 +116,8 @@
116
116
  "perf:compare": "node scripts/analyze-performance.mjs --baseline",
117
117
  "perf:dashboard": "node scripts/generate-performance-dashboard.mjs",
118
118
  "e2e:validate-categories": "node scripts/validate-test-categories.mjs",
119
- "size": "size-limit",
120
- "size:why": "size-limit --why",
119
+ "validate:spec-coverage": "node scripts/validate-spec-file-coverage.mjs",
120
+ "ci:timing": "node scripts/ci-timing-probe.mjs",
121
121
  "release": "node scripts/release.mjs",
122
122
  "release:preflight": "yarn lint && yarn test",
123
123
  "unused-css": "node scripts/unused-css-finder/cli.mjs"
@@ -137,8 +137,6 @@
137
137
  "@commitlint/cli": "20.4.4",
138
138
  "@commitlint/config-conventional": "20.4.4",
139
139
  "@playwright/test": "1.58.2",
140
- "@size-limit/esbuild-why": "12.0.1",
141
- "@size-limit/preset-small-lib": "12.0.1",
142
140
  "@storybook/addon-a11y": "10.2.19",
143
141
  "@storybook/addon-vitest": "10.2.19",
144
142
  "@storybook/html-vite": "10.2.19",
@@ -177,7 +175,6 @@
177
175
  "rollup-plugin-license": "3.7.0",
178
176
  "semantic-release": "25.0.3",
179
177
  "shiki": "^4.0.2",
180
- "size-limit": "12.0.1",
181
178
  "storybook": "10.2.19",
182
179
  "tailwindcss": "4.2.1",
183
180
  "typescript": "5.9.3",
@@ -224,6 +224,14 @@ export class Block extends EventsDispatcher<BlockEvents> {
224
224
  */
225
225
  private draggableCleanup: (() => void) | null = null;
226
226
 
227
+ /**
228
+ * Callbacks to invoke at the top of destroy(). External subscribers
229
+ * (e.g. an in-flight drag) register here so they can cancel themselves
230
+ * when the Block instance they hold becomes invalid — preventing stale
231
+ * Block references from reaching drop handlers mid-drag.
232
+ */
233
+ private destroyCallbacks: Set<() => void> = new Set();
234
+
227
235
  /**
228
236
  * Manages tool element composition and rendering
229
237
  */
@@ -529,10 +537,38 @@ export class Block extends EventsDispatcher<BlockEvents> {
529
537
  return this.dataPersistenceManager.setData(newData);
530
538
  }
531
539
 
540
+ /**
541
+ * Register a callback to be invoked when this Block is destroyed.
542
+ * Returns an unregister function.
543
+ *
544
+ * Used by DragController to cancel an in-flight drag when its source
545
+ * block is replaced (Yjs remote update, blockManager.update, tool
546
+ * conversion, etc). Without this hook, the state machine would hold
547
+ * a stale Block reference and drop the wrong block on mouseup.
548
+ * @param callback - Function to call during destroy()
549
+ * @returns Unregister function
550
+ */
551
+ public addDestroyCallback(callback: () => void): () => void {
552
+ this.destroyCallbacks.add(callback);
553
+
554
+ return (): void => {
555
+ this.destroyCallbacks.delete(callback);
556
+ };
557
+ }
558
+
532
559
  /**
533
560
  * Call Tool instance destroy method
534
561
  */
535
562
  public destroy(): void {
563
+ // Notify external subscribers FIRST — before any state is torn down —
564
+ // so listeners (e.g. DragController) can cancel cleanly while the
565
+ // Block is still consistent. Copy to a local list so a callback that
566
+ // unregisters itself doesn't mutate the set during iteration.
567
+ const callbacks = Array.from(this.destroyCallbacks);
568
+
569
+ this.destroyCallbacks.clear();
570
+ callbacks.forEach((cb) => cb());
571
+
536
572
  this.mutationHandler.destroy();
537
573
  this.inputManager.destroy();
538
574
 
@@ -117,6 +117,25 @@ export class Blocks {
117
117
  * @param {boolean} skipDOM - if true, do not manipulate DOM (useful when SortableJS already did it)
118
118
  */
119
119
  public move(toIndex: number, fromIndex: number, skipDOM = false, skipMovedHook = false): void {
120
+ /**
121
+ * Invalid-index guard (regression: wrong-block-dropped).
122
+ *
123
+ * `Array.splice(-1, 1)` removes the LAST element, and `splice(N, 1)` where
124
+ * N is past the end does nothing — both hide stale-reference bugs as silent
125
+ * data corruption. Every caller in every surface (drag, yjs-sync, API,
126
+ * tool conversion, undo) flows through here, so this is the lowest-level
127
+ * point to reject nonsense indices and keep the "wrong block dropped"
128
+ * class of bug from ever reappearing.
129
+ */
130
+ if (
131
+ fromIndex < 0 ||
132
+ fromIndex >= this.blocks.length ||
133
+ toIndex < 0 ||
134
+ toIndex >= this.blocks.length
135
+ ) {
136
+ return;
137
+ }
138
+
120
139
  /**
121
140
  * cut out the block, move the DOM element and insert at the desired index
122
141
  * again (the shifting within the blocks array will happen automatically).
@@ -156,8 +175,33 @@ export class Blocks {
156
175
  * @param {number} index — index to insert Block
157
176
  * @param {Block} block — Block to insert
158
177
  * @param {boolean} replace — it true, replace block on given index
178
+ * @param {boolean} appendToWorkingArea — if true, append to workingArea end (ignores position)
179
+ * @param {boolean} forceTopLevel — if true, place holder at workingArea root level even when
180
+ * the previous block in the flat array is nested inside another container. Used when the
181
+ * caller knows the new block is logically top-level (parentId === null). Prevents the
182
+ * Enter-after-callout regression family where insertAdjacentElement('afterend',
183
+ * previousBlock.holder) would otherwise drop the new holder inside a nested container.
159
184
  */
160
- public insert(index: number, block: Block, replace = false, appendToWorkingArea = false): void {
185
+ public insert(
186
+ index: number,
187
+ block: Block,
188
+ replace = false,
189
+ appendToWorkingArea = false,
190
+ forceTopLevel = false
191
+ ): void {
192
+ /**
193
+ * Invalid-index guard (regression: wrong-block-dropped via alt+drag).
194
+ *
195
+ * `Array.splice(-1, 0, block)` inserts BEFORE the last element — a silent
196
+ * divergence between the flat blocks array and the DOM that corrupts the
197
+ * next move() operation and drops an unrelated block. Mirrors the guard
198
+ * in Blocks.move so every caller (drag duplicate, yjs-sync, undo, API) is
199
+ * protected at the lowest level.
200
+ */
201
+ if (index < 0) {
202
+ return;
203
+ }
204
+
161
205
  if (!this.length) {
162
206
  this.push(block);
163
207
 
@@ -202,12 +246,62 @@ export class Blocks {
202
246
 
203
247
  if (insertIndex > 0) {
204
248
  const previousBlock = this.blocks[insertIndex - 1];
249
+ const nextBlock = this.blocks[insertIndex + 1] as Block | undefined;
250
+
251
+ if (forceTopLevel) {
252
+ this.insertAtRootLevel(block, insertIndex);
253
+
254
+ return;
255
+ }
256
+
257
+ /**
258
+ * Universal defense against the Enter-after-nested bug family, covering
259
+ * every insertion path (paste, toolbox, slash menu, plus button, markdown
260
+ * shortcut, conversion, split, public API, yjs-sync, drag, programmatic).
261
+ *
262
+ * The only configuration where flat-array adjacency disagrees with the
263
+ * caller's intended DOM container is when the predecessor is DOM-nested
264
+ * (inside a callout, toggle, header-toggleable, table cell, or any future
265
+ * nesting tool's `[data-blok-nested-blocks]` container) AND the successor
266
+ * is at workingArea root. In that case, anchoring `afterend previous`
267
+ * would leak the new holder into the nested container even though the
268
+ * caller's logical intent is a top-level sibling. Route to the successor
269
+ * instead, which correctly anchors at workingArea root.
270
+ *
271
+ * Every OTHER configuration stays on the original `afterend previous`
272
+ * rule, which preserves:
273
+ * - Plain top-level insertion: prev at root, any next → afterend prev.
274
+ * - Nested-sibling insertion (paste inside a toggle child, no top-level
275
+ * follower): prev nested, next undefined or same-container → afterend
276
+ * prev keeps the new block in the same nested container.
277
+ * - Insertion between two same-parent nested children: prev and next
278
+ * both in same nested container → afterend prev stays in container.
279
+ *
280
+ * The forceTopLevel flag is still honored above for callers that want to
281
+ * skip past a trailing nested subtree. This rule is the defense for every
282
+ * path that does NOT explicitly set forceTopLevel.
283
+ */
284
+ const prevIsAtRoot = previousBlock.holder.parentElement === this.workingArea;
285
+ const nextIsAtRoot = nextBlock !== undefined
286
+ && nextBlock.holder.parentElement === this.workingArea;
287
+
288
+ if (!prevIsAtRoot && nextIsAtRoot && nextBlock !== undefined) {
289
+ this.insertToDOM(block, 'beforebegin', nextBlock);
290
+
291
+ return;
292
+ }
205
293
 
206
294
  this.insertToDOM(block, 'afterend', previousBlock);
207
295
 
208
296
  return;
209
297
  }
210
298
 
299
+ if (forceTopLevel) {
300
+ this.insertAtRootLevel(block, insertIndex);
301
+
302
+ return;
303
+ }
304
+
211
305
  const nextBlock = this.blocks[insertIndex + 1] as Block | undefined;
212
306
 
213
307
  if (nextBlock) {
@@ -231,8 +325,12 @@ export class Blocks {
231
325
 
232
326
  const prevBlock = this.blocks[index];
233
327
 
234
- prevBlock.holder.replaceWith(block.holder);
235
328
  prevBlock.call(BlockToolAPI.REMOVED);
329
+ // Destroy releases the drag-handle listener bound to the shared settings
330
+ // toggler; skipping it leaves the orphan Block wired up and dragging it
331
+ // on next mousedown instead of whatever the user intended.
332
+ prevBlock.destroy();
333
+ prevBlock.holder.replaceWith(block.holder);
236
334
 
237
335
  this.blocks[index] = block;
238
336
 
@@ -245,6 +343,17 @@ export class Blocks {
245
343
  * @param index - index to insert blocks at
246
344
  */
247
345
  public insertMany(blocks: Block[], index: number ): void {
346
+ /**
347
+ * Invalid-index guard (regression: wrong-block-dropped family).
348
+ * Mirrors Blocks.move and Blocks.insert — `splice(-1, 0, ...blocks)`
349
+ * silently inserts BEFORE the last element, diverging array from DOM.
350
+ * Yjs batch-add paths feed this method with computed indices; a stale
351
+ * input would otherwise cause a later move() to drop an unrelated block.
352
+ */
353
+ if (index < 0) {
354
+ return;
355
+ }
356
+
248
357
  const fragment = new DocumentFragment();
249
358
 
250
359
  for (const block of blocks) {
@@ -293,6 +402,21 @@ export class Blocks {
293
402
  */
294
403
  public remove(index: number): void {
295
404
  const removeIndex = isNaN(index) ? this.length - 1 : index;
405
+
406
+ /**
407
+ * Layer 15: invalid-index guard (regression: wrong-block-dropped family).
408
+ *
409
+ * Before this guard, a stale caller passing a negative or out-of-bounds
410
+ * index would crash on `blockToRemove.call(REMOVED)` (undefined.call),
411
+ * aborting the surrounding batch (e.g. a Yjs undo transaction) partway
412
+ * and leaving the flat array inconsistent with the DOM — exactly the
413
+ * soil that grows the wrong-block-dropped bug class. Reject nonsense
414
+ * indices at the lowest level instead of throwing.
415
+ */
416
+ if (removeIndex < 0 || removeIndex >= this.blocks.length) {
417
+ return;
418
+ }
419
+
296
420
  const blockToRemove = this.blocks[removeIndex];
297
421
 
298
422
  /**
@@ -311,10 +435,16 @@ export class Blocks {
311
435
  * Remove all blocks
312
436
  */
313
437
  public removeAll(): void {
314
- this.workingArea.innerHTML = '';
315
-
316
- this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));
438
+ // Destroy each block so draggable listeners bound to shared DOM handles
439
+ // (e.g. the settings toggler) are released. Skipping destroy leaves stale
440
+ // mousedown handlers on the shared toggler, which later fire for whichever
441
+ // block is hovered and drags the wrong block.
442
+ this.blocks.forEach((block) => {
443
+ block.call(BlockToolAPI.REMOVED);
444
+ block.destroy();
445
+ });
317
446
 
447
+ this.workingArea.innerHTML = '';
318
448
  this.blocks.length = 0;
319
449
  }
320
450
 
@@ -327,6 +457,19 @@ export class Blocks {
327
457
  public insertAfter(targetBlock: Block, newBlock: Block): void {
328
458
  const index = this.blocks.indexOf(targetBlock);
329
459
 
460
+ /**
461
+ * Layer 14: stale target guard (regression: wrong-block-dropped family).
462
+ *
463
+ * Without this guard, a stale `targetBlock` makes `indexOf` return `-1`,
464
+ * then `insert(0, newBlock)` teleports the new block to the TOP of the
465
+ * document — the same symptom as wrong-block-dropped. insertAfter has
466
+ * no live callers today, but codifying the guard now prevents a future
467
+ * consumer from reintroducing the bug class through this entry point.
468
+ */
469
+ if (index === -1) {
470
+ return;
471
+ }
472
+
330
473
  this.insert(index + 1, newBlock);
331
474
  }
332
475
 
@@ -357,6 +500,16 @@ export class Blocks {
357
500
  * blocks exist in the array before any lifecycle hooks run.
358
501
  */
359
502
  public addToArray(index: number, block: Block): void {
503
+ /**
504
+ * Invalid-index guard (regression: wrong-block-dropped family).
505
+ * Same splice(-1, 0) vulnerability as Blocks.move/insert/insertMany.
506
+ * yjs-sync batch-add calls this during undo of hierarchical blocks;
507
+ * negative input would silently corrupt the flat array.
508
+ */
509
+ if (index < 0) {
510
+ return;
511
+ }
512
+
360
513
  const insertIndex = index > this.length ? this.length : index;
361
514
 
362
515
  this.blocks.splice(insertIndex, 0, block);
@@ -420,6 +573,39 @@ export class Blocks {
420
573
  block.call(BlockToolAPI.RENDERED);
421
574
  }
422
575
 
576
+ /**
577
+ * Place a single block's holder at workingArea root level. Finds the next
578
+ * top-level block at or after `insertIndex` in the flat array and inserts
579
+ * before it; falls back to appending to workingArea when no top-level
580
+ * sibling follows. Keeps DOM parent aligned with `parentId === null`.
581
+ */
582
+ private insertAtRootLevel(block: Block, insertIndex: number): void {
583
+ const nextTopLevel = this.blocks.slice(insertIndex + 1).find(
584
+ (b) => b.holder.parentElement === this.workingArea
585
+ );
586
+
587
+ if (nextTopLevel !== undefined) {
588
+ nextTopLevel.holder.insertAdjacentElement('beforebegin', block.holder);
589
+ } else {
590
+ this.workingArea.appendChild(block.holder);
591
+ }
592
+
593
+ /**
594
+ * Invariant: forceTopLevel insertion must land at workingArea root. If it
595
+ * does not, a future change broke the resolution logic above and the
596
+ * Enter-after-callout bug is one wrong neighbor away from reappearing.
597
+ * Fail loud and early so the regression cannot sneak in silently.
598
+ */
599
+ if (block.holder.parentElement !== this.workingArea) {
600
+ throw new Error(
601
+ '[Blocks.insertAtRootLevel] invariant violated: block holder did not land at workingArea root. ' +
602
+ 'This indicates the Enter-after-callout regression guard is broken.'
603
+ );
604
+ }
605
+
606
+ block.call(BlockToolAPI.RENDERED);
607
+ }
608
+
423
609
  /**
424
610
  * When the previous block in the flat array is nested (e.g., a table cell
425
611
  * paragraph), inserting after its top-level ancestor would place content
@@ -442,10 +442,12 @@ export class BlocksAPI extends Module {
442
442
 
443
443
  const newBlock = this.Blok.BlockManager.insertInsideParent(parentId, insertIndex);
444
444
 
445
- // Boundary after the atomic op so any deferred DOM callbacks stay in the same undo group.
446
- queueMicrotask(() => {
447
- this.Blok.YjsManager.stopCapturing();
448
- });
445
+ // NOTE: Do NOT call stopCapturing in a trailing microtask. The operations layer
446
+ // uses extendThroughRAF on its atomic wrapper to keep isSyncingFromYjs true
447
+ // through the next RAF, suppressing any stray mutation-observer-driven Yjs
448
+ // writes from deferred DOM callbacks. A trailing stopCapturing would force
449
+ // those late writes into a SEPARATE undo group, splitting the insertion
450
+ // across two CMD+Z pops.
449
451
 
450
452
  return new BlockAPI(newBlock);
451
453
  };
@@ -230,9 +230,18 @@ export class KeyboardNavigation extends BlockEventComposer {
230
230
  }
231
231
 
232
232
  private createBlockOnEnter(currentBlock: Block): Block {
233
+ /**
234
+ * When the current block is top-level (parentId === null), pass forceTopLevel
235
+ * so the new block's DOM holder is anchored at workingArea root level even if
236
+ * the previous block in the flat array is nested inside a callout/toggle/table.
237
+ * Without this, insertAdjacentElement('afterend', previousBlock.holder) drops
238
+ * the new holder inside the nested container — the Enter-after-callout bug.
239
+ */
240
+ const isCurrentTopLevel = currentBlock.parentId === null;
241
+
233
242
  // Case 1: Caret at start - insert block above
234
243
  if (currentBlock.currentInput !== undefined && isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia && (currentBlock.parentId === null || !currentBlock.isEmpty)) {
235
- const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex);
244
+ const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex, false, false, isCurrentTopLevel);
236
245
 
237
246
  /**
238
247
  * When the current block is a child of a toggle, the new block inserted above
@@ -258,19 +267,21 @@ export class KeyboardNavigation extends BlockEventComposer {
258
267
  return promotedBlock;
259
268
  }
260
269
 
261
- const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1);
262
-
263
270
  /**
264
271
  * When the current block is an open toggle (heading or list item),
265
272
  * the new block should become a child of the toggle rather than a sibling.
266
273
  * Detect via the data-blok-toggle-open DOM attribute set by toggle tools.
267
274
  *
268
- * When the current block is already a child of a toggle, the new block
269
- * should inherit the same parent so it stays inside the toggle.
275
+ * forceTopLevel is safe to pass only when the current block is top-level
276
+ * AND is not an open toggle (which intentionally nests the new block as its child).
270
277
  */
271
278
  const toggleWrapper = currentBlock.holder.querySelector('[data-blok-toggle-open]');
279
+ const isToggleOpen = toggleWrapper?.getAttribute('data-blok-toggle-open') === 'true';
280
+ const forceTopLevelCase2 = isCurrentTopLevel && !isToggleOpen;
281
+
282
+ const newBlock = this.Blok.BlockManager.insertDefaultBlockAtIndex(this.Blok.BlockManager.currentBlockIndex + 1, false, false, forceTopLevelCase2);
272
283
 
273
- if (toggleWrapper?.getAttribute('data-blok-toggle-open') === 'true') {
284
+ if (isToggleOpen) {
274
285
  this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.id);
275
286
  } else if (currentBlock.parentId !== null && newBlock.parentId !== currentBlock.parentId) {
276
287
  this.Blok.BlockManager.setBlockParent(newBlock, currentBlock.parentId);