feat: add shareable lesson links with URL routing

- Add share button with SVG link icon in lesson title row
- Create share dialog with copy URL functionality
- Implement URL hash-based routing for lesson navigation
- Support browser back/forward navigation
- Add i18n translations for share dialog in all languages
- Position share button between title and completion badge
- Add RTL support for title row layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-01-14 21:35:49 +01:00
parent a4563638a0
commit 0f14568d2c
5 changed files with 364 additions and 53 deletions

View File

@@ -3,6 +3,7 @@ import { CodeEditor } from "./impl/CodeEditor.js";
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js"; import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { loadModules } from "./config/lessons.js"; import { loadModules } from "./config/lessons.js";
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js"; import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
import { parseHash, updateHash, replaceHash, getShareableUrl } from "./helpers/router.js";
// Simplified state - LessonEngine now manages lesson state and progress // Simplified state - LessonEngine now manages lesson state and progress
const state = { const state = {
@@ -28,6 +29,7 @@ const elements = {
modulePill: document.getElementById("module-pill"), modulePill: document.getElementById("module-pill"),
moduleName: document.querySelector(".module-name"), moduleName: document.querySelector(".module-name"),
lessonTitle: document.getElementById("lesson-title"), lessonTitle: document.getElementById("lesson-title"),
lessonTitleRow: document.querySelector(".lesson-title-row"),
lessonDescription: document.getElementById("lesson-description"), lessonDescription: document.getElementById("lesson-description"),
taskInstruction: document.getElementById("task-instruction"), taskInstruction: document.getElementById("task-instruction"),
codeInput: document.getElementById("code-input"), codeInput: document.getElementById("code-input"),
@@ -71,7 +73,15 @@ const elements = {
resetCodeDialogClose: document.getElementById("reset-code-dialog-close"), resetCodeDialogClose: document.getElementById("reset-code-dialog-close"),
cancelResetCode: document.getElementById("cancel-reset-code"), cancelResetCode: document.getElementById("cancel-reset-code"),
confirmResetCode: document.getElementById("confirm-reset-code"), confirmResetCode: document.getElementById("confirm-reset-code"),
resetCodeDontShow: document.getElementById("reset-code-dont-show") resetCodeDontShow: document.getElementById("reset-code-dont-show"),
// Share dialog
shareBtn: document.getElementById("share-btn"),
shareDialog: document.getElementById("share-dialog"),
shareDialogClose: document.getElementById("share-dialog-close"),
shareUrlInput: document.getElementById("share-url-input"),
copyUrlBtn: document.getElementById("copy-url-btn"),
copyFeedback: document.getElementById("copy-feedback")
}; };
// Initialize the lesson engine - now the single source of truth // Initialize the lesson engine - now the single source of truth
@@ -283,14 +293,22 @@ function initializeModules() {
// Use the new renderModuleList function with both callbacks // Use the new renderModuleList function with both callbacks
renderModuleList(elements.moduleList, modules, selectModule, selectLesson); renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
// Load saved progress and select appropriate module // Check URL first for shareable links
const progressData = lessonEngine.loadUserProgress(); const urlState = parseHash();
const lastModuleId = progressData?.lastModuleId;
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) { if (urlState) {
selectModule(lastModuleId); // URL takes priority - navigate to specified lesson
} else if (modules.length > 0) { navigateToLesson(urlState.moduleId, urlState.lessonIndex, false);
selectModule(modules[0].id); } else {
// No URL - use saved progress (existing logic)
const progressData = lessonEngine.loadUserProgress();
const lastModuleId = progressData?.lastModuleId;
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
selectModule(lastModuleId);
} else if (modules.length > 0) {
selectModule(modules[0].id);
}
} }
updateProgressDisplay(); updateProgressDisplay();
@@ -307,6 +325,10 @@ function selectModule(moduleId) {
const success = lessonEngine.setModuleById(moduleId); const success = lessonEngine.setModuleById(moduleId);
if (!success) return; if (!success) return;
// Update URL
const engineState = lessonEngine.getCurrentState();
updateHash(moduleId, engineState.lessonIndex);
// Update module list UI to highlight the active module // Update module list UI to highlight the active module
const moduleItems = elements.moduleList.querySelectorAll(".module-header"); const moduleItems = elements.moduleList.querySelectorAll(".module-header");
moduleItems.forEach((item) => { moduleItems.forEach((item) => {
@@ -332,6 +354,10 @@ function selectLesson(moduleId, lessonIndex) {
} }
lessonEngine.setLessonByIndex(lessonIndex); lessonEngine.setLessonByIndex(lessonIndex);
// Update URL
updateHash(moduleId, lessonIndex);
loadCurrentLesson(); loadCurrentLesson();
// Close sidebar after selection on mobile // Close sidebar after selection on mobile
@@ -463,7 +489,7 @@ function loadCurrentLesson() {
const badge = document.createElement("span"); const badge = document.createElement("span");
badge.className = "completion-badge"; badge.className = "completion-badge";
badge.textContent = t("completed"); badge.textContent = t("completed");
elements.lessonTitle.appendChild(badge); elements.lessonTitleRow.appendChild(badge);
} }
// Show gradient border for completed lessons // Show gradient border for completed lessons
@@ -556,9 +582,12 @@ function nextLesson() {
const prevModuleId = lessonEngine.getCurrentState().module?.id; const prevModuleId = lessonEngine.getCurrentState().module?.id;
const success = lessonEngine.nextLesson(); const success = lessonEngine.nextLesson();
if (success) { if (success) {
const newModuleId = lessonEngine.getCurrentState().module?.id; const newState = lessonEngine.getCurrentState();
if (newModuleId !== prevModuleId) { // Update URL
updateModuleHighlight(newModuleId); updateHash(newState.module.id, newState.lessonIndex);
if (newState.module.id !== prevModuleId) {
updateModuleHighlight(newState.module.id);
} }
loadCurrentLesson(); loadCurrentLesson();
} }
@@ -568,9 +597,12 @@ function prevLesson() {
const prevModuleId = lessonEngine.getCurrentState().module?.id; const prevModuleId = lessonEngine.getCurrentState().module?.id;
const success = lessonEngine.previousLesson(); const success = lessonEngine.previousLesson();
if (success) { if (success) {
const newModuleId = lessonEngine.getCurrentState().module?.id; const newState = lessonEngine.getCurrentState();
if (newModuleId !== prevModuleId) { // Update URL
updateModuleHighlight(newModuleId); updateHash(newState.module.id, newState.lessonIndex);
if (newState.module.id !== prevModuleId) {
updateModuleHighlight(newState.module.id);
} }
loadCurrentLesson(); loadCurrentLesson();
} }
@@ -636,7 +668,7 @@ function runCode() {
const badge = document.createElement("span"); const badge = document.createElement("span");
badge.className = "completion-badge"; badge.className = "completion-badge";
badge.textContent = t("completed"); badge.textContent = t("completed");
elements.lessonTitle.appendChild(badge); elements.lessonTitleRow.appendChild(badge);
} }
// Add success visual indicators // Add success visual indicators
@@ -748,6 +780,93 @@ function handleResetCodeClick() {
} }
} }
// ================= SHARE DIALOG =================
function showShareDialog() {
const engineState = lessonEngine.getCurrentState();
if (engineState.module && engineState.lesson !== null) {
const shareUrl = getShareableUrl(engineState.module.id, engineState.lessonIndex);
elements.shareUrlInput.value = shareUrl;
elements.copyFeedback.hidden = true;
}
elements.shareDialog.showModal();
}
function closeShareDialog() {
elements.shareDialog.close();
}
async function copyShareUrl() {
try {
await navigator.clipboard.writeText(elements.shareUrlInput.value);
elements.copyFeedback.hidden = false;
setTimeout(() => {
elements.copyFeedback.hidden = true;
}, 2000);
} catch (err) {
// Fallback for older browsers
elements.shareUrlInput.select();
document.execCommand("copy");
elements.copyFeedback.hidden = false;
setTimeout(() => {
elements.copyFeedback.hidden = true;
}, 2000);
}
}
// ================= URL ROUTING =================
function initRouter() {
// Handle browser back/forward
window.addEventListener("popstate", handlePopState);
}
function handlePopState() {
const parsed = parseHash();
if (parsed) {
navigateToLesson(parsed.moduleId, parsed.lessonIndex, false);
}
}
function navigateToLesson(moduleId, lessonIndex, shouldUpdateUrl = true) {
// Validate moduleId exists
const module = lessonEngine.modules.find((m) => m.id === moduleId);
if (!module) {
// Invalid module - fallback to first module
const fallbackModule = lessonEngine.modules[0];
if (fallbackModule) {
replaceHash(fallbackModule.id, 0);
lessonEngine.setModuleById(fallbackModule.id);
lessonEngine.setLessonByIndex(0);
loadCurrentLesson();
updateModuleHighlight(fallbackModule.id);
}
return;
}
// Validate lessonIndex is in bounds
if (lessonIndex < 0 || lessonIndex >= module.lessons.length) {
// Invalid lesson - go to first lesson of module
replaceHash(moduleId, 0);
lessonEngine.setModuleById(moduleId);
lessonEngine.setLessonByIndex(0);
loadCurrentLesson();
updateModuleHighlight(moduleId);
return;
}
// Valid navigation
lessonEngine.setModuleById(moduleId);
lessonEngine.setLessonByIndex(lessonIndex);
if (shouldUpdateUrl) {
updateHash(moduleId, lessonIndex);
}
loadCurrentLesson();
updateModuleHighlight(moduleId);
}
// ================= INITIALIZATION ================= // ================= INITIALIZATION =================
function initCodeEditor() { function initCodeEditor() {
@@ -788,6 +907,9 @@ function init() {
// Load modules after editor is ready // Load modules after editor is ready
initializeModules(); initializeModules();
// Initialize URL router for shareable links
initRouter();
// Sidebar controls // Sidebar controls
elements.menuBtn.addEventListener("click", openSidebar); elements.menuBtn.addEventListener("click", openSidebar);
elements.closeSidebar.addEventListener("click", closeSidebar); elements.closeSidebar.addEventListener("click", closeSidebar);
@@ -797,7 +919,9 @@ function init() {
elements.logoLink.addEventListener("click", (e) => { elements.logoLink.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
lessonEngine.setModuleById("welcome"); lessonEngine.setModuleById("welcome");
updateHash("welcome", 0);
loadCurrentLesson(); loadCurrentLesson();
updateModuleHighlight("welcome");
}); });
// Language select // Language select
@@ -820,6 +944,7 @@ function init() {
if (codeEditor) codeEditor.redo(); if (codeEditor) codeEditor.redo();
}); });
elements.resetCodeBtn.addEventListener("click", handleResetCodeClick); elements.resetCodeBtn.addEventListener("click", handleResetCodeClick);
elements.shareBtn.addEventListener("click", showShareDialog);
// Dialogs // Dialogs
elements.helpBtn.addEventListener("click", showHelp); elements.helpBtn.addEventListener("click", showHelp);
@@ -841,6 +966,13 @@ function init() {
elements.cancelResetCode.addEventListener("click", closeResetCodeDialog); elements.cancelResetCode.addEventListener("click", closeResetCodeDialog);
elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm); elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm);
// Share dialog
elements.shareDialogClose.addEventListener("click", closeShareDialog);
elements.shareDialog.addEventListener("click", (e) => {
if (e.target === elements.shareDialog) closeShareDialog();
});
elements.copyUrlBtn.addEventListener("click", copyShareUrl);
// Settings // Settings
elements.disableFeedbackToggle.addEventListener("change", (e) => { elements.disableFeedbackToggle.addEventListener("change", (e) => {
state.userSettings.disableFeedbackErrors = !e.target.checked; state.userSettings.disableFeedbackErrors = !e.target.checked;

57
src/helpers/router.js Normal file
View File

@@ -0,0 +1,57 @@
/**
* URL Router for Code Crispies
* Handles hash-based routing for shareable lesson links
* Format: #module-id/lesson-index (e.g., #flexbox/2)
*/
/**
* Parse current URL hash into module and lesson info
* @returns {{ moduleId: string, lessonIndex: number } | null}
*/
export function parseHash() {
const hash = window.location.hash.slice(1); // Remove '#'
if (!hash) return null;
const parts = hash.split("/");
if (parts.length !== 2) return null;
const moduleId = parts[0];
const lessonIndex = parseInt(parts[1], 10);
if (!moduleId || isNaN(lessonIndex) || lessonIndex < 0) return null;
return { moduleId, lessonIndex };
}
/**
* Update URL hash with history entry (for navigation)
* @param {string} moduleId
* @param {number} lessonIndex
*/
export function updateHash(moduleId, lessonIndex) {
const newHash = `#${moduleId}/${lessonIndex}`;
if (window.location.hash !== newHash) {
history.pushState(null, "", newHash);
}
}
/**
* Replace URL hash without history entry (for invalid URL fallbacks)
* @param {string} moduleId
* @param {number} lessonIndex
*/
export function replaceHash(moduleId, lessonIndex) {
const newHash = `#${moduleId}/${lessonIndex}`;
history.replaceState(null, "", newHash);
}
/**
* Build full shareable URL for current lesson
* @param {string} moduleId
* @param {number} lessonIndex
* @returns {string}
*/
export function getShareableUrl(moduleId, lessonIndex) {
const base = window.location.origin + window.location.pathname;
return `${base}#${moduleId}/${lessonIndex}`;
}

View File

@@ -99,6 +99,13 @@ const translations = {
dontShowAgain: "Don't show this again", dontShowAgain: "Don't show this again",
reset: "Reset", reset: "Reset",
// Share dialog
shareDialogTitle: "Share Lesson",
shareDialogText: "Copy this URL to share the current lesson:",
shareTitle: "Share lesson",
copyUrl: "Copy",
urlCopied: "URL copied to clipboard!",
// Dynamic content // Dynamic content
loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.", loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.",
completed: "Completed", completed: "Completed",
@@ -207,6 +214,13 @@ const translations = {
dontShowAgain: "Nicht mehr anzeigen", dontShowAgain: "Nicht mehr anzeigen",
reset: "Zurücksetzen", reset: "Zurücksetzen",
// Share dialog
shareDialogTitle: "Lektion teilen",
shareDialogText: "Kopiere diese URL, um die aktuelle Lektion zu teilen:",
shareTitle: "Lektion teilen",
copyUrl: "Kopieren",
urlCopied: "URL in die Zwischenablage kopiert!",
// Dynamic content // Dynamic content
loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.", loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.",
completed: "Erledigt", completed: "Erledigt",
@@ -315,6 +329,13 @@ const translations = {
dontShowAgain: "Nie pokazuj ponownie", dontShowAgain: "Nie pokazuj ponownie",
reset: "Resetuj", reset: "Resetuj",
// Share dialog
shareDialogTitle: "Udostępnij lekcję",
shareDialogText: "Skopiuj ten URL, aby udostępnić bieżącą lekcję:",
shareTitle: "Udostępnij lekcję",
copyUrl: "Kopiuj",
urlCopied: "URL skopiowany do schowka!",
// Dynamic content // Dynamic content
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.", loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
completed: "Ukończono", completed: "Ukończono",
@@ -424,6 +445,13 @@ const translations = {
dontShowAgain: "No mostrar de nuevo", dontShowAgain: "No mostrar de nuevo",
reset: "Reiniciar", reset: "Reiniciar",
// Share dialog
shareDialogTitle: "Compartir lección",
shareDialogText: "Copia esta URL para compartir la lección actual:",
shareTitle: "Compartir lección",
copyUrl: "Copiar",
urlCopied: "¡URL copiada al portapapeles!",
// Dynamic content // Dynamic content
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.", loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
completed: "Completado", completed: "Completado",
@@ -531,6 +559,13 @@ const translations = {
dontShowAgain: "لا تظهر هذا مرة أخرى", dontShowAgain: "لا تظهر هذا مرة أخرى",
reset: "إعادة تعيين", reset: "إعادة تعيين",
// Share dialog
shareDialogTitle: "مشاركة الدرس",
shareDialogText: "انسخ هذا الرابط لمشاركة الدرس الحالي:",
shareTitle: "مشاركة الدرس",
copyUrl: "نسخ",
urlCopied: "تم نسخ الرابط إلى الحافظة!",
// Dynamic content // Dynamic content
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.", loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
completed: "مكتمل", completed: "مكتمل",
@@ -639,6 +674,13 @@ const translations = {
dontShowAgain: "Більше не показувати", dontShowAgain: "Більше не показувати",
reset: "Скинути", reset: "Скинути",
// Share dialog
shareDialogTitle: "Поділитися уроком",
shareDialogText: "Скопіюйте цю URL-адресу, щоб поділитися поточним уроком:",
shareTitle: "Поділитися уроком",
copyUrl: "Копіювати",
urlCopied: "URL-адресу скопійовано до буфера обміну!",
// Dynamic content // Dynamic content
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.", loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
completed: "Завершено", completed: "Завершено",

View File

@@ -34,7 +34,15 @@
<!-- Left Panel: Instructions + Editor --> <!-- Left Panel: Instructions + Editor -->
<div class="left-panel"> <div class="left-panel">
<section class="instructions"> <section class="instructions">
<h2 id="lesson-title"></h2> <div class="lesson-title-row">
<h2 id="lesson-title"></h2>
<button id="share-btn" class="share-btn" data-i18n-title="shareTitle" title="Share lesson" aria-label="Share lesson">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</button>
</div>
<div class="task-instruction" id="task-instruction"></div> <div class="task-instruction" id="task-instruction"></div>
<div class="lesson-description" id="lesson-description"></div> <div class="lesson-description" id="lesson-description"></div>
</section> </section>
@@ -257,6 +265,22 @@
</div> </div>
</div> </div>
</dialog> </dialog>
<!-- Share Dialog -->
<dialog id="share-dialog" class="dialog">
<div class="dialog-header">
<h3 data-i18n="shareDialogTitle">Share Lesson</h3>
<button id="share-dialog-close" class="dialog-close" aria-label="Close">&times;</button>
</div>
<div class="dialog-content">
<p data-i18n="shareDialogText">Copy this URL to share the current lesson:</p>
<div class="share-url-container">
<input type="text" id="share-url-input" class="share-url-input" readonly />
<button id="copy-url-btn" class="btn btn-primary" data-i18n="copyUrl">Copy</button>
</div>
<p id="copy-feedback" class="copy-feedback" data-i18n="urlCopied" hidden>URL copied to clipboard!</p>
</div>
</dialog>
</div> </div>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>

View File

@@ -257,7 +257,9 @@ kbd {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
color: var(--light-text); color: var(--light-text);
transition: color 0.2s, border-color 0.2s; transition:
color 0.2s,
border-color 0.2s;
} }
.help-toggle:hover { .help-toggle:hover {
@@ -356,15 +358,47 @@ kbd {
opacity: 0.8; opacity: 0.8;
} }
.lesson-title-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: var(--spacing-sm);
flex-wrap: wrap;
}
#lesson-title { #lesson-title {
font-size: 1.25rem; font-size: 1.25rem;
color: var(--primary-dark); color: var(--primary-dark);
margin-bottom: var(--spacing-sm); margin: 0;
}
.share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
color: var(--light-text);
border-radius: var(--border-radius-sm);
transition: color 0.2s, background 0.2s;
}
.share-btn:hover {
color: var(--primary-color);
background: var(--primary-bg-light);
}
.share-btn svg {
width: 16px;
height: 16px;
} }
.completion-badge { .completion-badge {
display: inline-block; display: inline-block;
margin-left: 0.5rem;
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
background: linear-gradient(135deg, #9b59b6, #e040fb, #00bcd4, #7c4dff); background: linear-gradient(135deg, #9b59b6, #e040fb, #00bcd4, #7c4dff);
color: white; color: white;
@@ -584,14 +618,7 @@ kbd {
position: absolute; position: absolute;
inset: var(--spacing-md); inset: var(--spacing-md);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
background: conic-gradient( background: conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6);
from var(--border-angle),
#9b59b6,
#e040fb,
#00bcd4,
#7c4dff,
#9b59b6
);
filter: blur(30px); filter: blur(30px);
opacity: 0; opacity: 0;
animation: spin-glow 3s ease-out forwards; animation: spin-glow 3s ease-out forwards;
@@ -681,14 +708,7 @@ kbd {
border: 6px solid transparent; border: 6px solid transparent;
background: background:
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box, linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
conic-gradient( conic-gradient(from 0deg, #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
from 0deg,
#9b59b6,
#e040fb,
#00bcd4,
#7c4dff,
#9b59b6
) border-box;
} }
.preview-wrapper.matched { .preview-wrapper.matched {
@@ -696,19 +716,11 @@ kbd {
border: 6px solid transparent; border: 6px solid transparent;
background: background:
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box, linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
conic-gradient( conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
from var(--border-angle),
#9b59b6,
#e040fb,
#00bcd4,
#7c4dff,
#9b59b6
) border-box;
animation: spin-border 3s ease-out forwards; animation: spin-border 3s ease-out forwards;
overflow: visible; overflow: visible;
} }
/* Animated CRISPY speech bubble with SVG tail */ /* Animated CRISPY speech bubble with SVG tail */
.preview-wrapper.matched::after { .preview-wrapper.matched::after {
content: var(--crispy-quote, "Crispyyyyyy!"); content: var(--crispy-quote, "Crispyyyyyy!");
@@ -716,7 +728,10 @@ kbd {
left: 55%; left: 55%;
bottom: 0; bottom: 0;
transform: translateX(-50%) translateY(100%) scale(0.5); transform: translateX(-50%) translateY(100%) scale(0.5);
font-family: system-ui, -apple-system, sans-serif; font-family:
system-ui,
-apple-system,
sans-serif;
font-size: 2rem; font-size: 2rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -1080,7 +1095,10 @@ button.lesson-list-item {
cursor: pointer; cursor: pointer;
font-family: var(--font-main); font-family: var(--font-main);
font-size: 0.9rem; font-size: 0.9rem;
transition: background 0.2s, color 0.2s, border-color 0.2s; transition:
background 0.2s,
color 0.2s,
border-color 0.2s;
} }
.btn:hover { .btn:hover {
@@ -1319,7 +1337,9 @@ input:checked + .toggle-slider::before {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; border-radius: 50%;
transition: background 0.2s, color 0.2s; transition:
background 0.2s,
color 0.2s;
} }
.dialog-close:hover { .dialog-close:hover {
@@ -1370,6 +1390,39 @@ input:checked + .toggle-slider::before {
margin-top: var(--spacing-lg); margin-top: var(--spacing-lg);
} }
/* Share Dialog */
.share-url-container {
display: flex;
gap: var(--spacing-sm);
margin: var(--spacing-md) 0;
}
.share-url-input {
flex: 1;
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-code);
font-size: 0.85rem;
background: var(--code-bg);
color: var(--text-color);
}
.share-url-input:focus {
outline: none;
border-color: var(--primary-color);
}
.copy-feedback {
color: var(--success-color);
font-size: 0.9rem;
margin-top: var(--spacing-sm);
}
[dir="rtl"] .share-url-container {
flex-direction: row-reverse;
}
/* Project Cards in Help Dialog */ /* Project Cards in Help Dialog */
.project-cards { .project-cards {
display: flex; display: flex;
@@ -1386,7 +1439,11 @@ input:checked + .toggle-slider::before {
border: 1px solid var(--primary-bg-medium); border: 1px solid var(--primary-bg-medium);
text-decoration: none; text-decoration: none;
color: var(--text-color); color: var(--text-color);
transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s; transition:
background 0.2s,
border-color 0.2s,
transform 0.2s,
box-shadow 0.2s;
} }
.project-card:hover { .project-card:hover {
@@ -1706,10 +1763,9 @@ input:checked + .toggle-slider::before {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
/* RTL: Completion badge spacing */ /* RTL: Lesson title row */
[dir="rtl"] .completion-badge { [dir="rtl"] .lesson-title-row {
margin-left: 0; flex-direction: row-reverse;
margin-right: 0.5rem;
} }
/* RTL: Lists - bullets/numbers on right side */ /* RTL: Lists - bullets/numbers on right side */