fuzzyworld

north-star / commit

adding hideable participant controls, added north as a participant,

3cd410275a6ec05b5bcb9049558a1514d17317f8 · Pavel Safronov · 2026-03-14 19:36:08 -0700 · parents fb3b6f329549

adding hideable participant controls, added north as a participant,
 readme.md      |  2 ++
 src/app.js     | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
 src/index.html |  2 ++
 src/style.css  | 49 +++++++++++++++++++++++++++++++++++++++----------
 4 files changed, 95 insertions(+), 13 deletions(-)

diff --git a/readme.md b/readme.md
index 09027f0..bc7696d 100644
--- a/readme.md
+++ b/readme.md
@@ -2,6 +2,8 @@
 
 ## Links
 
+Repo: https://gitlab.com/PavelSafronov/north-star
+
 GitLab: https://pavelsafronov.gitlab.io/north-star/#room=7c49d0a8
 
 Local: http://127.0.0.1:5500/src/index.html#room=7c49d0a8
diff --git a/src/app.js b/src/app.js
index 3ec084f..5952b78 100644
--- a/src/app.js
+++ b/src/app.js
@@ -16,6 +16,7 @@ const joinRoomLink = document.getElementById('join-room-link');
 const inviteRoomStatus = document.getElementById('invite-room-status');
 const createRoomLink = document.getElementById('create-room-link');
 const participantsPanel = document.getElementById('participants-panel');
+const participantsToggle = document.getElementById('participants-toggle');
 const logDebug = (message) => {
     debugParagraph.innerText = message;
 }
@@ -209,6 +210,7 @@ if (joinRoomLink) {
 const NAME_STORAGE_KEY = 'north-star-self-name';
 const COLOR_STORAGE_KEY = 'north-star-self-color';
 const PARTICIPANT_VISIBILITY_STORAGE_KEY = 'north-star-participant-visibility';
+const PARTICIPANTS_PANEL_VISIBLE_STORAGE_KEY = 'north-star-participants-panel-visible';
 const normalizeName = (value) => (value || '').trim().slice(0, 32);
 const normalizeColor = (value) => {
     const candidate = (value || '').trim().toLowerCase();
@@ -257,6 +259,37 @@ const participantVisibility = (() => {
     }
 })();
 
+const getInitialParticipantsPanelVisible = () => {
+    const saved = localStorage.getItem(PARTICIPANTS_PANEL_VISIBLE_STORAGE_KEY);
+    if (saved === 'true' || saved === 'false') {
+        return saved === 'true';
+    }
+
+    return !window.matchMedia('(max-width: 720px)').matches;
+};
+
+let participantsPanelVisible = getInitialParticipantsPanelVisible();
+const applyParticipantsPanelVisibility = () => {
+    if (participantsPanel) {
+        participantsPanel.classList.toggle('is-hidden', !participantsPanelVisible);
+    }
+    if (participantsToggle) {
+        participantsToggle.textContent = participantsPanelVisible ? 'Hide Participants' : 'Show Participants';
+        participantsToggle.setAttribute('aria-expanded', String(participantsPanelVisible));
+    }
+
+    localStorage.setItem(PARTICIPANTS_PANEL_VISIBLE_STORAGE_KEY, String(participantsPanelVisible));
+};
+
+if (participantsToggle) {
+    participantsToggle.addEventListener('click', () => {
+        participantsPanelVisible = !participantsPanelVisible;
+        applyParticipantsPanelVisibility();
+    });
+}
+
+applyParticipantsPanelVisibility();
+
 const isParticipantVisible = (participantId) => participantVisibility[participantId] !== false;
 const setParticipantVisibility = (participantId, visible) => {
     participantVisibility[participantId] = visible;
@@ -473,11 +506,13 @@ const renderParticipantControls = () => {
         mapLink.href = `https://www.google.com/maps?q=${participant.position.lat},${participant.position.lon}`;
         mapLink.target = '_blank';
         mapLink.rel = 'noopener noreferrer';
-        mapLink.textContent = 'Map';
+        mapLink.textContent = '📍';
+        mapLink.title = `Open ${participant.name} in Google Maps`;
+        mapLink.setAttribute('aria-label', `Open ${participant.name} in Google Maps`);
 
         row.appendChild(checkbox);
+        nameWrapper.appendChild(mapLink);
         row.appendChild(nameWrapper);
-        row.appendChild(mapLink);
         participantsPanel.appendChild(row);
     }
 };
@@ -485,6 +520,20 @@ const renderParticipantControls = () => {
 const getParticipantsToRender = () => {
     const participantsToRender = participants.filter(p => p.id !== selfParticipant.id);
 
+    // Add a synthetic North marker relative to the current self position for heading validation.
+    if (participantsToRender.findIndex(p => p.id === 'north-marker') === -1) {
+        participantsToRender.push({
+            id: 'north-marker',
+            name: 'North',
+            color: '#ffffff',
+            position: {
+                lat: selfPosition.lat + 0.001,
+                lon: selfPosition.lon
+            },
+            timestamp: Date.now()
+        });
+    }
+
     // Add Space Needle and Cascadia as a test participant if there are no other participants (for testing purposes, local only)
     if (participantsToRender.findIndex(p => p.id === 'space-needle') === -1) {
         participantsToRender.push({
@@ -598,7 +647,7 @@ const render = () => {
     document.body.style.backgroundColor = selfParticipant.color;
 
     // output debug info
-    logDebug(`${selfParticipant.name} | Self: ${selfParticipant.position.lat.toFixed(5)}, ${selfParticipant.position.lon.toFixed(5)} | Heading: ${selfHeading.toFixed(2)}° | Participants: ${participants.length}`);
+    logDebug(`Name: ${selfParticipant.name} | Position: ${selfParticipant.position.lat.toFixed(1)}, ${selfParticipant.position.lon.toFixed(1)} | Heading: ${selfHeading.toFixed(2)}° | Real participants: ${participants.length}`);
 };
 
 await syncSelfParticipant();
diff --git a/src/index.html b/src/index.html
index 91ed3ae..e7b3fe2 100644
--- a/src/index.html
+++ b/src/index.html
@@ -25,6 +25,8 @@
     </form>
     <div id="default-arrow" style="display: none;" class="arrow">⇡</div>
     <div id="content">
+        <button id="participants-toggle" class="participants-toggle" type="button" aria-expanded="true">Hide
+            Participants</button>
         <aside id="participants-panel" class="participants-panel" aria-label="Participants"></aside>
     </div>
     <div>
diff --git a/src/style.css b/src/style.css
index e4ba1ae..f8a3c8a 100644
--- a/src/style.css
+++ b/src/style.css
@@ -158,6 +158,20 @@ a {
     z-index: 1;
 }
 
+.participants-toggle {
+    position: absolute;
+    right: 10px;
+    top: 8px;
+    padding: 6px 10px;
+    border: 0;
+    border-radius: 8px;
+    background: rgba(15, 23, 42, 0.75);
+    color: #f8fafc;
+    font-size: 12px;
+    font-weight: 700;
+    z-index: 4;
+}
+
 .participants-panel {
     position: absolute;
     right: 10px;
@@ -173,6 +187,10 @@ a {
     z-index: 3;
 }
 
+.participants-panel.is-hidden {
+    display: none;
+}
+
 .participants-panel h3 {
     margin: 0 0 6px;
     font-size: 14px;
@@ -181,7 +199,7 @@ a {
 
 .participant-row {
     display: grid;
-    grid-template-columns: auto 1fr auto;
+    grid-template-columns: auto 1fr;
     gap: 8px;
     align-items: center;
     margin-bottom: 6px;
@@ -189,8 +207,12 @@ a {
 }
 
 .participant-name-badge {
-    display: inline-block;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 8px;
     width: 100%;
+    min-width: 0;
     padding: 5px 8px;
     border-radius: 8px;
     border: 1px solid rgba(255, 255, 255, 0.35);
@@ -199,13 +221,17 @@ a {
 }
 
 .participant-name {
+    flex: 1 1 auto;
+    min-width: 0;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
 }
 
 .participant-map-link {
-    font-size: 12px;
+    font-size: 17px;
+    line-height: 1;
+    text-decoration: none;
 }
 
 .arrow {
@@ -265,18 +291,21 @@ a {
 }
 
 @media (max-width: 720px) {
+    .participants-toggle {
+        right: 6px;
+        top: 6px;
+    }
+
     .participants-panel {
-        width: min(52vw, 220px);
+        width: min(88vw, 320px);
         right: 6px;
+        top: 38px;
+        transform: none;
+        max-height: calc(100% - 44px);
         padding: 6px;
     }
 
     .participant-row {
-        grid-template-columns: auto 1fr;
-    }
-
-    .participant-map-link {
-        grid-column: 2;
-        justify-self: start;
+        grid-template-columns: auto minmax(0, 1fr);
     }
 }
\ No newline at end of file