Compare commits
1 Commits
mimo/code/
...
mimo/build
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89907acf6a |
38
app.js
38
app.js
@@ -5,6 +5,7 @@ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
|
||||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||
import { SessionRooms } from './nexus/components/session-rooms.js';
|
||||
import { EvenniaRoomPanel } from './nexus/components/evennia-room-panel.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -707,6 +708,7 @@ async function init() {
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
SessionRooms.init(scene, camera, null);
|
||||
EvenniaRoomPanel.init();
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
@@ -2006,30 +2008,6 @@ function setupControls() {
|
||||
|
||||
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) {
|
||||
@@ -2098,6 +2076,7 @@ function connectHermes() {
|
||||
addChatMessage('system', 'Hermes link established.');
|
||||
updateWsHudStatus(true);
|
||||
refreshWorkshopPanel();
|
||||
EvenniaRoomPanel.setConnected(true);
|
||||
};
|
||||
|
||||
// Initialize MemPalace
|
||||
@@ -2126,6 +2105,7 @@ function connectHermes() {
|
||||
hermesWs = null;
|
||||
updateWsHudStatus(false);
|
||||
refreshWorkshopPanel();
|
||||
EvenniaRoomPanel.setConnected(false);
|
||||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = setTimeout(connectHermes, 5000);
|
||||
};
|
||||
@@ -2136,6 +2116,16 @@ function connectHermes() {
|
||||
}
|
||||
|
||||
function handleHermesMessage(data) {
|
||||
// ── Evennia room snapshot events (#728) ──
|
||||
if (data.type === 'evennia.room_snapshot') {
|
||||
EvenniaRoomPanel.onRoomSnapshot(data);
|
||||
return;
|
||||
}
|
||||
if (data.type === 'evennia.actor_located') {
|
||||
EvenniaRoomPanel.onActorLocated(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'chat') {
|
||||
addChatMessage(data.agent || 'timmy', data.text);
|
||||
} else if (data.type === 'tool_call') {
|
||||
|
||||
18
index.html
18
index.html
@@ -125,6 +125,19 @@
|
||||
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
|
||||
<div id="agent-log-content" class="agent-log-content"></div>
|
||||
</div>
|
||||
<!-- Evennia Room Snapshot Operator Panel (#728) -->
|
||||
<div id="evennia-room-panel" class="evennia-room-panel" aria-live="polite">
|
||||
<div class="erp-header">
|
||||
<span class="erp-icon">◈</span>
|
||||
<span class="erp-title">EVENNIA ROOM</span>
|
||||
<span class="erp-status-dot erp-offline"></span>
|
||||
</div>
|
||||
<div class="erp-body erp-empty-state">
|
||||
<div class="erp-empty-icon">⊘</div>
|
||||
<div class="erp-empty-label">DISCONNECTED</div>
|
||||
<div class="erp-empty-hint">No link to Evennia world.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Chat Interface -->
|
||||
@@ -233,11 +246,6 @@
|
||||
<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>
|
||||
|
||||
|
||||
229
nexus/components/evennia-room-panel.js
Normal file
229
nexus/components/evennia-room-panel.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// EVENNIA ROOM SNAPSHOT OPERATOR PANEL (Issue #728)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
//
|
||||
// Renders the current Evennia room state in the Nexus HUD.
|
||||
// Consumes evennia.room_snapshot and evennia.actor_located
|
||||
// events from the Hermes WebSocket bridge.
|
||||
//
|
||||
// States:
|
||||
// offline — no WS connection
|
||||
// awaiting — connected but no room data yet
|
||||
// in-room — room snapshot loaded, render full panel
|
||||
//
|
||||
// Usage from app.js:
|
||||
// EvenniaRoomPanel.init();
|
||||
// EvenniaRoomPanel.onRoomSnapshot(data);
|
||||
// EvenniaRoomPanel.onActorLocated(data);
|
||||
// EvenniaRoomPanel.setConnected(bool);
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
export const EvenniaRoomPanel = (() => {
|
||||
|
||||
// ─── STATE ────────────────────────────────────────────
|
||||
let _connected = false;
|
||||
let _roomData = null; // latest evennia.room_snapshot payload
|
||||
let _actorRoomId = null; // from evennia.actor_located
|
||||
let _lastUpdate = null; // timestamp of last snapshot
|
||||
let _panelEl = null; // DOM root
|
||||
let _init = false;
|
||||
|
||||
// ─── DOM REFS ─────────────────────────────────────────
|
||||
function _el(id) { return document.getElementById(id); }
|
||||
|
||||
// ─── INIT ─────────────────────────────────────────────
|
||||
function init() {
|
||||
_panelEl = _el('evennia-room-panel');
|
||||
if (!_panelEl) {
|
||||
console.warn('[EvenniaRoomPanel] Panel element not found in DOM.');
|
||||
return;
|
||||
}
|
||||
_init = true;
|
||||
_render();
|
||||
console.log('[EvenniaRoomPanel] Initialized.');
|
||||
}
|
||||
|
||||
// ─── EVENT HANDLERS ───────────────────────────────────
|
||||
|
||||
function onRoomSnapshot(data) {
|
||||
_roomData = data;
|
||||
_lastUpdate = data.timestamp || new Date().toISOString();
|
||||
_actorRoomId = data.room_id || data.room_key || null;
|
||||
_render();
|
||||
}
|
||||
|
||||
function onActorLocated(data) {
|
||||
_actorRoomId = data.room_id || data.room_key || null;
|
||||
// If we get a location but no snapshot yet, show awaiting
|
||||
if (!_roomData || (_roomData.room_id !== _actorRoomId && _roomData.room_key !== _actorRoomId)) {
|
||||
_render();
|
||||
}
|
||||
}
|
||||
|
||||
function setConnected(connected) {
|
||||
_connected = connected;
|
||||
if (!connected) {
|
||||
// Clear room data on disconnect — stale data is lying
|
||||
_roomData = null;
|
||||
_actorRoomId = null;
|
||||
_lastUpdate = null;
|
||||
}
|
||||
_render();
|
||||
}
|
||||
|
||||
// ─── RENDER ───────────────────────────────────────────
|
||||
|
||||
function _render() {
|
||||
if (!_panelEl) return;
|
||||
|
||||
if (!_connected) {
|
||||
_renderOffline();
|
||||
} else if (!_roomData) {
|
||||
_renderAwaiting();
|
||||
} else {
|
||||
_renderRoom();
|
||||
}
|
||||
}
|
||||
|
||||
function _renderOffline() {
|
||||
_panelEl.innerHTML = `
|
||||
<div class="erp-header">
|
||||
<span class="erp-icon">◈</span>
|
||||
<span class="erp-title">EVENNIA ROOM</span>
|
||||
<span class="erp-status-dot erp-offline"></span>
|
||||
</div>
|
||||
<div class="erp-body erp-empty-state">
|
||||
<div class="erp-empty-icon">⊘</div>
|
||||
<div class="erp-empty-label">DISCONNECTED</div>
|
||||
<div class="erp-empty-hint">No link to Evennia world.</div>
|
||||
</div>
|
||||
`;
|
||||
_panelEl.classList.add('erp-state-offline');
|
||||
_panelEl.classList.remove('erp-state-awaiting', 'erp-state-inroom');
|
||||
}
|
||||
|
||||
function _renderAwaiting() {
|
||||
_panelEl.innerHTML = `
|
||||
<div class="erp-header">
|
||||
<span class="erp-icon">◈</span>
|
||||
<span class="erp-title">EVENNIA ROOM</span>
|
||||
<span class="erp-status-dot erp-online"></span>
|
||||
</div>
|
||||
<div class="erp-body erp-empty-state">
|
||||
<div class="erp-empty-icon erp-pulse">◎</div>
|
||||
<div class="erp-empty-label">AWAITING SNAPSHOT</div>
|
||||
<div class="erp-empty-hint">Connected. Waiting for room data…</div>
|
||||
</div>
|
||||
`;
|
||||
_panelEl.classList.add('erp-state-awaiting');
|
||||
_panelEl.classList.remove('erp-state-offline', 'erp-state-inroom');
|
||||
}
|
||||
|
||||
function _renderRoom() {
|
||||
const room = _roomData;
|
||||
const title = _esc(room.title || room.room_name || room.room_key || 'Unknown Room');
|
||||
const desc = _esc(room.desc || 'No description available.');
|
||||
const exits = Array.isArray(room.exits) ? room.exits : [];
|
||||
const objects = Array.isArray(room.objects) ? room.objects : [];
|
||||
const occupants = Array.isArray(room.occupants) ? room.occupants : [];
|
||||
const roomId = _esc(room.room_id || room.room_key || '—');
|
||||
const timeStr = _formatTime(_lastUpdate);
|
||||
|
||||
// Build exits list
|
||||
let exitsHtml = '';
|
||||
if (exits.length > 0) {
|
||||
exitsHtml = exits.map(e => {
|
||||
const name = _esc(e.key || e.name || '?');
|
||||
const dest = _esc(e.destination_name || e.destination_id || e.destination_key || '');
|
||||
return `<div class="erp-exit-row">
|
||||
<span class="erp-exit-arrow">→</span>
|
||||
<span class="erp-exit-name">${name}</span>
|
||||
${dest ? `<span class="erp-exit-dest">${dest}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
exitsHtml = '<div class="erp-none">No visible exits.</div>';
|
||||
}
|
||||
|
||||
// Build objects list
|
||||
let objectsHtml = '';
|
||||
if (objects.length > 0) {
|
||||
objectsHtml = objects.map(o => {
|
||||
const name = _esc(o.key || o.id || '?');
|
||||
const desc = _esc(o.short_desc || '');
|
||||
return `<div class="erp-object-row">
|
||||
<span class="erp-object-icon">▪</span>
|
||||
<span class="erp-object-name">${name}</span>
|
||||
${desc ? `<span class="erp-object-desc">${desc}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
objectsHtml = '<div class="erp-none">No visible objects.</div>';
|
||||
}
|
||||
|
||||
// Build occupants list
|
||||
let occupantsHtml = '';
|
||||
if (occupants.length > 0) {
|
||||
occupantsHtml = occupants.map(o => {
|
||||
const name = _esc(typeof o === 'string' ? o : (o.name || o.key || '?'));
|
||||
return `<span class="erp-occupant">${name}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
_panelEl.innerHTML = `
|
||||
<div class="erp-header">
|
||||
<span class="erp-icon">◈</span>
|
||||
<span class="erp-title">${title}</span>
|
||||
<span class="erp-status-dot erp-online"></span>
|
||||
</div>
|
||||
<div class="erp-body">
|
||||
<div class="erp-room-id">${roomId}</div>
|
||||
<div class="erp-desc">${desc}</div>
|
||||
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-label">EXITS</div>
|
||||
<div class="erp-exits">${exitsHtml}</div>
|
||||
</div>
|
||||
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-label">OBJECTS</div>
|
||||
<div class="erp-objects">${objectsHtml}</div>
|
||||
</div>
|
||||
|
||||
${occupantsHtml ? `
|
||||
<div class="erp-section">
|
||||
<div class="erp-section-label">OCCUPANTS</div>
|
||||
<div class="erp-occupants">${occupantsHtml}</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="erp-footer">
|
||||
<span class="erp-time">${timeStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
_panelEl.classList.add('erp-state-inroom');
|
||||
_panelEl.classList.remove('erp-state-offline', 'erp-state-awaiting');
|
||||
}
|
||||
|
||||
// ─── UTILS ────────────────────────────────────────────
|
||||
|
||||
function _esc(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function _formatTime(isoStr) {
|
||||
if (!isoStr) return '—';
|
||||
try {
|
||||
const d = new Date(isoStr);
|
||||
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||
} catch {
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PUBLIC API ───────────────────────────────────────
|
||||
return { init, onRoomSnapshot, onActorLocated, setConnected };
|
||||
|
||||
})();
|
||||
@@ -652,93 +652,11 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init, placeMemory, removeMemory, update,
|
||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
|
||||
exportIndex, importIndex, searchNearby, REGIONS,
|
||||
saveToStorage, loadFromStorage, clearStorage,
|
||||
runGravityLayout
|
||||
};
|
||||
|
||||
263
style.css
263
style.css
@@ -1461,43 +1461,6 @@ 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)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
@@ -1617,3 +1580,229 @@ canvas#nexus-canvas {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
EVENNIA ROOM SNAPSHOT OPERATOR PANEL (#728)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
.evennia-room-panel {
|
||||
width: 280px;
|
||||
background: rgba(5, 5, 16, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(74, 240, 192, 0.18);
|
||||
border-left: 3px solid var(--color-primary, #4af0c0);
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-body, 'JetBrains Mono', monospace);
|
||||
font-size: 11px;
|
||||
color: var(--color-text, #e0f0ff);
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.evennia-room-panel.erp-state-offline {
|
||||
border-left-color: var(--color-danger, #ff4466);
|
||||
}
|
||||
|
||||
.evennia-room-panel.erp-state-awaiting {
|
||||
border-left-color: var(--color-warning, #ffaa22);
|
||||
}
|
||||
|
||||
.evennia-room-panel.erp-state-inroom {
|
||||
border-left-color: var(--color-primary, #4af0c0);
|
||||
}
|
||||
|
||||
.erp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
|
||||
background: rgba(74, 240, 192, 0.04);
|
||||
}
|
||||
|
||||
.erp-icon {
|
||||
font-size: 12px;
|
||||
color: var(--color-primary, #4af0c0);
|
||||
}
|
||||
|
||||
.erp-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
color: var(--color-primary, #4af0c0);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.erp-status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.erp-status-dot.erp-offline {
|
||||
background: var(--color-danger, #ff4466);
|
||||
box-shadow: 0 0 6px var(--color-danger, #ff4466);
|
||||
}
|
||||
|
||||
.erp-status-dot.erp-online {
|
||||
background: var(--color-primary, #4af0c0);
|
||||
box-shadow: 0 0 6px var(--color-primary, #4af0c0);
|
||||
}
|
||||
|
||||
.erp-body {
|
||||
padding: 8px 10px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Empty / offline states */
|
||||
.erp-empty-state {
|
||||
text-align: center;
|
||||
padding: 18px 10px;
|
||||
}
|
||||
|
||||
.erp-empty-icon {
|
||||
font-size: 22px;
|
||||
color: rgba(74, 240, 192, 0.25);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.erp-empty-icon.erp-pulse {
|
||||
animation: erpPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes erpPulse {
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 0.9; }
|
||||
}
|
||||
|
||||
.erp-empty-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--color-text-muted, #8a9ab8);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.erp-empty-hint {
|
||||
font-size: 10px;
|
||||
color: rgba(138, 154, 184, 0.55);
|
||||
}
|
||||
|
||||
/* Room content */
|
||||
.erp-room-id {
|
||||
font-size: 9px;
|
||||
color: rgba(138, 154, 184, 0.4);
|
||||
letter-spacing: 0.8px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.erp-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(224, 240, 255, 0.8);
|
||||
line-height: 1.45;
|
||||
margin-bottom: 10px;
|
||||
border-left: 2px solid rgba(74, 240, 192, 0.15);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.erp-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.erp-section-label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--color-primary, #4af0c0);
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.erp-none {
|
||||
font-size: 10px;
|
||||
color: rgba(138, 154, 184, 0.35);
|
||||
font-style: italic;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Exits */
|
||||
.erp-exit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.erp-exit-arrow {
|
||||
color: var(--color-secondary, #7b5cff);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.erp-exit-name {
|
||||
color: var(--color-secondary, #7b5cff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.erp-exit-dest {
|
||||
color: rgba(138, 154, 184, 0.45);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Objects */
|
||||
.erp-object-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
padding: 2px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.erp-object-icon {
|
||||
color: var(--color-gold, #ffd700);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.erp-object-name {
|
||||
color: rgba(224, 240, 255, 0.75);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.erp-object-desc {
|
||||
color: rgba(138, 154, 184, 0.45);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Occupants */
|
||||
.erp-occupants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.erp-occupant {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(74, 240, 192, 0.08);
|
||||
border: 1px solid rgba(74, 240, 192, 0.15);
|
||||
border-radius: 3px;
|
||||
color: var(--color-primary, #4af0c0);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.erp-footer {
|
||||
margin-top: 6px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid rgba(74, 240, 192, 0.06);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.erp-time {
|
||||
font-size: 9px;
|
||||
color: rgba(138, 154, 184, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user