ui: refactor navbar and improve mobile experience
- Move navigation menu to the leftmost position and consolidate actions into a dropdown. - Replace clunky mobile sheet handle with explicit minimize/maximize buttons. - Fix home page content margins for list elements. - Ensure student identity remains visible on mobile devices with optimized font sizing. - Synchronize CircuitJS simulator theme with global light/dark mode. - Update elemes.sh to provide cleaner output by silencing podman-compose logs by default.master
parent
dc1d65ac15
commit
dce4916b94
56
elemes.sh
56
elemes.sh
|
|
@ -5,6 +5,26 @@ PARENT_DIR="$(dirname "$SCRIPT_DIR")"
|
|||
EXAMPLES_DIR="$SCRIPT_DIR/examples"
|
||||
PROJECT_NAME="$(basename "$PARENT_DIR")"
|
||||
|
||||
# Check for --verbose flag
|
||||
VERBOSE=0
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" == "--verbose" ]; then
|
||||
VERBOSE=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Function to run podman-compose quietly unless verbose
|
||||
run_compose() {
|
||||
# Ensure we are in the script directory so podman-compose finds the yaml file
|
||||
cd "$SCRIPT_DIR" || exit
|
||||
if [ "$VERBOSE" -eq 1 ]; then
|
||||
podman-compose -p "$PROJECT_NAME" --env-file "$PARENT_DIR/.env" "$@"
|
||||
else
|
||||
podman-compose -p "$PROJECT_NAME" --env-file "$PARENT_DIR/.env" "$@" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
init)
|
||||
echo "✨ === Elemes Quick Start === ✨"
|
||||
|
|
@ -61,7 +81,7 @@ init)
|
|||
;;
|
||||
stop | run | runbuild | runclearbuild)
|
||||
echo "🛑 Menghentikan container yang sedang berjalan..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env down
|
||||
run_compose down
|
||||
;;&
|
||||
stop)
|
||||
echo "✅ Container berhasil dihentikan."
|
||||
|
|
@ -70,20 +90,20 @@ runclearbuild)
|
|||
echo "🧹 Membersihkan container dan image (prune)..."
|
||||
podman image prune -f
|
||||
echo "🏗️ Membangun ulang container dari awal (no-cache)..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env build --no-cache
|
||||
run_compose build --no-cache
|
||||
;;&
|
||||
runbuild)
|
||||
echo "🏗️ Membangun container..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env build
|
||||
run_compose build
|
||||
;;&
|
||||
runbuild | runclearbuild)
|
||||
echo "🚀 Menjalankan container di background..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env up --force-recreate -d
|
||||
run_compose up --force-recreate -d
|
||||
echo "✅ Elemes berhasil dijalankan!"
|
||||
;;
|
||||
run)
|
||||
echo "🚀 Menjalankan container..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env up
|
||||
run_compose up -d
|
||||
echo "✅ Elemes berhasil dijalankan!"
|
||||
;;
|
||||
generatetoken)
|
||||
|
|
@ -93,6 +113,7 @@ generatetoken)
|
|||
exportall)
|
||||
echo "📦 === Mengekspor Semua Image (Pre-Compiled Bundle) ==="
|
||||
TAR_FILE="lms-c-precompiled.tar"
|
||||
cd "$SCRIPT_DIR" || exit
|
||||
|
||||
echo "🏗️ 1. Mem-build Backend Elemes..."
|
||||
podman build -t lms-c-backend:latest -f Dockerfile .
|
||||
|
|
@ -179,31 +200,6 @@ verify)
|
|||
fi
|
||||
done
|
||||
;;
|
||||
loadtest)
|
||||
echo "📊 === Elemes Load Testing ==="
|
||||
cd "$SCRIPT_DIR/load-test" || exit
|
||||
|
||||
if [ ! -d "env" ]; then
|
||||
echo "⚙️ Membuat Python Virtual Environment (env/)..."
|
||||
python3 -m venv env
|
||||
fi
|
||||
|
||||
echo "⚙️ Mengaktifkan environment & menginstall requirements..."
|
||||
source env/bin/activate
|
||||
pip install -r requirements.txt >/dev/null 2>&1
|
||||
|
||||
echo "⚙️ Mempersiapkan Test Data & menginjeksi akun Bot..."
|
||||
python3 content_parser.py --num-tokens 50
|
||||
|
||||
echo ""
|
||||
echo "🚀 Memulai Locust Test Suite..."
|
||||
echo "👉 Buka http://localhost:8089 di browser web milikmu."
|
||||
echo "👉 Masukkan URL backend Elemes sebagai Host (contoh: http://localhost:5000)"
|
||||
echo "👉 Tekan CTRL+C di terminal ini untuk menghentikan test."
|
||||
echo ""
|
||||
|
||||
locust -f locustfile.py
|
||||
;;
|
||||
*)
|
||||
echo "💡 Cara Penggunaan elemes.sh:"
|
||||
echo " ./elemes.sh init # Inisialisasi konfigurasi, folder, & data tokens"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ function loadCircuitEmbed(div: HTMLElement) {
|
|||
wrapper.style.width = width;
|
||||
wrapper.style.height = height;
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = '/circuitjs1/circuitjs.html';
|
||||
iframe.src = `/circuitjs1/circuitjs.html?whiteBackground=${!isDark}`;
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.border = 'none';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import CrosshairOverlay from './CrosshairOverlay.svelte';
|
||||
import type { CircuitJSApi } from '$lib/types/circuitjs';
|
||||
import { themeDark } from '$stores/theme';
|
||||
|
||||
interface Props {
|
||||
initialCircuit?: string;
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
let saveTimeout: ReturnType<typeof setTimeout>;
|
||||
let autoSaveInterval: ReturnType<typeof setInterval>;
|
||||
let lastLoadedCircuit = $state('');
|
||||
let lastStorageKey = $state<string | undefined>(undefined);
|
||||
|
||||
function saveToStorage(text: string) {
|
||||
if (!storageKey) return;
|
||||
|
|
@ -152,6 +154,8 @@
|
|||
export function getApi(): CircuitJSApi | null {
|
||||
return simApi;
|
||||
}
|
||||
|
||||
let iframeSrc = $derived(`/circuitjs1/circuitjs.html?whiteBackground=${!$themeDark}`);
|
||||
</script>
|
||||
|
||||
<div class="circuit-container">
|
||||
|
|
@ -162,7 +166,7 @@
|
|||
|
||||
<iframe
|
||||
bind:this={iframe}
|
||||
src="/circuitjs1/circuitjs.html"
|
||||
src={iframeSrc}
|
||||
title="Circuit Simulator"
|
||||
onload={handleIframeLoad}
|
||||
class:visible={ready}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import ProgressBadge from '$components/ProgressBadge.svelte';
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
||||
let showDropdown = $state(false);
|
||||
let showLoginModal = $state(false);
|
||||
let tokenInput = $state('');
|
||||
let loginError = $state('');
|
||||
|
|
@ -31,47 +32,72 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={() => (showDropdown = false)} />
|
||||
|
||||
<nav class="navbar" onclickcapture={() => auth.recordActivity()}>
|
||||
<div class="container navbar-inner">
|
||||
{#if $lessonContext}
|
||||
<!-- Lesson mode -->
|
||||
<div class="navbar-left">
|
||||
<div class="navbar-left">
|
||||
<div class="nav-dropdown">
|
||||
<button type="button" class="btn-icon-sm dropdown-toggle" onclick={(e) => { e.stopPropagation(); showDropdown = !showDropdown; }} title="Menu">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="18" x2="20" y2="18"></line></svg>
|
||||
</button>
|
||||
{#if showDropdown}
|
||||
<div class="dropdown-menu" onclick={(e) => e.stopPropagation()}>
|
||||
<button type="button" class="dropdown-item" onclick={() => { theme.toggle(); showDropdown = false; }}>
|
||||
{#if $themeDark}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
||||
Tema Terang
|
||||
{:else}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||
Tema Gelap
|
||||
{/if}
|
||||
</button>
|
||||
<a href="/help" target="_blank" class="dropdown-item" onclick={() => (showDropdown = false)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
Bantuan
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{#if $authLoggedIn}
|
||||
<button type="button" class="dropdown-item" onclick={() => { auth.logout(); showDropdown = false; }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||
Keluar
|
||||
</button>
|
||||
{:else}
|
||||
<button type="button" class="dropdown-item" onclick={() => { (showLoginModal = true); (showDropdown = false); }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
|
||||
Masuk
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $lessonContext}
|
||||
<a href="/" class="nav-home-btn" title="Semua Pelajaran">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
</a>
|
||||
{:else}
|
||||
<a href="/" class="navbar-brand">{env.PUBLIC_APP_BAR_TITLE || 'Elemes LMS'}</a>
|
||||
{/if}
|
||||
|
||||
{#if $lessonContext}
|
||||
<h1 class="navbar-lesson-title">{$lessonContext.title}</h1>
|
||||
{#if $lessonContext.completed && $authLoggedIn}
|
||||
<ProgressBadge completed={true} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Home mode -->
|
||||
<a href="/" class="navbar-brand">{env.PUBLIC_APP_BAR_TITLE || 'Elemes LMS'}</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="navbar-actions">
|
||||
{#if $authLoggedIn}
|
||||
<span class="user-label">{$authStudentName}</span>
|
||||
{/if}
|
||||
|
||||
{#if $lessonContext?.nextLesson}
|
||||
<a href="/lesson/{$lessonContext.nextLesson.filename}" class="btn btn-nav-next" title="{$lessonContext.nextLesson.title}">
|
||||
{$lessonContext.nextLesson.title} ›
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<button class="btn-icon-sm" onclick={() => theme.toggle()} title="Toggle tema">
|
||||
{$themeDark ? '\u2600\uFE0F' : '\uD83C\uDF19'}
|
||||
</button>
|
||||
|
||||
<a href="/help" target="_blank" class="nav-help-link" title="Panduan Penggunaan">
|
||||
Bantuan
|
||||
</a>
|
||||
|
||||
{#if $authLoggedIn}
|
||||
<span class="user-label">{$authStudentName}</span>
|
||||
<button class="btn btn-danger btn-xs" onclick={() => auth.logout()}>Keluar</button>
|
||||
{:else}
|
||||
<button class="btn btn-primary btn-xs" onclick={() => (showLoginModal = true)}>
|
||||
Masuk
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -163,6 +189,63 @@
|
|||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* ── Dropdown ──────────────────────────────────── */
|
||||
.nav-dropdown {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
transition: color 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.dropdown-toggle:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 200;
|
||||
}
|
||||
.dropdown-item {
|
||||
padding: 0.6rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* ── Right section ─────────────────────────────── */
|
||||
.navbar-actions {
|
||||
display: flex;
|
||||
|
|
@ -193,6 +276,10 @@
|
|||
.user-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
}
|
||||
.btn-icon-sm {
|
||||
background: none;
|
||||
|
|
@ -266,16 +353,21 @@
|
|||
|
||||
/* ── Mobile ─────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-inner {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.navbar-lesson-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.user-label {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
max-width: 80px;
|
||||
}
|
||||
.btn-nav-next {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
max-width: 100px;
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -44,12 +44,6 @@
|
|||
const hasMultiLang = $derived(hasC && hasPython);
|
||||
const hasCircuit = $derived(activeTabs?.includes('circuit') ?? false);
|
||||
|
||||
function cycleMobileSheet() {
|
||||
if (mobileMode === 'hidden') mobileMode = 'half';
|
||||
else if (mobileMode === 'half') mobileMode = 'full';
|
||||
else mobileMode = 'hidden';
|
||||
}
|
||||
|
||||
function onSheetTouchStart(e: TouchEvent) {
|
||||
touchStartY = e.touches[0].clientY;
|
||||
}
|
||||
|
|
@ -94,15 +88,16 @@
|
|||
|
||||
{#if isMobile}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="panel-header sheet-handle"
|
||||
<div class="panel-header"
|
||||
ontouchstart={onSheetTouchStart}
|
||||
ontouchend={onSheetTouchEnd}>
|
||||
<button type="button" class="sheet-handle-bar-btn" onclick={cycleMobileSheet} aria-label="Resize panel">
|
||||
<div class="sheet-handle-bar"></div>
|
||||
</button>
|
||||
<div class="chrome-tabs">
|
||||
{@render chromeTabs()}
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<button type="button" class="panel-btn" onclick={(e) => { e.stopPropagation(); if (mobileMode === 'full') mobileMode = 'half'; else if (mobileMode === 'half') mobileMode = 'hidden'; }} title="Minimize" disabled={mobileMode === 'hidden'}>▽</button>
|
||||
<button type="button" class="panel-btn" onclick={(e) => { e.stopPropagation(); if (mobileMode === 'hidden') mobileMode = 'half'; else if (mobileMode === 'half') mobileMode = 'full'; }} title="Maximize" disabled={mobileMode === 'full'}>△</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if floating && !minimized}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
|
@ -232,31 +227,4 @@
|
|||
border-color: var(--color-border);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Mobile sheet handle ────────────────────────────── */
|
||||
.sheet-handle {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border: none;
|
||||
border-bottom: none;
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
padding: 4px 8px 0;
|
||||
}
|
||||
.sheet-handle-bar-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sheet-handle-bar {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: var(--color-border);
|
||||
border-radius: 2px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,14 @@
|
|||
.home-intro {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.home-intro :global(ul),
|
||||
.home-intro :global(ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.home-intro :global(li) {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.lesson-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
|
|
|
|||
Loading…
Reference in New Issue