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,260 @@
1
+ // The frame both screens may put at the root of a page: a collapsible sidebar holding the organization switcher, the nav and the user menu, and an inset holding the site header over the page.
2
+ // A row that goes nowhere is not drawn, so there are no quick-create or inbox buttons, documents group, or help and search rows: none has a destination in this template.
3
+ import { useState, useSyncExternalStore, type CSSProperties, type ReactNode } from 'react'
4
+ import {
5
+ ArrowUpRight,
6
+ BookOpen,
7
+ Building2,
8
+ CalendarDays,
9
+ ClipboardList,
10
+ LayoutDashboard,
11
+ Settings2,
12
+ ShieldCheck,
13
+ Table2,
14
+ Tags,
15
+ UserCog,
16
+ UserRound,
17
+ Users,
18
+ type LucideIcon,
19
+ } from 'lucide-react'
20
+ import type { ShellProps } from 'mithaq/admin'
21
+ import { useLocale, useNav, useScreen, useUser } from 'mithaq/admin/react'
22
+ import { authArm } from '@/api.ts'
23
+ import { locales, switchLocale } from '@/copy.ts'
24
+ import { DiscardDialog } from '@/components/mithaq/form.tsx'
25
+ import { NavSidebar, NavUserMenu, OrganizationSwitcher, type NavOrganization } from '@/components/mithaq/nav.tsx'
26
+ import { buttonVariants } from '@/components/ui/button'
27
+ import { Separator } from '@/components/ui/separator'
28
+ import {
29
+ SidebarContent,
30
+ SidebarFooter,
31
+ SidebarGroup,
32
+ SidebarGroupContent,
33
+ SidebarHeader,
34
+ SidebarInset,
35
+ SidebarMenu,
36
+ SidebarMenuButton,
37
+ SidebarMenuItem,
38
+ SidebarProvider,
39
+ SidebarTrigger,
40
+ } from '@/components/ui/sidebar'
41
+ import { navLabel } from './screen-nav.tsx'
42
+
43
+ export const regions = ['header', 'main'] as const
44
+
45
+ /** One icon per nav entry, keyed by the entry's path under the screen, and `Table2` for any other. */
46
+ const icons: Record<string, LucideIcon> = {
47
+ '/': LayoutDashboard,
48
+ '/users': UserRound,
49
+ '/organizations': Building2,
50
+ '/members': Users,
51
+ '/courses': BookOpen,
52
+ '/sessions': CalendarDays,
53
+ '/enrollments': ClipboardList,
54
+ '/tags': Tags,
55
+ '/roles': ShieldCheck,
56
+ '/member-roles': UserCog,
57
+ '/settings': Settings2,
58
+ }
59
+ export const iconOf = (path: string) => (Object.hasOwn(icons, path) ? icons[path]! : Table2)
60
+
61
+ /**
62
+ * The path the browser is on. The server has no location, so nothing is
63
+ * marked current in the first paint and the entry is marked once it hydrates.
64
+ */
65
+ const usePathname = () => useSyncExternalStore(() => () => {}, () => window.location.pathname, () => undefined)
66
+
67
+ /**
68
+ * The locale switch and sign-out, in the user menu. The locale is a cookie and a reload, because the request middleware reads it before any route runs;
69
+ * the switch is asked first, in the app's discard dialog, where a form holds a change nobody saved.
70
+ */
71
+ export function UserMenu() {
72
+ const { locale, t } = useLocale()
73
+ // The language asked for while a form holds a change nobody saved, which the
74
+ // app's own discard dialog confirms in the page's words.
75
+ const [asked, setAsked] = useState<string>()
76
+ return (
77
+ <>
78
+ <NavUserMenu
79
+ user={useUser()}
80
+ t={t}
81
+ locale={locale}
82
+ locales={locales}
83
+ onLocale={(next) => (window.onbeforeunload ? setAsked(next) : switchLocale({ to: next }))}
84
+ // Only once the provider has answered: its sign-out answers 200 with a
85
+ // session or without one, so a refusal stays where it is. `replace`,
86
+ // so Back does not return to the page signed out of. The sign-out that
87
+ // ends an impersonation answers the platform's address, where the staff
88
+ // member goes next.
89
+ onSignOut={() => void authArm('sign-out').then((answer) => answer.ok && window.location.replace(String(answer.body?.url ?? '/auth/sign-in')))}
90
+ />
91
+ <DiscardDialog open={asked !== undefined} onOpenChange={(open) => !open && setAsked(undefined)} onDiscard={() => switchLocale({ to: asked! })} t={t} />
92
+ </>
93
+ )
94
+ }
95
+
96
+ /**
97
+ * The organization switcher over the organizations the session came with. The host sends them with the session, so it is drawn with the page and never flashes a skeleton.
98
+ * Switching reloads the page, because every loader reads its rows under the active organization on the server.
99
+ */
100
+ function Organizations() {
101
+ const { t } = useLocale()
102
+ const { organizations = [], activeOrganizationId } = useUser()
103
+ return (
104
+ <OrganizationSwitcher
105
+ organizations={organizations}
106
+ activeOrganizationId={activeOrganizationId}
107
+ t={t}
108
+ onSwitch={(organizationId) => void authArm('organization/set-active', { organizationId }).then((answer) => answer.ok && window.location.reload())}
109
+ />
110
+ )
111
+ }
112
+
113
+ /**
114
+ * One entry per destination, the shell's own first and the entities after.
115
+ * The dashboard is current on its own path alone; every other entry on its path and below it.
116
+ */
117
+ function NavMain({ home }: { home?: string }) {
118
+ const { locale, t } = useLocale()
119
+ const screen = useScreen()
120
+ const pathname = usePathname()
121
+ const entries = [
122
+ { path: '/', label: home ?? t('dashboard.dashboard', 'Dashboard') },
123
+ ...[...useNav()].sort((one, other) => Number(!!one.kayan) - Number(!!other.kayan)).map((entry) => ({ path: entry.path, label: navLabel({ entry, screen, locale, t }) })),
124
+ ]
125
+ return (
126
+ <SidebarGroup>
127
+ <SidebarGroupContent className="flex flex-col gap-2">
128
+ <SidebarMenu>
129
+ {entries.map((entry) => {
130
+ const href = entry.path === '/' ? screen.path : `${screen.path}${entry.path}`
131
+ const Icon = iconOf(entry.path)
132
+ const isActive = pathname !== undefined && (pathname === href || (entry.path !== '/' && pathname.startsWith(`${href}/`)))
133
+ return (
134
+ <SidebarMenuItem key={entry.path}>
135
+ {/* A 44 pixel row wherever the sidebar is the phone's sheet. */}
136
+ <SidebarMenuButton
137
+ isActive={isActive}
138
+ className="max-md:h-11"
139
+ render={
140
+ <a href={href}>
141
+ <Icon />
142
+ <span>{entry.label}</span>
143
+ </a>
144
+ }
145
+ />
146
+ </SidebarMenuItem>
147
+ )
148
+ })}
149
+ </SidebarMenu>
150
+ </SidebarGroupContent>
151
+ </SidebarGroup>
152
+ )
153
+ }
154
+
155
+ export const frame = { '--sidebar-width': 'calc(var(--spacing) * 72)', '--header-height': 'calc(var(--spacing) * 12)' } as CSSProperties
156
+
157
+ /**
158
+ * The site header's bar, with the page's own title. It is sticky: its trigger is the only way back to a hidden sidebar, so it stays on screen however far the page is scrolled.
159
+ * Below sm the title may take two lines, set tight so both fit the bar, since on a phone it is often the record's only name; from sm up it is one line, cut at its end.
160
+ * From sm up the row wraps and the bar grows with it, so a form's controls that do not fit take a line of their own rather than run over the title or past the bar's end.
161
+ * A bar that wrapped is taller than `--header-height`, so it measures itself into `--header-bar-height`, which what sticks under it reads.
162
+ */
163
+ export const headerBar = 'sticky top-0 z-10 bg-background rounded-t-[inherit] flex min-h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:min-h-(--header-height)'
164
+ export const headerRow = 'flex w-full min-w-0 items-center gap-1 px-4 sm:flex-wrap sm:gap-y-2 sm:py-2 lg:gap-2 lg:px-6 [&_h1]:min-w-0 max-sm:[&_h1]:line-clamp-2 max-sm:[&_h1]:leading-tight max-sm:[&_h1]:wrap-break-word sm:[&_h1]:truncate [&_h1]:text-base [&_h1]:font-medium [&_h1]:tracking-normal'
165
+
166
+ /**
167
+ * The header's own height on the document, as `--header-bar-height`, kept while the header is on screen and taken off with it.
168
+ * A variable of its own and never `--header-height`, which the bar's `min-h-(--header-height)` reads, so the measure would feed itself.
169
+ * A module function, so React hands the ref the same callback on every render and observes the header once.
170
+ */
171
+ const measureHeader = (header: HTMLElement | null) => {
172
+ if (!header) return
173
+ const root = document.documentElement.style
174
+ // `borderBoxSize` is missing before Safari 15.4, so the header's own `offsetHeight` stands in there.
175
+ const observer = new ResizeObserver(([entry]) => {
176
+ if (entry) root.setProperty('--header-bar-height', `${entry.borderBoxSize?.[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight}px`)
177
+ })
178
+ observer.observe(header)
179
+ return () => {
180
+ observer.disconnect()
181
+ root.removeProperty('--header-bar-height')
182
+ }
183
+ }
184
+
185
+ /**
186
+ * The one way from a tenant screen to the platform app, in the header, for an actor whose own `user.role` holds a platform role that enters it.
187
+ * The link grants nothing here: the platform app's own server decides entry.
188
+ * Below xl it is its icon alone, a 44 pixel square on a phone, its words kept for a screen reader, so the page's title keeps the header's width.
189
+ */
190
+ export function PlatformLink() {
191
+ const { t } = useLocale()
192
+ const platform = useUser().screens?.find((one) => one.name === 'platform')
193
+ if (!platform) return null
194
+ return (
195
+ <a href={platform.path} data-platform-link="" className={buttonVariants({ variant: 'outline', size: 'sm', className: 'ms-auto max-sm:size-11 max-xl:px-2.5 max-sm:px-0 [[data-node=controls]:not(:empty)~&]:ms-0' })}>
196
+ <span className="max-xl:sr-only">{t('dashboard.openPlatform', 'Open the platform')}</span>
197
+ <ArrowUpRight data-icon="inline-end" className="rtl:-scale-x-100" />
198
+ </a>
199
+ )
200
+ }
201
+
202
+ /** The sidebar on the side the document's direction starts from. */
203
+ export function AppShell({ regions, brand, home }: ShellProps<readonly ['header', 'main'], ReactNode> & { brand?: ReactNode; home?: string }) {
204
+ const { dir, t } = useLocale()
205
+ return (
206
+ <SidebarProvider data-shell="app" style={frame}>
207
+ <NavSidebar variant="inset" side={dir === 'rtl' ? 'right' : 'left'} t={t}>
208
+ {/* One navigation landmark around the whole sidebar, named in the
209
+ page's language, so the switcher and the user menu are inside a
210
+ landmark as the links are. It is a box of its own and not a role on
211
+ the sidebar, whose props reach no element on a phone, where it is a
212
+ sheet. */}
213
+ <nav aria-label={t('admin.navigation', 'Navigation')} className="flex min-h-0 flex-1 flex-col">
214
+ <SidebarHeader>{brand ?? <Organizations />}</SidebarHeader>
215
+ <SidebarContent>
216
+ <NavMain home={home} />
217
+ </SidebarContent>
218
+ <SidebarFooter>
219
+ <UserMenu />
220
+ </SidebarFooter>
221
+ </nav>
222
+ </NavSidebar>
223
+ {/* `min-w-0` so the inset, a flex item beside the sidebar, never takes
224
+ the width of the widest thing a page holds: without it a table wider
225
+ than a phone would widen the document, header and trigger with it. A
226
+ wide table scrolls in its own frame instead. */}
227
+ <SidebarInset className="min-w-0">
228
+ <header data-region="header" ref={measureHeader} className={headerBar}>
229
+ <div className={headerRow}>
230
+ <SidebarTrigger aria-label={t('admin.toggleSidebar', 'Toggle Sidebar')} className="-ms-1 max-md:size-11" />
231
+ <Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
232
+ {regions.header}
233
+ <PlatformLink />
234
+ </div>
235
+ </header>
236
+ <div className="flex min-w-0 flex-1 flex-col px-4 py-4 lg:px-6 lg:py-6">
237
+ {/* `min-w-0` so a table wider than the page scrolls rather than
238
+ pushing the sidebar off it. A div and not a `main`, because the
239
+ inset around it is already the page's one main landmark. */}
240
+ <div data-region="main" className="@container/main flex min-w-0 flex-1 flex-col gap-2">{regions.main}</div>
241
+ </div>
242
+ </SidebarInset>
243
+ </SidebarProvider>
244
+ )
245
+ }
246
+
247
+ /**
248
+ * The platform's frame, the dashboard's with two changes: the app's name where the organization switcher sits, since staff work above every organization and switch to none of them,
249
+ * and the home entry named as its page names itself, so the nav and the page say one word.
250
+ */
251
+ export function PlatformShell(props: ShellProps<readonly ['header', 'main'], ReactNode>) {
252
+ const { t } = useLocale()
253
+ return (
254
+ <AppShell
255
+ {...props}
256
+ brand={<span data-brand="" className="truncate px-2 py-1.5 text-base font-semibold">{t('platform.brand', 'Asas platform')}</span>}
257
+ home={t('platform.title', 'Platform')}
258
+ />
259
+ )
260
+ }
@@ -0,0 +1,25 @@
1
+ // One figure on a card, which the dashboard's home and the portal's home both draw.
2
+ import type { ReactNode } from 'react'
3
+ import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
4
+
5
+ /**
6
+ * A figure on a card, marked with the figure it shows. On a phone, two to a row,
7
+ * a card is about 136 pixels wide, so there the padding is a step smaller, and
8
+ * below 220 pixels of card the figure is set in the body size, which holds a sum
9
+ * such as "SAR 5,150.00" on one line in either locale; one longer still breaks at
10
+ * the edge rather than being cut.
11
+ */
12
+ export function KpiCard({ kpi, icon, title, value, footer }: { kpi: string; icon: ReactNode; title?: string; value: ReactNode; footer: ReactNode }) {
13
+ return (
14
+ <Card data-kpi={kpi} className="@container/card min-w-0">
15
+ <CardHeader className="max-sm:px-4">
16
+ <CardDescription className="flex items-center gap-2 text-sm font-medium">
17
+ <span className="text-muted-foreground inline-flex">{icon}</span>
18
+ <span>{title}</span>
19
+ </CardDescription>
20
+ <CardTitle className="min-w-0 text-base font-semibold tabular-nums wrap-break-word @[220px]/card:text-2xl @[250px]/card:text-3xl">{value}</CardTitle>
21
+ </CardHeader>
22
+ <CardFooter className="text-muted-foreground pt-2 text-xs max-sm:px-4">{footer}</CardFooter>
23
+ </Card>
24
+ )
25
+ }
@@ -0,0 +1,50 @@
1
+ // The frame of one record: the form in `main`, what sits beside it in `side`, and the buttons a
2
+ // person should not reach by accident in `danger`. A page fills only the regions this file exports,
3
+ // which the build checks.
4
+ import type { ReactNode } from 'react'
5
+ import type { LayoutProps } from 'mithaq/admin'
6
+ import { useKayan, useLocale, useRecord } from 'mithaq/admin/react'
7
+
8
+ export const regions = ['main', 'side', 'danger'] as const
9
+
10
+ export function RecordPage({ regions, page }: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
11
+ const { t } = useLocale()
12
+ const record = useRecord()
13
+ return (
14
+ // main, side, danger in document order is how a phone stacks them: the danger zone last. From lg up
15
+ // the grid puts the side beside both and the danger zone back under the form; the second row's `1fr`
16
+ // takes whatever height the side adds, so the danger zone sits right under the form. A page with
17
+ // nothing beside its form gives the form the whole width. A side marked `data-lead`, such as an
18
+ // image beside its description, comes before the form on a phone instead.
19
+ <div data-layout="record" data-page={page.path} data-record={record ? 'row' : 'none'} className={regions.side ? 'grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(16rem,1fr)] lg:grid-rows-[auto_1fr]' : 'grid gap-4'}>
20
+ <div data-region="main" className="grid min-w-0 gap-4 lg:col-start-1">
21
+ {regions.main}
22
+ </div>
23
+ {regions.side ? <aside data-region="side" className="grid h-fit gap-4 max-lg:has-[[data-lead]]:order-first lg:col-start-2 lg:row-span-2 lg:row-start-1">{regions.side}</aside> : null}
24
+ {/* The danger zone, fenced by the destructive border, under the form rather than in its row. */}
25
+ {regions.danger ? (
26
+ <section data-region="danger" className="grid h-fit min-w-0 gap-2 rounded-lg border border-destructive/50 p-4 lg:col-start-1">
27
+ <h2 className="text-base font-medium text-destructive">{t('admin.dangerZone', 'Danger zone')}</h2>
28
+ <DangerWarning />
29
+ <div className="flex flex-wrap items-center gap-2">{regions.danger}</div>
30
+ </section>
31
+ ) : null}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ /**
37
+ * What the danger zone says under its heading: the kayan's own sentence, or the shared one with the
38
+ * kind put in. Its own component, since only a kayan's page has a danger zone and `useKayan` refuses
39
+ * any other.
40
+ */
41
+ function DangerWarning() {
42
+ const { t, locale } = useLocale()
43
+ const kayan = useKayan()
44
+ return (
45
+ <p className="text-muted-foreground text-sm">
46
+ {t(`${kayan.name}.dangerZoneWarning`)
47
+ ?? (t('admin.dangerZoneWarning', 'These actions change this {kind} for everyone.') ?? '').replace('{kind}', () => (t(`${kayan.name}.singular`) ?? kayan.name).toLocaleLowerCase(locale))}
48
+ </p>
49
+ )
50
+ }
@@ -0,0 +1,15 @@
1
+ // What every screen's rich text editor and viewer draw with: this project's
2
+ // own node list, the one its server parses every write with, each class swapped
3
+ // for its React subclass where one extends it, and the `/` menu's picks. A
4
+ // provider each screen names, so no control, read view or panel can take
5
+ // another list.
6
+ import type { ReactNode } from 'react'
7
+ import type { ProviderProps } from 'mithaq/admin'
8
+ import { RichTextContext, defaultPicks, defaultViews, viewsOf } from '@/components/mithaq/rich-text.tsx'
9
+ import { UploadPlugin, UploadView, imagePick } from '@/components/mithaq/upload.tsx'
10
+ import { richTextNodes } from '../rich-text-nodes.ts'
11
+
12
+ // The upload node's `/` entry "Image" opens its dialog in every editor.
13
+ const value = { nodes: viewsOf(richTextNodes, [...defaultViews, UploadView]), picks: [...defaultPicks, imagePick], Plugin: UploadPlugin }
14
+
15
+ export const RichTextNodes = ({ children }: ProviderProps<ReactNode>) => <RichTextContext value={value}>{children}</RichTextContext>
@@ -0,0 +1,28 @@
1
+ // The nav a page drawn whole puts in its shell's nav region, since such a page
2
+ // has no tree for a `{ nav: {} }` node to sit in: one link per entry of the
3
+ // screen's nav, which the host has already cut to the viewer, read through the hooks.
4
+ import { use } from 'react'
5
+ import type { NavEntry, ScreenClient, Translate } from 'mithaq/admin'
6
+ import { ScreenContext, useLocale, useScreen } from 'mithaq/admin/react'
7
+ import { currentOf } from '@/components/mithaq/nodes.tsx'
8
+
9
+ /** What an entry is called: its own label, else its kayan's name in the singular for a global and the plural otherwise. */
10
+ export const navLabel = ({ entry, screen, locale, t }: { entry: NavEntry; screen: ScreenClient; locale: string; t: Translate }) =>
11
+ entry.label?.[locale] ?? t(`${entry.kayan}.${screen.kayan[entry.kayan ?? '']?.single ? 'singular' : 'plural'}`, entry.kayan)
12
+
13
+ export function ScreenNav() {
14
+ const { locale, t } = useLocale()
15
+ const screen = useScreen()
16
+ const kayan = use(ScreenContext)?.kayan?.name
17
+ return (
18
+ <ul className="flex flex-col gap-1">
19
+ {screen.nav.map((entry) => (
20
+ <li key={entry.page + entry.path}>
21
+ <a href={`${screen.path}${entry.path}`} aria-current={currentOf(entry, kayan)} className="text-sm">
22
+ {navLabel({ entry, screen, locale, t })}
23
+ </a>
24
+ </li>
25
+ ))}
26
+ </ul>
27
+ )
28
+ }
@@ -0,0 +1,36 @@
1
+ // How a session reads wherever a picker lists or holds one: its course by the
2
+ // course's title, then when it starts, since a session's own title, the start
3
+ // alone, makes two sessions on one day read alike.
4
+ //
5
+ // A session holds its course as an id, and a read populates no relationship, so
6
+ // the courses are a second read: one of every course the picker's rows point at,
7
+ // shared through the query client. Each read is the viewer's, so a course the
8
+ // viewer may not read leaves the session's title, its start, as it was.
9
+ import type { ReactNode } from 'react'
10
+ import { useQuery } from '@tanstack/react-query'
11
+ import type { ProviderProps, Row } from 'mithaq/admin'
12
+ import { useClient, useLocale, useScreen } from 'mithaq/admin/react'
13
+ import { formatDate } from '@/components/mithaq/controls.tsx'
14
+ import { nameOfRow, RowLabelContext, titleFieldOf, type UseRowLabel } from '@/components/mithaq/relationship-control.tsx'
15
+ import { coursesOf } from '../enrollments/ui/session-cell.tsx'
16
+
17
+ /** The session's course and its start, or nothing for any other kayan and for a session whose course is not read yet. */
18
+ export const useSessionLabel: UseRowLabel = (target, rows) => {
19
+ const client = useClient()
20
+ const { locale } = useLocale()
21
+ const { kayan, maxLimit } = useScreen()
22
+ const sessions = target === 'sessions'
23
+ // Sorted, so the same courses in another order are the same query.
24
+ const ids = sessions ? [...new Set(rows.flatMap((one) => (typeof one.course === 'string' ? [one.course] : [])))].sort() : []
25
+ const { data: courses } = useQuery({ queryKey: ['session-courses', ids], queryFn: () => coursesOf({ client, ids, maxLimit }), enabled: ids.length > 0, staleTime: 30_000 })
26
+ if (!sessions) return undefined
27
+ return (row: Row) => {
28
+ const course = courses?.get(String(row.course))
29
+ const when = formatDate({ value: row.startsAt, locale })
30
+ // The date's own spaces are no-break ones, so a box too narrow for the whole label breaks between the course
31
+ // and the date and never inside the date.
32
+ return course && when ? `${nameOfRow({ row: course, title: titleFieldOf(kayan.courses), locale })} · ${when.replaceAll(' ', '\u00a0')}` : undefined
33
+ }
34
+ }
35
+
36
+ export const SessionLabels = ({ children }: ProviderProps<ReactNode>) => <RowLabelContext value={useSessionLabel}>{children}</RowLabelContext>
@@ -0,0 +1,9 @@
1
+ // The layout of a page that lists rows: one region, `main`.
2
+ import type { ReactNode } from 'react'
3
+ import type { LayoutProps } from 'mithaq/admin'
4
+
5
+ export const regions = ['main'] as const
6
+
7
+ export const TablePage = ({ regions, page }: LayoutProps<readonly ['main'], ReactNode>) => (
8
+ <div data-layout="table" data-page={page.path} className="grid gap-4">{regions.main}</div>
9
+ )
@@ -0,0 +1,60 @@
1
+ // The second frame both screens may put at the root of a page: the nav as a
2
+ // bar across the top, the header under it, and one column below. The header is the app
3
+ // shell's own bar with the user menu at its end in place of the sidebar
4
+ // trigger, so both frames sign out and switch locale from the same menu. The
5
+ // provider is there for the menu, whose primitives read the sidebar's context;
6
+ // no sidebar is drawn.
7
+ import { use, type ReactNode } from 'react'
8
+ import type { ShellProps } from 'mithaq/admin'
9
+ import { ScreenContext, useLocale, useScreen } from 'mithaq/admin/react'
10
+ import { SidebarProvider } from '@/components/ui/sidebar'
11
+ import { PlatformLink, UserMenu, frame, headerBar, headerRow } from './app-shell.tsx'
12
+
13
+ export const regions = ['header', 'nav', 'main'] as const
14
+
15
+ /** The dashboard's top shell, whose header links the platform app for staff the way the app shell's does. */
16
+ export function TopShell(props: ShellProps<readonly ['header', 'nav', 'main'], ReactNode>) {
17
+ return <Top {...props} platform />
18
+ }
19
+
20
+ /**
21
+ * The portal's, the same shell with no platform link: the portal is where a
22
+ * tenant's people work, and staff reach the platform from the dashboard
23
+ * alone. Which of the two a page draws is the shell its screen names under
24
+ * `top`, never the URL.
25
+ */
26
+ export function PortalTopShell(props: ShellProps<readonly ['header', 'nav', 'main'], ReactNode>) {
27
+ return <Top {...props} platform={false} />
28
+ }
29
+
30
+ function Top({ regions, platform }: ShellProps<readonly ['header', 'nav', 'main'], ReactNode> & { platform: boolean }) {
31
+ const { t } = useLocale()
32
+ const screen = useScreen()
33
+ // Home is current on the screen's own home alone, whose path is the
34
+ // screen's, and each entry on its kayan's pages; the current one is drawn in
35
+ // the foreground and the rest muted. On a phone each link is a 44 pixel row,
36
+ // a touch target.
37
+ const home = use(ScreenContext)?.path === screen.path
38
+ return (
39
+ <SidebarProvider data-shell="top" style={frame} className="flex-col">
40
+ <nav data-region="nav" aria-label={t('admin.mainNavigation', 'Main navigation')} className="mx-auto flex w-full max-w-4xl flex-wrap items-center gap-x-4 gap-y-1 px-6 pt-4 text-sm [&_a]:text-muted-foreground [&_a:hover]:text-foreground [&_a[aria-current=page]]:font-semibold [&_a[aria-current=page]]:text-foreground [&_ul]:flex [&_ul]:flex-row [&_ul]:flex-wrap [&_ul]:gap-x-4 [&_ul]:gap-y-1 max-sm:[&_a]:inline-flex max-sm:[&_a]:min-h-11 max-sm:[&_a]:min-w-11 max-sm:[&_a]:items-center max-sm:[&_a]:justify-center">
41
+ <a href={screen.path} aria-current={home ? 'page' : undefined}>{t('portal.home', 'Home')}</a>
42
+ {regions.nav}
43
+ </nav>
44
+ <header data-region="header" className={`${headerBar} mx-auto w-full max-w-4xl`}>
45
+ <div className={headerRow}>
46
+ {regions.header}
47
+ <div className="ms-auto flex items-center gap-2">
48
+ {platform ? <PlatformLink /> : null}
49
+ {/* The avatar alone on a phone, the whole card from sm up, so the
50
+ title keeps the room it needs. */}
51
+ <div className="w-12 sm:w-56">
52
+ <UserMenu />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </header>
57
+ <main data-region="main" className="mx-auto grid w-full max-w-4xl grid-cols-[minmax(0,1fr)] gap-4 p-6">{regions.main}</main>
58
+ </SidebarProvider>
59
+ )
60
+ }