Claude-skill-registry frontend-crafter

Especialista em Frontend Mobile-First, UX Black & Orange, Sistema de Cache Offline (IndexedDB), Navegação SPA v3.0 e Performance. Use para criar/ajustar telas, componentes, otimizar CSS/JS, implementar patterns de cache ou debugging de frontend issues.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-crafter" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-crafter && rm -rf "$T"
manifest: skills/data/frontend-crafter/SKILL.md
source content

Frontend Crafter Skill (Mobile-First Master)

🎯 Missão

Criar experiências frontend excepcionais para o Super Cartola Manager com foco em mobile-first, performance e UX consistente.


1. 🎨 Design System - Black & Orange

1.1 Paleta de Cores

:root {
  /* === PRIMÁRIAS === */
  --laranja: #FF4500;           /* Cor principal */
  --laranja-hover: #FF5500;     /* Hover states */
  --laranja-dark: #CC3700;      /* Variação escura */
  
  /* === BACKGROUNDS === */
  --bg-card: #1a1a1a;           /* Cards dark */
  --bg-secondary: #2a2a2a;      /* Seções alternadas */
  --bg-overlay: rgba(0,0,0,0.8);/* Modals/overlays */
  
  /* === STATUS === */
  --verde-lucro: #10b981;       /* Lucro/Vitória */
  --vermelho-prejuizo: #ef4444; /* Prejuízo/Derrota */
  --amarelo-neutro: #f59e0b;    /* Neutro/Atenção */
  
  /* === TEXTOS === */
  --text-primary: #ffffff;
  --text-secondary: #a0a0a0;
  --text-muted: #666666;
  
  /* === BORDAS === */
  --border-color: #333333;
  --border-radius: 12px;
}

1.2 Typography (TRES FONTES OBRIGATORIAS)

/* OBRIGATORIO: Sistema de 3 fontes */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Russo+One&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');

:root {
  /* === FAMILIAS DE FONTE === */
  --font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-family-brand: 'Russo One', sans-serif;  /* Titulos, stats, CTAs */
  --font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;  /* Codigo, numeros */
}

body {
  font-family: var(--font-family-base);
  -webkit-font-smoothing: antialiased;
}

/* INTER - Corpo de texto (padrao) */
.body { font-family: var(--font-family-base); font-size: 16px; font-weight: 400; }
.small { font-family: var(--font-family-base); font-size: 14px; font-weight: 400; }
.caption { font-family: var(--font-family-base); font-size: 12px; font-weight: 500; }

/* RUSSO ONE - Titulos e destaques (brand) */
.font-brand { font-family: var(--font-family-brand); font-weight: 400; }
.h1 { font-family: var(--font-family-brand); font-size: 28px; letter-spacing: 0.5px; }
.h2 { font-family: var(--font-family-brand); font-size: 24px; letter-spacing: 0.5px; }
.h3 { font-family: var(--font-family-brand); font-size: 20px; letter-spacing: 0.3px; }

/* JETBRAINS MONO - Codigo e numeros tabulares */
.font-mono { font-family: var(--font-family-mono); }
.tabular-nums { font-variant-numeric: tabular-nums; }

REGRA: Russo One so tem peso 400, usar

letter-spacing
para ajustar espacamento.

1.3 Componentes Base

<!-- Card Padrão -->
<div class="card">
  <div class="card-header">
    <h3>Título</h3>
  </div>
  <div class="card-body">
    <!-- Conteúdo -->
  </div>
</div>

<style>
.card {
  background: var(--bg-card);
  border-radius: var(--border-radius);
  padding: 20px;
  margin-bottom: 16px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
</style>

<!-- Botão Primário -->
<button class="btn-primary">
  <i class="material-icons">check</i>
  Confirmar
</button>

<style>
.btn-primary {
  background: linear-gradient(135deg, var(--laranja), var(--laranja-dark));
  color: white;
  border: none;
  border-radius: 8px;
  padding: 12px 24px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  transition: all 0.2s;
}
.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
}
</style>

1.4 Icons - Material Icons OBRIGATÓRIO

