diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index 3298a7d..b717305 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -1,5 +1,6 @@
-
+{#if !isFullViewport}
+
+{/if}
-
+
{@render children()}
-
+{#if !isFullViewport}
+
+{/if}
diff --git a/frontend/src/routes/lesson/[slug]/VelxioTab.svelte b/frontend/src/routes/lesson/[slug]/VelxioTab.svelte
index 344ec84..1c462f3 100644
--- a/frontend/src/routes/lesson/[slug]/VelxioTab.svelte
+++ b/frontend/src/routes/lesson/[slug]/VelxioTab.svelte
@@ -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
>
{/if}
diff --git a/frontend/src/routes/lesson/[slug]/lesson.css b/frontend/src/routes/lesson/[slug]/lesson.css
index ab02e68..dc2b9e3 100644
--- a/frontend/src/routes/lesson/[slug]/lesson.css
+++ b/frontend/src/routes/lesson/[slug]/lesson.css
@@ -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;
diff --git a/frontend/src/routes/playground/+page.svelte b/frontend/src/routes/playground/+page.svelte
new file mode 100644
index 0000000..c4e5186
--- /dev/null
+++ b/frontend/src/routes/playground/+page.svelte
@@ -0,0 +1,97 @@
+
+
+
+ Developer Playground — Velxio
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/playground/+page.ts b/frontend/src/routes/playground/+page.ts
new file mode 100644
index 0000000..707dc65
--- /dev/null
+++ b/frontend/src/routes/playground/+page.ts
@@ -0,0 +1,3 @@
+// Disable SSR — Web Bluetooth & iframe PostMessage need client-side
+export const prerender = false;
+export const ssr = false;
diff --git a/frontend/static/sw.js b/frontend/src/service-worker.ts
similarity index 86%
rename from frontend/static/sw.js
rename to frontend/src/service-worker.ts
index b9021d5..eccdb3a 100644
--- a/frontend/static/sw.js
+++ b/frontend/src/service-worker.ts
@@ -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 {
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 {
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 {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkFetch = fetch(request).then(response => {