react-os-shell 3.10.1 → 3.11.2

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 (31) hide show
  1. package/dist/{Browser-3SDMP5NU.js → Browser-FX577ZG2.js} +4 -4
  2. package/dist/{Browser-3SDMP5NU.js.map → Browser-FX577ZG2.js.map} +1 -1
  3. package/dist/{Documents-FOCBCZHY.js → Documents-JJKFUSJ6.js} +3 -3
  4. package/dist/{Documents-FOCBCZHY.js.map → Documents-JJKFUSJ6.js.map} +1 -1
  5. package/dist/Files-L5SN24XK.js +12 -0
  6. package/dist/{Files-P37UZJVD.js.map → Files-L5SN24XK.js.map} +1 -1
  7. package/dist/{Notepad-WDDJQVXF.js → Notepad-S2EZKPZQ.js} +3 -3
  8. package/dist/{Notepad-WDDJQVXF.js.map → Notepad-S2EZKPZQ.js.map} +1 -1
  9. package/dist/Preview-BSQJ3GGL.js +8 -0
  10. package/dist/{Preview-CESYNTOE.js.map → Preview-BSQJ3GGL.js.map} +1 -1
  11. package/dist/{Spreadsheet-6SICRWKK.js → Spreadsheet-HF47TRAV.js} +4 -4
  12. package/dist/{Spreadsheet-6SICRWKK.js.map → Spreadsheet-HF47TRAV.js.map} +1 -1
  13. package/dist/apps/index.js +12 -12
  14. package/dist/{chunk-OZFGITVF.js → chunk-AW2BPHVS.js} +3 -3
  15. package/dist/{chunk-OZFGITVF.js.map → chunk-AW2BPHVS.js.map} +1 -1
  16. package/dist/{chunk-WEQXKNOA.js → chunk-BI5XBNI5.js} +3 -3
  17. package/dist/{chunk-WEQXKNOA.js.map → chunk-BI5XBNI5.js.map} +1 -1
  18. package/dist/{chunk-3SOG3MFC.js → chunk-CCIP3ADU.js} +4 -4
  19. package/dist/{chunk-3SOG3MFC.js.map → chunk-CCIP3ADU.js.map} +1 -1
  20. package/dist/{chunk-SRFLPGGO.js → chunk-NZLE6YR6.js} +3 -3
  21. package/dist/{chunk-SRFLPGGO.js.map → chunk-NZLE6YR6.js.map} +1 -1
  22. package/dist/{chunk-YPIDICFR.js → chunk-OKFJBJY6.js} +3 -3
  23. package/dist/{chunk-YPIDICFR.js.map → chunk-OKFJBJY6.js.map} +1 -1
  24. package/dist/{chunk-L5SPKS76.js → chunk-P3DVY4XJ.js} +4 -4
  25. package/dist/{chunk-L5SPKS76.js.map → chunk-P3DVY4XJ.js.map} +1 -1
  26. package/dist/index.d.ts +138 -2
  27. package/dist/index.js +555 -60
  28. package/dist/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/dist/Files-P37UZJVD.js +0 -12
  31. package/dist/Preview-CESYNTOE.js +0 -8
package/dist/index.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import { subscribePomo, getPomoSnapshot } from './chunk-VGJ3KCOP.js';
2
2
  export { setShellTodoProvider } from './chunk-VGJ3KCOP.js';
3
- import { PREVIEW_OPENED_EVENT, publishDesktopFolders, requestFilesTrashView, FolderGlyph, openPreviewFile, requestFilesDesktopFolderView, FileIconTile, hashGradient } from './chunk-L5SPKS76.js';
4
- export { Breadcrumbs } from './chunk-L5SPKS76.js';
3
+ import { PREVIEW_OPENED_EVENT, publishDesktopFolders, requestFilesTrashView, FolderGlyph, openPreviewFile, requestFilesDesktopFolderView, FileIconTile, hashGradient } from './chunk-P3DVY4XJ.js';
4
+ export { Breadcrumbs } from './chunk-P3DVY4XJ.js';
5
5
  import { SidebarLayout } from './chunk-YG45DAUT.js';
6
6
  export { SidebarLayout } from './chunk-YG45DAUT.js';
7
7
  import { playNotification, playStartup, soundsEnabled, getSoundConfig, SOUND_PACK_KEYS, SOUND_PACKS, SOUND_TYPES, SOUND_TYPE_LABELS, setSoundForType, previewSound, setAllSounds, playLogout } from './chunk-D7PYW2QS.js';
8
- import { setPdfPreview } from './chunk-WEQXKNOA.js';
9
- export { ImageAnnotator_default as ImageAnnotator } from './chunk-WEQXKNOA.js';
8
+ import { setPdfPreview } from './chunk-BI5XBNI5.js';
9
+ export { ImageAnnotator_default as ImageAnnotator } from './chunk-BI5XBNI5.js';
10
10
  import { toast_default } from './chunk-VENYVK3L.js';
11
11
  export { toast_default as toast } from './chunk-VENYVK3L.js';
12
- import { EditableGrid } from './chunk-YPIDICFR.js';
13
- export { EditableGrid } from './chunk-YPIDICFR.js';
12
+ import { EditableGrid } from './chunk-OKFJBJY6.js';
13
+ export { EditableGrid } from './chunk-OKFJBJY6.js';
14
14
  import { useAuth, useShellAuth } from './chunk-ADJ3CERD.js';
