Premium Widgets
Custom Kanban
Kanban en React
Representa tareas o procesos como tarjetas en columnas de estado. La selección y el estado guardado del tablero pertenecen a tu aplicación React.
Referencia para @arcrider/arcwidgets-react@0.1.0-alpha.5. <a href="/docs/react/intro/setup-react">Instalación</a> · <a href="/docs/react/intro/react-components">Todos los componentes</a>.
Ejemplo completo
import {
ArcWidgetBadge,
ArcWidgetIcon,
ArcWidgetKanban,
ArcWidgetLayout,
ArcWidgetText,
} from '@arcrider/arcwidgets-react';
import { useState } from 'react';
const lang = document.documentElement.lang.startsWith('en') ? 'en' : document.documentElement.lang.startsWith('es') ? 'es' : 'de'; const requested = new URLSearchParams(location.search).get('data'); const pack = requested === 'empty' || requested === 'busy' ? requested : 'demo'; const t = (de: string, en: string, es: string) => lang === 'en' ? en : lang === 'es' ? es : de;
const LANES = { neu: { id: 'neu', title: t('Neu', 'New', 'Nuevo'), borderColor: '#F59E0B', }, offer: { id: 'offer', title: t('Angebot verschickt', 'Offer sent', 'Oferta enviada'), borderColor: '#8B5CF6', }, won: { id: 'won', title: t('Gewonnen', 'Won', 'Ganado'), borderColor: '#22C55E', }, lost: { id: 'lost', title: t('Verloren', 'Lost', 'Perdido'), borderColor: '#E04F5F', }, } as const;
type LaneId = keyof typeof LANES;
type Deal = { id: string; title: string; company: string; value: string; chance: number; overdue?: boolean; };
const deal = ( id: string, title: string, company: string, value: string, chance: number, overdue = false, ): Deal => ({ id, title, company, value, chance, overdue });
const demoDeals: Record<LaneId, Deal[]> = { neu: [ deal( '1', t('Website-Relaunch', 'Website relaunch', 'Rediseño web'), 'Müller GmbH', '18.400,00 €', 20, true, ), deal( '2', t('Erstgespräch', 'First call', 'Primera llamada'), 'Stahlwerk West', '6.200,00 €', 10, ), deal( '7', t('Shop-Anbindung', 'Shop integration', 'Integración tienda'), 'Nordlicht KG', '9.600,00 €', 15, ), deal( '8', t('App-Redesign', 'App redesign', 'Rediseño app'), 'Bauer AG', '24.100,00 €', 25, ), ], offer: [ deal( '3', t('CRM-Einführung', 'CRM rollout', 'Implantación CRM'), 'Globex Inc.', '42.000,00 €', 60, ), deal( '4', t('Servicevertrag', 'Service contract', 'Contrato servicio'), 'Acme Corp', '12.800,00 €', 45, ), deal( '9', t('Schulungspaket', 'Training package', 'Paquete formación'), 'Wayne Ent.', '16.400,00 €', 55, ), ], won: [ deal( '5', t('Lizenzpaket', 'Licence pack', 'Paquete licencias'), 'Umbrella Co.', '96.500,00 €', 100, ), deal( '10', t('Wartung 24/7', 'Maintenance 24/7', 'Mantenimiento 24/7'), 'Stark Ind.', '54.000,00 €', 100, ), deal( '11', t('Schnittstellen', 'Integration', 'Integración'), 'Soylent Ltd.', '31.200,00 €', 100, ), ], lost: [ deal( '6', t('Pilotprojekt', 'Pilot project', 'Proyecto piloto'), 'Initech', '8.900,00 €', 0, ), deal( '12', t('Workshop-Tag', 'Workshop day', 'Día de taller'), 'Pied Piper', '4.800,00 €', 0, ), ], };
const extraDeals: Record<LaneId, Deal[]> = { neu: [ deal( '13', t('Newsletter-Relaunch', 'Newsletter relaunch', 'Rediseño newsletter'), 'Hooli', '7.400,00 €', 18, ), deal( '14', t('Messestand', 'Fair booth', 'Stand feria'), 'InGen', '11.200,00 €', 22, ), ], offer: [ deal( '15', t('Cloud-Migration', 'Cloud migration', 'Migración cloud'), 'Cyberdyne', '38.000,00 €', 50, ), deal( '16', t('Support-Plus', 'Support plus', 'Soporte plus'), 'Massive Dyn.', '9.100,00 €', 40, ), ], won: [ deal( '17', t('Jahreslizenz', 'Annual licence', 'Licencia anual'), 'Pied Piper', '21.000,00 €', 100, ), ], lost: [ deal( '18', t('PoC intern', 'Internal PoC', 'PoC interno'), 'Hooli', '3.200,00 €', 0, ), ], };
const busyDeals: Record<LaneId, Deal[]> = { neu: [...demoDeals.neu, ...extraDeals.neu], offer: [...demoDeals.offer, ...extraDeals.offer], won: [...demoDeals.won, ...extraDeals.won], lost: [...demoDeals.lost, ...extraDeals.lost], };
const emptyDeals: Record<LaneId, Deal[]> = { neu: [], offer: [], won: [], lost: [], };
const deals = pack === 'empty' ? emptyDeals : pack === 'busy' ? busyDeals : demoDeals;
const itemView = (item: Deal, borderColor: string) => ({ id: item.id, clickable: true, draggable: true, paddingX: '12px', paddingY: '10px', backgroundColor: '#ffffff', borderPosition: 'left', borderColor: item.overdue ? '#DC2626' : borderColor, borderWidth: '3px', title: ( <span style={{ fontSize: 12, fontWeight: 600, color: '#18181b' }}> {item.title} </span> ), subtitle: ( <span> <span style={{ fontSize: 11, color: '#52525b' }}>{item.company}</span> <br /> <span style={{ fontSize: 11, fontWeight: 600, color: '#18181b' }}> {item.value} </span> </span> ), value: ( <span style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%', marginTop: 2, }} > <span style={{ flex: 1, height: 6, borderRadius: 999, background: '#E5E7EB', overflow: 'hidden', }} > <span style={{ display: 'block', width: ${item.chance}%, height: '100%', background: borderColor, borderRadius: 999, }} /> </span> <span style={{ fontSize: 10, fontWeight: 600, color: '#18181b', minWidth: 28, textAlign: 'right', }} > {item.chance}% </span> </span> ), });
const startLanes = (Object.keys(LANES) as LaneId[]).map((id) => ({ ...LANES[id], items: deals[id], }));
const PLUS_BLOCK = 4;
function LaneHeader({ title, count, dot, onAdd, }: { title: string; count: number; dot: string; onAdd: () => void; }) { return ( <ArcWidgetLayout data={{ embedded: true, direction: 'horizontal', alignX: 'left', alignY: 'center', gap: '8px', width: '100%', height: 'auto', blocks: [ { width: 'auto', height: 'auto', value: ( <ArcWidgetIcon data={{ name: 'circle', style: 'fill', iconSize: '10px', color: dot, containerSize: '12px', backgroundColor: 'transparent', }} /> ), }, { width: 'auto', height: 'auto', value: ( <ArcWidgetText data={{ value: title, fontSize: '14px', fontWeight: '700', fontColor: '#111827', }} /> ), }, { width: 'auto', height: 'auto', value: ( <ArcWidgetBadge data={{ value: String(count), fontSize: '11px', fontWeight: '700', fontColor: '#6B21A8', backgroundColor: '#F3E8FF', borderColor: '#E9D5FF', borderRadius: '999px', paddingX: '8px', paddingY: '2px', }} /> ), }, { width: 'fraction', height: '1px', value: '' }, { width: 'auto', height: 'auto', clickable: true, value: ( <ArcWidgetIcon data={{ name: 'plus', style: 'bold', iconSize: '14px', color: '#6B7280', containerSize: '24px', borderRadius: '8px', backgroundColor: '#F4F5F7', }} /> ), }, ], }} onBlockClick={(block_index) => { if (block_index === PLUS_BLOCK) onAdd(); }} /> ); }
export default function Example() { const [lanes, setLanes] = useState(startLanes); const [selected, setSelected] = useState( t( 'Karte ziehen oder anklicken', 'Drag or click a card', 'Arrastra o pulsa una tarjeta', ), ); const addDeal = (laneId: LaneId) => { setLanes((current) => current.map((lane) => lane.id === laneId ? { ...lane, items: [ ...lane.items, deal( ${laneId}-${lane.items.length + 1}-${Date.now()}, t('Neuer Deal', 'New deal', 'Nuevo trato'), t('Ohne Kontakt', 'No contact', 'Sin contacto'), '—', laneId === 'won' ? 100 : laneId === 'lost' ? 0 : 15, ), ], } : lane, ), ); setSelected( ${t('Deal angelegt in', 'Deal added to', 'Trato añadido a')} ${LANES[laneId].title}, ); }; return ( <> <ArcWidgetKanban id="deals" data={{ height: '380px', collapsible: false, showScrollbar: true, empty: { enabled: true, value: t('Keine Deals', 'No deals', 'Sin tratos'), }, swimlanes: lanes.map((lane) => ({ id: lane.id, title: ( <LaneHeader title={lane.title} count={lane.items.length} dot={lane.borderColor} onAdd={() => addDeal(lane.id)} /> ), minWidth: '300px', fontColor: '#111827', backgroundColorHeader: '#FFFFFF', backgroundColorBody: '#F4F5F7', showAmount: false, empty: true, items: lane.items.map((item) => itemView(item, lane.borderColor)), })), }} onItemClick={(_event, { item_index }) => setSelected(${t('Deal', 'Deal', 'Trato')} ${item_index + 1}) } onItemDrop={async (payload) => { const next = lanes.map((lane) => ({ ...lane, items: [...lane.items], })); const [item] = next[payload.source_swimlane_index].items.splice( payload.item_index_in_section, 1, ); next[payload.target_swimlane_index].items.splice( payload.target_index_in_section, 0, item, ); setLanes(next); setSelected(t('Karte verschoben', 'Card moved', 'Tarjeta movida')); }} /> <p aria-live="polite">{selected}</p> </> ); }</code></pre>
Props del componente
id
ID opcional y estable de la instancia del widget. Sin especificarlo, React genera un ID.
data
Configuración de tipo ArcWidgetKanbanData. Pasa los cambios como objetos nuevos; gestiona los datos de negocio y la selección en el estado de React.
onAction
Fallback opcional (type: string, ...args: unknown[]) => unknown. El callback concreto tiene prioridad. Detalles: <a href="/docs/react/patterns/react-state-events">Estado y eventos</a>.
onItemClick
Evento del navegador y posición de la tarjeta.
onItemClick?: (event: Event, payload: ArcWidgetKanbanItemClickPayload) => void;
onItemDrop
Nueva posición de una tarjeta movida. Un promise de guardado devuelto mantiene la operación abierta hasta que finaliza; el rechazo revierte el cambio.
onItemDrop?: (payload: ArcWidgetKanbanItemDropPayload) => void | Promise<void>;
Configuración
Los siguientes campos pertenecen a data. Los tamaños se indican como strings, los números y valores de tiempo según su tipo respectivo.
height
Altura del widget, p. ej. "320px". Los valores porcentuales necesitan un contenedor padre con altura definida.
Tipo: string.
empty
Visualización para columnas vacías; true o una etiqueta y colores propios.
Tipo: boolean | string | ArcWidgetKanbanEmpty.
swimlanes
Columnas con items planos o sections con nombre.
Tipo: ArcWidgetKanbanSwimlane[].
scrollSettings
Comportamiento de scroll del área de contenido.
El tipo y los campos anidados están en ArcWidgetKanbanData, bajo tipos de datos.
showScrollbar
Visibilidad de la barra de scroll.
Tipo: boolean.
collapsible
Expandir o contraer; el formato concreto está en el tipo.
Tipo: boolean.
Comportamiento y límites
- Una columna contiene o bien
items planos o bien sections con nombre, cada una con sus propios items. Los grupos necesitan un id estable.
onItemClick entrega primero el evento del navegador, luego los índices de columna, grupo y tarjeta. Estos índices se refieren a la estructura de datos pasada.
- Activa
draggable solo con un onItemDrop adecuado. Devuelve su promise de guardado; un promise rechazado cancela el cambio optimista. Actualiza tu estado tras un guardado exitoso.
- La visualización de columna vacía se activa con
empty: true o una configuración propia.
Los campos ArcWidgetSlot admitidos permiten elementos JSX individuales, texto o descriptores de widget. Pasa texto externo como texto JSX; los strings HTML se interpretan como HTML. <a href="/docs/react/patterns/nested-widget-descriptors">Combinar contenidos</a>.
Tipos de datos
Estos tipos relacionados con el widget se exportan desde el paquete. También están disponibles tipos comunes como ArcWidgetSlot, ArcWidgetStyles y ArcReactWidgetProps. Las firmas de índice permiten extensiones; no garantizan que el widget evalúe campos arbitrarios.
export interface ArcWidgetKanbanItemClickPayload {
swimlane_index: number;
section_index: number;
item_index: number;
item_index_in_section: number;
}
export interface ArcWidgetKanbanItemDropPayload { source_swimlane_index: number; target_swimlane_index: number; item_index: number; source_section_index: number; target_section_index: number; item_index_in_section: number; target_index_in_section: number; }
export interface ArcWidgetKanbanItem { id?: string | number; title?: ArcWidgetSlot; subtitle?: ArcWidgetSlot; value?: ArcWidgetSlot; clickable?: boolean; draggable?: boolean; paddingX?: string; paddingY?: string; borderPosition?: string; borderColor?: string; borderWidth?: string; }
export interface ArcWidgetKanbanSection { id: string; title?: ArcWidgetSlot; items?: ArcWidgetKanbanItem[]; hideWhenEmpty?: boolean; collapsed?: boolean; [key: string]: unknown; }
export interface ArcWidgetKanbanEmpty { enabled?: boolean; value?: ArcWidgetSlot; backgroundColor?: string; fontColor?: string; fontWeight?: string | number; }
export interface ArcWidgetKanbanSwimlane { id?: string | number; title?: ArcWidgetSlot; minWidth?: string; empty?: boolean | string | ArcWidgetKanbanEmpty; items?: ArcWidgetKanbanItem[]; sections?: ArcWidgetKanbanSection[]; scrollY?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown }; [key: string]: unknown; }
export interface ArcWidgetKanbanData extends Omit<ArcWidgetBaseData, 'actions'> { height?: string; empty?: boolean | string | ArcWidgetKanbanEmpty; swimlanes?: ArcWidgetKanbanSwimlane[]; scrollSettings?: { scrollX?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown } | boolean; scrollY?: { scrollbar?: { enabled?: boolean }; [key: string]: unknown } | boolean; [key: string]: unknown; }; showScrollbar?: boolean; collapsible?: boolean; }
export type ArcWidgetKanbanProps = ArcReactWidgetProps<ArcWidgetKanbanData> & { onItemClick?: (event: Event, payload: ArcWidgetKanbanItemClickPayload) => void; onItemDrop?: (payload: ArcWidgetKanbanItemDropPayload) => void | Promise<void>; };</code></pre>