feat: migrate frontend React 18 → Svelte 5 + SvelteKit
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
Core logic portable (economy, balance, cosmetics, migrateSave) — zero rewrite. 136 tests green, identiques. Backend inchangé. - Svelte 5 runes stores (game, auth, toast) remplacent Zustand - SvelteKit adapter-static SPA (dist/ output, fallback index.html) - Tailwind v4 conservé, design system .gp-* porté - Transitions natives : slide, fly, scale, fade sur toute l'UI - Sidebar tabbée (Production/Evolution/Collection) + CollapsiblePanel - Mobile bottom sheet avec FAB toggle + backdrop blur - Click particles réactifs Svelte (plus de DOM impératif) - TadpoleSprite bounce + glow ring au clic - Guide refait en accordéon, Achievements avec filtres - a11y : focus-visible, Escape modals, aria-current, aria-labels - CI/CD adapté (tests + build + rsync) - Build 504K (vs ~1.2MB React)
This commit is contained in:
54
Frontend/src/lib/components/CosmeticsPanel.svelte
Normal file
54
Frontend/src/lib/components/CosmeticsPanel.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { COSMETICS, type CosmeticSlot } from '$lib/core/cosmetics';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
|
||||
const SLOT_LABELS: Record<CosmeticSlot, string> = {
|
||||
hat: 'Tete', eyes: 'Yeux', body: 'Corps', tail: 'Queue', accessory: 'Aura',
|
||||
};
|
||||
const SLOT_ICONS: Record<CosmeticSlot, string> = {
|
||||
hat: '👑', eyes: '👁', body: '🛡', tail: '🦎', accessory: '✨',
|
||||
};
|
||||
const SLOT_ORDER: CosmeticSlot[] = ['hat', 'eyes', 'body', 'tail', 'accessory'];
|
||||
|
||||
let inventory = $derived(gameStore.state.cosmeticInventory);
|
||||
let equipped = $derived(gameStore.state.cosmeticEquipped);
|
||||
let ownedCosmetics = $derived(COSMETICS.filter((c) => inventory.includes(c.id)));
|
||||
</script>
|
||||
|
||||
{#if inventory.length > 0}
|
||||
<CollapsiblePanel
|
||||
title="Cosmetiques"
|
||||
badge="{inventory.length}/{COSMETICS.length}"
|
||||
defaultOpen={false}
|
||||
>
|
||||
{#each SLOT_ORDER as slot, si}
|
||||
{@const slotCosmetics = ownedCosmetics.filter((c) => c.slot === slot)}
|
||||
{#if slotCosmetics.length > 0}
|
||||
<div
|
||||
class="flex flex-col gap-0.5"
|
||||
in:fly={{ y: 15, delay: si * 60, duration: 250, easing: quintOut }}
|
||||
>
|
||||
<span class="gp-zone-label">{SLOT_ICONS[slot]} {SLOT_LABELS[slot]}</span>
|
||||
{#each slotCosmetics as cos}
|
||||
{@const isEquipped = equipped[slot] === cos.id}
|
||||
<div class="gp-row {isEquipped ? 'gp-row--unlocked' : 'gp-row--active'}">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="gp-value text-[0.7rem]!">{cos.name}</span>
|
||||
<span class="gp-label">{cos.description}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => isEquipped ? gameStore.unequipCosmetic(slot) : gameStore.equipCosmetic(cos.id)}
|
||||
class="gp-btn {isEquipped ? 'gp-btn--disabled' : 'gp-btn--buy'}"
|
||||
>
|
||||
{isEquipped ? 'Retirer' : 'Equiper'}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</CollapsiblePanel>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user