skeleton loader
This commit is contained in:
parent
276a266977
commit
6d38676a1b
@ -55,6 +55,10 @@ const zone = fmt({ timeZoneName: "short" }).find((p) => p.type === "timeZoneName
|
|||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
|
.clock__mer {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.8ch;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="music" id="music">
|
<div class="music" id="music" data-state="loading">
|
||||||
|
<span class="chip-skel chip-skel--music" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
class="music__trigger"
|
class="music__trigger"
|
||||||
type="button"
|
type="button"
|
||||||
@ -21,6 +22,7 @@ import { Icon } from "astro-icon/components";
|
|||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
<span class="music__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
|
<span class="music__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
|
||||||
|
<Icon class="music__logo" name="ph:spotify-logo" aria-hidden="true" />
|
||||||
<span class="music__main">
|
<span class="music__main">
|
||||||
<span class="music__label" id="music-label">now playing</span>
|
<span class="music__label" id="music-label">now playing</span>
|
||||||
<span class="music__sub" id="music-sub" aria-hidden="true"></span>
|
<span class="music__sub" id="music-sub" aria-hidden="true"></span>
|
||||||
@ -78,9 +80,6 @@ import { Icon } from "astro-icon/components";
|
|||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
|
|
||||||
.music {
|
|
||||||
margin-top: var(--gap);
|
|
||||||
}
|
|
||||||
.music__trigger {
|
.music__trigger {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -135,6 +134,31 @@ import { Icon } from "astro-icon/components";
|
|||||||
flex: none;
|
flex: none;
|
||||||
padding: 0 0.1rem;
|
padding: 0 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.music__logo {
|
||||||
|
display: none;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
flex: none;
|
||||||
|
color: var(--faint);
|
||||||
|
}
|
||||||
|
[data-state="idle"] .music__logo {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
[data-state="idle"] .music__art,
|
||||||
|
[data-state="idle"] .music__eq,
|
||||||
|
[data-state="idle"] .music__arw,
|
||||||
|
[data-state="idle"] .music__sub,
|
||||||
|
[data-state="idle"] .music__bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[data-state="idle"] .music__trigger {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--faint);
|
||||||
|
}
|
||||||
|
[data-state="idle"] .music__trigger:hover {
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
.music__eq i {
|
.music__eq i {
|
||||||
width: 2.5px;
|
width: 2.5px;
|
||||||
height: 28%;
|
height: 28%;
|
||||||
@ -216,6 +240,8 @@ import { Icon } from "astro-icon/components";
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.music__arw {
|
.music__arw {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.8ch;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@ -508,7 +534,7 @@ import { Icon } from "astro-icon/components";
|
|||||||
const openWall = hashDialog({
|
const openWall = hashDialog({
|
||||||
name: "music",
|
name: "music",
|
||||||
dialog,
|
dialog,
|
||||||
canOpen: () => !!open && !open.hidden,
|
canOpen: () => !!open && !open.hidden && !open.disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
open?.addEventListener("click", () => (openWall ? openWall() : dialog?.showModal()));
|
open?.addEventListener("click", () => (openWall ? openWall() : dialog?.showModal()));
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="reading" id="books">
|
<div class="reading" id="books" data-state="loading">
|
||||||
|
<span class="chip-skel chip-skel--reading" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
class="reading__trigger"
|
class="reading__trigger"
|
||||||
type="button"
|
type="button"
|
||||||
@ -173,6 +174,8 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.reading__arw {
|
.reading__arw {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.8ch;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="stats" id="stats">
|
<div class="stats" id="stats" data-state="loading">
|
||||||
|
<span class="chip-skel chip-skel--stats" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
class="stats__trigger"
|
class="stats__trigger"
|
||||||
type="button"
|
type="button"
|
||||||
@ -102,6 +103,8 @@ import { Icon } from "astro-icon/components";
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.stats__unit {
|
.stats__unit {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.8ch;
|
||||||
font-size: 0.62em;
|
font-size: 0.62em;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
@ -284,13 +287,17 @@ import { Icon } from "astro-icon/components";
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<boolean> {
|
||||||
let d: any;
|
let d: any;
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/api/stats.json", { headers: { accept: "application/json" } });
|
const mm = (window as any).__mm?.stats as Promise<any> | undefined;
|
||||||
d = await r.json();
|
d = mm
|
||||||
|
? await mm
|
||||||
|
: await fetch("/api/stats.json", { headers: { accept: "application/json" } }).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(!d || !d.configured || (!d.pageviews && !d.visitors)) &&
|
(!d || !d.configured || (!d.pageviews && !d.visitors)) &&
|
||||||
@ -311,11 +318,11 @@ import { Icon } from "astro-icon/components";
|
|||||||
{ ref: "news.ycombinator.com", count: 880 },
|
{ ref: "news.ycombinator.com", count: 880 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
if (!d || !d.configured || (!d.pageviews && !d.visitors)) return;
|
if (!d || !d.configured || (!d.pageviews && !d.visitors)) return false;
|
||||||
|
|
||||||
const trigger = document.querySelector<HTMLButtonElement>(".stats__trigger");
|
const trigger = document.querySelector<HTMLButtonElement>(".stats__trigger");
|
||||||
const reel = $("stats-reel");
|
const reel = $("stats-reel");
|
||||||
if (!trigger || !reel) return;
|
if (!trigger || !reel) return false;
|
||||||
|
|
||||||
const pv = Math.max(0, Math.round(Number(d.pageviews) || 0));
|
const pv = Math.max(0, Math.round(Number(d.pageviews) || 0));
|
||||||
reel.textContent = fmt(pv);
|
reel.textContent = fmt(pv);
|
||||||
@ -344,6 +351,7 @@ import { Icon } from "astro-icon/components";
|
|||||||
);
|
);
|
||||||
$("sb-refs-block")!.hidden = false;
|
$("sb-refs-block")!.hidden = false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = document.querySelector<HTMLDialogElement>("[data-stats-dialog]");
|
const dialog = document.querySelector<HTMLDialogElement>("[data-stats-dialog]");
|
||||||
@ -362,5 +370,8 @@ import { Icon } from "astro-icon/components";
|
|||||||
if (e.target === dialog) dialog.close();
|
if (e.target === dialog) dialog.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
load();
|
const host = document.getElementById("stats");
|
||||||
|
load()
|
||||||
|
.then((shown) => host?.setAttribute("data-state", shown ? "ready" : "empty"))
|
||||||
|
.catch(() => host?.setAttribute("data-state", "empty"));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -37,6 +37,14 @@ const kaoCount = kaoMs > 0 ? `${Math.floor(kaoMs / 86_400_000)}d` : "in effect";
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark" />
|
||||||
<meta name="theme-color" content="#1b1a18" />
|
<meta name="theme-color" content="#1b1a18" />
|
||||||
|
{
|
||||||
|
path === "/" && (
|
||||||
|
<>
|
||||||
|
<link rel="preconnect" href="https://assets.hardcover.app" crossorigin />
|
||||||
|
<link rel="preconnect" href="https://i.scdn.co" crossorigin />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
try {
|
try {
|
||||||
if (localStorage.getItem("kao-ribbon") === "off")
|
if (localStorage.getItem("kao-ribbon") === "off")
|
||||||
|
|||||||
@ -90,7 +90,10 @@ export const GET: APIRoute = async () => {
|
|||||||
const json = (data: unknown, status = 200) =>
|
const json = (data: unknown, status = 200) =>
|
||||||
new Response(JSON.stringify(data), {
|
new Response(JSON.stringify(data), {
|
||||||
status,
|
status,
|
||||||
headers: { "content-type": "application/json", "cache-control": "public, max-age=300" },
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"cache-control": "public, max-age=300, s-maxage=1800, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) return json(empty);
|
if (!token) return json(empty);
|
||||||
|
|||||||
@ -64,12 +64,14 @@ function mapTrack(t: any): Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function nowPlaying(token: string): Promise<NowPlaying | null> {
|
async function nowPlaying(token: string): Promise<NowPlaying | null> {
|
||||||
let res = await fetch(`${API}/me/player/currently-playing`, {
|
const h = { Authorization: `Bearer ${token}` };
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
const [curRes, recRes] = await Promise.all([
|
||||||
});
|
fetch(`${API}/me/player/currently-playing`, { headers: h }),
|
||||||
|
fetch(`${API}/me/player/recently-played?limit=1`, { headers: h }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (curRes.status === 200) {
|
||||||
const json = (await res.json()) as any;
|
const json = (await curRes.json()) as any;
|
||||||
if (json?.item) {
|
if (json?.item) {
|
||||||
return {
|
return {
|
||||||
...mapTrack(json.item),
|
...mapTrack(json.item),
|
||||||
@ -81,11 +83,8 @@ async function nowPlaying(token: string): Promise<NowPlaying | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res = await fetch(`${API}/me/player/recently-played?limit=1`, {
|
if (!recRes.ok) return null;
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
const json = (await recRes.json()) as any;
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const json = (await res.json()) as any;
|
|
||||||
const item = json?.items?.[0];
|
const item = json?.items?.[0];
|
||||||
if (!item?.track) return null;
|
if (!item?.track) return null;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -29,7 +29,10 @@ export const GET: APIRoute = async () => {
|
|||||||
const json = (data: unknown, status = 200) =>
|
const json = (data: unknown, status = 200) =>
|
||||||
new Response(JSON.stringify(data), {
|
new Response(JSON.stringify(data), {
|
||||||
status,
|
status,
|
||||||
headers: { "content-type": "application/json", "cache-control": "public, max-age=300" },
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"cache-control": "public, max-age=600, s-maxage=3600, stale-while-revalidate=86400",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hidden = {
|
const hidden = {
|
||||||
|
|||||||
@ -10,6 +10,19 @@ import { site, elsewhere } from "../data/site";
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base>
|
||||||
|
<script is:inline>
|
||||||
|
window.__mm = {};
|
||||||
|
for (const k of ["spotify", "reading", "stats"]) {
|
||||||
|
window.__mm[k] = fetch("/api/" + k + ".json", { headers: { accept: "application/json" } })
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<section class="intro">
|
<section class="intro">
|
||||||
<h1 class="sr-only">{site.name} — {site.tagline}</h1>
|
<h1 class="sr-only">{site.name} — {site.tagline}</h1>
|
||||||
<p class="bio">{site.bio}</p>
|
<p class="bio">{site.bio}</p>
|
||||||
@ -39,14 +52,10 @@ import { site, elsewhere } from "../data/site";
|
|||||||
|
|
||||||
<section class="moreme" id="more-about-me" aria-label="more about me">
|
<section class="moreme" id="more-about-me" aria-label="more about me">
|
||||||
<h2 class="label moreme__label">more about me</h2>
|
<h2 class="label moreme__label">more about me</h2>
|
||||||
<div class="foot">
|
<div class="mm-grid">
|
||||||
<Music />
|
<Music />
|
||||||
<Reading />
|
<Reading />
|
||||||
</div>
|
|
||||||
<div class="foot">
|
|
||||||
<Stats />
|
<Stats />
|
||||||
</div>
|
|
||||||
<div class="foot foot--clock">
|
|
||||||
<Clock />
|
<Clock />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -131,16 +131,22 @@ function mockData() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
type MusicState = "ready" | "idle" | "off";
|
||||||
|
|
||||||
|
async function load(): Promise<MusicState> {
|
||||||
let data: any;
|
let data: any;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/spotify.json", { headers: { accept: "application/json" } });
|
const mm = (window as any).__mm?.spotify as Promise<any> | undefined;
|
||||||
data = await res.json();
|
data = mm
|
||||||
|
? await mm
|
||||||
|
: await fetch("/api/spotify.json", { headers: { accept: "application/json" } }).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return "idle";
|
||||||
}
|
}
|
||||||
if ((!data || !data.configured) && location.search.includes("mockmusic")) data = mockData();
|
if (location.search.includes("mockmusic")) data = mockData();
|
||||||
if (!data || !data.configured) return;
|
if (!data || !data.configured) return "off";
|
||||||
if (data.nowPlaying) renderNowPlaying(data.nowPlaying as NP);
|
if (data.nowPlaying) renderNowPlaying(data.nowPlaying as NP);
|
||||||
if (Array.isArray(data.top)) renderTop(data.top as Track[]);
|
if (Array.isArray(data.top)) renderTop(data.top as Track[]);
|
||||||
|
|
||||||
@ -180,7 +186,24 @@ async function load(): Promise<void> {
|
|||||||
if (sub) sub.textContent = "last 4 weeks";
|
if (sub) sub.textContent = "last 4 weeks";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return "ready";
|
||||||
}
|
}
|
||||||
|
return "idle";
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
const host = document.getElementById("music");
|
||||||
|
const apply = (state: MusicState) => {
|
||||||
|
host?.setAttribute("data-state", state);
|
||||||
|
if (state === "idle") {
|
||||||
|
const trigger = document.querySelector<HTMLButtonElement>(".music__trigger");
|
||||||
|
if (trigger) {
|
||||||
|
trigger.hidden = false;
|
||||||
|
trigger.disabled = true;
|
||||||
|
const label = document.getElementById("music-label");
|
||||||
|
if (label) label.textContent = "not playing";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
load()
|
||||||
|
.then(apply)
|
||||||
|
.catch(() => apply("idle"));
|
||||||
|
|||||||
@ -28,20 +28,24 @@ function bookCard(b: Book, mini = false): string {
|
|||||||
)}"><span class="rbook__frame">${inner}${prog}</span>${meta}</a>`;
|
)}"><span class="rbook__frame">${inner}${prog}</span>${meta}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<boolean> {
|
||||||
let data: any;
|
let data: any;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/reading.json", { headers: { accept: "application/json" } });
|
const mm = (window as any).__mm?.reading as Promise<any> | undefined;
|
||||||
data = await res.json();
|
data = mm
|
||||||
|
? await mm
|
||||||
|
: await fetch("/api/reading.json", { headers: { accept: "application/json" } }).then((r) =>
|
||||||
|
r.json(),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!data || !data.configured) return;
|
if (!data || !data.configured) return false;
|
||||||
|
|
||||||
const current: Book[] = Array.isArray(data.current) ? data.current : [];
|
const current: Book[] = Array.isArray(data.current) ? data.current : [];
|
||||||
const want: Book[] = Array.isArray(data.want) ? data.want : [];
|
const want: Book[] = Array.isArray(data.want) ? data.want : [];
|
||||||
const recent: Book[] = Array.isArray(data.recent) ? data.recent : [];
|
const recent: Book[] = Array.isArray(data.recent) ? data.recent : [];
|
||||||
if (!current.length && !want.length && !recent.length) return;
|
if (!current.length && !want.length && !recent.length) return false;
|
||||||
|
|
||||||
if (current.length) {
|
if (current.length) {
|
||||||
const block = $("reading-current");
|
const block = $("reading-current");
|
||||||
@ -75,7 +79,7 @@ async function load(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trigger = document.querySelector<HTMLButtonElement>(".reading__trigger");
|
const trigger = document.querySelector<HTMLButtonElement>(".reading__trigger");
|
||||||
if (!trigger) return;
|
if (!trigger) return false;
|
||||||
trigger.hidden = false;
|
trigger.hidden = false;
|
||||||
|
|
||||||
const lead = current[0] || recent[0] || want[0];
|
const lead = current[0] || recent[0] || want[0];
|
||||||
@ -107,6 +111,10 @@ async function load(): Promise<void> {
|
|||||||
if (sub) sub.textContent = `${lead.title} · ${lead.author}`;
|
if (sub) sub.textContent = `${lead.title} · ${lead.author}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
const host = document.getElementById("books");
|
||||||
|
load()
|
||||||
|
.then((shown) => host?.setAttribute("data-state", shown ? "ready" : "empty"))
|
||||||
|
.catch(() => host?.setAttribute("data-state", "empty"));
|
||||||
|
|||||||
@ -436,18 +436,71 @@ img {
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot {
|
.mm-grid {
|
||||||
display: flex;
|
width: min(30rem, 100%);
|
||||||
flex-wrap: wrap;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 0.55rem 0.8rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
.foot > .music,
|
.mm-grid > * {
|
||||||
.foot > .reading {
|
min-width: 0;
|
||||||
margin-top: 0;
|
|
||||||
}
|
}
|
||||||
.foot + .foot {
|
.mm-grid > script {
|
||||||
margin-top: 0.7rem;
|
display: none;
|
||||||
|
}
|
||||||
|
.mm-grid > *[data-state="empty"],
|
||||||
|
.mm-grid > *[data-state="off"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music__trigger,
|
||||||
|
.reading__trigger,
|
||||||
|
.stats__trigger,
|
||||||
|
.clock {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-skel {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 2.05rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
[data-state="loading"] > .chip-skel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
[data-state="loading"] > .music__trigger,
|
||||||
|
[data-state="loading"] > .reading__trigger,
|
||||||
|
[data-state="loading"] > .stats__trigger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.chip-skel::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.045),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
animation: chip-skel 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes chip-skel {
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.chip-skel::after {
|
||||||
|
animation: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.colophon {
|
.colophon {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user