@gallop.software/studio 0.1.5 → 0.1.7

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.
@@ -22,10 +22,13 @@ var defaultState = {
22
22
  selectedItems: /* @__PURE__ */ new Set(),
23
23
  toggleSelection: () => {
24
24
  },
25
+ selectRange: () => {
26
+ },
25
27
  selectAll: () => {
26
28
  },
27
29
  clearSelection: () => {
28
30
  },
31
+ lastSelectedPath: null,
29
32
  viewMode: "grid",
30
33
  setViewMode: () => {
31
34
  },
@@ -184,9 +187,27 @@ function StudioToolbar() {
184
187
  const handleReprocess = _react.useCallback.call(void 0, () => {
185
188
  console.log("Reprocess clicked", selectedItems);
186
189
  }, [selectedItems]);
187
- const handleDelete = _react.useCallback.call(void 0, () => {
188
- console.log("Delete clicked", selectedItems);
189
- }, [selectedItems]);
190
+ const handleDelete = _react.useCallback.call(void 0, async () => {
191
+ if (selectedItems.size === 0) return;
192
+ if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return;
193
+ try {
194
+ const response = await fetch("/api/studio/delete", {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ paths: Array.from(selectedItems) })
198
+ });
199
+ if (response.ok) {
200
+ clearSelection();
201
+ triggerRefresh();
202
+ } else {
203
+ const error = await response.json();
204
+ alert(`Delete failed: ${error.error || "Unknown error"}`);
205
+ }
206
+ } catch (error) {
207
+ console.error("Delete error:", error);
208
+ alert("Delete failed. Check console for details.");
209
+ }
210
+ }, [selectedItems, clearSelection, triggerRefresh]);
190
211
  const handleSyncCdn = _react.useCallback.call(void 0, () => {
191
212
  console.log("Sync CDN clicked", selectedItems);
192
213
  }, [selectedItems]);
@@ -529,10 +550,34 @@ var styles3 = {
529
550
  font-size: 12px;
530
551
  color: #9ca3af;
531
552
  margin: 0;
553
+ `,
554
+ selectAllRow: _react3.css`
555
+ display: flex;
556
+ align-items: center;
557
+ margin-bottom: 12px;
558
+ padding-bottom: 12px;
559
+ border-bottom: 1px solid #e5e7eb;
560
+ `,
561
+ selectAllLabel: _react3.css`
562
+ display: flex;
563
+ align-items: center;
564
+ gap: 8px;
565
+ font-size: 14px;
566
+ color: #6b7280;
567
+ cursor: pointer;
568
+
569
+ &:hover {
570
+ color: #374151;
571
+ }
572
+ `,
573
+ selectAllCheckbox: _react3.css`
574
+ width: 16px;
575
+ height: 16px;
576
+ accent-color: #9333ea;
532
577
  `
533
578
  };
534
579
  function StudioFileGrid() {
535
- const { currentPath, setCurrentPath, selectedItems, toggleSelection, refreshKey } = useStudio();
580
+ const { currentPath, setCurrentPath, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey } = useStudio();
536
581
  const [items, setItems] = _react.useState.call(void 0, []);
537
582
  const [loading, setLoading] = _react.useState.call(void 0, true);
538
583
  _react.useEffect.call(void 0, () => {
@@ -566,31 +611,67 @@ function StudioFileGrid() {
566
611
  if (a.type !== "folder" && b.type === "folder") return 1;
567
612
  return a.name.localeCompare(b.name);
568
613
  });
569
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles3.grid, children: sortedItems.map((item) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
570
- GridItem,
571
- {
572
- item,
573
- isSelected: selectedItems.has(item.path),
574
- onSelect: () => toggleSelection(item.path),
575
- onOpen: () => {
576
- if (item.type === "folder") {
577
- setCurrentPath(item.path);
614
+ const files = sortedItems.filter((item) => item.type !== "folder");
615
+ const allFilesSelected = files.length > 0 && files.every((item) => selectedItems.has(item.path));
616
+ const someFilesSelected = files.some((item) => selectedItems.has(item.path));
617
+ const handleSelectAll = () => {
618
+ if (allFilesSelected) {
619
+ clearSelection();
620
+ } else {
621
+ selectAll(files);
622
+ }
623
+ };
624
+ const handleItemClick = (item, e) => {
625
+ if (item.type === "folder") {
626
+ setCurrentPath(item.path);
627
+ return;
628
+ }
629
+ if (e.shiftKey && lastSelectedPath) {
630
+ selectRange(lastSelectedPath, item.path, sortedItems);
631
+ } else {
632
+ toggleSelection(item.path);
633
+ }
634
+ };
635
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { children: [
636
+ files.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles3.selectAllRow, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "label", { css: styles3.selectAllLabel, children: [
637
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
638
+ "input",
639
+ {
640
+ type: "checkbox",
641
+ css: styles3.selectAllCheckbox,
642
+ checked: allFilesSelected,
643
+ ref: (el) => {
644
+ if (el) el.indeterminate = someFilesSelected && !allFilesSelected;
645
+ },
646
+ onChange: handleSelectAll
578
647
  }
579
- }
580
- },
581
- item.path
582
- )) });
648
+ ),
649
+ "Select all (",
650
+ files.length,
651
+ ")"
652
+ ] }) }),
653
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles3.grid, children: sortedItems.map((item) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
654
+ GridItem,
655
+ {
656
+ item,
657
+ isSelected: selectedItems.has(item.path),
658
+ onClick: (e) => handleItemClick(item, e)
659
+ },
660
+ item.path
661
+ )) })
662
+ ] });
583
663
  }
584
- function GridItem({ item, isSelected, onSelect, onOpen }) {
664
+ function GridItem({ item, isSelected, onClick }) {
585
665
  const isFolder = item.type === "folder";
586
- return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: [styles3.item, isSelected && styles3.itemSelected], onDoubleClick: onOpen, children: [
587
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
666
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: [styles3.item, isSelected && styles3.itemSelected], onClick, children: [
667
+ !isFolder && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
588
668
  "input",
589
669
  {
590
670
  type: "checkbox",
591
671
  css: styles3.checkbox,
592
672
  checked: isSelected,
593
- onChange: onSelect,
673
+ onChange: () => {
674
+ },
594
675
  onClick: (e) => e.stopPropagation()
595
676
  }
596
677
  ),
@@ -734,7 +815,7 @@ var styles4 = {
734
815
  `
735
816
  };
