@xenide-io/the-old-ui-theme 0.9.13 → 0.9.14
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/dist/suite.d.mts +49 -4
- package/dist/suite.d.ts +49 -4
- package/dist/suite.js +892 -778
- package/dist/suite.mjs +617 -498
- package/package.json +1 -1
- package/src/suite/components/suite-integration-picker.test.tsx +36 -2
- package/src/suite/components/suite-integration-picker.tsx +104 -72
- package/src/suite/components/suite-notification-bell.test.tsx +12 -8
- package/src/suite/components/suite-notification-bell.tsx +23 -170
- package/src/suite/index.ts +7 -5
- package/src/suite/lib/suite-notifications.tsx +282 -0
package/src/suite/index.ts
CHANGED
|
@@ -10,11 +10,15 @@ export {
|
|
|
10
10
|
type SuiteAppEntry,
|
|
11
11
|
type SuiteAppSelectOptions,
|
|
12
12
|
} from "./components/app-switcher";
|
|
13
|
+
export { SuiteNotificationBell } from "./components/suite-notification-bell";
|
|
13
14
|
export {
|
|
14
|
-
|
|
15
|
+
SuiteNotificationsProvider,
|
|
16
|
+
useSuiteNotifications,
|
|
17
|
+
SUITE_NOTIFICATIONS_POLL_MS,
|
|
15
18
|
type SuiteNotification,
|
|
16
19
|
type SuiteNotificationsResponse,
|
|
17
|
-
|
|
20
|
+
type SuiteNotificationsContextValue,
|
|
21
|
+
} from "./lib/suite-notifications";
|
|
18
22
|
export { CommandPaletteHost } from "./components/command-palette-host";
|
|
19
23
|
export { DeferredChrome } from "./components/deferred-chrome";
|
|
20
24
|
export { SuiteMobileDrawer } from "./components/suite-mobile-drawer";
|
|
@@ -174,9 +178,7 @@ export {
|
|
|
174
178
|
type SuiteDirectoryProject,
|
|
175
179
|
type SuiteDirectoryProjectFields,
|
|
176
180
|
} from "./components/suite-clients-projects-directory";
|
|
177
|
-
export {
|
|
178
|
-
SuiteEntityIcon,
|
|
179
|
-
} from "./components/suite-entity-icon";
|
|
181
|
+
export { SuiteEntityIcon } from "./components/suite-entity-icon";
|
|
180
182
|
export {
|
|
181
183
|
SuitePaletteLibrary,
|
|
182
184
|
type SuitePalettePick,
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { useRouter } from "next/navigation";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
resolveSuiteNotificationHref,
|
|
16
|
+
suiteAppBaseUrl,
|
|
17
|
+
suiteAppSlugForNotificationSource,
|
|
18
|
+
} from "./apps";
|
|
19
|
+
|
|
20
|
+
/** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */
|
|
21
|
+
export interface SuiteNotification {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
href: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
source_app: string;
|
|
28
|
+
read_at: string | null;
|
|
29
|
+
created_at: string;
|
|
30
|
+
workspace: string | null;
|
|
31
|
+
organisation: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SuiteNotificationsResponse {
|
|
35
|
+
notifications: SuiteNotification[];
|
|
36
|
+
unread_count: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SuiteNotificationsContextValue {
|
|
40
|
+
notifications: SuiteNotification[];
|
|
41
|
+
unreadCount: number;
|
|
42
|
+
open: boolean;
|
|
43
|
+
setOpen: (open: boolean) => void;
|
|
44
|
+
refresh: () => void;
|
|
45
|
+
select: (notification: SuiteNotification) => void;
|
|
46
|
+
markAllRead: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const SUITE_NOTIFICATIONS_POLL_MS = 60_000;
|
|
50
|
+
|
|
51
|
+
const SuiteNotificationsContext =
|
|
52
|
+
createContext<SuiteNotificationsContextValue | null>(null);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read the shared notification state. Must be rendered inside a
|
|
56
|
+
* `SuiteNotificationsProvider`: one provider per app owns the single poller,
|
|
57
|
+
* however many bells consume it.
|
|
58
|
+
*/
|
|
59
|
+
export function useSuiteNotifications(): SuiteNotificationsContextValue {
|
|
60
|
+
const value = useContext(SuiteNotificationsContext);
|
|
61
|
+
if (!value) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"useSuiteNotifications must be used within a SuiteNotificationsProvider",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Application-layer notification runtime. Owns fetching, polling cadence,
|
|
71
|
+
* tab-visibility gating, stale-while-revalidate caching, optimistic read
|
|
72
|
+
* state, and cross-app navigation. The UI (SuiteNotificationBell) stays
|
|
73
|
+
* presentational and consumes this context, so multiple bells share one
|
|
74
|
+
* request per interval instead of polling independently.
|
|
75
|
+
*/
|
|
76
|
+
export function SuiteNotificationsProvider({
|
|
77
|
+
fetchNotifications,
|
|
78
|
+
markRead,
|
|
79
|
+
markAllRead: markAllReadRequest,
|
|
80
|
+
pollMs = SUITE_NOTIFICATIONS_POLL_MS,
|
|
81
|
+
cacheKey = "suite-notifications-cache",
|
|
82
|
+
enabled = true,
|
|
83
|
+
children,
|
|
84
|
+
}: {
|
|
85
|
+
fetchNotifications: () => Promise<SuiteNotificationsResponse>;
|
|
86
|
+
markRead: (id: string) => Promise<unknown>;
|
|
87
|
+
markAllRead: () => Promise<unknown>;
|
|
88
|
+
/** Poll cadence while the tab is visible. Defaults to 60s. */
|
|
89
|
+
pollMs?: number;
|
|
90
|
+
/** sessionStorage key for stale-while-revalidate; `null` disables caching. */
|
|
91
|
+
cacheKey?: string | null;
|
|
92
|
+
/** Set false to suspend fetching entirely (e.g. before auth resolves). */
|
|
93
|
+
enabled?: boolean;
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
}) {
|
|
96
|
+
const router = useRouter();
|
|
97
|
+
const [notifications, setNotifications] = useState<SuiteNotification[]>([]);
|
|
98
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
99
|
+
const [open, setOpen] = useState(false);
|
|
100
|
+
|
|
101
|
+
const load = useCallback(async () => {
|
|
102
|
+
try {
|
|
103
|
+
const data = await fetchNotifications();
|
|
104
|
+
setNotifications(data.notifications);
|
|
105
|
+
setUnreadCount(data.unread_count);
|
|
106
|
+
if (cacheKey) {
|
|
107
|
+
try {
|
|
108
|
+
sessionStorage.setItem(
|
|
109
|
+
cacheKey,
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
notifications: data.notifications,
|
|
112
|
+
unread_count: data.unread_count,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
// ignore quota / private mode
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Non-critical chrome — never surface bell errors to the user.
|
|
121
|
+
}
|
|
122
|
+
}, [fetchNotifications, cacheKey]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!enabled) return;
|
|
126
|
+
|
|
127
|
+
if (cacheKey) {
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
try {
|
|
130
|
+
const raw = sessionStorage.getItem(cacheKey);
|
|
131
|
+
if (!raw) return;
|
|
132
|
+
const cached = JSON.parse(raw) as {
|
|
133
|
+
notifications?: SuiteNotification[];
|
|
134
|
+
unread_count?: number;
|
|
135
|
+
};
|
|
136
|
+
if (Array.isArray(cached.notifications)) {
|
|
137
|
+
setNotifications(cached.notifications);
|
|
138
|
+
setUnreadCount(cached.unread_count ?? 0);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let timer: number | undefined;
|
|
147
|
+
const stop = () => {
|
|
148
|
+
if (timer !== undefined) {
|
|
149
|
+
window.clearInterval(timer);
|
|
150
|
+
timer = undefined;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const start = () => {
|
|
154
|
+
if (timer !== undefined) return;
|
|
155
|
+
void load();
|
|
156
|
+
timer = window.setInterval(() => void load(), pollMs);
|
|
157
|
+
};
|
|
158
|
+
const onVisibilityChange = () => {
|
|
159
|
+
if (document.visibilityState === "visible") start();
|
|
160
|
+
else stop();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (document.visibilityState === "visible") start();
|
|
164
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
165
|
+
return () => {
|
|
166
|
+
stop();
|
|
167
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
168
|
+
};
|
|
169
|
+
}, [enabled, load, pollMs, cacheKey]);
|
|
170
|
+
|
|
171
|
+
const setOpenAndRefresh = useCallback(
|
|
172
|
+
(next: boolean) => {
|
|
173
|
+
setOpen(next);
|
|
174
|
+
if (next) void load();
|
|
175
|
+
},
|
|
176
|
+
[load],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const select = useCallback(
|
|
180
|
+
(notification: SuiteNotification) => {
|
|
181
|
+
setOpen(false);
|
|
182
|
+
if (!notification.read_at) {
|
|
183
|
+
setNotifications((prev) =>
|
|
184
|
+
prev.map((item) =>
|
|
185
|
+
item.id === notification.id
|
|
186
|
+
? { ...item, read_at: new Date().toISOString() }
|
|
187
|
+
: item,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
setUnreadCount((count) => Math.max(0, count - 1));
|
|
191
|
+
void (async () => {
|
|
192
|
+
try {
|
|
193
|
+
await markRead(notification.id);
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const target = resolveSuiteNotificationHref(
|
|
201
|
+
notification.href,
|
|
202
|
+
notification.source_app,
|
|
203
|
+
);
|
|
204
|
+
if (!target) return;
|
|
205
|
+
if (/^https?:\/\//i.test(target)) {
|
|
206
|
+
try {
|
|
207
|
+
const url = new URL(target);
|
|
208
|
+
const sourceApp = suiteAppSlugForNotificationSource(
|
|
209
|
+
notification.source_app,
|
|
210
|
+
);
|
|
211
|
+
const path = `${url.pathname}${url.search}${url.hash}`;
|
|
212
|
+
if (url.origin === window.location.origin) {
|
|
213
|
+
router.push(path);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (sourceApp && sourceApp !== "shellstack") {
|
|
217
|
+
const sourceOrigin = new URL(suiteAppBaseUrl(sourceApp)).origin;
|
|
218
|
+
if (url.origin === sourceOrigin) {
|
|
219
|
+
const launchUrl = new URL(
|
|
220
|
+
`/launch/${sourceApp}`,
|
|
221
|
+
suiteAppBaseUrl("shellstack"),
|
|
222
|
+
);
|
|
223
|
+
launchUrl.searchParams.set("next", path);
|
|
224
|
+
window.location.assign(launchUrl.toString());
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Fall through to the original external link.
|
|
230
|
+
}
|
|
231
|
+
window.location.assign(target);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
router.push(target);
|
|
235
|
+
},
|
|
236
|
+
[router, markRead],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const markAllRead = useCallback(() => {
|
|
240
|
+
setNotifications((prev) =>
|
|
241
|
+
prev.map((item) => ({
|
|
242
|
+
...item,
|
|
243
|
+
read_at: item.read_at ?? new Date().toISOString(),
|
|
244
|
+
})),
|
|
245
|
+
);
|
|
246
|
+
setUnreadCount(0);
|
|
247
|
+
void (async () => {
|
|
248
|
+
try {
|
|
249
|
+
await markAllReadRequest();
|
|
250
|
+
} catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
}, [markAllReadRequest]);
|
|
255
|
+
|
|
256
|
+
const value = useMemo<SuiteNotificationsContextValue>(
|
|
257
|
+
() => ({
|
|
258
|
+
notifications,
|
|
259
|
+
unreadCount,
|
|
260
|
+
open,
|
|
261
|
+
setOpen: setOpenAndRefresh,
|
|
262
|
+
refresh: () => void load(),
|
|
263
|
+
select,
|
|
264
|
+
markAllRead,
|
|
265
|
+
}),
|
|
266
|
+
[
|
|
267
|
+
notifications,
|
|
268
|
+
unreadCount,
|
|
269
|
+
open,
|
|
270
|
+
setOpenAndRefresh,
|
|
271
|
+
load,
|
|
272
|
+
select,
|
|
273
|
+
markAllRead,
|
|
274
|
+
],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<SuiteNotificationsContext.Provider value={value}>
|
|
279
|
+
{children}
|
|
280
|
+
</SuiteNotificationsContext.Provider>
|
|
281
|
+
);
|
|
282
|
+
}
|