re-design
All checks were successful
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m5s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 33s

This commit is contained in:
2026-06-12 06:13:19 -03:00
parent 21186c9270
commit 83867e4255
19 changed files with 1869 additions and 816 deletions

View File

@@ -5,6 +5,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..950&display=swap" rel="stylesheet" />
<title>Mindforge</title> <title>Mindforge</title>
</head> </head>

View File

@@ -3,60 +3,23 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: fadeIn 0.8s ease-out; padding: 2rem 1rem;
} }
.hero-title { .hero-title {
font-size: 4rem; font-family: Georgia, "Times New Roman", serif;
font-weight: 700; font-size: clamp(32px, 5vw, 56px);
text-transform: uppercase; font-weight: 800;
letter-spacing: 2px; letter-spacing: -.03em;
margin-bottom: 1rem; color: var(--ink);
text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); margin-bottom: 0.5rem;
background: linear-gradient(90deg, #f4f5f5, #00b4d8);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
} }
.hero-subtitle { .hero-subtitle {
font-size: 1.5rem; font-size: 18px;
color: rgba(244, 245, 245, 0.8); color: var(--muted);
font-weight: 300; font-weight: 400;
letter-spacing: 1px; max-width: 480px;
}
.module-content {
width: 100%;
max-width: 800px;
margin: 0 auto;
animation: slideUp 0.5s ease-out;
}
.subtitle {
font-size: 1.2rem;
color: rgba(244, 245, 245, 0.8);
margin-bottom: 2rem;
}
.placeholder-box {
background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 4rem 2rem;
font-size: 1.5rem;
color: rgba(244, 245, 245, 0.5);
text-align: center; text-align: center;
backdrop-filter: blur(4px); line-height: 1.6;
-webkit-backdrop-filter: blur(4px);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
} }

View File

@@ -11,32 +11,30 @@ export function App() {
const [activeModule, setActiveModule] = useState<'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada'>('home'); const [activeModule, setActiveModule] = useState<'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada'>('home');
return ( return (
<> <div class="app">
<Header onGoHome={() => setActiveModule('home')} />
<div class="main-layout">
<Sidebar activeModule={activeModule} onModuleChange={setActiveModule} /> <Sidebar activeModule={activeModule} onModuleChange={setActiveModule} />
<main class="content-area"> <div class="main">
<Header onGoHome={() => setActiveModule('home')} />
<div style={{ display: activeModule === 'home' || !activeModule ? 'block' : 'none' }}> <div style={{ display: activeModule === 'home' || !activeModule ? 'block' : 'none' }}>
<div class="home-hero"> <div class="home-hero">
<img src="/assets/mindforge-banner.png" alt="Mindforge Banner" style={{ maxWidth: '100%', height: 'auto', marginBottom: '2rem', borderRadius: '12px', boxShadow: '0 4px 15px rgba(0,0,0,0.5)', zIndex: -10 }} /> <img src="/assets/mindforge-banner.png" alt="Mindforge Banner" style={{ maxWidth: '100%', height: 'auto', marginBottom: '1.5rem', borderRadius: '16px', boxShadow: '0 20px 60px rgba(76,48,12,.15)' }} />
<h1 class="hero-title">Mindforge - Forja Mental</h1> <h1 class="hero-title">Mindforge Forja Mental</h1>
<p class="hero-subtitle">Sua ferramenta de estudos para concursos.</p> <p class="hero-subtitle">Sua ferramenta de estudos para concursos.</p>
</div> </div>
</div> </div>
<div style={{ display: activeModule === 'verificador' ? 'block' : 'none', height: '100%', width: '100%' }}> <div style={{ display: activeModule === 'verificador' ? 'block' : 'none', width: '100%' }}>
<VerificadorComponent /> <VerificadorComponent />
</div> </div>
<div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}> <div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', width: '100%' }}>
<FlashcardComponent /> <FlashcardComponent />
</div> </div>
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}> <div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', width: '100%' }}>
<FlashcardReviewComponent /> <FlashcardReviewComponent />
</div> </div>
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', height: '100%', width: '100%' }}> <div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', width: '100%' }}>
<SpacedReviewComponent /> <SpacedReviewComponent />
</div> </div>
</main>
</div> </div>
</> </div>
); );
} }

View File

@@ -1,32 +1,30 @@
.btn { .btn {
font-family: var(--font-main); font-family: inherit;
font-weight: 700; font-weight: 850;
font-size: 1rem; font-size: 13px;
padding: 0.8rem 1.5rem; padding: 0 18px;
border-radius: 8px; min-height: 42px;
border: none; border-radius: 999px;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
transition: all 0.3s ease; transition: .25s var(--ease);
backdrop-filter: blur(8px); letter-spacing: 0;
-webkit-backdrop-filter: blur(8px); display: inline-flex;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); align-items: center;
letter-spacing: 0.5px; justify-content: center;
gap: 9px;
border: 1px solid rgba(96, 67, 28, .15);
background: rgba(255,255,255,.62);
color: #4b3b27;
box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
} }
.btn-primary { .btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-creamy);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 20px rgba(86, 57, 17, .12), inset 0 1px 0 rgba(255,255,255,.72);
} }
.btn-primary:active { .btn:active {
transform: translateY(0); transform: translateY(0);
} }
@@ -37,12 +35,21 @@
box-shadow: none; box-shadow: none;
} }
.btn-secondary { .btn-primary {
background: transparent; background: linear-gradient(135deg, var(--blue), var(--blue-deep));
color: var(--color-text-creamy); color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1); border-color: rgba(37, 95, 141, .3);
box-shadow: 0 16px 34px rgba(63, 44, 20, .18), inset 0 1px 0 rgba(255,255,255,.28);
font-weight: 950;
font-size: 14px;
min-height: 48px;
} }
.btn-secondary:hover { .btn-primary:hover {
background: rgba(255, 255, 255, 0.05); transform: translateY(-3px);
box-shadow: 0 22px 42px rgba(63, 44, 20, .22), inset 0 1px 0 rgba(255,255,255,.28);
}
.btn-secondary {
background: rgba(255,255,255,.45);
} }

View File