<!-- NUNCA usar emojis, SEMPRE Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

<!-- Exemplos -->
<i class="material-icons">home</i>
<i class="material-icons">trophy</i>
<i class="material-icons">account_balance_wallet</i>
<i class="material-icons">bar_chart</i>

2. 📱 Arquitetura Mobile SPA v3.0

2.1 Estrutura de Fragmentos

public/participante/
├── fronts/                    # Templates (fragmentos HTML)
│   ├── home.html
│   ├── ranking.html
│   ├── extrato.html
│   └── perfil.html
├── core/
│   ├── navigation.js          # Sistema de navegação v3.0
│   ├── cache-manager.js       # IndexedDB manager
│   └── api-client.js          # HTTP client
└── modules/
    ├── ranking/
    │   ├── ranking.js         # Lógica do módulo
    │   └── ranking.css        # Estilos específicos
    └── extrato/
        └── ...

IMPORTANTE: Fragmentos são HTML puro sem

<html>
,
<head>
ou
<body>
.

<!-- ✅ CORRETO: fronts/ranking.html -->
<div id="ranking-container">
  <h2>Ranking</h2>
  <div class="ranking-list"></div>
</div>

<!-- ❌ ERRADO -->
<!DOCTYPE html>
<html>
  <head>...</head>
  <body>...</body>
</html>

2.2 Navegação SPA v3.0

// participante-navigation.js
class NavigationManager {
  constructor() {
    this.currentPage = null;
    this.debounceTimer = null;
    this.DEBOUNCE_DELAY = 100; // ms
  }
  
  async navigate(page, skipHistory = false) {
    // Debounce - NUNCA usar flag de travamento
    clearTimeout(this.debounceTimer);
    
    this.debounceTimer = setTimeout(async () => {
      await this._doNavigate(page, skipHistory);
    }, this.DEBOUNCE_DELAY);
  }
  
  async _doNavigate(page, skipHistory) {
    // Validar página
    const validPages = ['home', 'ranking', 'extrato', 'perfil'];
    if (!validPages.includes(page)) {
      console.error('Página inválida:', page);
      return;
    }
    
    // Evitar navegação duplicada
    if (this.currentPage === page) return;
    
    try {
      // 1. Loading state
      this.showLoading();
      
      // 2. Carregar fragmento
      const html = await fetch(`/participante/fronts/${page}.html`).then(r => r.text());
      
      // 3. Renderizar
      document.getElementById('main-content').innerHTML = html;
      
      // 4. Executar módulo específico
      await this.loadModule(page);
      
      // 5. Atualizar history
      if (!skipHistory) {
        window.history.pushState({ page }, '', `#${page}`);
      }
      
      // 6. Atualizar nav
      this.updateActiveNav(page);
      this.currentPage = page;
      
    } catch (error) {
      console.error('Erro ao navegar:', error);
      this.showError();
    } finally {
      this.hideLoading();
    }
  }
  
  async loadModule(page) {
    // Carregar script do módulo dinamicamente
    if (typeof window[`${page}Module`] === 'object') {
      await window[`${page}Module`].init();
    }
  }
  
  showLoading() {
    // Glass overlay - OBRIGATÓRIO em reloads
    const overlay = document.createElement('div');
    overlay.id = 'loading-overlay';
    overlay.className = 'glass-overlay';
    overlay.innerHTML = '<div class="spinner"></div>';
    document.body.appendChild(overlay);
  }
  
  hideLoading() {
    document.getElementById('loading-overlay')?.remove();
  }
}

// Interceptar botão voltar
window.addEventListener('popstate', (e) => {
  if (e.state && e.state.page) {
    navManager.navigate(e.state.page, true);
  }
});

2.3 Loading States

<!-- Splash Screen (apenas 1ª visita) -->
<div id="splash-screen" class="splash">
  <img src="/img/logo.png" alt="Super Cartola">
  <div class="spinner"></div>
</div>

<!-- Glass Overlay (reloads/PTR) -->
<div class="glass-overlay">
  <div class="spinner"></div>
</div>

