create-mithaq 0.1.1 → 0.2.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 (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 +1094 -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
@@ -7,17 +7,27 @@ import { ScreenContext, useScreen, type ScreenState } from 'mithaq/admin/react'
7
7
  import { actingAs, arm, authArm, caused, clientOf, readsOf, takesKey } from './api.ts'
8
8
  import { clampedHref, routedHref } from './links.ts'
9
9
  import { mergedHeaders } from './request-headers.ts'
10
+ import { nameIn } from 'asas/organizations'
10
11
  import { documentHead, localeOf, locales, translator, type Locale } from './copy.ts'
11
- import { FieldValue, type FormClient, type FormProps } from './components/mithaq/form.tsx'
12
- import type { Page } from './components/mithaq/list.tsx'
13
- import { QueryRefused, findOf, listRaw, prefixOf, readQuery, rulesOf, scopedTo, withList, type RawQuery } from './components/mithaq/list-query.ts'
14
- import { createsIn, newTitleOf, titleOf } from './components/mithaq/controls.tsx'
15
- import { NavLinks, NodeButton, OwnButton, RecordFrame, formIn, holdFocus, frameOf, listIn, nodesOf, pageButtonOf, pageOf, readsAsValues, refOf, relatedIn, rowTitle, scopeOf, type Draw, type Refs, type TreePage } from './components/mithaq/nodes.tsx'
16
- import { idsOf } from './components/mithaq/relationship-control.tsx'
12
+ import { FieldValue } from 'mithaq-admin/field-value.tsx'
13
+ import type { FormClient, FormProps } from 'mithaq-admin/form-types.tsx'
14
+ import type { Page } from 'mithaq-admin/list-types.tsx'
15
+ import { QueryRefused, findOf, listRaw, prefixOf, readQuery, rulesOf, scopedTo, withList, type RawQuery } from 'mithaq-admin/list-query.ts'
16
+ import { newTitleOf, titleOf } from 'mithaq-admin/copy.ts'
17
+ import { createsIn } from 'mithaq-admin/field-values.ts'
18
+ import { NavLinks } from 'mithaq-admin/nav-links.tsx'
19
+ import { NodeButton, OwnButton, pageButtonOf } from 'mithaq-admin/node-buttons.tsx'
20
+ import { formIn, listIn, nodesOf, readsAsValues, relatedIn, scopeOf, type Draw } from 'mithaq-admin/node-tree.ts'
21
+ import { frameOf, pageOf } from 'mithaq-admin/nodes.tsx'
22
+ import { RecordFrame, holdFocus } from 'mithaq-admin/record-frame.tsx'
23
+ import { rowTitle } from 'mithaq-admin/row-title.tsx'
24
+ import { refOf, type Refs, type TreePage } from 'mithaq-admin/screen-refs.ts'
25
+ import { idsOf } from 'mithaq-admin/relationship-control.tsx'
17
26
  import { ScreenForm } from './screen-form.tsx'
18
27
  import { ScreenList, askedFor, readsDrafts, rowReadsDrafts, rowsKey } from './screen-list.tsx'
19
28
  import { screenNamed } from './screen-names.ts'
20
29
  import { slide } from './slide.ts'
30
+ import { Toaster } from 'mithaq-admin/toast.tsx'
21
31
  import { useCopy } from './routes/__root.tsx'
22
32
 
23
33
  /**
@@ -29,13 +39,13 @@ export const getAsas = createServerOnlyFn(async () => (await import('asas')).asa
29
39
  type InProcess = Record<string, { operations: Record<string, (input: object, options: object) => Promise<unknown>> }>
30
40
 
31
41
  /**
32
- * Each kayan's operations in process. In the platform app a read of a kayan asks `tenant: 'all'`,
33
- * which the runner grants platform staff there alone, so staff read every organization's rows; a
42
+ * Each kayan's operations in process. In the backoffice app a read of a kayan asks `tenant: 'all'`,
43
+ * which the runner grants backoffice staff there alone, so staff read every organization's rows; a
34
44
  * global is one organization's row and asks for none, and a write never asks.
35
45
  */
36
46
  const inProcess = async (asas: Awaited<ReturnType<typeof getAsas>>): Promise<InProcess> => {
37
47
  const kayan = asas.kayan as unknown as InProcess
38
- if (!(await import('asas')).onPlatform()) return kayan
48
+ if (!(await import('asas')).onBackoffice()) return kayan
39
49
  const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `${one.kayan}/${one.name}`))
