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:
53
app/src/atividades/letramento/letramentoRegistry.js
Normal file
53
app/src/atividades/letramento/letramentoRegistry.js
Normal 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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
212
app/src/atividades/letramento/mouse/mouse-basico/activity.js
Normal file
212
app/src/atividades/letramento/mouse/mouse-basico/activity.js
Normal 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);
|
||||
68
app/src/atividades/letramento/mouse/mouse-basico/index.html
Normal file
68
app/src/atividades/letramento/mouse/mouse-basico/index.html
Normal 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>
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
592
app/src/atividades/letramento/mouse/mouse-completo/activity.js
Normal file
592
app/src/atividades/letramento/mouse/mouse-completo/activity.js
Normal 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();
|
||||
@@ -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>
|
||||
144
app/src/atividades/letramento/mouse/mouse-controle/activity.js
Normal file
144
app/src/atividades/letramento/mouse/mouse-controle/activity.js
Normal 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 });
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>
|
||||
152
app/src/atividades/letramento/mouse/mouseRegistry.js
Normal file
152
app/src/atividades/letramento/mouse/mouseRegistry.js
Normal 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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
8109
app/src/atividades/letramento/shared/letramento.css
Normal file
8109
app/src/atividades/letramento/shared/letramento.css
Normal file
File diff suppressed because it is too large
Load Diff
18134
app/src/atividades/letramento/shared/lucide.js
Normal file
18134
app/src/atividades/letramento/shared/lucide.js
Normal file
File diff suppressed because it is too large
Load Diff
3
app/src/atividades/letramento/shared/tailwind-input.css
Normal file
3
app/src/atividades/letramento/shared/tailwind-input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
331
app/src/atividades/letramento/teclado/chuva/activity.js
Normal file
331
app/src/atividades/letramento/teclado/chuva/activity.js
Normal 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);
|
||||
83
app/src/atividades/letramento/teclado/chuva/index.html
Normal file
83
app/src/atividades/letramento/teclado/chuva/index.html
Normal 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>
|
||||
97
app/src/atividades/letramento/teclado/enter-esc/activity.js
Normal file
97
app/src/atividades/letramento/teclado/enter-esc/activity.js
Normal 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');
|
||||
}
|
||||
});
|
||||
59
app/src/atividades/letramento/teclado/enter-esc/index.html
Normal file
59
app/src/atividades/letramento/teclado/enter-esc/index.html
Normal 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>
|
||||
190
app/src/atividades/letramento/teclado/labirinto/activity.js
Normal file
190
app/src/atividades/letramento/teclado/labirinto/activity.js
Normal 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);
|
||||
}
|
||||
});
|
||||
49
app/src/atividades/letramento/teclado/labirinto/index.html
Normal file
49
app/src/atividades/letramento/teclado/labirinto/index.html
Normal 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>
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
53
app/src/atividades/letramento/teclado/setas-texto/index.html
Normal file
53
app/src/atividades/letramento/teclado/setas-texto/index.html
Normal 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>
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 (A–Z)',
|
||||
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 (0–9)',
|
||||
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);
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
182
app/src/atividades/letramento/teclado/tecladoRegistry.js
Normal file
182
app/src/atividades/letramento/teclado/tecladoRegistry.js
Normal 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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user