Pular para o conteúdo principal

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

DatasetPacoteQuando usar
lucide@iconify-json/lucideParidade com os 146 ícones lucide-react que o projeto usava. Default pra ícones genéricos de UI
simple-icons@iconify-json/simple-iconsBrand logos (WhatsApp, Facebook, X, Telegram, Instagram, TikTok, YouTube, Kwai, Twitch, Threads)
mdi@iconify-json/mdiEsportes específicos (soccer/basketball/tennis/volleyball/controller) e alternativas pros emojis de config (mdi:slot-machine, mdi:trophy, mdi:crown)
customapp/icons/custom/*.svgSVGs 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: AlarmClockalarm-clock, Gamepad2gamepad-2, CheckCircle2check-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:

ArquivoPapel
RenderSectionIcon.tsxDispatch inline. Decide entre componente, string vetorial e texto
SectionIcon.tsxResolve a string <set>:<name> pro componente
registry.tsMapa <set>:<name> → componente
resolve.tsHelpers 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

  1. Verifica se existe no Iconify antes de codar: https://icon-sets.iconify.design/
  2. Estático: add import direto no componente.
  3. Dinâmico: add entry em app/components/section-title/registry.ts.
  4. Custom: drop SVG em app/icons/custom/.

Gotchas

  • Nem todo ícone Lucide/MDI existe no Iconify com o mesmo nome. Ex: mdi:dragon não existe (dragon caiu como fallback emoji após codemod). Se o build falhar com Icon ... not found, troca o nome ou volta pro emoji.
  • String sem prefixo de set conhecidoRenderSectionIcon renderiza 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.ts carrega 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-react pre-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 — converte size={X}width={X} height={X}.
  • migrate-emoji-icons.mjs — substitui emojis em icon: "…" literals por IconNames.

(O migrate-lucide-to-unplugin-icons.mjs não existe mais — a migração terminou.)

Documentação relacionada