@@ -1,11 +1,13 @@
.file-tree { .file-tree {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 250, 239, .68);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(104, 69, 22, .13);
border-radius: 8px; border-radius: var(--radius-md);
padding: 0.75rem; padding: 0.75rem;
max-height: 380px; max-height: 380px;
overflow-y: auto; overflow-y: auto;
font-size: 0.9rem; font-size: 0.9rem;
box-shadow: 0 12px 32px rgba(86, 57, 17, .06);
backdrop-filter: blur(12px);
} }
.tree-folder { .tree-folder {
@@ -17,30 +19,30 @@
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 4px; border-radius: 8px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
color: rgba(255, 255, 255, 0.85); color: var(--ink);
font-weight: 600; font-weight: 700;
} }
.tree-folder-header:hover { .tree-folder-header:hover {
background: rgba(255, 255, 255, 0.07); background: rgba(255,255,255,.52);
} }
.tree-folder-arrow { .tree-folder-arrow {
font-size: 0.75rem; font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5); color: var(--muted);
width: 12px; width: 12px;
} }
.tree-folder-name { .tree-folder-name {
color: rgba(255, 255, 255, 0.85); color: var(--ink);
} }
.tree-folder-children { .tree-folder-children {
padding-left: 18px; padding-left: 18px;
border-left: 1px solid rgba(255, 255, 255, 0.08); border-left: 1px solid var(--line);
margin-left: 6px; margin-left: 6px;
} }
@@ -53,18 +55,18 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 4px 6px; padding: 4px 6px;
border-radius: 4px; border-radius: 8px;
cursor: pointer; cursor: pointer;
color: rgba(255, 255, 255, 0.7); color: var(--muted);
} }
.tree-file-label:hover { .tree-file-label:hover {
background: rgba(255, 255, 255, 0.06); background: rgba(255,255,255,.45);
color: rgba(255, 255, 255, 0.9); color: var(--ink);
} }
.tree-file-label input[type="checkbox"] { .tree-file-label input[type="checkbox"] {
accent-color: var(--color-accent); accent-color: var(--blue);
width: 14px; width: 14px;
height: 14px; height: 14px;
cursor: pointer; cursor: pointer;
@@ -79,11 +81,11 @@
.tree-error, .tree-error,
.tree-empty { .tree-empty {
padding: 1rem; padding: 1rem;
color: rgba(255, 255, 255, 0.5); color: var(--muted);
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
} }
.tree-error { .tree-error {
color: #ff7b72; color: var(--red);
} }

View File

@@ -2,27 +2,37 @@
width: 100%; width: 100%;
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
animation: slideUp 0.5s ease-out;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.flashcard-title { .flashcard-title {
font-size: 2.5rem; font-family: Georgia, "Times New Roman", serif;
font-size: clamp(24px, 3.5vw, 40px);
font-weight: 800;
letter-spacing: -.03em;
color: var(--ink);
margin: 0;
}
.flashcard-subtitle {
color: var(--muted);
font-size: 15px;
line-height: 1.55;
max-width: 660px;
} }
.flashcard-form { .flashcard-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.2rem; gap: 1.2rem;
background: rgba(255, 255, 255, 0.06); background: rgba(255, 250, 239, .68);
padding: 2rem; padding: 2rem;
border-radius: 12px; border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(104, 69, 22, .13);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2); box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(10px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(10px);
} }
.input-group { .input-group {
@@ -34,12 +44,13 @@
.input-group label { .input-group label {
font-weight: 700; font-weight: 700;
color: var(--color-text-creamy); color: var(--ink);
font-size: 0.9rem;
} }
.selection-meta { .selection-meta {
font-size: 0.85rem; font-size: 0.85rem;
color: rgba(255, 255, 255, 0.55); color: var(--muted);
margin-top: 0.4rem; margin-top: 0.4rem;
} }
@@ -55,7 +66,7 @@
appearance: none; appearance: none;
width: 100%; width: 100%;
height: 8px; height: 8px;
background: rgba(0, 0, 0, 0.3); background: rgba(95, 72, 35, .14);
border-radius: 999px; border-radius: 999px;
outline: none; outline: none;
} }
@@ -66,34 +77,37 @@
width: 22px; width: 22px;
height: 22px; height: 22px;
border-radius: 50%; border-radius: 50%;
background: #f4f5f5; background: var(--paper);
border: 2px solid rgba(var(--color-accent-rgb), 0.9); border: 2px solid var(--blue);
box-shadow: 0 6px 16px rgba(var(--color-accent-rgb), 0.35); box-shadow: 0 6px 16px rgba(63, 124, 172, .25);
cursor: pointer; cursor: pointer;
} }
.amount-display { .amount-display {
font-weight: 700; font-weight: 800;
color: var(--color-accent); color: var(--blue-deep);
min-width: 44px; min-width: 44px;
text-align: right; text-align: right;
font-size: 1.1rem; font-size: 1.1rem;
} }
.flashcard-error { .flashcard-error {
color: #ff9c96; color: var(--red);
margin-top: 1rem; font-size: 0.9rem;
background: rgba(183, 91, 77, 0.08);
border: 1px solid rgba(183, 91, 77, 0.2);
border-radius: var(--radius-md);
padding: 0.8rem 1rem;
} }
.radio-group { .radio-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
background: rgba(0, 0, 0, 0.28); background: rgba(95, 72, 35, .08);
padding: 4px; padding: 4px;
border-radius: 12px; border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid var(--line);
width: fit-content; width: fit-content;
overflow: hidden;
gap: 4px; gap: 4px;
} }
@@ -115,54 +129,57 @@
justify-content: center; justify-content: center;
padding: 0.8rem 1.2rem; padding: 0.8rem 1.2rem;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 12px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 700; font-weight: 800;
color: rgba(255, 255, 255, 0.65); color: var(--muted);
transition: all 0.25s ease; transition: all .25s var(--ease);
white-space: nowrap; white-space: nowrap;
} }
.radio-item input[type="radio"]:checked + .radio-label { .radio-item input[type="radio"]:checked + .radio-label {
background: var(--color-accent); background: var(--blue);
color: #012f3b; color: #fff;
box-shadow: 0 4px 12px rgba(var(--color-accent-rgb), 0.35); box-shadow: 0 8px 20px rgba(63, 124, 172, .25);
} }
.radio-item:hover .radio-label { .radio-item:hover .radio-label {
color: rgba(255, 255, 255, 0.92); color: var(--ink);
} }
.spinner-container { .loading-indicator {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 10px;
justify-content: center; justify-content: center;
padding: 2rem 0; padding: 2rem;
gap: 1rem; color: var(--muted);
font-size: 0.95rem;
} }
.spinner { .loading-dot {
width: 50px; width: 10px;
height: 50px; height: 10px;
border: 4px solid rgba(255, 255, 255, 0.12);
border-left-color: var(--color-accent);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; background: var(--blue);
box-shadow: 0 0 0 4px rgba(63, 124, 172, .16);
} }
.flashcard-result-panel { .flashcard-result-panel {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 250, 239, .68);
border-radius: 12px; border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(104, 69, 22, .13);
padding: 1.5rem; padding: 1.5rem;
text-align: left; text-align: left;
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
} }
.flashcard-result-panel h3 { .flashcard-result-panel h3 {
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
font-size: 1.1rem; font-size: 1.1rem;
color: rgba(255, 255, 255, 0.94); font-family: Georgia, serif;
color: var(--ink);
} }
.flashcard-result-list { .flashcard-result-list {
@@ -172,9 +189,9 @@
} }
.flashcard-result-item { .flashcard-result-item {
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(82, 54, 17, .12);
background: rgba(0, 0, 0, 0.2); background: rgba(255,255,255,.52);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.85rem 1rem; padding: 0.85rem 1rem;
} }
@@ -184,10 +201,12 @@
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--ink);
} }
.flashcard-result-header span { .flashcard-result-header span {
color: rgba(255, 255, 255, 0.72); color: var(--muted);
font-size: 0.85rem;
} }
.flashcard-result-meta { .flashcard-result-meta {
@@ -196,16 +215,7 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.85rem; gap: 0.85rem;
font-size: 0.85rem; font-size: 0.85rem;
color: rgba(255, 255, 255, 0.65); color: var(--muted);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
@media (max-width: 720px) { @media (max-width: 720px) {

View File

@@ -49,8 +49,8 @@ export function FlashcardComponent() {
return ( return (
<div className="flashcard-container"> <div className="flashcard-container">
<h2 className="title flashcard-title">Gerador de Flashcards</h2> <h2 className="flashcard-title">Gerador de Flashcards</h2>
<p className="subtitle">Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.</p> <p className="flashcard-subtitle">Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.</p>
<div className="flashcard-form"> <div className="flashcard-form">
<div className="input-group"> <div className="input-group">
@@ -111,7 +111,7 @@ export function FlashcardComponent() {
</div> </div>
</div> </div>
<Button variant="primary" onClick={handleGenerate} disabled={loading} style={{ marginTop: '1rem' }}> <Button variant="primary" onClick={handleGenerate} disabled={loading} style={{ marginTop: '0.5rem' }}>
{loading ? 'Gerando...' : 'Gerar Bibliotecas de Flashcards'} {loading ? 'Gerando...' : 'Gerar Bibliotecas de Flashcards'}
</Button> </Button>
@@ -119,9 +119,9 @@ export function FlashcardComponent() {
</div> </div>
{loading && ( {loading && (
<div className="spinner-container"> <div class="loading-indicator">
<div className="spinner"></div> <span class="loading-dot"></span>
<p>Gerando os flashcards com IA e salvando no banco. Aguarde...</p> <span>Gerando os flashcards com IA e salvando no banco. Aguarde...</span>
</div> </div>
)} )}

View File

@@ -1,40 +1,47 @@
.review-container { .review-container {
width: 100%; width: 100%;
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.2rem;
animation: slideUp 0.45s ease-out;
} }
.review-title { .review-title {
font-size: 2.35rem; font-family: Georgia, "Times New Roman", serif;
font-size: clamp(24px, 3.5vw, 40px);
font-weight: 800;
letter-spacing: -.03em;
color: var(--ink);
margin: 0 0 0.3rem;
}
.review-subtitle {
color: var(--muted);
font-size: 15px;
line-height: 1.55;
max-width: 660px;
margin-bottom: 1.2rem;
} }
.review-error { .review-error {
color: #ff9c96; color: var(--red);
text-align: left; font-size: 0.9rem;
background: rgba(255, 69, 58, 0.12); background: rgba(183, 91, 77, 0.08);
border: 1px solid rgba(255, 69, 58, 0.4); border: 1px solid rgba(183, 91, 77, 0.2);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.8rem 1rem; padding: 0.8rem 1rem;
margin-bottom: 1rem;
} }
.review-select-panel, /* Library Selection */
.review-session-panel { .review-select-panel {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 250, 239, .68);
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(104, 69, 22, .13);
border-radius: 12px; border-radius: var(--radius-lg);
padding: 1.25rem; padding: 1.5rem;
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
text-align: left; text-align: left;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.18);
} }
.review-state { .review-state {
color: rgba(255, 255, 255, 0.72); color: var(--muted);
font-size: 0.95rem; font-size: 0.95rem;
} }
@@ -44,16 +51,17 @@
} }
.review-subject-section { .review-subject-section {
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(82, 54, 17, .10);
background: rgba(0, 0, 0, 0.18); background: rgba(255,255,255,.45);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.9rem; padding: 0.9rem;
} }
.review-subject-section h3 { .review-subject-section h3 {
margin: 0 0 0.8rem; margin: 0 0 0.8rem;
font-size: 1rem; font-size: 1rem;
color: rgba(255, 255, 255, 0.95); font-family: Georgia, serif;
color: var(--ink);
} }
.review-library-list { .review-library-list {
@@ -65,18 +73,18 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 0.7rem; gap: 0.7rem;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(82, 54, 17, .10);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.6rem 0.7rem; padding: 0.6rem 0.7rem;
cursor: pointer; cursor: pointer;
background: rgba(255, 255, 255, 0.03); background: rgba(255,255,255,.48);
} }
.review-library-item input[type="checkbox"] { .review-library-item input[type="checkbox"] {
margin-top: 0.15rem; margin-top: 0.15rem;
width: 16px; width: 16px;
height: 16px; height: 16px;
accent-color: var(--color-accent); accent-color: var(--blue);
} }
.review-library-texts { .review-library-texts {
@@ -87,10 +95,11 @@
.review-library-texts strong { .review-library-texts strong {
font-size: 0.95rem; font-size: 0.95rem;
color: var(--ink);
} }
.review-library-texts span { .review-library-texts span {
color: rgba(255, 255, 255, 0.68); color: var(--muted);
font-size: 0.84rem; font-size: 0.84rem;
} }
@@ -100,72 +109,548 @@
justify-content: flex-end; justify-content: flex-end;
} }
.review-progress { /* Session Panel - Content Grid */
display: flex; .content-grid {
align-items: center; display: grid;
gap: 0.9rem; grid-template-columns: minmax(0, 1fr) 330px;
margin-bottom: 0.8rem; gap: 24px;
align-items: start;
} }
.review-progress span { /* Review Panel (main flashcard area) */
font-size: 0.9rem; .review-panel {
color: rgba(255, 255, 255, 0.8); position: relative;
min-width: 60px; min-height: 650px;
} padding: clamp(18px, 3vw, 34px);
border-radius: var(--radius-xl);
.review-progress-bar { background:
width: 100%; linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,240,202,.46)),
height: 8px; radial-gradient(circle at 15% 20%, rgba(255, 201, 101, .25), transparent 35%),
border-radius: 999px; radial-gradient(circle at 90% 10%, rgba(63, 124, 172, .16), transparent 32%);
background: rgba(0, 0, 0, 0.25); border: 1px solid rgba(104, 69, 22, .13);
box-shadow: var(--shadow);
overflow: hidden; overflow: hidden;
} }
.review-progress-fill { .review-panel::before,
height: 100%; .review-panel::after {
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.8), rgba(var(--color-accent-rgb), 1)); content: "";
position: absolute;
border-radius: 999px; border-radius: 999px;
transition: width 0.25s ease; pointer-events: none;
filter: blur(2px);
opacity: .5;
} }
.review-card { .review-panel::before {
border: 1px solid rgba(255, 255, 255, 0.16); width: 220px;
border-radius: 10px; height: 220px;
padding: 1rem; right: -92px;
background: rgba(0, 0, 0, 0.2); top: 100px;
min-height: 250px; background: rgba(63, 124, 172, .13);
} }
.review-card header { .review-panel::after {
margin-bottom: 0.8rem; width: 180px;
height: 180px;
left: -70px;
bottom: 60px;
background: rgba(199, 149, 57, .18);
} }
.review-card small { .session-header {
color: rgba(255, 255, 255, 0.62); position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
} }
.review-card h3 { .session-title h3 {
margin: 0 0 0.4rem; margin: 0;
font-size: 0.98rem; font-family: Georgia, serif;
color: rgba(255, 255, 255, 0.9); font-size: clamp(24px, 3vw, 36px);
letter-spacing: -.04em;
color: var(--ink);
} }
.review-card p { .session-title p {
margin: 0 0 1rem; max-width: 660px;
font-size: 1rem; color: var(--muted);
font-size: 15px;
line-height: 1.55;
margin: 4px 0 0;
}
.score-pill {
min-width: 76px;
padding: 11px 12px;
border-radius: 18px;
background: rgba(255,255,255,.55);
border: 1px solid rgba(100, 65, 18, .12);
text-align: center;
box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
}
.score-pill b {
display: block;
font-size: 19px;
color: var(--ink);
}
.score-pill span {
color: var(--muted);
font-size: 10px;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
}
/* Stage (flashcard container) */
.stage {
position: relative;
z-index: 1;
display: grid;
place-items: center;
min-height: 390px;
perspective: 1400px;
padding: 22px 0;
}
/* Flashcard */
.flashcard {
position: relative;
width: min(680px, 100%);
min-height: 355px;
cursor: pointer;
transform-style: preserve-3d;
transition: transform .78s var(--ease), filter .35s var(--ease);
outline: none;
}
.flashcard:hover .card-face {
border-color: rgba(63, 124, 172, .28);
}
.flashcard.is-flipped {
transform: rotateY(180deg);
}
.flashcard.is-reviewed {
animation: cardExit .58s var(--ease);
}
.card-face {
position: absolute;
inset: 0;
min-height: 355px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: clamp(24px, 5vw, 42px);
border-radius: 30px;
backface-visibility: hidden;
background:
linear-gradient(90deg, rgba(168, 111, 36, .09) 0 1px, transparent 1px 22px),
linear-gradient(rgba(168, 111, 36, .08) 0 1px, transparent 1px 30px),
linear-gradient(145deg, #fffaf0, #f5dfaa);
border: 1px solid rgba(82, 54, 17, .18);
box-shadow: var(--card-shadow), inset 0 0 0 8px rgba(255,255,255,.24);
overflow: hidden;
}
.card-face::before {
content: "";
position: absolute;
inset: 18px;
border: 1px dashed rgba(82, 54, 17, .18);
border-radius: 22px;
pointer-events: none;
}
.card-face::after {
content: "";
position: absolute;
width: 140px;
height: 140px;
right: -46px;
bottom: -50px;
border-radius: 50%;
background: radial-gradient(circle, rgba(199,149,57,.25), transparent 66%);
pointer-events: none;
}
.card-back {
transform: rotateY(180deg);
background:
linear-gradient(90deg, rgba(63,124,172,.08) 0 1px, transparent 1px 22px),
linear-gradient(rgba(63,124,172,.07) 0 1px, transparent 1px 30px),
linear-gradient(145deg, #fffaf1, #dfeef2);
}
.card-meta {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
color: var(--muted);
font-size: 12px;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
}
.tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
border-radius: 999px;
background: rgba(255,255,255,.54);
border: 1px solid rgba(82, 54, 17, .12);
}
.card-question,
.card-answer {
position: relative;
z-index: 1;
display: grid;
align-content: center;
gap: 12px;
min-height: 190px;
color: var(--ink);
font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: clamp(17px, 2.2vw, 20px);
line-height: 1.55; line-height: 1.55;
} }
.review-session-actions { .card-footer {
margin-top: 1rem; position: relative;
z-index: 1;
display: flex; display: flex;
gap: 0.65rem; justify-content: space-between;
flex-wrap: wrap; gap: 14px;
align-items: center;
color: var(--muted);
font-size: 13px;
font-weight: 800;
} }
@media (max-width: 740px) { .spacebar {
.review-session-actions { padding: 4px 9px;
border-radius: 8px;
color: #4f3a1d;
background: rgba(255,255,255,.58);
border: 1px solid rgba(82, 54, 17, .14);
box-shadow: inset 0 -2px 0 rgba(82, 54, 17, .08);
font-size: 11px;
font-weight: 950;
letter-spacing: .08em;
text-transform: uppercase;
}
/* Controls */
.controls {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
opacity: .36;
transform: translateY(10px);
pointer-events: none;
transition: .35s var(--ease);
margin-top: 20px;
}
.controls.ready {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.review-button {
position: relative;
min-width: 170px;
min-height: 60px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 0;
border-radius: 20px;
color: white;
font-family: inherit;
font-weight: 950;
font-size: 16px;
cursor: pointer;
box-shadow: 0 16px 34px rgba(63, 44, 20, .18), inset 0 1px 0 rgba(255,255,255,.32);
overflow: hidden;
transition: .25s var(--ease);
}
.review-button.correct {
background: linear-gradient(135deg, var(--green), var(--green-deep));
}
.review-button.wrong {
background: linear-gradient(135deg, var(--red), var(--red-deep));
}
.review-button:hover {
transform: translateY(-4px);
box-shadow: 0 22px 42px rgba(63, 44, 20, .22), inset 0 1px 0 rgba(255,255,255,.32);
}
.review-button::before {
content: "";
position: absolute;
inset: -90% -40%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent);
transform: rotate(20deg) translateX(-80%);
transition: .55s var(--ease);
}
.review-button:hover::before {
transform: rotate(20deg) translateX(80%);
}
.review-button:disabled {
opacity: 0.45;
cursor: not-allowed;
transform: none !important;
}
/* Stamp */
.stamp {
position: absolute;
right: 38px;
top: 36px;
z-index: 4;
padding: 12px 18px;
border: 4px double currentColor;
border-radius: 10px;
font-family: Georgia, serif;
font-size: 26px;
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
opacity: 0;
transform: rotate(-12deg) scale(1.3);
pointer-events: none;
}
.stamp.correct {
color: var(--green-deep);
}
.stamp.wrong {
color: var(--red-deep);
}
.stamp.show {
animation: stampIn .7s var(--ease);
}
/* Side Panel */
.side-panel {
display: grid; display: grid;
gap: 18px;
}
.panel-card {
padding: 20px;
border-radius: 26px;
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
}
.panel-card h3 {
margin: 0 0 14px;
font-family: Georgia, serif;
font-size: 22px;
letter-spacing: -.03em;
color: var(--ink);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.stat {
padding: 14px;
border-radius: 18px;
background: rgba(255,255,255,.52);
border: 1px solid rgba(82, 54, 17, .10);
}
.stat b {
display: block;
font-size: 24px;
letter-spacing: -.04em;
color: var(--ink);
}
.stat span {
color: var(--muted);
font-size: 11px;
font-weight: 950;
letter-spacing: .10em;
text-transform: uppercase;
}
.track {
height: 14px;
border-radius: 999px;
overflow: hidden;
background: rgba(80, 54, 18, .12);
margin-top: 8px;
}
.track span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
}
.queue {
display: grid;
gap: 10px;
}
.queue-item {
display: grid;
grid-template-columns: 38px minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
padding: 10px;
border-radius: 16px;
background: rgba(255,255,255,.48);
border: 1px solid rgba(82, 54, 17, .10);
}
.queue-item strong {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--ink);
font-size: 13px;
}
.queue-item span {
color: var(--muted);
font-size: 11px;
}
.queue-number {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 13px;
background: #fff5d8;
border: 1px solid rgba(82, 54, 17, .12);
color: #74531c;
font-family: Georgia, serif;
font-weight: 900;
font-size: 14px;
}
/* Confetti Canvas */
.confetti-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
}
/* Keyframes */
@keyframes cardExit {
0% { transform: translateX(0) rotateY(180deg) rotateZ(0); opacity: 1; }
45% { transform: translateX(28px) rotateY(180deg) rotateZ(2deg); opacity: .9; }
100% { transform: translateX(-32px) rotateY(180deg) rotateZ(-2deg); opacity: 0; }
}
@keyframes stampIn {
0% { opacity: 0; transform: rotate(-18deg) scale(1.8); }
38% { opacity: 1; transform: rotate(-10deg) scale(.9); }
58% { transform: rotate(-12deg) scale(1.04); }
100% { opacity: 0; transform: rotate(-12deg) scale(1); }
}
/* Session End */
.session-end {
text-align: center;
padding: 3rem 1rem;
color: var(--muted);
}
.session-end h3 {
font-family: Georgia, serif;
font-size: 28px;
color: var(--ink);
margin: 0 0 0.5rem;
}
/* Navigation buttons row */
.review-nav-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 16px;
justify-content: center;
}
@media (max-width: 1120px) {
.content-grid {
grid-template-columns: 1fr;
}
.side-panel {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 760px) {
.session-header {
flex-direction: column;
}
.review-panel {
min-height: auto;
}
.stage {
min-height: 420px;
}
.flashcard,
.card-face {
min-height: 380px;
}
.side-panel {
grid-template-columns: 1fr;
}
.review-button {
width: 100%;
}
.controls {
flex-direction: column;
align-items: stretch;
}
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { import {
MindforgeApiService, MindforgeApiService,
type FlashcardCard, type FlashcardCard,
@@ -26,15 +26,93 @@ function difficultyLabel(difficulty: string) {
function shuffleCards(cards: FlashcardCard[]) { function shuffleCards(cards: FlashcardCard[]) {
const shuffled = [...cards]; const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i--) { for (let i = shuffled.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1)); const randomIndex = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]]; [shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]];
} }
return shuffled; return shuffled;
} }
interface ConfettiParticle {
x: number;
y: number;
vx: number;
vy: number;
color: string;
size: number;
life: number;
maxLife: number;
rotation: number;
rotationSpeed: number;
}
function fireConfetti(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const c = ctx;
const w = canvas.width = window.innerWidth;
const h = canvas.height = window.innerHeight;
const colors = ['#4f8f5a', '#3f7cac', '#c79539', '#7e65a8', '#f2dfb3', '#b75b4d'];
const particles: ConfettiParticle[] = [];
for (let i = 0; i < 120; i++) {
particles.push({
x: Math.random() * w,
y: -20 - Math.random() * h * 0.5,
vx: (Math.random() - 0.5) * 6,
vy: Math.random() * 5 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 4,
life: 0,
maxLife: 80 + Math.random() * 60,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.3,
});
}
let animating = true;
function animate() {
if (!animating) return;
c.clearRect(0, 0, w, h);
let alive = 0;
for (const p of particles) {
p.life++;
if (p.life >= p.maxLife) continue;
alive++;
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08;
p.vx *= 0.995;
p.rotation += p.rotationSpeed;
const alpha = 1 - p.life / p.maxLife;
c.save();
c.globalAlpha = alpha;
c.translate(p.x, p.y);
c.rotate(p.rotation);
c.fillStyle = p.color;
c.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
c.restore();
}
if (alive > 0) {
requestAnimationFrame(animate);
} else {
c.clearRect(0, 0, w, h);
animating = false;
}
}
requestAnimationFrame(animate);
return () => {
animating = false;
};
}
export function FlashcardReviewComponent() { export function FlashcardReviewComponent() {
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]); const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]); const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
@@ -45,6 +123,13 @@ export function FlashcardReviewComponent() {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = useState(false); const [showAnswer, setShowAnswer] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false); const [submittingAnswer, setSubmittingAnswer] = useState(false);
const [cardExiting, setCardExiting] = useState(false);
const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null);
const [flipped, setFlipped] = useState(false);
const [sessionAnswers, setSessionAnswers] = useState<Record<number, boolean>>({});
const confettiRef = useRef<HTMLCanvasElement>(null);
const flashcardRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -75,6 +160,12 @@ export function FlashcardReviewComponent() {
}; };
}, []); }, []);
useEffect(() => {
if (showAnswer && flipped) return;
if (!showAnswer && !flipped) return;
setFlipped(showAnswer);
}, [showAnswer, flipped]);
const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]); const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]);
const libraryById = useMemo(() => { const libraryById = useMemo(() => {
@@ -86,12 +177,34 @@ export function FlashcardReviewComponent() {
? ((currentIndex + 1) / sessionCards.length) * 100 ? ((currentIndex + 1) / sessionCards.length) * 100
: 0; : 0;
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (sessionCards.length === 0 || !currentCard) return;
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) return;
if (e.code === 'Space' || e.code === 'Enter') {
e.preventDefault();
if (!showAnswer) {
setShowAnswer(true);
}
} else if (e.code === 'KeyC' && showAnswer && !submittingAnswer) {
e.preventDefault();
registerReviewAnswer(true);
} else if (e.code === 'KeyW' && showAnswer && !submittingAnswer) {
e.preventDefault();
registerReviewAnswer(false);
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [sessionCards.length, showAnswer, submittingAnswer, currentCard]);
const toggleLibrary = (libraryId: number) => { const toggleLibrary = (libraryId: number) => {
if (selectedLibraryIds.includes(libraryId)) { if (selectedLibraryIds.includes(libraryId)) {
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId)); setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
return; return;
} }
setSelectedLibraryIds([...selectedLibraryIds, libraryId]); setSelectedLibraryIds([...selectedLibraryIds, libraryId]);
}; };
@@ -112,6 +225,10 @@ export function FlashcardReviewComponent() {
setSessionCards(shuffleCards(response.cards)); setSessionCards(shuffleCards(response.cards));
setCurrentIndex(0); setCurrentIndex(0);
setShowAnswer(false); setShowAnswer(false);
setFlipped(false);
setStampState(null);
setCardExiting(false);
setSessionAnswers({});
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Falha ao iniciar sessao de revisao.'); setError(err?.message || 'Falha ao iniciar sessao de revisao.');
} finally { } finally {
@@ -124,48 +241,84 @@ export function FlashcardReviewComponent() {
setCurrentIndex(0); setCurrentIndex(0);
setShowAnswer(false); setShowAnswer(false);
setSubmittingAnswer(false); setSubmittingAnswer(false);
setFlipped(false);
setStampState(null);
setCardExiting(false);
setSessionAnswers({});
}; };
const goToPrevious = () => { const goToPrevious = () => {
if (currentIndex === 0) { if (currentIndex === 0) return;
return;
}
setCurrentIndex(currentIndex - 1); setCurrentIndex(currentIndex - 1);
setShowAnswer(false); setShowAnswer(false);
setFlipped(false);
setStampState(null);
};
const advanceCard = () => {
if (currentIndex >= sessionCards.length - 1) {
endSession();
return;
}
setCurrentIndex(currentIndex + 1);
setShowAnswer(false);
setFlipped(false);
setStampState(null);
}; };
const registerReviewAnswer = async (correct: boolean) => { const registerReviewAnswer = async (correct: boolean) => {
if (!currentCard) { if (!currentCard) return;
return;
}
setSubmittingAnswer(true); setSubmittingAnswer(true);
setError(null); setError(null);
if (correct) {
setStampState('correct');
if (confettiRef.current) {
fireConfetti(confettiRef.current);
}
} else {
setStampState('wrong');
}
setCardExiting(true);
setTimeout(() => {
setCardExiting(false);
setStampState(null);
}, 600);
try { try {
await MindforgeApiService.recordFlashcardReviewAnswer({ await MindforgeApiService.recordFlashcardReviewAnswer({
cardId: currentCard.id, cardId: currentCard.id,
correct, correct,
}); });
if (currentIndex >= sessionCards.length - 1) { setSessionAnswers((currentAnswers) => ({
endSession(); ...currentAnswers,
return; [currentCard.id]: correct,
} }));
setCurrentIndex(currentIndex + 1); setTimeout(() => {
setShowAnswer(false); advanceCard();
setSubmittingAnswer(false);
}, 580);
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Falha ao registrar resposta da revisao.'); setError(err?.message || 'Falha ao registrar resposta da revisao.');
} finally {
setSubmittingAnswer(false); setSubmittingAnswer(false);
setCardExiting(false);
setStampState(null);
} }
}; };
const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
const remainingCount = sessionCards.length - currentIndex;
return ( return (
<div className="review-container"> <div className="review-container">
<h2 className="title review-title">Revisao Flashcards</h2> <canvas ref={confettiRef} class="confetti-canvas" />
<p className="subtitle">Escolha as bibliotecas para estudar e inicie uma sessao de revisao.</p> <h2 className="review-title">Revisao Flashcards</h2>
<p className="review-subtitle">Escolha as bibliotecas para estudar e inicie uma sessao de revisao.</p>
{error && <div className="review-error">{error}</div>} {error && <div className="review-error">{error}</div>}
@@ -212,59 +365,137 @@ export function FlashcardReviewComponent() {
)} )}
{sessionCards.length > 0 && currentCard && ( {sessionCards.length > 0 && currentCard && (
<div className="review-session-panel"> <div class="content-grid">
<div className="review-progress"> <div class="review-panel">
<span>{currentIndex + 1} / {sessionCards.length}</span> <div class="session-header">
<div className="review-progress-bar"> <div class="session-title">
<div className="review-progress-fill" style={{ width: `${progressPercent}%` }} /> <h3>Sessao de Revisao</h3>
<p>{currentIndex + 1} de {sessionCards.length} cards</p>
</div>
<div class="score-pill">
<b>{correctCount}</b>
<span>Corretos</span>
</div> </div>
</div> </div>
<article className="review-card"> <div class="stage">
<header> <div
<small> ref={flashcardRef}
{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'} - class={`flashcard${flipped ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`}
{' '} onClick={() => { if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
{libraryById.get(currentCard.libraryId)?.subject || 'Geral'} tabIndex={0}
</small> role="button"
</header> aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaco para revelar'}
<h3>Frente</h3> >
<p>{currentCard.front}</p> <div class="card-face">
{showAnswer && ( {stampState && (
<> <div class={`stamp ${stampState}${stampState ? ' show' : ''}`}>
<h3>Verso</h3> {stampState === 'correct' ? 'Correto!' : 'Errado'}
<p>{currentCard.back}</p> </div>
</>
)} )}
</article> <div class="card-meta">
<span class="tag">{libraryById.get(currentCard.libraryId)?.subject || 'Geral'}</span>
<span>{difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}</span>
</div>
<div class="card-question">
{currentCard.front}
</div>
<div class="card-footer">
<span>{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'}</span>
<span class="spacebar">Espaco</span>
</div>
</div>
<div className="review-session-actions"> <div class="card-face card-back">
<div class="card-meta">
<span class="tag">Resposta</span>
<span>{difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}</span>
</div>
<div class="card-answer">
{currentCard.back}
</div>
<div class="card-footer">
<span>{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'}</span>
<span class="spacebar">C / W</span>
</div>
</div>
</div>
</div>
<div class={`controls${showAnswer ? ' ready' : ''}`}>
<button
class="review-button correct"
onClick={() => registerReviewAnswer(true)}
disabled={!showAnswer || submittingAnswer}
>
Correto
</button>
<button
class="review-button wrong"
onClick={() => registerReviewAnswer(false)}
disabled={!showAnswer || submittingAnswer}
>
Incorreto
</button>
</div>
<div class="review-nav-row">
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}> <Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
Anterior Anterior
</Button> </Button>
{!showAnswer && (
<Button variant="primary" onClick={() => setShowAnswer(true)}>
Revelar Resposta
</Button>
)}
{showAnswer && (
<>
<Button variant="primary" onClick={() => registerReviewAnswer(true)} disabled={submittingAnswer}>
Acertei
</Button>
<Button variant="secondary" onClick={() => registerReviewAnswer(false)} disabled={submittingAnswer}>
Errei
</Button>
</>
)}
<Button variant="secondary" onClick={endSession}> <Button variant="secondary" onClick={endSession}>
Encerrar Sessao Encerrar Sessao
</Button> </Button>
</div> </div>
</div> </div>
<div class="side-panel">
<div class="panel-card">
<h3>Progresso</h3>
<div class="stat-grid">
<div class="stat">
<b>{currentIndex + 1}/{sessionCards.length}</b>
<span>Atual</span>
</div>
<div class="stat">
<b>{correctCount}</b>
<span>Corretos</span>
</div>
</div>
<div class="track">
<span style={{ width: `${progressPercent}%` }} />
</div>
</div>
<div class="panel-card">
<h3>Fila</h3>
<div class="queue">
{sessionCards.slice(currentIndex, currentIndex + 5).map((card, idx) => (
<div key={card.id} class="queue-item">
<span class="queue-number">{currentIndex + idx + 1}</span>
<strong>{card.front.substring(0, 40)}{card.front.length > 40 ? '...' : ''}</strong>
<span>{libraryById.get(card.libraryId)?.subject || ''}</span>
</div>
))}
{remainingCount > 5 && (
<div style="text-align:center;color:var(--muted);font-size:12px;padding:4px">
+{remainingCount - 5} restantes
</div>
)}
</div>
</div>
</div>
</div>
)}
{sessionCards.length > 0 && !currentCard && (
<div class="session-end">
<h3>Sessao concluida!</h3>
<p>Todos os cards foram revisados.</p>
<div style="margin-top:16px">
<Button variant="primary" onClick={endSession}>Voltar a selecao</Button>
</div>
</div>
)} )}
</div> </div>
); );

View File

@@ -1,61 +1,72 @@
.header { .topbar {
position: fixed; min-height: 72px;
top: 0;
left: 0;
right: 0;
height: 70px;
background-color: var(--color-header);
/* Imposing black with glassy effect */
background: rgba(15, 15, 15, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
z-index: 1000; gap: 18px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); padding: 14px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); margin-bottom: 24px;
background: rgba(255, 250, 239, .72);
border: 1px solid rgba(104, 69, 22, .13);
border-radius: 26px;
backdrop-filter: blur(18px);
box-shadow: 0 18px 50px rgba(86, 57, 17, .08);
} }
.header-content { .topbar-left {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
position: relative;
}
.header-repo {
position: absolute;
right: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 12px;
background: rgba(255, 255, 255, 0.06); cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.1); min-width: 0;
border-radius: 6px;
padding: 4px 10px;
} }
.header-repo-icon { .topbar-logo {
font-size: 0.9rem; width: 42px;
color: rgba(255, 255, 255, 0.5); height: 42px;
border-radius: 12px;
flex-shrink: 0;
} }
.header-repo-name { .topbar-title {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.65);
font-family: var(--font-main);
letter-spacing: 0.5px;
}
.header-title {
color: var(--color-text-creamy);
font-family: var(--font-main);
font-size: 1.8rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
margin: 0; margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); font-family: Georgia, "Times New Roman", serif;
font-size: clamp(24px, 3vw, 34px);
font-weight: 800;
letter-spacing: -.04em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--ink);
}
.topbar-right {
flex-shrink: 0;
}
.chip {
min-height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid rgba(96, 67, 28, .15);
background: rgba(255,255,255,.62);
color: #4b3b27;
font-weight: 850;
font-size: 13px;
box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
}
@media (max-width: 760px) {
.topbar {
align-items: flex-start;
flex-direction: column;
}
.topbar-title {
white-space: normal;
}
} }

View File

@@ -16,19 +16,19 @@ export function Header({ onGoHome }: HeaderProps) {
}, []); }, []);
return ( return (
<header class="header"> <header class="topbar">
<div class="header-content"> <div class="topbar-left" onClick={onGoHome}>
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={onGoHome}> <img class="topbar-logo" src="/assets/mindforge.png" alt="Mindforge" width="42" height="42" />
<img src="/assets/mindforge.png" alt="Mindforge" width="55" height="55" style={{ marginRight: '10px' }} /> <h1 class="topbar-title">Mindforge</h1>
<h1 class="header-title">Mindforge</h1>
</div> </div>
{repoName && ( {repoName && (
<div class="header-repo"> <div class="topbar-right">
<span class="header-repo-icon"></span> <span class="chip">
<span class="header-repo-name">{repoName}</span> <span style="font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:var(--blue-deep)">Repo</span>
{repoName}
</span>
</div> </div>
)} )}
</div>
</header> </header>
); );
} }

View File

@@ -1,41 +1,174 @@
.sidebar { .sidebar {
width: 240px;
background-color: var(--color-sidebar);
border-right: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
padding: 2rem 1.5rem;
box-shadow: 4px 0 15px rgba(0, 0, 0, 0.2);
/* Ensure it fits cleanly below the header or is independent */
height: calc(100vh - 70px);
position: sticky; position: sticky;
top: 70px; top: 0;
} height: 100vh;
padding: 24px 18px;
.sidebar-header { background:
margin-bottom: 2rem; linear-gradient(180deg, rgba(255, 249, 234, .9), rgba(245, 226, 183, .84)),
} repeating-linear-gradient(45deg, rgba(113, 74, 18, .04) 0 1px, transparent 1px 8px);
border-right: 1px solid var(--line);
.sidebar-title { box-shadow: 16px 0 40px rgba(80, 54, 18, .08);
color: rgba(244, 245, 245, 0.6); z-index: 2;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 700;
margin: 0;
}
.sidebar-nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem;
} }
.sidebar-btn { .brand {
width: 100%;
text-align: left;
display: flex; display: flex;
justify-content: flex-start; align-items: center;
padding: 1rem 1.2rem; gap: 14px;
font-size: 1.05rem; padding: 12px 12px 24px;
}
.brand-mark {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 15px;
background: linear-gradient(135deg, #f2cf82, #fff2c8);
border: 1px solid rgba(105, 73, 21, .18);
box-shadow: inset 0 1px 0 rgba(255,255,255,.7), 0 10px 22px rgba(129, 86, 27, .17);
font-family: Georgia, serif;
font-size: 22px;
font-weight: 800;
color: #51340d;
flex-shrink: 0;
}
.brand-mark img {
width: 28px;
height: 28px;
border-radius: 8px;
}
.brand h1 {
margin: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: 22px;
font-weight: 800;
letter-spacing: -.03em;
color: var(--ink);
}
.nav-section-title {
margin: 20px 12px 10px;
color: var(--muted);
font-size: 11px;
font-weight: 900;
letter-spacing: .14em;
text-transform: uppercase;
}
.nav-list {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
overflow-y: auto;
}
.nav-item {
position: relative;
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 13px 13px;
border: 1px solid transparent;
border-radius: 16px;
color: #4b3b27;
text-decoration: none;
font-weight: 800;
font-size: 14px;
transition: .25s var(--ease);
cursor: pointer;
background: transparent;
font-family: inherit;
text-align: left;
}
.nav-item:hover {
transform: translateX(4px);
background: rgba(255,255,255,.45);
border-color: var(--line);
}
.nav-item.active {
background: #fff7df;
border-color: rgba(129, 86, 27, .16);
box-shadow: 0 12px 28px rgba(104, 65, 14, .10);
}
.nav-icon {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 11px;
background: rgba(255,255,255,.64);
border: 1px solid rgba(80, 54, 18, .10);
font-size: 14px;
flex-shrink: 0;
}
.nav-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 1120px) {
.sidebar {
padding: 20px 10px;
}
.brand {
justify-content: center;
padding: 8px 0 18px;
}
.brand h1,
.nav-section-title,
.nav-text {
display: none;
}
.nav-item {
justify-content: center;
padding: 12px;
}
}
@media (max-width: 760px) {
.sidebar {
position: static;
height: auto;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
overflow-x: auto;
border-right: 0;
border-bottom: 1px solid var(--line);
padding: 12px 16px;
}
.brand {
min-width: 64px;
padding: 0;
}
.nav-section-title {
display: none;
}
.nav-list {
flex-direction: row;
}
.nav-item {
min-width: 56px;
justify-content: center;
}
} }

