edytor 1.0.0-rc.2 → 1.0.0-rc.3

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 (173) hide show
  1. package/README.md +1 -1
  2. package/dist/block/block.svelte.js +20 -4
  3. package/dist/clipboard/htmlFlow.d.ts +6 -1
  4. package/dist/clipboard/htmlFlow.js +19 -6
  5. package/dist/cloudflare/DocumentRoom.d.ts +18 -0
  6. package/dist/cloudflare/DocumentRoom.js +7 -0
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/index.js +1 -1
  9. package/dist/cloudflare/room/context.d.ts +1 -0
  10. package/dist/cloudflare/room/storage.d.ts +8 -0
  11. package/dist/cloudflare/room/storage.js +24 -1
  12. package/dist/collaboration/history/HistoryPanel.svelte +49 -20
  13. package/dist/collaboration/history/index.d.ts +1 -1
  14. package/dist/collaboration/history/panel.d.ts +18 -0
  15. package/dist/components/Block.svelte +155 -98
  16. package/dist/components/Block.svelte.d.ts +4 -1
  17. package/dist/components/Content.svelte +90 -39
  18. package/dist/components/CustomMark.svelte +46 -0
  19. package/dist/components/CustomMark.svelte.d.ts +11 -0
  20. package/dist/components/Edytor.svelte +29 -1
  21. package/dist/components/InlineBlock.svelte +4 -9
  22. package/dist/components/Mark.svelte +46 -35
  23. package/dist/components/Mark.svelte.d.ts +7 -0
  24. package/dist/components/Text.svelte +50 -13
  25. package/dist/{plugins/blockHandles → dnd}/autoScroll.d.ts +2 -1
  26. package/dist/{plugins/blockHandles → dnd}/autoScroll.js +3 -2
  27. package/dist/edytor.svelte.js +3 -1
  28. package/dist/events/beforeInputCommands.js +4 -3
  29. package/dist/events/onDrop.d.ts +6 -0
  30. package/dist/events/onDrop.js +6 -3
  31. package/dist/events/onFocus.js +48 -6
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +1 -1
  34. package/dist/labels.d.ts +18 -2
  35. package/dist/labels.js +13 -1
  36. package/dist/plugins/blockHandles/BlockHandle.svelte +5 -2
  37. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +41 -20
  38. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +83 -47
  39. package/dist/plugins/blockHandles/BlockHandles.svelte +19 -12
  40. package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +23 -3
  41. package/dist/plugins/blockHandles/blockHandlesPlugin.js +33 -9
  42. package/dist/plugins/blockHandles/dragPreview.js +2 -1
  43. package/dist/plugins/blockHandles/geometry.d.ts +44 -0
  44. package/dist/plugins/blockHandles/geometry.js +47 -0
  45. package/dist/plugins/blockMenu/BlockMenu.svelte +16 -128
  46. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +43 -2
  47. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +142 -0
  48. package/dist/plugins/chrome.d.ts +72 -0
  49. package/dist/plugins/chrome.js +64 -0
  50. package/dist/plugins/code/CodeHeader.svelte +40 -54
  51. package/dist/plugins/code/CodeHeader.svelte.d.ts +1 -0
  52. package/dist/plugins/code/CodePlugin.svelte +42 -20
  53. package/dist/plugins/code/LanguageMenu.svelte +134 -0
  54. package/dist/plugins/code/LanguageMenu.svelte.d.ts +8 -0
  55. package/dist/plugins/code/code.css +37 -20
  56. package/dist/plugins/code/header.svelte.d.ts +56 -0
  57. package/dist/plugins/code/header.svelte.js +168 -0
  58. package/dist/plugins/code/languageMenu.svelte.d.ts +104 -0
  59. package/dist/plugins/code/languageMenu.svelte.js +329 -0
  60. package/dist/plugins/code/languages.d.ts +21 -0
  61. package/dist/plugins/comments/CommentsSidebar.svelte +3 -2
  62. package/dist/plugins/comments/CommentsSidebar.svelte.d.ts +3 -6
  63. package/dist/plugins/comments/commentsPlugin.d.ts +10 -5
  64. package/dist/plugins/equation/EquationEditor.svelte +31 -28
  65. package/dist/plugins/equation/EquationEditor.svelte.d.ts +2 -0
  66. package/dist/plugins/equation/EquationPlugin.svelte +15 -4
  67. package/dist/plugins/equation/EquationPlugin.svelte.d.ts +5 -3
  68. package/dist/plugins/equation/equation.svelte.d.ts +43 -18
  69. package/dist/plugins/equation/equation.svelte.js +90 -8
  70. package/dist/plugins/equation/importUrl.d.ts +6 -0
  71. package/dist/plugins/equation/importUrl.js +6 -0
  72. package/dist/plugins/equation/katex.d.ts +8 -0
  73. package/dist/plugins/equation/katex.js +80 -0
  74. package/dist/plugins/image/ImageChrome.svelte +27 -12
  75. package/dist/plugins/image/ImageChrome.svelte.d.ts +2 -0
  76. package/dist/plugins/image/ImageEmpty.svelte +25 -48
  77. package/dist/plugins/image/ImageEmpty.svelte.d.ts +3 -0
  78. package/dist/plugins/image/ImagePlugin.svelte +27 -6
  79. package/dist/plugins/image/ImagePlugin.svelte.d.ts +18 -0
  80. package/dist/plugins/image/controls.svelte.d.ts +14 -35
  81. package/dist/plugins/image/controls.svelte.js +56 -10
  82. package/dist/plugins/image/empty.svelte.d.ts +56 -0
  83. package/dist/plugins/image/empty.svelte.js +83 -0
  84. package/dist/plugins/index.d.ts +14 -4
  85. package/dist/plugins/index.js +3 -2
  86. package/dist/plugins/marquee/MarqueeBox.svelte +32 -0
  87. package/dist/plugins/marquee/MarqueeBox.svelte.d.ts +9 -0
  88. package/dist/plugins/marquee/MarqueeController.svelte.d.ts +48 -0
  89. package/dist/plugins/marquee/MarqueeController.svelte.js +246 -0
  90. package/dist/plugins/marquee/marquee.d.ts +33 -0
  91. package/dist/plugins/marquee/marquee.js +106 -0
  92. package/dist/plugins/marquee/marqueePlugin.d.ts +31 -0
  93. package/dist/plugins/marquee/marqueePlugin.js +67 -0
  94. package/dist/plugins/media/AudioPlugin.svelte +2 -0
  95. package/dist/plugins/media/BookmarkPlugin.svelte +22 -3
  96. package/dist/plugins/media/BookmarkPlugin.svelte.d.ts +18 -0
  97. package/dist/plugins/media/EmbedPlugin.svelte +22 -2
  98. package/dist/plugins/media/EmbedPlugin.svelte.d.ts +18 -0
  99. package/dist/plugins/media/FilePlugin.svelte +2 -0
  100. package/dist/plugins/media/MediaEmpty.svelte +35 -53
  101. package/dist/plugins/media/UrlPasteMenu.svelte +16 -7
  102. package/dist/plugins/media/UrlPasteMenu.svelte.d.ts +2 -0
  103. package/dist/plugins/media/VideoPlugin.svelte +2 -0
  104. package/dist/plugins/media/empty.svelte.d.ts +77 -0
  105. package/dist/plugins/media/empty.svelte.js +115 -0
  106. package/dist/plugins/media/media.d.ts +17 -0
  107. package/dist/plugins/media/media.js +15 -0
  108. package/dist/plugins/media/urlPaste.svelte.d.ts +22 -14
  109. package/dist/plugins/media/urlPaste.svelte.js +48 -7
  110. package/dist/plugins/mention/MentionPlugin.svelte +11 -7
  111. package/dist/plugins/mention/MentionPlugin.svelte.d.ts +7 -0
  112. package/dist/plugins/pageLink/PageLinkPlugin.svelte +11 -7
  113. package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +7 -0
  114. package/dist/plugins/richtext/CalloutIcon.svelte +38 -0
  115. package/dist/plugins/richtext/CalloutIcon.svelte.d.ts +9 -0
  116. package/dist/plugins/richtext/CalloutIconMenu.svelte +98 -0
  117. package/dist/plugins/richtext/CalloutIconMenu.svelte.d.ts +9 -0
  118. package/dist/plugins/richtext/EmptyBody.svelte +32 -0
  119. package/dist/plugins/richtext/EmptyBody.svelte.d.ts +8 -0
  120. package/dist/plugins/richtext/RichTextPlugin.svelte +201 -61
  121. package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +21 -1
  122. package/dist/plugins/richtext/TodoCheckbox.svelte +2 -2
  123. package/dist/plugins/richtext/body.d.ts +9 -0
  124. package/dist/plugins/richtext/body.js +16 -0
  125. package/dist/plugins/richtext/calloutIcons.svelte.d.ts +102 -0
  126. package/dist/plugins/richtext/calloutIcons.svelte.js +302 -0
  127. package/dist/plugins/richtext/labels.d.ts +6 -2
  128. package/dist/plugins/richtext/labels.js +6 -2
  129. package/dist/plugins/slashMenu/SlashMenu.svelte +25 -109
  130. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +19 -0
  131. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +95 -0
  132. package/dist/plugins/slashMenu/slashMenuPlugin.d.ts +15 -11
  133. package/dist/plugins/suggestions/SuggestionBars.svelte +6 -1
  134. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +3 -1
  135. package/dist/plugins/table/TableChrome.svelte +174 -121
  136. package/dist/plugins/table/TableChrome.svelte.d.ts +5 -1
  137. package/dist/plugins/table/TablePlugin.svelte +16 -9
  138. package/dist/plugins/table/chrome.svelte.d.ts +190 -94
  139. package/dist/plugins/table/chrome.svelte.js +658 -54
  140. package/dist/plugins/table/dragPreview.d.ts +29 -0
  141. package/dist/plugins/table/dragPreview.js +103 -0
  142. package/dist/plugins/table/table.css +59 -2
  143. package/dist/plugins/table/table.d.ts +18 -0
  144. package/dist/plugins/table/table.js +1 -33
  145. package/dist/plugins/toolbar/LinkCard.svelte +6 -2
  146. package/dist/plugins/toolbar/LinkCard.svelte.d.ts +2 -0
  147. package/dist/plugins/toolbar/Toolbar.svelte +10 -61
  148. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +27 -0
  149. package/dist/plugins/toolbar/ToolbarController.svelte.js +101 -0
  150. package/dist/plugins/toolbar/toolbarPlugin.d.ts +14 -3
  151. package/dist/plugins/toolbar/toolbarPlugin.js +1 -1
  152. package/dist/plugins/triggers/TriggerController.svelte.d.ts +25 -0
  153. package/dist/plugins/triggers/TriggerController.svelte.js +36 -0
  154. package/dist/plugins/triggers/TriggerMenu.svelte +27 -36
  155. package/dist/plugins.d.ts +25 -13
  156. package/dist/selection/pointer.svelte.d.ts +4 -0
  157. package/dist/selection/pointer.svelte.js +124 -0
  158. package/dist/selection/selection.svelte.js +39 -0
  159. package/dist/selection/visibility.d.ts +8 -0
  160. package/dist/selection/visibility.js +16 -3
  161. package/dist/session/attempt.js +4 -2
  162. package/dist/session/navigation.d.ts +15 -1
  163. package/dist/session/navigation.js +83 -1
  164. package/dist/session/ports.d.ts +12 -0
  165. package/dist/session/selection.js +3 -0
  166. package/dist/session/suggestions.svelte.js +22 -0
  167. package/dist/surface/lines.d.ts +32 -0
  168. package/dist/surface/lines.js +43 -0
  169. package/dist/surface/observer.svelte.js +20 -4
  170. package/dist/surface/overlay.d.ts +8 -4
  171. package/dist/surface/overlay.js +64 -9
  172. package/dist/themes/notion.css +53 -21
  173. package/package.json +1 -1
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  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.
19
19
 
