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,118 @@
1
+ import { Controller, ViewModel, Collection, singleton, teardownAll } from 'mvc-kit';
2
+
3
+ // Controller: Stateless multi-ViewModel orchestrator
4
+ //
5
+ // Controllers coordinate between ViewModels, Models, and Services when
6
+ // a single ViewModel can't own the workflow. This is rare — most
7
+ // orchestration fits in a single ViewModel.
8
+ //
9
+ // Use Controller only for pure cross-cutting coordination with no state
10
+ // of its own: multi-step checkout, drag-and-drop between lists, etc.
11
+ //
12
+ // What Controller provides:
13
+ // - init() / dispose() lifecycle
14
+ // - subscribeTo() / listenTo() with auto-cleanup
15
+ // - disposeSignal for cancelling async operations
16
+ // - addCleanup() for custom teardown
17
+ //
18
+ // What Controller does NOT provide:
19
+ // - State (no set())
20
+ // - Computed getters
21
+ // - Async tracking
22
+ // - Events (no emit())
23
+
24
+ // --- Supporting types ---
25
+
26
+ interface Task {
27
+ id: string;
28
+ title: string;
29
+ status: 'todo' | 'done';
30
+ }
31
+
32
+ interface TaskListState {
33
+ items: Task[];
34
+ }
35
+
36
+ // --- Two ViewModels that the Controller coordinates ---
37
+
38
+ class TodoListViewModel extends ViewModel<TaskListState> {
39
+ removeItem(id: string) {
40
+ this.set({ items: this.state.items.filter(t => t.id !== id) });
41
+ }
42
+
43
+ addItem(task: Task) {
44
+ this.set({ items: [...this.state.items, task] });
45
+ }
46
+ }
47
+
48
+ class DoneListViewModel extends ViewModel<TaskListState> {
49
+ addItem(task: Task) {
50
+ this.set({ items: [...this.state.items, { ...task, status: 'done' as const }] });
51
+ }
52
+ }
53
+
54
+ // --- Controller definition ---
55
+
56
+ class TaskTransferController extends Controller {
57
+ constructor(
58
+ private todoVM: TodoListViewModel,
59
+ private doneVM: DoneListViewModel,
60
+ ) {
61
+ super();
62
+ }
63
+
64
+ protected onInit() {
65
+ // subscribeTo auto-cleans up on dispose
66
+ this.subscribeTo(this.todoVM, (state) => {
67
+ console.log(`Todo list: ${state.items.length} items`);
68
+ });
69
+
70
+ this.subscribeTo(this.doneVM, (state) => {
71
+ console.log(`Done list: ${state.items.length} items`);
72
+ });
73
+ }
74
+
75
+ // Pure coordination — moves an item from todo to done
76
+ completeTask(taskId: string) {
77
+ const task = this.todoVM.state.items.find(t => t.id === taskId);
78
+ if (!task) return;
79
+ this.todoVM.removeItem(taskId);
80
+ this.doneVM.addItem(task);
81
+ }
82
+
83
+ protected onDispose() {
84
+ console.log('TaskTransferController disposed');
85
+ }
86
+ }
87
+
88
+ // --- Usage ---
89
+
90
+ const todoVM = new TodoListViewModel({
91
+ items: [
92
+ { id: '1', title: 'Learn mvc-kit', status: 'todo' },
93
+ { id: '2', title: 'Build an app', status: 'todo' },
94
+ { id: '3', title: 'Write tests', status: 'todo' },
95
+ ],
96
+ });
97
+ todoVM.init();
98
+
99
+ const doneVM = new DoneListViewModel({ items: [] });
100
+ doneVM.init();
101
+
102
+ const controller = new TaskTransferController(todoVM, doneVM);
103
+ controller.init();
104
+
105
+ console.log('Todo:', todoVM.state.items.length); // 3
106
+ console.log('Done:', doneVM.state.items.length); // 0
107
+
108
+ // Controller coordinates the move
109
+ controller.completeTask('1');
110
+
111
+ console.log('Todo:', todoVM.state.items.length); // 2
112
+ console.log('Done:', doneVM.state.items.length); // 1
113
+ console.log('Done item:', doneVM.state.items[0]?.title); // 'Learn mvc-kit'
114
+
115
+ // Cleanup
116
+ controller.dispose();
117
+ todoVM.dispose();
118
+ doneVM.dispose();
@@ -0,0 +1,108 @@
1
+ import { ViewModel, singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
2
+
3
+ // ViewModel: Reactive state + computed getters + async tracking + typed events
4
+ //
5
+ // The core building block. Holds state, derives computed values via getters,
6
+ // provides actions to update state, and tracks async method loading/error
7
+ // automatically. After init(), getters are auto-memoized and only recompute
8
+ // when their dependencies change.
9
+
10
+ // Define state interface
11
+ interface CounterState {
12
+ count: number;
13
+ }
14
+
15
+ // Create a ViewModel by extending the base class
16
+ class CounterViewModel extends ViewModel<CounterState> {
17
+ // --- Computed getters (auto-memoized after init) ---
18
+ get doubled(): number {
19
+ return this.state.count * 2;
20
+ }
21
+
22
+ get isPositive(): boolean {
23
+ return this.state.count > 0;
24
+ }
25
+
26
+ get parity(): 'even' | 'odd' {
27
+ return this.state.count % 2 === 0 ? 'even' : 'odd';
28
+ }
29
+
30
+ // --- Actions ---
31
+ increment() {
32
+ this.set({ count: this.state.count + 1 });
33
+ }
34
+
35
+ decrement() {
36
+ this.set({ count: this.state.count - 1 });
37
+ }
38
+
39
+ reset() {
40
+ this.set({ count: 0 });
41
+ }
42
+
43
+ // Updater function pattern for derived updates
44
+ multiplyBy(factor: number) {
45
+ this.set(prev => ({ count: prev.count * factor }));
46
+ }
47
+
48
+ // Lifecycle hook: called after each state update
49
+ protected onSet(prev: Readonly<CounterState>, next: Readonly<CounterState>) {
50
+ console.log(`Count changed: ${prev.count} → ${next.count}`);
51
+ }
52
+
53
+ // Lifecycle hook: called when disposed
54
+ protected onDispose() {
55
+ console.log('CounterViewModel disposed');
56
+ }
57
+ }
58
+
59
+ // --- Basic usage ---
60
+
61
+ const counter = new CounterViewModel({ count: 0 });
62
+ counter.init(); // activates getter memoization and async tracking
63
+
64
+ // Subscribe to state changes
65
+ const unsubscribe = counter.subscribe((state, prev) => {
66
+ console.log(`Subscriber notified: ${prev.count} → ${state.count}`);
67
+ });
68
+
69
+ counter.increment(); // Count changed: 0 → 1
70
+ counter.increment(); // Count changed: 1 → 2
71
+ counter.multiplyBy(5); // Count changed: 2 → 10
72
+
73
+ // Access current state
74
+ console.log('Current count:', counter.state.count); // 10
75
+
76
+ // --- Computed getters ---
77
+
78
+ console.log('Doubled:', counter.doubled); // 20
79
+ console.log('Is positive:', counter.isPositive); // true
80
+ console.log('Parity:', counter.parity); // even
81
+
82
+ counter.decrement(); // Count changed: 10 → 9
83
+ console.log('Doubled:', counter.doubled); // 18
84
+ console.log('Parity:', counter.parity); // odd
85
+
86
+ // Cleanup
87
+ unsubscribe();
88
+ counter.dispose();
89
+
90
+ // --- Singleton pattern ---
91
+
92
+ // Get or create a singleton instance
93
+ const shared1 = singleton(CounterViewModel, { count: 100 });
94
+ shared1.init(); // activate getter memoization on the singleton too
95
+ const shared2 = singleton(CounterViewModel, { count: 999 }); // args ignored, same instance returned
96
+
97
+ console.log('Same instance:', shared1 === shared2); // true
98
+ console.log('Singleton count:', shared1.state.count); // 100
99
+
100
+ // Check if singleton exists
101
+ console.log('Has singleton:', hasSingleton(CounterViewModel)); // true
102
+
103
+ // Cleanup singleton
104
+ teardown(CounterViewModel);
105
+ console.log('Has singleton after teardown:', hasSingleton(CounterViewModel)); // false
106
+
107
+ // Cleanup all singletons (useful in tests)
108
+ teardownAll();
@@ -0,0 +1 @@
1
+ declare const __MVC_KIT_DEV__: boolean;
@@ -0,0 +1,77 @@
1
+ import { EventBus } from 'mvc-kit';
2
+
3
+ // EventBus: Typed pub/sub for decoupled communication
4
+
5
+ // Define event types
6
+ interface AppEvents {
7
+ 'user:login': { userId: string; timestamp: number };
8
+ 'user:logout': { userId: string };
9
+ 'notification': { message: string; type: 'info' | 'warning' | 'error' };
10
+ 'cart:updated': { itemCount: number };
11
+ }
12
+
13
+ // --- Basic usage ---
14
+
15
+ const bus = new EventBus<AppEvents>();
16
+
17
+ // Subscribe to events
18
+ const unsubLogin = bus.on('user:login', ({ userId, timestamp }) => {
19
+ console.log(`User ${userId} logged in at ${new Date(timestamp).toISOString()}`);
20
+ });
21
+
22
+ const unsubNotification = bus.on('notification', ({ message, type }) => {
23
+ console.log(`[${type.toUpperCase()}] ${message}`);
24
+ });
25
+
26
+ // Emit events
27
+ bus.emit('user:login', { userId: '123', timestamp: Date.now() });
28
+ bus.emit('notification', { message: 'Welcome back!', type: 'info' });
29
+
30
+ // One-time subscription - auto-unsubscribes after first event
31
+ bus.once('user:logout', ({ userId }) => {
32
+ console.log(`User ${userId} logged out (one-time handler)`);
33
+ });
34
+
35
+ bus.emit('user:logout', { userId: '123' }); // Handler called
36
+ bus.emit('user:logout', { userId: '123' }); // Handler NOT called (already unsubscribed)
37
+
38
+ // Unsubscribe manually
39
+ unsubLogin();
40
+ unsubNotification();
41
+
42
+ // After unsubscribe, handlers are not called
43
+ bus.emit('user:login', { userId: '456', timestamp: Date.now() }); // No output
44
+
45
+ // --- Practical pattern: Cross-component communication ---
46
+
47
+ class AuthService {
48
+ constructor(private bus: EventBus<AppEvents>) {}
49
+
50
+ login(userId: string) {
51
+ // ... authentication logic ...
52
+ this.bus.emit('user:login', { userId, timestamp: Date.now() });
53
+ }
54
+
55
+ logout(userId: string) {
56
+ // ... logout logic ...
57
+ this.bus.emit('user:logout', { userId });
58
+ }
59
+ }
60
+
61
+ class NotificationManager {
62
+ constructor(private bus: EventBus<AppEvents>) {
63
+ // React to login events
64
+ this.bus.on('user:login', () => {
65
+ this.bus.emit('notification', { message: 'Welcome!', type: 'info' });
66
+ });
67
+ }
68
+ }
69
+
70
+ // Components can communicate without direct references
71
+ const auth = new AuthService(bus);
72
+ new NotificationManager(bus);
73
+
74
+ auth.login('789'); // Triggers login event, which triggers notification
75
+
76
+ // Cleanup
77
+ bus.dispose();
@@ -0,0 +1,162 @@
1
+ import { Feed } from 'mvc-kit';
2
+
3
+ // Feed: Cursor-based pagination for infinite scroll / load-more
4
+ //
5
+ // A subscribable helper that accumulates items across pages using
6
+ // cursor-based server pagination. Tracks cursor position, hasMore
7
+ // status, and the growing item list.
8
+ //
9
+ // Designed to be a property on a ViewModel — auto-tracked so
10
+ // ViewModel getters that read feed state recompute automatically.
11
+ //
12
+ // Typical flow:
13
+ // 1. ViewModel calls API with feed.cursor
14
+ // 2. API returns { items, hasMore, cursor }
15
+ // 3. ViewModel calls feed.appendPage(result)
16
+ // 4. Repeat until hasMore is false
17
+
18
+ // --- Entity type ---
19
+
20
+ interface Post {
21
+ id: string;
22
+ title: string;
23
+ createdAt: string;
24
+ }
25
+
26
+ // --- Simulated API ---
27
+
28
+ function fakeFetchPosts(cursor: string | null): {
29
+ items: Post[];
30
+ hasMore: boolean;
31
+ cursor: string;
32
+ } {
33
+ const pages: Record<string, { items: Post[]; hasMore: boolean; cursor: string }> = {
34
+ initial: {
35
+ items: [
36
+ { id: '1', title: 'First post', createdAt: '2024-01-01' },
37
+ { id: '2', title: 'Second post', createdAt: '2024-01-02' },
38
+ { id: '3', title: 'Third post', createdAt: '2024-01-03' },
39
+ ],
40
+ hasMore: true,
41
+ cursor: 'page2',
42
+ },
43
+ page2: {
44
+ items: [
45
+ { id: '4', title: 'Fourth post', createdAt: '2024-01-04' },
46
+ { id: '5', title: 'Fifth post', createdAt: '2024-01-05' },
47
+ ],
48
+ hasMore: true,
49
+ cursor: 'page3',
50
+ },
51
+ page3: {
52
+ items: [
53
+ { id: '6', title: 'Sixth post', createdAt: '2024-01-06' },
54
+ ],
55
+ hasMore: false,
56
+ cursor: 'end',
57
+ },
58
+ };
59
+
60
+ const key = cursor ?? 'initial';
61
+ return pages[key]!;
62
+ }
63
+
64
+ // --- Basic usage ---
65
+
66
+ const feed = new Feed<Post>();
67
+
68
+ // Subscribe to feed state changes
69
+ feed.subscribe(() => {
70
+ console.log(`Feed: ${feed.count} items, hasMore=${feed.hasMore}, cursor=${feed.cursor}`);
71
+ });
72
+
73
+ // Initial state
74
+ console.log('Cursor:', feed.cursor); // null
75
+ console.log('Has more:', feed.hasMore); // true
76
+ console.log('Items:', feed.count); // 0
77
+
78
+ // --- Load first page ---
79
+
80
+ const page1 = fakeFetchPosts(feed.cursor);
81
+ feed.appendPage(page1);
82
+
83
+ console.log('After page 1:', feed.count); // 3
84
+ console.log('Cursor:', feed.cursor); // 'page2'
85
+ console.log('Has more:', feed.hasMore); // true
86
+
87
+ // --- Load next page (items accumulate) ---
88
+
89
+ const page2 = fakeFetchPosts(feed.cursor);
90
+ feed.appendPage(page2);
91
+
92
+ console.log('After page 2:', feed.count); // 5
93
+ console.log('Cursor:', feed.cursor); // 'page3'
94
+
95
+ // --- Load final page ---
96
+
97
+ const page3 = fakeFetchPosts(feed.cursor);
98
+ feed.appendPage(page3);
99
+
100
+ console.log('After page 3:', feed.count); // 6
101
+ console.log('Has more:', feed.hasMore); // false (no more pages)
102
+
103
+ // Access all accumulated items
104
+ console.log('All items:', feed.items.map(p => p.title));
105
+
106
+ // --- Prepend (for chat UIs / newest-first feeds) ---
107
+
108
+ const chatFeed = new Feed<Post>();
109
+
110
+ chatFeed.appendPage({
111
+ items: [
112
+ { id: 'c1', title: 'Latest message', createdAt: '2024-01-10' },
113
+ { id: 'c2', title: 'Previous message', createdAt: '2024-01-09' },
114
+ ],
115
+ hasMore: true,
116
+ cursor: 'older',
117
+ });
118
+
119
+ // Prepend older messages at the top (e.g., scrolling up in a chat)
120
+ chatFeed.prependPage({
121
+ items: [
122
+ { id: 'c3', title: 'Oldest message', createdAt: '2024-01-08' },
123
+ ],
124
+ hasMore: false,
125
+ cursor: 'start',
126
+ });
127
+
128
+ console.log('Chat order:', chatFeed.items.map(p => p.title));
129
+ // ['Oldest message', 'Latest message', 'Previous message']
130
+
131
+ // --- Push items without affecting cursor ---
132
+
133
+ feed.push({ id: '7', title: 'Optimistic post', createdAt: '2024-01-07' });
134
+ console.log('After push:', feed.count); // 7
135
+
136
+ // --- Filter items ---
137
+
138
+ feed.filter(p => p.id !== '7'); // remove the optimistic post
139
+ console.log('After filter:', feed.count); // 6
140
+
141
+ // --- Pull-to-refresh (replacePage) ---
142
+
143
+ feed.replacePage({
144
+ items: [
145
+ { id: 'r1', title: 'Refreshed post 1', createdAt: '2024-02-01' },
146
+ { id: 'r2', title: 'Refreshed post 2', createdAt: '2024-02-02' },
147
+ ],
148
+ hasMore: true,
149
+ cursor: 'refreshed-page2',
150
+ });
151
+ console.log('After replacePage:', feed.count); // 2 (replaced all)
152
+
153
+ // --- setResult (update cursor/hasMore only) ---
154
+
155
+ feed.setResult({ hasMore: false, cursor: 'final' });
156
+ console.log('After setResult hasMore:', feed.hasMore); // false
157
+ console.log('Items unchanged:', feed.count); // 2
158
+
159
+ // --- Reset ---
160
+
161
+ feed.reset();
162
+ console.log('After reset:', feed.count, feed.cursor, feed.hasMore); // 0, null, true
@@ -0,0 +1,82 @@
1
+ import { Model } from 'mvc-kit';
2
+
3
+ // Model: Reactive entity with validation and dirty tracking
4
+
5
+ interface UserFormState {
6
+ name: string;
7
+ email: string;
8
+ age: number;
9
+ }
10
+
11
+ class UserFormModel extends Model<UserFormState> {
12
+ setName(name: string) {
13
+ this.set({ name });
14
+ }
15
+
16
+ setEmail(email: string) {
17
+ this.set({ email });
18
+ }
19
+
20
+ setAge(age: number) {
21
+ this.set({ age });
22
+ }
23
+
24
+ // Override to provide validation logic
25
+ protected validate(state: UserFormState) {
26
+ const errors: Partial<Record<keyof UserFormState, string>> = {};
27
+
28
+ if (!state.name.trim()) {
29
+ errors.name = 'Name is required';
30
+ }
31
+
32
+ if (!state.email.includes('@')) {
33
+ errors.email = 'Invalid email address';
34
+ }
35
+
36
+ if (state.age < 0 || state.age > 150) {
37
+ errors.age = 'Age must be between 0 and 150';
38
+ }
39
+
40
+ return errors;
41
+ }
42
+ }
43
+
44
+ // --- Usage ---
45
+
46
+ const form = new UserFormModel({ name: '', email: '', age: 0 });
47
+
48
+ // Subscribe to state changes
49
+ form.subscribe((state) => {
50
+ console.log('Form state:', state);
51
+ });
52
+
53
+ // Check validation
54
+ console.log('Initial valid:', form.valid); // false
55
+ console.log('Initial errors:', form.errors); // { name: 'Name is required', email: 'Invalid email...' }
56
+
57
+ // Update fields
58
+ form.setName('John Doe');
59
+ form.setEmail('john@example.com');
60
+ form.setAge(30);
61
+
62
+ console.log('After updates valid:', form.valid); // true
63
+ console.log('After updates errors:', form.errors); // {}
64
+
65
+ // Dirty tracking - check if state differs from committed baseline
66
+ console.log('Is dirty:', form.dirty); // true (differs from initial state)
67
+
68
+ // Commit - mark current state as the new baseline
69
+ form.commit();
70
+ console.log('After commit dirty:', form.dirty); // false
71
+
72
+ // Make more changes
73
+ form.setName('Jane Doe');
74
+ console.log('After change dirty:', form.dirty); // true
75
+
76
+ // Rollback - revert to committed state
77
+ form.rollback();
78
+ console.log('After rollback name:', form.state.name); // 'John Doe'
79
+ console.log('After rollback dirty:', form.dirty); // false
80
+
81
+ // Cleanup
82
+ form.dispose();
@@ -0,0 +1,91 @@
1
+ import { Pagination } from 'mvc-kit';
2
+
3
+ // Pagination: Page-based state with array slicing pipeline
4
+ //
5
+ // A subscribable helper that manages page/pageSize state and slices
6
+ // arrays accordingly. Designed to be a property on a ViewModel —
7
+ // auto-tracked so ViewModel getters that read pagination state
8
+ // recompute automatically.
9
+
10
+ // --- Sample data ---
11
+
12
+ const allItems = Array.from({ length: 47 }, (_, i) => ({
13
+ id: String(i + 1),
14
+ title: `Item ${i + 1}`,
15
+ }));
16
+
17
+ // --- Basic usage ---
18
+
19
+ const pagination = new Pagination(); // default pageSize: 10
20
+
21
+ // Subscribe to page changes
22
+ pagination.subscribe(() => {
23
+ console.log(`Page ${pagination.page} of ${pagination.pageCount(allItems.length)}`);
24
+ });
25
+
26
+ // Read current state
27
+ console.log('Page:', pagination.page); // 1
28
+ console.log('Page size:', pagination.pageSize); // 10
29
+
30
+ // --- Apply slice pipeline ---
31
+
32
+ const page1 = pagination.apply(allItems);
33
+ console.log('Page 1 items:', page1.length); // 10
34
+ console.log('First:', page1[0].title); // 'Item 1'
35
+ console.log('Last:', page1[9].title); // 'Item 10'
36
+
37
+ // --- Navigation ---
38
+
39
+ pagination.nextPage();
40
+ const page2 = pagination.apply(allItems);
41
+ console.log('Page 2 first:', page2[0].title); // 'Item 11'
42
+
43
+ pagination.nextPage();
44
+ pagination.nextPage();
45
+ pagination.nextPage(); // page 5 (last page)
46
+
47
+ const lastPage = pagination.apply(allItems);
48
+ console.log('Last page items:', lastPage.length); // 7 (47 - 40)
49
+ console.log('Last page first:', lastPage[0].title); // 'Item 41'
50
+
51
+ // --- Boundary checks ---
52
+
53
+ console.log('Page count:', pagination.pageCount(allItems.length)); // 5
54
+ console.log('Has next:', pagination.hasNext(allItems.length)); // false (on last page)
55
+ console.log('Has prev:', pagination.hasPrev()); // true
56
+
57
+ pagination.prevPage();
58
+ console.log('After prevPage:', pagination.page); // 4
59
+
60
+ // prevPage on page 1 is a no-op
61
+ pagination.setPage(1);
62
+ pagination.prevPage();
63
+ console.log('After prevPage on page 1:', pagination.page); // 1
64
+
65
+ // --- Change page size ---
66
+
67
+ pagination.setPageSize(20); // resets to page 1 automatically
68
+ console.log('New page size:', pagination.pageSize); // 20
69
+ console.log('Page after resize:', pagination.page); // 1
70
+ console.log('New page count:', pagination.pageCount(allItems.length)); // 3
71
+
72
+ const bigPage = pagination.apply(allItems);
73
+ console.log('Items on resized page:', bigPage.length); // 20
74
+
75
+ // --- Navigate to specific page ---
76
+
77
+ pagination.setPage(3);
78
+ const thirdPage = pagination.apply(allItems);
79
+ console.log('Page 3 items:', thirdPage.length); // 7 (47 - 40)
80
+
81
+ // --- Custom initial page size ---
82
+
83
+ const smallPages = new Pagination({ pageSize: 5 });
84
+ console.log('Custom page size:', smallPages.pageSize); // 5
85
+ console.log('Custom page count:', smallPages.pageCount(allItems.length)); // 10
86
+
87
+ // --- Reset ---
88
+
89
+ pagination.setPage(3);
90
+ pagination.reset(); // resets to page 1
91
+ console.log('After reset page:', pagination.page); // 1