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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { auth } from '$stores/auth';
|
import { auth } from '$stores/auth';
|
||||||
import { theme } from '$stores/theme';
|
import { theme } from '$stores/theme';
|
||||||
import Navbar from '$components/Navbar.svelte';
|
import Navbar from '$components/Navbar.svelte';
|
||||||
|
|
@ -12,12 +13,19 @@
|
||||||
theme.init();
|
theme.init();
|
||||||
auth.init();
|
auth.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Suppress Navbar/Footer on full-viewport routes (e.g. /playground)
|
||||||
|
let isFullViewport = $derived($page.url.pathname === '/playground');
|
||||||
</script>
|
</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()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
{#if !isFullViewport}
|
||||||
|
<Footer />
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
class="velxio-iframe"
|
class="velxio-iframe"
|
||||||
src="/velxio/editor?embed=true{hasArduinoCode ? '' : '&hideEditor=true'}&lockComponents=true"
|
src="/velxio/editor?embed=true{hasArduinoCode ? '' : '&hideEditor=true'}&lockComponents=true"
|
||||||
onload={(e) => onSetupBridge(e.currentTarget as HTMLIFrameElement)}
|
onload={(e) => onSetupBridge(e.currentTarget as HTMLIFrameElement)}
|
||||||
allow="cross-origin-isolated"
|
allow="cross-origin-isolated; fullscreen"
|
||||||
|
allowfullscreen
|
||||||
></iframe>
|
></iframe>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,8 @@
|
||||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.2s ease;
|
||||||
}
|
}
|
||||||
.editor-area.mobile-hidden {
|
.editor-area.mobile-hidden {
|
||||||
|
|
@ -244,6 +245,10 @@
|
||||||
.editor-area.mobile-full .cm-scroller {
|
.editor-area.mobile-full .cm-scroller {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.editor-area.mobile-full .velxio-iframe {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Tab panels ────────────────────────────────────────── */
|
/* ── Tab panels ────────────────────────────────────────── */
|
||||||
.tab-panel {
|
.tab-panel {
|
||||||
|
|
@ -300,9 +305,25 @@
|
||||||
.velxio-iframe {
|
.velxio-iframe {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 400px;
|
min-height: 0; /* CHANGED: was 400px — allow shrink in flex */
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius);
|
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 {
|
.velxio-fallback {
|
||||||
padding: 2rem;
|
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
|
// src/service-worker.ts — SvelteKit Service Worker
|
||||||
const CACHE_VERSION = 'elemes-v14';
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
|
const CACHE_VERSION = `elemes-v${APP_VERSION}`;
|
||||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||||
const API_CACHE = `${CACHE_VERSION}-api`;
|
const API_CACHE = `${CACHE_VERSION}-api`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
@ -75,9 +77,9 @@ self.addEventListener('fetch', (event) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaleWhileRevalidate: HTML pages
|
// NetworkFirst: HTML pages (new routes need network first)
|
||||||
if (request.headers.get('accept')?.includes('text/html')) {
|
if (request.headers.get('accept')?.includes('text/html')) {
|
||||||
event.respondWith(staleWhileRevalidate(request, STATIC_CACHE));
|
event.respondWith(networkFirst(request, STATIC_CACHE, 1440));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +92,7 @@ self.addEventListener('fetch', (event) => {
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────
|
// ── 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 cache = await caches.open(cacheName);
|
||||||
const cached = await cache.match(request);
|
const cached = await cache.match(request);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
@ -99,7 +101,7 @@ async function cacheFirst(request, cacheName, maxAgeDays = 7) {
|
||||||
return response;
|
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);
|
const cache = await caches.open(cacheName);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(request);
|
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 cache = await caches.open(cacheName);
|
||||||
const cached = await cache.match(request);
|
const cached = await cache.match(request);
|
||||||
const networkFetch = fetch(request).then(response => {
|
const networkFetch = fetch(request).then(response => {
|
||||||
Loading…
Reference in New Issue