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
@@ -1,22 +1,49 @@
1
1
  'use client';
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useState } from 'react';
4
- import { Alert, Button, Group, Loader, Progress, ScrollArea, SimpleGrid, Stack, Text, TextInput, } from '@mantine/core';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { ActionIcon, Alert, Button, Center, Group, Progress, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Text, TextInput, Tooltip, VisuallyHidden, } from '@mantine/core';
5
5
  import { Dropzone } from '@mantine/dropzone';
6
- import { modals } from '@mantine/modals';
7
- import { IconAlertCircle, IconUpload } from '@tabler/icons-react';
6
+ import { openConfirm } from '../utils/confirm-modal.js';
7
+ import { IconAlertCircle, IconGridDots, IconLayoutGrid, IconList, IconSearch, IconUpload, IconX, } from '@tabler/icons-react';
8
8
  import { getErrorMessage } from '../../utils/error.js';
9
+ import { isNonApiResponse } from '../../api/client.js';
9
10
  // Import directly from helpers (not the authorization barrel) to keep server-only
10
11
  // authorization code out of this client bundle - matches BranchManager's convention.
11
12
  import { isAdmin } from '../../authorization/helpers.js';
12
13
  import { useApiClient, useAssetContext } from '../context/index.js';
13
14
  import { useUserContext } from '../hooks/index.js';
14
15
  import { AssetCard } from './AssetCard.js';
16
+ import { AssetListRow } from './AssetListRow.js';
17
+ import { MediaLibrarySkeleton } from './MediaLibrarySkeleton.js';
18
+ import { useAssetLibrary } from './useAssetLibrary.js';
19
+ import { useStableAssetOrder } from './useStableAssetOrder.js';
15
20
  import { useAssetUpload } from './useAssetUpload.js';
21
+ import { useMediaLibraryPrefs } from './media-library-prefs.js';
22
+ import { GRID_COLUMNS, fileCountLabel, matchesKind, matchesText, } from './media-library-view.js';
16
23
  import { ACCEPTED_ASSET_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES, MAX_UPLOAD_BYTES, } from './upload-constants.js';
17
- const PAGE_SIZE = 40;
24
+ /** How many assets render at first, and how many each reveal adds. All of them load regardless. */
25
+ const RENDER_WINDOW = 40;
26
+ const SORT_OPTIONS = [
27
+ { value: 'newest', label: 'Newest first' },
28
+ { value: 'name', label: 'Name (A–Z)' },
29
+ ];
30
+ const KIND_OPTIONS = [
31
+ { value: 'all', label: 'All' },
32
+ { value: 'images', label: 'Images' },
33
+ { value: 'pdfs', label: 'PDFs' },
34
+ ];
35
+ /** An icon-only radio segment: the visible icon, a tooltip, and hidden text for the radio's name. */
36
+ const DensityLabel = ({ icon, text }) => (_jsx(Tooltip, { label: text, children: _jsxs(Center, { children: [icon, _jsx(VisuallyHidden, { children: text })] }) }));
37
+ const DENSITY_OPTIONS = [
38
+ { value: 'grid', label: _jsx(DensityLabel, { icon: _jsx(IconLayoutGrid, { size: 16 }), text: "Grid" }) },
39
+ {
40
+ value: 'compact',
41
+ label: _jsx(DensityLabel, { icon: _jsx(IconGridDots, { size: 16 }), text: "Compact grid" }),
42
+ },
43
+ { value: 'list', label: _jsx(DensityLabel, { icon: _jsx(IconList, { size: 16 }), text: "List" }) },
44
+ ];
18
45
  /**
19
- * Shared grid/upload/filter/pagination/delete core for both MediaLibrary
46
+ * Shared grid/list/upload/search/delete core for both MediaLibrary
20
47
  * presentations (Drawer "manage" mode, Modal "picker" mode) - see
21
48
  * MediaLibrary.tsx for the two wrappers.
22
49
  */
