feat: cockpit sidebar — design tokens, panels harmonisés, header stats fixe
- Design tokens game dans root.scss (--gp-*) — un seul endroit pour thémiser - game-panels.scss : classes partagées (gp, gp-row, gp-btn, gp-progress, etc.) - CockpitHeader : dashboard résumé (prod/s, ponte, mult, ADN, génération) - Tous les panels refactorisés sur le système gp-* (tailles, couleurs, spacing) - Sidebar structurée en zones : header → progression → générateurs → prestige → évolution
This commit is contained in:
37
Frontend/src/components/CockpitHeader.tsx
Normal file
37
Frontend/src/components/CockpitHeader.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// CockpitHeader.tsx — Dashboard résumé toujours visible en haut du cockpit
|
||||||
|
|
||||||
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { formatNumber } from "../utils/formatNumber";
|
||||||
|
|
||||||
|
export function CockpitHeader() {
|
||||||
|
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
|
||||||
|
const { clickMultiplier, prestigeMultiplier, ancestralDna, prestigeCount } =
|
||||||
|
useGameStore((s) => s.state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gp gp-cockpit-header">
|
||||||
|
<div className="gp-stat">
|
||||||
|
<span className="gp-label">Prod/s</span>
|
||||||
|
<span className="gp-value gp-accent-green">
|
||||||
|
{formatNumber(productionPerSecond)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="gp-stat">
|
||||||
|
<span className="gp-label">Ponte</span>
|
||||||
|
<span className="gp-value">x{clickMultiplier}</span>
|
||||||
|
</div>
|
||||||
|
<div className="gp-stat">
|
||||||
|
<span className="gp-label">Mult</span>
|
||||||
|
<span className="gp-value">x{prestigeMultiplier.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="gp-stat">
|
||||||
|
<span className="gp-label">ADN</span>
|
||||||
|
<span className="gp-value gp-accent-purple">{ancestralDna}</span>
|
||||||
|
</div>
|
||||||
|
<div className="gp-stat">
|
||||||
|
<span className="gp-label">Gén.</span>
|
||||||
|
<span className="gp-value">{prestigeCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
// EvolutionTree.tsx — Arbre d'Évolution permanent (jamais reset)
|
// EvolutionTree.tsx — Arbre d'Évolution permanent (jamais reset)
|
||||||
// Visible après le premier prestige (prestigeCount >= 1)
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { useGameStore } from "../store/useGameStore";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
import { canBuyEvolutionNode } from "../core/economy";
|
import { canBuyEvolutionNode } from "../core/economy";
|
||||||
import type { EvolutionNode } from "../core/economy";
|
import type { EvolutionNode } from "../core/economy";
|
||||||
|
|
||||||
const EFFECT_DESCRIPTIONS: Record<string, (value: number) => string> = {
|
const EFFECT_LABELS: Record<string, (v: number) => string> = {
|
||||||
click_multiplier: (v) => `x${v} puissance de Ponte`,
|
click_multiplier: (v) => `x${v} ponte`,
|
||||||
production_multiplier: (v) => `x${v} production tous générateurs`,
|
production_multiplier: (v) => `x${v} production`,
|
||||||
start_bonus: (v) => `+${v} têtards au début de chaque run`,
|
start_bonus: (v) => `+${v} têtards au départ`,
|
||||||
unlock_generator: () => `Débloque le Lac Mystique dès le début`,
|
unlock_generator: () => `Lac Mystique dès le début`,
|
||||||
achievement_scaling: (v) => `+${(v * 100).toFixed(0)}% production par succès`,
|
achievement_scaling: (v) => `+${(v * 100).toFixed(0)}% prod/succès`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function NodeCard({
|
function NodeRow({
|
||||||
node,
|
node,
|
||||||
canBuy,
|
canBuy,
|
||||||
onBuy,
|
onBuy,
|
||||||
@@ -23,36 +21,28 @@ function NodeCard({
|
|||||||
canBuy: boolean;
|
canBuy: boolean;
|
||||||
onBuy: () => void;
|
onBuy: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const rowClass = node.unlocked
|
||||||
<div
|
? "gp-row gp-row--unlocked"
|
||||||
className={`flex flex-col gap-2 p-3 rounded-lg border text-sm transition-colors ${
|
|
||||||
node.unlocked
|
|
||||||
? "border-emerald-500/50 bg-emerald-950/30"
|
|
||||||
: canBuy
|
: canBuy
|
||||||
? "border-amber-500/50 bg-amber-950/20"
|
? "gp-row gp-row--evolution"
|
||||||
: "border-gray-700/50 bg-gray-800/30 opacity-50"
|
: "gp-row gp-row--locked";
|
||||||
}`}
|
|
||||||
>
|
return (
|
||||||
<div className="flex justify-between items-center">
|
<div className={rowClass}>
|
||||||
<span className="text-white font-semibold">{node.name}</span>
|
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="gp-value">{node.name}</span>
|
||||||
{node.unlocked ? "Débloqué" : `${node.cost} ADN`}
|
<span className="gp-label">{EFFECT_LABELS[node.effect](node.value)}</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-300">
|
{node.unlocked ? (
|
||||||
{EFFECT_DESCRIPTIONS[node.effect](node.value)}
|
<span className="gp-label gp-accent-green">OK</span>
|
||||||
</p>
|
) : (
|
||||||
{!node.unlocked && (
|
|
||||||
<button
|
<button
|
||||||
disabled={!canBuy}
|
disabled={!canBuy}
|
||||||
onClick={onBuy}
|
onClick={onBuy}
|
||||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors cursor-pointer ${
|
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
|
||||||
canBuy
|
style={canBuy ? { background: "#d97706" } : {}}
|
||||||
? "bg-amber-600 hover:bg-amber-500 text-white"
|
|
||||||
: "bg-gray-700 text-gray-500 cursor-not-allowed"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{canBuy ? "Débloquer" : "Verrouillé"}
|
{node.cost} ADN
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -67,33 +57,19 @@ export function EvolutionTree() {
|
|||||||
if (prestigeCount < 1) return null;
|
if (prestigeCount < 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
|
<div className="gp">
|
||||||
<div className="flex justify-between items-center">
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
<h3 className="text-lg font-bold text-white">Arbre d'Évolution</h3>
|
<span className="gp-title">Évolution</span>
|
||||||
<span className="text-sm text-amber-300">{state.ancestralDna} ADN</span>
|
<span className="gp-value gp-accent-amber">{state.ancestralDna} ADN</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
{evolutionTree.map((node) => (
|
||||||
{evolutionTree.map((node, index) => (
|
<NodeRow
|
||||||
<React.Fragment key={node.id}>
|
key={node.id}
|
||||||
{index > 0 && (
|
|
||||||
<div
|
|
||||||
className={`text-center text-xs ${
|
|
||||||
evolutionTree[index - 1].unlocked
|
|
||||||
? "text-emerald-400"
|
|
||||||
: "text-gray-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<NodeCard
|
|
||||||
node={node}
|
node={node}
|
||||||
canBuy={canBuyEvolutionNode(state, node.id)}
|
canBuy={canBuyEvolutionNode(state, node.id)}
|
||||||
onBuy={() => buyNode(node.id)}
|
onBuy={() => buyNode(node.id)}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ export function GeneratorShop() {
|
|||||||
const generatorCost = useGameStore((s) => s.generatorCost);
|
const generatorCost = useGameStore((s) => s.generatorCost);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
|
<div className="gp">
|
||||||
<div className="flex justify-between items-center">
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
<h2 className="text-sm font-bold text-white">Générateurs</h2>
|
<span className="gp-title">Générateurs</span>
|
||||||
<span className="text-xs text-emerald-400 font-medium">
|
<span className="gp-value gp-accent-green">{formatNumber(productionPerSecond)}/s</span>
|
||||||
{formatNumber(productionPerSecond)}/s
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{generators.map((gen) => {
|
{generators.map((gen) => {
|
||||||
const cost = generatorCost(gen);
|
const cost = generatorCost(gen);
|
||||||
@@ -26,32 +24,24 @@ export function GeneratorShop() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={gen.id}
|
key={gen.id}
|
||||||
className={`flex items-center justify-between gap-2 p-2.5 rounded-lg border transition-colors ${
|
className={`gp-row ${canAfford ? "gp-row--active" : "gp-row--locked"}`}
|
||||||
canAfford
|
|
||||||
? "border-emerald-500/50 bg-emerald-950/30 hover:bg-emerald-950/50"
|
|
||||||
: "border-gray-700/50 bg-gray-800/30 opacity-60"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col min-w-0">
|
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
<div className="flex items-center gap-2">
|
<div style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
|
||||||
<span className="text-white font-semibold text-sm">{gen.name}</span>
|
<span className="gp-value">{gen.name}</span>
|
||||||
{gen.owned > 0 && (
|
{gen.owned > 0 && (
|
||||||
<span className="text-emerald-400 text-xs font-medium">x{gen.owned}</span>
|
<span className="gp-label gp-accent-green">x{gen.owned}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-400 text-xs">
|
<span className="gp-label">
|
||||||
+{gen.baseProduction}/s chacun
|
+{gen.baseProduction}/s
|
||||||
{gen.owned > 0 && ` · ${formatNumber(currentProd)}/s total`}
|
{gen.owned > 0 && ` · ${formatNumber(currentProd)}/s total`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => buy(gen.id)}
|
onClick={() => buy(gen.id)}
|
||||||
disabled={!canAfford}
|
disabled={!canAfford}
|
||||||
className={`shrink-0 px-3 py-1.5 rounded-md text-xs font-medium transition-colors cursor-pointer ${
|
className={`gp-btn ${canAfford ? "gp-btn--buy" : "gp-btn--disabled"}`}
|
||||||
canAfford
|
|
||||||
? "bg-emerald-600 hover:bg-emerald-500 text-white"
|
|
||||||
: "bg-gray-700 text-gray-500 cursor-not-allowed"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{formatNumber(cost)}
|
{formatNumber(cost)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// MilestoneBar.tsx — Progression vers le prochain prestige
|
// MilestoneBar.tsx — Progression vers le prochain prestige
|
||||||
// Barre visuelle ressources / 1 000 000
|
|
||||||
|
|
||||||
import { useGameStore } from "../store/useGameStore";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
import { formatNumber } from "../utils/formatNumber";
|
import { formatNumber } from "../utils/formatNumber";
|
||||||
@@ -14,24 +13,27 @@ export function MilestoneBar() {
|
|||||||
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
|
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 max-w-md w-full">
|
<div className="gp" style={{ gap: "0.25rem" }}>
|
||||||
<div className="text-xs text-gray-300 flex justify-between">
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<span>Prochaine Génération</span>
|
<span className="gp-label">Prochaine Génération</span>
|
||||||
<span>
|
<span className="gp-label">
|
||||||
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
|
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
<div className="gp-progress">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-purple-600 to-purple-400 transition-all duration-500 rounded-full"
|
className="gp-progress-fill"
|
||||||
style={{ width: `${progressPercent}%` }}
|
style={{
|
||||||
|
width: `${progressPercent}%`,
|
||||||
|
background: "linear-gradient(90deg, #7c3aed, #a78bfa)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 text-right">
|
<span className="gp-label" style={{ textAlign: "right" }}>
|
||||||
{remaining > 0
|
{remaining > 0
|
||||||
? `${formatNumber(remaining)} têtards restants`
|
? `${formatNumber(remaining)} restants`
|
||||||
: "Nouvelle Génération disponible !"}
|
: "Nouvelle Génération disponible !"}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// PrestigePanel.tsx — Nouvelle Génération (prestige)
|
// PrestigePanel.tsx — Nouvelle Génération (prestige)
|
||||||
// Visible uniquement quand canPrestige = true (ressources >= 1 000 000)
|
|
||||||
|
|
||||||
import { useGameStore } from "../store/useGameStore";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
import { computePrestigeDna } from "../core/economy";
|
import { computePrestigeDna } from "../core/economy";
|
||||||
|
|
||||||
export function PrestigePanel() {
|
export function PrestigePanel() {
|
||||||
const { prestigeCount, prestigeMultiplier, ancestralDna, lifetimeTadpoles } =
|
const { lifetimeTadpoles } = useGameStore((s) => s.state);
|
||||||
useGameStore((s) => s.state);
|
|
||||||
const canPrestige = useGameStore((s) => s.canPrestige);
|
const canPrestige = useGameStore((s) => s.canPrestige);
|
||||||
const prestige = useGameStore((s) => s.prestige);
|
const prestige = useGameStore((s) => s.prestige);
|
||||||
|
|
||||||
@@ -18,40 +16,26 @@ export function PrestigePanel() {
|
|||||||
`Reset : têtards et générateurs à zéro.\n` +
|
`Reset : têtards et générateurs à zéro.\n` +
|
||||||
`Récompense : +${dnaPreview} ADN Ancestral\n` +
|
`Récompense : +${dnaPreview} ADN Ancestral\n` +
|
||||||
` +0.1x multiplicateur permanent\n\n` +
|
` +0.1x multiplicateur permanent\n\n` +
|
||||||
`ADN actuel : ${ancestralDna}\n` +
|
|
||||||
`ADN après : ${ancestralDna + dnaPreview}\n` +
|
|
||||||
`Multiplicateur : x${prestigeMultiplier.toFixed(1)} → x${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
|
|
||||||
`L'Arbre d'Évolution persiste.\n\n` +
|
`L'Arbre d'Évolution persiste.\n\n` +
|
||||||
`Confirmer la Nouvelle Génération ?`
|
`Confirmer ?`
|
||||||
);
|
);
|
||||||
if (confirmed) prestige();
|
if (confirmed) prestige();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4 rounded-xl bg-purple-900/60 backdrop-blur-sm max-w-md w-full">
|
<div className="gp">
|
||||||
<h2 className="text-sm font-bold text-purple-100">Prestige</h2>
|
<span className="gp-title">Prestige</span>
|
||||||
<div className="flex flex-wrap gap-3 text-xs text-purple-200">
|
|
||||||
<span>Gén. {prestigeCount}</span>
|
|
||||||
<span>Mult x{prestigeMultiplier.toFixed(1)}</span>
|
|
||||||
<span>ADN {ancestralDna}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{canPrestige ? (
|
{canPrestige ? (
|
||||||
<div className="flex flex-col gap-2 mt-1">
|
<div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
|
||||||
<p className="text-sm text-purple-100">
|
<span className="gp-value gp-accent-purple">
|
||||||
+{dnaPreview} ADN · +0.1x mult
|
+{dnaPreview} ADN · +0.1x mult
|
||||||
</p>
|
</span>
|
||||||
<button
|
<button onClick={handlePrestige} className="gp-btn gp-btn--prestige">
|
||||||
onClick={handlePrestige}
|
|
||||||
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-semibold text-sm transition-colors cursor-pointer animate-pulse"
|
|
||||||
>
|
|
||||||
Nouvelle Génération
|
Nouvelle Génération
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-purple-300/60">
|
<span className="gp-label">Atteins 1M têtards pour prestige</span>
|
||||||
Atteins 1M têtards pour prestige
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ import { GeneratorShop } from "../components/GeneratorShop";
|
|||||||
import { PrestigePanel } from "../components/PrestigePanel";
|
import { PrestigePanel } from "../components/PrestigePanel";
|
||||||
import { EvolutionTree } from "../components/EvolutionTree";
|
import { EvolutionTree } from "../components/EvolutionTree";
|
||||||
import { MilestoneBar } from "../components/MilestoneBar";
|
import { MilestoneBar } from "../components/MilestoneBar";
|
||||||
|
import { CockpitHeader } from "../components/CockpitHeader";
|
||||||
import { ACHIEVEMENTS } from "../data/achievements";
|
import { ACHIEVEMENTS } from "../data/achievements";
|
||||||
import "../scss/home.scss";
|
import "../scss/home.scss";
|
||||||
|
import "../scss/components/game-panels.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [toggleRain] = useOutletContext();
|
const [toggleRain] = useOutletContext();
|
||||||
const click = useGameStore((s) => s.click);
|
const click = useGameStore((s) => s.click);
|
||||||
const resources = useGameStore((s) => s.state.resources);
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
||||||
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
|
|
||||||
const state = useGameStore((s) => s.state);
|
|
||||||
const prestigeMultiplier = state.prestigeMultiplier;
|
|
||||||
|
|
||||||
const createParticle = useCallback((clientX, clientY) => {
|
const createParticle = useCallback((clientX, clientY) => {
|
||||||
const particle = document.createElement("span");
|
const particle = document.createElement("span");
|
||||||
@@ -122,24 +121,20 @@ export default function Home() {
|
|||||||
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
||||||
{formatNumber(resources)}
|
{formatNumber(resources)}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-bar">
|
|
||||||
<span>{formatNumber(productionPerSecond)}/s</span>
|
|
||||||
<span className="stats-sep">·</span>
|
|
||||||
<span>x{clickMultiplier} ponte</span>
|
|
||||||
<span className="stats-sep">·</span>
|
|
||||||
<span>x{prestigeMultiplier.toFixed(1)} mult</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Game panels — sidebar (right desktop, bottom mobile) */}
|
{/* Cockpit — sidebar structurée en zones */}
|
||||||
<aside className="game-sidebar">
|
<aside className="game-sidebar">
|
||||||
|
<CockpitHeader />
|
||||||
|
<div className="gp-sep" />
|
||||||
<MilestoneBar />
|
<MilestoneBar />
|
||||||
<GeneratorShop />
|
<GeneratorShop />
|
||||||
|
<div className="gp-sep" />
|
||||||
<PrestigePanel />
|
<PrestigePanel />
|
||||||
<EvolutionTree />
|
<EvolutionTree />
|
||||||
<a href="/achievements" className="achieve-badge">
|
<a href="/achievements" className="achieve-badge">
|
||||||
{ACHIEVEMENTS.filter((a) => a.check(state)).length}/{ACHIEVEMENTS.length} succès
|
{ACHIEVEMENTS.filter((a) => a.check(useGameStore.getState().state)).length}/{ACHIEVEMENTS.length} succès
|
||||||
</a>
|
</a>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
172
Frontend/src/scss/components/game-panels.scss
Normal file
172
Frontend/src/scss/components/game-panels.scss
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// game-panels.scss — Système de style partagé pour tous les panels du cockpit
|
||||||
|
// Modifier les tokens dans root.scss, pas ici.
|
||||||
|
|
||||||
|
// --- Panel de base ---
|
||||||
|
.gp {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gp-gap);
|
||||||
|
padding: var(--gp-padding);
|
||||||
|
background: var(--gp-bg);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid var(--gp-border);
|
||||||
|
border-radius: var(--gp-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-title {
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: var(--gp-title);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gp-text-color);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-label {
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: var(--gp-text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--gp-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-value {
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: var(--gp-text);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gp-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-accent-green { color: var(--gp-accent-green); }
|
||||||
|
.gp-accent-purple { color: var(--gp-accent-purple); }
|
||||||
|
.gp-accent-amber { color: var(--gp-accent-amber); }
|
||||||
|
|
||||||
|
// --- Row item (générateur, noeud évolution) ---
|
||||||
|
.gp-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
border-radius: calc(var(--gp-radius) - 0.15rem);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-row--active {
|
||||||
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
|
background: var(--gp-accent-green-bg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-row--locked {
|
||||||
|
border-color: var(--gp-border);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-row--evolution {
|
||||||
|
border-color: rgba(251, 191, 36, 0.3);
|
||||||
|
background: var(--gp-accent-amber-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-row--unlocked {
|
||||||
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
|
background: var(--gp-accent-green-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bouton achat ---
|
||||||
|
.gp-btn {
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: var(--gp-text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-btn--buy {
|
||||||
|
background: var(--gp-btn-bg);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--gp-btn-bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-btn--disabled {
|
||||||
|
background: var(--gp-btn-disabled);
|
||||||
|
color: var(--gp-btn-text-disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-btn--prestige {
|
||||||
|
background: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: var(--gp-text);
|
||||||
|
animation: gp-pulse 2s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #8b5cf6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gp-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 6px rgba(124, 58, 237, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Header cockpit (stats résumé) ---
|
||||||
|
.gp-cockpit-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Progress bar ---
|
||||||
|
.gp-progress {
|
||||||
|
height: 0.35rem;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gp-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Section separator ---
|
||||||
|
.gp-sep {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--gp-border);
|
||||||
|
margin: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Zone titles in sidebar ---
|
||||||
|
.gp-zone-label {
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: var(--gp-text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gp-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
}
|
||||||
@@ -47,45 +47,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Stats bar sous le compteur ---
|
// --- Stats sous le compteur ---
|
||||||
|
|
||||||
.click-zone-stats {
|
.click-zone-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-sep {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Badge achievements sidebar ---
|
// --- Badge achievements sidebar ---
|
||||||
|
|
||||||
.achieve-badge {
|
.achieve-badge {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.5rem;
|
padding: 0.4rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: var(--gp-radius);
|
||||||
background: rgba(16, 185, 129, 0.1);
|
background: var(--gp-accent-green-bg);
|
||||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
font-size: 0.8rem;
|
font-size: var(--gp-text-sm);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: #6ee7b7;
|
color: var(--gp-accent-green);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,35 @@
|
|||||||
--bg-color: var(--color-blue-light);
|
--bg-color: var(--color-blue-light);
|
||||||
|
|
||||||
--font: "Hanken Grotesk", sans-serif;
|
--font: "Hanken Grotesk", sans-serif;
|
||||||
|
|
||||||
|
// --- Game panel tokens ---
|
||||||
|
--gp-bg: rgba(17, 17, 17, 0.75);
|
||||||
|
--gp-bg-hover: rgba(17, 17, 17, 0.85);
|
||||||
|
--gp-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--gp-radius: 0.75rem;
|
||||||
|
--gp-padding: 0.75rem;
|
||||||
|
--gp-gap: 0.5rem;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
--gp-title: 0.8rem;
|
||||||
|
--gp-text: 0.75rem;
|
||||||
|
--gp-text-sm: 0.65rem;
|
||||||
|
--gp-text-color: rgba(255, 255, 255, 0.9);
|
||||||
|
--gp-text-muted: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
// Accent colors
|
||||||
|
--gp-accent-green: #34d399;
|
||||||
|
--gp-accent-purple: #a78bfa;
|
||||||
|
--gp-accent-amber: #fbbf24;
|
||||||
|
--gp-accent-green-bg: rgba(16, 185, 129, 0.12);
|
||||||
|
--gp-accent-purple-bg: rgba(139, 92, 246, 0.12);
|
||||||
|
--gp-accent-amber-bg: rgba(251, 191, 36, 0.12);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
--gp-btn-bg: #059669;
|
||||||
|
--gp-btn-bg-hover: #10b981;
|
||||||
|
--gp-btn-disabled: rgba(255, 255, 255, 0.08);
|
||||||
|
--gp-btn-text-disabled: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|||||||
Reference in New Issue
Block a user