Files
openmonetis/components/lancamentos/shared/estabelecimento-input.tsx
Felipe Coutinho f50261208a feat(v1.4.0): design system semântico, correções de revalidação e melhorias de UX
- Adicionar tokens semânticos de estado (success, warning, info) no globals.css
- Migrar ~60+ componentes de cores hardcoded do Tailwind para tokens semânticos
- Unificar 3 arrays duplicados de cores de categorias em importação única
- Corrigir widgets de boleto/fatura que não atualizavam após pagamento
  (actions de fatura e antecipação não invalidavam cache do dashboard)
- Corrigir scroll em listas Popover+Command (modal prop)
- Adicionar link "detalhes" no card de orçamento para página da categoria
- Adicionar indicadores de tendência coloridos nos cards de métricas
- Estender cores de chart de 6 para 10
- Normalizar dark mode e remover tokens não utilizados

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:14:59 +00:00

130 lines
3.2 KiB
TypeScript

"use client";
import { RiCheckLine, RiSearchLine } from "@remixicon/react";
import * as React from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils/ui";
export interface EstabelecimentoInputProps {
id?: string;
value: string;
onChange: (value: string) => void;
estabelecimentos: string[];
placeholder?: string;
required?: boolean;
maxLength?: number;
}
export function EstabelecimentoInput({
id,
value,
onChange,
estabelecimentos = [],
placeholder = "Ex.: Padaria",
required = false,
maxLength = 20,
}: EstabelecimentoInputProps) {
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const handleSelect = (selectedValue: string) => {
onChange(selectedValue);
setOpen(false);
setSearchValue("");
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setSearchValue(newValue);
// Open popover when user types and there are suggestions
if (newValue.length > 0 && estabelecimentos.length > 0) {
setOpen(true);
}
};
const filteredEstabelecimentos = React.useMemo(() => {
if (!searchValue) return estabelecimentos;
const lowerSearch = searchValue.toLowerCase();
return estabelecimentos.filter((item) =>
item.toLowerCase().includes(lowerSearch),
);
}, [estabelecimentos, searchValue]);
return (
<Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild>
<div className="relative">
<Input
id={id}
value={value}
onChange={handleInputChange}
placeholder={placeholder}
required={required}
maxLength={maxLength}
autoComplete="off"
onFocus={() => {
if (estabelecimentos.length > 0) {
setOpen(true);
}
}}
/>
{estabelecimentos.length > 0 && (
<RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />
)}
</div>
</PopoverTrigger>
{estabelecimentos.length > 0 && (
<PopoverContent
className="p-0 w-[--radix-popover-trigger-width]"
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command>
<CommandList className="max-h-[300px] overflow-y-auto">
<CommandEmpty className="p-6">
Nenhum estabelecimento encontrado.
</CommandEmpty>
<CommandGroup className="p-1">
{filteredEstabelecimentos.map((item) => (
<CommandItem
key={item}
value={item}
onSelect={() => handleSelect(item)}
className="cursor-pointer gap-1"
>
<RiCheckLine
className={cn(
"size-4 shrink-0",
value === item
? "opacity-100 text-success"
: "opacity-5",
)}
/>
<span className="truncate flex-1">{item}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
)}
</Popover>
);
}