Initial commit with the now playing
This commit is contained in:
commit
de2f9cccb7
25 changed files with 2729 additions and 0 deletions
BIN
app/assets/web/art/album.png
Normal file
BIN
app/assets/web/art/album.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
app/assets/web/art/placeholder.png
Normal file
BIN
app/assets/web/art/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
125
app/assets/web/index.html
Normal file
125
app/assets/web/index.html
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Streamer Widgets</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
margin: 40px 20px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
header {
|
||||
background: #0f172a;
|
||||
color: white;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.content {
|
||||
padding: 32px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px;
|
||||
color: #1e293b;
|
||||
}
|
||||
.widget-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.widget-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.widget-name {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
text-decoration: none;
|
||||
}
|
||||
.widget-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.widget-url-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.footer {
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 16px 32px;
|
||||
background: #f8fafc;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
code {
|
||||
font-family: inherit;
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Streamer Widgets</h1>
|
||||
<div class="subtitle">Local OBS overlay server</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<h2>Available Widgets</h2>
|
||||
<ul class="widget-list">
|
||||
{{WIDGET_LIST}}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Server running at <code>{{HOSTPORT}}</code>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
194
app/assets/web/widgets/nowplaying/app.js
Normal file
194
app/assets/web/widgets/nowplaying/app.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
const card = document.getElementById("card");
|
||||
const artWrap = document.getElementById("artWrap");
|
||||
const artImg = document.getElementById("artImg");
|
||||
const bubbleContainer = document.getElementById("bubbleContainer");
|
||||
const bgBubbles = document.getElementById("bgBubbles");
|
||||
const bgImg = document.getElementById("bgImg");
|
||||
const titleEl = document.getElementById("title");
|
||||
const albumEl = document.getElementById("album");
|
||||
const artistEl = document.getElementById("artist");
|
||||
const pill = document.getElementById("statusPill");
|
||||
|
||||
let lastKey = "";
|
||||
let lastTitle = "";
|
||||
let lastAlbum = "";
|
||||
let lastArtist = "";
|
||||
|
||||
function createBubbles(container, count, scale = 1) {
|
||||
if (!container || container.childElementCount > 0) return;
|
||||
|
||||
const random = (min, max) => Math.random() * (max - min) + min;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const bubble = document.createElement("div");
|
||||
bubble.classList.add("bubble");
|
||||
|
||||
const size = random(12, 48) * scale;
|
||||
const left = random(0, 100);
|
||||
const duration = random(9, 18);
|
||||
const delay = random(-18, 0);
|
||||
const opacity = random(0.4, 0.85);
|
||||
const swayDistance = random(-24, 24) * scale;
|
||||
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
bubble.style.left = `${left}%`;
|
||||
bubble.style.animationDuration = `${duration}s`;
|
||||
bubble.style.animationDelay = `${delay}s`;
|
||||
bubble.style.setProperty("--bubble-opacity", opacity);
|
||||
bubble.style.setProperty("--sway-distance", `${swayDistance}px`);
|
||||
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
}
|
||||
|
||||
function initBubbles(){
|
||||
createBubbles(bubbleContainer, 16, 1);
|
||||
createBubbles(bgBubbles, 8, 2.5); // fewer bubbles, scaled up
|
||||
}
|
||||
|
||||
function safeText(s){
|
||||
return (s ?? "").toString().trim();
|
||||
}
|
||||
|
||||
function setTextWithMarquee(el, rawValue){
|
||||
const content = safeText(rawValue) || "—";
|
||||
|
||||
// Reset to plain text for accurate measurement
|
||||
el.classList.remove("marquee");
|
||||
el.innerHTML = content;
|
||||
|
||||
const needsMarquee = content !== "—" && el.scrollWidth > el.clientWidth + 2;
|
||||
if(needsMarquee){
|
||||
el.classList.add("marquee");
|
||||
// Calculate duration based on content length for consistent speed
|
||||
// e.g., 50px per second
|
||||
const width = el.scrollWidth;
|
||||
const duration = width / 50;
|
||||
|
||||
el.innerHTML = `<div class="marqueeInner" style="animation-duration:${duration}s"><span>${content}</span><span>${content}</span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyNowPlaying(data){
|
||||
try{
|
||||
|
||||
const title = safeText(data.title);
|
||||
const album = safeText(data.album);
|
||||
const artist = safeText(data.artist);
|
||||
const playing = !!data.playing;
|
||||
|
||||
// If nothing is active, hide the card
|
||||
const hasAny = title || album || artist;
|
||||
if(!hasAny){
|
||||
card.classList.add("hidden");
|
||||
lastTitle = "";
|
||||
lastAlbum = "";
|
||||
lastArtist = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${title}||${album}||${artist}||${playing}`;
|
||||
|
||||
// Update text with marquee when overflowing
|
||||
if(title !== lastTitle){
|
||||
setTextWithMarquee(titleEl, title);
|
||||
lastTitle = title;
|
||||
}
|
||||
if(album !== lastAlbum){
|
||||
setTextWithMarquee(albumEl, album);
|
||||
lastAlbum = album;
|
||||
}
|
||||
if(artist !== lastArtist){
|
||||
setTextWithMarquee(artistEl, artist);
|
||||
lastArtist = artist;
|
||||
}
|
||||
|
||||
// Update status
|
||||
pill.textContent = playing ? "Playing" : "Paused";
|
||||
pill.classList.toggle("playing", playing);
|
||||
|
||||
const hasArt = !!data.has_art;
|
||||
artWrap.classList.toggle("placeholder", !hasArt);
|
||||
card.classList.toggle("no-art", !hasArt);
|
||||
if(!hasArt){
|
||||
initBubbles();
|
||||
bgBubbles.classList.remove("hidden");
|
||||
bgImg.style.display = "none";
|
||||
} else {
|
||||
bgBubbles.classList.add("hidden");
|
||||
bgImg.style.display = "block";
|
||||
}
|
||||
|
||||
// If track changed, swap art with a cache-buster
|
||||
if(key !== lastKey){
|
||||
const artUrl = `${data.art_url}?v=${data.updated_unix}`;
|
||||
artImg.src = artUrl;
|
||||
bgImg.src = artUrl;
|
||||
|
||||
// small “pop-in” animation
|
||||
card.classList.add("hidden");
|
||||
requestAnimationFrame(() => {
|
||||
card.classList.remove("hidden");
|
||||
});
|
||||
|
||||
lastKey = key;
|
||||
} else {
|
||||
card.classList.remove("hidden");
|
||||
}
|
||||
} catch (e){
|
||||
// ignore render issues
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
// Clear cache to force re-evaluation of overflow
|
||||
lastTitle = "";
|
||||
lastAlbum = "";
|
||||
lastArtist = "";
|
||||
});
|
||||
|
||||
let ws = null;
|
||||
let retryMs = 500;
|
||||
|
||||
function wsUrl(){
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${proto}://${location.host}/ws`;
|
||||
}
|
||||
|
||||
function connect(){
|
||||
try{
|
||||
ws = new WebSocket(wsUrl());
|
||||
} catch (e){
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
retryMs = 500;
|
||||
};
|
||||
|
||||
ws.onmessage = (evt) => {
|
||||
try{
|
||||
const msg = JSON.parse(evt.data);
|
||||
if(msg?.type === "nowplaying"){
|
||||
applyNowPlaying(msg.data || {});
|
||||
}
|
||||
} catch (e){
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => scheduleReconnect();
|
||||
ws.onerror = () => {
|
||||
try{ ws.close(); } catch(e){}
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect(){
|
||||
const wait = retryMs;
|
||||
retryMs = Math.min(5000, retryMs * 1.5);
|
||||
setTimeout(connect, wait);
|
||||
}
|
||||
|
||||
connect();
|
||||
40
app/assets/web/widgets/nowplaying/index.html
Normal file
40
app/assets/web/widgets/nowplaying/index.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Now Playing</title>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div id="card" class="card hidden">
|
||||
<div class="art" id="artWrap">
|
||||
<div class="bubble-container" id="bubbleContainer"></div>
|
||||
<div class="bubble-overlay"></div>
|
||||
<img id="artImg" alt="Album art" />
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div id="title" class="title">—</div>
|
||||
<div class="sub">
|
||||
<span id="artist" class="artist">—</span>
|
||||
<span class="dot">•</span>
|
||||
<span id="album" class="album">—</span>
|
||||
</div>
|
||||
<div class="statusRow">
|
||||
<span id="statusPill" class="pill">Paused</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg">
|
||||
<div id="bgBubbles" class="bg-bubbles hidden"></div>
|
||||
<img id="bgImg" alt="" />
|
||||
</div>
|
||||
<div class="bgShade"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
294
app/assets/web/widgets/nowplaying/style.css
Normal file
294
app/assets/web/widgets/nowplaying/style.css
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
:root{
|
||||
/* Responsive sizing based on viewport width */
|
||||
--outer-pad: 16px;
|
||||
--min-w: 320px;
|
||||
--w: max(var(--min-w), calc(100vw - (var(--outer-pad) * 2)));
|
||||
--h: clamp(120px, calc(var(--w) * 0.22), 190px);
|
||||
--art: clamp(88px, calc(var(--w) * 0.15), 128px);
|
||||
|
||||
--radius: clamp(16px, calc(var(--w) * 0.03), 26px);
|
||||
/* Art corners slightly smaller than the card */
|
||||
--art-radius: max(12px, calc(var(--radius) - 6px));
|
||||
--pad: clamp(12px, calc(var(--w) * 0.02), 18px);
|
||||
--gap: clamp(10px, calc(var(--w) * 0.02), 18px);
|
||||
--blur: 18px;
|
||||
--shadow: 0 18px 50px rgba(0,0,0,.35);
|
||||
--stroke: rgba(255,255,255,.16);
|
||||
--text: rgba(255,255,255,.92);
|
||||
--muted: rgba(255,255,255,.70);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: inherit; }
|
||||
|
||||
.wrap{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
padding: var(--outer-pad);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card{
|
||||
position: relative;
|
||||
width: var(--w);
|
||||
height: var(--h);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--stroke);
|
||||
background: rgba(20,20,24,.40);
|
||||
backdrop-filter: blur(var(--blur));
|
||||
-webkit-backdrop-filter: blur(var(--blur));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap);
|
||||
padding: var(--pad);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
transition: opacity .25s ease, transform .25s ease;
|
||||
}
|
||||
|
||||
.card.hidden{
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.card.no-art{
|
||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.4), rgba(15, 23, 42, 0.6));
|
||||
border-color: rgba(56, 189, 248, 0.3);
|
||||
box-shadow:
|
||||
0 18px 50px rgba(0,0,0,.45),
|
||||
inset 0 0 20px rgba(56, 189, 248, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(24px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.2);
|
||||
}
|
||||
|
||||
.bg-bubbles{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
filter: blur(20px);
|
||||
transform: scale(1.2);
|
||||
background: linear-gradient(to bottom right, #0f172a, #1e40af, #0f172a);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.bg-bubbles.hidden{
|
||||
display: none;
|
||||
}
|
||||
.bg{
|
||||
position:absolute;
|
||||
inset:-20px;
|
||||
z-index:0;
|
||||
opacity: .55;
|
||||
filter: blur(26px) saturate(1.1);
|
||||
transform: scale(1.12);
|
||||
}
|
||||
.bg img{
|
||||
width:100%;
|
||||
height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bgShade{
|
||||
position:absolute;
|
||||
inset:0;
|
||||
z-index:0;
|
||||
background: radial-gradient(120% 160% at 20% 20%, rgba(255,255,255,.10), rgba(0,0,0,.55));
|
||||
}
|
||||
|
||||
.art{
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: var(--art);
|
||||
height: var(--art);
|
||||
border-radius: var(--art-radius);
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
background: rgba(0,0,0,.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.art img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity .25s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.meta{
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding-right: clamp(8px, calc(var(--w) * 0.02), 14px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title{
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
font-size: clamp(18px, calc(var(--h) * 0.16), 22px);
|
||||
letter-spacing: .2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
.sub{
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
display:flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.sub span.artist{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 0 1 auto; /* shrinking is ok, growing beyond content isn't forced */
|
||||
min-width: 0;
|
||||
max-width: 60%; /* prevent it from starving the album entirely if very long */
|
||||
}
|
||||
.sub span.album{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1; /* takes remaining space */
|
||||
min-width: 0;
|
||||
}
|
||||
.dot{
|
||||
opacity: .7;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.statusRow{
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.pill{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .25px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,.18);
|
||||
background: rgba(0,0,0,.22);
|
||||
color: rgba(255,255,255,.80);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pill.playing{
|
||||
background: rgba(0,0,0,.18);
|
||||
color: rgba(255,255,255,.90);
|
||||
}
|
||||
|
||||
.bubble-container,
|
||||
.bubble-overlay{
|
||||
position:absolute;
|
||||
inset:0;
|
||||
pointer-events:none;
|
||||
opacity:0;
|
||||
transition: opacity .25s ease;
|
||||
}
|
||||
.bubble-overlay{
|
||||
z-index:1;
|
||||
background: radial-gradient(circle at center, rgba(96,165,250,0.18), transparent 55%, transparent);
|
||||
}
|
||||
.bubble-container{
|
||||
z-index:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
.art.placeholder .bubble-container,
|
||||
.art.placeholder .bubble-overlay{
|
||||
opacity:1;
|
||||
}
|
||||
.art.placeholder{
|
||||
background: linear-gradient(to bottom right, #0f172a, #1e3a8a, #0f172a);
|
||||
}
|
||||
.art.placeholder img{
|
||||
opacity: 0;
|
||||
}
|
||||
.bubble{
|
||||
position:absolute;
|
||||
border-radius:50%;
|
||||
background: radial-gradient(circle at 30% 30%,
|
||||
rgba(255,255,255,0.9),
|
||||
rgba(56,189,248,0.6) 20%,
|
||||
rgba(37,99,235,0.3) 60%,
|
||||
transparent 80%);
|
||||
box-shadow: 0 0 10px rgba(56,189,248,0.4), inset 0 0 10px rgba(56,189,248,0.2);
|
||||
animation: float-up linear infinite;
|
||||
filter: blur(1px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Marquee for overflowing text */
|
||||
.marquee{
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
.marquee .marqueeInner{
|
||||
display: inline-flex;
|
||||
gap: 32px;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
animation: marquee linear infinite;
|
||||
}
|
||||
.marquee .marqueeInner span{
|
||||
white-space: nowrap;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
@keyframes marquee{
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(calc(-50% - 16px)); }
|
||||
}
|
||||
|
||||
@keyframes float-up{
|
||||
0% {
|
||||
top: 110%;
|
||||
transform: translateX(0) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: var(--bubble-opacity);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(var(--sway-distance)) scale(1.2);
|
||||
}
|
||||
90% {
|
||||
opacity: var(--bubble-opacity);
|
||||
}
|
||||
100% {
|
||||
top: -20%;
|
||||
transform: translateX(0) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue