@gallop.software/studio 0.1.33 → 0.1.35

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.
@@ -1100,6 +1100,36 @@ var styles3 = {
1100
1100
  object-fit: contain;
1101
1101
  border-radius: 4px;
1102
1102
  `,
1103
+ noThumbnail: css3`
1104
+ display: flex;
1105
+ flex-direction: column;
1106
+ align-items: center;
1107
+ justify-content: center;
1108
+ gap: 8px;
1109
+ padding: 16px;
1110
+ background: ${colors.background};
1111
+ border: 2px dashed ${colors.border};
1112
+ border-radius: 8px;
1113
+ cursor: pointer;
1114
+ transition: all 0.15s ease;
1115
+ width: 80%;
1116
+ height: 60%;
1117
+
1118
+ &:hover {
1119
+ border-color: ${colors.primary};
1120
+ background: ${colors.surfaceHover};
1121
+ }
1122
+ `,
1123
+ noThumbnailIcon: css3`
1124
+ width: 32px;
1125
+ height: 32px;
1126
+ color: ${colors.textMuted};
1127
+ `,
1128
+ noThumbnailText: css3`
1129
+ font-size: ${fontSize.xs};
1130
+ color: ${colors.textMuted};
1131
+ text-align: center;
1132
+ `,
1103
1133
  label: css3`
1104
1134
  padding: 10px 12px;
1105
1135
  background-color: ${colors.surface};
@@ -1179,7 +1209,7 @@ var styles3 = {
1179
1209
  `
1180
1210
  };
1181
1211
  function StudioFileGrid() {
1182
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem } = useStudio();
1212
+ const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh } = useStudio();
1183
1213
  const [items, setItems] = useState2([]);
1184
1214
  const [loading, setLoading] = useState2(true);
1185
1215
  const isInitialLoad = useRef2(true);
@@ -1235,6 +1265,19 @@ function StudioFileGrid() {
1235
1265
  setFocusedItem(item);
1236
1266
  }
1237
1267
  };
1268
+ const handleGenerateThumbnail = async (item) => {
1269
+ try {
1270
+ const imageKey = item.path.replace(/^public\//, "");
1271
+ await fetch("/api/studio/reprocess", {
1272
+ method: "POST",
1273
+ headers: { "Content-Type": "application/json" },
1274
+ body: JSON.stringify({ imageKeys: [imageKey] })
1275
+ });
1276
+ triggerRefresh();
1277
+ } catch (error) {
1278
+ console.error("Failed to generate thumbnail:", error);
1279
+ }
1280
+ };
1238
1281
  const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
1239
1282
  const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
1240
1283
  const handleSelectAll = () => {
@@ -1271,7 +1314,7 @@ function StudioFileGrid() {
1271
1314
  children: [
1272
1315
  /* @__PURE__ */ jsx3("div", { css: styles3.content, children: /* @__PURE__ */ jsx3("svg", { css: styles3.parentIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" }) }) }),
1273
1316
  /* @__PURE__ */ jsxs3("div", { css: styles3.label, children: [
1274
- /* @__PURE__ */ jsx3("p", { css: styles3.name, children: getParentPath(currentPath) }),
1317
+ /* @__PURE__ */ jsx3("p", { css: styles3.name, children: ".." }),
1275
1318
  /* @__PURE__ */ jsx3("p", { css: styles3.size, children: "Parent folder" })
1276
1319
  ] })
1277
1320
  ]
@@ -1283,15 +1326,17 @@ function StudioFileGrid() {
1283
1326
  item,
1284
1327
  isSelected: selectedItems.has(item.path),
1285
1328
  onClick: (e) => handleItemClick(item, e),
1286
- onOpen: () => handleOpen(item)
1329
+ onOpen: () => handleOpen(item),
1330
+ onGenerateThumbnail: () => handleGenerateThumbnail(item)
1287
1331
  },
1288
1332
  item.path
1289
1333
  ))
1290
1334
  ] })
1291
1335
  ] });
1292
1336
  }
1293
- function GridItem({ item, isSelected, onClick, onOpen }) {
1337
+ function GridItem({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
1294
1338
  const isFolder = item.type === "folder";
1339
+ const isImage = !isFolder && item.thumbnail !== void 0;
1295
1340
  return /* @__PURE__ */ jsxs3(
1296
1341
  "div",
1297
1342
  {
@@ -1315,7 +1360,7 @@ function GridItem({ item, isSelected, onClick, onOpen }) {
1315
1360
  }
1316
1361
  ),
1317
1362
  item.cdnSynced && /* @__PURE__ */ jsx3("span", { css: styles3.cdnBadge, children: "CDN" }),
1318
- /* @__PURE__ */ jsx3("div", { css: styles3.content, children: isFolder ? /* @__PURE__ */ jsx3("svg", { css: styles3.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : item.thumbnail ? /* @__PURE__ */ jsx3(
1363
+ /* @__PURE__ */ jsx3("div", { css: styles3.content, children: isFolder ? /* @__PURE__ */ jsx3("svg", { css: styles3.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : isImage && item.hasThumbnail ? /* @__PURE__ */ jsx3(
1319
1364
  "img",
1320
1365
  {
1321
1366
  css: styles3.image,
@@ -1323,6 +1368,20 @@ function GridItem({ item, isSelected, onClick, onOpen }) {
1323
1368
  alt: item.name,
1324
1369
  loading: "lazy"
1325
1370
  }
1371
+ ) : isImage && !item.hasThumbnail ? /* @__PURE__ */ jsxs3(
1372
+ "button",
1373
+ {
1374
+ css: styles3.noThumbnail,
1375
+ onClick: (e) => {
1376
+ e.stopPropagation();
1377
+ onGenerateThumbnail();
1378
+ },
1379
+ title: "Generate thumbnail",
1380
+ children: [
1381
+ /* @__PURE__ */ jsx3("svg", { css: styles3.noThumbnailIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("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" }) }),
1382
+ /* @__PURE__ */ jsx3("span", { css: styles3.noThumbnailText, children: "Generate" })
1383
+ ]
1384
+ }
1326
1385
  ) : /* @__PURE__ */ jsx3("svg", { css: styles3.fileIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" }) }) }),
1327
1386
  /* @__PURE__ */ jsx3("div", { css: styles3.label, children: /* @__PURE__ */ jsxs3("div", { css: styles3.labelRow, children: [
1328
1387
  /* @__PURE__ */ jsxs3("div", { css: styles3.labelText, children: [
@@ -1354,11 +1413,6 @@ function formatFileSize(bytes) {
1354
1413
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1355
1414
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1356
1415
  }
1357
- function getParentPath(path) {
1358
- const parts = path.split("/");
1359
- parts.pop();
1360
- return parts.join("/") + "/";
1361
- }
1362
1416
  function truncateMiddle(str, maxLength = 24) {
1363
1417
  if (str.length <= maxLength) return str;
1364
1418
  const lastDot = str.lastIndexOf(".");
@@ -1501,12 +1555,37 @@ var styles4 = {
1501
1555
  flex-shrink: 0;
1502
1556
  `,
1503
1557
  thumbnail: css4`
1558
+ max-width: 48px;
1559
+ max-height: 36px;
1560
+ width: auto;
1561
+ height: auto;
1562
+ object-fit: contain;
1563
+ border-radius: 4px;
1564
+ flex-shrink: 0;
1565
+ border: 1px solid ${colors.borderLight};
1566
+ `,
1567
+ noThumbnail: css4`
1504
1568
  width: 36px;
1505
1569
  height: 36px;
1506
- object-fit: cover;
1507
- border-radius: 6px;
1570
+ display: flex;
1571
+ align-items: center;
1572
+ justify-content: center;
1573
+ background: ${colors.background};
1574
+ border: 1px dashed ${colors.border};
1575
+ border-radius: 4px;
1508
1576
  flex-shrink: 0;
1509
- border: 1px solid ${colors.borderLight};
1577
+ cursor: pointer;
1578
+ transition: all 0.15s ease;
1579
+
1580
+ &:hover {
1581
+ border-color: ${colors.primary};
1582
+ background: ${colors.surfaceHover};
1583
+ }
1584
+ `,
1585
+ noThumbnailIcon: css4`
1586
+ width: 16px;
1587
+ height: 16px;
1588
+ color: ${colors.textMuted};
1510
1589
  `,
1511
1590
  name: css4`
1512
1591
  font-size: ${fontSize.base};
@@ -1556,7 +1635,7 @@ var styles4 = {
1556
1635
  `
1557
1636
  };
1558
1637
  function StudioFileList() {
1559
- const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem } = useStudio();
1638
+ const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem, triggerRefresh } = useStudio();
1560
1639
  const [items, setItems] = useState3([]);
