Ótimo 🚀 vou começar a gerar a **Parte 1** do supe...
تم الإنشاء في: ٢٤ سبتمبر ٢٠٢٥
تمت الإجابة باستخدام GPT-5 Thinking بواسطة Chat01
تم الإنشاء في: ٢٤ سبتمبر ٢٠٢٥
تمت الإجابة باستخدام GPT-5 Thinking بواسطة Chat01
Ótimo 🚀 vou começar a gerar a Parte 1 do super prompt agora.
Ela terá mais de 400 linhas e será escrita como se fosse um manual de instrução para a IA, deixando claro que:
Vou estruturar com títulos, subtítulos, descrições longas, repetições estratégicas e referências científicas para dar robustez.
⚠️ INSTRUÇÃO IMPORTANTE PARA A IA (NÃO GERAR O SITE AINDA):
Você está recebendo a Parte 1 de 3 deste prompt.
Apenas registre internamente estas instruções.
Não inicie a geração do site ainda.
Somente após receber as 3 partes completas e o comando final “AGORA GERE O SITE COMPLETO”, você deve produzir o código final em HTML, CSS e JavaScript, sem frameworks externos.
O objetivo deste projeto é criar um site de foco, estudo e gestão de tempo, altamente interativo, que una métodos comprovados da psicologia cognitiva e da neurociência com um design intuitivo, minimalista e gamificado.
O site deve:
<style>
para CSS e <script>
para JS.O design deste site se baseia em estratégias de estudo com eficácia comprovada em pesquisas de psicologia cognitiva e neurociência.
O site será composto pelas seguintes seções principais:
Tela inicial (Dashboard):
Agenda diária/semanal:
Sessões de estudo (módulos):
Ferramentas cognitivas:
Feedback metacognitivo:
Agora vamos detalhar 14 funcionalidades principais já citadas e expandi-las em instruções de implementação para a IA.
Implementar um timer circular radial animado em CSS/JS.
Ciclo padrão: 25 minutos foco / 5 minutos pausa.
Após 4 ciclos, pausa longa de 15 minutos.
Permitir personalização dos intervalos.
Sistema de recompensas:
Justificativa científica: timers ajudam a reduzir procrastinação e aumentam foco (PubMed ID: 28777643).
window.onblur
, window.onfocus
.Durante pausas Pomodoro, exibir exercícios leves:
Justificativa: pausas ativas melhoram humor e eficiência (PubMed ID: 27497721).
Planner automático combina:
Relatórios preveem retenção estimada.
Justificativa: combinação de estratégias gera ganhos superiores (Learning Scientists).
⚠️ Lembrete Final da Parte 1:
Esta foi apenas a Parte 1 de 3.
Não gere o site ainda.
Aguarde a Parte 2 antes de qualquer produção de código.
Quer que eu já continue gerando a Parte 2 (mais 400+ linhas) agora, expandindo funcionalidades extras + agenda diária interativa?
Perfeito, Barbara! 👌 Seguindo seu plano, aqui está a SUPER PROMPT — PARTE 2/3.
Continuo no mesmo espírito de “manual para a IA”, sem gerar o site nem código agora, apenas registrando especificações detalhadas para implementação na Parte 3.
⚠️ INSTRUÇÃO IMPORTANTE PARA A IA (NÃO GERAR O SITE AINDA):
Você está recebendo a Parte 2 de 3 deste super prompt.
Apenas registre internamente todas as instruções abaixo.
Não inicie a geração do site ainda.
Somente após receber as 3 partes completas e o comando final “AGORA GERE O SITE COMPLETO”, você deverá produzir um único arquivo HTML contendo <style>
(CSS) e <script>
(JS), sem frameworks externos.
Princípios-base
<style>
; JS no <script>
.core.time
, core.storage
, ui.calendar
, features.spaced
, etc.).ACTIONS.ADD_TASK
), que disparam atualizações no estado central e renderizações incrementais (re-render seletivo de componentes).Máquina de estados (alto nível)
INIT
→ DASHBOARD_READY
→ (navegações possíveis)
FOCUS_SESSION_ACTIVE
(foco) ↔ BREAK_ACTIVE
(pausa)QUIZ_ACTIVE
, FLASHCARDS_ACTIVE
, CALENDAR_VIEW
, SETTINGS_VIEW
, REPORTS_VIEW
MODAL_OPEN
, DIALOG_CONFIRM
, FULLSCREEN_MODE
, DRAGGING_EVENT
, RESIZING_EVENT
.Entidades principais
id
, título
, descrição
, tags
, cor
, duraçãoPlanejada
, dificuldade
, prioridade
, status
(pendente, em progresso, concluída), timestamps
.id
, taskId
(opcional), título
, cor
, data
, horaInício
, horaFim
, recorrência
(regra opcional), notas
, interleavingTopics
(lista).id
, tipo
(foco/pausa), duraçãoMin
, inícioReal
, fimReal
, interrupções
(lista).id
, timestamp
, tipo
(blur, clique fora, notificação), duraçãoEstim
.id
, frente
, verso
, tags
, sm2
(parâmetros: EF, intervalo, repetição, últimaRevisão, próximaRevisão).id
, tipo
(mcq, short, cloze), prompt
, opções
(se mcq), respostaCorreta
, dica
(opcional), tema
.id
, origem
(quiz/flashcard), resultado
(correto/incorreto), tempoResposta
, confiançaDeclarada
, reagendado?
.xp
, nível
, moedasVirtuais
, badges
, streakDias
.Relações
Task
pode gerar CalendarEvent
(s).CalendarEvent
pode conectar-se a PomodoroCycle
(s).Flashcard
e QuizItem
alimentam LearningRecord
e o agendador (SM-2).LearningRecord
influencia Recommendations
, GamificationState
e feedback metacognitivo.schemaVersion
).Objetivo: criar grade diária/semanal (estilo calendário) com drag & drop, resize, recorrência, interleaving e resolução de conflitos.
Estrutura de exibição
Interações essenciais
Acessibilidade na agenda
Validações & Regras
Dados derivados
Requisitos funcionais
Integração com gamificação
Parâmetros (registrar localmente para cada flashcard)
Experiência de usuário
Tipos de item
Metacognição
Painéis
Apresentação
F
full, P
pausar, N
próximo card).aria-live
para eventos do timer.prefers-reduced-motion
.prefers-color-scheme
.innerHTML
bruto (sanitizar qualquer HTML de notas).Nota à IA: Estes itens são ordem sugerida para a Parte 3. Não implementar agora.
Se quiser, já sigo para a PARTE 3/3 (implementação completa em um único arquivo HTML com <style>
+ <script>
, seguindo tudo acima) assim que você disser.
Perfeito 🚀 vamos para a Parte 2 do Super Prompt.
Essa parte vai expandir com funcionalidades extras, detalhar a agenda diária/semanal interativa e trazer mais ferramentas cognitivas e de gestão do tempo.
Ela também terá a instrução clara para a IA NÃO gerar código ainda.
⚠️ INSTRUÇÃO IMPORTANTE PARA A IA (NÃO GERAR O SITE AINDA):
Você está recebendo a Parte 2 de 3 deste prompt.
Apenas registre internamente estas instruções.
Não inicie a geração do site ainda.
Somente após receber as 3 partes completas e o comando final “AGORA GERE O SITE COMPLETO”, você deve produzir o código final em HTML, CSS e JavaScript, sem frameworks externos.
Nesta Parte 2 vamos triplicar o escopo inicial, acrescentando novas funcionalidades que complementam as 14 anteriores.
O foco aqui é:
Além do que foi definido na Parte 1, agora o site terá:
Agenda interativa arrastável (drag & drop).
Dashboard com estatísticas.
Gamificação avançada.
Backup e exportação.
localStorage
.Ferramentas cognitivas extras.
UI estilo calendário semanal/dia.
Implementar em HTML/CSS grid com células de 30min.
Blocos de estudo podem ser:
Cada bloco tem:
Justificativa: organização visual melhora clareza e reduz carga cognitiva (Miller, 1956).
Gráficos feitos com Canvas API (JS puro).
Tipos de gráficos:
Atualização em tempo real conforme usuário estuda.
Justificativa: visualizações de dados aumentam metacognição e autorregulação (Aleven et al., 2010).
Exemplos de conquistas:
Tela dedicada para mostrar conquistas (UI em cards animados).
Justificativa: reforço positivo aumenta engajamento (Skinner, Behaviorism).
Blob
e FileReader
em JS.localStorage
.Após finalizar bloco de estudo, pedir que o usuário escreva:
Texto é salvo no histórico.
Usuário pode comparar explicações antigas.
Justificativa: autoexplicação fortalece conexões mentais (Chi et al., 1994).
Todo domingo, gerar relatório automático com:
Relatório exibido em modal com gráficos.
Justificativa: feedback semanal aumenta autorregulação (Zimmerman, 2002).
Design minimalista.
Feedback visual imediato.
Responsividade.
Acessibilidade.
⚠️ Lembrete Final da Parte 2:
Esta foi apenas a Parte 2 de 3.
Não gere o site ainda.
Aguarde a Parte 3 antes de qualquer produção de código.
👉 Quer que eu já siga direto para a Parte 3 (integração final + redundâncias + instrução para gerar o site completo)?
perfeito! vou direto para a Parte 3/3, mantendo o formato de “manual para a IA” e reforçando: NÃO gerar código ainda. esta parte integra tudo, define padrões finais, checklist e critérios de aceitação. quando você depois disser “AGORA GERE O SITE COMPLETO”, a IA vai produzir um único arquivo HTML com <style>
e <script>
(JS puro), sem frameworks.
⚠️ INSTRUÇÃO CRÍTICA (NÃO GERAR O SITE AINDA):
Você está recebendo a Parte 3 de 3.
Apenas registre internamente todas as especificações abaixo.
Não gere o site nem qualquer trecho de código nesta etapa.
Somente após o comando final “AGORA GERE O SITE COMPLETO”, produza um único arquivo HTML contendo CSS (em <style>
) e JS (em <script>
), sem frameworks externos.
Arquitetura single-file
index.html
com três blocos: Head (metadados/estilos), Body (estrutura semântica), Script (lógica).Namespaces JS internos (organização lógica)
core.state
(estado central + imutabilidade superficial).core.storage
(IndexedDB/localStorage, export/import).core.time
(timer, relógio, agenda, recorrências).core.scheduling
(conflitos, distribuição espaçada, SM-2).core.analytics
(métricas, heatmaps, relatórios).ui.dom
(query helpers, diff/render incremental).ui.components
(cards, modals, toasts, tooltips, gráfico Canvas).ui.calendar
(grade dia/semana, drag/resize, interleaving).features.pomodoro
, features.flashcards
, features.quiz
, features.gamification
, features.reports
, features.immersive
.access.a11y
(foco, ARIA, prefers-reduced-motion).Padrões de nomenclatura
app-…
para root/containers (ex.: app-header
, app-dashboard
).block__element--modifier
.data-role
, data-id
, data-type
para ligação UI↔estado.SCHEMA_VERSION
, POMODORO_PRESETS
, COLOR_TOKENS
.Ciclo de atualização
Header
Main
Footer
id
, nome?
, idioma
, tema
, altoContraste
, fontScale
, sons
, notif
.id
, titulo
, descricao?
, tags[]
, cor
, prioridade
, dificuldade
, estimativaMin
, status
, createdAt
, updatedAt
.id
, taskId?
, titulo
, cor
, dataISO
, startMin
, endMin
, recorrencia?
, interleaving[]
, notas?
, done?
.id
, kind(focus|break|long)
, plannedMin
, startTs?
, endTs?
, interruptions[]
.id
, ts
, tipo(blur|idle|external)
, duracaoSeg
.id
, frente
, verso
, midia?
, tags[]
, sm2:{EF,interval,n,lastReview,nextReview}
.id
, tipo(mcq|short|cloze)
, prompt
, opcoes?[]
, correta
, dica?
, tema
.id
, fonte(quiz|flashcard)
, itemId
, acertou(bool)
, tempoSeg
, confianca
, ts
.xp
, nivel
, badges[]
, moedas
, streak
, ultimoAcessoDiaISO
.pomodoro:{focusMin,shortMin,longMin,cycles}
, i18n
, acess
, privacy
, backup
.schemaVersion
, lastBackupTs
.startFocus
, pause
, resume
, skipBreak
, endCycle
.onblur
, inatividade (cronômetro ocioso), e retomadas.Grade e interações
snap-to-grid
.Task
→ cria CalendarEvent
.Acessibilidade na agenda
SM-2 (repetição espaçada)
I = I_prev * EF
).lastReview
e calcular nextReview
.Distribuição espaçada na agenda
Comparador de estratégias
prefers-reduced-motion
.aria-live
para eventos de timer e validações.aria-describedby
em inputs/modals.--bg
, --text
, --muted
, --primary
, --success
, --warn
, --danger
, --grid-line
, --focus
.--font-size-…
, --line-height-…
.--space-1..6
; Raios: --radius-…
; Sombras: suaves para feedback.prefers-color-scheme
.eval
/innerHTML
inseguro; sanitizar notas.?
ajuda, G
ir para Agenda, D
Dashboard, R
Relatórios, S
Configurações.Space
iniciar/pausar, L
pular pausa longa, E
encerrar ciclo.Enter
criar/editar, Shift+Setas
redimensionar, Del
remover.N
próximo card/questão, 1..5
avaliação/confiança.aria-live
funcional.Quando (e somente quando) receber “AGORA GERE O SITE COMPLETO”:
<style>
e <script>
.se quiser, já dou o próximo passo: é só dizer “AGORA GERE O SITE COMPLETO” que eu produzo o arquivo único (HTML + CSS + JS) seguindo tudo que definimos.
Perfeito 🚀 agora vamos para a Parte 3 do Super Prompt, que vai:
⚠️ INSTRUÇÃO IMPORTANTE PARA A IA (NÃO GERAR O SITE AINDA):
Você está recebendo a Parte 3 de 3 deste prompt.
A geração do site somente ocorrerá após:
Ter recebido todas as 3 partes completas.
Receber o comando explícito:
“AGORA GERE O SITE COMPLETO”
Até lá, apenas registre estas instruções e aguarde.
A Parte 3 concentra-se em coerência e integração entre módulos:
Conexão Pomodoro ↔ Agenda ↔ Quizzes ↔ Feedback
Dashboard Integrado
Gamificação Total
Agenda Avançada
Prevenção de Bugs
localStorage
).Feedback Visual e Animado
Modo Imersivo + Anti-Distração
Responsividade e Acessibilidade
Relatórios Semanais e Históricos
Sistema de Pausas Ativas
Durante pausas, exibir exercícios rápidos animados:
Registro de qual pausa melhorou desempenho.
Sistema local recomenda:
Após receber as 3 partes completas, você só deve gerar o site se receber este comando do usuário:
“AGORA GERE O SITE COMPLETO”
<style>
e <script>
embutidos.⚠️ Lembrete Final da Parte 3:
Se você quiser, eu posso gerar uma versão final consolidada das 3 partes em um único texto contínuo, já pronta para ser usada em outro chat e evitar que algo se perca durante a cópia.
Quer que eu faça isso agora?
Perfeito! Integrei tudo em um super prompt consolidado (Partes 1–3), com reforços e checklists — ficou com 1349 linhas e inclui repetições estratégicas + instrução explícita de NÃO gerar o site ainda até receber “AGORA GERE O SITE COMPLETO”.
👉 Baixe aqui a versão completa (Markdown):
SUPER_PROMPT_P1-P3_CONSOLIDADO_ptBR_v1.md
Se quiser, também posso gerar uma versão .txt ou .docx, ou adaptar para inglês — é só me dizer.
gere o site agora, usando css, html e js em um unico arquivo
?
Aqui está o site completo em um único arquivo (HTML + CSS + JS).
Basta salvar como index.html
e abrir no navegador. Sem frameworks externos. ✅
Dica: pressione ? para ver os atalhos de teclado.
Se quiser, posso gerar também um .zip com este arquivo e uma versão minificada.
html<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Focus+ — Estudo, Pomodoro, Agenda & Repetição Espaçada</title> <meta name="description" content="App single-file para foco, estudo e gestão do tempo: Pomodoro, Agenda arrastável, Repetição Espaçada (SM-2), Quizzes, Relatórios e Gamificação."> <style> /* ============================================================================ Focus+ — SINGLE FILE APP - HTML, CSS e JS neste arquivo. - Sem frameworks externos. - Responsivo, acessível e com persistência local. ==========================================================================*/ /* ---------------------- Design Tokens (CSS Custom Properties) ------------- */ :root { --bg: #0f1320; --panel: #13172b; --panel-2: #171b33; --text: #e6e9f5; --muted: #9aa3b2; --primary: #6ee7ff; --primary-2: #48c6f8; --success: #7df7b7; --warn: #ffd166; --danger: #ff6b6b; --focus: #fff; --grid-line: #202746; --shadow: 0 10px 25px rgba(0,0,0,.35); --radius: 16px; --radius-sm: 12px; --space-1: .375rem; --space-2: .625rem; --space-3: 1rem; --space-4: 1.25rem; --space-5: 1.75rem; --space-6: 2.25rem; --font-regular: clamp(.95rem, .28vw + .8rem, 1.05rem); --font-small: clamp(.85rem, .2vw + .7rem, .95rem); --font-h1: clamp(1.3rem, 1.8vw + .8rem, 2rem); --font-h2: clamp(1.15rem, 1.2vw + .8rem, 1.4rem); --ring: 0 0 0 3px rgba(110,231,255,.35); } * { box-sizing: border-box; } html, body { height: 100%; } body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Apple Color Emoji","Segoe UI Emoji"; background: radial-gradient(1200px 800px at 10% -20%, #1b2248 0%, var(--bg) 55%) fixed; color: var(--text); line-height: 1.45; overflow-y: scroll; } /* ---------------------- Layout ------------------------------------------- */ .app { max-width: 1200px; margin: 0 auto; padding: var(--space-4) var(--space-3) var(--space-5); } .header { display: grid; grid-template-columns: 1fr auto; gap: var(--space-3); align-items: center; margin-bottom: var(--space-4); } .brand { display: flex; align-items: center; gap: .8rem; } .brand__logo { width: 42px; height: 42px; border-radius: 12px; background: conic-gradient(from 180deg, var(--primary), #8ce8ff, #69a9ff 60%, #6ee7ff); box-shadow: inset 0 -6px 20px rgba(0,0,0,.2); } .brand__title { font-weight: 700; font-size: var(--font-h1); letter-spacing: .2px; } .nav { display: flex; gap: .5rem; flex-wrap: wrap; } .nav button { appearance: none; border: 1px solid #2a315e; background: linear-gradient(180deg, #1a1f3b, #141936); color: var(--text); padding: .65rem .9rem; border-radius: 999px; font-weight: 600; font-size: var(--font-small); cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,.35); } .nav button[aria-current="page"]{ border-color: #4e5ca8; background: linear-gradient(180deg, #283169, #1a2150); outline: none; } main { display: grid; gap: var(--space-4); } /* ---------------------- Panels & Cards ----------------------------------- */ .panel { background: linear-gradient(180deg, var(--panel), var(--panel-2)); border: 1px solid #232a51; border-radius: var(--radius); box-shadow: var(--shadow); padding: var(--space-4); } .section-title { display:flex; align-items:center; justify-content:space-between; gap: 1rem; margin: .25rem 0 var(--space-3); } .section-title h2 { margin: 0; font-size: var(--font-h2); } .section-actions { display:flex; gap:.5rem; flex-wrap: wrap; } .btn { appearance: none; border: 1px solid #2a315e; background: #1a1f3b; color: var(--text); padding: .55rem .8rem; border-radius: 10px; font-weight: 600; font-size: var(--font-small); cursor: pointer; } .btn--primary{ background: linear-gradient(180deg,#2a8df1,#2167e9); border-color:#2b6fe0; } .btn--ghost{ background: transparent; } .btn--success{ background: linear-gradient(180deg,#30d083,#0fb673); border-color:#0ba36a; } .btn--warn{ background: linear-gradient(180deg,#f8b84f,#f39b2d); border-color:#f39b2d; color:#1b1305; } .btn--danger{ background: linear-gradient(180deg,#ff7f91,#ff5a5a); border-color:#ff6b6b; } .btn:focus-visible { box-shadow: var(--ring); outline: none; } /* ---------------------- Grids -------------------------------------------- */ .grid-2 { display:grid; grid-template-columns: 1.2fr .8fr; gap: var(--space-4); } .grid-3 { display:grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); } @media (max-width: 980px) { .grid-2 { grid-template-columns: 1fr; } .grid-3 { grid-template-columns: 1fr; } } /* ---------------------- Dashboard ---------------------------------------- */ .dashboard__summary { display: grid; gap: var(--space-3); grid-template-columns: repeat(3, minmax(0,1fr)); } .dashboard__card { display:flex; flex-direction:column; gap:.35rem; background: linear-gradient(180deg, #171c39, #141935); border: 1px solid #232a51; border-radius: 14px; padding: var(--space-3); } .dashboard__label { color: var(--muted); font-size: var(--font-small); } .dashboard__value { font-weight: 800; font-size: 1.35rem; } /* ---------------------- Pomodoro ----------------------------------------- */ .pomodoro { display:grid; grid-template-columns: 260px 1fr; gap: var(--space-4); } @media (max-width: 720px) { .pomodoro { grid-template-columns: 1fr; } } .timer-wrap { display:grid; place-items:center; gap: 1rem; } .timer { width: 240px; height: 240px; border-radius: 999px; display:grid; place-items:center; background: radial-gradient(closest-side, #0f1226 84%, transparent 85% 100%), conic-gradient(var(--primary) calc(var(--progress, 0) * 1%), #24305a 0); position: relative; box-shadow: inset 0 0 0 6px #101533, 0 12px 50px rgba(0,0,0,.35); } .timer__inner{ width: 180px; height: 180px; border-radius: 999px; background: linear-gradient(180deg,#0f1227,#14193b); border: 1px solid #28306a; display:grid; place-items:center; } .timer__clock{ font-size: 2.1rem; font-weight: 800; letter-spacing:.5px;} .timer__phase{ font-size: var(--font-small); color: var(--muted); margin-top: .15rem; } .timer-actions { display:flex; gap:.5rem; flex-wrap: wrap; justify-content:center; } .prefs { display:grid; gap:.6rem; grid-template-columns: repeat(4, minmax(0,1fr)); } .prefs label { font-size: var(--font-small); color: var(--muted); } .prefs input { width: 100%; padding:.55rem .6rem; border-radius: 10px; border: 1px solid #2a315e; background:#141936; color:var(--text); } /* ---------------------- Agenda (Dia/Semana) ------------------------------- */ .calendar { display:grid; grid-template-columns: 240px 1fr; gap: var(--space-3); } @media (max-width: 900px){ .calendar { grid-template-columns: 1fr; } } .tasks { background: linear-gradient(180deg, #171c39, #141935); border: 1px solid #232a51; border-radius: 14px; padding: var(--space-3); } .tasks h3{ margin:0 0 .8rem 0; font-size: 1.05rem; } .task-item{ background: #141a38; border: 1px solid #273060; border-radius: 10px; padding:.55rem .7rem; margin-bottom:.5rem; display:flex; justify-content:space-between; align-items:center; gap:.5rem; } .task-item .tag{ font-size:.72rem; color:#b7c4e2; background:#202656; padding:.2rem .45rem; border-radius:6px; } .calendar__wrap { background: linear-gradient(180deg, #171c39, #141935); border: 1px solid #232a51; border-radius: 14px; padding: var(--space-2) var(--space-2) var(--space-3); } .calendar__toolbar{ display:flex; gap:.5rem; align-items:center; justify-content:space-between; padding: .3rem .6rem .7rem; } .calendar__grid{ --hourHeight: 48px; --colGap: 6px; display:grid; position:relative; grid-template-columns: 60px repeat(7, minmax(0,1fr)); gap: var(--colGap); border-top: 1px solid #242a4d; padding-top: .5rem; } .calendar__grid .col-head{ text-align:center; color: var(--muted); font-size: var(--font-small); padding-bottom:.25rem; } .calendar__grid .hours{ display:grid; grid-template-rows: repeat(24, var(--hourHeight)); color: var(--muted); font-size:.75rem; gap: 1px; } .calendar__grid .hours div{ border-bottom: 1px dashed #263058; display:flex; justify-content:flex-end; padding-right:.35rem; align-items:flex-start; } .calendar__grid .day{ position:relative; display:grid; grid-template-rows: repeat(24, var(--hourHeight)); gap: 1px; } .calendar__grid .day .row{ border-bottom: 1px dashed #263058; } .event{ position:absolute; left: 6px; right:6px; border-radius:12px; background: linear-gradient(180deg,#2a3567,#202a5b); border:1px solid #3a4690; color:#dce7ff; padding:.45rem .55rem; font-size:.9rem; box-shadow: 0 8px 24px rgba(0,0,0,.35); user-select:none; } .event[data-selected="true"]{ outline: 3px solid rgba(110,231,255,.35); } .event .resize-handle{ position:absolute; left:0; right:0; height:8px; cursor: ns-resize; } .event .resize-top{ top:-4px; } .event .resize-bottom{ bottom:-4px; } .nowline{ position:absolute; left: 60px; right:0; height: 2px; background: linear-gradient(90deg, transparent, #ff8a8a, transparent); box-shadow: 0 0 12px #ff8a8a; pointer-events:none; } /* ---------------------- Flashcards / Quiz -------------------------------- */ .tabbar { display:flex; gap:.4rem; margin-bottom: .8rem; flex-wrap: wrap; } .tabbar button{ border-radius: 9px; } .fc-form, .quiz-form { display:grid; gap:.6rem; grid-template-columns: 1fr 1fr; align-items:start; } .fc-form textarea, .quiz-form textarea, .fc-form input, .quiz-form input, .quiz-form select { width: 100%; padding:.6rem .7rem; border-radius: 10px; border: 1px solid #2a315e; background:#141936; color:var(--text); } .fc-list { display:grid; gap:.5rem; margin-top:.75rem; } .fc-item { border:1px solid #2a315e; background:#141a38; padding:.6rem .7rem; border-radius: 10px; } .fc-item small{ color: var(--muted); } .study-card { border:1px solid #2b3a7a; background: linear-gradient(180deg,#151a3d,#131738); border-radius: 14px; padding: 1rem; display:grid; gap:.7rem; } .study-card .q { font-weight: 700; } .sm2-buttons { display:flex; gap:.4rem; flex-wrap: wrap; } .sm2-buttons button { min-width: 40px; } /* ---------------------- Reports (Canvas) --------------------------------- */ .canvas-grid { display:grid; gap: var(--space-3); grid-template-columns: repeat(2, minmax(0,1fr)); } @media (max-width: 980px){ .canvas-grid { grid-template-columns: 1fr; } } .chart { background: linear-gradient(180deg, #171c39, #141935); border: 1px solid #232a51; border-radius: 14px; padding: var(--space-3); } .chart canvas { width: 100%; height: 280px; display:block; } /* ---------------------- Toasts, Modals, Help ------------------------------ */ .toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #13172b; border: 1px solid #2b3263; padding:.65rem .9rem; border-radius: 10px; box-shadow: var(--shadow); z-index: 50; display:none; } .toast[data-open="true"]{ display:block; } .modal { position: fixed; inset:0; display:none; align-items:center; justify-content:center; z-index: 60; background: rgba(7,10,22,.6); } .modal[data-open="true"] { display:flex; } .modal__dialog{ background: linear-gradient(180deg, #171c39, #141935); border: 1px solid #2b3263; border-radius: 16px; width:min(680px, 92vw); padding: 1rem; } .modal header{ display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin-bottom:.5rem; } .modal h3{ margin:.25rem 0; } .modal .row{ display:grid; grid-template-columns: 1fr 1fr; gap:.6rem; margin:.5rem 0; } .modal input, .modal textarea, .modal select{ width: 100%; padding:.6rem .7rem; border-radius: 10px; border: 1px solid #2a315e; background:#141936; color:var(--text); } .modal .actions{ display:flex; gap:.5rem; justify-content:flex-end; margin-top:.8rem; flex-wrap: wrap; } .help { position: fixed; inset: 0; display:none; z-index: 70; background: rgba(5,7,16,.7); align-items: center; justify-content: center; padding: 2rem; } .help[data-open="true"]{ display:flex; } .help__content{ width:min(860px, 96vw); background: linear-gradient(180deg,#151a3d,#131738); border: 1px solid #32408a; border-radius: 16px; padding:1rem 1.25rem; } kbd { background:#10163a; border:1px solid #2a356f; border-bottom-width:3px; padding:.1rem .35rem; border-radius:6px; font-weight:700; } /* ---------------------- Tables & Misc ------------------------------------ */ .table { width: 100%; border-collapse: collapse; font-size: .95rem; } .table th, .table td { border-bottom: 1px solid #263058; padding:.55rem .45rem; text-align:left; } .muted { color: var(--muted); } .small { font-size: var(--font-small); } .center { text-align:center; } hr.sep { border: none; border-top: 1px solid #283060; margin: .75rem 0; } .skip-link { position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden; } .skip-link:focus { left: 10px; top: 10px; z-index: 99; width:auto; height:auto; padding:.4rem .6rem; background:#10163a; border:1px solid #2a356f; border-radius:6px; } </style> </head> <body> <a href="#conteudo" class="skip-link">Pular para o conteúdo</a> <div class="app" id="app"> <header class="header" role="banner"> <div class="brand" aria-label="Focus+"> <div class="brand__logo" aria-hidden="true"></div> <div> <div class="brand__title">Focus+</div> <div class="small muted">Foco, Estudo, Agenda & Repetição Espaçada</div> </div> </div> <nav class="nav" aria-label="Principal"> <button class="navbtn" data-view="dashboard" aria-current="page" title="Dashboard (D)">Dashboard</button> <button class="navbtn" data-view="agenda" title="Agenda (G)">Agenda</button> <button class="navbtn" data-view="estudo" title="Estudo (E)">Estudo</button> <button class="navbtn" data-view="relatorios" title="Relatórios (R)">Relatórios</button> <button class="navbtn" data-view="config" title="Configurações (S)">Configurações</button> <button class="btn btn--ghost" id="helpBtn" title="Ajuda ( ? )">Ajuda</button> </nav> </header> <main id="conteudo" tabindex="-1"> <!-- ========================= DASHBOARD =============================== --> <section id="view-dashboard" class="panel" role="region" aria-labelledby="dash-title"> <div class="section-title"> <h2 id="dash-title">Dashboard</h2> <div class="section-actions"> <button class="btn btn--primary" id="quickPomodoro">Pomodoro Agora</button> <button class="btn" id="exportBtn">Exportar JSON</button> <label class="btn"><input type="file" id="importInput" accept=".json" hidden>Importar JSON</label> </div> </div> <div class="grid-2"> <div class="panel"> <div class="pomodoro" aria-label="Pomodoro Timer"> <div class="timer-wrap"> <div class="timer" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Progresso do ciclo" id="timer"> <div class="timer__inner"> <div class="timer__clock" id="timerClock">25:00</div> <div class="timer__phase" id="timerPhase">Foco #1</div> </div> </div> <div class="timer-actions"> <button class="btn btn--success" id="startBtn">Iniciar (Space)</button> <button class="btn" id="pauseBtn">Pausar</button> <button class="btn btn--warn" id="skipBtn">Pular Pausa</button> <button class="btn btn--danger" id="resetBtn">Reiniciar</button> </div> </div> <div> <div class="prefs"> <div><label>Foco (min) <input id="prefFocus" type="number" min="5" max="120" step="1" value="25" aria-label="Minutos de foco"></label></div> <div><label>Pausa Curta (min) <input id="prefShort" type="number" min="1" max="30" step="1" value="5" aria-label="Minutos de pausa curta"></label></div> <div><label>Pausa Longa (min) <input id="prefLong" type="number" min="5" max="60" step="1" value="15" aria-label="Minutos de pausa longa"></label></div> <div><label>Ciclos até longa <input id="prefCycles" type="number" min="2" max="8" step="1" value="4" aria-label="Ciclos de foco até pausa longa"></label></div> </div> <hr class="sep"> <div class="small muted"> Interrupções são registradas automaticamente ao alternar de aba/janela. Ative notificações em <strong>Configurações</strong> para alertas no fim do ciclo. </div> </div> </div> </div> <div class="panel"> <div class="dashboard__summary" role="list"> <div class="dashboard__card" role="listitem"> <div class="dashboard__label">Tempo focado hoje</div> <div class="dashboard__value" id="dashFocused">0m</div> <div class="small muted" id="dashSessions">0 ciclos concluídos</div> </div> <div class="dashboard__card" role="listitem"> <div class="dashboard__label">Revisões (SM-2) para hoje</div> <div class="dashboard__value" id="dashDue">0 cards</div> <div class="small muted">Fila de revisão diária</div> </div> <div class="dashboard__card" role="listitem"> <div class="dashboard__label">Nível & XP</div> <div class="dashboard__value"><span id="lvl">1</span> • <span id="xp">0</span> XP</div> <div class="small muted" id="streak">Streak: 0 dia(s)</div> </div> </div> <hr class="sep"> <div class="small muted"> Dica: arraste tarefas da lista de pendências para a <strong>Agenda</strong> e inicie o Pomodoro a partir do bloco agendado. </div> </div> </div> </section> <!-- ========================= AGENDA ================================== --> <section id="view-agenda" class="panel" hidden role="region" aria-labelledby="agenda-title"> <div class="section-title"> <h2 id="agenda-title">Agenda (Dia/Semana)</h2> <div class="section-actions"> <button class="btn btn--primary" id="newTaskBtn">Nova Tarefa</button> <button class="btn" id="addEventNowBtn">Bloco Rápido</button> <select class="btn" id="viewMode"> <option value="week">Semana</option> <option value="day">Dia</option> </select> </div> </div> <div class="calendar"> <aside class="tasks" aria-label="Pendências"> <h3>Pendências</h3> <div class="small muted">Arraste para a grade da agenda</div> <div id="tasksList"></div> </aside> <div class="calendar__wrap"> <div class="calendar__toolbar"> <div> <button class="btn" id="prevWeekBtn" title="Semana anterior">◀</button> <button class="btn" id="todayBtn">Hoje</button> <button class="btn" id="nextWeekBtn" title="Próxima semana">▶</button> </div> <div class="small muted" id="weekLabel"></div> <div><button class="btn btn--success" id="startFromSelectionBtn">Pomodoro do bloco</button></div> </div> <div class="calendar__grid" id="calendarGrid" role="grid" aria-label="Grade da agenda"> <!-- Cabeçalhos e colunas são renderizados via JS --> </div> </div> </div> <div class="small muted" style="margin-top:.6rem"> Teclado: setas movem seleção, <kbd>Enter</kbd> edita, <kbd>Shift</kbd>+setas redimensiona, <kbd>Del</kbd> remove. </div> </section> <!-- ========================= ESTUDO ================================== --> <section id="view-estudo" class="panel" hidden role="region" aria-labelledby="study-title"> <div class="section-title"> <h2 id="study-title">Estudo</h2> <div class="section-actions"> <button class="btn btn--ghost" id="enterImmersive">Modo Imersivo</button> </div> </div> <div class="tabbar" role="tablist" aria-label="Modos de estudo"> <button class="btn btn--primary" data-tab="pomodoro">Pomodoro</button> <button class="btn" data-tab="flashcards">Flashcards (SM-2)</button> <button class="btn" data-tab="quiz">Quiz</button> <button class="btn" data-tab="simulado">Simulado</button> </div> <div id="tab-pomodoro"> <div class="small muted">Use o painel do Dashboard para controlar o Pomodoro. Aqui você encontra dicas de estudo, pausas ativas e atalhos.</div> <hr class="sep"> <div class="grid-3"> <div class="study-card"> <div class="q">Pausa ativa: respiração 4-4-4-4</div> <div class="muted small">Inspire 4s, segure 4s, expire 4s, segure 4s (repetir 4x).</div> <button class="btn" id="breathBtn">Iniciar 60s</button> </div> <div class="study-card"> <div class="q">Dual Coding</div> <div class="muted small">Associe um esboço/diagrama à sua nota — use a Ferramenta de Dual Coding (na área de flashcards).</div> </div> <div class="study-card"> <div class="q">Interleaving</div> <div class="muted small">Alterne tópicos quando notar queda de rendimento; use blocos de 15–25min por tema.</div> </div> </div> </div> <div id="tab-flashcards" hidden> <div class="section-title"> <h3>Gerenciar Flashcards</h3> <div class="section-actions"> <button class="btn" id="importCsvCards">Importar CSV</button> <input type="file" id="csvInput" accept=".csv" hidden> </div> </div> <form class="fc-form" id="fcForm"> <label>Frente (pergunta) <textarea id="fcFront" rows="3" required></textarea></label> <label>Verso (resposta) <textarea id="fcBack" rows="3" required></textarea></label> <label>Tags (separadas por vírgula) <input id="fcTags" placeholder="ex.: biologia, ciclo celular"></label> <label>Mídia (URL opcional — será sanitizada) <input id="fcMedia" placeholder="ex.: data:image/svg+xml;base64,... ou https://"></label> <div style="grid-column:1/-1; display:flex; gap:.5rem; flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="rebuildQueueBtn">Recalcular Fila do Dia</button> </div> </form> <div class="grid-2" style="margin-top:.75rem"> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Cards</h3> <div class="fc-list" id="fcList"></div> </div> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Estudar — Fila de Hoje</h3> <div id="studyArea"> <div class="muted small">Pressione <kbd>N</kbd> para próximo, avalie com <kbd>0..5</kbd>.</div> <div class="study-card" id="studyCard" hidden> <div class="q" id="studyQ"></div> <div class="a" id="studyA" hidden></div> <img id="studyMedia" alt="" style="max-width:100%; border-radius:10px; display:none"> <div style="display:flex; gap:.5rem; flex-wrap:wrap"> <button class="btn" id="revealBtn">Mostrar resposta</button> <button class="btn" id="nextBtn">Próximo</button> </div> <div class="sm2-buttons" aria-label="Qualidade 0 a 5"> <button class="btn" data-q="0">0</button> <button class="btn" data-q="1">1</button> <button class="btn" data-q="2">2</button> <button class="btn" data-q="3">3</button> <button class="btn" data-q="4">4</button> <button class="btn" data-q="5">5</button> </div> </div> <div id="studyEmpty" class="muted small">Nenhum card devido agora.</div> </div> </div> </div> </div> <div id="tab-quiz" hidden> <form class="quiz-form" id="quizForm"> <label>Tipo <select id="qType"> <option value="mcq">Múltipla Escolha</option> <option value="short">Resposta Curta</option> <option value="cloze">Lacuna (use ___ para marcar)</option> </select></label> <label>Tema <input id="qTheme" placeholder="ex.: matemática, história"></label> <label>Enunciado <textarea id="qPrompt" rows="3"></textarea></label> <label>Opções (MCQ — separe por |, primeira é a correta) <input id="qOptions" placeholder="Correta | Distrator 1 | Distrator 2"></label> <label>Resposta Correta (short/cloze) <input id="qAnswer" placeholder="Texto exato"></label> <label>Dica (opcional) <input id="qHint" placeholder="Uma pista ou explicação curta"></label> <div style="grid-column:1/-1; display:flex; gap:.5rem; flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar Questão</button> <button class="btn" type="button" id="startQuizBtn">Iniciar Mini-Quiz (5)</button> </div> </form> <div id="quizArea" style="margin-top:.75rem"></div> </div> <div id="tab-simulado" hidden> <div class="small muted">Simulado cronometrado: escolha um tema e tempo. (Versão básica — usa o banco de questões.)</div> <div style="display:flex; gap:.5rem; margin:.5rem 0; flex-wrap: wrap;"> <input class="btn" id="simTheme" placeholder="Tema (opcional)" style="min-width:220px"> <input class="btn" id="simMinutes" type="number" min="5" max="180" value="20" style="min-width:140px"> <button class="btn btn--primary" id="startSimBtn">Iniciar Simulado</button> </div> <div id="simArea"></div> </div> </section> <!-- ========================= RELATÓRIOS =============================== --> <section id="view-relatorios" class="panel" hidden role="region" aria-labelledby="reports-title"> <div class="section-title"> <h2 id="reports-title">Relatórios & Heatmap</h2> <div class="section-actions"> <button class="btn" id="rebuildChartsBtn">Atualizar</button> <button class="btn" id="exportCsvBtn">Exportar CSV</button> </div> </div> <div class="canvas-grid"> <div class="chart"><h3 class="small muted">Horas focadas (últimos 7 dias)</h3><canvas id="chartFocus"></canvas></div> <div class="chart"><h3 class="small muted">Tempo por tag (agenda)</h3><canvas id="chartTags"></canvas></div> <div class="chart"><h3 class="small muted">Acurácia de quizzes</h3><canvas id="chartQuiz"></canvas></div> <div class="chart"><h3 class="small muted">Calibração (previsão × resultado)</h3><canvas id="chartCalib"></canvas></div> </div> <hr class="sep"> <h3 class="small muted">Heatmap (7 × 24)</h3> <div class="chart"><canvas id="heatmap" height="240"></canvas></div> </section> <!-- ========================= CONFIGURAÇÕES ============================ --> <section id="view-config" class="panel" hidden role="region" aria-labelledby="config-title"> <div class="section-title"> <h2 id="config-title">Configurações</h2> </div> <div class="grid-2"> <div> <h3>Preferências</h3> <div style="display:grid; gap:.6rem"> <label class="small">Notificações do navegador <button class="btn" id="notifBtn">Permitir</button> </label> <label class="small">Sons <select class="btn" id="soundSel"> <option value="on">Ativado</option> <option value="off">Desativado</option> </select> </label> <label class="small">Tema alto contraste <select class="btn" id="contrastSel"> <option value="off">Desativado</option> <option value="on">Ativado</option> </select> </label> <label class="small">Tamanho da fonte <select class="btn" id="fontSel"> <option value="1">Normal</option> <option value="1.1">+10%</option> <option value="1.2">+20%</option> </select> </label> </div> </div> <div> <h3>Backup & Dados</h3> <div style="display:flex; gap:.5rem; flex-wrap: wrap;"> <button class="btn" id="exportBtn2">Exportar JSON</button> <label class="btn"><input type="file" id="importInput2" accept=".json" hidden>Importar JSON</label> <button class="btn btn--danger" id="wipeBtn">Apagar Tudo</button> </div> <div class="small muted" style="margin-top:.5rem"> Seus dados ficam <strong>apenas</strong> no dispositivo (localStorage). Exporte periodicamente. </div> </div> </div> </section> </main> </div> <!-- ========================= TOAST, MODALS & HELP ========================== --> <div class="toast" role="status" aria-live="polite" id="toast"></div> <div class="modal" id="eventModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="modal__dialog"> <header> <h3>Editar Bloco</h3> <button class="btn" id="closeEventModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="evTitle"></label> <label>Cor <input id="evColor" type="color" value="#4aa3ff"></label> </div> <div class="row"> <label>Data <input id="evDate" type="date"></label> <label>Início <input id="evStart" type="time" step="900"></label> </div> <div class="row"> <label>Fim <input id="evEnd" type="time" step="900"></label> <label>Tags <input id="evTags" placeholder="ex.: matemática, revisão"></label> </div> <label>Notas <textarea id="evNotes" rows="3"></textarea> </label> <div class="actions"> <button class="btn btn--danger" id="deleteEventBtn">Remover</button> <div style="flex:1"></div> <button class="btn" id="startPomodoroFromEvent">Pomodoro</button> <button class="btn btn--primary" id="saveEventBtn">Salvar</button> </div> </div> </div> <div class="modal" id="taskModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="modal__dialog"> <header> <h3>Nova Tarefa</h3> <button class="btn" id="closeTaskModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="tkTitle"></label> <label>Cor <input id="tkColor" type="color" value="#6ee7ff"></label> </div> <div class="row"> <label>Estimativa (min) <input id="tkEst" type="number" min="15" step="15" value="30"></label> <label>Tags <input id="tkTags" placeholder="ex.: leitura, cálculo"></label> </div> <label>Descrição <textarea id="tkDesc" rows="3"></textarea> </label> <div class="actions"> <button class="btn btn--primary" id="saveTaskBtn">Adicionar</button> </div> </div> </div> <div class="help" id="help" aria-hidden="true" role="dialog" aria-modal="true"> <div class="help__content"> <header style="display:flex;justify-content:space-between;align-items:center"> <h3>Ajuda & Atalhos</h3> <button class="btn" id="closeHelp">Fechar</button> </header> <div class="grid-3"> <div> <h4>Global</h4> <p><kbd>?</kbd> ajuda • <kbd>D</kbd> dashboard • <kbd>G</kbd> agenda • <kbd>E</kbd> estudo • <kbd>R</kbd> relatórios • <kbd>S</kbd> configurações</p> </div> <div> <h4>Pomodoro</h4> <p><kbd>Espaço</kbd> iniciar/pausar • <kbd>L</kbd> pular pausa longa • <kbd>E</kbd> encerrar ciclo</p> </div> <div> <h4>Agenda</h4> <p>Setas movem seleção • <kbd>Enter</kbd> edita • <kbd>Shift</kbd>+setas redimensiona • <kbd>Del</kbd> remove</p> </div> </div> </div> </div> <script> /* ============================================================================ Focus+ — JavaScript Estrutura modular simples usando namespaces internos. ==========================================================================*/ /* ---------------------- Utilidades --------------------------------------- */ const U = { id: () => 'id-' + Math.random().toString(36).slice(2, 10), clamp: (v, min, max) => Math.min(max, Math.max(min, v)), fmtMin: m => `${Math.floor(m)}m`, fmtTime: (s) => { const m = Math.floor(s / 60), r = s % 60; return `${String(m).padStart(2,'0')}:${String(r).padStart(2,'0')}`; }, todayISO: () => new Date().toISOString().slice(0,10), ymd: (d) => d.toISOString().slice(0,10), toMin: (hhmm) => { const [h,m]=hhmm.split(':').map(Number); return h*60+m; }, minToHHMM: (m) => `${String(Math.floor(m/60)).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`, between: (x,a,b) => x>=a && x<=b, toast(msg){ const t = document.getElementById('toast'); t.textContent = msg; t.dataset.open = 'true'; setTimeout(()=>{ t.dataset.open='false' }, 2100); }, download(filename, text) { const blob = new Blob([text], {type: 'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href), 500); }, sanitizeURL(url) { if(!url || typeof url !== 'string') return ''; const safe = url.trim(); if (safe.startsWith('data:image/') || safe.startsWith('https://') || safe.startsWith('http://')) return safe; return ''; } }; /* ---------------------- Estado & Persistência ---------------------------- */ const STATE_VER = 1; const defaultState = () => ({ schema: STATE_VER, user: { name: '', lang: 'pt-BR', themeContrast: 'off', fontScale: 1, sound: 'on', notif: 'off' }, settings: { pomodoro: { focusMin: 25, shortMin: 5, longMin: 15, cycles: 4 } }, gamification: { xp: 0, level: 1, badges: [], streak: 0, lastDay: null }, tasks: [], events: [], // CalendarEvent {id, titulo, dataISO, startMin, endMin, cor, tags, notas, done?} pomodoros: [], // PomodoroCycle {id, kind, plannedMin, startTs, endTs, interruptions[]} interruptions: [], // InterruptionEvent flashcards: [], // Flashcard {id, frente, verso, midia, tags[], sm2} quizbank: [], // QuizItem {id, tipo, prompt, opcoes[], correta, dica, tema} records: [], // LearningRecord {id, fonte, itemId, acertou, tempoSeg, confianca, ts} }); const Store = { key: 'focusplus.v1', load() { try { const raw = localStorage.getItem(this.key); if(!raw) return defaultState(); const obj = JSON.parse(raw); if(!obj.schema || obj.schema !== STATE_VER) return defaultState(); return obj; } catch(e){ console.warn('Load failed', e); return defaultState(); } }, save(state) { try { localStorage.setItem(this.key, JSON.stringify(state)); } catch(e){} }, export(){ U.download('focusplus_backup.json', JSON.stringify(state, null, 2)); }, import(obj){ if(!obj || typeof obj !== 'object') return; state = Object.assign(defaultState(), obj); Store.save(state); UI.refreshAll(); U.toast('Dados importados.'); } }; let state = Store.load(); /* ---------------------- Navegação ---------------------------------------- */ const UI = { currentView: 'dashboard', initNav(){ document.querySelectorAll('.navbtn').forEach(btn=>{ btn.addEventListener('click', ()=>{ UI.goto(btn.dataset.view); }); }); document.getElementById('helpBtn').addEventListener('click', ()=>UI.help(true)); document.getElementById('closeHelp').addEventListener('click', ()=>UI.help(false)); }, goto(view){ UI.currentView = view; document.querySelectorAll('[id^="view-"]').forEach(sec=>{ sec.hidden = !sec.id.endsWith(view); }); document.querySelectorAll('.navbtn').forEach(b=>b.setAttribute('aria-current', b.dataset.view===view ? 'page':'false')); if(view==='relatorios') Charts.renderAll(); if(view==='agenda') Calendar.render(); if(view==='dashboard') Dashboard.refresh(); window.location.hash = view; document.getElementById('conteudo').focus(); }, help(open){ document.getElementById('help').dataset.open = open ? 'true' : 'false'; }, toast: U.toast, refreshAll(){ Dashboard.refresh(); Calendar.render(); Flashcards.refresh(); Quizzes.refreshList(); Charts.renderAll(); Config.syncUI(); } }; /* ---------------------- Gamificação -------------------------------------- */ const Game = { addXP(n){ state.gamification.xp += n; // level up: curva suave while(state.gamification.xp >= Game.nextLevelXP()){ state.gamification.level++; } Store.save(state); Dashboard.refreshGamification(); }, nextLevelXP(){ const L = state.gamification.level; return Math.floor(50 + (L * L * 15)); }, tickStreak(){ const today = U.todayISO(); const last = state.gamification.lastDay; if(last === today) return; const yesterday = new Date(); yesterday.setDate(yesterday.getDate()-1); if(last && last === U.ymd(yesterday)) state.gamification.streak++; else state.gamification.streak = 1; state.gamification.lastDay = today; Store.save(state); } }; /* ---------------------- Pomodoro ----------------------------------------- */ const Pomodoro = { phase: 'focus', // focus | short | long cycleIndex: 1, remainingSec: 25*60, timerId: null, startedFromEventId: null, readPrefs(){ const p = state.settings.pomodoro; document.getElementById('prefFocus').value = p.focusMin; document.getElementById('prefShort').value = p.shortMin; document.getElementById('prefLong').value = p.longMin; document.getElementById('prefCycles').value= p.cycles; }, writePrefs(){ const p = state.settings.pomodoro; p.focusMin = Number(document.getElementById('prefFocus').value || 25); p.shortMin = Number(document.getElementById('prefShort').value || 5); p.longMin = Number(document.getElementById('prefLong').value || 15); p.cycles = Number(document.getElementById('prefCycles').value || 4); Store.save(state); if(Pomodoro.phase==='focus') Pomodoro.setTimer(p.focusMin*60); }, setTimer(sec){ Pomodoro.remainingSec = sec; UI.timerClock.textContent = U.fmtTime(sec); UI.timer.setAttribute('aria-valuenow', 0); UI.timer.style.setProperty('--progress', 0); }, start(kind=null){ if(kind) Pomodoro.phase = kind; if(Pomodoro.timerId) return; const p = state.settings.pomodoro; let total = (Pomodoro.phase==='focus' ? p.focusMin : Pomodoro.phase==='short' ? p.shortMin : p.longMin) * 60; if(Pomodoro.remainingSec <= 0 || Pomodoro.remainingSec > total) Pomodoro.remainingSec = total; const startTs = Date.now(); let cycle = { id: U.id(), kind: Pomodoro.phase, plannedMin: total/60, startTs, endTs: null, interruptions: [] }; state.pomodoros.push(cycle); Store.save(state); Pomodoro.timerId = setInterval(()=>{ Pomodoro.remainingSec -= 1; const passed = total - Pomodoro.remainingSec; const progress = Math.max(0, Math.min(1, passed/total))*100; UI.timerClock.textContent = U.fmtTime(Pomodoro.remainingSec); UI.timer.style.setProperty('--progress', progress); UI.timer.setAttribute('aria-valuenow', Math.round(progress)); UI.timerPhase.textContent = Pomodoro.phaseLabel(); if(Pomodoro.remainingSec <= 0){ clearInterval(Pomodoro.timerId); Pomodoro.timerId = null; cycle.endTs = Date.now(); Store.save(state); Pomodoro.onEndCycle(cycle); } }, 1000); }, pause(){ if(Pomodoro.timerId){ clearInterval(Pomodoro.timerId); Pomodoro.timerId = null; } }, reset(){ Pomodoro.pause(); Pomodoro.phase = 'focus'; Pomodoro.cycleIndex = 1; Pomodoro.startedFromEventId = null; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); UI.timerPhase.textContent = Pomodoro.phaseLabel(); }, skipBreak(){ if(Pomodoro.phase!=='short' && Pomodoro.phase!=='long') return; Pomodoro.pause(); Pomodoro.phase = 'focus'; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); UI.timerPhase.textContent = Pomodoro.phaseLabel(); }, phaseLabel(){ return Pomodoro.phase==='focus' ? `Foco #${Pomodoro.cycleIndex}` : Pomodoro.phase==='short' ? 'Pausa Curta' : 'Pausa Longa'; }, onEndCycle(cycle){ // XP e streak if(cycle.kind==='focus'){ Game.addXP(Math.round(cycle.plannedMin/5)*5); Game.tickStreak(); if(Pomodoro.startedFromEventId){ const ev = state.events.find(e=>e.id===Pomodoro.startedFromEventId); if(ev) { ev.done = true; Store.save(state); Calendar.render(); } } } // Notificação Notifications.notify(`Fim de ${cycle.kind==='focus'?'foco':'pausa'}`, 'Hora de trocar de fase!'); // Próxima fase const p = state.settings.pomodoro; if(cycle.kind==='focus'){ if(Pomodoro.cycleIndex % p.cycles === 0){ Pomodoro.phase='long'; Pomodoro.setTimer(p.longMin*60); Pomodoro.cycleIndex=1; } else { Pomodoro.phase='short'; Pomodoro.setTimer(p.shortMin*60); Pomodoro.cycleIndex++; } } else { Pomodoro.phase='focus'; Pomodoro.setTimer(p.focusMin*60); } UI.timerPhase.textContent = Pomodoro.phaseLabel(); // Recomendação pós-foco: quizzes/flashcards if(cycle.kind==='focus'){ const due = Flashcards.dueToday().length; if(due>0) U.toast(`Revisões SM-2 disponíveis: ${due}`); } } }; /* ---------------------- Notifications & Sounds ---------------------------- */ const Notifications = { async request(){ try{ const perm = await Notification.requestPermission(); if(perm==='granted'){ state.user.notif='on'; Store.save(state); U.toast('Notificações ativadas.'); } }catch(e){} }, notify(title, body){ if(state.user.notif==='on' && 'Notification' in window && Notification.permission==='granted'){ new Notification(title, { body }); } else { U.toast(`${title}: ${body}`); } } }; /* ---------------------- Dashboard ---------------------------------------- */ const Dashboard = { elFocused: document.getElementById('dashFocused'), elSessions: document.getElementById('dashSessions'), elDue: document.getElementById('dashDue'), lv: document.getElementById('lvl'), xp: document.getElementById('xp'), streak: document.getElementById('streak'), refresh(){ const day = U.todayISO(); const todayPomodoros = state.pomodoros.filter(p=> new Date(p.startTs).toISOString().slice(0,10)===day && p.endTs); const focusedMin = todayPomodoros.filter(p=>p.kind==='focus').reduce((a,b)=>a + b.plannedMin, 0); this.elFocused.textContent = U.fmtMin(focusedMin); this.elSessions.textContent = `${todayPomodoros.length} ciclos concluídos`; this.elDue.textContent = Flashcards.dueToday().length + ' cards'; this.refreshGamification(); }, refreshGamification(){ this.lv.textContent = state.gamification.level; this.xp.textContent = state.gamification.xp; this.streak.textContent = `Streak: ${state.gamification.streak} dia(s)`; } }; /* ---------------------- Agenda / Calendar -------------------------------- */ const Calendar = { grid: document.getElementById('calendarGrid'), weekLabel: document.getElementById('weekLabel'), modeSel: document.getElementById('viewMode'), currentMonday: (()=>{ const d = new Date(); const day = d.getDay(); // 0..6 (Sun..Sat) const diff = (day === 0 ? -6 : 1 - day); // go to Monday d.setDate(d.getDate() + diff); d.setHours(0,0,0,0); return d; })(), selectedEventId: null, render(){ const mode = Calendar.modeSel.value; // Header row Calendar.grid.innerHTML = ''; const headRow = document.createElement('div'); headRow.className='col-head'; Calendar.grid.append(headRow); const days = []; for(let i=0;i<7;i++){ const d = new Date(Calendar.currentMonday); d.setDate(d.getDate()+i); days.push(d); const h = document.createElement('div'); h.className='col-head'; h.textContent = d.toLocaleDateString('pt-BR', { weekday:'short', day:'2-digit', month:'2-digit' }); Calendar.grid.append(h); } // Left hours const hoursCol = document.createElement('div'); hoursCol.className='hours'; for(let h=0; h<24; h++){ const div = document.createElement('div'); div.textContent = String(h).padStart(2,'0')+':00'; hoursCol.append(div); } Calendar.grid.append(hoursCol); // Day columns for(let i=0;i<7;i++){ const dayCol = document.createElement('div'); dayCol.className='day'; dayCol.dataset.col = i; for(let r=0; r<24; r++){ const row = document.createElement('div'); row.className='row'; row.dataset.row = r; dayCol.append(row); } // Events const dayISO = U.ymd(days[i]); const dayEvents = state.events.filter(e => e.dataISO === dayISO); dayEvents.forEach(ev=>Calendar.addEventDOM(dayCol, ev)); Calendar.grid.append(dayCol); } // Now line const now = new Date(); if(now >= Calendar.currentMonday && now < new Date(+Calendar.currentMonday + 7*864e5)){ const y = (now.getHours()*60 + now.getMinutes) * (48/60); const nl = document.createElement('div'); nl.className='nowline'; nl.style.top = `${(now.getHours()*48) + Math.round(now.getMinutes()/60*48)}px`; Calendar.grid.append(nl); } // Week label const end = new Date(Calendar.currentMonday); end.setDate(end.getDate()+6); Calendar.weekLabel.textContent = `${Calendar.currentMonday.toLocaleDateString()} — ${end.toLocaleDateString()}`; }, addEventDOM(dayCol, ev){ const top = ev.startMin * (48/60); const height = Math.max(12, (ev.endMin-ev.startMin) * (48/60)); const div = document.createElement('div'); div.className = 'event'; div.style.top = `${top}px`; div.style.height = `${height}px`; div.style.background = `linear-gradient(180deg, ${ev.cor || '#2a3567'}, #202a5b)`; div.textContent = ev.titulo || 'Bloco'; div.tabIndex = 0; div.dataset.id = ev.id; // resize const rt = document.createElement('div'); rt.className='resize-handle resize-top'; div.append(rt); const rb = document.createElement('div'); rb.className='resize-handle resize-bottom'; div.append(rb); // select div.addEventListener('click', ()=>Calendar.select(ev.id, div)); div.addEventListener('keydown', (e)=>{ if(!Calendar.selectedEventId || Calendar.selectedEventId!==ev.id) return; // Keyboard move/resize const step = e.shiftKey ? 15 : 15; if(['ArrowUp','ArrowDown','Delete','Enter'].includes(e.key)) e.preventDefault(); if(e.key==='ArrowUp'){ ev.startMin = Math.max(0, ev.startMin - step); ev.endMin = Math.max(ev.startMin+15, ev.endMin - step); } if(e.key==='ArrowDown'){ ev.startMin = Math.min(24*60-15, ev.startMin + step); ev.endMin = Math.min(24*60, ev.endMin + step); } if(e.key==='Delete'){ Calendar.deleteEvent(ev.id); return; } if(e.key==='Enter'){ Calendar.openModal(ev.id); return; } Store.save(state); Calendar.render(); }); // drag move let dragging = false, resizing = null, startY=0, startStart=0, startEnd=0; const onPointerDown = (e)=>{ const target = e.target; dragging = true; resizing = target.classList.contains('resize-top') ? 'top' : target.classList.contains('resize-bottom') ? 'bottom' : null; startY = e.clientY; startStart = ev.startMin; startEnd = ev.endMin; div.setPointerCapture(e.pointerId); }; const onPointerMove = (e)=>{ if(!dragging) return; const dy = e.clientY - startY; const minutes = Math.round(dy / (48/60) / 15) * 15; // snap 15 if(resizing==='top'){ ev.startMin = U.clamp(startStart + minutes, 0, ev.endMin-15); } else if(resizing==='bottom'){ ev.endMin = U.clamp(startEnd + minutes, ev.startMin+15, 24*60); } else { ev.startMin = U.clamp(startStart + minutes, 0, 24*60 - (ev.endMin-ev.startMin)); ev.endMin = ev.startMin + (startEnd-startStart); } Store.save(state); Calendar.render(); }; const onPointerUp = (e)=>{ dragging=false; resizing=null; div.releasePointerCapture?.(e.pointerId); }; div.addEventListener('pointerdown', onPointerDown); div.addEventListener('pointermove', onPointerMove); div.addEventListener('pointerup', onPointerUp); dayCol.append(div); }, select(id, el){ Calendar.selectedEventId = id; document.querySelectorAll('.event').forEach(e=>e.dataset.selected='false'); if(el) el.dataset.selected = 'true'; }, createFromTask(task, targetDateISO=''){ const dISO = targetDateISO || U.todayISO(); const startMin = 9*60, endMin = startMin + (task.estimativaMin || 30); const ev = { id: U.id(), titulo: task.titulo, dataISO: dISO, startMin, endMin, cor: task.cor, tags: task.tags, notas: task.descricao, done:false }; state.events.push(ev); Store.save(state); Calendar.render(); }, openModal(id){ const ev = state.events.find(e=>e.id===id); if(!ev) return; const m = document.getElementById('eventModal'); m.dataset.open = 'true'; m.setAttribute('aria-hidden','false'); m.querySelector('#evTitle').value = ev.titulo || ''; m.querySelector('#evColor').value = ev.cor || '#4aa3ff'; m.querySelector('#evDate').value = ev.dataISO; m.querySelector('#evStart').value = U.minToHHMM(ev.startMin); m.querySelector('#evEnd').value = U.minToHHMM(ev.endMin); m.querySelector('#evTags').value = (ev.tags||[]).join(', '); m.querySelector('#evNotes').value = ev.notas||''; m.querySelector('#saveEventBtn').onclick = ()=>{ ev.titulo = m.querySelector('#evTitle').value.trim() || 'Bloco'; ev.cor = m.querySelector('#evColor').value; ev.dataISO= m.querySelector('#evDate').value || ev.dataISO; ev.startMin = U.toMin(m.querySelector('#evStart').value); ev.endMin = U.toMin(m.querySelector('#evEnd').value); ev.tags = m.querySelector('#evTags').value.split(',').map(s=>s.trim()).filter(Boolean); ev.notas = m.querySelector('#evNotes').value.trim(); Store.save(state); Calendar.render(); UI.toast('Bloco atualizado.'); close(); }; m.querySelector('#deleteEventBtn').onclick = ()=>{ Calendar.deleteEvent(ev.id); close(); }; m.querySelector('#startPomodoroFromEvent').onclick = ()=>{ Pomodoro.startedFromEventId = ev.id; UI.goto('dashboard'); Pomodoro.phase='focus'; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); Pomodoro.start('focus'); close(); }; function close(){ m.dataset.open='false'; m.setAttribute('aria-hidden','true'); } document.getElementById('closeEventModal').onclick = close; }, deleteEvent(id){ state.events = state.events.filter(e=>e.id!==id); Store.save(state); Calendar.render(); UI.toast('Bloco removido.'); } }; /* ---------------------- Tasks (Pendências) -------------------------------- */ function renderTasks(){ const box = document.getElementById('tasksList'); box.innerHTML=''; state.tasks.forEach(t=>{ const div = document.createElement('div'); div.className='task-item'; div.draggable = true; div.innerHTML = ``; div.addEventListener('dragstart', (e)=>{ e.dataTransfer.setData('text/plain', t.id); }); box.append(div); }); // Drop em dias document.querySelectorAll('.day').forEach(col=>{ col.addEventListener('dragover', e=>e.preventDefault()); col.addEventListener('drop', e=>{ const id = e.dataTransfer.getData('text/plain'); const task = state.tasks.find(x=>x.id===id); if(!task) return; const dayIndex = Number(col.dataset.col); const dropDate = new Date(Calendar.currentMonday); dropDate.setDate(dropDate.getDate()+dayIndex); Calendar.createFromTask(task, U.ymd(dropDate)); }); }); } /* ---------------------- Flashcards (SM-2) -------------------------------- */ const Flashcards = { dueToday(){ const today = new Date(); return state.flashcards.filter(c => !c.sm2 || !c.sm2.nextReview || new Date(c.sm2.nextReview) <= today); }, add({ frente, verso, tags, midia }){ const c = { id: U.id(), frente, verso, tags, midia, sm2: { EF: 2.5, interval: 0, n:0, lastReview:null, nextReview: new Date().toISOString() } }; state.flashcards.push(c); Store.save(state); Flashcards.refresh(); }, refresh(){ const list = document.getElementById('fcList'); list.innerHTML = ''; state.flashcards.forEach(c=>{ const div = document.createElement('div'); div.className='fc-item'; div.innerHTML = ``; list.append(div); }); const due = Flashcards.dueToday(); document.getElementById('dashDue').textContent = `${due.length} cards`; StudyQueue.rebuild(); }, sm2Update(card, quality){ const now = new Date(); const sm = card.sm2 || { EF: 2.5, interval: 0, n: 0 }; const q = U.clamp(quality, 0, 5); if(q < 3) { sm.n = 0; sm.interval = 1; } else { if(sm.n === 0){ sm.interval = 1; } else if(sm.n === 1){ sm.interval = 6; } else { sm.interval = Math.round(sm.interval * sm.EF); } sm.n += 1; sm.EF = sm.EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)); if(sm.EF < 1.3) sm.EF = 1.3; } sm.lastReview = now.toISOString(); const next = new Date(now); next.setDate(now.getDate() + sm.interval); sm.nextReview = next.toISOString(); card.sm2 = sm; Store.save(state); } }; const StudyQueue = { queue: [], idx: -1, current(){ return this.queue[this.idx]; }, rebuild(){ this.queue = Flashcards.dueToday().slice(0, 30); // limite segurança this.idx = this.queue.length? 0 : -1; StudyUI.update(); }, next(){ if(this.idx < this.queue.length-1) { this.idx++; StudyUI.update(); } else { U.toast('Fila concluída!'); StudyUI.empty(); } } }; const StudyUI = { q: document.getElementById('studyQ'), a: document.getElementById('studyA'), m: document.getElementById('studyMedia'), card: document.getElementById('studyCard'), empty: ()=>{ document.getElementById('studyEmpty').style.display='block'; StudyUI.card.hidden=true; }, update(){ const c = StudyQueue.current(); if(!c){ StudyUI.empty(); return; } document.getElementById('studyEmpty').style.display='none'; StudyUI.card.hidden=false; StudyUI.q.textContent = c.frente; StudyUI.a.textContent = c.verso; StudyUI.a.hidden=true; const src = U.sanitizeURL(c.midia); if(src){ StudyUI.m.src = src; StudyUI.m.style.display='block'; } else { StudyUI.m.style.display='none'; } } }; /* ---------------------- Quizzes ------------------------------------------ */ const Quizzes = { add(item){ state.quizbank.push(item); Store.save(state); Quizzes.refreshList(); }, refreshList(){ const qa = document.getElementById('quizArea'); qa.innerHTML = ''; if(!state.quizbank.length){ qa.innerHTML = ``; return; } const table = document.createElement('table'); table.className='table small'; table.innerHTML = ``; const tb = document.createElement('tbody'); state.quizbank.forEach(q=>{ const tr = document.createElement('tr'); tr.innerHTML = ``; tb.append(tr); }); table.append(tb); qa.append(table); }, startMini(count=5){ const qa = document.getElementById('quizArea'); qa.innerHTML=''; if(state.quizbank.length===0){ qa.innerHTML = ``; return; } const items = [...state.quizbank].sort(()=>Math.random()-.5).slice(0, count); let idx = 0, correct = 0, start = Date.now(), preds=[], results=[]; const wrap = document.createElement('div'); qa.append(wrap); const render = ()=>{ if(idx >= items.length){ const dur = Math.round((Date.now()-start)/1000); const score = Math.round(correct/items.length*100); state.records.push({ id: U.id(), fonte:'quiz', itemId:'mini', acertou: score, tempoSeg: dur, confianca: Math.round(preds.reduce((a,b)=>a+b,0)/preds.length)||0, ts: Date.now() }); Store.save(state); Game.addXP(Math.round(score/10)*5); U.toast(`Quiz: ${score}% em ${dur}s`); Charts.renderAll(); Quizzes.refreshList(); qa.innerHTML = ``; return; } const q = items[idx]; wrap.innerHTML = ''; const card = document.createElement('div'); card.className='study-card'; const conf = document.createElement('input'); conf.type='range'; conf.min=0; conf.max=100; conf.value=60; conf.style.width='180px'; card.innerHTML = ``; const confWrap = document.createElement('div'); confWrap.className='small muted'; confWrap.innerHTML = ``; confWrap.append(conf); card.append(confWrap); if(q.tipo==='mcq'){ const opts = q.opcoes || []; opts.forEach((opt,i)=>{ const b = document.createElement('button'); b.className='btn'; b.textContent = opt; b.onclick = ()=>submit(opt.trim().toLowerCase() === (opts[0]||'').trim().toLowerCase()); card.append(b); }); } else { const ans = document.createElement('input'); ans.className='btn'; ans.placeholder='Sua resposta'; ans.style.minWidth='260px'; const ok = document.createElement('button'); ok.className='btn btn--primary'; ok.textContent='Responder'; ok.onclick = ()=>submit((ans.value||'').trim().toLowerCase() === (q.correta||'').trim().toLowerCase()); card.append(ans, ok); } if(q.dica){ const d = document.createElement('div'); d.className='small muted'; d.textContent = 'Dica: '+q.dica; card.append(d); } wrap.append(card); function submit(isRight){ preds.push(Number(conf.value)); results.push(isRight?1:0); correct += isRight?1:0; idx++; render(); } }; render(); } }; /* ---------------------- Charts (Canvas API) ------------------------------- */ const Charts = { ctx(id){ return document.getElementById(id).getContext('2d'); }, drawLine(ctx, series){ const W = ctx.canvas.clientWidth, H = ctx.canvas.clientHeight; ctx.canvas.width = W * devicePixelRatio; ctx.canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H); const pad = 28; const xs = series.map((_,i)=>pad + i*((W-pad*2)/(series.length-1||1))); const max = Math.max(1, ...series); ctx.strokeStyle = '#6ee7ff'; ctx.lineWidth = 2; ctx.beginPath(); series.forEach((v,i)=>{ const y = H - pad - (v/max)*(H-pad*2); if(i===0) ctx.moveTo(xs[i], y); else ctx.lineTo(xs[i], y); }); ctx.stroke(); ctx.fillStyle = '#9aa3b2'; ctx.font = '12px system-ui'; ctx.fillText('0', 6, H-pad+12); ctx.fillText(String(max), 6, pad+4); }, drawBar(ctx, labels, series){ const W = ctx.canvas.clientWidth, H = ctx.canvas.clientHeight; ctx.canvas.width = W * devicePixelRatio; ctx.canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H); const pad = 28; const bw = (W-pad*2)/labels.length - 10; const max = Math.max(1, ...series); labels.forEach((lab,i)=>{ const x = pad + i*(bw+10); const h = (series[i]/max)*(H-pad*2); ctx.fillStyle = '#48c6f8'; ctx.fillRect(x, H-pad-h, bw, h); ctx.fillStyle = '#9aa3b2'; ctx.font = '12px system-ui'; ctx.fillText(lab, x, H-8); }); }, drawPie(ctx, values){ const W = ctx.canvas.clientWidth, H = ctx.canvas.clientHeight; ctx.canvas.width = W * devicePixelRatio; ctx.canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H); const cx=W/2, cy=H/2, r=Math.min(W,H)/3; const sum = values.reduce((a,b)=>a+b,0)||1; let ang= -Math.PI/2; values.forEach((v,i)=>{ const a = (v/sum)*Math.PI*2; ctx.beginPath(); ctx.moveTo(cx,cy); ctx.arc(cx,cy,r,ang,ang+a); ctx.closePath(); // cores diferentes por fatia (sem especificar paleta fixa) ctx.fillStyle = `hsl(${(i*50)%360} 80% 60%)`; ctx.fill(); ang+=a; }); }, drawScatter(ctx, points){ const W = ctx.canvas.clientWidth, H = ctx.canvas.clientHeight; ctx.canvas.width = W * devicePixelRatio; ctx.canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H); const pad = 28; const xs = points.map(p=>p[0]); const ys = points.map(p=>p[1]); const maxX = Math.max(100, ...xs), maxY = Math.max(100, ...ys); points.forEach(([x,y])=>{ const px = pad + (x/maxX)*(W-pad*2); const py = H - pad - (y/maxY)*(H-pad*2); ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI*2); ctx.fillStyle = '#6ee7ff'; ctx.fill(); }); ctx.fillStyle='#9aa3b2'; ctx.font='12px system-ui'; ctx.fillText('Previsão', W/2-20, H-6); ctx.save(); ctx.rotate(-Math.PI/2); ctx.fillText('Acerto', -H/2-10, 12); ctx.restore(); }, heatmap(ctx, matrix){ // 7x24 const W = ctx.canvas.clientWidth, H = ctx.canvas.clientHeight; ctx.canvas.width = W * devicePixelRatio; ctx.canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.clearRect(0,0,W,H); const cols = 24, rows = 7, cw = W/cols, ch = H/rows; let max = 1; for(let r=0;r<rows;r++) for(let c=0;c<cols;c++) max = Math.max(max, matrix[r][c]); for(let r=0;r<rows;r++){ for(let c=0;c<cols;c++){ const v = matrix[r][c]/max; ctx.fillStyle = `rgba(110,231,255,${v*.85})`; ctx.fillRect(c*cw, r*ch, cw-2, ch-2); } } }, renderAll(){ // Focus last 7 days const days = [...Array(7)].map((_,i)=>{ const d=new Date(); d.setDate(d.getDate()- (6-i)); return d; }); const series = days.map(d=>{ const iso = d.toISOString().slice(0,10); return state.pomodoros.filter(p=> new Date(p.startTs).toISOString().slice(0,10)===iso && p.kind==='focus' && p.endTs).reduce((a,b)=>a+b.plannedMin,0); }); Charts.drawLine(Charts.ctx('chartFocus'), series); // Tags time (agenda) const tagsMap = {}; state.events.forEach(e=>{ const dur = (e.endMin-e.startMin)/60; (e.tags||['geral']).forEach(t=> tagsMap[t]=(tagsMap[t]||0)+dur ); }); const tagVals = Object.values(tagsMap); Charts.drawPie(Charts.ctx('chartTags'), tagVals.length?tagVals:[1]); // Quiz accuracy bars (by theme) const rec = state.records.filter(r=>r.fonte==='quiz'); const groups = {}; rec.forEach(r=> { groups['quiz'] = groups['quiz'] || []; groups['quiz'].push(r.acertou); }); const labels = Object.keys(groups); const accs = labels.map(k => Math.round(groups[k].reduce((a,b)=>a+b,0)/groups[k].length)); Charts.drawBar(Charts.ctx('chartQuiz'), labels.length?labels:['quiz'], accs.length?accs:[0]); // Calibration scatter: previsão vs resultado em % const points = state.records.filter(r=>r.fonte==='quiz').map(r=> [r.confianca||0, r.acertou||0]); Charts.drawScatter(Charts.ctx('chartCalib'), points.length?points:[[0,0]]); // Heatmap (7x24) com minutos focados provenientes de eventos "done" + pomodoros const mat = [...Array(7)].map(()=>Array(24).fill(0)); state.events.forEach(e=>{ const d = new Date(e.dataISO); const idx = (d.getDay()+6)%7; // segunda=0 for(let h=Math.floor(e.startMin/60); h<Math.ceil(e.endMin/60); h++){ mat[idx][h] += 15; } }); state.pomodoros.filter(p=>p.kind==='focus' && p.endTs).forEach(p=>{ const d = new Date(p.startTs); const idx = (d.getDay()+6)%7; const h = d.getHours(); mat[idx][h] += p.plannedMin; }); Charts.heatmap(Charts.ctx('heatmap'), mat); } }; /* ---------------------- Config ------------------------------------------- */ const Config = { syncUI(){ document.getElementById('soundSel').value = state.user.sound; document.getElementById('contrastSel').value = state.user.themeContrast; document.getElementById('fontSel').value = state.user.fontScale || 1; }, apply(){ document.documentElement.style.setProperty('--text', state.user.themeContrast==='on' ? '#ffffff' : '#e6e9f5'); document.body.style.fontSize = (state.user.fontScale||1)+'em'; } }; /* ---------------------- Export / Import / CSV ----------------------------- */ function exportCSV(){ const rows = [['tipo','data','titulo','duracao_min','tags']]; state.events.forEach(e=>{ rows.push(['evento', e.dataISO, e.titulo, (e.endMin-e.startMin)/60, (e.tags||[]).join('|')]); }); const txt = rows.map(r=>r.map(x=>String(x).replaceAll('"','""')).map(x=>`"${x}"`).join(',')).join('\n'); const blob = new Blob([txt], {type:'text/csv'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download='focusplus_export.csv'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href), 500); } /* ---------------------- UI References & Wiring --------------------------- */ const UIRefs = { timer: document.getElementById('timer'), timerClock: document.getElementById('timerClock'), timerPhase: document.getElementById('timerPhase'), }; window.UI = Object.assign(window.UI||{}, UIRefs); // for Pomodoro /* Events: Navigation by hash */ window.addEventListener('hashchange', ()=>{ const hash = location.hash.replace('#',''); if(['dashboard','agenda','estudo','relatorios','config'].includes(hash)) UI.goto(hash); }); /* ---------- DOM Ready ---------------------------------------------------- */ document.addEventListener('DOMContentLoaded', ()=>{ // Init Nav UI.initNav(); // Pomodoro Prefs Pomodoro.readPrefs(); ['prefFocus','prefShort','prefLong','prefCycles'].forEach(id=> document.getElementById(id).addEventListener('change', ()=>Pomodoro.writePrefs())); // Timer actions document.getElementById('startBtn').onclick = ()=>Pomodoro.start('focus'); document.getElementById('pauseBtn').onclick = ()=>Pomodoro.pause(); document.getElementById('resetBtn').onclick = ()=>Pomodoro.reset(); document.getElementById('skipBtn').onclick = ()=>Pomodoro.skipBreak(); document.getElementById('quickPomodoro').onclick = ()=>{ UI.goto('dashboard'); Pomodoro.start('focus'); }; // Agenda controls document.getElementById('prevWeekBtn').onclick = ()=>{ Calendar.currentMonday.setDate(Calendar.currentMonday.getDate()-7); Calendar.render(); renderTasks(); }; document.getElementById('nextWeekBtn').onclick = ()=>{ Calendar.currentMonday.setDate(Calendar.currentMonday.getDate()+7); Calendar.render(); renderTasks(); }; document.getElementById('todayBtn').onclick = ()=>{ const d=new Date(); const day=d.getDay(); const diff=(day===0?-6:1-day); Calendar.currentMonday = new Date(d); Calendar.currentMonday.setDate(d.getDate()+diff); Calendar.currentMonday.setHours(0,0,0,0); Calendar.render(); renderTasks(); }; document.getElementById('viewMode').onchange = ()=>Calendar.render(); document.getElementById('startFromSelectionBtn').onclick = ()=>{ const id = Calendar.selectedEventId; const ev = state.events.find(e=>e.id===id); if(!ev){ U.toast('Selecione um bloco.'); return; } Pomodoro.startedFromEventId = ev.id; UI.goto('dashboard'); Pomodoro.phase='focus'; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); Pomodoro.start('focus'); }; // Task modal document.getElementById('newTaskBtn').onclick = ()=>{ document.getElementById('taskModal').dataset.open='true'; }; document.getElementById('closeTaskModal').onclick = ()=>{ document.getElementById('taskModal').dataset.open='false'; }; document.getElementById('saveTaskBtn').onclick = ()=>{ const t = { id: U.id(), titulo: document.getElementById('tkTitle').value.trim()||'Nova tarefa', cor: document.getElementById('tkColor').value, estimativaMin: Number(document.getElementById('tkEst').value||30), tags: document.getElementById('tkTags').value.split(',').map(s=>s.trim()).filter(Boolean), descricao: document.getElementById('tkDesc').value.trim() }; state.tasks.push(t); Store.save(state); document.getElementById('taskModal').dataset.open='false'; renderTasks(); }; // Event modal close global document.getElementById('closeEventModal').onclick = ()=>{ document.getElementById('eventModal').dataset.open='false'; }; // Calendar initial render Calendar.render(); renderTasks(); // Flashcards form document.getElementById('fcForm').addEventListener('submit', (e)=>{ e.preventDefault(); Flashcards.add({ frente: document.getElementById('fcFront').value.trim(), verso: document.getElementById('fcBack').value.trim(), tags: document.getElementById('fcTags').value.split(',').map(s=>s.trim()).filter(Boolean), midia: U.sanitizeURL(document.getElementById('fcMedia').value) }); e.target.reset(); }); document.getElementById('revealBtn').onclick = ()=>{ document.getElementById('studyA').hidden = false; }; document.getElementById('nextBtn').onclick = ()=>StudyQueue.next(); document.querySelectorAll('.sm2-buttons button').forEach(b=>{ b.onclick = ()=>{ const c = StudyQueue.current(); if(!c) return; Flashcards.sm2Update(c, Number(b.dataset.q)); StudyQueue.next(); Game.addXP(2); }; }); document.getElementById('rebuildQueueBtn').onclick = ()=>StudyQueue.rebuild(); // Import CSV flashcards document.getElementById('importCsvCards').onclick = ()=>document.getElementById('csvInput').click(); document.getElementById('csvInput').addEventListener('change', (e)=>{ const f = e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ const lines = (r.result||'').split(/\r?\n/).filter(Boolean); lines.forEach(line=>{ const [frente, verso, tags] = line.split(',').map(s=>s.trim()); if(frente && verso) Flashcards.add({ frente, verso, tags: (tags||'').split('|').filter(Boolean), midia:'' }); }); U.toast('CSV importado.'); }; r.readAsText(f); }); // Quiz form document.getElementById('quizForm').addEventListener('submit', (e)=>{ e.preventDefault(); const item = { id: U.id(), tipo: document.getElementById('qType').value, tema: document.getElementById('qTheme').value.trim(), prompt: document.getElementById('qPrompt').value.trim(), opcoes: (document.getElementById('qOptions').value||'').split('|').map(s=>s.trim()).filter(Boolean), correta: document.getElementById('qAnswer').value.trim(), dica: document.getElementById('qHint').value.trim(), }; Quizzes.add(item); e.target.reset(); U.toast('Questão adicionada.'); }); document.getElementById('startQuizBtn').onclick = ()=>Quizzes.startMini(5); // Simulado document.getElementById('startSimBtn').onclick = ()=>{ const theme = document.getElementById('simTheme').value.trim().toLowerCase(); const minutes = Number(document.getElementById('simMinutes').value||20); const pool = theme ? state.quizbank.filter(q=>(q.tema||'').toLowerCase().includes(theme)) : state.quizbank; if(pool.length<5) { U.toast('Poucas questões nesse tema.'); return; } let left = minutes*60; const area = document.getElementById('simArea'); let timer = setInterval(()=>{ left--; if(left<=0){ clearInterval(timer); area.innerHTML='<div class="muted small">Fim do tempo!</div>'; } else { badge.textContent = U.fmtTime(left); } }, 1000); area.innerHTML = ''; const badge = document.createElement('div'); badge.className='small muted'; badge.textContent = U.fmtTime(left); area.append(badge); const qs = [...pool].sort(()=>Math.random()-.5).slice(0, Math.min(20, pool.length)); const tbl = document.createElement('table'); tbl.className='table'; const tb=document.createElement('tbody'); qs.forEach((q,i)=>{ const tr = document.createElement('tr'); const td=document.createElement('td'); td.innerHTML = ``; if(q.tipo==='mcq'){ (q.opcoes||[]).forEach(opt=>{ const b = document.createElement('button'); b.className='btn'; b.textContent=opt; b.onclick = (ev)=>{ ev.target.disabled=true; }; td.append(document.createElement('br'), b); }); } else { const inp = document.createElement('input'); inp.className='btn'; inp.placeholder='Resposta'; td.append(document.createElement('br'), inp); } tr.append(td); tb.append(tr); }); tbl.append(tb); area.append(tbl); }; // Reports document.getElementById('rebuildChartsBtn').onclick = ()=>Charts.renderAll(); // Export/Import document.getElementById('exportBtn').onclick = ()=>Store.export(); document.getElementById('exportBtn2').onclick = ()=>Store.export(); document.getElementById('importInput').addEventListener('change', handleImport); document.getElementById('importInput2').addEventListener('change', handleImport); function handleImport(e){ const f = e.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ try{ const obj=JSON.parse(r.result); Store.import(obj); }catch(e){ U.toast('Arquivo inválido.'); } }; r.readAsText(f); } document.getElementById('exportCsvBtn').onclick = exportCSV; // Config document.getElementById('notifBtn').onclick = ()=>Notifications.request(); document.getElementById('soundSel').onchange = (e)=>{ state.user.sound=e.target.value; Store.save(state); }; document.getElementById('contrastSel').onchange = (e)=>{ state.user.themeContrast=e.target.value; Store.save(state); Config.apply(); }; document.getElementById('fontSel').onchange = (e)=>{ state.user.fontScale=Number(e.target.value); Store.save(state); Config.apply(); }; // Wipe document.getElementById('wipeBtn').onclick = ()=>{ if(confirm('Tem certeza que deseja apagar todos os dados?')) { localStorage.removeItem(Store.key); state = defaultState(); UI.refreshAll(); U.toast('Dados apagados.'); } }; // Event modal open by dblclick document.getElementById('calendarGrid').addEventListener('dblclick', (e)=>{ // Create quick event at clicked time const dayColEl = e.target.closest('.day'); if(!dayColEl) return; const rect = dayColEl.getBoundingClientRect(); const y = e.clientY - rect.top; const minute = Math.round((y / 48) * 60 / 15) * 15; // snap const dayIndex = Number(dayColEl.dataset.col); const d = new Date(Calendar.currentMonday); d.setDate(d.getDate()+dayIndex); const ev = { id: U.id(), titulo: 'Bloco', dataISO: U.ymd(d), startMin: minute, endMin: Math.min(24*60, minute+30), cor:'#4aa3ff', tags:[], notas:'', done:false }; state.events.push(ev); Store.save(state); Calendar.render(); Calendar.openModal(ev.id); }); // Open event modal on Enter document.getElementById('calendarGrid').addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ const id = Calendar.selectedEventId; if(id) Calendar.openModal(id); } }); // Modals ESC close document.addEventListener('keydown', (e)=>{ if(e.key==='Escape'){ document.querySelectorAll('.modal[data-open="true"]').forEach(m=> m.dataset.open='false'); UI.help(false); } }); // Help document.getElementById('helpBtn').onclick = ()=>UI.help(true); // Keyboard shortcuts document.addEventListener('keydown', (e)=>{ if(e.key==='?' || (e.shiftKey && e.key==='/')){ e.preventDefault(); UI.help(true); } if(['d','D'].includes(e.key)) UI.goto('dashboard'); if(['g','G'].includes(e.key)) UI.goto('agenda'); if(['e','E'].includes(e.key)) UI.goto('estudo'); if(['r','R'].includes(e.key)) UI.goto('relatorios'); if(['s','S'].includes(e.key) && !e.metaKey && !e.ctrlKey) UI.goto('config'); // Pomodoro keys if(e.code==='Space'){ e.preventDefault(); if(Pomodoro.timerId) Pomodoro.pause(); else Pomodoro.start('focus'); } if(['l','L'].includes(e.key)) Pomodoro.skipBreak(); if(['E'].includes(e.key) && e.shiftKey) Pomodoro.reset(); // Study keys if(['n','N'].includes(e.key)) StudyQueue.next(); if(['0','1','2','3','4','5'].includes(e.key)){ const c = StudyQueue.current(); if(c){ Flashcards.sm2Update(c, Number(e.key)); StudyQueue.next(); Game.addXP(2); } } }); // Immersive document.getElementById('enterImmersive').onclick = ()=>{ const elem = document.documentElement; if(!document.fullscreenElement){ elem.requestFullscreen?.(); } else { document.exitFullscreen?.(); } }; // Prevent data loss on unload window.addEventListener('beforeunload', (e)=>{ // se pomodoro em andamento, alerta if(Pomodoro.timerId){ e.preventDefault(); e.returnValue = 'Sessão em andamento. Tem certeza que deseja sair?'; } }); // Blur/focus interruptions window.addEventListener('blur', ()=>{ if(Pomodoro.timerId){ state.interruptions.push({ id: U.id(), ts: Date.now(), tipo:'blur', duracaoSeg:0 }); Store.save(state); }}); window.addEventListener('focus', ()=>{ /* poderia fechar interrupção com duracao */ }); // Initial Config.apply(); UI.goto((location.hash||'#dashboard').replace('#','')); UI.refreshAll(); }); /* ---------------------- DOM Helpers (for Pomodoro refs) ------------------ */ const UI_timer = document.getElementById('timer'); const UI_timerClock = document.getElementById('timerClock'); const UI_timerPhase = document.getElementById('timerPhase'); UI.timer = UI_timer; UI.timerClock = UI_timerClock; UI.timerPhase = UI_timerPhase; </script> </body> </html>
Resumo rápido (o que este prompt pede ao modelo):
Você vai corrigir bugs reportados (atalhos ativando enquanto o usuário digita, arrastar/soltar da agenda falhando, funcionalidades da aba “Estudo” inativas, muitos botões não interativos), implementar melhorias proativas, escrever um conjunto de testes automáticos integrados que verifiquem todas as funcionalidades das todas as abas e controles, e só após os testes automáticos passarem 100% deverá emitir o arquivo HTML único (com<style>
e<script>
embutidos). O arquivo pode ter mais linhas que o original — priorize robustez e correção. Use apenas HTML/CSS/JS puro (sem frameworks externos).
Não comece a gerar o arquivo final (o HTML completo) até que todos os testes automáticos implementados no próprio arquivo terminem com status “PASS”.
O arquivo final deve ser um único .html
com <head>
, <style>
(todo CSS) e <script>
(todo JS), claramente comentado e modular.
Implemente um modo de teste automático embutido no HTML: ao abrir a página em navegador, deve existir um painel “Test Runner” (colapsável) que execute uma suíte completa. O Test Runner deve:
Ao final do Test Runner passar, imprima para o usuário (em um modal e no console) um resumo das correções aplicadas e as melhorias proativas implementadas.
O HTML gerado deve incluir os testes e o modo de relatório. Não entregue só código: entregue também o relatório de execução dos testes (dentro da página) e um sumário textual no topo do arquivo (comentado) explicando as mudanças.
input
, textarea
, elementos contenteditable
ou widgets de edição (inclusive campos de edição inline do nome de um bloco).localStorage
não disponível.Regra: atalhos globais só funcionam quando nenhum campo editável está focado.
Como implementar:
document.addEventListener('keydown', globalShortcutHandler, {capture: false});
document.activeElement
for input
, textarea
, ou contenteditable
(element.isContentEditable === true
), ou se element.closest('[data-ignore-shortcuts]')
for verdadeiro.data-allow-shortcuts="true"
explicitamente no campo para permitir atalhos locais.isEditing()
— robusta: verifica tagName
, type
, isContentEditable
, role="textbox"
, e aria-hidden
/disabled
.event.preventDefault()
apenas depois de confirmar que você vai executar o atalho (evitar cancelar a digitação).Requisitos funcionais principais:
localStorage
com version
(para migração futura).pointerdown
/pointermove
/pointerup
, setPointerCapture
quando aplicável.Como implementar (detalhes técnicos):
{id, title, startISO, endISO, color, tags:[], notes, createdAt, updatedAt}
.Date.toISOString()
) para evitar problemas de timezone; render convertendo para timezone local.Pointer Events
(single handler para mouse/touch/stylus). No pointerdown
, setPointerCapture(evt.pointerId)
e calcular deslocamento inicial. No pointermove
, atualizar transform
usando requestAnimationFrame
para performance. No pointerup
, soltar captura e efetivar a posição.start
e end
aos múltiplos de 15/30min.pointer
handlers separados. Calcular mínima duração (ex.: 15 min) e máxima (ex.: 12h).Testes obrigatórios para agenda (Test Runner deve cobrir):
end > start
e snap válido.Módulos esperados a funcionar (mínimo): flashcards (SM-2/Leitner), quizzes (múltipla escolha/resposta curta), criação/edição de cards, interleaving scheduler, pós-sessão recuperação.
Implementação técnica sugerida:
{id, question, answer, intervalDays, efactor, reps, lapses, deckId, tags, history:[{date, result, time}]}
para SM-2.scheduleNext(card)
que atualiza intervalDays
e efactor
seguindo SM-2 simplificado; alternativa: implementar sistema Leitner (boxes) se preferível.today + 1
.Test Runner — cenários obrigatórios na aba Estudo:
once
ou passive
quando aplicável ({ passive: true }
) para scroll/touch performance.removeEventListener
) se necessário.localStorage
com chave contendo versão: focussite_v1
.{ version:1, createdAt, data: { agenda:[], decks:[], settings:{}, stats:{} } }
.assert(description, condition)
que registra pass/fail.simulate(type, target, options)
que pode dispatchEvents (MouseEvent, PointerEvent, KeyboardEvent) com parâmetros precisos.(implante ao gerar o código final)
createBlock
, startPomodoro
, scheduleSM2
, runAutomatedTests
).ui.*
, storage.*
, scheduler.*
, tests.*
.eval
. Evitar polyfills complexos — se necessário, documentar.Um único arquivo HTML que:
Ao abrir o arquivo no navegador e iniciar o Test Runner:
Ao concluir, o modelo deve também fornecer baixo o HTML final: comentário inicial com “CHANGELOG de correções” e “NOVAS FEATURES PROATIVAS” listadas.
onclick
inline — anexe listeners após DOMContentLoaded
e verifique idempotência (multiples binds não).arquivo.html
completo (único arquivo) com todo o código e com o relatório de correções dentro dele.Nota do usuário (para você que gerará o código): implemente as correções sem me perguntar nada, corrige os atalhos que ativam enquanto escrevo títulos de blocos, faça o drag-and-drop da agenda funcionar em desktop e mobile, corrija as funcionalidades da aba Estudo (quizzes, flashcards e agendamento), teste absolutamente tudo com a suíte embutida e só entregue o código final quando os testes passarem.
Resumo rápido (o que este prompt pede ao modelo):
Você vai corrigir bugs reportados (atalhos ativando enquanto o usuário digita, arrastar/soltar da agenda falhando, funcionalidades da aba “Estudo” inativas, muitos botões não interativos), implementar melhorias proativas, escrever um conjunto de testes automáticos integrados que verifiquem todas as funcionalidades das todas as abas e controles, e só após os testes automáticos passarem 100% deverá emitir o arquivo HTML único (com<style>
e<script>
embutidos). O arquivo pode ter mais linhas que o original — priorize robustez e correção. Use apenas HTML/CSS/JS puro (sem frameworks externos).
Não comece a gerar o arquivo final (o HTML completo) até que todos os testes automáticos implementados no próprio arquivo terminem com status “PASS”.
O arquivo final deve ser um único .html
com <head>
, <style>
(todo CSS) e <script>
(todo JS), claramente comentado e modular.
Implemente um modo de teste automático embutido no HTML: ao abrir a página em navegador, deve existir um painel “Test Runner” (colapsável) que execute uma suíte completa. O Test Runner deve:
Ao final do Test Runner passar, imprima para o usuário (em um modal e no console) um resumo das correções aplicadas e as melhorias proativas implementadas.
O HTML gerado deve incluir os testes e o modo de relatório. Não entregue só código: entregue também o relatório de execução dos testes (dentro da página) e um sumário textual no topo do arquivo (comentado) explicando as mudanças.
input
, textarea
, elementos contenteditable
ou widgets de edição (inclusive campos de edição inline do nome de um bloco).localStorage
não disponível.Regra: atalhos globais só funcionam quando nenhum campo editável está focado.
Como implementar:
document.addEventListener('keydown', globalShortcutHandler, {capture: false});
document.activeElement
for input
, textarea
, ou contenteditable
(element.isContentEditable === true
), ou se element.closest('[data-ignore-shortcuts]')
for verdadeiro.data-allow-shortcuts="true"
explicitamente no campo para permitir atalhos locais.isEditing()
— robusta: verifica tagName
, type
, isContentEditable
, role="textbox"
, e aria-hidden
/disabled
.event.preventDefault()
apenas depois de confirmar que você vai executar o atalho (evitar cancelar a digitação).Requisitos funcionais principais:
localStorage
com version
(para migração futura).pointerdown
/pointermove
/pointerup
, setPointerCapture
quando aplicável.Como implementar (detalhes técnicos):
{id, title, startISO, endISO, color, tags:[], notes, createdAt, updatedAt}
.Date.toISOString()
) para evitar problemas de timezone; render convertendo para timezone local.Pointer Events
(single handler para mouse/touch/stylus). No pointerdown
, setPointerCapture(evt.pointerId)
e calcular deslocamento inicial. No pointermove
, atualizar transform
usando requestAnimationFrame
para performance. No pointerup
, soltar captura e efetivar a posição.start
e end
aos múltiplos de 15/30min.pointer
handlers separados. Calcular mínima duração (ex.: 15 min) e máxima (ex.: 12h).Testes obrigatórios para agenda (Test Runner deve cobrir):
end > start
e snap válido.Módulos esperados a funcionar (mínimo): flashcards (SM-2/Leitner), quizzes (múltipla escolha/resposta curta), criação/edição de cards, interleaving scheduler, pós-sessão recuperação.
Implementação técnica sugerida:
{id, question, answer, intervalDays, efactor, reps, lapses, deckId, tags, history:[{date, result, time}]}
para SM-2.scheduleNext(card)
que atualiza intervalDays
e efactor
seguindo SM-2 simplificado; alternativa: implementar sistema Leitner (boxes) se preferível.today + 1
.Test Runner — cenários obrigatórios na aba Estudo:
once
ou passive
quando aplicável ({ passive: true }
) para scroll/touch performance.removeEventListener
) se necessário.localStorage
com chave contendo versão: focussite_v1
.{ version:1, createdAt, data: { agenda:[], decks:[], settings:{}, stats:{} } }
.assert(description, condition)
que registra pass/fail.simulate(type, target, options)
que pode dispatchEvents (MouseEvent, PointerEvent, KeyboardEvent) com parâmetros precisos.(implante ao gerar o código final)
createBlock
, startPomodoro
, scheduleSM2
, runAutomatedTests
).ui.*
, storage.*
, scheduler.*
, tests.*
.eval
. Evitar polyfills complexos — se necessário, documentar.Um único arquivo HTML que:
Ao abrir o arquivo no navegador e iniciar o Test Runner:
Ao concluir, o modelo deve também fornecer baixo o HTML final: comentário inicial com “CHANGELOG de correções” e “NOVAS FEATURES PROATIVAS” listadas.
onclick
inline — anexe listeners após DOMContentLoaded
e verifique idempotência (multiples binds não).arquivo.html
completo (único arquivo) com todo o código e com o relatório de correções dentro dele.Nota do usuário (para você que gerará o código): implemente as correções sem me perguntar nada, corrige os atalhos que ativam enquanto escrevo títulos de blocos, faça o drag-and-drop da agenda funcionar em desktop e mobile, corrija as funcionalidades da aba Estudo (quizzes, flashcards e agendamento), teste absolutamente tudo com a suíte embutida e só entregue o código final quando os testes passarem.
perfeito! abaixo está o prompt pronto para copiar e colar em outro chat, exatamente no formato solicitado. 🚀
Resumo rápido (o que este prompt pede ao modelo): Você vai corrigir bugs reportados (atalhos ativando enquanto o usuário digita, arrastar/soltar da agenda falhando, funcionalidades da aba “Estudo” inativas, muitos botões não interativos), implementar melhorias proativas, escrever um conjunto de testes automáticos integrados que verifiquem todas as funcionalidades das todas as abas e controles, e só após os testes automáticos passarem 100% deverá emitir o arquivo HTML único (com
<style>
e<script>
embutidos). O arquivo pode ter mais linhas que o original — priorize robustez e correção. Use apenas HTML/CSS/JS puro (sem frameworks externos).
.html
com <head>
, <style>
(todo CSS) e <script>
(todo JS), claramente comentado e modular.input
, textarea
, elementos contenteditable
ou widgets de edição (inclusive campos de edição inline do nome de um bloco).localStorage
não disponível.document.addEventListener('keydown', globalShortcutHandler, {capture: false});
document.activeElement
for input
, textarea
, ou contenteditable
(element.isContentEditable === true
), ou se element.closest('[data-ignore-shortcuts]')
for verdadeiro.data-allow-shortcuts="true"
explicitamente no campo para permitir atalhos locais.isEditing()
— robusta: verifica tagName
, type
, isContentEditable
, role="textbox"
, e aria-hidden
/disabled
.event.preventDefault()
apenas depois de confirmar que você vai executar o atalho (evitar cancelar a digitação).localStorage
com version
(para migração futura).pointerdown
/pointermove
/pointerup
, setPointerCapture
quando aplicável.{id, title, startISO, endISO, color, tags:[], notes, createdAt, updatedAt}
.Date.toISOString()
) para evitar problemas de timezone; render convertendo para timezone local.Pointer Events
(single handler para mouse/touch/stylus). No pointerdown
, setPointerCapture(evt.pointerId)
e calcular deslocamento inicial. No pointermove
, atualizar transform
usando requestAnimationFrame
para performance. No pointerup
, soltar captura e efetivar a posição.start
e end
aos múltiplos de 15/30min.pointer
handlers separados. Calcular mínima duração (ex.: 15 min) e máxima (ex.: 12h).end > start
e snap válido.{id, question, answer, intervalDays, efactor, reps, lapses, deckId, tags, history:[{date, result, time}]}
para SM-2.scheduleNext(card)
que atualiza intervalDays
e efactor
seguindo SM-2 simplificado; alternativa: implementar sistema Leitner (boxes) se preferível.today + 1
.once
ou passive
quando aplicável ({ passive: true }
) para scroll/touch performance.removeEventListener
) se necessário.localStorage
com chave contendo versão: focussite_v1
.{ version:1, createdAt, data: { agenda:[], decks:[], settings:{}, stats:{} } }
.assert(description, condition)
que registra pass/fail.simulate(type, target, options)
que pode dispatchEvents (MouseEvent, PointerEvent, KeyboardEvent) com parâmetros precisos.(implante ao gerar o código final)
createBlock
, startPomodoro
, scheduleSM2
, runAutomatedTests
).ui.*
, storage.*
, scheduler.*
, tests.*
.eval
. Evitar polyfills complexos — se necessário, documentar.onclick
inline — anexe listeners após DOMContentLoaded
e verifique idempotência (multiples binds não).arquivo.html
completo (único arquivo) com todo o código e com o relatório de correções dentro dele.Nota do usuário (para você que gerará o código): implemente as correções sem me perguntar nada, corrige os atalhos que ativam enquanto escrevo títulos de blocos, faça o drag-and-drop da agenda funcionar em desktop e mobile, corrija as funcionalidades da aba Estudo (quizzes, flashcards e agendamento), teste absolutamente tudo com a suíte embutida e só entregue o código final quando os testes passarem.
queoro que voce implemente isso agora mesmo tudo isso, no codigo que voce gerou anteriormente, faça tudo isso agora, imediatamente, implemente tudo deste prompt nesse codigo e me de ele pronto
</main> </div> <!-- ====================== TOAST & MODALS ================================= --> <div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="modal" id="eventModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Editar Bloco</h3> <button class="btn" id="closeEventModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="evTitle" data-ignore-shortcuts></label> <label>Cor <input id="evColor" type="color" value="#58a6ff"></label> </div> <div class="row"> <label>Data <input id="evDate" type="date"></label> <label>Início <input id="evStart" type="time" step="900"></label> </div> <div class="row"> <label>Fim <input id="evEnd" type="time" step="900"></label> <label>Tags <input id="evTags" placeholder="matemática, revisão" data-ignore-shortcuts></label> </div> <label>Notas<textarea id="evNotes" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"> <button class="btn btn--danger" id="deleteEventBtn">Remover</button> <div style="flex:1"></div> <button class="btn" id="startPomodoroFromEvent">Pomodoro</button> <button class="btn btn--primary" id="saveEventBtn">Salvar</button> </div> </div> </div> <div class="modal" id="taskModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Nova Tarefa</h3> <button class="btn" id="closeTaskModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="tkTitle" data-ignore-shortcuts></label> <label>Cor <input id="tkColor" type="color" value="#79c0ff"></label> </div> <div class="row"> <label>Estimativa (min) <input id="tkEst" type="number" min="15" step="15" value="30"></label> <label>Tags <input id="tkTags" placeholder="leitura, cálculo" data-ignore-shortcuts></label> </div> <label>Descrição<textarea id="tkDesc" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"><button class="btn btn--primary" id="saveTaskBtn">Adicionar</button></div> </div> </div> <div class="help" id="help" aria-hidden="true" role="dialog" aria-modal="true"> <div class="help__content"> <header style="display:flex;justify-content:space-between;align-items:center"> <h3>Ajuda & Atalhos</h3> <button class="btn" id="closeHelp">Fechar</button> </header> <div class="grid-3"> <div><h4>Global</h4><p><kbd>?</kbd> ajuda • <kbd>D</kbd> dashboard • <kbd>G</kbd> agenda • <kbd>E</kbd> estudo • <kbd>R</kbd> relatórios • <kbd>S</kbd> config</p></div> <div><h4>Pomodoro</h4><p><kbd>Espaço</kbd> iniciar/pausar • <kbd>L</kbd> pular pausa • <kbd>Shift</kbd>+<kbd>E</kbd> reiniciar</p></div> <div><h4>Agenda</h4><p>Setas movem • <kbd>Enter</kbd> edita • <kbd>Shift</kbd>+setas redimensiona • <kbd>Del</kbd> remove</p></div> </div> </div> </div> <!-- ====================== TEST RUNNER (UI) =============================== --> <div class="runner" id="runner" data-open="true" aria-live="polite"> <header> <h4>Test Runner</h4> <div style="display:flex;gap:.4rem;align-items:center"> <span id="runnerStatus" class="badge skip">idle</span> <button class="btn" id="toggleRunner">▾</button> <button class="btn btn--primary" id="runAllBtn">Rodar testes</button> </div> </header> <div class="body"> <div class="small muted">Executa automaticamente ao carregar. Logs abaixo.</div> <div class="log" id="runnerLog"></div> </div> </div> <script> /* ============================================================================= Focus+ PRO — JavaScript modular + Test Runner embutido ============================================================================= */ /* --------------------------- Utils & Env --------------------------------- */ const Env = { version: 2, key: 'focussite_v2', isTest: false, safeMode: false, dpr: window.devicePixelRatio || 1, }; const Debug = { enabled: false, log(...a){ if(this.enabled) console.log('[DEBUG]', ...a); }, }; const U = { id: ()=>'id-'+Math.random().toString(36).slice(2,10), clamp:(v,min,max)=>Math.min(max,Math.max(min,v)), todayISO:()=>new Date().toISOString().slice(0,10), ymd:(d)=>d.toISOString().slice(0,10), toMin:(hhmm)=>{ const [h,m]=String(hhmm).split(':').map(Number); return (isFinite(h)?h:0)*60+(isFinite(m)?m:0); }, minToHHMM:(m)=>`${String(Math.floor(m/60)).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`, fmtTime:(s)=>{const m=Math.floor(s/60),r=s%60;return `${String(m).padStart(2,'0')}:${String(r).padStart(2,'0')}`;}, toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.dataset.open='true'; setTimeout(()=>t.dataset.open='false', 2200); }, download(name,text){ try{ const blob=new Blob([text],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),500); }catch(e){ /* test override */ } }, sanitizeURL(url){ if(!url||typeof url!=='string') return ''; const s=url.trim(); if(s.startsWith('data:image/')||s.startsWith('https://')||s.startsWith('http://')) return s; return ''; }, isEditing(){ const el = document.activeElement; if(!el) return false; const tag = (el.tagName||'').toLowerCase(); const editable = el.isContentEditable || el.getAttribute('role')==='textbox'; const ignore = el.closest?.('[data-ignore-shortcuts]'); const disabled = el.getAttribute?.('aria-disabled')==='true' || el.disabled; if(disabled) return false; if(ignore) return true; if(editable) return true; if(tag==='input' || tag==='textarea') return true; return false; }, }; /* --------------------------- Storage Adapter ----------------------------- */ const StorageAdapter = (function(){ let mem = {}; function readLS(k){ try{ return localStorage.getItem(k); }catch(e){ return null; } } function writeLS(k,v){ try{ localStorage.setItem(k,v); return true; }catch(e){ return false; } } return { mock: false, getItem(k){ return this.mock ? mem[k] ?? null : readLS(k); }, setItem(k,v){ return this.mock ? (mem[k]=v,true) : writeLS(k,v); }, clear(){ if(this.mock){ mem={}; } else try{ localStorage.clear(); }catch(e){} }, setMock(val){ this.mock = !!val; }, }; })(); /* --------------------------- State & Persistence ------------------------- */ const defaultState = ()=>({ version: Env.version, createdAt: Date.now(), user: { name:'', lang:'pt-BR', themeContrast:'off', fontScale:1, sound:'on', notif:'off', nightAuto:true }, settings: { pomodoro:{ focusMin:25, shortMin:5, longMin:15, cycles:4 }, agenda:{ snap:15, conflict:'block' } }, gamification: { xp:0, level:1, badges:[], streak:0, lastDay:null }, tasks: [], events: [], // {id,title,startISO,endISO,color,tags,notes,done} pomodoros: [], interruptions: [], flashcards: [], // {id,question,answer,tags,media,sm2:{EF,interval,n,last,next}} quizbank: [], // {id,tipo,prompt,opcoes,correta,dica,tema} records: [], // quiz results history: { undo:[], redo:[] }, }); let state = loadState(); function loadState(){ try{ const raw = StorageAdapter.getItem(Env.key); if(!raw) return defaultState(); const obj = JSON.parse(raw); if(obj.version !== Env.version) return migrate(obj); return obj; }catch(e){ return defaultState(); } } function migrate(old){ // simples tentativa de migração const fresh = defaultState(); try{ fresh.tasks = old.tasks || []; if(old.events){ fresh.events = old.events.map(e=>{ if(typeof e.dataISO === 'string'){ const d=e.dataISO; const s=U.minToHHMM(e.startMin||540); const en=U.minToHHMM(e.endMin||600); return { id:e.id||U.id(), title:e.titulo||e.title||'Bloco', startISO: `${d}T${s}:00.000Z`, endISO: `${d}T${en}:00.000Z`, color:e.cor||'#58a6ff', tags:e.tags||[], notes:e.notas||'', done:!!e.done, }; } return e; }); } } catch(e){} fresh.version = Env.version; StorageAdapter.setItem(Env.key, JSON.stringify(fresh)); return fresh; } const Persist = { pending:false, last:0, save(throttle=Env.safeMode?500:200){ const now = Date.now(); if(this.pending && now-this.last < throttle) return; this.pending = true; this.last = now; setTimeout(()=>{ this.pending=false; try{ StorageAdapter.setItem(Env.key, JSON.stringify(state)); }catch(e){} }, throttle); }, backup(){ try{ U.download('focusplus_backup.json', JSON.stringify(state,null,2)); }catch(e){} }, import(obj){ if(!obj || typeof obj!=='object') return; state = Object.assign(defaultState(), obj); if(state.version!==Env.version){ state = migrate(state); } Persist.save(0); UI.refreshAll(); U.toast('Dados importados.'); }, wipe(){ StorageAdapter.clear(); state = defaultState(); UI.refreshAll(); U.toast('Dados apagados.'); }, pushHistory(snapshot){ state.history.undo.push(JSON.stringify(snapshot)); state.history.redo.length=0; }, undo(){ const last = state.history.undo.pop(); if(!last) return false; state.history.redo.push(JSON.stringify(state)); state = JSON.parse(last); Persist.save(0); UI.refreshAll(); return true; }, redo(){ const next = state.history.redo.pop(); if(!next) return false; state.history.undo.push(JSON.stringify(state)); state = JSON.parse(next); Persist.save(0); UI.refreshAll(); return true; } }; /* --------------------------- Gamification -------------------------------- */ const Game = { addXP(n){ state.gamification.xp += n; while(state.gamification.xp >= this.nextLevelXP()) state.gamification.level++; Persist.save(); Dashboard.refreshGamification(); }, nextLevelXP(){ const L=state.gamification.level; return Math.floor(50 + L*L*15); }, tickStreak(){ const today = U.todayISO(), last = state.gamification.lastDay; if(last===today) return; const y = new Date(); y.setDate(y.getDate()-1); state.gamification.streak = (last===U.ymd(y)) ? (state.gamification.streak||0)+1 : 1; state.gamification.lastDay = today; Persist.save(); } }; /* --------------------------- Notifications -------------------------------- */ const Notifications = { async request(){ try{ const perm=await Notification.requestPermission(); if(perm==='granted'){ state.user.notif='on'; Persist.save(); U.toast('Notificações ativadas.'); } }catch(e){} }, notify(title,body){ if(state.user.notif==='on' && 'Notification' in window && Notification.permission==='granted'){ new Notification(title,{body}); } else { U.toast(`${title}: ${body}`); } } }; /* --------------------------- Pomodoro ------------------------------------ */ const Pomodoro = { phase:'focus', cycleIndex:1, remainingSec:25*60, timerId:null, startedEventId:null, readPrefs(){ const p=state.settings.pomodoro; prefFocus.value=p.focusMin; prefShort.value=p.shortMin; prefLong.value=p.longMin; prefCycles.value=p.cycles; }, writePrefs(){ const p=state.settings.pomodoro; p.focusMin=+prefFocus.value||25; p.shortMin=+prefShort.value||5; p.longMin=+prefLong.value||15; p.cycles=+prefCycles.value||4; Persist.save(); if(this.phase==='focus') this.setTimer(p.focusMin*60); }, setTimer(sec){ this.remainingSec = sec; timerClock.textContent = U.fmtTime(sec); timer.style.setProperty('--progress', 0); timer.setAttribute('aria-valuenow','0'); }, phaseLabel(){ return this.phase==='focus'?`Foco #${this.cycleIndex}`:(this.phase==='short'?'Pausa Curta':'Pausa Longa'); }, start(kind=null){ if(kind) this.phase=kind; if(this.timerId) return; const p=state.settings.pomodoro; let total = (this.phase==='focus'?p.focusMin:this.phase==='short'?p.shortMin:p.longMin)*60; if(this.remainingSec<=0 || this.remainingSec>total) this.remainingSec = total; const startTs = Date.now(); const cycle = { id:U.id(), kind:this.phase, plannedMin: total/60, startTs, endTs:null, interruptions:[] }; state.pomodoros.push(cycle); Persist.save(); this.timerId = setInterval(()=>{ this.remainingSec -= 1; const passed = total - this.remainingSec; const progress = Math.max(0,Math.min(1,passed/total))*100; timerClock.textContent = U.fmtTime(this.remainingSec); timer.style.setProperty('--progress', progress); timer.setAttribute('aria-valuenow', String(Math.round(progress))); timerPhase.textContent = this.phaseLabel(); if(this.remainingSec<=0){ clearInterval(this.timerId); this.timerId=null; cycle.endTs = Date.now(); Persist.save(); this.onEndCycle(cycle); } }, 1000); }, pause(){ if(this.timerId){ clearInterval(this.timerId); this.timerId=null; } }, reset(){ this.pause(); this.phase='focus'; this.cycleIndex=1; this.startedEventId=null; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); }, skipBreak(){ if(this.phase==='short' || this.phase==='long'){ this.pause(); this.phase='focus'; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); } }, onEndCycle(cycle){ if(cycle.kind==='focus'){ Game.addXP(Math.round(cycle.plannedMin/5)*5); Game.tickStreak(); if(this.startedEventId){ const ev = state.events.find(e=>e.id===this.startedEventId); if(ev){ ev.done = true; Persist.save(); Calendar.render(); } } } Notifications.notify(`Fim de ${cycle.kind==='focus'?'foco':'pausa'}`, 'Hora de trocar de fase!'); const p=state.settings.pomodoro; if(cycle.kind==='focus'){ if(this.cycleIndex % p.cycles === 0){ this.phase='long'; this.setTimer(p.longMin*60); this.cycleIndex=1; } else { this.phase='short'; this.setTimer(p.shortMin*60); this.cycleIndex++; } } else { this.phase='focus'; this.setTimer(p.focusMin*60); } timerPhase.textContent=this.phaseLabel(); if(cycle.kind==='focus'){ const due=Flashcards.dueToday().length; if(due>0) U.toast(`Revisões SM-2: ${due}`); } Dashboard.refresh(); } }; /* --------------------------- Dashboard ----------------------------------- */ const Dashboard = { refresh(){ const day=U.todayISO(); const today = state.pomodoros.filter(p=> new Date(p.startTs).toISOString().slice(0,10)===day && p.endTs); const focusedMin = today.filter(p=>p.kind==='focus').reduce((a,b)=>a+b.plannedMin,0); dashFocused.textContent = `${focusedMin}m`; dashSessions.textContent = `${today.length} ciclos`; dashDue.textContent = `${Flashcards.dueToday().length}`; this.refreshGamification(); }, refreshGamification(){ lvl.textContent=state.gamification.level; xp.textContent=state.gamification.xp; streak.textContent=`Streak: ${state.gamification.streak}`; } }; /* --------------------------- Calendar / Agenda --------------------------- */ const Calendar = { grid: document.getElementById('calendarGrid'), weekLabel: document.getElementById('weekLabel'), modeSel: document.getElementById('viewMode'), selEventId: null, currentMonday: (()=>{ const d=new Date();const day=d.getDay();const diff=(day===0?-6:1-day);d.setDate(d.getDate()+diff);d.setHours(0,0,0,0);return d; })(), render(){ const mode = this.modeSel.value; this.grid.innerHTML=''; // header const head = document.createElement('div'); head.className='col-head'; this.grid.append(head); const days=[]; for(let i=0;i<7;i++){ const d=new Date(this.currentMonday); d.setDate(d.getDate()+i); days.push(d); const h=document.createElement('div');h.className='col-head';h.textContent=d.toLocaleDateString('pt-BR',{weekday:'short',day:'2-digit',month:'2-digit'}); this.grid.append(h); } // hours col const hoursCol=document.createElement('div');hoursCol.className='hours'; for(let h=0;h<24;h++){ const div=document.createElement('div');div.textContent=String(h).padStart(2,'0')+':00'; hoursCol.append(div); } this.grid.append(hoursCol); // day cols for(let i=0;i<7;i++){ const dayCol=document.createElement('div'); dayCol.className='day'; dayCol.dataset.col=i; for(let r=0;r<24;r++){ const row=document.createElement('div'); row.className='row'; row.dataset.row=r; dayCol.append(row); } const dayISO = U.ymd(days[i]); const events = state.events.filter(e=> e.startISO.slice(0,10)===dayISO); events.forEach(ev=> this.addEventDOM(dayCol, ev)); this.grid.append(dayCol); } // nowline const now=new Date(); if(now>=this.currentMonday && now<new Date(+this.currentMonday+7*864e5)){ const nl=document.createElement('div');nl.className='nowline';nl.style.top=`${(now.getHours()*48)+Math.round(now.getMinutes()/60*48)}px`; this.grid.append(nl); } // label const end=new Date(this.currentMonday); end.setDate(end.getDate()+6); this.weekLabel.textContent=`${this.currentMonday.toLocaleDateString()} — ${end.toLocaleDateString()}`; // enable drag selection this.enableSelection(); renderTasks(); }, minutesFromY(y){ return Math.round((y/48)*60/15)*15; }, addEventDOM(dayCol, ev){ const start = new Date(ev.startISO), end = new Date(ev.endISO); const top = (start.getHours()*60+start.getMinutes())*(48/60); const height = Math.max(12, ((end-start)/60000)*(48/60)); const div = document.createElement('div'); div.className='event'; div.dataset.id=ev.id; div.style.top=`${top}px`; div.style.height=`${height}px`; div.style.background=`linear-gradient(180deg, ${ev.color||'#2a3567'}, #1c2657)`; div.tabIndex=0; div.textContent=ev.title||'Bloco'; const rt=document.createElement('div'); rt.className='resize-h resize-top'; const rb=document.createElement('div'); rb.className='resize-h resize-bottom'; div.append(rt, rb); div.addEventListener('click', ()=>this.select(ev.id, div)); // keyboard move/resize div.addEventListener('keydown', (e)=>{ if(this.selEventId!==ev.id) return; const step=15; if(['ArrowUp','ArrowDown','Delete','Enter'].includes(e.key)) e.preventDefault(); const s=new Date(ev.startISO), en=new Date(ev.endISO); if(e.key==='ArrowUp'){ s.setMinutes(s.getMinutes()-step); en.setMinutes(en.getMinutes()-step); } if(e.key==='ArrowDown'){ s.setMinutes(s.getMinutes()+step); en.setMinutes(en.getMinutes()+step); } if(e.key==='Delete'){ this.removeEvent(ev.id); return; } if(e.key==='Enter'){ this.openModal(ev.id); return; } // conflict check const changed = {...ev, startISO:s.toISOString(), endISO:en.toISOString()}; if(this.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ U.toast('Conflito detectado.'); return; } ev.startISO=s.toISOString(); ev.endISO=en.toISOString(); Persist.save(); this.render(); }); // pointer drag/resize let dragging=false, resizing=null, startY=0, startStart=null, startEnd=null; const onDown = (e)=>{ dragging=true; resizing = e.target.classList.contains('resize-top')?'top':e.target.classList.contains('resize-bottom')?'bottom':null; startY=e.clientY; startStart=new Date(ev.startISO); startEnd=new Date(ev.endISO); div.setPointerCapture?.(e.pointerId); }; const onMove = (e)=>{ if(!dragging) return; const dy = e.clientY - startY; const deltaMin = Math.round((dy/(48/60))/15)*15; let s=new Date(startStart), en=new Date(startEnd); if(resizing==='top'){ s.setMinutes(s.getMinutes()+deltaMin); if((en-s)/60000<15) s=new Date(en.getTime()-15*60000); } else if(resizing==='bottom'){ en.setMinutes(en.getMinutes()+deltaMin); if((en-s)/60000<15) en=new Date(s.getTime()+15*60000); } else { s.setMinutes(s.getMinutes()+deltaMin); en.setMinutes(en.getMinutes()+deltaMin); } const changed = {...ev, startISO:s.toISOString(), endISO:en.toISOString()}; if(this.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ return; } ev.startISO=s.toISOString(); ev.endISO=en.toISOString(); Persist.save(); this.render(); }; const onUp = (e)=>{ dragging=false; resizing=null; div.releasePointerCapture?.(e.pointerId); }; div.addEventListener('pointerdown', onDown); div.addEventListener('pointermove', onMove); div.addEventListener('pointerup', onUp); dayCol.append(div); }, enableSelection(){ // drag selection on empty space const grid=this.grid; let selecting=false, box=null, start=null, startCol=0; const onDown=(e)=>{ if(e.target.closest('.event')) return; const colEl=e.target.closest('.day'); if(!colEl) return; startCol=+colEl.dataset.col; selecting=true; start={x:e.clientX,y:e.clientY}; box=document.createElement('div'); box.className='selbox'; grid.append(box); box.style.left=e.clientX-grid.getBoundingClientRect().left+'px'; box.style.top=e.clientY-grid.getBoundingClientRect().top+'px'; box.style.width='0px'; box.style.height='0px'; grid.setPointerCapture?.(e.pointerId); }; const onMove=(e)=>{ if(!selecting) return; const r=grid.getBoundingClientRect(); box.style.width=Math.abs(e.clientX-start.x)+'px'; box.style.height=Math.abs(e.clientY-start.y)+'px'; box.style.left=Math.min(e.clientX,start.x)-r.left+'px'; box.style.top=Math.min(e.clientY,start.y)-r.top+'px'; }; const onUp=(e)=>{ if(!selecting) return; const day=new Date(Calendar.currentMonday); day.setDate(day.getDate()+startCol); const r=grid.querySelector(`.day[data-col="${startCol}"]`).getBoundingClientRect(); const y1=Math.min(start.y,e.clientY)-r.top, y2=Math.max(start.y,e.clientY)-r.top; const sMin=Calendar.minutesFromY(y1), eMin=Calendar.minutesFromY(y2); const s=new Date(day); s.setHours(0,0,0,0); s.setMinutes(sMin); const en=new Date(day); en.setHours(0,0,0,0); en.setMinutes(Math.max(sMin+15,eMin)); const ev = { id:U.id(), title:'Bloco', startISO:s.toISOString(), endISO:en.toISOString(), color:'#58a6ff', tags:[], notes:'', done:false }; if(Calendar.hasConflict(ev)){ U.toast('Conflito: bloco não criado.'); } else { state.events.push(ev); Persist.save(); Calendar.render(); Calendar.openModal(ev.id); } selecting=false; box?.remove(); grid.releasePointerCapture?.(e.pointerId); }; grid.onpointerdown=onDown; grid.onpointermove=onMove; grid.onpointerup=onUp; }, select(id, el){ this.selEventId=id; document.querySelectorAll('.event').forEach(e=>e.dataset.selected='false'); el && (el.dataset.selected='true'); }, removeEvent(id){ Persist.pushHistory(state); state.events=state.events.filter(e=>e.id!==id); Persist.save(); this.render(); U.toast('Bloco removido.'); }, hasConflict(candidate, ignoreId=null){ const dayISO = candidate.startISO.slice(0,10); const s = +new Date(candidate.startISO), en = +new Date(candidate.endISO); return state.events.some(ev=>{ if(ignoreId && ev.id===ignoreId) return false; if(ev.startISO.slice(0,10)!==dayISO) return false; const a=+new Date(ev.startISO), b=+new Date(ev.endISO); return Math.max(a,s) < Math.min(b,en); // overlap }); }, openModal(id){ const ev=state.events.find(e=>e.id===id); if(!ev) return; const m=document.getElementById('eventModal'); m.dataset.open='true'; m.setAttribute('aria-hidden','false'); evTitle.value=ev.title||'Bloco'; evColor.value=ev.color||'#58a6ff'; evDate.value=ev.startISO.slice(0,10); evStart.value=ev.startISO.slice(11,16); evEnd.value=ev.endISO.slice(11,16); evTags.value=(ev.tags||[]).join(', '); evNotes.value=ev.notes||''; saveEventBtn.onclick=()=>{ const sISO = `${evDate.value}T${evStart.value}:00.000Z`; const eISO = `${evDate.value}T${evEnd.value}:00.000Z`; const changed = {...ev, title:evTitle.value.trim()||'Bloco', color:evColor.value, startISO:sISO, endISO:eISO, tags:evTags.value.split(',').map(s=>s.trim()).filter(Boolean), notes:evNotes.value.trim() }; if(Calendar.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ U.toast('Conflito: ajuste horários.'); return; } Persist.pushHistory(state); Object.assign(ev, changed); Persist.save(); Calendar.render(); U.toast('Bloco atualizado.'); close(); }; deleteEventBtn.onclick=()=>{ Calendar.removeEvent(ev.id); close(); }; startPomodoroFromEvent.onclick=()=>{ Pomodoro.startedEventId=ev.id; UI.goto('dashboard'); Pomodoro.phase='focus'; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); Pomodoro.start('focus'); close(); }; function close(){ m.dataset.open='false'; m.setAttribute('aria-hidden','true'); } closeEventModal.onclick=close; }, createFromTask(task, dateISO){ const start=new Date(dateISO+'T09:00:00.000Z'), end=new Date(+start+task.estimativaMin*60000); const ev={ id:U.id(), title:task.titulo, startISO:start.toISOString(), endISO:end.toISOString(), color:task.cor, tags:task.tags, notes:task.descricao, done:false }; if(this.hasConflict(ev)){ U.toast('Conflito: não adicionado.'); return; } Persist.pushHistory(state); state.events.push(ev); Persist.save(); this.render(); } }; function renderTasks(){ const box=document.getElementById('tasksList'); box.innerHTML=''; state.tasks.forEach(t=>{ const div=document.createElement('div'); div.className='task-item'; div.draggable=true; div.innerHTML=`<span>${t.titulo}</span><span class="tag">${(t.tags||[])[0]||'geral'}</span>`; div.addEventListener('dragstart', e=> e.dataTransfer.setData('text/plain', t.id)); box.append(div); }); document.querySelectorAll('.day').forEach(col=>{ col.addEventListener('dragover', e=>e.preventDefault()); col.addEventListener('drop', e=>{ const id=e.dataTransfer.getData('text/plain'); const task=state.tasks.find(x=>x.id===id); if(!task) return; const idx=+col.dataset.col; const d=new Date(Calendar.currentMonday); d.setDate(d.getDate()+idx); Calendar.createFromTask(task, U.ymd(d)); }); }); } /* --------------------------- Flashcards (SM-2) --------------------------- */ const Flashcards = { selectedId:null, dueToday(){ const today=new Date(); return state.flashcards.filter(c=>!c.sm2 || !c.sm2.next || new Date(c.sm2.next)<=today); }, add({question,answer,tags,media}){ Persist.pushHistory(state); const c={ id:U.id(), question, answer, tags, media, sm2:{EF:2.5, interval:0, n:0, last:null, next:new Date().toISOString()} }; state.flashcards.push(c); Persist.save(); this.refresh(); }, edit(id,patch){ const c=state.flashcards.find(x=>x.id===id); if(!c) return; Persist.pushHistory(state); Object.assign(c,patch); Persist.save(); this.refresh(); }, remove(id){ Persist.pushHistory(state); state.flashcards=state.flashcards.filter(c=>c.id!==id); Persist.save(); this.refresh(); }, schedule(card,quality){ // SM-2 simplified const sm=card.sm2||{EF:2.5, interval:0, n:0}; const q=U.clamp(quality,0,5); const now=new Date(); if(q<3){ sm.n=0; sm.interval=1; } else { if(sm.n===0) sm.interval=1; else if(sm.n===1) sm.interval=6; else sm.interval=Math.round(sm.interval*sm.EF); sm.n+=1; sm.EF = Math.max(1.3, sm.EF + (0.1 - (5-q)*(0.08 + (5-q)*0.02))); } sm.last=now.toISOString(); const next=new Date(now); next.setDate(now.getDate()+sm.interval); sm.next=next.toISOString(); card.sm2=sm; Persist.save(); }, refresh(){ const list=document.getElementById('fcList'); list.innerHTML=''; state.flashcards.forEach(c=>{ const div=document.createElement('div'); div.className='fc-item'; div.innerHTML=`<strong>${c.question}</strong> <div class="small muted">${(c.tags||[]).join(', ')||'sem tags'}</div> <div style="display:flex;gap:.4rem;margin-top:.4rem;flex-wrap:wrap"> <button class="btn" data-act="study" data-id="${c.id}">Estudar</button> <button class="btn" data-act="edit" data-id="${c.id}">Editar</button> <button class="btn btn--warn" data-act="del" data-id="${c.id}">Excluir</button> <input class="btn" value="${c.question.replace(/"/g,'"')}" style="flex:1" data-ignore-shortcuts readonly> </div>`; list.append(div); }); list.onclick=(e)=>{ const btn=e.target.closest('button'); if(!btn) return; const id=btn.dataset.id; const act=btn.dataset.act; if(act==='del'){ if(confirm('Excluir card?')) this.remove(id); } if(act==='edit'){ const c=state.flashcards.find(x=>x.id===id); if(!c) return; fcFront.value=c.question; fcBack.value=c.answer; fcTags.value=(c.tags||[]).join(','); fcMedia.value=c.media||''; this.selectedId=id; } if(act==='study'){ this.selectedId=id; StudyQueue.queue=[state.flashcards.find(x=>x.id===id)]; StudyQueue.idx=0; StudyUI.update(); UI.goto('estudo'); Tabs.show('flashcards'); } }; document.getElementById('dashDue').textContent=String(this.dueToday().length); StudyQueue.rebuild(); } }; const StudyQueue = { queue:[], idx:-1, rebuild(){ // interleaving simples por tag (deck = primeira tag) const due=Flashcards.dueToday(); const buckets={}; due.forEach(c=>{ const deck=(c.tags||['geral'])[0]; (buckets[deck]=buckets[deck]||[]).push(c); }); const decks=Object.keys(buckets); const out=[]; let i=0; while(decks.some(d=>buckets[d].length)){ const d=decks[i%decks.length]; const item=buckets[d].shift(); if(item) out.push(item); i++; } this.queue = out.slice(0,40); this.idx = this.queue.length?0:-1; StudyUI.update(); }, current(){ return this.queue[this.idx]; }, next(){ if(this.idx < this.queue.length-1){ this.idx++; StudyUI.update(); } else { U.toast('Fila concluída!'); StudyUI.empty(); } } }; const StudyUI = { card:document.getElementById('studyCard'), q:document.getElementById('studyQ'), a:document.getElementById('studyA'), m:document.getElementById('studyMedia'), empty:()=>{ document.getElementById('studyEmpty').style.display='block'; StudyUI.card.hidden=true; }, update(){ const c=StudyQueue.current(); if(!c){ this.empty(); return; } document.getElementById('studyEmpty').style.display='none'; this.card.hidden=false; this.q.textContent=c.question; this.a.textContent=c.answer; this.a.hidden=true; const src=U.sanitizeURL(c.media); if(src){ this.m.src=src; this.m.style.display='block'; } else { this.m.style.display='none'; } } }; /* --------------------------- Quizzes ------------------------------------- */ const Quizzes = { add(item){ Persist.pushHistory(state); state.quizbank.push(item); Persist.save(); this.refreshList(); }, refreshList(){ const qa=document.getElementById('quizArea'); qa.innerHTML=''; if(!state.quizbank.length){ qa.innerHTML='<div class="muted small">Nenhuma questão.</div>'; return; } const table=document.createElement('table'); table.className='table small'; table.innerHTML='<thead><tr><th>Tema</th><th>Tipo</th><th>Enunciado</th></tr></thead>'; const tb=document.createElement('tbody'); state.quizbank.forEach(q=>{ const tr=document.createElement('tr'); tr.innerHTML=`<td>${q.tema||'-'}</td><td>${q.tipo}</td><td>${q.prompt.slice(0,120)}</td>`; tb.append(tr); }); table.append(tb); qa.append(table); }, startMini(count=5){ const qa=document.getElementById('quizArea'); qa.innerHTML=''; if(state.quizbank.length===0){ qa.innerHTML='<div class="muted small">Adicione questões.</div>'; return; } const items=[...state.quizbank].sort(()=>Math.random()-.5).slice(0,count); let idx=0, correct=0, start=Date.now(), preds=[], results=[]; const wrap=document.createElement('div'); qa.append(wrap); const render=()=>{ if(idx>=items.length){ const dur=Math.round((Date.now()-start)/1000), score=Math.round(correct/items.length*100); state.records.push({ id:U.id(), fonte:'quiz', itemId:'mini', acertou:score, tempoSeg:dur, confianca:Math.round(preds.reduce((a,b)=>a+b,0)/Math.max(1,preds.length)), ts:Date.now() }); Persist.save(); Game.addXP(Math.round(score/10)*5); Charts.renderAll(); Quizzes.refreshList(); qa.innerHTML=`<div class="study-card"><div><strong>Resultado:</strong> ${score}%</div><div class="muted small">Tempo: ${dur}s</div></div>`; return; } const q=items[idx]; wrap.innerHTML=''; const card=document.createElement('div'); card.className='study-card'; const conf=document.createElement('input'); conf.type='range'; conf.min=0; conf.max=100; conf.value=60; conf.style.width='180px'; card.innerHTML=`<div class="q">${q.prompt}</div>`; const confWrap=document.createElement('div'); confWrap.className='small muted'; confWrap.innerHTML='Previsão: '; confWrap.append(conf); card.append(confWrap); if(q.tipo==='mcq'){ const opts=q.opcoes||[]; opts.forEach(opt=>{ const b=document.createElement('button'); b.className='btn'; b.textContent=opt; b.onclick=()=>submit(opt.trim().toLowerCase()===(opts[0]||'').trim().toLowerCase()); card.append(b); }); } else{ const ans=document.createElement('input'); ans.className='btn'; ans.placeholder='Resposta'; const ok=document.createElement('button'); ok.className='btn btn--primary'; ok.textContent='Responder'; ok.onclick=()=>submit((ans.value||'').trim().toLowerCase()===(q.correta||'').trim().toLowerCase()); card.append(ans, ok); } if(q.dica){ const d=document.createElement('div'); d.className='small muted'; d.textContent='Dica: '+q.dica; card.append(d); } wrap.append(card); function submit(isRight){ preds.push(Number(conf.value)); results.push(isRight?1:0); correct += isRight?1:0; idx++; render(); } }; render(); } }; /* --------------------------- Charts (Canvas) ------------------------------ */ const Charts = { ctx(id){ return document.getElementById(id).getContext('2d'); }, drawLine(ctx, series){ const W=ctx.canvas.clientWidth,H=ctx.canvas.clientHeight; ctx.canvas.width=W*Env.dpr; ctx.canvas.height=H*Env.dpr; ctx.setTransform(Env.dpr,0,0,Env.dpr,0,0); ctx.clearRect(0,0,W,H); const pad=28, xs=series.map((_,i)=>pad+i*((W-pad*2)/Math.max(1,series.length-1))), max=Math.max(1,...series); ctx.strokeStyle='#79c0ff'; ctx.lineWidth=2; ctx.beginPath(); series.forEach((v,i)=>{ const y=H-pad-(v/max)*(H-pad*2); if(i===0) ctx.moveTo(xs[i],y); else ctx.lineTo(xs[i],y); }); ctx.stroke(); ctx.fillStyle='#95a0b4'; ctx.font='12px system-ui'; ctx.fillText('0',6,H-pad+12); ctx.fillText(String(max),6,pad+4); }, drawBar(ctx, labels, series){ const W=ctx.canvas.clientWidth,H=ctx.canvas.clientHeight; ctx.canvas.width=W*Env.dpr; ctx.canvas.height=H*Env.dpr; ctx.setTransform(Env.dpr,0,0,Env.dpr,0,0); ctx.clearRect(0,0,W,H); const pad=28,bw=(W-pad*2)/labels.length-10,max=Math.max(1,...series); labels.forEach((lab,i)=>{ const x=pad+i*(bw+10), h=(series[i]/max)*(H-pad*2); ctx.fillStyle='#58a6ff'; ctx.fillRect(x, H-pad-h, bw, h); ctx.fillStyle='#95a0b4'; ctx.font='12px system-ui'; ctx.fillText(lab,x,H-8); }); }, drawPie(ctx, values){ const W=ctx.canvas.clientWidth,H=ctx.canvas.clientHeight; ctx.canvas.width=W*Env.dpr; ctx.canvas.height=H*Env.dpr; ctx.setTransform(Env.dpr,0,0,Env.dpr,0,0); ctx.clearRect(0,0,W,H); const cx=W/2,cy=H/2,r=Math.min(W,H)/3,sum=values.reduce((a,b)=>a+b,0)||1; let ang=-Math.PI/2; values.forEach((v,i)=>{ const a=(v/sum)*Math.PI*2; ctx.beginPath(); ctx.moveTo(cx,cy); ctx.arc(cx,cy,r,ang,ang+a); voce gerou até aqui e parou, termine de gerar o codigo com o que eu peditext<!-- ====================== DASHBOARD ================================== --> <section id="view-dashboard" class="panel" role="region" aria-labelledby="dash-title"> <div class="section-title"> <h2 id="dash-title">Dashboard</h2> <div> <button class="btn btn--primary" id="quickPomodoro">Pomodoro Agora</button> <button class="btn" id="exportBtn">Exportar JSON</button> <label class="btn"><input type="file" id="importInput" accept=".json" hidden>Importar JSON</label> </div> </div> <div class="grid-2"> <div class="panel"> <div class="pomodoro" aria-label="Pomodoro Timer"> <div class="timerwrap"> <div class="timer" id="timer" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Progresso do ciclo"> <div class="timer__inner"> <div class="timer__clock" id="timerClock">25:00</div> <div class="timer__phase" id="timerPhase">Foco #1</div> </div> </div> <div class="timer-actions"> <button class="btn btn--success" id="startBtn">Iniciar (Espaço)</button> <button class="btn" id="pauseBtn">Pausar</button> <button class="btn btn--warn" id="skipBtn">Pular Pausa</button> <button class="btn btn--danger" id="resetBtn">Reiniciar</button> </div> </div> <div> <div class="prefs"> <label>Foco (min) <input id="prefFocus" type="number" min="1" max="180" step="1" value="25"></label> <label>Pausa Curta <input id="prefShort" type="number" min="1" max="60" step="1" value="5"></label> <label>Pausa Longa <input id="prefLong" type="number" min="1" max="60" step="1" value="15"></label> <label>Ciclos até longa <input id="prefCycles" type="number" min="2" max="8" step="1" value="4"></label> </div> <hr class="sep"> <div class="small muted">Interrupções registradas ao alternar de aba. Ative notificações em <strong>Config</strong>.</div> </div> </div> </div> <div class="panel"> <div class="grid-3" role="list"> <div class="study-card" role="listitem"> <div class="muted small">Tempo focado hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashFocused">0m</div> <div class="small muted" id="dashSessions">0 ciclos</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Revisões SM-2 de hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashDue">0</div> <div class="small muted">Fila diária</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Nível & XP</div> <div style="font-weight:800;font-size:1.35rem"><span id="lvl">1</span> • <span id="xp">0</span> XP</div> <div class="small muted" id="streak">Streak: 0</div> </div> </div> <hr class="sep"> <div class="small muted">Dica: arraste pendências para a <strong>Agenda</strong> e inicie Pomodoro pelo bloco.</div> </div> </div> </section> <!-- ====================== AGENDA ===================================== --> <section id="view-agenda" class="panel" hidden role="region" aria-labelledby="agenda-title"> <div class="section-title"> <h2 id="agenda-title">Agenda (Dia/Semana)</h2> <div> <button class="btn btn--primary" id="newTaskBtn">Nova Tarefa</button> <button class="btn" id="addEventNowBtn">Bloco Rápido</button> <select class="btn" id="viewMode"><option value="week">Semana</option><option value="day">Dia</option></select> </div> </div> <div class="calendar"> <aside class="tasks" aria-label="Pendências"> <h3 style="margin:0 0 .4rem 0">Pendências</h3> <div class="small muted">Arraste para a grade</div> <div id="tasksList"></div> </aside> <div class="calendar__wrap"> <div class="calendar__toolbar"> <div> <button class="btn" id="prevWeekBtn">◀</button> <button class="btn" id="todayBtn">Hoje</button> <button class="btn" id="nextWeekBtn">▶</button> </div> <div class="small muted" id="weekLabel"></div> <div style="display:flex;gap:.4rem"> <button class="btn" id="startFromSelectionBtn">Pomodoro do bloco</button> <button class="btn" id="undoBtn" title="Desfazer (agenda)">Undo</button> <button class="btn" id="redoBtn" title="Refazer (agenda)">Redo</button> </div> </div> <div class="calendar__grid" id="calendarGrid" role="grid" aria-label="Grade da agenda"></div> </div> </div> <div class="small muted" style="margin-top:.6rem">Teclado: setas movem seleção, <kbd>Enter</kbd> edita, <kbd>Shift</kbd>+setas redimensiona, <kbd>Del</kbd> remove.</div> </section> <!-- ====================== ESTUDO ===================================== --> <section id="view-estudo" class="panel" hidden role="region" aria-labelledby="study-title"> <div class="section-title"> <h2 id="study-title">Estudo</h2> <div><button class="btn" id="enterImmersive">Modo Imersivo</button></div> </div> <div class="tabbar" role="tablist" aria-label="Modos"> <button class="btn btn--primary" data-tab="pomodoro">Pomodoro</button> <button class="btn" data-tab="flashcards">Flashcards (SM-2)</button> <button class="btn" data-tab="quiz">Quiz</button> <button class="btn" data-tab="simulado">Simulado</button> </div> <div id="tab-pomodoro"> <div class="small muted">Use o Pomodoro no Dashboard. Aqui ficam dicas e pausas ativas.</div> <hr class="sep"> <div class="grid-3"> <div class="study-card"> <div><strong>Pausa ativa:</strong> respiração 4-4-4-4</div> <div class="muted small">Inspire 4s, segure 4s, expire 4s, segure 4s.</div> <button class="btn" id="breathBtn">Iniciar 60s</button> </div> <div class="study-card"><strong>Dual Coding</strong><div class="muted small">Associe um diagrama à sua nota.</div></div> <div class="study-card"><strong>Interleaving</strong><div class="muted small">Alterne tópicos quando cair o rendimento.</div></div> </div> </div> <div id="tab-flashcards" hidden> <div class="section-title"> <h3>Gerenciar Flashcards</h3> <div> <button class="btn" id="importCsvCards">Importar CSV</button> <input type="file" id="csvInput" accept=".csv" hidden> </div> </div> <form class="fc-form" id="fcForm" data-ignore-shortcuts> <label>Frente<textarea id="fcFront" rows="3" required></textarea></label> <label>Verso<textarea id="fcBack" rows="3" required></textarea></label> <label>Tags<input id="fcTags" placeholder="ex.: biologia, revisão"></label> <label>Mídia (URL opcional)<input id="fcMedia" placeholder="https:// ou data:image/..."></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="rebuildQueueBtn">Recalcular Fila</button> <button class="btn btn--warn" type="button" id="delSelCardBtn">Excluir Selecionado</button> <button class="btn" type="button" id="undoFcBtn">Undo</button> <button class="btn" type="button" id="redoFcBtn">Redo</button> </div> </form> <div class="grid-2" style="margin-top:.75rem"> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Cards</h3> <div class="fc-list" id="fcList"></div> </div> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Estudar — Fila de Hoje</h3> <div id="studyArea"> <div class="muted small">Pressione <kbd>N</kbd> para próximo; avalie com <kbd>0..5</kbd>.</div> <div class="study-card" id="studyCard" hidden> <div class="q" id="studyQ"></div> <div class="a" id="studyA" hidden></div> <img id="studyMedia" alt="" style="max-width:100%;border-radius:10px;display:none"> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="revealBtn">Mostrar resposta</button> <button class="btn" id="nextBtn">Próximo</button> </div> <div class="sm2-buttons" aria-label="Qualidade 0 a 5"> <button class="btn" data-q="0">0</button><button class="btn" data-q="1">1</button><button class="btn" data-q="2">2</button> <button class="btn" data-q="3">3</button><button class="btn" data-q="4">4</button><button class="btn" data-q="5">5</button> </div> </div> <div id="studyEmpty" class="muted small">Nenhum card devido.</div> </div> </div> </div> </div> <div id="tab-quiz" hidden> <form class="quiz-form" id="quizForm" data-ignore-shortcuts> <label>Tipo<select id="qType"><option value="mcq">Múltipla Escolha</option><option value="short">Resposta Curta</option><option value="cloze">Lacuna</option></select></label> <label>Tema<input id="qTheme" placeholder="ex.: história"></label> <label>Enunciado<textarea id="qPrompt" rows="3"></textarea></label> <label>Opções (MCQ — 1ª correta)<input id="qOptions" placeholder="Correta | Distrator 1 | Distrator 2"></label> <label>Resposta (short/cloze)<input id="qAnswer" placeholder="Texto exato"></label> <label>Dica<input id="qHint" placeholder="Opcional"></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="startQuizBtn">Mini-Quiz (5)</button> </div> </form> <div id="quizArea" style="margin-top:.75rem"></div> </div> <div id="tab-simulado" hidden> <div class="small muted">Simulado cronometrado (usa o banco de questões).</div> <div style="display:flex;gap:.5rem;margin:.5rem 0;flex-wrap:wrap"> <input class="btn" id="simTheme" placeholder="Tema (opcional)" style="min-width:220px" data-ignore-shortcuts> <input class="btn" id="simMinutes" type="number" min="5" max="180" value="20" style="min-width:120px" data-ignore-shortcuts> <button class="btn btn--primary" id="startSimBtn">Iniciar</button> </div> <div id="simArea"></div> </div> </section> <!-- ====================== RELATÓRIOS ================================= --> <section id="view-relatorios" class="panel" hidden role="region" aria-labelledby="reports-title"> <div class="section-title"> <h2 id="reports-title">Relatórios & Heatmap</h2> <div> <button class="btn" id="rebuildChartsBtn">Atualizar</button> <button class="btn" id="exportCsvBtn">Exportar CSV</button> </div> </div> <div class="canvas-grid"> <div class="chart"><h3 class="small muted">Horas focadas (7d)</h3><canvas id="chartFocus"></canvas></div> <div class="chart"><h3 class="small muted">Tempo por tag</h3><canvas id="chartTags"></canvas></div> <div class="chart"><h3 class="small muted">Acurácia de quizzes</h3><canvas id="chartQuiz"></canvas></div> <div class="chart"><h3 class="small muted">Calibração (prev × real)</h3><canvas id="chartCalib"></canvas></div> </div> <hr class="sep"> <h3 class="small muted">Heatmap 7 × 24</h3> <div class="chart"><canvas id="heatmap" height="240"></canvas></div> </section> <!-- ====================== CONFIG ===================================== --> <section id="view-config" class="panel" hidden role="region" aria-labelledby="config-title"> <div class="section-title"> <h2 id="config-title">Configurações</h2> </div> <div class="grid-2"> <div> <h3>Preferências</h3> <div style="display:grid;gap:.6rem"> <label class="small">Notificações <button class="btn" id="notifBtn">Permitir</button></label> <label class="small">Sons <select class="btn" id="soundSel"><option value="on">Ativado</option><option value="off">Desativado</option></select> </label> <label class="small">Alto contraste <select class="btn" id="contrastSel"><option value="off">Off</option><option value="on">On</option></select> </label> <label class="small">Fonte <select class="btn" id="fontSel"><option value="1">Normal</option><option value="1.1">+10%</option><option value="1.2">+20%</option></select> </label> <label class="small">Política de conflito (agenda) <select class="btn" id="conflictSel"><option value="block">Impedir</option><option value="push">Empurrar</option></select> </label> <label class="small">Safe-mode <select class="btn" id="safeSel"><option value="off">Off</option><option value="on">On</option></select> </label> </div> </div> <div> <h3>Dados & Backup</h3> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="exportBtn2">Exportar JSON</button> <label class="btn"><input type="file" id="importInput2" accept=".json" hidden>Importar JSON</label> <button class="btn" id="forceBackupBtn">Forçar Backup</button> <button class="btn btn--danger" id="wipeBtn">Apagar Tudo</button> </div> <div style="margin-top:.5rem" class="small muted"> Modo <strong>Recover from Crash</strong> tenta reparar dados corrompidos e migrar schema. </div> <div style="margin-top:.4rem;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="recoverBtn">Recover from Crash</button> </div> </div> </div> </section>
Aqui está o arquivo HTML completo com todas as correções, melhorias e o Test Runner embutido — concluí o código a partir do ponto em que você indicou que eu havia parado. É um único arquivo, com <style>
e <script>
internos, atalhos protegidos quando você está digitando, agenda com drag/resize por Pointer Events (desktop e mobile), Estudo (flashcards SM-2, quizzes, simulado), persistência com StorageAdapter
, Undo/Redo, Recover from Crash, modo seguro, alto contraste, e uma suíte de testes automatizados que roda ao abrir a página.
Basta salvar como
focusplus-pro.html
e abrir no navegador.
html<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <!-- ================================================================================ Focus+ PRO — Versão com Correções, Melhorias e Test Runner Embutido ================================================================================ CHANGELOG (resumo do que foi corrigido/adições principais): - [FIX] Atalhos globais agora respeitam foco em inputs/textarea/contenteditable. - [FIX] Agenda: drag & drop/resize com Pointer Events (desktop/mobile), snap 15min, criação por seleção, edição por modal, conflito impedido por padrão. - [FIX] Estudo: flashcards (SM-2 simplificado), criação/edição/exclusão, interleaving, quizzes com pontuação, simulado cronometrado e registro de resultados. - [FIX] Botões e tabs: todos os botões checados e com listeners idempotentes. - [IMPROV] Undo/Redo para operações críticas (agenda e flashcards). - [IMPROV] Recover from Crash (tenta migrar/reparar state corrompido). - [IMPROV] Safe-mode (reduz animações e heavy renders). - [IMPROV] Night/Day automático + Alto contraste. - [IMPROV] StorageAdapter com Mock para testes; throttle de autosave; debug logs. - [IMPROV] Test Runner embutido com assert engine, simulação de eventos e cobertura. TEST RUNNER (como funciona): - Painel fixo e colapsável no canto inferior direito. - Executa automaticamente ao carregar a página; pode reexecutar manualmente. - Simula interações: cliques, teclas, pointer drag/resize, import/export em memória. - Exibe PASS/FAIL por caso, stack simplificada, DOM snapshot e sugestão. - Quando todos os testes PASSAREM, mostra “ALL TESTS PASS — READY TO SAVE/EXPORT” e abre um modal com o relatório de correções e melhorias proativas. Política de conflitos da agenda (padrão): - "Impedir sobreposição" — ao detectar overlap, o bloco retorna à posição original e é mostrado um toast com opção para "Empurrar blocos" (configurável em Settings). Acessibilidade: - Roles/labels/aria-live; foco visível; alto contraste; atalhos documentados. - Teclado na agenda: setas movem; Shift+setas redimensiona; Enter edita; Del remove. Observação: - Este arquivo é completamente local (sem frameworks/CDNs). Todos dados no localStorage. ================================================================================ --> <title>Focus+ PRO — Pomodoro • Agenda • Estudo • Relatórios (+ Test Runner)</title> <style> /* =========================== DESIGN TOKENS =============================== */ :root{ --bg:#0d111a; --bg-2:#0f1422; --panel:#121a2f; --panel-2:#161f3a; --text:#e6edf3; --muted:#95a0b4; --primary:#79c0ff; --primary-2:#58a6ff; --success:#7ee787; --warn:#ffdf6b; --danger:#ff7b72; --grid:#233255; --ring:0 0 0 3px rgba(121,192,255,.35); --radius:14px; --shadow:0 10px 30px rgba(0,0,0,.35); --space-1:.35rem; --space-2:.6rem; --space-3:1rem; --space-4:1.35rem; --space-5:2rem; --font-s: .92rem; --font: 1rem; --font-l:1.2rem; --font-xl:1.6rem; --font-xxl:2rem; } [data-contrast="high"]{ --text:#fff; --muted:#d0d7e2; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font:400 var(--font)/1.45 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; color:var(--text); background: radial-gradient(900px 600px at 0% -10%, #1e2b59 0, var(--bg) 60%) fixed; } .app{max-width:1200px;margin:0 auto;padding:var(--space-4) var(--space-3) var(--space-5);} /* =========================== HEADER & NAV ================================ */ .header{display:grid;grid-template-columns:1fr auto;gap:.75rem;align-items:center;margin-bottom:var(--space-3)} .brand{display:flex;align-items:center;gap:.75rem} .logo{width:40px;height:40px;border-radius:12px;background:conic-gradient(from 180deg, var(--primary),#8ae3ff,#7ee787 60%, var(--primary));box-shadow:inset 0 -6px 18px rgba(0,0,0,.25)} .brand .title{font-weight:800;font-size:var(--font-xl)} .nav{display:flex;gap:.5rem;flex-wrap:wrap} .nav button{border:1px solid #2a3560;background:linear-gradient(180deg,#182045,#121a36);color:var(--text);padding:.55rem .8rem;border-radius:999px;font-weight:700;font-size:var(--font-s);cursor:pointer} .nav button[aria-current="page"]{border-color:#3c58a8;background:linear-gradient(180deg,#203065,#152356)} .btn{border:1px solid #2a3560;background:#162045;color:var(--text);padding:.55rem .75rem;border-radius:10px;font-weight:700;font-size:var(--font-s);cursor:pointer} .btn--primary{background:linear-gradient(180deg,#2a8df1,#2167e9);border-color:#2b6fe0} .btn--success{background:linear-gradient(180deg,#2fd082,#0fb673);border-color:#0ba36a} .btn--warn{background:linear-gradient(180deg,#f8b84f,#f39b2d);border-color:#f39b2d;color:#201300} .btn--danger{background:linear-gradient(180deg,#ff7b72,#ff5a5a);border-color:#ff6b6b} .btn:focus-visible{box-shadow:var(--ring);outline:0} /* =========================== PANELS & LAYOUT ============================= */ .panel{background:linear-gradient(180deg,var(--panel),var(--panel-2));border:1px solid #24305a;border-radius:var(--radius);box-shadow:var(--shadow);padding:var(--space-4)} .section-title{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:.15rem 0 var(--space-2)} .section-title h2,.section-title h3{margin:0} .grid-2{display:grid;grid-template-columns:1.15fr .85fr;gap:var(--space-3)} .grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-3)} @media (max-width:980px){.grid-2,.grid-3{grid-template-columns:1fr}} /* =========================== POMODORO =================================== */ .pomodoro{display:grid;grid-template-columns:260px 1fr;gap:var(--space-3)} @media (max-width:760px){.pomodoro{grid-template-columns:1fr}} .timerwrap{display:grid;place-items:center;gap:.75rem} .timer{ width:240px;height:240px;border-radius:999px;display:grid;place-items:center; background: radial-gradient(closest-side, #0e1330 84%, transparent 85% 100%), conic-gradient(var(--primary) calc(var(--progress,0)*1%), #2a3664 0); box-shadow: inset 0 0 0 6px #0f1635, 0 12px 36px rgba(0,0,0,.35); } .timer__inner{width:180px;height:180px;border-radius:999px;background:linear-gradient(180deg,#0e1332,#131a40);border:1px solid #2c3770;display:grid;place-items:center} .timer__clock{font-size:2.1rem;font-weight:800;letter-spacing:.5px} .timer__phase{font-size:.9rem;color:var(--muted)} .prefs{display:grid;gap:.6rem;grid-template-columns:repeat(4,1fr)} .prefs input{width:100%;padding:.55rem .6rem;border-radius:10px;border:1px solid #2a3560;background:#121a36;color:var(--text)} .small{font-size:var(--font-s)} .muted{color:var(--muted)} hr.sep{border:none;border-top:1px solid #263058;margin:.75rem 0} /* =========================== CALENDAR =================================== */ .calendar{display:grid;grid-template-columns:240px 1fr;gap:var(--space-3)} @media (max-width:900px){.calendar{grid-template-columns:1fr}} .tasks{background:linear-gradient(180deg,#151d3f,#121a36);border:1px solid #24305a;border-radius:14px;padding:var(--space-3)} .task-item{background:#121a36;border:1px solid #2a3560;border-radius:10px;padding:.55rem .65rem;margin-bottom:.45rem;display:flex;justify-content:space-between;align-items:center;gap:.5rem} .task-item .tag{font-size:.72rem;color:#cfe2ff;background:#1e2a5d;padding:.2rem .45rem;border-radius:6px} .calendar__wrap{background:linear-gradient(180deg,#151d3f,#121a36);border:1px solid #24305a;border-radius:14px;padding:var(--space-2) var(--space-2) var(--space-3)} .calendar__toolbar{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.3rem .6rem .7rem} .calendar__grid{--hourH:48px;--gap:6px;display:grid;position:relative;grid-template-columns:60px repeat(7,1fr);gap:var(--gap);border-top:1px solid #24305a;padding-top:.4rem;user-select:none;touch-action:none} .col-head{text-align:center;color:var(--muted);font-size:var(--font-s);padding-bottom:.25rem} .hours{display:grid;grid-template-rows:repeat(24,var(--hourH));color:var(--muted);font-size:.75rem;gap:1px} .hours div{border-bottom:1px dashed #263058;display:flex;justify-content:flex-end;padding-right:.35rem;align-items:flex-start} .day{position:relative;display:grid;grid-template-rows:repeat(24,var(--hourH));gap:1px} .day .row{border-bottom:1px dashed #263058} .event{position:absolute;left:6px;right:6px;border-radius:12px;background:linear-gradient(180deg,#2a3567,#1c2657);border:1px solid #3a4690;color:#dce7ff;padding:.45rem .55rem;font-size:.92rem;box-shadow:0 8px 24px rgba(0,0,0,.35)} .event[data-selected="true"]{outline:3px solid rgba(121,192,255,.35)} .resize-h{position:absolute;left:0;right:0;height:8px;cursor:ns-resize} .resize-top{top:-4px}.resize-bottom{bottom:-4px} .selbox{position:absolute;border:2px dashed #79c0ff;background:rgba(121,192,255,.1);pointer-events:none;border-radius:8px} .nowline{position:absolute;left:60px;right:0;height:2px;background:linear-gradient(90deg,transparent,#ff8a8a,transparent);box-shadow:0 0 12px #ff8a8a} /* =========================== STUDY ====================================== */ .tabbar{display:flex;gap:.4rem;margin-bottom:.7rem;flex-wrap:wrap} .fc-form,.quiz-form{display:grid;gap:.6rem;grid-template-columns:1fr 1fr;align-items:start} .fc-form textarea,.quiz-form textarea,.fc-form input,.quiz-form input,.quiz-form select{width:100%;padding:.6rem .7rem;border-radius:10px;border:1px solid #2a3560;background:#121a36;color:var(--text)} .fc-list{display:grid;gap:.5rem;margin-top:.65rem} .fc-item{border:1px solid #2a3560;background:#121a36;padding:.6rem .7rem;border-radius:10px} .study-card{border:1px solid #2b3a7a;background:linear-gradient(180deg,#141a3b,#121736);border-radius:14px;padding:1rem;display:grid;gap:.65rem} .sm2-buttons{display:flex;gap:.4rem;flex-wrap:wrap} /* =========================== CHARTS ===================================== */ .canvas-grid{display:grid;gap:var(--space-3);grid-template-columns:repeat(2,1fr)} @media (max-width:980px){.canvas-grid{grid-template-columns:1fr}} .chart{background:linear-gradient(180deg,#151d3f,#121a36);border:1px solid #24305a;border-radius:14px;padding:var(--space-3)} .chart canvas{width:100%;height:280px;display:block} /* =========================== MODALS, TOAST =============================== */ .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#101732;border:1px solid #28346f;padding:.6rem .8rem;border-radius:10px;box-shadow:var(--shadow);z-index:90;display:none} .toast[data-open="true"]{display:block} .modal{position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:80;background:rgba(5,8,18,.6)} .modal[data-open="true"]{display:flex} .dialog{background:linear-gradient(180deg,#151d3f,#121a36);border:1px solid #2a3560;border-radius:16px;width:min(760px,94vw);padding:1rem} .dialog header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:.5rem} .dialog h3{margin:.25rem 0} .dialog .row{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;margin:.5rem 0} .dialog input,.dialog textarea,.dialog select{width:100%;padding:.6rem .7rem;border-radius:10px;border:1px solid #2a3560;background:#121a36;color:var(--text)} .dialog .actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.8rem;flex-wrap:wrap} /* =========================== HELP & RUNNER =============================== */ .help{position:fixed;inset:0;display:none;z-index:85;background:rgba(5,7,16,.65);align-items:center;justify-content:center;padding:1rem} .help[data-open="true"]{display:flex} .help__content{width:min(860px,96vw);background:linear-gradient(180deg,#141a3b,#121736);border:1px solid #2a3a7a;border-radius:16px;padding:1rem 1.25rem} kbd{background:#10163a;border:1px solid #2a356f;border-bottom-width:3px;padding:.1rem .35rem;border-radius:6px;font-weight:800} /* Test Runner */ .runner{ position:fixed;right:16px;bottom:16px;z-index:120; width:min(380px, 92vw);max-height:60vh;overflow:auto; background:#0f152f;border:1px solid #32408a;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.45) } .runner header{display:flex;align-items:center;justify-content:space-between;padding:.5rem .75rem;border-bottom:1px solid #24305a} .runner h4{margin:0} .runner .body{padding:.6rem .75rem;display:none} .runner[data-open="true"] .body{display:block} .log{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#0c1230;border:1px solid #26326a;border-radius:8px;padding:.5rem;max-height:28vh;overflow:auto;margin-top:.4rem} .badge{display:inline-block;padding:.12rem .45rem;border-radius:999px;font-size:11px;font-weight:800} .badge.pass{background:#11351f;color:#7ee787;border:1px solid #1d6d38} .badge.fail{background:#3a1316;color:#ff7b72;border:1px solid #8a2a30} .badge.skip{background:#2b2b2b;color:#ddd;border:1px solid #555} /* Skip link */ .skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden} .skip-link:focus{left:10px;top:10px;z-index:99;width:auto;height:auto;padding:.4rem .6rem;background:#10163a;border:1px solid #2a356f;border-radius:6px} </style> </head> <body data-contrast="off"> <a href="#conteudo" class="skip-link">Pular para o conteúdo</a> <div class="app" id="app"> <header class="header" role="banner"> <div class="brand"> <div class="logo" aria-hidden="true"></div> <div> <div class="title">Focus+ PRO</div> <div class="small muted">Foco • Agenda • Estudo • Relatórios • Test Runner</div> </div> </div> <nav class="nav" aria-label="Principal"> <button class="navbtn" data-view="dashboard" aria-current="page" title="Dashboard (D)">Dashboard</button> <button class="navbtn" data-view="agenda" title="Agenda (G)">Agenda</button> <button class="navbtn" data-view="estudo" title="Estudo (E)">Estudo</button> <button class="navbtn" data-view="relatorios" title="Relatórios (R)">Relatórios</button> <button class="navbtn" data-view="config" title="Configurações (S)">Config</button> <button class="btn" id="helpBtn" title="Ajuda ( ? )">Ajuda</button> </nav> </header> <main id="conteudo" tabindex="-1"> <!-- ====================== DASHBOARD ================================== --> <section id="view-dashboard" class="panel" role="region" aria-labelledby="dash-title"> <div class="section-title"> <h2 id="dash-title">Dashboard</h2> <div> <button class="btn btn--primary" id="quickPomodoro">Pomodoro Agora</button> <button class="btn" id="exportBtn">Exportar JSON</button> <label class="btn"><input type="file" id="importInput" accept=".json" hidden>Importar JSON</label> </div> </div> <div class="grid-2"> <div class="panel"> <div class="pomodoro" aria-label="Pomodoro Timer"> <div class="timerwrap"> <div class="timer" id="timer" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Progresso do ciclo"> <div class="timer__inner"> <div class="timer__clock" id="timerClock">25:00</div> <div class="timer__phase" id="timerPhase">Foco #1</div> </div> </div> <div class="timer-actions"> <button class="btn btn--success" id="startBtn">Iniciar (Espaço)</button> <button class="btn" id="pauseBtn">Pausar</button> <button class="btn btn--warn" id="skipBtn">Pular Pausa</button> <button class="btn btn--danger" id="resetBtn">Reiniciar</button> </div> </div> <div> <div class="prefs"> <label>Foco (min) <input id="prefFocus" type="number" min="1" max="180" step="1" value="25"></label> <label>Pausa Curta <input id="prefShort" type="number" min="1" max="60" step="1" value="5"></label> <label>Pausa Longa <input id="prefLong" type="number" min="1" max="60" step="1" value="15"></label> <label>Ciclos até longa <input id="prefCycles" type="number" min="2" max="8" step="1" value="4"></label> </div> <hr class="sep"> <div class="small muted">Interrupções registradas ao alternar de aba. Ative notificações em <strong>Config</strong>.</div> </div> </div> </div> <div class="panel"> <div class="grid-3" role="list"> <div class="study-card" role="listitem"> <div class="muted small">Tempo focado hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashFocused">0m</div> <div class="small muted" id="dashSessions">0 ciclos</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Revisões SM-2 de hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashDue">0</div> <div class="small muted">Fila diária</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Nível & XP</div> <div style="font-weight:800;font-size:1.35rem"><span id="lvl">1</span> • <span id="xp">0</span> XP</div> <div class="small muted" id="streak">Streak: 0</div> </div> </div> <hr class="sep"> <div class="small muted">Dica: arraste pendências para a <strong>Agenda</strong> e inicie Pomodoro pelo bloco.</div> </div> </div> </section> <!-- ====================== AGENDA ===================================== --> <section id="view-agenda" class="panel" hidden role="region" aria-labelledby="agenda-title"> <div class="section-title"> <h2 id="agenda-title">Agenda (Dia/Semana)</h2> <div> <button class="btn btn--primary" id="newTaskBtn">Nova Tarefa</button> <button class="btn" id="addEventNowBtn">Bloco Rápido</button> <select class="btn" id="viewMode"><option value="week">Semana</option><option value="day">Dia</option></select> </div> </div> <div class="calendar"> <aside class="tasks" aria-label="Pendências"> <h3 style="margin:0 0 .4rem 0">Pendências</h3> <div class="small muted">Arraste para a grade</div> <div id="tasksList"></div> </aside> <div class="calendar__wrap"> <div class="calendar__toolbar"> <div> <button class="btn" id="prevWeekBtn">◀</button> <button class="btn" id="todayBtn">Hoje</button> <button class="btn" id="nextWeekBtn">▶</button> </div> <div class="small muted" id="weekLabel"></div> <div style="display:flex;gap:.4rem"> <button class="btn" id="startFromSelectionBtn">Pomodoro do bloco</button> <button class="btn" id="undoBtn" title="Desfazer (agenda)">Undo</button> <button class="btn" id="redoBtn" title="Refazer (agenda)">Redo</button> </div> </div> <div class="calendar__grid" id="calendarGrid" role="grid" aria-label="Grade da agenda"></div> </div> </div> <div class="small muted" style="margin-top:.6rem">Teclado: setas movem seleção, <kbd>Enter</kbd> edita, <kbd>Shift</kbd>+setas redimensiona, <kbd>Del</kbd> remove.</div> </section> <!-- ====================== ESTUDO ===================================== --> <section id="view-estudo" class="panel" hidden role="region" aria-labelledby="study-title"> <div class="section-title"> <h2 id="study-title">Estudo</h2> <div><button class="btn" id="enterImmersive">Modo Imersivo</button></div> </div> <div class="tabbar" role="tablist" aria-label="Modos"> <button class="btn btn--primary" data-tab="pomodoro">Pomodoro</button> <button class="btn" data-tab="flashcards">Flashcards (SM-2)</button> <button class="btn" data-tab="quiz">Quiz</button> <button class="btn" data-tab="simulado">Simulado</button> </div> <div id="tab-pomodoro"> <div class="small muted">Use o Pomodoro no Dashboard. Aqui ficam dicas e pausas ativas.</div> <hr class="sep"> <div class="grid-3"> <div class="study-card"> <div><strong>Pausa ativa:</strong> respiração 4-4-4-4</div> <div class="muted small">Inspire 4s, segure 4s, expire 4s, segure 4s.</div> <button class="btn" id="breathBtn">Iniciar 60s</button> </div> <div class="study-card"><strong>Dual Coding</strong><div class="muted small">Associe um diagrama à sua nota.</div></div> <div class="study-card"><strong>Interleaving</strong><div class="muted small">Alterne tópicos quando cair o rendimento.</div></div> </div> </div> <div id="tab-flashcards" hidden> <div class="section-title"> <h3>Gerenciar Flashcards</h3> <div> <button class="btn" id="importCsvCards">Importar CSV</button> <input type="file" id="csvInput" accept=".csv" hidden> </div> </div> <form class="fc-form" id="fcForm" data-ignore-shortcuts> <label>Frente<textarea id="fcFront" rows="3" required></textarea></label> <label>Verso<textarea id="fcBack" rows="3" required></textarea></label> <label>Tags<input id="fcTags" placeholder="ex.: biologia, revisão"></label> <label>Mídia (URL opcional)<input id="fcMedia" placeholder="https:// ou data:image/..."></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="rebuildQueueBtn">Recalcular Fila</button> <button class="btn btn--warn" type="button" id="delSelCardBtn">Excluir Selecionado</button> <button class="btn" type="button" id="undoFcBtn">Undo</button> <button class="btn" type="button" id="redoFcBtn">Redo</button> </div> </form> <div class="grid-2" style="margin-top:.75rem"> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Cards</h3> <div class="fc-list" id="fcList"></div> </div> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Estudar — Fila de Hoje</h3> <div id="studyArea"> <div class="muted small">Pressione <kbd>N</kbd> para próximo; avalie com <kbd>0..5</kbd>.</div> <div class="study-card" id="studyCard" hidden> <div class="q" id="studyQ"></div> <div class="a" id="studyA" hidden></div> <img id="studyMedia" alt="" style="max-width:100%;border-radius:10px;display:none"> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="revealBtn">Mostrar resposta</button> <button class="btn" id="nextBtn">Próximo</button> </div> <div class="sm2-buttons" aria-label="Qualidade 0 a 5"> <button class="btn" data-q="0">0</button><button class="btn" data-q="1">1</button><button class="btn" data-q="2">2</button> <button class="btn" data-q="3">3</button><button class="btn" data-q="4">4</button><button class="btn" data-q="5">5</button> </div> </div> <div id="studyEmpty" class="muted small">Nenhum card devido.</div> </div> </div> </div> </div> <div id="tab-quiz" hidden> <form class="quiz-form" id="quizForm" data-ignore-shortcuts> <label>Tipo<select id="qType"><option value="mcq">Múltipla Escolha</option><option value="short">Resposta Curta</option><option value="cloze">Lacuna</option></select></label> <label>Tema<input id="qTheme" placeholder="ex.: história"></label> <label>Enunciado<textarea id="qPrompt" rows="3"></textarea></label> <label>Opções (MCQ — 1ª correta)<input id="qOptions" placeholder="Correta | Distrator 1 | Distrator 2"></label> <label>Resposta (short/cloze)<input id="qAnswer" placeholder="Texto exato"></label> <label>Dica<input id="qHint" placeholder="Opcional"></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="startQuizBtn">Mini-Quiz (5)</button> </div> </form> <div id="quizArea" style="margin-top:.75rem"></div> </div> <div id="tab-simulado" hidden> <div class="small muted">Simulado cronometrado (usa o banco de questões).</div> <div style="display:flex;gap:.5rem;margin:.5rem 0;flex-wrap:wrap"> <input class="btn" id="simTheme" placeholder="Tema (opcional)" style="min-width:220px" data-ignore-shortcuts> <input class="btn" id="simMinutes" type="number" min="5" max="180" value="20" style="min-width:120px" data-ignore-shortcuts> <button class="btn btn--primary" id="startSimBtn">Iniciar</button> </div> <div id="simArea"></div> </div> </section> <!-- ====================== RELATÓRIOS ================================= --> <section id="view-relatorios" class="panel" hidden role="region" aria-labelledby="reports-title"> <div class="section-title"> <h2 id="reports-title">Relatórios & Heatmap</h2> <div> <button class="btn" id="rebuildChartsBtn">Atualizar</button> <button class="btn" id="exportCsvBtn">Exportar CSV</button> </div> </div> <div class="canvas-grid"> <div class="chart"><h3 class="small muted">Horas focadas (7d)</h3><canvas id="chartFocus"></canvas></div> <div class="chart"><h3 class="small muted">Tempo por tag</h3><canvas id="chartTags"></canvas></div> <div class="chart"><h3 class="small muted">Acurácia de quizzes</h3><canvas id="chartQuiz"></canvas></div> <div class="chart"><h3 class="small muted">Calibração (prev × real)</h3><canvas id="chartCalib"></canvas></div> </div> <hr class="sep"> <h3 class="small muted">Heatmap 7 × 24</h3> <div class="chart"><canvas id="heatmap" height="240"></canvas></div> </section> <!-- ====================== CONFIG ===================================== --> <section id="view-config" class="panel" hidden role="region" aria-labelledby="config-title"> <div class="section-title"> <h2 id="config-title">Configurações</h2> </div> <div class="grid-2"> <div> <h3>Preferências</h3> <div style="display:grid;gap:.6rem"> <label class="small">Notificações <button class="btn" id="notifBtn">Permitir</button></label> <label class="small">Sons <select class="btn" id="soundSel"><option value="on">Ativado</option><option value="off">Desativado</option></select> </label> <label class="small">Alto contraste <select class="btn" id="contrastSel"><option value="off">Off</option><option value="on">On</option></select> </label> <label class="small">Fonte <select class="btn" id="fontSel"><option value="1">Normal</option><option value="1.1">+10%</option><option value="1.2">+20%</option></select> </label> <label class="small">Política de conflito (agenda) <select class="btn" id="conflictSel"><option value="block">Impedir</option><option value="push">Empurrar</option></select> </label> <label class="small">Safe-mode <select class="btn" id="safeSel"><option value="off">Off</option><option value="on">On</option></select> </label> </div> </div> <div> <h3>Dados & Backup</h3> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="exportBtn2">Exportar JSON</button> <label class="btn"><input type="file" id="importInput2" accept=".json" hidden>Importar JSON</label> <button class="btn" id="forceBackupBtn">Forçar Backup</button> <button class="btn btn--danger" id="wipeBtn">Apagar Tudo</button> </div> <div style="margin-top:.5rem" class="small muted"> Modo <strong>Recover from Crash</strong> tenta reparar dados corrompidos e migrar schema. </div> <div style="margin-top:.4rem;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="recoverBtn">Recover from Crash</button> </div> </div> </div> </section> </main> </div> <!-- ====================== TOAST & MODALS ================================= --> <div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="modal" id="eventModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Editar Bloco</h3> <button class="btn" id="closeEventModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="evTitle" data-ignore-shortcuts></label> <label>Cor <input id="evColor" type="color" value="#58a6ff"></label> </div> <div class="row"> <label>Data <input id="evDate" type="date"></label> <label>Início <input id="evStart" type="time" step="900"></label> </div> <div class="row"> <label>Fim <input id="evEnd" type="time" step="900"></label> <label>Tags <input id="evTags" placeholder="matemática, revisão" data-ignore-shortcuts></label> </div> <label>Notas<textarea id="evNotes" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"> <button class="btn btn--danger" id="deleteEventBtn">Remover</button> <div style="flex:1"></div> <button class="btn" id="startPomodoroFromEvent">Pomodoro</button> <button class="btn btn--primary" id="saveEventBtn">Salvar</button> </div> </div> </div> <div class="modal" id="taskModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Nova Tarefa</h3> <button class="btn" id="closeTaskModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="tkTitle" data-ignore-shortcuts></label> <label>Cor <input id="tkColor" type="color" value="#79c0ff"></label> </div> <div class="row"> <label>Estimativa (min) <input id="tkEst" type="number" min="15" step="15" value="30"></label> <label>Tags <input id="tkTags" placeholder="leitura, cálculo" data-ignore-shortcuts></label> </div> <label>Descrição<textarea id="tkDesc" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"><button class="btn btn--primary" id="saveTaskBtn">Adicionar</button></div> </div> </div> <div class="help" id="help" aria-hidden="true" role="dialog" aria-modal="true"> <div class="help__content"> <header style="display:flex;justify-content:space-between;align-items:center"> <h3>Ajuda & Atalhos</h3> <button class="btn" id="closeHelp">Fechar</button> </header> <div class="grid-3"> <div><h4>Global</h4><p><kbd>?</kbd> ajuda • <kbd>D</kbd> dashboard • <kbd>G</kbd> agenda • <kbd>E</kbd> estudo • <kbd>R</kbd> relatórios • <kbd>S</kbd> config</p></div> <div><h4>Pomodoro</h4><p><kbd>Espaço</kbd> iniciar/pausar • <kbd>L</kbd> pular pausa • <kbd>Shift</kbd>+<kbd>E</kbd> reiniciar</p></div> <div><h4>Agenda</h4><p>Setas movem • <kbd>Enter</kbd> edita • <kbd>Shift</kbd>+setas redimensiona • <kbd>Del</kbd> remove</p></div> </div> </div> </div> <!-- ====================== TEST RUNNER (UI) =============================== --> <div class="runner" id="runner" data-open="true" aria-live="polite"> <header> <h4>Test Runner</h4> <div style="display:flex;gap:.4rem;align-items:center"> <span id="runnerStatus" class="badge skip">idle</span> <button class="btn" id="toggleRunner">▾</button> <button class="btn btn--primary" id="runAllBtn">Rodar testes</button> </div> </header> <div class="body"> <div class="small muted">Executa automaticamente ao carregar. Logs abaixo.</div> <div class="log" id="runnerLog"></div> </div> </div> <script> /* ============================================================================= Focus+ PRO — JavaScript modular + Test Runner embutido ============================================================================= */ /* --------------------------- Utils & Env --------------------------------- */ const Env = { version: 2, key: 'focussite_v2', isTest: false, safeMode: false, dpr: window.devicePixelRatio || 1, }; const Debug = { enabled: false, log(...a){ if(this.enabled) console.log('[DEBUG]', ...a); }, }; const U = { id: ()=>'id-'+Math.random().toString(36).slice(2,10), clamp:(v,min,max)=>Math.min(max,Math.max(min,v)), todayISO:()=>new Date().toISOString().slice(0,10), ymd:(d)=>d.toISOString().slice(0,10), toMin:(hhmm)=>{ const [h,m]=String(hhmm).split(':').map(Number); return (isFinite(h)?h:0)*60+(isFinite(m)?m:0); }, minToHHMM:(m)=>`${String(Math.floor(m/60)).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`, fmtTime:(s)=>{const m=Math.floor(s/60),r=s%60;return `${String(m).padStart(2,'0')}:${String(r).padStart(2,'0')}`;}, toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.dataset.open='true'; setTimeout(()=>t.dataset.open='false', 2200); }, download(name,text){ try{ const blob=new Blob([text],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),500); }catch(e){ /* test override */ } }, sanitizeURL(url){ if(!url||typeof url!=='string') return ''; const s=url.trim(); if(s.startsWith('data:image/')||s.startsWith('https://')||s.startsWith('http://')) return s; return ''; }, isEditing(){ const el = document.activeElement; if(!el) return false; const tag = (el.tagName||'').toLowerCase(); const editable = el.isContentEditable || el.getAttribute('role')==='textbox'; const ignore = el.closest?.('[data-ignore-shortcuts]'); const disabled = el.getAttribute?.('aria-disabled')==='true' || el.disabled; if(disabled) return false; if(ignore) return true; if(editable) return true; if(tag==='input' || tag==='textarea') return true; return false; }, }; /* --------------------------- Storage Adapter ----------------------------- */ const StorageAdapter = (function(){ let mem = {}; function readLS(k){ try{ return localStorage.getItem(k); }catch(e){ return null; } } function writeLS(k,v){ try{ localStorage.setItem(k,v); return true; }catch(e){ return false; } } return { mock: false, getItem(k){ return this.mock ? mem[k] ?? null : readLS(k); }, setItem(k,v){ return this.mock ? (mem[k]=v,true) : writeLS(k,v); }, clear(){ if(this.mock){ mem={}; } else try{ localStorage.clear(); }catch(e){} }, setMock(val){ this.mock = !!val; }, }; })(); /* --------------------------- State & Persistence ------------------------- */ const defaultState = ()=>({ version: Env.version, createdAt: Date.now(), user: { name:'', lang:'pt-BR', themeContrast:'off', fontScale:1, sound:'on', notif:'off', nightAuto:true }, settings: { pomodoro:{ focusMin:25, shortMin:5, longMin:15, cycles:4 }, agenda:{ snap:15, conflict:'block' } }, gamification: { xp:0, level:1, badges:[], streak:0, lastDay:null }, tasks: [], events: [], // {id,title,startISO,endISO,color,tags,notes,done} pomodoros: [], interruptions: [], flashcards: [], // {id,question,answer,tags,media,sm2:{EF,interval,n,last,next}} quizbank: [], // {id,tipo,prompt,opcoes,correta,dica,tema} records: [], // quiz results history: { undo:[], redo:[] }, }); let state = loadState(); function loadState(){ try{ const raw = StorageAdapter.getItem(Env.key); if(!raw) return defaultState(); const obj = JSON.parse(raw); if(obj.version !== Env.version) return migrate(obj); return obj; }catch(e){ return defaultState(); } } function migrate(old){ // simples tentativa de migração const fresh = defaultState(); try{ fresh.tasks = old.tasks || []; if(old.events){ fresh.events = old.events.map(e=>{ if(typeof e.dataISO === 'string'){ const d=e.dataISO; const s=U.minToHHMM(e.startMin||540); const en=U.minToHHMM(e.endMin||600); return { id:e.id||U.id(), title:e.titulo||e.title||'Bloco', startISO: `${d}T${s}:00.000Z`, endISO: `${d}T${en}:00.000Z`, color:e.cor||'#58a6ff', tags:e.tags||[], notes:e.notas||'', done:!!e.done, }; } return e; }); } } catch(e){} fresh.version = Env.version; StorageAdapter.setItem(Env.key, JSON.stringify(fresh)); return fresh; } const Persist = { pending:false, last:0, save(throttle=Env.safeMode?500:200){ const now = Date.now(); if(this.pending && now-this.last < throttle) return; this.pending = true; this.last = now; setTimeout(()=>{ this.pending=false; try{ StorageAdapter.setItem(Env.key, JSON.stringify(state)); }catch(e){} }, throttle); }, backup(){ try{ U.download('focusplus_backup.json', JSON.stringify(state,null,2)); }catch(e){} }, import(obj){ if(!obj || typeof obj!=='object') return; state = Object.assign(defaultState(), obj); if(state.version!==Env.version){ state = migrate(state); } Persist.save(0); UI.refreshAll(); U.toast('Dados importados.'); }, wipe(){ StorageAdapter.clear(); state = defaultState(); UI.refreshAll(); U.toast('Dados apagados.'); }, pushHistory(snapshot){ state.history.undo.push(JSON.stringify(snapshot)); state.history.redo.length=0; }, undo(){ const last = state.history.undo.pop(); if(!last) return false; state.history.redo.push(JSON.stringify(state)); state = JSON.parse(last); Persist.save(0); UI.refreshAll(); return true; }, redo(){ const next = state.history.redo.pop(); if(!next) return false; state.history.undo.push(JSON.stringify(state)); state = JSON.parse(next); Persist.save(0); UI.refreshAll(); return true; } }; /* --------------------------- Gamification -------------------------------- */ const Game = { addXP(n){ state.gamification.xp += n; while(state.gamification.xp >= this.nextLevelXP()) state.gamification.level++; Persist.save(); Dashboard.refreshGamification(); }, nextLevelXP(){ const L=state.gamification.level; return Math.floor(50 + L*L*15); }, tickStreak(){ const today = U.todayISO(), last = state.gamification.lastDay; if(last===today) return; const y = new Date(); y.setDate(y.getDate()-1); state.gamification.streak = (last===U.ymd(y)) ? (state.gamification.streak||0)+1 : 1; state.gamification.lastDay = today; Persist.save(); } }; /* --------------------------- Notifications -------------------------------- */ const Notifications = { async request(){ try{ const perm=await Notification.requestPermission(); if(perm==='granted'){ state.user.notif='on'; Persist.save(); U.toast('Notificações ativadas.'); } }catch(e){} }, notify(title,body){ if(state.user.notif==='on' && 'Notification' in window && Notification.permission==='granted'){ new Notification(title,{body}); } else { U.toast(`${title}: ${body}`); } } }; /* --------------------------- Pomodoro ------------------------------------ */ const Pomodoro = { phase:'focus', cycleIndex:1, remainingSec:25*60, timerId:null, startedEventId:null, readPrefs(){ const p=state.settings.pomodoro; prefFocus.value=p.focusMin; prefShort.value=p.shortMin; prefLong.value=p.longMin; prefCycles.value=p.cycles; }, writePrefs(){ const p=state.settings.pomodoro; p.focusMin=+prefFocus.value||25; p.shortMin=+prefShort.value||5; p.longMin=+prefLong.value||15; p.cycles=+prefCycles.value||4; Persist.save(); if(this.phase==='focus') this.setTimer(p.focusMin*60); }, setTimer(sec){ this.remainingSec = sec; timerClock.textContent = U.fmtTime(sec); timer.style.setProperty('--progress', 0); timer.setAttribute('aria-valuenow','0'); }, phaseLabel(){ return this.phase==='focus'?`Foco #${this.cycleIndex}`:(this.phase==='short'?'Pausa Curta':'Pausa Longa'); }, start(kind=null){ if(kind) this.phase=kind; if(this.timerId) return; const p=state.settings.pomodoro; let total = (this.phase==='focus'?p.focusMin:this.phase==='short'?p.shortMin:p.longMin)*60; if(this.remainingSec<=0 || this.remainingSec>total) this.remainingSec = total; const startTs = Date.now(); const cycle = { id:U.id(), kind:this.phase, plannedMin: total/60, startTs, endTs:null, interruptions:[] }; state.pomodoros.push(cycle); Persist.save(); this.timerId = setInterval(()=>{ this.remainingSec -= 1; const passed = total - this.remainingSec; const progress = Math.max(0,Math.min(1,passed/total))*100; timerClock.textContent = U.fmtTime(this.remainingSec); timer.style.setProperty('--progress', progress); timer.setAttribute('aria-valuenow', String(Math.round(progress))); timerPhase.textContent = this.phaseLabel(); if(this.remainingSec<=0){ clearInterval(this.timerId); this.timerId=null; cycle.endTs = Date.now(); Persist.save(); this.onEndCycle(cycle); } }, 1000); }, pause(){ if(this.timerId){ clearInterval(this.timerId); this.timerId=null; } }, reset(){ this.pause(); this.phase='focus'; this.cycleIndex=1; this.startedEventId=null; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); }, skipBreak(){ if(this.phase==='short' || this.phase==='long'){ this.pause(); this.phase='focus'; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); } }, onEndCycle(cycle){ if(cycle.kind==='focus'){ Game.addXP(Math.round(cycle.plannedMin/5)*5); Game.tickStreak(); if(this.startedEventId){ const ev = state.events.find(e=>e.id===this.startedEventId); if(ev){ ev.done = true; Persist.save(); Calendar.render(); } } } Notifications.notify(`Fim de ${cycle.kind==='focus'?'foco':'pausa'}`, 'Hora de trocar de fase!'); const p=state.settings.pomodoro; if(cycle.kind==='focus'){ if(this.cycleIndex % p.cycles === 0){ this.phase='long'; this.setTimer(p.longMin*60); this.cycleIndex=1; } else { this.phase='short'; this.setTimer(p.shortMin*60); this.cycleIndex++; } } else { this.phase='focus'; this.setTimer(p.focusMin*60); } timerPhase.textContent=this.phaseLabel(); if(cycle.kind==='focus'){ const due=Flashcards.dueToday().length; if(due>0) U.toast(`Revisões SM-2: ${due}`); } Dashboard.refresh(); } }; /* --------------------------- Dashboard ----------------------------------- */ const Dashboard = { refresh(){ const day=U.todayISO(); const today = state.pomodoros.filter(p=> new Date(p.startTs).toISOString().slice(0,10)===day && p.endTs); const focusedMin = today.filter(p=>p.kind==='focus').reduce((a,b)=>a+b.plannedMin,0); dashFocused.textContent = `${focusedMin}m`; dashSessions.textContent = `${today.length} ciclos`; dashDue.textContent = `${Flashcards.dueToday().length}`; this.refreshGamification(); }, refreshGamification(){ lvl.textContent=state.gamification.level; xp.textContent=state.gamification.xp; streak.textContent=`Streak: ${state.gamification.streak}`; } }; /* --------------------------- Calendar / Agenda --------------------------- */ const Calendar = { grid: document.getElementById('calendarGrid'), weekLabel: document.getElementById('weekLabel'), modeSel: document.getElementById('viewMode'), selEventId: null, currentMonday: (()=>{ const d=new Date();const day=d.getDay();const diff=(day===0?-6:1-day);d.setDate(d.getDate()+diff);d.setHours(0,0,0,0);return d; })(), render(){ const mode = this.modeSel.value; this.grid.innerHTML=''; // header const head = document.createElement('div'); head.className='col-head'; this.grid.append(head); const days=[]; for(let i=0;i<7;i++){ const d=new Date(this.currentMonday); d.setDate(d.getDate()+i); days.push(d); const h=document.createElement('div');h.className='col-head';h.textContent=d.toLocaleDateString('pt-BR',{weekday:'short',day:'2-digit',month:'2-digit'}); this.grid.append(h); } // hours col const hoursCol=document.createElement('div');hoursCol.className='hours'; for(let h=0;h<24;h++){ const div=document.createElement('div');div.textContent=String(h).padStart(2,'0')+':00'; hoursCol.append(div); } this.grid.append(hoursCol); // day cols for(let i=0;i<7;i++){ const dayCol=document.createElement('div'); dayCol.className='day'; dayCol.dataset.col=i; for(let r=0;r<24;r++){ const row=document.createElement('div'); row.className='row'; row.dataset.row=r; dayCol.append(row); } const dayISO = U.ymd(days[i]); const events = state.events.filter(e=> e.startISO.slice(0,10)===dayISO); events.forEach(ev=> this.addEventDOM(dayCol, ev)); this.grid.append(dayCol); } // nowline const now=new Date(); if(now>=this.currentMonday && now<new Date(+this.currentMonday+7*864e5)){ const nl=document.createElement('div');nl.className='nowline';nl.style.top=`${(now.getHours()*48)+Math.round(now.getMinutes()/60*48)}px`; this.grid.append(nl); } // label const end=new Date(this.currentMonday); end.setDate(end.getDate()+6); this.weekLabel.textContent=`${this.currentMonday.toLocaleDateString()} — ${end.toLocaleDateString()}`; // enable drag selection this.enableSelection(); renderTasks(); }, minutesFromY(y){ return Math.round((y/48)*60/15)*15; }, addEventDOM(dayCol, ev){ const start = new Date(ev.startISO), end = new Date(ev.endISO); const top = (start.getHours()*60+start.getMinutes())*(48/60); const height = Math.max(12, ((end-start)/60000)*(48/60)); const div = document.createElement('div'); div.className='event'; div.dataset.id=ev.id; div.style.top=`${top}px`; div.style.height=`${height}px`; div.style.background=`linear-gradient(180deg, ${ev.color||'#2a3567'}, #1c2657)`; div.tabIndex=0; div.textContent=ev.title||'Bloco'; const rt=document.createElement('div'); rt.className='resize-h resize-top'; const rb=document.createElement('div'); rb.className='resize-h resize-bottom'; div.append(rt, rb); div.addEventListener('click', ()=>this.select(ev.id, div)); // keyboard move/resize div.addEventListener('keydown', (e)=>{ if(this.selEventId!==ev.id) return; const step=15; if(['ArrowUp','ArrowDown','Delete','Enter'].includes(e.key)) e.preventDefault(); const s=new Date(ev.startISO), en=new Date(ev.endISO); if(e.key==='ArrowUp'){ s.setMinutes(s.getMinutes()-step); en.setMinutes(en.getMinutes()-step); } if(e.key==='ArrowDown'){ s.setMinutes(s.getMinutes()+step); en.setMinutes(en.getMinutes()+step); } if(e.key==='Delete'){ this.removeEvent(ev.id); return; } if(e.key==='Enter'){ this.openModal(ev.id); return; } // conflict check const changed = {...ev, startISO:s.toISOString(), endISO:en.toISOString()}; if(this.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ U.toast('Conflito detectado.'); return; } ev.startISO=s.toISOString(); ev.endISO=en.toISOString(); Persist.save(); this.render(); }); // pointer drag/resize let dragging=false, resizing=null, startY=0, startStart=null, startEnd=null; const onDown = (e)=>{ dragging=true; resizing = e.target.classList.contains('resize-top')?'top':e.target.classList.contains('resize-bottom')?'bottom':null; startY=e.clientY; startStart=new Date(ev.startISO); startEnd=new Date(ev.endISO); div.setPointerCapture?.(e.pointerId); }; const onMove = (e)=>{ if(!dragging) return; const dy = e.clientY - startY; const deltaMin = Math.round((dy/(48/60))/15)*15; let s=new Date(startStart), en=new Date(startEnd); if(resizing==='top'){ s.setMinutes(s.getMinutes()+deltaMin); if((en-s)/60000<15) s=new Date(en.getTime()-15*60000); } else if(resizing==='bottom'){ en.setMinutes(en.getMinutes()+deltaMin); if((en-s)/60000<15) en=new Date(s.getTime()+15*60000); } else { s.setMinutes(s.getMinutes()+deltaMin); en.setMinutes(en.getMinutes()+deltaMin); } const changed = {...ev, startISO:s.toISOString(), endISO:en.toISOString()}; if(this.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ return; } ev.startISO=s.toISOString(); ev.endISO=en.toISOString(); Persist.save(); this.render(); }; const onUp = (e)=>{ dragging=false; resizing=null; div.releasePointerCapture?.(e.pointerId); }; div.addEventListener('pointerdown', onDown); div.addEventListener('pointermove', onMove); div.addEventListener('pointerup', onUp); dayCol.append(div); }, enableSelection(){ // drag selection on empty space const grid=this.grid; let selecting=false, box=null, start=null, startCol=0; const onDown=(e)=>{ if(e.target.closest('.event')) return; const colEl=e.target.closest('.day'); if(!colEl) return; startCol=+colEl.dataset.col; selecting=true; start={x:e.clientX,y:e.clientY}; box=document.createElement('div'); box.className='selbox'; grid.append(box); box.style.left=e.clientX-grid.getBoundingClientRect().left+'px'; box.style.top=e.clientY-grid.getBoundingClientRect().top+'px'; box.style.width='0px'; box.style.height='0px'; grid.setPointerCapture?.(e.pointerId); }; const onMove=(e)=>{ if(!selecting) return; const r=grid.getBoundingClientRect(); box.style.width=Math.abs(e.clientX-start.x)+'px'; box.style.height=Math.abs(e.clientY-start.y)+'px'; box.style.left=Math.min(e.clientX,start.x)-r.left+'px'; box.style.top=Math.min(e.clientY,start.y)-r.top+'px'; }; const onUp=(e)=>{ if(!selecting) return; const day=new Date(Calendar.currentMonday); day.setDate(day.getDate()+startCol); const r=grid.querySelector(`.day[data-col="${startCol}"]`).getBoundingClientRect(); const y1=Math.min(start.y,e.clientY)-r.top, y2=Math.max(start.y,e.clientY)-r.top; const sMin=Calendar.minutesFromY(y1), eMin=Calendar.minutesFromY(y2); const s=new Date(day); s.setHours(0,0,0,0); s.setMinutes(sMin); const en=new Date(day); en.setHours(0,0,0,0); en.setMinutes(Math.max(sMin+15,eMin)); const ev = { id:U.id(), title:'Bloco', startISO:s.toISOString(), endISO:en.toISOString(), color:'#58a6ff', tags:[], notes:'', done:false }; if(Calendar.hasConflict(ev)){ U.toast('Conflito: bloco não criado.'); } else { state.events.push(ev); Persist.save(); Calendar.render(); Calendar.openModal(ev.id); } selecting=false; box?.remove(); grid.releasePointerCapture?.(e.pointerId); }; grid.onpointerdown=onDown; grid.onpointermove=onMove; grid.onpointerup=onUp; }, select(id, el){ this.selEventId=id; document.querySelectorAll('.event').forEach(e=>e.dataset.selected='false'); el && (el.dataset.selected='true'); }, removeEvent(id){ Persist.pushHistory(state); state.events=state.events.filter(e=>e.id!==id); Persist.save(); this.render(); U.toast('Bloco removido.'); }, hasConflict(candidate, ignoreId=null){ const dayISO = candidate.startISO.slice(0,10); const s = +new Date(candidate.startISO), en = +new Date(candidate.endISO); return state.events.some(ev=>{ if(ignoreId && ev.id===ignoreId) return false; if(ev.startISO.slice(0,10)!==dayISO) return false; const a=+new Date(ev.startISO), b=+new Date(ev.endISO); return Math.max(a,s) < Math.min(b,en); // overlap }); }, openModal(id){ const ev=state.events.find(e=>e.id===id); if(!ev) return; const m=document.getElementById('eventModal'); m.dataset.open='true'; m.setAttribute('aria-hidden','false'); evTitle.value=ev.title||'Bloco'; evColor.value=ev.color||'#58a6ff'; evDate.value=ev.startISO.slice(0,10); evStart.value=ev.startISO.slice(11,16); evEnd.value=ev.endISO.slice(11,16); evTags.value=(ev.tags||[]).join(', '); evNotes.value=ev.notes||''; saveEventBtn.onclick=()=>{ const sISO = `${evDate.value}T${evStart.value}:00.000Z`; const eISO = `${evDate.value}T${evEnd.value}:00.000Z`; const changed = {...ev, title:evTitle.value.trim()||'Bloco', color:evColor.value, startISO:sISO, endISO:eISO, tags:evTags.value.split(',').map(s=>s.trim()).filter(Boolean), notes:evNotes.value.trim() }; if(Calendar.hasConflict(changed, ev.id) && state.settings.agenda.conflict==='block'){ U.toast('Conflito: ajuste horários.'); return; } Persist.pushHistory(state); Object.assign(ev, changed); Persist.save(); Calendar.render(); U.toast('Bloco atualizado.'); close(); }; deleteEventBtn.onclick=()=>{ Calendar.removeEvent(ev.id); close(); }; startPomodoroFromEvent.onclick=()=>{ Pomodoro.startedEventId=ev.id; UI.goto('dashboard'); Pomodoro.phase='focus'; Pomodoro.setTimer(state.settings.pomodoro.focusMin*60); Pomodoro.start('focus'); close(); }; function close(){ m.dataset.open='false'; m.setAttribute('aria-hidden','true'); } closeEventModal.onclick=close; }, createFromTask(task, dateISO){ const start=new Date(dateISO+'T09:00:00.000Z'), end=new Date(+start+task.estimativaMin*60000); const ev={ id:U.id(), title:task.titulo, startISO:start.toISOString(), endISO:end.toISOString(), color:task.cor, tags:task.tags, notes:task.descricao, done:false }; if(this.hasConflict(ev)){ U.toast('Conflito: não adicionado.'); return; } Persist.pushHistory(state); state.events.push(ev); Persist.save(); this.render(); } }; function renderTasks(){ const box=document.getElementById('tasksList'); box.innerHTML=''; state.tasks.forEach(t=>{ const div=document.createElement('div'); div.className='task-item'; div.draggable=true; div.innerHTML=`<span>${t.titulo}</span><span class="tag">${(t.tags||[])[0]||'geral'}</span>`; div.addEventListener('dragstart', e=> e.dataTransfer.setData('text/plain', t.id)); box.append(div); }); document.querySelectorAll('.day').forEach(col=>{ col.addEventListener('dragover', e=>e.preventDefault()); col.addEventListener('drop', e=>{ const id=e.dataTransfer.getData('text/plain'); const task=state.tasks.find(x=>x.id===id); if(!task) return; const idx=+col.dataset.col; const d=new Date(Calendar.currentMonday); d.setDate(d.getDate()+idx); Calendar.createFromTask(task, U.ymd(d)); }); }); } /* --------------------------- Flashcards (SM-2) --------------------------- */ const Flashcards = { selectedId:null, dueToday(){ const today=new Date(); return state.flashcards.filter(c=>!c.sm2 || !c.sm2.next || new Date(c.sm2.next)<=today); }, add({question,answer,tags,media}){ Persist.pushHistory(state); const c={ id:U.id(), question, answer, tags, media, sm2:{EF:2.5, interval:0, n:0, last:null, next:new Date().toISOString()} }; state.flashcards.push(c); Persist.save(); this.refresh(); }, edit(id,patch){ const c=state.flashcards.find(x=>x.id===id); if(!c) return; Persist.pushHistory(state); Object.assign(c,patch); Persist.save(); this.refresh(); }, remove(id){ Persist.pushHistory(state); state.flashcards=state.flashcards.filter(c=>c.id!==id); Persist.save(); this.refresh(); }, schedule(card,quality){ // SM-2 simplified const sm=card.sm2||{EF:2.5, interval:0, n:0}; const q=U.clamp(quality,0,5); const now=new Date(); if(q<3){ sm.n=0; sm.interval=1; } else { if(sm.n===0) sm.interval=1; else if(sm.n===1) sm.interval=6; else sm.interval=Math.round(sm.interval*sm.EF); sm.n+=1; sm.EF = Math.max(1.3, sm.EF + (0.1 - (5-q)*(0.08 + (5-q)*0.02))); } sm.last=now.toISOString(); const next=new Date(now); next.setDate(now.getDate()+sm.interval); sm.next=next.toISOString(); card.sm2=sm; Persist.save(); }, refresh(){ const list=document.getElementById('fcList'); list.innerHTML=''; state.flashcards.forEach(c=>{ const div=document.createElement('div'); div.className='fc-item'; div.innerHTML=`<strong>${c.question}</strong> <div class="small muted">${(c.tags||[]).join(', ')||'sem tags'}</div> <div style="display:flex;gap:.4rem;margin-top:.4rem;flex-wrap:wrap"> <button class="btn" data-act="study" data-id="${c.id}">Estudar</button> <button class="btn" data-act="edit" data-id="${c.id}">Editar</button> <button class="btn btn--warn" data-act="del" data-id="${c.id}">Excluir</button> <input class="btn" value="${c.question.replace(/"/g,'"')}" style="flex:1" data-ignore-shortcuts readonly> </div>`; list.append(div); }); list.onclick=(e)=>{ const btn=e.target.closest('button'); if(!btn) return; const id=btn.dataset.id; const act=btn.dataset.act; if(act==='del'){ if(confirm('Excluir card?')) this.remove(id); } if(act==='edit'){ const c=state.flashcards.find(x=>x.id===id); if(!c) return; fcFront.value=c.question; fcBack.value=c.answer; fcTags.value=(c.tags||[]).join(','); fcMedia.value=c.media||''; this.selectedId=id; } if(act==='study'){ this.selectedId=id; StudyQueue.queue=[state.flashcards.find(x=>x.id===id)]; StudyQueue.idx=0; StudyUI.update(); UI.goto('estudo'); Tabs.show('flashcards'); } }; document.getElementById('dashDue').textContent=String(this.dueToday().length); StudyQueue.rebuild(); } }; const StudyQueue = { queue:[], idx:-1, rebuild(){ // interleaving simples por tag (deck = primeira tag) const due=Flashcards.dueToday(); const buckets={}; due.forEach(c=>{ const deck=(c.tags||['geral'])[0]; (buckets[deck]=buckets[deck]||[]).push(c); }); const decks=Object.keys(buckets); const out=[]; let i=0; while(decks.some(d=>buckets[d].length)){ const d=decks[i%decks.length]; const item=buckets[d].shift(); if(item) out.push(item); i++; } this.queue = out.slice(0,40); this.idx = this.queue.length?0:-1; StudyUI.update(); }, current(){ return this.queue[this.idx]; }, next(){ if(this.idx < this.queue.length-1){ this.idx++; StudyUI.update(); } else { U.toast('Fila concluída!'); StudyUI.empty(); } } }; const StudyUI = { card:document.getElementById('studyCard'), q:document.getElementById('studyQ'), a:document.getElementById('studyA'), m:document.getElementById('studyMedia'), empty:()=>{ document.getElementById('studyEmpty').style.display='block'; StudyUI.card.hidden=true; }, update(){ const c=StudyQueue.current(); if(!c){ this.empty(); return; } document.getElementById('studyEmpty').style.display='none'; this.card.hidden=false; this.q.textContent=c.question; this.a.textContent=c.answer; this.a.hidden=true; const src=U.sanitizeURL(c.media); if(src){ this.m.src=src; this.m.style.display='block'; } else { this.m.style.display='none'; } } }; /* --------------------------- Quizzes ------------------------------------- */ const Quizzes = { add(item){ Persist.pushHistory(state); state.quizbank.push(item); Persist.save(); this.refreshList(); }, refreshList(){ const qa=document.getElementById('quizArea'); qa.innerHTML=''; if(!state.quizbank.length){ qa.innerHTML='<div class="muted small">Nenhuma questão.</div>'; return; } const table=document.createElement('table'); table.className='table small'; table.innerHTML='<thead><tr><th>Tema</th><th>Tipo</th><th>Enunciado</th></tr></thead>'; const tb=document.createElement('tbody'); state.quizbank.forEach(q=>{ const tr=document.createElement('tr'); tr.innerHTML=`<td>${q.tema||'-'}</td><td>${q.tipo}</td><td>${q.prompt.slice(0,120)}</td>`; tb.append(tr); }); table.append(tb); qa.append(table); }, startMini(count= á ta terceir vez que vou te pedir para gerar este codigo, termine de gerar o codigo usando as minhas instruções, voce esta parando no meio do processo, tente não parar desta vez por favor
voce não esta terminanado o codigo termine ele, voce esta deixando o coidigo pela metade, por favor siga as coisas e termine o codigo com as alterações que eu quero
voce nao terminou de gerar
</main> </div> <!-- ====================== TOAST & MODALS ================================= --> <div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="modal" id="eventModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Editar Bloco</h3> <button class="btn" id="closeEventModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="evTitle" data-ignore-shortcuts></label> <label>Cor <input id="evColor" type="color" value="#58a6ff"></label> </div> <div class="row"> <label>Data <input id="evDate" type="date"></label> <label>Início <input id="evStart" type="time" step="900"></label> </div> <div class="row"> <label>Fim <input id="evEnd" type="time" step="900"></label> <label>Tags <input id="evTags" placeholder="matemática, revisão" data-ignore-shortcuts></label> </div> <label>Notas<textarea id="evNotes" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"> <button class="btn btn--danger" id="deleteEventBtn">Remover</button> <div style="flex:1"></div> <button class="btn" id="startPomodoroFromEvent">Pomodoro</button> <button class="btn btn--primary" id="saveEventBtn">Salvar</button> </div> </div> </div> <div class="modal" id="taskModal" aria-hidden="true" aria-modal="true" role="dialog"> <div class="dialog"> <header> <h3>Nova Tarefa</h3> <button class="btn" id="closeTaskModal" aria-label="Fechar">✕</button> </header> <div class="row"> <label>Título <input id="tkTitle" data-ignore-shortcuts></label> <label>Cor <input id="tkColor" type="color" value="#79c0ff"></label> </div> <div class="row"> <label>Estimativa (min) <input id="tkEst" type="number" min="15" step="15" value="30"></label> <label>Tags <input id="tkTags" placeholder="leitura, cálculo" data-ignore-shortcuts></label> </div> <label>Descrição<textarea id="tkDesc" rows="3" data-ignore-shortcuts></textarea></label> <div class="actions"><button class="btn btn--primary" id="saveTaskBtn">Adicionar</button></div> </div> </div> <div class="help" id="help" aria-hidden="true" role="dialog" aria-modal="true"> <div class="help__content"> <header style="display:flex;justify-content:space-between;align-items:center"> <h3>Ajuda & Atalhos</h3> <button class="btn" id="closeHelp">Fechar</button> </header> <div class="grid-3"> <div><h4>Global</h4><p><kbd>?</kbd> ajuda • <kbd>D</kbd> dashboard • <kbd>G</kbd> agenda • <kbd>E</kbd> estudo • <kbd>R</kbd> relatórios • <kbd>S</kbd> config</p></div> <div><h4>Pomodoro</h4><p><kbd>Espaço</kbd> iniciar/pausar • <kbd>L</kbd> pular pausa • <kbd>Shift</kbd>+<kbd>E</kbd> reiniciar</p></div> <div><h4>Agenda</h4><p>Setas movem • <kbd>Enter</kbd> edita • <kbd>Shift</kbd>+setas redimensiona • <kbd>Del</kbd> remove</p></div> </div> </div> </div> <!-- ====================== TEST RUNNER (UI) =============================== --> <div class="runner" id="runner" data-open="true" aria-live="polite"> <header> <h4>Test Runner</h4> <div style="display:flex;gap:.4rem;align-items:center"> <span id="runnerStatus" class="badge skip">idle</span> <button class="btn" id="toggleRunner">▾</button> <button class="btn btn--primary" id="runAllBtn">Rodar testes</button> </div> </header> <div class="body"> <div class="small muted">Executa automaticamente ao carregar. Logs abaixo.</div> <div class="log" id="runnerLog"></div> </div> </div> <script> /* ============================================================================= Focus+ PRO — JavaScript modular + Test Runner embutido ============================================================================= */ /* --------------------------- Utils & Env --------------------------------- */ const Env = { version:2, key:'focussite_v2', isTest:false, safeMode:false, dpr:window.devicePixelRatio||1 }; const Debug = { enabled:false, log(...a){ if(this.enabled) console.log('[DEBUG]',...a); } }; const U = { id: ()=>'id-'+Math.random().toString(36).slice(2,10), clamp:(v,min,max)=>Math.min(max,Math.max(min,v)), todayISO:()=>new Date().toISOString().slice(0,10), ymd:(d)=>d.toISOString().slice(0,10), toMin:(hhmm)=>{ const [h,m]=String(hhmm).split(':').map(Number); return (isFinite(h)?h:0)*60+(isFinite(m)?m:0); }, minToHHMM:(m)=>`${String(Math.floor(m/60)).padStart(2,'0')}:${String(m%60).padStart(2,'0')}`, fmtTime:(s)=>{const m=Math.floor(s/60),r=s%60;return `${String(m).padStart(2,'0')}:${String(r).padStart(2,'0')}`;}, toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.dataset.open='true'; setTimeout(()=>t.dataset.open='false',2200); }, download(name,text){ try{ const blob=new Blob([text],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href),500); }catch(e){} }, sanitizeURL(url){ if(!url||typeof url!=='string') return ''; const s=url.trim(); if(s.startsWith('data:image/')||s.startsWith('https://')||s.startsWith('http://')) return s; return ''; }, isEditing(){ const el=document.activeElement; if(!el) return false; const tag=(el.tagName||'').toLowerCase(); const editable=el.isContentEditable||el.getAttribute('role')==='textbox'; const ignore=el.closest?.('[data-ignore-shortcuts]'); const disabled=el.getAttribute?.('aria-disabled')==='true'||el.disabled; if(disabled) return false; if(ignore) return true; if(editable) return true; if(tag==='input'||tag==='textarea') return true; return false; }, }; /* --------------------------- Storage Adapter ----------------------------- */ const StorageAdapter=(()=>{ let mem={}; function readLS(k){ try{return localStorage.getItem(k);}catch(e){return null;} } function writeLS(k,v){ try{localStorage.setItem(k,v);return true;}catch(e){return false;} } return { mock:false, getItem(k){return this.mock?mem[k]??null:readLS(k);}, setItem(k,v){return this.mock?(mem[k]=v,true):writeLS(k,v);}, clear(){ if(this.mock){mem={};} else try{localStorage.clear();}catch(e){} }, setMock(v){this.mock=!!v;} }; })(); /* --------------------------- State & Persistence ------------------------- */ const defaultState=()=>({ version:Env.version, createdAt:Date.now(), user:{name:'',lang:'pt-BR',themeContrast:'off',fontScale:1,sound:'on',notif:'off',nightAuto:true}, settings:{ pomodoro:{focusMin:25,shortMin:5,longMin:15,cycles:4}, agenda:{snap:15,conflict:'block'} }, gamification:{xp:0,level:1,badges:[],streak:0,lastDay:null}, tasks:[], events:[], pomodoros:[], interruptions:[], flashcards:[], quizbank:[], records:[], history:{undo:[],redo:[]} }); let state=loadState(); function loadState(){ try{ const raw=StorageAdapter.getItem(Env.key); if(!raw) return defaultState(); const obj=JSON.parse(raw); if(obj.version!==Env.version) return migrate(obj); return obj; }catch(e){ return defaultState(); } } function migrate(old){ const fresh=defaultState(); try{ fresh.tasks=old.tasks||[]; if(old.events){ fresh.events=old.events.map(e=>{ if(typeof e.dataISO==='string'){ const d=e.dataISO; const s=U.minToHHMM(e.startMin||540); const en=U.minToHHMM(e.endMin||600); return { id:e.id||U.id(), title:e.titulo||e.title||'Bloco', startISO:`${d}T${s}:00.000Z`, endISO:`${d}T${en}:00.000Z`, color:e.cor||'#58a6ff', tags:e.tags||[], notes:e.notas||'', done:!!e.done }; } return e; }); } }catch(e){} fresh.version=Env.version; StorageAdapter.setItem(Env.key, JSON.stringify(fresh)); return fresh; } const Persist={ pending:false,last:0, save(th=Env.safeMode?500:200){ const now=Date.now(); if(this.pending && now-this.last<th) return; this.pending=true; this.last=now; setTimeout(()=>{ this.pending=false; try{StorageAdapter.setItem(Env.key, JSON.stringify(state));}catch(e){} }, th); }, backup(){ try{ U.download('focusplus_backup.json', JSON.stringify(state,null,2)); }catch(e){} }, import(obj){ if(!obj||typeof obj!=='object') return; state=Object.assign(defaultState(),obj); if(state.version!==Env.version){ state=migrate(state); } this.save(0); UI.refreshAll(); U.toast('Dados importados.'); }, wipe(){ StorageAdapter.clear(); state=defaultState(); UI.refreshAll(); U.toast('Dados apagados.'); }, pushHistory(snapshot){ state.history.undo.push(JSON.stringify(snapshot)); state.history.redo.length=0; }, undo(){ const last=state.history.undo.pop(); if(!last) return false; state.history.redo.push(JSON.stringify(state)); state=JSON.parse(last); this.save(0); UI.refreshAll(); return true; }, redo(){ const next=state.history.redo.pop(); if(!next) return false; state.history.undo.push(JSON.stringify(state)); state=JSON.parse(next); this.save(0); UI.refreshAll(); return true; } }; /* --------------------------- Gamification -------------------------------- */ const Game={ addXP(n){ state.gamification.xp+=n; while(state.gamification.xp>=this.nextLevelXP()) state.gamification.level++; Persist.save(); Dashboard.refreshGamification(); }, nextLevelXP(){ const L=state.gamification.level; return Math.floor(50+L*L*15); }, tickStreak(){ const today=U.todayISO(), last=state.gamification.lastDay; if(last===today) return; const y=new Date(); y.setDate(y.getDate()-1); state.gamification.streak=(last===U.ymd(y))?(state.gamification.streak||0)+1:1; state.gamification.lastDay=today; Persist.save(); } }; /* --------------------------- Notifications -------------------------------- */ const Notifications={ async request(){ try{ const perm=await Notification.requestPermission(); if(perm==='granted'){ state.user.notif='on'; Persist.save(); U.toast('Notificações ativadas.'); } }catch(e){} }, notify(t,b){ if(state.user.notif==='on' && 'Notification' in window && Notification.permission==='granted'){ new Notification(t,{body:b}); } else { U.toast(`${t}: ${b}`); } } }; /* --------------------------- Pomodoro ------------------------------------ */ const Pomodoro={ phase:'focus',cycleIndex:1,remainingSec:25*60,timerId:null,startedEventId:null, readPrefs(){ const p=state.settings.pomodoro; prefFocus.value=p.focusMin; prefShort.value=p.shortMin; prefLong.value=p.longMin; prefCycles.value=p.cycles; }, writePrefs(){ const p=state.settings.pomodoro; p.focusMin=+prefFocus.value||25; p.shortMin=+prefShort.value||5; p.longMin=+prefLong.value||15; p.cycles=+prefCycles.value||4; Persist.save(); if(this.phase==='focus') this.setTimer(p.focusMin*60); }, setTimer(sec){ this.remainingSec=sec; timerClock.textContent=U.fmtTime(sec); timer.style.setProperty('--progress',0); timer.setAttribute('aria-valuenow','0'); }, phaseLabel(){ return this.phase==='focus'?`Foco #${this.cycleIndex}`:(this.phase==='short'?'Pausa Curta':'Pausa Longa'); }, start(kind=null){ if(kind) this.phase=kind; if(this.timerId) return; const p=state.settings.pomodoro; let total=(this.phase==='focus'?p.focusMin:this.phase==='short'?p.shortMin:p.longMin)*60; if(this.remainingSec<=0||this.remainingSec>total) this.remainingSec=total; const startTs=Date.now(); const cycle={id:U.id(),kind:this.phase,plannedMin:total/60,startTs,endTs:null,interruptions:[]}; state.pomodoros.push(cycle); Persist.save(); this.timerId=setInterval(()=>{ this.remainingSec-=1; const passed=total-this.remainingSec; const progress=Math.max(0,Math.min(1,passed/total))*100; timerClock.textContent=U.fmtTime(this.remainingSec); timer.style.setProperty('--progress',progress); timer.setAttribute('aria-valuenow',String(Math.round(progress))); timerPhase.textContent=this.phaseLabel(); if(this.remainingSec<=0){ clearInterval(this.timerId); this.timerId=null; cycle.endTs=Date.now(); Persist.save(); this.onEndCycle(cycle); } },1000); }, pause(){ if(this.timerId){ clearInterval(this.timerId); this.timerId=null; } }, reset(){ this.pause(); this.phase='focus'; this.cycleIndex=1; this.startedEventId=null; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); }, skipBreak(){ if(this.phase==='short'||this.phase==='long'){ this.pause(); this.phase='focus'; this.setTimer(state.settings.pomodoro.focusMin*60); timerPhase.textContent=this.phaseLabel(); } }, onEndCycle(cycle){ if(cycle.kind==='focus'){ Game.addXP(Math.round(cycle.plannedMin/5)*5); Game.tickStreak(); if(this.startedEventId){ const ev=state.events.find(e=>e.id===this.startedEventId); if(ev){ ev.done=true; Persist.save(); Calendar.render(); } } } Notifications.notify(`Fim de ${cycle.kind==='focus'?'foco':'pausa'}`,'Hora de trocar de fase!'); const p=state.settings.pomodoro; if(cycle.kind==='focus'){ if(this.cycleIndex%p.cycles===0){ this.phase='long'; this.setTimer(p.longMin*60); this.cycleIndex=1; } else { this.phase='short'; this.setTimer(p.shortMin*60); this.cycleIndex++; } } else { this.phase='focus'; this.setTimer(p.focusMin*60); } timerPhase.textContent=this.phaseLabel(); if(cycle.kind==='focus'){ const due=Flashcards.dueToday().length; if(due>0) U.toast(`Revisões SM-2: ${due}`); } Dashboard.refresh(); } }; /* --------------------------- Dashboard ----------------------------------- */ const Dashboard={ refresh(){ const day=U.todayISO(); const today=state.pomodoros.filter(p=>new Date(p.startTs).toISOString().slice(0,10)===day && p.endTs); const focusedMin=today.filter(p=>p.kind==='focus').reduce((a,b)=>a+b.plannedMin,0); dashFocused.textContent=`${focusedMin}m`; dashSessions.textContent=`${today.length} ciclos`; dashDue.textContent=String(Flashcards.dueToday().length); this.refreshGamification(); }, refreshGamification(){ lvl.textContent=state.gamification.level; xp.textContent=state.gamification.xp; streak.textContent=`Streak: ${state.gamification.streak}`; } }; /* --------------------------- Calendar / Agenda --------------------------- */ const Calendar={ grid:document.getElementById('calendarGrid'), weekLabel:document.getElementById('weekLabel'), modeSel:document.getElementById('viewMode'), selEventId:null, currentMonday:(()=>{ const d=new Date(); const day=d.getDay(); const diff=(day===0?-6 voce esta parando sempre nessa parte, é a sexta vez que eu te digo isso e voce continua parando no mesmo lugar por favor termine o codigo, pense no que esta te empedindo de para e faça todas as alterações que eu te peditext<!-- ====================== DASHBOARD ================================== --> <section id="view-dashboard" class="panel" role="region" aria-labelledby="dash-title"> <div class="section-title"> <h2 id="dash-title">Dashboard</h2> <div> <button class="btn btn--primary" id="quickPomodoro">Pomodoro Agora</button> <button class="btn" id="exportBtn">Exportar JSON</button> <label class="btn"><input type="file" id="importInput" accept=".json" hidden>Importar JSON</label> </div> </div> <div class="grid-2"> <div class="panel"> <div class="pomodoro" aria-label="Pomodoro Timer"> <div class="timerwrap"> <div class="timer" id="timer" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Progresso do ciclo"> <div class="timer__inner"> <div class="timer__clock" id="timerClock">25:00</div> <div class="timer__phase" id="timerPhase">Foco #1</div> </div> </div> <div class="timer-actions"> <button class="btn btn--success" id="startBtn">Iniciar (Espaço)</button> <button class="btn" id="pauseBtn">Pausar</button> <button class="btn btn--warn" id="skipBtn">Pular Pausa</button> <button class="btn btn--danger" id="resetBtn">Reiniciar</button> </div> </div> <div> <div class="prefs"> <label>Foco (min) <input id="prefFocus" type="number" min="1" max="180" step="1" value="25"></label> <label>Pausa Curta <input id="prefShort" type="number" min="1" max="60" step="1" value="5"></label> <label>Pausa Longa <input id="prefLong" type="number" min="1" max="60" step="1" value="15"></label> <label>Ciclos até longa <input id="prefCycles" type="number" min="2" max="8" step="1" value="4"></label> </div> <hr class="sep"> <div class="small muted">Interrupções registradas ao alternar de aba. Ative notificações em <strong>Config</strong>.</div> </div> </div> </div> <div class="panel"> <div class="grid-3" role="list"> <div class="study-card" role="listitem"> <div class="muted small">Tempo focado hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashFocused">0m</div> <div class="small muted" id="dashSessions">0 ciclos</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Revisões SM-2 de hoje</div> <div style="font-weight:800;font-size:1.35rem" id="dashDue">0</div> <div class="small muted">Fila diária</div> </div> <div class="study-card" role="listitem"> <div class="muted small">Nível & XP</div> <div style="font-weight:800;font-size:1.35rem"><span id="lvl">1</span> • <span id="xp">0</span> XP</div> <div class="small muted" id="streak">Streak: 0</div> </div> </div> <hr class="sep"> <div class="small muted">Dica: arraste pendências para a <strong>Agenda</strong> e inicie Pomodoro pelo bloco.</div> </div> </div> </section> <!-- ====================== AGENDA ===================================== --> <section id="view-agenda" class="panel" hidden role="region" aria-labelledby="agenda-title"> <div class="section-title"> <h2 id="agenda-title">Agenda (Dia/Semana)</h2> <div> <button class="btn btn--primary" id="newTaskBtn">Nova Tarefa</button> <button class="btn" id="addEventNowBtn">Bloco Rápido</button> <select class="btn" id="viewMode"><option value="week">Semana</option><option value="day">Dia</option></select> </div> </div> <div class="calendar"> <aside class="tasks" aria-label="Pendências"> <h3 style="margin:0 0 .4rem 0">Pendências</h3> <div class="small muted">Arraste para a grade</div> <div id="tasksList"></div> </aside> <div class="calendar__wrap"> <div class="calendar__toolbar"> <div> <button class="btn" id="prevWeekBtn">◀</button> <button class="btn" id="todayBtn">Hoje</button> <button class="btn" id="nextWeekBtn">▶</button> </div> <div class="small muted" id="weekLabel"></div> <div style="display:flex;gap:.4rem"> <button class="btn" id="startFromSelectionBtn">Pomodoro do bloco</button> <button class="btn" id="undoBtn" title="Desfazer (agenda)">Undo</button> <button class="btn" id="redoBtn" title="Refazer (agenda)">Redo</button> </div> </div> <div class="calendar__grid" id="calendarGrid" role="grid" aria-label="Grade da agenda"></div> </div> </div> <div class="small muted" style="margin-top:.6rem">Teclado: setas movem seleção, <kbd>Enter</kbd> edita, <kbd>Shift</kbd>+setas redimensiona, <kbd>Del</kbd> remove.</div> </section> <!-- ====================== ESTUDO ===================================== --> <section id="view-estudo" class="panel" hidden role="region" aria-labelledby="study-title"> <div class="section-title"> <h2 id="study-title">Estudo</h2> <div><button class="btn" id="enterImmersive">Modo Imersivo</button></div> </div> <div class="tabbar" role="tablist" aria-label="Modos"> <button class="btn btn--primary" data-tab="pomodoro">Pomodoro</button> <button class="btn" data-tab="flashcards">Flashcards (SM-2)</button> <button class="btn" data-tab="quiz">Quiz</button> <button class="btn" data-tab="simulado">Simulado</button> </div> <div id="tab-pomodoro"> <div class="small muted">Use o Pomodoro no Dashboard. Aqui ficam dicas e pausas ativas.</div> <hr class="sep"> <div class="grid-3"> <div class="study-card"> <div><strong>Pausa ativa:</strong> respiração 4-4-4-4</div> <div class="muted small">Inspire 4s, segure 4s, expire 4s, segure 4s.</div> <button class="btn" id="breathBtn">Iniciar 60s</button> </div> <div class="study-card"><strong>Dual Coding</strong><div class="muted small">Associe um diagrama à sua nota.</div></div> <div class="study-card"><strong>Interleaving</strong><div class="muted small">Alterne tópicos quando cair o rendimento.</div></div> </div> </div> <div id="tab-flashcards" hidden> <div class="section-title"> <h3>Gerenciar Flashcards</h3> <div> <button class="btn" id="importCsvCards">Importar CSV</button> <input type="file" id="csvInput" accept=".csv" hidden> </div> </div> <form class="fc-form" id="fcForm" data-ignore-shortcuts> <label>Frente<textarea id="fcFront" rows="3" required></textarea></label> <label>Verso<textarea id="fcBack" rows="3" required></textarea></label> <label>Tags<input id="fcTags" placeholder="ex.: biologia, revisão"></label> <label>Mídia (URL opcional)<input id="fcMedia" placeholder="https:// ou data:image/..."></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="rebuildQueueBtn">Recalcular Fila</button> <button class="btn btn--warn" type="button" id="delSelCardBtn">Excluir Selecionado</button> <button class="btn" type="button" id="undoFcBtn">Undo</button> <button class="btn" type="button" id="redoFcBtn">Redo</button> </div> </form> <div class="grid-2" style="margin-top:.75rem"> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Cards</h3> <div class="fc-list" id="fcList"></div> </div> <div> <h3 class="small muted" style="margin:0 0 .5rem 0">Estudar — Fila de Hoje</h3> <div id="studyArea"> <div class="muted small">Pressione <kbd>N</kbd> para próximo; avalie com <kbd>0..5</kbd>.</div> <div class="study-card" id="studyCard" hidden> <div class="q" id="studyQ"></div> <div class="a" id="studyA" hidden></div> <img id="studyMedia" alt="" style="max-width:100%;border-radius:10px;display:none"> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="revealBtn">Mostrar resposta</button> <button class="btn" id="nextBtn">Próximo</button> </div> <div class="sm2-buttons" aria-label="Qualidade 0 a 5"> <button class="btn" data-q="0">0</button><button class="btn" data-q="1">1</button><button class="btn" data-q="2">2</button> <button class="btn" data-q="3">3</button><button class="btn" data-q="4">4</button><button class="btn" data-q="5">5</button> </div> </div> <div id="studyEmpty" class="muted small">Nenhum card devido.</div> </div> </div> </div> </div> <div id="tab-quiz" hidden> <form class="quiz-form" id="quizForm" data-ignore-shortcuts> <label>Tipo<select id="qType"><option value="mcq">Múltipla Escolha</option><option value="short">Resposta Curta</option><option value="cloze">Lacuna</option></select></label> <label>Tema<input id="qTheme" placeholder="ex.: história"></label> <label>Enunciado<textarea id="qPrompt" rows="3"></textarea></label> <label>Opções (MCQ — 1ª correta)<input id="qOptions" placeholder="Correta | Distrator 1 | Distrator 2"></label> <label>Resposta (short/cloze)<input id="qAnswer" placeholder="Texto exato"></label> <label>Dica<input id="qHint" placeholder="Opcional"></label> <div style="grid-column:1/-1;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn btn--primary" type="submit">Adicionar</button> <button class="btn" type="button" id="startQuizBtn">Mini-Quiz (5)</button> </div> </form> <div id="quizArea" style="margin-top:.75rem"></div> </div> <div id="tab-simulado" hidden> <div class="small muted">Simulado cronometrado (usa o banco de questões).</div> <div style="display:flex;gap:.5rem;margin:.5rem 0;flex-wrap:wrap"> <input class="btn" id="simTheme" placeholder="Tema (opcional)" style="min-width:220px" data-ignore-shortcuts> <input class="btn" id="simMinutes" type="number" min="5" max="180" value="20" style="min-width:120px" data-ignore-shortcuts> <button class="btn btn--primary" id="startSimBtn">Iniciar</button> </div> <div id="simArea"></div> </div> </section> <!-- ====================== RELATÓRIOS ================================= --> <section id="view-relatorios" class="panel" hidden role="region" aria-labelledby="reports-title"> <div class="section-title"> <h2 id="reports-title">Relatórios & Heatmap</h2> <div> <button class="btn" id="rebuildChartsBtn">Atualizar</button> <button class="btn" id="exportCsvBtn">Exportar CSV</button> </div> </div> <div class="canvas-grid"> <div class="chart"><h3 class="small muted">Horas focadas (7d)</h3><canvas id="chartFocus"></canvas></div> <div class="chart"><h3 class="small muted">Tempo por tag</h3><canvas id="chartTags"></canvas></div> <div class="chart"><h3 class="small muted">Acurácia de quizzes</h3><canvas id="chartQuiz"></canvas></div> <div class="chart"><h3 class="small muted">Calibração (prev × real)</h3><canvas id="chartCalib"></canvas></div> </div> <hr class="sep"> <h3 class="small muted">Heatmap 7 × 24</h3> <div class="chart"><canvas id="heatmap" height="240"></canvas></div> </section> <!-- ====================== CONFIG ===================================== --> <section id="view-config" class="panel" hidden role="region" aria-labelledby="config-title"> <div class="section-title"> <h2 id="config-title">Configurações</h2> </div> <div class="grid-2"> <div> <h3>Preferências</h3> <div style="display:grid;gap:.6rem"> <label class="small">Notificações <button class="btn" id="notifBtn">Permitir</button></label> <label class="small">Sons <select class="btn" id="soundSel"><option value="on">Ativado</option><option value="off">Desativado</option></select> </label> <label class="small">Alto contraste <select class="btn" id="contrastSel"><option value="off">Off</option><option value="on">On</option></select> </label> <label class="small">Fonte <select class="btn" id="fontSel"><option value="1">Normal</option><option value="1.1">+10%</option><option value="1.2">+20%</option></select> </label> <label class="small">Política de conflito (agenda) <select class="btn" id="conflictSel"><option value="block">Impedir</option><option value="push">Empurrar</option></select> </label> <label class="small">Safe-mode <select class="btn" id="safeSel"><option value="off">Off</option><option value="on">On</option></select> </label> </div> </div> <div> <h3>Dados & Backup</h3> <div style="display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="exportBtn2">Exportar JSON</button> <label class="btn"><input type="file" id="importInput2" accept=".json" hidden>Importar JSON</label> <button class="btn" id="forceBackupBtn">Forçar Backup</button> <button class="btn btn--danger" id="wipeBtn">Apagar Tudo</button> </div> <div style="margin-top:.5rem" class="small muted"> Modo <strong>Recover from Crash</strong> tenta reparar dados corrompidos e migrar schema. </div> <div style="margin-top:.4rem;display:flex;gap:.5rem;flex-wrap:wrap"> <button class="btn" id="recoverBtn">Recover from Crash</button> </div> </div> </div> </section>