opacacms 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 (399) hide show
  1. package/bun.lock +34 -0
  2. package/dist/admin/api-client.d.ts +8 -0
  3. package/dist/admin/auth-client.d.ts +940 -0
  4. package/dist/admin/custom-field.d.ts +71 -0
  5. package/dist/admin/index.d.ts +11 -0
  6. package/dist/admin/react.d.ts +3 -0
  7. package/dist/admin/router.d.ts +7 -0
  8. package/dist/admin/stores/admin-queries.d.ts +32 -0
  9. package/dist/admin/stores/auth.d.ts +33 -0
  10. package/dist/admin/stores/column-visibility.d.ts +21 -0
  11. package/dist/admin/stores/config.d.ts +7 -0
  12. package/dist/admin/stores/media.d.ts +44 -0
  13. package/dist/admin/stores/query.d.ts +4 -0
  14. package/dist/admin/stores/ui.d.ts +11 -0
  15. package/dist/admin/ui/admin-client.d.ts +7 -0
  16. package/dist/admin/ui/admin-layout.d.ts +14 -0
  17. package/dist/admin/ui/components/ColumnVisibilityToggle.d.ts +10 -0
  18. package/dist/admin/ui/components/DataDetailSheet.d.ts +13 -0
  19. package/dist/admin/ui/components/DataDetailView.d.ts +9 -0
  20. package/dist/admin/ui/components/Table.d.ts +10 -0
  21. package/dist/admin/ui/components/fields/ArrayField.d.ts +13 -0
  22. package/dist/admin/ui/components/fields/BlocksField.d.ts +17 -0
  23. package/dist/admin/ui/components/fields/BooleanField.d.ts +13 -0
  24. package/dist/admin/ui/components/fields/CollapsibleField.d.ts +16 -0
  25. package/dist/admin/ui/components/fields/DateField.d.ts +13 -0
  26. package/dist/admin/ui/components/fields/FileField.d.ts +23 -0
  27. package/dist/admin/ui/components/fields/GroupField.d.ts +13 -0
  28. package/dist/admin/ui/components/fields/JoinField.d.ts +15 -0
  29. package/dist/admin/ui/components/fields/NumberField.d.ts +14 -0
  30. package/dist/admin/ui/components/fields/RadioField.d.ts +17 -0
  31. package/dist/admin/ui/components/fields/RelationshipField.d.ts +16 -0
  32. package/dist/admin/ui/components/fields/RowField.d.ts +12 -0
  33. package/dist/admin/ui/components/fields/SelectField.d.ts +18 -0
  34. package/dist/admin/ui/components/fields/TabsField.d.ts +15 -0
  35. package/dist/admin/ui/components/fields/TextAreaField.d.ts +14 -0
  36. package/dist/admin/ui/components/fields/TextField.d.ts +14 -0
  37. package/dist/admin/ui/components/fields/VirtualField.d.ts +8 -0
  38. package/dist/admin/ui/components/fields/index.d.ts +28 -0
  39. package/dist/admin/ui/components/fields/richtext-editor/index.d.ts +10 -0
  40. package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.d.ts +7 -0
  41. package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageNode.d.ts +27 -0
  42. package/dist/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.d.ts +1 -0
  43. package/dist/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.d.ts +5 -0
  44. package/dist/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.d.ts +1 -0
  45. package/dist/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.d.ts +1 -0
  46. package/dist/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.d.ts +5 -0
  47. package/dist/admin/ui/components/fields/utils.d.ts +1 -0
  48. package/dist/admin/ui/components/link.d.ts +8 -0
  49. package/dist/admin/ui/components/media/AssetManagerModal.d.ts +17 -0
  50. package/dist/admin/ui/components/toast.d.ts +10 -0
  51. package/dist/admin/ui/components/ui/accordion.d.ts +11 -0
  52. package/dist/admin/ui/components/ui/alert-dialog.d.ts +12 -0
  53. package/dist/admin/ui/components/ui/blocks.d.ts +5 -0
  54. package/dist/admin/ui/components/ui/breadcrumbs.d.ts +7 -0
  55. package/dist/admin/ui/components/ui/button.d.ts +7 -0
  56. package/dist/admin/ui/components/ui/collapsible.d.ts +16 -0
  57. package/dist/admin/ui/components/ui/dialog.d.ts +27 -0
  58. package/dist/admin/ui/components/ui/group.d.ts +6 -0
  59. package/dist/admin/ui/components/ui/index.d.ts +17 -0
  60. package/dist/admin/ui/components/ui/input.d.ts +5 -0
  61. package/dist/admin/ui/components/ui/join.d.ts +7 -0
  62. package/dist/admin/ui/components/ui/label.d.ts +3 -0
  63. package/dist/admin/ui/components/ui/radio-group.d.ts +13 -0
  64. package/dist/admin/ui/components/ui/relationship-detail-sheet.d.ts +9 -0
  65. package/dist/admin/ui/components/ui/relationship.d.ts +8 -0
  66. package/dist/admin/ui/components/ui/scroll-area.d.ts +7 -0
  67. package/dist/admin/ui/components/ui/select.d.ts +37 -0
  68. package/dist/admin/ui/components/ui/separator.d.ts +8 -0
  69. package/dist/admin/ui/components/ui/sheet.d.ts +28 -0
  70. package/dist/admin/ui/components/ui/tabs.d.ts +17 -0
  71. package/dist/admin/ui/components/ui/utils.d.ts +1 -0
  72. package/dist/admin/ui/hooks/use-debounce.d.ts +1 -0
  73. package/dist/admin/ui/views/collection-list-view.d.ts +5 -0
  74. package/dist/admin/ui/views/dashboard-view.d.ts +10 -0
  75. package/dist/admin/ui/views/document-edit-view.d.ts +7 -0
  76. package/dist/admin/ui/views/global-edit-view.d.ts +19 -0
  77. package/dist/admin/ui/views/init-view.d.ts +4 -0
  78. package/dist/admin/ui/views/login-view.d.ts +4 -0
  79. package/dist/admin/ui/views/media-registry-view.d.ts +7 -0
  80. package/dist/admin/ui/views/settings-view.d.ts +7 -0
  81. package/dist/admin/webcomponent.d.ts +1 -0
  82. package/dist/api.d.ts +6 -0
  83. package/dist/auth/index.d.ts +2107 -0
  84. package/dist/auth/migrations.d.ts +5 -0
  85. package/dist/auth/premissions.d.ts +6 -0
  86. package/dist/chunk-16vgcf3k.js +88 -0
  87. package/dist/chunk-2zm8cy1w.js +9482 -0
  88. package/dist/chunk-5gvbp2qa.js +167 -0
  89. package/dist/chunk-62ev8gnc.js +41 -0
  90. package/dist/chunk-6dhs73zq.js +126 -0
  91. package/dist/chunk-6ew02s0c.js +472 -0
  92. package/dist/chunk-7a9kn0np.js +116 -0
  93. package/dist/chunk-8gkhn1d4.js +309 -0
  94. package/dist/chunk-8sqjbsgt.js +42 -0
  95. package/dist/chunk-9kxpbcb1.js +85 -0
  96. package/dist/chunk-cvdd4eqh.js +110 -0
  97. package/dist/chunk-d3ffeqp9.js +87 -0
  98. package/dist/chunk-dy5t83hr.js +261 -0
  99. package/dist/chunk-f3nvxn63.js +17 -0
  100. package/dist/chunk-hmhcense.js +1352 -0
  101. package/dist/chunk-j4d50hrx.js +20 -0
  102. package/dist/chunk-jwjk85ze.js +15 -0
  103. package/dist/chunk-kwp83w8b.js +250 -0
  104. package/dist/chunk-s8mqwnm1.js +14 -0
  105. package/dist/chunk-srsac177.js +85 -0
  106. package/dist/chunk-v521d72w.js +10 -0
  107. package/dist/chunk-xa7rjsn2.js +20 -0
  108. package/dist/chunk-xg35h5a3.js +15 -0
  109. package/dist/chunk-ybbbqj63.js +130 -0
  110. package/dist/chunk-zvwb67nd.js +332 -0
  111. package/dist/cli/commands/generate-types.d.ts +1 -0
  112. package/dist/cli/commands/init.d.ts +1 -0
  113. package/dist/cli/commands/migrate-commands.d.ts +5 -0
  114. package/dist/cli/commands/seed-command.d.ts +2 -0
  115. package/dist/cli/d1-mock.d.ts +30 -0
  116. package/dist/cli/index.d.ts +5 -0
  117. package/dist/cli/index.test.d.ts +1 -0
  118. package/dist/cli/r2-mock.d.ts +46 -0
  119. package/dist/cli/seeding.d.ts +17 -0
  120. package/dist/client.d.ts +51 -0
  121. package/dist/config-utils.d.ts +6 -0
  122. package/dist/config.d.ts +10 -0
  123. package/dist/db/adapter.d.ts +34 -0
  124. package/dist/db/better-sqlite.d.ts +40 -0
  125. package/dist/db/bun-sqlite.d.ts +40 -0
  126. package/dist/db/d1.d.ts +42 -0
  127. package/dist/db/kysely/data-mapper.d.ts +6 -0
  128. package/dist/db/kysely/field-mapper.d.ts +22 -0
  129. package/dist/db/kysely/migration-generator.d.ts +9 -0
  130. package/dist/db/kysely/query-builder.d.ts +9 -0
  131. package/dist/db/kysely/schema-builder.d.ts +15 -0
  132. package/dist/db/kysely/sql-utils.d.ts +1 -0
  133. package/dist/db/postgres.d.ts +51 -0
  134. package/dist/db/sqlite.d.ts +41 -0
  135. package/dist/db/system-schema.d.ts +2 -0
  136. package/dist/index.d.ts +6 -0
  137. package/dist/runtimes/bun.d.ts +17 -0
  138. package/dist/runtimes/cloudflare-workers.d.ts +10 -0
  139. package/dist/runtimes/next.d.ts +16 -0
  140. package/dist/runtimes/node.d.ts +18 -0
  141. package/dist/schema/collection.d.ts +100 -0
  142. package/dist/schema/fields/base.d.ts +83 -0
  143. package/dist/schema/fields/index.d.ts +135 -0
  144. package/dist/schema/global.d.ts +82 -0
  145. package/dist/schema/index.d.ts +4 -0
  146. package/dist/schema/infer.d.ts +55 -0
  147. package/dist/server/admin-router.d.ts +9 -0
  148. package/dist/server/admin.d.ts +18 -0
  149. package/dist/server/assets.d.ts +47 -0
  150. package/dist/server/collection-router.d.ts +14 -0
  151. package/dist/server/handlers.d.ts +76 -0
  152. package/dist/server/middlewares/admin.d.ts +6 -0
  153. package/dist/server/middlewares/auth.d.ts +16 -0
  154. package/dist/server/middlewares/context.d.ts +9 -0
  155. package/dist/server/middlewares/cors.d.ts +3 -0
  156. package/dist/server/middlewares/database-init.d.ts +11 -0
  157. package/dist/server/middlewares/rate-limit.d.ts +3 -0
  158. package/dist/server/router.d.ts +7 -0
  159. package/dist/server/setup-middlewares.d.ts +17 -0
  160. package/dist/server/system-router.d.ts +9 -0
  161. package/dist/server.d.ts +6 -0
  162. package/dist/src/admin/index.css +47 -0
  163. package/dist/src/admin/index.js +176 -0
  164. package/dist/src/admin/webcomponent.js +19 -0
  165. package/dist/src/api.js +27 -0
  166. package/dist/src/cli/index.js +157 -0
  167. package/dist/src/client.js +9 -0
  168. package/dist/src/db/bun-sqlite.js +523 -0
  169. package/dist/src/db/d1.js +568 -0
  170. package/dist/src/db/postgres.js +520 -0
  171. package/dist/src/db/sqlite.js +534 -0
  172. package/dist/src/index.js +20 -0
  173. package/dist/src/runtimes/bun.js +36 -0
  174. package/dist/src/runtimes/cloudflare-workers.js +29 -0
  175. package/dist/src/runtimes/next.js +26 -0
  176. package/dist/src/runtimes/node.js +38 -0
  177. package/dist/src/server.js +27 -0
  178. package/dist/src/storage/index.js +355 -0
  179. package/dist/storage/adapters/cloudflare-r2.d.ts +6 -0
  180. package/dist/storage/adapters/local.d.ts +6 -0
  181. package/dist/storage/adapters/s3.d.ts +13 -0
  182. package/dist/storage/errors.d.ts +12 -0
  183. package/dist/storage/index.d.ts +5 -0
  184. package/dist/storage/types.d.ts +31 -0
  185. package/dist/types.d.ts +484 -0
  186. package/dist/utils/lexical.d.ts +5 -0
  187. package/dist/utils/logger.d.ts +35 -0
  188. package/dist/validation.d.ts +300 -0
  189. package/dist/validator.d.ts +9 -0
  190. package/global.d.ts +11 -0
  191. package/package.json +151 -0
  192. package/src/admin/api-client.ts +63 -0
  193. package/src/admin/auth-client.ts +40 -0
  194. package/src/admin/custom-field.ts +179 -0
  195. package/src/admin/index.ts +15 -0
  196. package/src/admin/react.tsx +72 -0
  197. package/src/admin/router.ts +9 -0
  198. package/src/admin/stores/admin-queries.ts +121 -0
  199. package/src/admin/stores/auth.ts +61 -0
  200. package/src/admin/stores/column-visibility.ts +67 -0
  201. package/src/admin/stores/config.ts +15 -0
  202. package/src/admin/stores/media.ts +95 -0
  203. package/src/admin/stores/query.ts +13 -0
  204. package/src/admin/stores/ui.ts +29 -0
  205. package/src/admin/ui/admin-client.tsx +283 -0
  206. package/src/admin/ui/admin-layout.tsx +276 -0
  207. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +141 -0
  208. package/src/admin/ui/components/DataDetailSheet.tsx +141 -0
  209. package/src/admin/ui/components/DataDetailView.tsx +175 -0
  210. package/src/admin/ui/components/Table.tsx +67 -0
  211. package/src/admin/ui/components/fields/ArrayField.tsx +166 -0
  212. package/src/admin/ui/components/fields/BlocksField.tsx +202 -0
  213. package/src/admin/ui/components/fields/BooleanField.tsx +50 -0
  214. package/src/admin/ui/components/fields/CollapsibleField.tsx +75 -0
  215. package/src/admin/ui/components/fields/DateField.tsx +45 -0
  216. package/src/admin/ui/components/fields/FileField.tsx +322 -0
  217. package/src/admin/ui/components/fields/GroupField.tsx +50 -0
  218. package/src/admin/ui/components/fields/JoinField.tsx +23 -0
  219. package/src/admin/ui/components/fields/NumberField.tsx +46 -0
  220. package/src/admin/ui/components/fields/RadioField.tsx +62 -0
  221. package/src/admin/ui/components/fields/RelationshipField.tsx +278 -0
  222. package/src/admin/ui/components/fields/RowField.tsx +40 -0
  223. package/src/admin/ui/components/fields/SelectField.tsx +59 -0
  224. package/src/admin/ui/components/fields/TabsField.tsx +101 -0
  225. package/src/admin/ui/components/fields/TextAreaField.tsx +54 -0
  226. package/src/admin/ui/components/fields/TextField.tsx +49 -0
  227. package/src/admin/ui/components/fields/VirtualField.tsx +53 -0
  228. package/src/admin/ui/components/fields/index.tsx +371 -0
  229. package/src/admin/ui/components/fields/richtext-editor/index.tsx +211 -0
  230. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +142 -0
  231. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +95 -0
  232. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +226 -0
  233. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +16 -0
  234. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +184 -0
  235. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +240 -0
  236. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +40 -0
  237. package/src/admin/ui/components/fields/utils.ts +1 -0
  238. package/src/admin/ui/components/link.tsx +41 -0
  239. package/src/admin/ui/components/media/AssetManagerModal.tsx +334 -0
  240. package/src/admin/ui/components/toast.tsx +72 -0
  241. package/src/admin/ui/components/ui/accordion.tsx +51 -0
  242. package/src/admin/ui/components/ui/alert-dialog.tsx +98 -0
  243. package/src/admin/ui/components/ui/blocks.tsx +32 -0
  244. package/src/admin/ui/components/ui/breadcrumbs.tsx +59 -0
  245. package/src/admin/ui/components/ui/button.tsx +26 -0
  246. package/src/admin/ui/components/ui/collapsible.tsx +124 -0
  247. package/src/admin/ui/components/ui/dialog.tsx +79 -0
  248. package/src/admin/ui/components/ui/group.tsx +20 -0
  249. package/src/admin/ui/components/ui/index.ts +17 -0
  250. package/src/admin/ui/components/ui/input.tsx +12 -0
  251. package/src/admin/ui/components/ui/join.tsx +53 -0
  252. package/src/admin/ui/components/ui/label.tsx +11 -0
  253. package/src/admin/ui/components/ui/radio-group.tsx +75 -0
  254. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +122 -0
  255. package/src/admin/ui/components/ui/relationship.tsx +58 -0
  256. package/src/admin/ui/components/ui/scroll-area.tsx +19 -0
  257. package/src/admin/ui/components/ui/select.tsx +187 -0
  258. package/src/admin/ui/components/ui/separator.tsx +21 -0
  259. package/src/admin/ui/components/ui/sheet.tsx +106 -0
  260. package/src/admin/ui/components/ui/tabs.tsx +116 -0
  261. package/src/admin/ui/components/ui/utils.ts +3 -0
  262. package/src/admin/ui/hooks/use-debounce.ts +15 -0
  263. package/src/admin/ui/styles/_locale-switcher.scss +33 -0
  264. package/src/admin/ui/styles/accordion.scss +60 -0
  265. package/src/admin/ui/styles/animations.scss +41 -0
  266. package/src/admin/ui/styles/asset-manager.scss +547 -0
  267. package/src/admin/ui/styles/badge.scss +13 -0
  268. package/src/admin/ui/styles/base.scss +22 -0
  269. package/src/admin/ui/styles/button.scss +161 -0
  270. package/src/admin/ui/styles/card.scss +13 -0
  271. package/src/admin/ui/styles/collapsible.scss +75 -0
  272. package/src/admin/ui/styles/data-detail.scss +92 -0
  273. package/src/admin/ui/styles/dialog.scss +102 -0
  274. package/src/admin/ui/styles/empty-state.scss +22 -0
  275. package/src/admin/ui/styles/group.scss +19 -0
  276. package/src/admin/ui/styles/index.scss +33 -0
  277. package/src/admin/ui/styles/input.scss +80 -0
  278. package/src/admin/ui/styles/label.scss +12 -0
  279. package/src/admin/ui/styles/layout.scss +56 -0
  280. package/src/admin/ui/styles/lexical.scss +469 -0
  281. package/src/admin/ui/styles/loading.scss +102 -0
  282. package/src/admin/ui/styles/media-registry.scss +597 -0
  283. package/src/admin/ui/styles/pagination.scss +20 -0
  284. package/src/admin/ui/styles/radio-group.scss +66 -0
  285. package/src/admin/ui/styles/row.scss +17 -0
  286. package/src/admin/ui/styles/scrollbar.scss +36 -0
  287. package/src/admin/ui/styles/select.scss +121 -0
  288. package/src/admin/ui/styles/separator.scss +14 -0
  289. package/src/admin/ui/styles/sheet.scss +152 -0
  290. package/src/admin/ui/styles/sidebar.scss +148 -0
  291. package/src/admin/ui/styles/switch.scss +59 -0
  292. package/src/admin/ui/styles/table.scss +207 -0
  293. package/src/admin/ui/styles/tabs.scss +62 -0
  294. package/src/admin/ui/styles/toast.scss +45 -0
  295. package/src/admin/ui/styles/variables.scss +24 -0
  296. package/src/admin/ui/views/collection-list-view.tsx +720 -0
  297. package/src/admin/ui/views/dashboard-view.tsx +263 -0
  298. package/src/admin/ui/views/document-edit-view.tsx +384 -0
  299. package/src/admin/ui/views/global-edit-view.tsx +226 -0
  300. package/src/admin/ui/views/init-view.tsx +182 -0
  301. package/src/admin/ui/views/login-view.tsx +123 -0
  302. package/src/admin/ui/views/media-registry-view.tsx +1104 -0
  303. package/src/admin/ui/views/settings-view.tsx +729 -0
  304. package/src/admin/webcomponent.tsx +15 -0
  305. package/src/api.ts +9 -0
  306. package/src/auth/index.ts +194 -0
  307. package/src/auth/migrations.ts +87 -0
  308. package/src/auth/premissions.ts +46 -0
  309. package/src/cli/commands/generate-types.ts +116 -0
  310. package/src/cli/commands/init.ts +95 -0
  311. package/src/cli/commands/migrate-commands.ts +160 -0
  312. package/src/cli/commands/seed-command.ts +11 -0
  313. package/src/cli/d1-mock.ts +101 -0
  314. package/src/cli/index.test.ts +84 -0
  315. package/src/cli/index.ts +183 -0
  316. package/src/cli/r2-mock.ts +217 -0
  317. package/src/cli/seeding.ts +405 -0
  318. package/src/client.ts +181 -0
  319. package/src/config-utils.ts +102 -0
  320. package/src/config.ts +49 -0
  321. package/src/db/adapter.ts +53 -0
  322. package/src/db/better-sqlite.ts +630 -0
  323. package/src/db/bun-sqlite.ts +646 -0
  324. package/src/db/d1.ts +711 -0
  325. package/src/db/kysely/data-mapper.ts +142 -0
  326. package/src/db/kysely/field-mapper.ts +148 -0
  327. package/src/db/kysely/migration-generator.ts +223 -0
  328. package/src/db/kysely/query-builder.ts +92 -0
  329. package/src/db/kysely/schema-builder.ts +439 -0
  330. package/src/db/kysely/sql-utils.ts +13 -0
  331. package/src/db/postgres.ts +621 -0
  332. package/src/db/sqlite.ts +658 -0
  333. package/src/db/system-schema.ts +121 -0
  334. package/src/index.ts +13 -0
  335. package/src/runtimes/README.md +59 -0
  336. package/src/runtimes/bun.ts +49 -0
  337. package/src/runtimes/cloudflare-workers.ts +38 -0
  338. package/src/runtimes/next.ts +26 -0
  339. package/src/runtimes/node.ts +52 -0
  340. package/src/schema/collection.ts +184 -0
  341. package/src/schema/fields/base.ts +164 -0
  342. package/src/schema/fields/index.ts +427 -0
  343. package/src/schema/global.ts +145 -0
  344. package/src/schema/index.ts +4 -0
  345. package/src/schema/infer.ts +72 -0
  346. package/src/server/admin-router.ts +20 -0
  347. package/src/server/admin.ts +142 -0
  348. package/src/server/assets.ts +306 -0
  349. package/src/server/collection-router.ts +55 -0
  350. package/src/server/handlers.ts +722 -0
  351. package/src/server/middlewares/admin.ts +27 -0
  352. package/src/server/middlewares/auth.ts +89 -0
  353. package/src/server/middlewares/context.ts +17 -0
  354. package/src/server/middlewares/cors.ts +24 -0
  355. package/src/server/middlewares/database-init.ts +74 -0
  356. package/src/server/middlewares/rate-limit.ts +71 -0
  357. package/src/server/router.ts +47 -0
  358. package/src/server/setup-middlewares.ts +58 -0
  359. package/src/server/system-router.ts +35 -0
  360. package/src/server.ts +9 -0
  361. package/src/storage/adapters/cloudflare-r2.ts +136 -0
  362. package/src/storage/adapters/local.ts +146 -0
  363. package/src/storage/adapters/s3.ts +186 -0
  364. package/src/storage/errors.ts +46 -0
  365. package/src/storage/index.ts +5 -0
  366. package/src/storage/types.ts +39 -0
  367. package/src/types.ts +577 -0
  368. package/src/utils/lexical.ts +37 -0
  369. package/src/utils/logger.ts +73 -0
  370. package/src/validation.ts +429 -0
  371. package/src/validator.ts +179 -0
  372. package/test/admin-custom-field.test.ts +162 -0
  373. package/test/admin-react-field.test.tsx +134 -0
  374. package/test/api-features.test.ts +78 -0
  375. package/test/api.test.ts +178 -0
  376. package/test/auth.test.ts +62 -0
  377. package/test/cli-integration.test.ts +146 -0
  378. package/test/cli.test.ts +25 -0
  379. package/test/db/postgres.test.ts +95 -0
  380. package/test/db/sqlite-filter.test.ts +53 -0
  381. package/test/db/sqlite.test.ts +82 -0
  382. package/test/engine-features.test.ts +79 -0
  383. package/test/globals.test.ts +74 -0
  384. package/test/integration-tmp/db-app/opacacms.config.ts +15 -0
  385. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +25 -0
  386. package/test/integration-tmp/my-test-app/index.ts +8 -0
  387. package/test/integration-tmp/my-test-app/opacacms.config.ts +16 -0
  388. package/test/integration-tmp/my-test-app/package.json +12 -0
  389. package/test/populate.test.ts +79 -0
  390. package/test/runtimes.test.ts +43 -0
  391. package/test/schema-builder.test.ts +107 -0
  392. package/test/schema-features.test.ts +63 -0
  393. package/test/seeding.test.ts +68 -0
  394. package/test/storage/local.test.ts +72 -0
  395. package/test/storage/s3.test.ts +60 -0
  396. package/test/structural-data.test.ts +100 -0
  397. package/test/test-setup.ts +11 -0
  398. package/test/validation.test.ts +162 -0
  399. package/tsconfig.json +42 -0
