feat(content-editor): dynamic tabs for C/Python/circuit preview
Add dynamic chrome tabs based on previewActiveTabs from markdown parsing. When teacher writes ---INITIAL_CODE--- or ---INITIAL_PYTHON--- blocks, the corresponding C/Python tab appears with read-only code preview. - Widen activeTab type to string for dynamic tab IDs - Extract code blocks client-side from raw markdown body - Render chrome tabs dynamically from allTabs derived state - Auto-switch away from invalid tabs when preview updates
This commit is contained in:
parent
0d299055b0
commit
fbc8a3fb03
|
|
@ -24,6 +24,9 @@ export class ContentEditorManager {
|
||||||
mobileMode = $state<'hidden' | 'h30' | 'h50' | 'h70' | 'full'>('hidden');
|
mobileMode = $state<'hidden' | 'h30' | 'h50' | 'h70' | 'full'>('hidden');
|
||||||
isMobile = $state(false);
|
isMobile = $state(false);
|
||||||
|
|
||||||
|
// Tab state — 'editor' | 'preview' | 'exercise' | 'quiz' | dynamic tabs from active_tabs ('c', 'python', 'circuit', 'velxio', 'flowchart')
|
||||||
|
activeTab = $state<string>('editor');
|
||||||
|
|
||||||
// Tree state
|
// Tree state
|
||||||
contentTree = $state<any[]>([]);
|
contentTree = $state<any[]>([]);
|
||||||
assetsTree = $state<any[]>([]);
|
assetsTree = $state<any[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { authIsTeacher, authLoggedIn } from '$stores/auth';
|
import { authIsTeacher } from '$stores/auth';
|
||||||
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
import { createFloatingPanel } from '$actions/floatingPanel.svelte';
|
||||||
import CodeEditor from '$components/CodeEditor.svelte';
|
import CodeEditor from '$components/CodeEditor.svelte';
|
||||||
import ContentFileTree from '$components/ContentFileTree.svelte';
|
import ContentFileTree from '$components/ContentFileTree.svelte';
|
||||||
|
|
@ -38,6 +38,73 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Available tabs based on preview data
|
||||||
|
let hasExercise = $derived(!!mgr.previewExerciseHtml);
|
||||||
|
let hasQuiz = $derived(mgr.previewQuizData.length > 0);
|
||||||
|
|
||||||
|
// Dynamic tabs from previewActiveTabs (e.g. 'c', 'python', 'circuit', 'velxio', 'flowchart')
|
||||||
|
type DynamicTab = { id: string; label: string; show?: boolean };
|
||||||
|
const TAB_LABELS: Record<string, string> = {
|
||||||
|
c: 'C',
|
||||||
|
python: 'Python',
|
||||||
|
circuit: 'Circuit',
|
||||||
|
velxio: 'Arduino',
|
||||||
|
flowchart: 'Flowchart',
|
||||||
|
};
|
||||||
|
let dynamicTabs = $derived(
|
||||||
|
(mgr.previewActiveTabs ?? [])
|
||||||
|
.filter((t) => !['quiz'].includes(t)) // quiz handled separately
|
||||||
|
.map((t): DynamicTab => ({ id: t, label: TAB_LABELS[t] ?? t }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract code blocks from raw markdown body for dynamic tabs
|
||||||
|
function extractCode(body: string, marker: string): string {
|
||||||
|
const startTag = `---${marker}---`;
|
||||||
|
const endTag = `---END_${marker}---`;
|
||||||
|
const startIdx = body.indexOf(startTag);
|
||||||
|
if (startIdx === -1) return '';
|
||||||
|
const codeStart = startIdx + startTag.length;
|
||||||
|
const endIdx = body.indexOf(endTag, codeStart);
|
||||||
|
if (endIdx === -1) return body.substring(codeStart).trim();
|
||||||
|
return body.substring(codeStart, endIdx).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let extractedCodes = $derived((() => {
|
||||||
|
const codes: Record<string, string> = {};
|
||||||
|
for (const tab of dynamicTabs) {
|
||||||
|
if (tab.id === 'c') codes.c = extractCode(mgr.body, 'INITIAL_CODE');
|
||||||
|
else if (tab.id === 'python') codes.python = extractCode(mgr.body, 'INITIAL_PYTHON');
|
||||||
|
else if (tab.id === 'circuit') codes.circuit = extractCode(mgr.body, 'INITIAL_CIRCUIT');
|
||||||
|
else if (tab.id === 'velxio') codes.velxio = extractCode(mgr.body, 'INITIAL_CODE_ARDUINO');
|
||||||
|
else if (tab.id === 'flowchart') codes.flowchart = extractCode(mgr.body, 'INITIAL_FLOWCHART');
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
})());
|
||||||
|
|
||||||
|
// Tab label map for chrome tabs
|
||||||
|
let allTabs = $derived([
|
||||||
|
{ id: 'editor', label: 'Editor' },
|
||||||
|
{ id: 'preview', label: 'Preview', show: !!mgr.previewHtml },
|
||||||
|
...dynamicTabs,
|
||||||
|
{ id: 'exercise', label: 'Exercise', show: hasExercise },
|
||||||
|
{ id: 'quiz', label: 'Quiz', show: hasQuiz },
|
||||||
|
].filter((t) => t.show !== false));
|
||||||
|
|
||||||
|
// Auto-switch to editor tab when switching files
|
||||||
|
$effect(() => {
|
||||||
|
if (mgr.activePath) {
|
||||||
|
mgr.activeTab = 'editor';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-switch away from a tab that's no longer available
|
||||||
|
$effect(() => {
|
||||||
|
const validIds = allTabs.map((t) => t.id);
|
||||||
|
if (!validIds.includes(mgr.activeTab)) {
|
||||||
|
mgr.activeTab = 'editor';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function loadTrees() {
|
async function loadTrees() {
|
||||||
mgr.treeLoading = true;
|
mgr.treeLoading = true;
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,6 +137,7 @@
|
||||||
mgr.previewExerciseHtml = '';
|
mgr.previewExerciseHtml = '';
|
||||||
mgr.previewQuizData = [];
|
mgr.previewQuizData = [];
|
||||||
mgr.previewError = null;
|
mgr.previewError = null;
|
||||||
|
mgr.activeTab = 'editor';
|
||||||
} else {
|
} else {
|
||||||
mgr.loadDraft(path);
|
mgr.loadDraft(path);
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +164,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTabClick(tab: string) {
|
||||||
|
mgr.activeTab = tab;
|
||||||
|
if (mgr.isMobile && mgr.mobileMode === 'hidden') {
|
||||||
|
mgr.mobileMode = 'h50';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tree operations
|
// Tree operations
|
||||||
async function handleCreateFolder(parentPath: string) {
|
async function handleCreateFolder(parentPath: string) {
|
||||||
const name = window.prompt('Nama folder baru:');
|
const name = window.prompt('Nama folder baru:');
|
||||||
|
|
@ -196,7 +271,7 @@
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopyAssetPath(e?: MouseEvent) {
|
function handleCopyAssetPath() {
|
||||||
const path = mgr.activePath;
|
const path = mgr.activePath;
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
navigator.clipboard.writeText(`/assets/${path}`);
|
navigator.clipboard.writeText(`/assets/${path}`);
|
||||||
|
|
@ -279,75 +354,79 @@
|
||||||
class:mobile-h70={mgr.isMobile && mgr.mobileMode === 'h70'}
|
class:mobile-h70={mgr.isMobile && mgr.mobileMode === 'h70'}
|
||||||
class:mobile-full={mgr.isMobile && mgr.mobileMode === 'full'}
|
class:mobile-full={mgr.isMobile && mgr.mobileMode === 'full'}
|
||||||
style={float.style}
|
style={float.style}
|
||||||
> <!-- Drag Handle Bar (floating mode) --> {#if float.floating && !mgr.isMobile}
|
>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- Panel Header (floating drag handle) -->
|
||||||
<div class="panel-header draggable" onmousedown={float.onDragStart} ontouchstart={float.onTouchDragStart}>
|
{#if float.floating && !mgr.isMobile}
|
||||||
<span class="resize-handle" onmousedown={(e) => { e.stopPropagation(); float.onResizeStart(e); }} ontouchstart={float.onTouchResizeStart}>◳</span>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="panel-title">Workspace</div>
|
<div class="panel-header draggable" onmousedown={float.onDragStart} ontouchstart={float.onTouchDragStart}>
|
||||||
<div class="panel-actions">
|
<span class="resize-handle" onmousedown={(e) => { e.stopPropagation(); float.onResizeStart(e); }} ontouchstart={float.onTouchResizeStart}>◳</span>
|
||||||
<button class="panel-btn" title="Kembali ke dock (inline)" onclick={float.toggle}>⊞</button>
|
<div class="chrome-tabs">
|
||||||
<button class="panel-btn" title="Minimize" onclick={float.minimize}>▽</button>
|
{#each allTabs as tab}
|
||||||
</div>
|
<button class="chrome-tab" class:active={mgr.activeTab === tab.id} onclick={() => handleTabClick(tab.id)}>{tab.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button class="panel-btn" title="Minimize" onclick={float.minimize}>▽</button>
|
||||||
|
<button class="panel-btn" title="Kembali ke dock (inline)" onclick={float.toggle}>⊞</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if mgr.isMobile}
|
||||||
|
<!-- Mobile: panel header with tabs + swipe -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="panel-header" ontouchstart={(e) => { /* future swipe support */ }}>
|
||||||
|
{#if !mobileShowTree && mgr.activePath}
|
||||||
|
<button class="panel-btn mobile-back-btn" onclick={handleMobileBackToTree} title="Kembali ke tree">◀</button>
|
||||||
|
{/if}
|
||||||
|
<div class="chrome-tabs">
|
||||||
|
{#if !mobileShowTree}
|
||||||
|
{#each allTabs as tab}
|
||||||
|
<button class="chrome-tab" class:active={mgr.activeTab === tab.id} onclick={() => handleTabClick(tab.id)}>{tab.label}</button>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<span class="chrome-tab active">File Tree</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button class="panel-btn" onclick={() => { if (mgr.mobileMode !== 'hidden') mgr.mobileMode = mgr.mobileMode === 'full' ? 'h50' : 'hidden'; else mgr.mobileMode = 'h50'; }} title="Minimize" disabled={mgr.mobileMode === 'hidden'}>▽</button>
|
||||||
|
<button class="panel-btn" onclick={() => { if (mgr.mobileMode !== 'full') mgr.mobileMode = mgr.mobileMode === 'hidden' ? 'h50' : 'full'; else mgr.mobileMode = 'h50'; }} title="Maximize" disabled={mgr.mobileMode === 'full'}>△</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Desktop: panel header with tabs + actions -->
|
||||||
|
<div class="panel-header">
|
||||||
|
{#if mgr.activePath && !isAssetFile}
|
||||||
|
<button class="panel-btn mobile-back-btn" onclick={handleMobileBackToTree} title="Kembali ke tree">◀</button>
|
||||||
|
{/if}
|
||||||
|
<div class="chrome-tabs">
|
||||||
|
{#if mgr.activePath}
|
||||||
|
{#each allTabs as tab}
|
||||||
|
<button class="chrome-tab" class:active={mgr.activeTab === tab.id} onclick={() => handleTabClick(tab.id)}>{tab.label}</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
{#if !isAssetFile}
|
||||||
|
<button class="panel-btn save-btn" onclick={() => mgr.handleSave()} disabled={mgr.saving || !mgr.dirty || !mgr.activePath}>
|
||||||
|
{mgr.saving ? 'Menyimpan...' : '💾 Simpan'}
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn publish-btn" onclick={() => mgr.handlePublish()} disabled={mgr.publishing || !mgr.draftId}>
|
||||||
|
{mgr.publishing ? 'Mempublikasikan...' : '🚀 Publish'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="btn-float-toggle" onclick={float.toggle} title={float.floating ? 'Dock ke layout' : 'Float (detach)'}>⊞</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Editor Header -->
|
<!-- Editor Body: tab panels -->
|
||||||
<div class="editor-header" class:draggable-header={float.floating && !mgr.isMobile}
|
|
||||||
onmousedown={float.floating && !mgr.isMobile ? float.onDragStart : undefined}
|
|
||||||
>
|
|
||||||
<!-- Mobile: back to tree button --> {#if mgr.isMobile && !mobileShowTree}
|
|
||||||
<button class="panel-btn mobile-back-btn" onclick={handleMobileBackToTree} title="Kembali ke tree">◀</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<span class="editor-filename">{activeFileName || 'Editor'}</span>
|
|
||||||
<div class="editor-actions">
|
|
||||||
<!-- Desktop: Dock/Float toggle -->
|
|
||||||
{#if !mgr.isMobile}
|
|
||||||
<button class="btn-float-toggle" onclick={float.toggle} title={float.floating ? 'Dock ke layout' : 'Float (detach)'}>⊞</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !isAssetFile}
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-secondary"
|
|
||||||
onclick={() => mgr.handleSave()}
|
|
||||||
disabled={mgr.saving || !mgr.dirty || !mgr.activePath}
|
|
||||||
>
|
|
||||||
{mgr.saving ? 'Menyimpan...' : '💾 Simpan Draft'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-primary"
|
|
||||||
onclick={() => mgr.handlePublish()}
|
|
||||||
disabled={mgr.publishing || !mgr.draftId}
|
|
||||||
>
|
|
||||||
{mgr.publishing ? 'Mempublikasikan...' : '🚀 Publish'}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{#if mgr.isMobile}
|
|
||||||
<div class="panel-actions">
|
|
||||||
<button class="panel-btn" onclick={() => { if (mgr.mobileMode !== 'hidden') mgr.mobileMode = mgr.mobileMode === 'full' ? 'h50' : 'hidden'; else mgr.mobileMode = 'h50'; }} title="Minimize" disabled={mgr.mobileMode === 'hidden'}>▽</button>
|
|
||||||
<button class="panel-btn" onclick={() => { if (mgr.mobileMode !== 'full') mgr.mobileMode = mgr.mobileMode === 'hidden' ? 'h50' : 'full'; else mgr.mobileMode = 'h50'; }} title="Maximize" disabled={mgr.mobileMode === 'full'}>△</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editor Body -->
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mgr.isMobile && mobileShowTree}
|
{#if mgr.isMobile && mobileShowTree}
|
||||||
<!-- MOBILE: Show full-width tree -->
|
<!-- MOBILE: Show full-width tree -->
|
||||||
<div class="tree-panel mobile-tree-full">
|
<div class="tree-panel mobile-tree-full">
|
||||||
<div class="tree-header">
|
<div class="tree-header">
|
||||||
<div class="tree-tabs">
|
<div class="tree-tabs">
|
||||||
<button
|
<button class="tree-tab" class:active={mgr.treeRoot === 'content'} onclick={() => mgr.treeRoot = 'content'}>Materi</button>
|
||||||
class="tree-tab"
|
<button class="tree-tab" class:active={mgr.treeRoot === 'assets'} onclick={() => mgr.treeRoot = 'assets'}>Assets</button>
|
||||||
class:active={mgr.treeRoot === 'content'}
|
|
||||||
onclick={() => mgr.treeRoot = 'content'}
|
|
||||||
>Materi</button>
|
|
||||||
<button
|
|
||||||
class="tree-tab"
|
|
||||||
class:active={mgr.treeRoot === 'assets'}
|
|
||||||
onclick={() => mgr.treeRoot = 'assets'}
|
|
||||||
>Assets</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-actions-header">
|
<div class="tree-actions-header">
|
||||||
{#if mgr.treeRoot === 'content'}
|
{#if mgr.treeRoot === 'content'}
|
||||||
|
|
@ -376,21 +455,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- DESKTOP or MOBILE: Tree + Code side by side -->
|
<!-- DESKTOP or MOBILE: Tree + Tab panels side by side -->
|
||||||
<!-- File Tree -->
|
<!-- File Tree -->
|
||||||
<div class="tree-panel" class:mobile-tree-hidden={mgr.isMobile}>
|
<div class="tree-panel" class:mobile-tree-hidden={mgr.isMobile}>
|
||||||
<div class="tree-header">
|
<div class="tree-header">
|
||||||
<div class="tree-tabs">
|
<div class="tree-tabs">
|
||||||
<button
|
<button class="tree-tab" class:active={mgr.treeRoot === 'content'} onclick={() => mgr.treeRoot = 'content'}>Materi</button>
|
||||||
class="tree-tab"
|
<button class="tree-tab" class:active={mgr.treeRoot === 'assets'} onclick={() => mgr.treeRoot = 'assets'}>Assets</button>
|
||||||
class:active={mgr.treeRoot === 'content'}
|
|
||||||
onclick={() => mgr.treeRoot = 'content'}
|
|
||||||
>Materi</button>
|
|
||||||
<button
|
|
||||||
class="tree-tab"
|
|
||||||
class:active={mgr.treeRoot === 'assets'}
|
|
||||||
onclick={() => mgr.treeRoot = 'assets'}
|
|
||||||
>Assets</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-actions-header">
|
<div class="tree-actions-header">
|
||||||
{#if mgr.treeRoot === 'content'}
|
{#if mgr.treeRoot === 'content'}
|
||||||
|
|
@ -419,34 +490,99 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Code Editor / Asset Preview -->
|
<!-- Tab Panels -->
|
||||||
<div class="code-panel">
|
<div class="tab-panels">
|
||||||
{#if mgr.activePath}
|
<!-- Editor Tab -->
|
||||||
{#if isImageFile}
|
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== 'editor'}>
|
||||||
<div class="asset-preview">
|
{#if mgr.activePath}
|
||||||
<div class="asset-preview-image">
|
{#if isImageFile}
|
||||||
<img src="/assets/{mgr.activePath}" alt={activeFileName} loading="lazy" />
|
<div class="asset-preview">
|
||||||
</div>
|
<div class="asset-preview-image">
|
||||||
<div class="asset-preview-info">
|
<img src="/assets/{mgr.activePath}" alt={activeFileName} loading="lazy" />
|
||||||
<span class="asset-name">{activeFileName}</span>
|
|
||||||
<span class="asset-path">/assets/{mgr.activePath}</span> <div class="asset-actions">
|
|
||||||
<button class="btn btn-sm btn-secondary" onclick={handleCopyAssetPath}>📋 Salin Path</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="asset-preview-info">
|
||||||
|
<span class="asset-name">{activeFileName}</span>
|
||||||
|
<span class="asset-path">/assets/{mgr.activePath}</span>
|
||||||
|
<div class="asset-actions">
|
||||||
|
<button class="panel-btn" onclick={handleCopyAssetPath}>📋 Salin Path</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="code-tab-content">
|
||||||
|
<CodeEditor
|
||||||
|
bind:this={editorRef}
|
||||||
|
code={mgr.body}
|
||||||
|
language="markdown"
|
||||||
|
onchange={handleEditorChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="tab-empty">
|
||||||
|
<p>Pilih file dari tree untuk mulai mengedit.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Tab -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== 'preview'}>
|
||||||
|
{#if mgr.previewLoading}
|
||||||
|
<div class="tab-empty">Memuat preview...</div>
|
||||||
|
{:else if mgr.previewHtml}
|
||||||
|
<div class="tab-content prose">
|
||||||
|
{@html mgr.previewHtml}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<CodeEditor
|
<div class="tab-empty">
|
||||||
bind:this={editorRef}
|
<p>Buka file materi dan edit untuk melihat preview.</p>
|
||||||
code={mgr.body}
|
</div>
|
||||||
language="markdown"
|
|
||||||
onchange={handleEditorChange}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<div class="code-empty">
|
|
||||||
<p>Pilih file dari tree untuk mulai mengedit.</p>
|
<!-- Exercise Tab -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== 'exercise'}>
|
||||||
|
{#if mgr.previewExerciseHtml}
|
||||||
|
<div class="tab-content prose">
|
||||||
|
<h2 class="tab-heading">Latihan</h2>
|
||||||
|
{@html mgr.previewExerciseHtml}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="tab-empty">
|
||||||
|
<p>Tidak ada exercise untuk file ini.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quiz Tab -->
|
||||||
|
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== 'quiz'}>
|
||||||
|
{#if mgr.previewQuizData.length > 0}
|
||||||
|
<QuizPreviewReadonly quizData={mgr.previewQuizData as any} />
|
||||||
|
{:else}
|
||||||
|
<div class="tab-empty">
|
||||||
|
<p>Tidak ada quiz untuk file ini.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic code tabs (C, Python, Circuit, etc.) -->
|
||||||
|
{#each dynamicTabs as dynTab}
|
||||||
|
<div class="tab-panel" class:tab-hidden={mgr.activeTab !== dynTab.id}>
|
||||||
|
{#if extractedCodes[dynTab.id]}
|
||||||
|
<div class="code-tab-content">
|
||||||
|
<CodeEditor
|
||||||
|
code={extractedCodes[dynTab.id]}
|
||||||
|
language={dynTab.id === 'c' ? 'c' : dynTab.id === 'python' ? 'python' : 'plaintext'}
|
||||||
|
onchange={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="tab-empty">
|
||||||
|
<p>Belum ada kode {dynTab.label} di konten ini.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -458,9 +594,10 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Resize handle (floating mode) --> {#if float.floating && !mgr.isMobile && !float.minimized}
|
<!-- Resize handle (floating mode) -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
{#if float.floating && !mgr.isMobile && !float.minimized}
|
||||||
<div class="resize-handle" onmousedown={float.onResizeStart} ontouchstart={float.onTouchResizeStart}>◳</div>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="resize-handle" onmousedown={float.onResizeStart} ontouchstart={float.onTouchResizeStart}>◳</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -575,6 +712,8 @@
|
||||||
}
|
}
|
||||||
.panel-btn:hover { background: var(--color-border); }
|
.panel-btn:hover { background: var(--color-border); }
|
||||||
.panel-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
.panel-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
|
.panel-btn.save-btn { color: var(--color-text-muted); }
|
||||||
|
.panel-btn.publish-btn { color: var(--color-primary, #339af0); font-weight: 600; }
|
||||||
.panel-title {
|
.panel-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -598,7 +737,92 @@
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Chrome-style tabs (reuse from WorkspaceHeader) ── */
|
||||||
|
.chrome-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.chrome-tabs::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.chrome-tab {
|
||||||
|
position: relative;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
z-index: 0;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.chrome-tab:hover:not(.active) {
|
||||||
|
background: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.chrome-tab.active {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Panels */
|
||||||
|
.tab-panels {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.tab-panel {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.tab-panel.tab-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.tab-heading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.tab-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code tab content fills available space */
|
||||||
|
.code-tab-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-header {
|
.editor-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -623,8 +847,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.editor-body {
|
.editor-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -701,7 +923,7 @@
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code Panel */
|
/* Code Panel (legacy, kept for reference) */
|
||||||
.code-panel {
|
.code-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -730,6 +952,20 @@
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Force CodeEditor in tab to fill parent */
|
||||||
|
.code-tab-content :global(.editor-wrapper) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.code-tab-content :global(.cm-editor) {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.code-tab-content :global(.cm-scroller) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Asset Preview */
|
/* Asset Preview */
|
||||||
.asset-preview {
|
.asset-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue