@agent-native/core 0.114.0 → 0.114.2

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 (223) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +23 -0
  5. package/corpus/core/src/cli/skills.ts +24 -0
  6. package/corpus/core/src/client/build-compatibility.ts +77 -0
  7. package/corpus/core/src/client/route-chunk-recovery/index.ts +1 -0
  8. package/corpus/core/src/client/use-action.ts +32 -1
  9. package/corpus/core/src/deploy/build.ts +4 -1
  10. package/corpus/core/src/org/context.ts +56 -30
  11. package/corpus/core/src/server/action-routes.ts +52 -2
  12. package/corpus/core/src/server/auth-plugin.ts +9 -4
  13. package/corpus/core/src/server/better-auth-instance.ts +5 -1
  14. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  15. package/corpus/core/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  16. package/corpus/core/src/templates/default/AGENTS.md +4 -1
  17. package/corpus/core/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  18. package/corpus/core/src/templates/headless/AGENTS.md +5 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +26 -0
  20. package/corpus/core/src/templates/workspace-core/AGENTS.md +3 -1
  21. package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -1
  22. package/corpus/core/src/vite/client.ts +17 -0
  23. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  24. package/corpus/templates/analytics/AGENTS.md +5 -0
  25. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  26. package/corpus/templates/assets/AGENTS.md +5 -0
  27. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  28. package/corpus/templates/brain/AGENTS.md +5 -0
  29. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  30. package/corpus/templates/calendar/AGENTS.md +5 -0
  31. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  32. package/corpus/templates/chat/AGENTS.md +4 -1
  33. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  34. package/corpus/templates/clips/AGENTS.md +5 -0
  35. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  36. package/corpus/templates/content/AGENTS.md +7 -0
  37. package/corpus/templates/content/actions/_content-database-personal-view.ts +49 -5
  38. package/corpus/templates/content/actions/_content-document-access.ts +21 -0
  39. package/corpus/templates/content/actions/_content-favorites.ts +91 -0
  40. package/corpus/templates/content/actions/_content-files.ts +79 -23
  41. package/corpus/templates/content/actions/_content-sidebar-state.ts +24 -0
  42. package/corpus/templates/content/actions/_content-space-access.ts +15 -17
  43. package/corpus/templates/content/actions/_content-space-catalog-guards.ts +20 -0
  44. package/corpus/templates/content/actions/_content-spaces.ts +364 -167
  45. package/corpus/templates/content/actions/_database-source-utils.ts +8 -0
  46. package/corpus/templates/content/actions/_database-utils.ts +249 -15
  47. package/corpus/templates/content/actions/_delete-content-space.ts +84 -0
  48. package/corpus/templates/content/actions/_files-system-properties.ts +499 -0
  49. package/corpus/templates/content/actions/_local-file-documents.ts +0 -1
  50. package/corpus/templates/content/actions/_property-utils.ts +18 -9
  51. package/corpus/templates/content/actions/bind-content-database-source-field.ts +3 -0
  52. package/corpus/templates/content/actions/configure-document-property.ts +3 -0
  53. package/corpus/templates/content/actions/create-content-database.ts +2 -2
  54. package/corpus/templates/content/actions/create-content-space.ts +34 -0
  55. package/corpus/templates/content/actions/create-document.ts +4 -1
  56. package/corpus/templates/content/actions/delete-content-space.ts +19 -0
  57. package/corpus/templates/content/actions/delete-database-items.ts +28 -1
  58. package/corpus/templates/content/actions/delete-document-property.ts +3 -0
  59. package/corpus/templates/content/actions/delete-document.ts +39 -2
  60. package/corpus/templates/content/actions/duplicate-database-item.ts +6 -0
  61. package/corpus/templates/content/actions/duplicate-database-items.ts +6 -0
  62. package/corpus/templates/content/actions/duplicate-document-property.ts +3 -0
  63. package/corpus/templates/content/actions/get-content-database.ts +11 -2
  64. package/corpus/templates/content/actions/get-content-sidebar-state.ts +23 -0
  65. package/corpus/templates/content/actions/get-document.ts +32 -7
  66. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  67. package/corpus/templates/content/actions/list-content-spaces.ts +34 -2
  68. package/corpus/templates/content/actions/list-documents.ts +39 -7
  69. package/corpus/templates/content/actions/reorder-document-property.ts +9 -0
  70. package/corpus/templates/content/actions/set-document-property.ts +41 -23
  71. package/corpus/templates/content/actions/share-local-file-document.ts +15 -0
  72. package/corpus/templates/content/actions/submit-content-database-form.ts +7 -0
  73. package/corpus/templates/content/actions/sync-local-folder-source.ts +14 -0
  74. package/corpus/templates/content/actions/update-content-database-view.ts +1 -0
  75. package/corpus/templates/content/actions/update-content-sidebar-state.ts +27 -0
  76. package/corpus/templates/content/actions/update-document.ts +158 -66
  77. package/corpus/templates/content/app/components/EmptyState.tsx +4 -57
  78. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +8 -1
  79. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +519 -178
  80. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +39 -0
  81. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  82. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +266 -7
  83. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +430 -152
  84. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +467 -66
  85. package/corpus/templates/content/app/components/editor/database/view-config.ts +9 -3
  86. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +720 -196
  87. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +173 -1
  88. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +1 -1
  89. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +91 -16
  90. package/corpus/templates/content/app/entry.client.tsx +5 -1
  91. package/corpus/templates/content/app/hooks/use-content-database.ts +84 -3
  92. package/corpus/templates/content/app/hooks/use-content-spaces.ts +60 -2
  93. package/corpus/templates/content/app/hooks/use-create-page.ts +30 -0
  94. package/corpus/templates/content/app/hooks/use-documents.ts +270 -19
  95. package/corpus/templates/content/app/i18n-data.ts +243 -20
  96. package/corpus/templates/content/app/lib/local-folder-picker-safety.ts +54 -0
  97. package/corpus/templates/content/app/lib/local-folder-picker-support.ts +18 -0
  98. package/corpus/templates/content/app/routes/_app.favorites.tsx +30 -0
  99. package/corpus/templates/content/app/routes/_app.local-files.tsx +18 -6
  100. package/corpus/templates/content/changelog/2026-07-17-workspace-files-now-stay-in-a-loading-state-until-automatic-.md +6 -0
  101. package/corpus/templates/content/changelog/2026-07-17-workspace-switching-now-opens-the-selected-files-database-pr.md +6 -0
  102. package/corpus/templates/content/changelog/2026-07-17-workspaces-now-expand-above-their-own-files-list-without-dup.md +6 -0
  103. package/corpus/templates/content/changelog/2026-07-18-create-named-workspaces-each-with-its-own-private-files-data.md +6 -0
  104. package/corpus/templates/content/changelog/2026-07-18-favorites-align-with-workspace-and-file-rows-in-the-sidebar.md +6 -0
  105. package/corpus/templates/content/changelog/2026-07-18-local-folder-selection-no-longer-invokes-the-unsafe-native-p.md +6 -0
  106. package/corpus/templates/content/changelog/2026-07-18-made-content-workspaces-independently-expandable-restored-or.md +6 -0
  107. package/corpus/templates/content/changelog/2026-07-18-page-details-now-live-in-a-shared-info-and-comments-rail-dat.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-18-workspace-database-views-now-control-sidebar-workspace-navig.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-18-workspace-navigation-now-aligns-favorites-with-workspace-pag.md +6 -0
  110. package/corpus/templates/content/changelog/2026-07-19-favorites-now-open-as-a-table-and-compact-breadcrumbs-make-w.md +6 -0
  111. package/corpus/templates/content/changelog/2026-07-19-files-databases-now-start-unfiltered-sidebar-rows-exactly-fo.md +6 -0
  112. package/corpus/templates/content/changelog/2026-07-19-sidebar-page-trees-now-open-only-when-expanded-remember-thei.md +6 -0
  113. package/corpus/templates/content/changelog/2026-07-19-workspace-file-databases-now-use-the-workspace-name-open-as-.md +6 -0
  114. package/corpus/templates/content/changelog/2026-07-19-workspace-navigation-now-stays-stable-on-hover-uses-folder-i.md +6 -0
  115. package/corpus/templates/content/changelog/2026-07-19-workspace-rows-now-use-folder-icons-and-deleting-a-user-crea.md +6 -0
  116. package/corpus/templates/content/changelog/2026-07-19-workspaces-can-be-created-without-leaving-the-workspaces-dat.md +6 -0
  117. package/corpus/templates/content/e2e/global-setup.ts +3 -2
  118. package/corpus/templates/content/parity/matrix.md +28 -28
  119. package/corpus/templates/content/parity/matrix.ts +8 -2
  120. package/corpus/templates/content/server/db/index.ts +1 -0
  121. package/corpus/templates/content/server/db/schema.ts +10 -0
  122. package/corpus/templates/content/server/lib/documents.ts +12 -7
  123. package/corpus/templates/content/server/plugins/db.ts +17 -0
  124. package/corpus/templates/content/shared/api.ts +9 -0
  125. package/corpus/templates/content/shared/database-form.ts +11 -2
  126. package/corpus/templates/content/vite.config.ts +1 -0
  127. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  128. package/corpus/templates/design/AGENTS.md +5 -0
  129. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  130. package/corpus/templates/dispatch/AGENTS.md +5 -0
  131. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  132. package/corpus/templates/forms/AGENTS.md +5 -0
  133. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  134. package/corpus/templates/macros/AGENTS.md +5 -0
  135. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  136. package/corpus/templates/mail/AGENTS.md +5 -0
  137. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  138. package/corpus/templates/plan/AGENTS.md +5 -0
  139. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  140. package/corpus/templates/slides/AGENTS.md +5 -0
  141. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  142. package/corpus/templates/tasks/AGENTS.md +5 -0
  143. package/dist/cli/create.d.ts.map +1 -1
  144. package/dist/cli/create.js +21 -0
  145. package/dist/cli/create.js.map +1 -1
  146. package/dist/cli/skills.d.ts.map +1 -1
  147. package/dist/cli/skills.js +19 -0
  148. package/dist/cli/skills.js.map +1 -1
  149. package/dist/client/build-compatibility.d.ts +11 -0
  150. package/dist/client/build-compatibility.d.ts.map +1 -0
  151. package/dist/client/build-compatibility.js +56 -0
  152. package/dist/client/build-compatibility.js.map +1 -0
  153. package/dist/client/route-chunk-recovery/index.d.ts +1 -0
  154. package/dist/client/route-chunk-recovery/index.d.ts.map +1 -1
  155. package/dist/client/route-chunk-recovery/index.js +1 -0
  156. package/dist/client/route-chunk-recovery/index.js.map +1 -1
  157. package/dist/client/use-action.d.ts.map +1 -1
  158. package/dist/client/use-action.js +19 -1
  159. package/dist/client/use-action.js.map +1 -1
  160. package/dist/collab/routes.d.ts +1 -1
  161. package/dist/deploy/build.js +2 -1
  162. package/dist/deploy/build.js.map +1 -1
  163. package/dist/notifications/routes.d.ts +1 -1
  164. package/dist/observability/routes.d.ts +3 -3
  165. package/dist/org/context.d.ts +4 -2
  166. package/dist/org/context.d.ts.map +1 -1
  167. package/dist/org/context.js +44 -22
  168. package/dist/org/context.js.map +1 -1
  169. package/dist/progress/routes.d.ts +1 -1
  170. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  171. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  172. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  173. package/dist/resources/handlers.d.ts +1 -1
  174. package/dist/server/action-routes.d.ts.map +1 -1
  175. package/dist/server/action-routes.js +32 -2
  176. package/dist/server/action-routes.js.map +1 -1
  177. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  178. package/dist/server/auth-plugin.d.ts.map +1 -1
  179. package/dist/server/auth-plugin.js +9 -5
  180. package/dist/server/auth-plugin.js.map +1 -1
  181. package/dist/server/better-auth-instance.d.ts.map +1 -1
  182. package/dist/server/better-auth-instance.js +4 -1
  183. package/dist/server/better-auth-instance.js.map +1 -1
  184. package/dist/server/transcribe-voice.d.ts +1 -1
  185. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  186. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  187. package/dist/shared/mcp-embed-headers.js +1 -1
  188. package/dist/shared/mcp-embed-headers.js.map +1 -1
  189. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  190. package/dist/templates/chat/AGENTS.md +4 -1
  191. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  192. package/dist/templates/default/AGENTS.md +4 -1
  193. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  194. package/dist/templates/headless/AGENTS.md +5 -1
  195. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +26 -0
  196. package/dist/templates/workspace-core/AGENTS.md +3 -1
  197. package/dist/templates/workspace-root/AGENTS.md +3 -1
  198. package/dist/vite/client.d.ts +6 -0
  199. package/dist/vite/client.d.ts.map +1 -1
  200. package/dist/vite/client.js +8 -0
  201. package/dist/vite/client.js.map +1 -1
  202. package/package.json +1 -1
  203. package/src/cli/create.ts +23 -0
  204. package/src/cli/skills.ts +24 -0
  205. package/src/client/build-compatibility.ts +77 -0
  206. package/src/client/route-chunk-recovery/index.ts +1 -0
  207. package/src/client/use-action.ts +32 -1
  208. package/src/deploy/build.ts +4 -1
  209. package/src/org/context.ts +56 -30
  210. package/src/server/action-routes.ts +52 -2
  211. package/src/server/auth-plugin.ts +9 -4
  212. package/src/server/better-auth-instance.ts +5 -1
  213. package/src/shared/mcp-embed-headers.ts +1 -1
  214. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  215. package/src/templates/chat/AGENTS.md +4 -1
  216. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  217. package/src/templates/default/AGENTS.md +4 -1
  218. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  219. package/src/templates/headless/AGENTS.md +5 -1
  220. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +26 -0
  221. package/src/templates/workspace-core/AGENTS.md +3 -1
  222. package/src/templates/workspace-root/AGENTS.md +3 -1
  223. package/src/vite/client.ts +17 -0
