@agent-native/core-corpus 0.210.2 → 0.211.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.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  IconTrash,
12
12
  } from "@tabler/icons-react";
13
13
  import { Fragment, type Node as ProseMirrorNode } from "@tiptap/pm/model";
14
+ import { TextSelection } from "@tiptap/pm/state";
14
15
  import { findTable } from "@tiptap/pm/tables";
15
16
  import { Editor } from "@tiptap/react";
16
17
  import {
@@ -35,6 +36,8 @@ import {
35
36
  } from "@/components/ui/tooltip";
36
37
  import { cn } from "@/lib/utils";
37
38
 
39
+ import { LOCAL_FILE_USER_EDIT_META } from "./extensions/LocalMdxComponentNode";
40
+
38
41
  interface TableHoverControlsProps {
39
42
  editor: Editor;
40
43
  }
@@ -107,13 +110,19 @@ const MENU_COLLISION_PADDING = 8;
107
110
 
108
111
  const menuIconClass = "size-4 shrink-0 text-muted-foreground";
109
112
 
113
+ // A reference preview inside a cell brings its own tables, so these keep only
114
+ // the rows and cells that belong to the given table or row.
110
115
  function getRows(table: HTMLElement) {
111
- return Array.from(table.querySelectorAll("tr")) as HTMLTableRowElement[];
116
+ return (
117
+ Array.from(table.querySelectorAll("tr")) as HTMLTableRowElement[]
118
+ ).filter((row) => row.closest("table") === table);
112
119
  }
113
120
 
114
121
  function getCells(row: HTMLTableRowElement | null | undefined) {
115
122
  return row
116
- ? (Array.from(row.querySelectorAll("td, th")) as HTMLElement[])
123
+ ? (Array.from(row.querySelectorAll("td, th")) as HTMLElement[]).filter(
124
+ (cell) => cell.closest("tr") === row,
125
+ )
117
126
  : [];
118
127
  }
119
128
 
@@ -137,14 +146,24 @@ function getLastRowCell(table: HTMLElement) {
137
146
  return cells[0] ?? null;
138
147
  }
139
148
 
149
+ // A reference preview renders its own read-only editor, tables included,
150
+ // inside this one. Those tables aren't in this editor's document.
151
+ function isInEditor(root: HTMLElement, element: Element) {
152
+ return element.closest(".ProseMirror") === root;
153
+ }
154
+
155
+ export function getEditorTables(root: HTMLElement) {
156
+ return (Array.from(root.querySelectorAll("table")) as HTMLElement[]).filter(
157
+ (table) => isInEditor(root, table),
158
+ );
159
+ }
160
+
140
161
  function getCellAddress(editor: Editor, cell: HTMLElement) {
141
162
  const table = cell.closest("table") as HTMLElement | null;
142
163
  const row = cell.closest("tr") as HTMLTableRowElement | null;
143
164
  if (!table || !row) return null;
144
165
 
145
- const tables = Array.from(
146
- editor.view.dom.querySelectorAll("table"),
147
- ) as HTMLElement[];
166
+ const tables = getEditorTables(editor.view.dom);
148
167
  const tableIndex = tables.indexOf(table);
149
168
  const rowIndex = getRows(table).indexOf(row);
150
169
  const columnIndex = getCells(row).indexOf(cell);
@@ -154,7 +173,7 @@ function getCellAddress(editor: Editor, cell: HTMLElement) {
154
173
  return { columnIndex, rowIndex, tableIndex };
155
174
  }
156
175
 
157
- function getCellAtAddress(
176
+ export function getCellAtAddress(
158
177
  tables: HTMLElement[],
159
178
  address: SelectedCellAddress | null,
160
179
  ) {
@@ -236,6 +255,26 @@ function getChildren(node: ProseMirrorNode) {
236
255
  return children;
237
256
  }
238
257
 
258
+ // Where a cell starts in a table that starts at tablePos.
259
+ function getCellPos(
260
+ table: ProseMirrorNode,
261
+ tablePos: number,
262
+ address: SelectedCellAddress,
263
+ ) {
264
+ const row = table.maybeChild(address.rowIndex);
265
+ if (!row || address.columnIndex >= row.childCount) return null;
266
+
267
+ let pos = tablePos + 1;
268
+ for (let index = 0; index < address.rowIndex; index += 1) {
269
+ pos += table.child(index).nodeSize;
270
+ }
271
+ pos += 1;
272
+ for (let index = 0; index < address.columnIndex; index += 1) {
273
+ pos += row.child(index).nodeSize;
274
+ }
275
+ return pos;
276
+ }
277
+
239
278
  function findTableNodeByIndex(
240
279
  doc: ProseMirrorNode,
241
280
  tableIndex: number,
@@ -297,6 +336,18 @@ function hasHeaderColumnAtAddress(
297
336
  );
298
337
  }
299
338
 
339
+ // The menus and edge controls act while they, not the editor, hold focus, and
340
+ // Content saves an unfocused change only when it is marked as the user's.
341
+ function userEditChain(editor: Editor) {
342
+ return editor
343
+ .chain()
344
+ .focus()
345
+ .command(({ tr }) => {
346
+ tr.setMeta(LOCAL_FILE_USER_EDIT_META, true);
347
+ return true;
348
+ });
349
+ }
350
+
300
351
  function getTargetElement(target: EventTarget | null) {
301
352
  if (target instanceof Element) return target;
302
353
  if (target instanceof Node) return target.parentElement;
@@ -325,7 +376,7 @@ function getHoveredTableEdges(editor: Editor, target: EventTarget | null) {
325
376
  }
326
377
 
327
378
  const cell = targetElement.closest("td, th") as HTMLElement | null;
328
- if (!cell || !view.dom.contains(cell)) {
379
+ if (!cell || !isInEditor(view.dom, cell)) {
329
380
  return { columnTable: null, rowTable: null };
330
381
  }
331
382
 
@@ -337,7 +388,11 @@ function getHoveredTableEdges(editor: Editor, target: EventTarget | null) {
337
388
 
338
389
  const rows = getRows(table);
339
390
  const rowCells = getCells(row);
340
- const isLastColumn = rowCells[rowCells.length - 1] === cell;
391
+ // A table wider than its scrollport keeps its add-column control at the
392
+ // scrollport's edge, beside the column the edge cuts through.
393
+ const isLastColumn =
394
+ rowCells[rowCells.length - 1] === cell ||
395
+ cell.getBoundingClientRect().right > getTableScrollport(table).right + 1;
341
396
  const isLastRow = rows[rows.length - 1] === row;
342
397
 
343
398
  return {
@@ -394,8 +449,11 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
394
449
  });
395
450
  const pointerResizeRef = useRef<PointerResizeState | null>(null);
396
451
  const tablesRef = useRef<HTMLElement[]>([]);
452
+ const hoveredCellRef = useRef<HTMLElement | null>(null);
453
+ const menuDismissedOutsideRef = useRef(false);
397
454
  const [tables, setTables] = useState<HTMLElement[]>([]);
398
455
  const [selectedCell, setSelectedCell] = useState<HTMLElement | null>(null);
456
+ const [hoveredCell, setHoveredCell] = useState<HTMLElement | null>(null);
399
457
  const [hoveredEdges, setHoveredEdges] = useState<HoveredTableEdges>({
400
458
  columnTable: null,
401
459
  rowTable: null,
@@ -418,6 +476,20 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
418
476
  setRowMenuOpen(false);
419
477
  };
420
478
 
479
+ // Radix returns focus to the menu's trigger, which strands the keyboard
480
+ // outside the editor: Ctrl+Z there runs the browser's own undo on cell text.
481
+ // The menus aren't modal, so a click elsewhere both closes them and lands;
482
+ // refocusing the editor after it would put back the cell the menu acted on.
483
+ const returnFocusToEditor = (event: Event) => {
484
+ event.preventDefault();
485
+ const dismissedOutside = menuDismissedOutsideRef.current;
486
+ menuDismissedOutsideRef.current = false;
487
+ if (!dismissedOutside) getMountedEditorView(editor)?.focus();
488
+ };
489
+ const noteMenuDismissedOutside = () => {
490
+ menuDismissedOutsideRef.current = true;
491
+ };
492
+
421
493
  const clearSelectedCell = () => {
422
494
  closeTableMenus();
423
495
  selectedCellRef.current = null;
@@ -442,9 +514,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
442
514
  return;
443
515
  }
444
516
 
445
- const liveTables = Array.from(
446
- view.dom.querySelectorAll("table"),
447
- ) as HTMLElement[];
517
+ const liveTables = getEditorTables(view.dom);
448
518
  const nextSelectedCell =
449
519
  (selectedCellRef.current?.isConnected
450
520
  ? selectedCellRef.current
@@ -510,7 +580,22 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
510
580
  setHoveredEdges(nextHoveredEdges);
511
581
  };
512
582
 
513
- const selectCellForCommand = (cell: HTMLElement | null) => {
583
+ // The row and column options follow the hovered cell, so the pointer keeps
584
+ // it while crossing onto them or into their menu.
585
+ const updateHoveredCell = (target: EventTarget | null) => {
586
+ const element = getTargetElement(target);
587
+ if (element?.closest(".content-table-axis-trigger, [role='menu']")) return;
588
+
589
+ const cell = element?.closest("td, th") as HTMLElement | null;
590
+ const root = getMountedEditorView(editor)?.dom;
591
+ const nextCell = cell && root && isInEditor(root, cell) ? cell : null;
592
+ if (hoveredCellRef.current === nextCell) return;
593
+
594
+ hoveredCellRef.current = nextCell;
595
+ setHoveredCell(nextCell);
596
+ };
597
+
598
+ const selectCellForCommand = (cell: HTMLElement | null, focus = true) => {
514
599
  if (!cell || editor.isDestroyed) return false;
515
600
 
516
601
  const view = getMountedEditorView(editor);
@@ -519,9 +604,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
519
604
  const pos = view.posAtDOM(cell, 0);
520
605
  if (pos < 0) return false;
521
606
 
522
- editor
523
- .chain()
524
- .focus()
607
+ (focus ? editor.chain().focus() : editor.chain())
525
608
  .setTextSelection(Math.min(pos + 1, editor.state.doc.content.size))
526
609
  .run();
527
610
 
@@ -531,6 +614,16 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
531
614
  return true;
532
615
  };
533
616
 
617
+ // Without focusing the editor: an unfocused editor takes focus a frame later,
618
+ // after the menu opened, and the menu closes when focus leaves it. The menu's
619
+ // commands focus the editor themselves.
620
+ const selectHoveredCell = () => {
621
+ const cell = hoveredCellRef.current;
622
+ if (cell?.isConnected && cell !== selectedCellRef.current) {
623
+ selectCellForCommand(cell, false);
624
+ }
625
+ };
626
+
534
627
  const runAtCell = (cell: HTMLElement | null, action: () => void) => {
535
628
  if (!selectCellForCommand(cell)) return;
536
629
  action();
@@ -542,13 +635,18 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
542
635
 
543
636
  const addColumnAtEnd = (table: HTMLElement) => {
544
637
  runAtCell(getLastColumnCell(table), () => {
545
- editor.chain().focus().addColumnAfter().run();
638
+ userEditChain(editor).addColumnAfter().run();
639
+ });
640
+ requestAnimationFrame(() => {
641
+ const scrollport =
642
+ getCellFromEditorSelection(editor)?.closest(".tableWrapper");
643
+ scrollport?.scrollTo({ left: scrollport.scrollWidth });
546
644
  });
547
645
  };
548
646
 
549
647
  const addRowAtEnd = (table: HTMLElement) => {
550
648
  runAtCell(getLastRowCell(table), () => {
551
- editor.chain().focus().addRowAfter().run();
649
+ userEditChain(editor).addRowAfter().run();
552
650
  });
553
651
  };
554
652
 
@@ -572,11 +670,14 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
572
670
  return { address, table };
573
671
  };
574
672
 
673
+ // The header switches keep their menu open, and the menu isn't modal, so
674
+ // focusing the editor would close it. Closing it focuses the editor instead.
575
675
  const replaceTable = (
576
676
  tablePos: number,
577
677
  oldTable: ProseMirrorNode,
578
678
  nextTable: ProseMirrorNode,
579
679
  nextAddress: SelectedCellAddress,
680
+ { keepMenuFocus = false } = {},
580
681
  ) => {
581
682
  const transaction = editor.state.tr.replaceWith(
582
683
  tablePos,
@@ -584,12 +685,22 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
584
685
  nextTable,
585
686
  );
586
687
 
688
+ // Replacing the table moves the selection out of it, and the controls
689
+ // follow the selected cell: they would unmount, menu and all, until the
690
+ // next frame selects the cell again.
691
+ const cellPos = getCellPos(nextTable, tablePos, nextAddress);
692
+ if (cellPos !== null) {
693
+ transaction.setSelection(
694
+ TextSelection.near(transaction.doc.resolve(cellPos + 1)),
695
+ );
696
+ }
697
+ transaction.setMeta(LOCAL_FILE_USER_EDIT_META, true);
587
698
  editor.view.dispatch(transaction.scrollIntoView());
588
- editor.view.focus();
699
+ if (!keepMenuFocus) editor.view.focus();
589
700
 
590
701
  requestAnimationFrame(() => {
591
702
  const nextCell = getCellAtAddress(
592
- Array.from(editor.view.dom.querySelectorAll("table")) as HTMLElement[],
703
+ getEditorTables(editor.view.dom),
593
704
  nextAddress,
594
705
  );
595
706
 
@@ -598,7 +709,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
598
709
  ? getCellAddress(editor, nextCell)
599
710
  : nextAddress;
600
711
  if (nextCell) {
601
- selectCellForCommand(nextCell);
712
+ selectCellForCommand(nextCell, !keepMenuFocus);
602
713
  } else {
603
714
  syncOverlay();
604
715
  }
@@ -735,6 +846,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
735
846
  table.node,
736
847
  table.node.copy(Fragment.fromArray(nextRows)),
737
848
  address,
849
+ { keepMenuFocus: true },
738
850
  );
739
851
  };
740
852
 
@@ -776,6 +888,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
776
888
  table.node,
777
889
  table.node.copy(Fragment.fromArray(nextRows)),
778
890
  address,
891
+ { keepMenuFocus: true },
779
892
  );
780
893
  };
781
894
 
@@ -785,13 +898,13 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
785
898
 
786
899
  while (columnCount < nextCount) {
787
900
  if (!selectCellForCommand(getLastColumnCell(table))) break;
788
- editor.chain().focus().addColumnAfter().run();
901
+ userEditChain(editor).addColumnAfter().run();
789
902
  columnCount += 1;
790
903
  }
791
904
 
792
905
  while (columnCount > nextCount) {
793
906
  if (!selectCellForCommand(getLastColumnCell(table))) break;
794
- editor.chain().focus().deleteColumn().run();
907
+ userEditChain(editor).deleteColumn().run();
795
908
  columnCount -= 1;
796
909
  }
797
910
 
@@ -804,13 +917,13 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
804
917
 
805
918
  while (rowCount < nextCount) {
806
919
  if (!selectCellForCommand(getLastRowCell(table))) break;
807
- editor.chain().focus().addRowAfter().run();
920
+ userEditChain(editor).addRowAfter().run();
808
921
  rowCount += 1;
809
922
  }
810
923
 
811
924
  while (rowCount > nextCount) {
812
925
  if (!selectCellForCommand(getLastRowCell(table))) break;
813
- editor.chain().focus().deleteRow().run();
926
+ userEditChain(editor).deleteRow().run();
814
927
  rowCount -= 1;
815
928
  }
816
929
 
@@ -835,20 +948,22 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
835
948
  : "content-table-resizing-row",
836
949
  );
837
950
 
951
+ // Count from where the drag started, not from the table's start: the
952
+ // column control can sit at a scrollport edge well short of the table's end.
838
953
  if (current.kind === "column") {
839
954
  const averageWidth = current.tableRect.width / current.baseColumnCount;
840
- const target = Math.round(
841
- (event.clientX - current.tableRect.left) / averageWidth,
842
- );
955
+ const target =
956
+ current.baseColumnCount +
957
+ Math.round((event.clientX - current.startX) / averageWidth);
843
958
  if (target !== current.lastTarget) {
844
959
  current.lastTarget = target;
845
960
  adjustColumnCount(current.table, target);
846
961
  }
847
962
  } else {
848
963
  const averageHeight = current.tableRect.height / current.baseRowCount;
849
- const target = Math.round(
850
- (event.clientY - current.tableRect.top) / averageHeight,
851
- );
964
+ const target =
965
+ current.baseRowCount +
966
+ Math.round((event.clientY - current.startY) / averageHeight);
852
967
  if (target !== current.lastTarget) {
853
968
  current.lastTarget = target;
854
969
  adjustRowCount(current.table, target);
@@ -856,7 +971,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
856
971
  }
857
972
  };
858
973
 
859
- const handlePointerUp = () => {
974
+ const endPointerResize = () => {
860
975
  const current = pointerResizeRef.current;
861
976
  pointerResizeRef.current = null;
862
977
  document.documentElement.classList.remove(
@@ -865,7 +980,19 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
865
980
  );
866
981
  window.removeEventListener("pointermove", handlePointerMove);
867
982
  window.removeEventListener("pointerup", handlePointerUp);
983
+ window.removeEventListener("pointercancel", handlePointerCancel);
984
+ return current;
985
+ };
868
986
 
987
+ // The browser cancels a touch it takes over, e.g. to scroll. That ends the
988
+ // drag without the click's added row or column.
989
+ const handlePointerCancel = () => {
990
+ endPointerResize();
991
+ syncOverlay();
992
+ };
993
+
994
+ const handlePointerUp = () => {
995
+ const current = endPointerResize();
869
996
  if (!current) return;
870
997
 
871
998
  if (!current.didResize) {
@@ -902,6 +1029,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
902
1029
 
903
1030
  window.addEventListener("pointermove", handlePointerMove);
904
1031
  window.addEventListener("pointerup", handlePointerUp);
1032
+ window.addEventListener("pointercancel", handlePointerCancel);
905
1033
  };
906
1034
 
907
1035
  useEffect(() => {
@@ -925,6 +1053,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
925
1053
  if (columnAxisTrigger) {
926
1054
  event.preventDefault();
927
1055
  event.stopPropagation();
1056
+ selectHoveredCell();
928
1057
  holdTableUiSelection();
929
1058
  setColumnMenuOpen(true);
930
1059
  return;
@@ -936,6 +1065,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
936
1065
  if (rowAxisTrigger) {
937
1066
  event.preventDefault();
938
1067
  event.stopPropagation();
1068
+ selectHoveredCell();
939
1069
  holdTableUiSelection();
940
1070
  setRowMenuOpen(true);
941
1071
  return;
@@ -943,7 +1073,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
943
1073
 
944
1074
  const cell = target.closest("td, th") as HTMLElement | null;
945
1075
 
946
- if (cell && view.dom.contains(cell)) {
1076
+ if (cell && isInEditor(view.dom, cell)) {
947
1077
  closeTableMenus();
948
1078
  selectedCellRef.current = cell;
949
1079
  selectedCellAddressRef.current = getCellAddress(editor, cell);
@@ -963,14 +1093,31 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
963
1093
  syncOverlay();
964
1094
  }
965
1095
  };
1096
+ // The pointer keeps an edge handle shown while it's on the handle.
1097
+ const updateEdgesFromTarget = (target: EventTarget | null) => {
1098
+ if (getTargetElement(target)?.closest("[data-table-edge-control]")) {
1099
+ return;
1100
+ }
1101
+ updateHoveredEdges(getHoveredTableEdges(editor, target));
1102
+ };
966
1103
  const handleHoverPointerMove = (event: PointerEvent) => {
967
1104
  if (pointerResizeRef.current) return;
968
- updateHoveredEdges(getHoveredTableEdges(editor, event.target));
1105
+ // Touch has no hover. A finger that moves during a tap would show a
1106
+ // handle under it before the tap lands, so taps use handleTapEdges.
1107
+ if (event.pointerType !== "touch") updateEdgesFromTarget(event.target);
1108
+ updateHoveredCell(event.target);
1109
+ };
1110
+ // Once a tap has placed the caret, the cell it landed on shows its edge
1111
+ // handles, so a second tap can use them.
1112
+ const handleTapEdges = (event: MouseEvent) => {
1113
+ if (pointerResizeRef.current) return;
1114
+ updateEdgesFromTarget(event.target);
969
1115
  };
970
1116
 
971
1117
  const handleKeyUp = () => {
972
1118
  const cell = getCellFromEditorSelection(editor);
973
- selectedCellRef.current = cell && view.dom.contains(cell) ? cell : null;
1119
+ selectedCellRef.current =
1120
+ cell && isInEditor(view.dom, cell) ? cell : null;
974
1121
  selectedCellAddressRef.current = selectedCellRef.current
975
1122
  ? getCellAddress(editor, selectedCellRef.current)
976
1123
  : null;
@@ -984,7 +1131,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
984
1131
  }
985
1132
 
986
1133
  const cell = getCellFromEditorSelection(editor);
987
- if (cell && view.dom.contains(cell)) {
1134
+ if (cell && isInEditor(view.dom, cell)) {
988
1135
  selectedCellRef.current = cell;
989
1136
  selectedCellAddressRef.current = getCellAddress(editor, cell);
990
1137
  } else if (!selectedCellRef.current?.isConnected) {
@@ -1016,6 +1163,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1016
1163
  document.addEventListener("mousedown", handlePointerDown, true);
1017
1164
  document.addEventListener("click", handlePointerDown, true);
1018
1165
  document.addEventListener("pointermove", handleHoverPointerMove, true);
1166
+ document.addEventListener("click", handleTapEdges, true);
1019
1167
  window.addEventListener("resize", handleGeometryChange);
1020
1168
  window.addEventListener("scroll", handleScroll, true);
1021
1169
  view.dom.addEventListener("keyup", handleKeyUp);
@@ -1026,10 +1174,12 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1026
1174
  document.removeEventListener("mousedown", handlePointerDown, true);
1027
1175
  document.removeEventListener("click", handlePointerDown, true);
1028
1176
  document.removeEventListener("pointermove", handleHoverPointerMove, true);
1177
+ document.removeEventListener("click", handleTapEdges, true);
1029
1178
  window.removeEventListener("resize", handleGeometryChange);
1030
1179
  window.removeEventListener("scroll", handleScroll, true);
1031
1180
  window.removeEventListener("pointermove", handlePointerMove);
1032
1181
  window.removeEventListener("pointerup", handlePointerUp);
1182
+ window.removeEventListener("pointercancel", handlePointerCancel);
1033
1183
  view.dom.removeEventListener("keyup", handleKeyUp);
1034
1184
  editor.off("selectionUpdate", handleSelectionUpdate);
1035
1185
  };
@@ -1118,6 +1268,29 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1118
1268
  ROW_OPTIONS_MENU_HEIGHT >
1119
1269
  viewportHeight - MENU_COLLISION_PADDING
1120
1270
  : false;
1271
+ const hoveredAxisCell =
1272
+ !columnMenuOpen &&
1273
+ !rowMenuOpen &&
1274
+ hoveredCell?.isConnected &&
1275
+ hoveredCell !== selectedCell
1276
+ ? hoveredCell
1277
+ : null;
1278
+ const hoveredAxisTable = hoveredAxisCell?.closest(
1279
+ "table",
1280
+ ) as HTMLElement | null;
1281
+ const axisTableRect = hoveredAxisTable
1282
+ ? hoveredAxisTable.getBoundingClientRect()
1283
+ : selectedTableRect;
1284
+ const axisScrollport = hoveredAxisTable
1285
+ ? getTableScrollport(hoveredAxisTable)
1286
+ : selectedScrollport;
1287
+ const axisRect =
1288
+ hoveredAxisCell && hoveredAxisTable && axisScrollport
1289
+ ? clampToScrollport(
1290
+ hoveredAxisCell.getBoundingClientRect(),
1291
+ axisScrollport,
1292
+ )
1293
+ : visibleSelectedRect;
1121
1294
  const columnMenuAlign = columnMenuWouldOverflowRight ? "end" : "start";
1122
1295
  const columnMenuAlignOffset = columnMenuWouldOverflowRight
1123
1296
  ? -COLUMN_OPTIONS_MENU_ALIGN_OFFSET
@@ -1131,8 +1304,9 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1131
1304
  "content-table-axis-trigger group absolute z-40 flex items-center justify-center rounded-md bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
1132
1305
  const axisMarkerClass =
1133
1306
  "block rounded-full bg-muted-foreground/55 transition-colors group-hover:bg-primary group-focus-visible:bg-primary";
1307
+ // touch-none lets a finger drag a handle instead of scrolling the page.
1134
1308
  const handleClass =
1135
- "table-hover-controls group absolute z-40 flex items-center justify-center rounded-md bg-transparent opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
1309
+ "table-hover-controls group absolute z-40 flex touch-none items-center justify-center rounded-md bg-transparent opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
1136
1310
  const edgeHandleMarkerClass =
1137
1311
  "block rounded-full bg-muted-foreground/55 transition-colors group-hover:bg-primary group-focus-visible:bg-primary";
1138
1312
 
@@ -1181,15 +1355,24 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1181
1355
  }}
1182
1356
  />
1183
1357
  ) : null}
1358
+ </>
1359
+ ) : null}
1184
1360
 
1185
- <DropdownMenu open={columnMenuOpen} onOpenChange={setColumnMenuOpen}>
1361
+ {axisRect && axisScrollport && axisTableRect ? (
1362
+ <>
1363
+ <DropdownMenu
1364
+ open={columnMenuOpen}
1365
+ onOpenChange={(open) => {
1366
+ // Enter, Space and ArrowDown open the menu without a pointer.
1367
+ if (open) selectHoveredCell();
1368
+ setColumnMenuOpen(open);
1369
+ }}
1370
+ modal={false}
1371
+ >
1186
1372
  <DropdownMenuTrigger asChild>
1187
1373
  <button
1188
1374
  aria-label={t("database.columnOptions")}
1189
- className={cn(
1190
- lineButtonClass,
1191
- !visibleSelectedRect.width && "invisible",
1192
- )}
1375
+ className={cn(lineButtonClass, !axisRect.width && "invisible")}
1193
1376
  data-testid="table-column-options"
1194
1377
  onClickCapture={(event) => {
1195
1378
  event.preventDefault();
@@ -1207,12 +1390,9 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1207
1390
  setColumnMenuOpen(true);
1208
1391
  }}
1209
1392
  style={{
1210
- left:
1211
- visibleSelectedRect.left -
1212
- wrapperRect.left +
1213
- visibleSelectedRect.width / 2,
1393
+ left: axisRect.left - wrapperRect.left + axisRect.width / 2,
1214
1394
  top:
1215
- selectedTableRect.top -
1395
+ axisTableRect.top -
1216
1396
  wrapperRect.top -
1217
1397
  COLUMN_OPTIONS_HIT_HEIGHT / 2,
1218
1398
  height: COLUMN_OPTIONS_HIT_HEIGHT,
@@ -1236,6 +1416,8 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1236
1416
  align={columnMenuAlign}
1237
1417
  alignOffset={columnMenuAlignOffset}
1238
1418
  collisionPadding={MENU_COLLISION_PADDING}
1419
+ onCloseAutoFocus={returnFocusToEditor}
1420
+ onInteractOutside={noteMenuDismissedOutside}
1239
1421
  >
1240
1422
  <DropdownMenuLabel>{t("database.column")}</DropdownMenuLabel>
1241
1423
  {canToggleHeaderColumn ? (
@@ -1278,7 +1460,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1278
1460
  className="gap-2"
1279
1461
  onSelect={() =>
1280
1462
  runAtCell(getSelectedCellElement(), () => {
1281
- editor.chain().focus().addColumnBefore().run();
1463
+ userEditChain(editor).addColumnBefore().run();
1282
1464
  })
1283
1465
  }
1284
1466
  >
@@ -1289,7 +1471,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1289
1471
  className="gap-2"
1290
1472
  onSelect={() =>
1291
1473
  runAtCell(getSelectedCellElement(), () => {
1292
- editor.chain().focus().addColumnAfter().run();
1474
+ userEditChain(editor).addColumnAfter().run();
1293
1475
  })
1294
1476
  }
1295
1477
  >
@@ -1316,9 +1498,9 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1316
1498
  onSelect={() =>
1317
1499
  runAtCell(getSelectedCellElement(), () => {
1318
1500
  if (selectedDimensions.columnCount <= 1) {
1319
- editor.chain().focus().deleteTable().run();
1501
+ userEditChain(editor).deleteTable().run();
1320
1502
  } else {
1321
- editor.chain().focus().deleteColumn().run();
1503
+ userEditChain(editor).deleteColumn().run();
1322
1504
  }
1323
1505
  })
1324
1506
  }
@@ -1329,7 +1511,15 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1329
1511
  </DropdownMenuContent>
1330
1512
  </DropdownMenu>
1331
1513
 
1332
- <DropdownMenu open={rowMenuOpen} onOpenChange={setRowMenuOpen}>
1514
+ <DropdownMenu
1515
+ open={rowMenuOpen}
1516
+ onOpenChange={(open) => {
1517
+ // Enter, Space and ArrowDown open the menu without a pointer.
1518
+ if (open) selectHoveredCell();
1519
+ setRowMenuOpen(open);
1520
+ }}
1521
+ modal={false}
1522
+ >
1333
1523
  <DropdownMenuTrigger asChild>
1334
1524
  <button
1335
1525
  aria-label={t("database.rowOptions")}
@@ -1352,13 +1542,10 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1352
1542
  }}
1353
1543
  style={{
1354
1544
  left:
1355
- Math.max(selectedTableRect.left, selectedScrollport.left) -
1545
+ Math.max(axisTableRect.left, axisScrollport.left) -
1356
1546
  wrapperRect.left -
1357
1547
  ROW_OPTIONS_HIT_WIDTH / 2,
1358
- top:
1359
- visibleSelectedRect.top -
1360
- wrapperRect.top +
1361
- visibleSelectedRect.height / 2,
1548
+ top: axisRect.top - wrapperRect.top + axisRect.height / 2,
1362
1549
  height: ROW_OPTIONS_HIT_HEIGHT,
1363
1550
  transform: "translateY(-50%)",
1364
1551
  width: ROW_OPTIONS_HIT_WIDTH,
@@ -1377,6 +1564,8 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1377
1564
  align={rowMenuAlign}
1378
1565
  alignOffset={rowMenuAlignOffset}
1379
1566
  collisionPadding={MENU_COLLISION_PADDING}
1567
+ onCloseAutoFocus={returnFocusToEditor}
1568
+ onInteractOutside={noteMenuDismissedOutside}
1380
1569
  side="right"
1381
1570
  >
1382
1571
  <DropdownMenuLabel>{t("database.row")}</DropdownMenuLabel>
@@ -1420,7 +1609,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1420
1609
  className="gap-2"
1421
1610
  onSelect={() =>
1422
1611
  runAtCell(getSelectedCellElement(), () => {
1423
- editor.chain().focus().addRowBefore().run();
1612
+ userEditChain(editor).addRowBefore().run();
1424
1613
  })
1425
1614
  }
1426
1615
  >
@@ -1431,7 +1620,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1431
1620
  className="gap-2"
1432
1621
  onSelect={() =>
1433
1622
  runAtCell(getSelectedCellElement(), () => {
1434
- editor.chain().focus().addRowAfter().run();
1623
+ userEditChain(editor).addRowAfter().run();
1435
1624
  })
1436
1625
  }
1437
1626
  >
@@ -1458,9 +1647,9 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1458
1647
  onSelect={() =>
1459
1648
  runAtCell(getSelectedCellElement(), () => {
1460
1649
  if (selectedDimensions.rowCount <= 1) {
1461
- editor.chain().focus().deleteTable().run();
1650
+ userEditChain(editor).deleteTable().run();
1462
1651
  } else {
1463
- editor.chain().focus().deleteRow().run();
1652
+ userEditChain(editor).deleteRow().run();
1464
1653
  }
1465
1654
  })
1466
1655
  }
@@ -1481,59 +1670,59 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1481
1670
  const scrollportRect = getTableScrollport(table);
1482
1671
  const visibleLeft = Math.max(rect.left, scrollportRect.left);
1483
1672
  const visibleRight = Math.min(rect.right, scrollportRect.right);
1484
- const isTableEndVisible = rect.right <= visibleRight + 1;
1485
1673
  const top = rect.top - wrapperRect.top;
1486
- const right = rect.right - wrapperRect.left;
1674
+ const right = visibleRight - wrapperRect.left;
1487
1675
  const bottom = rect.bottom - wrapperRect.top;
1488
1676
  const visibleCenter =
1489
1677
  (visibleLeft + visibleRight) / 2 - wrapperRect.left;
1678
+ // A hidden handle lets clicks through to the cell under it.
1490
1679
  const isColumnHandleVisible = hoveredEdges.columnTable === table;
1491
1680
  const isRowHandleVisible = hoveredEdges.rowTable === table;
1492
1681
 
1493
1682
  return (
1494
1683
  <div key={tableId}>
1495
- {isTableEndVisible ? (
1496
- <Tooltip delayDuration={EDGE_HANDLE_TOOLTIP_DELAY}>
1497
- <TooltipTrigger asChild>
1498
- <button
1499
- aria-label={t("database.tableColumnHandle")}
1500
- className={cn(
1501
- handleClass,
1502
- "cursor-col-resize -translate-y-1/2",
1503
- isColumnHandleVisible && "opacity-100",
1504
- )}
1505
- data-table-edge-control
1506
- data-table-edge="column"
1507
- data-table-id={tableId}
1508
- data-testid="table-column-handle"
1509
- onPointerDown={(event) =>
1510
- startHandlePointer("column", table, event)
1511
- }
1684
+ <Tooltip delayDuration={EDGE_HANDLE_TOOLTIP_DELAY}>
1685
+ <TooltipTrigger asChild>
1686
+ <button
1687
+ aria-label={t("database.tableColumnHandle")}
1688
+ className={cn(
1689
+ handleClass,
1690
+ "cursor-col-resize -translate-y-1/2",
1691
+ isColumnHandleVisible
1692
+ ? "opacity-100"
1693
+ : "pointer-events-none",
1694
+ )}
1695
+ data-table-edge-control
1696
+ data-table-edge="column"
1697
+ data-table-id={tableId}
1698
+ data-testid="table-column-handle"
1699
+ onPointerDown={(event) =>
1700
+ startHandlePointer("column", table, event)
1701
+ }
1702
+ style={{
1703
+ height: COLUMN_EDGE_HANDLE_HIT_HEIGHT,
1704
+ left: right - COLUMN_EDGE_HANDLE_HIT_WIDTH / 2,
1705
+ top: top + rect.height / 2,
1706
+ width: COLUMN_EDGE_HANDLE_HIT_WIDTH,
1707
+ }}
1708
+ type="button"
1709
+ >
1710
+ <span
1711
+ className={edgeHandleMarkerClass}
1512
1712
  style={{
1513
- height: COLUMN_EDGE_HANDLE_HIT_HEIGHT,
1514
- left: right - COLUMN_EDGE_HANDLE_HIT_WIDTH / 2,
1515
- top: top + rect.height / 2,
1516
- width: COLUMN_EDGE_HANDLE_HIT_WIDTH,
1713
+ height: EDGE_HANDLE_LENGTH,
1714
+ width: EDGE_HANDLE_THICKNESS,
1517
1715
  }}
1518
- type="button"
1519
- >
1520
- <span
1521
- className={edgeHandleMarkerClass}
1522
- style={{
1523
- height: EDGE_HANDLE_LENGTH,
1524
- width: EDGE_HANDLE_THICKNESS,
1525
- }}
1526
- />
1527
- </button>
1528
- </TooltipTrigger>
1529
- <TooltipContent>
1530
- <strong>{t("database.click")}</strong>{" "}
1531
- {t("database.toAddAColumn")}{" "}
1532
- <strong>{t("database.drag")}</strong>{" "}
1533
- {t("database.toAddOrRemoveColumns")}
1534
- </TooltipContent>
1535
- </Tooltip>
1536
- ) : null}
1716
+ />
1717
+ </button>
1718
+ </TooltipTrigger>
1719
+ <TooltipContent>
1720
+ <strong>{t("database.click")}</strong>{" "}
1721
+ {t("database.toAddAColumn")}{" "}
1722
+ <strong>{t("database.drag")}</strong>{" "}
1723
+ {t("database.toAddOrRemoveColumns")}
1724
+ </TooltipContent>
1725
+ </Tooltip>
1537
1726
 
1538
1727
  <Tooltip delayDuration={EDGE_HANDLE_TOOLTIP_DELAY}>
1539
1728
  <TooltipTrigger asChild>
@@ -1542,7 +1731,7 @@ export function TableHoverControls({ editor }: TableHoverControlsProps) {
1542
1731
  className={cn(
1543
1732
  handleClass,
1544
1733
  "cursor-row-resize -translate-x-1/2",
1545
- isRowHandleVisible && "opacity-100",
1734
+ isRowHandleVisible ? "opacity-100" : "pointer-events-none",
1546
1735
  )}
1547
1736
  data-table-edge-control
1548
1737
  data-table-edge="row"