@polderlabs/bizar 5.3.1 → 5.4.1
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/bizar-dash/dist/assets/EnvVarsSection-DhLHZ5ua.js +4 -0
- package/bizar-dash/dist/assets/EnvVarsSection-DhLHZ5ua.js.map +1 -0
- package/bizar-dash/dist/assets/MobileChat-9UpvDP6-.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-OidrxXR9.js.map → MobileChat-9UpvDP6-.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-BFmN9ZWR.js.map +1 -0
- package/bizar-dash/dist/assets/Toast-BlcKl_dN.js +1 -0
- package/bizar-dash/dist/assets/Toast-BlcKl_dN.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Bd8piSb2.js → icons-CMPxNV8t.js} +73 -73
- package/bizar-dash/dist/assets/icons-CMPxNV8t.js.map +1 -0
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js +16 -0
- package/bizar-dash/dist/assets/main-Dhrvp9Gt.js.map +1 -0
- package/bizar-dash/dist/assets/{main-3G6mOhiK.css → main-O2tZVdHm.css} +1 -1
- package/bizar-dash/dist/assets/mobile-DDY1lquG.js +1 -0
- package/bizar-dash/dist/assets/mobile-DDY1lquG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-BjkoHh1W.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CsZQAswA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/useSlashCommands-eFCiWHVt.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-eFCiWHVt.js.map +1 -0
- package/bizar-dash/dist/index.html +8 -6
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/web/MobileApp.tsx +18 -20
- package/bizar-dash/src/web/components/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/components/MobileDrawer.tsx +68 -0
- package/bizar-dash/src/web/components/MobileHeader.tsx +50 -0
- package/bizar-dash/src/web/components/MobileLayout.tsx +63 -0
- package/bizar-dash/src/web/components/TaskCard.tsx +69 -0
- package/bizar-dash/src/web/components/TaskCreateSheet.tsx +76 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/MobileDoctor.tsx +80 -0
- package/bizar-dash/src/web/mobile/MobileEval.tsx +167 -0
- package/bizar-dash/src/web/mobile/MobileMarketplace.tsx +106 -0
- package/bizar-dash/src/web/mobile/MobileMemory.tsx +108 -0
- package/bizar-dash/src/web/mobile/MobileOverview.tsx +213 -0
- package/bizar-dash/src/web/mobile/MobilePlugins.tsx +71 -0
- package/bizar-dash/src/web/mobile/MobileSettings.tsx +235 -180
- package/bizar-dash/src/web/mobile/MobileTasks.tsx +94 -0
- package/bizar-dash/src/web/mobile.tsx +1 -0
- package/bizar-dash/src/web/styles/main.css +388 -0
- package/bizar-dash/src/web/styles/memory.css +45 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +233 -0
- package/bizar-dash/src/web/styles/mobile.css +180 -0
- package/bizar-dash/src/web/styles/settings.css +156 -0
- package/bizar-dash/src/web/styles/tasks.css +305 -0
- package/bizar-dash/tests/mobile-chat.test.tsx +82 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +332 -0
- package/bizar-dash/tests/mobile-misc.test.tsx +321 -0
- package/bizar-dash/tests/mobile-overview.test.tsx +214 -0
- package/bizar-dash/tests/mobile-settings.test.tsx +181 -0
- package/bizar-dash/tests/mobile-tasks.test.tsx +162 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileChat-OidrxXR9.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js.map +0 -1
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +0 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +0 -19
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js +0 -2
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js.map +0 -1
|
@@ -1,196 +1,251 @@
|
|
|
1
|
-
// src/web/mobile/MobileSettings.tsx —
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// src/web/mobile/MobileSettings.tsx — v5.4 mobile settings with full desktop parity.
|
|
2
|
+
// All 9 sections in an accordion layout with search.
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Settings2, Brain, KeyRound, Cpu, RefreshCw, Globe, Info, ChevronRight,
|
|
6
|
+
} from 'lucide-react';
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
import type { Settings, Snapshot } from '../lib/types';
|
|
9
|
+
|
|
10
|
+
// Sub-section components — each renders its section content inline.
|
|
11
|
+
import { GeneralSection } from '../views/settings/GeneralSection';
|
|
12
|
+
import { EnvVarsSection } from '../views/settings/EnvVarsSection';
|
|
13
|
+
import { MemorySection } from '../views/settings/MemorySection';
|
|
14
|
+
import { SystemLlmSection } from '../views/settings/SystemLlmSection';
|
|
15
|
+
import { UpdatesSection } from '../views/settings/UpdatesSection';
|
|
16
|
+
import { HeadroomSection } from '../views/settings/HeadroomSection';
|
|
17
|
+
import { TailscaleSettings } from '../components/TailscaleSettings';
|
|
18
|
+
import { ActivitySection } from '../views/settings/ActivitySection';
|
|
19
|
+
|
|
20
|
+
// Lazy-load heavy sections that pull in large dependencies.
|
|
21
|
+
import React from 'react';
|
|
7
22
|
|
|
8
23
|
type Props = {
|
|
9
24
|
settings: Settings;
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
/** Called when the user changes any top-level setting */
|
|
26
|
+
onSettingsChange?: <K extends keyof Settings>(key: K, value: Settings[K]) => void;
|
|
27
|
+
/** Debounced auto-save to API (key, value) */
|
|
28
|
+
autoSave?: (key: keyof Settings, value: Settings[keyof Settings]) => void;
|
|
29
|
+
/** Optional snapshot for counts / agent list */
|
|
30
|
+
snapshot?: Snapshot | null;
|
|
31
|
+
/** v5.3 compatibility — called after settings are saved (MobileApp.tsx passes refreshSnapshot) */
|
|
32
|
+
onRefresh?: () => Promise<void>;
|
|
12
33
|
};
|
|
13
34
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{ name: 'Orange', accent: '#f97316' },
|
|
25
|
-
{ name: 'Red', accent: '#ef4444' },
|
|
26
|
-
{ name: 'Pink', accent: '#ec4899' },
|
|
27
|
-
{ name: 'Cyan', accent: '#06b6d4' },
|
|
28
|
-
{ name: 'Mono', accent: '#6b7280' },
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
export function MobileSettings({ settings: initial, snapshot, onRefresh }: Props) {
|
|
32
|
-
const [settings, setSettings] = useState<Settings>(initial);
|
|
33
|
-
const [dirty, setDirty] = useState(false);
|
|
34
|
-
const [saving, setSaving] = useState(false);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
setSettings(initial);
|
|
38
|
-
setDirty(false);
|
|
39
|
-
if (initial.theme) applyThemeTokens(initial.theme);
|
|
40
|
-
}, [initial]);
|
|
41
|
-
|
|
42
|
-
const patchTheme = (patch: Partial<Settings['theme']>) => {
|
|
43
|
-
setSettings((cur) => {
|
|
44
|
-
const next = { ...cur, theme: { ...cur.theme, ...patch } };
|
|
45
|
-
applyTheme(next.theme);
|
|
46
|
-
applyThemeTokens(next.theme);
|
|
47
|
-
return next;
|
|
48
|
-
});
|
|
49
|
-
setDirty(true);
|
|
35
|
+
// ─── Section definitions ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function GeneralSettings({ settings, onSettingsChange, autoSave }: Props) {
|
|
38
|
+
const patchTop = <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
|
39
|
+
onSettingsChange?.(key, value);
|
|
40
|
+
autoSave?.(key, value);
|
|
41
|
+
};
|
|
42
|
+
const patchUi = (patch: Partial<Settings['ui']>) => {
|
|
43
|
+
onSettingsChange?.('ui', { ...settings.ui, ...patch });
|
|
44
|
+
autoSave?.('ui', { ...settings.ui, ...patch });
|
|
50
45
|
};
|
|
46
|
+
return (
|
|
47
|
+
<GeneralSection
|
|
48
|
+
settings={settings}
|
|
49
|
+
patchUi={patchUi}
|
|
50
|
+
patchTop={patchTop}
|
|
51
|
+
autoSave={autoSave}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
function ProvidersSettings({ settings, onSettingsChange, autoSave }: Props) {
|
|
57
|
+
// Providers are managed server-side; show a link to the desktop providers view.
|
|
58
|
+
// Mobile can't add/edit providers without more complex auth flow.
|
|
59
|
+
void settings; void onSettingsChange; void autoSave;
|
|
60
|
+
return (
|
|
61
|
+
<div style={{ fontSize: 13, color: 'var(--text-dim)', padding: '8px 0' }}>
|
|
62
|
+
<p style={{ marginBottom: 8 }}>
|
|
63
|
+
Provider management is available on desktop.
|
|
64
|
+
</p>
|
|
65
|
+
<a href="/?desktop=1#settings-providers" className="mobile-btn mobile-btn-secondary" style={{ display: 'inline-block', fontSize: 12, padding: '6px 12px' }}>
|
|
66
|
+
Open desktop to manage providers
|
|
67
|
+
</a>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function EnvVarsSettings() {
|
|
73
|
+
return <EnvVarsSection />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function MemorySettings() {
|
|
77
|
+
return <MemorySection />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SystemLlmSettings({ settings, onSettingsChange, autoSave }: Props) {
|
|
81
|
+
const patchTop = <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
|
82
|
+
onSettingsChange?.(key, value);
|
|
83
|
+
autoSave?.(key, value);
|
|
66
84
|
};
|
|
85
|
+
return <SystemLlmSection settings={settings} patchTop={patchTop} />;
|
|
86
|
+
}
|
|
67
87
|
|
|
88
|
+
function UpdatesSettings() {
|
|
89
|
+
return <UpdatesSection />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function HeadroomSettingsSection({ settings }: { settings: Settings }) {
|
|
93
|
+
const [localSettings, setLocalSettings] = useState(settings);
|
|
94
|
+
const [dirty, setDirty] = useState(false);
|
|
68
95
|
return (
|
|
69
|
-
<
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
<HeadroomSection
|
|
97
|
+
settings={localSettings}
|
|
98
|
+
setSettings={setLocalSettings as React.Dispatch<React.SetStateAction<Settings>>}
|
|
99
|
+
setDirty={setDirty}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function TailscaleSettingsMobile() {
|
|
105
|
+
return <TailscaleSettings />;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function AboutSection({ settings }: { settings: Settings }) {
|
|
109
|
+
const about = settings.about || { version: '—', homepage: 'https://github.com/DrB0rk/BizarHarness', license: 'MIT' };
|
|
110
|
+
return <ActivitySection about={about} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Main component ──────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export function MobileSettings({ settings, onSettingsChange, autoSave, snapshot }: Props) {
|
|
116
|
+
const [search, setSearch] = useState('');
|
|
117
|
+
const [expandedSection, setExpandedSection] = useState<string | null>(null);
|
|
118
|
+
|
|
119
|
+
const sections = [
|
|
120
|
+
{
|
|
121
|
+
id: 'general',
|
|
122
|
+
title: 'General',
|
|
123
|
+
icon: Settings2,
|
|
124
|
+
description: 'Theme, accent, layout',
|
|
125
|
+
component: <GeneralSettings settings={settings} onSettingsChange={onSettingsChange} autoSave={autoSave} />,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'providers',
|
|
129
|
+
title: 'AI Providers',
|
|
130
|
+
icon: Brain,
|
|
131
|
+
description: `${snapshot?.providers?.length ?? 0} configured`,
|
|
132
|
+
component: <ProvidersSettings settings={settings} onSettingsChange={onSettingsChange} autoSave={autoSave} />,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'env-vars',
|
|
136
|
+
title: 'Environment Variables',
|
|
137
|
+
icon: KeyRound,
|
|
138
|
+
description: 'API keys, secrets',
|
|
139
|
+
component: <EnvVarsSettings />,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'memory',
|
|
143
|
+
title: 'Memory Vault',
|
|
144
|
+
icon: Brain,
|
|
145
|
+
description: 'Git remote, LightRAG config',
|
|
146
|
+
component: <MemorySettings />,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'system-llm',
|
|
150
|
+
title: 'System LLM',
|
|
151
|
+
icon: Cpu,
|
|
152
|
+
description: settings.systemLlm?.model || 'not set',
|
|
153
|
+
component: <SystemLlmSettings settings={settings} onSettingsChange={onSettingsChange} autoSave={autoSave} />,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'updates',
|
|
157
|
+
title: 'Updates',
|
|
158
|
+
icon: RefreshCw,
|
|
159
|
+
description: 'Check for updates',
|
|
160
|
+
component: <UpdatesSettings />,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'headroom',
|
|
164
|
+
title: 'Headroom',
|
|
165
|
+
icon: Cpu,
|
|
166
|
+
description: settings.headroom?.enabled ? 'Enabled' : 'Disabled',
|
|
167
|
+
component: <HeadroomSettingsSection settings={settings} />,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'tailscale',
|
|
171
|
+
title: 'Tailscale',
|
|
172
|
+
icon: Globe,
|
|
173
|
+
description: 'Tailscale serve & auth',
|
|
174
|
+
component: <TailscaleSettingsMobile />,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'about',
|
|
178
|
+
title: 'About',
|
|
179
|
+
icon: Info,
|
|
180
|
+
description: `Bizar v${settings.about?.version || '—'}`,
|
|
181
|
+
component: <AboutSection settings={settings} />,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const filtered = sections.filter(
|
|
186
|
+
(s) =>
|
|
187
|
+
!search ||
|
|
188
|
+
s.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
189
|
+
s.description.toLowerCase().includes(search.toLowerCase()),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="mobile-settings">
|
|
194
|
+
<div className="mobile-settings-search">
|
|
195
|
+
<SearchIcon />
|
|
196
|
+
<input
|
|
197
|
+
value={search}
|
|
198
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
199
|
+
placeholder="Search settings..."
|
|
200
|
+
aria-label="Search settings"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="mobile-settings-list">
|
|
205
|
+
{filtered.map((s) => {
|
|
206
|
+
const Icon = s.icon;
|
|
207
|
+
const isExpanded = expandedSection === s.id;
|
|
208
|
+
return (
|
|
209
|
+
<div key={s.id} className={cn('mobile-settings-item', isExpanded && 'is-expanded')}>
|
|
210
|
+
<button
|
|
211
|
+
className="mobile-settings-item-head"
|
|
212
|
+
onClick={() => setExpandedSection(isExpanded ? null : s.id)}
|
|
213
|
+
aria-expanded={isExpanded}
|
|
214
|
+
aria-controls={`mobile-settings-body-${s.id}`}
|
|
215
|
+
>
|
|
216
|
+
<div className="mobile-settings-item-icon">
|
|
217
|
+
<Icon size={18} />
|
|
218
|
+
</div>
|
|
219
|
+
<div className="mobile-settings-item-text">
|
|
220
|
+
<div className="mobile-settings-item-title">{s.title}</div>
|
|
221
|
+
<div className="mobile-settings-item-desc">{s.description}</div>
|
|
222
|
+
</div>
|
|
223
|
+
<ChevronRight
|
|
224
|
+
size={16}
|
|
225
|
+
className={cn('mobile-settings-item-chevron', isExpanded && 'is-rotated')}
|
|
110
226
|
/>
|
|
111
|
-
|
|
227
|
+
</button>
|
|
228
|
+
{isExpanded && (
|
|
229
|
+
<div id={`mobile-settings-body-${s.id}`} className="mobile-settings-item-body">
|
|
230
|
+
{s.component}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
112
233
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type="range"
|
|
119
|
-
min={12}
|
|
120
|
-
max={20}
|
|
121
|
-
value={settings.theme.fontSize}
|
|
122
|
-
onChange={(e) => patchTheme({ fontSize: Number(e.target.value) })}
|
|
123
|
-
style={{ width: '100%' }}
|
|
124
|
-
/>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<div className="mobile-setting-row">
|
|
128
|
-
<span className="mobile-setting-label">Compact mode</span>
|
|
129
|
-
<label className="mobile-toggle">
|
|
130
|
-
<input
|
|
131
|
-
type="checkbox"
|
|
132
|
-
checked={settings.theme.compactMode}
|
|
133
|
-
onChange={(e) => patchTheme({ compactMode: e.target.checked })}
|
|
134
|
-
/>
|
|
135
|
-
<span className="mobile-toggle-slider" />
|
|
136
|
-
</label>
|
|
137
|
-
</div>
|
|
138
|
-
<div className="mobile-setting-row">
|
|
139
|
-
<span className="mobile-setting-label">Animations</span>
|
|
140
|
-
<label className="mobile-toggle">
|
|
141
|
-
<input
|
|
142
|
-
type="checkbox"
|
|
143
|
-
checked={settings.theme.animations}
|
|
144
|
-
onChange={(e) => patchTheme({ animations: e.target.checked })}
|
|
145
|
-
/>
|
|
146
|
-
<span className="mobile-toggle-slider" />
|
|
147
|
-
</label>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</section>
|
|
151
|
-
|
|
152
|
-
{/* Chat defaults */}
|
|
153
|
-
<section className="mobile-section">
|
|
154
|
-
<h3 className="mobile-section-title">Chat Defaults</h3>
|
|
155
|
-
<div className="mobile-card">
|
|
156
|
-
<label className="mobile-field-label">Default Agent</label>
|
|
157
|
-
<select
|
|
158
|
-
className="mobile-input"
|
|
159
|
-
value={settings.defaultAgent || 'odin'}
|
|
160
|
-
onChange={(e) => setSettings((cur) => ({ ...cur, defaultAgent: e.target.value }))}
|
|
161
|
-
>
|
|
162
|
-
{(snapshot?.agents || []).map((a) => (
|
|
163
|
-
<option key={a.name} value={a.name}>@{a.name}</option>
|
|
164
|
-
))}
|
|
165
|
-
</select>
|
|
166
|
-
</div>
|
|
167
|
-
</section>
|
|
168
|
-
|
|
169
|
-
{/* About */}
|
|
170
|
-
<section className="mobile-section">
|
|
171
|
-
<h3 className="mobile-section-title">About</h3>
|
|
172
|
-
<div className="mobile-card">
|
|
173
|
-
<div className="mobile-setting-row">
|
|
174
|
-
<span className="mobile-setting-label">Version</span>
|
|
175
|
-
<span className="mobile-setting-value mono">{settings.about?.version || '—'}</span>
|
|
176
|
-
</div>
|
|
177
|
-
<div className="mobile-setting-row">
|
|
178
|
-
<span className="mobile-setting-label">Agents</span>
|
|
179
|
-
<span className="mobile-setting-value">{snapshot?.agents?.length || 0}</span>
|
|
180
|
-
</div>
|
|
181
|
-
<div className="mobile-setting-row">
|
|
182
|
-
<span className="mobile-setting-label">Tasks</span>
|
|
183
|
-
<span className="mobile-setting-value">{snapshot?.tasks?.length || 0}</span>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
</section>
|
|
187
|
-
|
|
188
|
-
{/* Switch to desktop */}
|
|
189
|
-
<div className="mobile-view-footer">
|
|
190
|
-
<a href="/?desktop=1" className="mobile-btn mobile-btn-secondary">
|
|
191
|
-
Switch to Desktop
|
|
192
|
-
</a>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
236
|
+
{filtered.length === 0 && (
|
|
237
|
+
<div className="mobile-settings-empty">No settings match "{search}"</div>
|
|
238
|
+
)}
|
|
193
239
|
</div>
|
|
194
240
|
</div>
|
|
195
241
|
);
|
|
196
242
|
}
|
|
243
|
+
|
|
244
|
+
function SearchIcon() {
|
|
245
|
+
return (
|
|
246
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
247
|
+
<circle cx="11" cy="11" r="8" />
|
|
248
|
+
<path d="m21 21-4.35-4.35" />
|
|
249
|
+
</svg>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/mobile/MobileTasks.tsx — filter-tab task list with search and FAB create sheet.
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Search, Plus } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import { TaskCard } from '../components/TaskCard';
|
|
6
|
+
import { TaskCreateSheet } from '../components/TaskCreateSheet';
|
|
7
|
+
import type { Snapshot } from '../lib/types';
|
|
8
|
+
|
|
9
|
+
type Filter = 'all' | 'todo' | 'doing' | 'done' | 'failed';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
snapshot: Snapshot;
|
|
13
|
+
refreshSnapshot: () => Promise<void>;
|
|
14
|
+
userId?: string;
|
|
15
|
+
workspaceId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function MobileTasks({ snapshot, refreshSnapshot, userId, workspaceId }: Props) {
|
|
19
|
+
const [filter, setFilter] = useState<Filter>('all');
|
|
20
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
21
|
+
const [search, setSearch] = useState('');
|
|
22
|
+
|
|
23
|
+
const tasks = (snapshot?.tasks || []).filter((t) => {
|
|
24
|
+
if (filter === 'todo' && t.status !== 'queued' && t.status !== 'todo') return false;
|
|
25
|
+
if (filter === 'doing' && t.status !== 'doing') return false;
|
|
26
|
+
if (filter === 'done' && t.status !== 'done') return false;
|
|
27
|
+
if (filter === 'failed' && t.status !== 'failed' && t.status !== 'blocked') return false;
|
|
28
|
+
if (search && !(t.title || '').toLowerCase().includes(search.toLowerCase())) return false;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const counts: Record<Filter, number> = {
|
|
33
|
+
all: snapshot?.tasks?.length || 0,
|
|
34
|
+
todo: snapshot?.tasks?.filter((t) => t.status === 'queued' || t.status === 'todo').length || 0,
|
|
35
|
+
doing: snapshot?.tasks?.filter((t) => t.status === 'doing').length || 0,
|
|
36
|
+
done: snapshot?.tasks?.filter((t) => t.status === 'done').length || 0,
|
|
37
|
+
failed: snapshot?.tasks?.filter((t) => t.status === 'failed' || t.status === 'blocked').length || 0,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="mobile-tasks">
|
|
42
|
+
<div className="mobile-tasks-search">
|
|
43
|
+
<Search size={16} className="mobile-tasks-search-icon" />
|
|
44
|
+
<input
|
|
45
|
+
value={search}
|
|
46
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
47
|
+
placeholder="Search tasks..."
|
|
48
|
+
className="mobile-tasks-search-input"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="mobile-tasks-tabs">
|
|
53
|
+
{(['all', 'todo', 'doing', 'done', 'failed'] as Filter[]).map((f) => (
|
|
54
|
+
<button
|
|
55
|
+
key={f}
|
|
56
|
+
type="button"
|
|
57
|
+
className={cn('mobile-tasks-tab', filter === f && 'is-active')}
|
|
58
|
+
onClick={() => setFilter(f)}
|
|
59
|
+
>
|
|
60
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
61
|
+
<span className="mobile-tasks-count">{counts[f]}</span>
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="mobile-tasks-list">
|
|
67
|
+
{tasks.length === 0 ? (
|
|
68
|
+
<p className="mobile-empty">No tasks. Create one to get started.</p>
|
|
69
|
+
) : (
|
|
70
|
+
tasks.map((t) => (
|
|
71
|
+
<TaskCard key={t.id} task={t} workspaceId={workspaceId} />
|
|
72
|
+
))
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
className="mobile-fab"
|
|
79
|
+
onClick={() => setShowCreate(true)}
|
|
80
|
+
aria-label="Create new task"
|
|
81
|
+
>
|
|
82
|
+
<Plus size={24} />
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
{showCreate && (
|
|
86
|
+
<TaskCreateSheet
|
|
87
|
+
workspaceId={workspaceId}
|
|
88
|
+
onClose={() => setShowCreate(false)}
|
|
89
|
+
onCreated={refreshSnapshot}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -3,6 +3,7 @@ import { StrictMode } from 'react';
|
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
4
|
import { MobileApp } from './MobileApp';
|
|
5
5
|
import './styles/mobile.css';
|
|
6
|
+
import './styles/mobile-layout.css';
|
|
6
7
|
|
|
7
8
|
const root = document.getElementById('root');
|
|
8
9
|
if (!root) throw new Error('Root element #root not found');
|