feat(ui): centraliza estilo de filtros/busca (components/filterStyles) + SearchInput

Fonte única para os campos de filtro (inputNew/labelNew) e um componente
SearchInput padrão (ícone + input arredondado, encaminha ref/props). Profissionais
e Salas passam a importar do módulo (dedupe). Aplica SearchInput na busca do
Orçamentos como 1ª adoção. Buscas que são autocomplete com dropdown (Pacientes,
Relatórios) ficam para conversão cuidadosa caso a caso (não quebrar o dropdown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-07 02:36:17 +02:00
parent e0b37797b9
commit 29f9139b6c
4 changed files with 34 additions and 16 deletions
+28
View File
@@ -0,0 +1,28 @@
// Estilo PADRÃO de filtros/busca do app (fonte única). Referência visual:
// marketplace de profissionais / alugar salas.
import React, { forwardRef } from 'react';
import { Search } from 'lucide-react';
// Classes de campo e rótulo dos filtros (select/input dentro do card de filtros).
export const inputNew = 'w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-semibold text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors';
export const labelNew = 'text-[11px] font-bold text-gray-500 mb-1.5 block';
// Campo de BUSCA padrão (ícone à esquerda + input arredondado). Encaminha ref e
// todos os props nativos (value, onChange, onKeyDown, onFocus, autoFocus, etc.).
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
/** classe do wrapper (ex.: largura: 'w-full' | 'min-w-[220px]'). */
wrapperClassName?: string;
}
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
({ wrapperClassName = 'w-full', className = '', ...rest }, ref) => (
<div className={`relative ${wrapperClassName}`}>
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" />
<input
ref={ref}
{...rest}
className={`w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors ${className}`}
/>
</div>
),
);
SearchInput.displayName = 'SearchInput';
+2 -10
View File
@@ -6,6 +6,7 @@ import { useHybridBackend } from '../hooks/useHybridBackend.ts';
import { useToast } from '../contexts/ToastContext.tsx';
import { useConfirm } from '../contexts/ConfirmContext.tsx';
import { PageHeader } from '../components/PageHeader.tsx';
import { SearchInput } from '../components/filterStyles.tsx';
import { OrcamentoModal, DEFAULT_CLAUSULAS } from './OrcamentoModal.tsx';
type StatusFilter = 'TODOS' | 'Ativo' | 'Rascunho' | 'Assinado' | 'Encerrado';
@@ -148,16 +149,7 @@ export const OrcamentosView: React.FC = () => {
{/* Search + status filters */}
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={16} />
<input
type="text"
placeholder="Buscar por paciente ou título..."
className="w-full pl-9 pr-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-100 focus:border-emerald-300 outline-none bg-white"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<SearchInput wrapperClassName="flex-1" placeholder="Buscar por paciente ou título..." value={search} onChange={e => setSearch(e.target.value)} />
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1 overflow-x-auto">
{STATUS_TABS.map(s => (
<button
@@ -230,9 +230,8 @@ const CAT_META: Record<CatModelo, { label: string; icon: any }> = {
const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-teal-400';
const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1';
// Estilo da busca (versão nova) — rótulos title-case e campos maiores/arredondados.
const inputNew = 'w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-semibold text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors';
const labelNew = 'text-[11px] font-bold text-gray-500 mb-1.5 block';
// Estilo padrão de filtros — fonte única em components/filterStyles.
import { inputNew, labelNew } from '../../components/filterStyles.tsx';
// ─── Modal: enviar proposta ──────────────────────────────────────────────────
const PropostaModal: React.FC<{ prof: Profissional; onClose: () => void; onSent: () => void }> = ({ prof, onClose, onSent }) => {
+2 -3
View File
@@ -108,9 +108,8 @@ const precos = (s: Sala): Array<[string, string]> => {
const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-teal-400';
const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1';
// Estilo da área de filtros (mesmo do marketplace de profissionais).
const inputNew = 'w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-semibold text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors';
const labelNew = 'text-[11px] font-bold text-gray-500 mb-1.5 block';
// Estilo padrão de filtros — fonte única em components/filterStyles.
import { inputNew, labelNew } from '../../components/filterStyles.tsx';
// ─── Modal: cadastrar / editar sala ──────────────────────────────────────────
const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved: () => void }> = ({ sala, onClose, onSaved }) => {