diff --git a/changelog/index.html b/changelog/index.html new file mode 100644 index 0000000..f4f4099 --- /dev/null +++ b/changelog/index.html @@ -0,0 +1,143 @@ + + + + + + + Clove Twilight - Changelog + + + + + + + + + + + + + + + + + + + v1.4.0 + +
+
+

Changelog

+

What's new on clove.is-a.dev

+
+ +
+
+
+ v1.4.0 + +
+
    +
  • Added "Now Playing" widget pulling my latest Last.fm scrobble
  • +
  • Added Secret Cat Modes — click the cat to unlock new looks
  • +
  • Added this changelog + version tag
  • +
  • Changed bottom-left page nav with smooth cross-fade transitions
  • +
+
+ +
+
+ v1.3.0 + +
+
    +
  • Added Tech Stack page with palette-themed badges
  • +
  • Changed restructured the project into css/ js/ images/
  • +
+
+ +
+
+ v1.2.0 + +
+
    +
  • Added profile picture to the header
  • +
  • Added estrogen-molecule background watermark
  • +
  • Changed moved the system badges into the corner
  • +
+
+ +
+
+ v1.1.0 + +
+
    +
  • Added custom domain: clove.is-a.dev
  • +
  • Added git.gay to the links
  • +
+
+ +
+
+ v1.0.0 + +
+
    +
  • Added rebuilt the site as a link hub (icon grid, Catppuccin theme)
  • +
  • Added oneko cat + cosmetic system badges
  • +
+
+ +
+
+ v0.1.0 + +
+
    +
  • Added initial site — a simple redirect page
  • +
