@gallop.software/studio 0.1.0

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.
@@ -0,0 +1,991 @@
1
+ "use client";
2
+
3
+ // src/components/StudioUI.tsx
4
+ import { useEffect as useEffect3, useCallback as useCallback2, useState as useState4 } from "react";
5
+
6
+ // src/components/StudioContext.tsx
7
+ import { createContext, useContext } from "react";
8
+ var defaultState = {
9
+ isOpen: false,
10
+ openStudio: () => {
11
+ },
12
+ closeStudio: () => {
13
+ },
14
+ toggleStudio: () => {
15
+ },
16
+ currentPath: "public",
17
+ setCurrentPath: () => {
18
+ },
19
+ navigateUp: () => {
20
+ },
21
+ selectedItems: /* @__PURE__ */ new Set(),
22
+ toggleSelection: () => {
23
+ },
24
+ selectAll: () => {
25
+ },
26
+ clearSelection: () => {
27
+ },
28
+ viewMode: "grid",
29
+ setViewMode: () => {
30
+ },
31
+ meta: null,
32
+ setMeta: () => {
33
+ },
34
+ isLoading: false,
35
+ setIsLoading: () => {
36
+ }
37
+ };
38
+ var StudioContext = createContext(defaultState);
39
+ function useStudio() {
40
+ return useContext(StudioContext);
41
+ }
42
+
43
+ // src/components/StudioToolbar.tsx
44
+ import { useCallback } from "react";
45
+ import { jsx, jsxs } from "react/jsx-runtime";
46
+ function StudioToolbar() {
47
+ const { selectedItems, viewMode, setViewMode, clearSelection } = useStudio();
48
+ const handleUpload = useCallback(() => {
49
+ console.log("Upload clicked");
50
+ }, []);
51
+ const handleReprocess = useCallback(() => {
52
+ console.log("Reprocess clicked", selectedItems);
53
+ }, [selectedItems]);
54
+ const handleDelete = useCallback(() => {
55
+ console.log("Delete clicked", selectedItems);
56
+ }, [selectedItems]);
57
+ const handleSyncCdn = useCallback(() => {
58
+ console.log("Sync CDN clicked", selectedItems);
59
+ }, [selectedItems]);
60
+ const handleScan = useCallback(() => {
61
+ console.log("Scan clicked");
62
+ }, []);
63
+ const hasSelection = selectedItems.size > 0;
64
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-3 bg-gray-50 border-b border-gray-200", children: [
65
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
66
+ /* @__PURE__ */ jsx(ToolbarButton, { onClick: handleUpload, icon: "upload", label: "Upload" }),
67
+ /* @__PURE__ */ jsx(
68
+ ToolbarButton,
69
+ {
70
+ onClick: handleReprocess,
71
+ icon: "refresh",
72
+ label: "Reprocess",
73
+ disabled: !hasSelection
74
+ }
75
+ ),
76
+ /* @__PURE__ */ jsx(
77
+ ToolbarButton,
78
+ {
79
+ onClick: handleDelete,
80
+ icon: "trash",
81
+ label: "Delete",
82
+ disabled: !hasSelection,
83
+ variant: "danger"
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx(
87
+ ToolbarButton,
88
+ {
89
+ onClick: handleSyncCdn,
90
+ icon: "cloud",
91
+ label: "Sync CDN",
92
+ disabled: !hasSelection
93
+ }
94
+ ),
95
+ /* @__PURE__ */ jsx(ToolbarButton, { onClick: handleScan, icon: "scan", label: "Scan" })
96
+ ] }),
97
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
98
+ hasSelection && /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-600", children: [
99
+ selectedItems.size,
100
+ " selected",
101
+ /* @__PURE__ */ jsx(
102
+ "button",
103
+ {
104
+ onClick: clearSelection,
105
+ className: "ml-2 text-purple-600 hover:underline",
106
+ children: "Clear"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center bg-white border border-gray-200 rounded-lg overflow-hidden", children: [
111
+ /* @__PURE__ */ jsx(
112
+ "button",
113
+ {
114
+ onClick: () => setViewMode("grid"),
115
+ className: `p-2 ${viewMode === "grid" ? "bg-purple-100 text-purple-700" : "text-gray-500 hover:bg-gray-50"}`,
116
+ "aria-label": "Grid view",
117
+ children: /* @__PURE__ */ jsx(GridIcon, {})
118
+ }
119
+ ),
120
+ /* @__PURE__ */ jsx(
121
+ "button",
122
+ {
123
+ onClick: () => setViewMode("list"),
124
+ className: `p-2 ${viewMode === "list" ? "bg-purple-100 text-purple-700" : "text-gray-500 hover:bg-gray-50"}`,
125
+ "aria-label": "List view",
126
+ children: /* @__PURE__ */ jsx(ListIcon, {})
127
+ }
128
+ )
129
+ ] })
130
+ ] })
131
+ ] });
132
+ }
133
+ function ToolbarButton({
134
+ onClick,
135
+ icon,
136
+ label,
137
+ disabled,
138
+ variant = "default"
139
+ }) {
140
+ const baseStyles = "flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors";
141
+ const variantStyles = variant === "danger" ? "text-red-600 hover:bg-red-50 disabled:text-red-300" : "text-gray-700 hover:bg-white disabled:text-gray-400";
142
+ return /* @__PURE__ */ jsxs(
143
+ "button",
144
+ {
145
+ onClick,
146
+ disabled,
147
+ className: `${baseStyles} ${variantStyles} ${disabled ? "cursor-not-allowed" : ""}`,
148
+ children: [
149
+ /* @__PURE__ */ jsx(IconComponent, { icon }),
150
+ label
151
+ ]
152
+ }
153
+ );
154
+ }
155
+ function IconComponent({ icon }) {
156
+ const className = "w-4 h-4";
157
+ switch (icon) {
158
+ case "upload":
159
+ return /* @__PURE__ */ jsx(
160
+ "svg",
161
+ {
162
+ className,
163
+ fill: "none",
164
+ stroke: "currentColor",
165
+ viewBox: "0 0 24 24",
166
+ children: /* @__PURE__ */ jsx(
167
+ "path",
168
+ {
169
+ strokeLinecap: "round",
170
+ strokeLinejoin: "round",
171
+ strokeWidth: 2,
172
+ d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
173
+ }
174
+ )
175
+ }
176
+ );
177
+ case "refresh":
178
+ return /* @__PURE__ */ jsx(
179
+ "svg",
180
+ {
181
+ className,
182
+ fill: "none",
183
+ stroke: "currentColor",
184
+ viewBox: "0 0 24 24",
185
+ children: /* @__PURE__ */ jsx(
186
+ "path",
187
+ {
188
+ strokeLinecap: "round",
189
+ strokeLinejoin: "round",
190
+ strokeWidth: 2,
191
+ 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"
192
+ }
193
+ )
194
+ }
195
+ );
196
+ case "trash":
197
+ return /* @__PURE__ */ jsx(
198
+ "svg",
199
+ {
200
+ className,
201
+ fill: "none",
202
+ stroke: "currentColor",
203
+ viewBox: "0 0 24 24",
204
+ children: /* @__PURE__ */ jsx(
205
+ "path",
206
+ {
207
+ strokeLinecap: "round",
208
+ strokeLinejoin: "round",
209
+ strokeWidth: 2,
210
+ 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"
211
+ }
212
+ )
213
+ }
214
+ );
215
+ case "cloud":
216
+ return /* @__PURE__ */ jsx(
217
+ "svg",
218
+ {
219
+ className,
220
+ fill: "none",
221
+ stroke: "currentColor",
222
+ viewBox: "0 0 24 24",
223
+ children: /* @__PURE__ */ jsx(
224
+ "path",
225
+ {
226
+ strokeLinecap: "round",
227
+ strokeLinejoin: "round",
228
+ strokeWidth: 2,
229
+ 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"
230
+ }
231
+ )
232
+ }
233
+ );
234
+ case "scan":
235
+ return /* @__PURE__ */ jsx(
236
+ "svg",
237
+ {
238
+ className,
239
+ fill: "none",
240
+ stroke: "currentColor",
241
+ viewBox: "0 0 24 24",
242
+ children: /* @__PURE__ */ jsx(
243
+ "path",
244
+ {
245
+ strokeLinecap: "round",
246
+ strokeLinejoin: "round",
247
+ strokeWidth: 2,
248
+ d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
249
+ }
250
+ )
251
+ }
252
+ );
253
+ default:
254
+ return null;
255
+ }
256
+ }
257
+ function GridIcon() {
258
+ return /* @__PURE__ */ jsx(
259
+ "svg",
260
+ {
261
+ className: "w-4 h-4",
262
+ fill: "none",
263
+ stroke: "currentColor",
264
+ viewBox: "0 0 24 24",
265
+ children: /* @__PURE__ */ jsx(
266
+ "path",
267
+ {
268
+ strokeLinecap: "round",
269
+ strokeLinejoin: "round",
270
+ strokeWidth: 2,
271
+ 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"
272
+ }
273
+ )
274
+ }
275
+ );
276
+ }
277
+ function ListIcon() {
278
+ return /* @__PURE__ */ jsx(
279
+ "svg",
280
+ {
281
+ className: "w-4 h-4",
282
+ fill: "none",
283
+ stroke: "currentColor",
284
+ viewBox: "0 0 24 24",
285
+ children: /* @__PURE__ */ jsx(
286
+ "path",
287
+ {
288
+ strokeLinecap: "round",
289
+ strokeLinejoin: "round",
290
+ strokeWidth: 2,
291
+ d: "M4 6h16M4 10h16M4 14h16M4 18h16"
292
+ }
293
+ )
294
+ }
295
+ );
296
+ }
297
+
298
+ // src/components/StudioBreadcrumb.tsx
299
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
300
+ function StudioBreadcrumb() {
301
+ const { currentPath, setCurrentPath, navigateUp } = useStudio();
302
+ const parts = currentPath.split("/").filter(Boolean);
303
+ const handleClick = (index) => {
304
+ const newPath = parts.slice(0, index + 1).join("/");
305
+ setCurrentPath(newPath);
306
+ };
307
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 px-6 py-2 bg-white border-b border-gray-100", children: [
308
+ currentPath !== "public" && /* @__PURE__ */ jsx2(
309
+ "button",
310
+ {
311
+ onClick: navigateUp,
312
+ className: "p-1 hover:bg-gray-100 rounded transition-colors",
313
+ "aria-label": "Go back",
314
+ children: /* @__PURE__ */ jsx2(
315
+ "svg",
316
+ {
317
+ className: "w-4 h-4 text-gray-500",
318
+ fill: "none",
319
+ stroke: "currentColor",
320
+ viewBox: "0 0 24 24",
321
+ children: /* @__PURE__ */ jsx2(
322
+ "path",
323
+ {
324
+ strokeLinecap: "round",
325
+ strokeLinejoin: "round",
326
+ strokeWidth: 2,
327
+ d: "M15 19l-7-7 7-7"
328
+ }
329
+ )
330
+ }
331
+ )
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsx2("nav", { className: "flex items-center gap-1 text-sm", children: parts.map((part, index) => /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1", children: [
335
+ index > 0 && /* @__PURE__ */ jsx2("span", { className: "text-gray-300", children: "/" }),
336
+ /* @__PURE__ */ jsx2(
337
+ "button",
338
+ {
339
+ onClick: () => handleClick(index),
340
+ className: `px-1 py-0.5 rounded hover:bg-gray-100 transition-colors ${index === parts.length - 1 ? "text-gray-900 font-medium" : "text-gray-500 hover:text-gray-700"}`,
341
+ children: part
342
+ }
343
+ )
344
+ ] }, index)) })
345
+ ] });
346
+ }
347
+
348
+ // src/components/StudioFileGrid.tsx
349
+ import { useEffect, useState } from "react";
350
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
351
+ function StudioFileGrid() {
352
+ const { currentPath, setCurrentPath, selectedItems, toggleSelection } = useStudio();
353
+ const [items, setItems] = useState([]);
354
+ const [loading, setLoading] = useState(true);
355
+ useEffect(() => {
356
+ async function loadItems() {
357
+ setLoading(true);
358
+ try {
359
+ const response = await fetch(
360
+ `/api/studio/list?path=${encodeURIComponent(currentPath)}`
361
+ );
362
+ if (response.ok) {
363
+ const data = await response.json();
364
+ setItems(data.items || []);
365
+ }
366
+ } catch (error) {
367
+ console.error("Failed to load items:", error);
368
+ }
369
+ setLoading(false);
370
+ }
371
+ loadItems();
372
+ }, [currentPath]);
373
+ if (loading) {
374
+ return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center h-64", children: /* @__PURE__ */ jsx3("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" }) });
375
+ }
376
+ if (items.length === 0) {
377
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center h-64 text-gray-500", children: [
378
+ /* @__PURE__ */ jsx3(
379
+ "svg",
380
+ {
381
+ className: "w-12 h-12 mb-4",
382
+ fill: "none",
383
+ stroke: "currentColor",
384
+ viewBox: "0 0 24 24",
385
+ children: /* @__PURE__ */ jsx3(
386
+ "path",
387
+ {
388
+ strokeLinecap: "round",
389
+ strokeLinejoin: "round",
390
+ strokeWidth: 1.5,
391
+ 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"
392
+ }
393
+ )
394
+ }
395
+ ),
396
+ /* @__PURE__ */ jsx3("p", { children: "No files in this folder" }),
397
+ /* @__PURE__ */ jsx3("p", { className: "text-sm", children: "Upload images to get started" })
398
+ ] });
399
+ }
400
+ const sortedItems = [...items].sort((a, b) => {
401
+ if (a.type === "folder" && b.type !== "folder") return -1;
402
+ if (a.type !== "folder" && b.type === "folder") return 1;
403
+ return a.name.localeCompare(b.name);
404
+ });
405
+ return /* @__PURE__ */ jsx3("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4", children: sortedItems.map((item) => /* @__PURE__ */ jsx3(
406
+ GridItem,
407
+ {
408
+ item,
409
+ isSelected: selectedItems.has(item.path),
410
+ onSelect: () => toggleSelection(item.path),
411
+ onOpen: () => {
412
+ if (item.type === "folder") {
413
+ setCurrentPath(item.path);
414
+ }
415
+ }
416
+ },
417
+ item.path
418
+ )) });
419
+ }
420
+ function GridItem({ item, isSelected, onSelect, onOpen }) {
421
+ const isFolder = item.type === "folder";
422
+ return /* @__PURE__ */ jsxs3(
423
+ "div",
424
+ {
425
+ className: `relative group rounded-lg border-2 overflow-hidden cursor-pointer transition-all ${isSelected ? "border-purple-500 bg-purple-50" : "border-transparent hover:border-gray-200 bg-gray-50"}`,
426
+ onDoubleClick: onOpen,
427
+ children: [
428
+ /* @__PURE__ */ jsx3("div", { className: "absolute top-2 left-2 z-10", children: /* @__PURE__ */ jsx3(
429
+ "input",
430
+ {
431
+ type: "checkbox",
432
+ checked: isSelected,
433
+ onChange: onSelect,
434
+ onClick: (e) => e.stopPropagation(),
435
+ className: "w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
436
+ }
437
+ ) }),
438
+ item.cdnSynced && /* @__PURE__ */ jsx3("div", { className: "absolute top-2 right-2 z-10", children: /* @__PURE__ */ jsx3("span", { className: "bg-green-100 text-green-700 text-xs px-1.5 py-0.5 rounded-full", children: "CDN" }) }),
439
+ /* @__PURE__ */ jsx3("div", { className: "aspect-square flex items-center justify-center p-4", children: isFolder ? /* @__PURE__ */ jsx3(FolderIcon, {}) : /* @__PURE__ */ jsx3(
440
+ "img",
441
+ {
442
+ src: item.path.replace("public", ""),
443
+ alt: item.name,
444
+ className: "max-w-full max-h-full object-contain rounded",
445
+ loading: "lazy"
446
+ }
447
+ ) }),
448
+ /* @__PURE__ */ jsxs3("div", { className: "px-2 py-1.5 bg-white border-t", children: [
449
+ /* @__PURE__ */ jsx3("p", { className: "text-xs text-gray-700 truncate", title: item.name, children: item.name }),
450
+ item.size && /* @__PURE__ */ jsx3("p", { className: "text-xs text-gray-400", children: formatFileSize(item.size) })
451
+ ] })
452
+ ]
453
+ }
454
+ );
455
+ }
456
+ function FolderIcon() {
457
+ return /* @__PURE__ */ jsx3(
458
+ "svg",
459
+ {
460
+ className: "w-16 h-16 text-yellow-400",
461
+ fill: "currentColor",
462
+ viewBox: "0 0 24 24",
463
+ 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" })
464
+ }
465
+ );
466
+ }
467
+ function formatFileSize(bytes) {
468
+ if (bytes < 1024) return `${bytes} B`;
469
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
470
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
471
+ }
472
+
473
+ // src/components/StudioFileList.tsx
474
+ import { useEffect as useEffect2, useState as useState2 } from "react";
475
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
476
+ function StudioFileList() {
477
+ const { currentPath, setCurrentPath, selectedItems, toggleSelection } = useStudio();
478
+ const [items, setItems] = useState2([]);
479
+ const [loading, setLoading] = useState2(true);
480
+ useEffect2(() => {
481
+ async function loadItems() {
482
+ setLoading(true);
483
+ try {
484
+ const response = await fetch(
485
+ `/api/studio/list?path=${encodeURIComponent(currentPath)}`
486
+ );
487
+ if (response.ok) {
488
+ const data = await response.json();
489
+ setItems(data.items || []);
490
+ }
491
+ } catch (error) {
492
+ console.error("Failed to load items:", error);
493
+ }
494
+ setLoading(false);
495
+ }
496
+ loadItems();
497
+ }, [currentPath]);
498
+ if (loading) {
499
+ return /* @__PURE__ */ jsx4("div", { className: "flex items-center justify-center h-64", children: /* @__PURE__ */ jsx4("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" }) });
500
+ }
501
+ if (items.length === 0) {
502
+ return /* @__PURE__ */ jsx4("div", { className: "flex flex-col items-center justify-center h-64 text-gray-500", children: /* @__PURE__ */ jsx4("p", { children: "No files in this folder" }) });
503
+ }
504
+ const sortedItems = [...items].sort((a, b) => {
505
+ if (a.type === "folder" && b.type !== "folder") return -1;
506
+ if (a.type !== "folder" && b.type === "folder") return 1;
507
+ return a.name.localeCompare(b.name);
508
+ });
509
+ return /* @__PURE__ */ jsxs4("table", { className: "w-full", children: [
510
+ /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { className: "text-left text-xs text-gray-500 uppercase tracking-wider", children: [
511
+ /* @__PURE__ */ jsx4("th", { className: "w-8 pb-2" }),
512
+ /* @__PURE__ */ jsx4("th", { className: "pb-2", children: "Name" }),
513
+ /* @__PURE__ */ jsx4("th", { className: "pb-2 w-24", children: "Size" }),
514
+ /* @__PURE__ */ jsx4("th", { className: "pb-2 w-32", children: "Dimensions" }),
515
+ /* @__PURE__ */ jsx4("th", { className: "pb-2 w-24", children: "CDN" })
516
+ ] }) }),
517
+ /* @__PURE__ */ jsx4("tbody", { className: "divide-y divide-gray-100", children: sortedItems.map((item) => /* @__PURE__ */ jsx4(
518
+ ListRow,
519
+ {
520
+ item,
521
+ isSelected: selectedItems.has(item.path),
522
+ onSelect: () => toggleSelection(item.path),
523
+ onOpen: () => {
524
+ if (item.type === "folder") {
525
+ setCurrentPath(item.path);
526
+ }
527
+ }
528
+ },
529
+ item.path
530
+ )) })
531
+ ] });
532
+ }
533
+ function ListRow({ item, isSelected, onSelect, onOpen }) {
534
+ const isFolder = item.type === "folder";
535
+ return /* @__PURE__ */ jsxs4(
536
+ "tr",
537
+ {
538
+ className: `cursor-pointer transition-colors ${isSelected ? "bg-purple-50" : "hover:bg-gray-50"}`,
539
+ onDoubleClick: onOpen,
540
+ children: [
541
+ /* @__PURE__ */ jsx4("td", { className: "py-2", children: /* @__PURE__ */ jsx4(
542
+ "input",
543
+ {
544
+ type: "checkbox",
545
+ checked: isSelected,
546
+ onChange: onSelect,
547
+ onClick: (e) => e.stopPropagation(),
548
+ className: "w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
549
+ }
550
+ ) }),
551
+ /* @__PURE__ */ jsx4("td", { className: "py-2", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
552
+ isFolder ? /* @__PURE__ */ jsx4(
553
+ "svg",
554
+ {
555
+ className: "w-5 h-5 text-yellow-400",
556
+ fill: "currentColor",
557
+ viewBox: "0 0 24 24",
558
+ 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" })
559
+ }
560
+ ) : /* @__PURE__ */ jsx4(
561
+ "svg",
562
+ {
563
+ className: "w-5 h-5 text-gray-400",
564
+ fill: "none",
565
+ stroke: "currentColor",
566
+ viewBox: "0 0 24 24",
567
+ children: /* @__PURE__ */ jsx4(
568
+ "path",
569
+ {
570
+ strokeLinecap: "round",
571
+ strokeLinejoin: "round",
572
+ strokeWidth: 1.5,
573
+ 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"
574
+ }
575
+ )
576
+ }
577
+ ),
578
+ /* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-900", children: item.name })
579
+ ] }) }),
580
+ /* @__PURE__ */ jsx4("td", { className: "py-2 text-sm text-gray-500", children: item.size ? formatFileSize2(item.size) : "--" }),
581
+ /* @__PURE__ */ jsx4("td", { className: "py-2 text-sm text-gray-500", children: item.dimensions ? `${item.dimensions.width}x${item.dimensions.height}` : "--" }),
582
+ /* @__PURE__ */ jsx4("td", { className: "py-2", children: item.cdnSynced ? /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1 text-xs text-green-700", children: [
583
+ /* @__PURE__ */ jsx4("svg", { className: "w-3 h-3", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx4(
584
+ "path",
585
+ {
586
+ fillRule: "evenodd",
587
+ 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",
588
+ clipRule: "evenodd"
589
+ }
590
+ ) }),
591
+ "Synced"
592
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "text-xs text-gray-400", children: "--" }) })
593
+ ]
594
+ }
595
+ );
596
+ }
597
+ function formatFileSize2(bytes) {
598
+ if (bytes < 1024) return `${bytes} B`;
599
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
600
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
601
+ }
602
+
603
+ // src/components/StudioPreview.tsx
604
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
605
+ function StudioPreview() {
606
+ const { selectedItems, meta } = useStudio();
607
+ if (selectedItems.size !== 1) {
608
+ return null;
609
+ }
610
+ const selectedPath = Array.from(selectedItems)[0];
611
+ const imageKey = selectedPath.replace(/^public\/images\//, "").replace(/^public\/originals\//, "");
612
+ const imageData = meta?.images?.[imageKey];
613
+ return /* @__PURE__ */ jsxs5("div", { className: "w-80 border-l border-gray-200 bg-gray-50 p-4 overflow-auto", children: [
614
+ /* @__PURE__ */ jsx5("h3", { className: "text-sm font-medium text-gray-900 mb-4", children: "Preview" }),
615
+ /* @__PURE__ */ jsx5("div", { className: "bg-white rounded-lg border border-gray-200 p-2 mb-4", children: /* @__PURE__ */ jsx5(
616
+ "img",
617
+ {
618
+ src: selectedPath.replace("public", ""),
619
+ alt: "Preview",
620
+ className: "w-full h-auto rounded"
621
+ }
622
+ ) }),
623
+ /* @__PURE__ */ jsxs5("div", { className: "space-y-3", children: [
624
+ /* @__PURE__ */ jsx5(InfoRow, { label: "Filename", value: selectedPath.split("/").pop() || "" }),
625
+ imageData && /* @__PURE__ */ jsxs5(Fragment, { children: [
626
+ /* @__PURE__ */ jsx5(
627
+ InfoRow,
628
+ {
629
+ label: "Original",
630
+ value: `${imageData.original.width}x${imageData.original.height}`
631
+ }
632
+ ),
633
+ /* @__PURE__ */ jsx5(
634
+ InfoRow,
635
+ {
636
+ label: "File size",
637
+ value: formatFileSize3(imageData.original.fileSize)
638
+ }
639
+ ),
640
+ /* @__PURE__ */ jsxs5("div", { className: "pt-2 border-t border-gray-200", children: [
641
+ /* @__PURE__ */ jsx5("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "Generated sizes" }),
642
+ Object.entries(imageData.sizes).map(([size, data]) => /* @__PURE__ */ jsx5(
643
+ InfoRow,
644
+ {
645
+ label: size,
646
+ value: `${data.width}x${data.height}`
647
+ },
648
+ size
649
+ ))
650
+ ] }),
651
+ imageData.cdn?.synced && /* @__PURE__ */ jsxs5("div", { className: "pt-2 border-t border-gray-200", children: [
652
+ /* @__PURE__ */ jsx5("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "CDN" }),
653
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 text-xs text-green-600", children: [
654
+ /* @__PURE__ */ jsx5(
655
+ "svg",
656
+ {
657
+ className: "w-4 h-4",
658
+ fill: "currentColor",
659
+ viewBox: "0 0 20 20",
660
+ children: /* @__PURE__ */ jsx5(
661
+ "path",
662
+ {
663
+ fillRule: "evenodd",
664
+ 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",
665
+ clipRule: "evenodd"
666
+ }
667
+ )
668
+ }
669
+ ),
670
+ "Synced to CDN"
671
+ ] }),
672
+ /* @__PURE__ */ jsx5(
673
+ "button",
674
+ {
675
+ onClick: () => {
676
+ navigator.clipboard.writeText(
677
+ `${imageData.cdn?.baseUrl}${imageData.sizes.full.path}`
678
+ );
679
+ },
680
+ className: "mt-2 text-xs text-purple-600 hover:underline",
681
+ children: "Copy CDN URL"
682
+ }
683
+ )
684
+ ] }),
685
+ imageData.blurhash && /* @__PURE__ */ jsxs5("div", { className: "pt-2 border-t border-gray-200", children: [
686
+ /* @__PURE__ */ jsx5(InfoRow, { label: "Blurhash", value: imageData.blurhash, truncate: true }),
687
+ /* @__PURE__ */ jsx5(
688
+ "div",
689
+ {
690
+ className: "mt-2 h-8 rounded",
691
+ style: { backgroundColor: imageData.dominantColor },
692
+ title: `Dominant color: ${imageData.dominantColor}`
693
+ }
694
+ )
695
+ ] })
696
+ ] })
697
+ ] }),
698
+ /* @__PURE__ */ jsxs5("div", { className: "mt-4 pt-4 border-t border-gray-200 space-y-2", children: [
699
+ /* @__PURE__ */ jsx5("button", { className: "w-full px-3 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors", children: "Rename" }),
700
+ /* @__PURE__ */ jsx5("button", { className: "w-full px-3 py-2 text-sm text-red-600 bg-white border border-gray-200 rounded-lg hover:bg-red-50 transition-colors", children: "Delete" })
701
+ ] })
702
+ ] });
703
+ }
704
+ function InfoRow({
705
+ label,
706
+ value,
707
+ truncate
708
+ }) {
709
+ return /* @__PURE__ */ jsxs5("div", { className: "flex justify-between text-xs", children: [
710
+ /* @__PURE__ */ jsx5("span", { className: "text-gray-500", children: label }),
711
+ /* @__PURE__ */ jsx5(
712
+ "span",
713
+ {
714
+ className: `text-gray-900 ${truncate ? "truncate max-w-32" : ""}`,
715
+ title: truncate ? value : void 0,
716
+ children: value
717
+ }
718
+ )
719
+ ] });
720
+ }
721
+ function formatFileSize3(bytes) {
722
+ if (bytes < 1024) return `${bytes} B`;
723
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
724
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
725
+ }
726
+
727
+ // src/components/StudioSettings.tsx
728
+ import { useState as useState3 } from "react";
729
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
730
+ function StudioSettings() {
731
+ const [isOpen, setIsOpen] = useState3(false);
732
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
733
+ /* @__PURE__ */ jsx6(
734
+ "button",
735
+ {
736
+ onClick: () => setIsOpen(true),
737
+ className: "p-2 hover:bg-gray-100 rounded-lg transition-colors",
738
+ "aria-label": "Settings",
739
+ children: /* @__PURE__ */ jsxs6(
740
+ "svg",
741
+ {
742
+ xmlns: "http://www.w3.org/2000/svg",
743
+ viewBox: "0 0 24 24",
744
+ fill: "none",
745
+ stroke: "currentColor",
746
+ strokeWidth: 2,
747
+ strokeLinecap: "round",
748
+ strokeLinejoin: "round",
749
+ className: "w-5 h-5 text-gray-500",
750
+ children: [
751
+ /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "3" }),
752
+ /* @__PURE__ */ jsx6("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" })
753
+ ]
754
+ }
755
+ )
756
+ }
757
+ ),
758
+ isOpen && /* @__PURE__ */ jsx6(SettingsPanel, { onClose: () => setIsOpen(false) })
759
+ ] });
760
+ }
761
+ function SettingsPanel({ onClose }) {
762
+ return /* @__PURE__ */ jsxs6("div", { className: "fixed inset-0 z-[10000] flex items-center justify-center", children: [
763
+ /* @__PURE__ */ jsx6("div", { className: "absolute inset-0 bg-black/30", onClick: onClose }),
764
+ /* @__PURE__ */ jsxs6("div", { className: "relative bg-white rounded-xl shadow-xl w-full max-w-lg p-6", children: [
765
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between mb-6", children: [
766
+ /* @__PURE__ */ jsx6("h2", { className: "text-lg font-semibold", children: "Settings" }),
767
+ /* @__PURE__ */ jsx6(
768
+ "button",
769
+ {
770
+ onClick: onClose,
771
+ className: "p-1 hover:bg-gray-100 rounded-lg",
772
+ children: /* @__PURE__ */ jsx6(
773
+ "svg",
774
+ {
775
+ className: "w-5 h-5 text-gray-500",
776
+ fill: "none",
777
+ stroke: "currentColor",
778
+ viewBox: "0 0 24 24",
779
+ children: /* @__PURE__ */ jsx6(
780
+ "path",
781
+ {
782
+ strokeLinecap: "round",
783
+ strokeLinejoin: "round",
784
+ strokeWidth: 2,
785
+ d: "M6 18L18 6M6 6l12 12"
786
+ }
787
+ )
788
+ }
789
+ )
790
+ }
791
+ )
792
+ ] }),
793
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-6", children: [
794
+ /* @__PURE__ */ jsxs6("section", { children: [
795
+ /* @__PURE__ */ jsx6("h3", { className: "text-sm font-medium text-gray-900 mb-3", children: "Cloudflare R2" }),
796
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-gray-500 mb-3", children: "Configure in .env.local file:" }),
797
+ /* @__PURE__ */ jsxs6("div", { className: "bg-gray-50 rounded-lg p-3 font-mono text-xs text-gray-600 space-y-1", children: [
798
+ /* @__PURE__ */ jsx6("div", { children: "CLOUDFLARE_R2_ACCOUNT_ID" }),
799
+ /* @__PURE__ */ jsx6("div", { children: "CLOUDFLARE_R2_ACCESS_KEY_ID" }),
800
+ /* @__PURE__ */ jsx6("div", { children: "CLOUDFLARE_R2_SECRET_ACCESS_KEY" }),
801
+ /* @__PURE__ */ jsx6("div", { children: "CLOUDFLARE_R2_BUCKET_NAME" }),
802
+ /* @__PURE__ */ jsx6("div", { children: "CLOUDFLARE_R2_PUBLIC_URL" })
803
+ ] })
804
+ ] }),
805
+ /* @__PURE__ */ jsxs6("section", { children: [
806
+ /* @__PURE__ */ jsx6("h3", { className: "text-sm font-medium text-gray-900 mb-3", children: "Custom CDN URL" }),
807
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-gray-500 mb-3", children: "Override the default R2 URL with a custom domain:" }),
808
+ /* @__PURE__ */ jsx6(
809
+ "input",
810
+ {
811
+ type: "text",
812
+ placeholder: "https://cdn.yourdomain.com",
813
+ className: "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
814
+ }
815
+ )
816
+ ] }),
817
+ /* @__PURE__ */ jsxs6("section", { children: [
818
+ /* @__PURE__ */ jsx6("h3", { className: "text-sm font-medium text-gray-900 mb-3", children: "Thumbnail Sizes" }),
819
+ /* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-3 gap-3", children: [
820
+ /* @__PURE__ */ jsxs6("div", { children: [
821
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-gray-500", children: "Small" }),
822
+ /* @__PURE__ */ jsx6(
823
+ "input",
824
+ {
825
+ type: "number",
826
+ defaultValue: 300,
827
+ className: "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
828
+ }
829
+ )
830
+ ] }),
831
+ /* @__PURE__ */ jsxs6("div", { children: [
832
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-gray-500", children: "Medium" }),
833
+ /* @__PURE__ */ jsx6(
834
+ "input",
835
+ {
836
+ type: "number",
837
+ defaultValue: 700,
838
+ className: "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
839
+ }
840
+ )
841
+ ] }),
842
+ /* @__PURE__ */ jsxs6("div", { children: [
843
+ /* @__PURE__ */ jsx6("label", { className: "text-xs text-gray-500", children: "Large" }),
844
+ /* @__PURE__ */ jsx6(
845
+ "input",
846
+ {
847
+ type: "number",
848
+ defaultValue: 1400,
849
+ className: "w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
850
+ }
851
+ )
852
+ ] })
853
+ ] })
854
+ ] })
855
+ ] }),
856
+ /* @__PURE__ */ jsxs6("div", { className: "mt-6 flex justify-end gap-3", children: [
857
+ /* @__PURE__ */ jsx6(
858
+ "button",
859
+ {
860
+ onClick: onClose,
861
+ className: "px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg",
862
+ children: "Cancel"
863
+ }
864
+ ),
865
+ /* @__PURE__ */ jsx6("button", { className: "px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg", children: "Save Changes" })
866
+ ] })
867
+ ] })
868
+ ] });
869
+ }
870
+
871
+ // src/components/StudioUI.tsx
872
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
873
+ function StudioUI({ onClose }) {
874
+ const [currentPath, setCurrentPathInternal] = useState4("public");
875
+ const [selectedItems, setSelectedItems] = useState4(/* @__PURE__ */ new Set());
876
+ const [viewMode, setViewMode] = useState4("grid");
877
+ const [meta, setMeta] = useState4(null);
878
+ const [isLoading, setIsLoading] = useState4(false);
879
+ const navigateUp = useCallback2(() => {
880
+ if (currentPath === "public") return;
881
+ const parts = currentPath.split("/");
882
+ parts.pop();
883
+ setCurrentPathInternal(parts.join("/") || "public");
884
+ setSelectedItems(/* @__PURE__ */ new Set());
885
+ }, [currentPath]);
886
+ const setCurrentPath = useCallback2((path) => {
887
+ setCurrentPathInternal(path);
888
+ setSelectedItems(/* @__PURE__ */ new Set());
889
+ }, []);
890
+ const toggleSelection = useCallback2((path) => {
891
+ setSelectedItems((prev) => {
892
+ const next = new Set(prev);
893
+ if (next.has(path)) {
894
+ next.delete(path);
895
+ } else {
896
+ next.add(path);
897
+ }
898
+ return next;
899
+ });
900
+ }, []);
901
+ const selectAll = useCallback2((items) => {
902
+ setSelectedItems(new Set(items.map((item) => item.path)));
903
+ }, []);
904
+ const clearSelection = useCallback2(() => {
905
+ setSelectedItems(/* @__PURE__ */ new Set());
906
+ }, []);
907
+ const handleKeyDown = useCallback2(
908
+ (e) => {
909
+ if (e.key === "Escape") {
910
+ onClose();
911
+ }
912
+ },
913
+ [onClose]
914
+ );
915
+ useEffect3(() => {
916
+ document.addEventListener("keydown", handleKeyDown);
917
+ document.body.style.overflow = "hidden";
918
+ return () => {
919
+ document.removeEventListener("keydown", handleKeyDown);
920
+ document.body.style.overflow = "";
921
+ };
922
+ }, [handleKeyDown]);
923
+ const contextValue = {
924
+ isOpen: true,
925
+ openStudio: () => {
926
+ },
927
+ closeStudio: onClose,
928
+ toggleStudio: onClose,
929
+ currentPath,
930
+ setCurrentPath,
931
+ navigateUp,
932
+ selectedItems,
933
+ toggleSelection,
934
+ selectAll,
935
+ clearSelection,
936
+ viewMode,
937
+ setViewMode,
938
+ meta,
939
+ setMeta,
940
+ isLoading,
941
+ setIsLoading
942
+ };
943
+ return /* @__PURE__ */ jsx7(StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs7("div", { className: "flex flex-col h-full", children: [
944
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between px-6 py-4 border-b border-gray-200", children: [
945
+ /* @__PURE__ */ jsx7("h1", { className: "text-xl font-semibold text-gray-900", children: "Studio" }),
946
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
947
+ /* @__PURE__ */ jsx7(StudioSettings, {}),
948
+ /* @__PURE__ */ jsx7(
949
+ "button",
950
+ {
951
+ onClick: onClose,
952
+ className: "p-2 hover:bg-gray-100 rounded-lg transition-colors",
953
+ "aria-label": "Close Studio",
954
+ children: /* @__PURE__ */ jsx7(CloseIcon, {})
955
+ }
956
+ )
957
+ ] })
958
+ ] }),
959
+ /* @__PURE__ */ jsx7(StudioToolbar, {}),
960
+ /* @__PURE__ */ jsx7(StudioBreadcrumb, {}),
961
+ /* @__PURE__ */ jsxs7("div", { className: "flex-1 flex overflow-hidden", children: [
962
+ /* @__PURE__ */ jsx7("div", { className: "flex-1 overflow-auto p-4", children: viewMode === "grid" ? /* @__PURE__ */ jsx7(StudioFileGrid, {}) : /* @__PURE__ */ jsx7(StudioFileList, {}) }),
963
+ /* @__PURE__ */ jsx7(StudioPreview, {})
964
+ ] })
965
+ ] }) });
966
+ }
967
+ function CloseIcon() {
968
+ return /* @__PURE__ */ jsxs7(
969
+ "svg",
970
+ {
971
+ xmlns: "http://www.w3.org/2000/svg",
972
+ viewBox: "0 0 24 24",
973
+ fill: "none",
974
+ stroke: "currentColor",
975
+ strokeWidth: 2,
976
+ strokeLinecap: "round",
977
+ strokeLinejoin: "round",
978
+ className: "w-5 h-5 text-gray-500",
979
+ children: [
980
+ /* @__PURE__ */ jsx7("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
981
+ /* @__PURE__ */ jsx7("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
982
+ ]
983
+ }
984
+ );
985
+ }
986
+ var StudioUI_default = StudioUI;
987
+ export {
988
+ StudioUI,
989
+ StudioUI_default as default
990
+ };
991
+ //# sourceMappingURL=StudioUI-4ST2P6R7.mjs.map