create-mithaq 0.0.0-stage → 0.1.1

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 (357) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/index.js +127 -0
  5. package/package.json +19 -4
  6. package/template/apps/asas-platform/components.json +25 -0
  7. package/template/apps/asas-platform/gitignore +3 -0
  8. package/template/apps/asas-platform/package.json +55 -0
  9. package/template/apps/asas-platform/public/favicon.svg +1 -0
  10. package/template/apps/asas-platform/src/api.ts +147 -0
  11. package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
  12. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
  13. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
  14. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
  15. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
  16. package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
  17. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
  18. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
  19. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
  20. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
  21. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
  22. package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
  23. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
  24. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
  25. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
  26. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
  27. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
  28. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
  29. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
  30. package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
  31. package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
  32. package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
  33. package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
  34. package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
  35. package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
  36. package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
  37. package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
  38. package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
  39. package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
  40. package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
  41. package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
  42. package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
  43. package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
  44. package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
  45. package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
  46. package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
  47. package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
  48. package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
  49. package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
  50. package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
  51. package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
  52. package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
  53. package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
  54. package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
  55. package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
  56. package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
  57. package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
  58. package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
  59. package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
  60. package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
  61. package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
  62. package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
  63. package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
  64. package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
  65. package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
  66. package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
  67. package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
  68. package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
  69. package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
  70. package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
  71. package/template/apps/asas-platform/src/copy.ts +1 -0
  72. package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
  73. package/template/apps/asas-platform/src/lib/utils.ts +1 -0
  74. package/template/apps/asas-platform/src/links.ts +38 -0
  75. package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
  76. package/template/apps/asas-platform/src/redirect.ts +1 -0
  77. package/template/apps/asas-platform/src/request-headers.ts +11 -0
  78. package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
  79. package/template/apps/asas-platform/src/router.tsx +7 -0
  80. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
  81. package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
  82. package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
  83. package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
  84. package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
  85. package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
  86. package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
  87. package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
  88. package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
  89. package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
  90. package/template/apps/asas-platform/src/screen-form.tsx +34 -0
  91. package/template/apps/asas-platform/src/screen-list.tsx +139 -0
  92. package/template/apps/asas-platform/src/screen-names.ts +10 -0
  93. package/template/apps/asas-platform/src/screens.tsx +868 -0
  94. package/template/apps/asas-platform/src/server.ts +20 -0
  95. package/template/apps/asas-platform/src/slide.ts +22 -0
  96. package/template/apps/asas-platform/src/start.ts +42 -0
  97. package/template/apps/asas-platform/src/styles.css +200 -0
  98. package/template/apps/asas-platform/src/theme.ts +2 -0
  99. package/template/apps/asas-platform/tsconfig.json +19 -0
  100. package/template/apps/asas-platform/vite.config.ts +26 -0
  101. package/template/apps/asas-tanstack/components.json +25 -0
  102. package/template/apps/asas-tanstack/gitignore +3 -0
  103. package/template/apps/asas-tanstack/package.json +56 -0
  104. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  105. package/template/apps/asas-tanstack/src/api.ts +147 -0
  106. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  107. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +112 -0
  108. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
  109. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
  110. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
  111. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
  112. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
  113. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
  114. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
  115. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
  116. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
  117. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
  118. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
  119. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
  120. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
  121. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
  122. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
  123. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
  124. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -0
  125. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  126. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  127. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  128. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  129. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  130. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  131. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  132. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  133. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  134. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  135. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  136. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  137. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  138. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  139. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  140. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  141. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +215 -0
  142. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  143. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  144. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  145. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  146. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  147. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  148. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  149. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  150. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  151. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  152. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  153. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  154. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  155. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  156. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  157. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  158. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  159. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  160. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  161. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  162. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  163. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  164. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  165. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  166. package/template/apps/asas-tanstack/src/copy.ts +870 -0
  167. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  168. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  169. package/template/apps/asas-tanstack/src/links.ts +38 -0
  170. package/template/apps/asas-tanstack/src/locale-switch.tsx +26 -0
  171. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  172. package/template/apps/asas-tanstack/src/redirect.ts +20 -0
  173. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  174. package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
  175. package/template/apps/asas-tanstack/src/router.tsx +7 -0
  176. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
  177. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
  178. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -0
  179. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  180. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  181. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  182. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +4 -0
  183. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
  184. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -0
  185. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  186. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  187. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  188. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  189. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  190. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +101 -0
  191. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  192. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  193. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +109 -0
  194. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
  195. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
  196. package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
  197. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
  198. package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
  199. package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
  200. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  201. package/template/apps/asas-tanstack/src/screens.tsx +868 -0
  202. package/template/apps/asas-tanstack/src/server.ts +16 -0
  203. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  204. package/template/apps/asas-tanstack/src/start.ts +57 -0
  205. package/template/apps/asas-tanstack/src/styles.css +159 -0
  206. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  207. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  208. package/template/apps/asas-tanstack/vite.config.ts +28 -0
  209. package/template/package.json +16 -0
  210. package/template/templates/asas/.env.example +16 -0
  211. package/template/templates/asas/components.json +22 -0
  212. package/template/templates/asas/mithaq.hints.json +25 -0
  213. package/template/templates/asas/package.json +38 -0
  214. package/template/templates/asas/src/asas.ts +93 -0
  215. package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
  216. package/template/templates/asas/src/audit-log/sink.ts +89 -0
  217. package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
  218. package/template/templates/asas/src/courses/courses.ts +612 -0
  219. package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
  220. package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
  221. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
  222. package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
  223. package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
  224. package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
  225. package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
  226. package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
  227. package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
  228. package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
  229. package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
  230. package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
  231. package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
  232. package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
  233. package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
  234. package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
  235. package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
  236. package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
  237. package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
  238. package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
  239. package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
  240. package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
  241. package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
  242. package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
  243. package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
  244. package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
  245. package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
  246. package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
  247. package/template/templates/asas/src/local-users/local-users.ts +143 -0
  248. package/template/templates/asas/src/media/media.ts +137 -0
  249. package/template/templates/asas/src/media/ui/preview.tsx +143 -0
  250. package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
  251. package/template/templates/asas/src/messages/account.json +106 -0
  252. package/template/templates/asas/src/messages/auditLog.json +186 -0
  253. package/template/templates/asas/src/messages/courses.json +270 -0
  254. package/template/templates/asas/src/messages/enrollments.json +42 -0
  255. package/template/templates/asas/src/messages/invitation.json +66 -0
  256. package/template/templates/asas/src/messages/localUsers.json +42 -0
  257. package/template/templates/asas/src/messages/media.json +66 -0
  258. package/template/templates/asas/src/messages/member.json +42 -0
  259. package/template/templates/asas/src/messages/memberRoles.json +42 -0
  260. package/template/templates/asas/src/messages/mithaq.json +170 -0
  261. package/template/templates/asas/src/messages/organization.json +50 -0
  262. package/template/templates/asas/src/messages/organizationRole.json +50 -0
  263. package/template/templates/asas/src/messages/roles.json +58 -0
  264. package/template/templates/asas/src/messages/session.json +82 -0
  265. package/template/templates/asas/src/messages/sessions.json +106 -0
  266. package/template/templates/asas/src/messages/settings.json +66 -0
  267. package/template/templates/asas/src/messages/tags.json +38 -0
  268. package/template/templates/asas/src/messages/user.json +90 -0
  269. package/template/templates/asas/src/messages/verification.json +50 -0
  270. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  271. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  272. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  273. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  274. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  275. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  276. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  277. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  278. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  279. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  280. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  281. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  282. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  283. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  284. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  285. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  286. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  287. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  288. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  289. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  290. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  291. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  292. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  293. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  294. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  295. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  296. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  297. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  298. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  299. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  300. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  301. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  302. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  303. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  304. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  305. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  306. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  307. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  308. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  309. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  310. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  311. package/template/templates/asas/src/migrations/schema.ts +29 -0
  312. package/template/templates/asas/src/mithaq.ts +406 -0
  313. package/template/templates/asas/src/pages.ts +74 -0
  314. package/template/templates/asas/src/platform-http.ts +96 -0
  315. package/template/templates/asas/src/rich-text-nodes.ts +11 -0
  316. package/template/templates/asas/src/roles/grants.ts +47 -0
  317. package/template/templates/asas/src/roles/roles.ts +184 -0
  318. package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
  319. package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
  320. package/template/templates/asas/src/roles.ts +16 -0
  321. package/template/templates/asas/src/seed.ts +325 -0
  322. package/template/templates/asas/src/sessions/sessions.ts +155 -0
  323. package/template/templates/asas/src/settings/settings.ts +69 -0
  324. package/template/templates/asas/src/statements.ts +11 -0
  325. package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
  326. package/template/templates/asas/src/storage/defaults.ts +156 -0
  327. package/template/templates/asas/src/storage/index.ts +95 -0
  328. package/template/templates/asas/src/storage/local.ts +143 -0
  329. package/template/templates/asas/src/storage/r2.ts +5 -0
  330. package/template/templates/asas/src/storage/s3.ts +91 -0
  331. package/template/templates/asas/src/storage/uploadthing.ts +59 -0
  332. package/template/templates/asas/src/tags/tags.ts +80 -0
  333. package/template/templates/asas/src/tenant-access.ts +221 -0
  334. package/template/templates/asas/src/tenant-roles.ts +262 -0
  335. package/template/templates/asas/src/ui/app-shell.tsx +260 -0
  336. package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
  337. package/template/templates/asas/src/ui/record-page.tsx +50 -0
  338. package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
  339. package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
  340. package/template/templates/asas/src/ui/session-labels.tsx +36 -0
  341. package/template/templates/asas/src/ui/table-page.tsx +9 -0
  342. package/template/templates/asas/src/ui/top-shell.tsx +60 -0
  343. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
  344. package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
  345. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  346. package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
  347. package/template/templates/asas/src/ui-platform/home.tsx +63 -0
  348. package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
  349. package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
  350. package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
  351. package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
  352. package/template/templates/asas/src/ui-portal/home.tsx +77 -0
  353. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  354. package/template/templates/auth/package.json +14 -0
  355. package/template/templates/auth/src/index.ts +690 -0
  356. package/template/templates/auth/src/platform-roles.ts +87 -0
  357. package/README.md +0 -3
