canopycms 0.0.68-int.98 → 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 (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -5,17 +5,41 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  *
6
6
  * A collection has a name (machine-readable id), label (display name), and
7
7
  * entries (entry types defining what content can be created). Create mode
8
- * requires at least one entry type; edit mode only allows changing name and
9
- * label (entry types are managed separately).
8
+ * stages everything locally and sends it with "Create collection"; Cancel
9
+ * discards it. Edit mode stages name, label and slug until "Save collection
10
+ * details", but every entry-type add, update and remove saves to the branch
11
+ * at once. The footer's dismiss button says what leaving does: "Discard
12
+ * changes" while details are staged, "Close" once an entry-type change has
13
+ * saved or may have, else "Cancel" (which would promise a revert it cannot do
14
+ * after one).
10
15
  */
11
- import { useState, useCallback, useEffect } from 'react';
12
- import { Modal, Stack, TextInput, Group, Button, Alert, Text, Paper, ActionIcon, Menu, Divider, Badge, } from '@mantine/core';
13
- import { IconAlertCircle, IconPlus, IconDotsVertical, IconTrash, IconEdit, IconStar, } from '@tabler/icons-react';
16
+ import { useState, useCallback, useEffect, useRef } from 'react';
17
+ import { Modal, Stack, TextInput, Group, Button, Alert, Text, Paper, ActionIcon, Menu, Title, Badge, Tooltip, } from '@mantine/core';
18
+ import { IconAlertCircle, IconAlertTriangle, IconRestore, IconCheck, IconPlus, IconDotsVertical, IconTrash, IconEdit, IconStar, } from '@tabler/icons-react';
14
19
  import { EntryTypeEditor } from './EntryTypeEditor.js';
15
20
  import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal.js';
21
+ import { DestructiveMenuItem } from '../components/DestructiveMenuItem.js';
16
22
  import { getErrorMessage } from '../../utils/error.js';
17
- export function CollectionEditor({ isOpen, editingCollection, parentPath, availableSchemas, onSave, onAddEntryType, onUpdateEntryType, onRemoveEntryType, onClose, isSaving = false, error = null, }) {
23
+ import { slugOf } from './collection-draft.js';
24
+ /** Whether a listed entry type holds every field an add or update sent. */
25
+ function showsFields(entryType, fields) {
26
+ return Object.entries(fields).every(([key, value]) => key === 'default'
27
+ ? !!entryType.default === !!value
28
+ : entryType[key] === value);
29
+ }
30
+ function usageOf(entryType) {
31
+ return 'usageCount' in entryType ? (entryType.usageCount ?? 0) : 0;
32
+ }
33
+ export function CollectionEditor({ isOpen, editingCollection, parentPath, availableSchemas, onSave, onAddEntryType, onUpdateEntryType, onRemoveEntryType, onReloadEntryTypes, onClose, isSaving = false, error = null, restored = null, onDiscardRestored, }) {
18
34
  const isEditMode = editingCollection !== null;
35
+ // Read when the dialog opens only: dropping it later must not reset what the user has typed.
36
+ const restoredRef = useRef(restored);
37
+ restoredRef.current = restored;
38
+ const [showingRestored, setShowingRestored] = useState(false);
39
+ const nameInputRef = useRef(null);
40
+ // Staged removals, latest last; Undo stays in the dialog, as a toast's is outside the focus trap.
41
+ const [stagedRemovals, setStagedRemovals] = useState([]);
42
+ const stagedRemoval = stagedRemovals.at(-1);
19
43
  const [formData, setFormData] = useState({
20
44
  name: '',
21
45
  label: '',
@@ -29,33 +53,95 @@ export function CollectionEditor({ isOpen, editingCollection, parentPath, availa
29
53
  const [editingEntryTypeIndex, setEditingEntryTypeIndex] = useState(null);
30
54
  const [entryTypeSaving, setEntryTypeSaving] = useState(false);
31
55
  const [entryTypeError, setEntryTypeError] = useState(null);
56
+ const [errorDismissed, setErrorDismissed] = useState(false);
57
+ // Edit mode renders this list rather than `editingCollection.entries`, a snapshot taken when
58
+ // the dialog opened that the parent never refreshes. Each immediate action patches it.
59
+ const [entryTypes, setEntryTypes] = useState(() => editingCollection?.entries ?? []);
60
+ // True once an immediate entry-type change has saved, or may have, in this opening of the dialog.
61
+ const [entryTypeChanged, setEntryTypeChanged] = useState(false);
62
+ const [entryTypeStatus, setEntryTypeStatus] = useState('');
63
+ // An unanswered remove the re-read did not settle, shown above the list.
64
+ const [entryTypeUnanswered, setEntryTypeUnanswered] = useState(null);
65
+ // An unanswered add or update the re-read did not settle, shown in the entry type dialog,
66
+ // which stays open with the user's input.
67
+ const [entryTypeWarning, setEntryTypeWarning] = useState(null);
68
+ const [removeFailure, setRemoveFailure] = useState(null);
69
+ const [removing, setRemoving] = useState(false);
70
+ const [menuOpenFor, setMenuOpenFor] = useState(null);
71
+ // Closing an overlay returns focus to its opener, but a row's menu item is gone once it acts,
72
+ // so Mantine's own return lands nowhere. `row` names the trigger to focus; null is the heading.
73
+ const [focusRequest, setFocusRequest] = useState(null);
74
+ const triggerRefs = useRef(new Map());
75
+ const entryTypesHeadingRef = useRef(null);
32
76
  const [deleteEntryTypeModalOpen, setDeleteEntryTypeModalOpen] = useState(false);
33
77
  const [deletingEntryType, setDeletingEntryType] = useState(null);
34
78
  useEffect(() => {
35
79
  if (isOpen) {
80
+ const prefill = restoredRef.current;
36
81
  if (editingCollection) {
37
82
  setFormData({
38
- name: editingCollection.name,
39
- label: editingCollection.label || '',
40
- entries: [], // Entry types are managed separately in edit mode
83
+ name: prefill?.name ?? editingCollection.name,
84
+ label: prefill?.label ?? (editingCollection.label || ''),
85
+ entries: [], // Edit mode keeps its entry types in `entryTypes`
41
86
  });
42
- // Slug is the logical path's last segment before its embedded id (e.g. "posts.abc123" -> "posts").
43
- const pathParts = editingCollection.logicalPath.split('/');
44
- const lastPart = pathParts[pathParts.length - 1];
45
- const slugPart = lastPart?.split('.')[0] || '';
46
- setSlug(slugPart);
87
+ setEntryTypes(editingCollection.entries);
88
+ setSlug(prefill?.slug ?? slugOf(editingCollection.logicalPath));
47
89
  }
48
90
  else {
49
91
  setFormData({
50
- name: '',
51
- label: '',
52
- entries: [],
92
+ name: prefill?.name ?? '',
93
+ label: prefill?.label ?? '',
94
+ entries: prefill?.entries ?? [],
53
95
  });
54
96
  setSlug('');
97
+ setEntryTypes([]);
55
98
  }
99
+ setShowingRestored(!!prefill);
100
+ setStagedRemovals([]);
56
101
  setValidationError(null);
102
+ setEntryTypeChanged(false);
103
+ setEntryTypeStatus('');
104
+ setEntryTypeUnanswered(null);
105
+ setRemoveFailure(null);
106
+ }
107
+ else {
108
+ // The parent closes this dialog (and nulls `editingCollection`) when details save, so a
109
+ // sub-dialog left open would stage into a closed dialog in create mode.
110
+ setEntryTypeEditorOpen(false);
111
+ setEditingEntryType(null);
112
+ setEditingEntryTypeIndex(null);
113
+ setEntryTypeError(null);
114
+ setDeleteEntryTypeModalOpen(false);
57
115
  }
58
116
  }, [isOpen, editingCollection]);
117
+ useEffect(() => {
118
+ setErrorDismissed(false);
119
+ }, [error, isOpen, editingCollection]);
120
+ useEffect(() => {
121
+ if (!focusRequest)
122
+ return;
123
+ // Runs after the overlay's own focus return, which Mantine's useFocusReturn delays by 10ms.
124
+ const timer = window.setTimeout(() => {
125
+ const target = focusRequest.row === null
126
+ ? entryTypesHeadingRef.current
127
+ : triggerRefs.current.get(focusRequest.row);
128
+ target?.focus();
129
+ setFocusRequest(null);
130
+ }, 50);
131
+ return () => window.clearTimeout(timer);
132
+ }, [focusRequest]);
133
+ const discardRestored = useCallback(() => {
134
+ setFormData({
135
+ name: editingCollection?.name ?? '',
136
+ label: editingCollection?.label ?? '',
137
+ entries: [],
138
+ });
139
+ setSlug(editingCollection ? slugOf(editingCollection.logicalPath) : '');
140
+ setShowingRestored(false);
141
+ setStagedRemovals([]);
142
+ onDiscardRestored?.();
143
+ nameInputRef.current?.focus();
144
+ }, [editingCollection, onDiscardRestored]);
59
145
  const updateField = useCallback((field, value) => {
60
146
  setFormData((prev) => ({ ...prev, [field]: value }));
61
147
  setValidationError(null);
@@ -92,9 +178,7 @@ export function CollectionEditor({ isOpen, editingCollection, parentPath, availa
92
178
  if (formData.label !== (editingCollection?.label || '')) {
93
179
  updates.label = formData.label || undefined;
94
180
  }
95
- const pathParts = editingCollection?.logicalPath.split('/') || [];
96
- const lastPart = pathParts[pathParts.length - 1];
97
- const currentSlug = lastPart?.split('.')[0] || '';
181
+ const currentSlug = editingCollection ? slugOf(editingCollection.logicalPath) : '';
98
182
  if (slug && slug !== currentSlug) {
99
183
  updates.slug = slug;
100
184
  }
@@ -126,18 +210,101 @@ export function CollectionEditor({ isOpen, editingCollection, parentPath, availa
126
210
  setEntryTypeError(null);
127
211
  setEntryTypeEditorOpen(true);
128
212
  }, []);
213
+ const announceEntryTypeChange = useCallback((message) => {
214
+ setEntryTypeStatus(message);
215
+ setEntryTypeChanged(true);
216
+ }, []);
217
+ /**
218
+ * After an entry-type action the server never answered, which may have landed: re-reads the
219
+ * list from the branch and shows it, and counts the action as a possible change (the footer
220
+ * reads "Close"). Resolves to the re-read list, or null when it could not be read.
221
+ */
222
+ const reloadEntryTypes = useCallback(async (collectionPath) => {
223
+ setEntryTypeChanged(true);
224
+ const reloaded = await (onReloadEntryTypes?.(collectionPath) ?? Promise.resolve(null)).catch((err) => {
225
+ console.error(err);
226
+ return null;
227
+ });
228
+ if (reloaded)
229
+ setEntryTypes(reloaded);
230
+ return reloaded;
231
+ }, [onReloadEntryTypes]);
232
+ const closeEntryTypeEditor = useCallback(() => {
233
+ setEntryTypeEditorOpen(false);
234
+ setEditingEntryType(null);
235
+ setEditingEntryTypeIndex(null);
236
+ setEntryTypeError(null);
237
+ setEntryTypeWarning(null);
238
+ }, []);
239
+ // Esc, the close button and a click outside all land here; closing mid-request would hide
240
+ // the request's result.
241
+ const dismissEntryTypeEditor = useCallback(() => {
242
+ if (entryTypeSaving)
243
+ return;
244
+ const editedName = editingEntryType?.name;
245
+ closeEntryTypeEditor();
246
+ if (editedName)
247
+ setFocusRequest({ row: editedName });
248
+ }, [entryTypeSaving, editingEntryType, closeEntryTypeEditor]);
129
249
  const handleEntryTypeSave = useCallback(async (data, isNew) => {
250
+ const editedName = isNew ? undefined : editingEntryType?.name;
251
+ const finish = () => {
252
+ closeEntryTypeEditor();
253
+ if (editedName)
254
+ setFocusRequest({ row: editedName });
255
+ };
130
256
  if (isEditMode && editingCollection) {
257
+ // The server skips undefined fields, so a cleared label or max items keeps its stored
258
+ // value: the list shows what was stored, and an update with none saves nothing.
259
+ const stored = Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined));
260
+ if (!isNew && Object.keys(stored).length === 0) {
261
+ finish();
262
+ return;
263
+ }
131
264
  setEntryTypeSaving(true);
132
265
  setEntryTypeError(null);
266
+ setEntryTypeWarning(null);
267
+ setEntryTypeStatus('');
268
+ setEntryTypeUnanswered(null);
269
+ setRemoveFailure(null);
133
270
  try {
134
- // Absent props (optional chaining short-circuits to `undefined`) are
135
- // treated as success — there's nothing to report back for them.
136
- const result = isNew
137
- ? await onAddEntryType?.(editingCollection.logicalPath, data)
138
- : editingEntryType
139
- ? await onUpdateEntryType?.(editingCollection.logicalPath, editingEntryType.name, data)
140
- : undefined;
271
+ // An absent callback leaves `result` undefined: nothing was saved, so nothing is
272
+ // applied locally or announced.
273
+ let result;
274
+ if (isNew) {
275
+ const entryType = data;
276
+ result = await onAddEntryType?.(editingCollection.logicalPath, entryType);
277
+ if (result?.ok) {
278
+ setEntryTypes((prev) => [...prev, { ...entryType, usageCount: 0 }]);
279
+ announceEntryTypeChange(`Entry type "${entryType.name}" added`);
280
+ }
281
+ }
282
+ else if (editingEntryType) {
283
+ const name = editingEntryType.name;
284
+ result = await onUpdateEntryType?.(editingCollection.logicalPath, name, data);
285
+ if (result?.ok) {
286
+ setEntryTypes((prev) => prev.map((et) => (et.name === name ? { ...et, ...stored } : et)));
287
+ announceEntryTypeChange(`Entry type "${name}" updated`);
288
+ }
289
+ }
290
+ if (result && !result.ok && result.unanswered) {
291
+ // The re-read settles it when the branch already shows the change. Otherwise this
292
+ // dialog keeps the input: re-sending an update sends the same change, and re-sending
293
+ // an add that had landed is refused because the name exists (`schema-store.ts`).
294
+ const name = isNew ? data.name : editingEntryType?.name;
295
+ const reloaded = await reloadEntryTypes(editingCollection.logicalPath);
296
+ const now = reloaded?.find((et) => et.name === name);
297
+ if (name && now && showsFields(now, stored)) {
298
+ announceEntryTypeChange(`Entry type "${name}" ${isNew ? 'added' : 'updated'}`);
299
+ closeEntryTypeEditor();
300
+ setFocusRequest({ row: name });
301
+ return;
302
+ }
303
+ setEntryTypeWarning(isNew
304
+ ? `"${name}" may not have been added. Select "Add entry type" to try again: if it was added, you'll be told it already exists.`
305
+ : 'The change may not have been saved. Select "Save entry type" to send it again.');
306
+ return;
307
+ }
141
308
  if (result && !result.ok) {
142
309
  setEntryTypeError(result.error);
143
310
  return;
@@ -168,9 +335,7 @@ export function CollectionEditor({ isOpen, editingCollection, parentPath, availa
168
335
  }));
169
336
  }
170
337
  }
171
- setEntryTypeEditorOpen(false);
172
- setEditingEntryType(null);
173
- setEditingEntryTypeIndex(null);
338
+ finish();
174
339
  }, [
175
340
  isEditMode,
176
341
  editingCollection,
@@ -178,45 +343,179 @@ export function CollectionEditor({ isOpen, editingCollection, parentPath, availa
178
343
  editingEntryTypeIndex,
179
344
  onAddEntryType,
180
345
  onUpdateEntryType,
346
+ announceEntryTypeChange,
347
+ reloadEntryTypes,
348
+ closeEntryTypeEditor,
181
349
  ]);
350
+ const displayEntryTypes = isEditMode
351
+ ? entryTypes
352
+ : formData.entries;
353
+ // Create mode stages entry types, so a removal goes at once with an Undo; edit mode's saves at
354
+ // once with no restore, so it confirms (docs/ux-guidelines.md, Destructive actions).
355
+ const removeStagedEntryType = useCallback((entryType, index) => {
356
+ const names = formData.entries.map((et) => et.name);
357
+ const neighbour = names[index + 1] ?? names[index - 1] ?? null;
358
+ setFormData((prev) => ({ ...prev, entries: prev.entries.filter((_, i) => i !== index) }));
359
+ setStagedRemovals((prev) => [...prev, { entryType, index }]);
360
+ setFocusRequest({ row: neighbour });
361
+ }, [formData.entries]);
362
+ const undoStagedRemoval = useCallback(() => {
363
+ if (!stagedRemoval)
364
+ return;
365
+ const { entryType, index } = stagedRemoval;
366
+ // Into the list as it is now, unless an entry type of that name was added since.
367
+ setFormData((prev) => prev.entries.some((et) => et.name === entryType.name)
368
+ ? prev
369
+ : {
370
+ ...prev,
371
+ entries: [...prev.entries.slice(0, index), entryType, ...prev.entries.slice(index)],
372
+ });
373
+ setStagedRemovals((prev) => prev.slice(0, -1));
374
+ setFocusRequest({ row: entryType.name });
375
+ }, [stagedRemoval]);
182
376
  const handleRemoveEntryType = useCallback((entryType, index) => {
183
- setDeletingEntryType({ entryType, index });
377
+ if (!isEditMode) {
378
+ removeStagedEntryType(entryType, index);
379
+ return;
380
+ }
381
+ setDeletingEntryType({ entryType });
184
382
  setDeleteEntryTypeModalOpen(true);
383
+ }, [isEditMode, removeStagedEntryType]);
384
+ // Only closes: the target is replaced on the next open, so the message stays put while the
385
+ // dialog fades out.
386
+ const closeRemoveConfirm = useCallback(() => {
387
+ setDeleteEntryTypeModalOpen(false);
185
388
  }, []);
186
- const confirmRemoveEntryType = useCallback(() => {
389
+ const cancelRemoveConfirm = useCallback(() => {
390
+ closeRemoveConfirm();
391
+ if (deletingEntryType)
392
+ setFocusRequest({ row: deletingEntryType.entryType.name });
393
+ }, [closeRemoveConfirm, deletingEntryType]);
394
+ const removeEntryType = useCallback(async (name) => {
395
+ const names = displayEntryTypes.map((et) => et.name);
396
+ // Focus moves to the row that takes the removed one's place, else the one before it, else
397
+ // the heading.
398
+ const neighbour = names[names.indexOf(name) + 1] ?? names[names.indexOf(name) - 1] ?? null;
399
+ if (editingCollection) {
400
+ if (!onRemoveEntryType)
401
+ return;
402
+ setRemoving(true);
403
+ setEntryTypeStatus('');
404
+ setEntryTypeUnanswered(null);
405
+ try {
406
+ const result = await onRemoveEntryType(editingCollection.logicalPath, name);
407
+ if (result.ok) {
408
+ setEntryTypes((prev) => prev.filter((et) => et.name !== name));
409
+ setRemoveFailure(null);
410
+ announceEntryTypeChange(`Entry type "${name}" removed`);
411
+ setFocusRequest({ row: neighbour });
412
+ }
413
+ else if (result.unanswered) {
414
+ setRemoveFailure(null);
415
+ // The re-read settles it when the entry type is gone.
416
+ const reloaded = await reloadEntryTypes(editingCollection.logicalPath);
417
+ const listed = reloaded?.map((et) => et.name);
418
+ if (listed && !listed.includes(name)) {
419
+ announceEntryTypeChange(`Entry type "${name}" removed`);
420
+ setFocusRequest({ row: neighbour && listed.includes(neighbour) ? neighbour : null });
421
+ }
422
+ else {
423
+ setEntryTypeUnanswered(listed
424
+ ? `"${name}" is still listed, so it may not have been removed yet. Remove it again to retry, or close and reopen this collection later to check.`
425
+ : `"${name}" may already have been removed. Close and reopen this collection to check before trying again.`);
426
+ setFocusRequest({ row: name });
427
+ }
428
+ }
429
+ else {
430
+ setRemoveFailure({ name, message: result.error });
431
+ setFocusRequest({ row: name });
432
+ }
433
+ }
434
+ catch (err) {
435
+ setRemoveFailure({ name, message: getErrorMessage(err) });
436
+ setFocusRequest({ row: name });
437
+ }
438
+ finally {
439
+ setRemoving(false);
440
+ }
441
+ }
442
+ }, [
443
+ displayEntryTypes,
444
+ editingCollection,
445
+ onRemoveEntryType,
446
+ announceEntryTypeChange,
447
+ reloadEntryTypes,
448
+ ]);
449
+ const confirmRemoveEntryType = useCallback(async () => {
187
450
  if (!deletingEntryType)
188
451
  return;
189
- const { entryType, index } = deletingEntryType;
190
- if (isEditMode && editingCollection) {
191
- onRemoveEntryType?.(editingCollection.logicalPath, entryType.name);
452
+ await removeEntryType(deletingEntryType.entryType.name);
453
+ closeRemoveConfirm();
454
+ }, [deletingEntryType, removeEntryType, closeRemoveConfirm]);
455
+ const removeMessage = deletingEntryType
456
+ ? `Remove "${deletingEntryType.entryType.name}"? This saves immediately and cannot be undone.`
457
+ : '';
458
+ const removeBlockedReason = (entryType) => {
459
+ if (displayEntryTypes.length === 1) {
460
+ return 'A collection needs at least one entry type. Add another before removing this one.';
192
461
  }
193
- else {
194
- setFormData((prev) => ({
195
- ...prev,
196
- entries: prev.entries.filter((_, i) => i !== index),
197
- }));
198
- }
199
- setDeleteEntryTypeModalOpen(false);
200
- setDeletingEntryType(null);
201
- }, [deletingEntryType, isEditMode, editingCollection, onRemoveEntryType]);
202
- const displayEntryTypes = isEditMode
203
- ? editingCollection?.entries || []
204
- : formData.entries;
205
- const displayError = error || validationError;
206
- return (_jsxs(_Fragment, { children: [_jsx(Modal, { opened: isOpen, onClose: onClose, title: isEditMode ? `Edit Collection: ${editingCollection?.name}` : 'Create Collection', size: "lg", children: _jsxs(Stack, { gap: "md", children: [displayError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Error", withCloseButton: true, onClose: () => setValidationError(null), children: displayError })), _jsx(TextInput, { label: "Name", description: "Machine-readable identifier (e.g., posts, pages, articles)", placeholder: "posts", value: formData.name, onChange: (e) => updateField('name', e.target.value), required: true }), _jsx(TextInput, { label: "Label", description: "Human-readable display name", placeholder: "Blog Posts", value: formData.label, onChange: (e) => updateField('label', e.target.value) }), isEditMode && (_jsx(TextInput, { label: "Slug", description: "Directory name (filesystem path). Changing this renames the directory.", placeholder: "posts", value: slug, onChange: (e) => {
462
+ // The server refuses to remove an entry type that entries use. Staged ones have no entries.
463
+ const used = isEditMode ? usageOf(entryType) : 0;
464
+ return used > 0
465
+ ? `${used} ${used === 1 ? 'entry uses' : 'entries use'} this entry type. Move or delete ${used === 1 ? 'it' : 'them'} first.`
466
+ : undefined;
467
+ };
468
+ // The hook's single `isLoading` flag drives `isSaving`, so it is also true while an
469
+ // entry-type action runs; the footer must not spin or close over that action's result.
470
+ const entryTypeBusy = entryTypeSaving || removing;
471
+ // Any request in flight, a details save included: the parent closes this dialog when a details
472
+ // save succeeds, so an entry-type action started meanwhile would report into a closed dialog.
473
+ const requestBusy = isSaving || entryTypeBusy;
474
+ // Mantine's Escape handler fires on every open Modal, so one Esc would close a sub-dialog and
475
+ // this dialog together, losing staged details. A request in flight must not be hidden either.
476
+ const dismissBlocked = requestBusy || entryTypeEditorOpen || deleteEntryTypeModalOpen;
477
+ const propErrorVisible = !!error && !errorDismissed;
478
+ const displayError = propErrorVisible ? error : validationError;
479
+ const errorTitle = propErrorVisible
480
+ ? isEditMode
481
+ ? "Couldn't save collection details"
482
+ : "Couldn't create collection"
483
+ : undefined;
484
+ // The comparisons handleSave uses to build its update, so "Discard changes" shows exactly when
485
+ // that update would carry a change.
486
+ const detailsDirty = editingCollection !== null &&
487
+ (formData.name !== editingCollection.name ||
488
+ formData.label !== (editingCollection.label || '') ||
489
+ (!!slug && slug !== slugOf(editingCollection.logicalPath)));
490
+ const createStaged = !isEditMode && (!!formData.name || !!formData.label || formData.entries.length > 0);
491
+ const dismissLabel = detailsDirty
492
+ ? 'Discard changes'
493
+ : isEditMode && entryTypeChanged
494
+ ? 'Close'
495
+ : 'Cancel';
496
+ const entryTypesSection = (_jsxs(Stack, { gap: "md", children: [_jsxs("div", { children: [_jsx(Title, { order: 3, size: "h5", tabIndex: -1, ref: entryTypesHeadingRef, children: "Entry types" }), isEditMode && (_jsx(Text, { size: "sm", c: "dimmed", children: "Changes here save to the branch immediately." }))] }), entryTypeUnanswered && (_jsx(Alert, { icon: _jsx(IconAlertTriangle, { size: 16 }), color: "yellow", title: "No answer from the server", withCloseButton: true, onClose: () => {
497
+ setEntryTypeUnanswered(null);
498
+ setFocusRequest({ row: null });
499
+ }, children: entryTypeUnanswered })), displayEntryTypes.length === 0 ? (_jsx(Paper, { p: "md", withBorder: true, children: _jsx(Text, { c: "dimmed", ta: "center", size: "sm", children: "No entry types yet. Add at least one to choose what kinds of entries this collection can hold." }) })) : (_jsx(Stack, { gap: "xs", children: displayEntryTypes.map((entryType, index) => (_jsx(Paper, { p: "sm", withBorder: true, children: _jsxs(Group, { justify: "space-between", wrap: "nowrap", children: [_jsx(Group, { gap: "sm", children: _jsxs("div", { children: [_jsxs(Group, { gap: "xs", children: [_jsx(Text, { fw: 500, size: "sm", children: entryType.name }), entryType.default && (_jsx(Badge, { size: "xs", color: "blue", variant: "light", leftSection: _jsx(IconStar, { size: 10 }), children: "Default" })), entryType.maxItems === 1 && (_jsx(Badge, { size: "xs", color: "gray", variant: "light", children: "Singleton" }))] }), _jsxs(Text, { size: "xs", c: "dimmed", children: [entryType.format.toUpperCase(), " \u00B7 ", entryType.schema, entryType.label && ` · "${entryType.label}"`] })] }) }), _jsxs(Menu, { position: "bottom-end", withinPortal: true, onChange: (opened) => setMenuOpenFor(opened ? entryType.name : null), children: [_jsx(Tooltip, { label: `Actions for ${entryType.name}`, disabled: menuOpenFor === entryType.name, children: _jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", "aria-label": `Actions for ${entryType.name}`, disabled: requestBusy, ref: (el) => {
500
+ if (el)
501
+ triggerRefs.current.set(entryType.name, el);
502
+ else
503
+ triggerRefs.current.delete(entryType.name);
504
+ }, children: _jsx(IconDotsVertical, { size: 16, "aria-hidden": true }) }) }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconEdit, { size: 14 }), onClick: () => handleOpenEditEntryType(entryType, index), children: "Edit\u2026" }), _jsx(Menu.Divider, {}), _jsx(DestructiveMenuItem, { leftSection: _jsx(IconTrash, { size: 14 }), onClick: () => handleRemoveEntryType(entryType, index), blockedReason: removeBlockedReason(entryType), children: "Remove" })] })] })] }) }, entryType.name))) })), _jsxs(Group, { gap: 6, wrap: "nowrap", role: "status", mih: "calc(var(--mantine-font-size-sm) * var(--mantine-line-height-sm))", children: [isEditMode && entryTypeStatus && (_jsxs(_Fragment, { children: [_jsx(IconCheck, { size: 14, color: "var(--mantine-color-green-text)", "aria-hidden": true }), _jsx(Text, { size: "sm", children: entryTypeStatus })] })), !isEditMode && stagedRemoval && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", children: `Removed entry type "${stagedRemoval.entryType.name}".` }), _jsx(Button, { size: "compact-sm", variant: "subtle", onClick: undoStagedRemoval, "aria-label": `Undo removing entry type "${stagedRemoval.entryType.name}"`, children: "Undo" })] }))] }), removeFailure && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Couldn't remove entry type", withCloseButton: true, onClose: () => {
505
+ setRemoveFailure(null);
506
+ setFocusRequest({ row: null });
507
+ }, children: _jsxs(Stack, { gap: "xs", align: "flex-start", children: [_jsx(Text, { size: "sm", children: removeFailure.message }), _jsx(Button, { size: "xs", variant: "light", color: "red", loading: removing, disabled: requestBusy, onClick: () => void removeEntryType(removeFailure.name), children: "Retry" })] }) })), _jsx(Button, { size: "sm", variant: "light", leftSection: _jsx(IconPlus, { size: 16 }), onClick: handleOpenAddEntryType, disabled: requestBusy, children: "Add entry type\u2026" })] }));
508
+ return (_jsxs(_Fragment, { children: [_jsx(Modal, { opened: isOpen, onClose: dismissBlocked ? () => { } : onClose, closeOnClickOutside: !detailsDirty && !createStaged, title: isEditMode
509
+ ? `Edit collection: ${editingCollection?.label || editingCollection?.name}`
510
+ : 'Create collection', size: "lg", children: _jsxs(Stack, { gap: "md", children: [showingRestored && restored && (_jsx(Alert, { icon: _jsx(IconRestore, { size: 16 }), color: "blue", title: "Restored what you entered", children: _jsxs(Stack, { gap: "xs", align: "flex-start", children: [_jsx(Text, { size: "sm", children: isEditMode
511
+ ? "The server didn't answer your last save, and this collection doesn't show it, so your changes are filled in again."
512
+ : "The server didn't answer your last create, and the collection isn't in the content tree, so what you entered is filled in again." }), _jsx(Button, { size: "xs", variant: "light", onClick: discardRestored, children: "Discard changes" })] }) })), displayError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: errorTitle, withCloseButton: true, onClose: () => propErrorVisible ? setErrorDismissed(true) : setValidationError(null), children: displayError })), isEditMode && (_jsxs("div", { children: [_jsx(Title, { order: 3, size: "h5", children: "Collection details" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Saved when you select \"Save collection details\"." })] })), _jsx(TextInput, { ref: nameInputRef, label: "Name", description: "Machine-readable identifier (e.g., posts, pages, articles)", placeholder: "e.g. posts", value: formData.name, onChange: (e) => updateField('name', e.target.value), required: true }), _jsx(TextInput, { label: "Label", description: "Human-readable display name", placeholder: "e.g. Blog posts", value: formData.label, onChange: (e) => updateField('label', e.target.value) }), isEditMode && (_jsx(TextInput, { label: "Slug", description: "Directory name (filesystem path). Changing this renames the directory.", placeholder: "e.g. posts", value: slug, onChange: (e) => {
207
513
  setSlug(e.target.value);
208
514
  setValidationError(null);
209
- } })), parentPath && !isEditMode && (_jsxs(Text, { size: "sm", c: "dimmed", children: ["This collection will be created inside: ", _jsx("strong", { children: parentPath })] })), _jsx(Divider, { label: "Entry Types", labelPosition: "left", mt: "md" }), displayEntryTypes.length === 0 ? (_jsx(Paper, { p: "md", withBorder: true, children: _jsx(Text, { c: "dimmed", ta: "center", size: "sm", children: "No entry types defined. Add at least one entry type to define what content can be created in this collection." }) })) : (_jsx(Stack, { gap: "xs", children: displayEntryTypes.map((entryType, index) => (_jsx(Paper, { p: "sm", withBorder: true, children: _jsxs(Group, { justify: "space-between", wrap: "nowrap", children: [_jsx(Group, { gap: "sm", children: _jsxs("div", { children: [_jsxs(Group, { gap: "xs", children: [_jsx(Text, { fw: 500, size: "sm", children: entryType.name }), entryType.default && (_jsx(Badge, { size: "xs", color: "blue", leftSection: _jsx(IconStar, { size: 10 }), children: "Default" })), entryType.maxItems === 1 && (_jsx(Badge, { size: "xs", color: "gray", children: "Singleton" }))] }), _jsxs(Text, { size: "xs", c: "dimmed", children: [entryType.format.toUpperCase(), " \u00B7 ", entryType.schema, entryType.label && ` · "${entryType.label}"`] })] }) }), _jsxs(Menu, { position: "bottom-end", withinPortal: true, children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", size: "sm", children: _jsx(IconDotsVertical, { size: 16 }) }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconEdit, { size: 14 }), onClick: () => handleOpenEditEntryType(entryType, index), children: "Edit" }), _jsx(Menu.Divider, {}), _jsx(Menu.Item, { leftSection: _jsx(IconTrash, { size: 14 }), color: "red", onClick: () => handleRemoveEntryType(entryType, index), disabled: displayEntryTypes.length === 1, children: "Remove" })] })] })] }) }, entryType.name))) })), _jsx(Button, { variant: "light", leftSection: _jsx(IconPlus, { size: 16 }), onClick: handleOpenAddEntryType, children: "Add Entry Type" }), _jsxs(Group, { justify: "flex-end", gap: "sm", mt: "md", children: [_jsx(Button, { variant: "subtle", onClick: onClose, disabled: isSaving, children: "Cancel" }), _jsx(Button, { onClick: handleSave, loading: isSaving, children: isEditMode ? 'Save Changes' : 'Create Collection' })] })] }) }), _jsx(EntryTypeEditor, { isOpen: entryTypeEditorOpen, editingEntryType: editingEntryType, availableSchemas: availableSchemas, existingEntryTypeNames: displayEntryTypes.map((et) => et.name), onSave: handleEntryTypeSave, onClose: () => {
210
- setEntryTypeEditorOpen(false);
211
- setEditingEntryType(null);
212
- setEditingEntryTypeIndex(null);
213
- setEntryTypeError(null);
214
- }, isSaving: entryTypeSaving, error: entryTypeError }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryTypeModalOpen, title: "Remove Entry Type", message: deletingEntryType &&
215
- 'usageCount' in deletingEntryType.entryType &&
216
- deletingEntryType.entryType.usageCount
217
- ? `This entry type is used by ${deletingEntryType.entryType.usageCount} ${deletingEntryType.entryType.usageCount === 1 ? 'entry' : 'entries'}. Removing it will prevent editing those entries. Are you sure you want to remove this entry type?`
218
- : 'Are you sure you want to remove this entry type? This cannot be undone.', confirmLabel: "Remove Entry Type", onConfirm: confirmRemoveEntryType, onClose: () => {
219
- setDeleteEntryTypeModalOpen(false);
220
- setDeletingEntryType(null);
221
- } })] }));
515
+ } })), parentPath && !isEditMode && (_jsxs(Text, { size: "sm", c: "dimmed", children: ["This collection will be created inside: ", _jsx("strong", { children: parentPath })] })), isEditMode ? (_jsx(Paper, { withBorder: true, p: "md", children: entryTypesSection })) : (entryTypesSection), _jsxs(Group, { justify: "flex-end", gap: "sm", mt: "md", "data-testid": "collection-editor-footer", children: [_jsx(Button, { size: "sm", variant: "subtle", onClick: () => {
516
+ // "Discard changes" over refilled input discards the kept input too.
517
+ if (detailsDirty && showingRestored && restored)
518
+ onDiscardRestored?.();
519
+ onClose();
520
+ }, disabled: isSaving || entryTypeBusy, children: dismissLabel }), _jsx(Button, { size: "sm", onClick: handleSave, loading: isSaving && !entryTypeBusy, disabled: entryTypeBusy, children: isEditMode ? 'Save collection details' : 'Create collection' })] })] }) }), _jsx(EntryTypeEditor, { isOpen: entryTypeEditorOpen, editingEntryType: editingEntryType, availableSchemas: availableSchemas, existingEntryTypeNames: displayEntryTypes.map((et) => et.name), staged: !isEditMode, onSave: handleEntryTypeSave, onClose: dismissEntryTypeEditor, isSaving: entryTypeSaving, error: entryTypeError, warning: entryTypeWarning }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryTypeModalOpen, title: "Remove entry type", message: removeMessage, confirmLabel: "Remove entry type", onConfirm: confirmRemoveEntryType, onClose: removing ? () => { } : cancelRemoveConfirm, loading: removing })] }));
222
521
  }
@@ -24,9 +24,13 @@ export interface EntryTypeEditorProps {
24
24
  availableSchemas: string[];
25
25
  /** Existing entry type names in the collection (for duplicate validation) */
26
26
  existingEntryTypeNames?: string[];
27
+ /** The caller holds an edit until its own save, so confirming it updates rather than saves. */
28
+ staged?: boolean;
27
29
  onSave: (data: CreateEntryTypeInput | UpdateEntryTypeInput, isNew: boolean) => void;
28
30
  onClose: () => void;
29
31
  isSaving?: boolean;
30
32
  error?: string | null;
33
+ /** A note above the kept form that the last save's outcome is unknown, and how to retry. */
34
+ warning?: string | null;
31
35
  }
32
- export declare function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, existingEntryTypeNames, onSave, onClose, isSaving, error, }: EntryTypeEditorProps): import("react/jsx-runtime").JSX.Element;
36
+ export declare function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, existingEntryTypeNames, staged, onSave, onClose, isSaving, error, warning, }: EntryTypeEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -9,14 +9,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  */
10
10
  import { useState, useCallback, useEffect } from 'react';
11
11
  import { Modal, Stack, TextInput, Select, Switch, NumberInput, Group, Button, Text, Alert, Tooltip, } from '@mantine/core';
12
- import { IconAlertCircle, IconLock } from '@tabler/icons-react';
12
+ import { IconAlertCircle, IconAlertTriangle, IconLock } from '@tabler/icons-react';
13
13
  const FORMAT_OPTIONS = [
14
14
  { value: 'json', label: 'JSON' },
15
15
  { value: 'yaml', label: 'YAML' },
16
16
  { value: 'md', label: 'Markdown' },
17
17
  { value: 'mdx', label: 'MDX' },
18
18
  ];
19
- export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, existingEntryTypeNames = [], onSave, onClose, isSaving = false, error = null, }) {
19
+ export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, existingEntryTypeNames = [], staged = false, onSave, onClose, isSaving = false, error = null, warning = null, }) {
20
20
  const isEditMode = editingEntryType !== null;
21
21
  const usageCount = editingEntryType?.usageCount ?? 0;
22
22
  const isLocked = isEditMode && usageCount > 0;
@@ -29,6 +29,9 @@ export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, ex
29
29
  maxItems: undefined,
30
30
  });
31
31
  const [validationError, setValidationError] = useState(null);
32
+ // A refresh hands over a new `availableSchemas` array after every schema change; keying the reset
33
+ // on its first value rather than its identity keeps what the user is typing.
34
+ const defaultSchema = availableSchemas[0] || '';
32
35
  /* eslint-disable react-hooks/set-state-in-effect -- intentional: sync form state from props on open */
33
36
  useEffect(() => {
34
37
  if (isOpen) {
@@ -47,14 +50,14 @@ export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, ex
47
50
  name: '',
48
51
  label: '',
49
52
  format: 'json',
50
- schema: availableSchemas[0] || '',
53
+ schema: defaultSchema,
51
54
  default: false,
52
55
  maxItems: undefined,
53
56
  });
54
57
  }
55
58
  setValidationError(null);
56
59
  }
