tons o changes
This commit is contained in:
parent
2a0f022e3f
commit
011c123f8f
@ -12,3 +12,6 @@ SESSION_SECRET=
|
|||||||
SPOTIFY_CLIENT_ID=
|
SPOTIFY_CLIENT_ID=
|
||||||
SPOTIFY_CLIENT_SECRET=
|
SPOTIFY_CLIENT_SECRET=
|
||||||
SPOTIFY_REFRESH_TOKEN=
|
SPOTIFY_REFRESH_TOKEN=
|
||||||
|
|
||||||
|
# currently-reading widget — personal token from https://hardcover.app/account/api
|
||||||
|
HARDCOVER_TOKEN=
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
import cloudflare from '@astrojs/cloudflare';
|
import cloudflare from '@astrojs/cloudflare';
|
||||||
|
import icon from 'astro-icon';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'server',
|
output: 'server',
|
||||||
|
integrations: [icon()],
|
||||||
adapter: cloudflare({
|
adapter: cloudflare({
|
||||||
platformProxy: { enabled: true },
|
platformProxy: { enabled: true },
|
||||||
}),
|
}),
|
||||||
|
|||||||
116
package-lock.json
generated
116
package-lock.json
generated
@ -15,10 +15,28 @@
|
|||||||
"astro": "^7.3.1",
|
"astro": "^7.3.1",
|
||||||
"wrangler": "^4.129.0"
|
"wrangler": "^4.129.0"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/ph": "^1.2.2",
|
||||||
|
"astro-icon": "^1.2.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@antfu/install-pkg": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iCKVQcIC0e3oDxEfs3SHQGW+ovhBMZmS1TE+bTk50rVyMCBmCfClv7Qi3HQKlumYwvjb/iIMeWCW2i67q6kFfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"package-manager-detector": "^1.7.0",
|
||||||
|
"tinyexec": "^1.2.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@astrojs/cloudflare": {
|
"node_modules/@astrojs/cloudflare": {
|
||||||
"version": "14.3.0",
|
"version": "14.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/cloudflare/-/cloudflare-14.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/cloudflare/-/cloudflare-14.3.0.tgz",
|
||||||
@ -673,6 +691,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cyberalien/svg-utils": {
|
||||||
|
"version": "1.2.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cyberalien/svg-utils/-/svg-utils-1.2.19.tgz",
|
||||||
|
"integrity": "sha512-paDJoDu+LhuH5Ma1q0BDqBpg6d16Xk22cZZrlg/s/J9oB8KgnnpU/snaEETJu2G1pwYcWQINUz6WUqu0Wm9k6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.11.3",
|
"version": "1.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||||
@ -1149,6 +1177,51 @@
|
|||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/ph": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/ph/-/ph-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/tools": {
|
||||||
|
"version": "5.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/tools/-/tools-5.0.14.tgz",
|
||||||
|
"integrity": "sha512-wJ4ZE3/HZh2iBqLwG+8FOMHe7CMJAJqwSXZSpPwMdj1gZM3Z4t2hbQMVw4Vm45oQ985qn0g89PUIcDEdKTheBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@cyberalien/svg-utils": "^1.2.19",
|
||||||
|
"@iconify/types": "^2.0.0",
|
||||||
|
"@iconify/utils": "^3.1.4",
|
||||||
|
"fflate": "^0.8.3",
|
||||||
|
"modern-tar": "^0.8.4",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"svgo": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/types": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/utils": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@antfu/install-pkg": "^2.0.1",
|
||||||
|
"@iconify/types": "^2.0.0",
|
||||||
|
"import-meta-resolve": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
@ -2366,6 +2439,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/astro-icon": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/astro-icon/-/astro-icon-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-0848V2WZuTPhqGSqLeZimLJZ5wmamMtWYzvA2kvg8s2DrUPZ3/nrTcHJWN0eJHt62jy7uvNxgTNiUJGIr7JNxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/tools": "^5.0.12",
|
||||||
|
"@iconify/types": "^2.0.0",
|
||||||
|
"@iconify/utils": "^3.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@ -2839,6 +2927,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/find-proc": {
|
"node_modules/find-proc": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.1.0.tgz",
|
||||||
@ -2988,6 +3083,17 @@
|
|||||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/import-meta-resolve": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iron-webcrypto": {
|
"node_modules/iron-webcrypto": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||||
@ -4076,6 +4182,16 @@
|
|||||||
"node": ">=20.18.1"
|
"node": ">=20.18.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/modern-tar": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
|||||||
@ -24,5 +24,9 @@
|
|||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"esbuild": true,
|
"esbuild": true,
|
||||||
"workerd@1.20260903.1": true
|
"workerd@1.20260903.1": true
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/ph": "^1.2.2",
|
||||||
|
"astro-icon": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/badges/kevadesu.gif
Normal file
BIN
public/badges/kevadesu.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 961 B |
BIN
public/badges/ro-er.gif
Normal file
BIN
public/badges/ro-er.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -372,6 +372,8 @@ const badgeUrl = `https://${site.domain}/badges/zachy.gif`;
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { hashDialog } from "../scripts/hash-dialog.ts";
|
||||||
|
|
||||||
const dialog = document.querySelector<HTMLDialogElement>("[data-badges-dialog]");
|
const dialog = document.querySelector<HTMLDialogElement>("[data-badges-dialog]");
|
||||||
const open = document.querySelector<HTMLButtonElement>("[data-badges-open]");
|
const open = document.querySelector<HTMLButtonElement>("[data-badges-open]");
|
||||||
const close = document.querySelector<HTMLButtonElement>("[data-badges-close]");
|
const close = document.querySelector<HTMLButtonElement>("[data-badges-close]");
|
||||||
@ -386,10 +388,9 @@ const badgeUrl = `https://${site.domain}/badges/zachy.gif`;
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
open?.addEventListener("click", () => {
|
const openWall = hashDialog({ name: "badges", dialog, onOpen: loadEmbeds });
|
||||||
loadEmbeds();
|
|
||||||
dialog?.showModal();
|
open?.addEventListener("click", () => (openWall ? openWall() : dialog?.showModal()));
|
||||||
});
|
|
||||||
close?.addEventListener("click", () => dialog?.close());
|
close?.addEventListener("click", () => dialog?.close());
|
||||||
|
|
||||||
dialog?.addEventListener("click", (e) => {
|
dialog?.addEventListener("click", (e) => {
|
||||||
|
|||||||
91
src/components/Clock.astro
Normal file
91
src/components/Clock.astro
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
const TZ = "America/New_York";
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const parts = (opts: Intl.DateTimeFormatOptions) =>
|
||||||
|
new Intl.DateTimeFormat("en-US", { timeZone: TZ, ...opts }).formatToParts(now);
|
||||||
|
|
||||||
|
const hh = parts({ hour: "2-digit", hour12: false }).find((p) => p.type === "hour")?.value ?? "--";
|
||||||
|
const mm = parts({ minute: "2-digit" }).find((p) => p.type === "minute")?.value ?? "--";
|
||||||
|
const zone =
|
||||||
|
parts({ timeZoneName: "short" }).find((p) => p.type === "timeZoneName")?.value ?? "ET";
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="clock" data-tz={TZ} role="img" aria-label={`zach's local time — ${hh}:${mm} ${zone}`}>
|
||||||
|
<span class="clock__scr">
|
||||||
|
<span class="clock__time"
|
||||||
|
>{hh}<span class="clock__sep">:</span>{mm}</span
|
||||||
|
>
|
||||||
|
<span class="clock__zone" id="clock-zone">{zone}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clock {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.34rem 0.66rem;
|
||||||
|
background: #0e0d0b;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 3px rgba(0, 0, 0, 0.65),
|
||||||
|
inset 0 0 0 1px rgba(255, 180, 84, 0.04);
|
||||||
|
}
|
||||||
|
.clock__scr {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.6ch;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
.clock__time {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ffb454;
|
||||||
|
text-shadow: 0 0 6px rgba(255, 180, 84, 0.5);
|
||||||
|
}
|
||||||
|
.clock__sep {
|
||||||
|
animation: clock-blink 1s steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes clock-blink {
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clock__zone {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: #806b4f;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.clock__sep {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const root = document.querySelector<HTMLElement>(".clock");
|
||||||
|
const timeEl = root?.querySelector<HTMLElement>(".clock__time");
|
||||||
|
const zoneEl = document.getElementById("clock-zone");
|
||||||
|
if (root && timeEl) {
|
||||||
|
const tz = root.dataset.tz || "America/New_York";
|
||||||
|
const timeFmt = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: tz,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
const zoneFmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" });
|
||||||
|
const tick = () => {
|
||||||
|
const d = new Date();
|
||||||
|
const [h, m] = timeFmt.format(d).split(":");
|
||||||
|
timeEl.innerHTML = `${h}<span class="clock__sep">:</span>${m}`;
|
||||||
|
const z = zoneFmt.formatToParts(d).find((p) => p.type === "timeZoneName")?.value;
|
||||||
|
if (zoneEl && z) zoneEl.textContent = z;
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
setInterval(tick, 15_000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -6,6 +6,8 @@ interface Props {
|
|||||||
event?: string;
|
event?: string;
|
||||||
}
|
}
|
||||||
const { value, display = value, label = `copy ${value}`, event } = Astro.props;
|
const { value, display = value, label = `copy ${value}`, event } = Astro.props;
|
||||||
|
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -16,13 +18,8 @@ const { value, display = value, label = `copy ${value}`, event } = Astro.props;
|
|||||||
data-umami-event={event}
|
data-umami-event={event}
|
||||||
>
|
>
|
||||||
<span class="copy__text">{display}</span>
|
<span class="copy__text">{display}</span>
|
||||||
<svg class="copy__icon" viewBox="0 0 24 24" aria-hidden="true">
|
<Icon class="copy__icon" name="ph:copy" aria-hidden="true" />
|
||||||
<rect x="9" y="9" width="11" height="11" rx="2"></rect>
|
<Icon class="copy__check" name="ph:check-bold" aria-hidden="true" />
|
||||||
<path d="M5 15V5a2 2 0 0 1 2-2h10"></path>
|
|
||||||
</svg>
|
|
||||||
<svg class="copy__check" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M20 6 9 17l-5-5"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="tip" aria-hidden="true">copied</span>
|
<span class="tip" aria-hidden="true">copied</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
---
|
||||||
|
|
||||||
---
|
<div class="music" id="music">
|
||||||
|
|
||||||
<div class="music">
|
|
||||||
<button
|
<button
|
||||||
class="music__trigger"
|
class="music__trigger"
|
||||||
type="button"
|
type="button"
|
||||||
@ -15,14 +15,18 @@
|
|||||||
class="music__art"
|
class="music__art"
|
||||||
id="music-art"
|
id="music-art"
|
||||||
alt=""
|
alt=""
|
||||||
width="30"
|
width="42"
|
||||||
height="30"
|
height="42"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
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>
|
||||||
|
<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>
|
||||||
<span class="music__arw" aria-hidden="true">→</span>
|
<span class="music__arw" aria-hidden="true">→</span>
|
||||||
|
<span class="music__bar" id="music-bar" aria-hidden="true" hidden><i></i></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<dialog class="music-wall" data-music-dialog aria-label="music">
|
<dialog class="music-wall" data-music-dialog aria-label="music">
|
||||||
@ -47,24 +51,14 @@
|
|||||||
<div class="np__artist" id="np-artist">—</div>
|
<div class="np__artist" id="np-artist">—</div>
|
||||||
<div class="np__foot">
|
<div class="np__foot">
|
||||||
<span class="np__tx" aria-hidden="true">
|
<span class="np__tx" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24" width="11" height="11" fill="currentColor"
|
<Icon name="ph:skip-back-fill" />
|
||||||
><path d="M6 5h2v14H6zM20 5v14L9 12z"></path></svg
|
<Icon name="ph:play-fill" />
|
||||||
>
|
<Icon name="ph:skip-forward-fill" />
|
||||||
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"
|
|
||||||
><path d="M6 4l14 8-14 8z"></path></svg
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" width="11" height="11" fill="currentColor"
|
|
||||||
><path d="M16 5h2v14h-2zM4 5l11 7-11 7z"></path></svg
|
|
||||||
>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="np__dur" id="np-dur"></span>
|
<span class="np__dur" id="np-dur"></span>
|
||||||
<span class="np__ago" id="np-ago"></span>
|
<span class="np__ago" id="np-ago"></span>
|
||||||
<a class="np__src" id="np-src" href="#" target="_blank" rel="noopener">
|
<a class="np__src" id="np-src" href="#" target="_blank" rel="noopener">
|
||||||
<svg class="np__logo" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"
|
<Icon class="np__logo" name="ph:spotify-logo-fill" aria-hidden="true" /><span>spotify</span>
|
||||||
><path
|
|
||||||
d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.16-1.38-.72-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"
|
|
||||||
></path></svg
|
|
||||||
><span>spotify</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -88,6 +82,7 @@
|
|||||||
margin-top: var(--gap);
|
margin-top: var(--gap);
|
||||||
}
|
}
|
||||||
.music__trigger {
|
.music__trigger {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
@ -96,29 +91,41 @@
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
transition:
|
transition:
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
color 0.18s ease;
|
color 0.18s ease,
|
||||||
|
padding 0.22s ease;
|
||||||
}
|
}
|
||||||
.music__trigger:hover {
|
.music__trigger:hover,
|
||||||
|
.music__trigger:focus-visible {
|
||||||
border-color: var(--faint);
|
border-color: var(--faint);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
padding-bottom: 0.45rem;
|
||||||
}
|
}
|
||||||
.music__trigger:focus-visible {
|
.music__trigger:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
.music__art {
|
.music__art {
|
||||||
width: 30px;
|
width: 26px;
|
||||||
height: 30px;
|
height: 26px;
|
||||||
flex: none;
|
flex: none;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
transition:
|
||||||
|
width 0.22s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
height 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.music__trigger:hover .music__art,
|
||||||
|
.music__trigger:focus-visible .music__art {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
}
|
}
|
||||||
.music__eq {
|
.music__eq {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -134,6 +141,49 @@
|
|||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
.music__main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.05rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.music__sub {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
color: var(--faint);
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition:
|
||||||
|
max-height 0.22s ease,
|
||||||
|
opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
.music__trigger:hover .music__sub,
|
||||||
|
.music__trigger:focus-visible .music__sub {
|
||||||
|
max-height: 1.4em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.music__bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #2b2825;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.music__bar > i {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--p, 0%);
|
||||||
|
background: linear-gradient(90deg, rgb(255, 224, 138), rgb(255, 171, 94), rgb(255, 95, 109));
|
||||||
|
}
|
||||||
|
.music__trigger:hover .music__bar,
|
||||||
|
.music__trigger:focus-visible .music__bar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
.music__trigger--live .music__eq i {
|
.music__trigger--live .music__eq i {
|
||||||
animation: music-eq 0.95s ease-in-out infinite alternate;
|
animation: music-eq 0.95s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
@ -316,7 +366,7 @@
|
|||||||
.np__logo {
|
.np__logo {
|
||||||
width: 0.95em;
|
width: 0.95em;
|
||||||
height: 0.95em;
|
height: 0.95em;
|
||||||
fill: #1ed760;
|
color: #1ed760;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -444,12 +494,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../scripts/music.ts";
|
import "../scripts/music.ts";
|
||||||
|
import { hashDialog } from "../scripts/hash-dialog.ts";
|
||||||
|
|
||||||
const dialog = document.querySelector<HTMLDialogElement>("[data-music-dialog]");
|
const dialog = document.querySelector<HTMLDialogElement>("[data-music-dialog]");
|
||||||
const open = document.querySelector<HTMLButtonElement>("[data-music-open]");
|
const open = document.querySelector<HTMLButtonElement>("[data-music-open]");
|
||||||
const close = document.querySelector<HTMLButtonElement>("[data-music-close]");
|
const close = document.querySelector<HTMLButtonElement>("[data-music-close]");
|
||||||
|
|
||||||
open?.addEventListener("click", () => dialog?.showModal());
|
const openWall = hashDialog({
|
||||||
|
name: "music",
|
||||||
|
dialog,
|
||||||
|
canOpen: () => !!open && !open.hidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
open?.addEventListener("click", () => (openWall ? openWall() : dialog?.showModal()));
|
||||||
close?.addEventListener("click", () => dialog?.close());
|
close?.addEventListener("click", () => dialog?.close());
|
||||||
dialog?.addEventListener("click", (e) => {
|
dialog?.addEventListener("click", (e) => {
|
||||||
if (e.target === dialog) dialog.close();
|
if (e.target === dialog) dialog.close();
|
||||||
|
|||||||
383
src/components/Reading.astro
Normal file
383
src/components/Reading.astro
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="reading" id="books">
|
||||||
|
<button
|
||||||
|
class="reading__trigger"
|
||||||
|
type="button"
|
||||||
|
data-reading-open
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
data-umami-event="reading-open"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="reading__cover"
|
||||||
|
id="reading-cover"
|
||||||
|
alt=""
|
||||||
|
width="28"
|
||||||
|
height="40"
|
||||||
|
decoding="async"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<span class="reading__ico" aria-hidden="true">▢</span>
|
||||||
|
<span class="reading__main">
|
||||||
|
<span class="reading__label" id="reading-label">reading</span>
|
||||||
|
<span class="reading__sub" id="reading-sub" aria-hidden="true"></span>
|
||||||
|
</span>
|
||||||
|
<span class="reading__arw" aria-hidden="true">→</span>
|
||||||
|
<span class="reading__bar" id="reading-bar" aria-hidden="true" hidden><i></i></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog class="reading-wall" data-reading-dialog aria-label="reading">
|
||||||
|
<div class="reading-wall__bar">
|
||||||
|
<span class="reading-wall__title">reading</span>
|
||||||
|
<button
|
||||||
|
class="reading-wall__close"
|
||||||
|
type="button"
|
||||||
|
data-reading-close
|
||||||
|
aria-label="close"
|
||||||
|
autofocus>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reading-wall__body">
|
||||||
|
<div class="reading-block" id="reading-current" hidden>
|
||||||
|
<h2 class="label">currently reading</h2>
|
||||||
|
<div class="rshelf" id="reading-current-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="reading-block" id="reading-want" hidden>
|
||||||
|
<h2 class="label">up next</h2>
|
||||||
|
<div class="rshelf rshelf--recent rshelf--want" id="reading-want-strip"></div>
|
||||||
|
</div>
|
||||||
|
<div class="reading-block" id="reading-recent" hidden>
|
||||||
|
<h2 class="label">recently finished</h2>
|
||||||
|
<div class="rshelf rshelf--recent" id="reading-recent-strip"></div>
|
||||||
|
</div>
|
||||||
|
<p class="reading-wall__foot">
|
||||||
|
<a
|
||||||
|
href="https://hardcover.app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
data-umami-event="reading-hardcover">tracked on hardcover ↗</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
|
||||||
|
.reading {
|
||||||
|
}
|
||||||
|
.reading__trigger {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.3rem 0.6rem 0.3rem 0.4rem;
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--muted);
|
||||||
|
transition:
|
||||||
|
border-color 0.18s ease,
|
||||||
|
color 0.18s ease,
|
||||||
|
padding 0.22s ease;
|
||||||
|
}
|
||||||
|
.reading__trigger:hover,
|
||||||
|
.reading__trigger:focus-visible {
|
||||||
|
border-color: var(--faint);
|
||||||
|
color: var(--ink);
|
||||||
|
padding-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
.reading__trigger:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
.reading__cover {
|
||||||
|
width: 20px;
|
||||||
|
height: 28px;
|
||||||
|
flex: none;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition:
|
||||||
|
width 0.22s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
height 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.reading__trigger:hover .reading__cover,
|
||||||
|
.reading__trigger:focus-visible .reading__cover {
|
||||||
|
width: 30px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.reading__ico {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.reading__main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.05rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.reading__label {
|
||||||
|
max-width: 26ch;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.reading__sub {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
color: var(--faint);
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition:
|
||||||
|
max-height 0.22s ease,
|
||||||
|
opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
.reading__trigger:hover .reading__sub,
|
||||||
|
.reading__trigger:focus-visible .reading__sub {
|
||||||
|
max-height: 1.4em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.reading__bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #2b2825;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.reading__bar > i {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--p, 0%);
|
||||||
|
background: linear-gradient(90deg, rgb(255, 224, 138), rgb(255, 171, 94), rgb(255, 95, 109));
|
||||||
|
}
|
||||||
|
.reading__trigger:hover .reading__bar,
|
||||||
|
.reading__trigger:focus-visible .reading__bar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.reading__arw {
|
||||||
|
color: var(--faint);
|
||||||
|
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.reading__trigger:hover .reading__arw {
|
||||||
|
transform: translateX(0.25em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-wall {
|
||||||
|
margin: auto;
|
||||||
|
width: min(24rem, calc(100vw - 1.5rem));
|
||||||
|
max-height: min(82vh, 40rem);
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--paper-2);
|
||||||
|
color: var(--ink);
|
||||||
|
box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.75);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.reading-wall[open] {
|
||||||
|
animation: reading-in 0.28s cubic-bezier(0.17, 0.89, 0.32, 1.28);
|
||||||
|
}
|
||||||
|
@keyframes reading-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reading-wall::backdrop {
|
||||||
|
background: rgba(6, 5, 4, 0.68);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
.reading-wall__bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.reading-wall__title {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--faint);
|
||||||
|
}
|
||||||
|
.reading-wall__close {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.3rem;
|
||||||
|
}
|
||||||
|
.reading-wall__close:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.reading-wall__body {
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(min(82vh, 40rem) - 2.6rem);
|
||||||
|
}
|
||||||
|
.reading-block + .reading-block {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.reading-block .label {
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rshelf {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: 1.15rem 1.1rem;
|
||||||
|
}
|
||||||
|
.rbook {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
width: 86px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.rbook__frame {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 2 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--faint);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--paper);
|
||||||
|
box-shadow: 0 8px 18px -10px rgba(0, 0, 0, 0.7);
|
||||||
|
transition:
|
||||||
|
transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
box-shadow 0.25s ease;
|
||||||
|
}
|
||||||
|
.rbook:hover .rbook__frame,
|
||||||
|
.rbook:focus-visible .rbook__frame {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 16px 26px -12px rgba(0, 0, 0, 0.82);
|
||||||
|
}
|
||||||
|
.rbook__cover {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
span.rbook__cover {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.35rem;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(145deg, #2c2a26, #171614);
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-size: 0.64rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.rbook__prog {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
.rbook__prog > span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, rgb(255, 224, 138), rgb(255, 171, 94), rgb(255, 95, 109));
|
||||||
|
}
|
||||||
|
.rbook__title {
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.22;
|
||||||
|
color: var(--ink);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rbook__author {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.63rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: var(--faint);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rshelf--recent {
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
.rshelf--recent .rbook {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
.rshelf--recent .rbook__frame {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.rshelf--recent .rbook__cover {
|
||||||
|
filter: saturate(0.68) brightness(0.78);
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
.rshelf--recent .rbook:hover .rbook__cover,
|
||||||
|
.rshelf--recent .rbook:focus-visible .rbook__cover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rshelf--want .rbook__frame {
|
||||||
|
border-color: var(--faint);
|
||||||
|
}
|
||||||
|
.rshelf--want .rbook__cover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-wall__foot {
|
||||||
|
margin: 1.5rem 0 0;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.reading-wall__foot a {
|
||||||
|
color: var(--faint);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.reading-wall__foot a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "../scripts/reading.ts";
|
||||||
|
import { hashDialog } from "../scripts/hash-dialog.ts";
|
||||||
|
|
||||||
|
const dialog = document.querySelector<HTMLDialogElement>("[data-reading-dialog]");
|
||||||
|
const open = document.querySelector<HTMLButtonElement>("[data-reading-open]");
|
||||||
|
const close = document.querySelector<HTMLButtonElement>("[data-reading-close]");
|
||||||
|
|
||||||
|
const openWall = hashDialog({
|
||||||
|
name: "books",
|
||||||
|
dialog,
|
||||||
|
canOpen: () => !!open && !open.hidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
open?.addEventListener("click", () => (openWall ? openWall() : dialog?.showModal()));
|
||||||
|
close?.addEventListener("click", () => dialog?.close());
|
||||||
|
dialog?.addEventListener("click", (e) => {
|
||||||
|
if (e.target === dialog) dialog.close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -37,6 +37,8 @@ const aria: Record<string, string> = {
|
|||||||
next: "next site",
|
next: "next site",
|
||||||
random: "random site",
|
random: "random site",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@ -69,23 +71,7 @@ const aria: Record<string, string> = {
|
|||||||
data-umami-event-ring={r.key}
|
data-umami-event-ring={r.key}
|
||||||
data-umami-event-dir={dir}
|
data-umami-event-dir={dir}
|
||||||
>
|
>
|
||||||
{dir === "random" && (
|
{dir === "random" && <Icon class="ring__die" name="ph:dice-three" aria-hidden="true" />}
|
||||||
<svg class="ring__die" viewBox="0 0 20 20" aria-hidden="true">
|
|
||||||
<rect
|
|
||||||
x="1.5"
|
|
||||||
y="1.5"
|
|
||||||
width="17"
|
|
||||||
height="17"
|
|
||||||
rx="4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
/>
|
|
||||||
<circle cx="6" cy="6" r="2" />
|
|
||||||
<circle cx="10" cy="10" r="2" />
|
|
||||||
<circle cx="14" cy="14" r="2" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{glyph}
|
{glyph}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export const site = {
|
|||||||
wordmark: "ZACH",
|
wordmark: "ZACH",
|
||||||
domain: "zachy.cc",
|
domain: "zachy.cc",
|
||||||
version: "v3.0.1",
|
version: "v3.0.1",
|
||||||
tagline: "cool things for the web",
|
tagline: "i make cool stuff",
|
||||||
description:
|
description:
|
||||||
"i make cool things sometimes.",
|
"i make cool things sometimes.",
|
||||||
bio: "call me zoop, i like to make cool projects, and my website is one of them :).",
|
bio: "call me zoop, i like to make cool projects, and my website is one of them :).",
|
||||||
@ -84,4 +84,6 @@ export const buttons = [
|
|||||||
{ label: "snug nook", img: "/badges/snug-nook.gif", still: null, href: "https://snugnook.org" },
|
{ label: "snug nook", img: "/badges/snug-nook.gif", still: null, href: "https://snugnook.org" },
|
||||||
{ label: "ghostk.id", img: "/badges/ghostk.gif", still: null, href: "https://ghostk.id/" },
|
{ label: "ghostk.id", img: "/badges/ghostk.gif", still: null, href: "https://ghostk.id/" },
|
||||||
{ label: "increment", iframe: "//incr.easrng.net/badge?key=zachy.cc-2026", bg: "//incr.easrng.net/bg.gif", href: null },
|
{ label: "increment", iframe: "//incr.easrng.net/badge?key=zachy.cc-2026", bg: "//incr.easrng.net/bg.gif", href: null },
|
||||||
|
{ label: "ro.er", img: "/badges/ro-er.gif", still: null, href: "https://retrobloxxer.github.io/" },
|
||||||
|
{ label: "kevadesu", img: "/badges/kevadesu.gif", still: null, href: "https://kevadesu.github.io/" },
|
||||||
];
|
];
|
||||||
|
|||||||
1
src/icons/README.md
Normal file
1
src/icons/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# astro-icon scans this dir for local SVG icons; we only use the `ph:` set, but the dir must exist to avoid a build warning
|
||||||
@ -2,6 +2,7 @@
|
|||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
import { site } from "../data/site";
|
import { site } from "../data/site";
|
||||||
import Egg from "../components/Egg.astro";
|
import Egg from "../components/Egg.astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -119,6 +120,7 @@ const kaoCount = kaoMs > 0 ? `${Math.floor(kaoMs / 86_400_000)}d` : "in effect";
|
|||||||
rel="noopener"
|
rel="noopener"
|
||||||
data-umami-event="src"
|
data-umami-event="src"
|
||||||
>
|
>
|
||||||
|
<Icon class="srcline__icon" name="ph:git-branch" aria-hidden="true" />
|
||||||
source ↗
|
source ↗
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
130
src/pages/api/reading.json.ts
Normal file
130
src/pages/api/reading.json.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { env as cfEnv } from "cloudflare:workers";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
|
||||||
|
type Env = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
interface Book {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
cover: string | null;
|
||||||
|
url: string;
|
||||||
|
progress: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GQL = "https://api.hardcover.app/v1/graphql";
|
||||||
|
const SITE = "https://hardcover.app";
|
||||||
|
|
||||||
|
const QUERY = `
|
||||||
|
query SiteReading {
|
||||||
|
me {
|
||||||
|
current: user_books(where: {status_id: {_eq: 2}}, limit: 5) {
|
||||||
|
id
|
||||||
|
user_book_reads(order_by: {id: desc}, limit: 1) { progress_pages }
|
||||||
|
book {
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
pages
|
||||||
|
image { url }
|
||||||
|
contributions { author { name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
want: user_books(where: {status_id: {_eq: 1}}, order_by: {id: desc}, limit: 12) {
|
||||||
|
book {
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
image { url }
|
||||||
|
contributions { author { name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recent: user_books(where: {status_id: {_eq: 3}}, order_by: {id: desc}, limit: 8) {
|
||||||
|
book {
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
image { url }
|
||||||
|
contributions { author { name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
function getEnv(): Env {
|
||||||
|
const nodeEnv = typeof process !== "undefined" ? process.env : {};
|
||||||
|
let workerEnv: Env = {};
|
||||||
|
try {
|
||||||
|
workerEnv = cfEnv as unknown as Env;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
return { ...nodeEnv, ...workerEnv };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapBook(ub: any, withProgress: boolean): Book {
|
||||||
|
const b = ub?.book ?? {};
|
||||||
|
const author = (b?.contributions?.[0]?.author?.name ?? "").replace(/\s+/g, " ").trim();
|
||||||
|
const cover = b?.image?.url ?? null;
|
||||||
|
|
||||||
|
let progress: number | null = null;
|
||||||
|
if (withProgress) {
|
||||||
|
const read = ub?.user_book_reads?.[0];
|
||||||
|
const done = read?.progress_pages;
|
||||||
|
const total = b?.pages;
|
||||||
|
if (typeof done === "number" && typeof total === "number" && total > 0) {
|
||||||
|
progress = Math.max(0, Math.min(100, Math.round((done / total) * 100)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: b?.title ?? "",
|
||||||
|
author,
|
||||||
|
cover,
|
||||||
|
url: b?.slug ? `${SITE}/books/${b.slug}` : SITE,
|
||||||
|
progress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const token = getEnv().HARDCOVER_TOKEN;
|
||||||
|
const empty = { configured: false, current: [], want: [], recent: [] };
|
||||||
|
const json = (data: unknown, status = 200) =>
|
||||||
|
new Response(JSON.stringify(data), {
|
||||||
|
status,
|
||||||
|
headers: { "content-type": "application/json", "cache-control": "public, max-age=300" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) return json(empty);
|
||||||
|
|
||||||
|
let payload: any;
|
||||||
|
try {
|
||||||
|
const res = await fetch(GQL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`,
|
||||||
|
"user-agent": "zachy.cc reading widget",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: QUERY }),
|
||||||
|
});
|
||||||
|
payload = await res.json();
|
||||||
|
} catch {
|
||||||
|
return json({ ...empty, configured: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = Array.isArray(payload?.data?.me) ? payload.data.me[0] : payload?.data?.me;
|
||||||
|
if (!me || payload.errors) {
|
||||||
|
return json({ ...empty, configured: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const current: Book[] = (me.current ?? [])
|
||||||
|
.map((ub: any) => mapBook(ub, true))
|
||||||
|
.filter((b: Book) => b.title);
|
||||||
|
const want: Book[] = (me.want ?? [])
|
||||||
|
.map((ub: any) => mapBook(ub, false))
|
||||||
|
.filter((b: Book) => b.title);
|
||||||
|
const recent: Book[] = (me.recent ?? [])
|
||||||
|
.map((ub: any) => mapBook(ub, false))
|
||||||
|
.filter((b: Book) => b.title);
|
||||||
|
|
||||||
|
return json({ configured: true, current, want, recent });
|
||||||
|
};
|
||||||
@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
import Base from "../layouts/Base.astro";
|
import Base from "../layouts/Base.astro";
|
||||||
import Music from "../components/Music.astro";
|
import Music from "../components/Music.astro";
|
||||||
|
import Reading from "../components/Reading.astro";
|
||||||
|
import Clock from "../components/Clock.astro";
|
||||||
import Webrings from "../components/Webrings.astro";
|
import Webrings from "../components/Webrings.astro";
|
||||||
import Buttons from "../components/Buttons.astro";
|
import Buttons from "../components/Buttons.astro";
|
||||||
import { site, elsewhere } from "../data/site";
|
import { site, elsewhere } from "../data/site";
|
||||||
@ -33,5 +35,15 @@ import { site, elsewhere } from "../data/site";
|
|||||||
|
|
||||||
<Webrings />
|
<Webrings />
|
||||||
<Buttons />
|
<Buttons />
|
||||||
|
|
||||||
|
<section class="moreme" id="more-about-me" aria-label="more about me">
|
||||||
|
<h2 class="label moreme__label">more about me</h2>
|
||||||
|
<div class="foot">
|
||||||
<Music />
|
<Music />
|
||||||
|
<Reading />
|
||||||
|
</div>
|
||||||
|
<div class="foot foot--clock">
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Base>
|
</Base>
|
||||||
|
|||||||
46
src/scripts/hash-dialog.ts
Normal file
46
src/scripts/hash-dialog.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
interface Opts {
|
||||||
|
name: string;
|
||||||
|
dialog: HTMLDialogElement | null;
|
||||||
|
onOpen?: () => void;
|
||||||
|
canOpen?: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashDialog({ name, dialog, onOpen, canOpen }: Opts): (() => void) | undefined {
|
||||||
|
if (!dialog) return undefined;
|
||||||
|
const hash = `#${name}`;
|
||||||
|
const clean = () => location.pathname + location.search;
|
||||||
|
|
||||||
|
const open = (): void => {
|
||||||
|
if (dialog.open || (canOpen && !canOpen())) return;
|
||||||
|
onOpen?.();
|
||||||
|
dialog.showModal();
|
||||||
|
if (location.hash !== hash) history.replaceState(null, "", hash);
|
||||||
|
};
|
||||||
|
|
||||||
|
new MutationObserver(() => {
|
||||||
|
if (!dialog.open && location.hash === hash) {
|
||||||
|
history.replaceState(null, "", clean());
|
||||||
|
}
|
||||||
|
}).observe(dialog, { attributes: true, attributeFilter: ["open"] });
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", () => {
|
||||||
|
if (location.hash === hash) open();
|
||||||
|
else if (dialog.open) dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (location.hash === hash) {
|
||||||
|
let tries = 0;
|
||||||
|
const tick = (): void => {
|
||||||
|
if (dialog.open) return;
|
||||||
|
if (!canOpen || canOpen()) {
|
||||||
|
open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tries++ < 40) window.setTimeout(tick, 100);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return open;
|
||||||
|
}
|
||||||
@ -154,14 +154,26 @@ async function load(): Promise<void> {
|
|||||||
artEl.src = cover;
|
artEl.src = cover;
|
||||||
artEl.hidden = false;
|
artEl.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sub = $("music-sub");
|
||||||
|
const bar = $("music-bar");
|
||||||
|
const fill = bar?.firstElementChild as HTMLElement | null;
|
||||||
|
|
||||||
if (label) {
|
if (label) {
|
||||||
if (np?.isPlaying) {
|
if (np?.isPlaying) {
|
||||||
trigger.classList.add("music__trigger--live");
|
trigger.classList.add("music__trigger--live");
|
||||||
label.textContent = `${np.title} · ${np.artist}`;
|
label.textContent = np.title;
|
||||||
|
if (sub) sub.textContent = np.artist;
|
||||||
|
if (bar && fill && np.progressMs != null && np.durationMs) {
|
||||||
|
fill.style.setProperty("--p", `${Math.min(100, (np.progressMs / np.durationMs) * 100)}%`);
|
||||||
|
bar.hidden = false;
|
||||||
|
}
|
||||||
} else if (np?.playedAt) {
|
} else if (np?.playedAt) {
|
||||||
label.textContent = `last played · ${np.title}`;
|
label.textContent = "last played";
|
||||||
|
if (sub) sub.textContent = `${np.title} · ${np.artist}`;
|
||||||
} else {
|
} else {
|
||||||
label.textContent = "top tracks";
|
label.textContent = "top tracks";
|
||||||
|
if (sub) sub.textContent = "last 4 weeks";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
src/scripts/reading.ts
Normal file
112
src/scripts/reading.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
interface Book {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
cover: string | null;
|
||||||
|
url: string;
|
||||||
|
progress: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = (id: string) => document.getElementById(id);
|
||||||
|
const esc = (s: string): string =>
|
||||||
|
s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]!);
|
||||||
|
|
||||||
|
function bookCard(b: Book, mini = false): string {
|
||||||
|
const inner = b.cover
|
||||||
|
? `<img class="rbook__cover" src="${esc(b.cover)}" alt="" loading="lazy" decoding="async">`
|
||||||
|
: `<span class="rbook__cover">${esc(b.title)}</span>`;
|
||||||
|
const prog =
|
||||||
|
!mini && b.progress != null
|
||||||
|
? `<span class="rbook__prog"><span style="width:${b.progress}%"></span></span>`
|
||||||
|
: "";
|
||||||
|
const shortTitle = b.title.split(/\s*[:(]/)[0].trim() || b.title;
|
||||||
|
const meta = mini
|
||||||
|
? ""
|
||||||
|
: `<span class="rbook__title">${esc(shortTitle)}</span><span class="rbook__author">${esc(b.author)}</span>`;
|
||||||
|
return `<a class="rbook" href="${esc(b.url)}" target="_blank" rel="noopener" title="${esc(
|
||||||
|
`${b.title} — ${b.author}`,
|
||||||
|
)}"><span class="rbook__frame">${inner}${prog}</span>${meta}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
let data: any;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/reading.json", { headers: { accept: "application/json" } });
|
||||||
|
data = await res.json();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data || !data.configured) return;
|
||||||
|
|
||||||
|
const current: Book[] = Array.isArray(data.current) ? data.current : [];
|
||||||
|
const want: Book[] = Array.isArray(data.want) ? data.want : [];
|
||||||
|
const recent: Book[] = Array.isArray(data.recent) ? data.recent : [];
|
||||||
|
if (!current.length && !want.length && !recent.length) return;
|
||||||
|
|
||||||
|
if (current.length) {
|
||||||
|
const block = $("reading-current");
|
||||||
|
const list = $("reading-current-list");
|
||||||
|
if (block && list) {
|
||||||
|
list.innerHTML = current.map((b) => bookCard(b)).join("");
|
||||||
|
block.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (want.length) {
|
||||||
|
const block = $("reading-want");
|
||||||
|
const strip = $("reading-want-strip");
|
||||||
|
if (block && strip) {
|
||||||
|
const withCovers = want.filter((b) => b.cover);
|
||||||
|
if (withCovers.length) {
|
||||||
|
strip.innerHTML = withCovers.map((b) => bookCard(b, true)).join("");
|
||||||
|
block.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (recent.length) {
|
||||||
|
const block = $("reading-recent");
|
||||||
|
const strip = $("reading-recent-strip");
|
||||||
|
if (block && strip) {
|
||||||
|
const withCovers = recent.filter((b) => b.cover);
|
||||||
|
if (withCovers.length) {
|
||||||
|
strip.innerHTML = withCovers.map((b) => bookCard(b, true)).join("");
|
||||||
|
block.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trigger = document.querySelector<HTMLButtonElement>(".reading__trigger");
|
||||||
|
if (!trigger) return;
|
||||||
|
trigger.hidden = false;
|
||||||
|
|
||||||
|
const lead = current[0] || recent[0] || want[0];
|
||||||
|
const label = $("reading-label");
|
||||||
|
const cover = $("reading-cover") as HTMLImageElement | null;
|
||||||
|
if (lead && cover && lead.cover) {
|
||||||
|
cover.src = lead.cover;
|
||||||
|
cover.hidden = false;
|
||||||
|
const ico = trigger.querySelector(".reading__ico") as HTMLElement | null;
|
||||||
|
if (ico) ico.hidden = true;
|
||||||
|
}
|
||||||
|
const sub = $("reading-sub");
|
||||||
|
const bar = $("reading-bar");
|
||||||
|
const fill = bar?.firstElementChild as HTMLElement | null;
|
||||||
|
|
||||||
|
if (label && lead) {
|
||||||
|
if (current.length) {
|
||||||
|
label.textContent = lead.title;
|
||||||
|
if (sub) sub.textContent = lead.author;
|
||||||
|
if (bar && fill && lead.progress != null) {
|
||||||
|
fill.style.setProperty("--p", `${lead.progress}%`);
|
||||||
|
bar.hidden = false;
|
||||||
|
}
|
||||||
|
} else if (recent.length) {
|
||||||
|
label.textContent = "last read";
|
||||||
|
if (sub) sub.textContent = `${lead.title} · ${lead.author}`;
|
||||||
|
} else {
|
||||||
|
label.textContent = "want to read";
|
||||||
|
if (sub) sub.textContent = `${lead.title} · ${lead.author}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
@ -370,21 +370,17 @@ img {
|
|||||||
flex: none;
|
flex: none;
|
||||||
width: 0.95em;
|
width: 0.95em;
|
||||||
height: 0.95em;
|
height: 0.95em;
|
||||||
fill: none;
|
color: var(--faint);
|
||||||
stroke: var(--faint);
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
}
|
||||||
.copy__check {
|
.copy__check {
|
||||||
display: none;
|
display: none;
|
||||||
stroke: var(--accent-2);
|
color: var(--accent-2);
|
||||||
}
|
}
|
||||||
.copy:hover {
|
.copy:hover {
|
||||||
border-color: var(--accent-2);
|
border-color: var(--accent-2);
|
||||||
}
|
}
|
||||||
.copy:hover .copy__icon {
|
.copy:hover .copy__icon {
|
||||||
stroke: var(--accent-2);
|
color: var(--accent-2);
|
||||||
}
|
}
|
||||||
.copy.copied .copy__icon {
|
.copy.copied .copy__icon {
|
||||||
display: none;
|
display: none;
|
||||||
@ -419,6 +415,41 @@ img {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.moreme {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-top: var(--gap);
|
||||||
|
padding: 1.2rem 1.15rem 1.05rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.moreme__label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0.85rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0.45rem;
|
||||||
|
background: var(--paper);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem 0.8rem;
|
||||||
|
}
|
||||||
|
.foot > .music,
|
||||||
|
.foot > .reading {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.foot + .foot {
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.colophon {
|
.colophon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -435,6 +466,9 @@ img {
|
|||||||
margin: 0.5rem 0 0;
|
margin: 0.5rem 0 0;
|
||||||
}
|
}
|
||||||
.srcline a {
|
.srcline a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@ -445,6 +479,11 @@ img {
|
|||||||
.srcline a:hover {
|
.srcline a:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
.srcline__icon {
|
||||||
|
width: 0.95em;
|
||||||
|
height: 0.95em;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.backlink {
|
.backlink {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user