feat: add frontend as flat files (was submodule)

This commit is contained in:
2026-05-15 09:13:20 +02:00
parent 679929cffe
commit ce1972c6fa
23 changed files with 3677 additions and 1 deletions

30
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,30 @@
import { useState } from 'react'
import { IS_DEV } from './env'
import DevBanner from './components/DevBanner'
import Hero from './components/Hero'
import GelShowcase from './components/GelShowcase'
import KerboulistanBanner from './components/KerboulistanBanner'
import GameCanvas from './components/GameCanvas'
import Footer from './components/Footer'
function App() {
const [isPlaying, setIsPlaying] = useState(false)
if (isPlaying) {
return <GameCanvas onBack={() => setIsPlaying(false)} />
}
return (
<div className="min-h-screen">
<DevBanner />
{/* Offset content when dev banner is visible */}
{IS_DEV && <div className="h-8" />}
<Hero onPlay={() => setIsPlaying(true)} />
<GelShowcase />
<KerboulistanBanner />
<Footer />
</div>
)
}
export default App

View File

@@ -0,0 +1,26 @@
import { IS_DEV, theme } from '../env'
export default function DevBanner() {
if (!IS_DEV) return null
return (
<div
className="fixed top-0 left-0 right-0 z-[100] flex items-center justify-center gap-2 py-1.5 text-xs font-mono tracking-wider text-black/80 select-none"
style={{
background: `repeating-linear-gradient(
-45deg,
${theme.accent},
${theme.accent} 10px,
${theme.gradientTo} 10px,
${theme.gradientTo} 20px
)`,
}}
>
<span>{theme.badge}</span>
<span className="font-bold uppercase">Environnement de développement</span>
<span></span>
<span className="opacity-70">Ne pas utiliser en conditions réelles</span>
<span>{theme.badge}</span>
</div>
)
}

View File