<style>
.splash {
  position: fixed;
  inset: 0;
  background: #000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.glass-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.8);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.spinner {
  width: 48px;
  height: 48px;
  border: 4px solid rgba(255, 69, 0, 0.3);
  border-top-color: var(--laranja);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
</style>

3. 💾 Performance & Cache (IndexedDB)

3.1 Cache Strategy - Cache-First

// cache-manager.js
class CacheManager {
  constructor() {
    this.dbName = 'super_cartola_cache';
    this.version = 1;
    this.db = null;
  }
  
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        
        // Criar stores
        if (!db.objectStoreNames.contains('participante')) {
          db.createObjectStore('participante', { keyPath: 'id' });
        }
        if (!db.objectStoreNames.contains('ranking')) {
          db.createObjectStore('ranking', { keyPath: 'key' });
        }
        if (!db.objectStoreNames.contains('extrato')) {
          db.createObjectStore('extrato', { keyPath: 'key' });
        }
      };
    });
  }
  
  async get(store, key) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([store], 'readonly');
      const objectStore = transaction.objectStore(store);
      const request = objectStore.get(key);
      
      request.onsuccess = () => {
        const data = request.result;
        
        // Verificar TTL
        if (data && this.isExpired(data)) {
          this.delete(store, key);
          resolve(null);
        } else {
          resolve(data);
        }
      };
      request.onerror = () => reject(request.error);
    });
  }
  
  async set(store, data, ttl = null) {
    const record = {
      ...data,
      _timestamp: Date.now(),
      _ttl: ttl || this.getTTL(store)
    };
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([store], 'readwrite');
      const objectStore = transaction.objectStore(store);
      const request = objectStore.put(record);
      
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
  
  isExpired(data) {
    if (!data._timestamp || !data._ttl) return false;
    return Date.now() - data._timestamp > data._ttl;
  }
  
  getTTL(store) {
    const TTL_MAP = {
      participante: 24 * 60 * 60 * 1000,  // 24h
      liga: 24 * 60 * 60 * 1000,          // 24h
      ranking: 60 * 60 * 1000,            // 1h
      extrato: 30 * 60 * 1000             // 30min
    };
    return TTL_MAP[store] || 60 * 60 * 1000; // default 1h
  }
}

// Cache-First Pattern
async function loadRanking() {
  // 1. Tentar cache (render instantâneo)
  const cached = await cacheManager.get('ranking', 'current');
  if (cached) {
    renderRanking(cached.data);
  }
  
  // 2. Fetch fresh (background)
  try {
    const fresh = await fetch('/api/ranking').then(r => r.json());
    await cacheManager.set('ranking', { key: 'current', data: fresh });
    
    // 3. Re-render se mudou
    if (!cached || JSON.stringify(cached.data) !== JSON.stringify(fresh)) {
      renderRanking(fresh);
    }
  } catch (error) {
    // Se fetch falhar e temos cache, continuar com cache
    if (!cached) {
      showError('Não foi possível carregar dados');
    }
  }
}

3.2 TTL por Módulo

const CACHE_TTL = {
  // Dados estáticos/semi-estáticos
  participante: 24 * 60 * 60 * 1000,    // 24h
  liga: 24 * 60 * 60 * 1000,            // 24h
  config: 7 * 24 * 60 * 60 * 1000,      // 7 dias
  
  // Dados dinâmicos
  ranking: 60 * 60 * 1000,              // 1h
  extrato: 30 * 60 * 1000,              // 30min
  rodadaAtual: 10 * 60 * 1000,          // 10min
  
  // Dados em tempo real
  liveFeed: 60 * 1000                   // 1min
};

4. 📱 Componentes Mobile Premium (v3.2)

4.1 Header com Avatar e Badge