57
- }, [isOpen, editingEntryType, availableSchemas]);
60
+ }, [isOpen, editingEntryType, defaultSchema]);
58
61
  /* eslint-enable react-hooks/set-state-in-effect */
59
62
  const updateField = useCallback((field, value) => {
60
63
  setFormData((prev) => ({ ...prev, [field]: value }));
@@ -128,7 +131,9 @@ export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, ex
128
131
  label: key,
129
132
  }));
130
133
  const displayError = error || validationError;
131
- return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: isEditMode ? `Edit Entry Type: ${editingEntryType?.name}` : 'Add Entry Type', size: "md", children: _jsxs(Stack, { gap: "md", children: [displayError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Error", withCloseButton: true, onClose: () => setValidationError(null), children: displayError })), _jsx(TextInput, { label: "Name", description: "Machine-readable identifier (e.g., post, page, article)", placeholder: "post", value: formData.name, onChange: (e) => updateField('name', e.target.value), disabled: isEditMode, required: !isEditMode }), _jsx(TextInput, { label: "Label", description: "Human-readable display name", placeholder: "Blog Post", value: formData.label, onChange: (e) => updateField('label', e.target.value) }), _jsx(Tooltip, { label: isLocked
134
+ return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: isEditMode
135
+ ? `Edit entry type: ${editingEntryType?.label || editingEntryType?.name}`
136
+ : 'Add entry type', size: "md", children: _jsxs(Stack, { gap: "md", children: [warning && !displayError && (_jsx(Alert, { icon: _jsx(IconAlertTriangle, { size: 16 }), color: "yellow", title: "No answer from the server", children: warning })), displayError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Error", withCloseButton: true, onClose: () => setValidationError(null), children: displayError })), _jsx(TextInput, { label: "Name", description: "Machine-readable identifier (e.g., post, page, article)", placeholder: "e.g. post", value: formData.name, onChange: (e) => updateField('name', e.target.value), disabled: isEditMode, required: !isEditMode }), _jsx(TextInput, { label: "Label", description: "Human-readable display name", placeholder: "e.g. Blog post", value: formData.label, onChange: (e) => updateField('label', e.target.value) }), _jsx(Tooltip, { label: isLocked
132
137
  ? `Cannot change format: ${usageCount} ${usageCount === 1 ? 'entry' : 'entries'} use this type`
133
138
  : '', disabled: !isLocked, withinPortal: true, children: _jsx(Select, { label: "Format", description: isLocked
134
139
  ? `Locked (${usageCount} ${usageCount === 1 ? 'entry' : 'entries'} exist)`
@@ -136,5 +141,5 @@ export function EntryTypeEditor({ isOpen, editingEntryType, availableSchemas, ex
136
141
  ? `Cannot change schema: ${usageCount} ${usageCount === 1 ? 'entry' : 'entries'} use this type`
137
142
  : '', disabled: !isLocked, withinPortal: true, children: _jsx(Select, { label: "Schema", description: isLocked
138
143
  ? `Locked (${usageCount} ${usageCount === 1 ? 'entry' : 'entries'} exist)`
139
- : 'Field definitions for this entry type', data: schemaOptions, value: formData.schema, onChange: (value) => value && updateField('schema', value), searchable: true, required: true, placeholder: "Select a schema", disabled: isLocked, rightSection: isLocked ? _jsx(IconLock, { size: 14 }) : undefined }) }), _jsx(Switch, { label: "Default entry type", description: "Use this type when adding new items to the collection", checked: formData.default, onChange: (e) => updateField('default', e.currentTarget.checked) }), _jsx(NumberInput, { label: "Max Items", description: "Limit number of items (leave empty for unlimited, use 1 for singleton-like behavior)", placeholder: "Unlimited", value: formData.maxItems ?? '', onChange: (value) => updateField('maxItems', typeof value === 'number' ? value : undefined), min: 1, allowNegative: false, allowDecimal: false }), formData.maxItems === 1 && (_jsx(Text, { size: "xs", c: "dimmed", children: "With maxItems=1, this entry type behaves like a singleton (e.g., homepage, settings)" })), _jsxs(Group, { justify: "flex-end", gap: "sm", mt: "md", children: [_jsx(Button, { variant: "subtle", onClick: onClose, disabled: isSaving, children: "Cancel" }), _jsx(Button, { onClick: handleSave, loading: isSaving, children: isEditMode ? 'Save Changes' : 'Add Entry Type' })] })] }) }));
144
+ : 'Field definitions for this entry type', data: schemaOptions, value: formData.schema, onChange: (value) => value && updateField('schema', value), searchable: true, required: true, placeholder: "Select a schema", disabled: isLocked, rightSection: isLocked ? _jsx(IconLock, { size: 14 }) : undefined }) }), _jsx(Switch, { label: "Default entry type", description: "Use this type when adding new items to the collection", checked: formData.default, onChange: (e) => updateField('default', e.currentTarget.checked) }), _jsx(NumberInput, { label: "Max items", description: "Limit number of items (leave empty for unlimited, use 1 for singleton-like behavior)", placeholder: "Unlimited", value: formData.maxItems ?? '', onChange: (value) => updateField('maxItems', typeof value === 'number' ? value : undefined), min: 1, allowNegative: false, allowDecimal: false }), formData.maxItems === 1 && (_jsx(Text, { size: "xs", c: "dimmed", children: "With maxItems=1, this entry type behaves like a singleton (e.g., homepage, settings)" })), _jsxs(Group, { justify: "flex-end", gap: "sm", mt: "md", children: [_jsx(Button, { size: "sm", variant: "subtle", onClick: onClose, disabled: isSaving, children: "Cancel" }), _jsx(Button, { size: "sm", onClick: handleSave, loading: isSaving, children: isEditMode ? (staged ? 'Update entry type' : 'Save entry type') : 'Add entry type' })] })] }) }));
140
145
  }
@@ -0,0 +1,22 @@
1
+ import type { LogicalPath } from '../../paths/types.js';
2
+ import type { CreateCollectionInput, UpdateCollectionInput } from '../../schema/schema-store-types.js';
3
+ import type { EditorCollection } from '../Editor.js';
4
+ /**
5
+ * What was sent by a collection create or details save that got no answer, so the dialog can fill
6
+ * it in again. `Editor.tsx` keeps it until the dialog's opener reads it landed, a save of the same
7
+ * thing succeeds, or the user discards it.
8
+ */
9
+ export type CollectionDraft = {
10
+ branch: string;
11
+ } & ({
12
+ kind: 'create';
13
+ input: CreateCollectionInput;
14
+ } | {
15
+ kind: 'details';
16
+ collectionPath: LogicalPath;
17
+ updates: UpdateCollectionInput;
18
+ });
19
+ /** The directory name in a logical path, without its embedded id ("posts.abc123" -> "posts"). */
20
+ export declare function slugOf(logicalPath: LogicalPath): string;
21
+ /** Whether `collections`, read from the draft's branch, show the draft's write. */
22
+ export declare function collectionDraftLanded(collections: readonly EditorCollection[], draft: CollectionDraft): boolean;