@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
@@ -0,0 +1,77 @@
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 MonthViewProps {
7
+ model: CalendarModel;
8
+ className?: string;
9
+ }
10
+
11
+ const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
12
+
13
+ export const MonthView = observer<MonthViewProps>(({ model, className = '' }) => {
14
+ const d = model.currentDate;
15
+ const year = d.getFullYear();
16
+ const month = d.getMonth();
17
+ const today = new Date();
18
+
19
+ const firstDay = new Date(year, month, 1).getDay();
20
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
21
+
22
+ const weeks: (number | null)[][] = [];
23
+ let week: (number | null)[] = [];
24
+ for (let i = 0; i < firstDay; i++) week.push(null);
25
+ for (let day = 1; day <= daysInMonth; day++) {
26
+ week.push(day);
27
+ if (week.length === 7) { weeks.push(week); week = []; }
28
+ }
29
+ if (week.length > 0) {
30
+ while (week.length < 7) week.push(null);
31
+ weeks.push(week);
32
+ }
33
+
34
+ const isToday = (day: number) =>
35
+ day === today.getDate() && month === today.getMonth() && year === today.getFullYear();
36
+
37
+ return (
38
+ <div className={`flex flex-col h-full ${className}`}>
39
+ <div className="grid grid-cols-7 border-b border-gray-200">
40
+ {DAYS.map(day => (
41
+ <div key={day} className="px-2 py-2 text-xs font-medium text-gray-500 text-center">{day.slice(0, 3)}</div>
42
+ ))}
43
+ </div>
44
+ <div className="flex-1 grid grid-rows-[repeat(auto-fill,1fr)]">
45
+ {weeks.map((week, wi) => (
46
+ <div key={wi} className="grid grid-cols-7 border-b border-gray-100 min-h-[80px]">
47
+ {week.map((day, di) => {
48
+ const dateKey = day ? `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` : null;
49
+ const events = dateKey ? (model.eventsByDay.get(dateKey) ?? []) : [];
50
+ return (
51
+ <div key={di} className={`border-r border-gray-100 p-1 ${day === null ? 'bg-gray-50' : ''}`}>
52
+ {day !== null && (
53
+ <>
54
+ <div className={`text-xs mb-0.5 w-6 h-6 flex items-center justify-center rounded-full ${
55
+ isToday(day) ? 'bg-blue-500 text-white font-bold' : 'text-gray-700'
56
+ }`}>
57
+ {day}
58
+ </div>
59
+ <div className="space-y-0.5">
60
+ {events.slice(0, 3).map(ev => (
61
+ <EventCard key={ev.id} event={ev} compact onClick={() => model.selectEvent(ev)} />
62
+ ))}
63
+ {events.length > 3 && (
64
+ <p className="text-[10px] text-gray-400 pl-1">+{events.length - 3} more</p>
65
+ )}
66
+ </div>
67
+ </>
68
+ )}
69
+ </div>
70
+ );
71
+ })}
72
+ </div>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ );
77
+ });
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import type { CalendarModel } from './CalendarModel';
4
+
5
+ export interface WeekViewProps {
6
+ model: CalendarModel;
7
+ className?: string;
8
+ }
9
+
10
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
11
+
12
+ export const WeekView = observer<WeekViewProps>(({ model, className = '' }) => {
13
+ const { start } = model.visibleDateRange;
14
+ const days = Array.from({ length: 7 }, (_, i) => {
15
+ const d = new Date(start);
16
+ d.setDate(start.getDate() + i);
17
+ return d;
18
+ });
19
+
20
+ const now = new Date();
21
+ const isToday = (d: Date) => d.toDateString() === now.toDateString();
22
+ const currentHour = now.getHours();
23
+ const currentMinute = now.getMinutes();
24
+
25
+ // Collect all-day events for the visible week
26
+ const allDayEventsByDay = days.map(d => {
27
+ const dateKey = d.toISOString().slice(0, 10);
28
+ return (model.eventsByDay.get(dateKey) ?? []).filter(e => e.allDay);
29
+ });
30
+ const hasAllDay = allDayEventsByDay.some(events => events.length > 0);
31
+
32
+ return (
33
+ <div className={`flex flex-col h-full overflow-auto ${className}`}>
34
+ {/* Day headers */}
35
+ <div className="sticky top-0 bg-white z-10 border-b border-gray-200">
36
+ <div className="grid" style={{ gridTemplateColumns: '60px repeat(7, 1fr)' }}>
37
+ <div />
38
+ {days.map((d, i) => (
39
+ <div key={i} className={`text-center py-2 border-l border-gray-100 ${isToday(d) ? 'bg-blue-50' : ''}`}>
40
+ <div className="text-xs text-gray-500">{d.toLocaleDateString(undefined, { weekday: 'short' })}</div>
41
+ <div className={`text-lg font-medium ${isToday(d) ? 'text-blue-600' : 'text-gray-900'}`}>{d.getDate()}</div>
42
+ </div>
43
+ ))}
44
+ </div>
45
+
46
+ {/* All-day events row */}
47
+ {hasAllDay && (
48
+ <div className="grid border-t border-gray-100" style={{ gridTemplateColumns: '60px repeat(7, 1fr)' }}>
49
+ <div className="text-[10px] text-gray-400 text-right pr-2 py-1">all-day</div>
50
+ {allDayEventsByDay.map((events, i) => (
51
+ <div key={i} className="border-l border-gray-100 px-0.5 py-0.5 space-y-0.5">
52
+ {events.map(ev => (
53
+ <button
54
+ key={ev.id}
55
+ onClick={() => model.selectEvent(ev)}
56
+ className="w-full text-left text-[10px] px-1 py-0.5 rounded truncate"
57
+ style={{
58
+ backgroundColor: `${ev.color ?? '#3b82f6'}20`,
59
+ color: ev.color ?? '#3b82f6',
60
+ borderLeft: `2px solid ${ev.color ?? '#3b82f6'}`,
61
+ }}
62
+ title={ev.title}
63
+ >
64
+ {ev.title}
65
+ </button>
66
+ ))}
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )}
71
+ </div>
72
+
73
+ {/* Time grid */}
74
+ <div className="flex-1 relative">
75
+ {HOURS.map(hour => (
76
+ <div key={hour} className="grid border-b border-gray-50" style={{ gridTemplateColumns: '60px repeat(7, 1fr)', height: '48px' }}>
77
+ <div className="text-[10px] text-gray-400 text-right pr-2 -mt-2">
78
+ {hour === 0 ? '' : `${hour % 12 || 12} ${hour < 12 ? 'AM' : 'PM'}`}
79
+ </div>
80
+ {days.map((d, i) => {
81
+ const dateKey = d.toISOString().slice(0, 10);
82
+ const events = (model.eventsByDay.get(dateKey) ?? []).filter(e =>
83
+ !e.allDay && e.startDate.getHours() === hour
84
+ );
85
+ const showTimeLine = isToday(d) && hour === currentHour;
86
+ return (
87
+ <div key={i} className="border-l border-gray-100 relative">
88
+ {/* Current time indicator */}
89
+ {showTimeLine && (
90
+ <div
91
+ className="absolute left-0 right-0 z-10 pointer-events-none"
92
+ style={{ top: `${(currentMinute / 60) * 48}px` }}
93
+ >
94
+ <div className="w-2 h-2 bg-red-500 rounded-full absolute -left-1 -top-1" />
95
+ <div className="h-px bg-red-500 w-full" />
96
+ </div>
97
+ )}
98
+ {events.map((ev, ei) => {
99
+ const durationHours = Math.max(0.5, (ev.endDate.getTime() - ev.startDate.getTime()) / 3600000);
100
+ const topOffset = (ev.startDate.getMinutes() / 60) * 48;
101
+ return (
102
+ <button
103
+ key={ev.id}
104
+ onClick={() => model.selectEvent(ev)}
105
+ className="absolute text-xs px-1 py-0.5 rounded truncate z-[1]"
106
+ title={ev.title}
107
+ style={{
108
+ backgroundColor: `${ev.color ?? '#3b82f6'}20`,
109
+ color: ev.color ?? '#3b82f6',
110
+ borderLeft: `2px solid ${ev.color ?? '#3b82f6'}`,
111
+ height: `${durationHours * 48}px`,
112
+ top: `${topOffset}px`,
113
+ left: `${ei * 4 + 2}px`,
114
+ right: '2px',
115
+ }}
116
+ >
117
+ {ev.title}
118
+ </button>
119
+ );
120
+ })}
121
+ </div>
122
+ );
123
+ })}
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ );
129
+ });
@@ -0,0 +1,18 @@
1
+ // Types
2
+ export type { CalendarEvent, CalendarInfo, ICalendarProvider } from './types';
3
+
4
+ // Model
5
+ export { CalendarModel } from './CalendarModel';
6
+ export type { CalendarViewMode } from './CalendarModel';
7
+
8
+ // Components
9
+ export { CalendarBrowser, type CalendarBrowserProps } from './CalendarBrowser';
10
+ export { MonthView, type MonthViewProps } from './MonthView';
11
+ export { WeekView, type WeekViewProps } from './WeekView';
12
+ export { DayView, type DayViewProps } from './DayView';
13
+ export { AgendaView, type AgendaViewProps } from './AgendaView';
14
+ export { EventCard, type EventCardProps } from './EventCard';
15
+ export { CalendarSidebar, type CalendarSidebarProps } from './CalendarSidebar';
16
+
17
+ // Mock Provider
18
+ export { MockCalendarProvider } from './MockCalendarProvider';
@@ -0,0 +1,25 @@
1
+ import type { ObjectMetadata, IObjectProvider } from '../types-common';
2
+
3
+ export interface CalendarEvent extends ObjectMetadata {
4
+ type: 'calendar-event';
5
+ startDate: Date;
6
+ endDate: Date;
7
+ allDay: boolean;
8
+ location?: string;
9
+ attendees?: string[];
10
+ recurrence?: string;
11
+ color?: string;
12
+ calendarId: string;
13
+ }
14
+
15
+ export interface CalendarInfo {
16
+ id: string;
17
+ name: string;
18
+ color: string;
19
+ }
20
+
21
+ export interface ICalendarProvider extends IObjectProvider<CalendarEvent> {
22
+ getCalendars(): Promise<CalendarInfo[]>;
23
+ getEventsByRange(start: Date, end: Date): Promise<CalendarEvent[]>;
24
+ getEventsByCalendar(calendarId: string): Promise<CalendarEvent[]>;
25
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+
3
+ export interface ContactAvatarProps {
4
+ firstName: string;
5
+ lastName: string;
6
+ avatar?: string;
7
+ size?: 'sm' | 'md' | 'lg';
8
+ className?: string;
9
+ }
10
+
11
+ const sizeClasses = {
12
+ sm: 'w-8 h-8 text-xs',
13
+ md: 'w-10 h-10 text-sm',
14
+ lg: 'w-16 h-16 text-xl',
15
+ };
16
+
17
+ const bgColors = [
18
+ 'bg-blue-500', 'bg-green-500', 'bg-purple-500', 'bg-pink-500',
19
+ 'bg-indigo-500', 'bg-teal-500', 'bg-orange-500', 'bg-red-500',
20
+ ];
21
+
22
+ export const ContactAvatar = ({ firstName, lastName, avatar, size = 'md', className = '' }: ContactAvatarProps) => {
23
+ const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
24
+ const colorIdx = (firstName.charCodeAt(0) + lastName.charCodeAt(0)) % bgColors.length;
25
+
26
+ if (avatar) {
27
+ return <img src={avatar} alt={`${firstName} ${lastName}`} className={`${sizeClasses[size]} rounded-full object-cover ${className}`} />;
28
+ }
29
+
30
+ return (
31
+ <div className={`${sizeClasses[size]} ${bgColors[colorIdx]} rounded-full flex items-center justify-center text-white font-medium ${className}`}>
32
+ {initials}
33
+ </div>
34
+ );
35
+ };
@@ -0,0 +1,56 @@
1
+ import React, { useEffect } from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { Search, Loader2 } from 'lucide-react';
4
+ import { BrowserError } from '@anymux/ui/components/browser-error';
5
+ import type { ContactListModel } from './ContactListModel';
6
+ import { ContactList } from './ContactList';
7
+ import { ContactDetail } from './ContactDetail';
8
+ import { ContactGroupSidebar } from './ContactGroupSidebar';
9
+
10
+ export interface ContactBrowserProps {
11
+ model: ContactListModel;
12
+ className?: string;
13
+ showSidebar?: boolean;
14
+ }
15
+
16
+ export const ContactBrowser = observer<ContactBrowserProps>(({ model, className = '', showSidebar = true }) => {
17
+ useEffect(() => { model.loadContacts(); }, [model]);
18
+
19
+ return (
20
+ <div className={`flex h-full bg-white rounded-xl border border-gray-200 overflow-hidden ${className}`}>
21
+ {showSidebar && <ContactGroupSidebar model={model} />}
22
+
23
+ <div className="flex-1 flex flex-col min-w-0 border-r border-gray-200" style={{ maxWidth: '380px' }}>
24
+ {/* Search */}
25
+ <div className="px-4 py-2 border-b border-gray-200">
26
+ <div className="relative">
27
+ <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
28
+ <input
29
+ type="text"
30
+ placeholder="Search contacts..."
31
+ value={model.searchQuery}
32
+ onChange={e => model.setSearch(e.target.value)}
33
+ className="w-full pl-9 pr-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
34
+ />
35
+ </div>
36
+ </div>
37
+
38
+ {model.loading ? (
39
+ <div className="flex items-center justify-center h-64">
40
+ <Loader2 size={24} className="animate-spin text-gray-400" />
41
+ </div>
42
+ ) : model.error ? (
43
+ <BrowserError
44
+ error={model.error}
45
+ context="Contacts"
46
+ onRetry={() => model.loadContacts()}
47
+ />
48
+ ) : (
49
+ <ContactList model={model} className="flex-1" />
50
+ )}
51
+ </div>
52
+
53
+ <ContactDetail model={model} className="flex-1" />
54
+ </div>
55
+ );
56
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { Mail, Phone, Building } from 'lucide-react';
3
+ import type { ContactItem } from './types';
4
+ import { ContactAvatar } from './ContactAvatar';
5
+
6
+ export interface ContactCardProps {
7
+ contact: ContactItem;
8
+ selected?: boolean;
9
+ onClick?: () => void;
10
+ className?: string;
11
+ }
12
+
13
+ export const ContactCard = ({ contact, selected = false, onClick, className = '' }: ContactCardProps) => (
14
+ <button
15
+ onClick={onClick}
16
+ className={`flex items-center gap-3 px-4 py-3 w-full text-left transition-colors ${
17
+ selected ? 'bg-blue-50 border-l-2 border-blue-500' : 'hover:bg-gray-50 border-l-2 border-transparent'
18
+ } ${className}`}
19
+ >
20
+ <ContactAvatar firstName={contact.firstName} lastName={contact.lastName} avatar={contact.avatar} />
21
+ <div className="flex-1 min-w-0">
22
+ <p className="text-sm font-medium text-gray-900 truncate" title={`${contact.firstName} ${contact.lastName}`}>
23
+ {contact.firstName} {contact.lastName}
24
+ </p>
25
+ {contact.company && (
26
+ <p className="text-xs text-gray-500 flex items-center gap-1 truncate" title={contact.company}>
27
+ <Building size={12} />
28
+ {contact.company}
29
+ </p>
30
+ )}
31
+ </div>
32
+ <div className="flex items-center gap-1.5 text-gray-400">
33
+ {contact.email && <Mail size={14} />}
34
+ {contact.phone && <Phone size={14} />}
35
+ </div>
36
+ </button>
37
+ );
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { Mail, Phone, MapPin, Building, Calendar, Tag } from 'lucide-react';
4
+ import type { ContactListModel } from './ContactListModel';
5
+ import { ContactAvatar } from './ContactAvatar';
6
+
7
+ export interface ContactDetailProps {
8
+ model: ContactListModel;
9
+ className?: string;
10
+ }
11
+
12
+ const DetailRow = ({ icon: Icon, label, value }: { icon: React.ElementType; label: string; value: string }) => (
13
+ <div className="flex items-start gap-3 py-2">
14
+ <Icon size={16} className="text-gray-400 mt-0.5 flex-shrink-0" />
15
+ <div>
16
+ <p className="text-xs text-gray-500">{label}</p>
17
+ <p className="text-sm text-gray-900">{value}</p>
18
+ </div>
19
+ </div>
20
+ );
21
+
22
+ export const ContactDetail = observer<ContactDetailProps>(({ model, className = '' }) => {
23
+ const contact = model.selectedContact;
24
+
25
+ if (!contact) {
26
+ return (
27
+ <div className={`flex items-center justify-center h-full text-gray-400 text-sm ${className}`}>
28
+ Select a contact to view details
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className={`p-6 overflow-y-auto ${className}`}>
35
+ <div className="flex flex-col items-center mb-6">
36
+ <ContactAvatar firstName={contact.firstName} lastName={contact.lastName} avatar={contact.avatar} size="lg" />
37
+ <h2 className="mt-3 text-lg font-semibold text-gray-900">{contact.firstName} {contact.lastName}</h2>
38
+ {contact.company && <p className="text-sm text-gray-500">{contact.company}</p>}
39
+ </div>
40
+
41
+ <div className="divide-y divide-gray-100">
42
+ {contact.email && <DetailRow icon={Mail} label="Email" value={contact.email} />}
43
+ {contact.phone && <DetailRow icon={Phone} label="Phone" value={contact.phone} />}
44
+ {contact.company && <DetailRow icon={Building} label="Company" value={contact.company} />}
45
+ {contact.address && <DetailRow icon={MapPin} label="Address" value={contact.address} />}
46
+ {contact.birthday && <DetailRow icon={Calendar} label="Birthday" value={contact.birthday.toLocaleDateString()} />}
47
+ {contact.groups && contact.groups.length > 0 && (
48
+ <div className="flex items-start gap-3 py-2">
49
+ <Tag size={16} className="text-gray-400 mt-0.5 flex-shrink-0" />
50
+ <div>
51
+ <p className="text-xs text-gray-500">Groups</p>
52
+ <div className="flex flex-wrap gap-1 mt-1">
53
+ {contact.groups.map(g => (
54
+ <span key={g} className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">{g}</span>
55
+ ))}
56
+ </div>
57
+ </div>
58
+ </div>
59
+ )}
60
+ </div>
61
+ </div>
62
+ );
63
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import { Users, User } from 'lucide-react';
4
+ import type { ContactListModel } from './ContactListModel';
5
+
6
+ export interface ContactGroupSidebarProps {
7
+ model: ContactListModel;
8
+ className?: string;
9
+ }
10
+
11
+ export const ContactGroupSidebar = observer<ContactGroupSidebarProps>(({ model, className = '' }) => (
12
+ <div className={`w-56 border-r border-gray-200 bg-gray-50 overflow-y-auto ${className}`}>
13
+ <div className="p-3">
14
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Groups</h3>
15
+ <button
16
+ onClick={() => model.setGroup(null)}
17
+ className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm transition-colors ${
18
+ model.currentGroup === null ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'
19
+ }`}
20
+ >
21
+ <Users size={16} />
22
+ <span>All Contacts</span>
23
+ <span className="ml-auto text-xs text-gray-400">{model.contacts.length}</span>
24
+ </button>
25
+ {model.groups.map(group => (
26
+ <button
27
+ key={group.id}
28
+ onClick={() => model.setGroup(group.id)}
29
+ className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm transition-colors ${
30
+ model.currentGroup === group.id ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'
31
+ }`}
32
+ >
33
+ <User size={16} />
34
+ <span className="truncate" title={group.name}>{group.name}</span>
35
+ <span className="ml-auto text-xs text-gray-400">{group.count}</span>
36
+ </button>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ ));
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react-lite';
3
+ import type { ContactListModel } from './ContactListModel';
4
+ import { ContactCard } from './ContactCard';
5
+
6
+ export interface ContactListProps {
7
+ model: ContactListModel;
8
+ className?: string;
9
+ }
10
+
11
+ export const ContactList = observer<ContactListProps>(({ model, className = '' }) => (
12
+ <div className={`overflow-y-auto ${className}`}>
13
+ {Array.from(model.groupedByLetter.entries()).map(([letter, contacts]) => (
14
+ <div key={letter}>
15
+ <div className="sticky top-0 bg-gray-50/90 backdrop-blur-sm px-4 py-1 border-b border-gray-100">
16
+ <span className="text-xs font-semibold text-gray-500">{letter}</span>
17
+ </div>
18
+ {contacts.map(contact => (
19
+ <ContactCard
20
+ key={contact.id}
21
+ contact={contact}
22
+ selected={model.selectedContact?.id === contact.id}
23
+ onClick={() => model.selectContact(contact)}
24
+ />
25
+ ))}
26
+ </div>
27
+ ))}
28
+ {model.filteredContacts.length === 0 && (
29
+ <div className="flex items-center justify-center h-32 text-gray-400 text-sm">No contacts found</div>
30
+ )}
31
+ </div>
32
+ ));
@@ -0,0 +1,120 @@
1
+ import { makeAutoObservable, runInAction } from 'mobx';
2
+ import type { IContactProvider, ContactItem, ContactGroup } from './types';
3
+
4
+ export type ContactSortBy = 'firstName' | 'lastName' | 'company';
5
+
6
+ export class ContactListModel {
7
+ contacts: ContactItem[] = [];
8
+ selectedContact: ContactItem | null = null;
9
+ groups: ContactGroup[] = [];
10
+ currentGroup: string | null = null;
11
+ searchQuery = '';
12
+ sortBy: ContactSortBy = 'lastName';
13
+ loading = false;
14
+ error: string | null = null;
15
+
16
+ constructor(private provider: IContactProvider) {
17
+ makeAutoObservable(this);
18
+ }
19
+
20
+ get filteredContacts(): ContactItem[] {
21
+ let result = this.contacts;
22
+ if (this.searchQuery) {
23
+ const q = this.searchQuery.toLowerCase();
24
+ result = result.filter(c =>
25
+ c.firstName.toLowerCase().includes(q) ||
26
+ c.lastName.toLowerCase().includes(q) ||
27
+ c.email?.toLowerCase().includes(q) ||
28
+ c.company?.toLowerCase().includes(q)
29
+ );
30
+ }
31
+ return this.sortContacts(result);
32
+ }
33
+
34
+ get groupedByLetter(): Map<string, ContactItem[]> {
35
+ const groups = new Map<string, ContactItem[]>();
36
+ for (const contact of this.filteredContacts) {
37
+ const letter = (this.sortBy === 'firstName' ? contact.firstName : contact.lastName)
38
+ .charAt(0).toUpperCase();
39
+ const group = groups.get(letter) ?? [];
40
+ group.push(contact);
41
+ groups.set(letter, group);
42
+ }
43
+ return groups;
44
+ }
45
+
46
+ get groupedByGroup(): Map<string, ContactItem[]> {
47
+ const groups = new Map<string, ContactItem[]>();
48
+ for (const contact of this.filteredContacts) {
49
+ for (const groupName of contact.groups ?? ['Ungrouped']) {
50
+ const group = groups.get(groupName) ?? [];
51
+ group.push(contact);
52
+ groups.set(groupName, group);
53
+ }
54
+ }
55
+ return groups;
56
+ }
57
+
58
+ async loadContacts() {
59
+ this.loading = true;
60
+ this.error = null;
61
+ try {
62
+ const [contacts, groups] = await Promise.all([
63
+ this.currentGroup
64
+ ? this.provider.getByGroup(this.currentGroup)
65
+ : this.provider.listItems(),
66
+ this.provider.getGroups()
67
+ ]);
68
+ runInAction(() => {
69
+ this.contacts = contacts;
70
+ this.groups = groups;
71
+ });
72
+ } catch (err: any) {
73
+ runInAction(() => { this.error = err?.message || 'Failed to load contacts'; });
74
+ } finally {
75
+ runInAction(() => { this.loading = false; });
76
+ }
77
+ }
78
+
79
+ selectContact(contact: ContactItem | null) {
80
+ this.selectedContact = contact;
81
+ }
82
+
83
+ setGroup(groupId: string | null) {
84
+ this.currentGroup = groupId;
85
+ this.loadContacts();
86
+ }
87
+
88
+ setSearch(query: string) {
89
+ this.searchQuery = query;
90
+ }
91
+
92
+ setSort(sortBy: ContactSortBy) {
93
+ this.sortBy = sortBy;
94
+ }
95
+
96
+ async addContact(contact: Omit<ContactItem, 'id' | 'createdAt' | 'updatedAt'>) {
97
+ const created = await this.provider.createItem(contact);
98
+ runInAction(() => { this.contacts.push(created); });
99
+ return created;
100
+ }
101
+
102
+ async deleteContact(id: string) {
103
+ await this.provider.deleteItem(id);
104
+ runInAction(() => {
105
+ this.contacts = this.contacts.filter(c => c.id !== id);
106
+ if (this.selectedContact?.id === id) this.selectedContact = null;
107
+ });
108
+ }
109
+
110
+ private sortContacts(contacts: ContactItem[]): ContactItem[] {
111
+ return [...contacts].sort((a, b) => {
112
+ switch (this.sortBy) {
113
+ case 'firstName': return a.firstName.localeCompare(b.firstName);
114
+ case 'lastName': return a.lastName.localeCompare(b.lastName);
115
+ case 'company': return (a.company ?? '').localeCompare(b.company ?? '');
116
+ default: return 0;
117
+ }
118
+ });
119
+ }
120
+ }