40
50
  return Object.fromEntries(Object.entries(kayan).map(([name, one]) => [name, {
41
51
  operations: Object.fromEntries(Object.entries(one.operations).map(([operation, run]) =>
@@ -117,7 +127,7 @@ type Actor = {
117
127
  readonly email?: string | null
118
128
  readonly impersonatedBy?: unknown
119
129
  readonly activeOrganizationId?: string | null
120
- readonly organizations?: readonly { readonly id: string; readonly name: string; readonly slug?: string }[]
130
+ readonly organizations?: readonly { readonly id: string; readonly name: string; readonly slug?: string; readonly names?: unknown }[]
121
131
  }
122
132
 
123
133
  // `'unauthorized'` is answered before the projection is read, so a refused screen sends no kayan, field, row or page;
@@ -140,15 +150,15 @@ const resolveScreen = async (asked: unknown) => {
140
150
  .filter(([one]) => report.collections?.[one]?.read === true)
141
151
  .map(([one, value]) => [one, { ...value, fields: value.fields.filter((field) => report.collections?.[one]?.fields?.[field.name]?.read === true) }]),
142
152
  )
143
- const { onPlatform, platformEntry, PLATFORM_SCREEN } = await import('asas')
144
- const platform = platformEntry(actor)
145
- // The other screens of this app the actor may enter. The platform's own name
146
- // is left out of a tenant app's list, so the one link to it is `platformEntry`'s alone.
153
+ const { onBackoffice, backofficeEntry, BACKOFFICE_SCREEN } = await import('asas')
154
+ const backoffice = backofficeEntry(actor)
155
+ // The other screens of this app the actor may enter. The backoffice's own name
156
+ // is left out of a tenant app's list, so the one link to it is `backofficeEntry`'s alone.
147
157
  const served = screensOf(asas.targets) as Record<string, { client: ScreenClient }>
148
158
  const siblings = (report.screens ?? [])
149
- .filter((one) => one !== name && Object.hasOwn(served, one) && (one === PLATFORM_SCREEN) === onPlatform())
159
+ .filter((one) => one !== name && Object.hasOwn(served, one) && (one === BACKOFFICE_SCREEN) === onBackoffice())
150
160
  .map((one) => ({ name: one, path: served[one]!.client.path }))
151
- const others = [...siblings, ...(platform ? [{ name: PLATFORM_SCREEN, path: platform }] : [])]
161
+ const others = [...siblings, ...(backoffice ? [{ name: BACKOFFICE_SCREEN, path: backoffice }] : [])]
152
162
  return {
153
163
  asas,
154
164
  request,
@@ -164,7 +174,8 @@ const resolveScreen = async (asked: unknown) => {
164
174
  ...(actor.impersonatedBy ? { impersonating: true } : {}),
165
175
  ...(others.length ? { screens: others } : {}),
166
176
  ...(actor.organizations && {
167
- organizations: actor.organizations.map(({ id, name, slug }) => ({ id, name, slug })),
177
+ // Every locale's, because the frame is read before the viewer's locale is known here; the shell picks one.
178
+ organizations: actor.organizations.map(({ id, name, slug, names }) => ({ id, name, slug, ...(typeof names === 'string' && { names }) })),
168
179
  activeOrganizationId: actor.activeOrganizationId ?? null,
169
180
  }),
170
181
  },
@@ -206,14 +217,14 @@ export const loadEnterable = createServerFn({ method: 'GET' })
206
217
  others?: { id: string; name: string; slug?: string }[]
207
218
  impersonating?: string
208
219
  } | null> => {
209
- const [{ getRequest }, asas, { onPlatform, PLATFORM_SCREEN }] = await Promise.all([import('@tanstack/react-start/server'), getAsas(), import('asas')])
220
+ const [{ getRequest }, asas, { onBackoffice, BACKOFFICE_SCREEN }] = await Promise.all([import('@tanstack/react-start/server'), getAsas(), import('asas')])
210
221
  const request = getRequest()
211
222
  const actor = (await asas.auth.actor(request.headers)) as Actor | null
212
223
  if (!actor?.email) return null
213
224
  const report = (await (await asas.handler(new Request(new URL('/api/access', request.url), { headers: request.headers }))).json()) as AccessReport
214
225
  const screens = screensOf(asas.targets) as Record<string, { client: ScreenClient }>
215
226
  const entered = (report.screens ?? [])
216
- .filter((name) => Object.hasOwn(screens, name) && (name === PLATFORM_SCREEN) === onPlatform())
227
+ .filter((name) => Object.hasOwn(screens, name) && (name === BACKOFFICE_SCREEN) === onBackoffice())
217
228
  .map((name) => ({ name, path: screens[name]!.client.path }))
218
229
  const impersonating = actor.impersonatedBy ? { impersonating: actor.name ?? actor.email } : {}
219
230
  if (entered.length) {
@@ -226,11 +237,14 @@ export const loadEnterable = createServerFn({ method: 'GET' })
226
237
  }
227
238
  const memberships = actor.organizations ?? []
228
239
  const active = memberships.find((one) => one.id === actor.activeOrganizationId)
240
+ // Named here, where the locale is known: the page draws what it is handed, and a name the viewer cannot read
241
+ // in the default locale would be no name to them.
242
+ const named = ({ id, name, slug, names }: NonNullable<Actor['organizations']>[number]) => ({ id, name: nameIn({ name, names }, data.locale), slug })
229
243
  return {
230
244
  screens: entered,
231
245
  email: actor.email,
232
- organizations: (active ? [active] : memberships).map(({ id, name, slug }) => ({ id, name, slug })),
233
- others: onPlatform() ? [] : memberships.filter((one) => one !== active).map(({ id, name, slug }) => ({ id, name, slug })),
246
+ organizations: (active ? [active] : memberships).map(named),
247
+ others: onBackoffice() ? [] : memberships.filter((one) => one !== active).map(named),
234
248
  ...impersonating,
235
249
  }
236
250
  })
@@ -473,7 +487,8 @@ const nameOfPage = ({ screen, name, match, row, titledBy, copy }: {
473
487
  if (match.kayan === undefined) return screen.nav.find((entry) => !entry.kayan && entry.page === match.page)?.label?.[copy.locale] ?? copy.t(`screens.${name}`, name) ?? name
474
488
  const kayan = screen.kayan[match.kayan]!
475
489
  const page = kayan.pages[match.page]
476
- const many = !!page && 'page' in page && listIn(nodesOf(page)) !== undefined
490
+ // A list page draws every row, whether a tree's list node or a component drawing it whole.
491
+ const many = match.page === 'list' || (!!page && 'page' in page && listIn(nodesOf(page)) !== undefined)
477
492
  if (match.page === 'create') return newTitleOf({ kayan, copy })
478
493
  const field = kayan.fields.find((one) => one.name === kayan.useAsTitle)
479
494
  const target = field?.kind === 'relationship' ? screen.kayan[field.relationTo ?? ''] : undefined
@@ -500,7 +515,7 @@ const useScreenState = (data: ScreenData): ScreenState => {
500
515
 
501
516
  /**
502
517
  * The bar that says the session is an impersonation, with the way out. Stopping
503
- * is a sign-out: the staff member's own session lives in the platform app under
518
+ * is a sign-out: the staff member's own session lives in the backoffice app under
504
519
  * that app's cookie, and the sign-out answers where they go next.
505
520
  */
506
521
  export function ImpersonationBar({ name }: { name: string }): ReactElement {
@@ -544,6 +559,7 @@ function ScreenBar({ user }: { user: ScreenData['user'] }): ReactElement | null
544
559
 
545
560
  /** The screen's own frame: its providers, outermost first, around the frame's bar and the page. */
546
561
  export function ScreenFrame({ files, data, children }: { files: Files; data: ScreenData; children: ReactNode }): ReactElement {
562
+ const { t } = useCopy()
547
563
  const refs = refsOf(files, data.screen)
548
564
  // One per frame: made per request on the server, so no row one actor read
549
565
  // reaches another's render; held for the frame's life in the browser.
@@ -590,6 +606,8 @@ export function ScreenFrame({ files, data, children }: { files: Files; data: Scr
590
606
  <>
591
607
  <ScreenBar user={data.user} />
592
608
  {children}
609
+ {/* The one place every write's toast is drawn, kept across the screen's pages. */}
610
+ <Toaster closeLabel={t('admin.close', 'Close') ?? 'Close'} />
593
611
  </>,
594
612
  )}
595
613
  </Suspense>
@@ -666,7 +684,7 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
666
684
  return path ? `${base}${id ? path.replace(':id', encodeURIComponent(id)) : path}` : undefined
667
685
  }
668
686
  // A row opens its kayan's edit page, else the one page whose path draws a
669
- // row (how the platform opens a record it only reads).
687
+ // row (how the backoffice opens a record it only reads).
670
688
  const rowPage = (target: ClientKayan) => (target.pages.edit ? 'edit' : Object.keys(target.pages).find((one) => target.pages[one]!.path.includes(':id')))
671
689
  const related = relatedOf(data.screen, kayan)
672
690
  const go = (href: string | undefined) => void (href ? router.navigate({ href }) : router.invalidate())
@@ -844,7 +862,9 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
844
862
  api={readsOf(related)}
845
863
  operations={data.operations}
846
864
  related={draw(page).list}
847
- edit={(id) => hrefOf(kayan, 'edit', id)}
865
+ // A create opened over its list closes back to it once it lands, the new row in the list, as Payload's drawer does;
866
+ // one whose kayan autosaves its drafts moves to the row's page instead, since its first autosave is a create too.
867
+ edit={(id) => (page.mode && listHref && !data.drafts?.autosave ? listHref : hrefOf(kayan, 'edit', id))}
848
868
  list={listHref}
849
869
  drafts={data.drafts}
850
870
  expectedUpdatedAt={data.expectedUpdatedAt}
@@ -865,4 +885,4 @@ function PageBody({ files, data, query, onQuery }: PageViewProps): ReactElement
865
885
  }
866
886
  return form
867
887
  }
868
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 d96a3dd603b5aead8e3d82e8792fa40d5f4dac1bfbee2e7064ab2be888b595a6
888
+ // mithaq-admin 0.0.0 da2e742262e16a7f9f8dab050c2a4201517478795101af68736907464a0d52ce
@@ -0,0 +1,30 @@
1
+ import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
2
+ import type { ScreenClient } from 'mithaq/admin'
3
+ import { checkComponents } from 'mithaq-admin/components.tsx'
4
+ import { components } from './components.ts'
5
+ import { withQuietAbort } from '../../asas-tanstack/src/quiet-abort.ts'
6
+
7
+ // Every screen this app serves, checked against its components map as it starts, so a field whose kind has no
8
+ // control fails the start, naming the kind and the field, and never reaches a page. Read on the server alone,
9
+ // since a screen.json names every field of the screen.
10
+ for (const client of Object.values(import.meta.glob<ScreenClient>('./routes/(screens)/*/screen.json', { eager: true, import: 'default' }))) {
11
+ checkComponents({ client, components })
12
+ }
13
+
14
+ // A refused start (a migration not applied) exits 1 rather than leave a server up that cannot serve.
15
+ const { env } = process
16
+ if (env.NODE_ENV === 'production') {
17
+ await (await import('asas')).asas.connect().catch((error: unknown) => {
18
+ console.error(error)
19
+ process.exit(1)
20
+ })
21
+ }
22
+
23
+ // Every request runs as the backoffice; nothing a request carries decides it, and the tenant app never does.
24
+ // `asas` is imported per request so it is the same module every route reads.
25
+ export default createServerEntry({
26
+ fetch: async (request: Request) => {
27
+ const { asBackoffice } = await import('asas')
28
+ return asBackoffice(() => handler.fetch(withQuietAbort(request)))
29
+ },
30
+ })
@@ -19,4 +19,4 @@ export const slide = async ({
19
19
  // Set once with every strategy's cookies: a second set replaces the first.
20
20
  if (cookies.length) setResponseHeader('set-cookie', cookies)
21
21
  }
22
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 bc0221e1088103f8638e0333b84aafb6b326e3991a187ced198bce2d090ecbe9
22
+ // mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 bc0221e1088103f8638e0333b84aafb6b326e3991a187ced198bce2d090ecbe9
@@ -21,7 +21,7 @@ const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'server
21
21
 
22
22
  // The location is a path of this origin, so nothing the request's host header says reaches it.
23
23
  const home = createMiddleware({ type: 'request' }).server(({ request, next }) =>
24
- new URL(request.url).pathname === '/' ? new Response(null, { status: 307, headers: { location: '/platform' } }) : next(),
24
+ new URL(request.url).pathname === '/' ? new Response(null, { status: 307, headers: { location: '/backoffice' } }) : next(),
25
25
  )
26
26
 
27
27
  // Only this origin may frame the pages, so no other site can lay this app under its own page and steer a click.
@@ -10,12 +10,14 @@
10
10
  for it, listed first in `--font-sans` below so an Arabic page reads in it and
11
11
  a Latin one falls through to the preset's. */
12
12
  @import "@fontsource/ibm-plex-sans-arabic";
13
+ /* The admin's pieces in node_modules, which Tailwind never scans on its own. */
14
+ @import "mithaq-admin/source.css";
13
15
 
14
- /* The project's own shells, pages and components, which the platform screen
15
- loads through the linked package, as the tenant app lists them
16
- (`apps/asas-tanstack/src/styles.css`). Tailwind finds this app's own files,
17
- the copied `src/components/mithaq` among them, without one. */
18
- @source "../node_modules/asas/src";
16
+ /* The project's own shells, pages and components, which the backoffice screen
17
+ loads, as the tenant app lists them (`apps/asas-tanstack/src/styles.css`).
18
+ Tailwind finds this app's own files, an ejected `src/components/mithaq`
19
+ among them, without one. */
20
+ @source "../../../templates";
19
21
 
20
22
  @custom-variant dark (&:is(.dark *));
21
23
 
@@ -143,7 +145,7 @@
143
145
  @apply font-sans;
144
146
  }
145
147
  }
146
- /* What the platform draws beyond the preset, each a rule over a marker the
148
+ /* What the backoffice draws beyond the preset, each a rule over a marker the
147
149
  shared pieces already carry, so the tenant app, which does not load this
148
150
  file, is unchanged. */
149
151
 
@@ -188,7 +190,7 @@ a[data-row-link]:empty::before,
188
190
  color: var(--muted-foreground);
189
191
  }
190
192
 
191
- /* The platform's cards are square, as its buttons, inputs and home cards are:
193
+ /* The backoffice's cards are square, as its buttons, inputs and home cards are:
192
194
  the section and form cards the registry draws with a rounded class of their
193
195
  own, the frame a list's table sits in, the filter panel above it and the
194
196
  danger region of a record page. */
@@ -13,7 +13,7 @@
13
13
  "allowImportingTsExtensions": true,
14
14
  "resolveJsonModule": true,
15
15
  "types": ["node", "vite/client"],
16
- "paths": { "@/*": ["./src/*"] }
16
+ "paths": { "@/*": ["./src/*"], "mithaq-admin/*": ["./src/components/mithaq/*", "./node_modules/mithaq-admin/registry/*"] }
17
17
  },
18
- "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "../../templates/asas/src/ui/*.tsx", "../../templates/asas/src/ui-*/*.tsx"]
18
+ "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "../../templates/mithaq/src/ui/*.tsx", "../../templates/asas/src/ui-*/*.tsx"]
19
19
  }
@@ -1,19 +1,25 @@
1
1
  import tailwindcss from '@tailwindcss/vite'
2
2
  import { tanstackStart } from '@tanstack/react-start/plugin/vite'
3
3
  import viteReact from '@vitejs/plugin-react'
4
+ import { mithaqAdmin } from 'mithaq/vite'
4
5
  import { defineConfig } from 'vite'
5
6
  import { fileURLToPath } from 'node:url'
6
7
 
7
8
  export default defineConfig({
8
- // An alias rather than `resolve.tsconfigPaths`: the shells in `templates/asas/src` fall under a tsconfig
9
+ // An alias rather than `resolve.tsconfigPaths`: the shells in the language package's `src/ui/` fall under a tsconfig
9
10
  // with no `@/` and still import it. `^@/` leaves `@tanstack/*` alone.
10
11
  resolve: { alias: [{ find: /^@\//, replacement: fileURLToPath(new URL('./src/', import.meta.url)) }] },
11
12
  plugins: [
12
13
  tailwindcss(),
14
+ // A file under this folder takes the place of mithaq-admin's file of the same name, for every importer.
15
+ mithaqAdmin(fileURLToPath(new URL('./src/components/mithaq/', import.meta.url))),
13
16
  // `mithaq generate screens` writes `files.ts` and `types.ts` beside the route files; neither is a route.
14
17
  tanstackStart({ router: { routeFileIgnorePattern: '^(files|types)\\.ts$' } }),
15
18
  viteReact(),
16
19
  ],
20
+ // Behind the proxy that routes by Host: bound to the loopback address it targets, and answering
21
+ // the public name in BACKOFFICE_URL, which Vite refuses in a Host header unless named here.
22
+ preview: { host: '127.0.0.1', allowedHosts: process.env.BACKOFFICE_URL ? [new URL(process.env.BACKOFFICE_URL).hostname] : [] },
17
23
  environments: {
18
24
  ssr: {
19
25
  // `asas` and `mithaq` stay external: Vite bundles linked workspace packages by default, and bundling
@@ -12,7 +12,7 @@
12
12
  "@base-ui/react": "^1.5.0",
13
13
  "@fontsource-variable/raleway": "^5.2.8",
14
14
  "@fontsource/ibm-plex-sans-arabic": "^5.2.8",
15
- "@hugeicons/core-free-icons": "^4.1.4",
15
+ "@hugeicons/core-free-icons": "4.3.5",
16
16
  "@hugeicons/react": "^1.1.6",
17
17
  "@lexical/extension": "0.52.0",
18
18
  "@lexical/link": "0.52.0",
@@ -34,9 +34,11 @@
34
34
  "lexical": "0.52.0",
35
35
  "lucide-react": "^1.16.0",
36
36
  "mithaq": "*",
37
+ "mithaq-admin": "*",
37
38
  "react": "^19.3.0",
38
39
  "react-day-picker": "^10.0.1",
39
40
  "react-dom": "^19.3.0",
41
+ "react-is": "^19.3.0",
40
42
  "react-resizable-panels": "^4.13.3",
41
43
  "recharts": "3.8.0",
42
44
  "tailwind-merge": "^3.6.0",
@@ -48,9 +50,9 @@
48
50
  "@types/react": "^19.3.0",
49
51
  "@types/react-dom": "^19.3.0",
50
52
  "@vitejs/plugin-react": "^6.0.2",
51
- "mithaq-admin": "*",
52
53
  "shadcn": "4.18.0",
53
54
  "tailwindcss": "^4.3.0",
55
+ "typescript": "^7.0.2",
54
56
  "vite": "^8.0.13"
55
57
  }
56
58
  }
@@ -144,4 +144,4 @@ export const authArm = async (path: string, body: unknown = {}) => {
144
144
  // The status too, because a rate limit's refusal carries no code to read.
145
145
  return { ok: response.ok, status: response.status, message: answered?.message, body: answered }
146
146
  }
147
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 94db2fc49ad4391c0d1c2183cb41001dd08d62017c76af86cc5f611488c22c68
147
+ // mithaq-admin 2d3ab739c92c6c2b397041b76bf4b23b2902b7a2 94db2fc49ad4391c0d1c2183cb41001dd08d62017c76af86cc5f611488c22c68
@@ -7,10 +7,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
7
7
  import { Input } from '@/components/ui/input'
8
8
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'
9
9
  import { authArm } from './api.ts'
10
- import { FieldShell, describedBy } from './components/mithaq/field-shell.tsx'
10
+ import { FieldShell, describedBy } from 'mithaq-admin/field-shell.tsx'
11
11
  import { refusalKey } from './copy.ts'
12
12
  import { LocaleSwitch } from './locale-switch.tsx'
13
- import { wanted } from './redirect.ts'
13
+ import { landing } from './redirect.ts'
14
14
  import { useCopy } from './routes/__root.tsx'
15
15
 
16
16
  /** The one search key the auth pages read, off the raw search. */
@@ -21,8 +21,8 @@ export const redirectSearch = (search: Record<string, unknown>): { redirect?: st
21
21
  export const signedInAs = async (actor: Promise<unknown>) => Boolean(((await actor.catch(() => null)) as { email?: unknown } | null)?.email)
22
22
 
23
23
  /**
24
- * Whether this request carries a session, where signing in lands for it, and the provider's password bounds.
25
- * No auth page sends a referrer, since the reset page's address carries its token.
24
+ * Whether this request carries a session, the screens it opens, the first of them as `home`, which a link back names,
25
+ * and the provider's password bounds. No auth page sends a referrer, since the reset page's address carries its token.
26
26
  */
27
27
  export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
28
28
  // Imported inside the server half so no auth page ships the screen host to the browser.
@@ -31,9 +31,9 @@ export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
31
31
  type Provider = { name: string; server: { $context: Promise<{ password: { config: { minPasswordLength: number; maxPasswordLength: number } } }> } }
32
32
  const provider = (asas.auth.strategies as unknown as Provider[]).find((one) => one.name === 'user')!
33
33
  const [signedIn, { password }] = await Promise.all([signedInAs(asas.auth.actor(getRequest().headers)), provider.server.$context])
34
- const first = signedIn ? (await loadEnterable())?.screens[0] : undefined
35
- const home: { path: string; name?: string } = first ?? { path: '/unauthorized' }
36
- return { signedIn, home, password: { min: password.config.minPasswordLength, max: password.config.maxPasswordLength } }
34
+ const entered = signedIn ? ((await loadEnterable())?.screens ?? []) : []
35
+ const home: { path: string; name?: string } = entered[0] ?? { path: '/unauthorized' }
36
+ return { signedIn, home, screens: entered, password: { min: password.config.minPasswordLength, max: password.config.maxPasswordLength } }
37
37
  })
38
38
 
39
39
  /**
@@ -42,8 +42,8 @@ export const authGate = createServerFn({ method: 'GET' }).handler(async () => {
42
42
  * route context.
43
43
  */
44
44
  export const gate = async ({ search }: { search: { redirect?: string } }) => {
45
- const { signedIn, home, password } = await authGate()
46
- if (signedIn) throw redirect({ href: wanted({ asked: search.redirect, home: home.path }) })
45
+ const { signedIn, screens, password } = await authGate()
46
+ if (signedIn) throw redirect({ href: landing({ asked: search.redirect, screens }) })
47
47
  return { password }
48
48
  }
49
49
 
@@ -57,10 +57,10 @@ export const useHomeLabel = () => {
57
57
  }
58
58
 
59
59
  /** What the forgot and reset pages draw for someone already signed in: the way on, and a sign-out. */
60
- export function AlreadySignedIn({ redirect: asked, home }: { redirect?: string; home: { path: string; name?: string } }) {
60
+ export function AlreadySignedIn({ redirect: asked, home, screens }: { redirect?: string; home: { path: string; name?: string }; screens: readonly { name: string; path: string }[] }) {
61
61
  const { t } = useCopy()
62
62
  const label = useHomeLabel()
63
- const to = wanted({ asked, home: home.path })
63
+ const to = landing({ asked, screens })
64
64
  return (
65
65
  <AuthFrame title={t('auth.alreadySignedIn', 'You are already signed in')}>
66
66
  <div className="grid gap-4">
@@ -147,4 +147,4 @@ export function AuthBox({
147
147
  </FieldShell>
148
148
  )
149
149
  }
150
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 ab822afe361dfc5f82c504570fd5d0ea13ec4572e60adb679a3f9532ccf67803
150
+ // mithaq-admin 0.0.0 e498423cf4a61adb8cf6358d043d31ae92d853de4c6939a9c0fe138c848b7211
@@ -0,0 +1,150 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { Link, useNavigate } from '@tanstack/react-router'
3
+ import { createServerFn } from '@tanstack/react-start'
4
+ import { Button } from '@/components/ui/button'
5
+ import { authArm } from './api.ts'
6
+ import { AlreadySignedIn, AuthBox, AuthFrame, authGate, redirectSearch, useRefusal } from './auth-frame.tsx'
7
+ import { useCopy } from './routes/__root.tsx'
8
+
9
+ /** The search keys of a page a mailed link opens: its token, and where to go after. */
10
+ export const tokenSearch = (search: Record<string, unknown>): { redirect?: string; token?: string } => ({
11
+ ...redirectSearch(search),
12
+ ...(typeof search.token === 'string' && search.token ? { token: search.token } : {}),
13
+ })
14
+
15
+ /**
16
+ * The address of the account a live link chooses the password of, or `null` for a spent, expired or made-up one: the
17
+ * provider's own answer, read in process. A POST, so the token rides in no further URL.
18
+ */
19
+ const passwordLink = createServerFn({ method: 'POST' })
20
+ .validator((token: unknown) => (typeof token === 'string' ? token : ''))
21
+ .handler(async ({ data }) => {
22
+ // Imported inside the server half so no page ships the provider to the browser.
23
+ const { asas } = await import('asas')
24
+ type Provider = { name: string; server: { api: { setPasswordState(args: { query: { token: string } }): Promise<{ email: string }> } } }
25
+ const provider = (asas.auth.strategies as unknown as Provider[]).find((one) => one.name === 'user')!
26
+ return data ? provider.server.api.setPasswordState({ query: { token: data } }).then(({ email }) => email, () => null) : null
27
+ })
28
+
29
+ /** The `beforeLoad` of a page a mailed link opens: the auth gate's answer, and the account the link is for. */
30
+ export const passwordGate = async ({ token }: { token?: string }) => ({ ...(await authGate()), email: token ? await passwordLink({ data: token }) : null })
31
+
32
+ type Props = {
33
+ /** A first password, chosen from a sign-up's or an added person's link, rather than a forgotten one replaced. */
34
+ first: boolean
35
+ token?: string
36
+ redirect?: string
37
+ signedIn: boolean
38
+ home: { path: string; name?: string }
39
+ /** The screens a signed-in person opens. */
40
+ screens: readonly { name: string; path: string }[]
41
+ password: { min: number; max: number }
42
+ /** The account the link is for; `null` for a link that names none, `undefined` while unread. */
43
+ email?: string | null
44
+ }
45
+
46
+ /**
47
+ * The page a mailed link opens to choose an account's password: a first one, or one forgotten. It shows the account
48
+ * it is for and signs the person in with the password just chosen, through the ordinary sign-in, as Payload's reset
49
+ * does: every rule a sign-in keeps still holds, and nothing is known that the person did not just type.
50
+ */
51
+ export function ChoosePassword({ first, token, redirect, signedIn, home, screens, password: bounds, email }: Props) {
52
+ const { t } = useCopy()
53
+ const navigate = useNavigate()
54
+ const refused = useRefusal()
55
+ const [password, setPassword] = useState('')
56
+ const [confirmation, setConfirmation] = useState('')
57
+ const [pending, setPending] = useState(false)
58
+ const [done, setDone] = useState(false)
59
+ // The answer, not its sentence, so a language switch says it again in the language chosen.
60
+ const [refusal, setRefusal] = useState<Parameters<typeof refused>[0] | null>(null)
61
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
62
+ const title = first ? t('auth.setPassword', 'Choose your password') : t('auth.resetPassword', 'Reset password')
63
+ const wrong = {
64
+ password: () =>
65
+ !password ? t('auth.required', 'This field is required.')
66
+ : password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
67
+ : password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
68
+ : undefined,
69
+ confirmation: () => (!confirmation ? t('auth.required', 'This field is required.') : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
70
+ }
71
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
72
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
73
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
74
+ )
75
+ const signIn = (
76
+ <p className="mt-4 text-center text-sm">
77
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-in">
78
+ {t('auth.signIn', 'Sign in')}
79
+ </Link>
80
+ </p>
81
+ )
82
+
83
+ const submit = async (event: React.FormEvent) => {
84
+ event.preventDefault()
85
+ setLeft(new Set(Object.keys(wrong)))
86
+ if (Object.values(wrong).some((check) => check())) return
87
+ setPending(true)
88
+ setRefusal(null)
89
+ const answered = await authArm('reset-password', { newPassword: password, token })
90
+ if (!answered.ok) {
91
+ setPending(false)
92
+ return setRefusal(answered)
93
+ }
94
+ // A sign-in this refuses, a banned account's, is left to the sign-in page, which says why.
95
+ // Signed in, the sign-in page's own gate sends the person on to `redirect` or their first screen.
96
+ if (email && (await authArm('sign-in/email', { email, password })).ok) return navigate({ to: '/auth/sign-in', search: { redirect } })
97
+ setPending(false)
98
+ setDone(true)
99
+ }
100
+
101
+ if (signedIn) return <AlreadySignedIn redirect={redirect} home={home} screens={screens} />
102
+ if (!token || email === null) {
103
+ return (
104
+ <AuthFrame title={title} description={t('auth.tokenInvalid', 'Token is either invalid or has expired.')}>
105
+ <p className="text-center text-sm">
106
+ <Link className="text-primary underline underline-offset-4" to="/auth/forgot-password">
107
+ {t('auth.forgotPassword', 'Forgot password')}
108
+ </Link>
109
+ </p>
110
+ </AuthFrame>
111
+ )
112
+ }
113
+ if (done) {
114
+ return (
115
+ <AuthFrame title={title} description={first ? t('auth.passwordChosen', 'Your address is confirmed and your password is set. Sign in with it.') : t('auth.passwordReset', 'Password reset successfully.')}>
116
+ {signIn}
117
+ </AuthFrame>
118
+ )
119
+ }
120
+ return (
121
+ <AuthFrame title={title}>
122
+ <form className="grid gap-4" noValidate onSubmit={submit}>
123
+ {/* The account the password is for, which a password manager saves it under. */}
124
+ {email ? <AuthBox id="email" label={t('auth.email', 'Email')} type="email" autoComplete="username" readOnly value={email} onLeave={() => {}} /> : null}
125
+ {box('password', t('auth.newPassword', 'New password'), {
126
+ autoComplete: 'new-password',
127
+ description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
128
+ onChange: (event) => setPassword(event.target.value),
129
+ type: 'password',
130
+ value: password,
131
+ })}
132
+ {box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
133
+ autoComplete: 'new-password',
134
+ onChange: (event) => setConfirmation(event.target.value),
135
+ type: 'password',
136
+ value: confirmation,
137
+ })}
138
+ {refusal && (
139
+ <p className="text-destructive text-sm" role="alert">
140
+ {refused(refusal, t('auth.resetFailed', 'That reset did not work.'))}
141
+ </p>
142
+ )}
143
+ <Button disabled={pending} type="submit">
144
+ {first ? t('auth.setPasswordSubmit', 'Confirm and set password') : t('auth.resetPassword', 'Reset password')}
145
+ </Button>
146
+ </form>
147
+ {signIn}
148
+ </AuthFrame>
149
+ )
150
+ }
@@ -0,0 +1,35 @@
1
+ import type { Language } from 'asas'
2
+ import type { ComponentsOf } from 'mithaq-admin/components.tsx'
3
+ import { defaultComponents } from 'mithaq-admin/default-components.tsx'
4
+ // add-on: rich-text
5
+ import { richTextComponents } from 'mithaq-admin/rich-text.tsx'
6
+ // end add-on: rich-text
7
+ // add-on: uploads
8
+ import { uploadComponents } from 'mithaq-admin/upload.tsx'
9
+ // end add-on: uploads
10
+
11
+ // Typed against the language, so a kind of the project's own with no control or cell here is a type error, and so
12
+ // is leaving out the rich text or files add-on while the language holds its kind.
13
+ // Spread defaultComponents.controls or defaultComponents.cells to add or replace one.
14
+ // A file of its own, so the server entry reads the map without evaluating the router and its route tree.
15
+ export const components: ComponentsOf<Language> = {
16
+ ...defaultComponents,
17
+ controls: {
18
+ ...defaultComponents.controls,
19
+ // add-on: rich-text
20
+ ...richTextComponents.controls,
21
+ // end add-on: rich-text
22
+ // add-on: uploads
23
+ ...uploadComponents.controls,
24
+ // end add-on: uploads
25
+ },
26
+ cells: {
27
+ ...defaultComponents.cells,
28
+ // add-on: rich-text
29
+ ...richTextComponents.cells,
30
+ // end add-on: rich-text
31
+ // add-on: uploads
32
+ ...uploadComponents.cells,
33
+ // end add-on: uploads
34
+ },
35
+ }