@openrails/contentkit-ui 0.72.0 → 0.74.0
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/README.md +52 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +20 -18
- package/dist/client/client.js.map +1 -1
- package/dist/client/generated/routes.d.ts +66 -1
- package/dist/client/generated/routes.js +85 -0
- package/dist/client/generated/routes.js.map +1 -1
- package/dist/client/generated/wire.d.ts +30 -0
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/client/http.d.ts +8 -1
- package/dist/client/http.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/media/queue.d.ts +1 -1
- package/dist/client/media/queue.js +1 -1
- package/dist/client/media/queue.js.map +1 -1
- package/dist/client/notifications.d.ts +23 -0
- package/dist/client/notifications.js +46 -0
- package/dist/client/notifications.js.map +1 -0
- package/dist/client.js +5 -4
- package/dist/components/admin/media-item-panel.d.ts +27 -0
- package/dist/components/admin/media-item-panel.js +397 -0
- package/dist/components/admin/media-item-panel.js.map +1 -0
- package/dist/components/admin/upload-queue.d.ts +23 -0
- package/dist/components/admin/upload-queue.js +365 -0
- package/dist/components/admin/upload-queue.js.map +1 -0
- package/dist/components/notifications.d.ts +28 -0
- package/dist/components/notifications.js +222 -0
- package/dist/components/notifications.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -2
- package/dist/locales/admin/de.js +51 -0
- package/dist/locales/admin/de.js.map +1 -1
- package/dist/locales/admin/en.d.ts +51 -0
- package/dist/locales/admin/en.js +51 -0
- package/dist/locales/admin/en.js.map +1 -1
- package/dist/locales/admin/es.js +51 -0
- package/dist/locales/admin/es.js.map +1 -1
- package/dist/locales/admin/ja.js +51 -0
- package/dist/locales/admin/ja.js.map +1 -1
- package/dist/locales/admin/ko.js +51 -0
- package/dist/locales/admin/ko.js.map +1 -1
- package/dist/locales/admin/zh.js +51 -0
- package/dist/locales/admin/zh.js.map +1 -1
- package/dist/locales/en.d.ts +74 -0
- package/dist/locales/social/de.js +23 -0
- package/dist/locales/social/de.js.map +1 -1
- package/dist/locales/social/en.d.ts +23 -0
- package/dist/locales/social/en.js +23 -0
- package/dist/locales/social/en.js.map +1 -1
- package/dist/locales/social/es.js +23 -0
- package/dist/locales/social/es.js.map +1 -1
- package/dist/locales/social/ja.js +23 -0
- package/dist/locales/social/ja.js.map +1 -1
- package/dist/locales/social/ko.js +23 -0
- package/dist/locales/social/ko.js.map +1 -1
- package/dist/locales/social/zh.js +23 -0
- package/dist/locales/social/zh.js.map +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/folder.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/notifications.d.ts +29 -0
- package/dist/react/notifications.js +56 -0
- package/dist/react/notifications.js.map +1 -0
- package/dist/react/resources.d.ts +1 -0
- package/dist/react/resources.js +26 -0
- package/dist/react/resources.js.map +1 -1
- package/dist/react/upload.d.ts +10 -10
- package/dist/react/upload.js +1 -1
- package/dist/react/upload.js.map +1 -1
- package/dist/react.js +4 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { toContentKitError as e } from "../../client/errors.js";
|
|
2
|
+
import { samePath as t } from "../../client/media/client.js";
|
|
3
|
+
import { megabytes as n } from "../../i18n/messages.js";
|
|
4
|
+
import { useMessages as r } from "../../i18n/context.js";
|
|
5
|
+
import { useErrorReporter as i, useGuarded as a } from "../../react/context.js";
|
|
6
|
+
import { Button as o } from "../../ui/button.js";
|
|
7
|
+
import { Badge as s } from "../../ui/badge.js";
|
|
8
|
+
import { ErrorLine as c } from "../social/parts.js";
|
|
9
|
+
import { AdminRoot as l } from "./shell.js";
|
|
10
|
+
import { useFlowClient as u } from "../../react/admin.js";
|
|
11
|
+
import { useCallback as d, useEffect as f, useRef as p, useState as m } from "react";
|
|
12
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
13
|
+
import { cn as v } from "cn";
|
|
14
|
+
//#region src/components/admin/upload-queue.tsx
|
|
15
|
+
var y = [
|
|
16
|
+
"creating",
|
|
17
|
+
"uploading",
|
|
18
|
+
"committing"
|
|
19
|
+
], b = (e) => `${e.name}\u0000${e.size}\u0000${e.lastModified}`, x = 0;
|
|
20
|
+
function S(e) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(localStorage.getItem(e) ?? "{}");
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function C(e, t) {
|
|
28
|
+
try {
|
|
29
|
+
let n = S(e);
|
|
30
|
+
t(n), Object.keys(n).length ? localStorage.setItem(e, JSON.stringify(n)) : localStorage.removeItem(e);
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
function w({ client: e, baseUrl: t, auth: n, guard: r, appearance: i, className: a, language: o, messages: s, ...c }) {
|
|
34
|
+
let d = u({
|
|
35
|
+
client: e,
|
|
36
|
+
baseUrl: t,
|
|
37
|
+
auth: n
|
|
38
|
+
});
|
|
39
|
+
return /* @__PURE__ */ g(l, {
|
|
40
|
+
flow: "upload-queue",
|
|
41
|
+
appearance: i,
|
|
42
|
+
className: a,
|
|
43
|
+
language: o,
|
|
44
|
+
messages: s,
|
|
45
|
+
guard: r,
|
|
46
|
+
children: /* @__PURE__ */ g(T, {
|
|
47
|
+
...c,
|
|
48
|
+
client: d
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function T({ client: l, createItem: u, path: w = "source", accept: T, concurrency: E = 2, onUploaded: D, storageKey: O = "ckui.uploads", onError: k }) {
|
|
53
|
+
let A = r(), { t: j } = A, M = i(k), N = a(), [P, F] = m([]), [I, L] = m(() => Object.values(S(O))), [R, z] = m(!1), B = p(null), V = p(/* @__PURE__ */ new Map()), H = p({
|
|
54
|
+
createItem: u,
|
|
55
|
+
onUploaded: D
|
|
56
|
+
});
|
|
57
|
+
f(() => {
|
|
58
|
+
H.current = {
|
|
59
|
+
createItem: u,
|
|
60
|
+
onUploaded: D
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
let U = d((e, t) => F((n) => n.map((n) => n.id === e ? {
|
|
64
|
+
...n,
|
|
65
|
+
...t
|
|
66
|
+
} : n)), []), W = d(async (n) => {
|
|
67
|
+
let r = new AbortController();
|
|
68
|
+
V.current.set(n.id, r);
|
|
69
|
+
let i = S(O)[n.key];
|
|
70
|
+
try {
|
|
71
|
+
let a = n.ref ?? i?.ref;
|
|
72
|
+
if (!a) {
|
|
73
|
+
U(n.id, {
|
|
74
|
+
status: "creating",
|
|
75
|
+
error: void 0
|
|
76
|
+
}), a = await H.current.createItem(n.file);
|
|
77
|
+
let e = a;
|
|
78
|
+
C(O, (t) => void (t[n.key] = {
|
|
79
|
+
ref: e,
|
|
80
|
+
name: n.file.name,
|
|
81
|
+
size: n.file.size
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
let o = a;
|
|
85
|
+
U(n.id, {
|
|
86
|
+
status: "uploading",
|
|
87
|
+
ref: a,
|
|
88
|
+
error: void 0
|
|
89
|
+
});
|
|
90
|
+
let s = {
|
|
91
|
+
ref: a,
|
|
92
|
+
path: w,
|
|
93
|
+
signal: r.signal,
|
|
94
|
+
onProgress: (e) => U(n.id, { progress: e }),
|
|
95
|
+
onState: (e) => C(O, (t) => void (t[n.key] = {
|
|
96
|
+
ref: o,
|
|
97
|
+
name: n.file.name,
|
|
98
|
+
size: n.file.size,
|
|
99
|
+
state: e ?? void 0
|
|
100
|
+
}))
|
|
101
|
+
}, c = await N(async () => {
|
|
102
|
+
try {
|
|
103
|
+
return await l.media.upload(n.file, {
|
|
104
|
+
...s,
|
|
105
|
+
resume: i?.state
|
|
106
|
+
});
|
|
107
|
+
} catch (t) {
|
|
108
|
+
if (i?.state && e(t).code === "resume_mismatch") return l.media.upload(n.file, s);
|
|
109
|
+
throw t;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
U(n.id, { status: "committing" });
|
|
113
|
+
let u = await N(() => l.media.commit(o, [{
|
|
114
|
+
op: "put",
|
|
115
|
+
path: c.path,
|
|
116
|
+
blob: c.blob
|
|
117
|
+
}], { signal: r.signal }));
|
|
118
|
+
C(O, (e) => void delete e[n.key]), U(n.id, {
|
|
119
|
+
status: "done",
|
|
120
|
+
progress: void 0
|
|
121
|
+
});
|
|
122
|
+
let d = u.find((e) => t(e.path, c.path));
|
|
123
|
+
d && H.current.onUploaded?.(o, d);
|
|
124
|
+
} catch (t) {
|
|
125
|
+
let r = e(t);
|
|
126
|
+
r.code === "aborted" ? U(n.id, { status: "paused" }) : (U(n.id, {
|
|
127
|
+
status: "failed",
|
|
128
|
+
error: r
|
|
129
|
+
}), M(r, "upload", n.file.name));
|
|
130
|
+
} finally {
|
|
131
|
+
V.current.delete(n.id);
|
|
132
|
+
}
|
|
133
|
+
}, [
|
|
134
|
+
l,
|
|
135
|
+
w,
|
|
136
|
+
O,
|
|
137
|
+
N,
|
|
138
|
+
U,
|
|
139
|
+
M
|
|
140
|
+
]);
|
|
141
|
+
f(() => {
|
|
142
|
+
let e = P.filter((e) => y.includes(e.status)).length, t = P.filter((e) => e.status === "queued").slice(0, Math.max(0, E - e));
|
|
143
|
+
if (t.length) {
|
|
144
|
+
F((e) => e.map((e) => t.some((t) => t.id === e.id) ? {
|
|
145
|
+
...e,
|
|
146
|
+
status: "creating"
|
|
147
|
+
} : e));
|
|
148
|
+
for (let e of t) W(e);
|
|
149
|
+
}
|
|
150
|
+
}, [
|
|
151
|
+
P,
|
|
152
|
+
E,
|
|
153
|
+
W
|
|
154
|
+
]);
|
|
155
|
+
let G = P.some((e) => y.includes(e.status) || e.status === "queued");
|
|
156
|
+
f(() => {
|
|
157
|
+
if (!G) return;
|
|
158
|
+
let e = (e) => {
|
|
159
|
+
e.preventDefault(), e.returnValue = "";
|
|
160
|
+
};
|
|
161
|
+
return addEventListener("beforeunload", e), () => removeEventListener("beforeunload", e);
|
|
162
|
+
}, [G]), f(() => {
|
|
163
|
+
let e = V.current;
|
|
164
|
+
return () => {
|
|
165
|
+
for (let t of e.values()) t.abort();
|
|
166
|
+
};
|
|
167
|
+
}, []);
|
|
168
|
+
let K = (e) => {
|
|
169
|
+
let t = S(O), n = [];
|
|
170
|
+
for (let r of e) {
|
|
171
|
+
let e = b(r);
|
|
172
|
+
P.some((t) => t.key === e && t.status !== "done") || n.some((t) => t.key === e) || n.push({
|
|
173
|
+
id: `u${++x}`,
|
|
174
|
+
file: r,
|
|
175
|
+
key: e,
|
|
176
|
+
status: "queued",
|
|
177
|
+
ref: t[e]?.ref,
|
|
178
|
+
resumed: !!t[e]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
n.length && (L((e) => e.filter((e) => !n.some((t) => t.file.name === e.name && t.file.size === e.size))), F((e) => [...e, ...n]));
|
|
182
|
+
}, q = (e) => {
|
|
183
|
+
for (let [t, n] of V.current) (!e || t === e) && n.abort();
|
|
184
|
+
F((t) => t.map((t) => t.status === "queued" && (!e || t.id === e) ? {
|
|
185
|
+
...t,
|
|
186
|
+
status: "paused"
|
|
187
|
+
} : t));
|
|
188
|
+
}, J = (e) => F((t) => t.map((t) => (t.status === "paused" || t.status === "failed") && (!e || t.id === e) ? {
|
|
189
|
+
...t,
|
|
190
|
+
status: "queued",
|
|
191
|
+
error: void 0
|
|
192
|
+
} : t)), Y = (e) => {
|
|
193
|
+
V.current.get(e.id)?.abort(), e.status !== "done" && C(O, (t) => void delete t[e.key]), F((t) => t.filter((t) => t.id !== e.id));
|
|
194
|
+
}, X = (e) => {
|
|
195
|
+
C(O, (t) => {
|
|
196
|
+
for (let [n, r] of Object.entries(t)) r.name === e.name && r.size === e.size && delete t[n];
|
|
197
|
+
}), L((t) => t.filter((t) => t !== e));
|
|
198
|
+
}, Z = P.filter((e) => e.status === "done").length, Q = P.some((e) => y.includes(e.status) || e.status === "queued"), ee = P.some((e) => e.status === "paused" || e.status === "failed"), $ = (e) => e.progress?.total ? Math.round(e.progress.loaded / e.progress.total * 100) : 0;
|
|
199
|
+
return /* @__PURE__ */ _(h, { children: [
|
|
200
|
+
/* @__PURE__ */ _("div", {
|
|
201
|
+
className: v("grid justify-items-center gap-2 rounded-lg border-2 border-dashed border-border p-6 text-center transition-colors", R && "border-primary bg-accent"),
|
|
202
|
+
"data-ckui": "upload-drop",
|
|
203
|
+
onDragOver: (e) => {
|
|
204
|
+
e.preventDefault(), z(!0);
|
|
205
|
+
},
|
|
206
|
+
onDragLeave: () => z(!1),
|
|
207
|
+
onDrop: (e) => {
|
|
208
|
+
e.preventDefault(), z(!1), K(e.dataTransfer.files);
|
|
209
|
+
},
|
|
210
|
+
children: [
|
|
211
|
+
/* @__PURE__ */ g("p", {
|
|
212
|
+
className: "text-muted-foreground",
|
|
213
|
+
children: j("uploadQueue.drop")
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ g(o, {
|
|
216
|
+
size: "sm",
|
|
217
|
+
onClick: () => B.current?.click(),
|
|
218
|
+
children: j("uploadQueue.add")
|
|
219
|
+
}),
|
|
220
|
+
/* @__PURE__ */ g("input", {
|
|
221
|
+
ref: B,
|
|
222
|
+
type: "file",
|
|
223
|
+
multiple: !0,
|
|
224
|
+
hidden: !0,
|
|
225
|
+
accept: T,
|
|
226
|
+
"aria-label": j("uploadQueue.add"),
|
|
227
|
+
onChange: (e) => {
|
|
228
|
+
K(e.target.files ?? []), e.target.value = "";
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
}),
|
|
233
|
+
I.length > 0 && /* @__PURE__ */ _("div", {
|
|
234
|
+
className: "grid gap-2 rounded-lg border border-border p-3",
|
|
235
|
+
"data-ckui": "upload-waiting",
|
|
236
|
+
role: "status",
|
|
237
|
+
children: [
|
|
238
|
+
/* @__PURE__ */ g("p", {
|
|
239
|
+
className: "font-medium",
|
|
240
|
+
children: j("uploadQueue.unfinished")
|
|
241
|
+
}),
|
|
242
|
+
/* @__PURE__ */ g("ul", {
|
|
243
|
+
className: "grid gap-1",
|
|
244
|
+
children: I.map((e) => /* @__PURE__ */ _("li", {
|
|
245
|
+
className: "flex flex-wrap items-center gap-2",
|
|
246
|
+
children: [
|
|
247
|
+
/* @__PURE__ */ g("span", {
|
|
248
|
+
className: "min-w-0 flex-1 break-all",
|
|
249
|
+
children: e.name
|
|
250
|
+
}),
|
|
251
|
+
/* @__PURE__ */ g("span", {
|
|
252
|
+
className: "text-xs text-muted-foreground",
|
|
253
|
+
children: n(e.size)
|
|
254
|
+
}),
|
|
255
|
+
/* @__PURE__ */ g(o, {
|
|
256
|
+
size: "xs",
|
|
257
|
+
variant: "ghost",
|
|
258
|
+
onClick: () => X(e),
|
|
259
|
+
children: j("uploadQueue.forget")
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
}, `${e.name}:${e.size}`))
|
|
263
|
+
}),
|
|
264
|
+
/* @__PURE__ */ g("p", {
|
|
265
|
+
className: "text-xs text-muted-foreground",
|
|
266
|
+
children: j("uploadQueue.unfinishedHint")
|
|
267
|
+
})
|
|
268
|
+
]
|
|
269
|
+
}),
|
|
270
|
+
P.length > 0 && /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ _("div", {
|
|
271
|
+
className: "flex flex-wrap items-center gap-2",
|
|
272
|
+
children: [/* @__PURE__ */ g("p", {
|
|
273
|
+
className: "text-muted-foreground",
|
|
274
|
+
"aria-live": "polite",
|
|
275
|
+
children: j("uploadQueue.summary", {
|
|
276
|
+
done: Z,
|
|
277
|
+
total: P.length
|
|
278
|
+
})
|
|
279
|
+
}), /* @__PURE__ */ _("div", {
|
|
280
|
+
className: "ms-auto flex gap-2",
|
|
281
|
+
children: [Q && /* @__PURE__ */ g(o, {
|
|
282
|
+
size: "sm",
|
|
283
|
+
variant: "outline",
|
|
284
|
+
onClick: () => q(),
|
|
285
|
+
children: j("uploadQueue.pauseAll")
|
|
286
|
+
}), ee && /* @__PURE__ */ g(o, {
|
|
287
|
+
size: "sm",
|
|
288
|
+
variant: "outline",
|
|
289
|
+
onClick: () => J(),
|
|
290
|
+
children: j("uploadQueue.resumeAll")
|
|
291
|
+
})]
|
|
292
|
+
})]
|
|
293
|
+
}), /* @__PURE__ */ g("ul", {
|
|
294
|
+
className: "grid divide-y divide-border rounded-lg border border-border",
|
|
295
|
+
children: P.map((e) => /* @__PURE__ */ _("li", {
|
|
296
|
+
className: "grid gap-1.5 p-3",
|
|
297
|
+
"data-ckui": "upload-entry",
|
|
298
|
+
"data-status": e.status,
|
|
299
|
+
children: [
|
|
300
|
+
/* @__PURE__ */ _("div", {
|
|
301
|
+
className: "flex flex-wrap items-center gap-2",
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ g("span", {
|
|
304
|
+
className: "min-w-0 flex-1 font-medium break-all",
|
|
305
|
+
children: e.file.name
|
|
306
|
+
}),
|
|
307
|
+
/* @__PURE__ */ g("span", {
|
|
308
|
+
className: "text-xs text-muted-foreground",
|
|
309
|
+
children: n(e.file.size)
|
|
310
|
+
}),
|
|
311
|
+
e.resumed && e.status !== "done" && /* @__PURE__ */ g(s, {
|
|
312
|
+
variant: "secondary",
|
|
313
|
+
children: j("uploadQueue.resumed")
|
|
314
|
+
}),
|
|
315
|
+
/* @__PURE__ */ g(s, {
|
|
316
|
+
variant: e.status === "failed" ? "destructive" : e.status === "done" ? "outline" : "secondary",
|
|
317
|
+
children: j(`uploadQueue.${e.status}`)
|
|
318
|
+
})
|
|
319
|
+
]
|
|
320
|
+
}),
|
|
321
|
+
e.status === "uploading" && /* @__PURE__ */ g("div", {
|
|
322
|
+
className: "h-1.5 overflow-hidden rounded-full bg-muted",
|
|
323
|
+
role: "progressbar",
|
|
324
|
+
"aria-label": e.file.name,
|
|
325
|
+
"aria-valuenow": $(e),
|
|
326
|
+
"aria-valuemin": 0,
|
|
327
|
+
"aria-valuemax": 100,
|
|
328
|
+
children: /* @__PURE__ */ g("div", {
|
|
329
|
+
className: "h-full bg-primary transition-[width]",
|
|
330
|
+
style: { width: `${$(e)}%` }
|
|
331
|
+
})
|
|
332
|
+
}),
|
|
333
|
+
e.error && /* @__PURE__ */ g(c, { children: A.error(e.error) }),
|
|
334
|
+
/* @__PURE__ */ _("div", {
|
|
335
|
+
className: "flex flex-wrap gap-2",
|
|
336
|
+
children: [
|
|
337
|
+
(y.includes(e.status) || e.status === "queued") && /* @__PURE__ */ g(o, {
|
|
338
|
+
size: "xs",
|
|
339
|
+
variant: "ghost",
|
|
340
|
+
onClick: () => q(e.id),
|
|
341
|
+
children: j("uploadQueue.pause")
|
|
342
|
+
}),
|
|
343
|
+
(e.status === "paused" || e.status === "failed") && /* @__PURE__ */ g(o, {
|
|
344
|
+
size: "xs",
|
|
345
|
+
variant: "outline",
|
|
346
|
+
onClick: () => J(e.id),
|
|
347
|
+
children: j(e.status === "failed" ? "common.retry" : "uploadQueue.resume")
|
|
348
|
+
}),
|
|
349
|
+
e.status !== "done" && /* @__PURE__ */ g(o, {
|
|
350
|
+
size: "xs",
|
|
351
|
+
variant: "ghost",
|
|
352
|
+
onClick: () => Y(e),
|
|
353
|
+
children: j("common.remove")
|
|
354
|
+
})
|
|
355
|
+
]
|
|
356
|
+
})
|
|
357
|
+
]
|
|
358
|
+
}, e.id))
|
|
359
|
+
})] })
|
|
360
|
+
] });
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
export { w as UploadQueue };
|
|
364
|
+
|
|
365
|
+
//# sourceMappingURL=upload-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-queue.js","names":[],"sources":["../../../src/components/admin/upload-queue.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { FileInfo, RefBody } from \"../../client/generated/wire.js\";\nimport { samePath, type Progress, type UploadState } from \"../../client/media/client.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { megabytes } from \"../../i18n/messages.js\";\nimport { useFlowClient } from \"../../react/admin.js\";\nimport { useErrorReporter, useGuarded } from \"../../react/context.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { Button } from \"#ckui/ui/button\";\nimport { ErrorLine } from \"../social/parts.js\";\nimport { AdminRoot, type AdminFlowProps } from \"./shell.js\";\n\nexport interface UploadQueueProps extends AdminFlowProps {\n /** Makes the host's item for a file (e.g. a video record) and resolves with its ref; once per file, a reload included. */\n createItem: (file: File) => Promise<RefBody>;\n /** The upload path in each item. Default \"source\". */\n path?: string;\n /** The file picker's accept, e.g. \"video/*\". */\n accept?: string;\n /** Files uploading at once. Default 2. */\n concurrency?: number;\n /** Each committed file: its item and the upload as an editor reads it. */\n onUploaded?: (ref: RefBody, file: FileInfo) => void;\n /** The localStorage key unfinished uploads are kept under, to resume after a reload. Default \"ckui.uploads\". */\n storageKey?: string;\n}\n\ntype Status = \"queued\" | \"creating\" | \"uploading\" | \"committing\" | \"done\" | \"failed\" | \"paused\";\n\ninterface Entry {\n id: string;\n file: File;\n key: string;\n status: Status;\n progress?: Progress;\n ref?: RefBody;\n error?: ContentKitError;\n /** Resumed from an upload saved before a reload. */\n resumed?: boolean;\n}\n\ninterface Saved {\n ref: RefBody;\n name: string;\n size: number;\n state?: UploadState;\n}\n\nconst ACTIVE: readonly Status[] = [\"creating\", \"uploading\", \"committing\"];\nconst fingerprint = (f: File) => `${f.name}\\u0000${f.size}\\u0000${f.lastModified}`;\nlet seq = 0;\n\nfunction readSaved(key: string): Record<string, Saved> {\n try {\n return JSON.parse(localStorage.getItem(key) ?? \"{}\") as Record<string, Saved>;\n } catch {\n return {};\n }\n}\n\nfunction writeSaved(key: string, fn: (all: Record<string, Saved>) => void) {\n try {\n const all = readSaved(key);\n fn(all);\n if (Object.keys(all).length) localStorage.setItem(key, JSON.stringify(all));\n else localStorage.removeItem(key);\n } catch {\n // Without storage, a reload starts over.\n }\n}\n\n/**\n * Many files, each into its own new item (createItem): uploads in the\n * background with progress, pause, retry and resume. An unfinished upload is\n * kept in localStorage; adding the same file after a reload continues it in\n * the same item. Leaving the page while files upload asks first.\n */\nexport function UploadQueue({ client: own, baseUrl, auth, guard, appearance, className, language, messages, ...p }: UploadQueueProps) {\n const client = useFlowClient({ client: own, baseUrl, auth });\n return (\n <AdminRoot flow=\"upload-queue\" appearance={appearance} className={className} language={language} messages={messages} guard={guard}>\n <Queue {...p} client={client} />\n </AdminRoot>\n );\n}\n\ntype Inner = Omit<UploadQueueProps, \"client\" | \"baseUrl\" | \"auth\" | \"guard\" | \"appearance\" | \"className\" | \"language\" | \"messages\"> & { client: ContentKitClient };\n\nfunction Queue({ client, createItem, path = \"source\", accept, concurrency = 2, onUploaded, storageKey = \"ckui.uploads\", onError }: Inner) {\n const m = useMessages();\n const { t } = m;\n const report = useErrorReporter(onError);\n const guarded = useGuarded();\n const [entries, setEntries] = useState<Entry[]>([]);\n const [waiting, setWaiting] = useState<Saved[]>(() => Object.values(readSaved(storageKey)));\n const [over, setOver] = useState(false);\n const input = useRef<HTMLInputElement>(null);\n const controllers = useRef(new Map<string, AbortController>());\n const latest = useRef({ createItem, onUploaded });\n useEffect(() => {\n latest.current = { createItem, onUploaded };\n });\n\n const patch = useCallback((id: string, next: Partial<Entry>) => setEntries((es) => es.map((e) => (e.id === id ? { ...e, ...next } : e))), []);\n\n const run = useCallback(\n async (entry: Entry) => {\n const ctl = new AbortController();\n controllers.current.set(entry.id, ctl);\n const saved = readSaved(storageKey)[entry.key];\n try {\n let ref = entry.ref ?? saved?.ref;\n if (!ref) {\n patch(entry.id, { status: \"creating\", error: undefined });\n ref = await latest.current.createItem(entry.file);\n const made = ref;\n writeSaved(storageKey, (all) => void (all[entry.key] = { ref: made, name: entry.file.name, size: entry.file.size }));\n }\n const target = ref;\n patch(entry.id, { status: \"uploading\", ref, error: undefined });\n const remember = (state: UploadState | null) =>\n writeSaved(storageKey, (all) => void (all[entry.key] = { ref: target, name: entry.file.name, size: entry.file.size, state: state ?? undefined }));\n const options = { ref, path, signal: ctl.signal, onProgress: (progress: Progress) => patch(entry.id, { progress }), onState: remember };\n const up = await guarded(async () => {\n try {\n return await client.media.upload(entry.file, { ...options, resume: saved?.state });\n } catch (e) {\n if (saved?.state && toContentKitError(e).code === \"resume_mismatch\") return client.media.upload(entry.file, options);\n throw e;\n }\n });\n patch(entry.id, { status: \"committing\" });\n const files = await guarded(() => client.media.commit(target, [{ op: \"put\", path: up.path, blob: up.blob }], { signal: ctl.signal }));\n writeSaved(storageKey, (all) => void delete all[entry.key]);\n patch(entry.id, { status: \"done\", progress: undefined });\n const file = files.find((f) => samePath(f.path, up.path));\n if (file) latest.current.onUploaded?.(target, file);\n } catch (e) {\n const error = toContentKitError(e);\n if (error.code === \"aborted\") patch(entry.id, { status: \"paused\" });\n else {\n patch(entry.id, { status: \"failed\", error });\n report(error, \"upload\", entry.file.name);\n }\n } finally {\n controllers.current.delete(entry.id);\n }\n },\n [client, path, storageKey, guarded, patch, report],\n );\n\n // Starts queued files while fewer than concurrency run.\n useEffect(() => {\n const active = entries.filter((e) => ACTIVE.includes(e.status)).length;\n const next = entries.filter((e) => e.status === \"queued\").slice(0, Math.max(0, concurrency - active));\n if (!next.length) return;\n setEntries((es) => es.map((e) => (next.some((n) => n.id === e.id) ? { ...e, status: \"creating\" } : e)));\n for (const e of next) void run(e);\n }, [entries, concurrency, run]);\n\n const busy = entries.some((e) => ACTIVE.includes(e.status) || e.status === \"queued\");\n useEffect(() => {\n if (!busy) return;\n const ask = (e: BeforeUnloadEvent) => {\n e.preventDefault();\n e.returnValue = \"\";\n };\n addEventListener(\"beforeunload\", ask);\n return () => removeEventListener(\"beforeunload\", ask);\n }, [busy]);\n useEffect(() => {\n const running = controllers.current;\n return () => {\n for (const c of running.values()) c.abort();\n };\n }, []);\n\n const add = (files: Iterable<File>) => {\n const saved = readSaved(storageKey);\n const added: Entry[] = [];\n for (const file of files) {\n const key = fingerprint(file);\n if (entries.some((e) => e.key === key && e.status !== \"done\") || added.some((e) => e.key === key)) continue;\n added.push({ id: `u${++seq}`, file, key, status: \"queued\", ref: saved[key]?.ref, resumed: !!saved[key] });\n }\n if (!added.length) return;\n setWaiting((w) => w.filter((s) => !added.some((a) => a.file.name === s.name && a.file.size === s.size)));\n setEntries((es) => [...es, ...added]);\n };\n const pause = (id?: string) => {\n for (const [k, c] of controllers.current) if (!id || k === id) c.abort();\n setEntries((es) => es.map((e) => (e.status === \"queued\" && (!id || e.id === id) ? { ...e, status: \"paused\" } : e)));\n };\n const resume = (id?: string) => setEntries((es) => es.map((e) => ((e.status === \"paused\" || e.status === \"failed\") && (!id || e.id === id) ? { ...e, status: \"queued\", error: undefined } : e)));\n const remove = (entry: Entry) => {\n controllers.current.get(entry.id)?.abort();\n if (entry.status !== \"done\") writeSaved(storageKey, (all) => void delete all[entry.key]);\n setEntries((es) => es.filter((e) => e.id !== entry.id));\n };\n const forget = (s: Saved) => {\n writeSaved(storageKey, (all) => {\n for (const [k, v] of Object.entries(all)) if (v.name === s.name && v.size === s.size) delete all[k];\n });\n setWaiting((w) => w.filter((x) => x !== s));\n };\n\n const done = entries.filter((e) => e.status === \"done\").length;\n const pausable = entries.some((e) => ACTIVE.includes(e.status) || e.status === \"queued\");\n const resumable = entries.some((e) => e.status === \"paused\" || e.status === \"failed\");\n const percent = (e: Entry) => (e.progress?.total ? Math.round((e.progress.loaded / e.progress.total) * 100) : 0);\n return (\n <>\n <div\n className={cn(\"grid justify-items-center gap-2 rounded-lg border-2 border-dashed border-border p-6 text-center transition-colors\", over && \"border-primary bg-accent\")}\n data-ckui=\"upload-drop\"\n onDragOver={(e) => {\n e.preventDefault();\n setOver(true);\n }}\n onDragLeave={() => setOver(false)}\n onDrop={(e) => {\n e.preventDefault();\n setOver(false);\n add(e.dataTransfer.files);\n }}\n >\n <p className=\"text-muted-foreground\">{t(\"uploadQueue.drop\")}</p>\n <Button size=\"sm\" onClick={() => input.current?.click()}>\n {t(\"uploadQueue.add\")}\n </Button>\n <input\n ref={input}\n type=\"file\"\n multiple\n hidden\n accept={accept}\n aria-label={t(\"uploadQueue.add\")}\n onChange={(e) => {\n add(e.target.files ?? []);\n e.target.value = \"\";\n }}\n />\n </div>\n {waiting.length > 0 && (\n <div className=\"grid gap-2 rounded-lg border border-border p-3\" data-ckui=\"upload-waiting\" role=\"status\">\n <p className=\"font-medium\">{t(\"uploadQueue.unfinished\")}</p>\n <ul className=\"grid gap-1\">\n {waiting.map((s) => (\n <li key={`${s.name}:${s.size}`} className=\"flex flex-wrap items-center gap-2\">\n <span className=\"min-w-0 flex-1 break-all\">{s.name}</span>\n <span className=\"text-xs text-muted-foreground\">{megabytes(s.size)}</span>\n <Button size=\"xs\" variant=\"ghost\" onClick={() => forget(s)}>\n {t(\"uploadQueue.forget\")}\n </Button>\n </li>\n ))}\n </ul>\n <p className=\"text-xs text-muted-foreground\">{t(\"uploadQueue.unfinishedHint\")}</p>\n </div>\n )}\n {entries.length > 0 && (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-muted-foreground\" aria-live=\"polite\">\n {t(\"uploadQueue.summary\", { done, total: entries.length })}\n </p>\n <div className=\"ms-auto flex gap-2\">\n {pausable && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => pause()}>\n {t(\"uploadQueue.pauseAll\")}\n </Button>\n )}\n {resumable && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => resume()}>\n {t(\"uploadQueue.resumeAll\")}\n </Button>\n )}\n </div>\n </div>\n <ul className=\"grid divide-y divide-border rounded-lg border border-border\">\n {entries.map((e) => (\n <li key={e.id} className=\"grid gap-1.5 p-3\" data-ckui=\"upload-entry\" data-status={e.status}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"min-w-0 flex-1 font-medium break-all\">{e.file.name}</span>\n <span className=\"text-xs text-muted-foreground\">{megabytes(e.file.size)}</span>\n {e.resumed && e.status !== \"done\" && <Badge variant=\"secondary\">{t(\"uploadQueue.resumed\")}</Badge>}\n <Badge variant={e.status === \"failed\" ? \"destructive\" : e.status === \"done\" ? \"outline\" : \"secondary\"}>{t(`uploadQueue.${e.status}`)}</Badge>\n </div>\n {e.status === \"uploading\" && (\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\" role=\"progressbar\" aria-label={e.file.name} aria-valuenow={percent(e)} aria-valuemin={0} aria-valuemax={100}>\n <div className=\"h-full bg-primary transition-[width]\" style={{ width: `${percent(e)}%` }} />\n </div>\n )}\n {e.error && <ErrorLine>{m.error(e.error)}</ErrorLine>}\n <div className=\"flex flex-wrap gap-2\">\n {(ACTIVE.includes(e.status) || e.status === \"queued\") && (\n <Button size=\"xs\" variant=\"ghost\" onClick={() => pause(e.id)}>\n {t(\"uploadQueue.pause\")}\n </Button>\n )}\n {(e.status === \"paused\" || e.status === \"failed\") && (\n <Button size=\"xs\" variant=\"outline\" onClick={() => resume(e.id)}>\n {t(e.status === \"failed\" ? \"common.retry\" : \"uploadQueue.resume\")}\n </Button>\n )}\n {e.status !== \"done\" && (\n <Button size=\"xs\" variant=\"ghost\" onClick={() => remove(e)}>\n {t(\"common.remove\")}\n </Button>\n )}\n </div>\n </li>\n ))}\n </ul>\n </>\n )}\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ContentKitUiAppearance } from '../appearance.js';
|
|
3
|
+
import { ContentKitClient } from '../client/client.js';
|
|
4
|
+
import { Item } from '../client/generated/wire.js';
|
|
5
|
+
import { ContentKitErrorHandler } from '../react/context.js';
|
|
6
|
+
import { UseNotificationsOptions } from '../react/notifications.js';
|
|
7
|
+
export interface NotificationInboxProps {
|
|
8
|
+
/** Host-owned rendering for every kind, including unknown future kinds. */
|
|
9
|
+
renderItem: (item: Item) => ReactNode;
|
|
10
|
+
/** Domain actions/links; the inbox itself never executes a source's workflow. */
|
|
11
|
+
renderActions?: (item: Item) => ReactNode;
|
|
12
|
+
client?: ContentKitClient;
|
|
13
|
+
pageSize?: number;
|
|
14
|
+
initialFilter?: "all" | "unread" | "archived";
|
|
15
|
+
refreshInterval?: UseNotificationsOptions["refreshInterval"];
|
|
16
|
+
onSignIn?: () => void;
|
|
17
|
+
onError?: ContentKitErrorHandler;
|
|
18
|
+
appearance?: ContentKitUiAppearance;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A user inbox with cursor pagination and independent local read state. */
|
|
22
|
+
export declare function NotificationInbox({ renderItem, renderActions, client, pageSize, initialFilter, refreshInterval, onSignIn, onError, appearance, className }: NotificationInboxProps): import('react').JSX.Element;
|
|
23
|
+
export interface NotificationBellProps extends NotificationInboxProps {
|
|
24
|
+
/** Unread indicator refresh. Default 30 seconds while the page is visible. */
|
|
25
|
+
unreadRefreshInterval?: UseNotificationsOptions["refreshInterval"];
|
|
26
|
+
}
|
|
27
|
+
/** A keyboard-accessible inbox popover. Its dot means unread items exist, not a guessed count. */
|
|
28
|
+
export declare function NotificationBell({ appearance, className, unreadRefreshInterval, ...inbox }: NotificationBellProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { useMessages as e } from "../i18n/context.js";
|
|
2
|
+
import { ContentKitUiRoot as t, useScopeProps as n } from "../scope.js";
|
|
3
|
+
import { useErrorReporter as r } from "../react/context.js";
|
|
4
|
+
import { Button as i } from "../ui/button.js";
|
|
5
|
+
import { useContentScope as a } from "../react/use-resource.js";
|
|
6
|
+
import { Badge as o } from "../ui/badge.js";
|
|
7
|
+
import { Skeleton as s } from "../ui/skeleton.js";
|
|
8
|
+
import { ErrorLine as c, RelativeTime as l, useAction as u } from "./social/parts.js";
|
|
9
|
+
import { useNotificationUnread as d, useNotifications as f } from "../react/notifications.js";
|
|
10
|
+
import { useEffect as p, useId as m, useRef as h, useState as g } from "react";
|
|
11
|
+
import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
12
|
+
import { cn as b } from "cn";
|
|
13
|
+
import { Notification01Icon as x, RefreshIcon as S } from "@hugeicons/core-free-icons";
|
|
14
|
+
import { HugeiconsIcon as C } from "@hugeicons/react";
|
|
15
|
+
import { Popover as w } from "@base-ui/react/popover";
|
|
16
|
+
//#region src/components/notifications.tsx
|
|
17
|
+
function T({ renderItem: n, renderActions: o, client: l, pageSize: u, initialFilter: d = "all", refreshInterval: x, onSignIn: w, onError: T, appearance: D, className: O }) {
|
|
18
|
+
let { t: k } = e(), A = m(), [j, M] = g(d), N = f({
|
|
19
|
+
client: l,
|
|
20
|
+
pageSize: u,
|
|
21
|
+
unread: j === "unread",
|
|
22
|
+
dismissed: j === "archived",
|
|
23
|
+
refreshInterval: x
|
|
24
|
+
}), { onSignIn: P } = a(l), F = w ?? P, I = h(null), L = r(T);
|
|
25
|
+
return p(() => {
|
|
26
|
+
N.error && L(N.error, "notifications.load");
|
|
27
|
+
}, [N.error, L]), /* @__PURE__ */ y(t, {
|
|
28
|
+
appearance: D,
|
|
29
|
+
className: b("grid gap-4 text-sm", O),
|
|
30
|
+
"data-ckui": "notification-inbox",
|
|
31
|
+
role: "region",
|
|
32
|
+
"aria-labelledby": A,
|
|
33
|
+
children: [/* @__PURE__ */ y("header", {
|
|
34
|
+
className: "flex items-center justify-between gap-3",
|
|
35
|
+
children: [/* @__PURE__ */ v("h2", {
|
|
36
|
+
id: A,
|
|
37
|
+
className: "text-base font-semibold",
|
|
38
|
+
children: k("notifications.title")
|
|
39
|
+
}), !N.signedOut && /* @__PURE__ */ v(i, {
|
|
40
|
+
size: "icon-sm",
|
|
41
|
+
variant: "ghost",
|
|
42
|
+
"aria-label": k("notifications.refresh"),
|
|
43
|
+
disabled: N.loading,
|
|
44
|
+
onClick: N.reload,
|
|
45
|
+
children: /* @__PURE__ */ v(C, {
|
|
46
|
+
icon: S,
|
|
47
|
+
strokeWidth: 2
|
|
48
|
+
})
|
|
49
|
+
})]
|
|
50
|
+
}), N.signedOut ? /* @__PURE__ */ y("div", {
|
|
51
|
+
className: "grid justify-items-start gap-3 py-4 text-muted-foreground",
|
|
52
|
+
children: [/* @__PURE__ */ v("p", { children: k("notifications.signInHint") }), F && /* @__PURE__ */ v(i, {
|
|
53
|
+
variant: "outline",
|
|
54
|
+
onClick: F,
|
|
55
|
+
children: k("notifications.signIn")
|
|
56
|
+
})]
|
|
57
|
+
}) : /* @__PURE__ */ y(_, { children: [
|
|
58
|
+
/* @__PURE__ */ v("div", {
|
|
59
|
+
className: "flex gap-1",
|
|
60
|
+
role: "group",
|
|
61
|
+
"aria-label": k("notifications.filter"),
|
|
62
|
+
children: [
|
|
63
|
+
"all",
|
|
64
|
+
"unread",
|
|
65
|
+
"archived"
|
|
66
|
+
].map((e) => /* @__PURE__ */ v(i, {
|
|
67
|
+
ref: j === e ? I : void 0,
|
|
68
|
+
size: "sm",
|
|
69
|
+
variant: j === e ? "secondary" : "ghost",
|
|
70
|
+
"aria-pressed": j === e,
|
|
71
|
+
onClick: () => M(e),
|
|
72
|
+
children: k(`notifications.${e}`)
|
|
73
|
+
}, e))
|
|
74
|
+
}),
|
|
75
|
+
N.error && /* @__PURE__ */ y("div", {
|
|
76
|
+
className: "grid justify-items-start gap-2",
|
|
77
|
+
children: [/* @__PURE__ */ v(c, { children: k("notifications.loadFailed") }), /* @__PURE__ */ v(i, {
|
|
78
|
+
size: "sm",
|
|
79
|
+
variant: "outline",
|
|
80
|
+
onClick: N.reload,
|
|
81
|
+
children: k("common.retry")
|
|
82
|
+
})]
|
|
83
|
+
}),
|
|
84
|
+
N.loading && !N.items.length ? /* @__PURE__ */ y("div", {
|
|
85
|
+
role: "status",
|
|
86
|
+
"aria-label": k("notifications.loading"),
|
|
87
|
+
className: "grid gap-3",
|
|
88
|
+
children: [/* @__PURE__ */ v(s, { className: "h-20 w-full" }), /* @__PURE__ */ v(s, { className: "h-20 w-full" })]
|
|
89
|
+
}) : !N.items.length && !N.error ? /* @__PURE__ */ v("p", {
|
|
90
|
+
role: "status",
|
|
91
|
+
className: "py-8 text-center text-muted-foreground",
|
|
92
|
+
children: k(j === "unread" ? "notifications.emptyUnread" : "notifications.empty")
|
|
93
|
+
}) : /* @__PURE__ */ v("ul", {
|
|
94
|
+
className: "grid divide-y divide-border overflow-hidden rounded-lg border border-border",
|
|
95
|
+
"aria-busy": N.loading,
|
|
96
|
+
children: N.items.map((e) => /* @__PURE__ */ v(E, {
|
|
97
|
+
item: e,
|
|
98
|
+
renderItem: n,
|
|
99
|
+
renderActions: o,
|
|
100
|
+
onRead: async (e, t) => {
|
|
101
|
+
let n = await N.setRead(e, t);
|
|
102
|
+
return j === "unread" && t && I.current?.focus(), n;
|
|
103
|
+
},
|
|
104
|
+
onDismiss: async (e, t) => {
|
|
105
|
+
let n = await N.setDismissed(e, t);
|
|
106
|
+
return I.current?.focus(), n;
|
|
107
|
+
},
|
|
108
|
+
onError: T
|
|
109
|
+
}, e.id))
|
|
110
|
+
}),
|
|
111
|
+
N.hasMore && /* @__PURE__ */ v(i, {
|
|
112
|
+
variant: "outline",
|
|
113
|
+
disabled: N.loadingMore || N.loading,
|
|
114
|
+
onClick: () => void N.loadMore(),
|
|
115
|
+
children: k(N.loadingMore ? "notifications.loading" : "notifications.loadMore")
|
|
116
|
+
})
|
|
117
|
+
] })]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function E({ item: t, renderItem: n, renderActions: r, onRead: a, onDismiss: s, onError: d }) {
|
|
121
|
+
let { t: f } = e(), p = u("notifications.read", d), h = u("notifications.dismiss", d), g = m(), _ = t.status === "open" ? f("notifications.open") : t.status === "resolved" ? f("notifications.resolved") : t.status === "dismissed" ? f("notifications.dismissed") : void 0;
|
|
122
|
+
return /* @__PURE__ */ y("li", {
|
|
123
|
+
className: b("grid gap-3 p-4", !t.read_at && "bg-primary/5"),
|
|
124
|
+
"data-ckui": "notification-item",
|
|
125
|
+
"data-unread": !t.read_at,
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ y("div", {
|
|
128
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
129
|
+
children: [
|
|
130
|
+
!t.read_at && /* @__PURE__ */ v(o, {
|
|
131
|
+
variant: "secondary",
|
|
132
|
+
children: f("notifications.unread")
|
|
133
|
+
}),
|
|
134
|
+
_ && /* @__PURE__ */ v(o, {
|
|
135
|
+
variant: t.status === "open" ? "outline" : "secondary",
|
|
136
|
+
children: _
|
|
137
|
+
}),
|
|
138
|
+
/* @__PURE__ */ v(l, { at: t.created_at })
|
|
139
|
+
]
|
|
140
|
+
}),
|
|
141
|
+
/* @__PURE__ */ v("div", {
|
|
142
|
+
id: g,
|
|
143
|
+
className: "min-w-0 break-words",
|
|
144
|
+
children: n(t)
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ y("div", {
|
|
147
|
+
className: "flex flex-wrap items-center gap-2",
|
|
148
|
+
children: [
|
|
149
|
+
r?.(t),
|
|
150
|
+
/* @__PURE__ */ v(i, {
|
|
151
|
+
size: "sm",
|
|
152
|
+
variant: "ghost",
|
|
153
|
+
"aria-describedby": g,
|
|
154
|
+
disabled: p.pending || h.pending,
|
|
155
|
+
onClick: () => void p.run(() => a(t.id, !t.read_at)),
|
|
156
|
+
children: f(t.read_at ? "notifications.markUnread" : "notifications.markRead")
|
|
157
|
+
}),
|
|
158
|
+
/* @__PURE__ */ v(i, {
|
|
159
|
+
size: "sm",
|
|
160
|
+
variant: "ghost",
|
|
161
|
+
"aria-describedby": g,
|
|
162
|
+
disabled: p.pending || h.pending,
|
|
163
|
+
onClick: () => void h.run(() => s(t.id, !t.dismissed_at)),
|
|
164
|
+
children: f(t.dismissed_at ? "notifications.restore" : "notifications.dismiss")
|
|
165
|
+
})
|
|
166
|
+
]
|
|
167
|
+
}),
|
|
168
|
+
/* @__PURE__ */ v(c, { error: p.error ?? h.error })
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function D({ appearance: a, className: o, unreadRefreshInterval: s, ...c }) {
|
|
173
|
+
let { t: l } = e(), u = d({
|
|
174
|
+
client: c.client,
|
|
175
|
+
refreshInterval: s
|
|
176
|
+
}), f = n(a), m = r(c.onError);
|
|
177
|
+
return p(() => {
|
|
178
|
+
u.error && m(u.error, "notifications.load");
|
|
179
|
+
}, [u.error, m]), /* @__PURE__ */ y(w.Root, {
|
|
180
|
+
onOpenChange: (e) => {
|
|
181
|
+
e && u.reload();
|
|
182
|
+
},
|
|
183
|
+
children: [/* @__PURE__ */ v(t, {
|
|
184
|
+
appearance: a,
|
|
185
|
+
className: b("inline-flex", o),
|
|
186
|
+
"data-ckui": "notification-bell",
|
|
187
|
+
children: /* @__PURE__ */ y(w.Trigger, {
|
|
188
|
+
render: /* @__PURE__ */ v(i, {
|
|
189
|
+
variant: "ghost",
|
|
190
|
+
size: "icon"
|
|
191
|
+
}),
|
|
192
|
+
"aria-label": l(u.unread ? "notifications.unreadLabel" : "notifications.title"),
|
|
193
|
+
className: "relative",
|
|
194
|
+
children: [/* @__PURE__ */ v(C, {
|
|
195
|
+
icon: x,
|
|
196
|
+
strokeWidth: 2
|
|
197
|
+
}), u.unread && /* @__PURE__ */ v("span", {
|
|
198
|
+
className: "absolute right-1.5 top-1.5 size-2 rounded-full bg-primary ring-2 ring-background",
|
|
199
|
+
"aria-hidden": "true",
|
|
200
|
+
"data-ckui": "notification-unread"
|
|
201
|
+
})]
|
|
202
|
+
})
|
|
203
|
+
}), /* @__PURE__ */ v(w.Portal, { children: /* @__PURE__ */ v(w.Positioner, {
|
|
204
|
+
...f,
|
|
205
|
+
className: b(f.className, "z-50"),
|
|
206
|
+
sideOffset: 8,
|
|
207
|
+
align: "end",
|
|
208
|
+
children: /* @__PURE__ */ v(w.Popup, {
|
|
209
|
+
className: "max-h-[min(80vh,40rem)] w-[min(26rem,calc(100vw-2rem))] overflow-auto rounded-xl border border-border bg-background p-4 text-foreground shadow-lg outline-none",
|
|
210
|
+
"aria-label": l("notifications.title"),
|
|
211
|
+
children: /* @__PURE__ */ v(T, {
|
|
212
|
+
...c,
|
|
213
|
+
appearance: a
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
}) })]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
export { D as NotificationBell, T as NotificationInbox };
|
|
221
|
+
|
|
222
|
+
//# sourceMappingURL=notifications.js.map
|