mvc-kit 2.13.0 → 2.13.2

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 (218) hide show
  1. package/BEST_PRACTICES.md +1390 -0
  2. package/agent-config/claude-code/agents/mvc-kit-architect.md +8 -3
  3. package/agent-config/claude-code/skills/{guide → mvc-kit}/SKILL.md +10 -1
  4. package/agent-config/lib/install-claude.mjs +39 -110
  5. package/examples/primitive/channel.ts +109 -0
  6. package/examples/primitive/collection.ts +118 -0
  7. package/examples/primitive/controller.ts +118 -0
  8. package/examples/primitive/counter.ts +108 -0
  9. package/examples/primitive/env.d.ts +1 -0
  10. package/examples/primitive/eventbus.ts +77 -0
  11. package/examples/primitive/feed.ts +162 -0
  12. package/examples/primitive/model.ts +82 -0
  13. package/examples/primitive/pagination.ts +91 -0
  14. package/examples/primitive/pending.ts +189 -0
  15. package/examples/primitive/persistent-collection.ts +116 -0
  16. package/examples/primitive/resource.ts +114 -0
  17. package/examples/primitive/selection.ts +96 -0
  18. package/examples/primitive/sorting.ts +112 -0
  19. package/examples/primitive/timer.ts +58 -0
  20. package/examples/primitive/trackable.ts +225 -0
  21. package/examples/primitive/tsconfig.json +20 -0
  22. package/examples/primitive/viewmodel-service.ts +161 -0
  23. package/examples/react/AuthExample/index.html +12 -0
  24. package/examples/react/AuthExample/src/App.tsx +29 -0
  25. package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
  26. package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
  27. package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
  28. package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
  29. package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
  30. package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
  31. package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
  32. package/examples/react/AuthExample/src/env.d.ts +10 -0
  33. package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
  34. package/examples/react/AuthExample/src/main.tsx +10 -0
  35. package/examples/react/AuthExample/src/mock/api.ts +78 -0
  36. package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
  37. package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
  38. package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
  39. package/examples/react/AuthExample/src/styles.css +445 -0
  40. package/examples/react/AuthExample/src/types/auth.ts +12 -0
  41. package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
  42. package/examples/react/AuthExample/tsconfig.json +22 -0
  43. package/examples/react/AuthExample/vite.config.ts +18 -0
  44. package/examples/react/ComplexApp/index.html +12 -0
  45. package/examples/react/ComplexApp/src/App.tsx +17 -0
  46. package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
  47. package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
  48. package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
  49. package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
  50. package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
  51. package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
  52. package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
  53. package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
  54. package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
  55. package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
  56. package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
  57. package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
  58. package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
  59. package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
  60. package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
  61. package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
  62. package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
  63. package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
  64. package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
  65. package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
  66. package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
  67. package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
  68. package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
  69. package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
  70. package/examples/react/ComplexApp/src/env.d.ts +10 -0
  71. package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
  72. package/examples/react/ComplexApp/src/main.tsx +10 -0
  73. package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
  74. package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
  75. package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
  76. package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
  77. package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
  78. package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
  79. package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
  80. package/examples/react/ComplexApp/src/styles.css +463 -0
  81. package/examples/react/ComplexApp/src/types/activity.ts +8 -0
  82. package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
  83. package/examples/react/ComplexApp/src/types/social.ts +8 -0
  84. package/examples/react/ComplexApp/src/types/users.ts +6 -0
  85. package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
  86. package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
  87. package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
  88. package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
  89. package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
  90. package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
  91. package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
  92. package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
  93. package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
  94. package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
  95. package/examples/react/ComplexApp/tsconfig.json +22 -0
  96. package/examples/react/ComplexApp/vite.config.ts +18 -0
  97. package/examples/react/FullApp/index.html +12 -0
  98. package/examples/react/FullApp/src/App.tsx +28 -0
  99. package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
  100. package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
  101. package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
  102. package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
  103. package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
  104. package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
  105. package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
  106. package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
  107. package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
  108. package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
  109. package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
  110. package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
  111. package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
  112. package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
  113. package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
  114. package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
  115. package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
  116. package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
  117. package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
  118. package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
  119. package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
  120. package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
  121. package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
  122. package/examples/react/FullApp/src/env.d.ts +10 -0
  123. package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
  124. package/examples/react/FullApp/src/main.tsx +10 -0
  125. package/examples/react/FullApp/src/mock/delay.ts +21 -0
  126. package/examples/react/FullApp/src/mock/locations.ts +76 -0
  127. package/examples/react/FullApp/src/mock/messages.ts +237 -0
  128. package/examples/react/FullApp/src/mock/users.ts +84 -0
  129. package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
  130. package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
  131. package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
  132. package/examples/react/FullApp/src/services/AuthService.ts +18 -0
  133. package/examples/react/FullApp/src/services/LocationService.ts +23 -0
  134. package/examples/react/FullApp/src/services/MessageService.ts +65 -0
  135. package/examples/react/FullApp/src/services/UserService.ts +23 -0
  136. package/examples/react/FullApp/src/styles.css +767 -0
  137. package/examples/react/FullApp/src/types/conversation.ts +7 -0
  138. package/examples/react/FullApp/src/types/location.ts +12 -0
  139. package/examples/react/FullApp/src/types/message.ts +7 -0
  140. package/examples/react/FullApp/src/types/user.ts +10 -0
  141. package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
  142. package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
  143. package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
  144. package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
  145. package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
  146. package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
  147. package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
  148. package/examples/react/FullApp/tsconfig.json +22 -0
  149. package/examples/react/FullApp/vite.config.ts +18 -0
  150. package/examples/react/WorkerApp/index.html +12 -0
  151. package/examples/react/WorkerApp/src/App.tsx +24 -0
  152. package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
  153. package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
  154. package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
  155. package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
  156. package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
  157. package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
  158. package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
  159. package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
  160. package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
  161. package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
  162. package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
  163. package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
  164. package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
  165. package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
  166. package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
  167. package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
  168. package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
  169. package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
  170. package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
  171. package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
  172. package/examples/react/WorkerApp/src/env.d.ts +10 -0
  173. package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
  174. package/examples/react/WorkerApp/src/main.tsx +10 -0
  175. package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
  176. package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
  177. package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
  178. package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
  179. package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
  180. package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
  181. package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
  182. package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
  183. package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
  184. package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
  185. package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
  186. package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
  187. package/examples/react/WorkerApp/src/styles.css +756 -0
  188. package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
  189. package/examples/react/WorkerApp/src/types/message.ts +7 -0
  190. package/examples/react/WorkerApp/src/types/shift.ts +13 -0
  191. package/examples/react/WorkerApp/src/types/site.ts +8 -0
  192. package/examples/react/WorkerApp/src/types/worker.ts +8 -0
  193. package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
  194. package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
  195. package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
  196. package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
  197. package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
  198. package/examples/react/WorkerApp/tsconfig.json +22 -0
  199. package/examples/react/WorkerApp/vite.config.ts +18 -0
  200. package/package.json +4 -2
  201. /package/agent-config/claude-code/skills/{guide → mvc-kit}/anti-patterns.md +0 -0
  202. /package/agent-config/claude-code/skills/{guide → mvc-kit}/api-reference.md +0 -0
  203. /package/agent-config/claude-code/skills/{guide → mvc-kit}/patterns.md +0 -0
  204. /package/agent-config/claude-code/skills/{guide → mvc-kit}/recipes.md +0 -0
  205. /package/agent-config/claude-code/skills/{guide → mvc-kit}/testing.md +0 -0
  206. /package/agent-config/claude-code/skills/{review → mvc-kit-review}/SKILL.md +0 -0
  207. /package/agent-config/claude-code/skills/{review → mvc-kit-review}/checklist.md +0 -0
  208. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/SKILL.md +0 -0
  209. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/channel.md +0 -0
  210. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/collection.md +0 -0
  211. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/controller.md +0 -0
  212. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/eventbus.md +0 -0
  213. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/model.md +0 -0
  214. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/page-component.md +0 -0
  215. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/persistent-collection.md +0 -0
  216. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/resource.md +0 -0
  217. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/service.md +0 -0
  218. /package/agent-config/claude-code/skills/{scaffold → mvc-kit-scaffold}/templates/viewmodel.md +0 -0
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class RevenueChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'revenue';
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class TrafficChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'traffic';
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class UsersMetricChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'active-users';
5
+ }
@@ -0,0 +1,6 @@
1
+ import { Collection } from 'mvc-kit';
2
+ import type { DashboardDataPoint } from '../types/dashboard';
3
+
4
+ export class DashboardCollection extends Collection<DashboardDataPoint> {
5
+ static override MAX_SIZE = 50;
6
+ }
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class ErrorsCollection extends DashboardCollection {}
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class LatencyCollection extends DashboardCollection {}
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class OrdersCollection extends DashboardCollection {}
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class RevenueCollection extends DashboardCollection {}
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class TrafficCollection extends DashboardCollection {}
@@ -0,0 +1,3 @@
1
+ import { DashboardCollection } from './DashboardCollection';
2
+
3
+ export class UsersMetricCollection extends DashboardCollection {}
@@ -0,0 +1,31 @@
1
+ import { useLocal } from 'mvc-kit/react';
2
+ import { ActivityFeedViewModel } from '../../viewmodels/ActivityFeedViewModel';
3
+ import { ActivityItemRow } from './ActivityItemRow';
4
+ import { StatusIndicator } from '../shared/StatusIndicator';
5
+ import { Spinner } from '../shared/Spinner';
6
+
7
+ export function ActivityFeed() {
8
+ const [, vm] = useLocal(ActivityFeedViewModel);
9
+ const { loading, error } = vm.async.load;
10
+
11
+ return (
12
+ <div className="activity-feed">
13
+ <div className="section-header">
14
+ <h2>Activity Feed</h2>
15
+ <StatusIndicator connected={vm.isConnected} label={vm.isOnline ? 'Live' : 'Offline'} />
16
+ <span className="item-count">{vm.itemCount} items</span>
17
+ </div>
18
+ {loading && vm.itemCount === 0 && (
19
+ <div className="loading-center"><Spinner /></div>
20
+ )}
21
+ {error && (
22
+ <div className="error-banner">{error}</div>
23
+ )}
24
+ <div className="activity-list">
25
+ {vm.items.map(item => (
26
+ <ActivityItemRow key={item.id} item={item} />
27
+ ))}
28
+ </div>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,35 @@
1
+ import type { ActivityItem } from '../../types/activity';
2
+
3
+ interface ActivityItemRowProps {
4
+ item: ActivityItem;
5
+ }
6
+
7
+ export function ActivityItemRow({ item }: ActivityItemRowProps) {
8
+ const timeAgo = getTimeAgo(item.timestamp);
9
+
10
+ return (
11
+ <div className="activity-row">
12
+ <img
13
+ className={`activity-avatar ${item.userOnline ? '' : 'avatar-offline'}`}
14
+ src={item.userAvatar}
15
+ alt={item.userName}
16
+ width={36}
17
+ height={36}
18
+ />
19
+ <div className="activity-content">
20
+ <span className="activity-text">{item.text}</span>
21
+ <span className="activity-time">{timeAgo}</span>
22
+ </div>
23
+ </div>
24
+ );
25
+ }
26
+
27
+ function getTimeAgo(timestamp: string): string {
28
+ const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
29
+ if (seconds < 5) return 'just now';
30
+ if (seconds < 60) return `${seconds}s ago`;
31
+ const minutes = Math.floor(seconds / 60);
32
+ if (minutes < 60) return `${minutes}m ago`;
33
+ const hours = Math.floor(minutes / 60);
34
+ return `${hours}h ago`;
35
+ }
@@ -0,0 +1,37 @@
1
+ import { useLocal } from 'mvc-kit/react';
2
+ import type { DashboardCardViewModel } from '../../viewmodels/DashboardCardViewModel';
3
+ import { StatusIndicator } from '../shared/StatusIndicator';
4
+
5
+ interface DashboardCardProps {
6
+ VMClass: new () => DashboardCardViewModel;
7
+ title: string;
8
+ icon: string;
9
+ unit?: string;
10
+ }
11
+
12
+ export function DashboardCard({ VMClass, title, icon, unit }: DashboardCardProps) {
13
+ const [, vm] = useLocal(VMClass);
14
+
15
+ const trendArrow = vm.trend === 'up' ? '\u2191' : vm.trend === 'down' ? '\u2193' : '\u2192';
16
+ const trendClass = `trend-${vm.trend}`;
17
+
18
+ return (
19
+ <div className={`card ${vm.isOnline ? '' : 'card-disconnected'}`}>
20
+ <div className="card-header">
21
+ <span className="card-icon">{icon}</span>
22
+ <span className="card-title">{title}</span>
23
+ <StatusIndicator connected={vm.isConnected} />
24
+ </div>
25
+ <div className="card-value">
26
+ <span className="card-number">
27
+ {vm.latestValue.toLocaleString()}
28
+ </span>
29
+ {unit && <span className="card-unit">{unit}</span>}
30
+ <span className={`card-trend ${trendClass}`}>{trendArrow}</span>
31
+ </div>
32
+ <div className="card-footer">
33
+ {vm.dataPointCount} data points
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,34 @@
1
+ import { DashboardCard } from './DashboardCard';
2
+ import type { DashboardCardViewModel } from '../../viewmodels/DashboardCardViewModel';
3
+ import { OrdersCardViewModel } from '../../viewmodels/OrdersCardViewModel';
4
+ import { RevenueCardViewModel } from '../../viewmodels/RevenueCardViewModel';
5
+ import { UsersMetricCardViewModel } from '../../viewmodels/UsersMetricCardViewModel';
6
+ import { ErrorsCardViewModel } from '../../viewmodels/ErrorsCardViewModel';
7
+ import { LatencyCardViewModel } from '../../viewmodels/LatencyCardViewModel';
8
+ import { TrafficCardViewModel } from '../../viewmodels/TrafficCardViewModel';
9
+
10
+ interface CardConfig {
11
+ VMClass: new () => DashboardCardViewModel;
12
+ title: string;
13
+ icon: string;
14
+ unit?: string;
15
+ }
16
+
17
+ const cards: CardConfig[] = [
18
+ { VMClass: OrdersCardViewModel, title: 'Orders', icon: '\uD83D\uDCE6', unit: '/hr' },
19
+ { VMClass: RevenueCardViewModel, title: 'Revenue', icon: '\uD83D\uDCB0', unit: '$' },
20
+ { VMClass: UsersMetricCardViewModel, title: 'Active Users', icon: '\uD83D\uDC65' },
21
+ { VMClass: ErrorsCardViewModel, title: 'Errors', icon: '\u26A0\uFE0F', unit: '/min' },
22
+ { VMClass: LatencyCardViewModel, title: 'Latency', icon: '\u23F1\uFE0F', unit: 'ms' },
23
+ { VMClass: TrafficCardViewModel, title: 'Traffic', icon: '\uD83C\uDF10', unit: 'req/s' },
24
+ ];
25
+
26
+ export function DashboardPage() {
27
+ return (
28
+ <div className="dashboard-grid">
29
+ {cards.map(({ VMClass, title, icon, unit }) => (
30
+ <DashboardCard key={title} VMClass={VMClass} title={title} icon={icon} unit={unit} />
31
+ ))}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,32 @@
1
+ import { useSingleton } from 'mvc-kit/react';
2
+ import { AppStateViewModel } from '../../viewmodels/AppStateViewModel';
3
+ import { StatusIndicator } from '../shared/StatusIndicator';
4
+ import { SocialFeedPanel } from './SocialFeedPanel';
5
+
6
+ export function Navbar() {
7
+ const [state, vm] = useSingleton(AppStateViewModel);
8
+
9
+ return (
10
+ <>
11
+ <nav className="navbar">
12
+ <span className="navbar-brand">ComplexApp</span>
13
+ <div className="navbar-actions">
14
+ <StatusIndicator connected={state.online} label={state.online ? 'Online' : 'Offline'} />
15
+ <button
16
+ className={`btn ${state.online ? 'btn-danger' : 'btn-success'}`}
17
+ onClick={() => vm.toggleOnline()}
18
+ >
19
+ {state.online ? 'Go Offline' : 'Go Online'}
20
+ </button>
21
+ <button
22
+ className={`btn btn-secondary ${state.socialPanelOpen ? 'btn-active' : ''}`}
23
+ onClick={() => vm.toggleSocialPanel()}
24
+ >
25
+ Social Feed
26
+ </button>
27
+ </div>
28
+ </nav>
29
+ {state.socialPanelOpen && <SocialFeedPanel />}
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,57 @@
1
+ import { useLocal } from 'mvc-kit/react';
2
+ import { InfiniteScroll, CardList } from 'mvc-kit/react';
3
+ import { SocialFeedViewModel } from '../../viewmodels/SocialFeedViewModel';
4
+ import { Spinner } from '../shared/Spinner';
5
+ import type { SocialPost } from '../../types/social';
6
+
7
+ function PostCard({ post }: { post: SocialPost }) {
8
+ return (
9
+ <div className="social-post">
10
+ <div className="social-post-header">
11
+ <img
12
+ className="social-avatar"
13
+ src={post.avatarUrl}
14
+ alt={post.author}
15
+ width={32}
16
+ height={32}
17
+ />
18
+ <span className="social-author">{post.author}</span>
19
+ </div>
20
+ <p className="social-content">{post.content}</p>
21
+ <div className="social-footer">
22
+ <span className="social-likes">{'\u2764\uFE0F'} {post.likes}</span>
23
+ <span className="social-time">
24
+ {new Date(post.timestamp).toLocaleTimeString()}
25
+ </span>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export function SocialFeedPanel() {
32
+ const [state, vm] = useLocal(SocialFeedViewModel, { page: 0 });
33
+ const { loading } = vm.async.loadMore;
34
+
35
+ return (
36
+ <div className="social-feed-panel">
37
+ <h3 className="social-feed-title">Social Feed</h3>
38
+ <InfiniteScroll
39
+ hasMore={vm.hasMore}
40
+ loading={loading}
41
+ onLoadMore={() => vm.loadMore()}
42
+ renderLoading={() => (
43
+ <div className="loading-center"><Spinner /></div>
44
+ )}
45
+ renderEnd={() =>
46
+ vm.postCount > 0 ? <p className="end-of-feed">No more posts</p> : null
47
+ }
48
+ >
49
+ <CardList
50
+ items={vm.posts}
51
+ keyOf={(post: SocialPost) => post.id}
52
+ renderItem={(post: SocialPost) => <PostCard post={post} />}
53
+ />
54
+ </InfiniteScroll>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,3 @@
1
+ export function Spinner() {
2
+ return <span className="spinner" />;
3
+ }
@@ -0,0 +1,13 @@
1
+ interface StatusIndicatorProps {
2
+ connected: boolean;
3
+ label?: string;
4
+ }
5
+
6
+ export function StatusIndicator({ connected, label }: StatusIndicatorProps) {
7
+ return (
8
+ <span className="status-indicator">
9
+ <span className={`status-dot ${connected ? 'status-dot-online' : 'status-dot-offline'}`} />
10
+ {label && <span className="status-label">{label}</span>}
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,40 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useSingleton, useEvent } from 'mvc-kit/react';
3
+ import { AppEventBus } from '../../events/AppEventBus';
4
+
5
+ interface ToastItem {
6
+ id: number;
7
+ message: string;
8
+ type: 'info' | 'success' | 'error';
9
+ }
10
+
11
+ let nextId = 0;
12
+
13
+ export function Toast() {
14
+ const bus = useSingleton(AppEventBus);
15
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
16
+
17
+ const addToast = useCallback((item: Omit<ToastItem, 'id'>) => {
18
+ const id = nextId++;
19
+ setToasts(prev => [...prev, { ...item, id }]);
20
+ setTimeout(() => {
21
+ setToasts(prev => prev.filter(t => t.id !== id));
22
+ }, 3000);
23
+ }, []);
24
+
25
+ useEvent(bus, 'toast:show', ({ message, type }) => {
26
+ addToast({ message, type: type ?? 'info' });
27
+ });
28
+
29
+ if (toasts.length === 0) return null;
30
+
31
+ return (
32
+ <div className="toast-container">
33
+ {toasts.map(t => (
34
+ <div key={t.id} className={`toast toast-${t.type}`}>
35
+ {t.message}
36
+ </div>
37
+ ))}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,10 @@
1
+ declare const __MVC_KIT_DEV__: boolean;
2
+
3
+ declare module 'react-dom/client' {
4
+ import type { ReactNode } from 'react';
5
+ interface Root {
6
+ render(children: ReactNode): void;
7
+ unmount(): void;
8
+ }
9
+ export function createRoot(container: Element): Root;
10
+ }
@@ -0,0 +1,7 @@
1
+ import { EventBus } from 'mvc-kit';
2
+
3
+ export interface AppEvents {
4
+ 'toast:show': { message: string; type?: 'info' | 'success' | 'error' };
5
+ }
6
+
7
+ export class AppEventBus extends EventBus<AppEvents> {}
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,38 @@
1
+ export interface MockWSConfig {
2
+ minInterval: number;
3
+ maxInterval: number;
4
+ generator: () => any;
5
+ }
6
+
7
+ export class MockWebSocket {
8
+ private timer: ReturnType<typeof setTimeout> | null = null;
9
+ private closed = false;
10
+
11
+ constructor(private config: MockWSConfig) {}
12
+
13
+ connect(onMessage: (data: any) => void, signal?: AbortSignal): void {
14
+ if (signal?.aborted) return;
15
+ signal?.addEventListener('abort', () => this.close(), { once: true });
16
+ this.scheduleNext(onMessage);
17
+ }
18
+
19
+ close(): void {
20
+ if (this.closed) return;
21
+ this.closed = true;
22
+ if (this.timer !== null) {
23
+ clearTimeout(this.timer);
24
+ this.timer = null;
25
+ }
26
+ }
27
+
28
+ private scheduleNext(onMessage: (data: any) => void): void {
29
+ if (this.closed) return;
30
+ const { minInterval, maxInterval, generator } = this.config;
31
+ const interval = minInterval + Math.random() * (maxInterval - minInterval);
32
+ this.timer = setTimeout(() => {
33
+ if (this.closed) return;
34
+ onMessage(generator());
35
+ this.scheduleNext(onMessage);
36
+ }, interval);
37
+ }
38
+ }
@@ -0,0 +1,48 @@
1
+ import type { ActivityItem } from '../types/activity';
2
+ import type { MockWSConfig } from './MockWebSocket';
3
+ import { mockFetch } from './delay';
4
+
5
+ const NAMES = [
6
+ 'Alice Johnson', 'Bob Smith', 'Carol White', 'Dave Brown', 'Eve Davis',
7
+ 'Frank Wilson', 'Grace Lee', 'Hank Taylor', 'Ivy Martin', 'Jack Anderson',
8
+ ];
9
+
10
+ const ACTIONS = [
11
+ 'created a new order', 'updated their profile', 'submitted a report',
12
+ 'closed a ticket', 'uploaded a document', 'left a comment',
13
+ 'approved a request', 'joined the team', 'completed a task', 'shared a file',
14
+ ];
15
+
16
+ function randomItem<T>(arr: T[]): T {
17
+ return arr[Math.floor(Math.random() * arr.length)]!;
18
+ }
19
+
20
+ let activitySeq = 0;
21
+
22
+ function generateActivity(): ActivityItem {
23
+ const name = randomItem(NAMES);
24
+ return {
25
+ id: `act-${++activitySeq}`,
26
+ text: `${name} ${randomItem(ACTIONS)}`,
27
+ userName: name,
28
+ userAvatar: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`,
29
+ userOnline: Math.random() > 0.3,
30
+ timestamp: new Date().toISOString(),
31
+ };
32
+ }
33
+
34
+ export async function fetchInitialActivity(signal?: AbortSignal): Promise<ActivityItem[]> {
35
+ const items: ActivityItem[] = [];
36
+ for (let i = 0; i < 50; i++) {
37
+ items.push(generateActivity());
38
+ }
39
+ return mockFetch(items, 600, signal);
40
+ }
41
+
42
+ export function getActivityWSConfig(): MockWSConfig {
43
+ return {
44
+ minInterval: 1000,
45
+ maxInterval: 3000,
46
+ generator: generateActivity,
47
+ };
48
+ }
@@ -0,0 +1,45 @@
1
+ import type { MockWSConfig } from './MockWebSocket';
2
+
3
+ interface GeneratorConfig {
4
+ baseline: number;
5
+ variance: number;
6
+ min?: number;
7
+ minInterval?: number;
8
+ maxInterval?: number;
9
+ }
10
+
11
+ const configs: Record<string, GeneratorConfig> = {
12
+ orders: { baseline: 150, variance: 10 },
13
+ revenue: { baseline: 52000, variance: 500 },
14
+ 'active-users': { baseline: 1200, variance: 50 },
15
+ errors: { baseline: 3, variance: 2, min: 0 },
16
+ latency: { baseline: 145, variance: 30, min: 1 },
17
+ traffic: { baseline: 8500, variance: 500 },
18
+ };
19
+
20
+ let counters: Record<string, number> = {};
21
+
22
+ function randomWalk(serviceId: string, cfg: GeneratorConfig): number {
23
+ if (!(serviceId in counters)) {
24
+ counters[serviceId] = cfg.baseline;
25
+ }
26
+ const delta = (Math.random() - 0.5) * 2 * cfg.variance;
27
+ counters[serviceId] += delta;
28
+ if (cfg.min !== undefined && counters[serviceId] < cfg.min) {
29
+ counters[serviceId] = cfg.min;
30
+ }
31
+ return Math.round(counters[serviceId]);
32
+ }
33
+
34
+ export function getDashboardConfig(serviceId: string): MockWSConfig {
35
+ const cfg = configs[serviceId] ?? { baseline: 100, variance: 10 };
36
+ return {
37
+ minInterval: cfg.minInterval ?? 400,
38
+ maxInterval: cfg.maxInterval ?? 1500,
39
+ generator: () => ({
40
+ id: `${serviceId}-${Date.now()}`,
41
+ timestamp: new Date().toISOString(),
42
+ count: randomWalk(serviceId, cfg),
43
+ }),
44
+ };
45
+ }
@@ -0,0 +1,18 @@
1
+ export function delay(ms: number, signal?: AbortSignal): Promise<void> {
2
+ return new Promise((resolve, reject) => {
3
+ if (signal?.aborted) {
4
+ reject(signal.reason);
5
+ return;
6
+ }
7
+ const timer = setTimeout(resolve, ms);
8
+ signal?.addEventListener('abort', () => {
9
+ clearTimeout(timer);
10
+ reject(signal.reason);
11
+ }, { once: true });
12
+ });
13
+ }
14
+
15
+ export async function mockFetch<T>(data: T, ms = 300, signal?: AbortSignal): Promise<T> {
16
+ await delay(ms, signal);
17
+ return data;
18
+ }
@@ -0,0 +1,55 @@
1
+ import type { SocialPost } from '../types/social';
2
+ import { mockFetch } from './delay';
3
+
4
+ const AUTHORS = [
5
+ 'Sarah Chen', 'Mike Rodriguez', 'Emily Park', 'Chris Taylor', 'Jordan Lee',
6
+ 'Sam Williams', 'Alex Murphy', 'Kim Nguyen', 'Pat O\'Brien', 'Robin Cruz',
7
+ ];
8
+
9
+ const CONTENTS = [
10
+ 'Just shipped a new feature! The team crushed it this sprint.',
11
+ 'Anyone else excited about the new TypeScript release?',
12
+ 'Great architecture review today. Clean patterns make all the difference.',
13
+ 'Coffee and code — name a better duo.',
14
+ 'TIL: Object.freeze is shallow. Always has been.',
15
+ 'Pair programming session was incredibly productive today.',
16
+ 'Hot take: tests are documentation.',
17
+ 'Finally figured out that race condition. AbortController saves the day!',
18
+ 'Reading through the codebase and loving the consistency.',
19
+ 'Deployed to production with zero downtime. Feels good.',
20
+ 'State management doesn\'t have to be complicated.',
21
+ 'Code review feedback is a gift, not a critique.',
22
+ 'The best code is the code you don\'t have to write.',
23
+ 'Refactoring legacy code is oddly satisfying.',
24
+ 'Remember: premature optimization is the root of all evil.',
25
+ ];
26
+
27
+ let postSeq = 0;
28
+ const TOTAL_POSTS = 120;
29
+
30
+ export async function fetchSocialFeed(
31
+ page: number,
32
+ pageSize: number,
33
+ signal?: AbortSignal,
34
+ ): Promise<{ items: SocialPost[]; hasMore: boolean }> {
35
+ const start = page * pageSize;
36
+ if (start >= TOTAL_POSTS) {
37
+ return mockFetch({ items: [], hasMore: false }, 200, signal);
38
+ }
39
+
40
+ const count = Math.min(pageSize, TOTAL_POSTS - start);
41
+ const items: SocialPost[] = [];
42
+ for (let i = 0; i < count; i++) {
43
+ const author = AUTHORS[Math.floor(Math.random() * AUTHORS.length)]!;
44
+ items.push({
45
+ id: `post-${++postSeq}`,
46
+ content: CONTENTS[Math.floor(Math.random() * CONTENTS.length)]!,
47
+ author,
48
+ avatarUrl: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(author)}`,
49
+ timestamp: new Date(Date.now() - Math.random() * 86400000).toISOString(),
50
+ likes: Math.floor(Math.random() * 50),
51
+ });
52
+ }
53
+
54
+ return mockFetch({ items, hasMore: start + count < TOTAL_POSTS }, 500, signal);
55
+ }
@@ -0,0 +1,12 @@
1
+ import { Resource } from 'mvc-kit';
2
+ import type { ActivityItem } from '../types/activity';
3
+ import { fetchInitialActivity } from '../mock-remote/activity-api';
4
+
5
+ export class ActivityResource extends Resource<ActivityItem> {
6
+ static override MAX_SIZE = 100;
7
+
8
+ async loadInitial(): Promise<void> {
9
+ const items = await fetchInitialActivity(this.disposeSignal);
10
+ this.reset(items);
11
+ }
12
+ }
@@ -0,0 +1,17 @@
1
+ import { Resource } from 'mvc-kit';
2
+ import type { SocialPost } from '../types/social';
3
+ import { fetchSocialFeed } from '../mock-remote/social-api';
4
+
5
+ export class SocialFeedResource extends Resource<SocialPost> {
6
+ private _hasMore = true;
7
+
8
+ get hasMore(): boolean {
9
+ return this._hasMore;
10
+ }
11
+
12
+ async loadPage(page: number): Promise<void> {
13
+ const { items, hasMore } = await fetchSocialFeed(page, 20, this.disposeSignal);
14
+ this._hasMore = hasMore;
15
+ this.upsert(...items);
16
+ }
17
+ }