View File

@@ -1,4 +1,3 @@
import { Button } from './Button';
import './Sidebar.css'; import './Sidebar.css';
interface SidebarProps { interface SidebarProps {
@@ -6,42 +5,35 @@ interface SidebarProps {
activeModule: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada'; activeModule: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada';
} }
const NAV_ITEMS: { module: SidebarProps['activeModule']; icon: string; label: string }[] = [
{ module: 'verificador', icon: '\u2713', label: 'Verificador' },
{ module: 'flashcards', icon: '\u25A6', label: 'Flashcards' },
{ module: 'revisao-flashcards', icon: '\u25B3', label: 'Revisao Flashcards' },
{ module: 'revisao-espacada', icon: '\u25CB', label: 'Revisao Espacada' },
];
export function Sidebar({ onModuleChange, activeModule }: SidebarProps) { export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
return ( return (
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header"> <div class="brand">
<h2 class="sidebar-title">Modulos</h2> <div class="brand-mark">
<img src="/assets/mindforge.png" alt="M" />
</div> </div>
<div class="sidebar-nav"> <h1>Mindforge</h1>
<Button
variant={activeModule === 'verificador' ? 'primary' : 'secondary'}
onClick={() => onModuleChange('verificador')}
className="sidebar-btn"
>
Verificador
</Button>
<Button
variant={activeModule === 'flashcards' ? 'primary' : 'secondary'}
onClick={() => onModuleChange('flashcards')}
className="sidebar-btn"
>
Flashcards
</Button>
<Button
variant={activeModule === 'revisao-flashcards' ? 'primary' : 'secondary'}
onClick={() => onModuleChange('revisao-flashcards')}
className="sidebar-btn"
>
Revisao Flashcards
</Button>
<Button
variant={activeModule === 'revisao-espacada' ? 'primary' : 'secondary'}
onClick={() => onModuleChange('revisao-espacada')}
className="sidebar-btn"
>
Revisao Espacada
</Button>
</div> </div>
<div class="nav-section-title">Modulos</div>
<nav class="nav-list">
{NAV_ITEMS.map(({ module, icon, label }) => (
<button
key={module}
class={`nav-item${activeModule === module ? ' active' : ''}`}
onClick={() => onModuleChange(module)}
>
<span class="nav-icon">{icon}</span>
<span class="nav-text">{label}</span>
</button>
))}
</nav>
</aside> </aside>
); );
} }

