feat(frontend): Velxio iframe fullscreen + playground route + SvelteKit service worker
- VelxioTab: add allowfullscreen to Velxio editor iframe - lesson.css: flex/shrink fixes for .velxio-iframe on mobile - +layout.svelte: hide Navbar/Footer on /playground full-viewport route - Add /playground route: Velxio editor iframe + DeployFAB bridge - Migrate static sw.js to SvelteKit module service-worker.ts
This commit is contained in:
parent
d6bb2073ac
commit
0cbac64c6f
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { auth } from '$stores/auth';
|
||||
import { theme } from '$stores/theme';
|
||||
import Navbar from '$components/Navbar.svelte';
|
||||
|
|
@ -12,12 +13,19 @@
|
|||
theme.init();
|
||||
auth.init();
|
||||
});
|
||||
|
||||
// Suppress Navbar/Footer on full-viewport routes (e.g. /playground)
|
||||
let isFullViewport = $derived($page.url.pathname === '/playground');
|
||||
</script>
|
||||
|
||||
<Navbar />
|
||||
{#if !isFullViewport}
|
||||
<Navbar />
|
||||
{/if}
|
||||
|
||||
<main class="container" style="flex: 1; padding-block: 1.5rem;">
|
||||
<main class="container" style="flex: 1; padding-block: {isFullViewport ? '0' : '1.5rem'}; overflow: {isFullViewport ? 'hidden' : 'visible'};">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
{#if !isFullViewport}
|
||||
<Footer />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
class="velxio-iframe"
|
||||
src="/velxio/editor?embed=true{hasArduinoCode ? '' : '&hideEditor=true'}&lockComponents=true"
|
||||
onload={(e) => onSetupBridge(e.currentTarget as HTMLIFrameElement)}
|
||||
allow="cross-origin-isolated"
|
||||
allow="cross-origin-isolated; fullscreen"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -204,7 +204,8 @@
|
|||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.2s ease;
|
||||
}
|
||||
.editor-area.mobile-hidden {
|
||||
|
|
@ -244,6 +245,10 @@
|
|||
.editor-area.mobile-full .cm-scroller {
|
||||
flex: 1;
|
||||
}
|
||||
.editor-area.mobile-full .velxio-iframe {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Tab panels ────────────────────────────────────────── */
|
||||
.tab-panel {
|
||||
|
|
@ -300,9 +305,25 @@
|
|||
.velxio-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
min-height: 0; /* CHANGED: was 400px — allow shrink in flex */
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
touch-action: pan-y; /* allow vertical pan inside iframe */
|
||||
overscroll-behavior: contain; /* prevent overscroll bounce */
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.velxio-iframe {
|
||||
min-height: 0; /* CHANGED: was 200px — let flex sizing work fully */
|
||||
}
|
||||
/* Ensure sheet never exceeds visual viewport when keyboard open */
|
||||
.editor-area.mobile-sheet {
|
||||
max-height: 100vh; /* fallback for browsers without dvh */
|
||||
max-height: 100dvh; /* dynamic viewport height */
|
||||
}
|
||||
.editor-area.mobile-full {
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
}
|
||||
}
|
||||
.velxio-fallback {
|
||||
padding: 2rem;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import DeployFAB from '$components/DeployFAB.svelte';
|
||||
|
||||
let velxioIframe = $state<HTMLIFrameElement | null>(null);
|
||||
type DeployFABHandle = { setHex: (hex: string | null) => void };
|
||||
let fabComponent = $state<DeployFABHandle | null>(null);
|
||||
|
||||
const iframeSrc = '/velxio/editor?embed=true&desktopLayout=true';
|
||||
|
||||
function handleMessage(e: MessageEvent) {
|
||||
if (!e.data?.type) return;
|
||||
if (e.data.type === 'velxio:hex_ready') {
|
||||
fabComponent?.setHex(e.data.hex);
|
||||
}
|
||||
}
|
||||
|
||||
function pushSerialToIframe(text: string) {
|
||||
if (!velxioIframe?.contentWindow) return;
|
||||
velxioIframe.contentWindow.postMessage({
|
||||
type: 'elemes:push_serial',
|
||||
text,
|
||||
boardId: null
|
||||
}, window.location.origin);
|
||||
}
|
||||
|
||||
function notifySerialStart() {
|
||||
velxioIframe?.contentWindow?.postMessage({ type: 'elemes:start_hardware_serial' }, window.location.origin);
|
||||
}
|
||||
|
||||
function notifySerialStop() {
|
||||
velxioIframe?.contentWindow?.postMessage({ type: 'elemes:stop_hardware_serial' }, window.location.origin);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Developer Playground — Velxio</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="playground-page">
|
||||
<iframe
|
||||
bind:this={velxioIframe}
|
||||
src={iframeSrc}
|
||||
title="Velxio Editor"
|
||||
allow="bluetooth; clipboard-read; clipboard-write; fullscreen"
|
||||
class="velxio-iframe"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
<DeployFAB
|
||||
bind:this={fabComponent}
|
||||
{velxioIframe}
|
||||
onPushSerial={pushSerialToIframe}
|
||||
onSerialStart={notifySerialStart}
|
||||
onStopSerial={notifySerialStop}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.playground-page {
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.velxio-iframe {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0; /* critical: allows flex child to shrink below content size */
|
||||
border: none;
|
||||
display: block;
|
||||
touch-action: pan-y; /* allow vertical pan inside iframe */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.playground-page {
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// Disable SSR — Web Bluetooth & iframe PostMessage need client-side
|
||||
export const prerender = false;
|
||||
export const ssr = false;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
// static/sw.js
|
||||
const CACHE_VERSION = 'elemes-v14';
|
||||
// src/service-worker.ts — SvelteKit Service Worker
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
const CACHE_VERSION = `elemes-v${APP_VERSION}`;
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const API_CACHE = `${CACHE_VERSION}-api`;
|
||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||
|
|
@ -75,9 +77,9 @@ self.addEventListener('fetch', (event) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// StaleWhileRevalidate: HTML pages
|
||||
// NetworkFirst: HTML pages (new routes need network first)
|
||||
if (request.headers.get('accept')?.includes('text/html')) {
|
||||
event.respondWith(staleWhileRevalidate(request, STATIC_CACHE));
|
||||
event.respondWith(networkFirst(request, STATIC_CACHE, 1440));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ self.addEventListener('fetch', (event) => {
|
|||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
async function cacheFirst(request, cacheName, maxAgeDays = 7) {
|
||||
async function cacheFirst(request: Request, cacheName: string, maxAgeDays = 7): Promise<Response> {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
|
|
@ -99,7 +101,7 @@ async function cacheFirst(request, cacheName, maxAgeDays = 7) {
|
|||
return response;
|
||||
}
|
||||
|
||||
async function networkFirst(request, cacheName, maxAgeMinutes = 60) {
|
||||
async function networkFirst(request: Request, cacheName: string, maxAgeMinutes = 60): Promise<Response> {
|
||||
const cache = await caches.open(cacheName);
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
|
|
@ -115,7 +117,7 @@ async function networkFirst(request, cacheName, maxAgeMinutes = 60) {
|
|||
}
|
||||
}
|
||||
|
||||
async function staleWhileRevalidate(request, cacheName) {
|
||||
async function staleWhileRevalidate(request: Request, cacheName: string): Promise<Response> {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
const networkFetch = fetch(request).then(response => {
|
||||
Loading…
Reference in New Issue