590 lines
26 KiB
Vue
590 lines
26 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
|
import { Link } from '@inertiajs/vue3';
|
|
import {
|
|
Search, X, Clock, Gamepad2, AtSign, Layers,
|
|
Play, ChevronRight, Loader2, SearchX, UserX, Sparkles
|
|
} from 'lucide-vue-next';
|
|
|
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
|
|
const query = ref('');
|
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
const gameResults = ref<any[]>([]);
|
|
const userResults = ref<any[]>([]);
|
|
const providerResults = ref<any[]>([]);
|
|
const allGames = ref<any[]>([]);
|
|
const loading = ref(false);
|
|
|
|
const mode = computed(() => {
|
|
const q = query.value;
|
|
if (q.startsWith('@')) return 'users';
|
|
if (q.toLowerCase().startsWith('p:') || q.toLowerCase().startsWith('p ')) return 'providers';
|
|
return 'games';
|
|
});
|
|
|
|
const searchTerm = computed(() => {
|
|
if (mode.value === 'users') return query.value.slice(1).trim();
|
|
if (mode.value === 'providers') return query.value.slice(2).trim();
|
|
return query.value.trim();
|
|
});
|
|
|
|
onMounted(async () => {
|
|
nextTick(() => inputRef.value?.focus());
|
|
try {
|
|
const res = await fetch('/api/games');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const list = Array.isArray(data) ? data : (data?.games || data?.items || []);
|
|
allGames.value = list.map((g: any, idx: number) => ({
|
|
slug: g.slug ?? g.id ?? String(idx),
|
|
name: g.name ?? g.title ?? `Game ${idx}`,
|
|
provider: g.provider ?? 'BetiX',
|
|
image: g.image ?? g.thumbnail ?? '',
|
|
type: g.type ?? 'slot',
|
|
}));
|
|
}
|
|
} catch {}
|
|
});
|
|
|
|
let debounceTimer: any;
|
|
watch(query, () => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => doSearch(), 250);
|
|
});
|
|
|
|
async function doSearch() {
|
|
const term = searchTerm.value;
|
|
if (!term) {
|
|
gameResults.value = [];
|
|
userResults.value = [];
|
|
providerResults.value = [];
|
|
return;
|
|
}
|
|
|
|
if (mode.value === 'games') {
|
|
const lc = term.toLowerCase();
|
|
gameResults.value = allGames.value.filter(g => g.name.toLowerCase().includes(lc)).slice(0, 12);
|
|
providerResults.value = [];
|
|
userResults.value = [];
|
|
} else if (mode.value === 'providers') {
|
|
const lc = term.toLowerCase();
|
|
const map: Record<string, any[]> = {};
|
|
for (const g of allGames.value) {
|
|
if (g.provider.toLowerCase().includes(lc)) {
|
|
if (!map[g.provider]) map[g.provider] = [];
|
|
map[g.provider].push(g);
|
|
}
|
|
}
|
|
providerResults.value = Object.entries(map).slice(0, 8).map(([name, games]) => ({ name, count: games.length }));
|
|
gameResults.value = [];
|
|
userResults.value = [];
|
|
} else if (mode.value === 'users') {
|
|
if (term.length < 1) { userResults.value = []; return; }
|
|
loading.value = true;
|
|
try {
|
|
const res = await fetch(`/api/users/search?q=${encodeURIComponent(term)}`);
|
|
if (res.ok) userResults.value = (await res.json()).slice(0, 8);
|
|
} catch {}
|
|
loading.value = false;
|
|
gameResults.value = [];
|
|
providerResults.value = [];
|
|
}
|
|
}
|
|
|
|
// Recent searches
|
|
const RECENT_KEY = 'betix_recent_searches';
|
|
const recentSearches = ref<string[]>([]);
|
|
onMounted(() => {
|
|
try { recentSearches.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch {}
|
|
});
|
|
|
|
function addRecent(term: string) {
|
|
const list = [term, ...recentSearches.value.filter(r => r !== term)].slice(0, 6);
|
|
recentSearches.value = list;
|
|
try { localStorage.setItem(RECENT_KEY, JSON.stringify(list)); } catch {}
|
|
}
|
|
|
|
function clearRecent() {
|
|
recentSearches.value = [];
|
|
try { localStorage.removeItem(RECENT_KEY); } catch {}
|
|
}
|
|
|
|
function applyRecent(term: string) { query.value = term; }
|
|
|
|
// Keyboard
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') emit('close');
|
|
}
|
|
onMounted(() => document.addEventListener('keydown', handleKeydown));
|
|
onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
|
|
|
function playGame(slug: string, name: string, provider?: string) {
|
|
addRecent(name);
|
|
emit('close');
|
|
const prov = (provider ?? 'betix').toLowerCase().replace(/\s+/g, '-');
|
|
window.location.href = `/games/play/${encodeURIComponent(prov)}/${encodeURIComponent(slug)}`;
|
|
}
|
|
|
|
function goProfile(username: string) {
|
|
addRecent('@' + username);
|
|
emit('close');
|
|
}
|
|
|
|
function goProvider(name: string) {
|
|
addRecent('P:' + name);
|
|
emit('close');
|
|
window.location.href = `/dashboard?provider=${encodeURIComponent(name)}`;
|
|
}
|
|
|
|
// Quick shortcuts (shown on empty state)
|
|
const shortcuts = [
|
|
{ label: '@Username', hint: 'Spieler suchen', prefix: '@', icon: AtSign },
|
|
{ label: 'P:Provider', hint: 'Anbieter suchen', prefix: 'P:', icon: Layers },
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<teleport to="body">
|
|
<div class="sm-overlay" @click.self="$emit('close')">
|
|
<div class="sm-modal">
|
|
|
|
<!-- Search input row -->
|
|
<div class="sm-input-row">
|
|
<div class="sm-input-wrap">
|
|
<Search :size="18" class="sm-search-icon" />
|
|
<input
|
|
ref="inputRef"
|
|
v-model="query"
|
|
class="sm-input"
|
|
placeholder="Spiele, @User, P:Provider..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
<button v-if="query" class="sm-clear-btn" @click="query = ''" title="Löschen">
|
|
<X :size="15" />
|
|
</button>
|
|
</div>
|
|
<button class="sm-esc-btn" @click="$emit('close')">
|
|
<kbd>ESC</kbd>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mode tabs -->
|
|
<div class="sm-tabs">
|
|
<button class="sm-tab" :class="{ active: mode === 'games' }" @click="query = query.startsWith('@') || query.toLowerCase().startsWith('p') ? '' : query">
|
|
<Gamepad2 :size="13" /> Spiele
|
|
</button>
|
|
<button class="sm-tab" :class="{ active: mode === 'users' }" @click="query = '@'">
|
|
<AtSign :size="13" /> @Spieler
|
|
</button>
|
|
<button class="sm-tab" :class="{ active: mode === 'providers' }" @click="query = 'P:'">
|
|
<Layers :size="13" /> Provider
|
|
</button>
|
|
<div class="sm-tab-indicator" :style="{ left: mode === 'games' ? '4px' : mode === 'users' ? 'calc(33.3% + 2px)' : 'calc(66.6% + 2px)', width: 'calc(33.3% - 4px)' }"></div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="sm-body">
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="!query || (!searchTerm && mode !== 'games')">
|
|
|
|
<!-- Recent searches -->
|
|
<div v-if="recentSearches.length" class="sm-block">
|
|
<div class="sm-block-head">
|
|
<span><Clock :size="12" /> Zuletzt gesucht</span>
|
|
<button class="sm-text-btn" @click="clearRecent">Löschen</button>
|
|
</div>
|
|
<div class="sm-tags">
|
|
<button
|
|
v-for="r in recentSearches"
|
|
:key="r"
|
|
class="sm-tag"
|
|
@click="applyRecent(r)"
|
|
>{{ r }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shortcuts -->
|
|
<div class="sm-block">
|
|
<div class="sm-block-head"><span><Sparkles :size="12" /> Schnellsuche</span></div>
|
|
<div class="sm-shortcuts">
|
|
<button
|
|
v-for="s in shortcuts"
|
|
:key="s.prefix"
|
|
class="sm-shortcut"
|
|
@click="query = s.prefix"
|
|
>
|
|
<component :is="s.icon" :size="15" class="sm-sc-icon" />
|
|
<div>
|
|
<div class="sm-sc-label">{{ s.label }}</div>
|
|
<div class="sm-sc-hint">{{ s.hint }}</div>
|
|
</div>
|
|
<ChevronRight :size="14" class="sm-sc-arrow" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty hint -->
|
|
<div v-if="!recentSearches.length" class="sm-empty">
|
|
<Search :size="32" class="sm-empty-icon" />
|
|
<p>Tippe um zu suchen</p>
|
|
<small>Spiele, Spieler oder Provider</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Game results -->
|
|
<div v-if="mode === 'games' && searchTerm && gameResults.length" class="sm-block">
|
|
<div class="sm-block-head">
|
|
<span><Gamepad2 :size="12" /> Spiele</span>
|
|
<span class="sm-count">{{ gameResults.length }} Treffer</span>
|
|
</div>
|
|
<div class="sm-games-grid">
|
|
<button
|
|
v-for="g in gameResults"
|
|
:key="g.slug"
|
|
class="sm-game"
|
|
@click="playGame(g.slug, g.name, g.provider)"
|
|
>
|
|
<div class="sg-thumb" :style="g.image ? { backgroundImage: `url(${g.image})` } : {}">
|
|
<span v-if="!g.image" class="sg-letter">{{ g.name[0] }}</span>
|
|
<div class="sg-overlay"><Play :size="18" /></div>
|
|
</div>
|
|
<div class="sg-name">{{ g.name }}</div>
|
|
<div class="sg-prov">{{ g.provider }}</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No game results -->
|
|
<div v-if="mode === 'games' && searchTerm && !gameResults.length" class="sm-no-results">
|
|
<SearchX :size="36" class="sm-nr-icon" />
|
|
<p>Keine Spiele für <strong>"{{ searchTerm }}"</strong></p>
|
|
</div>
|
|
|
|
<!-- Provider results -->
|
|
<div v-if="mode === 'providers' && searchTerm && providerResults.length" class="sm-block">
|
|
<div class="sm-block-head"><span><Layers :size="12" /> Provider</span></div>
|
|
<div class="sm-providers">
|
|
<button
|
|
v-for="p in providerResults"
|
|
:key="p.name"
|
|
class="sm-provider"
|
|
@click="goProvider(p.name)"
|
|
>
|
|
<div class="sp-icon"><Layers :size="18" /></div>
|
|
<div class="sp-info">
|
|
<div class="sp-name">{{ p.name }}</div>
|
|
<div class="sp-count">{{ p.count }} Spiele</div>
|
|
</div>
|
|
<ChevronRight :size="15" class="sp-arrow" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No provider results -->
|
|
<div v-if="mode === 'providers' && searchTerm && !providerResults.length" class="sm-no-results">
|
|
<SearchX :size="36" class="sm-nr-icon" />
|
|
<p>Keine Provider für <strong>"{{ searchTerm }}"</strong></p>
|
|
</div>
|
|
|
|
<!-- User results -->
|
|
<div v-if="mode === 'users' && searchTerm && userResults.length" class="sm-block">
|
|
<div class="sm-block-head"><span><AtSign :size="12" /> Spieler</span></div>
|
|
<div class="sm-users">
|
|
<Link
|
|
v-for="u in userResults"
|
|
:key="u.id"
|
|
:href="`/profile/${u.username}`"
|
|
class="sm-user"
|
|
@click="goProfile(u.username)"
|
|
>
|
|
<div class="su-avatar">
|
|
<img v-if="u.avatar || u.avatar_url" :src="u.avatar || u.avatar_url" alt="" />
|
|
<span v-else>{{ u.username[0].toUpperCase() }}</span>
|
|
</div>
|
|
<div class="su-info">
|
|
<div class="su-name">{{ u.username }}</div>
|
|
<div class="su-vip">VIP {{ u.vip_level ?? 0 }}</div>
|
|
</div>
|
|
<ChevronRight :size="14" class="su-arrow" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading users -->
|
|
<div v-if="mode === 'users' && loading" class="sm-loading">
|
|
<Loader2 :size="24" class="sm-spin" />
|
|
</div>
|
|
|
|
<!-- No user results -->
|
|
<div v-if="mode === 'users' && searchTerm && !loading && !userResults.length" class="sm-no-results">
|
|
<UserX :size="36" class="sm-nr-icon" />
|
|
<p>Keine Spieler gefunden</p>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Footer hint -->
|
|
<div class="sm-footer">
|
|
<span><kbd>↑↓</kbd> Navigieren</span>
|
|
<span><kbd>↵</kbd> Öffnen</span>
|
|
<span><kbd>ESC</kbd> Schließen</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Overlay ─────────────────────────────────────────────── */
|
|
.sm-overlay {
|
|
position: fixed; inset: 0;
|
|
background: rgba(0,0,0,.7);
|
|
backdrop-filter: blur(8px);
|
|
z-index: 9999;
|
|
display: flex; align-items: flex-start; justify-content: center;
|
|
padding-top: clamp(48px, 9vh, 110px);
|
|
animation: sm-overlay-in .15s ease;
|
|
}
|
|
@keyframes sm-overlay-in { from { opacity: 0; } to { opacity: 1; } }
|
|
|
|
/* ── Modal ───────────────────────────────────────────────── */
|
|
.sm-modal {
|
|
width: min(700px, calc(100vw - 32px));
|
|
background: #0b0b0e;
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
box-shadow:
|
|
0 0 0 1px rgba(255,255,255,.04),
|
|
0 40px 100px rgba(0,0,0,.9),
|
|
0 0 60px rgba(223,0,106,.05);
|
|
animation: sm-modal-in .22s cubic-bezier(.16,1,.3,1);
|
|
}
|
|
@keyframes sm-modal-in {
|
|
from { opacity: 0; transform: translateY(-18px) scale(.97); }
|
|
to { opacity: 1; transform: none; }
|
|
}
|
|
|
|
/* ── Input row ───────────────────────────────────────────── */
|
|
.sm-input-row {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 14px 16px 14px 20px;
|
|
border-bottom: 1px solid rgba(255,255,255,.05);
|
|
}
|
|
.sm-input-wrap {
|
|
flex: 1; display: flex; align-items: center; gap: 12px;
|
|
background: rgba(255,255,255,.04);
|
|
border: 1px solid rgba(255,255,255,.07);
|
|
border-radius: 12px; padding: 0 12px;
|
|
transition: border-color .2s, box-shadow .2s;
|
|
}
|
|
.sm-input-wrap:focus-within {
|
|
border-color: rgba(223,0,106,.35);
|
|
box-shadow: 0 0 0 3px rgba(223,0,106,.08);
|
|
}
|
|
.sm-search-icon { color: #555; flex-shrink: 0; transition: color .2s; }
|
|
.sm-input-wrap:focus-within .sm-search-icon { color: var(--primary, #df006a); }
|
|
.sm-input {
|
|
flex: 1; background: transparent; border: none; outline: none;
|
|
color: #fff; font-size: 15px; font-weight: 500; font-family: inherit;
|
|
height: 46px; min-width: 0;
|
|
}
|
|
.sm-input::placeholder { color: #333; }
|
|
.sm-clear-btn {
|
|
background: rgba(255,255,255,.06); border: none; cursor: pointer;
|
|
color: #666; border-radius: 8px; width: 28px; height: 28px;
|
|
display: flex; align-items: center; justify-content: center; transition: .15s;
|
|
}
|
|
.sm-clear-btn:hover { color: #fff; background: rgba(255,255,255,.1); }
|
|
.sm-esc-btn {
|
|
background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.09);
|
|
border-radius: 8px; padding: 4px 10px; cursor: pointer; transition: .15s; flex-shrink: 0;
|
|
}
|
|
.sm-esc-btn kbd { font-size: 11px; color: #555; font-family: monospace; letter-spacing: .5px; }
|
|
.sm-esc-btn:hover kbd { color: #aaa; }
|
|
|
|
/* ── Tabs ────────────────────────────────────────────────── */
|
|
.sm-tabs {
|
|
display: flex; position: relative;
|
|
border-bottom: 1px solid rgba(255,255,255,.05);
|
|
padding: 6px 8px;
|
|
gap: 0;
|
|
}
|
|
.sm-tab {
|
|
flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
height: 34px; background: none; border: none; cursor: pointer;
|
|
font-size: 12px; font-weight: 700; color: #444;
|
|
border-radius: 8px; transition: color .2s; position: relative; z-index: 1;
|
|
letter-spacing: .5px;
|
|
}
|
|
.sm-tab.active { color: #fff; }
|
|
.sm-tab-indicator {
|
|
position: absolute; bottom: 6px; height: 34px;
|
|
background: rgba(255,255,255,.07);
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 8px;
|
|
transition: left .25s cubic-bezier(.16,1,.3,1);
|
|
z-index: 0;
|
|
}
|
|
|
|
/* ── Body ────────────────────────────────────────────────── */
|
|
.sm-body {
|
|
padding: 14px; max-height: 58vh; overflow-y: auto;
|
|
scrollbar-width: thin; scrollbar-color: #222 transparent;
|
|
}
|
|
.sm-body::-webkit-scrollbar { width: 4px; }
|
|
.sm-body::-webkit-scrollbar-thumb { background: #222; border-radius: 2px; }
|
|
|
|
/* ── Block ───────────────────────────────────────────────── */
|
|
.sm-block { margin-bottom: 18px; }
|
|
.sm-block-head {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
}
|
|
.sm-block-head > span {
|
|
display: flex; align-items: center; gap: 5px;
|
|
font-size: 10px; font-weight: 800; color: #444;
|
|
text-transform: uppercase; letter-spacing: 1px;
|
|
}
|
|
.sm-count { font-size: 10px; color: #333; font-weight: 700; }
|
|
.sm-text-btn { background: none; border: none; color: #444; font-size: 11px; cursor: pointer; font-weight: 700; transition: color .15s; }
|
|
.sm-text-btn:hover { color: #888; }
|
|
|
|
/* ── Recent tags ─────────────────────────────────────────── */
|
|
.sm-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.sm-tag {
|
|
background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.08);
|
|
color: #777; padding: 5px 12px; border-radius: 20px;
|
|
font-size: 12px; font-weight: 600; cursor: pointer; transition: .15s;
|
|
}
|
|
.sm-tag:hover { background: rgba(223,0,106,.1); border-color: rgba(223,0,106,.25); color: #fff; }
|
|
|
|
/* ── Shortcuts ───────────────────────────────────────────── */
|
|
.sm-shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.sm-shortcut {
|
|
display: flex; align-items: center; gap: 10px; padding: 10px 12px;
|
|
background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.06);
|
|
border-radius: 10px; cursor: pointer; transition: .15s; text-align: left;
|
|
}
|
|
.sm-shortcut:hover { border-color: rgba(223,0,106,.2); background: rgba(223,0,106,.04); }
|
|
.sm-sc-icon { color: var(--primary, #df006a); flex-shrink: 0; }
|
|
.sm-sc-label { font-size: 12px; font-weight: 700; color: #aaa; }
|
|
.sm-sc-hint { font-size: 11px; color: #444; margin-top: 1px; }
|
|
.sm-sc-arrow { color: #333; margin-left: auto; flex-shrink: 0; }
|
|
|
|
/* ── Empty state ─────────────────────────────────────────── */
|
|
.sm-empty { text-align: center; padding: 32px 0 16px; color: #333; }
|
|
.sm-empty-icon { margin: 0 auto 12px; opacity: .3; }
|
|
.sm-empty p { font-size: 14px; font-weight: 600; color: #444; margin: 0 0 4px; }
|
|
.sm-empty small { font-size: 12px; color: #2a2a2a; }
|
|
|
|
/* ── Games grid ──────────────────────────────────────────── */
|
|
.sm-games-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 8px;
|
|
}
|
|
.sm-game {
|
|
background: none; border: 1px solid rgba(255,255,255,.05);
|
|
border-radius: 10px; overflow: hidden; cursor: pointer;
|
|
transition: border-color .15s, transform .15s; text-align: left;
|
|
padding: 0;
|
|
}
|
|
.sm-game:hover { border-color: rgba(223,0,106,.3); transform: translateY(-2px); }
|
|
|
|
.sg-thumb {
|
|
width: 100%; aspect-ratio: 4/3;
|
|
background: #1a1a1e; background-size: cover; background-position: center;
|
|
position: relative; display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.sg-letter { font-size: 24px; font-weight: 900; color: #333; }
|
|
.sg-overlay {
|
|
position: absolute; inset: 0; background: rgba(0,0,0,.6);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--primary, #df006a); opacity: 0; transition: opacity .15s;
|
|
}
|
|
.sm-game:hover .sg-overlay { opacity: 1; }
|
|
|
|
.sg-name {
|
|
font-size: 11px; font-weight: 700; color: #ccc;
|
|
padding: 6px 8px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.sg-prov { font-size: 10px; color: #444; padding: 0 8px 6px; }
|
|
|
|
/* ── Providers ───────────────────────────────────────────── */
|
|
.sm-providers { display: flex; flex-direction: column; gap: 4px; }
|
|
.sm-provider {
|
|
display: flex; align-items: center; gap: 12px; padding: 10px 12px;
|
|
background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.05);
|
|
border-radius: 10px; cursor: pointer; transition: .15s; text-align: left; width: 100%;
|
|
}
|
|
.sm-provider:hover { border-color: rgba(223,0,106,.25); background: rgba(223,0,106,.04); }
|
|
.sp-icon { color: #444; }
|
|
.sm-provider:hover .sp-icon { color: var(--primary, #df006a); }
|
|
.sp-info { flex: 1; }
|
|
.sp-name { font-size: 14px; font-weight: 700; color: #ddd; }
|
|
.sp-count { font-size: 11px; color: #555; margin-top: 1px; }
|
|
.sp-arrow { color: #333; }
|
|
|
|
/* ── Users ───────────────────────────────────────────────── */
|
|
.sm-users { display: flex; flex-direction: column; gap: 3px; }
|
|
.sm-user {
|
|
display: flex; align-items: center; gap: 12px; padding: 8px 10px;
|
|
border-radius: 10px; text-decoration: none; transition: background .15s;
|
|
}
|
|
.sm-user:hover { background: rgba(255,255,255,.04); }
|
|
.su-avatar {
|
|
width: 38px; height: 38px; border-radius: 50%;
|
|
background: #1a1a1e; overflow: hidden; flex-shrink: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-weight: 900; color: #444; font-size: 16px;
|
|
border: 1px solid rgba(255,255,255,.08);
|
|
}
|
|
.su-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
.su-info { flex: 1; }
|
|
.su-name { font-size: 14px; font-weight: 700; color: #ddd; }
|
|
.su-vip { font-size: 11px; color: #444; margin-top: 1px; }
|
|
.su-arrow { color: #333; }
|
|
|
|
/* ── Loading ─────────────────────────────────────────────── */
|
|
.sm-loading { display: flex; justify-content: center; padding: 24px; }
|
|
.sm-spin { color: var(--primary, #df006a); animation: spin .8s linear infinite; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* ── No results ──────────────────────────────────────────── */
|
|
.sm-no-results { text-align: center; padding: 32px 0; }
|
|
.sm-nr-icon { color: #2a2a2a; margin: 0 auto 12px; }
|
|
.sm-no-results p { font-size: 13px; color: #444; }
|
|
.sm-no-results strong { color: #666; }
|
|
|
|
/* ── Footer ──────────────────────────────────────────────── */
|
|
.sm-footer {
|
|
display: flex; align-items: center; gap: 16px; justify-content: center;
|
|
padding: 10px 16px;
|
|
border-top: 1px solid rgba(255,255,255,.04);
|
|
background: rgba(0,0,0,.3);
|
|
}
|
|
.sm-footer span {
|
|
display: flex; align-items: center; gap: 5px;
|
|
font-size: 11px; color: #333;
|
|
}
|
|
.sm-footer kbd {
|
|
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 4px; padding: 1px 5px; font-family: monospace;
|
|
font-size: 10px; color: #555;
|
|
}
|
|
|
|
/* ── Mobile ──────────────────────────────────────────────── */
|
|
@media (max-width: 480px) {
|
|
.sm-overlay { align-items: flex-end; padding-top: 0; }
|
|
.sm-modal { border-radius: 20px 20px 0 0; }
|
|
.sm-footer { display: none; }
|
|
.sm-games-grid { grid-template-columns: repeat(3, 1fr); }
|
|
.sm-shortcuts { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|