removing and clean
All checks were successful
Build Authoring Service / build (push) Successful in 9s

This commit is contained in:
2026-07-08 22:48:31 +01:00
parent 1528d245b4
commit d7d0054af4
4 changed files with 61 additions and 208 deletions

View File

@@ -27,45 +27,6 @@
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
// ----------------------------- // -----------------------------
@@ -151,39 +112,13 @@
// 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) { function hiddenMessageMarkup(message) {
const character = characterForMessage(message); return `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
return character
? `${avatarMarkup(character)}<span class="hidden-message-text">${escapeHtml(message)}</span>`
: `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
} }
function setHiddenMessage(el, message) { function setHiddenMessage(el, message) {
const character = characterForMessage(message); delete el.dataset.hiddenCharacter;
el.classList.toggle("has-hidden-avatar", Boolean(character)); el.innerHTML = hiddenMessageMarkup(message);
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) {
@@ -372,8 +307,7 @@
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";
const character = characterForMessage(text); panel.innerHTML = `<div class="hidden-drift-message">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
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);
} }
@@ -568,8 +502,7 @@
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;
const character = characterForMessage(line); return `<li class="hidden-layer-message"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
return `<li class="${character ? "hidden-layer-message" : ""}"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
}).join(""); }).join("");
} }

View File

@@ -94,13 +94,6 @@ 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": "https://zainezq.com/assets/avatars/young-z.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/young-z.jpeg",
"nostalgic": "https://zainezq.com/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", "shimmer": "soft"}, "observatory": {"territory": "nostalgic/playful memories", "shimmer": "soft"},
"presenceTones": ["nostalgic", "funny", "hopeful"], "presenceTones": ["nostalgic", "funny", "hopeful"],
@@ -116,31 +109,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Z", "z", "zaine"], "aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a", "territoryColor": "#d6c38a",
"glow": "#ead68e", "glow": "#ead68e",
"avatar": "https://zainezq.com/assets/avatars/z.jpeg",
"fallbackAvatar": "https://zainezq.com/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},
"observatory": {"territory": "self/archive memories", "shimmer": "steady"},
"presenceTones": ["warm", "soft", "hopeful"],
"presenceKeywords": ["website", "archive", "home", "self", "making", "return", "zaine"],
"symbol": "Z",
"motifs": ["archive", "website", "home"],
"themes": ["selfhood", "return", "making"],
"affinities": ["lima", "young z", "future z"],
},
"aphy": {
"id": "aphy",
"displayLabel": "aphy",
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089",
"glow": "#9dd3a6",
"avatar": "https://zainezq.com/assets/avatars/aphy.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/aphy.jpeg",
"system": "https://zainezq.com/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", "shimmer": "diagnostic"}, "observatory": {"territory": "system/symbolic areas", "shimmer": "diagnostic"},
"presenceTones": ["funny", "strange", "hopeful"], "presenceTones": ["funny", "strange", "hopeful"],
@@ -156,14 +124,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Lima", "lima"], "aliases": ["Lima", "lima"],
"territoryColor": "#d06b78", "territoryColor": "#d06b78",
"glow": "#f2c58b", "glow": "#f2c58b",
"avatar": "https://zainezq.com/assets/avatars/lima.jpg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/lima.jpg",
"calm": "https://zainezq.com/assets/avatars/lima.jpg",
"warm": "https://zainezq.com/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", "shimmer": "warm"}, "observatory": {"territory": "warm/protective arcs", "shimmer": "warm"},
"presenceTones": ["warm", "protective", "soft"], "presenceTones": ["warm", "protective", "soft"],
@@ -179,13 +139,6 @@ 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": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"reflective": "https://zainezq.com/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", "shimmer": "quiet"}, "observatory": {"territory": "reflective areas", "shimmer": "quiet"},
"presenceTones": ["wise", "melancholy", "soft"], "presenceTones": ["wise", "melancholy", "soft"],
@@ -201,13 +154,6 @@ 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": "https://zainezq.com/assets/avatars/future-z.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/future-z.jpeg",
"temporal": "https://zainezq.com/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", "shimmer": "temporal"}, "observatory": {"territory": "temporal regions", "shimmer": "temporal"},
"presenceTones": ["hopeful", "melancholy", "wise"], "presenceTones": ["hopeful", "melancholy", "wise"],

View File

@@ -1263,22 +1263,27 @@ HIDDEN_APP_HTML = r"""<!doctype html>
background: rgba(251, 241, 220, 0.08); background: rgba(251, 241, 220, 0.08);
font-size: 12px; font-size: 12px;
} }
.avatar-pill, .avatar-row, .character-tag { .character-pill, .character-row, .character-tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
} }
.avatar-thumb { .character-mark {
width: var(--avatar-size, 28px); width: var(--character-size, 28px);
height: var(--avatar-size, 28px); height: var(--character-size, 28px);
flex: 0 0 var(--avatar-size, 28px); flex: 0 0 var(--character-size, 28px);
border-radius: 50%; border-radius: 50%;
object-fit: cover;
border: 1px solid rgba(255, 246, 223, 0.72); border: 1px solid rgba(255, 246, 223, 0.72);
background: rgba(251, 241, 220, 0.08); background: rgba(251, 241, 220, 0.08);
box-shadow: 0 0 18px color-mix(in srgb, var(--avatar-glow, #d3a64d) 42%, transparent); box-shadow: 0 0 18px color-mix(in srgb, var(--character-glow, #d3a64d) 42%, transparent);
color: #fff4d7;
display: inline-grid;
font-size: calc(var(--character-size, 28px) * 0.46);
font-weight: 900;
line-height: 1;
place-items: center;
} }
.avatar-row { .character-row {
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
margin-top: 10px; margin-top: 10px;
@@ -1287,7 +1292,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
border-radius: 8px; border-radius: 8px;
background: rgba(251, 241, 220, 0.055); background: rgba(251, 241, 220, 0.055);
} }
.avatar-row .subtle { font-size: 12px; } .character-row .subtle { font-size: 12px; }
.character-tag { .character-tag {
min-height: 32px; min-height: 32px;
border: 1px solid rgba(232, 202, 139, 0.2); border: 1px solid rgba(232, 202, 139, 0.2);
@@ -1298,7 +1303,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
} }
.character-tag .avatar-thumb { --avatar-size: 24px; } .character-tag .character-mark { --character-size: 24px; }
.layer-key { display: grid; gap: 8px; margin-top: 14px; } .layer-key { display: grid; gap: 8px; margin-top: 14px; }
.layer-row { .layer-row {
display: grid; display: grid;
@@ -1346,7 +1351,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
.memory-node.selected { opacity: 1; filter: drop-shadow(0 0 18px rgba(255, 231, 173, 0.45)); } .memory-node.selected { opacity: 1; filter: drop-shadow(0 0 18px rgba(255, 231, 173, 0.45)); }
.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 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; }
.hit-target { fill: transparent; stroke: transparent; pointer-events: all; } .hit-target { fill: transparent; stroke: transparent; pointer-events: all; }
.memory-node .node-shell, .memory-node .node-portrait, .memory-node .node-marker, .memory-node .fragment-star { .memory-node .node-shell, .memory-node .node-shell, .memory-node .node-marker, .memory-node .fragment-star {
stroke: rgba(255, 246, 223, 0.82); stroke: rgba(255, 246, 223, 0.82);
stroke-width: 1.4; stroke-width: 1.4;
filter: drop-shadow(0 0 7px var(--node-glow, rgba(255, 232, 177, 0.18))); filter: drop-shadow(0 0 7px var(--node-glow, rgba(255, 232, 177, 0.18)));
@@ -1354,16 +1359,16 @@ HIDDEN_APP_HTML = r"""<!doctype html>
} }
.memory-node .fragment-star { stroke-width: 1.1; } .memory-node .fragment-star { stroke-width: 1.1; }
.memory-node .fragment-ring { fill: none; stroke: rgba(255, 246, 223, 0.24); stroke-width: 1; stroke-dasharray: 2 7; } .memory-node .fragment-ring { fill: none; stroke: rgba(255, 246, 223, 0.24); stroke-width: 1; stroke-dasharray: 2 7; }
.memory-node .node-breathe, .cluster-node .node-breathe { animation: avatar-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } .memory-node .node-breathe, .cluster-node .node-breathe { animation: character-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.sequence-badge { fill: rgba(18, 16, 13, 0.88); stroke: rgba(255, 243, 199, 0.68); stroke-width: 1; } .sequence-badge { fill: rgba(18, 16, 13, 0.88); stroke: rgba(255, 243, 199, 0.68); stroke-width: 1; }
.sequence-number { fill: #ffe7ad; font-size: 10px; font-weight: 850; paint-order: stroke; stroke: rgba(14, 11, 8, 0.72); stroke-width: 2px; } .sequence-number { fill: #ffe7ad; font-size: 10px; font-weight: 850; paint-order: stroke; stroke: rgba(14, 11, 8, 0.72); stroke-width: 2px; }
.selection-halo { fill: rgba(255, 231, 173, 0.1); stroke: rgba(255, 243, 199, 0.95); stroke-width: 3; stroke-dasharray: 4 9; filter: drop-shadow(0 0 22px rgba(255, 231, 173, 0.62)); animation: selection-pulse 2.8s ease-in-out infinite; pointer-events: none; } .selection-halo { fill: rgba(255, 231, 173, 0.1); stroke: rgba(255, 243, 199, 0.95); stroke-width: 3; stroke-dasharray: 4 9; filter: drop-shadow(0 0 22px rgba(255, 231, 173, 0.62)); animation: selection-pulse 2.8s ease-in-out infinite; pointer-events: none; }
.memory-node:hover .node-shell, .memory-node:hover .node-portrait, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); } .memory-node:hover .node-shell, .memory-node:hover .node-shell, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); }
.memory-node.selected .node-shell, .memory-node.selected .node-portrait, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; } .memory-node.selected .node-shell, .memory-node.selected .node-shell, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging .node-shell, .memory-node.dragging .node-portrait { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; } .memory-node.dragging .node-shell, .memory-node.dragging .node-shell { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
.memory-node.compatible .node-shell, .memory-node.compatible .node-portrait { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); } .memory-node.compatible .node-shell, .memory-node.compatible .node-shell { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
.memory-node.drop-target .node-shell, .memory-node.drop-target .node-portrait { stroke: #fff5c9; stroke-width: 4; filter: drop-shadow(0 0 24px rgba(255, 232, 177, 0.7)); } .memory-node.drop-target .node-shell, .memory-node.drop-target .node-shell { stroke: #fff5c9; stroke-width: 4; filter: drop-shadow(0 0 24px rgba(255, 232, 177, 0.7)); }
.memory-node.invalid-drop .node-shell, .memory-node.invalid-drop .node-portrait { stroke: rgba(208, 107, 120, 0.9); stroke-width: 3; } .memory-node.invalid-drop .node-shell, .memory-node.invalid-drop .node-shell { stroke: rgba(208, 107, 120, 0.9); stroke-width: 3; }
.cluster-node { cursor: pointer; transition: transform 160ms ease, filter 160ms ease; } .cluster-node { cursor: pointer; transition: transform 160ms ease, filter 160ms ease; }
.cluster-node circle { .cluster-node circle {
stroke: rgba(255, 243, 199, 0.75); stroke: rgba(255, 243, 199, 0.75);
@@ -1671,7 +1676,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
0%, 100% { opacity: 0.72; } 0%, 100% { opacity: 0.72; }
50% { opacity: 1; } 50% { opacity: 1; }
} }
@keyframes avatar-breathe { @keyframes character-breathe {
0%, 100% { transform: scale(1); opacity: 0.92; } 0%, 100% { transform: scale(1); opacity: 0.92; }
50% { transform: scale(1.035); opacity: 1; } 50% { transform: scale(1.035); opacity: 1; }
} }
@@ -1890,7 +1895,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<section class="panel"> <section class="panel">
<h2 id="drawerTitle">Choose a memory</h2> <h2 id="drawerTitle">Choose a memory</h2>
<div id="drawerSub" class="subtle">The side panel opens from the constellation.</div> <div id="drawerSub" class="subtle">The side panel opens from the constellation.</div>
<div id="drawerAvatars" class="pills" style="margin-top:10px"></div> <div id="drawerCharacters" class="pills" style="margin-top:10px"></div>
<div id="preview" class="preview" style="margin-top:10px"></div> <div id="preview" class="preview" style="margin-top:10px"></div>
</section> </section>
<section class="panel editor-panel story-editor-panel" id="storyEditorPanel"> <section class="panel editor-panel story-editor-panel" id="storyEditorPanel">
@@ -2017,7 +2022,6 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"]; const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
const $ = (id) => document.getElementById(id); 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" }; const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
const defaultAvatarPath = "/assets/avatars/z.jpeg";
const appearanceLabels = { const appearanceLabels = {
observatory: "Observatory stars", observatory: "Observatory stars",
tooltip: "Soft tooltips", tooltip: "Soft tooltips",
@@ -2062,30 +2066,25 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function entryById(id) { function entryById(id) {
return state.entries.find((entry) => entry.id === id); return state.entries.find((entry) => entry.id === id);
} }
function avatarRegistry() { function characterRegistry() {
return state.meta.characterRegistry || {}; return state.meta.characterRegistry || {};
} }
function characterConfig(character) { function characterConfig(character) {
const registry = avatarRegistry(); const registry = characterRegistry();
return registry[character] || registry.z || {}; return registry[character] || registry.z || {};
} }
function characterLabel(character) { function characterLabel(character) {
return characterConfig(character).displayLabel || character || "z"; return characterConfig(character).displayLabel || character || "z";
} }
function avatarPath(character, expression = "default") { function characterGlow(character) {
const config = characterConfig(character);
const expressions = config.expressions || {};
return expressions[expression] || expressions.default || config.avatar || config.fallbackAvatar || defaultAvatarPath;
}
function avatarGlow(character) {
const config = characterConfig(character); const config = characterConfig(character);
return config.glow || config.territoryColor || "#d3a64d"; return config.glow || config.territoryColor || "#d3a64d";
} }
function avatarSize(character, size = "medium") { function characterSize(character, size = "medium") {
const sizes = characterConfig(character).sizes || {}; const sizes = characterConfig(character).sizes || {};
return Number(sizes[size] || sizes.medium || 42); return Number(sizes[size] || sizes.medium || 42);
} }
function avatarExpressionForEntry(entry) { function characterExpressionForEntry(entry) {
const tone = entry?.emotionalTone || ""; const tone = entry?.emotionalTone || "";
if (tone === "protective") return "protective"; if (tone === "protective") return "protective";
if (tone === "wise") return "wise"; if (tone === "wise") return "wise";
@@ -2095,10 +2094,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
if (String(entry?.type || "").includes("future")) return "temporal"; if (String(entry?.type || "").includes("future")) return "temporal";
return "default"; return "default";
} }
function avatarImg(character, size = "medium", expression = "default", className = "") { function characterImg(character, size = "medium", expression = "default", className = "") {
const px = avatarSize(character, size); const px = characterSize(character, size);
const label = characterLabel(character); const label = characterLabel(character);
return `<img class="avatar-thumb ${html(className)}" src="${html(avatarPath(character, expression))}" alt="${html(label)} portrait" loading="lazy" style="--avatar-size:${px}px;--avatar-glow:${html(avatarGlow(character))}" onerror="this.onerror=null;this.src='${html(characterConfig(character).fallbackAvatar || defaultAvatarPath)}'">`; return `<span class="character-mark ${html(className)}" title="${html(label)}" aria-label="${html(label)}" style="--character-size:${px}px;--character-glow:${html(characterGlow(character))}">${html(characterSymbol(character))}</span>`;
} }
function entryTextCorpus(entry) { function entryTextCorpus(entry) {
return [ return [
@@ -2127,7 +2126,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
return explicit + tone + keywords + motifs; return explicit + tone + keywords + motifs;
} }
function primaryCharacterForEntry(entry) { function primaryCharacterForEntry(entry) {
const candidates = Object.keys(avatarRegistry()); const candidates = Object.keys(characterRegistry());
const scored = candidates.map((character) => ({ character, score: characterPresenceScore(entry, character) })).sort((a, b) => b.score - a.score); const scored = candidates.map((character) => ({ character, score: characterPresenceScore(entry, character) })).sort((a, b) => b.score - a.score);
return scored[0]?.score > 0 ? scored[0].character : (entry.characters || [])[0] || "z"; return scored[0]?.score > 0 ? scored[0].character : (entry.characters || [])[0] || "z";
} }
@@ -2136,13 +2135,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const primary = primaryCharacterForEntry(entry); const primary = primaryCharacterForEntry(entry);
return [primary, ...listed.filter((character) => character !== primary)].filter(Boolean); return [primary, ...listed.filter((character) => character !== primary)].filter(Boolean);
} }
function svgAvatar(character, radius, clipId, expression = "default", extraClass = "") { function svgCharacter(character, radius, clipId, expression = "default", extraClass = "") {
const diameter = radius * 2;
const fallback = characterSymbol(character); const fallback = characterSymbol(character);
return `<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="${html(characterColor(character) || avatarGlow(character))}" opacity="0.22"></circle> return `<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="${html(characterColor(character) || characterGlow(character))}" opacity="0.22"></circle>
<image class="node-portrait ${html(extraClass)}" href="${html(avatarPath(character, expression))}" x="${-radius}" y="${-radius}" width="${diameter}" height="${diameter}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${html(clipId)})"></image>
<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="none"></circle> <circle class="node-shell ${html(extraClass)}" r="${radius}" fill="none"></circle>
<text class="node-fallback" x="0" y="4" text-anchor="middle" opacity="0.32">${html(fallback)}</text>`; <text class="node-fallback ${html(extraClass)}" x="0" y="4" text-anchor="middle" opacity="0.74">${html(fallback)}</text>`;
} }
function normalizeCharacterName(value) { function normalizeCharacterName(value) {
const raw = String(value || "").trim(); const raw = String(value || "").trim();
@@ -2511,11 +2508,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
} }
function renderCharacterPresence(entry) { function renderCharacterPresence(entry) {
const characters = charactersForEntry(entry); const characters = charactersForEntry(entry);
$("drawerAvatars").innerHTML = characters.map((character, index) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, index === 0 ? "medium" : "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join(""); $("drawerCharacters").innerHTML = characters.map((character, index) => `<span class="character-pill pill" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, index === 0 ? "medium" : "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("characterTags").innerHTML = characters.map((character) => `<span class="character-tag" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join(""); $("characterTags").innerHTML = characters.map((character) => `<span class="character-tag" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
const primary = primaryCharacterForEntry(entry); const primary = primaryCharacterForEntry(entry);
const territory = characterConfig(primary).observatory?.territory || "this memory territory"; const territory = characterConfig(primary).observatory?.territory || "this memory territory";
$("presencePreview").innerHTML = `<span class="avatar-row" style="--avatar-glow:${html(avatarGlow(primary))}"><span>${avatarImg(primary, "medium", avatarExpressionForEntry(entry))}</span><span><strong>${html(characterLabel(primary))}</strong><br><span class="subtle">presence preview: ${html(territory)}</span></span></span>`; $("presencePreview").innerHTML = `<span class="character-row" style="--character-glow:${html(characterGlow(primary))}"><span>${characterImg(primary, "medium", characterExpressionForEntry(entry))}</span><span><strong>${html(characterLabel(primary))}</strong><br><span class="subtle">presence preview: ${html(territory)}</span></span></span>`;
} }
function layerName(layer) { function layerName(layer) {
return (state.meta.layers || []).find((item) => item.id === String(layer))?.name || `Layer ${layer}`; return (state.meta.layers || []).find((item) => item.id === String(layer))?.name || `Layer ${layer}`;
@@ -2867,13 +2864,13 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const label = labelForCluster(cluster, pos, size, occupied); const label = labelForCluster(cluster, pos, size, occupied);
const activeDrop = state.drag?.targetKind === "cluster" && state.drag.targetId === cluster.id; const activeDrop = state.drag?.targetKind === "cluster" && state.drag.targetId === cluster.id;
const grow = activeDrop ? 1.08 : 1; const grow = activeDrop ? 1.08 : 1;
const portraitRadius = Math.max(28, size * 0.46) * grow; const symbolRadius = Math.max(28, size * 0.46) * grow;
const clipId = `clip-cluster-${slugify(cluster.id)}`; const clipId = `clip-cluster-${slugify(cluster.id)}`;
defs.push(`<clipPath id="${html(clipId)}"><circle r="${portraitRadius}"></circle></clipPath>`); defs.push(`<clipPath id="${html(clipId)}"><circle r="${symbolRadius}"></circle></clipPath>`);
return `<g class="cluster-node${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}"> return `<g class="cluster-node${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(characterGlow(character))}">
<circle class="cluster-halo" r="${(size + 13) * grow}"></circle> <circle class="cluster-halo" r="${(size + 13) * grow}"></circle>
<circle r="${size * grow}" fill="${color}" opacity="0.58"></circle> <circle r="${size * grow}" fill="${color}" opacity="0.58"></circle>
${state.zoom > 0 || state.mode === "character" ? svgAvatar(character, portraitRadius, clipId, "default", "node-breathe") : ""} ${state.zoom > 0 || state.mode === "character" ? svgCharacter(character, symbolRadius, clipId, "default", "node-breathe") : ""}
<text class="count" x="0" y="7" text-anchor="middle">${cluster.entries.length}</text> <text class="count" x="0" y="7" text-anchor="middle">${cluster.entries.length}</text>
</g>${label}`; </g>${label}`;
}).join(""); }).join("");
@@ -2884,7 +2881,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const color = characterColor(character) || toneColors[entry.emotionalTone] || "#d3a64d"; const color = characterColor(character) || toneColors[entry.emotionalTone] || "#d3a64d";
const isFragment = (entry.contentClass || "fragment") === "fragment" || (entry.observatoryRole || "") === "ambient"; const isFragment = (entry.contentClass || "fragment") === "fragment" || (entry.observatoryRole || "") === "ambient";
const baseSize = (isFragment ? 10 : 18) + Math.min(isFragment ? 10 : 18, Number(entry.resonanceScore || 3) * (isFragment ? 1.6 : 2.2)) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0); const baseSize = (isFragment ? 10 : 18) + Math.min(isFragment ? 10 : 18, Number(entry.resonanceScore || 3) * (isFragment ? 1.6 : 2.2)) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0);
const zoomSize = state.zoom === 0 ? avatarSize(character, "tiny") : state.zoom === 1 ? avatarSize(character, "medium") : state.zoom === 2 ? avatarSize(character, "close") : avatarSize(character, "focus"); const zoomSize = state.zoom === 0 ? characterSize(character, "tiny") : state.zoom === 1 ? characterSize(character, "medium") : state.zoom === 2 ? characterSize(character, "close") : characterSize(character, "focus");
const size = isFragment ? baseSize : Math.max(baseSize, zoomSize / 2); const size = isFragment ? baseSize : Math.max(baseSize, zoomSize / 2);
const label = labelForEntry(entry, pos, size, occupied); const label = labelForEntry(entry, pos, size, occupied);
const dragClass = dragClassForEntry(entry.id); const dragClass = dragClassForEntry(entry.id);
@@ -2899,14 +2896,14 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const sequenceBadge = sequenceNumber && (state.camera.k > 0.26 || activeStoryIds.has(entry.id) || related) const sequenceBadge = sequenceNumber && (state.camera.k > 0.26 || activeStoryIds.has(entry.id) || related)
? `<g transform="translate(${Math.max(12, size * 0.62)} ${-Math.max(12, size * 0.62)})"><circle class="sequence-badge" r="${badgeRadius}"></circle><text class="sequence-number" x="0" y="3.5" text-anchor="middle">${sequenceNumber}</text></g>` ? `<g transform="translate(${Math.max(12, size * 0.62)} ${-Math.max(12, size * 0.62)})"><circle class="sequence-badge" r="${badgeRadius}"></circle><text class="sequence-number" x="0" y="3.5" text-anchor="middle">${sequenceNumber}</text></g>`
: ""; : "";
return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}"> return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(characterGlow(character))}">
<circle class="hit-target" r="${Math.max(34, size + 16)}"></circle> <circle class="hit-target" r="${Math.max(34, size + 16)}"></circle>
${isSelected ? `<circle class="selection-halo" r="${Math.max(38, size + 18)}"></circle>` : ""} ${isSelected ? `<circle class="selection-halo" r="${Math.max(38, size + 18)}"></circle>` : ""}
${isFragment ${isFragment
? `<circle class="fragment-ring constellation-shimmer" r="${size + 9}" opacity="${entry.enabled === false ? 0.22 : 0.62}"></circle><circle class="fragment-star" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.34 : 0.84}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>` ? `<circle class="fragment-ring constellation-shimmer" r="${size + 9}" opacity="${entry.enabled === false ? 0.22 : 0.62}"></circle><circle class="fragment-star" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.34 : 0.84}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>`
: distant : distant
? `<circle class="node-marker constellation-shimmer" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.32 : 0.78}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>` ? `<circle class="node-marker constellation-shimmer" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.32 : 0.78}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>`
: svgAvatar(character, size, clipId, avatarExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")} : svgCharacter(character, size, clipId, characterExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")}
${sequenceBadge} ${sequenceBadge}
</g>${label}`; </g>${label}`;
}).join(""); }).join("");
@@ -3055,7 +3052,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function renderBreadcrumb(scope) { function renderBreadcrumb(scope) {
const filters = activeFilters(); const filters = activeFilters();
const parts = [`<button type="button" data-action="home">Observatory</button>`, `<button type="button" data-clear="mode">${html(state.mode)}</button>`]; const parts = [`<button type="button" data-action="home">Observatory</button>`, `<button type="button" data-clear="mode">${html(state.mode)}</button>`];
filters.forEach((filter) => parts.push(`<button type="button" title="${html(filter.help)}" data-clear="${html(filter.id)}">${filter.character ? avatarImg(filter.character, "tiny") : ""}${html(filter.label)}</button>`)); filters.forEach((filter) => parts.push(`<button type="button" title="${html(filter.help)}" data-clear="${html(filter.id)}">${filter.character ? characterImg(filter.character, "tiny") : ""}${html(filter.label)}</button>`));
if (scope.focusedCluster) parts.push(`<button type="button" data-clear="focus">${html(scope.focusedCluster.label)}</button>`); if (scope.focusedCluster) parts.push(`<button type="button" data-clear="focus">${html(scope.focusedCluster.label)}</button>`);
if (state.focusIds) parts.push(`<button type="button" data-clear="focus">${html(state.focusLabel || "Focused thread")}</button>`); if (state.focusIds) parts.push(`<button type="button" data-clear="focus">${html(state.focusLabel || "Focused thread")}</button>`);
$("breadcrumb").innerHTML = parts.join(""); $("breadcrumb").innerHTML = parts.join("");
@@ -3085,7 +3082,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("activeState").innerHTML = `<span class="subtle">Whole sky visible. You can always return here.</span>`; $("activeState").innerHTML = `<span class="subtle">Whole sky visible. You can always return here.</span>`;
return; return;
} }
$("activeState").innerHTML = active.map((item) => `<span class="filter-pill" title="${html(item.help)}">${item.character ? avatarImg(item.character, "tiny") : ""}${html(item.label)}${item.id === "selection" ? "" : `<button type="button" aria-label="Remove ${html(item.label)}" data-clear="${html(item.id)}">x</button>`}</span>`).join("") + `<button type="button" data-action="soft">Clear Focus</button><button type="button" data-action="full">Whole Sky</button>`; $("activeState").innerHTML = active.map((item) => `<span class="filter-pill" title="${html(item.help)}">${item.character ? characterImg(item.character, "tiny") : ""}${html(item.label)}${item.id === "selection" ? "" : `<button type="button" aria-label="Remove ${html(item.label)}" data-clear="${html(item.id)}">x</button>`}</span>`).join("") + `<button type="button" data-action="soft">Clear Focus</button><button type="button" data-action="full">Whole Sky</button>`;
$("activeState").querySelectorAll("[data-clear]").forEach((button) => button.addEventListener("click", () => clearExplorationPart(button.dataset.clear))); $("activeState").querySelectorAll("[data-clear]").forEach((button) => button.addEventListener("click", () => clearExplorationPart(button.dataset.clear)));
$("activeState").querySelector("[data-action='soft']")?.addEventListener("click", softReset); $("activeState").querySelector("[data-action='soft']")?.addEventListener("click", softReset);
$("activeState").querySelector("[data-action='full']")?.addEventListener("click", fullReset); $("activeState").querySelector("[data-action='full']")?.addEventListener("click", fullReset);
@@ -3554,8 +3551,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
} }
function renderPreview(entry) { function renderPreview(entry) {
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>"); const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
const avatarPills = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join(""); const characterPills = charactersForEntry(entry).map((character) => `<span class="character-pill pill" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("preview").innerHTML = `${body || "<span class='subtle'>This entry is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${avatarPills}<span class="pill">${html(entry.contentClass || "fragment")}</span><span class="pill">${html(entry.observatoryRole || "ambient")}</span><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.surfaces || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`; $("preview").innerHTML = `${body || "<span class='subtle'>This entry is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${characterPills}<span class="pill">${html(entry.contentClass || "fragment")}</span><span class="pill">${html(entry.observatoryRole || "ambient")}</span><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.surfaces || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
} }
function renderConnections(entry) { function renderConnections(entry) {
const stories = storiesForEntry(entry.id); const stories = storiesForEntry(entry.id);
@@ -3582,7 +3579,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("storyFlow").innerHTML = ""; $("storyFlow").innerHTML = "";
return; return;
} }
const characters = (story.characters || []).map((character) => `<span class="avatar-pill pill">${avatarImg(character, "tiny")}${html(characterLabel(character))}</span>`).join(""); const characters = (story.characters || []).map((character) => `<span class="character-pill pill">${characterImg(character, "tiny")}${html(characterLabel(character))}</span>`).join("");
$("storyCover").innerHTML = `<strong>${html(story.title)}</strong><p class="subtle">${html(story.description || "A quiet route waiting for a summary.")}</p><div class="pills">${characters}<span class="pill">${html(story.tone || "warm")}</span><span class="pill">${html(story.discoveryStyle || "gradual")}</span>${story.hidden ? "<span class='pill'>hidden</span>" : ""}${(story.markers || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div><div class="actions" style="margin-top:10px"><button type="button" data-read-story="${html(story.id)}">Read this story</button></div>`; $("storyCover").innerHTML = `<strong>${html(story.title)}</strong><p class="subtle">${html(story.description || "A quiet route waiting for a summary.")}</p><div class="pills">${characters}<span class="pill">${html(story.tone || "warm")}</span><span class="pill">${html(story.discoveryStyle || "gradual")}</span>${story.hidden ? "<span class='pill'>hidden</span>" : ""}${(story.markers || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div><div class="actions" style="margin-top:10px"><button type="button" data-read-story="${html(story.id)}">Read this story</button></div>`;
$("storyCover").querySelector("[data-read-story]")?.addEventListener("click", () => openStoryReader(story.id)); $("storyCover").querySelector("[data-read-story]")?.addEventListener("click", () => openStoryReader(story.id));
const items = storyItems(story); const items = storyItems(story);
@@ -3631,7 +3628,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const pool = state.entries.filter((entry) => !inStory.has(entry.id) && (!query || entryTextCorpus(entry).includes(query))).slice(0, 24); const pool = state.entries.filter((entry) => !inStory.has(entry.id) && (!query || entryTextCorpus(entry).includes(query))).slice(0, 24);
$("memoryPool").innerHTML = pool.map((entry) => { $("memoryPool").innerHTML = pool.map((entry) => {
const character = primaryCharacterForEntry(entry); const character = primaryCharacterForEntry(entry);
return `<button class="connection avatar-row" type="button" draggable="true" data-id="${html(entry.id)}" style="--avatar-glow:${html(avatarGlow(character))}"><span>${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.emotionalTone || "warm")} / ${html(entry.type || "memory")}</span></span></button>`; return `<button class="connection character-row" type="button" draggable="true" data-id="${html(entry.id)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "tiny", characterExpressionForEntry(entry))}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.emotionalTone || "warm")} / ${html(entry.type || "memory")}</span></span></button>`;
}).join("") || "<div class='subtle'>Every visible memory is already in this story.</div>"; }).join("") || "<div class='subtle'>Every visible memory is already in this story.</div>";
$("memoryPool").querySelectorAll("[data-id]").forEach((button) => { $("memoryPool").querySelectorAll("[data-id]").forEach((button) => {
button.addEventListener("click", () => selectNode(button.dataset.id)); button.addEventListener("click", () => selectNode(button.dataset.id));
@@ -3672,7 +3669,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("readerStage").innerHTML = `<article class="reader-scene reader-section"><div class="reader-kicker">Story scene</div><h2>${html(item.title || "Story section")}</h2><p class="reader-copy">${html(item.content || "This scene is waiting for words.")}</p></article>`; $("readerStage").innerHTML = `<article class="reader-scene reader-section"><div class="reader-kicker">Story scene</div><h2>${html(item.title || "Story section")}</h2><p class="reader-copy">${html(item.content || "This scene is waiting for words.")}</p></article>`;
} else { } else {
const entry = entryById(item.id); const entry = entryById(item.id);
const characters = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join(""); const characters = charactersForEntry(entry).map((character) => `<span class="character-pill pill">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
const kind = (entry.contentClass || "fragment") === "fragment" ? "Reusable memory light" : "Reusable memory"; const kind = (entry.contentClass || "fragment") === "fragment" ? "Reusable memory light" : "Reusable memory";
$("readerStage").innerHTML = `<article class="reader-scene reader-memory"><div class="reader-kicker">${html(kind)}</div><h2>${html(entry.title || "Untitled memory")}</h2><p class="reader-copy">${html(entry.content || "This memory is waiting for words.")}</p><div class="pills" style="justify-content:center;margin-top:18px">${characters}<span class="pill">${html(entry.emotionalTone || "warm")}</span><span class="pill">appears in ${storiesForEntry(entry.id).length} ${storiesForEntry(entry.id).length === 1 ? "story" : "stories"}</span></div></article>`; $("readerStage").innerHTML = `<article class="reader-scene reader-memory"><div class="reader-kicker">${html(kind)}</div><h2>${html(entry.title || "Untitled memory")}</h2><p class="reader-copy">${html(entry.content || "This memory is waiting for words.")}</p><div class="pills" style="justify-content:center;margin-top:18px">${characters}<span class="pill">${html(entry.emotionalTone || "warm")}</span><span class="pill">appears in ${storiesForEntry(entry.id).length} ${storiesForEntry(entry.id).length === 1 ? "story" : "stories"}</span></div></article>`;
} }
@@ -3916,10 +3913,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("toneFilter").innerHTML = opt("", "All tones") + state.meta.tones.map((value) => opt(value)).join(""); $("toneFilter").innerHTML = opt("", "All tones") + state.meta.tones.map((value) => opt(value)).join("");
$("rarityFilter").innerHTML = opt("", "All rarities") + state.meta.rarities.map((value) => opt(value)).join(""); $("rarityFilter").innerHTML = opt("", "All rarities") + state.meta.rarities.map((value) => opt(value)).join("");
$("layerKey").innerHTML = (state.meta.layers || []).map((layer) => `<div class="layer-row"><span class="depth-mark">${html(layer.id)}</span><span><strong>${html(layer.name)}</strong><span class="subtle">${html(layer.meaning)}</span></span></div>`).join(""); $("layerKey").innerHTML = (state.meta.layers || []).map((layer) => `<div class="layer-row"><span class="depth-mark">${html(layer.id)}</span><span><strong>${html(layer.name)}</strong><span class="subtle">${html(layer.meaning)}</span></span></div>`).join("");
$("characterAtlas").innerHTML = Object.keys(avatarRegistry()).map((character) => { $("characterAtlas").innerHTML = Object.keys(characterRegistry()).map((character) => {
const config = characterConfig(character); const config = characterConfig(character);
const territory = config.observatory?.territory || "memory territory"; const territory = config.observatory?.territory || "memory territory";
return `<button class="connection avatar-row" type="button" data-character="${html(character)}" style="--avatar-glow:${html(avatarGlow(character))}"><span>${avatarImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`; return `<button class="connection character-row" type="button" data-character="${html(character)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`;
}).join(""); }).join("");
$("characterAtlas").querySelectorAll("[data-character]").forEach((button) => button.addEventListener("click", () => { $("characterAtlas").querySelectorAll("[data-character]").forEach((button) => button.addEventListener("click", () => {
snapshotExploration(`character territory: ${button.dataset.character}`); snapshotExploration(`character territory: ${button.dataset.character}`);

View File

@@ -3,14 +3,12 @@
from __future__ import annotations from __future__ import annotations
import json import json
import mimetypes
import os import os
import sys import sys
import time import time
from email.utils import formatdate from email.utils import formatdate
from http import HTTPStatus from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
@@ -89,27 +87,6 @@ class Handler(BaseHTTPRequestHandler):
except Exception as exc: except Exception as exc:
self.send_json({"error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR) self.send_json({"error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
return return
if parsed.path.startswith("/assets/avatars/"):
try:
relative = Path(parsed.path.lstrip("/"))
if relative.is_absolute() or ".." in relative.parts:
self.send_error(HTTPStatus.NOT_FOUND)
return
asset_path = (ROOT / relative).resolve()
avatars_root = (ROOT / "assets" / "avatars").resolve()
if not asset_path.is_file() or avatars_root not in asset_path.parents:
self.send_error(HTTPStatus.NOT_FOUND)
return
body = asset_path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", mimetypes.guess_type(asset_path.name)[0] or "application/octet-stream")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
except Exception:
self.send_error(HTTPStatus.NOT_FOUND)
return
self.send_error(HTTPStatus.NOT_FOUND) self.send_error(HTTPStatus.NOT_FOUND)
def do_POST(self) -> None: def do_POST(self) -> None: