@portone/docx-editor 0.5.1 → 0.6.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @portone/docx-editor
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#131](https://github.com/portone-io/docx-editor/pull/131) [`5dd468a`](https://github.com/portone-io/docx-editor/commit/5dd468ae14d6210070ab6c8275d16e03b20cf64c) Thanks [@Deea222](https://github.com/Deea222)! - A comment now outlives the text it was written for, and a resolved one stops marking the page.
8
+
9
+ Deleting commented text used to delete the comment and everything said under it. The thread now
10
+ stays. Trim the highlighted text - type over part of it, backspace through its edge - and the
11
+ comment follows what is left. Delete all of it and the comment comes back detached: no longer drawn
12
+ beside the page, since there is nothing to draw it next to, and listed under All comments marked
13
+ "Original content deleted". Cutting commented text leaves it behind the same way instead of dropping
14
+ it. Word has no detached state, so such a comment reaches the saved file as an ordinary comment
15
+ sitting where the deleted text was. Deleting a comment from its own Delete button still deletes it,
16
+ and undo and redo still take a deletion back and make it again.
17
+
18
+ Resolving a thread now takes its highlight off the text, and reopening puts it back. Clicking a
19
+ resolved comment's card in All comments selects the text it was written about and scrolls it into
20
+ view, which `selectComment` now does for every caller.
21
+
22
+ `documentComments` reports a new `anchored` field on every comment, `false` for one that no longer
23
+ marks a stretch of the document. Its existing fields are unchanged, so a panel of your own keeps
24
+ working and can read the new field to decide what a click on a card should do.
25
+
3
26
  ## 0.5.1
4
27
 
5
28
  ### Patch Changes
package/CONTRIBUTING.md CHANGED
@@ -44,22 +44,28 @@ Run a specialized check when your change affects its area:
44
44
  pnpm test:package
45
45
  pnpm verify:package
46
46
  pnpm test:e2e
