@hienlh/ppm 0.2.19 → 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.
Files changed (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CLAUDE.md +18 -1
  3. package/bun.lock +57 -59
  4. package/dist/ppm +0 -0
  5. package/dist/web/assets/api-client-BCjah751.js +1 -0
  6. package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
  7. package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
  8. package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
  9. package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
  10. package/dist/web/assets/index-3zt5mBwZ.css +2 -0
  11. package/dist/web/assets/index-CaUQy3Zs.js +21 -0
  12. package/dist/web/assets/input-CTnwfHVN.js +41 -0
  13. package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
  14. package/dist/web/assets/{terminal-tab-DlRo-KzS.js → terminal-tab-BEFAYT4S.js} +1 -1
  15. package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
  16. package/dist/web/index.html +35 -9
  17. package/dist/web/sw.js +1 -1
  18. package/docs/codebase-summary.md +13 -8
  19. package/docs/project-roadmap.md +22 -4
  20. package/docs/system-architecture.md +59 -0
  21. package/package.json +6 -14
  22. package/src/providers/claude-agent-sdk.ts +2 -2
  23. package/src/providers/registry.ts +12 -11
  24. package/src/server/routes/projects.ts +43 -0
  25. package/src/server/routes/settings.ts +42 -8
  26. package/src/server/ws/chat.ts +2 -2
  27. package/src/services/config.service.ts +5 -1
  28. package/src/services/project.service.ts +1 -0
  29. package/src/types/config.ts +37 -0
  30. package/src/types/project.ts +1 -0
  31. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
  32. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
  33. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
  34. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
  35. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
  36. package/src/web/app.tsx +43 -5
  37. package/src/web/components/chat/chat-history-panel.tsx +106 -0
  38. package/src/web/components/chat/chat-tab.tsx +27 -19
  39. package/src/web/components/editor/code-editor.tsx +101 -173
  40. package/src/web/components/editor/diff-viewer.tsx +67 -172
  41. package/src/web/components/git/git-status-panel.tsx +4 -11
  42. package/src/web/components/layout/add-project-form.tsx +151 -0
  43. package/src/web/components/layout/command-palette.tsx +3 -1
  44. package/src/web/components/layout/editor-panel.tsx +6 -4
  45. package/src/web/components/layout/mobile-drawer.tsx +48 -180
  46. package/src/web/components/layout/mobile-nav.tsx +89 -6
  47. package/src/web/components/layout/panel-layout.tsx +16 -10
  48. package/src/web/components/layout/project-bar.tsx +329 -0
  49. package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
  50. package/src/web/components/layout/sidebar.tsx +56 -142
  51. package/src/web/components/layout/tab-bar.tsx +1 -6
  52. package/src/web/components/layout/tab-content.tsx +0 -10
  53. package/src/web/components/ui/dialog.tsx +1 -1
  54. package/src/web/lib/project-avatar.ts +45 -0
  55. package/src/web/lib/project-palette.ts +18 -0
  56. package/src/web/lib/use-monaco-theme.ts +29 -0
  57. package/src/web/stores/panel-store.ts +96 -9
  58. package/src/web/stores/project-store.ts +87 -3
  59. package/src/web/stores/settings-store.ts +52 -5
  60. package/src/web/stores/tab-store.ts +0 -2
  61. package/vite.config.ts +6 -2
  62. package/dist/web/assets/api-client-B_eCZViO.js +0 -1
  63. package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
  64. package/dist/web/assets/button-CvHWF07y.js +0 -41
  65. package/dist/web/assets/chat-tab-DJvME48K.js +0 -6
  66. package/dist/web/assets/code-editor-81Tzd5aV.js +0 -2
  67. package/dist/web/assets/dialog-Cn5zGuid.js +0 -5
  68. package/dist/web/assets/diff-viewer-pieRctzs.js +0 -4
  69. package/dist/web/assets/dist-B6sG2GPc.js +0 -1
  70. package/dist/web/assets/dist-CBiGQxfr.js +0 -46
  71. package/dist/web/assets/git-graph-CWI6hxtE.js +0 -1
  72. package/dist/web/assets/git-status-panel-CAjReViM.js +0 -1
  73. package/dist/web/assets/index-BdUoflYx.css +0 -2
  74. package/dist/web/assets/index-CqpLusQd.js +0 -17
  75. package/dist/web/assets/project-list-MAvAY2K3.js +0 -1
  76. package/dist/web/assets/react-C32bf_ch.js +0 -1
  77. package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
  78. package/dist/web/assets/settings-tab-zeZrAFld.js +0 -1
  79. package/dist/web/assets/trash-2-Dc17nbCE.js +0 -1
  80. package/dist/web/assets/x-Bpqyw40Y.js +0 -1
  81. /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
  82. /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
  83. /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
  84. /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
  85. /package/dist/web/assets/{utils-61GRB9Cb.js → utils-B-_GCz7E.js} +0 -0
@@ -1,95 +1,32 @@
1
- import { useState, useMemo, useCallback } from "react";
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, sortByRecent } from "@/stores/project-store";
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 { projects, activeProject, setActiveProject } = useProjectStore();
55
- const openTab = useTabStore((s) => s.openTab);
27
+ const { activeProject } = useProjectStore();
56
28
  const version = useSettingsStore((s) => s.version);
57
- const [projectPickerOpen, setProjectPickerOpen] = useState(false);
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">PPM</span>
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
- {/* File tree — scrollable, takes remaining space */}
130
- <div className="flex-1 overflow-y-auto">
131
- {activeProject ? (
132
- <FileTree onFileOpen={onClose} />
133
- ) : (
134
- <p className="px-4 py-6 text-xs text-text-secondary text-center">
135
- Select a project below
136
- </p>
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 sectionactions within thumb reach */}
87
+ {/* Bottom tab bar — thumb-friendly */}
141
88
  <div className="shrink-0 border-t border-border">
142
- {/* New tab actions */}
143
- <div className="px-2 py-2 space-y-0.5">
144
- {NEW_TAB_OPTIONS.map((opt) => {
145
- const Icon = TAB_ICONS[opt.type];
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={opt.type}
149
- onClick={() => handleNewTab(opt.type)}
150
- className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm text-text-secondary hover:bg-surface-elevated hover:text-foreground transition-colors min-h-[40px]"
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 shrink-0" />
153
- <span>{opt.label}</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
- FolderOpen, Terminal, MessageSquare, GitBranch, GitCommitHorizontal,
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
- projects: FolderOpen, terminal: Terminal, chat: MessageSquare, editor: FileCode,
13
- "git-graph": GitBranch, "git-status": GitCommitHorizontal, "git-diff": FileDiff, settings: Settings,
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
- export function PanelLayout() {
7
- const grid = usePanelStore((s) => s.grid);
8
- const panelCount = Object.keys(usePanelStore((s) => s.panels)).length;
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
  </>