avatars plus other changes
All checks were successful
Build Org Website / build (push) Successful in 51s

This commit is contained in:
2026-05-11 15:44:30 +01:00
parent ae591de204
commit 5df4871ecc
7 changed files with 532 additions and 18 deletions

View File

@@ -0,0 +1,150 @@
(function () {
"use strict";
const fallback = "z";
const sharedSizes = {
marker: 18,
tiny: 22,
small: 32,
medium: 44,
large: 64,
focus: 88
};
const characters = {
"young z": {
id: "young z",
displayLabel: "young z",
avatar: "/assets/avatars/young-z.jpeg",
glow: "#d8a95a",
accent: "#f4bf5f",
fallback,
expressions: {
calm: "/assets/avatars/young-z.jpeg",
happy: "/assets/avatars/young-z.jpeg",
surprised: "/assets/avatars/young-z.jpeg"
},
sizes: sharedSizes,
observatory: {
territory: "nostalgic-playful",
presenceKeywords: ["childhood", "play", "crayon", "sun", "blanket", "game", "desk", "drawing", "nostalgia"],
atmosphere: "warm childhood light",
focusScale: 1.08
}
},
z: {
id: "z",
displayLabel: "z",
avatar: "/assets/avatars/z.jpeg",
glow: "#d6c38a",
accent: "#d8c891",
fallback,
expressions: {
calm: "/assets/avatars/z.jpeg",
receptive: "/assets/avatars/z.jpeg"
},
sizes: sharedSizes,
observatory: {
territory: "archive-return",
presenceKeywords: ["website", "archive", "observer", "traveler", "making", "return", "home"],
atmosphere: "steady archive lantern",
focusScale: 1
}
},
aphy: {
id: "aphy",
displayLabel: "aphy",
avatar: "/assets/avatars/aphy.jpeg",
glow: "#7fb089",
accent: "#83d4df",
fallback,
expressions: {
calm: "/assets/avatars/aphy.jpeg",
curious: "/assets/avatars/aphy.jpeg",
thinking: "/assets/avatars/aphy.jpeg"
},
sizes: sharedSizes,
observatory: {
territory: "system-symbolic",
presenceKeywords: ["system", "terminal", "logic", "error", "debug", "console", "css", "search", "backup", "symbol"],
atmosphere: "soft diagnostic glow",
focusScale: 1.04
}
},
lima: {
id: "lima",
displayLabel: "lima",
avatar: "/assets/avatars/lima.jpg",
glow: "#d06b78",
accent: "#e7a176",
fallback,
expressions: {
calm: "/assets/avatars/lima.jpg",
happy: "/assets/avatars/lima.jpg",
protective: "/assets/avatars/lima.jpg"
},
sizes: sharedSizes,
observatory: {
territory: "warm-protective",
presenceKeywords: ["warm", "home", "safe", "protect", "kitchen", "ring", "love", "rest", "care", "light"],
atmosphere: "protective kitchen warmth",
focusScale: 1.12
}
},
"sensei chi": {
id: "sensei chi",
displayLabel: "sensei chi",
avatar: "/assets/avatars/sensei-chi.jpeg",
glow: "#75a9bd",
accent: "#9ec4be",
fallback,
expressions: {
calm: "/assets/avatars/sensei-chi.jpeg",
reflective: "/assets/avatars/sensei-chi.jpeg"
},
sizes: sharedSizes,
observatory: {
territory: "reflective-patient",
presenceKeywords: ["quiet", "reflect", "patience", "tea", "garden", "lesson", "wisdom", "still"],
atmosphere: "slow reflective tea light",
focusScale: 1.06
}
},
"future z": {
id: "future z",
displayLabel: "future z",
avatar: "/assets/avatars/future-z.jpeg",
glow: "#a58ac9",
accent: "#b7a6cf",
fallback,
expressions: {
calm: "/assets/avatars/future-z.jpeg",
nostalgic: "/assets/avatars/future-z.jpeg",
warning: "/assets/avatars/future-z.jpeg"
},
sizes: sharedSizes,
observatory: {
territory: "temporal",
presenceKeywords: ["future", "time", "age 40", "later", "return", "warning", "ordinary", "clock"],
atmosphere: "distant temporal shimmer",
focusScale: 1.1
}
}
};
function get(character) {
return characters[character] || characters[fallback];
}
function source(character, expression) {
const config = get(character);
return (config.expressions && expression && config.expressions[expression]) || config.avatar || characters[fallback].avatar;
}
window.MemoryAvatarRegistry = {
fallback,
characters,
get,
source
};
}());

