desenvolvimento pre-lancamento

Commit inicial - add do repo privado para o repo NT

style: changes header's logo and colors

style: changes home page first session layout

feat: creates about us home page section

chore: creates home page section for whom

chore: creates student materails home page section

chore: creates teachers materials home page section

chore: creates teacher materials home page section

style: changes primary color

style: changes color at activities page

style: changes about page color

style: changes name to Decoda

fix: changes route to about page at footer

fix: changes background color

style: changes game page header colors

style: changes footer colors

chore: adds home page sections title

style: changes main font family to Lato

style: adds title font

fix: changes sizes to be more responsive for mobile

ajuste no build vercel

atualiza regras envio homol

Adiciona instrucoes de uso

add JupyterLite

fix solucao turtle

Add Mole Mash e Modal de Falhas

Add Progress Bar na pagina de Atividades

fix game name

chore: atualiza lockfile removendo vercel analytics

inclusão de efeito ao mudar de fase

add mecanismo de solução de fases em debug

vite config test

add BaseGame e refator do MoleMash

refatoração turtle

refatoração automato

refatoração automato

add tag

bug 1 e 2 automato

mostrar apenas games em homologação na pagina de atividades

aumentar timeout das fases finais do Turtle

fix bug scroll

add video

refactor semaforo

arrumar ordem das cores

add build docs

update vercel

update vercel

update vercel

update vercel

update vercel

add vercel jupyter

add vercel jupyter

fix deploy Vercel

fix deploy Vercel

fix deploy Vercel

add cripto

add cripto

refatoração

fix tour Mole Mash

.

remover arquivos de controle

chore: adds development tag for activity card

remover arquivos de status indevidamente versionados

atualizar cores nas atividades

add Quebra Cabeças

add Quebra Cabeças

add iniciativas

add Iniciativas

alteração de fotos pesadas

fix menu mobile

fix menu mobile

fix menu mobile

add Aspirador

update icons

update identidade visual documentação

update jupyter

add kernel python local

add kernel python local

add kernel python local

feat: add health check

feat: add primeiros passos

add letramento

mover letramento de lugar

update path games

update path games

fix: ajuste clique rapido no botão executar

remover dead code

fix: refactor: extract shared utilities for storage, phase unlock and mobile detection

stabilize context references and fix stale closure

extrair GameProgressContext do GameStateContext (SRP)

refactor(game): extrair usePhaser e useGameModals de GameBase + corrigir bugs descobertos

refactor(game): remove todos os aliases PT/EN duplicados

Remover aliases PT/EN da camada de modais

refactor + tests

security: add CodeSanitizer and integrate into GameInterpreter

- CodeSanitizer.js: 4 built-in rules (max_length, infinite_while,
  infinite_for, excessive_nesting) with pluggable extra rules
- GameInterpreter.executeCode: calls sanitizeCode() before js-interpreter,
  differentiates CodeSanitizationError (warn) from other errors (error)
- 21 unit tests for CodeSanitizer (100% coverage)
- 4 integration tests in GameInterpreter for sanitization paths

add CodeSanitizer

fix: fase 10 aspirador

fix: bug semaforo

teste

feat: add version

Ajusta a landing page para ficar mais próxima ao protótipo

ajusta raio da borda do botão de Acesse nosso Laboratório

pequenos ajustes de layout na página de iniciativas

atualiza tabela de jogos educativos com os jogos disponíveis atualmente

ajustados pequenos detalhes e informações do jogos na seção de guias pedagógicos

troca nome playground para laboratório e adiciona imagens do lab

adiciona documentação de conceitos básicos de programação

ajustado pequenos erros de digitação

adiciona tooltip com conceitos escondidos em hover na tag +N de conceitos

update docs dev

desativar tour

setup matriz MoleMash

setup matriz MoleMash

fix: link

update version

update docs

update docs

mudou o layout de quem somos

mudei as imgs dos icons e baixei o botao

centraliza titulo com imagem e ajusta sessão com gradiente vermelho-rosa

adiciona responsividade para a pagina quem somos

ajusta botão de conheça nossa história

ajustes

ajustes na home + add. teclado

update version

security

security

feat: add tapume para telas pequenas

v1.1.0

feat: decoda offline

feat: doc offline

offline

fix: ajustes para release

fix: navbar; config ordenação; versão

fix: rotas docs e jupyter para pwa

delete private files

Co-authored-by: Indra Araujo <indra.araujo.santos@gmail.com>
Co-authored-by: solange dos santos <sollangelive71@gmail.com>
This commit is contained in:
2025-10-29 21:30:14 -03:00
committed by Graciano
parent e24ee22b5a
commit 3da7d323e8
577 changed files with 121994 additions and 158 deletions

View File

@@ -0,0 +1,53 @@
import { MOUSE_ATIVIDADES_REGISTRY } from './mouse/mouseRegistry';
import { TECLADO_ATIVIDADES_REGISTRY } from './teclado/tecladoRegistry';
// Registry of available letramento (digital literacy) activities.
// Each category owns its own configuration file and this module only composes them.
export const ATIVIDADES_REGISTRY = {
...MOUSE_ATIVIDADES_REGISTRY,
...TECLADO_ATIVIDADES_REGISTRY,
};
/**
* @param {string} id
* @returns {Object|null}
*/
export function getAtividade(id) {
return ATIVIDADES_REGISTRY[id] ?? null;
}
/**
* @returns {Object[]}
*/
export function listarAtividades() {
return Object.values(ATIVIDADES_REGISTRY);
}
/**
* Lista atividades de uma categoria específica, ordenadas pela sequência da trilha
* @param {string} categoria
* @returns {Object[]}
*/
export function listarAtividadesPorCategoria(categoria) {
const atividades = Object.values(ATIVIDADES_REGISTRY).filter(
(atividade) => atividade.categoria === categoria
);
// Ordena pela sequência da trilha (começando pela que não aparece em 'proxima')
const atividadesOrdenadas = [];
const atividadesMap = new Map(atividades.map((a) => [a.id, a]));
// Encontra a primeira atividade (que não é 'proxima' de nenhuma outra)
const proximasIds = new Set(atividades.map((a) => a.proxima).filter(Boolean));
const primeira = atividades.find((a) => !proximasIds.has(a.id));
if (!primeira) return atividades; // Fallback caso haja ciclo
let atual = primeira;
while (atual && atividadesOrdenadas.length < atividades.length) {
atividadesOrdenadas.push(atual);
atual = atual.proxima ? atividadesMap.get(atual.proxima) : null;
}
return atividadesOrdenadas;
}

View File

