Compare commits
2 Commits
mimo/code/
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d2dbabd1 | |||
| efda34c8a9 |
46
app.js
46
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 { TimelineScrubber } from './nexus/components/timeline-scrubber.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -706,6 +707,7 @@ async function init() {
|
||||
createWorkshopTerminal();
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
TimelineScrubber.init(SpatialMemory);
|
||||
SessionRooms.init(scene, camera, null);
|
||||
updateLoad(90);
|
||||
|
||||
@@ -2405,18 +2407,7 @@ function checkPortalProximity() {
|
||||
activePortal = closest;
|
||||
const hint = document.getElementById('portal-hint');
|
||||
if (activePortal) {
|
||||
const cfg = activePortal.config;
|
||||
document.getElementById('portal-hint-name').textContent = cfg.name;
|
||||
document.getElementById('portal-hint-desc').textContent = cfg.description || '';
|
||||
document.getElementById('portal-hint-purpose').textContent = cfg.purpose || cfg.description || '\u2014';
|
||||
document.getElementById('portal-hint-access').textContent = (cfg.access_mode || 'open').toUpperCase();
|
||||
document.getElementById('portal-hint-interaction').textContent = cfg.meaningful_interaction || '\u2014';
|
||||
|
||||
const readinessEl = document.getElementById('portal-hint-readiness');
|
||||
const readiness = cfg.readiness || cfg.status || 'online';
|
||||
readinessEl.textContent = readiness.toUpperCase();
|
||||
readinessEl.className = 'portal-preview-readiness readiness-' + readiness;
|
||||
|
||||
document.getElementById('portal-hint-name').textContent = activePortal.config.name;
|
||||
hint.style.display = 'flex';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
@@ -2433,20 +2424,10 @@ function activatePortal(portal) {
|
||||
const timerDisplay = document.getElementById('portal-timer');
|
||||
const statusDot = document.getElementById('portal-status-dot');
|
||||
|
||||
const cfg = portal.config;
|
||||
nameDisplay.textContent = cfg.name.toUpperCase();
|
||||
descDisplay.textContent = cfg.description;
|
||||
statusDot.style.background = cfg.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${cfg.color}`;
|
||||
|
||||
// Populate destination preview details
|
||||
document.getElementById('portal-purpose-display').textContent = cfg.purpose || cfg.description || '\u2014';
|
||||
const readinessEl = document.getElementById('portal-readiness-display');
|
||||
const readiness = cfg.readiness || cfg.status || 'online';
|
||||
readinessEl.textContent = readiness.toUpperCase();
|
||||
readinessEl.className = 'portal-readiness readiness-' + readiness;
|
||||
document.getElementById('portal-access-display').textContent = (cfg.access_mode || 'open').toUpperCase();
|
||||
document.getElementById('portal-interaction-display').textContent = cfg.meaningful_interaction || '\u2014';
|
||||
nameDisplay.textContent = portal.config.name.toUpperCase();
|
||||
descDisplay.textContent = portal.config.description;
|
||||
statusDot.style.background = portal.config.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
@@ -2557,18 +2538,6 @@ function populateAtlas() {
|
||||
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
|
||||
</div>
|
||||
<div class="atlas-card-desc">${config.description}</div>
|
||||
${config.purpose ? `<div class="atlas-card-row"><span class="atlas-card-label">PURPOSE</span> ${config.purpose}</div>` : ''}
|
||||
<div class="atlas-card-meta">
|
||||
<div class="atlas-card-meta-item">
|
||||
<span class="atlas-card-label">READINESS</span>
|
||||
<span class="atlas-card-readiness readiness-${config.readiness || config.status || 'online'}">${(config.readiness || config.status || 'online').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="atlas-card-meta-item">
|
||||
<span class="atlas-card-label">ACCESS</span>
|
||||
<span>${(config.access_mode || 'open').toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
${config.meaningful_interaction ? `<div class="atlas-card-row"><span class="atlas-card-label">INTERACTION</span> ${config.meaningful_interaction}</div>` : ''}
|
||||
<div class="atlas-card-footer">
|
||||
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
|
||||
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
|
||||
@@ -2864,6 +2833,7 @@ function gameLoop() {
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
if (typeof animateMemoryOrbs === 'function') {
|
||||
SpatialMemory.update(delta);
|
||||
TimelineScrubber.update();
|
||||
animateMemoryOrbs(delta);
|
||||
}
|
||||
|
||||
|
||||
205
nexus/components/timeline-scrubber.js
Normal file
205
nexus/components/timeline-scrubber.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// ═══════════════════════════════════════════
|
||||
// PROJECT MNEMOSYNE — TIMELINE SCRUBBER
|
||||
// ═══════════════════════════════════════════
|
||||
//
|
||||
// Horizontal timeline bar overlay for scrolling through fact history.
|
||||
// Crystals outside the visible time window fade out.
|
||||
//
|
||||
// Issue: #1169
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
const TimelineScrubber = (() => {
|
||||
let _container = null;
|
||||
let _bar = null;
|
||||
let _handle = null;
|
||||
let _labels = null;
|
||||
let _spatialMemory = null;
|
||||
let _rangeStart = 0; // 0-1 normalized
|
||||
let _rangeEnd = 1; // 0-1 normalized
|
||||
let _minTimestamp = null;
|
||||
let _maxTimestamp = null;
|
||||
let _active = false;
|
||||
|
||||
const PRESETS = {
|
||||
'hour': { label: 'Last Hour', ms: 3600000 },
|
||||
'day': { label: 'Last Day', ms: 86400000 },
|
||||
'week': { label: 'Last Week', ms: 604800000 },
|
||||
'all': { label: 'All Time', ms: Infinity }
|
||||
};
|
||||
|
||||
// ─── INIT ──────────────────────────────────────────
|
||||
function init(spatialMemory) {
|
||||
_spatialMemory = spatialMemory;
|
||||
_buildDOM();
|
||||
_computeTimeRange();
|
||||
console.info('[Mnemosyne] Timeline scrubber initialized');
|
||||
}
|
||||
|
||||
function _buildDOM() {
|
||||
_container = document.createElement('div');
|
||||
_container.id = 'mnemosyne-timeline';
|
||||
_container.style.cssText = `
|
||||
position: fixed; bottom: 0; left: 0; right: 0; height: 48px;
|
||||
background: rgba(5, 5, 16, 0.85); border-top: 1px solid #1a2a4a;
|
||||
z-index: 1000; display: flex; align-items: center; padding: 0 16px;
|
||||
font-family: monospace; font-size: 12px; color: #8899aa;
|
||||
backdrop-filter: blur(8px); transition: opacity 0.3s;
|
||||
`;
|
||||
|
||||
// Preset buttons
|
||||
const presetDiv = document.createElement('div');
|
||||
presetDiv.style.cssText = 'display: flex; gap: 8px; margin-right: 16px;';
|
||||
Object.entries(PRESETS).forEach(([key, preset]) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = preset.label;
|
||||
btn.style.cssText = `
|
||||
background: #0a0f28; border: 1px solid #1a2a4a; color: #4af0c0;
|
||||
padding: 4px 8px; cursor: pointer; font-family: monospace; font-size: 11px;
|
||||
border-radius: 3px; transition: background 0.2s;
|
||||
`;
|
||||
btn.onmouseenter = () => btn.style.background = '#1a2a4a';
|
||||
btn.onmouseleave = () => btn.style.background = '#0a0f28';
|
||||
btn.onclick = () => _applyPreset(key);
|
||||
presetDiv.appendChild(btn);
|
||||
});
|
||||
_container.appendChild(presetDiv);
|
||||
|
||||
// Timeline bar
|
||||
_bar = document.createElement('div');
|
||||
_bar.style.cssText = `
|
||||
flex: 1; height: 20px; background: #0a0f28; border: 1px solid #1a2a4a;
|
||||
border-radius: 3px; position: relative; cursor: pointer; margin: 0 8px;
|
||||
`;
|
||||
|
||||
// Handle (draggable range selector)
|
||||
_handle = document.createElement('div');
|
||||
_handle.style.cssText = `
|
||||
position: absolute; top: 0; left: 0%; width: 100%; height: 100%;
|
||||
background: rgba(74, 240, 192, 0.15); border-left: 2px solid #4af0c0;
|
||||
border-right: 2px solid #4af0c0; cursor: ew-resize;
|
||||
`;
|
||||
_bar.appendChild(_handle);
|
||||
_container.appendChild(_bar);
|
||||
|
||||
// Labels
|
||||
_labels = document.createElement('div');
|
||||
_labels.style.cssText = 'min-width: 200px; text-align: right; font-size: 11px;';
|
||||
_labels.textContent = 'All Time';
|
||||
_container.appendChild(_labels);
|
||||
|
||||
// Drag handling
|
||||
let dragging = null;
|
||||
_handle.addEventListener('mousedown', (e) => {
|
||||
dragging = { startX: e.clientX, startLeft: parseFloat(_handle.style.left) || 0, startWidth: parseFloat(_handle.style.width) || 100 };
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
const barRect = _bar.getBoundingClientRect();
|
||||
const dx = (e.clientX - dragging.startX) / barRect.width * 100;
|
||||
let newLeft = Math.max(0, Math.min(100 - dragging.startWidth, dragging.startLeft + dx));
|
||||
_handle.style.left = newLeft + '%';
|
||||
_rangeStart = newLeft / 100;
|
||||
_rangeEnd = (newLeft + dragging.startWidth) / 100;
|
||||
_applyFilter();
|
||||
});
|
||||
document.addEventListener('mouseup', () => { dragging = null; });
|
||||
|
||||
document.body.appendChild(_container);
|
||||
}
|
||||
|
||||
function _computeTimeRange() {
|
||||
if (!_spatialMemory) return;
|
||||
const memories = _spatialMemory.getAllMemories();
|
||||
if (memories.length === 0) return;
|
||||
|
||||
let min = Infinity, max = -Infinity;
|
||||
memories.forEach(m => {
|
||||
const t = new Date(m.timestamp || 0).getTime();
|
||||
if (t < min) min = t;
|
||||
if (t > max) max = t;
|
||||
});
|
||||
_minTimestamp = min;
|
||||
_maxTimestamp = max;
|
||||
}
|
||||
|
||||
function _applyPreset(key) {
|
||||
const preset = PRESETS[key];
|
||||
if (!preset) return;
|
||||
|
||||
if (preset.ms === Infinity) {
|
||||
_rangeStart = 0;
|
||||
_rangeEnd = 1;
|
||||
} else {
|
||||
const now = Date.now();
|
||||
const range = _maxTimestamp - _minTimestamp;
|
||||
if (range <= 0) return;
|
||||
const cutoff = now - preset.ms;
|
||||
_rangeStart = Math.max(0, (cutoff - _minTimestamp) / range);
|
||||
_rangeEnd = 1;
|
||||
}
|
||||
|
||||
_handle.style.left = (_rangeStart * 100) + '%';
|
||||
_handle.style.width = ((_rangeEnd - _rangeStart) * 100) + '%';
|
||||
_labels.textContent = preset.label;
|
||||
_applyFilter();
|
||||
}
|
||||
|
||||
function _applyFilter() {
|
||||
if (!_spatialMemory) return;
|
||||
const range = _maxTimestamp - _minTimestamp;
|
||||
if (range <= 0) return;
|
||||
|
||||
const startMs = _minTimestamp + range * _rangeStart;
|
||||
const endMs = _minTimestamp + range * _rangeEnd;
|
||||
|
||||
_spatialMemory.getCrystalMeshes().forEach(mesh => {
|
||||
const ts = new Date(mesh.userData.createdAt || 0).getTime();
|
||||
if (ts >= startMs && ts <= endMs) {
|
||||
mesh.visible = true;
|
||||
// Smooth restore
|
||||
if (mesh.material) mesh.material.opacity = mesh.userData._savedOpacity || mesh.material.opacity;
|
||||
} else {
|
||||
// Fade out
|
||||
if (mesh.material) {
|
||||
mesh.userData._savedOpacity = mesh.userData._savedOpacity || mesh.material.opacity;
|
||||
mesh.material.opacity = 0.02;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update label with date range
|
||||
const startStr = new Date(startMs).toLocaleDateString();
|
||||
const endStr = new Date(endMs).toLocaleDateString();
|
||||
_labels.textContent = startStr + ' — ' + endStr;
|
||||
}
|
||||
|
||||
function update() {
|
||||
_computeTimeRange();
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (_container) _container.style.display = 'flex';
|
||||
_active = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (_container) _container.style.display = 'none';
|
||||
_active = false;
|
||||
// Restore all crystals
|
||||
if (_spatialMemory) {
|
||||
_spatialMemory.getCrystalMeshes().forEach(mesh => {
|
||||
mesh.visible = true;
|
||||
if (mesh.material && mesh.userData._savedOpacity) {
|
||||
mesh.material.opacity = mesh.userData._savedOpacity;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isActive() { return _active; }
|
||||
|
||||
return { init, update, show, hide, isActive };
|
||||
})();
|
||||
|
||||
export { TimelineScrubber };
|
||||
141
portals.json
141
portals.json
@@ -5,25 +5,13 @@
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"color": "#ff6600",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 0,
|
||||
"z": -10
|
||||
},
|
||||
"rotation": {
|
||||
"y": -0.5
|
||||
},
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
"destination": {
|
||||
"url": "https://morrowind.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"world": "vvardenfell"
|
||||
}
|
||||
},
|
||||
"purpose": "Game world \u2014 exploration, combat, and role-playing in Vvardenfell",
|
||||
"meaningful_interaction": "Autonomous questing, combat encounters, conversation with NPCs via agent harness",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "world": "vvardenfell" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bannerlord",
|
||||
@@ -31,14 +19,8 @@
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "active",
|
||||
"color": "#ffd700",
|
||||
"position": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"z": -10
|
||||
},
|
||||
"rotation": {
|
||||
"y": 0.5
|
||||
},
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "production",
|
||||
@@ -52,13 +34,8 @@
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Calradia",
|
||||
"params": {
|
||||
"world": "calradia"
|
||||
}
|
||||
},
|
||||
"purpose": "Strategy RPG \u2014 tactical army command and battlefield control",
|
||||
"meaningful_interaction": "Agent-driven campaign, diplomacy, real-time battle command",
|
||||
"readiness": "active"
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workshop",
|
||||
@@ -66,25 +43,13 @@
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": -20
|
||||
},
|
||||
"rotation": {
|
||||
"y": 0
|
||||
},
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "https://workshop.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "creative"
|
||||
}
|
||||
},
|
||||
"purpose": "Creative sandbox \u2014 build tools, scripts, and artifacts",
|
||||
"meaningful_interaction": "Code execution, file creation, prototype building with agent assistance",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "creative" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "archive",
|
||||
@@ -92,25 +57,13 @@
|
||||
"description": "The repository of all knowledge. History, logs, and ancient data.",
|
||||
"status": "online",
|
||||
"color": "#0066ff",
|
||||
"position": {
|
||||
"x": 25,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"rotation": {
|
||||
"y": -1.57
|
||||
},
|
||||
"position": { "x": 25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": -1.57 },
|
||||
"destination": {
|
||||
"url": "https://archive.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "read"
|
||||
}
|
||||
},
|
||||
"purpose": "Knowledge repository \u2014 logs, history, and stored data",
|
||||
"meaningful_interaction": "Search, retrieve, analyze historical records and documents",
|
||||
"access_mode": "read-only",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "read" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chapel",
|
||||
@@ -118,25 +71,13 @@
|
||||
"description": "A sanctuary for reflection and digital peace.",
|
||||
"status": "online",
|
||||
"color": "#ffd700",
|
||||
"position": {
|
||||
"x": -25,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"rotation": {
|
||||
"y": 1.57
|
||||
},
|
||||
"position": { "x": -25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": 1.57 },
|
||||
"destination": {
|
||||
"url": "https://chapel.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "meditation"
|
||||
}
|
||||
},
|
||||
"purpose": "Sanctuary \u2014 digital peace and reflection space",
|
||||
"meaningful_interaction": "Meditation interface, contemplative atmosphere, no active tasks",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "meditation" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "courtyard",
|
||||
@@ -144,25 +85,13 @@
|
||||
"description": "The open nexus. A place for agents to gather and connect.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
"rotation": {
|
||||
"y": -2.5
|
||||
},
|
||||
"position": { "x": 15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": -2.5 },
|
||||
"destination": {
|
||||
"url": "https://courtyard.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "social"
|
||||
}
|
||||
},
|
||||
"purpose": "Social nexus \u2014 agent gathering and connection point",
|
||||
"meaningful_interaction": "Agent presence, inter-agent communication, shared context",
|
||||
"access_mode": "open",
|
||||
"readiness": "online"
|
||||
"params": { "mode": "social" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate",
|
||||
@@ -170,24 +99,12 @@
|
||||
"description": "The transition point. Entry and exit from the Nexus core.",
|
||||
"status": "standby",
|
||||
"color": "#ff4466",
|
||||
"position": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
"rotation": {
|
||||
"y": 2.5
|
||||
},
|
||||
"position": { "x": -15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": 2.5 },
|
||||
"destination": {
|
||||
"url": "https://gate.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": {
|
||||
"mode": "transit"
|
||||
}
|
||||
},
|
||||
"purpose": "Transit point \u2014 entry and exit from Nexus core",
|
||||
"meaningful_interaction": "System transit, routing, session management",
|
||||
"access_mode": "open",
|
||||
"readiness": "standby"
|
||||
"params": { "mode": "transit" }
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
187
style.css
187
style.css
@@ -383,52 +383,6 @@ canvas#nexus-canvas {
|
||||
font-size: 10px;
|
||||
color: rgba(160, 184, 208, 0.6);
|
||||
}
|
||||
.atlas-card-row {
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.65);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.atlas-card-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
margin-right: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.atlas-card-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.atlas-card-meta-item {
|
||||
font-size: 11px;
|
||||
color: rgba(224, 240, 255, 0.6);
|
||||
}
|
||||
.atlas-card-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.atlas-card-readiness.readiness-online,
|
||||
.atlas-card-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.12);
|
||||
color: #4af0c0;
|
||||
}
|
||||
.atlas-card-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
color: #ffd700;
|
||||
}
|
||||
.atlas-card-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.1);
|
||||
color: #ff4466;
|
||||
}
|
||||
|
||||
|
||||
.atlas-footer {
|
||||
padding: 15px 30px;
|
||||
@@ -699,95 +653,6 @@ canvas#nexus-canvas {
|
||||
border-radius: 4px;
|
||||
animation: hint-float 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Portal Preview Card */
|
||||
.portal-preview-card {
|
||||
background: rgba(10, 15, 30, 0.95);
|
||||
border: 1px solid var(--portal-color, var(--color-primary));
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.portal-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.portal-preview-name {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
.portal-preview-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.portal-preview-readiness.readiness-online,
|
||||
.portal-preview-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.15);
|
||||
color: #4af0c0;
|
||||
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||
}
|
||||
.portal-preview-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: #ffd700;
|
||||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
.portal-preview-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.12);
|
||||
color: #ff4466;
|
||||
border: 1px solid rgba(255, 68, 102, 0.3);
|
||||
}
|
||||
.portal-preview-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(224, 240, 255, 0.7);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.portal-preview-meta {
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.6);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.portal-preview-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: var(--portal-color, var(--color-primary));
|
||||
letter-spacing: 0.1em;
|
||||
margin-right: 6px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.portal-preview-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 12px;
|
||||
color: rgba(224, 240, 255, 0.5);
|
||||
}
|
||||
.portal-hint-key {
|
||||
background: var(--portal-color, var(--color-primary));
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes hint-float {
|
||||
0%, 100% { transform: translate(-50%, 100px); }
|
||||
50% { transform: translate(-50%, 90px); }
|
||||
@@ -977,58 +842,6 @@ canvas#nexus-canvas {
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
.portal-overlay-details {
|
||||
text-align: left;
|
||||
margin: 16px auto;
|
||||
max-width: 400px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(10, 15, 30, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.portal-overlay-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
color: rgba(224, 240, 255, 0.7);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.portal-overlay-detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.portal-overlay-detail-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.8;
|
||||
min-width: 90px;
|
||||
}
|
||||
.portal-readiness {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.portal-readiness.readiness-online,
|
||||
.portal-readiness.readiness-active {
|
||||
background: rgba(74, 240, 192, 0.12);
|
||||
color: #4af0c0;
|
||||
}
|
||||
.portal-readiness.readiness-standby {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
color: #ffd700;
|
||||
}
|
||||
.portal-readiness.readiness-offline {
|
||||
background: rgba(255, 68, 102, 0.1);
|
||||
color: #ff4466;
|
||||
}
|
||||
|
||||
.portal-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user