View File

@@ -103,6 +103,17 @@
}
};
const AVATAR_REGISTRY = window.MemoryAvatarRegistry || {
fallback: "z",
characters: CHARACTER_REGISTRY,
get(character) {
return this.characters[character] || this.characters[this.fallback] || {};
},
source(character) {
return this.get(character).avatar || "";
}
};
const familyLayers = [
"Layer 0: surface website - notes, pages, tools, normal navigation.",
"Layer 1: casual hidden jokes - aphy notices clicks, typos, tabs, and old-web habits.",
@@ -642,9 +653,15 @@
return `<svg width="36" height="36" viewBox="0 0 36 36" aria-hidden="true" focusable="false"><rect x="2" y="2" width="32" height="32" rx="10" fill="${secondary}" opacity=".34"></rect><circle cx="18" cy="18" r="15" fill="${accent}" opacity=".18"></circle>${motif}<path d="M10 29c1.6-5.1 5-7.4 8.2-7.4S25 23.9 27 29" fill="${clothes}" opacity=".94"></path><path d="M9.5 16.4c.2-6.4 4.1-10.3 8.8-10.3s8.2 3.8 8.4 10.3c-2-2-4.4-3.1-7.2-3.1-3.7 0-6.7 1.1-10 3.1z" fill="${hair}"></path><path d="M10.6 17.8c0-5.3 3.4-8.7 7.7-8.7s7.7 3.4 7.7 8.7c0 5.9-3.3 9.7-7.7 9.7s-7.7-3.8-7.7-9.7z" fill="${skin}"></path><path d="M10.7 16.2c2.4-4.2 6.5-5.3 12.1-4.4 1.1.8 2.2 2.2 3 4.1-3.6-.7-6.5-2.2-8.8-4.1-1.3 2-3.5 3.6-6.3 4.4z" fill="${hair}"></path>${eye}<circle cx="12.4" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><circle cx="24.2" cy="20" r="1.25" fill="#ef8e8e" opacity=".34"></circle><path d="${mouth}" stroke="#3f2d29" stroke-width="1.45" stroke-linecap="round" fill="none"></path><text x="28.8" y="31.4" text-anchor="middle" font-size="7" fill="#fff7e8" font-family="monospace">${symbol}</text></svg>`;
}
function avatarHtml(character, text) {
function avatarHtml(character, text, size = "small", extraClass = "") {
const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z;
return `<span class="hidden-avatar" style="--hidden-avatar-color:${config.territoryColor || "#d3a64d"}" title="${escapeHtml(config.displayLabel || character)}">${avatarSvg(character, avatarExpression(character, text))}</span>`;
const portrait = AVATAR_REGISTRY.get(character);
const expression = avatarExpression(character, text);
const src = AVATAR_REGISTRY.source(character, expression);
const color = portrait.glow || portrait.accent || config.territoryColor || "#d3a64d";
const label = config.displayLabel || portrait.displayLabel || character;
const fallback = escapeHtml(config.symbol || "?");
return `<span class="hidden-avatar hidden-avatar--${escapeHtml(size)} ${escapeHtml(extraClass)}" style="--hidden-avatar-color:${escapeHtml(color)}" title="${escapeHtml(label)}" data-character="${escapeHtml(character)}" data-expression="${escapeHtml(expression)}"><img src="${escapeHtml(src)}" alt="" loading="lazy" decoding="async" onerror="this.hidden=true;this.nextElementSibling.hidden=false"><span class="hidden-avatar__fallback" hidden>${fallback}</span></span>`;
}
function toast(message, duration, character) {

View File

@@ -9,6 +9,53 @@
"A tiny bell rings in a room you have not built yet.",
"The page remembers that you came here."
];
const avatarRegistry = window.MemoryAvatarRegistry || { fallback: "z", characters: {} };
function avatarConfig(character) {
return avatarRegistry.characters?.[character] || avatarRegistry.characters?.[avatarRegistry.fallback || "z"] || {};
}
function avatarSource(character, expression = "calm") {
const config = avatarConfig(character);
return config.expressions?.[expression] || config.avatar || avatarConfig("z").avatar || "";
}
function avatarExpression(entry, character) {
const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.symbols || []), ...(entry?.tags || []), ...(entry?.narrativeArcs || [])].join(" ").toLowerCase();
if (character === "aphy" && /error|terminal|system|diagnostic|debug|console|search|symbol/.test(text)) return "thinking";
if (character === "lima" && /safe|home|warm|protect|care|eat|rest|light/.test(text)) return "protective";
if (character === "young z" && /draw|crayon|game|play|sun|blanket|child/.test(text)) return "surprised";
if (character === "future z" && /time|future|return|ordinary|warning|later/.test(text)) return "nostalgic";
if (character === "sensei chi" && /quiet|tea|patience|lesson|garden|wisdom/.test(text)) return "reflective";
return "calm";
}
function primaryCharacter(entry) {
return characterPresence(entry)[0]?.character || (entry?.characters || [])[0] || "z";
}
function characterPresence(entry) {
const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.pageLocation, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.tags || []), ...(entry?.symbols || []), ...(entry?.narrativeArcs || [])].join(" ").toLowerCase();
const scores = Object.keys(avatarRegistry.characters || {}).map((character) => {
const config = avatarConfig(character);
let score = (entry?.characters || []).includes(character) ? 10 : 0;
(config.observatory?.presenceKeywords || []).forEach((keyword) => {
if (text.includes(String(keyword).toLowerCase())) score += 3;
});
if (character === "future z" && entryDepth(entry || {}) === 4) score += 3;
return { character, score };
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
return scores.length ? scores : [{ character: "z", score: 1 }];
}
function avatarHtml(character, entry, size = "small", extraClass = "") {
const config = avatarConfig(character);
const expression = avatarExpression(entry, character);
const src = avatarSource(character, expression);
const label = config.displayLabel || character;
const color = config.glow || config.accent || "#d3a64d";
return `<span class="observatory-avatar observatory-avatar--${escapeAttr(size)} ${escapeAttr(extraClass)}" style="--avatar-glow:${escapeAttr(color)}" title="${escapeAttr(label)}" data-character="${escapeAttr(character)}"><img src="${escapeAttr(src)}" alt="" loading="lazy" decoding="async" onerror="this.hidden=true;this.nextElementSibling.hidden=false"><span class="observatory-avatar__fallback" hidden>${escapeHtml((label || "?").slice(0, 1))}</span></span>`;
}
document.addEventListener("DOMContentLoaded", () => {
const root = $(".play-root[data-play-page]");
@@ -945,8 +992,14 @@
const angle = (index / entries.length) * Math.PI * 2 - Math.PI / 2;
const radius = 34 + entryDepth(entry) * 8;
const active = selected.some((item) => item.id === entry.id);
const character = primaryCharacter(entry);
const presence = characterPresence(entry)[0]?.score || 1;
const config = avatarConfig(character);
const atmosphere = config.observatory?.atmosphere || "observatory light";
const focus = active ? " focus" : presence >= 10 ? " close" : presence >= 6 ? " medium" : " distant";
return `<button type="button" class="observatory-star${active ? " is-active" : ""}" data-id="${escapeAttr(entry.id)}" style="--x:${50 + Math.cos(angle) * radius}%;--y:${50 + Math.sin(angle) * radius * 0.74}%;--size:${18 + Number(entry.resonanceScore || 3) * 2}px">
<span>${escapeHtml((entry.characters || ["z"])[0].slice(0, 1))}</span>
${avatarHtml(character, entry, "marker", `observatory-star-avatar${focus}`)}
<span class="observatory-star-label">${escapeHtml(entry.title || atmosphere)}</span>
</button>`;
}).join("");
$$("[data-id]", sky).forEach((button) => {
@@ -961,6 +1014,7 @@
selected.push(entry);
title.textContent = entry.title;
reading.textContent = characterLine(entry);
root.style.setProperty("--active-avatar-glow", avatarConfig(primaryCharacter(entry)).glow || "#d6b36a");
if (selected.length === 2) saveThread(selected[0], selected[1]);
render();
}
@@ -968,7 +1022,11 @@
function renderLog() {
const threads = state.restoredThreads || [];
log.innerHTML = threads.length
? threads.slice(-6).reverse().map((thread) => `<p><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</p>`).join("")
? threads.slice(-6).reverse().map((thread) => {
const entry = entries.find((item) => item.id === thread.source || item.id === thread.target);
const character = primaryCharacter(entry || { characters: ["z"] });
return `<p class="observatory-thread-row">${avatarHtml(character, entry, "tiny")}<span><strong>${escapeHtml(thread.relation.replace("Links", ""))}</strong>: ${escapeHtml(thread.title)}</span></p>`;
}).join("")
: "<p>No thread has been restored here yet.</p>";
}
@@ -1024,8 +1082,9 @@
: patient ? "sensei chi notices the quiet you brought from another room." : "aphy has no elapsed-time anomaly to report yet.";
list.innerHTML = entries.map((entry, index) => {
const seen = visits[entry.id] || {};
const character = primaryCharacter(entry);
return `<button type="button" class="return-memory" data-id="${escapeAttr(entry.id)}">
<strong>${escapeHtml(entry.title)}</strong>
<span class="return-memory-head">${avatarHtml(character, entry, "small")}<strong>${escapeHtml(entry.title)}</strong></span>
<span>${seen.count ? `observed ${seen.count} time${seen.count === 1 ? "" : "s"}` : "not observed here yet"}</span>
</button>`;
}).join("");
@@ -1079,9 +1138,12 @@
const symbols = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 16);
function render() {
cloud.innerHTML = symbols.map(([symbol, count]) => `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
<strong>${escapeHtml(symbol)}</strong><span>${count}</span>
</button>`).join("");
cloud.innerHTML = symbols.map(([symbol, count]) => {
const character = primaryCharacter({ title: symbol, content: symbol, symbols: [symbol] });
return `<button type="button" class="symbol-chip${selected.includes(symbol) ? " is-active" : ""}" data-symbol="${escapeAttr(symbol)}">
<span class="symbol-chip-head">${avatarHtml(character, { title: symbol, symbols: [symbol] }, "tiny")}<strong>${escapeHtml(symbol)}</strong></span><span>${count}</span>
</button>`;
}).join("");
$$("[data-symbol]", cloud).forEach((button) => button.addEventListener("click", () => toggleSymbol(button.dataset.symbol)));
const known = state.discoveredSymbols || {};
routes.innerHTML = Object.keys(known).length

View File

@@ -49,8 +49,9 @@
}
.hidden-avatar {
width: 2rem;
height: 2rem;
--hidden-avatar-size: 2rem;
width: var(--hidden-avatar-size);
height: var(--hidden-avatar-size);
display: inline-grid;
place-items: center;
flex: 0 0 auto;
@@ -60,13 +61,40 @@
0 0 18px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 18%, transparent);
overflow: hidden;
animation: hidden-avatar-breathe 6s ease-in-out infinite;
transition: box-shadow 180ms ease, filter 180ms ease, transform 180ms ease;
}
.hidden-avatar svg {
.hidden-avatar--tiny { --hidden-avatar-size: 1.35rem; }
.hidden-avatar--small { --hidden-avatar-size: 2rem; }
.hidden-avatar--medium { --hidden-avatar-size: 2.75rem; }
.hidden-avatar--large { --hidden-avatar-size: 4rem; }
.hidden-avatar img,
.hidden-avatar__fallback {
width: 100%;
height: 100%;
display: block;
shape-rendering: geometricPrecision;
}
.hidden-avatar img {
object-fit: cover;
border-radius: inherit;
}
.hidden-avatar__fallback {
place-items: center;
color: color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 38%, var(--fg));
font-weight: 850;
}
.hidden-avatar__fallback:not([hidden]) {
display: grid;
}
.hidden-avatar:hover {
box-shadow: 0 0 0 1px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 54%, color-mix(in oklab, #fff 20%, transparent)),
0 0 24px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 28%, transparent);
filter: saturate(1.08);
}
.hidden-toast.is-visible {

View File

@@ -502,6 +502,7 @@
--ritual-red: #d06b78;
--ritual-green: #7fb089;
--ritual-violet: #a58ac9;
--active-avatar-glow: var(--ritual-gold);
}
.observatory-panel {
@@ -519,6 +520,7 @@
radial-gradient(circle at 50% 50%, rgba(214, 179, 106, 0.16), transparent 12%),
radial-gradient(circle at 28% 34%, rgba(117, 169, 189, 0.14), transparent 18%),
radial-gradient(circle at 72% 70%, rgba(208, 107, 120, 0.12), transparent 16%),
radial-gradient(circle at 50% 48%, color-mix(in oklab, var(--active-avatar-glow) 14%, transparent), transparent 26%),
repeating-radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07) 0 1px, transparent 1px 58px),
var(--ritual-sky);
}
@@ -547,6 +549,8 @@
font-weight: 900;
transform: translate(-50%, -50%);
box-shadow: 0 0 18px rgba(214, 179, 106, 0.28);
transition: width 180ms ease, height 180ms ease, box-shadow 180ms ease, transform 180ms ease, filter 180ms ease;
animation: observatory-breathe 7s ease-in-out infinite;
}
.observatory-star:hover,
@@ -554,9 +558,10 @@
background: radial-gradient(circle, #fff8e8 0 18%, var(--ritual-red) 19% 56%, rgba(208, 107, 120, 0.22) 57% 100%);
color: #fff8e8;
box-shadow: 0 0 28px rgba(208, 107, 120, 0.46);
filter: saturate(1.08);
}
.observatory-star span {
.observatory-star > span:not(.observatory-avatar) {
display: grid;
place-items: center;
width: 100%;
@@ -565,6 +570,110 @@
text-transform: uppercase;
}
.observatory-avatar {
--avatar-size: 2rem;
display: inline-grid;
place-items: center;
width: var(--avatar-size);
height: var(--avatar-size);
flex: 0 0 auto;
border-radius: 50%;
overflow: hidden;
background: color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 20%, #17130f);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 54%, rgba(255, 248, 232, 0.28)),
0 0 20px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 28%, transparent);
}
.observatory-avatar--marker { --avatar-size: 100%; }
.observatory-avatar--tiny { --avatar-size: 1.35rem; }
.observatory-avatar--small { --avatar-size: 2rem; }
.observatory-avatar--medium { --avatar-size: 2.75rem; }
.observatory-avatar--large { --avatar-size: 4rem; }
.observatory-avatar img,
.observatory-avatar__fallback {
display: block;
width: 100%;
height: 100%;
}
.observatory-avatar img {
object-fit: cover;
border-radius: inherit;
}
.observatory-avatar__fallback:not([hidden]) {
display: grid;
place-items: center;
color: #fff8e8;
font-weight: 900;
}
.observatory-star-avatar {
position: absolute;
inset: 0;
opacity: 0.76;
}
.observatory-star-avatar.distant {
opacity: 0.42;
transform: scale(0.58);
}
.observatory-star-avatar.medium {
opacity: 0.82;
transform: scale(0.9);
}
.observatory-star-avatar.close,
.observatory-star-avatar.focus {
opacity: 1;
transform: scale(1.08);
}
.observatory-star-avatar.focus {
transform: scale(1.22);
box-shadow: 0 0 0 2px rgba(255, 248, 232, 0.42), 0 0 34px color-mix(in oklab, var(--avatar-glow, var(--ritual-gold)) 48%, transparent);
}
.observatory-star-label {
position: absolute;
left: 50%;
top: calc(100% + 0.38rem);
width: max-content;
max-width: 13rem;
height: auto !important;
padding: 0.18rem 0.42rem;
border-radius: 999px;
background: rgba(23, 19, 15, 0.72);
color: #fff8e8;
font-size: 0.68rem !important;
line-height: 1.25;
opacity: 0;
pointer-events: none;
text-transform: none !important;
transform: translateX(-50%) translateY(-2px);
transition: opacity 160ms ease, transform 160ms ease;
}
.observatory-star:hover .observatory-star-label,
.observatory-star.is-active .observatory-star-label {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.observatory-thread-row,
.return-memory-head,
.symbol-chip-head {
display: flex;
align-items: center;
gap: 0.55rem;
}
.observatory-thread-row {
margin: 0.45rem 0;
}
.observatory-drawer,
.dream-sentence {
padding: 1rem;
@@ -591,6 +700,16 @@
gap: 0.35rem;
min-height: 100px;
text-align: left;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
.return-memory:hover,
.return-memory:focus-visible,
.symbol-chip:hover,
.symbol-chip:focus-visible {
border-color: color-mix(in oklab, var(--active-avatar-glow, var(--ritual-gold)) 38%, var(--border));
box-shadow: 0 0 18px color-mix(in oklab, var(--active-avatar-glow, var(--ritual-gold)) 18%, transparent);
transform: translateY(-1px);
}
.return-memory span,
@@ -610,6 +729,7 @@
text-align: left;
background:
linear-gradient(135deg, color-mix(in oklab, var(--surface) 88%, var(--ritual-blue) 12%), var(--surface));
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
}
.symbol-chip.is-active {
@@ -617,6 +737,11 @@
color: #fff8e8;
}
@keyframes observatory-breathe {
0%, 100% { box-shadow: 0 0 18px rgba(214, 179, 106, 0.24); }
50% { box-shadow: 0 0 26px rgba(214, 179, 106, 0.4); }
}
.symbol-chip.is-active span {
color: rgba(255, 248, 232, 0.72);
}

View File

@@ -66,6 +66,7 @@ IMAGE_ASSETS_DIR = ROOT / "assets" / "images"
HZONE_ASSETS_DIR = IMAGE_ASSETS_DIR / "hzone"
HIDDEN_DETAILS_JS = ROOT / "assets" / "scripts" / "hidden-details.js"
HIDDEN_CONTENT_JSON = ROOT / "assets" / "content" / "hidden-details.json"
AVATAR_REGISTRY_JS = ROOT / "assets" / "scripts" / "avatar-registry.js"
HIDDEN_BACKUP_DIR = ROOT / "backups" / "hidden-details"
EXCLUDED_CONTENT_DIR_NAMES = {
".agents",
@@ -270,6 +271,115 @@ CHARACTER_REGISTRY = {
"affinities": ["z", "young z", "lima", "sensei chi"],
},
}
AVATAR_REGISTRY_META = {
"young z": {
"avatar": "/assets/avatars/young-z.jpeg",
"glow": "#d8a95a",
"accent": "#f4bf5f",
"fallback": "z",
"expressions": {
"calm": "/assets/avatars/young-z.jpeg",
"happy": "/assets/avatars/young-z.jpeg",
"surprised": "/assets/avatars/young-z.jpeg",
},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "nostalgic-playful",
"presenceKeywords": ["childhood", "play", "crayon", "sun", "blanket", "game", "desk", "drawing", "nostalgia"],
"atmosphere": "warm childhood light",
"focusScale": 1.08,
},
},
"z": {
"avatar": "/assets/avatars/z.jpeg",
"glow": "#d6c38a",
"accent": "#d8c891",
"fallback": "z",
"expressions": {"calm": "/assets/avatars/z.jpeg", "receptive": "/assets/avatars/z.jpeg"},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "archive-return",
"presenceKeywords": ["website", "archive", "observer", "traveler", "making", "return", "home"],
"atmosphere": "steady archive lantern",
"focusScale": 1,
},
},
"aphy": {
"avatar": "/assets/avatars/aphy.jpeg",
"glow": "#7fb089",
"accent": "#83d4df",
"fallback": "z",
"expressions": {
"calm": "/assets/avatars/aphy.jpeg",
"curious": "/assets/avatars/aphy.jpeg",
"thinking": "/assets/avatars/aphy.jpeg",
},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "system-symbolic",
"presenceKeywords": ["system", "terminal", "logic", "error", "debug", "console", "css", "search", "backup", "symbol"],
"atmosphere": "soft diagnostic glow",
"focusScale": 1.04,
},
},
"lima": {
"avatar": "/assets/avatars/lima.jpg",
"glow": "#d06b78",
"accent": "#e7a176",
"fallback": "z",
"expressions": {
"calm": "/assets/avatars/lima.jpg",
"happy": "/assets/avatars/lima.jpg",
"protective": "/assets/avatars/lima.jpg",
},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "warm-protective",
"presenceKeywords": ["warm", "home", "safe", "protect", "kitchen", "ring", "love", "rest", "care", "light"],
"atmosphere": "protective kitchen warmth",
"focusScale": 1.12,
},
},
"sensei chi": {
"avatar": "/assets/avatars/sensei-chi.jpeg",
"glow": "#75a9bd",
"accent": "#9ec4be",
"fallback": "z",
"expressions": {"calm": "/assets/avatars/sensei-chi.jpeg", "reflective": "/assets/avatars/sensei-chi.jpeg"},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "reflective-patient",
"presenceKeywords": ["quiet", "reflect", "patience", "tea", "garden", "lesson", "wisdom", "still"],
"atmosphere": "slow reflective tea light",
"focusScale": 1.06,
},
},
"future z": {
"avatar": "/assets/avatars/future-z.jpeg",
"glow": "#a58ac9",
"accent": "#b7a6cf",
"fallback": "z",
"expressions": {
"calm": "/assets/avatars/future-z.jpeg",
"nostalgic": "/assets/avatars/future-z.jpeg",
"warning": "/assets/avatars/future-z.jpeg",
},
"sizes": {"marker": 18, "tiny": 22, "small": 32, "medium": 44, "large": 64, "focus": 88},
"observatory": {
"territory": "temporal",
"presenceKeywords": ["future", "time", "age 40", "later", "return", "warning", "ordinary", "clock"],
"atmosphere": "distant temporal shimmer",
"focusScale": 1.1,
},
},
}
for character_id, avatar_meta in AVATAR_REGISTRY_META.items():
if character_id in CHARACTER_REGISTRY:
CHARACTER_REGISTRY[character_id]["avatar"] = {**CHARACTER_REGISTRY[character_id].get("avatar", {}), **avatar_meta}
CHARACTER_REGISTRY[character_id]["glow"] = avatar_meta["glow"]
CHARACTER_REGISTRY[character_id]["accent"] = avatar_meta["accent"]
HIDDEN_CHARACTERS = list(CHARACTER_REGISTRY)
HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"]
HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"]
@@ -2604,7 +2714,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
overflow: hidden;
vertical-align: middle;
}
.avatar svg { width: 100%; height: 100%; display: block; shape-rendering: geometricPrecision; }
.avatar img, .avatar-fallback { width: 100%; height: 100%; display: block; }
.avatar img { object-fit: cover; border-radius: inherit; }
.avatar-fallback:not([hidden]) { display: grid; place-items: center; color: #fff4d3; font-size: 11px; font-weight: 850; }
.avatar--xs { --avatar-size: 18px; }
.avatar--sm { --avatar-size: 24px; }
.avatar--md { --avatar-size: 34px; }
@@ -2693,8 +2805,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
.memory-node text, .node-label text, .cluster-label text { fill: #f7ead3; paint-order: stroke; stroke: rgba(14, 11, 8, 0.82); stroke-width: 3px; stroke-linejoin: round; }
.memory-node circle { stroke: rgba(255, 246, 223, 0.82); stroke-width: 1.4; filter: drop-shadow(0 0 7px rgba(255, 232, 177, 0.18)); transition: r 160ms ease, stroke 160ms ease, opacity 160ms ease; }
.memory-node .character-halo { fill: color-mix(in oklab, var(--node-character-color, rgba(211, 166, 77, 0.46)) 8%, transparent); stroke: var(--node-character-color, rgba(211, 166, 77, 0.46)); stroke-width: 1.6; opacity: 0.34; stroke-dasharray: 2 7; animation: spirit-shimmer 8s ease-in-out infinite; }
.memory-node .node-avatar { opacity: 0; transform: scale(0.72); transition: opacity 220ms ease, transform 220ms ease; }
.zoom-2 .memory-node .node-avatar, .zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 0.96; transform: scale(1); }
.memory-node .node-avatar { opacity: 0; transform: scale(0.58); transition: opacity 220ms ease, transform 220ms ease; }
.zoom-1 .memory-node .node-avatar { opacity: 0.5; transform: scale(0.7); }
.zoom-2 .memory-node .node-avatar { opacity: 0.9; transform: scale(0.95); }
.zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 1; transform: scale(1.12); }
.zoom-0 .memory-node text { opacity: 1; }
.zoom-3 .memory-node text, .memory-node.selected text { transform: translateY(27px); }
.memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
.memory-node.compatible circle { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
@@ -3041,6 +3157,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
const $ = (id) => document.getElementById(id);
const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
let avatarClipSeq = 0;
function html(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
@@ -3060,6 +3177,18 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function avatarSvg(character, expression = "soft") {
const config = characterConfig(character);
const avatar = config.avatar || {};
const srcSvg = avatar.expressions?.[expression] || avatar.avatar || config.avatarPath || config.portrait || state.meta.characterRegistry?.z?.avatar?.avatar || "/assets/avatars/z.jpeg";
const colorSvg = config.glow || avatar.glow || config.territoryColor || avatar.accent || "#d3a64d";
const labelSvg = html(config.displayLabel || character || "z");
const symbolSvg = html(config.symbol || "?");
const clipSvg = `avatarClip-${String(character || "z").replace(/[^a-z0-9]+/gi, "-")}-${String(expression || "calm").replace(/[^a-z0-9]+/gi, "-")}-${avatarClipSeq += 1}`;
return `<svg width="36" height="36" viewBox="0 0 36 36" aria-label="${labelSvg}" role="img" focusable="false">
<defs><clipPath id="${clipSvg}"><circle cx="18" cy="18" r="16"></circle></clipPath></defs>
<circle cx="18" cy="18" r="17" fill="${html(colorSvg)}" opacity=".18"></circle>
<circle cx="18" cy="18" r="16.5" fill="rgba(18,16,13,.72)" stroke="${html(colorSvg)}" stroke-width="1.6"></circle>
<image href="${html(srcSvg)}" x="2" y="2" width="32" height="32" preserveAspectRatio="xMidYMid slice" clip-path="url(#${clipSvg})"></image>
<text x="28.8" y="31.4" text-anchor="middle" font-size="7" fill="#fff7e8" font-family="monospace">${symbolSvg}</text>
</svg>`;
const accent = avatar.accent || config.territoryColor || "#d3a64d";
const secondary = avatar.secondary || "#f6dfb4";
const hair = avatar.hair || "#5d4632";
@@ -3108,8 +3237,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
}
function avatarHtml(character, size = "sm", expression = "soft", extraClass = "") {
const config = characterConfig(character);
const color = config.territoryColor || config.avatar?.accent || "#d3a64d";
return `<span class="avatar avatar--${size} ${extraClass}" style="--avatar-color:${html(color)}" title="${html(config.displayLabel || character)} - ${html(config.avatar?.description || "observatory spirit")}">${avatarSvg(character, expression)}</span>`;
const avatar = config.avatar || {};
const color = config.glow || avatar.glow || config.territoryColor || avatar.accent || "#d3a64d";
const src = avatar.expressions?.[expression] || avatar.avatar || state.meta.characterRegistry?.z?.avatar?.avatar || "/assets/avatars/z.jpeg";
return `<span class="avatar avatar--${size} ${extraClass}" style="--avatar-color:${html(color)}" title="${html(config.displayLabel || character)} - ${html(config.avatar?.description || avatar.observatory?.atmosphere || "observatory spirit")}"><img src="${html(src)}" alt="" loading="lazy" decoding="async" onerror="this.hidden=true;this.nextElementSibling.hidden=false"><span class="avatar-fallback" hidden>${html(config.symbol || "?")}</span></span>`;
}
function avatarChip(character, entry = null) {
const config = characterConfig(character);

View File

@@ -215,6 +215,7 @@ FMT / ARGS are passed to `format'."
(format "<script src=\"%s\" defer></script>"
(z/asset-url (format "assets/scripts/%s" f))))
'("script.js"
"avatar-registry.js"
"lunr.js"
"competency-status-board.js"
"notes.js"