Compare commits
7 Commits
mimo/code/
...
feat/spati
| Author | SHA1 | Date | |
|---|---|---|---|
| 72eecf6ee4 | |||
| 0bd3e1f470 | |||
| b73d846334 | |||
| 4c267de5bc | |||
| 684f648027 | |||
| e842e35833 | |||
| 065e83c94e |
219
app.js
219
app.js
@@ -1,3 +1,5 @@
|
||||
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
|
||||
chdir: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
|
||||
import * as THREE from 'three';
|
||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||
@@ -1982,30 +1984,97 @@ function setupControls() {
|
||||
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.quick-action-btn');
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
|
||||
switch(action) {
|
||||
case 'status':
|
||||
sendChatMessage("Timmy, what is the current system status?");
|
||||
break;
|
||||
case 'agents':
|
||||
sendChatMessage("Timmy, check on all active agents.");
|
||||
break;
|
||||
case 'portals':
|
||||
openPortalAtlas();
|
||||
break;
|
||||
case 'help':
|
||||
sendChatMessage("Timmy, I need assistance with Nexus navigation.");
|
||||
break;
|
||||
}
|
||||
handleQuickAction(btn.dataset.action);
|
||||
});
|
||||
|
||||
// ═══ QUICK ACTION HANDLER ═══
|
||||
function handleQuickAction(action) {
|
||||
switch(action) {
|
||||
case 'status': {
|
||||
const portalCount = portals.length;
|
||||
const onlinePortals = portals.filter(p => p.userData && p.userData.status === 'online').length;
|
||||
const agentCount = agents.length;
|
||||
const wsState = wsConnected ? 'ONLINE' : 'OFFLINE';
|
||||
const wsColor = wsConnected ? '#4af0c0' : '#ff4466';
|
||||
addChatMessage('system', `[SYSTEM STATUS]`);
|
||||
addChatMessage('timmy', `Nexus operational. ${portalCount} portals registered (${onlinePortals} online). ${agentCount} agent presences active. Hermes WebSocket: ${wsState}. Navigation mode: ${NAV_MODES[navModeIdx].toUpperCase()}. Performance tier: ${performanceTier.toUpperCase()}.`);
|
||||
break;
|
||||
}
|
||||
case 'agents': {
|
||||
addChatMessage('system', `[AGENT ROSTER]`);
|
||||
if (agents.length === 0) {
|
||||
addChatMessage('timmy', 'No active agent presences detected in the Nexus. The thought stream and harness pulse are the primary indicators of system activity.');
|
||||
} else {
|
||||
const roster = agents.map(a => `- ${(a.userData && a.userData.name) || a.name || 'Unknown'}: ${(a.userData && a.userData.status) || 'active'}`).join('\n');
|
||||
addChatMessage('timmy', `Active agents:\n${roster}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'portals':
|
||||
openPortalAtlas();
|
||||
break;
|
||||
case 'heartbeat': {
|
||||
const agentLog = document.getElementById('agent-log-content');
|
||||
const recentEntries = agentLog ? agentLog.querySelectorAll('.agent-log-entry') : [];
|
||||
const entryCount = recentEntries.length;
|
||||
addChatMessage('system', `[HEARTBEAT INSPECTION]`);
|
||||
addChatMessage('timmy', `Hermes heartbeat ${wsConnected ? 'active' : 'inactive'}. ${entryCount} recent entries in thought stream. WebSocket reconnect timer: ${wsReconnectTimer ? 'active' : 'idle'}. Harness pulse mesh: ${harnessPulseMesh ? 'rendering' : 'standby'}.`);
|
||||
break;
|
||||
}
|
||||
case 'thoughts': {
|
||||
const agentLog = document.getElementById('agent-log-content');
|
||||
const entries = agentLog ? Array.from(agentLog.querySelectorAll('.agent-log-entry')).slice(0, 5) : [];
|
||||
addChatMessage('system', `[THOUGHT STREAM]`);
|
||||
if (entries.length === 0) {
|
||||
addChatMessage('timmy', 'The thought stream is quiet. No recent agent entries detected.');
|
||||
} else {
|
||||
const summary = entries.map(e => '> ' + e.textContent.trim()).join('\n');
|
||||
addChatMessage('timmy', `Recent thoughts:\n${summary}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'help': {
|
||||
addChatMessage('system', `[NEXUS HELP]`);
|
||||
addChatMessage('timmy', `Navigation: WASD to move, mouse to look around.\n` +
|
||||
`Press V to cycle: Walk / Orbit / Fly mode.\n` +
|
||||
`Enter to chat. Escape to close overlays.\n` +
|
||||
`Press F near a portal to enter. Press E near a vision point to read.\n` +
|
||||
`Press Tab for Portal Atlas.\n` +
|
||||
`The Batcave Terminal shows system logs. The Workshop Terminal shows tool output.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
|
||||
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
|
||||
|
||||
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
|
||||
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
|
||||
|
||||
// Mnemosyne export/import (#1174)
|
||||
document.getElementById('mnemosyne-export-btn').addEventListener('click', () => {
|
||||
const result = SpatialMemory.exportToFile();
|
||||
if (result) {
|
||||
addChatMessage('system', 'Mnemosyne: Exported ' + result.count + ' memories to ' + result.filename);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('mnemosyne-import-btn').addEventListener('click', () => {
|
||||
document.getElementById('mnemosyne-import-file').click();
|
||||
});
|
||||
|
||||
document.getElementById('mnemosyne-import-file').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const result = await SpatialMemory.importFromFile(file);
|
||||
addChatMessage('system', 'Mnemosyne: Imported ' + result.count + ' of ' + result.total + ' memories');
|
||||
} catch (err) {
|
||||
addChatMessage('system', 'Mnemosyne: Import failed — ' + err.message);
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
function sendChatMessage(overrideText = null) {
|
||||
@@ -3375,6 +3444,122 @@ init().then(() => {
|
||||
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
||||
SpatialMemory.runGravityLayout();
|
||||
|
||||
|
||||
// ═══ SPATIAL SEARCH (Mnemosyne #1170) ═══
|
||||
(() => {
|
||||
const input = document.getElementById('spatial-search-input');
|
||||
const resultsDiv = document.getElementById('spatial-search-results');
|
||||
if (!input || !resultsDiv) return;
|
||||
|
||||
let searchTimeout = null;
|
||||
let currentMatches = [];
|
||||
|
||||
function runSearch(query) {
|
||||
if (!query.trim()) {
|
||||
SpatialMemory.clearSearch();
|
||||
resultsDiv.classList.remove('visible');
|
||||
resultsDiv.innerHTML = '';
|
||||
currentMatches = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = SpatialMemory.searchContent(query);
|
||||
currentMatches = matches;
|
||||
|
||||
if (matches.length === 0) {
|
||||
SpatialMemory.clearSearch();
|
||||
resultsDiv.innerHTML = '<div class="spatial-search-count">No matches</div>';
|
||||
resultsDiv.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
SpatialMemory.highlightSearchResults(matches);
|
||||
|
||||
// Build results list
|
||||
const allMems = SpatialMemory.getAllMemories();
|
||||
let html = `<div class="spatial-search-count">${matches.length} match${matches.length > 1 ? 'es' : ''}</div>`;
|
||||
matches.forEach(id => {
|
||||
const mem = allMems.find(m => m.id === id);
|
||||
if (mem) {
|
||||
const label = (mem.content || id).slice(0, 60);
|
||||
const region = mem.category || '?';
|
||||
html += `<div class="spatial-search-result-item" data-mem-id="${id}">
|
||||
<span class="result-region">[${region}]</span>${label}
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
resultsDiv.innerHTML = html;
|
||||
resultsDiv.classList.add('visible');
|
||||
|
||||
// Click handler for result items
|
||||
resultsDiv.querySelectorAll('.spatial-search-result-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const memId = el.getAttribute('data-mem-id');
|
||||
flyToMemory(memId);
|
||||
});
|
||||
});
|
||||
|
||||
// Fly camera to first match
|
||||
if (matches.length > 0) {
|
||||
flyToMemory(matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function flyToMemory(memId) {
|
||||
const pos = SpatialMemory.getSearchMatchPosition(memId);
|
||||
if (!pos) return;
|
||||
|
||||
// Smooth camera fly-to: place camera above and in front of crystal
|
||||
const targetPos = new THREE.Vector3(pos.x, pos.y + 4, pos.z + 6);
|
||||
|
||||
// Use simple lerp animation over ~800ms
|
||||
const startPos = playerPos.clone();
|
||||
const startTime = performance.now();
|
||||
const duration = 800;
|
||||
|
||||
function animateCamera(now) {
|
||||
const elapsed = now - startTime;
|
||||
const t = Math.min(1, elapsed / duration);
|
||||
// Ease out cubic
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
|
||||
playerPos.lerpVectors(startPos, targetPos, ease);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
// Look at crystal
|
||||
const lookTarget = pos.clone();
|
||||
lookTarget.y += 1.5;
|
||||
camera.lookAt(lookTarget);
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(animateCamera);
|
||||
} else {
|
||||
SpatialMemory.highlightMemory(memId);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animateCamera);
|
||||
}
|
||||
|
||||
// Debounced input handler
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => runSearch(input.value), 200);
|
||||
});
|
||||
|
||||
// Escape clears search
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
input.value = '';
|
||||
SpatialMemory.clearSearch();
|
||||
resultsDiv.classList.remove('visible');
|
||||
resultsDiv.innerHTML = '';
|
||||
currentMatches = [];
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
// Project Mnemosyne — seed demo session rooms (#1171)
|
||||
// Sessions group facts by conversation/work session with a timestamp.
|
||||
const demoSessions = [
|
||||
|
||||
78
index.html
78
index.html
@@ -1,3 +1,5 @@
|
||||
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
|
||||
chdir: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
@@ -64,6 +66,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Spatial Search Overlay (Mnemosyne #1170) -->
|
||||
<div id="spatial-search" class="spatial-search-overlay">
|
||||
<input type="text" id="spatial-search-input" class="spatial-search-input"
|
||||
placeholder="🔍 Search memories..." autocomplete="off" spellcheck="false">
|
||||
<div id="spatial-search-results" class="spatial-search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- HUD Overlay -->
|
||||
<div id="hud" class="game-ui" style="display:none;">
|
||||
<!-- GOFAI HUD Panels -->
|
||||
@@ -113,15 +123,15 @@
|
||||
|
||||
<!-- Top Right: Agent Log & Atlas Toggle -->
|
||||
<div class="hud-top-right">
|
||||
<button id="atlas-toggle-btn" class="hud-icon-btn" title="Portal Atlas">
|
||||
<span class="hud-icon">🌐</span>
|
||||
<button id="atlas-toggle-btn" class="hud-icon-btn" aria-label="Open Portal Atlas — browse all available portals" title="Open Portal Atlas" data-tooltip="Portal Atlas (M)">
|
||||
<span class="hud-icon" aria-hidden="true">🌐</span>
|
||||
<span class="hud-btn-label">ATLAS</span>
|
||||
</button>
|
||||
<div id="bannerlord-status" class="hud-status-item" title="Bannerlord Readiness">
|
||||
<span class="status-dot"></span>
|
||||
<div id="bannerlord-status" class="hud-status-item" role="status" aria-label="Bannerlord system readiness indicator" title="Bannerlord Readiness" data-tooltip="Bannerlord Status">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span class="status-label">BANNERLORD</span>
|
||||
</div>
|
||||
<div class="hud-agent-log" id="hud-agent-log" aria-label="Agent Thought Stream">
|
||||
<div class="hud-agent-log" id="hud-agent-log" role="log" aria-label="Agent Thought Stream — live activity feed" aria-live="polite">
|
||||
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
|
||||
<div id="agent-log-content" class="agent-log-content"></div>
|
||||
</div>
|
||||
@@ -143,10 +153,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-quick-actions" class="chat-quick-actions">
|
||||
<button class="quick-action-btn" data-action="status">System Status</button>
|
||||
<button class="quick-action-btn" data-action="agents">Agent Check</button>
|
||||
<button class="quick-action-btn" data-action="portals">Portal Atlas</button>
|
||||
<button class="quick-action-btn" data-action="help">Help</button>
|
||||
<div class="starter-label">STARTER PROMPTS</div>
|
||||
<div class="starter-grid">
|
||||
<button class="starter-btn" data-action="heartbeat" title="Check Timmy heartbeat and system health">
|
||||
<span class="starter-icon">◈</span>
|
||||
<span class="starter-text">Inspect Heartbeat</span>
|
||||
<span class="starter-desc">System health & connectivity</span>
|
||||
</button>
|
||||
<button class="starter-btn" data-action="portals" title="Browse the portal atlas">
|
||||
<span class="starter-icon">🌐</span>
|
||||
<span class="starter-text">Portal Atlas</span>
|
||||
<span class="starter-desc">Browse connected worlds</span>
|
||||
</button>
|
||||
<button class="starter-btn" data-action="agents" title="Check active agent status">
|
||||
<span class="starter-icon">◎</span>
|
||||
<span class="starter-text">Agent Status</span>
|
||||
<span class="starter-desc">Who is in the fleet</span>
|
||||
</button>
|
||||
<button class="starter-btn" data-action="memory" title="View memory crystals">
|
||||
<span class="starter-icon">◇</span>
|
||||
<span class="starter-text">Memory Crystals</span>
|
||||
<span class="starter-desc">Inspect stored knowledge</span>
|
||||
</button>
|
||||
<button class="starter-btn" data-action="ask" title="Ask Timmy anything">
|
||||
<span class="starter-icon">→</span>
|
||||
<span class="starter-text">Ask Timmy</span>
|
||||
<span class="starter-desc">Start a conversation</span>
|
||||
</button>
|
||||
<button class="starter-btn" data-action="sovereignty" title="Learn about sovereignty">
|
||||
<span class="starter-icon">△</span>
|
||||
<span class="starter-text">Sovereignty</span>
|
||||
<span class="starter-desc">What this space is</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-row">
|
||||
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
|
||||
@@ -155,11 +194,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Controls hint + nav mode -->
|
||||
<div class="hud-controls">
|
||||
<div class="hud-controls" aria-label="Keyboard and mouse controls">
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot" role="status" aria-label="Hermes WebSocket connection status"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Portal Hint -->
|
||||
@@ -183,7 +222,7 @@
|
||||
</div>
|
||||
<h2 id="vision-title-display">SOVEREIGNTY</h2>
|
||||
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
|
||||
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
|
||||
<button id="vision-close-btn" class="vision-close-btn" aria-label="Close vision point overlay">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +241,7 @@
|
||||
</div>
|
||||
<div class="portal-error-box" id="portal-error-box" style="display:none;">
|
||||
<div class="portal-error-msg">DESTINATION NOT YET LINKED</div>
|
||||
<button id="portal-close-btn" class="portal-close-btn">CLOSE</button>
|
||||
<button id="portal-close-btn" class="portal-close-btn" aria-label="Close portal redirect">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,8 +254,8 @@
|
||||
<span class="memory-category-badge" id="memory-panel-category-badge">MEM</span>
|
||||
<div class="memory-panel-region-dot" id="memory-panel-region-dot"></div>
|
||||
<div class="memory-panel-region" id="memory-panel-region">MEMORY</div>
|
||||
<button id="memory-panel-pin" class="memory-panel-pin" title="Pin panel">📌</button>
|
||||
<button id="memory-panel-close" class="memory-panel-close" onclick="_dismissMemoryPanelForce()">\u2715</button>
|
||||
<button id="memory-panel-pin" class="memory-panel-pin" aria-label="Pin memory panel" title="Pin panel" data-tooltip="Pin Panel">📌</button>
|
||||
<button id="memory-panel-close" class="memory-panel-close" aria-label="Close memory panel" data-tooltip="Close" onclick="_dismissMemoryPanelForce()">\u2715</button>
|
||||
</div>
|
||||
<div class="memory-entity-name" id="memory-panel-entity-name">\u2014</div>
|
||||
<div class="memory-panel-body" id="memory-panel-content">(empty)</div>
|
||||
@@ -233,6 +272,11 @@
|
||||
<div class="memory-meta-row"><span class="memory-meta-label">Time</span><span id="memory-panel-time">\u2014</span></div>
|
||||
<div class="memory-meta-row memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
|
||||
</div>
|
||||
<div class="memory-panel-actions">
|
||||
<button id="mnemosyne-export-btn" class="mnemosyne-action-btn" title="Export spatial memory to JSON">⤓ Export</button>
|
||||
<button id="mnemosyne-import-btn" class="mnemosyne-action-btn" title="Import spatial memory from JSON">⤒ Import</button>
|
||||
<input type="file" id="mnemosyne-import-file" accept=".json" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,7 +286,7 @@
|
||||
<div class="session-room-header">
|
||||
<span class="session-room-icon">□</span>
|
||||
<div class="session-room-title">SESSION CHAMBER</div>
|
||||
<button class="session-room-close" id="session-room-close" title="Close">✕</button>
|
||||
<button class="session-room-close" id="session-room-close" aria-label="Close session room panel" title="Close" data-tooltip="Close">✕</button>
|
||||
</div>
|
||||
<div class="session-room-timestamp" id="session-room-timestamp">—</div>
|
||||
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
|
||||
@@ -259,7 +303,7 @@
|
||||
<span class="atlas-icon">🌐</span>
|
||||
<h2>PORTAL ATLAS</h2>
|
||||
</div>
|
||||
<button id="atlas-close-btn" class="atlas-close-btn">CLOSE</button>
|
||||
<button id="atlas-close-btn" class="atlas-close-btn" aria-label="Close Portal Atlas overlay">CLOSE</button>
|
||||
</div>
|
||||
<div class="atlas-grid" id="atlas-grid">
|
||||
<!-- Portals will be injected here -->
|
||||
|
||||
@@ -652,13 +652,168 @@ const SpatialMemory = (() => {
|
||||
return _selectedId;
|
||||
}
|
||||
|
||||
// ─── FILE EXPORT ──────────────────────────────────────
|
||||
function exportToFile() {
|
||||
const index = exportIndex();
|
||||
const json = JSON.stringify(index, null, 2);
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const filename = 'mnemosyne-export-' + date + '.json';
|
||||
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.info('[Mnemosyne] Exported', index.memories.length, 'memories to', filename);
|
||||
return { filename, count: index.memories.length };
|
||||
}
|
||||
|
||||
// ─── FILE IMPORT ──────────────────────────────────────
|
||||
function importFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) {
|
||||
reject(new Error('No file provided'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
|
||||
// Schema validation
|
||||
if (!data || typeof data !== 'object') {
|
||||
reject(new Error('Invalid JSON: not an object'));
|
||||
return;
|
||||
}
|
||||
if (typeof data.version !== 'number') {
|
||||
reject(new Error('Invalid schema: missing version field'));
|
||||
return;
|
||||
}
|
||||
if (data.version !== STORAGE_VERSION) {
|
||||
reject(new Error('Version mismatch: got ' + data.version + ', expected ' + STORAGE_VERSION));
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.memories)) {
|
||||
reject(new Error('Invalid schema: memories is not an array'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate each memory entry
|
||||
for (let i = 0; i < data.memories.length; i++) {
|
||||
const mem = data.memories[i];
|
||||
if (!mem.id || typeof mem.id !== 'string') {
|
||||
reject(new Error('Invalid memory at index ' + i + ': missing or invalid id'));
|
||||
return;
|
||||
}
|
||||
if (!mem.category || typeof mem.category !== 'string') {
|
||||
reject(new Error('Invalid memory "' + mem.id + '": missing category'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const count = importIndex(data);
|
||||
saveToStorage();
|
||||
console.info('[Mnemosyne] Imported', count, 'memories from file');
|
||||
resolve({ count, total: data.memories.length });
|
||||
} catch (parseErr) {
|
||||
reject(new Error('Failed to parse JSON: ' + parseErr.message));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ─── SPATIAL SEARCH (issue #1170) ────────────────────
|
||||
let _searchOriginalState = {}; // memId -> { emissiveIntensity, opacity } for restore
|
||||
|
||||
function searchContent(query) {
|
||||
if (!query || !query.trim()) return [];
|
||||
const q = query.toLowerCase().trim();
|
||||
const matches = [];
|
||||
|
||||
Object.values(_memoryObjects).forEach(obj => {
|
||||
const d = obj.data;
|
||||
const searchable = [
|
||||
d.content || '',
|
||||
d.id || '',
|
||||
d.category || '',
|
||||
d.source || '',
|
||||
...(d.connections || [])
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
if (searchable.includes(q)) {
|
||||
matches.push(d.id);
|
||||
}
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function highlightSearchResults(matchIds) {
|
||||
// Save original state and apply search highlighting
|
||||
_searchOriginalState = {};
|
||||
const matchSet = new Set(matchIds);
|
||||
|
||||
Object.entries(_memoryObjects).forEach(([id, obj]) => {
|
||||
const mat = obj.mesh.material;
|
||||
_searchOriginalState[id] = {
|
||||
emissiveIntensity: mat.emissiveIntensity,
|
||||
opacity: mat.opacity
|
||||
};
|
||||
|
||||
if (matchSet.has(id)) {
|
||||
// Match: bright white glow
|
||||
mat.emissive.setHex(0xffffff);
|
||||
mat.emissiveIntensity = 5.0;
|
||||
mat.opacity = 1.0;
|
||||
} else {
|
||||
// Non-match: dim to 10% opacity
|
||||
mat.opacity = 0.1;
|
||||
mat.emissiveIntensity = 0.2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
Object.entries(_memoryObjects).forEach(([id, obj]) => {
|
||||
const mat = obj.mesh.material;
|
||||
const saved = _searchOriginalState[id];
|
||||
if (saved) {
|
||||
// Restore original emissive color from region
|
||||
const region = REGIONS[obj.region] || REGIONS.working;
|
||||
mat.emissive.copy(region.color);
|
||||
mat.emissiveIntensity = saved.emissiveIntensity;
|
||||
mat.opacity = saved.opacity;
|
||||
}
|
||||
});
|
||||
_searchOriginalState = {};
|
||||
}
|
||||
|
||||
function getSearchMatchPosition(matchId) {
|
||||
const obj = _memoryObjects[matchId];
|
||||
return obj ? obj.mesh.position.clone() : null;
|
||||
}
|
||||
|
||||
return {
|
||||
init, placeMemory, removeMemory, update,
|
||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||
exportIndex, importIndex, searchNearby, REGIONS,
|
||||
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
|
||||
saveToStorage, loadFromStorage, clearStorage,
|
||||
runGravityLayout
|
||||
runGravityLayout,
|
||||
searchContent, highlightSearchResults, clearSearch, getSearchMatchPosition
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# Bannerlord Local Install Guide (macOS / Apple Silicon)
|
||||
|
||||
## Goal
|
||||
Run the GOG Mount & Blade II: Bannerlord build natively on Alexander's Mac (arm64, macOS Sequoia+).
|
||||
|
||||
## Prerequisites
|
||||
- macOS 14+ on Apple Silicon (arm64)
|
||||
- ~60 GB free disk space (game + Wine prefix)
|
||||
- GOG installer files in `~/Downloads/`:
|
||||
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124)-1.bin` through `-13.bin`
|
||||
|
||||
## Step 1: Install Porting Kit
|
||||
|
||||
Porting Kit (free) wraps Wine/GPTK for macOS. It has a GUI but we automate what we can.
|
||||
|
||||
```bash
|
||||
brew install --cask porting-kit
|
||||
```
|
||||
|
||||
Launch it once to complete first-run setup:
|
||||
```bash
|
||||
open -a "Porting Kit"
|
||||
```
|
||||
|
||||
## Step 2: Create Wine Prefix + Install Game
|
||||
|
||||
**Option A: Via Porting Kit GUI (recommended)**
|
||||
|
||||
1. Open Porting Kit
|
||||
2. Click "Install Game" → "Custom Port" or search for Bannerlord
|
||||
3. Point it at: `~/Downloads/setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||
4. Follow the GOG installer wizard
|
||||
5. Install to default path inside the Wine prefix
|
||||
6. When done, note the prefix path (usually `~/Library/Application Support/PortingKit/...`)
|
||||
|
||||
**Option B: Manual Wine prefix (advanced)**
|
||||
|
||||
If you have Homebrew Wine (or GPTK) installed:
|
||||
|
||||
```bash
|
||||
# Create prefix
|
||||
export WINEPREFIX="$HOME/Games/Bannerlord"
|
||||
wine64 boot /init
|
||||
|
||||
# Run the GOG installer (it auto-chains the .bin files)
|
||||
cd ~/Downloads
|
||||
wine64 setup_mount__blade_ii_bannerlord_1.3.15.109797_\(64bit\)_\(89124\).exe
|
||||
```
|
||||
|
||||
Follow the GOG installer wizard. Default install path is fine.
|
||||
|
||||
## Step 3: Locate the Game Binary
|
||||
|
||||
After installation, the game executable is at:
|
||||
```
|
||||
$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||
```
|
||||
|
||||
Or inside Porting Kit's prefix at:
|
||||
```
|
||||
~/Library/Application Support/PortingKit/<prefix-name>/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||
```
|
||||
|
||||
## Step 4: First Launch
|
||||
|
||||
```bash
|
||||
# Find the actual path first, then:
|
||||
cd "$HOME/Games/Bannerlord/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client"
|
||||
wine64 Bannerlord.exe
|
||||
```
|
||||
|
||||
Or use the launcher script:
|
||||
```bash
|
||||
./portal/bannerlord/launch.sh
|
||||
```
|
||||
|
||||
## Step 5: Proof (Operator Checklist)
|
||||
|
||||
- [ ] Game window opens and is visible on screen
|
||||
- [ ] At least the main menu renders (TaleWorlds logo, "Campaign", "Custom Battle", etc.)
|
||||
- [ ] Screenshot taken: save to `portal/bannerlord/proof/`
|
||||
- [ ] Launch command recorded below for repeatability
|
||||
|
||||
**Launch command (fill in after install):**
|
||||
```
|
||||
# Repeatable launch:
|
||||
./portal/bannerlord/launch.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Black screen on launch:**
|
||||
- Try: `wine64 Bannerlord.exe -force-d3d11` or `-force-vulkan`
|
||||
- Set Windows version: `winecfg` → set to Windows 10
|
||||
|
||||
**Missing DLLs:**
|
||||
- Install DirectX runtime: `winetricks d3dx9 d3dx10 d3dx11 vcrun2019`
|
||||
|
||||
**Performance:**
|
||||
- GPTK/Rosetta overhead is expected; 30-60 FPS is normal on M1/M2
|
||||
- Lower in-game graphics settings to "Medium" for first run
|
||||
|
||||
**Installer won't chain .bin files:**
|
||||
- Make sure all .bin files are in the same directory as the .exe
|
||||
- Verify with: `ls -la ~/Downloads/setup_mount__blade_ii_bannerlord_*`
|
||||
|
||||
## References
|
||||
- GamePortal Protocol: `GAMEPORTAL_PROTOCOL.md`
|
||||
- Portal config: `portals.json` (entry: "bannerlord")
|
||||
- GOG App ID: Mount & Blade II: Bannerlord
|
||||
- Steam App ID: 261550 (for Steam stats integration)
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bannerlord Launcher for macOS (Apple Silicon via Wine/GPTK)
|
||||
# Usage: ./portal/bannerlord/launch.sh [--wine-prefix PATH] [--exe PATH]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Defaults — override with flags or environment
|
||||
WINEPREFIX="${WINEPREFIX:-$HOME/Games/Bannerlord}"
|
||||
BANNERLORD_EXE=""
|
||||
WINE_CMD=""
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--wine-prefix) WINEPREFIX="$2"; shift 2 ;;
|
||||
--exe) BANNERLORD_EXE="$2"; shift 2 ;;
|
||||
--help)
|
||||
echo "Usage: $0 [--wine-prefix PATH] [--exe PATH]"
|
||||
echo ""
|
||||
echo "Defaults:"
|
||||
echo " Wine prefix: $WINEPREFIX"
|
||||
echo " Auto-discovers Bannerlord.exe in the prefix"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Find wine command
|
||||
find_wine() {
|
||||
if command -v wine64 &>/dev/null; then
|
||||
echo "wine64"
|
||||
elif command -v wine &>/dev/null; then
|
||||
echo "wine"
|
||||
elif [ -f "/Applications/Whisky.app/Contents/Resources/WhiskyCmd" ]; then
|
||||
echo "/Applications/Whisky.app/Contents/Resources/WhiskyCmd"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
WINE_CMD="$(find_wine)"
|
||||
if [ -z "$WINE_CMD" ]; then
|
||||
echo "ERROR: No Wine runtime found."
|
||||
echo "Install one of:"
|
||||
echo " brew install --cask porting-kit"
|
||||
echo " brew install --cask crossover"
|
||||
echo " brew tap apple/apple && brew install game-porting-toolkit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Wine runtime: $WINE_CMD"
|
||||
echo "Wine prefix: $WINEPREFIX"
|
||||
|
||||
# Find Bannerlord.exe if not specified
|
||||
if [ -z "$BANNERLORD_EXE" ]; then
|
||||
# Search common GOG install paths
|
||||
SEARCH_PATHS=(
|
||||
"$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
"$WINEPREFIX/drive_c/GOG Games/Mount Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
"$WINEPREFIX/drive_c/Program Files/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||
)
|
||||
|
||||
# Also search PortingKit prefixes
|
||||
while IFS= read -r -d '' exe; do
|
||||
SEARCH_PATHS+=("$exe")
|
||||
done < <(find "$HOME/Library/Application Support/PortingKit" -name "Bannerlord.exe" -print0 2>/dev/null || true)
|
||||
|
||||
for path in "${SEARCH_PATHS[@]}"; do
|
||||
if [ -f "$path" ]; then
|
||||
BANNERLORD_EXE="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$BANNERLORD_EXE" ] || [ ! -f "$BANNERLORD_EXE" ]; then
|
||||
echo "ERROR: Bannerlord.exe not found."
|
||||
echo "Searched:"
|
||||
echo " $WINEPREFIX/drive_c/GOG Games/"
|
||||
echo " ~/Library/Application Support/PortingKit/"
|
||||
echo ""
|
||||
echo "Run the install first. See: portal/bannerlord/INSTALL.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Game binary: $BANNERLORD_EXE"
|
||||
echo "Launching..."
|
||||
echo ""
|
||||
|
||||
# Log the launch for proof
|
||||
LAUNCH_LOG="$SCRIPT_DIR/proof/launch_$(date +%Y%m%d_%H%M%S).log"
|
||||
mkdir -p "$SCRIPT_DIR/proof"
|
||||
{
|
||||
echo "=== Bannerlord Launch ==="
|
||||
echo "Date: $(date -Iseconds)"
|
||||
echo "Wine: $WINE_CMD"
|
||||
echo "Prefix: $WINEPREFIX"
|
||||
echo "Binary: $BANNERLORD_EXE"
|
||||
echo "User: $(whoami)"
|
||||
echo "macOS: $(sw_vers -productVersion)"
|
||||
echo "Arch: $(uname -m)"
|
||||
echo "========================="
|
||||
} > "$LAUNCH_LOG"
|
||||
echo "Launch log: $LAUNCH_LOG"
|
||||
echo ""
|
||||
|
||||
# Set the prefix and launch
|
||||
export WINEPREFIX
|
||||
EXE_DIR="$(dirname "$BANNERLORD_EXE")"
|
||||
cd "$EXE_DIR"
|
||||
exec "$WINE_CMD" "Bannerlord.exe" "$@"
|
||||
@@ -1,16 +0,0 @@
|
||||
# Bannerlord Proof
|
||||
|
||||
Screenshots and launch logs proving the game runs locally on the Mac.
|
||||
|
||||
## How to capture proof
|
||||
|
||||
1. Launch the game: `./portal/bannerlord/launch.sh`
|
||||
2. Wait for main menu to render
|
||||
3. Take screenshot: `screencapture -x portal/bannerlord/proof/main_menu_$(date +%Y%m%d).png`
|
||||
4. Save launch log (auto-generated by launch.sh)
|
||||
|
||||
## Expected proof files
|
||||
|
||||
- `main_menu_*.png` — screenshot of game main menu
|
||||
- `launch_*.log` — launch command + environment details
|
||||
- `ingame_*.png` — optional in-game screenshots
|
||||
13
portals.json
13
portals.json
@@ -23,21 +23,18 @@
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "local",
|
||||
"environment": "production",
|
||||
"access_mode": "operator",
|
||||
"readiness_state": "active",
|
||||
"telemetry_source": "local-desktop:bannerlord",
|
||||
"telemetry_source": "hermes-harness:bannerlord",
|
||||
"owner": "Timmy",
|
||||
"app_id": 261550,
|
||||
"window_title": "Mount & Blade II: Bannerlord",
|
||||
"install_source": "gog",
|
||||
"gog_version": "1.3.15.109797",
|
||||
"launcher_script": "portal/bannerlord/launch.sh",
|
||||
"install_guide": "portal/bannerlord/INSTALL.md",
|
||||
"destination": {
|
||||
"type": "local-launch",
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Calradia",
|
||||
"params": { "world": "calradia", "runtime": "wine/gptk" }
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
247
style.css
247
style.css
@@ -200,6 +200,61 @@ canvas#nexus-canvas {
|
||||
box-shadow: 0 0 20px var(--color-primary);
|
||||
}
|
||||
|
||||
/* === TOOLTIP SYSTEM === */
|
||||
/* Any element with data-tooltip gets a hover tooltip label */
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
right: calc(100% + 10px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(5, 5, 16, 0.95);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--color-primary-dim);
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
|
||||
z-index: 100;
|
||||
}
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:focus-visible::after {
|
||||
opacity: 1;
|
||||
}
|
||||
/* For elements positioned on the right side, tooltip appears to the left */
|
||||
.hud-top-right [data-tooltip]::after {
|
||||
right: calc(100% + 10px);
|
||||
}
|
||||
/* For inline/badge elements where right-side tooltip might clip */
|
||||
.hud-status-item[data-tooltip]::after {
|
||||
right: auto;
|
||||
left: calc(100% + 10px);
|
||||
}
|
||||
|
||||
/* Focus-visible ring for keyboard navigation */
|
||||
.hud-icon-btn:focus-visible,
|
||||
.hud-status-item:focus-visible,
|
||||
.atlas-close-btn:focus-visible,
|
||||
.vision-close-btn:focus-visible,
|
||||
.portal-close-btn:focus-visible,
|
||||
.memory-panel-close:focus-visible,
|
||||
.memory-panel-pin:focus-visible,
|
||||
.session-room-close:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 16px rgba(74, 240, 192, 0.4);
|
||||
}
|
||||
|
||||
.hud-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -983,7 +1038,7 @@ canvas#nexus-canvas {
|
||||
|
||||
.chat-quick-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
@@ -991,6 +1046,75 @@ canvas#nexus-canvas {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.chat-quick-actions.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.starter-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--color-primary-dim);
|
||||
text-transform: uppercase;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.starter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.starter-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
background: rgba(74, 240, 192, 0.06);
|
||||
border: 1px solid rgba(74, 240, 192, 0.15);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-body);
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-ui);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.starter-btn:hover {
|
||||
background: rgba(74, 240, 192, 0.15);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.starter-btn:hover .starter-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.starter-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.starter-icon {
|
||||
font-size: 12px;
|
||||
color: var(--color-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.starter-text {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.starter-desc {
|
||||
font-size: 8px;
|
||||
color: rgba(74, 240, 192, 0.5);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Add hover effect for MemPalace mining button */
|
||||
.quick-action-btn:hover {
|
||||
background: var(--color-primary-dim);
|
||||
@@ -1136,6 +1260,9 @@ canvas#nexus-canvas {
|
||||
.hud-location {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.starter-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -1461,6 +1588,43 @@ canvas#nexus-canvas {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
PROJECT MNEMOSYNE — EXPORT/IMPORT ACTIONS (#1174)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
.memory-panel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(123, 92, 255, 0.15);
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
background: rgba(123, 92, 255, 0.12);
|
||||
border: 1px solid rgba(123, 92, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #a08cff;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn:hover {
|
||||
background: rgba(123, 92, 255, 0.25);
|
||||
border-color: rgba(123, 92, 255, 0.6);
|
||||
color: #c4b5ff;
|
||||
}
|
||||
|
||||
.mnemosyne-action-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
@@ -1580,3 +1744,84 @@ canvas#nexus-canvas {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
/* ═══ SPATIAL SEARCH OVERLAY (Mnemosyne #1170) ═══ */
|
||||
.spatial-search-overlay {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.spatial-search-input {
|
||||
width: 260px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #e0f0ff;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.spatial-search-input:focus {
|
||||
border-color: rgba(74, 240, 192, 0.7);
|
||||
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
|
||||
}
|
||||
|
||||
.spatial-search-input::placeholder {
|
||||
color: rgba(224, 240, 255, 0.35);
|
||||
}
|
||||
|
||||
.spatial-search-results {
|
||||
margin-top: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border: 1px solid rgba(74, 240, 192, 0.15);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: #a0c0d0;
|
||||
width: 260px;
|
||||
backdrop-filter: blur(8px);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spatial-search-results.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spatial-search-result-item {
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.08);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.spatial-search-result-item:hover {
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
color: #e0f0ff;
|
||||
}
|
||||
|
||||
.spatial-search-result-item .result-region {
|
||||
color: #4af0c0;
|
||||
font-size: 9px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.spatial-search-count {
|
||||
padding: 4px 10px;
|
||||
color: rgba(74, 240, 192, 0.6);
|
||||
font-size: 10px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user