15
15
  export { ShellAuthProvider, setShellAuthBridge, useShellAuth } from './chunk-ADJ3CERD.js';
16
- import { APP_VERSION, VERSION } from './chunk-SRFLPGGO.js';
17
- export { VERSION } from './chunk-SRFLPGGO.js';
16
+ import { APP_VERSION, VERSION } from './chunk-NZLE6YR6.js';
17
+ export { VERSION } from './chunk-NZLE6YR6.js';
18
18
  import { useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, useShellPrefs, useIsMobile, ModalActions, useModalActive, client_default, LoadingSpinner, CancelButton, setWindowPosition, ThumbCard, activateModal } from './chunk-HLWEKEKD.js';
19
19
  export { CancelButton, CopyButton, DocFavStar, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, commitExposeHighlight, exitExposeMode, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, registerModalEscapeInterceptor, setExposeHighlight, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useLocalStoragePrefs, useModalActive, useShellPrefs, useWidgetSettings, useWindowManager, useWindowMenuItem, useWindowTitle } from './chunk-HLWEKEKD.js';
20
20
  export { ConfirmProvider, confirm, confirmDestructive, prompt } from './chunk-UBN4IUDE.js';
21
21
  import { glassStyle, startMenuCategories, navSections, isSection, GLASS_INPUT_BG, navIcons, sectionIcons } from './chunk-ZF6AYO4G.js';
22
22
  export { GLASS_DIVIDER, GLASS_INPUT_BG, glassStyle, setShellNavIcons } from './chunk-ZF6AYO4G.js';
23
- import { createContext, lazy, forwardRef, useCallback, useState, useRef, useEffect, useMemo, useLayoutEffect, useContext, isValidElement, cloneElement, Suspense, Fragment as Fragment$1, Children, useId, useSyncExternalStore } from 'react';
23
+ import { createContext, lazy, forwardRef, useCallback, useState, useRef, useEffect, useMemo, useLayoutEffect, useContext, isValidElement, cloneElement, Suspense, Fragment as Fragment$1, useId, Children, useSyncExternalStore } from 'react';
24
24
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
25
25
  import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react';
26
26
  import { createPortal } from 'react-dom';
@@ -591,15 +591,6 @@ function SidebarActionButton({
591
591
  }
592
592
  );
593
593
  }
594
- function useClickOutside(ref, onClose) {
595
- useEffect(() => {
596
- const handler = (e) => {
597
- if (ref.current && !ref.current.contains(e.target)) onClose();
598
- };
599
- document.addEventListener("mousedown", handler);
600
- return () => document.removeEventListener("mousedown", handler);
601
- }, [ref, onClose]);
602
- }
603
594
 
604
595
  // src/forms/styles.ts
605
596
  var INPUT_BASE = "block w-full rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-800 placeholder:text-gray-400 shadow-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-400/30";
@@ -608,15 +599,43 @@ var INPUT_DISABLED = "disabled:cursor-not-allowed disabled:opacity-60";
608
599
  function inputClasses(opts) {
609
600
  return [INPUT_BASE, opts?.invalid ? INPUT_INVALID : "", INPUT_DISABLED, opts?.className ?? ""].filter(Boolean).join(" ");
610
601
  }
611
- function useDropdownAlignment(triggerRef, open, maxWidth) {
612
- const [alignRight, setAlignRight] = useState(false);
602
+ var POPUP_MAX_WIDTH = 448;
603
+ var MENU_MAX_HEIGHT = 240;
604
+ var MENU_GAP = 4;
605
+ var VIEWPORT_MARGIN = 8;
606
+ function useDropdownPosition(triggerRef, open) {
607
+ const [pos, setPos] = useState(null);
613
608
  useLayoutEffect(() => {
614
- if (!open) return;
615
- const rect = triggerRef.current?.getBoundingClientRect();
616
- if (!rect) return;
617
- setAlignRight(window.innerWidth - rect.left < maxWidth);
618
- }, [open, maxWidth, triggerRef]);
619
- return alignRight;
609
+ if (!open) {
610
+ setPos(null);
611
+ return;
612
+ }
613
+ const compute = () => {
614
+ const rect = triggerRef.current?.getBoundingClientRect();
615
+ if (!rect) return;
616
+ const spaceBelow = window.innerHeight - rect.bottom - MENU_GAP - VIEWPORT_MARGIN;
617
+ const spaceAbove = rect.top - MENU_GAP - VIEWPORT_MARGIN;
618
+ const placeAbove = spaceBelow < Math.min(MENU_MAX_HEIGHT, 160) && spaceAbove > spaceBelow;
619
+ const maxHeight = Math.max(96, Math.min(MENU_MAX_HEIGHT, placeAbove ? spaceAbove : spaceBelow));
620
+ const next = { minWidth: rect.width, maxHeight };
621
+ if (window.innerWidth - rect.left < POPUP_MAX_WIDTH) {
622
+ next.right = Math.max(VIEWPORT_MARGIN, window.innerWidth - rect.right);
623
+ } else {
624
+ next.left = Math.max(VIEWPORT_MARGIN, rect.left);
625
+ }
626
+ if (placeAbove) next.bottom = window.innerHeight - rect.top + MENU_GAP;
627
+ else next.top = rect.bottom + MENU_GAP;
628
+ setPos(next);
629
+ };
630
+ compute();
631
+ window.addEventListener("scroll", compute, true);
632
+ window.addEventListener("resize", compute);
633
+ return () => {
634
+ window.removeEventListener("scroll", compute, true);
635
+ window.removeEventListener("resize", compute);
636
+ };
637
+ }, [open, triggerRef]);
638
+ return pos;
620
639
  }