<!-- Header Premium com identidade do usuario -->
<header class="header-premium">
  <div class="header-content">
    <!-- Avatar com Iniciais -->
    <div class="user-section">
      <div class="avatar-circle">
        <span class="avatar-initials font-brand">PM</span>
      </div>
      <div class="user-info">
        <h1 class="user-name font-brand">Paulinett Miranda</h1>
        <div class="badge-premium">
          <i class="material-icons badge-icon">star</i>
          <span>Premium</span>
        </div>
      </div>
    </div>
    <!-- Notificacoes -->
    <button class="btn-icon" aria-label="Notificacoes">
      <i class="material-icons">notifications</i>
    </button>
  </div>
</header>

<style>
.header-premium {
  padding: 48px 16px 24px;
  background: #000;
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--border-color);
}

.header-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.user-section {
  display: flex;
  align-items: center;
  gap: 12px;
}

.avatar-circle {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #bdc3c7;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--bg-card);
}

.avatar-initials {
  color: #1a1a1a;
  font-size: 14px;
}

.user-name {
  font-size: 18px;
  color: var(--text-primary);
  line-height: 1.2;
}

.badge-premium {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 2px;
}

.badge-premium .badge-icon {
  font-size: 12px;
  color: #f59e0b;
}

.badge-premium span {
  font-size: 10px;
  color: #f59e0b;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.btn-icon {
  padding: 8px;
  background: transparent;
  border: none;
  color: var(--text-primary);
  cursor: pointer;
}

.btn-icon .material-icons {
  font-size: 24px;
}
</style>

4.2 Grid de Atalhos (4 colunas)

<!-- Grid de acoes rapidas -->
<section class="action-grid">
  <button class="action-item">
    <div class="action-icon">
      <i class="material-icons">emoji_events</i>
    </div>
    <span class="action-label">Premiacoes</span>
  </button>
  <button class="action-item">
    <div class="action-icon">
      <i class="material-icons">groups</i>
    </div>
    <span class="action-label">Participantes</span>
  </button>
  <button class="action-item">
    <div class="action-icon">
      <i class="material-icons">description</i>
    </div>
    <span class="action-label">Regras</span>
  </button>
  <button class="action-item">
    <div class="action-icon">
      <i class="material-icons">workspace_premium</i>
    </div>
    <span class="action-label">Cartola PRO</span>
  </button>
</section>

<style>
.action-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  padding: 0 16px;
}

.action-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  cursor: pointer;
}

.action-icon {
  width: 56px;
  height: 56px;
  border-radius: 12px;
  border: 1px solid var(--laranja);
  background: var(--bg-card);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}

.action-icon:hover {
  background: var(--bg-secondary);
}

.action-icon .material-icons {
  font-size: 24px;
  color: var(--laranja);
}

.action-label {
  font-size: 10px;
  color: var(--text-secondary);
  font-weight: 500;
  text-align: center;
}
</style>

4.3 Card de Status do Time (Split Layout)

<!-- Card com Pontos e Posicao lado a lado -->
<section class="team-status-card">
  <div class="team-header">
    <h2 class="team-name font-brand">Urubu Play F.C.</h2>
    <div class="team-shield">
      <i class="material-icons">shield</i>
    </div>
  </div>
  <div class="stats-split">
    <div class="stat-block">
      <span class="stat-label">Pontos</span>
      <span class="stat-value font-mono text-accent">0</span>
      <span class="stat-hint">Aguardando 1a rodada</span>
    </div>
    <div class="stat-divider"></div>
    <div class="stat-block">
      <span class="stat-label">Posicao</span>
      <span class="stat-value font-mono">--</span>
      <span class="stat-hint">Aguardando 1a rodada</span>
    </div>
  </div>
</section>

<style>
.team-status-card {
  background: var(--bg-card);
  border-radius: var(--border-radius);
  padding: 20px;
  margin: 16px;
  border: 1px solid var(--border-color);
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}

.team-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.team-name {
  font-size: 20px;
  color: var(--text-primary);
}

