@sentroy-co/client-sdk 2.13.4 → 2.13.6
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/README.md +52 -0
- package/dist/react/crop/CropDialog.d.ts +1 -2
- package/dist/react/crop/CropDialog.d.ts.map +1 -1
- package/dist/react/crop/CropDialog.js +48 -159
- package/dist/react/crop/CropDialog.js.map +1 -1
- package/dist/react/crop/styles.css +802 -59
- package/package.json +2 -2
- package/src/react/crop/CropDialog.tsx +125 -361
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentroy-co/client-sdk",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.6",
|
|
4
4
|
"description": "TypeScript SDK + CLI for the Sentroy platform — mail, storage, env vault + React components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -101,6 +101,6 @@
|
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"motion": "^12.38.0",
|
|
104
|
-
"react-
|
|
104
|
+
"react-mobile-cropper": "^0.10.0"
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
2
|
-
import { Cropper, type CropperRef } from "react-
|
|
2
|
+
import { Cropper, type CropperRef } from "react-mobile-cropper"
|
|
3
3
|
import { motion, AnimatePresence } from "motion/react"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Image crop dialog — iOS Photos benzeri full-screen crop UI.
|
|
7
|
-
* `react-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Image crop dialog — iOS Photos benzeri full-screen crop UI.
|
|
7
|
+
* `react-mobile-cropper` üzerine kurulu; paket native olarak iOS-vari
|
|
8
|
+
* stencil + handler + transition davranışı veriyor (manual layout/icon
|
|
9
|
+
* gerek yok).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* iOS Photos pattern:
|
|
12
|
+
* - Header: Cancel (sol) — "Crop" başlığı (orta) — Done (sağ)
|
|
13
|
+
* - Main: Cropper full width, stencil ile karartılmış kenar
|
|
14
|
+
* - Bottom toolbar: aspect chip'leri (Free / 1:1 / 4:3 / 16:9 / 3:2 / 9:16)
|
|
15
|
+
* + sağ tarafta tek rotate ikonu
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - Rotate 90° CW/CCW butonları (R / Shift+R)
|
|
20
|
-
* - Cropper'ın `getCanvas()` çıktısından **live preview**
|
|
21
|
-
* thumbnail — kullanıcı Apply'a basmadan sonucu görür.
|
|
22
|
-
* - Output pixel boyutu (örn. 1200×800) read-out.
|
|
23
|
-
* - Apply: ref üzerinden `getCanvas().toBlob` → File.
|
|
24
|
-
* - "Use original": orijinal File döner; Cancel: null.
|
|
25
|
-
*
|
|
26
|
-
* Tam ekran: `inset-0`, scrim'siz; consent flow gibi destination-only UX —
|
|
27
|
-
* dialog her şeyi kaplar, dikkat dağıtmaz.
|
|
17
|
+
* Preview thumbnail kasıtlı olarak yok — iOS Photos'ta da yok; kullanıcı
|
|
18
|
+
* stencil'in içini direkt görüyor.
|
|
28
19
|
*
|
|
29
20
|
* Lazy subpath (`@sentroy-co/client-sdk/react/crop`) — ana SDK import'u
|
|
30
21
|
* cropper bundle'ı yutmasın.
|
|
@@ -43,8 +34,7 @@ const ASPECT_PRESETS: Array<{
|
|
|
43
34
|
{ id: "9:16", label: "9:16", aspect: 9 / 16 },
|
|
44
35
|
]
|
|
45
36
|
|
|
46
|
-
const MAX_PIXEL_GUARD = 50_000_000 // ~24 MP
|
|
47
|
-
const PREVIEW_MAX_DIM = 240
|
|
37
|
+
const MAX_PIXEL_GUARD = 50_000_000 // ~24 MP
|
|
48
38
|
|
|
49
39
|
export interface CropDialogProps {
|
|
50
40
|
open: boolean
|
|
@@ -55,8 +45,7 @@ export interface CropDialogProps {
|
|
|
55
45
|
onClose: (result: File | null) => void
|
|
56
46
|
/** Default aspect preset id'si — 'free' (default) veya '1:1', '16:9', vb. */
|
|
57
47
|
defaultAspect?: string
|
|
58
|
-
/** Output JPEG quality 0-1 (default 0.92).
|
|
59
|
-
* veya orijinal MIME (PNG'ler için PNG korunur). */
|
|
48
|
+
/** Output JPEG quality 0-1 (default 0.92). PNG'ler için PNG korunur. */
|
|
60
49
|
outputQuality?: number
|
|
61
50
|
}
|
|
62
51
|
|
|
@@ -71,13 +60,7 @@ export function CropDialog({
|
|
|
71
60
|
const [aspectId, setAspectId] = useState(defaultAspect)
|
|
72
61
|
const [busy, setBusy] = useState(false)
|
|
73
62
|
const [tooLarge, setTooLarge] = useState(false)
|
|
74
|
-
const [outputSize, setOutputSize] = useState<{ w: number; h: number } | null>(
|
|
75
|
-
null,
|
|
76
|
-
)
|
|
77
|
-
const [previewDataUrl, setPreviewDataUrl] = useState<string | null>(null)
|
|
78
|
-
|
|
79
63
|
const cropperRef = useRef<CropperRef | null>(null)
|
|
80
|
-
const previewRafRef = useRef<number | null>(null)
|
|
81
64
|
|
|
82
65
|
// Object URL lifecycle
|
|
83
66
|
useEffect(() => {
|
|
@@ -86,9 +69,6 @@ export function CropDialog({
|
|
|
86
69
|
setImageUrl(url)
|
|
87
70
|
setAspectId(defaultAspect)
|
|
88
71
|
setTooLarge(false)
|
|
89
|
-
setOutputSize(null)
|
|
90
|
-
setPreviewDataUrl(null)
|
|
91
|
-
// Pixel guard — large image decode tarayıcıyı çökertir
|
|
92
72
|
const img = new Image()
|
|
93
73
|
img.onload = () => {
|
|
94
74
|
if (img.naturalWidth * img.naturalHeight > MAX_PIXEL_GUARD) {
|
|
@@ -96,98 +76,36 @@ export function CropDialog({
|
|
|
96
76
|
}
|
|
97
77
|
}
|
|
98
78
|
img.src = url
|
|
99
|
-
return () =>
|
|
100
|
-
URL.revokeObjectURL(url)
|
|
101
|
-
if (previewRafRef.current !== null) {
|
|
102
|
-
cancelAnimationFrame(previewRafRef.current)
|
|
103
|
-
previewRafRef.current = null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
79
|
+
return () => URL.revokeObjectURL(url)
|
|
106
80
|
}, [open, file, defaultAspect])
|
|
107
81
|
|
|
108
82
|
const aspectRatio =
|
|
109
83
|
ASPECT_PRESETS.find((p) => p.id === aspectId)?.aspect ?? undefined
|
|
110
84
|
|
|
111
|
-
// Aspect preset değişirse
|
|
112
|
-
// güncelle. `setCoordinates` ile aspect'i zorla.
|
|
85
|
+
// Aspect preset değişirse stencil koordinatlarını yeni aspect'e snap'le
|
|
113
86
|
useEffect(() => {
|
|
114
|
-
|
|
115
|
-
if (aspectRatio === undefined) return
|
|
116
|
-
const state =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
// Aspect ratio'ya snap — width'i koru, height'i hesapla
|
|
123
|
-
const newWidth = coordinates.width
|
|
124
|
-
const newHeight = coordinates.width / aspectRatio
|
|
125
|
-
cropperRef.current.setCoordinates({
|
|
126
|
-
width: newWidth,
|
|
127
|
-
height: newHeight,
|
|
128
|
-
})
|
|
87
|
+
const cropper = cropperRef.current
|
|
88
|
+
if (!cropper || aspectRatio === undefined) return
|
|
89
|
+
const state = cropper.getState()
|
|
90
|
+
const c = state?.coordinates
|
|
91
|
+
if (!c) return
|
|
92
|
+
const newWidth = c.width
|
|
93
|
+
const newHeight = c.width / aspectRatio
|
|
94
|
+
cropper.setCoordinates({ width: newWidth, height: newHeight })
|
|
129
95
|
}, [aspectRatio, aspectId])
|
|
130
96
|
|
|
131
|
-
// Live preview render — cropper change event'inde getCanvas() ile
|
|
132
|
-
// küçük thumbnail üret. RAF ile throttle.
|
|
133
|
-
const renderPreview = useCallback(() => {
|
|
134
|
-
if (previewRafRef.current !== null) {
|
|
135
|
-
cancelAnimationFrame(previewRafRef.current)
|
|
136
|
-
}
|
|
137
|
-
previewRafRef.current = requestAnimationFrame(() => {
|
|
138
|
-
previewRafRef.current = null
|
|
139
|
-
const cropper = cropperRef.current
|
|
140
|
-
if (!cropper) return
|
|
141
|
-
// Önce gerçek crop pixel boyutunu öğren; preview canvas'ını aspect
|
|
142
|
-
// koruyarak max edge PREVIEW_MAX_DIM'e fit ediyoruz. `getCanvas`
|
|
143
|
-
// sadece `width` verince paket aspect'i koruyup height'i otomatik
|
|
144
|
-
// hesaplıyor. `maxWidth/maxHeight` paket API'sinde yok — eski
|
|
145
|
-
// çağrımız sessiz fail edip canvas üretmiyordu.
|
|
146
|
-
const state = cropper.getState()
|
|
147
|
-
const c = state?.coordinates
|
|
148
|
-
if (!c || c.width === 0 || c.height === 0) return
|
|
149
|
-
const scale =
|
|
150
|
-
c.width >= c.height
|
|
151
|
-
? PREVIEW_MAX_DIM / c.width
|
|
152
|
-
: PREVIEW_MAX_DIM / c.height
|
|
153
|
-
const previewWidth = Math.max(1, Math.round(c.width * scale))
|
|
154
|
-
const canvas = cropper.getCanvas({
|
|
155
|
-
width: previewWidth,
|
|
156
|
-
imageSmoothingEnabled: true,
|
|
157
|
-
imageSmoothingQuality: "medium",
|
|
158
|
-
})
|
|
159
|
-
if (!canvas) return
|
|
160
|
-
setOutputSize({ w: Math.round(c.width), h: Math.round(c.height) })
|
|
161
|
-
// Data URL — küçük canvas; performant
|
|
162
|
-
try {
|
|
163
|
-
setPreviewDataUrl(canvas.toDataURL("image/jpeg", 0.7))
|
|
164
|
-
} catch {
|
|
165
|
-
// toDataURL nadir tainted-canvas durumunda fail edebilir; sessiz geç
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
}, [])
|
|
169
|
-
|
|
170
|
-
const handleCropperChange = useCallback(() => {
|
|
171
|
-
renderPreview()
|
|
172
|
-
}, [renderPreview])
|
|
173
|
-
|
|
174
|
-
const handleCropperReady = useCallback(() => {
|
|
175
|
-
renderPreview()
|
|
176
|
-
}, [renderPreview])
|
|
177
|
-
|
|
178
97
|
const handleApply = useCallback(async () => {
|
|
179
98
|
const cropper = cropperRef.current
|
|
180
99
|
if (!cropper) return
|
|
181
100
|
setBusy(true)
|
|
182
101
|
try {
|
|
183
|
-
const canvas = cropper.getCanvas({
|
|
184
|
-
imageSmoothingQuality: "high",
|
|
185
|
-
})
|
|
102
|
+
const canvas = cropper.getCanvas({ imageSmoothingQuality: "high" })
|
|
186
103
|
if (!canvas) {
|
|
187
104
|
setBusy(false)
|
|
188
105
|
return
|
|
189
106
|
}
|
|
190
|
-
const outputMime =
|
|
107
|
+
const outputMime =
|
|
108
|
+
file.type === "image/png" ? "image/png" : "image/jpeg"
|
|
191
109
|
const blob = await new Promise<Blob | null>((resolve) => {
|
|
192
110
|
canvas.toBlob(
|
|
193
111
|
(b) => resolve(b),
|
|
@@ -212,14 +130,11 @@ export function CropDialog({
|
|
|
212
130
|
|
|
213
131
|
const handleUseOriginal = useCallback(() => onClose(file), [file, onClose])
|
|
214
132
|
const handleCancel = useCallback(() => onClose(null), [onClose])
|
|
215
|
-
const handleRotate = useCallback((
|
|
216
|
-
cropperRef.current?.rotateImage(
|
|
217
|
-
}, [])
|
|
218
|
-
const handleFlip = useCallback((axis: "h" | "v") => {
|
|
219
|
-
cropperRef.current?.flipImage(axis === "h", axis === "v")
|
|
133
|
+
const handleRotate = useCallback(() => {
|
|
134
|
+
cropperRef.current?.rotateImage(90)
|
|
220
135
|
}, [])
|
|
221
136
|
|
|
222
|
-
//
|
|
137
|
+
// ESC kapat, R rotate
|
|
223
138
|
useEffect(() => {
|
|
224
139
|
if (!open) return
|
|
225
140
|
const onKey = (e: KeyboardEvent) => {
|
|
@@ -236,7 +151,7 @@ export function CropDialog({
|
|
|
236
151
|
}
|
|
237
152
|
if (e.key === "r" || e.key === "R") {
|
|
238
153
|
e.preventDefault()
|
|
239
|
-
handleRotate(
|
|
154
|
+
handleRotate()
|
|
240
155
|
}
|
|
241
156
|
}
|
|
242
157
|
window.addEventListener("keydown", onKey)
|
|
@@ -252,22 +167,36 @@ export function CropDialog({
|
|
|
252
167
|
animate={{ opacity: 1 }}
|
|
253
168
|
exit={{ opacity: 0 }}
|
|
254
169
|
transition={{ duration: 0.2 }}
|
|
255
|
-
//
|
|
256
|
-
|
|
170
|
+
// Inline color — host app'in tema değişkenleri text-white
|
|
171
|
+
// utility'sini override etse bile child icon/text bu rengi
|
|
172
|
+
// inherit eder (rotate ikonu, chip metinleri vs).
|
|
173
|
+
style={{ color: "#ffffff" }}
|
|
174
|
+
className="fixed inset-0 z-[60] flex flex-col bg-black"
|
|
257
175
|
>
|
|
258
|
-
{/* Header */}
|
|
259
|
-
<
|
|
176
|
+
{/* Header — iOS Photos: Cancel sol / başlık orta / Done sağ */}
|
|
177
|
+
<header
|
|
178
|
+
className="flex items-center justify-between gap-3 px-4 py-3"
|
|
179
|
+
style={{
|
|
180
|
+
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
181
|
+
background: "rgba(0,0,0,0.4)",
|
|
182
|
+
backdropFilter: "blur(8px)",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
260
185
|
<button
|
|
261
186
|
type="button"
|
|
262
187
|
onClick={handleCancel}
|
|
263
188
|
disabled={busy}
|
|
264
|
-
|
|
189
|
+
style={{ color: "rgba(255,255,255,0.85)" }}
|
|
190
|
+
className="rounded-md px-3 py-1.5 text-sm transition-colors hover:bg-white/10 disabled:opacity-50"
|
|
265
191
|
>
|
|
266
192
|
Cancel
|
|
267
193
|
</button>
|
|
268
194
|
<div className="flex min-w-0 flex-col items-center text-center">
|
|
269
|
-
<span className="text-sm font-semibold">Crop
|
|
270
|
-
<span
|
|
195
|
+
<span className="text-sm font-semibold">Crop</span>
|
|
196
|
+
<span
|
|
197
|
+
style={{ color: "rgba(255,255,255,0.5)" }}
|
|
198
|
+
className="truncate max-w-xs text-[11px]"
|
|
199
|
+
>
|
|
271
200
|
{file.name}
|
|
272
201
|
</span>
|
|
273
202
|
</div>
|
|
@@ -275,19 +204,49 @@ export function CropDialog({
|
|
|
275
204
|
type="button"
|
|
276
205
|
onClick={handleApply}
|
|
277
206
|
disabled={busy || tooLarge}
|
|
278
|
-
// Inline color — host app'in Tailwind tema CSS değişkenleri
|
|
279
|
-
// `text-black` / `bg-white` class'larını override edebiliyor
|
|
280
|
-
// (Sentroy console gibi). Sabit hex ile kontrast garanti.
|
|
281
207
|
style={{ backgroundColor: "#fff", color: "#0a0a0a" }}
|
|
282
208
|
className="rounded-md px-3 py-1.5 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
283
209
|
>
|
|
284
|
-
{busy ? "Cropping…" : "
|
|
210
|
+
{busy ? "Cropping…" : "Done"}
|
|
285
211
|
</button>
|
|
212
|
+
</header>
|
|
213
|
+
|
|
214
|
+
{/* Main: cropper full bleed */}
|
|
215
|
+
<div className="relative flex-1 min-h-0 bg-black">
|
|
216
|
+
{tooLarge ? (
|
|
217
|
+
<div
|
|
218
|
+
style={{ color: "rgba(255,255,255,0.7)" }}
|
|
219
|
+
className="flex h-full w-full items-center justify-center p-6 text-center text-sm"
|
|
220
|
+
>
|
|
221
|
+
Image too large to crop in browser. Upload as-is or resize
|
|
222
|
+
beforehand.
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
<Cropper
|
|
226
|
+
ref={cropperRef}
|
|
227
|
+
src={imageUrl}
|
|
228
|
+
stencilProps={{
|
|
229
|
+
aspectRatio: aspectRatio,
|
|
230
|
+
}}
|
|
231
|
+
className="sentroy-mobile-cropper"
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
286
234
|
</div>
|
|
287
235
|
|
|
288
|
-
{/*
|
|
289
|
-
|
|
290
|
-
|
|
236
|
+
{/* Bottom toolbar — aspect chips (sol) + rotate (sağ).
|
|
237
|
+
iOS Photos pattern. */}
|
|
238
|
+
<footer
|
|
239
|
+
className="flex items-center gap-2 px-3 py-3"
|
|
240
|
+
style={{
|
|
241
|
+
borderTop: "1px solid rgba(255,255,255,0.08)",
|
|
242
|
+
background: "rgba(0,0,0,0.4)",
|
|
243
|
+
backdropFilter: "blur(8px)",
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<div
|
|
247
|
+
className="flex flex-1 items-center gap-1 overflow-x-auto"
|
|
248
|
+
style={{ scrollbarWidth: "none" }}
|
|
249
|
+
>
|
|
291
250
|
{ASPECT_PRESETS.map((p) => {
|
|
292
251
|
const active = aspectId === p.id
|
|
293
252
|
return (
|
|
@@ -298,155 +257,53 @@ export function CropDialog({
|
|
|
298
257
|
style={
|
|
299
258
|
active
|
|
300
259
|
? { backgroundColor: "#fff", color: "#0a0a0a" }
|
|
301
|
-
:
|
|
260
|
+
: { color: "rgba(255,255,255,0.7)" }
|
|
261
|
+
}
|
|
262
|
+
className={
|
|
263
|
+
"shrink-0 rounded-full px-3 py-1.5 text-xs transition-colors " +
|
|
264
|
+
(active ? "font-medium" : "hover:bg-white/10")
|
|
302
265
|
}
|
|
303
|
-
className={cls(
|
|
304
|
-
"rounded-full px-3 py-1 text-xs transition-colors",
|
|
305
|
-
active
|
|
306
|
-
? "font-medium"
|
|
307
|
-
: "text-white/60 hover:bg-white/10 hover:text-white",
|
|
308
|
-
)}
|
|
309
266
|
>
|
|
310
267
|
{p.label}
|
|
311
268
|
</button>
|
|
312
269
|
)
|
|
313
270
|
})}
|
|
314
271
|
</div>
|
|
315
|
-
<div
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
onClick={() => handleFlip("v")}
|
|
340
|
-
title="Flip vertical"
|
|
341
|
-
ariaLabel="Flip vertical"
|
|
342
|
-
>
|
|
343
|
-
<FlipVerticalIcon />
|
|
344
|
-
</ToolbarIconButton>
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* Main: cropper + side panel */}
|
|
349
|
-
<div className="flex flex-1 min-h-0 flex-col md:flex-row">
|
|
350
|
-
{/* Cropper stage */}
|
|
351
|
-
<div className="relative flex-1 min-h-[240px] min-w-0 bg-black">
|
|
352
|
-
{tooLarge ? (
|
|
353
|
-
<div className="flex h-full w-full items-center justify-center p-6 text-center text-sm text-white/70">
|
|
354
|
-
Image too large to crop in browser. Upload as-is or resize
|
|
355
|
-
beforehand.
|
|
356
|
-
</div>
|
|
357
|
-
) : (
|
|
358
|
-
<Cropper
|
|
359
|
-
ref={cropperRef}
|
|
360
|
-
src={imageUrl}
|
|
361
|
-
className="sentroy-cropper"
|
|
362
|
-
// Stencil props — aspect lock + iOS-like rect stencil grid
|
|
363
|
-
stencilProps={{
|
|
364
|
-
aspectRatio: aspectRatio,
|
|
365
|
-
grid: true,
|
|
366
|
-
movable: true,
|
|
367
|
-
resizable: true,
|
|
368
|
-
}}
|
|
369
|
-
// Background overlay'i koyu yap (image dışı kalan kısım)
|
|
370
|
-
backgroundClassName="sentroy-cropper-background"
|
|
371
|
-
onChange={handleCropperChange}
|
|
372
|
-
onReady={handleCropperReady}
|
|
373
|
-
/>
|
|
374
|
-
)}
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
|
-
{/* Side panel: live preview + readout. Mobile'da max-h ile
|
|
378
|
-
clamp + overflow-y-auto — uzun bilgilerle taşmasın. */}
|
|
379
|
-
<aside className="flex w-full shrink-0 flex-col gap-4 overflow-y-auto border-t border-white/10 bg-black/30 p-4 md:w-72 md:max-h-none md:border-l md:border-t-0 max-h-[42vh]">
|
|
380
|
-
<div className="flex items-center justify-between">
|
|
381
|
-
<span className="text-[10px] font-medium uppercase tracking-wider text-white/50">
|
|
382
|
-
Preview
|
|
383
|
-
</span>
|
|
384
|
-
{outputSize && (
|
|
385
|
-
<span className="font-mono text-[10px] text-white/40">
|
|
386
|
-
{outputSize.w}×{outputSize.h}
|
|
387
|
-
</span>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
<div className="flex min-h-[160px] items-center justify-center rounded-lg border border-white/10 bg-black/50 p-3">
|
|
391
|
-
{tooLarge ? (
|
|
392
|
-
<span className="text-[11px] text-white/50">
|
|
393
|
-
Preview unavailable
|
|
394
|
-
</span>
|
|
395
|
-
) : previewDataUrl ? (
|
|
396
|
-
/* eslint-disable-next-line @next/next/no-img-element */
|
|
397
|
-
<img
|
|
398
|
-
src={previewDataUrl}
|
|
399
|
-
alt="Crop preview"
|
|
400
|
-
className="max-h-[220px] max-w-full rounded-md object-contain shadow-md"
|
|
401
|
-
/>
|
|
402
|
-
) : (
|
|
403
|
-
<span className="text-[11px] text-white/40">
|
|
404
|
-
Adjust crop…
|
|
405
|
-
</span>
|
|
406
|
-
)}
|
|
407
|
-
</div>
|
|
408
|
-
<div className="flex flex-col gap-1.5 text-[11px] text-white/60">
|
|
409
|
-
<Stat
|
|
410
|
-
label="Aspect"
|
|
411
|
-
value={
|
|
412
|
-
ASPECT_PRESETS.find((p) => p.id === aspectId)?.label ??
|
|
413
|
-
"Free"
|
|
414
|
-
}
|
|
415
|
-
/>
|
|
416
|
-
{outputSize && (
|
|
417
|
-
<Stat
|
|
418
|
-
label="Output"
|
|
419
|
-
value={`${outputSize.w} × ${outputSize.h} px`}
|
|
420
|
-
/>
|
|
421
|
-
)}
|
|
422
|
-
<Stat
|
|
423
|
-
label="Format"
|
|
424
|
-
value={file.type === "image/png" ? "PNG" : "JPEG"}
|
|
425
|
-
/>
|
|
426
|
-
</div>
|
|
427
|
-
<button
|
|
428
|
-
type="button"
|
|
429
|
-
onClick={handleUseOriginal}
|
|
430
|
-
disabled={busy}
|
|
431
|
-
className="w-full rounded-md border border-white/20 px-3 py-1.5 text-xs text-white/70 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-50 md:mt-auto"
|
|
432
|
-
>
|
|
433
|
-
Use original (skip crop)
|
|
434
|
-
</button>
|
|
435
|
-
</aside>
|
|
436
|
-
</div>
|
|
272
|
+
<div
|
|
273
|
+
className="mx-2 hidden h-5 w-px md:block"
|
|
274
|
+
style={{ background: "rgba(255,255,255,0.15)" }}
|
|
275
|
+
/>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={handleRotate}
|
|
279
|
+
title="Rotate (R)"
|
|
280
|
+
aria-label="Rotate"
|
|
281
|
+
style={{ color: "rgba(255,255,255,0.85)" }}
|
|
282
|
+
className="inline-flex shrink-0 size-9 items-center justify-center rounded-full transition-colors hover:bg-white/10"
|
|
283
|
+
>
|
|
284
|
+
<RotateIcon />
|
|
285
|
+
</button>
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
onClick={handleUseOriginal}
|
|
289
|
+
disabled={busy}
|
|
290
|
+
style={{ color: "rgba(255,255,255,0.6)" }}
|
|
291
|
+
className="hidden shrink-0 rounded-md px-3 py-1.5 text-[11px] transition-colors hover:bg-white/10 disabled:opacity-50 sm:inline-flex"
|
|
292
|
+
>
|
|
293
|
+
Use original
|
|
294
|
+
</button>
|
|
295
|
+
</footer>
|
|
437
296
|
|
|
438
|
-
{/* Cropper container —
|
|
439
|
-
|
|
440
|
-
(
|
|
297
|
+
{/* Cropper container sizing — paketin kendi style.css'i çoğu
|
|
298
|
+
görseli sağlar; bizim sadece full-bleed boyutlama. Cropper'ın
|
|
299
|
+
ana rengini (stencil border + handler accent) `color`
|
|
300
|
+
property'si üzerinden geçir; root'a beyaz set ettik. */}
|
|
441
301
|
<style>{`
|
|
442
|
-
.sentroy-cropper {
|
|
302
|
+
.sentroy-mobile-cropper {
|
|
443
303
|
height: 100%;
|
|
444
304
|
width: 100%;
|
|
445
305
|
background: #000;
|
|
446
306
|
}
|
|
447
|
-
.sentroy-cropper-background {
|
|
448
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
449
|
-
}
|
|
450
307
|
`}</style>
|
|
451
308
|
</motion.div>
|
|
452
309
|
)}
|
|
@@ -454,44 +311,7 @@ export function CropDialog({
|
|
|
454
311
|
)
|
|
455
312
|
}
|
|
456
313
|
|
|
457
|
-
function
|
|
458
|
-
return (
|
|
459
|
-
<div className="flex items-center justify-between">
|
|
460
|
-
<span>{label}</span>
|
|
461
|
-
<span className="font-mono text-white/80">{value}</span>
|
|
462
|
-
</div>
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function ToolbarIconButton({
|
|
467
|
-
onClick,
|
|
468
|
-
title,
|
|
469
|
-
ariaLabel,
|
|
470
|
-
children,
|
|
471
|
-
}: {
|
|
472
|
-
onClick: () => void
|
|
473
|
-
title: string
|
|
474
|
-
ariaLabel: string
|
|
475
|
-
children: React.ReactNode
|
|
476
|
-
}) {
|
|
477
|
-
return (
|
|
478
|
-
<button
|
|
479
|
-
type="button"
|
|
480
|
-
onClick={onClick}
|
|
481
|
-
title={title}
|
|
482
|
-
aria-label={ariaLabel}
|
|
483
|
-
className="inline-flex size-8 items-center justify-center rounded-md text-white/70 transition-colors hover:bg-white/10 hover:text-white"
|
|
484
|
-
>
|
|
485
|
-
{children}
|
|
486
|
-
</button>
|
|
487
|
-
)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function cls(...arr: Array<string | false | null | undefined>): string {
|
|
491
|
-
return arr.filter(Boolean).join(" ")
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function RotateLeftIcon() {
|
|
314
|
+
function RotateIcon() {
|
|
495
315
|
return (
|
|
496
316
|
<svg
|
|
497
317
|
viewBox="0 0 24 24"
|
|
@@ -500,25 +320,7 @@ function RotateLeftIcon() {
|
|
|
500
320
|
strokeWidth="2"
|
|
501
321
|
strokeLinecap="round"
|
|
502
322
|
strokeLinejoin="round"
|
|
503
|
-
className="size-
|
|
504
|
-
aria-hidden="true"
|
|
505
|
-
>
|
|
506
|
-
<path d="M3 12a9 9 0 1 0 3-6.7" />
|
|
507
|
-
<path d="M3 4v5h5" />
|
|
508
|
-
</svg>
|
|
509
|
-
)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function RotateRightIcon() {
|
|
513
|
-
return (
|
|
514
|
-
<svg
|
|
515
|
-
viewBox="0 0 24 24"
|
|
516
|
-
fill="none"
|
|
517
|
-
stroke="currentColor"
|
|
518
|
-
strokeWidth="2"
|
|
519
|
-
strokeLinecap="round"
|
|
520
|
-
strokeLinejoin="round"
|
|
521
|
-
className="size-4"
|
|
323
|
+
className="size-5"
|
|
522
324
|
aria-hidden="true"
|
|
523
325
|
>
|
|
524
326
|
<path d="M21 12a9 9 0 1 1-3-6.7" />
|
|
@@ -526,41 +328,3 @@ function RotateRightIcon() {
|
|
|
526
328
|
</svg>
|
|
527
329
|
)
|
|
528
330
|
}
|
|
529
|
-
|
|
530
|
-
function FlipHorizontalIcon() {
|
|
531
|
-
return (
|
|
532
|
-
<svg
|
|
533
|
-
viewBox="0 0 24 24"
|
|
534
|
-
fill="none"
|
|
535
|
-
stroke="currentColor"
|
|
536
|
-
strokeWidth="2"
|
|
537
|
-
strokeLinecap="round"
|
|
538
|
-
strokeLinejoin="round"
|
|
539
|
-
className="size-4"
|
|
540
|
-
aria-hidden="true"
|
|
541
|
-
>
|
|
542
|
-
<path d="M12 3v18" />
|
|
543
|
-
<path d="M16 7l4 5-4 5" />
|
|
544
|
-
<path d="M8 7l-4 5 4 5" />
|
|
545
|
-
</svg>
|
|
546
|
-
)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function FlipVerticalIcon() {
|
|
550
|
-
return (
|
|
551
|
-
<svg
|
|
552
|
-
viewBox="0 0 24 24"
|
|
553
|
-
fill="none"
|
|
554
|
-
stroke="currentColor"
|
|
555
|
-
strokeWidth="2"
|
|
556
|
-
strokeLinecap="round"
|
|
557
|
-
strokeLinejoin="round"
|
|
558
|
-
className="size-4"
|
|
559
|
-
aria-hidden="true"
|
|
560
|
-
>
|
|
561
|
-
<path d="M3 12h18" />
|
|
562
|
-
<path d="M7 8l5-4 5 4" />
|
|
563
|
-
<path d="M7 16l5 4 5-4" />
|
|
564
|
-
</svg>
|
|
565
|
-
)
|
|
566
|
-
}
|