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,445 @@
1
+ *, *::before, *::after {
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ :root {
8
+ --color-bg: #f5f7fa;
9
+ --color-surface: #ffffff;
10
+ --color-primary: #4f46e5;
11
+ --color-primary-hover: #4338ca;
12
+ --color-text: #1e293b;
13
+ --color-text-secondary: #64748b;
14
+ --color-border: #e2e8f0;
15
+ --color-error: #ef4444;
16
+ --color-success: #22c55e;
17
+ --color-warning: #f59e0b;
18
+ --radius: 8px;
19
+ --header-height: 56px;
20
+ }
21
+
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24
+ background: var(--color-bg);
25
+ color: var(--color-text);
26
+ line-height: 1.5;
27
+ }
28
+
29
+ /* Auth Page (login/register) */
30
+ .auth-page {
31
+ min-height: 100vh;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background: var(--color-bg);
36
+ }
37
+
38
+ .auth-card {
39
+ background: var(--color-surface);
40
+ border: 1px solid var(--color-border);
41
+ border-radius: var(--radius);
42
+ padding: 2rem;
43
+ width: 100%;
44
+ max-width: 420px;
45
+ }
46
+
47
+ .auth-title {
48
+ font-size: 1.5rem;
49
+ font-weight: 700;
50
+ margin-bottom: 0.5rem;
51
+ text-align: center;
52
+ }
53
+
54
+ .auth-subtitle {
55
+ color: var(--color-text-secondary);
56
+ text-align: center;
57
+ margin-bottom: 1.5rem;
58
+ font-size: 0.875rem;
59
+ line-height: 1.6;
60
+ }
61
+
62
+ .auth-subtitle code {
63
+ background: var(--color-bg);
64
+ padding: 0.125rem 0.375rem;
65
+ border-radius: 4px;
66
+ font-size: 0.8125rem;
67
+ }
68
+
69
+ .auth-toggle {
70
+ text-align: center;
71
+ margin-top: 1.25rem;
72
+ font-size: 0.875rem;
73
+ color: var(--color-text-secondary);
74
+ }
75
+
76
+ .auth-loading {
77
+ min-height: 100vh;
78
+ display: flex;
79
+ flex-direction: column;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: 1rem;
83
+ color: var(--color-text-secondary);
84
+ }
85
+
86
+ /* App Header (horizontal nav) */
87
+ .app-header {
88
+ height: var(--header-height);
89
+ background: var(--color-surface);
90
+ border-bottom: 1px solid var(--color-border);
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ padding: 0 1.5rem;
95
+ position: sticky;
96
+ top: 0;
97
+ z-index: 10;
98
+ }
99
+
100
+ .header-nav {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.25rem;
104
+ }
105
+
106
+ .header-logo {
107
+ font-size: 1.125rem;
108
+ font-weight: 700;
109
+ color: var(--color-primary);
110
+ margin-right: 1.5rem;
111
+ }
112
+
113
+ .header-link {
114
+ padding: 0.5rem 0.75rem;
115
+ border-radius: var(--radius);
116
+ text-decoration: none;
117
+ color: var(--color-text-secondary);
118
+ font-weight: 500;
119
+ font-size: 0.875rem;
120
+ transition: background 0.15s, color 0.15s;
121
+ }
122
+
123
+ .header-link:hover {
124
+ background: var(--color-bg);
125
+ color: var(--color-text);
126
+ }
127
+
128
+ .header-link.active {
129
+ background: #eef2ff;
130
+ color: var(--color-primary);
131
+ }
132
+
133
+ .header-user {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 0.75rem;
137
+ }
138
+
139
+ .header-user-name {
140
+ font-size: 0.875rem;
141
+ font-weight: 500;
142
+ }
143
+
144
+ /* Page Content */
145
+ .page-content {
146
+ padding: 1.5rem;
147
+ max-width: 960px;
148
+ margin: 0 auto;
149
+ width: 100%;
150
+ }
151
+
152
+ .page-title {
153
+ font-size: 1.5rem;
154
+ font-weight: 700;
155
+ margin-bottom: 1.5rem;
156
+ }
157
+
158
+ /* Cards */
159
+ .card {
160
+ background: var(--color-surface);
161
+ border: 1px solid var(--color-border);
162
+ border-radius: var(--radius);
163
+ padding: 1.25rem;
164
+ }
165
+
166
+ .stats-grid {
167
+ display: grid;
168
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
169
+ gap: 1rem;
170
+ margin-bottom: 1.5rem;
171
+ }
172
+
173
+ .stat-card {
174
+ background: var(--color-surface);
175
+ border: 1px solid var(--color-border);
176
+ border-radius: var(--radius);
177
+ padding: 1.25rem;
178
+ }
179
+
180
+ .stat-value {
181
+ font-size: 2rem;
182
+ font-weight: 700;
183
+ color: var(--color-primary);
184
+ }
185
+
186
+ .stat-label {
187
+ color: var(--color-text-secondary);
188
+ font-size: 0.875rem;
189
+ margin-top: 0.25rem;
190
+ }
191
+
192
+ /* Profile */
193
+ .profile-card {
194
+ max-width: 480px;
195
+ }
196
+
197
+ .profile-header {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 1rem;
201
+ margin-bottom: 1.5rem;
202
+ }
203
+
204
+ .profile-details {
205
+ border-top: 1px solid var(--color-border);
206
+ }
207
+
208
+ .detail-row {
209
+ display: flex;
210
+ justify-content: space-between;
211
+ padding: 0.75rem 0;
212
+ border-bottom: 1px solid var(--color-border);
213
+ font-size: 0.875rem;
214
+ }
215
+
216
+ .detail-label {
217
+ color: var(--color-text-secondary);
218
+ }
219
+
220
+ /* Avatar */
221
+ .avatar {
222
+ width: 32px;
223
+ height: 32px;
224
+ border-radius: 50%;
225
+ background: var(--color-primary);
226
+ color: white;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ font-weight: 600;
231
+ font-size: 0.75rem;
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ .avatar-lg {
236
+ width: 64px;
237
+ height: 64px;
238
+ font-size: 1.25rem;
239
+ }
240
+
241
+ /* Access Denied */
242
+ .access-denied {
243
+ text-align: center;
244
+ padding: 3rem 1rem;
245
+ background: var(--color-surface);
246
+ border: 1px solid var(--color-border);
247
+ border-radius: var(--radius);
248
+ }
249
+
250
+ .access-denied h2 {
251
+ color: var(--color-error);
252
+ margin-bottom: 1rem;
253
+ }
254
+
255
+ .access-denied p {
256
+ color: var(--color-text-secondary);
257
+ margin-bottom: 0.5rem;
258
+ }
259
+
260
+ /* Forms */
261
+ .form-group {
262
+ margin-bottom: 1rem;
263
+ }
264
+
265
+ .form-label {
266
+ display: block;
267
+ font-weight: 500;
268
+ font-size: 0.875rem;
269
+ margin-bottom: 0.375rem;
270
+ color: var(--color-text);
271
+ }
272
+
273
+ .form-input {
274
+ width: 100%;
275
+ padding: 0.5rem 0.75rem;
276
+ border: 1px solid var(--color-border);
277
+ border-radius: var(--radius);
278
+ font-size: 0.875rem;
279
+ outline: none;
280
+ transition: border-color 0.15s;
281
+ }
282
+
283
+ .form-input:focus {
284
+ border-color: var(--color-primary);
285
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
286
+ }
287
+
288
+ .form-input.error {
289
+ border-color: var(--color-error);
290
+ }
291
+
292
+ .form-error {
293
+ color: var(--color-error);
294
+ font-size: 0.75rem;
295
+ margin-top: 0.25rem;
296
+ }
297
+
298
+ /* Buttons */
299
+ .btn {
300
+ display: inline-flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ padding: 0.5rem 1rem;
304
+ border: none;
305
+ border-radius: var(--radius);
306
+ font-size: 0.875rem;
307
+ font-weight: 500;
308
+ cursor: pointer;
309
+ transition: background 0.15s, opacity 0.15s;
310
+ }
311
+
312
+ .btn:disabled {
313
+ opacity: 0.5;
314
+ cursor: not-allowed;
315
+ }
316
+
317
+ .btn-primary {
318
+ background: var(--color-primary);
319
+ color: white;
320
+ }
321
+
322
+ .btn-primary:hover:not(:disabled) {
323
+ background: var(--color-primary-hover);
324
+ }
325
+
326
+ .btn-secondary {
327
+ background: var(--color-bg);
328
+ color: var(--color-text);
329
+ border: 1px solid var(--color-border);
330
+ }
331
+
332
+ .btn-secondary:hover:not(:disabled) {
333
+ background: var(--color-border);
334
+ }
335
+
336
+ .btn-danger {
337
+ background: var(--color-error);
338
+ color: white;
339
+ }
340
+
341
+ .btn-sm {
342
+ padding: 0.25rem 0.5rem;
343
+ font-size: 0.75rem;
344
+ }
345
+
346
+ /* Badges */
347
+ .badge {
348
+ display: inline-block;
349
+ padding: 0.125rem 0.5rem;
350
+ border-radius: 999px;
351
+ font-size: 0.75rem;
352
+ font-weight: 500;
353
+ }
354
+
355
+ .badge-admin { background: #e0e7ff; color: #3730a3; }
356
+ .badge-manager { background: #dbeafe; color: #1e40af; }
357
+ .badge-member { background: #f1f5f9; color: #475569; }
358
+
359
+ /* Error Banner */
360
+ .error-banner {
361
+ background: #fef2f2;
362
+ border: 1px solid #fecaca;
363
+ color: #991b1b;
364
+ padding: 0.75rem 1rem;
365
+ border-radius: var(--radius);
366
+ margin-bottom: 1rem;
367
+ font-size: 0.875rem;
368
+ }
369
+
370
+ /* Spinner */
371
+ .spinner {
372
+ display: inline-block;
373
+ width: 20px;
374
+ height: 20px;
375
+ border: 2px solid var(--color-border);
376
+ border-top-color: var(--color-primary);
377
+ border-radius: 50%;
378
+ animation: spin 0.6s linear infinite;
379
+ }
380
+
381
+ .spinner-lg {
382
+ width: 32px;
383
+ height: 32px;
384
+ border-width: 3px;
385
+ }
386
+
387
+ @keyframes spin {
388
+ to { transform: rotate(360deg); }
389
+ }
390
+
391
+ /* Links */
392
+ .link {
393
+ color: var(--color-primary);
394
+ text-decoration: none;
395
+ cursor: pointer;
396
+ background: none;
397
+ border: none;
398
+ font-size: inherit;
399
+ font-family: inherit;
400
+ }
401
+
402
+ .link:hover {
403
+ text-decoration: underline;
404
+ }
405
+
406
+ /* Toast */
407
+ .toast-container {
408
+ position: fixed;
409
+ bottom: 1.5rem;
410
+ right: 1.5rem;
411
+ z-index: 1000;
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 0.5rem;
415
+ }
416
+
417
+ .toast {
418
+ padding: 0.75rem 1rem;
419
+ border-radius: var(--radius);
420
+ font-size: 0.875rem;
421
+ font-weight: 500;
422
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
423
+ animation: slideUp 0.3s ease-out;
424
+ min-width: 250px;
425
+ }
426
+
427
+ .toast-success {
428
+ background: #166534;
429
+ color: white;
430
+ }
431
+
432
+ .toast-error {
433
+ background: #991b1b;
434
+ color: white;
435
+ }
436
+
437
+ .toast-info {
438
+ background: #1e40af;
439
+ color: white;
440
+ }
441
+
442
+ @keyframes slideUp {
443
+ from { opacity: 0; transform: translateY(10px); }
444
+ to { opacity: 1; transform: translateY(0); }
445
+ }
@@ -0,0 +1,12 @@
1
+ export interface AuthUser {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ role: 'admin' | 'manager' | 'member';
6
+ createdAt: string;
7
+ }
8
+
9
+ export interface AuthResponse {
10
+ user: AuthUser;
11
+ accessToken: string;
12
+ }
@@ -0,0 +1,111 @@
1
+ import { ViewModel, singleton, isAbortError, classifyError } from 'mvc-kit';
2
+ import type { AuthUser } from '../types/auth';
3
+ import { AuthService } from '../services/AuthService';
4
+ import { AppEventBus } from '../events/AppEventBus';
5
+
6
+ interface AuthState {
7
+ user: AuthUser | null;
8
+ accessToken: string | null;
9
+ }
10
+
11
+ interface AuthEvents {
12
+ loginFailed: { message: string };
13
+ sessionExpired: undefined;
14
+ }
15
+
16
+ const TOKEN_KEY = 'auth_token';
17
+
18
+ export class AuthViewModel extends ViewModel<AuthState, AuthEvents> {
19
+ static DEFAULT_STATE: AuthState = { user: null, accessToken: null };
20
+
21
+ // --- Private fields ---
22
+ private authService = singleton(AuthService);
23
+ private bus = singleton(AppEventBus);
24
+
25
+ // --- Computed getters ---
26
+ get isAuthenticated(): boolean {
27
+ return this.state.user !== null && this.state.accessToken !== null;
28
+ }
29
+
30
+ get displayName(): string {
31
+ return this.state.user?.name ?? '';
32
+ }
33
+
34
+ get initials(): string {
35
+ const { user } = this.state;
36
+ if (!user) return '';
37
+ return user.name
38
+ .split(' ')
39
+ .map(p => p[0])
40
+ .join('')
41
+ .toUpperCase()
42
+ .slice(0, 2);
43
+ }
44
+
45
+ get isAdmin(): boolean {
46
+ return this.state.user?.role === 'admin';
47
+ }
48
+
49
+ get userRole(): string {
50
+ return this.state.user?.role ?? '';
51
+ }
52
+
53
+ // --- Lifecycle ---
54
+ async onInit() {
55
+ const token = localStorage.getItem(TOKEN_KEY);
56
+ if (token) {
57
+ await this.restoreSession(token);
58
+ }
59
+ }
60
+
61
+ // --- Actions ---
62
+ async login(email: string, password: string) {
63
+ try {
64
+ const { user, accessToken } = await this.authService.login(email, password, this.disposeSignal);
65
+ localStorage.setItem(TOKEN_KEY, accessToken);
66
+ this.set({ user, accessToken });
67
+ this.bus.emit('toast:show', { message: `Welcome, ${user.name}!`, severity: 'success' });
68
+ } catch (e) {
69
+ if (!isAbortError(e)) {
70
+ this.emit('loginFailed', { message: classifyError(e).message });
71
+ }
72
+ throw e;
73
+ }
74
+ }
75
+
76
+ async register(name: string, email: string, password: string) {
77
+ try {
78
+ const { user, accessToken } = await this.authService.register(name, email, password, this.disposeSignal);
79
+ localStorage.setItem(TOKEN_KEY, accessToken);
80
+ this.set({ user, accessToken });
81
+ this.bus.emit('toast:show', { message: `Welcome, ${user.name}!`, severity: 'success' });
82
+ } catch (e) {
83
+ if (!isAbortError(e)) {
84
+ this.emit('loginFailed', { message: classifyError(e).message });
85
+ }
86
+ throw e;
87
+ }
88
+ }
89
+
90
+ logout() {
91
+ const { accessToken } = this.state;
92
+ if (accessToken) {
93
+ this.authService.logout(accessToken).catch(() => {});
94
+ }
95
+ localStorage.removeItem(TOKEN_KEY);
96
+ this.set({ user: null, accessToken: null });
97
+ this.bus.emit('toast:show', { message: 'Logged out', severity: 'info' });
98
+ }
99
+
100
+ // --- Private ---
101
+ private async restoreSession(token: string) {
102
+ try {
103
+ const user = await this.authService.getProfile(token, this.disposeSignal);
104
+ this.set({ user, accessToken: token });
105
+ } catch (e) {
106
+ if (isAbortError(e)) throw e;
107
+ localStorage.removeItem(TOKEN_KEY);
108
+ this.emit('sessionExpired', undefined);
109
+ }
110
+ }
111
+ }
@@ -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
+ }