@chrono-os/image-editor-react
v0.6.4
Published
Editor de imagens com crop visual: <ImagePicker> controlado + <ImageCropPicker> standalone + <BadgeOverlay> + editores de badge (<BadgeEditorCard>, <BadgePartControls>, <SliderRow>)
Downloads
692
Readme
@chrono-os/image-editor-react
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).
Install
yarn add @chrono-os/image-editor-reactPublicado em registry.npmjs.org público — sem auth pra consumir.
Peer dependencies: react@>=18 react-dom@>=18.
Setup tema (recomendado)
Importar o CSS de tokens no layout root (Next.js App Router):
// app/layout.tsx
import '@chrono-os/image-editor-react/theme.css'E sobrescrever as CSS vars no globals.css do consumer (ou em qualquer ancestor com classe .image-editor-root):
:root {
--ie-accent: #C9A961; /* default electric blue #556FFF */
--ie-accent-hover: #A88842;
--ie-border: #d4d4d8;
--ie-bg: #fafafa;
--ie-text: #404040;
--ie-text-muted: #737373;
--ie-danger: #dc2626;
}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.
Setup Tailwind (opcional)
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:
// tailwind.config.ts
export default {
content: [
'./app/**/*.{ts,tsx}',
'./node_modules/@chrono-os/image-editor-react/dist/**/*.{js,cjs}',
],
// ...
}Exemplo: <ImagePicker> completo
'use client'
import { useState } from 'react'
import {
ImagePicker,
type UploadAdapter,
type CropValue,
type BadgeData,
type ImagePreset,
} from '@chrono-os/image-editor-react'
import '@chrono-os/image-editor-react/theme.css'
const uploadAdapter: UploadAdapter = {
list: async () => {
const r = await fetch('/api/admin/uploads', { credentials: 'include' })
if (!r.ok) throw new Error('Falha ao listar')
const data = await r.json()
return data.items
},
upload: async (file) => {
const form = new FormData()
form.append('file', file, file.name)
const r = await fetch('/api/admin/uploads', {
method: 'POST',
body: form,
credentials: 'include',
})
if (!r.ok) throw new Error('Upload falhou')
return r.json()
},
delete: async (filename) => {
const r = await fetch(
`/api/admin/uploads/${encodeURIComponent(filename)}`,
{ method: 'DELETE', credentials: 'include' },
)
return r.ok
},
}
const presets: ImagePreset[] = [
{ label: 'Hero Naírio', url: '/branding/hero-nairio.webp' },
{ label: 'Background ABS', url: '/branding/abs-pattern.webp' },
]
export function HeroImageField() {
const [url, setUrl] = useState('')
const [crop, setCrop] = useState<CropValue>({})
const badge: BadgeData = {
numero: '201',
label: 'advogado',
numeroEnabled: true,
labelEnabled: true,
}
return (
<ImagePicker
label="Foto do hero"
hint="Recomendado: 1200×1600px (3:4)"
value={url}
onChange={(next) => setUrl(next)}
aspect="3/4"
crop={crop}
onCropChange={setCrop}
badge={badge}
uploadAdapter={uploadAdapter}
presets={presets}
/>
)
}Exemplo: <BadgeOverlay> standalone
Pra renderizar só o badge sobreposto (sem o picker — útil em previews de listagem, cards, etc):
import { BadgeOverlay } from '@chrono-os/image-editor-react/badge'
export function HeroCard({ imageUrl }: { imageUrl: string }) {
return (
<div
className="relative aspect-[3/4] overflow-hidden rounded-lg"
style={{ containerType: 'inline-size' }}
>
<img src={imageUrl} alt="" className="h-full w-full object-cover" />
<BadgeOverlay
badge={{
numero: '201',
label: 'advogado',
numeroEnabled: true,
labelEnabled: true,
}}
/>
</div>
)
}Importante: o pai do
<BadgeOverlay>precisa decontainerType: 'inline-size'no CSS pra que oscqw(container query width) funcionem proporcionalmente em qualquer tamanho.
Exemplo: <BadgeEditorCard> no extraCards
Renderiza um subcard completo de edição de parte do badge (título + checkbox "Exibir" + texto + cor + sliders). Encaixa direto no extraCards do <ImagePicker>:
'use client'
import {
ImagePicker,
BadgeEditorCard,
type BadgePartState,
type RecentColorsAdapter,
} from '@chrono-os/image-editor-react'
// Adapter opcional pra integrar com sistema de cores recentes do consumer.
const recentColors: RecentColorsAdapter = {
colors: { text: ['#13203B', '#C9A961', '#FFFFFF'] },
addColor: (hex, kind) => {
// persistir no DB do consumer
},
}
export function HeroBadgeFields({
badge,
onBadgeChange,
}: {
badge: { numero?: BadgePartState; label?: BadgePartState }
onBadgeChange: (next: typeof badge) => void
}) {
return (
<ImagePicker
// ...props do ImagePicker
value="/img/hero.webp"
onChange={() => {}}
uploadAdapter={uploadAdapter}
extraCards={[
<BadgeEditorCard
key="numero"
title="Badge — Número"
enabledLabel="Exibir número"
value={badge.numero ?? {}}
onChange={(next) => onBadgeChange({ ...badge, numero: next })}
defaultColor="#C9A961"
defaultPosX={6}
defaultPosY={84}
recentColors={recentColors}
/>,
<BadgeEditorCard
key="label"
title="Badge — Label"
enabledLabel="Exibir label"
value={badge.label ?? {}}
onChange={(next) => onBadgeChange({ ...badge, label: next })}
defaultColor="#FFFFFF"
defaultPosX={36}
defaultPosY={88}
recentColors={recentColors}
/>,
]}
/>
)
}A interface BadgePartState é:
export interface BadgePartState {
enabled?: boolean // undefined === true (exibe), false === oculto
text?: string
color?: string // #RRGGBB
size?: number // % do default (50-500)
offsetX?: number // % horizontal (0-100)
offsetY?: number // % vertical (0-100)
}Exemplo: <BadgePartControls> standalone
Se você já tem o subcard externo (título + checkbox + texto) e quer só os controles compactos de cor + sliders:
'use client'
import { BadgePartControls } from '@chrono-os/image-editor-react'
export function MyBadgeControls({ partState, onPatch }) {
return (
<BadgePartControls
color={partState.color}
defaultColor="#C9A961"
size={partState.size}
offsetX={partState.offsetX}
offsetY={partState.offsetY}
defaultPosX={6}
defaultPosY={84}
onColorChange={(v) => onPatch({ color: v })}
onSizeChange={(v) => onPatch({ size: v })}
onOffsetXChange={(v) => onPatch({ offsetX: v })}
onOffsetYChange={(v) => onPatch({ offsetY: v })}
/>
)
}Exemplo: <ImageCropPicker> standalone
Pra quem só quer o editor de crop sem o wrapper de upload/preview:
'use client'
import { useState } from 'react'
import { ImageCropPicker, type CropValue } from '@chrono-os/image-editor-react'
export function CropOnly({ imageUrl }: { imageUrl: string }) {
const [crop, setCrop] = useState<CropValue>({})
return (
<ImageCropPicker
imageUrl={imageUrl}
targetAspect={3 / 4}
value={crop}
onChange={setCrop}
maxHeight={520}
/>
)
}Props do <ImagePicker>
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| value | string | — | URL atual da imagem (controlado). |
| onChange | (url: string, aspectRatio?: number) => void | — | Chamado quando o consumer escolhe nova imagem. |
| uploadAdapter | UploadAdapter | — | Adapter contra o backend (list/upload/delete/resolveUrl). |
| label | string | — | Rótulo do field acima do preview. |
| hint | string | — | Texto de ajuda abaixo do label. |
| aspect | '1/1'\|'3/4'\|'16/9'\|'4/3' | '3/4' | Aspect ratio do frame de destino. |
| crop | CropValue | {} | Valor de crop (position/scale/mirror/filters/backdrop). |
| onCropChange | (crop: CropValue) => void | — | Chamado quando o usuário ajusta o crop no modal. |
| badge | BadgeData | — | Badge sobreposto na preview (número + label). |
| extraCards | ReactNode[] | — | Cards extras ao lado direito do preview (slots de UI custom). |
| presets | ImagePreset[] | — | Atalhos de imagem curados (mostrados na aba "Imagens"). |
Interface UploadAdapter
export interface UploadAdapter {
/** Lista uploads existentes (ordem decrescente por createdAt no consumer). */
list: () => Promise<UploadItem[]>
/** Sobe um arquivo e devolve o item criado. */
upload: (file: File) => Promise<UploadItem>
/** Remove um upload por filename. Retorna true em sucesso. */
delete: (filename: string) => Promise<boolean>
/**
* Resolve uma URL armazenada (relativa ou absoluta) pra URL absoluta usada
* no <img src>. Default = identity. Útil quando backend serve /uploads/*
* em domínio diferente sem rewrite.
*/
resolveUrl?: (url: string) => string
}
export interface UploadItem {
filename: string
url: string
sizeBytes: number
createdAt?: string
width?: number
height?: number
}Interface CropValue
export interface CropValue {
/** CSS object-position style: "50% 50%" ou "center top". */
position?: string
/** Zoom. 1 = cover normal, >1 = zoom in, <1 = zoom out (mostra backdrop). */
scale?: number
mirror?: boolean
grayscale?: number
saturation?: number
sepia?: number
sepiaColor?: string
/** Cor de fundo do container (#RRGGBB ou 'transparent'). */
backdropColor?: string
/** Opacidade da cor de backdrop 0-100. */
backdropOpacity?: number
/** Translate offset adicional (frame fora da imagem). */
overflowX?: number
overflowY?: number
}Interface BadgeData
export interface BadgeData {
numero?: string
label?: string
numeroEnabled?: boolean
labelEnabled?: boolean
numeroColor?: string
numeroSize?: number // 50-500% (default 100)
numeroOffsetX?: number // %, default 6
numeroOffsetY?: number // %, default 84
labelColor?: string
labelSize?: number // 50-500% (default 100)
labelOffsetX?: number // %, default 36
labelOffsetY?: number // %, default 88
}Props do <BadgeEditorCard>
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| title | string | — | Título do subcard (ex "Badge — Número"). |
| value | BadgePartState | — | Estado controlado (enabled/text/color/size/offsetX/offsetY). |
| onChange | (next: BadgePartState) => void | — | Recebe o estado completo a cada edição. |
| defaultColor | string | — | Hex mostrado quando value.color é undefined. |
| defaultPosX | number | — | % horizontal exibida no slider quando undefined. |
| defaultPosY | number | — | % vertical exibida no slider quando undefined. |
| enabledLabel | string | 'Exibir' | Label da checkbox. |
| textPlaceholder | string | — | Placeholder do field texto. |
| recentColors | RecentColorsAdapter | — | Adapter pra cores recentes (sumir swatches se ausente). |
| recentKind | string | 'text' | Chave de cor pra agrupar recentes (do adapter). |
| seedColors | string[] | ['#FFFFFF', '#000000'] | Sementes quando recentes acabam. |
| sizeMin | number | 50 | Mínimo do slider de Tamanho (%). |
| sizeMax | number | 500 | Máximo do slider de Tamanho (%). |
Props do <BadgePartControls>
| Prop | Tipo | Default | Descrição |
|---|---|---|---|
| color | string \| undefined | — | Hex atual (undefined cai pra defaultColor). |
| defaultColor | string | — | Hex placeholder quando color é undefined. |
| size | number \| undefined | — | Tamanho atual (%). |
| offsetX | number \| undefined | — | Posição X atual (%). |
| offsetY | number \| undefined | — | Posição Y atual (%). |
| defaultPosX | number | — | Posição X exibida quando offsetX é undefined. |
| defaultPosY | number | — | Posição Y exibida quando offsetY é undefined. |
| onColorChange | (v?: string) => void | — | Emit. Recebe undefined em reset. |
| onSizeChange | (v?: number) => void | — | Emit. undefined em reset. |
| onOffsetXChange | (v?: number) => void | — | idem. |
| onOffsetYChange | (v?: number) => void | — | idem. |
| recentColors | RecentColorsAdapter | — | Adapter opcional de cores recentes. |
| recentKind | string | 'text' | Chave do adapter. |
| seedColors | string[] | ['#FFFFFF', '#000000'] | Sementes neutras. |
| sizeMin | number | 50 | Mínimo do slider Tamanho. |
| sizeMax | number | 500 | Máximo do slider Tamanho. |
Roadmap
- Drag-and-drop de arquivos no card de uploads
- Mais aspect ratios (
2/3,9/16) - Animações suaves na troca de modal/preview
- Adapter de recentes (cores usadas anteriormente nos badges)
License
MIT
