create-mithaq 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/index.js +127 -0
  5. package/package.json +19 -4
  6. package/template/apps/asas-platform/components.json +25 -0
  7. package/template/apps/asas-platform/gitignore +3 -0
  8. package/template/apps/asas-platform/package.json +55 -0
  9. package/template/apps/asas-platform/public/favicon.svg +1 -0
  10. package/template/apps/asas-platform/src/api.ts +147 -0
  11. package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
  12. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
  13. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
  14. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
  15. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
  16. package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
  17. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
  18. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
  19. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
  20. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
  21. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
  22. package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
  23. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
  24. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
  25. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
  26. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
  27. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
  28. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
  29. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
  30. package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
  31. package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
  32. package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
  33. package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
  34. package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
  35. package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
  36. package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
  37. package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
  38. package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
  39. package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
  40. package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
  41. package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
  42. package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
  43. package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
  44. package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
  45. package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
  46. package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
  47. package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
  48. package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
  49. package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
  50. package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
  51. package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
  52. package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
  53. package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
  54. package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
  55. package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
  56. package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
  57. package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
  58. package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
  59. package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
  60. package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
  61. package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
  62. package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
  63. package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
  64. package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
  65. package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
  66. package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
  67. package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
  68. package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
  69. package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
  70. package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
  71. package/template/apps/asas-platform/src/copy.ts +1 -0
  72. package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
  73. package/template/apps/asas-platform/src/lib/utils.ts +1 -0
  74. package/template/apps/asas-platform/src/links.ts +38 -0
  75. package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
  76. package/template/apps/asas-platform/src/redirect.ts +1 -0
  77. package/template/apps/asas-platform/src/request-headers.ts +11 -0
  78. package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
  79. package/template/apps/asas-platform/src/router.tsx +7 -0
  80. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
  81. package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
  82. package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
  83. package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
  84. package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
  85. package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
  86. package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
  87. package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
  88. package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
  89. package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
  90. package/template/apps/asas-platform/src/screen-form.tsx +34 -0
  91. package/template/apps/asas-platform/src/screen-list.tsx +139 -0
  92. package/template/apps/asas-platform/src/screen-names.ts +10 -0
  93. package/template/apps/asas-platform/src/screens.tsx +868 -0
  94. package/template/apps/asas-platform/src/server.ts +20 -0
  95. package/template/apps/asas-platform/src/slide.ts +22 -0
  96. package/template/apps/asas-platform/src/start.ts +42 -0
  97. package/template/apps/asas-platform/src/styles.css +200 -0
  98. package/template/apps/asas-platform/src/theme.ts +2 -0
  99. package/template/apps/asas-platform/tsconfig.json +19 -0
  100. package/template/apps/asas-platform/vite.config.ts +26 -0
  101. package/template/apps/asas-tanstack/components.json +25 -0
  102. package/template/apps/asas-tanstack/gitignore +3 -0
  103. package/template/apps/asas-tanstack/package.json +56 -0
  104. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  105. package/template/apps/asas-tanstack/src/api.ts +147 -0
  106. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  107. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +112 -0
  108. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
  109. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
  110. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
  111. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
  112. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
  113. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
  114. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
  115. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
  116. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
  117. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
  118. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
  119. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
  120. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
  121. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
  122. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
  123. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
  124. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -0
  125. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  126. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  127. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  128. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  129. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  130. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  131. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  132. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  133. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  134. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  135. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  136. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  137. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  138. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  139. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  140. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  141. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +215 -0
  142. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  143. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  144. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  145. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  146. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  147. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  148. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  149. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  150. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  151. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  152. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  153. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  154. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  155. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  156. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  157. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  158. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  159. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  160. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  161. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  162. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  163. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  164. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  165. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  166. package/template/apps/asas-tanstack/src/copy.ts +870 -0
  167. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  168. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  169. package/template/apps/asas-tanstack/src/links.ts +38 -0
  170. package/template/apps/asas-tanstack/src/locale-switch.tsx +26 -0
  171. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  172. package/template/apps/asas-tanstack/src/redirect.ts +20 -0
  173. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  174. package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
  175. package/template/apps/asas-tanstack/src/router.tsx +7 -0
  176. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
  177. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
  178. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -0
  179. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  180. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  181. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  182. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +4 -0
  183. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
  184. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -0
  185. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  186. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  187. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  188. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  189. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  190. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +101 -0
  191. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  192. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  193. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +109 -0
  194. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
  195. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
  196. package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
  197. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
  198. package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
  199. package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
  200. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  201. package/template/apps/asas-tanstack/src/screens.tsx +868 -0
  202. package/template/apps/asas-tanstack/src/server.ts +16 -0
  203. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  204. package/template/apps/asas-tanstack/src/start.ts +57 -0
  205. package/template/apps/asas-tanstack/src/styles.css +159 -0
  206. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  207. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  208. package/template/apps/asas-tanstack/vite.config.ts +28 -0
  209. package/template/package.json +16 -0
  210. package/template/templates/asas/.env.example +16 -0
  211. package/template/templates/asas/components.json +22 -0
  212. package/template/templates/asas/mithaq.hints.json +25 -0
  213. package/template/templates/asas/package.json +38 -0
  214. package/template/templates/asas/src/asas.ts +93 -0
  215. package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
  216. package/template/templates/asas/src/audit-log/sink.ts +89 -0
  217. package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
  218. package/template/templates/asas/src/courses/courses.ts +612 -0
  219. package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
  220. package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
  221. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
  222. package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
  223. package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
  224. package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
  225. package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
  226. package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
  227. package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
  228. package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
  229. package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
  230. package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
  231. package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
  232. package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
  233. package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
  234. package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
  235. package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
  236. package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
  237. package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
  238. package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
  239. package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
  240. package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
  241. package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
  242. package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
  243. package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
  244. package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
  245. package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
  246. package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
  247. package/template/templates/asas/src/local-users/local-users.ts +143 -0
  248. package/template/templates/asas/src/media/media.ts +137 -0
  249. package/template/templates/asas/src/media/ui/preview.tsx +143 -0
  250. package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
  251. package/template/templates/asas/src/messages/account.json +106 -0
  252. package/template/templates/asas/src/messages/auditLog.json +186 -0
  253. package/template/templates/asas/src/messages/courses.json +270 -0
  254. package/template/templates/asas/src/messages/enrollments.json +42 -0
  255. package/template/templates/asas/src/messages/invitation.json +66 -0
  256. package/template/templates/asas/src/messages/localUsers.json +42 -0
  257. package/template/templates/asas/src/messages/media.json +66 -0
  258. package/template/templates/asas/src/messages/member.json +42 -0
  259. package/template/templates/asas/src/messages/memberRoles.json +42 -0
  260. package/template/templates/asas/src/messages/mithaq.json +170 -0
  261. package/template/templates/asas/src/messages/organization.json +50 -0
  262. package/template/templates/asas/src/messages/organizationRole.json +50 -0
  263. package/template/templates/asas/src/messages/roles.json +58 -0
  264. package/template/templates/asas/src/messages/session.json +82 -0
  265. package/template/templates/asas/src/messages/sessions.json +106 -0
  266. package/template/templates/asas/src/messages/settings.json +66 -0
  267. package/template/templates/asas/src/messages/tags.json +38 -0
  268. package/template/templates/asas/src/messages/user.json +90 -0
  269. package/template/templates/asas/src/messages/verification.json +50 -0
  270. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  271. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  272. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  273. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  274. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  275. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  276. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  277. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  278. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  279. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  280. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  281. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  282. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  283. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  284. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  285. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  286. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  287. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  288. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  289. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  290. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  291. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  292. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  293. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  294. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  295. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  296. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  297. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  298. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  299. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  300. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  301. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  302. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  303. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  304. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  305. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  306. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  307. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  308. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  309. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  310. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  311. package/template/templates/asas/src/migrations/schema.ts +29 -0
  312. package/template/templates/asas/src/mithaq.ts +406 -0
  313. package/template/templates/asas/src/pages.ts +74 -0
  314. package/template/templates/asas/src/platform-http.ts +96 -0
  315. package/template/templates/asas/src/rich-text-nodes.ts +11 -0
  316. package/template/templates/asas/src/roles/grants.ts +47 -0
  317. package/template/templates/asas/src/roles/roles.ts +184 -0
  318. package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
  319. package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
  320. package/template/templates/asas/src/roles.ts +16 -0
  321. package/template/templates/asas/src/seed.ts +325 -0
  322. package/template/templates/asas/src/sessions/sessions.ts +155 -0
  323. package/template/templates/asas/src/settings/settings.ts +69 -0
  324. package/template/templates/asas/src/statements.ts +11 -0
  325. package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
  326. package/template/templates/asas/src/storage/defaults.ts +156 -0
  327. package/template/templates/asas/src/storage/index.ts +95 -0
  328. package/template/templates/asas/src/storage/local.ts +143 -0
  329. package/template/templates/asas/src/storage/r2.ts +5 -0
  330. package/template/templates/asas/src/storage/s3.ts +91 -0
  331. package/template/templates/asas/src/storage/uploadthing.ts +59 -0
  332. package/template/templates/asas/src/tags/tags.ts +80 -0
  333. package/template/templates/asas/src/tenant-access.ts +221 -0
  334. package/template/templates/asas/src/tenant-roles.ts +262 -0
  335. package/template/templates/asas/src/ui/app-shell.tsx +260 -0
  336. package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
  337. package/template/templates/asas/src/ui/record-page.tsx +50 -0
  338. package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
  339. package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
  340. package/template/templates/asas/src/ui/session-labels.tsx +36 -0
  341. package/template/templates/asas/src/ui/table-page.tsx +9 -0
  342. package/template/templates/asas/src/ui/top-shell.tsx +60 -0
  343. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
  344. package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
  345. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  346. package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
  347. package/template/templates/asas/src/ui-platform/home.tsx +63 -0
  348. package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
  349. package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
  350. package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
  351. package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
  352. package/template/templates/asas/src/ui-portal/home.tsx +77 -0
  353. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  354. package/template/templates/auth/package.json +14 -0
  355. package/template/templates/auth/src/index.ts +690 -0
  356. package/template/templates/auth/src/platform-roles.ts +87 -0
  357. package/README.md +0 -3
