create-mithaq 0.1.1 → 0.2.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 (348) hide show
  1. package/LICENSES/openstatus-time-picker.txt +21 -0
  2. package/index.js +457 -63
  3. package/package.json +1 -1
  4. package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
  5. package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
  6. package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
  7. package/template/apps/asas-backoffice/src/components.ts +35 -0
  8. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  9. package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
  10. package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
  11. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  12. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  13. package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
  14. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  15. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  16. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  17. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
  18. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
  19. package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
  20. package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
  21. package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
  22. package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
  23. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  24. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  25. package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
  26. package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
  27. package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
  28. package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
  29. package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
  30. package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
  31. package/template/apps/asas-backoffice/src/server.ts +30 -0
  32. package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
  33. package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
  34. package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
  35. package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
  36. package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
  37. package/template/apps/asas-tanstack/package.json +4 -2
  38. package/template/apps/asas-tanstack/src/api.ts +1 -1
  39. package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
  40. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  41. package/template/apps/asas-tanstack/src/components.ts +35 -0
  42. package/template/apps/asas-tanstack/src/copy.ts +416 -79
  43. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  44. package/template/apps/asas-tanstack/src/links.ts +1 -1
  45. package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
  46. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  47. package/template/apps/asas-tanstack/src/redirect.ts +20 -1
  48. package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
  49. package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
  50. package/template/apps/asas-tanstack/src/router.tsx +12 -1
  51. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
  52. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
  53. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
  54. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
  55. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
  56. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
  57. package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
  58. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
  59. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  60. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
  61. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
  62. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
  63. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  64. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
  65. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
  66. package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
  67. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
  68. package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
  69. package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
  70. package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
  71. package/template/apps/asas-tanstack/src/screens.tsx +46 -26
  72. package/template/apps/asas-tanstack/src/server.ts +10 -0
  73. package/template/apps/asas-tanstack/src/slide.ts +1 -1
  74. package/template/apps/asas-tanstack/src/styles.css +7 -3
  75. package/template/apps/asas-tanstack/tsconfig.json +2 -2
  76. package/template/apps/asas-tanstack/vite.config.ts +7 -1
  77. package/template/package.json +3 -3
  78. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  79. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  80. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  81. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  82. package/template/templates/asas/.env.example +22 -5
  83. package/template/templates/asas/AGENTS.md +101 -0
  84. package/template/templates/asas/CLAUDE.md +1 -0
  85. package/template/templates/asas/README.md +1056 -0
  86. package/template/templates/asas/package.json +19 -4
  87. package/template/templates/asas/src/asas.ts +81 -56
  88. package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
  89. package/template/templates/asas/src/dev-setup.ts +112 -0
  90. package/template/templates/asas/src/local-database.ts +22 -0
  91. package/template/templates/asas/src/messages/account.json +129 -103
  92. package/template/templates/asas/src/messages/auditLog.json +223 -105
  93. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  94. package/template/templates/asas/src/messages/courses.json +256 -214
  95. package/template/templates/asas/src/messages/enrollments.json +45 -27
  96. package/template/templates/asas/src/messages/invitation.json +72 -56
  97. package/template/templates/asas/src/messages/localUsers.json +34 -28
  98. package/template/templates/asas/src/messages/media.json +31 -21
  99. package/template/templates/asas/src/messages/member.json +59 -39
  100. package/template/templates/asas/src/messages/memberRoles.json +37 -31
  101. package/template/templates/asas/src/messages/mithaq.json +206 -162
  102. package/template/templates/asas/src/messages/organization.json +63 -41
  103. package/template/templates/asas/src/messages/organizationRole.json +59 -47
  104. package/template/templates/asas/src/messages/roles.json +59 -45
  105. package/template/templates/asas/src/messages/session.json +99 -79
  106. package/template/templates/asas/src/messages/sessions.json +110 -80
  107. package/template/templates/asas/src/messages/settings.json +51 -53
  108. package/template/templates/asas/src/messages/tags.json +31 -21
  109. package/template/templates/asas/src/messages/user.json +109 -87
  110. package/template/templates/asas/src/messages/verification.json +59 -47
  111. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  112. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  113. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  114. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  115. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  116. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  117. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  118. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  119. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  120. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  121. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  122. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  123. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  124. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  125. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  126. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  127. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  128. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  129. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  130. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  131. package/template/templates/asas/src/migrations/schema.ts +1 -0
  132. package/template/templates/asas/src/mithaq.ts +4 -406
  133. package/template/templates/asas/src/organizations.ts +3 -0
  134. package/template/templates/asas/src/seed.ts +147 -45
  135. package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
  136. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  137. package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
  138. package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
  139. package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
  140. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
  141. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  142. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  143. package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
  144. package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
  145. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  146. package/template/templates/asas/src/ui-portal/home.tsx +9 -13
  147. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  148. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  149. package/template/templates/features/audit-log/index.ts +4 -0
  150. package/template/templates/features/audit-log/package.json +13 -0
  151. package/template/templates/features/auth/index.ts +11 -0
  152. package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
  153. package/template/templates/{auth → features/auth}/package.json +2 -3
  154. package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
  155. package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
  156. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
  157. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
  158. package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
  159. package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
  160. package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
  161. package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
  162. package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
  163. package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
  164. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
  165. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
  166. package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
  167. package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
  168. package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
  169. package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
  170. package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
  171. package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
  172. package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
  173. package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
  174. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  175. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  176. package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
  177. package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
  178. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  179. package/template/templates/features/courses/domain/archive.ts +39 -0
  180. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  181. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  182. package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
  183. package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
  184. package/template/templates/features/courses/index.ts +8 -0
  185. package/template/templates/features/courses/package.json +24 -0
  186. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  187. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  188. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  189. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  190. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  191. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  192. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  193. package/template/templates/features/courses/sessions/seats.ts +42 -0
  194. package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
  195. package/template/templates/features/media/index.ts +10 -0
  196. package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
  197. package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
  198. package/template/templates/features/media/package.json +19 -0
  199. package/template/templates/features/roles/index.ts +4 -0
  200. package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
  201. package/template/templates/features/roles/package.json +21 -0
  202. package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
  203. package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
  204. package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
  205. package/template/templates/features/settings/index.ts +3 -0
  206. package/template/templates/features/settings/package.json +13 -0
  207. package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
  208. package/template/templates/features/tags/index.ts +3 -0
  209. package/template/templates/features/tags/package.json +14 -0
  210. package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
  211. package/template/templates/mithaq/package.json +33 -0
  212. package/template/templates/mithaq/src/mail/index.ts +32 -0
  213. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  214. package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
  215. package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
  216. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  217. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  218. package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
  219. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  220. package/template/templates/mithaq/src/mithaq.ts +593 -0
  221. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  222. package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
  223. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  224. package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
  225. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  226. package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
  227. package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
  228. package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
  229. package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
  230. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  231. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  232. package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
  233. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  234. package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
  235. package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
  236. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
  237. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
  238. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
  239. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
  240. package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
  241. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
  242. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
  243. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
  244. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
  245. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
  246. package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
  247. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
  248. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
  249. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
  250. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
  251. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
  252. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
  253. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
  254. package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
  255. package/template/apps/asas-platform/src/redirect.ts +0 -1
  256. package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
  257. package/template/apps/asas-platform/src/router.tsx +0 -7
  258. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
  259. package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
  260. package/template/apps/asas-platform/src/server.ts +0 -20
  261. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
  262. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
  263. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
  264. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
  265. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
  266. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
  267. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
  268. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
  269. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
  270. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
  271. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
  272. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
  273. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
  274. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
  275. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
  276. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
  277. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
  278. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
  279. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
  280. package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
  281. package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
  282. package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
  283. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
  284. package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
  285. package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
  286. package/template/templates/asas/src/rich-text-nodes.ts +0 -11
  287. package/template/templates/asas/src/roles.ts +0 -16
  288. package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
  289. package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
  290. package/template/templates/auth/src/index.ts +0 -690
  291. /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
  292. /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
  293. /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
  294. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
  295. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
  296. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
  297. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
  298. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
  299. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
  300. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
  301. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
  302. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
  303. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
  304. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
  305. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
  306. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
  307. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
  308. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
  309. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
  310. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
  311. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
  312. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
  313. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
  314. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
  315. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
  316. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
  317. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
  318. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
  319. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
  320. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
  321. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
  322. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
  323. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
  324. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
  325. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
  326. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
  327. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
  328. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
  329. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
  330. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
  331. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
  332. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
  333. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
  334. /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
  335. /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
  336. /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
  337. /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
  338. /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
  339. /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
  340. /package/template/templates/{asas → mithaq}/components.json +0 -0
  341. /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
  342. /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
  343. /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
  344. /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
  345. /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
  346. /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
  347. /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
  348. /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
