@emdash-cms/admin 0.34.0 → 0.35.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 (149) hide show
  1. package/dist/{LocaleDirectionProvider-B56zZskH.js → LocaleDirectionProvider-CnKC3o_O.js} +29 -29
  2. package/dist/{LocaleDirectionProvider-B56zZskH.js.map → LocaleDirectionProvider-CnKC3o_O.js.map} +1 -1
  3. package/dist/index.d.ts +11 -7
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +719 -212
  6. package/dist/index.js.map +1 -1
  7. package/dist/locales/ar/messages.mjs +1 -1
  8. package/dist/locales/ca/messages.mjs +1 -1
  9. package/dist/locales/cs/messages.mjs +1 -1
  10. package/dist/locales/de/messages.mjs +1 -1
  11. package/dist/locales/en/messages.mjs +1 -1
  12. package/dist/locales/en-GB/messages.mjs +1 -1
  13. package/dist/locales/es-419/messages.mjs +1 -1
  14. package/dist/locales/es-ES/messages.mjs +1 -1
  15. package/dist/locales/eu/messages.mjs +1 -1
  16. package/dist/locales/fa/messages.mjs +1 -1
  17. package/dist/locales/fr/messages.mjs +1 -1
  18. package/dist/locales/hi/messages.mjs +1 -1
  19. package/dist/locales/hu/messages.mjs +1 -1
  20. package/dist/locales/id/messages.mjs +1 -1
  21. package/dist/locales/index.js +1 -1
  22. package/dist/locales/ja/messages.mjs +1 -1
  23. package/dist/locales/ko/messages.mjs +1 -1
  24. package/dist/locales/nb/messages.mjs +1 -1
  25. package/dist/locales/nl/messages.mjs +1 -1
  26. package/dist/locales/pl/messages.mjs +1 -1
  27. package/dist/locales/pseudo/messages.mjs +1 -1
  28. package/dist/locales/pt-BR/messages.mjs +1 -1
  29. package/dist/locales/sr-Latn/messages.mjs +1 -1
  30. package/dist/locales/sv/messages.mjs +1 -1
  31. package/dist/locales/th/messages.mjs +1 -1
  32. package/dist/locales/tr/messages.mjs +1 -1
  33. package/dist/locales/uk/messages.mjs +1 -1
  34. package/dist/locales/zh-CN/messages.mjs +1 -1
  35. package/dist/locales/zh-TW/messages.mjs +1 -1
  36. package/dist/messages-1TWaf8a2.js +6 -0
  37. package/dist/messages-1TWaf8a2.js.map +1 -0
  38. package/dist/messages-B5sF4YmM.js +6 -0
  39. package/dist/messages-B5sF4YmM.js.map +1 -0
  40. package/dist/messages-B9T8Kc4V.js +6 -0
  41. package/dist/messages-B9T8Kc4V.js.map +1 -0
  42. package/dist/messages-BOwqz7Dx.js +6 -0
  43. package/dist/messages-BOwqz7Dx.js.map +1 -0
  44. package/dist/messages-B_M5MNtr.js +6 -0
  45. package/dist/messages-B_M5MNtr.js.map +1 -0
  46. package/dist/messages-Bh-BQAdD.js +6 -0
  47. package/dist/messages-Bh-BQAdD.js.map +1 -0
  48. package/dist/messages-ByjvPBKi.js +6 -0
  49. package/dist/messages-ByjvPBKi.js.map +1 -0
  50. package/dist/messages-C4xWRT0x.js +6 -0
  51. package/dist/messages-C4xWRT0x.js.map +1 -0
  52. package/dist/messages-CAdtKwS0.js +6 -0
  53. package/dist/messages-CAdtKwS0.js.map +1 -0
  54. package/dist/messages-CEdjuD3T.js +6 -0
  55. package/dist/messages-CEdjuD3T.js.map +1 -0
  56. package/dist/messages-CYCJB96m.js +6 -0
  57. package/dist/messages-CYCJB96m.js.map +1 -0
  58. package/dist/messages-C_ry9289.js +6 -0
  59. package/dist/messages-C_ry9289.js.map +1 -0
  60. package/dist/messages-CbGBMnfZ.js +6 -0
  61. package/dist/messages-CbGBMnfZ.js.map +1 -0
  62. package/dist/messages-Cpt4WQXi.js +6 -0
  63. package/dist/messages-Cpt4WQXi.js.map +1 -0
  64. package/dist/messages-D0Q9lbJr.js +6 -0
  65. package/dist/messages-D0Q9lbJr.js.map +1 -0
  66. package/dist/messages-D6SjorpS.js +6 -0
  67. package/dist/messages-D6SjorpS.js.map +1 -0
  68. package/dist/messages-DACzFKkc.js +6 -0
  69. package/dist/messages-DACzFKkc.js.map +1 -0
  70. package/dist/messages-DD7UzBCg.js +6 -0
  71. package/dist/messages-DD7UzBCg.js.map +1 -0
  72. package/dist/messages-DQhv3VpQ.js +6 -0
  73. package/dist/messages-DQhv3VpQ.js.map +1 -0
  74. package/dist/messages-DkmI0StQ.js +6 -0
  75. package/dist/messages-DkmI0StQ.js.map +1 -0
  76. package/dist/messages-DwVVrprv.js +6 -0
  77. package/dist/messages-DwVVrprv.js.map +1 -0
  78. package/dist/messages-LPfCWgcA.js +6 -0
  79. package/dist/messages-LPfCWgcA.js.map +1 -0
  80. package/dist/messages-NbBfxfDD.js +6 -0
  81. package/dist/messages-NbBfxfDD.js.map +1 -0
  82. package/dist/messages-UfXVNVGm.js +6 -0
  83. package/dist/messages-UfXVNVGm.js.map +1 -0
  84. package/dist/messages-kV2LI902.js +6 -0
  85. package/dist/messages-kV2LI902.js.map +1 -0
  86. package/dist/messages-lZ7KdJCT.js +6 -0
  87. package/dist/messages-lZ7KdJCT.js.map +1 -0
  88. package/dist/messages-nYVfh2dw.js +6 -0
  89. package/dist/messages-nYVfh2dw.js.map +1 -0
  90. package/dist/messages-ttfIlVd2.js +6 -0
  91. package/dist/messages-ttfIlVd2.js.map +1 -0
  92. package/dist/styles.css +1 -1
  93. package/package.json +2 -2
  94. package/dist/messages-4l4q4Avf.js +0 -6
  95. package/dist/messages-4l4q4Avf.js.map +0 -1
  96. package/dist/messages-8Ng0p6M4.js +0 -6
  97. package/dist/messages-8Ng0p6M4.js.map +0 -1
  98. package/dist/messages-BGJ61mbR.js +0 -6
  99. package/dist/messages-BGJ61mbR.js.map +0 -1
  100. package/dist/messages-BJjz3hov.js +0 -6
  101. package/dist/messages-BJjz3hov.js.map +0 -1
  102. package/dist/messages-BV7vld-9.js +0 -6
  103. package/dist/messages-BV7vld-9.js.map +0 -1
  104. package/dist/messages-BVaZauc8.js +0 -6
  105. package/dist/messages-BVaZauc8.js.map +0 -1
  106. package/dist/messages-BZ7favjd.js +0 -6
  107. package/dist/messages-BZ7favjd.js.map +0 -1
  108. package/dist/messages-BdxqPqXL.js +0 -6
  109. package/dist/messages-BdxqPqXL.js.map +0 -1
  110. package/dist/messages-BfSXfW7E.js +0 -6
  111. package/dist/messages-BfSXfW7E.js.map +0 -1
  112. package/dist/messages-CPTfUKBv.js +0 -6
  113. package/dist/messages-CPTfUKBv.js.map +0 -1
  114. package/dist/messages-CQS1a4au.js +0 -6
  115. package/dist/messages-CQS1a4au.js.map +0 -1
  116. package/dist/messages-CXtDjstE.js +0 -6
  117. package/dist/messages-CXtDjstE.js.map +0 -1
  118. package/dist/messages-Chlpzhy7.js +0 -6
  119. package/dist/messages-Chlpzhy7.js.map +0 -1
  120. package/dist/messages-D4t7iekH.js +0 -6
  121. package/dist/messages-D4t7iekH.js.map +0 -1
  122. package/dist/messages-D546HP55.js +0 -6
  123. package/dist/messages-D546HP55.js.map +0 -1
  124. package/dist/messages-DJ24Nb04.js +0 -6
  125. package/dist/messages-DJ24Nb04.js.map +0 -1
  126. package/dist/messages-DR6ynPV7.js +0 -6
  127. package/dist/messages-DR6ynPV7.js.map +0 -1
  128. package/dist/messages-Ddk7AuUS.js +0 -6
  129. package/dist/messages-Ddk7AuUS.js.map +0 -1
  130. package/dist/messages-DizwSrvr.js +0 -6
  131. package/dist/messages-DizwSrvr.js.map +0 -1
  132. package/dist/messages-Dn1fHt89.js +0 -6
  133. package/dist/messages-Dn1fHt89.js.map +0 -1
  134. package/dist/messages-DynzD2co.js +0 -6
  135. package/dist/messages-DynzD2co.js.map +0 -1
  136. package/dist/messages-DzCOsjCj.js +0 -6
  137. package/dist/messages-DzCOsjCj.js.map +0 -1
  138. package/dist/messages-JIi-gAO0.js +0 -6
  139. package/dist/messages-JIi-gAO0.js.map +0 -1
  140. package/dist/messages-Ve8shT7p.js +0 -6
  141. package/dist/messages-Ve8shT7p.js.map +0 -1
  142. package/dist/messages-f3phxB2R.js +0 -6
  143. package/dist/messages-f3phxB2R.js.map +0 -1
  144. package/dist/messages-rngIIrhI.js +0 -6
  145. package/dist/messages-rngIIrhI.js.map +0 -1
  146. package/dist/messages-sKfxXmTC.js +0 -6
  147. package/dist/messages-sKfxXmTC.js.map +0 -1
  148. package/dist/messages-tGLAc0ra.js +0 -6
  149. package/dist/messages-tGLAc0ra.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as fetchPlugins, c as updatePluginSettings, d as fetchAuthMode, f as fetchManifest, i as fetchPluginSettings, l as API_BASE, m as throwResponseError, n as enablePlugin, p as parseApiResponse, r as fetchPlugin, s as setPluginMcpEnabled, t as disablePlugin, u as apiFetch } from "./plugins-CQAV8Ap0.js";
