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" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<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>
</head>

View File

@@ -3,60 +3,23 @@
flex-direction: column;
align-items: center;
justify-content: center;
animation: fadeIn 0.8s ease-out;
padding: 2rem 1rem;
}
.hero-title {
font-size: 4rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 1rem;
text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
background: linear-gradient(90deg, #f4f5f5, #00b4d8);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(32px, 5vw, 56px);
font-weight: 800;
letter-spacing: -.03em;
color: var(--ink);
margin-bottom: 0.5rem;
}
.hero-subtitle {
font-size: 1.5rem;
color: rgba(244, 245, 245, 0.8);
font-weight: 300;
letter-spacing: 1px;
}
.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);
font-size: 18px;
color: var(--muted);
font-weight: 400;
max-width: 480px;
text-align: center;
backdrop-filter: blur(4px);
-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); }
line-height: 1.6;
}

View File

@@ -11,32 +11,30 @@ export function App() {
const [activeModule, setActiveModule] = useState<'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada'>('home');
return (
<>
<Header onGoHome={() => setActiveModule('home')} />
<div class="main-layout">
<div class="app">
<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 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 }} />
<h1 class="hero-title">Mindforge - Forja Mental</h1>
<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>
<p class="hero-subtitle">Sua ferramenta de estudos para concursos.</p>
</div>
</div>
<div style={{ display: activeModule === 'verificador' ? 'block' : 'none', height: '100%', width: '100%' }}>
<div style={{ display: activeModule === 'verificador' ? 'block' : 'none', width: '100%' }}>
<VerificadorComponent />
</div>
<div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}>
<div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', width: '100%' }}>
<FlashcardComponent />
</div>
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}>
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', width: '100%' }}>
<FlashcardReviewComponent />
</div>
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', height: '100%', width: '100%' }}>
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', width: '100%' }}>
<SpacedReviewComponent />
</div>
</main>
</div>
</>
</div>
);
}

View File

