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:
1 parent
b7969b4c9e
commit
f09384d2e6
37 files changed
+4130
-273
No files matched your search
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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?.()}
|
||||
|
||||
@@ -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?.()}
|
||||
|
||||
Reference in new issue
Block a user