621
640
  function ClearButton({ onClear, ariaLabel = "Clear selection" }) {
622
641
  return /* @__PURE__ */ jsx(
@@ -655,6 +674,7 @@ function SearchableSelect({
655
674
  };
656
675
  const wrapRef = useRef(null);
657
676
  const triggerRef = useRef(null);
677
+ const menuRef = useRef(null);
658
678
  const adornRef = useRef(null);
659
679
  const [adornPad, setAdornPad] = useState(null);
660
680
  useLayoutEffect(() => {
@@ -665,20 +685,26 @@ function SearchableSelect({
665
685
  const w = adornRef.current?.offsetWidth ?? 0;
666
686
  setAdornPad(w ? w + (value && !disabled ? 32 : 8) + 6 : null);
667
687
  });
668
- const POPUP_MAX_WIDTH = 448;
669
- const alignRight = useDropdownAlignment(triggerRef, open, POPUP_MAX_WIDTH);
688
+ const menuPos = useDropdownPosition(triggerRef, open);
670
689
  const selectedLabel = useMemo(() => {
671
690
  const match = options.find((o) => o.value === value);
672
691
  if (match) return match.label;
673
692
  return allowFreeText ? value : "";
674
693
  }, [options, value, allowFreeText]);
675
- useClickOutside(wrapRef, () => {
676
- if (allowFreeText && search.trim() && search.trim() !== value) {
677
- onChange(search.trim());
678
- }
679
- setOpen(false);
680
- setSearch("");
681
- });
694
+ useEffect(() => {
695
+ if (!open) return;
696
+ const handler = (e) => {
697
+ const t = e.target;
698
+ if (wrapRef.current?.contains(t) || menuRef.current?.contains(t)) return;
699
+ if (allowFreeText && search.trim() && search.trim() !== value) {
700
+ onChange(search.trim());
701
+ }
702
+ setOpen(false);
703
+ setSearch("");
704
+ };
705
+ document.addEventListener("mousedown", handler);
706
+ return () => document.removeEventListener("mousedown", handler);
707
+ }, [open, allowFreeText, search, value, onChange]);
682
708
  const dedupedOptions = useMemo(() => {
683
709
  const seen = /* @__PURE__ */ new Set();
684
710
  const out = [];
@@ -749,29 +775,45 @@ function SearchableSelect({
749
775
  setSearch("");
750
776
  } }),
751
777
  rightAdornment && !open && /* @__PURE__ */ jsx("div", { ref: adornRef, className: `absolute top-1/2 -translate-y-1/2 ${value && !disabled ? "right-8" : "right-2"} flex items-center gap-1 flex-nowrap justify-end pointer-events-none`, children: rightAdornment }),
752
- open && /* @__PURE__ */ jsx(
753
- "div",
754
- {
755
- className: `absolute z-[200] mt-1 rounded-2xl overflow-hidden ${alignRight ? "right-0" : "left-0"} min-w-full max-w-[28rem] w-max`,
756
- style: glassStyle(),
757
- children: /* @__PURE__ */ jsx("div", { className: "max-h-60 overflow-y-auto", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-3 text-sm text-gray-400 text-center", children: "No matches" }) : filtered.map((o) => /* @__PURE__ */ jsxs(
758
- "button",
759
- {
760
- type: "button",
761
- onMouseDown: () => {
762
- onChange(o.value);
763
- setOpen(false);
764
- setSearch("");
765
- },
766
- className: `w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 flex items-center justify-between gap-2 whitespace-nowrap ${value === o.value ? "text-blue-600 font-medium bg-blue-50/50" : "text-gray-700"}`,
767
- children: [
768
- /* @__PURE__ */ jsx("span", { children: o.label }),
769
- o.sublabel && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 shrink-0", children: o.sublabel })
770
- ]
778
+ open && createPortal(
779
+ /* @__PURE__ */ jsx(
780
+ "div",
781
+ {
782
+ ref: menuRef,
783
+ className: "fixed z-[400] rounded-2xl overflow-hidden",
784
+ style: {
785
+ left: menuPos?.left,
786
+ right: menuPos?.right,
787
+ top: menuPos?.top,
788
+ bottom: menuPos?.bottom,
789
+ minWidth: menuPos?.minWidth,
790
+ maxWidth: POPUP_MAX_WIDTH,
791
+ width: "max-content",
792
+ // Hidden for the first paint until the layout effect measures the
793
+ // trigger, so the menu never flashes at (0,0).
794
+ visibility: menuPos ? void 0 : "hidden",
795
+ ...glassStyle()
771
796
  },
772
- o.value
773
- )) })
774
- }
797
+ children: /* @__PURE__ */ jsx("div", { className: "overflow-y-auto", style: { maxHeight: menuPos?.maxHeight ?? MENU_MAX_HEIGHT }, children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-3 text-sm text-gray-400 text-center", children: "No matches" }) : filtered.map((o) => /* @__PURE__ */ jsxs(
798
+ "button",
799
+ {
800
+ type: "button",
801
+ onMouseDown: () => {
802
+ onChange(o.value);
803
+ setOpen(false);
804
+ setSearch("");
805
+ },
806
+ className: `w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 flex items-center justify-between gap-2 whitespace-nowrap ${value === o.value ? "text-blue-600 font-medium bg-blue-50/50" : "text-gray-700"}`,
807
+ children: [
808
+ /* @__PURE__ */ jsx("span", { children: o.label }),
809
+ o.sublabel && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 shrink-0", children: o.sublabel })
810
+ ]
811
+ },
812
+ o.value
813
+ )) })
814
+ }
815
+ ),
816
+ document.body
775
817
  )
776
818
  ] });
777
819
  }