20
- > **Release candidate.** Edytor `1.0.0-rc.2` is the 1.0 release candidate (`edytor` on npm): its API is frozen, and a change to it before 1.0 is a listed breaking change. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
20
+ > **Release candidate.** Edytor `1.0.0-rc.3` is the 1.0 release candidate (`edytor` on npm): its API is frozen, and a change to it before 1.0 is a listed breaking change. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
21
21
 
22
22
  ## Features
23
23
 
@@ -117,10 +117,10 @@ export class Block {
117
117
  this.patchData({ ops: [{ path: [], value: data }] });
118
118
  };
119
119
  get selected() {
120
- return this.edytor.selection.selectedBlocks.has(this);
120
+ return this.edytor.selection.isSelected(this);
121
121
  }
122
122
  get focused() {
123
- return this.edytor.selection.focusedBlocks.has(this) && !this.selected;
123
+ return this.edytor.selection.isFocused(this) && !this.selected;
124
124
  }
125
125
  /** May this block move at all: `canPlace` without a destination (drag handles). */
126
126
  get movable() {
@@ -195,10 +195,24 @@ export class Block {
195
195
  return this.edytor.facade.isVisibleBlock(this.id);
196
196
  }
197
197
  get nextBlock() {
198
- return this.parent ? this.parent.children[this.index + 1] : null;
198
+ return this.#sibling(1);
199
199
  }
200
200
  get previousBlock() {
201
- return this.parent ? this.parent.children[this.index - 1] : null;
201
+ return this.#sibling(-1);
202
+ }
203
+ /**
204
+ * The sibling `step` places away (none past either end): one id read, no
205
+ * handle for every sibling (a top-level block's are the whole page's).
206
+ * Tracks what `parent` and `index` track: this block's cell and its
207
+ * parent's child list.
208
+ */
209
+ #sibling(step) {
210
+ const parent = this.parent;
211
+ if (!parent)
212
+ return null;
213
+ const ids = this.edytor.facade.childrenIds(parent.isRoot ? null : parent.id);
214
+ const id = ids[this.index + step];
215
+ return id === undefined ? null : this.edytor.idToBlock.block(id);
202
216
  }
