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,26 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import { AppEventBus } from '../events/AppEventBus';
3
+
4
+ interface AppState {
5
+ online: boolean;
6
+ socialPanelOpen: boolean;
7
+ }
8
+
9
+ export class AppStateViewModel extends ViewModel<AppState> {
10
+ static DEFAULT_STATE: AppState = { online: true, socialPanelOpen: false };
11
+
12
+ private bus = singleton(AppEventBus);
13
+
14
+ toggleOnline() {
15
+ const online = !this.state.online;
16
+ this.set({ online });
17
+ this.bus.emit('toast:show', {
18
+ message: online ? 'Back online' : 'Gone offline',
19
+ type: online ? 'success' : 'error',
20
+ });
21
+ }
22
+
23
+ toggleSocialPanel() {
24
+ this.set({ socialPanelOpen: !this.state.socialPanelOpen });
25
+ }
26
+ }
@@ -0,0 +1,69 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import type { Collection } from 'mvc-kit';
3
+ import type { DashboardChannel } from '../channels/DashboardChannel';
4
+ import type { DashboardDataPoint } from '../types/dashboard';
5
+ import { AppStateViewModel } from './AppStateViewModel';
6
+
7
+ export abstract class DashboardCardViewModel extends ViewModel {
8
+ protected abstract channel: DashboardChannel;
9
+ protected abstract collection: Collection<DashboardDataPoint>;
10
+ private appState = singleton(AppStateViewModel);
11
+
12
+ // --- Computed getters ---
13
+
14
+ get dataPoints(): DashboardDataPoint[] {
15
+ return this.collection.items;
16
+ }
17
+
18
+ get latestValue(): number {
19
+ const items = this.collection.items;
20
+ return items.length > 0 ? items[items.length - 1]!.count : 0;
21
+ }
22
+
23
+ get previousValue(): number {
24
+ const items = this.collection.items;
25
+ return items.length > 1 ? items[items.length - 2]!.count : 0;
26
+ }
27
+
28
+ get trend(): 'up' | 'down' | 'flat' {
29
+ if (this.collection.length < 2) return 'flat';
30
+ const diff = this.latestValue - this.previousValue;
31
+ if (diff > 0) return 'up';
32
+ if (diff < 0) return 'down';
33
+ return 'flat';
34
+ }
35
+
36
+ get isConnected(): boolean {
37
+ return this.channel.state.connected;
38
+ }
39
+
40
+ get isOnline(): boolean {
41
+ return this.appState.state.online;
42
+ }
43
+
44
+ get dataPointCount(): number {
45
+ return this.collection.length;
46
+ }
47
+
48
+ // --- Lifecycle ---
49
+
50
+ protected onInit() {
51
+ this.pipeChannel(this.channel, 'data', this.collection);
52
+
53
+ if (this.appState.state.online) {
54
+ this.channel.connect();
55
+ }
56
+
57
+ this.subscribeTo(this.appState, () => {
58
+ if (this.appState.state.online) {
59
+ this.channel.connect();
60
+ } else {
61
+ this.channel.disconnect();
62
+ }
63
+ });
64
+ }
65
+
66
+ protected onDispose() {
67
+ this.channel.dispose();
68
+ }
69
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { ErrorsChannel } from '../channels/ErrorsChannel';
4
+ import { ErrorsCollection } from '../collections/ErrorsCollection';
5
+
6
+ export class ErrorsCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(ErrorsChannel);
8
+ protected collection = singleton(ErrorsCollection);
9
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { LatencyChannel } from '../channels/LatencyChannel';
4
+ import { LatencyCollection } from '../collections/LatencyCollection';
5
+
6
+ export class LatencyCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(LatencyChannel);
8
+ protected collection = singleton(LatencyCollection);
9
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { OrdersChannel } from '../channels/OrdersChannel';
4
+ import { OrdersCollection } from '../collections/OrdersCollection';
5
+
6
+ export class OrdersCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(OrdersChannel);
8
+ protected collection = singleton(OrdersCollection);
9
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { RevenueChannel } from '../channels/RevenueChannel';
4
+ import { RevenueCollection } from '../collections/RevenueCollection';
5
+
6
+ export class RevenueCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(RevenueChannel);
8
+ protected collection = singleton(RevenueCollection);
9
+ }
@@ -0,0 +1,39 @@
1
+ import { ViewModel, singleton } from 'mvc-kit';
2
+ import type { SocialPost } from '../types/social';
3
+ import { SocialFeedResource } from '../resources/SocialFeedResource';
4
+
5
+ interface SocialFeedState {
6
+ page: number;
7
+ }
8
+
9
+ export class SocialFeedViewModel extends ViewModel<SocialFeedState> {
10
+ private resource = singleton(SocialFeedResource);
11
+
12
+ // --- Computed getters ---
13
+
14
+ get posts(): SocialPost[] {
15
+ return this.resource.items;
16
+ }
17
+
18
+ get hasMore(): boolean {
19
+ return this.resource.hasMore;
20
+ }
21
+
22
+ get postCount(): number {
23
+ return this.resource.length;
24
+ }
25
+
26
+ // --- Lifecycle ---
27
+
28
+ protected onInit() {
29
+ if (this.resource.length === 0) this.loadMore();
30
+ }
31
+
32
+ // --- Actions ---
33
+
34
+ async loadMore() {
35
+ const page = this.state.page;
36
+ this.set({ page: page + 1 });
37
+ await this.resource.loadPage(page);
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { TrafficChannel } from '../channels/TrafficChannel';
4
+ import { TrafficCollection } from '../collections/TrafficCollection';
5
+
6
+ export class TrafficCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(TrafficChannel);
8
+ protected collection = singleton(TrafficCollection);
9
+ }
@@ -0,0 +1,9 @@
1
+ import { singleton } from 'mvc-kit';
2
+ import { DashboardCardViewModel } from './DashboardCardViewModel';
3
+ import { UsersMetricChannel } from '../channels/UsersMetricChannel';
4
+ import { UsersMetricCollection } from '../collections/UsersMetricCollection';
5
+
6
+ export class UsersMetricCardViewModel extends DashboardCardViewModel {
7
+ protected channel = singleton(UsersMetricChannel);
8
+ protected collection = singleton(UsersMetricCollection);
9
+ }
@@ -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: 3001,
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 Full 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,28 @@
1
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
2
+ import { AppShell } from './components/layout/AppShell';
3
+ import { LoginPage } from './components/auth/LoginPage';
4
+ import { DashboardPage } from './components/dashboard/DashboardPage';
5
+ import { UsersPage } from './components/users/UsersPage';
6
+ import { LocationsPage } from './components/locations/LocationsPage';
7
+ import { LocationProfilePage } from './components/locations/LocationProfilePage';
8
+ import { MessagingPage } from './components/messaging/MessagingPage';
9
+ import { Toast } from './components/shared/Toast';
10
+
11
+ export function App() {
12
+ return (
13
+ <BrowserRouter>
14
+ <Routes>
15
+ <Route path="/login" element={<LoginPage />} />
16
+ <Route element={<AppShell />}>
17
+ <Route path="/dashboard" element={<DashboardPage />} />
18
+ <Route path="/users" element={<UsersPage />} />
19
+ <Route path="/locations" element={<LocationsPage />} />
20
+ <Route path="/locations/:id" element={<LocationProfilePage />} />
21
+ <Route path="/messaging" element={<MessagingPage />} />
22
+ </Route>
23
+ <Route path="*" element={<Navigate to="/dashboard" replace />} />
24
+ </Routes>
25
+ <Toast />
26
+ </BrowserRouter>
27
+ );
28
+ }
@@ -0,0 +1,4 @@
1
+ import { Collection } from 'mvc-kit';
2
+ import type { ConversationState } from '../types/conversation';
3
+
4
+ export class ConversationsCollection extends Collection<ConversationState> {}
@@ -0,0 +1,4 @@
1
+ import { Collection } from 'mvc-kit';
2
+ import type { LocationState } from '../types/location';
3
+
4
+ export class LocationsCollection extends Collection<LocationState> {}
@@ -0,0 +1,80 @@
1
+ import { useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useSingleton, useEvent, useModel } from 'mvc-kit/react';
4
+ import { AuthViewModel } from '../../viewmodels/AuthViewModel';
5
+ import { LoginFormModel } from '../../models/LoginFormModel';
6
+
7
+ export function LoginPage() {
8
+ const [authState, authVM] = useSingleton(AuthViewModel);
9
+ const { loading, error } = authVM.async.login;
10
+ const { state, errors, valid, dirty, model } = useModel(
11
+ () => new LoginFormModel({ email: '', password: '' }),
12
+ );
13
+ const navigate = useNavigate();
14
+
15
+ // Redirect if already authenticated
16
+ useEffect(() => {
17
+ if (authState.isAuthenticated) {
18
+ navigate('/dashboard');
19
+ }
20
+ }, [authState.isAuthenticated, navigate]);
21
+
22
+ // Show inline error from loginFailed event
23
+ useEvent(authVM, 'loginFailed', () => {
24
+ // Error is already shown via vm.async.login error state
25
+ });
26
+
27
+ const handleSubmit = (e: React.FormEvent) => {
28
+ e.preventDefault();
29
+ if (!valid) return;
30
+ authVM.login(state.email, state.password);
31
+ };
32
+
33
+ return (
34
+ <div className="login-page">
35
+ <div className="login-card">
36
+ <h1 className="login-title">mvc-kit Demo</h1>
37
+ <p className="login-subtitle">
38
+ Enter any email and password (6+ chars) to log in
39
+ </p>
40
+
41
+ {error && <div className="error-banner">{error}</div>}
42
+
43
+ <form onSubmit={handleSubmit}>
44
+ <div className="form-group">
45
+ <label className="form-label">Email</label>
46
+ <input
47
+ className={`form-input ${errors.email && dirty ? 'error' : ''}`}
48
+ type="text"
49
+ value={state.email}
50
+ onChange={e => model.setEmail(e.target.value)}
51
+ placeholder="alice@example.com"
52
+ />
53
+ {errors.email && dirty && <div className="form-error">{errors.email}</div>}
54
+ </div>
55
+
56
+ <div className="form-group">
57
+ <label className="form-label">Password</label>
58
+ <input
59
+ className={`form-input ${errors.password && dirty ? 'error' : ''}`}
60
+ type="password"
61
+ value={state.password}
62
+ onChange={e => model.setPassword(e.target.value)}
63
+ placeholder="6+ characters"
64
+ />
65
+ {errors.password && dirty && <div className="form-error">{errors.password}</div>}
66
+ </div>
67
+
68
+ <button
69
+ type="submit"
70
+ className="btn btn-primary"
71
+ style={{ width: '100%', marginTop: '0.5rem' }}
72
+ disabled={!valid || loading}
73
+ >
74
+ {loading ? 'Signing in...' : 'Sign In'}
75
+ </button>
76
+ </form>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,29 @@
1
+ import { useLocal } from 'mvc-kit/react';
2
+ import { DashboardViewModel } from '../../viewmodels/DashboardViewModel';
3
+ import { StatsCard } from './StatsCard';
4
+ import { RecentActivityCard } from './RecentActivityCard';
5
+ import { Spinner } from '../shared/Spinner';
6
+ import { ErrorBanner } from '../shared/ErrorBanner';
7
+
8
+ export function DashboardPage() {
9
+ const [, vm] = useLocal(DashboardViewModel, {});
10
+ const { loading, error } = vm.async.load;
11
+
12
+ return (
13
+ <div>
14
+ <h1 className="page-title">Dashboard</h1>
15
+
16
+ {loading && <Spinner />}
17
+ {error && <ErrorBanner message={error} />}
18
+
19
+ <div className="stats-grid">
20
+ <StatsCard title="Total Users" value={vm.totalUsers} subtitle={`${vm.activeUsers} active`} />
21
+ <StatsCard title="Total Locations" value={vm.totalLocations} subtitle={`${vm.activeLocations} active`} />
22
+ <StatsCard title="Admins" value={vm.usersByRole['admin'] ?? 0} />
23
+ <StatsCard title="Managers" value={vm.usersByRole['manager'] ?? 0} />
24
+ </div>
25
+
26
+ <RecentActivityCard users={vm.recentUsers} />
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,35 @@
1
+ import type { UserState } from '../../types/user';
2
+
3
+ interface RecentActivityCardProps {
4
+ users: UserState[];
5
+ }
6
+
7
+ export function RecentActivityCard({ users }: RecentActivityCardProps) {
8
+ return (
9
+ <div className="card">
10
+ <h3 style={{ marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600 }}>
11
+ Recent Team Members
12
+ </h3>
13
+ <ul className="activity-list">
14
+ {users.map(user => (
15
+ <li key={user.id} className="activity-item">
16
+ <div className="avatar">
17
+ {user.firstName[0]}{user.lastName[0]}
18
+ </div>
19
+ <div>
20
+ <div style={{ fontWeight: 500 }}>
21
+ {user.firstName} {user.lastName}
22
+ </div>
23
+ <div style={{ fontSize: '0.75rem', color: 'var(--color-text-secondary)' }}>
24
+ {user.role} &middot; Joined {new Date(user.createdAt).toLocaleDateString()}
25
+ </div>
26
+ </div>
27
+ <span className={`badge badge-${user.status}`} style={{ marginLeft: 'auto' }}>
28
+ {user.status}
29
+ </span>
30
+ </li>
31
+ ))}
32
+ </ul>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,19 @@
1
+ interface StatsCardProps {
2
+ title: string;
3
+ value: number | string;
4
+ subtitle?: string;
5
+ }
6
+
7
+ export function StatsCard({ title, value, subtitle }: StatsCardProps) {
8
+ return (
9
+ <div className="stat-card">
10
+ <div className="stat-value">{value}</div>
11
+ <div className="stat-label">{title}</div>
12
+ {subtitle && (
13
+ <div style={{ fontSize: '0.75rem', color: 'var(--color-text-secondary)', marginTop: '0.25rem' }}>
14
+ {subtitle}
15
+ </div>
16
+ )}
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect } from 'react';
2
+ import { Outlet, useNavigate } from 'react-router-dom';
3
+ import { useSingleton } from 'mvc-kit/react';
4
+ import { AuthViewModel } from '../../viewmodels/AuthViewModel';
5
+ import { Sidebar } from './Sidebar';
6
+ import { Header } from './Header';
7
+
8
+ export function AppShell() {
9
+ const [state, vm] = useSingleton(AuthViewModel);
10
+ const navigate = useNavigate();
11
+
12
+ useEffect(() => {
13
+ if (!state.isAuthenticated) {
14
+ navigate('/login');
15
+ }
16
+ }, [state.isAuthenticated, navigate]);
17
+
18
+ if (!state.isAuthenticated || !state.user) return null;
19
+
20
+ return (
21
+ <div className="app-shell">
22
+ <Sidebar />
23
+ <div className="main-area">
24
+ <Header user={state.user} onLogout={() => vm.logout()} />
25
+ <div className="page-content">
26
+ <Outlet />
27
+ </div>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,25 @@
1
+ import type { UserState } from '../../types/user';
2
+
3
+ interface HeaderProps {
4
+ user: UserState;
5
+ onLogout: () => void;
6
+ }
7
+
8
+ export function Header({ user, onLogout }: HeaderProps) {
9
+ return (
10
+ <header className="header">
11
+ <div />
12
+ <div className="header-user">
13
+ <span style={{ fontSize: '0.875rem', color: 'var(--color-text-secondary)' }}>
14
+ {user.firstName} {user.lastName}
15
+ </span>
16
+ <div className="avatar">
17
+ {user.firstName[0]}{user.lastName[0]}
18
+ </div>
19
+ <button className="btn btn-secondary btn-sm" onClick={onLogout}>
20
+ Logout
21
+ </button>
22
+ </div>
23
+ </header>
24
+ );
25
+ }
@@ -0,0 +1,29 @@
1
+ import { Link, useLocation } from 'react-router-dom';
2
+
3
+ const navItems = [
4
+ { path: '/dashboard', label: 'Dashboard' },
5
+ { path: '/users', label: 'Users' },
6
+ { path: '/locations', label: 'Locations' },
7
+ { path: '/messaging', label: 'Messaging' },
8
+ ];
9
+
10
+ export function Sidebar() {
11
+ const { pathname } = useLocation();
12
+
13
+ return (
14
+ <aside className="sidebar">
15
+ <div className="sidebar-logo">mvc-kit</div>
16
+ <nav className="sidebar-nav">
17
+ {navItems.map(item => (
18
+ <Link
19
+ key={item.path}
20
+ to={item.path}
21
+ className={`sidebar-link ${pathname.startsWith(item.path) ? 'active' : ''}`}
22
+ >
23
+ {item.label}
24
+ </Link>
25
+ ))}
26
+ </nav>
27
+ </aside>
28
+ );
29
+ }
@@ -0,0 +1,60 @@
1
+ import type { LocationState } from '../../types/location';
2
+
3
+ interface LocationFiltersProps {
4
+ search: string;
5
+ typeFilter: 'all' | LocationState['type'];
6
+ statusFilter: 'all' | LocationState['status'];
7
+ onSearchChange: (value: string) => void;
8
+ onTypeFilterChange: (value: 'all' | LocationState['type']) => void;
9
+ onStatusFilterChange: (value: 'all' | LocationState['status']) => void;
10
+ }
11
+
12
+ export function LocationFilters({
13
+ search,
14
+ typeFilter,
15
+ statusFilter,
16
+ onSearchChange,
17
+ onTypeFilterChange,
18
+ onStatusFilterChange,
19
+ }: LocationFiltersProps) {
20
+ return (
21
+ <div className="filters">
22
+ <div className="filter-group">
23
+ <label className="filter-label">Search</label>
24
+ <input
25
+ className="filter-input"
26
+ type="text"
27
+ value={search}
28
+ onChange={e => onSearchChange(e.target.value)}
29
+ placeholder="Search by name or city..."
30
+ />
31
+ </div>
32
+ <div className="filter-group">
33
+ <label className="filter-label">Type</label>
34
+ <select
35
+ className="filter-select"
36
+ value={typeFilter}
37
+ onChange={e => onTypeFilterChange(e.target.value as 'all' | LocationState['type'])}
38
+ >
39
+ <option value="all">All Types</option>
40
+ <option value="office">Office</option>
41
+ <option value="warehouse">Warehouse</option>
42
+ <option value="retail">Retail</option>
43
+ </select>
44
+ </div>
45
+ <div className="filter-group">
46
+ <label className="filter-label">Status</label>
47
+ <select
48
+ className="filter-select"
49
+ value={statusFilter}
50
+ onChange={e => onStatusFilterChange(e.target.value as 'all' | LocationState['status'])}
51
+ >
52
+ <option value="all">All Statuses</option>
53
+ <option value="active">Active</option>
54
+ <option value="inactive">Inactive</option>
55
+ <option value="maintenance">Maintenance</option>
56
+ </select>
57
+ </div>
58
+ </div>
59
+ );
60
+ }