avatar support next to messages x2
All checks were successful
Build Org Website / build (push) Successful in 56s

This commit is contained in:
2026-05-11 16:30:31 +01:00
parent 92093b9351
commit 1278027484
3 changed files with 160 additions and 38 deletions

View File

@@ -27,6 +27,44 @@
const hour = now.getHours();
const path = window.location.pathname;
const state = readState();
const CHARACTER_AVATARS = {
"lima": {
avatar: "/assets/avatars/lima.jpg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["lima"],
glow: "#f2c58b"
},
"aphy": {
avatar: "/assets/avatars/aphy.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["aphy"],
glow: "#9dd3a6"
},
"sensei chi": {
avatar: "/assets/avatars/sensei-chi.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["sensei chi", "sensei"],
glow: "#9ccddd"
},
"young z": {
avatar: "/assets/avatars/young-z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["young z", "young"],
glow: "#f0b85c"
},
"future z": {
avatar: "/assets/avatars/future-z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["future z", "future"],
glow: "#c2a4ee"
},
"z": {
avatar: "/assets/avatars/z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["z", "zaine"],
glow: "#ead68e"
}
};
// -----------------------------
// EDITABLE CONTENT
@@ -474,6 +512,41 @@
// UI HELPERS
// -----------------------------
function characterForMessage(message) {
const text = String(message || "").toLowerCase();
return Object.entries(CHARACTER_AVATARS).find(([, config]) => {
return (config.aliases || []).some((alias) => {
const escaped = alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(^|[^a-z0-9-])${escaped}([^a-z0-9-]|$)`).test(text);
});
})?.[0] || "";
}
function avatarMarkup(character) {
const config = CHARACTER_AVATARS[character];
if (!config) return "";
return `<img class="hidden-message-avatar" src="${escapeHtml(config.avatar)}" alt="" loading="lazy" style="--hidden-avatar-glow:${escapeHtml(config.glow)}" onerror="this.onerror=null;this.src='${escapeHtml(config.fallback)}'">`;
}
function hiddenMessageMarkup(message) {
const character = characterForMessage(message);
return character
? `${avatarMarkup(character)}<span class="hidden-message-text">${escapeHtml(message)}</span>`
: `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
}
function setHiddenMessage(el, message) {
const character = characterForMessage(message);
el.classList.toggle("has-hidden-avatar", Boolean(character));
if (character) {
el.dataset.hiddenCharacter = character;
el.innerHTML = hiddenMessageMarkup(message);
} else {
delete el.dataset.hiddenCharacter;
el.textContent = message;
}
}
function toast(message, duration) {
let el = document.querySelector(".hidden-toast");
if (!el) {
@@ -482,7 +555,7 @@
el.setAttribute("role", "status");
document.body.appendChild(el);
}
el.textContent = message;
setHiddenMessage(el, message);
el.classList.add("is-visible");
window.clearTimeout(el._timer);
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
@@ -542,17 +615,17 @@
const note = document.createElement("p");
note.className = "hidden-footer-note";
const base = pick(details, "footer");
note.textContent = memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base;
setHiddenMessage(note, memory.visits > 4 ? `${base} You have passed through ${memory.visits} times.` : base);
footer.appendChild(note);
}
function addHomepageGreeting(memory) {
const target = document.querySelector("#db-greeting");
if (!target) return;
target.textContent = pick(greetings, `greeting-${memory.visits}`);
setHiddenMessage(target, pick(greetings, `greeting-${memory.visits}`));
const extra = document.createElement("p");
extra.className = "hidden-greeting";
extra.textContent = memory.visits > 1 ? "aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies.";
setHiddenMessage(extra, memory.visits > 1 ? "aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies.");
target.closest("div")?.appendChild(extra);
}
@@ -665,7 +738,8 @@
function showDriftNote(text, art) {
const panel = document.createElement("aside");
panel.className = "hidden-drift-note";
panel.innerHTML = `<div>${escapeHtml(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
const character = characterForMessage(text);
panel.innerHTML = `<div class="hidden-drift-message${character ? " has-hidden-avatar" : ""}">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
panel.querySelector("button").addEventListener("click", () => panel.remove());
document.body.appendChild(panel);
}
@@ -813,7 +887,7 @@
if (comments && !comments.querySelector(".hidden-guestbook-line")) {
const line = document.createElement("p");
line.className = "hidden-guestbook-line";
line.textContent = pick(lore.fakeUsers, "comment-lore");
setHiddenMessage(line, pick(lore.fakeUsers, "comment-lore"));
comments.appendChild(line);
}
}
@@ -886,7 +960,8 @@
const visits = readState().layerVisits || {};
target.innerHTML = familyLayers.map((line, layer) => {
const count = visits[layer] || 0;
return `<li><strong>${escapeHtml(line)}</strong><br><span>local encounters: ${count}</span></li>`;
const character = characterForMessage(line);
return `<li class="${character ? "hidden-layer-message" : ""}"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
}).join("");
}
@@ -910,7 +985,7 @@
seconds += 1;
count.textContent = String(seconds);
if ([30, 90, 180].includes(seconds)) {
target.textContent = seconds === 30 ? "lima's note becomes easier to read." : seconds === 90 ? "sensei chi nods once." : "future z says this quiet was not wasted.";
setHiddenMessage(target, seconds === 30 ? "lima's note becomes easier to read." : seconds === 90 ? "sensei chi nods once." : "future z says this quiet was not wasted.");
}
}, 1000);
}
@@ -933,7 +1008,7 @@
if (event.key !== "Enter") return;
const value = input.value.trim().toLowerCase();
const line = document.createElement("p");
line.textContent = `> ${value}\n${commands[value] || "aphy: unknown command. sensei chi: not all unknowns are errors."}`;
setHiddenMessage(line, `> ${value}\n${commands[value] || "aphy: unknown command. sensei chi: not all unknowns are errors."}`);
log.appendChild(line);
input.value = "";
});

View File

@@ -50,6 +50,53 @@
transform: translateY(0);
}
.has-hidden-avatar {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.hidden-toast.has-hidden-avatar,
.hidden-drift-message.has-hidden-avatar {
display: flex;
}
.hidden-footer-note.has-hidden-avatar,
.hidden-greeting.has-hidden-avatar,
.hidden-guestbook-line.has-hidden-avatar {
display: flex;
justify-content: center;
}
.hidden-message-avatar {
width: 2.1rem;
height: 2.1rem;
flex: 0 0 2.1rem;
border-radius: 50%;
object-fit: cover;
border: 1px solid color-mix(in oklab, var(--hidden-avatar-glow, var(--accent)) 62%, var(--border));
box-shadow: 0 0 18px color-mix(in oklab, var(--hidden-avatar-glow, var(--accent)) 34%, transparent);
background: color-mix(in oklab, var(--surface) 82%, transparent);
}
.hidden-message-text {
min-width: 0;
}
.hidden-layer-message strong {
display: inline-flex;
align-items: center;
gap: 0.55rem;
}
.hidden-terminal-log .has-hidden-avatar {
align-items: flex-start;
}
.hidden-terminal-log .hidden-message-avatar {
margin-top: 0.1rem;
}
.hidden-corner-object {
position: fixed;
left: 0.65rem;