@gallop.software/studio 0.1.24 → 0.1.25
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.
- package/dist/{StudioUI-YO6WPG5E.js → StudioUI-4TDLHJCA.js} +208 -33
- package/dist/StudioUI-4TDLHJCA.js.map +1 -0
- package/dist/{StudioUI-F2C4N66F.mjs → StudioUI-BPOKRRW7.mjs} +214 -39
- package/dist/StudioUI-BPOKRRW7.mjs.map +1 -0
- package/dist/handlers.d.mts +2 -24
- package/dist/handlers.d.ts +2 -24
- package/dist/handlers.js +175 -149
- package/dist/handlers.js.map +1 -1
- package/dist/handlers.mjs +175 -149
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{types-DIXDq6Cy.d.mts → types-lg2VkHIb.d.mts} +1 -1
- package/dist/{types-DIXDq6Cy.d.ts → types-lg2VkHIb.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/StudioUI-F2C4N66F.mjs.map +0 -1
- package/dist/StudioUI-YO6WPG5E.js.map +0 -1
|
@@ -204,6 +204,98 @@ function AlertModal({
|
|
|
204
204
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.footer, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: [styles.btn, styles.btnConfirm], onClick: onClose, children: buttonLabel }) })
|
|
205
205
|
] }) });
|
|
206
206
|
}
|
|
207
|
+
var progressStyles = {
|
|
208
|
+
progressContainer: _react3.css`
|
|
209
|
+
margin-top: 16px;
|
|
210
|
+
`,
|
|
211
|
+
progressBar: _react3.css`
|
|
212
|
+
width: 100%;
|
|
213
|
+
height: 8px;
|
|
214
|
+
background-color: ${_chunkAY2DAS6Wjs.colors.background};
|
|
215
|
+
border-radius: 4px;
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
margin-bottom: 12px;
|
|
218
|
+
`,
|
|
219
|
+
progressFill: _react3.css`
|
|
220
|
+
height: 100%;
|
|
221
|
+
background: linear-gradient(90deg, ${_chunkAY2DAS6Wjs.colors.primary}, ${_chunkAY2DAS6Wjs.colors.primaryHover});
|
|
222
|
+
border-radius: 4px;
|
|
223
|
+
transition: width 0.3s ease;
|
|
224
|
+
`,
|
|
225
|
+
progressText: _react3.css`
|
|
226
|
+
font-size: ${_chunkAY2DAS6Wjs.fontSize.sm};
|
|
227
|
+
color: ${_chunkAY2DAS6Wjs.colors.textSecondary};
|
|
228
|
+
margin: 0;
|
|
229
|
+
display: flex;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
align-items: center;
|
|
232
|
+
`,
|
|
233
|
+
currentFile: _react3.css`
|
|
234
|
+
font-size: ${_chunkAY2DAS6Wjs.fontSize.xs};
|
|
235
|
+
color: ${_chunkAY2DAS6Wjs.colors.textMuted};
|
|
236
|
+
margin: 8px 0 0;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
text-overflow: ellipsis;
|
|
240
|
+
`
|
|
241
|
+
};
|
|
242
|
+
function ProgressModal({
|
|
243
|
+
title,
|
|
244
|
+
progress,
|
|
245
|
+
onClose
|
|
246
|
+
}) {
|
|
247
|
+
const isComplete = progress.status === "complete";
|
|
248
|
+
const isError = progress.status === "error";
|
|
249
|
+
const canClose = isComplete || isError;
|
|
250
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.overlay, onClick: canClose ? onClose : void 0, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles.modal, onClick: (e) => e.stopPropagation(), children: [
|
|
251
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.header, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { css: styles.title, children: title }) }),
|
|
252
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.body, children: isError ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles.message, children: progress.message || "An error occurred" }) : isComplete ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { css: styles.message, children: [
|
|
253
|
+
"Processed ",
|
|
254
|
+
progress.processed,
|
|
255
|
+
" image",
|
|
256
|
+
progress.processed !== 1 ? "s" : "",
|
|
257
|
+
".",
|
|
258
|
+
progress.orphansRemoved && progress.orphansRemoved > 0 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
259
|
+
" Removed ",
|
|
260
|
+
progress.orphansRemoved,
|
|
261
|
+
" orphaned thumbnail",
|
|
262
|
+
progress.orphansRemoved !== 1 ? "s" : "",
|
|
263
|
+
"."
|
|
264
|
+
] }),
|
|
265
|
+
progress.errors && progress.errors > 0 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
266
|
+
" ",
|
|
267
|
+
progress.errors,
|
|
268
|
+
" error",
|
|
269
|
+
progress.errors !== 1 ? "s" : "",
|
|
270
|
+
" occurred."
|
|
271
|
+
] })
|
|
272
|
+
] }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
273
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: styles.message, children: progress.status === "cleanup" ? "Cleaning up orphaned files..." : `Processing images...` }),
|
|
274
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: progressStyles.progressContainer, children: [
|
|
275
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: progressStyles.progressBar, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
276
|
+
"div",
|
|
277
|
+
{
|
|
278
|
+
css: progressStyles.progressFill,
|
|
279
|
+
style: { width: `${progress.percent}%` }
|
|
280
|
+
}
|
|
281
|
+
) }),
|
|
282
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: progressStyles.progressText, children: [
|
|
283
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { children: [
|
|
284
|
+
progress.current,
|
|
285
|
+
" of ",
|
|
286
|
+
progress.total
|
|
287
|
+
] }),
|
|
288
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { children: [
|
|
289
|
+
progress.percent,
|
|
290
|
+
"%"
|
|
291
|
+
] })
|
|
292
|
+
] }),
|
|
293
|
+
progress.currentFile && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { css: progressStyles.currentFile, title: progress.currentFile, children: progress.currentFile })
|
|
294
|
+
] })
|
|
295
|
+
] }) }),
|
|
296
|
+
canClose && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.footer, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { css: [styles.btn, styles.btnConfirm], onClick: onClose, children: "Done" }) })
|
|
297
|
+
] }) });
|
|
298
|
+
}
|
|
207
299
|
|
|
208
300
|
// src/components/StudioToolbar.tsx
|
|
209
301
|
|
|
@@ -351,6 +443,13 @@ function StudioToolbar() {
|
|
|
351
443
|
const [processing, setProcessing] = _react.useState.call(void 0, false);
|
|
352
444
|
const [showDeleteConfirm, setShowDeleteConfirm] = _react.useState.call(void 0, false);
|
|
353
445
|
const [showProcessConfirm, setShowProcessConfirm] = _react.useState.call(void 0, false);
|
|
446
|
+
const [showProgress, setShowProgress] = _react.useState.call(void 0, false);
|
|
447
|
+
const [progressState, setProgressState] = _react.useState.call(void 0, {
|
|
448
|
+
current: 0,
|
|
449
|
+
total: 0,
|
|
450
|
+
percent: 0,
|
|
451
|
+
status: "processing"
|
|
452
|
+
});
|
|
354
453
|
const [processCount, setProcessCount] = _react.useState.call(void 0, 0);
|
|
355
454
|
const [processMode, setProcessMode] = _react.useState.call(void 0, "all");
|
|
356
455
|
const [alertMessage, setAlertMessage] = _react.useState.call(void 0, null);
|
|
@@ -425,12 +524,12 @@ function StudioToolbar() {
|
|
|
425
524
|
setShowProcessConfirm(true);
|
|
426
525
|
} else {
|
|
427
526
|
try {
|
|
428
|
-
const response = await fetch("/api/studio/count-
|
|
527
|
+
const response = await fetch("/api/studio/count-images");
|
|
429
528
|
const data = await response.json();
|
|
430
529
|
if (data.count === 0) {
|
|
431
530
|
setAlertMessage({
|
|
432
|
-
title: "
|
|
433
|
-
message: "
|
|
531
|
+
title: "No Images Found",
|
|
532
|
+
message: "No images found in the public folder to process."
|
|
434
533
|
});
|
|
435
534
|
return;
|
|
436
535
|
}
|
|
@@ -438,10 +537,10 @@ function StudioToolbar() {
|
|
|
438
537
|
setProcessMode("all");
|
|
439
538
|
setShowProcessConfirm(true);
|
|
440
539
|
} catch (error) {
|
|
441
|
-
console.error("Failed to count
|
|
540
|
+
console.error("Failed to count images:", error);
|
|
442
541
|
setAlertMessage({
|
|
443
542
|
title: "Error",
|
|
444
|
-
message: "Failed to count
|
|
543
|
+
message: "Failed to count images."
|
|
445
544
|
});
|
|
446
545
|
}
|
|
447
546
|
}
|
|
@@ -451,30 +550,80 @@ function StudioToolbar() {
|
|
|
451
550
|
setProcessing(true);
|
|
452
551
|
try {
|
|
453
552
|
if (processMode === "all") {
|
|
553
|
+
setShowProgress(true);
|
|
554
|
+
setProgressState({
|
|
555
|
+
current: 0,
|
|
556
|
+
total: processCount,
|
|
557
|
+
percent: 0,
|
|
558
|
+
status: "processing"
|
|
559
|
+
});
|
|
454
560
|
const response = await fetch("/api/studio/process-all", {
|
|
455
561
|
method: "POST"
|
|
456
562
|
});
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
563
|
+
if (!response.body) {
|
|
564
|
+
throw new Error("No response body");
|
|
565
|
+
}
|
|
566
|
+
const reader = response.body.getReader();
|
|
567
|
+
const decoder = new TextDecoder();
|
|
568
|
+
while (true) {
|
|
569
|
+
const { done, value } = await reader.read();
|
|
570
|
+
if (done) break;
|
|
571
|
+
const text = decoder.decode(value);
|
|
572
|
+
const lines = text.split("\n\n").filter((line) => line.startsWith("data: "));
|
|
573
|
+
for (const line of lines) {
|
|
574
|
+
try {
|
|
575
|
+
const data = JSON.parse(line.replace("data: ", ""));
|
|
576
|
+
if (data.type === "start") {
|
|
577
|
+
setProgressState((prev) => ({
|
|
578
|
+
...prev,
|
|
579
|
+
total: data.total
|
|
580
|
+
}));
|
|
581
|
+
} else if (data.type === "progress") {
|
|
582
|
+
setProgressState({
|
|
583
|
+
current: data.current,
|
|
584
|
+
total: data.total,
|
|
585
|
+
percent: data.percent,
|
|
586
|
+
currentFile: data.currentFile,
|
|
587
|
+
status: "processing"
|
|
588
|
+
});
|
|
589
|
+
} else if (data.type === "cleanup") {
|
|
590
|
+
setProgressState((prev) => ({
|
|
591
|
+
...prev,
|
|
592
|
+
status: "cleanup",
|
|
593
|
+
currentFile: void 0
|
|
594
|
+
}));
|
|
595
|
+
} else if (data.type === "complete") {
|
|
596
|
+
setProgressState({
|
|
597
|
+
current: data.processed,
|
|
598
|
+
total: data.processed,
|
|
599
|
+
percent: 100,
|
|
600
|
+
status: "complete",
|
|
601
|
+
processed: data.processed,
|
|
602
|
+
orphansRemoved: data.orphansRemoved,
|
|
603
|
+
errors: data.errors
|
|
604
|
+
});
|
|
605
|
+
triggerRefresh();
|
|
606
|
+
} else if (data.type === "error") {
|
|
607
|
+
setProgressState((prev) => ({
|
|
608
|
+
...prev,
|
|
609
|
+
status: "error",
|
|
610
|
+
message: data.message
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
} catch (e2) {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
474
616
|
}
|
|
475
617
|
} else {
|
|
618
|
+
setShowProgress(true);
|
|
619
|
+
setProgressState({
|
|
620
|
+
current: 0,
|
|
621
|
+
total: processCount,
|
|
622
|
+
percent: 0,
|
|
623
|
+
status: "processing"
|
|
624
|
+
});
|
|
476
625
|
const selectedImageKeys = Array.from(selectedItems).filter((p) => {
|
|
477
|
-
const ext = _optionalChain([p, 'access',
|
|
626
|
+
const ext = _optionalChain([p, 'access', _10 => _10.split, 'call', _11 => _11("."), 'access', _12 => _12.pop, 'call', _13 => _13(), 'optionalAccess', _14 => _14.toLowerCase, 'call', _15 => _15()]) || "";
|
|
478
627
|
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"].includes(ext);
|
|
479
628
|
}).map((p) => p.replace(/^public\//, ""));
|
|
480
629
|
const response = await fetch("/api/studio/reprocess", {
|
|
@@ -484,29 +633,39 @@ function StudioToolbar() {
|
|
|
484
633
|
});
|
|
485
634
|
const data = await response.json();
|
|
486
635
|
if (response.ok) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
636
|
+
setProgressState({
|
|
637
|
+
current: _optionalChain([data, 'access', _16 => _16.processed, 'optionalAccess', _17 => _17.length]) || 0,
|
|
638
|
+
total: _optionalChain([data, 'access', _18 => _18.processed, 'optionalAccess', _19 => _19.length]) || 0,
|
|
639
|
+
percent: 100,
|
|
640
|
+
status: "complete",
|
|
641
|
+
processed: _optionalChain([data, 'access', _20 => _20.processed, 'optionalAccess', _21 => _21.length]) || 0,
|
|
642
|
+
errors: _optionalChain([data, 'access', _22 => _22.errors, 'optionalAccess', _23 => _23.length]) || 0
|
|
490
643
|
});
|
|
491
644
|
clearSelection();
|
|
492
645
|
triggerRefresh();
|
|
493
646
|
} else {
|
|
494
|
-
|
|
495
|
-
|
|
647
|
+
setProgressState({
|
|
648
|
+
current: 0,
|
|
649
|
+
total: 0,
|
|
650
|
+
percent: 0,
|
|
651
|
+
status: "error",
|
|
496
652
|
message: data.error || "Unknown error"
|
|
497
653
|
});
|
|
498
654
|
}
|
|
499
655
|
}
|
|
500
656
|
} catch (error) {
|
|
501
657
|
console.error("Processing error:", error);
|
|
502
|
-
|
|
503
|
-
|
|
658
|
+
setProgressState({
|
|
659
|
+
current: 0,
|
|
660
|
+
total: 0,
|
|
661
|
+
percent: 0,
|
|
662
|
+
status: "error",
|
|
504
663
|
message: "Processing failed. Check console for details."
|
|
505
664
|
});
|
|
506
665
|
} finally {
|
|
507
666
|
setProcessing(false);
|
|
508
667
|
}
|
|
509
|
-
}, [processMode, selectedItems, clearSelection, triggerRefresh]);
|
|
668
|
+
}, [processMode, processCount, selectedItems, clearSelection, triggerRefresh]);
|
|
510
669
|
const handleDeleteClick = _react.useCallback.call(void 0, () => {
|
|
511
670
|
if (selectedItems.size === 0) return;
|
|
512
671
|
setShowDeleteConfirm(true);
|
|
@@ -563,12 +722,28 @@ function StudioToolbar() {
|
|
|
563
722
|
ConfirmModal,
|
|
564
723
|
{
|
|
565
724
|
title: "Process Images",
|
|
566
|
-
message: processMode === "all" ? `Found ${processCount}
|
|
725
|
+
message: processMode === "all" ? `Found ${processCount} image${processCount !== 1 ? "s" : ""} in the public folder. This will regenerate all thumbnails and remove any orphaned files from the images folder.` : `Process ${processCount} selected image${processCount !== 1 ? "s" : ""}? This will regenerate thumbnails for these files.`,
|
|
567
726
|
confirmLabel: processing ? "Processing..." : "Process",
|
|
568
727
|
onConfirm: handleProcessConfirm,
|
|
569
728
|
onCancel: () => setShowProcessConfirm(false)
|
|
570
729
|
}
|
|
571
730
|
),
|
|
731
|
+
showProgress && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
732
|
+
ProgressModal,
|
|
733
|
+
{
|
|
734
|
+
title: "Processing Images",
|
|
735
|
+
progress: progressState,
|
|
736
|
+
onClose: () => {
|
|
737
|
+
setShowProgress(false);
|
|
738
|
+
setProgressState({
|
|
739
|
+
current: 0,
|
|
740
|
+
total: 0,
|
|
741
|
+
percent: 0,
|
|
742
|
+
status: "processing"
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
),
|
|
572
747
|
alertMessage && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
573
748
|
AlertModal,
|
|
574
749
|
{
|
|
@@ -2222,4 +2397,4 @@ var StudioUI_default = StudioUI;
|
|
|
2222
2397
|
|
|
2223
2398
|
|
|
2224
2399
|
exports.StudioUI = StudioUI; exports.default = StudioUI_default;
|
|
2225
|
-
//# sourceMappingURL=StudioUI-
|
|
2400
|
+
//# sourceMappingURL=StudioUI-4TDLHJCA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/StudioUI-4TDLHJCA.js","../src/components/StudioUI.tsx","../src/components/StudioContext.tsx","../src/components/StudioToolbar.tsx","../src/components/StudioModal.tsx","../src/components/StudioFileGrid.tsx","../src/components/StudioFileList.tsx","../src/components/StudioDetailView.tsx","../src/components/StudioSettings.tsx"],"names":["Fragment","keyframes","css","jsxs","jsx","useState","useEffect","useCallback"],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/StudioUI-4TDLHJCA.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useCallback, useState } from 'react'\nimport { css } from '@emotion/react'\nimport { StudioContext } from './StudioContext'\nimport { StudioToolbar } from './StudioToolbar'\nimport { StudioFileGrid } from './StudioFileGrid'\nimport { StudioFileList } from './StudioFileList'\nimport { StudioDetailView } from './StudioDetailView'\nimport { StudioSettings } from './StudioSettings'\nimport { colors, fontSize, baseReset } from './tokens'\nimport type { FileItem, StudioMeta } from '../types'\n\ninterface StudioUIProps {\n onClose: () => void\n}\n\n// Standard button height for consistency\nconst btnHeight = '36px'\n\nconst styles = {\n container: css`\n ${baseReset}\n display: flex;\n flex-direction: column;\n height: 100%;\n background: ${colors.background};\n `,\n header: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background: ${colors.surface};\n border-bottom: 1px solid ${colors.border};\n `,\n title: css`\n font-size: ${fontSize.lg};\n font-weight: 600;\n color: ${colors.text};\n margin: 0;\n letter-spacing: -0.02em;\n `,\n headerActions: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n headerBtn: css`\n height: ${btnHeight};\n padding: 0 12px;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n headerIcon: css`\n width: 16px;\n height: 16px;\n color: ${colors.textSecondary};\n `,\n content: css`\n flex: 1;\n display: flex;\n overflow: hidden;\n `,\n fileBrowser: css`\n flex: 1;\n min-width: 0;\n overflow: auto;\n padding: 20px 24px;\n `,\n}\n\n/**\n * Main Studio UI - contains all panels and manages internal state\n * Rendered inside the modal via lazy loading\n */\nexport function StudioUI({ onClose }: StudioUIProps) {\n const [currentPath, setCurrentPathInternal] = useState('public')\n const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set())\n const [lastSelectedPath, setLastSelectedPath] = useState<string | null>(null)\n const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')\n const [focusedItem, setFocusedItem] = useState<FileItem | null>(null)\n const [meta, setMeta] = useState<StudioMeta | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const [refreshKey, setRefreshKey] = useState(0)\n\n const triggerRefresh = useCallback(() => {\n setRefreshKey((k) => k + 1)\n }, [])\n\n const navigateUp = useCallback(() => {\n if (currentPath === 'public') return\n const parts = currentPath.split('/')\n parts.pop()\n setCurrentPathInternal(parts.join('/') || 'public')\n setSelectedItems(new Set())\n }, [currentPath])\n\n const setCurrentPath = useCallback((path: string) => {\n setCurrentPathInternal(path)\n setSelectedItems(new Set())\n setFocusedItem(null)\n }, [])\n\n const toggleSelection = useCallback((path: string) => {\n setSelectedItems((prev) => {\n const next = new Set(prev)\n if (next.has(path)) {\n next.delete(path)\n } else {\n next.add(path)\n }\n return next\n })\n setLastSelectedPath(path)\n }, [])\n\n const selectRange = useCallback((fromPath: string, toPath: string, allItems: FileItem[]) => {\n const fromIndex = allItems.findIndex(item => item.path === fromPath)\n const toIndex = allItems.findIndex(item => item.path === toPath)\n \n if (fromIndex === -1 || toIndex === -1) return\n \n const start = Math.min(fromIndex, toIndex)\n const end = Math.max(fromIndex, toIndex)\n \n setSelectedItems((prev) => {\n const next = new Set(prev)\n for (let i = start; i <= end; i++) {\n next.add(allItems[i].path)\n }\n return next\n })\n setLastSelectedPath(toPath)\n }, [])\n\n const selectAll = useCallback((items: FileItem[]) => {\n setSelectedItems(new Set(items.map((item) => item.path)))\n }, [])\n\n const clearSelection = useCallback(() => {\n setSelectedItems(new Set())\n }, [])\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n if (focusedItem) {\n setFocusedItem(null)\n } else {\n onClose()\n }\n }\n },\n [onClose, focusedItem]\n )\n\n useEffect(() => {\n document.addEventListener('keydown', handleKeyDown)\n document.body.style.overflow = 'hidden'\n return () => {\n document.removeEventListener('keydown', handleKeyDown)\n document.body.style.overflow = ''\n }\n }, [handleKeyDown])\n\n const contextValue = {\n isOpen: true,\n openStudio: () => {},\n closeStudio: onClose,\n toggleStudio: onClose,\n currentPath,\n setCurrentPath,\n navigateUp,\n selectedItems,\n toggleSelection,\n selectRange,\n selectAll,\n clearSelection,\n lastSelectedPath,\n viewMode,\n setViewMode,\n focusedItem,\n setFocusedItem,\n meta,\n setMeta,\n isLoading,\n setIsLoading,\n refreshKey,\n triggerRefresh,\n }\n\n return (\n <StudioContext.Provider value={contextValue}>\n <div css={styles.container}>\n <div css={styles.header}>\n <h1 css={styles.title}>Studio</h1>\n <div css={styles.headerActions}>\n <StudioSettings />\n <button\n css={styles.headerBtn}\n onClick={onClose}\n aria-label=\"Close Studio\"\n >\n <CloseIcon />\n </button>\n </div>\n </div>\n\n <StudioToolbar />\n\n <div css={styles.content}>\n {focusedItem ? (\n <StudioDetailView />\n ) : (\n <div css={styles.fileBrowser}>\n {viewMode === 'grid' ? <StudioFileGrid /> : <StudioFileList />}\n </div>\n )}\n </div>\n </div>\n </StudioContext.Provider>\n )\n}\n\nfunction CloseIcon() {\n return (\n <svg\n css={styles.headerIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nexport default StudioUI\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { FileItem, StudioMeta } from '../types'\n\n/**\n * Studio state interface\n * State is managed by StudioUI and provided to all child components\n */\nexport interface StudioState {\n isOpen: boolean\n openStudio: () => void\n closeStudio: () => void\n toggleStudio: () => void\n\n // Navigation\n currentPath: string\n setCurrentPath: (path: string) => void\n navigateUp: () => void\n\n // Selection\n selectedItems: Set<string>\n toggleSelection: (path: string) => void\n selectRange: (fromPath: string, toPath: string, allItems: FileItem[]) => void\n selectAll: (items: FileItem[]) => void\n clearSelection: () => void\n lastSelectedPath: string | null\n\n // View\n viewMode: 'grid' | 'list'\n setViewMode: (mode: 'grid' | 'list') => void\n\n // Focused item (for detail view)\n focusedItem: FileItem | null\n setFocusedItem: (item: FileItem | null) => void\n\n // Meta\n meta: StudioMeta | null\n setMeta: (meta: StudioMeta) => void\n\n // Loading\n isLoading: boolean\n setIsLoading: (loading: boolean) => void\n\n // Refresh trigger\n refreshKey: number\n triggerRefresh: () => void\n}\n\nconst defaultState: StudioState = {\n isOpen: false,\n openStudio: () => {},\n closeStudio: () => {},\n toggleStudio: () => {},\n currentPath: 'public',\n setCurrentPath: () => {},\n navigateUp: () => {},\n selectedItems: new Set(),\n toggleSelection: () => {},\n selectRange: () => {},\n selectAll: () => {},\n clearSelection: () => {},\n lastSelectedPath: null,\n viewMode: 'grid',\n setViewMode: () => {},\n focusedItem: null,\n setFocusedItem: () => {},\n meta: null,\n setMeta: () => {},\n isLoading: false,\n setIsLoading: () => {},\n refreshKey: 0,\n triggerRefresh: () => {},\n}\n\nexport const StudioContext = createContext<StudioState>(defaultState)\n\n/**\n * Hook to access Studio state from child components\n */\nexport function useStudio() {\n return useContext(StudioContext)\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useCallback, useRef, useState } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport { ConfirmModal, AlertModal, ProgressModal, type ProgressState } from './StudioModal'\nimport { colors, fontSize } from './tokens'\n\n// Standard button height for consistency\nconst btnHeight = '36px'\n\nconst spin = keyframes`\n to { transform: rotate(360deg); }\n`\n\nconst styles = {\n toolbar: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background-color: ${colors.surface};\n border-bottom: 1px solid ${colors.border};\n `,\n left: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n right: css`\n display: flex;\n align-items: center;\n gap: 8px;\n `,\n btn: css`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: ${btnHeight};\n padding: 0 14px;\n border-radius: 6px;\n font-size: ${fontSize.base};\n font-weight: 500;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n cursor: pointer;\n transition: all 0.15s ease;\n color: ${colors.text};\n letter-spacing: -0.01em;\n \n &:hover:not(:disabled) {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n \n &:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n }\n `,\n btnIconOnly: css`\n padding: 0 10px;\n `,\n btnPrimary: css`\n background: ${colors.primary};\n border-color: ${colors.primary};\n color: white;\n \n &:hover:not(:disabled) {\n background: ${colors.primaryHover};\n border-color: ${colors.primaryHover};\n }\n `,\n btnDanger: css`\n color: ${colors.danger};\n \n &:hover:not(:disabled) {\n background-color: ${colors.dangerLight};\n border-color: ${colors.danger};\n }\n `,\n icon: css`\n width: 16px;\n height: 16px;\n `,\n iconSpin: css`\n animation: ${spin} 1s linear infinite;\n `,\n selectionCount: css`\n font-size: ${fontSize.base};\n color: ${colors.textSecondary};\n display: flex;\n align-items: center;\n gap: 8px;\n margin-right: 8px;\n `,\n clearBtn: css`\n color: ${colors.primary};\n background: none;\n border: none;\n cursor: pointer;\n font-size: ${fontSize.base};\n font-weight: 500;\n padding: 0;\n \n &:hover {\n text-decoration: underline;\n }\n `,\n divider: css`\n width: 1px;\n height: 24px;\n background: ${colors.border};\n margin: 0 4px;\n `,\n viewToggle: css`\n display: flex;\n align-items: center;\n height: ${btnHeight};\n background-color: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n overflow: hidden;\n `,\n viewBtn: css`\n height: 100%;\n padding: 0 10px;\n background: transparent;\n border: none;\n cursor: pointer;\n color: ${colors.textSecondary};\n transition: all 0.15s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n \n &:hover {\n color: ${colors.text};\n background-color: ${colors.surfaceHover};\n }\n `,\n viewBtnActive: css`\n background-color: ${colors.background};\n color: ${colors.text};\n `,\n}\n\nexport function StudioToolbar() {\n const { selectedItems, viewMode, setViewMode, clearSelection, currentPath, triggerRefresh, focusedItem } = useStudio()\n const fileInputRef = useRef<HTMLInputElement>(null)\n const [uploading, setUploading] = useState(false)\n const [refreshing, setRefreshing] = useState(false)\n const [processing, setProcessing] = useState(false)\n const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)\n const [showProcessConfirm, setShowProcessConfirm] = useState(false)\n const [showProgress, setShowProgress] = useState(false)\n const [progressState, setProgressState] = useState<ProgressState>({\n current: 0,\n total: 0,\n percent: 0,\n status: 'processing',\n })\n const [processCount, setProcessCount] = useState(0)\n const [processMode, setProcessMode] = useState<'all' | 'selected'>('all')\n const [alertMessage, setAlertMessage] = useState<{ title: string; message: string } | null>(null)\n\n // Check if we're in the images folder (uploads not allowed there)\n const isInImagesFolder = currentPath === 'public/images' || currentPath.startsWith('public/images/')\n\n const handleUpload = useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n const handleRefresh = useCallback(() => {\n setRefreshing(true)\n triggerRefresh()\n setTimeout(() => setRefreshing(false), 600)\n }, [triggerRefresh])\n\n const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files\n if (!files || files.length === 0) return\n\n setUploading(true)\n try {\n for (const file of Array.from(files)) {\n const formData = new FormData()\n formData.append('file', file)\n formData.append('path', currentPath)\n\n const response = await fetch('/api/studio/upload', {\n method: 'POST',\n body: formData,\n })\n\n if (!response.ok) {\n const error = await response.json()\n if (response.status >= 500) {\n console.error('Upload error:', error)\n setAlertMessage({\n title: 'Upload Failed',\n message: `Failed to upload ${file.name}: ${error.error || 'Unknown error'}`,\n })\n } else {\n setAlertMessage({\n title: 'Cannot Upload Here',\n message: error.error || 'Upload not allowed in this location.',\n })\n }\n }\n }\n triggerRefresh()\n } catch (error) {\n console.error('Upload error:', error)\n setAlertMessage({\n title: 'Upload Failed',\n message: 'Upload failed. Check console for details.',\n })\n } finally {\n setUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n }, [currentPath, triggerRefresh])\n\n const handleProcessImages = useCallback(async () => {\n const hasSelection = selectedItems.size > 0\n \n if (hasSelection) {\n // Process selected images\n const selectedImagePaths = Array.from(selectedItems).filter(p => {\n const ext = p.split('.').pop()?.toLowerCase() || ''\n return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff', 'tif'].includes(ext)\n })\n \n if (selectedImagePaths.length === 0) {\n setAlertMessage({\n title: 'No Images Selected',\n message: 'Please select image files to process.',\n })\n return\n }\n \n setProcessCount(selectedImagePaths.length)\n setProcessMode('selected')\n setShowProcessConfirm(true)\n } else {\n // Count ALL images for \"process all\"\n try {\n const response = await fetch('/api/studio/count-images')\n const data = await response.json()\n \n if (data.count === 0) {\n setAlertMessage({\n title: 'No Images Found',\n message: 'No images found in the public folder to process.',\n })\n return\n }\n \n setProcessCount(data.count)\n setProcessMode('all')\n setShowProcessConfirm(true)\n } catch (error) {\n console.error('Failed to count images:', error)\n setAlertMessage({\n title: 'Error',\n message: 'Failed to count images.',\n })\n }\n }\n }, [selectedItems])\n\n const handleProcessConfirm = useCallback(async () => {\n setShowProcessConfirm(false)\n setProcessing(true)\n\n try {\n if (processMode === 'all') {\n // Process all images with streaming progress\n setShowProgress(true)\n setProgressState({\n current: 0,\n total: processCount,\n percent: 0,\n status: 'processing',\n })\n\n const response = await fetch('/api/studio/process-all', {\n method: 'POST',\n })\n\n if (!response.body) {\n throw new Error('No response body')\n }\n\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n const text = decoder.decode(value)\n const lines = text.split('\\n\\n').filter(line => line.startsWith('data: '))\n\n for (const line of lines) {\n try {\n const data = JSON.parse(line.replace('data: ', ''))\n \n if (data.type === 'start') {\n setProgressState(prev => ({\n ...prev,\n total: data.total,\n }))\n } else if (data.type === 'progress') {\n setProgressState({\n current: data.current,\n total: data.total,\n percent: data.percent,\n currentFile: data.currentFile,\n status: 'processing',\n })\n } else if (data.type === 'cleanup') {\n setProgressState(prev => ({\n ...prev,\n status: 'cleanup',\n currentFile: undefined,\n }))\n } else if (data.type === 'complete') {\n setProgressState({\n current: data.processed,\n total: data.processed,\n percent: 100,\n status: 'complete',\n processed: data.processed,\n orphansRemoved: data.orphansRemoved,\n errors: data.errors,\n })\n triggerRefresh()\n } else if (data.type === 'error') {\n setProgressState(prev => ({\n ...prev,\n status: 'error',\n message: data.message,\n }))\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n } else {\n // Process selected images (no streaming for now)\n setShowProgress(true)\n setProgressState({\n current: 0,\n total: processCount,\n percent: 0,\n status: 'processing',\n })\n\n const selectedImageKeys = Array.from(selectedItems)\n .filter(p => {\n const ext = p.split('.').pop()?.toLowerCase() || ''\n return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff', 'tif'].includes(ext)\n })\n .map(p => p.replace(/^public\\//, ''))\n \n const response = await fetch('/api/studio/reprocess', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ imageKeys: selectedImageKeys }),\n })\n \n const data = await response.json()\n \n if (response.ok) {\n setProgressState({\n current: data.processed?.length || 0,\n total: data.processed?.length || 0,\n percent: 100,\n status: 'complete',\n processed: data.processed?.length || 0,\n errors: data.errors?.length || 0,\n })\n clearSelection()\n triggerRefresh()\n } else {\n setProgressState({\n current: 0,\n total: 0,\n percent: 0,\n status: 'error',\n message: data.error || 'Unknown error',\n })\n }\n }\n } catch (error) {\n console.error('Processing error:', error)\n setProgressState({\n current: 0,\n total: 0,\n percent: 0,\n status: 'error',\n message: 'Processing failed. Check console for details.',\n })\n } finally {\n setProcessing(false)\n }\n }, [processMode, processCount, selectedItems, clearSelection, triggerRefresh])\n\n const handleDeleteClick = useCallback(() => {\n if (selectedItems.size === 0) return\n setShowDeleteConfirm(true)\n }, [selectedItems])\n\n const handleDeleteConfirm = useCallback(async () => {\n setShowDeleteConfirm(false)\n \n try {\n const response = await fetch('/api/studio/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ paths: Array.from(selectedItems) }),\n })\n\n if (response.ok) {\n clearSelection()\n triggerRefresh()\n } else {\n const error = await response.json()\n setAlertMessage({\n title: 'Delete Failed',\n message: error.error || 'Unknown error',\n })\n }\n } catch (error) {\n console.error('Delete error:', error)\n setAlertMessage({\n title: 'Delete Failed',\n message: 'Delete failed. Check console for details.',\n })\n }\n }, [selectedItems, clearSelection, triggerRefresh])\n\n const handleSyncCdn = useCallback(() => {\n console.log('Sync CDN clicked', selectedItems)\n }, [selectedItems])\n\n const handleScan = useCallback(() => {\n console.log('Scan clicked')\n }, [])\n\n const hasSelection = selectedItems.size > 0\n\n // Hide toolbar actions when viewing detail\n if (focusedItem) {\n return null\n }\n\n return (\n <>\n {showDeleteConfirm && (\n <ConfirmModal\n title=\"Delete Items\"\n message={`Are you sure you want to delete ${selectedItems.size} item(s)? This action cannot be undone.`}\n confirmLabel=\"Delete\"\n variant=\"danger\"\n onConfirm={handleDeleteConfirm}\n onCancel={() => setShowDeleteConfirm(false)}\n />\n )}\n\n {showProcessConfirm && (\n <ConfirmModal\n title=\"Process Images\"\n message={processMode === 'all' \n ? `Found ${processCount} image${processCount !== 1 ? 's' : ''} in the public folder. This will regenerate all thumbnails and remove any orphaned files from the images folder.`\n : `Process ${processCount} selected image${processCount !== 1 ? 's' : ''}? This will regenerate thumbnails for these files.`\n }\n confirmLabel={processing ? 'Processing...' : 'Process'}\n onConfirm={handleProcessConfirm}\n onCancel={() => setShowProcessConfirm(false)}\n />\n )}\n\n {showProgress && (\n <ProgressModal\n title=\"Processing Images\"\n progress={progressState}\n onClose={() => {\n setShowProgress(false)\n setProgressState({\n current: 0,\n total: 0,\n percent: 0,\n status: 'processing',\n })\n }}\n />\n )}\n\n {alertMessage && (\n <AlertModal\n title={alertMessage.title}\n message={alertMessage.message}\n onClose={() => setAlertMessage(null)}\n />\n )}\n\n <div css={styles.toolbar}>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept=\"image/*,video/*,audio/*,.pdf\"\n onChange={handleFileChange}\n style={{ display: 'none' }}\n />\n \n <div css={styles.left}>\n <button\n css={[styles.btn, styles.btnPrimary]}\n onClick={handleUpload}\n disabled={uploading || isInImagesFolder}\n >\n <UploadIcon />\n {uploading ? 'Uploading...' : 'Upload'}\n </button>\n \n <div css={styles.divider} />\n \n <button\n css={styles.btn}\n onClick={handleProcessImages}\n disabled={processing}\n >\n <ImageStackIcon />\n {processing ? 'Processing...' : 'Process Images'}\n </button>\n <button\n css={[styles.btn, styles.btnDanger]}\n onClick={handleDeleteClick}\n disabled={!hasSelection}\n >\n <TrashIcon />\n Delete\n </button>\n <button\n css={styles.btn}\n onClick={handleSyncCdn}\n disabled={!hasSelection}\n >\n <CloudIcon />\n Sync CDN\n </button>\n <button css={styles.btn} onClick={handleScan}>\n <ScanIcon />\n Scan\n </button>\n </div>\n\n <div css={styles.right}>\n {hasSelection && (\n <span css={styles.selectionCount}>\n {selectedItems.size} selected\n <button css={styles.clearBtn} onClick={clearSelection}>\n Clear\n </button>\n </span>\n )}\n\n <button\n css={[styles.btn, styles.btnIconOnly]}\n onClick={handleRefresh}\n >\n <RefreshIcon spinning={refreshing} />\n </button>\n\n <div css={styles.viewToggle}>\n <button\n css={[styles.viewBtn, viewMode === 'grid' && styles.viewBtnActive]}\n onClick={() => setViewMode('grid')}\n aria-label=\"Grid view\"\n >\n <GridIcon />\n </button>\n <button\n css={[styles.viewBtn, viewMode === 'list' && styles.viewBtnActive]}\n onClick={() => setViewMode('list')}\n aria-label=\"List view\"\n >\n <ListIcon />\n </button>\n </div>\n </div>\n </div>\n </>\n )\n}\n\nfunction UploadIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12\" />\n </svg>\n )\n}\n\nfunction RefreshIcon({ spinning }: { spinning?: boolean }) {\n return (\n <svg css={[styles.icon, spinning && styles.iconSpin]} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n )\n}\n\nfunction TrashIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n )\n}\n\nfunction CloudIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12\" />\n </svg>\n )\n}\n\nfunction ScanIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n )\n}\n\nfunction GridIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\" />\n </svg>\n )\n}\n\nfunction ListIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6h16M4 10h16M4 14h16M4 18h16\" />\n </svg>\n )\n}\n\nfunction ImageStackIcon() {\n return (\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n )\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { css, keyframes } from '@emotion/react'\nimport { colors, fontSize, fontStack, baseReset } from './tokens'\n\nconst fadeIn = keyframes`\n from { opacity: 0; }\n to { opacity: 1; }\n`\n\nconst slideIn = keyframes`\n from { \n opacity: 0;\n transform: translateY(-8px) scale(0.98);\n }\n to { \n opacity: 1;\n transform: translateY(0) scale(1);\n }\n`\n\nconst styles = {\n overlay: css`\n position: fixed;\n inset: 0;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n animation: ${fadeIn} 0.15s ease-out;\n font-family: ${fontStack};\n `,\n modal: css`\n ${baseReset}\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n max-width: 420px;\n width: 90%;\n animation: ${slideIn} 0.2s ease-out;\n overflow: hidden;\n `,\n header: css`\n padding: 24px 24px 0;\n `,\n title: css`\n font-size: ${fontSize.lg};\n font-weight: 600;\n color: ${colors.text};\n margin: 0;\n letter-spacing: -0.02em;\n `,\n body: css`\n padding: 12px 24px 24px;\n `,\n message: css`\n font-size: ${fontSize.base};\n color: ${colors.textSecondary};\n margin: 0;\n line-height: 1.6;\n `,\n footer: css`\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n padding: 16px 24px;\n border-top: 1px solid ${colors.border};\n background-color: ${colors.background};\n `,\n btn: css`\n padding: 10px 18px;\n font-size: ${fontSize.base};\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n letter-spacing: -0.01em;\n `,\n btnCancel: css`\n background-color: ${colors.surface};\n border: 1px solid ${colors.border};\n color: ${colors.text};\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n btnConfirm: css`\n background-color: ${colors.primary};\n border: 1px solid ${colors.primary};\n color: white;\n \n &:hover {\n background-color: ${colors.primaryHover};\n border-color: ${colors.primaryHover};\n }\n `,\n btnDanger: css`\n background-color: ${colors.danger};\n border: 1px solid ${colors.danger};\n color: white;\n \n &:hover {\n background-color: ${colors.dangerHover};\n border-color: ${colors.dangerHover};\n }\n `,\n}\n\ninterface ConfirmModalProps {\n title: string\n message: string\n confirmLabel?: string\n cancelLabel?: string\n variant?: 'default' | 'danger'\n onConfirm: () => void\n onCancel: () => void\n}\n\nexport function ConfirmModal({\n title,\n message,\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n variant = 'default',\n onConfirm,\n onCancel,\n}: ConfirmModalProps) {\n return (\n <div css={styles.overlay} onClick={onCancel}>\n <div css={styles.modal} onClick={(e) => e.stopPropagation()}>\n <div css={styles.header}>\n <h3 css={styles.title}>{title}</h3>\n </div>\n <div css={styles.body}>\n <p css={styles.message}>{message}</p>\n </div>\n <div css={styles.footer}>\n <button css={[styles.btn, styles.btnCancel]} onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n css={[styles.btn, variant === 'danger' ? styles.btnDanger : styles.btnConfirm]}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\ninterface AlertModalProps {\n title: string\n message: string\n buttonLabel?: string\n onClose: () => void\n}\n\nexport function AlertModal({\n title,\n message,\n buttonLabel = 'OK',\n onClose,\n}: AlertModalProps) {\n return (\n <div css={styles.overlay} onClick={onClose}>\n <div css={styles.modal} onClick={(e) => e.stopPropagation()}>\n <div css={styles.header}>\n <h3 css={styles.title}>{title}</h3>\n </div>\n <div css={styles.body}>\n <p css={styles.message}>{message}</p>\n </div>\n <div css={styles.footer}>\n <button css={[styles.btn, styles.btnConfirm]} onClick={onClose}>\n {buttonLabel}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nconst progressStyles = {\n progressContainer: css`\n margin-top: 16px;\n `,\n progressBar: css`\n width: 100%;\n height: 8px;\n background-color: ${colors.background};\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 12px;\n `,\n progressFill: css`\n height: 100%;\n background: linear-gradient(90deg, ${colors.primary}, ${colors.primaryHover});\n border-radius: 4px;\n transition: width 0.3s ease;\n `,\n progressText: css`\n font-size: ${fontSize.sm};\n color: ${colors.textSecondary};\n margin: 0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n `,\n currentFile: css`\n font-size: ${fontSize.xs};\n color: ${colors.textMuted};\n margin: 8px 0 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n `,\n}\n\nexport interface ProgressState {\n current: number\n total: number\n percent: number\n currentFile?: string\n status: 'processing' | 'cleanup' | 'complete' | 'error'\n message?: string\n processed?: number\n orphansRemoved?: number\n errors?: number\n}\n\ninterface ProgressModalProps {\n title: string\n progress: ProgressState\n onClose?: () => void\n}\n\nexport function ProgressModal({\n title,\n progress,\n onClose,\n}: ProgressModalProps) {\n const isComplete = progress.status === 'complete'\n const isError = progress.status === 'error'\n const canClose = isComplete || isError\n\n return (\n <div css={styles.overlay} onClick={canClose ? onClose : undefined}>\n <div css={styles.modal} onClick={(e) => e.stopPropagation()}>\n <div css={styles.header}>\n <h3 css={styles.title}>{title}</h3>\n </div>\n <div css={styles.body}>\n {isError ? (\n <p css={styles.message}>{progress.message || 'An error occurred'}</p>\n ) : isComplete ? (\n <p css={styles.message}>\n Processed {progress.processed} image{progress.processed !== 1 ? 's' : ''}.\n {progress.orphansRemoved && progress.orphansRemoved > 0 && (\n <> Removed {progress.orphansRemoved} orphaned thumbnail{progress.orphansRemoved !== 1 ? 's' : ''}.</>\n )}\n {progress.errors && progress.errors > 0 && (\n <> {progress.errors} error{progress.errors !== 1 ? 's' : ''} occurred.</>\n )}\n </p>\n ) : (\n <>\n <p css={styles.message}>\n {progress.status === 'cleanup' \n ? 'Cleaning up orphaned files...' \n : `Processing images...`}\n </p>\n <div css={progressStyles.progressContainer}>\n <div css={progressStyles.progressBar}>\n <div \n css={progressStyles.progressFill} \n style={{ width: `${progress.percent}%` }} \n />\n </div>\n <div css={progressStyles.progressText}>\n <span>{progress.current} of {progress.total}</span>\n <span>{progress.percent}%</span>\n </div>\n {progress.currentFile && (\n <p css={progressStyles.currentFile} title={progress.currentFile}>\n {progress.currentFile}\n </p>\n )}\n </div>\n </>\n )}\n </div>\n {canClose && (\n <div css={styles.footer}>\n <button css={[styles.btn, styles.btnConfirm]} onClick={onClose}>\n Done\n </button>\n </div>\n )}\n </div>\n </div>\n )\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useState } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport { colors, fontSize } from './tokens'\nimport type { FileItem } from '../types'\n\nconst spin = keyframes`\n to { transform: rotate(360deg); }\n`\n\nconst styles = {\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 256px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n empty: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 256px;\n color: ${colors.textSecondary};\n `,\n emptyIcon: css`\n width: 48px;\n height: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n `,\n emptyText: css`\n font-size: ${fontSize.base};\n margin: 0 0 4px 0;\n \n &:last-child {\n color: ${colors.textMuted};\n font-size: ${fontSize.sm};\n }\n `,\n grid: css`\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 12px;\n \n @media (min-width: 640px) { grid-template-columns: repeat(3, 1fr); }\n @media (min-width: 768px) { grid-template-columns: repeat(4, 1fr); }\n @media (min-width: 1024px) { grid-template-columns: repeat(5, 1fr); }\n @media (min-width: 1280px) { grid-template-columns: repeat(6, 1fr); }\n `,\n item: css`\n position: relative;\n border-radius: 8px;\n border: 1px solid ${colors.border};\n overflow: hidden;\n cursor: pointer;\n transition: all 0.15s ease;\n background-color: ${colors.surface};\n user-select: none;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);\n \n &:hover {\n border-color: #d0d5dd;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);\n }\n `,\n itemSelected: css`\n border-color: ${colors.primary};\n box-shadow: 0 0 0 1px ${colors.primary};\n \n &:hover {\n border-color: ${colors.primary};\n }\n `,\n parentItem: css`\n cursor: pointer;\n \n &:hover {\n border-color: ${colors.primary};\n }\n `,\n checkboxWrapper: css`\n position: absolute;\n top: 0;\n left: 0;\n z-index: 10;\n padding: 8px;\n cursor: pointer;\n `,\n checkbox: css`\n width: 16px;\n height: 16px;\n accent-color: ${colors.primary};\n cursor: pointer;\n `,\n cdnBadge: css`\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 10;\n background-color: ${colors.successLight};\n color: ${colors.success};\n font-size: 11px;\n font-weight: 500;\n padding: 2px 8px;\n border-radius: 4px;\n `,\n content: css`\n aspect-ratio: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: ${colors.background};\n `,\n folderIcon: css`\n width: 56px;\n height: 56px;\n color: #f5a623;\n `,\n parentIcon: css`\n width: 56px;\n height: 56px;\n color: ${colors.textMuted};\n `,\n fileIcon: css`\n width: 40px;\n height: 40px;\n color: ${colors.textMuted};\n `,\n image: css`\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: 4px;\n `,\n label: css`\n padding: 10px 12px;\n background-color: ${colors.surface};\n border-top: 1px solid ${colors.borderLight};\n `,\n labelRow: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n `,\n labelText: css`\n flex: 1;\n min-width: 0;\n `,\n name: css`\n font-size: ${fontSize.sm};\n font-weight: 500;\n color: ${colors.text};\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n margin: 0;\n letter-spacing: -0.01em;\n `,\n size: css`\n font-size: ${fontSize.xs};\n color: ${colors.textMuted};\n margin: 2px 0 0 0;\n `,\n openBtn: css`\n flex-shrink: 0;\n height: 28px;\n font-size: ${fontSize.xs};\n font-weight: 500;\n color: ${colors.primary};\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n padding: 0 10px;\n cursor: pointer;\n border-radius: 4px;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n \n &:hover {\n background-color: ${colors.primaryLight};\n border-color: ${colors.primary};\n }\n `,\n selectAllRow: css`\n display: flex;\n align-items: center;\n margin-bottom: 16px;\n padding: 12px 16px;\n background: ${colors.surface};\n border-radius: 8px;\n border: 1px solid ${colors.border};\n `,\n selectAllLabel: css`\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: ${fontSize.base};\n font-weight: 500;\n color: ${colors.textSecondary};\n cursor: pointer;\n \n &:hover {\n color: ${colors.text};\n }\n `,\n selectAllCheckbox: css`\n width: 16px;\n height: 16px;\n accent-color: ${colors.primary};\n `,\n}\n\nexport function StudioFileGrid() {\n const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem } = useStudio()\n const [items, setItems] = useState<FileItem[]>([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n async function loadItems() {\n setLoading(true)\n try {\n const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`)\n if (response.ok) {\n const data = await response.json()\n setItems(data.items || [])\n }\n } catch (error) {\n console.error('Failed to load items:', error)\n }\n setLoading(false)\n }\n loadItems()\n }, [currentPath, refreshKey])\n\n if (loading) {\n return (\n <div css={styles.loading}>\n <div css={styles.spinner} />\n </div>\n )\n }\n\n const isAtRoot = currentPath === 'public'\n\n // Empty state only when truly empty (not counting parent folder)\n if (items.length === 0 && isAtRoot) {\n return (\n <div css={styles.empty}>\n <svg css={styles.emptyIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n <p css={styles.emptyText}>No files in this folder</p>\n <p css={styles.emptyText}>Upload images to get started</p>\n </div>\n )\n }\n\n const sortedItems = [...items].sort((a, b) => {\n if (a.type === 'folder' && b.type !== 'folder') return -1\n if (a.type !== 'folder' && b.type === 'folder') return 1\n return a.name.localeCompare(b.name)\n })\n\n const handleItemClick = (item: FileItem, e: React.MouseEvent) => {\n if (e.shiftKey && lastSelectedPath) {\n selectRange(lastSelectedPath, item.path, sortedItems)\n } else {\n toggleSelection(item.path)\n }\n }\n\n const handleOpen = (item: FileItem) => {\n if (item.type === 'folder') {\n setCurrentPath(item.path)\n } else {\n setFocusedItem(item)\n }\n }\n\n const allItemsSelected = sortedItems.length > 0 && sortedItems.every(item => selectedItems.has(item.path))\n const someItemsSelected = sortedItems.some(item => selectedItems.has(item.path))\n\n const handleSelectAll = () => {\n if (allItemsSelected) {\n clearSelection()\n } else {\n selectAll(sortedItems)\n }\n }\n\n return (\n <div>\n {sortedItems.length > 0 && (\n <div css={styles.selectAllRow}>\n <label css={styles.selectAllLabel}>\n <input\n type=\"checkbox\"\n css={styles.selectAllCheckbox}\n checked={allItemsSelected}\n ref={(el) => {\n if (el) el.indeterminate = someItemsSelected && !allItemsSelected\n }}\n onChange={handleSelectAll}\n />\n Select all ({sortedItems.length})\n </label>\n </div>\n )}\n <div css={styles.grid}>\n {/* Parent folder navigation */}\n {!isAtRoot && (\n <div \n css={[styles.item, styles.parentItem]}\n onClick={navigateUp}\n >\n <div css={styles.content}>\n <svg css={styles.parentIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6\" />\n </svg>\n </div>\n <div css={styles.label}>\n <p css={styles.name}>..</p>\n <p css={styles.size}>Parent folder</p>\n </div>\n </div>\n )}\n \n {sortedItems.map((item) => (\n <GridItem\n key={item.path}\n item={item}\n isSelected={selectedItems.has(item.path)}\n onClick={(e) => handleItemClick(item, e)}\n onOpen={() => handleOpen(item)}\n />\n ))}\n </div>\n </div>\n )\n}\n\ninterface GridItemProps {\n item: FileItem\n isSelected: boolean\n onClick: (e: React.MouseEvent) => void\n onOpen: () => void\n}\n\nfunction GridItem({ item, isSelected, onClick, onOpen }: GridItemProps) {\n const isFolder = item.type === 'folder'\n\n return (\n <div \n css={[styles.item, isSelected && styles.itemSelected]} \n onClick={onClick}\n >\n <div\n css={styles.checkboxWrapper}\n onClick={(e) => e.stopPropagation()}\n >\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={isSelected}\n onChange={() => onClick({} as React.MouseEvent)}\n />\n </div>\n\n {item.cdnSynced && <span css={styles.cdnBadge}>CDN</span>}\n\n <div css={styles.content}>\n {isFolder ? (\n <svg css={styles.folderIcon} fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z\" />\n </svg>\n ) : item.thumbnail ? (\n <img\n css={styles.image}\n src={item.thumbnail}\n alt={item.name}\n loading=\"lazy\"\n />\n ) : (\n <svg css={styles.fileIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n )}\n </div>\n\n <div css={styles.label}>\n <div css={styles.labelRow}>\n <div css={styles.labelText}>\n <p css={styles.name} title={item.name}>{item.name}</p>\n {isFolder ? (\n <p css={styles.size}>\n {item.fileCount !== undefined ? `${item.fileCount} files` : ''}\n {item.fileCount !== undefined && item.totalSize !== undefined ? ' · ' : ''}\n {item.totalSize !== undefined ? formatFileSize(item.totalSize) : ''}\n </p>\n ) : (\n item.size !== undefined && <p css={styles.size}>{formatFileSize(item.size)}</p>\n )}\n </div>\n <button\n css={styles.openBtn}\n onClick={(e) => {\n e.stopPropagation()\n onOpen()\n }}\n >\n Open\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useEffect, useState } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport { colors, fontSize } from './tokens'\nimport type { FileItem } from '../types'\n\nconst spin = keyframes`\n to { transform: rotate(360deg); }\n`\n\nconst styles = {\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 256px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n empty: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 256px;\n color: ${colors.textSecondary};\n `,\n tableWrapper: css`\n background: ${colors.surface};\n border-radius: 8px;\n border: 1px solid ${colors.border};\n overflow: hidden;\n `,\n table: css`\n width: 100%;\n border-collapse: collapse;\n `,\n th: css`\n text-align: left;\n font-size: 11px;\n color: ${colors.textMuted};\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 12px 16px;\n font-weight: 600;\n background: ${colors.background};\n border-bottom: 1px solid ${colors.border};\n `,\n thCheckbox: css`\n width: 48px;\n `,\n thSize: css`\n width: 96px;\n `,\n thDimensions: css`\n width: 128px;\n `,\n thCdn: css`\n width: 96px;\n `,\n tbody: css``,\n row: css`\n cursor: pointer;\n transition: background-color 0.15s ease;\n user-select: none;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n }\n \n &:not(:last-child) td {\n border-bottom: 1px solid ${colors.borderLight};\n }\n `,\n rowSelected: css`\n background-color: ${colors.primaryLight};\n \n &:hover {\n background-color: ${colors.primaryLight};\n }\n `,\n parentRow: css`\n cursor: pointer;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n }\n `,\n td: css`\n padding: 12px 16px;\n `,\n checkboxCell: css`\n padding: 12px 16px;\n cursor: pointer;\n `,\n checkbox: css`\n width: 16px;\n height: 16px;\n accent-color: ${colors.primary};\n cursor: pointer;\n `,\n nameCell: css`\n display: flex;\n align-items: center;\n gap: 12px;\n `,\n folderIcon: css`\n width: 20px;\n height: 20px;\n color: #f5a623;\n flex-shrink: 0;\n `,\n parentIcon: css`\n width: 20px;\n height: 20px;\n color: ${colors.textMuted};\n flex-shrink: 0;\n `,\n fileIcon: css`\n width: 20px;\n height: 20px;\n color: ${colors.textMuted};\n flex-shrink: 0;\n `,\n thumbnail: css`\n width: 36px;\n height: 36px;\n object-fit: cover;\n border-radius: 6px;\n flex-shrink: 0;\n border: 1px solid ${colors.borderLight};\n `,\n name: css`\n font-size: ${fontSize.base};\n font-weight: 500;\n color: ${colors.text};\n letter-spacing: -0.01em;\n `,\n meta: css`\n font-size: ${fontSize.sm};\n color: ${colors.textSecondary};\n `,\n cdnBadge: css`\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: ${fontSize.xs};\n font-weight: 500;\n color: ${colors.success};\n `,\n cdnIcon: css`\n width: 12px;\n height: 12px;\n `,\n cdnEmpty: css`\n font-size: ${fontSize.sm};\n color: ${colors.textMuted};\n `,\n openBtn: css`\n height: 28px;\n font-size: ${fontSize.xs};\n font-weight: 500;\n color: ${colors.primary};\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n padding: 0 12px;\n cursor: pointer;\n border-radius: 4px;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n margin-left: auto;\n \n &:hover {\n background-color: ${colors.primaryLight};\n border-color: ${colors.primary};\n }\n `,\n}\n\nexport function StudioFileList() {\n const { currentPath, setCurrentPath, navigateUp, selectedItems, toggleSelection, selectRange, lastSelectedPath, selectAll, clearSelection, refreshKey, setFocusedItem } = useStudio()\n const [items, setItems] = useState<FileItem[]>([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n async function loadItems() {\n setLoading(true)\n try {\n const response = await fetch(`/api/studio/list?path=${encodeURIComponent(currentPath)}`)\n if (response.ok) {\n const data = await response.json()\n setItems(data.items || [])\n }\n } catch (error) {\n console.error('Failed to load items:', error)\n }\n setLoading(false)\n }\n loadItems()\n }, [currentPath, refreshKey])\n\n if (loading) {\n return (\n <div css={styles.loading}>\n <div css={styles.spinner} />\n </div>\n )\n }\n\n const isAtRoot = currentPath === 'public'\n\n if (items.length === 0 && isAtRoot) {\n return (\n <div css={styles.empty}>\n <p>No files in this folder</p>\n </div>\n )\n }\n\n const sortedItems = [...items].sort((a, b) => {\n if (a.type === 'folder' && b.type !== 'folder') return -1\n if (a.type !== 'folder' && b.type === 'folder') return 1\n return a.name.localeCompare(b.name)\n })\n\n const handleItemClick = (item: FileItem, e: React.MouseEvent) => {\n if (e.shiftKey && lastSelectedPath) {\n selectRange(lastSelectedPath, item.path, sortedItems)\n } else {\n toggleSelection(item.path)\n }\n }\n\n const handleOpen = (item: FileItem) => {\n if (item.type === 'folder') {\n setCurrentPath(item.path)\n } else {\n setFocusedItem(item)\n }\n }\n\n const allItemsSelected = sortedItems.length > 0 && sortedItems.every(item => selectedItems.has(item.path))\n const someItemsSelected = sortedItems.some(item => selectedItems.has(item.path))\n\n const handleSelectAll = () => {\n if (allItemsSelected) {\n clearSelection()\n } else {\n selectAll(sortedItems)\n }\n }\n\n return (\n <div css={styles.tableWrapper}>\n <table css={styles.table}>\n <thead>\n <tr>\n <th css={[styles.th, styles.thCheckbox]}>\n {sortedItems.length > 0 && (\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={allItemsSelected}\n ref={(el) => {\n if (el) el.indeterminate = someItemsSelected && !allItemsSelected\n }}\n onChange={handleSelectAll}\n />\n )}\n </th>\n <th css={styles.th}>Name</th>\n <th css={[styles.th, styles.thSize]}>Size</th>\n <th css={[styles.th, styles.thDimensions]}>Dimensions</th>\n <th css={[styles.th, styles.thCdn]}>CDN</th>\n </tr>\n </thead>\n <tbody css={styles.tbody}>\n {/* Parent folder navigation */}\n {!isAtRoot && (\n <tr css={styles.parentRow} onClick={navigateUp}>\n <td css={styles.td}></td>\n <td css={styles.td}>\n <div css={styles.nameCell}>\n <svg css={styles.parentIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6\" />\n </svg>\n <span css={styles.name}>..</span>\n </div>\n </td>\n <td css={[styles.td, styles.meta]}>--</td>\n <td css={[styles.td, styles.meta]}>Parent folder</td>\n <td css={styles.td}>--</td>\n </tr>\n )}\n \n {sortedItems.map((item) => (\n <ListRow\n key={item.path}\n item={item}\n isSelected={selectedItems.has(item.path)}\n onClick={(e) => handleItemClick(item, e)}\n onOpen={() => handleOpen(item)}\n />\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n\ninterface ListRowProps {\n item: FileItem\n isSelected: boolean\n onClick: (e: React.MouseEvent) => void\n onOpen: () => void\n}\n\nfunction ListRow({ item, isSelected, onClick, onOpen }: ListRowProps) {\n const isFolder = item.type === 'folder'\n\n return (\n <tr \n css={[styles.row, isSelected && styles.rowSelected]} \n onClick={onClick}\n >\n <td\n css={[styles.td, styles.checkboxCell]}\n onClick={(e) => e.stopPropagation()}\n >\n <input\n type=\"checkbox\"\n css={styles.checkbox}\n checked={isSelected}\n onChange={() => onClick({} as React.MouseEvent)}\n />\n </td>\n <td css={styles.td}>\n <div css={styles.nameCell}>\n {isFolder ? (\n <svg css={styles.folderIcon} fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z\" />\n </svg>\n ) : item.thumbnail ? (\n <img css={styles.thumbnail} src={item.thumbnail} alt={item.name} loading=\"lazy\" />\n ) : (\n <svg css={styles.fileIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n )}\n <span css={styles.name}>{item.name}</span>\n <button\n css={styles.openBtn}\n onClick={(e) => {\n e.stopPropagation()\n onOpen()\n }}\n >\n Open\n </button>\n </div>\n </td>\n <td css={[styles.td, styles.meta]}>\n {isFolder \n ? (item.fileCount !== undefined ? `${item.fileCount} files` : '--')\n : (item.size !== undefined ? formatFileSize(item.size) : '--')\n }\n </td>\n <td css={[styles.td, styles.meta]}>\n {isFolder \n ? (item.totalSize !== undefined ? formatFileSize(item.totalSize) : '--')\n : (item.dimensions ? `${item.dimensions.width}x${item.dimensions.height}` : '--')\n }\n </td>\n <td css={styles.td}>\n {item.cdnSynced ? (\n <span css={styles.cdnBadge}>\n <svg css={styles.cdnIcon} fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fillRule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clipRule=\"evenodd\" />\n </svg>\n Synced\n </span>\n ) : (\n <span css={styles.cdnEmpty}>--</span>\n )}\n </td>\n </tr>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState } from 'react'\nimport { css } from '@emotion/react'\nimport { useStudio } from './StudioContext'\nimport { ConfirmModal, AlertModal } from './StudioModal'\nimport { colors, fontSize } from './tokens'\n\nconst IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif']\nconst VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v']\n\nfunction isImageFile(filename: string): boolean {\n const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))\n return IMAGE_EXTENSIONS.includes(ext)\n}\n\nfunction isVideoFile(filename: string): boolean {\n const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))\n return VIDEO_EXTENSIONS.includes(ext)\n}\n\nconst styles = {\n container: css`\n display: flex;\n flex: 1;\n overflow: hidden;\n `,\n main: css`\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: ${colors.background};\n overflow: auto;\n `,\n mediaWrapper: css`\n max-width: 100%;\n max-height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n `,\n image: css`\n max-width: 100%;\n max-height: calc(100vh - 200px);\n object-fit: contain;\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n `,\n video: css`\n max-width: 100%;\n max-height: calc(100vh - 200px);\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n `,\n filePlaceholder: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px;\n background: ${colors.surface};\n border-radius: 12px;\n border: 1px solid ${colors.border};\n `,\n fileIcon: css`\n width: 80px;\n height: 80px;\n color: ${colors.textMuted};\n margin-bottom: 16px;\n `,\n fileName: css`\n font-size: ${fontSize.lg};\n font-weight: 600;\n color: ${colors.text};\n margin: 0;\n `,\n sidebar: css`\n width: 280px;\n background: ${colors.surface};\n border-left: 1px solid ${colors.border};\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n sidebarHeader: css`\n padding: 16px 20px;\n border-bottom: 1px solid ${colors.border};\n display: flex;\n align-items: center;\n justify-content: space-between;\n `,\n sidebarTitle: css`\n font-size: ${fontSize.base};\n font-weight: 600;\n color: ${colors.text};\n margin: 0;\n `,\n closeBtn: css`\n padding: 6px;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n closeIcon: css`\n width: 16px;\n height: 16px;\n color: ${colors.textSecondary};\n `,\n sidebarContent: css`\n flex: 1;\n padding: 20px;\n overflow: auto;\n `,\n info: css`\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin-bottom: 24px;\n `,\n infoRow: css`\n display: flex;\n justify-content: space-between;\n font-size: ${fontSize.sm};\n `,\n infoLabel: css`\n color: ${colors.textSecondary};\n `,\n infoValue: css`\n color: ${colors.text};\n font-weight: 500;\n text-align: right;\n max-width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n `,\n actions: css`\n display: flex;\n flex-direction: column;\n gap: 8px;\n `,\n actionBtn: css`\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 12px 14px;\n font-size: ${fontSize.base};\n font-weight: 500;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n color: ${colors.text};\n text-align: left;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n actionBtnDanger: css`\n color: ${colors.danger};\n \n &:hover {\n background-color: ${colors.dangerLight};\n border-color: ${colors.danger};\n }\n `,\n actionIcon: css`\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n `,\n}\n\nexport function StudioDetailView() {\n const { focusedItem, setFocusedItem, triggerRefresh, clearSelection } = useStudio()\n const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)\n const [alertMessage, setAlertMessage] = useState<{ title: string; message: string } | null>(null)\n\n if (!focusedItem) return null\n\n const isImage = isImageFile(focusedItem.name)\n const isVideo = isVideoFile(focusedItem.name)\n const imageSrc = focusedItem.path.replace('public', '')\n\n const handleClose = () => {\n setFocusedItem(null)\n }\n\n const handleRename = () => {\n const newName = prompt('Enter new name:', focusedItem.name)\n if (newName && newName !== focusedItem.name) {\n console.log('Rename to:', newName)\n // TODO: Implement rename API\n }\n }\n\n const handleDelete = async () => {\n setShowDeleteConfirm(false)\n try {\n const response = await fetch('/api/studio/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ paths: [focusedItem.path] }),\n })\n\n if (response.ok) {\n clearSelection()\n triggerRefresh()\n setFocusedItem(null)\n } else {\n const error = await response.json()\n setAlertMessage({\n title: 'Delete Failed',\n message: error.error || 'Unknown error',\n })\n }\n } catch (error) {\n console.error('Delete error:', error)\n setAlertMessage({\n title: 'Delete Failed',\n message: 'Delete failed. Check console for details.',\n })\n }\n }\n\n const handleSync = () => {\n console.log('Sync to CDN:', focusedItem.path)\n // TODO: Implement sync API\n }\n\n const handleRegenerate = () => {\n console.log('Regenerate:', focusedItem.path)\n // TODO: Implement regenerate API\n }\n\n const renderMedia = () => {\n if (isImage) {\n return <img css={styles.image} src={imageSrc} alt={focusedItem.name} />\n }\n if (isVideo) {\n return <video css={styles.video} src={imageSrc} controls />\n }\n return (\n <div css={styles.filePlaceholder}>\n <svg css={styles.fileIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n <p css={styles.fileName}>{focusedItem.name}</p>\n </div>\n )\n }\n\n return (\n <>\n {showDeleteConfirm && (\n <ConfirmModal\n title=\"Delete File\"\n message={`Are you sure you want to delete \"${focusedItem.name}\"? This action cannot be undone.`}\n confirmLabel=\"Delete\"\n variant=\"danger\"\n onConfirm={handleDelete}\n onCancel={() => setShowDeleteConfirm(false)}\n />\n )}\n\n {alertMessage && (\n <AlertModal\n title={alertMessage.title}\n message={alertMessage.message}\n onClose={() => setAlertMessage(null)}\n />\n )}\n\n <div css={styles.container}>\n <div css={styles.main}>\n <div css={styles.mediaWrapper}>\n {renderMedia()}\n </div>\n </div>\n\n <div css={styles.sidebar}>\n <div css={styles.sidebarHeader}>\n <h3 css={styles.sidebarTitle}>Details</h3>\n <button css={styles.closeBtn} onClick={handleClose} aria-label=\"Close\">\n <svg css={styles.closeIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div css={styles.sidebarContent}>\n <div css={styles.info}>\n <div css={styles.infoRow}>\n <span css={styles.infoLabel}>Name</span>\n <span css={styles.infoValue} title={focusedItem.name}>{focusedItem.name}</span>\n </div>\n {focusedItem.size !== undefined && (\n <div css={styles.infoRow}>\n <span css={styles.infoLabel}>Size</span>\n <span css={styles.infoValue}>{formatFileSize(focusedItem.size)}</span>\n </div>\n )}\n {focusedItem.dimensions && (\n <div css={styles.infoRow}>\n <span css={styles.infoLabel}>Dimensions</span>\n <span css={styles.infoValue}>{focusedItem.dimensions.width} × {focusedItem.dimensions.height}</span>\n </div>\n )}\n <div css={styles.infoRow}>\n <span css={styles.infoLabel}>CDN Status</span>\n <span css={styles.infoValue}>{focusedItem.cdnSynced ? 'Synced' : 'Not synced'}</span>\n </div>\n </div>\n\n <div css={styles.actions}>\n <button css={styles.actionBtn} onClick={handleRename}>\n <svg css={styles.actionIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n Rename\n </button>\n <button css={styles.actionBtn} onClick={handleSync}>\n <svg css={styles.actionIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12\" />\n </svg>\n Sync to CDN\n </button>\n <button css={styles.actionBtn} onClick={handleRegenerate}>\n <svg css={styles.actionIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n Regenerate\n </button>\n <button css={[styles.actionBtn, styles.actionBtnDanger]} onClick={() => setShowDeleteConfirm(true)}>\n <svg css={styles.actionIcon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n Delete\n </button>\n </div>\n </div>\n </div>\n </div>\n </>\n )\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n","/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState } from 'react'\nimport { css } from '@emotion/react'\nimport { colors, fontSize, baseReset } from './tokens'\n\n// Standard button height for consistency\nconst btnHeight = '36px'\n\nconst styles = {\n btn: css`\n height: ${btnHeight};\n padding: 0 12px;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n icon: css`\n width: 16px;\n height: 16px;\n color: ${colors.textSecondary};\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n `,\n panel: css`\n ${baseReset}\n position: relative;\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n width: 100%;\n max-width: 512px;\n padding: 24px;\n `,\n header: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 24px;\n `,\n title: css`\n font-size: ${fontSize.xl};\n font-weight: 600;\n color: ${colors.text};\n margin: 0;\n letter-spacing: -0.02em;\n `,\n closeBtn: css`\n padding: 6px;\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n sections: css`\n display: flex;\n flex-direction: column;\n gap: 24px;\n `,\n sectionTitle: css`\n font-size: ${fontSize.base};\n font-weight: 600;\n color: ${colors.text};\n margin: 0 0 12px 0;\n `,\n description: css`\n font-size: ${fontSize.sm};\n color: ${colors.textSecondary};\n margin: 0 0 12px 0;\n `,\n code: css`\n background-color: ${colors.background};\n border-radius: 8px;\n padding: 12px;\n font-family: 'SF Mono', Monaco, Consolas, monospace;\n font-size: ${fontSize.xs};\n color: ${colors.textSecondary};\n border: 1px solid ${colors.border};\n `,\n codeLine: css`\n margin: 0 0 4px 0;\n \n &:last-child {\n margin: 0;\n }\n `,\n input: css`\n width: 100%;\n padding: 10px 14px;\n border: 1px solid ${colors.border};\n border-radius: 6px;\n font-size: ${fontSize.base};\n color: ${colors.text};\n background: ${colors.surface};\n transition: all 0.15s ease;\n \n &:focus {\n outline: none;\n border-color: ${colors.primary};\n box-shadow: 0 0 0 3px ${colors.primaryLight};\n }\n \n &::placeholder {\n color: ${colors.textMuted};\n }\n `,\n grid: css`\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 12px;\n `,\n label: css`\n font-size: ${fontSize.xs};\n font-weight: 500;\n color: ${colors.textSecondary};\n display: block;\n margin-bottom: 6px;\n `,\n footer: css`\n margin-top: 24px;\n padding-top: 20px;\n border-top: 1px solid ${colors.border};\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n `,\n cancelBtn: css`\n padding: 10px 18px;\n font-size: ${fontSize.base};\n font-weight: 500;\n color: ${colors.text};\n background: ${colors.surface};\n border: 1px solid ${colors.border};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n \n &:hover {\n background-color: ${colors.surfaceHover};\n border-color: ${colors.borderHover};\n }\n `,\n saveBtn: css`\n padding: 10px 18px;\n font-size: ${fontSize.base};\n font-weight: 500;\n color: white;\n background-color: ${colors.primary};\n border: 1px solid ${colors.primary};\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.15s ease;\n \n &:hover {\n background-color: ${colors.primaryHover};\n border-color: ${colors.primaryHover};\n }\n `,\n}\n\nexport function StudioSettings() {\n const [isOpen, setIsOpen] = useState(false)\n\n return (\n <>\n <button css={styles.btn} onClick={() => setIsOpen(true)} aria-label=\"Settings\">\n <svg\n css={styles.icon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z\" />\n </svg>\n </button>\n\n {isOpen && <SettingsPanel onClose={() => setIsOpen(false)} />}\n </>\n )\n}\n\nfunction SettingsPanel({ onClose }: { onClose: () => void }) {\n return (\n <div css={styles.overlay} onClick={onClose}>\n <div css={styles.panel} onClick={(e) => e.stopPropagation()}>\n <div css={styles.header}>\n <h2 css={styles.title}>Settings</h2>\n <button css={styles.closeBtn} onClick={onClose}>\n <svg css={styles.icon} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n\n <div css={styles.sections}>\n <section>\n <h3 css={styles.sectionTitle}>Cloudflare R2</h3>\n <p css={styles.description}>Configure in .env.local file:</p>\n <div css={styles.code}>\n <p css={styles.codeLine}>CLOUDFLARE_R2_ACCOUNT_ID</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_ACCESS_KEY_ID</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_SECRET_ACCESS_KEY</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_BUCKET_NAME</p>\n <p css={styles.codeLine}>CLOUDFLARE_R2_PUBLIC_URL</p>\n </div>\n </section>\n\n <section>\n <h3 css={styles.sectionTitle}>Custom CDN URL</h3>\n <p css={styles.description}>Override the default R2 URL with a custom domain:</p>\n <input css={styles.input} type=\"text\" placeholder=\"https://cdn.yourdomain.com\" />\n </section>\n\n <section>\n <h3 css={styles.sectionTitle}>Thumbnail Sizes</h3>\n <div css={styles.grid}>\n <div>\n <label css={styles.label}>Small</label>\n <input css={styles.input} type=\"number\" defaultValue={300} />\n </div>\n <div>\n <label css={styles.label}>Medium</label>\n <input css={styles.input} type=\"number\" defaultValue={700} />\n </div>\n <div>\n <label css={styles.label}>Large</label>\n <input css={styles.input} type=\"number\" defaultValue={1400} />\n </div>\n </div>\n </section>\n </div>\n\n <div css={styles.footer}>\n <button css={styles.cancelBtn} onClick={onClose}>Cancel</button>\n <button css={styles.saveBtn}>Save Changes</button>\n </div>\n </div>\n </div>\n )\n}\n"]}
|