feat: add portal atlas overlay

This commit is contained in:
Alexander Whitestone
2026-04-05 13:26:46 -04:00
parent 3c961beefd
commit 708212fa8a
5 changed files with 249 additions and 3 deletions

View File

@@ -47,9 +47,25 @@
<button id="timelapse-btn" class="chat-toggle-btn" aria-label="Start time-lapse replay" title="Time-lapse: replay today&#39;s activity in 30s [L]">
</button>
<button id="portal-atlas-toggle" class="chat-toggle-btn" aria-label="Open portal atlas" title="Open portal atlas [G]">
🜂
</button>
<audio id="ambient-sound" src="ambient.mp3" loop></audio>
</div>
<div id="portal-atlas-overlay" aria-live="polite" aria-label="Portal atlas">
<div id="portal-atlas-inner">
<div id="portal-atlas-header">
<div>
<div id="portal-atlas-title">PORTAL ATLAS</div>
<div id="portal-atlas-subtitle">Every place Timmy can drop into</div>
</div>
<button id="portal-atlas-close" aria-label="Close portal atlas"></button>
</div>
<div id="portal-atlas-list"></div>
</div>
</div>
<div id="overview-indicator">
<span>MAP VIEW</span>
<span class="overview-hint">[Tab] to exit</span>

View File

@@ -11,6 +11,60 @@ scene.add(portalGroup);
export let portals = [];
const atlasOverlay = document.getElementById('portal-atlas-overlay');
const atlasList = document.getElementById('portal-atlas-list');
const atlasToggle = document.getElementById('portal-atlas-toggle');
const atlasClose = document.getElementById('portal-atlas-close');
function renderPortalAtlas() {
if (!atlasList) return;
atlasList.innerHTML = portals.map((portal) => {
const env = portal.environment || 'unknown';
const readiness = portal.readiness_state || portal.status || 'unknown';
const access = portal.access_mode || 'unknown';
const actionLabel = portal.destination?.action_label || 'Inspect';
const url = portal.destination?.url || '#';
return `
<article class="portal-atlas-card" data-portal-id="${portal.id}">
<div class="portal-atlas-meta">
<span class="portal-atlas-name">${portal.name}</span>
<span class="portal-atlas-status status-${portal.status}">${portal.status}</span>
</div>
<p class="portal-atlas-description">${portal.description}</p>
<div class="portal-atlas-tags">
<span>${portal.portal_type || 'portal'}</span>
<span>${portal.world_category || 'world'}</span>
<span>${env}</span>
<span>${access}</span>
<span>${readiness}</span>
</div>
<div class="portal-atlas-footer">
<span class="portal-atlas-owner">Owner: ${portal.owner || 'unknown'}</span>
<a class="portal-atlas-link" href="${url}">${actionLabel}</a>
</div>
</article>
`;
}).join('');
}
function setAtlasOpen(open) {
if (!atlasOverlay) return;
atlasOverlay.classList.toggle('visible', open);
}
function initPortalAtlas() {
atlasToggle?.addEventListener('click', () => setAtlasOpen(!atlasOverlay?.classList.contains('visible')));
atlasClose?.addEventListener('click', () => setAtlasOpen(false));
document.addEventListener('keydown', (event) => {
if (event.key === 'g' || event.key === 'G') setAtlasOpen(!atlasOverlay?.classList.contains('visible'));
if (event.key === 'Escape') setAtlasOpen(false);
});
const params = new URLSearchParams(window.location.search);
if (params.get('atlas') === '1') setAtlasOpen(true); // atlas=1
}
initPortalAtlas();
function createPortals() {
const portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
@@ -54,6 +108,7 @@ export async function loadPortals() {
setPortalsRef(portals);
setPortalsRefAudio(portals);
createPortals();
renderPortalAtlas();
rebuildRuneRing();
if (_rebuildGravityZonesFn) _rebuildGravityZonesFn();
startPortalHums();

View File

@@ -3,13 +3,21 @@
"id": "morrowind",
"name": "Morrowind",
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
"status": "offline",
"status": "online",
"portal_type": "game-world",
"world_category": "rpg",
"environment": "production",
"access_mode": "public",
"readiness_state": "playable",
"telemetry_source": "hermes-harness:morrowind-mcp",
"owner": "Timmy",
"color": "#ff6600",
"position": { "x": 15, "y": 0, "z": -10 },
"rotation": { "y": -0.5 },
"destination": {
"url": "https://morrowind.timmy.foundation",
"type": "harness",
"action_label": "Enter Vvardenfell",
"params": { "world": "vvardenfell" }
}
},
@@ -17,13 +25,21 @@
"id": "bannerlord",
"name": "Bannerlord",
"description": "Calradia battle harness. Massive armies, tactical command.",
"status": "offline",
"status": "rebuilding",
"portal_type": "game-world",
"world_category": "strategy-rpg",
"environment": "staging",
"access_mode": "operator",
"readiness_state": "prototype",
"telemetry_source": "hermes-harness:bannerlord-bridge",
"owner": "Timmy",
"color": "#ffd700",
"position": { "x": -15, "y": 0, "z": -10 },
"rotation": { "y": 0.5 },
"destination": {
"url": "https://bannerlord.timmy.foundation",
"type": "harness",
"action_label": "Enter Calradia",
"params": { "world": "calradia" }
}
},
@@ -31,13 +47,21 @@
"id": "workshop",
"name": "Workshop",
"description": "The creative harness. Build, script, and manifest.",
"status": "offline",
"status": "online",
"portal_type": "operator-room",
"world_category": "workspace",
"environment": "local",
"access_mode": "local-only",
"readiness_state": "active",
"telemetry_source": "hermes-harness:workshop-state",
"owner": "Alexander",
"color": "#4af0c0",
"position": { "x": 0, "y": 0, "z": -20 },
"rotation": { "y": 0 },
"destination": {
"url": "https://workshop.timmy.foundation",
"type": "harness",
"action_label": "Enter Workshop",
"params": { "mode": "creative" }
}
}

