@anymux/ui-kit 0.1.0 → 0.2.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 (94) hide show
  1. package/dist/{calendar-DSlrbHoj.js → calendar-DQKfYSQS.js} +48 -45
  2. package/dist/calendar-DQKfYSQS.js.map +1 -0
  3. package/dist/calendar.d.ts +1 -1
  4. package/dist/calendar.js +1 -1
  5. package/dist/{contacts-DQXTZzHc.js → contacts-By9Wg3kn.js} +35 -33
  6. package/dist/contacts-By9Wg3kn.js.map +1 -0
  7. package/dist/contacts.d.ts +1 -1
  8. package/dist/contacts.js +1 -1
  9. package/dist/{file-browser-m5atC3kF.js → file-browser-CkhNwADU.js} +61 -133
  10. package/dist/file-browser-CkhNwADU.js.map +1 -0
  11. package/dist/file-browser.d.ts +6 -6
  12. package/dist/file-browser.js +4 -4
  13. package/dist/{git-B55e6LL-.js → git-m4lboTfx.js} +29 -29
  14. package/dist/git-m4lboTfx.js.map +1 -0
  15. package/dist/git.js +1 -1
  16. package/dist/{iconMap-V4B8P-Uh.js → iconMap-DDpe35ek.js} +5 -5
  17. package/dist/iconMap-DDpe35ek.js.map +1 -0
  18. package/dist/icons.js +1 -1
  19. package/dist/{index-Bryv_GCG.d.ts → index-BP4IYXiF.d.ts} +46 -53
  20. package/dist/index-BP4IYXiF.d.ts.map +1 -0
  21. package/dist/{index-kHr9udZD.d.ts → index-BkIh8oov.d.ts} +17 -17
  22. package/dist/{index-kHr9udZD.d.ts.map → index-BkIh8oov.d.ts.map} +1 -1
  23. package/dist/{index-DSu19mq0.d.ts → index-D3Ob3aXg.d.ts} +9 -9
  24. package/dist/{index-DSu19mq0.d.ts.map → index-D3Ob3aXg.d.ts.map} +1 -1
  25. package/dist/{index-Ml_SgiKa.d.ts → index-DGoLQBX6.d.ts} +18 -42
  26. package/dist/index-DGoLQBX6.d.ts.map +1 -0
  27. package/dist/index-DnJaZr08.d.ts +67 -0
  28. package/dist/index-DnJaZr08.d.ts.map +1 -0
  29. package/dist/{index-DmsyeHFr.d.ts → index-Pty-N7-g.d.ts} +5 -5
  30. package/dist/{index-DmsyeHFr.d.ts.map → index-Pty-N7-g.d.ts.map} +1 -1
  31. package/dist/index.d.ts +7 -7
  32. package/dist/index.js +10 -10
  33. package/dist/layout-BYsc16hD.js +183 -0
  34. package/dist/layout-BYsc16hD.js.map +1 -0
  35. package/dist/layout.d.ts +2 -2
  36. package/dist/layout.js +2 -2
  37. package/dist/{list-CxfT6hix.js → list-DAq-b6RR.js} +49 -63
  38. package/dist/list-DAq-b6RR.js.map +1 -0
  39. package/dist/list.d.ts +2 -2
  40. package/dist/list.js +4 -3
  41. package/dist/{media-DZ292aKK.js → media-DuczOGsk.js} +32 -31
  42. package/dist/media-DuczOGsk.js.map +1 -0
  43. package/dist/media.js +1 -1
  44. package/dist/{tree-Dd9Z0Aso.js → tree-B9VQcKBp.js} +2 -2
  45. package/dist/{tree-Dd9Z0Aso.js.map → tree-B9VQcKBp.js.map} +1 -1
  46. package/dist/tree.d.ts +1 -1
  47. package/dist/tree.js +2 -2
  48. package/package.json +2 -2
  49. package/src/calendar/AgendaView.tsx +2 -2
  50. package/src/calendar/CalendarBrowser.tsx +11 -11
  51. package/src/calendar/CalendarSidebar.tsx +10 -10
  52. package/src/calendar/DayView.tsx +5 -5
  53. package/src/calendar/EventCard.tsx +3 -3
  54. package/src/calendar/MonthView.tsx +6 -6
  55. package/src/calendar/WeekView.tsx +10 -10
  56. package/src/contacts/ContactBrowser.tsx +8 -8
  57. package/src/contacts/ContactCard.tsx +4 -4
  58. package/src/contacts/ContactDetail.tsx +10 -10
  59. package/src/contacts/ContactGroupSidebar.tsx +6 -6
  60. package/src/contacts/ContactList.tsx +3 -3
  61. package/src/file-browser/components/FileBrowser.tsx +3 -2
  62. package/src/file-browser/components/FileBrowserContent.tsx +1 -1
  63. package/src/file-browser/examples/BasicUsage.tsx +2 -2
  64. package/src/file-browser/index.ts +1 -1
  65. package/src/file-browser/providers/FileSystemProvider.ts +1 -1
  66. package/src/git/BranchList.tsx +12 -12
  67. package/src/git/CommitList.tsx +11 -11
  68. package/src/git/DiffViewer.tsx +11 -11
  69. package/src/icons/iconMap.ts +4 -4
  70. package/src/layout/index.ts +6 -2
  71. package/src/layout/models/ResponsiveLayoutModel.ts +116 -0
  72. package/src/list/components/ListItem.tsx +1 -1
  73. package/src/list/index.ts +1 -1
  74. package/src/media/AlbumSidebar.tsx +4 -4
  75. package/src/media/MediaBrowser.tsx +11 -11
  76. package/src/media/MediaGrid.tsx +3 -3
  77. package/src/media/MediaList.tsx +6 -6
  78. package/src/media/MediaPreview.tsx +2 -2
  79. package/src/media/MediaTimeline.tsx +3 -3
  80. package/src/{file-browser/components/shared → shared}/ErrorBoundary.tsx +3 -3
  81. package/dist/calendar-DSlrbHoj.js.map +0 -1
  82. package/dist/contacts-DQXTZzHc.js.map +0 -1
  83. package/dist/file-browser-m5atC3kF.js.map +0 -1
  84. package/dist/git-B55e6LL-.js.map +0 -1
  85. package/dist/iconMap-V4B8P-Uh.js.map +0 -1
  86. package/dist/index-Bryv_GCG.d.ts.map +0 -1
  87. package/dist/index-DzfY1Tok.d.ts +0 -32
  88. package/dist/index-DzfY1Tok.d.ts.map +0 -1
  89. package/dist/index-Ml_SgiKa.d.ts.map +0 -1
  90. package/dist/layout-Ca_4r8ka.js +0 -89
  91. package/dist/layout-Ca_4r8ka.js.map +0 -1
  92. package/dist/list-CxfT6hix.js.map +0 -1
  93. package/dist/media-DZ292aKK.js.map +0 -1
  94. package/src/list/components/shared/ErrorBoundary.tsx +0 -123