@@ -0,0 +1,145 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import type { ScreenClient as Client } from 'mithaq/admin'
3
+ export type { PageClient } from 'mithaq/admin'
4
+ export type UserValues = {
5
+ id: string
6
+ updatedAt: Date
7
+ createdAt: Date
8
+ name: string
9
+ email: string
10
+ emailVerified: boolean
11
+ image: string | null
12
+ role: string | null
13
+ banned: boolean | null
14
+ banReason: string | null
15
+ banExpires: Date | null
16
+ }
17
+ export type OrganizationValues = {
18
+ id: string
19
+ updatedAt: Date
20
+ createdAt: Date
21
+ name: string
22
+ slug: string
23
+ logo: string | null
24
+ metadata: string | null
25
+ }
26
+ export type MemberValues = {
27
+ id: string
28
+ updatedAt: Date
29
+ createdAt: Date
30
+ organizationId: string
31
+ userId: string
32
+ role: string
33
+ }
34
+ export type CoursesValues = {
35
+ id: string
36
+ updatedAt: Date
37
+ createdAt: Date
38
+ organization: string
39
+ title: string
40
+ summary: string | null
41
+ fee: number | null
42
+ currency: 'SAR' | 'USD' | null
43
+ level: number | null
44
+ notifications: boolean | null
45
+ delivery: 'live' | 'recorded' | null
46
+ audience: 'beginner' | 'advanced' | null
47
+ accessCode: string | null
48
+ archived: boolean | null
49
+ topics: string[] | null
50
+ opensOn: string | null
51
+ archiveReason: string | null
52
+ tags: string[] | null
53
+ scheduledSessions: Values['sessions'][]
54
+ _status: 'draft' | 'published' | null
55
+ details: { root: Record<string, unknown> } | null
56
+ cover: unknown
57
+ }
58
+ export type SessionsValues = {
59
+ id: string
60
+ updatedAt: Date
61
+ createdAt: Date
62
+ organization: string
63
+ course: string
64
+ startsAt: Date
65
+ capacity: number | null
66
+ venue: 'online' | 'campus' | null
67
+ format: 'workshop' | 'lecture' | null
68
+ archived: boolean | null
69
+ details: { root: Record<string, unknown> } | null
70
+ enrollments: Values['enrollments'][]
71
+ }
72
+ export type EnrollmentsValues = {
73
+ id: string
74
+ updatedAt: Date
75
+ createdAt: Date
76
+ organization: string
77
+ session: string
78
+ student: string
79
+ deletedAt: Date | null
80
+ }
81
+ export type TagsValues = {
82
+ id: string
83
+ updatedAt: Date
84
+ createdAt: Date
85
+ organization: string
86
+ name: Record<'ar' | 'en', string>
87
+ }
88
+ export type RolesValues = {
89
+ id: string
90
+ updatedAt: Date
91
+ createdAt: Date
92
+ organization: string
93
+ name: string
94
+ label: Record<'ar' | 'en', string>
95
+ permissions: string[]
96
+ }
97
+ export type MemberRolesValues = {
98
+ id: string
99
+ updatedAt: Date
100
+ createdAt: Date
101
+ organization: string
102
+ user: string
103
+ role: string
104
+ }
105
+ export type LocalUsersValues = {
106
+ id: string
107
+ updatedAt: Date
108
+ createdAt: Date
109
+ email: string
110
+ hash: string
111
+ salt: string
112
+ sessions: string | null
113
+ loginAttempts: number | null
114
+ lockUntil: Date | null
115
+ resetPasswordToken: string | null
116
+ resetPasswordAttempts: number | null
117
+ resetPasswordExpiration: Date | null
118
+ _verified: boolean | null
119
+ _verificationToken: string | null
120
+ }
121
+ export type Values = {
122
+ user: UserValues
123
+ organization: OrganizationValues
124
+ member: MemberValues
125
+ courses: CoursesValues
126
+ sessions: SessionsValues
127
+ enrollments: EnrollmentsValues
128
+ tags: TagsValues
129
+ roles: RolesValues
130
+ memberRoles: MemberRolesValues
131
+ localUsers: LocalUsersValues
132
+ }
133
+ export type Pages = {
134
+ user: 'list' | 'view'
135
+ organization: 'list' | 'view'
136
+ member: 'list' | 'view'
137
+ courses: 'list' | 'view'
138
+ sessions: 'list' | 'view'
139
+ enrollments: 'list' | 'view'
140
+ tags: 'list' | 'view'
141
+ roles: 'list' | 'view'
142
+ memberRoles: 'list' | 'view'
143
+ localUsers: 'list' | 'edit'
144
+ }
145
+ export type ScreenClient = Client<Pages>
@@ -0,0 +1,17 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './platform/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/platform/$')({
7
+ validateSearch: askedFor,
8
+ loader: ({ location, params }) => pageLoader({ screen: 'platform', files, path: params._splat ?? '', query: location.search, href: location.href }),
9
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
10
+ notFoundComponent: () => <ScreenNotFound files={files} screen={'platform'} />,
11
+ component: Page,
12
+ })
13
+
14
+ function Page() {
15
+ const navigate = Route.useNavigate()
16
+ return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
17
+ }
@@ -0,0 +1,10 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
4
+ import { files } from './platform/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/platform/')({
7
+ loader: ({ location }) => pageLoader({ screen: 'platform', files, path: '', query: {}, href: location.href }),
8
+ head: ({ loaderData, match }) => pageHead(loaderData, match.context),
9
+ component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
10
+ })
@@ -0,0 +1,13 @@
1
+ // Written by `mithaq generate screens`. Do not edit.
2
+ import { Outlet, createFileRoute } from '@tanstack/react-router'
3
+ import { ScreenFrame, frameLoader } from '@/screens.tsx'
4
+ import { files } from './platform/files.ts'
5
+
6
+ export const Route = createFileRoute('/(screens)/platform')({
7
+ loader: ({ location }) => frameLoader({ screen: 'platform', files, href: location.href }),
8
+ component: () => (
9
+ <ScreenFrame files={files} data={Route.useLoaderData()}>
10
+ <Outlet />
11
+ </ScreenFrame>
12
+ ),
13
+ })
@@ -0,0 +1,95 @@
1
+ import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'
2
+ import { DirectionProvider } from '@/components/ui/direction'
3
+ export { locales } from '../copy.ts'
4
+ import { dirOf, documentHead, localeOf, translator } from '../copy.ts'
5
+ import { ThemeProvider } from '../components/mithaq/nav.tsx'
6
+ import { themeStorageKey } from '../theme.ts'
7
+ import appCss from '../styles.css?url'
8
+
9
+ // Puts the chosen theme on the document before first paint, which the provider's effect cannot do.
10
+ // Also reloads a page restored from the back-forward cache, so Back after signing out asks the server again.
11
+ const theme = `
12
+ (function() {
13
+ var storageKey = ${JSON.stringify(themeStorageKey)};
14
+ var defaultTheme = "system";
15
+ try {
16
+ var theme = localStorage.getItem(storageKey) || defaultTheme;
17
+ var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
18
+ var root = document.documentElement;
19
+ root.classList.remove("light", "dark");
20
+ root.classList.add(theme === "system" ? systemTheme : theme);
21
+ } catch (e) {}
22
+ window.addEventListener("pageshow", function (event) {
23
+ if (event.persisted) location.reload();
24
+ });
25
+ })()
26
+ `
27
+
28
+ // A request context nothing registered is `never` in the router's types, so the shape is named and read
29
+ // through a cast; `localeOf` turns it back into a declared locale.
30
+ type FromRequest = { serverContext?: { locale?: unknown } }
31
+
32
+ export const Route = createRootRoute({
33
+ // On the client the locale is read back off the `lang` the server wrote on `<html>`, so the hydrated tree
34
+ // matches the one sent. `localeOf` on both refuses a hand-edited `lang` like a hand-written cookie.
35
+ beforeLoad: (ctx) => {
36
+ const locale =
37
+ typeof document === 'undefined'
38
+ ? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
39
+ : localeOf(document.documentElement.lang)
40
+ return { locale, dir: dirOf(locale), app: 'platform.brand' }
41
+ },
42
+ // The favicon stops the browser asking for a `/favicon.ico` nobody serves.
43
+ head: ({ match }) => ({
44
+ meta: [
45
+ { charSet: 'utf-8' },
46
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
47
+ ...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
48
+ ],
49
+ links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
50
+ }),
51
+ // Without it the router draws a development-only placeholder and warns.
52
+ notFoundComponent: NotFound,
53
+ component: RootDocument,
54
+ })
55
+
56
+ /** The locale and direction of this request and the `t` resolved for it, read from the root route's context. */
57
+ export const useCopy = () => {
58
+ const { dir, locale } = Route.useRouteContext()
59
+ return { dir, locale, t: translator(locale) }
60
+ }
61
+
62
+ function NotFound() {
63
+ const { t } = useCopy()
64
+ return (
65
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
66
+ <h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
67
+ <p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
68
+ {/* An anchor, because `/` is no route here. */}
69
+ <a className="text-foreground text-sm underline underline-offset-4" href="/">
70
+ {t('admin.goHome', 'Go to the home page')}
71
+ </a>
72
+ </main>
73
+ )
74
+ }
75
+
76
+ function RootDocument() {
77
+ const { dir, locale } = useCopy()
78
+ return (
79
+ <html lang={locale} dir={dir} suppressHydrationWarning>
80
+ <head>
81
+ <HeadContent />
82
+ {/* The string is this module's own and carries no request value, so writing it raw is safe. */}
83
+ <script dangerouslySetInnerHTML={{ __html: theme }} />
84
+ </head>
85
+ <body>
86
+ <DirectionProvider direction={dir}>
87
+ <ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
88
+ <Outlet />
89
+ </ThemeProvider>
90
+ </DirectionProvider>
91
+ <Scripts />
92
+ </body>
93
+ </html>
94
+ )
95
+ }
@@ -0,0 +1,30 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ // Loads the types of the `server` route option; erased at build.
3
+ import type {} from '@tanstack/start-client-core'
4
+ import { asas } from 'asas'
5
+
6
+ // The platform reads every organization: a read of a kayan asks `tenant: 'all'`, which the runner
7
+ // grants platform staff in this app alone and refuses anyone else. A global is one organization's row
8
+ // and asks for none.
9
+ const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `/api/${one.kayan}/${one.name}`))
10
+
11
+ const acrossTenants = async (request: Request) => {
12
+ const url = new URL(request.url)
13
+ if (!reads.has(url.pathname)) return request
14
+ url.searchParams.set('tenant', 'all')
15
+ const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text()
16
+ return new Request(url, { method: request.method, headers: request.headers, body })
17
+ }
18
+
19
+ // A write whose `Origin` is not this app's own is refused: the data plug checks no origin and leans on
20
+ // the session cookie's `SameSite=Lax`, which a sibling origin of the same site passes.
21
+ export const Route = createFileRoute('/api/$')({
22
+ server: {
23
+ handlers: {
24
+ ANY: async ({ request }) =>
25
+ request.method === 'GET' || request.method === 'HEAD' || request.headers.get('origin') === new URL(request.url).origin
26
+ ? asas.handler(await acrossTenants(request))
27
+ : new Response('Forbidden', { status: 403 }),
28
+ },
29
+ },
30
+ })
@@ -0,0 +1,72 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { createFileRoute, useNavigate } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
6
+ import { documentHead } from '../copy.ts'
7
+ import { useCopy } from './__root.tsx'
8
+ import { wanted } from '../redirect.ts'
9
+
10
+ export const Route = createFileRoute('/auth/sign-in')({
11
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.signIn')),
12
+ validateSearch: redirectSearch,
13
+ component: SignIn,
14
+ })
15
+
16
+ function SignIn() {
17
+ const navigate = useNavigate()
18
+ const { t } = useCopy()
19
+ const search = Route.useSearch()
20
+ const refused = useRefusal()
21
+ const [email, setEmail] = useState('')
22
+ const [password, setPassword] = useState('')
23
+ const [pending, setPending] = useState(false)
24
+ const [refusal, setRefusal] = useState<string | null>(null)
25
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
26
+ const required = t('auth.required', 'This field is required.')
27
+ const wrong = {
28
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
29
+ password: () => (password ? undefined : required),
30
+ }
31
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
32
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'>) => (
33
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
34
+ )
35
+
36
+ const submit = async (event: React.FormEvent) => {
37
+ event.preventDefault()
38
+ setLeft(new Set(Object.keys(wrong)))
39
+ if (Object.values(wrong).some((check) => check())) return
40
+ setPending(true)
41
+ setRefusal(null)
42
+ const answered = await authArm('sign-in/email', { email, password })
43
+ // Our own sentence for the provider's code, not its English `message`: a refusal is copy and carries a string per locale.
44
+ if (!answered.ok) {
45
+ setPending(false)
46
+ return setRefusal(refused(answered, t('auth.failed', 'That sign-in did not work.')))
47
+ }
48
+
49
+ setPending(false)
50
+
51
+ // A navigation, not a reload, so the screen's loader runs with the cookie just set.
52
+ await navigate({ to: wanted({ asked: search.redirect, home: (await authGate()).home.path }) })
53
+ }
54
+
55
+ return (
56
+ <AuthFrame title={t('auth.signIn', 'Sign in')} description={t('auth.signInPlatformDescription', 'Sign in to reach the platform.')}>
57
+ <form className="grid gap-4" noValidate onSubmit={submit}>
58
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
59
+ {box('password', t('auth.password', 'Password'), { autoComplete: 'current-password', onChange: (event) => setPassword(event.target.value), type: 'password', value: password })}
60
+ {/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
61
+ {refusal && (
62
+ <p className="text-destructive text-sm" role="alert">
63
+ {refusal}
64
+ </p>
65
+ )}
66
+ <Button disabled={pending} type="submit">
67
+ {pending ? t('auth.signInPending', 'Signing in…') : t('auth.signIn', 'Sign in')}
68
+ </Button>
69
+ </form>
70
+ </AuthFrame>
71
+ )
72
+ }
@@ -0,0 +1,122 @@
1
+ import { Fragment, useState, type ReactNode } from 'react'
2
+ import { createFileRoute, redirect } from '@tanstack/react-router'
3
+ import { Button } from '@/components/ui/button'
4
+ import { authArm } from '../api.ts'
5
+ import { AuthFrame, useRefusal } from '../auth-frame.tsx'
6
+ import { documentHead } from '../copy.ts'
7
+ import { ImpersonationBar, loadEnterable } from '../screens.tsx'
8
+ import { useCopy } from './__root.tsx'
9
+
10
+ export const Route = createFileRoute('/unauthorized')({
11
+ // The path a screen refused; the server only matches it against the build's pages.
12
+ validateSearch: (search: Record<string, unknown>): { from?: string } => (typeof search.from === 'string' ? { from: search.from } : {}),
13
+ loaderDeps: ({ search }) => ({ from: search.from }),
14
+ loader: async ({ deps, context }) => {
15
+ const entered = await loadEnterable({ data: { from: deps.from, locale: context.locale } })
16
+ // Carries this page's own path so a visitor whose session expired lands back here after signing in.
17
+ if (!entered) throw redirect({ to: '/auth/sign-in', search: { redirect: '/unauthorized' } })
18
+ return entered
19
+ },
20
+ // After the loader, so its data's type is inferred before this reads it.
21
+ head: ({ match, loaderData }) => documentHead(match.context, (t) => (loaderData?.screens.length === 0 ? t('auth.noAccess') : t('auth.unauthorized'))),
22
+ component: Unauthorized,
23
+ })
24
+
25
+ function Unauthorized() {
26
+ const { t } = useCopy()
27
+ const { screens, refused, email, impersonating, organizations = [], others = [] } = Route.useLoaderData()
28
+ const refusal = useRefusal()
29
+ const [switching, setSwitching] = useState<string>()
30
+ const [failed, setFailed] = useState<string>()
31
+ // The slug sits beside the name where another membership shares the name.
32
+ const memberships = [...organizations, ...others]
33
+ const named = (one: { id: string; name: string; slug?: string }): ReactNode =>
34
+ one.slug && memberships.some((other) => other.id !== one.id && other.name === one.name) ? (
35
+ <>
36
+ {one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
37
+ </>
38
+ ) : (
39
+ one.name
40
+ )
41
+ // Each name is isolated so an email or Latin name in an Arabic sentence keeps
42
+ // its order, and is one inline box that wraps whole, so a long address never
43
+ // runs past the card on a phone.
44
+ const whole = 'inline-block max-w-full [overflow-wrap:anywhere]'
45
+ const said = (text: string | undefined) =>
46
+ text!.split(/(\{email\}|\{organization\}|\{page\})/).map((part, at) =>
47
+ part === '{email}' ? <bdi key={at} className={whole}>{email}</bdi>
48
+ : part === '{organization}' ? <bdi key={at}>{organizations.map((one, index) => <Fragment key={one.id}>{index ? ', ' : ''}<span className={whole}>{named(one)}</span></Fragment>)}</bdi>
49
+ : part === '{page}' ? <bdi key={at}>{refused?.page}</bdi>
50
+ : part)
51
+ const link = 'text-foreground underline underline-offset-4'
52
+ const screenName = (name: string) => t(`screens.${name}`, name) ?? name
53
+ return (
54
+ <AuthFrame
55
+ title={screens.length ? t('auth.unauthorized', 'Not for your role') : t('auth.noAccess', 'Your account has no access yet')}
56
+ description={
57
+ <span data-no-access={screens.length ? undefined : ''}>
58
+ {refused
59
+ ? said(t('auth.unauthorizedPage', 'Your role does not open that page: {page}.'))
60
+ : screens.length
61
+ ? t('auth.unauthorizedDescription', 'Your role does not open that screen. These are the screens it opens:')
62
+ : organizations.length
63
+ ? said(t('auth.noAccessDescription', '{email} is a member of {organization} and holds no role that opens a screen here. Ask an administrator of {organization} to give you one.'))
64
+ : said(t('auth.noOrganization', '{email} belongs to no organization yet. Ask an administrator to invite you.'))}
65
+ </span>
66
+ }
67
+ >
68
+ <div className="grid gap-4">
69
+ {/* Stopping impersonation is the way out in place of Sign out. */}
70
+ {impersonating ? <ImpersonationBar name={impersonating} /> : null}
71
+ {refused ? (
72
+ <a className={link} href={refused.screen.path}>
73
+ {(t('auth.backTo', 'Back to {screen}') ?? 'Back to {screen}').replace('{screen}', () => screenName(refused.screen.name))}
74
+ </a>
75
+ ) : screens.length ? (
76
+ <ul className="grid gap-2">
77
+ {screens.map((screen) => (
78
+ <li key={screen.name}>
79
+ {/* An anchor: the path is the screen's own data, not a route the router knows. */}
80
+ <a className={link} href={screen.path}>
81
+ {screenName(screen.name)}
82
+ </a>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ ) : null}
87
+ {/* So a switch into an organization where the actor holds no role is not a dead end. */}
88
+ {others.map((one) => (
89
+ <Button
90
+ key={one.id}
91
+ type="button"
92
+ variant="secondary"
93
+ data-switch-organization={one.id}
94
+ // Busy while it runs, so a second press sends nothing.
95
+ disabled={switching !== undefined}
96
+ aria-busy={switching === one.id || undefined}
97
+ onClick={() => {
98
+ setSwitching(one.id)
99
+ setFailed(undefined)
100
+ void authArm('organization/set-active', { organizationId: one.id }).then((answer) => {
101
+ if (answer.ok) return window.location.assign('/auth/sign-in')
102
+ setSwitching(undefined)
103
+ setFailed(refusal(answer, t('auth.requestFailed', 'That request did not work.')))
104
+ })
105
+ }}
106
+ >
107
+ {/* One box, so the button's gap does not open beside the name. */}
108
+ <span>{(t('auth.switchTo', 'Switch to {organization}') ?? 'Switch to {organization}').split(/(\{organization\})/).map((part, at) => (part === '{organization}' ? <bdi key={at}>{named(one)}</bdi> : part))}</span>
109
+ </Button>
110
+ ))}
111
+ {failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
112
+ {/* `replace`, so Back does not return to the page signed out of. */}
113
+ {impersonating ? null : (
114
+ <Button type="button" variant="outline" className="max-sm:h-11" onClick={() => void authArm('sign-out').then(() => window.location.replace('/auth/sign-in'))}>
115
+ {t('admin.signOut', 'Sign out')}
116
+ </Button>
117
+ )}
118
+ </div>
119
+ </AuthFrame>
120
+ )
121
+ }
122
+ // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 3c5c304b3892c56759bf780f155ef3aa154ce3cc954e3d240b3537cdd97da735
@@ -0,0 +1,34 @@
1
+ import { useBlocker, useRouter } from '@tanstack/react-router'
2
+ import { useState, type ReactElement } from 'react'
3
+ import { Form, type FormProps } from './components/mithaq/form.tsx'
4
+
5
+ /**
6
+ * A kayan's form on a screen's page. A create moves to the new row's edit page, a delete to the list.
7
+ * While the form holds a change the router blocks navigation and the form asks its discard question.
8
+ */
9
+ export function ScreenForm({ edit, list, ...props }: Omit<FormProps, 'onDone' | 'onDirty' | 'onCancel' | 'blocked'> & {
10
+ /** The href of a row's edit page, or nothing where the kayan has none. */
11
+ edit: (id: string) => string | undefined
12
+ /** The href of the kayan's list, or nothing where it has none. */
13
+ list?: string
14
+ }): ReactElement {
15
+ const router = useRouter()
16
+ const [dirty, setDirty] = useState(false)
17
+ const blocker = useBlocker({ shouldBlockFn: () => dirty, withResolver: true, enableBeforeUnload: false })
18
+ // `ignoreBlocker`: the form reports clean only on the render after the save.
19
+ const go = (href: string | undefined) => void (href ? router.navigate({ href, ignoreBlocker: true }) : router.invalidate())
20
+ const landing = (row: unknown, effect?: string) => {
21
+ const id = (row as { id?: unknown } | null | undefined)?.id
22
+ return effect === 'delete' ? list : effect === 'create' && id !== undefined ? edit(String(id)) : undefined
23
+ }
24
+ return (
25
+ <Form
26
+ {...props}
27
+ onDirty={setDirty}
28
+ onDone={(row, effect) => go(landing(row, effect))}
29
+ onCancel={list ? () => go(list) : undefined}
30
+ blocked={blocker.status === 'blocked' ? blocker : undefined}
31
+ />
32
+ )
33
+ }
34
+ // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 7284b5e97770fd0c395fafa0b7459cab4ceb458e4bc982dc8233894126116170