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,432 @@
1
+ // The versions are read in the browser, as the viewer, from the kayan's own
2
+ // `findVersions`, which answers only the versions of rows the viewer reads, each
3
+ // cut to the columns it reads. The page draws this only for an actor holding that
4
+ // key, and the restore only for one holding `restoreVersion`; the server refuses
5
+ // either to anyone else whatever the page drew.
6
+ import { useContext, useEffect, useMemo, useState } from 'react'
7
+ import { same, type ClientField, type ComponentProps, type Row } from 'mithaq/admin'
8
+ import { FormContext, useAccess, useClient, useKayan, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
9
+ import { empty } from '../audit-log/sink.ts'
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogFooter,
17
+ AlertDialogHeader,
18
+ AlertDialogTitle,
19
+ } from '@/components/ui/alert-dialog'
20
+ import { Badge } from '@/components/ui/badge'
21
+ import { Button } from '@/components/ui/button'
22
+ import { Checkbox } from '@/components/ui/checkbox'
23
+ import { Label } from '@/components/ui/label'
24
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
25
+ import { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
26
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
27
+ import { blocksOf, cellFor, richTextPlainText } from '@/components/mithaq/cells.tsx'
28
+ import { formatDate, labelFor, refusalKeyOf, titleOf, type FindApi, type FoundPage } from '@/components/mithaq/controls.tsx'
29
+ import { endonym } from '@/components/mithaq/nav.tsx'
30
+ import { CloseButton } from '@/components/mithaq/nodes.tsx'
31
+ import { useRelationshipTitles } from '@/components/mithaq/relationship-cell.tsx'
32
+
33
+ /** One version as `findVersions` answers it. */
34
+ export type Version = {
35
+ id: string
36
+ latest: boolean
37
+ autosave: boolean
38
+ updatedAt: string | Date
39
+ version: Row
40
+ }
41
+
42
+ /** How many versions one read asks for, and how many more each "Show older" adds. */
43
+ const page = 10
44
+
45
+ /** The four status labels, each under its `version` key with the English it falls back to. */
46
+ const labels = {
47
+ currentDraft: 'Current draft',
48
+ draft: 'Draft',
49
+ currentlyPublished: 'Currently published',
50
+ previouslyPublished: 'Previously published',
51
+ } as const
52
+
53
+ /**
54
+ * Which of the four labels a version wears, over the row's versions newest first.
55
+ * A draft is the current draft when it is the latest version, since then no
56
+ * published version is newer than it. A published version is the currently
57
+ * published one when it is the newest published version: the row always holds the
58
+ * last published copy and nothing unpublishes it. The newest page is always read
59
+ * first, so that version is on it whenever any published version is.
60
+ */
61
+ const labelOf = (version: Version, versions: readonly Version[]): keyof typeof labels | undefined =>
62
+ // A reader without the `_status` column gets it cut from every version, and
63
+ // a status nobody can read is no status to draw.
64
+ !('_status' in version.version)
65
+ ? undefined
66
+ : version.version._status === 'draft'
67
+ ? version.latest ? 'currentDraft' : 'draft'
68
+ : version.id === versions.find((one) => one.version._status !== 'draft')?.id ? 'currentlyPublished' : 'previouslyPublished'
69
+
70
+ /** The columns every kayan of this project carries and no person edits, and the status the pill already says. */
71
+ const technical = new Set(['id', 'organization', 'createdAt', 'updatedAt', '_status'])
72
+
73
+ /**
74
+ * The words of each block of `side`, and whether `other` holds no block of the
75
+ * same value, as a block is the unit a person reads a rich text change by. A block
76
+ * is held by its whole value, so one whose format or link changed is marked though
77
+ * its words did not.
78
+ */
79
+ const changedBlocks = ({ side, other }: { side: unknown; other: unknown }) => {
80
+ const left = new Map<string, number>()
81
+ for (const block of blocksOf(other)) {
82
+ const key = JSON.stringify(block)
83
+ left.set(key, (left.get(key) ?? 0) + 1)
84
+ }
85
+ return blocksOf(side).flatMap((block) => {
86
+ const key = JSON.stringify(block)
87
+ const held = left.get(key) ?? 0
88
+ if (held) left.set(key, held - 1)
89
+ const text = richTextPlainText(block).trim()
90
+ return text ? [{ text, changed: held === 0 }] : []
91
+ })
92
+ }
93
+
94
+ const entriesOf = (value: unknown) => (value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value) : [])
95
+
96
+ /**
97
+ * The panel an edit page names as a component node. It reads the row the page
98
+ * draws and its kayan off the screen, so one file serves every kayan that keeps
99
+ * drafts.
100
+ */
101
+ export function Versions(_: ComponentProps) {
102
+ const { t, locale } = useLocale()
103
+ const kayan = useKayan()
104
+ const record = useRecord()
105
+ const client = useClient()
106
+ const id = record?.id === undefined ? undefined : String(record.id)
107
+ // How many saves have landed on the form this panel sits in, an autosave
108
+ // included. A draft save never moves the row's own `updatedAt`, so this is what
109
+ // tells a panel that stayed mounted through one that its list is stale.
110
+ const saves = useContext(FormContext)?.saves ?? 0
111
+ // The row as this page last drew it: a save moves its `updatedAt` or bumps
112
+ // `saves`, and the panel reads its versions again.
113
+ const seen = `${id}@${String(record?.updatedAt ?? '')}@${saves}`
114
+ const [limit, setLimit] = useState(page)
115
+ const [read, setRead] = useState<{ seen: string; docs: Version[]; more: boolean } | { seen: string; failed: true }>()
116
+ const [opened, setOpened] = useState<Version>()
117
+
118
+ // The client is left out of the dependencies: the host makes a new one on every
119
+ // render, so following it would read the same page again on every render.
120
+ useEffect(() => {
121
+ if (!id) return
122
+ let live = true
123
+ ;(client({ kayan: kayan.name, operation: 'findVersions', input: { where: { parent: { equals: id } }, sort: '-updatedAt', limit } }) as Promise<{ docs: Version[]; hasNextPage: boolean }>).then(
124
+ (found) => live && setRead({ seen, docs: found.docs, more: found.hasNextPage }),
125
+ () => live && setRead({ seen, failed: true }),
126
+ )
127
+ return () => {
128
+ live = false
129
+ }
130
+ }, [kayan.name, seen, limit])
131
+
132
+ const shown = read?.seen === seen ? read : undefined
133
+ const when = (at: string | Date) => formatDate({ value: at, locale }) ?? String(at)
134
+ return (
135
+ <div data-panel="versions" className="grid gap-4">
136
+ {shown === undefined ? (
137
+ <p className="text-muted-foreground text-sm">{t('version.loading', 'Loading versions…')}</p>
138
+ ) : 'failed' in shown ? (
139
+ <p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
140
+ ) : !shown.docs.length ? (
141
+ <p className="text-muted-foreground text-sm">{t('version.none', 'No versions yet.')}</p>
142
+ ) : (
143
+ <>
144
+ <Table>
145
+ <TableHeader>
146
+ <TableRow>
147
+ <TableHead>{t('version.updatedAt', 'Updated at')}</TableHead>
148
+ <TableHead>{t('version.status', 'Status')}</TableHead>
149
+ </TableRow>
150
+ </TableHeader>
151
+ <TableBody>
152
+ {shown.docs.map((version) => (
153
+ <TableRow key={version.id} data-version={version.id}>
154
+ <TableCell>
155
+ {/* Underlined and in the link colour at rest, since a phone has
156
+ no hover to show that the date opens the comparison, and
157
+ 44 pixels tall. */}
158
+ <button type="button" data-button="open-version" className="text-primary inline-flex min-h-11 cursor-pointer items-center underline underline-offset-4" onClick={() => setOpened(version)}>
159
+ <time dateTime={new Date(version.updatedAt).toISOString()}>{when(version.updatedAt)}</time>
160
+ </button>
161
+ </TableCell>
162
+ <TableCell>
163
+ <div className="flex flex-wrap gap-1">
164
+ {version.autosave ? <Badge variant="outline">{t('version.autosave', 'Autosave')}</Badge> : null}
165
+ <Pill version={version} versions={shown.docs} />
166
+ </div>
167
+ </TableCell>
168
+ </TableRow>
169
+ ))}
170
+ </TableBody>
171
+ </Table>
172
+ {shown.more ? (
173
+ <Button type="button" variant="outline" size="sm" onClick={() => setLimit(limit + page)}>
174
+ {t('version.more', 'Show older')}
175
+ </Button>
176
+ ) : null}
177
+ {opened && id ? <Compare key={opened.id} version={opened} versions={shown.docs} parent={id} onClose={() => setOpened(undefined)} /> : null}
178
+ </>
179
+ )}
180
+ </div>
181
+ )
182
+ }
183
+
184
+ /** A version's status pill, the currently published one standing out. */
185
+ function Pill({ version, versions }: { version: Version; versions: readonly Version[] }) {
186
+ const { t } = useLocale()
187
+ const label = labelOf(version, versions)
188
+ return label ? <Badge variant={label === 'currentlyPublished' ? 'default' : 'outline'}>{t(`version.${label}`, labels[label])}</Badge> : null
189
+ }
190
+
191
+ /**
192
+ * The sheet over the list: the version, and the one it is compared against, by
193
+ * default the newest one older than it, field by field, each value drawn by the
194
+ * field's own cell and a localized field one row per locale, modified fields only
195
+ * until the viewer asks for all. A column the viewer may not read is in neither
196
+ * snapshot, so it draws no row.
197
+ */
198
+ function Compare({ version, versions, parent, onClose }: { version: Version; versions: readonly Version[]; parent: string; onClose: () => void }) {
199
+ const { t, locale, dir } = useLocale()
200
+ const kayan = useKayan()
201
+ const client = useClient()
202
+ const screen = useScreen()
203
+ const restores = useAccess().collections?.[kayan.name]?.operations?.restoreVersion === true
204
+ /** Whether the kayan's restore takes `expectedUpdatedAt`, as the form this panel sits in says. */
205
+ const takes = useContext(FormContext)?.expectedUpdatedAtOn?.includes('restoreVersion') === true
206
+ /** The version compared against, or `''` for the one before. */
207
+ const [against, setAgainst] = useState('')
208
+ const [modifiedOnly, setModifiedOnly] = useState(true)
209
+ /** The version before this one, read once: the list holds every other version a person may pick. */
210
+ const [prior, setPrior] = useState<{ version: Version | null } | { failed: true }>()
211
+ /** The restore waiting on its confirm, and whether it restores as a draft. */
212
+ const [confirming, setConfirming] = useState<{ draft: boolean }>()
213
+ const [restoring, setRestoring] = useState(false)
214
+ /** A restore the server refused, drawn in the sheet, which stays open. */
215
+ const [restoreFailed, setRestoreFailed] = useState<unknown>(undefined)
216
+
217
+ useEffect(() => {
218
+ let live = true
219
+ const older = { and: [{ parent: { equals: parent } }, { updatedAt: { less_than: new Date(version.updatedAt).toISOString() } }] }
220
+ ;(client({ kayan: kayan.name, operation: 'findVersions', input: { where: older, sort: '-updatedAt', limit: 1 } }) as Promise<{ docs: Version[] }>).then(
221
+ (found) => live && setPrior({ version: found.docs[0] ?? null }),
222
+ () => live && setPrior({ failed: true }),
223
+ )
224
+ return () => {
225
+ live = false
226
+ }
227
+ }, [version.id])
228
+
229
+ const previous = prior && 'version' in prior ? prior.version : null
230
+ const from = against ? versions.find((one) => one.id === against) ?? null : previous
231
+ const rows = kayan.fields
232
+ .filter((field) => field.kind !== 'join' && !technical.has(field.name) && (field.name in version.version || (!!from && field.name in from.version)))
233
+ .flatMap((field): { key: string; field: ClientField; before: unknown; after: unknown; code?: string }[] => {
234
+ const before = from?.version[field.name]
235
+ const after = version.version[field.name]
236
+ if (field.rules.localized !== true) return [{ key: field.name, field, before, after }]
237
+ // Each locale's value alone, drawn by the field's cell as a plain value.
238
+ const plain: ClientField = { ...field, rules: { ...field.rules, localized: false } }
239
+ const codes = [...new Set([...entriesOf(before), ...entriesOf(after)])]
240
+ return codes.map((code) => ({
241
+ key: `${field.name}.${code}`,
242
+ field: plain,
243
+ before: (before as Record<string, unknown> | undefined)?.[code],
244
+ after: (after as Record<string, unknown> | undefined)?.[code],
245
+ code,
246
+ }))
247
+ })
248
+ // Whether the two sides differ, once per row, which both the filter and the row's mark read.
249
+ .map((row) => ({ ...row, changed: !(same(row.before, row.after) || (empty(row.before) && empty(row.after))) }))
250
+ .filter((row) => !modifiedOnly || row.changed)
251
+
252
+ // Every kayan this screen crosses, so a relationship's value is named the way
253
+ // the list names it.
254
+ const kayans = useMemo(() => Object.values(screen.kayan), [screen])
255
+ const api = useMemo<FindApi>(
256
+ () => Object.fromEntries(kayans.map((one) => [one.name, { find: (input: Row) => client({ kayan: one.name, operation: 'find', input }) as Promise<FoundPage> }])),
257
+ [kayans],
258
+ )
259
+ const titles = useRelationshipTitles({ fields: kayan.fields, docs: [version.version, from?.version ?? {}], context: { api, client: kayans, locale } })
260
+ const copy = { t, locale }
261
+ // A rich text value whole, a block to a line, where its list cell draws the
262
+ // first block alone: a change further down would read as no change. A block the
263
+ // other side does not hold is marked, as removed (`del`) on the side compared
264
+ // against and as added (`ins`) on the side viewed: struck through and underlined
265
+ // as well as tinted, so the mark does not rest on colour alone.
266
+ const drawn = (field: ClientField, value: unknown, other: unknown, mark: 'del' | 'ins') => {
267
+ if (empty(value)) return '—'
268
+ if (field.kind !== 'richText') return cellFor(field)({ kayan, field, value, t, locale, titles })
269
+ const blocks = changedBlocks({ side: value, other })
270
+ // A value of images or empty blocks alone has no words, and reads as empty.
271
+ if (!blocks.length) return '—'
272
+ return blocks.map(({ text, changed }, at) => {
273
+ const Mark = mark
274
+ return <p key={at} dir="auto" className="whitespace-pre-line">{changed ? <Mark data-block-changed="" className={mark === 'del' ? 'bg-destructive/15 line-through' : 'bg-primary/15 underline'}>{text}</Mark> : text}</p>
275
+ })
276
+ }
277
+ const when = (at: string | Date) => formatDate({ value: at, locale }) ?? String(at)
278
+ const named = (one: Version) => {
279
+ const label = labelOf(one, versions)
280
+ return label ? `${when(one.updatedAt)} · ${t(`version.${label}`, labels[label])}` : when(one.updatedAt)
281
+ }
282
+ const options = [
283
+ // With its date, so the select says which version it compares against.
284
+ { value: '', label: previous ? `${t('version.previousVersion', 'Previous version')} · ${when(previous.updatedAt)}` : (t('version.previousVersion', 'Previous version') ?? '') },
285
+ // The version before is offered once, as "Previous version".
286
+ ...versions.filter((one) => one.id !== version.id && one.id !== previous?.id).map((one) => ({ value: one.id, label: named(one) })),
287
+ ]
288
+
289
+ /**
290
+ * Puts the version back, onto the row or as a new draft, and draws the page
291
+ * again from the server, so the form reads what the restore wrote. A refused or
292
+ * failed restore keeps the sheet open and draws its own sentence and the
293
+ * server's (`refusalKeyOf`, which the form's own banner reads the same way).
294
+ */
295
+ const restore = async (draft: boolean) => {
296
+ setRestoring(true)
297
+ setRestoreFailed(undefined)
298
+ try {
299
+ // The instant of the latest version this panel read, where the kayan's
300
+ // restore takes it, so a restore over a newer draft another person saved
301
+ // since is refused 409 rather than burying it.
302
+ const read = takes ? versions.find((one) => one.latest)?.version.updatedAt : undefined
303
+ await client({ kayan: kayan.name, operation: 'restoreVersion', input: { id: version.id, ...(read !== undefined && { expectedUpdatedAt: read }) }, draft })
304
+ window.location.reload()
305
+ } catch (thrown) {
306
+ setRestoring(false)
307
+ setConfirming(undefined)
308
+ setRestoreFailed(thrown)
309
+ }
310
+ }
311
+ const restoreFailedKey = restoreFailed === undefined ? undefined : refusalKeyOf(restoreFailed)
312
+
313
+ return (
314
+ <Sheet open onOpenChange={(open) => !open && onClose()}>
315
+ {/* The primitive's `data-[side=*]:w-3/4` is outranked by naming the same
316
+ side, so the sheet is the phone's whole width. */}
317
+ <SheetContent showCloseButton={false} side={dir === 'rtl' ? 'left' : 'right'} className="w-full overflow-y-auto data-[side=left]:w-full data-[side=right]:w-full data-[side=left]:sm:max-w-2xl data-[side=right]:sm:max-w-2xl">
318
+ <SheetHeader>
319
+ <SheetTitle>{t('version.compareVersions', 'Compare versions')}</SheetTitle>
320
+ </SheetHeader>
321
+ <div className="grid gap-4 px-4">
322
+ <div className="grid gap-3 sm:grid-cols-2">
323
+ <div className="grid gap-1.5">
324
+ <span className="text-muted-foreground text-xs">{t('version.comparingAgainst', 'Comparing against')}</span>
325
+ <Select value={against} disabled={prior === undefined} onValueChange={(value) => setAgainst(String(value ?? ''))}>
326
+ <SelectTrigger className="w-full max-sm:data-[size=default]:h-11"><SelectValue>{options.find((one) => one.value === against)?.label}</SelectValue></SelectTrigger>
327
+ <SelectContent>
328
+ {options.map((one) => (
329
+ <SelectItem key={one.value} value={one.value}>{one.label}</SelectItem>
330
+ ))}
331
+ </SelectContent>
332
+ </Select>
333
+ </div>
334
+ <div className="grid gap-1.5">
335
+ <span className="text-muted-foreground text-xs">{t('version.currentlyViewing', 'Currently viewing')}</span>
336
+ <div className="flex flex-wrap items-center gap-1 text-sm">
337
+ <time dateTime={new Date(version.updatedAt).toISOString()}>{when(version.updatedAt)}</time>
338
+ {version.autosave ? <Badge variant="outline">{t('version.autosave', 'Autosave')}</Badge> : null}
339
+ <Pill version={version} versions={versions} />
340
+ </div>
341
+ </div>
342
+ </div>
343
+ <div className="flex items-center gap-2">
344
+ <Checkbox id={`${version.id}-modified-only`} data-modified-only checked={modifiedOnly} onCheckedChange={(checked) => setModifiedOnly(checked === true)} />
345
+ <Label htmlFor={`${version.id}-modified-only`}>{t('version.modifiedOnly', 'Modified only')}</Label>
346
+ </div>
347
+ {prior === undefined ? (
348
+ <p className="text-muted-foreground text-sm">{t('version.loading', 'Loading versions…')}</p>
349
+ ) : 'failed' in prior && !against ? (
350
+ <p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
351
+ ) : !rows.length ? (
352
+ <p className="text-muted-foreground text-sm">{t('auditLog.none', 'No field you may read changed.')}</p>
353
+ ) : (
354
+ <ul className="grid gap-3 text-sm">
355
+ {/* Which side is which, over the two columns, since on a phone
356
+ the two labels above stack in one. */}
357
+ <li aria-hidden="true" className="text-muted-foreground grid grid-cols-2 gap-2 text-xs">
358
+ <span>{t('version.comparingAgainst', 'Comparing against')}</span>
359
+ <span>{t('version.currentlyViewing', 'Currently viewing')}</span>
360
+ </li>
361
+ {rows.map((row) => {
362
+ // What the "Modified only" filter keeps, so with every field shown
363
+ // the rows that differ say so in words and in a rule at their start.
364
+ const { changed } = row
365
+ return (
366
+ <li key={row.key} data-diff={row.key} data-changed={changed ? '' : undefined} className={changed ? 'border-primary grid gap-1 border-s-2 ps-2' : 'grid gap-1'}>
367
+ <span className="flex items-center gap-1.5 font-medium">
368
+ {labelFor({ kayan, field: row.field, copy })}
369
+ {row.code ? <Badge variant="outline">{endonym(row.code)}</Badge> : null}
370
+ {changed && !modifiedOnly ? <Badge data-badge="changed">{t('version.fieldChanged', 'Changed')}</Badge> : null}
371
+ </span>
372
+ <div className="grid grid-cols-2 gap-2">
373
+ <div data-before className="bg-muted/50 min-w-0 rounded-md px-2 py-1 break-words">{drawn(row.field, row.before, row.after, 'del')}</div>
374
+ <div data-after className="bg-muted min-w-0 rounded-md px-2 py-1 break-words">{drawn(row.field, row.after, row.before, 'ins')}</div>
375
+ </div>
376
+ </li>
377
+ )
378
+ })}
379
+ </ul>
380
+ )}
381
+ {restoreFailed !== undefined ? (
382
+ <>
383
+ <p role="alert" className="rounded-md border border-destructive/50 px-3 py-2 text-sm text-destructive">
384
+ {t('version.problemRestoringVersion', 'There was a problem restoring this version')}
385
+ </p>
386
+ {restoreFailedKey ? (
387
+ <p role="alert" className="rounded-md border border-destructive/50 px-3 py-2 text-sm text-destructive">
388
+ {t(restoreFailedKey, restoreFailedKey)}
389
+ </p>
390
+ ) : null}
391
+ </>
392
+ ) : null}
393
+ </div>
394
+ {restores && labelOf(version, versions) !== 'currentlyPublished' ? (
395
+ // Not offered for the version the row already holds, where a restore would add a version and an
396
+ // activity line and change nothing. Sticky at the popup's foot, so Restore stays in reach however long the diff is.
397
+ <SheetFooter className="bg-inherit sticky bottom-0 flex-row flex-wrap justify-end border-t">
398
+ {'_status' in version.version && version.version._status !== 'draft' ? (
399
+ <Button type="button" variant="outline" className="max-sm:h-11" data-button="restore-as-draft" disabled={restoring} onClick={() => setConfirming({ draft: true })}>
400
+ {t('version.restoreAsDraft', 'Restore as draft')}
401
+ </Button>
402
+ ) : null}
403
+ <Button type="button" className="max-sm:h-11" data-button="restore" disabled={restoring} onClick={() => setConfirming({ draft: false })}>
404
+ {t('version.restoreThisVersion', 'Restore this version')}
405
+ </Button>
406
+ </SheetFooter>
407
+ ) : null}
408
+ <AlertDialog open={!!confirming} onOpenChange={(open) => !open && setConfirming(undefined)}>
409
+ <AlertDialogContent>
410
+ <AlertDialogHeader>
411
+ <AlertDialogTitle>{t('version.confirmVersionRestoration', 'Confirm version restoration')}</AlertDialogTitle>
412
+ <AlertDialogDescription>
413
+ {(t('version.aboutToRestore', 'You are about to restore this {label} to the state it was in on {versionDate}.') ?? '')
414
+ .replace('{label}', titleOf({ kayan, copy }))
415
+ .replace('{versionDate}', when(version.updatedAt))}
416
+ </AlertDialogDescription>
417
+ </AlertDialogHeader>
418
+ <AlertDialogFooter className="max-sm:[&_button]:h-11">
419
+ <AlertDialogCancel>{t('admin.cancel', 'Cancel')}</AlertDialogCancel>
420
+ <AlertDialogAction data-button="confirm-restore" disabled={restoring} onClick={() => confirming && void restore(confirming.draft)}>
421
+ {confirming?.draft ? t('version.restoreAsDraft', 'Restore as draft') : t('version.restore', 'Restore')}
422
+ </AlertDialogAction>
423
+ </AlertDialogFooter>
424
+ </AlertDialogContent>
425
+ </AlertDialog>
426
+ {/* The registry's close, named in the page's language, in place of the
427
+ primitive's English one. */}
428
+ <CloseButton Close={SheetClose} label={t('admin.close', 'Close') ?? 'Close'} />
429
+ </SheetContent>
430
+ </Sheet>
431
+ )
432
+ }
@@ -0,0 +1,63 @@
1
+ // What `/platform` itself draws: where staff start, inside the platform's
2
+ // shell, with the figure card the dashboard and the portal homes draw for each
3
+ // list the nav offers the viewer, with how many rows of it they may read across
4
+ // every organization and a link to it. A list whose count the viewer may not
5
+ // read, or whose count failed, shows a dash.
6
+ import { ArrowRight } from 'lucide-react'
7
+ import type { LoaderTools, PageProps } from 'mithaq/admin'
8
+ import { useLocale, useScreen } from 'mithaq/admin/react'
9
+ import { PlatformShell, iconOf } from '../ui/app-shell.tsx'
10
+ import { KpiCard } from '../ui/kpi-card.tsx'
11
+ import { navLabel } from '../ui/screen-nav.tsx'
12
+
13
+ /** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
14
+ export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
15
+ Object.fromEntries(
16
+ await Promise.all(
17
+ Object.entries(access.collections ?? {})
18
+ .filter(([, one]) => one?.operations?.count === true)
19
+ .map(async ([kayan]) => [kayan, await (client({ kayan, operation: 'count', input: {} }) as Promise<{ totalDocs: number }>).then((found) => found.totalDocs).catch(() => undefined)] as const),
20
+ ),
21
+ )
22
+
23
+ export function Home({ loaderData: counts }: PageProps<Record<string, number | undefined>>) {
24
+ const { locale, t } = useLocale()
25
+ const screen = useScreen()
26
+ return (
27
+ <PlatformShell
28
+ regions={{
29
+ header: <h1>{t('platform.title', 'Platform')}</h1>,
30
+ main: (
31
+ <section data-dashboard="platform" className="@container grid gap-4">
32
+ <p className="text-muted-foreground">{t('platform.about', 'Every organization and every person on the platform, for its staff.')}</p>
33
+ {/* Cards as tall as their content, the figure and the link: the card's own padding and gap
34
+ are the roomy ones of the platform's style, which a count and one link do not need. */}
35
+ <div className="grid grid-cols-2 items-start gap-4 @2xl:grid-cols-3 @5xl:grid-cols-4 [&_[data-slot=card]]:[--card-spacing:--spacing(4)]">
36
+ {screen.nav.flatMap((entry) => {
37
+ if (!entry.kayan) return []
38
+ const Icon = iconOf(entry.path)
39
+ const counted = counts?.[entry.kayan]
40
+ return [
41
+ <KpiCard
42
+ key={entry.kayan}
43
+ kpi={entry.kayan}
44
+ icon={<Icon className="size-4" />}
45
+ title={navLabel({ entry, screen, locale, t })}
46
+ value={counted === undefined ? '—' : new Intl.NumberFormat(locale).format(counted)}
47
+ // Underlined, with an arrow that follows the reading direction, so it reads as a link.
48
+ footer={
49
+ <a href={`${screen.path}${entry.path}`} className="text-foreground inline-flex items-center gap-1 underline underline-offset-4 max-sm:min-h-11">
50
+ {t('portal.open', 'Open the list')}
51
+ <ArrowRight className="size-3.5 rtl:-scale-x-100" aria-hidden="true" />
52
+ </a>
53
+ }
54
+ />,
55
+ ]
56
+ })}
57
+ </div>
58
+ </section>
59
+ ),
60
+ }}
61
+ />
62
+ )
63
+ }
@@ -0,0 +1,114 @@
1
+ // The list's cell for the `slug` column. Rename is drawn only where this actor holds
2
+ // `organization.rename`, and members only where this actor reads them; the route refuses a rename
3
+ // without the key. A rename that lands asks the list behind it to be read again through
4
+ // `mithaq:refresh`, so the list keeps its scroll.
5
+ import { useState, type FormEvent } from 'react'
6
+ import { holds, type CellProps } from 'mithaq/admin'
7
+ import { useAccess, useLocale, useScreen } from 'mithaq/admin/react'
8
+ import { writeQuery } from '@/components/mithaq/list-query.ts'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
11
+ import { Input } from '@/components/ui/input'
12
+ import { Label } from '@/components/ui/label'
13
+
14
+ const slugShape = /^[a-z0-9]+(-[a-z0-9]+)*$/
15
+
16
+ export function OrganizationActions({ value, row }: CellProps) {
17
+ const { t } = useLocale()
18
+ const access = useAccess()
19
+ const screen = useScreen()
20
+ const renames = holds(access, 'organization.rename')
21
+ const members = holds(access, 'member.find') && screen.kayan.member?.pages.list !== undefined
22
+ const [open, setOpen] = useState(false)
23
+ const [refusal, setRefusal] = useState<string>()
24
+ const [done, setDone] = useState<string>()
25
+ /** Said here rather than by the browser's own bubble, which is in the browser's language. */
26
+ const [wrong, setWrong] = useState<{ name?: string; slug?: string }>({})
27
+ const id = String(row.id ?? '')
28
+ const save = async (event: FormEvent<HTMLFormElement>) => {
29
+ event.preventDefault()
30
+ const box = (key: string) => (event.currentTarget.elements.namedItem(key) as HTMLInputElement | null)?.value ?? ''
31
+ const name = box('name')
32
+ const slug = box('slug')
33
+ const found = {
34
+ name: name.trim() ? undefined : t('organization.name.required', 'This field is required.'),
35
+ slug: !slug ? t('organization.slug.required', 'This field is required.') : slugShape.test(slug) ? undefined : t('platform.slugPattern', 'A slug is lowercase letters and digits joined by single dashes, such as admin-org.'),
36
+ }
37
+ setWrong(found)
38
+ if (found.name || found.slug) return
39
+ const response = await fetch('/api/platform/organizations/update', {
40
+ method: 'POST',
41
+ headers: { 'content-type': 'application/json' },
42
+ body: JSON.stringify({ id, name, slug }),
43
+ })
44
+ if (!response.ok) {
45
+ const key = ((await response.json().catch(() => null)) as { data?: { key?: string } } | null)?.data?.key
46
+ return setRefusal((key && t(key)) ?? t('platform.refused', 'The platform refused that action.'))
47
+ }
48
+ window.dispatchEvent(new Event('mithaq:refresh'))
49
+ // A function, so a name holding `$&` is put in as written.
50
+ setDone((t('platform.renamed', 'The organization is now named {name}.') ?? '').replace('{name}', () => name))
51
+ }
52
+ return (
53
+ <div data-organization={id} className="flex items-center gap-2 whitespace-nowrap">
54
+ {/* An identifier, read left to right in either locale. */}
55
+ <span dir="ltr">{String(value ?? '')}</span>
56
+ {renames ? (
57
+ <Button size="sm" variant="outline" className="max-sm:h-11" data-action="rename" onClick={() => setOpen(true)}>
58
+ {t('platform.rename', 'Rename')}
59
+ </Button>
60
+ ) : null}
61
+ {members ? (
62
+ <Button
63
+ size="sm"
64
+ variant="outline"
65
+ className="max-sm:h-11"
66
+ data-action="members"
67
+ nativeButton={false}
68
+ render={<a href={`${screen.path}${screen.kayan.member!.pages.list!.path}?${new URLSearchParams(writeQuery({ where: { or: [{ and: [{ organizationId: { equals: id } }] }] } }))}`} />}
69
+ >
70
+ {t('platform.members', 'Members')}
71
+ </Button>
72
+ ) : null}
73
+ {open ? (
74
+ <Dialog open onOpenChange={setOpen}>
75
+ {/* Cancel is the way out, so the primitive's own close, named in
76
+ English alone, is not drawn beside it. */}
77
+ <DialogContent showCloseButton={false}>
78
+ <DialogHeader>
79
+ <DialogTitle>{t('platform.rename', 'Rename')}</DialogTitle>
80
+ <DialogDescription aria-live="polite">{done ?? t('platform.renameAbout', 'The new name and slug take the place of the old ones for everyone in this organization.')}</DialogDescription>
81
+ </DialogHeader>
82
+ {done ? (
83
+ <DialogFooter>
84
+ <DialogClose render={<Button type="button" variant="outline" />}>{t('admin.close', 'Close')}</DialogClose>
85
+ </DialogFooter>
86
+ ) : (
87
+ <form noValidate onSubmit={(event) => void save(event)} className="grid gap-4">
88
+ <div className="grid gap-2">
89
+ <Label htmlFor={`organization-name-${id}`}>{t('organization.name', 'Name')}</Label>
90
+ <Input id={`organization-name-${id}`} name="name" required aria-required dir="auto" defaultValue={String(row.name ?? '')} aria-invalid={wrong.name ? true : undefined} aria-describedby={wrong.name ? `organization-name-${id}-error` : undefined} />
91
+ {wrong.name ? <p id={`organization-name-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.name}</p> : null}
92
+ </div>
93
+ <div className="grid gap-2">
94
+ <Label htmlFor={`organization-slug-${id}`}>{t('organization.slug', 'Slug')}</Label>
95
+ <Input id={`organization-slug-${id}`} name="slug" required aria-required dir="ltr" pattern={slugShape.source.slice(1, -1)} defaultValue={String(value ?? '')} aria-invalid={wrong.slug ? true : undefined} aria-describedby={wrong.slug ? `organization-slug-${id}-error` : undefined} />
96
+ {wrong.slug ? <p id={`organization-slug-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.slug}</p> : null}
97
+ </div>
98
+ {refusal ? (
99
+ <p role="alert" className="text-sm text-destructive">
100
+ {refusal}
101
+ </p>
102
+ ) : null}
103
+ <DialogFooter>
104
+ <DialogClose render={<Button type="button" variant="outline" />}>{t('admin.cancel', 'Cancel')}</DialogClose>
105
+ <Button type="submit">{t('admin.save', 'Save')}</Button>
106
+ </DialogFooter>
107
+ </form>
108
+ )}
109
+ </DialogContent>
110
+ </Dialog>
111
+ ) : null}
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,30 @@
1
+ // The platform's record page: the shared one under a link back to the list the
2
+ // record belongs to, so a person's page, an organization's and a role's are never
3
+ // a dead end. The link is named as the nav names the list, which is already the
4
+ // viewer's own word for it.
5
+ import type { ReactNode } from 'react'
6
+ import { ArrowLeft } from 'lucide-react'
7
+ import type { LayoutProps } from 'mithaq/admin'
8
+ import { useKayan, useLocale, useScreen } from 'mithaq/admin/react'
9
+ import { RecordPage } from '../ui/record-page.tsx'
10
+ import { navLabel } from '../ui/screen-nav.tsx'
11
+
12
+ export const regions = ['main', 'side', 'danger'] as const
13
+
14
+ export function PlatformRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
15
+ const { locale, t } = useLocale()
16
+ const screen = useScreen()
17
+ const kayan = useKayan()
18
+ const list = screen.nav.find((entry) => entry.kayan === kayan.name)
19
+ return (
20
+ <div className="grid gap-4">
21
+ {list ? (
22
+ <a href={`${screen.path}${list.path}`} data-back="" className="inline-flex w-fit items-center gap-1.5 text-sm underline underline-offset-4 max-sm:min-h-11">
23
+ <ArrowLeft className="size-4 rtl:-scale-x-100" aria-hidden="true" />
24
+ <span>{navLabel({ entry: list, screen, locale, t })}</span>
25
+ </a>
26
+ ) : null}
27
+ <RecordPage {...props} />
28
+ </div>
29
+ )
30
+ }