@gallop.software/studio 0.1.80 → 0.1.82

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.
@@ -7,8 +7,8 @@ import {
7
7
  } from "./chunk-HXE6XCG2.mjs";
8
8
 
9
9
  // src/components/StudioUI.tsx
10
- import { useEffect as useEffect4, useCallback as useCallback2, useState as useState7 } from "react";
11
- import { css as css8 } from "@emotion/react";
10
+ import { useEffect as useEffect3, useCallback as useCallback3, useState as useState8 } from "react";
11
+ import { css as css9 } from "@emotion/react";
12
12
 
13
13
  // src/components/StudioContext.tsx
14
14
  import { createContext, useContext } from "react";
@@ -52,6 +52,11 @@ var defaultState = {
52
52
  },
53
53
  searchQuery: "",
54
54
  setSearchQuery: () => {
55
+ },
56
+ error: null,
57
+ showError: () => {
58
+ },
59
+ clearError: () => {
55
60
  }
56
61
  };
57
62
  var StudioContext = createContext(defaultState);
@@ -891,6 +896,7 @@ function StudioToolbar() {
891
896
  const [imagesToProcess, setImagesToProcess] = useState2([]);
892
897
  const [alertMessage, setAlertMessage] = useState2(null);
893
898
  const [showNewFolderModal, setShowNewFolderModal] = useState2(false);
899
+ const [showRenameFolderModal, setShowRenameFolderModal] = useState2(false);
894
900
  const [showMoveModal, setShowMoveModal] = useState2(false);
895
901
  const isInImagesFolder = currentPath === "public/images" || currentPath.startsWith("public/images/");
896
902
  const handleUpload = useCallback(() => {
@@ -947,13 +953,13 @@ function StudioToolbar() {
947
953
  const handleProcessImages = useCallback(async () => {
948
954
  const hasSelection2 = selectedItems.size > 0;
949
955
  if (hasSelection2) {
950
- const selectedPaths = Array.from(selectedItems);
956
+ const selectedPaths2 = Array.from(selectedItems);
951
957
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
952
- const selectedImagePaths = selectedPaths.filter((p) => {
958
+ const selectedImagePaths = selectedPaths2.filter((p) => {
953
959
  const ext = p.split(".").pop()?.toLowerCase() || "";
954
960
  return imageExtensions.includes(ext);
955
961
  });
956
- const selectedFolders = selectedPaths.filter((p) => !p.includes(".") || p.endsWith("/"));
962
+ const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
957
963
  if (selectedFolders.length > 0) {
958
964
  try {
959
965
  const response = await fetch(`/api/studio/folder-images?folders=${encodeURIComponent(selectedFolders.join(","))}`);
@@ -1263,6 +1269,27 @@ function StudioToolbar() {
1263
1269
  }
1264
1270
  }, [setSearchQuery]);
1265
1271
  const hasSelection = selectedItems.size > 0;
1272
+ const selectedPaths = Array.from(selectedItems);
1273
+ const singleFolderSelected = selectedPaths.length === 1 && !selectedPaths[0].includes(".");
1274
+ const selectedFolderPath = singleFolderSelected ? selectedPaths[0] : null;
1275
+ const selectedFolderName = selectedFolderPath ? selectedFolderPath.split("/").pop() || "" : "";
1276
+ const handleRenameFolder = useCallback(async (newName) => {
1277
+ if (!selectedFolderPath) return;
1278
+ setShowRenameFolderModal(false);
1279
+ try {
1280
+ const response = await fetch("/api/studio/rename", {
1281
+ method: "POST",
1282
+ headers: { "Content-Type": "application/json" },
1283
+ body: JSON.stringify({ oldPath: selectedFolderPath, newName })
1284
+ });
1285
+ if (response.ok) {
1286
+ clearSelection();
1287
+ triggerRefresh();
1288
+ }
1289
+ } catch (error) {
1290
+ console.error("Failed to rename folder:", error);
1291
+ }
1292
+ }, [selectedFolderPath, clearSelection, triggerRefresh]);
1266
1293
  if (focusedItem) {
1267
1294
  return null;
1268
1295
  }
@@ -1328,6 +1355,18 @@ function StudioToolbar() {
1328
1355
  onCancel: () => setShowMoveModal(false)
1329
1356
  }
1330
1357
  ),
1358
+ showRenameFolderModal && selectedFolderPath && /* @__PURE__ */ jsx3(
1359
+ InputModal,
1360
+ {
1361
+ title: "Rename Folder",
1362
+ message: "Enter a new name for the folder:",
1363
+ placeholder: selectedFolderName,
1364
+ defaultValue: selectedFolderName,
1365
+ confirmLabel: "Rename",
1366
+ onConfirm: handleRenameFolder,
1367
+ onCancel: () => setShowRenameFolderModal(false)
1368
+ }
1369
+ ),
1331
1370
  alertMessage && /* @__PURE__ */ jsx3(
1332
1371
  AlertModal,
1333
1372
  {
@@ -1365,12 +1404,12 @@ function StudioToolbar() {
1365
1404
  "button",
1366
1405
  {
1367
1406
  css: styles3.btn,
1368
- onClick: () => setShowNewFolderModal(true),
1369
- disabled: isInImagesFolder,
1370
- title: isInImagesFolder ? "Cannot create folders in protected images folder" : void 0,
1407
+ onClick: () => singleFolderSelected ? setShowRenameFolderModal(true) : setShowNewFolderModal(true),
1408
+ disabled: isInImagesFolder && !singleFolderSelected,
1409
+ title: isInImagesFolder && !singleFolderSelected ? "Cannot create folders in protected images folder" : void 0,
1371
1410
  children: [
1372
- /* @__PURE__ */ jsx3(FolderPlusIcon, {}),
1373
- "New Folder"
1411
+ singleFolderSelected ? /* @__PURE__ */ jsx3(RenameIcon, {}) : /* @__PURE__ */ jsx3(FolderPlusIcon, {}),
1412
+ singleFolderSelected ? "Rename Folder" : "New Folder"
1374
1413
  ]
1375
1414
  }
1376
1415
  ),
@@ -1497,6 +1536,9 @@ function TrashIcon() {
1497
1536
  function FolderPlusIcon() {
1498
1537
  return /* @__PURE__ */ jsx3("svg", { css: styles3.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" }) });
1499
1538
  }
1539
+ function RenameIcon() {
1540
+ return /* @__PURE__ */ jsx3("svg", { css: styles3.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) });
1541
+ }
1500
1542
  function MoveIcon() {
1501
1543
  return /* @__PURE__ */ jsx3("svg", { css: styles3.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" }) });
1502
1544
  }
@@ -1514,8 +1556,196 @@ function ImageStackIcon() {
1514
1556
  }
1515
1557
 
1516
1558
  // src/components/StudioFileGrid.tsx
1517
- import { useEffect as useEffect2, useState as useState3, useRef as useRef2 } from "react";
1559
+ import { useState as useState4 } from "react";
1518
1560
  import { css as css4, keyframes as keyframes4 } from "@emotion/react";
1561
+
1562
+ // src/hooks/useFileList.ts
1563
+ import { useEffect as useEffect2, useState as useState3, useRef as useRef2, useCallback as useCallback2 } from "react";
1564
+
1565
+ // src/lib/api.ts
1566
+ var StudioApiClient = class {
1567
+ async get(url) {
1568
+ const response = await fetch(url);
1569
+ if (!response.ok) {
1570
+ const data = await response.json().catch(() => ({}));
1571
+ throw new Error(data.error || `Request failed: ${response.status}`);
1572
+ }
1573
+ return response.json();
1574
+ }
1575
+ async post(url, body) {
1576
+ const response = await fetch(url, {
1577
+ method: "POST",
1578
+ headers: body ? { "Content-Type": "application/json" } : void 0,
1579
+ body: body ? JSON.stringify(body) : void 0
1580
+ });
1581
+ if (!response.ok) {
1582
+ const data = await response.json().catch(() => ({}));
1583
+ throw new Error(data.error || `Request failed: ${response.status}`);
1584
+ }
1585
+ return response.json();
1586
+ }
1587
+ // List handlers
1588
+ async list(path = "public") {
1589
+ return this.get(`/api/studio/list?path=${encodeURIComponent(path)}`);
1590
+ }
1591
+ async search(query) {
1592
+ return this.get(`/api/studio/search?q=${encodeURIComponent(query)}`);
1593
+ }
1594
+ async listFolders() {
1595
+ return this.get("/api/studio/list-folders");
1596
+ }
1597
+ async countImages() {
1598
+ return this.get("/api/studio/count-images");
1599
+ }
1600
+ async folderImages(folders) {
1601
+ return this.get(`/api/studio/folder-images?folders=${encodeURIComponent(folders.join(","))}`);
1602
+ }
1603
+ // File handlers
1604
+ async upload(file, targetPath = "public") {
1605
+ const formData = new FormData();
1606
+ formData.append("file", file);
1607
+ formData.append("path", targetPath);
1608
+ const response = await fetch("/api/studio/upload", {
1609
+ method: "POST",
1610
+ body: formData
1611
+ });
1612
+ if (!response.ok) {
1613
+ const data = await response.json().catch(() => ({}));
1614
+ throw new Error(data.error || `Upload failed: ${response.status}`);
1615
+ }
1616
+ return response.json();
1617
+ }
1618
+ async delete(paths) {
1619
+ return this.post("/api/studio/delete", { paths });
1620
+ }
1621
+ async createFolder(parentPath, name) {
1622
+ return this.post("/api/studio/create-folder", { parentPath, name });
1623
+ }
1624
+ async rename(oldPath, newName) {
1625
+ return this.post("/api/studio/rename", { oldPath, newName });
1626
+ }
1627
+ async move(paths, destination) {
1628
+ return this.post("/api/studio/move", { paths, destination });
1629
+ }
1630
+ // Image handlers
1631
+ async sync(imageKeys) {
1632
+ return this.post("/api/studio/sync", { imageKeys });
1633
+ }
1634
+ async reprocess(imageKeys) {
1635
+ return this.post("/api/studio/reprocess", { imageKeys });
1636
+ }
1637
+ // Process all returns a stream, handle separately
1638
+ processAllStream() {
1639
+ return new EventSource("/api/studio/process-all");
1640
+ }
1641
+ };
1642
+ var studioApi = new StudioApiClient();
1643
+
1644
+ // src/hooks/useFileList.ts
1645
+ function useFileList() {
1646
+ const {
1647
+ currentPath,
1648
+ setCurrentPath,
1649
+ navigateUp,
1650
+ selectedItems,
1651
+ toggleSelection,
1652
+ selectRange,
1653
+ lastSelectedPath,
1654
+ selectAll,
1655
+ clearSelection,
1656
+ refreshKey,
1657
+ setFocusedItem,
1658
+ triggerRefresh,
1659
+ searchQuery,
1660
+ showError
1661
+ } = useStudio();
1662
+ const [items, setItems] = useState3([]);
1663
+ const [loading, setLoading] = useState3(true);
1664
+ const isInitialLoad = useRef2(true);
1665
+ const lastPath = useRef2(currentPath);
1666
+ useEffect2(() => {
1667
+ async function loadItems() {
1668
+ const isPathChange = lastPath.current !== currentPath;
1669
+ if (isInitialLoad.current || isPathChange) {
1670
+ setLoading(true);
1671
+ }
1672
+ lastPath.current = currentPath;
1673
+ try {
1674
+ const data = searchQuery && searchQuery.length >= 2 ? await studioApi.search(searchQuery) : await studioApi.list(currentPath);
1675
+ setItems(data.items || []);
1676
+ } catch (error) {
1677
+ const message = error instanceof Error ? error.message : "Failed to load items";
1678
+ showError("Load Error", message);
1679
+ setItems([]);
1680
+ }
1681
+ setLoading(false);
1682
+ isInitialLoad.current = false;
1683
+ }
1684
+ loadItems();
1685
+ }, [currentPath, refreshKey, searchQuery, showError]);
1686
+ const isAtRoot = currentPath === "public";
1687
+ const isSearching = searchQuery && searchQuery.length >= 2;
1688
+ const sortedItems = [...items].sort((a, b) => {
1689
+ if (a.type === "folder" && b.type !== "folder") return -1;
1690
+ if (a.type !== "folder" && b.type === "folder") return 1;
1691
+ return a.name.localeCompare(b.name);
1692
+ });
1693
+ const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
1694
+ const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
1695
+ const handleItemClick = useCallback2((item, e) => {
1696
+ if (e.shiftKey && lastSelectedPath) {
1697
+ selectRange(lastSelectedPath, item.path, sortedItems);
1698
+ } else {
1699
+ toggleSelection(item.path);
1700
+ }
1701
+ }, [lastSelectedPath, selectRange, sortedItems, toggleSelection]);
1702
+ const handleOpen = useCallback2((item) => {
1703
+ if (item.type === "folder") {
1704
+ setCurrentPath(item.path);
1705
+ } else {
1706
+ setFocusedItem(item);
1707
+ }
1708
+ }, [setCurrentPath, setFocusedItem]);
1709
+ const handleGenerateThumbnail = useCallback2(async (item) => {
1710
+ try {
1711
+ const imageKey = "/" + item.path.replace(/^public\//, "");
1712
+ await studioApi.reprocess([imageKey]);
1713
+ triggerRefresh();
1714
+ } catch (error) {
1715
+ const message = error instanceof Error ? error.message : "Failed to generate thumbnail";
1716
+ showError("Processing Error", message);
1717
+ }
1718
+ }, [triggerRefresh, showError]);
1719
+ const handleSelectAll = useCallback2(() => {
1720
+ if (allItemsSelected) {
1721
+ clearSelection();
1722
+ } else {
1723
+ selectAll(sortedItems);
1724
+ }
1725
+ }, [allItemsSelected, clearSelection, selectAll, sortedItems]);
1726
+ return {
1727
+ // State
1728
+ items,
1729
+ loading,
1730
+ sortedItems,
1731
+ // Computed
1732
+ isAtRoot,
1733
+ isSearching,
1734
+ allItemsSelected,
1735
+ someItemsSelected,
1736
+ // Context values
1737
+ currentPath,
1738
+ selectedItems,
1739
+ navigateUp,
1740
+ // Handlers
1741
+ handleItemClick,
1742
+ handleOpen,
1743
+ handleGenerateThumbnail,
1744
+ handleSelectAll
1745
+ };
1746
+ }
1747
+
1748
+ // src/components/StudioFileGrid.tsx
1519
1749
  import { jsx as jsx4, jsxs as jsxs4 } from "@emotion/react/jsx-runtime";
1520
1750
  var spin2 = keyframes4`
1521
1751
  to { transform: rotate(360deg); }
@@ -1582,10 +1812,6 @@ var styles4 = {
1582
1812
  &:hover {
1583
1813
  border-color: #d0d5dd;
1584
1814
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
1585
-
1586
- button[title="Rename"] {
1587
- opacity: 1;
1588
- }
1589
1815
  }
1590
1816
  `,
1591
1817
  itemSelected: css4`
@@ -1745,35 +1971,6 @@ var styles4 = {
1745
1971
  color: ${colors.text};
1746
1972
  }
1747
1973
  `,
1748
- nameRow: css4`
1749
- display: flex;
1750
- align-items: center;
1751
- gap: 4px;
1752
- `,
1753
- renameBtn: css4`
1754
- flex-shrink: 0;
1755
- height: 20px;
1756
- width: 20px;
1757
- color: ${colors.textMuted};
1758
- background: transparent;
1759
- border: none;
1760
- padding: 0;
1761
- cursor: pointer;
1762
- border-radius: 4px;
1763
- transition: all 0.15s ease;
1764
- display: flex;
1765
- align-items: center;
1766
- justify-content: center;
1767
- opacity: 0;
1768
-
1769
- &:hover {
1770
- color: ${colors.text};
1771
- }
1772
- `,
1773
- renameIcon: css4`
1774
- width: 14px;
1775
- height: 14px;
1776
- `,
1777
1974
  copyIcon: css4`
1778
1975
  width: 18px;
1779
1976
  height: 18px;
@@ -1871,116 +2068,31 @@ var styles4 = {
1871
2068
  `
1872
2069
  };
1873
2070
  function StudioFileGrid() {
1874
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh, searchQuery } = useStudio();
1875
- const [items, setItems] = useState3([]);
1876
- const [loading, setLoading] = useState3(true);
1877
- const [renameItem, setRenameItem] = useState3(null);
1878
- const isInitialLoad = useRef2(true);
1879
- const lastPath = useRef2(currentPath);
1880
- useEffect2(() => {
1881
- async function loadItems() {
1882
- const isPathChange = lastPath.current !== currentPath;
1883
- if (isInitialLoad.current || isPathChange) {
1884
- setLoading(true);
1885
- }
1886
- lastPath.current = currentPath;
1887
- try {
1888
- const url = searchQuery && searchQuery.length >= 2 ? `/api/studio/search?q=${encodeURIComponent(searchQuery)}` : `/api/studio/list?path=${encodeURIComponent(currentPath)}`;
1889
- const response = await fetch(url);
1890
- if (response.ok) {
1891
- const data = await response.json();
1892
- setItems(data.items || []);
1893
- }
1894
- } catch (error) {
1895
- console.error("Failed to load items:", error);
1896
- }
1897
- setLoading(false);
1898
- isInitialLoad.current = false;
1899
- }
1900
- loadItems();
1901
- }, [currentPath, refreshKey, searchQuery]);
2071
+ const {
2072
+ loading,
2073
+ sortedItems,
2074
+ isAtRoot,
2075
+ isSearching,
2076
+ allItemsSelected,
2077
+ someItemsSelected,
2078
+ selectedItems,
2079
+ navigateUp,
2080
+ handleItemClick,
2081
+ handleOpen,
2082
+ handleGenerateThumbnail,
2083
+ handleSelectAll
2084
+ } = useFileList();
1902
2085
  if (loading) {
1903
2086
  return /* @__PURE__ */ jsx4("div", { css: styles4.loading, children: /* @__PURE__ */ jsx4("div", { css: styles4.spinner }) });
1904
2087
  }
1905
- const isAtRoot = currentPath === "public";
1906
- if (items.length === 0 && isAtRoot) {
2088
+ if (sortedItems.length === 0 && isAtRoot) {
1907
2089
  return /* @__PURE__ */ jsxs4("div", { css: styles4.empty, children: [
1908
2090
  /* @__PURE__ */ jsx4("svg", { css: styles4.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }),
1909
2091
  /* @__PURE__ */ jsx4("p", { css: styles4.emptyText, children: "No files in this folder" }),
1910
2092
  /* @__PURE__ */ jsx4("p", { css: styles4.emptyText, children: "Upload images to get started" })
1911
2093
  ] });
1912
2094
  }
1913
- const isSearching = searchQuery && searchQuery.length >= 2;
1914
- const sortedItems = [...items].sort((a, b) => {
1915
- if (a.type === "folder" && b.type !== "folder") return -1;
1916
- if (a.type !== "folder" && b.type === "folder") return 1;
1917
- return a.name.localeCompare(b.name);
1918
- });
1919
- const handleItemClick = (item, e) => {
1920
- if (e.shiftKey && lastSelectedPath) {
1921
- selectRange(lastSelectedPath, item.path, sortedItems);
1922
- } else {
1923
- toggleSelection(item.path);
1924
- }
1925
- };
1926
- const handleOpen = (item) => {
1927
- if (item.type === "folder") {
1928
- setCurrentPath(item.path);
1929
- } else {
1930
- setFocusedItem(item);
1931
- }
1932
- };
1933
- const handleGenerateThumbnail = async (item) => {
1934
- try {
1935
- const imageKey = item.path.replace(/^public\//, "");
1936
- await fetch("/api/studio/reprocess", {
1937
- method: "POST",
1938
- headers: { "Content-Type": "application/json" },
1939
- body: JSON.stringify({ imageKeys: [imageKey] })
1940
- });
1941
- triggerRefresh();
1942
- } catch (error) {
1943
- console.error("Failed to generate thumbnail:", error);
1944
- }
1945
- };
1946
- const handleRename = async (newName) => {
1947
- if (!renameItem) return;
1948
- setRenameItem(null);
1949
- try {
1950
- const response = await fetch("/api/studio/rename", {
1951
- method: "POST",
1952
- headers: { "Content-Type": "application/json" },
1953
- body: JSON.stringify({ oldPath: renameItem.path, newName })
1954
- });
1955
- if (response.ok) {
1956
- triggerRefresh();
1957
- }
1958
- } catch (error) {
1959
- console.error("Failed to rename:", error);
1960
- }
1961
- };
1962
- const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
1963
- const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
1964
- const handleSelectAll = () => {
1965
- if (allItemsSelected) {
1966
- clearSelection();
1967
- } else {
1968
- selectAll(sortedItems);
1969
- }
1970
- };
1971
2095
  return /* @__PURE__ */ jsxs4("div", { children: [
1972
- renameItem && /* @__PURE__ */ jsx4(
1973
- InputModal,
1974
- {
1975
- title: renameItem.type === "folder" ? "Rename Folder" : "Rename File",
1976
- message: "Enter a new name:",
1977
- placeholder: renameItem.name,
1978
- defaultValue: renameItem.name,
1979
- confirmLabel: "Rename",
1980
- onConfirm: handleRename,
1981
- onCancel: () => setRenameItem(null)
1982
- }
1983
- ),
1984
2096
  sortedItems.length > 0 && /* @__PURE__ */ jsx4("div", { css: styles4.selectAllRow, children: /* @__PURE__ */ jsxs4("label", { css: styles4.selectAllLabel, children: [
1985
2097
  /* @__PURE__ */ jsx4(
1986
2098
  "input",
@@ -2020,16 +2132,15 @@ function StudioFileGrid() {
2020
2132
  isSelected: selectedItems.has(item.path),
2021
2133
  onClick: (e) => handleItemClick(item, e),
2022
2134
  onOpen: () => handleOpen(item),
2023
- onGenerateThumbnail: () => handleGenerateThumbnail(item),
2024
- onRename: () => setRenameItem(item)
2135
+ onGenerateThumbnail: () => handleGenerateThumbnail(item)
2025
2136
  },
2026
2137
  item.path
2027
2138
  ))
2028
2139
  ] })
2029
2140
  ] });
2030
2141
  }
2031
- function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail, onRename }) {
2032
- const [showCopied, setShowCopied] = useState3(false);
2142
+ function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
2143
+ const [showCopied, setShowCopied] = useState4(false);
2033
2144
  const isFolder = item.type === "folder";
2034
2145
  const isImage = !isFolder && item.thumbnail !== void 0;
2035
2146
  const isImagesFolder = isFolder && (item.name === "images" || item.path.includes("/images/"));
@@ -2115,21 +2226,7 @@ function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail, onRe
2115
2226
  ) : /* @__PURE__ */ jsx4("svg", { css: styles4.fileIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" }) })
2116
2227
  ] }),
2117
2228
  /* @__PURE__ */ jsx4("div", { css: styles4.label, children: /* @__PURE__ */ jsx4("div", { css: styles4.labelRow, children: /* @__PURE__ */ jsxs4("div", { css: styles4.labelText, children: [
2118
- /* @__PURE__ */ jsxs4("div", { css: styles4.nameRow, children: [
2119
- /* @__PURE__ */ jsx4("p", { css: styles4.name, title: item.name, children: truncateMiddle(item.name) }),
2120
- /* @__PURE__ */ jsx4(
2121
- "button",
2122
- {
2123
- css: styles4.renameBtn,
2124
- onClick: (e) => {
2125
- e.stopPropagation();
2126
- onRename();
2127
- },
2128
- title: "Rename",
2129
- children: /* @__PURE__ */ jsx4("svg", { css: styles4.renameIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) })
2130
- }
2131
- )
2132
- ] }),
2229
+ /* @__PURE__ */ jsx4("p", { css: styles4.name, title: item.name, children: truncateMiddle(item.name) }),
2133
2230
  isFolder ? /* @__PURE__ */ jsxs4("p", { css: styles4.size, children: [
2134
2231
  item.fileCount !== void 0 ? `${item.fileCount} files` : "",
2135
2232
  item.fileCount !== void 0 && item.totalSize !== void 0 ? " \xB7 " : "",
@@ -2160,7 +2257,7 @@ function truncateMiddle(str, maxLength = 24) {
2160
2257
  }
2161
2258
 
2162
2259
  // src/components/StudioFileList.tsx
2163
- import { useEffect as useEffect3, useState as useState4, useRef as useRef3 } from "react";
2260
+ import { useState as useState5 } from "react";
2164
2261
  import { css as css5, keyframes as keyframes5 } from "@emotion/react";
2165
2262
  import { jsx as jsx5, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
2166
2263
  var spin3 = keyframes5`
@@ -2472,160 +2569,72 @@ var styles5 = {
2472
2569
  `
2473
2570
  };
2474
2571
  function StudioFileList() {
2475
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh, searchQuery } = useStudio();
2476
- const [items, setItems] = useState4([]);
2477
- const [loading, setLoading] = useState4(true);
2478
- const [renameItem, setRenameItem] = useState4(null);
2479
- const isInitialLoad = useRef3(true);
2480
- const lastPath = useRef3(currentPath);
2481
- useEffect3(() => {
2482
- async function loadItems() {
2483
- const isPathChange = lastPath.current !== currentPath;
2484
- if (isInitialLoad.current || isPathChange) {
2485
- setLoading(true);
2486
- }
2487
- lastPath.current = currentPath;
2488
- try {
2489
- const url = searchQuery && searchQuery.length >= 2 ? `/api/studio/search?q=${encodeURIComponent(searchQuery)}` : `/api/studio/list?path=${encodeURIComponent(currentPath)}`;
2490
- const response = await fetch(url);
2491
- if (response.ok) {
2492
- const data = await response.json();
2493
- setItems(data.items || []);
2494
- }
2495
- } catch (error) {
2496
- console.error("Failed to load items:", error);
2497
- }
2498
- setLoading(false);
2499
- isInitialLoad.current = false;
2500
- }
2501
- loadItems();
2502
- }, [currentPath, refreshKey, searchQuery]);
2572
+ const {
2573
+ loading,
2574
+ sortedItems,
2575
+ isAtRoot,
2576
+ isSearching,
2577
+ allItemsSelected,
2578
+ someItemsSelected,
2579
+ selectedItems,
2580
+ navigateUp,
2581
+ handleItemClick,
2582
+ handleOpen,
2583
+ handleGenerateThumbnail,
2584
+ handleSelectAll
2585
+ } = useFileList();
2503
2586
  if (loading) {
2504
2587
  return /* @__PURE__ */ jsx5("div", { css: styles5.loading, children: /* @__PURE__ */ jsx5("div", { css: styles5.spinner }) });
2505
2588
  }
2506
- const isAtRoot = currentPath === "public";
2507
- if (items.length === 0 && isAtRoot) {
2589
+ if (sortedItems.length === 0 && isAtRoot) {
2508
2590
  return /* @__PURE__ */ jsx5("div", { css: styles5.empty, children: /* @__PURE__ */ jsx5("p", { children: "No files in this folder" }) });
2509
2591
  }
2510
- const isSearching = searchQuery && searchQuery.length >= 2;
2511
- const sortedItems = [...items].sort((a, b) => {
2512
- if (a.type === "folder" && b.type !== "folder") return -1;
2513
- if (a.type !== "folder" && b.type === "folder") return 1;
2514
- return a.name.localeCompare(b.name);
2515
- });
2516
- const handleItemClick = (item, e) => {
2517
- if (e.shiftKey && lastSelectedPath) {
2518
- selectRange(lastSelectedPath, item.path, sortedItems);
2519
- } else {
2520
- toggleSelection(item.path);
2521
- }
2522
- };
2523
- const handleOpen = (item) => {
2524
- if (item.type === "folder") {
2525
- setCurrentPath(item.path);
2526
- } else {
2527
- setFocusedItem(item);
2528
- }
2529
- };
2530
- const handleGenerateThumbnail = async (item) => {
2531
- try {
2532
- const imageKey = item.path.replace(/^public\//, "");
2533
- await fetch("/api/studio/reprocess", {
2534
- method: "POST",
2535
- headers: { "Content-Type": "application/json" },
2536
- body: JSON.stringify({ imageKeys: [imageKey] })
2537
- });
2538
- triggerRefresh();
2539
- } catch (error) {
2540
- console.error("Failed to generate thumbnail:", error);
2541
- }
2542
- };
2543
- const handleRename = async (newName) => {
2544
- if (!renameItem) return;
2545
- setRenameItem(null);
2546
- try {
2547
- const response = await fetch("/api/studio/rename", {
2548
- method: "POST",
2549
- headers: { "Content-Type": "application/json" },
2550
- body: JSON.stringify({ oldPath: renameItem.path, newName })
2551
- });
2552
- if (response.ok) {
2553
- triggerRefresh();
2554
- }
2555
- } catch (error) {
2556
- console.error("Failed to rename:", error);
2557
- }
2558
- };
2559
- const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
2560
- const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
2561
- const handleSelectAll = () => {
2562
- if (allItemsSelected) {
2563
- clearSelection();
2564
- } else {
2565
- selectAll(sortedItems);
2566
- }
2567
- };
2568
- return /* @__PURE__ */ jsxs5("div", { css: styles5.tableWrapper, children: [
2569
- renameItem && /* @__PURE__ */ jsx5(
2570
- InputModal,
2571
- {
2572
- title: renameItem.type === "folder" ? "Rename Folder" : "Rename File",
2573
- message: "Enter a new name:",
2574
- placeholder: renameItem.name,
2575
- defaultValue: renameItem.name,
2576
- confirmLabel: "Rename",
2577
- onConfirm: handleRename,
2578
- onCancel: () => setRenameItem(null)
2579
- }
2580
- ),
2581
- /* @__PURE__ */ jsxs5("table", { css: styles5.table, children: [
2582
- /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2583
- /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thCheckbox], children: sortedItems.length > 0 && /* @__PURE__ */ jsx5(
2584
- "input",
2585
- {
2586
- type: "checkbox",
2587
- css: styles5.checkbox,
2588
- checked: allItemsSelected,
2589
- ref: (el) => {
2590
- if (el) el.indeterminate = someItemsSelected && !allItemsSelected;
2591
- },
2592
- onChange: handleSelectAll
2593
- }
2594
- ) }),
2595
- /* @__PURE__ */ jsx5("th", { css: styles5.th, children: "Name" }),
2596
- /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thSize], children: "Size" }),
2597
- /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thDimensions], children: "Dimensions" }),
2598
- /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thCdn], children: "CDN" })
2599
- ] }) }),
2600
- /* @__PURE__ */ jsxs5("tbody", { css: styles5.tbody, children: [
2601
- !isAtRoot && !isSearching && /* @__PURE__ */ jsxs5("tr", { css: styles5.parentRow, onClick: navigateUp, children: [
2602
- /* @__PURE__ */ jsx5("td", { css: styles5.td }),
2603
- /* @__PURE__ */ jsx5("td", { css: styles5.td, children: /* @__PURE__ */ jsxs5("div", { css: styles5.nameCell, children: [
2604
- /* @__PURE__ */ jsx5("svg", { css: styles5.parentIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" }) }),
2605
- /* @__PURE__ */ jsx5("span", { css: styles5.name, children: ".." })
2606
- ] }) }),
2607
- /* @__PURE__ */ jsx5("td", { css: [styles5.td, styles5.meta], children: "--" }),
2608
- /* @__PURE__ */ jsx5("td", { css: [styles5.td, styles5.meta], children: "Parent folder" }),
2609
- /* @__PURE__ */ jsx5("td", { css: styles5.td, children: "--" })
2610
- ] }),
2611
- sortedItems.map((item) => /* @__PURE__ */ jsx5(
2612
- ListRow,
2613
- {
2614
- item,
2615
- isSelected: selectedItems.has(item.path),
2616
- onClick: (e) => handleItemClick(item, e),
2617
- onOpen: () => handleOpen(item),
2618
- onGenerateThumbnail: () => handleGenerateThumbnail(item),
2619
- onRename: () => setRenameItem(item)
2592
+ return /* @__PURE__ */ jsx5("div", { css: styles5.tableWrapper, children: /* @__PURE__ */ jsxs5("table", { css: styles5.table, children: [
2593
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2594
+ /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thCheckbox], children: sortedItems.length > 0 && /* @__PURE__ */ jsx5(
2595
+ "input",
2596
+ {
2597
+ type: "checkbox",
2598
+ css: styles5.checkbox,
2599
+ checked: allItemsSelected,
2600
+ ref: (el) => {
2601
+ if (el) el.indeterminate = someItemsSelected && !allItemsSelected;
2620
2602
  },
2621
- item.path
2622
- ))
2623
- ] })
2603
+ onChange: handleSelectAll
2604
+ }
2605
+ ) }),
2606
+ /* @__PURE__ */ jsx5("th", { css: styles5.th, children: "Name" }),
2607
+ /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thSize], children: "Size" }),
2608
+ /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thDimensions], children: "Dimensions" }),
2609
+ /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thCdn], children: "CDN" })
2610
+ ] }) }),
2611
+ /* @__PURE__ */ jsxs5("tbody", { css: styles5.tbody, children: [
2612
+ !isAtRoot && !isSearching && /* @__PURE__ */ jsxs5("tr", { css: styles5.parentRow, onClick: navigateUp, children: [
2613
+ /* @__PURE__ */ jsx5("td", { css: styles5.td }),
2614
+ /* @__PURE__ */ jsx5("td", { css: styles5.td, children: /* @__PURE__ */ jsxs5("div", { css: styles5.nameCell, children: [
2615
+ /* @__PURE__ */ jsx5("svg", { css: styles5.parentIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" }) }),
2616
+ /* @__PURE__ */ jsx5("span", { css: styles5.name, children: ".." })
2617
+ ] }) }),
2618
+ /* @__PURE__ */ jsx5("td", { css: [styles5.td, styles5.meta], children: "--" }),
2619
+ /* @__PURE__ */ jsx5("td", { css: [styles5.td, styles5.meta], children: "Parent folder" }),
2620
+ /* @__PURE__ */ jsx5("td", { css: styles5.td, children: "--" })
2621
+ ] }),
2622
+ sortedItems.map((item) => /* @__PURE__ */ jsx5(
2623
+ ListRow,
2624
+ {
2625
+ item,
2626
+ isSelected: selectedItems.has(item.path),
2627
+ onClick: (e) => handleItemClick(item, e),
2628
+ onOpen: () => handleOpen(item),
2629
+ onGenerateThumbnail: () => handleGenerateThumbnail(item)
2630
+ },
2631
+ item.path
2632
+ ))
2624
2633
  ] })
2625
- ] });
2634
+ ] }) });
2626
2635
  }
