@agent-native/core 0.128.0 → 0.128.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 (132) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +27 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/server/builder-design-systems.ts +299 -82
  5. package/corpus/core/src/server/email-template.ts +33 -0
  6. package/corpus/core/src/server/index.ts +8 -0
  7. package/corpus/templates/analytics/_gitignore +2 -0
  8. package/corpus/templates/assets/_gitignore +2 -0
  9. package/corpus/templates/calendar/_gitignore +2 -0
  10. package/corpus/templates/chat/_gitignore +2 -0
  11. package/corpus/templates/clips/AGENTS.md +6 -1
  12. package/corpus/templates/clips/_gitignore +2 -0
  13. package/corpus/templates/clips/actions/complete-transactional-email-summary.ts +56 -0
  14. package/corpus/templates/clips/actions/import-loom-recording.ts +258 -76
  15. package/corpus/templates/clips/actions/lib/direct-video.ts +96 -0
  16. package/corpus/templates/clips/actions/lib/loom-import-job.ts +53 -2
  17. package/corpus/templates/clips/actions/lib/loom-video.ts +2 -62
  18. package/corpus/templates/clips/actions/lib/video-download-limits.ts +62 -0
  19. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +249 -0
  20. package/corpus/templates/clips/app/components/library/library-grid.tsx +65 -0
  21. package/corpus/templates/clips/app/components/library/library-layout.tsx +55 -0
  22. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +28 -126
  23. package/corpus/templates/clips/app/global.css +23 -0
  24. package/corpus/templates/clips/app/hooks/use-transactional-email-bridge.ts +98 -0
  25. package/corpus/templates/clips/app/i18n/en-US.ts +20 -0
  26. package/corpus/templates/clips/app/routes/_app.tsx +2 -0
  27. package/corpus/templates/clips/app/routes/import.tsx +396 -0
  28. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +8 -6
  29. package/corpus/templates/clips/app/routes/record.tsx +13 -71
  30. package/corpus/templates/clips/changelog/2026-07-25-two-clip-emails-now-include-a-concise-summary-generated-secu.md +6 -0
  31. package/corpus/templates/clips/data/clips-transactional-emails/config.json +3 -0
  32. package/corpus/templates/clips/server/jobs/transactional-emails.ts +1034 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-store.ts +865 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +270 -0
  35. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  36. package/corpus/templates/clips/server/routes/api/view-event.post.ts +59 -2
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
  38. package/corpus/templates/content/AGENTS.md +8 -0
  39. package/corpus/templates/content/_gitignore +2 -0
  40. package/corpus/templates/content/actions/_content-database-personal-view.ts +28 -3
  41. package/corpus/templates/content/actions/_property-utils.ts +32 -2
  42. package/corpus/templates/content/actions/configure-document-property.ts +12 -2
  43. package/corpus/templates/content/actions/delete-document-property.ts +13 -3
  44. package/corpus/templates/content/actions/duplicate-document-property.ts +13 -3
  45. package/corpus/templates/content/actions/list-content-spaces.ts +5 -2
  46. package/corpus/templates/content/actions/list-document-properties.ts +12 -3
  47. package/corpus/templates/content/actions/move-database-item.ts +74 -55
  48. package/corpus/templates/content/actions/reorder-document-property.ts +20 -4
  49. package/corpus/templates/content/actions/set-document-property.ts +10 -1
  50. package/corpus/templates/content/actions/update-content-database-personal-view.ts +7 -0
  51. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +31 -21
  52. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +30 -3
  53. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +8 -1
  54. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +72 -20
  55. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +79 -10
  56. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +8 -1
  57. package/corpus/templates/content/app/components/editor/database/settings.tsx +6 -0
  58. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +246 -26
  59. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +491 -197
  60. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +3 -3
  61. package/corpus/templates/content/app/components/sidebar/sidebar-reorder.tsx +409 -0
  62. package/corpus/templates/content/app/hooks/use-content-database.ts +117 -3
  63. package/corpus/templates/content/app/hooks/use-content-spaces.ts +1 -0
  64. package/corpus/templates/content/app/hooks/use-document-properties.ts +62 -18
  65. package/corpus/templates/content/app/hooks/use-documents.ts +13 -5
  66. package/corpus/templates/content/app/i18n-data.ts +261 -3
  67. package/corpus/templates/content/app/routes/_app.page.$id.tsx +9 -2
  68. package/corpus/templates/content/changelog/2026-07-24-database-page-properties-keep-their-context.md +6 -0
  69. package/corpus/templates/content/changelog/2026-07-26-sidebar-references-can-now-be-personally-reordered.md +6 -0
  70. package/corpus/templates/content/docs/solutions/2026-07-24-database-property-context-switch-diagnosis.md +249 -0
  71. package/corpus/templates/content/docs/solutions/content-sidebar-order-shape.md +389 -0
  72. package/corpus/templates/content/shared/api.ts +28 -0
  73. package/corpus/templates/design/_gitignore +2 -0
  74. package/corpus/templates/design/app/i18n-data.ts +10 -0
  75. package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
  76. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
  77. package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
  78. package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
  79. package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
  80. package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
  81. package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  82. package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
  83. package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
  84. package/corpus/templates/dispatch/_gitignore +2 -0
  85. package/corpus/templates/forms/_gitignore +2 -0
  86. package/corpus/templates/macros/_gitignore +2 -0
  87. package/corpus/templates/mail/_gitignore +2 -0
  88. package/corpus/templates/plan/_gitignore +2 -0
  89. package/corpus/templates/slides/_gitignore +2 -0
  90. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
  91. package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
  92. package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
  93. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  94. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
  95. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
  96. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
  97. package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  98. package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
  99. package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
  100. package/corpus/templates/tasks/_gitignore +2 -0
  101. package/corpus/toolkit/CHANGELOG.md +6 -0
  102. package/corpus/toolkit/package.json +1 -1
  103. package/corpus/toolkit/src/editor/DragHandle.ts +63 -9
  104. package/dist/collab/routes.d.ts +1 -1
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/observability/routes.d.ts +3 -3
  107. package/dist/progress/routes.d.ts +1 -1
  108. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  109. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  110. package/dist/server/builder-design-systems.d.ts +49 -0
  111. package/dist/server/builder-design-systems.d.ts.map +1 -1
  112. package/dist/server/builder-design-systems.js +221 -69
  113. package/dist/server/builder-design-systems.js.map +1 -1
  114. package/dist/server/email-template.d.ts +7 -0
  115. package/dist/server/email-template.d.ts.map +1 -1
  116. package/dist/server/email-template.js +24 -0
  117. package/dist/server/email-template.js.map +1 -1
  118. package/dist/server/index.d.ts +1 -1
  119. package/dist/server/index.d.ts.map +1 -1
  120. package/dist/server/index.js +1 -1
  121. package/dist/server/index.js.map +1 -1
  122. package/dist/server/transcribe-voice.d.ts +1 -1
  123. package/dist/templates/chat/_gitignore +2 -0
  124. package/package.json +5 -3
  125. package/src/server/builder-design-systems.ts +299 -82
  126. package/src/server/email-template.ts +33 -0
  127. package/src/server/index.ts +8 -0
  128. package/src/templates/chat/_gitignore +2 -0
  129. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
  130. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
  131. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
  132. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
@@ -28,7 +28,9 @@ import {
28
28
  } from "@dnd-kit/sortable";
29
29
  import type {
30
30
  ContentDatabaseItem,
31
+ ContentDatabasePersonalViewOverrides,
31
32
  ContentDatabaseResponse,
33
+ ContentSidebarViewOrder,
32
34
  Document,
33
35
  DocumentTreeNode,
34
36
  } from "@shared/api";
@@ -36,6 +38,7 @@ import { CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION } from "@shared/api";
36
38
  import {
37
39
  IconFolder,
38
40
  IconFolderOpen,
41
+ IconArrowsSort,
39
42
  IconPlus,
40
43
  IconRestore,
41
44
  IconSearch,
@@ -61,7 +64,10 @@ import {
61
64
  import { Link, useLocation, useNavigate } from "react-router";
62
65
  import { toast } from "sonner";
63
66
 
64
- import { ContentFilesSidebarView } from "@/components/editor/database/sidebar";
67
+ import {
68
+ ContentFilesSidebarView,
69
+ type ContentFilesSidebarRenderReorder,
70
+ } from "@/components/editor/database/sidebar";
65
71
  import { QueryErrorState } from "@/components/QueryErrorState";
66
72
  import {
67
73
  AlertDialog,
@@ -78,6 +84,8 @@ import {
78
84
  DropdownMenu,
79
85
  DropdownMenuContent,
80
86
  DropdownMenuItem,
87
+ DropdownMenuRadioGroup,
88
+ DropdownMenuRadioItem,
81
89
  DropdownMenuSeparator,
82
90
  DropdownMenuTrigger,
83
91
  } from "@/components/ui/dropdown-menu";
@@ -94,6 +102,7 @@ import {
94
102
  removeOptimisticItemFromContentDatabase,
95
103
  useContentDatabaseById,
96
104
  useContentDatabasePersonalView,
105
+ useMoveDatabaseItem,
97
106
  useUpdateContentDatabasePersonalView,
98
107
  useCreateContentDatabase,
99
108
  useDeleteContentDatabase,
@@ -128,11 +137,7 @@ import {
128
137
  getDocumentSidebarSections,
129
138
  isDirectLocalDocument,
130
139
  } from "./document-sidebar-sections";
131
- import {
132
- DocumentSidebarIcon,
133
- DocumentTreeItem,
134
- FavoriteDocumentItem,
135
- } from "./DocumentTreeItem";
140
+ import { DocumentSidebarIcon, DocumentTreeItem } from "./DocumentTreeItem";
136
141
  import {
137
142
  contentSpaceAvailability,
138
143
  contentSpaceForStoredSelection,
@@ -143,6 +148,7 @@ import {
143
148
  selectContentSpace,
144
149
  toggleExpandedWorkspaceIds,
145
150
  } from "./select-content-space";
151
+ import { type SidebarReorderLabels } from "./sidebar-reorder";
146
152
  import {
147
153
  WorkspaceSourceMenu,
148
154
  type CreatedWorkspace,
@@ -247,13 +253,73 @@ interface RemoveLocalFileSourceResult {
247
253
  deleted: number;
248
254
  }
249
255
 
250
- function WorkspaceFilesSection({
256
+ function personalSidebarOrderForDatabase(
257
+ data: ContentDatabaseResponse | undefined,
258
+ overrides: ContentDatabasePersonalViewOverrides | null | undefined,
259
+ ) {
260
+ const savedActiveViewId =
261
+ data?.database.viewConfig.activeViewId ??
262
+ data?.database.viewConfig.views[0]?.id ??
263
+ "default";
264
+ const activeViewId =
265
+ overrides?.activeViewId &&
266
+ data?.database.viewConfig.views.some(
267
+ (view) => view.id === overrides.activeViewId,
268
+ )
269
+ ? overrides.activeViewId
270
+ : savedActiveViewId;
271
+ const saved = overrides?.views.find((view) => view.id === activeViewId);
272
+ return {
273
+ activeViewId,
274
+ order: saved?.sidebarOrder ?? {
275
+ mode: "custom" as const,
276
+ itemIds: data?.items.map((item) => item.id) ?? [],
277
+ },
278
+ };
279
+ }
280
+
281
+ function withPersonalSidebarOrder(
282
+ data: ContentDatabaseResponse | undefined,
283
+ overrides: ContentDatabasePersonalViewOverrides | null | undefined,
284
+ activeViewId: string,
285
+ order: ContentSidebarViewOrder,
286
+ ): ContentDatabasePersonalViewOverrides {
287
+ const savedView = data?.database.viewConfig.views.find(
288
+ (view) => view.id === activeViewId,
289
+ );
290
+ const existingView = overrides?.views.find(
291
+ (view) => view.id === activeViewId,
292
+ );
293
+ const nextView = {
294
+ id: activeViewId,
295
+ sorts: existingView?.sorts ?? savedView?.sorts ?? [],
296
+ filters: existingView?.filters ?? savedView?.filters ?? [],
297
+ filterMode:
298
+ existingView?.filterMode ?? savedView?.filterMode ?? ("and" as const),
299
+ sidebarOrder: order,
300
+ };
301
+ return {
302
+ version: CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION,
303
+ activeViewId,
304
+ views: [
305
+ ...(overrides?.views ?? []).filter((view) => view.id !== activeViewId),
306
+ nextView,
307
+ ],
308
+ };
309
+ }
310
+
311
+ function WorkspaceSidebarItem({
251
312
  space,
252
313
  selected,
314
+ expanded,
315
+ reorder,
316
+ createDocumentPending,
253
317
  activeDocumentId,
254
318
  expandedDocumentIds,
255
319
  onDocumentExpandedChange,
256
320
  onActivate,
321
+ onToggleExpanded,
322
+ onCreatePageInSpace,
257
323
  onCreateChildPage,
258
324
  onCreateChildDatabase,
259
325
  onDeleteItem,
@@ -261,10 +327,15 @@ function WorkspaceFilesSection({
261
327
  }: {
262
328
  space: ContentSpaceSummary;
263
329
  selected: boolean;
330
+ expanded: boolean;
331
+ reorder?: ContentFilesSidebarRenderReorder;
332
+ createDocumentPending: boolean;
264
333
  activeDocumentId: string | null;
265
334
  expandedDocumentIds: ReadonlySet<string>;
266
335
  onDocumentExpandedChange: (documentId: string, expanded: boolean) => void;
267
336
  onActivate: (space: ContentSpaceSummary, documentId?: string) => void;
337
+ onToggleExpanded: () => void;
338
+ onCreatePageInSpace: (space: ContentSpaceSummary) => void;
268
339
  onCreateChildPage: (
269
340
  space: ContentSpaceSummary,
270
341
  item: ContentDatabaseItem,
@@ -277,64 +348,233 @@ function WorkspaceFilesSection({
277
348
  onToggleFavorite: (item: ContentDatabaseItem) => void;
278
349
  }) {
279
350
  const t = useT();
280
- const filesDatabase = useContentDatabaseById(space.filesDatabaseId);
351
+ const activeFilesDatabaseId = expanded ? space.filesDatabaseId : null;
352
+ const filesDatabase = useContentDatabaseById(activeFilesDatabaseId);
281
353
  const filesPersonalView = useContentDatabasePersonalView(
282
- space.filesDatabaseId,
354
+ activeFilesDatabaseId,
283
355
  );
284
356
  const updateFilesPersonalView = useUpdateContentDatabasePersonalView(
285
- space.filesDatabaseId,
357
+ activeFilesDatabaseId,
286
358
  );
287
359
  const failed = filesDatabase.isError || filesPersonalView.isError;
360
+ const { activeViewId, order: sidebarOrder } = personalSidebarOrderForDatabase(
361
+ filesDatabase.data,
362
+ filesPersonalView.data?.overrides,
363
+ );
364
+ const reorderLabels: SidebarReorderLabels = {
365
+ drag: (label) => t("sidebar.dragToReorder", { label }),
366
+ moveUp: t("sidebar.moveUp"),
367
+ moveDown: t("sidebar.moveDown"),
368
+ moveTo: t("sidebar.moveToPosition"),
369
+ moveToPosition: (position) => t("sidebar.positionNumber", { position }),
370
+ };
371
+ const sidebarOrderModeLabels = {
372
+ custom: t("sidebar.orderMode.custom"),
373
+ last_edited: t("sidebar.orderMode.last_edited"),
374
+ name: t("sidebar.orderMode.name"),
375
+ created: t("sidebar.orderMode.created"),
376
+ };
377
+
378
+ function updateSidebarOrder(order: ContentSidebarViewOrder) {
379
+ updateFilesPersonalView.mutate(
380
+ {
381
+ databaseId: space.filesDatabaseId,
382
+ overrides: withPersonalSidebarOrder(
383
+ filesDatabase.data,
384
+ filesPersonalView.data?.overrides,
385
+ activeViewId,
386
+ order,
387
+ ),
388
+ },
389
+ {
390
+ onError: (error) => {
391
+ toast.error(t("sidebar.failedSaveOrder"), {
392
+ description:
393
+ error instanceof Error ? error.message : t("empty.genericError"),
394
+ });
395
+ },
396
+ },
397
+ );
398
+ }
288
399
 
289
400
  return (
290
- <div className="ms-3 pb-1 ps-1">
291
- {failed ? (
292
- <QueryErrorState
293
- compact
294
- onRetry={() => {
295
- void filesDatabase.refetch();
296
- void filesPersonalView.refetch();
297
- }}
298
- retrying={filesDatabase.isFetching || filesPersonalView.isFetching}
299
- />
300
- ) : (
301
- <ContentFilesSidebarView
302
- data={filesDatabase.data}
303
- overrides={filesPersonalView.data?.overrides}
304
- isLoading={filesDatabase.isLoading || filesPersonalView.isLoading}
305
- activeDocumentId={activeDocumentId}
306
- expandedDocumentIds={expandedDocumentIds}
307
- onDocumentExpandedChange={onDocumentExpandedChange}
308
- onSelectView={(viewId) => {
309
- const current = filesPersonalView.data?.overrides;
310
- updateFilesPersonalView.mutate({
311
- databaseId: space.filesDatabaseId,
312
- overrides: {
313
- version:
314
- current?.version ??
315
- CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION,
316
- activeViewId: viewId,
317
- views: current?.views ?? [],
318
- },
319
- });
320
- }}
321
- onOpenItem={(item: ContentDatabaseItem) => {
322
- if (selected) return false;
323
- onActivate(space, item.document.id);
324
- return true;
325
- }}
326
- onCreateChildPage={(item) => onCreateChildPage(space, item)}
327
- onCreateChildDatabase={(item) => onCreateChildDatabase(space, item)}
328
- onDeleteItem={onDeleteItem}
329
- onToggleFavorite={onToggleFavorite}
330
- labels={{
331
- noMatchesLabel: t("database.noRowsMatchThisView"),
332
- clearLabel: t("database.clearSearchAndFilters"),
333
- navigationLabel: `${space.name} ${t("sidebar.files")}`,
334
- untitledLabel: t("sidebar.untitled"),
401
+ <div className="min-w-0">
402
+ <div
403
+ className={cn(
404
+ "group/workspace-header flex h-7 w-full min-w-0 items-center rounded-md",
405
+ selected
406
+ ? "text-foreground"
407
+ : "text-muted-foreground hover:bg-accent/40 hover:text-foreground",
408
+ )}
409
+ >
410
+ <button
411
+ type="button"
412
+ aria-expanded={expanded}
413
+ aria-label={`${expanded ? t("sidebar.collapse") : t("sidebar.expand")} ${space.name}`}
414
+ className="group/workspace-toggle relative flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-background/60"
415
+ onClick={onToggleExpanded}
416
+ >
417
+ <span className="group-hover/workspace-header:opacity-0 group-focus-within/workspace-header:opacity-0 group-focus-visible/workspace-toggle:opacity-0">
418
+ {expanded ? <IconFolderOpen size={14} /> : <IconFolder size={14} />}
419
+ </span>
420
+ <span className="absolute inset-0 flex items-center justify-center opacity-0 group-hover/workspace-header:opacity-100 group-focus-within/workspace-header:opacity-100 group-focus-visible/workspace-toggle:opacity-100">
421
+ {expanded ? (
422
+ <IconChevronDown size={14} />
423
+ ) : (
424
+ <IconChevronRight size={14} />
425
+ )}
426
+ </span>
427
+ </button>
428
+ <Link
429
+ to={`/page/${space.filesDocumentId}`}
430
+ {...reorder?.controls.attributes}
431
+ {...reorder?.controls.listeners}
432
+ data-sidebar-reorder-item-id={reorder?.controls.itemId}
433
+ role="link"
434
+ className={cn(
435
+ "flex h-7 min-w-0 flex-1 items-center pe-2 text-start text-[10px] font-semibold uppercase leading-none tracking-wider",
436
+ reorder && "touch-none cursor-pointer select-none",
437
+ reorder?.controls.isDragging && "cursor-grabbing",
438
+ )}
439
+ onClick={(event) => {
440
+ if (
441
+ !event.metaKey &&
442
+ !event.ctrlKey &&
443
+ !event.shiftKey &&
444
+ !event.altKey
445
+ ) {
446
+ event.preventDefault();
447
+ onActivate(space);
448
+ }
335
449
  }}
336
- />
337
- )}
450
+ >
451
+ <span className="min-w-0 flex-1 truncate">{space.name}</span>
452
+ </Link>
453
+ {expanded ? (
454
+ <DropdownMenu>
455
+ <Tooltip>
456
+ <TooltipTrigger asChild>
457
+ <DropdownMenuTrigger asChild>
458
+ <button
459
+ type="button"
460
+ className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background/60 hover:text-foreground disabled:opacity-50"
461
+ aria-label={t("sidebar.orderButton", {
462
+ order: sidebarOrderModeLabels[sidebarOrder.mode],
463
+ })}
464
+ disabled={
465
+ filesDatabase.isLoading ||
466
+ filesPersonalView.isLoading ||
467
+ updateFilesPersonalView.isPending
468
+ }
469
+ >
470
+ <IconArrowsSort size={14} />
471
+ </button>
472
+ </DropdownMenuTrigger>
473
+ </TooltipTrigger>
474
+ <TooltipContent>
475
+ {t("sidebar.orderButton", {
476
+ order: sidebarOrderModeLabels[sidebarOrder.mode],
477
+ })}
478
+ </TooltipContent>
479
+ </Tooltip>
480
+ <DropdownMenuContent align="end">
481
+ <DropdownMenuRadioGroup
482
+ value={sidebarOrder.mode}
483
+ onValueChange={(value) =>
484
+ updateSidebarOrder({
485
+ ...sidebarOrder,
486
+ mode: value as ContentSidebarViewOrder["mode"],
487
+ })
488
+ }
489
+ >
490
+ {(
491
+ Object.keys(
492
+ sidebarOrderModeLabels,
493
+ ) as ContentSidebarViewOrder["mode"][]
494
+ ).map((mode) => (
495
+ <DropdownMenuRadioItem key={mode} value={mode}>
496
+ {sidebarOrderModeLabels[mode]}
497
+ </DropdownMenuRadioItem>
498
+ ))}
499
+ </DropdownMenuRadioGroup>
500
+ </DropdownMenuContent>
501
+ </DropdownMenu>
502
+ ) : null}
503
+ <button
504
+ type="button"
505
+ className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background/60 hover:text-foreground disabled:opacity-50"
506
+ disabled={createDocumentPending}
507
+ aria-label={`${t("sidebar.newPage")} — ${space.name}`}
508
+ onClick={() => onCreatePageInSpace(space)}
509
+ >
510
+ <IconPlus size={14} />
511
+ </button>
512
+ </div>
513
+ {expanded ? (
514
+ <div className="min-w-0 pb-1 ps-4">
515
+ {failed ? (
516
+ <QueryErrorState
517
+ compact
518
+ onRetry={() => {
519
+ void filesDatabase.refetch();
520
+ void filesPersonalView.refetch();
521
+ }}
522
+ retrying={
523
+ filesDatabase.isFetching || filesPersonalView.isFetching
524
+ }
525
+ />
526
+ ) : (
527
+ <ContentFilesSidebarView
528
+ data={filesDatabase.data}
529
+ overrides={filesPersonalView.data?.overrides}
530
+ isLoading={filesDatabase.isLoading || filesPersonalView.isLoading}
531
+ activeDocumentId={activeDocumentId}
532
+ expandedDocumentIds={expandedDocumentIds}
533
+ onDocumentExpandedChange={onDocumentExpandedChange}
534
+ onSelectView={(viewId) => {
535
+ const current = filesPersonalView.data?.overrides;
536
+ updateFilesPersonalView.mutate({
537
+ databaseId: space.filesDatabaseId,
538
+ overrides: {
539
+ version:
540
+ current?.version ??
541
+ CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION,
542
+ activeViewId: viewId,
543
+ views: current?.views ?? [],
544
+ },
545
+ });
546
+ }}
547
+ sidebarOrder={sidebarOrder}
548
+ manualReorder={
549
+ updateFilesPersonalView.isPending
550
+ ? undefined
551
+ : {
552
+ labels: reorderLabels,
553
+ onReorder: (itemIds) =>
554
+ updateSidebarOrder({ ...sidebarOrder, itemIds }),
555
+ }
556
+ }
557
+ onOpenItem={(item: ContentDatabaseItem) => {
558
+ if (selected) return false;
559
+ onActivate(space, item.document.id);
560
+ return true;
561
+ }}
562
+ onCreateChildPage={(item) => onCreateChildPage(space, item)}
563
+ onCreateChildDatabase={(item) =>
564
+ onCreateChildDatabase(space, item)
565
+ }
566
+ onDeleteItem={onDeleteItem}
567
+ onToggleFavorite={onToggleFavorite}
568
+ labels={{
569
+ noMatchesLabel: t("database.noRowsMatchThisView"),
570
+ clearLabel: t("database.clearSearchAndFilters"),
571
+ navigationLabel: `${space.name} ${t("sidebar.files")}`,
572
+ untitledLabel: t("sidebar.untitled"),
573
+ }}
574
+ />
575
+ )}
576
+ </div>
577
+ ) : null}
338
578
  </div>
339
579
  );
340
580
  }
@@ -351,6 +591,16 @@ export function DocumentSidebar({
351
591
  const location = useLocation();
352
592
  const queryClient = useQueryClient();
353
593
  const t = useT();
594
+ const sidebarReorderLabels = useMemo<SidebarReorderLabels>(
595
+ () => ({
596
+ drag: (label) => t("sidebar.dragToReorder", { label }),
597
+ moveUp: t("sidebar.moveUp"),
598
+ moveDown: t("sidebar.moveDown"),
599
+ moveTo: t("sidebar.moveToPosition"),
600
+ moveToPosition: (position) => t("sidebar.positionNumber", { position }),
601
+ }),
602
+ [t],
603
+ );
354
604
  const documentsQuery = useDocuments();
355
605
  const { data: documents = [], isLoading } = documentsQuery;
356
606
  const createDocument = useCreateDocument();
@@ -371,8 +621,13 @@ export function DocumentSidebar({
371
621
  const contentSpaces = contentSpacesQuery.data?.spaces ?? [];
372
622
  const workspaceCatalogDatabaseId =
373
623
  contentSpacesQuery.data?.catalogDatabaseId ?? null;
624
+ const workspaceCatalogDocumentId =
625
+ contentSpacesQuery.data?.catalogDocumentId ?? null;
626
+ const favoritesDatabaseId =
627
+ contentSpacesQuery.data?.favoritesDatabaseId ?? null;
374
628
  const favoritesDocumentId =
375
629
  contentSpacesQuery.data?.favoritesDocumentId ?? null;
630
+ const favoritesDatabase = useContentDatabaseById(favoritesDatabaseId);
376
631
  const workspaceCatalogDatabase = useContentDatabaseById(
377
632
  workspaceCatalogDatabaseId,
378
633
  );
@@ -381,6 +636,10 @@ export function DocumentSidebar({
381
636
  );
382
637
  const updateWorkspaceCatalogPersonalView =
383
638
  useUpdateContentDatabasePersonalView(workspaceCatalogDatabaseId);
639
+ const movePinnedItem = useMoveDatabaseItem(favoritesDocumentId ?? "");
640
+ const moveWorkspaceItem = useMoveDatabaseItem(
641
+ workspaceCatalogDocumentId ?? "",
642
+ );
384
643
  const spaceProvisionAttemptedRef = useRef(false);
385
644
  useEffect(() => {
386
645
  if (
@@ -581,6 +840,7 @@ export function DocumentSidebar({
581
840
  role: "owner",
582
841
  catalogItemId: created.catalogItemId,
583
842
  catalogDocumentId: created.catalogDocumentId,
843
+ catalogPosition: Number.MAX_SAFE_INTEGER,
584
844
  }),
585
845
  [handleSelectContentSpace],
586
846
  );
@@ -678,7 +938,6 @@ export function DocumentSidebar({
678
938
  localFileMode,
679
939
  localSourceDocuments,
680
940
  databaseDocuments,
681
- favorites,
682
941
  showFavorites,
683
942
  } = getDocumentSidebarSections(documents, treeDocuments);
684
943
  const localFileTree = buildDocumentTree(localSourceDocuments);
@@ -689,10 +948,6 @@ export function DocumentSidebar({
689
948
  (document) => document.source?.kind !== "folder",
690
949
  ).length;
691
950
  const canRemoveLocalFiles = localFileMode || importedLocalFileCount > 0;
692
- // Match the tree rows' right-side inset so favorite titles clip inside the
693
- // visible sidebar instead of widening the scroll surface.
694
- const favoriteRowWidth =
695
- width === undefined ? undefined : Math.max(208, width - 24);
696
951
  const activeDocument = activeDocumentId
697
952
  ? documents.find((doc) => doc.id === activeDocumentId)
698
953
  : null;
@@ -913,22 +1168,6 @@ export function DocumentSidebar({
913
1168
  [handleCreatePage, handleSelectContentSpace, selectedSpace?.id],
914
1169
  );
915
1170
 
916
- const handleOpenFavorite = useCallback(
917
- (document: Document) => {
918
- const space = contentSpaces.find(
919
- (candidate) =>
920
- candidate.filesDocumentId ===
921
- document.databaseMembership?.databaseDocumentId,
922
- );
923
- if (space) {
924
- void handleSelectContentSpace(space, document.id);
925
- return;
926
- }
927
- navigateToDocument(document.id);
928
- },
929
- [contentSpaces, handleSelectContentSpace, navigateToDocument],
930
- );
931
-
932
1171
  const handleDelete = useCallback(
933
1172
  async (id: string) => {
934
1173
  const deletedDocument = documents.find((doc) => doc.id === id) ?? null;
@@ -1090,6 +1329,54 @@ export function DocumentSidebar({
1090
1329
  [handleReorderPage, parentByDocumentId],
1091
1330
  );
1092
1331
 
1332
+ const handlePinnedReorder = useCallback(
1333
+ (_itemIds: string[], moved: { itemId: string; position: number }) => {
1334
+ if (!favoritesDatabaseId) return;
1335
+ movePinnedItem.mutate(
1336
+ {
1337
+ databaseId: favoritesDatabaseId,
1338
+ itemId: moved.itemId,
1339
+ position: moved.position,
1340
+ },
1341
+ {
1342
+ onError: (error) => {
1343
+ toast.error(t("sidebar.failedSaveOrder"), {
1344
+ description:
1345
+ error instanceof Error
1346
+ ? error.message
1347
+ : t("empty.genericError"),
1348
+ });
1349
+ },
1350
+ },
1351
+ );
1352
+ },
1353
+ [favoritesDatabaseId, movePinnedItem, t],
1354
+ );
1355
+
1356
+ const handleWorkspaceReorder = useCallback(
1357
+ (_itemIds: string[], moved: { itemId: string; position: number }) => {
1358
+ if (!workspaceCatalogDatabaseId) return;
1359
+ moveWorkspaceItem.mutate(
1360
+ {
1361
+ databaseId: workspaceCatalogDatabaseId,
1362
+ itemId: moved.itemId,
1363
+ position: moved.position,
1364
+ },
1365
+ {
1366
+ onError: (error) => {
1367
+ toast.error(t("sidebar.failedSaveOrder"), {
1368
+ description:
1369
+ error instanceof Error
1370
+ ? error.message
1371
+ : t("empty.genericError"),
1372
+ });
1373
+ },
1374
+ },
1375
+ );
1376
+ },
1377
+ [moveWorkspaceItem, t, workspaceCatalogDatabaseId],
1378
+ );
1379
+
1093
1380
  const handleToggleFavorite = useCallback(
1094
1381
  (id: string, isFavorite: boolean) => {
1095
1382
  updateDocument.mutate(
@@ -1469,92 +1756,47 @@ export function DocumentSidebar({
1469
1756
  );
1470
1757
  };
1471
1758
 
1472
- const renderWorkspaceRoot = (space: ContentSpaceSummary) => {
1473
- const selected = selectedSpace?.id === space.id;
1474
- const expanded = expandedWorkspaceIds.includes(space.id);
1475
- return (
1476
- <div className="min-w-0">
1477
- <div
1478
- className={cn(
1479
- "group/workspace-header flex h-7 w-full min-w-0 items-center rounded-md",
1480
- selected
1481
- ? "text-foreground"
1482
- : "text-muted-foreground hover:bg-accent/40 hover:text-foreground",
1483
- )}
1484
- >
1485
- <button
1486
- type="button"
1487
- aria-expanded={expanded}
1488
- aria-label={`${expanded ? t("sidebar.collapse") : t("sidebar.expand")} ${space.name}`}
1489
- className="group/workspace-toggle relative flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-background/60"
1490
- onClick={() =>
1491
- updateExpandedWorkspaceIds((current) =>
1492
- toggleExpandedWorkspaceIds(current, space.id),
1493
- )
1494
- }
1495
- >
1496
- <span className="group-hover/workspace-header:opacity-0 group-focus-visible/workspace-toggle:opacity-0">
1497
- {expanded ? (
1498
- <IconFolderOpen size={14} />
1499
- ) : (
1500
- <IconFolder size={14} />
1501
- )}
1502
- </span>
1503
- <span className="absolute inset-0 flex items-center justify-center opacity-0 group-hover/workspace-header:opacity-100 group-focus-visible/workspace-toggle:opacity-100">
1504
- {expanded ? (
1505
- <IconChevronDown size={14} />
1506
- ) : (
1507
- <IconChevronRight size={14} />
1508
- )}
1509
- </span>
1510
- </button>
1511
- <button
1512
- type="button"
1513
- className="h-7 min-w-0 flex-1 truncate pe-2 text-start text-[10px] font-semibold uppercase tracking-wider"
1514
- onClick={() => void handleSelectContentSpace(space)}
1515
- >
1516
- {space.name}
1517
- </button>
1518
- <button
1519
- type="button"
1520
- className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background/60 hover:text-foreground disabled:opacity-50"
1521
- disabled={createDocument.isPending}
1522
- aria-label={`${t("sidebar.newPage")} — ${space.name}`}
1523
- onClick={() => void handleCreatePageInSpace(space)}
1524
- >
1525
- <IconPlus size={14} />
1526
- </button>
1527
- </div>
1528
- {expanded && (
1529
- <WorkspaceFilesSection
1530
- space={space}
1531
- selected={selected}
1532
- activeDocumentId={activeDocumentId}
1533
- expandedDocumentIds={expandedDocumentIdSet}
1534
- onDocumentExpandedChange={handleDocumentExpandedChange}
1535
- onActivate={(nextSpace, documentId) =>
1536
- void handleSelectContentSpace(nextSpace, documentId)
1537
- }
1538
- onCreateChildPage={(nextSpace, item) =>
1539
- void handleCreatePage(
1540
- item.document.id,
1541
- nextSpace.id,
1542
- undefined,
1543
- nextSpace.filesDatabaseId,
1544
- )
1545
- }
1546
- onCreateChildDatabase={(nextSpace, item) =>
1547
- void handleCreateDatabase(item.document.id, nextSpace.id)
1548
- }
1549
- onDeleteItem={(item) => void handleDelete(item.document.id)}
1550
- onToggleFavorite={(item) =>
1551
- handleToggleFavorite(item.document.id, !item.document.isFavorite)
1552
- }
1553
- />
1554
- )}
1555
- </div>
1556
- );
1557
- };
1759
+ const renderWorkspaceRoot = (
1760
+ space: ContentSpaceSummary,
1761
+ reorder?: ContentFilesSidebarRenderReorder,
1762
+ ) => (
1763
+ <WorkspaceSidebarItem
1764
+ space={space}
1765
+ selected={selectedSpace?.id === space.id}
1766
+ expanded={expandedWorkspaceIds.includes(space.id)}
1767
+ reorder={reorder}
1768
+ createDocumentPending={createDocument.isPending}
1769
+ activeDocumentId={activeDocumentId}
1770
+ expandedDocumentIds={expandedDocumentIdSet}
1771
+ onDocumentExpandedChange={handleDocumentExpandedChange}
1772
+ onToggleExpanded={() =>
1773
+ updateExpandedWorkspaceIds((current) =>
1774
+ toggleExpandedWorkspaceIds(current, space.id),
1775
+ )
1776
+ }
1777
+ onActivate={(nextSpace, documentId) =>
1778
+ void handleSelectContentSpace(nextSpace, documentId)
1779
+ }
1780
+ onCreatePageInSpace={(nextSpace) =>
1781
+ void handleCreatePageInSpace(nextSpace)
1782
+ }
1783
+ onCreateChildPage={(nextSpace, item) =>
1784
+ void handleCreatePage(
1785
+ item.document.id,
1786
+ nextSpace.id,
1787
+ undefined,
1788
+ nextSpace.filesDatabaseId,
1789
+ )
1790
+ }
1791
+ onCreateChildDatabase={(nextSpace, item) =>
1792
+ void handleCreateDatabase(item.document.id, nextSpace.id)
1793
+ }
1794
+ onDeleteItem={(item) => void handleDelete(item.document.id)}
1795
+ onToggleFavorite={(item) =>
1796
+ handleToggleFavorite(item.document.id, !item.document.isFavorite)
1797
+ }
1798
+ />
1799
+ );
1558
1800
 
1559
1801
  const renderWorkspaceNavigation = () => (
1560
1802
  <div className="mb-2 min-w-0 overflow-x-hidden px-2">
@@ -1595,16 +1837,27 @@ export function DocumentSidebar({
1595
1837
  },
1596
1838
  });
1597
1839
  }}
1598
- renderItem={(item) => {
1840
+ manualReorder={
1841
+ moveWorkspaceItem.isPending
1842
+ ? undefined
1843
+ : {
1844
+ labels: sidebarReorderLabels,
1845
+ onReorder: handleWorkspaceReorder,
1846
+ }
1847
+ }
1848
+ renderItem={(item, reorder) => {
1599
1849
  const space = contentSpaces.find(
1600
1850
  (candidate) =>
1601
1851
  candidate.catalogDocumentId === item.document.id,
1602
1852
  );
1603
1853
  return space
1604
- ? renderWorkspaceRoot({
1605
- ...space,
1606
- name: item.document.title || space.name,
1607
- })
1854
+ ? renderWorkspaceRoot(
1855
+ {
1856
+ ...space,
1857
+ name: item.document.title || space.name,
1858
+ },
1859
+ reorder,
1860
+ )
1608
1861
  : null;
1609
1862
  }}
1610
1863
  scroll={false}
@@ -1978,14 +2231,14 @@ export function DocumentSidebar({
1978
2231
  </>
1979
2232
  ) : (
1980
2233
  <>
1981
- {/* Favorites */}
2234
+ {/* Pinned */}
1982
2235
  {showFavorites && (
1983
2236
  <div className="mb-2 min-w-0 px-2">
1984
2237
  <div className="group/favorites flex h-7 w-full min-w-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent/40 hover:text-foreground">
1985
2238
  <button
1986
2239
  type="button"
1987
2240
  aria-expanded={!collapsedSections.favorites}
1988
- aria-label={`${collapsedSections.favorites ? t("sidebar.expand") : t("sidebar.collapse")} ${t("sidebar.favorites")}`}
2241
+ aria-label={`${collapsedSections.favorites ? t("sidebar.expand") : t("sidebar.collapse")} ${t("sidebar.pinned")}`}
1989
2242
  className="group/favorites-toggle flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-background/60"
1990
2243
  onClick={() => toggleSection("favorites")}
1991
2244
  >
@@ -2016,34 +2269,75 @@ export function DocumentSidebar({
2016
2269
  "text-foreground",
2017
2270
  )}
2018
2271
  >
2019
- {t("sidebar.favorites")}
2272
+ {t("sidebar.pinned")}
2020
2273
  </Link>
2021
2274
  </div>
2022
- {!collapsedSections.favorites &&
2023
- (isLoading
2024
- ? renderTreeSkeleton()
2025
- : favorites.map((doc) => (
2026
- <FavoriteDocumentItem
2027
- key={doc.id}
2028
- document={doc}
2029
- active={doc.id === activeDocumentId}
2030
- sidebarWidth={favoriteRowWidth}
2031
- onSelect={() => {
2032
- handleOpenFavorite(doc);
2033
- onNavigate?.();
2034
- }}
2035
- onCreateChildPage={() =>
2036
- void handleCreatePage(doc.id)
2037
- }
2038
- onCreateChildDatabase={() =>
2039
- void handleCreateDatabase(doc.id)
2040
- }
2041
- onRemoveFavorite={() =>
2042
- handleToggleFavorite(doc.id, false)
2043
- }
2044
- onDelete={() => void handleDelete(doc.id)}
2045
- />
2046
- )))}
2275
+ {!collapsedSections.favorites ? (
2276
+ favoritesDatabase.isError ? (
2277
+ <QueryErrorState
2278
+ compact
2279
+ onRetry={() => void favoritesDatabase.refetch()}
2280
+ retrying={favoritesDatabase.isFetching}
2281
+ />
2282
+ ) : (
2283
+ <ContentFilesSidebarView
2284
+ data={favoritesDatabase.data}
2285
+ overrides={null}
2286
+ isLoading={favoritesDatabase.isLoading}
2287
+ activeDocumentId={activeDocumentId}
2288
+ manualReorder={
2289
+ movePinnedItem.isPending
2290
+ ? undefined
2291
+ : {
2292
+ labels: sidebarReorderLabels,
2293
+ onReorder: handlePinnedReorder,
2294
+ }
2295
+ }
2296
+ onOpenItem={(item) => {
2297
+ const document = documents.find(
2298
+ (candidate) => candidate.id === item.document.id,
2299
+ );
2300
+ const space = document
2301
+ ? contentSpaces.find(
2302
+ (candidate) =>
2303
+ candidate.filesDocumentId ===
2304
+ document.databaseMembership
2305
+ ?.databaseDocumentId,
2306
+ )
2307
+ : undefined;
2308
+ if (!space || selectedSpace?.id === space.id) {
2309
+ onNavigate?.();
2310
+ return false;
2311
+ }
2312
+ void handleSelectContentSpace(
2313
+ space,
2314
+ item.document.id,
2315
+ );
2316
+ onNavigate?.();
2317
+ return true;
2318
+ }}
2319
+ onCreateChildPage={(item) =>
2320
+ void handleCreatePage(item.document.id)
2321
+ }
2322
+ onCreateChildDatabase={(item) =>
2323
+ void handleCreateDatabase(item.document.id)
2324
+ }
2325
+ onDeleteItem={(item) =>
2326
+ void handleDelete(item.document.id)
2327
+ }
2328
+ onToggleFavorite={(item) =>
2329
+ handleToggleFavorite(item.document.id, false)
2330
+ }
2331
+ scroll={false}
2332
+ labels={{
2333
+ noMatchesLabel: t("database.noRowsMatchThisView"),
2334
+ clearLabel: t("database.clearSearchAndFilters"),
2335
+ navigationLabel: t("sidebar.pinned"),
2336
+ untitledLabel: t("sidebar.untitled"),
2337
+ }}
2338
+ />
2339
+ )
2340
+ ) : null}
2047
2341
  </div>
2048
2342
  )}
2049
2343