mvc-kit 2.13.0 → 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 (200) hide show
  1. package/BEST_PRACTICES.md +1390 -0
  2. package/agent-config/claude-code/agents/mvc-kit-architect.md +6 -1
  3. package/agent-config/claude-code/skills/guide/SKILL.md +9 -0
  4. package/agent-config/lib/install-claude.mjs +8 -2
  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
@@ -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
+ });
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mvc-kit Complex Example</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,17 @@
1
+ import { Navbar } from './components/layout/Navbar';
2
+ import { DashboardPage } from './components/dashboard/DashboardPage';
3
+ import { ActivityFeed } from './components/activity/ActivityFeed';
4
+ import { Toast } from './components/shared/Toast';
5
+
6
+ export function App() {
7
+ return (
8
+ <div className="app">
9
+ <Navbar />
10
+ <main className="main-content">
11
+ <DashboardPage />
12
+ <ActivityFeed />
13
+ </main>
14
+ <Toast />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,24 @@
1
+ import { Channel } from 'mvc-kit';
2
+ import { MockWebSocket } from '../mock-remote/MockWebSocket';
3
+ import { getActivityWSConfig } from '../mock-remote/activity-api';
4
+ import type { ActivityItem } from '../types/activity';
5
+
6
+ export interface ActivityMessages {
7
+ activity: ActivityItem;
8
+ }
9
+
10
+ export class ActivityChannel extends Channel<ActivityMessages> {
11
+ static override MAX_ATTEMPTS = 5;
12
+
13
+ private ws: MockWebSocket | null = null;
14
+
15
+ protected open(signal: AbortSignal): void {
16
+ this.ws = new MockWebSocket(getActivityWSConfig());
17
+ this.ws.connect((data) => this.receive('activity', data), signal);
18
+ }
19
+
20
+ protected close(): void {
21
+ this.ws?.close();
22
+ this.ws = null;
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ import { Channel } from 'mvc-kit';
2
+ import { MockWebSocket } from '../mock-remote/MockWebSocket';
3
+ import { getDashboardConfig } from '../mock-remote/dashboard-generators';
4
+ import type { DashboardDataPoint } from '../types/dashboard';
5
+
6
+ export interface DashboardMessages {
7
+ data: DashboardDataPoint;
8
+ }
9
+
10
+ export abstract class DashboardChannel extends Channel<DashboardMessages> {
11
+ static SERVICE_ID: string;
12
+ static override MAX_ATTEMPTS = 5;
13
+
14
+ private ws: MockWebSocket | null = null;
15
+
16
+ protected open(signal: AbortSignal): void {
17
+ const serviceId = (this.constructor as typeof DashboardChannel).SERVICE_ID;
18
+ this.ws = new MockWebSocket(getDashboardConfig(serviceId));
19
+ this.ws.connect((data) => this.receive('data', data), signal);
20
+ }
21
+
22
+ protected close(): void {
23
+ this.ws?.close();
24
+ this.ws = null;
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class ErrorsChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'errors';
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class LatencyChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'latency';
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DashboardChannel } from './DashboardChannel';
2
+
3
+ export class OrdersChannel extends DashboardChannel {
4
+ static override SERVICE_ID = 'orders';
5
+ }
@@ -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
+ }