@gallop.software/studio 0.1.53 → 0.1.55

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.
@@ -64,6 +64,7 @@ import { useCallback, useRef, useState } from "react";
64
64
  import { css as css2, keyframes as keyframes2 } from "@emotion/react";
65
65
 
66
66
  // src/components/StudioModal.tsx
67
+ import React from "react";
67
68
  import { css, keyframes } from "@emotion/react";
68
69
  import { Fragment, jsx, jsxs } from "@emotion/react/jsx-runtime";
69
70
  var fadeIn = keyframes`
@@ -195,6 +196,70 @@ function ConfirmModal({
195
196
  ] })
196
197
  ] }) });
197
198
  }
199
+ var inputStyles = {
200
+ input: css`
201
+ width: 100%;
202
+ padding: 10px 12px;
203
+ font-size: ${fontSize.base};
204
+ border: 1px solid ${colors.border};
205
+ border-radius: 6px;
206
+ background: ${colors.surface};
207
+ color: ${colors.text};
208
+ margin-top: 12px;
209
+ transition: all 0.15s ease;
210
+
211
+ &:focus {
212
+ outline: none;
213
+ border-color: ${colors.primary};
214
+ box-shadow: 0 0 0 2px ${colors.primaryLight};
215
+ }
216
+
217
+ &::placeholder {
218
+ color: ${colors.textMuted};
219
+ }
220
+ `
221
+ };
222
+ function InputModal({
223
+ title,
224
+ message,
225
+ inputLabel,
226
+ defaultValue = "",
227
+ placeholder,
228
+ confirmLabel = "Confirm",
229
+ cancelLabel = "Cancel",
230
+ onConfirm,
231
+ onCancel
232
+ }) {
233
+ const [value, setValue] = React.useState(defaultValue);
234
+ const handleSubmit = (e) => {
235
+ e.preventDefault();
236
+ if (value.trim()) {
237
+ onConfirm(value.trim());
238
+ }
239
+ };
240
+ return /* @__PURE__ */ jsx("div", { css: styles.overlay, onClick: onCancel, children: /* @__PURE__ */ jsx("div", { css: styles.modal, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
241
+ /* @__PURE__ */ jsx("div", { css: styles.header, children: /* @__PURE__ */ jsx("h3", { css: styles.title, children: title }) }),
242
+ /* @__PURE__ */ jsxs("div", { css: styles.body, children: [
243
+ message && /* @__PURE__ */ jsx("p", { css: styles.message, children: message }),
244
+ inputLabel && /* @__PURE__ */ jsx("label", { css: styles.message, children: inputLabel }),
245
+ /* @__PURE__ */ jsx(
246
+ "input",
247
+ {
248
+ css: inputStyles.input,
249
+ type: "text",
250
+ value,
251
+ onChange: (e) => setValue(e.target.value),
252
+ placeholder,
253
+ autoFocus: true
254
+ }
255
+ )
256
+ ] }),
257
+ /* @__PURE__ */ jsxs("div", { css: styles.footer, children: [
258
+ /* @__PURE__ */ jsx("button", { type: "button", css: [styles.btn, styles.btnCancel], onClick: onCancel, children: cancelLabel }),
259
+ /* @__PURE__ */ jsx("button", { type: "submit", css: [styles.btn, styles.btnConfirm], disabled: !value.trim(), children: confirmLabel })
260
+ ] })
261
+ ] }) }) });
262
+ }
198
263
  function AlertModal({
199
264
  title,
200
265
  message,
@@ -440,9 +505,14 @@ var styles2 = {
440
505
  border-radius: 6px;
441
506
  overflow: hidden;
442
507
  `,
508
+ searchWrapper: css2`
509
+ position: relative;
510
+ display: flex;
511
+ align-items: center;
512
+ `,
443
513
  searchInput: css2`
444
514
  height: ${btnHeight};
445
- padding: 0 12px;
515
+ padding: 0 32px 0 12px;
446
516
  border: 1px solid ${colors.border};
447
517
  border-radius: 6px;
448
518
  font-size: ${fontSize.base};
@@ -461,6 +531,26 @@ var styles2 = {
461
531
  color: ${colors.textMuted};
462
532
  }
463
533
  `,
534
+ searchClearBtn: css2`
535
+ position: absolute;
536
+ right: 6px;
537
+ top: 50%;
538
+ transform: translateY(-50%);
539
+ background: ${colors.textMuted};
540
+ border: none;
541
+ padding: 3px;
542
+ cursor: pointer;
543
+ color: white;
544
+ display: flex;
545
+ align-items: center;
546
+ justify-content: center;
547
+ border-radius: 50%;
548
+ transition: all 0.15s ease;
549
+
550
+ &:hover {
551
+ background: ${colors.text};
552
+ }
553
+ `,
464
554
  viewBtn: css2`
465
555
  height: 100%;
466
556
  padding: 0 10px;
@@ -811,6 +901,12 @@ function StudioToolbar() {
811
901
  const handleSearch = useCallback((e) => {
812
902
  setSearchQuery(e.target.value);
813
903
  }, [setSearchQuery]);
904
+ const handleSearchKeyDown = useCallback((e) => {
905
+ if (e.key === "Escape") {
906
+ setSearchQuery("");
907
+ e.target.blur();
908
+ }
909
+ }, [setSearchQuery]);
814
910
  const hasSelection = selectedItems.size > 0;
815
911
  const hasImagesSelected = Array.from(selectedItems).some(
816
912
  (path) => path === "public/images" || path.startsWith("public/images/")
@@ -929,16 +1025,28 @@ function StudioToolbar() {
929
1025
  ]
930
1026
  }
931
1027
  ),
932
- /* @__PURE__ */ jsx2(
933
- "input",
934
- {
935
- css: styles2.searchInput,
936
- type: "text",
937
- placeholder: "Search images...",
938
- value: searchQuery,
939
- onChange: handleSearch
940
- }
941
- )
1028
+ /* @__PURE__ */ jsxs2("div", { css: styles2.searchWrapper, children: [
1029
+ /* @__PURE__ */ jsx2(
1030
+ "input",
1031
+ {
1032
+ css: styles2.searchInput,
1033
+ type: "text",
1034
+ placeholder: "Search images...",
1035
+ value: searchQuery,
1036
+ onChange: handleSearch,
1037
+ onKeyDown: handleSearchKeyDown
1038
+ }
1039
+ ),
1040
+ searchQuery && /* @__PURE__ */ jsx2(
1041
+ "button",
1042
+ {
1043
+ css: styles2.searchClearBtn,
1044
+ onClick: () => setSearchQuery(""),
1045
+ title: "Clear search",
1046
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
1047
+ }
1048
+ )
1049
+ ] })
942
1050
  ] }),
943
1051
  /* @__PURE__ */ jsxs2("div", { css: styles2.right, children: [
944
1052
  hasSelection && /* @__PURE__ */ jsxs2("span", { css: styles2.selectionCount, children: [
@@ -2365,6 +2473,9 @@ var styles5 = {
2365
2473
  function StudioDetailView() {
2366
2474
  const { focusedItem, setFocusedItem, triggerRefresh, clearSelection } = useStudio();
2367
2475
  const [showDeleteConfirm, setShowDeleteConfirm] = useState4(false);
2476
+ const [showRenameModal, setShowRenameModal] = useState4(false);
2477
+ const [showProcessConfirm, setShowProcessConfirm] = useState4(false);
2478
+ const [processProgress, setProcessProgress] = useState4(null);
2368
2479
  const [alertMessage, setAlertMessage] = useState4(null);
2369
2480
  const [showCopied, setShowCopied] = useState4(false);
2370
2481
  if (!focusedItem) return null;
@@ -2380,10 +2491,35 @@ function StudioDetailView() {
2380
2491
  setShowCopied(true);
2381
2492
  setTimeout(() => setShowCopied(false), 1500);
2382
2493
  };
2383
- const handleRename = () => {
2384
- const newName = prompt("Enter new name:", focusedItem.name);
2494
+ const handleRename = async (newName) => {
2495
+ setShowRenameModal(false);
2385
2496
  if (newName && newName !== focusedItem.name) {
2386
- console.log("Rename to:", newName);
2497
+ try {
2498
+ const response = await fetch("/api/studio/rename", {
2499
+ method: "POST",
2500
+ headers: { "Content-Type": "application/json" },
2501
+ body: JSON.stringify({
2502
+ oldPath: focusedItem.path,
2503
+ newName
2504
+ })
2505
+ });
2506
+ if (response.ok) {
2507
+ triggerRefresh();
2508
+ setFocusedItem(null);
2509
+ } else {
2510
+ const data = await response.json();
2511
+ setAlertMessage({
2512
+ title: "Rename Failed",
2513
+ message: data.error || "Failed to rename file"
2514
+ });
2515
+ }
2516
+ } catch (error) {
2517
+ console.error("Rename error:", error);
2518
+ setAlertMessage({
2519
+ title: "Rename Failed",
2520
+ message: "An error occurred while renaming the file"
2521
+ });
2522
+ }
2387
2523
  }
2388
2524
  };
2389
2525
  const handleDelete = async () => {
@@ -2416,8 +2552,59 @@ function StudioDetailView() {
2416
2552
  const handleSync = () => {
2417
2553
  console.log("Sync to CDN:", focusedItem.path);
2418
2554
  };
2419
- const handleRegenerate = () => {
2420
- console.log("Regenerate:", focusedItem.path);
2555
+ const handleProcessImage = async () => {
2556
+ setShowProcessConfirm(false);
2557
+ setProcessProgress({
2558
+ current: 0,
2559
+ total: 1,
2560
+ percent: 0,
2561
+ status: "processing",
2562
+ currentFile: focusedItem.name
2563
+ });
2564
+ try {
2565
+ const response = await fetch("/api/studio/reprocess", {
2566
+ method: "POST",
2567
+ headers: { "Content-Type": "application/json" },
2568
+ body: JSON.stringify({
2569
+ paths: [focusedItem.path]
2570
+ })
2571
+ });
2572
+ if (!response.ok) {
2573
+ throw new Error("Processing failed");
2574
+ }
2575
+ const reader = response.body?.getReader();
2576
+ if (!reader) {
2577
+ throw new Error("No response body");
2578
+ }
2579
+ const decoder = new TextDecoder();
2580
+ let buffer = "";
2581
+ while (true) {
2582
+ const { done, value } = await reader.read();
2583
+ if (done) break;
2584
+ buffer += decoder.decode(value, { stream: true });
2585
+ const lines = buffer.split("\n");
2586
+ buffer = lines.pop() || "";
2587
+ for (const line of lines) {
2588
+ if (line.startsWith("data: ")) {
2589
+ try {
2590
+ const data = JSON.parse(line.slice(6));
2591
+ setProcessProgress(data);
2592
+ } catch {
2593
+ }
2594
+ }
2595
+ }
2596
+ }
2597
+ triggerRefresh();
2598
+ } catch (error) {
2599
+ console.error("Process error:", error);
2600
+ setProcessProgress({
2601
+ current: 0,
2602
+ total: 1,
2603
+ percent: 0,
2604
+ status: "error",
2605
+ message: "Failed to process image"
2606
+ });
2607
+ }
2421
2608
  };
2422
2609
  const renderMedia = () => {
2423
2610
  if (isImage) {
@@ -2451,6 +2638,36 @@ function StudioDetailView() {
2451
2638
  onClose: () => setAlertMessage(null)
2452
2639
  }
2453
2640
  ),
2641
+ showRenameModal && /* @__PURE__ */ jsx5(
2642
+ InputModal,
2643
+ {
2644
+ title: "Rename File",
2645
+ message: "Enter a new name for the file:",
2646
+ defaultValue: focusedItem.name,
2647
+ placeholder: "Enter new filename",
2648
+ confirmLabel: "Rename",
2649
+ onConfirm: handleRename,
2650
+ onCancel: () => setShowRenameModal(false)
2651
+ }
2652
+ ),
2653
+ showProcessConfirm && /* @__PURE__ */ jsx5(
2654
+ ConfirmModal,
2655
+ {
2656
+ title: "Process Image",
2657
+ message: `Generate thumbnails for "${focusedItem.name}"?`,
2658
+ confirmLabel: "Process",
2659
+ onConfirm: handleProcessImage,
2660
+ onCancel: () => setShowProcessConfirm(false)
2661
+ }
2662
+ ),
2663
+ processProgress && /* @__PURE__ */ jsx5(
2664
+ ProgressModal,
2665
+ {
2666
+ title: "Processing Image",
2667
+ progress: processProgress,
2668
+ onClose: () => setProcessProgress(null)
2669
+ }
2670
+ ),
2454
2671
  /* @__PURE__ */ jsx5("div", { css: styles5.overlay, onClick: handleClose, children: /* @__PURE__ */ jsxs5("div", { css: styles5.container, onClick: (e) => e.stopPropagation(), children: [
2455
2672
  /* @__PURE__ */ jsxs5("div", { css: styles5.main, children: [
2456
2673
  /* @__PURE__ */ jsxs5("div", { css: styles5.headerButtons, children: [
@@ -2492,7 +2709,7 @@ function StudioDetailView() {
2492
2709
  ] })
2493
2710
  ] }),
2494
2711
  /* @__PURE__ */ jsxs5("div", { css: styles5.actions, children: [
2495
- /* @__PURE__ */ jsxs5("button", { css: styles5.actionBtn, onClick: handleRename, children: [
2712
+ /* @__PURE__ */ jsxs5("button", { css: styles5.actionBtn, onClick: () => setShowRenameModal(true), children: [
2496
2713
  /* @__PURE__ */ jsx5("svg", { css: styles5.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }) }),
2497
2714
  "Rename"
2498
2715
  ] }),
@@ -2500,9 +2717,9 @@ function StudioDetailView() {
2500
2717
  /* @__PURE__ */ jsx5("svg", { css: styles5.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("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" }) }),
2501
2718
  "Sync to CDN"
2502
2719
  ] }),
2503
- /* @__PURE__ */ jsxs5("button", { css: styles5.actionBtn, onClick: handleRegenerate, children: [
2504
- /* @__PURE__ */ jsx5("svg", { css: styles5.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("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" }) }),
2505
- "Regenerate"
2720
+ /* @__PURE__ */ jsxs5("button", { css: styles5.actionBtn, onClick: () => setShowProcessConfirm(true), children: [
2721
+ /* @__PURE__ */ jsx5("svg", { css: styles5.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("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" }) }),
2722
+ "Process Image"
2506
2723
  ] }),
2507
2724
  /* @__PURE__ */ jsxs5("button", { css: [styles5.actionBtn, styles5.actionBtnDanger], onClick: () => setShowDeleteConfirm(true), children: [
2508
2725
  /* @__PURE__ */ jsx5("svg", { css: styles5.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("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" }) }),
@@ -3139,4 +3356,4 @@ export {
3139
3356
  StudioUI,
3140
3357
  StudioUI_default as default
3141
3358
  };
3142
- //# sourceMappingURL=StudioUI-IULAQEIK.mjs.map
3359
+ //# sourceMappingURL=StudioUI-5QGFJQXF.mjs.map