feat: playground multi-tab (Velxio/Flowchart/Circuit/Code) with auto-save
This commit is contained in:
parent
7e1139e7d1
commit
589f3a148d
|
|
@ -1,12 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import DeployFAB from '$components/DeployFAB.svelte';
|
import DeployFAB from '$components/DeployFAB.svelte';
|
||||||
|
import CircuitEditor from '$components/CircuitEditor.svelte';
|
||||||
|
import CodeEditor from '$components/CodeEditor.svelte';
|
||||||
|
import FlowchartTab from '../lesson/[slug]/FlowchartTab.svelte';
|
||||||
|
|
||||||
|
type PlaygroundTab = 'velxio' | 'flowchart' | 'circuit' | 'code';
|
||||||
|
|
||||||
|
let activeTab = $state<PlaygroundTab>('velxio');
|
||||||
|
let codeLanguage = $state<'c' | 'python'>('c');
|
||||||
|
let codeEditor = $state<CodeEditor | null>(null);
|
||||||
|
let currentCode = $state('');
|
||||||
|
|
||||||
|
// ── Velxio + DeployFAB ──
|
||||||
let velxioIframe = $state<HTMLIFrameElement | null>(null);
|
let velxioIframe = $state<HTMLIFrameElement | null>(null);
|
||||||
type DeployFABHandle = { setHex: (hex: string | null) => void };
|
type DeployFABHandle = { setHex: (hex: string | null) => void };
|
||||||
let fabComponent = $state<DeployFABHandle | null>(null);
|
let fabComponent = $state<DeployFABHandle | null>(null);
|
||||||
|
|
||||||
const iframeSrc = '/velxio/editor?embed=true&desktopLayout=true';
|
const velxioSrc = '/velxio/editor?embed=true&desktopLayout=true';
|
||||||
|
|
||||||
function handleMessage(e: MessageEvent) {
|
function handleMessage(e: MessageEvent) {
|
||||||
if (!e.data?.type) return;
|
if (!e.data?.type) return;
|
||||||
|
|
@ -32,6 +43,14 @@
|
||||||
velxioIframe?.contentWindow?.postMessage({ type: 'elemes:stop_hardware_serial' }, window.location.origin);
|
velxioIframe?.contentWindow?.postMessage({ type: 'elemes:stop_hardware_serial' }, window.location.origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Storage keys ──
|
||||||
|
const FLOWCHART_KEY = 'playground_flowchart_draft';
|
||||||
|
const CIRCUIT_KEY = 'playground_circuit_draft';
|
||||||
|
const CODE_C_KEY = 'playground_code_c_draft';
|
||||||
|
const CODE_PY_KEY = 'playground_code_python_draft';
|
||||||
|
|
||||||
|
let codeStorageKey = $derived(codeLanguage === 'python' ? CODE_PY_KEY : CODE_C_KEY);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('message', handleMessage);
|
window.addEventListener('message', handleMessage);
|
||||||
});
|
});
|
||||||
|
|
@ -42,56 +61,189 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Developer Playground — Velxio</title>
|
<title>Developer Playground — Elemes</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="playground-page">
|
<div class="playground-page">
|
||||||
<iframe
|
<!-- Tab bar -->
|
||||||
bind:this={velxioIframe}
|
<div class="pg-tabs">
|
||||||
src={iframeSrc}
|
<button class="pg-tab" class:active={activeTab === 'velxio'} onclick={() => activeTab = 'velxio'}>
|
||||||
title="Velxio Editor"
|
Arduino
|
||||||
allow="bluetooth; clipboard-read; clipboard-write; fullscreen"
|
</button>
|
||||||
class="velxio-iframe"
|
<button class="pg-tab" class:active={activeTab === 'flowchart'} onclick={() => activeTab = 'flowchart'}>
|
||||||
allowfullscreen
|
Flowchart
|
||||||
></iframe>
|
</button>
|
||||||
|
<button class="pg-tab" class:active={activeTab === 'circuit'} onclick={() => activeTab = 'circuit'}>
|
||||||
|
Circuit
|
||||||
|
</button>
|
||||||
|
<div class="pg-tab-group">
|
||||||
|
<button class="pg-tab pg-tab-sub" class:active={activeTab === 'code' && codeLanguage === 'c'} onclick={() => { activeTab = 'code'; codeLanguage = 'c'; }}>
|
||||||
|
C
|
||||||
|
</button>
|
||||||
|
<button class="pg-tab pg-tab-sub" class:active={activeTab === 'code' && codeLanguage === 'python'} onclick={() => { activeTab = 'code'; codeLanguage = 'python'; }}>
|
||||||
|
Python
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DeployFAB
|
<!-- Content area -->
|
||||||
bind:this={fabComponent}
|
<div class="pg-content">
|
||||||
{velxioIframe}
|
{#if activeTab === 'velxio'}
|
||||||
onPushSerial={pushSerialToIframe}
|
<iframe
|
||||||
onSerialStart={notifySerialStart}
|
bind:this={velxioIframe}
|
||||||
onStopSerial={notifySerialStop}
|
src={velxioSrc}
|
||||||
/>
|
title="Velxio Editor"
|
||||||
|
allow="bluetooth; clipboard-read; clipboard-write; fullscreen"
|
||||||
|
class="pg-iframe"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
{:else if activeTab === 'flowchart'}
|
||||||
|
<FlowchartTab
|
||||||
|
storageKey={FLOWCHART_KEY}
|
||||||
|
/>
|
||||||
|
{:else if activeTab === 'circuit'}
|
||||||
|
<div class="pg-circuit-wrap">
|
||||||
|
<CircuitEditor
|
||||||
|
storageKey={CIRCUIT_KEY}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if activeTab === 'code'}
|
||||||
|
<div class="pg-code-wrap">
|
||||||
|
{#key codeLanguage}
|
||||||
|
<CodeEditor
|
||||||
|
bind:this={codeEditor}
|
||||||
|
code={currentCode}
|
||||||
|
language={codeLanguage}
|
||||||
|
storageKey={codeStorageKey}
|
||||||
|
onchange={(val) => currentCode = val}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DeployFAB only on Velxio tab -->
|
||||||
|
{#if activeTab === 'velxio'}
|
||||||
|
<DeployFAB
|
||||||
|
bind:this={fabComponent}
|
||||||
|
{velxioIframe}
|
||||||
|
onPushSerial={pushSerialToIframe}
|
||||||
|
onSerialStart={notifySerialStart}
|
||||||
|
onStopSerial={notifySerialStop}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.playground-page {
|
.playground-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.velxio-iframe {
|
/* ── Tab bar ── */
|
||||||
width: 100%;
|
.pg-tabs {
|
||||||
flex: 1;
|
display: flex;
|
||||||
min-height: 0; /* critical: allows flex child to shrink below content size */
|
align-items: flex-end;
|
||||||
border: none;
|
gap: 2px;
|
||||||
display: block;
|
padding: 0 8px;
|
||||||
touch-action: pan-y; /* allow vertical pan inside iframe */
|
background: var(--color-bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
min-height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pg-tab {
|
||||||
|
padding: 5px 16px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-tab:hover:not(.active) {
|
||||||
|
background: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-tab.active {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-tab-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-tab-sub {
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Content area ── */
|
||||||
|
.pg-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-iframe {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-circuit-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pg-code-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FlowchartTab fill ── */
|
||||||
|
.pg-content :global(.flowchart-container) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.playground-page {
|
.playground-page {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: auto; /* allow page to grow */
|
|
||||||
min-height: 100dvh; /* but always at least fill viewport */
|
|
||||||
}
|
|
||||||
.velxio-iframe {
|
|
||||||
max-height: none; /* iframe can grow taller than viewport */
|
|
||||||
height: auto;
|
height: auto;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
.pg-content {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
.pg-iframe {
|
||||||
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue