diff --git a/app/static/css/style.css b/app/static/css/style.css index 2ba4f75..4b83906 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -7238,6 +7238,14 @@ body.drawer-open { max-width: 960px; margin: 0 auto; width: 100%; + box-sizing: border-box; +} + +@media (max-width: 768px) { + .lesson-view, .lesson-runner-view { + padding: 12px 10px; + padding-bottom: calc(75px + var(--sab)); + } } .lesson-configurator-container { @@ -7246,6 +7254,7 @@ body.drawer-open { gap: 20px; } +/* Header Bar */ .lesson-header-bar { display: flex; justify-content: space-between; @@ -7264,20 +7273,22 @@ body.drawer-open { align-items: center; gap: 10px; flex-wrap: wrap; + color: var(--text-main); } .lesson-subtitle { margin: 0; font-size: 14px; + color: var(--text-muted); } .badge-beta { - background: linear-gradient(135deg, #8b5cf6, #3b82f6); + background: linear-gradient(135deg, #8b5cf6, #38bdf8); color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; - border-radius: 999px; + border-radius: var(--radius-full); text-transform: uppercase; letter-spacing: 0.5px; } @@ -7300,14 +7311,42 @@ body.drawer-open { } .pulse-btn { - box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); + box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7); animation: btnPulse 2s infinite; } @keyframes btnPulse { - 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } - 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } - 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.4); } + 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(56, 189, 248, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(56, 189, 248, 0); } +} + +@media (max-width: 768px) { + .lesson-header-bar { + flex-direction: column; + align-items: stretch; + gap: 14px; + padding-bottom: 14px; + } + .lesson-title-wrap h2 { + font-size: 20px; + } + .lesson-subtitle { + font-size: 13px; + } + .lesson-header-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + width: 100%; + } + .lesson-header-actions #lesson-start-btn { + grid-column: span 2; + order: -1; + padding: 14px; + font-size: 16px; + font-weight: 800; + } } /* Presets Chips Bar */ @@ -7329,8 +7368,12 @@ body.drawer-open { display: flex; gap: 10px; overflow-x: auto; - padding-bottom: 4px; + padding-bottom: 6px; -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.lesson-presets-chips-row::-webkit-scrollbar { + display: none; } .lesson-preset-chip { @@ -7339,25 +7382,28 @@ body.drawer-open { gap: 8px; padding: 9px 16px; background: var(--bg-main); - border: 1px solid var(--border-color); - border-radius: 999px; + border: 1.5px solid var(--border-color); + border-radius: var(--radius-full); font-size: 13.5px; font-weight: 600; color: var(--text-main); cursor: pointer; white-space: nowrap; - transition: all 0.15s ease; + transition: var(--transition); + user-select: none; + -webkit-tap-highlight-color: transparent; } .lesson-preset-chip:hover { border-color: var(--primary); - background: var(--bg-hover); + background: var(--bg-surface-hover); } .lesson-preset-chip.active { background: var(--primary); color: #fff; border-color: var(--primary); + box-shadow: 0 2px 10px rgba(56, 189, 248, 0.35); } .preset-del-btn { @@ -7365,7 +7411,7 @@ body.drawer-open { opacity: 0.7; font-size: 12px; border-radius: 50%; - padding: 2px 4px; + padding: 2px 6px; } .preset-del-btn:hover { @@ -7376,6 +7422,7 @@ body.drawer-open { .preset-description-text { font-size: 13px; margin: 8px 0 0 0; + color: var(--text-muted); } /* Blocks List & Cards */ @@ -7392,10 +7439,12 @@ body.drawer-open { margin: 0; font-size: 18px; font-weight: 700; + color: var(--text-main); } .blocks-tip { font-size: 12.5px; + color: var(--text-muted); } .lesson-steps-list { @@ -7407,13 +7456,13 @@ body.drawer-open { .lesson-step-card { background: var(--bg-card); border: 1px solid var(--border-color); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); padding: 16px 18px; - transition: all 0.2s ease; + transition: border-color 0.2s ease, box-shadow 0.2s ease; } .lesson-step-card:hover { - border-color: rgba(59, 130, 246, 0.4); + border-color: rgba(56, 189, 248, 0.4); } .lesson-step-card.step-disabled { @@ -7425,18 +7474,20 @@ body.drawer-open { display: flex; justify-content: space-between; align-items: center; - gap: 12px; + gap: 10px; } .step-card-title-wrap { display: flex; align-items: center; gap: 10px; + min-width: 0; + flex: 1; } .step-num { - width: 26px; - height: 26px; + width: 28px; + height: 28px; background: var(--bg-main); border: 1px solid var(--border-color); border-radius: 50%; @@ -7445,28 +7496,48 @@ body.drawer-open { justify-content: center; font-size: 12px; font-weight: 800; + flex-shrink: 0; + color: var(--text-main); } .step-icon { - font-size: 18px; + font-size: 20px; + flex-shrink: 0; } .step-title { font-size: 15px; + font-weight: 700; + color: var(--text-main); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .step-card-actions { display: flex; align-items: center; gap: 8px; + flex-shrink: 0; +} + +.step-card-actions .icon-btn { + min-width: 34px; + min-height: 34px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + font-size: 13px; } /* Switch Toggle */ .switch-toggle { position: relative; display: inline-block; - width: 44px; - height: 24px; + width: 46px; + height: 26px; + flex-shrink: 0; } .switch-toggle input { @@ -7481,14 +7552,14 @@ body.drawer-open { top: 0; left: 0; right: 0; bottom: 0; background-color: var(--border-color); transition: .2s; - border-radius: 24px; + border-radius: 26px; } .toggle-slider:before { position: absolute; content: ""; - height: 18px; - width: 18px; + height: 20px; + width: 20px; left: 3px; bottom: 3px; background-color: white; @@ -7525,35 +7596,37 @@ input:checked + .toggle-slider:before { } .param-group label { - font-size: 12px; + font-size: 12.5px; font-weight: 600; color: var(--text-muted); } .param-group.flex-2 { flex: 2; - min-width: 220px; + min-width: 240px; } .segmented-control { display: inline-flex; background: var(--bg-main); border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 2px; - gap: 2px; + border-radius: var(--radius-md); + padding: 3px; + gap: 3px; } .seg-btn { border: none; background: transparent; - padding: 6px 14px; + padding: 7px 14px; font-size: 13px; font-weight: 600; color: var(--text-muted); - border-radius: 4px; + border-radius: var(--radius-sm); cursor: pointer; transition: all 0.15s ease; + user-select: none; + -webkit-tap-highlight-color: transparent; } .seg-btn:hover { @@ -7563,6 +7636,79 @@ input:checked + .toggle-slider:before { .seg-btn.active { background: var(--primary); color: #fff; + font-weight: 700; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} + +/* Mode Segmented Control: 2x2 grid on mobile, horizontal pill on desktop */ +.mode-segmented-control { + display: flex; + flex-wrap: wrap; + gap: 4px; + width: 100%; +} + +.mode-segmented-control .seg-btn { + flex: 1 1 calc(50% - 4px); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 8px; + font-size: 13px; + text-align: center; +} + +@media (min-width: 768px) { + .step-param-row { + flex-direction: row; + align-items: flex-end; + } + .mode-segmented-control { + display: inline-flex; + flex-wrap: nowrap; + width: auto; + } + .mode-segmented-control .seg-btn { + flex: initial; + padding: 7px 14px; + } +} + +@media (max-width: 767px) { + .step-param-row { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + .param-group { + width: 100%; + } + .param-group.flex-2 { + min-width: 0; + } + .step-select-param { + width: 100%; + padding: 11px 12px; + font-size: 14px; + } +} + +.step-select-param { + background: var(--bg-main); + color: var(--text-main); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 9px 12px; + font-size: 13.5px; + font-weight: 500; + cursor: pointer; + outline: none; + transition: var(--transition); +} + +.step-select-param:focus { + border-color: var(--primary); } .ai-debrief-info-box { @@ -7570,26 +7716,42 @@ input:checked + .toggle-slider:before { align-items: center; gap: 12px; background: rgba(139, 92, 246, 0.08); - border: 1px dashed rgba(139, 92, 246, 0.3); - padding: 12px 16px; + border: 1px dashed rgba(139, 92, 246, 0.35); + padding: 14px 16px; border-radius: var(--radius-md); font-size: 13.5px; color: var(--text-main); + line-height: 1.5; } .ai-box-icon { font-size: 24px; + flex-shrink: 0; } +/* Bottom Floating CTA */ .lesson-bottom-cta { padding: 10px 0; } +@media (max-width: 768px) { + .lesson-bottom-cta { + position: sticky; + bottom: calc(12px + var(--sab)); + z-index: 40; + padding: 8px 0; + margin-top: 16px; + } + .lesson-bottom-cta .btn { + box-shadow: 0 4px 20px rgba(56, 189, 248, 0.4); + } +} + .btn-xl { - padding: 14px 24px; + padding: 15px 24px; font-size: 17px; font-weight: 800; - border-radius: var(--radius-md); + border-radius: var(--radius-lg); } /* ======================================================================= @@ -7604,12 +7766,18 @@ input:checked + .toggle-slider:before { padding: 14px 0; border-bottom: 1px solid var(--border-color); flex-wrap: wrap; + position: sticky; + top: 0; + z-index: 50; + background: var(--bg-main); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } .runner-nav-left { display: flex; align-items: center; - gap: 14px; + gap: 12px; } .runner-timer-wrap { @@ -7618,33 +7786,39 @@ input:checked + .toggle-slider:before { font-weight: 700; color: var(--text-main); background: var(--bg-card); - padding: 4px 10px; - border-radius: var(--radius-sm); + padding: 5px 12px; + border-radius: var(--radius-full); border: 1px solid var(--border-color); + white-space: nowrap; } .runner-breadcrumbs-row { display: flex; align-items: center; - gap: 8px; + gap: 6px; overflow-x: auto; - max-width: 550px; + max-width: 540px; padding: 4px 0; -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.runner-breadcrumbs-row::-webkit-scrollbar { + display: none; } .runner-crumb { display: inline-flex; align-items: center; gap: 6px; - padding: 4px 10px; - border-radius: 999px; + padding: 5px 12px; + border-radius: var(--radius-full); font-size: 12px; font-weight: 600; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-muted); white-space: nowrap; + transition: var(--transition); } .crumb-num { @@ -7653,8 +7827,8 @@ input:checked + .toggle-slider:before { } .runner-crumb.step-done { - background: rgba(34, 197, 94, 0.15); - border-color: rgba(34, 197, 94, 0.4); + background: rgba(16, 185, 129, 0.15); + border-color: rgba(16, 185, 129, 0.4); color: var(--success); } @@ -7662,7 +7836,7 @@ input:checked + .toggle-slider:before { background: var(--primary); border-color: var(--primary); color: #fff; - box-shadow: 0 0 10px rgba(59, 130, 246, 0.4); + box-shadow: 0 0 12px rgba(56, 189, 248, 0.45); } .crumb-separator { @@ -7670,30 +7844,81 @@ input:checked + .toggle-slider:before { font-size: 11px; } +@media (max-width: 768px) { + .lesson-runner-header { + padding: 10px 8px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 8px; + align-items: center; + } + .runner-nav-left { + gap: 6px; + } + .runner-nav-left .btn { + padding: 6px 10px; + font-size: 12px; + } + .runner-timer-wrap { + font-size: 13px; + padding: 4px 8px; + } + .runner-breadcrumbs-row { + max-width: none; + flex-wrap: nowrap; + gap: 4px; + justify-content: center; + } + .runner-crumb { + padding: 4px 8px; + font-size: 11px; + } + /* On mobile, collapse inactive crumbs to save horizontal space */ + .runner-crumb:not(.step-active) .crumb-label { + display: none; + } + .crumb-separator { + font-size: 9px; + } + .runner-nav-right .btn { + padding: 6px 8px; + font-size: 12px; + white-space: nowrap; + } +} + .lesson-runner-progress-track { width: 100%; height: 4px; - background: var(--bg-hover); + background: var(--bg-surface-hover); overflow: hidden; margin-bottom: 24px; } .lesson-runner-progress-fill { height: 100%; - background: linear-gradient(90deg, #3b82f6, #8b5cf6); + background: linear-gradient(90deg, #38bdf8, #8b5cf6); transition: width 0.3s ease; } +/* Stage Card Box */ .step-card-box { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 28px 24px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); max-width: 760px; margin: 0 auto; } +@media (max-width: 640px) { + .step-card-box { + padding: 18px 14px; + border-radius: var(--radius-md); + } +} + .step-mini-header { display: flex; justify-content: space-between; @@ -7703,9 +7928,16 @@ input:checked + .toggle-slider:before { flex-wrap: wrap; } +.step-tags-wrap { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + .step-term-display { text-align: center; - padding: 20px 0; + padding: 16px 0; } .display-term { @@ -7713,6 +7945,7 @@ input:checked + .toggle-slider:before { font-weight: 800; margin: 0 0 6px 0; color: var(--text-main); + letter-spacing: -0.5px; } .display-phonetic { @@ -7726,30 +7959,149 @@ input:checked + .toggle-slider:before { padding: 12px 16px; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; margin-top: 14px; + font-size: 15px; + line-height: 1.5; } +/* ----------------------------------------------------------------------- + Vocabulary Step: Modes + ----------------------------------------------------------------------- */ + +/* Mode 1: Quiz (4 Options) */ +.lesson-quiz-card { + max-width: 680px; + margin: 0 auto; +} + +.lesson-quiz-options-grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + width: 100%; +} + +@media (min-width: 640px) { + .lesson-quiz-options-grid { + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } +} + +.lesson-quiz-opt-btn { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + min-height: 56px; + background: var(--bg-main); + border: 1.5px solid var(--border-color); + border-radius: var(--radius-md); + font-size: 15px; + font-weight: 600; + color: var(--text-main); + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.lesson-quiz-opt-btn:hover:not(:disabled) { + border-color: var(--primary); + background: var(--bg-surface-hover); + transform: translateY(-1px); +} + +.lesson-quiz-opt-btn:active:not(:disabled) { + transform: scale(0.98); +} + +.quiz-opt-num { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-card); + border: 1px solid var(--border-color); + font-size: 12px; + font-weight: 700; + color: var(--text-muted); + flex-shrink: 0; +} + +.quiz-opt-text { + flex: 1; + line-height: 1.35; + word-break: break-word; +} + +.lesson-quiz-opt-btn.opt-correct { + background: rgba(16, 185, 129, 0.18) !important; + border-color: var(--success) !important; + color: var(--success) !important; +} + +.lesson-quiz-opt-btn.opt-correct .quiz-opt-num { + background: var(--success); + color: #fff; + border-color: var(--success); +} + +.lesson-quiz-opt-btn.opt-wrong { + background: rgba(239, 68, 68, 0.18) !important; + border-color: var(--danger) !important; + color: var(--danger) !important; +} + +.lesson-quiz-opt-btn.opt-wrong .quiz-opt-num { + background: var(--danger); + color: #fff; + border-color: var(--danger); +} + +/* Mode 2: Flashcards */ .translation-reveal-banner { - background: rgba(59, 130, 246, 0.1); - border: 1px solid rgba(59, 130, 246, 0.3); - padding: 14px 18px; + background: rgba(56, 189, 248, 0.1); + border: 1px solid rgba(56, 189, 248, 0.3); + padding: 16px 20px; border-radius: var(--radius-md); text-align: center; - font-size: 20px; color: var(--text-main); } +.tr-text { + font-size: 22px; + display: block; + margin-top: 4px; +} + +.tr-definition { + font-size: 14px; +} + .rating-buttons-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; } +@media (max-width: 640px) { + .rating-buttons-row { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } +} + .srs-btn-rating { display: flex; flex-direction: column; align-items: center; padding: 12px 6px; gap: 4px; + min-height: 52px; + border-radius: var(--radius-md); } .rating-key { @@ -7762,7 +8114,69 @@ input:checked + .toggle-slider:before { font-weight: 700; } -/* Grammar Questions Stage */ +/* Mode 3: Typing & Mode 4: Dictation */ +.typing-input-wrap { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; +} + +@media (min-width: 640px) { + .typing-input-wrap { + flex-direction: row; + } + .typing-input-wrap .typing-main-input { + flex: 1; + } +} + +.typing-main-input { + font-size: 16px !important; /* Prevents auto-zoom on mobile safari/chrome */ + padding: 12px 16px; + border-radius: var(--radius-md); +} + +.dictation-hero-play { + padding: 10px 0; +} + +.dictation-play-btn { + padding: 14px 28px; + font-size: 16px; + font-weight: 700; + border-radius: var(--radius-full); +} + +.dictation-hint { + font-size: 14px; +} + +/* Shared Trainer Feedback Box */ +.trainer-feedback-box { + padding: 14px 18px; + border-radius: var(--radius-md); + font-size: 14.5px; + line-height: 1.5; + animation: fadeIn 0.2s ease-out; +} + +.trainer-feedback-box.feedback-correct { + background: rgba(16, 185, 129, 0.12); + border: 1px solid rgba(16, 185, 129, 0.35); + color: var(--text-main); +} + +.trainer-feedback-box.feedback-wrong { + background: rgba(239, 68, 68, 0.12); + border: 1px solid rgba(239, 68, 68, 0.35); + color: var(--text-main); +} + +/* ----------------------------------------------------------------------- + Grammar Questions & Theory Stage + ----------------------------------------------------------------------- */ + .grammar-question-box { background: var(--bg-main); border: 1px solid var(--border-color); @@ -7775,6 +8189,7 @@ input:checked + .toggle-slider:before { font-weight: 600; line-height: 1.5; margin: 0 0 10px 0; + color: var(--text-main); } .clue-tag { @@ -7788,13 +8203,21 @@ input:checked + .toggle-slider:before { gap: 12px; } +@media (max-width: 640px) { + .grammar-options-grid { + grid-template-columns: 1fr; + gap: 10px; + } +} + .grammar-opt-btn { display: flex; align-items: center; gap: 12px; padding: 14px 18px; + min-height: 54px; background: var(--bg-main); - border: 1px solid var(--border-color); + border: 1.5px solid var(--border-color); border-radius: var(--radius-md); cursor: pointer; font-size: 15px; @@ -7802,15 +8225,17 @@ input:checked + .toggle-slider:before { color: var(--text-main); transition: all 0.15s ease; text-align: left; + user-select: none; + -webkit-tap-highlight-color: transparent; } .grammar-opt-btn:hover:not(:disabled) { border-color: var(--primary); - background: var(--bg-hover); + background: var(--bg-surface-hover); } .grammar-opt-btn.opt-correct { - background: rgba(34, 197, 94, 0.18) !important; + background: rgba(16, 185, 129, 0.18) !important; border-color: var(--success) !important; color: var(--success) !important; } @@ -7822,15 +8247,17 @@ input:checked + .toggle-slider:before { } .opt-key { - width: 22px; - height: 22px; + width: 26px; + height: 26px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 11px; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; } .feedback-banner { @@ -7839,8 +8266,8 @@ input:checked + .toggle-slider:before { } .feedback-banner.fb-success { - background: rgba(34, 197, 94, 0.12); - border: 1px solid rgba(34, 197, 94, 0.35); + background: rgba(16, 185, 129, 0.12); + border: 1px solid rgba(16, 185, 129, 0.35); } .feedback-banner.fb-danger { @@ -7856,10 +8283,122 @@ input:checked + .toggle-slider:before { .fb-text { font-size: 14px; - line-height: 1.4; + line-height: 1.45; } -/* Audio Player Card */ +/* Murphy Golden Formula & Contrasts */ +.golden-formula-box { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.08), rgba(168, 85, 247, 0.08)); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: var(--radius-md); + padding: 16px; +} + +.formula-label { + font-size: 12px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--primary); + margin-bottom: 8px; +} + +.formula-code { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 10px 14px; + font-family: var(--font-mono, monospace); + font-size: 14px; + font-weight: 700; + color: var(--text-main); + overflow-x: auto; + white-space: pre-wrap; + margin: 0; +} + +.contrast-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.contrast-row { + display: flex; + flex-direction: column; + gap: 12px; +} + +@media (min-width: 640px) { + .contrast-row { + flex-direction: row; + gap: 16px; + } +} + +.contrast-col { + flex: 1; + background: var(--bg-main); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 12px; +} + +.contrast-col.a { + border-left: 3px solid #38bdf8; +} + +.contrast-col.b { + border-left: 3px solid #10b981; +} + +.contrast-col .col-title { + font-size: 13px; + font-weight: 700; + color: var(--text-main); + margin-bottom: 6px; +} + +.contrast-col .col-example { + font-size: 15px; + font-weight: 600; + color: var(--text-main); + margin-bottom: 4px; +} + +.contrast-col .col-trans { + font-size: 13px; + color: var(--text-muted); +} + +.contrast-diff { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: var(--radius-sm); + padding: 8px 12px; + font-size: 13px; + color: var(--text-main); +} + +.journaling-box { + background: rgba(56, 189, 248, 0.06); + border: 1px dashed rgba(56, 189, 248, 0.3); + border-radius: var(--radius-md); + padding: 14px 18px; +} + +.hw-text { + font-size: 14px; + font-weight: 600; + color: var(--text-main); +} + +/* ----------------------------------------------------------------------- + Listening Stage + ----------------------------------------------------------------------- */ + .audio-player-card { background: var(--bg-main); border: 1px solid var(--border-color); @@ -7881,6 +8420,12 @@ input:checked + .toggle-slider:before { gap: 10px; } +@media (max-width: 640px) { + .answer-input-group { + flex-direction: column; + } +} + .transcript-box { background: var(--bg-main); border: 1px solid var(--border-color); @@ -7889,30 +8434,33 @@ input:checked + .toggle-slider:before { font-size: 13.5px; } -/* Reading Article Interactive */ +/* ----------------------------------------------------------------------- + Reading Stage + ----------------------------------------------------------------------- */ + .article-paragraph-card { background: var(--bg-main); border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: 16px 18px; + padding: 18px; margin-bottom: 14px; } .article-para-en { font-size: 17px; - line-height: 1.7; + line-height: 1.75; margin: 0 0 10px 0; } .clickable-reader-word { cursor: pointer; border-radius: 3px; - padding: 1px 2px; + padding: 1px 3px; transition: background 0.15s; } .clickable-reader-word:hover { - background: rgba(59, 130, 246, 0.25); + background: rgba(56, 189, 248, 0.25); color: var(--primary); } @@ -7947,24 +8495,36 @@ input:checked + .toggle-slider:before { .vh-chip { background: var(--bg-card); border: 1px solid var(--border-color); - padding: 4px 10px; - border-radius: 999px; + padding: 5px 12px; + border-radius: var(--radius-full); font-size: 12.5px; } -/* Word Popup in Lesson */ +/* Word Popup in Lesson: Floating on laptop, Bottom sheet on mobile */ .lesson-popup { position: fixed; z-index: 1000; - width: 250px; + width: 270px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: 12px 14px; - box-shadow: 0 8px 30px rgba(0,0,0,0.18); + padding: 14px 16px; + box-shadow: 0 10px 30px rgba(0,0,0,0.25); animation: fadeIn 0.15s ease-out; } +@media (max-width: 640px) { + .lesson-popup { + left: 14px !important; + right: 14px !important; + bottom: calc(20px + var(--sab)) !important; + top: auto !important; + width: auto !important; + border-radius: var(--radius-lg); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); + } +} + .popup-header { display: flex; justify-content: space-between; @@ -7979,15 +8539,19 @@ input:checked + .toggle-slider:before { background: transparent; cursor: pointer; color: var(--text-muted); + font-size: 16px; } .popup-trans { - font-size: 14px; - font-weight: 600; + font-size: 15px; + font-weight: 700; color: var(--text-main); } -/* Debrief Final Card */ +/* ----------------------------------------------------------------------- + AI Debrief Stage + ----------------------------------------------------------------------- */ + .lesson-debrief-card { max-width: 800px; } @@ -8011,10 +8575,11 @@ input:checked + .toggle-slider:before { background: var(--bg-main); border: 3px solid var(--primary); border-radius: 50%; + flex-shrink: 0; } .grade-badge { - font-size: 32px; + font-size: 34px; font-weight: 900; line-height: 1; color: var(--primary); @@ -8028,12 +8593,14 @@ input:checked + .toggle-slider:before { .debrief-titles { flex: 1; + min-width: 220px; } .debrief-headline { font-size: 22px; font-weight: 800; margin: 0 0 8px 0; + color: var(--text-main); } .blocks-scores-grid { @@ -8055,6 +8622,7 @@ input:checked + .toggle-slider:before { font-size: 13px; font-weight: 600; margin-bottom: 6px; + color: var(--text-main); } .bs-bar { @@ -8083,11 +8651,12 @@ input:checked + .toggle-slider:before { } .tomorrow-focus-list li { - padding: 6px 10px; + padding: 8px 12px; background: var(--bg-main); border-left: 3px solid var(--primary); margin-bottom: 6px; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + font-size: 14px; } .error-cards-list { @@ -8105,6 +8674,7 @@ input:checked + .toggle-slider:before { border-radius: var(--radius-md); padding: 12px 14px; font-size: 14px; + line-height: 1.45; } .celebrate-badge { @@ -8116,6 +8686,7 @@ input:checked + .toggle-slider:before { font-size: 24px; font-weight: 800; margin-bottom: 8px; + color: var(--text-main); } .score-highlight { @@ -8168,19 +8739,8 @@ input:checked + .toggle-slider:before { } @media (max-width: 640px) { - .grammar-options-grid { - grid-template-columns: 1fr; - } - .rating-buttons-row { - grid-template-columns: repeat(2, 1fr); - } - .step-card-box { - padding: 18px 14px; - } .display-term { font-size: 26px; } } - - diff --git a/app/static/js/modules/lesson.js b/app/static/js/modules/lesson.js index 55caffe..2dc8383 100644 --- a/app/static/js/modules/lesson.js +++ b/app/static/js/modules/lesson.js @@ -80,6 +80,14 @@ function setupEventListeners() { if (historyBtn) { historyBtn.addEventListener('click', () => openHistoryModal()); } + + // Runner keyboard shortcuts listener + window.addEventListener('keydown', (e) => { + if (!session.active) return; + if (typeof session.currentStepKeyHandler === 'function') { + session.currentStepKeyHandler(e); + } + }); } // --------------------------------------------------------------------------- @@ -250,6 +258,7 @@ function getStepDefaultTitle(type) { function renderStepControls(step, idx) { if (step.type === 'vocab') { + const currentMode = step.mode || 'flashcards'; return `
@@ -260,14 +269,14 @@ function renderStepControls(step, idx) { `).join('')}
-
- - +
+ +
+ + + + +
`; @@ -418,10 +427,14 @@ function attachStepControlsEvents(container) { btn.addEventListener('click', () => { const idx = parseInt(ctrl.dataset.idx, 10); const param = ctrl.dataset.param; - const val = parseInt(btn.dataset.val, 10); - customSteps[idx][param] = val; + const rawVal = btn.dataset.val; + const val = (rawVal !== '' && !isNaN(Number(rawVal))) ? Number(rawVal) : rawVal; + if (customSteps[idx]) { + customSteps[idx][param] = val; + } ctrl.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); + updateEstimatedDuration(); }); }); }); @@ -430,7 +443,45 @@ function attachStepControlsEvents(container) { sel.addEventListener('change', () => { const idx = parseInt(sel.dataset.idx, 10); const param = sel.dataset.param; - customSteps[idx][param] = sel.value; + if (customSteps[idx]) { + customSteps[idx][param] = sel.value; + } + updateEstimatedDuration(); + }); + }); +} + +function updateEstimatedDuration() { + const timeEl = document.getElementById('lesson-preset-duration'); + if (timeEl) { + timeEl.textContent = `~${calculateEstimatedTime(customSteps)} мин`; + } +} + +function syncDomToCustomSteps() { + const list = document.getElementById('lesson-steps-list'); + if (!list) return; + list.querySelectorAll('.lesson-step-card').forEach(cardEl => { + const idx = parseInt(cardEl.dataset.idx, 10); + if (isNaN(idx) || !customSteps[idx]) return; + + const cb = cardEl.querySelector('.step-toggle-checkbox'); + if (cb) customSteps[idx].enabled = cb.checked; + + cardEl.querySelectorAll('.segmented-control').forEach(ctrl => { + const param = ctrl.dataset.param; + const activeBtn = ctrl.querySelector('.seg-btn.active'); + if (param && activeBtn) { + const rawVal = activeBtn.dataset.val; + customSteps[idx][param] = (rawVal !== '' && !isNaN(Number(rawVal))) ? Number(rawVal) : rawVal; + } + }); + + cardEl.querySelectorAll('.step-select-param').forEach(sel => { + const param = sel.dataset.param; + if (param && sel.value !== undefined) { + customSteps[idx][param] = sel.value; + } }); }); } @@ -488,6 +539,9 @@ function calculateEstimatedTime(steps) { // --------------------------------------------------------------------------- export async function startLessonSession(preset = null) { + if (!preset) { + syncDomToCustomSteps(); + } const stepsToRun = (preset ? preset.steps : customSteps).filter(s => s.enabled); if (!stepsToRun || stepsToRun.length === 0) { ui.showToast('В уроке нет активных блоков. Включите хотя бы один блок.', 'warning'); @@ -622,7 +676,7 @@ async function runVocabStep(step, container) { // Fetch queue const res = await api.srs.getQueue({ limit: limit }); - let cards = res.cards || []; + let cards = res.cards || (Array.isArray(res) ? res : []); if (cards.length === 0) { // Fallback: fetch random cards from library @@ -630,6 +684,16 @@ async function runVocabStep(step, container) { cards = Array.isArray(allRes) ? allRes : (allRes.cards || []); } + // Distractor candidate pool for quiz mode + let distractorPool = []; + if (mode === 'quiz') { + try { + const extraRes = await api.cards.getAll({ limit: 80 }); + const extraCards = Array.isArray(extraRes) ? extraRes : (extraRes.cards || []); + distractorPool = extraCards.map(c => c.translation).filter(Boolean); + } catch (_) {} + } + if (cards.length === 0) { container.innerHTML = `
@@ -645,17 +709,20 @@ async function runVocabStep(step, container) { let cardIdx = 0; let correctCount = 0; const totalCards = cards.length; + let stepActive = true; function renderCurrentCard() { if (cardIdx >= totalCards) { - // Finished step + stepActive = false; + session.currentStepKeyHandler = null; const score = Math.round((correctCount / totalCards) * 100); session.stepResults.push({ type: 'vocab', title: step.title, score: score, correct: correctCount, - total: totalCards + total: totalCards, + mode: mode }); container.innerHTML = ` @@ -663,46 +730,228 @@ async function runVocabStep(step, container) {
🎉

Блок слов завершён!

Результат: ${correctCount} из ${totalCards} (${score}%)

-
`; + document.getElementById('vocab-finish-btn')?.addEventListener('click', () => { + advanceStep(); + }); return; } const card = cards[cardIdx]; + + if (mode === 'quiz') { + renderQuizCard(card); + } else if (mode === 'typing') { + renderTypingCard(card); + } else if (mode === 'dictation') { + renderDictationCard(card); + } else { + renderFlashcardCard(card); + } + } + + // ----------------------------------------------------------------------- + // Mode A: 4 Options Quiz + // ----------------------------------------------------------------------- + function renderQuizCard(card) { + const correctText = card.translation; + const candidates = [ + ...cards.filter(c => c.id !== card.id && c.translation).map(c => c.translation), + ...distractorPool.filter(t => t && t !== correctText) + ]; + const uniqueCandidates = [...new Set(candidates.filter(t => t !== correctText))]; + shuffleArray(uniqueCandidates); + const options = [correctText, ...uniqueCandidates.slice(0, 3)]; + const fallbackOptions = ['определять', 'проверять', 'исследовать', 'сохранять', 'показывать', 'понимать', 'находить', 'создавать']; + while (options.length < 4) { + const f = fallbackOptions.find(o => !options.includes(o)) || `Вариант ${options.length + 1}`; + options.push(f); + } + shuffleArray(options); + + let answered = false; + container.innerHTML = ` -
+
- Слово ${cardIdx + 1} из ${totalCards} - +
+ Слово ${cardIdx + 1} из ${totalCards} + 🎯 4 Варианта +
+
-
+

${escapeHtml(card.term)}

${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''}
${card.context_sentence ? ` -
+
+ "${escapeHtml(card.context_sentence)}" +
+ ` : ''} + +
+ ${options.map((opt, oIdx) => ` + + `).join('')} +
+ + + + +
+ `; + + const playCardAudio = () => { + if (card.audio_url) playAudio(card.audio_url); + else playTextSpeech(card.term, 'en'); + }; + document.getElementById('srs-tts-btn')?.addEventListener('click', playCardAudio); + + const optBtns = container.querySelectorAll('.lesson-quiz-opt-btn'); + const handleOptionClick = (btn) => { + if (answered) return; + answered = true; + + const selectedText = btn.dataset.val; + const isCorrect = selectedText === correctText; + + optBtns.forEach(b => { + b.disabled = true; + if (b.dataset.val === correctText) { + b.classList.add('opt-correct'); + } else if (b === btn && !isCorrect) { + b.classList.add('opt-wrong'); + } + }); + + playCardAudio(); + + const fb = document.getElementById('lesson-quiz-feedback'); + const nextBar = document.getElementById('lesson-quiz-next-bar'); + const nextBtn = document.getElementById('lesson-quiz-next-btn'); + + if (isCorrect) { + correctCount++; + if (fb) { + fb.className = 'trainer-feedback-box feedback-correct'; + fb.innerHTML = '🎉 Отлично! Правильный перевод.'; + fb.classList.remove('hidden'); + } + api.srs.submitReview(card.id, 4).catch(() => {}); + } else { + session.errors.push({ + type: 'vocab', + term: card.term, + translation: card.translation, + context: card.context_sentence || '', + user_answer: selectedText, + mode: 'quiz' + }); + if (fb) { + fb.className = 'trainer-feedback-box feedback-wrong'; + fb.innerHTML = `❌ Ошибка! Правильно: ${escapeHtml(correctText)}`; + fb.classList.remove('hidden'); + } + api.srs.submitReview(card.id, 1).catch(() => {}); + } + + if (nextBar) nextBar.classList.remove('hidden'); + if (nextBtn) nextBtn.focus(); + }; + + optBtns.forEach(btn => { + btn.addEventListener('click', () => handleOptionClick(btn)); + }); + + const nextAction = () => { + cardIdx++; + renderCurrentCard(); + }; + + document.getElementById('lesson-quiz-next-btn')?.addEventListener('click', nextAction); + + session.currentStepKeyHandler = (e) => { + if (!stepActive) return; + if (!answered) { + if (['1', '2', '3', '4'].includes(e.key)) { + e.preventDefault(); + const n = parseInt(e.key, 10); + if (optBtns[n - 1]) handleOptionClick(optBtns[n - 1]); + } else if (e.code === 'KeyS' || e.code === 'KeyA') { + e.preventDefault(); + playCardAudio(); + } + } else { + if (e.key === 'Enter' || e.code === 'Space') { + e.preventDefault(); + nextAction(); + } + } + }; + } + + // ----------------------------------------------------------------------- + // Mode B: Traditional Flashcards + // ----------------------------------------------------------------------- + function renderFlashcardCard(card) { + let isFlipped = false; + + container.innerHTML = ` +
+
+
+ Слово ${cardIdx + 1} из ${totalCards} + 📇 Карточки +
+ +
+ +
+

${escapeHtml(card.term)}

+ ${card.phonetic ? `${escapeHtml(card.phonetic)}` : ''} +
+ + ${card.context_sentence ? ` +
"${escapeHtml(card.context_sentence)}"
` : ''}
- +