feat(web): PWA-Setup über @mana/shared-pwa (Adoption #9)
Some checks are pending
CI / validate (push) Waiting to run

createOfflineFirstPWAConfig — schließt sql-wasm.wasm aus dem Precache
(Wordeck nutzt sql.js für lokales Spaced-Repetition). themeBridge('forest')
#117e39 passend zum data-theme='forest'. 2 Shortcuts (Decks + Entdecken).
OfflineBanner/UpdatePrompt/InstallPrompt unter skip-link. Icons aus
wordeck-native AppIcon-1024.

Build grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-19 00:44:38 +02:00
parent b13c9dd914
commit 633f051f2d
12 changed files with 3479 additions and 56 deletions

View file

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Web \u2014 SvelteKit 2 + Svelte 5 Frontend f\u00fcr cardecky.mana.how.",
"description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.",
"scripts": {
"dev": "vite dev --port 3082 --host",
"build": "vite build",
@ -15,14 +15,17 @@
"clean": "rm -rf .svelte-kit build .turbo"
},
"dependencies": {
"@mana/shared-icons": "^1.0.0",
"@mana/shared-pwa": "0.1.0-alpha.3",
"@mana/shared-ui-2": "^0.1.0",
"@mana/themes": "^0.1.0",
"@vite-pwa/sveltekit": "^1.1.0",
"@wordeck/domain": "workspace:*",
"dompurify": "^3.4.2",
"jszip": "^3.10.1",
"marked": "^18.0.3",
"sql.js": "^1.14.1",
"@mana/themes": "^0.1.0",
"@mana/shared-ui-2": "^0.1.0",
"@mana/shared-icons": "^1.0.0"
"workbox-window": "^7.4.1"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
@ -32,6 +35,7 @@
"@types/dompurify": "^3.2.0",
"@types/jszip": "^3.4.1",
"@types/sql.js": "^1.4.11",
"sharp": "^0.34.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.2.4",

View file

@ -1,5 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
/// <reference types="vite-plugin-pwa/client" />
declare global {
namespace App {
// interface Error {}

View file

@ -2,8 +2,11 @@
<html lang="de" data-theme="forest">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="description" content="Wordeck — Spaced Repetition, text-first. Lernkarten-App des Vereins mana e.V." />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#117e39" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<title>Wordeck</title>
%sveltekit.head%
</head>

View file

@ -1,12 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
import '../app.css';
import Header from '$lib/components/Header.svelte';
import ToastStack from '$lib/components/ToastStack.svelte';
import { i18n, t } from '$lib/i18n/index.svelte.ts';
import { page } from '$app/state';
import {
InstallPrompt,
UpdatePrompt,
OfflineBanner,
pwaState,
} from '@mana/shared-pwa/components';
let { children } = $props();
onMount(async () => {
try {
// @ts-expect-error virtual module from vite-plugin-pwa
const { registerSW } = await import('virtual:pwa-register');
const updateSW = registerSW({
onNeedRefresh() {
pwaState.registerUpdateHandler(updateSW);
},
});
} catch {
// Dev oder Build ohne PWA-Plugin → ignorieren.
}
});
// Hält das `lang`-Attribut am <html> aktuell. Initial-SSR rendert
// "de"; sobald i18n-Detection im Browser läuft, ziehen wir nach.
$effect(() => {
@ -28,6 +49,10 @@
<a href="#main" class="skip-link">{t('common.skip_to_content')}</a>
<OfflineBanner />
<UpdatePrompt />
<InstallPrompt />
{#if !isFocusMode && !isLoginPage}
<Header />
{/if}

View file

@ -0,0 +1,31 @@
<script lang="ts">
function goBack() {
if (typeof history !== 'undefined' && history.length > 1) {
history.back();
} else {
location.href = '/';
}
}
</script>
<svelte:head>
<title>Offline · Wordeck</title>
</svelte:head>
<section
class="mx-auto mt-12 max-w-md rounded-lg border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] p-6 text-center"
>
<h1 class="text-xl font-semibold">Du bist offline</h1>
<p class="mt-2 text-sm text-[hsl(var(--color-muted-foreground))]">
Schon geladene Decks und Karten sind im Cache — Spaced-Repetition läuft
auch ohne Netz weiter (Wordeck ist text-only by design). Sync mit dem
Server passiert, sobald wieder Netz da ist.
</p>
<button
type="button"
class="mt-4 rounded-md bg-[hsl(var(--color-primary))] px-4 py-2 text-sm font-medium text-[hsl(var(--color-primary-foreground))]"
onclick={goBack}
>
Zurück
</button>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -7,6 +7,7 @@
"sourceMap": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true
"noEmit": true,
"skipLibCheck": true
}
}

View file

@ -1,9 +1,29 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
import { createOfflineFirstPWAConfig, themeBridge } from '@mana/shared-pwa';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
plugins: [
tailwindcss(),
sveltekit(),
SvelteKitPWA(
// Wordeck nutzt sql.js (WASM) für lokale Spaced-Repetition —
// createOfflineFirstPWAConfig excludet WASM-Blobs vom Precache,
// damit sie nicht das 8-MiB-Cache-Cap pro Datei sprengen.
createOfflineFirstPWAConfig({
name: 'Wordeck',
shortName: 'Wordeck',
description: 'Spaced Repetition, text-first. Lernkarten-App des Vereins mana e.V.',
...themeBridge('forest'),
shortcuts: [
{ name: 'Meine Decks', short_name: 'Decks', url: '/' },
{ name: 'Entdecken', short_name: 'Entdecken', url: '/explore' },
],
})
),
],
server: {
port: Number(process.env.WORDECK_WEB_PORT ?? 3082),
host: true,

3435
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff