@agent-native/core 0.77.10 → 0.77.12

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 (60) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/cli/create.ts +65 -15
  5. package/corpus/core/src/cli/sync-builder-starter-manifest.ts +261 -65
  6. package/corpus/core/src/collab/awareness.ts +12 -9
  7. package/corpus/core/src/collab/client.ts +4 -2
  8. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  9. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  10. package/corpus/templates/clips/desktop/src/app.tsx +35 -19
  11. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +38 -1
  12. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +343 -14
  14. package/corpus/templates/content/actions/list-trashed-content-databases.ts +36 -2
  15. package/corpus/templates/content/actions/move-document.ts +79 -33
  16. package/corpus/templates/content/actions/restore-content-database.ts +40 -3
  17. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +84 -2
  18. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +177 -49
  19. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +393 -219
  20. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +632 -467
  21. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +114 -14
  22. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +56 -15
  23. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +6 -5
  24. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +12 -4
  25. package/corpus/templates/content/app/global.css +9 -0
  26. package/corpus/templates/content/app/hooks/use-create-page.ts +36 -7
  27. package/corpus/templates/content/app/i18n-data.ts +140 -10
  28. package/corpus/templates/content/changelog/2026-06-25-comment-cards-now-track-their-highlighted-text-while-scrolli.md +6 -0
  29. package/corpus/templates/content/changelog/2026-06-25-comments-now-scroll-with-the-document-and-stay-below-the-pag.md +6 -0
  30. package/corpus/templates/content/changelog/2026-06-25-creating-a-database-from-the-slash-menu-no-longer-leaves-sta.md +6 -0
  31. package/corpus/templates/content/changelog/2026-06-25-sidebar-hover-controls-no-longer-leave-a-lingering-fade-when.md +6 -0
  32. package/corpus/templates/content/changelog/2026-06-25-sidebar-page-actions-are-now-easier-to-see-when-hovering-ina.md +6 -0
  33. package/corpus/templates/content/changelog/2026-06-25-text-selections-in-the-editor-no-longer-show-white-gaps-when.md +6 -0
  34. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-has-a-one-click-page-link-copy-button.md +6 -0
  35. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-shows-page-breadcrumbs-and-the-latest.md +6 -0
  36. package/corpus/templates/content/changelog/2026-06-25-the-page-command-now-leaves-a-notion-style-page-reference-an.md +6 -0
  37. package/corpus/templates/content/shared/nfm.ts +16 -0
  38. package/dist/cli/create.d.ts.map +1 -1
  39. package/dist/cli/create.js +53 -15
  40. package/dist/cli/create.js.map +1 -1
  41. package/dist/cli/sync-builder-starter-manifest.d.ts +36 -2
  42. package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -1
  43. package/dist/cli/sync-builder-starter-manifest.js +180 -37
  44. package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
  45. package/dist/collab/awareness.d.ts +1 -1
  46. package/dist/collab/awareness.d.ts.map +1 -1
  47. package/dist/collab/awareness.js +12 -8
  48. package/dist/collab/awareness.js.map +1 -1
  49. package/dist/collab/client.d.ts.map +1 -1
  50. package/dist/collab/client.js +4 -3
  51. package/dist/collab/client.js.map +1 -1
  52. package/dist/collab/routes.d.ts +1 -1
  53. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  54. package/dist/notifications/routes.d.ts +2 -2
  55. package/dist/observability/routes.d.ts +5 -5
  56. package/dist/progress/routes.d.ts +1 -1
  57. package/dist/resources/handlers.d.ts +3 -3
  58. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  59. package/dist/server/transcribe-voice.d.ts +1 -1
  60. package/package.json +3 -3
@@ -40,7 +40,6 @@ import {
40
40
  useUpdateDocument,
41
41
  } from "@/hooks/use-documents";
42
42
  import { useLocalStorage } from "@/hooks/use-local-storage";
43
- import { useIsMobile } from "@/hooks/use-mobile";
44
43
  import { useDocumentSyncStatus } from "@/hooks/use-notion";
45
44
  import {
46
45
  canWriteLinkedLocalSource,
@@ -155,6 +154,32 @@ interface DocumentEditorBodyProps {
155
154
  document: Document;
156
155
  }
157
156
 
157
+ type PendingDocumentSave = {
158
+ title: string;
159
+ content: string;
160
+ save: (title: string, content: string) => unknown | Promise<unknown>;
161
+ timeout: ReturnType<typeof setTimeout>;
162
+ };
163
+
164
+ type DocumentSaveResult = {
165
+ contentPersisted: boolean;
166
+ };
167
+
168
+ function useMinViewportWidth(minWidth: number) {
169
+ const [matches, setMatches] = useState(false);
170
+
171
+ useEffect(() => {
172
+ if (typeof window === "undefined") return;
173
+ const query = window.matchMedia(`(min-width: ${minWidth}px)`);
174
+ const update = () => setMatches(query.matches);
175
+ update();
176
+ query.addEventListener("change", update);
177
+ return () => query.removeEventListener("change", update);
178
+ }, [minWidth]);
179
+
180
+ return matches;
181
+ }
182
+
158
183
  export function documentEditorTitleRegionClassName(hasDatabase: boolean) {
159
184
  if (hasDatabase) {
160
185
  return cn(
@@ -184,6 +209,38 @@ export function databaseMembershipDatabaseTitle(
184
209
  return membership?.databaseTitle?.trim() || "Untitled database";
185
210
  }
186
211
 
212
+ export function documentEditorBreadcrumbItems(
213
+ document: Pick<Document, "id" | "parentId" | "title" | "icon">, // i18n-ignore type expression
214
+ documents: Pick<Document, "id" | "parentId" | "title" | "icon">[], // i18n-ignore type expression
215
+ ) {
216
+ const byId = new Map(documents.map((doc) => [doc.id, doc]));
217
+ const parents: { id: string; title: string; icon: string | null }[] = [];
218
+ const seen = new Set<string>([document.id]);
219
+ let parentId = document.parentId;
220
+
221
+ while (parentId) {
222
+ if (seen.has(parentId)) break;
223
+ seen.add(parentId);
224
+ const parent = byId.get(parentId);
225
+ if (!parent) break;
226
+ parents.unshift({
227
+ id: parent.id,
228
+ title: parent.title,
229
+ icon: parent.icon,
230
+ });
231
+ parentId = parent.parentId;
232
+ }
233
+
234
+ return [
235
+ ...parents,
236
+ {
237
+ id: document.id,
238
+ title: document.title,
239
+ icon: document.icon,
240
+ },
241
+ ];
242
+ }
243
+
187
244
  function DatabaseMembershipBreadcrumb({
188
245
  document,
189
246
  onOpenDatabase,
@@ -246,6 +303,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
246
303
  string | null
247
304
  >(document.updatedAt ?? null);
248
305
  const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
306
+ const pendingDocumentSaveRef = useRef<PendingDocumentSave | null>(null);
249
307
  // Separate freshness watermarks for title and content so that a content save
250
308
  // never suppresses adopting a newer external title and vice versa.
251
309
  const lastSavedTitleRef = useRef<{ title: string; updatedAt: string | null }>(
@@ -272,18 +330,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
272
330
  const shouldFocusTitleRef = useRef(false);
273
331
  const notionPageLinks = useMemo<NotionPageLink[]>(
274
332
  () =>
275
- documents.flatMap((doc) =>
276
- doc.notionPageId
277
- ? [
278
- {
279
- notionPageId: doc.notionPageId,
280
- documentId: doc.id,
281
- title: doc.title || "Untitled",
282
- icon: doc.icon,
283
- },
284
- ]
285
- : [],
286
- ),
333
+ documents.map((doc) => ({
334
+ notionPageId: doc.notionPageId || doc.id,
335
+ documentId: doc.id,
336
+ title: doc.title || "Untitled",
337
+ icon: doc.icon,
338
+ })),
287
339
  [documents],
288
340
  );
289
341
  const handleOpenNotionPageLink = useCallback(
@@ -346,6 +398,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
346
398
  if (saveTimeoutRef.current) {
347
399
  clearTimeout(saveTimeoutRef.current);
348
400
  saveTimeoutRef.current = null;
401
+ pendingDocumentSaveRef.current = null;
349
402
  }
350
403
  }
351
404
  if (!isInitializedRef.current) {
@@ -473,6 +526,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
473
526
  if (saveTimeoutRef.current) {
474
527
  clearTimeout(saveTimeoutRef.current);
475
528
  saveTimeoutRef.current = null;
529
+ pendingDocumentSaveRef.current = null;
476
530
  }
477
531
  setLocalContent(serverContent);
478
532
  lastSavedContentRef.current = {
@@ -574,67 +628,69 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
574
628
  [document, documentId, queryClient, updateDocument],
575
629
  );
576
630
 
577
- const debouncedSave = useCallback(
578
- (title: string, content: string) => {
579
- if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
580
- saveTimeoutRef.current = setTimeout(async () => {
581
- // Never clobber a newer server version (e.g. an agent edit we haven't
582
- // reconciled into the editor yet) with the editor's current — possibly
583
- // stale — content. Guard per-field using the field's own watermark.
584
- const titleIsStale =
585
- !isLinkedLocalSourceDocument &&
586
- documentUpdatedAtRef.current &&
587
- lastSavedTitleRef.current.updatedAt &&
588
- documentUpdatedAtRef.current > lastSavedTitleRef.current.updatedAt;
589
- const contentIsStale =
590
- !isLinkedLocalSourceDocument &&
591
- documentUpdatedAtRef.current &&
592
- lastSavedContentRef.current.updatedAt &&
593
- documentUpdatedAtRef.current > lastSavedContentRef.current.updatedAt;
594
-
595
- const updates: Record<string, string> = {};
596
- if (title !== lastSavedTitleRef.current.title && !titleIsStale)
597
- updates.title = title;
598
- if (content !== lastSavedContentRef.current.content && !contentIsStale)
599
- updates.content = content;
600
- if (Object.keys(updates).length === 0) return;
601
-
602
- const saved = await persistDocumentUpdates(updates);
603
- // Adopt the server updatedAt per saved field.
604
- const savedAt = saved?.updatedAt ?? new Date().toISOString();
605
- if (updates.title !== undefined) {
606
- lastSavedTitleRef.current = { title, updatedAt: savedAt };
607
- }
608
- if (updates.content !== undefined) {
609
- lastSavedContentRef.current = { content, updatedAt: savedAt };
610
- }
631
+ const saveDocumentImmediately = useCallback(
632
+ async (title: string, content: string): Promise<DocumentSaveResult> => {
633
+ // Never clobber a newer server version (e.g. an agent edit we haven't
634
+ // reconciled into the editor yet) with the editor's current — possibly
635
+ // stale content. Guard per-field using the field's own watermark.
636
+ const titleIsStale =
637
+ !isLinkedLocalSourceDocument &&
638
+ documentUpdatedAtRef.current &&
639
+ lastSavedTitleRef.current.updatedAt &&
640
+ documentUpdatedAtRef.current > lastSavedTitleRef.current.updatedAt;
641
+ const contentIsStale =
642
+ !isLinkedLocalSourceDocument &&
643
+ documentUpdatedAtRef.current &&
644
+ lastSavedContentRef.current.updatedAt &&
645
+ documentUpdatedAtRef.current > lastSavedContentRef.current.updatedAt;
646
+
647
+ const updates: Record<string, string> = {};
648
+ if (title !== lastSavedTitleRef.current.title && !titleIsStale)
649
+ updates.title = title;
650
+ const contentChanged = content !== lastSavedContentRef.current.content;
651
+ if (contentChanged && !contentIsStale) updates.content = content;
652
+ if (Object.keys(updates).length === 0) {
653
+ return { contentPersisted: !contentChanged };
654
+ }
611
655
 
612
- // Push-on-save: when auto-sync is on, trigger a Notion push
613
- // immediately after the save lands in SQL. This eliminates the
614
- // off-by-one race where a fixed-interval poll could fire between
615
- // the debounce and the next save, reading the previous content.
616
- // Pulls remain driven by the polling refetch in useDocumentSyncStatus.
617
- if (autoSync) {
618
- const status = queryClient.getQueryData<DocumentSyncStatus>([
619
- "document-sync",
620
- documentId,
621
- ]);
622
- if (status?.pageId && !status.hasConflict) {
623
- try {
624
- const res = await fetch(
625
- appApiPath(`/api/documents/${documentId}/notion/push`),
626
- { method: "POST" },
627
- );
628
- if (res.ok) {
629
- const next = (await res.json()) as DocumentSyncStatus;
630
- queryClient.setQueryData(["document-sync", documentId], next);
631
- }
632
- } catch {
633
- // Non-fatal — next polling refetch will surface any error.
656
+ const saved = await persistDocumentUpdates(updates);
657
+ // Adopt the server updatedAt per saved field.
658
+ const savedAt = saved?.updatedAt ?? new Date().toISOString();
659
+ if (updates.title !== undefined) {
660
+ lastSavedTitleRef.current = { title, updatedAt: savedAt };
661
+ }
662
+ if (updates.content !== undefined) {
663
+ lastSavedContentRef.current = { content, updatedAt: savedAt };
664
+ }
665
+
666
+ // Push-on-save: when auto-sync is on, trigger a Notion push
667
+ // immediately after the save lands in SQL. This eliminates the
668
+ // off-by-one race where a fixed-interval poll could fire between
669
+ // the debounce and the next save, reading the previous content.
670
+ // Pulls remain driven by the polling refetch in useDocumentSyncStatus.
671
+ if (autoSync) {
672
+ const status = queryClient.getQueryData<DocumentSyncStatus>([
673
+ "document-sync",
674
+ documentId,
675
+ ]);
676
+ if (status?.pageId && !status.hasConflict) {
677
+ try {
678
+ const res = await fetch(
679
+ appApiPath(`/api/documents/${documentId}/notion/push`),
680
+ { method: "POST" },
681
+ );
682
+ if (res.ok) {
683
+ const next = (await res.json()) as DocumentSyncStatus;
684
+ queryClient.setQueryData(["document-sync", documentId], next);
634
685
  }
686
+ } catch {
687
+ // Non-fatal — next polling refetch will surface any error.
635
688
  }
636
689
  }
637
- }, 500);
690
+ }
691
+ return {
692
+ contentPersisted: !contentChanged || updates.content !== undefined,
693
+ };
638
694
  },
639
695
  [
640
696
  documentId,
@@ -644,6 +700,37 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
644
700
  queryClient,
645
701
  ],
646
702
  );
703
+ const debouncedSave = useCallback(
704
+ (title: string, content: string) => {
705
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
706
+ const pending: PendingDocumentSave = {
707
+ title,
708
+ content,
709
+ save: saveDocumentImmediately,
710
+ timeout: setTimeout(() => {
711
+ if (pendingDocumentSaveRef.current === pending) {
712
+ pendingDocumentSaveRef.current = null;
713
+ }
714
+ saveTimeoutRef.current = null;
715
+ void pending.save(pending.title, pending.content);
716
+ }, 500),
717
+ };
718
+ pendingDocumentSaveRef.current = pending;
719
+ saveTimeoutRef.current = pending.timeout;
720
+ },
721
+ [saveDocumentImmediately],
722
+ );
723
+
724
+ useEffect(() => {
725
+ return () => {
726
+ const pending = pendingDocumentSaveRef.current;
727
+ if (!pending) return;
728
+ clearTimeout(pending.timeout);
729
+ saveTimeoutRef.current = null;
730
+ pendingDocumentSaveRef.current = null;
731
+ void pending.save(pending.title, pending.content);
732
+ };
733
+ }, [documentId]);
647
734
 
648
735
  // Collab-aware ingest flush: the `pull-document` action writes a one-shot
649
736
  // `flush-request-<id>` app-state key when an external agent wants to ingest
@@ -729,6 +816,25 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
729
816
  [canEdit, debouncedSave],
730
817
  );
731
818
 
819
+ const handleContentSaveNow = useCallback(
820
+ async (newContent: string) => {
821
+ if (!canEdit) return false;
822
+ if (saveTimeoutRef.current) {
823
+ clearTimeout(saveTimeoutRef.current);
824
+ saveTimeoutRef.current = null;
825
+ pendingDocumentSaveRef.current = null;
826
+ }
827
+ localContentRef.current = newContent;
828
+ setLocalContent(newContent);
829
+ const result = await saveDocumentImmediately(
830
+ localTitleRef.current,
831
+ newContent,
832
+ );
833
+ return result.contentPersisted;
834
+ },
835
+ [canEdit, saveDocumentImmediately],
836
+ );
837
+
732
838
  // Comments state — pending comment from text selection
733
839
  const [pendingComment, setPendingComment] = useState<{
734
840
  quotedText: string;
@@ -736,17 +842,19 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
736
842
  anchor?: CommentTextAnchor;
737
843
  range?: { from: number; to: number };
738
844
  } | null>(null);
739
- // The thread whose highlight + card are currently focused (click an inline
740
- // highlight to focus its card; hover a card to emphasize its highlight).
741
- const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
742
- const { data: threads } = useComments(
845
+ const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
846
+ const [hoveredThreadId, setHoveredThreadId] = useState<string | null>(null);
847
+ const activeThreadId = hoveredThreadId ?? selectedThreadId;
848
+ const { data: threads, isLoading: commentsLoading } = useComments(
743
849
  isLocalFileDocument ? null : documentId,
744
850
  );
745
851
  const hasComments =
746
852
  !isLocalFileDocument &&
747
853
  canEdit &&
748
854
  ((threads?.length ?? 0) > 0 || !!pendingComment);
749
- const isMobile = useIsMobile();
855
+ const hasCommentRailSpace = useMinViewportWidth(1024);
856
+ const showDesktopComments = hasComments && hasCommentRailSpace;
857
+ const showCommentsSheet = hasComments && canEdit && !showDesktopComments;
750
858
 
751
859
  const handleComment = useCallback(
752
860
  (
@@ -756,11 +864,30 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
756
864
  range?: { from: number; to: number },
757
865
  ) => {
758
866
  setPendingComment({ quotedText, offsetTop, anchor, range });
759
- setActiveThreadId(null);
867
+ setSelectedThreadId(null);
868
+ setHoveredThreadId(null);
760
869
  },
761
870
  [],
762
871
  );
763
872
 
873
+ const clearCommentFocus = useCallback(() => {
874
+ setSelectedThreadId(null);
875
+ setHoveredThreadId(null);
876
+ }, []);
877
+
878
+ useEffect(() => {
879
+ setPendingComment(null);
880
+ clearCommentFocus();
881
+ }, [clearCommentFocus, documentId]);
882
+
883
+ useEffect(() => {
884
+ const handleKeyDown = (event: KeyboardEvent) => {
885
+ if (event.key === "Escape") clearCommentFocus();
886
+ };
887
+ window.addEventListener("keydown", handleKeyDown);
888
+ return () => window.removeEventListener("keydown", handleKeyDown);
889
+ }, [clearCommentFocus]);
890
+
764
891
  const focusTitleEnd = useCallback(() => {
765
892
  const textarea = titleInputRef.current;
766
893
  if (!textarea) return;
@@ -816,6 +943,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
816
943
  }
817
944
  });
818
945
 
946
+ const toolbarBreadcrumbItems = useMemo(
947
+ () => documentEditorBreadcrumbItems(document, documents),
948
+ [document, documents],
949
+ );
950
+
819
951
  if (!isLocalFileDocument && collabLoading) {
820
952
  return <DocumentEditorSkeleton />;
821
953
  }
@@ -823,11 +955,15 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
823
955
  const sidebar = (
824
956
  <CommentsSidebar
825
957
  documentId={documentId}
958
+ threads={threads ?? []}
959
+ isLoading={commentsLoading}
826
960
  pendingComment={pendingComment}
827
961
  onPendingDone={() => setPendingComment(null)}
828
962
  scrollContainerRef={scrollContainerRef}
829
963
  activeThreadId={activeThreadId}
830
- onActiveThreadChange={setActiveThreadId}
964
+ selectedThreadId={selectedThreadId}
965
+ onSelectedThreadChange={setSelectedThreadId}
966
+ onHoveredThreadChange={setHoveredThreadId}
831
967
  currentUserEmail={session?.email}
832
968
  />
833
969
  );
@@ -850,12 +986,25 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
850
986
  <div
851
987
  className="relative flex min-h-0 min-w-0 flex-1"
852
988
  data-document-print-root
989
+ onPointerDownCapture={(event) => {
990
+ const target = event.target as HTMLElement | null;
991
+ if (
992
+ target?.closest("[data-comments-sidebar], [data-comment-thread]")
993
+ ) {
994
+ return;
995
+ }
996
+ clearCommentFocus();
997
+ }}
853
998
  >
854
999
  <div className="flex min-h-0 min-w-0 flex-1 flex-col">
855
1000
  <DocumentToolbar
856
1001
  documentId={documentId}
857
1002
  documentTitle={exportTitle}
858
1003
  documentContent={exportContent}
1004
+ breadcrumbItems={toolbarBreadcrumbItems.map((item) =>
1005
+ item.id === documentId ? { ...item, title: exportTitle } : item,
1006
+ )}
1007
+ documentUpdatedAt={document.updatedAt}
859
1008
  activeUsers={activeUsers}
860
1009
  agentPresent={agentPresent}
861
1010
  agentActive={agentActive}
@@ -875,160 +1024,187 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
875
1024
  data-document-print-scroll
876
1025
  >
877
1026
  <div
878
- className={documentEditorTitleRegionClassName(
879
- Boolean(document.database),
1027
+ className={cn(
1028
+ "flex min-h-full w-full min-w-0",
1029
+ showDesktopComments ? "justify-center" : "flex-col",
880
1030
  )}
1031
+ data-document-scroll-content
881
1032
  >
882
- <DatabaseMembershipBreadcrumb
883
- document={document}
884
- onOpenDatabase={(databaseDocumentId) =>
885
- navigate(`/page/${databaseDocumentId}`, { flushSync: true })
886
- }
887
- />
888
- {document.icon || !isDatabasePage ? (
889
- <div className="mb-1">
890
- {canEdit ? (
891
- <EmojiPicker
892
- icon={document.icon}
893
- defaultIcon={defaultIcon}
894
- defaultIconLabel={
895
- defaultIconKind === "database" ? "database" : "page"
896
- }
897
- onSelect={(emoji) => {
898
- void (async () => {
899
- await persistDocumentUpdates({ icon: emoji });
900
- })();
901
- }}
902
- />
903
- ) : document.icon ? (
904
- <div className="p-1 -ml-1 text-5xl leading-none">
905
- {document.icon}
906
- </div>
907
- ) : defaultIconKind === "database" && !isDatabasePage ? (
908
- <div className="-ml-1 flex size-14 items-center justify-center rounded-md text-muted-foreground">
909
- <IconDatabase className="size-12" aria-hidden="true" />
910
- </div>
911
- ) : null}
912
- </div>
913
- ) : null}
914
- <textarea
915
- ref={titleInputRef}
916
- rows={1}
917
- wrap="soft"
918
- value={localTitle}
919
- onChange={(e) =>
920
- handleTitleChange(normalizeTitleText(e.target.value))
921
- }
922
- onPaste={handleTitlePaste}
923
- onFocus={() => {
924
- titleFocusedRef.current = true;
925
- }}
926
- onBlur={() => {
927
- titleFocusedRef.current = false;
928
- }}
929
- onKeyDown={(e) => {
930
- if (e.key === "Enter") {
931
- e.preventDefault();
932
- const pm = window.document.querySelector(
933
- ".ProseMirror",
934
- ) as HTMLElement | null;
935
- pm?.focus();
936
- }
937
- }}
938
- aria-label={t("editor.documentTitle")}
939
- placeholder={t("editor.title")}
940
- readOnly={!canEdit}
941
- style={{ fieldSizing: "content" } as any}
1033
+ <div
942
1034
  className={cn(
943
- "block w-full resize-none overflow-hidden break-words border-none bg-transparent p-0 font-bold leading-tight text-foreground outline-none placeholder:text-muted-foreground/40",
944
- isDatabasePage ? "text-3xl" : "text-3xl md:text-4xl",
1035
+ "min-w-0",
1036
+ showDesktopComments ? "flex-1" : "w-full",
945
1037
  )}
946
- />
947
- {document.databaseMembership && !isLocalFileDocument ? (
948
- <DocumentProperties documentId={documentId} canEdit={canEdit} />
949
- ) : null}
950
- </div>
951
- {document.database ? (
952
- <div className={documentEditorDatabaseRegionClassName()}>
953
- <DocumentDatabase document={document} canEdit={canEdit} />
954
- </div>
955
- ) : null}
956
-
957
- <div
958
- className="flex-1 w-full max-w-3xl mx-auto px-4 pb-16 cursor-text sm:px-8 md:px-16"
959
- onClick={(e) => {
960
- if (e.target === e.currentTarget) {
961
- const pm = e.currentTarget.querySelector(
962
- ".ProseMirror",
963
- ) as HTMLElement | null;
964
- pm?.focus();
965
- }
966
- }}
967
- >
968
- {(() => {
969
- // The primary "Content" Blocks field IS the document body, with
970
- // the full collaborative editor. It renders chromeless when it's
971
- // the only Blocks field, or inside a header/collapsible shell
972
- // when the row has multiple Blocks fields.
973
- const primaryEditor = (
974
- <VisualEditor
975
- key={documentId}
976
- documentId={documentId}
977
- content={
978
- isLocalFileDocument ? localContent : document.content
979
- }
980
- contentUpdatedAt={
981
- isLocalFileDocument
982
- ? (localContentUpdatedAt ?? document.updatedAt)
983
- : document.updatedAt
1038
+ >
1039
+ <div
1040
+ className={documentEditorTitleRegionClassName(
1041
+ Boolean(document.database),
1042
+ )}
1043
+ >
1044
+ <DatabaseMembershipBreadcrumb
1045
+ document={document}
1046
+ onOpenDatabase={(databaseDocumentId) =>
1047
+ navigate(`/page/${databaseDocumentId}`, {
1048
+ flushSync: true,
1049
+ })
984
1050
  }
985
- onChange={handleContentChange}
986
- ydoc={canEdit && !isLocalFileDocument ? ydoc : null}
987
- awareness={
988
- canEdit && !isLocalFileDocument ? awareness : null
989
- }
990
- user={currentUser}
991
- editable={canEdit}
992
- localFileMode={isLocalFileDocument}
993
- onComment={
994
- canEdit && !isLocalFileDocument
995
- ? handleComment
996
- : undefined
997
- }
998
- commentThreads={threads ?? []}
999
- activeThreadId={activeThreadId}
1000
- pendingHighlight={pendingComment?.range ?? null}
1001
- onActivateThread={
1002
- canEdit && !isLocalFileDocument
1003
- ? setActiveThreadId
1004
- : undefined
1051
+ />
1052
+ {document.icon || !isDatabasePage ? (
1053
+ <div className="mb-1">
1054
+ {canEdit ? (
1055
+ <EmojiPicker
1056
+ icon={document.icon}
1057
+ defaultIcon={defaultIcon}
1058
+ defaultIconLabel={
1059
+ defaultIconKind === "database" ? "database" : "page"
1060
+ }
1061
+ onSelect={(emoji) => {
1062
+ void (async () => {
1063
+ await persistDocumentUpdates({ icon: emoji });
1064
+ })();
1065
+ }}
1066
+ />
1067
+ ) : document.icon ? (
1068
+ <div className="p-1 -ml-1 text-5xl leading-none">
1069
+ {document.icon}
1070
+ </div>
1071
+ ) : defaultIconKind === "database" && !isDatabasePage ? (
1072
+ <div className="-ml-1 flex size-14 items-center justify-center rounded-md text-muted-foreground">
1073
+ <IconDatabase
1074
+ className="size-12"
1075
+ aria-hidden="true"
1076
+ />
1077
+ </div>
1078
+ ) : null}
1079
+ </div>
1080
+ ) : null}
1081
+ <textarea
1082
+ ref={titleInputRef}
1083
+ rows={1}
1084
+ wrap="soft"
1085
+ value={localTitle}
1086
+ onChange={(e) =>
1087
+ handleTitleChange(normalizeTitleText(e.target.value))
1005
1088
  }
1006
- onJoinTitle={joinFirstBodyBlockToTitle}
1007
- notionPageLinks={notionPageLinks}
1008
- onOpenNotionPageLink={handleOpenNotionPageLink}
1009
- notionPageId={document.notionPageId}
1089
+ onPaste={handleTitlePaste}
1090
+ onFocus={() => {
1091
+ titleFocusedRef.current = true;
1092
+ }}
1093
+ onBlur={() => {
1094
+ titleFocusedRef.current = false;
1095
+ }}
1096
+ onKeyDown={(e) => {
1097
+ if (e.key === "Enter") {
1098
+ e.preventDefault();
1099
+ const pm = window.document.querySelector(
1100
+ ".ProseMirror",
1101
+ ) as HTMLElement | null;
1102
+ pm?.focus();
1103
+ }
1104
+ }}
1105
+ aria-label={t("editor.documentTitle")}
1106
+ placeholder={t("editor.title")}
1107
+ readOnly={!canEdit}
1108
+ style={{ fieldSizing: "content" } as any}
1109
+ className={cn(
1110
+ "block w-full resize-none overflow-hidden break-words border-none bg-transparent p-0 font-bold leading-tight text-foreground outline-none placeholder:text-muted-foreground/40",
1111
+ isDatabasePage ? "text-3xl" : "text-3xl md:text-4xl",
1112
+ )}
1010
1113
  />
1011
- );
1012
-
1013
- // Only database rows have Blocks fields. Standalone pages and
1014
- // local-file documents keep the plain chromeless body.
1015
- if (document.databaseMembership && !isLocalFileDocument) {
1016
- return (
1017
- <DocumentBlockFields
1114
+ {document.databaseMembership && !isLocalFileDocument ? (
1115
+ <DocumentProperties
1018
1116
  documentId={documentId}
1019
1117
  canEdit={canEdit}
1020
- primaryEditor={primaryEditor}
1021
1118
  />
1022
- );
1023
- }
1119
+ ) : null}
1120
+ </div>
1121
+ {document.database ? (
1122
+ <div className={documentEditorDatabaseRegionClassName()}>
1123
+ <DocumentDatabase document={document} canEdit={canEdit} />
1124
+ </div>
1125
+ ) : null}
1126
+
1127
+ <div
1128
+ className="flex-1 w-full max-w-3xl mx-auto px-4 pb-16 cursor-text sm:px-8 md:px-16"
1129
+ onClick={(e) => {
1130
+ if (e.target === e.currentTarget) {
1131
+ const pm = e.currentTarget.querySelector(
1132
+ ".ProseMirror",
1133
+ ) as HTMLElement | null;
1134
+ pm?.focus();
1135
+ }
1136
+ }}
1137
+ >
1138
+ {(() => {
1139
+ // The primary "Content" Blocks field IS the document body,
1140
+ // with the full collaborative editor. It renders chromeless
1141
+ // when it's the only Blocks field, or inside a
1142
+ // header/collapsible shell when the row has multiple Blocks
1143
+ // fields.
1144
+ const primaryEditor = (
1145
+ <VisualEditor
1146
+ key={documentId}
1147
+ documentId={documentId}
1148
+ content={
1149
+ isLocalFileDocument ? localContent : document.content
1150
+ }
1151
+ contentUpdatedAt={
1152
+ isLocalFileDocument
1153
+ ? (localContentUpdatedAt ?? document.updatedAt)
1154
+ : document.updatedAt
1155
+ }
1156
+ onChange={handleContentChange}
1157
+ onSaveContent={handleContentSaveNow}
1158
+ ydoc={canEdit && !isLocalFileDocument ? ydoc : null}
1159
+ awareness={
1160
+ canEdit && !isLocalFileDocument ? awareness : null
1161
+ }
1162
+ user={currentUser}
1163
+ editable={canEdit}
1164
+ localFileMode={isLocalFileDocument}
1165
+ onComment={
1166
+ canEdit && !isLocalFileDocument
1167
+ ? handleComment
1168
+ : undefined
1169
+ }
1170
+ commentThreads={threads ?? []}
1171
+ activeThreadId={activeThreadId}
1172
+ pendingHighlight={pendingComment?.range ?? null}
1173
+ onActivateThread={
1174
+ canEdit && !isLocalFileDocument
1175
+ ? setSelectedThreadId
1176
+ : undefined
1177
+ }
1178
+ onJoinTitle={joinFirstBodyBlockToTitle}
1179
+ notionPageLinks={notionPageLinks}
1180
+ onOpenNotionPageLink={handleOpenNotionPageLink}
1181
+ notionPageId={document.notionPageId}
1182
+ />
1183
+ );
1184
+
1185
+ // Only database rows have Blocks fields. Standalone pages
1186
+ // and local-file documents keep the plain chromeless body.
1187
+ if (document.databaseMembership && !isLocalFileDocument) {
1188
+ return (
1189
+ <DocumentBlockFields
1190
+ documentId={documentId}
1191
+ canEdit={canEdit}
1192
+ primaryEditor={primaryEditor}
1193
+ />
1194
+ );
1195
+ }
1196
+
1197
+ return primaryEditor;
1198
+ })()}
1199
+ </div>
1200
+ </div>
1024
1201
 
1025
- return primaryEditor;
1026
- })()}
1202
+ {showDesktopComments ? sidebar : null}
1027
1203
  </div>
1028
1204
  </div>
1029
1205
  </div>
1030
1206
 
1031
- {isMobile && canEdit ? (
1207
+ {showCommentsSheet ? (
1032
1208
  <Sheet
1033
1209
  open={hasComments}
1034
1210
  onOpenChange={(open) => {
@@ -1039,9 +1215,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1039
1215
  {sidebar}
1040
1216
  </SheetContent>
1041
1217
  </Sheet>
1042
- ) : (
1043
- hasComments && sidebar
1044
- )}
1218
+ ) : null}
1045
1219
  </div>
1046
1220
  </BlockRegistryProvider>
1047
1221
  );