2627
- function ListRow({ item, isSelected, onClick, onOpen, onGenerateThumbnail, onRename }) {
2628
- const [showCopied, setShowCopied] = useState4(false);
2636
+ function ListRow({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
2637
+ const [showCopied, setShowCopied] = useState5(false);
2629
2638
  const isFolder = item.type === "folder";
2630
2639
  const isImage = !isFolder && item.thumbnail !== void 0;
2631
2640
  const isImagesFolder = isFolder && (item.name === "images" || item.path.includes("/images/"));
@@ -2688,18 +2697,6 @@ function ListRow({ item, isSelected, onClick, onOpen, onGenerateThumbnail, onRen
2688
2697
  ]
2689
2698
  }
2690
2699
  ),
2691
- /* @__PURE__ */ jsx5(
2692
- "button",
2693
- {
2694
- css: styles5.copyBtn,
2695
- onClick: (e) => {
2696
- e.stopPropagation();
2697
- onRename();
2698
- },
2699
- title: "Rename",
2700
- children: /* @__PURE__ */ jsx5("svg", { css: styles5.copyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) })
2701
- }
2702
- ),
2703
2700
  /* @__PURE__ */ jsx5(
2704
2701
  "button",
2705
2702
  {
@@ -2743,7 +2740,7 @@ function truncateMiddle2(str, maxLength = 32) {
2743
2740
  }
2744
2741
 
2745
2742
  // src/components/StudioDetailView.tsx
2746
- import { useState as useState5 } from "react";
2743
+ import { useState as useState6 } from "react";
2747
2744
  import { css as css6 } from "@emotion/react";
2748
2745
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
2749
2746
  var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"];
@@ -3004,12 +3001,12 @@ var styles6 = {
3004
3001
  };
3005
3002
  function StudioDetailView() {
3006
3003
  const { focusedItem, setFocusedItem, triggerRefresh, clearSelection } = useStudio();
3007
- const [showDeleteConfirm, setShowDeleteConfirm] = useState5(false);
3008
- const [showRenameModal, setShowRenameModal] = useState5(false);
3009
- const [showProcessConfirm, setShowProcessConfirm] = useState5(false);
3010
- const [processProgress, setProcessProgress] = useState5(null);
3011
- const [alertMessage, setAlertMessage] = useState5(null);
3012
- const [showCopied, setShowCopied] = useState5(false);
3004
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState6(false);
3005
+ const [showRenameModal, setShowRenameModal] = useState6(false);
3006
+ const [showProcessConfirm, setShowProcessConfirm] = useState6(false);
3007
+ const [processProgress, setProcessProgress] = useState6(null);
3008
+ const [alertMessage, setAlertMessage] = useState6(null);
3009
+ const [showCopied, setShowCopied] = useState6(false);
3013
3010
  if (!focusedItem) return null;
3014
3011
  const isImage = isImageFile(focusedItem.name);
3015
3012
  const isVideo = isVideoFile(focusedItem.name);
@@ -3270,7 +3267,7 @@ function formatFileSize3(bytes) {
3270
3267
  }
3271
3268
 
3272
3269
  // src/components/StudioSettings.tsx
3273
- import { useState as useState6 } from "react";
3270
+ import { useState as useState7 } from "react";
3274
3271
  import { css as css7 } from "@emotion/react";
3275
3272
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "@emotion/react/jsx-runtime";
3276
3273
  var btnHeight2 = "36px";
@@ -3510,7 +3507,7 @@ var styles7 = {
3510
3507
  `
3511
3508
  };
3512
3509
  function StudioSettings() {
3513
- const [isOpen, setIsOpen] = useState6(false);
3510
+ const [isOpen, setIsOpen] = useState7(false);
3514
3511
  return /* @__PURE__ */ jsxs7(Fragment4, { children: [
3515
3512
  /* @__PURE__ */ jsx7("button", { css: styles7.btn, onClick: () => setIsOpen(true), "aria-label": "Settings", children: /* @__PURE__ */ jsxs7(
3516
3513
  "svg",
@@ -3538,7 +3535,7 @@ CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_access_key_here
3538
3535
  CLOUDFLARE_R2_BUCKET_NAME=my-images-bucket
3539
3536
  CLOUDFLARE_R2_PUBLIC_URL=https://cdn.yourdomain.com`;
3540
3537
  function SettingsPanel({ onClose }) {
3541
- const [copied, setCopied] = useState6(false);
3538
+ const [copied, setCopied] = useState7(false);
3542
3539
  const handleCopy = () => {
3543
3540
  navigator.clipboard.writeText(envTemplate);
3544
3541
  setCopied(true);
@@ -3592,18 +3589,93 @@ function SettingsPanel({ onClose }) {
3592
3589
  ] }) });
3593
3590
  }
3594
3591
 
3595
- // src/components/StudioUI.tsx
3592
+ // src/components/ErrorModal.tsx
3593
+ import { css as css8 } from "@emotion/react";
3596
3594
  import { jsx as jsx8, jsxs as jsxs8 } from "@emotion/react/jsx-runtime";
3597
- var btnHeight3 = "36px";
3598
3595
  var styles8 = {
3599
- container: css8`
3596
+ overlay: css8`
3597
+ position: fixed;
3598
+ inset: 0;
3599
+ background: rgba(0, 0, 0, 0.5);
3600
+ display: flex;
3601
+ align-items: center;
3602
+ justify-content: center;
3603
+ z-index: 1100;
3604
+ `,
3605
+ modal: css8`
3606
+ background: ${colors.surface};
3607
+ border-radius: 12px;
3608
+ padding: 24px;
3609
+ max-width: 400px;
3610
+ width: 90%;
3611
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
3612
+ `,
3613
+ header: css8`
3614
+ display: flex;
3615
+ align-items: center;
3616
+ gap: 12px;
3617
+ margin-bottom: 12px;
3618
+ `,
3619
+ icon: css8`
3620
+ width: 24px;
3621
+ height: 24px;
3622
+ color: ${colors.danger};
3623
+ flex-shrink: 0;
3624
+ `,
3625
+ title: css8`
3626
+ font-size: ${fontSize.lg};
3627
+ font-weight: 600;
3628
+ color: ${colors.text};
3629
+ margin: 0;
3630
+ `,
3631
+ message: css8`
3632
+ font-size: ${fontSize.base};
3633
+ color: ${colors.textSecondary};
3634
+ margin: 0 0 20px 0;
3635
+ line-height: 1.5;
3636
+ `,
3637
+ button: css8`
3638
+ width: 100%;
3639
+ padding: 10px 16px;
3640
+ border-radius: 6px;
3641
+ font-size: ${fontSize.base};
3642
+ font-weight: 500;
3643
+ border: none;
3644
+ background: ${colors.primary};
3645
+ color: white;
3646
+ cursor: pointer;
3647
+ transition: background 0.15s ease;
3648
+
3649
+ &:hover {
3650
+ background: ${colors.primaryHover};
3651
+ }
3652
+ `
3653
+ };
3654
+ function ErrorModal() {
3655
+ const { error, clearError } = useStudio();
3656
+ if (!error) return null;
3657
+ return /* @__PURE__ */ jsx8("div", { css: styles8.overlay, onClick: clearError, children: /* @__PURE__ */ jsxs8("div", { css: styles8.modal, onClick: (e) => e.stopPropagation(), children: [
3658
+ /* @__PURE__ */ jsxs8("div", { css: styles8.header, children: [
3659
+ /* @__PURE__ */ jsx8("svg", { css: styles8.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }),
3660
+ /* @__PURE__ */ jsx8("h3", { css: styles8.title, children: error.title })
3661
+ ] }),
3662
+ /* @__PURE__ */ jsx8("p", { css: styles8.message, children: error.message }),
3663
+ /* @__PURE__ */ jsx8("button", { css: styles8.button, onClick: clearError, children: "OK" })
3664
+ ] }) });
3665
+ }
3666
+
3667
+ // src/components/StudioUI.tsx
3668
+ import { jsx as jsx9, jsxs as jsxs9 } from "@emotion/react/jsx-runtime";
3669
+ var btnHeight3 = "36px";
3670
+ var styles9 = {
3671
+ container: css9`
3600
3672
  ${baseReset}
3601
3673
  display: flex;
3602
3674
  flex-direction: column;
3603
3675
  height: 100%;
3604
3676
  background: ${colors.background};
3605
3677
  `,
3606
- header: css8`
3678
+ header: css9`
3607
3679
  display: flex;
3608
3680
  align-items: center;
3609
3681
  justify-content: space-between;
@@ -3612,7 +3684,7 @@ var styles8 = {
3612
3684
  border-bottom: 1px solid ${colors.border};
3613
3685
  position: relative;
3614
3686
  `,
3615
- title: css8`
3687
+ title: css9`
3616
3688
  font-size: ${fontSize.lg};
3617
3689
  font-weight: 600;
3618
3690
  color: ${colors.text};
@@ -3620,14 +3692,14 @@ var styles8 = {
3620
3692
  letter-spacing: -0.02em;
3621
3693
  flex-shrink: 0;
3622
3694
  `,
3623
- headerLeft: css8`
3695
+ headerLeft: css9`
3624
3696
  display: flex;
3625
3697
  align-items: center;
3626
3698
  gap: 12px;
3627
3699
  flex: 1;
3628
3700
  min-width: 0;
3629
3701
  `,
3630
- headerCenter: css8`
3702
+ headerCenter: css9`
3631
3703
  position: absolute;
3632
3704
  left: 50%;
3633
3705
  transform: translateX(-50%);
@@ -3635,7 +3707,7 @@ var styles8 = {
3635
3707
  align-items: center;
3636
3708
  max-width: 50%;
3637
3709
  `,
3638
- breadcrumbs: css8`
3710
+ breadcrumbs: css9`
3639
3711
  display: flex;
3640
3712
  align-items: center;
3641
3713
  gap: 6px;
@@ -3643,11 +3715,11 @@ var styles8 = {
3643
3715
  color: ${colors.textSecondary};
3644
3716
  overflow: hidden;
3645
3717
  `,
3646
- breadcrumbSeparator: css8`
3718
+ breadcrumbSeparator: css9`
3647
3719
  color: ${colors.border};
3648
3720
  flex-shrink: 0;
3649
3721
  `,
3650
- breadcrumbItem: css8`
3722
+ breadcrumbItem: css9`
3651
3723
  color: ${colors.textSecondary};
3652
3724
  text-decoration: none;
3653
3725
  cursor: pointer;
@@ -3658,19 +3730,19 @@ var styles8 = {
3658
3730
  color: ${colors.primary};
3659
3731
  }
3660
3732
  `,
3661
- breadcrumbCurrent: css8`
3733
+ breadcrumbCurrent: css9`
3662
3734
  color: ${colors.text};
3663
3735
  font-weight: 500;
3664
3736
  white-space: nowrap;
3665
3737
  overflow: hidden;
3666
3738
  text-overflow: ellipsis;
3667
3739
  `,
3668
- headerActions: css8`
3740
+ headerActions: css9`
3669
3741
  display: flex;
3670
3742
  align-items: center;
3671
3743
  gap: 8px;
3672
3744
  `,
3673
- headerBtn: css8`
3745
+ headerBtn: css9`
3674
3746
  height: ${btnHeight3};
3675
3747
  padding: 0 12px;
3676
3748
  background: ${colors.surface};
@@ -3687,23 +3759,23 @@ var styles8 = {
3687
3759
  border-color: ${colors.borderHover};
3688
3760
  }
3689
3761
  `,
3690
- headerIcon: css8`
3762
+ headerIcon: css9`
3691
3763
  width: 16px;
3692
3764
  height: 16px;
3693
3765
  color: ${colors.textSecondary};
3694
3766
  `,
3695
- content: css8`
3767
+ content: css9`
3696
3768
  flex: 1;
3697
3769
  display: flex;
3698
3770
  overflow: hidden;
3699
3771
  `,
3700
- fileBrowser: css8`
3772
+ fileBrowser: css9`
3701
3773
  flex: 1;
3702
3774
  min-width: 0;
3703
3775
  overflow: auto;
3704
3776
  padding: 20px 24px;
3705
3777
  `,
3706
- dropOverlay: css8`
3778
+ dropOverlay: css9`
3707
3779
  position: absolute;
3708
3780
  top: 0;
3709
3781
  left: 0;
@@ -3718,7 +3790,7 @@ var styles8 = {
3718
3790
  z-index: 50;
3719
3791
  pointer-events: none;
3720
3792
  `,
3721
- dropMessage: css8`
3793
+ dropMessage: css9`
3722
3794
  display: flex;
3723
3795
  flex-direction: column;
3724
3796
  align-items: center;
@@ -3727,36 +3799,43 @@ var styles8 = {
3727
3799
  font-size: ${fontSize.lg};
3728
3800
  font-weight: 600;
3729
3801
  `,
3730
- dropIcon: css8`
3802
+ dropIcon: css9`
3731
3803
  width: 48px;
3732
3804
  height: 48px;
3733
3805
  `
3734
3806
  };
3735
3807
  function StudioUI({ onClose, isVisible = true }) {
3736
- const [currentPath, setCurrentPathInternal] = useState7("public");
3737
- const [selectedItems, setSelectedItems] = useState7(/* @__PURE__ */ new Set());
3738
- const [lastSelectedPath, setLastSelectedPath] = useState7(null);
3739
- const [viewMode, setViewMode] = useState7("grid");
3740
- const [focusedItem, setFocusedItem] = useState7(null);
3741
- const [meta, setMeta] = useState7(null);
3742
- const [isLoading, setIsLoading] = useState7(false);
3743
- const [refreshKey, setRefreshKey] = useState7(0);
3744
- const [searchQuery, setSearchQuery] = useState7("");
3745
- const [isDragging, setIsDragging] = useState7(false);
3746
- const triggerRefresh = useCallback2(() => {
3808
+ const [currentPath, setCurrentPathInternal] = useState8("public");
3809
+ const [selectedItems, setSelectedItems] = useState8(/* @__PURE__ */ new Set());
3810
+ const [lastSelectedPath, setLastSelectedPath] = useState8(null);
3811
+ const [viewMode, setViewMode] = useState8("grid");
3812
+ const [focusedItem, setFocusedItem] = useState8(null);
3813
+ const [meta, setMeta] = useState8(null);
3814
+ const [isLoading, setIsLoading] = useState8(false);
3815
+ const [refreshKey, setRefreshKey] = useState8(0);
3816
+ const [searchQuery, setSearchQuery] = useState8("");
3817
+ const [error, setError] = useState8(null);
3818
+ const [isDragging, setIsDragging] = useState8(false);
3819
+ const triggerRefresh = useCallback3(() => {
3747
3820
  setRefreshKey((k) => k + 1);
3748
3821
  }, []);
3749
- const handleDragOver = useCallback2((e) => {
3822
+ const showError = useCallback3((title, message) => {
3823
+ setError({ title, message });
3824
+ }, []);
3825
+ const clearError = useCallback3(() => {
3826
+ setError(null);
3827
+ }, []);
3828
+ const handleDragOver = useCallback3((e) => {
3750
3829
  e.preventDefault();
3751
3830
  e.stopPropagation();
3752
3831
  setIsDragging(true);
3753
3832
  }, []);
3754
- const handleDragLeave = useCallback2((e) => {
3833
+ const handleDragLeave = useCallback3((e) => {
3755
3834
  e.preventDefault();
3756
3835
  e.stopPropagation();
3757
3836
  setIsDragging(false);
3758
3837
  }, []);
3759
- const handleDrop = useCallback2(async (e) => {
3838
+ const handleDrop = useCallback3(async (e) => {
3760
3839
  e.preventDefault();
3761
3840
  e.stopPropagation();
3762
3841
  setIsDragging(false);
@@ -3774,25 +3853,25 @@ function StudioUI({ onClose, isVisible = true }) {
3774
3853
  method: "POST",
3775
3854
  body: formData
3776
3855
  });
3777
- } catch (error) {
3778
- console.error("Upload error:", error);
3856
+ } catch (error2) {
3857
+ console.error("Upload error:", error2);
3779
3858
  }
3780
3859
  }
3781
3860
  triggerRefresh();
3782
3861
  }, [currentPath, triggerRefresh]);
3783
- const navigateUp = useCallback2(() => {
3862
+ const navigateUp = useCallback3(() => {
3784
3863
  if (currentPath === "public") return;
3785
3864
  const parts = currentPath.split("/");
3786
3865
  parts.pop();
3787
3866
  setCurrentPathInternal(parts.join("/") || "public");
3788
3867
  setSelectedItems(/* @__PURE__ */ new Set());
3789
3868
  }, [currentPath]);
3790
- const setCurrentPath = useCallback2((path) => {
3869
+ const setCurrentPath = useCallback3((path) => {
3791
3870
  setCurrentPathInternal(path);
3792
3871
  setSelectedItems(/* @__PURE__ */ new Set());
3793
3872
  setFocusedItem(null);
3794
3873
  }, []);
3795
- const toggleSelection = useCallback2((path) => {
3874
+ const toggleSelection = useCallback3((path) => {
3796
3875
  setSelectedItems((prev) => {
3797
3876
  const next = new Set(prev);
3798
3877
  if (next.has(path)) {
@@ -3804,7 +3883,7 @@ function StudioUI({ onClose, isVisible = true }) {
3804
3883
  });
3805
3884
  setLastSelectedPath(path);
3806
3885
  }, []);
3807
- const selectRange = useCallback2((fromPath, toPath, allItems) => {
3886
+ const selectRange = useCallback3((fromPath, toPath, allItems) => {
3808
3887
  const fromIndex = allItems.findIndex((item) => item.path === fromPath);
3809
3888
  const toIndex = allItems.findIndex((item) => item.path === toPath);
3810
3889
  if (fromIndex === -1 || toIndex === -1) return;
@@ -3819,13 +3898,13 @@ function StudioUI({ onClose, isVisible = true }) {
3819
3898
  });
3820
3899
  setLastSelectedPath(toPath);
3821
3900
  }, []);
3822
- const selectAll = useCallback2((items) => {
3901
+ const selectAll = useCallback3((items) => {
3823
3902
  setSelectedItems(new Set(items.map((item) => item.path)));
3824
3903
  }, []);
3825
- const clearSelection = useCallback2(() => {
3904
+ const clearSelection = useCallback3(() => {
3826
3905
  setSelectedItems(/* @__PURE__ */ new Set());
3827
3906
  }, []);
3828
- const handleKeyDown = useCallback2(
3907
+ const handleKeyDown = useCallback3(
3829
3908
  (e) => {
3830
3909
  if (e.key === "Escape") {
3831
3910
  const target = e.target;
@@ -3841,7 +3920,7 @@ function StudioUI({ onClose, isVisible = true }) {
3841
3920
  },
3842
3921
  [onClose, focusedItem]
3843
3922
  );
3844
- useEffect4(() => {
3923
+ useEffect3(() => {
3845
3924
  if (isVisible) {
3846
3925
  document.addEventListener("keydown", handleKeyDown);
3847
3926
  document.body.style.overflow = "hidden";
@@ -3877,43 +3956,47 @@ function StudioUI({ onClose, isVisible = true }) {
3877
3956
  refreshKey,
3878
3957
  triggerRefresh,
3879
3958
  searchQuery,
3880
- setSearchQuery
3959
+ setSearchQuery,
3960
+ error,
3961
+ showError,
3962
+ clearError
3881
3963
  };
3882
- return /* @__PURE__ */ jsx8(StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs8("div", { css: styles8.container, children: [
3883
- /* @__PURE__ */ jsxs8("div", { css: styles8.header, children: [
3884
- /* @__PURE__ */ jsx8("div", { css: styles8.headerLeft, children: /* @__PURE__ */ jsx8("h1", { css: styles8.title, children: "Studio" }) }),
3885
- /* @__PURE__ */ jsx8("div", { css: styles8.headerCenter, children: /* @__PURE__ */ jsx8(Breadcrumbs, { currentPath, onNavigate: setCurrentPath }) }),
3886
- /* @__PURE__ */ jsxs8("div", { css: styles8.headerActions, children: [
3887
- /* @__PURE__ */ jsx8(StudioSettings, {}),
3888
- /* @__PURE__ */ jsx8(
3964
+ return /* @__PURE__ */ jsx9(StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs9("div", { css: styles9.container, children: [
3965
+ /* @__PURE__ */ jsxs9("div", { css: styles9.header, children: [
3966
+ /* @__PURE__ */ jsx9("div", { css: styles9.headerLeft, children: /* @__PURE__ */ jsx9("h1", { css: styles9.title, children: "Studio" }) }),
3967
+ /* @__PURE__ */ jsx9("div", { css: styles9.headerCenter, children: /* @__PURE__ */ jsx9(Breadcrumbs, { currentPath, onNavigate: setCurrentPath }) }),
3968
+ /* @__PURE__ */ jsxs9("div", { css: styles9.headerActions, children: [
3969
+ /* @__PURE__ */ jsx9(StudioSettings, {}),
3970
+ /* @__PURE__ */ jsx9(
3889
3971
  "button",
3890
3972
  {
3891
- css: styles8.headerBtn,
3973
+ css: styles9.headerBtn,
3892
3974
  onClick: onClose,
3893
3975
  "aria-label": "Close Studio",
3894
- children: /* @__PURE__ */ jsx8(CloseIcon, {})
3976
+ children: /* @__PURE__ */ jsx9(CloseIcon, {})
3895
3977
  }
3896
3978
  )
3897
3979
  ] })
3898
3980
  ] }),
3899
- /* @__PURE__ */ jsx8(StudioToolbar, {}),
3900
- /* @__PURE__ */ jsxs8(
3981
+ /* @__PURE__ */ jsx9(StudioToolbar, {}),
3982
+ /* @__PURE__ */ jsxs9(
3901
3983
  "div",
3902
3984
  {
3903
- css: styles8.content,
3985
+ css: styles9.content,
3904
3986
  onDragOver: handleDragOver,
3905
3987
  onDragLeave: handleDragLeave,
3906
3988
  onDrop: handleDrop,
3907
3989
  children: [
3908
- isDragging && /* @__PURE__ */ jsx8("div", { css: styles8.dropOverlay, children: /* @__PURE__ */ jsxs8("div", { css: styles8.dropMessage, children: [
3909
- /* @__PURE__ */ jsx8("svg", { css: styles8.dropIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" }) }),
3910
- /* @__PURE__ */ jsx8("span", { children: "Drop files to upload" })
3990
+ isDragging && /* @__PURE__ */ jsx9("div", { css: styles9.dropOverlay, children: /* @__PURE__ */ jsxs9("div", { css: styles9.dropMessage, children: [
3991
+ /* @__PURE__ */ jsx9("svg", { css: styles9.dropIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx9("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" }) }),
3992
+ /* @__PURE__ */ jsx9("span", { children: "Drop files to upload" })
3911
3993
  ] }) }),
3912
- /* @__PURE__ */ jsx8("div", { css: styles8.fileBrowser, children: viewMode === "grid" ? /* @__PURE__ */ jsx8(StudioFileGrid, {}) : /* @__PURE__ */ jsx8(StudioFileList, {}) })
3994
+ /* @__PURE__ */ jsx9("div", { css: styles9.fileBrowser, children: viewMode === "grid" ? /* @__PURE__ */ jsx9(StudioFileGrid, {}) : /* @__PURE__ */ jsx9(StudioFileList, {}) })
3913
3995
  ]
3914
3996
  }
3915
3997
  ),
3916
- focusedItem && /* @__PURE__ */ jsx8(StudioDetailView, {})
3998
+ focusedItem && /* @__PURE__ */ jsx9(StudioDetailView, {}),
3999
+ /* @__PURE__ */ jsx9(ErrorModal, {})
3917
4000
  ] }) });
3918
4001
  }
3919
4002
  function Breadcrumbs({ currentPath, onNavigate }) {
@@ -3922,12 +4005,12 @@ function Breadcrumbs({ currentPath, onNavigate }) {
3922
4005
  name: part,
3923
4006
  path: parts.slice(0, index + 1).join("/")
3924
4007
  }));
3925
- return /* @__PURE__ */ jsx8("div", { css: styles8.breadcrumbs, children: breadcrumbs.map((crumb, index) => /* @__PURE__ */ jsxs8("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3926
- index > 0 && /* @__PURE__ */ jsx8("span", { css: styles8.breadcrumbSeparator, children: "/" }),
3927
- index === breadcrumbs.length - 1 ? /* @__PURE__ */ jsx8("span", { css: styles8.breadcrumbCurrent, children: crumb.name }) : /* @__PURE__ */ jsx8(
4008
+ return /* @__PURE__ */ jsx9("div", { css: styles9.breadcrumbs, children: breadcrumbs.map((crumb, index) => /* @__PURE__ */ jsxs9("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
4009
+ index > 0 && /* @__PURE__ */ jsx9("span", { css: styles9.breadcrumbSeparator, children: "/" }),
4010
+ index === breadcrumbs.length - 1 ? /* @__PURE__ */ jsx9("span", { css: styles9.breadcrumbCurrent, children: crumb.name }) : /* @__PURE__ */ jsx9(
3928
4011
  "span",
3929
4012
  {
3930
- css: styles8.breadcrumbItem,
4013
+ css: styles9.breadcrumbItem,
3931
4014
  onClick: () => onNavigate(crumb.path),
3932
4015
  children: crumb.name
3933
4016
  }
@@ -3935,10 +4018,10 @@ function Breadcrumbs({ currentPath, onNavigate }) {
3935
4018
  ] }, crumb.path)) });
3936
4019
  }
3937
4020
  function CloseIcon() {
3938
- return /* @__PURE__ */ jsxs8(
4021
+ return /* @__PURE__ */ jsxs9(
3939
4022
  "svg",
3940
4023
  {
3941
- css: styles8.headerIcon,
4024
+ css: styles9.headerIcon,
3942
4025
  xmlns: "http://www.w3.org/2000/svg",
3943
4026
  viewBox: "0 0 24 24",
3944
4027
  fill: "none",
@@ -3947,8 +4030,8 @@ function CloseIcon() {
3947
4030
  strokeLinecap: "round",
3948
4031
  strokeLinejoin: "round",
3949
4032
  children: [
3950
- /* @__PURE__ */ jsx8("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3951
- /* @__PURE__ */ jsx8("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
4033
+ /* @__PURE__ */ jsx9("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
4034
+ /* @__PURE__ */ jsx9("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
3952
4035
  ]
3953
4036
  }
3954
4037
  );
@@ -3958,4 +4041,4 @@ export {
3958
4041
  StudioUI,
3959
4042
  StudioUI_default as default
3960
4043
  };
3961
- //# sourceMappingURL=StudioUI-VPNL5NMI.mjs.map
4044
+ //# sourceMappingURL=StudioUI-VJVOSOPD.mjs.map