203
217
  get closestPreviousBlock() {
204
218
  return this.edytor.blockBefore(this);
@@ -422,6 +436,8 @@ export class Block {
422
436
  node.setAttribute(name, value);
423
437
  this.node = node;
424
438
  const release = this.edytor.surface.register(node, 'block', this.id);
439
+ // Its attributes follow its own selected and focused membership.
440
+ this.edytor.selection.keep(this);
425
441
  // A block laid out again with no commit moves the chrome beside it (the overlay measures).
426
442
  const unobserve = this.edytor.overlay.observe(node);
427
443
  const onDestroy = this.edytor.plugins.flatMap((plugin) => {
@@ -7,7 +7,12 @@ export type ImportKinds = Pick<Edytor, 'blocks' | 'marks'> & Partial<Pick<Edytor
7
7
  document: Pick<Edytor['document'], 'defaultChild'>;
8
8
  };
9
9
  type Values = Record<string, SerializableContent>;
10
- /** The tag a kind writes for `data`: its export form's first tag, else its element's, else its content element's. */
10
+ /**
11
+ * The tag a kind writes for `data`: its export form's first tag, else its
12
+ * element's, else its content element's. A generic one (`div`, `span`) is
13
+ * none: an export form written as one (a callout's `<div data-edytor-callout>`)
14
+ * is found by its `parse` hook only.
15
+ */
11
16
  export declare const tagOf: (type: string, kind: BlockDefinition, data: Values) => string | undefined;
12
17
  /**
13
18
  * External HTML as a flow: the browser parses (inert: no script
@@ -33,14 +33,21 @@ const isGlyph = (node) => {
33
33
  };
34
34
  /** Tags that carry no meaning of their own: a kind rendering one is not found by it. */
35
35
  const GENERIC = /^(div|span)$/;
36
- /** The tag a kind writes for `data`: its export form's first tag, else its element's, else its content element's. */
36
+ /**
37
+ * The tag a kind writes for `data`: its export form's first tag, else its
38
+ * element's, else its content element's. A generic one (`div`, `span`) is
39
+ * none: an export form written as one (a callout's `<div data-edytor-callout>`)
40
+ * is found by its `parse` hook only.
41
+ */
37
42
  export const tagOf = (type, kind, data) => {
38
43
  const { html, element, contentElement } = kind;
39
44
  if (typeof html === 'string')
40
45
  return html;
41
46
  try {
42
- if (html)
43
- return /^<([a-z][\w-]*)/i.exec(html({ type, data }, '', ''))?.[1]?.toLowerCase();
47
+ if (html) {
48
+ const tag = /^<([a-z][\w-]*)/i.exec(html({ type, data }, '', ''))?.[1]?.toLowerCase();
49
+ return tag && !GENERIC.test(tag) ? tag : undefined;
50
+ }
44
51
  }
45
52
  catch {
46
53
  return undefined;
@@ -251,18 +258,24 @@ export const flowOfHtml = (kinds, html) => {
251
258
  texts.add(current);
252
259
  }
253
260
  };
261
+ /** Whether a leading paragraph gave the element its text: one only, empty or not. */
262
+ let led = false;
254
263
  for (const node of element.childNodes)
255
264
  if (isVoid(node))
256
265
  at.apart(node);
257
266
  else if (!isBlock(node))
258
267
  inline(node, at, {}, pre);
259
- // A leading plain paragraph is the element's own text (`<li><p>Item</p></li>`).
260
- else if (!current.content.length &&
268
+ // A leading plain paragraph is the element's own text (`<li><p>Item</p></li>`,
269
+ // a callout's title): an empty one too, so the paragraph after it is a child.
270
+ else if (!led &&
271
+ !current.content.length &&
261
272
  !current.children?.length &&
262
273
  !kindOf(node) &&
263
- !wraps(node))
274
+ !wraps(node)) {
275
+ led = true;
264
276
  for (const child of node.childNodes)
265
277
  inline(child, at, {}, pre);
278
+ }
266
279
  else
267
280
  (current.children ??= []).push(...blockOf(node, childType));
268
281
  for (const part of texts)
@@ -128,6 +128,8 @@ export declare const DEFAULT_SAVE_AFTER = 2000;
128
128
  * about 100 MB at worst. Raise it for documents of long text.
129
129
  */
130
130
  export declare const DEFAULT_MAX_DOCUMENT_BYTES: number;
131
+ /** The share of the document quota past which the room logs its size, by default. */
132
+ export declare const DEFAULT_DOCUMENT_WARNING = 0.8;
131
133
  /**
132
134
  * The largest frame a socket may send, reassembled from chunks: room for a
133
135
  * whole document at the default quota (a client seeding an empty room).
@@ -211,6 +213,8 @@ export type DocumentRoomEnv = {
211
213
  EDYTOR_COMPACT_AFTER?: string | number;
212
214
  EDYTOR_SAVE_AFTER?: string | number;
213
215
  EDYTOR_MAX_DOCUMENT_BYTES?: string | number;
216
+ /** The share of the document quota past which the room logs its size (`documentWarning`). */
217
+ EDYTOR_DOCUMENT_WARNING?: string | number;
214
218
  EDYTOR_MAX_INBOUND_FRAME_BYTES?: string | number;
215
219
  EDYTOR_MAX_BUFFERED_BYTES?: string | number;
216
220
  EDYTOR_MAX_UPDATES_PER_SECOND?: string | number;
@@ -290,6 +294,8 @@ export type Timing = {
290
294
  export type RoomMetrics = {
291
295
  /** The document's size as the document quota measures it: its records, uncompressed. */
292
296
  documentBytes: number;
297
+ /** The document quota (`maxDocumentBytes`): writes past it are refused (`4413`). */
298
+ documentLimit: number;
293
299
  /** What its rows take in storage (a snapshot compressed). */
294
300
  storedBytes: number;
295
301
  /** Logical records stored (the generation record included), and their rows. */
@@ -366,6 +372,11 @@ export type RoomLogEntry = {
366
372
  edytor: 'quota';
367
373
  user: string;
368
374
  quota: string;
375
+ } | {
376
+ edytor: 'size';
377
+ bytes: number;
378
+ limit: number;
379
+ share: number;
369
380
  } | {
370
381
  edytor: 'denied';
371
382
  user: string;
@@ -495,6 +506,12 @@ export type AttachRoomOptions = {
495
506
  tablePrefix?: string;
496
507
  /** Largest document, in stored bytes (default {@link DEFAULT_MAX_DOCUMENT_BYTES}). */
497
508
  maxDocumentBytes?: number;
509
+ /**
510
+ * The share of `maxDocumentBytes` past which the room logs the
511
+ * document's size (`size`, once per crossing), above 0 and at most 1
512
+ * (default {@link DEFAULT_DOCUMENT_WARNING}, 0.8).
513
+ */
514
+ documentWarning?: number;
498
515
  /** Largest frame a socket may send, reassembled (default {@link DEFAULT_MAX_INBOUND_FRAME_BYTES}). */
499
516
  maxInboundFrameBytes?: number;
500
517
  /**
@@ -588,6 +605,7 @@ export declare class AttachedDocument {
588
605
  readonly compactAfter: number;
589
606
  readonly saveAfter: number;
590
607
  readonly maxDocumentBytes: number;
608
+ readonly documentWarning: number;
591
609
  readonly maxInboundFrameBytes: number;
592
610
  readonly maxBufferedBytes: number;
593
611
  readonly maxUpdatesPerSecond: number;
@@ -137,6 +137,8 @@ export const DEFAULT_SAVE_AFTER = 2000;
137
137
  * about 100 MB at worst. Raise it for documents of long text.
138
138
  */
139
139
  export const DEFAULT_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
140
+ /** The share of the document quota past which the room logs its size, by default. */
141
+ export const DEFAULT_DOCUMENT_WARNING = 0.8;
140
142
  /**
141
143
  * The largest frame a socket may send, reassembled from chunks: room for a
142
144
  * whole document at the default quota (a client seeding an empty room).
@@ -200,6 +202,7 @@ export class AttachedDocument {
200
202
  compactAfter;
201
203
  saveAfter;
202
204
  maxDocumentBytes;
205
+ documentWarning;
203
206
  maxInboundFrameBytes;
204
207
  maxBufferedBytes;
205
208
  maxUpdatesPerSecond;
@@ -225,6 +228,8 @@ export class AttachedDocument {
225
228
  this.compactAfter = knob(options.compactAfter, DEFAULT_COMPACT_AFTER, 1e9);
226
229
  this.saveAfter = knob(options.saveAfter, DEFAULT_SAVE_AFTER, 1e9);
227
230
  this.maxDocumentBytes = knob(options.maxDocumentBytes, DEFAULT_MAX_DOCUMENT_BYTES, QUOTA_CEILING);
231
+ const warning = Number(options.documentWarning);
232
+ this.documentWarning = warning > 0 && warning <= 1 ? warning : DEFAULT_DOCUMENT_WARNING;
228
233
  this.maxInboundFrameBytes = knob(options.maxInboundFrameBytes, DEFAULT_MAX_INBOUND_FRAME_BYTES, QUOTA_CEILING);
229
234
  this.maxBufferedBytes = Math.max(this.maxInboundFrameBytes, knob(options.maxBufferedBytes, 2 * this.maxInboundFrameBytes, QUOTA_CEILING));
230
235
  this.maxUpdatesPerSecond = knob(options.maxUpdatesPerSecond, DEFAULT_MAX_UPDATES_PER_SECOND, QUOTA_CEILING);
@@ -492,6 +497,7 @@ export class AttachedDocument {
492
497
  const { counters } = room;
493
498
  return {
494
499
  documentBytes: room.storage.documentBytes,
500
+ documentLimit: room.limits.maxDocumentBytes,
495
501
  storedBytes: stored?.bytes ?? 0,
496
502
  records: stored?.records ?? 0,
497
503
  rows: stored?.rows ?? 0,
@@ -649,6 +655,7 @@ export class DocumentRoom extends DurableObject {
649
655
  compactAfter: Number(knobs.EDYTOR_COMPACT_AFTER),
650
656
  saveAfter: Number(knobs.EDYTOR_SAVE_AFTER),
651
657
  maxDocumentBytes: Number(knobs.EDYTOR_MAX_DOCUMENT_BYTES),
658
+ documentWarning: Number(knobs.EDYTOR_DOCUMENT_WARNING),
652
659
  maxInboundFrameBytes: Number(knobs.EDYTOR_MAX_INBOUND_FRAME_BYTES),
653
660
  maxBufferedBytes: Number(knobs.EDYTOR_MAX_BUFFERED_BYTES),
654
661
  maxUpdatesPerSecond: Number(knobs.EDYTOR_MAX_UPDATES_PER_SECOND),
@@ -4,7 +4,7 @@
4
4
  * `cloudflare:workers`); built on the Worker-safe CRDT entry. Documented in
5
5
  * the site's `server/` section (room, authorization, extending, protocol).
6
6
  */
7
- export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, type AttachRoomOptions, type CommentOptions, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers, type Attachment, type DocumentRoomEnv, type LoadedDocument, type Refusal, type ReplicaOwner, type SavedDocument, type SocketIdentity, type StoredRecord, type FrameValidation, type ValidatedBlock, type RoomMetrics, type RoomLogEntry, type RestoreResult, type Timing } from './DocumentRoom.js';
7
+ export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, type AttachRoomOptions, type CommentOptions, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers, type Attachment, type DocumentRoomEnv, type LoadedDocument, type Refusal, type ReplicaOwner, type SavedDocument, type SocketIdentity, type StoredRecord, type FrameValidation, type ValidatedBlock, type RoomMetrics, type RoomLogEntry, type RestoreResult, type Timing } from './DocumentRoom.js';
8
8
  export { DEFAULT_RETENTION_DAYS, HISTORY_MAX_VALUE_BYTES, R2_HISTORY_MAX_VALUE_BYTES, ROOM_HISTORY_MAX_BYTES, ROOM_HISTORY_MAX_VALUE_BYTES, kvHistory, r2History, roomHistory, type HistoryEntry, type HistoryMetadata, type HistoryOptions, type HistoryRoomStorage, type HistoryStore, type HistoryStoreFactory, type HistoryStoreRecord, type KVLike, type R2BucketLike } from './history.js';
9
9
  export { type PurgeReport } from '../crdt/purge.js';
10
10
  export { COMMENT_ROW_BYTES, COMMENT_STATUS, MAX_COMMENT_LENGTH, MAX_COMMENTS_PER_THREAD, MAX_THREADS, commentAnchors, commentBytes, decideComment, type ThreadComment, type CommentActor, type CommentChange, type CommentOutcome, type CommentRefusal, type CommentRequest, type CommentRun, type CommentSnapshot, type CommentThread } from '../crdt/protocols/comments.js';
@@ -4,7 +4,7 @@
4
4
  * `cloudflare:workers`); built on the Worker-safe CRDT entry. Documented in
5
5
  * the site's `server/` section (room, authorization, extending, protocol).
6
6
  */
7
- export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers } from './DocumentRoom.js';
7
+ export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers } from './DocumentRoom.js';
8
8
  export { DEFAULT_RETENTION_DAYS, HISTORY_MAX_VALUE_BYTES, R2_HISTORY_MAX_VALUE_BYTES, ROOM_HISTORY_MAX_BYTES, ROOM_HISTORY_MAX_VALUE_BYTES, kvHistory, r2History, roomHistory } from './history.js';
9
9
  export {} from '../crdt/purge.js';
10
10
  export { COMMENT_ROW_BYTES, COMMENT_STATUS, MAX_COMMENT_LENGTH, MAX_COMMENTS_PER_THREAD, MAX_THREADS, commentAnchors, commentBytes, decideComment } from '../crdt/protocols/comments.js';
@@ -685,6 +685,7 @@ export type RoomLimits = Readonly<{
685
685
  compactAfter: number;
686
686
  saveAfter: number;
687
687
  maxDocumentBytes: number;
688
+ documentWarning: number;
688
689
  maxInboundFrameBytes: number;
689
690
  maxBufferedBytes: number;
690
691
  maxUpdatesPerSecond: number;
@@ -10,6 +10,14 @@ export declare class RoomStorage {
10
10
  updates: number;
11
11
  /** What the records hold, uncompressed (the generation record aside): the document quota's measure. */
12
12
  documentBytes: number;
13
+ /** The size passed `documentWarning` of the quota and was logged (memory: a wake logs it again). */
14
+ private warned;
15
+ /**
16
+ * Log the document's size when it passes `documentWarning` of its quota
17
+ * (`room.quota.warning`): once per crossing; under it again, the next
18
+ * crossing logs again.
19
+ */
20
+ private watch;
13
21
  /** The waiting deletes stored as `pending` records (the engine may hold more, in memory). */
14
22
  storedWaiting: Decoded['ds'];
15
23
  /**
@@ -93,6 +93,25 @@ export class RoomStorage {
93
93
  updates = 0;
94
94
  /** What the records hold, uncompressed (the generation record aside): the document quota's measure. */
95
95
  documentBytes = 0;
96
+ /** The size passed `documentWarning` of the quota and was logged (memory: a wake logs it again). */
97
+ warned = false;
98
+ /**
99
+ * Log the document's size when it passes `documentWarning` of its quota
100
+ * (`room.quota.warning`): once per crossing; under it again, the next
101
+ * crossing logs again.
102
+ */
103
+ watch() {
104
+ const room = this.room;
105
+ const { maxDocumentBytes: limit, documentWarning: share } = room.limits;
106
+ if (this.documentBytes < limit * share) {
107
+ this.warned = false;
108
+ return;
109
+ }
110
+ if (this.warned)
111
+ return;
112
+ this.warned = true;
113
+ room.log({ edytor: 'size', bytes: this.documentBytes, limit, share });
114
+ }
96
115
  /** The waiting deletes stored as `pending` records (the engine may hold more, in memory). */
97
116
  storedWaiting = Y.createIdSet();
98
117
  /**
@@ -338,6 +357,7 @@ export class RoomStorage {
338
357
  // records keeps every keystroke's struct and the deleted content.
339
358
  const doc = admit(rest.map((record) => (record.v2 ? { v2: record.bytes } : record.bytes)), `room ${room.ctx.id}`, () => room.history.restoreKeep);
340
359
  this.documentBytes = rest.reduce((n, record) => n + record.bytes.length, 0);
360
+ this.watch();
341
361
  this.adopt(doc);
342
362
  // Every delete the engine holds waiting came from the rows.
343
363
  this.storedWaiting = pendingDeletes(doc);
@@ -388,6 +408,7 @@ export class RoomStorage {
388
408
  });
389
409
  room.history.clearRestore();
390
410
  this.documentBytes = snapshot.length;
411
+ this.watch();
391
412
  this.updates = 0;
392
413
  this.storedWaiting = Y.createIdSet();
393
414
  this.adopt(doc);
@@ -450,8 +471,10 @@ export class RoomStorage {
450
471
  }
451
472
  const record = this.nextRecord++;
452
473
  this.writeRecord(kind, record, bytes);
453
- if (kind !== 'generation')
474
+ if (kind !== 'generation') {
454
475
  this.documentBytes += bytes.length;
476
+ this.watch();
477
+ }
455
478
  return record;
456
479
  }
457
480
  /** Record `record`'s rows (at the row positions `seqs`, when given). */
@@ -15,6 +15,7 @@
15
15
  import { versionDiff } from './diff.js';
16
16
  import { versionHighlightsPlugin } from './versionHighlights.js';
17
17
  import { labelsWith } from '../../labels.js';
18
+ import { optionAttributes } from '../../plugins/chrome.js';
18
19
 
19
20
  let {
20
21
  client,
@@ -28,6 +29,7 @@
28
29
  class: className,
29
30
  previewClass,
30
31
  version: versionRow,
32
+ item: itemRow,
31
33
  onRestore,
32
34
  onUndo,
33
35
  onError
@@ -186,6 +188,20 @@
186
188
 
187
189
  const selectedEntry = $derived(versions?.find((entry) => entry.key === selected) ?? null);
188
190
 
191
+ /** The panel's own prefix: its rows' ids are page-unique. */
192
+ const uid = $props.id();
193
+ /** The row of `entry` at `index`: an option, the list's one tab stop when it is selected (else the first). */
194
+ const optionOf = (entry: HistoryVersion, index: number) => {
195
+ const isSelected = entry.key === selected;
196
+ return optionAttributes(
197
+ `${uid}-version-${index}`,
198
+ 'option',
199
+ isSelected,
200
+ { tabindex: isSelected || (selected === null && index === 0) ? 0 : -1 },
201
+ false
202
+ );
203
+ };
204
+
189
205
  /** The listbox's keys: the arrows, Home and End move the selection and the focus. */
190
206
  const onListKey = (event: KeyboardEvent) => {
191
207
  if (!versions?.length) return;
@@ -272,28 +288,41 @@
272
288
  aria-label={labels.versions}
273
289
  onkeydown={onListKey}
274
290
  >
275
- {#each versions as entry (entry.key)}
291
+ {#each versions as entry, index (entry.key)}
276
292
  {@const isSelected = entry.key === selected}
293
+ {@const option = optionOf(entry, index)}
294
+ {@const preview = () => void select(entry.key)}
277
295
  <li role="presentation">
278
- <button
279
- type="button"
280
- role="option"
281
- aria-selected={isSelected}
282
- tabindex={isSelected || (selected === null && entry === versions[0]) ? 0 : -1}
283
- data-edytor-history-version={entry.key}
284
- onclick={() => void select(entry.key)}
285
- >
286
- {#if versionRow}
287
- {@render versionRow(entry, { selected: isSelected })}
288
- {:else}
289
- <span data-edytor-history-version-title>{titleOf(entry)}</span>
290
- <span data-edytor-history-version-editors>{editorsOf(entry)}</span>
291
- <span data-edytor-history-version-time>
292
- {labels.saved}
293
- <time datetime={new Date(entry.at).toISOString()}>{timeOf(entry)}</time>
294
- </span>
295
- {/if}
296
- </button>
296
+ {#if itemRow}
297
+ {@render itemRow({
298
+ item: entry,
299
+ version: entry,
300
+ id: option.id,
301
+ label: titleOf(entry),
302
+ selected: isSelected,
303
+ run: preview,
304
+ select: preview,
305
+ option
306
+ })}
307
+ {:else}
308
+ <button
309
+ type="button"
310
+ {...option}
311
+ data-edytor-history-version={entry.key}
312
+ onclick={preview}
313
+ >
314
+ {#if versionRow}
315
+ {@render versionRow(entry, { selected: isSelected })}
316
+ {:else}
317
+ <span data-edytor-history-version-title>{titleOf(entry)}</span>
318
+ <span data-edytor-history-version-editors>{editorsOf(entry)}</span>
319
+ <span data-edytor-history-version-time>
320
+ {labels.saved}
321
+ <time datetime={new Date(entry.at).toISOString()}>{timeOf(entry)}</time>
322
+ </span>
323
+ {/if}
324
+ </button>
325
+ {/if}
297
326
  </li>
298
327
  {/each}
299
328
  </ul>
@@ -6,4 +6,4 @@
6
6
  export { default as HistoryPanel } from './HistoryPanel.svelte';
7
7
  export { createHistoryClient, HistoryRequestError, type HistoryClient, type HistoryClientOptions, type HistoryRestoreResult, type HistoryUndoResult, type HistoryVersion } from './client.js';
8
8
  export { versionDiff, type VersionChange, type VersionDiff } from './diff.js';
9
- export { defaultHistoryLabels, type HistoryPanelLabels, type HistoryPanelProps } from './panel.js';
9
+ export { defaultHistoryLabels, type HistoryPanelLabels, type HistoryPanelProps, type HistoryVersionItem } from './panel.js';
@@ -6,9 +6,21 @@ import type { EdytorDocument } from '../../crdt/document.js';
6
6
  import type { Plugin } from '../../plugins.js';
7
7
  import type { HistoryClient, HistoryRestoreResult, HistoryUndoResult, HistoryVersion } from './client.js';
8
8
  import { type HistoryPanelLabels, type PartialLabels } from '../../labels.js';
9
+ import type { MenuItemPayload } from '../../plugins/chrome.js';
9
10
  export type { HistoryPanelLabels };
10
11
  /** The panel's English words: the label dictionary's `history` section (`englishLabels.history`). */
11
12
  export declare const defaultHistoryLabels: HistoryPanelLabels;
13
+ /**
14
+ * One row of the versions list, for an `item` snippet: the row shape every
15
+ * menu shares (`item` is the version, `label` its date and slot, `run` and
16
+ * `select` preview it), with the version again. Spread `option` on the
17
+ * row's focusable element (its `role="option"`, id, `aria-selected` and the
18
+ * list's one tab stop): the list's arrows, Home and End walk those rows.
19
+ */
20
+ export type HistoryVersionItem = MenuItemPayload<HistoryVersion> & {
21
+ /** The version (the same as `item`). */
22
+ version: HistoryVersion;
23
+ };
12
24
  export type HistoryPanelProps = {
13
25
  /** Where the versions come from: `createHistoryClient(…)`, or any object with `list`, `read`, `restore` and `undo`. */
14
26
  client: HistoryClient;
@@ -34,6 +46,12 @@ export type HistoryPanelProps = {
34
46
  version?: Snippet<[HistoryVersion, {
35
47
  selected: boolean;
36
48
  }]>;
49
+ /**
50
+ * Replace a version's whole row (the list item's content): one object
51
+ * argument, the shape every menu's rows share. Spread its `option` on
52
+ * the row's button. Takes precedence over `version`.
53
+ */
54
+ item?: Snippet<[HistoryVersionItem]>;
37
55
  /** After a restore the room answered. */
38
56
  onRestore?: (result: HistoryRestoreResult) => void;
39
57
  /** After an undo the room answered. */