@hienlh/ppm 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/web/assets/{chat-tab-C6iTYbRI.js → chat-tab-d_HzPDhE.js} +2 -2
- package/dist/web/assets/{code-editor-hnDDc8JZ.js → code-editor-DFAu3knd.js} +1 -1
- package/dist/web/assets/{diff-viewer-BWeMVAvK.js → diff-viewer-Bue0mOJY.js} +1 -1
- package/dist/web/assets/{git-graph-D6oftHHC.js → git-graph-Cjq-lK5h.js} +1 -1
- package/dist/web/assets/index-D_IIxtVN.js +21 -0
- package/dist/web/assets/index-DhsWierF.css +2 -0
- package/dist/web/assets/{markdown-renderer-PHBaNQ3l.js → markdown-renderer-B9l76G5h.js} +1 -1
- package/dist/web/assets/{settings-tab-BpETyigv.js → settings-tab-BDPgdHPI.js} +1 -1
- package/dist/web/assets/{terminal-tab-BTumEYyO.js → terminal-tab-BEOvTEai.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/web/app.tsx +10 -1
- package/src/web/components/chat/chat-history-bar.tsx +0 -11
- package/src/web/components/explorer/file-tree.tsx +8 -0
- package/src/web/components/layout/mobile-drawer.tsx +16 -9
- package/src/web/components/layout/project-bar.tsx +26 -16
- package/src/web/components/layout/project-bottom-sheet.tsx +19 -6
- package/src/web/components/layout/sidebar.tsx +1 -1
- package/src/web/components/settings/settings-tab.tsx +16 -1
- package/src/web/hooks/use-push-notification.ts +25 -7
- package/src/web/stores/project-store.ts +7 -0
- package/dist/web/assets/index-CWwJBtaO.js +0 -21
- package/dist/web/assets/index-jmj5f_bQ.css +0 -2
|
@@ -34,7 +34,8 @@ function ProjectAvatar({ name, color, allNames }: { name: string; color: string;
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps) {
|
|
37
|
-
const { projects, activeProject, setActiveProject, setProjectColor,
|
|
37
|
+
const { projects, activeProject, setActiveProject, setProjectColor, reorderProjects, renameProject, deleteProject, customOrder } = useProjectStore();
|
|
38
|
+
|
|
38
39
|
const openTab = useTabStore((s) => s.openTab);
|
|
39
40
|
const version = useSettingsStore((s) => s.version);
|
|
40
41
|
|
|
@@ -77,10 +78,16 @@ export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps)
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function handleSettings() {
|
|
81
|
+
handleClose();
|
|
82
|
+
// Mobile: open drawer with settings tab
|
|
83
|
+
if (window.innerWidth < 768) {
|
|
84
|
+
window.dispatchEvent(new Event("open-mobile-settings"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Desktop: open sidebar settings tab
|
|
80
88
|
const { sidebarCollapsed, toggleSidebar, setSidebarActiveTab } = useSettingsStore.getState();
|
|
81
89
|
if (sidebarCollapsed) toggleSidebar();
|
|
82
90
|
setSidebarActiveTab("settings");
|
|
83
|
-
handleClose();
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
async function handleRename() {
|
|
@@ -133,16 +140,22 @@ export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps)
|
|
|
133
140
|
...(actionIdx > 0 ? [{
|
|
134
141
|
label: "Move Up",
|
|
135
142
|
icon: ChevronUp,
|
|
136
|
-
onClick:
|
|
137
|
-
|
|
143
|
+
onClick: () => {
|
|
144
|
+
const names = ordered.map((p) => p.name);
|
|
145
|
+
const [moved] = names.splice(actionIdx, 1);
|
|
146
|
+
names.splice(actionIdx - 1, 0, moved!);
|
|
147
|
+
reorderProjects(names);
|
|
138
148
|
setActionTarget(null);
|
|
139
149
|
},
|
|
140
150
|
}] : []),
|
|
141
151
|
...(actionIdx < ordered.length - 1 ? [{
|
|
142
152
|
label: "Move Down",
|
|
143
153
|
icon: ChevronDown,
|
|
144
|
-
onClick:
|
|
145
|
-
|
|
154
|
+
onClick: () => {
|
|
155
|
+
const names = ordered.map((p) => p.name);
|
|
156
|
+
const [moved] = names.splice(actionIdx, 1);
|
|
157
|
+
names.splice(actionIdx + 1, 0, moved!);
|
|
158
|
+
reorderProjects(names);
|
|
146
159
|
setActionTarget(null);
|
|
147
160
|
},
|
|
148
161
|
}] : []),
|
|
@@ -18,7 +18,7 @@ const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
|
|
|
18
18
|
|
|
19
19
|
export function SettingsTab() {
|
|
20
20
|
const { theme, setTheme } = useSettingsStore();
|
|
21
|
-
const { permission, isSubscribed, loading, subscribe, unsubscribe } = usePushNotification();
|
|
21
|
+
const { permission, isSubscribed, loading, error: pushError, subscribe, unsubscribe } = usePushNotification();
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<div className="h-full w-full overflow-auto">
|
|
@@ -78,6 +78,21 @@ export function SettingsTab() {
|
|
|
78
78
|
{loading ? "..." : isSubscribed ? "On" : "Off"}
|
|
79
79
|
</Button>
|
|
80
80
|
</div>
|
|
81
|
+
{isSubscribed && (
|
|
82
|
+
<Button
|
|
83
|
+
variant="outline"
|
|
84
|
+
size="sm"
|
|
85
|
+
className="h-7 text-xs w-full"
|
|
86
|
+
onClick={() => {
|
|
87
|
+
new Notification("PPM Test", { body: "Push notifications are working!" });
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
Test notification
|
|
91
|
+
</Button>
|
|
92
|
+
)}
|
|
93
|
+
{pushError && (
|
|
94
|
+
<p className="text-[11px] text-destructive">{pushError}</p>
|
|
95
|
+
)}
|
|
81
96
|
{permission === "denied" && (
|
|
82
97
|
<p className="text-[11px] text-destructive">
|
|
83
98
|
Notifications blocked. Enable in browser settings.
|
|
@@ -13,6 +13,7 @@ export function usePushNotification() {
|
|
|
13
13
|
const [permission, setPermission] = useState<NotificationPermission>("default");
|
|
14
14
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
15
15
|
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
17
|
|
|
17
18
|
// Check current permission and subscription state on mount
|
|
18
19
|
useEffect(() => {
|
|
@@ -24,13 +25,29 @@ export function usePushNotification() {
|
|
|
24
25
|
|
|
25
26
|
const subscribe = useCallback(async () => {
|
|
26
27
|
setLoading(true);
|
|
28
|
+
setError(null);
|
|
27
29
|
try {
|
|
28
30
|
// 1. Request notification permission
|
|
29
31
|
const perm = await Notification.requestPermission();
|
|
30
32
|
setPermission(perm);
|
|
31
|
-
if (perm !== "granted")
|
|
33
|
+
if (perm !== "granted") {
|
|
34
|
+
setError("Permission denied");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Check service worker is available (with timeout)
|
|
39
|
+
if (!navigator.serviceWorker?.controller && !navigator.serviceWorker?.ready) {
|
|
40
|
+
setError("Service worker not available (dev mode?)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
const swReady = await Promise.race([
|
|
45
|
+
navigator.serviceWorker.ready,
|
|
46
|
+
new Promise<null>((_, reject) => setTimeout(() => reject(new Error("Service worker timeout")), 5000)),
|
|
47
|
+
]);
|
|
48
|
+
if (!swReady) throw new Error("Service worker not ready");
|
|
49
|
+
|
|
50
|
+
// 3. Get VAPID public key from server
|
|
34
51
|
const headers: Record<string, string> = {};
|
|
35
52
|
const token = getAuthToken();
|
|
36
53
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
@@ -39,14 +56,13 @@ export function usePushNotification() {
|
|
|
39
56
|
const json = await res.json();
|
|
40
57
|
if (!json.ok) throw new Error(json.error || "Failed to get VAPID key");
|
|
41
58
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
const sub = await reg.pushManager.subscribe({
|
|
59
|
+
// 4. Subscribe via PushManager
|
|
60
|
+
const sub = await swReady.pushManager.subscribe({
|
|
45
61
|
userVisibleOnly: true,
|
|
46
62
|
applicationServerKey: urlBase64ToUint8Array(json.data.publicKey).buffer as ArrayBuffer,
|
|
47
63
|
});
|
|
48
64
|
|
|
49
|
-
//
|
|
65
|
+
// 5. Send subscription to server
|
|
50
66
|
await fetch("/api/push/subscribe", {
|
|
51
67
|
method: "POST",
|
|
52
68
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
@@ -56,6 +72,8 @@ export function usePushNotification() {
|
|
|
56
72
|
setIsSubscribed(true);
|
|
57
73
|
localStorage.setItem("ppm-push-subscribed", "true");
|
|
58
74
|
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : "Subscribe failed";
|
|
76
|
+
setError(msg);
|
|
59
77
|
console.error("[push] Subscribe failed:", err);
|
|
60
78
|
} finally {
|
|
61
79
|
setLoading(false);
|
|
@@ -92,5 +110,5 @@ export function usePushNotification() {
|
|
|
92
110
|
}
|
|
93
111
|
}, []);
|
|
94
112
|
|
|
95
|
-
return { permission, isSubscribed, loading, subscribe, unsubscribe };
|
|
113
|
+
return { permission, isSubscribed, loading, error, subscribe, unsubscribe };
|
|
96
114
|
}
|
|
@@ -92,6 +92,7 @@ interface ProjectStore {
|
|
|
92
92
|
addProject: (path: string, name?: string) => Promise<ProjectInfo>;
|
|
93
93
|
setProjectColor: (name: string, color: string | null) => Promise<void>;
|
|
94
94
|
moveProject: (name: string, direction: "up" | "down") => Promise<void>;
|
|
95
|
+
reorderProjects: (newOrder: string[]) => Promise<void>;
|
|
95
96
|
renameProject: (name: string, newName: string) => Promise<void>;
|
|
96
97
|
deleteProject: (name: string) => Promise<void>;
|
|
97
98
|
}
|
|
@@ -161,6 +162,12 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|
|
161
162
|
set({ customOrder: newOrder });
|
|
162
163
|
},
|
|
163
164
|
|
|
165
|
+
reorderProjects: async (newOrder) => {
|
|
166
|
+
saveCustomOrder(newOrder);
|
|
167
|
+
set({ customOrder: newOrder });
|
|
168
|
+
await api.patch("/api/projects/reorder", { order: newOrder }).catch(() => {});
|
|
169
|
+
},
|
|
170
|
+
|
|
164
171
|
renameProject: async (name, newName) => {
|
|
165
172
|
await api.patch(`/api/projects/${encodeURIComponent(name)}`, { name: newName });
|
|
166
173
|
// Refetch to get updated list
|