fuzzyworld

north-star / commit

changed the debug section a bit, added qr code folding

1e87ff1c2a06eb4a284b13b137e0c1cac3a369b7 · Pavel Safronov · 2026-03-15 22:53:44 -0700 · parents a2e2426cdc19

changed the debug section a bit, added qr code folding
 .vscode/settings.json |  5 ++-
 src/app.ts            | 38 ++++++++++++++--------
 src/index.html        |  6 +++-
 src/qr.ts             | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/style.css         | 33 +++++++++++++++++++
 5 files changed, 154 insertions(+), 16 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 97ed7dc..4927304 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -19,6 +19,9 @@
     },
     "cSpell.words": [
         "darkmode",
-        "wakelock"
+        "wakelock",
+        "Icongeek26",
+        "Flaticon",
+        "Freepik"
     ]
 }
\ No newline at end of file
diff --git a/src/app.ts b/src/app.ts
index b8196be..bbafe8f 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -11,9 +11,10 @@ import { subscribeToFriends } from './friends-sync';
 import { initCompassController } from './compass';
 import { createFriendsPanelController } from './friends-panel';
 import { renderArrows } from './renderer';
+import { bumpQrAutoCollapseCountdown } from './qr';
 import { PUBLIC_SETTINGS } from './public-settings';
 
-const debugParagraph = document.getElementById('debug');
+const debugCommit = document.getElementById('debug-commit');
 const nameForm = document.getElementById('name-form');
 const selfNameInput = document.getElementById('self-name-input');
 const selfColorInput = document.getElementById('self-color-input');
@@ -30,12 +31,6 @@ const arrow = document.getElementById('default-arrow');
 const startRoomButton = document.getElementById('start-room-button');
 const resumeRoomButton = document.getElementById('resume-room-button');
 
-const logDebug = (message) => {
-    if (message !== debugParagraph.innerHTML) {
-        debugParagraph.innerHTML = message;
-    }
-};
-
 document.head.appendChild(Object.assign(document.createElement('link'), { rel: 'icon', href: './compass.png' }));
 
 const FEET_TO_METERS = 0.3048;
@@ -67,6 +62,7 @@ let friendRef: ReturnType<typeof ref> | null = null;
 let friends: any[] = [];
 let sessionStarted = false;
 let startInProgress = false;
+let previousRemoteFriendCount = 0;
 
 initDarkMode(darkModeToggle);
 const { requestWakeLock } = createWakeLockManager();
@@ -99,12 +95,21 @@ const appTitleLink = document.getElementById('app-title-link');
 if (appTitleLink instanceof HTMLAnchorElement) {
     appTitleLink.href = isLocalCommit ? BASE_ROOM_URL : PUBLIC_SETTINGS.links.gitlabPagesBaseUrl;
 }
-
-const renderDebugInfo = () => {
-    logDebug(`Commit: ${isLocalCommit ? 'local' : `<a href="https://gitlab.com/PavelSafronov/north-star/-/commit/${GIT_COMMIT}" target="_blank">${GIT_COMMIT}</a>`} | Repo: <a href="https://gitlab.com/PavelSafronov/north-star" target="_blank">north-star</a>`);
-};
-
-renderDebugInfo();
+if (debugCommit instanceof HTMLAnchorElement) {
+    if (isLocalCommit) {
+        debugCommit.textContent = 'Local Build';
+        debugCommit.href = '#';
+        debugCommit.addEventListener('click', (event) => {
+            event.preventDefault();
+            alert(`This is a local development build, you have no reason to click this.`);
+        });
+    } else {
+        debugCommit.textContent = GIT_COMMIT;
+        debugCommit.href = `https://gitlab.com/PavelSafronov/north-star/-/commit/${GIT_COMMIT}`;
+        debugCommit.target = '_blank';
+        debugCommit.rel = 'noopener noreferrer';
+    }
+}
 
 // selfFriend is assigned below by initSelfProfile; syncSelfFriend is
 // only ever called after that point (from event handlers / intervals).