@@ -40,102 +67,190 @@ export const MediaLibraryBody = ({ opened, mode, onSelect }) => {
40
67
  // (see MediaLibraryProps.mode's doc comment) - both image-only contexts.
41
68
  // `manage` mode (the Editor's Media Library drawer) keeps accepting PDFs.
42
69
  const imageOnly = mode === 'picker';
43
- const [assets, setAssets] = useState([]);
44
- const [nextCursor, setNextCursor] = useState(undefined);
45
- const [loading, setLoading] = useState(false);
46
- const [loadingMore, setLoadingMore] = useState(false);
47
- const [listError, setListError] = useState(null);
70
+ const library = useAssetLibrary(opened);
71
+ const { sort, density, setSort, setDensity } = useMediaLibraryPrefs();
72
+ const [kind, setKind] = useState('all');
48
73
  const [filterText, setFilterText] = useState('');
49
- const loadFirstPage = useCallback(async () => {
50
- setLoading(true);
51
- setListError(null);
52
- try {
53
- const result = await client.assets.list({ limit: String(PAGE_SIZE) });
54
- if (!result.ok || !result.data) {
55
- setListError(result.error || 'Failed to load assets');
56
- return;
57
- }
58
- setAssets(result.data.assets);
59
- setNextCursor(result.data.nextCursor);
60
- }
61
- catch (err) {
62
- setListError(getErrorMessage(err));
63
- }
64
- finally {
65
- setLoading(false);
66
- }
67
- }, [client]);
74
+ const [actionError, setActionError] = useState(null);
75
+ // The result of the last remove, for the live region; the removed card itself is gone.
76
+ // `id` re-keys the region's text, so a second remove with the same words is announced too.
77
+ const [actionNotice, setActionNotice] = useState(null);
78
+ const searchRef = useRef(null);
79
+ // A removed card takes its focused button with it, so focus moves to the search field once the
80
+ // removal has rendered (Mantine returns focus to the trash button when the confirm closes).
81
+ const focusAfterRemove = useRef(false);
68
82
  useEffect(() => {
69
- if (!opened)
83
+ if (!focusAfterRemove.current)
70
84
  return;
71
- void loadFirstPage();
72
- // Refetch only when the drawer/modal (re)opens, not on every render where
73
- // loadFirstPage's identity happens to change.
74
- // eslint-disable-next-line react-hooks/exhaustive-deps
75
- }, [opened]);
76
- const handleLoadMore = async () => {
77
- if (!nextCursor)
78
- return;
79
- setLoadingMore(true);
80
- setListError(null);
81
- try {
82
- const result = await client.assets.list({ cursor: nextCursor, limit: String(PAGE_SIZE) });
83
- if (!result.ok || !result.data) {
84
- setListError(result.error || 'Failed to load more assets');
85
- return;
86
- }
87
- const page = result.data;
88
- setAssets((prev) => [...prev, ...page.assets]);
89
- setNextCursor(page.nextCursor);
90
- }
91
- catch (err) {
92
- setListError(getErrorMessage(err));
93
- }
94
- finally {
95
- setLoadingMore(false);
96
- }
85
+ focusAfterRemove.current = false;
86
+ searchRef.current?.focus();
87
+ });
88
+ // Clearing hides the control that did it, so focus moves to the search field.
89
+ const clearSearch = () => {
90
+ setFilterText('');
91
+ searchRef.current?.focus();
97
92
  };
93
+ // This opening's uploads, newest first; they lead the list whatever the sort.
94
+ const [pinned, setPinned] = useState([]);
95
+ // The render window resets whenever the list it windows changes shape. Adjusting it during
96
+ // render, not in an effect, avoids a frame with the old count.
97
+ const windowKey = `${filterText}|${sort}|${kind}|${density}`;
98
+ const [windowState, setWindowState] = useState({ key: windowKey, count: RENDER_WINDOW });
99
+ if (windowState.key !== windowKey)
100
+ setWindowState({ key: windowKey, count: RENDER_WINDOW });
101
+ const visibleCount = windowState.key === windowKey ? windowState.count : RENDER_WINDOW;
102
+ const showMore = useCallback(() => setWindowState({ key: windowKey, count: visibleCount + RENDER_WINDOW }), [windowKey, visibleCount]);
103
+ const needle = filterText.trim().toLowerCase();
104
+ const effectiveKind = imageOnly ? 'images' : kind;
105
+ // Picker mode is image-only (see `imageOnly` above) - pdf assets are
106
+ // excluded from the pickable list entirely, not just the upload accept
107
+ // list, since an already-uploaded pdf could otherwise still be selected.
108
+ const scopedAssets = useMemo(() => (imageOnly ? library.assets.filter((asset) => asset.kind !== 'pdf') : library.assets), [imageOnly, library.assets]);
109
+ const kindAssets = useMemo(() => scopedAssets.filter((asset) => matchesKind(asset, effectiveKind)), [scopedAssets, effectiveKind]);
110
+ const orderedAssets = useStableAssetOrder(scopedAssets, sort, library.complete, pinned);
111
+ const matchedAssets = useMemo(() => orderedAssets.filter((asset) => matchesKind(asset, effectiveKind) && matchesText(asset, needle)), [orderedAssets, effectiveKind, needle]);
112
+ const visibleAssets = matchedAssets.slice(0, visibleCount);
113
+ const hasMore = matchedAssets.length > visibleAssets.length;
114
+ const [sentinel, setSentinel] = useState(null);
115
+ // The scroll viewport is the observer's root: a margin on the default root would only widen
116
+ // the document viewport, not the ScrollArea that clips the sentinel.
117
+ const [viewport, setViewport] = useState(null);
118
+ useEffect(() => {
119
+ if (!sentinel || !hasMore || typeof IntersectionObserver === 'undefined')
120
+ return;
121
+ const observer = new IntersectionObserver((entries) => {
122
+ if (entries.some((entry) => entry.isIntersecting))
123
+ showMore();
124
+ }, { root: viewport, rootMargin: '200px' });
125
+ observer.observe(sentinel);
126
+ return () => observer.disconnect();
127
+ }, [sentinel, viewport, hasMore, showMore]);
98
128
  const handleDrop = async (files) => {
99
129
  const file = files[0];
100
130
  if (!file)
101
131
  return;
132
+ setActionError(null);
102
133
  const asset = await upload.upload(file);
103
- if (asset) {
104
- setAssets((prev) => [asset, ...prev]);
105
- }
134
+ if (!asset)
135
+ return;
136
+ library.prepend(asset);
137
+ setPinned((prev) => [asset.hash32, ...prev.filter((hash) => hash !== asset.hash32)]);
138
+ // The upload must be visible: a search or kind filter that would hide it is cleared.
139
+ setFilterText((prev) => (matchesText(asset, prev.trim().toLowerCase()) ? prev : ''));
140
+ setKind((prev) => (matchesKind(asset, prev) ? prev : 'all'));
106
141
  };
