{"version":3,"file":"FilterBar.cjs","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a filter bar renders a different\n * editor per filter kind — select, multi-select, range, date range, text — and the\n * body is that dispatch plus the chip summary, both reading the same value map.\n */\nimport { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n    defaultOperator,\n    describeFilter,\n    filterStrings,\n    isComplete,\n    isMulti,\n    isValueless,\n    operatorLabel,\n    operatorsFor,\n    type Filter,\n    type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n    /** Fields the user may filter by. */\n    fields: readonly FilterField[];\n    /** Applied filters. Controlled. */\n    value: readonly Filter[];\n    /** Next filter set. Combined with AND. */\n    onChange: (filters: Filter[]) => void;\n    /** Locale for the labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n    actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n    field: string;\n    operator: Filter[\"operator\"];\n    value: string;\n    to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n    return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n    if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n    if (draft.operator === \"between\") {\n        return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n    }\n    if (isMulti(draft.operator)) {\n        return {\n            field: draft.field,\n            operator: draft.operator,\n            value: draft.value\n                .split(\",\")\n                .map((part) => part.trim())\n                .filter(Boolean),\n        };\n    }\n    return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n *     filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n    fields,\n    value,\n    onChange,\n    locale = \"pt-BR\",\n    actions,\n    className,\n    ...rest\n}: FilterBarProps) {\n    const strings = filterStrings(locale);\n    const baseId = useId();\n    const [draft, setDraft] = useState<Draft | null>(null);\n\n    const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n    const pending = draft ? toFilter(draft) : null;\n    const ready = pending !== null && isComplete(pending);\n\n    const open = (): void => {\n        if (fields.length > 0) setDraft(draftFor(fields[0]));\n    };\n\n    const apply = (): void => {\n        if (!pending || !ready) return;\n        onChange([...value, pending]);\n        setDraft(null);\n    };\n\n    const removeAt = (index: number): void => {\n        onChange(value.filter((_, position) => position !== index));\n    };\n\n    return (\n        <div className={cn(styles.bar, className)} {...rest}>\n            <ul className={styles.chips}>\n                {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n                {value.map((filter, index) => {\n                    const description = describeFilter(filter, fields, locale);\n                    return (\n                        <li\n                            key={`${filter.field}-${filter.operator}-${index}`}\n                            className={styles.chip}\n                        >\n                            <span>{description}</span>\n                            <button\n                                type=\"button\"\n                                className={styles.remove}\n                                aria-label={strings.remove(description)}\n                                onClick={() => removeAt(index)}\n                            >\n                                ×\n                            </button>\n                        </li>\n                    );\n                })}\n            </ul>\n\n            {draft && field ? (\n                <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n                    <label className={styles.control}>\n                        <VisuallyHidden>{strings.field}</VisuallyHidden>\n                        <select\n                            className={styles.select}\n                            value={draft.field}\n                            onChange={(event) => {\n                                const next = fields.find(\n                                    (candidate) => candidate.name === event.target.value,\n                                );\n                                if (next) setDraft(draftFor(next));\n                            }}\n                        >\n                            {fields.map((candidate) => (\n                                <option key={candidate.name} value={candidate.name}>\n                                    {candidate.label}\n                                </option>\n                            ))}\n                        </select>\n                    </label>\n\n                    <label className={styles.control}>\n                        <VisuallyHidden>{strings.operator}</VisuallyHidden>\n                        <select\n                            className={styles.select}\n                            value={draft.operator}\n                            onChange={(event) =>\n                                setDraft({\n                                    ...draft,\n                                    operator: event.target.value as Filter[\"operator\"],\n                                    value: \"\",\n                                    to: \"\",\n                                })\n                            }\n                        >\n                            {operatorsFor(field).map((operator) => (\n                                <option key={operator} value={operator}>\n                                    {operatorLabel(operator, locale)}\n                                </option>\n                            ))}\n                        </select>\n                    </label>\n\n                    {!isValueless(draft.operator) && (\n                        <ValueInput\n                            id={`${baseId}-value`}\n                            field={field}\n                            draft={draft}\n                            strings={strings}\n                            onChange={setDraft}\n                        />\n                    )}\n\n                    <button\n                        type=\"button\"\n                        className={styles.primary}\n                        disabled={!ready}\n                        onClick={apply}\n                    >\n                        {strings.apply}\n                    </button>\n                    <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n                        {strings.cancel}\n                    </button>\n                </div>\n            ) : (\n                <div className={styles.actions}>\n                    <button\n                        type=\"button\"\n                        className={styles.secondary}\n                        disabled={fields.length === 0}\n                        onClick={open}\n                    >\n                        + {strings.add}\n                    </button>\n                    {value.length > 0 && (\n                        <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n                            {strings.clear}\n                        </button>\n                    )}\n                    {actions}\n                </div>\n            )}\n\n            <VisuallyHidden aria-live=\"polite\" role=\"status\">\n                {strings.active(value.length)}\n            </VisuallyHidden>\n        </div>\n    );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n    id,\n    field,\n    draft,\n    strings,\n    onChange,\n}: {\n    id: string;\n    field: FilterField;\n    draft: Draft;\n    strings: ReturnType<typeof filterStrings>;\n    onChange: (draft: Draft) => void;\n}) {\n    const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n    if (field.type === \"select\" && !isMulti(draft.operator)) {\n        return (\n            <label className={styles.control}>\n                <VisuallyHidden>{strings.value}</VisuallyHidden>\n                <select\n                    id={id}\n                    className={styles.select}\n                    value={draft.value}\n                    onChange={(event) => onChange({ ...draft, value: event.target.value })}\n                >\n                    <option value=\"\">—</option>\n                    {field.options?.map((option) => (\n                        <option key={option.value} value={option.value}>\n                            {option.label}\n                        </option>\n                    ))}\n                </select>\n            </label>\n        );\n    }\n\n    if (field.type === \"boolean\") {\n        return (\n            <label className={styles.control}>\n                <VisuallyHidden>{strings.value}</VisuallyHidden>\n                <select\n                    id={id}\n                    className={styles.select}\n                    value={draft.value}\n                    onChange={(event) => onChange({ ...draft, value: event.target.value })}\n                >\n                    <option value=\"\">—</option>\n                    <option value=\"true\">{strings.yes}</option>\n                    <option value=\"false\">{strings.no}</option>\n                </select>\n            </label>\n        );\n    }\n\n    if (draft.operator === \"between\") {\n        return (\n            <>\n                <label className={styles.control}>\n                    <VisuallyHidden>{strings.from}</VisuallyHidden>\n                    <input\n                        id={id}\n                        className={styles.input}\n                        type={type}\n                        value={draft.value}\n                        placeholder={strings.from}\n                        onChange={(event) => onChange({ ...draft, value: event.target.value })}\n                    />\n                </label>\n                <label className={styles.control}>\n                    <VisuallyHidden>{strings.to}</VisuallyHidden>\n                    <input\n                        className={styles.input}\n                        type={type}\n                        value={draft.to}\n                        placeholder={strings.to}\n                        onChange={(event) => onChange({ ...draft, to: event.target.value })}\n                    />\n                </label>\n            </>\n        );\n    }\n\n    return (\n        <label className={styles.control}>\n            <VisuallyHidden>{strings.value}</VisuallyHidden>\n            <input\n                id={id}\n                className={styles.input}\n                type={type}\n                value={draft.value}\n                placeholder={field.placeholder ?? strings.value}\n                onChange={(event) => onChange({ ...draft, value: event.target.value })}\n            />\n        </label>\n    );\n}\n"],"mappings":"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"}