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 hour = now.getHours();
const path = window.location.pathname; const path = window.location.pathname;
const state = readState(); 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 // EDITABLE CONTENT
@@ -474,6 +512,41 @@
// UI HELPERS // 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) { function toast(message, duration) {
let el = document.querySelector(".hidden-toast"); let el = document.querySelector(".hidden-toast");
if (!el) { if (!el) {
@@ -482,7 +555,7 @@
el.setAttribute("role", "status"); el.setAttribute("role", "status");
document.body.appendChild(el); document.body.appendChild(el);
} }
el.textContent = message; setHiddenMessage(el, message);
el.classList.add("is-visible"); el.classList.add("is-visible");
window.clearTimeout(el._timer); window.clearTimeout(el._timer);
el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600); el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600);
@@ -542,17 +615,17 @@
const note = document.createElement("p"); const note = document.createElement("p");
note.className = "hidden-footer-note"; note.className = "hidden-footer-note";
const base = pick(details, "footer"); 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); footer.appendChild(note);
} }
function addHomepageGreeting(memory) { function addHomepageGreeting(memory) {
const target = document.querySelector("#db-greeting"); const target = document.querySelector("#db-greeting");
if (!target) return; if (!target) return;
target.textContent = pick(greetings, `greeting-${memory.visits}`); setHiddenMessage(target, pick(greetings, `greeting-${memory.visits}`));
const extra = document.createElement("p"); const extra = document.createElement("p");
extra.className = "hidden-greeting"; 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); target.closest("div")?.appendChild(extra);
} }
@@ -665,7 +738,8 @@
function showDriftNote(text, art) { function showDriftNote(text, art) {
const panel = document.createElement("aside"); const panel = document.createElement("aside");
panel.className = "hidden-drift-note"; 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()); panel.querySelector("button").addEventListener("click", () => panel.remove());
document.body.appendChild(panel); document.body.appendChild(panel);
} }
@@ -813,7 +887,7 @@
if (comments && !comments.querySelector(".hidden-guestbook-line")) { if (comments && !comments.querySelector(".hidden-guestbook-line")) {
const line = document.createElement("p"); const line = document.createElement("p");
line.className = "hidden-guestbook-line"; line.className = "hidden-guestbook-line";
line.textContent = pick(lore.fakeUsers, "comment-lore"); setHiddenMessage(line, pick(lore.fakeUsers, "comment-lore"));
comments.appendChild(line); comments.appendChild(line);
} }
} }
@@ -886,7 +960,8 @@
const visits = readState().layerVisits || {}; const visits = readState().layerVisits || {};
target.innerHTML = familyLayers.map((line, layer) => { target.innerHTML = familyLayers.map((line, layer) => {
const count = visits[layer] || 0; 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(""); }).join("");
} }
@@ -910,7 +985,7 @@
seconds += 1; seconds += 1;
count.textContent = String(seconds); count.textContent = String(seconds);
if ([30, 90, 180].includes(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); }, 1000);
} }
@@ -933,7 +1008,7 @@
if (event.key !== "Enter") return; if (event.key !== "Enter") return;
const value = input.value.trim().toLowerCase(); const value = input.value.trim().toLowerCase();
const line = document.createElement("p"); 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); log.appendChild(line);
input.value = ""; input.value = "";
}); });

View File