@@ -0,0 +1,45 @@
1
+ // What each screen draws of the generated `user` kayan, which `mithaq generate
2
+ // auth` imports into `./user.ts` because this file sits beside it, spreading it
3
+ // into the kayan's `targets` after the generated titles of the screens it does
4
+ // not name, so the platform's block here is that screen's whole block, its
5
+ // title included, and the dashboard keeps the generated one. The dashboard
6
+ // titles a person by name for its cells and pickers, and the platform lists
7
+ // every person with the registry's list and acts on each from its row's menu
8
+ // and its own page.
9
+ import { platformUi } from '../../../mithaq.ts'
10
+ import { appPage } from '../../../pages.ts'
11
+
12
+ export const screens = {
13
+ platform: {
14
+ useAsTitle: 'name',
15
+ pages: {
16
+ list: {
17
+ path: '/users',
18
+ page: appPage([{ layout: 'table', main: [{ list: {
19
+ columns: ['name', 'email', { field: 'role', cell: platformUi('user-actions.tsx#PlatformRole') }, 'banned', 'createdAt'],
20
+ filters: ['name', 'email', 'role', 'banned', 'createdAt'],
21
+ search: ['name', 'email'],
22
+ sort: 'email',
23
+ // Set role, ban and impersonate, after the row menu's View.
24
+ menu: platformUi('user-actions.tsx#UserMenu'),
25
+ } }] }]),
26
+ },
27
+ // A person's record, which a row opens and which only reads: every
28
+ // column drawn by its cell, under the actions the row's menu offers, as buttons.
29
+ view: {
30
+ path: '/users/:id',
31
+ page: appPage([{
32
+ layout: 'record',
33
+ main: [
34
+ { component: platformUi('user-actions.tsx#UserButtons') },
35
+ { section: 'person', label: { en: 'Person', ar: 'الشخص' }, fields: ['name', 'email', 'emailVerified', 'image'] },
36
+ // The role by its label, where the column holds the slugs.
37
+ { section: 'standing', label: { en: 'Standing on the platform', ar: 'الوضع في المنصة' }, fields: [{ field: 'role', cell: platformUi('user-actions.tsx#PlatformRole') }, 'banned', 'banReason', 'banExpires'] },
38
+ ],
39
+ // No id: a person reads the record by its name, and the address bar holds the id.
40
+ side: [{ section: 'record', label: { en: 'Record', ar: 'السجل' }, fields: ['createdAt', 'updatedAt'] }],
41
+ }]),
42
+ },
43
+ },
44
+ },
45
+ } as const
@@ -0,0 +1,31 @@
1
+ // Written by `mithaq generate auth` from the Better Auth options generate.providers names. Do not edit.
2
+ // This kayan's access and what each screen draws of it are the project's:
3
+ // `user.access.ts` exporting `access` and `user.screens.ts` exporting
4
+ // `screens`, beside this file, are imported here on the next run. A kayan
5
+ // built under this one's name elsewhere is refused by every relationship to it.
6
+ import { betterAuth, defaultFields, defaultOperations } from 'mithaq'
7
+ import { defineKayan } from '../../../mithaq.ts'
8
+ import { providers } from '@asas/auth'
9
+ import { access } from './user.access.ts'
10
+ import { screens } from './user.screens.ts'
11
+
12
+ export const user = defineKayan({
13
+ name: 'user',
14
+ auth: betterAuth({ server: providers.user.server }),
15
+ targets: { dashboard: { useAsTitle: 'name' }, portal: { useAsTitle: 'name' }, ...screens, service: { operations: defaultOperations } },
16
+ access,
17
+ fields: {
18
+ ...defaultFields,
19
+ id: { kind: 'text' },
20
+ name: { kind: 'text', rules: { required: true } },
21
+ email: { kind: 'text', rules: { required: true, unique: true } },
22
+ emailVerified: { kind: 'checkbox', rules: { required: true } },
23
+ image: { kind: 'text' },
24
+ createdAt: { kind: 'datetime', rules: { required: true } },
25
+ updatedAt: { kind: 'datetime', rules: { required: true, nowOnUpdate: true, providedBy: { update: 'application' } } },
26
+ role: { kind: 'text' },
27
+ banned: { kind: 'checkbox' },
28
+ banReason: { kind: 'text' },
29
+ banExpires: { kind: 'datetime' },
30
+ },
31
+ })
@@ -0,0 +1,8 @@
1
+ // This project's access for the generated `verification` kayan, which `mithaq generate
2
+ // auth` imports into `./verification.ts` because this file sits beside it.
3
+ //
4
+ // Better Auth alone writes and reads this table, through its own adapter, so
5
+ // every managed operation on it is refused to every actor. Written out rather
6
+ // than left to the administrator's default, which a super admin passes in the
7
+ // platform app.
8
+ export { providerOnly as access } from '../../../tenant-access.ts'
@@ -0,0 +1,23 @@
1
+ // Written by `mithaq generate auth` from the Better Auth options generate.providers names. Do not edit.
2
+ // This kayan's access and what each screen draws of it are the project's:
3
+ // `verification.access.ts` exporting `access` and `verification.screens.ts` exporting
4
+ // `screens`, beside this file, are imported here on the next run. A kayan
5
+ // built under this one's name elsewhere is refused by every relationship to it.
6
+ import { defaultFields, defaultOperations } from 'mithaq'
7
+ import { defineKayan } from '../../../mithaq.ts'
8
+ import { access } from './verification.access.ts'
9
+
10
+ export const verification = defineKayan({
11
+ name: 'verification',
12
+ targets: { service: { operations: defaultOperations } },
13
+ access,
14
+ fields: {
15
+ ...defaultFields,
16
+ id: { kind: 'text' },
17
+ identifier: { kind: 'text', rules: { required: true } },
18
+ value: { kind: 'text', rules: { required: true } },
19
+ expiresAt: { kind: 'datetime', rules: { required: true } },
20
+ createdAt: { kind: 'datetime', rules: { required: true } },
21
+ updatedAt: { kind: 'datetime', rules: { required: true, nowOnUpdate: true, providedBy: { update: 'application' } } },
22
+ },
23
+ })
@@ -0,0 +1,143 @@
1
+ import {
2
+ defaultFields,
3
+ defaultLockTime,
4
+ defaultMaxLoginAttempts,
5
+ defaultTokenExpiration,
6
+ local,
7
+ localAuthFields,
8
+ localAuthOperations,
9
+ defaultOperations,
10
+ type Ctx,
11
+ } from 'mithaq'
12
+ import { defineKayan, requestContext } from '../mithaq.ts'
13
+ import { appPage, platformList } from '../pages.ts'
14
+ import { superAdmin } from '../tenant-access.ts'
15
+
16
+ /**
17
+ * The trusted-header names this deployment's own proxy overwrites with the real client address, read
18
+ * from the same `TRUSTED_IP_HEADER` variable Better Auth's `advanced.ipAddress.ipAddressHeaders` is
19
+ * fed from, so both limiters key on the same address. Unset, it falls back to `x-forwarded-for`.
20
+ */
21
+ export const ip = (request: Request): string | null => {
22
+ const headers = process.env.TRUSTED_IP_HEADER?.split(',').map((header) => header.trim()).filter(Boolean) ?? [
23
+ 'x-forwarded-for',
24
+ ]
25
+ for (const header of headers) {
26
+ const value = request.headers.get(header)?.split(',').at(-1)?.trim()
27
+ if (value) return value
28
+ }
29
+ return null
30
+ }
31
+
32
+ /**
33
+ * The secret local auth signs its tokens with, 32 characters at least, from the environment and
34
+ * never from this file: a secret written here is one anyone reading the repository forges a token
35
+ * with. Unset or empty, local auth is off as a whole: every POST to `/api/auth/local` answers
36
+ * `withoutSecret` below with a 503, and the public sign-up with a 403. The kayan keeps its provider,
37
+ * fields and operations, so the build and every file generated from it are the same in every
38
+ * environment.
39
+ */
40
+ const secret = process.env.LOCAL_AUTH_SECRET || undefined
41
+
42
+ const markVerified = (ctx: Ctx): Ctx => ({ ...ctx, data: { ...ctx.data, _verified: true } })
43
+
44
+ /**
45
+ * The kayan the local auth provider writes. It spreads the shipped `localAuthFields` and
46
+ * `localAuthOperations`, so every column and step it has is written here, and the build refuses a
47
+ * protected column left readable.
48
+ */
49
+ export const localUsers = defineKayan({
50
+ name: 'localUsers',
51
+ dbName: 'local_users',
52
+ messages: {
53
+ singular: { en: 'Local account', ar: 'حساب محلي' },
54
+ plural: { en: 'Local accounts', ar: 'الحسابات المحلية' },
55
+ },
56
+ // `context` is what first registration hands its steps.
57
+ //
58
+ // `ip` reads the last entry of whichever header `TRUSTED_IP_HEADER` names, the one the trusted proxy
59
+ // in front of it appended, because no host this template is served by hands the socket address to
60
+ // the `Request`. That is only safe behind exactly one such proxy: without one any caller writes the
61
+ // header, and behind more than one the last entry is the nearest proxy's own address, so every user
62
+ // would share one count; there, take the entry at the depth of the trusted proxies. A project served
63
+ // bare passes its server's own address instead, such as Bun's `server.requestIP(request)`. No header
64
+ // answers null, which leaves the request uncounted.
65
+ // No `email` is passed, so no mail is sent: a super admin verifies each sign-up in the platform app.
66
+ auth: local({
67
+ secret,
68
+ withoutSecret: 'Local sign-in is off on this server: LOCAL_AUTH_SECRET is not set. Set it to a secret of 32 characters at least and restart the server.',
69
+ context: requestContext,
70
+ ip,
71
+ tokenExpiration: defaultTokenExpiration,
72
+ maxLoginAttempts: defaultMaxLoginAttempts,
73
+ lockTime: defaultLockTime,
74
+ }),
75
+ targets: {
76
+ service: {
77
+ operations: {
78
+ ...defaultOperations,
79
+ ...localAuthOperations,
80
+ // The provider's own update with one step before it that writes `_verified: true` over whatever
81
+ // the call sent, so it can only verify; the access below judges it as an update with that one key
82
+ // as its data.
83
+ verify: { ...localAuthOperations.update, steps: [markVerified, ...localAuthOperations.update.steps] },
84
+ },
85
+ },
86
+ /**
87
+ * Where a super admin verifies a sign-up: the accounts by email, verified state and the date each
88
+ * was made, and an edit page whose form is the verified toggle alone. Its fields are written here,
89
+ * never read off `/api/access`, which reports `update` for `email` because the kayan's own `update`
90
+ * asks the data and the report has none; the access above still refuses any write but `_verified`.
91
+ * The form sends only what changed, so an unchanged toggle writes nothing and leaves `updatedAt`
92
+ * alone. The protected columns are named by no page, so no list offers one as a column or a filter.
93
+ */
94
+ platform: {
95
+ useAsTitle: 'email',
96
+ pages: {
97
+ ...platformList('/local-users', {
98
+ columns: ['email', '_verified', 'createdAt'],
99
+ filters: ['_verified'],
100
+ search: ['email'],
101
+ row: [{
102
+ button: 'verify',
103
+ when: { or: [{ 'record._verified': { exists: false } }, { 'record._verified': { equals: false } }] },
104
+ label: { en: 'Verify', ar: 'توثيق' },
105
+ confirm: {
106
+ title: { en: 'Verify this account?', ar: 'توثيق هذا الحساب؟' },
107
+ body: { en: 'Its owner can sign in from now on.', ar: 'يستطيع صاحبه تسجيل الدخول من الآن.' },
108
+ },
109
+ }],
110
+ }),
111
+ // Read-only above the toggle: a read-only leaf is drawn and never sent, so a save still writes
112
+ // `_verified` alone.
113
+ edit: {
114
+ path: '/local-users/:id',
115
+ page: appPage([{ layout: 'record', main: [{ form: [{ section: 'verification', label: { en: 'Verification', ar: 'التحقق' }, fields: [
116
+ { field: 'email', readOnly: true },
117
+ { field: 'createdAt', readOnly: true },
118
+ '_verified',
119
+ ] }] }] }]),
120
+ },
121
+ },
122
+ },
123
+ },
124
+ // Public sign-up stays open. A super admin, in the platform app alone, verifies an account by writing
125
+ // `_verified: true`, which the field's own write admits from the administrator and nobody else. The
126
+ // super admin writes `_verified` and nothing else: a password or an email it set would be an account
127
+ // it could then log in as, and no audit entry records a write to a kayan with no tenant. An
128
+ // `updateMany` is asked this again for each row it writes, with that row's values. Everyone else, a
129
+ // local user on its own row included, reads and writes none.
130
+ access: {
131
+ create: () => true,
132
+ read: superAdmin,
133
+ update: ({ user, data }) => superAdmin({ user }) && Object.keys(data ?? {}).every((key) => key === '_verified'),
134
+ delete: () => false,
135
+ },
136
+ // A label leaves the shipped access in place, which the build compares by identity.
137
+ fields: {
138
+ ...defaultFields,
139
+ ...localAuthFields,
140
+ email: { ...localAuthFields.email, label: { en: 'Email', ar: 'البريد الإلكتروني' } },
141
+ _verified: { ...localAuthFields._verified, label: { en: 'Verified', ar: 'موثق' } },
142
+ },
143
+ })
@@ -0,0 +1,137 @@
1
+ import { actorPermissions, defaultFields, defaultOperations, folderOf, uploadFields, withUploadReference, type Where } from 'mithaq'
2
+ import { defineKayan, serverTargets, organization } from '../mithaq.ts'
3
+ import { courses } from '../courses/courses.ts'
4
+ import { currentOrganization, readsPast } from '../tenant-access.ts'
5
+ import { appPage, danger } from '../pages.ts'
6
+ import { storageConfig } from '../storage/index.ts'
7
+
8
+ const ui = folderOf(new URL('./ui/', import.meta.url))
9
+
10
+ /**
11
+ * The images an actor reads, inside its active organization: every image of
12
+ * its organization where it holds `media.create`, and otherwise only the cover
13
+ * of a course it reads. `media.create` is the line
14
+ * because a role that adds images has to find one before any course names it,
15
+ * in the cover's picker. So a student or an instructor reads no image of a
16
+ * draft, of an archived course or of none, through find, findByID and
17
+ * `GET /api/media/file/<id>`, and the administrator and the registrar read all of them.
18
+ *
19
+ * "A course it reads" is a published course of the organization the actor's session has
20
+ * active, read off the trusted snapshot, under the courses kayan's own read
21
+ * access for this actor. The courses are read straight through the store,
22
+ * past the runner's scope, so the organization is named here and the read
23
+ * never reaches past it whatever the access answers.
24
+ * An image has no field that points at a course, so a row filter cannot reach
25
+ * one, and the covers are read first and named by id.
26
+ */
27
+ const coversOfReadableCourses = async ({ user }: { user: unknown }) => {
28
+ if (readsPast(user, 'media.create')) return true
29
+ const held = actorPermissions(user)
30
+ if (held !== true && !held?.has('courses.find')) return false
31
+ const active = currentOrganization(user)
32
+ const readable = active && (await courses.access!.read!({ user }))
33
+ if (!readable) return false
34
+ const published: Where = { and: [{ organization: { equals: active } }, { _status: { equals: 'published' } }] }
35
+ const found = await serverTargets.drizzle.find({ kayan: 'courses', args: {
36
+ limit: 0,
37
+ page: 1,
38
+ sort: { field: 'id', direction: 'asc' },
39
+ where: readable === true ? published : { and: [readable, published] },
40
+ } })
41
+ const ids = [...new Set(found.docs.map((row) => row.cover).filter((id): id is string => typeof id === 'string'))]
42
+ return ids.length ? { id: { in: ids } } : false
43
+ }
44
+
45
+ /**
46
+ * The organization's images: a row per file, whose object is kept where the
47
+ * project's storage puts it. A course points at one as its cover.
48
+ *
49
+ * It spreads `uploadFields`, the columns the create writes from the upload it
50
+ * verified and nobody writes after, and declares its own create and delete:
51
+ * the create takes the signed `uploadReference` that
52
+ * `POST /api/media/uploadInstructions` handed out and runs the project's
53
+ * `uploaded`, and the delete runs its `removed` after the commit. `buildAsas`
54
+ * refuses a create that skips the configured `uploaded`. A file is
55
+ * read at `GET /api/media/file/<id>` through this kayan's own `findByID`, so
56
+ * its organization and `coversOfReadableCourses` decide who reads it. It keeps no versions, since a file never changes once it is
57
+ * made; a course keeps the versions of its cover instead.
58
+ */
59
+ export const media = defineKayan({
60
+ name: 'media',
61
+ // ANDed with the inherited rows, so it narrows and never widens.
62
+ access: { read: coversOfReadableCourses },
63
+ messages: {
64
+ singular: { en: 'Image', ar: 'صورة' },
65
+ plural: { en: 'Images', ar: 'الصور' },
66
+ },
67
+ targets: {
68
+ storage: { mimeTypes: ['image/png', 'image/jpeg', 'image/webp'] },
69
+ // A file is named by its name wherever it stands for itself: the upload
70
+ // control's picker and a relationship column pointing here.
71
+ portal: { useAsTitle: 'filename' },
72
+ // The organization's images, listed and re-described. A file is made
73
+ // where it is used, by the upload control, and on this kayan's own New
74
+ // page, both through the upload dialog, which sends it before the row is
75
+ // made. The list draws each image's thumbnail beside its name and its size as a
76
+ // person reads it. The file's own columns are the runner's, so the edit
77
+ // page draws them read-only, with the image, its size, Download and
78
+ // Replace file beside the form, and above it on a phone.
79
+ dashboard: {
80
+ useAsTitle: 'filename',
81
+ description: { en: 'The images this organization uses, each with the words a screen reader says for it.', ar: 'الصور التي تستخدمها هذه المؤسسة، ولكل منها الكلمات التي يقرؤها قارئ الشاشة.' },
82
+ pages: {
83
+ list: { path: '/media', page: appPage([{ layout: 'table', main: [{ list: {
84
+ columns: [{ field: 'filename', cell: ui('preview.tsx#FileName') }, 'alt', 'mimeType', { field: 'filesize', cell: ui('preview.tsx#FileSize') }, 'createdAt'],
85
+ search: ['filename'],
86
+ sort: '-createdAt',
87
+ } }] }]) },
88
+ create: { path: '/media/new', component: ui('preview.tsx#NewImage') },
89
+ edit: {
90
+ path: '/media/:id',
91
+ page: appPage([{
92
+ layout: 'record',
93
+ main: [{ form: [
94
+ { section: 'description', label: { en: 'Description', ar: 'الوصف' }, fields: ['alt'] },
95
+ { section: 'file', label: { en: 'File', ar: 'الملف' }, fields: ['filename', 'mimeType', { row: ['width', 'height'] }] },
96
+ ] }],
97
+ side: [{ component: ui('preview.tsx#MediaPreview') }],
98
+ danger: danger(),
99
+ }]),
100
+ },
101
+ },
102
+ },
103
+ service: {
104
+ operations: {
105
+ ...defaultOperations,
106
+ // The upload inside the create. Before the transaction opens, the
107
+ // signed reference is checked and the stored object is read back,
108
+ // and every file column is written from what the server measured.
109
+ // Then the row is made in one insert.
110
+ create: {
111
+ ...defaultOperations.create,
112
+ input: ({ create }) => withUploadReference(create),
113
+ beforeTransaction: [storageConfig.uploaded],
114
+ },
115
+ // The object is removed only once the row's removal has committed,
116
+ // so a failure leaves an orphan for the sweep and never a row that
117
+ // names nothing.
118
+ delete: {
119
+ ...defaultOperations.delete,
120
+ afterCommit: ({ afterCommit }) => [...afterCommit, storageConfig.removed],
121
+ },
122
+ },
123
+ },
124
+ },
125
+ fields: {
126
+ ...defaultFields,
127
+ organization,
128
+ ...uploadFields,
129
+ // What a screen reader says for the image, in every locale, since an
130
+ // image with no words is no image to someone who cannot see it.
131
+ alt: {
132
+ kind: 'text',
133
+ label: { en: 'Alternative text', ar: 'النص البديل' },
134
+ rules: { localized: true, required: true, maxLength: 300 },
135
+ },
136
+ },
137
+ })
@@ -0,0 +1,143 @@
1
+ // What the `media` kayan draws of its own files: the image beside its edit
2
+ // form, the list's thumbnail and size cells, and the page a new image is made
3
+ // on. Each reads the row's computed `url` and `thumbnailURL`, so a file is
4
+ // fetched through the file route and the row's access decides. A new file is
5
+ // made in the registry's upload dialog, which sends the bytes before the row
6
+ // is made.
7
+ import { useRef, useState } from 'react'
8
+ import { Download, Upload } from 'lucide-react'
9
+ import type { CellProps, ComponentProps, Row } from 'mithaq/admin'
10
+ import { useAccess, useClient, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
11
+ import { locales } from '@/copy.ts'
12
+ import { Button, buttonVariants } from '@/components/ui/button'
13
+ import { FileThumb, FileValue, UploadDialog, sizeOf } from '@/components/mithaq/upload.tsx'
14
+ import { AppShell } from '../../ui/app-shell.tsx'
15
+
16
+ /** The `filesize` column: the size as a person reads it, never the raw bytes, in the upload dialog's own units. */
17
+ export function FileSize({ value }: CellProps) {
18
+ const { locale } = useLocale()
19
+ return typeof value === 'number' ? <span className="whitespace-nowrap">{sizeOf(value, locale)}</span> : null
20
+ }
21
+
22
+ /**
23
+ * The `filename` column: the image's thumbnail before its name. The name
24
+ * keeps at least seven rem and wraps onto a second line before it is cut,
25
+ * between words where it has them and anywhere only where it must, so
26
+ * on a phone the table gives the column room for it rather than squeezing it
27
+ * to "co…" beside the thumbnail.
28
+ */
29
+ export function FileName({ value, row }: CellProps) {
30
+ const { t, locale } = useLocale()
31
+ const name = String(value ?? '')
32
+ return (
33
+ <span className="inline-flex min-w-0 items-center gap-2">
34
+ <FileThumb row={row} t={t} locale={locale} size="xs" />
35
+ <span className="line-clamp-2 min-w-28 [overflow-wrap:anywhere]" dir="auto" title={name}>{name}</span>
36
+ </span>
37
+ )
38
+ }
39
+
40
+ const useEditHref = () => {
41
+ const { path, kayan } = useScreen()
42
+ return (id: unknown) => `${path}${kayan.media?.pages.edit?.path.replace(':id', encodeURIComponent(String(id)))}`
43
+ }
44
+
45
+ /** The upload dialog over this kayan, which opens the new image's page once it is made. */
46
+ function NewImageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
47
+ const { t, locale } = useLocale()
48
+ const { kayan } = useScreen()
49
+ const client = useClient()
50
+ const editHref = useEditHref()
51
+ // The dialog closes itself once the row is made, and that close is no
52
+ // Cancel: the new image's page is already opening.
53
+ const done = useRef(false)
54
+ return kayan.media ? (
55
+ <UploadDialog
56
+ open={open}
57
+ onOpenChange={(next) => done.current || onOpenChange(next)}
58
+ target={kayan.media}
59
+ client={client}
60
+ t={t}
61
+ locale={locale}
62
+ locales={locales}
63
+ onDone={(row: Row) => {
64
+ done.current = true
65
+ window.location.assign(editHref(row.id))
66
+ }}
67
+ />
68
+ ) : null
69
+ }
70
+
71
+ /**
72
+ * The image beside its edit form, and above it on a phone, where the record
73
+ * layout puts a side marked `data-lead` first: the image itself, its size, a
74
+ * link that downloads it, and Replace file. A file never changes once it is
75
+ * made, so Replace file uploads a new image and opens it, and this one stays,
76
+ * with whatever points at it, until it is deleted.
77
+ */
78
+ export function MediaPreview() {
79
+ const { t, locale } = useLocale()
80
+ const access = useAccess()
81
+ const row = useRecord()
82
+ const [replacing, setReplacing] = useState(false)
83
+ if (!row) return null
84
+ const creates = access.collections?.media?.create === true
85
+ const url = typeof row.url === 'string' ? row.url : undefined
86
+ return (
87
+ <div data-media-preview="" data-lead="" className="grid justify-items-start gap-3">
88
+ <FileValue row={row} t={t} locale={locale} />
89
+ {typeof row.filesize === 'number' ? <p className="text-muted-foreground text-sm">{sizeOf(row.filesize, locale)}</p> : null}
90
+ <div className="flex flex-wrap gap-2">
91
+ {url ? (
92
+ <a href={url} download={typeof row.filename === 'string' ? row.filename : ''} data-media-download="" className={buttonVariants({ variant: 'outline', size: 'sm' })}>
93
+ <Download className="size-4" aria-hidden />
94
+ {t('media.download', 'Download')}
95
+ </a>
96
+ ) : null}
97
+ {creates ? (
98
+ <Button type="button" variant="outline" size="sm" data-media-replace="" onClick={() => setReplacing(true)}>
99
+ <Upload className="size-4" aria-hidden />
100
+ {t('media.replace', 'Replace file')}
101
+ </Button>
102
+ ) : null}
103
+ </div>
104
+ {creates ? <p className="text-muted-foreground text-xs">{t('media.replaceAbout', 'Replace file uploads a new image and opens it. This one stays until you delete it.')}</p> : null}
105
+ <NewImageDialog open={replacing} onOpenChange={setReplacing} />
106
+ </div>
107
+ )
108
+ }
109
+
110
+ /**
111
+ * The New page, `/media/new`, which the list's New button opens: the upload
112
+ * dialog, open at once, and the page under it, which opens it again. Closing
113
+ * it goes back to the list.
114
+ */
115
+ export function NewImage(_props: ComponentProps) {
116
+ const { t } = useLocale()
117
+ const { path, kayan } = useScreen()
118
+ const [open, setOpen] = useState(true)
119
+ const list = `${path}${kayan.media?.pages.list?.path ?? ''}`
120
+ return (
121
+ <AppShell
122
+ regions={{
123
+ header: <h1>{t('media.new', 'New image')}</h1>,
124
+ main: (
125
+ <div data-media-new="" className="grid justify-items-start gap-4">
126
+ <p className="text-muted-foreground">{t('media.newAbout', 'Choose an image and describe it. It is uploaded before it is saved.')}</p>
127
+ <Button type="button" onClick={() => setOpen(true)}>
128
+ <Upload className="size-4" aria-hidden />
129
+ {t('admin.upload.new', 'Upload new')}
130
+ </Button>
131
+ <NewImageDialog
132
+ open={open}
133
+ onOpenChange={(next) => {
134
+ setOpen(next)
135
+ if (!next) window.location.assign(list)
136
+ }}
137
+ />
138
+ </div>
139
+ ),
140
+ }}
141
+ />
142
+ )
143
+ }