feat: QR-Code, vCard-Export, Org-Gruppenansicht im Adressbuch
Deploy / verify (push) Successful in 1m10s
Deploy / deploy (push) Successful in 1m45s

- vcard.ts: shared vCard-Generator (downloadVCard, downloadVCardAll, buildVCardString)
- QrModal.svelte + QrButton.svelte: wiederverwendbare QR-Komponenten mit PNG-Download
- AdressbuchBlock: Org-Gruppenansicht (Toggle), QR + vCard pro Kontakt,
  Buchstaben-Trennzeilen, Such-Highlight, "Alle exportieren", Print-Styles
- ContactCard: vCard-Download-Button
- PostActions: QR-Button für Seiten-URL
- qrcode npm-Paket hinzugefügt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-04 11:15:57 +02:00
parent 4642b32271
commit 982a7a3b73
10 changed files with 802 additions and 104 deletions
+26 -15
View File
@@ -2,6 +2,7 @@
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import type { ContactEntry } from "$lib/block-types";
import { downloadVCard } from "$lib/vcard";
let { contact }: { contact: ContactEntry } = $props();
@@ -17,24 +18,34 @@
class="not-prose card-surface relative flex h-full min-w-0 flex-col gap-2 overflow-hidden rounded-sm p-4 transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md"
>
<div class="flex flex-col gap-1.5">
<p class="truncate text-sm font-medium leading-tight text-stein-900">{contact.name ?? ""}</p>
<p class="truncate pr-6 text-sm font-medium leading-tight text-stein-900">{contact.name ?? ""}</p>
{#if contact.role || orgNames.length > 0}
<div class="flex flex-wrap gap-1">
{#if contact.role}
<span class="shrink-0 rounded-full border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">
{contact.role}
</span>
{/if}
{#each orgNames as org}
<span class="shrink-0 rounded-full border border-wald-200 bg-wald-50 px-1.5 py-0 text-[0.65rem] font-medium text-wald-700">
{org}
</span>
{/each}
</div>
{/if}
{#if contact.role || orgNames.length > 0}
<div class="flex flex-wrap gap-1">
{#if contact.role}
<span class="shrink-0 rounded-full border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">
{contact.role}
</span>
{/if}
{#each orgNames as org}
<span class="shrink-0 rounded-full border border-wald-200 bg-wald-50 px-1.5 py-0 text-[0.65rem] font-medium text-wald-700">
{org}
</span>
{/each}
</div>
{/if}
</div>
<!-- vCard download -->
<button
type="button"
title="Kontakt speichern (.vcf)"
onclick={() => downloadVCard(contact)}
class="absolute top-3 right-3 text-stein-300 transition-colors hover:text-wald-600"
>
<Icon icon="mdi:card-account-details-outline" class="size-4" />
</button>
{#if contact.phone || contact.email}
<div class="flex flex-col gap-1">
{#if contact.phone}