@anymux/ui-kit 0.1.0

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 (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. package/src/types-common.ts +21 -0
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "@anymux/ui-kit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Composite UI components for AnyMux — file browser, media, calendar, contacts, git",
7
+ "keywords": [
8
+ "anymux",
9
+ "ui-kit",
10
+ "react",
11
+ "file-browser",
12
+ "media",
13
+ "calendar",
14
+ "contacts"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/AnyMux/AnyMuxMonorepo.git",
19
+ "directory": "packages/ui-kit"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "main": "./src/index.ts",
25
+ "types": "./src/index.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./file-browser": {
32
+ "types": "./dist/file-browser.d.ts",
33
+ "import": "./dist/file-browser.js"
34
+ },
35
+ "./tree": {
36
+ "types": "./dist/tree.d.ts",
37
+ "import": "./dist/tree.js"
38
+ },
39
+ "./list": {
40
+ "types": "./dist/list.d.ts",
41
+ "import": "./dist/list.js"
42
+ },
43
+ "./git": {
44
+ "types": "./dist/git.d.ts",
45
+ "import": "./dist/git.js"
46
+ },
47
+ "./layout": {
48
+ "types": "./dist/layout.d.ts",
49
+ "import": "./dist/layout.js"
50
+ },
51
+ "./media": {
52
+ "types": "./dist/media.d.ts",
53
+ "import": "./dist/media.js"
54
+ },
55
+ "./calendar": {
56
+ "types": "./dist/calendar.d.ts",
57
+ "import": "./dist/calendar.js"
58
+ },
59
+ "./contacts": {
60
+ "types": "./dist/contacts.d.ts",
61
+ "import": "./dist/contacts.js"
62
+ },
63
+ "./icons": {
64
+ "types": "./dist/icons.d.ts",
65
+ "import": "./dist/icons.js"
66
+ }
67
+ },
68
+ "sideEffects": false,
69
+ "files": [
70
+ "dist",
71
+ "src",
72
+ "README.md"
73
+ ],
74
+ "dependencies": {
75
+ "@cushiontreemap/core": "^0.0.18",
76
+ "@panscale/core": "^0.3.0",
77
+ "@panscale/react": "^0.3.0",
78
+ "@panscale/web": "^0.3.0",
79
+ "clsx": "^2.1.0",
80
+ "idb": "^8.0.0",
81
+ "lucide-react": "^0.475.0",
82
+ "react-window": "^1.8.8",
83
+ "react-window-infinite-loader": "^1.0.9",
84
+ "squarify": "^1.1.0",
85
+ "tailwind-merge": "^2.2.1",
86
+ "transformation-matrix": "^3.1.0",
87
+ "@anymux/file-system": "0.1.0",
88
+ "@anymux/ui": "0.1.0"
89
+ },
90
+ "peerDependencies": {
91
+ "mobx": "^6.13.5",
92
+ "mobx-react-lite": "^4.0.7",
93
+ "react": "^19.0.0",
94
+ "react-dom": "^19.0.0"
95
+ },
96
+ "devDependencies": {
97
+ "@types/node": "^22.10.2",
98
+ "@types/react": "^19.0.0",
99
+ "@types/react-dom": "^19.0.0",
100
+ "@types/react-window": "^1.8.8",
101
+ "tsdown": "^0.10.2",
102
+ "typescript": "^5.7.3",
103
+ "@anymux/typescript-config": "0.0.0"
104
+ },
105
+ "scripts": {
106
+ "build": "tsdown",
107
+ "check": "tsc -p tsconfig.json --noEmit"
108
+ }
109
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import type { CalendarModel } from './CalendarModel';
4
+ import { EventCard } from './EventCard';
5
+
6
+ export interface AgendaViewProps {
7
+ model: CalendarModel;
8
+ className?: string;
9
+ }
10
+
11
+ export const AgendaView = observer<AgendaViewProps>(({ model, className = '' }) => {
12
+ const sortedEntries = Array.from(model.eventsByDay.entries())
13
+ .sort(([a], [b]) => a.localeCompare(b));
14
+
15
+ return (
16
+ <div className={`overflow-y-auto p-4 space-y-4 ${className}`}>
17
+ {sortedEntries.length === 0 && (
18
+ <div className="flex items-center justify-center h-32 text-gray-400 text-sm">No upcoming events</div>
19
+ )}
20
+ {sortedEntries.map(([dateKey, events]) => {
21
+ const date = new Date(dateKey + 'T00:00:00');
22
+ return (
23
+ <div key={dateKey}>
24
+ <h3 className="text-sm font-semibold text-gray-700 mb-2 sticky top-0 bg-white/90 backdrop-blur-sm py-1">
25
+ {date.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })}
26
+ </h3>
27
+ <div className="space-y-1.5">
28
+ {events.map(ev => (
29
+ <EventCard key={ev.id} event={ev} onClick={() => model.selectEvent(ev)} />
30
+ ))}
31
+ </div>
32
+ </div>
33
+ );
34
+ })}
35
+ </div>
36
+ );
37
+ });
@@ -0,0 +1,90 @@
1
+ import React, { useEffect } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
4
+ import { BrowserError } from '@anymux/ui/components/browser-error';
5
+ import type { CalendarModel, CalendarViewMode } from './CalendarModel';
6
+ import { MonthView } from './MonthView';
7
+ import { WeekView } from './WeekView';
8
+ import { DayView } from './DayView';
9
+ import { AgendaView } from './AgendaView';
10
+ import { CalendarSidebar } from './CalendarSidebar';
11
+
12
+ export interface CalendarBrowserProps {
13
+ model: CalendarModel;
14
+ className?: string;
15
+ showSidebar?: boolean;
16
+ }
17
+
18
+ const VIEW_LABELS: Record<CalendarViewMode, string> = {
19
+ month: 'Month',
20
+ week: 'Week',
21
+ day: 'Day',
22
+ agenda: 'Agenda',
23
+ };
24
+
25
+ export const CalendarBrowser = observer<CalendarBrowserProps>(({ model, className = '', showSidebar = true }) => {
26
+ useEffect(() => { model.loadEvents(); }, [model]);
27
+
28
+ return (
29
+ <div className={`flex h-full bg-white rounded-xl border border-gray-200 overflow-hidden ${className}`}>
30
+ {showSidebar && <CalendarSidebar model={model} />}
31
+
32
+ <div className="flex-1 flex flex-col min-w-0">
33
+ {/* Toolbar */}
34
+ <div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200">
35
+ <button onClick={() => model.today()} className="text-sm px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50">
36
+ Today
37
+ </button>
38
+ <button onClick={() => model.navigateBack()} className="p-1.5 hover:bg-gray-100 rounded-lg">
39
+ <ChevronLeft size={16} />
40
+ </button>
41
+ <button onClick={() => model.navigateForward()} className="p-1.5 hover:bg-gray-100 rounded-lg">
42
+ <ChevronRight size={16} />
43
+ </button>
44
+ <h2 className="text-sm font-medium text-gray-900 ml-2">
45
+ {model.currentDate.toLocaleDateString(undefined, {
46
+ month: 'long',
47
+ year: 'numeric',
48
+ ...(model.viewMode === 'day' ? { day: 'numeric', weekday: 'long' } : {})
49
+ })}
50
+ </h2>
51
+ <div className="ml-auto flex items-center border border-gray-200 rounded-lg overflow-hidden">
52
+ {(Object.keys(VIEW_LABELS) as CalendarViewMode[]).map(mode => (
53
+ <button
54
+ key={mode}
55
+ onClick={() => model.setViewMode(mode)}
56
+ className={`px-3 py-1.5 text-xs font-medium ${
57
+ model.viewMode === mode ? 'bg-blue-50 text-blue-600' : 'text-gray-500 hover:bg-gray-50'
58
+ }`}
59
+ >
60
+ {VIEW_LABELS[mode]}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ </div>
65
+
66
+ {/* Content */}
67
+ <div className="flex-1 overflow-hidden">
68
+ {model.loading ? (
69
+ <div className="flex items-center justify-center h-64">
70
+ <Loader2 size={24} className="animate-spin text-gray-400" />
71
+ </div>
72
+ ) : model.error ? (
73
+ <BrowserError
74
+ error={model.error}
75
+ context="Calendar"
76
+ onRetry={() => model.loadEvents()}
77
+ />
78
+ ) : (
79
+ <>
80
+ {model.viewMode === 'month' && <MonthView model={model} />}
81
+ {model.viewMode === 'week' && <WeekView model={model} />}
82
+ {model.viewMode === 'day' && <DayView model={model} />}
83
+ {model.viewMode === 'agenda' && <AgendaView model={model} />}
84
+ </>
85
+ )}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ });
@@ -0,0 +1,142 @@
1
+ import { makeAutoObservable, flow } from 'mobx';
2
+ import type { ICalendarProvider, CalendarEvent, CalendarInfo } from './types';
3
+
4
+ export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda';
5
+
6
+ export class CalendarModel {
7
+ events: CalendarEvent[] = [];
8
+ calendars: CalendarInfo[] = [];
9
+ currentDate: Date = new Date();
10
+ viewMode: CalendarViewMode = 'month';
11
+ selectedEvent: CalendarEvent | null = null;
12
+ loading = false;
13
+ error: string | null = null;
14
+
15
+ constructor(private provider: ICalendarProvider) {
16
+ makeAutoObservable(this);
17
+ }
18
+
19
+ get visibleDateRange(): { start: Date; end: Date } {
20
+ const d = this.currentDate;
21
+ switch (this.viewMode) {
22
+ case 'month': {
23
+ const start = new Date(d.getFullYear(), d.getMonth(), 1);
24
+ start.setDate(start.getDate() - start.getDay());
25
+ const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
26
+ end.setDate(end.getDate() + (6 - end.getDay()));
27
+ return { start, end };
28
+ }
29
+ case 'week': {
30
+ const start = new Date(d);
31
+ start.setDate(d.getDate() - d.getDay());
32
+ start.setHours(0, 0, 0, 0);
33
+ const end = new Date(start);
34
+ end.setDate(start.getDate() + 6);
35
+ end.setHours(23, 59, 59, 999);
36
+ return { start, end };
37
+ }
38
+ case 'day': {
39
+ const start = new Date(d);
40
+ start.setHours(0, 0, 0, 0);
41
+ const end = new Date(d);
42
+ end.setHours(23, 59, 59, 999);
43
+ return { start, end };
44
+ }
45
+ case 'agenda': {
46
+ const start = new Date(d);
47
+ start.setHours(0, 0, 0, 0);
48
+ const end = new Date(d);
49
+ end.setDate(end.getDate() + 30);
50
+ return { start, end };
51
+ }
52
+ }
53
+ }
54
+
55
+ get eventsForCurrentView(): CalendarEvent[] {
56
+ const { start, end } = this.visibleDateRange;
57
+ return this.events.filter(e =>
58
+ e.endDate >= start && e.startDate <= end
59
+ );
60
+ }
61
+
62
+ get eventsByDay(): Map<string, CalendarEvent[]> {
63
+ const map = new Map<string, CalendarEvent[]>();
64
+ for (const event of this.eventsForCurrentView) {
65
+ const key = event.startDate.toISOString().slice(0, 10);
66
+ const list = map.get(key) ?? [];
67
+ list.push(event);
68
+ map.set(key, list);
69
+ }
70
+ return map;
71
+ }
72
+
73
+ loadEvents = flow(function* (this: CalendarModel) {
74
+ this.loading = true;
75
+ this.error = null;
76
+ try {
77
+ const { start, end } = this.visibleDateRange;
78
+ const [events, calendars] = yield Promise.all([
79
+ this.provider.getEventsByRange(start, end),
80
+ this.provider.getCalendars()
81
+ ]);
82
+ this.events = events;
83
+ this.calendars = calendars;
84
+ } catch (err: any) {
85
+ this.error = err?.message || 'Failed to load calendar events';
86
+ } finally {
87
+ this.loading = false;
88
+ }
89
+ });
90
+
91
+ selectEvent(event: CalendarEvent | null) {
92
+ this.selectedEvent = event;
93
+ }
94
+
95
+ setDate(date: Date) {
96
+ this.currentDate = date;
97
+ this.loadEvents();
98
+ }
99
+
100
+ setViewMode(mode: CalendarViewMode) {
101
+ this.viewMode = mode;
102
+ this.loadEvents();
103
+ }
104
+
105
+ navigateForward() {
106
+ const d = new Date(this.currentDate);
107
+ switch (this.viewMode) {
108
+ case 'month': d.setMonth(d.getMonth() + 1); break;
109
+ case 'week': d.setDate(d.getDate() + 7); break;
110
+ case 'day': d.setDate(d.getDate() + 1); break;
111
+ case 'agenda': d.setDate(d.getDate() + 30); break;
112
+ }
113
+ this.setDate(d);
114
+ }
115
+
116
+ navigateBack() {
117
+ const d = new Date(this.currentDate);
118
+ switch (this.viewMode) {
119
+ case 'month': d.setMonth(d.getMonth() - 1); break;
120
+ case 'week': d.setDate(d.getDate() - 7); break;
121
+ case 'day': d.setDate(d.getDate() - 1); break;
122
+ case 'agenda': d.setDate(d.getDate() - 30); break;
123
+ }
124
+ this.setDate(d);
125
+ }
126
+
127
+ today() {
128
+ this.setDate(new Date());
129
+ }
130
+
131
+ createEvent = flow(function* (this: CalendarModel, event: Omit<CalendarEvent, 'id' | 'createdAt' | 'updatedAt'>) {
132
+ const created = yield this.provider.createItem(event);
133
+ this.events.push(created);
134
+ return created;
135
+ });
136
+
137
+ deleteEvent = flow(function* (this: CalendarModel, id: string) {
138
+ yield this.provider.deleteItem(id);
139
+ this.events = this.events.filter(e => e.id !== id);
140
+ if (this.selectedEvent?.id === id) this.selectedEvent = null;
141
+ });
142
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import type { CalendarModel } from './CalendarModel';
5
+
6
+ export interface CalendarSidebarProps {
7
+ model: CalendarModel;
8
+ className?: string;
9
+ }
10
+
11
+ const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
12
+
13
+ export const CalendarSidebar = observer<CalendarSidebarProps>(({ model, className = '' }) => {
14
+ const d = model.currentDate;
15
+ const year = d.getFullYear();
16
+ const month = d.getMonth();
17
+
18
+ const firstDay = new Date(year, month, 1).getDay();
19
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
20
+ const today = new Date();
21
+
22
+ const cells: (number | null)[] = [];
23
+ for (let i = 0; i < firstDay; i++) cells.push(null);
24
+ for (let i = 1; i <= daysInMonth; i++) cells.push(i);
25
+
26
+ const isToday = (day: number) =>
27
+ day === today.getDate() && month === today.getMonth() && year === today.getFullYear();
28
+
29
+ const isSelected = (day: number) =>
30
+ day === d.getDate();
31
+
32
+ return (
33
+ <div className={`w-56 border-r border-gray-200 bg-gray-50 p-3 ${className}`}>
34
+ {/* Mini month */}
35
+ <div className="mb-4">
36
+ <div className="flex items-center justify-between mb-2">
37
+ <button onClick={() => model.navigateBack()} className="p-1 hover:bg-gray-200 rounded">
38
+ <ChevronLeft size={14} />
39
+ </button>
40
+ <span className="text-sm font-medium">
41
+ {d.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}
42
+ </span>
43
+ <button onClick={() => model.navigateForward()} className="p-1 hover:bg-gray-200 rounded">
44
+ <ChevronRight size={14} />
45
+ </button>
46
+ </div>
47
+ <div className="grid grid-cols-7 gap-0.5 text-center">
48
+ {DAYS.map(day => (
49
+ <div key={day} className="text-[10px] text-gray-400 font-medium py-0.5">{day}</div>
50
+ ))}
51
+ {cells.map((day, i) => (
52
+ <button
53
+ key={i}
54
+ disabled={day === null}
55
+ onClick={() => day && model.setDate(new Date(year, month, day))}
56
+ className={`text-xs py-0.5 rounded ${
57
+ day === null ? '' :
58
+ isSelected(day) ? 'bg-blue-500 text-white' :
59
+ isToday(day) ? 'bg-blue-100 text-blue-700 font-bold' :
60
+ 'text-gray-700 hover:bg-gray-200'
61
+ }`}
62
+ >
63
+ {day}
64
+ </button>
65
+ ))}
66
+ </div>
67
+ </div>
68
+
69
+ {/* Calendar list */}
70
+ <div>
71
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Calendars</h3>
72
+ {model.calendars.map(cal => (
73
+ <div key={cal.id} className="flex items-center gap-2 px-2 py-1.5 text-sm text-gray-700">
74
+ <div className="w-3 h-3 rounded-full" style={{ backgroundColor: cal.color }} />
75
+ <span className="truncate" title={cal.name}>{cal.name}</span>
76
+ </div>
77
+ ))}
78
+ </div>
79
+ </div>
80
+ );
81
+ });
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import type { CalendarModel } from './CalendarModel';
4
+
5
+ export interface DayViewProps {
6
+ model: CalendarModel;
7
+ className?: string;
8
+ }
9
+
10
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
11
+
12
+ export const DayView = observer<DayViewProps>(({ model, className = '' }) => {
13
+ const d = model.currentDate;
14
+ const dateKey = d.toISOString().slice(0, 10);
15
+ const dayEvents = model.eventsByDay.get(dateKey) ?? [];
16
+ const allDayEvents = dayEvents.filter(e => e.allDay);
17
+ const timedEvents = dayEvents.filter(e => !e.allDay);
18
+
19
+ return (
20
+ <div className={`flex flex-col h-full overflow-auto ${className}`}>
21
+ {/* Header */}
22
+ <div className="border-b border-gray-200 px-4 py-2 sticky top-0 bg-white z-10">
23
+ <h2 className="text-lg font-medium text-gray-900">
24
+ {d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })}
25
+ </h2>
26
+ {allDayEvents.length > 0 && (
27
+ <div className="flex gap-1 mt-1">
28
+ {allDayEvents.map(ev => (
29
+ <button
30
+ key={ev.id}
31
+ onClick={() => model.selectEvent(ev)}
32
+ className="text-xs px-2 py-0.5 rounded"
33
+ style={{ backgroundColor: `${ev.color ?? '#3b82f6'}20`, color: ev.color ?? '#3b82f6' }}
34
+ >
35
+ {ev.title}
36
+ </button>
37
+ ))}
38
+ </div>
39
+ )}
40
+ </div>
41
+
42
+ {/* Time slots */}
43
+ <div className="flex-1">
44
+ {HOURS.map(hour => {
45
+ const hourEvents = timedEvents.filter(e => e.startDate.getHours() === hour);
46
+ return (
47
+ <div key={hour} className="grid grid-cols-[60px_1fr] border-b border-gray-50 min-h-[48px]">
48
+ <div className="text-[10px] text-gray-400 text-right pr-2 -mt-2">
49
+ {hour === 0 ? '' : `${hour % 12 || 12} ${hour < 12 ? 'AM' : 'PM'}`}
50
+ </div>
51
+ <div className="border-l border-gray-100 relative pl-1">
52
+ {hourEvents.map(ev => (
53
+ <button
54
+ key={ev.id}
55
+ onClick={() => model.selectEvent(ev)}
56
+ className="w-full text-left text-xs p-1.5 rounded mb-0.5"
57
+ style={{
58
+ backgroundColor: `${ev.color ?? '#3b82f6'}15`,
59
+ color: ev.color ?? '#3b82f6',
60
+ borderLeft: `2px solid ${ev.color ?? '#3b82f6'}`
61
+ }}
62
+ >
63
+ <span className="font-medium">{ev.title}</span>
64
+ <span className="ml-2 opacity-70">
65
+ {ev.startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
66
+ </span>
67
+ </button>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ );
72
+ })}
73
+ </div>
74
+ </div>
75
+ );
76
+ });
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { Clock, MapPin } from 'lucide-react';
3
+ import type { CalendarEvent } from './types';
4
+
5
+ export interface EventCardProps {
6
+ event: CalendarEvent;
7
+ compact?: boolean;
8
+ onClick?: () => void;
9
+ className?: string;
10
+ }
11
+
12
+ const formatTime = (d: Date) => d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
13
+
14
+ export const EventCard = ({ event, compact = false, onClick, className = '' }: EventCardProps) => {
15
+ const color = event.color ?? '#3b82f6';
16
+
17
+ if (compact) {
18
+ return (
19
+ <button
20
+ onClick={onClick}
21
+ className={`text-left text-xs px-1.5 py-0.5 rounded truncate w-full hover:opacity-80 transition-opacity ${className}`}
22
+ style={{ backgroundColor: `${color}20`, color, borderLeft: `2px solid ${color}` }}
23
+ title={event.title}
24
+ >
25
+ {event.title}
26
+ </button>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <button
32
+ onClick={onClick}
33
+ className={`text-left w-full p-3 rounded-lg border border-gray-200 hover:shadow-sm transition-shadow ${className}`}
34
+ style={{ borderLeftWidth: '3px', borderLeftColor: color }}
35
+ >
36
+ <p className="text-sm font-medium text-gray-900 truncate" title={event.title}>{event.title}</p>
37
+ <div className="mt-1 flex items-center gap-3 text-xs text-gray-500">
38
+ <span className="flex items-center gap-1">
39
+ <Clock size={12} />
40
+ {event.allDay ? 'All day' : `${formatTime(event.startDate)} - ${formatTime(event.endDate)}`}
41
+ </span>
42
+ {event.location && (
43
+ <span className="flex items-center gap-1">
44
+ <MapPin size={12} />
45
+ {event.location}
46
+ </span>
47
+ )}
48
+ </div>
49
+ </button>
50
+ );
51
+ };
@@ -0,0 +1,98 @@
1
+ import type { ICalendarProvider, CalendarEvent, CalendarInfo } from './types';
2
+
3
+ const CALENDARS: CalendarInfo[] = [
4
+ { id: 'personal', name: 'Personal', color: '#3b82f6' },
5
+ { id: 'work', name: 'Work', color: '#10b981' },
6
+ { id: 'family', name: 'Family', color: '#f59e0b' },
7
+ ];
8
+
9
+ const EVENT_TITLES = [
10
+ 'Team standup', 'Lunch with Sarah', 'Design review', 'Gym session',
11
+ 'Doctor appointment', 'Code review', 'Sprint planning', 'Movie night',
12
+ 'Grocery shopping', 'Piano lesson', 'Board meeting', 'Yoga class',
13
+ 'Project deadline', 'Coffee with Alex', 'Dentist', 'Book club',
14
+ ];
15
+
16
+ const LOCATIONS = ['Office', 'Zoom', 'Conference Room A', 'Downtown Cafe', 'Home', undefined];
17
+
18
+ function generateEvents(): CalendarEvent[] {
19
+ const events: CalendarEvent[] = [];
20
+ const now = new Date();
21
+ const baseDate = new Date(now.getFullYear(), now.getMonth(), 1);
22
+
23
+ for (let i = 0; i < 40; i++) {
24
+ const day = Math.floor(Math.random() * 35) - 5;
25
+ const hour = 8 + Math.floor(Math.random() * 10);
26
+ const duration = 1 + Math.floor(Math.random() * 3);
27
+ const allDay = i % 8 === 0;
28
+ const calendar = CALENDARS[i % CALENDARS.length];
29
+
30
+ const startDate = new Date(baseDate);
31
+ startDate.setDate(startDate.getDate() + day);
32
+ startDate.setHours(hour, 0, 0, 0);
33
+
34
+ const endDate = new Date(startDate);
35
+ if (allDay) {
36
+ endDate.setHours(23, 59, 59, 999);
37
+ } else {
38
+ endDate.setHours(hour + duration);
39
+ }
40
+
41
+ events.push({
42
+ id: `event-${i}`,
43
+ type: 'calendar-event',
44
+ title: EVENT_TITLES[i % EVENT_TITLES.length],
45
+ startDate,
46
+ endDate,
47
+ allDay,
48
+ location: LOCATIONS[i % LOCATIONS.length],
49
+ attendees: i % 3 === 0 ? ['alice@example.com', 'bob@example.com'] : undefined,
50
+ color: calendar.color,
51
+ calendarId: calendar.id,
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ });
55
+ }
56
+ return events;
57
+ }
58
+
59
+ export class MockCalendarProvider implements ICalendarProvider {
60
+ private events = generateEvents();
61
+
62
+ async listItems() { return this.events; }
63
+
64
+ async getItem(id: string) { return this.events.find(e => e.id === id) ?? null; }
65
+
66
+ async createItem(item: Omit<CalendarEvent, 'id' | 'createdAt' | 'updatedAt'>) {
67
+ const now = new Date();
68
+ const created: CalendarEvent = { ...item, id: `event-${Date.now()}`, createdAt: now, updatedAt: now } as CalendarEvent;
69
+ this.events.push(created);
70
+ return created;
71
+ }
72
+
73
+ async updateItem(id: string, updates: Partial<CalendarEvent>) {
74
+ const idx = this.events.findIndex(e => e.id === id);
75
+ if (idx === -1) throw new Error('Not found');
76
+ this.events[idx] = { ...this.events[idx], ...updates, updatedAt: new Date() };
77
+ return this.events[idx];
78
+ }
79
+
80
+ async deleteItem(id: string) {
81
+ this.events = this.events.filter(e => e.id !== id);
82
+ }
83
+
84
+ async search(query: string) {
85
+ const q = query.toLowerCase();
86
+ return this.events.filter(e => e.title.toLowerCase().includes(q));
87
+ }
88
+
89
+ async getCalendars() { return CALENDARS; }
90
+
91
+ async getEventsByRange(start: Date, end: Date) {
92
+ return this.events.filter(e => e.endDate >= start && e.startDate <= end);
93
+ }
94
+
95
+ async getEventsByCalendar(calendarId: string) {
96
+ return this.events.filter(e => e.calendarId === calendarId);
97
+ }
98
+ }