@gallop.software/studio 0.1.6 → 0.1.8

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.
@@ -1,1523 +0,0 @@
1
- "use client";
2
-
3
- // src/components/StudioUI.tsx
4
- import { useEffect as useEffect3, useCallback as useCallback2, useState as useState5 } from "react";
5
- import { css as css7 } from "@emotion/react";
6
-
7
- // src/components/StudioContext.tsx
8
- import { createContext, useContext } from "react";
9
- var defaultState = {
10
- isOpen: false,
11
- openStudio: () => {
12
- },
13
- closeStudio: () => {
14
- },
15
- toggleStudio: () => {
16
- },
17
- currentPath: "public",
18
- setCurrentPath: () => {
19
- },
20
- navigateUp: () => {
21
- },
22
- selectedItems: /* @__PURE__ */ new Set(),
23
- toggleSelection: () => {
24
- },
25
- selectAll: () => {
26
- },
27
- clearSelection: () => {
28
- },
29
- viewMode: "grid",
30
- setViewMode: () => {
31
- },
32
- meta: null,
33
- setMeta: () => {
34
- },
35
- isLoading: false,
36
- setIsLoading: () => {
37
- },
38
- refreshKey: 0,
39
- triggerRefresh: () => {
40
- }
41
- };
42
- var StudioContext = createContext(defaultState);
43
- function useStudio() {
44
- return useContext(StudioContext);
45
- }
46
-
47
- // src/components/StudioToolbar.tsx
48
- import { useCallback, useRef, useState } from "react";
49
- import { css } from "@emotion/react";
50
- import { jsx, jsxs } from "@emotion/react/jsx-runtime";
51
- var styles = {
52
- toolbar: css`
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- padding: 12px 24px;
57
- background-color: #f9fafb;
58
- border-bottom: 1px solid #e5e7eb;
59
- `,
60
- left: css`
61
- display: flex;
62
- align-items: center;
63
- gap: 8px;
64
- `,
65
- right: css`
66
- display: flex;
67
- align-items: center;
68
- gap: 16px;
69
- `,
70
- btn: css`
71
- display: flex;
72
- align-items: center;
73
- gap: 8px;
74
- padding: 8px 12px;
75
- border-radius: 8px;
76
- font-size: 14px;
77
- font-weight: 500;
78
- background: none;
79
- border: none;
80
- cursor: pointer;
81
- transition: background-color 0.15s;
82
-
83
- &:disabled {
84
- cursor: not-allowed;
85
- opacity: 0.5;
86
- }
87
- `,
88
- btnDefault: css`
89
- color: #374151;
90
-
91
- &:hover:not(:disabled) {
92
- background-color: white;
93
- }
94
- `,
95
- btnDanger: css`
96
- color: #dc2626;
97
-
98
- &:hover:not(:disabled) {
99
- background-color: #fef2f2;
100
- }
101
- `,
102
- icon: css`
103
- width: 16px;
104
- height: 16px;
105
- `,
106
- selectionCount: css`
107
- font-size: 14px;
108
- color: #4b5563;
109
- `,
110
- clearBtn: css`
111
- margin-left: 8px;
112
- color: #9333ea;
113
- background: none;
114
- border: none;
115
- cursor: pointer;
116
- font-size: 14px;
117
-
118
- &:hover {
119
- text-decoration: underline;
120
- }
121
- `,
122
- viewToggle: css`
123
- display: flex;
124
- align-items: center;
125
- background-color: white;
126
- border: 1px solid #e5e7eb;
127
- border-radius: 8px;
128
- overflow: hidden;
129
- `,
130
- viewBtn: css`
131
- padding: 8px;
132
- background: none;
133
- border: none;
134
- cursor: pointer;
135
- color: #6b7280;
136
- transition: all 0.15s;
137
-
138
- &:hover {
139
- background-color: #f9fafb;
140
- }
141
- `,
142
- viewBtnActive: css`
143
- background-color: #f3e8ff;
144
- color: #7c3aed;
145
- `
146
- };
147
- function StudioToolbar() {
148
- const { selectedItems, viewMode, setViewMode, clearSelection, currentPath, triggerRefresh } = useStudio();
149
- const fileInputRef = useRef(null);
150
- const [uploading, setUploading] = useState(false);
151
- const handleUpload = useCallback(() => {
152
- fileInputRef.current?.click();
153
- }, []);
154
- const handleFileChange = useCallback(async (e) => {
155
- const files = e.target.files;
156
- if (!files || files.length === 0) return;
157
- setUploading(true);
158
- try {
159
- for (const file of Array.from(files)) {
160
- const formData = new FormData();
161
- formData.append("file", file);
162
- formData.append("path", currentPath);
163
- const response = await fetch("/api/studio/upload", {
164
- method: "POST",
165
- body: formData
166
- });
167
- if (!response.ok) {
168
- const error = await response.json();
169
- console.error("Upload failed:", error);
170
- alert(`Failed to upload ${file.name}: ${error.error || "Unknown error"}`);
171
- }
172
- }
173
- triggerRefresh();
174
- } catch (error) {
175
- console.error("Upload error:", error);
176
- alert("Upload failed. Check console for details.");
177
- } finally {
178
- setUploading(false);
179
- if (fileInputRef.current) {
180
- fileInputRef.current.value = "";
181
- }
182
- }
183
- }, [currentPath, triggerRefresh]);
184
- const handleReprocess = useCallback(() => {
185
- console.log("Reprocess clicked", selectedItems);
186
- }, [selectedItems]);
187
- const handleDelete = useCallback(async () => {
188
- if (selectedItems.size === 0) return;
189
- if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return;
190
- try {
191
- const response = await fetch("/api/studio/delete", {
192
- method: "POST",
193
- headers: { "Content-Type": "application/json" },
194
- body: JSON.stringify({ paths: Array.from(selectedItems) })
195
- });
196
- if (response.ok) {
197
- clearSelection();
198
- triggerRefresh();
199
- } else {
200
- const error = await response.json();
201
- alert(`Delete failed: ${error.error || "Unknown error"}`);
202
- }
203
- } catch (error) {
204
- console.error("Delete error:", error);
205
- alert("Delete failed. Check console for details.");
206
- }
207
- }, [selectedItems, clearSelection, triggerRefresh]);
208
- const handleSyncCdn = useCallback(() => {
209
- console.log("Sync CDN clicked", selectedItems);
210
- }, [selectedItems]);
211
- const handleScan = useCallback(() => {
212
- console.log("Scan clicked");
213
- }, []);
214
- const hasSelection = selectedItems.size > 0;
215
- return /* @__PURE__ */ jsxs("div", { css: styles.toolbar, children: [
216
- /* @__PURE__ */ jsx(
217
- "input",
218
- {
219
- ref: fileInputRef,
220
- type: "file",
221
- multiple: true,
222
- accept: "image/*",
223
- onChange: handleFileChange,
224
- style: { display: "none" }
225
- }
226
- ),
227
- /* @__PURE__ */ jsxs("div", { css: styles.left, children: [
228
- /* @__PURE__ */ jsx(
229
- ToolbarButton,
230
- {
231
- onClick: handleUpload,
232
- icon: "upload",
233
- label: uploading ? "Uploading..." : "Upload",
234
- disabled: uploading
235
- }
236
- ),
237
- /* @__PURE__ */ jsx(
238
- ToolbarButton,
239
- {
240
- onClick: handleReprocess,
241
- icon: "refresh",
242
- label: "Reprocess",
243
- disabled: !hasSelection
244
- }
245
- ),
246
- /* @__PURE__ */ jsx(
247
- ToolbarButton,
248
- {
249
- onClick: handleDelete,
250
- icon: "trash",
251
- label: "Delete",
252
- disabled: !hasSelection,
253
- variant: "danger"
254
- }
255
- ),
256
- /* @__PURE__ */ jsx(
257
- ToolbarButton,
258
- {
259
- onClick: handleSyncCdn,
260
- icon: "cloud",
261
- label: "Sync CDN",
262
- disabled: !hasSelection
263
- }
264
- ),
265
- /* @__PURE__ */ jsx(ToolbarButton, { onClick: handleScan, icon: "scan", label: "Scan" })
266
- ] }),
267
- /* @__PURE__ */ jsxs("div", { css: styles.right, children: [
268
- hasSelection && /* @__PURE__ */ jsxs("span", { css: styles.selectionCount, children: [
269
- selectedItems.size,
270
- " selected",
271
- /* @__PURE__ */ jsx("button", { css: styles.clearBtn, onClick: clearSelection, children: "Clear" })
272
- ] }),
273
- /* @__PURE__ */ jsxs("div", { css: styles.viewToggle, children: [
274
- /* @__PURE__ */ jsx(
275
- "button",
276
- {
277
- css: [styles.viewBtn, viewMode === "grid" && styles.viewBtnActive],
278
- onClick: () => setViewMode("grid"),
279
- "aria-label": "Grid view",
280
- children: /* @__PURE__ */ jsx(GridIcon, {})
281
- }
282
- ),
283
- /* @__PURE__ */ jsx(
284
- "button",
285
- {
286
- css: [styles.viewBtn, viewMode === "list" && styles.viewBtnActive],
287
- onClick: () => setViewMode("list"),
288
- "aria-label": "List view",
289
- children: /* @__PURE__ */ jsx(ListIcon, {})
290
- }
291
- )
292
- ] })
293
- ] })
294
- ] });
295
- }
296
- function ToolbarButton({
297
- onClick,
298
- icon,
299
- label,
300
- disabled,
301
- variant = "default"
302
- }) {
303
- return /* @__PURE__ */ jsxs(
304
- "button",
305
- {
306
- css: [styles.btn, variant === "danger" ? styles.btnDanger : styles.btnDefault],
307
- onClick,
308
- disabled,
309
- children: [
310
- /* @__PURE__ */ jsx(IconComponent, { icon }),
311
- label
312
- ]
313
- }
314
- );
315
- }
316
- function IconComponent({ icon }) {
317
- switch (icon) {
318
- case "upload":
319
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("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" }) });
320
- case "refresh":
321
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("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" }) });
322
- case "trash":
323
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("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" }) });
324
- case "cloud":
325
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("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" }) });
326
- case "scan":
327
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) });
328
- default:
329
- return null;
330
- }
331
- }
332
- function GridIcon() {
333
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("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" }) });
334
- }
335
- function ListIcon() {
336
- return /* @__PURE__ */ jsx("svg", { css: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 10h16M4 14h16M4 18h16" }) });
337
- }
338
-
339
- // src/components/StudioBreadcrumb.tsx
340
- import { css as css2 } from "@emotion/react";
341
- import { jsx as jsx2, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
342
- var styles2 = {
343
- container: css2`
344
- display: flex;
345
- align-items: center;
346
- gap: 8px;
347
- padding: 8px 24px;
348
- background-color: white;
349
- border-bottom: 1px solid #f3f4f6;
350
- `,
351
- backBtn: css2`
352
- padding: 4px;
353
- background: none;
354
- border: none;
355
- border-radius: 4px;
356
- cursor: pointer;
357
- transition: background-color 0.15s;
358
-
359
- &:hover {
360
- background-color: #f3f4f6;
361
- }
362
- `,
363
- backIcon: css2`
364
- width: 16px;
365
- height: 16px;
366
- color: #6b7280;
367
- `,
368
- nav: css2`
369
- display: flex;
370
- align-items: center;
371
- gap: 4px;
372
- font-size: 14px;
373
- `,
374
- item: css2`
375
- display: flex;
376
- align-items: center;
377
- gap: 4px;
378
- `,
379
- separator: css2`
380
- color: #d1d5db;
381
- `,
382
- btn: css2`
383
- padding: 2px 4px;
384
- background: none;
385
- border: none;
386
- border-radius: 4px;
387
- cursor: pointer;
388
- transition: all 0.15s;
389
-
390
- &:hover {
391
- background-color: #f3f4f6;
392
- }
393
- `,
394
- btnActive: css2`
395
- color: #111827;
396
- font-weight: 500;
397
- `,
398
- btnInactive: css2`
399
- color: #6b7280;
400
-
401
- &:hover {
402
- color: #374151;
403
- }
404
- `
405
- };
406
- function StudioBreadcrumb() {
407
- const { currentPath, setCurrentPath, navigateUp } = useStudio();
408
- const parts = currentPath.split("/").filter(Boolean);
409
- const handleClick = (index) => {
410
- const newPath = parts.slice(0, index + 1).join("/");
411
- setCurrentPath(newPath);
412
- };
413
- return /* @__PURE__ */ jsxs2("div", { css: styles2.container, children: [
414
- currentPath !== "public" && /* @__PURE__ */ jsx2("button", { css: styles2.backBtn, onClick: navigateUp, "aria-label": "Go back", children: /* @__PURE__ */ jsx2("svg", { css: styles2.backIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }),
415
- /* @__PURE__ */ jsx2("nav", { css: styles2.nav, children: parts.map((part, index) => /* @__PURE__ */ jsxs2("span", { css: styles2.item, children: [
416
- index > 0 && /* @__PURE__ */ jsx2("span", { css: styles2.separator, children: "/" }),
417
- /* @__PURE__ */ jsx2(
418
- "button",
419
- {
420
- css: [styles2.btn, index === parts.length - 1 ? styles2.btnActive : styles2.btnInactive],
421
- onClick: () => handleClick(index),
422
- children: part
423
- }
424
- )
425
- ] }, index)) })
426
- ] });
427
- }
428
-
429
- // src/components/StudioFileGrid.tsx
430
- import { useEffect, useState as useState2 } from "react";
431
- import { css as css3, keyframes } from "@emotion/react";
432
- import { jsx as jsx3, jsxs as jsxs3 } from "@emotion/react/jsx-runtime";
433
- var spin = keyframes`
434
- to { transform: rotate(360deg); }
435
- `;
436
- var styles3 = {
437
- loading: css3`
438
- display: flex;
439
- align-items: center;
440
- justify-content: center;
441
- height: 256px;
442
- `,
443
- spinner: css3`
444
- width: 32px;
445
- height: 32px;
446
- border-radius: 50%;
447
- border: 2px solid transparent;
448
- border-bottom-color: #9333ea;
449
- animation: ${spin} 1s linear infinite;
450
- `,
451
- empty: css3`
452
- display: flex;
453
- flex-direction: column;
454
- align-items: center;
455
- justify-content: center;
456
- height: 256px;
457
- color: #6b7280;
458
- `,
459
- emptyIcon: css3`
460
- width: 48px;
461
- height: 48px;
462
- margin-bottom: 16px;
463
- `,
464
- emptyText: css3`
465
- font-size: 14px;
466
- margin: 0;
467
- `,
468
- grid: css3`
469
- display: grid;
470
- grid-template-columns: repeat(2, 1fr);
471
- gap: 16px;
472
-
473
- @media (min-width: 640px) { grid-template-columns: repeat(3, 1fr); }
474
- @media (min-width: 768px) { grid-template-columns: repeat(4, 1fr); }
475
- @media (min-width: 1024px) { grid-template-columns: repeat(5, 1fr); }
476
- @media (min-width: 1280px) { grid-template-columns: repeat(6, 1fr); }
477
- `,
478
- item: css3`
479
- position: relative;
480
- border-radius: 8px;
481
- border: 2px solid transparent;
482
- overflow: hidden;
483
- cursor: pointer;
484
- transition: all 0.15s;
485
- background-color: #f9fafb;
486
-
487
- &:hover {
488
- border-color: #e5e7eb;
489
- }
490
- `,
491
- itemSelected: css3`
492
- border-color: #a855f7;
493
- background-color: #faf5ff;
494
- `,
495
- checkbox: css3`
496
- position: absolute;
497
- top: 8px;
498
- left: 8px;
499
- z-index: 10;
500
- width: 16px;
501
- height: 16px;
502
- accent-color: #9333ea;
503
- `,
504
- cdnBadge: css3`
505
- position: absolute;
506
- top: 8px;
507
- right: 8px;
508
- z-index: 10;
509
- background-color: #dcfce7;
510
- color: #15803d;
511
- font-size: 12px;
512
- padding: 2px 6px;
513
- border-radius: 9999px;
514
- `,
515
- content: css3`
516
- aspect-ratio: 1;
517
- display: flex;
518
- align-items: center;
519
- justify-content: center;
520
- padding: 16px;
521
- `,
522
- folderIcon: css3`
523
- width: 64px;
524
- height: 64px;
525
- color: #facc15;
526
- `,
527
- image: css3`
528
- max-width: 100%;
529
- max-height: 100%;
530
- object-fit: contain;
531
- border-radius: 4px;
532
- `,
533
- label: css3`
534
- padding: 6px 8px;
535
- background-color: white;
536
- border-top: 1px solid #e5e7eb;
537
- `,
538
- name: css3`
539
- font-size: 12px;
540
- color: #374151;
541
- white-space: nowrap;
542
- overflow: hidden;
543
- text-overflow: ellipsis;
544
- margin: 0;
545
- `,
546
- size: css3`
547
- font-size: 12px;
548
- color: #9ca3af;
549
- margin: 0;
550
- `
551
- };
552
- function StudioFileGrid() {
553
- const { currentPath, setCurrentPath, selectedItems, toggleSelection, refreshKey } = useStudio();
554
- const [items, setItems] = useState2([]);
555
- const [loading, setLoading] = useState2(true);
556
- useEffect(() => {
557
- async function loadItems() {
558
- setLoading(true);
559
- try {
560
- const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`);
561
- if (response.ok) {
562
- const data = await response.json();
563
- setItems(data.items || []);
564
- }
565
- } catch (error) {
566
- console.error("Failed to load items:", error);
567
- }
568
- setLoading(false);
569
- }
570
- loadItems();
571
- }, [currentPath, refreshKey]);
572
- if (loading) {
573
- return /* @__PURE__ */ jsx3("div", { css: styles3.loading, children: /* @__PURE__ */ jsx3("div", { css: styles3.spinner }) });
574
- }
575
- if (items.length === 0) {
576
- return /* @__PURE__ */ jsxs3("div", { css: styles3.empty, children: [
577
- /* @__PURE__ */ jsx3("svg", { css: styles3.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }),
578
- /* @__PURE__ */ jsx3("p", { css: styles3.emptyText, children: "No files in this folder" }),
579
- /* @__PURE__ */ jsx3("p", { css: styles3.emptyText, children: "Upload images to get started" })
580
- ] });
581
- }
582
- const sortedItems = [...items].sort((a, b) => {
583
- if (a.type === "folder" && b.type !== "folder") return -1;
584
- if (a.type !== "folder" && b.type === "folder") return 1;
585
- return a.name.localeCompare(b.name);
586
- });
587
- return /* @__PURE__ */ jsx3("div", { css: styles3.grid, children: sortedItems.map((item) => /* @__PURE__ */ jsx3(
588
- GridItem,
589
- {
590
- item,
591
- isSelected: selectedItems.has(item.path),
592
- onSelect: () => toggleSelection(item.path),
593
- onOpen: () => {
594
- if (item.type === "folder") {
595
- setCurrentPath(item.path);
596
- }
597
- }
598
- },
599
- item.path
600
- )) });
601
- }
602
- function GridItem({ item, isSelected, onSelect, onOpen }) {
603
- const isFolder = item.type === "folder";
604
- const handleClick = () => {
605
- if (isFolder) {
606
- onOpen();
607
- } else {
608
- onSelect();
609
- }
610
- };
611
- return /* @__PURE__ */ jsxs3("div", { css: [styles3.item, isSelected && styles3.itemSelected], onClick: handleClick, children: [
612
- !isFolder && /* @__PURE__ */ jsx3(
613
- "input",
614
- {
615
- type: "checkbox",
616
- css: styles3.checkbox,
617
- checked: isSelected,
618
- onChange: onSelect,
619
- onClick: (e) => e.stopPropagation()
620
- }
621
- ),
622
- item.cdnSynced && /* @__PURE__ */ jsx3("span", { css: styles3.cdnBadge, children: "CDN" }),
623
- /* @__PURE__ */ jsx3("div", { css: styles3.content, children: isFolder ? /* @__PURE__ */ jsx3("svg", { css: styles3.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : /* @__PURE__ */ jsx3(
624
- "img",
625
- {
626
- css: styles3.image,
627
- src: item.path.replace("public", ""),
628
- alt: item.name,
629
- loading: "lazy"
630
- }
631
- ) }),
632
- /* @__PURE__ */ jsxs3("div", { css: styles3.label, children: [
633
- /* @__PURE__ */ jsx3("p", { css: styles3.name, title: item.name, children: item.name }),
634
- item.size && /* @__PURE__ */ jsx3("p", { css: styles3.size, children: formatFileSize(item.size) })
635
- ] })
636
- ] });
637
- }
638
- function formatFileSize(bytes) {
639
- if (bytes < 1024) return `${bytes} B`;
640
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
641
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
642
- }
643
-
644
- // src/components/StudioFileList.tsx
645
- import { useEffect as useEffect2, useState as useState3 } from "react";
646
- import { css as css4, keyframes as keyframes2 } from "@emotion/react";
647
- import { jsx as jsx4, jsxs as jsxs4 } from "@emotion/react/jsx-runtime";
648
- var spin2 = keyframes2`
649
- to { transform: rotate(360deg); }
650
- `;
651
- var styles4 = {
652
- loading: css4`
653
- display: flex;
654
- align-items: center;
655
- justify-content: center;
656
- height: 256px;
657
- `,
658
- spinner: css4`
659
- width: 32px;
660
- height: 32px;
661
- border-radius: 50%;
662
- border: 2px solid transparent;
663
- border-bottom-color: #9333ea;
664
- animation: ${spin2} 1s linear infinite;
665
- `,
666
- empty: css4`
667
- display: flex;
668
- flex-direction: column;
669
- align-items: center;
670
- justify-content: center;
671
- height: 256px;
672
- color: #6b7280;
673
- `,
674
- table: css4`
675
- width: 100%;
676
- border-collapse: collapse;
677
- `,
678
- th: css4`
679
- text-align: left;
680
- font-size: 12px;
681
- color: #6b7280;
682
- text-transform: uppercase;
683
- letter-spacing: 0.05em;
684
- padding-bottom: 8px;
685
- font-weight: normal;
686
- `,
687
- thCheckbox: css4`
688
- width: 32px;
689
- `,
690
- thSize: css4`
691
- width: 96px;
692
- `,
693
- thDimensions: css4`
694
- width: 128px;
695
- `,
696
- thCdn: css4`
697
- width: 96px;
698
- `,
699
- tbody: css4`
700
- border-top: 1px solid #f3f4f6;
701
- `,
702
- row: css4`
703
- cursor: pointer;
704
- transition: background-color 0.15s;
705
-
706
- &:hover {
707
- background-color: #f9fafb;
708
- }
709
- `,
710
- rowSelected: css4`
711
- background-color: #faf5ff;
712
- `,
713
- td: css4`
714
- padding: 8px 0;
715
- border-bottom: 1px solid #f3f4f6;
716
- `,
717
- checkbox: css4`
718
- width: 16px;
719
- height: 16px;
720
- accent-color: #9333ea;
721
- `,
722
- nameCell: css4`
723
- display: flex;
724
- align-items: center;
725
- gap: 8px;
726
- `,
727
- folderIcon: css4`
728
- width: 20px;
729
- height: 20px;
730
- color: #facc15;
731
- `,
732
- fileIcon: css4`
733
- width: 20px;
734
- height: 20px;
735
- color: #9ca3af;
736
- `,
737
- name: css4`
738
- font-size: 14px;
739
- color: #111827;
740
- `,
741
- meta: css4`
742
- font-size: 14px;
743
- color: #6b7280;
744
- `,
745
- cdnBadge: css4`
746
- display: inline-flex;
747
- align-items: center;
748
- gap: 4px;
749
- font-size: 12px;
750
- color: #15803d;
751
- `,
752
- cdnIcon: css4`
753
- width: 12px;
754
- height: 12px;
755
- `,
756
- cdnEmpty: css4`
757
- font-size: 12px;
758
- color: #9ca3af;
759
- `
760
- };
761
- function StudioFileList() {
762
- const { currentPath, setCurrentPath, selectedItems, toggleSelection, refreshKey } = useStudio();
763
- const [items, setItems] = useState3([]);
764
- const [loading, setLoading] = useState3(true);
765
- useEffect2(() => {
766
- async function loadItems() {
767
- setLoading(true);
768
- try {
769
- const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`);
770
- if (response.ok) {
771
- const data = await response.json();
772
- setItems(data.items || []);
773
- }
774
- } catch (error) {
775
- console.error("Failed to load items:", error);
776
- }
777
- setLoading(false);
778
- }
779
- loadItems();
780
- }, [currentPath, refreshKey]);
781
- if (loading) {
782
- return /* @__PURE__ */ jsx4("div", { css: styles4.loading, children: /* @__PURE__ */ jsx4("div", { css: styles4.spinner }) });
783
- }
784
- if (items.length === 0) {
785
- return /* @__PURE__ */ jsx4("div", { css: styles4.empty, children: /* @__PURE__ */ jsx4("p", { children: "No files in this folder" }) });
786
- }
787
- const sortedItems = [...items].sort((a, b) => {
788
- if (a.type === "folder" && b.type !== "folder") return -1;
789
- if (a.type !== "folder" && b.type === "folder") return 1;
790
- return a.name.localeCompare(b.name);
791
- });
792
- return /* @__PURE__ */ jsxs4("table", { css: styles4.table, children: [
793
- /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
794
- /* @__PURE__ */ jsx4("th", { css: [styles4.th, styles4.thCheckbox] }),
795
- /* @__PURE__ */ jsx4("th", { css: styles4.th, children: "Name" }),
796
- /* @__PURE__ */ jsx4("th", { css: [styles4.th, styles4.thSize], children: "Size" }),
797
- /* @__PURE__ */ jsx4("th", { css: [styles4.th, styles4.thDimensions], children: "Dimensions" }),
798
- /* @__PURE__ */ jsx4("th", { css: [styles4.th, styles4.thCdn], children: "CDN" })
799
- ] }) }),
800
- /* @__PURE__ */ jsx4("tbody", { css: styles4.tbody, children: sortedItems.map((item) => /* @__PURE__ */ jsx4(
801
- ListRow,
802
- {
803
- item,
804
- isSelected: selectedItems.has(item.path),
805
- onSelect: () => toggleSelection(item.path),
806
- onOpen: () => {
807
- if (item.type === "folder") {
808
- setCurrentPath(item.path);
809
- }
810
- }
811
- },
812
- item.path
813
- )) })
814
- ] });
815
- }
816
- function ListRow({ item, isSelected, onSelect, onOpen }) {
817
- const isFolder = item.type === "folder";
818
- const handleClick = () => {
819
- if (isFolder) {
820
- onOpen();
821
- } else {
822
- onSelect();
823
- }
824
- };
825
- return /* @__PURE__ */ jsxs4("tr", { css: [styles4.row, isSelected && styles4.rowSelected], onClick: handleClick, children: [
826
- /* @__PURE__ */ jsx4("td", { css: styles4.td, children: !isFolder && /* @__PURE__ */ jsx4(
827
- "input",
828
- {
829
- type: "checkbox",
830
- css: styles4.checkbox,
831
- checked: isSelected,
832
- onChange: onSelect,
833
- onClick: (e) => e.stopPropagation()
834
- }
835
- ) }),
836
- /* @__PURE__ */ jsx4("td", { css: styles4.td, children: /* @__PURE__ */ jsxs4("div", { css: styles4.nameCell, children: [
837
- isFolder ? /* @__PURE__ */ jsx4("svg", { css: styles4.folderIcon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { d: "M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" }) }) : /* @__PURE__ */ jsx4("svg", { css: styles4.fileIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "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" }) }),
838
- /* @__PURE__ */ jsx4("span", { css: styles4.name, children: item.name })
839
- ] }) }),
840
- /* @__PURE__ */ jsx4("td", { css: [styles4.td, styles4.meta], children: item.size ? formatFileSize2(item.size) : "--" }),
841
- /* @__PURE__ */ jsx4("td", { css: [styles4.td, styles4.meta], children: item.dimensions ? `${item.dimensions.width}x${item.dimensions.height}` : "--" }),
842
- /* @__PURE__ */ jsx4("td", { css: styles4.td, children: item.cdnSynced ? /* @__PURE__ */ jsxs4("span", { css: styles4.cdnBadge, children: [
843
- /* @__PURE__ */ jsx4("svg", { css: styles4.cdnIcon, fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx4("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" }) }),
844
- "Synced"
845
- ] }) : /* @__PURE__ */ jsx4("span", { css: styles4.cdnEmpty, children: "--" }) })
846
- ] });
847
- }
848
- function formatFileSize2(bytes) {
849
- if (bytes < 1024) return `${bytes} B`;
850
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
851
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
852
- }
853
-
854
- // src/components/StudioPreview.tsx
855
- import { css as css5 } from "@emotion/react";
856
- import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
857
- var styles5 = {
858
- panel: css5`
859
- width: 320px;
860
- border-left: 1px solid #e5e7eb;
861
- background-color: #f9fafb;
862
- padding: 16px;
863
- overflow: auto;
864
- `,
865
- title: css5`
866
- font-size: 14px;
867
- font-weight: 500;
868
- color: #111827;
869
- margin: 0 0 16px 0;
870
- `,
871
- imageContainer: css5`
872
- background-color: white;
873
- border-radius: 8px;
874
- border: 1px solid #e5e7eb;
875
- padding: 8px;
876
- margin-bottom: 16px;
877
- `,
878
- image: css5`
879
- width: 100%;
880
- height: auto;
881
- border-radius: 4px;
882
- `,
883
- info: css5`
884
- display: flex;
885
- flex-direction: column;
886
- gap: 12px;
887
- `,
888
- row: css5`
889
- display: flex;
890
- justify-content: space-between;
891
- font-size: 12px;
892
- `,
893
- label: css5`
894
- color: #6b7280;
895
- `,
896
- value: css5`
897
- color: #111827;
898
- `,
899
- valueTruncate: css5`
900
- max-width: 128px;
901
- white-space: nowrap;
902
- overflow: hidden;
903
- text-overflow: ellipsis;
904
- `,
905
- section: css5`
906
- padding-top: 8px;
907
- border-top: 1px solid #e5e7eb;
908
- `,
909
- sectionTitle: css5`
910
- font-size: 12px;
911
- font-weight: 500;
912
- color: #6b7280;
913
- margin: 0 0 8px 0;
914
- `,
915
- cdnStatus: css5`
916
- display: flex;
917
- align-items: center;
918
- gap: 8px;
919
- font-size: 12px;
920
- color: #16a34a;
921
- `,
922
- cdnIcon: css5`
923
- width: 16px;
924
- height: 16px;
925
- `,
926
- copyBtn: css5`
927
- margin-top: 8px;
928
- font-size: 12px;
929
- color: #9333ea;
930
- background: none;
931
- border: none;
932
- cursor: pointer;
933
- padding: 0;
934
-
935
- &:hover {
936
- text-decoration: underline;
937
- }
938
- `,
939
- colorSwatch: css5`
940
- margin-top: 8px;
941
- height: 32px;
942
- border-radius: 4px;
943
- `,
944
- emptyState: css5`
945
- display: flex;
946
- align-items: center;
947
- justify-content: center;
948
- height: 200px;
949
- `,
950
- emptyText: css5`
951
- font-size: 14px;
952
- color: #9ca3af;
953
- margin: 0;
954
- `,
955
- actions: css5`
956
- margin-top: 16px;
957
- padding-top: 16px;
958
- border-top: 1px solid #e5e7eb;
959
- display: flex;
960
- flex-direction: column;
961
- gap: 8px;
962
- `,
963
- actionBtn: css5`
964
- width: 100%;
965
- padding: 8px 12px;
966
- font-size: 14px;
967
- background-color: white;
968
- border: 1px solid #e5e7eb;
969
- border-radius: 8px;
970
- cursor: pointer;
971
- transition: background-color 0.15s;
972
- color: #374151;
973
-
974
- &:hover {
975
- background-color: #f9fafb;
976
- }
977
- `,
978
- actionBtnDanger: css5`
979
- color: #dc2626;
980
-
981
- &:hover {
982
- background-color: #fef2f2;
983
- }
984
- `
985
- };
986
- function StudioPreview() {
987
- const { selectedItems, meta, triggerRefresh, clearSelection } = useStudio();
988
- const handleDelete = async () => {
989
- if (selectedItems.size === 0) return;
990
- if (!confirm(`Delete ${selectedItems.size} item(s)?`)) return;
991
- try {
992
- const response = await fetch("/api/studio/delete", {
993
- method: "POST",
994
- headers: { "Content-Type": "application/json" },
995
- body: JSON.stringify({ paths: Array.from(selectedItems) })
996
- });
997
- if (response.ok) {
998
- clearSelection();
999
- triggerRefresh();
1000
- } else {
1001
- const error = await response.json();
1002
- alert(`Delete failed: ${error.error || "Unknown error"}`);
1003
- }
1004
- } catch (error) {
1005
- console.error("Delete error:", error);
1006
- alert("Delete failed. Check console for details.");
1007
- }
1008
- };
1009
- if (selectedItems.size === 0) {
1010
- return /* @__PURE__ */ jsxs5("div", { css: styles5.panel, children: [
1011
- /* @__PURE__ */ jsx5("h3", { css: styles5.title, children: "Preview" }),
1012
- /* @__PURE__ */ jsx5("div", { css: styles5.emptyState, children: /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "Select an image to preview" }) })
1013
- ] });
1014
- }
1015
- if (selectedItems.size > 1) {
1016
- return /* @__PURE__ */ jsxs5("div", { css: styles5.panel, children: [
1017
- /* @__PURE__ */ jsxs5("h3", { css: styles5.title, children: [
1018
- selectedItems.size,
1019
- " items selected"
1020
- ] }),
1021
- /* @__PURE__ */ jsx5("div", { css: styles5.actions, children: /* @__PURE__ */ jsxs5("button", { css: [styles5.actionBtn, styles5.actionBtnDanger], onClick: handleDelete, children: [
1022
- "Delete ",
1023
- selectedItems.size,
1024
- " items"
1025
- ] }) })
1026
- ] });
1027
- }
1028
- const selectedPath = Array.from(selectedItems)[0];
1029
- const imageKey = selectedPath.replace(/^public\/images\//, "").replace(/^public\/originals\//, "");
1030
- const imageData = meta?.images?.[imageKey];
1031
- return /* @__PURE__ */ jsxs5("div", { css: styles5.panel, children: [
1032
- /* @__PURE__ */ jsx5("h3", { css: styles5.title, children: "Preview" }),
1033
- /* @__PURE__ */ jsx5("div", { css: styles5.imageContainer, children: /* @__PURE__ */ jsx5(
1034
- "img",
1035
- {
1036
- css: styles5.image,
1037
- src: selectedPath.replace("public", ""),
1038
- alt: "Preview"
1039
- }
1040
- ) }),
1041
- /* @__PURE__ */ jsxs5("div", { css: styles5.info, children: [
1042
- /* @__PURE__ */ jsx5(InfoRow, { label: "Filename", value: selectedPath.split("/").pop() || "" }),
1043
- imageData && /* @__PURE__ */ jsxs5(Fragment, { children: [
1044
- /* @__PURE__ */ jsx5(
1045
- InfoRow,
1046
- {
1047
- label: "Original",
1048
- value: `${imageData.original.width}x${imageData.original.height}`
1049
- }
1050
- ),
1051
- /* @__PURE__ */ jsx5(
1052
- InfoRow,
1053
- {
1054
- label: "File size",
1055
- value: formatFileSize3(imageData.original.fileSize)
1056
- }
1057
- ),
1058
- /* @__PURE__ */ jsxs5("div", { css: styles5.section, children: [
1059
- /* @__PURE__ */ jsx5("p", { css: styles5.sectionTitle, children: "Generated sizes" }),
1060
- Object.entries(imageData.sizes).map(([size, data]) => /* @__PURE__ */ jsx5(InfoRow, { label: size, value: `${data.width}x${data.height}` }, size))
1061
- ] }),
1062
- imageData.cdn?.synced && /* @__PURE__ */ jsxs5("div", { css: styles5.section, children: [
1063
- /* @__PURE__ */ jsx5("p", { css: styles5.sectionTitle, children: "CDN" }),
1064
- /* @__PURE__ */ jsxs5("div", { css: styles5.cdnStatus, children: [
1065
- /* @__PURE__ */ jsx5("svg", { css: styles5.cdnIcon, fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx5("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" }) }),
1066
- "Synced to CDN"
1067
- ] }),
1068
- /* @__PURE__ */ jsx5(
1069
- "button",
1070
- {
1071
- css: styles5.copyBtn,
1072
- onClick: () => {
1073
- navigator.clipboard.writeText(`${imageData.cdn?.baseUrl}${imageData.sizes.full.path}`);
1074
- },
1075
- children: "Copy CDN URL"
1076
- }
1077
- )
1078
- ] }),
1079
- imageData.blurhash && /* @__PURE__ */ jsxs5("div", { css: styles5.section, children: [
1080
- /* @__PURE__ */ jsx5(InfoRow, { label: "Blurhash", value: imageData.blurhash, truncate: true }),
1081
- /* @__PURE__ */ jsx5(
1082
- "div",
1083
- {
1084
- css: styles5.colorSwatch,
1085
- style: { backgroundColor: imageData.dominantColor },
1086
- title: `Dominant color: ${imageData.dominantColor}`
1087
- }
1088
- )
1089
- ] })
1090
- ] })
1091
- ] }),
1092
- /* @__PURE__ */ jsxs5("div", { css: styles5.actions, children: [
1093
- /* @__PURE__ */ jsx5("button", { css: styles5.actionBtn, children: "Rename" }),
1094
- /* @__PURE__ */ jsx5("button", { css: [styles5.actionBtn, styles5.actionBtnDanger], onClick: handleDelete, children: "Delete" })
1095
- ] })
1096
- ] });
1097
- }
1098
- function InfoRow({ label, value, truncate }) {
1099
- return /* @__PURE__ */ jsxs5("div", { css: styles5.row, children: [
1100
- /* @__PURE__ */ jsx5("span", { css: styles5.label, children: label }),
1101
- /* @__PURE__ */ jsx5("span", { css: [styles5.value, truncate && styles5.valueTruncate], title: truncate ? value : void 0, children: value })
1102
- ] });
1103
- }
1104
- function formatFileSize3(bytes) {
1105
- if (bytes < 1024) return `${bytes} B`;
1106
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1107
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1108
- }
1109
-
1110
- // src/components/StudioSettings.tsx
1111
- import { useState as useState4 } from "react";
1112
- import { css as css6 } from "@emotion/react";
1113
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
1114
- var styles6 = {
1115
- btn: css6`
1116
- padding: 8px;
1117
- background: none;
1118
- border: none;
1119
- border-radius: 8px;
1120
- cursor: pointer;
1121
- transition: background-color 0.15s;
1122
-
1123
- &:hover {
1124
- background-color: #f3f4f6;
1125
- }
1126
- `,
1127
- icon: css6`
1128
- width: 20px;
1129
- height: 20px;
1130
- color: #6b7280;
1131
- `,
1132
- overlay: css6`
1133
- position: fixed;
1134
- top: 0;
1135
- right: 0;
1136
- bottom: 0;
1137
- left: 0;
1138
- z-index: 10000;
1139
- display: flex;
1140
- align-items: center;
1141
- justify-content: center;
1142
- `,
1143
- backdrop: css6`
1144
- position: absolute;
1145
- top: 0;
1146
- right: 0;
1147
- bottom: 0;
1148
- left: 0;
1149
- background-color: rgba(0, 0, 0, 0.3);
1150
- `,
1151
- panel: css6`
1152
- position: relative;
1153
- background-color: white;
1154
- border-radius: 12px;
1155
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1156
- width: 100%;
1157
- max-width: 512px;
1158
- padding: 24px;
1159
- `,
1160
- header: css6`
1161
- display: flex;
1162
- align-items: center;
1163
- justify-content: space-between;
1164
- margin-bottom: 24px;
1165
- `,
1166
- title: css6`
1167
- font-size: 18px;
1168
- font-weight: 600;
1169
- margin: 0;
1170
- `,
1171
- closeBtn: css6`
1172
- padding: 4px;
1173
- background: none;
1174
- border: none;
1175
- border-radius: 8px;
1176
- cursor: pointer;
1177
-
1178
- &:hover {
1179
- background-color: #f3f4f6;
1180
- }
1181
- `,
1182
- sections: css6`
1183
- display: flex;
1184
- flex-direction: column;
1185
- gap: 24px;
1186
- `,
1187
- sectionTitle: css6`
1188
- font-size: 14px;
1189
- font-weight: 500;
1190
- color: #111827;
1191
- margin: 0 0 12px 0;
1192
- `,
1193
- description: css6`
1194
- font-size: 12px;
1195
- color: #6b7280;
1196
- margin: 0 0 12px 0;
1197
- `,
1198
- code: css6`
1199
- background-color: #f9fafb;
1200
- border-radius: 8px;
1201
- padding: 12px;
1202
- font-family: monospace;
1203
- font-size: 12px;
1204
- color: #4b5563;
1205
- `,
1206
- codeLine: css6`
1207
- margin: 0 0 4px 0;
1208
-
1209
- &:last-child {
1210
- margin: 0;
1211
- }
1212
- `,
1213
- input: css6`
1214
- width: 100%;
1215
- padding: 8px 12px;
1216
- border: 1px solid #e5e7eb;
1217
- border-radius: 8px;
1218
- font-size: 14px;
1219
-
1220
- &:focus {
1221
- outline: none;
1222
- box-shadow: 0 0 0 2px #a855f7;
1223
- }
1224
- `,
1225
- grid: css6`
1226
- display: grid;
1227
- grid-template-columns: repeat(3, 1fr);
1228
- gap: 12px;
1229
- `,
1230
- label: css6`
1231
- font-size: 12px;
1232
- color: #6b7280;
1233
- display: block;
1234
- margin-bottom: 4px;
1235
- `,
1236
- footer: css6`
1237
- margin-top: 24px;
1238
- display: flex;
1239
- justify-content: flex-end;
1240
- gap: 12px;
1241
- `,
1242
- cancelBtn: css6`
1243
- padding: 8px 16px;
1244
- font-size: 14px;
1245
- color: #4b5563;
1246
- background: none;
1247
- border: none;
1248
- border-radius: 8px;
1249
- cursor: pointer;
1250
-
1251
- &:hover {
1252
- background-color: #f3f4f6;
1253
- }
1254
- `,
1255
- saveBtn: css6`
1256
- padding: 8px 16px;
1257
- font-size: 14px;
1258
- color: white;
1259
- background-color: #9333ea;
1260
- border: none;
1261
- border-radius: 8px;
1262
- cursor: pointer;
1263
-
1264
- &:hover {
1265
- background-color: #7c3aed;
1266
- }
1267
- `
1268
- };
1269
- function StudioSettings() {
1270
- const [isOpen, setIsOpen] = useState4(false);
1271
- return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1272
- /* @__PURE__ */ jsx6("button", { css: styles6.btn, onClick: () => setIsOpen(true), "aria-label": "Settings", children: /* @__PURE__ */ jsxs6(
1273
- "svg",
1274
- {
1275
- css: styles6.icon,
1276
- xmlns: "http://www.w3.org/2000/svg",
1277
- viewBox: "0 0 24 24",
1278
- fill: "none",
1279
- stroke: "currentColor",
1280
- strokeWidth: 2,
1281
- strokeLinecap: "round",
1282
- strokeLinejoin: "round",
1283
- children: [
1284
- /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "3" }),
1285
- /* @__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" })
1286
- ]
1287
- }
1288
- ) }),
1289
- isOpen && /* @__PURE__ */ jsx6(SettingsPanel, { onClose: () => setIsOpen(false) })
1290
- ] });
1291
- }
1292
- function SettingsPanel({ onClose }) {
1293
- return /* @__PURE__ */ jsxs6("div", { css: styles6.overlay, children: [
1294
- /* @__PURE__ */ jsx6("div", { css: styles6.backdrop, onClick: onClose }),
1295
- /* @__PURE__ */ jsxs6("div", { css: styles6.panel, children: [
1296
- /* @__PURE__ */ jsxs6("div", { css: styles6.header, children: [
1297
- /* @__PURE__ */ jsx6("h2", { css: styles6.title, children: "Settings" }),
1298
- /* @__PURE__ */ jsx6("button", { css: styles6.closeBtn, onClick: onClose, children: /* @__PURE__ */ jsx6("svg", { css: styles6.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })
1299
- ] }),
1300
- /* @__PURE__ */ jsxs6("div", { css: styles6.sections, children: [
1301
- /* @__PURE__ */ jsxs6("section", { children: [
1302
- /* @__PURE__ */ jsx6("h3", { css: styles6.sectionTitle, children: "Cloudflare R2" }),
1303
- /* @__PURE__ */ jsx6("p", { css: styles6.description, children: "Configure in .env.local file:" }),
1304
- /* @__PURE__ */ jsxs6("div", { css: styles6.code, children: [
1305
- /* @__PURE__ */ jsx6("p", { css: styles6.codeLine, children: "CLOUDFLARE_R2_ACCOUNT_ID" }),
1306
- /* @__PURE__ */ jsx6("p", { css: styles6.codeLine, children: "CLOUDFLARE_R2_ACCESS_KEY_ID" }),
1307
- /* @__PURE__ */ jsx6("p", { css: styles6.codeLine, children: "CLOUDFLARE_R2_SECRET_ACCESS_KEY" }),
1308
- /* @__PURE__ */ jsx6("p", { css: styles6.codeLine, children: "CLOUDFLARE_R2_BUCKET_NAME" }),
1309
- /* @__PURE__ */ jsx6("p", { css: styles6.codeLine, children: "CLOUDFLARE_R2_PUBLIC_URL" })
1310
- ] })
1311
- ] }),
1312
- /* @__PURE__ */ jsxs6("section", { children: [
1313
- /* @__PURE__ */ jsx6("h3", { css: styles6.sectionTitle, children: "Custom CDN URL" }),
1314
- /* @__PURE__ */ jsx6("p", { css: styles6.description, children: "Override the default R2 URL with a custom domain:" }),
1315
- /* @__PURE__ */ jsx6("input", { css: styles6.input, type: "text", placeholder: "https://cdn.yourdomain.com" })
1316
- ] }),
1317
- /* @__PURE__ */ jsxs6("section", { children: [
1318
- /* @__PURE__ */ jsx6("h3", { css: styles6.sectionTitle, children: "Thumbnail Sizes" }),
1319
- /* @__PURE__ */ jsxs6("div", { css: styles6.grid, children: [
1320
- /* @__PURE__ */ jsxs6("div", { children: [
1321
- /* @__PURE__ */ jsx6("label", { css: styles6.label, children: "Small" }),
1322
- /* @__PURE__ */ jsx6("input", { css: styles6.input, type: "number", defaultValue: 300 })
1323
- ] }),
1324
- /* @__PURE__ */ jsxs6("div", { children: [
1325
- /* @__PURE__ */ jsx6("label", { css: styles6.label, children: "Medium" }),
1326
- /* @__PURE__ */ jsx6("input", { css: styles6.input, type: "number", defaultValue: 700 })
1327
- ] }),
1328
- /* @__PURE__ */ jsxs6("div", { children: [
1329
- /* @__PURE__ */ jsx6("label", { css: styles6.label, children: "Large" }),
1330
- /* @__PURE__ */ jsx6("input", { css: styles6.input, type: "number", defaultValue: 1400 })
1331
- ] })
1332
- ] })
1333
- ] })
1334
- ] }),
1335
- /* @__PURE__ */ jsxs6("div", { css: styles6.footer, children: [
1336
- /* @__PURE__ */ jsx6("button", { css: styles6.cancelBtn, onClick: onClose, children: "Cancel" }),
1337
- /* @__PURE__ */ jsx6("button", { css: styles6.saveBtn, children: "Save Changes" })
1338
- ] })
1339
- ] })
1340
- ] });
1341
- }
1342
-
1343
- // src/components/StudioUI.tsx
1344
- import { jsx as jsx7, jsxs as jsxs7 } from "@emotion/react/jsx-runtime";
1345
- var styles7 = {
1346
- container: css7`
1347
- display: flex;
1348
- flex-direction: column;
1349
- height: 100%;
1350
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1351
- `,
1352
- header: css7`
1353
- display: flex;
1354
- align-items: center;
1355
- justify-content: space-between;
1356
- padding: 16px 24px;
1357
- border-bottom: 1px solid #e5e7eb;
1358
- `,
1359
- title: css7`
1360
- font-size: 20px;
1361
- font-weight: 600;
1362
- color: #111827;
1363
- margin: 0;
1364
- `,
1365
- headerActions: css7`
1366
- display: flex;
1367
- align-items: center;
1368
- gap: 8px;
1369
- `,
1370
- closeBtn: css7`
1371
- padding: 8px;
1372
- background: none;
1373
- border: none;
1374
- border-radius: 8px;
1375
- cursor: pointer;
1376
- transition: background-color 0.15s;
1377
-
1378
- &:hover {
1379
- background-color: #f3f4f6;
1380
- }
1381
- `,
1382
- closeIcon: css7`
1383
- width: 20px;
1384
- height: 20px;
1385
- color: #6b7280;
1386
- `,
1387
- content: css7`
1388
- flex: 1;
1389
- display: flex;
1390
- overflow: hidden;
1391
- `,
1392
- fileBrowser: css7`
1393
- flex: 1;
1394
- min-width: 0;
1395
- overflow: auto;
1396
- padding: 16px;
1397
- `
1398
- };
1399
- function StudioUI({ onClose }) {
1400
- const [currentPath, setCurrentPathInternal] = useState5("public");
1401
- const [selectedItems, setSelectedItems] = useState5(/* @__PURE__ */ new Set());
1402
- const [viewMode, setViewMode] = useState5("grid");
1403
- const [meta, setMeta] = useState5(null);
1404
- const [isLoading, setIsLoading] = useState5(false);
1405
- const [refreshKey, setRefreshKey] = useState5(0);
1406
- const triggerRefresh = useCallback2(() => {
1407
- setRefreshKey((k) => k + 1);
1408
- }, []);
1409
- const navigateUp = useCallback2(() => {
1410
- if (currentPath === "public") return;
1411
- const parts = currentPath.split("/");
1412
- parts.pop();
1413
- setCurrentPathInternal(parts.join("/") || "public");
1414
- setSelectedItems(/* @__PURE__ */ new Set());
1415
- }, [currentPath]);
1416
- const setCurrentPath = useCallback2((path) => {
1417
- setCurrentPathInternal(path);
1418
- setSelectedItems(/* @__PURE__ */ new Set());
1419
- }, []);
1420
- const toggleSelection = useCallback2((path) => {
1421
- setSelectedItems((prev) => {
1422
- const next = new Set(prev);
1423
- if (next.has(path)) {
1424
- next.delete(path);
1425
- } else {
1426
- next.add(path);
1427
- }
1428
- return next;
1429
- });
1430
- }, []);
1431
- const selectAll = useCallback2((items) => {
1432
- setSelectedItems(new Set(items.map((item) => item.path)));
1433
- }, []);
1434
- const clearSelection = useCallback2(() => {
1435
- setSelectedItems(/* @__PURE__ */ new Set());
1436
- }, []);
1437
- const handleKeyDown = useCallback2(
1438
- (e) => {
1439
- if (e.key === "Escape") {
1440
- onClose();
1441
- }
1442
- },
1443
- [onClose]
1444
- );
1445
- useEffect3(() => {
1446
- document.addEventListener("keydown", handleKeyDown);
1447
- document.body.style.overflow = "hidden";
1448
- return () => {
1449
- document.removeEventListener("keydown", handleKeyDown);
1450
- document.body.style.overflow = "";
1451
- };
1452
- }, [handleKeyDown]);
1453
- const contextValue = {
1454
- isOpen: true,
1455
- openStudio: () => {
1456
- },
1457
- closeStudio: onClose,
1458
- toggleStudio: onClose,
1459
- currentPath,
1460
- setCurrentPath,
1461
- navigateUp,
1462
- selectedItems,
1463
- toggleSelection,
1464
- selectAll,
1465
- clearSelection,
1466
- viewMode,
1467
- setViewMode,
1468
- meta,
1469
- setMeta,
1470
- isLoading,
1471
- setIsLoading,
1472
- refreshKey,
1473
- triggerRefresh
1474
- };
1475
- return /* @__PURE__ */ jsx7(StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs7("div", { css: styles7.container, children: [
1476
- /* @__PURE__ */ jsxs7("div", { css: styles7.header, children: [
1477
- /* @__PURE__ */ jsx7("h1", { css: styles7.title, children: "Studio" }),
1478
- /* @__PURE__ */ jsxs7("div", { css: styles7.headerActions, children: [
1479
- /* @__PURE__ */ jsx7(StudioSettings, {}),
1480
- /* @__PURE__ */ jsx7(
1481
- "button",
1482
- {
1483
- css: styles7.closeBtn,
1484
- onClick: onClose,
1485
- "aria-label": "Close Studio",
1486
- children: /* @__PURE__ */ jsx7(CloseIcon, {})
1487
- }
1488
- )
1489
- ] })
1490
- ] }),
1491
- /* @__PURE__ */ jsx7(StudioToolbar, {}),
1492
- /* @__PURE__ */ jsx7(StudioBreadcrumb, {}),
1493
- /* @__PURE__ */ jsxs7("div", { css: styles7.content, children: [
1494
- /* @__PURE__ */ jsx7("div", { css: styles7.fileBrowser, children: viewMode === "grid" ? /* @__PURE__ */ jsx7(StudioFileGrid, {}) : /* @__PURE__ */ jsx7(StudioFileList, {}) }),
1495
- /* @__PURE__ */ jsx7(StudioPreview, {})
1496
- ] })
1497
- ] }) });
1498
- }
1499
- function CloseIcon() {
1500
- return /* @__PURE__ */ jsxs7(
1501
- "svg",
1502
- {
1503
- css: styles7.closeIcon,
1504
- xmlns: "http://www.w3.org/2000/svg",
1505
- viewBox: "0 0 24 24",
1506
- fill: "none",
1507
- stroke: "currentColor",
1508
- strokeWidth: 2,
1509
- strokeLinecap: "round",
1510
- strokeLinejoin: "round",
1511
- children: [
1512
- /* @__PURE__ */ jsx7("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1513
- /* @__PURE__ */ jsx7("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1514
- ]
1515
- }
1516
- );
1517
- }
1518
- var StudioUI_default = StudioUI;
1519
- export {
1520
- StudioUI,
1521
- StudioUI_default as default
1522
- };
1523
- //# sourceMappingURL=StudioUI-PW3ZGLXL.mjs.map