refactor(subscriptions): compact row-based card layout

SubscriptionCard and PackageCard were large centered multi-cell cards
designed for a standalone pricing page. In the workbench context (narrow
card inside a carousel), they wasted too much vertical space for users
to compare plans at a glance.

Redesigned both as horizontal rows:
- Icon | name+mana | price | action — all in one line
- Badges (current/popular) inline next to the plan name
- No more 3-column internal grid with 70px min-height cells
- Clickable row replaces separate SubscriptionButton

SubscriptionPage:
- Drop the big centered header (icon + title + subtitle)
- Move Usage + Costs into a collapsed <details> section
- Section titles as small-caps labels
- Billing toggle at top, plans immediately visible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 12:38:30 +02:00
parent d83fc370a0
commit c6c4d630fe
3 changed files with 301 additions and 561 deletions

View file

@ -1,236 +1,127 @@
<script lang="ts">
import type { ManaPackage } from './plans';
import SubscriptionButton from './SubscriptionButton.svelte';
import ManaIcon from './ManaIcon.svelte';
interface Props {
package: ManaPackage;
onSelect: (packageId: string) => void;
// i18n labels
popularLabel?: string;
manaLabel?: string;
oneTimeLabel?: string;
buyLabel?: string;
}
let {
package: pkg,
onSelect,
popularLabel = 'Popular',
manaLabel = 'Mana',
oneTimeLabel = 'Einmalig',
buyLabel = 'Kaufen',
}: Props = $props();
let { package: pkg, onSelect, popularLabel = 'Beliebt', buyLabel = 'Kaufen' }: Props = $props();
function formatPrice(pkg: ManaPackage) {
return pkg.priceString || `${pkg.price.toFixed(2).replace('.', ',')}€`;
function formatPrice(p: ManaPackage) {
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
}
// Package-specific colors and background sizes
function getPackageStyles() {
function getColor() {
const id = pkg.id.toLowerCase();
if (id.includes('small')) return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
if (id.includes('medium')) return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
if (id.includes('large')) return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
if (id.includes('giant')) return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
if (id.includes('small')) return '#2196F3';
if (id.includes('medium')) return '#1976D2';
if (id.includes('large')) return '#1565C0';
if (id.includes('giant')) return '#0D47A1';
return '#0288D1';
}
const packageStyles = $derived(getPackageStyles());
// Hover state
let isHovered = $state(false);
</script>
<div
class="package-card"
class:package-card--popular={pkg.popular}
onmouseenter={() => (isHovered = true)}
onmouseleave={() => (isHovered = false)}
role="article"
>
{#if pkg.popular}
<div class="package-card__badge">
{popularLabel}
<button class="row" class:popular={pkg.popular} onclick={() => onSelect(pkg.id)}>
<div class="icon">
<ManaIcon size={20} color={getColor()} />
</div>
{/if}
<!-- Package Name -->
<h3 class="package-card__title">
<div class="info">
<span class="name">
{pkg.name}
</h3>
<!-- Three column layout -->
<div class="package-card__grid">
<!-- Mana Icon with background -->
<div class="package-card__cell">
<div
class="package-card__icon-wrapper"
style="width: {packageStyles.bgSize}; height: {packageStyles.bgSize}; background-color: {packageStyles.bg};"
>
<ManaIcon size={32} color={packageStyles.icon} />
</div>
{#if pkg.popular}
<span class="badge">{popularLabel}</span>
{/if}
</span>
<span class="mana">{pkg.manaAmount.toLocaleString('de-DE')} Mana</span>
</div>
<!-- Mana Amount -->
<div class="package-card__cell">
<p class="package-card__value">
{pkg.manaAmount}
</p>
<p class="package-card__label">{manaLabel}</p>
</div>
<!-- Price -->
<div class="package-card__cell">
<p class="package-card__price">
{formatPrice(pkg)}
</p>
<p class="package-card__sublabel">{oneTimeLabel}</p>
</div>
</div>
<SubscriptionButton
label={buyLabel}
onclick={() => onSelect(pkg.id)}
iconName="arrow-forward-outline"
leftIconName="cart-outline"
variant={pkg.popular ? 'accent' : 'primary'}
/>
</div>
<span class="price">{formatPrice(pkg)}</span>
<span class="action">{buyLabel}</span>
</button>
<style>
.package-card {
position: relative;
padding: 1rem;
border-radius: 0.75rem;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
min-width: 0;
overflow: hidden;
}
:global(.dark) .package-card {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.package-card:hover {
transform: translateY(-2px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .package-card:hover {
background: rgba(255, 255, 255, 0.12);
}
.package-card--popular {
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
}
.package-card__badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 600;
color: white;
background: hsl(var(--color-primary, 221 83% 53%));
z-index: 1;
}
.package-card__title {
margin: 1.5rem 0 1rem 0;
text-align: center;
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--color-foreground));
}
.package-card__grid {
.row {
display: flex;
justify-content: space-between;
gap: 0.375rem;
margin-bottom: 1.25rem;
}
.package-card__cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem;
min-height: 70px;
min-width: 0;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
background: rgba(0, 0, 0, 0.03);
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-card));
cursor: pointer;
transition: all 0.15s;
text-align: left;
color: hsl(var(--color-foreground));
font: inherit;
}
:global(.dark) .package-card__cell {
background: rgba(255, 255, 255, 0.05);
.row:hover {
background: hsl(var(--color-surface-hover));
border-color: hsl(var(--color-primary) / 0.3);
}
.package-card__icon-wrapper {
.row.popular {
border-color: hsl(var(--color-primary));
}
.icon {
flex-shrink: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
background: hsl(var(--color-muted) / 0.5);
}
.package-card__value {
margin: 0 0 0.125rem 0;
font-size: 1.25rem;
font-weight: 700;
color: hsl(var(--color-foreground));
white-space: nowrap;
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.package-card__price {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: hsl(var(--color-foreground));
white-space: nowrap;
.name {
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.375rem;
}
.package-card__label {
margin: 0;
font-size: 0.625rem;
color: hsl(var(--color-muted-foreground));
text-align: center;
}
.package-card__sublabel {
margin: 0.125rem 0 0 0;
font-size: 0.5rem;
color: hsl(var(--color-muted-foreground));
}
@media (min-width: 640px) {
.package-card__value {
font-size: 1.5rem;
}
.package-card__price {
font-size: 1.25rem;
}
.package-card__label {
.mana {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
}
.package-card__sublabel {
.badge {
font-size: 0.625rem;
padding: 0.0625rem 0.375rem;
border-radius: 9999px;
font-weight: 600;
color: white;
background: hsl(var(--color-primary));
}
.price {
flex-shrink: 0;
font-size: 0.875rem;
font-weight: 700;
}
.action {
flex-shrink: 0;
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--color-primary));
white-space: nowrap;
}
</style>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import type { SubscriptionPlan } from './plans';
import SubscriptionButton from './SubscriptionButton.svelte';
import ManaIcon from './ManaIcon.svelte';
interface Props {
@ -8,7 +7,6 @@
onSelect: (planId: string) => void;
isCurrentPlan?: boolean;
isLegacy?: boolean;
// i18n labels
currentPlanLabel?: string;
legacyPlanLabel?: string;
popularLabel?: string;
@ -25,254 +23,187 @@
onSelect,
isCurrentPlan = false,
isLegacy = false,
currentPlanLabel = 'Current Plan',
legacyPlanLabel = 'Legacy Plan',
popularLabel = 'Popular',
perMonthLabel = 'pro Monat',
perYearLabel = 'pro Jahr',
monthlyEquivalentLabel = '/Monat',
currentPlanLabel = 'Aktuell',
legacyPlanLabel = 'Legacy',
popularLabel = 'Beliebt',
perMonthLabel = '/Mo',
perYearLabel = '/Jahr',
buyLabel = 'Kaufen',
yourPlanLabel = 'Dein Plan',
yourLegacyPlanLabel = 'Dein Legacy-Plan',
}: Props = $props();
function formatPrice(plan: SubscriptionPlan) {
return plan.priceString || `${plan.price.toFixed(2).replace('.', ',')}€`;
function formatPrice(p: SubscriptionPlan) {
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
}
// Tier-specific background colors and sizes for Mana icon
function getTierStyles() {
function getTierColor() {
const id = plan.id.toLowerCase();
if (id.includes('free')) return { bg: '#F5F5F5', icon: '#9E9E9E', bgSize: '30%' };
if (id.includes('small')) return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
if (id.includes('medium')) return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
if (id.includes('large')) return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
if (id.includes('giant')) return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
if (id.includes('free')) return '#9E9E9E';
if (id.includes('small')) return '#2196F3';
if (id.includes('medium')) return '#1976D2';
if (id.includes('large')) return '#1565C0';
if (id.includes('giant')) return '#0D47A1';
return '#0288D1';
}
const tierStyles = $derived(getTierStyles());
// Hover state
let isHovered = $state(false);
const isFree = $derived(plan.id.toLowerCase().includes('free'));
</script>
<div
class="subscription-card"
class:subscription-card--current={isCurrentPlan}
class:subscription-card--popular={plan.popular && !isCurrentPlan}
onmouseenter={() => (isHovered = true)}
onmouseleave={() => (isHovered = false)}
role="article"
<button
class="row"
class:current={isCurrentPlan}
class:popular={plan.popular && !isCurrentPlan}
disabled={isCurrentPlan}
onclick={() => onSelect(plan.id)}
>
{#if isCurrentPlan}
<div class="subscription-card__badge subscription-card__badge--current">
{isLegacy ? legacyPlanLabel : currentPlanLabel}
<div class="icon" style="color: {getTierColor()}">
<ManaIcon size={20} color={getTierColor()} />
</div>
<div class="info">
<span class="name">
{plan.name}
{#if isCurrentPlan}
<span class="badge current-badge">{isLegacy ? legacyPlanLabel : currentPlanLabel}</span>
{/if}
{#if plan.popular && !isCurrentPlan}
<div class="subscription-card__badge subscription-card__badge--popular">
{popularLabel}
</div>
<span class="badge popular-badge">{popularLabel}</span>
{/if}
<!-- Tier Name -->
<h3 class="subscription-card__title">
{plan.name}
</h3>
<!-- Three column layout -->
<div class="subscription-card__grid">
<!-- Mana Icon with background -->
<div class="subscription-card__cell">
<div
class="subscription-card__icon-wrapper"
style="width: {tierStyles.bgSize}; height: {tierStyles.bgSize}; background-color: {tierStyles.bg};"
>
<ManaIcon size={32} color={tierStyles.icon} />
</div>
</span>
<span class="mana">{plan.monthlyMana} Mana{perMonthLabel}</span>
</div>
<!-- Mana Amount -->
<div class="subscription-card__cell">
<p class="subscription-card__value">
{plan.monthlyMana}
</p>
<p class="subscription-card__label">{perMonthLabel}</p>
</div>
<!-- Price -->
<div class="subscription-card__cell">
<p class="subscription-card__price">
{formatPrice(plan)}
</p>
<p class="subscription-card__label">
{plan.billingCycle === 'yearly' ? perYearLabel : perMonthLabel}
</p>
{#if plan.billingCycle === 'yearly' && plan.monthlyEquivalent}
<p class="subscription-card__sublabel">
({plan.monthlyEquivalent.toFixed(2).replace('.', ',')}{monthlyEquivalentLabel})
</p>
<div class="right">
{#if isFree}
<span class="price free">Kostenlos</span>
{:else}
<span class="price">{formatPrice(plan)}</span>
<span class="period">{plan.billingCycle === 'yearly' ? perYearLabel : perMonthLabel}</span>
{/if}
</div>
</div>
<!-- Button only show if NOT free plan -->
{#if !plan.id.toLowerCase().includes('free')}
<SubscriptionButton
label={isCurrentPlan ? (isLegacy ? yourLegacyPlanLabel : yourPlanLabel) : buyLabel}
onclick={() => onSelect(plan.id)}
iconName={isCurrentPlan ? 'checkmark-circle-outline' : 'arrow-forward-outline'}
variant={isCurrentPlan ? 'secondary' : plan.popular ? 'accent' : 'primary'}
disabled={isCurrentPlan}
/>
{#if !isFree && !isCurrentPlan}
<span class="action">{buyLabel}</span>
{:else if isCurrentPlan}
<span class="action muted">{yourPlanLabel}</span>
{/if}
</div>
</button>
<style>
.subscription-card {
position: relative;
padding: 1.25rem;
border-radius: 1rem;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
min-width: 0;
overflow: hidden;
}
:global(.dark) .subscription-card {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.subscription-card:hover {
transform: translateY(-2px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .subscription-card:hover {
background: rgba(255, 255, 255, 0.12);
}
.subscription-card--current {
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
}
.subscription-card--popular {
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
}
.subscription-card__badge {
position: absolute;
top: 0.75rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 600;
color: white;
background: hsl(var(--color-primary, 221 83% 53%));
z-index: 1;
}
.subscription-card__badge--current {
left: 0.75rem;
}
.subscription-card__badge--popular {
right: 0.75rem;
}
.subscription-card__title {
margin: 1.5rem 0 1rem 0;
text-align: center;
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--color-foreground));
}
.subscription-card__grid {
.row {
display: flex;
justify-content: space-between;
gap: 0.375rem;
margin-bottom: 1.25rem;
}
.subscription-card__cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem;
min-height: 70px;
min-width: 0;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
background: rgba(0, 0, 0, 0.03);
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-card));
cursor: pointer;
transition: all 0.15s;
text-align: left;
color: hsl(var(--color-foreground));
font: inherit;
}
:global(.dark) .subscription-card__cell {
background: rgba(255, 255, 255, 0.05);
.row:hover:not(:disabled) {
background: hsl(var(--color-surface-hover));
border-color: hsl(var(--color-primary) / 0.3);
}
.subscription-card__icon-wrapper {
.row:disabled {
cursor: default;
opacity: 0.8;
}
.row.current {
border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.05);
}
.row.popular {
border-color: hsl(var(--color-primary));
}
.icon {
flex-shrink: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
background: hsl(var(--color-muted) / 0.5);
}
.subscription-card__value {
margin: 0 0 0.25rem 0;
font-size: 1.25rem;
font-weight: 700;
color: hsl(var(--color-foreground));
white-space: nowrap;
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.subscription-card__price {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: hsl(var(--color-foreground));
white-space: nowrap;
.name {
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.375rem;
}
.subscription-card__label {
margin: 0;
font-size: 0.625rem;
color: hsl(var(--color-muted-foreground));
text-align: center;
}
.subscription-card__sublabel {
margin: 0.125rem 0 0 0;
font-size: 0.5rem;
color: hsl(var(--color-muted-foreground));
}
@media (min-width: 640px) {
.subscription-card__value {
font-size: 1.5rem;
}
.subscription-card__price {
font-size: 1.25rem;
}
.subscription-card__label {
.mana {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
}
.subscription-card__sublabel {
.badge {
font-size: 0.625rem;
padding: 0.0625rem 0.375rem;
border-radius: 9999px;
font-weight: 600;
color: white;
}
.current-badge {
background: hsl(var(--color-primary));
}
.popular-badge {
background: hsl(var(--color-primary));
}
.right {
flex-shrink: 0;
text-align: right;
}
.price {
display: block;
font-size: 0.875rem;
font-weight: 700;
}
.price.free {
color: hsl(var(--color-muted-foreground));
font-weight: 500;
}
.period {
font-size: 0.625rem;
color: hsl(var(--color-muted-foreground));
}
.action {
flex-shrink: 0;
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--color-primary));
white-space: nowrap;
}
.action.muted {
color: hsl(var(--color-muted-foreground));
font-weight: 500;
}
</style>

View file

@ -7,35 +7,22 @@
import UsageCard from '../UsageCard.svelte';
import CostCard from '../CostCard.svelte';
// Import default data
import defaultSubscriptionData from '../data/subscriptionData.json';
import defaultAppCosts from '../data/appCosts.json';
import defaultUsageData from '../data/defaultUsageData.json';
interface Props {
/** App name for the page title */
appName: string;
/** Handler when user selects a subscription plan */
onSubscribe: (planId: string) => void;
/** Handler when user selects a mana package */
onBuyPackage: (packageId: string) => void;
/** Current plan ID (e.g., 'free', 'Mana_Stream_Small_v1') */
currentPlanId?: string;
/** Current user's usage data (optional, uses defaults if not provided) */
usageData?: UsageData;
/** Custom subscription plans (optional, uses defaults if not provided) */
subscriptions?: SubscriptionPlan[];
/** Custom mana packages (optional, uses defaults if not provided) */
packages?: ManaPackage[];
/** Custom cost items (optional, uses defaults if not provided) */
costs?: CostItem[];
/** Page title */
pageTitle?: string;
/** Subscriptions section title */
subscriptionsTitle?: string;
/** One-time purchases section title */
packagesTitle?: string;
/** Yearly discount label */
yearlyDiscount?: string;
}
@ -54,194 +41,125 @@
yearlyDiscount = '33%',
}: Props = $props();
// State
let billingCycle = $state<BillingCycle>('monthly');
// Get current plan name for display
const currentPlanName = $derived(() => {
const plan = subscriptions.find((p) => p.id === currentPlanId);
return plan?.name || 'Free';
});
// Get all subscription plans for current billing cycle
function getSubscriptionPlans() {
return subscriptions.filter((plan) => plan.id !== 'free' && plan.billingCycle === billingCycle);
}
// Check if a plan is the current plan
function isCurrentPlan(planId: string) {
if (currentPlanId === 'free' && planId === 'free') return true;
return planId === currentPlanId;
}
</script>
<svelte:head>
<title>Mana - {appName}</title>
</svelte:head>
<div class="subscription-page">
<!-- Content Area -->
<div class="subscription-page__content">
<div class="subscription-page__container">
<!-- Header -->
<div class="subscription-page__header">
<div class="subscription-page__icon">
<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10">
<path
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
/>
</svg>
</div>
<h1 class="subscription-page__title">{pageTitle}</h1>
<p class="subscription-page__subtitle">Wähle das passende Paket für deine Bedürfnisse</p>
<div class="sub-page">
<!-- Billing toggle -->
<div class="toggle-row">
<BillingToggle {billingCycle} onChange={(cycle) => (billingCycle = cycle)} {yearlyDiscount} />
</div>
<!-- Active Section (Usage & Costs) -->
<section class="subscription-page__section">
<div class="subscription-page__usage-grid">
<UsageCard {usageData} currentPlan={currentPlanName()} />
<CostCard {costs} />
</div>
</section>
<!-- Billing Toggle -->
<div class="subscription-page__toggle">
<BillingToggle
{billingCycle}
onChange={(cycle: BillingCycle) => (billingCycle = cycle)}
{yearlyDiscount}
/>
</div>
<!-- Subscriptions Section -->
<section class="subscription-page__section">
<h2 class="subscription-page__section-title">{subscriptionsTitle}</h2>
<div class="subscription-page__cards-grid">
<!-- Free Tier -->
<!-- Subscriptions -->
<section class="section">
<h2 class="section-title">{subscriptionsTitle}</h2>
<div class="card-list">
<SubscriptionCard
plan={subscriptions.find((plan) => plan.id === 'free')!}
plan={subscriptions.find((plan) => plan.id === 'free')}
onSelect={onSubscribe}
isCurrentPlan={isCurrentPlan('free')}
/>
<!-- All Paid Subscriptions -->
{#each getSubscriptionPlans() as plan}
<SubscriptionCard
{plan}
onSelect={onSubscribe}
isCurrentPlan={isCurrentPlan(plan.id)}
/>
<SubscriptionCard {plan} onSelect={onSubscribe} isCurrentPlan={isCurrentPlan(plan.id)} />
{/each}
</div>
</section>
<!-- One-time Purchases Section -->
<section class="subscription-page__section">
<h2 class="subscription-page__section-title">{packagesTitle}</h2>
<div class="subscription-page__cards-grid">
<!-- One-time packages -->
<section class="section">
<h2 class="section-title">{packagesTitle}</h2>
<div class="card-list">
{#each packages as pkg}
<PackageCard package={pkg} onSelect={onBuyPackage} />
{/each}
</div>
</section>
<!-- Usage & Costs (collapsed, less prominent) -->
<section class="section">
<details class="details">
<summary class="summary">Verbrauch & Kosten</summary>
<div class="details-content">
<UsageCard {usageData} currentPlan={currentPlanName()} />
<CostCard {costs} />
</div>
</div>
</details>
</section>
</div>
<style>
.subscription-page {
.sub-page {
display: flex;
flex-direction: column;
min-height: 100%;
width: 100%;
}
.subscription-page__content {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
padding: 1rem;
width: 100%;
box-sizing: border-box;
}
.subscription-page__container {
gap: 1.25rem;
max-width: 40rem;
margin: 0 auto;
padding-bottom: 3rem;
width: 100%;
box-sizing: border-box;
}
.subscription-page__header {
text-align: center;
margin-bottom: 2.5rem;
}
.subscription-page__icon {
width: 5rem;
height: 5rem;
margin: 0 auto 1.5rem;
.toggle-row {
display: flex;
align-items: center;
justify-content: center;
border-radius: 1rem;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: hsl(var(--color-primary, 221 83% 53%));
}
:global(.dark) .subscription-page__icon {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
.section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.subscription-page__title {
font-size: 1.875rem;
.section-title {
font-size: 0.8125rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0 0 0.75rem 0;
}
.subscription-page__subtitle {
font-size: 1rem;
color: hsl(var(--color-muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0;
}
.subscription-page__section {
margin-bottom: 2.5rem;
}
.subscription-page__section-title {
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0 0 1.5rem 0;
}
.subscription-page__usage-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.subscription-page__toggle {
.card-list {
display: flex;
justify-content: center;
margin-bottom: 2rem;
flex-direction: column;
gap: 0.5rem;
}
.subscription-page__cards-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
.details {
border: 1px solid hsl(var(--color-border));
border-radius: 0.75rem;
overflow: hidden;
}
.summary {
padding: 0.75rem 1rem;
font-size: 0.8125rem;
font-weight: 600;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
user-select: none;
}
.summary:hover {
color: hsl(var(--color-foreground));
}
.details-content {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0 1rem 1rem;
}
</style>