@gallop.software/studio 0.1.81 → 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);
@@ -1551,8 +1556,196 @@ function ImageStackIcon() {
1551
1556
  }
1552
1557
 
1553
1558
  // src/components/StudioFileGrid.tsx
1554
- import { useEffect as useEffect2, useState as useState3, useRef as useRef2 } from "react";
1559
+ import { useState as useState4 } from "react";
1555
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
1556
1749
  import { jsx as jsx4, jsxs as jsxs4 } from "@emotion/react/jsx-runtime";
1557
1750
  var spin2 = keyframes4`
1558
1751
  to { transform: rotate(360deg); }
@@ -1875,86 +2068,30 @@ var styles4 = {
1875
2068
  `
1876
2069
  };
1877
2070
  function StudioFileGrid() {
1878
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh, searchQuery } = useStudio();
1879
- const [items, setItems] = useState3([]);
1880
- const [loading, setLoading] = useState3(true);
1881
- const isInitialLoad = useRef2(true);
1882
- const lastPath = useRef2(currentPath);
1883
- useEffect2(() => {
1884
- async function loadItems() {
1885
- const isPathChange = lastPath.current !== currentPath;
1886
- if (isInitialLoad.current || isPathChange) {
1887
- setLoading(true);
1888
- }
1889
- lastPath.current = currentPath;
1890
- try {
1891
- const url = searchQuery && searchQuery.length >= 2 ? `/api/studio/search?q=${encodeURIComponent(searchQuery)}` : `/api/studio/list?path=${encodeURIComponent(currentPath)}`;
1892
- const response = await fetch(url);
1893
- if (response.ok) {
1894
- const data = await response.json();
1895
- setItems(data.items || []);
1896
- }
1897
- } catch (error) {
1898
- console.error("Failed to load items:", error);
1899
- }
1900
- setLoading(false);
1901
- isInitialLoad.current = false;
1902
- }
1903
- loadItems();
1904
- }, [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();
1905
2085
  if (loading) {
1906
2086
  return /* @__PURE__ */ jsx4("div", { css: styles4.loading, children: /* @__PURE__ */ jsx4("div", { css: styles4.spinner }) });
1907
2087
  }
1908
- const isAtRoot = currentPath === "public";
1909
- if (items.length === 0 && isAtRoot) {
2088
+ if (sortedItems.length === 0 && isAtRoot) {
1910
2089
  return /* @__PURE__ */ jsxs4("div", { css: styles4.empty, children: [
1911
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" }) }),
1912
2091
  /* @__PURE__ */ jsx4("p", { css: styles4.emptyText, children: "No files in this folder" }),
1913
2092
  /* @__PURE__ */ jsx4("p", { css: styles4.emptyText, children: "Upload images to get started" })
1914
2093
  ] });
1915
2094
  }
1916
- const isSearching = searchQuery && searchQuery.length >= 2;
1917
- const sortedItems = [...items].sort((a, b) => {
1918
- if (a.type === "folder" && b.type !== "folder") return -1;
1919
- if (a.type !== "folder" && b.type === "folder") return 1;
1920
- return a.name.localeCompare(b.name);
1921
- });
1922
- const handleItemClick = (item, e) => {
1923
- if (e.shiftKey && lastSelectedPath) {
1924
- selectRange(lastSelectedPath, item.path, sortedItems);
1925
- } else {
1926
- toggleSelection(item.path);
1927
- }
1928
- };
1929
- const handleOpen = (item) => {
1930
- if (item.type === "folder") {
1931
- setCurrentPath(item.path);
1932
- } else {
1933
- setFocusedItem(item);
1934
- }
1935
- };
1936
- const handleGenerateThumbnail = async (item) => {
1937
- try {
1938
- const imageKey = item.path.replace(/^public\//, "");
1939
- await fetch("/api/studio/reprocess", {
1940
- method: "POST",
1941
- headers: { "Content-Type": "application/json" },
1942
- body: JSON.stringify({ imageKeys: [imageKey] })
1943
- });
1944
- triggerRefresh();
1945
- } catch (error) {
1946
- console.error("Failed to generate thumbnail:", error);
1947
- }
1948
- };
1949
- const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
1950
- const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
1951
- const handleSelectAll = () => {
1952
- if (allItemsSelected) {
1953
- clearSelection();
1954
- } else {
1955
- selectAll(sortedItems);
1956
- }
1957
- };
1958
2095
  return /* @__PURE__ */ jsxs4("div", { children: [
1959
2096
  sortedItems.length > 0 && /* @__PURE__ */ jsx4("div", { css: styles4.selectAllRow, children: /* @__PURE__ */ jsxs4("label", { css: styles4.selectAllLabel, children: [
1960
2097
  /* @__PURE__ */ jsx4(
@@ -2003,7 +2140,7 @@ function StudioFileGrid() {
2003
2140
  ] });
2004
2141
  }
2005
2142
  function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
2006
- const [showCopied, setShowCopied] = useState3(false);
2143
+ const [showCopied, setShowCopied] = useState4(false);
2007
2144
  const isFolder = item.type === "folder";
2008
2145
  const isImage = !isFolder && item.thumbnail !== void 0;
2009
2146
  const isImagesFolder = isFolder && (item.name === "images" || item.path.includes("/images/"));
@@ -2120,7 +2257,7 @@ function truncateMiddle(str, maxLength = 24) {
2120
2257
  }
2121
2258
 
2122
2259
  // src/components/StudioFileList.tsx
2123
- import { useEffect as useEffect3, useState as useState4, useRef as useRef3 } from "react";
2260
+ import { useState as useState5 } from "react";
2124
2261
  import { css as css5, keyframes as keyframes5 } from "@emotion/react";
2125
2262
  import { jsx as jsx5, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
2126
2263
  var spin3 = keyframes5`
@@ -2432,82 +2569,26 @@ var styles5 = {
2432
2569
  `
2433
2570
  };
2434
2571
  function StudioFileList() {
2435
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh, searchQuery } = useStudio();
2436
- const [items, setItems] = useState4([]);
2437
- const [loading, setLoading] = useState4(true);
2438
- const isInitialLoad = useRef3(true);
2439
- const lastPath = useRef3(currentPath);
2440
- useEffect3(() => {
2441
- async function loadItems() {
2442
- const isPathChange = lastPath.current !== currentPath;
2443
- if (isInitialLoad.current || isPathChange) {
2444
- setLoading(true);
2445
- }
2446
- lastPath.current = currentPath;
2447
- try {
2448
- const url = searchQuery && searchQuery.length >= 2 ? `/api/studio/search?q=${encodeURIComponent(searchQuery)}` : `/api/studio/list?path=${encodeURIComponent(currentPath)}`;
2449
- const response = await fetch(url);
2450
- if (response.ok) {
2451
- const data = await response.json();
2452
- setItems(data.items || []);
2453
- }
2454
- } catch (error) {
2455
- console.error("Failed to load items:", error);
2456
- }
2457
- setLoading(false);
2458
- isInitialLoad.current = false;
2459
- }
2460
- loadItems();
2461
- }, [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();
2462
2586
  if (loading) {
2463
2587
  return /* @__PURE__ */ jsx5("div", { css: styles5.loading, children: /* @__PURE__ */ jsx5("div", { css: styles5.spinner }) });
2464
2588
  }
2465
- const isAtRoot = currentPath === "public";
2466
- if (items.length === 0 && isAtRoot) {
2589
+ if (sortedItems.length === 0 && isAtRoot) {
2467
2590
  return /* @__PURE__ */ jsx5("div", { css: styles5.empty, children: /* @__PURE__ */ jsx5("p", { children: "No files in this folder" }) });
2468
2591
  }
2469
- const isSearching = searchQuery && searchQuery.length >= 2;
2470
- const sortedItems = [...items].sort((a, b) => {
2471
- if (a.type === "folder" && b.type !== "folder") return -1;
2472
- if (a.type !== "folder" && b.type === "folder") return 1;
2473
- return a.name.localeCompare(b.name);
2474
- });
2475
- const handleItemClick = (item, e) => {
2476
- if (e.shiftKey && lastSelectedPath) {
2477
- selectRange(lastSelectedPath, item.path, sortedItems);
2478
- } else {
2479
- toggleSelection(item.path);
2480
- }
2481
- };
2482
- const handleOpen = (item) => {
2483
- if (item.type === "folder") {
2484
- setCurrentPath(item.path);
2485
- } else {
2486
- setFocusedItem(item);
2487
- }
2488
- };
2489
- const handleGenerateThumbnail = async (item) => {
2490
- try {
2491
- const imageKey = item.path.replace(/^public\//, "");
2492
- await fetch("/api/studio/reprocess", {
2493
- method: "POST",
2494
- headers: { "Content-Type": "application/json" },
2495
- body: JSON.stringify({ imageKeys: [imageKey] })
2496
- });
2497
- triggerRefresh();
2498
- } catch (error) {
2499
- console.error("Failed to generate thumbnail:", error);
2500
- }
2501
- };
2502
- const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
2503
- const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
2504
- const handleSelectAll = () => {
2505
- if (allItemsSelected) {
2506
- clearSelection();
2507
- } else {
2508
- selectAll(sortedItems);
2509
- }
2510
- };
2511
2592
  return /* @__PURE__ */ jsx5("div", { css: styles5.tableWrapper, children: /* @__PURE__ */ jsxs5("table", { css: styles5.table, children: [
2512
2593
  /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
2513
2594
  /* @__PURE__ */ jsx5("th", { css: [styles5.th, styles5.thCheckbox], children: sortedItems.length > 0 && /* @__PURE__ */ jsx5(
@@ -2553,7 +2634,7 @@ function StudioFileList() {
2553
2634
  ] }) });
2554
2635
  }
2555
2636
  function ListRow({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
2556
- const [showCopied, setShowCopied] = useState4(false);
2637
+ const [showCopied, setShowCopied] = useState5(false);
2557
2638
  const isFolder = item.type === "folder";
2558
2639
  const isImage = !isFolder && item.thumbnail !== void 0;
2559
2640
  const isImagesFolder = isFolder && (item.name === "images" || item.path.includes("/images/"));
@@ -2659,7 +2740,7 @@ function truncateMiddle2(str, maxLength = 32) {
2659
2740
  }
2660
2741
 
2661
2742
  // src/components/StudioDetailView.tsx
2662
- import { useState as useState5 } from "react";
2743
+ import { useState as useState6 } from "react";
2663
2744
  import { css as css6 } from "@emotion/react";
2664
2745
  import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
2665
2746
  var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"];
@@ -2920,12 +3001,12 @@ var styles6 = {
2920
3001
  };
2921
3002
  function StudioDetailView() {
2922
3003
  const { focusedItem, setFocusedItem, triggerRefresh, clearSelection } = useStudio();
2923
- const [showDeleteConfirm, setShowDeleteConfirm] = useState5(false);
2924
- const [showRenameModal, setShowRenameModal] = useState5(false);
2925
- const [showProcessConfirm, setShowProcessConfirm] = useState5(false);
2926
- const [processProgress, setProcessProgress] = useState5(null);
2927
- const [alertMessage, setAlertMessage] = useState5(null);
2928
- 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);
2929
3010
  if (!focusedItem) return null;
2930
3011
  const isImage = isImageFile(focusedItem.name);
2931
3012
  const isVideo = isVideoFile(focusedItem.name);
@@ -3186,7 +3267,7 @@ function formatFileSize3(bytes) {
3186
3267
  }
3187
3268
 
3188
3269
  // src/components/StudioSettings.tsx
3189
- import { useState as useState6 } from "react";
3270
+ import { useState as useState7 } from "react";
3190
3271
  import { css as css7 } from "@emotion/react";
3191
3272
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "@emotion/react/jsx-runtime";
3192
3273
  var btnHeight2 = "36px";
@@ -3426,7 +3507,7 @@ var styles7 = {
3426
3507
  `
3427
3508
  };
3428
3509
  function StudioSettings() {
3429
- const [isOpen, setIsOpen] = useState6(false);
3510
+ const [isOpen, setIsOpen] = useState7(false);
3430
3511
  return /* @__PURE__ */ jsxs7(Fragment4, { children: [
3431
3512
  /* @__PURE__ */ jsx7("button", { css: styles7.btn, onClick: () => setIsOpen(true), "aria-label": "Settings", children: /* @__PURE__ */ jsxs7(
3432
3513
  "svg",
@@ -3454,7 +3535,7 @@ CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_access_key_here
3454
3535
  CLOUDFLARE_R2_BUCKET_NAME=my-images-bucket
3455
3536
  CLOUDFLARE_R2_PUBLIC_URL=https://cdn.yourdomain.com`;
3456
3537
  function SettingsPanel({ onClose }) {
3457
- const [copied, setCopied] = useState6(false);
3538
+ const [copied, setCopied] = useState7(false);
3458
3539
  const handleCopy = () => {
3459
3540
  navigator.clipboard.writeText(envTemplate);
3460
3541
  setCopied(true);
@@ -3508,18 +3589,93 @@ function SettingsPanel({ onClose }) {
3508
3589
  ] }) });
3509
3590
  }
3510
3591
 
3511
- // src/components/StudioUI.tsx
3592
+ // src/components/ErrorModal.tsx
3593
+ import { css as css8 } from "@emotion/react";
3512
3594
  import { jsx as jsx8, jsxs as jsxs8 } from "@emotion/react/jsx-runtime";
3513
- var btnHeight3 = "36px";
3514
3595
  var styles8 = {
3515
- 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`
3516
3672
  ${baseReset}
3517
3673
  display: flex;
3518
3674
  flex-direction: column;
3519
3675
  height: 100%;
3520
3676
  background: ${colors.background};
3521
3677
  `,
3522
- header: css8`
3678
+ header: css9`
3523
3679
  display: flex;
3524
3680
  align-items: center;
3525
3681
  justify-content: space-between;
@@ -3528,7 +3684,7 @@ var styles8 = {
3528
3684
  border-bottom: 1px solid ${colors.border};
3529
3685
  position: relative;
3530
3686
  `,
3531
- title: css8`
3687
+ title: css9`
3532
3688
  font-size: ${fontSize.lg};
3533
3689
  font-weight: 600;
3534
3690
  color: ${colors.text};
@@ -3536,14 +3692,14 @@ var styles8 = {
3536
3692
  letter-spacing: -0.02em;
3537
3693
  flex-shrink: 0;
3538
3694
  `,
3539
- headerLeft: css8`
3695
+ headerLeft: css9`
3540
3696
  display: flex;
3541
3697
  align-items: center;
3542
3698
  gap: 12px;
3543
3699
  flex: 1;
3544
3700
  min-width: 0;
3545
3701
  `,
3546
- headerCenter: css8`
3702
+ headerCenter: css9`
3547
3703
  position: absolute;
3548
3704
  left: 50%;
3549
3705
  transform: translateX(-50%);
@@ -3551,7 +3707,7 @@ var styles8 = {
3551
3707
  align-items: center;
3552
3708
  max-width: 50%;
3553
3709
  `,
3554
- breadcrumbs: css8`
3710
+ breadcrumbs: css9`
3555
3711
  display: flex;
3556
3712
  align-items: center;
3557
3713
  gap: 6px;
@@ -3559,11 +3715,11 @@ var styles8 = {
3559
3715
  color: ${colors.textSecondary};
3560
3716
  overflow: hidden;
3561
3717
  `,
3562
- breadcrumbSeparator: css8`
3718
+ breadcrumbSeparator: css9`
3563
3719
  color: ${colors.border};
3564
3720
  flex-shrink: 0;
3565
3721
  `,
3566
- breadcrumbItem: css8`
3722
+ breadcrumbItem: css9`
3567
3723
  color: ${colors.textSecondary};
3568
3724
  text-decoration: none;
3569
3725
  cursor: pointer;
@@ -3574,19 +3730,19 @@ var styles8 = {
3574
3730
  color: ${colors.primary};
3575
3731
  }
3576
3732
  `,
3577
- breadcrumbCurrent: css8`
3733
+ breadcrumbCurrent: css9`
3578
3734
  color: ${colors.text};
3579
3735
  font-weight: 500;
3580
3736
  white-space: nowrap;
3581
3737
  overflow: hidden;
3582
3738
  text-overflow: ellipsis;
3583
3739
  `,
3584
- headerActions: css8`
3740
+ headerActions: css9`
3585
3741
  display: flex;
3586
3742
  align-items: center;
3587
3743
  gap: 8px;
3588
3744
  `,
3589
- headerBtn: css8`
3745
+ headerBtn: css9`
3590
3746
  height: ${btnHeight3};
3591
3747
  padding: 0 12px;
3592
3748
  background: ${colors.surface};
@@ -3603,23 +3759,23 @@ var styles8 = {
3603
3759
  border-color: ${colors.borderHover};
3604
3760
  }
3605
3761
  `,
3606
- headerIcon: css8`
3762
+ headerIcon: css9`
3607
3763
  width: 16px;
3608
3764
  height: 16px;
3609
3765
  color: ${colors.textSecondary};
3610
3766
  `,
3611
- content: css8`
3767
+ content: css9`
3612
3768
  flex: 1;
3613
3769
  display: flex;
3614
3770
  overflow: hidden;
3615
3771
  `,
3616
- fileBrowser: css8`
3772
+ fileBrowser: css9`
3617
3773
  flex: 1;
3618
3774
  min-width: 0;
3619
3775
  overflow: auto;
3620
3776
  padding: 20px 24px;
3621
3777
  `,
3622
- dropOverlay: css8`
3778
+ dropOverlay: css9`
3623
3779
  position: absolute;
3624
3780
  top: 0;
3625
3781
  left: 0;
@@ -3634,7 +3790,7 @@ var styles8 = {
3634
3790
  z-index: 50;
3635
3791
  pointer-events: none;
3636
3792
  `,
3637
- dropMessage: css8`
3793
+ dropMessage: css9`
3638
3794
  display: flex;
3639
3795
  flex-direction: column;
3640
3796
  align-items: center;
@@ -3643,36 +3799,43 @@ var styles8 = {
3643
3799
  font-size: ${fontSize.lg};
3644
3800
  font-weight: 600;
3645
3801
  `,
3646
- dropIcon: css8`
3802
+ dropIcon: css9`
3647
3803
  width: 48px;
3648
3804
  height: 48px;
3649
3805
  `
3650
3806
  };
3651
3807
  function StudioUI({ onClose, isVisible = true }) {
3652
- const [currentPath, setCurrentPathInternal] = useState7("public");
3653
- const [selectedItems, setSelectedItems] = useState7(/* @__PURE__ */ new Set());
3654
- const [lastSelectedPath, setLastSelectedPath] = useState7(null);
3655
- const [viewMode, setViewMode] = useState7("grid");
3656
- const [focusedItem, setFocusedItem] = useState7(null);
3657
- const [meta, setMeta] = useState7(null);
3658
- const [isLoading, setIsLoading] = useState7(false);
3659
- const [refreshKey, setRefreshKey] = useState7(0);
3660
- const [searchQuery, setSearchQuery] = useState7("");
3661
- const [isDragging, setIsDragging] = useState7(false);
3662
- 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(() => {
3663
3820
  setRefreshKey((k) => k + 1);
3664
3821
  }, []);
3665
- 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) => {
3666
3829
  e.preventDefault();
3667
3830
  e.stopPropagation();
3668
3831
  setIsDragging(true);
3669
3832
  }, []);
3670
- const handleDragLeave = useCallback2((e) => {
3833
+ const handleDragLeave = useCallback3((e) => {
3671
3834
  e.preventDefault();
3672
3835
  e.stopPropagation();
3673
3836
  setIsDragging(false);
3674
3837
  }, []);
3675
- const handleDrop = useCallback2(async (e) => {
3838
+ const handleDrop = useCallback3(async (e) => {
3676
3839
  e.preventDefault();
3677
3840
  e.stopPropagation();
3678
3841
  setIsDragging(false);
@@ -3690,25 +3853,25 @@ function StudioUI({ onClose, isVisible = true }) {
3690
3853
  method: "POST",
3691
3854
  body: formData
3692
3855
  });
3693
- } catch (error) {
3694
- console.error("Upload error:", error);
3856
+ } catch (error2) {
3857
+ console.error("Upload error:", error2);
3695
3858
  }
3696
3859
  }
3697
3860
  triggerRefresh();
3698
3861
  }, [currentPath, triggerRefresh]);
3699
- const navigateUp = useCallback2(() => {
3862
+ const navigateUp = useCallback3(() => {
3700
3863
  if (currentPath === "public") return;
3701
3864
  const parts = currentPath.split("/");
3702
3865
  parts.pop();
3703
3866
  setCurrentPathInternal(parts.join("/") || "public");
3704
3867
  setSelectedItems(/* @__PURE__ */ new Set());
3705
3868
  }, [currentPath]);
3706
- const setCurrentPath = useCallback2((path) => {
3869
+ const setCurrentPath = useCallback3((path) => {
3707
3870
  setCurrentPathInternal(path);
3708
3871
  setSelectedItems(/* @__PURE__ */ new Set());
3709
3872
  setFocusedItem(null);
3710
3873
  }, []);
3711
- const toggleSelection = useCallback2((path) => {
3874
+ const toggleSelection = useCallback3((path) => {
3712
3875
  setSelectedItems((prev) => {
3713
3876
  const next = new Set(prev);
3714
3877
  if (next.has(path)) {
@@ -3720,7 +3883,7 @@ function StudioUI({ onClose, isVisible = true }) {
3720
3883
  });
3721
3884
  setLastSelectedPath(path);
3722
3885
  }, []);
3723
- const selectRange = useCallback2((fromPath, toPath, allItems) => {
3886
+ const selectRange = useCallback3((fromPath, toPath, allItems) => {
3724
3887
  const fromIndex = allItems.findIndex((item) => item.path === fromPath);
3725
3888
  const toIndex = allItems.findIndex((item) => item.path === toPath);
3726
3889
  if (fromIndex === -1 || toIndex === -1) return;
@@ -3735,13 +3898,13 @@ function StudioUI({ onClose, isVisible = true }) {
3735
3898
  });
3736
3899
  setLastSelectedPath(toPath);
3737
3900
  }, []);
3738
- const selectAll = useCallback2((items) => {
3901
+ const selectAll = useCallback3((items) => {
3739
3902
  setSelectedItems(new Set(items.map((item) => item.path)));
3740
3903
  }, []);
3741
- const clearSelection = useCallback2(() => {
3904
+ const clearSelection = useCallback3(() => {
3742
3905
  setSelectedItems(/* @__PURE__ */ new Set());
3743
3906
  }, []);
3744
- const handleKeyDown = useCallback2(
3907
+ const handleKeyDown = useCallback3(
3745
3908
  (e) => {
3746
3909
  if (e.key === "Escape") {
3747
3910
  const target = e.target;
@@ -3757,7 +3920,7 @@ function StudioUI({ onClose, isVisible = true }) {
3757
3920
  },
3758
3921
  [onClose, focusedItem]
3759
3922
  );
3760
- useEffect4(() => {
3923
+ useEffect3(() => {
3761
3924
  if (isVisible) {
3762
3925
  document.addEventListener("keydown", handleKeyDown);
3763
3926
  document.body.style.overflow = "hidden";
@@ -3793,43 +3956,47 @@ function StudioUI({ onClose, isVisible = true }) {
3793
3956
  refreshKey,
3794
3957
  triggerRefresh,
3795
3958
  searchQuery,
3796
- setSearchQuery
3959
+ setSearchQuery,
3960
+ error,
3961
+ showError,
3962
+ clearError
3797
3963
  };
3798
- return /* @__PURE__ */ jsx8(StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs8("div", { css: styles8.container, children: [
3799
- /* @__PURE__ */ jsxs8("div", { css: styles8.header, children: [
3800
- /* @__PURE__ */ jsx8("div", { css: styles8.headerLeft, children: /* @__PURE__ */ jsx8("h1", { css: styles8.title, children: "Studio" }) }),
3801
- /* @__PURE__ */ jsx8("div", { css: styles8.headerCenter, children: /* @__PURE__ */ jsx8(Breadcrumbs, { currentPath, onNavigate: setCurrentPath }) }),
3802
- /* @__PURE__ */ jsxs8("div", { css: styles8.headerActions, children: [
3803
- /* @__PURE__ */ jsx8(StudioSettings, {}),
3804
- /* @__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(
3805
3971
  "button",
3806
3972
  {
3807
- css: styles8.headerBtn,
3973
+ css: styles9.headerBtn,
3808
3974
  onClick: onClose,
3809
3975
  "aria-label": "Close Studio",
3810
- children: /* @__PURE__ */ jsx8(CloseIcon, {})
3976
+ children: /* @__PURE__ */ jsx9(CloseIcon, {})
3811
3977
  }
3812
3978
  )
3813
3979
  ] })
3814
3980
  ] }),
3815
- /* @__PURE__ */ jsx8(StudioToolbar, {}),
3816
- /* @__PURE__ */ jsxs8(
3981
+ /* @__PURE__ */ jsx9(StudioToolbar, {}),
3982
+ /* @__PURE__ */ jsxs9(
3817
3983
  "div",
3818
3984
  {
3819
- css: styles8.content,
3985
+ css: styles9.content,
3820
3986
  onDragOver: handleDragOver,
3821
3987
  onDragLeave: handleDragLeave,
3822
3988
  onDrop: handleDrop,
3823
3989
  children: [
3824
- isDragging && /* @__PURE__ */ jsx8("div", { css: styles8.dropOverlay, children: /* @__PURE__ */ jsxs8("div", { css: styles8.dropMessage, children: [
3825
- /* @__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" }) }),
3826
- /* @__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" })
3827
3993
  ] }) }),
3828
- /* @__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, {}) })
3829
3995
  ]
3830
3996
  }
3831
3997
  ),
3832
- focusedItem && /* @__PURE__ */ jsx8(StudioDetailView, {})
3998
+ focusedItem && /* @__PURE__ */ jsx9(StudioDetailView, {}),
3999
+ /* @__PURE__ */ jsx9(ErrorModal, {})
3833
4000
  ] }) });
3834
4001
  }
3835
4002
  function Breadcrumbs({ currentPath, onNavigate }) {
@@ -3838,12 +4005,12 @@ function Breadcrumbs({ currentPath, onNavigate }) {
3838
4005
  name: part,
3839
4006
  path: parts.slice(0, index + 1).join("/")
3840
4007
  }));
3841
- return /* @__PURE__ */ jsx8("div", { css: styles8.breadcrumbs, children: breadcrumbs.map((crumb, index) => /* @__PURE__ */ jsxs8("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3842
- index > 0 && /* @__PURE__ */ jsx8("span", { css: styles8.breadcrumbSeparator, children: "/" }),
3843
- 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(
3844
4011
  "span",
3845
4012
  {
3846
- css: styles8.breadcrumbItem,
4013
+ css: styles9.breadcrumbItem,
3847
4014
  onClick: () => onNavigate(crumb.path),
3848
4015
  children: crumb.name
3849
4016
  }
@@ -3851,10 +4018,10 @@ function Breadcrumbs({ currentPath, onNavigate }) {
3851
4018
  ] }, crumb.path)) });
3852
4019
  }
3853
4020
  function CloseIcon() {
3854
- return /* @__PURE__ */ jsxs8(
4021
+ return /* @__PURE__ */ jsxs9(
3855
4022
  "svg",
3856
4023
  {
3857
- css: styles8.headerIcon,
4024
+ css: styles9.headerIcon,
3858
4025
  xmlns: "http://www.w3.org/2000/svg",
3859
4026
  viewBox: "0 0 24 24",
3860
4027
  fill: "none",
@@ -3863,8 +4030,8 @@ function CloseIcon() {
3863
4030
  strokeLinecap: "round",
3864
4031
  strokeLinejoin: "round",
3865
4032
  children: [
3866
- /* @__PURE__ */ jsx8("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
3867
- /* @__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" })
3868
4035
  ]
3869
4036
  }
3870
4037
  );
@@ -3874,4 +4041,4 @@ export {
3874
4041
  StudioUI,
3875
4042
  StudioUI_default as default
3876
4043
  };
3877
- //# sourceMappingURL=StudioUI-SS3YMS53.mjs.map
4044
+ //# sourceMappingURL=StudioUI-VJVOSOPD.mjs.map