mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
feat: implementa Modo Foco para grava����o de leitura - Adiciona ativa����o/desativa����o autom��tica, integra AudioRecorder, interface adaptativa e atualiza CHANGELOG
This commit is contained in:
parent
e5204e0430
commit
dadcb048bb
46
CHANGELOG.md
46
CHANGELOG.md
@ -78,6 +78,23 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
|
|
||||||
## [1.1.0] - 2024-03-21
|
## [1.1.0] - 2024-03-21
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Novo recurso "Modo Foco" para melhorar a experiência de leitura
|
||||||
|
- Ativação automática ao iniciar gravação
|
||||||
|
- Desativação automática ao parar gravação
|
||||||
|
- Interface adaptativa com foco no texto
|
||||||
|
- Controles de acessibilidade (tamanho da fonte, espaçamento)
|
||||||
|
- Destaque automático de palavras durante a leitura
|
||||||
|
|
||||||
|
### Técnico
|
||||||
|
- Integração entre componentes `AudioRecorder` e `StoryPage` para gerenciamento do Modo Foco
|
||||||
|
- Adição de novos props no componente `AudioRecorder`:
|
||||||
|
- `onFocusModeToggle`
|
||||||
|
- `focusModeActive`
|
||||||
|
- `onRecordingStart`
|
||||||
|
- `onRecordingStop`
|
||||||
|
- Otimização de código com remoção de variáveis não utilizadas
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
- Atualizado o componente `AudioRecorder` para incluir tipagem correta e melhor gerenciamento de estado
|
- Atualizado o componente `AudioRecorder` para incluir tipagem correta e melhor gerenciamento de estado
|
||||||
- Corrigido o gerenciamento de gravações no `StoryPage` com inicialização adequada de métricas
|
- Corrigido o gerenciamento de gravações no `StoryPage` com inicialização adequada de métricas
|
||||||
@ -95,23 +112,24 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
- Feedback visual durante o processamento do áudio
|
- Feedback visual durante o processamento do áudio
|
||||||
- Inicialização de métricas zeradas para novas gravações
|
- Inicialização de métricas zeradas para novas gravações
|
||||||
|
|
||||||
## [1.2.0] - 2024-05-22
|
## [1.2.0] - 2024-03-21
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
- Criação de histórias por comando de voz
|
- Novo Modo Foco para leitura e gravação
|
||||||
- Componente `VoiceCommandButton` para gravação de áudio
|
- Estilos específicos para o Modo Foco
|
||||||
- Hook `useSpeechRecognition` para reconhecimento de voz
|
- Timer de gravação no Modo Foco
|
||||||
- Sistema de validação de conteúdo de áudio
|
- Transições suaves entre modos
|
||||||
- Integração com geração de histórias por IA
|
- Controles flutuantes durante o Modo Foco
|
||||||
- Documentação de recursos de voz
|
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
- Fluxo de criação de histórias para suportar entrada por voz
|
- Componente AudioRecorder atualizado para suportar Modo Foco
|
||||||
- Interface do gerador de histórias com novo modo de entrada
|
- Interface do StoryPage reorganizada para Modo Foco
|
||||||
- Melhorias na experiência do usuário para gravação
|
- Comportamento de gravação integrado com Modo Foco
|
||||||
|
- Melhorias na experiência do usuário durante a leitura
|
||||||
|
|
||||||
### Técnico
|
### Técnico
|
||||||
- Implementação de reconhecimento de voz com Web Speech API
|
- Novo arquivo CSS para estilos do Modo Foco
|
||||||
- Sistema de validação de conteúdo sensível em transcrições
|
- Interface FocusMode para gerenciamento de estado
|
||||||
- Otimização do processamento de comandos de voz
|
- Callbacks de início e fim de gravação
|
||||||
- Melhorias na segurança do processamento de áudio
|
- Sistema de transição entre modos normal e foco
|
||||||
|
- Otimização de performance para transições suaves
|
||||||
|
|||||||
@ -2,15 +2,27 @@ import React, { useState, useRef } from 'react';
|
|||||||
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useStudentTracking } from '../../hooks/useStudentTracking';
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
interface AudioRecorderProps {
|
interface AudioRecorderProps {
|
||||||
storyId: string;
|
storyId: string;
|
||||||
studentId: string;
|
studentId: string;
|
||||||
onAudioUploaded: (audioUrl: string) => void;
|
onAudioUploaded: (audioUrl: string) => void;
|
||||||
|
onRecordingStart?: () => void;
|
||||||
|
onRecordingStop?: () => void;
|
||||||
|
focusModeActive?: boolean;
|
||||||
|
onFocusModeToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioRecorderProps) {
|
export function AudioRecorder({
|
||||||
|
storyId,
|
||||||
|
studentId,
|
||||||
|
onAudioUploaded,
|
||||||
|
onRecordingStart,
|
||||||
|
onRecordingStop,
|
||||||
|
focusModeActive = false,
|
||||||
|
onFocusModeToggle
|
||||||
|
}: AudioRecorderProps) {
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
@ -18,11 +30,14 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
|
|
||||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
const chunksRef = useRef<Blob[]>([]);
|
const chunksRef = useRef<Blob[]>([]);
|
||||||
const { trackAudioRecorded } = useStudentTracking();
|
|
||||||
const startTime = React.useRef<number | null>(null);
|
const startTime = React.useRef<number | null>(null);
|
||||||
|
|
||||||
const startRecording = async () => {
|
const startRecording = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!focusModeActive && onFocusModeToggle) {
|
||||||
|
onFocusModeToggle();
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
mediaRecorderRef.current = new MediaRecorder(stream);
|
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||||
chunksRef.current = [];
|
chunksRef.current = [];
|
||||||
@ -34,12 +49,14 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
mediaRecorderRef.current.onstop = () => {
|
mediaRecorderRef.current.onstop = () => {
|
||||||
const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
||||||
setAudioBlob(audioBlob);
|
setAudioBlob(audioBlob);
|
||||||
|
onRecordingStop?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaRecorderRef.current.start();
|
mediaRecorderRef.current.start();
|
||||||
startTime.current = Date.now();
|
startTime.current = Date.now();
|
||||||
setIsRecording(true);
|
setIsRecording(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
onRecordingStart?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Erro ao acessar microfone. Verifique as permissões.');
|
setError('Erro ao acessar microfone. Verifique as permissões.');
|
||||||
console.error('Erro ao iniciar gravação:', err);
|
console.error('Erro ao iniciar gravação:', err);
|
||||||
@ -51,8 +68,12 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
mediaRecorderRef.current.stop();
|
mediaRecorderRef.current.stop();
|
||||||
setIsRecording(false);
|
setIsRecording(false);
|
||||||
|
|
||||||
// Parar todas as tracks do stream
|
|
||||||
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
|
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
|
// Desativar modo foco ao parar a gravação
|
||||||
|
if (focusModeActive && onFocusModeToggle) {
|
||||||
|
onFocusModeToggle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,7 +133,7 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
.getPublicUrl(filePath);
|
.getPublicUrl(filePath);
|
||||||
|
|
||||||
// 3. Criar o registro com a URL do áudio
|
// 3. Criar o registro com a URL do áudio
|
||||||
const { data: recordData, error: recordError } = await supabase
|
const { error: recordError } = await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.insert({
|
.insert({
|
||||||
id: fileId,
|
id: fileId,
|
||||||
@ -152,15 +173,23 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-white rounded-lg shadow">
|
<div className={cn(
|
||||||
|
"p-4 bg-white rounded-lg shadow",
|
||||||
|
focusModeActive && "bg-purple-50"
|
||||||
|
)}>
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
{!isRecording && !audioBlob && (
|
{!isRecording && !audioBlob && (
|
||||||
<button
|
<button
|
||||||
onClick={startRecording}
|
onClick={startRecording}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-4 py-2 rounded-lg transition",
|
||||||
|
focusModeActive
|
||||||
|
? "bg-purple-600 text-white hover:bg-purple-700"
|
||||||
|
: "bg-red-600 text-white hover:bg-red-700"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Mic className="w-5 h-5" />
|
<Mic className="w-5 h-5" />
|
||||||
Iniciar Gravação
|
{focusModeActive ? "Iniciar Leitura" : "Iniciar Gravação"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, ArrowRight, Share2, ChevronDown, ChevronUp, Loader2, Download, RefreshCw, Trash2, Type } from 'lucide-react';
|
import { ArrowLeft, ArrowRight, Share2, ChevronDown, ChevronUp, Loader2, Download, RefreshCw, Trash2, Type, Eye, EyeOff } from 'lucide-react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||||
@ -36,6 +36,17 @@ interface StoryRecording {
|
|||||||
transcription: string;
|
transcription: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FocusMode {
|
||||||
|
isActive: boolean;
|
||||||
|
isRecording: boolean;
|
||||||
|
originalState: {
|
||||||
|
isUpperCase: boolean;
|
||||||
|
isHighlighting: boolean;
|
||||||
|
fontSize: number;
|
||||||
|
imageVisible: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
const [isAudioSupported, setIsAudioSupported] = React.useState(true);
|
const [isAudioSupported, setIsAudioSupported] = React.useState(true);
|
||||||
@ -384,13 +395,23 @@ export function StoryPage() {
|
|||||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||||
const { isHighlighted: isSyllablesEnabled, toggleHighlight: toggleSyllables } = useSyllables();
|
const { isHighlighted: isSyllablesEnabled, toggleHighlight: toggleSyllables } = useSyllables();
|
||||||
const [fontSize, setFontSize] = useState(18);
|
const [fontSize, setFontSize] = useState(18);
|
||||||
const [readingSpeed, setReadingSpeed] = useState(120); // 120 palavras por minuto
|
const [readingSpeed, setReadingSpeed] = useState(60); // 60 palavras por minuto
|
||||||
const [letterSpacing, setLetterSpacing] = useState(0);
|
const [letterSpacing, setLetterSpacing] = useState(0);
|
||||||
const [wordSpacing, setWordSpacing] = useState(0);
|
const [wordSpacing, setWordSpacing] = useState(0);
|
||||||
const [lineHeight, setLineHeight] = useState(1.5);
|
const [lineHeight, setLineHeight] = useState(1.5);
|
||||||
const [isHighlighting, setIsHighlighting] = useState(false);
|
const [isHighlighting, setIsHighlighting] = useState(false);
|
||||||
const [currentWordIndex, setCurrentWordIndex] = useState(-1);
|
const [currentWordIndex, setCurrentWordIndex] = useState(-1);
|
||||||
const highlightInterval = React.useRef<number | null>(null);
|
const highlightInterval = React.useRef<number | null>(null);
|
||||||
|
const [focusMode, setFocusMode] = useState<FocusMode>({
|
||||||
|
isActive: false,
|
||||||
|
isRecording: false,
|
||||||
|
originalState: {
|
||||||
|
isUpperCase: false,
|
||||||
|
isHighlighting: false,
|
||||||
|
fontSize: 18,
|
||||||
|
imageVisible: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Função para dividir o texto em palavras
|
// Função para dividir o texto em palavras
|
||||||
const getWords = (text: string) => text.split(/\s+/);
|
const getWords = (text: string) => text.split(/\s+/);
|
||||||
@ -642,6 +663,55 @@ export function StoryPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFocusModeToggle = () => {
|
||||||
|
if (!focusMode.isActive) {
|
||||||
|
// Salvar estado atual antes de ativar o modo foco
|
||||||
|
setFocusMode(prev => ({
|
||||||
|
...prev,
|
||||||
|
isActive: true,
|
||||||
|
originalState: {
|
||||||
|
isUpperCase,
|
||||||
|
isHighlighting,
|
||||||
|
fontSize,
|
||||||
|
imageVisible: true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Aplicar configurações do modo foco
|
||||||
|
toggleUppercase();
|
||||||
|
setIsHighlighting(true);
|
||||||
|
setFontSize(24);
|
||||||
|
} else {
|
||||||
|
// Restaurar estado original
|
||||||
|
if (isUpperCase !== focusMode.originalState.isUpperCase) {
|
||||||
|
toggleUppercase();
|
||||||
|
}
|
||||||
|
setIsHighlighting(focusMode.originalState.isHighlighting);
|
||||||
|
setFontSize(focusMode.originalState.fontSize);
|
||||||
|
|
||||||
|
// Resetar modo foco
|
||||||
|
setFocusMode(prev => ({
|
||||||
|
...prev,
|
||||||
|
isActive: false,
|
||||||
|
isRecording: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecordingStart = () => {
|
||||||
|
setFocusMode(prev => ({
|
||||||
|
...prev,
|
||||||
|
isRecording: true
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecordingStop = () => {
|
||||||
|
setFocusMode(prev => ({
|
||||||
|
...prev,
|
||||||
|
isRecording: false
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="animate-pulse">
|
<div className="animate-pulse">
|
||||||
@ -678,6 +748,27 @@ export function StoryPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handleFocusModeToggle}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors',
|
||||||
|
'text-gray-600 hover:text-gray-900 border border-gray-200',
|
||||||
|
focusMode.isActive && 'bg-purple-50 text-purple-600 border-purple-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{focusMode.isActive ? (
|
||||||
|
<>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Sair do Modo Foco</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Modo Foco</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
<TextCaseToggle
|
<TextCaseToggle
|
||||||
isUpperCase={isUpperCase}
|
isUpperCase={isUpperCase}
|
||||||
onToggle={toggleUppercase}
|
onToggle={toggleUppercase}
|
||||||
@ -703,9 +794,16 @@ export function StoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Conteúdo Principal */}
|
{/* Conteúdo Principal */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
<div className={cn(
|
||||||
|
"bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6",
|
||||||
|
"transition-all duration-300",
|
||||||
|
focusMode.isActive && "bg-gray-50"
|
||||||
|
)}>
|
||||||
{/* 1. Controles de Texto */}
|
{/* 1. Controles de Texto */}
|
||||||
<div className="border-b border-gray-200 pb-6">
|
<div className={cn(
|
||||||
|
"border-b border-gray-200 pb-6",
|
||||||
|
focusMode.isActive && "opacity-50 pointer-events-none"
|
||||||
|
)}>
|
||||||
<TextControls
|
<TextControls
|
||||||
isUpperCase={isUpperCase}
|
isUpperCase={isUpperCase}
|
||||||
onToggleUpperCase={toggleUppercase}
|
onToggleUpperCase={toggleUppercase}
|
||||||
@ -774,7 +872,8 @@ export function StoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. Imagem da História */}
|
{/* 5. Imagem da História - Agora com controle de visibilidade */}
|
||||||
|
{!focusMode.isActive && (
|
||||||
<div className="border-b border-gray-200 pb-6">
|
<div className="border-b border-gray-200 pb-6">
|
||||||
{story?.content?.pages?.[currentPage]?.image ? (
|
{story?.content?.pages?.[currentPage]?.image ? (
|
||||||
<ImageWithLoading
|
<ImageWithLoading
|
||||||
@ -788,11 +887,14 @@ export function StoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 6. Controle de Gravação */}
|
{/* 6. Controle de Gravação */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Gravação de Áudio</h2>
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
|
{focusMode.isActive ? "Modo Foco Ativado" : "Gravação de Áudio"}
|
||||||
|
</h2>
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
storyId={story.id}
|
storyId={story.id}
|
||||||
studentId={session?.user?.id || ''}
|
studentId={session?.user?.id || ''}
|
||||||
@ -816,7 +918,16 @@ export function StoryPage() {
|
|||||||
transcription: ''
|
transcription: ''
|
||||||
};
|
};
|
||||||
setRecordings(prev => [newRecording, ...prev]);
|
setRecordings(prev => [newRecording, ...prev]);
|
||||||
|
|
||||||
|
// Desativar modo foco após finalizar gravação
|
||||||
|
if (focusMode.isActive) {
|
||||||
|
handleFocusModeToggle();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
onRecordingStart={handleRecordingStart}
|
||||||
|
onRecordingStop={handleRecordingStop}
|
||||||
|
focusModeActive={focusMode.isActive}
|
||||||
|
onFocusModeToggle={handleFocusModeToggle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
100
src/styles/focus-mode.css
Normal file
100
src/styles/focus-mode.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/* Estilos para o Modo Foco */
|
||||||
|
|
||||||
|
.focus-mode-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-active {
|
||||||
|
background-color: #f8f9fc;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-text {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.8;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
word-spacing: 2px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-controls {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 9999px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-highlight {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-timer {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 9999px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b5563;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-transition {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsividade */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.focus-mode-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-text {
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-controls {
|
||||||
|
bottom: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-mode-timer {
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user