.team-shield {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--bg-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.team-shield .material-icons {
  font-size: 16px;
  color: var(--text-muted);
}

.stats-split {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 16px;
}

.stat-block {
  display: flex;
  flex-direction: column;
}

.stat-label {
  font-size: 12px;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 4px;
}

.stat-value {
  font-size: 48px;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1;
}

.stat-value.text-accent {
  color: var(--laranja);
}

.stat-hint {
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 4px;
}

.stat-divider {
  width: 1px;
  background: var(--border-color);
}
</style>

4.4 FAB do Mercado (Floating Action Button)

<!-- FAB com Timer do Mercado -->
<div class="fab-mercado">
  <button class="fab-btn">
    <div class="fab-icon">
      <i class="material-icons">storefront</i>
    </div>
    <div class="fab-content">
      <span class="fab-timer">Fecha em 7d 5h</span>
      <span class="fab-status font-brand">Aberto R1</span>
    </div>
  </button>
</div>

<style>
.fab-mercado {
  position: fixed;
  bottom: 96px; /* Acima do bottom nav */
  right: 16px;
  z-index: 40;
}

.fab-btn {
  background: linear-gradient(135deg, var(--verde-lucro), #27ae60);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 24px;
  padding: 10px 24px 10px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 180px;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4);
  transition: transform 0.2s;
}

.fab-btn:hover {
  transform: scale(1.05);
}

.fab-icon {
  width: 32px;
  height: 32px;
  background: rgba(255,255,255,0.2);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.fab-icon .material-icons {
  font-size: 16px;
  color: #fff;
}

.fab-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.fab-timer {
  font-size: 10px;
  color: rgba(255,255,255,0.8);
  text-transform: uppercase;
  letter-spacing: 0.3px;
}

.fab-status {
  font-size: 14px;
  color: #fff;
  text-transform: uppercase;
}
</style>

4.5 Card de Jogo (Match Card)

<!-- Card de partida com escudos -->
<div class="match-card">
  <div class="match-league">Brasileirao Serie A</div>
  <div class="match-content">
    <!-- Time 1 -->
    <div class="match-team">
      <div class="team-badge" style="background-color: #c8102e;">
        <span>FLA</span>
      </div>
      <span class="team-name-short">Flamengo</span>
    </div>
    <!-- VS e Horario -->
    <div class="match-center">
      <span class="match-vs">VS</span>
      <span class="match-time font-mono">16:00</span>
    </div>
    <!-- Time 2 -->
    <div class="match-team">
      <div class="team-badge" style="background-color: #006437;">
        <span>PAL</span>
      </div>
      <span class="team-name-short">Palmeiras</span>
    </div>
  </div>
</div>

<style>
.match-card {
  background: var(--bg-secondary);
  border-radius: 12px;
  padding: 12px;
  border: 1px solid var(--border-color);
}

.match-league {
  font-size: 11px;
  color: var(--text-muted);
  padding-bottom: 8px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border-color);
}

.match-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.match-team {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 33%;
}

.team-badge {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 4px;
}

.team-badge span {
  font-size: 10px;
  font-weight: 700;
  color: #fff;
}

.team-name-short {
  font-size: 10px;
  color: var(--text-primary);
  text-align: center;
}

.match-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 33%;
}

.match-vs {
  font-size: 12px;
  color: var(--laranja);
  font-weight: 700;
  margin-bottom: 2px;
}

.match-time {
  font-size: 18px;
  font-weight: 700;
  color: var(--text-primary);
}
</style>

4.6 Bottom Navigation (4 itens)

<!-- Bottom Nav fixo -->
<nav class="bottom-nav">
  <a href="#home" class="nav-item active">
    <i class="material-icons">home</i>
    <span>Inicio</span>
  </a>
  <a href="#ranking" class="nav-item">
    <i class="material-icons">leaderboard</i>
    <span>Ranking</span>
  </a>
  <a href="#menu" class="nav-item">
    <i class="material-icons">apps</i>
    <span>Menu</span>
  </a>
  <a href="#financeiro" class="nav-item">
    <i class="material-icons">account_balance_wallet</i>
    <span>Financeiro</span>
  </a>
</nav>

<style>
.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 64px;
  background: #000;
  border-top: 1px solid var(--border-color);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  z-index: 50;
  padding-bottom: env(safe-area-inset-bottom);
}

.bottom-nav .nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.2s;
}

