@chrono-os/image-editor-react 0.2.0
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 +424 -0
- package/dist/badge-BRjFgV8X.d.cts +130 -0
- package/dist/badge-BRjFgV8X.d.ts +130 -0
- package/dist/badge.cjs +56 -0
- package/dist/badge.cjs.map +1 -0
- package/dist/badge.d.cts +3 -0
- package/dist/badge.d.ts +3 -0
- package/dist/badge.js +4 -0
- package/dist/badge.js.map +1 -0
- package/dist/chunk-P77DHS6Z.js +53 -0
- package/dist/chunk-P77DHS6Z.js.map +1 -0
- package/dist/index.cjs +1989 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +163 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +1928 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.css +30 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @chrono-os/image-editor-react
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Editor de imagens com crop visual + preview de badge. Inclui `<ImagePicker>` controlado (preview + modal de edição com crop draggable, snap nas bordas, zoom, filtros saturação/sépia, cor de fundo, espelhar), `<ImageCropPicker>` standalone (sem o wrapper), `<BadgeOverlay>` reutilizável e os editores de badge `<BadgeEditorCard>` / `<BadgePartControls>` / `<SliderRow>` (admin).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
yarn add @chrono-os/image-editor-react
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Publicado em `registry.npmjs.org` público — sem auth pra consumir.
|
|
15
|
+
|
|
16
|
+
Peer dependencies: `react@>=18` `react-dom@>=18`.
|
|
17
|
+
|
|
18
|
+
## Setup tema (recomendado)
|
|
19
|
+
|
|
20
|
+
Importar o CSS de tokens no layout root (Next.js App Router):
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// app/layout.tsx
|
|
24
|
+
import '@chrono-os/image-editor-react/theme.css'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
E sobrescrever as CSS vars no `globals.css` do consumer (ou em qualquer ancestor com classe `.image-editor-root`):
|
|
28
|
+
|
|
29
|
+
```css
|
|
30
|
+
:root {
|
|
31
|
+
--ie-accent: #C9A961; /* default electric blue #556FFF */
|
|
32
|
+
--ie-accent-hover: #A88842;
|
|
33
|
+
--ie-border: #d4d4d8;
|
|
34
|
+
--ie-bg: #fafafa;
|
|
35
|
+
--ie-text: #404040;
|
|
36
|
+
--ie-text-muted: #737373;
|
|
37
|
+
--ie-danger: #dc2626;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Os componentes usam `text-[color:var(--ie-accent,#556FFF)]`/`bg-[color:var(--ie-accent,#556FFF)]` (Tailwind arbitrary values), então qualquer wrapper aplicando essas vars é suficiente.
|
|
42
|
+
|
|
43
|
+
## Setup Tailwind (opcional)
|
|
44
|
+
|
|
45
|
+
Funciona com Tailwind 3+ sem config extra — as classes usadas são todas core utilities + arbitrary values. Se o consumer tiver `purge`/`content` configurado, garantir que o `node_modules/@chrono-os/image-editor-react/dist/**/*.{js,cjs}` está incluído:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// tailwind.config.ts
|
|
49
|
+
export default {
|
|
50
|
+
content: [
|
|
51
|
+
'./app/**/*.{ts,tsx}',
|
|
52
|
+
'./node_modules/@chrono-os/image-editor-react/dist/**/*.{js,cjs}',
|
|
53
|
+
],
|
|
54
|
+
// ...
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Exemplo: `<ImagePicker>` completo
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
'use client'
|
|
62
|
+
|
|
63
|
+
import { useState } from 'react'
|
|
64
|
+
import {
|
|
65
|
+
ImagePicker,
|
|
66
|
+
type UploadAdapter,
|
|
67
|
+
type CropValue,
|
|
68
|
+
type BadgeData,
|
|
69
|
+
type ImagePreset,
|
|
70
|
+
} from '@chrono-os/image-editor-react'
|
|
71
|
+
import '@chrono-os/image-editor-react/theme.css'
|
|
72
|
+
|
|
73
|
+
const uploadAdapter: UploadAdapter = {
|
|
74
|
+
list: async () => {
|
|
75
|
+
const r = await fetch('/api/admin/uploads', { credentials: 'include' })
|
|
76
|
+
if (!r.ok) throw new Error('Falha ao listar')
|
|
77
|
+
const data = await r.json()
|
|
78
|
+
return data.items
|
|
79
|
+
},
|
|
80
|
+
upload: async (file) => {
|
|
81
|
+
const form = new FormData()
|
|
82
|
+
form.append('file', file, file.name)
|
|
83
|
+
const r = await fetch('/api/admin/uploads', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: form,
|
|
86
|
+
credentials: 'include',
|
|
87
|
+
})
|
|
88
|
+
if (!r.ok) throw new Error('Upload falhou')
|
|
89
|
+
return r.json()
|
|
90
|
+
},
|
|
91
|
+
delete: async (filename) => {
|
|
92
|
+
const r = await fetch(
|
|
93
|
+
`/api/admin/uploads/${encodeURIComponent(filename)}`,
|
|
94
|
+
{ method: 'DELETE', credentials: 'include' },
|
|
95
|
+
)
|
|
96
|
+
return r.ok
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const presets: ImagePreset[] = [
|
|
101
|
+
{ label: 'Hero Naírio', url: '/branding/hero-nairio.webp' },
|
|
102
|
+
{ label: 'Background ABS', url: '/branding/abs-pattern.webp' },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
export function HeroImageField() {
|
|
106
|
+
const [url, setUrl] = useState('')
|
|
107
|
+
const [crop, setCrop] = useState<CropValue>({})
|
|
108
|
+
const badge: BadgeData = {
|
|
109
|
+
numero: '201',
|
|
110
|
+
label: 'advogado',
|
|
111
|
+
numeroEnabled: true,
|
|
112
|
+
labelEnabled: true,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<ImagePicker
|
|
117
|
+
label="Foto do hero"
|
|
118
|
+
hint="Recomendado: 1200×1600px (3:4)"
|
|
119
|
+
value={url}
|
|
120
|
+
onChange={(next) => setUrl(next)}
|
|
121
|
+
aspect="3/4"
|
|
122
|
+
crop={crop}
|
|
123
|
+
onCropChange={setCrop}
|
|
124
|
+
badge={badge}
|
|
125
|
+
uploadAdapter={uploadAdapter}
|
|
126
|
+
presets={presets}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Exemplo: `<BadgeOverlay>` standalone
|
|
133
|
+
|
|
134
|
+
Pra renderizar só o badge sobreposto (sem o picker — útil em previews de listagem, cards, etc):
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { BadgeOverlay } from '@chrono-os/image-editor-react/badge'
|
|
138
|
+
|
|
139
|
+
export function HeroCard({ imageUrl }: { imageUrl: string }) {
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className="relative aspect-[3/4] overflow-hidden rounded-lg"
|
|
143
|
+
style={{ containerType: 'inline-size' }}
|
|
144
|
+
>
|
|
145
|
+
<img src={imageUrl} alt="" className="h-full w-full object-cover" />
|
|
146
|
+
<BadgeOverlay
|
|
147
|
+
badge={{
|
|
148
|
+
numero: '201',
|
|
149
|
+
label: 'advogado',
|
|
150
|
+
numeroEnabled: true,
|
|
151
|
+
labelEnabled: true,
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> **Importante:** o pai do `<BadgeOverlay>` precisa de `containerType: 'inline-size'` no CSS pra que os `cqw` (container query width) funcionem proporcionalmente em qualquer tamanho.
|
|
160
|
+
|
|
161
|
+
## Exemplo: `<BadgeEditorCard>` no `extraCards`
|
|
162
|
+
|
|
163
|
+
Renderiza um subcard completo de edição de parte do badge (título + checkbox "Exibir" + texto + cor + sliders). Encaixa direto no `extraCards` do `<ImagePicker>`:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
'use client'
|
|
167
|
+
|
|
168
|
+
import {
|
|
169
|
+
ImagePicker,
|
|
170
|
+
BadgeEditorCard,
|
|
171
|
+
type BadgePartState,
|
|
172
|
+
type RecentColorsAdapter,
|
|
173
|
+
} from '@chrono-os/image-editor-react'
|
|
174
|
+
|
|
175
|
+
// Adapter opcional pra integrar com sistema de cores recentes do consumer.
|
|
176
|
+
const recentColors: RecentColorsAdapter = {
|
|
177
|
+
colors: { text: ['#13203B', '#C9A961', '#FFFFFF'] },
|
|
178
|
+
addColor: (hex, kind) => {
|
|
179
|
+
// persistir no DB do consumer
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function HeroBadgeFields({
|
|
184
|
+
badge,
|
|
185
|
+
onBadgeChange,
|
|
186
|
+
}: {
|
|
187
|
+
badge: { numero?: BadgePartState; label?: BadgePartState }
|
|
188
|
+
onBadgeChange: (next: typeof badge) => void
|
|
189
|
+
}) {
|
|
190
|
+
return (
|
|
191
|
+
<ImagePicker
|
|
192
|
+
// ...props do ImagePicker
|
|
193
|
+
value="/img/hero.webp"
|
|
194
|
+
onChange={() => {}}
|
|
195
|
+
uploadAdapter={uploadAdapter}
|
|
196
|
+
extraCards={[
|
|
197
|
+
<BadgeEditorCard
|
|
198
|
+
key="numero"
|
|
199
|
+
title="Badge — Número"
|
|
200
|
+
enabledLabel="Exibir número"
|
|
201
|
+
value={badge.numero ?? {}}
|
|
202
|
+
onChange={(next) => onBadgeChange({ ...badge, numero: next })}
|
|
203
|
+
defaultColor="#C9A961"
|
|
204
|
+
defaultPosX={6}
|
|
205
|
+
defaultPosY={84}
|
|
206
|
+
recentColors={recentColors}
|
|
207
|
+
/>,
|
|
208
|
+
<BadgeEditorCard
|
|
209
|
+
key="label"
|
|
210
|
+
title="Badge — Label"
|
|
211
|
+
enabledLabel="Exibir label"
|
|
212
|
+
value={badge.label ?? {}}
|
|
213
|
+
onChange={(next) => onBadgeChange({ ...badge, label: next })}
|
|
214
|
+
defaultColor="#FFFFFF"
|
|
215
|
+
defaultPosX={36}
|
|
216
|
+
defaultPosY={88}
|
|
217
|
+
recentColors={recentColors}
|
|
218
|
+
/>,
|
|
219
|
+
]}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A interface `BadgePartState` é:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
export interface BadgePartState {
|
|
229
|
+
enabled?: boolean // undefined === true (exibe), false === oculto
|
|
230
|
+
text?: string
|
|
231
|
+
color?: string // #RRGGBB
|
|
232
|
+
size?: number // % do default (50-500)
|
|
233
|
+
offsetX?: number // % horizontal (0-100)
|
|
234
|
+
offsetY?: number // % vertical (0-100)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Exemplo: `<BadgePartControls>` standalone
|
|
239
|
+
|
|
240
|
+
Se você já tem o subcard externo (título + checkbox + texto) e quer só os controles compactos de cor + sliders:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
'use client'
|
|
244
|
+
|
|
245
|
+
import { BadgePartControls } from '@chrono-os/image-editor-react'
|
|
246
|
+
|
|
247
|
+
export function MyBadgeControls({ partState, onPatch }) {
|
|
248
|
+
return (
|
|
249
|
+
<BadgePartControls
|
|
250
|
+
color={partState.color}
|
|
251
|
+
defaultColor="#C9A961"
|
|
252
|
+
size={partState.size}
|
|
253
|
+
offsetX={partState.offsetX}
|
|
254
|
+
offsetY={partState.offsetY}
|
|
255
|
+
defaultPosX={6}
|
|
256
|
+
defaultPosY={84}
|
|
257
|
+
onColorChange={(v) => onPatch({ color: v })}
|
|
258
|
+
onSizeChange={(v) => onPatch({ size: v })}
|
|
259
|
+
onOffsetXChange={(v) => onPatch({ offsetX: v })}
|
|
260
|
+
onOffsetYChange={(v) => onPatch({ offsetY: v })}
|
|
261
|
+
/>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Exemplo: `<ImageCropPicker>` standalone
|
|
267
|
+
|
|
268
|
+
Pra quem só quer o editor de crop sem o wrapper de upload/preview:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
'use client'
|
|
272
|
+
|
|
273
|
+
import { useState } from 'react'
|
|
274
|
+
import { ImageCropPicker, type CropValue } from '@chrono-os/image-editor-react'
|
|
275
|
+
|
|
276
|
+
export function CropOnly({ imageUrl }: { imageUrl: string }) {
|
|
277
|
+
const [crop, setCrop] = useState<CropValue>({})
|
|
278
|
+
return (
|
|
279
|
+
<ImageCropPicker
|
|
280
|
+
imageUrl={imageUrl}
|
|
281
|
+
targetAspect={3 / 4}
|
|
282
|
+
value={crop}
|
|
283
|
+
onChange={setCrop}
|
|
284
|
+
maxHeight={520}
|
|
285
|
+
/>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Props do `<ImagePicker>`
|
|
291
|
+
|
|
292
|
+
| Prop | Tipo | Default | Descrição |
|
|
293
|
+
|---|---|---|---|
|
|
294
|
+
| `value` | `string` | — | URL atual da imagem (controlado). |
|
|
295
|
+
| `onChange` | `(url: string, aspectRatio?: number) => void` | — | Chamado quando o consumer escolhe nova imagem. |
|
|
296
|
+
| `uploadAdapter` | `UploadAdapter` | — | Adapter contra o backend (list/upload/delete/resolveUrl). |
|
|
297
|
+
| `label` | `string` | — | Rótulo do field acima do preview. |
|
|
298
|
+
| `hint` | `string` | — | Texto de ajuda abaixo do label. |
|
|
299
|
+
| `aspect` | `'1/1'\|'3/4'\|'16/9'\|'4/3'` | `'3/4'` | Aspect ratio do frame de destino. |
|
|
300
|
+
| `crop` | `CropValue` | `{}` | Valor de crop (position/scale/mirror/filters/backdrop). |
|
|
301
|
+
| `onCropChange` | `(crop: CropValue) => void` | — | Chamado quando o usuário ajusta o crop no modal. |
|
|
302
|
+
| `badge` | `BadgeData` | — | Badge sobreposto na preview (número + label). |
|
|
303
|
+
| `extraCards` | `ReactNode[]` | — | Cards extras ao lado direito do preview (slots de UI custom). |
|
|
304
|
+
| `presets` | `ImagePreset[]` | — | Atalhos de imagem curados (mostrados na aba "Imagens"). |
|
|
305
|
+
|
|
306
|
+
## Interface `UploadAdapter`
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
export interface UploadAdapter {
|
|
310
|
+
/** Lista uploads existentes (ordem decrescente por createdAt no consumer). */
|
|
311
|
+
list: () => Promise<UploadItem[]>
|
|
312
|
+
/** Sobe um arquivo e devolve o item criado. */
|
|
313
|
+
upload: (file: File) => Promise<UploadItem>
|
|
314
|
+
/** Remove um upload por filename. Retorna true em sucesso. */
|
|
315
|
+
delete: (filename: string) => Promise<boolean>
|
|
316
|
+
/**
|
|
317
|
+
* Resolve uma URL armazenada (relativa ou absoluta) pra URL absoluta usada
|
|
318
|
+
* no <img src>. Default = identity. Útil quando backend serve /uploads/*
|
|
319
|
+
* em domínio diferente sem rewrite.
|
|
320
|
+
*/
|
|
321
|
+
resolveUrl?: (url: string) => string
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface UploadItem {
|
|
325
|
+
filename: string
|
|
326
|
+
url: string
|
|
327
|
+
sizeBytes: number
|
|
328
|
+
createdAt?: string
|
|
329
|
+
width?: number
|
|
330
|
+
height?: number
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Interface `CropValue`
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
export interface CropValue {
|
|
338
|
+
/** CSS object-position style: "50% 50%" ou "center top". */
|
|
339
|
+
position?: string
|
|
340
|
+
/** Zoom. 1 = cover normal, >1 = zoom in, <1 = zoom out (mostra backdrop). */
|
|
341
|
+
scale?: number
|
|
342
|
+
mirror?: boolean
|
|
343
|
+
grayscale?: number
|
|
344
|
+
saturation?: number
|
|
345
|
+
sepia?: number
|
|
346
|
+
sepiaColor?: string
|
|
347
|
+
/** Cor de fundo do container (#RRGGBB ou 'transparent'). */
|
|
348
|
+
backdropColor?: string
|
|
349
|
+
/** Opacidade da cor de backdrop 0-100. */
|
|
350
|
+
backdropOpacity?: number
|
|
351
|
+
/** Translate offset adicional (frame fora da imagem). */
|
|
352
|
+
overflowX?: number
|
|
353
|
+
overflowY?: number
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Interface `BadgeData`
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
export interface BadgeData {
|
|
361
|
+
numero?: string
|
|
362
|
+
label?: string
|
|
363
|
+
numeroEnabled?: boolean
|
|
364
|
+
labelEnabled?: boolean
|
|
365
|
+
numeroColor?: string
|
|
366
|
+
numeroSize?: number // 50-500% (default 100)
|
|
367
|
+
numeroOffsetX?: number // %, default 6
|
|
368
|
+
numeroOffsetY?: number // %, default 84
|
|
369
|
+
labelColor?: string
|
|
370
|
+
labelSize?: number // 50-500% (default 100)
|
|
371
|
+
labelOffsetX?: number // %, default 36
|
|
372
|
+
labelOffsetY?: number // %, default 88
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Props do `<BadgeEditorCard>`
|
|
377
|
+
|
|
378
|
+
| Prop | Tipo | Default | Descrição |
|
|
379
|
+
|---|---|---|---|
|
|
380
|
+
| `title` | `string` | — | Título do subcard (ex `"Badge — Número"`). |
|
|
381
|
+
| `value` | `BadgePartState` | — | Estado controlado (enabled/text/color/size/offsetX/offsetY). |
|
|
382
|
+
| `onChange` | `(next: BadgePartState) => void` | — | Recebe o estado completo a cada edição. |
|
|
383
|
+
| `defaultColor` | `string` | — | Hex mostrado quando `value.color` é undefined. |
|
|
384
|
+
| `defaultPosX` | `number` | — | % horizontal exibida no slider quando undefined. |
|
|
385
|
+
| `defaultPosY` | `number` | — | % vertical exibida no slider quando undefined. |
|
|
386
|
+
| `enabledLabel` | `string` | `'Exibir'` | Label da checkbox. |
|
|
387
|
+
| `textPlaceholder` | `string` | — | Placeholder do field texto. |
|
|
388
|
+
| `recentColors` | `RecentColorsAdapter` | — | Adapter pra cores recentes (sumir swatches se ausente). |
|
|
389
|
+
| `recentKind` | `string` | `'text'` | Chave de cor pra agrupar recentes (do adapter). |
|
|
390
|
+
| `seedColors` | `string[]` | `['#FFFFFF', '#000000']` | Sementes quando recentes acabam. |
|
|
391
|
+
| `sizeMin` | `number` | `50` | Mínimo do slider de Tamanho (%). |
|
|
392
|
+
| `sizeMax` | `number` | `500` | Máximo do slider de Tamanho (%). |
|
|
393
|
+
|
|
394
|
+
## Props do `<BadgePartControls>`
|
|
395
|
+
|
|
396
|
+
| Prop | Tipo | Default | Descrição |
|
|
397
|
+
|---|---|---|---|
|
|
398
|
+
| `color` | `string \| undefined` | — | Hex atual (undefined cai pra `defaultColor`). |
|
|
399
|
+
| `defaultColor` | `string` | — | Hex placeholder quando `color` é undefined. |
|
|
400
|
+
| `size` | `number \| undefined` | — | Tamanho atual (%). |
|
|
401
|
+
| `offsetX` | `number \| undefined` | — | Posição X atual (%). |
|
|
402
|
+
| `offsetY` | `number \| undefined` | — | Posição Y atual (%). |
|
|
403
|
+
| `defaultPosX` | `number` | — | Posição X exibida quando `offsetX` é undefined. |
|
|
404
|
+
| `defaultPosY` | `number` | — | Posição Y exibida quando `offsetY` é undefined. |
|
|
405
|
+
| `onColorChange` | `(v?: string) => void` | — | Emit. Recebe undefined em reset. |
|
|
406
|
+
| `onSizeChange` | `(v?: number) => void` | — | Emit. undefined em reset. |
|
|
407
|
+
| `onOffsetXChange` | `(v?: number) => void` | — | idem. |
|
|
408
|
+
| `onOffsetYChange` | `(v?: number) => void` | — | idem. |
|
|
409
|
+
| `recentColors` | `RecentColorsAdapter` | — | Adapter opcional de cores recentes. |
|
|
410
|
+
| `recentKind` | `string` | `'text'` | Chave do adapter. |
|
|
411
|
+
| `seedColors` | `string[]` | `['#FFFFFF', '#000000']` | Sementes neutras. |
|
|
412
|
+
| `sizeMin` | `number` | `50` | Mínimo do slider Tamanho. |
|
|
413
|
+
| `sizeMax` | `number` | `500` | Máximo do slider Tamanho. |
|
|
414
|
+
|
|
415
|
+
## Roadmap
|
|
416
|
+
|
|
417
|
+
- Drag-and-drop de arquivos no card de uploads
|
|
418
|
+
- Mais aspect ratios (`2/3`, `9/16`)
|
|
419
|
+
- Animações suaves na troca de modal/preview
|
|
420
|
+
- Adapter de recentes (cores usadas anteriormente nos badges)
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tipos compartilhados do `@chrono-os/image-editor-react`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valor do crop aplicado à imagem. Persistido pelo consumer no schema
|
|
10
|
+
* (geralmente JSONB).
|
|
11
|
+
*/
|
|
12
|
+
interface CropValue {
|
|
13
|
+
position?: string;
|
|
14
|
+
/** Zoom. 1 = cover normal, >1 = zoom in, <1 = zoom out (mostra backdrop). */
|
|
15
|
+
scale?: number;
|
|
16
|
+
mirror?: boolean;
|
|
17
|
+
grayscale?: number;
|
|
18
|
+
saturation?: number;
|
|
19
|
+
sepia?: number;
|
|
20
|
+
sepiaColor?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Cor de fundo do container (visível em PNG transparente ou zoom out).
|
|
23
|
+
* Pode ser `#RRGGBB` ou `"transparent"`. Quando undefined, consumer usa
|
|
24
|
+
* o default dele.
|
|
25
|
+
*/
|
|
26
|
+
backdropColor?: string;
|
|
27
|
+
/** Opacidade da cor de backdrop 0-100 (default 100). */
|
|
28
|
+
backdropOpacity?: number;
|
|
29
|
+
/** Translate offset additional to focal — em target frac signed. Permite frame fora da imagem. */
|
|
30
|
+
overflowX?: number;
|
|
31
|
+
overflowY?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Item de upload listado/criado pelo `UploadAdapter`. */
|
|
34
|
+
interface UploadItem {
|
|
35
|
+
filename: string;
|
|
36
|
+
url: string;
|
|
37
|
+
sizeBytes: number;
|
|
38
|
+
createdAt?: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resultado do upload (subset extendido do `UploadItem` retornado pelo
|
|
44
|
+
* `upload()`). Compatível com `UploadItem`.
|
|
45
|
+
*/
|
|
46
|
+
type UploadedImage = UploadItem;
|
|
47
|
+
/**
|
|
48
|
+
* Adapter de uploads — o consumer implementa essa interface contra o backend
|
|
49
|
+
* dele (REST, GraphQL, mock, etc.) e passa para `<ImagePicker>` via prop.
|
|
50
|
+
*
|
|
51
|
+
* Erros propagam (rejeitam a promise). `ImagePicker` mostra mensagem amigável.
|
|
52
|
+
*/
|
|
53
|
+
interface UploadAdapter {
|
|
54
|
+
/** Lista uploads existentes (ordem decrescente por createdAt no consumer). */
|
|
55
|
+
list: () => Promise<UploadItem[]>;
|
|
56
|
+
/** Sobe um arquivo e devolve o item criado. */
|
|
57
|
+
upload: (file: File) => Promise<UploadItem>;
|
|
58
|
+
/** Remove um upload por filename. Retorna true em sucesso. */
|
|
59
|
+
delete: (filename: string) => Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve uma URL armazenada (relativa ou absoluta) pra URL absoluta usada
|
|
62
|
+
* no `<img src>`. Default = identity (string passa direto).
|
|
63
|
+
* Útil quando o backend serve `/uploads/*` e o frontend roda em domínio
|
|
64
|
+
* diferente sem rewrite — consumer prefixa com baseUrl.
|
|
65
|
+
*/
|
|
66
|
+
resolveUrl?: (url: string) => string;
|
|
67
|
+
}
|
|
68
|
+
/** Preset de imagem do site (atalhos curados — opcional). */
|
|
69
|
+
interface ImagePreset {
|
|
70
|
+
label: string;
|
|
71
|
+
url: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adapter de cores recentes — opcional. Quando ausente, `BadgePartControls`
|
|
75
|
+
* (consumer) não mostra o swatch.
|
|
76
|
+
*/
|
|
77
|
+
interface RecentColorsAdapter {
|
|
78
|
+
colors: {
|
|
79
|
+
[kind: string]: string[];
|
|
80
|
+
};
|
|
81
|
+
addColor: (hex: string, kind: string) => void;
|
|
82
|
+
seed?: {
|
|
83
|
+
[kind: string]: string[];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Dados do badge sobreposto à imagem. Renderer real fica no consumer (ex
|
|
88
|
+
* @chrono-os/lp-react); aqui só usamos pra preview visual no editor.
|
|
89
|
+
*/
|
|
90
|
+
interface BadgeData {
|
|
91
|
+
numero?: string;
|
|
92
|
+
label?: string;
|
|
93
|
+
numeroEnabled?: boolean;
|
|
94
|
+
labelEnabled?: boolean;
|
|
95
|
+
numeroColor?: string;
|
|
96
|
+
numeroSize?: number;
|
|
97
|
+
numeroOffsetX?: number;
|
|
98
|
+
numeroOffsetY?: number;
|
|
99
|
+
labelColor?: string;
|
|
100
|
+
labelSize?: number;
|
|
101
|
+
labelOffsetX?: number;
|
|
102
|
+
labelOffsetY?: number;
|
|
103
|
+
}
|
|
104
|
+
type ImagePickerAspect = '1/1' | '3/4' | '16/9' | '4/3';
|
|
105
|
+
/** Props do `<ImagePicker>` controlado. */
|
|
106
|
+
interface ImagePickerProps {
|
|
107
|
+
value: string;
|
|
108
|
+
onChange: (url: string, aspectRatio?: number) => void;
|
|
109
|
+
/** Adapter pra upload/list/delete contra o backend do consumer. */
|
|
110
|
+
uploadAdapter: UploadAdapter;
|
|
111
|
+
label?: string;
|
|
112
|
+
hint?: string;
|
|
113
|
+
aspect?: ImagePickerAspect;
|
|
114
|
+
crop?: CropValue;
|
|
115
|
+
onCropChange?: (crop: CropValue) => void;
|
|
116
|
+
/** Badge opcional (número + label) sobreposto na imagem no preview. */
|
|
117
|
+
badge?: BadgeData;
|
|
118
|
+
/** Slots opcionais — cada um vira um subcard ao lado direito do preview. */
|
|
119
|
+
extraCards?: ReactNode[];
|
|
120
|
+
/** Presets de imagem do site (mostrados na aba "Imagens" abaixo dos uploads). */
|
|
121
|
+
presets?: ImagePreset[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface BadgeOverlayProps {
|
|
125
|
+
badge?: BadgeData;
|
|
126
|
+
size?: 'sm' | 'md';
|
|
127
|
+
}
|
|
128
|
+
declare function BadgeOverlay({ badge }: BadgeOverlayProps): react_jsx_runtime.JSX.Element | null;
|
|
129
|
+
|
|
130
|
+
export { type BadgeData as B, type CropValue as C, type ImagePickerAspect as I, type RecentColorsAdapter as R, type UploadAdapter as U, BadgeOverlay as a, type ImagePickerProps as b, type ImagePreset as c, type UploadItem as d, type UploadedImage as e };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tipos compartilhados do `@chrono-os/image-editor-react`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valor do crop aplicado à imagem. Persistido pelo consumer no schema
|
|
10
|
+
* (geralmente JSONB).
|
|
11
|
+
*/
|
|
12
|
+
interface CropValue {
|
|
13
|
+
position?: string;
|
|
14
|
+
/** Zoom. 1 = cover normal, >1 = zoom in, <1 = zoom out (mostra backdrop). */
|
|
15
|
+
scale?: number;
|
|
16
|
+
mirror?: boolean;
|
|
17
|
+
grayscale?: number;
|
|
18
|
+
saturation?: number;
|
|
19
|
+
sepia?: number;
|
|
20
|
+
sepiaColor?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Cor de fundo do container (visível em PNG transparente ou zoom out).
|
|
23
|
+
* Pode ser `#RRGGBB` ou `"transparent"`. Quando undefined, consumer usa
|
|
24
|
+
* o default dele.
|
|
25
|
+
*/
|
|
26
|
+
backdropColor?: string;
|
|
27
|
+
/** Opacidade da cor de backdrop 0-100 (default 100). */
|
|
28
|
+
backdropOpacity?: number;
|
|
29
|
+
/** Translate offset additional to focal — em target frac signed. Permite frame fora da imagem. */
|
|
30
|
+
overflowX?: number;
|
|
31
|
+
overflowY?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Item de upload listado/criado pelo `UploadAdapter`. */
|
|
34
|
+
interface UploadItem {
|
|
35
|
+
filename: string;
|
|
36
|
+
url: string;
|
|
37
|
+
sizeBytes: number;
|
|
38
|
+
createdAt?: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resultado do upload (subset extendido do `UploadItem` retornado pelo
|
|
44
|
+
* `upload()`). Compatível com `UploadItem`.
|
|
45
|
+
*/
|
|
46
|
+
type UploadedImage = UploadItem;
|
|
47
|
+
/**
|
|
48
|
+
* Adapter de uploads — o consumer implementa essa interface contra o backend
|
|
49
|
+
* dele (REST, GraphQL, mock, etc.) e passa para `<ImagePicker>` via prop.
|
|
50
|
+
*
|
|
51
|
+
* Erros propagam (rejeitam a promise). `ImagePicker` mostra mensagem amigável.
|
|
52
|
+
*/
|
|
53
|
+
interface UploadAdapter {
|
|
54
|
+
/** Lista uploads existentes (ordem decrescente por createdAt no consumer). */
|
|
55
|
+
list: () => Promise<UploadItem[]>;
|
|
56
|
+
/** Sobe um arquivo e devolve o item criado. */
|
|
57
|
+
upload: (file: File) => Promise<UploadItem>;
|
|
58
|
+
/** Remove um upload por filename. Retorna true em sucesso. */
|
|
59
|
+
delete: (filename: string) => Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve uma URL armazenada (relativa ou absoluta) pra URL absoluta usada
|
|
62
|
+
* no `<img src>`. Default = identity (string passa direto).
|
|
63
|
+
* Útil quando o backend serve `/uploads/*` e o frontend roda em domínio
|
|
64
|
+
* diferente sem rewrite — consumer prefixa com baseUrl.
|
|
65
|
+
*/
|
|
66
|
+
resolveUrl?: (url: string) => string;
|
|
67
|
+
}
|
|
68
|
+
/** Preset de imagem do site (atalhos curados — opcional). */
|
|
69
|
+
interface ImagePreset {
|
|
70
|
+
label: string;
|
|
71
|
+
url: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adapter de cores recentes — opcional. Quando ausente, `BadgePartControls`
|
|
75
|
+
* (consumer) não mostra o swatch.
|
|
76
|
+
*/
|
|
77
|
+
interface RecentColorsAdapter {
|
|
78
|
+
colors: {
|
|
79
|
+
[kind: string]: string[];
|
|
80
|
+
};
|
|
81
|
+
addColor: (hex: string, kind: string) => void;
|
|
82
|
+
seed?: {
|
|
83
|
+
[kind: string]: string[];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Dados do badge sobreposto à imagem. Renderer real fica no consumer (ex
|
|
88
|
+
* @chrono-os/lp-react); aqui só usamos pra preview visual no editor.
|
|
89
|
+
*/
|
|
90
|
+
interface BadgeData {
|
|
91
|
+
numero?: string;
|
|
92
|
+
label?: string;
|
|
93
|
+
numeroEnabled?: boolean;
|
|
94
|
+
labelEnabled?: boolean;
|
|
95
|
+
numeroColor?: string;
|
|
96
|
+
numeroSize?: number;
|
|
97
|
+
numeroOffsetX?: number;
|
|
98
|
+
numeroOffsetY?: number;
|
|
99
|
+
labelColor?: string;
|
|
100
|
+
labelSize?: number;
|
|
101
|
+
labelOffsetX?: number;
|
|
102
|
+
labelOffsetY?: number;
|
|
103
|
+
}
|
|
104
|
+
type ImagePickerAspect = '1/1' | '3/4' | '16/9' | '4/3';
|
|
105
|
+
/** Props do `<ImagePicker>` controlado. */
|
|
106
|
+
interface ImagePickerProps {
|
|
107
|
+
value: string;
|
|
108
|
+
onChange: (url: string, aspectRatio?: number) => void;
|
|
109
|
+
/** Adapter pra upload/list/delete contra o backend do consumer. */
|
|
110
|
+
uploadAdapter: UploadAdapter;
|
|
111
|
+
label?: string;
|
|
112
|
+
hint?: string;
|
|
113
|
+
aspect?: ImagePickerAspect;
|
|
114
|
+
crop?: CropValue;
|
|
115
|
+
onCropChange?: (crop: CropValue) => void;
|
|
116
|
+
/** Badge opcional (número + label) sobreposto na imagem no preview. */
|
|
117
|
+
badge?: BadgeData;
|
|
118
|
+
/** Slots opcionais — cada um vira um subcard ao lado direito do preview. */
|
|
119
|
+
extraCards?: ReactNode[];
|
|
120
|
+
/** Presets de imagem do site (mostrados na aba "Imagens" abaixo dos uploads). */
|
|
121
|
+
presets?: ImagePreset[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface BadgeOverlayProps {
|
|
125
|
+
badge?: BadgeData;
|
|
126
|
+
size?: 'sm' | 'md';
|
|
127
|
+
}
|
|
128
|
+
declare function BadgeOverlay({ badge }: BadgeOverlayProps): react_jsx_runtime.JSX.Element | null;
|
|
129
|
+
|
|
130
|
+
export { type BadgeData as B, type CropValue as C, type ImagePickerAspect as I, type RecentColorsAdapter as R, type UploadAdapter as U, BadgeOverlay as a, type ImagePickerProps as b, type ImagePreset as c, type UploadItem as d, type UploadedImage as e };
|