107
142
  const handleReject = (rejections) => {
108
- setListError(rejections[0]?.errors[0]?.message ?? 'File rejected');
143
+ setActionError(rejections[0]?.errors[0]?.message ?? 'The file was rejected.');
109
144
  };
110
145
  const handleDelete = async (asset) => {
111
- const result = await client.assets.delete({ key: asset.hash32 });
112
- if (!result.ok) {
113
- setListError(result.error || 'Failed to delete asset');
114
- return;
146
+ setActionError(null);
147
+ const failure = (detail) => `Couldn't remove ${asset.filename} from the library. ${detail}`;
148
+ // The raw server message is a diagnostic, shown to admins only.
149
+ const generic = (serverMessage) => failure((isAdmin(userContext?.groups) && serverMessage) || 'Try again.');
150
+ try {
151
+ const result = await client.assets.delete({ key: asset.hash32 });
152
+ if (!result.ok) {
153
+ // An editor sees this button only for their own upload, and the endpoint answers a
154
+ // missing file with the same 403 as someone else's, so their 403 means it is gone.
155
+ const alreadyGone = result.status === 403 && !isNonApiResponse(result) && !isAdmin(userContext?.groups);
156
+ setActionError(alreadyGone ? failure('It may have been removed already.') : generic(result.error));
157
+ return;
158
+ }
159
+ library.remove(asset.hash32);
160
+ setActionNotice((prev) => ({
161
+ id: (prev?.id ?? 0) + 1,
162
+ text: `Removed ${asset.filename} from library`,
163
+ }));
164
+ focusAfterRemove.current = true;
165
+ }
166
+ catch (err) {
167
+ setActionError(generic(getErrorMessage(err)));
115
168
  }
116
- setAssets((prev) => prev.filter((a) => a.hash32 !== asset.hash32));
117
169
  };
118
170
  const handleDeleteClick = (asset) => {
119
- modals.openConfirmModal({
171
+ openConfirm({
120
172
  title: 'Remove from library',
121
- children: (_jsxs(Text, { size: "sm", children: ["Remove \u201C", asset.filename, "\u201D from the media library? Existing content that already references this file keeps working - removing it here only hides it from the library; the file itself is not deleted."] })),
122
- labels: { confirm: 'Remove', cancel: 'Cancel' },
173
+ children: (_jsxs(Text, { size: "sm", children: ["Remove \u201C", asset.filename, "\u201D from the media library? Content that already uses this file keeps working, and the file itself is not deleted."] })),
174
+ labels: { confirm: 'Remove from library', cancel: 'Cancel' },
123
175
  confirmProps: { color: 'red' },
124
176
  onConfirm: () => {
125
177
  void handleDelete(asset);
126
178
  },
127
179
  });
128
180
  };
129
- // Picker mode is image-only (see `imageOnly` above) - pdf assets are
130
- // filtered out of the pickable grid entirely, not just the upload accept
131
- // list, since an already-uploaded pdf could otherwise still be selected.
132
- const kindFiltered = imageOnly ? assets.filter((asset) => asset.kind !== 'pdf') : assets;
133
- const normalizedFilter = filterText.trim().toLowerCase();
134
- const filteredAssets = normalizedFilter
135
- ? kindFiltered.filter((asset) => asset.filename.toLowerCase().includes(normalizedFilter))
136
- : kindFiltered;
137
- return (_jsxs(Stack, { gap: "sm", h: "100%", style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Dropzone, { onDrop: (files) => void handleDrop(files), onReject: handleReject, maxSize: MAX_UPLOAD_BYTES, accept: imageOnly ? ACCEPTED_IMAGE_MIME_TYPES : ACCEPTED_ASSET_MIME_TYPES, loading: upload.uploading, multiple: false, "data-testid": "media-library-dropzone", children: _jsxs(Group, { justify: "center", gap: "xs", py: "sm", style: { pointerEvents: 'none' }, children: [_jsx(IconUpload, { size: 20 }), _jsx(Text, { size: "sm", children: imageOnly
181
+ // `text` is shown; `announce` is the live-region wording, which changes only between phases so
182
+ // a screen reader is not told every page's count.
183
+ const loadStatus = (() => {
184
+ if (library.error || !library.firstPageLoaded)
185
+ return null;
186
+ if (!library.complete) {
187
+ return needle
188
+ ? {
189
+ text: `Searching ${fileCountLabel(kindAssets.length)} loaded so far…`,
190
+ announce: 'Searching the files loaded so far…',
191
+ }
192
+ : {
193
+ text: `Loaded ${fileCountLabel(kindAssets.length)}, loading more…`,
194
+ announce: 'Loading more files…',
195
+ };
196
+ }
197
+ // With no match, the empty state below says so.
198
+ if (needle && matchedAssets.length > 0) {
199
+ const total = kindAssets.length;
200
+ const text = `${matchedAssets.length} of ${total} ${total === 1 ? 'file matches' : 'files match'}`;
201
+ return { text, announce: text };
202
+ }
203
+ return null;
204
+ })();
205
+ const kindNoun = effectiveKind === 'pdfs' ? 'PDFs' : effectiveKind === 'images' ? 'images' : 'files';
206
+ const libraryEmpty = library.complete && scopedAssets.length === 0;
207
+ const emptyState = (() => {
208
+ if (matchedAssets.length > 0)
209
+ return null;
210
+ // A later page failed: say what the loaded part holds; the alert explains the rest.
211
+ if (library.error && !library.complete) {
212
+ if (!library.firstPageLoaded || kindAssets.length === 0)
213
+ return null;
214
+ return needle
215
+ ? `No ${kindNoun} match “${filterText.trim()}” among the ${fileCountLabel(kindAssets.length)} loaded.`
216
+ : null;
217
+ }
218
+ if (!library.complete)
219
+ return null;
220
+ if (libraryEmpty) {
221
+ return imageOnly
222
+ ? 'No images yet. Drop an image above, or click there to choose one.'
223
+ : 'No files yet. Drop a file above, or click there to choose one.';
224
+ }
225
+ if (needle)
226
+ return `No ${kindNoun} match “${filterText.trim()}”.`;
227
+ return `No ${kindNoun} in the library.`;
228
+ })();
229
+ const renderList = () => {
230
+ if (density === 'list') {
231
+ return (_jsx(Stack, { gap: 4, children: visibleAssets.map((asset) => (_jsx(AssetListRow, { asset: asset, baseUrl: baseUrl, onSelect: mode === 'picker' ? onSelect : undefined, onDelete: canDeleteAsset(asset) ? () => handleDeleteClick(asset) : undefined }, asset.hash32))) }));
232
+ }
233
+ return (_jsx(SimpleGrid, { cols: GRID_COLUMNS[density], spacing: "sm", children: visibleAssets.map((asset) => (_jsx(AssetCard, { asset: asset, baseUrl: baseUrl, density: density, onSelect: mode === 'picker' ? onSelect : undefined, onDelete: canDeleteAsset(asset) ? () => handleDeleteClick(asset) : undefined }, asset.hash32))) }));
234
+ };
235
+ const listContent = (_jsxs(_Fragment, { children: [!library.firstPageLoaded && !library.error && visibleAssets.length === 0 && (_jsx(MediaLibrarySkeleton, { density: density })), visibleAssets.length > 0 && renderList(), emptyState && (_jsxs(Stack, { align: "center", gap: "xs", py: "xl", "data-testid": "media-library-empty", children: [_jsx(Text, { size: "sm", c: "dimmed", ta: "center", children: emptyState }), _jsxs(Group, { gap: "xs", justify: "center", children: [needle && (_jsx(Button, { variant: "light", size: "xs", onClick: clearSearch, children: "Clear search" })), kind !== 'all' && !libraryEmpty && (_jsx(Button, { variant: "light", size: "xs", onClick: () => setKind('all'), children: "Show all files" }))] })] })), hasMore && (_jsxs(_Fragment, { children: [_jsx("div", { ref: setSentinel, "aria-hidden": "true", style: { height: 1 } }), _jsx(Group, { justify: "center", py: "sm", children: _jsx(Button, { variant: "light", onClick: showMore, "data-testid": "media-library-show-more", children: "Show more" }) })] }))] }));
236
+ return (
237
+ // `h="100%"` bounds it in a fixed-height host (the MDX image dialog's library tab); `flex`
238
+ // bounds it in the drawer's flex body.
239
+ _jsxs(Stack, { gap: "sm", h: "100%", style: { flex: 1, minHeight: 0 }, children: [_jsx(Dropzone, { onDrop: (files) => void handleDrop(files), onReject: handleReject, maxSize: MAX_UPLOAD_BYTES, accept: imageOnly ? ACCEPTED_IMAGE_MIME_TYPES : ACCEPTED_ASSET_MIME_TYPES, loading: upload.uploading, multiple: false, "data-testid": "media-library-dropzone", children: _jsxs(Group, { justify: "center", gap: "xs", py: "sm", style: { pointerEvents: 'none' }, children: [_jsx(IconUpload, { size: 20 }), _jsx(Text, { size: "sm", children: imageOnly
138
240
  ? 'Drop an image here, or click to browse'
139
241
  : 'Drop an image or PDF here, or click to browse' })] }) }), upload.uploading &&
140
- (upload.progress !== null ? (_jsx(Progress, { value: upload.progress * 100, size: "sm", "data-testid": "upload-progress" })) : (_jsx(Progress, { value: 100, size: "sm", animated: true, "data-testid": "upload-progress-indeterminate" }))), upload.error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", "data-testid": "upload-error", children: upload.error })), _jsx(TextInput, { placeholder: "Filter by filename...", value: filterText, onChange: (event) => setFilterText(event.currentTarget.value), "data-testid": "media-library-filter" }), listError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", "data-testid": "media-library-error", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: listError }), _jsx(Button, { size: "xs", variant: "light", onClick: () => void loadFirstPage(), children: "Retry" })] }) })), _jsxs(ScrollArea, { style: { flex: 1 }, children: [loading ? (_jsx(Group, { justify: "center", py: "xl", children: _jsx(Loader, { size: "sm", "data-testid": "media-library-loading" }) })) : filteredAssets.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", py: "xl", ta: "center", children: "No assets found." })) : (_jsx(SimpleGrid, { cols: { base: 2, sm: 3, md: 4 }, spacing: "sm", children: filteredAssets.map((asset) => (_jsx(AssetCard, { asset: asset, baseUrl: baseUrl, onSelect: mode === 'picker' ? onSelect : undefined, onDelete: canDeleteAsset(asset) ? () => handleDeleteClick(asset) : undefined }, asset.hash32))) })), !loading && nextCursor && (_jsx(Group, { justify: "center", py: "sm", children: _jsx(Button, { variant: "light", size: "xs", loading: loadingMore, onClick: () => void handleLoadMore(), "data-testid": "media-library-load-more", children: "Load more" }) }))] })] }));
242
+ (upload.progress !== null ? (_jsx(Progress, { value: upload.progress * 100, size: "sm", "data-testid": "upload-progress" })) : (_jsx(Progress, { value: 100, size: "sm", animated: true, "data-testid": "upload-progress-indeterminate" }))), upload.error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", "data-testid": "upload-error", children: upload.error })), _jsx(TextInput, { placeholder: "Search by filename", "aria-label": "Search by filename", leftSection: _jsx(IconSearch, { size: 16 }), rightSection: filterText ? (_jsx(Tooltip, { label: "Clear search", children: _jsx(ActionIcon, { variant: "subtle", color: "gray", size: "sm", "aria-label": "Clear search", onClick: clearSearch, children: _jsx(IconX, { size: 14 }) }) })) : null, ref: searchRef, value: filterText, onChange: (event) => setFilterText(event.currentTarget.value), "data-mantine-stop-propagation": filterText ? 'true' : undefined, onKeyDown: (event) => {
243
+ if (event.key === 'Escape' && filterText)
244
+ setFilterText('');
245
+ }, "data-testid": "media-library-filter" }), _jsxs(Group, { justify: "space-between", wrap: "wrap", gap: "xs", children: [imageOnly ? (_jsx("span", {})) : (_jsx(SegmentedControl, { "aria-label": "File type", data: KIND_OPTIONS, value: kind, onChange: (value) => setKind(KIND_OPTIONS.find((o) => o.value === value)?.value ?? 'all'), "data-testid": "media-library-kind" })), _jsxs(Group, { gap: "xs", wrap: "wrap", children: [_jsx(Select, { "aria-label": "Sort by", data: SORT_OPTIONS, value: sort, onChange: (value) => {
246
+ const next = SORT_OPTIONS.find((option) => option.value === value);
247
+ if (next)
248
+ setSort(next.value);
249
+ }, allowDeselect: false, w: 150, "data-testid": "media-library-sort" }), _jsx(SegmentedControl, { "aria-label": "Layout", data: DENSITY_OPTIONS, value: density, onChange: (value) => {
250
+ const next = DENSITY_OPTIONS.find((option) => option.value === value);
251
+ if (next)
252
+ setDensity(next.value);
253
+ }, "data-testid": "media-library-density" })] })] }), _jsx(Text, { component: "div", size: "xs", c: "dimmed", mih: 18, children: loadStatus && (_jsx("span", { "aria-hidden": "true", "data-testid": "media-library-load-status", children: loadStatus.text })) }), _jsx(VisuallyHidden, { role: "status", "aria-live": "polite", children: loadStatus?.announce ?? '' }), _jsx(VisuallyHidden, { role: "status", "aria-live": "polite", "data-testid": "media-library-action-notice", children: actionNotice && _jsx("span", { children: actionNotice.text }, actionNotice.id) }), library.error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", "data-testid": "media-library-error", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: library.error.firstPage
254
+ ? "Couldn't load the media library."
255
+ : `Couldn't load the rest of the media library. Showing the ${fileCountLabel(scopedAssets.length)} loaded so far.` }), isAdmin(userContext?.groups) && (_jsx(Text, { size: "xs", c: "dimmed", children: library.error.message })), _jsx(Button, { variant: "light", onClick: library.retry, children: "Retry" })] }) })), actionError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", withCloseButton: true, closeButtonLabel: "Dismiss", onClose: () => setActionError(null), "data-testid": "media-library-action-error", children: actionError })), imageOnly ? (_jsx(ScrollArea.Autosize, { mah: "60vh", viewportRef: setViewport, children: listContent })) : (_jsx(ScrollArea, { style: { flex: 1, minHeight: 200 }, viewportRef: setViewport, children: listContent }))] }));
141
256
  };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { type MediaDensity } from './media-library-view.js';
