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,7 @@
1
+ export interface ConversationState {
2
+ id: string;
3
+ participantIds: string[];
4
+ lastMessage: string;
5
+ unreadCount: number;
6
+ updatedAt: number;
7
+ }
@@ -0,0 +1,7 @@
1
+ export interface MessageState {
2
+ id: string;
3
+ conversationId: string;
4
+ senderId: string;
5
+ text: string;
6
+ sentAt: number;
7
+ }
@@ -0,0 +1,13 @@
1
+ export interface BreakEntry {
2
+ start: number;
3
+ end: number | null;
4
+ }
5
+
6
+ export interface ShiftState {
7
+ id: string;
8
+ workerId: string;
9
+ siteId: string;
10
+ clockIn: number;
11
+ clockOut: number | null;
12
+ breaks: BreakEntry[];
13
+ }
@@ -0,0 +1,8 @@
1
+ export interface SiteState {
2
+ id: string;
3
+ name: string;
4
+ type: 'residential' | 'commercial' | 'industrial' | 'infrastructure';
5
+ status: 'active' | 'paused' | 'completed';
6
+ address: string;
7
+ workerCount: number;
8
+ }
@@ -0,0 +1,8 @@
1
+ export interface WorkerState {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ role: 'foreman' | 'electrician' | 'plumber' | 'carpenter' | 'laborer';
6
+ status: 'available' | 'on-shift' | 'on-break' | 'offline';
7
+ avatar: string;
8
+ }
@@ -0,0 +1,41 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import type { WorkerState } from '../types/worker';
3
+ import { MOCK_WORKERS } from '../mock/workers';
4
+ import { mockFetch } from '../mock/delay';
5
+ import { AppEventBus } from '../events/AppEventBus';
6
+
7
+ interface AuthState {
8
+ worker: WorkerState | null;
9
+ isAuthenticated: boolean;
10
+ }
11
+
12
+ export class AuthViewModel extends ViewModel<AuthState> {
13
+ static DEFAULT_STATE: AuthState = { worker: null, isAuthenticated: false };
14
+
15
+ // --- Private fields ---
16
+ private bus = singleton(AppEventBus);
17
+
18
+ // --- Computed getters ---
19
+ get displayName(): string {
20
+ const { worker } = this.state;
21
+ return worker ? worker.name : '';
22
+ }
23
+
24
+ get initials(): string {
25
+ const { worker } = this.state;
26
+ return worker ? worker.avatar : '';
27
+ }
28
+
29
+ // --- Actions ---
30
+ async login(email: string) {
31
+ const worker = MOCK_WORKERS.find(w => w.email === email) ?? MOCK_WORKERS[0]!;
32
+ await mockFetch(null, 300);
33
+ this.set({ worker, isAuthenticated: true });
34
+ this.bus.emit('toast:show', { message: `Welcome, ${worker.name}!`, severity: 'success' });
35
+ }
36
+
37
+ logout() {
38
+ this.set({ worker: null, isAuthenticated: false });
39
+ this.bus.emit('toast:show', { message: 'Logged out', severity: 'info' });
40
+ }
41
+ }
@@ -0,0 +1,83 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import type { ConversationState } from '../types/conversation';
3
+ import { ConversationsResource } from '../resources/ConversationsResource';
4
+ import { WorkersResource } from '../resources/WorkersResource';
5
+ import { AuthViewModel } from './AuthViewModel';
6
+
7
+ export interface ConversationDisplay extends ConversationState {
8
+ displayName: string;
9
+ }
10
+
11
+ interface ConversationsViewState {
12
+ search: string;
13
+ selectedId: string | null;
14
+ currentWorkerId: string;
15
+ }
16
+
17
+ export class ConversationsViewModel extends ViewModel<ConversationsViewState> {
18
+ // --- Private fields ---
19
+ private resource = singleton(ConversationsResource);
20
+ private workers = singleton(WorkersResource);
21
+
22
+ // --- Computed getters ---
23
+ get items(): ConversationState[] {
24
+ return this.resource.items as ConversationState[];
25
+ }
26
+
27
+ get filtered(): ConversationDisplay[] {
28
+ const { search, currentWorkerId } = this.state;
29
+ let result = this.items;
30
+
31
+ if (search) {
32
+ const q = search.toLowerCase();
33
+ result = result.filter(conv =>
34
+ conv.participantIds.some(id => {
35
+ const worker = this.workers.get(id);
36
+ return worker ? worker.name.toLowerCase().includes(q) : false;
37
+ }),
38
+ );
39
+ }
40
+
41
+ return result.map(conv => ({
42
+ ...conv,
43
+ displayName: conv.participantIds
44
+ .filter(id => id !== currentWorkerId)
45
+ .map(id => {
46
+ const worker = this.workers.get(id);
47
+ return worker ? worker.name : 'Unknown';
48
+ })
49
+ .join(', '),
50
+ }));
51
+ }
52
+
53
+ get selected(): ConversationDisplay | undefined {
54
+ return this.filtered.find(c => c.id === this.state.selectedId);
55
+ }
56
+
57
+ get totalUnread(): number {
58
+ return this.items.reduce((sum, c) => sum + c.unreadCount, 0);
59
+ }
60
+
61
+ // --- Lifecycle ---
62
+ protected onInit() {
63
+ const auth = singleton(AuthViewModel);
64
+ const currentWorkerId = auth.state.worker?.id ?? '';
65
+ this.set({ currentWorkerId });
66
+
67
+ if (this.workers.length === 0) this.workers.loadAll();
68
+ if (this.resource.length === 0) this.load();
69
+ }
70
+
71
+ // --- Actions ---
72
+ async load() {
73
+ const { currentWorkerId } = this.state;
74
+ await this.resource.loadAll(currentWorkerId);
75
+ }
76
+
77
+ selectConversation(id: string) {
78
+ this.set({ selectedId: id });
79
+ }
80
+
81
+ // --- Setters ---
82
+ setSearch(search: string) { this.set({ search }); }
83
+ }
@@ -0,0 +1,113 @@
1
+ import { ViewModel, singleton, Feed } from 'mvc-kit';
2
+ import type { MessageState } from '../types/message';
3
+ import { MessagesResource } from '../resources/MessagesResource';
4
+ import { MessagingChannel } from '../channels/MessagingChannel';
5
+ import { WorkersResource } from '../resources/WorkersResource';
6
+ import { AuthViewModel } from './AuthViewModel';
7
+
8
+ interface ThreadState {}
9
+
10
+ interface ThreadEvents {
11
+ messageSent: { conversationId: string };
12
+ }
13
+
14
+ export class MessageThreadViewModel extends ViewModel<ThreadState, ThreadEvents> {
15
+ // --- Private fields ---
16
+ private resource = singleton(MessagesResource);
17
+ private channel = singleton(MessagingChannel);
18
+ private workers = singleton(WorkersResource);
19
+ private _loadController: AbortController | null = null;
20
+ private _conversationId: string = '';
21
+
22
+ readonly feed = new Feed<MessageState>();
23
+
24
+ // --- Computed getters ---
25
+ get sortedMessages(): MessageState[] {
26
+ return [...this.feed.items].sort((a, b) => a.sentAt - b.sentAt);
27
+ }
28
+
29
+ get pending() {
30
+ return this.resource.pending;
31
+ }
32
+
33
+ get hasPendingMessages(): boolean {
34
+ return this.resource.pending.hasPending;
35
+ }
36
+
37
+ get hasFailedMessages(): boolean {
38
+ return this.resource.pending.hasFailed;
39
+ }
40
+
41
+ // --- Lifecycle ---
42
+ protected onInit() {
43
+ this.listenTo(this.channel, 'newMessage', (msg) => {
44
+ if (msg.conversationId === this._conversationId) {
45
+ this.feed.push(msg);
46
+ }
47
+ });
48
+ this.channel.connect();
49
+ }
50
+
51
+ // --- Actions ---
52
+ async loadConversation(conversationId: string) {
53
+ this._loadController?.abort();
54
+ this._loadController = new AbortController();
55
+ this._conversationId = conversationId;
56
+
57
+ this.feed.reset();
58
+
59
+ const page = await this.resource.loadMessages(
60
+ conversationId,
61
+ AbortSignal.any([this.disposeSignal, this._loadController.signal]),
62
+ );
63
+ this.feed.appendPage(page);
64
+ }
65
+
66
+ async loadOlderMessages() {
67
+ if (!this.feed.hasMore || !this._conversationId) return;
68
+
69
+ const page = await this.resource.loadMessages(
70
+ this._conversationId,
71
+ this.disposeSignal,
72
+ { cursor: this.feed.cursor },
73
+ );
74
+ this.feed.appendPage(page);
75
+ }
76
+
77
+ sendMessage(conversationId: string, text: string, recipientName: string) {
78
+ const trimmed = text.trim();
79
+ if (!trimmed) return;
80
+
81
+ const auth = singleton(AuthViewModel);
82
+ const senderId = auth.state.worker?.id ?? '';
83
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
84
+
85
+ // Optimistic: add to feed immediately
86
+ const optimisticMsg: MessageState = {
87
+ id: tempId,
88
+ conversationId,
89
+ senderId,
90
+ text: trimmed,
91
+ sentAt: Date.now(),
92
+ };
93
+ this.feed.push(optimisticMsg);
94
+
95
+ // Enqueue via resource Pending for retry — meta provides UI context
96
+ this.resource.enqueueSend(tempId, conversationId, senderId, trimmed, recipientName);
97
+
98
+ this.emit('messageSent', { conversationId });
99
+ }
100
+
101
+ retryMessage(id: string) {
102
+ this.resource.pending.retry(id);
103
+ }
104
+
105
+ dismissMessage(id: string) {
106
+ this.resource.pending.dismiss(id);
107
+ }
108
+
109
+ getSenderName(senderId: string): string {
110
+ const worker = this.workers.get(senderId);
111
+ return worker ? worker.name : 'Unknown';
112
+ }
113
+ }
@@ -0,0 +1,147 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import type { ShiftState } from '../types/shift';
3
+ import type { SiteState } from '../types/site';
4
+ import { ShiftResource } from '../resources/ShiftResource';
5
+ import { SitesResource } from '../resources/SitesResource';
6
+ import { AuthViewModel } from './AuthViewModel';
7
+
8
+ interface ShiftViewState {
9
+ selectedSiteId: string | null;
10
+ now: number;
11
+ }
12
+
13
+ export class ShiftViewModel extends ViewModel<ShiftViewState> {
14
+ // --- Private fields ---
15
+ private shiftResource = singleton(ShiftResource);
16
+ private sites = singleton(SitesResource);
17
+ private auth = singleton(AuthViewModel);
18
+ private _timer: ReturnType<typeof setInterval> | null = null;
19
+
20
+ // --- Computed getters ---
21
+ get currentShift(): ShiftState | null {
22
+ const items = this.shiftResource.items as ShiftState[];
23
+ return items.length > 0 ? items[0]! : null;
24
+ }
25
+
26
+ get isOnShift(): boolean {
27
+ return this.currentShift !== null && this.currentShift.clockOut === null;
28
+ }
29
+
30
+ get isOnBreak(): boolean {
31
+ const shift = this.currentShift;
32
+ if (!shift) return false;
33
+ return shift.breaks.some(b => b.end === null);
34
+ }
35
+
36
+ get shiftDuration(): number {
37
+ const shift = this.currentShift;
38
+ if (!shift) return 0;
39
+ const end = shift.clockOut ?? this.state.now;
40
+ return end - shift.clockIn;
41
+ }
42
+
43
+ get breakDuration(): number {
44
+ const shift = this.currentShift;
45
+ if (!shift) return 0;
46
+ return shift.breaks.reduce((sum, b) => {
47
+ const end = b.end ?? this.state.now;
48
+ return sum + (end - b.start);
49
+ }, 0);
50
+ }
51
+
52
+ get formattedShiftTime(): string {
53
+ return formatDuration(this.shiftDuration);
54
+ }
55
+
56
+ get formattedBreakTime(): string {
57
+ return formatDuration(this.breakDuration);
58
+ }
59
+
60
+ get currentSiteName(): string {
61
+ const shift = this.currentShift;
62
+ if (!shift) return '';
63
+ const site = this.sites.get(shift.siteId) as SiteState | undefined;
64
+ return site?.name ?? '';
65
+ }
66
+
67
+ get availableSites(): SiteState[] {
68
+ return (this.sites.items as SiteState[]).filter(s => s.status === 'active');
69
+ }
70
+
71
+ get pending() {
72
+ return this.shiftResource.pending;
73
+ }
74
+
75
+ get hasPendingOps(): boolean {
76
+ return this.shiftResource.pending.hasPending;
77
+ }
78
+
79
+ get hasFailedOps(): boolean {
80
+ return this.shiftResource.pending.hasFailed;
81
+ }
82
+
83
+ // --- Lifecycle ---
84
+ protected onInit() {
85
+ const workerId = this.auth.state.worker?.id;
86
+ if (workerId && this.shiftResource.length === 0) this.load();
87
+ if (this.sites.length === 0) this.sites.loadAll();
88
+
89
+ this._timer = setInterval(() => {
90
+ this.set({ now: Date.now() });
91
+ }, 1000);
92
+ this.addCleanup(() => {
93
+ if (this._timer) clearInterval(this._timer);
94
+ });
95
+ }
96
+
97
+ // --- Actions ---
98
+ async load() {
99
+ const workerId = this.auth.state.worker?.id;
100
+ if (!workerId) return;
101
+ await this.shiftResource.loadCurrent(workerId);
102
+ }
103
+
104
+ clockIn() {
105
+ const workerId = this.auth.state.worker?.id;
106
+ const siteId = this.state.selectedSiteId;
107
+ if (!workerId || !siteId) return;
108
+ this.shiftResource.clockIn(workerId, siteId, this.currentSiteName || siteId);
109
+ }
110
+
111
+ clockOut() {
112
+ const shift = this.currentShift;
113
+ if (!shift) return;
114
+ this.shiftResource.clockOut(shift.id, this.currentSiteName);
115
+ }
116
+
117
+ startBreak() {
118
+ const shift = this.currentShift;
119
+ if (!shift) return;
120
+ this.shiftResource.startBreak(shift.id, this.currentSiteName);
121
+ }
122
+
123
+ endBreak() {
124
+ const shift = this.currentShift;
125
+ if (!shift) return;
126
+ this.shiftResource.endBreak(shift.id, this.currentSiteName);
127
+ }
128
+
129
+ retryAll() {
130
+ this.shiftResource.pending.retryAll();
131
+ }
132
+
133
+ dismissAll() {
134
+ this.shiftResource.pending.dismissAll();
135
+ }
136
+
137
+ // --- Setters ---
138
+ selectSite(siteId: string) { this.set({ selectedSiteId: siteId }); }
139
+ }
140
+
141
+ function formatDuration(ms: number): string {
142
+ const totalSeconds = Math.floor(ms / 1000);
143
+ const hours = Math.floor(totalSeconds / 3600);
144
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
145
+ const seconds = totalSeconds % 60;
146
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
147
+ }
@@ -0,0 +1,82 @@
1
+ import { ViewModel, singleton, Sorting, Pagination, Selection } from 'mvc-kit';
2
+ import type { SiteState } from '../types/site';
3
+ import { SitesResource } from '../resources/SitesResource';
4
+
5
+ interface SitesViewState {
6
+ search: string;
7
+ typeFilter: 'all' | SiteState['type'];
8
+ statusFilter: 'all' | SiteState['status'];
9
+ }
10
+
11
+ export class SitesViewModel extends ViewModel<SitesViewState> {
12
+ // --- Private fields ---
13
+ private resource = singleton(SitesResource);
14
+
15
+ readonly sorting = new Sorting<SiteState>({ sorts: [{ key: 'name', direction: 'asc' }] });
16
+ readonly pagination = new Pagination({ pageSize: 10 });
17
+ readonly selection = new Selection<string>();
18
+
19
+ // --- Computed getters ---
20
+ get items(): SiteState[] {
21
+ return this.resource.items as SiteState[];
22
+ }
23
+
24
+ get filtered(): SiteState[] {
25
+ const { search, typeFilter, statusFilter } = this.state;
26
+ let result = this.items;
27
+
28
+ if (search) {
29
+ const q = search.toLowerCase();
30
+ result = result.filter(site =>
31
+ site.name.toLowerCase().includes(q) ||
32
+ site.address.toLowerCase().includes(q),
33
+ );
34
+ }
35
+ if (typeFilter !== 'all') {
36
+ result = result.filter(site => site.type === typeFilter);
37
+ }
38
+ if (statusFilter !== 'all') {
39
+ result = result.filter(site => site.status === statusFilter);
40
+ }
41
+ return result;
42
+ }
43
+
44
+ get sorted(): SiteState[] {
45
+ return this.sorting.apply(this.filtered);
46
+ }
47
+
48
+ get paged(): SiteState[] {
49
+ return this.pagination.apply(this.sorted);
50
+ }
51
+
52
+ get total(): number {
53
+ return this.items.length;
54
+ }
55
+
56
+ get filteredCount(): number {
57
+ return this.filtered.length;
58
+ }
59
+
60
+ get hasResults(): boolean {
61
+ return this.filtered.length > 0;
62
+ }
63
+
64
+ get selectedItems(): SiteState[] {
65
+ return this.selection.selectedFrom(this.filtered, site => site.id);
66
+ }
67
+
68
+ // --- Lifecycle ---
69
+ protected onInit() {
70
+ if (this.resource.length === 0) this.load();
71
+ }
72
+
73
+ // --- Actions ---
74
+ async load() {
75
+ await this.resource.loadAll();
76
+ }
77
+
78
+ // --- Setters ---
79
+ setSearch(search: string) { this.set({ search }); this.pagination.reset(); }
80
+ setTypeFilter(typeFilter: SitesViewState['typeFilter']) { this.set({ typeFilter }); this.pagination.reset(); }
81
+ setStatusFilter(statusFilter: SitesViewState['statusFilter']) { this.set({ statusFilter }); this.pagination.reset(); }
82
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": true,
12
+ "erasableSyntaxOnly": false,
13
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
14
+ "types": [],
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "mvc-kit": ["../../../src/index.ts"],
18
+ "mvc-kit/react": ["../../../src/react/index.ts"]
19
+ }
20
+ },
21
+ "include": ["src"]
22
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'node:path';
3
+
4
+ export default defineConfig({
5
+ root: import.meta.dirname,
6
+ define: {
7
+ __MVC_KIT_DEV__: true,
8
+ },
9
+ resolve: {
10
+ alias: {
11
+ 'mvc-kit/react': resolve(import.meta.dirname, '../../../src/react/index.ts'),
12
+ 'mvc-kit': resolve(import.meta.dirname, '../../../src/index.ts'),
13
+ },
14
+ },
15
+ server: {
16
+ port: 3002,
17
+ },
18
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mvc-kit",
3
- "version": "2.12.5",
3
+ "version": "2.13.1",
4
4
  "description": "Zero-magic, class-based reactive ViewModel library",
5
5
  "type": "module",
6
6
  "main": "./dist/mvc-kit.cjs",
@@ -64,9 +64,11 @@
64
64
  "files": [
65
65
  "./mvc-kit-logo.jpg",
66
66
  "dist",
67
+ "examples",
67
68
  "src",
68
69
  "agent-config",
69
- "README.md"
70
+ "README.md",
71
+ "BEST_PRACTICES.md"
70
72
  ],
71
73
  "bin": {
72
74
  "mvc-kit-setup": "./agent-config/bin/setup.mjs"
@@ -81,16 +83,16 @@
81
83
  },
82
84
  "devDependencies": {
83
85
  "@testing-library/react": "^16.0.0",
84
- "@types/react": "^18.3.0",
85
- "esbuild": "^0.25.12",
86
+ "@types/react": "^19.2.14",
87
+ "esbuild": "^0.28.0",
86
88
  "fake-indexeddb": "^6.2.5",
87
- "jsdom": "^26.0.0",
89
+ "jsdom": "^29.0.2",
88
90
  "msw": "^2.12.10",
89
- "react": "^18.3.0",
90
- "react-dom": "^18.3.0",
91
+ "react": "^19.2.5",
92
+ "react-dom": "^19.2.5",
91
93
  "react-router-dom": "^7.0.0",
92
- "typescript": "~5.9.3",
93
- "vite": "^7.3.1",
94
+ "typescript": "~6.0.2",
95
+ "vite": "^8.0.8",
94
96
  "vitest": "^4.0.18"
95
97
  }
96
98
  }
@@ -1510,8 +1510,7 @@ describe('Pending', () => {
1510
1510
 
1511
1511
  it('supersede replaces meta', async () => {
1512
1512
  const p = new Pending<string, TestMeta>();
1513
- let resolve1!: () => void;
1514
- const promise1 = new Promise<void>(r => { resolve1 = r; });
1513
+ const promise1 = new Promise<void>(() => {});
1515
1514
 
1516
1515
  p.enqueue('a', 'send', () => promise1, { label: 'first', priority: 1 });
1517
1516
  await vi.advanceTimersByTimeAsync(0);
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi } from 'vitest';
2
2
  import { Sorting } from './Sorting';
3
3
  import type { SortDescriptor } from './Sorting';
4
4
  import { ViewModel } from './ViewModel';
@@ -143,7 +143,7 @@ describe('produceDraft', () => {
143
143
  // ── Original immutability ───────────────────────────────────────
144
144
 
145
145
  it('never mutates original state', () => {
146
- const state = Object.freeze({ count: 0, name: 'test' });
146
+ const state = Object.freeze<{ count: number; name: string }>({ count: 0, name: 'test' });
147
147
  const result = produceDraft(state, (d) => {
148
148
  d.count = 5;
149
149
  d.name = 'updated';
@@ -154,8 +154,8 @@ describe('produceDraft', () => {
154
154
  });
155
155
 
156
156
  it('never mutates original nested objects', () => {
157
- const config = Object.freeze({ theme: 'dark', size: 14 });
158
- const state = Object.freeze({ config });
157
+ const config = { theme: 'dark', size: 14 };
158
+ const state = Object.freeze<{ config: { theme: string; size: number } }>({ config });
159
159
  const result = produceDraft(state, (d) => {
160
160
  d.config.theme = 'light';
161
161
  });
@@ -59,7 +59,7 @@ describe('CardList', () => {
59
59
  });
60
60
 
61
61
  it('uses custom keyOf', () => {
62
- const { container } = render(
62
+ render(
63
63
  <CardList
64
64
  items={[{ uid: 'x', label: 'X' }]}
65
65
  keyOf={(item: any) => item.uid}
@@ -411,7 +411,7 @@ describe('DataTable', () => {
411
411
  <DataTable
412
412
  items={users.slice(0, 1)}
413
413
  columns={columns}
414
- renderRow={(item, index, cells) => (
414
+ renderRow={(_item, _index, cells) => (
415
415
  <>{cells}<td data-testid="extra">Extra</td></>
416
416
  )}
417
417
  />,