@gallop.software/studio 0.1.88 → 0.1.90

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.
@@ -50,6 +50,11 @@ var defaultState = {
50
50
  refreshKey: 0,
51
51
  triggerRefresh: () => {
52
52
  },
53
+ scanRequested: false,
54
+ triggerScan: () => {
55
+ },
56
+ clearScanRequest: () => {
57
+ },
53
58
  searchQuery: "",
54
59
  setSearchQuery: () => {
55
60
  },
@@ -1285,11 +1290,11 @@ var styles4 = {
1285
1290
  `
1286
1291
  };
1287
1292
  function StudioToolbar() {
1288
- const { selectedItems, viewMode, setViewMode, clearSelection, currentPath, triggerRefresh, focusedItem } = useStudio();
1293
+ const { selectedItems, viewMode, setViewMode, clearSelection, currentPath, triggerRefresh, focusedItem, scanRequested, clearScanRequest } = useStudio();
1289
1294
  const fileInputRef = _react.useRef.call(void 0, null);
1290
1295
  const abortControllerRef = _react.useRef.call(void 0, null);
1291
1296
  const [uploading, setUploading] = _react.useState.call(void 0, false);
1292
- const [refreshing, setRefreshing] = _react.useState.call(void 0, false);
1297
+ const [scanning, setScanning] = _react.useState.call(void 0, false);
1293
1298
  const [processing, setProcessing] = _react.useState.call(void 0, false);
1294
1299
  const [showDeleteConfirm, setShowDeleteConfirm] = _react.useState.call(void 0, false);
1295
1300
  const [showProcessConfirm, setShowProcessConfirm] = _react.useState.call(void 0, false);
@@ -1315,11 +1320,88 @@ function StudioToolbar() {
1315
1320
  const handleUpload = _react.useCallback.call(void 0, () => {
1316
1321
  _optionalChain([fileInputRef, 'access', _2 => _2.current, 'optionalAccess', _3 => _3.click, 'call', _4 => _4()]);
1317
1322
  }, []);
1318
- const handleRefresh = _react.useCallback.call(void 0, () => {
1319
- setRefreshing(true);
1320
- triggerRefresh();
1321
- setTimeout(() => setRefreshing(false), 600);
1323
+ const handleScan = _react.useCallback.call(void 0, async () => {
1324
+ setScanning(true);
1325
+ setShowProgress(true);
1326
+ setProgressState({
1327
+ current: 0,
1328
+ total: 0,
1329
+ percent: 0,
1330
+ status: "processing",
1331
+ message: "Scanning for files..."
1332
+ });
1333
+ try {
1334
+ const response = await fetch("/api/studio/scan", { method: "POST" });
1335
+ const reader = _optionalChain([response, 'access', _5 => _5.body, 'optionalAccess', _6 => _6.getReader, 'call', _7 => _7()]);
1336
+ if (!reader) throw new Error("No reader");
1337
+ const decoder = new TextDecoder();
1338
+ let buffer = "";
1339
+ while (true) {
1340
+ const { done, value } = await reader.read();
1341
+ if (done) break;
1342
+ buffer += decoder.decode(value, { stream: true });
1343
+ const lines = buffer.split("\n\n");
1344
+ buffer = lines.pop() || "";
1345
+ for (const line of lines) {
1346
+ if (!line.startsWith("data: ")) continue;
1347
+ const data = JSON.parse(line.slice(6));
1348
+ if (data.type === "start") {
1349
+ setProgressState({
1350
+ current: 0,
1351
+ total: data.total,
1352
+ percent: 0,
1353
+ status: "processing",
1354
+ message: `Scanning ${data.total} files...`
1355
+ });
1356
+ } else if (data.type === "progress") {
1357
+ setProgressState({
1358
+ current: data.current,
1359
+ total: data.total,
1360
+ percent: data.percent,
1361
+ status: "processing",
1362
+ currentFile: data.currentFile
1363
+ });
1364
+ } else if (data.type === "complete") {
1365
+ setProgressState({
1366
+ current: data.total || 0,
1367
+ total: data.total || 0,
1368
+ percent: 100,
1369
+ status: "complete",
1370
+ processed: data.added,
1371
+ errors: data.errors,
1372
+ message: data.renamed > 0 ? `${data.renamed} file(s) renamed due to conflicts` : void 0
1373
+ });
1374
+ triggerRefresh();
1375
+ } else if (data.type === "error") {
1376
+ setProgressState({
1377
+ current: 0,
1378
+ total: 0,
1379
+ percent: 0,
1380
+ status: "error",
1381
+ message: data.message || "Scan failed"
1382
+ });
1383
+ }
1384
+ }
1385
+ }
1386
+ } catch (error) {
1387
+ console.error("Scan error:", error);
1388
+ setProgressState({
1389
+ current: 0,
1390
+ total: 0,
1391
+ percent: 0,
1392
+ status: "error",
1393
+ message: "Scan failed"
1394
+ });
1395
+ } finally {
1396
+ setScanning(false);
1397
+ }
1322
1398
  }, [triggerRefresh]);
1399
+ _react.useEffect.call(void 0, () => {
1400
+ if (scanRequested && !scanning) {
1401
+ clearScanRequest();
1402
+ handleScan();
1403
+ }
1404
+ }, [scanRequested, scanning, clearScanRequest, handleScan]);
1323
1405
  const handleFileChange = _react.useCallback.call(void 0, async (e) => {
1324
1406
  const files = e.target.files;
1325
1407
  if (!files || files.length === 0) return;
@@ -1422,7 +1504,7 @@ function StudioToolbar() {
1422
1504
  const selectedPaths2 = Array.from(selectedItems);
1423
1505
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1424
1506
  const selectedImagePaths = selectedPaths2.filter((p) => {
1425
- const ext = _optionalChain([p, 'access', _5 => _5.split, 'call', _6 => _6("."), 'access', _7 => _7.pop, 'call', _8 => _8(), 'optionalAccess', _9 => _9.toLowerCase, 'call', _10 => _10()]) || "";
1507
+ const ext = _optionalChain([p, 'access', _8 => _8.split, 'call', _9 => _9("."), 'access', _10 => _10.pop, 'call', _11 => _11(), 'optionalAccess', _12 => _12.toLowerCase, 'call', _13 => _13()]) || "";
1426
1508
  return imageExtensions.includes(ext);
1427
1509
  });
1428
1510
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1583,12 +1665,12 @@ function StudioToolbar() {
1583
1665
  const data = await response.json();
1584
1666
  if (response.ok) {
1585
1667
  setProgressState({
1586
- current: _optionalChain([data, 'access', _11 => _11.processed, 'optionalAccess', _12 => _12.length]) || 0,
1587
- total: _optionalChain([data, 'access', _13 => _13.processed, 'optionalAccess', _14 => _14.length]) || 0,
1668
+ current: _optionalChain([data, 'access', _14 => _14.processed, 'optionalAccess', _15 => _15.length]) || 0,
1669
+ total: _optionalChain([data, 'access', _16 => _16.processed, 'optionalAccess', _17 => _17.length]) || 0,
1588
1670
  percent: 100,
1589
1671
  status: "complete",
1590
- processed: _optionalChain([data, 'access', _15 => _15.processed, 'optionalAccess', _16 => _16.length]) || 0,
1591
- errors: _optionalChain([data, 'access', _17 => _17.errors, 'optionalAccess', _18 => _18.length]) || 0
1672
+ processed: _optionalChain([data, 'access', _18 => _18.processed, 'optionalAccess', _19 => _19.length]) || 0,
1673
+ errors: _optionalChain([data, 'access', _20 => _20.errors, 'optionalAccess', _21 => _21.length]) || 0
1592
1674
  });
1593
1675
  clearSelection();
1594
1676
  triggerRefresh();
@@ -1665,7 +1747,7 @@ function StudioToolbar() {
1665
1747
  const selectedPaths2 = Array.from(selectedItems);
1666
1748
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1667
1749
  const selectedImagePaths = selectedPaths2.filter((p) => {
1668
- const ext = _optionalChain([p, 'access', _19 => _19.split, 'call', _20 => _20("."), 'access', _21 => _21.pop, 'call', _22 => _22(), 'optionalAccess', _23 => _23.toLowerCase, 'call', _24 => _24()]) || "";
1750
+ const ext = _optionalChain([p, 'access', _22 => _22.split, 'call', _23 => _23("."), 'access', _24 => _24.pop, 'call', _25 => _25(), 'optionalAccess', _26 => _26.toLowerCase, 'call', _27 => _27()]) || "";
1669
1751
  return imageExtensions.includes(ext);
1670
1752
  });
1671
1753
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1700,7 +1782,7 @@ function StudioToolbar() {
1700
1782
  const selectedPaths2 = Array.from(selectedItems);
1701
1783
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1702
1784
  const selectedImagePaths = selectedPaths2.filter((p) => {
1703
- const ext = _optionalChain([p, 'access', _25 => _25.split, 'call', _26 => _26("."), 'access', _27 => _27.pop, 'call', _28 => _28(), 'optionalAccess', _29 => _29.toLowerCase, 'call', _30 => _30()]) || "";
1785
+ const ext = _optionalChain([p, 'access', _28 => _28.split, 'call', _29 => _29("."), 'access', _30 => _30.pop, 'call', _31 => _31(), 'optionalAccess', _32 => _32.toLowerCase, 'call', _33 => _33()]) || "";
1704
1786
  return imageExtensions.includes(ext);
1705
1787
  });
1706
1788
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1750,16 +1832,16 @@ function StudioToolbar() {
1750
1832
  });
1751
1833
  const data = await response.json();
1752
1834
  if (!response.ok) {
1753
- if (_optionalChain([data, 'access', _31 => _31.error, 'optionalAccess', _32 => _32.includes, 'call', _33 => _33("R2 not configured")]) || _optionalChain([data, 'access', _34 => _34.error, 'optionalAccess', _35 => _35.includes, 'call', _36 => _36("CLOUDFLARE_R2")])) {
1835
+ if (_optionalChain([data, 'access', _34 => _34.error, 'optionalAccess', _35 => _35.includes, 'call', _36 => _36("R2 not configured")]) || _optionalChain([data, 'access', _37 => _37.error, 'optionalAccess', _38 => _38.includes, 'call', _39 => _39("CLOUDFLARE_R2")])) {
1754
1836
  setShowProgress(false);
1755
1837
  setShowR2SetupModal(true);
1756
1838
  return;
1757
1839
  }
1758
1840
  errors++;
1759
1841
  errorMessages.push(data.error || `Failed: ${imageKey}`);
1760
- } else if (_optionalChain([data, 'access', _37 => _37.synced, 'optionalAccess', _38 => _38.length]) > 0) {
1842
+ } else if (_optionalChain([data, 'access', _40 => _40.synced, 'optionalAccess', _41 => _41.length]) > 0) {
1761
1843
  synced++;
1762
- } else if (_optionalChain([data, 'access', _39 => _39.errors, 'optionalAccess', _40 => _40.length]) > 0) {
1844
+ } else if (_optionalChain([data, 'access', _42 => _42.errors, 'optionalAccess', _43 => _43.length]) > 0) {
1763
1845
  errors++;
1764
1846
  for (const errMsg of data.errors) {
1765
1847
  errorMessages.push(errMsg);
@@ -1903,8 +1985,8 @@ function StudioToolbar() {
1903
1985
  showSyncConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
1904
1986
  ConfirmModal,
1905
1987
  {
1906
- title: "Sync to CDN",
1907
- message: `Sync ${syncImageCount} image${syncImageCount !== 1 ? "s" : ""} to Cloudflare R2? Images must be processed first. After syncing, local thumbnails will be deleted.`,
1988
+ title: "Push to CDN",
1989
+ message: `Push ${syncImageCount} image${syncImageCount !== 1 ? "s" : ""} to Cloudflare R2? Images must be processed first. After pushing, local files will be deleted.`,
1908
1990
  confirmLabel: "Sync",
1909
1991
  onConfirm: handleSyncConfirm,
1910
1992
  onCancel: () => setShowSyncConfirm(false)
@@ -2071,7 +2153,7 @@ function StudioToolbar() {
2071
2153
  disabled: !hasSelection,
2072
2154
  children: [
2073
2155
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, CloudIcon, {}),
2074
- "Sync CDN"
2156
+ "Push CDN"
2075
2157
  ]
2076
2158
  }
2077
2159
  ),
@@ -2104,12 +2186,16 @@ function StudioToolbar() {
2104
2186
  " selected",
2105
2187
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: styles4.clearBtn, onClick: clearSelection, children: "Clear" })
2106
2188
  ] }),
2107
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
2189
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
2108
2190
  "button",
2109
2191
  {
2110
- css: [styles4.btn, styles4.btnIconOnly],
2111
- onClick: handleRefresh,
2112
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, RefreshIcon, { spinning: refreshing })
2192
+ css: styles4.btn,
2193
+ onClick: handleScan,
2194
+ disabled: scanning,
2195
+ children: [
2196
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ScanIcon, { spinning: scanning }),
2197
+ "Scan"
2198
+ ]
2113
2199
  }
2114
2200
  ),
2115
2201
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles4.viewToggle, children: [
@@ -2139,7 +2225,7 @@ function StudioToolbar() {
2139
2225
  function UploadIcon() {
2140
2226
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles4.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "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" }) });
2141
2227
  }
2142
- function RefreshIcon({ spinning }) {
2228
+ function ScanIcon({ spinning }) {
2143
2229
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: [styles4.icon, spinning && styles4.iconSpin], fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) });
2144
2230
  }
2145
2231
  function TrashIcon() {
@@ -2268,11 +2354,13 @@ function useFileList() {
2268
2354
  refreshKey,
2269
2355
  setFocusedItem,
2270
2356
  triggerRefresh,
2357
+ triggerScan,
2271
2358
  searchQuery,
2272
2359
  showError
2273
2360
  } = useStudio();
2274
2361
  const [items, setItems] = _react.useState.call(void 0, []);
2275
2362
  const [loading, setLoading] = _react.useState.call(void 0, true);
2363
+ const [metaEmpty, setMetaEmpty] = _react.useState.call(void 0, false);
2276
2364
  const isInitialLoad = _react.useRef.call(void 0, true);
2277
2365
  const lastPath = _react.useRef.call(void 0, currentPath);
2278
2366
  _react.useEffect.call(void 0, () => {
@@ -2285,10 +2373,12 @@ function useFileList() {
2285
2373
  try {
2286
2374
  const data = searchQuery && searchQuery.length >= 2 ? await studioApi.search(searchQuery) : await studioApi.list(currentPath);
2287
2375
  setItems(data.items || []);
2376
+ setMetaEmpty(data.isEmpty === true);
2288
2377
  } catch (error) {
2289
2378
  const message = error instanceof Error ? error.message : "Failed to load items";
2290
2379
  showError("Load Error", message);
2291
2380
  setItems([]);
2381
+ setMetaEmpty(false);
2292
2382
  }
2293
2383
  setLoading(false);
2294
2384
  isInitialLoad.current = false;
@@ -2340,6 +2430,7 @@ function useFileList() {
2340
2430
  items,
2341
2431
  loading,
2342
2432
  sortedItems,
2433
+ metaEmpty,
2343
2434
  // Computed
2344
2435
  isAtRoot,
2345
2436
  isSearching,
@@ -2353,7 +2444,8 @@ function useFileList() {
2353
2444
  handleItemClick,
2354
2445
  handleOpen,
2355
2446
  handleGenerateThumbnail,
2356
- handleSelectAll
2447
+ handleSelectAll,
2448
+ triggerScan
2357
2449
  };
2358
2450
  }
2359
2451
 
@@ -2367,7 +2459,8 @@ var styles5 = {
2367
2459
  display: flex;
2368
2460
  align-items: center;
2369
2461
  justify-content: center;
2370
- height: 256px;
2462
+ flex: 1;
2463
+ min-height: 300px;
2371
2464
  `,
2372
2465
  spinner: _react3.css`
2373
2466
  width: 32px;
@@ -2382,7 +2475,8 @@ var styles5 = {
2382
2475
  flex-direction: column;
2383
2476
  align-items: center;
2384
2477
  justify-content: center;
2385
- height: 256px;
2478
+ flex: 1;
2479
+ min-height: 300px;
2386
2480
  color: ${_chunkUFCWGUAGjs.colors.textSecondary};
2387
2481
  `,
2388
2482
  emptyIcon: _react3.css`
@@ -2400,6 +2494,27 @@ var styles5 = {
2400
2494
  font-size: ${_chunkUFCWGUAGjs.fontSize.sm};
2401
2495
  }
2402
2496
  `,
2497
+ scanButton: _react3.css`
2498
+ margin-top: 16px;
2499
+ padding: 10px 24px;
2500
+ font-size: ${_chunkUFCWGUAGjs.fontSize.base};
2501
+ font-weight: 500;
2502
+ background: ${_chunkUFCWGUAGjs.colors.primary};
2503
+ color: white;
2504
+ border: none;
2505
+ border-radius: 8px;
2506
+ cursor: pointer;
2507
+ transition: background 0.15s ease;
2508
+
2509
+ &:hover:not(:disabled) {
2510
+ background: ${_chunkUFCWGUAGjs.colors.primaryHover};
2511
+ }
2512
+
2513
+ &:disabled {
2514
+ opacity: 0.6;
2515
+ cursor: not-allowed;
2516
+ }
2517
+ `,
2403
2518
  grid: _react3.css`
2404
2519
  display: grid;
2405
2520
  grid-template-columns: 1fr;
@@ -2683,6 +2798,7 @@ function StudioFileGrid() {
2683
2798
  const {
2684
2799
  loading,
2685
2800
  sortedItems,
2801
+ metaEmpty,
2686
2802
  isAtRoot,
2687
2803
  isSearching,
2688
2804
  allItemsSelected,
@@ -2692,16 +2808,32 @@ function StudioFileGrid() {
2692
2808
  handleItemClick,
2693
2809
  handleOpen,
2694
2810
  handleGenerateThumbnail,
2695
- handleSelectAll
2811
+ handleSelectAll,
2812
+ triggerScan
2696
2813
  } = useFileList();
2697
2814
  if (loading) {
2698
2815
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.loading, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.spinner }) });
2699
2816
  }
2817
+ if (metaEmpty && isAtRoot) {
2818
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.empty, children: [
2819
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles5.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }),
2820
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "No files tracked yet" }),
2821
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Click Scan to discover files in your public folder" }),
2822
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
2823
+ "button",
2824
+ {
2825
+ css: styles5.scanButton,
2826
+ onClick: triggerScan,
2827
+ children: "Scan for Files"
2828
+ }
2829
+ )
2830
+ ] });
2831
+ }
2700
2832
  if (sortedItems.length === 0 && isAtRoot) {
2701
2833
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.empty, children: [
2702
2834
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles5.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "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" }) }),
2703
2835
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "No files in this folder" }),
2704
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Upload images to get started" })
2836
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Upload images or click Scan in the toolbar" })
2705
2837
  ] });
2706
2838
  }
2707
2839
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { children: [
@@ -2880,7 +3012,8 @@ var styles6 = {
2880
3012
  display: flex;
2881
3013
  align-items: center;
2882
3014
  justify-content: center;
2883
- height: 256px;
3015
+ flex: 1;
3016
+ min-height: 300px;
2884
3017
  `,
2885
3018
  spinner: _react3.css`
2886
3019
  width: 32px;
@@ -2895,9 +3028,36 @@ var styles6 = {
2895
3028
  flex-direction: column;
2896
3029
  align-items: center;
2897
3030
  justify-content: center;
2898
- height: 256px;
3031
+ flex: 1;
3032
+ min-height: 300px;
2899
3033
  color: ${_chunkUFCWGUAGjs.colors.textSecondary};
2900
3034
  `,
3035
+ emptyHint: _react3.css`
3036
+ font-size: ${_chunkUFCWGUAGjs.fontSize.sm};
3037
+ color: ${_chunkUFCWGUAGjs.colors.textMuted};
3038
+ margin-top: 4px;
3039
+ `,
3040
+ scanButton: _react3.css`
3041
+ margin-top: 16px;
3042
+ padding: 10px 24px;
3043
+ font-size: ${_chunkUFCWGUAGjs.fontSize.base};
3044
+ font-weight: 500;
3045
+ background: ${_chunkUFCWGUAGjs.colors.primary};
3046
+ color: white;
3047
+ border: none;
3048
+ border-radius: 8px;
3049
+ cursor: pointer;
3050
+ transition: background 0.15s ease;
3051
+
3052
+ &:hover:not(:disabled) {
3053
+ background: ${_chunkUFCWGUAGjs.colors.primaryHover};
3054
+ }
3055
+
3056
+ &:disabled {
3057
+ opacity: 0.6;
3058
+ cursor: not-allowed;
3059
+ }
3060
+ `,
2901
3061
  tableWrapper: _react3.css`
2902
3062
  background: ${_chunkUFCWGUAGjs.colors.surface};
2903
3063
  border-radius: 8px;
@@ -3184,6 +3344,7 @@ function StudioFileList() {
3184
3344
  const {
3185
3345
  loading,
3186
3346
  sortedItems,
3347
+ metaEmpty,
3187
3348
  isAtRoot,
3188
3349
  isSearching,
3189
3350
  allItemsSelected,
@@ -3193,13 +3354,31 @@ function StudioFileList() {
3193
3354
  handleItemClick,
3194
3355
  handleOpen,
3195
3356
  handleGenerateThumbnail,
3196
- handleSelectAll
3357
+ handleSelectAll,
3358
+ triggerScan
3197
3359
  } = useFileList();
3198
3360
  if (loading) {
3199
3361
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.loading, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.spinner }) });
3200
3362
  }
3363
+ if (metaEmpty && isAtRoot) {
3364
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles6.empty, children: [
3365
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children: "No files tracked yet" }),
3366
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles6.emptyHint, children: "Click Scan to discover files in your public folder" }),
3367
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
3368
+ "button",
3369
+ {
3370
+ css: styles6.scanButton,
3371
+ onClick: triggerScan,
3372
+ children: "Scan for Files"
3373
+ }
3374
+ )
3375
+ ] });
3376
+ }
3201
3377
  if (sortedItems.length === 0 && isAtRoot) {
3202
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.empty, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children: "No files in this folder" }) });
3378
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles6.empty, children: [
3379
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children: "No files in this folder" }),
3380
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles6.emptyHint, children: "Upload images or click Scan in the toolbar" })
3381
+ ] });
3203
3382
  }
3204
3383
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.tableWrapper, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "table", { css: styles6.table, children: [
3205
3384
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "thead", { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { children: [
@@ -3709,7 +3888,7 @@ function StudioDetailView() {
3709
3888
  });
3710
3889
  triggerRefresh();
3711
3890
  } else {
3712
- if (_optionalChain([data, 'access', _41 => _41.error, 'optionalAccess', _42 => _42.includes, 'call', _43 => _43("R2 not configured")]) || _optionalChain([data, 'access', _44 => _44.error, 'optionalAccess', _45 => _45.includes, 'call', _46 => _46("CLOUDFLARE_R2")])) {
3891
+ if (_optionalChain([data, 'access', _44 => _44.error, 'optionalAccess', _45 => _45.includes, 'call', _46 => _46("R2 not configured")]) || _optionalChain([data, 'access', _47 => _47.error, 'optionalAccess', _48 => _48.includes, 'call', _49 => _49("CLOUDFLARE_R2")])) {
3713
3892
  setShowR2SetupModal(true);
3714
3893
  } else {
3715
3894
  setAlertMessage({
@@ -3748,7 +3927,7 @@ function StudioDetailView() {
3748
3927
  if (!response.ok) {
3749
3928
  throw new Error("Processing failed");
3750
3929
  }
3751
- const reader = _optionalChain([response, 'access', _47 => _47.body, 'optionalAccess', _48 => _48.getReader, 'call', _49 => _49()]);
3930
+ const reader = _optionalChain([response, 'access', _50 => _50.body, 'optionalAccess', _51 => _51.getReader, 'call', _52 => _52()]);
3752
3931
  if (!reader) {
3753
3932
  throw new Error("No response body");
3754
3933
  }
@@ -3898,7 +4077,7 @@ function StudioDetailView() {
3898
4077
  ] }),
3899
4078
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { css: styles7.actionBtn, onClick: handleSync, disabled: syncing, children: [
3900
4079
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles7.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" }) }),
3901
- syncing ? "Syncing..." : "Sync to CDN"
4080
+ syncing ? "Pushing..." : "Push to CDN"
3902
4081
  ] }),
3903
4082
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { css: styles7.actionBtn, onClick: () => setShowProcessConfirm(true), children: [
3904
4083
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles7.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, 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" }) }),
@@ -4428,6 +4607,8 @@ var styles10 = {
4428
4607
  min-width: 0;
4429
4608
  overflow: auto;
4430
4609
  padding: 20px 24px;
4610
+ display: flex;
4611
+ flex-direction: column;
4431
4612
  `,
4432
4613
  dropOverlay: _react3.css`
4433
4614
  position: absolute;
@@ -4467,12 +4648,19 @@ function StudioUI({ onClose, isVisible = true }) {
4467
4648
  const [meta, setMeta] = _react.useState.call(void 0, null);
4468
4649
  const [isLoading, setIsLoading] = _react.useState.call(void 0, false);
4469
4650
  const [refreshKey, setRefreshKey] = _react.useState.call(void 0, 0);
4651
+ const [scanRequested, setScanRequested] = _react.useState.call(void 0, false);
4470
4652
  const [searchQuery, setSearchQuery] = _react.useState.call(void 0, "");
4471
4653
  const [error, setError] = _react.useState.call(void 0, null);
4472
4654
  const [isDragging, setIsDragging] = _react.useState.call(void 0, false);
4473
4655
  const triggerRefresh = _react.useCallback.call(void 0, () => {
4474
4656
  setRefreshKey((k) => k + 1);
4475
4657
  }, []);
4658
+ const triggerScan = _react.useCallback.call(void 0, () => {
4659
+ setScanRequested(true);
4660
+ }, []);
4661
+ const clearScanRequest = _react.useCallback.call(void 0, () => {
4662
+ setScanRequested(false);
4663
+ }, []);
4476
4664
  const showError = _react.useCallback.call(void 0, (title, message) => {
4477
4665
  setError({ title, message });
4478
4666
  }, []);
@@ -4609,6 +4797,9 @@ function StudioUI({ onClose, isVisible = true }) {
4609
4797
  setIsLoading,
4610
4798
  refreshKey,
4611
4799
  triggerRefresh,
4800
+ scanRequested,
4801
+ triggerScan,
4802
+ clearScanRequest,
4612
4803
  searchQuery,
4613
4804
  setSearchQuery,
4614
4805
  error,
@@ -4695,4 +4886,4 @@ var StudioUI_default = StudioUI;
4695
4886
 
4696
4887
 
4697
4888
  exports.StudioUI = StudioUI; exports.default = StudioUI_default;
4698
- //# sourceMappingURL=StudioUI-Y35A2T7S.js.map
4889
+ //# sourceMappingURL=StudioUI-HWUO2H6J.js.map