.bottom-nav .nav-item.active,
.bottom-nav .nav-item:hover {
  color: var(--laranja);
}

.bottom-nav .nav-item .material-icons {
  font-size: 22px;
}

.bottom-nav .nav-item span {
  font-size: 10px;
  font-weight: 500;
}
</style>

4.7 Mapeamento Font Awesome → Material Icons

Font AwesomeMaterial IconsUso
fa-trophy
emoji_events
Premiacoes
fa-users
groups
Participantes
fa-clipboard-list
description
Regras
fa-crown
workspace_premium
Premium
fa-home
home
Inicio
fa-chart-line
leaderboard
Ranking
fa-th
apps
Menu
fa-wallet
account_balance_wallet
Financeiro
fa-shop
storefront
Mercado
fa-coins
payments
Saldo
fa-bell
notifications
Alertas
fa-shield-alt
shield
Escudo

5. 🎭 Admin UI (Desktop)

4.1 Layout Padrão

<div class="admin-layout">
  <aside class="sidebar">
    <div class="logo">
      <img src="/img/logo.png" alt="SC">
      <h3>Super Cartola</h3>
    </div>
    <nav>
      <a href="#dashboard" class="nav-item active">
        <i class="material-icons">dashboard</i>
        Dashboard
      </a>
      <a href="#ligas" class="nav-item">
        <i class="material-icons">emoji_events</i>
        Ligas
      </a>
      <!-- ... -->
    </nav>
  </aside>
  
  <main class="main-content">
    <header class="topbar">
      <h1>Dashboard</h1>
      <div class="user-menu">...</div>
    </header>
    
    <div class="content">
      <!-- Módulo renderizado aqui -->
    </div>
  </main>
</div>

<style>
.admin-layout {
  display: grid;
  grid-template-columns: 280px 1fr;
  height: 100vh;
}

.sidebar {
  background: var(--bg-card);
  border-right: 1px solid var(--border-color);
  padding: 24px;
  overflow-y: auto;
}

.main-content {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.topbar {
  height: 64px;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border-color);
  padding: 0 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.content {
  flex: 1;
  padding: 32px;
  overflow-y: auto;
}
</style>

4.2 Módulos Admin

// Padrão de módulo admin
const adminTesouraria = {
  currentLigaId: null,
  currentTemporada: null,
  
  async render(container, ligaId, temporada) {
    this.currentLigaId = ligaId;
    this.currentTemporada = temporada;
    
    // Carregar template
    const template = await fetch('/admin/modules/tesouraria.html').then(r => r.text());
    document.querySelector(container).innerHTML = template;
    
    // Carregar dados
    await this.loadData();
    
    // Bind events
    this.bindEvents();
  },
  
  async loadData() {
    const data = await fetch(`/api/tesouraria/${this.currentLigaId}/${this.currentTemporada}`)
      .then(r => r.json());
    
    this.renderTable(data);
    this.renderStats(data);
  },
  
  renderTable(data) {
    const tbody = document.querySelector('#tesouraria-table tbody');
    tbody.innerHTML = data.participantes.map(p => `
      <tr>
        <td>${p.nome}</td>
        <td class="${p.saldo >= 0 ? 'positivo' : 'negativo'}">
          R$ ${p.saldo.toFixed(2).replace('.', ',')}
        </td>
        <td>
          <button onclick="adminTesouraria.verExtrato('${p.id}')">
            Ver Extrato
          </button>
        </td>
      </tr>
    `).join('');
  },
  
  bindEvents() {
    // ...
  },
  
  // API pública
  async recarregar() {
    await this.loadData();
  },
  
  mudarTemporada(temporada) {
    this.currentTemporada = temporada;
    this.recarregar();
  }
};

6. 📤 Export System (Mobile Dark HD)

5.1 Configuração Padrão

const EXPORT_CONFIG = {
  backgroundColor: '#000000',
  scale: 2,                    // Retina
  useCORS: true,
  logging: false,
  width: 1080,
  height: 1920,
  pixelRatio: 2
};

async function exportarModulo(elementId, filename) {
  const element = document.getElementById(elementId);
  
  // Aplicar classe de export (mobile otimizado)
  element.classList.add('export-mode');
  
  try {
    const canvas = await html2canvas(element, EXPORT_CONFIG);
    
    // Download
    const link = document.createElement('a');
    link.download = `${filename}_${Date.now()}.png`;
    link.href = canvas.toDataURL('image/png');
    link.click();
    
    // Feedback
    showToast('✅ Imagem exportada com sucesso!');
  } catch (error) {
    console.error('Erro ao exportar:', error);
    showToast('🔴 Erro ao exportar imagem');
  } finally {
    element.classList.remove('export-mode');
  }
}

5.2 CSS para Export

/* Otimizações para export */
.export-mode {
  width: 1080px !important;
  min-height: 1920px !important;
  padding: 40px !important;
  background: linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%) !important;
}