@@ -8,13 +8,19 @@ import {
8
8
  type CollabUser,
9
9
  } from "@agent-native/core/client/collab";
10
10
  import {
11
+ setClientAppState,
11
12
  useAvatarUrl,
12
13
  useDbSync,
13
14
  useSession,
14
15
  } from "@agent-native/core/client/hooks";
15
16
  import { useT } from "@agent-native/core/client/i18n";
16
17
  import type { Document, DocumentSyncStatus } from "@shared/api";
17
- import { IconArrowLeft, IconDatabase, IconLoader2 } from "@tabler/icons-react";
18
+ import {
19
+ IconDatabase,
20
+ IconFileText,
21
+ IconLoader2,
22
+ IconX,
23
+ } from "@tabler/icons-react";
18
24
  import { IconLock } from "@tabler/icons-react";
19
25
  import { useQueryClient } from "@tanstack/react-query";
20
26
  import {
@@ -33,13 +39,33 @@ import {
33
39
  contentBlockRegistry,
34
40
  createContentBlockRenderContext,
35
41
  } from "@/blocks/contentBlockRegistry";
42
+ import {
43
+ createContentSpaceSelectionQueue,
44
+ SELECTED_CONTENT_SPACE_STORAGE_KEY,
45
+ selectContentSpace,
46
+ } from "@/components/sidebar/select-content-space";
36
47
  import { Button } from "@/components/ui/button";
37
- import { Sheet, SheetContent } from "@/components/ui/sheet";
48
+ import {
49
+ Sheet,
50
+ SheetContent,
51
+ SheetHeader,
52
+ SheetTitle,
53
+ } from "@/components/ui/sheet";
38
54
  import { useComments } from "@/hooks/use-comments";
39
- import { useProcessBuilderBodyHydration } from "@/hooks/use-content-database";
55
+ import {
56
+ useCreateContentDatabase,
57
+ useDeleteContentDatabase,
58
+ useProcessBuilderBodyHydration,
59
+ } from "@/hooks/use-content-database";
60
+ import {
61
+ useContentSpaces,
62
+ type ContentSpaceSummary,
63
+ } from "@/hooks/use-content-spaces";
40
64
  import {
41
65
  isDocumentUpdateConflict,
66
+ patchDocumentCaches,
42
67
  useDocument,
68
+ useDeleteDocument,
43
69
  useDocuments,
44
70
  useUpdateDocument,
45
71
  } from "@/hooks/use-documents";
@@ -63,12 +89,11 @@ import {
63
89
  import { BuilderBodySyncingNotice } from "./BuilderBodySyncingNotice";
64
90
  import type { CommentTextAnchor } from "./comment-anchors";
65
91
  import { CommentsSidebar } from "./CommentsSidebar";
66
- import { DescriptionField } from "./DescriptionField";
67
92
  import { DocumentBlockFields } from "./DocumentBlockFields";
68
93
  import { DocumentDatabase } from "./DocumentDatabase";
69
94
  import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
70
- import { DocumentProperties } from "./DocumentProperties";
71
- import { DocumentToolbar } from "./DocumentToolbar";
95
+ import { DocumentInfoPanel } from "./DocumentInfoPanel";
96
+ import { DocumentToolbar, type ToolbarBreadcrumbItem } from "./DocumentToolbar";
72
97
  import { EmojiPicker } from "./EmojiPicker";
73
98
  import { NotionConflictBanner } from "./NotionConflictBanner";
74
99
  import {
@@ -86,6 +111,37 @@ interface DocumentEditorProps {
86
111
 
87
112
  type FieldSaveWatermark = { title: string; updatedAt: string | null };
88
113
  type ContentSaveWatermark = { content: string; updatedAt: string | null };
114
+ type DocumentUtilityPanel = "info" | "comments" | null;
115
+
116
+ export function metadataUpdatesWithPendingTitle<
117
+ T extends {
118
+ title?: string;
119
+ content?: string;
120
+ description?: string;
121
+ icon?: string | null;
122
+ },
123
+ >(
124
+ updates: T,
125
+ currentTitle: string,
126
+ savedTitle: string,
127
+ ): T & { title?: string } {
128
+ if (updates.title !== undefined || currentTitle === savedTitle)
129
+ return updates;
130
+ return { ...updates, title: currentTitle };
131
+ }
132
+
133
+ export function titleMatchConfirmsSave(args: {
134
+ serverTitle: string;
135
+ localTitle: string;
136
+ lastSavedTitle: string;
137
+ pendingTitle: string | null;
138
+ }) {
139
+ if (args.serverTitle !== args.localTitle) return false;
140
+ return !(
141
+ args.pendingTitle === args.localTitle &&
142
+ args.localTitle !== args.lastSavedTitle
143
+ );
144
+ }
89
145
 
90
146
  function adoptConfirmedSaveWatermarks({
91
147
  saved,
@@ -249,7 +305,10 @@ export function databaseMembershipDatabaseTitle(
249
305
  }
250
306
 
251
307
  export function documentEditorBreadcrumbItems(
252
- document: Pick<Document, "id" | "parentId" | "title" | "icon">, // i18n-ignore type expression
308
+ document: Pick<
309
+ Document,
310
+ "id" | "parentId" | "title" | "icon" | "databaseMembership"
311
+ >, // i18n-ignore type expression
253
312
  documents: Pick<Document, "id" | "parentId" | "title" | "icon">[], // i18n-ignore type expression
254
313
  ) {
255
314
  const byId = new Map(documents.map((doc) => [doc.id, doc]));
@@ -270,7 +329,7 @@ export function documentEditorBreadcrumbItems(
270
329
  parentId = parent.parentId;
271
330
  }
272
331
 
273
- return [
332
+ const pageItems = [
274
333
  ...parents,
275
334
  {
276
335
  id: document.id,
@@ -278,39 +337,117 @@ export function documentEditorBreadcrumbItems(
278
337
  icon: document.icon,
279
338
  },
280
339
  ];
281
- }
282
-
283
- function DatabaseMembershipBreadcrumb({
284
- document,
285
- onOpenDatabase,
286
- }: {
287
- document: Document;
288
- onOpenDatabase: (databaseDocumentId: string) => void;
289
- }) {
290
340
  const membership = document.databaseMembership;
291
- if (!membership) return null;
341
+ if (
342
+ !membership ||
343
+ pageItems.some((item) => item.id === membership.databaseDocumentId)
344
+ ) {
345
+ return pageItems;
346
+ }
292
347
 
293
- const databaseTitle = databaseMembershipDatabaseTitle(membership);
348
+ return [
349
+ {
350
+ id: membership.databaseDocumentId,
351
+ title: databaseMembershipDatabaseTitle(membership),
352
+ icon: null,
353
+ },
354
+ ...pageItems,
355
+ ];
356
+ }
294
357
 
295
- return (
296
- <div className="mb-3 -ml-1 flex min-w-0 items-center">
297
- <button
298
- type="button"
299
- aria-label={`Open database ${databaseTitle}`}
300
- className="inline-flex h-7 max-w-full items-center gap-1.5 rounded px-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
301
- onClick={() => onOpenDatabase(membership.databaseDocumentId)}
302
- >
303
- <IconArrowLeft className="size-3.5 shrink-0" />
304
- <IconDatabase className="size-4 shrink-0" />
305
- <span className="truncate">{databaseTitle}</span>
306
- </button>
307
- </div>
358
+ export function documentEditorBreadcrumbNavigationItems(
359
+ items: ToolbarBreadcrumbItem[],
360
+ documents: Pick<
361
+ Document,
362
+ | "id"
363
+ | "parentId"
364
+ | "title"
365
+ | "icon"
366
+ | "position"
367
+ | "databaseMembership"
368
+ | "source"
369
+ >[], // i18n-ignore type expression
370
+ spaces: Pick<ContentSpaceSummary, "filesDocumentId" | "name">[], // i18n-ignore type expression
371
+ context?: {
372
+ currentDocumentId: string;
373
+ currentParentId: string | null;
374
+ currentDatabaseSystemRole: string | null;
375
+ catalogDocumentId: string | null;
376
+ workspacesTitle: string;
377
+ },
378
+ ): ToolbarBreadcrumbItem[] {
379
+ const documentById = new Map(documents.map((item) => [item.id, item]));
380
+ const workspaceDocumentIds = new Set(
381
+ spaces.map((space) => space.filesDocumentId),
308
382
  );
383
+
384
+ const navigationItems = items.map<ToolbarBreadcrumbItem>((item) => {
385
+ if (item.id && workspaceDocumentIds.has(item.id)) {
386
+ return {
387
+ ...item,
388
+ iconKind: "folder",
389
+ menuItems: spaces.map((space) => ({
390
+ id: space.filesDocumentId,
391
+ title: space.name,
392
+ icon: null,
393
+ iconKind: "folder",
394
+ })),
395
+ };
396
+ }
397
+
398
+ const current = item.id ? documentById.get(item.id) : null;
399
+ if (!current) return item;
400
+ const membershipDocumentId =
401
+ current.databaseMembership?.databaseDocumentId ?? null;
402
+ const siblings = documents
403
+ .filter((candidate) => {
404
+ if (candidate.source?.kind === "folder") return false;
405
+ if (candidate.parentId !== current.parentId) return false;
406
+ if (current.parentId) return true;
407
+ return (
408
+ candidate.databaseMembership?.databaseDocumentId ===
409
+ membershipDocumentId
410
+ );
411
+ })
412
+ .sort(
413
+ (left, right) =>
414
+ left.position - right.position ||
415
+ left.title.localeCompare(right.title),
416
+ );
417
+ if (siblings.length < 2) return item;
418
+ return {
419
+ ...item,
420
+ menuItems: siblings.map((sibling) => ({
421
+ id: sibling.id,
422
+ title: sibling.title,
423
+ icon: sibling.icon,
424
+ })),
425
+ };
426
+ });
427
+
428
+ if (
429
+ context?.catalogDocumentId &&
430
+ context.currentParentId === null &&
431
+ context.currentDatabaseSystemRole === "files" &&
432
+ workspaceDocumentIds.has(context.currentDocumentId)
433
+ ) {
434
+ const workspacesItem: ToolbarBreadcrumbItem = {
435
+ id: context.catalogDocumentId,
436
+ title: context.workspacesTitle,
437
+ iconKind: "folder",
438
+ };
439
+ return [workspacesItem, ...navigationItems];
440
+ }
441
+
442
+ return navigationItems;
309
443
  }
310
444
 
311
445
  function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
312
446
  const t = useT();
313
447
  const updateDocument = useUpdateDocument();
448
+ const createDatabase = useCreateContentDatabase(documentId);
449
+ const deleteContentDatabase = useDeleteContentDatabase();
450
+ const deleteDocument = useDeleteDocument();
314
451
  const queryClient = useQueryClient();
315
452
  const processBuilderBodies = useProcessBuilderBodyHydration(
316
453
  document.databaseMembership?.databaseDocumentId ?? documentId,
@@ -328,9 +465,22 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
328
465
  const navigate = useNavigate();
329
466
  const documentsQuery = useDocuments();
330
467
  const documents: Document[] = documentsQuery.data ?? [];
468
+ const contentSpacesQuery = useContentSpaces();
469
+ const contentSpaces = contentSpacesQuery.data?.spaces ?? [];
470
+ const workspaceSelectionQueueRef = useRef(createContentSpaceSelectionQueue());
471
+ const [, setStoredSpaceId] = useLocalStorage<string | null>(
472
+ SELECTED_CONTENT_SPACE_STORAGE_KEY,
473
+ null,
474
+ );
331
475
  // Shared with DocumentToolbar via the same localStorage key — both read it.
332
476
  const [autoSync] = useLocalStorage(`notion-auto-sync:${documentId}`, false);
333
477
  const isLocalFileDocument = document.source?.mode === "local-files";
478
+ const canDelete =
479
+ !isLocalFileDocument &&
480
+ !document.database?.systemRole &&
481
+ (document.canManage === true ||
482
+ document.accessRole === "owner" ||
483
+ document.accessRole === "admin");
334
484
  const isLinkedLocalSourceDocument = canWriteLinkedLocalSource(
335
485
  documentId,
336
486
  document.source,
@@ -343,9 +493,34 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
343
493
  const pushDocumentToNotion = usePushDocumentToNotion(documentId);
344
494
  const [localTitle, setLocalTitle] = useState("");
345
495
  const [localContent, setLocalContent] = useState("");
496
+ const [newDocumentTypeChosen, setNewDocumentTypeChosen] = useState(false);
346
497
  const [localContentUpdatedAt, setLocalContentUpdatedAt] = useState<
347
498
  string | null
348
499
  >(document.updatedAt ?? null);
500
+ const handleDeleteDocument = useCallback(async () => {
501
+ try {
502
+ if (document.database) {
503
+ await deleteContentDatabase.mutateAsync({
504
+ databaseId: document.database.id,
505
+ });
506
+ } else {
507
+ await deleteDocument.mutateAsync({ id: documentId });
508
+ }
509
+ navigate("/", { replace: true, flushSync: true });
510
+ } catch (error) {
511
+ toast.error(t("sidebar.failedDeletePage"), {
512
+ description:
513
+ error instanceof Error ? error.message : t("empty.genericError"),
514
+ });
515
+ }
516
+ }, [
517
+ deleteContentDatabase,
518
+ deleteDocument,
519
+ document.database,
520
+ documentId,
521
+ navigate,
522
+ t,
523
+ ]);
349
524
  const flushRequestKey = `flush-request-${documentId}`;
350
525
  const [flushRequestWake, setFlushRequestWake] = useState(0);
351
526
  const handleFlushRequestEvent = useCallback(
@@ -515,6 +690,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
515
690
  if (prevDocIdRef.current !== documentId) {
516
691
  prevDocIdRef.current = documentId;
517
692
  isInitializedRef.current = false;
693
+ setNewDocumentTypeChosen(false);
518
694
  if (saveTimeoutRef.current) {
519
695
  clearTimeout(saveTimeoutRef.current);
520
696
  saveTimeoutRef.current = null;
@@ -610,7 +786,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
610
786
  useEffect(() => {
611
787
  if (!document || !isInitializedRef.current) return;
612
788
  if (isLinkedLocalSourceDocument) return;
613
- const titleMatchesLocal = document.title === localTitle;
789
+ const titleMatchesLocal = titleMatchConfirmsSave({
790
+ serverTitle: document.title,
791
+ localTitle,
792
+ lastSavedTitle: lastSavedTitleRef.current.title,
793
+ pendingTitle: pendingDocumentSaveRef.current?.title ?? null,
794
+ });
614
795
  const contentMatchesLocal = document.content === localContent;
615
796
 
616
797
  if (titleMatchesLocal) {
@@ -696,6 +877,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
696
877
  ...(baseUpdatedAt !== undefined ? { baseUpdatedAt } : {}),
697
878
  });
698
879
  } catch (error) {
880
+ if (updates.title !== undefined) {
881
+ patchDocumentCaches(queryClient, documentId, {
882
+ title: lastSavedTitleRef.current.title,
883
+ });
884
+ }
699
885
  if (!isLinkedLocalSource) throw error;
700
886
  toast.warning(t("editor.localFileSavedHistoryNotUpdated"), {
701
887
  description:
@@ -1110,10 +1296,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1110
1296
  const handleTitleChange = useCallback(
1111
1297
  (newTitle: string) => {
1112
1298
  if (!editorCanEdit) return;
1299
+ localTitleRef.current = newTitle;
1113
1300
  setLocalTitle(newTitle);
1301
+ patchDocumentCaches(queryClient, documentId, { title: newTitle });
1114
1302
  debouncedSave(newTitle, localContentRef.current);
1115
1303
  },
1116
- [debouncedSave, editorCanEdit],
1304
+ [debouncedSave, documentId, editorCanEdit, queryClient],
1117
1305
  );
1118
1306
 
1119
1307
  const handleContentChange = useCallback(
@@ -1153,18 +1341,15 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1153
1341
  } | null>(null);
1154
1342
  const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
1155
1343
  const [hoveredThreadId, setHoveredThreadId] = useState<string | null>(null);
1344
+ const [utilityPanel, setUtilityPanel] = useState<DocumentUtilityPanel>(null);
1156
1345
  const activeThreadId = hoveredThreadId ?? selectedThreadId;
1157
1346
  const { data: threads, isLoading: commentsLoading } = useComments(
1158
1347
  canEdit && !isLocalFileDocument ? documentId : null,
1159
1348
  );
1160
- const hasComments =
1161
- !isLocalFileDocument &&
1162
- editorCanEdit &&
1163
- ((threads?.length ?? 0) > 0 || !!pendingComment);
1164
- const hasCommentRailSpace = useMinViewportWidth(1024);
1165
- const showDesktopComments = hasComments && hasCommentRailSpace;
1166
- const showCommentsSheet =
1167
- hasComments && editorCanEdit && !showDesktopComments;
1349
+ const hasUtilityRailSpace = useMinViewportWidth(1024);
1350
+ const showDesktopUtilityPanel = utilityPanel !== null && hasUtilityRailSpace;
1351
+ const showUtilityPanelSheet =
1352
+ utilityPanel !== null && !showDesktopUtilityPanel;
1168
1353
 
1169
1354
  const handleComment = useCallback(
1170
1355
  (
@@ -1174,6 +1359,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1174
1359
  range?: { from: number; to: number },
1175
1360
  ) => {
1176
1361
  setPendingComment({ quotedText, offsetTop, anchor, range });
1362
+ setUtilityPanel("comments");
1177
1363
  setSelectedThreadId(null);
1178
1364
  setHoveredThreadId(null);
1179
1365
  },
@@ -1187,6 +1373,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1187
1373
 
1188
1374
  useEffect(() => {
1189
1375
  setPendingComment(null);
1376
+ setUtilityPanel(null);
1190
1377
  clearCommentFocus();
1191
1378
  }, [clearCommentFocus, documentId]);
1192
1379
 
@@ -1254,11 +1441,67 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1254
1441
  });
1255
1442
 
1256
1443
  const toolbarBreadcrumbItems = useMemo(
1257
- () => documentEditorBreadcrumbItems(document, documents),
1258
- [document, documents],
1444
+ () =>
1445
+ documentEditorBreadcrumbNavigationItems(
1446
+ documentEditorBreadcrumbItems(document, documents),
1447
+ documents,
1448
+ contentSpaces,
1449
+ {
1450
+ currentDocumentId: document.id,
1451
+ currentParentId: document.parentId,
1452
+ currentDatabaseSystemRole: document.database?.systemRole ?? null,
1453
+ catalogDocumentId: contentSpacesQuery.data?.catalogDocumentId ?? null,
1454
+ workspacesTitle: t("sidebar.workspaces"),
1455
+ },
1456
+ ),
1457
+ [
1458
+ contentSpaces,
1459
+ contentSpacesQuery.data?.catalogDocumentId,
1460
+ document,
1461
+ documents,
1462
+ t,
1463
+ ],
1464
+ );
1465
+
1466
+ const handleOpenToolbarBreadcrumb = useCallback(
1467
+ (targetId: string) => {
1468
+ const targetDocument = documents.find((item) => item.id === targetId);
1469
+ const filesDocumentId =
1470
+ targetDocument?.databaseMembership?.databaseDocumentId ?? targetId;
1471
+ const space = contentSpaces.find(
1472
+ (candidate) => candidate.filesDocumentId === filesDocumentId,
1473
+ );
1474
+ if (!space) {
1475
+ navigate(`/page/${targetId}`, { flushSync: true });
1476
+ return;
1477
+ }
1478
+ void workspaceSelectionQueueRef
1479
+ .current(() =>
1480
+ selectContentSpace({
1481
+ space,
1482
+ syncApplicationState: (selected) =>
1483
+ setClientAppState(
1484
+ "content-space",
1485
+ {
1486
+ spaceId: selected.id,
1487
+ name: selected.name,
1488
+ kind: selected.kind,
1489
+ filesDatabaseId: selected.filesDatabaseId,
1490
+ },
1491
+ { requestSource: "content-breadcrumb" },
1492
+ ),
1493
+ persistSelection: setStoredSpaceId,
1494
+ openFiles: () => navigate(`/page/${targetId}`, { flushSync: true }),
1495
+ }),
1496
+ )
1497
+ .catch((error) => {
1498
+ toast.error(error instanceof Error ? error.message : String(error));
1499
+ });
1500
+ },
1501
+ [contentSpaces, documents, navigate, setStoredSpaceId],
1259
1502
  );
1260
1503
 
1261
- const sidebar = (
1504
+ const commentsSidebar = (
1262
1505
  <CommentsSidebar
1263
1506
  documentId={documentId}
1264
1507
  threads={threads ?? []}
@@ -1271,10 +1514,34 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1271
1514
  onSelectedThreadChange={setSelectedThreadId}
1272
1515
  onHoveredThreadChange={setHoveredThreadId}
1273
1516
  currentUserEmail={session?.email}
1517
+ forceVisible
1274
1518
  />
1275
1519
  );
1276
1520
  const defaultIconKind = documentEditorDefaultIconKind(document);
1277
1521
  const isDatabasePage = Boolean(document.database);
1522
+ const showNewDocumentTypeChooser =
1523
+ canEdit &&
1524
+ !isLocalFileDocument &&
1525
+ !isDatabasePage &&
1526
+ !newDocumentTypeChosen &&
1527
+ !localTitle.trim() &&
1528
+ !document.description?.trim() &&
1529
+ isEffectivelyEmptyDocumentContent(localContent);
1530
+ const handleChoosePage = useCallback(() => {
1531
+ setNewDocumentTypeChosen(true);
1532
+ requestAnimationFrame(() => titleInputRef.current?.focus());
1533
+ }, []);
1534
+ const handleChooseDatabase = useCallback(async () => {
1535
+ try {
1536
+ await createDatabase.mutateAsync({ documentId });
1537
+ setNewDocumentTypeChosen(true);
1538
+ } catch (error) {
1539
+ toast.error(t("sidebar.failedCreateDatabase"), {
1540
+ description:
1541
+ error instanceof Error ? error.message : t("empty.genericError"),
1542
+ });
1543
+ }
1544
+ }, [createDatabase, documentId, t]);
1278
1545
  const defaultIcon =
1279
1546
  defaultIconKind === "database" && !isDatabasePage ? (
1280
1547
  <IconDatabase className="size-12" aria-hidden="true" />
@@ -1283,6 +1550,36 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1283
1550
  const exportContent = isInitializedRef.current
1284
1551
  ? localContent
1285
1552
  : document.content;
1553
+ const utilityPanelTitle =
1554
+ utilityPanel === "info" ? t("editor.toolbar.info") : t("comments.title");
1555
+ const utilityPanelContent = utilityPanel ? (
1556
+ <div className="w-full min-w-0 bg-background" data-document-utility-panel>
1557
+ <div className="sticky top-0 z-10 flex h-12 items-center border-b border-border bg-background px-4">
1558
+ <h2 className="text-sm font-semibold">{utilityPanelTitle}</h2>
1559
+ {hasUtilityRailSpace ? (
1560
+ <button
1561
+ type="button"
1562
+ className="ms-auto flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1563
+ aria-label={t("editor.toolbar.closeUtilityPanel")}
1564
+ onClick={() => setUtilityPanel(null)}
1565
+ >
1566
+ <IconX size={16} />
1567
+ </button>
1568
+ ) : null}
1569
+ </div>
1570
+ {utilityPanel === "info" ? (
1571
+ <DocumentInfoPanel
1572
+ document={document}
1573
+ canEdit={editorCanEdit}
1574
+ onSaveDescription={(description) =>
1575
+ persistDocumentUpdates({ description })
1576
+ }
1577
+ />
1578
+ ) : (
1579
+ commentsSidebar
1580
+ )}
1581
+ </div>
1582
+ ) : null;
1286
1583
 
1287
1584
  return (
1288
1585
  <BlockRegistryProvider
@@ -1318,6 +1615,15 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1318
1615
  canEdit={canEdit}
1319
1616
  hideFromSearch={document.hideFromSearch}
1320
1617
  source={document.source}
1618
+ canDelete={canDelete}
1619
+ deletePending={
1620
+ deleteDocument.isPending || deleteContentDatabase.isPending
1621
+ }
1622
+ onDelete={handleDeleteDocument}
1623
+ utilityPanel={utilityPanel}
1624
+ onUtilityPanelChange={setUtilityPanel}
1625
+ showCommentsControl={editorCanEdit && !isLocalFileDocument}
1626
+ onOpenBreadcrumbItem={handleOpenToolbarBreadcrumb}
1321
1627
  />
1322
1628
 
1323
1629
  {!isLocalFileDocument ? (
@@ -1332,14 +1638,14 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1332
1638
  <div
1333
1639
  className={cn(
1334
1640
  "flex min-h-full w-full min-w-0",
1335
- showDesktopComments ? "justify-center" : "flex-col",
1641
+ showDesktopUtilityPanel ? "justify-center" : "flex-col",
1336
1642
  )}
1337
1643
  data-document-scroll-content
1338
1644
  >
1339
1645
  <div
1340
1646
  className={cn(
1341
1647
  "min-w-0",
1342
- showDesktopComments ? "flex-1" : "w-full",
1648
+ showDesktopUtilityPanel ? "flex-1" : "w-full",
1343
1649
  )}
1344
1650
  >
1345
1651
  <div
@@ -1347,14 +1653,6 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1347
1653
  Boolean(document.database),
1348
1654
  )}
1349
1655
  >
1350
- <DatabaseMembershipBreadcrumb
1351
- document={document}
1352
- onOpenDatabase={(databaseDocumentId) =>
1353
- navigate(`/page/${databaseDocumentId}`, {
1354
- flushSync: true,
1355
- })
1356
- }
1357
- />
1358
1656
  {document.icon || !isDatabasePage ? (
1359
1657
  <div className="mb-1">
1360
1658
  {editorCanEdit ? (
@@ -1366,9 +1664,13 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1366
1664
  }
1367
1665
  onSelect={(emoji) => {
1368
1666
  void (async () => {
1369
- const saved = await persistDocumentUpdates({
1370
- icon: emoji,
1371
- });
1667
+ const updates = metadataUpdatesWithPendingTitle(
1668
+ { icon: emoji },
1669
+ localTitleRef.current,
1670
+ lastSavedTitleRef.current.title,
1671
+ );
1672
+ const saved =
1673
+ await persistDocumentUpdates(updates);
1372
1674
  // Icon-only save: never CAS-guarded server-side
1373
1675
  // (no content in this call), so this can't come
1374
1676
  // back as a conflict — narrow defensively anyway
@@ -1381,7 +1683,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1381
1683
  saved?.updatedAt ?? new Date().toISOString(),
1382
1684
  title: localTitleRef.current,
1383
1685
  content: localContentRef.current,
1384
- updates: { icon: emoji },
1686
+ updates,
1385
1687
  lastSavedTitleRef,
1386
1688
  lastSavedContentRef,
1387
1689
  });
@@ -1436,23 +1738,6 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1436
1738
  isDatabasePage ? "text-3xl" : "text-3xl md:text-4xl",
1437
1739
  )}
1438
1740
  />
1439
- {!document.database ? (
1440
- <DescriptionField
1441
- description={document.description}
1442
- canEdit={editorCanEdit}
1443
- label={t("editor.properties.description")}
1444
- placeholder={t("editor.properties.addPageDescription")}
1445
- onSave={(description) =>
1446
- persistDocumentUpdates({ description })
1447
- }
1448
- />
1449
- ) : null}
1450
- {document.databaseMembership && !isLocalFileDocument ? (
1451
- <DocumentProperties
1452
- documentId={documentId}
1453
- canEdit={editorCanEdit}
1454
- />
1455
- ) : null}
1456
1741
  </div>
1457
1742
  {document.database ? (
1458
1743
  <div className={documentEditorDatabaseRegionClassName()}>
@@ -1460,123 +1745,179 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1460
1745
  </div>
1461
1746
  ) : null}
1462
1747
 
1463
- <div
1464
- className="flex-1 w-full max-w-3xl mx-auto px-4 pb-16 cursor-text sm:px-8 md:px-16"
1465
- onClick={(e) => {
1466
- if (e.target === e.currentTarget) {
1467
- const pm = e.currentTarget.querySelector(
1468
- ".ProseMirror",
1469
- ) as HTMLElement | null;
1470
- pm?.focus();
1471
- }
1472
- }}
1473
- >
1474
- {(() => {
1475
- if (bodyHydrationPending) {
1476
- return (
1477
- <BuilderBodySyncingNotice
1478
- title={t("editor.builderBodySyncing")}
1479
- description={t(
1480
- "editor.builderBodySyncingDescription",
1481
- )}
1482
- />
1483
- );
1484
- }
1748
+ {!isDatabasePage ? (
1749
+ <div
1750
+ className="flex-1 w-full max-w-3xl mx-auto px-4 pb-16 cursor-text sm:px-8 md:px-16"
1751
+ onClick={(e) => {
1752
+ if (e.target === e.currentTarget) {
1753
+ const pm = e.currentTarget.querySelector(
1754
+ ".ProseMirror",
1755
+ ) as HTMLElement | null;
1756
+ pm?.focus();
1757
+ }
1758
+ }}
1759
+ >
1760
+ {(() => {
1761
+ if (bodyHydrationPending) {
1762
+ return (
1763
+ <BuilderBodySyncingNotice
1764
+ title={t("editor.builderBodySyncing")}
1765
+ description={t(
1766
+ "editor.builderBodySyncingDescription",
1767
+ )}
1768
+ />
1769
+ );
1770
+ }
1485
1771
 
1486
- // The primary "Content" Blocks field IS the document body,
1487
- // with the full collaborative editor. It renders chromeless
1488
- // when it's the only Blocks field, or inside a
1489
- // header/collapsible shell when the row has multiple Blocks
1490
- // fields.
1491
- const primaryEditor = (
1492
- <VisualEditor
1493
- key={`${documentId}:${editorCanEdit && !isLocalFileDocument ? "live" : "snapshot"}`}
1494
- documentId={documentId}
1495
- content={
1496
- isLocalFileDocument ? localContent : document.content
1497
- }
1498
- contentUpdatedAt={
1499
- isLocalFileDocument
1500
- ? (localContentUpdatedAt ?? document.updatedAt)
1501
- : document.updatedAt
1502
- }
1503
- onChange={handleContentChange}
1504
- onSaveContent={handleContentSaveNow}
1505
- // Bind the shared Y.Doc/awareness for viewers too — the
1506
- // editor is non-editable for them and VisualEditor blocks
1507
- // any local Y.Doc mutation, so they get live edits +
1508
- // cursors without ever writing. Excludes local-file docs.
1509
- ydoc={collabEnabled ? ydoc : null}
1510
- collabSynced={collabEnabled ? collabSynced : true}
1511
- awareness={collabEnabled ? awareness : null}
1512
- user={currentUser}
1513
- editable={editorCanEdit}
1514
- localFileMode={isLocalFileDocument}
1515
- localFilePath={
1516
- isLocalFileDocument ? document.source?.path : null
1517
- }
1518
- onComment={
1519
- editorCanEdit && !isLocalFileDocument
1520
- ? handleComment
1521
- : undefined
1522
- }
1523
- commentThreads={threads ?? []}
1524
- activeThreadId={activeThreadId}
1525
- pendingHighlight={pendingComment?.range ?? null}
1526
- onActivateThread={
1527
- editorCanEdit && !isLocalFileDocument
1528
- ? setSelectedThreadId
1529
- : undefined
1530
- }
1531
- onJoinTitle={joinFirstBodyBlockToTitle}
1532
- notionPageLinks={notionPageLinks}
1533
- onOpenNotionPageLink={handleOpenNotionPageLink}
1534
- notionPageId={document.notionPageId}
1535
- />
1536
- );
1537
-
1538
- // Only database rows have Blocks fields. Standalone pages
1539
- // and local-file documents keep the plain chromeless body.
1540
- if (document.databaseMembership && !isLocalFileDocument) {
1541
- return (
1542
- <DocumentBlockFields
1772
+ if (showNewDocumentTypeChooser) {
1773
+ return (
1774
+ <div
1775
+ className="flex flex-wrap gap-2 pt-3"
1776
+ aria-label={t("sidebar.newPage")}
1777
+ >
1778
+ <Button
1779
+ type="button"
1780
+ variant="outline"
1781
+ className="justify-start gap-2"
1782
+ disabled={
1783
+ !editorCanEdit || createDatabase.isPending
1784
+ }
1785
+ onClick={handleChoosePage}
1786
+ >
1787
+ <IconFileText />
1788
+ {t("sidebar.page")}
1789
+ </Button>
1790
+ <Button
1791
+ type="button"
1792
+ variant="outline"
1793
+ className="justify-start gap-2"
1794
+ disabled={
1795
+ !editorCanEdit || createDatabase.isPending
1796
+ }
1797
+ onClick={() => void handleChooseDatabase()}
1798
+ >
1799
+ {createDatabase.isPending ? (
1800
+ <IconLoader2 className="animate-spin" />
1801
+ ) : (
1802
+ <IconDatabase />
1803
+ )}
1804
+ {t("sidebar.database")}
1805
+ </Button>
1806
+ </div>
1807
+ );
1808
+ }
1809
+
1810
+ // The primary "Content" Blocks field IS the document body,
1811
+ // with the full collaborative editor. It renders chromeless
1812
+ // when it's the only Blocks field, or inside a
1813
+ // header/collapsible shell when the row has multiple Blocks
1814
+ // fields.
1815
+ const primaryEditor = (
1816
+ <VisualEditor
1817
+ key={`${documentId}:${editorCanEdit && !isLocalFileDocument ? "live" : "snapshot"}`}
1543
1818
  documentId={documentId}
1544
- canEdit={editorCanEdit}
1545
- primaryEditor={primaryEditor}
1819
+ content={
1820
+ isLocalFileDocument
1821
+ ? localContent
1822
+ : document.content
1823
+ }
1824
+ contentUpdatedAt={
1825
+ isLocalFileDocument
1826
+ ? (localContentUpdatedAt ?? document.updatedAt)
1827
+ : document.updatedAt
1828
+ }
1829
+ onChange={handleContentChange}
1830
+ onSaveContent={handleContentSaveNow}
1831
+ // Bind the shared Y.Doc/awareness for viewers too — the
1832
+ // editor is non-editable for them and VisualEditor blocks
1833
+ // any local Y.Doc mutation, so they get live edits +
1834
+ // cursors without ever writing. Excludes local-file docs.
1835
+ ydoc={collabEnabled ? ydoc : null}
1836
+ collabSynced={collabEnabled ? collabSynced : true}
1837
+ awareness={collabEnabled ? awareness : null}
1838
+ user={currentUser}
1839
+ editable={editorCanEdit}
1840
+ localFileMode={isLocalFileDocument}
1841
+ localFilePath={
1842
+ isLocalFileDocument ? document.source?.path : null
1843
+ }
1844
+ onComment={
1845
+ editorCanEdit && !isLocalFileDocument
1846
+ ? handleComment
1847
+ : undefined
1848
+ }
1849
+ commentThreads={threads ?? []}
1850
+ activeThreadId={activeThreadId}
1851
+ pendingHighlight={pendingComment?.range ?? null}
1852
+ onActivateThread={
1853
+ editorCanEdit && !isLocalFileDocument
1854
+ ? setSelectedThreadId
1855
+ : undefined
1856
+ }
1857
+ onJoinTitle={joinFirstBodyBlockToTitle}
1858
+ notionPageLinks={notionPageLinks}
1859
+ onOpenNotionPageLink={handleOpenNotionPageLink}
1860
+ notionPageId={document.notionPageId}
1546
1861
  />
1547
1862
  );
1548
- }
1549
1863
 
1550
- return primaryEditor;
1551
- })()}
1552
- {!bodyHydrationPending &&
1553
- !isLocalFileDocument &&
1554
- collabLoading ? (
1555
- <div
1556
- className="mt-4 inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
1557
- role="status"
1558
- >
1559
- <IconLoader2 className="size-3.5 animate-spin" />
1560
- {t("editor.collabConnectingReadOnly")}
1561
- </div>
1562
- ) : null}
1563
- </div>
1864
+ // Only database rows have Blocks fields. Standalone pages
1865
+ // and local-file documents keep the plain chromeless body.
1866
+ if (document.databaseMembership && !isLocalFileDocument) {
1867
+ return (
1868
+ <DocumentBlockFields
1869
+ documentId={documentId}
1870
+ canEdit={editorCanEdit}
1871
+ primaryEditor={primaryEditor}
1872
+ />
1873
+ );
1874
+ }
1875
+
1876
+ return primaryEditor;
1877
+ })()}
1878
+ {!bodyHydrationPending &&
1879
+ !isLocalFileDocument &&
1880
+ collabLoading ? (
1881
+ <div
1882
+ className="mt-4 inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
1883
+ role="status"
1884
+ >
1885
+ <IconLoader2 className="size-3.5 animate-spin" />
1886
+ {t("editor.collabConnectingReadOnly")}
1887
+ </div>
1888
+ ) : null}
1889
+ </div>
1890
+ ) : null}
1564
1891
  </div>
1565
1892
 
1566
- {showDesktopComments ? sidebar : null}
1893
+ {showDesktopUtilityPanel ? (
1894
+ <aside className="w-80 shrink-0 border-s border-border">
1895
+ {utilityPanelContent}
1896
+ </aside>
1897
+ ) : null}
1567
1898
  </div>
1568
1899
  </div>
1569
1900
  </div>
1570
1901
 
1571
- {showCommentsSheet ? (
1902
+ {showUtilityPanelSheet ? (
1572
1903
  <Sheet
1573
- open={hasComments}
1904
+ open={utilityPanel !== null}
1574
1905
  onOpenChange={(open) => {
1575
- if (!open) setPendingComment(null);
1906
+ if (!open) {
1907
+ setUtilityPanel(null);
1908
+ setPendingComment(null);
1909
+ }
1576
1910
  }}
1577
1911
  >
1578
- <SheetContent side="right" className="w-[85vw] max-w-sm p-0">
1579
- {sidebar}
1912
+ <SheetContent
1913
+ side="right"
1914
+ className="w-[85vw] max-w-sm p-0"
1915
+ aria-describedby={undefined}
1916
+ >
1917
+ <SheetHeader className="sr-only">
1918
+ <SheetTitle>{utilityPanelTitle}</SheetTitle>
1919
+ </SheetHeader>
1920
+ {utilityPanelContent}
1580
1921
  </SheetContent>
1581
1922
  </Sheet>
1582
1923
  ) : null}