View File

@@ -1,43 +1,52 @@
.spaced-review-container { .spaced-review-container {
width: 100%; width: 100%;
max-width: 1020px;
margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.2rem; gap: 1.2rem;
animation: slideUp 0.45s ease-out;
} }
.spaced-review-title { .spaced-review-title {
font-size: 2.35rem; font-family: Georgia, "Times New Roman", serif;
font-size: clamp(24px, 3.5vw, 40px);
font-weight: 800;
letter-spacing: -.03em;
color: var(--ink);
margin: 0;
}
.spaced-review-subtitle {
color: var(--muted);
font-size: 15px;
line-height: 1.55;
max-width: 660px;
} }
.spaced-review-error { .spaced-review-error {
color: #ff9c96; color: var(--red);
text-align: left; font-size: 0.9rem;
background: rgba(255, 69, 58, 0.12); background: rgba(183, 91, 77, 0.08);
border: 1px solid rgba(255, 69, 58, 0.4); border: 1px solid rgba(183, 91, 77, 0.2);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.8rem 1rem; padding: 0.8rem 1rem;
margin-bottom: 1rem;
} }
.spaced-review-panel, .spaced-review-panel {
.spaced-review-session-panel { background: rgba(255, 250, 239, .68);
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(104, 69, 22, .13);
border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--radius-lg);
border-radius: 12px; padding: 1.5rem;
padding: 1.25rem; box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
text-align: left; text-align: left;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.18);
} }
.spaced-review-state { .spaced-review-state {
color: rgba(255, 255, 255, 0.72); color: var(--muted);
font-size: 0.95rem; font-size: 0.95rem;
} }
/* Filters */
.spaced-review-filters { .spaced-review-filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -49,35 +58,116 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.45rem; gap: 0.45rem;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(82, 54, 17, .14);
border-radius: 999px; border-radius: 999px;
padding: 0.35rem 0.8rem; padding: 0.35rem 0.8rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255,255,255,.48);
font-size: 0.88rem; font-size: 0.88rem;
cursor: pointer;
color: var(--ink);
} }
.spaced-review-filter input[type="checkbox"], .spaced-review-filter input[type="checkbox"] {
.spaced-review-library-item input[type="checkbox"] {
width: 15px; width: 15px;
height: 15px; height: 15px;
accent-color: var(--color-accent); accent-color: var(--blue);
} }
/* RAG Badges */
.rag-badge,
.rag-badge-inline {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 999px;
border: 1px solid rgba(82, 54, 17, .14);
padding: 0.22rem 0.55rem;
font-size: 0.76rem;
font-weight: 800;
line-height: 1;
}
.rag-icon {
width: 16px;
height: 16px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.74rem;
font-weight: 700;
}
.rag-red .rag-badge,
.rag-badge.rag-red,
.spaced-review-filter.rag-red {
background: rgba(183, 91, 77, 0.12);
border-color: rgba(183, 91, 77, 0.22);
}
.rag-red .rag-icon,
.rag-badge.rag-red .rag-icon,
.rag-badge-inline.rag-red .rag-icon {
background: rgba(183, 91, 77, 0.28);
}
.rag-amber .rag-badge,
.rag-badge.rag-amber,
.spaced-review-filter.rag-amber {
background: rgba(199, 149, 57, 0.14);
border-color: rgba(199, 149, 57, 0.24);
}
.rag-amber .rag-icon,
.rag-badge.rag-amber .rag-icon,
.rag-badge-inline.rag-amber .rag-icon {
background: rgba(199, 149, 57, 0.3);
}
.rag-green .rag-badge,
.rag-badge.rag-green,
.spaced-review-filter.rag-green {
background: rgba(79, 143, 90, 0.12);
border-color: rgba(79, 143, 90, 0.22);
}
.rag-green .rag-icon,
.rag-badge.rag-green .rag-icon,
.rag-badge-inline.rag-green .rag-icon {
background: rgba(79, 143, 90, 0.28);
}
.rag-grey .rag-badge,
.rag-badge.rag-grey,
.spaced-review-filter.rag-grey {
background: rgba(123, 106, 80, 0.10);
border-color: rgba(123, 106, 80, 0.18);
}
.rag-grey .rag-icon,
.rag-badge.rag-grey .rag-icon,
.rag-badge-inline.rag-grey .rag-icon {
background: rgba(123, 106, 80, 0.22);
}
/* Subjects */
.spaced-review-subjects { .spaced-review-subjects {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
} }
.spaced-review-subject { .spaced-review-subject {
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(82, 54, 17, .10);
border-radius: 10px; border-radius: var(--radius-md);
background: rgba(0, 0, 0, 0.18); background: rgba(255,255,255,.45);
padding: 0.9rem; padding: 0.9rem;
} }
.spaced-review-subject-header h3 { .spaced-review-subject-header h3 {
margin: 0; margin: 0;
font-size: 1.05rem; font-size: 1.05rem;
font-family: Georgia, serif;
color: var(--ink);
} }
.spaced-review-subject-toggle, .spaced-review-subject-toggle,
@@ -92,19 +182,19 @@
.spaced-review-subsubject-toggle input[type="checkbox"] { .spaced-review-subsubject-toggle input[type="checkbox"] {
width: 16px; width: 16px;
height: 16px; height: 16px;
accent-color: var(--color-accent); accent-color: var(--blue);
} }
.spaced-review-subject-header p { .spaced-review-subject-header p {
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
font-size: 0.9rem; font-size: 0.9rem;
color: rgba(255, 255, 255, 0.9); color: var(--ink);
} }
.spaced-review-subject-header small { .spaced-review-subject-header small {
display: block; display: block;
margin-top: 0.35rem; margin-top: 0.35rem;
color: rgba(255, 255, 255, 0.67); color: var(--muted);
font-size: 0.79rem; font-size: 0.79rem;
} }
@@ -115,10 +205,10 @@
} }
.spaced-review-subsubject-block { .spaced-review-subsubject-block {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(82, 54, 17, .10);
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.7rem; padding: 0.7rem;
background: rgba(255, 255, 255, 0.03); background: rgba(255,255,255,.48);
} }
.spaced-review-subsubject-header { .spaced-review-subsubject-header {
@@ -130,16 +220,17 @@
.spaced-review-subsubject-header strong { .spaced-review-subsubject-header strong {
font-size: 0.93rem; font-size: 0.93rem;
color: var(--ink);
} }
.spaced-review-subsubject-header span { .spaced-review-subsubject-header span {
font-size: 0.82rem; font-size: 0.82rem;
color: rgba(255, 255, 255, 0.78); color: var(--muted);
} }
.spaced-review-subsubject-header small { .spaced-review-subsubject-header small {
font-size: 0.78rem; font-size: 0.78rem;
color: rgba(255, 255, 255, 0.6); color: var(--muted);
} }
.spaced-review-library-list { .spaced-review-library-list {
@@ -151,16 +242,39 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.7rem; gap: 0.7rem;
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid rgba(82, 54, 17, .12);
border-left-width: 4px; border-left-width: 4px;
border-radius: 10px; border-radius: var(--radius-md);
padding: 0.6rem 0.7rem; padding: 0.6rem 0.7rem;
background: rgba(255, 255, 255, 0.03); background: rgba(255,255,255,.48);
cursor: pointer; cursor: pointer;
} }
.spaced-review-library-item input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--blue);
}
.spaced-review-library-item.selected { .spaced-review-library-item.selected {
box-shadow: 0 0 0 1px rgba(var(--color-accent-rgb), 0.45); box-shadow: 0 0 0 2px rgba(63, 124, 172, .25);
border-color: rgba(63, 124, 172, .3);
}
.spaced-review-library-item.rag-red {
border-left-color: var(--red);
}
.spaced-review-library-item.rag-amber {
border-left-color: var(--gold);
}
.spaced-review-library-item.rag-green {
border-left-color: var(--green);
}
.spaced-review-library-item.rag-grey {
border-left-color: var(--muted);
} }
.spaced-review-library-texts { .spaced-review-library-texts {
@@ -173,6 +287,7 @@
.spaced-review-library-texts strong { .spaced-review-library-texts strong {
font-size: 0.91rem; font-size: 0.91rem;
color: var(--ink);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -180,103 +295,15 @@
.spaced-review-library-texts span { .spaced-review-library-texts span {
font-size: 0.8rem; font-size: 0.8rem;
color: rgba(255, 255, 255, 0.78); color: var(--muted);
} }
.spaced-review-library-texts small { .spaced-review-library-texts small {
font-size: 0.77rem; font-size: 0.77rem;
color: rgba(255, 255, 255, 0.62); color: var(--muted);
}
.rag-badge,
.rag-badge-inline {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.18);
padding: 0.22rem 0.55rem;
font-size: 0.76rem;
font-weight: 700;
line-height: 1;
}
.rag-icon {
width: 16px;
height: 16px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.74rem;
font-weight: 700;
background: rgba(255, 255, 255, 0.18);
}
.rag-red {
border-left-color: #ff5d5d;
}
.rag-red .rag-badge,
.rag-badge.rag-red,
.spaced-review-filter.rag-red {
background: rgba(255, 93, 93, 0.15);
}
.rag-red .rag-icon,
.rag-badge.rag-red .rag-icon,
.rag-badge-inline.rag-red .rag-icon {
background: rgba(255, 93, 93, 0.35);
}
.rag-amber {
border-left-color: #ffbe55;
}
.rag-amber .rag-badge,
.rag-badge.rag-amber,
.spaced-review-filter.rag-amber {
background: rgba(255, 190, 85, 0.16);
}
.rag-amber .rag-icon,
.rag-badge.rag-amber .rag-icon,
.rag-badge-inline.rag-amber .rag-icon {
background: rgba(255, 190, 85, 0.36);
}
.rag-green {
border-left-color: #46d18a;
}
.rag-green .rag-badge,
.rag-badge.rag-green,
.spaced-review-filter.rag-green {
background: rgba(70, 209, 138, 0.16);
}
.rag-green .rag-icon,
.rag-badge.rag-green .rag-icon,
.rag-badge-inline.rag-green .rag-icon {
background: rgba(70, 209, 138, 0.35);
}
.rag-grey {
border-left-color: #9aa6b5;
}
.rag-grey .rag-badge,
.rag-badge.rag-grey,
.spaced-review-filter.rag-grey {
background: rgba(154, 166, 181, 0.16);
}
.rag-grey .rag-icon,
.rag-badge.rag-grey .rag-icon,
.rag-badge-inline.rag-grey .rag-icon {
background: rgba(154, 166, 181, 0.35);
} }
/* Footer */
.spaced-review-footer { .spaced-review-footer {
margin-top: 1rem; margin-top: 1rem;
display: flex; display: flex;
@@ -288,42 +315,53 @@
.spaced-review-footer p { .spaced-review-footer p {
font-size: 0.93rem; font-size: 0.93rem;
color: rgba(255, 255, 255, 0.78); color: var(--ink);
}
/* Session Panel */
.spaced-review-session-panel {
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
border-radius: var(--radius-xl);
padding: clamp(18px, 3vw, 34px);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
text-align: left;
} }
.spaced-review-progress { .spaced-review-progress {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.9rem; gap: 0.9rem;
margin-bottom: 0.8rem; margin-bottom: 1.2rem;
} }
.spaced-review-progress span { .spaced-review-progress span {
font-size: 0.9rem; font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8); color: var(--ink);
min-width: 60px; min-width: 60px;
font-weight: 800;
} }
.spaced-review-progress-bar { .spaced-review-progress-bar {
width: 100%; width: 100%;
height: 8px; height: 10px;
border-radius: 999px; border-radius: 999px;
background: rgba(0, 0, 0, 0.25);
overflow: hidden; overflow: hidden;
background: rgba(80, 54, 18, .12);
} }
.spaced-review-progress-fill { .spaced-review-progress-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.8), rgba(var(--color-accent-rgb), 1)); background: linear-gradient(90deg, var(--blue), #79a9c8, var(--gold));
border-radius: 999px; border-radius: 999px;
transition: width 0.25s ease;
} }
.spaced-review-card { .spaced-review-card {
border: 1px solid rgba(255, 255, 255, 0.16); border: 1px solid rgba(82, 54, 17, .14);
border-radius: 10px; border-radius: var(--radius-lg);
padding: 1rem; padding: 1.25rem;
background: rgba(0, 0, 0, 0.2); background: rgba(255,255,255,.52);
min-height: 260px; min-height: 260px;
} }
@@ -337,26 +375,32 @@
} }
.spaced-review-card small { .spaced-review-card small {
color: rgba(255, 255, 255, 0.62); color: var(--muted);
font-size: 0.85rem;
} }
.spaced-review-card h3 { .spaced-review-card h3 {
margin: 0 0 0.4rem; margin: 0 0 0.4rem;
font-size: 0.98rem; font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9); font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--blue-deep);
} }
.spaced-review-card p { .spaced-review-card p {
margin: 0 0 1rem; margin: 0 0 1rem;
font-size: 1rem; font-size: 1.05rem;
line-height: 1.55; line-height: 1.6;
color: var(--ink);
font-family: "Segoe UI", Inter, system-ui, sans-serif;
} }
.spaced-review-card-meta { .spaced-review-card-meta {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
color: rgba(255, 255, 255, 0.75); color: var(--muted);
font-size: 0.84rem; font-size: 0.84rem;
} }
@@ -367,6 +411,89 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Review content grid for session mode */
.spaced-review-content-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 330px;
gap: 24px;
align-items: start;
}
.spaced-review-side-panel {
display: grid;
gap: 18px;
}
.spaced-review-panel-card {
padding: 20px;
border-radius: 26px;
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
}
.spaced-review-panel-card h3 {
margin: 0 0 14px;
font-family: Georgia, serif;
font-size: 22px;
letter-spacing: -.03em;
color: var(--ink);
}
.spaced-review-stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.spaced-review-stat {
padding: 14px;
border-radius: 18px;
background: rgba(255,255,255,.52);
border: 1px solid rgba(82, 54, 17, .10);
}
.spaced-review-stat b {
display: block;
font-size: 24px;
letter-spacing: -.04em;
color: var(--ink);
}
.spaced-review-stat span {
color: var(--muted);
font-size: 11px;
font-weight: 950;
letter-spacing: .10em;
text-transform: uppercase;
}
.spaced-review-track {
height: 14px;
border-radius: 999px;
overflow: hidden;
background: rgba(80, 54, 18, .12);
margin-top: 8px;
}
.spaced-review-track span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
}
@media (max-width: 1120px) {
.spaced-review-content-grid {
grid-template-columns: 1fr;
}
.spaced-review-side-panel {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) { @media (max-width: 760px) {
.spaced-review-library-item { .spaced-review-library-item {
flex-wrap: wrap; flex-wrap: wrap;
@@ -376,4 +503,8 @@
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.spaced-review-side-panel {
grid-template-columns: 1fr;
}
} }

View File

@@ -361,8 +361,8 @@ export function SpacedReviewComponent() {
return ( return (
<div className="spaced-review-container"> <div className="spaced-review-container">
<h2 className="title spaced-review-title">Revisao espacada</h2> <h2 className="spaced-review-title">Revisao espacada</h2>
<p className="subtitle">Acompanhe o status RAG por arquivo de flashcards.</p> <p className="spaced-review-subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
{error && <div className="spaced-review-error">{error}</div>} {error && <div className="spaced-review-error">{error}</div>}
@@ -492,6 +492,7 @@ export function SpacedReviewComponent() {
)} )}
{sessionCards.length > 0 && currentCard && ( {sessionCards.length > 0 && currentCard && (
<div class="spaced-review-content-grid">
<div className="spaced-review-session-panel"> <div className="spaced-review-session-panel">
<div className="spaced-review-progress"> <div className="spaced-review-progress">
<span>{currentIndex + 1} / {sessionCards.length}</span> <span>{currentIndex + 1} / {sessionCards.length}</span>
@@ -553,6 +554,36 @@ export function SpacedReviewComponent() {
</Button> </Button>
</div> </div>
</div> </div>
<div class="spaced-review-side-panel">
<div class="spaced-review-panel-card">
<h3>Progresso</h3>
<div class="spaced-review-stat-grid">
<div class="spaced-review-stat">
<b>{currentIndex + 1}/{sessionCards.length}</b>
<span>Atual</span>
</div>
<div class="spaced-review-stat">
<b class={`rag-badge ${currentStatusMeta.className}`} style="font-size:14px;padding:4px 8px">
<span class="rag-icon">{currentStatusMeta.icon}</span>
{currentStatusMeta.label}
</b>
<span>Status RAG</span>
</div>
</div>
<div class="spaced-review-track">
<span style={{ width: `${progressPercent}%` }} />
</div>
</div>
</div>
</div>
)}
{sessionCards.length > 0 && !currentCard && (
<div className="spaced-review-panel" style="text-align:center;padding:3rem 1rem">
<h3 style="font-family:Georgia,serif;font-size:28px;color:var(--ink);margin:0 0 0.5rem">Sessao concluida!</h3>
<p style="color:var(--muted)">Todos os cards foram revisados.</p>
</div>
)} )}
</div> </div>
); );

View File

@@ -2,38 +2,37 @@
width: 100%; width: 100%;
max-width: 1000px; max-width: 1000px;
margin: 0 auto; margin: 0 auto;
animation: slideUp 0.5s ease-out;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.file-result-block { .verificador-title {
background: rgba(255, 255, 255, 0.03); font-family: Georgia, "Times New Roman", serif;
border: 1px solid rgba(255, 255, 255, 0.08); font-size: clamp(24px, 3.5vw, 40px);
border-radius: 10px; font-weight: 800;
padding: 1rem; letter-spacing: -.03em;
margin-bottom: 1rem; color: var(--ink);
margin: 0;
} }
.file-result-title { .verificador-subtitle {
font-size: 0.9rem; color: var(--muted);
font-weight: 600; font-size: 15px;
color: rgba(255, 255, 255, 0.6); line-height: 1.55;
margin-bottom: 0.75rem; max-width: 660px;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
font-family: monospace;
} }
.verificador-form { .verificador-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.2rem; gap: 1.2rem;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 250, 239, .68);
padding: 2rem; padding: 2rem;
border-radius: 12px; border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(104, 69, 22, .13);
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
} }
.input-group { .input-group {
@@ -45,78 +44,38 @@
.input-group label { .input-group label {
font-weight: 700; font-weight: 700;
color: var(--color-text-creamy); color: var(--ink);
} font-size: 0.9rem;
.text-area {
width: 100%;
min-height: 200px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 1rem;
color: var(--color-text-creamy);
font-family: inherit;
font-size: 1rem;
resize: vertical;
}
.text-area:focus {
outline: none;
border-color: var(--color-accent);
}
.file-input-wrapper {
display: flex;
align-items: center;
gap: 1rem;
}
.file-input {
display: none;
}
.file-input-label {
background: var(--color-sidebar);
color: var(--color-text-creamy);
padding: 0.6rem 1.2rem;
border-radius: 6px;
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
}
.file-input-label:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--color-accent);
} }
.select-input { .select-input {
background: rgba(0, 0, 0, 0.2); background: rgba(255, 250, 239, .72);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(104, 69, 22, .15);
color: var(--color-text-creamy); color: var(--ink);
padding: 0.8rem; padding: 0.8rem;
border-radius: 8px; border-radius: var(--radius-md);
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
font-family: inherit;
} }
.select-input:focus { .select-input:focus {
outline: none; outline: none;
border-color: var(--color-accent); border-color: var(--blue);
} }
.select-input option { .select-input option {
background: var(--color-bg); background: var(--paper);
color: var(--color-text-creamy); color: var(--ink);
} }
/* Response Section */
.response-section { .response-section {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 250, 239, .68);
padding: 2rem; padding: 2rem;
border-radius: 12px; border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(104, 69, 22, .13);
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
@@ -125,30 +84,35 @@
.response-content { .response-content {
text-align: left; text-align: left;
white-space: pre-wrap; white-space: pre-wrap;
background: rgba(0, 0, 0, 0.2); background: rgba(255,255,255,.52);
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(82, 54, 17, .10);
font-family: monospace;
font-size: 0.95rem; font-size: 0.95rem;
line-height: 1.5; line-height: 1.5;
overflow-x: auto; overflow-x: auto;
color: var(--ink);
}
.response-content.markdown-body {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
} }
.diff-view { .diff-view {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-family: "Segoe UI", Inter, system-ui, sans-serif;
} }
.diff-added { .diff-added {
background-color: rgba(46, 160, 67, 0.3); background-color: rgba(79, 143, 90, 0.18);
color: #7ee787; color: var(--green-deep);
border-radius: 3px; border-radius: 3px;
} }
.diff-removed { .diff-removed {
background-color: rgba(248, 81, 73, 0.3); background-color: rgba(183, 91, 77, 0.18);
color: #ff7b72; color: var(--red-deep);
text-decoration: line-through; text-decoration: line-through;
border-radius: 3px; border-radius: 3px;
} }
@@ -168,32 +132,74 @@
} }
.pane-title { .pane-title {
font-size: 1.1rem; font-size: 0.85rem;
font-weight: 700; font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
text-align: center; text-align: center;
color: var(--color-accent); color: var(--blue-deep);
} }
/* Spinner */ .file-result-block {
.spinner-container { background: rgba(255,255,255,.45);
border: 1px solid rgba(82, 54, 17, .12);
border-radius: var(--radius-md);
padding: 1rem;
margin-bottom: 1rem;
}
.file-result-title {
font-size: 0.9rem;
font-weight: 700;
color: var(--muted);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--line);
font-family: "Segoe UI", Inter, monospace;
}
.select-status {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem;
color: var(--muted);
font-size: 0.85rem;
}
.verificador-error {
color: var(--red);
font-size: 0.9rem;
background: rgba(183, 91, 77, 0.08);
border: 1px solid rgba(183, 91, 77, 0.2);
border-radius: var(--radius-md);
padding: 0.8rem 1rem;
}
.loading-indicator {
display: flex;
align-items: center;
gap: 10px;
justify-content: center; justify-content: center;
padding: 3rem 0; padding: 2rem;
gap: 1rem; color: var(--muted);
font-size: 0.95rem;
} }
.spinner { .loading-dot {
width: 50px; width: 10px;
height: 50px; height: 10px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: var(--color-accent);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; background: var(--blue);
box-shadow: 0 0 0 4px rgba(63, 124, 172, .16);
} }
@keyframes spin { @media (max-width: 760px) {
0% { transform: rotate(0deg); } .verificador-form,
100% { transform: rotate(360deg); } .response-section {
padding: 1.2rem;
}
.side-by-side {
flex-direction: column;
}
} }

