diff --git a/assets/scripts/hidden-details.js b/assets/scripts/hidden-details.js index c89ffa1..6942ce1 100644 --- a/assets/scripts/hidden-details.js +++ b/assets/scripts/hidden-details.js @@ -34,6 +34,75 @@ // Generated by the hidden narrative authoring page. // Friendly source of truth: assets/content/hidden-details.json + const CHARACTER_REGISTRY = { + "young z": { + "id": "young z", + "displayLabel": "young z", + "aliases": ["Young Z", "young z", "young-z", "young_z", "young"], + "territoryColor": "#d8a95a", + "symbol": "Y", + "avatar": {"shape": "sun-child", "accent": "#f1bc5b", "shadow": "#7a5630", "mood": "excited", "motif": "crayon sun", "description": "small crayon-sun spirit with oversized curiosity"}, + "motifs": ["crayon sun", "blanket cape", "childhood desk"], + "themes": ["childhood", "play", "memory", "safety"], + "affinities": ["z", "future z", "aphy", "lima"] + }, + "z": { + "id": "z", + "displayLabel": "z", + "aliases": ["Z", "z", "zaine"], + "territoryColor": "#d6c38a", + "symbol": "Z", + "avatar": {"shape": "traveler", "accent": "#d6c38a", "shadow": "#5d533a", "mood": "receptive", "motif": "small lantern", "description": "present observer spirit, quiet and receptive"}, + "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", + "symbol": "A", + "avatar": {"shape": "terminal", "accent": "#7fd0a0", "shadow": "#315944", "mood": "curious", "motif": "terminal cursor", "description": "curious tiny system companion with a soft terminal glow"}, + "motifs": ["console", "diagnostic", "backup"], + "themes": ["humor", "systems", "care through tools"], + "affinities": ["z", "lima", "sensei chi"] + }, + "lima": { + "id": "lima", + "displayLabel": "lima", + "aliases": ["Lima", "lima"], + "territoryColor": "#d06b78", + "symbol": "L", + "avatar": {"shape": "hearth", "accent": "#e78f8d", "shadow": "#6d3f43", "mood": "warm", "motif": "kitchen light", "description": "warm grounding spirit with calm eyes and a soft light"}, + "motifs": ["warmth", "kitchen light", "ring"], + "themes": ["love", "home", "grounding"], + "affinities": ["z", "aphy", "future z", "young z"] + }, + "sensei chi": { + "id": "sensei chi", + "displayLabel": "sensei chi", + "aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"], + "territoryColor": "#75a9bd", + "symbol": "S", + "avatar": {"shape": "sage", "accent": "#84b8c8", "shadow": "#36566a", "mood": "smiling", "motif": "tea lantern", "description": "patient elderly spirit in simple robes"}, + "motifs": ["tea", "garden", "quiet lesson"], + "themes": ["reflection", "patience", "wisdom"], + "affinities": ["aphy", "future z"] + }, + "future z": { + "id": "future z", + "displayLabel": "future z", + "aliases": ["Future Z", "future z", "future-z", "future_z", "future"], + "territoryColor": "#a58ac9", + "symbol": "F", + "avatar": {"shape": "moon", "accent": "#b8a2d7", "shadow": "#51496a", "mood": "reflective", "motif": "distant clock", "description": "older reflective spirit, distant but caring"}, + "motifs": ["clock", "age 40", "future log"], + "themes": ["time", "reassurance", "continuity"], + "affinities": ["z", "young z", "lima", "sensei chi"] + } +}; + 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.", @@ -498,7 +567,70 @@ // UI HELPERS // ----------------------------- - function toast(message, duration) { + function characterFromText(value) { + const text = String(value || "").toLowerCase(); + let best = "z"; + let bestScore = 0; + Object.entries(CHARACTER_REGISTRY).forEach(([id, config]) => { + let score = text.includes(id) ? 6 : 0; + (config.aliases || []).forEach((alias) => { + if (text.includes(String(alias).toLowerCase())) score += 3; + }); + (config.themes || []).forEach((theme) => { + if (text.includes(String(theme).toLowerCase())) score += 2; + }); + (config.motifs || []).forEach((motif) => { + if (text.includes(String(motif).toLowerCase())) score += 3; + }); + if (id === "lima" && /warm|home|safe|kitchen|ring|love|rest/.test(text)) score += 3; + if (id === "aphy" && /system|terminal|logic|error|debug|console|css|search|backup/.test(text)) score += 3; + if (id === "sensei chi" && /quiet|patience|tea|garden|lesson|wisdom|still/.test(text)) score += 3; + if (id === "young z" && /play|child|crayon|sun|blanket|game|desk|drawing/.test(text)) score += 3; + if (id === "future z" && /future|time|age 40|later|return|warning|ordinary/.test(text)) score += 3; + if (score > bestScore) { + best = id; + bestScore = score; + } + }); + return best; + } + + function avatarExpression(character, text) { + const value = String(text || "").toLowerCase(); + if (character === "aphy" && /error|glitch|system|diagnostic|debug|console/.test(value)) return "glitching"; + if (character === "lima" && /safe|home|warm|protect|care|eat|rest|light/.test(value)) return "concerned"; + if (character === "young z" && /draw|crayon|game|play|sun|blanket/.test(value)) return "drawing"; + if (character === "future z" && /time|future|return|ordinary|warning|later/.test(value)) return "reflective"; + if (character === "sensei chi" && /quiet|tea|patience|lesson|garden/.test(value)) return "smiling"; + return CHARACTER_REGISTRY[character]?.avatar?.mood || "soft"; + } + + function avatarSvg(character, expression) { + const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z; + const avatar = config.avatar || {}; + const accent = avatar.accent || config.territoryColor || "#d3a64d"; + const shadow = avatar.shadow || "#5d4632"; + const symbol = escapeHtml(config.symbol || "?"); + const shape = avatar.shape || "traveler"; + const eye = expression === "reflective" || expression === "smiling" ? "M12 15h3M21 15h3" : expression === "glitching" ? "M11 14h5M21 13h3M22 17h4" : "M12 14h3M21 14h3"; + const mouth = expression === "concerned" ? "M15 23q3-2 6 0" : expression === "excited" || expression === "drawing" || expression === "smiling" ? "M15 22q3 3 7 0" : "M16 22h6"; + const motif = { + "sun-child": ``, + terminal: ``, + sage: ``, + hearth: ``, + moon: ``, + traveler: `` + }[shape] || ""; + return ``; + } + + function avatarHtml(character, text) { + const config = CHARACTER_REGISTRY[character] || CHARACTER_REGISTRY.z; + return `${avatarSvg(character, avatarExpression(character, text))}`; + } + + function toast(message, duration, character) { let el = document.querySelector(".hidden-toast"); if (!el) { el = document.createElement("div"); @@ -506,7 +638,8 @@ el.setAttribute("role", "status"); document.body.appendChild(el); } - el.textContent = message; + const spirit = character || characterFromText(message); + el.innerHTML = `${avatarHtml(spirit, message)}${escapeHtml(message)}`; el.classList.add("is-visible"); window.clearTimeout(el._timer); el._timer = window.setTimeout(() => el.classList.remove("is-visible"), duration || 5600); @@ -590,9 +723,10 @@ mark.tabIndex = 0; mark.textContent = " ?"; mark.title = pick(index % 2 ? poems : details, `whisper-${index}`); + mark.dataset.character = characterFromText(mark.title); mark.addEventListener("click", () => { awardLayer(index % 2 ? 2 : 1, "paragraph whisper"); - toast(mark.title); + toast(mark.title, undefined, mark.dataset.character); }); p.appendChild(mark); }); @@ -688,8 +822,9 @@ function showDriftNote(text, art) { const panel = document.createElement("aside"); + const character = characterFromText(text); panel.className = "hidden-drift-note"; - panel.innerHTML = `
${escapeHtml(text)}
${escapeHtml(art)}
`; + panel.innerHTML = `
${avatarHtml(character, text)}
${escapeHtml(text)}
${escapeHtml(art)}
`; panel.querySelector("button").addEventListener("click", () => panel.remove()); document.body.appendChild(panel); } diff --git a/assets/styles/hidden-details.css b/assets/styles/hidden-details.css index 668ecd9..8571ae2 100644 --- a/assets/styles/hidden-details.css +++ b/assets/styles/hidden-details.css @@ -31,6 +31,9 @@ right: 1rem; bottom: 1rem; max-width: min(23rem, calc(100vw - 2rem)); + display: flex; + align-items: center; + gap: 0.65rem; padding: 0.8rem 0.95rem; border: 1px solid var(--border); border-radius: 8px; @@ -45,6 +48,27 @@ transition: opacity 220ms ease, transform 220ms ease; } +.hidden-avatar { + width: 2rem; + height: 2rem; + display: inline-grid; + place-items: center; + flex: 0 0 auto; + border-radius: 8px; + background: color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 16%, var(--surface)); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 42%, transparent), + 0 0 18px color-mix(in oklab, var(--hidden-avatar-color, var(--accent)) 22%, transparent); + overflow: hidden; + animation: hidden-avatar-breathe 6s ease-in-out infinite; +} + +.hidden-avatar svg { + width: 100%; + height: 100%; + display: block; + shape-rendering: crispEdges; +} + .hidden-toast.is-visible { opacity: 1; transform: translateY(0); @@ -97,6 +121,13 @@ z-index: 40; } +.hidden-drift-note__head { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.7rem; + align-items: center; +} + .hidden-drift-note pre { margin: 0.75rem 0 0; white-space: pre; @@ -213,6 +244,18 @@ box-shadow: 0 12px 28px color-mix(in oklab, #000 12%, transparent); } +@keyframes hidden-avatar-breathe { + 0%, 100% { + transform: translateY(0); + filter: saturate(0.96); + } + + 50% { + transform: translateY(-1px); + filter: saturate(1.14); + } +} + body.hidden-home-takeover .db-root { filter: saturate(0.9) contrast(1.03); } diff --git a/authoring_server.py b/authoring_server.py index c6b5c4f..55942eb 100755 --- a/authoring_server.py +++ b/authoring_server.py @@ -149,6 +149,14 @@ CHARACTER_REGISTRY = { "aliases": ["Young Z", "young z", "young-z", "young_z", "young"], "territoryColor": "#d8a95a", "symbol": "Y", + "avatar": { + "shape": "sun-child", + "accent": "#f1bc5b", + "shadow": "#7a5630", + "mood": "excited", + "motif": "crayon sun", + "description": "small crayon-sun spirit with oversized curiosity", + }, "motifs": ["crayon sun", "blanket cape", "childhood desk"], "themes": ["childhood", "play", "memory", "safety"], "affinities": ["z", "future z", "aphy", "lima"], @@ -159,6 +167,14 @@ CHARACTER_REGISTRY = { "aliases": ["Z", "z", "zaine"], "territoryColor": "#d6c38a", "symbol": "Z", + "avatar": { + "shape": "traveler", + "accent": "#d6c38a", + "shadow": "#5d533a", + "mood": "receptive", + "motif": "small lantern", + "description": "present observer spirit, quiet and receptive", + }, "motifs": ["archive", "website", "home"], "themes": ["selfhood", "return", "making"], "affinities": ["lima", "young z", "future z"], @@ -169,6 +185,14 @@ CHARACTER_REGISTRY = { "aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"], "territoryColor": "#7fb089", "symbol": "A", + "avatar": { + "shape": "terminal", + "accent": "#7fd0a0", + "shadow": "#315944", + "mood": "curious", + "motif": "terminal cursor", + "description": "curious tiny system companion with a soft terminal glow", + }, "motifs": ["console", "diagnostic", "backup"], "themes": ["humor", "systems", "care through tools"], "affinities": ["z", "lima", "sensei chi"], @@ -179,6 +203,14 @@ CHARACTER_REGISTRY = { "aliases": ["Lima", "lima"], "territoryColor": "#d06b78", "symbol": "L", + "avatar": { + "shape": "hearth", + "accent": "#e78f8d", + "shadow": "#6d3f43", + "mood": "warm", + "motif": "kitchen light", + "description": "warm grounding spirit with calm eyes and a soft light", + }, "motifs": ["warmth", "kitchen light", "ring"], "themes": ["love", "home", "grounding"], "affinities": ["z", "aphy", "future z", "young z"], @@ -189,6 +221,14 @@ CHARACTER_REGISTRY = { "aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"], "territoryColor": "#75a9bd", "symbol": "S", + "avatar": { + "shape": "sage", + "accent": "#84b8c8", + "shadow": "#36566a", + "mood": "smiling", + "motif": "tea lantern", + "description": "patient elderly spirit in simple robes", + }, "motifs": ["tea", "garden", "quiet lesson"], "themes": ["reflection", "patience", "wisdom"], "affinities": ["aphy", "future z"], @@ -199,6 +239,14 @@ CHARACTER_REGISTRY = { "aliases": ["Future Z", "future z", "future-z", "future_z", "future"], "territoryColor": "#a58ac9", "symbol": "F", + "avatar": { + "shape": "moon", + "accent": "#b8a2d7", + "shadow": "#51496a", + "mood": "reflective", + "motif": "distant clock", + "description": "older reflective spirit, distant but caring", + }, "motifs": ["clock", "age 40", "future log"], "themes": ["time", "reassurance", "continuity"], "affinities": ["z", "young z", "lima", "sensei chi"], @@ -1228,6 +1276,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str: " // -----------------------------\n" " // Generated by the hidden narrative authoring page.\n" " // Friendly source of truth: assets/content/hidden-details.json\n\n" + f"{js_const('CHARACTER_REGISTRY', CHARACTER_REGISTRY)}\n" f"{js_const('familyLayers', family_layers)}\n" f"{js_const('details', details)}\n" f"{js_const('poems', poems)}\n" @@ -2525,6 +2574,54 @@ HIDDEN_APP_HTML = r""" .filters { display: grid; gap: 9px; margin: 14px 0; } .modebar, .actions, .pills { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; } .modebar button.active { background: rgba(211, 166, 77, 0.26); border-color: var(--gold); } + .avatar { + width: var(--avatar-size, 24px); + height: var(--avatar-size, 24px); + display: inline-grid; + place-items: center; + flex: 0 0 auto; + border-radius: 7px; + background: color-mix(in oklab, var(--avatar-color, #d3a64d) 20%, rgba(18, 16, 13, 0.72)); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, transparent), 0 0 14px color-mix(in oklab, var(--avatar-color, #d3a64d) 22%, transparent); + overflow: hidden; + vertical-align: middle; + } + .avatar svg { width: 100%; height: 100%; display: block; shape-rendering: crispEdges; } + .avatar--xs { --avatar-size: 18px; border-radius: 5px; } + .avatar--sm { --avatar-size: 24px; } + .avatar--md { --avatar-size: 34px; } + .avatar--lg { --avatar-size: 52px; border-radius: 10px; } + .avatar--breathing { animation: spirit-breathe 5.8s ease-in-out infinite; } + .character-chip, .avatar-chip { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, var(--line)); + border-radius: 999px; + padding: 3px 8px 3px 4px; + color: #f3ddb2; + background: color-mix(in oklab, var(--avatar-color, #d3a64d) 12%, rgba(251, 241, 220, 0.07)); + font-size: 12px; + } + .drawer-presence, .character-preview, .timeline-presence { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 7px; + margin-top: 9px; + } + .drawer-heading { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px; + align-items: center; + } + .drawer-heading h2 { margin: 0; } + .guided-spirit { + display: inline-flex; + align-items: center; + gap: 8px; + } .pill { border: 1px solid var(--line); border-radius: 999px; @@ -2577,6 +2674,9 @@ HIDDEN_APP_HTML = r""" .memory-node:active { cursor: grabbing; } .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: none; stroke: var(--node-character-color, rgba(211, 166, 77, 0.46)); stroke-width: 2; opacity: 0.42; stroke-dasharray: 2 5; 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.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)); } @@ -2600,6 +2700,8 @@ HIDDEN_APP_HTML = r""" } .cluster-node .count { font-size: 18px; font-weight: 850; } .cluster-halo { fill: rgba(211, 166, 77, 0.08); stroke: rgba(211, 166, 77, 0.22); stroke-dasharray: 3 8; } + .territory-anchor .cluster-halo { animation: spirit-shimmer 7.5s ease-in-out infinite; } + .cluster-avatar { filter: drop-shadow(0 0 10px rgba(255, 232, 177, 0.24)); } .drag-preview-line { stroke: rgba(255, 232, 177, 0.78); stroke-width: 3; stroke-dasharray: 8 8; pointer-events: none; } .drag-preview-halo { fill: rgba(211, 166, 77, 0.1); stroke: rgba(255, 232, 177, 0.58); stroke-width: 2; stroke-dasharray: 4 7; pointer-events: none; } .drag-preview-label rect { fill: rgba(18, 16, 13, 0.86); stroke: rgba(255, 232, 177, 0.42); rx: 7; } @@ -2629,7 +2731,7 @@ HIDDEN_APP_HTML = r""" gap: 6px; max-width: min(720px, calc(100% - 40px)); } - .breadcrumb button { min-height: 26px; padding: 0 8px; } + .breadcrumb button { min-height: 26px; padding: 0 8px; display: inline-flex; align-items: center; gap: 6px; } .active-state { position: absolute; z-index: 2; @@ -2744,6 +2846,14 @@ HIDDEN_APP_HTML = r""" 0%, 100% { opacity: 0.72; } 50% { opacity: 1; } } + @keyframes spirit-breathe { + 0%, 100% { transform: translateY(0); filter: saturate(0.94); } + 50% { transform: translateY(-1px); filter: saturate(1.14); } + } + @keyframes spirit-shimmer { + 0%, 100% { opacity: 0.28; stroke-dashoffset: 0; } + 50% { opacity: 0.68; stroke-dashoffset: 11; } + } .core-glow { animation: breathe 7s ease-in-out infinite; } @media (max-width: 1120px) { body { overflow: auto; } @@ -2800,11 +2910,11 @@ HIDDEN_APP_HTML = r"""

Guided Exploration

- - - - - + + + + +
@@ -2843,8 +2953,14 @@ HIDDEN_APP_HTML = r"""