feat: implement \"Coba\" button for code blocks

and improve mobile UX

   - Menambahkan fitur tombol \"Coba ▶\" untuk menyalin kode
     materi ke editor secara otomatis.
   - Implementasi setupTryButtons.ts (Svelte Action) untuk
     injeksi tombol berdasarkan label bahasa Markdown (c, python,
  `arduino).
   - Mengatur perilaku tombol agar hanya memuat kode dan
     memfokuskan tab (No Auto-Run) untuk memberikan kesempatan
     siswa meninjau kode.
   - Optimasi tampilan Mobile (< 768px):
       - Tombol \"Coba\" selalu terlihat (tanpa hover).
       - Otomatis membuka workspace ke mode 'half-sheet' dan
         melakukan smooth scroll ke editor saat tombol diklik.
   - Sinkronisasi urutan render: setupTryButtons dijalankan
     sebelum highlightAllCode untuk memastikan deteksi bahasa
     yang akurat.
   - Update documentation.md dengan panduan penggunaan fitur
     tombol \"Coba\" dan dokumentasi refactoring modularitas
     halaman lesson.
   - Menambahkan modul uji coba LaTeX ke daftar materi di
     home.md
This commit is contained in:
a2nr 2026-04-27 21:38:36 +07:00
parent 5aebd4487d
commit f8de0cab5f
6 changed files with 209 additions and 2 deletions

View File

@ -149,11 +149,11 @@ lms-c/
│ │ ├── compiler.ts │ │ ├── compiler.ts
│ │ └── circuitjs.ts # CircuitJSApi interface │ │ └── circuitjs.ts # CircuitJSApi interface
│ ├── routes/ │ ├── routes/
│ │ ├── +layout.svelte # App shell (Navbar, theme) │ │ ├── +layout.svelte # App shell (Navbar, theme sinkronisasi)
│ │ ├── +page.svelte # Home (lesson grid) │ │ ├── +page.svelte # Home (lesson grid)
│ │ ├── +page.ts # SSR data loader │ │ ├── +page.ts # SSR data loader
│ │ ├── lesson/[slug]/ │ │ ├── lesson/[slug]/
│ │ │ ├── +page.svelte # Lesson viewer (semua mode) │ │ │ ├── +page.svelte # Lesson viewer (Layout utama & inisialisasi)
│ │ │ └── +page.ts # SSR data loader │ │ │ └── +page.ts # SSR data loader
│ │ └── progress/ │ │ └── progress/
│ │ └── +page.svelte # Teacher dashboard │ │ └── +page.svelte # Teacher dashboard
@ -183,6 +183,13 @@ lms-c/
## Keputusan Teknis ## 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? ### Kenapa SvelteKit, bukan Flutter?
- CodeMirror 6 (code editor production-grade) — tidak ada equivalent di Flutter - CodeMirror 6 (code editor production-grade) — tidak ada equivalent di Flutter
- Bundle ~250KB vs Flutter Web 2-4MB - 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_CODE---` / `---END_SOLUTION_CODE---` | Solusi kode (ditampilkan setelah selesai) |
| `---SOLUTION_CIRCUIT---` / `---END_SOLUTION_CIRCUIT---` | Solusi circuit | | `---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) ## Velxio Integration (Arduino Simulator)

View File

@ -15,6 +15,7 @@ Semua materi bisa dikerjakan langsung di browser!
### Dasar-Dasar ### Dasar-Dasar
1. [Hello, World!](lesson/hello_world.md) 1. [Hello, World!](lesson/hello_world.md)
2. [Variabel](lesson/variabel.md) 2. [Variabel](lesson/variabel.md)
3. [Uji Coba LaTeX](/lesson/latex_test)
### Elektronika (Hybrid) ### Elektronika (Hybrid)
3. [Rangkaian Dasar](lesson/rangkaian_dasar.md) 3. [Rangkaian Dasar](lesson/rangkaian_dasar.md)

View File

@ -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 <stdio.h>
int main() {
printf("4\n");
return 0;
}
---END_INITIAL_CODE---
---EXPECTED_OUTPUT---
4
---END_EXPECTED_OUTPUT---

View File

@ -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() {}
};
}

View File

@ -22,6 +22,7 @@
import { noSelect } from '$actions/noSelect'; import { noSelect } from '$actions/noSelect';
import { createFloatingPanel } from '$actions/floatingPanel.svelte'; import { createFloatingPanel } from '$actions/floatingPanel.svelte';
import { highlightAllCode } from '$actions/highlightCode'; import { highlightAllCode } from '$actions/highlightCode';
import { setupTryButtons } from '$actions/setupTryButtons';
import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds'; import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds';
import { renderMath, autoRenderMath } from '$lib/actions/renderMath'; import { renderMath, autoRenderMath } from '$lib/actions/renderMath';
import { tick, untrack } from 'svelte'; import { tick, untrack } from 'svelte';
@ -242,16 +243,50 @@
if (velxioBridge) { velxioBridge.destroy(); velxioBridge = null; } 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 // Apply syntax highlighting + circuit embeds after content renders
$effect(() => { $effect(() => {
if (data) { if (data) {
tick().then(() => { tick().then(() => {
if (contentEl) { if (contentEl) {
setupTryButtons(contentEl, handleTryCode);
highlightAllCode(contentEl); highlightAllCode(contentEl);
renderCircuitEmbeds(contentEl); renderCircuitEmbeds(contentEl);
autoRenderMath(contentEl); autoRenderMath(contentEl);
} }
if (tabsEl) { if (tabsEl) {
setupTryButtons(tabsEl, handleTryCode);
highlightAllCode(tabsEl); highlightAllCode(tabsEl);
renderCircuitEmbeds(tabsEl); renderCircuitEmbeds(tabsEl);
autoRenderMath(tabsEl); autoRenderMath(tabsEl);

View File

@ -15,6 +15,34 @@
padding: 0.75rem; padding: 0.75rem;
border-radius: var(--radius); border-radius: var(--radius);
overflow-x: auto; 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, .prose code,
.tab-content code { .tab-content code {