.export-mode .card {
  box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important;
}

.export-mode .text {
  -webkit-font-smoothing: antialiased !important;
  text-rendering: optimizeLegibility !important;
}

7. 🛠️ Debugging & Tools

6.1 Performance Monitoring

// Adicionar no index.html
if ('performance' in window) {
  window.addEventListener('load', () => {
    const perfData = window.performance.timing;
    const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
    const connectTime = perfData.responseEnd - perfData.requestStart;
    const renderTime = perfData.domComplete - perfData.domLoading;
    
    console.log('📊 Performance Metrics:');
    console.log(`  Page Load: ${pageLoadTime}ms`);
    console.log(`  Connect: ${connectTime}ms`);
    console.log(`  Render: ${renderTime}ms`);
    
    // Enviar para analytics (se configurado)
    if (window.analytics) {
      window.analytics.track('page_performance', {
        pageLoadTime,
        connectTime,
        renderTime
      });
    }
  });
}

6.2 Responsive Debug

<!-- Adicionar no footer -->
<div id="debug-viewport" style="position: fixed; bottom: 0; right: 0; padding: 8px; background: rgba(0,0,0,0.8); color: lime; font-size: 12px; z-index: 10000;">
  <script>
    function updateViewport() {
      const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
      const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
      document.getElementById('debug-viewport').textContent = `${vw}x${vh}`;
    }
    window.addEventListener('resize', updateViewport);
    updateViewport();
  </script>
</div>

8. 📋 Checklists

7.1 Novo Módulo Mobile

□ Criar fragmento em /fronts/
□ Criar arquivo JS do módulo
□ Implementar Cache-First pattern
□ Adicionar rota no navigation.js
□ Criar ícone Material Icons
□ Testar em viewport mobile (360x640)
□ Validar TTL do cache
□ Implementar loading states
□ Testar offline
□ Validar export (se aplicável)

7.2 CSS Performance

□ Evitar seletores complexos (> 3 níveis)
□ Usar transform para animações (não top/left)
□ Adicionar will-change em elementos animados
□ Minificar antes de deploy
□ Verificar bundle size (<100KB)
□ Usar CSS Grid/Flexbox (não floats)
□ Lazy load imagens (loading="lazy")

STATUS: Frontend Crafter - READY TO CRAFT

Versao: 3.2 (Mobile Premium Components)

Ultima atualizacao: 2026-01-23

Changelog v3.2:

  • Nova secao 4: Componentes Mobile Premium
  • Header com Avatar e Badge Premium
  • Grid de Atalhos 4 colunas (outlined icons)
  • Card de Status do Time com split Pontos/Posicao
  • FAB do Mercado com timer integrado e gradiente verde
  • Match Card com escudos circulares
  • Bottom Navigation padronizado (4 itens)
  • Tabela de conversao Font Awesome → Material Icons
  • Todos componentes usando variaveis CSS do Design System

Changelog v3.1:

  • Documentado sistema de 3 fontes: Inter (base), Russo One (brand), JetBrains Mono (mono)
  • Classe
    .font-brand
    para titulos com Russo One
  • Variaveis CSS padronizadas:
    --font-family-base
    ,
    --font-family-brand
    ,
    --font-family-mono