From 82ee53c4ed7dae563e9dbdf555fa72b04c2e97ba Mon Sep 17 00:00:00 2001 From: Matheus Pompeo Date: Wed, 19 Aug 2026 10:05:43 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(design-system):=20funda=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20design=20tokens=20(Fase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cria a camada única de tokens que serve de base para a padronização de UI (auditoria de design system, Fase 0/1 concluídas em conversa). - src/styles/_tokens.scss: custom properties centralizando cor, tipografia, espaçamento, border-radius, sombra e duração/easing de transição. Adiciona tokens "on-image" (--dex-white/black/scrim/ icon-on-photo) que hoje vazavam como #fff/#000/rgba() soltos. - src/styles/_breakpoints.scss: breakpoints como variáveis Sass (sm/md/lg = 480/768/1200px) + mixins bp.up()/bp.down(), já que @media não aceita var(). Substitui o outlier 600px de .dex-page. - src/styles.scss: consome os tokens nas classes utilitárias globais (.dex-page, .dex-search, .dex-title, .dex-back, tooltip, snackbar) como demonstração de uso; migração das telas fica para a Fase 4. - src/index.html: corrige o meta theme-color (#CC0000 → #FF0000) para bater com --pokedex-red. - .gitignore: ignora .claude/worktrees/ (isolamento local de agente). Build de produção e desenvolvimento validados; CSS final ficou byte-idêntico em tamanho ao anterior (14.49 kB), confirmando que a extração para tokens não alterou nenhum valor computado. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 + src/index.html | 2 +- src/styles.scss | 109 +++++++++-------------------- src/styles/_breakpoints.scss | 40 +++++++++++ src/styles/_tokens.scss | 129 +++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 src/styles/_breakpoints.scss create mode 100644 src/styles/_tokens.scss diff --git a/.gitignore b/.gitignore index c661cd6..9de4a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ /out-tsc /bazel-out +# Claude Code worktrees (isolamento local de agente, nunca deve ir pro repo) +.claude/worktrees/ + # Node /node_modules npm-debug.log diff --git a/src/index.html b/src/index.html index 31faec4..7c8b541 100644 --- a/src/index.html +++ b/src/index.html @@ -12,7 +12,7 @@ - + diff --git a/src/styles.scss b/src/styles.scss index bafb0a5..0238a0f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,5 +1,7 @@ @use '@angular/material' as mat; +@use 'styles/breakpoints' as bp; +@forward 'styles/tokens'; html { @include mat.theme(( @@ -12,101 +14,56 @@ html { )); } -:root { - --pokedex-red: #FF0000; - --pokedex-red-dark: #A00000; - --pokedex-blue: #3B4CCA; - --pokedex-yellow: #FFDE00; - --pokedex-green: #2ECC71; - - --dex-ink: #232333; - --dex-cream: #FFF6E9; - --dex-cream-deep: #F3E6CC; - --dex-gold: #E8B923; - --dex-bg: #fff; - --dex-bg-card: #f5f0eb; - --dex-text: #444; - --dex-text-muted: #7a7168; - --dex-nav-active: #332F99; - --dex-badge-baby-bg: #f8d5a0; - --dex-badge-baby-text: #7a5a20; - --dex-badge-legendary-bg: #e8c840; - --dex-badge-legendary-text: #6b5500; - --dex-badge-mythical-bg: #c8a0f8; - --dex-badge-mythical-text: #502a7a; - - --dex-shadow-sm: 0 2px 8px color-mix(in srgb, var(--dex-ink) 10%, transparent); - --dex-shadow-md: 0 4px 12px color-mix(in srgb, var(--dex-ink) 14%, transparent); - --dex-shadow-lg: 0 8px 24px color-mix(in srgb, var(--dex-ink) 16%, transparent); - --dex-shadow-xl: 0 12px 32px color-mix(in srgb, var(--dex-ink) 18%, transparent); -} - -.dark-mode { - --dex-ink: #e8e8f0; - --dex-cream: #2a2a35; - --dex-cream-deep: #333340; - --dex-gold: #d4a820; - --dex-bg: #1a1a24; - --dex-bg-card: #2a2a35; - --dex-text: #d0d0d8; - --dex-text-muted: #9a9ab0; - --dex-nav-active: #9999d0; - --dex-badge-baby-bg: #b89050; - --dex-badge-baby-text: #f0d0a0; - --dex-badge-legendary-bg: #a08820; - --dex-badge-legendary-text: #f0e080; - --dex-badge-mythical-bg: #8060a0; - --dex-badge-mythical-text: #d0b0f0; -} - -// Tooltip padrão do projeto: sempre preto com texto branco, formato pill. +// Tooltip padrão do projeto: sempre preto com texto branco, formato pill, +// independente do tema claro/escuro (por isso usa --dex-black/--dex-white, +// não --dex-ink/--dex-bg). .mat-mdc-tooltip { - --mdc-tooltip-container-color: #000000; - --mdc-tooltip-supporting-text-color: #ffffff; - --mdc-tooltip-container-shape: 999px; - --mdc-tooltip-supporting-text-size: 0.75rem; - --mdc-tooltip-supporting-text-line-height: 1.1; + --mdc-tooltip-container-color: var(--dex-black); + --mdc-tooltip-supporting-text-color: var(--dex-white); + --mdc-tooltip-container-shape: var(--dex-radius-pill); + --mdc-tooltip-supporting-text-size: var(--dex-font-xs); + --mdc-tooltip-supporting-text-line-height: var(--dex-leading-tight); --mdc-tooltip-supporting-text-font: 'Nunito Sans', sans-serif; - --mdc-tooltip-supporting-text-weight: 700; + --mdc-tooltip-supporting-text-weight: var(--dex-weight-bold); } .dex-page { - padding: 16px; + padding: var(--dex-space-6); - @media (min-width: 600px) { - padding: 20px; + @include bp.up(md) { + padding: var(--dex-space-7); } } .dex-back { display: inline-flex; align-items: center; - gap: 4px; + gap: var(--dex-space-2); color: var(--pokedex-red); text-decoration: none; - font-weight: 700; - font-size: 0.8125rem; - margin-bottom: 12px; + font-weight: var(--dex-weight-bold); + font-size: var(--dex-font-sm); + margin-bottom: var(--dex-space-5); } .dex-title { - font-size: 1.25rem; - font-weight: 800; + font-size: var(--dex-font-xl); + font-weight: var(--dex-weight-black); color: var(--dex-ink); - margin: 0 0 16px; + margin: 0 0 var(--dex-space-6); } .dex-search { display: flex; align-items: center; - gap: 8px; + gap: var(--dex-space-4); flex: 1 1 220px; background: var(--dex-bg); border: 2px solid color-mix(in srgb, var(--dex-ink) 12%, transparent); - border-radius: 999px; - padding: 8px 16px; + border-radius: var(--dex-radius-pill); + padding: var(--dex-space-4) var(--dex-space-6); color: var(--dex-ink); - transition: border-color 0.15s ease; + transition: border-color var(--dex-duration-fast) var(--dex-ease-standard); &:focus-within { border-color: var(--dex-gold); @@ -117,8 +74,9 @@ html { border: none; outline: none; background: transparent; - // 16px é o mínimo para o Safari/iOS não aplicar zoom automático ao focar o input. - font-size: 1rem; + // 16px (--dex-font-base) é o mínimo para o Safari/iOS não aplicar zoom + // automático ao focar o input — não trocar por um token menor. + font-size: var(--dex-font-base); flex: 1; color: var(--dex-ink); font-family: inherit; @@ -134,7 +92,8 @@ body { background: var(--dex-bg); overflow-x: hidden; overscroll-behavior: contain; - transition: background-color 0.3s ease, color 0.3s ease; + transition: background-color var(--dex-duration-slow) var(--dex-ease-standard), + color var(--dex-duration-slow) var(--dex-ease-standard); } body.dark-mode { @@ -149,13 +108,13 @@ app-root { * { scrollbar-width: none; -ms-overflow-style: none; } ::backdrop { - transition: background-color 0.3s ease; + transition: background-color var(--dex-duration-slow) var(--dex-ease-standard); } .app-snackbar { .mdc-snackbar__surface { background: var(--dex-bg) !important; - border-radius: 999px !important; + border-radius: var(--dex-radius-pill) !important; box-shadow: var(--dex-shadow-lg); } @@ -177,7 +136,7 @@ app-root { } } -@media (max-width: 767px) { +@include bp.down(md) { .dex-search .dex-search__icon { display: none; } @@ -187,5 +146,3 @@ app-root { padding: 0; } } - - diff --git a/src/styles/_breakpoints.scss b/src/styles/_breakpoints.scss new file mode 100644 index 0000000..d52e0cc --- /dev/null +++ b/src/styles/_breakpoints.scss @@ -0,0 +1,40 @@ +// ============================================================================ +// BREAKPOINTS — fonte única de verdade de responsividade. +// +// Só existem 3: sm (celular grande/tablet pequeno), md (tablet), lg (desktop). +// Ficam em Sass (compile-time) e não em custom properties porque @media não +// aceita var() — usar sempre os mixins abaixo em vez de min-width/max-width +// literal em um componente. +// +// Uso: +// @use '../../styles/breakpoints' as bp; // ajuste o caminho relativo +// +// .card { +// padding: 12px; +// @include bp.up(md) { padding: 20px; } +// } +// ============================================================================ + +@use 'sass:map'; + +$breakpoints: ( + sm: 480px, + md: 768px, + lg: 1200px, +); + +@function bp($name) { + @return map.get($breakpoints, $name); +} + +@mixin up($name) { + @media (min-width: bp($name)) { + @content; + } +} + +@mixin down($name) { + @media (max-width: bp($name) - 1px) { + @content; + } +} diff --git a/src/styles/_tokens.scss b/src/styles/_tokens.scss new file mode 100644 index 0000000..a28ca56 --- /dev/null +++ b/src/styles/_tokens.scss @@ -0,0 +1,129 @@ +// ============================================================================ +// DESIGN TOKENS — fonte única de verdade de estilo do projeto. +// +// Nenhuma cor, tamanho de fonte, espaçamento, radius, sombra ou duração de +// transição deve ser escrito literal em um componente — sempre via var(--dex-*) +// definida aqui. Breakpoints são a única exceção: como @media não aceita +// custom properties, eles vivem em _breakpoints.scss como variáveis Sass. +// +// Convenção de nome: --dex--, escala sempre do menor pro +// maior (3xs → 3xl, sm → xl). Cores de marca mantêm o prefixo --pokedex-* +// já em uso no restante do projeto. +// ============================================================================ + +:root { + // ---- Cor de marca (Pokédex) --------------------------------------------- + --pokedex-red: #FF0000; + --pokedex-red-dark: #A00000; + --pokedex-blue: #3B4CCA; + --pokedex-yellow: #FFDE00; + --pokedex-green: #2ECC71; + + // ---- Cor de superfície / tema (light) ----------------------------------- + --dex-ink: #232333; + --dex-cream: #FFF6E9; + --dex-cream-deep: #F3E6CC; + --dex-gold: #E8B923; + --dex-bg: #fff; + --dex-bg-card: #f5f0eb; + --dex-text: #444; + --dex-text-muted: #7a7168; + --dex-nav-active: #332F99; + --dex-badge-baby-bg: #f8d5a0; + --dex-badge-baby-text: #7a5a20; + --dex-badge-legendary-bg: #e8c840; + --dex-badge-legendary-text: #6b5500; + --dex-badge-mythical-bg: #c8a0f8; + --dex-badge-mythical-text: #502a7a; + + // ---- Cor "on-image" — independe do tema claro/escuro -------------------- + // Usada quando o elemento fica sobre uma foto/artwork (ícone de favorito, + // texto sobre splash de pokémon), nunca sobre o fundo do app. + --dex-white: #ffffff; + --dex-black: #000000; + --dex-scrim: rgba(0, 0, 0, 0.45); // backdrop atrás de modal/bottom sheet + --dex-icon-on-photo: rgba(255, 255, 255, 0.9); // ícone sobre foto, estado ativo + --dex-icon-on-photo-muted: rgba(255, 255, 255, 0.5); // ícone sobre foto, estado inativo + + // ---- Tipografia ----------------------------------------------------------- + // Escala baseada nos valores já dominantes no projeto (rem, raiz 16px). + --dex-font-3xs: 0.625rem; // 10px + --dex-font-2xs: 0.6875rem; // 11px + --dex-font-xs: 0.75rem; // 12px + --dex-font-sm: 0.8125rem; // 13px + --dex-font-md: 0.875rem; // 14px + --dex-font-base: 1rem; // 16px — corpo de texto + --dex-font-lg: 1.125rem; // 18px + --dex-font-xl: 1.25rem; // 20px + --dex-font-2xl: 1.5rem; // 24px + --dex-font-3xl: 2rem; // 32px + + --dex-weight-regular: 400; + --dex-weight-semibold: 600; + --dex-weight-bold: 700; + --dex-weight-black: 800; + + --dex-leading-tight: 1.1; + --dex-leading-snug: 1.25; + --dex-leading-normal: 1.5; + --dex-leading-relaxed: 1.6; + + // ---- Espaçamento (base 2px/4px) ------------------------------------------- + --dex-space-1: 2px; + --dex-space-2: 4px; + --dex-space-3: 6px; + --dex-space-4: 8px; + --dex-space-5: 12px; + --dex-space-6: 16px; + --dex-space-7: 20px; + --dex-space-8: 24px; + --dex-space-9: 32px; + --dex-space-10: 40px; + --dex-space-11: 48px; + --dex-space-12: 64px; + + // ---- Border-radius ---------------------------------------------------- + --dex-radius-sm: 4px; + --dex-radius-md: 8px; + --dex-radius-lg: 14px; + --dex-radius-xl: 20px; + --dex-radius-sheet: 32px; // topo de bottom sheet (padrão do projeto — ver STYLEGUIDE) + --dex-radius-pill: 999px; + --dex-radius-circle: 50%; + + // ---- Sombra / elevação -------------------------------------------------- + --dex-shadow-sm: 0 2px 8px color-mix(in srgb, var(--dex-ink) 10%, transparent); + --dex-shadow-md: 0 4px 12px color-mix(in srgb, var(--dex-ink) 14%, transparent); + --dex-shadow-lg: 0 8px 24px color-mix(in srgb, var(--dex-ink) 16%, transparent); + --dex-shadow-xl: 0 12px 32px color-mix(in srgb, var(--dex-ink) 18%, transparent); + // Glow por tipo de Pokémon — depende de --type-color setado localmente no elemento. + --dex-shadow-type: 0 2px 10px color-mix(in srgb, var(--type-color, var(--dex-ink)) 40%, transparent); + + // ---- Transição / animação ------------------------------------------------ + --dex-duration-fast: 150ms; + --dex-duration-base: 250ms; + --dex-duration-slow: 400ms; + --dex-ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --dex-ease-linear: linear; +} + +.dark-mode { + --dex-ink: #e8e8f0; + --dex-cream: #2a2a35; + --dex-cream-deep: #333340; + --dex-gold: #d4a820; + --dex-bg: #1a1a24; + --dex-bg-card: #2a2a35; + --dex-text: #d0d0d8; + --dex-text-muted: #9a9ab0; + --dex-nav-active: #9999d0; + --dex-badge-baby-bg: #b89050; + --dex-badge-baby-text: #f0d0a0; + --dex-badge-legendary-bg: #a08820; + --dex-badge-legendary-text: #f0e080; + --dex-badge-mythical-bg: #8060a0; + --dex-badge-mythical-text: #d0b0f0; + + // Tokens "on-image" não mudam com o tema — fotos/artworks têm o mesmo + // tratamento em light e dark — por isso não são redeclarados aqui. +} From 4aa309f21a181fb45f8ffe1636e8d6b3257c3f63 Mon Sep 17 00:00:00 2001 From: Matheus Pompeo Date: Wed, 19 Aug 2026 13:56:24 -0300 Subject: [PATCH 2/4] feat(design-system): componente compartilhado IconButtonComponent (Fase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extrai o padrão de botão-ícone circular que estava reinventado (com pequenas variações de tamanho/cor) em pelo menos 6 arquivos (picker, team, pokemon-detail, pokemon-list, compare, app.scss). Cobre 4 tamanhos (xs/sm/md/lg) e 4 tons (ghost/on-photo/solid-accent/soft-accent) identificados nos usos existentes, documentado via JSDoc no próprio componente. Aplicado nesta rodada apenas em pokemon-picker (pp-close/pp-clear), substituindo os dois botões hand-rolled originais e removendo o CSS morto correspondente — troca mecânica, sem mudança de comportamento (verificado via build). O modal hand-rolled do próprio picker (.pp-overlay/.pp-dialog) fica como exceção documentada por ora: está fundido com a lógica de autocomplete do componente, e separar isso para usar MatDialog/CDK Overlay é refatoração de comportamento (foco, teclado, animação), não extração mecânica — decisão explícita do usuário nesta rodada. Aplicação em app.scss/team/pokemon-detail/pokemon-list/compare fica para confirmação antes de prosseguir (arquivos grandes, mais próximo do escopo da Fase 4). --- .../icon-button/icon-button.component.ts | 122 ++++++++++++++++++ .../pokemon-picker.component.html | 8 +- .../pokemon-picker.component.scss | 64 --------- .../pokemon-picker.component.ts | 3 +- 4 files changed, 126 insertions(+), 71 deletions(-) create mode 100644 src/app/shared/components/icon-button/icon-button.component.ts diff --git a/src/app/shared/components/icon-button/icon-button.component.ts b/src/app/shared/components/icon-button/icon-button.component.ts new file mode 100644 index 0000000..08381a5 --- /dev/null +++ b/src/app/shared/components/icon-button/icon-button.component.ts @@ -0,0 +1,122 @@ +import { Component, input, output } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; + +/** + * Botão-ícone circular compartilhado. Absorve o padrão que se repetia + * reinventado (com pequenas variações de tamanho/cor) em pokemon-picker, + * team, pokemon-detail, pokemon-list, compare e app.scss — ver STYLEGUIDE.md. + * + * Uso: + * + * + * + */ +@Component({ + selector: 'app-icon-button', + standalone: true, + imports: [MatIconModule], + template: ` + + `, + styles: [ + ` + .dex-icon-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + border: none; + border-radius: var(--dex-radius-circle); + cursor: pointer; + transition: background var(--dex-duration-fast) var(--dex-ease-standard), + color var(--dex-duration-fast) var(--dex-ease-standard); + + // Expande a área de toque pra perto de 44x44px sem aumentar o botão + // visualmente — mesmo truque que já existia nos botões reinventados. + &::before { + content: ''; + position: absolute; + inset: -6px; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + mat-icon { + display: block; + } + } + + // ---- Tamanho ------------------------------------------------------- + .dex-icon-btn--xs { width: 24px; height: 24px; mat-icon { font-size: 14px; width: 14px; height: 14px; } } + .dex-icon-btn--sm { width: 32px; height: 32px; mat-icon { font-size: 18px; width: 18px; height: 18px; } } + .dex-icon-btn--md { width: 40px; height: 40px; mat-icon { font-size: 20px; width: 20px; height: 20px; } } + .dex-icon-btn--lg { width: 44px; height: 44px; mat-icon { font-size: 22px; width: 22px; height: 22px; } } + + // ---- Tom ------------------------------------------------------------- + // ghost: uso padrão sobre fundo do app (cards, painéis). + .dex-icon-btn--ghost { + background: color-mix(in srgb, var(--dex-ink) 8%, transparent); + color: var(--dex-ink); + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--dex-ink) 16%, transparent); + } + } + + // on-photo: sobre foto/artwork de Pokémon (hero, splash). + .dex-icon-btn--on-photo { + background: color-mix(in srgb, var(--dex-white) 20%, transparent); + color: var(--dex-white); + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--dex-white) 30%, transparent); + } + } + + // solid-accent: ação primária de destaque (ex.: voltar ao topo). + .dex-icon-btn--solid-accent { + background: var(--pokedex-red); + color: var(--dex-white); + + &:hover:not(:disabled) { + background: var(--pokedex-red-dark); + } + } + + // soft-accent: ação de destaque, tom suave (ex.: confirmar, tentar de novo). + .dex-icon-btn--soft-accent { + background: color-mix(in srgb, var(--pokedex-red) 10%, transparent); + color: color-mix(in srgb, var(--pokedex-red) 55%, var(--dex-bg)); + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--pokedex-red) 18%, transparent); + } + } + `, + ], +}) +export class IconButtonComponent { + /** Nome do ícone Material (ex.: 'close', 'favorite', 'refresh'). */ + icon = input.required(); + /** Obrigatório — botão-ícone não tem texto visível, precisa de label acessível. */ + ariaLabel = input.required(); + size = input<'xs' | 'sm' | 'md' | 'lg'>('sm'); + tone = input<'ghost' | 'on-photo' | 'solid-accent' | 'soft-accent'>('ghost'); + disabled = input(false); + + clicked = output(); +} diff --git a/src/app/shared/components/pokemon-picker/pokemon-picker.component.html b/src/app/shared/components/pokemon-picker/pokemon-picker.component.html index 0a5860f..eef8eb3 100644 --- a/src/app/shared/components/pokemon-picker/pokemon-picker.component.html +++ b/src/app/shared/components/pokemon-picker/pokemon-picker.component.html @@ -9,9 +9,7 @@

{{ title() }}

{{ hint() }}

} - + @@ -40,9 +38,7 @@

{{ title() }}

(blur)="onBlur()" /> @if (clearable() && query()) { - + } @if (filterButton()) { diff --git a/src/app/shared/components/pokemon-picker/pokemon-picker.component.scss b/src/app/shared/components/pokemon-picker/pokemon-picker.component.scss index 7123991..b022725 100644 --- a/src/app/shared/components/pokemon-picker/pokemon-picker.component.scss +++ b/src/app/shared/components/pokemon-picker/pokemon-picker.component.scss @@ -104,70 +104,6 @@ color: var(--dex-text-muted); } -.pp-close { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - flex-shrink: 0; - border: none; - border-radius: 50%; - background: color-mix(in srgb, var(--dex-ink) 8%, transparent); - color: var(--dex-ink); - cursor: pointer; - - // Expande a área de toque para 44x44px sem aumentar o botão visualmente. - &::before { - content: ''; - position: absolute; - inset: -6px; - } - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - - &:hover { - background: color-mix(in srgb, var(--dex-ink) 16%, transparent); - } -} - -.pp-clear { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - flex-shrink: 0; - border: none; - border-radius: 50%; - background: color-mix(in srgb, var(--dex-ink) 8%, transparent); - color: var(--dex-ink); - cursor: pointer; - - // Expande a área de toque para 44x44px sem aumentar o botão visualmente. - &::before { - content: ''; - position: absolute; - inset: -10px; - } - - mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - } - - &:hover { - background: color-mix(in srgb, var(--dex-ink) 16%, transparent); - } -} - .pp-filter-btn { position: relative; display: flex; diff --git a/src/app/shared/components/pokemon-picker/pokemon-picker.component.ts b/src/app/shared/components/pokemon-picker/pokemon-picker.component.ts index ec1a5fe..0b2d326 100644 --- a/src/app/shared/components/pokemon-picker/pokemon-picker.component.ts +++ b/src/app/shared/components/pokemon-picker/pokemon-picker.component.ts @@ -11,13 +11,14 @@ import { loadDataParts } from '../../load-data-parts'; import { formatPokemonId } from '../../format-utils'; import { TypeBadgeComponent } from '../type-badge/type-badge.component'; import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; +import { IconButtonComponent } from '../icon-button/icon-button.component'; const ATTACKING_TYPES = Object.keys(TYPE_CHART); @Component({ selector: 'app-pokemon-picker', standalone: true, - imports: [NgTemplateOutlet, MatIconModule, MatTooltipModule, TypeBadgeComponent, LoadingSpinnerComponent], + imports: [NgTemplateOutlet, MatIconModule, MatTooltipModule, TypeBadgeComponent, LoadingSpinnerComponent, IconButtonComponent], templateUrl: './pokemon-picker.component.html', styleUrl: './pokemon-picker.component.scss', host: { From 3386188999789422238f09b4a9ef925752a10cda Mon Sep 17 00:00:00 2001 From: Matheus Pompeo Date: Wed, 19 Aug 2026 17:17:08 -0300 Subject: [PATCH 3/4] =?UTF-8?q?feat(design-system):=20estende=20IconButton?= =?UTF-8?q?Component=20aos=20demais=20bot=C3=B5es-=C3=ADcone=20(Fase=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aplica o componente compartilhado (commit anterior) nos 4 pontos onde o padrão batia exatamente, eliminando o CSS reinventado correspondente: - app.html: botão de alternar tema (ghost, lg) - pokemon-detail: botão "Voltar" (tom novo `transparent` — sem círculo de fundo, diferente do nav-btn/action-btn; ver nota abaixo) e botão de adicionar/remover do time (on-photo, lg, com estado --active) - team: botão de fechar o painel de análise (ghost, sm) Novo tom `transparent` no IconButtonComponent: ao mapear pdetail__back pro tom on-photo por engano (mesmo tamanho 44px dos vizinhos), pré-build percebi que o CSS original não tinha nenhum background — é só uma área de toque de 44px com ícone branco. Corrigido antes do commit; teria sido uma regressão visual sutil (círculo aparecendo onde nunca existiu). Também incorporado ao componente: outline dourado de :focus-visible que os 5+ botões reinventados repetiam individualmente. Validado com build limpo + checagem visual via Chrome (tema, voltar, adicionar ao time, fechar análise) nas 3 telas afetadas. Deixados de fora deste corte, com motivo: - pdetail__nav-btn (prev/next): é ``, não ` + diff --git a/src/app/app.scss b/src/app/app.scss index 3edad87..84645fb 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -70,38 +70,6 @@ align-items: center; } - &__theme-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - padding: 0; - border: none; - border-radius: 50%; - background: color-mix(in srgb, var(--dex-ink) 8%, transparent); - color: var(--dex-ink); - cursor: pointer; - flex-shrink: 0; - position: relative; - z-index: 1; - - &:focus-visible { - outline: 2px solid var(--dex-gold); - outline-offset: 2px; - } - - mat-icon { - font-size: 20px; - width: 20px; - height: 20px; - } - - &:hover { - background: color-mix(in srgb, var(--dex-ink) 16%, transparent); - } - } - &__nav-divider { align-self: center; flex-shrink: 0; diff --git a/src/app/app.ts b/src/app/app.ts index bde5719..09d4248 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -9,6 +9,7 @@ import { FavoritesService } from './core/services/favorites.service'; import { PageBackgroundService } from './core/services/page-background.service'; import { NavigationService } from './core/services/navigation.service'; import { routeFade } from './shared/animations'; +import { IconButtonComponent } from './shared/components/icon-button/icon-button.component'; /** * Resolve qualquer cor CSS (color-mix(), color(srgb ...), etc.) para um @@ -32,7 +33,7 @@ function resolveToRgb(cssColor: string): string { @Component({ selector: 'app-root', - imports: [RouterOutlet, RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule], + imports: [RouterOutlet, RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule, IconButtonComponent], templateUrl: './app.html', styleUrl: './app.scss', animations: [routeFade], diff --git a/src/app/features/pokemon-detail/pokemon-detail.component.html b/src/app/features/pokemon-detail/pokemon-detail.component.html index 65649b9..904a902 100644 --- a/src/app/features/pokemon-detail/pokemon-detail.component.html +++ b/src/app/features/pokemon-detail/pokemon-detail.component.html @@ -35,9 +35,7 @@
- +
- +
- +
@if (statsById().get(id); as stats) { diff --git a/src/app/features/team/team.component.scss b/src/app/features/team/team.component.scss index 6bb5505..01ffc39 100644 --- a/src/app/features/team/team.component.scss +++ b/src/app/features/team/team.component.scss @@ -716,37 +716,6 @@ margin-top: 4px; } -.team__analysis-close { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - flex-shrink: 0; - border: none; - border-radius: 50%; - background: color-mix(in srgb, var(--dex-ink) 8%, transparent); - color: var(--dex-ink); - cursor: pointer; - - // Expande a área de toque para 44x44px sem aumentar o botão visualmente. - &::before { - content: ''; - position: absolute; - inset: -6px; - } - - mat-icon { - font-size: 18px; - width: 18px; - height: 18px; - } - - &:hover { - background: color-mix(in srgb, var(--dex-ink) 16%, transparent); - } -} .team__analysis-stats { display: grid; diff --git a/src/app/features/team/team.component.ts b/src/app/features/team/team.component.ts index 2423f64..7e86844 100644 --- a/src/app/features/team/team.component.ts +++ b/src/app/features/team/team.component.ts @@ -16,6 +16,7 @@ import { TypeBadgeComponent } from '../../shared/components/type-badge/type-badg import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; import { PokemonPickerComponent } from '../../shared/components/pokemon-picker/pokemon-picker.component'; import { PokemonCardComponent } from '../../shared/components/pokemon-card/pokemon-card.component'; +import { IconButtonComponent } from '../../shared/components/icon-button/icon-button.component'; const ATTACKING_TYPES = Object.keys(TYPE_CHART); @@ -30,6 +31,7 @@ const ATTACKING_TYPES = Object.keys(TYPE_CHART); LoadingSpinnerComponent, PokemonPickerComponent, PokemonCardComponent, + IconButtonComponent, ], templateUrl: './team.component.html', styleUrl: './team.component.scss', diff --git a/src/app/shared/components/icon-button/icon-button.component.ts b/src/app/shared/components/icon-button/icon-button.component.ts index 08381a5..87f1898 100644 --- a/src/app/shared/components/icon-button/icon-button.component.ts +++ b/src/app/shared/components/icon-button/icon-button.component.ts @@ -1,27 +1,38 @@ import { Component, input, output } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule, TooltipPosition } from '@angular/material/tooltip'; /** * Botão-ícone circular compartilhado. Absorve o padrão que se repetia * reinventado (com pequenas variações de tamanho/cor) em pokemon-picker, - * team, pokemon-detail, pokemon-list, compare e app.scss — ver STYLEGUIDE.md. + * team, pokemon-detail, pokemon-list e app.scss — ver STYLEGUIDE.md. + * + * Não cobre: botões de navegação que precisam ser `` (ex.: + * pdetail__nav-btn) — semântica de link importa (abrir em nova aba, SEO); + * nem o par compacto de 28px com hover destrutivo do team__slot-action-btn + * (variante de tamanho própria, migrada junto da Fase 4 de `team`). * * Uso: * * * + * + * */ @Component({ selector: 'app-icon-button', standalone: true, - imports: [MatIconModule], + imports: [MatIconModule, MatTooltipModule], template: `