@portone/docx-editor 0.5.1 → 0.6.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.
Files changed (146) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/CONTRIBUTING.md +8 -2
  3. package/README.md +1 -1
  4. package/dist/DocxEditor.js +87 -29
  5. package/dist/docx/exportDocx.d.ts +2 -1
  6. package/dist/docx/exportDocx.js +3 -9
  7. package/dist/docx/headersFooters.js +35 -23
  8. package/dist/docx/identities.d.ts +28 -0
  9. package/dist/docx/identities.js +42 -10
  10. package/dist/docx/importDocx.js +25 -26
  11. package/dist/docx/importParagraph.d.ts +1 -4
  12. package/dist/docx/importParagraph.js +4 -18
  13. package/dist/docx/invariants.d.ts +1 -1
  14. package/dist/docx/invariants.js +45 -4
  15. package/dist/docx/notes/newNote.d.ts +39 -0
  16. package/dist/docx/notes/newNote.js +122 -0
  17. package/dist/docx/notes/numbering.d.ts +27 -0
  18. package/dist/docx/notes/numbering.js +120 -0
  19. package/dist/docx/notes/reading.d.ts +58 -0
  20. package/dist/docx/notes/reading.js +98 -0
  21. package/dist/docx/notes/writing.d.ts +19 -0
  22. package/dist/docx/notes/writing.js +96 -0
  23. package/dist/docx/partPlan.d.ts +3 -0
  24. package/dist/docx/partPlanners.d.ts +13 -0
  25. package/dist/docx/partPlanners.js +48 -0
  26. package/dist/docx/sections.d.ts +5 -0
  27. package/dist/docx/sections.js +7 -2
  28. package/dist/docx/serializeParagraph.js +7 -2
  29. package/dist/docx/session.d.ts +5 -0
  30. package/dist/docx/session.js +6 -0
  31. package/dist/docx/storyParts.d.ts +112 -0
  32. package/dist/docx/storyParts.js +289 -0
  33. package/dist/editor/clipboard/htmlReader.d.ts +24 -4
  34. package/dist/editor/clipboard/htmlReader.js +39 -4
  35. package/dist/editor/clipboard/internalChannel.d.ts +30 -3
  36. package/dist/editor/clipboard/internalChannel.js +43 -3
  37. package/dist/editor/clipboard/normalizers.d.ts +35 -8
  38. package/dist/editor/clipboard/normalizers.js +60 -11
  39. package/dist/editor/clipboard/plugin.js +42 -22
  40. package/dist/editor/commands/comments/editing.d.ts +4 -1
  41. package/dist/editor/commands/comments/editing.js +1 -3
  42. package/dist/editor/commands/comments/model.d.ts +7 -0
  43. package/dist/editor/commands/index.d.ts +1 -0
  44. package/dist/editor/commands/index.js +18 -0
  45. package/dist/editor/commands/noteCommands.d.ts +47 -0
  46. package/dist/editor/commands/noteCommands.js +95 -0
  47. package/dist/editor/commands/noteQueries.d.ts +22 -3
  48. package/dist/editor/commands/noteQueries.js +21 -13
  49. package/dist/editor/createEditor.js +25 -3
  50. package/dist/editor/documentStyles.d.ts +3 -0
  51. package/dist/editor/documentStyles.js +2 -1
  52. package/dist/editor/editorDocument.d.ts +24 -0
  53. package/dist/editor/editorDocument.js +34 -4
  54. package/dist/editor/notes/noteSurface.d.ts +53 -0
  55. package/dist/editor/notes/noteSurface.js +246 -0
  56. package/dist/editor/plugins/commentDecorations.js +5 -4
  57. package/dist/editor/plugins/commentRestoration.d.ts +20 -0
  58. package/dist/editor/plugins/commentRestoration.js +168 -0
  59. package/dist/editor/plugins/compositionSelection.d.ts +20 -0
  60. package/dist/editor/plugins/compositionSelection.js +19 -0
  61. package/dist/editor/plugins/keymap.d.ts +14 -0
  62. package/dist/editor/plugins/keymap.js +13 -1
  63. package/dist/editor/plugins/noteLifecycle.d.ts +18 -0
  64. package/dist/editor/plugins/noteLifecycle.js +117 -0
  65. package/dist/editor/plugins/noteNavigation.d.ts +45 -0
  66. package/dist/editor/plugins/noteNavigation.js +86 -0
  67. package/dist/editor/plugins/noteNumbering.d.ts +13 -0
  68. package/dist/editor/plugins/noteNumbering.js +58 -0
  69. package/dist/editor/plugins/rowResize.js +2 -7
  70. package/dist/editor/plugins/tabLayout.js +2 -7
  71. package/dist/editor/stories/storyMarkup.d.ts +24 -0
  72. package/dist/editor/stories/storyMarkup.js +35 -0
  73. package/dist/editor/stories/storyState.d.ts +34 -0
  74. package/dist/editor/stories/storyState.js +74 -0
  75. package/dist/editor/stories/storyView.d.ts +115 -0
  76. package/dist/editor/stories/storyView.js +144 -0
  77. package/dist/numbering/spellers.d.ts +1 -1
  78. package/dist/numbering/spellers.js +6 -1
  79. package/dist/page/blockKinds.d.ts +23 -4
  80. package/dist/page/demands/footnoteDemands.d.ts +18 -0
  81. package/dist/page/demands/footnoteDemands.js +51 -0
  82. package/dist/page/demands/index.d.ts +43 -0
  83. package/dist/page/demands/index.js +8 -0
  84. package/dist/page/kinds/paragraphKind.js +7 -1
  85. package/dist/page/kinds/tableKind.js +9 -1
  86. package/dist/page/measureBlocks.d.ts +2 -0
  87. package/dist/page/measureBlocks.js +31 -10
  88. package/dist/page/pageDecorations.d.ts +9 -2
  89. package/dist/page/pageDecorations.js +18 -5
  90. package/dist/page/pageLayout.d.ts +77 -1
  91. package/dist/page/pageLayout.js +186 -9
  92. package/dist/page/usePageLayout.d.ts +57 -2
  93. package/dist/page/usePageLayout.js +50 -5
  94. package/dist/schema/attrRoles.js +3 -2
  95. package/dist/schema/docxSchema.js +5 -2
  96. package/dist/schema/editGuard.d.ts +1 -1
  97. package/dist/schema/guards.js +1 -2
  98. package/dist/schema/preservedGuards.d.ts +4 -6
  99. package/dist/schema/preservedGuards.js +0 -9
  100. package/dist/schema/stories.d.ts +34 -2
  101. package/dist/schema/stories.js +22 -5
  102. package/dist/styles/classNames.d.ts +23 -6
  103. package/dist/styles/classNames.js +23 -6
  104. package/dist/styles/inlineStyle.d.ts +7 -0
  105. package/dist/styles/inlineStyle.js +11 -5
  106. package/dist/styles/visualScale.d.ts +19 -0
  107. package/dist/styles/visualScale.js +26 -0
  108. package/dist/styles.css +96 -36
  109. package/dist/ui/CommentsPanel.d.ts +6 -1
  110. package/dist/ui/CommentsPanel.js +26 -8
  111. package/dist/ui/InsertImageButton.d.ts +3 -1
  112. package/dist/ui/InsertImageButton.js +3 -2
  113. package/dist/ui/TableMenu.d.ts +5 -2
  114. package/dist/ui/TableMenu.js +18 -1
  115. package/dist/ui/TextMenu.d.ts +5 -2
  116. package/dist/ui/TextMenu.js +21 -6
  117. package/dist/ui/Toolbar.d.ts +17 -2
  118. package/dist/ui/Toolbar.js +23 -10
  119. package/dist/ui/comments/useCommentRailLayout.js +2 -4
  120. package/dist/ui/noteItems.d.ts +21 -0
  121. package/dist/ui/noteItems.js +30 -0
  122. package/dist/ui/notes/NoteAreas.d.ts +73 -0
  123. package/dist/ui/notes/NoteAreas.js +131 -0
  124. package/dist/ui/notes/NoteList.d.ts +25 -0
  125. package/dist/ui/notes/NoteList.js +58 -0
  126. package/dist/ui/notes/StoryRow.d.ts +75 -0
  127. package/dist/ui/notes/StoryRow.js +122 -0
  128. package/dist/ui/notes/noteBands.d.ts +100 -0
  129. package/dist/ui/notes/noteBands.js +142 -0
  130. package/dist/ui/notes/noteHeights.d.ts +10 -0
  131. package/dist/ui/notes/noteHeights.js +24 -0
  132. package/dist/ui/notes/noteSeparator.d.ts +12 -0
  133. package/dist/ui/notes/noteSeparator.js +9 -0
  134. package/dist/ui/notes/useStorySurface.d.ts +68 -0
  135. package/dist/ui/notes/useStorySurface.js +95 -0
  136. package/dist/ui/runCommand.d.ts +8 -1
  137. package/dist/ui/runCommand.js +2 -2
  138. package/dist/ui/shortcutLabels.d.ts +16 -0
  139. package/dist/ui/shortcutLabels.js +9 -0
  140. package/dist/ui/usePageRoom.d.ts +13 -0
  141. package/dist/ui/usePageRoom.js +31 -0
  142. package/package.json +2 -1
  143. package/dist/docx/notes.d.ts +0 -27
  144. package/dist/docx/notes.js +0 -55
  145. package/dist/ui/NotesPanel.d.ts +0 -6
  146. package/dist/ui/NotesPanel.js +0 -33