@@ -270,6 +275,12 @@ const startSession = async () => {
             roomId,
             roomKey,
             onUpdate: (updated) => {
+                const remoteFriendCount = updated.filter((friend) => friend?.id && friend.id !== selfFriend.id).length;
+                if (remoteFriendCount > previousRemoteFriendCount && inviteRoomQrList instanceof HTMLElement) {
+                    bumpQrAutoCollapseCountdown(inviteRoomQrList);
+                }
+                previousRemoteFriendCount = remoteFriendCount;
+
                 const friendsToRender = friendsController.getFriendsToRender(updated);
                 friendsController.renderFriendControls(friendsToRender);
                 requestRender();
@@ -356,7 +367,6 @@ document.addEventListener('visibilitychange', () => {
 });
 
 const render = () => {
-    renderDebugInfo();
     if (!sessionStarted) return;
 
     const now = Date.now();
diff --git a/src/index.html b/src/index.html
index 4e43167..2672e2b 100644
--- a/src/index.html
+++ b/src/index.html
@@ -59,7 +59,11 @@
         <div id="invite-room-qr-list" class="room-qr-list" aria-label="Room invite QR codes"></div>
     </div>
     <div id="debug-info">
-        <p id="debug"></p>
+        <p id="debug">
+            <a id="debug-commit" href="https://gitlab.com/PavelSafronov/north-star/-/commits/main"
+                target="_blank">Current Commit</a>
+            <a href="https://gitlab.com/PavelSafronov/north-star" target="_blank">Repo</a>
+        </p>
         <p id="attributions">
             <a href="https://www.flaticon.com/free-icons/tools-and-utensils" title="Tools and utensils icons">Tools and
                 utensils
diff --git a/src/qr.ts b/src/qr.ts
index 75028e3..0c49af2 100644
--- a/src/qr.ts
+++ b/src/qr.ts
@@ -1,5 +1,67 @@
 import QRCode from 'qrcode';
 
+const QR_AUTO_COLLAPSE_MS = 2 * 60 * 1000;
+
+type QrContainerState = {
+    deadlineMs: number;
+    rafId: number | null;
+};
+
+const qrContainerState = new WeakMap<HTMLElement, QrContainerState>();
+
+const getQrCards = (container: HTMLElement) => Array.from(container.querySelectorAll<HTMLElement>('.room-qr-card'));
+
+const setCardsCollapsed = (container: HTMLElement, collapsed: boolean) => {
+    getQrCards(container).forEach((qrCard) => {
+        qrCard.classList.toggle('is-collapsed', collapsed);
+        qrCard.setAttribute('aria-expanded', String(!collapsed));
+    });
+};
+
+const runCountdownTick = (container: HTMLElement) => {
+    const state = qrContainerState.get(container);
+    if (!state) return;
+
+    if (Date.now() >= state.deadlineMs) {
+        setCardsCollapsed(container, true);
+        state.rafId = null;
+        return;
+    }
+
+    state.rafId = requestAnimationFrame(() => {
+        runCountdownTick(container);
+    });
+};
+
+const ensureCountdownLoop = (container: HTMLElement) => {
+    const state = qrContainerState.get(container);
+    if (!state) return;
+    if (state.rafId !== null) return;
+    state.rafId = requestAnimationFrame(() => {
+        runCountdownTick(container);
+    });
+};
+
+const extendCountdown = (container: HTMLElement) => {
+    const existing = qrContainerState.get(container);
+    if (existing) {
+        existing.deadlineMs = Date.now() + QR_AUTO_COLLAPSE_MS;
+        ensureCountdownLoop(container);
+        return;
+    }
+
+    qrContainerState.set(container, {
+        deadlineMs: Date.now() + QR_AUTO_COLLAPSE_MS,
+        rafId: null
+    });
+    ensureCountdownLoop(container);
+};
+
+export const bumpQrAutoCollapseCountdown = (container: HTMLElement | null) => {
+    if (!container) return;
+    extendCountdown(container);
+};
+
 export const renderInviteQrCard = async ({
     container,
     title,
@@ -9,9 +71,14 @@ export const renderInviteQrCard = async ({
 }) => {
     if (!container) return;
 
+    extendCountdown(container);
+
     const card = document.createElement('div');
     card.className = 'room-qr-card';
     card.setAttribute('aria-label', title ? `${title} invite QR code` : 'Invite QR code');
+    card.setAttribute('role', 'button');
+    card.setAttribute('tabindex', '0');
+    card.setAttribute('aria-expanded', 'true');
 
     if (title) {
         const label = document.createElement('p');
@@ -30,6 +97,27 @@ export const renderInviteQrCard = async ({
     status.textContent = statusText;
     card.appendChild(status);
 
+    const collapsedLabel = document.createElement('p');
+    collapsedLabel.className = 'room-qr-collapsed-label';
+    collapsedLabel.textContent = 'QR Code';
+    card.appendChild(collapsedLabel);
+
+    const toggleCollapsed = () => {
+        extendCountdown(container);
+        const nextCollapsed = !card.classList.contains('is-collapsed');
+        setCardsCollapsed(container, nextCollapsed);
+    };
+
+    card.addEventListener('click', () => {
+        toggleCollapsed();
+    });
+
+    card.addEventListener('keydown', (event) => {
+        if (event.key !== 'Enter' && event.key !== ' ') return;
+        event.preventDefault();
+        toggleCollapsed();
+    });
+
     container.appendChild(card);
 
     try {
diff --git a/src/style.css b/src/style.css
index f6cc61b..d2a5b63 100644
--- a/src/style.css
+++ b/src/style.css
@@ -120,6 +120,12 @@ body.dark .app-icon {
     border-radius: 12px;
     background: rgba(255, 255, 255, 0.96);
     box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
+    cursor: pointer;
+    user-select: none;
+}
+
+.room-qr-card.is-collapsed {
+    padding: 12px 8px;
 }
 
 .room-qr-title {
@@ -138,6 +144,33 @@ body.dark .app-icon {
     image-rendering: pixelated;
 }
 
+.room-qr-collapsed-label {
+    display: none;
+    align-items: center;
+    justify-content: center;
+    width: 200px;
+    height: 56px;
+    margin: 0;
+    border: 1px solid #e2e8f0;
+    border-radius: 10px;
+    background: #ffffff;
+    color: #0f172a;
+    font-size: 0.86em;
+    font-weight: 800;
+    letter-spacing: 0.08em;
+    text-transform: uppercase;
+}
+
+.room-qr-card.is-collapsed .room-qr,
+.room-qr-card.is-collapsed .room-qr-title,
+.room-qr-card.is-collapsed .room-link-status {
+    display: none;
+}
+
+.room-qr-card.is-collapsed .room-qr-collapsed-label {
+    display: flex;
+}
+
 .room-links span {
     color: rgba(51, 65, 85, 0.8);
 }