@@ -50,6 +50,53 @@
transform: translateY(0); 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 { .hidden-corner-object {
position: fixed; position: fixed;
left: 0.65rem; left: 0.65rem;

View File

@@ -150,12 +150,12 @@ CHARACTER_REGISTRY = {
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"], "aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
"territoryColor": "#d8a95a", "territoryColor": "#d8a95a",
"glow": "#f0b85c", "glow": "#f0b85c",
"avatar": "/assets/avatars/young-z.jpeg", "avatar": "https://zainezq.com/assets/avatars/young-z.jpeg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": { "expressions": {
"default": "/assets/avatars/young-z.jpeg", "default": "https://zainezq.com/assets/avatars/young-z.jpeg",
"nostalgic": "/assets/avatars/young-z.jpeg", "nostalgic": "https://zainezq.com/assets/avatars/young-z.jpeg",
"playful": "/assets/avatars/young-z.jpeg", "playful": "https://zainezq.com/assets/avatars/young-z.jpeg",
}, },
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "nostalgic/playful memories", "anchor": {"x": 0.23, "y": 0.7}, "shimmer": "soft"}, "observatory": {"territory": "nostalgic/playful memories", "anchor": {"x": 0.23, "y": 0.7}, "shimmer": "soft"},
@@ -172,9 +172,9 @@ CHARACTER_REGISTRY = {
"aliases": ["Z", "z", "zaine"], "aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a", "territoryColor": "#d6c38a",
"glow": "#ead68e", "glow": "#ead68e",
"avatar": "/assets/avatars/z.jpeg", "avatar": "https://zainezq.com/assets/avatars/z.jpeg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {"default": "/assets/avatars/z.jpeg", "calm": "/assets/avatars/z.jpeg"}, "expressions": {"default": "https://zainezq.com/assets/avatars/z.jpeg", "calm": "https://zainezq.com/assets/avatars/z.jpeg"},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "self/archive memories", "anchor": {"x": 0.5, "y": 0.52}, "shimmer": "steady"}, "observatory": {"territory": "self/archive memories", "anchor": {"x": 0.5, "y": 0.52}, "shimmer": "steady"},
"presenceTones": ["warm", "soft", "hopeful"], "presenceTones": ["warm", "soft", "hopeful"],
@@ -190,12 +190,12 @@ CHARACTER_REGISTRY = {
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"], "aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089", "territoryColor": "#7fb089",
"glow": "#9dd3a6", "glow": "#9dd3a6",
"avatar": "/assets/avatars/aphy.jpeg", "avatar": "https://zainezq.com/assets/avatars/aphy.jpeg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": { "expressions": {
"default": "/assets/avatars/aphy.jpeg", "default": "https://zainezq.com/assets/avatars/aphy.jpeg",
"system": "/assets/avatars/aphy.jpeg", "system": "https://zainezq.com/assets/avatars/aphy.jpeg",
"amused": "/assets/avatars/aphy.jpeg", "amused": "https://zainezq.com/assets/avatars/aphy.jpeg",
}, },
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "system/symbolic areas", "anchor": {"x": 0.77, "y": 0.35}, "shimmer": "diagnostic"}, "observatory": {"territory": "system/symbolic areas", "anchor": {"x": 0.77, "y": 0.35}, "shimmer": "diagnostic"},
@@ -212,13 +212,13 @@ CHARACTER_REGISTRY = {
"aliases": ["Lima", "lima"], "aliases": ["Lima", "lima"],
"territoryColor": "#d06b78", "territoryColor": "#d06b78",
"glow": "#f2c58b", "glow": "#f2c58b",
"avatar": "/assets/avatars/lima.jpg", "avatar": "https://zainezq.com/assets/avatars/lima.jpg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": { "expressions": {
"default": "/assets/avatars/lima.jpg", "default": "https://zainezq.com/assets/avatars/lima.jpg",
"calm": "/assets/avatars/lima.jpg", "calm": "https://zainezq.com/assets/avatars/lima.jpg",
"warm": "/assets/avatars/lima.jpg", "warm": "https://zainezq.com/assets/avatars/lima.jpg",
"protective": "/assets/avatars/lima.jpg", "protective": "https://zainezq.com/assets/avatars/lima.jpg",
}, },
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "warm/protective arcs", "anchor": {"x": 0.29, "y": 0.36}, "shimmer": "warm"}, "observatory": {"territory": "warm/protective arcs", "anchor": {"x": 0.29, "y": 0.36}, "shimmer": "warm"},
@@ -235,12 +235,12 @@ CHARACTER_REGISTRY = {
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"], "aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
"territoryColor": "#75a9bd", "territoryColor": "#75a9bd",
"glow": "#9ccddd", "glow": "#9ccddd",
"avatar": "/assets/avatars/sensei-chi.jpeg", "avatar": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": { "expressions": {
"default": "/assets/avatars/sensei-chi.jpeg", "default": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"reflective": "/assets/avatars/sensei-chi.jpeg", "reflective": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"wise": "/assets/avatars/sensei-chi.jpeg", "wise": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
}, },
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "reflective areas", "anchor": {"x": 0.68, "y": 0.68}, "shimmer": "quiet"}, "observatory": {"territory": "reflective areas", "anchor": {"x": 0.68, "y": 0.68}, "shimmer": "quiet"},
@@ -257,12 +257,12 @@ CHARACTER_REGISTRY = {
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"], "aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
"territoryColor": "#a58ac9", "territoryColor": "#a58ac9",
"glow": "#c2a4ee", "glow": "#c2a4ee",
"avatar": "/assets/avatars/future-z.jpeg", "avatar": "https://zainezq.com/assets/avatars/future-z.jpeg",
"fallbackAvatar": "/assets/avatars/z.jpeg", "fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": { "expressions": {
"default": "/assets/avatars/future-z.jpeg", "default": "https://zainezq.com/assets/avatars/future-z.jpeg",
"temporal": "/assets/avatars/future-z.jpeg", "temporal": "https://zainezq.com/assets/avatars/future-z.jpeg",
"reassuring": "/assets/avatars/future-z.jpeg", "reassuring": "https://zainezq.com/assets/avatars/future-z.jpeg",
}, },
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82}, "sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "temporal regions", "anchor": {"x": 0.73, "y": 0.18}, "shimmer": "temporal"}, "observatory": {"territory": "temporal regions", "anchor": {"x": 0.73, "y": 0.18}, "shimmer": "temporal"},