north-star / commit
added ordering
src/app.js | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- src/style.css | 38 +++++++++++++++++- 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/src/app.js b/src/app.js index f1b11f7..fe06c90 100644 --- a/src/app.js +++ b/src/app.js @@ -250,6 +250,9 @@ 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 PARTICIPANT_ORDER_STORAGE_KEY = 'north-star-participant-order'; +const NORTH_MARKER_ID = 'north-marker'; +const GUARDIAN_ANGEL_ID = 'test'; const normalizeName = (value) => (value || '').trim().slice(0, 32); const normalizeColor = (value) => { const candidate = (value || '').trim().toLowerCase(); @@ -297,6 +300,75 @@ const participantVisibility = (() => { return {}; } })(); +let participantOrder = (() => { + try { + const raw = localStorage.getItem(PARTICIPANT_ORDER_STORAGE_KEY); + const parsed = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? parsed.filter(id => typeof id === 'string') : []; + } catch { + return []; + } +})(); + +const saveParticipantOrder = () => { + localStorage.setItem(PARTICIPANT_ORDER_STORAGE_KEY, JSON.stringify(participantOrder)); +}; + +const getParticipantDefaultRank = (participantId) => { + if (participantId === NORTH_MARKER_ID) return 0; + if (participantId === GUARDIAN_ANGEL_ID || participantId === 'guardian-angel') return 1; + return 2; +}; + +const ensureParticipantOrder = (participantsToRender) => { + const ids = participantsToRender.map(participant => participant.id); + const idSet = new Set(ids); + const preserved = participantOrder.filter(id => idSet.has(id)); + const preservedSet = new Set(preserved); + const missing = ids.filter(id => !preservedSet.has(id)); + + missing.sort((a, b) => { + const rankDelta = getParticipantDefaultRank(a) - getParticipantDefaultRank(b); + if (rankDelta !== 0) return rankDelta; + return a.localeCompare(b); + }); + + const nextOrder = [...preserved, ...missing]; + const changed = + nextOrder.length !== participantOrder.length || + nextOrder.some((id, index) => id !== participantOrder[index]); + + if (changed) { + participantOrder = nextOrder; + saveParticipantOrder(); + } + + return participantOrder; +}; + +const sortParticipantsForDisplay = (participantsToRender) => { + const order = ensureParticipantOrder(participantsToRender); + const orderIndex = new Map(order.map((id, index) => [id, index])); + + return [...participantsToRender].sort((a, b) => { + const aIndex = orderIndex.get(a.id); + const bIndex = orderIndex.get(b.id); + return aIndex - bIndex; + }); +}; + +const moveParticipantInOrder = (participantId, direction) => { + const index = participantOrder.indexOf(participantId); + if (index < 0) return; + + const nextIndex = index + direction; + if (nextIndex < 0 || nextIndex >= participantOrder.length) return; + + [participantOrder[index], participantOrder[nextIndex]] = [participantOrder[nextIndex], participantOrder[index]]; + saveParticipantOrder(); + renderParticipantControls(); + requestRender(); +}; const getInitialParticipantsPanelVisible = () => { const saved = localStorage.getItem(PARTICIPANTS_PANEL_VISIBLE_STORAGE_KEY); @@ -526,8 +598,8 @@ const renderParticipantControls = () => { heading.textContent = 'Participants'; participantsPanel.appendChild(heading); - for (const participant of participantsToRender) { - const row = document.createElement('label'); + for (const [index, participant] of participantsToRender.entries()) { + const row = document.createElement('div'); row.className = 'participant-row'; const checkbox = document.createElement('input'); @@ -560,17 +632,47 @@ const renderParticipantControls = () => { row.appendChild(checkbox); nameWrapper.appendChild(mapLink); row.appendChild(nameWrapper); + + const orderControls = document.createElement('div'); + orderControls.className = 'participant-order-controls'; + + const moveUpButton = document.createElement('button'); + moveUpButton.type = 'button'; + moveUpButton.className = 'participant-order-button'; + moveUpButton.textContent = '↑'; + moveUpButton.title = `Move ${participant.name} up`; + moveUpButton.setAttribute('aria-label', `Move ${participant.name} up`); + moveUpButton.disabled = index === 0; + moveUpButton.addEventListener('click', () => { + moveParticipantInOrder(participant.id, -1); + }); + + const moveDownButton = document.createElement('button'); + moveDownButton.type = 'button'; + moveDownButton.className = 'participant-order-button'; + moveDownButton.textContent = '↓'; + moveDownButton.title = `Move ${participant.name} down`; + moveDownButton.setAttribute('aria-label', `Move ${participant.name} down`); + moveDownButton.disabled = index === participantsToRender.length - 1; + moveDownButton.addEventListener('click', () => { + moveParticipantInOrder(participant.id, 1); + }); + + orderControls.appendChild(moveUpButton); + orderControls.appendChild(moveDownButton); + row.appendChild(orderControls); participantsPanel.appendChild(row); } }; +let lastSAngle = null; 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) { + if (participantsToRender.findIndex(p => p.id === NORTH_MARKER_ID) === -1) { participantsToRender.push({ - id: 'north-marker', + id: NORTH_MARKER_ID, name: 'North', color: '#ffffff', position: { @@ -590,7 +692,7 @@ const getParticipantsToRender = () => { lon: selfPosition.lon + stalkerDistance * Math.sin(stalkerAngle) }; const testParticipant = { - id: 'test', + id: GUARDIAN_ANGEL_ID, name: 'Guardian Angel', color: '#ff0000', position: stalkerPosition, @@ -598,11 +700,12 @@ const getParticipantsToRender = () => { }; participantsToRender.push(testParticipant); - return participantsToRender; + return sortParticipantsForDisplay(participantsToRender); }; const render = () => { const participantsToRender = getParticipantsToRender(); + const content = document.getElementById('content'); // For each participant, calculate the angle from self to the participant, and rotate the arrow accordingly. Also set the color of the arrow to the participant's color. // Skip self participant, we don't want to render an arrow for ourselves. @@ -627,10 +730,13 @@ const render = () => { arrowLabelAnchor.appendChild(arrowLabel); newArrow.appendChild(arrowLabelAnchor); - document.getElementById('content').appendChild(newArrow); + content.appendChild(newArrow); existingArrow = newArrow; } + // Keep DOM order in sync with participant order so stacking matches the list. + content.appendChild(existingArrow); + if (!isParticipantVisible(target.id)) { existingArrow.style.display = 'none'; continue; @@ -668,7 +774,7 @@ const render = () => { document.body.style.backgroundColor = selfParticipant.color; // output debug info - logDebug(`Name: ${selfParticipant.name} | Position: ${selfParticipant.position.lat.toFixed(1)}, ${selfParticipant.position.lon.toFixed(1)} | Heading: ${selfHeading.toFixed(2)}° | Corr: ${headingCorrectionDeg.toFixed(1)}° | Real participants: ${participants.length} | Commit: ${GIT_COMMIT}`); + logDebug(`Name: ${selfParticipant.name} | Real participants: ${participants.length} | Commit: ${GIT_COMMIT}`); }; await syncSelfParticipant(); diff --git a/src/style.css b/src/style.css index 78de62c..c3cad48 100644 --- a/src/style.css +++ b/src/style.css @@ -206,7 +206,7 @@ a { .participant-row { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: auto 1fr auto; gap: 8px; align-items: center; margin-bottom: 6px; @@ -241,6 +241,29 @@ a { text-decoration: none; } +.participant-order-controls { + display: flex; + gap: 4px; +} + +.participant-order-button { + width: 24px; + height: 24px; + border: 0; + border-radius: 6px; + background: rgba(255, 255, 255, 0.2); + color: #f8fafc; + font-size: 14px; + font-weight: 700; + line-height: 1; + cursor: pointer; +} + +.participant-order-button:disabled { + opacity: 0.45; + cursor: default; +} + .arrow { position: absolute; top: 50%; @@ -335,6 +358,17 @@ a { } .participant-row { - grid-template-columns: auto minmax(0, 1fr); + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 6px; + } + + .participant-order-controls { + gap: 3px; + } + + .participant-order-button { + width: 22px; + height: 22px; + font-size: 12px; } } \ No newline at end of file