@gallop.software/studio 2.0.1 → 2.0.3

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.
Files changed (47) hide show
  1. package/app/page.tsx +1 -1
  2. package/dist/components/StudioUI.d.mts +15 -0
  3. package/dist/components/StudioUI.d.ts +15 -0
  4. package/dist/components/StudioUI.js +6572 -0
  5. package/dist/components/StudioUI.js.map +1 -0
  6. package/dist/components/StudioUI.mjs +6572 -0
  7. package/dist/components/StudioUI.mjs.map +1 -0
  8. package/dist/handlers/index.js.map +1 -1
  9. package/dist/handlers/index.mjs.map +1 -1
  10. package/next.config.mjs +11 -5
  11. package/package.json +6 -2
  12. package/src/components/AddNewModal.tsx +0 -402
  13. package/src/components/ErrorModal.tsx +0 -89
  14. package/src/components/R2SetupModal.tsx +0 -400
  15. package/src/components/StudioBreadcrumb.tsx +0 -115
  16. package/src/components/StudioButton.tsx +0 -200
  17. package/src/components/StudioContext.tsx +0 -219
  18. package/src/components/StudioDetailView.tsx +0 -714
  19. package/src/components/StudioFileGrid.tsx +0 -704
  20. package/src/components/StudioFileList.tsx +0 -743
  21. package/src/components/StudioFolderPicker.tsx +0 -342
  22. package/src/components/StudioModal.tsx +0 -473
  23. package/src/components/StudioPreview.tsx +0 -399
  24. package/src/components/StudioSettings.tsx +0 -536
  25. package/src/components/StudioToolbar.tsx +0 -1448
  26. package/src/components/StudioUI.tsx +0 -731
  27. package/src/components/styles/common.ts +0 -236
  28. package/src/components/tokens.ts +0 -78
  29. package/src/components/useStudioActions.tsx +0 -497
  30. package/src/config/index.ts +0 -7
  31. package/src/config/workspace.ts +0 -52
  32. package/src/handlers/favicon.ts +0 -152
  33. package/src/handlers/files.ts +0 -784
  34. package/src/handlers/images.ts +0 -949
  35. package/src/handlers/import.ts +0 -190
  36. package/src/handlers/index.ts +0 -168
  37. package/src/handlers/list.ts +0 -627
  38. package/src/handlers/scan.ts +0 -311
  39. package/src/handlers/utils/cdn.ts +0 -234
  40. package/src/handlers/utils/files.ts +0 -64
  41. package/src/handlers/utils/index.ts +0 -4
  42. package/src/handlers/utils/meta.ts +0 -102
  43. package/src/handlers/utils/thumbnails.ts +0 -98
  44. package/src/hooks/useFileList.ts +0 -143
  45. package/src/index.tsx +0 -36
  46. package/src/lib/api.ts +0 -176
  47. package/src/types.ts +0 -119
