@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CLAUDE.md +5 -0
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/bun.lock +2135 -0
  6. package/bunfig.toml +2 -0
  7. package/dist/web/assets/{audio-preview-YOG6Biao.js → audio-preview-bQ4k3Rdv.js} +1 -1
  8. package/dist/web/assets/{chat-tab-DbdDJuLu.js → chat-tab-DISlwA7-.js} +3 -3
  9. package/dist/web/assets/code-editor-Cni2pSOw.js +8 -0
  10. package/dist/web/assets/{conflict-editor-DnGfriL5.js → conflict-editor-82mk659D.js} +1 -1
  11. package/dist/web/assets/{database-viewer-AodppoTs.js → database-viewer-eCnvGdDi.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-DykLUwna.js → diff-viewer-cezBVQp6.js} +1 -1
  13. package/dist/web/assets/{extension-webview-Bck7QuaB.js → extension-webview-B5dN_Qrm.js} +1 -1
  14. package/dist/web/assets/file-store-DOxcU_7s.js +1 -0
  15. package/dist/web/assets/{glide-data-grid-BVt0mwcA.js → glide-data-grid-yscGXxJe.js} +1 -1
  16. package/dist/web/assets/{image-preview-DaSmrIvY.js → image-preview-CGdBnOP0.js} +1 -1
  17. package/dist/web/assets/index-C_pdjLi6.js +27 -0
  18. package/dist/web/assets/index-nC9UURj4.css +2 -0
  19. package/dist/web/assets/keybindings-store-LHrHsvXn.js +1 -0
  20. package/dist/web/assets/{markdown-renderer-B1me_hz2.js → markdown-renderer-DF-Ga1mN.js} +1 -1
  21. package/dist/web/assets/{pdf-preview-Dci7TIL1.js → pdf-preview-C15gYiMf.js} +1 -1
  22. package/dist/web/assets/{port-forwarding-tab-BeM40G-J.js → port-forwarding-tab-BcpVh4oH.js} +1 -1
  23. package/dist/web/assets/{postgres-viewer-CGVBOwA9.js → postgres-viewer-DqcY70o6.js} +1 -1
  24. package/dist/web/assets/{settings-tab-CYS8VfNl.js → settings-tab-CMso6o_A.js} +1 -1
  25. package/dist/web/assets/{sql-query-editor-DstPySPF.js → sql-query-editor-C0Lq3NYC.js} +1 -1
  26. package/dist/web/assets/{sqlite-viewer-SUGEk_G1.js → sqlite-viewer-CFHqKvjt.js} +1 -1
  27. package/dist/web/assets/{terminal-tab-CJvjF79J.js → terminal-tab-ej7HGI3k.js} +1 -1
  28. package/dist/web/assets/{video-preview-gJSKmPQr.js → video-preview-BZLGMaKk.js} +1 -1
  29. package/dist/web/index.html +3 -3
  30. package/dist/web/sw.js +1 -1
  31. package/package.json +1 -1
  32. package/src/index.ts +0 -0
  33. package/src/server/routes/files.ts +15 -0
  34. package/src/services/file.service.ts +15 -0
  35. package/src/web/components/editor/editor-breadcrumb.tsx +88 -36
  36. package/src/web/components/explorer/file-actions.tsx +12 -129
  37. package/src/web/components/explorer/file-icon-map.ts +69 -0
  38. package/src/web/components/explorer/file-tree.tsx +177 -362
  39. package/src/web/components/explorer/inline-tree-input.tsx +120 -0
  40. package/src/web/components/explorer/tree-node-context-menu.tsx +97 -0
  41. package/src/web/components/explorer/tree-node.tsx +343 -0
  42. package/src/web/components/explorer/use-file-upload-drag.ts +77 -0
  43. package/src/web/components/explorer/use-tree-keyboard-nav.ts +126 -0
  44. package/src/web/components/layout/mobile-nav.tsx +73 -84
  45. package/src/web/components/layout/project-bottom-sheet.tsx +61 -82
  46. package/src/web/components/ui/adaptive-context-menu.tsx +245 -0
  47. package/src/web/components/ui/mobile-bottom-sheet.tsx +155 -0
  48. package/src/web/hooks/use-is-mobile.ts +28 -0
  49. package/src/web/hooks/use-swipe-to-dismiss.ts +46 -0
  50. package/src/web/stores/file-store.ts +74 -3
  51. package/src/web/stores/git-status-store.ts +87 -2
  52. package/dist/web/assets/code-editor-C4nuAsy6.js +0 -8
  53. package/dist/web/assets/file-store-4BpOJthN.js +0 -1
  54. package/dist/web/assets/index-CSK33ACc.css +0 -2
  55. package/dist/web/assets/index-gZKF1YKy.js +0 -27
  56. 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
- <div className="fixed inset-0 z-50" onClick={() => setNewTabSheetOpen(false)} />
306
- <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">
307
- <div className="px-3 py-2 text-xs text-text-secondary border-b border-border">New Tab</div>
308
- {NEW_TAB_OPTIONS.map((opt) => {
309
- const Icon = TAB_ICONS[opt.type];
310
- return (
311
- <button
312
- key={opt.type}
313
- onClick={() => handleNewTab(opt.type)}
314
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated"
315
- >
316
- <Icon className="size-4" /> {opt.label}
317
- </button>
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
- {/* Backdrop */}
328
- <div className="fixed inset-0 z-50" onClick={() => setMenuTabId(null)} />
329
- {/* Action sheet */}
330
- <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">
331
- <div className="px-3 py-2 text-xs text-text-secondary border-b border-border truncate">
332
- {menuTab.title}
333
- </div>
334
- {menuTab.type === "editor" && (
335
- <>
336
- <button onClick={() => handleFileAction(menuTab, "copy-path")}
337
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
338
- <Copy className="size-4" /> Copy Path
339
- </button>
340
- <button onClick={() => handleFileAction(menuTab, "download")}
341
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
342
- <Download className="size-4" /> Download
343
- </button>
344
- <button onClick={() => handleFileAction(menuTab, "rename")}
345
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
346
- <Pencil className="size-4" /> Rename
347
- </button>
348
- <button onClick={() => handleFileAction(menuTab, "delete")}
349
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-error active:bg-surface-elevated">
350
- <Trash2 className="size-4" /> Delete
351
- </button>
352
- <div className="h-px bg-border mx-2" />
353
- </>
354
- )}
355
- {menuTab.closable && (
356
- <button onClick={() => { usePanelStore.getState().closeTab(menuTabId!); setMenuTabId(null); }}
357
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
358
- <X className="size-4" /> Close
359
- </button>
360
- )}
361
- {menuTabIdx > 0 && (
362
- <button onClick={() => { moveTabLeft(menuTabId!); setMenuTabId(null); }}
363
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
364
- <ArrowLeft className="size-4" /> Move Left
365
- </button>
366
- )}
367
- {menuTabIdx < menuTabPanelTabs.length - 1 && (
368
- <button onClick={() => { moveTabRight(menuTabId!); setMenuTabId(null); }}
369
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
370
- <ArrowRight className="size-4" /> Move Right
371
- </button>
372
- )}
373
- {canSplitDown && menuTabPanelTabs.length > 1 && (
374
- <button onClick={() => { splitDown(menuTabId!); setMenuTabId(null); }}
375
- className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
376
- <SplitSquareVertical className="size-4" /> Split to Bottom
377
- </button>
378
- )}
379
- {otherPanelIds.map((pid, i) => (
380
- <button key={pid} onClick={() => { moveToPanel(menuTabId!, pid); setMenuTabId(null); }}
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
- {/* Backdrop */}
174
- <div
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
- </div>
276
+ </BottomSheet>
297
277
 
298
278
  {/* Long-press action sheet */}
299
- {actionTarget && !colorPickerOpen && (
300
- <>
301
- <div className="fixed inset-0 z-[60] md:hidden" onClick={() => setActionTarget(null)} />
302
- <div className="fixed bottom-0 left-0 right-0 z-[61] md:hidden bg-surface border-t border-border rounded-t-2xl shadow-2xl animate-in slide-in-from-bottom-2 duration-150">
303
- <div className="px-4 py-2 border-b border-border">
304
- <p className="text-xs font-medium text-text-secondary">{actionTarget}</p>
305
- </div>
306
- {actionItems.map((item) => {
307
- const Icon = item.icon;
308
- return (
309
- <button
310
- key={item.label}
311
- onClick={item.onClick}
312
- className={cn(
313
- "w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors active:bg-surface-elevated",
314
- item.destructive ? "text-destructive" : "text-foreground",
315
- )}
316
- >
317
- <Icon className="size-4 shrink-0" />
318
- {item.label}
319
- </button>
320
- );
321
- })}
322
- </div>
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
- {colorPickerOpen && actionTarget && (
328
- <>
329
- <div className="fixed inset-0 z-[60] md:hidden" onClick={() => { setColorPickerOpen(false); setActionTarget(null); }} />
330
- <div className="fixed bottom-0 left-0 right-0 z-[61] md:hidden bg-surface border-t border-border rounded-t-2xl shadow-2xl p-4 space-y-4">
331
- <p className="text-sm font-medium">Change Color</p>
332
- <div className="flex flex-wrap gap-3">
333
- {PROJECT_PALETTE.map((c) => (
334
- <button
335
- key={c}
336
- type="button"
337
- onClick={() => setActionColor(c)}
338
- className={cn(
339
- "size-9 rounded-full border-2 transition-all",
340
- actionColor === c ? "border-primary scale-110" : "border-transparent",
341
- )}
342
- style={{ background: c }}
343
- />
344
- ))}
345
- </div>
346
- <div className="flex gap-2 pt-2">
347
- <button
348
- onClick={() => { setColorPickerOpen(false); setActionTarget(null); }}
349
- className="flex-1 py-2 text-sm text-text-secondary border border-border rounded-md"
350
- >Cancel</button>
351
- <button
352
- onClick={() => handleColorSave(actionTarget, actionColor)}
353
- className="flex-1 py-2 text-sm bg-primary text-white rounded-md"
354
- >Save</button>
355
- </div>
356
- </div>
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
  }