{"version":3,"file":"Transfer.cjs","names":[],"sources":["../../../src/components/Transfer/Transfer.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — two lists that hand\n * items to each other: items and value are the model, onChange the move,\n * sourceTitle/targetTitle/renderItem/locale/height the presentation, and\n * searchable/disabled the behaviour. The body keeps both selections and the filter\n * query, and every move reads all three.\n */\nimport { useId, useMemo, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { Checkbox } from \"../Checkbox\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n    applyMove,\n    filterItems,\n    movableIds,\n    splitSides,\n    transferStrings,\n    type TransferItem,\n    type TransferSide,\n} from \"./transfer-state\";\nimport styles from \"./Transfer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\" | \"defaultValue\";\n\nexport interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n    /** The whole catalogue. Both panes are derived from it. */\n    items: readonly TransferItem[];\n    /** Ids on the target side. Controlled. */\n    value: readonly string[];\n    /** Next value, always in catalogue order. */\n    onChange: (value: string[]) => void;\n    /** Heading of the left pane. Default `\"Disponíveis\"`. */\n    sourceTitle?: ReactNode;\n    /** Heading of the right pane. Default `\"Selecionados\"`. */\n    targetTitle?: ReactNode;\n    /** Show a search box above each pane. Default `true` past 8 items. */\n    searchable?: boolean;\n    /** Custom row body. Gets the item and which side it is on. */\n    renderItem?: (item: TransferItem, side: TransferSide) => ReactNode;\n    /** Locale for labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Height of each pane's scroll area, any CSS length. Default `\"16rem\"`. */\n    height?: string;\n    /** Block every move. */\n    disabled?: boolean;\n}\n\n/**\n * Two panes and a set of move controls: pick a subset out of a catalogue.\n *\n * Controlled on **ids of the target side only**. The panes are derived on every\n * render, never stored, because two stored lists drift the moment the catalogue\n * changes under them — a permission removed upstream lingers in whichever pane\n * held it, and an id in both panes is a bug nobody can see.\n *\n * @example\n * const [roles, setRoles] = useState<string[]>([]);\n *\n * <Transfer\n *     items={allRoles}\n *     value={roles}\n *     onChange={setRoles}\n *     sourceTitle=\"Papéis disponíveis\"\n *     targetTitle=\"Papéis do usuário\"\n * />\n */\nexport function Transfer({\n    items,\n    value,\n    onChange,\n    sourceTitle,\n    targetTitle,\n    searchable,\n    renderItem,\n    locale = \"pt-BR\",\n    height = \"16rem\",\n    disabled = false,\n    className,\n    ...rest\n}: TransferProps) {\n    const strings = transferStrings(locale);\n    const baseId = useId();\n    const [checked, setChecked] = useState<Set<string>>(new Set());\n    const [queries, setQueries] = useState<{ source: string; target: string }>({\n        source: \"\",\n        target: \"\",\n    });\n    const [announcement, setAnnouncement] = useState(\"\");\n\n    const { source, target } = useMemo(() => splitSides(items, value), [items, value]);\n    const withSearch = searchable ?? items.length > 8;\n\n    /**\n     * What each pane is showing right now — the filter included.\n     *\n     * Every control acts on this, not on the pane's full contents: filtering to\n     * \"sao\" and pressing \"move all\" has to move what you are looking at. Moving\n     * rows the filter hid is the kind of surprise that makes people stop trusting\n     * the bulk button.\n     */\n    const visible: Record<TransferSide, TransferItem[]> = {\n        source: withSearch ? filterItems(source, queries.source) : [...source],\n        target: withSearch ? filterItems(target, queries.target) : [...target],\n    };\n\n    const panes: Array<{ side: TransferSide; title: ReactNode; items: TransferItem[] }> = [\n        { side: \"source\", title: sourceTitle ?? strings.sourceTitle, items: source },\n        { side: \"target\", title: targetTitle ?? strings.targetTitle, items: target },\n    ];\n\n    /**\n     * Move ids and drop them from the checked set.\n     *\n     * Clearing the checks is the point: after a move the rows are on the other\n     * side, and leaving them checked means the next click on the opposite button\n     * sends them straight back — which reads as the component undoing itself.\n     */\n    const move = (moving: readonly string[], to: TransferSide): void => {\n        if (disabled || moving.length === 0) return;\n        const next = applyMove({ value, moving, to, items });\n        const changed = to === \"target\" ? next.length - value.length : value.length - next.length;\n        onChange(next);\n        setChecked((current) => {\n            const remaining = new Set(current);\n            for (const id of moving) remaining.delete(id);\n            return remaining;\n        });\n        if (changed > 0) {\n            const sideName = to === \"target\" ? strings.targetTitle : strings.sourceTitle;\n            setAnnouncement(strings.moved(changed, String(sideName).toLowerCase()));\n        }\n    };\n\n    const toggle = (id: string): void => {\n        setChecked((current) => {\n            const next = new Set(current);\n            if (next.has(id)) next.delete(id);\n            else next.add(id);\n            return next;\n        });\n    };\n\n    /** Ids checked **and visible** on one side — what the move buttons act on. */\n    const checkedOn = (side: TransferSide): string[] =>\n        movableIds(visible[side]).filter((id) => checked.has(id));\n\n    const checkedSource = checkedOn(\"source\");\n    const checkedTarget = checkedOn(\"target\");\n\n    return (\n        <div className={cn(styles.wrapper, className)} {...rest}>\n            {panes.map((pane) => {\n                const shown = visible[pane.side];\n                const movable = movableIds(shown);\n                const checkedHere = movable.filter((id) => checked.has(id));\n                const allChecked = movable.length > 0 && checkedHere.length === movable.length;\n                const titleId = `${baseId}-${pane.side}-title`;\n\n                return (\n                    <section key={pane.side} className={styles.pane} aria-labelledby={titleId}>\n                        <div className={styles.paneHeader}>\n                            <div className={styles.paneTitle}>\n                                <Checkbox\n                                    checked={allChecked}\n                                    indeterminate={checkedHere.length > 0 && !allChecked}\n                                    disabled={disabled || movable.length === 0}\n                                    aria-label={\n                                        typeof pane.title === \"string\"\n                                            ? pane.title\n                                            : String(pane.side)\n                                    }\n                                    onChange={() =>\n                                        setChecked((current) => {\n                                            const next = new Set(current);\n                                            for (const id of movable) {\n                                                if (allChecked) next.delete(id);\n                                                else next.add(id);\n                                            }\n                                            return next;\n                                        })\n                                    }\n                                />\n                                <h3 className={styles.title} id={titleId}>\n                                    {pane.title}\n                                </h3>\n                            </div>\n                            <span className={styles.count}>\n                                {strings.selected(checkedHere.length, pane.items.length)}\n                            </span>\n                        </div>\n\n                        {withSearch && (\n                            <input\n                                type=\"search\"\n                                className={styles.search}\n                                value={queries[pane.side]}\n                                placeholder={strings.search}\n                                aria-label={`${strings.search}: ${\n                                    typeof pane.title === \"string\" ? pane.title : pane.side\n                                }`}\n                                onChange={(event) =>\n                                    setQueries((current) => ({\n                                        ...current,\n                                        [pane.side]: event.target.value,\n                                    }))\n                                }\n                            />\n                        )}\n\n                        <ul className={styles.list} style={{ height }}>\n                            {shown.length === 0 ? (\n                                <li className={styles.empty}>\n                                    {queries[pane.side] ? strings.noMatches : strings.empty}\n                                </li>\n                            ) : (\n                                shown.map((item) => (\n                                    <li key={item.id} className={styles.row}>\n                                        <Checkbox\n                                            checked={checked.has(item.id)}\n                                            disabled={disabled || item.disabled}\n                                            label={\n                                                renderItem\n                                                    ? renderItem(item, pane.side)\n                                                    : item.label\n                                            }\n                                            onChange={() => toggle(item.id)}\n                                            onDoubleClick={() =>\n                                                move(\n                                                    [item.id],\n                                                    pane.side === \"source\" ? \"target\" : \"source\",\n                                                )\n                                            }\n                                        />\n                                    </li>\n                                ))\n                            )}\n                        </ul>\n                    </section>\n                );\n            })}\n\n            <div className={styles.controls}>\n                <button\n                    type=\"button\"\n                    className={styles.control}\n                    disabled={disabled || movableIds(visible.source).length === 0}\n                    aria-label={strings.allToTarget}\n                    title={strings.allToTarget}\n                    onClick={() => move(movableIds(visible.source), \"target\")}\n                >\n                    »\n                </button>\n                <button\n                    type=\"button\"\n                    className={styles.control}\n                    disabled={disabled || checkedSource.length === 0}\n                    aria-label={strings.toTarget}\n                    title={strings.toTarget}\n                    onClick={() => move(checkedSource, \"target\")}\n                >\n                    ›\n                </button>\n                <button\n                    type=\"button\"\n                    className={styles.control}\n                    disabled={disabled || checkedTarget.length === 0}\n                    aria-label={strings.toSource}\n                    title={strings.toSource}\n                    onClick={() => move(checkedTarget, \"source\")}\n                >\n                    ‹\n                </button>\n                <button\n                    type=\"button\"\n                    className={styles.control}\n                    disabled={disabled || movableIds(visible.target).length === 0}\n                    aria-label={strings.allToSource}\n                    title={strings.allToSource}\n                    onClick={() => move(movableIds(visible.target), \"source\")}\n                >\n                    «\n                </button>\n            </div>\n\n            <VisuallyHidden aria-live=\"polite\" role=\"status\">\n                {announcement}\n            </VisuallyHidden>\n        </div>\n    );\n}\n"],"mappings":"yPAqEA,SAAgB,EAAS,CACrB,QACA,QACA,WACA,cACA,cACA,aACA,aACA,SAAS,QACT,SAAS,QACT,WAAW,GACX,YACA,GAAG,GACW,CACd,IAAM,EAAU,EAAA,gBAAgB,CAAM,EAChC,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAsB,IAAI,GAAK,EACvD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAA6C,CACvE,OAAQ,GACR,OAAQ,EACZ,CAAC,EACK,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAS,EAAE,EAE7C,CAAE,SAAQ,WAAA,EAAW,EAAA,QAAA,KAAc,EAAA,WAAW,EAAO,CAAK,EAAG,CAAC,EAAO,CAAK,CAAC,EAC3E,EAAa,GAAc,EAAM,OAAS,EAU1C,EAAgD,CAClD,OAAQ,EAAa,EAAA,YAAY,EAAQ,EAAQ,MAAM,EAAI,CAAC,GAAG,CAAM,EACrE,OAAQ,EAAa,EAAA,YAAY,EAAQ,EAAQ,MAAM,EAAI,CAAC,GAAG,CAAM,CACzE,EAEM,EAAgF,CAClF,CAAE,KAAM,SAAU,MAAO,GAAe,EAAQ,YAAa,MAAO,CAAO,EAC3E,CAAE,KAAM,SAAU,MAAO,GAAe,EAAQ,YAAa,MAAO,CAAO,CAC/E,EASM,GAAQ,EAA2B,IAA2B,CAChE,GAAI,GAAY,EAAO,SAAW,EAAG,OACrC,IAAM,EAAO,EAAA,UAAU,CAAE,QAAO,SAAQ,KAAI,OAAM,CAAC,EAC7C,EAAU,IAAO,SAAW,EAAK,OAAS,EAAM,OAAS,EAAM,OAAS,EAAK,OAOnF,GANA,EAAS,CAAI,EACb,EAAY,GAAY,CACpB,IAAM,EAAY,IAAI,IAAI,CAAO,EACjC,IAAK,IAAM,KAAM,EAAQ,EAAU,OAAO,CAAE,EAC5C,OAAO,CACX,CAAC,EACG,EAAU,EAAG,CACb,IAAM,EAAW,IAAO,SAAW,EAAQ,YAAc,EAAQ,YACjE,EAAgB,EAAQ,MAAM,EAAS,OAAO,CAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1E,CACJ,EAEM,EAAU,GAAqB,CACjC,EAAY,GAAY,CACpB,IAAM,EAAO,IAAI,IAAI,CAAO,EAG5B,OAFI,EAAK,IAAI,CAAE,EAAG,EAAK,OAAO,CAAE,EAC3B,EAAK,IAAI,CAAE,EACT,CACX,CAAC,CACL,EAGM,EAAa,GACf,EAAA,WAAW,EAAQ,EAAK,CAAC,CAAC,OAAQ,GAAO,EAAQ,IAAI,CAAE,CAAC,EAEtD,EAAgB,EAAU,QAAQ,EAClC,EAAgB,EAAU,QAAQ,EAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,EAAG,GAAI,EAAnD,SAAA,CACK,EAAM,IAAK,GAAS,CACjB,IAAM,EAAQ,EAAQ,EAAK,MACrB,EAAU,EAAA,WAAW,CAAK,EAC1B,EAAc,EAAQ,OAAQ,GAAO,EAAQ,IAAI,CAAE,CAAC,EACpD,EAAa,EAAQ,OAAS,GAAK,EAAY,SAAW,EAAQ,OAClE,EAAU,GAAG,EAAO,GAAG,EAAK,KAAK,QAEvC,OACI,EAAA,EAAA,KAAA,CAAC,UAAD,CAAyB,UAAW,EAAA,QAAO,KAAM,kBAAiB,EAAlE,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,WAAvB,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,UAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,QAAS,EACT,cAAe,EAAY,OAAS,GAAK,CAAC,EAC1C,SAAU,GAAY,EAAQ,SAAW,EACzC,aACI,OAAO,EAAK,OAAU,SAChB,EAAK,MACL,OAAO,EAAK,IAAI,EAE1B,aACI,EAAY,GAAY,CACpB,IAAM,EAAO,IAAI,IAAI,CAAO,EAC5B,IAAK,IAAM,KAAM,EACT,EAAY,EAAK,OAAO,CAAE,EACzB,EAAK,IAAI,CAAE,EAEpB,OAAO,CACX,CAAC,CAER,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,GAAI,EAC5B,SAAA,EAAK,KACN,CAAA,CACH,CACL,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MACnB,SAAA,EAAQ,SAAS,EAAY,OAAQ,EAAK,MAAM,MAAM,CACrD,CAAA,CACL,IAEJ,IACG,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,MAAO,EAAQ,EAAK,MACpB,YAAa,EAAQ,OACrB,aAAY,GAAG,EAAQ,OAAO,IAC1B,OAAO,EAAK,OAAU,SAAW,EAAK,MAAQ,EAAK,OAEvD,SAAW,GACP,EAAY,IAAa,CACrB,GAAG,GACF,EAAK,MAAO,EAAM,OAAO,KAC9B,EAAE,CAET,CAAA,GAGL,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,QAAO,EACvC,SAAA,EAAM,SAAW,GACd,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MACjB,SAAA,EAAQ,EAAK,MAAQ,EAAQ,UAAY,EAAQ,KAClD,CAAA,EAEJ,EAAM,IAAK,IACP,EAAA,EAAA,IAAA,CAAC,KAAD,CAAkB,UAAW,EAAA,QAAO,IAChC,UAAA,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,QAAS,EAAQ,IAAI,EAAK,EAAE,EAC5B,SAAU,GAAY,EAAK,SAC3B,MACI,EACM,EAAW,EAAM,EAAK,IAAI,EAC1B,EAAK,MAEf,aAAgB,EAAO,EAAK,EAAE,EAC9B,kBACI,EACI,CAAC,EAAK,EAAE,EACR,EAAK,OAAS,SAAW,SAAW,QACxC,CAEP,CAAA,CACD,EAjBK,EAAK,EAiBV,CACP,CAEL,CAAA,CACC,CA9EK,EAAA,EAAK,IA8EV,CAEjB,CAAC,GAED,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,SAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAA,WAAW,EAAQ,MAAM,CAAC,CAAC,SAAW,EAC5D,aAAY,EAAQ,YACpB,MAAO,EAAQ,YACf,YAAe,EAAK,EAAA,WAAW,EAAQ,MAAM,EAAG,QAAQ,EAC3D,SAAA,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAc,SAAW,EAC/C,aAAY,EAAQ,SACpB,MAAO,EAAQ,SACf,YAAe,EAAK,EAAe,QAAQ,EAC9C,SAAA,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAc,SAAW,EAC/C,aAAY,EAAQ,SACpB,MAAO,EAAQ,SACf,YAAe,EAAK,EAAe,QAAQ,EAC9C,SAAA,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAY,EAAA,WAAW,EAAQ,MAAM,CAAC,CAAC,SAAW,EAC5D,aAAY,EAAQ,YACpB,MAAO,EAAQ,YACf,YAAe,EAAK,EAAA,WAAW,EAAQ,MAAM,EAAG,QAAQ,EAC3D,SAAA,GAEO,CAAA,CACP,KAEL,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAgB,YAAU,SAAS,KAAK,SACnC,SAAA,CACW,CAAA,CACf,GAEb"}