{"version":3,"file":"Kanban.cjs","names":[],"sources":["../../../src/components/Kanban/Kanban.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — drag-and-drop needs an\n * accessible twin: cardRoleDescription and label exist so the keyboard path\n * announces what the pointer path shows, and renderCard/emptyLabel are the slots.\n * The body is the move calculation shared by both paths.\n */\n/* eslint-disable react-hooks/refs -- `getItemProps` is called during render and its\n   handlers close over `useSortable`'s mirror refs, so the rule reports the call site\n   here. The refs are only written from event handlers and read from a window\n   listener; the two reads that were genuinely render-time (the drag group in the\n   rendered class/aria, and the group lookup inside `commit`) were fixed instead of\n   silenced — see the hook's docstring. */\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useSortable } from \"@/hooks/use-sortable\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Kanban.module.css\";\n\n/** One card on the board. */\nexport interface KanbanCard {\n    /** Stable identifier, unique across the whole board. */\n    id: string;\n    /** Card body. A string renders as the card's title. */\n    content: ReactNode;\n}\n\n/** One column of the board. */\nexport interface KanbanColumn {\n    /** Stable identifier, used as the drop group. */\n    id: string;\n    /** Column heading. */\n    title: ReactNode;\n    /** Cards in display order. */\n    cards: KanbanCard[];\n    /**\n     * Refuses drops into this column. The cards inside can still be picked up and\n     * moved elsewhere — a \"done\" column that no longer accepts work, for instance.\n     */\n    locked?: boolean;\n}\n\n/** Where a card ended up. */\nexport interface KanbanMove {\n    cardId: string;\n    fromColumn: string;\n    toColumn: string;\n    /** Index within the destination column. */\n    toIndex: number;\n}\n\nexport interface KanbanProps {\n    columns: KanbanColumn[];\n    /**\n     * Called once per committed move — on pointer release or on a keyboard drop.\n     * You own the data: apply the move and pass the new `columns` back.\n     */\n    onMove: (move: KanbanMove) => void;\n    /** Renders a card. Defaults to its `content`. */\n    renderCard?: (card: KanbanCard, column: KanbanColumn) => ReactNode;\n    /** Text shown in a column with no cards. */\n    emptyLabel?: ReactNode;\n    /** Announced per card, so a screen reader user learns the keyboard contract. */\n    cardRoleDescription?: string;\n    /**\n     * Accessible name of the board. A `listbox` without a name is unusable with a\n     * screen reader, so this has a default rather than being optional in practice.\n     */\n    label?: string;\n    /** Blocks all dragging. */\n    disabled?: boolean;\n    className?: string;\n}\n\n/**\n * Kanban board: columns of cards, reorderable within a column and movable across\n * columns, by pointer **or** keyboard.\n *\n * The drag machine is `useSortable` — the board does not reimplement it. Cards are\n * indexed globally across columns so a single sortable can span the whole board;\n * the component translates that flat index back into `{ column, index }` for\n * `onMove`, which is the shape an app actually needs.\n *\n * Nothing is mutated here: `onMove` reports the intent once and the parent applies\n * it. That keeps the board a controlled value and avoids a re-render per pointer\n * frame.\n *\n * ARIA shape: **one `listbox` per column**, named by the column title, containing\n * only `option` cards. A single board-wide listbox does not survive the markup a\n * board needs — `listbox` requires `option`/`group` children, and the column header\n * and card wrapper in between break that ownership. Per-column listboxes also read\n * better: a screen reader announces which column it is in and how many cards it\n * holds. The column header is a plain `div`, not a `<header>`, because outside a\n * sectioning element every `<header>` becomes a page-level banner landmark — with\n * three columns that is three duplicate banners. An **empty** column is not marked\n * as a listbox at all: a listbox with zero options fails `aria-required-children`,\n * and \"no cards\" is exactly zero options.\n *\n * Known limitation: a **keyboard** move can only target positions that hold a card,\n * because the move walks the flat index space of existing cards. Dropping into an\n * empty column works by pointer; by keyboard it does not. Moving a card to a column\n * that already has one card, then reordering, is the workaround until column-switch\n * keys land.\n *\n * @example\n * ```tsx\n * const [columns, setColumns] = useState<KanbanColumn[]>([\n *   { id: \"todo\", title: \"A fazer\", cards: [{ id: \"1\", content: \"Corrigir login\" }] },\n *   { id: \"doing\", title: \"Fazendo\", cards: [] },\n *   { id: \"done\", title: \"Feito\", cards: [], locked: true },\n * ]);\n *\n * <Kanban\n *   columns={columns}\n *   onMove={({ cardId, fromColumn, toColumn, toIndex }) =>\n *     setColumns((current) => applyKanbanMove(current, { cardId, fromColumn, toColumn, toIndex }))\n *   }\n * />\n * ```\n */\nexport function Kanban({\n    columns,\n    onMove,\n    renderCard,\n    emptyLabel = \"Nenhum card\",\n    cardRoleDescription = \"Card do quadro — Espaço pega, setas movem, Espaço solta\",\n    label = \"Quadro\",\n    disabled = false,\n    className,\n}: KanbanProps) {\n    /**\n     * Flat view of every card, in column order.\n     *\n     * `useSortable` works on a single index space, so the board flattens itself and\n     * keeps the mapping back to `{ column, index }` here — the only place that has\n     * to know both shapes.\n     */\n    const flat = useMemo(\n        () =>\n            columns.flatMap((column, columnIndex) =>\n                column.cards.map((card, cardIndex) => ({ card, column, columnIndex, cardIndex })),\n            ),\n        [columns],\n    );\n\n    const sortable = useSortable({\n        itemCount: flat.length,\n        disabled,\n        roleDescription: cardRoleDescription,\n        onReorder: ({ from, to, fromGroup, toGroup }) => {\n            const source = flat[from];\n            if (!source) return;\n\n            const destinationId = toGroup ?? fromGroup ?? source.column.id;\n            const destination = columns.find((column) => column.id === destinationId);\n            if (!destination || destination.locked) return;\n\n            const target = flat[to];\n            const toIndex =\n                target && target.column.id === destinationId\n                    ? target.cardIndex\n                    : destination.cards.length;\n\n            onMove({\n                cardId: source.card.id,\n                fromColumn: source.column.id,\n                toColumn: destinationId,\n                toIndex,\n            });\n        },\n    });\n\n    /**\n     * First flat index of each column.\n     *\n     * Derived instead of counted with a mutable `flatIndex++` during render: a\n     * render-scoped counter mutated inside the map is exactly the pattern the React\n     * Compiler rules reject, and for good reason — a partial or replayed render\n     * would resume from a half-advanced counter.\n     */\n    const columnOffsets = useMemo(() => {\n        const offsets: number[] = [];\n        let start = 0;\n        for (const column of columns) {\n            offsets.push(start);\n            start += column.cards.length;\n        }\n        return offsets;\n    }, [columns]);\n\n    return (\n        <div className={cn(styles.board, className)} aria-label={label} ref={sortable.setContainer}>\n            {columns.map((column, columnIndex) => {\n                const isDropTarget = sortable.overGroup === column.id && !column.locked;\n                return (\n                    <div\n                        key={column.id}\n                        className={cn(\n                            styles.column,\n                            isDropTarget && styles.columnActive,\n                            column.locked && styles.columnLocked,\n                        )}\n                    >\n                        <div className={styles.columnHeader}>\n                            <span className={styles.columnTitle}>{column.title}</span>\n                            <span className={styles.count}>{column.cards.length}</span>\n                        </div>\n\n                        <div\n                            className={styles.cards}\n                            {...(column.cards.length > 0 ? sortable.getListProps() : {})}\n                            aria-label={typeof column.title === \"string\" ? column.title : column.id}\n                        >\n                            {column.cards.map((card, cardIndex) => {\n                                const index = columnOffsets[columnIndex] + cardIndex;\n                                const isActive = sortable.activeIndex === index;\n                                const isOver = sortable.overIndex === index && !isActive;\n                                return (\n                                    <div\n                                        key={card.id}\n                                        {...sortable.getItemProps(index, column.id)}\n                                        className={cn(\n                                            styles.card,\n                                            isActive && styles.cardActive,\n                                            isOver && styles.cardOver,\n                                        )}\n                                    >\n                                        {renderCard ? renderCard(card, column) : card.content}\n                                    </div>\n                                );\n                            })}\n\n                            {column.cards.length === 0 ? (\n                                <div\n                                    {...sortable.getEmptyGroupProps(column.id)}\n                                    className={styles.empty}\n                                >\n                                    {emptyLabel}\n                                </div>\n                            ) : null}\n                        </div>\n                    </div>\n                );\n            })}\n        </div>\n    );\n}\n\n/**\n * Apply a {@link KanbanMove} to a column list, returning new arrays.\n *\n * Exported because every consumer needs the same reducer.\n *\n * `toIndex` is the position of the card that was dropped **onto**, read from the\n * board as it looked before the move. Inserting at that same index *after* removing\n * the dragged card lands where the user aimed in both directions — moving down, the\n * removal shifts the target up by one and the insertion lands after it; moving up,\n * nothing shifted. Compensating for the shift (the \"obvious\" `toIndex - 1`) is what\n * turns a one-step move down into a no-op.\n *\n * @param columns - Current board.\n * @param move - The move reported by `onMove`.\n * @returns A new column list, or the same reference when the move cannot apply.\n */\nexport function applyKanbanMove(columns: KanbanColumn[], move: KanbanMove): KanbanColumn[] {\n    const fromColumn = columns.find((column) => column.id === move.fromColumn);\n    const card = fromColumn?.cards.find((candidate) => candidate.id === move.cardId);\n    if (!fromColumn || !card) return columns;\n\n    const insertAt = move.toIndex;\n\n    return columns.map((column) => {\n        if (column.id === move.fromColumn && column.id === move.toColumn) {\n            const without = column.cards.filter((candidate) => candidate.id !== move.cardId);\n            without.splice(insertAt, 0, card);\n            return { ...column, cards: without };\n        }\n        if (column.id === move.fromColumn) {\n            return { ...column, cards: column.cards.filter((c) => c.id !== move.cardId) };\n        }\n        if (column.id === move.toColumn) {\n            const next = column.cards.slice();\n            next.splice(insertAt, 0, card);\n            return { ...column, cards: next };\n        }\n        return column;\n    });\n}\n"],"mappings":"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"}