@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.
- package/dist/{calendar-DSlrbHoj.js → calendar-DQKfYSQS.js} +48 -45
- package/dist/calendar-DQKfYSQS.js.map +1 -0
- package/dist/calendar.d.ts +1 -1
- package/dist/calendar.js +1 -1
- package/dist/{contacts-DQXTZzHc.js → contacts-By9Wg3kn.js} +35 -33
- package/dist/contacts-By9Wg3kn.js.map +1 -0
- package/dist/contacts.d.ts +1 -1
- package/dist/contacts.js +1 -1
- package/dist/{file-browser-m5atC3kF.js → file-browser-CkhNwADU.js} +61 -133
- package/dist/file-browser-CkhNwADU.js.map +1 -0
- package/dist/file-browser.d.ts +6 -6
- package/dist/file-browser.js +4 -4
- package/dist/{git-B55e6LL-.js → git-m4lboTfx.js} +29 -29
- package/dist/git-m4lboTfx.js.map +1 -0
- package/dist/git.js +1 -1
- package/dist/{iconMap-V4B8P-Uh.js → iconMap-DDpe35ek.js} +5 -5
- package/dist/iconMap-DDpe35ek.js.map +1 -0
- package/dist/icons.js +1 -1
- package/dist/{index-Bryv_GCG.d.ts → index-BP4IYXiF.d.ts} +46 -53
- package/dist/index-BP4IYXiF.d.ts.map +1 -0
- package/dist/{index-kHr9udZD.d.ts → index-BkIh8oov.d.ts} +17 -17
- package/dist/{index-kHr9udZD.d.ts.map → index-BkIh8oov.d.ts.map} +1 -1
- package/dist/{index-DSu19mq0.d.ts → index-D3Ob3aXg.d.ts} +9 -9
- package/dist/{index-DSu19mq0.d.ts.map → index-D3Ob3aXg.d.ts.map} +1 -1
- package/dist/{index-Ml_SgiKa.d.ts → index-DGoLQBX6.d.ts} +18 -42
- package/dist/index-DGoLQBX6.d.ts.map +1 -0
- package/dist/index-DnJaZr08.d.ts +67 -0
- package/dist/index-DnJaZr08.d.ts.map +1 -0
- package/dist/{index-DmsyeHFr.d.ts → index-Pty-N7-g.d.ts} +5 -5
- package/dist/{index-DmsyeHFr.d.ts.map → index-Pty-N7-g.d.ts.map} +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +10 -10
- package/dist/layout-BYsc16hD.js +183 -0
- package/dist/layout-BYsc16hD.js.map +1 -0
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +2 -2
- package/dist/{list-CxfT6hix.js → list-DAq-b6RR.js} +49 -63
- package/dist/list-DAq-b6RR.js.map +1 -0
- package/dist/list.d.ts +2 -2
- package/dist/list.js +4 -3
- package/dist/{media-DZ292aKK.js → media-DuczOGsk.js} +32 -31
- package/dist/media-DuczOGsk.js.map +1 -0
- package/dist/media.js +1 -1
- package/dist/{tree-Dd9Z0Aso.js → tree-B9VQcKBp.js} +2 -2
- package/dist/{tree-Dd9Z0Aso.js.map → tree-B9VQcKBp.js.map} +1 -1
- package/dist/tree.d.ts +1 -1
- package/dist/tree.js +2 -2
- package/package.json +2 -2
- package/src/calendar/AgendaView.tsx +2 -2
- package/src/calendar/CalendarBrowser.tsx +11 -11
- package/src/calendar/CalendarSidebar.tsx +10 -10
- package/src/calendar/DayView.tsx +5 -5
- package/src/calendar/EventCard.tsx +3 -3
- package/src/calendar/MonthView.tsx +6 -6
- package/src/calendar/WeekView.tsx +10 -10
- package/src/contacts/ContactBrowser.tsx +8 -8
- package/src/contacts/ContactCard.tsx +4 -4
- package/src/contacts/ContactDetail.tsx +10 -10
- package/src/contacts/ContactGroupSidebar.tsx +6 -6
- package/src/contacts/ContactList.tsx +3 -3
- package/src/file-browser/components/FileBrowser.tsx +3 -2
- package/src/file-browser/components/FileBrowserContent.tsx +1 -1
- package/src/file-browser/examples/BasicUsage.tsx +2 -2
- package/src/file-browser/index.ts +1 -1
- package/src/file-browser/providers/FileSystemProvider.ts +1 -1
- package/src/git/BranchList.tsx +12 -12
- package/src/git/CommitList.tsx +11 -11
- package/src/git/DiffViewer.tsx +11 -11
- package/src/icons/iconMap.ts +4 -4
- package/src/layout/index.ts +6 -2
- package/src/layout/models/ResponsiveLayoutModel.ts +116 -0
- package/src/list/components/ListItem.tsx +1 -1
- package/src/list/index.ts +1 -1
- package/src/media/AlbumSidebar.tsx +4 -4
- package/src/media/MediaBrowser.tsx +11 -11
- package/src/media/MediaGrid.tsx +3 -3
- package/src/media/MediaList.tsx +6 -6
- package/src/media/MediaPreview.tsx +2 -2
- package/src/media/MediaTimeline.tsx +3 -3
- package/src/{file-browser/components/shared → shared}/ErrorBoundary.tsx +3 -3
- package/dist/calendar-DSlrbHoj.js.map +0 -1
- package/dist/contacts-DQXTZzHc.js.map +0 -1
- package/dist/file-browser-m5atC3kF.js.map +0 -1
- package/dist/git-B55e6LL-.js.map +0 -1
- package/dist/iconMap-V4B8P-Uh.js.map +0 -1
- package/dist/index-Bryv_GCG.d.ts.map +0 -1
- package/dist/index-DzfY1Tok.d.ts +0 -32
- package/dist/index-DzfY1Tok.d.ts.map +0 -1
- package/dist/index-Ml_SgiKa.d.ts.map +0 -1
- package/dist/layout-Ca_4r8ka.js +0 -89
- package/dist/layout-Ca_4r8ka.js.map +0 -1
- package/dist/list-CxfT6hix.js.map +0 -1
- package/dist/media-DZ292aKK.js.map +0 -1
- package/src/list/components/shared/ErrorBoundary.tsx +0 -123
package/src/git/BranchList.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
79
|
-
: 'hover:bg-
|
|
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-
|
|
85
|
+
<Check className="h-3.5 w-3.5 text-primary" />
|
|
86
86
|
) : (
|
|
87
|
-
<GitBranchIcon className="h-3.5 w-3.5 text-
|
|
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-
|
|
96
|
-
: 'text-
|
|
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-
|
|
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>
|
package/src/git/CommitList.tsx
CHANGED
|
@@ -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-
|
|
101
|
+
className={`group border-b border-border transition-colors ${
|
|
102
102
|
isSelected
|
|
103
|
-
? 'bg-
|
|
104
|
-
: 'hover:bg-
|
|
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-
|
|
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-
|
|
147
|
+
<span className="text-[10px] text-muted-foreground">
|
|
148
148
|
{commit.author.name}
|
|
149
149
|
</span>
|
|
150
|
-
<span className="text-[10px] text-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
176
|
+
<span className="text-muted-foreground"><{commit.author.email}></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-
|
|
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
|
);
|
package/src/git/DiffViewer.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
123
|
-
<span className="text-
|
|
122
|
+
<span className="text-muted-foreground">{entry.previousPath}</span>
|
|
123
|
+
<span className="text-muted-foreground mx-1">→</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-
|
|
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-
|
|
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-
|
|
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-
|
|
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>
|
package/src/icons/iconMap.ts
CHANGED
|
@@ -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-
|
|
31
|
-
'file-type': 'text-
|
|
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-
|
|
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-
|
|
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
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
//
|
|
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-
|
|
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 '
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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} />}
|
package/src/media/MediaGrid.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
27
|
-
{item.mediaType === 'video' ? <Play size={32} className="text-
|
|
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' && (
|
package/src/media/MediaList.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
27
|
-
model.selectedItems.has(item.id) ? 'bg-
|
|
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-
|
|
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-
|
|
39
|
-
<p className="text-xs text-
|
|
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-
|
|
44
|
-
<div className="w-48 h-48 bg-
|
|
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-
|
|
18
|
-
{item.mediaType === 'video' ? <Play size={24} className="text-
|
|
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-
|
|
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 '
|
|
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('
|
|
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
|
|
88
|
+
{error?.message || 'An unexpected error occurred.'}
|
|
89
89
|
</p>
|
|
90
90
|
|
|
91
91
|
<div className="flex gap-2">
|