@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.
Files changed (73) hide show
  1. package/README.md +52 -0
  2. package/dist/client/client.d.ts +3 -0
  3. package/dist/client/client.js +20 -18
  4. package/dist/client/client.js.map +1 -1
  5. package/dist/client/generated/routes.d.ts +66 -1
  6. package/dist/client/generated/routes.js +85 -0
  7. package/dist/client/generated/routes.js.map +1 -1
  8. package/dist/client/generated/wire.d.ts +30 -0
  9. package/dist/client/generated/wire.js.map +1 -1
  10. package/dist/client/http.d.ts +8 -1
  11. package/dist/client/http.js.map +1 -1
  12. package/dist/client/index.d.ts +2 -1
  13. package/dist/client/media/queue.d.ts +1 -1
  14. package/dist/client/media/queue.js +1 -1
  15. package/dist/client/media/queue.js.map +1 -1
  16. package/dist/client/notifications.d.ts +23 -0
  17. package/dist/client/notifications.js +46 -0
  18. package/dist/client/notifications.js.map +1 -0
  19. package/dist/client.js +5 -4
  20. package/dist/components/admin/media-item-panel.d.ts +27 -0
  21. package/dist/components/admin/media-item-panel.js +397 -0
  22. package/dist/components/admin/media-item-panel.js.map +1 -0
  23. package/dist/components/admin/upload-queue.d.ts +23 -0
  24. package/dist/components/admin/upload-queue.js +365 -0
  25. package/dist/components/admin/upload-queue.js.map +1 -0
  26. package/dist/components/notifications.d.ts +28 -0
  27. package/dist/components/notifications.js +222 -0
  28. package/dist/components/notifications.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +5 -2
  31. package/dist/locales/admin/de.js +51 -0
  32. package/dist/locales/admin/de.js.map +1 -1
  33. package/dist/locales/admin/en.d.ts +51 -0
  34. package/dist/locales/admin/en.js +51 -0
  35. package/dist/locales/admin/en.js.map +1 -1
  36. package/dist/locales/admin/es.js +51 -0
  37. package/dist/locales/admin/es.js.map +1 -1
  38. package/dist/locales/admin/ja.js +51 -0
  39. package/dist/locales/admin/ja.js.map +1 -1
  40. package/dist/locales/admin/ko.js +51 -0
  41. package/dist/locales/admin/ko.js.map +1 -1
  42. package/dist/locales/admin/zh.js +51 -0
  43. package/dist/locales/admin/zh.js.map +1 -1
  44. package/dist/locales/en.d.ts +74 -0
  45. package/dist/locales/social/de.js +23 -0
  46. package/dist/locales/social/de.js.map +1 -1
  47. package/dist/locales/social/en.d.ts +23 -0
  48. package/dist/locales/social/en.js +23 -0
  49. package/dist/locales/social/en.js.map +1 -1
  50. package/dist/locales/social/es.js +23 -0
  51. package/dist/locales/social/es.js.map +1 -1
  52. package/dist/locales/social/ja.js +23 -0
  53. package/dist/locales/social/ja.js.map +1 -1
  54. package/dist/locales/social/ko.js +23 -0
  55. package/dist/locales/social/ko.js.map +1 -1
  56. package/dist/locales/social/zh.js +23 -0
  57. package/dist/locales/social/zh.js.map +1 -1
  58. package/dist/react/context.d.ts +1 -1
  59. package/dist/react/context.js.map +1 -1
  60. package/dist/react/folder.js.map +1 -1
  61. package/dist/react/index.d.ts +1 -0
  62. package/dist/react/notifications.d.ts +29 -0
  63. package/dist/react/notifications.js +56 -0
  64. package/dist/react/notifications.js.map +1 -0
  65. package/dist/react/resources.d.ts +1 -0
  66. package/dist/react/resources.js +26 -0
  67. package/dist/react/resources.js.map +1 -1
  68. package/dist/react/upload.d.ts +10 -10
  69. package/dist/react/upload.js +1 -1
  70. package/dist/react/upload.js.map +1 -1
  71. package/dist/react.js +4 -3
  72. package/dist/styles.css +1 -1
  73. 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