@@ -1,536 +0,0 @@
1
- /** @jsxImportSource @emotion/react */
2
- 'use client'
3
-
4
- import { useState, useEffect, useCallback } from 'react'
5
- import { css } from '@emotion/react'
6
- import { colors, fontSize, baseReset } from './tokens'
7
- import { useStudio } from './StudioContext'
8
-
9
- // Standard button height for consistency
10
- const btnHeight = '36px'
11
-
12
- const styles = {
13
- btn: css`
14
- height: ${btnHeight};
15
- padding: 0 12px;
16
- background: ${colors.surface};
17
- border: 1px solid ${colors.border};
18
- border-radius: 6px;
19
- cursor: pointer;
20
- transition: all 0.15s ease;
21
- display: flex;
22
- align-items: center;
23
- justify-content: center;
24
-
25
- &:hover {
26
- background-color: ${colors.surfaceHover};
27
- border-color: ${colors.borderHover};
28
- }
29
- `,
30
- icon: css`
31
- width: 16px;
32
- height: 16px;
33
- color: ${colors.textSecondary};
34
- `,
35
- overlay: css`
36
- position: fixed;
37
- top: 0;
38
- right: 0;
39
- bottom: 0;
40
- left: 0;
41
- z-index: 10000;
42
- display: flex;
43
- align-items: center;
44
- justify-content: center;
45
- background-color: rgba(26, 31, 54, 0.4);
46
- backdrop-filter: blur(4px);
47
- `,
48
- panel: css`
49
- ${baseReset}
50
- position: relative;
51
- background-color: ${colors.surface};
52
- border-radius: 12px;
53
- box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);
54
- width: 100%;
55
- max-width: 512px;
56
- padding: 24px;
57
- `,
58
- header: css`
59
- display: flex;
60
- align-items: center;
61
- justify-content: space-between;
62
- margin-bottom: 24px;
63
- `,
64
- title: css`
65
- font-size: ${fontSize.xl};
66
- font-weight: 600;
67
- color: ${colors.text};
68
- margin: 0;
69
- letter-spacing: -0.02em;
70
- `,
71
- closeBtn: css`
72
- padding: 6px;
73
- background: ${colors.surface};
74
- border: 1px solid ${colors.border};
75
- border-radius: 6px;
76
- cursor: pointer;
77
- transition: all 0.15s ease;
78
- display: flex;
79
- align-items: center;
80
- justify-content: center;
81
-
82
- &:hover {
83
- background-color: ${colors.surfaceHover};
84
- border-color: ${colors.borderHover};
85
- }
86
- `,
87
- sections: css`
88
- display: flex;
89
- flex-direction: column;
90
- gap: 24px;
91
- `,
92
- sectionTitle: css`
93
- font-size: ${fontSize.base};
94
- font-weight: 600;
95
- color: ${colors.text};
96
- margin: 0 0 12px 0;
97
- `,
98
- description: css`
99
- font-size: ${fontSize.sm};
100
- color: ${colors.textSecondary};
101
- margin: 0 0 12px 0;
102
- `,
103
- codeWrapper: css`
104
- position: relative;
105
- `,
106
- code: css`
107
- background-color: ${colors.background};
108
- border-radius: 8px;
109
- padding: 12px;
110
- padding-right: 40px;
111
- font-family: 'SF Mono', Monaco, Consolas, monospace;
112
- font-size: ${fontSize.xs};
113
- color: ${colors.textSecondary};
114
- border: 1px solid ${colors.border};
115
- overflow-x: auto;
116
- white-space: nowrap;
117
- `,
118
- copyBtn: css`
119
- position: absolute;
120
- top: 8px;
121
- right: 8px;
122
- padding: 4px;
123
- background: ${colors.surface};
124
- border: 1px solid ${colors.border};
125
- border-radius: 4px;
126
- cursor: pointer;
127
- transition: all 0.15s ease;
128
- display: flex;
129
- align-items: center;
130
- justify-content: center;
131
-
132
- &:hover {
133
- background-color: ${colors.surfaceHover};
134
- border-color: ${colors.borderHover};
135
- }
136
- `,
137
- tooltip: css`
138
- position: absolute;
139
- bottom: 100%;
140
- left: 50%;
141
- transform: translateX(-50%);
142
- background: #1a1f36;
143
- color: white;
144
- padding: 4px 8px;
145
- border-radius: 4px;
146
- font-size: 12px;
147
- white-space: nowrap;
148
- margin-bottom: 6px;
149
- pointer-events: none;
150
- z-index: 100;
151
-
152
- &::after {
153
- content: '';
154
- position: absolute;
155
- top: 100%;
156
- left: 50%;
157
- transform: translateX(-50%);
158
- border: 4px solid transparent;
159
- border-top-color: #1a1f36;
160
- }
161
- `,
162
- copyIcon: css`
163
- width: 14px;
164
- height: 14px;
165
- color: ${colors.textSecondary};
166
- `,
167
- codeLine: css`
168
- margin: 0 0 4px 0;
169
-
170
- &:last-child {
171
- margin: 0;
172
- }
173
- `,
174
- input: css`
175
- width: 100%;
176
- padding: 10px 14px;
177
- border: 1px solid ${colors.border};
178
- border-radius: 6px;
179
- font-size: ${fontSize.base};
180
- color: ${colors.text};
181
- background: ${colors.surface};
182
- transition: all 0.15s ease;
183
-
184
- &:focus {
185
- outline: none;
186
- border-color: ${colors.primary};
187
- box-shadow: 0 0 0 3px ${colors.primaryLight};
188
- }
189
-
190
- &::placeholder {
191
- color: ${colors.textMuted};
192
- }
193
- `,
194
- grid: css`
195
- display: grid;
196
- grid-template-columns: repeat(3, 1fr);
197
- gap: 12px;
198
- `,
199
- label: css`
200
- font-size: ${fontSize.xs};
201
- font-weight: 500;
202
- color: ${colors.textSecondary};
203
- display: block;
204
- margin-bottom: 6px;
205
- `,
206
- footer: css`
207
- margin-top: 24px;
208
- padding-top: 20px;
209
- border-top: 1px solid ${colors.border};
210
- display: flex;
211
- justify-content: flex-end;
212
- gap: 12px;
213
- `,
214
- cancelBtn: css`
215
- padding: 10px 18px;
216
- font-size: ${fontSize.base};
217
- font-weight: 500;
218
- color: ${colors.text};
219
- background: ${colors.surface};
220
- border: 1px solid ${colors.border};
221
- border-radius: 6px;
222
- cursor: pointer;
223
- transition: all 0.15s ease;
224
-
225
- &:hover {
226
- background-color: ${colors.surfaceHover};
227
- border-color: ${colors.borderHover};
228
- }
229
- `,
230
- saveBtn: css`
231
- padding: 10px 18px;
232
- font-size: ${fontSize.base};
233
- font-weight: 500;
234
- color: white;
235
- background-color: ${colors.primary};
236
- border: 1px solid ${colors.primary};
237
- border-radius: 6px;
238
- cursor: pointer;
239
- transition: all 0.15s ease;
240
-
241
- &:hover {
242
- background-color: ${colors.primaryHover};
243
- border-color: ${colors.primaryHover};
244
- }
245
-
246
- &:disabled {
247
- opacity: 0.6;
248
- cursor: not-allowed;
249
- }
250
- `,
251
- cdnList: css`
252
- display: flex;
253
- flex-direction: column;
254
- gap: 8px;
255
- `,
256
- cdnRow: css`
257
- display: flex;
258
- gap: 8px;
259
- align-items: center;
260
- `,
261
- cdnInput: css`
262
- flex: 1;
263
- padding: 8px 12px;
264
- border: 1px solid ${colors.border};
265
- border-radius: 6px;
266
- font-size: ${fontSize.sm};
267
- color: ${colors.text};
268
- background: ${colors.surface};
269
- transition: all 0.15s ease;
270
-
271
- &:focus {
272
- outline: none;
273
- border-color: ${colors.primary};
274
- box-shadow: 0 0 0 3px ${colors.primaryLight};
275
- }
276
- `,
277
- cdnIndex: css`
278
- font-size: ${fontSize.xs};
279
- color: ${colors.textMuted};
280
- width: 24px;
281
- text-align: center;
282
- flex-shrink: 0;
283
- `,
284
- cdnDeleteBtn: css`
285
- padding: 6px;
286
- background: none;
287
- border: none;
288
- cursor: pointer;
289
- color: ${colors.textMuted};
290
- transition: color 0.15s;
291
- flex-shrink: 0;
292
-
293
- &:hover {
294
- color: ${colors.danger};
295
- }
296
- `,
297
- cdnAddBtn: css`
298
- padding: 8px 12px;
299
- font-size: ${fontSize.sm};
300
- font-weight: 500;
301
- color: ${colors.primary};
302
- background: ${colors.primaryLight};
303
- border: 1px dashed ${colors.primary};
304
- border-radius: 6px;
305
- cursor: pointer;
306
- transition: all 0.15s ease;
307
- display: flex;
308
- align-items: center;
309
- justify-content: center;
310
- gap: 6px;
311
- margin-top: 8px;
312
-
313
- &:hover {
314
- background: ${colors.surface};
315
- }
316
- `,
317
- warning: css`
318
- font-size: ${fontSize.xs};
319
- color: ${colors.danger};
320
- margin-top: 8px;
321
- padding: 8px 12px;
322
- background: ${colors.dangerLight};
323
- border-radius: 6px;
324
- `,
325
- }
326
-
327
- export function StudioSettings() {
328
- const [isOpen, setIsOpen] = useState(false)
329
-
330
- return (
331
- <>
332
- <button css={styles.btn} onClick={() => setIsOpen(true)} aria-label="Settings">
333
- <svg
334
- css={styles.icon}
335
- xmlns="http://www.w3.org/2000/svg"
336
- viewBox="0 0 24 24"
337
- fill="none"
338
- stroke="currentColor"
339
- strokeWidth={2}
340
- strokeLinecap="round"
341
- strokeLinejoin="round"
342
- >
343
- <circle cx="12" cy="12" r="3" />
344
- <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" />
345
- </svg>
346
- </button>
347
-
348
- {isOpen && <SettingsPanel onClose={() => setIsOpen(false)} />}
349
- </>
350
- )
351
- }
352
-
353
- const envTemplate = `CLOUDFLARE_R2_ACCOUNT_ID=abc123def456ghi789
354
- CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key_id_here
355
- CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_access_key_here
356
- CLOUDFLARE_R2_BUCKET_NAME=my-images-bucket
357
- CLOUDFLARE_R2_PUBLIC_URL=https://cdn.yourdomain.com`
358
-
359
- function SettingsPanel({ onClose }: { onClose: () => void }) {
360
- const { triggerRefresh } = useStudio()
361
- const [copied, setCopied] = useState(false)
362
- const [cdnUrls, setCdnUrls] = useState<string[]>([])
363
- const [loading, setLoading] = useState(true)
364
- const [saving, setSaving] = useState(false)
365
- const [hasChanges, setHasChanges] = useState(false)
366
-
367
- // Load CDN URLs on mount
368
- useEffect(() => {
369
- async function loadCdns() {
370
- try {
371
- const response = await fetch('/api/studio/cdns')
372
- const data = await response.json()
373
- setCdnUrls(data.cdns || [])
374
- } catch (error) {
375
- console.error('Failed to load CDN URLs:', error)
376
- } finally {
377
- setLoading(false)
378
- }
379
- }
380
- loadCdns()
381
- }, [])
382
-
383
- const handleCopy = () => {
384
- navigator.clipboard.writeText(envTemplate)
385
- setCopied(true)
386
- setTimeout(() => setCopied(false), 2000)
387
- }
388
-
389
- const handleCdnChange = useCallback((index: number, value: string) => {
390
- setCdnUrls(prev => {
391
- const updated = [...prev]
392
- updated[index] = value
393
- return updated
394
- })
395
- setHasChanges(true)
396
- }, [])
397
-
398
- const handleAddCdn = useCallback(() => {
399
- setCdnUrls(prev => [...prev, ''])
400
- setHasChanges(true)
401
- }, [])
402
-
403
- const handleDeleteCdn = useCallback((index: number) => {
404
- setCdnUrls(prev => prev.filter((_, i) => i !== index))
405
- setHasChanges(true)
406
- }, [])
407
-
408
- const handleSave = useCallback(async () => {
409
- setSaving(true)
410
- try {
411
- const response = await fetch('/api/studio/cdns', {
412
- method: 'POST',
413
- headers: { 'Content-Type': 'application/json' },
414
- // Preserve empty strings as placeholders to maintain indices
415
- body: JSON.stringify({ cdns: cdnUrls }),
416
- })
417
-
418
- if (response.ok) {
419
- setHasChanges(false)
420
- triggerRefresh()
421
- onClose()
422
- }
423
- } catch (error) {
424
- console.error('Failed to save CDN URLs:', error)
425
- } finally {
426
- setSaving(false)
427
- }
428
- }, [cdnUrls, triggerRefresh, onClose])
429
-
430
- return (
431
- <div css={styles.overlay} onClick={onClose}>
432
- <div css={styles.panel} onClick={(e) => e.stopPropagation()}>
433
- <div css={styles.header}>
434
- <h2 css={styles.title}>Settings</h2>
435
- <button css={styles.closeBtn} onClick={onClose}>
436
- <svg css={styles.icon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
437
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
438
- </svg>
439
- </button>
440
- </div>
441
-
442
- <div css={styles.sections}>
443
- <section>
444
- <h3 css={styles.sectionTitle}>CDN URLs</h3>
445
- <p css={styles.description}>Manage CDN base URLs used by your images:</p>
446
- {loading ? (
447
- <p css={styles.description}>Loading...</p>
448
- ) : (
449
- <>
450
- <div css={styles.cdnList}>
451
- {cdnUrls.map((url, index) => (
452
- <div key={index} css={styles.cdnRow}>
453
- <span css={styles.cdnIndex}>{index}</span>
454
- <input
455
- css={styles.cdnInput}
456
- type="text"
457
- value={url}
458
- onChange={(e) => handleCdnChange(index, e.target.value)}
459
- placeholder="https://cdn.example.com"
460
- />
461
- <button
462
- css={styles.cdnDeleteBtn}
463
- onClick={() => handleDeleteCdn(index)}
464
- title="Delete CDN URL"
465
- >
466
- <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
467
- <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" />
468
- </svg>
469
- </button>
470
- </div>
471
- ))}
472
- </div>
473
- <button css={styles.cdnAddBtn} onClick={handleAddCdn}>
474
- <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
475
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
476
- </svg>
477
- Add CDN URL
478
- </button>
479
- {cdnUrls.length > 0 && (
480
- <p css={styles.warning}>
481
- Warning: Changing CDN URLs may break image references. The index numbers correspond to image `c` values.
482
- </p>
483
- )}
484
- </>
485
- )}
486
- </section>
487
-
488
- <section>
489
- <h3 css={styles.sectionTitle}>Cloudflare R2 Credentials</h3>
490
- <p css={styles.description}>Configure in .env.local file:</p>
491
- <div css={styles.codeWrapper}>
492
- <button css={styles.copyBtn} onClick={handleCopy} title="Copy to clipboard">
493
- {copied && <span css={styles.tooltip}>Copied!</span>}
494
- <svg css={styles.copyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
495
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
496
- </svg>
497
- </button>
498
- <div css={styles.code}>
499
- <p css={styles.codeLine}>CLOUDFLARE_R2_ACCOUNT_ID=abc123def456ghi789</p>
500
- <p css={styles.codeLine}>CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key_id_here</p>
501
- <p css={styles.codeLine}>CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_access_key_here</p>
502
- <p css={styles.codeLine}>CLOUDFLARE_R2_BUCKET_NAME=my-images-bucket</p>
503
- <p css={styles.codeLine}>CLOUDFLARE_R2_PUBLIC_URL=https://cdn.yourdomain.com</p>
504
- </div>
505
- </div>
506
- </section>
507
-
508
- <section>
509
- <h3 css={styles.sectionTitle}>Thumbnail Sizes</h3>
510
- <div css={styles.grid}>
511
- <div>
512
- <label css={styles.label}>Small</label>
513
- <input css={styles.input} type="number" defaultValue={300} />
514
- </div>
515
- <div>
516
- <label css={styles.label}>Medium</label>
517
- <input css={styles.input} type="number" defaultValue={700} />
518
- </div>
519
- <div>
520
- <label css={styles.label}>Large</label>
521
- <input css={styles.input} type="number" defaultValue={1400} />
522
- </div>
523
- </div>
524
- </section>
525
- </div>
526
-
527
- <div css={styles.footer}>
528
- <button css={styles.cancelBtn} onClick={onClose}>Cancel</button>
529
- <button css={styles.saveBtn} onClick={handleSave} disabled={saving || !hasChanges}>
530
- {saving ? 'Saving...' : 'Save Changes'}
531
- </button>
532
- </div>
533
- </div>
534
- </div>
535
- )
536
- }