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:
164
src/app.js
164
src/app.js
@@ -3,6 +3,7 @@ import { CodeEditor } from "./impl/CodeEditor.js";
|
||||
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
||||
import { loadModules } from "./config/lessons.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
|
||||
const state = {
|
||||
@@ -28,6 +29,7 @@ const elements = {
|
||||
modulePill: document.getElementById("module-pill"),
|
||||
moduleName: document.querySelector(".module-name"),
|
||||
lessonTitle: document.getElementById("lesson-title"),
|
||||
lessonTitleRow: document.querySelector(".lesson-title-row"),
|
||||
lessonDescription: document.getElementById("lesson-description"),
|
||||
taskInstruction: document.getElementById("task-instruction"),
|
||||
codeInput: document.getElementById("code-input"),
|
||||
@@ -71,7 +73,15 @@ const elements = {
|
||||
resetCodeDialogClose: document.getElementById("reset-code-dialog-close"),
|
||||
cancelResetCode: document.getElementById("cancel-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
|
||||
@@ -283,14 +293,22 @@ function initializeModules() {
|
||||
// Use the new renderModuleList function with both callbacks
|
||||
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
|
||||
|
||||
// Load saved progress and select appropriate module
|
||||
const progressData = lessonEngine.loadUserProgress();
|
||||
const lastModuleId = progressData?.lastModuleId;
|
||||
// Check URL first for shareable links
|
||||
const urlState = parseHash();
|
||||
|
||||
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
|
||||
selectModule(lastModuleId);
|
||||
} else if (modules.length > 0) {
|
||||
selectModule(modules[0].id);
|
||||
if (urlState) {
|
||||
// URL takes priority - navigate to specified lesson
|
||||
navigateToLesson(urlState.moduleId, urlState.lessonIndex, false);
|
||||
} 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();
|
||||
@@ -307,6 +325,10 @@ function selectModule(moduleId) {
|
||||
const success = lessonEngine.setModuleById(moduleId);
|
||||
if (!success) return;
|
||||
|
||||
// Update URL
|
||||
const engineState = lessonEngine.getCurrentState();
|
||||
updateHash(moduleId, engineState.lessonIndex);
|
||||
|
||||
// Update module list UI to highlight the active module
|
||||
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
|
||||
moduleItems.forEach((item) => {
|
||||
@@ -332,6 +354,10 @@ function selectLesson(moduleId, lessonIndex) {
|
||||
}
|
||||
|
||||
lessonEngine.setLessonByIndex(lessonIndex);
|
||||
|
||||
// Update URL
|
||||
updateHash(moduleId, lessonIndex);
|
||||
|
||||
loadCurrentLesson();
|
||||
|
||||
// Close sidebar after selection on mobile
|
||||
@@ -463,7 +489,7 @@ function loadCurrentLesson() {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "completion-badge";
|
||||
badge.textContent = t("completed");
|
||||
elements.lessonTitle.appendChild(badge);
|
||||
elements.lessonTitleRow.appendChild(badge);
|
||||
}
|
||||
|
||||
// Show gradient border for completed lessons
|
||||
@@ -556,9 +582,12 @@ function nextLesson() {
|
||||
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
||||
const success = lessonEngine.nextLesson();
|
||||
if (success) {
|
||||
const newModuleId = lessonEngine.getCurrentState().module?.id;
|
||||
if (newModuleId !== prevModuleId) {
|
||||
updateModuleHighlight(newModuleId);
|
||||
const newState = lessonEngine.getCurrentState();
|
||||
// Update URL
|
||||
updateHash(newState.module.id, newState.lessonIndex);
|
||||
|
||||
if (newState.module.id !== prevModuleId) {
|
||||
updateModuleHighlight(newState.module.id);
|
||||
}
|
||||
loadCurrentLesson();
|
||||
}
|
||||
@@ -568,9 +597,12 @@ function prevLesson() {
|
||||
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
||||
const success = lessonEngine.previousLesson();
|
||||
if (success) {
|
||||
const newModuleId = lessonEngine.getCurrentState().module?.id;
|
||||
if (newModuleId !== prevModuleId) {
|
||||
updateModuleHighlight(newModuleId);
|
||||
const newState = lessonEngine.getCurrentState();
|
||||
// Update URL
|
||||
updateHash(newState.module.id, newState.lessonIndex);
|
||||
|
||||
if (newState.module.id !== prevModuleId) {
|
||||
updateModuleHighlight(newState.module.id);
|
||||
}
|
||||
loadCurrentLesson();
|
||||
}
|
||||
@@ -636,7 +668,7 @@ function runCode() {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "completion-badge";
|
||||
badge.textContent = t("completed");
|
||||
elements.lessonTitle.appendChild(badge);
|
||||
elements.lessonTitleRow.appendChild(badge);
|
||||
}
|
||||
|
||||
// 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 =================
|
||||
|
||||
function initCodeEditor() {
|
||||
@@ -788,6 +907,9 @@ function init() {
|
||||
// Load modules after editor is ready
|
||||
initializeModules();
|
||||
|
||||
// Initialize URL router for shareable links
|
||||
initRouter();
|
||||
|
||||
// Sidebar controls
|
||||
elements.menuBtn.addEventListener("click", openSidebar);
|
||||
elements.closeSidebar.addEventListener("click", closeSidebar);
|
||||
@@ -797,7 +919,9 @@ function init() {
|
||||
elements.logoLink.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
lessonEngine.setModuleById("welcome");
|
||||
updateHash("welcome", 0);
|
||||
loadCurrentLesson();
|
||||
updateModuleHighlight("welcome");
|
||||
});
|
||||
|
||||
// Language select
|
||||
@@ -820,6 +944,7 @@ function init() {
|
||||
if (codeEditor) codeEditor.redo();
|
||||
});
|
||||
elements.resetCodeBtn.addEventListener("click", handleResetCodeClick);
|
||||
elements.shareBtn.addEventListener("click", showShareDialog);
|
||||
|
||||
// Dialogs
|
||||
elements.helpBtn.addEventListener("click", showHelp);
|
||||
@@ -841,6 +966,13 @@ function init() {
|
||||
elements.cancelResetCode.addEventListener("click", closeResetCodeDialog);
|
||||
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
|
||||
elements.disableFeedbackToggle.addEventListener("change", (e) => {
|
||||
state.userSettings.disableFeedbackErrors = !e.target.checked;
|
||||
|
||||
57
src/helpers/router.js
Normal file
57
src/helpers/router.js
Normal 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}`;
|
||||
}
|
||||
42
src/i18n.js
42
src/i18n.js
@@ -99,6 +99,13 @@ const translations = {
|
||||
dontShowAgain: "Don't show this again",
|
||||
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
|
||||
loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.",
|
||||
completed: "Completed",
|
||||
@@ -207,6 +214,13 @@ const translations = {
|
||||
dontShowAgain: "Nicht mehr anzeigen",
|
||||
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
|
||||
loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.",
|
||||
completed: "Erledigt",
|
||||
@@ -315,6 +329,13 @@ const translations = {
|
||||
dontShowAgain: "Nie pokazuj ponownie",
|
||||
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
|
||||
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
|
||||
completed: "Ukończono",
|
||||
@@ -424,6 +445,13 @@ const translations = {
|
||||
dontShowAgain: "No mostrar de nuevo",
|
||||
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
|
||||
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
|
||||
completed: "Completado",
|
||||
@@ -531,6 +559,13 @@ const translations = {
|
||||
dontShowAgain: "لا تظهر هذا مرة أخرى",
|
||||
reset: "إعادة تعيين",
|
||||
|
||||
// Share dialog
|
||||
shareDialogTitle: "مشاركة الدرس",
|
||||
shareDialogText: "انسخ هذا الرابط لمشاركة الدرس الحالي:",
|
||||
shareTitle: "مشاركة الدرس",
|
||||
copyUrl: "نسخ",
|
||||
urlCopied: "تم نسخ الرابط إلى الحافظة!",
|
||||
|
||||
// Dynamic content
|
||||
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
|
||||
completed: "مكتمل",
|
||||
@@ -639,6 +674,13 @@ const translations = {
|
||||
dontShowAgain: "Більше не показувати",
|
||||
reset: "Скинути",
|
||||
|
||||
// Share dialog
|
||||
shareDialogTitle: "Поділитися уроком",
|
||||
shareDialogText: "Скопіюйте цю URL-адресу, щоб поділитися поточним уроком:",
|
||||
shareTitle: "Поділитися уроком",
|
||||
copyUrl: "Копіювати",
|
||||
urlCopied: "URL-адресу скопійовано до буфера обміну!",
|
||||
|
||||
// Dynamic content
|
||||
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
|
||||
completed: "Завершено",
|
||||
|
||||
@@ -34,7 +34,15 @@
|
||||
<!-- Left Panel: Instructions + Editor -->
|
||||
<div class="left-panel">
|
||||
<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="lesson-description" id="lesson-description"></div>
|
||||
</section>
|
||||
@@ -257,6 +265,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</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">×</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>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
|
||||
128
src/main.css
128
src/main.css
@@ -257,7 +257,9 @@ kbd {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: var(--light-text);
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
transition:
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
|
||||
.help-toggle:hover {
|
||||
@@ -356,15 +358,47 @@ kbd {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.lesson-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#lesson-title {
|
||||
font-size: 1.25rem;
|
||||
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 {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: linear-gradient(135deg, #9b59b6, #e040fb, #00bcd4, #7c4dff);
|
||||
color: white;
|
||||
@@ -584,14 +618,7 @@ kbd {
|
||||
position: absolute;
|
||||
inset: var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
background: conic-gradient(
|
||||
from var(--border-angle),
|
||||
#9b59b6,
|
||||
#e040fb,
|
||||
#00bcd4,
|
||||
#7c4dff,
|
||||
#9b59b6
|
||||
);
|
||||
background: conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6);
|
||||
filter: blur(30px);
|
||||
opacity: 0;
|
||||
animation: spin-glow 3s ease-out forwards;
|
||||
@@ -681,14 +708,7 @@ kbd {
|
||||
border: 6px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||
conic-gradient(
|
||||
from 0deg,
|
||||
#9b59b6,
|
||||
#e040fb,
|
||||
#00bcd4,
|
||||
#7c4dff,
|
||||
#9b59b6
|
||||
) border-box;
|
||||
conic-gradient(from 0deg, #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
|
||||
}
|
||||
|
||||
.preview-wrapper.matched {
|
||||
@@ -696,19 +716,11 @@ kbd {
|
||||
border: 6px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||
conic-gradient(
|
||||
from var(--border-angle),
|
||||
#9b59b6,
|
||||
#e040fb,
|
||||
#00bcd4,
|
||||
#7c4dff,
|
||||
#9b59b6
|
||||
) border-box;
|
||||
conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
|
||||
animation: spin-border 3s ease-out forwards;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
/* Animated CRISPY speech bubble with SVG tail */
|
||||
.preview-wrapper.matched::after {
|
||||
content: var(--crispy-quote, "Crispyyyyyy!");
|
||||
@@ -716,7 +728,10 @@ kbd {
|
||||
left: 55%;
|
||||
bottom: 0;
|
||||
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-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -1080,7 +1095,10 @@ button.lesson-list-item {
|
||||
cursor: pointer;
|
||||
font-family: var(--font-main);
|
||||
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 {
|
||||
@@ -1319,7 +1337,9 @@ input:checked + .toggle-slider::before {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
@@ -1370,6 +1390,39 @@ input:checked + .toggle-slider::before {
|
||||
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 {
|
||||
display: flex;
|
||||
@@ -1386,7 +1439,11 @@ input:checked + .toggle-slider::before {
|
||||
border: 1px solid var(--primary-bg-medium);
|
||||
text-decoration: none;
|
||||
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 {
|
||||
@@ -1706,10 +1763,9 @@ input:checked + .toggle-slider::before {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* RTL: Completion badge spacing */
|
||||
[dir="rtl"] .completion-badge {
|
||||
margin-left: 0;
|
||||
margin-right: 0.5rem;
|
||||
/* RTL: Lesson title row */
|
||||
[dir="rtl"] .lesson-title-row {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* RTL: Lists - bullets/numbers on right side */
|
||||
|
||||
Reference in New Issue
Block a user