@@ -46,15 +46,15 @@ export const BranchList: React.FC<BranchListProps> = ({
46
46
  return (
47
47
  <div className={`flex flex-col h-full ${className ?? ''}`}>
48
48
  {/* Search input */}
49
- <div className="px-2 py-2 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
49
+ <div className="px-2 py-2 border-b border-border flex-shrink-0">
50
50
  <div className="relative">
51
- <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-gray-400" />
51
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
52
52
  <input
53
53
  type="text"
54
54
  placeholder="Filter branches..."
55
55
  value={search}
56
56
  onChange={(e) => setSearch(e.target.value)}
57
- className="w-full pl-7 pr-2 py-1.5 text-xs rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-gray-400"
57
+ className="w-full pl-7 pr-2 py-1.5 text-xs rounded-md border border-border bg-muted/50 focus:outline-none focus:ring-1 focus:ring-ring placeholder-muted-foreground"
58
58
  />
59
59
  </div>
60
60
  </div>
@@ -62,7 +62,7 @@ export const BranchList: React.FC<BranchListProps> = ({
62
62
  {/* Branch list */}
63
63
  <div className="flex-1 overflow-auto">
64
64
  {sorted.length === 0 ? (
65
- <div className="px-3 py-6 text-center text-xs text-gray-400">
65
+ <div className="px-3 py-6 text-center text-xs text-muted-foreground">
66
66
  {search ? 'No matching branches' : 'No branches found'}
67
67
  </div>
68
68
  ) : (
@@ -73,18 +73,18 @@ export const BranchList: React.FC<BranchListProps> = ({
73
73
  <button
74
74
  key={branch.name}
75
75
  onClick={() => onSelectBranch?.(branch)}
76
- className={`flex items-center gap-2 w-full px-3 py-2 text-left transition-colors border-b border-gray-50 dark:border-gray-800/50 ${
76
+ className={`flex items-center gap-2 w-full px-3 py-2 text-left transition-colors border-b border-border ${
77
77
  isCurrent
78
- ? 'bg-blue-50 dark:bg-blue-900/20'
79
- : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
78
+ ? 'bg-primary/10'
79
+ : 'hover:bg-muted/50'
80
80
  }`}
81
81
  >
82
82
  {/* Current indicator */}
83
83
  <span className="w-4 flex-shrink-0 flex items-center justify-center">
84
84
  {isCurrent ? (
85
- <Check className="h-3.5 w-3.5 text-blue-500" />
85
+ <Check className="h-3.5 w-3.5 text-primary" />
86
86
  ) : (
87
- <GitBranchIcon className="h-3.5 w-3.5 text-gray-400" />
87
+ <GitBranchIcon className="h-3.5 w-3.5 text-muted-foreground" />
88
88
  )}
89
89
  </span>
90
90
 
@@ -92,8 +92,8 @@ export const BranchList: React.FC<BranchListProps> = ({
92
92
  <span
93
93
  className={`text-xs truncate flex-1 ${
94
94
  isCurrent
95
- ? 'font-semibold text-blue-700 dark:text-blue-300'
96
- : 'text-gray-700 dark:text-gray-300'
95
+ ? 'font-semibold text-primary'
96
+ : 'text-foreground'
97
97
  }`}
98
98
  >
99
99
  {branch.name}
@@ -115,7 +115,7 @@ export const BranchList: React.FC<BranchListProps> = ({
115
115
  </div>
116
116
 
117
117
  {/* Short SHA */}
118
- <code className="text-[10px] font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
118
+ <code className="text-[10px] font-mono text-muted-foreground flex-shrink-0">
119
119
  {branch.sha.slice(0, 7)}
120
120
  </code>
121
121
  </button>
@@ -98,10 +98,10 @@ function CommitRow({ commit, isHead, changedFileCount, isSelected, onSelect }: C
98
98
 
99
99
  return (
100
100
  <div
101
- className={`group border-b border-gray-100 dark:border-gray-800 transition-colors ${
101
+ className={`group border-b border-border transition-colors ${
102
102
  isSelected
103
- ? 'bg-blue-50 dark:bg-blue-900/20'
104
- : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
103
+ ? 'bg-primary/10'
104
+ : 'hover:bg-muted/50'
105
105
  }`}
106
106
  >
107
107
  {/* Main row */}
@@ -115,7 +115,7 @@ function CommitRow({ commit, isHead, changedFileCount, isSelected, onSelect }: C
115
115
  e.stopPropagation();
116
116
  setExpanded(!expanded);
117
117
  }}
118
- className="w-4 h-4 flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 flex-shrink-0"
118
+ className="w-4 h-4 flex items-center justify-center text-muted-foreground hover:text-foreground flex-shrink-0"
119
119
  >
120
120
  {expanded ? (
121
121
  <ChevronDown className="h-3 w-3" />
@@ -144,17 +144,17 @@ function CommitRow({ commit, isHead, changedFileCount, isSelected, onSelect }: C
144
144
  )}
145
145
  </div>
146
146
  <div className="flex items-center gap-2 mt-0.5">
147
- <span className="text-[10px] text-gray-500 dark:text-gray-400">
147
+ <span className="text-[10px] text-muted-foreground">
148
148
  {commit.author.name}
149
149
  </span>
150
- <span className="text-[10px] text-gray-400 dark:text-gray-500">
150
+ <span className="text-[10px] text-muted-foreground">
151
151
  {formatRelativeTime(commit.author.date)}
152
152
  </span>
153
153
  </div>
154
154
  </div>
155
155
 
156
156
  {/* SHA badge */}
157
- <code className="text-[10px] font-mono text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded flex-shrink-0">
157
+ <code className="text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded flex-shrink-0">
158
158
  {shortSha(commit.sha)}
159
159
  </code>
160
160
  </button>
@@ -164,16 +164,16 @@ function CommitRow({ commit, isHead, changedFileCount, isSelected, onSelect }: C
164
164
  <div className="px-3 pb-3 pl-[52px] space-y-2">
165
165
  {/* Full commit message */}
166
166
  {hasFullBody && (
167
- <div className="text-xs text-gray-600 dark:text-gray-300 whitespace-pre-wrap bg-gray-50 dark:bg-gray-800/50 rounded p-2 border border-gray-100 dark:border-gray-700">
167
+ <div className="text-xs text-muted-foreground whitespace-pre-wrap bg-muted/50 rounded p-2 border border-border">
168
168
  {commit.message}
169
169
  </div>
170
170
  )}
171
171
 
172
- <div className="flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-gray-500 dark:text-gray-400">
172
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-muted-foreground">
173
173
  <div className="flex items-center gap-1">
174
174
  <User className="h-3 w-3" />
175
175
  <span>{commit.author.name}</span>
176
- <span className="text-gray-400 dark:text-gray-500">&lt;{commit.author.email}&gt;</span>
176
+ <span className="text-muted-foreground">&lt;{commit.author.email}&gt;</span>
177
177
  </div>
178
178
  <div className="flex items-center gap-1">
179
179
  <Clock className="h-3 w-3" />
@@ -224,7 +224,7 @@ export const CommitList: React.FC<CommitListProps> = ({
224
224
 
225
225
  if (commits.length === 0) {
226
226
  return (
227
- <div className={`flex items-center justify-center py-8 text-xs text-gray-400 ${className ?? ''}`}>
227
+ <div className={`flex items-center justify-center py-8 text-xs text-muted-foreground ${className ?? ''}`}>
228
228
  No commits found
229
229
  </div>
230
230
  );
@@ -23,7 +23,7 @@ function statusIcon(status: GitDiffEntry['status']) {
23
23
  case 'modified':
24
24
  return <FileEdit className="h-3.5 w-3.5 text-yellow-500" />;
25
25
  case 'renamed':
26
- return <ArrowRightLeft className="h-3.5 w-3.5 text-blue-500" />;
26
+ return <ArrowRightLeft className="h-3.5 w-3.5 text-primary" />;
27
27
  }
28
28
  }
29
29
 
@@ -77,7 +77,7 @@ function lineClassName(type: PatchLine['type']): string {
77
77
  case 'header':
78
78
  return 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium';
79
79
  default:
80
- return 'text-gray-700 dark:text-gray-300';
80
+ return 'text-foreground';
81
81
  }
82
82
  }
83
83
 
@@ -100,13 +100,13 @@ function FileDiffSection({ entry, defaultExpanded = false }: FileDiffSectionProp
100
100
  const patchLines = entry.patch ? parsePatch(entry.patch) : [];
101
101
 
102
102
  return (
103
- <div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
103
+ <div className="border border-border rounded-lg overflow-hidden">
104
104
  {/* File header */}
105
105
  <button
106
106
  onClick={() => setExpanded(!expanded)}
107
- className="flex items-center gap-2 w-full px-3 py-2 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors text-left"
107
+ className="flex items-center gap-2 w-full px-3 py-2 bg-muted/50 hover:bg-muted transition-colors text-left"
108
108
  >
109
- <span className="flex-shrink-0 text-gray-400">
109
+ <span className="flex-shrink-0 text-muted-foreground">
110
110
  {expanded ? (
111
111
  <ChevronDown className="h-3.5 w-3.5" />
112
112
  ) : (
@@ -119,8 +119,8 @@ function FileDiffSection({ entry, defaultExpanded = false }: FileDiffSectionProp
119
119
  <span className="text-xs font-mono truncate flex-1">
120
120
  {entry.previousPath && entry.status === 'renamed' ? (
121
121
  <>
122
- <span className="text-gray-400">{entry.previousPath}</span>
123
- <span className="text-gray-500 mx-1">&rarr;</span>
122
+ <span className="text-muted-foreground">{entry.previousPath}</span>
123
+ <span className="text-muted-foreground mx-1">&rarr;</span>
124
124
  <span>{entry.path}</span>
125
125
  </>
126
126
  ) : (
@@ -163,7 +163,7 @@ function FileDiffSection({ entry, defaultExpanded = false }: FileDiffSectionProp
163
163
  ))}
164
164
  </pre>
165
165
  ) : (
166
- <div className="px-3 py-4 text-center text-xs text-gray-400 italic">
166
+ <div className="px-3 py-4 text-center text-xs text-muted-foreground italic">
167
167
  No diff content available
168
168
  </div>
169
169
  )}
@@ -178,7 +178,7 @@ function FileDiffSection({ entry, defaultExpanded = false }: FileDiffSectionProp
178
178
  export const DiffViewer: React.FC<DiffViewerProps> = ({ entries, className }) => {
179
179
  if (entries.length === 0) {
180
180
  return (
181
- <div className={`flex items-center justify-center py-8 text-xs text-gray-400 ${className ?? ''}`}>
181
+ <div className={`flex items-center justify-center py-8 text-xs text-muted-foreground ${className ?? ''}`}>
182
182
  No changes to display
183
183
  </div>
184
184
  );
@@ -190,9 +190,9 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ entries, className }) =>
190
190
  return (
191
191
  <div className={`space-y-2 ${className ?? ''}`}>
192
192
  {/* Summary bar */}
193
- <div className="flex items-center gap-3 px-3 py-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs text-gray-600 dark:text-gray-300">
193
+ <div className="flex items-center gap-3 px-3 py-2 bg-muted/50 rounded-lg text-xs text-muted-foreground">
194
194
  <div className="flex items-center gap-1">
195
- <FileText className="h-3.5 w-3.5 text-gray-400" />
195
+ <FileText className="h-3.5 w-3.5 text-muted-foreground" />
196
196
  <span className="font-medium">{entries.length}</span>
197
197
  <span>file{entries.length !== 1 ? 's' : ''} changed</span>
198
198
  </div>
@@ -27,8 +27,8 @@ export const iconColorMap: Record<string, string> = {
27
27
  'file-video': 'text-orange-500',
28
28
  'file-audio': 'text-violet-500',
29
29
  'file-code': 'text-blue-500',
30
- 'file-text': 'text-gray-500',
31
- 'file-type': 'text-gray-500',
30
+ 'file-text': 'text-muted-foreground',
31
+ 'file-type': 'text-muted-foreground',
32
32
  'file-json': 'text-yellow-500',
33
33
  'file-terminal': 'text-green-500',
34
34
  'file-spreadsheet': 'text-emerald-600',
@@ -37,7 +37,7 @@ export const iconColorMap: Record<string, string> = {
37
37
  'database': 'text-purple-500',
38
38
  'globe': 'text-blue-400',
39
39
  'presentation': 'text-orange-500',
40
- 'file': 'text-gray-400',
40
+ 'file': 'text-muted-foreground',
41
41
  };
42
42
 
43
43
  // Comprehensive file extension to icon name mapping
@@ -140,7 +140,7 @@ export const contextMenuIconMap: Record<string, LucideIcon> = {
140
140
  export function resolveIcon(name: string, className?: string): React.ReactElement {
141
141
  const iconName = name || 'file';
142
142
  const MappedIcon = lucideIconMap[iconName] || File;
143
- const colorClass = iconColorMap[iconName] || 'text-gray-400';
143
+ const colorClass = iconColorMap[iconName] || 'text-muted-foreground';
144
144
  const finalClassName = className ? `${className} ${colorClass}` : `w-4 h-4 ${colorClass}`;
145
145
  return React.createElement(MappedIcon, { className: finalClassName });
146
146
  }
@@ -1,6 +1,10 @@
1
- // AICODE-NOTE: Main exports for EditorLayout module
1
+ // Layout components
2
2
  export { ExplorerLayout } from './components/ExplorerLayout/ExplorerLayout';
3
3
  export type { ExplorerLayoutProps, ExplorerSections } from './components/ExplorerLayout/ExplorerLayout';
4
4
 
5
+ // Responsive layout model
6
+ export { ResponsiveLayoutModel, getResponsiveLayout } from './models/ResponsiveLayoutModel';
7
+ export type { Breakpoint } from './models/ResponsiveLayoutModel';
8
+
5
9
  // Examples
6
- export { SimpleExample as ExplorerLayoutSimpleExample } from './examples/SimpleExample';
10
+ export { SimpleExample as ExplorerLayoutSimpleExample } from './examples/SimpleExample';
@@ -0,0 +1,116 @@
1
+ import { makeAutoObservable, action } from 'mobx';
2
+
3
+ export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
4
+
5
+ const BREAKPOINTS: Record<Breakpoint, number> = {
6
+ xs: 0,
7
+ sm: 640,
8
+ md: 768,
9
+ lg: 1024,
10
+ xl: 1280,
11
+ '2xl': 1536,
12
+ };
13
+
14
+ /**
15
+ * MobX model that tracks viewport breakpoints via matchMedia.
16
+ * Shared across all ui-kit components for consistent responsive behavior.
17
+ *
18
+ * Usage:
19
+ * const responsive = new ResponsiveLayoutModel();
20
+ * responsive.attach(); // start listening
21
+ * // ... use responsive.isMobile, responsive.breakpoint, etc.
22
+ * responsive.detach(); // stop listening
23
+ */
24
+ export class ResponsiveLayoutModel {
25
+ breakpoint: Breakpoint = 'lg';
26
+ width = typeof window !== 'undefined' ? window.innerWidth : 1024;
27
+
28
+ private queries = new Map<string, MediaQueryList>();
29
+ private cleanups: Array<() => void> = [];
30
+
31
+ constructor() {
32
+ makeAutoObservable(this, {
33
+ attach: false,
34
+ detach: false,
35
+ });
36
+ }
37
+
38
+ get isMobile(): boolean {
39
+ return this.width < BREAKPOINTS.md;
40
+ }
41
+
42
+ get isTablet(): boolean {
43
+ return this.width >= BREAKPOINTS.md && this.width < BREAKPOINTS.lg;
44
+ }
45
+
46
+ get isDesktop(): boolean {
47
+ return this.width >= BREAKPOINTS.lg;
48
+ }
49
+
50
+ /** True if viewport is at or above the given breakpoint */
51
+ isAtLeast(bp: Breakpoint): boolean {
52
+ return this.width >= BREAKPOINTS[bp];
53
+ }
54
+
55
+ /** Suggested column count for grid layouts */
56
+ get gridColumns(): number {
57
+ if (this.width < BREAKPOINTS.sm) return 1;
58
+ if (this.width < BREAKPOINTS.md) return 2;
59
+ if (this.width < BREAKPOINTS.lg) return 4;
60
+ if (this.width < BREAKPOINTS.xl) return 6;
61
+ return 8;
62
+ }
63
+
64
+ /** Start listening for viewport changes */
65
+ attach(): void {
66
+ if (typeof window === 'undefined') return;
67
+
68
+ const entries = Object.entries(BREAKPOINTS) as Array<[Breakpoint, number]>;
69
+ // Sort descending so we match the largest breakpoint first
70
+ entries.sort((a, b) => b[1] - a[1]);
71
+
72
+ for (const [bp, minWidth] of entries) {
73
+ const mql = window.matchMedia(`(min-width: ${minWidth}px)`);
74
+ this.queries.set(bp, mql);
75
+
76
+ const handler = action(() => {
77
+ this.width = window.innerWidth;
78
+ this.breakpoint = this.computeBreakpoint();
79
+ });
80
+
81
+ mql.addEventListener('change', handler);
82
+ this.cleanups.push(() => mql.removeEventListener('change', handler));
83
+ }
84
+
85
+ // Set initial values
86
+ this.width = window.innerWidth;
87
+ this.breakpoint = this.computeBreakpoint();
88
+ }
89
+
90
+ /** Stop listening */
91
+ detach(): void {
92
+ this.cleanups.forEach((fn) => fn());
93
+ this.cleanups = [];
94
+ this.queries.clear();
95
+ }
96
+
97
+ private computeBreakpoint(): Breakpoint {
98
+ const entries = Object.entries(BREAKPOINTS) as Array<[Breakpoint, number]>;
99
+ entries.sort((a, b) => b[1] - a[1]);
100
+ for (const [bp, minWidth] of entries) {
101
+ if (this.width >= minWidth) return bp;
102
+ }
103
+ return 'xs';
104
+ }
105
+ }
106
+
107
+ /** Singleton for app-wide responsive state */
108
+ let _instance: ResponsiveLayoutModel | null = null;
109
+
110
+ export function getResponsiveLayout(): ResponsiveLayoutModel {
111
+ if (!_instance) {
112
+ _instance = new ResponsiveLayoutModel();
113
+ _instance.attach();
114
+ }
115
+ return _instance;
116
+ }
@@ -200,7 +200,7 @@ const ListItemComponent = observer<ListItemProps>(({
200
200
  const resolveItemIcon = (iconName: string | undefined, sizeClass = 'w-4 h-4') => {
201
201
  const name = iconName || 'file';
202
202
  const MappedIcon = lucideIconMap[name] || File;
203
- const colorClass = iconColorMap[name] || 'text-gray-400';
203
+ const colorClass = iconColorMap[name] || 'text-muted-foreground';
204
204
  return <MappedIcon className={`${sizeClass} ${colorClass}`} />;
205
205
  };
206
206
 
package/src/list/index.ts CHANGED
@@ -14,7 +14,7 @@ export { CalculatedGridView } from './components/CalculatedGridView';
14
14
  // Shared components
15
15
  export { ListLoader } from './components/shared/ListLoader';
16
16
  export { LoadingIndicator, InlineLoading, LoadingProgress } from './components/shared/LoadingIndicator';
17
- export { ListErrorBoundary } from './components/shared/ErrorBoundary';
17
+ export { ErrorBoundary as ListErrorBoundary } from '../shared/ErrorBoundary';
18
18
  export { ErrorDisplay, NetworkError, LoadError } from './components/shared/ErrorDisplay';
19
19
  export { EmptyState, NoItems, NoSearchResults, NoSelection } from './components/shared/EmptyState';
20
20
 
@@ -18,13 +18,13 @@ export const AlbumSidebar = observer<AlbumSidebarProps>(({ model, provider, clas
18
18
  }, [provider]);
19
19
 
20
20
  return (
21
- <div className={`w-56 border-r border-gray-200 bg-gray-50 overflow-y-auto ${className}`}>
21
+ <div className={`w-56 border-r border-border bg-muted/30 overflow-y-auto ${className}`}>
22
22
  <div className="p-3">
23
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Albums</h3>
23
+ <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">Albums</h3>
24
24
  <button
25
25
  onClick={() => model.setAlbum(null)}
26
26
  className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm transition-colors ${
27
- model.currentAlbum === null ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'
27
+ model.currentAlbum === null ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-muted'
28
28
  }`}
29
29
  >
30
30
  <Image size={16} />
@@ -35,7 +35,7 @@ export const AlbumSidebar = observer<AlbumSidebarProps>(({ model, provider, clas
35
35
  key={album}
36
36
  onClick={() => model.setAlbum(album)}
37
37
  className={`flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm transition-colors ${
38
- model.currentAlbum === album ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'
38
+ model.currentAlbum === album ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-muted'
39
39
  }`}
40
40
  >
41
41
  <FolderOpen size={16} />
@@ -21,29 +21,29 @@ export const MediaBrowser = observer<MediaBrowserProps>(({ model, provider, clas
21
21
  useEffect(() => { model.loadItems(); }, [model]);
22
22
 
23
23
  return (
24
- <div className={`flex h-full bg-white rounded-xl border border-gray-200 overflow-hidden ${className}`}>
25
- {showSidebar && <AlbumSidebar model={model} provider={provider} />}
24
+ <div className={`flex h-full bg-background rounded-xl border border-border overflow-hidden ${className}`}>
25
+ {showSidebar && <AlbumSidebar model={model} provider={provider} className="hidden md:block" />}
26
26
 
27
27
  <div className="flex-1 flex flex-col min-w-0">
28
28
  {/* Toolbar */}
29
- <div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200">
30
- <div className="relative flex-1 max-w-xs">
31
- <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
29
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-border flex-wrap sm:flex-nowrap">
30
+ <div className="relative flex-1 min-w-[120px] sm:max-w-xs">
31
+ <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
32
32
  <input
33
33
  type="text"
34
34
  placeholder="Search media..."
35
35
  value={model.searchQuery}
36
36
  onChange={e => model.search(e.target.value)}
37
- 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"
37
+ className="w-full pl-9 pr-3 py-1.5 text-sm border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
38
38
  />
39
39
  </div>
40
40
 
41
- <div className="flex items-center border border-gray-200 rounded-lg overflow-hidden">
41
+ <div className="flex items-center border border-border rounded-lg overflow-hidden">
42
42
  {([['grid', Grid3X3], ['list', List], ['timeline', Clock]] as const).map(([mode, Icon]) => (
43
43
  <button
44
44
  key={mode}
45
45
  onClick={() => model.setViewMode(mode)}
46
- className={`p-1.5 ${model.viewMode === mode ? 'bg-blue-50 text-blue-600' : 'text-gray-500 hover:bg-gray-50'}`}
46
+ className={`p-1.5 ${model.viewMode === mode ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted'}`}
47
47
  >
48
48
  <Icon size={16} />
49
49
  </button>
@@ -53,7 +53,7 @@ export const MediaBrowser = observer<MediaBrowserProps>(({ model, provider, clas
53
53
  <select
54
54
  value={model.filterByType ?? ''}
55
55
  onChange={e => model.setFilter(e.target.value as 'photo' | 'video' | 'audio' || null)}
56
- className="text-sm border border-gray-200 rounded-lg px-2 py-1.5"
56
+ className="text-sm border border-border rounded-lg px-2 py-1.5 bg-background text-foreground"
57
57
  >
58
58
  <option value="">All types</option>
59
59
  <option value="photo">Photos</option>
@@ -66,7 +66,7 @@ export const MediaBrowser = observer<MediaBrowserProps>(({ model, provider, clas
66
66
  <div className="flex-1 overflow-y-auto">
67
67
  {model.loading ? (
68
68
  <div className="flex items-center justify-center h-64">
69
- <Loader2 size={24} className="animate-spin text-gray-400" />
69
+ <Loader2 size={24} className="animate-spin text-muted-foreground" />
70
70
  </div>
71
71
  ) : model.error ? (
72
72
  <BrowserError
@@ -75,7 +75,7 @@ export const MediaBrowser = observer<MediaBrowserProps>(({ model, provider, clas
75
75
  onRetry={() => model.loadItems()}
76
76
  />
77
77
  ) : model.filteredItems.length === 0 ? (
78
- <div className="flex items-center justify-center h-64 text-gray-400 text-sm">No media found</div>
78
+ <div className="flex items-center justify-center h-64 text-muted-foreground text-sm">No media found</div>
79
79
  ) : (
80
80
  <>
81
81
  {model.viewMode === 'grid' && <MediaGrid model={model} />}
@@ -13,7 +13,7 @@ const MediaThumbnail = ({ item, onClick, selected }: { item: MediaItem; onClick:
13
13
  <button
14
14
  onClick={onClick}
15
15
  className={`relative aspect-square rounded-lg overflow-hidden cursor-pointer group border-2 transition-all ${
16
- selected ? 'border-blue-500 ring-2 ring-blue-200' : 'border-transparent hover:border-gray-300'
16
+ selected ? 'border-primary ring-2 ring-ring' : 'border-transparent hover:border-border'
17
17
  }`}
18
18
  >
19
19
  {item.thumbnail || item.mediaType === 'photo' ? (
@@ -23,8 +23,8 @@ const MediaThumbnail = ({ item, onClick, selected }: { item: MediaItem; onClick:
23
23
  className="w-full h-full object-cover"
24
24
  />
25
25
  ) : (
26
- <div className="w-full h-full bg-gray-100 flex items-center justify-center">
27
- {item.mediaType === 'video' ? <Play size={32} className="text-gray-400" /> : <Music size={32} className="text-gray-400" />}
26
+ <div className="w-full h-full bg-muted flex items-center justify-center">
27
+ {item.mediaType === 'video' ? <Play size={32} className="text-muted-foreground" /> : <Music size={32} className="text-muted-foreground" />}
28
28
  </div>
29
29
  )}
30
30
  {item.mediaType === 'video' && (
@@ -18,16 +18,16 @@ const mediaIcon = (item: MediaItem) => {
18
18
  };
19
19
 
20
20
  export const MediaList = observer<MediaListProps>(({ model, className = '' }) => (
21
- <div className={`divide-y divide-gray-100 ${className}`}>
21
+ <div className={`divide-y divide-border ${className}`}>
22
22
  {model.filteredItems.map(item => (
23
23
  <button
24
24
  key={item.id}
25
25
  onClick={() => model.openPreview(item)}
26
- className={`flex items-center gap-3 px-4 py-3 w-full text-left hover:bg-gray-50 transition-colors ${
27
- model.selectedItems.has(item.id) ? 'bg-blue-50' : ''
26
+ className={`flex items-center gap-3 px-4 py-3 w-full text-left hover:bg-muted/50 transition-colors ${
27
+ model.selectedItems.has(item.id) ? 'bg-primary/10' : ''
28
28
  }`}
29
29
  >
30
- <div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 bg-gray-100 flex items-center justify-center">
30
+ <div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 bg-muted flex items-center justify-center">
31
31
  {item.thumbnail ? (
32
32
  <img src={item.thumbnail} alt={item.title} className="w-full h-full object-cover" />
33
33
  ) : (
@@ -35,8 +35,8 @@ export const MediaList = observer<MediaListProps>(({ model, className = '' }) =>
35
35
  )}
36
36
  </div>
37
37
  <div className="flex-1 min-w-0">
38
- <p className="text-sm font-medium text-gray-900 truncate" title={item.title}>{item.title}</p>
39
- <p className="text-xs text-gray-500">
38
+ <p className="text-sm font-medium text-foreground truncate" title={item.title}>{item.title}</p>
39
+ <p className="text-xs text-muted-foreground">
40
40
  {item.mediaType} {item.album && `· ${item.album}`} · {item.createdAt.toLocaleDateString()}
41
41
  </p>
42
42
  </div>
@@ -40,8 +40,8 @@ export const MediaPreview = observer<MediaPreviewProps>(({ model, className = ''
40
40
  <video src={item.url} controls className="max-w-full max-h-[80vh] rounded-lg" />
41
41
  )}
42
42
  {item.mediaType === 'audio' && (
43
- <div className="bg-gray-900 rounded-xl p-8 flex flex-col items-center gap-4">
44
- <div className="w-48 h-48 bg-gray-800 rounded-xl flex items-center justify-center text-6xl">🎵</div>
43
+ <div className="bg-background rounded-xl p-8 flex flex-col items-center gap-4">
44
+ <div className="w-48 h-48 bg-muted rounded-xl flex items-center justify-center text-6xl">🎵</div>
45
45
  <audio src={item.url} controls className="w-80" />
46
46
  </div>
47
47
  )}
@@ -14,8 +14,8 @@ const TimelineThumbnail = ({ item, onClick }: { item: MediaItem; onClick: () =>
14
14
  {item.thumbnail || item.mediaType === 'photo' ? (
15
15
  <img src={item.thumbnail ?? item.url} alt={item.title} className="w-full h-full object-cover" />
16
16
  ) : (
17
- <div className="w-full h-full bg-gray-100 flex items-center justify-center">
18
- {item.mediaType === 'video' ? <Play size={24} className="text-gray-400" /> : <Music size={24} className="text-gray-400" />}
17
+ <div className="w-full h-full bg-muted flex items-center justify-center">
18
+ {item.mediaType === 'video' ? <Play size={24} className="text-muted-foreground" /> : <Music size={24} className="text-muted-foreground" />}
19
19
  </div>
20
20
  )}
21
21
  <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
@@ -26,7 +26,7 @@ export const MediaTimeline = observer<MediaTimelineProps>(({ model, className =
26
26
  <div className={`space-y-6 p-4 ${className}`}>
27
27
  {Array.from(model.groupedByDate.entries()).map(([date, items]) => (
28
28
  <div key={date}>
29
- <h3 className="text-sm font-semibold text-gray-700 mb-2 sticky top-0 bg-white/90 backdrop-blur-sm py-1">{date}</h3>
29
+ <h3 className="text-sm font-semibold text-foreground mb-2 sticky top-0 bg-background/90 backdrop-blur-sm py-1">{date}</h3>
30
30
  <div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-1">
31
31
  {items.map(item => (
32
32
  <TimelineThumbnail key={item.id} item={item} onClick={() => model.openPreview(item)} />
@@ -1,6 +1,6 @@
1
1
  import React, { Component, ReactNode } from 'react';
2
2
  import { AlertTriangle } from 'lucide-react';
3
- import { cn } from '../../../lib/utils';
3
+ import { cn } from '../lib/utils';
4
4
 
5
5
  export interface ErrorBoundaryProps {
6
6
  children: ReactNode;
@@ -43,7 +43,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
43
43
  this.props.onError?.(error, errorInfoString);
44
44
 
45
45
  // Log error for debugging
46
- console.error('FileBrowser Error Boundary caught an error:', error);
46
+ console.error('ErrorBoundary caught an error:', error);
47
47
  console.error('Error Info:', errorInfo);
48
48
  }
49
49
 
@@ -85,7 +85,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
85
85
  </h2>
86
86
 
87
87
  <p className="text-sm text-muted-foreground mb-4 max-w-md">
88
- {error?.message || 'An unexpected error occurred while rendering the file browser.'}
88
+ {error?.message || 'An unexpected error occurred.'}
89
89
  </p>
90
90
 
91
91
  <div className="flex gap-2">