View File

@@ -32,7 +32,7 @@ export function VerificadorComponent() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (selectedPaths.length === 0) { if (selectedPaths.length === 0) {
setError('Selecione pelo menos um arquivo do repositório.'); setError('Selecione pelo menos um arquivo do repositorio.');
return; return;
} }
@@ -97,44 +97,44 @@ export function VerificadorComponent() {
return ( return (
<div className="verificador-container"> <div className="verificador-container">
<h2 className="title" style={{ fontSize: '2.5rem' }}>Verificador de Arquivos</h2> <h2 className="verificador-title">Verificador de Arquivos</h2>
<p className="subtitle">Selecione os arquivos do repositório para validação de linguagem ou conteúdo.</p> <p className="verificador-subtitle">Selecione os arquivos do repositorio para validação de linguagem ou conteudo.</p>
<div className="verificador-form"> <div className="verificador-form">
<div className="input-group"> <div className="input-group">
<label>Arquivos do Repositório</label> <label>Arquivos do Repositorio</label>
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} /> <FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
{selectedPaths.length > 0 && ( {selectedPaths.length > 0 && (
<div style={{ fontSize: '0.85rem', color: 'rgba(255,255,255,0.5)', marginTop: '0.4rem' }}> <div class="select-status">
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''} {selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
</div> </div>
)} )}
</div> </div>
<div className="input-group"> <div className="input-group">
<label>Tipo de Verificação</label> <label>Tipo de Verificacao</label>
<select <select
className="select-input" className="select-input"
value={checkType} value={checkType}
onChange={(e) => setCheckType((e.target as HTMLSelectElement).value as CheckTypeEnum)} onChange={(e) => setCheckType((e.target as HTMLSelectElement).value as CheckTypeEnum)}
> >
<option value="language">Linguagem</option> <option value="language">Linguagem</option>
<option value="content">Conteúdo</option> <option value="content">Conteudo</option>
<option value="both">Linguagem e Conteúdo</option> <option value="both">Linguagem e Conteudo</option>
</select> </select>
</div> </div>
<Button variant="primary" onClick={handleSubmit} disabled={loading} style={{ marginTop: '1rem' }}> <Button variant="primary" onClick={handleSubmit} disabled={loading} style={{ marginTop: '0.5rem' }}>
{loading ? 'Processando...' : 'Verificar'} {loading ? 'Processando...' : 'Verificar'}
</Button> </Button>
{error && <div style={{ color: '#ff7b72', marginTop: '1rem' }}>{error}</div>} {error && <div class="verificador-error">{error}</div>}
</div> </div>
{loading && ( {loading && (
<div className="spinner-container"> <div class="loading-indicator">
<div className="spinner"></div> <span class="loading-dot"></span>
<p>Analisando sua forja mental...</p> <span>Analisando sua forja mental...</span>
</div> </div>
)} )}
@@ -145,7 +145,7 @@ export function VerificadorComponent() {
<div className="file-result-title">{fileResult.fileName}</div> <div className="file-result-title">{fileResult.fileName}</div>
{fileResult.error && ( {fileResult.error && (
<div style={{ color: '#ff7b72', padding: '0.5rem' }}>{fileResult.error}</div> <div class="verificador-error">{fileResult.error}</div>
)} )}
{!fileResult.error && checkType === 'language' && fileResult.languageResult && ( {!fileResult.error && checkType === 'language' && fileResult.languageResult && (
@@ -159,7 +159,7 @@ export function VerificadorComponent() {
{!fileResult.error && checkType === 'content' && fileResult.contentResult && ( {!fileResult.error && checkType === 'content' && fileResult.contentResult && (
<div className="side-pane"> <div className="side-pane">
<div className="pane-title">Conteúdo</div> <div className="pane-title">Conteudo</div>
<div <div
className="response-content markdown-body" className="response-content markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }} dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
@@ -176,7 +176,7 @@ export function VerificadorComponent() {
</div> </div>
</div> </div>
<div className="side-pane"> <div className="side-pane">
<div className="pane-title">Conteúdo</div> <div className="pane-title">Conteudo</div>
<div <div
className="response-content markdown-body" className="response-content markdown-body"
style={{ minHeight: '200px' }} style={{ minHeight: '200px' }}

View File

@@ -1,15 +1,29 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap');
:root { :root {
--color-bg: #005873; --bg: #fbf3df;
--color-header: #0f0f0f; --bg-soft: #fff9ea;
--color-sidebar: #013a4c; --paper: #fffaf0;
--color-text-creamy: #f4f5f5; --paper-deep: #f2dfb3;
--color-accent: #00b4d8; --ink: #2b241c;
--color-accent-rgb: 0, 180, 216; --muted: #7b6a50;
--color-accent-hover: #0096c7; --line: rgba(82, 60, 28, 0.18);
--font-main: 'Lato', sans-serif; --blue: #3f7cac;
--blue-deep: #255f8d;
--green: #4f8f5a;
--green-deep: #32683b;
--red: #b75b4d;
--red-deep: #8d3c32;
--gold: #c79539;
--violet: #7e65a8;
--shadow: 0 28px 70px rgba(58, 42, 17, 0.18);
--card-shadow: 0 30px 90px rgba(76, 48, 12, 0.20);
--radius-xl: 32px;
--radius-lg: 22px;
--radius-md: 16px;
--ease: cubic-bezier(.2,.75,.2,1);
} }
* { * {
@@ -19,39 +33,57 @@
} }
body { body {
font-family: var(--font-main);
background-color: var(--color-bg);
color: var(--color-text-creamy);
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
color: var(--ink);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(255, 221, 146, .8), transparent 35%),
radial-gradient(circle at 85% 15%, rgba(99, 153, 188, .22), transparent 34%),
linear-gradient(135deg, #fff8e6 0%, #f9edcc 50%, #fbf5e7 100%);
overflow-x: hidden;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(92, 68, 30, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(92, 68, 30, 0.035) 1px, transparent 1px);
background-size: 24px 24px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,.65), rgba(0,0,0,.12));
}
#app { #app {
display: flex;
flex-direction: column;
min-height: 100vh; min-height: 100vh;
} }
.main-layout { .app {
display: flex; min-height: 100vh;
flex: 1; display: grid;
margin-top: 70px; /* offset for fixed header */ grid-template-columns: 288px minmax(0, 1fr);
} }
.content-area { .main {
flex: 1; min-width: 0;
padding: 2rem; padding: 22px 28px 38px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
} }
.title { @media (max-width: 1120px) {
font-size: 3rem; .app {
font-weight: 700; grid-template-columns: 84px minmax(0, 1fr);
margin-bottom: 2rem; }
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
@media (max-width: 760px) {
.app {
grid-template-columns: 1fr;
}
.main {
padding: 16px;
}
} }

View File

@@ -4,7 +4,7 @@
Mindforge é uma ferramenta de estudo para auxiliar na preparação para concursos públicos brasileiros. O sistema permite validar e gerar materiais de estudo a partir de arquivos Markdown hospedados em repositórios Git, utilizando IA (API compatível com OpenRouter/OpenAI) para processamento. Mindforge é uma ferramenta de estudo para auxiliar na preparação para concursos públicos brasileiros. O sistema permite validar e gerar materiais de estudo a partir de arquivos Markdown hospedados em repositórios Git, utilizando IA (API compatível com OpenRouter/OpenAI) para processamento.
A interface é em **português brasileiro** e possui um tema escuro com efeito vidro ("glassy-look"). A interface é em **português brasileiro** e possui um tema claro quente com estética vintage acadêmica — tons de creme, pergaminho e dourado com texturas de papel, seguindo as diretrizes em `GUIDELINES-ESTILO.md`.
--- ---
@@ -20,7 +20,7 @@ A interface é em **português brasileiro** e possui um tema escuro com efeito v
| **diff** | ^8.0.3 | Diff de texto (word-level) | | **diff** | ^8.0.3 | Diff de texto (word-level) |
> Nota: `marked` v17+ inclui tipos TypeScript nativos. `@types/marked` foi removido. `@types/diff` v7 permanece em devDependencies como fallback de tipos. > Nota: `marked` v17+ inclui tipos TypeScript nativos. `@types/marked` foi removido. `@types/diff` v7 permanece em devDependencies como fallback de tipos.
| **Google Fonts (Lato)** | 300/400/700 | Tipografia da interface | | **Google Fonts (Inter)** | 400..950 (variável) | Tipografia da interface |
### Backend API (Mindforge.API) ### Backend API (Mindforge.API)
| Tecnologia | Versão | Finalidade | | Tecnologia | Versão | Finalidade |
@@ -125,9 +125,10 @@ Formas de requisição principais:
- **CSS**: Um arquivo `.css` por componente, importado diretamente. **Sem CSS Modules.** - **CSS**: Um arquivo `.css` por componente, importado diretamente. **Sem CSS Modules.**
- **Estado**: Apenas estado local (`useState`). Sem store global, sem Context API. - **Estado**: Apenas estado local (`useState`). Sem store global, sem Context API.
- **Roteamento**: Alternância de componentes via `display: block/none` com o estado `activeModule`. Não usa `react-router`. - **Roteamento**: Alternância de componentes via `display: block/none` com o estado `activeModule`. Não usa `react-router`.
- **API**: Chamadas via `MindforgeApiService` (objeto singleton com métodos estáticos usando `fetch`). - **API**: Chamadas via `MindforgeApiService` (objeto singleton com métodos assíncronos usando `fetch`).
- **TypeScript**: Modo estrito, `erasableSyntaxOnly`, `verbatimModuleSyntax`, `noUnusedLocals`, `noUnusedParameters`. - **TypeScript**: Modo estrito, `erasableSyntaxOnly`, `verbatimModuleSyntax`, `noUnusedLocals`, `noUnusedParameters`.
- **Alias**: `react` e `react-dom` mapeados para `preact/compat/` no tsconfig. - **Alias**: `react` e `react-dom` mapeados para `preact/compat/` no tsconfig.
- **Layout**: `.app` usa CSS Grid (`grid-template-columns: 288px 1fr`) com Sidebar sticky + `.main` contendo Topbar e área de conteúdo.
### Backend (C#/.NET 9) ### Backend (C#/.NET 9)
- **Namespaces**: `Mindforge.API.Controllers`, `Mindforge.API.Services`, etc. - **Namespaces**: `Mindforge.API.Controllers`, `Mindforge.API.Services`, etc.
@@ -138,21 +139,38 @@ Formas de requisição principais:
### UI/UX ### UI/UX
- **Idioma**: Todo texto em **português brasileiro**. - **Idioma**: Todo texto em **português brasileiro**.
- **Tema**: Escuro com efeito vidro (glassy). `backdrop-filter: blur()`, fundos `rgba` semitransparentes. - **Tema**: Claro quente vintage ("mesa de estudo pessoal"). Fundos creme/pergaminho/dourado com textura de papel (grid via `body::before`). Vidro translúcido com `backdrop-filter` em tons quentes.
- **Botões**: Estilo iOS-like, modernos. Variantes `primary` (com blur) e `secondary` (transparente). - **Botões**: Estilo pill quente com sombras marrons. Variantes `primary` (gradiente azul, com brilho hover) e `secondary` (translúcido pergaminho).
- **Tipografia**: Lato (Google Fonts), pesos 300/400/700. - **Tipografia**: Inter (UI global), Georgia/Times New Roman (marca e cabeçalhos), Segoe UI/Inter (conteúdo do flashcard).
- **Background**: `#005873` (azul petróleo escuro). Não muito escuro. - **Background**: Gradiente radial quente (dourado/azul) sobre base `#fff8e6` com overlay de grid de papel 24px.
- **Layout**: CSS Grid (`grid-template-columns: 288px minmax(0, 1fr)`). Sidebar sticky com textura diagonal. Topbar integrada ao fluxo (não fixa).
- **Animações**: Apenas sob ação do usuário (flip 3D do flashcard, carimbo, saída do cartão, confete canvas). Sem animações infinitas ou spinners.
- **Flashcard**: Cartão 3D com efeito `rotateY(180deg)`, frente papel pautado com borda tracejada, verso azulado. Carimbo de feedback (correto/incorreto). Confete canvas ao acertar.
- **Responsivo**: Breakpoints em 1120px (sidebar colapsada) e 760px (layout single-column).
### Variáveis CSS (definidas em `index.css`) ### Variáveis CSS (definidas em `index.css`)
```css ```css
--color-bg: #005873; --bg: #fbf3df;
--color-header: #0f0f0f; --bg-soft: #fff9ea;
--color-sidebar: #013a4c; --paper: #fffaf0;
--color-text-creamy: #f4f5f5; --paper-deep: #f2dfb3;
--color-accent: #00b4d8; --ink: #2b241c;
--color-accent-rgb: 0, 180, 216; --muted: #7b6a50;
--color-accent-hover: #0096c7; --line: rgba(82, 60, 28, 0.18);
--font-main: 'Lato', sans-serif; --blue: #3f7cac;
--blue-deep: #255f8d;
--green: #4f8f5a;
--green-deep: #32683b;
--red: #b75b4d;
--red-deep: #8d3c32;
--gold: #c79539;
--violet: #7e65a8;
--shadow: 0 28px 70px rgba(58, 42, 17, 0.18);
--card-shadow: 0 30px 90px rgba(76, 48, 12, 0.20);
--radius-xl: 32px;
--radius-lg: 22px;
--radius-md: 16px;
--ease: cubic-bezier(.2,.75,.2,1);
``` ```
### Git e Scripts de Build/Deploy ### Git e Scripts de Build/Deploy