3
+ /** Placeholder shaped like the layout it stands in for, so nothing shifts when assets arrive. */
4
+ export declare const MediaLibrarySkeleton: React.FC<{
5
+ density: MediaDensity;
6
+ }>;
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, SimpleGrid, Skeleton, Stack } from '@mantine/core';
4
+ import { CARD_THUMBNAIL_HEIGHT, GRID_COLUMNS } from './media-library-view.js';
5
+ const SKELETON_COUNT = 8;
6
+ /** Placeholder shaped like the layout it stands in for, so nothing shifts when assets arrive. */
7
+ export const MediaLibrarySkeleton = ({ density }) => {
8
+ const items = Array.from({ length: SKELETON_COUNT }, (_, index) => index);
9
+ if (density === 'list') {
10
+ return (_jsx(Stack, { gap: 4, "data-testid": "media-library-loading", "aria-busy": "true", children: items.map((index) => (_jsx(Skeleton, { height: 54, radius: "md" }, index))) }));
11
+ }
12
+ const thumbnailHeight = CARD_THUMBNAIL_HEIGHT[density];
13
+ return (_jsx(SimpleGrid, { cols: GRID_COLUMNS[density], spacing: "sm", "data-testid": "media-library-loading", "aria-busy": "true", children: items.map((index) => (_jsxs(Card, { withBorder: true, radius: "md", padding: "xs", children: [_jsx(Card.Section, { children: _jsx(Skeleton, { height: thumbnailHeight, radius: 0 }) }), _jsx(Skeleton, { height: 12, width: "70%", mt: 8 }), density === 'grid' && _jsx(Skeleton, { height: 10, width: "40%", mt: 6 })] }, index))) }));
14
+ };
@@ -16,10 +16,9 @@ export interface CropAreaPercent {
16
16
  }
