Add interactive dice panel with custom components

This commit introduces a new DicePanel component, complete with custom dice (DieD6 and HouseDie) for enhanced interactivity. The panel is integrated into the UI with responsive styling, hover effects, and animations, providing a polished and functional user experience. Styles and layout adjustments in `AppLayout.css` support the new feature.
This commit is contained in:
2026-02-07 18:56:20 +01:00
parent b6e75cedc4
commit 8f912d9edb
2 changed files with 265 additions and 1 deletions

View File

@@ -541,6 +541,261 @@ export default function App() {
); );
}; };
// App.jsx (füge diese Komponente irgendwo unter deinen anderen Komponenten ein)
// ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck)
const DicePanel = () => {
return (
<div
style={{
pointerEvents: "auto", // overlay ist sonst pointer-events none im CSS
display: "grid",
gap: 10,
alignContent: "start",
}}
>
<div
style={{
borderRadius: 18,
border: `1px solid ${stylesTokens.panelBorder}`,
background: stylesTokens.panelBg,
boxShadow: "0 12px 30px rgba(0,0,0,0.35)",
backdropFilter: "blur(10px)",
padding: 12,
overflow: "visible",
minWidth: 0,
}}
>
<div
style={{
fontWeight: 900,
color: stylesTokens.textMain,
fontSize: 13,
letterSpacing: 0.2,
lineHeight: 1.15,
}}
>
Würfel
</div>
<div style={{ marginTop: 6, color: stylesTokens.textDim, fontSize: 12, opacity: 0.95 }}>
2× d6 + 1× Spezial
</div>
<div
style={{
marginTop: 10,
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 10,
alignItems: "center",
}}
>
<DieD6 value={4} />
<DieD6 value={2} />
<HouseDie face="gryffindor" />
</div>
</div>
</div>
);
};
const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)" }) => {
return (
<div
style={{
width: 62,
height: 62,
borderRadius: 18,
border: `1px solid ${stylesTokens.panelBorder}`,
background:
"radial-gradient(120% 120% at 20% 10%, rgba(255,255,255,0.10), rgba(0,0,0,0.35) 55%, rgba(0,0,0,0.55))",
boxShadow: "0 18px 50px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(255,255,255,0.06)",
position: "relative",
overflow: "hidden",
display: "grid",
placeItems: "center",
cursor: "pointer",
transition: "transform 160ms ease, box-shadow 160ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-3px) rotate(-1deg)";
e.currentTarget.style.boxShadow =
"0 26px 70px rgba(0,0,0,0.65), inset 0 0 0 1px rgba(255,255,255,0.08)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0px) rotate(0deg)";
e.currentTarget.style.boxShadow =
"0 18px 50px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(255,255,255,0.06)";
}}
title="Würfel (placeholder)"
>
{/* subtle inner ring */}
<div
style={{
position: "absolute",
inset: 7,
borderRadius: 14,
border: `1px solid ${ringColor}`,
opacity: 0.85,
pointerEvents: "none",
}}
/>
{/* gloss */}
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(120deg, rgba(255,255,255,0.10) 0%, transparent 38%, transparent 70%, rgba(255,255,255,0.06) 100%)",
opacity: 0.75,
pointerEvents: "none",
}}
/>
{children}
</div>
);
};
const DieD6 = ({ value = 1 }) => {
// pip layout positions on 3x3 grid
const P = ({ x, y }) => (
<div
style={{
position: "absolute",
left: `${x * 50}%`,
top: `${y * 50}%`,
transform: "translate(-50%, -50%)",
width: 7.5,
height: 7.5,
borderRadius: 999,
background: "rgba(245,245,245,0.92)",
boxShadow: "0 3px 10px rgba(0,0,0,0.35)",
}}
/>
);
const faces = {
1: [{ x: 1, y: 1 }],
2: [
{ x: 0, y: 0 },
{ x: 2, y: 2 },
],
3: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
{ x: 2, y: 2 },
],
4: [
{ x: 0, y: 0 },
{ x: 2, y: 0 },
{ x: 0, y: 2 },
{ x: 2, y: 2 },
],
5: [
{ x: 0, y: 0 },
{ x: 2, y: 0 },
{ x: 1, y: 1 },
{ x: 0, y: 2 },
{ x: 2, y: 2 },
],
6: [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: 2 },
{ x: 2, y: 0 },
{ x: 2, y: 1 },
{ x: 2, y: 2 },
],
};
return (
<DieShell ringColor="rgba(242,210,122,0.18)">
<div style={{ width: 44, height: 44, position: "relative" }}>
{(faces[value] || faces[1]).map((p, idx) => (
<P key={idx} x={p.x} y={p.y} />
))}
</div>
{/* tiny label */}
<div
style={{
position: "absolute",
bottom: 6,
right: 8,
fontSize: 10,
fontWeight: 900,
color: "rgba(255,255,255,0.55)",
letterSpacing: 0.6,
}}
>
d6
</div>
</DieShell>
);
};
const HouseDie = ({ face = "gryffindor" }) => {
// 4 Häuser + Hilfkarte + Dunkles Deck
const faces = {
gryffindor: { label: "G", color: "#ef4444", sub: "Gryff." },
slytherin: { label: "S", color: "#22c55e", sub: "Slyth." },
ravenclaw: { label: "R", color: "#3b82f6", sub: "Raven." },
hufflepuff: { label: "H", color: "#facc15", sub: "Huff." },
help: { label: "?", color: "#f2d27a", sub: "Hilf" },
dark: { label: "☾", color: "rgba(255,255,255,0.70)", sub: "Dark" },
};
const f = faces[face] || faces.gryffindor;
return (
<DieShell ringColor={`${f.color}55`}>
<div
style={{
width: 44,
height: 44,
borderRadius: 14,
display: "grid",
placeItems: "center",
background:
"radial-gradient(120% 120% at 30% 20%, rgba(255,255,255,0.10), rgba(0,0,0,0.35) 70%, rgba(0,0,0,0.55))",
border: `1px solid rgba(255,255,255,0.08)`,
boxShadow: `inset 0 0 0 1px ${f.color}22`,
position: "relative",
}}
>
<div
style={{
fontWeight: 1000,
fontSize: 22,
color: f.color,
textShadow: `0 0 16px ${f.color}55, 0 8px 24px rgba(0,0,0,0.55)`,
letterSpacing: 0.5,
}}
>
{f.label}
</div>
</div>
<div
style={{
position: "absolute",
bottom: 6,
left: 10,
fontSize: 10,
fontWeight: 900,
color: "rgba(255,255,255,0.55)",
letterSpacing: 0.6,
}}
>
Spezial
</div>
</DieShell>
);
};
const PlayerIdentityCard = ({ const PlayerIdentityCard = ({
name = "Harry Potter", name = "Harry Potter",
houseLabel = "Gryffindor", houseLabel = "Gryffindor",
@@ -761,7 +1016,7 @@ export default function App() {
</PlaceholderCard> </PlaceholderCard>
<div className="diceOverlay"> <div className="diceOverlay">
<PlaceholderCard title="Würfel" variant="compact" /> <DicePanel />
</div> </div>
</div> </div>

View File

@@ -167,6 +167,15 @@ body {
padding-right: 4px; padding-right: 4px;
} }
.diceOverlay {
position: absolute;
bottom: 14px;
right: 18px;
width: 220px;
pointer-events: auto; /* ✅ damit Hover/Clicks für Würfel gehen */
opacity: 0.98;
}
/* --- Responsive: shrink gracefully (no scroll outside notes) --- */ /* --- Responsive: shrink gracefully (no scroll outside notes) --- */
@media (max-height: 860px) { @media (max-height: 860px) {
.leftPane { .leftPane {