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:
parent
5aebd4487d
commit
f8de0cab5f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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---
|
||||
|
|
@ -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() {}
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue