style(shopping/loyalty): split buttons, enlarge barcodes

Replaces the single card button with two brand-colored buttons
(Coop blue, Migros orange) that each open only their own card.
Modal now wears the brand gradient directly, drops the red cross
close button pattern from BibleModal, and scales the Data Matrix
+ linear barcode to fill the modal on phones for easy scanning.
This commit is contained in:
2026-04-23 16:39:54 +02:00
parent 0ab98690eb
commit 43ea2cca22
3 changed files with 140 additions and 94 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.47.0", "version": "1.47.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+104 -88
View File
@@ -1,56 +1,60 @@
<script lang="ts"> <script lang="ts">
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
let { open = $bindable(false), hasSupercard = false, hasCumulus = false } = $props<{ type CardType = 'supercard' | 'cumulus' | null;
open?: boolean;
let { card = $bindable(null), hasSupercard = false, hasCumulus = false } = $props<{
card?: CardType;
hasSupercard?: boolean; hasSupercard?: boolean;
hasCumulus?: boolean; hasCumulus?: boolean;
}>(); }>();
function close() { open = false; } function close() { card = null; }
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') close(); if (e.key === 'Escape') close();
} }
const showSupercard = $derived(card === 'supercard' && hasSupercard);
const showCumulus = $derived(card === 'cumulus' && hasCumulus);
</script> </script>
<svelte:window onkeydown={onKeydown} /> <svelte:window onkeydown={onKeydown} />
{#if open} {#if card}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="backdrop" onclick={close}> <div class="backdrop" onclick={close}>
<div class="modal" role="dialog" aria-modal="true" aria-label="Kundenkarten" tabindex="-1" onclick={(e) => e.stopPropagation()}> <div
<button class="close" onclick={close} aria-label="Schliessen"> class="modal"
<X size={18} /> class:is-supercard={showSupercard}
class:is-cumulus={showCumulus}
role="dialog"
aria-modal="true"
aria-label={showSupercard ? 'Coop Supercard' : 'Migros Cumulus'}
tabindex="-1"
onclick={(e) => e.stopPropagation()}
>
<button class="close-button" onclick={close} aria-label="Schliessen">
<X />
</button> </button>
{#if hasSupercard} {#if showSupercard}
<article class="card supercard"> <div class="brand-head">
<header> <span class="brand">SUPERCARD</span>
<span class="brand">SUPERCARD</span> <span class="sub">Coop</span>
<span class="sub">Coop</span> </div>
</header> <div class="barcode barcode-square">
<div class="barcode barcode-square"> <img src="/shopping/supercard.svg" alt="Supercard Data Matrix" />
<img src="/shopping/supercard.svg" alt="Supercard Data Matrix" /> </div>
</div> {:else if showCumulus}
</article> <div class="brand-head">
{/if} <span class="brand">CUMULUS</span>
<span class="sub">Migros</span>
{#if hasCumulus} </div>
<article class="card cumulus"> <div class="barcode barcode-linear">
<header> <img src="/shopping/cumulus.svg" alt="Cumulus barcode" />
<span class="brand">CUMULUS</span> </div>
<span class="sub">Migros</span>
</header>
<div class="barcode barcode-linear">
<img src="/shopping/cumulus.svg" alt="Cumulus barcode" />
</div>
</article>
{/if}
{#if !hasSupercard && !hasCumulus}
<p class="empty">Keine Karten konfiguriert.</p>
{/if} {/if}
</div> </div>
</div> </div>
@@ -60,103 +64,115 @@
.backdrop { .backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.65);
z-index: 200; z-index: 200;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 1rem; padding: 1rem;
} }
.modal { .modal {
position: relative; position: relative;
background: var(--color-bg-secondary); border-radius: 24px;
border-radius: 20px; padding: 1.5rem 1.25rem 1.25rem;
padding: 1.25rem;
width: 100%; width: 100%;
max-width: 420px; max-width: 440px;
max-height: 90vh; color: white;
overflow-y: auto; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.close { .modal.is-supercard {
position: absolute;
top: 0.6rem;
right: 0.6rem;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.15);
color: white;
cursor: pointer;
z-index: 1;
}
.close:hover { background: rgba(0, 0, 0, 0.3); }
.card {
border-radius: 16px;
padding: 1rem 1.1rem 1.1rem;
color: white;
display: flex;
flex-direction: column;
gap: 0.9rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.supercard {
background: linear-gradient(135deg, #0a6fc2 0%, #055a9e 100%); background: linear-gradient(135deg, #0a6fc2 0%, #055a9e 100%);
} }
.cumulus { .modal.is-cumulus {
background: linear-gradient(135deg, #ff6a00 0%, #e55300 100%); background: linear-gradient(135deg, #ff6a00 0%, #e55300 100%);
} }
header { /* Red cross button — same pattern as BibleModal */
.close-button {
position: absolute;
top: -1rem;
right: -1rem;
background-color: var(--nord11);
border: none;
cursor: pointer;
padding: 0.75rem;
border-radius: var(--radius-pill);
color: white;
transition: var(--transition-normal);
box-shadow: 0 0 1em 0.2em rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.close-button :global(svg) {
width: 1.5rem;
height: 1.5rem;
}
.close-button:hover {
background-color: var(--nord0);
transform: scale(1.1);
box-shadow: 0 0 1em 0.4em rgba(0, 0, 0, 0.35);
}
.close-button:active {
transition: 50ms;
scale: 0.9 0.9;
}
.brand-head {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.5rem;
padding: 0 0.25rem;
} }
.brand { .brand {
font-weight: 800; font-weight: 800;
font-size: 1.25rem; font-size: 1.4rem;
letter-spacing: 0.08em; letter-spacing: 0.1em;
} }
.sub { .sub {
font-size: 0.7rem; font-size: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.14em; letter-spacing: 0.16em;
opacity: 0.85; opacity: 0.9;
} }
.barcode { .barcode {
background: white; background: white;
border-radius: 10px; border-radius: 14px;
padding: 0.75rem; padding: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.barcode-square img { .barcode img {
width: min(220px, 60%);
height: auto;
display: block; display: block;
image-rendering: pixelated; /* crisp barcode modules at any scale */
}
.barcode-square img {
width: 100%;
max-width: 360px;
height: auto;
aspect-ratio: 1 / 1;
} }
.barcode-linear img { .barcode-linear img {
width: 100%; width: 100%;
height: auto; height: auto;
max-height: 90px; min-height: 140px;
display: block; max-height: 30vh;
} }
.empty { @media (max-width: 480px) {
text-align: center; .backdrop { padding: 0.5rem; }
color: var(--color-text-secondary); .modal { padding: 1.25rem 1rem 1rem; border-radius: 20px; }
margin: 1rem 0; .brand { font-size: 1.25rem; }
font-size: 0.9rem; .barcode { padding: 0.75rem; }
.barcode-square img { max-width: none; }
.barcode-linear img { min-height: 160px; }
} }
</style> </style>
@@ -296,10 +296,10 @@
} }
// --- Loyalty cards --- // --- Loyalty cards ---
let showLoyalty = $state(false); /** @type {'supercard' | 'cumulus' | null} */
let activeCard = $state(null);
const hasSupercard = $derived(!!data.loyalty?.hasSupercard); const hasSupercard = $derived(!!data.loyalty?.hasSupercard);
const hasCumulus = $derived(!!data.loyalty?.hasCumulus); const hasCumulus = $derived(!!data.loyalty?.hasCumulus);
const hasAnyCard = $derived(hasSupercard || hasCumulus);
// --- Share links --- // --- Share links ---
let showShareModal = $state(false); let showShareModal = $state(false);
@@ -429,8 +429,13 @@
<div class="header-row"> <div class="header-row">
<h1 class="sr-only">{t('shopping_list_title', lang)}</h1> <h1 class="sr-only">{t('shopping_list_title', lang)}</h1>
<SyncIndicator status={sync.status} /> <SyncIndicator status={sync.status} />
{#if hasAnyCard} {#if hasSupercard}
<button class="btn-share" onclick={() => showLoyalty = true} title="Kundenkarten" aria-label="Kundenkarten"> <button class="btn-card btn-card-coop" onclick={() => activeCard = 'supercard'} title="Coop Supercard" aria-label="Coop Supercard">
<CreditCard size={16} />
</button>
{/if}
{#if hasCumulus}
<button class="btn-card btn-card-migros" onclick={() => activeCard = 'cumulus'} title="Migros Cumulus" aria-label="Migros Cumulus">
<CreditCard size={16} /> <CreditCard size={16} />
</button> </button>
{/if} {/if}
@@ -530,7 +535,7 @@
</div> </div>
<LoyaltyCards bind:open={showLoyalty} {hasSupercard} {hasCumulus} /> <LoyaltyCards bind:card={activeCard} {hasSupercard} {hasCumulus} />
{#if editingItem} {#if editingItem}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -705,6 +710,31 @@
background: var(--color-bg-elevated); background: var(--color-bg-elevated);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.btn-card {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
transition: transform 150ms ease, filter 150ms ease, box-shadow 150ms ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.btn-card:hover {
transform: translateY(-1px);
filter: brightness(1.08);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.btn-card:active { transform: translateY(0); filter: brightness(0.95); }
.btn-card-coop {
background: linear-gradient(135deg, #0a6fc2 0%, #055a9e 100%);
}
.btn-card-migros {
background: linear-gradient(135deg, #ff6a00 0%, #e55300 100%);
}
.subtitle { .subtitle {
margin: 0.25rem 0 0; margin: 0.25rem 0 0;
color: var(--color-text-secondary); color: var(--color-text-secondary);