feat: Enhance lesson tab functionality and UI improvements
parent
a0d6f2615a
commit
614ade6994
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
let showSolution = $state(false);
|
let showSolution = $state(false);
|
||||||
let activeTab = $state<'editor' | 'output'>('editor');
|
let activeTab = $state<'info' | 'exercise' | 'editor' | 'output'>('info');
|
||||||
|
|
||||||
let editor = $state<CodeEditor | null>(null);
|
let editor = $state<CodeEditor | null>(null);
|
||||||
let showCelebration = $state(false);
|
let showCelebration = $state(false);
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
let mobileExpanded = $state(true);
|
let mobileExpanded = $state(true);
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let lessonContentEl = $state<HTMLDivElement>();
|
let lessonContentEl = $state<HTMLDivElement>();
|
||||||
let lessonInfoEl = $state<HTMLDetailsElement>();
|
let infoTabEl = $state<HTMLDivElement>();
|
||||||
|
let exerciseTabEl = $state<HTMLDivElement>();
|
||||||
|
|
||||||
// Drag & resize state for floating panel
|
// Drag & resize state for floating panel
|
||||||
let dragging = $state(false);
|
let dragging = $state(false);
|
||||||
|
|
@ -146,10 +147,11 @@
|
||||||
const node = sel.anchorNode;
|
const node = sel.anchorNode;
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
// Only clear if selection is inside lesson content or lesson info
|
// Only clear if selection is inside lesson content or info/exercise tab panels
|
||||||
const inLesson = lessonContentEl?.contains(node);
|
const inLesson = lessonContentEl?.contains(node);
|
||||||
const inInfo = lessonInfoEl?.contains(node);
|
const inInfo = infoTabEl?.contains(node);
|
||||||
if (inLesson || inInfo) {
|
const inExercise = exerciseTabEl?.contains(node);
|
||||||
|
if (inLesson || inInfo || inExercise) {
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +210,9 @@
|
||||||
compileError = '';
|
compileError = '';
|
||||||
compileSuccess = null;
|
compileSuccess = null;
|
||||||
showSolution = false;
|
showSolution = false;
|
||||||
activeTab = 'editor';
|
if (lesson.lesson_info) activeTab = 'info';
|
||||||
|
else if (lesson.exercise_content) activeTab = 'exercise';
|
||||||
|
else activeTab = 'editor';
|
||||||
mobileExpanded = true;
|
mobileExpanded = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -288,16 +292,6 @@
|
||||||
<title>{data?.lesson_title ?? 'Pelajaran'} - Elemes LMS</title>
|
<title>{data?.lesson_title ?? 'Pelajaran'} - Elemes LMS</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<!-- Celebration overlay -->
|
|
||||||
{#if showCelebration}
|
|
||||||
<div class="celebration-overlay">
|
|
||||||
<div class="celebration-content">
|
|
||||||
<div class="celebration-icon">✓</div>
|
|
||||||
<p class="celebration-text">Selamat! Latihan Selesai!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if data}
|
{#if data}
|
||||||
<!-- Navigation breadcrumb -->
|
<!-- Navigation breadcrumb -->
|
||||||
<div class="lesson-nav">
|
<div class="lesson-nav">
|
||||||
|
|
@ -309,17 +303,6 @@
|
||||||
|
|
||||||
<h1 class="lesson-title">{data.lesson_title}</h1>
|
<h1 class="lesson-title">{data.lesson_title}</h1>
|
||||||
|
|
||||||
<!-- Lesson info (collapsible, selection prevented) -->
|
|
||||||
{#if data.lesson_info}
|
|
||||||
<details class="lesson-info" bind:this={lessonInfoEl}
|
|
||||||
onselectstart={(e) => e.preventDefault()}
|
|
||||||
oncopy={(e) => e.preventDefault()}
|
|
||||||
oncontextmenu={(e) => e.preventDefault()}>
|
|
||||||
<summary>Informasi Pelajaran</summary>
|
|
||||||
<div class="info-content">{@html data.lesson_info}</div>
|
|
||||||
</details>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Main content area -->
|
<!-- Main content area -->
|
||||||
<div class="lesson-layout" class:single-col={editorFloating || isMobile}>
|
<div class="lesson-layout" class:single-col={editorFloating || isMobile}>
|
||||||
<!-- Left: Lesson content (selection & copy prevention) -->
|
<!-- Left: Lesson content (selection & copy prevention) -->
|
||||||
|
|
@ -332,13 +315,6 @@
|
||||||
oncut={(e) => e.preventDefault()}
|
oncut={(e) => e.preventDefault()}
|
||||||
oncontextmenu={(e) => e.preventDefault()}>
|
oncontextmenu={(e) => e.preventDefault()}>
|
||||||
<div class="prose">{@html data.lesson_content}</div>
|
<div class="prose">{@html data.lesson_content}</div>
|
||||||
|
|
||||||
{#if data.exercise_content}
|
|
||||||
<div class="exercise-section">
|
|
||||||
<h2>Latihan</h2>
|
|
||||||
<div class="prose">{@html data.exercise_content}</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Floating restore button (visible when minimized) -->
|
<!-- Floating restore button (visible when minimized) -->
|
||||||
|
|
@ -358,21 +334,21 @@
|
||||||
class:mobile-collapsed={isMobile && !mobileExpanded}
|
class:mobile-collapsed={isMobile && !mobileExpanded}
|
||||||
style={floatStyle}>
|
style={floatStyle}>
|
||||||
|
|
||||||
<!-- Floating/Sheet header -->
|
<!-- Panel header -->
|
||||||
{#if isMobile}
|
{#if isMobile}
|
||||||
<button class="panel-header sheet-handle"
|
<button class="panel-header sheet-handle"
|
||||||
ontouchstart={onSheetTouchStart}
|
ontouchstart={onSheetTouchStart}
|
||||||
ontouchend={onSheetTouchEnd}
|
ontouchend={onSheetTouchEnd}
|
||||||
onclick={toggleMobileSheet}>
|
onclick={toggleMobileSheet}>
|
||||||
<div class="sheet-handle-bar"></div>
|
<div class="sheet-handle-bar"></div>
|
||||||
<span class="panel-title">Code Editor</span>
|
<span class="panel-title">Workspace</span>
|
||||||
</button>
|
</button>
|
||||||
{:else if editorFloating && !editorMinimized}
|
{:else if editorFloating && !editorMinimized}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="panel-header draggable" onmousedown={onDragStart}>
|
<div class="panel-header draggable" onmousedown={onDragStart}>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<span class="resize-handle" onmousedown={(e) => { e.stopPropagation(); onResizeStart(e); }} title="Resize">◳</span>
|
<span class="resize-handle" onmousedown={(e) => { e.stopPropagation(); onResizeStart(e); }} title="Resize">◳</span>
|
||||||
<span class="panel-title">Code Editor</span>
|
<span class="panel-title">Workspace</span>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button type="button" class="panel-btn" onclick={minimizeFloat}
|
<button type="button" class="panel-btn" onclick={minimizeFloat}
|
||||||
title="Minimize">▽</button>
|
title="Minimize">▽</button>
|
||||||
|
|
@ -380,20 +356,62 @@
|
||||||
title="Dock editor">⊡</button>
|
title="Dock editor">⊡</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if !isMobile}
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">Workspace</span>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button type="button" class="btn-float-toggle" onclick={toggleFloat} title="Float editor">⊞</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Editor body -->
|
<!-- Editor body -->
|
||||||
<div class="editor-body" class:body-hidden={isMobile && !mobileExpanded}>
|
<div class="editor-body" class:body-hidden={isMobile && !mobileExpanded}>
|
||||||
{#if isMobile}
|
<!-- Tabs (shared 4-tab for mobile & desktop) -->
|
||||||
<div class="mobile-tabs">
|
<div class="panel-tabs">
|
||||||
|
{#if data.lesson_info}
|
||||||
|
<button class="tab" class:active={activeTab === 'info'}
|
||||||
|
onclick={() => (activeTab = 'info')}>Informasi</button>
|
||||||
|
{/if}
|
||||||
|
{#if data.exercise_content}
|
||||||
|
<button class="tab" class:active={activeTab === 'exercise'}
|
||||||
|
onclick={() => (activeTab = 'exercise')}>Exercise</button>
|
||||||
|
{/if}
|
||||||
<button class="tab" class:active={activeTab === 'editor'}
|
<button class="tab" class:active={activeTab === 'editor'}
|
||||||
onclick={() => (activeTab = 'editor')}>Editor</button>
|
onclick={() => (activeTab = 'editor')}>Code Editor</button>
|
||||||
<button class="tab" class:active={activeTab === 'output'}
|
<button class="tab" class:active={activeTab === 'output'}
|
||||||
onclick={() => (activeTab = 'output')}>Output</button>
|
onclick={() => (activeTab = 'output')}>Output</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Info tab panel -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={activeTab !== 'info'}
|
||||||
|
bind:this={infoTabEl}
|
||||||
|
onselectstart={(e) => e.preventDefault()}
|
||||||
|
oncopy={(e) => e.preventDefault()}
|
||||||
|
oncontextmenu={(e) => e.preventDefault()}>
|
||||||
|
{#if data.lesson_info}
|
||||||
|
<div class="info-content">{@html data.lesson_info}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Exercise tab panel -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={activeTab !== 'exercise'}
|
||||||
|
bind:this={exerciseTabEl}
|
||||||
|
onselectstart={(e) => e.preventDefault()}
|
||||||
|
oncopy={(e) => e.preventDefault()}
|
||||||
|
oncontextmenu={(e) => e.preventDefault()}>
|
||||||
|
{#if data.exercise_content}
|
||||||
|
<div class="exercise-section">
|
||||||
|
<h2>Latihan</h2>
|
||||||
|
<div class="prose">{@html data.exercise_content}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor tab panel -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={activeTab !== 'editor'}>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button type="button" class="btn btn-success" onclick={handleRun} disabled={compiling}>
|
<button type="button" class="btn btn-success" onclick={handleRun} disabled={compiling}>
|
||||||
{compiling ? 'Compiling...' : '▶ Run'}
|
{compiling ? 'Compiling...' : '▶ Run'}
|
||||||
|
|
@ -405,13 +423,9 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="lang-label">{data.language_display_name}</span>
|
<span class="lang-label">{data.language_display_name}</span>
|
||||||
{#if !isMobile && !editorFloating}
|
|
||||||
<button type="button" class="btn-float-toggle" onclick={toggleFloat} title="Float editor">⊞</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editor panel -->
|
<div class="panel">
|
||||||
<div class="panel" class:hidden-mobile={isMobile && activeTab !== 'editor'}>
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
bind:this={editor}
|
bind:this={editor}
|
||||||
code={currentCode}
|
code={currentCode}
|
||||||
|
|
@ -421,17 +435,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Output panel -->
|
|
||||||
<div class="panel" class:hidden-mobile={isMobile && activeTab !== 'output'}>
|
|
||||||
<OutputPanel
|
|
||||||
output={compileOutput}
|
|
||||||
error={compileError}
|
|
||||||
loading={compiling}
|
|
||||||
success={compileSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Expected output hint -->
|
|
||||||
{#if data.expected_output}
|
{#if data.expected_output}
|
||||||
<details class="expected-output">
|
<details class="expected-output">
|
||||||
<summary>Expected Output</summary>
|
<summary>Expected Output</summary>
|
||||||
|
|
@ -439,6 +442,27 @@
|
||||||
</details>
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Output tab panel -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={activeTab !== 'output'}>
|
||||||
|
<OutputPanel
|
||||||
|
output={compileOutput}
|
||||||
|
error={compileError}
|
||||||
|
loading={compiling}
|
||||||
|
success={compileSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Celebration overlay (scoped to editor-area) -->
|
||||||
|
{#if showCelebration}
|
||||||
|
<div class="celebration-overlay">
|
||||||
|
<div class="celebration-content">
|
||||||
|
<div class="celebration-icon">✓</div>
|
||||||
|
<p class="celebration-text">Selamat! Latihan Selesai!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -473,20 +497,7 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-info {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
}
|
|
||||||
.lesson-info summary {
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.info-content {
|
.info-content {
|
||||||
margin-top: 0.75rem;
|
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -507,12 +518,6 @@
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-info {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose :global(pre) {
|
.prose :global(pre) {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
|
@ -533,19 +538,31 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.exercise-section {
|
.exercise-section {
|
||||||
margin-top: 1.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-top: 1rem;
|
|
||||||
border-top: 2px solid var(--color-primary);
|
|
||||||
}
|
}
|
||||||
.exercise-section h2 {
|
.exercise-section h2 {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Editor area (inline mode) ─────────────────────────── */
|
/* ── Editor area (docked mode — styled like floating) ──── */
|
||||||
.editor-area {
|
.editor-area {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 4rem;
|
top: 4rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 85vh;
|
||||||
|
}
|
||||||
|
.editor-area .editor-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Single-column layout ──────────────────────────────── */
|
/* ── Single-column layout ──────────────────────────────── */
|
||||||
|
|
@ -693,7 +710,7 @@
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
cursor: nwse-resize;
|
cursor: nwse-resize;
|
||||||
|
|
@ -707,12 +724,6 @@
|
||||||
background: var(--color-border);
|
background: var(--color-border);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
.editor-area.floating .editor-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.5rem;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.editor-area.floating-hidden {
|
.editor-area.floating-hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
@ -738,9 +749,6 @@
|
||||||
transform: translateY(calc(100% - 48px));
|
transform: translateY(calc(100% - 48px));
|
||||||
}
|
}
|
||||||
.mobile-sheet .editor-body {
|
.mobile-sheet .editor-body {
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.5rem;
|
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
.sheet-handle {
|
.sheet-handle {
|
||||||
|
|
@ -761,8 +769,8 @@
|
||||||
margin: 0 auto 0.25rem;
|
margin: 0 auto 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Mobile tabs ───────────────────────────────────────── */
|
/* ── Tabs ─────────────────────────────────────────────── */
|
||||||
.mobile-tabs {
|
.panel-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|
@ -778,16 +786,22 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Utility ───────────────────────────────────────────── */
|
/* ── Tab panels ────────────────────────────────────────── */
|
||||||
.hidden-mobile {
|
.tab-panel {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.tab-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Utility ───────────────────────────────────────────── */
|
||||||
.editor-body.body-hidden {
|
.editor-body.body-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -801,17 +815,18 @@
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Celebration overlay ──────────────────────────────── */
|
/* ── Celebration overlay (scoped to editor-area) ─────── */
|
||||||
.celebration-overlay {
|
.celebration-overlay {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 200;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
animation: celebFadeIn 0.3s ease-out;
|
animation: celebFadeIn 0.3s ease-out;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
.celebration-content {
|
.celebration-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue