north-star / commit
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); }