@@ -1,32 +1,30 @@
.btn {
font-family: var(--font-main);
font-weight: 700;
font-size: 1rem;
padding: 0.8rem 1.5rem;
border-radius: 8px;
border: none;
font-family: inherit;
font-weight: 850;
font-size: 13px;
padding: 0 18px;
min-height: 42px;
border-radius: 999px;
cursor: pointer;
outline: none;
transition: all 0.3s ease;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
letter-spacing: 0.5px;
transition: .25s var(--ease);
letter-spacing: 0;
display: inline-flex;
align-items: center;
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 {
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);
.btn:hover {
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);
}
@@ -37,12 +35,21 @@
box-shadow: none;
}
.btn-secondary {
background: transparent;
color: var(--color-text-creamy);
border: 1px solid rgba(255, 255, 255, 0.1);
.btn-primary {
background: linear-gradient(135deg, var(--blue), var(--blue-deep));
color: #fff;
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 {
background: rgba(255, 255, 255, 0.05);
.btn-primary:hover {
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 {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
border-radius: var(--radius-md);
padding: 0.75rem;
max-height: 380px;
overflow-y: auto;
font-size: 0.9rem;
box-shadow: 0 12px 32px rgba(86, 57, 17, .06);
backdrop-filter: blur(12px);
}
.tree-folder {
@@ -17,30 +19,30 @@
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 4px;
border-radius: 8px;
cursor: pointer;
user-select: none;
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
color: var(--ink);
font-weight: 700;
}
.tree-folder-header:hover {
background: rgba(255, 255, 255, 0.07);
background: rgba(255,255,255,.52);
}
.tree-folder-arrow {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
color: var(--muted);
width: 12px;
}
.tree-folder-name {
color: rgba(255, 255, 255, 0.85);
color: var(--ink);
}
.tree-folder-children {
padding-left: 18px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
border-left: 1px solid var(--line);
margin-left: 6px;
}
@@ -53,18 +55,18 @@
align-items: center;
gap: 8px;
padding: 4px 6px;
border-radius: 4px;
border-radius: 8px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
color: var(--muted);
}
.tree-file-label:hover {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.9);
background: rgba(255,255,255,.45);
color: var(--ink);
}
.tree-file-label input[type="checkbox"] {
accent-color: var(--color-accent);
accent-color: var(--blue);
width: 14px;
height: 14px;
cursor: pointer;
@@ -79,11 +81,11 @@
.tree-error,
.tree-empty {
padding: 1rem;
color: rgba(255, 255, 255, 0.5);
color: var(--muted);
font-size: 0.9rem;
text-align: center;
}
.tree-error {
color: #ff7b72;
color: var(--red);
}

View File

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

View File

@@ -49,8 +49,8 @@ export function FlashcardComponent() {
return (
<div className="flashcard-container">
<h2 className="title flashcard-title">Gerador de Flashcards</h2>
<p className="subtitle">Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.</p>
<h2 className="flashcard-title">Gerador de Flashcards</h2>
<p className="flashcard-subtitle">Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.</p>
<div className="flashcard-form">
<div className="input-group">
@@ -111,7 +111,7 @@ export function FlashcardComponent() {
</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'}
</Button>
@@ -119,9 +119,9 @@ export function FlashcardComponent() {
</div>
{loading && (
<div className="spinner-container">
<div className="spinner"></div>
<p>Gerando os flashcards com IA e salvando no banco. Aguarde...</p>
<div class="loading-indicator">
<span class="loading-dot"></span>
<span>Gerando os flashcards com IA e salvando no banco. Aguarde...</span>
</div>
)}

View File

@@ -1,40 +1,47 @@
.review-container {
width: 100%;
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.2rem;
animation: slideUp 0.45s ease-out;
}
.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 {
color: #ff9c96;
text-align: left;
background: rgba(255, 69, 58, 0.12);
border: 1px solid rgba(255, 69, 58, 0.4);
border-radius: 10px;
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;
margin-bottom: 1rem;
}
.review-select-panel,
.review-session-panel {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 12px;
padding: 1.25rem;
/* Library Selection */
.review-select-panel {
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
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 {
color: rgba(255, 255, 255, 0.72);
color: var(--muted);
font-size: 0.95rem;
}
@@ -44,16 +51,17 @@
}
.review-subject-section {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
border-radius: 10px;
border: 1px solid rgba(82, 54, 17, .10);
background: rgba(255,255,255,.45);
border-radius: var(--radius-md);
padding: 0.9rem;
}
.review-subject-section h3 {
margin: 0 0 0.8rem;
font-size: 1rem;
color: rgba(255, 255, 255, 0.95);
font-family: Georgia, serif;
color: var(--ink);
}
.review-library-list {
@@ -65,18 +73,18 @@
display: flex;
align-items: flex-start;
gap: 0.7rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
border: 1px solid rgba(82, 54, 17, .10);
border-radius: var(--radius-md);
padding: 0.6rem 0.7rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.03);
background: rgba(255,255,255,.48);
}
.review-library-item input[type="checkbox"] {
margin-top: 0.15rem;
width: 16px;
height: 16px;
accent-color: var(--color-accent);
accent-color: var(--blue);
}
.review-library-texts {
@@ -87,10 +95,11 @@
.review-library-texts strong {
font-size: 0.95rem;
color: var(--ink);
}
.review-library-texts span {
color: rgba(255, 255, 255, 0.68);
color: var(--muted);
font-size: 0.84rem;
}
@@ -100,72 +109,548 @@
justify-content: flex-end;
}
.review-progress {
display: flex;
align-items: center;
gap: 0.9rem;
margin-bottom: 0.8rem;
/* Session Panel - Content Grid */
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 330px;
gap: 24px;
align-items: start;
}
.review-progress span {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
min-width: 60px;
}
.review-progress-bar {
width: 100%;
height: 8px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.25);
/* Review Panel (main flashcard area) */
.review-panel {
position: relative;
min-height: 650px;
padding: clamp(18px, 3vw, 34px);
border-radius: var(--radius-xl);
background:
linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,240,202,.46)),
radial-gradient(circle at 15% 20%, rgba(255, 201, 101, .25), transparent 35%),
radial-gradient(circle at 90% 10%, rgba(63, 124, 172, .16), transparent 32%);
border: 1px solid rgba(104, 69, 22, .13);
box-shadow: var(--shadow);
overflow: hidden;
}
.review-progress-fill {
height: 100%;
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.8), rgba(var(--color-accent-rgb), 1));
.review-panel::before,
.review-panel::after {
content: "";
position: absolute;
border-radius: 999px;
transition: width 0.25s ease;
pointer-events: none;
filter: blur(2px);
opacity: .5;
}
.review-card {
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 10px;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
min-height: 250px;
.review-panel::before {
width: 220px;
height: 220px;
right: -92px;
top: 100px;
background: rgba(63, 124, 172, .13);
}
.review-card header {
margin-bottom: 0.8rem;
.review-panel::after {
width: 180px;
height: 180px;
left: -70px;
bottom: 60px;
background: rgba(199, 149, 57, .18);
}
.review-card small {
color: rgba(255, 255, 255, 0.62);
.session-header {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
}
.review-card h3 {
margin: 0 0 0.4rem;
font-size: 0.98rem;
color: rgba(255, 255, 255, 0.9);
.session-title h3 {
margin: 0;
font-family: Georgia, serif;
font-size: clamp(24px, 3vw, 36px);
letter-spacing: -.04em;
color: var(--ink);
}
.review-card p {
margin: 0 0 1rem;
font-size: 1rem;
.session-title p {
max-width: 660px;
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;
}
.review-session-actions {
margin-top: 1rem;
.card-footer {
position: relative;
z-index: 1;
display: flex;
gap: 0.65rem;
flex-wrap: wrap;
justify-content: space-between;
gap: 14px;
align-items: center;
color: var(--muted);
font-size: 13px;
font-weight: 800;
}
@media (max-width: 740px) {
.review-session-actions {
.spacebar {
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;
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));
}
}
@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 {
MindforgeApiService,
type FlashcardCard,
@@ -26,15 +26,93 @@ function difficultyLabel(difficulty: string) {
function shuffleCards(cards: FlashcardCard[]) {
const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]];
}
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() {
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
@@ -45,6 +123,13 @@ export function FlashcardReviewComponent() {
const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = 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(() => {
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 libraryById = useMemo(() => {
@@ -86,12 +177,34 @@ export function FlashcardReviewComponent() {
? ((currentIndex + 1) / sessionCards.length) * 100
: 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) => {
if (selectedLibraryIds.includes(libraryId)) {
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
return;
}
setSelectedLibraryIds([...selectedLibraryIds, libraryId]);
};
@@ -112,6 +225,10 @@ export function FlashcardReviewComponent() {
setSessionCards(shuffleCards(response.cards));
setCurrentIndex(0);
setShowAnswer(false);
setFlipped(false);
setStampState(null);
setCardExiting(false);
setSessionAnswers({});
} catch (err: any) {
setError(err?.message || 'Falha ao iniciar sessao de revisao.');
} finally {
@@ -124,48 +241,84 @@ export function FlashcardReviewComponent() {
setCurrentIndex(0);
setShowAnswer(false);
setSubmittingAnswer(false);
setFlipped(false);
setStampState(null);
setCardExiting(false);
setSessionAnswers({});
};
const goToPrevious = () => {
if (currentIndex === 0) {
return;
}
if (currentIndex === 0) return;
setCurrentIndex(currentIndex - 1);
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) => {
if (!currentCard) {
return;
}
if (!currentCard) return;
setSubmittingAnswer(true);
setError(null);
if (correct) {
setStampState('correct');
if (confettiRef.current) {
fireConfetti(confettiRef.current);
}
} else {
setStampState('wrong');
}
setCardExiting(true);
setTimeout(() => {
setCardExiting(false);
setStampState(null);
}, 600);
try {
await MindforgeApiService.recordFlashcardReviewAnswer({
cardId: currentCard.id,
correct,
});
if (currentIndex >= sessionCards.length - 1) {
endSession();
return;
}
setSessionAnswers((currentAnswers) => ({
...currentAnswers,
[currentCard.id]: correct,
}));
setCurrentIndex(currentIndex + 1);
setShowAnswer(false);
setTimeout(() => {
advanceCard();
setSubmittingAnswer(false);
}, 580);
} catch (err: any) {
setError(err?.message || 'Falha ao registrar resposta da revisao.');
} finally {
setSubmittingAnswer(false);
setCardExiting(false);
setStampState(null);
}
};
const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
const remainingCount = sessionCards.length - currentIndex;
return (
<div className="review-container">
<h2 className="title review-title">Revisao Flashcards</h2>
<p className="subtitle">Escolha as bibliotecas para estudar e inicie uma sessao de revisao.</p>
<canvas ref={confettiRef} class="confetti-canvas" />
<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>}
@@ -212,59 +365,137 @@ export function FlashcardReviewComponent() {
)}
{sessionCards.length > 0 && currentCard && (
<div className="review-session-panel">
<div className="review-progress">
<span>{currentIndex + 1} / {sessionCards.length}</span>
<div className="review-progress-bar">
<div className="review-progress-fill" style={{ width: `${progressPercent}%` }} />
<div class="content-grid">
<div class="review-panel">
<div class="session-header">
<div class="session-title">
<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>
<article className="review-card">
<header>
<small>
{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'} -
{' '}
{libraryById.get(currentCard.libraryId)?.subject || 'Geral'}
</small>
</header>
<h3>Frente</h3>
<p>{currentCard.front}</p>
{showAnswer && (
<>
<h3>Verso</h3>
<p>{currentCard.back}</p>
</>
<div class="stage">
<div
ref={flashcardRef}
class={`flashcard${flipped ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`}
onClick={() => { if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
tabIndex={0}
role="button"
aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaco para revelar'}
>
<div class="card-face">
{stampState && (
<div class={`stamp ${stampState}${stampState ? ' show' : ''}`}>
{stampState === 'correct' ? 'Correto!' : 'Errado'}
</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}>
Anterior
</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}>
Encerrar Sessao
</Button>
</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>
);

View File

@@ -1,61 +1,72 @@
.header {
position: fixed;
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);
.topbar {
min-height: 72px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
justify-content: space-between;
gap: 18px;
padding: 14px 18px;
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 {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
position: relative;
}
.header-repo {
position: absolute;
right: 24px;
.topbar-left {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 4px 10px;
gap: 12px;
cursor: pointer;
min-width: 0;
}
.header-repo-icon {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
.topbar-logo {
width: 42px;
height: 42px;
border-radius: 12px;
flex-shrink: 0;
}
.header-repo-name {
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;
.topbar-title {
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 (
<header class="header">
<div class="header-content">
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={onGoHome}>
<img src="/assets/mindforge.png" alt="Mindforge" width="55" height="55" style={{ marginRight: '10px' }} />
<h1 class="header-title">Mindforge</h1>
<header class="topbar">
<div class="topbar-left" onClick={onGoHome}>
<img class="topbar-logo" src="/assets/mindforge.png" alt="Mindforge" width="42" height="42" />
<h1 class="topbar-title">Mindforge</h1>
</div>
{repoName && (
<div class="header-repo">
<span class="header-repo-icon"></span>
<span class="header-repo-name">{repoName}</span>
<div class="topbar-right">
<span class="chip">
<span style="font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:var(--blue-deep)">Repo</span>
{repoName}
</span>
</div>
)}
</div>
</header>
);
}

View File

@@ -1,41 +1,174 @@
.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;
top: 70px;
}
.sidebar-header {
margin-bottom: 2rem;
}
.sidebar-title {
color: rgba(244, 245, 245, 0.6);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 700;
margin: 0;
}
.sidebar-nav {
top: 0;
height: 100vh;
padding: 24px 18px;
background:
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);
box-shadow: 16px 0 40px rgba(80, 54, 18, .08);
z-index: 2;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sidebar-btn {
width: 100%;
text-align: left;
.brand {
display: flex;
justify-content: flex-start;
padding: 1rem 1.2rem;
font-size: 1.05rem;
align-items: center;
gap: 14px;
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';
interface SidebarProps {
@@ -6,42 +5,35 @@ interface SidebarProps {
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) {
return (
<aside class="sidebar">
<div class="sidebar-header">
<h2 class="sidebar-title">Modulos</h2>
<div class="brand">
<div class="brand-mark">
<img src="/assets/mindforge.png" alt="M" />
</div>
<div class="sidebar-nav">
<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>
<h1>Mindforge</h1>
</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>
);
}

View File

@@ -1,43 +1,52 @@
.spaced-review-container {
width: 100%;
max-width: 1020px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.2rem;
animation: slideUp 0.45s ease-out;
}
.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 {
color: #ff9c96;
text-align: left;
background: rgba(255, 69, 58, 0.12);
border: 1px solid rgba(255, 69, 58, 0.4);
border-radius: 10px;
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;
margin-bottom: 1rem;
}
.spaced-review-panel,
.spaced-review-session-panel {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 12px;
padding: 1.25rem;
.spaced-review-panel {
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
backdrop-filter: blur(16px);
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 {
color: rgba(255, 255, 255, 0.72);
color: var(--muted);
font-size: 0.95rem;
}
/* Filters */
.spaced-review-filters {
display: flex;
flex-wrap: wrap;
@@ -49,35 +58,116 @@
display: flex;
align-items: center;
gap: 0.45rem;
border: 1px solid rgba(255, 255, 255, 0.14);
border: 1px solid rgba(82, 54, 17, .14);
border-radius: 999px;
padding: 0.35rem 0.8rem;
background: rgba(255, 255, 255, 0.05);
background: rgba(255,255,255,.48);
font-size: 0.88rem;
cursor: pointer;
color: var(--ink);
}
.spaced-review-filter input[type="checkbox"],
.spaced-review-library-item input[type="checkbox"] {
.spaced-review-filter input[type="checkbox"] {
width: 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 {
display: grid;
gap: 1rem;
}
.spaced-review-subject {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
background: rgba(0, 0, 0, 0.18);
border: 1px solid rgba(82, 54, 17, .10);
border-radius: var(--radius-md);
background: rgba(255,255,255,.45);
padding: 0.9rem;
}
.spaced-review-subject-header h3 {
margin: 0;
font-size: 1.05rem;
font-family: Georgia, serif;
color: var(--ink);
}
.spaced-review-subject-toggle,
@@ -92,19 +182,19 @@
.spaced-review-subsubject-toggle input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--color-accent);
accent-color: var(--blue);
}
.spaced-review-subject-header p {
margin: 0.35rem 0 0;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.9);
color: var(--ink);
}
.spaced-review-subject-header small {
display: block;
margin-top: 0.35rem;
color: rgba(255, 255, 255, 0.67);
color: var(--muted);
font-size: 0.79rem;
}
@@ -115,10 +205,10 @@
}
.spaced-review-subsubject-block {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
border: 1px solid rgba(82, 54, 17, .10);
border-radius: var(--radius-md);
padding: 0.7rem;
background: rgba(255, 255, 255, 0.03);
background: rgba(255,255,255,.48);
}
.spaced-review-subsubject-header {
@@ -130,16 +220,17 @@
.spaced-review-subsubject-header strong {
font-size: 0.93rem;
color: var(--ink);
}
.spaced-review-subsubject-header span {
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.78);
color: var(--muted);
}
.spaced-review-subsubject-header small {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.6);
color: var(--muted);
}
.spaced-review-library-list {
@@ -151,16 +242,39 @@
display: flex;
align-items: center;
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-radius: 10px;
border-radius: var(--radius-md);
padding: 0.6rem 0.7rem;
background: rgba(255, 255, 255, 0.03);
background: rgba(255,255,255,.48);
cursor: pointer;
}
.spaced-review-library-item input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--blue);
}
.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 {
@@ -173,6 +287,7 @@
.spaced-review-library-texts strong {
font-size: 0.91rem;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -180,103 +295,15 @@
.spaced-review-library-texts span {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.78);
color: var(--muted);
}
.spaced-review-library-texts small {
font-size: 0.77rem;
color: rgba(255, 255, 255, 0.62);
}
.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);
color: var(--muted);
}
/* Footer */
.spaced-review-footer {
margin-top: 1rem;
display: flex;
@@ -288,42 +315,53 @@
.spaced-review-footer p {
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 {
display: flex;
align-items: center;
gap: 0.9rem;
margin-bottom: 0.8rem;
margin-bottom: 1.2rem;
}
.spaced-review-progress span {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
color: var(--ink);
min-width: 60px;
font-weight: 800;
}
.spaced-review-progress-bar {
width: 100%;
height: 8px;
height: 10px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.25);
overflow: hidden;
background: rgba(80, 54, 18, .12);
}
.spaced-review-progress-fill {
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;
transition: width 0.25s ease;
}
.spaced-review-card {
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 10px;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(82, 54, 17, .14);
border-radius: var(--radius-lg);
padding: 1.25rem;
background: rgba(255,255,255,.52);
min-height: 260px;
}
@@ -337,26 +375,32 @@
}
.spaced-review-card small {
color: rgba(255, 255, 255, 0.62);
color: var(--muted);
font-size: 0.85rem;
}
.spaced-review-card h3 {
margin: 0 0 0.4rem;
font-size: 0.98rem;
color: rgba(255, 255, 255, 0.9);
font-size: 0.85rem;
font-weight: 950;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--blue-deep);
}
.spaced-review-card p {
margin: 0 0 1rem;
font-size: 1rem;
line-height: 1.55;
font-size: 1.05rem;
line-height: 1.6;
color: var(--ink);
font-family: "Segoe UI", Inter, system-ui, sans-serif;
}
.spaced-review-card-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
color: rgba(255, 255, 255, 0.75);
color: var(--muted);
font-size: 0.84rem;
}
@@ -367,6 +411,89 @@
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) {
.spaced-review-library-item {
flex-wrap: wrap;
@@ -376,4 +503,8 @@
display: grid;
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 (
<div className="spaced-review-container">
<h2 className="title spaced-review-title">Revisao espacada</h2>
<p className="subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
<h2 className="spaced-review-title">Revisao espacada</h2>
<p className="spaced-review-subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
{error && <div className="spaced-review-error">{error}</div>}
@@ -492,6 +492,7 @@ export function SpacedReviewComponent() {
)}
{sessionCards.length > 0 && currentCard && (
<div class="spaced-review-content-grid">
<div className="spaced-review-session-panel">
<div className="spaced-review-progress">
<span>{currentIndex + 1} / {sessionCards.length}</span>
@@ -553,6 +554,36 @@ export function SpacedReviewComponent() {
</Button>
</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>
);

View File

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

View File

@@ -32,7 +32,7 @@ export function VerificadorComponent() {
const handleSubmit = async () => {
if (selectedPaths.length === 0) {
setError('Selecione pelo menos um arquivo do repositório.');
setError('Selecione pelo menos um arquivo do repositorio.');
return;
}
@@ -97,44 +97,44 @@ export function VerificadorComponent() {
return (
<div className="verificador-container">
<h2 className="title" style={{ fontSize: '2.5rem' }}>Verificador de Arquivos</h2>
<p className="subtitle">Selecione os arquivos do repositório para validação de linguagem ou conteúdo.</p>
<h2 className="verificador-title">Verificador de Arquivos</h2>
<p className="verificador-subtitle">Selecione os arquivos do repositorio para validação de linguagem ou conteudo.</p>
<div className="verificador-form">
<div className="input-group">
<label>Arquivos do Repositório</label>
<label>Arquivos do Repositorio</label>
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
{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' : ''}
</div>
)}
</div>
<div className="input-group">
<label>Tipo de Verificação</label>
<label>Tipo de Verificacao</label>
<select
className="select-input"
value={checkType}
onChange={(e) => setCheckType((e.target as HTMLSelectElement).value as CheckTypeEnum)}
>
<option value="language">Linguagem</option>
<option value="content">Conteúdo</option>
<option value="both">Linguagem e Conteúdo</option>
<option value="content">Conteudo</option>
<option value="both">Linguagem e Conteudo</option>
</select>
</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'}
</Button>
{error && <div style={{ color: '#ff7b72', marginTop: '1rem' }}>{error}</div>}
{error && <div class="verificador-error">{error}</div>}
</div>
{loading && (
<div className="spinner-container">
<div className="spinner"></div>
<p>Analisando sua forja mental...</p>
<div class="loading-indicator">
<span class="loading-dot"></span>
<span>Analisando sua forja mental...</span>
</div>
)}
@@ -145,7 +145,7 @@ export function VerificadorComponent() {
<div className="file-result-title">{fileResult.fileName}</div>
{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 && (
@@ -159,7 +159,7 @@ export function VerificadorComponent() {
{!fileResult.error && checkType === 'content' && fileResult.contentResult && (
<div className="side-pane">
<div className="pane-title">Conteúdo</div>
<div className="pane-title">Conteudo</div>
<div
className="response-content markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
@@ -176,7 +176,7 @@ export function VerificadorComponent() {
</div>
</div>
<div className="side-pane">
<div className="pane-title">Conteúdo</div>
<div className="pane-title">Conteudo</div>
<div
className="response-content markdown-body"
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 {
--color-bg: #005873;
--color-header: #0f0f0f;
--color-sidebar: #013a4c;
--color-text-creamy: #f4f5f5;
--color-accent: #00b4d8;
--color-accent-rgb: 0, 180, 216;
--color-accent-hover: #0096c7;
--bg: #fbf3df;
--bg-soft: #fff9ea;
--paper: #fffaf0;
--paper-deep: #f2dfb3;
--ink: #2b241c;
--muted: #7b6a50;
--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 {
font-family: var(--font-main);
background-color: var(--color-bg);
color: var(--color-text-creamy);
margin: 0;
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;
}
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 {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-layout {
display: flex;
flex: 1;
margin-top: 70px; /* offset for fixed header */
.app {
min-height: 100vh;
display: grid;
grid-template-columns: 288px minmax(0, 1fr);
}
.content-area {
flex: 1;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
.main {
min-width: 0;
padding: 22px 28px 38px;
}
.title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 2rem;
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
@media (max-width: 1120px) {
.app {
grid-template-columns: 84px minmax(0, 1fr);
}
}
@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.
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) |
> 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)
| Tecnologia | Versão | Finalidade |
@@ -125,9 +125,10 @@ Formas de requisição principais:
- **CSS**: Um arquivo `.css` por componente, importado diretamente. **Sem CSS Modules.**
- **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`.
- **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`.
- **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)
- **Namespaces**: `Mindforge.API.Controllers`, `Mindforge.API.Services`, etc.
@@ -138,21 +139,38 @@ Formas de requisição principais:
### UI/UX
- **Idioma**: Todo texto em **português brasileiro**.
- **Tema**: Escuro com efeito vidro (glassy). `backdrop-filter: blur()`, fundos `rgba` semitransparentes.
- **Botões**: Estilo iOS-like, modernos. Variantes `primary` (com blur) e `secondary` (transparente).
- **Tipografia**: Lato (Google Fonts), pesos 300/400/700.
- **Background**: `#005873` (azul petróleo escuro). Não muito escuro.
- **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 pill quente com sombras marrons. Variantes `primary` (gradiente azul, com brilho hover) e `secondary` (translúcido pergaminho).
- **Tipografia**: Inter (UI global), Georgia/Times New Roman (marca e cabeçalhos), Segoe UI/Inter (conteúdo do flashcard).
- **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`)
```css
--color-bg: #005873;
--color-header: #0f0f0f;
--color-sidebar: #013a4c;
--color-text-creamy: #f4f5f5;
--color-accent: #00b4d8;
--color-accent-rgb: 0, 180, 216;
--color-accent-hover: #0096c7;
--font-main: 'Lato', sans-serif;
--bg: #fbf3df;
--bg-soft: #fff9ea;
--paper: #fffaf0;
--paper-deep: #f2dfb3;
--ink: #2b241c;
--muted: #7b6a50;
--line: rgba(82, 60, 28, 0.18);
--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