@hienlh/ppm 0.2.20 → 0.2.21
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +18 -1
- package/bun.lock +57 -59
- package/dist/ppm +0 -0
- package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
- package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
- package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
- package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
- package/dist/web/assets/index-3zt5mBwZ.css +2 -0
- package/dist/web/assets/index-CaUQy3Zs.js +21 -0
- package/dist/web/assets/input-CTnwfHVN.js +41 -0
- package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
- package/dist/web/assets/{terminal-tab-sGlqTp7k.js → terminal-tab-BEFAYT4S.js} +1 -1
- package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
- package/dist/web/index.html +35 -8
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +13 -8
- package/docs/project-roadmap.md +22 -4
- package/docs/system-architecture.md +59 -0
- package/package.json +6 -14
- package/src/providers/claude-agent-sdk.ts +2 -2
- package/src/providers/registry.ts +12 -11
- package/src/server/routes/projects.ts +43 -0
- package/src/server/routes/settings.ts +42 -8
- package/src/server/ws/chat.ts +2 -2
- package/src/services/config.service.ts +5 -1
- package/src/services/project.service.ts +1 -0
- package/src/types/config.ts +37 -0
- package/src/types/project.ts +1 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
- package/src/web/app.tsx +43 -5
- package/src/web/components/chat/chat-history-panel.tsx +106 -0
- package/src/web/components/chat/chat-tab.tsx +27 -19
- package/src/web/components/editor/code-editor.tsx +78 -197
- package/src/web/components/editor/diff-viewer.tsx +59 -176
- package/src/web/components/layout/add-project-form.tsx +151 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +6 -4
- package/src/web/components/layout/mobile-drawer.tsx +48 -180
- package/src/web/components/layout/mobile-nav.tsx +89 -6
- package/src/web/components/layout/panel-layout.tsx +16 -10
- package/src/web/components/layout/project-bar.tsx +329 -0
- package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
- package/src/web/components/layout/sidebar.tsx +56 -142
- package/src/web/components/layout/tab-bar.tsx +1 -6
- package/src/web/components/layout/tab-content.tsx +0 -10
- package/src/web/components/ui/dialog.tsx +1 -1
- package/src/web/lib/project-avatar.ts +45 -0
- package/src/web/lib/project-palette.ts +18 -0
- package/src/web/lib/use-monaco-theme.ts +29 -0
- package/src/web/stores/panel-store.ts +96 -9
- package/src/web/stores/project-store.ts +87 -3
- package/src/web/stores/settings-store.ts +31 -4
- package/src/web/stores/tab-store.ts +0 -2
- package/vite.config.ts +6 -2
- package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
- package/dist/web/assets/button-CQ5h5gxS.js +0 -41
- package/dist/web/assets/chat-tab-Cfw__7vJ.js +0 -6
- package/dist/web/assets/code-editor-D8Pz69sx.js +0 -2
- package/dist/web/assets/dialog-BL9i7XEo.js +0 -5
- package/dist/web/assets/diff-viewer-CWS5n7ur.js +0 -4
- package/dist/web/assets/dist-0XHv8Vwc.js +0 -1
- package/dist/web/assets/dist-Ca3N8Xbh.js +0 -46
- package/dist/web/assets/git-graph-DwA62J8-.js +0 -1
- package/dist/web/assets/git-status-panel-DaB-zzSF.js +0 -1
- package/dist/web/assets/index-BYIXPY6U.css +0 -2
- package/dist/web/assets/index-DbTCLiox.js +0 -17
- package/dist/web/assets/project-list-Z4lhtp6P.js +0 -1
- package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
- package/dist/web/assets/settings-tab-BW6MGcir.js +0 -1
- package/dist/web/assets/trash-2-CGlFXde_.js +0 -1
- package/dist/web/assets/x-C0Rw5Giw.js +0 -1
- /package/dist/web/assets/{api-client-DzH9zCD7.js → api-client-BCjah751.js} +0 -0
- /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
- /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
- /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
- /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
- /package/dist/web/assets/{utils-D6me7KDg.js → utils-B-_GCz7E.js} +0 -0
|
@@ -1,95 +1,32 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
|
-
FolderOpen,
|
|
4
|
-
Terminal,
|
|
5
|
-
MessageSquare,
|
|
6
|
-
GitBranch,
|
|
7
|
-
GitCommitHorizontal,
|
|
8
|
-
FileDiff,
|
|
9
|
-
Settings,
|
|
10
|
-
X,
|
|
11
|
-
FileCode,
|
|
12
|
-
ChevronDown,
|
|
13
|
-
Check,
|
|
14
|
-
Plus,
|
|
15
|
-
Search,
|
|
16
|
-
Bug,
|
|
3
|
+
X, Bug, FolderOpen, GitBranch, MessageSquare,
|
|
17
4
|
} from "lucide-react";
|
|
18
|
-
import { useProjectStore
|
|
19
|
-
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
5
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
20
6
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
21
|
-
import { cn } from "@/lib/utils";
|
|
22
|
-
import { Separator } from "@/components/ui/separator";
|
|
23
7
|
import { FileTree } from "@/components/explorer/file-tree";
|
|
8
|
+
import { GitStatusPanel } from "@/components/git/git-status-panel";
|
|
9
|
+
import { ChatHistoryPanel } from "@/components/chat/chat-history-panel";
|
|
24
10
|
import { openBugReport } from "@/lib/report-bug";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
type DrawerTab = "explorer" | "git" | "history";
|
|
14
|
+
|
|
15
|
+
const TABS: { id: DrawerTab; label: string; icon: React.ElementType }[] = [
|
|
16
|
+
{ id: "explorer", label: "Explorer", icon: FolderOpen },
|
|
17
|
+
{ id: "git", label: "Git", icon: GitBranch },
|
|
18
|
+
{ id: "history", label: "History", icon: MessageSquare },
|
|
19
|
+
];
|
|
25
20
|
|
|
26
21
|
interface MobileDrawerProps {
|
|
27
22
|
isOpen: boolean;
|
|
28
23
|
onClose: () => void;
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
32
|
-
projects: FolderOpen,
|
|
33
|
-
terminal: Terminal,
|
|
34
|
-
chat: MessageSquare,
|
|
35
|
-
editor: FileCode,
|
|
36
|
-
"git-graph": GitBranch,
|
|
37
|
-
"git-status": GitCommitHorizontal,
|
|
38
|
-
"git-diff": FileDiff,
|
|
39
|
-
settings: Settings,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
43
|
-
{ type: "terminal", label: "Terminal" },
|
|
44
|
-
{ type: "chat", label: "AI Chat" },
|
|
45
|
-
{ type: "git-status", label: "Git Status" },
|
|
46
|
-
{ type: "git-graph", label: "Git Graph" },
|
|
47
|
-
{ type: "settings", label: "Settings" },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
/** Max projects shown before needing to search (mobile — larger items) */
|
|
51
|
-
const MAX_VISIBLE_MOBILE = 5;
|
|
52
|
-
|
|
53
26
|
export function MobileDrawer({ isOpen, onClose }: MobileDrawerProps) {
|
|
54
|
-
const {
|
|
55
|
-
const openTab = useTabStore((s) => s.openTab);
|
|
27
|
+
const { activeProject } = useProjectStore();
|
|
56
28
|
const version = useSettingsStore((s) => s.version);
|
|
57
|
-
const [
|
|
58
|
-
const [query, setQuery] = useState("");
|
|
59
|
-
|
|
60
|
-
const sorted = useMemo(() => sortByRecent(projects), [projects]);
|
|
61
|
-
|
|
62
|
-
const filtered = useMemo(() => {
|
|
63
|
-
if (!query.trim()) return sorted.slice(0, MAX_VISIBLE_MOBILE);
|
|
64
|
-
const q = query.toLowerCase();
|
|
65
|
-
return sorted.filter(
|
|
66
|
-
(p) => p.name.toLowerCase().includes(q) || p.path.toLowerCase().includes(q),
|
|
67
|
-
);
|
|
68
|
-
}, [sorted, query]);
|
|
69
|
-
|
|
70
|
-
const showSearch = projects.length > MAX_VISIBLE_MOBILE || query.length > 0;
|
|
71
|
-
|
|
72
|
-
function handleNewTab(type: TabType) {
|
|
73
|
-
const needsProject =
|
|
74
|
-
type === "git-graph" || type === "git-status" || type === "git-diff" || type === "terminal" || type === "chat";
|
|
75
|
-
const metadata = needsProject
|
|
76
|
-
? { projectName: activeProject?.name }
|
|
77
|
-
: undefined;
|
|
78
|
-
const label = NEW_TAB_OPTIONS.find((o) => o.type === type)?.label ?? type;
|
|
79
|
-
openTab({ type, title: label, metadata, projectId: activeProject?.name ?? null, closable: true });
|
|
80
|
-
onClose();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function handleSelectProject(project: typeof projects[number]) {
|
|
84
|
-
setActiveProject(project);
|
|
85
|
-
setProjectPickerOpen(false);
|
|
86
|
-
setQuery("");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function handleTogglePicker() {
|
|
90
|
-
setProjectPickerOpen((v) => !v);
|
|
91
|
-
setQuery("");
|
|
92
|
-
}
|
|
29
|
+
const [activeTab, setActiveTab] = useState<DrawerTab>("explorer");
|
|
93
30
|
|
|
94
31
|
const handleReportBug = useCallback(() => openBugReport(version), [version]);
|
|
95
32
|
|
|
@@ -117,7 +54,9 @@ export function MobileDrawer({ isOpen, onClose }: MobileDrawerProps) {
|
|
|
117
54
|
>
|
|
118
55
|
{/* Header — logo + close */}
|
|
119
56
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
120
|
-
<span className="text-sm font-bold text-primary tracking-tight">
|
|
57
|
+
<span className="text-sm font-bold text-primary tracking-tight">
|
|
58
|
+
{activeProject?.name ?? "PPM"}
|
|
59
|
+
</span>
|
|
121
60
|
<button
|
|
122
61
|
onClick={onClose}
|
|
123
62
|
className="flex items-center justify-center size-8 rounded-md hover:bg-surface-elevated transition-colors"
|
|
@@ -126,118 +65,47 @@ export function MobileDrawer({ isOpen, onClose }: MobileDrawerProps) {
|
|
|
126
65
|
</button>
|
|
127
66
|
</div>
|
|
128
67
|
|
|
129
|
-
{/*
|
|
130
|
-
<div className="flex-1 overflow-y-auto">
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
68
|
+
{/* Tab content — scrollable */}
|
|
69
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
70
|
+
{activeTab === "explorer" && (
|
|
71
|
+
activeProject ? (
|
|
72
|
+
<FileTree onFileOpen={onClose} />
|
|
73
|
+
) : (
|
|
74
|
+
<p className="px-4 py-6 text-xs text-text-secondary text-center">
|
|
75
|
+
Select a project from the bottom nav bar
|
|
76
|
+
</p>
|
|
77
|
+
)
|
|
78
|
+
)}
|
|
79
|
+
{activeTab === "git" && (
|
|
80
|
+
<GitStatusPanel metadata={{ projectName: activeProject?.name }} />
|
|
81
|
+
)}
|
|
82
|
+
{activeTab === "history" && (
|
|
83
|
+
<ChatHistoryPanel projectName={activeProject?.name} />
|
|
137
84
|
)}
|
|
138
85
|
</div>
|
|
139
86
|
|
|
140
|
-
{/* Bottom
|
|
87
|
+
{/* Bottom tab bar — thumb-friendly */}
|
|
141
88
|
<div className="shrink-0 border-t border-border">
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
89
|
+
<div className="flex items-center">
|
|
90
|
+
{TABS.map((tab) => {
|
|
91
|
+
const Icon = tab.icon;
|
|
92
|
+
const isActive = activeTab === tab.id;
|
|
146
93
|
return (
|
|
147
94
|
<button
|
|
148
|
-
key={
|
|
149
|
-
onClick={() =>
|
|
150
|
-
className=
|
|
95
|
+
key={tab.id}
|
|
96
|
+
onClick={() => setActiveTab(tab.id)}
|
|
97
|
+
className={cn(
|
|
98
|
+
"flex-1 flex flex-col items-center gap-0.5 py-2.5 text-[10px] transition-colors",
|
|
99
|
+
isActive ? "text-primary" : "text-text-secondary",
|
|
100
|
+
)}
|
|
151
101
|
>
|
|
152
|
-
<Icon className="size-4
|
|
153
|
-
<span>{
|
|
102
|
+
<Icon className="size-4" />
|
|
103
|
+
<span>{tab.label}</span>
|
|
154
104
|
</button>
|
|
155
105
|
);
|
|
156
106
|
})}
|
|
157
107
|
</div>
|
|
158
108
|
|
|
159
|
-
<Separator />
|
|
160
|
-
|
|
161
|
-
{/* Project switcher — at very bottom for easy thumb access */}
|
|
162
|
-
<div className="relative">
|
|
163
|
-
<button
|
|
164
|
-
onClick={handleTogglePicker}
|
|
165
|
-
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-surface-elevated transition-colors"
|
|
166
|
-
>
|
|
167
|
-
<FolderOpen className="size-4 text-primary shrink-0" />
|
|
168
|
-
<span className="text-sm font-medium truncate flex-1">
|
|
169
|
-
{activeProject?.name ?? "Select Project"}
|
|
170
|
-
</span>
|
|
171
|
-
<ChevronDown className={cn(
|
|
172
|
-
"size-3.5 text-text-subtle shrink-0 transition-transform",
|
|
173
|
-
projectPickerOpen && "rotate-180",
|
|
174
|
-
)} />
|
|
175
|
-
</button>
|
|
176
|
-
|
|
177
|
-
{/* Project list popover — opens upward */}
|
|
178
|
-
{projectPickerOpen && (
|
|
179
|
-
<div className="absolute bottom-full left-0 right-0 bg-background border border-border rounded-t-lg shadow-lg overflow-hidden">
|
|
180
|
-
{/* Search */}
|
|
181
|
-
{showSearch && (
|
|
182
|
-
<div className="flex items-center gap-2 px-3 py-2 border-b border-border">
|
|
183
|
-
<Search className="size-3.5 text-text-subtle shrink-0" />
|
|
184
|
-
<input
|
|
185
|
-
type="text"
|
|
186
|
-
value={query}
|
|
187
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
188
|
-
placeholder="Search projects..."
|
|
189
|
-
className="flex-1 bg-transparent text-sm outline-none placeholder:text-text-subtle text-text-primary"
|
|
190
|
-
autoFocus
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{/* Project list */}
|
|
196
|
-
<div className="max-h-56 overflow-y-auto">
|
|
197
|
-
{filtered.map((project) => (
|
|
198
|
-
<button
|
|
199
|
-
key={project.name}
|
|
200
|
-
onClick={() => handleSelectProject(project)}
|
|
201
|
-
className={cn(
|
|
202
|
-
"w-full flex items-center gap-2.5 px-4 py-2 text-left transition-colors",
|
|
203
|
-
activeProject?.name === project.name
|
|
204
|
-
? "bg-accent/10 text-text-primary"
|
|
205
|
-
: "text-text-secondary hover:bg-surface-elevated",
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
208
|
-
<FolderOpen className="size-4 shrink-0" />
|
|
209
|
-
<div className="flex-1 min-w-0">
|
|
210
|
-
<p className="text-sm font-medium truncate">{project.name}</p>
|
|
211
|
-
<p className="text-xs text-text-subtle truncate">{project.path}</p>
|
|
212
|
-
</div>
|
|
213
|
-
{activeProject?.name === project.name && (
|
|
214
|
-
<Check className="size-4 text-primary shrink-0" />
|
|
215
|
-
)}
|
|
216
|
-
</button>
|
|
217
|
-
))}
|
|
218
|
-
{filtered.length === 0 && (
|
|
219
|
-
<p className="px-4 py-3 text-xs text-text-subtle text-center">
|
|
220
|
-
{query ? "No matches" : "No projects"}
|
|
221
|
-
</p>
|
|
222
|
-
)}
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
{/* Add project */}
|
|
226
|
-
<button
|
|
227
|
-
onClick={() => {
|
|
228
|
-
setProjectPickerOpen(false);
|
|
229
|
-
openTab({ type: "projects", title: "Projects", projectId: null, closable: true });
|
|
230
|
-
onClose();
|
|
231
|
-
}}
|
|
232
|
-
className="w-full flex items-center gap-2 px-4 py-2.5 text-left text-sm text-text-secondary hover:bg-surface-elevated border-t border-border"
|
|
233
|
-
>
|
|
234
|
-
<Plus className="size-4 shrink-0" />
|
|
235
|
-
<span>Add Project...</span>
|
|
236
|
-
</button>
|
|
237
|
-
</div>
|
|
238
|
-
)}
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
109
|
{/* Report Bug + Version */}
|
|
242
110
|
<div className="flex items-center justify-between px-4 py-2 border-t border-border">
|
|
243
111
|
{version && <span className="text-[10px] text-text-subtle">v{version}</span>}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical,
|
|
3
|
+
Terminal, MessageSquare, GitBranch,
|
|
4
|
+
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
|
|
5
5
|
} from "lucide-react";
|
|
6
6
|
import { usePanelStore } from "@/stores/panel-store";
|
|
7
|
+
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
7
8
|
import { findPanelPosition, MAX_ROWS } from "@/stores/panel-utils";
|
|
9
|
+
import { resolveProjectColor } from "@/lib/project-palette";
|
|
10
|
+
import { getProjectInitials } from "@/lib/project-avatar";
|
|
8
11
|
import type { TabType } from "@/stores/tab-store";
|
|
9
12
|
import { cn } from "@/lib/utils";
|
|
10
13
|
|
|
14
|
+
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
15
|
+
{ type: "terminal", label: "Terminal" },
|
|
16
|
+
{ type: "chat", label: "AI Chat" },
|
|
17
|
+
{ type: "git-graph", label: "Git Graph" },
|
|
18
|
+
{ type: "settings", label: "Settings" },
|
|
19
|
+
];
|
|
20
|
+
const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_TAB_OPTIONS.map((o) => [o.type, o.label]));
|
|
21
|
+
|
|
11
22
|
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
12
|
-
|
|
13
|
-
"git-graph": GitBranch, "git-
|
|
23
|
+
terminal: Terminal, chat: MessageSquare, editor: FileCode,
|
|
24
|
+
"git-graph": GitBranch, "git-diff": FileDiff, settings: Settings,
|
|
14
25
|
};
|
|
15
26
|
|
|
16
|
-
interface MobileNavProps { onMenuPress: () => void; }
|
|
27
|
+
interface MobileNavProps { onMenuPress: () => void; onProjectsPress: () => void; }
|
|
17
28
|
|
|
18
|
-
export function MobileNav({ onMenuPress }: MobileNavProps) {
|
|
29
|
+
export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
19
30
|
const focusedPanelId = usePanelStore((s) => s.focusedPanelId);
|
|
20
31
|
const panel = usePanelStore((s) => s.panels[s.focusedPanelId]);
|
|
21
32
|
const panelCount = usePanelStore((s) => Object.keys(s.panels).length);
|
|
@@ -26,6 +37,7 @@ export function MobileNav({ onMenuPress }: MobileNavProps) {
|
|
|
26
37
|
const prevTabCount = useRef(tabs.length);
|
|
27
38
|
|
|
28
39
|
const [menuTabId, setMenuTabId] = useState<string | null>(null);
|
|
40
|
+
const [newTabSheetOpen, setNewTabSheetOpen] = useState(false);
|
|
29
41
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
30
42
|
|
|
31
43
|
useEffect(() => {
|
|
@@ -66,6 +78,29 @@ export function MobileNav({ onMenuPress }: MobileNavProps) {
|
|
|
66
78
|
const menuTab = menuTabId ? tabs.find((t) => t.id === menuTabId) : null;
|
|
67
79
|
const menuTabIdx = menuTabId ? tabs.findIndex((t) => t.id === menuTabId) : -1;
|
|
68
80
|
|
|
81
|
+
const { activeProject: activeProjectForTab } = useProjectStore.getState();
|
|
82
|
+
function handleNewTab(type: TabType) {
|
|
83
|
+
const needsProject = type === "git-graph" || type === "git-diff" || type === "terminal" || type === "chat";
|
|
84
|
+
const metadata = needsProject ? { projectName: activeProjectForTab?.name } : undefined;
|
|
85
|
+
usePanelStore.getState().openTab(
|
|
86
|
+
{ type, title: NEW_TAB_LABELS[type] ?? type, metadata, projectId: activeProjectForTab?.name ?? null, closable: true },
|
|
87
|
+
focusedPanelId,
|
|
88
|
+
);
|
|
89
|
+
setNewTabSheetOpen(false);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Active project avatar for the Projects button
|
|
93
|
+
const { activeProject, projects, customOrder } = useProjectStore();
|
|
94
|
+
const ordered = resolveOrder(projects, customOrder ?? null);
|
|
95
|
+
const allNames = ordered.map((p) => p.name);
|
|
96
|
+
const activeIdx = ordered.findIndex((p) => p.name === activeProject?.name);
|
|
97
|
+
const activeColor = activeProject
|
|
98
|
+
? resolveProjectColor(activeProject.color, activeIdx >= 0 ? activeIdx : 0)
|
|
99
|
+
: "#4f86c6";
|
|
100
|
+
const activeInitials = activeProject
|
|
101
|
+
? getProjectInitials(activeProject.name, allNames)
|
|
102
|
+
: null;
|
|
103
|
+
|
|
69
104
|
return (
|
|
70
105
|
<nav className="fixed bottom-0 left-0 right-0 md:hidden bg-background border-t border-border z-40 select-none">
|
|
71
106
|
<div className="flex items-center h-12">
|
|
@@ -103,8 +138,56 @@ export function MobileNav({ onMenuPress }: MobileNavProps) {
|
|
|
103
138
|
);
|
|
104
139
|
})}
|
|
105
140
|
</div>
|
|
141
|
+
|
|
142
|
+
{/* Add tab button */}
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => setNewTabSheetOpen(true)}
|
|
145
|
+
className="flex items-center justify-center size-12 shrink-0 border-t-2 border-transparent text-text-secondary"
|
|
146
|
+
>
|
|
147
|
+
<Plus className="size-4" />
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
{/* Projects button (rightmost) */}
|
|
151
|
+
<button
|
|
152
|
+
onClick={onProjectsPress}
|
|
153
|
+
className="flex items-center justify-center size-12 shrink-0 text-text-secondary border-l border-border"
|
|
154
|
+
title="Switch project"
|
|
155
|
+
>
|
|
156
|
+
{activeInitials ? (
|
|
157
|
+
<div
|
|
158
|
+
className="size-7 rounded-full flex items-center justify-center text-[10px] font-bold text-white"
|
|
159
|
+
style={{ background: activeColor }}
|
|
160
|
+
>
|
|
161
|
+
{activeInitials}
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
<Layers className="size-5" />
|
|
165
|
+
)}
|
|
166
|
+
</button>
|
|
106
167
|
</div>
|
|
107
168
|
|
|
169
|
+
{/* New tab action sheet */}
|
|
170
|
+
{newTabSheetOpen && (
|
|
171
|
+
<>
|
|
172
|
+
<div className="fixed inset-0 z-50" onClick={() => setNewTabSheetOpen(false)} />
|
|
173
|
+
<div className="fixed bottom-14 left-2 right-2 z-50 bg-surface border border-border rounded-lg shadow-lg overflow-hidden animate-in slide-in-from-bottom-2 duration-150">
|
|
174
|
+
<div className="px-3 py-2 text-xs text-text-secondary border-b border-border">New Tab</div>
|
|
175
|
+
{NEW_TAB_OPTIONS.map((opt) => {
|
|
176
|
+
const Icon = TAB_ICONS[opt.type];
|
|
177
|
+
return (
|
|
178
|
+
<button
|
|
179
|
+
key={opt.type}
|
|
180
|
+
onClick={() => handleNewTab(opt.type)}
|
|
181
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated"
|
|
182
|
+
>
|
|
183
|
+
<Icon className="size-4" /> {opt.label}
|
|
184
|
+
</button>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
|
|
108
191
|
{/* Long-press action sheet */}
|
|
109
192
|
{menuTab && (
|
|
110
193
|
<>
|
|
@@ -3,34 +3,40 @@ import { GripVertical, GripHorizontal } from "lucide-react";
|
|
|
3
3
|
import { usePanelStore } from "@/stores/panel-store";
|
|
4
4
|
import { EditorPanel } from "./editor-panel";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
interface PanelLayoutProps {
|
|
7
|
+
projectName: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PanelLayout({ projectName }: PanelLayoutProps) {
|
|
11
|
+
const grid = usePanelStore((s) =>
|
|
12
|
+
s.currentProject === projectName ? s.grid : (s.projectGrids[projectName] ?? [[]]),
|
|
13
|
+
);
|
|
14
|
+
const panelCount = grid.flat().length;
|
|
9
15
|
|
|
10
16
|
if (panelCount <= 1 && grid[0]?.[0]) {
|
|
11
|
-
return <EditorPanel panelId={grid[0][0]} />;
|
|
17
|
+
return <EditorPanel panelId={grid[0][0]} projectName={projectName} />;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
return (
|
|
15
21
|
<Group orientation="horizontal" style={{ height: "100%" }}>
|
|
16
22
|
{grid.map((column, colIdx) => (
|
|
17
|
-
<ColumnPanel key={`col-${colIdx}`} column={column} colIdx={colIdx} totalCols={grid.length} />
|
|
23
|
+
<ColumnPanel key={`col-${colIdx}`} column={column} colIdx={colIdx} totalCols={grid.length} projectName={projectName} />
|
|
18
24
|
))}
|
|
19
25
|
</Group>
|
|
20
26
|
);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
function ColumnPanel({ column, colIdx, totalCols }: { column: string[]; colIdx: number; totalCols: number }) {
|
|
29
|
+
function ColumnPanel({ column, colIdx, totalCols, projectName }: { column: string[]; colIdx: number; totalCols: number; projectName: string }) {
|
|
24
30
|
const defaultSize = `${Math.round(100 / totalCols)}%`;
|
|
25
31
|
return (
|
|
26
32
|
<>
|
|
27
33
|
<Panel minSize="15%" defaultSize={defaultSize}>
|
|
28
34
|
{column.length === 1 ? (
|
|
29
|
-
<EditorPanel panelId={column[0]!} />
|
|
35
|
+
<EditorPanel panelId={column[0]!} projectName={projectName} />
|
|
30
36
|
) : (
|
|
31
37
|
<Group orientation="vertical">
|
|
32
38
|
{column.map((panelId, rowIdx) => (
|
|
33
|
-
<RowPanel key={panelId} panelId={panelId} rowIdx={rowIdx} totalRows={column.length} />
|
|
39
|
+
<RowPanel key={panelId} panelId={panelId} rowIdx={rowIdx} totalRows={column.length} projectName={projectName} />
|
|
34
40
|
))}
|
|
35
41
|
</Group>
|
|
36
42
|
)}
|
|
@@ -40,12 +46,12 @@ function ColumnPanel({ column, colIdx, totalCols }: { column: string[]; colIdx:
|
|
|
40
46
|
);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
function RowPanel({ panelId, rowIdx, totalRows }: { panelId: string; rowIdx: number; totalRows: number }) {
|
|
49
|
+
function RowPanel({ panelId, rowIdx, totalRows, projectName }: { panelId: string; rowIdx: number; totalRows: number; projectName: string }) {
|
|
44
50
|
const defaultSize = `${Math.round(100 / totalRows)}%`;
|
|
45
51
|
return (
|
|
46
52
|
<>
|
|
47
53
|
<Panel minSize="15%" defaultSize={defaultSize}>
|
|
48
|
-
<EditorPanel panelId={panelId} />
|
|
54
|
+
<EditorPanel panelId={panelId} projectName={projectName} />
|
|
49
55
|
</Panel>
|
|
50
56
|
{rowIdx < totalRows - 1 && <ResizeHandle orientation="horizontal" />}
|
|
51
57
|
</>
|