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,211 @@
1
+ import { CodeNode } from "@lexical/code";
2
+ import { AutoLinkNode, LinkNode } from "@lexical/link";
3
+ import { ListItemNode, ListNode } from "@lexical/list";
4
+ import { MarkNode } from "@lexical/mark";
5
+ import { LexicalComposer } from "@lexical/react/LexicalComposer";
6
+ import { ContentEditable } from "@lexical/react/LexicalContentEditable";
7
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
8
+ import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
9
+ import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
10
+ import { ListPlugin } from "@lexical/react/LexicalListPlugin";
11
+ import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
12
+ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
13
+ import { HeadingNode, QuoteNode } from "@lexical/rich-text";
14
+ import type { EditorState } from "lexical";
15
+ import { useState } from "react";
16
+ import { ImageNode } from "./nodes/ImageNode";
17
+ import { ComponentPickerPlugin } from "./plugins/ComponentPickerPlugin";
18
+ import { EditableSyncPlugin } from "./plugins/EditableSyncPlugin";
19
+ import { NotionToolbarPlugin } from "./plugins/NotionToolbarPlugin";
20
+ import { SimpleToolbarPlugin } from "./plugins/SimpleToolbarPlugin";
21
+ import { ValueSyncPlugin } from "./plugins/ValueSyncPlugin";
22
+ import "../../../styles/lexical.scss";
23
+
24
+ export interface RichTextEditorProps {
25
+ value: string;
26
+ onChange: (value: string) => void;
27
+ defaultMode?: "simple" | "notion";
28
+ disabled?: boolean;
29
+ readOnly?: boolean;
30
+ }
31
+
32
+ const theme = {
33
+ ltr: "ltr",
34
+ rtl: "rtl",
35
+ placeholder: "editor-placeholder",
36
+ paragraph: "editor-paragraph",
37
+ quote: "editor-quote",
38
+ heading: {
39
+ h1: "editor-heading-h1",
40
+ h2: "editor-heading-h2",
41
+ h3: "editor-heading-h3",
42
+ h4: "editor-heading-h4",
43
+ h5: "editor-heading-h5",
44
+ h6: "editor-heading-h6",
45
+ },
46
+ list: {
47
+ nested: {
48
+ listitem: "editor-nested-listitem",
49
+ },
50
+ ol: "editor-list-ol",
51
+ ul: "editor-list-ul",
52
+ listitem: "editor-listitem",
53
+ listitemChecked: "editor-listitem-checked",
54
+ listitemUnchecked: "editor-listitem-unchecked",
55
+ },
56
+ image: "editor-image",
57
+ link: "editor-link",
58
+ text: {
59
+ bold: "editor-text-bold",
60
+ italic: "editor-text-italic",
61
+ overflowed: "editor-text-overflowed",
62
+ hashtag: "editor-text-hashtag",
63
+ underline: "editor-text-underline",
64
+ strikethrough: "editor-text-strikethrough",
65
+ underlineStrikethrough: "editor-text-underlineStrikethrough",
66
+ code: "editor-text-code",
67
+ },
68
+ code: "editor-code",
69
+ codeHighlight: {
70
+ atrule: "editor-tokenAttr",
71
+ attr: "editor-tokenAttr",
72
+ boolean: "editor-tokenProperty",
73
+ builtin: "editor-tokenSelector",
74
+ cdata: "editor-tokenComment",
75
+ char: "editor-tokenSelector",
76
+ class: "editor-tokenFunction",
77
+ "class-name": "editor-tokenFunction",
78
+ comment: "editor-tokenComment",
79
+ constant: "editor-tokenProperty",
80
+ deleted: "editor-tokenProperty",
81
+ doctype: "editor-tokenComment",
82
+ entity: "editor-tokenOperator",
83
+ function: "editor-tokenFunction",
84
+ important: "editor-tokenVariable",
85
+ inserted: "editor-tokenSelector",
86
+ keyword: "editor-tokenAttr",
87
+ namespace: "editor-tokenVariable",
88
+ number: "editor-tokenProperty",
89
+ operator: "editor-tokenOperator",
90
+ prolog: "editor-tokenComment",
91
+ property: "editor-tokenProperty",
92
+ punctuation: "editor-tokenPunctuation",
93
+ regex: "editor-tokenVariable",
94
+ selector: "editor-tokenSelector",
95
+ string: "editor-tokenSelector",
96
+ symbol: "editor-tokenProperty",
97
+ tag: "editor-tokenProperty",
98
+ url: "editor-tokenOperator",
99
+ variable: "editor-tokenVariable",
100
+ },
101
+ };
102
+
103
+ function Placeholder() {
104
+ return (
105
+ <div className="opaca-lexical-placeholder">Press "/" for commands, or start typing...</div>
106
+ );
107
+ }
108
+
109
+ export const normalizeEditorState = (value: unknown): string | undefined => {
110
+ if (!value) return undefined;
111
+
112
+ // already string
113
+ if (typeof value === "string") {
114
+ const trimmed = value.trim();
115
+ return trimmed.startsWith("{") ? trimmed : undefined;
116
+ }
117
+
118
+ // object → stringify
119
+ if (typeof value === "object") {
120
+ try {
121
+ return JSON.stringify(value);
122
+ } catch {
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ return undefined;
128
+ };
129
+
130
+ export function RichTextEditor({
131
+ value,
132
+ onChange,
133
+ defaultMode = "simple",
134
+ disabled,
135
+ readOnly,
136
+ }: RichTextEditorProps) {
137
+ const [mode, setMode] = useState<"simple" | "notion">(defaultMode);
138
+ const isEditable = !disabled && !readOnly;
139
+
140
+ const initialConfig = {
141
+ namespace: "OpacaLexical",
142
+ theme,
143
+ editable: isEditable,
144
+ nodes: [
145
+ HeadingNode,
146
+ ListNode,
147
+ ListItemNode,
148
+ QuoteNode,
149
+ CodeNode,
150
+ LinkNode,
151
+ AutoLinkNode,
152
+ MarkNode,
153
+ ImageNode,
154
+ ],
155
+ onError: (error: Error) => {
156
+ console.error(error);
157
+ },
158
+ editorState: normalizeEditorState(value),
159
+ };
160
+
161
+ const handleOnChange = (editorState: EditorState) => {
162
+ // Only call onChange if we have actually serialized new content, otherwise we'll create infinite loops
163
+ // In Lexical, we generally stringify the JSON
164
+ const jsonString = JSON.stringify(editorState.toJSON());
165
+ onChange(jsonString);
166
+ };
167
+
168
+ return (
169
+ <div className="opaca-lexical-wrapper">
170
+ {isEditable && (
171
+ <div className="opaca-lexical-modes">
172
+ <button
173
+ type="button"
174
+ className={`opaca-btn ${mode === "simple" ? "opaca-btn-primary" : "opaca-btn-outline"}`}
175
+ onClick={() => setMode("simple")}
176
+ >
177
+ Simple
178
+ </button>
179
+ <button
180
+ type="button"
181
+ className={`opaca-btn ${mode === "notion" ? "opaca-btn-primary" : "opaca-btn-outline"}`}
182
+ onClick={() => setMode("notion")}
183
+ >
184
+ Notion-like
185
+ </button>
186
+ </div>
187
+ )}
188
+
189
+ <div className={`opaca-lexical-container mode-${mode} ${!isEditable ? "is-readonly" : ""}`}>
190
+ <LexicalComposer initialConfig={initialConfig}>
191
+ {mode === "simple" && isEditable && <SimpleToolbarPlugin />}
192
+ {mode === "notion" && isEditable && <NotionToolbarPlugin />}
193
+ <div className="opaca-lexical-editor-inner">
194
+ <RichTextPlugin
195
+ contentEditable={<ContentEditable className="opaca-lexical-content" />}
196
+ placeholder={<Placeholder />}
197
+ ErrorBoundary={LexicalErrorBoundary as any}
198
+ />
199
+ <OnChangePlugin onChange={handleOnChange} ignoreSelectionChange />
200
+ <ValueSyncPlugin value={value} />
201
+ <EditableSyncPlugin isEditable={isEditable} />
202
+ <HistoryPlugin />
203
+ <ListPlugin />
204
+ <LinkPlugin />
205
+ <ComponentPickerPlugin />
206
+ </div>
207
+ </LexicalComposer>
208
+ </div>
209
+ </div>
210
+ );
211
+ }
@@ -0,0 +1,142 @@
1
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
+ import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection";
3
+ import { $getNodeByKey } from "lexical";
4
+ import { Trash2 } from "lucide-react";
5
+ import type * as React from "react";
6
+ import { useCallback, useEffect, useRef, useState } from "react";
7
+
8
+ // To avoid circular dependency, we'll check for the type property directly if needed
9
+ // or we can just trust the nodeKey refers to an ImageNode.
10
+
11
+ export default function ImageComponent({
12
+ src,
13
+ altText,
14
+ nodeKey,
15
+ width,
16
+ height,
17
+ }: {
18
+ src: string;
19
+ altText: string;
20
+ nodeKey: string;
21
+ width?: number;
22
+ height?: number;
23
+ }) {
24
+ const [editor] = useLexicalComposerContext();
25
+ const [isSelected, setSelected] = useLexicalNodeSelection(nodeKey);
26
+ const [isResizing, setIsResizing] = useState(false);
27
+ const imageRef = useRef<HTMLImageElement>(null);
28
+ const [resizingWidth, setResizingWidth] = useState<number | undefined>(width);
29
+
30
+ const onDelete = useCallback(() => {
31
+ editor.update(() => {
32
+ const node = $getNodeByKey(nodeKey);
33
+ if (node) {
34
+ node.remove();
35
+ }
36
+ });
37
+ }, [editor, nodeKey]);
38
+
39
+ const onResizeStart = (e: React.MouseEvent) => {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ setIsResizing(true);
43
+ };
44
+
45
+ useEffect(() => {
46
+ if (!isResizing) return;
47
+
48
+ const onMouseMove = (e: MouseEvent) => {
49
+ if (imageRef.current) {
50
+ const { left } = imageRef.current.getBoundingClientRect();
51
+ const newWidth = Math.max(50, e.clientX - left);
52
+ setResizingWidth(newWidth);
53
+ }
54
+ };
55
+
56
+ const onMouseUp = () => {
57
+ setIsResizing(false);
58
+ if (resizingWidth !== undefined) {
59
+ editor.update(() => {
60
+ const node = $getNodeByKey(nodeKey);
61
+ // @ts-expect-error - node has setWidth if it is an ImageNode
62
+ if (node && typeof node.setWidth === "function") {
63
+ // @ts-expect-error
64
+ node.setWidth(resizingWidth);
65
+ }
66
+ });
67
+ }
68
+ };
69
+
70
+ document.addEventListener("mousemove", onMouseMove);
71
+ document.addEventListener("mouseup", onMouseUp);
72
+
73
+ return () => {
74
+ document.removeEventListener("mousemove", onMouseMove);
75
+ document.removeEventListener("mouseup", onMouseUp);
76
+ };
77
+ }, [isResizing, resizingWidth, editor, nodeKey]);
78
+
79
+ useEffect(() => {
80
+ setResizingWidth(width);
81
+ }, [width]);
82
+
83
+ return (
84
+ <div
85
+ role="button"
86
+ tabIndex={-1}
87
+ className={`editor-image-wrapper ${isSelected ? "is-selected" : ""}`}
88
+ onClick={(e) => {
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+ setSelected(!isSelected);
92
+ }}
93
+ onKeyDown={(e) => {
94
+ if (e.key === "Enter" || e.key === " ") {
95
+ e.preventDefault();
96
+ setSelected(!isSelected);
97
+ }
98
+ }}
99
+ style={{
100
+ display: "inline-block",
101
+ position: "relative",
102
+ cursor: "default",
103
+ lineHeight: 0,
104
+ zIndex: isSelected ? 10 : 1,
105
+ }}
106
+ >
107
+ <img
108
+ ref={imageRef}
109
+ src={src}
110
+ alt={altText}
111
+ style={{
112
+ width: resizingWidth || (width ? `${width}px` : "auto"),
113
+ height: height ? `${height}px` : "auto",
114
+ maxWidth: "100%",
115
+ display: "block",
116
+ }}
117
+ className="editor-image-img"
118
+ />
119
+ {isSelected && (
120
+ <>
121
+ <button
122
+ type="button"
123
+ className="editor-image-resizer"
124
+ onMouseDown={onResizeStart}
125
+ aria-label="Resize image"
126
+ />
127
+ <button
128
+ type="button"
129
+ className="editor-image-delete"
130
+ onClick={(e) => {
131
+ e.stopPropagation();
132
+ onDelete();
133
+ }}
134
+ aria-label="Delete image"
135
+ >
136
+ <Trash2 size={14} />
137
+ </button>
138
+ </>
139
+ )}
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,95 @@
1
+ import { DecoratorNode, type NodeKey, type SerializedLexicalNode } from "lexical";
2
+ import type React from "react";
3
+ import ImageComponent from "./ImageComponent";
4
+
5
+ export interface SerializedImageNode extends SerializedLexicalNode {
6
+ src: string;
7
+ altText: string;
8
+ height?: number;
9
+ width?: number;
10
+ type: "image";
11
+ }
12
+
13
+ export class ImageNode extends DecoratorNode<React.ReactElement> {
14
+ __src: string;
15
+ __altText: string;
16
+ __height?: number;
17
+ __width?: number;
18
+
19
+ static override getType(): string {
20
+ return "image";
21
+ }
22
+
23
+ static override clone(node: ImageNode): ImageNode {
24
+ return new ImageNode(node.__src, node.__altText, node.__height, node.__width, node.__key);
25
+ }
26
+
27
+ constructor(src: string, altText: string, height?: number, width?: number, key?: NodeKey) {
28
+ super(key);
29
+ this.__src = src;
30
+ this.__altText = altText;
31
+ this.__height = height;
32
+ this.__width = width;
33
+ }
34
+
35
+ override exportJSON(): SerializedImageNode {
36
+ return {
37
+ altText: this.__altText,
38
+ height: this.__height,
39
+ src: this.__src,
40
+ type: "image",
41
+ version: 1,
42
+ width: this.__width,
43
+ };
44
+ }
45
+
46
+ static override importJSON(serializedNode: SerializedImageNode): ImageNode {
47
+ const { altText, height, width, src } = serializedNode;
48
+ return $createImageNode(src, altText, height, width);
49
+ }
50
+
51
+ override createDOM(_config: unknown): HTMLElement {
52
+ const span = document.createElement("span");
53
+ span.className = "editor-image";
54
+ return span;
55
+ }
56
+
57
+ override updateDOM(): false {
58
+ return false;
59
+ }
60
+
61
+ setWidth(width: number): void {
62
+ const writable = this.getWritable();
63
+ writable.__width = width;
64
+ }
65
+
66
+ setHeight(height: number): void {
67
+ const writable = this.getWritable();
68
+ writable.__height = height;
69
+ }
70
+
71
+ override decorate(): React.ReactElement {
72
+ return (
73
+ <ImageComponent
74
+ src={this.__src}
75
+ altText={this.__altText}
76
+ nodeKey={this.__key}
77
+ width={this.__width}
78
+ height={this.__height}
79
+ />
80
+ );
81
+ }
82
+ }
83
+
84
+ export function $createImageNode(
85
+ src: string,
86
+ altText: string,
87
+ height?: number,
88
+ width?: number,
89
+ ): ImageNode {
90
+ return new ImageNode(src, altText, height, width);
91
+ }
92
+
93
+ export function $isImageNode(node: unknown): node is ImageNode {
94
+ return node instanceof ImageNode;
95
+ }
@@ -0,0 +1,226 @@
1
+ import { $createCodeNode } from "@lexical/code";
2
+ import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
3
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
4
+ import {
5
+ LexicalTypeaheadMenuPlugin,
6
+ MenuOption,
7
+ useBasicTypeaheadTriggerMatch,
8
+ } from "@lexical/react/LexicalTypeaheadMenuPlugin";
9
+ import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
10
+ import { $setBlocksType } from "@lexical/selection";
11
+ import { $getSelection, $insertNodes, $isRangeSelection, type TextNode } from "lexical";
12
+ import {
13
+ Code,
14
+ Heading1,
15
+ Heading2,
16
+ Image as ImageIcon,
17
+ List,
18
+ ListOrdered,
19
+ Quote,
20
+ Type,
21
+ } from "lucide-react";
22
+ import type * as React from "react";
23
+ import { useCallback, useMemo, useState } from "react";
24
+ import * as ReactDOM from "react-dom";
25
+ import { getCurrentBaseURL } from "../../../../../api-client";
26
+ import { AssetManagerModal } from "../../../media/AssetManagerModal";
27
+ import { ScrollArea } from "../../../ui/scroll-area";
28
+ import { $createImageNode } from "../nodes/ImageNode";
29
+
30
+ class ComponentPickerOption extends MenuOption {
31
+ title: string;
32
+ icon: React.ReactNode;
33
+ description: string;
34
+ onSelect: (queryString: string) => void;
35
+
36
+ constructor(
37
+ title: string,
38
+ options: {
39
+ icon: React.ReactNode;
40
+ description: string;
41
+ onSelect: (queryString: string) => void;
42
+ },
43
+ ) {
44
+ super(title);
45
+ this.title = title;
46
+ this.icon = options.icon;
47
+ this.description = options.description;
48
+ this.onSelect = options.onSelect;
49
+ }
50
+ }
51
+
52
+ export function ComponentPickerPlugin() {
53
+ const [editor] = useLexicalComposerContext();
54
+ const [queryString, setQueryString] = useState<string | null>(null);
55
+ const [isModalOpen, setIsModalOpen] = useState(false);
56
+
57
+ const checkForTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
58
+ minLength: 0,
59
+ });
60
+
61
+ const handleInsertImage = (asset: any) => {
62
+ const url = asset.url || `${getCurrentBaseURL()}/api/assets/${asset.id || asset.assetId}/view`;
63
+ editor.update(() => {
64
+ const imageNode = $createImageNode(url, asset.filename || "Image");
65
+ $insertNodes([imageNode]);
66
+ });
67
+ setIsModalOpen(false);
68
+ };
69
+
70
+ const options = useMemo(() => {
71
+ const baseOptions = [
72
+ new ComponentPickerOption("Paragraph", {
73
+ icon: <Type size={18} />,
74
+ description: "Just start typing with plain text.",
75
+ onSelect: () => {
76
+ editor.update(() => {
77
+ const selection = $getSelection();
78
+ if ($isRangeSelection(selection)) {
79
+ $setBlocksType(selection, () => $createHeadingNode("h1")); // Default logic often involves reset, simplify
80
+ $setBlocksType(selection, () => $createHeadingNode("h1")); // placeholder
81
+ }
82
+ });
83
+ },
84
+ }),
85
+ new ComponentPickerOption("Heading 1", {
86
+ icon: <Heading1 size={18} />,
87
+ description: "Large section heading.",
88
+ onSelect: () => {
89
+ editor.update(() => {
90
+ const selection = $getSelection();
91
+ if ($isRangeSelection(selection)) {
92
+ $setBlocksType(selection, () => $createHeadingNode("h1"));
93
+ }
94
+ });
95
+ },
96
+ }),
97
+ new ComponentPickerOption("Heading 2", {
98
+ icon: <Heading2 size={18} />,
99
+ description: "Medium section heading.",
100
+ onSelect: () => {
101
+ editor.update(() => {
102
+ const selection = $getSelection();
103
+ if ($isRangeSelection(selection)) {
104
+ $setBlocksType(selection, () => $createHeadingNode("h2"));
105
+ }
106
+ });
107
+ },
108
+ }),
109
+ new ComponentPickerOption("Bullet List", {
110
+ icon: <List size={18} />,
111
+ description: "Create a simple bullet list.",
112
+ onSelect: () => {
113
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
114
+ },
115
+ }),
116
+ new ComponentPickerOption("Numbered List", {
117
+ icon: <ListOrdered size={18} />,
118
+ description: "Create a list with numbering.",
119
+ onSelect: () => {
120
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
121
+ },
122
+ }),
123
+ new ComponentPickerOption("Quote", {
124
+ icon: <Quote size={18} />,
125
+ description: "Capture a quotation.",
126
+ onSelect: () => {
127
+ editor.update(() => {
128
+ const selection = $getSelection();
129
+ if ($isRangeSelection(selection)) {
130
+ $setBlocksType(selection, () => $createQuoteNode());
131
+ }
132
+ });
133
+ },
134
+ }),
135
+ new ComponentPickerOption("Code Block", {
136
+ icon: <Code size={18} />,
137
+ description: "Write code snippets.",
138
+ onSelect: () => {
139
+ editor.update(() => {
140
+ const selection = $getSelection();
141
+ if ($isRangeSelection(selection)) {
142
+ $setBlocksType(selection, () => $createCodeNode());
143
+ }
144
+ });
145
+ },
146
+ }),
147
+ new ComponentPickerOption("Image", {
148
+ icon: <ImageIcon size={18} />,
149
+ description: "Insert an image from media library.",
150
+ onSelect: () => {
151
+ setIsModalOpen(true);
152
+ },
153
+ }),
154
+ ];
155
+
156
+ if (!queryString) return baseOptions;
157
+
158
+ return baseOptions.filter((option) =>
159
+ option.title.toLowerCase().includes(queryString.toLowerCase()),
160
+ );
161
+ }, [editor, queryString]);
162
+
163
+ const onSelectOption = useCallback(
164
+ (
165
+ selectedOption: ComponentPickerOption,
166
+ nodeToRemove: TextNode | null,
167
+ closeMenu: () => void,
168
+ ) => {
169
+ editor.update(() => {
170
+ if (nodeToRemove) {
171
+ nodeToRemove.remove();
172
+ }
173
+ selectedOption.onSelect(queryString || "");
174
+ closeMenu();
175
+ });
176
+ },
177
+ [editor, queryString],
178
+ );
179
+
180
+ return (
181
+ <>
182
+ <LexicalTypeaheadMenuPlugin<ComponentPickerOption>
183
+ onQueryChange={setQueryString}
184
+ onSelectOption={onSelectOption}
185
+ triggerFn={checkForTriggerMatch}
186
+ options={options}
187
+ menuRenderFn={(
188
+ anchorElementRef,
189
+ { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
190
+ ) =>
191
+ anchorElementRef.current && options.length > 0
192
+ ? ReactDOM.createPortal(
193
+ <ScrollArea className="opaca-slash-menu" maxHeight="300px">
194
+ {options.map((option, i) => (
195
+ <button
196
+ key={option.key}
197
+ className={`opaca-slash-menu-item ${
198
+ selectedIndex === i ? "is-selected" : ""
199
+ }`}
200
+ onClick={() => selectOptionAndCleanUp(option)}
201
+ onMouseEnter={() => setHighlightedIndex(i)}
202
+ >
203
+ <div className="opaca-slash-menu-icon">{option.icon}</div>
204
+ <div className="opaca-slash-menu-text">
205
+ <span className="opaca-slash-menu-title">{option.title}</span>
206
+ <span className="opaca-slash-menu-desc">{option.description}</span>
207
+ </div>
208
+ </button>
209
+ ))}
210
+ </ScrollArea>,
211
+ anchorElementRef.current,
212
+ )
213
+ : null
214
+ }
215
+ />
216
+
217
+ {isModalOpen && (
218
+ <AssetManagerModal
219
+ bucket={"default"}
220
+ onClose={() => setIsModalOpen(false)}
221
+ onSelect={handleInsertImage}
222
+ />
223
+ )}
224
+ </>
225
+ );
226
+ }
@@ -0,0 +1,16 @@
1
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
+ import { useEffect } from "react";
3
+
4
+ interface EditableSyncPluginProps {
5
+ isEditable: boolean;
6
+ }
7
+
8
+ export function EditableSyncPlugin({ isEditable }: EditableSyncPluginProps) {
9
+ const [editor] = useLexicalComposerContext();
10
+
11
+ useEffect(() => {
12
+ editor.setEditable(isEditable);
13
+ }, [editor, isEditable]);
14
+
15
+ return null;
16
+ }