@@ -0,0 +1,27 @@
export default function Footer() {
return (
<footer className="py-8 px-4 border-t border-rolld-border/50">
<div className="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4 text-sm text-rolld-muted">
<div className="flex items-center gap-2">
<span className="font-bold text-rolld-text">ROLL'D</span>
<span>·</span>
<span>Marble MMO</span>
</div>
<div className="flex items-center gap-4 text-xs">
<span>Unity · React · Colyseus</span>
<span>·</span>
<span className="text-rolld-accent/60">v0.1.2-dev</span>
<span>·</span>
<a
href="https://git.kerboul.me/kerboul/rolld_frontend"
target="_blank"
rel="noopener noreferrer"
className="hover:text-rolld-accent-light transition-colors"
>
Source
</a>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,189 @@
import { useState, useEffect, useCallback } from 'react'
// Check if Unity build files exist
const UNITY_BUILD_PATH = '/unity-build/Build'
// Cache-busting version — update this after each Unity build
const UNITY_BUILD_VERSION = '20260310c'
const LOADER_URL = `${UNITY_BUILD_PATH}/nouveau_build.loader.js?v=${UNITY_BUILD_VERSION}`
// Game server URL (Colyseus WebSocket)
const GAME_SERVER_URL = import.meta.env.VITE_GAME_SERVER_URL || 'ws://localhost:2567'
export default function GameCanvas({ onBack }) {
const [loadingProgress, setLoadingProgress] = useState(0)
const [isLoaded, setIsLoaded] = useState(false)
const [hasUnityBuild, setHasUnityBuild] = useState(null) // null = checking, true/false = result
const [error, setError] = useState(null)
useEffect(() => {
// Check if Unity build exists
fetch(LOADER_URL, { method: 'HEAD' })
.then((res) => {
if (res.ok) {
setHasUnityBuild(true)
loadUnity()
} else {
setHasUnityBuild(false)
}
})
.catch(() => {
setHasUnityBuild(false)
})
}, [])
const loadUnity = useCallback(() => {
// Dynamically load Unity loader
const script = document.createElement('script')
script.src = LOADER_URL
script.onload = () => {
if (typeof window.createUnityInstance === 'function') {
const canvas = document.getElementById('unity-canvas')
window.createUnityInstance(canvas, {
dataUrl: `${UNITY_BUILD_PATH}/nouveau_build.data?v=${UNITY_BUILD_VERSION}`,
frameworkUrl: `${UNITY_BUILD_PATH}/nouveau_build.framework.js?v=${UNITY_BUILD_VERSION}`,
codeUrl: `${UNITY_BUILD_PATH}/nouveau_build.wasm?v=${UNITY_BUILD_VERSION}`,
streamingAssetsUrl: '/unity-build/StreamingAssets',
companyName: 'ROLLD',
productName: 'ROLLD',
productVersion: '0.1',
}, (progress) => {
setLoadingProgress(Math.round(progress * 100))
}).then((instance) => {
setIsLoaded(true)
window.__unityInstance = instance
// Patch requestPointerLock to catch SecurityError (user exits lock before promise resolves)
const canvas = document.getElementById('unity-canvas')
if (canvas) {
const origLock = canvas.requestPointerLock.bind(canvas)
canvas.requestPointerLock = function (...args) {
try {
const result = origLock(...args)
if (result && typeof result.catch === 'function') {
return result.catch((e) => {
if (e.name === 'SecurityError') {
console.warn('[ROLLD] Pointer lock interrupted (SecurityError) — ignored')
} else {
throw e
}
})
}
return result
} catch (e) {
if (e.name === 'SecurityError') {
console.warn('[ROLLD] Pointer lock sync error — ignored')
} else {
throw e
}
}
}
}
// Pass game server URL to Unity's NetworkManager
instance.SendMessage('NetworkManager', 'SetServerURL', GAME_SERVER_URL)
console.log('[ROLLD] Unity loaded, server URL sent:', GAME_SERVER_URL)
}).catch((err) => {
setError(err.message)
})
}
}
script.onerror = () => setError('Failed to load Unity loader')
document.body.appendChild(script)
}, [])
return (
<div className="fixed inset-0 bg-rolld-bg z-50 flex flex-col">
{/* Top bar */}
<div className="flex items-center justify-between px-4 py-2 bg-rolld-surface/80 backdrop-blur border-b border-rolld-border">
<button
onClick={onBack}
className="flex items-center gap-2 text-rolld-muted hover:text-rolld-text transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Retour
</button>
<span className="text-rolld-accent font-bold tracking-wide text-sm">ROLL'D</span>
<div className="w-16" /> {/* Spacer */}
</div>
{/* Game area */}
<div className="flex-1 relative flex items-center justify-center">
{/* Unity Canvas (hidden until build exists) */}
<canvas
id="unity-canvas"
className={`w-full h-full ${hasUnityBuild && isLoaded ? 'block' : 'hidden'}`}
tabIndex={-1}
/>
{/* Loading state */}
{hasUnityBuild === true && !isLoaded && !error && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6">
<div className="text-4xl font-black">
<span className="text-rolld-text">ROLL</span>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-rolld-accent to-rolld-violet">'D</span>
</div>
<div className="w-64">
<div className="h-2 rounded-full bg-rolld-surface overflow-hidden">
<div
className="h-full rounded-full bg-gradient-to-r from-rolld-accent to-rolld-violet transition-all duration-300"
style={{ width: `${loadingProgress}%` }}
/>
</div>
<p className="text-rolld-muted text-sm text-center mt-2">Chargement {loadingProgress}%</p>
</div>
</div>
)}
{/* No build placeholder */}
{hasUnityBuild === false && (
<div className="flex flex-col items-center justify-center gap-6 text-center px-4">
<div className="w-24 h-24 rounded-3xl bg-rolld-surface border border-rolld-border flex items-center justify-center text-5xl animate-float">
🎮
</div>
<div>
<h2 className="text-2xl font-bold text-rolld-text mb-2">Build Unity en attente</h2>
<p className="text-rolld-muted max-w-md leading-relaxed">
Le build WebGL n'est pas encore disponible. Placez les fichiers dans{' '}
<code className="px-2 py-0.5 rounded bg-rolld-surface border border-rolld-border text-rolld-accent-light text-sm font-mono">
public/unity-build/Build/
</code>
</p>
</div>
<div className="glass rounded-xl p-4 text-left text-sm text-rolld-muted font-mono max-w-sm w-full">
<p className="text-rolld-accent-light mb-2">Fichiers requis :</p>
<p>├── nouveau_build.loader.js</p>
<p>├── nouveau_build.data</p>
<p>├── nouveau_build.framework.js</p>
<p>└── nouveau_build.wasm</p>
</div>
</div>
)}
{/* Checking state */}
{hasUnityBuild === null && (
<div className="flex flex-col items-center gap-4">
<div className="w-8 h-8 border-2 border-rolld-accent border-t-transparent rounded-full animate-spin" />
<p className="text-rolld-muted text-sm">Vérification du build…</p>
</div>
)}
{/* Error state */}
{error && (
<div className="flex flex-col items-center gap-4 text-center px-4">
<div className="text-5xl">⚠️</div>
<h2 className="text-xl font-bold text-red-400">Erreur de chargement</h2>
<p className="text-rolld-muted max-w-md text-sm">{error}</p>
<button
onClick={onBack}
className="px-4 py-2 rounded-lg bg-rolld-surface border border-rolld-border text-rolld-text hover:border-rolld-accent/40 transition-colors text-sm"
>
Retour à l'accueil
</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,95 @@
const gels = [
{
name: 'Gel Orange',
emoji: '🟠',
description: 'Surface de boost — multiplie votre vitesse. Prenez de l\'élan et catapultez-vous vers de nouveaux sommets.',
gradient: 'from-orange-500 to-amber-600',
borderColor: 'border-orange-500/30',
glowColor: 'hover:shadow-orange-500/20',
bgGlow: 'bg-orange-500/5',
},
{
name: 'Gel Violet',
emoji: '🟣',
description: 'Surface sticky — collez aux murs et plafonds. La gravité s\'inverse pour vous plaquer contre la surface.',
gradient: 'from-purple-500 to-violet-600',
borderColor: 'border-purple-500/30',
glowColor: 'hover:shadow-purple-500/20',
bgGlow: 'bg-purple-500/5',
},
{
name: 'Gel Bleu',
emoji: '🔵',
description: 'Surface rebondissante — transformez votre bille en super balle. Plus vous arrivez vite, plus vous rebondissez haut.',
gradient: 'from-blue-500 to-cyan-600',
borderColor: 'border-blue-500/30',
glowColor: 'hover:shadow-blue-500/20',
bgGlow: 'bg-blue-500/5',
},
]
export default function GelShowcase() {
return (
<section id="gels" className="py-24 px-4">
<div className="max-w-6xl mx-auto">
{/* Section header */}
<div className="text-center mb-16">
<h2 className="text-3xl md:text-5xl font-bold text-rolld-text mb-4">
Trois gels, infinies possibilités
</h2>
<p className="text-rolld-muted text-lg max-w-xl mx-auto">
Chaque surface change radicalement votre physique. Maîtrisez-les pour dominer l'arène.
</p>
</div>
{/* Gel cards */}
<div className="grid md:grid-cols-3 gap-6">
{gels.map((gel) => (
<div
key={gel.name}
className={`group relative rounded-2xl border ${gel.borderColor} bg-rolld-surface p-8 transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl ${gel.glowColor}`}
>
{/* Glow effect */}
<div className={`absolute inset-0 rounded-2xl ${gel.bgGlow} opacity-0 group-hover:opacity-100 transition-opacity duration-500`} />
<div className="relative z-10">
{/* Icon */}
<div className="text-5xl mb-4">{gel.emoji}</div>
{/* Title */}
<h3 className={`text-xl font-bold mb-3 bg-gradient-to-r ${gel.gradient} bg-clip-text text-transparent`}>
{gel.name}
</h3>
{/* Description */}
<p className="text-rolld-muted leading-relaxed text-sm">
{gel.description}
</p>
</div>
</div>
))}
</div>
{/* Controls hint */}
<div className="mt-16 glass rounded-2xl p-8 text-center">
<h3 className="text-xl font-semibold text-rolld-text mb-6">Contrôles</h3>
<div className="flex flex-wrap justify-center gap-6">
{[
{ keys: 'ZQSD', label: 'Mouvement' },
{ keys: 'ESPACE', label: 'Sauter (maintenir = +force)' },
{ keys: 'SOURIS', label: 'Caméra' },
{ keys: 'CLIC DROIT', label: 'Libérer le curseur' },
].map((control) => (
<div key={control.keys} className="flex flex-col items-center gap-2">
<kbd className="px-3 py-1.5 rounded-lg bg-rolld-bg border border-rolld-border text-rolld-accent-light font-mono text-sm">
{control.keys}
</kbd>
<span className="text-rolld-muted text-xs">{control.label}</span>
</div>
))}
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,83 @@
import { IS_DEV, theme } from '../env'
export default function Hero({ onPlay }) {
return (
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 overflow-hidden">
{/* Background effects */}
<div className="absolute inset-0 pointer-events-none">
{/* Gradient orbs */}
<div
className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full blur-[120px] animate-float"
style={{ background: `rgba(${theme.accentRgb}, 0.1)` }}
/>
<div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] rounded-full bg-rolld-orange/8 blur-[100px] animate-float" style={{ animationDelay: '-3s' }} />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] h-[300px] rounded-full bg-rolld-violet/8 blur-[80px]" />
{/* Grid */}
<div className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(rgba(${theme.accentRgb},0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(${theme.accentRgb},0.5) 1px, transparent 1px)`,
backgroundSize: '60px 60px',
}}
/>
</div>
{/* Content */}
<div className="relative z-10 text-center max-w-4xl mx-auto animate-slide-up">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass text-sm mb-8" style={{ color: theme.accentLight }}>
<span className="w-2 h-2 rounded-full animate-pulse" style={{ background: theme.accent }} />
{IS_DEV ? '🚧 DEV · ' : ''}Marble MMO · Multijoueur temps réel
</div>
{/* Title */}
<h1 className="text-6xl md:text-8xl font-black tracking-tight mb-6">
<span className="text-rolld-text">ROLL</span>
<span
className="text-transparent bg-clip-text"
style={{ backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo}, ${IS_DEV ? '#e67e22' : '#f39c12'})` }}
>'D</span>
</h1>
{/* Subtitle */}
<p className="text-lg md:text-xl text-rolld-muted max-w-2xl mx-auto mb-12 leading-relaxed">
Un monde de billes, de gels et de physique. Roulez sur des surfaces qui boostent,
collent ou font rebondir — et affrontez d'autres joueurs en temps réel.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<button
onClick={onPlay}
className="group relative px-8 py-4 rounded-2xl text-white font-bold text-lg transition-all duration-300 hover:scale-105"
style={{
backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo})`,
boxShadow: `0 0 30px rgba(${theme.accentRgb}, 0.4)`,
}}
>
<span className="relative z-10 flex items-center gap-3">
<svg className="w-6 h-6 group-hover:translate-x-0.5 transition-transform" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg>
Jouer maintenant
</span>
</button>
<a
href="#gels"
className="px-6 py-3 rounded-xl border border-rolld-border text-rolld-muted hover:text-rolld-text hover:border-rolld-accent/40 transition-all duration-300"
>
Découvrir les mécaniques
</a>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
<div className="w-5 h-8 rounded-full border-2 border-rolld-muted/30 flex items-start justify-center p-1">
<div className="w-1 h-2 rounded-full bg-rolld-muted/50" />
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,122 @@
export default function KerboulistanBanner() {
return (
<section className="relative py-20 px-4 overflow-hidden">
{/* Transition gradient — ROLL'D dark to gov gold tint and back */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-0 bg-gradient-to-b from-rolld-bg via-[#0d0c08] to-rolld-bg" />
{/* Subtle gold fog */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] rounded-full bg-amber-500/[0.04] blur-[100px]" />
{/* Scanlines — bureaucratic CRT vibe */}
<div
className="absolute inset-0 opacity-[0.015]"
style={{
backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(217,175,78,0.4) 2px, rgba(217,175,78,0.4) 3px)',
}}
/>
</div>
<div className="relative z-10 max-w-3xl mx-auto text-center">
{/* Classification header */}
<div className="inline-flex items-center gap-2 px-3 py-1 rounded border border-amber-700/30 bg-amber-900/10 mb-8">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500/80 animate-pulse" />
<span className="text-[10px] tracking-[0.2em] uppercase text-amber-500/60 font-mono">
Directive n°2026-042 · Programme homologué
</span>
</div>
{/* Government seal / emblem area */}
<div className="flex items-center justify-center gap-4 mb-6">
<div className="h-px flex-1 max-w-[80px] bg-gradient-to-r from-transparent to-amber-600/30" />
<a
href="https://gov.kerboul.me"
target="_blank"
rel="noopener noreferrer"
className="group relative"
>
{/* Seal ring */}
<div className="w-20 h-20 rounded-full border border-amber-600/30 group-hover:border-amber-500/50 flex items-center justify-center transition-all duration-500 group-hover:shadow-[0_0_30px_rgba(217,175,78,0.1)]">
<div className="w-16 h-16 rounded-full border border-amber-700/20 flex items-center justify-center">
<span className="text-2xl grayscale group-hover:grayscale-0 transition-all duration-500">🏛</span>
</div>
</div>
{/* Seal text ring (simulated) */}
<div className="absolute -inset-2 rounded-full border border-dashed border-amber-800/15 group-hover:border-amber-700/25 transition-colors duration-500 animate-[spin_60s_linear_infinite]" />
</a>
<div className="h-px flex-1 max-w-[80px] bg-gradient-to-l from-transparent to-amber-600/30" />
</div>
{/* Title — gov style */}
<h2 className="text-xs tracking-[0.3em] uppercase text-amber-500/50 font-medium mb-2">
République Libre Populaire Démocratique
</h2>
<h3 className="text-2xl md:text-3xl font-bold tracking-tight mb-2">
<span className="text-amber-100/80">du </span>
<a
href="https://gov.kerboul.me"
target="_blank"
rel="noopener noreferrer"
className="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-amber-600 hover:from-amber-300 hover:to-amber-500 transition-all duration-300"
>
Kerboulistan
</a>
</h3>
{/* Divider */}
<div className="flex items-center justify-center gap-3 my-6">
<div className="h-px w-12 bg-amber-700/30" />
<span className="text-amber-600/30 text-xs"></span>
<div className="h-px w-12 bg-amber-700/30" />
</div>
{/* Body text — bureaucratic tone */}
<p className="text-rolld-muted/70 text-sm leading-relaxed max-w-lg mx-auto mb-2">
Ce programme vidéoludique a été développé sous l'autorité directe du{' '}
<span className="text-amber-400/60">Couple Présidentiel</span> et financé par le{' '}
<span className="text-amber-400/60">Ministère de la Défense</span> dans le cadre de
la Directive de Divertissement Stratégique.
</p>
<p className="text-rolld-muted/40 text-xs leading-relaxed max-w-md mx-auto mb-8">
Budget alloué : 35 Memes ($K) · Temps de développement : classifié ·
Taux de satisfaction prévu : 420%
</p>
{/* Stamps / tags */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-8">
{[
{ label: 'Homologué RLPDK', icon: '🛡' },
{ label: 'Approuvé FBC', icon: '📡' },
{ label: 'MEMECON 5', icon: '🟢' },
].map((stamp) => (
<div
key={stamp.label}
className="flex items-center gap-1.5 px-3 py-1.5 rounded border border-amber-800/20 bg-amber-900/5 text-amber-500/40 text-[10px] tracking-[0.15em] uppercase font-mono"
>
<span className="text-xs">{stamp.icon}</span>
{stamp.label}
</div>
))}
</div>
{/* CTA — gov link */}
<a
href="https://gov.kerboul.me"
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-amber-700/25 hover:border-amber-600/40 bg-amber-900/5 hover:bg-amber-900/10 transition-all duration-300"
>
<span className="text-amber-500/50 group-hover:text-amber-400/70 text-xs tracking-[0.1em] uppercase transition-colors duration-300">
Consulter le Gouvernement
</span>
<svg className="w-3 h-3 text-amber-600/40 group-hover:text-amber-500/60 group-hover:translate-x-0.5 transition-all duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
{/* Bottom motto */}
<p className="mt-8 text-[9px] tracking-[0.25em] uppercase text-amber-700/25 font-mono">
« Endgame, Combined Arms, et Zougoulag ! »
</p>
</div>
</section>
)
}

31
frontend/src/env.js Normal file
View File

@@ -0,0 +1,31 @@
// Environment configuration
// Set VITE_ENV=dev in Coolify build args for the dev application
// Defaults to 'prod' if not set
export const ENV = import.meta.env.VITE_ENV || 'prod'
export const IS_DEV = ENV === 'dev'
export const IS_PROD = ENV === 'prod'
// Theme overrides per environment
export const envTheme = {
dev: {
accent: '#f39c12', // Orange
accentLight: '#f1c40f', // Yellow-orange
accentRgb: '243, 156, 18',
gradientFrom: '#f39c12',
gradientTo: '#e67e22',
label: 'DEV',
badge: '🚧',
},
prod: {
accent: '#6c5ce7', // Purple (default)
accentLight: '#a29bfe',
accentRgb: '108, 92, 231',
gradientFrom: '#6c5ce7',
gradientTo: '#9b59b6',
label: 'PROD',
badge: '🚀',
},
}
export const theme = envTheme[ENV] || envTheme.prod

43
frontend/src/index.css Normal file
View File

@@ -0,0 +1,43 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--rolld-accent: #6c5ce7;
--rolld-accent-light: #a29bfe;
--rolld-accent-rgb: 108, 92, 231;
}
body {
font-family: 'Inter', system-ui, sans-serif;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #0a0a0f;
}
::-webkit-scrollbar-thumb {
background: var(--rolld-accent);
border-radius: 3px;
}
}
@layer components {
.gel-gradient-orange {
background: linear-gradient(135deg, #f39c12, #e67e22);
}
.gel-gradient-violet {
background: linear-gradient(135deg, #9b59b6, #8e44ad);
}
.gel-gradient-blue {
background: linear-gradient(135deg, #3498db, #2980b9);
}
.glass {
background: rgba(18, 18, 26, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(var(--rolld-accent-rgb), 0.15);
}
}

10
frontend/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)