feat: add lesson difficulty indicators and improve mobile sidebar
- Add computeLessonDifficulty function to determine lesson difficulty based on selector complexity (easy/medium/hard) - Display difficulty badge with bar indicator in lesson title row - Add mobile navigation links (CSS, HTML, Tailwind) to sidebar - Add mobile auth trigger button in sidebar - Redesign settings section with card layout and native toggles - Add difficulty translations for all 6 languages - Fix module pill overflow on narrow screens
This commit is contained in:
19
src/app.js
19
src/app.js
@@ -1,6 +1,6 @@
|
||||
import { LessonEngine } from "./impl/LessonEngine.js";
|
||||
import { CodeEditor, crispyEditorTheme } from "./impl/CodeEditor.js";
|
||||
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
||||
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar, renderDifficultyBadge } from "./helpers/renderer.js";
|
||||
import { loadModules } from "./config/lessons.js";
|
||||
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
|
||||
import { parseHash, updateHash, replaceHash, getShareableUrl, RouteType, navigateTo } from "./helpers/router.js";
|
||||
@@ -648,6 +648,9 @@ function loadCurrentLesson() {
|
||||
lesson
|
||||
);
|
||||
|
||||
// Render difficulty badge
|
||||
renderDifficultyBadge(elements.lessonTitleRow, lesson);
|
||||
|
||||
// Set user code in CodeMirror (clear history to prevent undo/redo across lessons)
|
||||
if (codeEditor) {
|
||||
codeEditor.setValueAndClearHistory(engineState.userCode);
|
||||
@@ -657,12 +660,13 @@ function loadCurrentLesson() {
|
||||
if (engineState.isCompleted) {
|
||||
elements.runBtn.querySelector("span").textContent = t("rerun");
|
||||
|
||||
// Add completion badge if not present
|
||||
if (!document.querySelector(".completion-badge")) {
|
||||
// Add completion badge to difficulty-wrapper if not present
|
||||
const wrapper = document.querySelector(".difficulty-wrapper");
|
||||
if (wrapper && !wrapper.querySelector(".completion-badge")) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "completion-badge";
|
||||
badge.textContent = t("completed");
|
||||
elements.lessonTitleRow.appendChild(badge);
|
||||
wrapper.appendChild(badge);
|
||||
}
|
||||
|
||||
// Show gradient border and glow for completed lessons
|
||||
@@ -671,7 +675,7 @@ function loadCurrentLesson() {
|
||||
} else {
|
||||
elements.runBtn.querySelector("span").textContent = t("run");
|
||||
|
||||
// Remove completion badge and border if exists
|
||||
// Remove completion badge if exists
|
||||
const badge = document.querySelector(".completion-badge");
|
||||
if (badge) badge.remove();
|
||||
elements.previewWrapper?.classList.remove("completed-glow");
|
||||
@@ -2480,6 +2484,11 @@ function init() {
|
||||
elements.closeSidebar.addEventListener("click", closeSidebar);
|
||||
elements.sidebarBackdrop.addEventListener("click", closeSidebar);
|
||||
|
||||
// Sidebar nav links (mobile) - close sidebar on click
|
||||
document.querySelectorAll(".sidebar-nav-link").forEach((link) => {
|
||||
link.addEventListener("click", closeSidebar);
|
||||
});
|
||||
|
||||
// Logo click - navigate to home landing
|
||||
elements.logoLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
10
src/auth.js
10
src/auth.js
@@ -153,6 +153,7 @@ function updateAuthUI(user) {
|
||||
|
||||
// Sidebar elements
|
||||
const authTriggerSidebar = document.getElementById("auth-trigger-sidebar");
|
||||
const authTriggerMobile = document.getElementById("auth-trigger-mobile");
|
||||
const userMenuSidebar = document.getElementById("user-menu-sidebar");
|
||||
const userEmailSidebar = document.getElementById("user-email-sidebar");
|
||||
const sidebarHint = document.querySelector(".sidebar-auth-hint");
|
||||
@@ -161,6 +162,7 @@ function updateAuthUI(user) {
|
||||
authTriggerHeader?.classList.add("hidden");
|
||||
userEmailHeader?.classList.remove("hidden");
|
||||
authTriggerSidebar?.classList.add("hidden");
|
||||
authTriggerMobile?.classList.add("hidden");
|
||||
userMenuSidebar?.classList.remove("hidden");
|
||||
sidebarHint?.classList.add("hidden");
|
||||
if (userEmailHeader) userEmailHeader.textContent = user.email;
|
||||
@@ -169,6 +171,7 @@ function updateAuthUI(user) {
|
||||
authTriggerHeader?.classList.remove("hidden");
|
||||
userEmailHeader?.classList.add("hidden");
|
||||
authTriggerSidebar?.classList.remove("hidden");
|
||||
authTriggerMobile?.classList.remove("hidden");
|
||||
userMenuSidebar?.classList.add("hidden");
|
||||
sidebarHint?.classList.remove("hidden");
|
||||
}
|
||||
@@ -257,7 +260,7 @@ function setupAuthForms() {
|
||||
.getElementById("show-reset")
|
||||
?.addEventListener("click", () => switchForm("reset"));
|
||||
|
||||
// Dialog triggers (both header and sidebar)
|
||||
// Dialog triggers (header, sidebar, and mobile)
|
||||
document
|
||||
.getElementById("auth-trigger-header")
|
||||
?.addEventListener("click", () => {
|
||||
@@ -268,6 +271,11 @@ function setupAuthForms() {
|
||||
?.addEventListener("click", () => {
|
||||
authDialog?.showModal();
|
||||
});
|
||||
document
|
||||
.getElementById("auth-trigger-mobile")
|
||||
?.addEventListener("click", () => {
|
||||
authDialog?.showModal();
|
||||
});
|
||||
|
||||
// Logout button (sidebar only)
|
||||
document
|
||||
|
||||
@@ -3,6 +3,49 @@
|
||||
*/
|
||||
import { t } from "../i18n.js";
|
||||
|
||||
/**
|
||||
* Compute lesson difficulty based on lesson structure
|
||||
* - Easy: selector is provided in codePrefix (student only writes properties)
|
||||
* - Medium: student writes a simple selector (single element/class)
|
||||
* - Hard: student writes compound selectors (descendant, chained classes, type+class)
|
||||
* @param {Object} lesson - The lesson object
|
||||
* @returns {"easy"|"medium"|"hard"} The computed difficulty
|
||||
*/
|
||||
export function computeLessonDifficulty(lesson) {
|
||||
const codePrefix = lesson.codePrefix || "";
|
||||
const solution = lesson.solution || "";
|
||||
|
||||
// If codePrefix contains an opening brace, selector is provided → Easy
|
||||
if (codePrefix.includes("{")) {
|
||||
return "easy";
|
||||
}
|
||||
|
||||
// No codePrefix with selector - check the solution complexity
|
||||
// Hard: descendant selectors (space before {), chained classes (.a.b), type+class (a.class)
|
||||
const selectorMatch = solution.match(/^([^{]+)\{/);
|
||||
if (selectorMatch) {
|
||||
const selector = selectorMatch[1].trim();
|
||||
|
||||
// Descendant selector: has space (e.g., ".nav a", ".card p")
|
||||
if (/\S\s+\S/.test(selector)) {
|
||||
return "hard";
|
||||
}
|
||||
|
||||
// Chained classes: multiple dots without space (e.g., ".btn.primary")
|
||||
if ((selector.match(/\./g) || []).length > 1) {
|
||||
return "hard";
|
||||
}
|
||||
|
||||
// Type + class: element followed by dot (e.g., "a.btn", "div.card")
|
||||
if (/^[a-z]+\.[a-z]/i.test(selector)) {
|
||||
return "hard";
|
||||
}
|
||||
}
|
||||
|
||||
// Simple selector → Medium
|
||||
return "medium";
|
||||
}
|
||||
|
||||
// Feedback elements cache
|
||||
let feedbackElement = null;
|
||||
let feedbackTimeout = null;
|
||||
@@ -138,6 +181,42 @@ export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl
|
||||
// The LessonEngine will handle this when it's first set
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the difficulty badge (right-aligned in title row)
|
||||
* @param {HTMLElement} container - The container element (lesson-title-row)
|
||||
* @param {Object} lesson - The lesson object
|
||||
*/
|
||||
export function renderDifficultyBadge(container, lesson) {
|
||||
// Remove existing difficulty wrapper if any
|
||||
const existingWrapper = container.querySelector(".difficulty-wrapper");
|
||||
if (existingWrapper) {
|
||||
existingWrapper.remove();
|
||||
}
|
||||
|
||||
// Compute difficulty
|
||||
const difficulty = computeLessonDifficulty(lesson);
|
||||
|
||||
// Create wrapper for right-alignment
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.className = "difficulty-wrapper";
|
||||
|
||||
// Create badge element with three bars
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `difficulty-badge difficulty-${difficulty}`;
|
||||
badge.setAttribute("aria-label", t(`difficulty_${difficulty}_label`));
|
||||
badge.setAttribute("title", t(`difficulty_${difficulty}`));
|
||||
|
||||
// Add three bars
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const bar = document.createElement("span");
|
||||
bar.className = "bar";
|
||||
badge.appendChild(bar);
|
||||
}
|
||||
|
||||
wrapper.appendChild(badge);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the level indicator
|
||||
* @param {HTMLElement} element - The level indicator element
|
||||
|
||||
40
src/i18n.js
40
src/i18n.js
@@ -112,6 +112,12 @@ const translations = {
|
||||
// Dynamic content
|
||||
loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.",
|
||||
completed: "Completed",
|
||||
difficulty_easy: "Easy",
|
||||
difficulty_medium: "Medium",
|
||||
difficulty_hard: "Hard",
|
||||
difficulty_easy_label: "Easy difficulty - selector provided",
|
||||
difficulty_medium_label: "Medium difficulty - simple selector required",
|
||||
difficulty_hard_label: "Hard difficulty - compound selector required",
|
||||
successMessage: "CRISPY! ٩(◕‿◕)۶ Your code works correctly.",
|
||||
keepTrying: "Keep trying!",
|
||||
failedToLoad: "Failed to load modules. Please refresh the page.",
|
||||
@@ -336,7 +342,13 @@ const translations = {
|
||||
|
||||
// Dynamic content
|
||||
loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.",
|
||||
completed: "Erledigt",
|
||||
completed: "Fertig",
|
||||
difficulty_easy: "Einfach",
|
||||
difficulty_medium: "Mittel",
|
||||
difficulty_hard: "Schwer",
|
||||
difficulty_easy_label: "Einfach - Selektor vorgegeben",
|
||||
difficulty_medium_label: "Mittel - einfacher Selektor erforderlich",
|
||||
difficulty_hard_label: "Schwer - zusammengesetzter Selektor erforderlich",
|
||||
successMessage: "CRISPY! ٩(◕‿◕)۶ Dein Code funktioniert.",
|
||||
keepTrying: "Weiter versuchen!",
|
||||
failedToLoad: "Module konnten nicht geladen werden. Bitte Seite neu laden.",
|
||||
@@ -345,7 +357,7 @@ const translations = {
|
||||
untitledLesson: "Unbenannte Lektion",
|
||||
|
||||
// Landing page
|
||||
landingHeroTitle: "Lerne Web Entwicklung",
|
||||
landingHeroTitle: "Web Entwicklung lernen",
|
||||
landingHeroHighlight: "mit CODE CRISPIES",
|
||||
landingHeroSubtitle: "Meistere HTML, CSS und Tailwind durch praktische Übungen mit sofortigem Feedback. Kostenlos und Open Source.",
|
||||
landingCtaStart: "Jetzt starten",
|
||||
@@ -560,6 +572,12 @@ const translations = {
|
||||
// Dynamic content
|
||||
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
|
||||
completed: "Ukończono",
|
||||
difficulty_easy: "Łatwe",
|
||||
difficulty_medium: "Średnie",
|
||||
difficulty_hard: "Trudne",
|
||||
difficulty_easy_label: "Łatwe - selektor podany",
|
||||
difficulty_medium_label: "Średnie - wymagany prosty selektor",
|
||||
difficulty_hard_label: "Trudne - wymagany złożony selektor",
|
||||
successMessage: "CRISPY! ٩(◕‿◕)۶ Twój kod działa poprawnie.",
|
||||
keepTrying: "Próbuj dalej!",
|
||||
failedToLoad: "Nie udało się załadować modułów. Odśwież stronę.",
|
||||
@@ -784,6 +802,12 @@ const translations = {
|
||||
// Dynamic content
|
||||
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
|
||||
completed: "Completado",
|
||||
difficulty_easy: "Fácil",
|
||||
difficulty_medium: "Medio",
|
||||
difficulty_hard: "Difícil",
|
||||
difficulty_easy_label: "Fácil - selector proporcionado",
|
||||
difficulty_medium_label: "Medio - selector simple requerido",
|
||||
difficulty_hard_label: "Difícil - selector compuesto requerido",
|
||||
successMessage: "¡CRISPY! ٩(◕‿◕)۶ Tu código funciona correctamente.",
|
||||
keepTrying: "¡Sigue intentando!",
|
||||
failedToLoad: "No se pudieron cargar los módulos. Actualiza la página.",
|
||||
@@ -1007,6 +1031,12 @@ const translations = {
|
||||
// Dynamic content
|
||||
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
|
||||
completed: "مكتمل",
|
||||
difficulty_easy: "سهل",
|
||||
difficulty_medium: "متوسط",
|
||||
difficulty_hard: "صعب",
|
||||
difficulty_easy_label: "سهل - المحدد مُعطى",
|
||||
difficulty_medium_label: "متوسط - يتطلب محدد بسيط",
|
||||
difficulty_hard_label: "صعب - يتطلب محدد مركب",
|
||||
successMessage: "CRISPY! ٩(◕‿◕)۶ الكود يعمل بشكل صحيح.",
|
||||
keepTrying: "استمر في المحاولة!",
|
||||
failedToLoad: "فشل تحميل الوحدات. قم بتحديث الصفحة.",
|
||||
@@ -1228,6 +1258,12 @@ const translations = {
|
||||
// Dynamic content
|
||||
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
|
||||
completed: "Завершено",
|
||||
difficulty_easy: "Легко",
|
||||
difficulty_medium: "Середнє",
|
||||
difficulty_hard: "Складно",
|
||||
difficulty_easy_label: "Легко - селектор наданий",
|
||||
difficulty_medium_label: "Середнє - потрібен простий селектор",
|
||||
difficulty_hard_label: "Складно - потрібен складений селектор",
|
||||
successMessage: "CRISPY! ٩(◕‿◕)۶ Ваш код працює правильно.",
|
||||
keepTrying: "Продовжуйте спроби!",
|
||||
failedToLoad: "Не вдалося завантажити модулі. Оновіть сторінку.",
|
||||
|
||||
@@ -463,6 +463,13 @@
|
||||
<button id="close-sidebar" class="close-btn" data-i18n-aria-label="closeMenu" aria-label="Close menu">×</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-section sidebar-nav-mobile" aria-label="Learning paths">
|
||||
<a href="#css" class="sidebar-nav-link" data-section="css">CSS</a>
|
||||
<a href="#html" class="sidebar-nav-link" data-section="html">HTML</a>
|
||||
<a href="#tailwind" class="sidebar-nav-link" data-section="tailwind">Tailwind</a>
|
||||
<button id="auth-trigger-mobile" class="sidebar-nav-link sidebar-auth-link" data-i18n="authLogin">Log In</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h4 data-i18n="progress">Progress</h4>
|
||||
<div class="progress-display milestone-progress" id="progress-display">
|
||||
@@ -504,23 +511,27 @@
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h4 data-i18n="settings">Settings</h4>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label" data-i18n="language">Language</span>
|
||||
<select id="lang-select" class="lang-select">
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="pl">Polski</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="uk">Українська</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="disable-feedback-toggle" checked />
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-label" data-i18n="showHints">Show Hints</span>
|
||||
</label>
|
||||
<button id="reset-btn" class="btn btn-text" data-i18n="resetAllProgress">Reset All Progress</button>
|
||||
<div class="settings-card">
|
||||
<label class="settings-row">
|
||||
<span class="settings-label" data-i18n="language">Language</span>
|
||||
<select id="lang-select" class="lang-select">
|
||||
<option value="en">English</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="pl">Polski</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="uk">Українська</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-row">
|
||||
<span class="settings-label" data-i18n="showHints">Show Hints</span>
|
||||
<input type="checkbox" id="disable-feedback-toggle" class="settings-toggle" checked />
|
||||
</label>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label" data-i18n="resetAllProgress">Reset All Progress</span>
|
||||
<button id="reset-btn" class="btn btn-sm btn-ghost" data-i18n="reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
|
||||
168
src/main.css
168
src/main.css
@@ -308,6 +308,16 @@ kbd {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
#auth-trigger-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
#auth-trigger-header {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= GAME LAYOUT ================= */
|
||||
.game-layout {
|
||||
display: flex;
|
||||
@@ -374,6 +384,7 @@ kbd {
|
||||
gap: 0.5rem;
|
||||
background: var(--primary-bg-medium);
|
||||
color: var(--primary-color);
|
||||
min-width: 0;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
@@ -385,12 +396,18 @@ kbd {
|
||||
.module-name {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.module-pill .level-indicator {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lesson-title-row {
|
||||
@@ -398,7 +415,13 @@ kbd {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lesson-title-row .difficulty-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#lesson-title {
|
||||
@@ -447,6 +470,28 @@ kbd {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.difficulty-badge .bar {
|
||||
width: 3px;
|
||||
border-radius: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.difficulty-badge .bar:nth-child(1) { height: 6px; }
|
||||
.difficulty-badge .bar:nth-child(2) { height: 9px; }
|
||||
.difficulty-badge .bar:nth-child(3) { height: 12px; }
|
||||
|
||||
.difficulty-easy .bar:nth-child(1),
|
||||
.difficulty-medium .bar:nth-child(1),
|
||||
.difficulty-medium .bar:nth-child(2),
|
||||
.difficulty-hard .bar { background: var(--light-text); }
|
||||
|
||||
.lesson-description {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
@@ -996,14 +1041,69 @@ kbd {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Mobile navigation in sidebar */
|
||||
.sidebar-nav-mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav-link {
|
||||
display: block;
|
||||
padding: 0.6rem var(--spacing-md);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-nav-link:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sidebar-nav-link:hover {
|
||||
background: var(--primary-bg-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sidebar-auth-link {
|
||||
width: calc(100% - 2 * var(--spacing-md));
|
||||
margin: var(--spacing-sm) var(--spacing-md);
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
background: var(--primary-color);
|
||||
color: var(--white-text);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-auth-link:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.sidebar-nav-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Make the lessons nav section fill available space */
|
||||
nav.sidebar-section {
|
||||
nav.sidebar-section:not(.sidebar-nav-mobile) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.sidebar-nav-mobile {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-section h4 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
@@ -1388,8 +1488,63 @@ button.lesson-list-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ================= TOGGLE SWITCH ================= */
|
||||
/* Setting row (for label + control) */
|
||||
/* ================= SETTINGS CARD ================= */
|
||||
.settings-card {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.settings-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.settings-toggle {
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
appearance: none;
|
||||
background: var(--border-color);
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.settings-toggle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.settings-toggle:checked {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.settings-toggle:checked::before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
/* Legacy setting row (for label + control) */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3177,11 +3332,16 @@ input:checked + .toggle-slider::before {
|
||||
|
||||
.module-pill {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin: 0 var(--spacing-sm);
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||
import { renderModuleList, renderLesson, renderLevelIndicator, showFeedback, clearFeedback } from "../../src/helpers/renderer.js";
|
||||
import { renderModuleList, renderLesson, renderLevelIndicator, showFeedback, clearFeedback, computeLessonDifficulty } from "../../src/helpers/renderer.js";
|
||||
|
||||
describe("Renderer Module", () => {
|
||||
beforeEach(() => {
|
||||
@@ -176,4 +176,68 @@ describe("Renderer Module", () => {
|
||||
clearFeedback();
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLessonDifficulty", () => {
|
||||
test("should return 'easy' when codePrefix contains selector", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: ".text {\n ",
|
||||
solution: "color: coral;"
|
||||
})).toBe("easy");
|
||||
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "h1, h2, h3 {\n ",
|
||||
solution: "color: steelblue;"
|
||||
})).toBe("easy");
|
||||
});
|
||||
|
||||
test("should return 'medium' for simple type selector", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: "p {\n color: steelblue;\n}"
|
||||
})).toBe("medium");
|
||||
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: "a {\n color: coral;\n}"
|
||||
})).toBe("medium");
|
||||
});
|
||||
|
||||
test("should return 'medium' for simple class selector", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: ".badge {\n background: tomato;\n}"
|
||||
})).toBe("medium");
|
||||
});
|
||||
|
||||
test("should return 'hard' for descendant selectors", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: ".nav a {\n color: white;\n}"
|
||||
})).toBe("hard");
|
||||
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: ".card p {\n font-size: 0.9rem;\n}"
|
||||
})).toBe("hard");
|
||||
});
|
||||
|
||||
test("should return 'hard' for chained class selectors", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: ".btn.primary {\n background: steelblue;\n}"
|
||||
})).toBe("hard");
|
||||
});
|
||||
|
||||
test("should return 'hard' for type+class selectors", () => {
|
||||
expect(computeLessonDifficulty({
|
||||
codePrefix: "",
|
||||
solution: "a.btn {\n text-decoration: none;\n}"
|
||||
})).toBe("hard");
|
||||
});
|
||||
|
||||
test("should handle missing fields gracefully", () => {
|
||||
expect(computeLessonDifficulty({})).toBe("medium");
|
||||
expect(computeLessonDifficulty({ codePrefix: null })).toBe("medium");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user