From b41e664d8af27cd1a0853167c2e1b5326a290d72 Mon Sep 17 00:00:00 2001 From: Gabriel Menezes Date: Thu, 22 Oct 2026 10:25:33 -0400 Subject: [PATCH] refactor(ui): modernize Select component with HeroUI v3 aesthetics and replace native selects --- .../dashboard/CreateBiCardModal.tsx | 119 ++++++++++-------- frontend/src/components/ui/Select.tsx | 100 +++++++++------ frontend/src/pages/AnalyticsPage.tsx | 27 ++-- 3 files changed, 144 insertions(+), 102 deletions(-) diff --git a/frontend/src/components/dashboard/CreateBiCardModal.tsx b/frontend/src/components/dashboard/CreateBiCardModal.tsx index 59d478d..590f187 100644 --- a/frontend/src/components/dashboard/CreateBiCardModal.tsx +++ b/frontend/src/components/dashboard/CreateBiCardModal.tsx @@ -13,6 +13,7 @@ import { } from 'lucide-react' import type { DashboardV2Card, BiMetricType, BiPeriodFilter, BiCardScope } from '../../lib/userPreferences' import type { Project, Category } from '../../types/task' +import { CustomSelect } from '../ui/Select' interface CreateBiCardModalProps { isOpen: boolean @@ -253,69 +254,67 @@ export const CreateBiCardModal: React.FC = ({ {/* Period Filter */}
- + onChange={(val) => setPeriod(val as BiPeriodFilter)} + options={[ + { value: 'ALL', label: 'Todo o Histórico' }, + { value: 'TODAY', label: 'Apenas Hoje' }, + { value: 'WEEK', label: 'Esta Semana' }, + { value: 'MONTH', label: 'Este Mês' }, + ]} + />
{/* Status Filter */}
- + onChange={(val) => setStatusFilter(val as any)} + options={[ + { value: 'ALL', label: 'Todos os Status' }, + { value: 'COMPLETED', label: 'Apenas Concluídas', color: '#10b981' }, + { value: 'IN_PROGRESS', label: 'Apenas Em Andamento', color: '#14b8a6' }, + { value: 'NOT_STARTED', label: 'Apenas A Fazer', color: '#71717a' }, + ]} + />
{/* Project Filter */}
- + setProjectId(Number(val) === 0 ? undefined : Number(val))} + placeholder="Todos os Projetos" + options={[ + { value: 0, label: 'Todos os Projetos' }, + ...projects.map((p) => ({ + value: p.id, + label: p.name, + color: p.color, + })), + ]} + />
{/* Category Filter */}
- + setCategoryId(Number(val) === 0 ? undefined : Number(val))} + placeholder="Todas as Categorias" + options={[ + { value: 0, label: 'Todas as Categorias' }, + ...categories.map((c) => ({ + value: c.id, + label: c.name, + color: c.color, + })), + ]} + />
{/* Data Scope Filter */} @@ -324,15 +323,27 @@ export const CreateBiCardModal: React.FC = ({ Escopo dos Dados (Atribuição) Individual vs Equipe - + onChange={(val) => setScope(val as BiCardScope)} + options={[ + { + value: 'INHERIT', + label: '🔗 Seguir filtro global da página (Padrão)', + description: 'Acompanha a alternância da tela', + }, + { + value: 'MINE', + label: '👤 Sempre Apenas Minhas Tarefas (Individual)', + description: 'Fixo nas suas demandas', + }, + { + value: 'TEAM', + label: '👥 Sempre Toda a Equipe (Workspace Geral)', + description: 'Fixo em todo o time', + }, + ]} + /> diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx index ca6dbbb..3cc4fd6 100644 --- a/frontend/src/components/ui/Select.tsx +++ b/frontend/src/components/ui/Select.tsx @@ -20,8 +20,16 @@ export interface CustomSelectProps { disabled?: boolean leftIcon?: ReactNode size?: 'sm' | 'md' + label?: string } +/** + * CustomSelect com estética e tokens do HeroUI v3: + * - Acabamento bordered com backdrop-blur suave + * - Foco dinâmico com ring colorido e leve elevação + * - Popover em glassmorphism com animação de zoom/fade + * - Itens com highlight de hover e seleção com checkmark + */ export function CustomSelect({ value, onChange, @@ -32,14 +40,15 @@ export function CustomSelect({ disabled = false, leftIcon, size = 'md', + label, }: CustomSelectProps) { const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) - // Find currently selected option + // Opção atualmente selecionada const selectedOption = options.find((opt) => opt.value === value) - // Close on outside click + // Fechar ao clicar fora useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { @@ -55,7 +64,7 @@ export function CustomSelect({ } }, [isOpen]) - // Close on Escape key + // Fechar ao pressionar Escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { @@ -74,54 +83,71 @@ export function CustomSelect({ const isSmall = size === 'sm' return ( -
- {/* Trigger Button */} +
+ {/* Label opcional no estilo HeroUI */} + {label && ( + + )} + + {/* Trigger Button - Estilo HeroUI v3 Bordered / Faint */} - {/* Dropdown Popover */} + {/* Popover ListBox - Estilo HeroUI v3 Dropdown */} {isOpen && ( -
+
{options.map((option) => { const isSelected = option.value === value @@ -129,29 +155,31 @@ export function CustomSelect({ ) diff --git a/frontend/src/pages/AnalyticsPage.tsx b/frontend/src/pages/AnalyticsPage.tsx index ea9075c..5c034fb 100644 --- a/frontend/src/pages/AnalyticsPage.tsx +++ b/frontend/src/pages/AnalyticsPage.tsx @@ -20,6 +20,7 @@ import { categoryService } from '../services/categoryService' import { organizationService, type OrganizationMember } from '../services/organizationService' import type { Task, Project, Category } from '../types/task' import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils' +import { CustomSelect } from '../components/ui/Select' export const AnalyticsPage: React.FC = () => { const { user, activeOrg } = useAuth() @@ -316,19 +317,21 @@ export const AnalyticsPage: React.FC = () => {
{/* Specific Member Dropdown */} -
- + onChange={(val) => handleMemberChange(val || 'ALL')} + placeholder="Outro Membro..." + options={[ + { value: '', label: 'Outro Membro...' }, + ...members.map((m) => ({ + value: m.userEmail, + label: m.userName, + description: m.userEmail, + })), + ]} + />