mvc-kit 2.12.5 → 2.13.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 (382) hide show
  1. package/BEST_PRACTICES.md +1390 -0
  2. package/agent-config/bin/postinstall.mjs +4 -3
  3. package/agent-config/bin/setup.mjs +5 -1
  4. package/agent-config/claude-code/agents/mvc-kit-architect.md +16 -8
  5. package/agent-config/claude-code/skills/guide/SKILL.md +29 -7
  6. package/agent-config/claude-code/skills/guide/patterns.md +12 -0
  7. package/agent-config/claude-code/skills/guide/recipes.md +510 -0
  8. package/agent-config/claude-code/skills/guide/testing.md +297 -0
  9. package/agent-config/claude-code/skills/review/SKILL.md +3 -13
  10. package/agent-config/claude-code/skills/review/checklist.md +30 -5
  11. package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
  12. package/agent-config/lib/install-claude.mjs +90 -25
  13. package/dist/Channel.cjs +276 -300
  14. package/dist/Channel.cjs.map +1 -1
  15. package/dist/Channel.js +275 -299
  16. package/dist/Channel.js.map +1 -1
  17. package/dist/Collection.cjs +424 -504
  18. package/dist/Collection.cjs.map +1 -1
  19. package/dist/Collection.js +423 -503
  20. package/dist/Collection.js.map +1 -1
  21. package/dist/Controller.cjs +70 -67
  22. package/dist/Controller.cjs.map +1 -1
  23. package/dist/Controller.js +69 -66
  24. package/dist/Controller.js.map +1 -1
  25. package/dist/EventBus.cjs +77 -88
  26. package/dist/EventBus.cjs.map +1 -1
  27. package/dist/EventBus.js +76 -87
  28. package/dist/EventBus.js.map +1 -1
  29. package/dist/Feed.cjs +81 -77
  30. package/dist/Feed.cjs.map +1 -1
  31. package/dist/Feed.js +80 -76
  32. package/dist/Feed.js.map +1 -1
  33. package/dist/Model.cjs +181 -207
  34. package/dist/Model.cjs.map +1 -1
  35. package/dist/Model.js +179 -205
  36. package/dist/Model.js.map +1 -1
  37. package/dist/Pagination.cjs +75 -73
  38. package/dist/Pagination.cjs.map +1 -1
  39. package/dist/Pagination.js +74 -72
  40. package/dist/Pagination.js.map +1 -1
  41. package/dist/Pending.cjs +255 -287
  42. package/dist/Pending.cjs.map +1 -1
  43. package/dist/Pending.js +253 -285
  44. package/dist/Pending.js.map +1 -1
  45. package/dist/PersistentCollection.cjs +242 -285
  46. package/dist/PersistentCollection.cjs.map +1 -1
  47. package/dist/PersistentCollection.js +241 -284
  48. package/dist/PersistentCollection.js.map +1 -1
  49. package/dist/Resource.cjs +166 -174
  50. package/dist/Resource.cjs.map +1 -1
  51. package/dist/Resource.js +164 -172
  52. package/dist/Resource.js.map +1 -1
  53. package/dist/Selection.cjs +84 -94
  54. package/dist/Selection.cjs.map +1 -1
  55. package/dist/Selection.js +83 -93
  56. package/dist/Selection.js.map +1 -1
  57. package/dist/Service.cjs +54 -55
  58. package/dist/Service.cjs.map +1 -1
  59. package/dist/Service.js +53 -54
  60. package/dist/Service.js.map +1 -1
  61. package/dist/Sorting.cjs +102 -101
  62. package/dist/Sorting.cjs.map +1 -1
  63. package/dist/Sorting.js +102 -101
  64. package/dist/Sorting.js.map +1 -1
  65. package/dist/Trackable.cjs +112 -80
  66. package/dist/Trackable.cjs.map +1 -1
  67. package/dist/Trackable.js +111 -79
  68. package/dist/Trackable.js.map +1 -1
  69. package/dist/ViewModel.cjs +528 -576
  70. package/dist/ViewModel.cjs.map +1 -1
  71. package/dist/ViewModel.js +525 -573
  72. package/dist/ViewModel.js.map +1 -1
  73. package/dist/bindPublicMethods.cjs +43 -24
  74. package/dist/bindPublicMethods.cjs.map +1 -1
  75. package/dist/bindPublicMethods.js +43 -24
  76. package/dist/bindPublicMethods.js.map +1 -1
  77. package/dist/errors.cjs +67 -68
  78. package/dist/errors.cjs.map +1 -1
  79. package/dist/errors.js +68 -71
  80. package/dist/errors.js.map +1 -1
  81. package/dist/mvc-kit.cjs +44 -46
  82. package/dist/mvc-kit.js +5 -32
  83. package/dist/produceDraft.cjs +105 -95
  84. package/dist/produceDraft.cjs.map +1 -1
  85. package/dist/produceDraft.js +106 -97
  86. package/dist/produceDraft.js.map +1 -1
  87. package/dist/react/components/CardList.cjs +30 -40
  88. package/dist/react/components/CardList.cjs.map +1 -1
  89. package/dist/react/components/CardList.js +31 -41
  90. package/dist/react/components/CardList.js.map +1 -1
  91. package/dist/react/components/DataTable.cjs +146 -169
  92. package/dist/react/components/DataTable.cjs.map +1 -1
  93. package/dist/react/components/DataTable.js +147 -170
  94. package/dist/react/components/DataTable.js.map +1 -1
  95. package/dist/react/components/InfiniteScroll.cjs +51 -42
  96. package/dist/react/components/InfiniteScroll.cjs.map +1 -1
  97. package/dist/react/components/InfiniteScroll.js +52 -43
  98. package/dist/react/components/InfiniteScroll.js.map +1 -1
  99. package/dist/react/components/types.cjs +10 -6
  100. package/dist/react/components/types.cjs.map +1 -1
  101. package/dist/react/components/types.js +11 -9
  102. package/dist/react/components/types.js.map +1 -1
  103. package/dist/react/guards.cjs +10 -6
  104. package/dist/react/guards.cjs.map +1 -1
  105. package/dist/react/guards.js +11 -9
  106. package/dist/react/guards.js.map +1 -1
  107. package/dist/react/provider.cjs +23 -20
  108. package/dist/react/provider.cjs.map +1 -1
  109. package/dist/react/provider.js +23 -21
  110. package/dist/react/provider.js.map +1 -1
  111. package/dist/react/use-event-bus.cjs +24 -20
  112. package/dist/react/use-event-bus.cjs.map +1 -1
  113. package/dist/react/use-event-bus.js +24 -21
  114. package/dist/react/use-event-bus.js.map +1 -1
  115. package/dist/react/use-instance.cjs +43 -36
  116. package/dist/react/use-instance.cjs.map +1 -1
  117. package/dist/react/use-instance.js +43 -36
  118. package/dist/react/use-instance.js.map +1 -1
  119. package/dist/react/use-local.cjs +48 -64
  120. package/dist/react/use-local.cjs.map +1 -1
  121. package/dist/react/use-local.js +47 -63
  122. package/dist/react/use-local.js.map +1 -1
  123. package/dist/react/use-model.cjs +84 -98
  124. package/dist/react/use-model.cjs.map +1 -1
  125. package/dist/react/use-model.js +84 -100
  126. package/dist/react/use-model.js.map +1 -1
  127. package/dist/react/use-singleton.cjs +19 -23
  128. package/dist/react/use-singleton.cjs.map +1 -1
  129. package/dist/react/use-singleton.js +16 -20
  130. package/dist/react/use-singleton.js.map +1 -1
  131. package/dist/react/use-subscribe-only.cjs +28 -22
  132. package/dist/react/use-subscribe-only.cjs.map +1 -1
  133. package/dist/react/use-subscribe-only.js +28 -22
  134. package/dist/react/use-subscribe-only.js.map +1 -1
  135. package/dist/react/use-teardown.cjs +20 -19
  136. package/dist/react/use-teardown.cjs.map +1 -1
  137. package/dist/react/use-teardown.js +20 -19
  138. package/dist/react/use-teardown.js.map +1 -1
  139. package/dist/react-native/NativeCollection.cjs +98 -78
  140. package/dist/react-native/NativeCollection.cjs.map +1 -1
  141. package/dist/react-native/NativeCollection.js +97 -77
  142. package/dist/react-native/NativeCollection.js.map +1 -1
  143. package/dist/react-native.cjs +2 -4
  144. package/dist/react-native.js +1 -4
  145. package/dist/react.cjs +24 -26
  146. package/dist/react.js +1 -17
  147. package/dist/singleton.cjs +28 -22
  148. package/dist/singleton.cjs.map +1 -1
  149. package/dist/singleton.js +29 -26
  150. package/dist/singleton.js.map +1 -1
  151. package/dist/walkPrototypeChain.cjs +20 -12
  152. package/dist/walkPrototypeChain.cjs.map +1 -1
  153. package/dist/walkPrototypeChain.js +21 -13
  154. package/dist/walkPrototypeChain.js.map +1 -1
  155. package/dist/web/IndexedDBCollection.cjs +53 -36
  156. package/dist/web/IndexedDBCollection.cjs.map +1 -1
  157. package/dist/web/IndexedDBCollection.js +52 -35
  158. package/dist/web/IndexedDBCollection.js.map +1 -1
  159. package/dist/web/WebStorageCollection.cjs +82 -84
  160. package/dist/web/WebStorageCollection.cjs.map +1 -1
  161. package/dist/web/WebStorageCollection.js +81 -83
  162. package/dist/web/WebStorageCollection.js.map +1 -1
  163. package/dist/web/idb.cjs +107 -99
  164. package/dist/web/idb.cjs.map +1 -1
  165. package/dist/web/idb.js +108 -105
  166. package/dist/web/idb.js.map +1 -1
  167. package/dist/web.cjs +4 -6
  168. package/dist/web.js +1 -5
  169. package/dist/wrapAsyncMethods.cjs +141 -168
  170. package/dist/wrapAsyncMethods.cjs.map +1 -1
  171. package/dist/wrapAsyncMethods.js +141 -168
  172. package/dist/wrapAsyncMethods.js.map +1 -1
  173. package/examples/primitive/channel.ts +109 -0
  174. package/examples/primitive/collection.ts +118 -0
  175. package/examples/primitive/controller.ts +118 -0
  176. package/examples/primitive/counter.ts +108 -0
  177. package/examples/primitive/env.d.ts +1 -0
  178. package/examples/primitive/eventbus.ts +77 -0
  179. package/examples/primitive/feed.ts +162 -0
  180. package/examples/primitive/model.ts +82 -0
  181. package/examples/primitive/pagination.ts +91 -0
  182. package/examples/primitive/pending.ts +189 -0
  183. package/examples/primitive/persistent-collection.ts +116 -0
  184. package/examples/primitive/resource.ts +114 -0
  185. package/examples/primitive/selection.ts +96 -0
  186. package/examples/primitive/sorting.ts +112 -0
  187. package/examples/primitive/timer.ts +58 -0
  188. package/examples/primitive/trackable.ts +225 -0
  189. package/examples/primitive/tsconfig.json +20 -0
  190. package/examples/primitive/viewmodel-service.ts +161 -0
  191. package/examples/react/AuthExample/index.html +12 -0
  192. package/examples/react/AuthExample/src/App.tsx +29 -0
  193. package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
  194. package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
  195. package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
  196. package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
  197. package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
  198. package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
  199. package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
  200. package/examples/react/AuthExample/src/env.d.ts +10 -0
  201. package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
  202. package/examples/react/AuthExample/src/main.tsx +10 -0
  203. package/examples/react/AuthExample/src/mock/api.ts +78 -0
  204. package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
  205. package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
  206. package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
  207. package/examples/react/AuthExample/src/styles.css +445 -0
  208. package/examples/react/AuthExample/src/types/auth.ts +12 -0
  209. package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
  210. package/examples/react/AuthExample/tsconfig.json +22 -0
  211. package/examples/react/AuthExample/vite.config.ts +18 -0
  212. package/examples/react/ComplexApp/index.html +12 -0
  213. package/examples/react/ComplexApp/src/App.tsx +17 -0
  214. package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
  215. package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
  216. package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
  217. package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
  218. package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
  219. package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
  220. package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
  221. package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
  222. package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
  223. package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
  224. package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
  225. package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
  226. package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
  227. package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
  228. package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
  229. package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
  230. package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
  231. package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
  232. package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
  233. package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
  234. package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
  235. package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
  236. package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
  237. package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
  238. package/examples/react/ComplexApp/src/env.d.ts +10 -0
  239. package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
  240. package/examples/react/ComplexApp/src/main.tsx +10 -0
  241. package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
  242. package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
  243. package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
  244. package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
  245. package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
  246. package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
  247. package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
  248. package/examples/react/ComplexApp/src/styles.css +463 -0
  249. package/examples/react/ComplexApp/src/types/activity.ts +8 -0
  250. package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
  251. package/examples/react/ComplexApp/src/types/social.ts +8 -0
  252. package/examples/react/ComplexApp/src/types/users.ts +6 -0
  253. package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
  254. package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
  255. package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
  256. package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
  257. package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
  258. package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
  259. package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
  260. package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
  261. package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
  262. package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
  263. package/examples/react/ComplexApp/tsconfig.json +22 -0
  264. package/examples/react/ComplexApp/vite.config.ts +18 -0
  265. package/examples/react/FullApp/index.html +12 -0
  266. package/examples/react/FullApp/src/App.tsx +28 -0
  267. package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
  268. package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
  269. package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
  270. package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
  271. package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
  272. package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
  273. package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
  274. package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
  275. package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
  276. package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
  277. package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
  278. package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
  279. package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
  280. package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
  281. package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
  282. package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
  283. package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
  284. package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
  285. package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
  286. package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
  287. package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
  288. package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
  289. package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
  290. package/examples/react/FullApp/src/env.d.ts +10 -0
  291. package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
  292. package/examples/react/FullApp/src/main.tsx +10 -0
  293. package/examples/react/FullApp/src/mock/delay.ts +21 -0
  294. package/examples/react/FullApp/src/mock/locations.ts +76 -0
  295. package/examples/react/FullApp/src/mock/messages.ts +237 -0
  296. package/examples/react/FullApp/src/mock/users.ts +84 -0
  297. package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
  298. package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
  299. package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
  300. package/examples/react/FullApp/src/services/AuthService.ts +18 -0
  301. package/examples/react/FullApp/src/services/LocationService.ts +23 -0
  302. package/examples/react/FullApp/src/services/MessageService.ts +65 -0
  303. package/examples/react/FullApp/src/services/UserService.ts +23 -0
  304. package/examples/react/FullApp/src/styles.css +767 -0
  305. package/examples/react/FullApp/src/types/conversation.ts +7 -0
  306. package/examples/react/FullApp/src/types/location.ts +12 -0
  307. package/examples/react/FullApp/src/types/message.ts +7 -0
  308. package/examples/react/FullApp/src/types/user.ts +10 -0
  309. package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
  310. package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
  311. package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
  312. package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
  313. package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
  314. package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
  315. package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
  316. package/examples/react/FullApp/tsconfig.json +22 -0
  317. package/examples/react/FullApp/vite.config.ts +18 -0
  318. package/examples/react/WorkerApp/index.html +12 -0
  319. package/examples/react/WorkerApp/src/App.tsx +24 -0
  320. package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
  321. package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
  322. package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
  323. package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
  324. package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
  325. package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
  326. package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
  327. package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
  328. package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
  329. package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
  330. package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
  331. package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
  332. package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
  333. package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
  334. package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
  335. package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
  336. package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
  337. package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
  338. package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
  339. package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
  340. package/examples/react/WorkerApp/src/env.d.ts +10 -0
  341. package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
  342. package/examples/react/WorkerApp/src/main.tsx +10 -0
  343. package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
  344. package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
  345. package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
  346. package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
  347. package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
  348. package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
  349. package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
  350. package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
  351. package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
  352. package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
  353. package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
  354. package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
  355. package/examples/react/WorkerApp/src/styles.css +756 -0
  356. package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
  357. package/examples/react/WorkerApp/src/types/message.ts +7 -0
  358. package/examples/react/WorkerApp/src/types/shift.ts +13 -0
  359. package/examples/react/WorkerApp/src/types/site.ts +8 -0
  360. package/examples/react/WorkerApp/src/types/worker.ts +8 -0
  361. package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
  362. package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
  363. package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
  364. package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
  365. package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
  366. package/examples/react/WorkerApp/tsconfig.json +22 -0
  367. package/examples/react/WorkerApp/vite.config.ts +18 -0
  368. package/package.json +11 -9
  369. package/src/Pending.test.ts +1 -2
  370. package/src/Sorting.test.ts +1 -1
  371. package/src/produceDraft.test.ts +3 -3
  372. package/src/react/components/CardList.test.tsx +1 -1
  373. package/src/react/components/DataTable.test.tsx +1 -1
  374. package/src/react/components/InfiniteScroll.test.tsx +5 -5
  375. package/dist/mvc-kit.cjs.map +0 -1
  376. package/dist/mvc-kit.js.map +0 -1
  377. package/dist/react-native.cjs.map +0 -1
  378. package/dist/react-native.js.map +0 -1
  379. package/dist/react.cjs.map +0 -1
  380. package/dist/react.js.map +0 -1
  381. package/dist/web.cjs.map +0 -1
  382. package/dist/web.js.map +0 -1
