diff --git a/documentation.md b/documentation.md index 3335013..54bbcb7 100644 --- a/documentation.md +++ b/documentation.md @@ -149,11 +149,11 @@ lms-c/ │ │ ├── compiler.ts │ │ └── circuitjs.ts # CircuitJSApi interface │ ├── routes/ - │ │ ├── +layout.svelte # App shell (Navbar, theme) + │ │ ├── +layout.svelte # App shell (Navbar, theme sinkronisasi) │ │ ├── +page.svelte # Home (lesson grid) │ │ ├── +page.ts # SSR data loader │ │ ├── lesson/[slug]/ - │ │ │ ├── +page.svelte # Lesson viewer (semua mode) + │ │ │ ├── +page.svelte # Lesson viewer (Layout utama & inisialisasi) │ │ │ └── +page.ts # SSR data loader │ │ └── progress/ │ │ └── +page.svelte # Teacher dashboard @@ -183,6 +183,13 @@ lms-c/ ## Keputusan Teknis +### Refactoring Halaman Lesson (Modularitas) +Halaman lesson (`+page.svelte`) sebelumnya bertindak sebagai "God Component" (>1.300 baris) yang menangani state, integrasi iframe, logika kompilasi, dan UI rendering. Untuk skalabilitas, halaman ini telah direfaktor menjadi arsitektur modular: +1. **`lessonState.svelte.ts`**: Menggunakan Svelte 5 Runes (`$state`, `$derived`) untuk mengelola seluruh state pelajaran secara reaktif di luar komponen UI. +2. **`LessonWorkspace.svelte`**: Komponen presentasional yang membungkus seluruh area interaktif (Editor, Output, Tab). +3. **`VelxioIframe.svelte`**: Mengisolasi logika inisialisasi bridge, `postMessage`, dan *auto-save* khusus untuk simulator Arduino. +4. **`highlightCode.ts` (Svelte Action)**: Syntax highlighting menggunakan eksekusi level DOM (via `use:highlightAction`) yang dipicu ulang setiap kali string HTML materi berubah, menggantikan efek reaktif (`$effect`) yang rentan *race-condition*. + ### Kenapa SvelteKit, bukan Flutter? - CodeMirror 6 (code editor production-grade) — tidak ada equivalent di Flutter - Bundle ~250KB vs Flutter Web 2-4MB @@ -302,6 +309,21 @@ Elemes mendukung beberapa mode lesson melalui **marker** di file markdown. Mode | `---SOLUTION_CODE---` / `---END_SOLUTION_CODE---` | Solusi kode (ditampilkan setelah selesai) | | `---SOLUTION_CIRCUIT---` / `---END_SOLUTION_CIRCUIT---` | Solusi circuit | +### Fitur Tombol "Coba" (Code Try-out) + +LMS menyediakan fitur tombol **"Coba ▶"** pada blok kode di dalam materi pelajaran. + +**Penentuan Kemunculan Tombol:** +- Tombol **hanya muncul** jika instruktur secara eksplisit memberikan label bahasa pada *code fence* di file Markdown (contoh: ` ```c `, ` ```python `, ` ```arduino ` atau ` ```cpp `). +- Jika blok kode bersifat generik atau tidak memiliki label (hanya ` ``` `), tombol **tidak akan muncul**. Ini memberikan kendali untuk membedakan mana cuplikan kode yang bisa dicoba dan mana yang sekadar teks penjelasan/diagram. + +**Perilaku Tombol:** +1. **Load & Switch Tab:** Mengklik tombol akan menyalin teks kode ke editor dan mengalihkan fokus LMS ke tab yang relevan (C, Python, atau Arduino/Velxio). +2. **Review Sebelum Eksekusi:** Kode **tidak langsung dieksekusi** (No Auto-Run). Siswa berkesempatan untuk meninjau atau memodifikasi kode sebelum menekan tombol "Run". +3. **Mobile UX (Layar < 768px):** + - Tombol "Coba ▶" selalu terlihat (karena ketiadaan fitur *hover* pada layar sentuh). + - Saat diklik, panel *workspace* di bawah akan otomatis membuka setengah layar (`half-sheet`) dan browser akan melakukan *smooth scroll* agar editor langsung terfokus. + --- ## Velxio Integration (Arduino Simulator) diff --git a/examples/content/home.md b/examples/content/home.md index 4865ed1..7cbe46d 100644 --- a/examples/content/home.md +++ b/examples/content/home.md @@ -15,6 +15,7 @@ Semua materi bisa dikerjakan langsung di browser! ### Dasar-Dasar 1. [Hello, World!](lesson/hello_world.md) 2. [Variabel](lesson/variabel.md) +3. [Uji Coba LaTeX](/lesson/latex_test) ### Elektronika (Hybrid) 3. [Rangkaian Dasar](lesson/rangkaian_dasar.md) diff --git a/examples/content/latex_test.md b/examples/content/latex_test.md new file mode 100644 index 0000000..31806ce --- /dev/null +++ b/examples/content/latex_test.md @@ -0,0 +1,70 @@ +---LESSON_INFO--- +**Learning Objectives:** +- Memahami cara penulisan LaTeX di LMS. +- Verifikasi perenderan rumus matematika. +---END_LESSON_INFO--- + +# Uji Coba LaTeX + +Selamat datang di modul uji coba LaTeX. LMS ini sekarang mendukung penulisan rumus matematika menggunakan KaTeX. + +## Rumus Inline +Rumus inline dapat ditulis dengan menggunakan satu simbol dollar, contohnya: $E = mc^2$. +Anda juga bisa menuliskan rumus yang lebih kompleks seperti akar kuadrat: $\sqrt{x^2 + y^2} = r$. + +## Rumus Block +Rumus block ditulis dengan menggunakan double dollar: +$$\int_{a}^{b} x^2 dx = \frac{1}{3}(b^3 - a^3)$$ + +### Persamaan Multi-baris (Aligned) +Gunakan environment `aligned` untuk menjabarkan langkah-langkah perhitungan agar simbol sama dengan (`=`) sejajar: +$$ +\begin{aligned} +(a+b)^2 &= (a+b)(a+b) \\ +&= a^2 + ab + ba + b^2 \\ +&= a^2 + 2ab + b^2 +\end{aligned} +$$ + +### Fungsi Piecewise (Cases) +Contoh penulisan fungsi dengan beberapa kondisi: +$$ +f(n) = +\begin{cases} +n/2 & \text{jika } n \text{ genap} \\ +3n+1 & \text{jika } n \text{ ganjil} +\end{cases} +$$ + +### Matriks dan Vektor +$$ +A = \begin{pmatrix} +a_{11} & a_{12} \\ +a_{21} & a_{22} +\end{pmatrix}, \quad +\mathbf{v} = \begin{bmatrix} +x \\ +y +\end{bmatrix} +$$ + +## Penggunaan dalam Latihan +Anda juga bisa melihat rumus di bagian latihan di samping. + +---EXERCISE--- + +Selesaikan masalah berikut: +Jika diketahui $f(x) = 2x + 3$, hitunglah nilai dari $\int_{0}^{1} f(x) dx$. + +---INITIAL_CODE--- +#include + +int main() { + printf("4\n"); + return 0; +} +---END_INITIAL_CODE--- + +---EXPECTED_OUTPUT--- +4 +---END_EXPECTED_OUTPUT--- diff --git a/frontend/src/lib/actions/setupTryButtons.ts b/frontend/src/lib/actions/setupTryButtons.ts new file mode 100644 index 0000000..f2f416a --- /dev/null +++ b/frontend/src/lib/actions/setupTryButtons.ts @@ -0,0 +1,51 @@ +export function setupTryButtons(container: HTMLElement, callback: (code: string, lang: string) => void) { + // Function to apply buttons + const applyButtons = () => { + container.querySelectorAll('pre code').forEach((block) => { + const pre = block.parentElement; + if (!pre) return; + + // Don't add if already added + if (pre.querySelector('.btn-try')) return; + + let lang = ''; + const classes = Array.from(block.classList); + + // Strictly follow the language tag provided in markdown fences + // highlight.js adds 'language-xxxx' class based on the fence tag + if (classes.includes('language-c')) { + lang = 'c'; + } else if (classes.includes('language-python')) { + lang = 'python'; + } else if (classes.some(c => ['language-cpp', 'language-arduino', 'language-ino'].includes(c))) { + lang = 'cpp'; + } + + // If no specific language class is found, we don't add the button. + // This allows teachers to hide the button by using generic code blocks (```) + // or inline code which doesn't get these classes. + + if (lang) { + const btn = document.createElement('button'); + btn.className = 'btn btn-try'; + btn.textContent = 'Coba ▶'; + btn.onclick = () => { + callback(block.textContent || '', lang); + }; + + pre.style.position = 'relative'; + pre.appendChild(btn); + } + }); + }; + + applyButtons(); + + return { + update(newCallback: (code: string, lang: string) => void) { + callback = newCallback; + applyButtons(); + }, + destroy() {} + }; +} diff --git a/frontend/src/routes/lesson/[slug]/+page.svelte b/frontend/src/routes/lesson/[slug]/+page.svelte index 80e3e3a..b129ce8 100644 --- a/frontend/src/routes/lesson/[slug]/+page.svelte +++ b/frontend/src/routes/lesson/[slug]/+page.svelte @@ -22,6 +22,7 @@ import { noSelect } from '$actions/noSelect'; import { createFloatingPanel } from '$actions/floatingPanel.svelte'; import { highlightAllCode } from '$actions/highlightCode'; + import { setupTryButtons } from '$actions/setupTryButtons'; import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds'; import { renderMath, autoRenderMath } from '$lib/actions/renderMath'; import { tick, untrack } from 'svelte'; @@ -242,16 +243,50 @@ if (velxioBridge) { velxioBridge.destroy(); velxioBridge = null; } }); + function handleTryCode(code: string, lang: string) { + if (lang === 'python' || lang === 'c') { + currentLanguage = lang; + currentCode = code; + if (lang === 'c') cCode = code; + else pythonCode = code; + editor?.setCode(code); + activeTab = 'editor'; + if (!isMobile) { + float.floating = true; + } else { + mobileMode = 'half'; + tick().then(() => { + document.querySelector('.editor-area')?.scrollIntoView({ behavior: 'smooth' }); + }); + } + } else if (lang === 'cpp') { + if (isVelxio && velxioBridge) { + velxioBridge.loadCode([{ name: 'sketch.ino', content: code }]); + activeTab = 'velxio'; + if (!isMobile) { + float.floating = true; + } else { + mobileMode = 'half'; + tick().then(() => { + document.querySelector('.editor-area')?.scrollIntoView({ behavior: 'smooth' }); + }); + } + } + } + } + // Apply syntax highlighting + circuit embeds after content renders $effect(() => { if (data) { tick().then(() => { if (contentEl) { + setupTryButtons(contentEl, handleTryCode); highlightAllCode(contentEl); renderCircuitEmbeds(contentEl); autoRenderMath(contentEl); } if (tabsEl) { + setupTryButtons(tabsEl, handleTryCode); highlightAllCode(tabsEl); renderCircuitEmbeds(tabsEl); autoRenderMath(tabsEl); diff --git a/frontend/src/routes/lesson/[slug]/lesson.css b/frontend/src/routes/lesson/[slug]/lesson.css index 3b813b6..3c8bc00 100644 --- a/frontend/src/routes/lesson/[slug]/lesson.css +++ b/frontend/src/routes/lesson/[slug]/lesson.css @@ -15,6 +15,34 @@ padding: 0.75rem; border-radius: var(--radius); overflow-x: auto; + position: relative; +} + +.btn-try { + position: absolute; + top: 0.5rem; + right: 0.5rem; + opacity: 0; + transition: opacity 0.2s ease-in-out; + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + border-radius: var(--radius); + background-color: var(--color-primary); + color: #fff; + border: none; + cursor: pointer; + z-index: 10; +} + +.prose pre:hover .btn-try, +.tab-content pre:hover .btn-try { + opacity: 1; +} + +@media (max-width: 768px) { + .btn-try { + opacity: 1; + } } .prose code, .tab-content code {