@@ -0,0 +1,720 @@
1
+ import { useStore } from "@nanostores/react";
2
+ import {
3
+ ArrowDownAZ,
4
+ ArrowUpZA,
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ Edit2,
8
+ Inbox,
9
+ Loader2,
10
+ Image as LucideImage,
11
+ Plus,
12
+ Search,
13
+ } from "lucide-react";
14
+ import { useEffect, useMemo, useState } from "react";
15
+ import type { SerializableCollection } from "../../../types";
16
+ import { lexicalToPlainText } from "../../../utils/lexical";
17
+ import { api, getCurrentBaseURL } from "../../api-client";
18
+ import { $collectionData, $collectionQuery } from "../../stores/admin-queries";
19
+ import { $columnVisibility, initColumnVisibility } from "../../stores/column-visibility";
20
+ import { $config } from "../../stores/config";
21
+ import type { ToastType } from "../../stores/ui";
22
+ import { notify } from "../../stores/ui";
23
+ import { ColumnVisibilityToggle } from "../components/ColumnVisibilityToggle";
24
+ import { DataDetailSheet } from "../components/DataDetailSheet";
25
+ import { Link } from "../components/link";
26
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/Table";
27
+ import { RelationshipDetailSheet } from "../components/ui/relationship-detail-sheet";
28
+ import { useDebounce } from "../hooks/use-debounce";
29
+
30
+ function toSnakeCase(str: string): string {
31
+ return str
32
+ .replace(/([A-Z])/g, "_$1")
33
+ .toLowerCase()
34
+ .replace(/^_/, "");
35
+ }
36
+
37
+ function formatTableDate(date: string | number | Date | undefined | null, includeTime = false) {
38
+ if (!date) return "-";
39
+ const d = new Date(date);
40
+ if (Number.isNaN(d.getTime())) return String(date);
41
+ return includeTime ? d.toLocaleString() : d.toLocaleDateString();
42
+ }
43
+
44
+ export interface CollectionListViewProps {
45
+ collection: SerializableCollection;
46
+ }
47
+
48
+ export function CollectionListView({ collection }: CollectionListViewProps) {
49
+ const [page, setPage] = useState(1);
50
+ const [limit, setLimit] = useState(10);
51
+ const [sort, setSort] = useState<string>("");
52
+ const [search, setSearch] = useState("");
53
+ const [activeView, setActiveView] = useState<number | null>(null);
54
+ const [inspection, setInspection] = useState<{
55
+ open: boolean;
56
+ title: string;
57
+ data: any;
58
+ field?: string;
59
+ docId?: string | number;
60
+ }>({ open: false, title: "", data: null });
61
+
62
+ const [detailView, setDetailView] = useState<{
63
+ open: boolean;
64
+ relationTo: string;
65
+ id: string | number;
66
+ }>({ open: false, relationTo: "", id: "" });
67
+ const debouncedSearch = useDebounce(search, 400);
68
+
69
+ const visibility = useStore($columnVisibility);
70
+
71
+ // All named fields (flattened)
72
+ const allNamedFields = useMemo(
73
+ () => collection.fields.filter((f) => f.name),
74
+ [collection.fields],
75
+ );
76
+
77
+ // Initialize defaults once per collection
78
+ useEffect(() => {
79
+ const defaultCols =
80
+ typeof collection.admin === "object" ? collection.admin?.defaultColumns : undefined;
81
+ initColumnVisibility(collection.slug, allNamedFields, defaultCols);
82
+ }, [
83
+ collection.slug,
84
+ allNamedFields,
85
+ typeof collection.admin === "object" ? collection.admin?.defaultColumns : undefined,
86
+ ]);
87
+
88
+ // Derive visible fields from store
89
+ const visibleFields = useMemo(() => {
90
+ const visibleFieldNames = visibility[collection.slug];
91
+ return visibleFieldNames
92
+ ? allNamedFields.filter((f) => visibleFieldNames.includes(f.name as string))
93
+ : allNamedFields;
94
+ }, [visibility, collection.slug, allNamedFields]);
95
+
96
+ // Update query atom when params change
97
+ useEffect(() => {
98
+ const defaultSearchField =
99
+ typeof collection.admin === "object" && collection.admin !== null
100
+ ? collection.admin.useAsTitle
101
+ : undefined;
102
+
103
+ const searchField =
104
+ defaultSearchField ||
105
+ allNamedFields.find((f) => f.name === "title" || f.name === "name" || f.name === "slug")
106
+ ?.name ||
107
+ "id";
108
+
109
+ const populate = collection.fields
110
+ .filter((f) => f.type === "relationship" && (f as any).displayField)
111
+ .map((f) => f.name)
112
+ .join(",");
113
+
114
+ $collectionQuery.set({
115
+ slug: collection.apiPath || collection.slug,
116
+ page,
117
+ limit,
118
+ sort,
119
+ search: debouncedSearch,
120
+ searchField,
121
+ filter:
122
+ activeView !== null && typeof collection.admin === "object"
123
+ ? collection.admin?.views?.[activeView]?.filter
124
+ : undefined,
125
+ populate: populate || undefined,
126
+ });
127
+ }, [
128
+ collection.slug,
129
+ collection.apiPath,
130
+ collection.admin,
131
+ allNamedFields,
132
+ page,
133
+ limit,
134
+ sort,
135
+ debouncedSearch,
136
+ activeView,
137
+ ]);
138
+
139
+ const { data, loading } = useStore($collectionData);
140
+
141
+ const handleSort = (fieldName: string) => {
142
+ if (sort === fieldName) {
143
+ setSort(`-${fieldName}`); // toggle to desc
144
+ } else if (sort === `-${fieldName}`) {
145
+ setSort(""); // remove sort
146
+ } else {
147
+ setSort(fieldName); // asc
148
+ }
149
+ setPage(1); // reset to first page on sort change
150
+ };
151
+
152
+ const handleQuickEditSave = async (updatedData: any) => {
153
+ if (!inspection.field || !inspection.docId) return;
154
+
155
+ try {
156
+ const payload = { [inspection.field]: updatedData };
157
+ await api
158
+ .patch(`api/${(collection.apiPath || collection.slug) as string}/${inspection.docId}`, {
159
+ json: payload,
160
+ })
161
+ .json();
162
+
163
+ // Trigger a re-fetch of the collection data
164
+ const currentQuery = $collectionQuery.get();
165
+ if (currentQuery) {
166
+ $collectionQuery.set({
167
+ ...currentQuery,
168
+ slug: currentQuery.slug as string, // Ensure slug is string
169
+ });
170
+ }
171
+ notify("Document updated successfully", "success");
172
+ setInspection({ ...inspection, open: false });
173
+ } catch (e) {
174
+ console.error(e);
175
+ notify("Failed to update document", "error");
176
+ }
177
+ };
178
+
179
+ const getSortIcon = (fieldName: string) => {
180
+ if (sort === fieldName) {
181
+ return <ArrowDownAZ size={14} style={{ marginLeft: "4px", opacity: 0.7 }} />;
182
+ }
183
+ if (sort === `-${fieldName}`) {
184
+ return <ArrowUpZA size={14} style={{ marginLeft: "4px", opacity: 0.7 }} />;
185
+ }
186
+ return null;
187
+ };
188
+
189
+ const docs = data?.docs || [];
190
+
191
+ const createdAtField = useMemo(() => {
192
+ if (typeof collection.timestamps === "object" && collection.timestamps?.createdAt) {
193
+ return toSnakeCase(collection.timestamps.createdAt);
194
+ }
195
+ return "created_at";
196
+ }, [collection.timestamps]);
197
+
198
+ return (
199
+ <div className={`opaca-view-container ${loading ? "loading" : ""}`}>
200
+ <div className="opaca-header">
201
+ <div>
202
+ <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
203
+ <h1 className="opaca-title">
204
+ {collection.label ||
205
+ (collection.slug.startsWith("_")
206
+ ? collection.slug
207
+ .replace(/^_+/, "")
208
+ .replace(/_/g, " ")
209
+ .replace(/\b\w/g, (c) => c.toUpperCase())
210
+ : collection.slug.charAt(0).toUpperCase() + collection.slug.slice(1))}
211
+ </h1>
212
+ {data && (
213
+ <span className="opaca-badge">
214
+ {data.totalDocs} {data.totalDocs === 1 ? "document" : "documents"}
215
+ </span>
216
+ )}
217
+ </div>
218
+ <p className="opaca-subtitle">Manage your {collection.slug} entries.</p>
219
+ </div>
220
+
221
+ <div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
222
+ <div style={{ position: "relative", width: "240px" }}>
223
+ <Search
224
+ size={16}
225
+ style={{
226
+ position: "absolute",
227
+ left: "10px",
228
+ top: "50%",
229
+ transform: "translateY(-50%)",
230
+ color: "var(--opaca-text-muted)",
231
+ }}
232
+ />
233
+ <input
234
+ type="text"
235
+ className="opaca-input"
236
+ placeholder="Search documents..."
237
+ value={search}
238
+ onChange={(e) => {
239
+ setSearch(e.target.value);
240
+ setPage(1);
241
+ }}
242
+ style={{ paddingLeft: "36px" }}
243
+ />
244
+ </div>
245
+ <ColumnVisibilityToggle slug={collection.slug} fields={allNamedFields} />
246
+ <Link
247
+ href={`/admin/collections/${collection.slug}/create`}
248
+ className="opaca-btn opaca-btn-primary"
249
+ style={{ textDecoration: "none" }}
250
+ >
251
+ <Plus size={18} />
252
+ Create New
253
+ </Link>
254
+ </div>
255
+ </div>
256
+
257
+ {typeof collection.admin === "object" &&
258
+ collection.admin?.views &&
259
+ collection.admin.views.length > 0 && (
260
+ <div
261
+ style={{
262
+ display: "flex",
263
+ gap: "0.5rem",
264
+ marginBottom: "1.25rem",
265
+ overflowX: "auto",
266
+ paddingBottom: "0.5rem",
267
+ }}
268
+ >
269
+ <button
270
+ type="button"
271
+ className={`opaca-btn ${activeView === null ? "opaca-btn-primary" : "opaca-btn-outline"}`}
272
+ onClick={() => {
273
+ setActiveView(null);
274
+ setPage(1);
275
+ }}
276
+ style={{ padding: "0.4rem 1rem", fontSize: "0.875rem" }}
277
+ >
278
+ All
279
+ </button>
280
+ {collection.admin.views.map((view, idx) => (
281
+ <button
282
+ key={view.name}
283
+ type="button"
284
+ className={`opaca-btn ${activeView === idx ? "opaca-btn-primary" : "opaca-btn-outline"}`}
285
+ onClick={() => {
286
+ setActiveView(idx);
287
+ setPage(1);
288
+ }}
289
+ style={{ padding: "0.4rem 1rem", fontSize: "0.875rem" }}
290
+ >
291
+ {view.name}
292
+ </button>
293
+ ))}
294
+ </div>
295
+ )}
296
+
297
+ <div className="opaca-table-container">
298
+ {loading && (
299
+ <div className="opaca-loading-overlay">
300
+ <Loader2 size={24} className="opaca-spin" />
301
+ </div>
302
+ )}
303
+
304
+ <DataDetailSheet
305
+ open={inspection.open}
306
+ onOpenChange={(open) => setInspection({ ...inspection, open })}
307
+ title={inspection.title}
308
+ data={inspection.data}
309
+ onSave={handleQuickEditSave}
310
+ field={inspection.field}
311
+ />
312
+
313
+ {docs.length > 0 ? (
314
+ <>
315
+ <Table>
316
+ <TableHeader>
317
+ <TableRow>
318
+ {visibleFields.map((field) => (
319
+ <TableHead
320
+ key={field.name}
321
+ onClick={() => handleSort(field.name as string)}
322
+ style={{ cursor: "pointer", userSelect: "none" }}
323
+ >
324
+ <div style={{ display: "flex", alignItems: "center" }}>
325
+ {field.label || field.name}
326
+ {getSortIcon(field.name as string)}
327
+ </div>
328
+ </TableHead>
329
+ ))}
330
+ {collection.timestamps && (
331
+ <TableHead
332
+ onClick={() => handleSort("createdAt")}
333
+ style={{ cursor: "pointer", userSelect: "none" }}
334
+ >
335
+ <div style={{ display: "flex", alignItems: "center" }}>
336
+ Created At
337
+ {getSortIcon("createdAt")}
338
+ </div>
339
+ </TableHead>
340
+ )}
341
+ <TableHead style={{ width: "50px" }}></TableHead>
342
+ </TableRow>
343
+ </TableHeader>
344
+ <TableBody>
345
+ {docs.map((doc: Record<string, unknown>) => (
346
+ <TableRow key={doc.id as string | number}>
347
+ {visibleFields.map((field) => {
348
+ const value = doc[field.name as string];
349
+ return (
350
+ <TableCell
351
+ key={field.name as string}
352
+ className={
353
+ ["richtext", "json", "group", "relationship"].includes(field.type)
354
+ ? "opaca-cell-interactive"
355
+ : ""
356
+ }
357
+ onClick={(e) => {
358
+ if (!["richtext", "json", "group", "relationship"].includes(field.type))
359
+ return;
360
+ e.stopPropagation();
361
+ setInspection({
362
+ open: true,
363
+ title: field.label || field.name || "Details",
364
+ data: value,
365
+ field: field.name as string,
366
+ docId: doc.id as string | number,
367
+ });
368
+ }}
369
+ >
370
+ {field.type === "boolean" ? (
371
+ value ? (
372
+ <span
373
+ className="opaca-badge"
374
+ style={{ color: "var(--opaca-success)" }}
375
+ >
376
+ Yes
377
+ </span>
378
+ ) : (
379
+ <span
380
+ className="opaca-badge"
381
+ style={{ color: "var(--opaca-text-dim)" }}
382
+ >
383
+ No
384
+ </span>
385
+ )
386
+ ) : collection.slug === "_opaca_assets" && field.name === "filename" ? (
387
+ <div
388
+ style={{
389
+ display: "flex",
390
+ alignItems: "center",
391
+ gap: "0.75rem",
392
+ }}
393
+ >
394
+ <div
395
+ style={{
396
+ width: "32px",
397
+ height: "32px",
398
+ borderRadius: "6px",
399
+ background: "var(--opaca-surface)",
400
+ overflow: "hidden",
401
+ display: "flex",
402
+ alignItems: "center",
403
+ justifyContent: "center",
404
+ border: "1px solid var(--opaca-border)",
405
+ }}
406
+ >
407
+ {(() => {
408
+ const mime = String(doc.mimeType || doc.mime_type);
409
+ return mime.startsWith("image/") ? (
410
+ <img
411
+ src={`${getCurrentBaseURL()}/api/${collection.apiPath || collection.slug}/${doc.id}/view`}
412
+ style={{
413
+ width: "100%",
414
+ height: "100%",
415
+ objectFit: "cover",
416
+ }}
417
+ alt=""
418
+ />
419
+ ) : (
420
+ <LucideImage size={14} style={{ opacity: 0.5 }} />
421
+ );
422
+ })()}
423
+ </div>
424
+ <span style={{ fontWeight: 500 }}>{String(value ?? "-")}</span>
425
+ </div>
426
+ ) : field.type === "blocks" && Array.isArray(value) ? (
427
+ <button
428
+ type="button"
429
+ onClick={(e) => {
430
+ e.stopPropagation();
431
+ setInspection({
432
+ open: true,
433
+ title: field.label || field.name || "Blocks",
434
+ data: value,
435
+ field: field.name as string,
436
+ docId: doc.id as string | number,
437
+ });
438
+ }}
439
+ className="opaca-badge opaca-badge-interactive"
440
+ style={{ color: "var(--opaca-text-dim)" }}
441
+ >
442
+ {value.length} block
443
+ {value.length !== 1 ? "s" : ""}
444
+ </button>
445
+ ) : field.type === "relationship" ? (
446
+ <div
447
+ style={{
448
+ display: "flex",
449
+ alignItems: "center",
450
+ gap: "0.5rem",
451
+ }}
452
+ >
453
+ {(() => {
454
+ const firstItemItem = Array.isArray(value) ? value[0] : value;
455
+ if (
456
+ firstItemItem &&
457
+ typeof firstItemItem === "object" &&
458
+ (firstItemItem as any).filename
459
+ ) {
460
+ return (
461
+ <div
462
+ style={{
463
+ width: "24px",
464
+ height: "24px",
465
+ borderRadius: "4px",
466
+ overflow: "hidden",
467
+ border: "1px solid var(--opaca-border)",
468
+ flexShrink: 0,
469
+ }}
470
+ >
471
+ <img
472
+ src={`${getCurrentBaseURL()}/api/_opaca_assets/${(firstItemItem as any).id}/view`}
473
+ style={{
474
+ width: "100%",
475
+ height: "100%",
476
+ objectFit: "cover",
477
+ }}
478
+ alt=""
479
+ />
480
+ </div>
481
+ );
482
+ }
483
+ return null;
484
+ })()}
485
+ {(() => {
486
+ const firstItem = Array.isArray(value) ? value[0] : value;
487
+ return (
488
+ <button
489
+ type="button"
490
+ className="opaca-badge opaca-badge-interactive"
491
+ style={{ color: "var(--opaca-text-dim)" }}
492
+ onClick={(e) => {
493
+ e.stopPropagation();
494
+ const id =
495
+ (firstItem as any)?.id ||
496
+ (typeof firstItem === "string" ? firstItem : "");
497
+ if (id) {
498
+ setDetailView({
499
+ open: true,
500
+ relationTo: field.relationTo || "",
501
+ id,
502
+ });
503
+ }
504
+ }}
505
+ >
506
+ {field.hasMany && Array.isArray(value)
507
+ ? `${value.length} item${value.length !== 1 ? "s" : ""}`
508
+ : typeof firstItem === "object" && firstItem !== null
509
+ ? (firstItem as any)[(field as any).displayField || "id"] ||
510
+ (firstItem as any).id ||
511
+ "[Object]"
512
+ : String(value ?? "-")}
513
+ </button>
514
+ );
515
+ })()}
516
+ </div>
517
+ ) : field.type === "group" &&
518
+ typeof value === "object" &&
519
+ value !== null ? (
520
+ <span
521
+ className="opaca-badge"
522
+ style={{
523
+ background: "rgba(255, 255, 255, 0.05)",
524
+ border: "1px solid var(--opaca-border)",
525
+ color: "var(--opaca-text-dim)",
526
+ }}
527
+ >
528
+ {`{ ${Object.keys(value).length} keys }`}
529
+ </span>
530
+ ) : field.type === "json" &&
531
+ typeof value === "object" &&
532
+ value !== null ? (
533
+ <span
534
+ style={{
535
+ color: "var(--opaca-text-dim)",
536
+ fontFamily: "monospace",
537
+ fontSize: "0.85em",
538
+ }}
539
+ >
540
+ {JSON.stringify(value).slice(0, 30)}
541
+ {JSON.stringify(value).length > 30 ? "..." : ""}
542
+ </span>
543
+ ) : field.type === "date" ? (
544
+ <span
545
+ style={{
546
+ color: "var(--opaca-text)",
547
+ opacity: 0.9,
548
+ }}
549
+ >
550
+ {formatTableDate(value as string)}
551
+ </span>
552
+ ) : typeof value === "object" && value !== null ? (
553
+ <span
554
+ className="opaca-badge"
555
+ style={{
556
+ background: "rgba(255, 255, 255, 0.05)",
557
+ border: "1px solid var(--opaca-border)",
558
+ color: "var(--opaca-text-dim)",
559
+ }}
560
+ >
561
+ [Object]
562
+ </span>
563
+ ) : (
564
+ <span
565
+ style={{
566
+ color: "var(--opaca-text)",
567
+ opacity: 0.9,
568
+ }}
569
+ >
570
+ {field.type === "richtext"
571
+ ? lexicalToPlainText(value as string).slice(0, 30) +
572
+ (lexicalToPlainText(value as string).length > 30 ? "..." : "")
573
+ : typeof value === "string" && value.length > 30
574
+ ? `${value.slice(0, 30)}...`
575
+ : String(value ?? "-")}
576
+ </span>
577
+ )}
578
+ </TableCell>
579
+ );
580
+ })}
581
+ {collection.timestamps && (
582
+ <TableCell
583
+ style={{
584
+ color: "var(--opaca-text-muted)",
585
+ fontSize: "12px",
586
+ }}
587
+ >
588
+ {formatTableDate(doc[createdAtField] as any, true)}
589
+ </TableCell>
590
+ )}
591
+ <TableCell>
592
+ <Link
593
+ href={`/admin/collections/${collection.slug}/${doc.id}`}
594
+ className="opaca-btn opaca-btn-outline"
595
+ style={{ padding: "0.4rem" }}
596
+ title="Edit Document"
597
+ >
598
+ <Edit2 size={14} />
599
+ </Link>
600
+ </TableCell>
601
+ </TableRow>
602
+ ))}
603
+ </TableBody>
604
+ </Table>
605
+
606
+ {data && (
607
+ <div
608
+ className="opaca-pagination"
609
+ style={{
610
+ display: "flex",
611
+ justifyContent: "space-between",
612
+ alignItems: "center",
613
+ marginTop: "1rem",
614
+ }}
615
+ >
616
+ <div
617
+ className="opaca-pagination-info"
618
+ style={{
619
+ color: "var(--opaca-text-muted)",
620
+ fontSize: "0.875rem",
621
+ }}
622
+ >
623
+ Showing {(data.page - 1) * data.limit + 1} to{" "}
624
+ {Math.min(data.page * data.limit, data.totalDocs)} of {data.totalDocs} documents
625
+ </div>
626
+
627
+ <div
628
+ style={{
629
+ display: "flex",
630
+ alignItems: "center",
631
+ gap: "1.5rem",
632
+ }}
633
+ >
634
+ <div
635
+ style={{
636
+ display: "flex",
637
+ alignItems: "center",
638
+ gap: "0.5rem",
639
+ }}
640
+ >
641
+ <span
642
+ style={{
643
+ fontSize: "0.875rem",
644
+ color: "var(--opaca-text-muted)",
645
+ }}
646
+ >
647
+ Rows per page:
648
+ </span>
649
+ <select
650
+ value={limit}
651
+ onChange={(e) => {
652
+ setLimit(Number(e.target.value));
653
+ setPage(1);
654
+ }}
655
+ className="opaca-input"
656
+ style={{
657
+ padding: "0.25rem 0.5rem",
658
+ height: "auto",
659
+ width: "auto",
660
+ }}
661
+ >
662
+ <option value="10">10</option>
663
+ <option value="25">25</option>
664
+ <option value="50">50</option>
665
+ <option value="100">100</option>
666
+ </select>
667
+ </div>
668
+
669
+ <div
670
+ className="opaca-pagination-actions"
671
+ style={{ display: "flex", gap: "0.5rem" }}
672
+ >
673
+ <button
674
+ type="button"
675
+ className="opaca-btn opaca-btn-outline"
676
+ disabled={!data.hasPrevPage || loading}
677
+ onClick={() => setPage(page - 1)}
678
+ style={{ padding: "0.4rem" }}
679
+ >
680
+ <ChevronLeft size={16} />
681
+ </button>
682
+ <button
683
+ type="button"
684
+ className="opaca-btn opaca-btn-outline"
685
+ disabled={!data.hasNextPage || loading}
686
+ onClick={() => setPage(page + 1)}
687
+ style={{ padding: "0.4rem" }}
688
+ >
689
+ <ChevronRight size={16} />
690
+ </button>
691
+ </div>
692
+ </div>
693
+ </div>
694
+ )}
695
+ </>
696
+ ) : !loading ? (
697
+ <div className="opaca-empty">
698
+ <div className="opaca-empty-icon">
699
+ <Inbox size={32} />
700
+ </div>
701
+ <h3 style={{ fontSize: "1.125rem", fontWeight: "600" }}>No documents found</h3>
702
+ <p className="opaca-subtitle" style={{ maxWidth: "300px" }}>
703
+ This collection is currently empty. Start by creating your first document.
704
+ </p>
705
+ <Link
706
+ href={`/admin/collections/${collection.slug}/create`}
707
+ className="opaca-btn opaca-btn-primary"
708
+ style={{ textDecoration: "none", marginTop: "0.5rem" }}
709
+ >
710
+ <Plus size={18} />
711
+ Create First {collection.label || collection.slug}
712
+ </Link>
713
+ </div>
714
+ ) : (
715
+ <div style={{ height: "200px" }} /> /* Placeholder while loading empty state */
716
+ )}
717
+ </div>
718
+ </div>
719
+ );
720
+ }