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"), o dispatch vive em
app/components/section-title/ — não existe app/icons/registry.ts nem
~/components/ui/Icon:
| Arquivo | Papel |
|---|---|
RenderSectionIcon.tsx | Dispatch inline. Decide entre componente, string vetorial e texto |
SectionIcon.tsx | Resolve a string <set>:<name> pro componente |
registry.ts | Mapa <set>:<name> → componente |
resolve.ts | Helpers de resolução |
O gate é isVectorIconString: só mdi:, custom:, simple-icons: e lucide:
resolvem pra vetor. Qualquer outra string renderiza como texto inline — é
assim que emoji literal de config legado continua funcionando, sem <SmartIcon>
e sem registry global.
Consumido por SectionTitleDefault e SectionTitleGradient (os dois únicos
variants em app/components/section-title/).
:::caution RenderSectionIcon é import ESTÁTICO de propósito
Já foi lazy() com Suspense fallback={null} e os ícones piscavam no load: o
SSR pintava o ícone, o primeiro render client caía no fallback null (ícone
some), o chunk chegava e o ícone voltava. Em rede lenta a janela era de segundos.
O custo aceito: ~59 SVGs (~10kb gzip) no bundle principal. Não volte pra lazy()
sem resolver o fallback (precisaria de preload SSR-safe do chunk antes da
hidratação).
:::
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/components/section-title/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. - String sem prefixo de set conhecido →
RenderSectionIconrenderiza como texto, não como ícone. Use isso intencionalmente pra fallback emoji. - Mixed import. Não importe de
lucide-react. O pacote não é dependência do projeto (só o dataset@iconify-json/lucideé) —app/types/icon.tscarrega um comentário de proibição explícito.
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 que sobraram em scripts/:
migrate-icon-size-prop.mjs— convertesize={X}→width={X} height={X}.migrate-emoji-icons.mjs— substitui emojis emicon: "…"literals por IconNames.
(O migrate-lucide-to-unplugin-icons.mjs não existe mais — a migração terminou.)
Documentação relacionada
- Layout Composition — onde ícones aparecem (sidebar, header, widgets)
- Performance PSI — bundle-size budget e SSR