736
817
  function StudioFileList() {
737
- const { currentPath, setCurrentPath, selectedItems, toggleSelection, refreshKey } = useStudio();
818
+ const { currentPath, setCurrentPath, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey } = useStudio();
738
819
  const [items, setItems] = _react.useState.call(void 0, []);
739
820
  const [loading, setLoading] = _react.useState.call(void 0, true);
740
821
  _react.useEffect.call(void 0, () => {
@@ -764,9 +845,41 @@ function StudioFileList() {
764
845
  if (a.type !== "folder" && b.type === "folder") return 1;
765
846
  return a.name.localeCompare(b.name);
766
847
  });
848
+ const files = sortedItems.filter((item) => item.type !== "folder");
849
+ const allFilesSelected = files.length > 0 && files.every((item) => selectedItems.has(item.path));
850
+ const someFilesSelected = files.some((item) => selectedItems.has(item.path));
851
+ const handleSelectAll = () => {
852
+ if (allFilesSelected) {
853
+ clearSelection();
854
+ } else {
855
+ selectAll(files);
856
+ }
857
+ };
858
+ const handleItemClick = (item, e) => {
859
+ if (item.type === "folder") {
860
+ setCurrentPath(item.path);
861
+ return;
862
+ }
863
+ if (e.shiftKey && lastSelectedPath) {
864
+ selectRange(lastSelectedPath, item.path, sortedItems);
865
+ } else {
866
+ toggleSelection(item.path);
867
+ }
868
+ };
767
869
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "table", { css: styles4.table, children: [
768
870
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "thead", { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { children: [
769
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { css: [styles4.th, styles4.thCheckbox] }),
871
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { css: [styles4.th, styles4.thCheckbox], children: files.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
872
+ "input",
873
+ {
874
+ type: "checkbox",
875
+ css: styles4.checkbox,
876
+ checked: allFilesSelected,
877
+ ref: (el) => {
878
+ if (el) el.indeterminate = someFilesSelected && !allFilesSelected;
879
+ },
880
+ onChange: handleSelectAll
881
+ }
882
+ ) }),
770
883
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { css: styles4.th, children: "Name" }),
771
884
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { css: [styles4.th, styles4.thSize], children: "Size" }),
772
885
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { css: [styles4.th, styles4.thDimensions], children: "Dimensions" }),
@@ -777,27 +890,23 @@ function StudioFileList() {
777
890
  {
778
891
  item,
779
892
  isSelected: selectedItems.has(item.path),
780
- onSelect: () => toggleSelection(item.path),
781
- onOpen: () => {
782
- if (item.type === "folder") {
783
- setCurrentPath(item.path);
784
- }
785
- }
893
+ onClick: (e) => handleItemClick(item, e)
786
894
  },
787
895
  item.path
788
896
  )) })
789
897
  ] });
790
898
  }
791
- function ListRow({ item, isSelected, onSelect, onOpen }) {
899
+ function ListRow({ item, isSelected, onClick }) {
792
900
  const isFolder = item.type === "folder";
793
- return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { css: [styles4.row, isSelected && styles4.rowSelected], onDoubleClick: onOpen, children: [
794
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { css: styles4.td, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
901
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { css: [styles4.row, isSelected && styles4.rowSelected], onClick, children: [
902
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { css: styles4.td, children: !isFolder && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
795
903
  "input",
796
904
  {
797
905
  type: "checkbox",
798
906
  css: styles4.checkbox,
799
907
  checked: isSelected,
800
- onChange: onSelect,
908
+ onChange: () => {
909
+ },
801
910
  onClick: (e) => e.stopPropagation()
802
911
  }
803
912
  ) }),
@@ -909,6 +1018,17 @@ var styles5 = {
909
1018
  height: 32px;
910
1019
  border-radius: 4px;
911
1020
  `,
1021
+ emptyState: _react3.css`
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: center;
1025
+ height: 200px;
1026
+ `,
1027
+ emptyText: _react3.css`
1028
+ font-size: 14px;
1029
+ color: #9ca3af;
1030
+ margin: 0;
1031
+ `,
912
1032
  actions: _react3.css`
913
1033
  margin-top: 16px;
914
1034
  padding-top: 16px;
@@ -941,9 +1061,46 @@ var styles5 = {
941
1061
  `
942
1062
  };
943
1063
  function StudioPreview() {
944
- const { selectedItems, meta } = useStudio();
945
- if (selectedItems.size !== 1) {
946
- return null;
1064
+ const { selectedItems, meta, triggerRefresh, clearSelection } = useStudio();
1065
+ const handleDelete = async () => {
1066
+ if (selectedItems.size === 0) return;
1067
+ if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return;
1068
+ try {
1069
+ const response = await fetch("/api/studio/delete", {
1070
+ method: "POST",
1071
+ headers: { "Content-Type": "application/json" },
1072
+ body: JSON.stringify({ paths: Array.from(selectedItems) })
1073
+ });
1074
+ if (response.ok) {
1075
+ clearSelection();
1076
+ triggerRefresh();
1077
+ } else {
1078
+ const error = await response.json();
1079
+ alert(`Delete failed: ${error.error || "Unknown error"}`);
1080
+ }
1081
+ } catch (error) {
1082
+ console.error("Delete error:", error);
1083
+ alert("Delete failed. Check console for details.");
1084
+ }
1085
+ };
1086
+ if (selectedItems.size === 0) {
1087
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.panel, children: [
1088
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { css: styles5.title, children: "Preview" }),
1089
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.emptyState, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles5.emptyText, children: "Select an image to preview" }) })
1090
+ ] });
1091
+ }
1092
+ if (selectedItems.size > 1) {
1093
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.panel, children: [
1094
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "h3", { css: styles5.title, children: [
1095
+ selectedItems.size,
1096
+ " items selected"
1097
+ ] }),
1098
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles5.actions, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { css: [styles5.actionBtn, styles5.actionBtnDanger], onClick: handleDelete, children: [
1099
+ "Delete ",
1100
+ selectedItems.size,
1101
+ " items"
1102
+ ] }) })
1103
+ ] });
947
1104
  }
948
1105
  const selectedPath = Array.from(selectedItems)[0];
949
1106
  const imageKey = selectedPath.replace(/^public\/images\//, "").replace(/^public\/originals\//, "");
@@ -1011,7 +1168,7 @@ function StudioPreview() {
1011
1168
  ] }),
1012
1169
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles5.actions, children: [
1013
1170
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: styles5.actionBtn, children: "Rename" }),
1014
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: [styles5.actionBtn, styles5.actionBtnDanger], children: "Delete" })
1171
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: [styles5.actionBtn, styles5.actionBtnDanger], onClick: handleDelete, children: "Delete" })
1015
1172
  ] })
1016
1173
  ] });
1017
1174
  }
@@ -1311,6 +1468,7 @@ var styles7 = {
1311
1468
  `,
1312
1469
  fileBrowser: _react3.css`
1313
1470
  flex: 1;
1471
+ min-width: 0;
1314
1472
  overflow: auto;
1315
1473
  padding: 16px;
1316
1474
  `
@@ -1318,6 +1476,7 @@ var styles7 = {
1318
1476
  function StudioUI({ onClose }) {
1319
1477
  const [currentPath, setCurrentPathInternal] = _react.useState.call(void 0, "public");
1320
1478
  const [selectedItems, setSelectedItems] = _react.useState.call(void 0, /* @__PURE__ */ new Set());
1479
+ const [lastSelectedPath, setLastSelectedPath] = _react.useState.call(void 0, null);
1321
1480
  const [viewMode, setViewMode] = _react.useState.call(void 0, "grid");
1322
1481
  const [meta, setMeta] = _react.useState.call(void 0, null);
1323
1482
  const [isLoading, setIsLoading] = _react.useState.call(void 0, false);
@@ -1346,6 +1505,23 @@ function StudioUI({ onClose }) {
1346
1505
  }
1347
1506
  return next;
1348
1507
  });
1508
+ setLastSelectedPath(path);
1509
+ }, []);
1510
+ const selectRange = _react.useCallback.call(void 0, (fromPath, toPath, allItems) => {
1511
+ const files = allItems.filter((item) => item.type !== "folder");
1512
+ const fromIndex = files.findIndex((item) => item.path === fromPath);
1513
+ const toIndex = files.findIndex((item) => item.path === toPath);
1514
+ if (fromIndex === -1 || toIndex === -1) return;
1515
+ const start = Math.min(fromIndex, toIndex);
1516
+ const end = Math.max(fromIndex, toIndex);
1517
+ setSelectedItems((prev) => {
1518
+ const next = new Set(prev);
1519
+ for (let i = start; i <= end; i++) {
1520
+ next.add(files[i].path);
1521
+ }
1522
+ return next;
1523
+ });
1524
+ setLastSelectedPath(toPath);
1349
1525
  }, []);
1350
1526
  const selectAll = _react.useCallback.call(void 0, (items) => {
1351
1527
  setSelectedItems(new Set(items.map((item) => item.path)));
@@ -1380,8 +1556,10 @@ function StudioUI({ onClose }) {
1380
1556
  navigateUp,
1381
1557
  selectedItems,
1382
1558
  toggleSelection,
1559
+ selectRange,
1383
1560
  selectAll,
1384
1561
  clearSelection,
1562
+ lastSelectedPath,
1385
1563
  viewMode,
1386
1564
  setViewMode,
1387
1565
  meta,
@@ -1439,4 +1617,4 @@ var StudioUI_default = StudioUI;
1439
1617
 
1440
1618
 
1441
1619
  exports.StudioUI = StudioUI; exports.default = StudioUI_default;
1442
- //# sourceMappingURL=StudioUI-Z3DOQO7P.js.map
1620
+ //# sourceMappingURL=StudioUI-ZAD65UPD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/StudioUI-ZAD65UPD.js","../src/components/StudioUI.tsx","../src/components/StudioContext.tsx","../src/components/StudioToolbar.tsx","../src/components/StudioBreadcrumb.tsx","../src/components/StudioFileGrid.tsx","../src/components/StudioFileList.tsx","../src/components/StudioPreview.tsx","../src/components/StudioSettings.tsx"],"names":["css","jsx","styles","keyframes","jsxs","useCallback"],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/StudioUI-ZAD65UPD.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useCallback, useState } from 'react'\nimport { css } from '@emotion/react'\nimport { StudioContext } from './StudioContext'\nimport { StudioToolbar } from './StudioToolbar'\nimport { StudioBreadcrumb } from './StudioBreadcrumb'\nimport { StudioFileGrid } from './StudioFileGrid'\nimport { StudioFileList } from './StudioFileList'\nimport { StudioPreview } from './StudioPreview'\nimport { StudioSettings } from './StudioSettings'\nimport type { FileItem, StudioMeta } from '../types'\n\ninterface StudioUIProps {\n onClose: () => void\n}\n\nconst styles = {\n container: css`\n display: flex;\n flex-direction: column;\n height: 100%;\n font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `,\n header: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid #e5e7eb;\n `,\n title: css`\n font-size: 20px;\n font-weight: 600;\n color: #111827;\n margin: 0;\n `,\n headerActions: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n closeBtn: css`\n padding: 8px;\n background: none;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n closeIcon: css`\n width: 20px;\n height: 20px;\n color: #6b7280;\n `,\n content: css`\n flex: 1;\n display: flex;\n overflow: hidden;\n `,\n fileBrowser: css`\n flex: 1;\n min-width: 0;\n overflow: auto;\n padding: 16px;\n `,\n}\n\n/**\n * Main Studio UI - contains all panels and manages internal state\n * Rendered inside the modal via lazy loading\n */\nexport function StudioUI({ onClose }: StudioUIProps) {\n const [currentPath, setCurrentPathInternal] = useState('public')\n const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set())\n const [lastSelectedPath, setLastSelectedPath] = useState<string | null>(null)\n const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')\n const [meta, setMeta] = useState<StudioMeta | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [refreshKey, setRefreshKey] = useState(0)\n\n const triggerRefresh = useCallback(() => {\n setRefreshKey((k) => k + 1)\n }, [])\n\n const navigateUp = useCallback(() => {\n if (currentPath === 'public') return\n const parts = currentPath.split('/')\n parts.pop()\n setCurrentPathInternal(parts.join('/') || 'public')\n setSelectedItems(new Set())\n }, [currentPath])\n\n const setCurrentPath = useCallback((path: string) => {\n setCurrentPathInternal(path)\n setSelectedItems(new Set())\n }, [])\n\n const toggleSelection = useCallback((path: string) => {\n setSelectedItems((prev) => {\n const next = new Set(prev)\n if (next.has(path)) {\n next.delete(path)\n } else {\n next.add(path)\n }\n return next\n })\n setLastSelectedPath(path)\n }, [])\n\n const selectRange = useCallback((fromPath: string, toPath: string, allItems: FileItem[]) => {\n // Get only files (not folders)\n const files = allItems.filter(item => item.type !== 'folder')\n const fromIndex = files.findIndex(item => item.path === fromPath)\n const toIndex = files.findIndex(item => item.path === toPath)\n \n if (fromIndex === -1 || toIndex === -1) return\n \n const start = Math.min(fromIndex, toIndex)\n const end = Math.max(fromIndex, toIndex)\n \n setSelectedItems((prev) => {\n const next = new Set(prev)\n for (let i = start; i <= end; i++) {\n next.add(files[i].path)\n }\n return next\n })\n setLastSelectedPath(toPath)\n }, [])\n\n const selectAll = useCallback((items: FileItem[]) => {\n setSelectedItems(new Set(items.map((item) => item.path)))\n }, [])\n\n const clearSelection = useCallback(() => {\n setSelectedItems(new Set())\n }, [])\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n onClose()\n }\n },\n [onClose]\n )\n\n useEffect(() => {\n document.addEventListener('keydown', handleKeyDown)\n document.body.style.overflow = 'hidden'\n return () => {\n document.removeEventListener('keydown', handleKeyDown)\n document.body.style.overflow = ''\n }\n }, [handleKeyDown])\n\n const contextValue = {\n isOpen: true,\n openStudio: () => {},\n closeStudio: onClose,\n toggleStudio: onClose,\n currentPath,\n setCurrentPath,\n navigateUp,\n selectedItems,\n toggleSelection,\n selectRange,\n selectAll,\n clearSelection,\n lastSelectedPath,\n viewMode,\n setViewMode,\n meta,\n setMeta,\n isLoading,\n setIsLoading,\n refreshKey,\n triggerRefresh,\n }\n\n return (\n <StudioContext.Provider value={contextValue}>\n <div css={styles.container}>\n <div css={styles.header}>\n <h1 css={styles.title}>Studio</h1>\n <div css={styles.headerActions}>\n <StudioSettings />\n <button\n css={styles.closeBtn}\n onClick={onClose}\n aria-label=\"Close Studio\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n\n <StudioToolbar />\n <StudioBreadcrumb />\n\n <div css={styles.content}>\n <div css={styles.fileBrowser}>\n {viewMode === 'grid' ? <StudioFileGrid /> : <StudioFileList />}\n </div>\n <StudioPreview />\n </div>\n </div>\n </StudioContext.Provider>\n )\n}\n\nfunction CloseIcon() {\n return (\n <svg\n css={styles.closeIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nexport default StudioUI\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { FileItem, StudioMeta } from '../types'\n\n/**\n * Studio state interface\n * State is managed by StudioUI and provided to all child components\n */\nexport interface StudioState {\n isOpen: boolean\n openStudio: () => void\n closeStudio: () => void\n toggleStudio: () => void\n\n // Navigation\n currentPath: string\n setCurrentPath: (path: string) => void\n navigateUp: () => void\n\n // Selection\n selectedItems: Set<string>\n toggleSelection: (path: string) => void\n selectRange: (fromPath: string, toPath: string, allItems: FileItem[]) => void\n selectAll: (items: FileItem[]) => void\n clearSelection: () => void\n lastSelectedPath: string | null\n\n // View\n viewMode: 'grid' | 'list'\n setViewMode: (mode: 'grid' | 'list') => void\n\n // Meta\n meta: StudioMeta | null\n setMeta: (meta: StudioMeta) => void\n\n // Loading\n isLoading: boolean\n setIsLoading: (loading: boolean) => void\n\n // Refresh trigger\n refreshKey: number\n triggerRefresh: () => void\n}\n\nconst defaultState: StudioState = {\n isOpen: false,\n openStudio: () => {},\n closeStudio: () => {},\n toggleStudio: () => {},\n currentPath: 'public',\n setCurrentPath: () => {},\n navigateUp: () => {},\n selectedItems: new Set(),\n toggleSelection: () => {},\n selectRange: () => {},\n selectAll: () => {},\n clearSelection: () => {},\n lastSelectedPath: null,\n viewMode: 'grid',\n setViewMode: () => {},\n meta: null,\n setMeta: () => {},\n isLoading: false,\n setIsLoading: () => {},\n refreshKey: 0,\n triggerRefresh: () => {},\n}\n\nexport const StudioContext = createContext<StudioState>(defaultState)\n\n/**\n * Hook to access Studio state from child components\n */\nexport function useStudio() {\n return useContext(StudioContext)\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useCallback, useRef, useState } from 'react'\nimport { css } from '@emotion/react'\nimport { useStudio } from './StudioContext'\n\nconst styles = {\n toolbar: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background-color: #f9fafb;\n border-bottom: 1px solid #e5e7eb;\n `,\n left: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n right: css`\n display: flex;\n align-items: center;\n gap: 16px;\n `,\n btn: css`\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-radius: 8px;\n font-size: 14px;\n font-weight: 500;\n background: none;\n border: none;\n cursor: pointer;\n transition: background-color 0.15s;\n \n &:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n }\n `,\n btnDefault: css`\n color: #374151;\n \n &:hover:not(:disabled) {\n background-color: white;\n }\n `,\n btnDanger: css`\n color: #dc2626;\n \n &:hover:not(:disabled) {\n background-color: #fef2f2;\n }\n `,\n icon: css`\n width: 16px;\n height: 16px;\n `,\n selectionCount: css`\n font-size: 14px;\n color: #4b5563;\n `,\n clearBtn: css`\n margin-left: 8px;\n color: #9333ea;\n background: none;\n border: none;\n cursor: pointer;\n font-size: 14px;\n \n &:hover {\n text-decoration: underline;\n }\n `,\n viewToggle: css`\n display: flex;\n align-items: center;\n background-color: white;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow: hidden;\n `,\n viewBtn: css`\n padding: 8px;\n background: none;\n border: none;\n cursor: pointer;\n color: #6b7280;\n transition: all 0.15s;\n \n &:hover {\n background-color: #f9fafb;\n }\n `,\n viewBtnActive: css`\n background-color: #f3e8ff;\n color: #7c3aed;\n `,\n}\n\nexport function StudioToolbar() {\n const { selectedItems, viewMode, setViewMode, clearSelection, currentPath, triggerRefresh } = useStudio()\n const fileInputRef = useRef<HTMLInputElement>(null)\n const [uploading, setUploading] = useState(false)\n\n const handleUpload = useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files\n if (!files || files.length === 0) return\n\n setUploading(true)\n try {\n for (const file of Array.from(files)) {\n const formData = new FormData()\n formData.append('file', file)\n formData.append('path', currentPath)\n\n const response = await fetch('/api/studio/upload', {\n method: 'POST',\n body: formData,\n })\n\n if (!response.ok) {\n const error = await response.json()\n console.error('Upload failed:', error)\n alert(`Failed to upload ${file.name}: ${error.error || 'Unknown error'}`)\n }\n }\n triggerRefresh()\n } catch (error) {\n console.error('Upload error:', error)\n alert('Upload failed. Check console for details.')\n } finally {\n setUploading(false)\n // Reset input so same file can be uploaded again\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n }, [currentPath, triggerRefresh])\n\n const handleReprocess = useCallback(() => {\n console.log('Reprocess clicked', selectedItems)\n }, [selectedItems])\n\n const handleDelete = useCallback(async () => {\n if (selectedItems.size === 0) return\n if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return\n\n try {\n const response = await fetch('/api/studio/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ paths: Array.from(selectedItems) }),\n })\n\n if (response.ok) {\n clearSelection()\n triggerRefresh()\n } else {\n const error = await response.json()\n alert(`Delete failed: ${error.error || 'Unknown error'}`)\n }\n } catch (error) {\n console.error('Delete error:', error)\n alert('Delete failed. Check console for details.')\n }\n }, [selectedItems, clearSelection, triggerRefresh])\n\n const handleSyncCdn = useCallback(() => {\n console.log('Sync CDN clicked', selectedItems)\n }, [selectedItems])\n\n const handleScan = useCallback(() => {\n console.log('Scan clicked')\n }, [])\n\n const hasSelection = selectedItems.size > 0\n\n return (\n <div css={styles.toolbar}>\n {/* Hidden file input for upload */}\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept=\"image/*\"\n onChange={handleFileChange}\n style={{ display: 'none' }}\n />\n \n <div css={styles.left}>\n <ToolbarButton \n onClick={handleUpload} \n icon=\"upload\" \n label={uploading ? 'Uploading...' : 'Upload'} \n disabled={uploading}\n />\n <ToolbarButton\n onClick={handleReprocess}\n icon=\"refresh\"\n label=\"Reprocess\"\n disabled={!hasSelection}\n />\n <ToolbarButton\n onClick={handleDelete}\n icon=\"trash\"\n label=\"Delete\"\n disabled={!hasSelection}\n variant=\"danger\"\n />\n <ToolbarButton\n onClick={handleSyncCdn}\n icon=\"cloud\"\n label=\"Sync CDN\"\n disabled={!hasSelection}\n />\n <ToolbarButton onClick={handleScan} icon=\"scan\" label=\"Scan\" />\n </div>\n\n <div css={styles.right}>\n {hasSelection && (\n <span css={styles.selectionCount}>\n {selectedItems.size} selected\n <button css={styles.clearBtn} onClick={clearSelection}>\n Clear\n </button>\n </span>\n )}\n\n <div css={styles.viewToggle}>\n <button\n css={[styles.viewBtn, viewMode === 'grid' && styles.viewBtnActive]}\n onClick={() => setViewMode('grid')}\n aria-label=\"Grid view\"\n >\n <GridIcon />\n </button>\n <button\n css={[styles.viewBtn, viewMode === 'list' && styles.viewBtnActive]}\n onClick={() => setViewMode('list')}\n aria-label=\"List view\"\n >\n <ListIcon />\n </button>\n </div>\n </div>\n </div>\n )\n}\n\ninterface ToolbarButtonProps {\n onClick: () => void\n icon: 'upload' | 'refresh' | 'trash' | 'cloud' | 'scan'\n label: string\n disabled?: boolean\n variant?: 'default' | 'danger'\n}\n\nfunction ToolbarButton({\n onClick,\n icon,\n label,\n disabled,\n variant = 'default',\n}: ToolbarButtonProps) {\n return (\n <button\n css={[styles.btn, variant === 'danger' ? styles.btnDanger : styles.btnDefault]}\n onClick={onClick}\n disabled={disabled}\n >\n <IconComponent icon={icon} />\n {label}\n </button>\n )\n}\n\nfunction IconComponent({ icon }: { icon: string }) {\n switch (icon) {\n case 'upload':\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n )\n case 'refresh':\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n )\n case 'trash':\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n )\n case 'cloud':\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n )\n case 'scan':\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n )\n default:\n return null\n }\n}\n\nfunction GridIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\" />\n </svg>\n )\n}\n\nfunction ListIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6h16M4 10h16M4 14h16M4 18h16\" />\n </svg>\n )\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { css } from '@emotion/react'\nimport { useStudio } from './StudioContext'\n\nconst styles = {\n container: css`\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 24px;\n background-color: white;\n border-bottom: 1px solid #f3f4f6;\n `,\n backBtn: css`\n padding: 4px;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition: background-color 0.15s;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n backIcon: css`\n width: 16px;\n height: 16px;\n color: #6b7280;\n `,\n nav: css`\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 14px;\n `,\n item: css`\n display: flex;\n align-items: center;\n gap: 4px;\n `,\n separator: css`\n color: #d1d5db;\n `,\n btn: css`\n padding: 2px 4px;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.15s;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n btnActive: css`\n color: #111827;\n font-weight: 500;\n `,\n btnInactive: css`\n color: #6b7280;\n \n &:hover {\n color: #374151;\n }\n `,\n}\n\nexport function StudioBreadcrumb() {\n const { currentPath, setCurrentPath, navigateUp } = useStudio()\n\n const parts = currentPath.split('/').filter(Boolean)\n\n const handleClick = (index: number) => {\n const newPath = parts.slice(0, index + 1).join('/')\n setCurrentPath(newPath)\n }\n\n return (\n <div css={styles.container}>\n {currentPath !== 'public' && (\n <button css={styles.backBtn} onClick={navigateUp} aria-label=\"Go back\">\n <svg css={styles.backIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n )}\n\n <nav css={styles.nav}>\n {parts.map((part, index) => (\n <span key={index} css={styles.item}>\n {index > 0 && <span css={styles.separator}>/</span>}\n <button\n css={[styles.btn, index === parts.length - 1 ? styles.btnActive : styles.btnInactive]}\n onClick={() => handleClick(index)}\n >\n {part}\n </button>\n </span>\n ))}\n </nav>\n </div>\n )\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useState } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport type { FileItem } from '../types'\n\nconst spin = keyframes`\n to { transform: rotate(360deg); }\n`\n\nconst styles = {\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 256px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-bottom-color: #9333ea;\n animation: ${spin} 1s linear infinite;\n `,\n empty: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 256px;\n color: #6b7280;\n `,\n emptyIcon: css`\n width: 48px;\n height: 48px;\n margin-bottom: 16px;\n `,\n emptyText: css`\n font-size: 14px;\n margin: 0;\n `,\n grid: css`\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 16px;\n \n @media (min-width: 640px) { grid-template-columns: repeat(3, 1fr); }\n @media (min-width: 768px) { grid-template-columns: repeat(4, 1fr); }\n @media (min-width: 1024px) { grid-template-columns: repeat(5, 1fr); }\n @media (min-width: 1280px) { grid-template-columns: repeat(6, 1fr); }\n `,\n item: css`\n position: relative;\n border-radius: 8px;\n border: 2px solid transparent;\n overflow: hidden;\n cursor: pointer;\n transition: all 0.15s;\n background-color: #f9fafb;\n \n &:hover {\n border-color: #e5e7eb;\n }\n `,\n itemSelected: css`\n border-color: #a855f7;\n background-color: #faf5ff;\n `,\n checkbox: css`\n position: absolute;\n top: 8px;\n left: 8px;\n z-index: 10;\n width: 16px;\n height: 16px;\n accent-color: #9333ea;\n `,\n cdnBadge: css`\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 10;\n background-color: #dcfce7;\n color: #15803d;\n font-size: 12px;\n padding: 2px 6px;\n border-radius: 9999px;\n `,\n content: css`\n aspect-ratio: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n `,\n folderIcon: css`\n width: 64px;\n height: 64px;\n color: #facc15;\n `,\n image: css`\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: 4px;\n `,\n label: css`\n padding: 6px 8px;\n background-color: white;\n border-top: 1px solid #e5e7eb;\n `,\n name: css`\n font-size: 12px;\n color: #374151;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n margin: 0;\n `,\n size: css`\n font-size: 12px;\n color: #9ca3af;\n margin: 0;\n `,\n selectAllRow: css`\n display: flex;\n align-items: center;\n margin-bottom: 12px;\n padding-bottom: 12px;\n border-bottom: 1px solid #e5e7eb;\n `,\n selectAllLabel: css`\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n color: #6b7280;\n cursor: pointer;\n \n &:hover {\n color: #374151;\n }\n `,\n selectAllCheckbox: css`\n width: 16px;\n height: 16px;\n accent-color: #9333ea;\n `,\n}\n\nexport function StudioFileGrid() {\n const { currentPath, setCurrentPath, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey } = useStudio()\n const [items, setItems] = useState<FileItem[]>([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n async function loadItems() {\n setLoading(true)\n try {\n const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`)\n if (response.ok) {\n const data = await response.json()\n setItems(data.items || [])\n }\n } catch (error) {\n console.error('Failed to load items:', error)\n }\n setLoading(false)\n }\n loadItems()\n }, [currentPath, refreshKey])\n\n if (loading) {\n return (\n <div css={styles.loading}>\n <div css={styles.spinner} />\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div css={styles.empty}>\n <svg css={styles.emptyIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n <p css={styles.emptyText}>No files in this folder</p>\n <p css={styles.emptyText}>Upload images to get started</p>\n </div>\n )\n }\n\n const sortedItems = [...items].sort((a, b) => {\n if (a.type === 'folder' && b.type !== 'folder') return -1\n if (a.type !== 'folder' && b.type === 'folder') return 1\n return a.name.localeCompare(b.name)\n })\n\n const files = sortedItems.filter(item => item.type !== 'folder')\n const allFilesSelected = files.length > 0 && files.every(item => selectedItems.has(item.path))\n const someFilesSelected = files.some(item => selectedItems.has(item.path))\n\n const handleSelectAll = () => {\n if (allFilesSelected) {\n clearSelection()\n } else {\n selectAll(files)\n }\n }\n\n const handleItemClick = (item: FileItem, e: React.MouseEvent) => {\n if (item.type === 'folder') {\n setCurrentPath(item.path)\n return\n }\n\n if (e.shiftKey && lastSelectedPath) {\n selectRange(lastSelectedPath, item.path, sortedItems)\n } else {\n toggleSelection(item.path)\n }\n }\n\n return (\n <div>\n {files.length > 0 && (\n <div css={styles.selectAllRow}>\n <label css={styles.selectAllLabel}>\n <input\n type=\"checkbox\"\n css={styles.selectAllCheckbox}\n checked={allFilesSelected}\n ref={(el) => {\n if (el) el.indeterminate = someFilesSelected && !allFilesSelected\n }}\n onChange={handleSelectAll}\n />\n Select all ({files.length})\n </label>\n </div>\n )}\n <div css={styles.grid}>\n {sortedItems.map((item) => (\n <GridItem\n key={item.path}\n item={item}\n isSelected={selectedItems.has(item.path)}\n onClick={(e) => handleItemClick(item, e)}\n />\n ))}\n </div>\n </div>\n )\n}\n\ninterface GridItemProps {\n item: FileItem\n isSelected: boolean\n onClick: (e: React.MouseEvent) => void\n}\n\nfunction GridItem({ item, isSelected, onClick }: GridItemProps) {\n const isFolder = item.type === 'folder'\n\n return (\n <div css={[styles.item, isSelected && styles.itemSelected]} onClick={onClick}>\n {/* Only show checkbox for files, not folders */}\n {!isFolder && (\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={isSelected}\n onChange={() => {}}\n onClick={(e) => e.stopPropagation()}\n />\n )}\n\n {item.cdnSynced && <span css={styles.cdnBadge}>CDN</span>}\n\n <div css={styles.content}>\n {isFolder ? (\n <svg css={styles.folderIcon} fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n ) : (\n <img\n css={styles.image}\n src={item.path.replace('public', '')}\n alt={item.name}\n loading=\"lazy\"\n />\n )}\n </div>\n\n <div css={styles.label}>\n <p css={styles.name} title={item.name}>{item.name}</p>\n {item.size && <p css={styles.size}>{formatFileSize(item.size)}</p>}\n </div>\n </div>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useState } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport type { FileItem } from '../types'\n\nconst spin = keyframes`\n to { transform: rotate(360deg); }\n`\n\nconst styles = {\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 256px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-bottom-color: #9333ea;\n animation: ${spin} 1s linear infinite;\n `,\n empty: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 256px;\n color: #6b7280;\n `,\n table: css`\n width: 100%;\n border-collapse: collapse;\n `,\n th: css`\n text-align: left;\n font-size: 12px;\n color: #6b7280;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding-bottom: 8px;\n font-weight: normal;\n `,\n thCheckbox: css`\n width: 32px;\n `,\n thSize: css`\n width: 96px;\n `,\n thDimensions: css`\n width: 128px;\n `,\n thCdn: css`\n width: 96px;\n `,\n tbody: css`\n border-top: 1px solid #f3f4f6;\n `,\n row: css`\n cursor: pointer;\n transition: background-color 0.15s;\n \n &:hover {\n background-color: #f9fafb;\n }\n `,\n rowSelected: css`\n background-color: #faf5ff;\n `,\n td: css`\n padding: 8px 0;\n border-bottom: 1px solid #f3f4f6;\n `,\n checkbox: css`\n width: 16px;\n height: 16px;\n accent-color: #9333ea;\n `,\n nameCell: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n folderIcon: css`\n width: 20px;\n height: 20px;\n color: #facc15;\n `,\n fileIcon: css`\n width: 20px;\n height: 20px;\n color: #9ca3af;\n `,\n name: css`\n font-size: 14px;\n color: #111827;\n `,\n meta: css`\n font-size: 14px;\n color: #6b7280;\n `,\n cdnBadge: css`\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n color: #15803d;\n `,\n cdnIcon: css`\n width: 12px;\n height: 12px;\n `,\n cdnEmpty: css`\n font-size: 12px;\n color: #9ca3af;\n `,\n}\n\nexport function StudioFileList() {\n const { currentPath, setCurrentPath, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey } = useStudio()\n const [items, setItems] = useState<FileItem[]>([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n async function loadItems() {\n setLoading(true)\n try {\n const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`)\n if (response.ok) {\n const data = await response.json()\n setItems(data.items || [])\n }\n } catch (error) {\n console.error('Failed to load items:', error)\n }\n setLoading(false)\n }\n loadItems()\n }, [currentPath, refreshKey])\n\n if (loading) {\n return (\n <div css={styles.loading}>\n <div css={styles.spinner} />\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div css={styles.empty}>\n <p>No files in this folder</p>\n </div>\n )\n }\n\n const sortedItems = [...items].sort((a, b) => {\n if (a.type === 'folder' && b.type !== 'folder') return -1\n if (a.type !== 'folder' && b.type === 'folder') return 1\n return a.name.localeCompare(b.name)\n })\n\n const files = sortedItems.filter(item => item.type !== 'folder')\n const allFilesSelected = files.length > 0 && files.every(item => selectedItems.has(item.path))\n const someFilesSelected = files.some(item => selectedItems.has(item.path))\n\n const handleSelectAll = () => {\n if (allFilesSelected) {\n clearSelection()\n } else {\n selectAll(files)\n }\n }\n\n const handleItemClick = (item: FileItem, e: React.MouseEvent) => {\n if (item.type === 'folder') {\n setCurrentPath(item.path)\n return\n }\n\n if (e.shiftKey && lastSelectedPath) {\n selectRange(lastSelectedPath, item.path, sortedItems)\n } else {\n toggleSelection(item.path)\n }\n }\n\n return (\n <table css={styles.table}>\n <thead>\n <tr>\n <th css={[styles.th, styles.thCheckbox]}>\n {files.length > 0 && (\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={allFilesSelected}\n ref={(el) => {\n if (el) el.indeterminate = someFilesSelected && !allFilesSelected\n }}\n onChange={handleSelectAll}\n />\n )}\n </th>\n <th css={styles.th}>Name</th>\n <th css={[styles.th, styles.thSize]}>Size</th>\n <th css={[styles.th, styles.thDimensions]}>Dimensions</th>\n <th css={[styles.th, styles.thCdn]}>CDN</th>\n </tr>\n </thead>\n <tbody css={styles.tbody}>\n {sortedItems.map((item) => (\n <ListRow\n key={item.path}\n item={item}\n isSelected={selectedItems.has(item.path)}\n onClick={(e) => handleItemClick(item, e)}\n />\n ))}\n </tbody>\n </table>\n )\n}\n\ninterface ListRowProps {\n item: FileItem\n isSelected: boolean\n onClick: (e: React.MouseEvent) => void\n}\n\nfunction ListRow({ item, isSelected, onClick }: ListRowProps) {\n const isFolder = item.type === 'folder'\n\n return (\n <tr css={[styles.row, isSelected && styles.rowSelected]} onClick={onClick}>\n <td css={styles.td}>\n {/* Only show checkbox for files, not folders */}\n {!isFolder && (\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={isSelected}\n onChange={() => {}}\n onClick={(e) => e.stopPropagation()}\n />\n )}\n </td>\n <td css={styles.td}>\n <div css={styles.nameCell}>\n {isFolder ? (\n <svg css={styles.folderIcon} fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n ) : (\n <svg css={styles.fileIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n )}\n <span css={styles.name}>{item.name}</span>\n </div>\n </td>\n <td css={[styles.td, styles.meta]}>\n {item.size ? formatFileSize(item.size) : '--'}\n </td>\n <td css={[styles.td, styles.meta]}>\n {item.dimensions ? `${item.dimensions.width}x${item.dimensions.height}` : '--'}\n </td>\n <td css={styles.td}>\n {item.cdnSynced ? (\n <span css={styles.cdnBadge}>\n <svg css={styles.cdnIcon} fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fillRule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clipRule=\"evenodd\" />\n </svg>\n Synced\n </span>\n ) : (\n <span css={styles.cdnEmpty}>--</span>\n )}\n </td>\n </tr>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { css } from '@emotion/react'\nimport { useStudio } from './StudioContext'\n\nconst styles = {\n panel: css`\n width: 320px;\n border-left: 1px solid #e5e7eb;\n background-color: #f9fafb;\n padding: 16px;\n overflow: auto;\n `,\n title: css`\n font-size: 14px;\n font-weight: 500;\n color: #111827;\n margin: 0 0 16px 0;\n `,\n imageContainer: css`\n background-color: white;\n border-radius: 8px;\n border: 1px solid #e5e7eb;\n padding: 8px;\n margin-bottom: 16px;\n `,\n image: css`\n width: 100%;\n height: auto;\n border-radius: 4px;\n `,\n info: css`\n display: flex;\n flex-direction: column;\n gap: 12px;\n `,\n row: css`\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n `,\n label: css`\n color: #6b7280;\n `,\n value: css`\n color: #111827;\n `,\n valueTruncate: css`\n max-width: 128px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n `,\n section: css`\n padding-top: 8px;\n border-top: 1px solid #e5e7eb;\n `,\n sectionTitle: css`\n font-size: 12px;\n font-weight: 500;\n color: #6b7280;\n margin: 0 0 8px 0;\n `,\n cdnStatus: css`\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: #16a34a;\n `,\n cdnIcon: css`\n width: 16px;\n height: 16px;\n `,\n copyBtn: css`\n margin-top: 8px;\n font-size: 12px;\n color: #9333ea;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n \n &:hover {\n text-decoration: underline;\n }\n `,\n colorSwatch: css`\n margin-top: 8px;\n height: 32px;\n border-radius: 4px;\n `,\n emptyState: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 200px;\n `,\n emptyText: css`\n font-size: 14px;\n color: #9ca3af;\n margin: 0;\n `,\n actions: css`\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid #e5e7eb;\n display: flex;\n flex-direction: column;\n gap: 8px;\n `,\n actionBtn: css`\n width: 100%;\n padding: 8px 12px;\n font-size: 14px;\n background-color: white;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s;\n color: #374151;\n \n &:hover {\n background-color: #f9fafb;\n }\n `,\n actionBtnDanger: css`\n color: #dc2626;\n \n &:hover {\n background-color: #fef2f2;\n }\n `,\n}\n\nexport function StudioPreview() {\n const { selectedItems, meta, triggerRefresh, clearSelection } = useStudio()\n\n const handleDelete = async () => {\n if (selectedItems.size === 0) return\n if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return\n\n try {\n const response = await fetch('/api/studio/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ paths: Array.from(selectedItems) }),\n })\n\n if (response.ok) {\n clearSelection()\n triggerRefresh()\n } else {\n const error = await response.json()\n alert(`Delete failed: ${error.error || 'Unknown error'}`)\n }\n } catch (error) {\n console.error('Delete error:', error)\n alert('Delete failed. Check console for details.')\n }\n }\n\n // Always show the sidebar\n if (selectedItems.size === 0) {\n return (\n <div css={styles.panel}>\n <h3 css={styles.title}>Preview</h3>\n <div css={styles.emptyState}>\n <p css={styles.emptyText}>Select an image to preview</p>\n </div>\n </div>\n )\n }\n\n if (selectedItems.size > 1) {\n return (\n <div css={styles.panel}>\n <h3 css={styles.title}>{selectedItems.size} items selected</h3>\n <div css={styles.actions}>\n <button css={[styles.actionBtn, styles.actionBtnDanger]} onClick={handleDelete}>\n Delete {selectedItems.size} items\n </button>\n </div>\n </div>\n )\n }\n\n const selectedPath = Array.from(selectedItems)[0]\n const imageKey = selectedPath\n .replace(/^public\\/images\\//, '')\n .replace(/^public\\/originals\\//, '')\n\n const imageData = meta?.images?.[imageKey]\n\n return (\n <div css={styles.panel}>\n <h3 css={styles.title}>Preview</h3>\n\n <div css={styles.imageContainer}>\n <img\n css={styles.image}\n src={selectedPath.replace('public', '')}\n alt=\"Preview\"\n />\n </div>\n\n <div css={styles.info}>\n <InfoRow label=\"Filename\" value={selectedPath.split('/').pop() || ''} />\n\n {imageData && (\n <>\n <InfoRow\n label=\"Original\"\n value={`${imageData.original.width}x${imageData.original.height}`}\n />\n <InfoRow\n label=\"File size\"\n value={formatFileSize(imageData.original.fileSize)}\n />\n\n <div css={styles.section}>\n <p css={styles.sectionTitle}>Generated sizes</p>\n {Object.entries(imageData.sizes).map(([size, data]) => (\n <InfoRow key={size} label={size} value={`${data.width}x${data.height}`} />\n ))}\n </div>\n\n {imageData.cdn?.synced && (\n <div css={styles.section}>\n <p css={styles.sectionTitle}>CDN</p>\n <div css={styles.cdnStatus}>\n <svg css={styles.cdnIcon} fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fillRule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clipRule=\"evenodd\" />\n </svg>\n Synced to CDN\n </div>\n <button\n css={styles.copyBtn}\n onClick={() => {\n navigator.clipboard.writeText(`${imageData.cdn?.baseUrl}${imageData.sizes.full.path}`)\n }}\n >\n Copy CDN URL\n </button>\n </div>\n )}\n\n {imageData.blurhash && (\n <div css={styles.section}>\n <InfoRow label=\"Blurhash\" value={imageData.blurhash} truncate />\n <div\n css={styles.colorSwatch}\n style={{ backgroundColor: imageData.dominantColor }}\n title={`Dominant color: ${imageData.dominantColor}`}\n />\n </div>\n )}\n </>\n )}\n </div>\n\n <div css={styles.actions}>\n <button css={styles.actionBtn}>Rename</button>\n <button css={[styles.actionBtn, styles.actionBtnDanger]} onClick={handleDelete}>Delete</button>\n </div>\n </div>\n )\n}\n\nfunction InfoRow({ label, value, truncate }: { label: string; value: string; truncate?: boolean }) {\n return (\n <div css={styles.row}>\n <span css={styles.label}>{label}</span>\n <span css={[styles.value, truncate && styles.valueTruncate]} title={truncate ? value : undefined}>\n {value}\n </span>\n </div>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState } from 'react'\nimport { css } from '@emotion/react'\n\nconst styles = {\n btn: css`\n padding: 8px;\n background: none;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n icon: css`\n width: 20px;\n height: 20px;\n color: #6b7280;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(0, 0, 0, 0.3);\n `,\n panel: css`\n position: relative;\n background-color: white;\n border-radius: 12px;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);\n width: 100%;\n max-width: 512px;\n padding: 24px;\n `,\n header: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 24px;\n `,\n title: css`\n font-size: 18px;\n font-weight: 600;\n margin: 0;\n `,\n closeBtn: css`\n padding: 4px;\n background: none;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n sections: css`\n display: flex;\n flex-direction: column;\n gap: 24px;\n `,\n sectionTitle: css`\n font-size: 14px;\n font-weight: 500;\n color: #111827;\n margin: 0 0 12px 0;\n `,\n description: css`\n font-size: 12px;\n color: #6b7280;\n margin: 0 0 12px 0;\n `,\n code: css`\n background-color: #f9fafb;\n border-radius: 8px;\n padding: 12px;\n font-family: monospace;\n font-size: 12px;\n color: #4b5563;\n `,\n codeLine: css`\n margin: 0 0 4px 0;\n \n &:last-child {\n margin: 0;\n }\n `,\n input: css`\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n font-size: 14px;\n \n &:focus {\n outline: none;\n box-shadow: 0 0 0 2px #a855f7;\n }\n `,\n grid: css`\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 12px;\n `,\n label: css`\n font-size: 12px;\n color: #6b7280;\n display: block;\n margin-bottom: 4px;\n `,\n footer: css`\n margin-top: 24px;\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n `,\n cancelBtn: css`\n padding: 8px 16px;\n font-size: 14px;\n color: #4b5563;\n background: none;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n \n &:hover {\n background-color: #f3f4f6;\n }\n `,\n saveBtn: css`\n padding: 8px 16px;\n font-size: 14px;\n color: white;\n background-color: #9333ea;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n \n &:hover {\n background-color: #7c3aed;\n }\n `,\n}\n\nexport function StudioSettings() {\n const [isOpen, setIsOpen] = useState(false)\n\n return (\n <>\n <button css={styles.btn} onClick={() => setIsOpen(true)} aria-label=\"Settings\">\n <svg\n css={styles.icon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z\" />\n </svg>\n </button>\n\n {isOpen && <SettingsPanel onClose={() => setIsOpen(false)} />}\n </>\n )\n}\n\nfunction SettingsPanel({ onClose }: { onClose: () => void }) {\n return (\n <div css={styles.overlay}>\n <div css={styles.backdrop} onClick={onClose} />\n\n <div css={styles.panel}>\n <div css={styles.header}>\n <h2 css={styles.title}>Settings</h2>\n <button css={styles.closeBtn} onClick={onClose}>\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div css={styles.sections}>\n <section>\n <h3 css={styles.sectionTitle}>Cloudflare R2</h3>\n <p css={styles.description}>Configure in .env.local file:</p>\n <div css={styles.code}>\n <p css={styles.codeLine}>CLOUDFLARE_R2_ACCOUNT_ID</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_ACCESS_KEY_ID</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_SECRET_ACCESS_KEY</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_BUCKET_NAME</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_PUBLIC_URL</p>\n </div>\n </section>\n\n <section>\n <h3 css={styles.sectionTitle}>Custom CDN URL</h3>\n <p css={styles.description}>Override the default R2 URL with a custom domain:</p>\n <input css={styles.input} type=\"text\" placeholder=\"https://cdn.yourdomain.com\" />\n </section>\n\n <section>\n <h3 css={styles.sectionTitle}>Thumbnail Sizes</h3>\n <div css={styles.grid}>\n <div>\n <label css={styles.label}>Small</label>\n <input css={styles.input} type=\"number\" defaultValue={300} />\n </div>\n <div>\n <label css={styles.label}>Medium</label>\n <input css={styles.input} type=\"number\" defaultValue={700} />\n </div>\n <div>\n <label css={styles.label}>Large</label>\n <input css={styles.input} type=\"number\" defaultValue={1400} />\n </div>\n </div>\n </section>\n </div>\n\n <div css={styles.footer}>\n <button css={styles.cancelBtn} onClick={onClose}>Cancel</button>\n <button css={styles.saveBtn}>Save Changes</button>\n </div>\n </div>\n </div>\n )\n}\n"]}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  var _react = require('react');
5
5
  var _react3 = require('@emotion/react');
6
6
  var _jsxruntime = require('@emotion/react/jsx-runtime');
7
- var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-Z3DOQO7P.js"))));
7
+ var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-ZAD65UPD.js"))));
8
8
  var spin = _react3.keyframes`
9
9
  to {
10
10
  transform: rotate(360deg);
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { useState, useEffect, lazy, Suspense } from "react";
5
5
  import { css, keyframes } from "@emotion/react";
6
6
  import { Fragment, jsx, jsxs } from "@emotion/react/jsx-runtime";
7
- var StudioUI = lazy(() => import("./StudioUI-DSMTGRJ3.mjs"));
7
+ var StudioUI = lazy(() => import("./StudioUI-2CBIV4Q5.mjs"));
8
8
  var spin = keyframes`
9
9
  to {
10
10
  transform: rotate(360deg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallop.software/studio",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Media manager for Gallop templates - upload, process, and sync images to CDN",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",