@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,400 +0,0 @@
1
- /** @jsxImportSource @emotion/react */
2
- 'use client'
3
-
4
- import { useState } from 'react'
5
- import { css } from '@emotion/react'
6
- import { colors, fontSize } from './tokens'
7
-
8
- const ENV_TEMPLATE = `CLOUDFLARE_R2_ACCOUNT_ID=your_account_id
9
- CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key
10
- CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key
11
- CLOUDFLARE_R2_BUCKET_NAME=your_bucket_name
12
- CLOUDFLARE_R2_PUBLIC_URL=https://pub-xxx.r2.dev
13
- NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL=https://pub-xxx.r2.dev`
14
-
15
- interface R2SetupModalProps {
16
- isOpen: boolean
17
- onClose: () => void
18
- }
19
-
20
- const styles = {
21
- overlay: css`
22
- position: fixed;
23
- inset: 0;
24
- background: rgba(0, 0, 0, 0.6);
25
- display: flex;
26
- align-items: center;
27
- justify-content: center;
28
- z-index: 1100;
29
- padding: 20px;
30
- `,
31
- modal: css`
32
- background: ${colors.surface};
33
- border-radius: 12px;
34
- max-width: 560px;
35
- width: 100%;
36
- max-height: 90vh;
37
- overflow-y: auto;
38
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
39
- `,
40
- header: css`
41
- display: flex;
42
- align-items: center;
43
- gap: 12px;
44
- padding: 20px 24px;
45
- border-bottom: 1px solid ${colors.border};
46
- `,
47
- icon: css`
48
- width: 32px;
49
- height: 32px;
50
- color: ${colors.primary};
51
- flex-shrink: 0;
52
- `,
53
- title: css`
54
- font-size: ${fontSize.xl};
55
- font-weight: 600;
56
- color: ${colors.text};
57
- margin: 0;
58
- `,
59
- closeBtn: css`
60
- margin-left: auto;
61
- background: none;
62
- border: none;
63
- padding: 4px;
64
- cursor: pointer;
65
- color: ${colors.textMuted};
66
- border-radius: 4px;
67
-
68
- &:hover {
69
- color: ${colors.text};
70
- background: ${colors.surfaceHover};
71
- }
72
- `,
73
- closeIcon: css`
74
- width: 20px;
75
- height: 20px;
76
- `,
77
- content: css`
78
- padding: 24px;
79
- `,
80
- intro: css`
81
- font-size: ${fontSize.base};
82
- color: ${colors.textSecondary};
83
- margin: 0 0 20px 0;
84
- line-height: 1.6;
85
- `,
86
- steps: css`
87
- list-style: none;
88
- padding: 0;
89
- margin: 0;
90
- display: flex;
91
- flex-direction: column;
92
- gap: 16px;
93
- `,
94
- step: css`
95
- display: flex;
96
- gap: 12px;
97
- `,
98
- stepNumber: css`
99
- width: 28px;
100
- height: 28px;
101
- border-radius: 50%;
102
- background: ${colors.primaryLight};
103
- color: ${colors.primary};
104
- font-size: ${fontSize.sm};
105
- font-weight: 600;
106
- display: flex;
107
- align-items: center;
108
- justify-content: center;
109
- flex-shrink: 0;
110
- `,
111
- stepContent: css`
112
- flex: 1;
113
- padding-top: 3px;
114
- `,
115
- stepTitle: css`
116
- font-size: ${fontSize.base};
117
- font-weight: 500;
118
- color: ${colors.text};
119
- margin: 0 0 4px 0;
120
- `,
121
- stepDesc: css`
122
- font-size: ${fontSize.sm};
123
- color: ${colors.textSecondary};
124
- margin: 0;
125
- line-height: 1.5;
126
- `,
127
- link: css`
128
- color: ${colors.primary};
129
- text-decoration: none;
130
- font-weight: 500;
131
-
132
- &:hover {
133
- text-decoration: underline;
134
- }
135
- `,
136
- envVarsWrapper: css`
137
- position: relative;
138
- margin-top: 20px;
139
- `,
140
- envVars: css`
141
- background: ${colors.background};
142
- border: 1px solid ${colors.border};
143
- border-radius: 8px;
144
- padding: 16px;
145
- padding-right: 48px;
146
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
147
- font-size: 13px;
148
- line-height: 1.8;
149
- color: ${colors.text};
150
- overflow-x: auto;
151
- `,
152
- envVar: css`
153
- display: block;
154
- `,
155
- envKey: css`
156
- color: ${colors.primary};
157
- `,
158
- envValue: css`
159
- color: ${colors.textSecondary};
160
- `,
161
- copyBtn: css`
162
- position: absolute;
163
- top: 8px;
164
- right: 8px;
165
- width: 32px;
166
- height: 32px;
167
- display: flex;
168
- align-items: center;
169
- justify-content: center;
170
- background: ${colors.surface};
171
- border: 1px solid ${colors.border};
172
- border-radius: 6px;
173
- cursor: pointer;
174
- color: ${colors.textMuted};
175
- transition: all 0.15s ease;
176
-
177
- &:hover {
178
- background: ${colors.surfaceHover};
179
- color: ${colors.text};
180
- border-color: #d0d5dd;
181
- }
182
- `,
183
- copyIcon: css`
184
- width: 16px;
185
- height: 16px;
186
- `,
187
- copiedTooltip: css`
188
- position: absolute;
189
- top: 50%;
190
- right: 100%;
191
- transform: translateY(-50%);
192
- background: #1a1f36;
193
- color: white;
194
- padding: 4px 8px;
195
- border-radius: 4px;
196
- font-size: 12px;
197
- white-space: nowrap;
198
- margin-right: 6px;
199
- pointer-events: none;
200
-
201
- &::before {
202
- content: '';
203
- position: absolute;
204
- right: -4px;
205
- top: 50%;
206
- transform: translateY(-50%);
207
- border-left: 4px solid #1a1f36;
208
- border-top: 4px solid transparent;
209
- border-bottom: 4px solid transparent;
210
- }
211
- `,
212
- footer: css`
213
- padding: 16px 24px;
214
- border-top: 1px solid ${colors.border};
215
- display: flex;
216
- justify-content: flex-end;
217
- gap: 12px;
218
- `,
219
- docsBtn: css`
220
- padding: 10px 16px;
221
- border-radius: 6px;
222
- font-size: ${fontSize.base};
223
- font-weight: 500;
224
- border: 1px solid ${colors.border};
225
- background: ${colors.surface};
226
- color: ${colors.text};
227
- cursor: pointer;
228
- text-decoration: none;
229
- display: inline-flex;
230
- align-items: center;
231
- gap: 6px;
232
- transition: all 0.15s ease;
233
-
234
- &:hover {
235
- background: ${colors.surfaceHover};
236
- border-color: #d0d5dd;
237
- }
238
- `,
239
- doneBtn: css`
240
- padding: 10px 20px;
241
- border-radius: 6px;
242
- font-size: ${fontSize.base};
243
- font-weight: 500;
244
- border: none;
245
- background: ${colors.primary};
246
- color: white;
247
- cursor: pointer;
248
- transition: background 0.15s ease;
249
-
250
- &:hover {
251
- background: ${colors.primaryHover};
252
- }
253
- `,
254
- externalIcon: css`
255
- width: 14px;
256
- height: 14px;
257
- `,
258
- }
259
-
260
- export function R2SetupModal({ isOpen, onClose }: R2SetupModalProps) {
261
- const [copied, setCopied] = useState(false)
262
-
263
- const handleCopy = async () => {
264
- try {
265
- await navigator.clipboard.writeText(ENV_TEMPLATE)
266
- setCopied(true)
267
- setTimeout(() => setCopied(false), 2000)
268
- } catch (error) {
269
- console.error('Failed to copy:', error)
270
- }
271
- }
272
-
273
- if (!isOpen) return null
274
-
275
- return (
276
- <div css={styles.overlay} onClick={onClose}>
277
- <div css={styles.modal} onClick={(e) => e.stopPropagation()}>
278
- <div css={styles.header}>
279
- <svg css={styles.icon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
280
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
281
- </svg>
282
- <h2 css={styles.title}>Set Up CDN Storage</h2>
283
- <button css={styles.closeBtn} onClick={onClose}>
284
- <svg css={styles.closeIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
285
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
286
- </svg>
287
- </button>
288
- </div>
289
-
290
- <div css={styles.content}>
291
- <p css={styles.intro}>
292
- Sync your images to Cloudflare R2 for faster global delivery. R2 offers generous free tier with no egress fees.
293
- </p>
294
-
295
- <ol css={styles.steps}>
296
- <li css={styles.step}>
297
- <span css={styles.stepNumber}>1</span>
298
- <div css={styles.stepContent}>
299
- <h4 css={styles.stepTitle}>Create a Cloudflare account</h4>
300
- <p css={styles.stepDesc}>
301
- Sign up at{' '}
302
- <a css={styles.link} href="https://dash.cloudflare.com/sign-up" target="_blank" rel="noopener noreferrer">
303
- dash.cloudflare.com
304
- </a>
305
- {' '}if you don't have one already.
306
- </p>
307
- </div>
308
- </li>
309
-
310
- <li css={styles.step}>
311
- <span css={styles.stepNumber}>2</span>
312
- <div css={styles.stepContent}>
313
- <h4 css={styles.stepTitle}>Create an R2 bucket</h4>
314
- <p css={styles.stepDesc}>
315
- Go to R2 in your Cloudflare dashboard and create a new bucket. Choose a name like <code>my-images</code>.
316
- </p>
317
- </div>
318
- </li>
319
-
320
- <li css={styles.step}>
321
- <span css={styles.stepNumber}>3</span>
322
- <div css={styles.stepContent}>
323
- <h4 css={styles.stepTitle}>Enable public access</h4>
324
- <p css={styles.stepDesc}>
325
- In bucket settings, enable "Public Access" and copy the public URL (e.g., <code>https://pub-xxx.r2.dev</code>).
326
- </p>
327
- </div>
328
- </li>
329
-
330
- <li css={styles.step}>
331
- <span css={styles.stepNumber}>4</span>
332
- <div css={styles.stepContent}>
333
- <h4 css={styles.stepTitle}>Create API token</h4>
334
- <p css={styles.stepDesc}>
335
- Go to R2 → Manage R2 API Tokens → Create API Token. Select "Object Read & Write" permissions for your bucket.
336
- </p>
337
- </div>
338
- </li>
339
-
340
- <li css={styles.step}>
341
- <span css={styles.stepNumber}>5</span>
342
- <div css={styles.stepContent}>
343
- <h4 css={styles.stepTitle}>Add environment variables</h4>
344
- <p css={styles.stepDesc}>
345
- Add these to your <code>.env.local</code> file:
346
- </p>
347
- </div>
348
- </li>
349
- </ol>
350
-
351
- <div css={styles.envVarsWrapper}>
352
- <div css={styles.envVars}>
353
- <span css={styles.envVar}>
354
- <span css={styles.envKey}>CLOUDFLARE_R2_ACCOUNT_ID</span>=<span css={styles.envValue}>your_account_id</span>
355
- </span>
356
- <span css={styles.envVar}>
357
- <span css={styles.envKey}>CLOUDFLARE_R2_ACCESS_KEY_ID</span>=<span css={styles.envValue}>your_access_key</span>
358
- </span>
359
- <span css={styles.envVar}>
360
- <span css={styles.envKey}>CLOUDFLARE_R2_SECRET_ACCESS_KEY</span>=<span css={styles.envValue}>your_secret_key</span>
361
- </span>
362
- <span css={styles.envVar}>
363
- <span css={styles.envKey}>CLOUDFLARE_R2_BUCKET_NAME</span>=<span css={styles.envValue}>your_bucket_name</span>
364
- </span>
365
- <span css={styles.envVar}>
366
- <span css={styles.envKey}>CLOUDFLARE_R2_PUBLIC_URL</span>=<span css={styles.envValue}>https://pub-xxx.r2.dev</span>
367
- </span>
368
- <span css={styles.envVar}>
369
- <span css={styles.envKey}>NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL</span>=<span css={styles.envValue}>https://pub-xxx.r2.dev</span>
370
- </span>
371
- </div>
372
- <button css={styles.copyBtn} onClick={handleCopy} title="Copy to clipboard">
373
- {copied && <span css={styles.copiedTooltip}>Copied!</span>}
374
- <svg css={styles.copyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
375
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
376
- </svg>
377
- </button>
378
- </div>
379
- </div>
380
-
381
- <div css={styles.footer}>
382
- <a
383
- css={styles.docsBtn}
384
- href="https://developers.cloudflare.com/r2/get-started/"
385
- target="_blank"
386
- rel="noopener noreferrer"
387
- >
388
- R2 Documentation
389
- <svg css={styles.externalIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
390
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
391
- </svg>
392
- </a>
393
- <button css={styles.doneBtn} onClick={onClose}>
394
- Got it
395
- </button>
396
- </div>
397
- </div>
398
- </div>
399
- )
400
- }
@@ -1,115 +0,0 @@
1
- /** @jsxImportSource @emotion/react */
2
- 'use client'
3
-
4
- import { css } from '@emotion/react'
5
- import { useStudio } from './StudioContext'
6
- import { colors, fontSize } from './tokens'
7
-
8
- const styles = {
9
- container: css`
10
- display: flex;
11
- align-items: center;
12
- gap: 8px;
13
- padding: 10px 24px;
14
- background-color: ${colors.surface};
15
- border-bottom: 1px solid ${colors.borderLight};
16
- `,
17
- backBtn: css`
18
- padding: 6px;
19
- background: ${colors.surface};
20
- border: 1px solid ${colors.border};
21
- border-radius: 6px;
22
- cursor: pointer;
23
- transition: all 0.15s ease;
24
- display: flex;
25
- align-items: center;
26
- justify-content: center;
27
-
28
- &:hover {
29
- background-color: ${colors.surfaceHover};
30
- border-color: ${colors.borderHover};
31
- }
32
- `,
33
- backIcon: css`
34
- width: 16px;
35
- height: 16px;
36
- color: ${colors.textSecondary};
37
- `,
38
- nav: css`
39
- display: flex;
40
- align-items: center;
41
- gap: 2px;
42
- font-size: ${fontSize.base};
43
- `,
44
- item: css`
45
- display: flex;
46
- align-items: center;
47
- gap: 2px;
48
- `,
49
- separator: css`
50
- color: ${colors.textMuted};
51
- margin: 0 2px;
52
- `,
53
- btn: css`
54
- padding: 4px 8px;
55
- background: none;
56
- border: none;
57
- border-radius: 4px;
58
- cursor: pointer;
59
- transition: all 0.15s ease;
60
- font-size: ${fontSize.base};
61
- letter-spacing: -0.01em;
62
-
63
- &:hover {
64
- background-color: ${colors.surfaceHover};
65
- }
66
- `,
67
- btnActive: css`
68
- color: ${colors.text};
69
- font-weight: 600;
70
- `,
71
- btnInactive: css`
72
- color: ${colors.textSecondary};
73
-
74
- &:hover {
75
- color: ${colors.text};
76
- }
77
- `,
78
- }
79
-
80
- export function StudioBreadcrumb() {
81
- const { currentPath, setCurrentPath, navigateUp } = useStudio()
82
-
83
- const parts = currentPath.split('/').filter(Boolean)
84
-
85
- const handleClick = (index: number) => {
86
- const newPath = parts.slice(0, index + 1).join('/')
87
- setCurrentPath(newPath)
88
- }
89
-
90
- return (
91
- <div css={styles.container}>
92
- {currentPath !== 'public' && (
93
- <button css={styles.backBtn} onClick={navigateUp} aria-label="Go back">
94
- <svg css={styles.backIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
96
- </svg>
97
- </button>
98
- )}
99
-
100
- <nav css={styles.nav}>
101
- {parts.map((part, index) => (
102
- <span key={index} css={styles.item}>
103
- {index > 0 && <span css={styles.separator}>/</span>}
104
- <button
105
- css={[styles.btn, index === parts.length - 1 ? styles.btnActive : styles.btnInactive]}
106
- onClick={() => handleClick(index)}
107
- >
108
- {part}
109
- </button>
110
- </span>
111
- ))}
112
- </nav>
113
- </div>
114
- )
115
- }
@@ -1,200 +0,0 @@
1
- /** @jsxImportSource @emotion/react */
2
- 'use client'
3
-
4
- import { useState, useEffect, lazy, Suspense } from 'react'
5
- import { css, keyframes } from '@emotion/react'
6
- import { colors, fontStack, fontSize, baseReset } from './tokens'
7
-
8
- // Lazy load the full Studio UI to avoid bundling in production
9
- const StudioUI = lazy(() => import('./StudioUI'))
10
-
11
- const spin = keyframes`
12
- to {
13
- transform: rotate(360deg);
14
- }
15
- `
16
-
17
- const styles = {
18
- button: css`
19
- position: fixed;
20
- bottom: 24px;
21
- right: 24px;
22
- z-index: 9998;
23
- width: 52px;
24
- height: 52px;
25
- border-radius: 50%;
26
- background: ${colors.primary};
27
- color: white;
28
- box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};
29
- display: flex;
30
- align-items: center;
31
- justify-content: center;
32
- border: none;
33
- cursor: pointer;
34
- transition: all 0.15s ease;
35
- font-family: ${fontStack};
36
-
37
- &:hover {
38
- transform: translateY(-2px);
39
- box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};
40
- background: ${colors.primaryHover};
41
- }
42
-
43
- &:active {
44
- transform: translateY(0);
45
- }
46
- `,
47
- buttonIcon: css`
48
- width: 24px;
49
- height: 24px;
50
- `,
51
- overlay: css`
52
- position: fixed;
53
- top: 0;
54
- right: 0;
55
- bottom: 0;
56
- left: 0;
57
- z-index: 9999;
58
- transition: opacity 0.2s ease, visibility 0.2s ease;
59
- `,
60
- overlayHidden: css`
61
- opacity: 0;
62
- visibility: hidden;
63
- pointer-events: none;
64
- `,
65
- backdrop: css`
66
- position: absolute;
67
- top: 0;
68
- right: 0;
69
- bottom: 0;
70
- left: 0;
71
- background-color: rgba(26, 31, 54, 0.4);
72
- backdrop-filter: blur(4px);
73
- `,
74
- modal: css`
75
- ${baseReset}
76
- position: absolute;
77
- top: 24px;
78
- right: 24px;
79
- bottom: 24px;
80
- left: 24px;
81
- background-color: ${colors.surface};
82
- border-radius: 12px;
83
- box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);
84
- display: flex;
85
- flex-direction: column;
86
- overflow: hidden;
87
- `,
88
- loading: css`
89
- display: flex;
90
- align-items: center;
91
- justify-content: center;
92
- height: 100%;
93
- background: ${colors.background};
94
- font-family: ${fontStack};
95
- `,
96
- loadingContent: css`
97
- display: flex;
98
- flex-direction: column;
99
- align-items: center;
100
- gap: 16px;
101
- `,
102
- spinner: css`
103
- width: 36px;
104
- height: 36px;
105
- border-radius: 50%;
106
- border: 3px solid ${colors.border};
107
- border-top-color: ${colors.primary};
108
- animation: ${spin} 0.8s linear infinite;
109
- `,
110
- loadingText: css`
111
- color: ${colors.textSecondary};
112
- font-size: ${fontSize.base};
113
- font-weight: 500;
114
- margin: 0;
115
- letter-spacing: -0.01em;
116
- `,
117
- }
118
-
119
- /**
120
- * Floating button that opens the Studio modal.
121
- * Fixed position in bottom-right corner.
122
- * Only renders in development mode.
123
- */
124
- export function StudioButton() {
125
- const [mounted, setMounted] = useState(false)
126
- const [isOpen, setIsOpen] = useState(false)
127
- const [hasBeenOpened, setHasBeenOpened] = useState(false)
128
-
129
- // Only render on client to avoid hydration mismatch
130
- useEffect(() => {
131
- setMounted(true)
132
- }, [])
133
-
134
- const handleOpen = () => {
135
- setIsOpen(true)
136
- setHasBeenOpened(true)
137
- }
138
-
139
- // Only render in development and on client
140
- if (!mounted || process.env.NODE_ENV !== 'development') {
141
- return null
142
- }
143
-
144
- return (
145
- <>
146
- {!isOpen && (
147
- <button
148
- css={styles.button}
149
- onClick={handleOpen}
150
- title="Open Studio"
151
- aria-label="Open Studio media manager"
152
- >
153
- <ImageIcon />
154
- </button>
155
- )}
156
-
157
- {/* Keep mounted once opened to preserve state */}
158
- {hasBeenOpened && (
159
- <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>
160
- <div css={styles.backdrop} onClick={() => setIsOpen(false)} />
161
- <div css={styles.modal}>
162
- <Suspense fallback={<LoadingState />}>
163
- <StudioUI onClose={() => setIsOpen(false)} isVisible={isOpen} />
164
- </Suspense>
165
- </div>
166
- </div>
167
- )}
168
- </>
169
- )
170
- }
171
-
172
- function ImageIcon() {
173
- return (
174
- <svg
175
- css={styles.buttonIcon}
176
- xmlns="http://www.w3.org/2000/svg"
177
- viewBox="0 0 24 24"
178
- fill="none"
179
- stroke="currentColor"
180
- strokeWidth={2}
181
- strokeLinecap="round"
182
- strokeLinejoin="round"
183
- >
184
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
185
- <circle cx="8.5" cy="8.5" r="1.5" />
186
- <polyline points="21 15 16 10 5 21" />
187
- </svg>
188
- )
189
- }
190
-
191
- function LoadingState() {
192
- return (
193
- <div css={styles.loading}>
194
- <div css={styles.loadingContent}>
195
- <div css={styles.spinner} />
196
- <p css={styles.loadingText}>Loading Studio...</p>
197
- </div>
198
- </div>
199
- )
200
- }