2
- import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-B56zZskH.js";
2
+ import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, s as getLocaleDir, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-CnKC3o_O.js";
3
3
  import "./locales/index.js";
4
4
  import { slugify } from "./slugify.js";
5
5
  import { Autocomplete, Badge, Banner, Button, Checkbox, ClipboardText, Collapsible, Combobox, CommandPalette, Dialog, DropdownMenu, Field, Input, InputArea, Label, LayerCard, LinkButton, LinkProvider, Loader, Popover, Select, Sidebar, Sidebar as KumoSidebar, SkeletonLine, Switch, Tabs, Text, Toast, Toasty, Tooltip, TooltipProvider, buttonVariants, inputVariants, useKumoToastManager, useSidebar, useSidebar as useSidebar$1 } from "@cloudflare/kumo";
@@ -11,7 +11,7 @@ import * as React$1 from "react";
11
11
  import React, { createContext, isValidElement, useCallback, useContext, useEffect, useRef, useState } from "react";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
13
  import { Dialog as Dialog$1 } from "@cloudflare/kumo/primitives";
14
- import { Archive, ArrowCounterClockwise, ArrowDown, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowUp, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, Bell, BookOpen, BracketsAngle, BracketsCurly, Browser, Calendar, CalendarBlank, CalendarDots, CardsThree, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChartBar, ChartLine, Chats, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, ColumnsPlusLeft, ColumnsPlusRight, Copy, Crop, Cube, Database, DeviceMobile, DotsSixVertical, Download, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, Faders, File, FileArrowDown, FileText, Files, FloppyDisk, Folder, FolderOpen, Folders, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, IdentificationCard, Image as Image$1, ImageBroken, ImageSquare, Images, ImagesSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, LockKey, MagnifyingGlass, Medal, Minus, Monitor, Moon, Newspaper, Palette, PaperPlaneTilt, Paperclip, Paragraph, Path, Pencil, PencilSimple, Plug, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Robot, Rows, RowsPlusBottom, RowsPlusTop, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, Signature, SlidersHorizontal, Sparkle, SquaresFour, Stack, Star, Storefront, Sun, Table, Tag, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextH, TextHFive, TextHFour, TextHOne, TextHSix, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextSubscript, TextSuperscript, TextT, TextUnderline, ToggleLeft, Trash, Trophy, Upload, User, UserCircle, UserCircleIcon, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
14
+ import { Archive, ArrowClockwise, ArrowCounterClockwise, ArrowDown, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowUp, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, Bell, BookOpen, BracketsAngle, BracketsCurly, Browser, Calendar, CalendarBlank, CalendarDots, CardsThree, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChartBar, ChartLine, Chats, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, ColumnsPlusLeft, ColumnsPlusRight, Copy, Crop, Cube, Database, DeviceMobile, DotsSixVertical, Download, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, Faders, File, FileArrowDown, FileAudio, FileImage, FilePdf, FileText, FileVideo, Files, FloppyDisk, Folder, FolderOpen, Folders, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, IdentificationCard, Image as Image$1, ImageBroken, ImageSquare, Images, ImagesSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, LockKey, MagnifyingGlass, Medal, Minus, Monitor, Moon, Newspaper, Palette, PaperPlaneTilt, Paperclip, Paragraph, Path, Pencil, PencilSimple, Plug, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Robot, Rows, RowsPlusBottom, RowsPlusTop, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, Signature, SlidersHorizontal, Sparkle, SquaresFour, Stack, Star, Storefront, Sun, Table, Tag, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextH, TextHFive, TextHFour, TextHOne, TextHSix, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextSubscript, TextSuperscript, TextT, TextUnderline, ToggleLeft, Trash, Trophy, Upload, UploadSimple, User, UserCircle, UserCircleIcon, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
15
15
  import { clsx } from "clsx";
16
16
  import { twMerge } from "tailwind-merge";
17
17
  import { DndContext, DragOverlay, KeyboardSensor, MeasuringStrategy, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
@@ -511,20 +511,25 @@ async function fetchMediaList(options) {
511
511
  * Used to resolve an id-only reference (e.g. a byline's `avatarMediaId`)
512
512
  * back into a full media item for display.
513
513
  */
514
- async function fetchMediaItem(id) {
515
- return (await parseApiResponse(await apiFetch(`${API_BASE}/media/${id}`), i18n._({
514
+ async function fetchMediaItem(id, options) {
515
+ return (await parseApiResponse(await apiFetch(`${API_BASE}/media/${id}`, { signal: options?.signal }), i18n._({
516
516
  id: "8rtatN",
517
517
  message: "Failed to fetch media item"
518
518
  }))).item;
519
519
  }
520
520
  const MAX_CLIENT_HASH_BYTES = 8 * 1024 * 1024;
521
- async function computeContentHash(file) {
521
+ async function computeContentHash(file, signal) {
522
+ signal?.throwIfAborted();
522
523
  const subtle = globalThis.crypto?.subtle;
523
524
  if (!subtle || file.size === 0 || file.size > MAX_CLIENT_HASH_BYTES) return void 0;
524
525
  try {
525
- const hash = await subtle.digest("SHA-1", await file.arrayBuffer());
526
+ const bytes = await file.arrayBuffer();
527
+ signal?.throwIfAborted();
528
+ const hash = await subtle.digest("SHA-1", bytes);
529
+ signal?.throwIfAborted();
526
530
  return `sha1:${Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
527
531
  } catch {
532
+ signal?.throwIfAborted();
528
533
  return;
529
534
  }
530
535
  }
@@ -534,10 +539,11 @@ async function computeContentHash(file) {
534
539
  */
535
540
  async function getUploadUrl(file, opts) {
536
541
  try {
537
- const contentHash = await computeContentHash(file);
542
+ const contentHash = await computeContentHash(file, opts?.signal);
538
543
  const response = await apiFetch(`${API_BASE}/media/upload-url`, {
539
544
  method: "POST",
540
545
  headers: { "Content-Type": "application/json" },
546
+ signal: opts?.signal,
541
547
  body: JSON.stringify({
542
548
  filename: file.name,
543
549
  contentType: file.type,
@@ -552,6 +558,7 @@ async function getUploadUrl(file, opts) {
552
558
  message: "Failed to get upload URL"
553
559
  }));
554
560
  } catch (error) {
561
+ opts?.signal?.throwIfAborted();
555
562
  if (error instanceof TypeError && error.message.includes("fetch")) return null;
556
563
  throw error;
557
564
  }
@@ -559,11 +566,12 @@ async function getUploadUrl(file, opts) {
559
566
  /**
560
567
  * Confirm upload after uploading to signed URL
561
568
  */
562
- async function confirmUpload(mediaId, metadata) {
569
+ async function confirmUpload(mediaId, metadata, options) {
563
570
  return (await parseApiResponse(await apiFetch(`${API_BASE}/media/${mediaId}/confirm`, {
564
571
  method: "POST",
565
572
  headers: { "Content-Type": "application/json" },
566
- body: JSON.stringify(metadata || {})
573
+ body: JSON.stringify(metadata || {}),
574
+ signal: options?.signal
567
575
  }), i18n._({
568
576
  id: "QZDXMs",
569
577
  message: "Failed to confirm upload"
@@ -572,14 +580,15 @@ async function confirmUpload(mediaId, metadata) {
572
580
  /**
573
581
  * Upload directly to signed URL
574
582
  */
575
- async function uploadToSignedUrl(file, uploadInfo) {
583
+ async function uploadToSignedUrl(file, uploadInfo, options) {
576
584
  const response = await fetch(uploadInfo.uploadUrl, {
577
585
  method: uploadInfo.method,
578
586
  headers: {
579
587
  ...uploadInfo.headers,
580
588
  "Content-Type": file.type
581
589
  },
582
- body: file
590
+ body: file,
591
+ signal: options?.signal
583
592
  });
584
593
  if (!response.ok) await throwResponseError(response, i18n._({
585
594
  id: "4Gz2wC",
@@ -589,29 +598,47 @@ async function uploadToSignedUrl(file, uploadInfo) {
589
598
  /**
590
599
  * Get image dimensions from a file
591
600
  */
592
- async function getImageDimensions(file) {
601
+ async function getImageDimensions(file, options) {
602
+ options?.signal?.throwIfAborted();
593
603
  if (!file.type.startsWith("image/")) return null;
594
- return new Promise((resolve) => {
604
+ return new Promise((resolve, reject) => {
595
605
  const img = new Image();
606
+ const objectUrl = URL.createObjectURL(file);
607
+ const cleanup = () => {
608
+ img.onload = null;
609
+ img.onerror = null;
610
+ options?.signal?.removeEventListener("abort", handleAbort);
611
+ URL.revokeObjectURL(objectUrl);
612
+ };
613
+ const handleAbort = () => {
614
+ cleanup();
615
+ reject(options?.signal?.reason);
616
+ };
596
617
  img.onload = () => {
597
- resolve({
618
+ const dimensions = {
598
619
  width: img.naturalWidth,
599
620
  height: img.naturalHeight
600
- });
601
- URL.revokeObjectURL(img.src);
621
+ };
622
+ cleanup();
623
+ resolve(dimensions);
602
624
  };
603
625
  img.onerror = () => {
626
+ cleanup();
604
627
  resolve(null);
605
- URL.revokeObjectURL(img.src);
606
628
  };
607
- img.src = URL.createObjectURL(file);
629
+ options?.signal?.addEventListener("abort", handleAbort, { once: true });
630
+ if (options?.signal?.aborted) {
631
+ handleAbort();
632
+ return;
633
+ }
634
+ img.src = objectUrl;
608
635
  });
609
636
  }
610
637
  /**
611
638
  * Upload media file via direct upload (legacy/local storage)
612
639
  */
613
640
  async function uploadMediaDirect(file, opts) {
614
- const dimensions = await getImageDimensions(file);
641
+ const dimensions = await getImageDimensions(file, opts);
615
642
  const formData = new FormData();
616
643
  formData.append("file", file);
617
644
  if (dimensions?.width) formData.append("width", String(dimensions.width));
@@ -619,7 +646,8 @@ async function uploadMediaDirect(file, opts) {
619
646
  if (opts?.fieldId) formData.append("fieldId", opts.fieldId);
620
647
  return (await parseApiResponse(await apiFetch(`${API_BASE}/media`, {
621
648
  method: "POST",
622
- body: formData
649
+ body: formData,
650
+ signal: opts?.signal
623
651
  }), i18n._({
624
652
  id: "5IFNMM",
625
653
  message: "Failed to upload media"
@@ -632,16 +660,17 @@ async function uploadMediaDirect(file, opts) {
632
660
  * (for local storage) if signed URLs are not supported.
633
661
  */
634
662
  async function uploadMedia(file, opts) {
663
+ opts?.signal?.throwIfAborted();
635
664
  const uploadInfo = await getUploadUrl(file, opts);
636
665
  if (!uploadInfo) return uploadMediaDirect(file, opts);
637
- if ("existing" in uploadInfo) return fetchMediaItem(uploadInfo.mediaId);
638
- await uploadToSignedUrl(file, uploadInfo);
639
- const dimensions = await getImageDimensions(file);
666
+ if ("existing" in uploadInfo) return fetchMediaItem(uploadInfo.mediaId, opts);
667
+ await uploadToSignedUrl(file, uploadInfo, opts);
668
+ const dimensions = await getImageDimensions(file, opts);
640
669
  return confirmUpload(uploadInfo.mediaId, {
641
670
  size: file.size,
642
671
  width: dimensions?.width,
643
672
  height: dimensions?.height
644
- });
673
+ }, opts);
645
674
  }
646
675
  /**
647
676
  * Delete media
@@ -695,13 +724,15 @@ async function fetchProviderMedia(providerId, options) {
695
724
  /**
696
725
  * Upload media to a specific provider
697
726
  */
698
- async function uploadToProvider(providerId, file, alt) {
727
+ async function uploadToProvider(providerId, file, alt, options) {
728
+ options?.signal?.throwIfAborted();
699
729
  const formData = new FormData();
700
730
  formData.append("file", file);
701
731
  if (alt) formData.append("alt", alt);
702
732
  return (await parseApiResponse(await apiFetch(`${API_BASE}/media/providers/${providerId}`, {
703
733
  method: "POST",
704
- body: formData
734
+ body: formData,
735
+ signal: options?.signal
705
736
  }), i18n._({
706
737
  id: "j66jlb",
707
738
  message: "Failed to upload to provider"
@@ -26714,6 +26745,514 @@ function formatDate$1(isoString) {
26714
26745
  });
26715
26746
  }
26716
26747
 
26748
+ //#endregion
26749
+ //#region src/components/MediaUploadDialog.tsx
26750
+ const LOCAL_MEDIA_UPLOAD_ACCEPT = "image/png,image/jpeg,image/gif,image/webp,video/*,audio/*,application/pdf";
26751
+ const DEFAULT_CONCURRENCY = 3;
26752
+ const MAX_CONCURRENCY = 6;
26753
+ const MAX_VISIBLE_ROWS = 100;
26754
+ const MAX_PREVIEW_BYTES = 8 * 1024 * 1024;
26755
+ const PREVIEW_MIME_TYPES = new Set([
26756
+ "image/jpeg",
26757
+ "image/png",
26758
+ "image/gif",
26759
+ "image/webp"
26760
+ ]);
26761
+ function previewUrlFor(file) {
26762
+ if (file.size > MAX_PREVIEW_BYTES || !PREVIEW_MIME_TYPES.has(file.type)) return void 0;
26763
+ try {
26764
+ return URL.createObjectURL(file);
26765
+ } catch {
26766
+ return;
26767
+ }
26768
+ }
26769
+ function FileKindIcon({ file }) {
26770
+ const className = "h-5 w-5";
26771
+ if (file.type.startsWith("image/")) return /* @__PURE__ */ jsx(FileImage, {
26772
+ className,
26773
+ "aria-hidden": "true"
26774
+ });
26775
+ if (file.type.startsWith("video/")) return /* @__PURE__ */ jsx(FileVideo, {
26776
+ className,
26777
+ "aria-hidden": "true"
26778
+ });
26779
+ if (file.type.startsWith("audio/")) return /* @__PURE__ */ jsx(FileAudio, {
26780
+ className,
26781
+ "aria-hidden": "true"
26782
+ });
26783
+ if (file.type === "application/pdf") return /* @__PURE__ */ jsx(FilePdf, {
26784
+ className,
26785
+ "aria-hidden": "true"
26786
+ });
26787
+ if (file.type.startsWith("text/") || file.type.includes("document")) return /* @__PURE__ */ jsx(FileText, {
26788
+ className,
26789
+ "aria-hidden": "true"
26790
+ });
26791
+ return /* @__PURE__ */ jsx(File, {
26792
+ className,
26793
+ "aria-hidden": "true"
26794
+ });
26795
+ }
26796
+ function UploadStatusLabel({ status }) {
26797
+ const { _: _t } = useLingui();
26798
+ if (status === "uploading") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader, { size: "sm" }), _t({
26799
+ id: "/eguJc",
26800
+ message: "Uploading"
26801
+ })] });
26802
+ if (status === "complete") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CheckCircle, {
26803
+ className: "h-4 w-4 text-kumo-success",
26804
+ weight: "fill",
26805
+ "aria-hidden": "true"
26806
+ }), _t({
26807
+ id: "bD8I7O",
26808
+ message: "Complete"
26809
+ })] });
26810
+ if (status === "failed") return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(WarningCircle, {
26811
+ className: "h-4 w-4 text-kumo-danger",
26812
+ weight: "fill",
26813
+ "aria-hidden": "true"
26814
+ }), _t({
26815
+ id: "b3Thhd",
26816
+ message: "Upload failed"
26817
+ })] });
26818
+ return /* @__PURE__ */ jsx(Fragment, { children: _t({
26819
+ id: "8wu9lr",
26820
+ message: "Queued"
26821
+ }) });
26822
+ }
26823
+ function UploadFileRow({ row, onCancel, onRetry, onRemove }) {
26824
+ const { _: _t2 } = useLingui();
26825
+ const isBusy = row.status === "queued" || row.status === "uploading";
26826
+ return /* @__PURE__ */ jsxs("li", {
26827
+ className: "grid grid-cols-[2.5rem_minmax(0,1fr)_auto] items-center gap-3 rounded-lg bg-kumo-base px-3 py-2.5 ring ring-kumo-line",
26828
+ children: [
26829
+ /* @__PURE__ */ jsx("div", {
26830
+ className: "flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-md bg-kumo-tint text-kumo-subtle",
26831
+ children: row.previewUrl ? /* @__PURE__ */ jsx("img", {
26832
+ src: row.previewUrl,
26833
+ alt: "",
26834
+ loading: "lazy",
26835
+ className: "h-full w-full object-cover"
26836
+ }) : /* @__PURE__ */ jsx(FileKindIcon, { file: row.file })
26837
+ }),
26838
+ /* @__PURE__ */ jsxs("div", {
26839
+ className: "min-w-0",
26840
+ children: [/* @__PURE__ */ jsx("p", {
26841
+ className: "line-clamp-2 text-sm font-medium leading-5",
26842
+ title: row.file.name,
26843
+ children: row.file.name
26844
+ }), /* @__PURE__ */ jsxs("div", {
26845
+ className: "mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-kumo-subtle",
26846
+ children: [
26847
+ /* @__PURE__ */ jsx("span", {
26848
+ className: "inline-flex items-center gap-1.5",
26849
+ children: /* @__PURE__ */ jsx(UploadStatusLabel, { status: row.status })
26850
+ }),
26851
+ /* @__PURE__ */ jsx("span", {
26852
+ "aria-hidden": "true",
26853
+ children: "·"
26854
+ }),
26855
+ /* @__PURE__ */ jsx("span", {
26856
+ className: "tabular-nums",
26857
+ children: formatFileSize(row.file.size)
26858
+ })
26859
+ ]
26860
+ })]
26861
+ }),
26862
+ /* @__PURE__ */ jsxs("div", {
26863
+ className: "flex items-center gap-1",
26864
+ children: [row.status === "failed" && /* @__PURE__ */ jsx(Button, {
26865
+ variant: "ghost",
26866
+ shape: "square",
26867
+ size: "sm",
26868
+ onClick: onRetry,
26869
+ "aria-label": _t2({
26870
+ id: "XUH9K1",
26871
+ message: "Retry {0}",
26872
+ values: { 0: row.file.name }
26873
+ }),
26874
+ icon: /* @__PURE__ */ jsx(ArrowClockwise, { "aria-hidden": "true" })
26875
+ }), /* @__PURE__ */ jsx(Button, {
26876
+ variant: "ghost",
26877
+ shape: "square",
26878
+ size: "sm",
26879
+ onClick: isBusy ? onCancel : onRemove,
26880
+ "aria-label": isBusy ? _t2({
26881
+ id: "x9WS62",
26882
+ message: "Cancel {0}",
26883
+ values: { 0: row.file.name }
26884
+ }) : row.status === "complete" ? _t2({
26885
+ id: "UsbCOm",
26886
+ message: "Dismiss completed {0}",
26887
+ values: { 0: row.file.name }
26888
+ }) : _t2({
26889
+ id: "w61W3L",
26890
+ message: "Remove {0}",
26891
+ values: { 0: row.file.name }
26892
+ }),
26893
+ icon: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
26894
+ })]
26895
+ })
26896
+ ]
26897
+ });
26898
+ }
26899
+ function MediaUploadDialog({ open, providerName, accept, enqueueRequest, onEnqueueRequestConsumed, onOpenChange, onCloseComplete, onQueueIdle, upload, concurrency = DEFAULT_CONCURRENCY }) {
26900
+ const { _: _t3 } = useLingui();
26901
+ const [rows, setRows] = React$1.useState([]);
26902
+ const [overflowCount, setOverflowCount] = React$1.useState(0);
26903
+ const rowsRef = React$1.useRef([]);
26904
+ const activeRef = React$1.useRef(/* @__PURE__ */ new Map());
26905
+ const nextIdRef = React$1.useRef(0);
26906
+ const lastRequestIdRef = React$1.useRef(null);
26907
+ const busyRunRef = React$1.useRef(false);
26908
+ const unmountingRef = React$1.useRef(false);
26909
+ const inputRef = React$1.useRef(null);
26910
+ const requestedConcurrency = Number.isFinite(concurrency) ? Math.floor(concurrency) : DEFAULT_CONCURRENCY;
26911
+ const maxConcurrent = Math.min(MAX_CONCURRENCY, Math.max(1, requestedConcurrency));
26912
+ const updateRows = React$1.useCallback((update) => {
26913
+ const next = update(rowsRef.current);
26914
+ rowsRef.current = next;
26915
+ setRows(next);
26916
+ }, []);
26917
+ const addFiles = React$1.useCallback((files) => {
26918
+ const available = Math.max(0, MAX_VISIBLE_ROWS - rowsRef.current.length);
26919
+ const accepted = files.slice(0, available);
26920
+ setOverflowCount(Math.max(0, files.length - accepted.length));
26921
+ if (accepted.length === 0) return;
26922
+ busyRunRef.current = true;
26923
+ const added = accepted.map((file) => ({
26924
+ id: nextIdRef.current += 1,
26925
+ file,
26926
+ status: "queued",
26927
+ attempt: 1,
26928
+ previewUrl: previewUrlFor(file)
26929
+ }));
26930
+ updateRows((current) => [...current, ...added]);
26931
+ }, [updateRows]);
26932
+ React$1.useEffect(() => {
26933
+ if (!open || !enqueueRequest || enqueueRequest.id === lastRequestIdRef.current) return;
26934
+ lastRequestIdRef.current = enqueueRequest.id;
26935
+ addFiles(enqueueRequest.files);
26936
+ onEnqueueRequestConsumed(enqueueRequest.id);
26937
+ }, [
26938
+ addFiles,
26939
+ enqueueRequest,
26940
+ onEnqueueRequestConsumed,
26941
+ open
26942
+ ]);
26943
+ React$1.useEffect(() => {
26944
+ const slots = maxConcurrent - activeRef.current.size;
26945
+ if (slots <= 0) return;
26946
+ const candidates = rowsRef.current.filter((row) => row.status === "queued" && !activeRef.current.has(row.id)).slice(0, slots);
26947
+ if (candidates.length === 0) return;
26948
+ for (const row of candidates) {
26949
+ const controller = new AbortController();
26950
+ activeRef.current.set(row.id, {
26951
+ attempt: row.attempt,
26952
+ controller
26953
+ });
26954
+ updateRows((current) => current.map((item) => item.id === row.id && item.attempt === row.attempt ? {
26955
+ ...item,
26956
+ status: "uploading"
26957
+ } : item));
26958
+ Promise.resolve().then(() => upload(row.file, { signal: controller.signal })).then(() => {
26959
+ if (activeRef.current.get(row.id)?.attempt !== row.attempt) return void 0;
26960
+ activeRef.current.delete(row.id);
26961
+ updateRows((current) => current.map((item) => item.id === row.id && item.attempt === row.attempt ? {
26962
+ ...item,
26963
+ status: "complete"
26964
+ } : item));
26965
+ }, () => {
26966
+ if (activeRef.current.get(row.id)?.attempt !== row.attempt) return void 0;
26967
+ activeRef.current.delete(row.id);
26968
+ if (controller.signal.aborted) return void 0;
26969
+ updateRows((current) => current.map((item) => item.id === row.id && item.attempt === row.attempt ? {
26970
+ ...item,
26971
+ status: "failed"
26972
+ } : item));
26973
+ });
26974
+ }
26975
+ }, [
26976
+ maxConcurrent,
26977
+ rows,
26978
+ updateRows,
26979
+ upload
26980
+ ]);
26981
+ const hasUnfinished = rows.some((row) => row.status === "queued" || row.status === "uploading");
26982
+ React$1.useEffect(() => {
26983
+ if (hasUnfinished || !busyRunRef.current || unmountingRef.current) return;
26984
+ busyRunRef.current = false;
26985
+ onQueueIdle?.();
26986
+ }, [hasUnfinished, onQueueIdle]);
26987
+ React$1.useEffect(() => () => {
26988
+ unmountingRef.current = true;
26989
+ activeRef.current.forEach(({ controller }) => controller.abort());
26990
+ activeRef.current.clear();
26991
+ rowsRef.current.forEach((row) => row.previewUrl && URL.revokeObjectURL(row.previewUrl));
26992
+ }, []);
26993
+ const removeRow = React$1.useCallback((id) => {
26994
+ const row = rowsRef.current.find((item) => item.id === id);
26995
+ if (!row) return;
26996
+ activeRef.current.get(id)?.controller.abort();
26997
+ activeRef.current.delete(id);
26998
+ if (row.previewUrl) URL.revokeObjectURL(row.previewUrl);
26999
+ updateRows((current) => current.filter((item) => item.id !== id));
27000
+ }, [updateRows]);
27001
+ const retryRow = React$1.useCallback((id) => {
27002
+ busyRunRef.current = true;
27003
+ updateRows((current) => current.map((row) => row.id === id && row.status === "failed" ? {
27004
+ ...row,
27005
+ status: "queued",
27006
+ attempt: row.attempt + 1
27007
+ } : row));
27008
+ }, [updateRows]);
27009
+ const retryFailed = () => {
27010
+ if (!rowsRef.current.some((row) => row.status === "failed")) return;
27011
+ busyRunRef.current = true;
27012
+ updateRows((current) => current.map((row) => row.status === "failed" ? {
27013
+ ...row,
27014
+ status: "queued",
27015
+ attempt: row.attempt + 1
27016
+ } : row));
27017
+ };
27018
+ const cancelRemaining = () => {
27019
+ activeRef.current.forEach(({ controller }) => controller.abort());
27020
+ activeRef.current.clear();
27021
+ rowsRef.current.filter((row) => row.status === "queued" || row.status === "uploading").forEach((row) => row.previewUrl && URL.revokeObjectURL(row.previewUrl));
27022
+ updateRows((current) => current.filter((row) => row.status !== "queued" && row.status !== "uploading"));
27023
+ };
27024
+ const clearCompleted = () => {
27025
+ rowsRef.current.filter((row) => row.status === "complete").forEach((row) => row.previewUrl && URL.revokeObjectURL(row.previewUrl));
27026
+ updateRows((current) => current.filter((row) => row.status !== "complete"));
27027
+ };
27028
+ const reset = () => {
27029
+ activeRef.current.forEach(({ controller }) => controller.abort());
27030
+ activeRef.current.clear();
27031
+ rowsRef.current.forEach((row) => row.previewUrl && URL.revokeObjectURL(row.previewUrl));
27032
+ rowsRef.current = [];
27033
+ setRows([]);
27034
+ setOverflowCount(0);
27035
+ busyRunRef.current = false;
27036
+ };
27037
+ const completedCount = rows.filter((row) => row.status === "complete").length;
27038
+ const failedCount = rows.filter((row) => row.status === "failed").length;
27039
+ const liveMessage = hasUnfinished ? _t3({
27040
+ id: "9XrV3P",
27041
+ message: "{completedCount} of {0} uploads complete",
27042
+ values: {
27043
+ 0: rows.length,
27044
+ completedCount
27045
+ }
27046
+ }) : failedCount > 0 ? i18n._({
27047
+ id: "65Ew2q",
27048
+ message: "{failedCount, plural, one {# upload failed} other {# uploads failed}}",
27049
+ values: { failedCount }
27050
+ }) : rows.length > 0 ? _t3({
27051
+ id: "h0W4PA",
27052
+ message: "All uploads finished"
27053
+ }) : "";
27054
+ return /* @__PURE__ */ jsx(Dialog.Root, {
27055
+ open,
27056
+ onOpenChange: (nextOpen) => {
27057
+ if (!nextOpen && hasUnfinished) return;
27058
+ onOpenChange(nextOpen);
27059
+ },
27060
+ onOpenChangeComplete: (nextOpen) => {
27061
+ if (nextOpen) return;
27062
+ reset();
27063
+ onCloseComplete();
27064
+ },
27065
+ disablePointerDismissal: hasUnfinished,
27066
+ children: /* @__PURE__ */ jsxs(Dialog, {
27067
+ size: "lg",
27068
+ className: "flex max-h-[min(88dvh,46rem)] flex-col overflow-hidden p-0",
27069
+ style: { width: "min(94vw, 42rem)" },
27070
+ children: [
27071
+ /* @__PURE__ */ jsxs("div", {
27072
+ className: "flex shrink-0 items-start justify-between gap-4 border-b border-kumo-line px-6 py-5",
27073
+ children: [/* @__PURE__ */ jsxs("div", {
27074
+ className: "min-w-0",
27075
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
27076
+ className: "text-lg font-semibold leading-tight",
27077
+ children: _t3({
27078
+ id: "NKvTB9",
27079
+ message: "Upload to {providerName}",
27080
+ values: { providerName }
27081
+ })
27082
+ }), /* @__PURE__ */ jsx(Dialog.Description, {
27083
+ className: "mt-1 text-sm leading-5 text-kumo-subtle",
27084
+ children: _t3({
27085
+ id: "FAQCNs",
27086
+ message: "Files upload as soon as you add them."
27087
+ })
27088
+ })]
27089
+ }), /* @__PURE__ */ jsx(Button, {
27090
+ variant: "ghost",
27091
+ shape: "square",
27092
+ size: "sm",
27093
+ disabled: hasUnfinished,
27094
+ onClick: () => onOpenChange(false),
27095
+ "aria-label": _t3({
27096
+ id: "yz7wBu",
27097
+ message: "Close"
27098
+ }),
27099
+ icon: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
27100
+ })]
27101
+ }),
27102
+ /* @__PURE__ */ jsxs("div", {
27103
+ className: "min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-6 py-5",
27104
+ children: [
27105
+ /* @__PURE__ */ jsxs("div", {
27106
+ className: "flex min-h-64 flex-col items-center justify-center gap-4 rounded-xl border-[3px] border-dashed border-kumo-subtle/60 bg-kumo-base px-6 py-10 text-center sm:min-h-72",
27107
+ onDragOver: (event) => event.preventDefault(),
27108
+ onDrop: (event) => {
27109
+ event.preventDefault();
27110
+ event.stopPropagation();
27111
+ addFiles([...event.dataTransfer.files]);
27112
+ },
27113
+ children: [
27114
+ /* @__PURE__ */ jsx("div", {
27115
+ className: "flex size-14 items-center justify-center rounded-full text-kumo-subtle ring ring-kumo-line",
27116
+ children: /* @__PURE__ */ jsx(UploadSimple, {
27117
+ className: "h-7 w-7",
27118
+ weight: "regular",
27119
+ "aria-hidden": "true"
27120
+ })
27121
+ }),
27122
+ /* @__PURE__ */ jsxs("div", {
27123
+ className: "space-y-1",
27124
+ children: [/* @__PURE__ */ jsx("p", {
27125
+ className: "text-lg font-semibold leading-6",
27126
+ children: _t3({
27127
+ id: "+ywUTE",
27128
+ message: "Drag and drop files here"
27129
+ })
27130
+ }), /* @__PURE__ */ jsx("p", {
27131
+ className: "text-sm leading-5 text-kumo-subtle",
27132
+ children: _t3({
27133
+ id: "DbFk6j",
27134
+ message: "Or browse files from your computer"
27135
+ })
27136
+ })]
27137
+ }),
27138
+ /* @__PURE__ */ jsx(Button, {
27139
+ variant: "outline",
27140
+ size: "base",
27141
+ className: "text-sm",
27142
+ onClick: () => inputRef.current?.click(),
27143
+ icon: /* @__PURE__ */ jsx(UploadSimple, { "aria-hidden": "true" }),
27144
+ children: _t3({
27145
+ id: "JYj5R2",
27146
+ message: "Browse files"
27147
+ })
27148
+ }),
27149
+ /* @__PURE__ */ jsx("input", {
27150
+ ref: inputRef,
27151
+ type: "file",
27152
+ multiple: true,
27153
+ accept,
27154
+ className: "sr-only",
27155
+ tabIndex: -1,
27156
+ "aria-label": _t3({
27157
+ id: "0Io5gK",
27158
+ message: "Browse files to upload"
27159
+ }),
27160
+ onChange: (event) => {
27161
+ addFiles([...event.currentTarget.files ?? []]);
27162
+ event.currentTarget.value = "";
27163
+ }
27164
+ })
27165
+ ]
27166
+ }),
27167
+ overflowCount > 0 && /* @__PURE__ */ jsx("p", {
27168
+ role: "alert",
27169
+ className: "text-sm text-kumo-danger",
27170
+ children: i18n._({
27171
+ id: "VINwRP",
27172
+ message: "{overflowCount, plural, one {# file was not added because the upload list is full.} other {# files were not added because the upload list is full.}}",
27173
+ values: { overflowCount }
27174
+ })
27175
+ }),
27176
+ rows.length > 0 && /* @__PURE__ */ jsx("ul", {
27177
+ className: "space-y-2",
27178
+ children: rows.map((row) => /* @__PURE__ */ jsx(UploadFileRow, {
27179
+ row,
27180
+ onCancel: () => removeRow(row.id),
27181
+ onRetry: () => retryRow(row.id),
27182
+ onRemove: () => removeRow(row.id)
27183
+ }, row.id))
27184
+ })
27185
+ ]
27186
+ }),
27187
+ /* @__PURE__ */ jsxs("div", {
27188
+ className: "flex shrink-0 flex-wrap items-center justify-between gap-3 border-t border-kumo-line px-6 py-4",
27189
+ children: [/* @__PURE__ */ jsx("p", {
27190
+ className: "text-sm text-kumo-subtle tabular-nums",
27191
+ "aria-hidden": "true",
27192
+ children: rows.length > 0 ? _t3({
27193
+ id: "0zZ04n",
27194
+ message: "{completedCount} of {0} complete",
27195
+ values: {
27196
+ 0: rows.length,
27197
+ completedCount
27198
+ }
27199
+ }) : _t3({
27200
+ id: "UGK1Dr",
27201
+ message: "No files added"
27202
+ })
27203
+ }), /* @__PURE__ */ jsxs("div", {
27204
+ className: "flex flex-wrap items-center justify-end gap-2",
27205
+ children: [
27206
+ completedCount > 0 && /* @__PURE__ */ jsx(Button, {
27207
+ variant: "ghost",
27208
+ size: "sm",
27209
+ onClick: clearCompleted,
27210
+ children: _t3({
27211
+ id: "3Cz86c",
27212
+ message: "Clear completed"
27213
+ })
27214
+ }),
27215
+ failedCount > 0 && /* @__PURE__ */ jsx(Button, {
27216
+ variant: "outline",
27217
+ size: "sm",
27218
+ onClick: retryFailed,
27219
+ icon: /* @__PURE__ */ jsx(ArrowClockwise, { "aria-hidden": "true" }),
27220
+ children: _t3({
27221
+ id: "H+XODI",
27222
+ message: "Retry failed"
27223
+ })
27224
+ }),
27225
+ hasUnfinished ? /* @__PURE__ */ jsx(Button, {
27226
+ variant: "outline",
27227
+ size: "sm",
27228
+ onClick: cancelRemaining,
27229
+ children: _t3({
27230
+ id: "GLYB+o",
27231
+ message: "Cancel remaining"
27232
+ })
27233
+ }) : /* @__PURE__ */ jsx(Button, {
27234
+ variant: "primary",
27235
+ size: "sm",
27236
+ onClick: () => onOpenChange(false),
27237
+ children: _t3({
27238
+ id: "DPfwMq",
27239
+ message: "Done"
27240
+ })
27241
+ })
27242
+ ]
27243
+ })]
27244
+ }),
27245
+ /* @__PURE__ */ jsx("span", {
27246
+ className: "sr-only",
27247
+ role: "status",
27248
+ "aria-live": "polite",
27249
+ children: liveMessage
27250
+ })
27251
+ ]
27252
+ })
27253
+ });
27254
+ }
27255
+
26717
27256
  //#endregion
26718
27257
  //#region src/components/MediaLibrary.tsx
26719
27258
  /** Maps a coarse type-filter choice to the media list's `mimeType` filter. */
@@ -26747,8 +27286,13 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26747
27286
  activeProvider,
26748
27287
  onLocalSearchChange
26749
27288
  ]);
26750
- const [uploadState, setUploadState] = React$1.useState({ status: "idle" });
26751
- const fileInputRef = React$1.useRef(null);
27289
+ const [uploadDialogOpen, setUploadDialogOpen] = React$1.useState(false);
27290
+ const [enqueueRequest, setEnqueueRequest] = React$1.useState(null);
27291
+ const [uploadTarget, setUploadTarget] = React$1.useState(null);
27292
+ const [isFileDragActive, setIsFileDragActive] = React$1.useState(false);
27293
+ const enqueueIdRef = React$1.useRef(0);
27294
+ const dragDepthRef = React$1.useRef(0);
27295
+ const returnFocusRef = React$1.useRef(null);
26752
27296
  const [loadedDimensions, setLoadedDimensions] = React$1.useState({});
26753
27297
  const { data: providers } = useQuery({
26754
27298
  queryKey: ["media-providers"],
@@ -26787,6 +27331,8 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26787
27331
  providers,
26788
27332
  _t
26789
27333
  ]);
27334
+ const canUpload = activeProviderInfo?.capabilities.upload ?? false;
27335
+ const canSearch = activeProviderInfo?.capabilities.search ?? false;
26790
27336
  const cancelPendingDetailOpen = React$1.useCallback(() => {
26791
27337
  if (detailOpenFrameRef.current === null) return;
26792
27338
  window.cancelAnimationFrame(detailOpenFrameRef.current);
@@ -26809,130 +27355,76 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26809
27355
  const handleDetailClosed = React$1.useCallback(() => {
26810
27356
  setDetailItem(null);
26811
27357
  }, []);
27358
+ const enqueueFiles = React$1.useCallback((files, returnFocus) => {
27359
+ if (!canUpload || !activeProviderInfo || files.length === 0) return;
27360
+ if (returnFocus) returnFocusRef.current = returnFocus;
27361
+ setUploadTarget({
27362
+ id: activeProviderInfo.id,
27363
+ name: activeProviderInfo.name
27364
+ });
27365
+ setEnqueueRequest({
27366
+ id: enqueueIdRef.current += 1,
27367
+ files
27368
+ });
27369
+ setUploadDialogOpen(true);
27370
+ }, [activeProviderInfo, canUpload]);
27371
+ const openUploadDialog = (event) => {
27372
+ if (!canUpload || !activeProviderInfo) return;
27373
+ returnFocusRef.current = event.currentTarget;
27374
+ setUploadTarget({
27375
+ id: activeProviderInfo.id,
27376
+ name: activeProviderInfo.name
27377
+ });
27378
+ setEnqueueRequest(null);
27379
+ setUploadDialogOpen(true);
27380
+ };
26812
27381
  React$1.useEffect(() => {
26813
- if (uploadState.status === "success" || uploadState.status === "error") {
26814
- const timer = setTimeout(() => {
26815
- setUploadState({ status: "idle" });
26816
- }, 3e3);
26817
- return () => clearTimeout(timer);
26818
- }
26819
- }, [uploadState.status]);
26820
- const handleFileSelect = async (e) => {
26821
- const files = e.target.files;
26822
- if (files && files.length > 0) {
26823
- const fileArray = [...files];
26824
- const total = fileArray.length;
26825
- if (activeProvider === "local") {
26826
- setUploadState({
26827
- status: "uploading",
26828
- progress: {
26829
- current: 0,
26830
- total
26831
- }
26832
- });
26833
- let uploaded = 0;
26834
- let failed = 0;
26835
- for (const file of fileArray) {
26836
- try {
26837
- await onUpload?.(file);
26838
- uploaded++;
26839
- } catch (error) {
26840
- console.error("Upload failed:", error);
26841
- failed++;
26842
- }
26843
- setUploadState({
26844
- status: "uploading",
26845
- progress: {
26846
- current: uploaded + failed,
26847
- total
26848
- }
26849
- });
26850
- }
26851
- if (failed === 0) setUploadState({
26852
- status: "success",
26853
- message: i18n._({
26854
- id: "btYZgm",
26855
- message: "{total, plural, one {File uploaded} other {# files uploaded}}",
26856
- values: { total }
26857
- })
26858
- });
26859
- else if (uploaded === 0) setUploadState({
26860
- status: "error",
26861
- message: i18n._({
26862
- id: "TUbRbs",
26863
- message: "{total, plural, one {Upload failed} other {All # uploads failed}}",
26864
- values: { total }
26865
- })
26866
- });
26867
- else setUploadState({
26868
- status: "error",
26869
- message: _t({
26870
- id: "f5MgHH",
26871
- message: "{uploaded} uploaded, {failed} failed",
26872
- values: {
26873
- uploaded,
26874
- failed
26875
- }
26876
- })
26877
- });
26878
- } else if (activeProviderInfo?.capabilities.upload) {
26879
- setUploadState({
26880
- status: "uploading",
26881
- progress: {
26882
- current: 0,
26883
- total
26884
- }
26885
- });
26886
- let uploaded = 0;
26887
- let failed = 0;
26888
- for (const file of fileArray) {
26889
- try {
26890
- await uploadToProvider(activeProvider, file);
26891
- uploaded++;
26892
- } catch (error) {
26893
- console.error("Upload failed:", error);
26894
- failed++;
26895
- }
26896
- setUploadState({
26897
- status: "uploading",
26898
- progress: {
26899
- current: uploaded + failed,
26900
- total
26901
- }
26902
- });
26903
- }
26904
- if (failed === 0) setUploadState({
26905
- status: "success",
26906
- message: i18n._({
26907
- id: "btYZgm",
26908
- message: "{total, plural, one {File uploaded} other {# files uploaded}}",
26909
- values: { total }
26910
- })
26911
- });
26912
- else if (uploaded === 0) setUploadState({
26913
- status: "error",
26914
- message: i18n._({
26915
- id: "TUbRbs",
26916
- message: "{total, plural, one {Upload failed} other {All # uploads failed}}",
26917
- values: { total }
26918
- })
26919
- });
26920
- else setUploadState({
26921
- status: "error",
26922
- message: _t({
26923
- id: "f5MgHH",
26924
- message: "{uploaded} uploaded, {failed} failed",
26925
- values: {
26926
- uploaded,
26927
- failed
26928
- }
26929
- })
26930
- });
26931
- refetchProviderMedia();
27382
+ const hasFiles = (event) => event.dataTransfer?.types.includes("Files") ?? false;
27383
+ const resetDrag = () => {
27384
+ dragDepthRef.current = 0;
27385
+ setIsFileDragActive(false);
27386
+ };
27387
+ const handleDragEnter = (event) => {
27388
+ if (!hasFiles(event)) return;
27389
+ event.preventDefault();
27390
+ if (uploadDialogOpen || !canUpload) return;
27391
+ dragDepthRef.current += 1;
27392
+ setIsFileDragActive(true);
27393
+ };
27394
+ const handleDragOver = (event) => {
27395
+ if (hasFiles(event)) event.preventDefault();
27396
+ };
27397
+ const handleDragLeave = (event) => {
27398
+ if (dragDepthRef.current === 0 || uploadDialogOpen || !canUpload) return;
27399
+ if (event.relatedTarget === null) {
27400
+ resetDrag();
27401
+ return;
26932
27402
  }
26933
- }
26934
- if (fileInputRef.current) fileInputRef.current.value = "";
26935
- };
27403
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
27404
+ if (dragDepthRef.current === 0) setIsFileDragActive(false);
27405
+ };
27406
+ const handleDrop = (event) => {
27407
+ if (!hasFiles(event)) return;
27408
+ event.preventDefault();
27409
+ resetDrag();
27410
+ if (uploadDialogOpen || !canUpload) return;
27411
+ enqueueFiles([...event.dataTransfer?.files ?? []], mediaHeadingRef.current);
27412
+ };
27413
+ window.addEventListener("dragenter", handleDragEnter);
27414
+ window.addEventListener("dragover", handleDragOver);
27415
+ window.addEventListener("dragleave", handleDragLeave);
27416
+ window.addEventListener("drop", handleDrop);
27417
+ return () => {
27418
+ window.removeEventListener("dragenter", handleDragEnter);
27419
+ window.removeEventListener("dragover", handleDragOver);
27420
+ window.removeEventListener("dragleave", handleDragLeave);
27421
+ window.removeEventListener("drop", handleDrop);
27422
+ };
27423
+ }, [
27424
+ canUpload,
27425
+ enqueueFiles,
27426
+ uploadDialogOpen
27427
+ ]);
26936
27428
  const providerTabs = React$1.useMemo(() => {
26937
27429
  const tabs = [{
26938
27430
  id: "local",
@@ -26954,8 +27446,6 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26954
27446
  const currentItems = activeProvider === "local" ? items : [];
26955
27447
  const currentProviderItems = activeProvider !== "local" ? providerData?.items || [] : [];
26956
27448
  const currentLoading = activeProvider === "local" ? isLoading : providerLoading;
26957
- const canUpload = activeProviderInfo?.capabilities.upload ?? false;
26958
- const canSearch = activeProviderInfo?.capabilities.search ?? false;
26959
27449
  const resultCount = activeProvider === "local" ? currentItems.length : currentProviderItems.length;
26960
27450
  const hasMoreCurrentItems = activeProvider === "local" ? Boolean(hasMore) : Boolean(providerData?.nextCursor);
26961
27451
  const resultCountText = resultCount > 0 && !hasMoreCurrentItems ? i18n._({
@@ -26976,9 +27466,51 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26976
27466
  if (activeProvider === "local" && next.trim() === "") onLocalSearchChange?.("");
26977
27467
  };
26978
27468
  const showToolbar = resultCount > 0 || hasActiveQuery;
27469
+ const uploadFile = React$1.useCallback(async (file, options) => {
27470
+ if (!uploadTarget) throw new Error("Upload target unavailable");
27471
+ if (uploadTarget.id === "local") {
27472
+ if (!onUpload) throw new Error("Upload callback unavailable");
27473
+ await onUpload(file, options);
27474
+ return;
27475
+ }
27476
+ await uploadToProvider(uploadTarget.id, file, void 0, options);
27477
+ }, [onUpload, uploadTarget]);
27478
+ const handleUploadDialogClosed = React$1.useCallback(() => {
27479
+ setEnqueueRequest(null);
27480
+ setUploadTarget(null);
27481
+ const returnTarget = returnFocusRef.current;
27482
+ returnFocusRef.current = null;
27483
+ window.requestAnimationFrame(() => {
27484
+ (returnTarget?.isConnected ? returnTarget : mediaHeadingRef.current)?.focus();
27485
+ });
27486
+ }, []);
27487
+ const handleUploadQueueIdle = React$1.useCallback(() => {
27488
+ if (uploadTarget?.id !== "local") refetchProviderMedia();
27489
+ }, [refetchProviderMedia, uploadTarget?.id]);
26979
27490
  return /* @__PURE__ */ jsxs("div", {
26980
27491
  className: "space-y-4",
27492
+ "data-media-library": true,
26981
27493
  children: [
27494
+ isFileDragActive && /* @__PURE__ */ jsx("div", {
27495
+ className: "pointer-events-none fixed inset-0 z-50 bg-kumo-base/70 p-4 backdrop-blur-sm sm:p-8",
27496
+ "aria-hidden": "true",
27497
+ children: /* @__PURE__ */ jsx("div", {
27498
+ className: "flex h-full items-center justify-center rounded-2xl border-[3px] border-dashed border-kumo-brand/80 bg-kumo-tint/70",
27499
+ children: /* @__PURE__ */ jsxs("div", {
27500
+ className: "flex flex-col items-center gap-3 text-center",
27501
+ children: [/* @__PURE__ */ jsx(Upload, {
27502
+ className: "h-10 w-10 text-kumo-subtle",
27503
+ "aria-hidden": "true"
27504
+ }), /* @__PURE__ */ jsx("p", {
27505
+ className: "text-lg font-semibold",
27506
+ children: _t({
27507
+ id: "8o3dPc",
27508
+ message: "Drop files to upload"
27509
+ })
27510
+ })]
27511
+ })
27512
+ })
27513
+ }),
26982
27514
  /* @__PURE__ */ jsxs("div", {
26983
27515
  className: "flex flex-wrap items-center justify-between gap-4",
26984
27516
  children: [/* @__PURE__ */ jsx("h1", {
@@ -26989,62 +27521,20 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
26989
27521
  id: "ia4TsE",
26990
27522
  message: "Media Library"
26991
27523
  })
26992
- }), /* @__PURE__ */ jsxs("div", {
27524
+ }), /* @__PURE__ */ jsx("div", {
26993
27525
  className: "flex items-center gap-3",
26994
- children: [
26995
- uploadState.status === "uploading" && /* @__PURE__ */ jsxs("div", {
26996
- className: "flex items-center gap-2 text-sm text-kumo-subtle",
26997
- children: [/* @__PURE__ */ jsx(Loader, { size: "sm" }), /* @__PURE__ */ jsx("span", { children: uploadState.progress && uploadState.progress.total > 1 ? _t({
26998
- id: "oplKZn",
26999
- message: "Uploading {0}/{1}...",
27000
- values: {
27001
- 0: uploadState.progress.current,
27002
- 1: uploadState.progress.total
27003
- }
27004
- }) : _t({
27005
- id: "GxkJXS",
27006
- message: "Uploading..."
27007
- }) })]
27008
- }),
27009
- uploadState.status === "success" && /* @__PURE__ */ jsxs("div", {
27010
- className: "flex items-center gap-2 text-sm text-kumo-success",
27011
- children: [/* @__PURE__ */ jsx(Check, {
27012
- className: "h-4 w-4",
27013
- "aria-hidden": "true"
27014
- }), /* @__PURE__ */ jsx("span", { children: uploadState.message })]
27015
- }),
27016
- uploadState.status === "error" && /* @__PURE__ */ jsxs("div", {
27017
- className: "flex items-center gap-2 text-sm text-kumo-danger",
27018
- children: [/* @__PURE__ */ jsx(X, {
27019
- className: "h-4 w-4",
27020
- "aria-hidden": "true"
27021
- }), /* @__PURE__ */ jsx("span", { children: uploadState.message })]
27022
- }),
27023
- canUpload && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
27024
- onClick: () => fileInputRef.current?.click(),
27025
- disabled: uploadState.status === "uploading",
27026
- icon: uploadState.status === "uploading" ? /* @__PURE__ */ jsx(Loader, { size: "sm" }) : /* @__PURE__ */ jsx(Upload, {}),
27027
- children: _t({
27028
- id: "f9mKbV",
27029
- message: "Upload to {0}",
27030
- values: { 0: activeProviderInfo?.name || _t({
27031
- id: "exYcTF",
27032
- message: "Library"
27033
- }) }
27034
- })
27035
- }), /* @__PURE__ */ jsx("input", {
27036
- ref: fileInputRef,
27037
- type: "file",
27038
- multiple: true,
27039
- accept: "image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx",
27040
- className: "sr-only",
27041
- onChange: handleFileSelect,
27042
- "aria-label": _t({
27043
- id: "sCtyHQ",
27044
- message: "Upload files"
27045
- })
27046
- })] })
27047
- ]
27526
+ children: canUpload && /* @__PURE__ */ jsx(Button, {
27527
+ onClick: openUploadDialog,
27528
+ icon: /* @__PURE__ */ jsx(Upload, {}),
27529
+ children: _t({
27530
+ id: "f9mKbV",
27531
+ message: "Upload to {0}",
27532
+ values: { 0: activeProviderInfo?.name || _t({
27533
+ id: "exYcTF",
27534
+ message: "Library"
27535
+ }) }
27536
+ })
27537
+ })
27048
27538
  })]
27049
27539
  }),
27050
27540
  providerTabs.length > 1 && /* @__PURE__ */ jsx(Tabs, {
@@ -27220,7 +27710,7 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
27220
27710
  message: "Upload images, videos, and documents to keep reusable assets in one place."
27221
27711
  }),
27222
27712
  action: /* @__PURE__ */ jsx(Button, {
27223
- onClick: () => fileInputRef.current?.click(),
27713
+ onClick: openUploadDialog,
27224
27714
  icon: /* @__PURE__ */ jsx(Upload, {}),
27225
27715
  children: _t({
27226
27716
  id: "HAKBY9",
@@ -27256,7 +27746,7 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
27256
27746
  message: "Upload media to keep reusable assets in one place."
27257
27747
  }),
27258
27748
  action: /* @__PURE__ */ jsx(Button, {
27259
- onClick: () => fileInputRef.current?.click(),
27749
+ onClick: openUploadDialog,
27260
27750
  icon: /* @__PURE__ */ jsx(Upload, {}),
27261
27751
  children: _t({
27262
27752
  id: "HAKBY9",
@@ -27388,6 +27878,20 @@ function MediaLibrary({ items = [], isLoading, onUpload, onItemUpdated, hasMore,
27388
27878
  })
27389
27879
  })
27390
27880
  }),
27881
+ /* @__PURE__ */ jsx(MediaUploadDialog, {
27882
+ open: uploadDialogOpen,
27883
+ providerName: uploadTarget?.name ?? activeProviderInfo?.name ?? _t({
27884
+ id: "exYcTF",
27885
+ message: "Library"
27886
+ }),
27887
+ accept: uploadTarget?.id === "local" ? LOCAL_MEDIA_UPLOAD_ACCEPT : void 0,
27888
+ enqueueRequest,
27889
+ onEnqueueRequestConsumed: (id) => setEnqueueRequest((current) => current?.id === id ? null : current),
27890
+ onOpenChange: setUploadDialogOpen,
27891
+ onCloseComplete: handleUploadDialogClosed,
27892
+ onQueueIdle: handleUploadQueueIdle,
27893
+ upload: uploadFile
27894
+ }),
27391
27895
  detailItem && /* @__PURE__ */ jsx(MediaDetailPanel, {
27392
27896
  open: isDetailOpen,
27393
27897
  item: detailItem,
@@ -47719,7 +48223,7 @@ function MediaPage() {
47719
48223
  getNextPageParam: (lastPage) => lastPage.nextCursor
47720
48224
  });
47721
48225
  const uploadMutation = useMutation({
47722
- mutationFn: (file) => uploadMedia(file),
48226
+ mutationFn: ({ file, options }) => uploadMedia(file, options),
47723
48227
  onSuccess: () => {
47724
48228
  queryClient.invalidateQueries({ queryKey: ["media"] });
47725
48229
  }
@@ -47733,8 +48237,11 @@ function MediaPage() {
47733
48237
  isLoading: isLoading || isFetchingNextPage,
47734
48238
  hasMore: !!hasNextPage,
47735
48239
  onLoadMore: () => void fetchNextPage(),
47736
- onUpload: async (file) => {
47737
- await uploadMutation.mutateAsync(file);
48240
+ onUpload: async (file, options) => {
48241
+ await uploadMutation.mutateAsync({
48242
+ file,
48243
+ options
48244
+ });
47738
48245
  },
47739
48246
  onLocalSearchChange: setSearch,
47740
48247
  onLocalMimeFilterChange: setMimeFilter