package/dist/styles.css CHANGED
@@ -270,12 +270,15 @@
270
270
  margin-top: 2px;
271
271
  }
272
272
 
273
- button.docx-editor-comment-meta {
274
- cursor: pointer;
273
+ .docx-editor-comment-detached {
274
+ display: block;
275
+ margin-top: 2px;
276
+ color: #8a8a8a;
277
+ font-style: italic;
275
278
  }
276
279
 
277
- button.docx-editor-comment-meta:disabled {
278
- cursor: default;
280
+ button.docx-editor-comment-meta {
281
+ cursor: pointer;
279
282
  }
280
283
 
281
284
  .docx-editor-comment-body {
@@ -358,48 +361,82 @@ button.docx-editor-comment-body {
358
361
  cursor: help;
359
362
  }
360
363
 
361
- .docx-editor-notes {
364
+ /*
365
+ * Notes drawn from their stories (`ui/notes`): footnotes over the paper at the foot of the page
366
+ * that refers to them, and notes listed after the last page. The editor hands both boxes the
367
+ * document's own text defaults, and the text is set as `prosemirror-view` sets an editor's, so a
368
+ * note wraps where it would in one.
369
+ */
370
+ .docx-editor-note-areas {
371
+ position: absolute;
372
+ pointer-events: none;
373
+ }
374
+
375
+ .docx-editor-footnote-area {
376
+ position: absolute;
377
+ overflow: hidden;
378
+ pointer-events: auto;
379
+ }
380
+
381
+ /* The endnotes after the last paragraph, drawn over the room the layout kept for them */
382
+ .docx-editor-endnote-area {
383
+ position: absolute;
384
+ overflow: hidden;
385
+ pointer-events: auto;
386
+ }
387
+
388
+ .docx-editor-footnote-area,
389
+ .docx-editor-endnote-area,
390
+ .docx-editor-note-list {
362
391
  box-sizing: border-box;
392
+ color: #000000;
393
+ font-family: var(--docx-editor-font-family, sans-serif);
394
+ font-size: var(--docx-editor-font-size, 10pt);
395
+ line-height: var(--docx-editor-line-height, 1.3);
396
+ white-space: break-spaces;
397
+ word-wrap: break-word;
398
+ font-variant-ligatures: none;
399
+ font-feature-settings: "liga" 0;
400
+ }
401
+
402
+ .docx-editor-note-list {
363
403
  margin: 16px auto 24px;
364
- padding: 18px 24px;
365
- border-top: 1px solid rgb(0 0 0 / 18%);
404
+ padding-block: 24px;
366
405
  background: #ffffff;
367
406
  box-shadow: 0 1px 6px rgb(0 0 0 / 18%);
368
- color: #242424;
369
407
  }
370
408
 
371
- .docx-editor-notes-heading {
372
- margin: 0 0 10px;
373
- font-size: 14px;
409
+ /* A one-pixel rule across the middle of the height the layout keeps for it */
410
+ .docx-editor-note-separator {
411
+ background:
412
+ linear-gradient(#000000, #000000) left center / 100% 1px no-repeat;
374
413
  }
375
414
 
376
- .docx-editor-notes-list {
377
- display: flex;
378
- flex-direction: column;
379
- gap: 8px;
380
- padding: 0;
381
- margin: 0;
382
- list-style: none;
415
+ /* Holds the margins of the paragraphs inside it, so a row measures as tall as it stands */
416
+ .docx-editor-note-row {
417
+ display: flow-root;
383
418
  }
384
419
 
385
- .docx-editor-notes-list li {
386
- display: grid;
387
- grid-template-columns: auto 1fr;
388
- gap: 10px;
389
- align-items: baseline;
420
+ /*
421
+ * The note the caret is in.
422
+ *
423
+ * Neither the tint nor the ring around it takes any room, so the row measures the same height
424
+ * whether it is markup or the editing view, and the page keeps the room it already kept.
425
+ */
426
+ .docx-editor-note-row-open {
427
+ border-radius: 2px;
428
+ background: rgb(49 93 168 / 7%);
429
+ box-shadow: 0 0 0 3px rgb(49 93 168 / 7%);
390
430
  }
391
431
 
392
- .docx-editor-note-label {
393
- color: #315da8;
394
- font-size: 12px;
395
- font-weight: 600;
396
- white-space: nowrap;
432
+ /* The view one story is edited in. It sets its text from the box it is drawn in */
433
+ .docx-editor-story-body {
434
+ outline: none;
397
435
  }
398
436
 
399
- .docx-editor-note-body {
400
- margin: 0;
401
- white-space: pre-wrap;
402
- overflow-wrap: anywhere;
437
+ .docx-editor-note-mark {
438
+ font-size: 0.72em;
439
+ line-height: 0;
403
440
  }
404
441
 
405
442
  /* The fallback for browsers that do not know the two properties above (Safari) */
@@ -434,20 +471,43 @@ button.docx-editor-comment-body {
434
471
  background: transparent;
435
472
  }
436
473
 
474
+ /*
475
+ * The reader's zoom scales the page layer with a transform and not with the `zoom`
476
+ * property, so the paper is laid out at its own size whatever it is being read at and only
477
+ * the painting is scaled. Under `zoom` Chromium lays every box out on whole device pixels
478
+ * of the scaled rendering, so a table row came out a fraction taller at one zoom than at
479
+ * another and a document sitting near a page boundary broke into a different number of
480
+ * pages for each reader.
481
+ *
482
+ * A transform leaves the layout box at its unscaled size, and a scroll box takes in both
483
+ * that box and the transformed one, so a shrunk layer left in the flow would keep the
484
+ * scroll area of a full-size one. The layer is therefore taken out of the flow, and this
485
+ * box stands in the flow in its place, holding the height the layer is drawn at
486
+ * (`ui/usePageRoom`).
487
+ */
488
+ .docx-editor-page-box {
489
+ position: relative;
490
+ width: 100%;
491
+ }
492
+
437
493
  /*
438
494
  * The paper and the boundary guides overlaid on it share one positioning origin.
439
495
  *
440
496
  * The side padding lives here rather than on the scroll box, because Chromium does not
441
497
  * draw a scroll box's own padding past the right edge of sideways-scrolled content.
442
498
  * Shrink-wrapping puts the padding beyond the paper even then, and min-width keeps the
443
- * layer filling the box on wide screens so the paper stays centered.
499
+ * layer filling the box on wide screens so the paper stays centered - widened by the
500
+ * reciprocal of the scale, since it is the scaled layer that has to fill the box.
444
501
  */
445
502
  .docx-editor-page-layer {
446
- position: relative;
447
- width: fit-content;
448
- min-width: 100%;
503
+ position: absolute;
504
+ inset: 0 auto auto 0;
505
+ width: max-content;
506
+ min-width: calc(100% / var(--docx-editor-zoom, 1));
449
507
  box-sizing: border-box;
450
508
  padding: 0 16px;
509
+ transform: scale(var(--docx-editor-zoom, 1));
510
+ transform-origin: 0 0;
451
511
  }
452
512
 
453
513
  @media (max-width: 840px) {
@@ -1,7 +1,12 @@
1
1
  import type { EditorState } from "prosemirror-state";
2
2
  import type { EditorView } from "prosemirror-view";
3
3
  import { type ReactElement } from "react";
4
- import { type CommentAuthor } from "../editor/commands/commentCommands";
4
+ import { type CommentAuthor, type DocumentComment } from "../editor/commands/commentCommands";
5
+ /**
6
+ * Whether the rail beside the page carries this comment: the thread is unsettled, and the text it
7
+ * was written for is still there to draw it next to.
8
+ */
9
+ export declare function shownBesideThePage(comment: DocumentComment): boolean;
5
10
  export interface CommentsPanelProps {
6
11
  view: EditorView;
7
12
  state: EditorState;
@@ -43,6 +43,19 @@ function run(view, command) {
43
43
  }
44
44
  var REFUSED_COMMENT = "This comment could not be added. The text it was written for may have been changed or locked since.";
45
45
  var REFUSED_REPLY = "This reply could not be added. The comment it answers may have been changed or deleted since.";
46
+ var DETACHED_LABEL = "Original content deleted";
47
+ function shownBesideThePage(comment) {
48
+ return !comment.resolved && comment.anchored;
49
+ }
50
+ function CommentIdentity({
51
+ comment
52
+ }) {
53
+ const date = shownDate(comment.date);
54
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
55
+ /* @__PURE__ */ jsx("span", { className: editorClassNames.commentAuthor, children: comment.author ?? "Unknown author" }),
56
+ date && /* @__PURE__ */ jsx("span", { className: editorClassNames.commentDate, children: date })
57
+ ] });
58
+ }
46
59
  function sameTarget(left, right) {
47
60
  return left?.commentId === right.commentId && left.replyId === right.replyId;
48
61
  }
@@ -74,7 +87,7 @@ function CommentsPanel({
74
87
  if (Number.isNaN(leftTime)) return 1;
75
88
  if (Number.isNaN(rightTime)) return -1;
76
89
  return rightTime - leftTime;
77
- }) : comments.filter((comment) => !comment.resolved),
90
+ }) : comments.filter(shownBesideThePage),
78
91
  [allCommentsOpen, comments]
79
92
  );
80
93
  const { positions, canvasHeight } = useCommentRailLayout({
@@ -158,22 +171,26 @@ function CommentsPanel({
158
171
  {
159
172
  className: editorClassNames.commentPosition,
160
173
  "data-resolved": comment.resolved ? "true" : void 0,
174
+ "data-detached": comment.anchored ? void 0 : "true",
161
175
  "data-comment-position": comment.id,
162
176
  style: allCommentsOpen ? void 0 : { top: `${positions.get(comment.id) ?? 64}px` },
163
177
  children: /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentCard, children: [
164
178
  /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentHeader, children: [
165
- /* @__PURE__ */ jsxs(
179
+ comment.anchored ? /* @__PURE__ */ jsx(
166
180
  "button",
167
181
  {
168
182
  type: "button",
169
183
  className: editorClassNames.commentMeta,
170
184
  onClick: () => run(view, selectComment(comment.id)),
171
- disabled: comment.resolved,
172
- children: [
173
- /* @__PURE__ */ jsx("span", { className: editorClassNames.commentAuthor, children: comment.author ?? "Unknown author" }),
174
- shownDate(comment.date) && /* @__PURE__ */ jsx("span", { className: editorClassNames.commentDate, children: shownDate(comment.date) })
175
- ]
185
+ children: /* @__PURE__ */ jsx(CommentIdentity, { comment })
176
186
  }
187
+ ) : (
188
+ // Nothing on the page to go to, so the identity is text rather than a button:
189
+ // a click here leaves the caret and the scroll where the reader left them
190
+ /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentMeta, children: [
191
+ /* @__PURE__ */ jsx(CommentIdentity, { comment }),
192
+ /* @__PURE__ */ jsx("span", { className: editorClassNames.commentDetached, children: DETACHED_LABEL })
193
+ ] })
177
194
  ),
178
195
  writer !== null && !rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentIconActions, children: [
179
196
  rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
@@ -361,5 +378,6 @@ function CommentsPanel({
361
378
  );
362
379
  }
363
380
  export {
364
- CommentsPanel
381
+ CommentsPanel,
382
+ shownBesideThePage
365
383
  };
@@ -5,5 +5,7 @@ import { type ReactElement } from "react";
5
5
  export interface InsertImageButtonProps {
6
6
  view: EditorView;
7
7
  state: EditorState;
8
+ /** Turned off whatever the state says, which is what a surface taking no image sets */
9
+ disabled?: boolean;
8
10
  }
9
- export declare function InsertImageButton({ view, state, }: InsertImageButtonProps): ReactElement;
11
+ export declare function InsertImageButton({ view, state, disabled, }: InsertImageButtonProps): ReactElement;
@@ -11,7 +11,8 @@ import { ToolbarButton } from "./ToolbarButton.js";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  function InsertImageButton({
13
13
  view,
14
- state
14
+ state,
15
+ disabled = false
15
16
  }) {
16
17
  const pickerRef = useRef(null);
17
18
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -20,7 +21,7 @@ function InsertImageButton({
20
21
  {
21
22
  label: "Insert image",
22
23
  icon: Image,
23
- disabled: !view.editable || !canInsertImage(state),
24
+ disabled: disabled || !view.editable || !canInsertImage(state),
24
25
  onRun: () => pickerRef.current?.click()
25
26
  }
26
27
  ),
@@ -1,12 +1,15 @@
1
- /** Table context menu with an optional Unlock action for whole-cell locks. */
1
+ /** Table context menu with footnote insertion and an optional Unlock action for whole-cell locks. */
2
2
  import type { EditorState } from "prosemirror-state";
3
3
  import type { EditorView } from "prosemirror-view";
4
4
  import { type ReactElement } from "react";
5
5
  import { type TableMenuAnchor } from "../editor/plugins/tableContextMenu";
6
+ import type { SurfaceCapabilities } from "../editor/stories/storyView";
6
7
  export interface TableMenuProps {
7
8
  view: EditorView;
8
9
  state: EditorState;
9
10
  anchor: TableMenuAnchor;
11
+ /** What the surface holding the caret takes, which the entries that put something in ask */
12
+ takes: SurfaceCapabilities;
10
13
  allowLocking?: boolean;
11
14
  }
12
- export declare function TableMenu({ view, state, anchor, allowLocking, }: TableMenuProps): ReactElement;
15
+ export declare function TableMenu({ view, state, anchor, takes, allowLocking, }: TableMenuProps): ReactElement;
@@ -16,6 +16,7 @@ import {
16
16
  mergeCells,
17
17
  splitCell
18
18
  } from "../table/index.js";
19
+ import { noteItems } from "./noteItems.js";
19
20
  import { usePanelAtPoint } from "./panelPlacement.js";
20
21
  import { commandRunner } from "./runCommand.js";
21
22
  import { useDismiss } from "./useDismiss.js";
@@ -54,10 +55,21 @@ var UNLOCK_GROUP = {
54
55
  name: "lock",
55
56
  items: [{ label: "Unlock", command: unlockSelection }]
56
57
  };
58
+ function noteGroup(takes) {
59
+ const notes = noteItems(takes);
60
+ return notes.length === 0 ? null : {
61
+ name: "note",
62
+ items: notes.map((note) => ({
63
+ label: note.label,
64
+ command: note.command
65
+ }))
66
+ };
67
+ }
57
68
  function TableMenu({
58
69
  view,
59
70
  state,
60
71
  anchor,
72
+ takes,
61
73
  allowLocking = false
62
74
  }) {
63
75
  const box = useRef(null);
@@ -74,7 +86,12 @@ function TableMenu({
74
86
  run(command);
75
87
  close();
76
88
  };
77
- const groups = allowLocking ? [...MENU_GROUPS, UNLOCK_GROUP] : MENU_GROUPS;
89
+ const note = noteGroup(takes);
90
+ const groups = [
91
+ ...MENU_GROUPS,
92
+ ...note === null ? [] : [note],
93
+ ...allowLocking ? [UNLOCK_GROUP] : []
94
+ ];
78
95
  return /* @__PURE__ */ jsx(
79
96
  "div",
80
97
  {
@@ -1,13 +1,16 @@
1
- /** Custom text context menu with clipboard, comment, and optional locking actions. */
1
+ /** Custom text context menu with clipboard, comment, footnote, and optional locking actions. */
2
2
  import type { EditorState } from "prosemirror-state";
3
3
  import type { EditorView } from "prosemirror-view";
4
4
  import { type ReactElement } from "react";
5
5
  import { type TextMenuAnchor } from "../editor/plugins/textContextMenu";
6
+ import type { SurfaceCapabilities } from "../editor/stories/storyView";
6
7
  export interface TextMenuProps {
7
8
  view: EditorView;
8
9
  state: EditorState;
9
10
  anchor: TextMenuAnchor;
11
+ /** What the surface holding the caret takes, which the entries that put something in ask */
12
+ takes: SurfaceCapabilities;
10
13
  /** Whether the entries that lock and unlock a stretch of text are offered */
11
14
  allowLocking?: boolean;
12
15
  }
13
- export declare function TextMenu({ view, state, anchor, allowLocking, }: TextMenuProps): ReactElement;
16
+ export declare function TextMenu({ view, state, anchor, takes, allowLocking, }: TextMenuProps): ReactElement;
@@ -23,13 +23,14 @@ import {
23
23
  } from "../editor/plugins/textContextMenu.js";
24
24
  import { editsShut } from "../schema/protectionState.js";
25
25
  import { editorClassNames } from "../styles/classNames.js";
26
+ import { noteItems } from "./noteItems.js";
26
27
  import { usePanelAtPoint } from "./panelPlacement.js";
27
28
  import { commandRunner } from "./runCommand.js";
29
+ import { modifierLabels } from "./shortcutLabels.js";
28
30
  import { ICON_SIZE } from "./ToolbarButton.js";
29
31
  import { useDismiss } from "./useDismiss.js";
30
32
  import { useMenuKeyboard } from "./useMenuKeyboard.js";
31
33
  import { jsx, jsxs } from "react/jsx-runtime";
32
- var MOD = typeof navigator !== "undefined" && navigator.platform.includes("Mac") ? "\u2318" : "Ctrl+";
33
34
  function clipboardCommand(view, command) {
34
35
  view.focus();
35
36
  document.execCommand(command);
@@ -95,6 +96,7 @@ function TextMenu({
95
96
  view,
96
97
  state,
97
98
  anchor,
99
+ takes,
98
100
  allowLocking = false
99
101
  }) {
100
102
  const box = useRef(null);
@@ -108,11 +110,12 @@ function TextMenu({
108
110
  const keys = useMenuKeyboard({ menu: box, onClose: close });
109
111
  const selected = !state.selection.empty;
110
112
  const shut = selectionTouchesLocked(state);
113
+ const { mod } = modifierLabels();
111
114
  const bodyOpen = !editsShut(state);
112
115
  const copy = {
113
116
  label: "Copy",
114
117
  icon: Copy,
115
- hint: `${MOD}C`,
118
+ hint: `${mod}C`,
116
119
  enabled: selected,
117
120
  run: () => clipboardCommand(view, "copy")
118
121
  };
@@ -121,7 +124,7 @@ function TextMenu({
121
124
  {
122
125
  label: "Cut",
123
126
  icon: Scissors,
124
- hint: `${MOD}X`,
127
+ hint: `${mod}X`,
125
128
  enabled: selected && !shut,
126
129
  run: () => clipboardCommand(view, "cut")
127
130
  },
@@ -129,7 +132,7 @@ function TextMenu({
129
132
  {
130
133
  label: "Paste",
131
134
  icon: ClipboardPaste,
132
- hint: `${MOD}V`,
135
+ hint: `${mod}V`,
133
136
  enabled: !shut,
134
137
  run: () => {
135
138
  void pasteFromClipboard(view);
@@ -143,14 +146,26 @@ function TextMenu({
143
146
  }
144
147
  ] : [copy]
145
148
  ];
146
- groups.push([
149
+ const anchored = [
147
150
  {
148
151
  label: "Add comment",
149
152
  icon: MessageSquarePlus,
150
153
  enabled: canAddComment(state),
151
154
  run: () => run(openCommentComposer)
152
155
  }
153
- ]);
156
+ ];
157
+ if (bodyOpen) {
158
+ for (const note of noteItems(takes)) {
159
+ anchored.push({
160
+ label: note.label,
161
+ icon: note.icon,
162
+ hint: note.hint,
163
+ enabled: note.command(state),
164
+ run: () => run(note.command)
165
+ });
166
+ }
167
+ }
168
+ groups.push(anchored);
154
169
  if (allowLocking && bodyOpen) {
155
170
  groups.push([lockItem(selectionLock(state), run)]);
156
171
  }
@@ -2,12 +2,27 @@
2
2
  import type { EditorState } from "prosemirror-state";
3
3
  import type { EditorView } from "prosemirror-view";
4
4
  import { type ReactElement } from "react";
5
+ import type { ActiveSurface } from "../editor/stories/storyView";
5
6
  import type { FontFallbacks } from "../styles/fontStack";
6
7
  import type { DocxEditorPresets } from "./presets";
7
8
  import type { DocxEditorZoom } from "./zoom";
8
- export interface ToolbarProps {
9
+ /** One editing surface the toolbar can act on: the document's own, or a story's */
10
+ export interface ToolbarSurface {
9
11
  view: EditorView;
10
12
  state: EditorState;
13
+ }
14
+ export interface ToolbarProps {
15
+ /**
16
+ * The document itself, which undo and redo always read: a story edit is written into the
17
+ * document as one change, so the document's history is the one both surfaces share
18
+ * (`editor/stories`).
19
+ */
20
+ main: ToolbarSurface;
21
+ /**
22
+ * Where the caret is. Everything but undo and redo is asked of it and dispatched to it, so the
23
+ * formatting controls act on the note being edited rather than on the text behind it.
24
+ */
25
+ active: ActiveSurface;
11
26
  fontFallbacks?: FontFallbacks;
12
27
  /** Optional values offered by toolbar pickers. */
13
28
  presets?: DocxEditorPresets;
@@ -16,4 +31,4 @@ export interface ToolbarProps {
16
31
  zoom: DocxEditorZoom;
17
32
  onZoomChange: (zoom: DocxEditorZoom) => void;
18
33
  }
19
- export declare function Toolbar({ view, state, fontFallbacks, presets, onToggleComments, commentsOpen, zoom, onZoomChange, }: ToolbarProps): ReactElement;
34
+ export declare function Toolbar({ main, active, fontFallbacks, presets, onToggleComments, commentsOpen, zoom, onZoomChange, }: ToolbarProps): ReactElement;
@@ -272,7 +272,11 @@ function ParagraphLayoutGroup({
272
272
  )
273
273
  ] });
274
274
  }
275
- function ListGroup({ state, run }) {
275
+ function ListGroup({
276
+ state,
277
+ run,
278
+ disabled
279
+ }) {
276
280
  const kind = activeListKind(state);
277
281
  return /* @__PURE__ */ jsx("div", { className: editorClassNames.toolbarGroup, children: LIST_TOGGLES.map((toggle) => /* @__PURE__ */ jsx(
278
282
  ToolbarButton,
@@ -280,7 +284,7 @@ function ListGroup({ state, run }) {
280
284
  label: toggle.label,
281
285
  icon: toggle.icon,
282
286
  pressed: kind === toggle.kind,
283
- disabled: !toggle.command(state),
287
+ disabled: disabled || !toggle.command(state),
284
288
  onRun: () => run(toggle.command)
285
289
  },
286
290
  toggle.kind
@@ -299,8 +303,8 @@ function IndentGroup({ state, run }) {
299
303
  )) });
300
304
  }
301
305
  function Toolbar({
302
- view,
303
- state,
306
+ main,
307
+ active,
304
308
  fontFallbacks,
305
309
  presets,
306
310
  onToggleComments,
@@ -308,7 +312,9 @@ function Toolbar({
308
312
  zoom,
309
313
  onZoomChange
310
314
  }) {
315
+ const { view, state, takes } = active;
311
316
  const run = commandRunner(view);
317
+ const runOnMain = commandRunner(main.view, view);
312
318
  const bar = useRef(null);
313
319
  const keys = useLinearWalk({
314
320
  container: bar,
@@ -340,7 +346,7 @@ function Toolbar({
340
346
  onKeyDown,
341
347
  ...hushedAttribute(hushed),
342
348
  children: [
343
- /* @__PURE__ */ jsx(HistoryGroup, { state, run }),
349
+ /* @__PURE__ */ jsx(HistoryGroup, { state: main.state, run: runOnMain }),
344
350
  /* @__PURE__ */ jsx(ZoomSelect, { zoom, onZoomChange }),
345
351
  /* @__PURE__ */ jsx(Separator, {}),
346
352
  /* @__PURE__ */ jsxs("div", { className: editorClassNames.toolbarGroup, children: [
@@ -394,7 +400,7 @@ function Toolbar({
394
400
  }
395
401
  ),
396
402
  /* @__PURE__ */ jsx(Separator, {}),
397
- /* @__PURE__ */ jsx(ListGroup, { state, run }),
403
+ /* @__PURE__ */ jsx(ListGroup, { state, run, disabled: !takes.has("list") }),
398
404
  /* @__PURE__ */ jsx(IndentGroup, { state, run }),
399
405
  /* @__PURE__ */ jsx(Separator, {}),
400
406
  /* @__PURE__ */ jsx(
@@ -414,17 +420,24 @@ function Toolbar({
414
420
  label: "Insert table",
415
421
  icon: Table,
416
422
  panel: "dialog",
417
- disabled: !canInsertTable(state),
423
+ disabled: !takes.has("table") || !canInsertTable(state),
418
424
  children: ({ close, takeFocus }) => /* @__PURE__ */ jsx(TableSizePicker, { run, close, takeFocus })
419
425
  }
420
426
  ),
421
- /* @__PURE__ */ jsx(InsertImageButton, { view, state }),
427
+ /* @__PURE__ */ jsx(
428
+ InsertImageButton,
429
+ {
430
+ view,
431
+ state,
432
+ disabled: !takes.has("image")
433
+ }
434
+ ),
422
435
  /* @__PURE__ */ jsx(
423
436
  ToolbarButton,
424
437
  {
425
438
  label: "Link",
426
439
  icon: Link,
427
- disabled: !openLinkPanel(state),
440
+ disabled: !takes.has("link") || !openLinkPanel(state),
428
441
  onRun: () => run(openLinkPanel)
429
442
  }
430
443
  ),
@@ -434,7 +447,7 @@ function Toolbar({
434
447
  label: commentsOpen ? "Hide comments" : "Show comments",
435
448
  icon: MessagesSquare,
436
449
  pressed: commentsOpen,
437
- disabled: !onToggleComments,
450
+ disabled: !takes.has("comment") || !onToggleComments,
438
451
  onRun: () => onToggleComments?.()
439
452
  }
440
453
  )
@@ -1,6 +1,7 @@
1
1
  // src/ui/comments/useCommentRailLayout.ts
2
2
  import { useLayoutEffect, useRef, useState } from "react";
3
3
  import { editorClassNames } from "../../styles/classNames.js";
4
+ import { visualScaleOf } from "../../styles/visualScale.js";
4
5
  var COMPOSER_POSITION = "composer";
5
6
  var VERTICAL_SCROLLBAR_CLEARANCE = 16;
6
7
  function estimatedCardHeight(comment) {
@@ -90,10 +91,7 @@ function useCommentRailLayout({
90
91
  const reserveCommentWidth = () => {
91
92
  if (!pageLayer) return;
92
93
  const drawnWidth = rail.getBoundingClientRect().width || rail.offsetWidth;
93
- const computedZoom = Number.parseFloat(
94
- frameWindow?.getComputedStyle(pageLayer).zoom ?? ""
95
- );
96
- const scale = Number.isFinite(computedZoom) && computedZoom > 0 ? computedZoom : 1;
94
+ const scale = visualScaleOf(pageLayer);
97
95
  pageLayer.style.paddingRight = `${(drawnWidth + 16 + VERTICAL_SCROLLBAR_CLEARANCE) / scale}px`;
98
96
  };
99
97
  const placeBesidePaper = () => {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The controls that put a note in, which every menu over the text draws its rows from.
3
+ *
4
+ * A right click is answered by one of two menus - the text menu, and the table menu wherever the
5
+ * click lands in a cell with nothing selected (`editor/plugins/textContextMenu`) - and a note goes
6
+ * into a table cell as readily as into a paragraph, so each row is written down once here. The
7
+ * command asked without a dispatch is what each menu draws a dead row from, and whether the rows
8
+ * are there at all is the surface's own declaration (`editor/stories/storyView`).
9
+ */
10
+ import type { LucideIcon } from "lucide-react";
11
+ import type { Command } from "prosemirror-state";
12
+ import type { SurfaceCapabilities } from "../editor/stories/storyView";
13
+ export interface NoteItem {
14
+ readonly label: string;
15
+ readonly icon: LucideIcon;
16
+ /** The shortcut for the same action, for a menu that labels its rows with theirs */
17
+ readonly hint: string;
18
+ readonly command: Command;
19
+ }
20
+ /** The rows a menu draws, and none for a surface that takes no note */
21
+ export declare function noteItems(takes: SurfaceCapabilities): readonly NoteItem[];
@@ -0,0 +1,30 @@
1
+ // src/ui/noteItems.ts
2
+ import { ListEnd, Superscript } from "lucide-react";
3
+ import { insertEndnote, insertFootnote } from "../editor/commands/noteCommands.js";
4
+ import { NOTE_KEYS } from "../editor/plugins/keymap.js";
5
+ import { modifierLabels } from "./shortcutLabels.js";
6
+ function keyLetter(binding) {
7
+ return (binding.split("-").at(-1) ?? "").toUpperCase();
8
+ }
9
+ var NO_ITEMS = [];
10
+ function noteItems(takes) {
11
+ if (!takes.has("note")) return NO_ITEMS;
12
+ const { mod, alt } = modifierLabels();
13
+ return [
14
+ {
15
+ label: "Insert footnote",
16
+ icon: Superscript,
17
+ hint: `${mod}${alt}${keyLetter(NOTE_KEYS.footnote)}`,
18
+ command: insertFootnote
19
+ },
20
+ {
21
+ label: "Insert endnote",
22
+ icon: ListEnd,
23
+ hint: `${mod}${alt}${keyLetter(NOTE_KEYS.endnote)}`,
24
+ command: insertEndnote
25
+ }
26
+ ];
27
+ }
28
+ export {
29
+ noteItems
30
+ };