pwa / offline mode

- @vite-pwa/sveltekit: workbox precache (incl. pdfjs worker), generated manifest, prompt-style updates via PwaUpdatePrompt toast
- prerendered routes for offline navigation, PWA icons, theme-color + apple-touch-icon
- pwa offline/update e2e suite (pnpm test:pwa, playwright.pwa.config.ts)
- datenschutz: offline/install section; prettier cleanup of shadcn components
This commit is contained in:
rehlert committed 2026-10-04 12:55:46 +02:00
1 parent b7969b4c9e
commit f09384d2e6
37 files changed
+4130 -273

No files matched your search

+105 -99
View File
@@ -1,129 +1,135 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount } from 'svelte';
import type {ZipAttachment} from "$lib/zip-processing";
import type { ZipAttachment } from '$lib/zip-processing';
type Props = {
attachment: ZipAttachment;
class?: string;
};
type Props = {
attachment: ZipAttachment;
class?: string;
};
let { attachment, class: className = "" }: Props = $props();
let { attachment, class: className = '' }: Props = $props();
let imageUrl = $state<string | null>(null);
let previewError = $state<string | null>(null);
let imageUrl = $state<string | null>(null);
let previewError = $state<string | null>(null);
onMount(() => {
let objectUrl: string | null = null;
let cancelled = false;
onMount(() => {
let objectUrl: string | null = null;
let cancelled = false;
const revokeObjectUrl = () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
objectUrl = null;
}
};
const revokeObjectUrl = () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
objectUrl = null;
}
};
const setBlobImage = (blob: Blob) => {
revokeObjectUrl();
objectUrl = URL.createObjectURL(blob);
imageUrl = objectUrl;
};
const setBlobImage = (blob: Blob) => {
revokeObjectUrl();
objectUrl = URL.createObjectURL(blob);
imageUrl = objectUrl;
};
(async () => {
const pdfjsLib = await import("pdfjs-dist");
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
(async () => {
const pdfjsLib = await import('pdfjs-dist');
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
GlobalWorkerOptions.workerSrc = await import("pdfjs-dist/build/pdf.worker.mjs?url").then((m) => m.default);
GlobalWorkerOptions.workerSrc = await import('pdfjs-dist/build/pdf.worker.mjs?url').then(
(m) => m.default
);
const loadPreview = async () => {
previewError = null;
imageUrl = null;
const loadPreview = async () => {
previewError = null;
imageUrl = null;
if (attachment.kind === "image") {
const imageBuffer = attachment.data.buffer.slice(
attachment.data.byteOffset,
attachment.data.byteOffset + attachment.data.byteLength,
) as ArrayBuffer;
setBlobImage(new Blob([imageBuffer]));
return;
}
if (attachment.kind === 'image') {
const imageBuffer = attachment.data.buffer.slice(
attachment.data.byteOffset,
attachment.data.byteOffset + attachment.data.byteLength
) as ArrayBuffer;
setBlobImage(new Blob([imageBuffer]));
return;
}
try {
// create a copy, getDocument seems to manipulate the data -> creating pdfs fails
const buffer = new Uint8Array(attachment.data);
const loadingTask = getDocument({ data: buffer });
const pdfDocument = await loadingTask.promise;
const page = await pdfDocument.getPage(1);
try {
// create a copy, getDocument seems to manipulate the data -> creating pdfs fails
const buffer = new Uint8Array(attachment.data);
const loadingTask = getDocument({ data: buffer });
const pdfDocument = await loadingTask.promise;
const page = await pdfDocument.getPage(1);
if (cancelled) {
void loadingTask.destroy();
return;
}
if (cancelled) {
void loadingTask.destroy();
return;
}
const viewport = page.getViewport({ scale: 1 });
const canvasElement = document.createElement("canvas");
const viewport = page.getViewport({ scale: 1 });
const canvasElement = document.createElement('canvas');
canvasElement.width = viewport.width;
canvasElement.height = viewport.height;
canvasElement.width = viewport.width;
canvasElement.height = viewport.height;
const canvasContext = canvasElement.getContext("2d");
const canvasContext = canvasElement.getContext('2d');
if (!canvasContext) {
throw new Error("Could not create thumbnail canvas context.");
}
if (!canvasContext) {
throw new Error('Could not create thumbnail canvas context.');
}
await page.render({ canvas: canvasElement, canvasContext, viewport }).promise;
await page.render({ canvas: canvasElement, canvasContext, viewport }).promise;
if (cancelled) {
void loadingTask.destroy();
return;
}
if (cancelled) {
void loadingTask.destroy();
return;
}
const blob = await new Promise<Blob>((resolve, reject) => {
canvasElement.toBlob((result) => {
if (result) {
resolve(result);
return;
}
const blob = await new Promise<Blob>((resolve, reject) => {
canvasElement.toBlob((result) => {
if (result) {
resolve(result);
return;
}
reject(new Error("Could not create thumbnail blob."));
}, "image/png");
});
reject(new Error('Could not create thumbnail blob.'));
}, 'image/png');
});
setBlobImage(blob);
setBlobImage(blob);
await loadingTask.destroy();
} catch (error) {
console.error(`Failed to render preview for ${attachment.path}`, error);
previewError = "PDF-Vorschau konnte nicht geladen werden.";
}
};
await loadingTask.destroy();
} catch (error) {
console.error(`Failed to render preview for ${attachment.path}`, error);
previewError = 'PDF-Vorschau konnte nicht geladen werden.';
}
};
void loadPreview();
})();
void loadPreview();
})();
return () => {
cancelled = true;
revokeObjectUrl();
};
});
return () => {
cancelled = true;
revokeObjectUrl();
};
});
</script>
<div class={className}>
{#if previewError}
<div class="flex h-full min-h-0 items-center justify-center rounded-md border border-dashed border-primary-200 bg-primary-50 px-3 py-4 text-center text-[12px] text-primary-800">
{previewError}
</div>
{:else if imageUrl}
<img
alt={attachment.name}
class="block rounded-md border border-primary-100 bg-white object-contain"
src={imageUrl}
/>
{:else}
<div class="flex items-center justify-center rounded-md border border-dashed border-primary-200 bg-primary-50 px-3 py-4 text-center text-[12px] text-primary-800">
Vorschau wird erstellt
</div>
{/if}
{#if previewError}
<div
class="flex h-full min-h-0 items-center justify-center rounded-md border border-dashed border-primary-200 bg-primary-50 px-3 py-4 text-center text-[12px] text-primary-800"
>
{previewError}
</div>
{:else if imageUrl}
<img
alt={attachment.name}
class="block rounded-md border border-primary-100 bg-white object-contain"
src={imageUrl}
/>
{:else}
<div
class="flex items-center justify-center rounded-md border border-dashed border-primary-200 bg-primary-50 px-3 py-4 text-center text-[12px] text-primary-800"
>
Vorschau wird erstellt
</div>
{/if}
</div>
+92
View File
@@ -0,0 +1,92 @@
<script lang="ts">
import { pwaInfo } from 'virtual:pwa-info';
import Button from '$lib/components/ui/button.svelte';
/**
* Service-worker registration and the update hint for the PWA.
*
* Rendered only in the browser: the plugin's virtual modules are no-ops during SSR and in
* `pnpm dev` (PWA `devOptions` are disabled), so `pwaInfo` is `undefined` there and nothing is
* registered.
*
* Updates are never applied automatically (registerType `prompt`): a waiting worker is only
* activated when the user confirms, so an in-progress archive is never interrupted.
*/
let needRefresh = $state(false);
let offlineReady = $state(false);
let updateServiceWorker = $state<(reloadPage?: boolean) => Promise<void>>(async () => {});
let offlineReadyTimer: ReturnType<typeof setTimeout> | undefined;
$effect(() => {
if (!pwaInfo) return;
let cancelled = false;
(async () => {
const { registerSW } = await import('virtual:pwa-register');
if (cancelled) return;
updateServiceWorker = registerSW({
immediate: true,
onNeedRefresh() {
offlineReady = false;
needRefresh = true;
},
onOfflineReady() {
offlineReady = true;
offlineReadyTimer = setTimeout(close, 8000);
},
onRegisterError(error: unknown) {
console.error('Service Worker konnte nicht registriert werden.', error);
}
});
})();
return () => {
cancelled = true;
if (offlineReadyTimer) clearTimeout(offlineReadyTimer);
};
});
function close() {
needRefresh = false;
offlineReady = false;
if (offlineReadyTimer) {
clearTimeout(offlineReadyTimer);
offlineReadyTimer = undefined;
}
}
async function reload() {
await updateServiceWorker(true);
}
</script>
{#if needRefresh || offlineReady}
<div
role="status"
aria-live="polite"
class="fixed inset-x-4 bottom-4 z-50 flex flex-col gap-3 rounded-2xl border border-border bg-white p-4 shadow-lg sm:left-auto sm:w-[min(24rem,calc(100vw-2rem))]"
>
<p class="text-sm font-bold text-primary">
{needRefresh
? 'Neue Version von beA-Edit verfügbar.'
: 'beA-Edit funktioniert jetzt offline.'}
</p>
{#if needRefresh}
<p class="text-sm text-muted-foreground">
Die neue Version wird geladen, sobald Sie neu laden. Ein laufender Vorgang wird dabei nicht
unterbrochen.
</p>
{/if}
<div class="flex flex-row flex-wrap gap-3">
{#if needRefresh}
<Button variant="primary" size="standard" onclick={reload}>Neu laden</Button>
{/if}
<Button variant="bordered" size="standard" onclick={close}>Später</Button>
</div>
</div>
{/if}
+13 -3
View File
@@ -2,7 +2,17 @@
</script>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5618 6.94745L14.1001 3.31245C15.0133 2.37424 16.5206 2.37424 17.4338 3.31245L19.0544 4.97737C19.9334 5.88045 19.9334 7.31929 19.0544 8.22237L14.3368 13.069C13.4235 14.0072 11.9162 14.0072 11.003 13.069L10.5537 12.6075" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.8717 16.4126L7.66659 21.76L4.37911 18.3826C3.50007 17.4796 3.50008 16.0407 4.37911 15.1376L9.09675 10.291C10.01 9.35276 11.5173 9.35276 12.4305 10.291L12.8798 10.7525" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
<path
d="M10.5618 6.94745L14.1001 3.31245C15.0133 2.37424 16.5206 2.37424 17.4338 3.31245L19.0544 4.97737C19.9334 5.88045 19.9334 7.31929 19.0544 8.22237L14.3368 13.069C13.4235 14.0072 11.9162 14.0072 11.003 13.069L10.5537 12.6075"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12.8717 16.4126L7.66659 21.76L4.37911 18.3826C3.50007 17.4796 3.50008 16.0407 4.37911 15.1376L9.09675 10.291C10.01 9.35276 11.5173 9.35276 12.4305 10.291L12.8798 10.7525"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
/>
</svg>
+25 -5
View File
@@ -2,9 +2,29 @@
</script>
<svg width="23" height="24" viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.6875 19.7575C18.6875 21.0807 17.6148 22.1533 16.2917 22.1533H4.3125V5.99287C4.3125 5.39212 4.54755 4.81522 4.96738 4.38552L7.17097 2.13015C7.60374 1.68721 8.19683 1.4375 8.81609 1.4375H16.3875C17.6578 1.4375 18.6875 2.46725 18.6875 3.7375V19.7575Z" stroke="currentColor" stroke-width="1.86875"/>
<path d="M7.06787 12.5889H15.6929" stroke="currentColor" stroke-width="1.725" stroke-linecap="round"/>
<path d="M7.06787 8.71875H15.6929" stroke="currentColor" stroke-width="1.725" stroke-linecap="round"/>
<path d="M7.06787 16.464H12.8179" stroke="currentColor" stroke-width="1.725" stroke-linecap="round"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.6875 19.7575C18.6875 21.0807 17.6148 22.1533 16.2917 22.1533H4.3125V5.99287C4.3125 5.39212 4.54755 4.81522 4.96738 4.38552L7.17097 2.13015C7.60374 1.68721 8.19683 1.4375 8.81609 1.4375H16.3875C17.6578 1.4375 18.6875 2.46725 18.6875 3.7375V19.7575Z"
stroke="currentColor"
stroke-width="1.86875"
/>
<path
d="M7.06787 12.5889H15.6929"
stroke="currentColor"
stroke-width="1.725"
stroke-linecap="round"
/>
<path
d="M7.06787 8.71875H15.6929"
stroke="currentColor"
stroke-width="1.725"
stroke-linecap="round"
/>
<path
d="M7.06787 16.464H12.8179"
stroke="currentColor"
stroke-width="1.725"
stroke-linecap="round"
/>
</svg>
+18 -5
View File
@@ -2,9 +2,22 @@
</script>
<svg width="21" height="15" viewBox="0 0 21 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.2188C0 12.5457 0.545653 12 1.21875 12H16.8256C17.339 12 17.6536 12.5629 17.3845 13.0002L16.5 14.4375H1.21875C0.545653 14.4375 0 13.8918 0 13.2188Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 7.21875C0 6.54565 0.545653 6 1.21875 6H18.2738C18.7979 6 19.1106 6.58416 18.8198 7.02027L17.875 8.4375H1.21875C0.545653 8.4375 0 7.89185 0 7.21875Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.21875C0 0.545653 0.545653 0 1.21875 0H19.721C20.2555 0 20.5658 0.60479 20.2541 1.03898L19.25 2.4375H1.21875C0.545653 2.4375 0 1.89185 0 1.21875Z" fill="currentColor"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 13.2188C0 12.5457 0.545653 12 1.21875 12H16.8256C17.339 12 17.6536 12.5629 17.3845 13.0002L16.5 14.4375H1.21875C0.545653 14.4375 0 13.8918 0 13.2188Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 7.21875C0 6.54565 0.545653 6 1.21875 6H18.2738C18.7979 6 19.1106 6.58416 18.8198 7.02027L17.875 8.4375H1.21875C0.545653 8.4375 0 7.89185 0 7.21875Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 1.21875C0 0.545653 0.545653 0 1.21875 0H19.721C20.2555 0 20.5658 0.60479 20.2541 1.03898L19.25 2.4375H1.21875C0.545653 2.4375 0 1.89185 0 1.21875Z"
fill="currentColor"
/>
</svg>
+10 -4
View File
@@ -2,8 +2,14 @@
</script>
<svg width="23" height="24" viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4129 7.29267L14.1203 1C14.1203 1 11.8437 8.76535 4.14746 10.9744L11.7731 18.6C13.9805 10.9022 21.7475 8.62562 21.7475 8.62562L15.4548 2.33295" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<path d="M7.28204 14.2833L8.8 15.8656L1.51796 23.0833L1.36219 22.921C0.606308 22.1331 0.622023 20.8845 1.3975 20.1159L7.28204 14.2833Z" fill="currentColor"/>
<path
d="M20.4129 7.29267L14.1203 1C14.1203 1 11.8437 8.76535 4.14746 10.9744L11.7731 18.6C13.9805 10.9022 21.7475 8.62562 21.7475 8.62562L15.4548 2.33295"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
/>
<path
d="M7.28204 14.2833L8.8 15.8656L1.51796 23.0833L1.36219 22.921C0.606308 22.1331 0.622023 20.8845 1.3975 20.1159L7.28204 14.2833Z"
fill="currentColor"
/>
</svg>
+19 -10
View File
@@ -2,14 +2,23 @@
</script>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9914 12.6763V15.4458V20.4H7.98481H3.6001V16.046V4.27311C3.6001 3.32208 4.49438 2.6246 5.41678 2.85624L11.8863 4.48089C12.5359 4.64402 12.9914 5.228 12.9914 5.89777V12.6763Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path d="M13.6491 8.40002L20.5221 10.2682C21.1583 10.4411 21.5998 11.0186 21.5998 11.6779V12.6424V15.424V18.9392C21.5998 19.746 20.9457 20.4 20.1389 20.4H17.4409H12.2085" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path d="M4.5 8.3H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.5 12.1H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.5 15.9H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 13.1H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 16.9H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.9914 12.6763V15.4458V20.4H7.98481H3.6001V16.046V4.27311C3.6001 3.32208 4.49438 2.6246 5.41678 2.85624L11.8863 4.48089C12.5359 4.64402 12.9914 5.228 12.9914 5.89777V12.6763Z"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<path
d="M13.6491 8.40002L20.5221 10.2682C21.1583 10.4411 21.5998 11.0186 21.5998 11.6779V12.6424V15.424V18.9392C21.5998 19.746 20.9457 20.4 20.1389 20.4H17.4409H12.2085"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<path d="M4.5 8.3H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M4.5 12.1H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M4.5 15.9H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M14 13.1H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M14 16.9H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
+7 -2
View File
@@ -1,4 +1,9 @@
<svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.27193 9.97498L13.4818 11.6926C14.3848 12.0611 14.9751 12.9394 14.9751 13.9148V15.4118V18.475C14.9751 19.8557 13.8558 20.975 12.4751 20.975H7.51156H0.975098V16.0858V13.8946C0.975098 12.9294 1.55327 12.0581 2.44261 11.6831L6.49358 9.97498" stroke="currentColor" stroke-width="1.95" stroke-linecap="round"/>
<circle cx="7.4751" cy="5.47498" r="4.5" stroke="currentColor" stroke-width="1.95"/>
<path
d="M9.27193 9.97498L13.4818 11.6926C14.3848 12.0611 14.9751 12.9394 14.9751 13.9148V15.4118V18.475C14.9751 19.8557 13.8558 20.975 12.4751 20.975H7.51156H0.975098V16.0858V13.8946C0.975098 12.9294 1.55327 12.0581 2.44261 11.6831L6.49358 9.97498"
stroke="currentColor"
stroke-width="1.95"
stroke-linecap="round"
/>
<circle cx="7.4751" cy="5.47498" r="4.5" stroke="currentColor" stroke-width="1.95" />
</svg>

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 518 B

+15 -7
View File
@@ -2,11 +2,19 @@
</script>
<svg width="22" height="23" viewBox="0 0 22 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 17.5834C21.5523 17.5834 22 18.0311 22 18.5834V19.5834H1C0.447716 19.5834 0 19.1357 0 18.5834C0 18.0311 0.447715 17.5834 1 17.5834H21Z" fill="#122F62"/>
<path d="M21 10.5834C21.5523 10.5834 22 11.0311 22 11.5834V12.5834H1C0.447716 12.5834 0 12.1357 0 11.5834C0 11.0311 0.447715 10.5834 1 10.5834H21Z" fill="#122F62"/>
<path d="M21 3.08337C21.5523 3.08337 22 3.53109 22 4.08337V5.08337H1C0.447716 5.08337 0 4.63566 0 4.08337C0 3.53109 0.447715 3.08337 1 3.08337H21Z" fill="#122F62"/>
<circle cx="6.5" cy="3.75" r="2.75" fill="white" stroke="#122F62" stroke-width="2"/>
<circle cx="15.5835" cy="11.0834" r="2.75" fill="white" stroke="#122F62" stroke-width="2"/>
<circle cx="6.5" cy="18.4167" r="2.75" fill="white" stroke="#122F62" stroke-width="2"/>
<path
d="M21 17.5834C21.5523 17.5834 22 18.0311 22 18.5834V19.5834H1C0.447716 19.5834 0 19.1357 0 18.5834C0 18.0311 0.447715 17.5834 1 17.5834H21Z"
fill="#122F62"
/>
<path
d="M21 10.5834C21.5523 10.5834 22 11.0311 22 11.5834V12.5834H1C0.447716 12.5834 0 12.1357 0 11.5834C0 11.0311 0.447715 10.5834 1 10.5834H21Z"
fill="#122F62"
/>
<path
d="M21 3.08337C21.5523 3.08337 22 3.53109 22 4.08337V5.08337H1C0.447716 5.08337 0 4.63566 0 4.08337C0 3.53109 0.447715 3.08337 1 3.08337H21Z"
fill="#122F62"
/>
<circle cx="6.5" cy="3.75" r="2.75" fill="white" stroke="#122F62" stroke-width="2" />
<circle cx="15.5835" cy="11.0834" r="2.75" fill="white" stroke="#122F62" stroke-width="2" />
<circle cx="6.5" cy="18.4167" r="2.75" fill="white" stroke="#122F62" stroke-width="2" />
</svg>
+8 -2
View File
@@ -1,4 +1,10 @@
<svg width="24" height="23" viewBox="0 0 24 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4142 5.48509C10.7066 5.19265 11.101 5.0248 11.5145 5.01676L17.3775 4.90268C18.2946 4.88483 19.039 5.64016 19.0077 6.55694L18.8111 12.3183C18.7973 12.7235 18.6301 13.1084 18.3434 13.3951L10.5219 21.2166C9.89704 21.8414 8.88398 21.8414 8.25914 21.2166L1.47091 14.4284L10.4142 5.48509Z" stroke="currentColor" stroke-width="2.08"/>
<circle cx="13.7192" cy="9.90002" r="1.5" fill="currentColor"/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.4142 5.48509C10.7066 5.19265 11.101 5.0248 11.5145 5.01676L17.3775 4.90268C18.2946 4.88483 19.039 5.64016 19.0077 6.55694L18.8111 12.3183C18.7973 12.7235 18.6301 13.1084 18.3434 13.3951L10.5219 21.2166C9.89704 21.8414 8.88398 21.8414 8.25914 21.2166L1.47091 14.4284L10.4142 5.48509Z"
stroke="currentColor"
stroke-width="2.08"
/>
<circle cx="13.7192" cy="9.90002" r="1.5" fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 563 B

+7 -5
View File
@@ -2,9 +2,11 @@
</script>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 7V19C18 20.1046 17.1046 21 16 21H6V7" stroke="currentColor" stroke-width="2"/>
<path d="M8 4C8 3.44772 8.44772 3 9 3H15C15.5523 3 16 3.44772 16 4V7H8V4Z" stroke="currentColor" stroke-width="1.8"/>
<path d="M3 7.5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M18 7V19C18 20.1046 17.1046 21 16 21H6V7" stroke="currentColor" stroke-width="2" />
<path
d="M8 4C8 3.44772 8.44772 3 9 3H15C15.5523 3 16 3.44772 16 4V7H8V4Z"
stroke="currentColor"
stroke-width="1.8"
/>
<path d="M3 7.5H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
+6 -2
View File
@@ -2,6 +2,10 @@
</script>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.49023 1H12.6572C13.5748 1 14.375 1.62449 14.5977 2.51465L17.0977 12.5146C17.4132 13.7769 16.4584 15 15.1572 15H1.20508L3.52441 2.63184C3.70178 1.68589 4.5278 1 5.49023 1Z" stroke="currentColor" stroke-width="2"/>
<rect x="5" y="17" width="8" height="2" rx="1" fill="currentColor"/>
<path
d="M5.49023 1H12.6572C13.5748 1 14.375 1.62449 14.5977 2.51465L17.0977 12.5146C17.4132 13.7769 16.4584 15 15.1572 15H1.20508L3.52441 2.63184C3.70178 1.68589 4.5278 1 5.49023 1Z"
stroke="currentColor"
stroke-width="2"
/>
<rect x="5" y="17" width="8" height="2" rx="1" fill="currentColor" />
</svg>
+81 -77
View File
@@ -1,89 +1,93 @@
<script lang="ts" module>
import {ripple} from "$lib/actions/ripple";
import {cn, type WithElementRef} from "$lib/utils.js";
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from "svelte/elements";
import {type VariantProps, tv} from "tailwind-variants";
import type {Component} from "svelte";
import { ripple } from '$lib/actions/ripple';
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
import type { Component } from 'svelte';
export const buttonVariants = tv({
base: "relative overflow-hidden font-bold w-fit min-w-28 hover:not-disabled:cursor-pointer disabled:bg-[#E6E7EA] disabled:text-[#0B1F4366] inline-flex items-center justify-center gap-4",
variants: {
variant: {
primary: "bg-primary text-primary-foreground",
secondary: "bg-linear-to-r from-secondary to-[#FF6F6D] text-primary-foreground",
bordered: "text-primary border-[#E6E7EA] border-2 bg-white hover:bg-[#122F621A]",
tertiary: "text-primary hover:bg-[#122F621A]",
success: "text-white bg-[#0FAD80]"
},
size: {
standard: "px-6 h-8 rounded-[16px]",
large: "px-8 h-12 rounded-[24px]"
},
},
defaultVariants: {
variant: "primary",
size: "standard",
},
});
export const buttonVariants = tv({
base: 'relative overflow-hidden font-bold w-fit min-w-28 hover:not-disabled:cursor-pointer disabled:bg-[#E6E7EA] disabled:text-[#0B1F4366] inline-flex items-center justify-center gap-4',
variants: {
variant: {
primary: 'bg-primary text-primary-foreground',
secondary: 'bg-linear-to-r from-secondary to-[#FF6F6D] text-primary-foreground',
bordered: 'text-primary border-[#E6E7EA] border-2 bg-white hover:bg-[#122F621A]',
tertiary: 'text-primary hover:bg-[#122F621A]',
success: 'text-white bg-[#0FAD80]'
},
size: {
standard: 'px-6 h-8 rounded-[16px]',
large: 'px-8 h-12 rounded-[24px]'
}
},
defaultVariants: {
variant: 'primary',
size: 'standard'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
icon?: Component;
};
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
icon?: Component;
};
</script>
<script lang="ts">
let {
class: className,
variant = "primary",
size = "standard",
icon,
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
let {
class: className,
variant = 'primary',
size = 'standard',
icon,
ref = $bindable(null),
href = undefined,
type = 'button',
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
use:ripple
bind:this={ref}
data-slot="button"
class={cn("", buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
{#if icon}
{@const Icon = icon}
<Icon/>
{/if}
</a>
<a
use:ripple
bind:this={ref}
data-slot="button"
class={cn('', buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
{#if icon}
{@const Icon = icon}
<Icon />
{/if}
</a>
{:else}
<button
use:ripple
bind:this={ref}
data-slot="button"
class={cn("inline-flex items-center justify-center gap-4", buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
{#if icon}
{@const Icon = icon}
<Icon/>
{/if}
</button>
<button
use:ripple
bind:this={ref}
data-slot="button"
class={cn(
'inline-flex items-center justify-center gap-4',
buttonVariants({ variant, size }),
className
)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
{#if icon}
{@const Icon = icon}
<Icon />
{/if}
</button>
{/if}
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card-action"
class={cn(
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
'cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className
)}
{...restProps}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="card-content"
class={cn("px-4 group-data-[size=sm]/card:px-3", className)}
class={cn('px-4 group-data-[size=sm]/card:px-3', className)}
{...restProps}
>
{@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -13,7 +13,7 @@
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
>
{@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="card-footer"
class={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)}
class={cn(
'bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center',
className
)}
{...restProps}
>
{@render children?.()}
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card-header"
class={cn(
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
'gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
className
)}
{...restProps}
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="card-title"
class={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
class={cn('text-base leading-snug font-medium group-data-[size=sm]/card:text-sm', className)}
{...restProps}
>
{@render children?.()}
+8 -5
View File
@@ -1,21 +1,24 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children,
size = "default",
size = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props();
</script>
<div
bind:this={ref}
data-slot="card"
data-size={size}
class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
class={cn(
'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
className
)}
{...restProps}
>
{@render children?.()}