edytor 0.1.0-next.16 → 0.1.0-next.18

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 (85) hide show
  1. package/README.md +14 -13
  2. package/dist/block/block.svelte.d.ts +2 -0
  3. package/dist/block/block.utils.d.ts +14 -0
  4. package/dist/clipboard/createClipboardFragment.js +5 -2
  5. package/dist/clipboard/htmlFlow.js +8 -0
  6. package/dist/components/Block.svelte +13 -5
  7. package/dist/components/Edytor.svelte +3 -8
  8. package/dist/crdt/document.d.ts +3 -2
  9. package/dist/crdt/document.js +9 -4
  10. package/dist/crdt/edytor-doc.d.ts +17 -0
  11. package/dist/crdt/edytor-doc.js +329 -24
  12. package/dist/crdt/flow.d.ts +9 -2
  13. package/dist/crdt/flow.js +25 -2
  14. package/dist/crdt/index.d.ts +8 -1
  15. package/dist/crdt/index.js +1 -1
  16. package/dist/crdt/placement/model.d.ts +25 -5
  17. package/dist/crdt/placement/model.js +28 -14
  18. package/dist/crdt/rangeDelete.d.ts +7 -0
  19. package/dist/crdt/rangeDelete.js +2 -0
  20. package/dist/crdt/semantics.d.ts +27 -2
  21. package/dist/crdt/semantics.js +18 -3
  22. package/dist/crdt/text/runs.d.ts +12 -1
  23. package/dist/crdt/text/runs.js +93 -4
  24. package/dist/edytor.svelte.js +5 -7
  25. package/dist/events/beforeInputDeleteCommands.js +19 -3
  26. package/dist/events/onBeforeInput.d.ts +1 -1
  27. package/dist/events/onBeforeInput.js +28 -1
  28. package/dist/events/onDrop.d.ts +1 -1
  29. package/dist/events/onFocus.d.ts +11 -0
  30. package/dist/events/onFocus.js +80 -16
  31. package/dist/events/onKeyDown.js +25 -0
  32. package/dist/events/onPaste.js +9 -3
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/kinds.d.ts +23 -0
  36. package/dist/kinds.js +49 -1
  37. package/dist/plugins/blockHandles/BlockHandle.svelte +9 -3
  38. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +110 -6
  39. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +430 -25
  40. package/dist/plugins/blockHandles/BlockHandles.svelte +94 -5
  41. package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +10 -3
  42. package/dist/plugins/blockHandles/blockHandlesPlugin.js +38 -6
  43. package/dist/plugins/blockHandles/dragPreview.d.ts +2 -2
  44. package/dist/plugins/blockHandles/dragPreview.js +6 -4
  45. package/dist/plugins/blockMenu/BlockMenu.svelte +9 -1
  46. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +10 -1
  47. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +32 -12
  48. package/dist/plugins/blockMenu/blockMenuPlugin.js +35 -6
  49. package/dist/plugins/columns/ColumnResize.svelte +87 -0
  50. package/dist/plugins/columns/ColumnResize.svelte.d.ts +7 -0
  51. package/dist/plugins/columns/ColumnsPlugin.svelte +237 -0
  52. package/dist/plugins/columns/ColumnsPlugin.svelte.d.ts +40 -0
  53. package/dist/plugins/columns/columns.css +52 -0
  54. package/dist/plugins/columns/gaps.d.ts +33 -0
  55. package/dist/plugins/columns/gaps.js +41 -0
  56. package/dist/plugins/columns/resize.svelte.d.ts +109 -0
  57. package/dist/plugins/columns/resize.svelte.js +253 -0
  58. package/dist/plugins/columns/stacking.d.ts +7 -0
  59. package/dist/plugins/columns/stacking.js +7 -0
  60. package/dist/plugins/icons.js +10 -0
  61. package/dist/plugins/index.d.ts +1 -0
  62. package/dist/plugins/index.js +1 -0
  63. package/dist/plugins.d.ts +21 -4
  64. package/dist/selection/replaceSelection.d.ts +19 -1
  65. package/dist/selection/replaceSelection.js +58 -4
  66. package/dist/selection/selection.svelte.d.ts +20 -4
  67. package/dist/selection/selection.svelte.js +26 -2
  68. package/dist/session/attempt.d.ts +15 -0
  69. package/dist/session/attempt.js +20 -0
  70. package/dist/session/bindings.js +28 -8
  71. package/dist/session/commands.d.ts +2 -0
  72. package/dist/session/commands.js +19 -2
  73. package/dist/session/composition.svelte.d.ts +17 -2
  74. package/dist/session/composition.svelte.js +49 -13
  75. package/dist/session/history.d.ts +8 -4
  76. package/dist/session/history.js +20 -4
  77. package/dist/session/moves.d.ts +9 -2
  78. package/dist/session/moves.js +42 -10
  79. package/dist/session/navigation.d.ts +7 -0
  80. package/dist/session/navigation.js +22 -5
  81. package/dist/surface/observer.svelte.js +3 -0
  82. package/dist/surface/projector.svelte.d.ts +26 -2
  83. package/dist/surface/projector.svelte.js +69 -3
  84. package/dist/themes/notion.css +16 -0
  85. package/package.json +1 -1
package/README.md CHANGED
@@ -7,22 +7,23 @@
7
7
  [![Svelte v5](https://img.shields.io/badge/Svelte-v5-FF3E00.svg)](https://svelte.dev)
8
8
 
9
9
  <p>
10
- <a href="https://edytor-docs.beynar.workers.dev/docs">Documentation</a> •
10
+ <a href="https://edytor.dev/docs">Documentation</a> •
11
11
  <a href="#quick-start">Quick start</a> •
12
- <a href="https://edytor-docs.beynar.workers.dev/docs/reference/migration">Migrating from 0.0.11</a>
12
+ <a href="https://edytor.dev/docs/reference/migration">Migrating from 0.0.11</a>
13
13
  </p>
14
14
  </div>
15
15
 
16
16
  Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
17
17
 
18
- > **Work in progress.** Edytor is a pre-release (`0.1.0-next.16`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
18
+ > **Work in progress.** Edytor is a pre-release (`0.1.0-next.18`, `edytor@next` on npm) and not ready for production; the API changes between releases without a compatibility layer. The untagged `edytor@0.0.11` on npm predates the current API. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
19
19
 
20
20
  ## Features
21
21
 
22
22
  - **Notion-style editing out of the box.** `<Edytor />` alone is a rich text editor with images: headings, lists, to-dos, toggles, callouts, quotes, dividers, ten marks, Notion's markdown shortcuts and hotkeys. Add the Notion theme, block handles with a block menu (drag one block, or every block the selection covers), a slash menu, a selection toolbar and code blocks.
23
+ - **Columns, opt-in.** List `columnsPlugin` for Notion's multi-column layouts: drag a block to the edge of another to put them side by side, or pick "2 columns" to "5 columns"; drag the gap to resize. Layouts converge across collaborators and the room ([Columns](https://edytor.dev/docs/plugins/columns)).
23
24
  - **Your markup.** Blocks, marks and inline atoms render through your Svelte snippets; the slash menu, toolbar, block menu and handles take a snippet and keep their behavior.
24
25
  - **Synced properties.** `block.data`, `atom.data` and `edytor.data` read and write like plain objects (`bind:value={block.data.title}`); each property and each array item syncs on its own, so concurrent edits of different properties, or of different items of one array, merge.
25
- - **AI suggestions.** `edytor.suggestions.add(position, content)` proposes text, paragraphs, lists, to-dos or images after, before or inside a block, at the end of its text, or in place of the selection; it streams in, shows only on your screen, and becomes one undo step when accepted ([Suggestions](https://edytor-docs.beynar.workers.dev/docs/editor/suggestions)).
26
+ - **AI suggestions.** `edytor.suggestions.add(position, content)` proposes text, paragraphs, lists, to-dos or images after, before or inside a block, at the end of its text, or in place of the selection; it streams in, shows only on your screen, and becomes one undo step when accepted ([Suggestions](https://edytor.dev/docs/editor/suggestions)).
26
27
  - **Plugins that can veto anything.** Every command is prepared before it writes; plugins see the command and each planned step and can refuse or replace it.
27
28
  - **Real-time collaboration.** One `EdytorDocument` shared by any number of views, or none (headless). Presence cursors, identity-preserving moves, splits and merges, and undo that only takes back your own edits.
28
29
  - **Offline first.** A local IndexedDB copy and cross-tab sync; offline edits survive reloads and reach the server on reconnect.
@@ -37,7 +38,7 @@ The pre-release is on npm under the `next` tag. Name the tag: a bare `edytor` is
37
38
  pnpm add edytor@next
38
39
  ```
39
40
 
40
- `svelte@^5` is a peer dependency. Do not install `yjs`: the v14 engine is vendored (`edytor/crdt`). See [installation](https://edytor-docs.beynar.workers.dev/docs/getting-started).
41
+ `svelte@^5` is a peer dependency. Do not install `yjs`: the v14 engine is vendored (`edytor/crdt`). See [installation](https://edytor.dev/docs/getting-started).
41
42
 
42
43
  ## Quick start
43
44
 
@@ -77,7 +78,7 @@ pnpm add edytor@next
77
78
  </div>
78
79
  ```
79
80
 
80
- `<Edytor>` adds the rich text, image, arrow-move and suggestions plugins after yours, and block handles. To collaborate, name a room and a server running the [`edytor/cloudflare` room](https://edytor-docs.beynar.workers.dev/docs/server/quick-start):
81
+ `<Edytor>` adds the rich text, image, arrow-move and suggestions plugins after yours, and block handles. To collaborate, name a room and a server running the [`edytor/cloudflare` room](https://edytor.dev/docs/server/quick-start):
81
82
 
82
83
  ```svelte
83
84
  <Edytor
@@ -90,14 +91,14 @@ pnpm add edytor@next
90
91
 
91
92
  ## Documentation
92
93
 
93
- The [documentation site](https://edytor-docs.beynar.workers.dev/docs) is the single source for the API and behavior; this README only introduces the package.
94
+ The [documentation site](https://edytor.dev/docs) is the single source for the API and behavior; this README only introduces the package.
94
95
 
95
- - [Getting started](https://edytor-docs.beynar.workers.dev/docs/getting-started): installation, quick start, SvelteKit, entry points and bundle size
96
- - [Concepts](https://edytor-docs.beynar.workers.dev/docs/concepts/document-model): the document model, blocks, void and island roles
97
- - [Editor](https://edytor-docs.beynar.workers.dev/docs/editor/edytor-component): the component, commands, selection, history, clipboard, readonly
98
- - [Plugins](https://edytor-docs.beynar.workers.dev/docs/plugins) and [customization](https://edytor-docs.beynar.workers.dev/docs/customization/blocks): bundled plugins, custom blocks and marks, [hotkeys and editing behavior](https://edytor-docs.beynar.workers.dev/docs/customization/hotkeys)
99
- - [Collaboration](https://edytor-docs.beynar.workers.dev/docs/collaboration) and [server](https://edytor-docs.beynar.workers.dev/docs/server/quick-start): documents, providers, presence, the Durable Object room and its protocol
100
- - [Reference](https://edytor-docs.beynar.workers.dev/docs/reference/document-api): the document API, [troubleshooting](https://edytor-docs.beynar.workers.dev/docs/reference/troubleshooting), [migration from 0.0.11](https://edytor-docs.beynar.workers.dev/docs/reference/migration), limitations
96
+ - [Getting started](https://edytor.dev/docs/getting-started): installation, quick start, SvelteKit, entry points and bundle size
97
+ - [Concepts](https://edytor.dev/docs/concepts/document-model): the document model, blocks, void and island roles
98
+ - [Editor](https://edytor.dev/docs/editor/edytor-component): the component, commands, selection, history, clipboard, readonly
99
+ - [Plugins](https://edytor.dev/docs/plugins) and [customization](https://edytor.dev/docs/customization/blocks): bundled plugins, custom blocks and marks, [hotkeys and editing behavior](https://edytor.dev/docs/customization/hotkeys)
100
+ - [Collaboration](https://edytor.dev/docs/collaboration) and [server](https://edytor.dev/docs/server/quick-start): documents, providers, presence, the Durable Object room and its protocol
101
+ - [Reference](https://edytor.dev/docs/reference/document-api): the document API, [troubleshooting](https://edytor.dev/docs/reference/troubleshooting), [migration from 0.0.11](https://edytor.dev/docs/reference/migration), limitations
101
102
 
102
103
  ## Contributing
103
104
 
@@ -145,10 +145,12 @@ export declare class Block {
145
145
  }, plan?: import("../crdt/index.js").Prepared | undefined) => void;
146
146
  moveBlock: (this: Block, payload: {
147
147
  path: number[];
148
+ beside?: import("./block.utils.js").BlockBeside;
148
149
  }, plan?: import("../crdt/index.js").Prepared | undefined) => Block | null;
149
150
  moveBlocks: (this: Block, payload: {
150
151
  blocks: Block[];
151
152
  path: number[];
153
+ beside?: import("./block.utils.js").BlockBeside;
152
154
  }, plan?: import("../crdt/index.js").Prepared | undefined) => Block[];
153
155
  pushContentIntoBlock: (args_0: {
154
156
  value: (Text | InlineBlock)[];
@@ -38,6 +38,11 @@ import type { DataPatch } from '../crdt/data.js';
38
38
  import type { ResolvedAt } from '../session/suggestions.svelte.js';
39
39
  import { type JSONBlock, type JSONInlineBlock, type JSONText } from '../utils/json.js';
40
40
  import { InlineBlock } from './inlineBlock.svelte.js';
41
+ /** A move beside `target`, to its `side` (`prepare.placeBeside`, `layout.place-beside`). */
42
+ export type BlockBeside = {
43
+ target: Block;
44
+ side: 'left' | 'right';
45
+ };
41
46
  export type BlockOperations = {
42
47
  removeInlineBlock: {
43
48
  index: number;
@@ -88,12 +93,15 @@ export type BlockOperations = {
88
93
  block: JSONInlineBlock;
89
94
  text: Text;
90
95
  };
96
+ /** `beside`: placed beside a target (`left`/`right` moves); `path` is then the target's. */
91
97
  moveBlock: {
92
98
  path: number[];
99
+ beside?: BlockBeside;
93
100
  };
94
101
  moveBlocks: {
95
102
  blocks: Block[];
96
103
  path: number[];
104
+ beside?: BlockBeside;
97
105
  };
98
106
  pushContentIntoBlock: {
99
107
  value: (Text | InlineBlock)[];
@@ -128,6 +136,12 @@ export type BlockOperations = {
128
136
  deleteBlocks: {
129
137
  blocks: Block[];
130
138
  };
139
+ /** Wrap sibling blocks in a new layout of `kind` and `columns` items, one block per item (`layout.wrap`). */
140
+ wrapBlocks: {
141
+ blocks: Block[];
142
+ kind?: string;
143
+ columns?: number;
144
+ };
131
145
  /** A divider at the caret: its steps are the conversion, insertion or split it plans. */
132
146
  insertDivider: {};
133
147
  };
@@ -3,6 +3,7 @@ import { Text } from '../text/text.svelte.js';
3
3
  import { sliceTextValue } from '../block/contentRange.js';
4
4
  import { cloneJson } from '../utils/json.js';
5
5
  import { rangeCovers, selectedMembers } from '../selection/visibility.js';
6
+ import { liftLayouts } from '../selection/replaceSelection.js';
6
7
  const extractContentRange = (block, startText, startOffset, endText, endOffset) => {
7
8
  const content = [];
8
9
  const startIndex = block.content.indexOf(startText);
@@ -67,13 +68,15 @@ const extractBlockRange = (edytor) => {
67
68
  });
68
69
  };
69
70
  export const createEdytorClipboardFragment = (edytor) => {
70
- const selectedBlocks = selectedMembers(edytor);
71
+ // A layout the block selection covers whole is copied as the layout (D3).
72
+ const selectedBlocks = selectedMembers(edytor, liftLayouts(edytor.selection.selectedBlocks));
71
73
  if (selectedBlocks.length > 0) {
72
74
  return {
73
75
  version: 1,
74
76
  source: 'edytor',
75
77
  kind: 'blocks',
76
- // Exactly the members (`sel.blocks.exact`): what a cut copies is what it deletes.
78
+ // Exactly the members (`sel.blocks.exact`), a layout covered whole as itself, which
79
+ // deleting its blocks removes: what a cut copies is what it deletes.
77
80
  blocks: nestMembers(selectedBlocks, (block) => block.value),
78
81
  whole: true
79
82
  };
@@ -135,6 +135,14 @@ export const flowOfHtml = (kinds, html) => {
135
135
  }));
136
136
  return [{ type: claim.type, data: claim.data, content: [], children }];
137
137
  }
138
+ // A kind that shows no text of its own (a list, a layout, a column) takes none: its
139
+ // inline runs and a leading paragraph are lines of its default child, never hidden text.
140
+ if (claim && blocks.get(claim.type)?.rendersContent === false) {
141
+ const children = linesOf(element, kinds.document.defaultChild(claim.type));
142
+ return [
143
+ { type: claim.type, data: claim.data, content: [], ...(children.length && { children }) }
144
+ ];
145
+ }
138
146
  const wraps = (element) => [...element.children].some(isBlock);
139
147
  // An element that only wraps blocks (a `div`, a `ul`) is not a line of its own.
140
148
  if (!claim && wraps(element))
@@ -44,12 +44,18 @@
44
44
  /** Tags that take no content: the kind renders the element only. */
45
45
  const VOID_TAGS = new Set(['area', 'br', 'col', 'embed', 'hr', 'img', 'input', 'wbr']);
46
46
 
47
- /** The element a kind declares (O45): a tag, or tag and attributes, from the block's data. */
47
+ /**
48
+ * The element a kind declares (O45): a tag, or tag and attributes, from the
49
+ * block's data and id (a kind's own view state keyed by block, such as a
50
+ * column's width while a resize drags; reactive state the function reads
51
+ * re-renders the element).
52
+ */
48
53
  const elementOf = (
49
54
  declared: BlockDefinition['element'],
50
- data: Record<string, unknown> | undefined
55
+ data: Record<string, unknown> | undefined,
56
+ id: string
51
57
  ) => {
52
- const spec = typeof declared === 'function' ? declared(data ?? {}) : declared;
58
+ const spec = typeof declared === 'function' ? declared(data ?? {}, id) : declared;
53
59
  if (spec === undefined) return undefined;
54
60
  return typeof spec === 'string' ? { tag: spec, attributes: {} } : { attributes: {}, ...spec };
55
61
  };
@@ -100,9 +106,11 @@
100
106
  );
101
107
  const definition = $derived(cell && edytor.definitionOf(cell.type));
102
108
  // The core renders the block element from the definition; the snippet renders inside it (R11).
103
- const element = $derived(definition && elementOf(definition.element ?? 'div', cell?.data));
109
+ const element = $derived(definition && elementOf(definition.element ?? 'div', cell?.data, id));
104
110
  // The element around the block's own text (a heading's `h2`): the core's, so an override keeps it.
105
- const contentElement = $derived(definition && elementOf(definition.contentElement, cell?.data));
111
+ const contentElement = $derived(
112
+ definition && elementOf(definition.contentElement, cell?.data, id)
113
+ );
106
114
  /** Registers the block element (O45): one element per block, re-registered when the tag changes. */
107
115
  const register = (node: HTMLElement) => block.handle?.attach(node);
108
116
  /** The block element's attributes: the kind's, then the core's. */
@@ -323,18 +323,13 @@
323
323
 
324
324
  const nonNativeEditableBlockChromeSelection = (node: HTMLElement) => {
325
325
  const { selection } = edytor;
326
- const pointerdown = (event: PointerEvent) =>
327
- selection.handleNonNativeEditableBlockChromePointerDown(event);
328
- node.addEventListener('pointerdown', pointerdown, true);
329
326
  // `selectstart` is the only event fired before a drag-selection
330
327
  // begins — the guard keeps one from starting on non-editable
331
- // chrome (markers, void/island chrome, plugin UI).
328
+ // chrome (markers, void/island chrome, plugin UI). A press on that
329
+ // chrome is the press's (`events/onFocus.ts`).
332
330
  node.addEventListener('selectstart', selection.onSelectStart);
333
331
  return {
334
- destroy: () => {
335
- node.removeEventListener('pointerdown', pointerdown, true);
336
- node.removeEventListener('selectstart', selection.onSelectStart);
337
- }
332
+ destroy: () => node.removeEventListener('selectstart', selection.onSelectStart)
338
333
  };
339
334
  };
340
335
  </script>
@@ -24,7 +24,7 @@ export type DocumentActor = {
24
24
  * part of this: they are view-side rendering policy.
25
25
  */
26
26
  export type DocumentSemanticsConfig = {
27
- /** Structural role per block type (`{void?, island?, lines?}` — absent flags mean false). */
27
+ /** Structural role per block type (`{void?, island?, lines?, layout?}` — absent flags mean false). */
28
28
  roles?: Record<string, BlockRole>;
29
29
  /** Whether a kind renders its own content slot (undeclared kinds do). */
30
30
  rendersContent?: Record<string, boolean>;
@@ -343,7 +343,8 @@ export declare class EdytorDocument {
343
343
  */
344
344
  adoptSemantics: (config: DocumentSemanticsConfig) => void;
345
345
  /**
346
- * DEV: `lines` needs an island with a default child (its line kind), and
346
+ * DEV: `layout` needs a default child (its item kind); `lines` needs an
347
+ * island with a default child (its line kind), and
347
348
  * the line kind belongs to it — outside such an island a block of that
348
349
  * kind shows as its parent's default child, so a kind other blocks use
349
350
  * (the default type, another kind's default child) would be recast.
@@ -202,7 +202,8 @@ export const DEFAULT_READINESS_BOUND = 1000;
202
202
  const normalizeRole = (role) => ({
203
203
  void: role?.void === true,
204
204
  island: role?.island === true,
205
- lines: role?.lines === true
205
+ lines: role?.lines === true,
206
+ ...(role?.layout === true && { layout: true })
206
207
  });
207
208
  const anonymousActor = () => ({
208
209
  id: `anon-${crypto.randomUUID()}`
@@ -496,12 +497,14 @@ export class EdytorDocument {
496
497
  if (DEV)
497
498
  this._warnLines();
498
499
  // A newly void kind sheds its children at read time (UW-21b); a block
499
- // promoted out of a newly island kind displays as a default child.
500
- if (Object.values(config.roles ?? {}).some((role) => role?.void || role?.island))
500
+ // promoted out of a newly island kind displays as a default child; a
501
+ // newly layout kind displays only its items (`layout.*`).
502
+ if (Object.values(config.roles ?? {}).some((role) => role?.void || role?.island || role?.layout))
501
503
  this.facade.rolesChanged();
502
504
  };
503
505
  /**
504
- * DEV: `lines` needs an island with a default child (its line kind), and
506
+ * DEV: `layout` needs a default child (its item kind); `lines` needs an
507
+ * island with a default child (its line kind), and
505
508
  * the line kind belongs to it — outside such an island a block of that
506
509
  * kind shows as its parent's default child, so a kind other blocks use
507
510
  * (the default type, another kind's default child) would be recast.
@@ -509,6 +512,8 @@ export class EdytorDocument {
509
512
  _warnLines() {
510
513
  const { roles, defaultChild } = this._capability;
511
514
  for (const [type, role] of roles) {
515
+ if (role.layout && !defaultChild.has(type))
516
+ console.warn(`[edytor] "${type}" declares \`layout\` without a \`defaultChild\` (its column kind): it is no layout.`);
512
517
  if (!role.lines)
513
518
  continue;
514
519
  const line = defaultChild.get(type);
@@ -113,6 +113,13 @@ export type BlockRole = {
113
113
  * `island` and a `defaultChild`; other islands keep their structure.
114
114
  */
115
115
  lines?: boolean;
116
+ /**
117
+ * A layout (columns): it displays only its items — its `defaultChild`
118
+ * kind, a container that renders no content — side by side, and only
119
+ * while it shows two or more (`layout.*` in the delete contract). Needs
120
+ * a `defaultChild`.
121
+ */
122
+ layout?: boolean;
116
123
  };
117
124
  /** Island-sealing policy for a walk in document order (R5; see `next`). */
118
125
  export type OrderPolicy = {
@@ -436,6 +443,8 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
436
443
  keep?: boolean;
437
444
  after?: readonly BlockSpec[];
438
445
  }) => Prepared;
446
+ placeBeside: (ids: readonly BlockId[], target: BlockId, side: "left" | "right", kind?: string) => Prepared;
447
+ wrapInLayout: (ids: readonly BlockId[], kind?: string, columns?: number) => Prepared;
439
448
  splitBlock: (id: BlockId, offset: number, newId: BlockId, tail?: SplitTail) => Prepared;
440
449
  mergeBlocks: (fromId: BlockId, intoId: BlockId) => Prepared;
441
450
  mergeBackward: (id: BlockId) => Prepared;
@@ -561,6 +570,8 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
561
570
  keep?: boolean;
562
571
  after?: readonly BlockSpec[];
563
572
  } | undefined) => OpResult;
573
+ placeBeside: (ids: readonly string[], target: string, side: "left" | "right", kind?: string | undefined) => OpResult;
574
+ wrapInLayout: (ids: readonly string[], kind?: string | undefined, columns?: number | undefined) => OpResult;
564
575
  splitBlock: (id: string, offset: number, newId: string, tail?: SplitTail | undefined) => OpResult;
565
576
  mergeBlocks: (fromId: string, intoId: string) => OpResult;
566
577
  mergeBackward: (id: string) => OpResult;
@@ -654,6 +665,12 @@ export declare const bindEdytorDoc: (Y: EngineApi) => {
654
665
  isVoid: (id: string) => boolean;
655
666
  isIsland: (id: string) => boolean;
656
667
  isLines: (id: string) => boolean;
668
+ /** `id` is a layout: its kind's role says `layout` (`layout.*`). */
669
+ isLayout: (id: string) => boolean;
670
+ /** `id` is a layout item: of its layout's item kind, directly in it (a column). */
671
+ isLayoutItem: (id: string) => boolean;
672
+ /** The block a beside placement at `id` stands beside (`placeBeside`'s resolution). */
673
+ besideAt: (id: BlockId, kind?: string) => string;
657
674
  islandOf: (id: string) => string | null;
658
675
  insideIsland: (id: string) => boolean;
659
676
  canPlace: (ids: readonly BlockId[], parent?: BlockId | null) => boolean;