@@ -0,0 +1,92 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const banner = document.getElementById('successBanner');
let filesPlaced = 0;
const TOTAL_FILES = 6;
notify('started');
// Create drop zone (folder)
const dropZone = document.createElement('div');
dropZone.className = 'absolute bottom-10 right-10 w-48 h-48 bg-yellow-100 border-4 border-yellow-300 rounded-xl flex flex-col items-center justify-center transition-all';
dropZone.innerHTML = `
<i data-lucide="folder" class="w-24 h-24 text-yellow-600 mb-2"></i>
<p class="text-lg font-bold text-gray-800">Meus Arquivos</p>
`;
arena.appendChild(dropZone);
lucide.createIcons();
// Create draggable files
const fileTypes = [
{ icon: 'file-text', color: 'red', name: 'Texto.txt' },
{ icon: 'image', color: 'purple', name: 'Foto.jpg' },
{ icon: 'music', color: 'green', name: 'Musica.mp3' },
{ icon: 'video', color: 'blue', name: 'Video.mp4' },
{ icon: 'file-code', color: 'orange', name: 'Codigo.js' },
{ icon: 'file-archive', color: 'gray', name: 'Arquivo.zip' },
];
fileTypes.forEach((file, i) => {
createDraggableFile(file, i);
});
function createDraggableFile(file, index) {
const fileEl = document.createElement('div');
fileEl.className = `absolute w-24 h-24 bg-white border-2 border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-grab shadow-lg transition-all hover:shadow-xl`;
const row = Math.floor(index / 3);
const col = index % 3;
fileEl.style.left = `${50 + col * 110}px`;
fileEl.style.top = `${50 + row * 110}px`;
fileEl.innerHTML = `
<i data-lucide="${file.icon}" class="w-8 h-8 text-${file.color}-500 mb-1"></i>
<p class="text-xs font-semibold text-gray-700">${file.name}</p>
`;
fileEl.draggable = true;
arena.appendChild(fileEl);
lucide.createIcons();
fileEl.addEventListener('dragstart', (e) => {
fileEl.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
fileEl.addEventListener('dragend', () => {
fileEl.classList.remove('dragging');
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('ring-4', 'ring-green-400', 'scale-105');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('ring-4', 'ring-green-400', 'scale-105');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('ring-4', 'ring-green-400', 'scale-105');
const dragging = document.querySelector('.dragging');
if (dragging) {
dragging.remove();
filesPlaced++;
notify('running', { step: filesPlaced });
if (filesPlaced >= TOTAL_FILES) {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}
}
});

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Arrastar</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
.dragging { opacity: 0.5; cursor: grabbing !important; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="move" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Arraste os arquivos para a pasta</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você aprendeu a arrastar!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,212 @@
// ═══════════════════════════════════════════════════════════════════════
// Mouse Basic Activity - Logic
// ═══════════════════════════════════════════════════════════════════════
// ── postMessage helpers ──────────────────────────────────────────────
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
// ── DOM Elements ─────────────────────────────────────────────────────
const arena = document.getElementById('arena');
const instrEl = document.getElementById('instruction');
const hintEl = document.getElementById('hint');
const coverWrap = document.getElementById('coverageWrap');
const coverBar = document.getElementById('coverageBar');
const progLabel = document.getElementById('progressLabel');
const banner = document.getElementById('successBanner');
const successMsg = document.getElementById('successMsg');
// ── State ────────────────────────────────────────────────────────────
const TOTAL_STEPS = 3;
let currentStep = 0;
let started = false;
const steps = [
{
instruction: 'Passo 1 de 3: Mova o mouse pela área abaixo',
hint: 'Explore toda a área movendo o mouse. Preencha pelo menos 60% dela.',
setup: setupMoveStep,
},
{
instruction: 'Passo 2 de 3: Clique no botão',
hint: 'Mova o cursor até o botão e clique uma vez.',
setup: setupClickStep,
},
{
instruction: 'Passo 3 de 3: Dê um duplo clique no botão',
hint: 'Dois cliques rápidos sobre o botão!',
setup: setupDblClickStep,
},
];
// ── Rendering ────────────────────────────────────────────────────────
function renderStep(step) {
const s = steps[step];
instrEl.textContent = s.instruction;
hintEl.textContent = s.hint;
clearArena();
s.setup();
}
function clearArena() {
arena.innerHTML = '';
arena.style.cursor = 'default';
}
function nextStep(feedbackMsg) {
if (!started) {
notify('started', { step: currentStep + 1 });
started = true;
}
notify('running', { step: currentStep + 1, message: feedbackMsg });
currentStep++;
setTimeout(() => {
if (currentStep >= TOTAL_STEPS) {
finishActivity();
} else {
renderStep(currentStep);
}
}, 1200);
}
function finishActivity() {
instrEl.textContent = 'Você completou todos os passos!';
hintEl.textContent = '';
clearArena();
arena.classList.add('hidden');
coverWrap.classList.add('hidden');
successMsg.textContent = 'Agora você sabe como mover e clicar com o mouse. Excelente trabalho!';
banner.classList.remove('hidden');
// Reinitialize icons in success banner
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}
// ═══════════════════════════════════════════════════════════════════════
// STEP 1: Move Mouse & Track Coverage
// ═══════════════════════════════════════════════════════════════════════
function setupMoveStep() {
coverWrap.classList.remove('hidden');
arena.style.cursor = 'crosshair';
const GRID_SIZE = 32;
const cols = Math.ceil(arena.clientWidth / GRID_SIZE);
const rows = Math.ceil(arena.clientHeight / GRID_SIZE);
const totalCells = cols * rows;
const visitedCells = new Set();
arena.addEventListener('mousemove', handleMouseMove);
function handleMouseMove(e) {
const rect = arena.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Track visited cells
const col = Math.floor(x / GRID_SIZE);
const row = Math.floor(y / GRID_SIZE);
const cellId = `${col},${row}`;
visitedCells.add(cellId);
const coverage = (visitedCells.size / totalCells) * 100;
coverBar.style.width = `${coverage}%`;
progLabel.textContent = `${Math.floor(coverage)}%`;
// Create cursor trail
const trail = document.createElement('div');
trail.className = 'absolute w-8 h-8 rounded-full bg-red-400/30 pointer-events-none';
trail.style.left = `${x}px`;
trail.style.top = `${y}px`;
trail.style.transform = 'translate(-50%, -50%)';
arena.appendChild(trail);
setTimeout(() => trail.remove(), 800);
// Success condition
if (coverage >= 60) {
arena.removeEventListener('mousemove', handleMouseMove);
nextStep('Ótimo! Você explorou a área com sucesso!');
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// STEP 2: Single Click
// ═══════════════════════════════════════════════════════════════════════
function setupClickStep() {
arena.style.cursor = 'default';
placeTarget(false);
}
// ═══════════════════════════════════════════════════════════════════════
// STEP 3: Double Click
// ═══════════════════════════════════════════════════════════════════════
function setupDblClickStep() {
arena.style.cursor = 'default';
placeTarget(true);
}
function placeTarget(isDblClick) {
const SIZE = 140;
const maxX = arena.clientWidth - SIZE - 20;
const maxY = arena.clientHeight - SIZE - 20;
const x = Math.floor(Math.random() * maxX) + 10;
const y = Math.floor(Math.random() * maxY) + 10;
const button = document.createElement('button');
button.className = 'absolute bg-green-500 hover:bg-green-600 active:scale-95 text-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-3 text-2xl font-bold';
button.style.width = `${SIZE}px`;
button.style.height = `${SIZE}px`;
button.style.left = `${x}px`;
button.style.top = `${y}px`;
// Add Lucide icon
const icon = document.createElement('i');
icon.setAttribute('data-lucide', isDblClick ? 'pointer' : 'hand');
icon.className = 'w-16 h-16';
button.appendChild(icon);
arena.appendChild(button);
// Initialize the icon
lucide.createIcons();
if (isDblClick) {
button.addEventListener('dblclick', () => {
button.remove();
nextStep('Incrível! Você aprendeu o duplo clique!');
});
// Mobile fallback: two taps
let tapCount = 0;
let tapTimer;
button.addEventListener('click', (e) => {
tapCount++;
if (tapCount === 1) {
tapTimer = setTimeout(() => { tapCount = 0; }, 400);
} else if (tapCount === 2) {
clearTimeout(tapTimer);
button.remove();
nextStep('Incrível! Você aprendeu o duplo clique!');
}
e.stopPropagation();
});
} else {
button.addEventListener('click', () => {
button.remove();
nextStep('Perfeito! Você clicou no alvo!');
});
}
}
// ── Initialize ───────────────────────────────────────────────────────
renderStep(0);

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Básico</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body {
overflow: hidden;
height: 100vh;
width: 100vw;
}
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<!-- Activity Card -->
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<!-- Instructions Header -->
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="mouse-pointer" class="w-12 h-12 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Carregando...</h2>
<p class="text-lg text-gray-600" id="hint"></p>
</div>
</div>
<!-- Content Container -->
<div class="flex-1 p-6 overflow-hidden flex flex-col">
<!-- Progress Bar (Step 1 only) -->
<div id="coverageWrap" class="hidden mb-4">
<div class="flex items-center justify-between mb-2">
<p class="text-lg font-medium text-gray-700">Cobertura da área:</p>
<p class="text-lg font-bold text-red-600" id="progressLabel">0%</p>
</div>
<div class="w-full h-6 bg-gray-200 rounded-full overflow-hidden shadow-inner">
<div id="coverageBar" class="h-full bg-red-500 transition-all duration-300 rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- Arena -->
<div id="arena" class="relative flex-1 bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<!-- Success Banner -->
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700" id="successMsg"></p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>
// Initialize Lucide icons
lucide.createIcons();
</script>
</body>
</html>

View File

@@ -0,0 +1,73 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const contextMenu = document.getElementById('contextMenu');
const banner = document.getElementById('successBanner');
let currentStep = 0;
notify('started');
// Create target icon
const target = document.createElement('div');
target.className = 'flex items-center gap-3 bg-white border-2 border-gray-300 rounded-lg p-6 shadow-lg';
target.innerHTML = `
<i data-lucide="file" class="w-16 h-16 text-red-500"></i>
<div>
<p class="text-2xl font-bold text-gray-800">Documento.txt</p>
<p class="text-sm text-gray-600">Clique com botão direito aqui</p>
</div>
`;
arena.appendChild(target);
lucide.createIcons();
target.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (currentStep === 0) {
notify('running', { step: 1 });
currentStep = 1;
}
showContextMenu(e.pageX, e.pageY);
});
function showContextMenu(x, y) {
contextMenu.style.left = `${x - arena.getBoundingClientRect().left}px`;
contextMenu.style.top = `${y - arena.getBoundingClientRect().top}px`;
contextMenu.innerHTML = `
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="open">
<i data-lucide="folder-open" class="w-4 h-4"></i>
Abrir
</button>
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="copy">
<i data-lucide="copy" class="w-4 h-4"></i>
Copiar
</button>
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 rounded flex items-center gap-2" data-action="delete">
<i data-lucide="trash-2" class="w-4 h-4"></i>
Excluir
</button>
`;
contextMenu.classList.remove('hidden');
lucide.createIcons();
contextMenu.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
contextMenu.classList.add('hidden');
target.remove();
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
});
});
}
document.addEventListener('click', () => {
contextMenu.classList.add('hidden');
});

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Botão Direito</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="mouse-pointer-click" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique com botão direito</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden flex items-center justify-center">
<div id="contextMenu" class="hidden absolute bg-white border-2 border-gray-300 rounded-lg shadow-2xl p-2 min-w-[200px]"></div>
</div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você aprendeu o botão direito!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,64 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const banner = document.getElementById('successBanner');
let remaining = 10;
notify('started');
function createTargets() {
for (let i = 0; i < 10; i++) {
const SIZE = 80;
const maxX = arena.clientWidth - SIZE - 20;
const maxY = arena.clientHeight - SIZE - 20;
let x, y, overlapping;
do {
overlapping = false;
x = Math.floor(Math.random() * maxX) + 10;
y = Math.floor(Math.random() * maxY) + 10;
document.querySelectorAll('.target').forEach(existing => {
const ex = parseInt(existing.style.left);
const ey = parseInt(existing.style.top);
const dist = Math.sqrt((x - ex) ** 2 + (y - ey) ** 2);
if (dist < SIZE + 20) overlapping = true;
});
} while (overlapping);
const target = document.createElement('button');
target.className = 'target absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
target.style.width = `${SIZE}px`;
target.style.height = `${SIZE}px`;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
arena.appendChild(target);
target.addEventListener('click', () => {
target.classList.remove('from-red-500', 'to-red-600');
target.classList.add('from-green-400', 'to-green-500', 'opacity-50');
target.disabled = true;
remaining--;
notify('running', { remaining });
if (remaining === 0) {
setTimeout(() => {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}, 500);
}
});
}
}
createTargets();

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Múltiplos Cliques</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="grid-3x3" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique em todos os círculos vermelhos</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você clicou em todos!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,592 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const instrEl = document.getElementById('instruction');
const banner = document.getElementById('successBanner');
let currentChallenge = 0;
const challenges = [
{ name: 'Fase 1: Cliques Precisos', task: precisionChallenge },
{ name: 'Fase 2: Sequência Rápida', task: sequenceSpeedChallenge },
{ name: 'Fase 3: Seguir Trilha', task: pathFollowChallenge },
{ name: 'Fase 4: Múltiplos Alvos', task: multiTargetChallenge },
{ name: 'Fase 5: Menu de Contexto', task: contextMenuChallenge },
{ name: 'Fase 6: Organizar Arquivos', task: fileOrganizerChallenge },
{ name: 'Fase 7: Desenhar Círculo', task: drawCircleChallenge },
{ name: 'Fase 8: Desafio Final', task: finalBossChallenge },
];
notify('started');
function nextChallenge() {
if (currentChallenge >= challenges.length) {
finishActivity();
return;
}
arena.innerHTML = '';
const challenge = challenges[currentChallenge];
instrEl.textContent = challenge.name;
notify('running', { step: currentChallenge + 1 });
challenge.task();
}
// Fase 1: Cliques em alvos progressivamente menores (precisão)
function precisionChallenge() {
let targets = 0;
const sizes = [80, 60, 45, 35];
function createTarget() {
if (targets >= sizes.length) {
currentChallenge++;
setTimeout(nextChallenge, 800);
return;
}
const target = document.createElement('button');
const size = sizes[targets];
const maxX = arena.clientWidth - size - 20;
const maxY = arena.clientHeight - size - 20;
target.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-xl hover:scale-110 transition-transform';
target.style.width = `${size}px`;
target.style.height = `${size}px`;
target.style.left = `${20 + Math.random() * maxX}px`;
target.style.top = `${20 + Math.random() * maxY}px`;
target.innerHTML = `<span class="text-white text-xl font-bold">${targets + 1}</span>`;
target.addEventListener('click', () => {
target.remove();
targets++;
createTarget();
});
arena.appendChild(target);
}
createTarget();
}
// Fase 2: Clicar números 1-5 o mais rápido possível
function sequenceSpeedChallenge() {
const startTime = Date.now();
let current = 1;
const total = 5;
for (let i = 1; i <= total; i++) {
const btn = document.createElement('button');
btn.className = 'absolute w-20 h-20 bg-red-500 text-white text-2xl font-bold rounded-full shadow-lg hover:scale-110 transition-transform';
btn.textContent = i;
btn.style.left = `${Math.random() * (arena.clientWidth - 100)}px`;
btn.style.top = `${Math.random() * (arena.clientHeight - 100)}px`;
btn.addEventListener('click', () => {
if (i === current) {
btn.classList.remove('bg-red-500', 'to-red-600');
btn.classList.add('bg-green-400', 'to-green-500', 'opacity-50');
// btn.style.background = '#10b981';
btn.disabled = true;
current++;
if (current > total) {
const time = ((Date.now() - startTime) / 1000).toFixed(1);
setTimeout(() => {
instrEl.textContent = `Fase 2: Concluída em ${time}s!`;
currentChallenge++;
setTimeout(nextChallenge, 1500);
}, 300);
}
} else {
btn.classList.add('animate-bounce');
setTimeout(() => btn.classList.remove('animate-bounce'), 500);
}
});
arena.appendChild(btn);
}
}
// Fase 3: Seguir caminho sem sair (CÓPIA EXATA da atividade mouse-controle)
function pathFollowChallenge() {
const width = arena.clientWidth;
const height = arena.clientHeight;
// Criar SVG
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', width);
svg.setAttribute('height', height);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.className = 'absolute inset-0';
arena.appendChild(svg);
let started = false;
let progress = 0;
let completed = false; // Flag para evitar múltiplas conclusões
const TARGET_PROGRESS = 90;
// Create curved path
const pathData = `M 50,${height/2}
Q ${width/4},${height/4} ${width/2},${height/2}
T ${width-50},${height/2}`;
// Background path (wider, gray)
const bgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
bgPath.setAttribute('d', pathData);
bgPath.setAttribute('stroke', '#e5e7eb');
bgPath.setAttribute('stroke-width', '120');
bgPath.setAttribute('fill', 'none');
bgPath.setAttribute('stroke-linecap', 'round');
svg.appendChild(bgPath);
// Progress path (green)
const progressPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
progressPath.setAttribute('d', pathData);
progressPath.setAttribute('stroke', '#22c55e');
progressPath.setAttribute('stroke-width', '100');
progressPath.setAttribute('fill', 'none');
progressPath.setAttribute('stroke-linecap', 'round');
progressPath.setAttribute('stroke-dasharray', '1000');
progressPath.setAttribute('stroke-dashoffset', '1000');
svg.appendChild(progressPath);
// Start point with pulse animation
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
startCircle.setAttribute('cx', '50');
startCircle.setAttribute('cy', height/2);
startCircle.setAttribute('r', '30');
startCircle.setAttribute('fill', '#ef4444');
startCircle.setAttribute('cursor', 'pointer');
startCircle.style.animation = 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite';
svg.appendChild(startCircle);
// Pulse ring for extra visibility
const pulseRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
pulseRing.setAttribute('cx', '50');
pulseRing.setAttribute('cy', height/2);
pulseRing.setAttribute('r', '30');
pulseRing.setAttribute('fill', 'none');
pulseRing.setAttribute('stroke', '#ef4444');
pulseRing.setAttribute('stroke-width', '3');
svg.appendChild(pulseRing);
// Animate pulse ring
pulseRing.innerHTML = '<animate attributeName="r" from="30" to="50" dur="1.5s" repeatCount="indefinite" /><animate attributeName="opacity" from="0.8" to="0" dur="1.5s" repeatCount="indefinite" />';
// Add "Click here" text
const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
startText.setAttribute('x', '50');
startText.setAttribute('y', height/2 + 60);
startText.setAttribute('text-anchor', 'middle');
startText.setAttribute('fill', '#ef4444');
startText.setAttribute('font-size', '16');
startText.setAttribute('font-weight', 'bold');
startText.textContent = 'Clique aqui';
svg.appendChild(startText);
// Add click to start
startCircle.addEventListener('click', () => {
if (!started) {
started = true;
startCircle.setAttribute('fill', '#22c55e');
startCircle.style.animation = '';
pulseRing.remove();
startText.remove();
}
});
// End point
const endCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
endCircle.setAttribute('cx', width - 50);
endCircle.setAttribute('cy', height/2);
endCircle.setAttribute('r', '30');
endCircle.setAttribute('fill', '#22c55e');
svg.appendChild(endCircle);
// Track mouse movement
svg.addEventListener('mousemove', (e) => {
if (!started) return;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check if on path
const pathLength = bgPath.getTotalLength();
let minDist = Infinity;
for (let i = 0; i < pathLength; i += 10) {
const point = bgPath.getPointAtLength(i);
const dist = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
if (dist < minDist) minDist = dist;
}
if (minDist < 60) {
// On path
const progressPercent = (x / width) * 100;
if (progressPercent > progress) {
progress = progressPercent;
const offset = 1000 - (progress / 100) * 1000;
progressPath.setAttribute('stroke-dashoffset', offset);
if (progress >= TARGET_PROGRESS && !completed) {
// Avança para a próxima fase do desafio completo
completed = true;
currentChallenge++;
setTimeout(nextChallenge, 800);
}
}
}
});
}
// Fase 4: Clicar 8 alvos simultâneos
function multiTargetChallenge() {
let remaining = 8;
for (let i = 0; i < remaining; i++) {
const target = document.createElement('button');
target.className = 'absolute w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
target.style.left = `${50 + Math.random() * (arena.clientWidth - 150)}px`;
target.style.top = `${50 + Math.random() * (arena.clientHeight - 150)}px`;
const bullseye = document.createElement('div');
bullseye.className = 'absolute inset-0 flex items-center justify-center';
bullseye.innerHTML = '<div class="w-6 h-6 bg-white rounded-full"></div>';
target.appendChild(bullseye);
target.addEventListener('click', () => {
target.remove();
remaining--;
if (remaining === 0) {
currentChallenge++;
setTimeout(nextChallenge, 800);
}
});
arena.appendChild(target);
}
lucide.createIcons();
}
// Fase 5: Menu de contexto e seleção correta
function contextMenuChallenge() {
const file = document.createElement('div');
file.className = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-32 h-32 bg-white border-2 border-gray-300 rounded-lg flex flex-col items-center justify-center gap-2 cursor-pointer';
file.innerHTML = '<i data-lucide="file-text" class="w-16 h-16 text-blue-500"></i><span class="text-sm font-semibold">arquivo.txt</span>';
arena.appendChild(file);
const menu = document.createElement('div');
menu.className = 'absolute hidden bg-white rounded-lg shadow-2xl border border-gray-200 py-2 min-w-[180px]';
menu.innerHTML = `
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="open">
<i data-lucide="folder-open" class="w-4 h-4"></i>Abrir
</button>
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="copy">
<i data-lucide="copy" class="w-4 h-4"></i>Copiar
</button>
<button class="w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-2" data-action="delete">
<i data-lucide="trash-2" class="w-4 h-4 text-red-500"></i>Excluir
</button>
<button class="w-full px-4 py-2 text-left hover:bg-green-100 text-green-700 font-semibold flex items-center gap-2" data-action="properties">
<i data-lucide="info" class="w-4 h-4"></i>Propriedades ✓
</button>
`;
arena.appendChild(menu);
lucide.createIcons();
file.addEventListener('contextmenu', (e) => {
e.preventDefault();
menu.classList.remove('hidden');
menu.style.left = `${e.clientX - arena.getBoundingClientRect().left}px`;
menu.style.top = `${e.clientY - arena.getBoundingClientRect().top}px`;
});
menu.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.action === 'properties') {
currentChallenge++;
setTimeout(nextChallenge, 800);
} else {
menu.classList.add('hidden');
btn.classList.add('bg-red-100');
setTimeout(() => {
btn.classList.remove('bg-red-100');
menu.classList.add('hidden');
}, 500);
}
});
});
arena.addEventListener('click', () => {
menu.classList.add('hidden');
});
}
// Fase 6: Arrastar 5 itens para pastas corretas
function fileOrganizerChallenge() {
const files = [
{ name: 'foto.jpg', type: 'image', icon: 'image', folder: 'images' },
{ name: 'musica.mp3', type: 'music', icon: 'music', folder: 'music' },
{ name: 'video.mp4', type: 'video', icon: 'video', folder: 'videos' },
{ name: 'doc.pdf', type: 'document', icon: 'file-text', folder: 'documents' },
{ name: 'code.js', type: 'code', icon: 'code', folder: 'code' },
];
const folders = [
{ id: 'images', label: 'Imagens', color: 'blue' },
{ id: 'music', label: 'Músicas', color: 'purple' },
{ id: 'videos', label: 'Vídeos', color: 'red' },
{ id: 'documents', label: 'Documentos', color: 'yellow' },
{ id: 'code', label: 'Código', color: 'green' },
];
let completed = 0;
// Criar pastas
folders.forEach((folder, i) => {
const div = document.createElement('div');
div.className = `absolute w-28 h-28 border-4 border-dashed border-${folder.color}-400 bg-${folder.color}-50 rounded-xl flex flex-col items-center justify-center gap-1`;
div.style.left = `${50 + i * 140}px`;
div.style.bottom = '30px';
div.innerHTML = `<i data-lucide="folder" class="w-8 h-8 text-${folder.color}-600"></i><span class="text-xs font-semibold text-${folder.color}-700">${folder.label}</span>`;
div.dataset.folderId = folder.id;
arena.appendChild(div);
div.addEventListener('dragover', (e) => {
e.preventDefault();
div.classList.add('scale-110', 'shadow-xl');
});
div.addEventListener('dragleave', () => {
div.classList.remove('scale-110', 'shadow-xl');
});
div.addEventListener('drop', (e) => {
e.preventDefault();
div.classList.remove('scale-110', 'shadow-xl');
const fileId = e.dataTransfer.getData('text/plain');
const file = files.find(f => f.name === fileId);
if (file && file.folder === folder.id) {
const dragged = document.querySelector(`[data-file="${fileId}"]`);
if (dragged) {
dragged.remove();
completed++;
if (completed === files.length) {
currentChallenge++;
setTimeout(nextChallenge, 800);
}
}
} else {
div.classList.add('animate-bounce');
setTimeout(() => div.classList.remove('animate-bounce'), 500);
}
});
});
// Criar arquivos
files.forEach((file, i) => {
const div = document.createElement('div');
div.className = 'absolute w-20 h-20 bg-white border-2 border-gray-300 rounded-lg cursor-grab flex flex-col items-center justify-center gap-1 hover:shadow-lg transition-shadow';
div.style.left = `${100 + i * 100}px`;
div.style.top = '80px';
div.innerHTML = `<i data-lucide="${file.icon}" class="w-8 h-8 text-gray-700"></i><span class="text-xs font-medium truncate">${file.name}</span>`;
div.draggable = true;
div.dataset.file = file.name;
div.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', file.name);
div.classList.add('opacity-50');
});
div.addEventListener('dragend', () => {
div.classList.remove('opacity-50');
});
arena.appendChild(div);
});
lucide.createIcons();
}
// Fase 7: Desenhar círculo com 60% de cobertura
function drawCircleChallenge() {
const canvas = document.createElement('canvas');
canvas.width = arena.clientWidth;
canvas.height = arena.clientHeight;
canvas.className = 'absolute inset-0';
arena.appendChild(canvas);
const ctx = canvas.getContext('2d');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 120;
// Desenha círculo guia pontilhado
ctx.setLineDash([10, 10]);
ctx.strokeStyle = '#9ca3af';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Texto instrução
ctx.fillStyle = '#6b7280';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Desenhe sobre o círculo (60% mínimo)', centerX, centerY - radius - 30);
let isDrawing = false;
const segments = new Set();
const totalSegments = 120;
canvas.addEventListener('mousedown', () => {
isDrawing = true;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dist = Math.hypot(x - centerX, y - centerY);
if (Math.abs(dist - radius) < 25) {
const angle = Math.atan2(y - centerY, x - centerX);
const segmentId = Math.floor(((angle + Math.PI) / (2 * Math.PI)) * totalSegments);
segments.add(segmentId);
// Desenha ponto
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
const coverage = (segments.size / totalSegments) * 100;
if (coverage >= 60) {
isDrawing = false;
setTimeout(() => {
currentChallenge++;
nextChallenge();
}, 500);
}
}
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
});
}
// Fase 8: Desafio final - alvos móveis com tempo limitado
function finalBossChallenge() {
let score = 0;
let timeLeft = 20;
const targetCount = 25;
const timer = document.createElement('div');
timer.className = 'absolute top-4 left-1/2 -translate-x-1/2 text-4xl font-bold text-red-600 bg-white px-6 py-2 rounded-full shadow-xl';
arena.appendChild(timer);
const scoreDisplay = document.createElement('div');
scoreDisplay.className = 'absolute top-4 right-4 text-2xl font-bold text-green-600 bg-white px-4 py-2 rounded-full shadow-xl';
arena.appendChild(scoreDisplay);
function updateDisplay() {
timer.textContent = `${timeLeft}s`;
scoreDisplay.textContent = `${score}/${targetCount}`;
}
function createMovingTarget() {
const target = document.createElement('button');
const size = 40 + Math.random() * 30;
target.className = 'absolute bg-gradient-to-br from-red-500 to-orange-500 rounded-full shadow-xl hover:scale-125 transition-transform';
target.style.width = `${size}px`;
target.style.height = `${size}px`;
let x = Math.random() * (arena.clientWidth - size);
let y = Math.random() * (arena.clientHeight - size);
let vx = (Math.random() - 0.5) * 3;
let vy = (Math.random() - 0.5) * 3;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
const moveInterval = setInterval(() => {
x += vx;
y += vy;
if (x < 0 || x > arena.clientWidth - size) vx *= -1;
if (y < 0 || y > arena.clientHeight - size) vy *= -1;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}, 20);
target.addEventListener('click', () => {
clearInterval(moveInterval);
target.remove();
score++;
updateDisplay();
if (score >= targetCount) {
clearInterval(gameInterval);
setTimeout(() => {
currentChallenge++;
nextChallenge();
}, 500);
} else {
createMovingTarget();
}
});
arena.appendChild(target);
}
// Criar alvos iniciais
for (let i = 0; i < 5; i++) {
createMovingTarget();
}
updateDisplay();
const gameInterval = setInterval(() => {
timeLeft--;
updateDisplay();
if (timeLeft <= 0) {
clearInterval(gameInterval);
if (score >= targetCount * 0.7) {
alert(`Bom trabalho! ${score}/${targetCount} alvos`);
currentChallenge++;
nextChallenge();
} else {
alert(`Tempo esgotado! Você acertou ${score}/${targetCount}. Tente novamente!`);
finalBossChallenge();
}
}
}, 1000);
}
function finishActivity() {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}
nextChallenge();

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Desafio Completo</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
.dragging { opacity: 0.5; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="trophy" class="w-12 h-12 text-red-600"></i>
<h2 id="instruction" class="text-2xl font-bold text-gray-800">Desafio Completo</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Parabéns!</h2>
<p class="text-xl text-gray-700">Você dominou todas as habilidades do mouse!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,144 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const svg = document.getElementById('pathSvg');
const banner = document.getElementById('successBanner');
const width = arena.clientWidth;
const height = arena.clientHeight;
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
let started = false;
let onPath = true;
let progress = 0;
const TARGET_PROGRESS = 90;
notify('started');
// Create curved path
const pathData = `M 50,${height/2}
Q ${width/4},${height/4} ${width/2},${height/2}
T ${width-50},${height/2}`;
// Background path (wider, gray)
const bgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
bgPath.setAttribute('d', pathData);
bgPath.setAttribute('stroke', '#e5e7eb');
bgPath.setAttribute('stroke-width', '120');
bgPath.setAttribute('fill', 'none');
bgPath.setAttribute('stroke-linecap', 'round');
svg.appendChild(bgPath);
// Progress path (green)
const progressPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
progressPath.setAttribute('d', pathData);
progressPath.setAttribute('stroke', '#22c55e');
progressPath.setAttribute('stroke-width', '100');
progressPath.setAttribute('fill', 'none');
progressPath.setAttribute('stroke-linecap', 'round');
progressPath.setAttribute('stroke-dasharray', '1000');
progressPath.setAttribute('stroke-dashoffset', '1000');
svg.appendChild(progressPath);
// Start point with pulse animation
const startCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
startCircle.setAttribute('cx', '50');
startCircle.setAttribute('cy', height/2);
startCircle.setAttribute('r', '30');
startCircle.setAttribute('fill', '#ef4444');
startCircle.setAttribute('cursor', 'pointer');
startCircle.classList.add('pulse-circle');
svg.appendChild(startCircle);
// Pulse ring for extra visibility
const pulseRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
pulseRing.setAttribute('cx', '50');
pulseRing.setAttribute('cy', height/2);
pulseRing.setAttribute('r', '30');
pulseRing.setAttribute('fill', 'none');
pulseRing.setAttribute('stroke', '#ef4444');
pulseRing.setAttribute('stroke-width', '3');
svg.appendChild(pulseRing);
// Animate pulse ring
const animatePulseRing = () => {
pulseRing.innerHTML = '<animate attributeName="r" from="30" to="50" dur="1.5s" repeatCount="indefinite" /><animate attributeName="opacity" from="0.8" to="0" dur="1.5s" repeatCount="indefinite" />';
};
animatePulseRing();
// Add "Click here" text
const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
startText.setAttribute('x', '50');
startText.setAttribute('y', height/2 + 60);
startText.setAttribute('text-anchor', 'middle');
startText.setAttribute('fill', '#ef4444');
startText.setAttribute('font-size', '16');
startText.setAttribute('font-weight', 'bold');
startText.textContent = 'Clique aqui';
svg.appendChild(startText);
// Add click to start
startCircle.addEventListener('click', () => {
if (!started) {
started = true;
startCircle.setAttribute('fill', '#22c55e');
startCircle.classList.remove('pulse-circle');
pulseRing.remove(); // Remove o anel pulsante
startText.remove(); // Remove o texto
notify('running', { progress: 0 });
}
});
// End point
const endCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
endCircle.setAttribute('cx', width - 50);
endCircle.setAttribute('cy', height/2);
endCircle.setAttribute('r', '30');
endCircle.setAttribute('fill', '#22c55e');
svg.appendChild(endCircle);
// Track mouse movement
svg.addEventListener('mousemove', (e) => {
if (!started) return; // Só processa se a atividade foi iniciada
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check if on path (simplified: check distance from path)
const pathLength = bgPath.getTotalLength();
let minDist = Infinity;
for (let i = 0; i < pathLength; i += 10) {
const point = bgPath.getPointAtLength(i);
const dist = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
if (dist < minDist) minDist = dist;
}
if (minDist < 60) {
// On path
const progressPercent = (x / width) * 100;
if (progressPercent > progress) {
progress = progressPercent;
const offset = 1000 - (progress / 100) * 1000;
progressPath.setAttribute('stroke-dashoffset', offset);
notify('running', { progress: Math.floor(progress) });
if (progress >= TARGET_PROGRESS) {
finishActivity();
}
}
}
});
function finishActivity() {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Controle</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
#path { cursor: pointer; }
@keyframes pulse {
0%, 100% {
r: 30;
opacity: 1;
}
50% {
r: 35;
opacity: 0.8;
}
}
.pulse-circle {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse-ring {
0% {
r: 30;
opacity: 0.8;
}
100% {
r: 50;
opacity: 0;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="navigation" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique no círculo vermelho e siga o caminho</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden flex items-center justify-center">
<svg id="pathSvg" class="w-full h-full"></svg>
</div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você tem ótimo controle do mouse!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const banner = document.getElementById('successBanner');
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
let drawing = false;
let coverage = 0;
const TARGET_COVERAGE = 60;
const circleSegments = new Set(); // Track which parts of the circle were drawn
notify('started');
// Draw guide path (dotted circle)
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(canvas.width, canvas.height) * 0.3;
ctx.setLineDash([10, 10]);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
// User drawing
let points = [];
canvas.addEventListener('mousedown', (e) => {
drawing = true;
const rect = canvas.getBoundingClientRect();
points = [{ x: e.clientX - rect.left, y: e.clientY - rect.top }];
});
canvas.addEventListener('mousemove', (e) => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
points.push({ x, y });
// Draw line
ctx.strokeStyle = '#22c55e';
ctx.lineWidth = 8;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
ctx.lineTo(x, y);
ctx.stroke();
// Check if close to guide circle and mark segment as covered
const dist = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
if (Math.abs(dist - radius) < 30) {
// Calculate angle (0-360) to determine which segment of the circle
const angle = Math.atan2(y - centerY, x - centerX);
const degrees = ((angle * 180 / Math.PI) + 360) % 360;
const segment = Math.floor(degrees / 3); // Divide circle in 120 segments (360/3)
if (!circleSegments.has(segment)) {
circleSegments.add(segment);
coverage = (circleSegments.size / 120) * 100;
notify('running', { coverage: Math.floor(coverage) });
if (coverage >= TARGET_COVERAGE) {
finishActivity();
}
}
}
});
canvas.addEventListener('mouseup', () => {
drawing = false;
});
canvas.addEventListener('mouseleave', () => {
drawing = false;
});
function finishActivity() {
drawing = false;
canvas.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Desenhar</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
#canvas { cursor: crosshair; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="pencil" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Desenhe seguindo a linha pontilhada</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<canvas id="canvas" class="w-full h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner"></canvas>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você completou o traçado!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const banner = document.getElementById('successBanner');
let completed = 0;
const TOTAL_TARGETS = 5;
notify('started');
function placeTarget() {
const SIZE = 80;
const maxX = arena.clientWidth - SIZE - 20;
const maxY = arena.clientHeight - SIZE - 20;
const x = Math.floor(Math.random() * maxX) + 10;
const y = Math.floor(Math.random() * maxY) + 10;
const target = document.createElement('button');
target.className = 'absolute w-16 h-16 bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-lg hover:scale-110 transition-transform';
target.style.width = `${SIZE}px`;
target.style.height = `${SIZE}px`;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
const bullseye = document.createElement('div');
bullseye.className = 'absolute inset-0 flex items-center justify-center';
bullseye.innerHTML = '<div class="w-6 h-6 bg-white rounded-full"></div>';
target.appendChild(bullseye);
arena.appendChild(target);
target.addEventListener('click', () => {
target.remove();
completed++;
notify('running', { step: completed });
if (completed < TOTAL_TARGETS) {
setTimeout(() => placeTarget(), 300);
} else {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}
});
}
placeTarget();

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Precisão</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
@keyframes shrink { 0% { transform: scale(1); } 100% { transform: scale(0.5); } }
.shrinking { animation: shrink 5s linear infinite; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="target" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique no centro dos alvos</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Sua precisão está excelente!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,70 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const banner = document.getElementById('successBanner');
let currentTarget = 1;
const TOTAL_TARGETS = 5;
const targets = [];
notify('started');
function createTargets() {
for (let i = 1; i <= TOTAL_TARGETS; i++) {
const SIZE = 120;
const maxX = arena.clientWidth - SIZE - 20;
const maxY = arena.clientHeight - SIZE - 20;
const x = Math.floor(Math.random() * maxX) + 10;
const y = Math.floor(Math.random() * maxY) + 10;
const button = document.createElement('button');
button.className = 'absolute bg-gradient-to-br from-gray-400 to-gray-500 rounded-full shadow-lg transition-all text-white font-bold text-4xl';
button.style.width = `${SIZE}px`;
button.style.height = `${SIZE}px`;
button.style.left = `${x}px`;
button.style.top = `${y}px`;
button.textContent = i;
button.dataset.number = i;
arena.appendChild(button);
targets.push(button);
button.addEventListener('click', () => handleClick(parseInt(button.dataset.number)));
}
updateActiveTarget();
}
function updateActiveTarget() {
targets.forEach((btn) => {
const num = parseInt(btn.dataset.number);
if (num === currentTarget) {
btn.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-2xl text-white font-bold text-4xl pulse-active';
} else if (num < currentTarget) {
btn.className = 'absolute bg-gradient-to-br from-green-400 to-green-500 rounded-full shadow-lg text-white font-bold text-4xl opacity-50';
}
});
}
function handleClick(num) {
if (num === currentTarget) {
notify('running', { step: currentTarget });
currentTarget++;
if (currentTarget > TOTAL_TARGETS) {
arena.classList.add('hidden');
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
updateActiveTarget();
}
}
}
createTargets();

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Sequência</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
.pulse-active { animation: pulse 1s ease-in-out infinite; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="list-ordered" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique na ordem: 1 → 2 → 3 → 4 → 5</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700">Você acertou a sequência!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,80 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const arena = document.getElementById('arena');
const banner = document.getElementById('successBanner');
const finalScoreEl = document.getElementById('finalScore');
let score = 0;
let timeLeft = 30;
let gameActive = true;
// Criar timer na cena
const timerEl = document.createElement('div');
timerEl.className = 'absolute top-4 left-1/2 -translate-x-1/2 text-4xl font-bold text-red-600 bg-white px-6 py-2 rounded-full shadow-xl';
timerEl.textContent = `${timeLeft}s`;
arena.appendChild(timerEl);
// Criar contador de score na cena
const scoreEl = document.createElement('div');
scoreEl.className = 'absolute top-4 right-4 text-2xl font-bold text-green-600 bg-white px-4 py-2 rounded-full shadow-xl';
scoreEl.textContent = `0 alvos`;
arena.appendChild(scoreEl);
notify('started');
const spawnInterval = setInterval(() => {
if (gameActive) spawnTarget();
}, 800);
const countdown = setInterval(() => {
timeLeft--;
timerEl.textContent = `${timeLeft}s`;
if (timeLeft <= 0) {
endGame();
}
}, 1000);
function spawnTarget() {
const SIZE = 100;
const maxX = arena.clientWidth - SIZE - 20;
const maxY = arena.clientHeight - SIZE - 20;
const x = Math.floor(Math.random() * maxX) + 10;
const y = Math.floor(Math.random() * maxY) + 10;
const target = document.createElement('button');
target.className = 'absolute bg-gradient-to-br from-red-500 to-red-600 rounded-full shadow-2xl fading';
target.style.width = `${SIZE}px`;
target.style.height = `${SIZE}px`;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
arena.appendChild(target);
target.addEventListener('click', () => {
target.remove();
score++;
scoreEl.textContent = `${score} alvos`;
notify('running', { score });
});
setTimeout(() => target.remove(), 1500);
}
function endGame() {
gameActive = false;
clearInterval(spawnInterval);
clearInterval(countdown);
arena.classList.add('hidden');
finalScoreEl.textContent = `Você clicou ${score} alvos em 30 segundos!`;
banner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score });
notify('completed', { score });
}

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mouse - Velocidade</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
@keyframes fadeOut { to { opacity: 0; transform: scale(0); } }
.fading { animation: fadeOut 1.5s linear forwards; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-white/20">
<i data-lucide="zap" class="w-12 h-12 text-red-600"></i>
<h2 class="text-2xl font-bold text-gray-800">Clique rápido antes que desapareçam!</h2>
</div>
<div class="flex-1 p-6 overflow-hidden">
<div id="arena" class="relative h-full bg-white/50 rounded-xl border-2 border-gray-200 shadow-inner overflow-hidden"></div>
<div id="successBanner" class="hidden mt-6 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-8 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-16 h-16"></i>
<div>
<h2 class="text-4xl font-bold text-green-700 mb-2">Muito bem!</h2>
<p class="text-xl text-gray-700" id="finalScore"></p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,152 @@
export const MOUSE_ATIVIDADES_REGISTRY = {
'mouse-basico': {
id: 'mouse-basico',
titulo: 'Mouse Básico',
descricao: 'Aprenda a mover, clicar e fazer duplo clique.',
categoria: 'mouse',
dificuldade: 'iniciante',
duracao: 3,
htmlFile: '/atividades/letramento/mouse/mouse-basico/index.html',
proxima: 'mouse-precisao',
passos: [
{ id: 1, label: 'Mover' },
{ id: 2, label: 'Clicar' },
{ id: 3, label: 'Duplo clique' },
],
},
'mouse-precisao': {
id: 'mouse-precisao',
titulo: 'Precisão com Mouse',
descricao: 'Clique no centro de alvos que diminuem de tamanho.',
categoria: 'mouse',
dificuldade: 'iniciante',
duracao: 3,
htmlFile: '/atividades/letramento/mouse/mouse-precisao/index.html',
proxima: 'mouse-clique-multiplo',
passos: [
{ id: 1, label: 'Alvo 1' },
{ id: 2, label: 'Alvo 2' },
{ id: 3, label: 'Alvo 3' },
{ id: 4, label: 'Alvo 4' },
{ id: 5, label: 'Alvo 5' },
],
},
// 'mouse-controle': {
// id: 'mouse-controle',
// titulo: 'Controle do Mouse',
// descricao: 'Siga o caminho sem sair da linha.',
// categoria: 'mouse',
// dificuldade: 'iniciante',
// duracao: 3,
// htmlFile: '/atividades/letramento/mouse/mouse-controle/index.html',
// proxima: 'mouse-clique-multiplo',
// passos: [
// { id: 1, label: 'Seguir caminho' },
// ],
// },
'mouse-clique-multiplo': {
id: 'mouse-clique-multiplo',
titulo: 'Múltiplos Cliques',
descricao: 'Clique em vários alvos que aparecem ao mesmo tempo.',
categoria: 'mouse',
dificuldade: 'iniciante',
duracao: 2,
htmlFile: '/atividades/letramento/mouse/mouse-clique-multiplo/index.html',
proxima: 'mouse-sequencia',
passos: [
{ id: 1, label: 'Clicar todos' },
],
},
'mouse-sequencia': {
id: 'mouse-sequencia',
titulo: 'Sequência Numérica',
descricao: 'Clique nos números na ordem correta: 1, 2, 3...',
categoria: 'mouse',
dificuldade: 'intermediario',
duracao: 3,
htmlFile: '/atividades/letramento/mouse/mouse-sequencia/index.html',
proxima: 'mouse-velocidade',
passos: [
{ id: 1, label: '1' },
{ id: 2, label: '2' },
{ id: 3, label: '3' },
{ id: 4, label: '4' },
{ id: 5, label: '5' },
],
},
'mouse-velocidade': {
id: 'mouse-velocidade',
titulo: 'Velocidade e Reflexo',
descricao: 'Clique o máximo de alvos em 30 segundos.',
categoria: 'mouse',
dificuldade: 'intermediario',
duracao: 2,
htmlFile: '/atividades/letramento/mouse/mouse-velocidade/index.html',
proxima: 'mouse-botao-direito',
passos: [
{ id: 1, label: 'Desafio' },
],
},
'mouse-botao-direito': {
id: 'mouse-botao-direito',
titulo: 'Botão Direito',
descricao: 'Aprenda a usar o botão direito e menu de contexto.',
categoria: 'mouse',
dificuldade: 'intermediario',
duracao: 2,
htmlFile: '/atividades/letramento/mouse/mouse-botao-direito/index.html',
proxima: 'mouse-arrastar',
passos: [
{ id: 1, label: 'Contexto' },
{ id: 2, label: 'Selecionar' },
],
},
'mouse-arrastar': {
id: 'mouse-arrastar',
titulo: 'Arrastar e Soltar',
descricao: 'Aprenda a arrastar arquivos para uma pasta.',
categoria: 'mouse',
dificuldade: 'intermediario',
duracao: 3,
htmlFile: '/atividades/letramento/mouse/mouse-arrastar/index.html',
proxima: 'mouse-desenhar',
passos: [
{ id: 1, label: 'Arquivo 1' },
{ id: 2, label: 'Arquivo 2' },
{ id: 3, label: 'Arquivo 3' },
],
},
'mouse-desenhar': {
id: 'mouse-desenhar',
titulo: 'Desenhar Traçados',
descricao: 'Siga a linha pontilhada desenhando com o mouse.',
categoria: 'mouse',
dificuldade: 'avancado',
duracao: 4,
htmlFile: '/atividades/letramento/mouse/mouse-desenhar/index.html',
proxima: 'mouse-completo',
passos: [
{ id: 1, label: 'Traçar círculo' },
],
},
'mouse-completo': {
id: 'mouse-completo',
titulo: 'Desafio Completo',
descricao: 'Combinação de todas as habilidades do mouse.',
categoria: 'mouse',
dificuldade: 'avancado',
duracao: 5,
htmlFile: '/atividades/letramento/mouse/mouse-completo/index.html',
proxima: null,
passos: [
{ id: 1, label: 'Precisão' },
{ id: 2, label: 'Sequência' },
{ id: 3, label: 'Trilha' },
{ id: 4, label: 'Múltiplos' },
{ id: 5, label: 'Contexto' },
{ id: 6, label: 'Organizar' },
{ id: 7, label: 'Desenhar' },
{ id: 8, label: 'Final' },
],
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,86 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const challenges = [
{ type: 'key', lucideIcon: 'keyboard', title: 'Etapa 1', target: 'G', hint: 'Encontre e pressione G', check: e => e.key.toUpperCase() === 'G' },
{ type: 'number', lucideIcon: 'hash', title: 'Etapa 2', target: '5', hint: 'Encontre e pressione 5', check: e => e.key === '5' },
{ type: 'upper', lucideIcon: 'arrow-big-up', title: 'Etapa 3', target: 'T', hint: 'Use Shift + T', check: e => e.key === 'T' && e.shiftKey },
{ type: 'symbol', lucideIcon: 'at-sign', title: 'Etapa 4', target: '@', hint: 'Use Shift + 2', check: e => e.key === '@' },
{ type: 'arrow', lucideIcon: 'move-vertical', title: 'Etapa 5', target: '↓', hint: 'Pressione a seta para baixo', check: e => e.key === 'ArrowDown' },
{ type: 'enter', lucideIcon: 'corner-down-left', title: 'Etapa 6', target: 'Enter', hint: 'Pressione Enter', check: e => e.key === 'Enter' },
{ type: 'esc', lucideIcon: 'x-circle', title: 'Etapa 7', target: 'Esc', hint: 'Pressione Esc', check: e => e.key === 'Escape' },
{ type: 'key', lucideIcon: 'trophy', title: 'Etapa 8', target: 'Z', hint: 'Pressione Z para concluir', check: e => e.key.toUpperCase() === 'Z' },
];
let idx = 0;
let locked = false;
notify('started');
const challengeIcon = document.getElementById('challengeIcon');
const challengeTitle = document.getElementById('challengeTitle');
const challengeTarget = document.getElementById('challengeTarget');
const challengeHint = document.getElementById('challengeHint');
const feedbackEl = document.getElementById('feedback');
const progressFill = document.getElementById('progressFill');
const progressLabel = document.getElementById('progressLabel');
const successBanner = document.getElementById('successBanner');
const challengeCard = document.getElementById('challengeCard');
const bgMap = {
key: 'bg-fuchsia-50 border-fuchsia-300 text-fuchsia-700',
upper: 'bg-orange-50 border-orange-300 text-orange-700',
symbol: 'bg-amber-50 border-amber-300 text-amber-700',
arrow: 'bg-teal-50 border-teal-300 text-teal-700',
enter: 'bg-blue-50 border-blue-300 text-blue-700',
esc: 'bg-gray-50 border-gray-300 text-gray-700',
number: 'bg-green-50 border-green-300 text-green-700',
};
function showChallenge() {
locked = false;
const c = challenges[idx];
// challengeIcon.innerHTML = `<i data-lucide="${c.lucideIcon}" class="w-16 h-16"></i>`;
// lucide.createIcons();
challengeTitle.textContent = c.title;
const bg = bgMap[c.type] || bgMap.key;
challengeTarget.className = `text-[6rem] font-black rounded-2xl px-8 py-4 border-4 text-center leading-none ${bg}`;
challengeTarget.textContent = c.target;
challengeHint.textContent = c.hint;
feedbackEl.classList.add('hidden');
progressFill.style.width = `${(idx / challenges.length) * 100}%`;
progressLabel.textContent = `Etapa ${idx} de ${challenges.length}`;
}
showChallenge();
document.addEventListener('keydown', (e) => {
if (locked) return;
const c = challenges[idx];
if (c.check(e)) {
e.preventDefault();
locked = true;
feedbackEl.textContent = 'Correto!';
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
feedbackEl.classList.remove('hidden');
idx++;
notify('running', { step: idx });
progressFill.style.width = `${(idx / challenges.length) * 100}%`;
progressLabel.textContent = `Etapa ${idx} de ${challenges.length}`;
setTimeout(() => {
locked = false;
if (idx >= challenges.length) {
challengeCard.classList.add('hidden');
progressFill.parentElement.parentElement.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showChallenge();
}
}, 700);
}
if (e.key === 'Enter' || e.key === 'Escape') e.preventDefault();
});

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Atividade Final</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }
.challenge-badge { transition: all 0.2s; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="trophy" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Atividade Final</h2>
<p class="text-lg text-gray-600">Complete todas as etapas e integre habilidades do teclado.</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
<div id="challengeCard" class="flex flex-col items-center gap-3 w-full max-w-md">
<span class="text-6xl" id="challengeIcon"></span>
<p class="text-2xl font-bold text-gray-700 text-center" id="challengeTitle"></p>
<div id="challengeTarget" class="text-[6rem] font-black rounded-2xl px-8 py-4 border-4 text-center bg-fuchsia-50 border-fuchsia-300 text-fuchsia-700 leading-none"></div>
<p class="text-base text-gray-500 text-center" id="challengeHint"></p>
</div>
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
<div class="w-full max-w-xs">
<div class="h-3 bg-gray-200 rounded-full">
<div id="progressFill" class="h-full bg-fuchsia-500 rounded-full transition-all" style="width:0%"></div>
</div>
<p class="text-center text-gray-500 text-sm mt-1" id="progressLabel">Etapa 0 de 8</p>
</div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="trophy" class="text-yellow-500 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Parabéns!</h2>
<p class="text-lg text-gray-700">Você concluiu a Atividade Final do teclado!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,331 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const SINGLE_CHARACTER_POOL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-=+_'.split('');
const TOKEN_POOLS_BY_LENGTH = {
2: [
'ab', 'cd', 'ef', 'hj', 'mn', 'pq', '44', '17', '29', '80',
'GG', 'HJ', 'KL', 'MN', 'PR', 'TV', 'WW', 'XX', 'YY', 'ZZ',
],
3: [
'sol', 'mar', 'ceu', 'rio', 'lua', 'dia', 'som', 'mel', 'paz', 'voz',
'bom', 'fim', 'cor', 'lar', 'giz', 'asa', 'luz', 'rei', 'nuv', 'fio',
],
4: [
'casa', 'mesa', 'jogo', 'foco', 'dado', 'bola', 'pato', 'gato', 'rede', 'nave',
'copo', 'lago', 'tela', 'nota', 'rima', 'sapo', 'coro', 'vela', 'pulo', 'pipa',
],
};
const STAGES = [
{ goal: 5, speedMultiplier: 1, mode: 'single' },
{ goal: 5, speedMultiplier: 1.45, mode: 'single' },
{ goal: 5, speedMultiplier: 1.75, mode: 'single' },
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 2 },
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 3 },
{ goal: 5, speedMultiplier: 1, mode: 'token', tokenLength: 4 },
];
let stageIndex = 0;
let score = 0;
let gameActive = true;
let falling = [];
let spawnTimer = null;
let totalInputs = 0;
let correctInputs = 0;
let inputBuffer = '';
let tokenCycle = [];
notify('started');
const arena = document.getElementById('arena');
const scoreEl = document.getElementById('score');
const goalEl = document.getElementById('goal');
const stageLabelEl = document.getElementById('stageLabel');
const accuracyLabel = document.getElementById('accuracyLabel');
const progressFill = document.getElementById('progressFill');
const feedbackEl = document.getElementById('feedback');
const successBanner = document.getElementById('successBanner');
const gameoverBanner = document.getElementById('gameoverBanner');
const gameoverStageLabel = document.getElementById('gameoverStageLabel');
const retryStageBtn = document.getElementById('retryStageBtn');
const COLORS = [
['bg-cyan-500 text-white border-cyan-300', 'cyan'],
['bg-violet-500 text-white border-violet-300', 'violet'],
['bg-orange-500 text-white border-orange-300', 'orange'],
['bg-pink-500 text-white border-pink-300', 'pink'],
];
function getCurrentStage() {
return STAGES[stageIndex];
}
function getAccuracy() {
if (totalInputs === 0) return 100;
return (correctInputs / totalInputs) * 100;
}
function updateStageUi() {
const stage = getCurrentStage();
scoreEl.textContent = score;
goalEl.textContent = stage.goal;
stageLabelEl.textContent = `${stageIndex + 1} de ${STAGES.length}`;
accuracyLabel.textContent = `${Math.round(getAccuracy())}%`;
progressFill.style.width = `${(score / stage.goal) * 100}%`;
}
function showFeedback(message, kind) {
feedbackEl.textContent = message;
feedbackEl.className = `text-2xl font-bold py-1 text-center ${kind === 'error' ? 'text-red-500' : 'text-green-600'}`;
feedbackEl.classList.remove('hidden');
}
function clearFeedback() {
feedbackEl.classList.add('hidden');
}
function restartCurrentStage() {
gameActive = true;
score = 0;
totalInputs = 0;
correctInputs = 0;
inputBuffer = '';
tokenCycle = [];
falling.forEach((item) => item.el.remove());
falling = [];
lastTime = null;
gameoverBanner.classList.add('hidden');
clearFeedback();
updateStageUi();
startSpawnLoop();
spawnCharacter();
requestAnimationFrame(gameLoop);
}
function finishFailure() {
gameActive = false;
if (spawnTimer) clearInterval(spawnTimer);
arena.innerHTML = '';
falling = [];
gameoverStageLabel.textContent = `Etapa ${stageIndex + 1}: você não atingiu 60% de precisão.`;
gameoverBanner.classList.remove('hidden');
lucide.createIcons();
// Não notifica 'failure' ao host para evitar que o modal externo cause reload do iframe.
// O retry é gerenciado internamente pelo botão desta tela.
}
function finishSuccess() {
gameActive = false;
if (spawnTimer) clearInterval(spawnTimer);
arena.innerHTML = '';
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}
function shuffle(list) {
const copy = [...list];
for (let index = copy.length - 1; index > 0; index -= 1) {
const swapIndex = Math.floor(Math.random() * (index + 1));
[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]];
}
return copy;
}
function refillTokenCycle(length) {
const pool = TOKEN_POOLS_BY_LENGTH[length] ?? [];
tokenCycle = shuffle(pool);
}
function pickRandomToken(length) {
if (tokenCycle.length === 0) {
refillTokenCycle(length);
}
return tokenCycle.pop();
}
function makeEntityForStage(stage) {
if (stage.mode === 'single') {
return SINGLE_CHARACTER_POOL[Math.floor(Math.random() * SINGLE_CHARACTER_POOL.length)];
}
return pickRandomToken(stage.tokenLength);
}
function getEntityFontClass(entity) {
if (entity.length >= 4) return 'text-xl';
if (entity.length === 3) return 'text-2xl';
return 'text-3xl';
}
function evaluateStageAndAdvance() {
const accuracy = getAccuracy();
if (accuracy < 60) {
showFeedback('Precisão abaixo de 60%. Tente novamente.', 'error');
finishFailure();
return;
}
stageIndex += 1;
score = 0;
falling.forEach((item) => item.el.remove());
falling = [];
inputBuffer = '';
tokenCycle = [];
if (stageIndex >= STAGES.length) {
finishSuccess();
return;
}
updateStageUi();
showFeedback(`Etapa ${stageIndex + 1} liberada!`, 'success');
notify('running', { step: stageIndex + 1 });
spawnCharacter();
}
function spawnCharacter() {
if (!gameActive) return;
const stage = getCurrentStage();
const character = makeEntityForStage(stage);
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
const arenaW = arena.clientWidth - 140;
const x = Math.floor(Math.random() * arenaW);
const id = `fall-${Date.now()}-${Math.random()}`;
const speed = (60 + Math.random() * 40) * stage.speedMultiplier;
const el = document.createElement('div');
el.id = id;
el.className = `falling-char border-2 ${getEntityFontClass(character)} ${color[0]}`;
el.textContent = character;
el.style.left = `${x}px`;
el.style.top = '0px';
arena.appendChild(el);
const item = { id, character, el, top: 0, speed };
falling.push(item);
return item;
}
let lastTime = null;
function gameLoop(timestamp) {
if (!gameActive) return;
if (!lastTime) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
const arenaH = arena.clientHeight;
const stage = getCurrentStage();
falling = falling.filter(item => {
item.top += item.speed * dt;
item.el.style.top = `${item.top}px`;
if (item.top + 64 >= arenaH) {
item.el.remove();
totalInputs += 1;
updateStageUi();
showFeedback(stage.mode === 'token' ? 'Uma combinação caiu. Tente a próxima.' : 'Um caractere caiu. Tente o próximo.', 'error');
return false;
}
return true;
});
requestAnimationFrame(gameLoop);
}
document.addEventListener('keydown', (e) => {
if (!gameActive) return;
if (e.key.length !== 1) return;
const stage = getCurrentStage();
const key = e.key;
if (stage.mode === 'single') {
totalInputs += 1;
const idx = falling.findIndex((item) => item.character === key);
if (idx !== -1) {
const item = falling[idx];
item.el.style.background = '#22c55e';
item.el.style.color = 'white';
item.el.style.transform = 'scale(0.92)';
setTimeout(() => item.el.remove(), 200);
falling.splice(idx, 1);
score += 1;
correctInputs += 1;
updateStageUi();
clearFeedback();
notify('running', { step: stageIndex + 1 });
if (score >= stage.goal) {
evaluateStageAndAdvance();
}
return;
}
updateStageUi();
showFeedback('Esse não era o caractere da tela.', 'error');
return;
}
inputBuffer += key;
const tokenLength = stage.tokenLength ?? 2;
if (inputBuffer.length < tokenLength) {
return;
}
const attempt = inputBuffer.slice(-tokenLength);
inputBuffer = '';
totalInputs += 1;
const idx = falling.findIndex((item) => item.character === attempt);
if (idx !== -1) {
const item = falling[idx];
item.el.style.background = '#22c55e';
item.el.style.color = 'white';
item.el.style.transform = 'scale(0.92)';
setTimeout(() => item.el.remove(), 200);
falling.splice(idx, 1);
score += 1;
correctInputs += 1;
updateStageUi();
clearFeedback();
notify('running', { step: stageIndex + 1 });
if (score >= stage.goal) {
evaluateStageAndAdvance();
}
} else {
updateStageUi();
showFeedback(`Combinação incorreta: ${attempt}`, 'error');
}
});
function startSpawnLoop() {
if (spawnTimer) clearInterval(spawnTimer);
spawnTimer = setInterval(() => {
if (!gameActive) {
clearInterval(spawnTimer);
return;
}
if (falling.length < 5) spawnCharacter();
}, 1500);
}
retryStageBtn.addEventListener('click', restartCurrentStage);
updateStageUi();
startSpawnLoop();
spawnCharacter();
document.addEventListener('visibilitychange', () => {
if (!gameActive) { clearInterval(spawnTimer); return; }
if (!document.hidden && falling.length === 0) {
spawnCharacter();
}
});
requestAnimationFrame(gameLoop);

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chuva de Letras</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
.falling-char {
position: absolute;
min-width: 64px;
height: 64px;
border-radius: 16px;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.7rem;
font-weight: 900;
user-select: none;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.18);
transition: transform 0.12s ease, opacity 0.12s ease, background 0.12s ease;
}
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="cloud-rain" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Chuva de Letras</h2>
<p class="text-lg text-gray-600">Digite exatamente o alvo da tela. A atividade é sensível a maiúsculas e minúsculas.</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="rounded-xl bg-sky-50 border border-sky-200 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-sky-700">Etapa</p>
<p id="stageLabel" class="mt-1 text-xl font-black text-slate-800">1 de 6</p>
</div>
<div class="rounded-xl bg-violet-50 border border-violet-200 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-violet-700">Meta da Etapa</p>
<p class="mt-1 text-xl font-black text-slate-800"><span id="score">0</span>/<span id="goal">10</span></p>
</div>
<div class="rounded-xl bg-emerald-50 border border-emerald-200 px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-emerald-700">Precisão</p>
<p id="accuracyLabel" class="mt-1 text-xl font-black text-slate-800">100%</p>
</div>
</div>
<div class="w-full h-4 bg-gray-200 rounded-full overflow-hidden shadow-inner">
<div id="progressFill" class="h-full bg-sky-500 transition-all duration-300 rounded-full" style="width: 0%"></div>
</div>
<div id="arena" class="flex-1 relative bg-white/60 rounded-xl overflow-hidden border-2 border-gray-200 shadow-inner"></div>
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Você venceu!</h2>
<p class="text-lg text-gray-700">Você concluiu a bateria completa da chuva de caracteres.</p>
</div>
</div>
<div id="gameoverBanner" class="hidden bg-gradient-to-r from-red-50 to-rose-50 border-2 border-red-300 rounded-xl p-6 flex flex-col items-center justify-center gap-4">
<div class="flex items-center gap-4">
<i data-lucide="x-circle" class="text-red-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-red-700 mb-1">Precisão insuficiente!</h2>
<p id="gameoverStageLabel" class="text-lg text-gray-700">Você não atingiu 60% nesta etapa.</p>
</div>
</div>
<button id="retryStageBtn" class="mt-2 px-6 py-3 rounded-xl bg-red-500 hover:bg-red-600 text-white font-bold text-lg shadow transition">
Tentar esta etapa novamente
</button>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const dialogs = [
{
title: 'Salvar arquivo',
question: 'Deseja salvar o arquivo?',
instruction: 'Pressione Enter para SIM (salvar) ou Esc para NÃO (cancelar)',
expected: 'Enter',
feedback: 'Arquivo salvo! Enter confirma!',
},
{
title: 'Fechar janela',
question: 'Tem certeza que quer fechar?',
instruction: 'Pressione Esc para fechar (cancelar a ação)',
expected: 'Escape',
feedback: 'Janela fechada! Esc cancela!',
},
{
title: 'Enviar mensagem',
question: 'Enviar esta mensagem?',
instruction: 'Pressione Enter para confirmar o envio',
expected: 'Enter',
feedback: 'Mensagem enviada! Enter confirma!',
},
{
title: 'Descartar rascunho',
question: 'Apagar o rascunho sem salvar?',
instruction: 'Pressione Esc para cancelar (não apagar)',
expected: 'Escape',
feedback: 'Rascunho mantido! Esc cancela a ação!',
},
];
let currentDialog = 0;
notify('started');
const dialogTitle = document.getElementById('dialogTitle');
const dialogQuestion = document.getElementById('dialogQuestion');
const dialogInstruction = document.getElementById('dialogInstruction');
const feedbackEl = document.getElementById('feedback');
const progressDots = document.getElementById('progressDots');
const successBanner = document.getElementById('successBanner');
const dialogBox = document.getElementById('dialogBox');
let locked = false;
dialogs.forEach((_, i) => {
const dot = document.createElement('div');
dot.id = `dot-${i}`;
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
progressDots.appendChild(dot);
});
function showDialog() {
const d = dialogs[currentDialog];
dialogTitle.textContent = d.title;
dialogQuestion.textContent = d.question;
dialogInstruction.textContent = d.instruction;
feedbackEl.classList.add('hidden');
locked = false;
}
showDialog();
document.addEventListener('keydown', (e) => {
if (locked) return;
if (e.key !== 'Enter' && e.key !== 'Escape') return;
const d = dialogs[currentDialog];
if (e.key === d.expected) {
locked = true;
feedbackEl.textContent = d.feedback;
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
feedbackEl.classList.remove('hidden');
const dot = document.getElementById(`dot-${currentDialog}`);
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
notify('running', { step: currentDialog + 1 });
currentDialog++;
setTimeout(() => {
if (currentDialog >= dialogs.length) {
dialogBox.classList.add('hidden');
progressDots.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showDialog();
}
}, 1000);
} else {
feedbackEl.textContent = e.key === 'Enter' ? 'Aqui não é Enter! Use Esc.' : 'Aqui não é Esc! Use Enter.';
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-red-500';
feedbackEl.classList.remove('hidden');
}
});

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confirmar e Cancelar</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-rose-50 to-pink-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="check-square" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Confirmar e Cancelar</h2>
<p class="text-lg text-gray-600">Enter confirma. Esc cancela. Aprenda a diferença!</p>
</div>
</div>
<div class="flex-1 p-6 flex flex-col gap-4 justify-center items-center overflow-hidden">
<div class="grid grid-cols-2 gap-4 max-w-lg w-full">
<div class="bg-green-50 border-2 border-green-300 rounded-2xl p-4 text-center">
<kbd class="text-3xl font-black text-green-700 block">Enter ↵</kbd>
<p class="text-sm text-gray-600 mt-1">Confirma / Ok / Envia</p>
</div>
<div class="bg-red-50 border-2 border-red-300 rounded-2xl p-4 text-center">
<kbd class="text-3xl font-black text-red-700 block">Esc</kbd>
<p class="text-sm text-gray-600 mt-1">Cancela / Fecha / Não</p>
</div>
</div>
<div id="dialogBox" class="w-full max-w-lg border-2 border-gray-300 rounded-2xl shadow-xl overflow-hidden">
<div class="bg-gray-100 border-b border-gray-300 px-4 py-2 flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<span class="text-sm text-gray-600 ml-2" id="dialogTitle">Janela de diálogo</span>
</div>
<div class="p-6 text-center">
<p class="text-xl font-bold text-gray-700 mb-2" id="dialogQuestion"></p>
<p class="text-gray-500 text-base" id="dialogInstruction"></p>
</div>
</div>
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
<div class="flex gap-2 justify-center" id="progressDots"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
<p class="text-lg text-gray-700">Agora você sabe como confirmar e cancelar ações no computador!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,190 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const LEVELS = [
[
'#######',
'#S...E#',
'#.###.#',
'#.....#',
'#######',
],
[
'########',
'#S....k#',
'#.##D###',
'#..#..E#',
'########',
],
[
'#########',
'#S......#',
'#.###D###',
'#...#...#',
'#.#.###.#',
'#..k#..E#',
'#########',
],
[
'##########',
'#S..#....#',
'#.#.#.##D#',
'#.#...#..#',
'#.#####.##',
'#.....#..#',
'#.###k#E.#',
'##########',
],
[
'###########',
'#S...#...E#',
'#.#.#.#.###',
'#.#...#...#',
'#.#####.#.#',
'#.....#.#.#',
'###.#.###.#',
'#k..#.D...#',
'###########',
],
];
let levelIndex = 0;
let grid = [];
let pr = 0;
let pc = 0;
let hasKey = false;
let openedDoors = 0;
let levelHasDoor = false;
let done = false;
notify('started');
const mazeEl = document.getElementById('maze');
const feedbackEl = document.getElementById('feedback');
const successBanner = document.getElementById('successBanner');
const mazeInfo = document.getElementById('mazeInfo');
function loadLevel(index) {
grid = LEVELS[index].map((row) => row.split(''));
levelHasDoor = false;
for (let r = 0; r < grid.length; r += 1) {
for (let c = 0; c < grid[r].length; c += 1) {
if (grid[r][c] === 'S') {
pr = r;
pc = c;
}
if (grid[r][c] === 'D') {
levelHasDoor = true;
}
}
}
hasKey = false;
openedDoors = 0;
feedbackEl.classList.add('hidden');
mazeInfo.textContent = 'Use as setas para chegar à saída';
}
function buildMaze() {
mazeEl.innerHTML = '';
grid.forEach((row, r) => {
const rowEl = document.createElement('div');
rowEl.style.display = 'flex';
row.forEach((cell, c) => {
const el = document.createElement('div');
el.className = 'cell';
if (r === pr && c === pc) {
el.innerHTML = '<div style="width:36px;height:36px;border-radius:50%;background:#14b8a6;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">P</div>';
el.style.background = '#ecfdf5';
} else if (cell === '#') {
el.style.background = '#374151';
} else if (cell === 'E') {
el.innerHTML = '<div style="width:36px;height:36px;border-radius:8px;background:#22c55e;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">S</div>';
el.style.background = '#fef9c3';
} else if (cell === 'k') {
el.innerHTML = '<div style="width:32px;height:32px;border-radius:8px;background:#fef9c3;border:1px solid #fcd34d;display:flex;align-items:center;justify-content:center;"><i data-lucide="key-round" style="width:18px;height:18px;color:#a16207"></i></div>';
el.style.background = '#fffbeb';
} else if (cell === 'D') {
el.innerHTML = '<div style="width:32px;height:32px;border-radius:8px;background:#ffedd5;border:1px solid #fdba74;display:flex;align-items:center;justify-content:center;"><i data-lucide="lock" style="width:18px;height:18px;color:#b45309"></i></div>';
el.style.background = '#fffbeb';
} else {
el.style.background = '#f9fafb';
}
rowEl.appendChild(el);
});
mazeEl.appendChild(rowEl);
});
lucide.createIcons();
}
loadLevel(levelIndex);
buildMaze();
document.addEventListener('keydown', (e) => {
if (done) return;
const moves = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] };
if (!moves[e.key]) return;
e.preventDefault();
const [dr, dc] = moves[e.key];
const nr = pr + dr, nc = pc + dc;
if (nr < 0 || nr >= grid.length || nc < 0 || nc >= grid[0].length) return;
const cell = grid[nr][nc];
if (cell === '#') {
feedbackEl.textContent = 'Parede! Escolha outra direção.';
feedbackEl.className = 'text-xl font-bold py-1 text-center text-orange-500';
feedbackEl.classList.remove('hidden');
return;
}
if (cell === 'D' && !hasKey) {
feedbackEl.textContent = 'Porta trancada! Encontre a chave.';
feedbackEl.className = 'text-xl font-bold py-1 text-center text-amber-600';
feedbackEl.classList.remove('hidden');
return;
}
feedbackEl.classList.add('hidden');
if (cell === 'k') {
hasKey = true;
mazeInfo.textContent = 'Chave coletada! Agora abra a porta.';
grid[nr][nc] = '.';
}
if (cell === 'D' && hasKey) {
openedDoors += 1;
grid[nr][nc] = '.';
mazeInfo.textContent = 'Porta desbloqueada! Siga até a saída.';
}
if (cell === 'E' && levelHasDoor && openedDoors === 0) {
feedbackEl.textContent = 'A saída está bloqueada. Abra a porta primeiro.';
feedbackEl.className = 'text-xl font-bold py-1 text-center text-amber-600';
feedbackEl.classList.remove('hidden');
return;
}
pr = nr; pc = nc;
buildMaze();
notify('running', { step: levelIndex + 1 });
if (cell === 'E') {
if (levelIndex === LEVELS.length - 1) {
done = true;
mazeEl.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
return;
}
levelIndex += 1;
feedbackEl.textContent = `Fase ${levelIndex} concluída!`;
feedbackEl.className = 'text-xl font-bold py-1 text-center text-green-600';
feedbackEl.classList.remove('hidden');
notify('running', { step: levelIndex + 1 });
setTimeout(() => {
loadLevel(levelIndex);
buildMaze();
}, 700);
}
});

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Labirinto das Setas</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }
.cell { width: 42px; height: 42px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-size: 1.1rem; border: 2px solid #e5e7eb; }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="map" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Labirinto das Setas</h2>
<p class="text-lg text-gray-600">Complete 5 mapas com progressão de dificuldade, coletando chaves para abrir portas.</p>
</div>
</div>
<div class="flex-1 p-3 flex flex-col gap-2 justify-center items-center overflow-hidden">
<div class="w-full flex-1 flex items-center justify-center overflow-auto">
<div id="maze" class="flex flex-col gap-0 border-2 border-teal-400 rounded-lg overflow-hidden shadow-lg"></div>
</div>
<div class="flex gap-6 items-center flex-wrap justify-center">
<div class="flex items-center gap-2"><div style="width:36px;height:36px;border-radius:50%;background:#14b8a6;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">P</div><p class="text-gray-600">Você</p></div>
<div class="flex items-center gap-2"><div style="width:36px;height:36px;border-radius:8px;background:#22c55e;display:flex;align-items:center;justify-content:center;color:white;font-weight:900;font-size:1rem;">S</div><p class="text-gray-600">Saída</p></div>
<div class="flex items-center gap-2"><div class="w-6 h-6 bg-gray-700 rounded"></div><p class="text-gray-600">Parede</p></div>
<div class="flex items-center gap-2"><div class="w-8 h-8 rounded-md bg-yellow-100 border border-yellow-300 flex items-center justify-center"><i data-lucide="key-round" class="w-4 h-4 text-yellow-700"></i></div><p class="text-gray-600">Chave</p></div>
<div class="flex items-center gap-2"><div class="w-8 h-8 rounded-md bg-amber-100 border border-amber-300 flex items-center justify-center"><i data-lucide="lock" class="w-4 h-4 text-amber-700"></i></div><p class="text-gray-600">Porta</p></div>
</div>
<p class="text-gray-500 text-sm md:text-base" id="mazeInfo">Use as setas para chegar à saída</p>
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Labirintos concluídos!</h2>
<p class="text-lg text-gray-700">Você dominou setas, chaves e portas.</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
notify('started');
const messageArea = document.getElementById('messageArea');
const confirmBtn = document.getElementById('confirmBtn');
const successBanner = document.getElementById('successBanner');
const messagePreview = document.getElementById('messagePreview');
const reqWords = document.getElementById('req-words');
const reqUpper = document.getElementById('req-upper');
const reqPeriod = document.getElementById('req-period');
messageArea.focus();
function setReq(el, ok) {
el.className = ok
? 'px-4 py-2 rounded-full border-2 border-green-400 text-sm font-bold text-green-700 bg-green-50 transition-all'
: 'px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all';
}
messageArea.addEventListener('input', () => {
const v = messageArea.value.trim();
const words = v.split(/\s+/).filter(w => w.length > 0);
const hasWords = words.length >= 3;
const hasUpper = v.length > 0 && v[0] === v[0].toUpperCase() && /[A-Za-zÀ-ú]/.test(v[0]);
const hasPeriod = v.endsWith('.');
setReq(reqWords, hasWords);
setReq(reqUpper, hasUpper);
setReq(reqPeriod, hasPeriod);
if (hasWords && hasUpper && hasPeriod) {
confirmBtn.classList.remove('hidden');
notify('running', { step: 1 });
} else {
confirmBtn.classList.add('hidden');
}
});
confirmBtn.addEventListener('click', () => {
const v = messageArea.value.trim();
messagePreview.textContent = `"${v}"`;
messageArea.parentElement.classList.add('hidden');
document.getElementById('reqChecklist').classList.add('hidden');
confirmBtn.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
});

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Texto Coletivo</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-emerald-50 to-teal-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="mail" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Texto Coletivo</h2>
<p class="text-lg text-gray-600">Escreva uma mensagem sobre tecnologia, comunidade e soberania digital.</p>
</div>
</div>
<div class="flex-1 p-6 flex flex-col gap-4 overflow-hidden">
<div class="bg-emerald-50 border-2 border-emerald-200 rounded-2xl p-4 text-center">
<p class="text-lg text-emerald-800">
Escreva uma mensagem curta com: <strong>letra maiúscula no início</strong>, <strong>pelo menos 3 palavras</strong> e <strong>ponto final</strong>.
</p>
</div>
<div id="reqChecklist" class="flex gap-3 flex-wrap justify-center">
<div id="req-words" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Mínimo 3 palavras</div>
<div id="req-upper" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Começa com Maiúscula</div>
<div id="req-period" class="px-4 py-2 rounded-full border-2 border-gray-300 text-sm font-bold text-gray-600 transition-all">Termina com ponto</div>
</div>
<div class="flex-1 flex flex-col">
<textarea id="messageArea" rows="4" spellcheck="false" autocomplete="off"
class="flex-1 text-xl font-mono border-2 border-emerald-400 rounded-2xl p-4 resize-none outline-none focus:border-emerald-600 bg-white shadow text-gray-700"
placeholder="Exemplo: Tecnologia livre fortalece nossa comunidade."></textarea>
</div>
<button id="confirmBtn" class="hidden bg-emerald-500 hover:bg-emerald-600 text-white text-xl font-bold px-10 py-4 rounded-2xl shadow-lg transition-all active:scale-95 self-center">
Enviar Recado ✓
</button>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex flex-col items-center gap-3 w-full text-center">
<i data-lucide="mail-check" class="text-green-600 w-16 h-16"></i>
<h2 class="text-3xl font-bold text-green-700">Recado enviado!</h2>
<p id="messagePreview" class="text-xl text-gray-700 italic max-w-lg"></p>
<p class="text-lg text-gray-600">Você concluiu a etapa de escrita coletiva. Parabéns!</p>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,84 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const tasks = [
{
initial: 'Tecnologia livr fortalece comunidades',
expected: 'Tecnologia livre fortalece comunidades',
text: 'Falta uma letra no meio da palavra. Use as setas para posicionar o cursor e corrigir.',
hint: 'Mova com → até depois de "livr" e adicione "e".',
},
{
initial: 'Soberania digital protege os ddados da comunidade',
expected: 'Soberania digital protege os dados da comunidade',
text: 'Há uma letra repetida. Volte com as setas e corrija sem apagar tudo.',
hint: 'Use ← para voltar até "ddados" e deixe apenas "dados".',
},
];
let currentTask = 0;
notify('started');
const taskText = document.getElementById('taskText');
const taskHint = document.getElementById('taskHint');
const expectedText = document.getElementById('expectedText');
const editInput = document.getElementById('editInput');
const feedbackEl = document.getElementById('feedback');
const cursorPos = document.getElementById('cursorPos');
const progressDots = document.getElementById('progressDots');
const successBanner = document.getElementById('successBanner');
const taskBox = document.getElementById('taskBox');
tasks.forEach((_, i) => {
const dot = document.createElement('div');
dot.id = `dot-${i}`;
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
progressDots.appendChild(dot);
});
function showTask() {
const t = tasks[currentTask];
taskText.textContent = t.text;
taskHint.textContent = t.hint;
expectedText.textContent = t.expected;
editInput.value = t.initial;
editInput.setSelectionRange(0, 0);
feedbackEl.classList.add('hidden');
editInput.focus();
}
showTask();
editInput.addEventListener('keyup', () => {
cursorPos.textContent = editInput.selectionStart;
});
editInput.addEventListener('click', () => {
cursorPos.textContent = editInput.selectionStart;
});
editInput.addEventListener('input', () => {
const t = tasks[currentTask];
if (editInput.value === t.expected) {
feedbackEl.textContent = 'Correto!';
feedbackEl.className = 'text-2xl font-bold py-1 text-center text-green-600';
feedbackEl.classList.remove('hidden');
const dot = document.getElementById(`dot-${currentTask}`);
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
notify('running', { step: currentTask + 1 });
currentTask++;
setTimeout(() => {
if (currentTask >= tasks.length) {
taskBox.classList.add('hidden');
progressDots.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showTask();
}
}, 800);
}
});

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setas no Texto</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="move-horizontal" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Setas no Texto</h2>
<p class="text-lg text-gray-600">Use ← e → para corrigir frases sobre tecnologia e cidadania digital.</p>
</div>
</div>
<div class="flex-1 p-6 flex flex-col gap-4 justify-center items-center overflow-hidden">
<div class="bg-teal-50 border-2 border-teal-200 rounded-2xl p-4 max-w-lg text-center">
<p class="text-lg text-teal-800">
As setas <strong>← →</strong> movem o cursor <em>sem apagar nada</em>. Volte ao meio do texto para corrigir letras com precisão.
</p>
</div>
<div id="taskBox" class="w-full max-w-lg text-center">
<p class="text-xl font-bold text-gray-700" id="taskText"></p>
<p class="text-base text-gray-500 mt-1 mb-3" id="taskHint"></p>
<div class="bg-gray-50 rounded-xl border-2 border-gray-200 p-3 mb-3">
<p class="text-sm text-gray-400 mb-1">Resultado esperado:</p>
<p id="expectedText" class="text-2xl font-mono text-green-700 font-bold"></p>
</div>
<input id="editInput" type="text" autocomplete="off" spellcheck="false"
class="text-2xl font-mono text-center border-b-4 border-teal-400 bg-transparent outline-none w-full py-2 text-gray-700" />
<p class="text-sm text-gray-400 mt-1">Posição do cursor: <span id="cursorPos">0</span></p>
</div>
<div id="feedback" class="hidden text-2xl font-bold py-1 text-center"></div>
<div class="flex gap-2 justify-center" id="progressDots"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Parabéns!</h2>
<p class="text-lg text-gray-700">Você navegou no texto usando apenas as setas do teclado.</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,129 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const phases = [
{ label: 'Fase 1 — Uma letra extra', badgeClass: 'bg-blue-100 text-blue-700', barClass: 'bg-blue-400' },
{ label: 'Fase 2 — Letra no meio', badgeClass: 'bg-indigo-100 text-indigo-700', barClass: 'bg-indigo-400' },
{ label: 'Fase 3 — Apagar e redigitar', badgeClass: 'bg-orange-100 text-orange-700', barClass: 'bg-orange-400' },
{ label: 'Fase 4 — Duas correções', badgeClass: 'bg-red-100 text-red-700', barClass: 'bg-red-400' },
{ label: 'Fase 5 — Frases longas', badgeClass: 'bg-purple-100 text-purple-700', barClass: 'bg-purple-500' },
];
const tasks = [
// Fase 1 Uma letra extra no final
{ error: 'Dadoss', correct: 'Dados', hint: 'Apague a letra repetida no final.' },
{ error: 'Redee', correct: 'Rede', hint: 'Há um "e" extra no final.' },
{ error: 'Codigoo', correct: 'Código', hint: 'Apague o "o" repetido, cuidado com o acento.' },
{ error: 'Aprendisado', correct: 'Aprendizado', hint: 'Corrija a palavra "Aprendisado".' },
{ error: 'Auulass', correct: 'Aulas', hint: 'Apague o "u" e "s" duplicados.' },
{ error: 'Comunidadde', correct: 'Comunidade', hint: 'A palavra tem uma letra repetida no final.' },
// Fase 2 Letra extra em posição variada
{ error: 'Tecnoologia livre', correct: 'Tecnologia livre', hint: 'Corrija a palavra "Tecnoologia".' },
{ error: 'Soberannia digital', correct: 'Soberania digital', hint: 'Há um "n" a mais em "Soberannia".' },
{ error: 'Internet para toddos', correct: 'Internet para todos', hint: 'Corrija a palavra "toddos".' },
{ error: 'Laboratóriio de código', correct: 'Laboratório de código', hint: 'Um "i" está sobrando.' },
{ error: 'Educaçãao popular', correct: 'Educação popular', hint: 'Apague a letra extra em "Educaçãao".' },
{ error: 'Tecnologia para a comunnidade', correct: 'Tecnologia para a comunidade', hint: 'Há um "n" extra em "comunnidade".' },
// Fase 3 Apagar tudo e redigitar
{ error: 'SOBERANIA DIGITAL', correct: 'soberania digital', hint: 'Apague tudo e redigite em minúsculas.' },
{ error: 'dados abertos', correct: 'DADOS ABERTOS', hint: 'Reescreva em letras maiúsculas.' },
{ error: 'REDE COMUNITÁRIA', correct: 'rede comunitária', hint: 'Reescreva em letras minúsculas.' },
{ error: 'tecnologia social', correct: 'TECNOLOGIA SOCIAL', hint: 'Apague e redigite em MAIÚSCULAS.' },
{ error: 'CIDADANIA DIGITAL', correct: 'cidadania digital', hint: 'Apague e redigite em minúsculas.' },
// Fase 4 Frases com dois erros
{ error: 'Tecnoologia para toddos', correct: 'Tecnologia para todos', hint: 'Corrija duas palavras com letras extras.' },
{ error: 'Internet livre para a comunnidade', correct: 'Internet livre para a comunidade', hint: 'A palavra final tem uma letra repetida.' },
{ error: 'Educaçãao digital fortalece o territórrio', correct: 'Educação digital fortalece o território', hint: 'Corrija as duas palavras com letras sobrando.' },
{ error: 'Redee comunitária amplia vozess locais', correct: 'Rede comunitária amplia vozes locais', hint: 'Há duas letras repetidas em palavras diferentes.' },
{ error: 'Soberannia digital protege daddos', correct: 'Soberania digital protege dados', hint: 'Corrija as duas palavras com repetição.' },
// Fase 5 Frases mais longas
{ error: 'A tecnoologia livre fortalece a autonomia da comunnidade.', correct: 'A tecnologia livre fortalece a autonomia da comunidade.', hint: 'Corrija as duas palavras com letras extras.' },
{ error: 'Educaçãao popular e internet livre ampliam oportuniddades.', correct: 'Educação popular e internet livre ampliam oportunidades.', hint: 'Há duas palavras com letras repetidas.' },
{ error: 'Dadoss abertos apoiam a participaçãao social.', correct: 'Dados abertos apoiam a participação social.', hint: 'Corrija as duas palavras com letras sobrando.' },
{ error: 'A redee comunitária fortalece vínculoss no território.', correct: 'A rede comunitária fortalece vínculos no território.', hint: 'Remova as repetições em duas palavras.' },
{ error: 'Soberannia digital exige cuidado com dadoss pessoais.', correct: 'Soberania digital exige cuidado com dados pessoais.', hint: 'Corrija duas palavras com letras extras.' },
{ error: 'Você já domina o Backspace e corrige textoss com atenção.', correct: 'Você já domina o Backspace e corrige textos com atenção.', hint: 'Etapa final: encontre a letra repetida.' },
];
let current = 0;
let lastNotifiedPhase = -1;
notify('started');
const phaseBadge = document.getElementById('phaseBadge');
const stepCounter = document.getElementById('stepCounter');
const progressBar = document.getElementById('progressBar');
const errorText = document.getElementById('errorText');
const correctText = document.getElementById('correctText');
const taskHint = document.getElementById('taskHint');
const editInput = document.getElementById('editInput');
const feedbackEl = document.getElementById('feedback');
const successBanner = document.getElementById('successBanner');
const taskBox = document.getElementById('taskBox');
function getPhaseIdx(idx) { return Math.floor(idx / 6); }
function syncInputHeight() {
editInput.style.height = 'auto';
editInput.style.height = `${Math.max(editInput.scrollHeight, 120)}px`;
}
function showTask() {
const t = tasks[current];
const phaseIdx = getPhaseIdx(current);
const phase = phases[phaseIdx];
if (phaseIdx !== lastNotifiedPhase) {
notify('running', { step: phaseIdx + 1 });
lastNotifiedPhase = phaseIdx;
}
phaseBadge.textContent = phase.label;
phaseBadge.className = `text-xs font-bold px-3 py-1 rounded-full transition-all ${phase.badgeClass}`;
progressBar.className = `h-2.5 rounded-full transition-all duration-500 ${phase.barClass}`;
progressBar.style.width = `${(current / tasks.length) * 100}%`;
stepCounter.textContent = `Passo ${current + 1} de ${tasks.length}`;
errorText.textContent = t.error;
correctText.textContent = t.correct;
taskHint.textContent = t.hint;
editInput.value = t.error;
editInput.disabled = false;
syncInputHeight();
editInput.focus();
feedbackEl.classList.add('hidden');
}
showTask();
editInput.addEventListener('input', () => {
syncInputHeight();
if (editInput.value === tasks[current].correct) {
feedbackEl.textContent = 'Correto!';
feedbackEl.className = 'text-xl font-bold py-1 text-center text-green-600';
feedbackEl.classList.remove('hidden');
editInput.disabled = true;
current++;
setTimeout(() => {
if (current >= tasks.length) {
taskBox.classList.add('hidden');
progressBar.style.width = '100%';
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showTask();
}
}, 700);
}
});
editInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') e.preventDefault();
});

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backspace: Correções no Texto</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="eraser" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Backspace: Correções no Texto</h2>
<p class="text-base text-gray-500">Posicione o cursor, apague erros e reescreva frases sobre tecnologia cidadã.</p>
</div>
</div>
<!-- Main -->
<div class="flex-1 p-5 flex flex-col gap-4 justify-center items-center overflow-hidden">
<!-- Phase + progress -->
<div class="w-full max-w-lg">
<div class="flex items-center justify-between mb-2">
<span id="phaseBadge" class="text-xs font-bold px-3 py-1 rounded-full transition-all"></span>
<span id="stepCounter" class="text-sm font-semibold text-gray-500"></span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5 overflow-hidden">
<div id="progressBar" class="h-2.5 rounded-full transition-all duration-500 w-0"></div>
</div>
</div>
<!-- Task box -->
<div id="taskBox" class="w-full max-w-lg flex flex-col gap-3">
<p id="taskHint" class="text-base text-gray-500 text-center font-medium"></p>
<div class="flex gap-3">
<div class="flex-1 bg-red-50 border-2 border-red-200 rounded-xl p-3 text-center">
<p class="text-xs font-bold text-red-400 mb-1 uppercase tracking-wide">Com erro</p>
<p id="errorText" class="text-xl font-mono text-red-700 font-bold break-all"></p>
</div>
<div class="flex-1 bg-green-50 border-2 border-green-200 rounded-xl p-3 text-center">
<p class="text-xs font-bold text-green-400 mb-1 uppercase tracking-wide">Corrija para</p>
<p id="correctText" class="text-xl font-mono text-green-700 font-bold break-all"></p>
</div>
</div>
<textarea id="editInput" rows="3" autocomplete="off" spellcheck="false"
class="text-xl font-mono text-center border-2 border-indigo-300 rounded-xl bg-white/80 outline-none w-full py-3 px-4 text-indigo-700 resize-none overflow-y-auto leading-relaxed"></textarea>
<p class="text-xs text-gray-400 text-center">Use o Backspace para apagar, depois corrija o texto</p>
</div>
<div id="feedback" class="hidden text-xl font-bold py-1 text-center"></div>
<!-- Success -->
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Excelente!</h2>
<p class="text-lg text-gray-700">Você concluiu 30 etapas de correção. O Backspace está na ponta dos dedos.</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,75 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
notify('started');
const tasks = [
{ prompt: 'Digite exatamente: computador', target: 'computador', placeholder: 'computador' },
{ prompt: 'Digite exatamente: 2026', target: '2026', placeholder: '2026' },
{ prompt: 'Digite exatamente: dados são poder', target: 'dados são poder', placeholder: 'dados são poder' },
{ prompt: 'Digite exatamente: tecnologia popular', target: 'tecnologia popular', placeholder: 'tecnologia popular' },
{ prompt: 'Digite exatamente: soberania digital', target: 'soberania digital', placeholder: 'soberania digital' },
{ prompt: 'Digite exatamente: estou aprendendo tecnologia', target: 'estou aprendendo tecnologia', placeholder: 'estou aprendendo tecnologia' },
];
let current = 0;
const taskLabel = document.getElementById('taskLabel');
const stepLabel = document.getElementById('stepLabel');
const textInput = document.getElementById('textInput');
const feedback = document.getElementById('feedback');
const confirmBtn = document.getElementById('confirmBtn');
const successBanner = document.getElementById('successBanner');
const formWrap = document.getElementById('formWrap');
function showTask() {
const task = tasks[current];
stepLabel.textContent = `Passo ${current + 1} de ${tasks.length}`;
taskLabel.textContent = task.prompt;
textInput.value = '';
textInput.placeholder = task.placeholder;
feedback.textContent = '';
textInput.focus();
}
function checkInput() {
const task = tasks[current];
const value = textInput.value;
confirmBtn.disabled = value !== task.target;
if (!value) {
feedback.textContent = '';
return;
}
feedback.textContent = value === task.target ? 'Correto! Pode avançar.' : 'Confira letras, acentos e espaços.';
feedback.className = value === task.target ? 'text-base text-emerald-700 font-semibold' : 'text-base text-rose-600 font-semibold';
}
textInput.addEventListener('input', checkInput);
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !confirmBtn.disabled) {
e.preventDefault();
confirmBtn.click();
}
});
confirmBtn.addEventListener('click', () => {
notify('running', { step: current + 1 });
current += 1;
if (current >= tasks.length) {
formWrap.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
return;
}
showTask();
confirmBtn.disabled = true;
});
confirmBtn.disabled = true;
showTask();

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prática de Escrita</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="type" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Prática de Escrita</h2>
<p class="text-lg text-gray-600">Digite palavras, números e frases que serão solicitadas.</p>
</div>
</div>
<div class="flex-1 p-6 flex flex-col gap-5 justify-center items-center overflow-hidden">
<div id="formWrap" class="w-full max-w-2xl flex flex-col gap-4">
<div class="text-sm font-bold uppercase tracking-wide text-brand-700 text-center" id="stepLabel">Passo 1 de 10</div>
<div class="bg-brand-50 border-2 border-brand-200 rounded-xl p-4 text-center">
<p id="taskLabel" class="text-xl text-brand-900 font-semibold"></p>
</div>
<input id="textInput" type="text" autocomplete="off" spellcheck="false"
class="text-2xl font-mono text-center border-b-4 border-brand-400 bg-transparent outline-none w-full py-2 text-gray-700 placeholder:text-gray-400"
placeholder="Digite aqui" />
<p id="feedback" class="text-base text-gray-500 text-center"></p>
<button id="confirmBtn" class="mt-2 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed text-white text-xl font-bold px-10 py-4 rounded-2xl shadow-lg transition-all active:scale-95 self-center">
Avançar
</button>
</div>
<div id="successBanner" class="hidden bg-gradient-to-r from-brand-50 to-brand-100 border-2 border-brand-300 rounded-xl p-8 flex flex-col items-center justify-center gap-3 w-full max-w-xl text-center">
<i data-lucide="check-circle" class="text-brand-600 w-16 h-16"></i>
<h2 class="text-3xl font-bold text-brand-700">Muito bem!</h2>
<p class="text-lg text-gray-700">Você completou 10 passos de escrita com foco em tecnologia e cidadania digital.</p>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
notify('started');
const steps = [
{
text: 'Passo 1: Pressione → três vezes',
hint: 'A seta para direita move o cursor uma letra por vez.',
action: 'arrow-right',
count: 3,
},
{
text: 'Passo 2: Pressione ← duas vezes',
hint: 'A seta para esquerda volta o cursor.',
action: 'arrow-left',
count: 2,
},
{
text: 'Passo 3: Pressione Home',
hint: 'Home vai para o INÍCIO da linha.',
action: 'home',
count: 1,
},
{
text: 'Passo 4: Pressione End',
hint: 'End vai para o FINAL da linha.',
action: 'end',
count: 1,
},
];
let currentStep = 0;
let actionCount = 0;
let locked = false;
const textArea = document.getElementById('textArea');
const stepText = document.getElementById('stepText');
const stepHint = document.getElementById('stepHint');
const feedbackEl = document.getElementById('feedback');
const cursorPos = document.getElementById('cursorPos');
const progressDots = document.getElementById('progressDots');
const successBanner = document.getElementById('successBanner');
const stepBox = document.getElementById('stepBox');
textArea.value = 'Olá mundo!';
textArea.readOnly = false;
textArea.focus();
textArea.setSelectionRange(0, 0);
steps.forEach((_, i) => {
const dot = document.createElement('div');
dot.id = `dot-${i}`;
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
progressDots.appendChild(dot);
});
function showStep() {
const s = steps[currentStep];
stepText.textContent = s.text;
stepHint.textContent = s.hint;
actionCount = 0;
feedbackEl.classList.add('hidden');
textArea.focus();
}
showStep();
textArea.addEventListener('keydown', (e) => {
if (locked) return;
const s = steps[currentStep];
const pos = textArea.selectionStart;
cursorPos.textContent = `Posição do cursor: ${pos}`;
let matched = false;
if (s.action === 'arrow-right' && e.key === 'ArrowRight') matched = true;
if (s.action === 'arrow-left' && e.key === 'ArrowLeft') matched = true;
if (s.action === 'home' && e.key === 'Home') matched = true;
if (s.action === 'end' && e.key === 'End') matched = true;
if (matched) {
actionCount++;
feedbackEl.textContent = `${actionCount} de ${s.count}`;
feedbackEl.className = 'text-2xl font-bold py-1 text-green-600';
feedbackEl.classList.remove('hidden');
if (actionCount >= s.count) {
locked = true;
const dot = document.getElementById(`dot-${currentStep}`);
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
notify('running', { step: currentStep + 1 });
currentStep++;
setTimeout(() => {
locked = false;
if (currentStep >= steps.length) {
stepBox.classList.add('hidden');
progressDots.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showStep();
}
}, 700);
}
}
});

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setas de Navegação</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="move" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Setas: Mova o Cursor</h2>
<p class="text-lg text-gray-600">Use as setas para mover o cursor dentro do texto.</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
<div class="bg-teal-50 border-2 border-teal-200 rounded-2xl p-4 max-w-lg text-center">
<p class="text-lg text-teal-800">
← → move o cursor <strong>para os lados</strong>. ↑ ↓ movem linha acima/abaixo.
<strong>Home</strong> vai para o início da linha. <strong>End</strong> vai para o final.
</p>
</div>
<div id="stepBox" class="w-full max-w-lg text-center">
<p class="text-xl font-bold text-gray-700" id="stepText"></p>
<p class="text-lg text-gray-500 mt-1" id="stepHint"></p>
<div class="mt-3 relative inline-block">
<textarea id="textArea" rows="3" spellcheck="false" autocomplete="off"
class="text-xl font-mono border-2 border-teal-400 rounded-xl p-3 w-80 resize-none outline-none bg-white shadow focus:border-teal-600"
></textarea>
<div id="cursorPos" class="text-sm text-gray-400 mt-1">Posição do cursor: 0</div>
</div>
</div>
<div id="feedback" class="hidden text-2xl font-bold py-1"></div>
<div class="flex gap-2 justify-center" id="progressDots"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
<p class="text-lg text-gray-700">Agora você sabe navegar no texto sem o mouse!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,207 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const STEP_SIZES = [2, 4, 6, 8, 10];
const TOTAL_STEPS = STEP_SIZES.length;
function shuffle(list) {
const copy = [...list];
for (let i = copy.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
function init() {
const queryEls = () => ({
instruction: document.getElementById('instruction'),
hint: document.getElementById('hint'),
arena: document.getElementById('arena'),
successBanner: document.getElementById('successBanner'),
successMsg: document.getElementById('successMsg'),
});
let { instruction, hint, arena, successBanner, successMsg } = queryEls();
const required = { instruction, hint, arena, successBanner, successMsg };
const missing = Object.entries(required)
.filter(([, value]) => !value)
.map(([name]) => name);
if (missing.length > 0) {
console.warn(`[teclado-numeros] Layout incompleto. Aplicando fallback para: ${missing.join(', ')}`);
document.body.innerHTML = `
<div class="min-h-screen w-full bg-gradient-to-br from-sky-50 via-white to-indigo-50 p-4 flex items-center justify-center">
<div class="w-full max-w-5xl bg-white rounded-xl shadow-xl border border-slate-200 p-5 flex flex-col gap-4">
<div>
<h2 id="instruction" class="text-2xl font-bold text-slate-800">Números do Teclado</h2>
<p id="hint" class="text-slate-600"></p>
</div>
<div id="arena"></div>
<div id="successBanner" class="hidden rounded-xl border-2 border-emerald-300 bg-emerald-50 p-6 text-center">
<h2 class="text-2xl font-bold text-emerald-700">Muito bem!</h2>
<p id="successMsg" class="text-slate-700"></p>
</div>
</div>
</div>
`;
({ instruction, hint, arena, successBanner, successMsg } = queryEls());
}
notify('started');
let step = 1;
let sequence = [];
let index = 0;
let done = false;
function generateSequence(size) {
if (size <= DIGITS.length) {
return shuffle(DIGITS).slice(0, size);
}
const base = shuffle(DIGITS);
const extra = DIGITS[Math.floor(Math.random() * DIGITS.length)];
return [...base, extra];
}
function markSequence(indexToMark, state) {
const chip = document.getElementById(`seq-chip-${indexToMark}`);
if (!chip) return;
if (state === 'done') {
chip.className = 'h-11 w-11 rounded-lg border-2 border-emerald-300 bg-emerald-100 text-emerald-700 font-black text-xl flex items-center justify-center transition-all';
return;
}
if (state === 'error') {
chip.className = 'h-11 w-11 rounded-lg border-2 border-rose-300 bg-rose-100 text-rose-700 font-black text-xl flex items-center justify-center transition-all';
window.setTimeout(() => {
if (index === indexToMark) {
chip.className = 'h-11 w-11 rounded-lg border-2 border-brand-300 bg-brand-100 text-brand-700 font-black text-xl flex items-center justify-center transition-all';
}
}, 220);
return;
}
chip.className = 'h-11 w-11 rounded-lg border-2 border-brand-300 bg-brand-100 text-brand-700 font-black text-xl flex items-center justify-center transition-all';
}
function setFeedback(ok, typed, expected) {
const status = document.getElementById('statusFeedback');
if (!status) return;
if (ok) {
status.className = 'mt-4 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-emerald-700 font-semibold';
status.textContent = `Correto: ${typed}`;
return;
}
status.className = 'mt-4 rounded-xl border border-rose-300 bg-rose-50 px-4 py-3 text-rose-700 font-semibold';
status.textContent = `Ops: você digitou ${typed}. Próximo esperado: ${expected}`;
}
function renderSequenceChips() {
const container = document.getElementById('sequenceChips');
if (!container) return;
container.innerHTML = '';
sequence.forEach((digit, i) => {
const chip = document.createElement('div');
chip.id = `seq-chip-${i}`;
chip.className = 'h-11 w-11 rounded-lg border-2 border-slate-300 bg-white text-slate-700 font-black text-xl flex items-center justify-center transition-all';
chip.textContent = digit;
container.appendChild(chip);
});
markSequence(0, 'current');
}
function renderStep() {
sequence = generateSequence(STEP_SIZES[step - 1]);
index = 0;
hint.textContent = `Digite ${sequence.length} número(s) na ordem mostrada.`;
arena.innerHTML = `
<div class="mx-auto w-full max-w-5xl rounded-2xl bg-brand-50 border-2 border-brand-200 p-6 text-center">
<p class="text-base text-slate-700 font-semibold">Sequência da vez</p>
<div class="mt-3 overflow-x-auto pb-1">
<div id="sequenceChips" class="mx-auto flex w-fit min-w-full flex-nowrap items-center justify-center gap-2"></div>
</div>
<p class="mt-4 text-sm text-slate-500">Acertos na rodada: <span id="seqState">0</span>/${sequence.length}</p>
<p id="statusFeedback" class="mt-4 rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-600 font-medium">Digite o primeiro número destacado.</p>
</div>
`;
renderSequenceChips();
}
function flashKey(digit, ok) {
const el = document.getElementById(`kbd-${digit}`);
if (!el) return;
el.className = ok
? 'w-12 h-12 rounded-lg border-2 border-emerald-300 bg-emerald-100 flex items-center justify-center font-bold text-emerald-700'
: 'w-12 h-12 rounded-lg border-2 border-rose-300 bg-rose-100 flex items-center justify-center font-bold text-rose-700';
setTimeout(() => {
el.className = 'w-12 h-12 rounded-lg border-2 border-slate-300 bg-white flex items-center justify-center font-bold text-slate-700';
}, 180);
}
function handleDigit(digit) {
if (done) return;
const expected = sequence[index];
const ok = digit === expected;
flashKey(digit, ok);
if (!ok) {
markSequence(index, 'error');
setFeedback(false, digit, expected);
return;
}
markSequence(index, 'done');
setFeedback(true, digit, expected);
index += 1;
const seqState = document.getElementById('seqState');
if (seqState) seqState.textContent = String(index);
if (index < sequence.length) {
markSequence(index, 'current');
}
if (index >= sequence.length) {
notify('running', { step });
if (step >= TOTAL_STEPS) {
done = true;
arena.classList.add('hidden');
successBanner.classList.remove('hidden');
successMsg.textContent = 'Você agora sabe onde estão os números no teclado!';
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
return;
}
step += 1;
renderStep();
}
}
document.addEventListener('keydown', (event) => {
if (!/^Digit[0-9]$/.test(event.code)) return;
handleDigit(event.code.replace('Digit', ''));
});
renderStep();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Números do Teclado</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
.key { transition: all 0.15s; cursor: pointer; }
.key:hover { filter: brightness(1.1); transform: scale(1.05); }
.key.active { transform: scale(0.95); filter: brightness(0.85); }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="calculator" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Números do Teclado</h2>
<p class="text-lg text-gray-600" id="hint"></p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
<div id="arena" ></div>
<div id="successBanner" class="hidden rounded-xl border-2 border-emerald-300 bg-gradient-to-r from-emerald-50 to-green-50 p-8 text-center">
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 text-emerald-700"><i data-lucide="check-circle-2" class="h-10 w-10"></i></div>
<h2 id="successTitle" class="mb-2 text-3xl font-bold text-emerald-700">Muito bem!</h2>
<p id="successMsg" class="text-lg text-slate-700"></p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,150 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
const regions = [
{
id: 'letras',
label: 'Letras (AZ)',
color: 'bg-blue-400',
textColor: 'text-white',
title: 'Região das Letras',
desc: 'Aqui ficam todas as 26 letras do alfabeto. São as teclas mais usadas para escrever palavras.',
keys: ['Q','W','E','R','T','Y','U','I','O','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M'],
},
{
id: 'numeros',
label: 'Números (09)',
color: 'bg-green-400',
textColor: 'text-white',
title: 'Região dos Números',
desc: 'A linha superior tem os números de 1 a 0. Também há símbolos quando você usa o Shift.',
keys: ['1','2','3','4','5','6','7','8','9','0'],
},
{
id: 'especiais',
label: 'Teclas Especiais',
color: 'bg-orange-400',
textColor: 'text-white',
title: 'Teclas Especiais',
desc: 'Enter, Backspace, Shift, Caps Lock, Esc, Tab — cada uma tem uma função importante!',
keys: ['Enter','Backspace','Shift','Caps Lock','Esc','Tab'],
},
{
id: 'setas',
label: 'Setas de Navegação',
color: 'bg-purple-400',
textColor: 'text-white',
title: 'Setas de Navegação',
desc: 'As quatro setas movem o cursor no texto sem apagar nada. Home e End vão para início/fim da linha.',
keys: ['←','↑','↓','→','Home','End'],
},
];
const visitedRegions = new Set();
notify('started');
const keyboard = document.getElementById('keyboard');
const infoBox = document.getElementById('infoBox');
const infoTitle = document.getElementById('infoTitle');
const infoDesc = document.getElementById('infoDesc');
const progressArea = document.getElementById('progressArea');
const instruction = document.getElementById('instruction');
const hint = document.getElementById('hint');
const successBanner = document.getElementById('successBanner');
let isCompleted = false;
// Build visual keyboard rows
const rows = [
['Esc','1','2','3','4','5','6','7','8','9','0','Backspace'],
['Tab','Q','W','E','R','T','Y','U','I','O','P'],
['Caps Lock','A','S','D','F','G','H','J','K','L','Enter'],
['Shift','Z','X','C','V','B','N','M','Shift'],
['←','↑','↓','→','Home','End'],
];
rows.forEach(row => {
const rowEl = document.createElement('div');
rowEl.className = 'flex gap-1 justify-center';
row.forEach(key => {
const region = regions.find(r => r.keys.includes(key));
const keyEl = document.createElement('div');
const isWide = ['Backspace','Tab','Caps Lock','Enter','Shift'].includes(key);
keyEl.className = `key px-2 py-2 rounded-md text-sm font-bold shadow flex items-center justify-center min-w-[2.2rem] ${isWide ? 'px-3 min-w-[4rem]' : ''} ${region ? region.color + ' ' + region.textColor : 'bg-gray-200 text-gray-700'}`;
keyEl.textContent = key;
if (region) {
keyEl.addEventListener('click', () => handleRegionClick(region));
}
rowEl.appendChild(keyEl);
});
keyboard.appendChild(rowEl);
});
function normalizeKey(event) {
const key = event.key;
if (key.length === 1) {
return key.toUpperCase();
}
const aliases = {
ArrowLeft: '←',
ArrowUp: '↑',
ArrowDown: '↓',
ArrowRight: '→',
Escape: 'Esc',
CapsLock: 'Caps Lock',
' ': 'Space',
};
return aliases[key] ?? key;
}
function findRegionByKey(keyLabel) {
return regions.find((region) => region.keys.includes(keyLabel));
}
function handleRegionClick(region) {
if (isCompleted) return;
// Highlight all keys of this region
document.querySelectorAll('.key').forEach(k => {
k.classList.remove('ring-4','ring-yellow-400');
if (region.keys.includes(k.textContent.trim())) {
k.classList.add('ring-4','ring-yellow-400');
}
});
infoTitle.textContent = region.title;
infoDesc.textContent = region.desc;
infoBox.classList.remove('hidden');
if (!visitedRegions.has(region.id)) {
visitedRegions.add(region.id);
notify('running', { step: visitedRegions.size });
}
if (visitedRegions.size >= regions.length) {
isCompleted = true;
setTimeout(() => {
keyboard.classList.add('hidden');
infoBox.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
}, 1000);
}
}
document.addEventListener('keydown', (event) => {
if (isCompleted) return;
const normalizedKey = normalizeKey(event);
const region = findRegionByKey(normalizedKey);
if (!region) return;
handleRegionClick(region);
});

View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Regiões do Teclado</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>
body { overflow: hidden; height: 100vh; width: 100vw; }
.key { transition: all 0.15s; cursor: pointer; }
.key:hover { filter: brightness(1.1); transform: scale(1.05); }
.key.active { transform: scale(0.95); filter: brightness(0.85); }
</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="keyboard" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800" id="instruction">Conheça as regiões do teclado</h2>
<p class="text-lg text-gray-600" id="hint">Clique em uma região colorida ou pressione qualquer tecla dela.</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 overflow-hidden">
<div id="infoBox" class="hidden bg-indigo-50 border-2 border-indigo-300 rounded-xl p-4 text-center">
<p class="text-xl font-bold text-indigo-700" id="infoTitle"></p>
<p class="text-lg text-gray-700 mt-1" id="infoDesc"></p>
</div>
<div id="keyboard" class="flex-1 flex flex-col justify-center gap-2 select-none"></div>
<div id="progressArea" class="flex gap-3 justify-center flex-wrap"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Muito bem!</h2>
<p class="text-lg text-gray-700">Você conheceu todas as regiões do teclado!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
const channelToken = new URLSearchParams(window.location.hash.slice(1)).get('channelToken');
function notify(type, payload = {}) {
window.parent.postMessage({ type, token: channelToken, ...payload }, '*');
}
// Map: symbol → key hint
const sequence = [
{ symbol: '!', hint: 'Shift + 1', key: '!' },
{ symbol: '@', hint: 'Shift + 2', key: '@' },
{ symbol: '#', hint: 'Shift + 3', key: '#' },
{ symbol: '$', hint: 'Shift + 4', key: '$' },
{ symbol: '%', hint: 'Shift + 5', key: '%' },
{ symbol: '&', hint: 'Shift + 7', key: '&' },
{ symbol: '*', hint: 'Shift + 8', key: '*' },
{ symbol: '(', hint: 'Shift + 9', key: '(' },
];
let currentIdx = 0;
let locked = false;
notify('started');
const targetEl = document.getElementById('target');
const targetHintEl = document.getElementById('targetHint');
const feedbackEl = document.getElementById('feedback');
const progressDots = document.getElementById('progressDots');
const successBanner = document.getElementById('successBanner');
sequence.forEach((_, i) => {
const dot = document.createElement('div');
dot.id = `dot-${i}`;
dot.className = 'w-8 h-8 rounded-full border-2 border-gray-300 bg-gray-100 transition-all';
progressDots.appendChild(dot);
});
function showTarget() {
const s = sequence[currentIdx];
targetEl.textContent = s.symbol;
targetHintEl.textContent = `Pressione: ${s.hint}`;
feedbackEl.classList.add('hidden');
}
showTarget();
document.addEventListener('keydown', (e) => {
if (locked) return;
const s = sequence[currentIdx];
if (e.key === s.key) {
locked = true;
feedbackEl.textContent = 'Isso!';
feedbackEl.className = 'text-2xl font-bold py-1 text-green-600';
feedbackEl.classList.remove('hidden');
const dot = document.getElementById(`dot-${currentIdx}`);
if (dot) dot.className = 'w-8 h-8 rounded-full bg-green-400 border-2 border-green-500 transition-all';
notify('running', { step: currentIdx + 1 });
currentIdx++;
setTimeout(() => {
locked = false;
if (currentIdx >= sequence.length) {
targetEl.parentElement.classList.add('hidden');
progressDots.classList.add('hidden');
successBanner.classList.remove('hidden');
lucide.createIcons();
notify('success', { score: 100 });
notify('completed', { score: 100 });
} else {
showTarget();
}
}, 800);
} else if (e.key !== 'Shift') {
feedbackEl.textContent = `Não é esse. Tente: ${s.hint}`;
feedbackEl.className = 'text-2xl font-bold py-1 text-red-500';
feedbackEl.classList.remove('hidden');
}
});

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Símbolos com Shift</title>
<link rel="stylesheet" href="../../shared/letramento.css">
<script src="../../shared/lucide.js"></script>
<style>body { overflow: hidden; height: 100vh; width: 100vw; }</style>
</head>
<body class="bg-gradient-to-br from-brand-50 to-gray-100 flex items-center justify-center h-screen w-screen overflow-hidden p-4">
<div class="w-full h-full flex flex-col">
<div class="bg-white/95 backdrop-blur-sm border border-white/30 rounded-xl shadow-xl flex-1 flex flex-col overflow-hidden">
<div class="flex items-center justify-center gap-4 p-4 border-b border-gray-100">
<i data-lucide="at-sign" class="w-10 h-10 text-red-500"></i>
<div>
<h2 class="text-2xl font-bold text-gray-800">Símbolos com Shift + Número</h2>
<p class="text-lg text-gray-600">Segure Shift e pressione o número para obter o símbolo.</p>
</div>
</div>
<div class="flex-1 p-4 flex flex-col gap-4 justify-center items-center overflow-hidden">
<div class="bg-purple-50 border-2 border-purple-200 rounded-2xl p-4 max-w-lg text-center">
<p class="text-lg text-purple-800">
Cada tecla de número tem um <strong>símbolo escondido</strong> em cima dela. Para acessá-lo, segure <kbd class="bg-gray-200 px-2 py-0.5 rounded font-mono font-bold">Shift</kbd> e pressione o número!
</p>
</div>
<div id="target" class="text-8xl font-black text-purple-600 bg-purple-50 rounded-2xl w-44 h-44 flex items-center justify-center border-4 border-purple-300 shadow-lg"></div>
<p class="text-xl text-gray-500" id="targetHint"></p>
<div id="feedback" class="hidden text-2xl font-bold py-1"></div>
<div class="flex gap-2 justify-center" id="progressDots"></div>
<div id="successBanner" class="hidden bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl p-6 flex items-center justify-center gap-4 w-full max-w-lg">
<i data-lucide="check-circle" class="text-green-600 w-14 h-14"></i>
<div>
<h2 class="text-3xl font-bold text-green-700 mb-1">Incrível!</h2>
<p class="text-lg text-gray-700">Agora você sabe como digitar símbolos!</p>
</div>
</div>
</div>
</div>
</div>
<script src="./activity.js"></script>
<script>lucide.createIcons();</script>
</body>
</html>

View File

@@ -0,0 +1,182 @@
export const TECLADO_ATIVIDADES_REGISTRY = {
'teclado-regioes': {
id: 'teclado-regioes',
titulo: 'Regiões do Teclado',
descricao: 'Aprenda onde ficam as letras e os grupos principais do teclado.',
categoria: 'teclado',
dificuldade: 'iniciante',
duracao: 3,
htmlFile: '/atividades/letramento/teclado/teclado-regioes/index.html',
proxima: 'teclado-numeros',
passos: [
{ id: 1, label: 'Letras' },
{ id: 2, label: 'Números' },
{ id: 3, label: 'Teclas especiais' },
{ id: 4, label: 'Setas, Home e End' },
],
},
'teclado-numeros': {
id: 'teclado-numeros',
titulo: 'Números do Teclado',
descricao: 'Pratique sequências numéricas progressivas, começando com 2 e chegando a 10 números.',
categoria: 'teclado',
dificuldade: 'iniciante',
duracao: 5,
htmlFile: '/atividades/letramento/teclado/teclado-numeros/index.html',
proxima: 'teclado-escrita',
passos: [
{ id: 1, label: '2 números' },
{ id: 3, label: '4 números' },
{ id: 5, label: '6 números' },
{ id: 7, label: '8 números' },
{ id: 9, label: '10 números' },
],
},
'teclado-escrita': {
id: 'teclado-escrita',
titulo: 'Prática de Escrita',
descricao: 'Digite palavras e frases sobre tecnologia, cidadania e soberania digital.',
categoria: 'teclado',
dificuldade: 'iniciante',
duracao: 5,
htmlFile: '/atividades/letramento/teclado/teclado-escrever-nome/index.html',
proxima: 'teclado-simbolos',
passos: [
{ id: 1, label: 'Palavra 1' },
{ id: 3, label: 'Número' },
{ id: 4, label: 'Frase 1' },
{ id: 5, label: 'Frase 2' },
{ id: 6, label: 'Frase 3' },
{ id: 7, label: 'Frase 4' },
],
},
'teclado-simbolos': {
id: 'teclado-simbolos',
titulo: 'Símbolos com Shift',
descricao: 'Use Shift para formar símbolos comuns do teclado internacional.',
categoria: 'teclado',
dificuldade: 'iniciante',
duracao: 3,
htmlFile: '/atividades/letramento/teclado/teclado-simbolos/index.html',
proxima: 'teclado-backspace',
passos: [
{ id: 1, label: 'Shift' },
{ id: 2, label: 'Símbolos' },
],
},
'teclado-backspace': {
id: 'teclado-backspace',
titulo: 'Backspace e Correção',
descricao: 'Corrija frases sobre educação popular, tecnologia e soberania digital.',
categoria: 'teclado',
dificuldade: 'intermediario',
duracao: 4,
htmlFile: '/atividades/letramento/teclado/teclado-backspace/index.html',
proxima: 'teclado-labirinto',
passos: [
{ id: 1, label: 'Correções simples' },
{ id: 2, label: 'Correções no meio' },
{ id: 3, label: 'Reescrita' },
{ id: 4, label: 'Frases completas' },
{ id: 5, label: 'Frases longas' },
],
},
'teclado-labirinto': {
id: 'teclado-labirinto',
titulo: 'Labirinto das Setas',
descricao: 'Pratique navegação com setas para ganhar fluidez no teclado.',
categoria: 'teclado',
dificuldade: 'intermediario',
duracao: 3,
htmlFile: '/atividades/letramento/teclado/labirinto/index.html',
proxima: 'teclado-navegacao',
passos: [
{ id: 1, label: 'Fase 1' },
{ id: 2, label: 'Fase 2' },
{ id: 3, label: 'Fase 3' },
{ id: 4, label: 'Fase 4' },
{ id: 5, label: 'Fase 5' },
],
},
'teclado-navegacao': {
id: 'teclado-navegacao',
titulo: 'Teclas de Navegação',
descricao: 'Use setas, Home e End para revisar textos.',
categoria: 'teclado',
dificuldade: 'intermediario',
duracao: 3,
htmlFile: '/atividades/letramento/teclado/teclado-navegacao/index.html',
proxima: 'teclado-setas-texto',
passos: [
{ id: 1, label: 'Setas' },
{ id: 2, label: 'Home e End' },
],
},
'teclado-setas-texto': {
id: 'teclado-setas-texto',
titulo: 'Setas no Texto',
descricao: 'Edite frases sobre tecnologia e soberania digital sem apagar o conteúdo.',
categoria: 'teclado',
dificuldade: 'intermediario',
duracao: 3,
htmlFile: '/atividades/letramento/teclado/setas-texto/index.html',
proxima: 'teclado-chuva',
passos: [
{ id: 1, label: 'Mover cursor' },
{ id: 2, label: 'Inserir no lugar certo' },
],
},
// 'teclado-recado': {
// id: 'teclado-recado',
// titulo: 'Texto Coletivo',
// descricao: 'Escreva uma mensagem curta sobre tecnologia, comunidade e soberania digital.',
// categoria: 'teclado',
// dificuldade: 'intermediario',
// duracao: 4,
// htmlFile: '/atividades/letramento/teclado/recado-completo/index.html',
// proxima: 'teclado-chuva',
// passos: [
// { id: 1, label: 'Escrever' },
// { id: 2, label: 'Revisar' },
// { id: 3, label: 'Concluir' },
// ],
// },
'teclado-chuva': {
id: 'teclado-chuva',
titulo: 'Chuva de Letras',
descricao: 'Digite caracteres, palavras e combinações em etapas com velocidade progressiva.',
categoria: 'teclado',
dificuldade: 'intermediario',
duracao: 5,
htmlFile: '/atividades/letramento/teclado/chuva/index.html',
proxima: 'teclado-atividade-final',
passos: [
{ id: 1, label: '5 caracteres' },
{ id: 2, label: '5 caracteres' },
{ id: 3, label: '5 caracteres' },
{ id: 4, label: '2 caracteres' },
{ id: 5, label: '3 letras' },
{ id: 6, label: '4 letras' },
],
},
'teclado-atividade-final': {
id: 'teclado-atividade-final',
titulo: 'Atividade Final',
descricao: 'Integre as habilidades de digitação em uma sequência completa de teclado.',
categoria: 'teclado',
dificuldade: 'avancado',
duracao: 5,
htmlFile: '/atividades/letramento/teclado/atividade-final/index.html',
proxima: null,
passos: [
{ id: 1, label: 'Letra' },
{ id: 2, label: 'Número' },
{ id: 3, label: 'Maiúscula' },
{ id: 4, label: 'Símbolo' },
{ id: 5, label: 'Seta' },
{ id: 6, label: 'Enter' },
{ id: 7, label: 'Esc' },
{ id: 8, label: 'Final' },
],
},
};