canopycms 0.1.0-int.115 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +267 -231
  2. package/README.md +0 -1
  3. package/dist/ai/references.js +3 -1
  4. package/dist/api/assets.js +2 -2
  5. package/dist/api/branch-merge.d.ts +1 -1
  6. package/dist/api/branch.js +1 -1
  7. package/dist/api/content.js +1 -1
  8. package/dist/api/entries-constants.d.ts +1 -1
  9. package/dist/api/entries-constants.js +1 -1
  10. package/dist/api/entries.d.ts +2 -2
  11. package/dist/api/permissions.d.ts +2 -2
  12. package/dist/api/types.d.ts +3 -2
  13. package/dist/cli/cli.js +16 -14
  14. package/dist/cli/generate-ai-content.js +7 -5
  15. package/dist/cli/init.js +4 -4
  16. package/dist/client.d.ts +0 -1
  17. package/dist/client.js +0 -1
  18. package/dist/config/schemas/config.d.ts +0 -5
  19. package/dist/config/schemas/config.js +0 -1
  20. package/dist/config/types.d.ts +0 -1
  21. package/dist/content-index-generation.d.ts +1 -1
  22. package/dist/content-index-generation.js +1 -1
  23. package/dist/content-listing.js +1 -1
  24. package/dist/content-reader.js +1 -1
  25. package/dist/content-store.d.ts +1 -1
  26. package/dist/content-store.js +1 -1
  27. package/dist/dev-content-watcher.d.ts +1 -1
  28. package/dist/dev-content-watcher.js +1 -1
  29. package/dist/editor/BranchManager.d.ts +2 -0
  30. package/dist/editor/BranchManager.js +5 -4
  31. package/dist/editor/CanopyEditor.d.ts +1 -1
  32. package/dist/editor/CanopyEditor.js +1 -2
  33. package/dist/editor/CommentsPanel.js +3 -6
  34. package/dist/editor/Editor.d.ts +0 -2
  35. package/dist/editor/Editor.js +329 -181
  36. package/dist/editor/EditorAuthGate.js +8 -3
  37. package/dist/editor/EntryNavigator.d.ts +10 -0
  38. package/dist/editor/EntryNavigator.js +51 -2
  39. package/dist/editor/FormHeader.js +1 -1
  40. package/dist/editor/PreviewFrame.d.ts +5 -1
  41. package/dist/editor/PreviewFrame.js +3 -2
  42. package/dist/editor/PreviewPane.d.ts +47 -0
  43. package/dist/editor/PreviewPane.js +147 -0
  44. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  45. package/dist/editor/PreviewToolbarTools.js +73 -0
  46. package/dist/editor/comments/form-comments.js +2 -1
  47. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  48. package/dist/editor/components/BranchLockBanner.js +39 -0
  49. package/dist/editor/components/BranchPill.d.ts +32 -0
  50. package/dist/editor/components/BranchPill.js +33 -0
  51. package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
  52. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  53. package/dist/editor/components/EditorHeader.d.ts +50 -108
  54. package/dist/editor/components/EditorHeader.js +117 -138
  55. package/dist/editor/components/EditorRail.d.ts +49 -0
  56. package/dist/editor/components/EditorRail.js +65 -0
  57. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  58. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  59. package/dist/editor/components/QuickOpenPalette.d.ts +1 -1
  60. package/dist/editor/components/QuickOpenPalette.js +3 -2
  61. package/dist/editor/components/RenameEntryModal.d.ts +3 -1
  62. package/dist/editor/components/RenameEntryModal.js +3 -2
  63. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  64. package/dist/editor/components/SaveAllResult.js +86 -0
  65. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  66. package/dist/editor/components/SaveSplitButton.js +108 -0
  67. package/dist/editor/components/index.d.ts +2 -2
  68. package/dist/editor/components/index.js +2 -2
  69. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  70. package/dist/editor/context/EditorStateContext.js +4 -4
  71. package/dist/editor/copy.d.ts +3 -0
  72. package/dist/editor/copy.js +3 -0
  73. package/dist/editor/editor-utils.d.ts +11 -6
  74. package/dist/editor/editor-utils.js +8 -17
  75. package/dist/editor/fields/MarkdownField.js +3 -0
  76. package/dist/editor/hooks/save-all.d.ts +68 -0
  77. package/dist/editor/hooks/save-all.js +61 -0
  78. package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
  79. package/dist/editor/hooks/useCommentSystem.js +3 -0
  80. package/dist/editor/hooks/useDraftManager.d.ts +24 -1
  81. package/dist/editor/hooks/useDraftManager.js +297 -16
  82. package/dist/editor/hooks/useEditorLayout.d.ts +15 -11
  83. package/dist/editor/hooks/useEditorLayout.js +13 -25
  84. package/dist/editor/hooks/useEntryManager.d.ts +4 -0
  85. package/dist/editor/hooks/useEntryManager.js +30 -7
  86. package/dist/editor/media/MediaLibrary.d.ts +1 -1
  87. package/dist/editor/preview-path.d.ts +12 -0
  88. package/dist/editor/preview-path.js +45 -1
  89. package/dist/editor/preview-scale.d.ts +27 -0
  90. package/dist/editor/preview-scale.js +34 -0
  91. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  92. package/dist/editor/shell/ActivityRail.js +91 -0
  93. package/dist/editor/shell/EditorShell.d.ts +17 -0
  94. package/dist/editor/shell/EditorShell.js +31 -0
  95. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  96. package/dist/editor/shell/PaneLayout.js +116 -0
  97. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  98. package/dist/editor/shell/SidePanelHost.js +184 -0
  99. package/dist/editor/shell/TopBar.d.ts +12 -0
  100. package/dist/editor/shell/TopBar.js +14 -0
  101. package/dist/editor/shell/host-chrome.d.ts +16 -0
  102. package/dist/editor/shell/host-chrome.js +69 -0
  103. package/dist/editor/theme.js +18 -3
  104. package/dist/git-manager.d.ts +1 -1
  105. package/dist/git-manager.js +2 -2
  106. package/dist/operating-mode/types.d.ts +1 -1
  107. package/dist/resolve-canopy-user.d.ts +1 -1
  108. package/dist/resolve-canopy-user.js +1 -1
  109. package/dist/schema/resolver.d.ts +1 -1
  110. package/dist/schema/resolver.js +1 -1
  111. package/dist/schema/schema-store.d.ts +1 -1
  112. package/dist/schema/schema-store.js +2 -2
  113. package/dist/url-collision.js +3 -3
  114. package/dist/url-exclusivity-fixtures.d.ts +1 -1
  115. package/dist/url-exclusivity-fixtures.js +1 -1
  116. package/dist/utils/content-write-lock.d.ts +1 -1
  117. package/dist/utils/content-write-lock.js +1 -1
  118. package/dist/utils/debug.d.ts +1 -1
  119. package/dist/utils/debug.js +5 -3
  120. package/dist/utils/git.d.ts +4 -0
  121. package/dist/utils/git.js +27 -5
  122. package/dist/validation/block-structural-keys.d.ts +1 -1
  123. package/dist/validation/block-structural-keys.js +1 -1
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/dist/worker/cms-worker.d.ts +1 -1
  127. package/dist/worker/cms-worker.js +2 -2
  128. package/dist/worker/github-auth.d.ts +1 -1
  129. package/dist/worker/github-auth.js +1 -1
  130. package/dist/worker/github-gateway-wire.d.ts +2 -1
  131. package/dist/worker/github-gateway-wire.js +7 -3
  132. package/dist/worker/github-gateway.js +1 -1
  133. package/dist/worker/github-mirror.d.ts +2 -2
  134. package/dist/worker/github-mirror.js +2 -2
  135. package/dist/worker/history-rewrite.d.ts +1 -1
  136. package/dist/worker/history-rewrite.js +1 -1
  137. package/dist/worker/self-checks.d.ts +1 -1
  138. package/dist/worker/shared-repo-git.js +1 -1
  139. package/package.json +5 -4
  140. package/dist/editor/EditorPanes.d.ts +0 -20
  141. package/dist/editor/EditorPanes.js +0 -124
  142. package/dist/editor/components/EditorFooter.d.ts +0 -11
  143. package/dist/editor/components/EditorFooter.js +0 -9
  144. package/dist/editor/components/EditorSidebar.d.ts +0 -70
  145. package/dist/editor/components/EditorSidebar.js +0 -50
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export declare const EDITOR_ACTIONS: {
7
7
  readonly save: "Save";
8
+ readonly saveAll: "Save all";
9
+ readonly refreshFromServer: "Refresh from server";
8
10
  readonly submitForReview: "Submit for review";
9
11
  readonly withdrawFromReview: "Withdraw from review";
10
12
  readonly requestChanges: "Request changes";
@@ -12,4 +14,5 @@ export declare const EDITOR_ACTIONS: {
12
14
  readonly newBranch: "New branch";
13
15
  readonly remove: "Remove";
14
16
  readonly clear: "Clear";
17
+ readonly openEntry: "Open entry";
15
18
  };
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export const EDITOR_ACTIONS = {
7
7
  save: 'Save',
8
+ saveAll: 'Save all',
9
+ refreshFromServer: 'Refresh from server',
8
10
  submitForReview: 'Submit for review',
9
11
  withdrawFromReview: 'Withdraw from review',
10
12
  requestChanges: 'Request changes',
@@ -12,4 +14,5 @@ export const EDITOR_ACTIONS = {
12
14
  newBranch: 'New branch',
13
15
  remove: 'Remove',
14
16
  clear: 'Clear',
17
+ openEntry: 'Open entry',
15
18
  };
@@ -69,14 +69,19 @@ export declare const buildCollectionLabels: (collections?: EditorCollection[]) =
69
69
  export declare const entryTypeLabel: (collections: EditorCollection[] | undefined, entry: Pick<EditorEntry, "collectionPath" | "entryType">) => string | undefined;
70
70
  /** An entry's navigator badge: "Missing" when its file is gone, else its type's label or name. */
71
71
  export declare const entryStatusLabel: (collections: EditorCollection[] | undefined, entry: Pick<EditorEntry, "status" | "collectionPath" | "entryType">) => string | undefined;
72
+ /** One crumb before the entry's own: "All content", a collection, or a folder of a nested slug. */
73
+ export interface BreadcrumbSegment {
74
+ label: string;
75
+ /** The collection the crumb opens in the content panel; absent on "All content" and slug folders. */
76
+ collectionPath?: string;
77
+ /** A slug folder, which is not a node of the content tree, so it navigates nowhere. */
78
+ folder?: true;
79
+ }
72
80
  /**
73
- * Builds breadcrumb segments for an entry based on its collection hierarchy.
74
- *
75
- * @param currentEntry - The entry to build breadcrumbs for (or undefined for root)
76
- * @param collectionLabels - Map of collection IDs to labels
77
- * @returns Array of breadcrumb segment strings, starting with 'All content'
81
+ * Builds breadcrumb segments for an entry based on its collection hierarchy, starting with
82
+ * "All content". The first path part is skipped as the content root, which the tree does not show.
78
83
  */
79
- export declare const buildBreadcrumbSegments: (currentEntry: EditorEntry | undefined, collectionLabels: Map<string, string>) => string[];
84
+ export declare const buildBreadcrumbSegments: (currentEntry: EditorEntry | undefined, collectionLabels: Map<string, string>) => BreadcrumbSegment[];
80
85
  /**
81
86
  * Calculates which collection nodes need to be expanded to show the path to a specific entry.
82
87
  * Recursively walks the tree to find the target entry and marks all ancestor collections as expanded.
@@ -204,34 +204,25 @@ export const entryStatusLabel = (collections, entry) => {
204
204
  return entryTypeLabel(collections, entry) ?? (entry.status && humanizeName(entry.status));
205
205
  };
206
206
  /**
207
- * Builds breadcrumb segments for an entry based on its collection hierarchy.
208
- *
209
- * @param currentEntry - The entry to build breadcrumbs for (or undefined for root)
210
- * @param collectionLabels - Map of collection IDs to labels
211
- * @returns Array of breadcrumb segment strings, starting with 'All content'
207
+ * Builds breadcrumb segments for an entry based on its collection hierarchy, starting with
208
+ * "All content". The first path part is skipped as the content root, which the tree does not show.
212
209
  */
213
210
  export const buildBreadcrumbSegments = (currentEntry, collectionLabels) => {
211
+ const segments = [{ label: 'All content' }];
214
212
  if (!currentEntry)
215
- return ['All content'];
216
- const segments = ['All content'];
217
- // Show collection hierarchy for entries that belong to a collection
213
+ return segments;
218
214
  if (currentEntry.collectionPath) {
219
- // Split the collectionPath into path parts and build cumulative paths
220
- // e.g., "content/documentation/guides" -> ["content/documentation", "content/documentation/guides"]
221
215
  const parts = currentEntry.collectionPath.split('/').filter(Boolean);
222
216
  for (let i = 1; i < parts.length; i++) {
223
217
  const pathUpToHere = parts.slice(0, i + 1).join('/');
224
218
  const label = collectionLabels.get(pathUpToHere);
225
- if (label) {
226
- segments.push(label);
227
- }
219
+ if (label)
220
+ segments.push({ label, collectionPath: pathUpToHere });
228
221
  }
229
222
  }
230
- // Add slug path segments (for nested slugs like "folder/file")
231
223
  const slugSegments = (currentEntry.slug ?? '').split('/').filter(Boolean);
232
- if (slugSegments.length > 1) {
233
- segments.push(...slugSegments.slice(0, -1));
234
- }
224
+ for (const folder of slugSegments.slice(0, -1))
225
+ segments.push({ label: folder, folder: true });
235
226
  return segments;
236
227
  };
237
228
  /**
@@ -157,12 +157,15 @@ const editorWrapperStyle = {
157
157
  * rather than scrolling sideways (`toolbar-wrap.ts` hides a separator left at a row's edge).
158
158
  * MDXEditor's dark theme leaves its page background white, so editor and popups take tokens; in
159
159
  * dark mode its drop shadow vanishes over a subtle panel, so popups take a dropdown's border.
160
+ * Its popups portal into `<body>` at z-index 2, under the editor shell; they take the layer between
161
+ * the shell and Mantine's drawers and modals, so a drawer or modal still covers them.
160
162
  */
161
163
  const EditorContentStyles = () => (_jsx("style", { children: `
162
164
  .canopy-mdx-content { min-height: 120px; padding: 8px 12px; }
163
165
  .mdxeditor .canopy-mdx-toolbar { flex-wrap: wrap; overflow-x: visible; }
164
166
  .mdxeditor { --basePageBg: var(--mantine-color-body); }
165
167
  .mdxeditor-popup-container { --basePageBg: var(--canopy-surface-subtle); }
168
+ body > .mdxeditor-popup-container { z-index: calc(var(--mantine-z-index-modal) - 1); }
166
169
  [data-mantine-color-scheme='dark'] .mdxeditor-popup-container :is([data-radix-popper-content-wrapper] > *, [role='dialog']) {
167
170
  border: 1px solid var(--mantine-color-default-border);
168
171
  }
@@ -0,0 +1,68 @@
1
+ import type { LogicalPath } from '../../paths/types.js';
2
+ /**
3
+ * Why one entry of a Save all was not saved. `unanswered` and `conflict` lose nothing yet, so
4
+ * they warn (yellow); the rest are errors (red).
5
+ */
6
+ export type SaveAllFailureKind = 'unanswered' | 'conflict' | 'invalid' | 'blocked' | 'error';
7
+ export interface SaveAllFailure {
8
+ contentId: string;
9
+ label: string;
10
+ path: LogicalPath;
11
+ kind: SaveAllFailureKind;
12
+ /** A full sentence for the result's list, after the entry's name. */
13
+ reason: string;
14
+ /** "Save as new entry", for an entry deleted since its edits started. */
15
+ recreate?: () => void;
16
+ /** Refused for now only, so the result's "Try again" can pass it. */
17
+ retryable?: true;
18
+ }
19
+ export interface SaveAllResult {
20
+ branch: string;
21
+ /** The entries Save all set out to save. */
22
+ total: number;
23
+ saved: Array<{
24
+ contentId: string;
25
+ label: string;
26
+ path: LogicalPath;
27
+ }>;
28
+ failed: SaveAllFailure[];
29
+ }
30
+ export interface SaveAllProgress {
31
+ /** Entries settled so far, saved or not. */
32
+ done: number;
33
+ total: number;
34
+ }
35
+ /** One result toast at a time: a new run replaces it, and leaving its branch takes a partly failed one down. */
36
+ export declare const SAVE_ALL_RESULT_TOAST_ID = "canopy-save-all-result";
37
+ export declare const SAVE_ALL_REASONS: {
38
+ readonly unanswered: "Saving it may have gone through even though the server didn't answer. Open it and choose \"Refresh from server\" to check before saving again.";
39
+ readonly earlierUnanswered: "An earlier save of it may have gone through even though the server didn't answer. Open it and choose \"Refresh from server\" to check before saving again.";
40
+ readonly staleBase: "It changed on the server since your edits started, so saving would overwrite someone else's changes. Open it and choose \"Refresh from server\" to see the latest version (your unsaved edits will be lost).";
41
+ readonly unknownBase: "Its unsaved changes come from an older version of the editor. Open it and save it on its own to confirm the overwrite.";
42
+ readonly unavailable: "Its entry type isn't available in this version of the site, so it can't be saved.";
43
+ readonly noEditAccess: "You don't have edit access to it.";
44
+ readonly forbidden: "You don't have permission to save it. Ask a CanopyCMS admin for access.";
45
+ readonly loadFailed: "It couldn't be loaded from the server, so it wasn't saved. Try Save all again.";
46
+ readonly renamed: "It was renamed since your edits started, so it wasn't saved. Your changes moved with it: save again.";
47
+ readonly deleted: "It was deleted since your edits started, so it wasn't saved. Choose \"Save as new entry\" to keep your changes.";
48
+ readonly goneUnchecked: "It was deleted or renamed since your edits started, so it wasn't saved, and the editor couldn't check which. Choose \"Refresh branch\" in the branch menu: if it was renamed, your changes moved with it.";
49
+ readonly failed: "Save failed. Try again.";
50
+ };
51
+ export declare const invalidReason: (count: number) => string;
52
+ /**
53
+ * Classifies a refused or unanswered save the way `handleSave` does, minus its toasts: Save all
54
+ * reports every entry in one result. `ENTRY_GONE`, a may-have-landed 409 and an unanswered write
55
+ * need the caller's follow-up, so they come back as `gone`, `landed?` and `unanswered`.
56
+ */
57
+ export declare function classifySaveAllError(err: unknown, retryOfUnanswered: boolean): {
58
+ kind: 'gone';
59
+ } | {
60
+ kind: 'landed?';
61
+ } | {
62
+ kind: 'unanswered';
63
+ } | {
64
+ kind: Exclude<SaveAllFailureKind, 'unanswered'>;
65
+ reason: string;
66
+ fieldErrors?: boolean;
67
+ retryable?: true;
68
+ };
@@ -0,0 +1,61 @@
1
+ import { ENTRY_CHANGED_MESSAGE } from '../../api/entries-constants.js';
2
+ import { SCHEMA_UNAVAILABLE_CODE } from '../unavailable-entry-type.js';
3
+ import { isUnanswered } from '../unanswered.js';
4
+ import { EDITOR_ACTIONS } from '../copy.js';
5
+ import { SaveApiError } from './useEntryManager.js';
6
+ /** One result toast at a time: a new run replaces it, and leaving its branch takes a partly failed one down. */
7
+ export const SAVE_ALL_RESULT_TOAST_ID = 'canopy-save-all-result';
8
+ const ENTRY_GONE_CODE = 'ENTRY_GONE';
9
+ const BRANCH_SYNCING_CODE = 'BRANCH_SYNCING';
10
+ export const SAVE_ALL_REASONS = {
11
+ unanswered: `Saving it may have gone through even though the server didn't answer. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to check before saving again.`,
12
+ earlierUnanswered: `An earlier save of it may have gone through even though the server didn't answer. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to check before saving again.`,
13
+ staleBase: `It changed on the server since your edits started, so saving would overwrite someone else's changes. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to see the latest version (your unsaved edits will be lost).`,
14
+ unknownBase: 'Its unsaved changes come from an older version of the editor. Open it and save it on its own to confirm the overwrite.',
15
+ unavailable: "Its entry type isn't available in this version of the site, so it can't be saved.",
16
+ noEditAccess: "You don't have edit access to it.",
17
+ forbidden: "You don't have permission to save it. Ask a CanopyCMS admin for access.",
18
+ loadFailed: "It couldn't be loaded from the server, so it wasn't saved. Try Save all again.",
19
+ renamed: "It was renamed since your edits started, so it wasn't saved. Your changes moved with it: save again.",
20
+ deleted: 'It was deleted since your edits started, so it wasn\'t saved. Choose "Save as new entry" to keep your changes.',
21
+ goneUnchecked: 'It was deleted or renamed since your edits started, so it wasn\'t saved, and the editor couldn\'t check which. Choose "Refresh branch" in the branch menu: if it was renamed, your changes moved with it.',
22
+ failed: 'Save failed. Try again.',
23
+ };
24
+ export const invalidReason = (count) => `${count} ${count === 1 ? 'field needs' : 'fields need'} fixing. Open it to see which.`;
25
+ /**
26
+ * Classifies a refused or unanswered save the way `handleSave` does, minus its toasts: Save all
27
+ * reports every entry in one result. `ENTRY_GONE`, a may-have-landed 409 and an unanswered write
28
+ * need the caller's follow-up, so they come back as `gone`, `landed?` and `unanswered`.
29
+ */
30
+ export function classifySaveAllError(err, retryOfUnanswered) {
31
+ if (isUnanswered(err))
32
+ return { kind: 'unanswered' };
33
+ if (!(err instanceof SaveApiError))
34
+ return { kind: 'error', reason: SAVE_ALL_REASONS.failed };
35
+ if (err.status === 409) {
36
+ if (err.code === ENTRY_GONE_CODE)
37
+ return { kind: 'gone' };
38
+ // The retry of a save that got no answer, refused as changed, most likely bounced off itself.
39
+ if (err.code === 'WRITE_OUTCOME_UNKNOWN' ||
40
+ (retryOfUnanswered && err.serverMessage === ENTRY_CHANGED_MESSAGE)) {
41
+ return { kind: 'landed?' };
42
+ }
43
+ if (err.code === BRANCH_SYNCING_CODE) {
44
+ return { kind: 'conflict', reason: err.message, retryable: true };
45
+ }
46
+ // The version check's own wording names the single-entry recovery, so Save all says it its way.
47
+ const changed = !err.serverMessage || err.serverMessage === ENTRY_CHANGED_MESSAGE;
48
+ return { kind: 'conflict', reason: changed ? SAVE_ALL_REASONS.staleBase : err.serverMessage };
49
+ }
50
+ if (err.code === SCHEMA_UNAVAILABLE_CODE)
51
+ return { kind: 'blocked', reason: err.message };
52
+ if (err.status === 422) {
53
+ return { kind: 'invalid', reason: err.message, fieldErrors: !!err.fieldErrors?.length };
54
+ }
55
+ // A path rule's refusal says only "Forbidden"; the guards' own refusals explain themselves.
56
+ if (err.status === 403) {
57
+ const bare = !err.serverMessage || err.serverMessage === 'Forbidden';
58
+ return { kind: 'blocked', reason: bare ? SAVE_ALL_REASONS.forbidden : err.serverMessage };
59
+ }
60
+ return { kind: 'error', reason: SAVE_ALL_REASONS.failed };
61
+ }
@@ -9,6 +9,8 @@ export interface UseCommentSystemOptions {
9
9
  canResolveComments: boolean;
10
10
  setSelectedPath: (id: string) => void;
11
11
  setBranchManagerOpen: (open: boolean) => void;
12
+ /** Uncovers the form before anything here focuses one of its fields. */
13
+ onRevealForm?: () => void;
12
14
  onCommentsChange?: (comments: CommentThread[]) => void;
13
15
  }
14
16
  export interface UseCommentSystemReturn {
@@ -172,6 +172,7 @@ export function useCommentSystem(options) {
172
172
  later(() => {
173
173
  target.style.boxShadow = previous;
174
174
  }, 1200);
175
+ options.onRevealForm?.();
175
176
  // Cleared shortly after, once FieldWrapper has seen it and scrolled to the field.
176
177
  setFocusedFieldPath(found.path);
177
178
  later(() => setFocusedFieldPath(undefined), 100);
@@ -196,6 +197,7 @@ export function useCommentSystem(options) {
196
197
  if (target) {
197
198
  whenRevealed(target, () => target.scrollIntoView({ behavior: 'smooth', block: 'center' }));
198
199
  }
200
+ options.onRevealForm?.();
199
201
  setFocusedFieldPath(canopyPath);
200
202
  setHighlightThreadId(threadId);
201
203
  window.setTimeout(() => {
@@ -214,6 +216,7 @@ export function useCommentSystem(options) {
214
216
  if (formElement) {
215
217
  formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
216
218
  }
219
+ options.onRevealForm?.();
217
220
  setHighlightThreadId(threadId);
218
221
  window.setTimeout(() => {
219
222
  setHighlightThreadId(undefined);
@@ -2,10 +2,11 @@ import type { EditorEntry } from '../Editor.js';
2
2
  import type { LogicalPath } from '../../paths/types.js';
3
3
  import type { FormValue } from '../FormRenderer.js';
4
4
  import type { StampedEntry } from '../../stamp-on-publish.js';
5
+ import { type SaveAllProgress, type SaveAllResult } from './save-all.js';
5
6
  /** @internal Exported for tests. */
6
7
  export declare const SAVE_TIMED_OUT_MESSAGE = "Save didn't finish, and may not have gone through. Your changes are still here, so save again.";
7
8
  /** @internal Exported for tests. Shown when a save may already have landed, until a reload. */
8
- export declare const SAVE_MAY_HAVE_LANDED_MESSAGE = "Your last save may already have gone through. Before saving again, choose \"Reload entry\" in the menu under the entry's name; your unsaved edits will be lost.";
9
+ export declare const SAVE_MAY_HAVE_LANDED_MESSAGE = "Your last save may already have gone through. Before saving again, choose \"Refresh from server\" in the entry's menu, on its name; your unsaved edits will be lost.";
9
10
  /** @internal Exported for tests. A save refused because the entry was deleted. */
10
11
  export declare const ENTRY_DELETED_MESSAGE = "This entry was deleted since you opened it, so it was not saved. Your changes are still in the form until the content list next refreshes and closes it: choose \"Save as new entry\" to keep them.";
11
12
  /** @internal Exported for tests. A save refused because the entry was renamed; the editor has opened it. */
@@ -50,6 +51,8 @@ export interface UseDraftManagerOptions {
50
51
  * instead of the stale label from the last entries fetch.
51
52
  */
52
53
  onSaved?: () => void;
54
+ /** A save was refused for field errors, which the form now shows. */
55
+ onSaveBlocked?: () => void;
53
56
  /**
54
57
  * Fired when drafts are thrown away for the server's copy: Discard (one entry, or every entry
55
58
  * when `contentId` is undefined) and Reload. Anything built on the discarded draft, such as an
@@ -80,6 +83,26 @@ export interface UseDraftManagerReturn {
80
83
  label: string;
81
84
  }>;
82
85
  handleSave: () => Promise<void>;
86
+ /** The entry a single Save is writing, while it is in flight. */
87
+ singleSaving: {
88
+ branch: string;
89
+ contentId: string;
90
+ } | null;
91
+ /** The last Save that landed; its `seq` rises with each, and the Save button says "Saved". */
92
+ lastSaved: {
93
+ branch: string;
94
+ contentId: string;
95
+ seq: number;
96
+ } | null;
97
+ /**
98
+ * Saves every entry with unsaved changes on the branch, one at a time. Undefined when a run or
99
+ * a single Save is already in flight, or the branch changed or the editor unmounted meanwhile.
100
+ */
101
+ handleSaveAll: () => Promise<SaveAllResult | undefined>;
102
+ /** Set while Save all runs. */
103
+ saveAllProgress: SaveAllProgress | null;
104
+ /** Whether Save all is running now, read at the moment of a click rather than a render. */
105
+ isSaveAllRunning: () => boolean;
83
106
  handleDiscardDrafts: (returnFocusTo?: () => HTMLElement | null) => void;
84
107
  /** `announce` toasts the discard, for a draft the form isn't showing (a read-only branch's). */
85
108
  handleDiscardFileDraft: (returnFocusTo?: () => HTMLElement | null, options?: {