1561
1640
  const [loading, setLoading] = useState3(true);
1562
1641
  const isInitialLoad = useRef3(true);
@@ -1608,6 +1687,19 @@ function StudioFileList() {
1608
1687
  setFocusedItem(item);
1609
1688
  }
1610
1689
  };
1690
+ const handleGenerateThumbnail = async (item) => {
1691
+ try {
1692
+ const imageKey = item.path.replace(/^public\//, "");
1693
+ await fetch("/api/studio/reprocess", {
1694
+ method: "POST",
1695
+ headers: { "Content-Type": "application/json" },
1696
+ body: JSON.stringify({ imageKeys: [imageKey] })
1697
+ });
1698
+ triggerRefresh();
1699
+ } catch (error) {
1700
+ console.error("Failed to generate thumbnail:", error);
1701
+ }
1702
+ };
1611
1703
  const allItemsSelected = sortedItems.length > 0 && sortedItems.every((item) => selectedItems.has(item.path));
1612
1704
  const someItemsSelected = sortedItems.some((item) => selectedItems.has(item.path));
1613
1705
  const handleSelectAll = () => {
@@ -1641,7 +1733,7 @@ function StudioFileList() {
1641
1733
  /* @__PURE__ */ jsx4("td", { css: styles4.td }),
1642
1734
  /* @__PURE__ */ jsx4("td", { css: styles4.td, children: /* @__PURE__ */ jsxs4("div", { css: styles4.nameCell, children: [
1643
1735
  /* @__PURE__ */ jsx4("svg", { css: styles4.parentIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" }) }),
1644
- /* @__PURE__ */ jsx4("span", { css: styles4.name, children: getParentPath2(currentPath) })
1736
+ /* @__PURE__ */ jsx4("span", { css: styles4.name, children: ".." })
1645
1737
  ] }) }),
1646
1738
  /* @__PURE__ */ jsx4("td", { css: [styles4.td, styles4.meta], children: "--" }),
1647
1739
  /* @__PURE__ */ jsx4("td", { css: [styles4.td, styles4.meta], children: "Parent folder" }),
@@ -1653,15 +1745,17 @@ function StudioFileList() {
1653
1745
  item,
1654
1746
  isSelected: selectedItems.has(item.path),
1655
1747
  onClick: (e) => handleItemClick(item, e),
1656
- onOpen: () => handleOpen(item)
1748
+ onOpen: () => handleOpen(item),
1749
+ onGenerateThumbnail: () => handleGenerateThumbnail(item)
1657
1750
  },
1658
1751
  item.path
1659
1752
  ))
1660
1753
  ] })
