@sentroy-co/client-sdk 2.13.3 → 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.
@@ -1,30 +1,21 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react"
2
- import { Cropper, type CropperRef } from "react-advanced-cropper"
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. Önceki
7
- * `react-easy-crop` implementation'ı drag UX ve preview render
8
- * tarafında zayıftı; `react-advanced-cropper` daha modern stencil
9
- * sistemi + native-feel pinch/zoom verir.
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
- * **CSS:** Caller uygulamada `react-advanced-cropper/dist/style.css`'i
12
- * global olarak import edilmiş olmalıdır (örn. `globals.css` üzerinden veya
13
- * root layout). SDK içinden CSS import etsek `tsc`'nin CJS çıktısında
14
- * runtime `require("...css")` doğar, Next.js bunu bundle'a alamaz ve
15
- * "Module factory is not available" hatasıyla patlar.
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
- * Akış (storage upload pipeline'ından preprocess hook):
18
- * - Aspect preset toolbar (1:1, 4:3, 16:9, 3:2, 9:16, Free)
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 — üstü tarayıcı memory peak'i riskli
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). Convert sonucu daima image/jpeg
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,86 +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 cropper coordinates'ini yeni aspect'e göre
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
- if (!cropperRef.current) return
115
- if (aspectRatio === undefined) return
116
- const state = cropperRef.current.getState()
117
- if (!state) return
118
- const { coordinates } = state
119
- if (!coordinates) return
120
- const current = coordinates.width / coordinates.height
121
- if (Math.abs(current - aspectRatio) < 0.001) return
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
- const canvas = cropper.getCanvas({
142
- // Preview canvas — düşük boyut, smooth (output quality değil)
143
- maxWidth: PREVIEW_MAX_DIM * 2,
144
- maxHeight: PREVIEW_MAX_DIM * 2,
145
- imageSmoothingQuality: "medium",
146
- })
147
- if (!canvas) return
148
- setOutputSize({ w: canvas.width, h: canvas.height })
149
- // Data URL — küçük canvas; performant
150
- try {
151
- setPreviewDataUrl(canvas.toDataURL("image/jpeg", 0.7))
152
- } catch {
153
- // toDataURL nadir tainted-canvas durumunda fail edebilir; sessiz geç
154
- }
155
- })
156
- }, [])
157
-
158
- const handleCropperChange = useCallback(() => {
159
- renderPreview()
160
- }, [renderPreview])
161
-
162
- const handleCropperReady = useCallback(() => {
163
- renderPreview()
164
- }, [renderPreview])
165
-
166
97
  const handleApply = useCallback(async () => {
167
98
  const cropper = cropperRef.current
168
99
  if (!cropper) return
169
100
  setBusy(true)
170
101
  try {
171
- const canvas = cropper.getCanvas({
172
- imageSmoothingQuality: "high",
173
- })
102
+ const canvas = cropper.getCanvas({ imageSmoothingQuality: "high" })
174
103
  if (!canvas) {
175
104
  setBusy(false)
176
105
  return
177
106
  }
178
- const outputMime = file.type === "image/png" ? "image/png" : "image/jpeg"
107
+ const outputMime =
108
+ file.type === "image/png" ? "image/png" : "image/jpeg"
179
109
  const blob = await new Promise<Blob | null>((resolve) => {
180
110
  canvas.toBlob(
181
111
  (b) => resolve(b),
@@ -200,14 +130,11 @@ export function CropDialog({
200
130
 
201
131
  const handleUseOriginal = useCallback(() => onClose(file), [file, onClose])
202
132
  const handleCancel = useCallback(() => onClose(null), [onClose])
203
- const handleRotate = useCallback((delta: 90 | -90) => {
204
- cropperRef.current?.rotateImage(delta)
205
- }, [])
206
- const handleFlip = useCallback((axis: "h" | "v") => {
207
- cropperRef.current?.flipImage(axis === "h", axis === "v")
133
+ const handleRotate = useCallback(() => {
134
+ cropperRef.current?.rotateImage(90)
208
135
  }, [])
209
136
 
210
- // Keyboard shortcuts — ESC kapat, R rotate, F flip
137
+ // ESC kapat, R rotate
211
138
  useEffect(() => {
212
139
  if (!open) return
213
140
  const onKey = (e: KeyboardEvent) => {
@@ -224,7 +151,7 @@ export function CropDialog({
224
151
  }
225
152
  if (e.key === "r" || e.key === "R") {
226
153
  e.preventDefault()
227
- handleRotate(e.shiftKey ? -90 : 90)
154
+ handleRotate()
228
155
  }
229
156
  }
230
157
  window.addEventListener("keydown", onKey)
@@ -240,22 +167,36 @@ export function CropDialog({
240
167
  animate={{ opacity: 1 }}
241
168
  exit={{ opacity: 0 }}
242
169
  transition={{ duration: 0.2 }}
243
- // Tam ekranscrim değil, kendisi background; iOS Photos UX
244
- className="fixed inset-0 z-[60] flex flex-col bg-black text-white"
170
+ // Inline colorhost 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"
245
175
  >
246
- {/* Header */}
247
- <div className="flex items-center justify-between gap-3 border-b border-white/10 bg-black/40 px-4 py-3 backdrop-blur-sm">
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
+ >
248
185
  <button
249
186
  type="button"
250
187
  onClick={handleCancel}
251
188
  disabled={busy}
252
- className="rounded-md px-3 py-1.5 text-sm text-white/70 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-50"
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"
253
191
  >
254
192
  Cancel
255
193
  </button>
256
194
  <div className="flex min-w-0 flex-col items-center text-center">
257
- <span className="text-sm font-semibold">Crop image</span>
258
- <span className="truncate max-w-xs text-[11px] text-white/50">
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
+ >
259
200
  {file.name}
260
201
  </span>
261
202
  </div>
@@ -263,188 +204,106 @@ export function CropDialog({
263
204
  type="button"
264
205
  onClick={handleApply}
265
206
  disabled={busy || tooLarge}
266
- className="rounded-md bg-white px-3 py-1.5 text-sm font-medium text-black transition-opacity hover:opacity-90 disabled:opacity-50"
207
+ style={{ backgroundColor: "#fff", color: "#0a0a0a" }}
208
+ className="rounded-md px-3 py-1.5 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
267
209
  >
268
- {busy ? "Cropping…" : "Apply"}
210
+ {busy ? "Cropping…" : "Done"}
269
211
  </button>
270
- </div>
212
+ </header>
271
213
 
272
- {/* Aspect + rotate toolbar */}
273
- <div className="flex flex-wrap items-center gap-2 border-b border-white/10 bg-black/30 px-3 py-2">
274
- <div className="flex flex-1 flex-wrap items-center gap-1">
275
- {ASPECT_PRESETS.map((p) => (
276
- <button
277
- key={p.id}
278
- type="button"
279
- onClick={() => setAspectId(p.id)}
280
- className={cls(
281
- "rounded-full px-3 py-1 text-xs transition-colors",
282
- aspectId === p.id
283
- ? "bg-white text-black"
284
- : "text-white/60 hover:bg-white/10 hover:text-white",
285
- )}
286
- >
287
- {p.label}
288
- </button>
289
- ))}
290
- </div>
291
- <div className="flex items-center gap-1">
292
- <ToolbarIconButton
293
- onClick={() => handleRotate(-90)}
294
- title="Rotate left (Shift+R)"
295
- ariaLabel="Rotate left"
296
- >
297
- <RotateLeftIcon />
298
- </ToolbarIconButton>
299
- <ToolbarIconButton
300
- onClick={() => handleRotate(90)}
301
- title="Rotate right (R)"
302
- ariaLabel="Rotate right"
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"
303
220
  >
304
- <RotateRightIcon />
305
- </ToolbarIconButton>
306
- <span className="mx-1 h-5 w-px bg-white/15" />
307
- <ToolbarIconButton
308
- onClick={() => handleFlip("h")}
309
- title="Flip horizontal"
310
- ariaLabel="Flip horizontal"
311
- >
312
- <FlipHorizontalIcon />
313
- </ToolbarIconButton>
314
- <ToolbarIconButton
315
- onClick={() => handleFlip("v")}
316
- title="Flip vertical"
317
- ariaLabel="Flip vertical"
318
- >
319
- <FlipVerticalIcon />
320
- </ToolbarIconButton>
321
- </div>
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
+ )}
322
234
  </div>
323
235
 
324
- {/* Main: cropper + side panel */}
325
- <div className="flex flex-1 min-h-0 flex-col md:flex-row">
326
- {/* Cropper stage */}
327
- <div className="relative flex-1 bg-black min-h-[240px]">
328
- {tooLarge ? (
329
- <div className="flex h-full w-full items-center justify-center p-6 text-center text-sm text-white/70">
330
- Image too large to crop in browser. Upload as-is or resize
331
- beforehand.
332
- </div>
333
- ) : (
334
- <Cropper
335
- ref={cropperRef}
336
- src={imageUrl}
337
- className="sentroy-cropper"
338
- // Stencil props — aspect lock + iOS-like rect stencil grid
339
- stencilProps={{
340
- aspectRatio: aspectRatio,
341
- grid: true,
342
- movable: true,
343
- resizable: true,
344
- }}
345
- // Background overlay'i koyu yap (image dışı kalan kısım)
346
- backgroundClassName="sentroy-cropper-background"
347
- onChange={handleCropperChange}
348
- onReady={handleCropperReady}
349
- />
350
- )}
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
+ >
250
+ {ASPECT_PRESETS.map((p) => {
251
+ const active = aspectId === p.id
252
+ return (
253
+ <button
254
+ key={p.id}
255
+ type="button"
256
+ onClick={() => setAspectId(p.id)}
257
+ style={
258
+ active
259
+ ? { backgroundColor: "#fff", color: "#0a0a0a" }
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")
265
+ }
266
+ >
267
+ {p.label}
268
+ </button>
269
+ )
270
+ })}
351
271
  </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>
352
296
 
353
- {/* Side panel: live preview + readout */}
354
- <aside className="flex w-full shrink-0 flex-col gap-4 border-t border-white/10 bg-black/30 p-4 md:w-72 md:border-l md:border-t-0">
355
- <div className="flex items-center justify-between">
356
- <span className="text-[10px] font-medium uppercase tracking-wider text-white/50">
357
- Preview
358
- </span>
359
- {outputSize && (
360
- <span className="font-mono text-[10px] text-white/40">
361
- {outputSize.w}×{outputSize.h}
362
- </span>
363
- )}
364
- </div>
365
- <div className="flex min-h-[160px] items-center justify-center rounded-lg border border-white/10 bg-black/50 p-3">
366
- {tooLarge ? (
367
- <span className="text-[11px] text-white/50">
368
- Preview unavailable
369
- </span>
370
- ) : previewDataUrl ? (
371
- /* eslint-disable-next-line @next/next/no-img-element */
372
- <img
373
- src={previewDataUrl}
374
- alt="Crop preview"
375
- className="max-h-[240px] max-w-full rounded-md object-contain shadow-md"
376
- />
377
- ) : (
378
- <span className="text-[11px] text-white/40">
379
- Adjust crop…
380
- </span>
381
- )}
382
- </div>
383
- <div className="flex flex-col gap-1.5 text-[11px] text-white/60">
384
- <Stat
385
- label="Aspect"
386
- value={
387
- ASPECT_PRESETS.find((p) => p.id === aspectId)?.label ??
388
- "Free"
389
- }
390
- />
391
- {outputSize && (
392
- <Stat
393
- label="Output"
394
- value={`${outputSize.w} × ${outputSize.h} px`}
395
- />
396
- )}
397
- <Stat
398
- label="Format"
399
- value={file.type === "image/png" ? "PNG" : "JPEG"}
400
- />
401
- </div>
402
- <button
403
- type="button"
404
- onClick={handleUseOriginal}
405
- disabled={busy}
406
- className="mt-auto 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"
407
- >
408
- Use original (skip crop)
409
- </button>
410
- </aside>
411
- </div>
412
-
413
- {/* Local cropper styles — paket default'unu Sentroy paletine
414
- align ediyoruz. Stencil border ve grid çizgisini ince-keskin
415
- tutuyoruz (iOS Photos benzeri). */}
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. */}
416
301
  <style>{`
417
- .sentroy-cropper {
302
+ .sentroy-mobile-cropper {
418
303
  height: 100%;
419
304
  width: 100%;
420
305
  background: #000;
421
306
  }
422
- .sentroy-cropper-background {
423
- background-color: rgba(0, 0, 0, 0.7);
424
- }
425
- .sentroy-cropper .advanced-cropper-stencil-overlay {
426
- background: rgba(0, 0, 0, 0.55);
427
- }
428
- .sentroy-cropper .advanced-cropper-rectangle-stencil__draggable-area {
429
- border: 1px solid rgba(255, 255, 255, 0.95);
430
- }
431
- .sentroy-cropper .advanced-cropper-line-wrapper {
432
- background: rgba(255, 255, 255, 0.95);
433
- }
434
- .sentroy-cropper .advanced-cropper-handler-wrapper--west-north,
435
- .sentroy-cropper .advanced-cropper-handler-wrapper--north-east,
436
- .sentroy-cropper .advanced-cropper-handler-wrapper--east-south,
437
- .sentroy-cropper .advanced-cropper-handler-wrapper--south-west {
438
- width: 22px;
439
- height: 22px;
440
- }
441
- .sentroy-cropper .advanced-cropper-handler-wrapper__draggable {
442
- background: #fff;
443
- border: 2px solid #000;
444
- width: 12px;
445
- height: 12px;
446
- border-radius: 2px;
447
- }
448
307
  `}</style>
449
308
  </motion.div>
450
309
  )}
@@ -452,44 +311,7 @@ export function CropDialog({
452
311
  )
453
312
  }
454
313
 
455
- function Stat({ label, value }: { label: string; value: string }) {
456
- return (
457
- <div className="flex items-center justify-between">
458
- <span>{label}</span>
459
- <span className="font-mono text-white/80">{value}</span>
460
- </div>
461
- )
462
- }
463
-
464
- function ToolbarIconButton({
465
- onClick,
466
- title,
467
- ariaLabel,
468
- children,
469
- }: {
470
- onClick: () => void
471
- title: string
472
- ariaLabel: string
473
- children: React.ReactNode
474
- }) {
475
- return (
476
- <button
477
- type="button"
478
- onClick={onClick}
479
- title={title}
480
- aria-label={ariaLabel}
481
- className="inline-flex size-8 items-center justify-center rounded-md text-white/70 transition-colors hover:bg-white/10 hover:text-white"
482
- >
483
- {children}
484
- </button>
485
- )
486
- }
487
-
488
- function cls(...arr: Array<string | false | null | undefined>): string {
489
- return arr.filter(Boolean).join(" ")
490
- }
491
-
492
- function RotateLeftIcon() {
314
+ function RotateIcon() {
493
315
  return (
494
316
  <svg
495
317
  viewBox="0 0 24 24"
@@ -498,25 +320,7 @@ function RotateLeftIcon() {
498
320
  strokeWidth="2"
499
321
  strokeLinecap="round"
500
322
  strokeLinejoin="round"
501
- className="size-4"
502
- aria-hidden="true"
503
- >
504
- <path d="M3 12a9 9 0 1 0 3-6.7" />
505
- <path d="M3 4v5h5" />
506
- </svg>
507
- )
508
- }
509
-
510
- function RotateRightIcon() {
511
- return (
512
- <svg
513
- viewBox="0 0 24 24"
514
- fill="none"
515
- stroke="currentColor"
516
- strokeWidth="2"
517
- strokeLinecap="round"
518
- strokeLinejoin="round"
519
- className="size-4"
323
+ className="size-5"
520
324
  aria-hidden="true"
521
325
  >
522
326
  <path d="M21 12a9 9 0 1 1-3-6.7" />
@@ -524,41 +328,3 @@ function RotateRightIcon() {
524
328
  </svg>
525
329
  )
526
330
  }
527
-
528
- function FlipHorizontalIcon() {
529
- return (
530
- <svg
531
- viewBox="0 0 24 24"
532
- fill="none"
533
- stroke="currentColor"
534
- strokeWidth="2"
535
- strokeLinecap="round"
536
- strokeLinejoin="round"
537
- className="size-4"
538
- aria-hidden="true"
539
- >
540
- <path d="M12 3v18" />
541
- <path d="M16 7l4 5-4 5" />
542
- <path d="M8 7l-4 5 4 5" />
543
- </svg>
544
- )
545
- }
546
-
547
- function FlipVerticalIcon() {
548
- return (
549
- <svg
550
- viewBox="0 0 24 24"
551
- fill="none"
552
- stroke="currentColor"
553
- strokeWidth="2"
554
- strokeLinecap="round"
555
- strokeLinejoin="round"
556
- className="size-4"
557
- aria-hidden="true"
558
- >
559
- <path d="M3 12h18" />
560
- <path d="M7 8l5-4 5 4" />
561
- <path d="M7 16l5 4 5-4" />
562
- </svg>
563
- )
564
- }