17
17
  /**
18
18
  * Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
19
- * percentage area into a normalized `CropRect`, rounded to 4 decimals and
20
- * clamped to fit within bounds. Rounding can push `x+w`/`y+h` a hair over 1
21
- * (e.g. x=0.6667, w=0.3334 -> 1.0001) - shrink the extent to the remaining
22
- * space rather than reject a rect that was valid before rounding.
19
+ * percentage area into a normalized `CropRect`, clamped to [0,1] and then
20
+ * rounded by `roundCropRect` — the same rounding the transform URL's canonical
21
+ * form applies, so the stored crop is already canonical.
23
22
  *
24
23
  * Returns `null` if the input can't be coerced into a valid rect (e.g. a
25
24
  * zero-area selection).
@@ -6,13 +6,7 @@
6
6
  * (no react-easy-crop import) so it's trivially unit-testable and reusable
7
7
  * without pulling in the cropper UI library.
8
8
  */
9
- import { isValidCropRect } from '../../assets/transform-directives.js';
10
- /** Matches transform-directives.ts's CROP_PRECISION so a crop rect round-trips through the URL directive unchanged. */
11
- const CROP_PRECISION = 4;
12
- function roundTo(value, decimals) {
13
- const factor = 10 ** decimals;
14
- return Math.round(value * factor) / factor;
15
- }
9
+ import { isValidCropRect, roundCropRect } from '../../assets/transform-directives.js';
16
10
  function clamp01(value) {
17
11
  if (!Number.isFinite(value))
18
12
  return 0;
@@ -20,26 +14,21 @@ function clamp01(value) {
20
14
  }
21
15
  /**
22
16
  * Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
23
- * percentage area into a normalized `CropRect`, rounded to 4 decimals and
24
- * clamped to fit within bounds. Rounding can push `x+w`/`y+h` a hair over 1
25
- * (e.g. x=0.6667, w=0.3334 -> 1.0001) - shrink the extent to the remaining
26
- * space rather than reject a rect that was valid before rounding.
17
+ * percentage area into a normalized `CropRect`, clamped to [0,1] and then
18
+ * rounded by `roundCropRect` — the same rounding the transform URL's canonical
19
+ * form applies, so the stored crop is already canonical.
27
20
  *
28
21
  * Returns `null` if the input can't be coerced into a valid rect (e.g. a
29
22
  * zero-area selection).
30
23
  */
31
24
  export function cropAreaPercentToRect(area) {
32
- const x = roundTo(clamp01(area.x / 100), CROP_PRECISION);
33
- const y = roundTo(clamp01(area.y / 100), CROP_PRECISION);
34
- let w = roundTo(clamp01(area.width / 100), CROP_PRECISION);
35
- let h = roundTo(clamp01(area.height / 100), CROP_PRECISION);
36
- if (x + w > 1)
37
- w = roundTo(1 - x, CROP_PRECISION);
38
- if (y + h > 1)
39
- h = roundTo(1 - y, CROP_PRECISION);
40
- if (!isValidCropRect(x, y, w, h))
41
- return null;
42
- return { x, y, w, h };
25
+ const rect = roundCropRect({
26
+ x: clamp01(area.x / 100),
27
+ y: clamp01(area.y / 100),
28
+ w: clamp01(area.width / 100),
29
+ h: clamp01(area.height / 100),
30
+ });
31
+ return isValidCropRect(rect.x, rect.y, rect.w, rect.h) ? rect : null;
43
32
  }
44
33
  /**
45
34
  * Inverse of `cropAreaPercentToRect` - seeds react-easy-crop's
@@ -0,0 +1,8 @@
1
+ import { type AssetUrlOptions } from '../../assets/asset-url.js';
2
+ /**
3
+ * The URL the editor displays a content image at. An asset-store src goes behind `baseUrl` (the
4
+ * authenticated raw route, see `AssetContext`) with `opts` applied. Any other src keeps its own
5
+ * path, since the raw route serves only the asset store; `assetUrl` still neutralizes a spelling a
6
+ * browser would read as off-origin.
7
+ */
8
+ export declare function editorImageSrc(src: string, baseUrl: string, opts?: Omit<AssetUrlOptions, 'baseUrl'>): string;
@@ -0,0 +1,10 @@
1
+ import { assetUrl, isAssetStoreSrc } from '../../assets/asset-url.js';
2
+ /**
3
+ * The URL the editor displays a content image at. An asset-store src goes behind `baseUrl` (the
4
+ * authenticated raw route, see `AssetContext`) with `opts` applied. Any other src keeps its own
5
+ * path, since the raw route serves only the asset store; `assetUrl` still neutralizes a spelling a
6
+ * browser would read as off-origin.
7
+ */
8
+ export function editorImageSrc(src, baseUrl, opts = {}) {
9
+ return isAssetStoreSrc(src) ? assetUrl({ src }, { ...opts, baseUrl }) : assetUrl({ src });
10
+ }
@@ -0,0 +1,2 @@
1
+ /** "512 B", "2.3 MB", "14 KB": 1024 base, one decimal below 10, whole numbers above. */
2
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,23 @@
1
+ const UNITS = ['B', 'KB', 'MB', 'GB'];
2
+ /** "512 B", "2.3 MB", "14 KB": 1024 base, one decimal below 10, whole numbers above. */
3
+ export function formatBytes(bytes) {
4
+ if (!Number.isFinite(bytes) || bytes <= 0)
5
+ return '0 B';
6
+ let value = bytes;
7
+ let unit = 0;
8
+ while (value >= 1024 && unit < UNITS.length - 1) {
9
+ value /= 1024;
10
+ unit += 1;
11
+ }
12
+ // 1023.6 KB rounds to "1024 KB"; promote it to 1.0 MB instead.
13
+ if (unit > 0 && unit < UNITS.length - 1 && Math.round(value) >= 1024) {
14
+ value /= 1024;
15
+ unit += 1;
16
+ }
17
+ if (unit === 0)
18
+ return `${Math.round(value)} B`;
19
+ const oneDecimal = Math.round(value * 10) / 10;
20
+ if (oneDecimal < 10)
21
+ return `${oneDecimal.toFixed(1)} ${UNITS[unit]}`;
22
+ return `${Math.round(value)} ${UNITS[unit]}`;
23
+ }
@@ -0,0 +1,23 @@
1
+ import { type MediaDensity, type MediaSort } from './media-library-view.js';
2
+ /** The preferences that persist per viewer; the kind filter and search text do not. */
3
+ interface MediaLibraryPrefs {
4
+ sort: MediaSort;
5
+ density: MediaDensity;
6
+ }
7
+ /** @internal Exported for tests and stories. */
8
+ export declare const MEDIA_LIBRARY_PREFS_KEY = "canopycms:media-library:prefs";
9
+ /**
10
+ * @internal Exported for tests. Validates each field on its own, so a bad one takes its default and the other is kept. Mantine
11
+ * does not guard `deserialize`, so this never throws, whatever is under the key.
12
+ */
13
+ export declare function parseMediaLibraryPrefs(raw: string | undefined): MediaLibraryPrefs;
14
+ export interface UseMediaLibraryPrefsReturn extends MediaLibraryPrefs {
15
+ setSort: (sort: MediaSort) => void;
16
+ setDensity: (density: MediaDensity) => void;
17
+ }
18
+ /**
19
+ * Sort and density, read from localStorage after mount (SSR renders the defaults). A storage that
20
+ * throws leaves them in memory for the session.
21
+ */
22
+ export declare function useMediaLibraryPrefs(): UseMediaLibraryPrefsReturn;
23
+ export {};
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { useLocalStorage } from '@mantine/hooks';
3
+ import { isPlainRecord } from '../../validation/field-traversal.js';
4
+ import { MEDIA_DENSITIES, MEDIA_SORTS, } from './media-library-view.js';
5
+ /** @internal Exported for tests and stories. */
6
+ export const MEDIA_LIBRARY_PREFS_KEY = 'canopycms:media-library:prefs';
7
+ /** Module-level so its identity is stable: Mantine memoises its storage reader on the default. */
8
+ const DEFAULT_PREFS = { sort: 'newest', density: 'grid' };
9
+ const oneOf = (options, value, fallback) => options.find((option) => option === value) ?? fallback;
10
+ /**
11
+ * @internal Exported for tests. Validates each field on its own, so a bad one takes its default and the other is kept. Mantine
12
+ * does not guard `deserialize`, so this never throws, whatever is under the key.
13
+ */
14
+ export function parseMediaLibraryPrefs(raw) {
15
+ if (!raw)
16
+ return DEFAULT_PREFS;
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return DEFAULT_PREFS;
23
+ }
24
+ if (!isPlainRecord(parsed))
25
+ return DEFAULT_PREFS;
26
+ return {
27
+ sort: oneOf(MEDIA_SORTS, parsed.sort, DEFAULT_PREFS.sort),
28
+ density: oneOf(MEDIA_DENSITIES, parsed.density, DEFAULT_PREFS.density),
29
+ };
30
+ }
31
+ /**
32
+ * Sort and density, read from localStorage after mount (SSR renders the defaults). A storage that
33
+ * throws leaves them in memory for the session.
34
+ */
35
+ export function useMediaLibraryPrefs() {
36
+ const [prefs, setPrefs] = useLocalStorage({
37
+ key: MEDIA_LIBRARY_PREFS_KEY,
38
+ defaultValue: DEFAULT_PREFS,
39
+ deserialize: parseMediaLibraryPrefs,
40
+ // Each tab keeps its own choice; a change elsewhere reaches only tabs opened later.
41
+ sync: false,
42
+ });
43
+ return {
44
+ sort: prefs.sort,
45
+ density: prefs.density,
46
+ setSort: (sort) => setPrefs((prev) => ({ ...prev, sort })),
47
+ setDensity: (density) => setPrefs((prev) => ({ ...prev, density })),
48
+ };
49
+ }
@@ -0,0 +1,34 @@
1
+ import type { AssetRecord } from '../../api/index.js';
2
+ export type MediaSort = 'newest' | 'name';
3
+ export type MediaDensity = 'grid' | 'compact' | 'list';
4
+ export type MediaKind = 'all' | 'images' | 'pdfs';
5
+ export declare const MEDIA_SORTS: readonly MediaSort[];
6
+ export declare const MEDIA_DENSITIES: readonly MediaDensity[];
7
+ /**
8
+ * Orders every loaded asset. The list API pages in storage order, so the order is only
9
+ * meaningful here, across everything loaded.
10
+ */
11
+ export declare function sortAssets(assets: readonly AssetRecord[], sort: MediaSort): AssetRecord[];
12
+ export declare function matchesKind(asset: AssetRecord, kind: MediaKind): boolean;
13
+ /** `needle` is already trimmed and lower-cased; an empty needle matches everything. */
14
+ export declare function matchesText(asset: AssetRecord, needle: string): boolean;
15
+ /** "1 file", "2 files". */
16
+ export declare const fileCountLabel: (count: number) => string;
17
+ /** Grid columns per layout; the loading skeleton uses the same ones so nothing shifts. */
18
+ export declare const GRID_COLUMNS: {
19
+ readonly grid: {
20
+ readonly base: 2;
21
+ readonly sm: 3;
22
+ readonly md: 4;
23
+ };
24
+ readonly compact: {
25
+ readonly base: 3;
26
+ readonly sm: 5;
27
+ readonly md: 6;
28
+ };
29
+ };
30
+ /** Card thumbnail heights in px, shared with the skeleton that stands in for them. */
31
+ export declare const CARD_THUMBNAIL_HEIGHT: {
32
+ readonly grid: 100;
33
+ readonly compact: 64;
34
+ };
@@ -0,0 +1,40 @@
1
+ export const MEDIA_SORTS = ['newest', 'name'];
2
+ export const MEDIA_DENSITIES = ['grid', 'compact', 'list'];
3
+ const uploadedAtMs = (asset) => {
4
+ const ms = Date.parse(asset.uploadedAt);
5
+ return Number.isNaN(ms) ? 0 : ms;
6
+ };
7
+ const byNewest = (a, b) => uploadedAtMs(b) - uploadedAtMs(a);
8
+ const byFilename = (a, b) => a.filename.localeCompare(b.filename, undefined, { sensitivity: 'base', numeric: true });
9
+ /**
10
+ * Orders every loaded asset. The list API pages in storage order, so the order is only
11
+ * meaningful here, across everything loaded.
12
+ */
13
+ export function sortAssets(assets, sort) {
14
+ const compare = sort === 'name'
15
+ ? (a, b) => byFilename(a, b) || byNewest(a, b)
16
+ : (a, b) => byNewest(a, b) || byFilename(a, b);
17
+ return [...assets].sort(compare);
18
+ }
19
+ export function matchesKind(asset, kind) {
20
+ if (kind === 'pdfs')
21
+ return asset.kind === 'pdf';
22
+ if (kind === 'images')
23
+ return asset.kind !== 'pdf';
24
+ return true;
25
+ }
26
+ /** `needle` is already trimmed and lower-cased; an empty needle matches everything. */
27
+ export function matchesText(asset, needle) {
28
+ if (!needle)
29
+ return true;
30
+ return asset.filename.toLowerCase().includes(needle) || asset.slug.toLowerCase().includes(needle);
31
+ }
32
+ /** "1 file", "2 files". */
33
+ export const fileCountLabel = (count) => `${count} ${count === 1 ? 'file' : 'files'}`;
34
+ /** Grid columns per layout; the loading skeleton uses the same ones so nothing shifts. */
35
+ export const GRID_COLUMNS = {
36
+ grid: { base: 2, sm: 3, md: 4 },
37
+ compact: { base: 3, sm: 5, md: 6 },
38
+ };
39
+ /** Card thumbnail heights in px, shared with the skeleton that stands in for them. */
40
+ export const CARD_THUMBNAIL_HEIGHT = { grid: 100, compact: 64 };