re-design
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<Sidebar activeModule={activeModule} onModuleChange={setActiveModule} />
|
||||
<main class="content-area">
|
||||
<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>
|
||||
<p class="hero-subtitle">Sua ferramenta de estudos para concursos.</p>
|
||||
</div>
|
||||
<div class="app">
|
||||
<Sidebar activeModule={activeModule} onModuleChange={setActiveModule} />
|
||||
<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: '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 style={{ display: activeModule === 'verificador' ? 'block' : 'none', height: '100%', width: '100%' }}>
|
||||
<VerificadorComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}>
|
||||
<FlashcardComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}>
|
||||
<FlashcardReviewComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', height: '100%', width: '100%' }}>
|
||||
<SpacedReviewComponent />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'verificador' ? 'block' : 'none', width: '100%' }}>
|
||||
<VerificadorComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'flashcards' ? 'block' : 'none', width: '100%' }}>
|
||||
<FlashcardComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', width: '100%' }}>
|
||||
<FlashcardReviewComponent />
|
||||
</div>
|
||||
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', width: '100%' }}>
|
||||
<SpacedReviewComponent />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
display: grid;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,57 +365,135 @@ 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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
<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 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>
|
||||
<Button variant="secondary" onClick={endSession}>
|
||||
Encerrar Sessao
|
||||
</Button>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
<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 className="review-session-actions">
|
||||
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
|
||||
Anterior
|
||||
</Button>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{!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>
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
{repoName && (
|
||||
<div class="header-repo">
|
||||
<span class="header-repo-icon">⎇</span>
|
||||
<span class="header-repo-name">{repoName}</span>
|
||||
</div>
|
||||
)}
|
||||
<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="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>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
<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>
|
||||
<div class="brand">
|
||||
<div class="brand-mark">
|
||||
<img src="/assets/mindforge.png" alt="M" />
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,68 +492,99 @@ export function SpacedReviewComponent() {
|
||||
)}
|
||||
|
||||
{sessionCards.length > 0 && currentCard && (
|
||||
<div className="spaced-review-session-panel">
|
||||
<div className="spaced-review-progress">
|
||||
<span>{currentIndex + 1} / {sessionCards.length}</span>
|
||||
<div className="spaced-review-progress-bar">
|
||||
<div className="spaced-review-progress-fill" style={{ width: `${progressPercent}%` }} />
|
||||
<div class="spaced-review-content-grid">
|
||||
<div className="spaced-review-session-panel">
|
||||
<div className="spaced-review-progress">
|
||||
<span>{currentIndex + 1} / {sessionCards.length}</span>
|
||||
<div className="spaced-review-progress-bar">
|
||||
<div className="spaced-review-progress-fill" style={{ width: `${progressPercent}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article className="spaced-review-card">
|
||||
<header>
|
||||
<small>
|
||||
{currentLibrary?.fileName || 'Arquivo'} - {currentLibrary?.subject || 'Geral'} - {currentLibrary?.subSubject || 'Geral'}
|
||||
</small>
|
||||
<span className={`rag-badge ${currentStatusMeta.className}`}>
|
||||
<span className="rag-icon">{currentStatusMeta.icon}</span>
|
||||
{currentStatusMeta.label}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<h3>Frente</h3>
|
||||
<p>{currentCard.front}</p>
|
||||
|
||||
{showAnswer && (
|
||||
<>
|
||||
<h3>Verso</h3>
|
||||
<p>{currentCard.back}</p>
|
||||
<div className="spaced-review-card-meta">
|
||||
<span>Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'}</span>
|
||||
<span>Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<div className="spaced-review-session-actions">
|
||||
<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={() => registerAnswer(true)} disabled={submittingAnswer}>
|
||||
Acertei
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => registerAnswer(false)} disabled={submittingAnswer}>
|
||||
Errei
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="secondary" onClick={endSession}>
|
||||
Encerrar Sessao
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article className="spaced-review-card">
|
||||
<header>
|
||||
<small>
|
||||
{currentLibrary?.fileName || 'Arquivo'} - {currentLibrary?.subject || 'Geral'} - {currentLibrary?.subSubject || 'Geral'}
|
||||
</small>
|
||||
<span className={`rag-badge ${currentStatusMeta.className}`}>
|
||||
<span className="rag-icon">{currentStatusMeta.icon}</span>
|
||||
{currentStatusMeta.label}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<h3>Frente</h3>
|
||||
<p>{currentCard.front}</p>
|
||||
|
||||
{showAnswer && (
|
||||
<>
|
||||
<h3>Verso</h3>
|
||||
<p>{currentCard.back}</p>
|
||||
<div className="spaced-review-card-meta">
|
||||
<span>Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'}</span>
|
||||
<span>Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)}</span>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<div className="spaced-review-session-actions">
|
||||
<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={() => registerAnswer(true)} disabled={submittingAnswer}>
|
||||
Acertei
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => registerAnswer(false)} disabled={submittingAnswer}>
|
||||
Errei
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="secondary" onClick={endSession}>
|
||||
Encerrar Sessao
|
||||
</Button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user