+
+
+
+ + + + + + + + + + diff --git a/css/style.css b/css/style.css index 9af8d46..df039c4 100644 --- a/css/style.css +++ b/css/style.css @@ -271,6 +271,107 @@ body::before { color: var(--pink); } +/* "Now playing" widget, pinned top-left */ +.now-playing { + position: fixed; + top: 1rem; + left: 1rem; + z-index: 6; + display: flex; + align-items: center; + gap: 0.55rem; + max-width: 240px; + padding: 0.4rem 0.7rem 0.4rem 0.4rem; + border-radius: 999px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--text); + text-decoration: none; + transition: transform 0.15s ease, border-color 0.15s ease; +} + +.now-playing[hidden] { + display: none; +} + +.now-playing:hover { + transform: translateY(2px); + border-color: var(--pink); +} + +.np-art { + width: 34px; + height: 34px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +/* Equalizer bars (only animate while live) */ +.np-bars { + display: none; + align-items: flex-end; + gap: 2px; + height: 16px; + flex-shrink: 0; +} + +.now-playing.is-live .np-bars { + display: flex; +} + +.np-bars i { + width: 3px; + height: 100%; + border-radius: 2px; + background: var(--pink); + transform-origin: bottom; + animation: np-eq 0.9s ease-in-out infinite; +} + +.np-bars i:nth-child(2) { animation-delay: 0.15s; } +.np-bars i:nth-child(3) { animation-delay: 0.3s; } +.np-bars i:nth-child(4) { animation-delay: 0.45s; } + +@keyframes np-eq { + 0%, 100% { transform: scaleY(0.3); } + 50% { transform: scaleY(1); } +} + +@media (prefers-reduced-motion: reduce) { + .np-bars i { animation: none; transform: scaleY(0.6); } +} + +.np-text { + display: flex; + flex-direction: column; + min-width: 0; + line-height: 1.25; +} + +.np-label { + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pink); +} + +.np-track { + font-size: 0.8rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.np-artist { + font-size: 0.72rem; + color: var(--subtext-0); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* Page nav, pinned bottom-left, selected item with a pointer triangle */ .nav { position: fixed; @@ -427,3 +528,245 @@ body:has(.tech-stack) .hub { transform: translateY(-2px) scale(1.04); box-shadow: 0 4px 12px rgba(245, 194, 231, 0.25); } + +/* ===== Secret Cat Modes toast ===== */ +.cat-toast { + position: fixed; + left: 50%; + bottom: 4.5rem; + transform: translateX(-50%) translateY(8px); + z-index: 2147483647; + padding: 0.4rem 0.9rem; + border-radius: 999px; + background: var(--crust); + border: 1px solid var(--pink); + color: var(--text); + font-size: 0.8rem; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.cat-toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ===== Version tag (top-right) ===== */ +.version-tag { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 6; + padding: 0.25rem 0.6rem; + border-radius: 999px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--subtext-0); + font-size: 0.72rem; + text-decoration: none; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.version-tag:hover { + color: var(--pink); + border-color: var(--pink); +} + +/* ===== Changelog page ===== */ +html:has(.changelog), +body:has(.changelog) { + height: auto; + min-height: 100dvh; + overflow-y: auto; +} + +body:has(.changelog) { + align-items: flex-start; +} + +body:has(.changelog) .hub { + max-width: 540px; +} + +.changelog { + display: flex; + flex-direction: column; + gap: 1rem; + padding-bottom: 4.5rem; +} + +.release { + background: var(--surface-0); + border: 1px solid var(--surface-1); + border-radius: 14px; + padding: 1rem 1.15rem; +} + +.release-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.6rem; +} + +.release-version { + font-weight: 700; + font-size: 1.05rem; + color: var(--pink); +} + +.release-date { + font-size: 0.78rem; + color: var(--subtext-0); +} + +.release-notes { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.release-notes li { + font-size: 0.88rem; + line-height: 1.4; + color: var(--text); +} + +.tag { + display: inline-block; + margin-right: 0.4rem; + padding: 0.05rem 0.45rem; + border-radius: 6px; + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + vertical-align: 1px; +} + +.tag-add { + background: rgba(166, 227, 161, 0.18); + color: var(--green); +} + +.tag-change { + background: rgba(137, 180, 250, 0.18); + color: var(--blue); +} + +/* ===== Cat picker modal ===== */ +.cat-picker { + position: fixed; + inset: 0; + z-index: 2147483646; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(17, 17, 27, 0.65); + backdrop-filter: blur(2px); +} + +.cat-picker[hidden] { display: none; } + +.cat-picker-panel { + width: min(94vw, 430px); + max-height: 82vh; + overflow-y: auto; + background: var(--base); + border: 1px solid var(--surface-1); + border-radius: 16px; + padding: 1rem; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55); +} + +.cat-picker-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.85rem; + font-weight: 700; + color: var(--pink); +} + +.cat-picker-close { + background: none; + border: none; + color: var(--subtext-0); + font-size: 1.35rem; + line-height: 1; + cursor: pointer; + padding: 0 0.25rem; +} + +.cat-picker-close:hover { color: var(--text); } + +.cat-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.6rem; +} + +.cat-option { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + padding: 0.75rem 0.4rem 0.6rem; + border-radius: 12px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--text); + font-family: inherit; + font-size: 0.74rem; + text-align: center; + cursor: pointer; + transition: transform 0.15s ease, border-color 0.15s ease; +} + +.cat-option:hover:not(.locked) { + transform: translateY(-2px); + border-color: var(--pink); +} + +.cat-option.current { + border-color: var(--pink); + box-shadow: inset 0 0 0 1px var(--pink); +} + +.cat-option.locked { cursor: default; opacity: 0.75; } + +.cat-preview { + width: 32px; + height: 32px; + margin: 6px 0 10px; + background-repeat: no-repeat; + image-rendering: pixelated; + transform: scale(1.6); + transform-origin: center; +} + +.cat-name { font-weight: 500; } + +.cat-lock { + font-size: 0.62rem; + color: var(--subtext-0); +} + +.cat-hint { + margin: 0.85rem 0 0; + font-size: 0.68rem; + color: var(--subtext-0); + text-align: center; +} + +@media (max-width: 420px) { + .cat-grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/index.html b/index.html index 4d28e37..256854a 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,19 @@ + + + + v1.4.0 +
Clove Twilight avatar @@ -190,6 +203,8 @@ + + \ No newline at end of file diff --git a/js/cat-modes.js b/js/cat-modes.js new file mode 100644 index 0000000..64b4acf --- /dev/null +++ b/js/cat-modes.js @@ -0,0 +1,136 @@ +const CAT_MODES = [ + { name: "Classic", filter: "none" }, + { name: "Shadow Cat", filter: "invert(1) drop-shadow(0 0 3px #cba6f7)" }, + { name: "Ghost Cat", filter: "grayscale(1) brightness(1.7) opacity(0.55) drop-shadow(0 0 4px #89dceb)" }, + { name: "CRT Cat", filter: "invert(48%) sepia(80%) saturate(2000%) hue-rotate(85deg) brightness(0.9) contrast(1.2)" }, + { name: "Vaporwave Cat", filter: "invert(60%) sepia(90%) saturate(3000%) hue-rotate(280deg) brightness(0.95)" }, + { name: "Gold Cat", filter: "invert(75%) sepia(85%) saturate(1400%) hue-rotate(8deg) brightness(1.0)" }, + { name: "Sapphire Cat", filter: "invert(45%) sepia(90%) saturate(2500%) hue-rotate(200deg) brightness(1.0)" }, +]; +const UNLOCK_EVERY = 5; // clicks needed to unlock each new mode +const SPRITE = "/images/oneko.gif"; +const IDLE_POS = "-96px -96px"; // idle frame of the sprite sheet + +(function catModes() { + const oneko = document.getElementById("oneko"); + if (!oneko) return; + + oneko.style.pointerEvents = "auto"; + oneko.style.cursor = "pointer"; + + const ls = window.localStorage; + let clicks = parseInt(ls.getItem("onekoClicks") || "0", 10); + let mode = parseInt(ls.getItem("onekoMode") || "0", 10); + + const unlockedCount = () => + Math.min(CAT_MODES.length, 1 + Math.floor(clicks / UNLOCK_EVERY)); + const isUnlocked = (i) => i < unlockedCount(); + const apply = (i) => (oneko.style.filter = CAT_MODES[i].filter); + + /* ---------- picker overlay (no visible trigger — press C to find it) ---------- */ + const overlay = document.createElement("div"); + overlay.className = "cat-picker"; + overlay.hidden = true; + overlay.innerHTML = ` + `; + document.body.appendChild(overlay); + const grid = overlay.querySelector(".cat-grid"); + + function renderGrid() { + grid.innerHTML = ""; + CAT_MODES.forEach((c, i) => { + const unlocked = isUnlocked(i); + const opt = document.createElement(unlocked ? "button" : "div"); + opt.className = + "cat-option" + (unlocked ? "" : " locked") + (i === mode ? " current" : ""); + if (unlocked) opt.type = "button"; + const previewFilter = unlocked ? c.filter : "brightness(0) opacity(0.3)"; + opt.innerHTML = ` + + ${unlocked ? c.name : "???"}`; + if (unlocked) opt.addEventListener("click", () => selectMode(i)); + grid.appendChild(opt); + }); + } + + function selectMode(i) { + mode = i; + ls.setItem("onekoMode", String(i)); + apply(i); + renderGrid(); + } + + const openPicker = () => { + renderGrid(); + overlay.hidden = false; + }; + const closePicker = () => (overlay.hidden = true); + const togglePicker = () => (overlay.hidden ? openPicker() : closePicker()); + + overlay + .querySelector(".cat-picker-close") + .addEventListener("click", closePicker); + overlay.addEventListener("click", (e) => { + if (e.target === overlay) closePicker(); + }); + document.addEventListener("keydown", (e) => { + // ignore while typing in a field or with modifier keys held + const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || ""); + if (e.key === "Escape" && !overlay.hidden) { + closePicker(); + } else if ( + (e.key === "c" || e.key === "C") && + !e.ctrlKey && !e.metaKey && !e.altKey && !typing + ) { + togglePicker(); + } + }); + + /* ---------- toast ---------- */ + let toastEl, toastTimer; + function toast(msg) { + if (!toastEl) { + toastEl = document.createElement("div"); + toastEl.className = "cat-toast"; + document.body.appendChild(toastEl); + } + toastEl.textContent = msg; + toastEl.classList.remove("show"); + void toastEl.offsetWidth; + toastEl.classList.add("show"); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700); + } + + /* ---------- init + cat click ---------- */ + mode = Math.max(0, Math.min(mode, unlockedCount() - 1)); + apply(mode); + + oneko.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const before = unlockedCount(); + clicks += 1; + ls.setItem("onekoClicks", String(clicks)); + const after = unlockedCount(); + + if (after > before) { + mode = after - 1; + toast(`✨ Unlocked: ${CAT_MODES[mode].name}!`); + } else { + mode = (mode + 1) % after; + toast(CAT_MODES[mode].name); + } + + ls.setItem("onekoMode", String(mode)); + apply(mode); + if (!overlay.hidden) renderGrid(); + }); +})(); diff --git a/js/now-playing.js b/js/now-playing.js new file mode 100644 index 0000000..1d5c7cc --- /dev/null +++ b/js/now-playing.js @@ -0,0 +1,65 @@ +const LASTFM_USER = "Real_AlexTLM"; +const LASTFM_API_KEY = "768e8bd0d366f4d6c7874740ca6610ad"; + +const POLL_MS = 30000; // refresh every 30s + +(function nowPlaying() { + const el = document.getElementById("now-playing"); + if (!el) return; + + // Don't fire until configured (keeps the widget hidden on a fresh clone) + if ( + LASTFM_USER === "YOUR_LASTFM_USERNAME" || + LASTFM_API_KEY === "YOUR_LASTFM_API_KEY" + ) { + return; + } + + const artEl = el.querySelector(".np-art"); + const labelEl = el.querySelector(".np-label"); + const trackEl = el.querySelector(".np-track"); + const artistEl = el.querySelector(".np-artist"); + + const endpoint = + "https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks" + + `&user=${encodeURIComponent(LASTFM_USER)}` + + `&api_key=${encodeURIComponent(LASTFM_API_KEY)}` + + "&format=json&limit=1"; + + async function update() { + try { + const res = await fetch(endpoint); + if (!res.ok) return; + const data = await res.json(); + const track = data?.recenttracks?.track?.[0]; + if (!track) return; + + const nowPlaying = track["@attr"]?.nowplaying === "true"; + const name = track.name || ""; + const artist = track.artist?.["#text"] || ""; + const url = track.url || `https://www.last.fm/user/${LASTFM_USER}`; + const images = track.image || []; + const art = images[images.length - 1]?.["#text"] || ""; + + labelEl.textContent = nowPlaying ? "Now playing" : "Last played"; + trackEl.textContent = name; + artistEl.textContent = artist; + el.href = url; + el.classList.toggle("is-live", nowPlaying); + + if (art) { + artEl.src = art; + artEl.style.display = ""; + } else { + artEl.style.display = "none"; + } + + el.hidden = false; + } catch (e) { + /* network hiccup — keep last state, try again next tick */ + } + } + + update(); + setInterval(update, POLL_MS); +})(); diff --git a/tech-stack/index.html b/tech-stack/index.html index a64ef77..967539b 100644 --- a/tech-stack/index.html +++ b/tech-stack/index.html @@ -4,21 +4,21 @@ - Clove Twilight - Link Center + Clove Twilight - Tech Stack - + - + - + @@ -26,15 +26,15 @@ - + - + - + @@ -45,7 +45,19 @@ Tech Stack
- + + + + v1.4.0 +
Clove Twilight avatar @@ -145,6 +157,8 @@ + + \ No newline at end of file diff --git a/template/index.html b/template/index.html new file mode 100644 index 0000000..bb985d4 --- /dev/null +++ b/template/index.html @@ -0,0 +1,83 @@ + + + + + + + Clove Twilight - Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Clove Twilight avatar +

Clove Twilight

+

(fae/faer)

+

Template

+
+

Template

+

This is a simple template for the clove.is-a.dev website, it's to make it easier for myself to create new pages.

+
+ + + + + + + + \ No newline at end of file