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/app/app.html b/src/app/app.html
index d3f19a1..2ddaddb 100644
--- a/src/app/app.html
+++ b/src/app/app.html
@@ -71,16 +71,13 @@
Time
-
+
diff --git a/src/app/app.scss b/src/app/app.scss
index 3edad87..16e29f3 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -1,3 +1,5 @@
+@use '../styles/breakpoints' as bp;
+
:host {
display: block;
min-height: 100dvh;
@@ -70,38 +72,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;
@@ -234,7 +204,7 @@
}
-@media (min-width: 768px) {
+@include bp.up(md) {
.pokedex-frame {
&__content--sheet {
padding-bottom: 32px;
@@ -280,7 +250,7 @@
}
}
-@media (min-width: 1200px) {
+@include bp.up(lg) {
.pokedex-frame {
max-width: 1200px;
margin: 0 auto;
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/compare/compare.component.scss b/src/app/features/compare/compare.component.scss
index 10b622c..dcfad57 100644
--- a/src/app/features/compare/compare.component.scss
+++ b/src/app/features/compare/compare.component.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/breakpoints' as bp;
+
.compare {
position: relative;
width: 100%;
@@ -352,7 +354,7 @@
}
}
-@media (max-width: 767px) {
+@include bp.down(md) {
.dex-search .dex-search__icon {
display: inline-flex;
flex-shrink: 0;
@@ -459,7 +461,7 @@
}
}
-@media (max-width: 767px) {
+@include bp.down(md) {
.compare__search-row {
gap: 6px;
@@ -469,7 +471,7 @@
}
}
-@media (min-width: 768px) {
+@include bp.up(md) {
.compare__hero-bg {
height: 460px;
}
diff --git a/src/app/features/favorites/favorites.component.scss b/src/app/features/favorites/favorites.component.scss
index 1a6f40f..dfa7d2a 100644
--- a/src/app/features/favorites/favorites.component.scss
+++ b/src/app/features/favorites/favorites.component.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/breakpoints' as bp;
+
.favorites__grid {
display: grid;
grid-template-columns: 1fr;
@@ -112,7 +114,7 @@
50% { transform: translateY(-8px); }
}
-@media (min-width: 480px) {
+@include bp.up(sm) {
.favorites__grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
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..b4863eb 100644
--- a/src/app/features/team/team.component.scss
+++ b/src/app/features/team/team.component.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/breakpoints' as bp;
+
.team-page {
max-width: 1200px;
margin: 0 auto;
@@ -716,37 +718,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;
@@ -1005,14 +976,14 @@
50% { transform: translateY(-8px); }
}
-@media (min-width: 480px) {
+@include bp.up(sm) {
.team__grid {
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
}
-@media (min-width: 768px) {
+@include bp.up(md) {
.team__coverage {
grid-template-columns: repeat(2, 1fr);
}
@@ -1029,7 +1000,7 @@
}
}
-@media (min-width: 1200px) {
+@include bp.up(lg) {
.team__coverage {
grid-template-columns: repeat(4, 1fr);
}
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
new file mode 100644
index 0000000..87f1898
--- /dev/null
+++ b/src/app/shared/components/icon-button/icon-button.component.ts
@@ -0,0 +1,157 @@
+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 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, MatTooltipModule],
+ 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;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--dex-gold);
+ outline-offset: 2px;
+ }
+
+ 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) 35%, transparent);
+ }
+
+ // Estado ativo (ex.: já está no time) — mesmo tratamento em toda
+ // ação on-photo que alterna estado, não só uma tela específica.
+ &.dex-icon-btn--active {
+ background: var(--dex-gold);
+ color: var(--dex-black);
+ }
+ }
+
+ // transparent: hit-area de 44px sem fundo, ícone branco sobre foto/hero
+ // (ex.: voltar no topo do detalhe) — sem estado de hover, igual ao original.
+ .dex-icon-btn--transparent {
+ background: transparent;
+ color: var(--dex-white);
+ }
+
+ // 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' | 'transparent'>('ghost');
+ disabled = input(false);
+ /** Estado "ligado" de um botão que alterna (ex.: já favoritado/no time) — só visual no tom on-photo. */
+ active = input(false);
+ /** Texto do tooltip do Material. Vazio = sem tooltip (padrão do próprio MatTooltip). */
+ tooltip = input('');
+ tooltipPosition = input('above');
+
+ 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: {
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.
+}