121
style.css
View File

@@ -155,6 +155,127 @@ canvas {
color: var(--color-bg);
}
#portal-atlas-toggle {
margin-left: 8px;
}
#portal-atlas-overlay {
display: none;
position: fixed;
top: 56px;
right: 16px;
width: min(420px, calc(100vw - 32px));
max-height: calc(100vh - 88px);
overflow: auto;
background: rgba(5, 10, 24, 0.92);
border: 1px solid rgba(68, 136, 255, 0.45);
box-shadow: 0 0 24px rgba(68, 136, 255, 0.2);
z-index: 30;
border-radius: 8px;
padding: 12px;
}
#portal-atlas-overlay.visible {
display: block;
}
#portal-atlas-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
#portal-atlas-title {
font-size: 14px;
letter-spacing: 0.18em;
color: var(--color-primary);
}
#portal-atlas-subtitle {
font-size: 11px;
color: var(--color-text-muted);
margin-top: 4px;
}
#portal-atlas-close {
border: 1px solid rgba(68, 136, 255, 0.4);
background: rgba(68, 136, 255, 0.12);
color: var(--color-text);
border-radius: 4px;
width: 28px;
height: 28px;
cursor: pointer;
}
#portal-atlas-list {
display: grid;
gap: 10px;
}
.portal-atlas-card {
border: 1px solid rgba(68, 136, 255, 0.18);
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
padding: 10px;
}
.portal-atlas-meta,
.portal-atlas-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.portal-atlas-name {
font-size: 13px;
color: var(--color-text);
font-weight: bold;
}
.portal-atlas-status {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 2px 6px;
border-radius: 999px;
}
.status-online { background: rgba(42, 201, 122, 0.18); color: #7dffb2; }
.status-rebuilding { background: rgba(255, 170, 34, 0.18); color: #ffd37a; }
.status-offline { background: rgba(255, 68, 102, 0.18); color: #ff93aa; }
.portal-atlas-description {
margin: 8px 0;
color: var(--color-text);
font-size: 12px;
line-height: 1.5;
}
.portal-atlas-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.portal-atlas-tags span,
.portal-atlas-owner {
font-size: 10px;
color: var(--color-text-muted);
border: 1px solid rgba(68, 136, 255, 0.14);
padding: 2px 6px;
border-radius: 999px;
}
.portal-atlas-link {
color: var(--color-primary);
text-decoration: none;
font-size: 12px;
}
.collision-box {
outline: 2px solid red;
outline-offset: 2px;

View File

@@ -0,0 +1,30 @@
from pathlib import Path
def test_index_exposes_portal_atlas_ui_shell() -> None:
html = Path('index.html').read_text()
for token in [
'portal-atlas-toggle',
'portal-atlas-overlay',
'portal-atlas-list',
'PORTAL ATLAS',
]:
assert token in html
def test_portals_module_supports_atlas_render_and_query_open() -> None:
js = Path('modules/portals.js').read_text()
for token in [
'renderPortalAtlas',
'portal-atlas-list',
'portal-atlas-toggle',
'URLSearchParams',
'atlas=1',
'action_label',
'access_mode',
'readiness_state',
'environment',
]:
assert token in js