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,108 @@
1
+ import { useState, type ReactElement } from 'react'
2
+ import { ChevronDown, Columns as LayoutColumns } from 'lucide-react'
3
+ import type { ClientField, ClientKayan } from 'mithaq/admin'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Checkbox } from '@/components/ui/checkbox'
6
+ import { Label } from '@/components/ui/label'
7
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
8
+ import { checkboxSize, labelFor, type Translate } from './controls.tsx'
9
+ import type { ColumnChoice } from './list-query.ts'
10
+
11
+ export type ColumnsProps = {
12
+ kayan: ClientKayan
13
+ /** Every column, in order, with whether it is shown. */
14
+ columns: readonly ColumnChoice[]
15
+ onChange: (columns: readonly ColumnChoice[]) => void
16
+ t: Translate
17
+ locale: string
18
+ }
19
+
20
+ /** Which fields the popup may offer: the ones this actor may read. Chrome only: the read masks a denied column whatever the popup shows. */
21
+ export const columnCandidates = (
22
+ kayan: ClientKayan,
23
+ reads?: (kayan: string, field: string) => boolean,
24
+ ): readonly ClientField[] =>
25
+ kayan.fields.filter(
26
+ (field) => !reads || reads(kayan.name, field.name),
27
+ )
28
+
29
+ /** The next column in the same section, or -1 at its end. The popup draws one list twice, so a neighbour never crosses into the other section. */
30
+ export const neighbour = ({ columns, at, by }: { columns: readonly ColumnChoice[]; at: number; by: -1 | 1 }): number => {
31
+ for (let index = at + by; index >= 0 && index < columns.length; index += by) {
32
+ if (columns[index]!.active === columns[at]!.active) return index
33
+ }
34
+ return -1
35
+ }
36
+
37
+ /** One column swapped with its neighbour in the same section; at a section's end the same list comes back. */
38
+ export const move = ({ columns, at, by }: { columns: readonly ColumnChoice[]; at: number; by: -1 | 1 }): readonly ColumnChoice[] => {
39
+ const to = neighbour({ columns, at, by })
40
+ if (to < 0) return columns
41
+ const next = [...columns]
42
+ next[at] = columns[to]!
43
+ next[to] = columns[at]!
44
+ return next
45
+ }
46
+
47
+ export function Columns({ columns, kayan, locale, onChange, t }: ColumnsProps): ReactElement {
48
+ const [open, setOpen] = useState(false)
49
+ const byName = new Map(kayan.fields.map((field) => [field.name, field]))
50
+ const nameOf = (name: string) => {
51
+ const field = byName.get(name)
52
+ return field ? labelFor({ kayan, field, copy: { t, locale } }) : name
53
+ }
54
+ const toggle = (name: string) =>
55
+ onChange(columns.map((one) => (one.name === name ? { ...one, active: !one.active } : one)))
56
+ // The box is a `role="checkbox"` button, so the label points at it by id; the kayan name keeps ids unique when two lists share a screen.
57
+ const boxId = (name: string) => `mithaq-column-${kayan.name}-${name}`
58
+ const section = (active: boolean) => (
59
+ <ul className="flex flex-col gap-1">
60
+ {columns.map((column, at) =>
61
+ column.active !== active ? null : (
62
+ <li key={column.name} className="flex items-center justify-between gap-2">
63
+ <span className="flex items-center gap-2">
64
+ <Checkbox className={checkboxSize} id={boxId(column.name)} checked={column.active} onCheckedChange={() => toggle(column.name)} />
65
+ <Label htmlFor={boxId(column.name)} className="font-normal">
66
+ {nameOf(column.name)}
67
+ </Label>
68
+ </span>
69
+ <span className="flex gap-1">
70
+ <Button type="button" variant="outline" size="icon-xs" className="max-sm:size-11" disabled={neighbour({ columns, at, by: -1 }) < 0} onClick={() => onChange(move({ columns, at, by: -1 }))} aria-label={`${t('columns.up', 'Move up') ?? 'Move up'}: ${nameOf(column.name)}`}>
71
+ ↑
72
+ </Button>
73
+ <Button type="button" variant="outline" size="icon-xs" className="max-sm:size-11" disabled={neighbour({ columns, at, by: 1 }) < 0} onClick={() => onChange(move({ columns, at, by: 1 }))} aria-label={`${t('columns.down', 'Move down') ?? 'Move down'}: ${nameOf(column.name)}`}>
74
+ ↓
75
+ </Button>
76
+ </span>
77
+ </li>
78
+ ),
79
+ )}
80
+ </ul>
81
+ )
82
+
83
+ return (
84
+ <Popover open={open} onOpenChange={setOpen}>
85
+ <PopoverTrigger
86
+ render={
87
+ <Button type="button" variant="outline" size="sm" className="max-sm:h-11">
88
+ <LayoutColumns />
89
+ <span className="hidden lg:inline">{t('columns.customize', 'Customize Columns') ?? 'Customize Columns'}</span>
90
+ <span className="lg:hidden">{t('columns.title', 'Columns') ?? 'Columns'}</span>
91
+ <ChevronDown />
92
+ </Button>
93
+ }
94
+ />
95
+ <PopoverContent align="end" className="flex max-h-[min(var(--available-height),28rem)] w-56 flex-col gap-3 overflow-y-auto p-3">
96
+ <div className="flex flex-col gap-1">
97
+ <p className="text-xs font-medium uppercase text-muted-foreground">{t('columns.shown', 'Shown') ?? 'Shown'}</p>
98
+ {section(true)}
99
+ </div>
100
+ <div className="flex flex-col gap-1">
101
+ <p className="text-xs font-medium uppercase text-muted-foreground">{t('columns.hidden', 'Not shown') ?? 'Not shown'}</p>
102
+ {section(false)}
103
+ </div>
104
+ </PopoverContent>
105
+ </Popover>
106
+ )
107
+ }
108
+ // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 94cd106d0ec41f77e03f3e843d130f7cf848c1c56619ab392d98a5d0d95bd0f1
@@ -0,0 +1,368 @@
1
+ import { useEffect, useRef, useState, type ReactElement } from 'react'
2
+ import type { ClientKayan } from 'mithaq/admin'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Input } from '@/components/ui/input'
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
6
+ import { controls, labelFor, type FindApi, type Translate } from './controls.tsx'
7
+ import { RelationshipControl } from './relationship-control.tsx'
8
+ import { operatorsByKind, valueShapeOf, type FilterPath } from './filter-operators.ts'
9
+ import { instantOf, type Condition, type FilterWhere } from './list-query.ts'
10
+
11
+ export type FiltersProps = {
12
+ /** Every path this kayan offers, already narrowed by the access report. */
13
+ paths: readonly FilterPath[]
14
+ value?: FilterWhere
15
+ /** Where a committed panel goes. `undefined` is "no filter at all". */
16
+ onChange: (where: FilterWhere | undefined) => void
17
+ t: Translate
18
+ locale: string
19
+ /** Every kayan of the build, so a relationship row's picker can find its target. */
20
+ client?: readonly ClientKayan[]
21
+ /** The typed client's reads, for that picker's own paging. */
22
+ api?: FindApi
23
+ }
24
+
25
+ type Row = { path: string; operator: string; value: unknown }
26
+
27
+ const rowOf = (condition: Condition): Row => {
28
+ const [path = '', operators = {}] = Object.entries(condition)[0] ?? []
29
+ const [operator = 'equals', value] = Object.entries(operators)[0] ?? []
30
+ return { path, operator, value }
31
+ }
32
+
33
+ const conditionOf = (row: Row): Condition => ({ [row.path]: { [row.operator]: row.value } })
34
+
35
+ /**
36
+ * The rows the panel holds: the `or` of `and`s a `FilterWhere` carries. Held as state, not derived
37
+ * from `value`, because a row with no value yet does not survive the round trip through the URL.
38
+ */
39
+ export type Conditions = readonly { and: Condition[] }[]
40
+
41
+ /** What a set of rows commits: the filled rows, and no group that keeps none. */
42
+ export const committedOf = (conditions: Conditions): FilterWhere | undefined => {
43
+ const kept = conditions
44
+ .map((group) => ({ and: group.and.filter((one) => !isEmptyConditionValue(rowOf(one).value)) }))
45
+ .filter((group) => group.and.length > 0)
46
+ return kept.length ? { or: kept } : undefined
47
+ }
48
+
49
+ /** One more row, in a group of its own; `and` is reached by adding and then switching the join. */
50
+ export const addCondition = (conditions: Conditions, condition: Condition): Conditions => [
51
+ ...conditions,
52
+ { and: [condition] },
53
+ ]
54
+
55
+ /** One row gone, and the group it emptied gone with it. */
56
+ export const removeCondition = ({ conditions, orIndex, andIndex }: { conditions: Conditions; orIndex: number; andIndex: number }): Conditions =>
57
+ conditions
58
+ .map((group, o) => (o === orIndex ? { and: group.and.filter((_, a) => a !== andIndex) } : group))
59
+ .filter((group) => group.and.length > 0)
60
+
61
+ /** One row rewritten in place. */
62
+ export const updateCondition = ({
63
+ conditions,
64
+ orIndex,
65
+ andIndex,
66
+ condition,
67
+ }: {
68
+ conditions: Conditions
69
+ orIndex: number
70
+ andIndex: number
71
+ condition: Condition
72
+ }): Conditions =>
73
+ conditions.map((group, o) => (o === orIndex ? { and: group.and.map((one, a) => (a === andIndex ? condition : one)) } : group))
74
+
75
+ /** Moves one row between `and` and `or` by flattening the groups to one list and rebuilding them from each row's join. */
76
+ export const updateJoin = ({
77
+ conditions,
78
+ orIndex,
79
+ andIndex,
80
+ join,
81
+ }: {
82
+ conditions: Conditions
83
+ orIndex: number
84
+ andIndex: number
85
+ join: 'and' | 'or'
86
+ }): Conditions => {
87
+ const flattened: { condition: Condition; join: 'and' | 'or' | null }[] = []
88
+ conditions.forEach((group, o) => {
89
+ group.and.forEach((condition, a) => {
90
+ const isTarget = o === orIndex && a === andIndex
91
+ const current = flattened.length === 0 ? null : a === 0 ? 'or' : 'and'
92
+ flattened.push({ condition, join: isTarget ? join : current })
93
+ })
94
+ })
95
+ const rebuilt: { and: Condition[] }[] = []
96
+ flattened.forEach(({ condition, join: itemJoin }, index) => {
97
+ if (index === 0 || itemJoin === 'or') rebuilt.push({ and: [condition] })
98
+ else rebuilt[rebuilt.length - 1]!.and.push(condition)
99
+ })
100
+ return rebuilt
101
+ }
102
+
103
+ const isEmptyConditionValue = (value: unknown): boolean => value === undefined || value === null || value === ''
104
+
105
+ /**
106
+ * An empty row reports `undefined` as soon as it mounts, and committing that stores
107
+ * `{ [operator]: undefined }`, a row gone on the next parse. Only a value edit can be a no-op.
108
+ */
109
+ const isNoOpConditionValueUpdate = (stored: unknown, incoming: unknown): boolean =>
110
+ isEmptyConditionValue(stored) && isEmptyConditionValue(incoming)
111
+
112
+ // A select item's value may not be '' (the primitive reserves it for nothing chosen), so an unanswered row is carried under this name and mapped back to ''.
113
+ const unanswered = 'unanswered'
114
+
115
+ // `like` needs telling apart from `contains`: the drizzle store runs one `ilike` per whitespace-separated word. Labels go through `t` under `operator.<name>`.
116
+ const operatorFallbacks: Readonly<Record<string, string>> = {
117
+ contains: 'contains',
118
+ equals: 'is',
119
+ exists: 'exists',
120
+ greater_than: 'is after',
121
+ greater_than_equal: 'is at least',
122
+ in: 'is one of',
123
+ less_than: 'is before',
124
+ less_than_equal: 'is at most',
125
+ like: 'matches every word',
126
+ not_equals: 'is not',
127
+ not_in: 'is none of',
128
+ }
129
+
130
+ const asText = (value: unknown) =>
131
+ value === null || value === undefined ? '' : Array.isArray(value) ? value.join(', ') : String(value)
132
+
133
+ // A moment typed with no zone becomes the instant it names on the viewer's clock; a moment with its own zone, and text no clock reads, are sent as typed for the server to judge.
134
+ const valueFrom = (kind: string, operator: string, text: string): unknown => {
135
+ const shape = valueShapeOf(operator)
136
+ if (shape === 'boolean') return text === '' ? undefined : text === 'true'
137
+ if (shape === 'list') {
138
+ const tokens = text.split(',').map((one) => one.trim()).filter(Boolean)
139
+ return tokens.length ? (kind === 'number' || kind === 'integer' ? tokens.map(Number) : kind === 'datetime' ? tokens.map(instantOf) : tokens) : undefined
140
+ }
141
+ if (text === '') return undefined
142
+ if (kind === 'checkbox') return text === 'true'
143
+ return kind === 'number' || kind === 'integer' ? Number(text) : kind === 'datetime' ? instantOf(text) : text
144
+ }
145
+
146
+ function ValueInput({ id, locale, operator, onCommit, path, t, value }: { id: string; locale: string; operator: string; path: FilterPath; t: Translate; value: unknown; onCommit: (value: unknown) => void }): ReactElement {
147
+ const { kind } = path
148
+ const [text, setText] = useState(asText(value))
149
+ // `sent` is the last commit read back as text; it differs from the typed text where the commit converts it.
150
+ const reported = useRef(asText(value))
151
+ const sent = useRef(asText(value))
152
+ useEffect(() => {
153
+ // Re-seeding from this input's own commit coming back would drop keystrokes typed since, or swap the moment typed for its instant.
154
+ const incoming = asText(value)
155
+ if (incoming !== sent.current) {
156
+ reported.current = sent.current = incoming
157
+ setText(incoming)
158
+ }
159
+ }, [value])
160
+ useEffect(() => {
161
+ if (text === reported.current) return
162
+ const timer = setTimeout(() => {
163
+ const next = valueFrom(kind, operator, text)
164
+ reported.current = text
165
+ sent.current = asText(next)
166
+ onCommit(next)
167
+ }, 300)
168
+ return () => clearTimeout(timer)
169
+ }, [kind, onCommit, operator, text])
170
+
171
+ const shape = valueShapeOf(operator)
172
+ if (shape === 'boolean' || kind === 'checkbox') {
173
+ return (
174
+ <Select value={text === '' ? unanswered : text} onValueChange={(next) => setText(next === null || next === unanswered ? '' : next)}>
175
+ {/* The word is the trigger's own child: the item list mounts only while open, so a server render would show an empty trigger. */}
176
+ <SelectTrigger aria-label="value" size="sm">
177
+ <SelectValue>{text === '' ? '—' : text}</SelectValue>
178
+ </SelectTrigger>
179
+ <SelectContent>
180
+ <SelectItem value={unanswered}>—</SelectItem>
181
+ <SelectItem value="true">true</SelectItem>
182
+ <SelectItem value="false">false</SelectItem>
183
+ </SelectContent>
184
+ </Select>
185
+ )
186
+ }
187
+ // A moment is sent as its ISO instant, so 09:30 picked at UTC+3 asks for 06:30Z and the picker draws it back on the reader's clock. `in` and `not_in` keep the token list.
188
+ if ((kind === 'date' || kind === 'datetime') && shape !== 'list') {
189
+ const Picker = controls[kind]!
190
+ return (
191
+ <Picker
192
+ field={path.field}
193
+ kayan={path.kayan}
194
+ values={{}}
195
+ setValue={() => {}}
196
+ t={t}
197
+ locale={locale}
198
+ id={id}
199
+ value={text}
200
+ readOnly={false}
201
+ onChange={(next) => setText(next instanceof Date ? next.toISOString() : asText(next))}
202
+ />
203
+ )
204
+ }
205
+ // A relationship reaches this arm only for `in` and `not_in`, a list of ids; its scalar operators get the picker.
206
+ const type = shape === 'list' ? 'text' : kind === 'number' || kind === 'integer' ? 'number' : kind === 'email' ? 'email' : 'text'
207
+ return (
208
+ <Input
209
+ aria-label="value"
210
+ className="w-auto"
211
+ type={type}
212
+ step={kind === 'integer' && shape !== 'list' ? '1' : undefined}
213
+ inputMode={kind === 'decimal' ? 'decimal' : undefined}
214
+ dir="auto"
215
+ value={text}
216
+ placeholder={shape === 'list' ? 'a, b, c' : undefined}
217
+ onChange={(event) => setText(event.target.value)}
218
+ />
219
+ )
220
+ }
221
+
222
+ export function Filters({ api, client, locale, onChange, paths, t, value }: FiltersProps): ReactElement | null {
223
+ const [conditions, setConditions] = useState<Conditions>(() => value?.or ?? [])
224
+ // What this panel commits comes back through the URL with the unfilled rows gone; re-seeding from that would drop the row being written. A `value` it did not report is an outside change and does re-seed.
225
+ const reported = useRef(JSON.stringify(value ?? null))
226
+ useEffect(() => {
227
+ const incoming = JSON.stringify(value ?? null)
228
+ if (incoming === reported.current) return
229
+ reported.current = incoming
230
+ setConditions(value?.or ?? [])
231
+ }, [value])
232
+
233
+ // No panel for a kayan with no filterable field; after the hooks, which must run on every render.
234
+ if (!paths.length) return null
235
+ const first = paths[0]!
236
+ const flat = conditions.flatMap((group, orIndex) => group.and.map((condition, andIndex) => ({ andIndex, condition, orIndex })))
237
+ const blank = (): Condition => ({ [first.path]: { [(operatorsByKind[first.kind] ?? ['equals'])[0]!]: undefined } })
238
+
239
+ const commit = (next: Conditions) => {
240
+ setConditions(next)
241
+ const where = committedOf(next)
242
+ const encoded = JSON.stringify(where ?? null)
243
+ reported.current = encoded
244
+ // Nothing goes up when the commit equals the filter the host holds: a host resets to page one on any `onChange`, so Add filter on page three would lose the page.
245
+ if (encoded !== JSON.stringify(value ?? null)) onChange(where)
246
+ }
247
+
248
+ const replace = (orIndex: number, andIndex: number, row: Row) =>
249
+ commit(updateCondition({ conditions, orIndex, andIndex, condition: conditionOf(row) }))
250
+
251
+ // With nothing committed the panel shows the virtual row below, so the click makes it real and appends beside it.
252
+ const add = () => commit(flat.length ? addCondition(conditions, blank()) : [{ and: [blank()] }, { and: [blank()] }])
253
+
254
+ const labelOf = (path: FilterPath) => {
255
+ const own = labelFor({ kayan: path.kayan, field: path.field, copy: { t, locale } })
256
+ return path.through && path.through !== path.field.name ? `${path.through} > ${own}` : own
257
+ }
258
+
259
+ // The virtual first row commits on the first real edit rather than on mount, so an untouched panel writes no URL.
260
+ const rows = flat.length ? flat : [{ andIndex: 0, condition: blank(), orIndex: 0 }]
261
+
262
+ return (
263
+ <div className="flex flex-col gap-2 rounded-lg border p-3" data-mithaq-filters={rows.length}>
264
+ {rows.map(({ andIndex, condition, orIndex }, index) => {
265
+ const row = rowOf(condition)
266
+ const path = paths.find((one) => one.path === row.path) ?? first
267
+ const operators = operatorsByKind[path.kind] ?? []
268
+ const operator = operators.includes(row.operator as never) ? row.operator : (operators[0] ?? 'equals')
269
+ const virtual = flat.length === 0
270
+ const write = (next: Row) => (virtual ? commit([{ and: [conditionOf(next)] }]) : replace(orIndex, andIndex, next))
271
+ return (
272
+ <div key={`${orIndex}.${andIndex}`} className="flex flex-wrap items-center gap-2">
273
+ {index === 0 ? (
274
+ <span className="text-sm text-muted-foreground">{t('filter.where', 'Where') ?? 'Where'}</span>
275
+ ) : (
276
+ <Select
277
+ value={andIndex === 0 ? 'or' : 'and'}
278
+ onValueChange={(next) => commit(updateJoin({ conditions, orIndex, andIndex, join: next === 'or' ? 'or' : 'and' }))}
279
+ >
280
+ <SelectTrigger aria-label={t('filter.join', 'Join') ?? 'Join'} size="sm">
281
+ <SelectValue>
282
+ {andIndex === 0 ? (t('filter.or', 'or') ?? 'or') : (t('filter.and', 'and') ?? 'and')}
283
+ </SelectValue>
284
+ </SelectTrigger>
285
+ <SelectContent>
286
+ <SelectItem value="and">{t('filter.and', 'and') ?? 'and'}</SelectItem>
287
+ <SelectItem value="or">{t('filter.or', 'or') ?? 'or'}</SelectItem>
288
+ </SelectContent>
289
+ </Select>
290
+ )}
291
+ <Select
292
+ value={path.path}
293
+ onValueChange={(chosen) => {
294
+ const next = paths.find((one) => one.path === chosen) ?? first
295
+ const kept = (operatorsByKind[next.kind] ?? []).includes(operator as never) ? operator : (operatorsByKind[next.kind] ?? ['equals'])[0]!
296
+ write({ path: next.path, operator: kept, value: undefined })
297
+ }}
298
+ >
299
+ <SelectTrigger aria-label={t('filter.field', 'Field') ?? 'Field'} size="sm">
300
+ <SelectValue>{labelOf(path)}</SelectValue>
301
+ </SelectTrigger>
302
+ <SelectContent>
303
+ {paths.map((one) => (
304
+ <SelectItem key={one.path} value={one.path}>
305
+ {labelOf(one)}
306
+ </SelectItem>
307
+ ))}
308
+ </SelectContent>
309
+ </Select>
310
+ <Select value={operator} onValueChange={(chosen) => chosen && write({ path: path.path, operator: chosen, value: undefined })}>
311
+ <SelectTrigger aria-label={t('filter.operator', 'Operator') ?? 'Operator'} size="sm">
312
+ <SelectValue>{t(`operator.${operator}`, operatorFallbacks[operator]) ?? operator}</SelectValue>
313
+ </SelectTrigger>
314
+ <SelectContent>
315
+ {operators.map((one) => (
316
+ <SelectItem key={one} value={one}>
317
+ {t(`operator.${one}`, operatorFallbacks[one]) ?? one}
318
+ </SelectItem>
319
+ ))}
320
+ </SelectContent>
321
+ </Select>
322
+ {path.kind === 'relationship' && valueShapeOf(operator) === 'scalar' ? (
323
+ // The form's picker rather than a box for typing a uuid; `in` and `not_in` keep the token list because the picker settles on one id.
324
+ <RelationshipControl
325
+ field={path.field}
326
+ values={{}}
327
+ setValue={() => {}}
328
+ kayan={path.kayan}
329
+ client={client}
330
+ api={api}
331
+ t={t}
332
+ id={`filter-${orIndex}-${andIndex}`}
333
+ value={row.value}
334
+ readOnly={false}
335
+ onChange={(next) => {
336
+ if (isNoOpConditionValueUpdate(row.value, next)) return
337
+ write({ path: path.path, operator, value: next })
338
+ }}
339
+ />
340
+ ) : (
341
+ <ValueInput
342
+ id={`filter-${orIndex}-${andIndex}`}
343
+ locale={locale}
344
+ path={path}
345
+ t={t}
346
+ operator={operator}
347
+ value={row.value}
348
+ onCommit={(next) => {
349
+ if (isNoOpConditionValueUpdate(row.value, next)) return
350
+ write({ path: path.path, operator, value: next })
351
+ }}
352
+ />
353
+ )}
354
+ <Button type="button" variant="outline" size="sm" onClick={() => commit(virtual ? [] : removeCondition({ conditions, orIndex, andIndex }))}>
355
+ {t('filter.remove', 'Remove') ?? 'Remove'}
356
+ </Button>
357
+ </div>
358
+ )
359
+ })}
360
+ <div>
361
+ <Button type="button" variant="outline" size="sm" onClick={add}>
362
+ {t('filter.add', 'Add filter') ?? 'Add filter'}
363
+ </Button>
364
+ </div>
365
+ </div>
366
+ )
367
+ }
368
+ // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 8606b5fd4339471a01d4f8ff1c783d4c79ae3c0f92bd195586d04260a77a9ea0
@@ -0,0 +1,153 @@
1
+ import { useEffect, useState, type ReactElement } from 'react'
2
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
3
+ import { Button } from '@/components/ui/button'
4
+ import { DirectionalIcon } from '@/components/ui/directional-icon'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Label } from '@/components/ui/label'
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
8
+ import type { Translate } from './controls.tsx'
9
+ import { useScreen } from 'mithaq/admin/react'
10
+ import { limitsUpTo } from './list-query.ts'
11
+
12
+ /** As much of one page as the pager reads. */
13
+ export type PageInfo = {
14
+ hasNextPage: boolean
15
+ hasPrevPage: boolean
16
+ limit: number
17
+ nextPage: number | null
18
+ page: number
19
+ prevPage: number | null
20
+ totalDocs: number
21
+ totalPages: number
22
+ }
23
+
24
+ export type PaginationProps = {
25
+ page: PageInfo
26
+ onPage: (page: number) => void
27
+ /** A new page size. The host resets the page to one, as it does for a filter. */
28
+ onLimit: (limit: number) => void
29
+ t: Translate
30
+ locale: string
31
+ }
32
+
33
+ // The trigger is a button, not a native select, so the label points at it by id; one pager renders per screen.
34
+ const perPageId = 'mithaq-per-page'
35
+
36
+ export function Pagination({ locale, onLimit, onPage, page, t }: PaginationProps): ReactElement {
37
+ const [typed, setTyped] = useState(String(page.page))
38
+ useEffect(() => setTyped(String(page.page)), [page.page])
39
+ const { maxLimit } = useScreen()
40
+
41
+ const clamp = () => {
42
+ const parsed = Number.parseInt(typed, 10)
43
+ const within = Number.isInteger(parsed) ? Math.min(Math.max(parsed, 1), Math.max(page.totalPages, 1)) : page.page
44
+ setTyped(String(within))
45
+ if (within !== page.page) onPage(within)
46
+ }
47
+
48
+ const from = page.totalDocs === 0 ? 0 : (page.page - 1) * page.limit + 1
49
+ const to = Math.min(page.page * page.limit, page.totalDocs)
50
+
51
+ return (
52
+ <footer className="flex flex-wrap items-center justify-between gap-x-8 gap-y-3 px-2">
53
+ <span className="text-muted-foreground flex flex-1 text-sm whitespace-nowrap" data-mithaq-range={`${from}-${to}/${page.totalDocs}`}>
54
+ {from.toLocaleString(locale)}–{to.toLocaleString(locale)} {t('admin.of', 'of') ?? 'of'} {page.totalDocs.toLocaleString(locale)}
55
+ </span>
56
+ {/* `contents` on a phone, so the size box joins the range's line and the rest wraps. */}
57
+ <div className="ms-auto flex items-center gap-8 max-sm:contents">
58
+ <div className="flex items-center gap-2">
59
+ <Label className="text-sm font-medium" htmlFor={perPageId}>
60
+ {t('admin.perPage', 'Per page') ?? 'Per page'}
61
+ </Label>
62
+ {/* The size is the trigger's own child: the item list mounts only while open, so a server render would show an empty trigger. */}
63
+ <Select value={String(page.limit)} onValueChange={(next: string | null) => next && onLimit(Number(next))}>
64
+ <SelectTrigger id={perPageId} size="sm" className="h-8 w-[70px] max-sm:data-[size=sm]:h-11">
65
+ <SelectValue>{page.limit}</SelectValue>
66
+ </SelectTrigger>
67
+ <SelectContent side="top">
68
+ {limitsUpTo(maxLimit).map((one) => (
69
+ <SelectItem key={one} value={String(one)}>
70
+ {one}
71
+ </SelectItem>
72
+ ))}
73
+ </SelectContent>
74
+ </Select>
75
+ </div>
76
+ <div className="flex w-fit items-center justify-center gap-2 text-sm font-medium">
77
+ <Input
78
+ className="h-8 w-16 text-center max-sm:h-11"
79
+ type="number"
80
+ min={1}
81
+ max={Math.max(page.totalPages, 1)}
82
+ value={typed}
83
+ aria-label={t('admin.page', 'Page') ?? 'Page'}
84
+ onChange={(event) => setTyped(event.target.value)}
85
+ onBlur={clamp}
86
+ onKeyDown={(event) => {
87
+ if (event.key === 'Enter') clamp()
88
+ if (event.key === 'Escape') setTyped(String(page.page))
89
+ }}
90
+ />
91
+ <span className="text-muted-foreground">
92
+ {t('admin.of', 'of') ?? 'of'} {Math.max(page.totalPages, 1).toLocaleString(locale)}
93
+ </span>
94
+ </div>
95
+ <div className="ms-auto flex items-center gap-2 sm:ms-0">
96
+ <Button
97
+ type="button"
98
+ variant="outline"
99
+ size="icon"
100
+ className="hidden size-8 sm:flex"
101
+ disabled={!page.hasPrevPage}
102
+ onClick={() => onPage(1)}
103
+ >
104
+ <span className="sr-only">{t('admin.first', 'First') ?? 'First'}</span>
105
+ <DirectionalIcon>
106
+ <ChevronsLeft />
107
+ </DirectionalIcon>
108
+ </Button>
109
+ <Button
110
+ type="button"
111
+ variant="outline"
112
+ size="icon"
113
+ className="size-8 max-sm:size-11"
114
+ disabled={!page.hasPrevPage}
115
+ onClick={() => onPage(page.prevPage ?? 1)}
116
+ >
117
+ <span className="sr-only">{t('admin.previous', 'Previous') ?? 'Previous'}</span>
118
+ <DirectionalIcon>
119
+ <ChevronLeft />
120
+ </DirectionalIcon>
121
+ </Button>
122
+ <Button
123
+ type="button"
124
+ variant="outline"
125
+ size="icon"
126
+ className="size-8 max-sm:size-11"
127
+ disabled={!page.hasNextPage}
128
+ onClick={() => onPage(page.nextPage ?? page.page)}
129
+ >
130
+ <span className="sr-only">{t('admin.next', 'Next') ?? 'Next'}</span>
131
+ <DirectionalIcon>
132
+ <ChevronRight />
133
+ </DirectionalIcon>
134
+ </Button>
135
+ <Button
136
+ type="button"
137
+ variant="outline"
138
+ size="icon"
139
+ className="hidden size-8 sm:flex"
140
+ disabled={!page.hasNextPage}
141
+ onClick={() => onPage(Math.max(page.totalPages, 1))}
142
+ >
143
+ <span className="sr-only">{t('admin.last', 'Last') ?? 'Last'}</span>
144
+ <DirectionalIcon>
145
+ <ChevronsRight />
146
+ </DirectionalIcon>
147
+ </Button>
148
+ </div>
149
+ </div>
150
+ </footer>
151
+ )
152
+ }
153
+ // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 78d68b28893401072ea1b499a0b3c22588ded882bd7964122ccbb0048698d373