@hienlh/ppm 0.13.15 → 0.13.16
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 +22 -0
- package/CLAUDE.md +5 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +2135 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/{audio-preview-YOG6Biao.js → audio-preview-bQ4k3Rdv.js} +1 -1
- package/dist/web/assets/{chat-tab-DbdDJuLu.js → chat-tab-DISlwA7-.js} +3 -3
- package/dist/web/assets/code-editor-Cni2pSOw.js +8 -0
- package/dist/web/assets/{conflict-editor-DnGfriL5.js → conflict-editor-82mk659D.js} +1 -1
- package/dist/web/assets/{database-viewer-AodppoTs.js → database-viewer-eCnvGdDi.js} +1 -1
- package/dist/web/assets/{diff-viewer-DykLUwna.js → diff-viewer-cezBVQp6.js} +1 -1
- package/dist/web/assets/{extension-webview-Bck7QuaB.js → extension-webview-B5dN_Qrm.js} +1 -1
- package/dist/web/assets/file-store-DOxcU_7s.js +1 -0
- package/dist/web/assets/{glide-data-grid-BVt0mwcA.js → glide-data-grid-yscGXxJe.js} +1 -1
- package/dist/web/assets/{image-preview-DaSmrIvY.js → image-preview-CGdBnOP0.js} +1 -1
- package/dist/web/assets/index-C_pdjLi6.js +27 -0
- package/dist/web/assets/index-nC9UURj4.css +2 -0
- package/dist/web/assets/keybindings-store-LHrHsvXn.js +1 -0
- package/dist/web/assets/{markdown-renderer-B1me_hz2.js → markdown-renderer-DF-Ga1mN.js} +1 -1
- package/dist/web/assets/{pdf-preview-Dci7TIL1.js → pdf-preview-C15gYiMf.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BeM40G-J.js → port-forwarding-tab-BcpVh4oH.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CGVBOwA9.js → postgres-viewer-DqcY70o6.js} +1 -1
- package/dist/web/assets/{settings-tab-CYS8VfNl.js → settings-tab-CMso6o_A.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DstPySPF.js → sql-query-editor-C0Lq3NYC.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-SUGEk_G1.js → sqlite-viewer-CFHqKvjt.js} +1 -1
- package/dist/web/assets/{terminal-tab-CJvjF79J.js → terminal-tab-ej7HGI3k.js} +1 -1
- package/dist/web/assets/{video-preview-gJSKmPQr.js → video-preview-BZLGMaKk.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/server/routes/files.ts +15 -0
- package/src/services/file.service.ts +15 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +88 -36
- package/src/web/components/explorer/file-actions.tsx +12 -129
- package/src/web/components/explorer/file-icon-map.ts +69 -0
- package/src/web/components/explorer/file-tree.tsx +177 -362
- package/src/web/components/explorer/inline-tree-input.tsx +120 -0
- package/src/web/components/explorer/tree-node-context-menu.tsx +97 -0
- package/src/web/components/explorer/tree-node.tsx +343 -0
- package/src/web/components/explorer/use-file-upload-drag.ts +77 -0
- package/src/web/components/explorer/use-tree-keyboard-nav.ts +126 -0
- package/src/web/components/layout/mobile-nav.tsx +73 -84
- package/src/web/components/layout/project-bottom-sheet.tsx +61 -82
- package/src/web/components/ui/adaptive-context-menu.tsx +245 -0
- package/src/web/components/ui/mobile-bottom-sheet.tsx +155 -0
- package/src/web/hooks/use-is-mobile.ts +28 -0
- package/src/web/hooks/use-swipe-to-dismiss.ts +46 -0
- package/src/web/stores/file-store.ts +74 -3
- package/src/web/stores/git-status-store.ts +87 -2
- package/dist/web/assets/code-editor-C4nuAsy6.js +0 -8
- package/dist/web/assets/file-store-4BpOJthN.js +0 -1
- package/dist/web/assets/index-CSK33ACc.css +0 -2
- package/dist/web/assets/index-gZKF1YKy.js +0 -27
- package/dist/web/assets/keybindings-store-DBKLTPrk.js +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for file tree keyboard navigation.
|
|
3
|
+
* Arrow keys, Enter, F2, Delete on focused tree items.
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo, type KeyboardEvent } from "react";
|
|
6
|
+
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
7
|
+
|
|
8
|
+
interface UseTreeKeyboardNavOptions {
|
|
9
|
+
tree: FileNode[];
|
|
10
|
+
expandedPaths: Set<string>;
|
|
11
|
+
focusedPath: string | null;
|
|
12
|
+
setFocusedPath: (path: string | null) => void;
|
|
13
|
+
setExpanded: (path: string, expanded: boolean) => void;
|
|
14
|
+
toggleExpand: (projectName: string, path: string) => void;
|
|
15
|
+
projectName: string | undefined;
|
|
16
|
+
onAction: (action: string, node: FileNode) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useTreeKeyboardNav({
|
|
20
|
+
tree,
|
|
21
|
+
expandedPaths,
|
|
22
|
+
focusedPath,
|
|
23
|
+
setFocusedPath,
|
|
24
|
+
setExpanded,
|
|
25
|
+
toggleExpand,
|
|
26
|
+
projectName,
|
|
27
|
+
onAction,
|
|
28
|
+
}: UseTreeKeyboardNavOptions) {
|
|
29
|
+
/** Flat list of visible nodes (respects expand state and compact folders) */
|
|
30
|
+
const visibleNodes = useMemo(() => {
|
|
31
|
+
const result: FileNode[] = [];
|
|
32
|
+
function walk(nodes: FileNode[]) {
|
|
33
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
34
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
35
|
+
return a.name.localeCompare(b.name);
|
|
36
|
+
});
|
|
37
|
+
for (const n of sorted) {
|
|
38
|
+
// Skip compacted intermediate dirs (single-child chains rendered as one row)
|
|
39
|
+
let effective = n;
|
|
40
|
+
if (n.type === "directory" && expandedPaths.has(n.path) && n.children) {
|
|
41
|
+
while (
|
|
42
|
+
effective.children &&
|
|
43
|
+
effective.children.length === 1 &&
|
|
44
|
+
effective.children[0]!.type === "directory" &&
|
|
45
|
+
expandedPaths.has(effective.children[0]!.path)
|
|
46
|
+
) {
|
|
47
|
+
effective = effective.children[0]!;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
result.push(effective);
|
|
51
|
+
if (effective.type === "directory" && expandedPaths.has(effective.path) && effective.children) {
|
|
52
|
+
walk(effective.children);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
walk(tree);
|
|
57
|
+
return result;
|
|
58
|
+
}, [tree, expandedPaths]);
|
|
59
|
+
|
|
60
|
+
const focusedNode = useMemo(
|
|
61
|
+
() => visibleNodes.find((n) => n.path === focusedPath) ?? null,
|
|
62
|
+
[visibleNodes, focusedPath],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function handleTreeKeyDown(e: KeyboardEvent) {
|
|
66
|
+
if (!projectName) return;
|
|
67
|
+
const target = e.target as HTMLElement;
|
|
68
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return;
|
|
69
|
+
|
|
70
|
+
const idx = focusedPath != null ? visibleNodes.findIndex((n) => n.path === focusedPath) : -1;
|
|
71
|
+
|
|
72
|
+
switch (e.key) {
|
|
73
|
+
case "ArrowDown": {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
const next = idx < visibleNodes.length - 1 ? idx + 1 : 0;
|
|
76
|
+
setFocusedPath(visibleNodes[next]!.path);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "ArrowUp": {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const prev = idx > 0 ? idx - 1 : visibleNodes.length - 1;
|
|
82
|
+
setFocusedPath(visibleNodes[prev]!.path);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "ArrowRight": {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
if (focusedNode?.type === "directory" && !expandedPaths.has(focusedNode.path)) {
|
|
88
|
+
toggleExpand(projectName, focusedNode.path);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "ArrowLeft": {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
if (focusedNode?.type === "directory" && expandedPaths.has(focusedNode.path)) {
|
|
95
|
+
setExpanded(focusedNode.path, false);
|
|
96
|
+
} else if (focusedNode) {
|
|
97
|
+
const parentPath = focusedNode.path.includes("/")
|
|
98
|
+
? focusedNode.path.slice(0, focusedNode.path.lastIndexOf("/"))
|
|
99
|
+
: "";
|
|
100
|
+
if (parentPath || parentPath === "") {
|
|
101
|
+
const parent = visibleNodes.find((n) => n.path === parentPath);
|
|
102
|
+
if (parent) setFocusedPath(parent.path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "Enter": {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (focusedNode) onAction(focusedNode.type === "directory" ? "toggle-expand" : "open-file", focusedNode);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "F2": {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
if (focusedNode) onAction("rename", focusedNode);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "Delete": {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
if (focusedNode) onAction("delete", focusedNode);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { visibleNodes, focusedNode, handleTreeKeyDown };
|
|
126
|
+
}
|
|
@@ -20,6 +20,7 @@ import { useTabOverflow, getHiddenUnreadDirection } from "@/hooks/use-tab-overfl
|
|
|
20
20
|
import { downloadFile } from "@/lib/file-download";
|
|
21
21
|
import { FileActions } from "@/components/explorer/file-actions";
|
|
22
22
|
import { api, projectUrl } from "@/lib/api-client";
|
|
23
|
+
import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
23
24
|
|
|
24
25
|
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
25
26
|
{ type: "terminal", label: "Terminal" },
|
|
@@ -300,91 +301,79 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
300
301
|
</div>
|
|
301
302
|
|
|
302
303
|
{/* New tab action sheet */}
|
|
303
|
-
{newTabSheetOpen
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
);
|
|
319
|
-
})}
|
|
320
|
-
</div>
|
|
321
|
-
</>
|
|
322
|
-
)}
|
|
304
|
+
<BottomSheet open={newTabSheetOpen} onClose={() => setNewTabSheetOpen(false)}>
|
|
305
|
+
<div className="px-3 py-2 text-xs text-text-secondary border-b border-border">New Tab</div>
|
|
306
|
+
{NEW_TAB_OPTIONS.map((opt) => {
|
|
307
|
+
const Icon = TAB_ICONS[opt.type];
|
|
308
|
+
return (
|
|
309
|
+
<button
|
|
310
|
+
key={opt.type}
|
|
311
|
+
onClick={() => handleNewTab(opt.type)}
|
|
312
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated"
|
|
313
|
+
>
|
|
314
|
+
<Icon className="size-4" /> {opt.label}
|
|
315
|
+
</button>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
</BottomSheet>
|
|
323
319
|
|
|
324
|
-
{/* Long-press action sheet */}
|
|
325
|
-
{menuTab
|
|
326
|
-
|
|
327
|
-
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
382
|
-
<MoveVertical className="size-4" /> Move to Panel {i + 1 === 1 ? "Top" : "Bottom"}
|
|
383
|
-
</button>
|
|
384
|
-
))}
|
|
385
|
-
</div>
|
|
386
|
-
</>
|
|
387
|
-
)}
|
|
320
|
+
{/* Long-press tab action sheet */}
|
|
321
|
+
<BottomSheet open={!!menuTab} onClose={() => setMenuTabId(null)}>
|
|
322
|
+
<div className="px-3 py-2 text-xs text-text-secondary border-b border-border truncate">
|
|
323
|
+
{menuTab?.title}
|
|
324
|
+
</div>
|
|
325
|
+
{menuTab?.type === "editor" && (
|
|
326
|
+
<>
|
|
327
|
+
<button onClick={() => handleFileAction(menuTab, "copy-path")}
|
|
328
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
329
|
+
<Copy className="size-4" /> Copy Path
|
|
330
|
+
</button>
|
|
331
|
+
<button onClick={() => handleFileAction(menuTab, "download")}
|
|
332
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
333
|
+
<Download className="size-4" /> Download
|
|
334
|
+
</button>
|
|
335
|
+
<button onClick={() => handleFileAction(menuTab, "rename")}
|
|
336
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
337
|
+
<Pencil className="size-4" /> Rename
|
|
338
|
+
</button>
|
|
339
|
+
<button onClick={() => handleFileAction(menuTab, "delete")}
|
|
340
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-error active:bg-surface-elevated">
|
|
341
|
+
<Trash2 className="size-4" /> Delete
|
|
342
|
+
</button>
|
|
343
|
+
<div className="h-px bg-border mx-2" />
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
{menuTab?.closable && (
|
|
347
|
+
<button onClick={() => { usePanelStore.getState().closeTab(menuTabId!); setMenuTabId(null); }}
|
|
348
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
349
|
+
<X className="size-4" /> Close
|
|
350
|
+
</button>
|
|
351
|
+
)}
|
|
352
|
+
{menuTabIdx > 0 && (
|
|
353
|
+
<button onClick={() => { moveTabLeft(menuTabId!); setMenuTabId(null); }}
|
|
354
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
355
|
+
<ArrowLeft className="size-4" /> Move Left
|
|
356
|
+
</button>
|
|
357
|
+
)}
|
|
358
|
+
{menuTabIdx < menuTabPanelTabs.length - 1 && (
|
|
359
|
+
<button onClick={() => { moveTabRight(menuTabId!); setMenuTabId(null); }}
|
|
360
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
361
|
+
<ArrowRight className="size-4" /> Move Right
|
|
362
|
+
</button>
|
|
363
|
+
)}
|
|
364
|
+
{canSplitDown && menuTabPanelTabs.length > 1 && (
|
|
365
|
+
<button onClick={() => { splitDown(menuTabId!); setMenuTabId(null); }}
|
|
366
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
367
|
+
<SplitSquareVertical className="size-4" /> Split to Bottom
|
|
368
|
+
</button>
|
|
369
|
+
)}
|
|
370
|
+
{otherPanelIds.map((pid, i) => (
|
|
371
|
+
<button key={pid} onClick={() => { moveToPanel(menuTabId!, pid); setMenuTabId(null); }}
|
|
372
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
373
|
+
<MoveVertical className="size-4" /> Move to Panel {i + 1 === 1 ? "Top" : "Bottom"}
|
|
374
|
+
</button>
|
|
375
|
+
))}
|
|
376
|
+
</BottomSheet>
|
|
388
377
|
|
|
389
378
|
{fileActionState && (
|
|
390
379
|
<FileActions
|
|
@@ -8,6 +8,7 @@ import { AddProjectForm } from "@/components/layout/add-project-form";
|
|
|
8
8
|
import { resolveProjectColor, PROJECT_PALETTE } from "@/lib/project-palette";
|
|
9
9
|
import { getProjectInitials } from "@/lib/project-avatar";
|
|
10
10
|
import { cn } from "@/lib/utils";
|
|
11
|
+
import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
11
12
|
|
|
12
13
|
interface ProjectBottomSheetProps {
|
|
13
14
|
isOpen: boolean;
|
|
@@ -170,29 +171,8 @@ export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps)
|
|
|
170
171
|
|
|
171
172
|
return (
|
|
172
173
|
<>
|
|
173
|
-
{/*
|
|
174
|
-
<
|
|
175
|
-
className={cn(
|
|
176
|
-
"fixed inset-0 z-50 md:hidden transition-opacity duration-200",
|
|
177
|
-
isOpen ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
178
|
-
)}
|
|
179
|
-
onClick={handleClose}
|
|
180
|
-
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
181
|
-
/>
|
|
182
|
-
|
|
183
|
-
{/* Sheet */}
|
|
184
|
-
<div
|
|
185
|
-
className={cn(
|
|
186
|
-
"fixed bottom-0 left-0 right-0 z-50 md:hidden bg-background rounded-t-2xl border-t border-border shadow-2xl",
|
|
187
|
-
"transition-transform duration-300 ease-out",
|
|
188
|
-
isOpen ? "translate-y-0" : "translate-y-full",
|
|
189
|
-
)}
|
|
190
|
-
>
|
|
191
|
-
{/* Drag handle */}
|
|
192
|
-
<div className="flex justify-center pt-3 pb-1">
|
|
193
|
-
<div className="w-10 h-1 rounded-full bg-border" />
|
|
194
|
-
</div>
|
|
195
|
-
|
|
174
|
+
{/* Main project sheet */}
|
|
175
|
+
<BottomSheet open={isOpen} onClose={handleClose} className="bg-background">
|
|
196
176
|
{/* Header */}
|
|
197
177
|
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
198
178
|
<div className="flex items-center gap-2">
|
|
@@ -293,69 +273,68 @@ export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps)
|
|
|
293
273
|
</button>
|
|
294
274
|
{version && <span className="text-xs text-text-subtle">v{version}</span>}
|
|
295
275
|
</div>
|
|
296
|
-
</
|
|
276
|
+
</BottomSheet>
|
|
297
277
|
|
|
298
278
|
{/* Long-press action sheet */}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)}
|
|
279
|
+
<BottomSheet
|
|
280
|
+
open={!!actionTarget && !colorPickerOpen}
|
|
281
|
+
onClose={() => setActionTarget(null)}
|
|
282
|
+
zIndex={60}
|
|
283
|
+
>
|
|
284
|
+
<div className="px-4 py-2 border-b border-border">
|
|
285
|
+
<p className="text-xs font-medium text-text-secondary">{actionTarget}</p>
|
|
286
|
+
</div>
|
|
287
|
+
{actionItems.map((item) => {
|
|
288
|
+
const Icon = item.icon;
|
|
289
|
+
return (
|
|
290
|
+
<button
|
|
291
|
+
key={item.label}
|
|
292
|
+
onClick={item.onClick}
|
|
293
|
+
className={cn(
|
|
294
|
+
"w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors active:bg-surface-elevated",
|
|
295
|
+
item.destructive ? "text-destructive" : "text-foreground",
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
<Icon className="size-4 shrink-0" />
|
|
299
|
+
{item.label}
|
|
300
|
+
</button>
|
|
301
|
+
);
|
|
302
|
+
})}
|
|
303
|
+
</BottomSheet>
|
|
325
304
|
|
|
326
305
|
{/* Color picker sheet */}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
</
|
|
357
|
-
|
|
358
|
-
|
|
306
|
+
<BottomSheet
|
|
307
|
+
open={colorPickerOpen && !!actionTarget}
|
|
308
|
+
onClose={() => { setColorPickerOpen(false); setActionTarget(null); }}
|
|
309
|
+
zIndex={60}
|
|
310
|
+
className="p-4 space-y-4"
|
|
311
|
+
>
|
|
312
|
+
<p className="text-sm font-medium">Change Color</p>
|
|
313
|
+
<div className="flex flex-wrap gap-3">
|
|
314
|
+
{PROJECT_PALETTE.map((c) => (
|
|
315
|
+
<button
|
|
316
|
+
key={c}
|
|
317
|
+
type="button"
|
|
318
|
+
onClick={() => setActionColor(c)}
|
|
319
|
+
className={cn(
|
|
320
|
+
"size-9 rounded-full border-2 transition-all",
|
|
321
|
+
actionColor === c ? "border-primary scale-110" : "border-transparent",
|
|
322
|
+
)}
|
|
323
|
+
style={{ background: c }}
|
|
324
|
+
/>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
<div className="flex gap-2 pt-2">
|
|
328
|
+
<button
|
|
329
|
+
onClick={() => { setColorPickerOpen(false); setActionTarget(null); }}
|
|
330
|
+
className="flex-1 py-2 text-sm text-text-secondary border border-border rounded-md"
|
|
331
|
+
>Cancel</button>
|
|
332
|
+
<button
|
|
333
|
+
onClick={() => handleColorSave(actionTarget!, actionColor)}
|
|
334
|
+
className="flex-1 py-2 text-sm bg-primary text-white rounded-md"
|
|
335
|
+
>Save</button>
|
|
336
|
+
</div>
|
|
337
|
+
</BottomSheet>
|
|
359
338
|
</>
|
|
360
339
|
);
|
|
361
340
|
}
|