@@ -0,0 +1,225 @@
1
+ import { Trackable, ViewModel, bindPublicMethods } from 'mvc-kit';
2
+
3
+ // Trackable: Base class for custom reactive objects
4
+ //
5
+ // Provides the subscribable + disposable + auto-bind foundation that
6
+ // all composable helpers (Sorting, Selection, Feed, Pagination, Pending)
7
+ // are built on. Extend it to build your own reactive objects that
8
+ // integrate with ViewModel's auto-tracking system.
9
+ //
10
+ // What Trackable provides:
11
+ // - subscribe() / notify() for change notifications
12
+ // - dispose() / disposeSignal / addCleanup() for lifecycle
13
+ // - Auto-bound public methods (point-free callbacks)
14
+ //
15
+ // What Trackable does NOT provide:
16
+ // - State management (no set(), no state getter)
17
+ // - Computed getters / memoization
18
+ // - Async tracking (no vm.async)
19
+ // - init() lifecycle (implement Initializable yourself if needed)
20
+
21
+ // --- Example 1: Custom RPC query wrapper ---
22
+
23
+ interface RPCResponse<T> {
24
+ data: T;
25
+ success: boolean;
26
+ code: number;
27
+ }
28
+
29
+ class RPCQuery<Args, Data> extends Trackable {
30
+ private _data: Data | undefined = undefined;
31
+ private _loading = false;
32
+ private _error: string | null = null;
33
+ private _callCounter = 0;
34
+
35
+ // Computed getters — auto-tracked when read inside ViewModel getters
36
+ get data(): Data | undefined { return this._data; }
37
+ get loading(): boolean { return this._loading; }
38
+ get error(): string | null { return this._error; }
39
+
40
+ constructor(private _endpoint: string) {
41
+ super();
42
+ }
43
+
44
+ async call(args?: Args): Promise<RPCResponse<Data>> {
45
+ if (this.disposed) throw new Error('RPCQuery: call() after dispose');
46
+
47
+ const callId = ++this._callCounter;
48
+ this._loading = true;
49
+ this._error = null;
50
+ this.notify();
51
+
52
+ try {
53
+ // Simulate an RPC call
54
+ const response = await simulateRPC<Data>(this._endpoint, args);
55
+
56
+ // Stale-call guard: only apply if this is still the latest call
57
+ if (!this.disposed && callId === this._callCounter) {
58
+ this._data = response.data;
59
+ this._loading = false;
60
+ this._error = null;
61
+ this.notify();
62
+ }
63
+
64
+ return response;
65
+ } catch (err) {
66
+ if (!this.disposed && callId === this._callCounter) {
67
+ this._loading = false;
68
+ this._error = (err as Error).message;
69
+ this.notify();
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ clear() {
76
+ this._data = undefined;
77
+ this._error = null;
78
+ this.notify();
79
+ }
80
+ }
81
+
82
+ // Simulate RPC
83
+ async function simulateRPC<T>(_endpoint: string, _args?: unknown): Promise<RPCResponse<T>> {
84
+ await new Promise(r => setTimeout(r, 100));
85
+ return { data: [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }] as T, success: true, code: 200 };
86
+ }
87
+
88
+ // --- Basic usage ---
89
+
90
+ const query = new RPCQuery<{ search: string }, { id: string; name: string }[]>('Users.Search');
91
+
92
+ // Subscribe to changes
93
+ query.subscribe(() => {
94
+ console.log(`Query state: loading=${query.loading}, data=${query.data?.length ?? 0} items, error=${query.error}`);
95
+ });
96
+
97
+ console.log('Loading:', query.loading); // false
98
+ console.log('Data:', query.data); // undefined
99
+
100
+ await query.call({ search: 'alice' });
101
+ // Query state: loading=true, data=0 items, error=null
102
+ // Query state: loading=false, data=2 items, error=null
103
+
104
+ console.log('Data after call:', query.data); // [{ id: '1', name: 'Alice' }, ...]
105
+
106
+ // --- Method binding (point-free) ---
107
+
108
+ const { call, clear, dispose } = query;
109
+ // These work without wrapper functions — auto-bound by Trackable
110
+ clear(); // Resets data
111
+
112
+ // --- Dispose lifecycle ---
113
+
114
+ console.log('Disposed:', query.disposed); // false
115
+ console.log('Signal aborted:', query.disposeSignal.aborted); // false
116
+
117
+ dispose();
118
+
119
+ console.log('Disposed:', query.disposed); // true
120
+ console.log('Signal aborted:', query.disposeSignal.aborted); // true
121
+
122
+ // --- Example 2: ViewModel auto-tracking integration ---
123
+
124
+ interface User {
125
+ id: string;
126
+ name: string;
127
+ }
128
+
129
+ class UsersViewModel extends ViewModel {
130
+ // RPCQuery as a property — auto-tracked by ViewModel's dependency system
131
+ readonly users = new RPCQuery<void, User[]>('Users.List');
132
+
133
+ // This getter auto-invalidates when users.notify() fires
134
+ get userCount(): number {
135
+ return this.users.data?.length ?? 0;
136
+ }
137
+
138
+ get isLoading(): boolean {
139
+ return this.users.loading;
140
+ }
141
+
142
+ get userNames(): string[] {
143
+ return (this.users.data ?? []).map(u => u.name);
144
+ }
145
+
146
+ async onInit() {
147
+ await this.users.call();
148
+ }
149
+ }
150
+
151
+ const vm = new UsersViewModel();
152
+ vm.init();
153
+
154
+ // After init, the RPCQuery fires and getters update automatically
155
+ await new Promise(r => setTimeout(r, 200));
156
+
157
+ console.log('User count:', vm.userCount); // 2
158
+ console.log('User names:', vm.userNames); // ['Alice', 'Bob']
159
+
160
+ vm.dispose();
161
+
162
+ // --- Example 3: addCleanup for external subscriptions ---
163
+
164
+ class LivePrice extends Trackable {
165
+ private _price = 0;
166
+
167
+ get price(): number { return this._price; }
168
+
169
+ constructor(symbol: string) {
170
+ super();
171
+
172
+ // Simulate a WebSocket price subscription
173
+ const interval = setInterval(() => {
174
+ this._price = Math.round(Math.random() * 10000) / 100;
175
+ this.notify();
176
+ }, 1000);
177
+
178
+ // addCleanup runs on dispose — auto-cleanup
179
+ this.addCleanup(() => {
180
+ clearInterval(interval);
181
+ console.log(`Unsubscribed from ${symbol} price feed`);
182
+ });
183
+ }
184
+
185
+ protected onDispose() {
186
+ console.log('LivePrice disposed');
187
+ }
188
+ }
189
+
190
+ const btcPrice = new LivePrice('BTC');
191
+
192
+ const unsub = btcPrice.subscribe(() => {
193
+ console.log(`BTC: $${btcPrice.price}`);
194
+ });
195
+
196
+ // Let it tick a couple times
197
+ await new Promise(r => setTimeout(r, 2500));
198
+
199
+ btcPrice.dispose(); // Clears interval, logs "Unsubscribed...", logs "LivePrice disposed"
200
+ unsub(); // Safe to call after dispose — no-op
201
+
202
+ // --- Example 4: bindPublicMethods standalone utility ---
203
+ //
204
+ // For classes that don't extend Trackable but still want point-free methods.
205
+
206
+ class Formatter {
207
+ constructor(private locale: string) {
208
+ bindPublicMethods(this);
209
+ }
210
+
211
+ formatCurrency(amount: number): string {
212
+ return new Intl.NumberFormat(this.locale, { style: 'currency', currency: 'USD' }).format(amount);
213
+ }
214
+
215
+ formatDate(date: Date): string {
216
+ return new Intl.DateTimeFormat(this.locale).format(date);
217
+ }
218
+ }
219
+
220
+ const fmt = new Formatter('en-US');
221
+
222
+ // Destructured methods work — bound by bindPublicMethods
223
+ const { formatCurrency, formatDate } = fmt;
224
+ console.log(formatCurrency(1234.56)); // $1,234.56
225
+ console.log(formatDate(new Date(2025, 0, 1))); // 1/1/2025
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "erasableSyntaxOnly": false,
12
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
13
+ "types": [],
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "mvc-kit": ["../../src/index.ts"]
17
+ }
18
+ },
19
+ "include": ["."]
20
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ ViewModel,
3
+ Service,
4
+ Collection,
5
+ HttpError,
6
+ isAbortError,
7
+ singleton,
8
+ teardownAll,
9
+ } from 'mvc-kit';
10
+
11
+ // ViewModel + Service + Collection: Full data-loading pattern
12
+ //
13
+ // This example shows the typical data-loading architecture:
14
+ // - Service: Stateless HTTP adapter (wraps fetch with HttpError handling)
15
+ // - Collection: Shared reactive data cache (singleton)
16
+ // - ViewModel: Orchestrates loading, holds UI state, provides computed getters
17
+ //
18
+ // The ViewModel's async methods are automatically tracked — access
19
+ // loading/error state via vm.async.methodName without manual flags.
20
+
21
+ // --- Entity type ---
22
+
23
+ interface User {
24
+ id: string;
25
+ name: string;
26
+ role: 'admin' | 'member';
27
+ }
28
+
29
+ // --- Service ---
30
+
31
+ class UserService extends Service {
32
+ async getAll(signal?: AbortSignal): Promise<User[]> {
33
+ // Simulated API call — in production this would be fetch()
34
+ await delay(500, signal);
35
+ return [
36
+ { id: '1', name: 'Alice', role: 'admin' },
37
+ { id: '2', name: 'Bob', role: 'member' },
38
+ { id: '3', name: 'Carol', role: 'member' },
39
+ ];
40
+ }
41
+
42
+ async save(user: User, signal?: AbortSignal): Promise<User> {
43
+ await delay(300, signal);
44
+ if (!user.name.trim()) throw new HttpError(400, 'Name is required');
45
+ return user;
46
+ }
47
+ }
48
+
49
+ // --- Collection ---
50
+
51
+ class UsersCollection extends Collection<User> {}
52
+
53
+ // --- ViewModel ---
54
+
55
+ interface UsersState {
56
+ items: User[];
57
+ search: string;
58
+ }
59
+
60
+ class UsersViewModel extends ViewModel<UsersState> {
61
+ private service = singleton(UserService);
62
+ private collection = singleton(UsersCollection);
63
+
64
+ // --- Computed getters ---
65
+ get filtered(): User[] {
66
+ const { items, search } = this.state;
67
+ if (!search) return items;
68
+ const q = search.toLowerCase();
69
+ return items.filter(u => u.name.toLowerCase().includes(q));
70
+ }
71
+
72
+ get total(): number {
73
+ return this.state.items.length;
74
+ }
75
+
76
+ get adminCount(): number {
77
+ return this.state.items.filter(u => u.role === 'admin').length;
78
+ }
79
+
80
+ // --- Lifecycle ---
81
+ protected onInit() {
82
+ this.subscribeTo(this.collection, () => {
83
+ this.set({ items: this.collection.items as User[] });
84
+ });
85
+
86
+ if (this.collection.length > 0) {
87
+ this.set({ items: this.collection.items as User[] });
88
+ } else {
89
+ this.load();
90
+ }
91
+ }
92
+
93
+ // --- Actions ---
94
+ async load() {
95
+ const data = await this.service.getAll(this.disposeSignal);
96
+ this.collection.reset(data);
97
+ }
98
+
99
+ async save(user: User) {
100
+ try {
101
+ const saved = await this.service.save(user, this.disposeSignal);
102
+ this.collection.update(saved.id, saved);
103
+ } catch (e) {
104
+ if (!isAbortError(e)) {
105
+ console.error('Save failed:', (e as Error).message);
106
+ }
107
+ throw e; // re-throw so async tracking captures it
108
+ }
109
+ }
110
+
111
+ // --- Setters ---
112
+ setSearch(search: string) {
113
+ this.set({ search });
114
+ }
115
+ }
116
+
117
+ // --- Usage ---
118
+
119
+ const vm = new UsersViewModel({ items: [], search: '' });
120
+ vm.init(); // activates getter memoization + async tracking
121
+
122
+ // Async tracking — no manual loading/error state needed
123
+ console.log('Loading:', vm.async.load.loading); // true (load started in onInit)
124
+
125
+ await vm.load();
126
+
127
+ console.log('Loading:', vm.async.load.loading); // false
128
+ console.log('Error:', vm.async.load.error); // null
129
+ console.log('Total users:', vm.total); // 3
130
+ console.log('Admins:', vm.adminCount); // 1
131
+
132
+ // Computed getters recompute when state changes
133
+ vm.setSearch('alice');
134
+ console.log('Filtered:', vm.filtered.length); // 1
135
+
136
+ vm.setSearch('');
137
+ console.log('Filtered:', vm.filtered.length); // 3
138
+
139
+ // Cleanup
140
+ vm.dispose();
141
+ teardownAll();
142
+
143
+ // --- Helper ---
144
+
145
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
146
+ return new Promise((resolve, reject) => {
147
+ if (signal?.aborted) {
148
+ reject(signal.reason);
149
+ return;
150
+ }
151
+ const timer = setTimeout(resolve, ms);
152
+ signal?.addEventListener(
153
+ 'abort',
154
+ () => {
155
+ clearTimeout(timer);
156
+ reject(signal.reason);
157
+ },
158
+ { once: true },
159
+ );
160
+ });
161
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mvc-kit Auth Example</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,29 @@
1
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
2
+ import { AuthGuard } from './components/AuthGuard';
3
+ import { AppHeader } from './components/AppHeader';
4
+ import { DashboardPage } from './components/DashboardPage';
5
+ import { ProfilePage } from './components/ProfilePage';
6
+ import { AdminPage } from './components/AdminPage';
7
+ import { Toast } from './components/Toast';
8
+
9
+ export function App() {
10
+ return (
11
+ <BrowserRouter>
12
+ {/* AuthGuard wraps all authenticated content via composition.
13
+ When not authenticated, it shows the login/register screen
14
+ WITHOUT redirecting — preserving the current URL and any
15
+ query params. After login, children render immediately
16
+ at the original URL. */}
17
+ <AuthGuard>
18
+ <AppHeader />
19
+ <Routes>
20
+ <Route path="/" element={<DashboardPage />} />
21
+ <Route path="/profile" element={<ProfilePage />} />
22
+ <Route path="/admin" element={<AdminPage />} />
23
+ <Route path="*" element={<Navigate to="/" replace />} />
24
+ </Routes>
25
+ </AuthGuard>
26
+ <Toast />
27
+ </BrowserRouter>
28
+ );
29
+ }
@@ -0,0 +1,51 @@
1
+ import { useSingleton } from 'mvc-kit/react';
2
+ import { AuthViewModel } from '../viewmodels/AuthViewModel';
3
+
4
+ export function AdminPage() {
5
+ const [state, vm] = useSingleton(AuthViewModel);
6
+
7
+ // Role check is done inside the page, not via a route wrapper.
8
+ // This keeps routing simple and the access-denied message inline.
9
+ if (!vm.isAdmin) {
10
+ return (
11
+ <div className="page-content">
12
+ <div className="access-denied">
13
+ <h2>Access Denied</h2>
14
+ <p>
15
+ You are signed in as <strong>{vm.displayName}</strong> with
16
+ the <span className={`badge badge-${state.user!.role}`}>{state.user!.role}</span> role.
17
+ </p>
18
+ <p>This page requires the <span className="badge badge-admin">admin</span> role.</p>
19
+ </div>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <div className="page-content">
26
+ <h1 className="page-title">Admin Panel</h1>
27
+
28
+ <div className="card" style={{ marginBottom: '1rem' }}>
29
+ <h3 style={{ marginBottom: '0.5rem' }}>System Status</h3>
30
+ <div className="stats-grid">
31
+ <div className="stat-card">
32
+ <div className="stat-label">Active Users</div>
33
+ <div className="stat-value">3</div>
34
+ </div>
35
+ <div className="stat-card">
36
+ <div className="stat-label">Auth Tokens</div>
37
+ <div className="stat-value">1</div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="card">
43
+ <h3 style={{ marginBottom: '0.5rem' }}>Admin-Only Content</h3>
44
+ <p style={{ color: 'var(--color-text-secondary)' }}>
45
+ This content is only visible to users with the admin role.
46
+ The role check happens inside the page component, not at the route level.
47
+ </p>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,32 @@
1
+ import { NavLink } from 'react-router-dom';
2
+ import { useSingleton } from 'mvc-kit/react';
3
+ import { AuthViewModel } from '../viewmodels/AuthViewModel';
4
+
5
+ export function AppHeader() {
6
+ const [, vm] = useSingleton(AuthViewModel);
7
+
8
+ return (
9
+ <header className="app-header">
10
+ <nav className="header-nav">
11
+ <span className="header-logo">mvc-kit Auth</span>
12
+ <NavLink to="/" end className={({ isActive }) => `header-link ${isActive ? 'active' : ''}`}>
13
+ Dashboard
14
+ </NavLink>
15
+ <NavLink to="/profile" className={({ isActive }) => `header-link ${isActive ? 'active' : ''}`}>
16
+ Profile
17
+ </NavLink>
18
+ <NavLink to="/admin" className={({ isActive }) => `header-link ${isActive ? 'active' : ''}`}>
19
+ Admin
20
+ </NavLink>
21
+ </nav>
22
+
23
+ <div className="header-user">
24
+ <div className="avatar">{vm.initials}</div>
25
+ <span className="header-user-name">{vm.displayName}</span>
26
+ <button type="button" className="btn btn-secondary btn-sm" onClick={vm.logout}>
27
+ Logout
28
+ </button>
29
+ </div>
30
+ </header>
31
+ );
32
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from 'react';
2
+ import { useSingleton } from 'mvc-kit/react';
3
+ import { AuthViewModel } from '../viewmodels/AuthViewModel';
4
+ import { AuthScreen } from './AuthScreen';
5
+
6
+ /**
7
+ * AuthGuard — composition-based authentication wrapper.
8
+ *
9
+ * Instead of using <ProtectedRoute> wrappers or redirect-based guards,
10
+ * this component wraps the entire authenticated app via composition:
11
+ *
12
+ * <AuthGuard>
13
+ * <AppHeader />
14
+ * <Routes>...</Routes>
15
+ * </AuthGuard>
16
+ *
17
+ * How it works:
18
+ * - Reads auth state from the singleton AuthViewModel
19
+ * - While restoring a session (onInit loading), shows a spinner
20
+ * - When not authenticated, renders <AuthScreen /> (login/register UI)
21
+ * WITHOUT navigating away — the current URL and query params are preserved
22
+ * - When authenticated, renders {children} immediately at the current URL
23
+ *
24
+ * Benefits over redirect-based auth:
25
+ * - URL is always preserved — no redirect loops, no lost query params
26
+ * - No coupling between auth state and router configuration
27
+ * - Children mount immediately after login with no extra navigation
28
+ * - Simpler mental model: auth state controls what renders, not where you navigate
29
+ */
30
+ export function AuthGuard({ children }: { children: ReactNode }) {
31
+ const [, vm] = useSingleton(AuthViewModel);
32
+
33
+ // Session restore in progress — show a loading indicator
34
+ if (vm.async.onInit.loading) {
35
+ return (
36
+ <div className="auth-loading">
37
+ <div className="spinner spinner-lg" />
38
+ <p>Restoring session...</p>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ // Not authenticated — show login/register UI in place (no redirect)
44
+ if (!vm.isAuthenticated) {
45
+ return <AuthScreen />;
46
+ }
47
+
48
+ // Authenticated — render the app
49
+ return <>{children}</>;
50
+ }