@@ -1,11 +1,11 @@
1
1
  import { actorRoles, conflict, defaultFields, denied, folderOf, grantedPermissions, writableRoleName, defaultOperations, type Ctx, type WriteTools } from 'mithaq'
2
- import { user as users } from '../kayan/auth/user/user.ts'
2
+ import { user as users } from '@asas/auth'
3
3
  import { assignedCount, managerRemains, memberRoles } from '../member-roles/member-roles.ts'
4
- import { managesRoles } from './grants.ts'
5
- import { defineKayan, rows, organization } from '../mithaq.ts'
6
- import { appPage, platformList, platformRecord } from '../pages.ts'
7
- import { systemRoles } from '../roles.ts'
8
- import { platformReads } from '../tenant-access.ts'
4
+ import { managesRoles } from '@asas/mithaq/roles/grants.ts'
5
+ import { defineKayan, rows, organization } from '@asas/mithaq'
6
+ import { appPage, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
7
+ import { systemRoles } from '@asas/mithaq/roles/system-roles.ts'
8
+ import { backofficeReads } from '@asas/mithaq/roles/tenant-access.ts'
9
9
 
10
10
  const rolesUi = folderOf(new URL('./ui/', import.meta.url))
11
11
 
@@ -14,17 +14,17 @@ const rolesUi = folderOf(new URL('./ui/', import.meta.url))
14
14
  * a granter may not hand over a key it does not itself hold.
15
15
  */
16
16
  /**
17
- * A tenant's role never holds `screen.platform`, which only a platform role holds: a row naming it would be a tenant claiming the platform.
17
+ * A tenant's role never holds `screen.backoffice`, which only a backoffice role holds: a row naming it would be a tenant claiming the backoffice.
18
18
  * It is refused before the granter check, so a root granter holding `'*'` is refused too.
19
19
  */
20
- const PLATFORM_KEY = 'screen.platform'
20
+ const BACKOFFICE_KEY = 'screen.backoffice'
21
21
  const tenantKeysOnly = (ctx: Ctx): Ctx => {
22
22
  const written = (ctx.data as Record<string, unknown>).permissions
23
- if (!Array.isArray(written) || !written.includes(PLATFORM_KEY)) return ctx
23
+ if (!Array.isArray(written) || !written.includes(BACKOFFICE_KEY)) return ctx
24
24
  throw denied({
25
- message: `Forbidden: a "${ctx.kayan.name}" row is a tenant's role, and "${PLATFORM_KEY}" is held only by a platform role.`,
26
- key: 'error.forbidden.platform_key',
27
- params: { kayan: ctx.kayan.name, permissions: [PLATFORM_KEY] },
25
+ message: `Forbidden: a "${ctx.kayan.name}" row is a tenant's role, and "${BACKOFFICE_KEY}" is held only by a backoffice role.`,
26
+ key: 'error.forbidden.backoffice_key',
27
+ params: { kayan: ctx.kayan.name, permissions: [BACKOFFICE_KEY] },
28
28
  })
29
29
  }
30
30
 
@@ -98,24 +98,24 @@ const keepsManagers = async (ctx: Ctx, tools: WriteTools): Promise<Ctx> => {
98
98
  const rolesAccess = rows({
99
99
  '*': { create: true, read: true, update: true, delete: true },
100
100
  /**
101
- * Every platform role reads the role rows of every tenant, and writes none. A
101
+ * Every backoffice role reads the role rows of every tenant, and writes none. A
102
102
  * tenant that lost its administrator is repaired through `mithaq rbac assign`,
103
- * never from the platform app.
103
+ * never from the backoffice app.
104
104
  */
105
- ...platformReads,
105
+ ...backofficeReads,
106
106
  })
107
107
 
108
108
  export const roles = defineKayan({
109
109
  name: 'roles',
110
110
  targets: {
111
- /** Every organization's rows on the platform, read-only. */
112
- platform: {
111
+ /** Every organization's rows on the backoffice, read-only. */
112
+ backoffice: {
113
113
  useAsTitle: 'label',
114
114
  pages: {
115
115
  // A role by its label, never its slug, and its keys grouped as the
116
116
  // dashboard draws a fixed role's.
117
- ...platformList('/roles', { columns: ['label', 'organization'], filters: ['organization'], search: ['label'] }),
118
- ...platformRecord({
117
+ ...backofficeList('/roles', { columns: ['label', 'organization'], filters: ['organization'], search: ['label'] }),
118
+ ...backofficeRecord({
119
119
  path: '/roles/:id',
120
120
  main: [{ section: 'role', label: { en: 'Role', ar: 'الدور' }, fields: ['label', { field: 'permissions', cell: rolesUi('roles.tsx#Permissions') }] }],
121
121
  }),
@@ -142,15 +142,15 @@ export const roles = defineKayan({
142
142
  ar: 'ما يمكن لكل دور في هذه المؤسسة أن يراه ويفعله.',
143
143
  },
144
144
  pages: {
145
- list: { path: '/roles', page: appPage([{ layout: 'table', main: [{ list: { columns: ['label', 'name'], limit: 100 }, component: 'rolesTable' }] }]) },
146
- create: { path: '/roles/new', page: appPage([{ layout: 'record', main: [{ form: ['name', 'label', 'permissions'], component: 'roleForm' }] }]) },
145
+ list: { path: '/roles', page: appPage([{ layout: 'table', main: [{ list: { columns: ['label', 'name', 'permissions'], limit: 100 }, component: rolesUi('roles.tsx#RolesTable') }] }]) },
146
+ create: { path: '/roles/new', page: appPage([{ layout: 'record', main: [{ form: ['name', 'label', 'permissions'], component: rolesUi('role-form.tsx#RoleForm') }] }]) },
147
147
  edit: {
148
148
  path: '/roles/:id',
149
149
  page: appPage([
150
150
  {
151
151
  layout: 'record',
152
- main: [{ form: ['name', 'label', 'permissions'], component: 'roleForm' }],
153
- danger: [{ button: 'delete', label: { en: 'Delete', ar: 'حذف' }, component: 'deleteRole' }],
152
+ main: [{ form: ['name', 'label', 'permissions'], component: rolesUi('role-form.tsx#RoleForm') }],
153
+ danger: [{ button: 'delete', label: { en: 'Delete', ar: 'حذف' }, component: rolesUi('roles.tsx#DeleteRole') }],
154
154
  },
155
155
  ]),
156
156
  },
@@ -3,20 +3,22 @@
3
3
  // values; this reads and writes them through `useForm()`, and reads each box's
4
4
  // refusal through `useKayanField`, so a box says it is required, says what is
5
5
  // wrong under itself once left or refused, and marks itself invalid.
6
- import type { ReactNode } from 'react'
6
+ import type { ComponentProps as ElementProps, ReactNode } from 'react'
7
+ import { ChevronDown } from 'lucide-react'
7
8
  import type { ComponentProps } from 'mithaq/admin'
8
9
  import { useAccess, useForm, useKayan, useLocale, useScreen } from 'mithaq/admin/react'
9
10
  import { Badge } from '@/components/ui/badge'
10
11
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
12
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
11
13
  import { Input } from '@/components/ui/input'
12
- import { ariaRequired } from '@/components/mithaq/controls.tsx'
13
- import { FieldShell, describedBy } from '@/components/mithaq/field-shell.tsx'
14
- import { useKayanField } from '@/components/mithaq/form.tsx'
15
- import { grantable } from '../grants.ts'
16
- import { projectStatements } from '../../statements.ts'
14
+ import { ariaRequired } from 'mithaq-admin/control-props.ts'
15
+ import { FieldShell, describedBy } from 'mithaq-admin/field-shell.tsx'
16
+ import { useKayanField } from 'mithaq-admin/form-context.tsx'
17
+ import { grantable } from '@asas/mithaq/roles/grants.ts'
18
+ import { projectStatements } from '@asas/mithaq/roles/statements.ts'
17
19
 
18
20
  /** The key a tenant's role may never hold, which the role kayan refuses by name, so no badge offers it. */
19
- const PLATFORM_KEY = 'screen.platform'
21
+ const BACKOFFICE_KEY = 'screen.backoffice'
20
22
 
21
23
  type Key = { key: string; label: string }
22
24
  type Group = { name: string; label: string; operations: Key[]; fields: Key[] }
@@ -31,7 +33,7 @@ const useGroups = (): Group[] => {
31
33
  const screen = useScreen()
32
34
  const access = useAccess()
33
35
  const { locale, t } = useLocale()
34
- const screens = (access.screens ?? []).map((name) => `screen.${name}`).filter((key) => key !== PLATFORM_KEY)
36
+ const screens = (access.screens ?? []).map((name) => `screen.${name}`).filter((key) => key !== BACKOFFICE_KEY)
35
37
  return [
36
38
  {
37
39
  name: 'screen',
@@ -54,7 +56,45 @@ const useGroups = (): Group[] => {
54
56
  }
55
57
 
56
58
  /**
57
- * The keys a role holds as badges, grouped by screen and kayan. With `onChange`
59
+ * A role's or a group's keys behind a row that names them and counts what they grant, closed at
60
+ * first so a page of roles reads as a list of names; `defaultOpen` where the keys are the page's
61
+ * work. A closed panel is `hidden="until-found"` once the page runs, so it is in the first paint
62
+ * and a find on the page opens it. `named` names a row that draws no label of its own, so a reader
63
+ * of the screen hears whose keys it opens. Every permission has nothing to open, so it is said.
64
+ */
65
+ export function Grants({ label, named, held, heading, defaultOpen, children, ...root }: { label?: ReactNode; named?: string; held: readonly string[]; heading?: boolean; defaultOpen?: boolean; children: ReactNode } & ElementProps<'div'>) {
66
+ const { locale, t } = useLocale()
67
+ const count = held.length === 1
68
+ ? t('dashboard.permissionsCountOne', '1 permission')
69
+ : (t('dashboard.permissionsCount', '{count} permissions') ?? '{count} permissions').replace('{count}', () => new Intl.NumberFormat(locale).format(held.length))
70
+ if (held.includes('*')) {
71
+ return (
72
+ <div {...root} className="flex items-center gap-2">
73
+ {/* Where a chevron stands on every other row, so the names line up. */}
74
+ <span aria-hidden="true" className="size-4 shrink-0" />
75
+ {label}
76
+ <span className="text-muted-foreground text-xs">{t('dashboard.everyPermission', 'Every permission')}</span>
77
+ </div>
78
+ )
79
+ }
80
+ const trigger = (
81
+ <CollapsibleTrigger aria-label={named && `${named}: ${count}`} className="group flex w-full cursor-pointer items-center gap-2 text-start max-sm:min-h-11">
82
+ <ChevronDown aria-hidden="true" className="text-muted-foreground size-4 shrink-0 transition-transform group-data-[panel-open]:rotate-180" />
83
+ {label}
84
+ <span className="text-muted-foreground text-xs font-normal">{count}</span>
85
+ </CollapsibleTrigger>
86
+ )
87
+ return (
88
+ <Collapsible {...root} defaultOpen={defaultOpen}>
89
+ {heading ? <h3 className="text-sm font-medium">{trigger}</h3> : trigger}
90
+ <CollapsibleContent hiddenUntilFound className="ps-6 pt-2">{children}</CollapsibleContent>
91
+ </Collapsible>
92
+ )
93
+ }
94
+
95
+ /**
96
+ * The keys a role holds as badges, grouped by screen and kayan, each group behind its count: closed where they are
97
+ * read, open in the editor, whose work they are. With `onChange`
58
98
  * each badge is a toggle, a key the viewer may not hand over disabled; without it
59
99
  * only the keys held are drawn. A key no group names stays, under "Other", so a
60
100
  * save never drops one.
@@ -63,7 +103,7 @@ export function PermissionBadges({ held, onChange, busy }: { held: readonly stri
63
103
  const access = useAccess()
64
104
  const { t } = useLocale()
65
105
  // Read only, every screen the role enters is named, whichever the viewer
66
- // enters: the platform reads a tenant's role, which holds `screen.dashboard`.
106
+ // enters: the backoffice reads a tenant's role, which holds `screen.dashboard`.
67
107
  const groups = useGroups().map((group) =>
68
108
  group.name === 'screen' && !onChange
69
109
  ? { ...group, operations: held.filter((key) => key.startsWith('screen.')).map((key) => ({ key, label: t(`screens.${key.slice('screen.'.length)}`) ?? key })) }
@@ -111,28 +151,29 @@ export function PermissionBadges({ held, onChange, busy }: { held: readonly stri
111
151
  }
112
152
  if (held.includes('*')) return <Badge variant="outline">{t('dashboard.everyPermission', 'Every permission')}</Badge>
113
153
  return (
114
- <div data-permissions="" className="grid gap-4">
154
+ <div data-permissions="" className="grid gap-2">
115
155
  {groups.map((group) => {
116
156
  const operations = badges(group.operations)
117
157
  const fields = badges(group.fields)
158
+ const keys = [...group.operations, ...group.fields].map((one) => one.key)
118
159
  return operations || fields ? (
119
- <section key={group.name} data-group={group.name} className="grid gap-1.5">
120
- <h3 className="text-muted-foreground text-xs font-medium">{group.label}</h3>
121
- {operations}
122
- {fields ? (
123
- <>
124
- <p className="text-muted-foreground text-xs">{t('dashboard.permissionsFields', 'Fields')}</p>
125
- {fields}
126
- </>
127
- ) : null}
128
- </section>
160
+ <Grants key={group.name} data-group={group.name} heading defaultOpen={Boolean(onChange)} label={<span>{group.label}</span>} held={held.filter((key) => keys.includes(key))}>
161
+ <div className="grid gap-1.5">
162
+ {operations}
163
+ {fields ? (
164
+ <>
165
+ <p className="text-muted-foreground text-xs">{t('dashboard.permissionsFields', 'Fields')}</p>
166
+ {fields}
167
+ </>
168
+ ) : null}
169
+ </div>
170
+ </Grants>
129
171
  ) : null
130
172
  })}
131
173
  {other.length ? (
132
- <section data-group="" className="grid gap-1.5">
133
- <h3 className="text-muted-foreground text-xs font-medium">{t('dashboard.permissionsOther', 'Other')}</h3>
174
+ <Grants data-group="" heading defaultOpen={Boolean(onChange)} label={<span>{t('dashboard.permissionsOther', 'Other')}</span>} held={other}>
134
175
  {badges(other.map((key) => ({ key, label: otherLabel(key) })))}
135
- </section>
176
+ </Grants>
136
177
  ) : null}
137
178
  </div>
138
179
  )
@@ -4,8 +4,7 @@ import { useState } from 'react'
4
4
  import { Lock } from 'lucide-react'
5
5
  import type { ButtonProps, CellProps, Row } from 'mithaq/admin'
6
6
  import { useClient, useList, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
7
- import type { asas } from '../../asas.ts'
8
- import { tenantRoles } from '../../tenant-roles.ts'
7
+ import { tenantRoles } from '@asas/mithaq/roles/tenant-roles.ts'
9
8
  import {
10
9
  AlertDialog,
11
10
  AlertDialogAction,
@@ -19,7 +18,7 @@ import {
19
18
  import { Badge } from '@/components/ui/badge'
20
19
  import { Button } from '@/components/ui/button'
21
20
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
22
- import { PermissionBadges } from './role-form.tsx'
21
+ import { Grants, PermissionBadges } from './role-form.tsx'
23
22
 
24
23
  /** A role's label in the viewer's locale, and its slug only where a row carries none. */
25
24
  export const labelOf = (row: Row | null | undefined, locale: string) => {
@@ -28,9 +27,9 @@ export const labelOf = (row: Row | null | undefined, locale: string) => {
28
27
  }
29
28
 
30
29
  /**
31
- * The list node's component: the fixed roles first, from their code, each with a lock and its keys
32
- * one click away, then the organization's own, each opening its page. It draws no row menu; a custom
33
- * role's delete is its page's.
30
+ * The list node's component: the fixed roles first, from their code, each with a lock, then the
31
+ * organization's own. Each row names a role and counts what it grants, and opens to its keys and,
32
+ * for a custom role, a link to its page. It draws no row menu; a custom role's delete is its page's.
34
33
  */
35
34
  export function RolesTable() {
36
35
  const { page, creates, href, newHref } = useList()
@@ -56,33 +55,55 @@ export function RolesTable() {
56
55
  <TableBody>
57
56
  {Object.entries(tenantRoles).map(([name, role]) => (
58
57
  <TableRow key={name} data-row={name} data-fixed="true">
59
- <TableCell>
60
- <details data-fixed-role="">
61
- <summary className="flex cursor-pointer items-center gap-1.5 font-medium max-sm:min-h-11">
62
- <Lock data-lock="" className="text-muted-foreground size-3.5" aria-hidden="true" />
63
- <span>{(role.label as Readonly<Record<string, string>>)[locale] ?? name}</span>
64
- </summary>
65
- <div className="grid gap-2 pt-2">
58
+ <TableCell className="whitespace-normal">
59
+ <Grants
60
+ data-fixed-role=""
61
+ label={
62
+ <span className="flex items-center gap-1.5 font-medium">
63
+ <Lock data-lock="" className="text-muted-foreground size-3.5" aria-hidden="true" />
64
+ <span>{(role.label as Readonly<Record<string, string>>)[locale] ?? name}</span>
65
+ </span>
66
+ }
67
+ held={role.permissions === '*' ? ['*'] : role.permissions}
68
+ >
69
+ <div className="grid gap-2">
66
70
  <p className="text-muted-foreground text-sm">{t('dashboard.rolesFixedAbout', 'This role ships with the project. What it holds is code, so it cannot be edited or deleted here.')}</p>
67
71
  <PermissionBadges held={role.permissions === '*' ? ['*'] : role.permissions} />
68
72
  </div>
69
- </details>
73
+ </Grants>
70
74
  </TableCell>
71
- <TableCell>
75
+ <TableCell className="align-top">
72
76
  <Badge variant="secondary">{t('dashboard.rolesFixed', 'Fixed')}</Badge>
73
77
  </TableCell>
74
78
  </TableRow>
75
79
  ))}
76
80
  {page.docs.map((row) => {
77
81
  const id = String(row.id)
82
+ const label = labelOf(row, locale)
83
+ const open = (
84
+ <a href={href(id)} className="text-primary justify-self-start text-sm underline underline-offset-4 max-sm:inline-flex max-sm:min-h-11 max-sm:items-center">
85
+ {(t('dashboard.rolesOpen', 'Open {role}') ?? 'Open {role}').replace('{role}', () => label)}
86
+ </a>
87
+ )
78
88
  return (
79
89
  <TableRow key={id} data-row={id} data-fixed="false">
80
- <TableCell>
81
- <a href={href(id)} className="flex items-center gap-1.5 font-medium underline-offset-4 hover:underline max-sm:min-h-11">
82
- <span>{labelOf(row, locale)}</span>
83
- </a>
90
+ <TableCell className="whitespace-normal">
91
+ {/* A role whose keys the viewer may not read comes without them, so it is named and linked and counts nothing. */}
92
+ {Array.isArray(row.permissions) ? (
93
+ <Grants label={<span className="font-medium">{label}</span>} held={row.permissions as string[]}>
94
+ <div className="grid gap-2">
95
+ {open}
96
+ <PermissionBadges held={row.permissions as string[]} />
97
+ </div>
98
+ </Grants>
99
+ ) : (
100
+ <div className="grid gap-1">
101
+ <span className="font-medium">{label}</span>
102
+ {open}
103
+ </div>
104
+ )}
84
105
  </TableCell>
85
- <TableCell>
106
+ <TableCell className="align-top">
86
107
  <Badge variant="outline">{t('dashboard.rolesCustom', 'Custom')}</Badge>
87
108
  </TableCell>
88
109
  </TableRow>
@@ -103,6 +124,9 @@ export function Permissions({ value }: Pick<CellProps, 'value'>) {
103
124
  return <PermissionBadges held={Array.isArray(value) ? (value as string[]) : []} />
104
125
  }
105
126
 
127
+ /** A page of rows as the client answers it, read untyped since a feature never imports the asas it is built into. */
128
+ type Found = { docs?: Record<string, unknown>[] } | undefined
129
+
106
130
  /**
107
131
  * A custom role's delete, the danger zone's button. A role any member holds is not deleted, so the
108
132
  * button reads who holds it first: while anyone does, the dialog names them, each linking to their
@@ -111,7 +135,7 @@ export function Permissions({ value }: Pick<CellProps, 'value'>) {
111
135
  */
112
136
  export function DeleteRole({ run, busy }: ButtonProps) {
113
137
  const record = useRecord()
114
- const client = useClient<typeof asas.router>()
138
+ const client = useClient()
115
139
  // Read at the build's most rows one served read answers, since a page past
116
140
  // it is a 400 the catch below would read as no holders at all.
117
141
  const { path, maxLimit } = useScreen()
@@ -119,12 +143,12 @@ export function DeleteRole({ run, busy }: ButtonProps) {
119
143
  const [open, setOpen] = useState(false)
120
144
  const [holders, setHolders] = useState<{ id: string; name: string }[]>([])
121
145
  const ask = async () => {
122
- const held = await client({ kayan: 'memberRoles', operation: 'find', input: { where: { role: { equals: record?.name as string } }, limit: maxLimit, depth: 0 } }).catch(() => undefined)
123
- const ids = held?.docs?.map((row) => row.user) ?? []
124
- const named = ids.length ? await client({ kayan: 'user', operation: 'find', input: { where: { id: { in: ids } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) : undefined
146
+ const held = await client({ kayan: 'memberRoles', operation: 'find', input: { where: { role: { equals: record?.name as string } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) as Found
147
+ const ids = held?.docs?.map((row) => String(row.user)) ?? []
148
+ const named = ids.length ? await client({ kayan: 'user', operation: 'find', input: { where: { id: { in: ids } }, limit: maxLimit, depth: 0 } }).catch(() => undefined) as Found : undefined
125
149
  // A holder whose name the viewer may not read is "a member", never a raw id, and the link still
126
150
  // opens their roles.
127
- setHolders(ids.map((id) => ({ id, name: named?.docs?.find((one) => one.id === id)?.name ?? t('dashboard.rolesHolderUnnamed', 'A member') ?? '' })))
151
+ setHolders(ids.map((id) => ({ id, name: (named?.docs?.find((one) => one.id === id)?.name as string | undefined) ?? t('dashboard.rolesHolderUnnamed', 'A member') ?? '' })))
128
152
  setOpen(true)
129
153
  }
130
154
  return (
@@ -0,0 +1,3 @@
1
+ import { settings } from './settings/settings.ts'
2
+
3
+ export const global = [settings] as const
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@asas/settings",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@asas/mithaq": "*",
11
+ "mithaq": "*"
12
+ }
13
+ }
@@ -1,7 +1,7 @@
1
1
  import { defaultFields, defaultGlobalFields, defaultGlobalOperations } from 'mithaq'
2
- import { defineGlobal, organization, rows } from '../mithaq.ts'
3
- import { platformClosed, tenantAccess } from '../tenant-access.ts'
4
- import { appPage } from '../pages.ts'
2
+ import { defineGlobal, organization, rows } from '@asas/mithaq'
3
+ import { backofficeClosed, tenantAccess } from '@asas/mithaq/roles/tenant-access.ts'
4
+ import { appPage } from '@asas/mithaq/mithaq/pages.ts'
5
5
 
6
6
  /**
7
7
  * The project's settings, one row per tenant, because this global places
@@ -20,31 +20,26 @@ export const settings = defineGlobal({
20
20
  fields: {
21
21
  ...defaultFields,
22
22
  organization: { ...organization, rules: { ...organization.rules, unique: true } },
23
- siteTitle: {
24
- kind: 'text',
25
- label: { en: 'Site title', ar: 'عنوان الموقع' },
26
- // Trimmed, so a title of spaces is refused as required.
27
- rules: { required: true, trim: true, maxLength: 120 },
28
- },
23
+ // The site has no name of its own: it is the organization's, in every locale.
29
24
  maintenance: {
30
25
  kind: 'checkbox',
31
26
  label: { en: 'Maintenance', ar: 'الصيانة' },
32
27
  description: {
33
- en: 'While this is on, the site tells visitors it is closed for maintenance.',
34
- ar: 'ما دام هذا مفعلا، يخبر الموقع زواره بأنه مغلق للصيانة.',
28
+ en: 'While this is on, the portal tells students it is closed for maintenance. Staff keep working.',
29
+ ar: 'ما دام هذا مفعلا، تخبر البوابة الطلاب بأنها مغلقة للصيانة. ويواصل الموظفون عملهم.',
35
30
  },
36
31
  },
37
- // Where a visitor writes to, the one address the project shows.
32
+ // Where a student writes to, the one address the portal shows: at its foot, and on its maintenance page.
38
33
  contactEmail: { kind: 'email', label: { en: 'Contact email', ar: 'بريد التواصل' } },
39
34
  // The column default is the one the table was made with, so the committed
40
35
  // migrations still describe it.
41
36
  globalType: { ...defaultGlobalFields.globalType, rules: { required: true, defaultValue: 'settings' } },
42
37
  },
43
- // Its own rows: the language's, and every platform role closed. The platform
38
+ // Its own rows: the language's, and every backoffice role closed. The backoffice
44
39
  // lists no global, since a list across tenants reads by `find` and a global
45
40
  // declares none, so it reads none, 403 `error.forbidden.read`. A global takes
46
41
  // read and update alone, and the other two the rows compile are never reached.
47
- access: rows({ ...tenantAccess, ...platformClosed }) as Pick<ReturnType<typeof rows>, 'read' | 'update'>,
42
+ access: rows({ ...tenantAccess, ...backofficeClosed }) as Pick<ReturnType<typeof rows>, 'read' | 'update'>,
48
43
  targets: {
49
44
  service: { operations: defaultGlobalOperations },
50
45
  // A global is one row, so its page is a custom one with no id: its form
@@ -52,14 +47,14 @@ export const settings = defineGlobal({
52
47
  // yes or no setting a switch with its label beside it.
53
48
  dashboard: {
54
49
  description: {
55
- en: 'How the site presents itself, and whether it is open to visitors.',
56
- ar: 'كيف يظهر الموقع، وهل هو مفتوح للزوار.',
50
+ en: 'Where students reach the school, and whether the portal is open to them.',
51
+ ar: 'أين يتواصل الطلاب مع المدرسة، وهل البوابة مفتوحة لهم.',
57
52
  },
58
53
  pages: {
59
54
  settings: {
60
55
  path: '/settings',
61
56
  page: appPage([{ layout: 'record', main: [{ form: [
62
- { section: 'site', label: { en: 'Site', ar: 'الموقع' }, fields: ['siteTitle', 'contactEmail'] },
57
+ { section: 'contact', label: { en: 'Contact', ar: 'التواصل' }, fields: ['contactEmail'] },
63
58
  { section: 'availability', label: { en: 'Availability', ar: 'الإتاحة' }, fields: [{ field: 'maintenance', component: 'switch' }] },
64
59
  ] }] }]),
65
60
  },
@@ -0,0 +1,3 @@
1
+ import { tags } from './tags/tags.ts'
2
+
3
+ export const kayan = [tags] as const
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@asas/tags",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@asas/mithaq": "*",
11
+ "mithaq": "*",
12
+ "zod": "^4.6.5"
13
+ }
14
+ }
@@ -1,8 +1,8 @@
1
- import { defaultFields, defaultOperations } from 'mithaq'
1
+ import { defaultFields } from 'mithaq'
2
2
  import { z } from 'zod'
3
- import { defineKayan, organization } from '../mithaq.ts'
4
- import { appPage, danger, platformList, platformRecord } from '../pages.ts'
5
- import { expectedUpdatedAt } from '../steps/expected-updated-at.ts'
3
+ import { defineKayan, organization } from '@asas/mithaq'
4
+ import { appPage, danger, backofficeList, backofficeRecord } from '@asas/mithaq/mithaq/pages.ts'
5
+ import { expectedUpdatedAt } from '@asas/mithaq/steps/expected-updated-at.ts'
6
6
 
7
7
  const tag = [{ section: 'tag', label: { en: 'Tag', ar: 'الوسم' }, fields: ['name'] }] as const
8
8
 
@@ -21,13 +21,13 @@ export const tags = defineKayan({
21
21
  // What the picker on `courses.tags` shows for each row, and what its search
22
22
  // runs a `like` over. Without it every tag is a uuid.
23
23
  targets: {
24
- // Every organization's rows on the platform, read-only, and each row's
24
+ // Every organization's rows on the backoffice, read-only, and each row's
25
25
  // record, which only reads.
26
- platform: {
26
+ backoffice: {
27
27
  useAsTitle: 'name',
28
28
  pages: {
29
- ...platformList('/tags', { columns: ['name', 'organization', 'createdAt'], filters: ['organization'], search: ['name'] }),
30
- ...platformRecord({ path: '/tags/:id', main: [{ section: 'tag', label: { en: 'Tag', ar: 'الوسم' }, fields: ['name'] }] }),
29
+ ...backofficeList('/tags', { columns: ['name', 'organization', 'createdAt'], filters: ['organization'], search: ['name'] }),
30
+ ...backofficeRecord({ path: '/tags/:id', main: [{ section: 'tag', label: { en: 'Tag', ar: 'الوسم' }, fields: ['name'] }] }),
31
31
  },
32
32
  },
33
33
  // A create opens as a sheet over the list. The name is copy a screen shows
@@ -36,20 +36,20 @@ export const tags = defineKayan({
36
36
  dashboard: {
37
37
  useAsTitle: 'name',
38
38
  description: {
39
- en: 'Labels that group courses in the catalogue.',
40
- ar: 'تسميات تجمع الدورات في الدليل.',
39
+ en: 'Labels the organization groups its records by.',
40
+ ar: 'تسميات تجمع بها المؤسسة سجلاتها.',
41
41
  },
42
42
  pages: {
43
43
  list: { path: '/tags', page: appPage([{ layout: 'table', main: [{ list: { columns: ['name', 'createdAt'], search: ['name'] } }] }]) },
44
- // The sheet's title says it makes a tag, so its one box is drawn
44
+ // The drawer's title says it makes a tag, so its one box is drawn
45
45
  // bare rather than under a second heading that says "Tag" again.
46
- create: { path: '/tags/new', mode: 'sheet', page: appPage([{ layout: 'record', main: [{ form: ['name'] }] }]) },
46
+ create: { path: '/tags/new', mode: 'drawer', page: appPage([{ layout: 'record', main: [{ form: ['name'] }] }]) },
47
47
  edit: { path: '/tags/:id', page: appPage([{ layout: 'record', main: [{ form: tag }], danger: danger() }]) },
48
48
  },
49
49
  },
50
50
  service: {
51
- operations: {
52
- ...defaultOperations,
51
+ operations: ({ defaults }) => ({
52
+ ...defaults,
53
53
  /**
54
54
  * The shipped update list with the `expectedUpdatedAt` step before it and
55
55
  * its input widened by the key the step reads. A caller who sends the
@@ -63,7 +63,7 @@ export const tags = defineKayan({
63
63
  input: ({ update }) => update.extend({ expectedUpdatedAt: z.date().optional() }),
64
64
  steps: ({ steps }) => [expectedUpdatedAt, steps.update],
65
65
  },
66
- },
66
+ }),
67
67
  },
68
68
  },
69
69
  fields: {
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@asas/mithaq",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/mithaq.ts",
8
+ "./auth": "./src/mithaq/auth/index.ts",
9
+ "./auth/backoffice-roles": "./src/mithaq/auth/backoffice-roles.ts",
10
+ "./auth/organizations": "./src/mithaq/auth/organizations.ts",
11
+ "./*": "./src/*"
12
+ },
13
+ "dependencies": {
14
+ "@better-auth/core": "1.7.4",
15
+ "@lexical/extension": "0.52.0",
16
+ "@lexical/link": "0.52.0",
17
+ "@lexical/list": "0.52.0",
18
+ "@lexical/rich-text": "0.52.0",
19
+ "aws4fetch": "1.0.20",
20
+ "better-auth": "1.7.4",
21
+ "file-type": "22.0.1",
22
+ "image-dimensions": "2.5.1",
23
+ "lexical": "0.52.0",
24
+ "mithaq": "*",
25
+ "mithaq-admin": "*"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^19.3.0",
29
+ "lucide-react": "^1.16.0",
30
+ "react": "^19.3.0",
31
+ "zod": "^4.6.5"
32
+ }
33
+ }
@@ -0,0 +1,32 @@
1
+ // Which mailer this project sends with. `mailers.ts` beside this file is copied from the
2
+ // `mithaq-mail` registry item and owned by the project; a project on another service writes its
3
+ // adapter there and names it here. The auth setup sends the password reset, the invitation and the
4
+ // email verification through it, and refuses an open sign-up in production while none is configured.
5
+ import { defaultResendBaseURL, resendMailer, type Mailer } from './mailers.ts'
6
+
7
+ export { logMailer, type Mail, type Mailer } from './mailers.ts'
8
+
9
+ /**
10
+ * Resend when both `RESEND_API_KEY` and `MAIL_FROM` are set, none when neither is. Half the settings
11
+ * is refused, so a typo never leaves a deploy sending nothing while it seems configured.
12
+ * `RESEND_BASE_URL`, Resend's own SDK variable, points it at another address that speaks its API.
13
+ */
14
+ export const mailerFromEnv = (env: Record<string, string | undefined>): Mailer | undefined => {
15
+ const { RESEND_API_KEY: apiKey, MAIL_FROM: from } = env
16
+ if (!apiKey && !from) return undefined
17
+ if (!apiKey) throw new Error('MAIL_FROM is set and RESEND_API_KEY is not. Set both, or neither.')
18
+ if (!from) throw new Error('RESEND_API_KEY is set and MAIL_FROM is not: set MAIL_FROM to an address at a domain your Resend account verified.')
19
+ const baseURL = env.RESEND_BASE_URL || defaultResendBaseURL
20
+ // The key and every letter cross to it, so in the clear only to this machine, such as a test's catcher.
21
+ const at = URL.canParse(baseURL) ? new URL(baseURL) : undefined
22
+ if (!at || (at.protocol !== 'https:' && !(at.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(at.hostname)))) {
23
+ throw new Error(`RESEND_BASE_URL is ${JSON.stringify(baseURL)}: set an https address, or http on this machine.`)
24
+ }
25
+ return resendMailer({ apiKey, from, baseURL })
26
+ }
27
+
28
+ /**
29
+ * The mailer this deploy configured, or none; the auth setup logs mail in development without one.
30
+ * The auth setup asks it as it loads, so every load reads the environment it is started with.
31
+ */
32
+ export const configuredMailer = (): Mailer | undefined => mailerFromEnv(process.env)