Icon system
O front-web-base usa unplugin-icons (Vite plugin) com datasets Iconify pra todos os ícones. lucide-react foi removido em 2026-04-28.
Datasets ativos
| Dataset | Pacote | Quando usar |
|---|---|---|
lucide | @iconify-json/lucide | Paridade com os 146 ícones lucide-react que o projeto usava. Default pra ícones genéricos de UI |
simple-icons | @iconify-json/simple-icons | Brand logos (WhatsApp, Facebook, X, Telegram, Instagram, TikTok, YouTube, Kwai, Twitch, Threads) |
mdi | @iconify-json/mdi | Esportes específicos (soccer/basketball/tennis/volleyball/controller) e alternativas pros emojis de config (mdi:slot-machine, mdi:trophy, mdi:crown) |
custom | app/icons/custom/*.svg | SVGs próprios via FileSystemIconLoader. Auto-discovered. Pra logos de ligas/brand marks onde Iconify não basta |
⚠️ Não nomeie a coleção custom de
cactus. AGENTS.md proíbe vazar o termo interno em runtime —customé o default.
API de consumo
Estático (preferido)
Default import por nome kebab-case. Tree-shaking perfeito, zero runtime.
import Trophy from "~icons/lucide/trophy";
import IconWhatsapp from "~icons/simple-icons/whatsapp";
import IconSoccer from "~icons/mdi/soccer";
<Trophy className="w-4 h-4 text-primary" />
<IconWhatsapp width={20} height={20} className="text-success" />
Nomes kebab-case: AlarmClock → alarm-clock, Gamepad2 → gamepad-2, CheckCircle2 → check-circle-2.
Dinâmico (string-driven)
Quando o ícone vem de config (ex: icon: "mdi:soccer" em home-leagues), registrar em app/icons/registry.ts e consumir via <Icon>:
// app/icons/registry.ts
export const iconRegistry = {
"mdi:soccer": IconSoccer,
"mdi:basketball": IconBasketball,
// ...
} satisfies Record<string, IconComponent>;
// consumer
import { Icon } from "~/components/ui/Icon";
<Icon name="mdi:soccer" className="w-5 h-5" />
Smart dispatch (config legado com emoji)
Pra configs que misturam IconNames com emojis legados (🎰 🏆), usa <SmartIcon>:
import { SmartIcon } from "~/components/ui/SmartIcon";
<SmartIcon icon={config.icon} />
// renderiza <Icon> se "<set>:<name>" registrado, senão renderiza como texto
Usado em SectionTitleDefault/Gradient/Panel.
Tipos
// app/types/icon.ts
export type IconComponent = (props: SVGProps<SVGSVGElement> & { size?: number | string }) => JSX.Element;
Substitui o antigo LucideIcon (que não existe mais). Componentes gerados pelo unplugin-icons aceitam width/height (não size do Lucide).
Props
⚠️ size do Lucide não funciona com unplugin-icons. Use width={X} height={X}. Codemod (scripts/migrate-icon-size-prop.mjs) já converteu 471 ocorrências.
FlagIcon, SidebarMenuItem, createFlagMenuIcon, TopbarNotification aceitam width/height em vez de size (com size como alias legado mantido em FlagIcon).
Custom SVGs
Drop SVG em app/icons/custom/<filename>.svg → vira componente ~icons/custom/<filename>.
import IconLeaguePlaceholder from "~icons/custom/placeholder-league";
<IconLeaguePlaceholder className="w-12 h-12 text-primary" />
FileSystemIconLoader (configurado em vite.config.ts + vitest.config.ts) auto-injeta fill="currentColor" quando ausente. Garante que o ícone seja tintável via Tailwind theme tokens (text-primary, etc.).
Adicionar ícone novo
- Verifica se existe no Iconify antes de codar: https://icon-sets.iconify.design/
- Estático: add
importdireto no componente. - Dinâmico: add entry em
app/icons/registry.ts. - Custom: drop SVG em
app/icons/custom/.
Gotchas
- Nem todo ícone Lucide/MDI existe no Iconify com o mesmo nome. Ex:
mdi:dragonnão existe (dragon caiu como fallback emoji após codemod). Se o build falhar comIcon ... not found, troca o nome ou volta pro emoji. <SmartIcon>com string sem:→ renderiza como texto, não como ícone. Use isso intencionalmente pra fallback emoji.- Mixed import. Não misture
import { X } from "lucide-react"com unplugin-icons. Lucide-react foi removido dopackage.json.
Performance
- Cada ícone vira um componente React JSX inline no bundle final (~300-500 bytes gzip).
- Bundle total pra 146 lucide + 50 mdi + 11 simple-icons ≈ 60-80KB gzip — similar ao
lucide-reactpre-migration. - Zero fetch externo no SSR — todos os ícones vêm como string SVG embutida pelo plugin. Importante pra Cloudflare Workers (sem flash de "ícone vazio" durante hidratação).
Migração completa — referência
Histórico completo das 7 fases (Foundation → Lucide codemod → Quick wins → SocialIcon → Custom SVG pipeline → SmartIcon dispatcher → Emoji codemod) preservado em front-dev/docs/superpowers/archive/2026-04-28-theme-7k-branch-changelog.md (entry "Icon system overhaul").
Scripts reutilizáveis em scripts/:
migrate-lucide-to-unplugin-icons.mjs— converte imports de lucide-react.migrate-icon-size-prop.mjs— convertesize={X}→width={X} height={X}.migrate-emoji-icons.mjs— substitui emojis emicon: "…"literals por IconNames.
Documentação relacionada
- Layout Composition — onde ícones aparecem (sidebar, header, widgets)
- Performance PSI — bundle-size budget e SSR