1661
1754
  ] }) });
1662
1755
  }
1663
- function ListRow({ item, isSelected, onClick, onOpen }) {
1756
+ function ListRow({ item, isSelected, onClick, onOpen, onGenerateThumbnail }) {
1664
1757
  const isFolder = item.type === "folder";
1758
+ const isImage = !isFolder && item.thumbnail !== void 0;
1665
1759
  return /* @__PURE__ */ jsxs4(
1666
1760
  "tr",
1667
1761
  {
@@ -1685,7 +1779,18 @@ function ListRow({ item, isSelected, onClick, onOpen }) {
1685
1779
  }
1686
1780
  ),
1687
1781
  /* @__PURE__ */ jsx4("td", { css: styles4.td, children: /* @__PURE__ */ jsxs4("div", { css: styles4.nameCell, children: [
1688
- isFolder ? /* @__PURE__ */ jsx4("svg", { css: styles4.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : item.thumbnail ? /* @__PURE__ */ jsx4("img", { css: styles4.thumbnail, src: item.thumbnail, alt: item.name, loading: "lazy" }) : /* @__PURE__ */ jsx4("svg", { css: styles4.fileIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" }) }),
1782
+ isFolder ? /* @__PURE__ */ jsx4("svg", { css: styles4.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : isImage && item.hasThumbnail ? /* @__PURE__ */ jsx4("img", { css: styles4.thumbnail, src: item.thumbnail, alt: item.name, loading: "lazy" }) : isImage && !item.hasThumbnail ? /* @__PURE__ */ jsx4(
1783
+ "button",
1784
+ {
1785
+ css: styles4.noThumbnail,
1786
+ onClick: (e) => {
1787
+ e.stopPropagation();
1788
+ onGenerateThumbnail();
1789
+ },
1790
+ title: "Generate thumbnail",
1791
+ children: /* @__PURE__ */ jsx4("svg", { css: styles4.noThumbnailIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("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" }) })
1792
+ }
1793
+ ) : /* @__PURE__ */ jsx4("svg", { css: styles4.fileIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" }) }),
1689
1794
  /* @__PURE__ */ jsx4("span", { css: styles4.name, title: item.name, children: truncateMiddle2(item.name) }),
1690
1795
  /* @__PURE__ */ jsx4(
1691
1796
  "button",
@@ -1714,11 +1819,6 @@ function formatFileSize2(bytes) {
1714
1819
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1715
1820
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1716
1821
  }
1717
- function getParentPath2(path) {
1718
- const parts = path.split("/");
1719
- parts.pop();
1720
- return parts.join("/") + "/";
1721
- }
1722
1822
  function truncateMiddle2(str, maxLength = 32) {
1723
1823
  if (str.length <= maxLength) return str;
1724
1824
  const lastDot = str.lastIndexOf(".");
@@ -2021,6 +2121,10 @@ function StudioDetailView() {
2021
2121
  /* @__PURE__ */ jsx5("span", { css: styles5.infoLabel, children: "Name" }),
2022
2122
  /* @__PURE__ */ jsx5("span", { css: styles5.infoValueWrap, children: focusedItem.name })
2023
2123
  ] }),
2124
+ /* @__PURE__ */ jsxs5("div", { css: styles5.infoRow, children: [
2125
+ /* @__PURE__ */ jsx5("span", { css: styles5.infoLabel, children: "Path" }),
2126
+ /* @__PURE__ */ jsx5("span", { css: styles5.infoValueWrap, children: focusedItem.path.replace(/^public\//, "") })
2127
+ ] }),
2024
2128
  focusedItem.size !== void 0 && /* @__PURE__ */ jsxs5("div", { css: styles5.infoRow, children: [
2025
2129
  /* @__PURE__ */ jsx5("span", { css: styles5.infoLabel, children: "Size" }),
2026
2130
  /* @__PURE__ */ jsx5("span", { css: styles5.infoValue, children: formatFileSize3(focusedItem.size) })
@@ -2537,4 +2641,4 @@ export {
2537
2641
  StudioUI,
2538
2642
  StudioUI_default as default
2539
2643
  };
2540
- //# sourceMappingURL=StudioUI-IIVOARWK.mjs.map
2644
+ //# sourceMappingURL=StudioUI-Y4BA3RW5.mjs.map