@@ -2574,6 +2616,15 @@ function useTheme() {
2574
2616
  }, [accentColor, customBgColor, customTitleColor, customWindowColor, customButtonColor]);
2575
2617
  return { theme: saved, resolved };
2576
2618
  }
2619
+ function useClickOutside(ref, onClose) {
2620
+ useEffect(() => {
2621
+ const handler = (e) => {
2622
+ if (ref.current && !ref.current.contains(e.target)) onClose();
2623
+ };
2624
+ document.addEventListener("mousedown", handler);
2625
+ return () => document.removeEventListener("mousedown", handler);
2626
+ }, [ref, onClose]);
2627
+ }
2577
2628
  function StartupAnimation({ onComplete, ready = false, productName = "react-os-shell", subtitle }) {
2578
2629
  const [phase, setPhase] = useState("logo");
2579
2630
  const [minTimePassed, setMinTimePassed] = useState(false);
@@ -6436,6 +6487,7 @@ var Radio_default = Radio;
6436
6487
  function FormField({
6437
6488
  label,
6438
6489
  htmlFor,
6490
+ labelId,
6439
6491
  hint,
6440
6492
  error,
6441
6493
  required,
@@ -6443,12 +6495,12 @@ function FormField({
6443
6495
  children
6444
6496
  }) {
6445
6497
  return /* @__PURE__ */ jsxs("div", { className, children: [
6446
- label && /* @__PURE__ */ jsxs("label", { htmlFor, className: "mb-1 block text-xs font-medium text-gray-600", children: [
6498
+ label && /* @__PURE__ */ jsxs("label", { id: labelId, htmlFor, className: "mb-1 block text-xs font-medium text-gray-600", children: [
6447
6499
  label,
6448
6500
  required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-red-500", children: "*" })
6449
6501
  ] }),
6450
6502
  children,
6451
- error ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-red-600", children: error }) : hint ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-gray-400", children: hint }) : null
6503
+ error ? /* @__PURE__ */ jsx("p", { id: htmlFor ? `${htmlFor}-error` : void 0, className: "mt-1 text-[11px] text-red-600", children: error }) : hint ? /* @__PURE__ */ jsx("p", { id: htmlFor ? `${htmlFor}-hint` : void 0, className: "mt-1 text-[11px] text-gray-400", children: hint }) : null
6452
6504
  ] });
6453
6505
  }
6454
6506
  function Label({ required, children, className = "", ...rest }) {
@@ -6457,6 +6509,449 @@ function Label({ required, children, className = "", ...rest }) {
6457
6509
  required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-red-500", children: "*" })
6458
6510
  ] });
6459
6511
  }
6512
+ function mediaFileName(url) {
6513
+ const path = (url || "").split("?")[0].split("#")[0];
6514
+ const last = path.substring(path.lastIndexOf("/") + 1);
6515
+ try {
6516
+ return decodeURIComponent(last.replace(/^[0-9a-f]{32}_/i, "")) || last || "media";
6517
+ } catch {
6518
+ return last || "media";
6519
+ }
6520
+ }
6521
+ var VIDEO_EXT_RE = /\.(mp4|webm|ogg|ogv|mov|m4v)(\?|#|$)/i;
6522
+ function isVideoUrl(url, accept = "") {
6523
+ if (!url) return false;
6524
+ const acceptsVideo = accept.includes("video");
6525
+ const acceptsImage = accept.includes("image") || accept === "*" || accept === "";
6526
+ return VIDEO_EXT_RE.test(url) || acceptsVideo && !acceptsImage;
6527
+ }
6528
+ function Spinner2({ className = "h-5 w-5" }) {
6529
+ return /* @__PURE__ */ jsxs("svg", { className: `${className} animate-spin text-gray-400`, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
6530
+ /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
6531
+ /* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })
6532
+ ] });
6533
+ }
6534
+ function UploadGlyph({ className = "h-6 w-6" }) {
6535
+ return /* @__PURE__ */ jsxs(
6536
+ "svg",
6537
+ {
6538
+ viewBox: "0 0 24 24",
6539
+ fill: "none",
6540
+ stroke: "currentColor",
6541
+ strokeWidth: 1.6,
6542
+ strokeLinecap: "round",
6543
+ strokeLinejoin: "round",
6544
+ "aria-hidden": "true",
6545
+ className,
6546
+ children: [
6547
+ /* @__PURE__ */ jsx("path", { d: "M12 15V4" }),
6548
+ /* @__PURE__ */ jsx("path", { d: "M8 8l4-4 4 4" }),
6549
+ /* @__PURE__ */ jsx("path", { d: "M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" })
6550
+ ]
6551
+ }
6552
+ );
6553
+ }
6554
+ function MediaUploadField({
6555
+ value,
6556
+ onChange,
6557
+ onPick,
6558
+ label,
6559
+ hint,
6560
+ error,
6561
+ required,
6562
+ className,
6563
+ accept = "image/*",
6564
+ fit = "cover",
6565
+ height = 112,
6566
+ placeholder,
6567
+ cta = "Choose from library or upload",
6568
+ acceptHint,
6569
+ showFilename = true,
6570
+ allowReplace = true,
6571
+ allowRemove = true,
6572
+ replaceLabel = "Replace",
6573
+ removeLabel = "Remove",
6574
+ busy = false,
6575
+ busyLabel = "Uploading\u2026",
6576
+ disabled = false
6577
+ }) {
6578
+ const inputRef = useRef(null);
6579
+ const [dragOver, setDragOver] = useState(false);
6580
+ const [focused, setFocused] = useState(false);
6581
+ const fieldId = useId();
6582
+ const objectUrlRef = useRef(null);
6583
+ const [videoBlobUrl, setVideoBlobUrl] = useState(null);
6584
+ useEffect(() => () => {
6585
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
6586
+ }, []);
6587
+ const locked = disabled || busy;
6588
+ const acceptsVideo = accept.includes("video");
6589
+ const acceptsImage = accept.includes("image") || accept === "*" || accept === "";
6590
+ const isVideo = !!value && (isVideoUrl(value, accept) || value === videoBlobUrl);
6591
+ const kindWord = acceptsVideo && !acceptsImage ? "video" : acceptsImage && !acceptsVideo ? "image" : "file";
6592
+ const dimLine = placeholder ?? `Upload ${kindWord === "image" ? "an image" : `a ${kindWord}`}`;
6593
+ const emitObjectUrl = (file) => {
6594
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
6595
+ const url = URL.createObjectURL(file);
6596
+ objectUrlRef.current = url;
6597
+ setVideoBlobUrl(file.type.startsWith("video/") ? url : null);
6598
+ onChange(url);
6599
+ };
6600
+ const clearValue = () => {
6601
+ if (objectUrlRef.current) {
6602
+ URL.revokeObjectURL(objectUrlRef.current);
6603
+ objectUrlRef.current = null;
6604
+ }
6605
+ setVideoBlobUrl(null);
6606
+ onChange("");
6607
+ };
6608
+ const pick = (file) => {
6609
+ if (locked) return;
6610
+ if (onPick) {
6611
+ onPick(file ?? void 0);
6612
+ return;
6613
+ }
6614
+ if (file) {
6615
+ emitObjectUrl(file);
6616
+ return;
6617
+ }
6618
+ inputRef.current?.click();
6619
+ };
6620
+ const onNativeFile = (e) => {
6621
+ const file = e.target.files?.[0];
6622
+ if (file) emitObjectUrl(file);
6623
+ if (inputRef.current) inputRef.current.value = "";
6624
+ };
6625
+ const dropHandlers = locked ? {} : {
6626
+ onDragOver: (e) => {
6627
+ e.preventDefault();
6628
+ setDragOver(true);
6629
+ },
6630
+ onDragLeave: () => setDragOver(false),
6631
+ onDrop: (e) => {
6632
+ e.preventDefault();
6633
+ setDragOver(false);
6634
+ const file = e.dataTransfer.files?.[0];
6635
+ if (file) pick(file);
6636
+ }
6637
+ };
6638
+ const previewName = value ? mediaFileName(value) : "";
6639
+ const filled = /* @__PURE__ */ jsxs("div", { children: [
6640
+ /* @__PURE__ */ jsxs(
6641
+ "div",
6642
+ {
6643
+ ...dropHandlers,
6644
+ className: [
6645
+ "relative overflow-hidden rounded-md border bg-gray-100 transition-colors",
6646
+ dragOver ? "border-blue-500" : "border-gray-200"
6647
+ ].join(" "),
6648
+ style: { height },
6649
+ children: [
6650
+ isVideo ? /* @__PURE__ */ jsx(
6651
+ "video",
6652
+ {
6653
+ src: value ?? void 0,
6654
+ muted: true,
6655
+ playsInline: true,
6656
+ preload: "metadata",
6657
+ "aria-label": previewName || void 0,
6658
+ className: "h-full w-full",
6659
+ style: { objectFit: fit }
6660
+ }
6661
+ ) : /* @__PURE__ */ jsx("img", { src: value ?? void 0, alt: previewName, className: "h-full w-full", style: { objectFit: fit } }),
6662
+ showFilename && value && /* @__PURE__ */ jsx(
6663
+ "span",
6664
+ {
6665
+ className: "absolute inset-x-1 bottom-1 truncate rounded px-1.5 py-0.5 text-xs text-white",
6666
+ style: { background: "rgba(0,0,0,0.6)" },
6667
+ children: previewName
6668
+ }
6669
+ ),
6670
+ busy && /* @__PURE__ */ jsxs(
6671
+ "div",
6672
+ {
6673
+ role: "status",
6674
+ className: "absolute inset-0 flex items-center justify-center gap-2",
6675
+ style: { background: "rgba(255,255,255,0.6)" },
6676
+ children: [
6677
+ /* @__PURE__ */ jsx(Spinner2, {}),
6678
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-600", children: busyLabel })
6679
+ ]
6680
+ }
6681
+ )
6682
+ ]
6683
+ }
6684
+ ),
6685
+ !disabled && (allowReplace || allowRemove) && /* @__PURE__ */ jsxs("div", { className: "mt-2 flex gap-2", children: [
6686
+ allowReplace && /* @__PURE__ */ jsx(Button_default, { id: fieldId, variant: "secondary", size: "sm", onClick: () => pick(), disabled: busy, children: replaceLabel }),
6687
+ allowRemove && /* @__PURE__ */ jsx(Button_default, { variant: "ghost", size: "sm", onClick: clearValue, disabled: busy, children: removeLabel })
6688
+ ] })
6689
+ ] });
6690
+ const empty = /* @__PURE__ */ jsx(
6691
+ "button",
6692
+ {
6693
+ type: "button",
6694
+ id: fieldId,
6695
+ onClick: () => pick(),
6696
+ disabled: locked,
6697
+ "aria-label": typeof dimLine === "string" ? dimLine : "Upload media",
6698
+ "aria-busy": busy || void 0,
6699
+ "aria-invalid": error ? true : void 0,
6700
+ "aria-required": required || void 0,
6701
+ "aria-describedby": error ? `${fieldId}-error` : hint ? `${fieldId}-hint` : void 0,
6702
+ onFocus: () => setFocused(true),
6703
+ onBlur: () => setFocused(false),
6704
+ ...dropHandlers,
6705
+ className: [
6706
+ "flex w-full flex-col items-center justify-center gap-1.5 rounded-lg border-2 border-dashed text-center transition-colors",
6707
+ locked ? "cursor-not-allowed opacity-60" : "cursor-pointer",
6708
+ dragOver ? "border-blue-500 bg-blue-50" : "border-gray-300 bg-gray-50 hover:bg-gray-100"
6709
+ ].join(" "),
6710
+ style: {
6711
+ minHeight: height,
6712
+ outline: "none",
6713
+ boxShadow: focused && !locked ? "0 0 0 2px rgba(59,130,246,0.45)" : void 0
6714
+ },
6715
+ children: busy ? /* @__PURE__ */ jsxs("span", { role: "status", className: "flex flex-col items-center gap-1.5", children: [
6716
+ /* @__PURE__ */ jsx(Spinner2, {}),
6717
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-500", children: busyLabel })
6718
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6719
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: /* @__PURE__ */ jsx(UploadGlyph, {}) }),
6720
+ /* @__PURE__ */ jsx("span", { className: "px-3 text-xs text-gray-500", children: dimLine }),
6721
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-blue-600", children: cta }),
6722
+ acceptHint && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: acceptHint })
6723
+ ] })
6724
+ }
6725
+ );
6726
+ return /* @__PURE__ */ jsxs(FormField, { label, htmlFor: fieldId, hint, error, required, className, children: [
6727
+ value ? filled : empty,
6728
+ !onPick && /* @__PURE__ */ jsx("input", { ref: inputRef, type: "file", accept, className: "hidden", onChange: onNativeFile })
6729
+ ] });
6730
+ }
6731
+ var REMOVE_ICON = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": "true", className: "h-3 w-3", children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }) });
6732
+ function MediaUploadGrid({
6733
+ items,
6734
+ onPick,
6735
+ onRemove,
6736
+ onReorder,
6737
+ label,
6738
+ hint,
6739
+ error,
6740
+ required,
6741
+ className,
6742
+ accept = "image/*",
6743
+ fit = "cover",
6744
+ thumbSize = 96,
6745
+ placeholder,
6746
+ cta = "Choose from library or upload",
6747
+ acceptHint,
6748
+ showCover = false,
6749
+ coverLabel = "Cover",
6750
+ busy = false,
6751
+ busyLabel = "Uploading\u2026",
6752
+ disabled = false,
6753
+ readOnly = false
6754
+ }) {
6755
+ const gridId = useId();
6756
+ const [zoneDragOver, setZoneDragOver] = useState(false);
6757
+ const [draggingIndex, setDraggingIndex] = useState(null);
6758
+ const [focusedKey, setFocusedKey] = useState(null);
6759
+ const ringStyle = (key) => ({
6760
+ outline: "none",
6761
+ ...focusedKey === key && !locked ? { boxShadow: "0 0 0 2px rgba(59,130,246,0.45)" } : {}
6762
+ });
6763
+ const blurKey = (key) => setFocusedKey((k) => k === key ? null : k);
6764
+ const locked = disabled || busy;
6765
+ const canAdd = !!onPick && !locked && !readOnly;
6766
+ const canRemove = !!onRemove && !locked && !readOnly;
6767
+ const reorderEnabled = !!onReorder && !locked && !readOnly;
6768
+ const acceptsVideo = accept.includes("video");
6769
+ const acceptsImage = accept.includes("image") || accept === "*" || accept === "";
6770
+ const kindWord = acceptsVideo && !acceptsImage ? "video" : acceptsImage && !acceptsVideo ? "image" : "file";
6771
+ const dimLine = placeholder ?? `Upload ${kindWord === "image" ? "an image" : `a ${kindWord}`}`;
6772
+ const pick = (file) => {
6773
+ if (!canAdd) return;
6774
+ onPick(file ?? void 0);
6775
+ };
6776
+ const isFileDrag = (e) => Array.from(e.dataTransfer.types || []).includes("Files");
6777
+ const zoneHandlers = canAdd ? {
6778
+ onDragOver: (e) => {
6779
+ if (!isFileDrag(e)) return;
6780
+ e.preventDefault();
6781
+ setZoneDragOver(true);
6782
+ },
6783
+ onDragLeave: () => setZoneDragOver(false),
6784
+ onDrop: (e) => {
6785
+ const file = e.dataTransfer.files?.[0];
6786
+ if (!file) return;
6787
+ e.preventDefault();
6788
+ setZoneDragOver(false);
6789
+ pick(file);
6790
+ }
6791
+ } : {};
6792
+ const square = { width: thumbSize, height: thumbSize };
6793
+ const thumb = (item, index) => {
6794
+ const video = item.kind ? item.kind === "video" : isVideoUrl(item.url, accept);
6795
+ const name = mediaFileName(item.url);
6796
+ const isCover = showCover && (item.cover ?? index === 0);
6797
+ return /* @__PURE__ */ jsxs(
6798
+ "div",
6799
+ {
6800
+ draggable: reorderEnabled || void 0,
6801
+ onDragStart: reorderEnabled ? (e) => {
6802
+ e.dataTransfer.effectAllowed = "move";
6803
+ e.dataTransfer.setData("text/plain", String(index));
6804
+ setDraggingIndex(index);
6805
+ } : void 0,
6806
+ onDragEnd: reorderEnabled ? () => setDraggingIndex(null) : void 0,
6807
+ onDragOver: reorderEnabled ? (e) => {
6808
+ if (draggingIndex == null) return;
6809
+ e.preventDefault();
6810
+ e.stopPropagation();
6811
+ } : void 0,
6812
+ onDrop: reorderEnabled ? (e) => {
6813
+ if (draggingIndex == null) return;
6814
+ e.preventDefault();
6815
+ e.stopPropagation();
6816
+ if (draggingIndex !== index) onReorder(draggingIndex, index);
6817
+ setDraggingIndex(null);
6818
+ } : void 0,
6819
+ tabIndex: reorderEnabled ? 0 : void 0,
6820
+ "aria-label": reorderEnabled ? `${name}, item ${index + 1} of ${items.length}. Use arrow keys to reorder.` : void 0,
6821
+ onKeyDown: reorderEnabled ? (e) => {
6822
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
6823
+ if (index > 0) {
6824
+ e.preventDefault();
6825
+ onReorder(index, index - 1);
6826
+ }
6827
+ } else if (e.key === "ArrowRight" || e.key === "ArrowDown") {
6828
+ if (index < items.length - 1) {
6829
+ e.preventDefault();
6830
+ onReorder(index, index + 1);
6831
+ }
6832
+ }
6833
+ } : void 0,
6834
+ onFocus: reorderEnabled ? (e) => {
6835
+ if (e.target === e.currentTarget) setFocusedKey(`thumb:${item.id}`);
6836
+ } : void 0,
6837
+ onBlur: reorderEnabled ? (e) => {
6838
+ if (e.target === e.currentTarget) blurKey(`thumb:${item.id}`);
6839
+ } : void 0,
6840
+ className: [
6841
+ "relative shrink-0 overflow-hidden rounded-md border border-gray-200 bg-gray-100",
6842
+ draggingIndex === index ? "opacity-50" : ""
6843
+ ].join(" "),
6844
+ style: { ...square, cursor: reorderEnabled ? "move" : void 0, ...ringStyle(`thumb:${item.id}`) },
6845
+ children: [
6846
+ video ? /* @__PURE__ */ jsx("video", { src: item.url, muted: true, playsInline: true, preload: "metadata", "aria-label": name, className: "h-full w-full", style: { objectFit: fit } }) : /* @__PURE__ */ jsx("img", { src: item.url, alt: name, className: "h-full w-full", style: { objectFit: fit } }),
6847
+ isCover && /* @__PURE__ */ jsx("span", { className: "absolute left-1 top-1 rounded bg-blue-600 px-1.5 py-0.5 text-xs font-medium text-white", children: coverLabel }),
6848
+ item.caption != null && /* @__PURE__ */ jsx(
6849
+ "span",
6850
+ {
6851
+ className: "absolute inset-x-1 bottom-1 truncate rounded px-1.5 py-0.5 text-xs text-white",
6852
+ style: { background: "rgba(0,0,0,0.6)" },
6853
+ children: item.caption
6854
+ }
6855
+ ),
6856
+ canRemove && /* @__PURE__ */ jsx(
6857
+ "button",
6858
+ {
6859
+ type: "button",
6860
+ onClick: () => onRemove(item.id),
6861
+ "aria-label": `Remove ${name}`,
6862
+ onFocus: () => setFocusedKey(`rm:${item.id}`),
6863
+ onBlur: () => blurKey(`rm:${item.id}`),
6864
+ className: "absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full text-white",
6865
+ style: { background: "rgba(0,0,0,0.55)", ...ringStyle(`rm:${item.id}`) },
6866
+ children: REMOVE_ICON
6867
+ }
6868
+ )
6869
+ ]
6870
+ },
6871
+ item.id
6872
+ );
6873
+ };
6874
+ const addTile = canAdd && /* @__PURE__ */ jsxs(
6875
+ "button",
6876
+ {
6877
+ type: "button",
6878
+ onClick: () => pick(),
6879
+ "aria-label": "Add media",
6880
+ onFocus: () => setFocusedKey("add"),
6881
+ onBlur: () => blurKey("add"),
6882
+ className: "flex shrink-0 flex-col items-center justify-center gap-1 rounded-md border-2 border-dashed border-gray-300 bg-gray-50 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-500",
6883
+ style: { ...square, ...ringStyle("add") },
6884
+ children: [
6885
+ /* @__PURE__ */ jsx(UploadGlyph, { className: "h-5 w-5" }),
6886
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-500", children: "Add" })
6887
+ ]
6888
+ }
6889
+ );
6890
+ const emptyDropzone = canAdd ? /* @__PURE__ */ jsxs(
6891
+ "button",
6892
+ {
6893
+ type: "button",
6894
+ onClick: () => pick(),
6895
+ "aria-label": typeof dimLine === "string" ? dimLine : "Upload media",
6896
+ onFocus: () => setFocusedKey("zone"),
6897
+ onBlur: () => blurKey("zone"),
6898
+ className: [
6899
+ "flex w-full flex-col items-center justify-center gap-1.5 rounded-lg border-2 border-dashed text-center transition-colors cursor-pointer",
6900
+ zoneDragOver ? "border-blue-500 bg-blue-50" : "border-gray-300 bg-gray-50 hover:bg-gray-100"
6901
+ ].join(" "),
6902
+ style: { minHeight: thumbSize + 32, ...ringStyle("zone") },
6903
+ children: [
6904
+ /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: /* @__PURE__ */ jsx(UploadGlyph, {}) }),
6905
+ /* @__PURE__ */ jsx("span", { className: "px-3 text-xs text-gray-500", children: dimLine }),
6906
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-blue-600", children: cta }),
6907
+ acceptHint && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: acceptHint })
6908
+ ]
6909
+ }
6910
+ ) : /* @__PURE__ */ jsx(
6911
+ "div",
6912
+ {
6913
+ className: "flex w-full items-center justify-center rounded-lg border border-gray-200 bg-gray-50 px-3 text-xs text-gray-400",
6914
+ style: { minHeight: thumbSize + 32 },
6915
+ children: "No media."
6916
+ }
6917
+ );
6918
+ return /* @__PURE__ */ jsx(FormField, { label, labelId: label ? `${gridId}-label` : void 0, htmlFor: gridId, hint, error, required, className, children: /* @__PURE__ */ jsxs(
6919
+ "div",
6920
+ {
6921
+ id: gridId,
6922
+ role: "group",
6923
+ "aria-labelledby": label ? `${gridId}-label` : void 0,
6924
+ "aria-describedby": error ? `${gridId}-error` : hint ? `${gridId}-hint` : void 0,
6925
+ "aria-busy": busy || void 0,
6926
+ ...zoneHandlers,
6927
+ className: "relative rounded-lg transition-colors",
6928
+ style: {
6929
+ ...locked ? { opacity: 0.6 } : {},
6930
+ // Inline box-shadow, not `ring-*` — the kit's shipped stylesheet has no
6931
+ // Tailwind ring machinery (see MediaUploadField's focus ring).
6932
+ ...zoneDragOver ? { boxShadow: "0 0 0 2px rgba(59,130,246,0.5)" } : {}
6933
+ },
6934
+ children: [
6935
+ items.length === 0 ? emptyDropzone : /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
6936
+ items.map(thumb),
6937
+ addTile
6938
+ ] }),
6939
+ busy && /* @__PURE__ */ jsxs(
6940
+ "div",
6941
+ {
6942
+ role: "status",
6943
+ className: "absolute inset-0 flex items-center justify-center gap-2 rounded-lg",
6944
+ style: { background: "rgba(255,255,255,0.6)" },
6945
+ children: [
6946
+ /* @__PURE__ */ jsx(Spinner2, {}),
6947
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-600", children: busyLabel })
6948
+ ]
6949
+ }
6950
+ )
6951
+ ]
6952
+ }
6953
+ ) });
6954
+ }
6460
6955
  function Card({ children, header, footer, padded = true, className = "" }) {
6461
6956
  return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border border-gray-200 bg-white shadow-sm ${className}`.trim(), children: [
6462
6957
  header && /* @__PURE__ */ jsx("div", { className: "border-b border-gray-100 px-4 py-3 text-sm font-semibold text-gray-900", children: header }),
@@ -8968,6 +9463,6 @@ function useEditHotkey(callback) {
8968
9463
  }, [callback, isActive]);
8969
9464
  }
8970
9465
 
8971
- export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, Label, Layout, ListFooter, LoadingSpinner2 as LoadingSpinner, MOD, Markdown, MilestoneTimeline, NotificationBell, PageHeader, Pagination, PdfActionButton, Radio_default as Radio, ResizableTable, SHIFT, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, applyDevTitle, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isMac, mergeBulkItems, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
9466
+ export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, Label, Layout, ListFooter, LoadingSpinner2 as LoadingSpinner, MOD, Markdown, MediaUploadField, MediaUploadGrid, MilestoneTimeline, NotificationBell, PageHeader, Pagination, PdfActionButton, Radio_default as Radio, ResizableTable, SHIFT, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, applyDevTitle, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isMac, mediaFileName, mergeBulkItems, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
8972
9467
  //# sourceMappingURL=index.js.map
8973
9468
  //# sourceMappingURL=index.js.map