47
+ pnpm bench
47
48
  ```
48
49
 
50
+ A pull request that changes performance-sensitive code attaches its `pnpm bench` output to the description.
51
+
49
52
  [Testing](https://github.com/portone-io/docx-editor/blob/main/docs/testing.md) explains the scope and prerequisites of every check.
53
+ CI runs `pnpm test:package` on every pull request in a separate job; it is not part of the local `pnpm check` command.
50
54
 
51
55
  A command added to `./commands` or `./table` needs a writer probe in `src/docx/__testing__/writerProbes.ts` with a `check` of its immediate effect, so its export is validated before another command can overwrite it, or an entry in `NOT_A_WRITER` giving the reason it reaches no writer.
52
56
 
53
57
  ## Changesets
54
58
 
55
- If you changed anything under `src/`, run `pnpm changeset` and commit the file it writes.
59
+ If you changed production code under `src/`, run `pnpm changeset` and commit the file it writes.
56
60
  It becomes the CHANGELOG entry, so write it for someone reading release notes rather than the diff.
61
+ Changes limited to tests, fixtures, scripts, or documentation do not need a changeset.
57
62
 
58
- The public API is the exported names and types of the four entry points - `.`, `./core`, `./commands`, and `./table` - together with the parts of the document model that [What a plugin may rely on](https://docx-editor.portone.io/docs/custom-controls#what-a-plugin-may-rely-on) calls stable.
63
+ The public API is the exported names and types of the four entry points - `.`, `./core`, `./commands`, and `./table` - together with the parts of the document model that [What a plugin may rely on](https://docx-editor.portone.io/docs/editor-api/plugins-and-presets#what-a-plugin-may-rely-on) calls stable.
59
64
  An attr that page calls internal is not part of it, and neither is anything under `src/` that no entry point reaches.
60
65
 
61
66
  Choose `patch` unless the change adds to that surface, takes something out of it, or changes what a part of it means.
62
67
  Those are `minor` while the package is below 1.0.
68
+ A diff to `src/schema/attrRoles.ts` is where to check this: a change to a stable document-model attr is `minor` even when the declaration reports do not change.
63
69
  A declaration added, removed, or rewritten in `etc/*.api.md` after running `pnpm api:update` is what that looks like; a line that only gains or loses an `(undocumented)` marker is not.
64
70
  Commit the regenerated report in the same commit as the changeset.
65
71
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ export function Editor({
47
47
  - Preserves document structures and package parts across edits, and records who wrote each comment.
48
48
  - Reads document styles, theme fonts, page layout, and CJK font information for display.
49
49
 
50
- See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build [custom controls](https://docx-editor.portone.io/docs/custom-controls) or use [programmatic DOCX import and export](https://docx-editor.portone.io/docs/core).
50
+ See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build controls of your own with the [Editor API](https://docx-editor.portone.io/docs/editor-api) or use [programmatic DOCX import and export](https://docx-editor.portone.io/docs/core).
51
51
 
52
52
  The full [documentation](https://docx-editor.portone.io/docs) lives on the documentation site.
53
53
 
@@ -31,7 +31,7 @@ import { usePageLayout } from "./page/usePageLayout.js";
31
31
  import { editingProtection, protectionOf } from "./schema/protectionState.js";
32
32
  import { storyNodeOf } from "./schema/stories.js";
33
33
  import { editorClassNames } from "./styles/classNames.js";
34
- import { CommentsPanel } from "./ui/CommentsPanel.js";
34
+ import { CommentsPanel, shownBesideThePage } from "./ui/CommentsPanel.js";
35
35
  import { LinkCard } from "./ui/LinkCard.js";
36
36
  import { LinkPanel } from "./ui/LinkPanel.js";
37
37
  import { NotesPanel } from "./ui/NotesPanel.js";
@@ -280,9 +280,9 @@ function DocxEditorSurface({
280
280
  const tableAnchor = live && !textAnchor ? tableMenuAnchor(live.state) : null;
281
281
  const linkAtCursor = live && !live.view.composing && !isLinkPanelOpen(live.state) ? activeLinkSpan(live.state) : null;
282
282
  const comments = live?.state === void 0 ? [] : commentProjection.read(live.state).comments;
283
- const hasUnresolvedComments = comments.some((comment) => !comment.resolved);
283
+ const hasRailComments = comments.some(shownBesideThePage);
284
284
  const composerOpen = live !== null && isCommentComposerOpen(live.state);
285
- const showComments = live !== null && (commentsOpen || composerOpen || hasUnresolvedComments);
285
+ const showComments = live !== null && (commentsOpen || composerOpen || hasRailComments);
286
286
  const commentsPanel = live && showComments && /* @__PURE__ */ jsx(
287
287
  CommentsPanel,
288
288
  {
@@ -41,5 +41,8 @@ export declare function removeCommentReply(commentId: string, replyId: string):
41
41
  * document without standing for anything.
42
42
  */
43
43
  export declare function removeComment(id: string): Command;
44
- /** Selects the text anchored by a comment, or places the caret at a point comment. */
44
+ /**
45
+ * Selects the text a comment is anchored to and scrolls it into view. A comment marking no stretch
46
+ * - one written at a point, or one whose text was deleted - takes the caret to where it stands.
47
+ */
45
48
  export declare function selectComment(id: string): Command;
@@ -342,9 +342,7 @@ function selectComment(id) {
342
342
  const comment = commentById(state, id);
343
343
  if (!comment) return false;
344
344
  dispatch?.(
345
- state.tr.setSelection(
346
- TextSelection.create(state.doc, comment.from, comment.to)
347
- )
345
+ state.tr.setSelection(TextSelection.create(state.doc, comment.from, comment.to)).scrollIntoView()
348
346
  );
349
347
  return true;
350
348
  };
@@ -42,6 +42,13 @@ export interface DocumentComment {
42
42
  readonly from: number;
43
43
  readonly to: number;
44
44
  readonly referencePos: number;
45
+ /**
46
+ * Whether the comment still marks a stretch of the document: both range markers stand, and the
47
+ * opening one stands before the closing one. A comment that lost both markers with the text an
48
+ * edit deleted is detached - `from` and `to` are then both where its reference stands - and is
49
+ * drawn nowhere on the page.
50
+ */
51
+ readonly anchored: boolean;
45
52
  readonly resolved: boolean;
46
53
  readonly replies: readonly DocumentCommentReply[];
47
54
  }
@@ -26,6 +26,7 @@ import { imageFiles } from "./imageFiles.js";
26
26
  import { columnResize } from "./plugins/columnResize.js";
27
27
  import { commentComposer } from "./plugins/commentComposer.js";
28
28
  import { commentDecorations } from "./plugins/commentDecorations.js";
29
+ import { commentRestoration } from "./plugins/commentRestoration.js";
29
30
  import {
30
31
  displayDerivation,
31
32
  withDerivedDisplay
@@ -96,6 +97,9 @@ function createEditorState(doc, options = {}) {
96
97
  // The list beside the page, the lookup the comment commands ask, and these ranges are the
97
98
  // one walk this plugin holds (`plugins/commentDecorations`)
98
99
  commentDecorations(),
100
+ // A comment outlives the text it was written for: an edit that sweeps its reference away has
101
+ // it put back where the deletion left, detached from the page (`plugins/commentRestoration`)
102
+ commentRestoration(),
99
103
  // What the notes under the page are, worked out from the document the same way
100
104
  noteProjection.plugin,
101
105
  // Adjacent text tabs still need separate DOM ranges for layout and pointer selection.
@@ -34,7 +34,7 @@ function commentsIn(doc) {
34
34
  const id = stringAttr(node.attrs.id) ?? "";
35
35
  const start = markers.starts.get(id);
36
36
  const end = markers.ends.get(id);
37
- const hasRange = start !== void 0 && end !== void 0 && start < end;
37
+ const anchored = start !== void 0 && end !== void 0 && start < end;
38
38
  const point = start ?? end ?? pos;
39
39
  return {
40
40
  id,
@@ -43,9 +43,10 @@ function commentsIn(doc) {
43
43
  initials: stringAttr(node.attrs.initials),
44
44
  date: stringAttr(node.attrs.date),
45
45
  text: bodyText(doc, id),
46
- from: hasRange ? start + 1 : point,
47
- to: hasRange ? end : point,
46
+ from: anchored ? start + 1 : point,
47
+ to: anchored ? end : point,
48
48
  referencePos: pos,
49
+ anchored,
49
50
  resolved: node.attrs.resolved === true,
50
51
  replies: repliesAttr(node.attrs.replies).map((reply) => ({
51
52
  id: reply.id,
@@ -69,7 +70,7 @@ function deriveComments(doc) {
69
70
  byId,
70
71
  decorations: DecorationSet.create(
71
72
  doc,
72
- comments.filter((comment) => comment.from < comment.to).map(
73
+ comments.filter((comment) => !comment.resolved && comment.from < comment.to).map(
73
74
  (comment) => Decoration.inline(comment.from, comment.to, {
74
75
  class: editorClassNames.commentRange,
75
76
  "data-comment-id": comment.id
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Keeps a comment when the text it was written for goes away.
3
+ *
4
+ * Deleting commented text is an edit of the body, not a decision about the thread, so a body edit
5
+ * that takes a comment's nodes away has them put back at the spots the edit left: the thread stays
6
+ * anchored where one range marker survived, detached where both went (`DocumentComment.anchored`).
7
+ *
8
+ * The comment's story (`docx/story`) tells such an edit from `removeComment`: a body deletion does
9
+ * not reach the story, so a comment is put back while its story stands, which only `removeComment`
10
+ * takes away. The cost is that a reference the comments part held no body for is never put back.
11
+ *
12
+ * A restoration is an ordinary transaction the guards judge (`editor/plugins/lockedContent`): the
13
+ * nodes take the nearest spot left open, and a document leaving none loses the comment. A range
14
+ * marker with no home takes its surviving counterpart down, so no export carries half a range.
15
+ *
16
+ * A restoration needs no deferral around an open IME composition: the nodes go in beside the
17
+ * composed text rather than rewriting it (`e2e/hangulComposition.spec.ts`).
18
+ */
19
+ import { Plugin } from "prosemirror-state";
20
+ export declare function commentRestoration(): Plugin;
@@ -0,0 +1,168 @@
1
+ // src/editor/plugins/commentRestoration.ts
2
+ import {
3
+ Plugin,
4
+ TextSelection
5
+ } from "prosemirror-state";
6
+ import { storyKey, storyOf } from "../../docx/story.js";
7
+ import { docxSchema } from "../../schema/index.js";
8
+ import { editShut, transactionTouchesComments } from "../../schema/guards.js";
9
+ import { stringAttr } from "../commands/comments/model.js";
10
+ var NODE_FIELD = {
11
+ commentStart: "start",
12
+ commentEnd: "end",
13
+ commentReference: "reference"
14
+ };
15
+ var NODE_FIELDS = ["start", "end", "reference"];
16
+ var MAPPING_SIDE = {
17
+ start: 1,
18
+ end: -1,
19
+ reference: -1
20
+ };
21
+ var NO_NODES = { start: null, end: null, reference: null };
22
+ function commentNodesIn(doc) {
23
+ const found = /* @__PURE__ */ new Map();
24
+ doc.descendants((node, pos) => {
25
+ const field = NODE_FIELD[node.type.name];
26
+ if (field === void 0) return true;
27
+ const id = stringAttr(node.attrs.id);
28
+ if (id === null) return true;
29
+ const nodes = found.get(id) ?? { ...NO_NODES };
30
+ found.set(id, nodes);
31
+ if (nodes[field] === null) nodes[field] = { node, pos };
32
+ return true;
33
+ });
34
+ return found;
35
+ }
36
+ function inlineRanges(doc) {
37
+ const type = docxSchema.nodes.commentReference;
38
+ const ranges = [];
39
+ doc.descendants((node, pos) => {
40
+ if (!node.isTextblock) return true;
41
+ if (node.type.contentMatch.matchType(type) !== null) {
42
+ ranges.push({ from: pos + 1, to: pos + 1 + node.content.size });
43
+ }
44
+ return false;
45
+ });
46
+ return ranges;
47
+ }
48
+ function within(ranges, pos) {
49
+ return ranges.some((range) => pos >= range.from && pos <= range.to);
50
+ }
51
+ function* homes(ranges, wanted, caret) {
52
+ if (within(ranges, wanted)) yield wanted;
53
+ if (caret !== wanted && within(ranges, caret)) yield caret;
54
+ for (let index = ranges.length - 1; index >= 0; index -= 1) {
55
+ const range = ranges[index];
56
+ if (range !== void 0 && range.to < wanted) yield range.to;
57
+ }
58
+ for (const range of ranges) {
59
+ if (range.from > wanted) yield range.from;
60
+ }
61
+ }
62
+ function allowedHome(state, ranges, wanted, caret) {
63
+ for (const at of homes(ranges, wanted, caret)) {
64
+ if (!editShut(state, { kind: "insert", at })) return at;
65
+ }
66
+ return null;
67
+ }
68
+ function mappedThrough(transactions, pos, side) {
69
+ return transactions.reduce((at, tr) => tr.mapping.map(at, side), pos);
70
+ }
71
+ function restorable(node) {
72
+ return node.type.create(
73
+ node.attrs,
74
+ null,
75
+ node.marks.filter((mark) => mark.type === docxSchema.marks.run)
76
+ );
77
+ }
78
+ function planRestoration(transactions, oldState, newState) {
79
+ const before = commentNodesIn(oldState.doc);
80
+ const after = commentNodesIn(newState.doc);
81
+ const ranges = inlineRanges(newState.doc);
82
+ const caret = newState.selection.from;
83
+ const plan = { insertions: [], removals: [] };
84
+ for (const [id, was] of before) {
85
+ if (storyOf(newState.doc, storyKey("comment", id)) === null) continue;
86
+ const now = after.get(id) ?? NO_NODES;
87
+ for (const field of NODE_FIELDS) {
88
+ const lost = was[field];
89
+ if (lost === null || now[field] !== null) continue;
90
+ const counterpart = field === "start" ? now.end : field === "end" ? now.start : null;
91
+ if (field !== "reference" && counterpart === null) continue;
92
+ const at = allowedHome(
93
+ newState,
94
+ ranges,
95
+ mappedThrough(transactions, lost.pos, MAPPING_SIDE[field]),
96
+ caret
97
+ );
98
+ if (at !== null) {
99
+ plan.insertions.push({
100
+ node: restorable(lost.node),
101
+ at,
102
+ was: lost.pos
103
+ });
104
+ } else if (counterpart !== null && !markerShut(newState, counterpart)) {
105
+ plan.removals.push(counterpart);
106
+ }
107
+ }
108
+ }
109
+ return plan;
110
+ }
111
+ function markerShut(state, marker) {
112
+ return editShut(state, {
113
+ kind: "replace",
114
+ from: marker.pos,
115
+ to: marker.pos + marker.node.nodeSize
116
+ });
117
+ }
118
+ function pastCommentNodes(doc, pos, direction) {
119
+ let at = pos;
120
+ for (; ; ) {
121
+ const resolved = doc.resolve(at);
122
+ const next = direction < 0 ? resolved.nodeBefore : resolved.nodeAfter;
123
+ if (next === null || NODE_FIELD[next.type.name] === void 0) return at;
124
+ at += direction * next.nodeSize;
125
+ }
126
+ }
127
+ function withCaretPastMarkers(tr, oldState, newState) {
128
+ const { selection } = newState;
129
+ if (!(selection instanceof TextSelection) || !selection.empty) return tr;
130
+ if (!tr.doc.eq(oldState.doc)) return tr;
131
+ const direction = selection.from < oldState.selection.from ? -1 : 1;
132
+ return tr.setSelection(
133
+ TextSelection.create(
134
+ tr.doc,
135
+ pastCommentNodes(tr.doc, tr.selection.from, direction)
136
+ )
137
+ );
138
+ }
139
+ function restoration(transactions, oldState, newState) {
140
+ const plan = planRestoration(transactions, oldState, newState);
141
+ if (plan.insertions.length === 0 && plan.removals.length === 0) return null;
142
+ const tr = newState.tr;
143
+ for (const marker of [...plan.removals].sort(
144
+ (left, right) => right.pos - left.pos
145
+ )) {
146
+ tr.delete(marker.pos, marker.pos + marker.node.nodeSize);
147
+ }
148
+ const ordered = [...plan.insertions].sort(
149
+ (left, right) => left.at - right.at || left.was - right.was
150
+ );
151
+ for (const { node, at } of ordered) {
152
+ tr.insert(tr.mapping.map(at), node);
153
+ }
154
+ if (!tr.docChanged) return null;
155
+ return withCaretPastMarkers(tr, oldState, newState);
156
+ }
157
+ function commentRestoration() {
158
+ return new Plugin({
159
+ appendTransaction(transactions, oldState, newState) {
160
+ if (!transactions.some((tr) => tr.docChanged)) return null;
161
+ if (!transactions.some(transactionTouchesComments)) return null;
162
+ return restoration(transactions, oldState, newState);
163
+ }
164
+ });
165
+ }
166
+ export {
167
+ commentRestoration
168
+ };
@@ -140,6 +140,8 @@ export declare const editorClassNames: {
140
140
  readonly commentMeta: "docx-editor-comment-meta";
141
141
  readonly commentAuthor: "docx-editor-comment-author";
142
142
  readonly commentDate: "docx-editor-comment-date";
143
+ /** The note on a card whose comment lost the text it was written for */
144
+ readonly commentDetached: "docx-editor-comment-detached";
143
145
  readonly commentBody: "docx-editor-comment-body";
144
146
  readonly commentActions: "docx-editor-comment-actions";
145
147
  readonly commentPrimaryAction: "docx-editor-comment-primary-action";
@@ -136,6 +136,8 @@ var editorClassNames = {
136
136
  commentMeta: `${PREFIX}-comment-meta`,
137
137
  commentAuthor: `${PREFIX}-comment-author`,
138
138
  commentDate: `${PREFIX}-comment-date`,
139
+ /** The note on a card whose comment lost the text it was written for */
140
+ commentDetached: `${PREFIX}-comment-detached`,
139
141
  commentBody: `${PREFIX}-comment-body`,
140
142
  commentActions: `${PREFIX}-comment-actions`,
141
143
  commentPrimaryAction: `${PREFIX}-comment-primary-action`,
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 {
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portone/docx-editor",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A simple DOCX editor for React, built directly on OOXML.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -107,6 +107,7 @@
107
107
  "build:demo": "vite build --config demo/vite.config.ts",
108
108
  "dev:site": "pnpm --filter @portone/docx-editor-site dev",
109
109
  "build:site": "pnpm --filter @portone/docx-editor-site build",
110
+ "bench": "vitest run --config vitest.bench.config.ts --disableConsoleIntercept",
110
111
  "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm test:fixtures && pnpm test:site-release && pnpm test:release && pnpm check:demo-library",
111
112
  "typecheck": "tsc --noEmit && tsc --noEmit -p e2e && tsc --noEmit -p demo && pnpm --filter @portone/docx-editor-site typecheck",
112
113
  "lint": "pnpm biome check . --diagnostic-level=error",