@gallop.software/studio 0.1.87 → 0.1.89

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.
@@ -310,6 +310,23 @@ var progressStyles = {
310
310
  white-space: nowrap;
311
311
  overflow: hidden;
312
312
  text-overflow: ellipsis;
313
+ `,
314
+ errorList: _react3.css`
315
+ margin-top: 12px;
316
+ padding: 12px;
317
+ background: #fef2f2;
318
+ border: 1px solid #fecaca;
319
+ border-radius: 6px;
320
+ max-height: 200px;
321
+ overflow-y: auto;
322
+ `,
323
+ errorItem: _react3.css`
324
+ font-size: ${_chunkUFCWGUAGjs.fontSize.xs};
325
+ color: #991b1b;
326
+ margin: 0 0 4px;
327
+ &:last-child {
328
+ margin-bottom: 0;
329
+ }
313
330
  `
314
331
  };
315
332
  function ProgressModal({
@@ -331,26 +348,36 @@ function ProgressModal({
331
348
  " image",
332
349
  (_nullishCoalesce(progress.processed, () => ( progress.current))) !== 1 ? "s" : "",
333
350
  " before stopping."
334
- ] }) : isComplete ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { css: styles.message, children: [
335
- "Processed ",
336
- progress.processed,
337
- " image",
338
- progress.processed !== 1 ? "s" : "",
339
- ".",
340
- progress.orphansRemoved !== void 0 && progress.orphansRemoved > 0 ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
341
- " Removed ",
342
- progress.orphansRemoved,
343
- " orphaned thumbnail",
344
- progress.orphansRemoved !== 1 ? "s" : "",
345
- "."
346
- ] }) : null,
347
- progress.errors !== void 0 && progress.errors > 0 ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
348
- " ",
349
- progress.errors,
350
- " error",
351
- progress.errors !== 1 ? "s" : "",
352
- " occurred."
353
- ] }) : null
351
+ ] }) : isComplete ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
352
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { css: styles.message, children: [
353
+ "Processed ",
354
+ progress.processed,
355
+ " image",
356
+ progress.processed !== 1 ? "s" : "",
357
+ ".",
358
+ progress.orphansRemoved !== void 0 && progress.orphansRemoved > 0 ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
359
+ " Removed ",
360
+ progress.orphansRemoved,
361
+ " orphaned thumbnail",
362
+ progress.orphansRemoved !== 1 ? "s" : "",
363
+ "."
364
+ ] }) : null,
365
+ progress.errors !== void 0 && progress.errors > 0 ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
366
+ " ",
367
+ progress.errors,
368
+ " error",
369
+ progress.errors !== 1 ? "s" : "",
370
+ " occurred."
371
+ ] }) : null
372
+ ] }),
373
+ progress.errorMessages && progress.errorMessages.length > 0 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: progressStyles.errorList, children: [
374
+ progress.errorMessages.slice(0, 10).map((msg, i) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: progressStyles.errorItem, children: msg }, i)),
375
+ progress.errorMessages.length > 10 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { css: progressStyles.errorItem, children: [
376
+ "...and ",
377
+ progress.errorMessages.length - 10,
378
+ " more"
379
+ ] })
380
+ ] })
354
381
  ] }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
355
382
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles.message, children: progress.status === "cleanup" ? "Cleaning up orphaned files..." : `Processing images...` }),
356
383
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: progressStyles.progressContainer, children: [
@@ -1262,7 +1289,7 @@ function StudioToolbar() {
1262
1289
  const fileInputRef = _react.useRef.call(void 0, null);
1263
1290
  const abortControllerRef = _react.useRef.call(void 0, null);
1264
1291
  const [uploading, setUploading] = _react.useState.call(void 0, false);
1265
- const [refreshing, setRefreshing] = _react.useState.call(void 0, false);
1292
+ const [scanning, setScanning] = _react.useState.call(void 0, false);
1266
1293
  const [processing, setProcessing] = _react.useState.call(void 0, false);
1267
1294
  const [showDeleteConfirm, setShowDeleteConfirm] = _react.useState.call(void 0, false);
1268
1295
  const [showProcessConfirm, setShowProcessConfirm] = _react.useState.call(void 0, false);
@@ -1288,10 +1315,81 @@ function StudioToolbar() {
1288
1315
  const handleUpload = _react.useCallback.call(void 0, () => {
1289
1316
  _optionalChain([fileInputRef, 'access', _2 => _2.current, 'optionalAccess', _3 => _3.click, 'call', _4 => _4()]);
1290
1317
  }, []);
1291
- const handleRefresh = _react.useCallback.call(void 0, () => {
1292
- setRefreshing(true);
1293
- triggerRefresh();
1294
- setTimeout(() => setRefreshing(false), 600);
1318
+ const handleScan = _react.useCallback.call(void 0, async () => {
1319
+ setScanning(true);
1320
+ setShowProgress(true);
1321
+ setProgressState({
1322
+ current: 0,
1323
+ total: 0,
1324
+ percent: 0,
1325
+ status: "processing",
1326
+ message: "Scanning for files..."
1327
+ });
1328
+ try {
1329
+ const response = await fetch("/api/studio/scan", { method: "POST" });
1330
+ const reader = _optionalChain([response, 'access', _5 => _5.body, 'optionalAccess', _6 => _6.getReader, 'call', _7 => _7()]);
1331
+ if (!reader) throw new Error("No reader");
1332
+ const decoder = new TextDecoder();
1333
+ let buffer = "";
1334
+ while (true) {
1335
+ const { done, value } = await reader.read();
1336
+ if (done) break;
1337
+ buffer += decoder.decode(value, { stream: true });
1338
+ const lines = buffer.split("\n\n");
1339
+ buffer = lines.pop() || "";
1340
+ for (const line of lines) {
1341
+ if (!line.startsWith("data: ")) continue;
1342
+ const data = JSON.parse(line.slice(6));
1343
+ if (data.type === "start") {
1344
+ setProgressState({
1345
+ current: 0,
1346
+ total: data.total,
1347
+ percent: 0,
1348
+ status: "processing",
1349
+ message: `Scanning ${data.total} files...`
1350
+ });
1351
+ } else if (data.type === "progress") {
1352
+ setProgressState({
1353
+ current: data.current,
1354
+ total: data.total,
1355
+ percent: data.percent,
1356
+ status: "processing",
1357
+ currentFile: data.currentFile
1358
+ });
1359
+ } else if (data.type === "complete") {
1360
+ setProgressState({
1361
+ current: data.total || 0,
1362
+ total: data.total || 0,
1363
+ percent: 100,
1364
+ status: "complete",
1365
+ processed: data.added,
1366
+ errors: data.errors,
1367
+ message: data.renamed > 0 ? `${data.renamed} file(s) renamed due to conflicts` : void 0
1368
+ });
1369
+ triggerRefresh();
1370
+ } else if (data.type === "error") {
1371
+ setProgressState({
1372
+ current: 0,
1373
+ total: 0,
1374
+ percent: 0,
1375
+ status: "error",
1376
+ message: data.message || "Scan failed"
1377
+ });
1378
+ }
1379
+ }
1380
+ }
1381
+ } catch (error) {
1382
+ console.error("Scan error:", error);
1383
+ setProgressState({
1384
+ current: 0,
1385
+ total: 0,
1386
+ percent: 0,
1387
+ status: "error",
1388
+ message: "Scan failed"
1389
+ });
1390
+ } finally {
1391
+ setScanning(false);
1392
+ }
1295
1393
  }, [triggerRefresh]);
1296
1394
  const handleFileChange = _react.useCallback.call(void 0, async (e) => {
1297
1395
  const files = e.target.files;
@@ -1395,7 +1493,7 @@ function StudioToolbar() {
1395
1493
  const selectedPaths2 = Array.from(selectedItems);
1396
1494
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1397
1495
  const selectedImagePaths = selectedPaths2.filter((p) => {
1398
- 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()]) || "";
1496
+ 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()]) || "";
1399
1497
  return imageExtensions.includes(ext);
1400
1498
  });
1401
1499
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1556,12 +1654,12 @@ function StudioToolbar() {
1556
1654
  const data = await response.json();
1557
1655
  if (response.ok) {
1558
1656
  setProgressState({
1559
- current: _optionalChain([data, 'access', _11 => _11.processed, 'optionalAccess', _12 => _12.length]) || 0,
1560
- total: _optionalChain([data, 'access', _13 => _13.processed, 'optionalAccess', _14 => _14.length]) || 0,
1657
+ current: _optionalChain([data, 'access', _14 => _14.processed, 'optionalAccess', _15 => _15.length]) || 0,
1658
+ total: _optionalChain([data, 'access', _16 => _16.processed, 'optionalAccess', _17 => _17.length]) || 0,
1561
1659
  percent: 100,
1562
1660
  status: "complete",
1563
- processed: _optionalChain([data, 'access', _15 => _15.processed, 'optionalAccess', _16 => _16.length]) || 0,
1564
- errors: _optionalChain([data, 'access', _17 => _17.errors, 'optionalAccess', _18 => _18.length]) || 0
1661
+ processed: _optionalChain([data, 'access', _18 => _18.processed, 'optionalAccess', _19 => _19.length]) || 0,
1662
+ errors: _optionalChain([data, 'access', _20 => _20.errors, 'optionalAccess', _21 => _21.length]) || 0
1565
1663
  });
1566
1664
  clearSelection();
1567
1665
  triggerRefresh();
@@ -1638,7 +1736,7 @@ function StudioToolbar() {
1638
1736
  const selectedPaths2 = Array.from(selectedItems);
1639
1737
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1640
1738
  const selectedImagePaths = selectedPaths2.filter((p) => {
1641
- 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()]) || "";
1739
+ 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()]) || "";
1642
1740
  return imageExtensions.includes(ext);
1643
1741
  });
1644
1742
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1673,7 +1771,7 @@ function StudioToolbar() {
1673
1771
  const selectedPaths2 = Array.from(selectedItems);
1674
1772
  const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1675
1773
  const selectedImagePaths = selectedPaths2.filter((p) => {
1676
- 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()]) || "";
1774
+ 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()]) || "";
1677
1775
  return imageExtensions.includes(ext);
1678
1776
  });
1679
1777
  const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
@@ -1704,6 +1802,7 @@ function StudioToolbar() {
1704
1802
  setShowProgress(true);
1705
1803
  let synced = 0;
1706
1804
  let errors = 0;
1805
+ const errorMessages = [];
1707
1806
  try {
1708
1807
  for (let i = 0; i < imageKeys.length; i++) {
1709
1808
  const imageKey = imageKeys[i];
@@ -1722,19 +1821,24 @@ function StudioToolbar() {
1722
1821
  });
1723
1822
  const data = await response.json();
1724
1823
  if (!response.ok) {
1725
- 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")])) {
1824
+ 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")])) {
1726
1825
  setShowProgress(false);
1727
1826
  setShowR2SetupModal(true);
1728
1827
  return;
1729
1828
  }
1730
1829
  errors++;
1731
- } else if (_optionalChain([data, 'access', _37 => _37.synced, 'optionalAccess', _38 => _38.length]) > 0) {
1830
+ errorMessages.push(data.error || `Failed: ${imageKey}`);
1831
+ } else if (_optionalChain([data, 'access', _40 => _40.synced, 'optionalAccess', _41 => _41.length]) > 0) {
1732
1832
  synced++;
1733
- } else if (_optionalChain([data, 'access', _39 => _39.errors, 'optionalAccess', _40 => _40.length]) > 0) {
1833
+ } else if (_optionalChain([data, 'access', _42 => _42.errors, 'optionalAccess', _43 => _43.length]) > 0) {
1734
1834
  errors++;
1835
+ for (const errMsg of data.errors) {
1836
+ errorMessages.push(errMsg);
1837
+ }
1735
1838
  }
1736
- } catch (e4) {
1839
+ } catch (err) {
1737
1840
  errors++;
1841
+ errorMessages.push(`Network error: ${imageKey}`);
1738
1842
  }
1739
1843
  }
1740
1844
  setProgressState({
@@ -1743,7 +1847,8 @@ function StudioToolbar() {
1743
1847
  percent: 100,
1744
1848
  status: "complete",
1745
1849
  processed: synced,
1746
- errors
1850
+ errors,
1851
+ errorMessages: errorMessages.length > 0 ? errorMessages : void 0
1747
1852
  });
1748
1853
  clearSelection();
1749
1854
  triggerRefresh();
@@ -2070,12 +2175,16 @@ function StudioToolbar() {
2070
2175
  " selected",
2071
2176
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: styles4.clearBtn, onClick: clearSelection, children: "Clear" })
2072
2177
  ] }),
2073
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
2178
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
2074
2179
  "button",
2075
2180
  {
2076
- css: [styles4.btn, styles4.btnIconOnly],
2077
- onClick: handleRefresh,
2078
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, RefreshIcon, { spinning: refreshing })
2181
+ css: styles4.btn,
2182
+ onClick: handleScan,
2183
+ disabled: scanning,
2184
+ children: [
2185
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ScanIcon, { spinning: scanning }),
2186
+ "Scan"
2187
+ ]
2079
2188
  }
2080
2189
  ),
2081
2190
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles4.viewToggle, children: [
@@ -2105,7 +2214,7 @@ function StudioToolbar() {
2105
2214
  function UploadIcon() {
2106
2215
  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" }) });
2107
2216
  }
2108
- function RefreshIcon({ spinning }) {
2217
+ function ScanIcon({ spinning }) {
2109
2218
  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" }) });
2110
2219
  }
2111
2220
  function TrashIcon() {
@@ -2239,6 +2348,7 @@ function useFileList() {
2239
2348
  } = useStudio();
2240
2349
  const [items, setItems] = _react.useState.call(void 0, []);
2241
2350
  const [loading, setLoading] = _react.useState.call(void 0, true);
2351
+ const [metaEmpty, setMetaEmpty] = _react.useState.call(void 0, false);
2242
2352
  const isInitialLoad = _react.useRef.call(void 0, true);
2243
2353
  const lastPath = _react.useRef.call(void 0, currentPath);
2244
2354
  _react.useEffect.call(void 0, () => {
@@ -2251,10 +2361,12 @@ function useFileList() {
2251
2361
  try {
2252
2362
  const data = searchQuery && searchQuery.length >= 2 ? await studioApi.search(searchQuery) : await studioApi.list(currentPath);
2253
2363
  setItems(data.items || []);
2364
+ setMetaEmpty(data.isEmpty === true);
2254
2365
  } catch (error) {
2255
2366
  const message = error instanceof Error ? error.message : "Failed to load items";
2256
2367
  showError("Load Error", message);
2257
2368
  setItems([]);
2369
+ setMetaEmpty(false);
2258
2370
  }
2259
2371
  setLoading(false);
2260
2372
  isInitialLoad.current = false;
@@ -2306,6 +2418,7 @@ function useFileList() {
2306
2418
  items,
2307
2419
  loading,
2308
2420
  sortedItems,
2421
+ metaEmpty,
2309
2422
  // Computed
2310
2423
  isAtRoot,
2311
2424
  isSearching,
@@ -2366,6 +2479,27 @@ var styles5 = {
2366
2479
  font-size: ${_chunkUFCWGUAGjs.fontSize.sm};
2367
2480
  }
2368
2481
  `,
2482
+ scanButton: _react3.css`
2483
+ margin-top: 16px;
2484
+ padding: 10px 24px;
2485
+ font-size: ${_chunkUFCWGUAGjs.fontSize.base};
2486
+ font-weight: 500;
2487
+ background: ${_chunkUFCWGUAGjs.colors.primary};
2488
+ color: white;
2489
+ border: none;
2490
+ border-radius: 8px;
2491
+ cursor: pointer;
2492
+ transition: background 0.15s ease;
2493
+
2494
+ &:hover:not(:disabled) {
2495
+ background: ${_chunkUFCWGUAGjs.colors.primaryHover};
2496
+ }
2497
+
2498
+ &:disabled {
2499
+ opacity: 0.6;
2500
+ cursor: not-allowed;
2501
+ }
2502
+ `,
2369
2503
  grid: _react3.css`
2370
2504
  display: grid;
2371
2505
  grid-template-columns: 1fr;
@@ -2649,6 +2783,7 @@ function StudioFileGrid() {
2649
2783
  const {
2650
2784
  loading,
2651
2785
  sortedItems,
2786
+ metaEmpty,
2652
2787
  isAtRoot,
2653
2788
  isSearching,
2654
2789
  allItemsSelected,
@@ -2660,14 +2795,42 @@ function StudioFileGrid() {
2660
2795
  handleGenerateThumbnail,
2661
2796
  handleSelectAll
2662
2797
  } = useFileList();
2798
+ const [scanning, setScanning] = _react.useState.call(void 0, false);
2799
+ const handleScan = async () => {
2800
+ setScanning(true);
2801
+ try {
2802
+ await fetch("/api/studio/scan", { method: "POST" });
2803
+ window.location.reload();
2804
+ } catch (error) {
2805
+ console.error("Scan failed:", error);
2806
+ } finally {
2807
+ setScanning(false);
2808
+ }
2809
+ };
2663
2810
  if (loading) {
2664
2811
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.loading, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.spinner }) });
2665
2812
  }
2813
+ if (metaEmpty && isAtRoot) {
2814
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.empty, children: [
2815
+ /* @__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" }) }),
2816
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "No files tracked yet" }),
2817
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Click Scan to discover files in your public folder" }),
2818
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
2819
+ "button",
2820
+ {
2821
+ css: styles5.scanButton,
2822
+ onClick: handleScan,
2823
+ disabled: scanning,
2824
+ children: scanning ? "Scanning..." : "Scan for Files"
2825
+ }
2826
+ )
2827
+ ] });
2828
+ }
2666
2829
  if (sortedItems.length === 0 && isAtRoot) {
2667
2830
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.empty, children: [
2668
2831
  /* @__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" }) }),
2669
2832
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "No files in this folder" }),
2670
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Upload images to get started" })
2833
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Upload images or click Scan in the toolbar" })
2671
2834
  ] });
2672
2835
  }
2673
2836
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { children: [
@@ -2864,6 +3027,32 @@ var styles6 = {
2864
3027
  height: 256px;
2865
3028
  color: ${_chunkUFCWGUAGjs.colors.textSecondary};
2866
3029
  `,
3030
+ emptyHint: _react3.css`
3031
+ font-size: ${_chunkUFCWGUAGjs.fontSize.sm};
3032
+ color: ${_chunkUFCWGUAGjs.colors.textMuted};
3033
+ margin-top: 4px;
3034
+ `,
3035
+ scanButton: _react3.css`
3036
+ margin-top: 16px;
3037
+ padding: 10px 24px;
3038
+ font-size: ${_chunkUFCWGUAGjs.fontSize.base};
3039
+ font-weight: 500;
3040
+ background: ${_chunkUFCWGUAGjs.colors.primary};
3041
+ color: white;
3042
+ border: none;
3043
+ border-radius: 8px;
3044
+ cursor: pointer;
3045
+ transition: background 0.15s ease;
3046
+
3047
+ &:hover:not(:disabled) {
3048
+ background: ${_chunkUFCWGUAGjs.colors.primaryHover};
3049
+ }
3050
+
3051
+ &:disabled {
3052
+ opacity: 0.6;
3053
+ cursor: not-allowed;
3054
+ }
3055
+ `,
2867
3056
  tableWrapper: _react3.css`
2868
3057
  background: ${_chunkUFCWGUAGjs.colors.surface};
2869
3058
  border-radius: 8px;
@@ -3150,6 +3339,7 @@ function StudioFileList() {
3150
3339
  const {
3151
3340
  loading,
3152
3341
  sortedItems,
3342
+ metaEmpty,
3153
3343
  isAtRoot,
3154
3344
  isSearching,
3155
3345
  allItemsSelected,
@@ -3161,11 +3351,41 @@ function StudioFileList() {
3161
3351
  handleGenerateThumbnail,
3162
3352
  handleSelectAll
3163
3353
  } = useFileList();
3354
+ const [scanning, setScanning] = _react.useState.call(void 0, false);
3355
+ const handleScan = async () => {
3356
+ setScanning(true);
3357
+ try {
3358
+ await fetch("/api/studio/scan", { method: "POST" });
3359
+ window.location.reload();
3360
+ } catch (error) {
3361
+ console.error("Scan failed:", error);
3362
+ } finally {
3363
+ setScanning(false);
3364
+ }
3365
+ };
3164
3366
  if (loading) {
3165
3367
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.loading, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.spinner }) });
3166
3368
  }
3369
+ if (metaEmpty && isAtRoot) {
3370
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles6.empty, children: [
3371
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children: "No files tracked yet" }),
3372
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles6.emptyHint, children: "Click Scan to discover files in your public folder" }),
3373
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
3374
+ "button",
3375
+ {
3376
+ css: styles6.scanButton,
3377
+ onClick: handleScan,
3378
+ disabled: scanning,
3379
+ children: scanning ? "Scanning..." : "Scan for Files"
3380
+ }
3381
+ )
3382
+ ] });
3383
+ }
3167
3384
  if (sortedItems.length === 0 && isAtRoot) {
3168
- 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" }) });
3385
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles6.empty, children: [
3386
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children: "No files in this folder" }),
3387
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles6.emptyHint, children: "Upload images or click Scan in the toolbar" })
3388
+ ] });
3169
3389
  }
3170
3390
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles6.tableWrapper, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "table", { css: styles6.table, children: [
3171
3391
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "thead", { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { children: [
@@ -3675,7 +3895,7 @@ function StudioDetailView() {
3675
3895
  });
3676
3896
  triggerRefresh();
3677
3897
  } else {
3678
- 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")])) {
3898
+ 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")])) {
3679
3899
  setShowR2SetupModal(true);
3680
3900
  } else {
3681
3901
  setAlertMessage({
@@ -3714,7 +3934,7 @@ function StudioDetailView() {
3714
3934
  if (!response.ok) {
3715
3935
  throw new Error("Processing failed");
3716
3936
  }
3717
- const reader = _optionalChain([response, 'access', _47 => _47.body, 'optionalAccess', _48 => _48.getReader, 'call', _49 => _49()]);
3937
+ const reader = _optionalChain([response, 'access', _50 => _50.body, 'optionalAccess', _51 => _51.getReader, 'call', _52 => _52()]);
3718
3938
  if (!reader) {
3719
3939
  throw new Error("No response body");
3720
3940
  }
@@ -3731,7 +3951,7 @@ function StudioDetailView() {
3731
3951
  try {
3732
3952
  const data = JSON.parse(line.slice(6));
3733
3953
  setProcessProgress(data);
3734
- } catch (e5) {
3954
+ } catch (e4) {
3735
3955
  }
3736
3956
  }
3737
3957
  }
@@ -4661,4 +4881,4 @@ var StudioUI_default = StudioUI;
4661
4881
 
4662
4882
 
4663
4883
  exports.StudioUI = StudioUI; exports.default = StudioUI_default;
4664
- //# sourceMappingURL=StudioUI-ZBSTYTUV.js.map
4884
+ //# sourceMappingURL=StudioUI-JQHRTF45.js.map