sanjang 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/app.js +897 -132
- package/dashboard/index.html +19 -6
- package/dashboard/style.css +102 -43
- package/dist/bin/sanjang.js +43 -1
- package/dist/lib/server.js +69 -2
- package/package.json +1 -1
package/dashboard/app.js
CHANGED
|
@@ -22,6 +22,29 @@ let currentWorkspace = null;
|
|
|
22
22
|
/** @type {number|null} polling interval for workspace changes */
|
|
23
23
|
let wsPollingInterval = null;
|
|
24
24
|
|
|
25
|
+
const SHERPA_QUOTES = [
|
|
26
|
+
"요구사항 또 바뀌었댜... 뭐 그러려니 하쥬",
|
|
27
|
+
"'간단한 건데~' 그 말이 제일 무섭댜",
|
|
28
|
+
"CEO가 데모 전날 피드백 줬어유. 다 뜯으래유.",
|
|
29
|
+
"이거 금방 되쥬? 네... 산도 금방 오르쥬.",
|
|
30
|
+
"스펙 확정이랬는디... 또 바뀌었댜",
|
|
31
|
+
"우선순위 1이 열두 개여유. 산봉우리가 열두 개.",
|
|
32
|
+
"디자인 안 나왔는디 개발 먼저 하래유",
|
|
33
|
+
"어제 합의한 거 오늘 모른댜. 나도 모르겠댜.",
|
|
34
|
+
"v1인디 왜 v3 기능을 넣으래유",
|
|
35
|
+
"PRD 다섯 번째 바뀌는 중이여유. 난 여그 서있쥬.",
|
|
36
|
+
"아 그거 빼자고 한 거... 다시 넣는댜. 그러쥬 뭐.",
|
|
37
|
+
"고객이 원한댜. 근디 고객이 누구여.",
|
|
38
|
+
"MVP라며유. M이 자꾸 빠져유.",
|
|
39
|
+
"이번엔 진짜 마지막 수정이래유. 네 번째 마지막.",
|
|
40
|
+
"배포하면 쉬는 거 아니었어유?",
|
|
41
|
+
"'빨리 한번 해봐유' 3주째 하는 중이쥬.",
|
|
42
|
+
"피벗이래유. 하산 아니래유. 글쎄유.",
|
|
43
|
+
"개발 부채가 배낭보다 무겁댜. 그래도 가야쥬.",
|
|
44
|
+
"야근이여유? 셰르파는 원래 이러쥬 뭐.",
|
|
45
|
+
"롤백한 거 아무도 모르쥬? 나도 모른댜.",
|
|
46
|
+
];
|
|
47
|
+
|
|
25
48
|
// ---------------------------------------------------------------------------
|
|
26
49
|
// Utility
|
|
27
50
|
// ---------------------------------------------------------------------------
|
|
@@ -1218,9 +1241,22 @@ function renderWorkspace(data) {
|
|
|
1218
1241
|
previewEl.querySelector('.ws-preview-fallback').style.display = 'flex';
|
|
1219
1242
|
});
|
|
1220
1243
|
} else {
|
|
1221
|
-
previewEl.innerHTML =
|
|
1222
|
-
|
|
1223
|
-
|
|
1244
|
+
previewEl.innerHTML = `
|
|
1245
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:16px;user-select:none;">
|
|
1246
|
+
<div style="width:4px;height:4px;image-rendering:pixelated;color:transparent;box-shadow:
|
|
1247
|
+
/* tent peak */
|
|
1248
|
+
12px 0 0 #6b7394,
|
|
1249
|
+
8px 4px 0 #6b7394, 12px 4px 0 #6b7394, 16px 4px 0 #6b7394,
|
|
1250
|
+
4px 8px 0 #6b7394, 8px 8px 0 #6b7394, 12px 8px 0 #6b7394, 16px 8px 0 #6b7394, 20px 8px 0 #6b7394,
|
|
1251
|
+
0px 12px 0 #4a5170, 4px 12px 0 #4a5170, 8px 12px 0 #4a5170, 12px 12px 0 #4a5170, 16px 12px 0 #4a5170, 20px 12px 0 #4a5170, 24px 12px 0 #4a5170,
|
|
1252
|
+
/* zzz */
|
|
1253
|
+
36px 0 0 #4a5170, 40px 4px 0 #4a5170, 36px 8px 0 #4a5170;
|
|
1254
|
+
transform:scale(2);margin-bottom:8px;
|
|
1255
|
+
"></div>
|
|
1256
|
+
<div style="color:var(--text-muted);font-size:14px;text-align:center;margin-top:24px;">
|
|
1257
|
+
캠프가 자고 있어유... zzZ
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>`;
|
|
1224
1260
|
}
|
|
1225
1261
|
|
|
1226
1262
|
// Terminal button label
|
|
@@ -1280,7 +1316,7 @@ function renderBlocks(files) {
|
|
|
1280
1316
|
}
|
|
1281
1317
|
container.innerHTML = files.map(f => {
|
|
1282
1318
|
const type = f.status === '수정' ? 'mod' : f.status === '새 파일' ? 'new' : 'del';
|
|
1283
|
-
return `<div class="ws-block ws-block-${type}" title="${f.path}"></div>`;
|
|
1319
|
+
return `<div class="ws-block ws-block-${type}" title="${escHtml(f.path)}"></div>`;
|
|
1284
1320
|
}).join('');
|
|
1285
1321
|
container.classList.toggle('ws-blocks-wobble', files.length >= 5);
|
|
1286
1322
|
}
|
|
@@ -1614,48 +1650,6 @@ window.togglePanel = function() {
|
|
|
1614
1650
|
// ---------------------------------------------------------------------------
|
|
1615
1651
|
|
|
1616
1652
|
|
|
1617
|
-
async function loadSuggestions() {
|
|
1618
|
-
const section = document.getElementById('portal-suggestions-section');
|
|
1619
|
-
const list = document.getElementById('portal-suggestions');
|
|
1620
|
-
if (!section || !list) return;
|
|
1621
|
-
|
|
1622
|
-
try {
|
|
1623
|
-
const suggestions = await api('GET', '/api/suggestions');
|
|
1624
|
-
if (!suggestions || suggestions.length === 0) {
|
|
1625
|
-
section.style.display = 'none';
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// Exclude "recent" commits (noise) and deduplicate with "이어하기"
|
|
1630
|
-
const workTitles = new Set([...playgrounds.values()].map(p => p.branch));
|
|
1631
|
-
const filtered = suggestions
|
|
1632
|
-
.filter(item => item.type !== 'recent')
|
|
1633
|
-
.filter(item => !workTitles.has(item.action))
|
|
1634
|
-
.slice(0, 5);
|
|
1635
|
-
|
|
1636
|
-
if (filtered.length === 0) {
|
|
1637
|
-
section.style.display = 'none';
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
const iconMap = { issue: '🔵', pr: '🟡' };
|
|
1642
|
-
list.innerHTML = filtered.map(item => {
|
|
1643
|
-
const icon = iconMap[item.type] || '⚪';
|
|
1644
|
-
return `
|
|
1645
|
-
<div class="portal-work-item">
|
|
1646
|
-
<div class="portal-work-left">
|
|
1647
|
-
<span class="portal-work-icon">${icon}</span>
|
|
1648
|
-
<div>
|
|
1649
|
-
<div class="portal-work-title">${escHtml(item.title)}</div>
|
|
1650
|
-
</div>
|
|
1651
|
-
</div>
|
|
1652
|
-
</div>`;
|
|
1653
|
-
}).join('');
|
|
1654
|
-
section.style.display = '';
|
|
1655
|
-
} catch {
|
|
1656
|
-
section.style.display = 'none';
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
1653
|
async function loadPortal() {
|
|
1660
1654
|
const workList = document.getElementById('portal-work');
|
|
1661
1655
|
if (!workList) return;
|
|
@@ -1756,111 +1750,882 @@ window.autoFix = async function autoFix(name) {
|
|
|
1756
1750
|
};
|
|
1757
1751
|
|
|
1758
1752
|
// ---------------------------------------------------------------------------
|
|
1759
|
-
//
|
|
1753
|
+
// Sherpa Guide Mode (replaces overlay onboarding)
|
|
1760
1754
|
// ---------------------------------------------------------------------------
|
|
1761
1755
|
|
|
1762
1756
|
const ONBOARDING_KEY = 'sanjang-onboarded';
|
|
1763
1757
|
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1758
|
+
const SHERPA_GUIDE = [
|
|
1759
|
+
"여기에 하고 싶은 거 적으면 되유. AI가 캠프 만들어줄겨.",
|
|
1760
|
+
"캠프 들어가면 프리뷰 전체화면으로 보여유. 편하쥬?",
|
|
1761
|
+
"세이브는 게임 세이브처럼 저장이여유. 💾 버튼 누르면 되유.",
|
|
1762
|
+
"팀에 보내기 누르면 PR 만들어주유. 셰르파가 다 해줄겨.",
|
|
1763
|
+
"그럼 이제 시작해봐유. 화이팅이여유~ 🏔️",
|
|
1764
|
+
];
|
|
1765
|
+
|
|
1766
|
+
// ---------------------------------------------------------------------------
|
|
1767
|
+
// Sherpa Mode System (guide ↔ grumpy toggle)
|
|
1768
|
+
// ---------------------------------------------------------------------------
|
|
1769
|
+
|
|
1770
|
+
let sherpaInterval = null;
|
|
1771
|
+
let sherpaMode = 'grumpy'; // 'guide' or 'grumpy'
|
|
1772
|
+
let sherpaQueue = [];
|
|
1773
|
+
let sherpaIdx = 0;
|
|
1774
|
+
|
|
1775
|
+
function shuffleArray(arr) {
|
|
1776
|
+
const a = [...arr];
|
|
1777
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1778
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1779
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
1780
|
+
}
|
|
1781
|
+
return a;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function setSherpaMode(mode) {
|
|
1785
|
+
sherpaMode = mode;
|
|
1786
|
+
sherpaIdx = 0;
|
|
1787
|
+
sherpaQueue = mode === 'guide' ? [...SHERPA_GUIDE] : shuffleArray(SHERPA_QUOTES);
|
|
1788
|
+
|
|
1789
|
+
const el = document.getElementById('sherpa-quote');
|
|
1790
|
+
const speech = document.getElementById('sherpa-speech');
|
|
1791
|
+
if (!el || !speech) return;
|
|
1792
|
+
|
|
1793
|
+
// Visual mode indicator
|
|
1794
|
+
speech.classList.toggle('guide-mode', mode === 'guide');
|
|
1795
|
+
|
|
1796
|
+
// Fade transition to first message
|
|
1797
|
+
el.style.opacity = '0';
|
|
1798
|
+
setTimeout(() => {
|
|
1799
|
+
el.textContent = sherpaQueue[0];
|
|
1800
|
+
el.style.opacity = '1';
|
|
1801
|
+
}, 300);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function advanceSherpa() {
|
|
1805
|
+
const el = document.getElementById('sherpa-quote');
|
|
1806
|
+
if (!el) return;
|
|
1807
|
+
|
|
1808
|
+
el.style.opacity = '0';
|
|
1809
|
+
setTimeout(() => {
|
|
1810
|
+
sherpaIdx++;
|
|
1811
|
+
if (sherpaIdx >= sherpaQueue.length) {
|
|
1812
|
+
if (sherpaMode === 'guide') {
|
|
1813
|
+
// Guide done → switch to grumpy
|
|
1814
|
+
localStorage.setItem(ONBOARDING_KEY, '1');
|
|
1815
|
+
setSherpaMode('grumpy');
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
// Reshuffle grumpy quotes
|
|
1819
|
+
sherpaQueue = shuffleArray(SHERPA_QUOTES);
|
|
1820
|
+
sherpaIdx = 0;
|
|
1821
|
+
}
|
|
1822
|
+
el.textContent = sherpaQueue[sherpaIdx];
|
|
1823
|
+
el.style.opacity = '1';
|
|
1824
|
+
}, 500);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// ---------------------------------------------------------------------------
|
|
1828
|
+
// Basecamp Scene — Time-based Himalaya SVG
|
|
1829
|
+
// ---------------------------------------------------------------------------
|
|
1830
|
+
|
|
1831
|
+
const SCENE_THEMES = {
|
|
1832
|
+
dawn: {
|
|
1833
|
+
skyGradient: [['0%','#050810'],['60%','#0a1028'],['100%','#141830']],
|
|
1834
|
+
farRange: '#1a2040',
|
|
1835
|
+
midRange: '#141a30',
|
|
1836
|
+
ground: '#12151e',
|
|
1837
|
+
snowColor: 'rgba(200,215,240,0.35)',
|
|
1838
|
+
snowHighlight: 'rgba(220,230,250,0.4)',
|
|
1770
1839
|
},
|
|
1771
|
-
{
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1840
|
+
morning: {
|
|
1841
|
+
skyGradient: [['0%','#1a2540'],['40%','#2d3a5c'],['70%','#5c4a6e'],['100%','#c4785a']],
|
|
1842
|
+
farRange: '#2a3058',
|
|
1843
|
+
midRange: '#1e2444',
|
|
1844
|
+
ground: '#12151e',
|
|
1845
|
+
snowColor: 'rgba(255,220,180,0.45)',
|
|
1846
|
+
snowHighlight: 'rgba(255,200,150,0.55)',
|
|
1777
1847
|
},
|
|
1778
|
-
{
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1848
|
+
day: {
|
|
1849
|
+
skyGradient: [['0%','#1a3050'],['50%','#2a4a6a'],['100%','#3a5a7a']],
|
|
1850
|
+
farRange: '#253a58',
|
|
1851
|
+
midRange: '#1a2e48',
|
|
1852
|
+
ground: '#12151e',
|
|
1853
|
+
snowColor: 'rgba(255,255,255,0.5)',
|
|
1854
|
+
snowHighlight: 'rgba(255,255,255,0.6)',
|
|
1784
1855
|
},
|
|
1785
|
-
|
|
1856
|
+
evening: {
|
|
1857
|
+
skyGradient: [['0%','#060810'],['40%','#0e1530'],['75%','#1a1535'],['100%','#12151e']],
|
|
1858
|
+
farRange: '#182040',
|
|
1859
|
+
midRange: '#121830',
|
|
1860
|
+
ground: '#12151e',
|
|
1861
|
+
snowColor: 'rgba(200,180,230,0.35)',
|
|
1862
|
+
snowHighlight: 'rgba(220,200,240,0.4)',
|
|
1863
|
+
},
|
|
1864
|
+
};
|
|
1786
1865
|
|
|
1787
|
-
function
|
|
1788
|
-
|
|
1789
|
-
|
|
1866
|
+
function renderBasecampScene() {
|
|
1867
|
+
const container = document.getElementById('bc-scene-container');
|
|
1868
|
+
if (!container) return;
|
|
1790
1869
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1870
|
+
const P = 4; // pixel size (4px grid)
|
|
1871
|
+
const hour = new Date().getHours();
|
|
1872
|
+
let period;
|
|
1873
|
+
if (hour >= 0 && hour < 6) period = 'dawn';
|
|
1874
|
+
else if (hour >= 6 && hour < 12) period = 'morning';
|
|
1875
|
+
else if (hour >= 12 && hour < 18) period = 'day';
|
|
1876
|
+
else period = 'evening';
|
|
1877
|
+
|
|
1878
|
+
const theme = SCENE_THEMES[period];
|
|
1879
|
+
|
|
1880
|
+
// Build gradient stops
|
|
1881
|
+
const stops = theme.skyGradient.map(([offset, color]) =>
|
|
1882
|
+
`<stop offset="${offset}" stop-color="${color}"/>`
|
|
1883
|
+
).join('');
|
|
1884
|
+
|
|
1885
|
+
// --- Stars ---
|
|
1886
|
+
let starsHtml = '';
|
|
1887
|
+
if (period === 'dawn') {
|
|
1888
|
+
const starPositions = [
|
|
1889
|
+
[45,18],[120,30],[200,12],[280,25],[360,8],[440,22],[520,15],[590,28],[150,40],[400,35],[60,42],[500,38],[330,10]
|
|
1890
|
+
];
|
|
1891
|
+
starsHtml = starPositions.map(([x,y]) =>
|
|
1892
|
+
`<rect x="${x}" y="${y}" width="2" height="2" fill="#fff" opacity="${0.4 + Math.random()*0.4}"><animate attributeName="opacity" values="${0.3};${0.8};${0.3}" dur="${1.5 + Math.random()*2}s" repeatCount="indefinite"/></rect>`
|
|
1893
|
+
).join('');
|
|
1894
|
+
// Crescent moon (pixel)
|
|
1895
|
+
starsHtml += `
|
|
1896
|
+
<rect x="576" y="20" width="${P}" height="${P}" fill="#c8cee6" opacity="0.3"/>
|
|
1897
|
+
<rect x="580" y="16" width="${P}" height="${P}" fill="#c8cee6" opacity="0.3"/>
|
|
1898
|
+
<rect x="580" y="20" width="${P}" height="${P}" fill="#c8cee6" opacity="0.25"/>
|
|
1899
|
+
<rect x="584" y="16" width="${P}" height="${P}" fill="#c8cee6" opacity="0.3"/>
|
|
1900
|
+
<rect x="584" y="20" width="${P}" height="${P}" fill="#c8cee6" opacity="0.15"/>
|
|
1901
|
+
<rect x="588" y="20" width="${P}" height="${P}" fill="#c8cee6" opacity="0.3"/>
|
|
1902
|
+
<rect x="580" y="24" width="${P}" height="${P}" fill="#c8cee6" opacity="0.25"/>
|
|
1903
|
+
<rect x="584" y="24" width="${P}" height="${P}" fill="#c8cee6" opacity="0.3"/>
|
|
1904
|
+
`;
|
|
1905
|
+
} else if (period === 'morning') {
|
|
1906
|
+
const starPositions = [[120,20],[400,15],[550,30]];
|
|
1907
|
+
starsHtml = starPositions.map(([x,y]) =>
|
|
1908
|
+
`<rect x="${x}" y="${y}" width="2" height="2" fill="#fff" opacity="0.2"/>`
|
|
1909
|
+
).join('');
|
|
1910
|
+
} else if (period === 'day') {
|
|
1911
|
+
// Pixel clouds (4px grid)
|
|
1912
|
+
starsHtml = `
|
|
1913
|
+
<g opacity="0.12">
|
|
1914
|
+
<rect x="104" y="28" width="8" height="4" fill="#fff"/>
|
|
1915
|
+
<rect x="100" y="32" width="16" height="4" fill="#fff"/>
|
|
1916
|
+
<rect x="108" y="36" width="4" height="4" fill="#fff"/>
|
|
1917
|
+
<rect x="436" y="24" width="12" height="4" fill="#fff"/>
|
|
1918
|
+
<rect x="432" y="28" width="20" height="4" fill="#fff"/>
|
|
1919
|
+
<rect x="440" y="32" width="8" height="4" fill="#fff"/>
|
|
1920
|
+
</g>
|
|
1921
|
+
`;
|
|
1922
|
+
} else {
|
|
1923
|
+
const starPositions = [
|
|
1924
|
+
[80,15],[160,35],[250,10],[340,28],[430,18],[510,32],[590,12],[140,45],[380,40],[620,25]
|
|
1925
|
+
];
|
|
1926
|
+
starsHtml = starPositions.map(([x,y]) =>
|
|
1927
|
+
`<rect x="${x}" y="${y}" width="2" height="2" fill="#fff" opacity="${0.3 + Math.random()*0.5}"><animate attributeName="opacity" values="${0.2};${0.7};${0.2}" dur="${2 + Math.random()*2}s" repeatCount="indefinite"/></rect>`
|
|
1928
|
+
).join('');
|
|
1929
|
+
}
|
|
1794
1930
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1931
|
+
// --- Mountains ---
|
|
1932
|
+
const farRangePoly = `0,220 0,150 20,150 20,140 40,140 40,125 55,125 55,115 70,115 70,105 85,105 85,95
|
|
1933
|
+
95,95 95,85 105,85 105,80 115,80 115,85 125,85 125,95 135,95 135,105
|
|
1934
|
+
150,105 150,115 165,115 165,125 180,125 180,135 200,135 200,145 220,145
|
|
1935
|
+
220,135 235,135 235,120 250,120 250,105 260,105 260,90 270,90 270,78 280,78
|
|
1936
|
+
280,70 288,70 288,62 295,62 295,56 302,56 302,50 308,50 308,45 314,45
|
|
1937
|
+
314,50 320,50 320,56 326,56 326,65 335,65 335,78 345,78 345,90
|
|
1938
|
+
355,90 355,105 370,105 370,120 385,120 385,135 400,135
|
|
1939
|
+
400,125 415,125 415,110 425,110 425,98 435,98 435,88 445,88 445,78
|
|
1940
|
+
450,78 450,70 456,70 456,64 462,64 462,58 466,58 466,54 470,54
|
|
1941
|
+
470,50 474,50 474,46 478,46 478,50 482,50 482,56 486,56
|
|
1942
|
+
486,64 492,64 492,72 498,72 498,82 508,82 508,95 518,95
|
|
1943
|
+
518,108 530,108 530,120 545,120 545,135 560,135
|
|
1944
|
+
560,125 570,125 570,112 580,112 580,100 590,100 590,88 598,88 598,78
|
|
1945
|
+
605,78 605,70 612,70 612,76 618,76 618,85 625,85 625,95
|
|
1946
|
+
635,95 635,108 645,108 645,120 658,120 658,135 680,135 680,220`;
|
|
1947
|
+
|
|
1948
|
+
const midRangePoly = `0,220 0,170 30,170 30,160 60,160 60,152 80,152 80,160 110,160 110,168
|
|
1949
|
+
140,168 140,158 160,158 160,148 175,148 175,140 188,140 188,135 198,135 198,140
|
|
1950
|
+
210,140 210,150 230,150 230,162 260,162 260,155 280,155 280,145 295,145 295,138
|
|
1951
|
+
310,138 310,145 330,145 330,155 350,155 350,165
|
|
1952
|
+
380,165 380,155 400,155 400,148 415,148 415,140 425,140 425,135 432,135 432,140
|
|
1953
|
+
440,140 440,150 460,150 460,160 480,160 480,168
|
|
1954
|
+
510,168 510,158 530,158 530,148 545,148 545,142 555,142 555,148
|
|
1955
|
+
565,148 565,158 585,158 585,165 610,165 610,158 630,158 630,165 660,165 660,170 680,170 680,220`;
|
|
1956
|
+
|
|
1957
|
+
// --- Snow caps (4px grid) ---
|
|
1958
|
+
const snowCaps = `
|
|
1959
|
+
<!-- Main peak snow -->
|
|
1960
|
+
<rect x="308" y="45" width="${P*2}" height="${P}" fill="${theme.snowHighlight}"/>
|
|
1961
|
+
<rect x="304" y="49" width="${P*4}" height="${P}" fill="${theme.snowColor}"/>
|
|
1962
|
+
<!-- Second peak snow -->
|
|
1963
|
+
<rect x="474" y="46" width="${P*2}" height="${P}" fill="${theme.snowHighlight}"/>
|
|
1964
|
+
<rect x="470" y="50" width="${P*4}" height="${P}" fill="${theme.snowColor}"/>
|
|
1965
|
+
<!-- Smaller peaks -->
|
|
1966
|
+
<rect x="104" y="80" width="${P*2}" height="${P}" fill="${theme.snowHighlight}"/>
|
|
1967
|
+
<rect x="604" y="70" width="${P*2}" height="${P}" fill="${theme.snowHighlight}"/>
|
|
1968
|
+
<!-- Mid-range snow -->
|
|
1969
|
+
<rect x="188" y="135" width="${P*2}" height="${P}" fill="${theme.snowColor}" opacity="0.5"/>
|
|
1970
|
+
<rect x="424" y="135" width="${P*2}" height="${P}" fill="${theme.snowColor}" opacity="0.5"/>
|
|
1971
|
+
<rect x="544" y="142" width="${P*2}" height="${P}" fill="${theme.snowColor}" opacity="0.5"/>
|
|
1972
|
+
`;
|
|
1973
|
+
|
|
1974
|
+
// --- Ground + texture ---
|
|
1975
|
+
const groundHtml = `
|
|
1976
|
+
<rect x="0" y="185" width="680" height="35" fill="${theme.ground}"/>
|
|
1977
|
+
<rect x="50" y="188" width="8" height="3" fill="#1a1d28" opacity="0.5"/>
|
|
1978
|
+
<rect x="200" y="190" width="6" height="2" fill="#1a1d28" opacity="0.4"/>
|
|
1979
|
+
<rect x="350" y="187" width="10" height="3" fill="#1a1d28" opacity="0.5"/>
|
|
1980
|
+
<rect x="500" y="191" width="7" height="2" fill="#1a1d28" opacity="0.4"/>
|
|
1981
|
+
<rect x="620" y="189" width="5" height="3" fill="#1a1d28" opacity="0.5"/>
|
|
1982
|
+
`;
|
|
1983
|
+
|
|
1984
|
+
// --- Shared basecamp elements (all 4px grid pixel art) ---
|
|
1985
|
+
|
|
1986
|
+
const tents = `
|
|
1987
|
+
<!-- Yellow expedition tent (pixel pyramid) -->
|
|
1988
|
+
<g>
|
|
1989
|
+
<rect x="88" y="172" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1990
|
+
<rect x="84" y="176" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1991
|
+
<rect x="88" y="176" width="${P}" height="${P}" fill="#a08818"/>
|
|
1992
|
+
<rect x="92" y="176" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1993
|
+
<rect x="80" y="180" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1994
|
+
<rect x="84" y="180" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1995
|
+
<rect x="88" y="180" width="${P}" height="${P}" fill="#2c2210"/>
|
|
1996
|
+
<rect x="92" y="180" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1997
|
+
<rect x="96" y="180" width="${P}" height="${P}" fill="#c8a820"/>
|
|
1998
|
+
</g>
|
|
1999
|
+
<!-- Blue dome tent (pixel) -->
|
|
2000
|
+
<g>
|
|
2001
|
+
<rect x="520" y="176" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2002
|
+
<rect x="524" y="176" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2003
|
+
<rect x="516" y="180" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2004
|
+
<rect x="520" y="180" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2005
|
+
<rect x="524" y="180" width="${P}" height="${P}" fill="#1a2040"/>
|
|
2006
|
+
<rect x="528" y="180" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2007
|
+
<rect x="532" y="180" width="${P}" height="${P}" fill="#2855a0"/>
|
|
2008
|
+
</g>
|
|
2009
|
+
<!-- Green small tent (pixel) -->
|
|
2010
|
+
<g>
|
|
2011
|
+
<rect x="580" y="176" width="${P}" height="${P}" fill="#1e8040"/>
|
|
2012
|
+
<rect x="576" y="180" width="${P}" height="${P}" fill="#1e8040"/>
|
|
2013
|
+
<rect x="580" y="180" width="${P}" height="${P}" fill="#166030"/>
|
|
2014
|
+
<rect x="584" y="180" width="${P}" height="${P}" fill="#1e8040"/>
|
|
2015
|
+
</g>
|
|
2016
|
+
<!-- Red expedition tent (pixel) -->
|
|
2017
|
+
<g>
|
|
2018
|
+
<rect x="448" y="172" width="${P}" height="${P}" fill="#b83030"/>
|
|
2019
|
+
<rect x="444" y="176" width="${P}" height="${P}" fill="#b83030"/>
|
|
2020
|
+
<rect x="448" y="176" width="${P}" height="${P}" fill="#902020"/>
|
|
2021
|
+
<rect x="452" y="176" width="${P}" height="${P}" fill="#b83030"/>
|
|
2022
|
+
<rect x="440" y="180" width="${P}" height="${P}" fill="#b83030"/>
|
|
2023
|
+
<rect x="444" y="180" width="${P}" height="${P}" fill="#b83030"/>
|
|
2024
|
+
<rect x="448" y="180" width="${P}" height="${P}" fill="#401010"/>
|
|
2025
|
+
<rect x="452" y="180" width="${P}" height="${P}" fill="#b83030"/>
|
|
2026
|
+
<rect x="456" y="180" width="${P}" height="${P}" fill="#b83030"/>
|
|
2027
|
+
</g>
|
|
2028
|
+
`;
|
|
2029
|
+
|
|
2030
|
+
const flagColors = ['#e74c3c','#f39c12','#fff','#2ecc71','#3498db'];
|
|
2031
|
+
const prayerFlags1 = flagColors.map((c, i) =>
|
|
2032
|
+
`<rect x="${156 + i*8}" y="172" width="${P}" height="${P}" fill="${c}" opacity="0.7"/>`
|
|
2033
|
+
).join('') + flagColors.map((c, i) =>
|
|
2034
|
+
`<rect x="${156 + i*8}" y="168" width="${P}" height="1" fill="#4a5170" opacity="0.5"/>`
|
|
2035
|
+
).join('');
|
|
2036
|
+
|
|
2037
|
+
const prayerFlags2 = flagColors.map((c, i) =>
|
|
2038
|
+
`<rect x="${420 + i*8}" y="168" width="${P}" height="${P}" fill="${c}" opacity="0.6"/>`
|
|
2039
|
+
).join('') + flagColors.map((c, i) =>
|
|
2040
|
+
`<rect x="${420 + i*8}" y="164" width="${P}" height="1" fill="#4a5170" opacity="0.4"/>`
|
|
2041
|
+
).join('');
|
|
2042
|
+
|
|
2043
|
+
const supplies = `
|
|
2044
|
+
<!-- Supply crates (pixel) -->
|
|
2045
|
+
<rect x="128" y="180" width="${P*2}" height="${P}" fill="#6b4a28"/>
|
|
2046
|
+
<rect x="128" y="180" width="${P*2}" height="1" fill="#8b6a38"/>
|
|
2047
|
+
<rect x="128" y="176" width="${P*2}" height="${P}" fill="#5a3a20"/>
|
|
2048
|
+
<rect x="128" y="176" width="${P*2}" height="1" fill="#7a5a30"/>
|
|
2049
|
+
<rect x="136" y="180" width="${P}" height="${P}" fill="#5a3a20"/>
|
|
2050
|
+
<!-- Oxygen tanks (pixel) -->
|
|
2051
|
+
<rect x="472" y="180" width="${P}" height="${P}" fill="#4a6a8a"/>
|
|
2052
|
+
<rect x="472" y="176" width="${P}" height="${P}" fill="#6a8aaa"/>
|
|
2053
|
+
<rect x="476" y="180" width="${P}" height="${P}" fill="#4a6a8a"/>
|
|
2054
|
+
<rect x="476" y="176" width="${P}" height="${P}" fill="#6a8aaa"/>
|
|
2055
|
+
<!-- Signpost (pixel) -->
|
|
2056
|
+
<rect x="300" y="172" width="${P}" height="${P*3}" fill="#5a4a30"/>
|
|
2057
|
+
<rect x="296" y="172" width="${P*3}" height="${P}" fill="#6b5a38"/>
|
|
2058
|
+
<rect x="308" y="173" width="${P}" height="2" fill="#6b5a38"/>
|
|
2059
|
+
<!-- Rope coil (pixel) -->
|
|
2060
|
+
<rect x="600" y="176" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2061
|
+
<rect x="604" y="176" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2062
|
+
<rect x="596" y="180" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2063
|
+
<rect x="608" y="180" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2064
|
+
<rect x="600" y="184" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2065
|
+
<rect x="604" y="184" width="${P}" height="${P}" fill="#8b7a50"/>
|
|
2066
|
+
<!-- Ice axe (pixel) -->
|
|
2067
|
+
<rect x="144" y="168" width="${P}" height="${P}" fill="#8090b0"/>
|
|
2068
|
+
<rect x="144" y="172" width="${P}" height="${P}" fill="#6b5a38"/>
|
|
2069
|
+
<rect x="144" y="176" width="${P}" height="${P}" fill="#6b5a38"/>
|
|
2070
|
+
<rect x="140" y="168" width="${P}" height="${P}" fill="#aab0c0"/>
|
|
2071
|
+
`;
|
|
2072
|
+
|
|
2073
|
+
// --- Stone ring around campfire (4px grid) ---
|
|
2074
|
+
const stoneRing = `
|
|
2075
|
+
<rect x="320" y="184" width="${P}" height="${P}" fill="#3a3a40"/>
|
|
2076
|
+
<rect x="324" y="184" width="${P}" height="${P}" fill="#454550"/>
|
|
2077
|
+
<rect x="336" y="184" width="${P}" height="${P}" fill="#454550"/>
|
|
2078
|
+
<rect x="340" y="184" width="${P}" height="${P}" fill="#3a3a40"/>
|
|
2079
|
+
<rect x="318" y="180" width="${P}" height="${P}" fill="#454550"/>
|
|
2080
|
+
<rect x="342" y="180" width="${P}" height="${P}" fill="#3a3a40"/>
|
|
2081
|
+
`;
|
|
2082
|
+
|
|
2083
|
+
// --- Campfire (period-specific) ---
|
|
2084
|
+
let campfireHtml = '';
|
|
2085
|
+
if (period === 'dawn') {
|
|
2086
|
+
// Dim embers (pixel)
|
|
2087
|
+
campfireHtml = `
|
|
2088
|
+
${stoneRing}
|
|
2089
|
+
<rect x="328" y="180" width="${P}" height="${P}" fill="#8b2200" opacity="0.5"/>
|
|
2090
|
+
<rect x="332" y="180" width="${P}" height="${P}" fill="#a03000" opacity="0.4"/>
|
|
2091
|
+
`;
|
|
2092
|
+
} else if (period === 'morning') {
|
|
2093
|
+
// Smoke only (pixel)
|
|
2094
|
+
campfireHtml = `
|
|
2095
|
+
${stoneRing}
|
|
2096
|
+
<rect x="328" y="180" width="${P}" height="${P}" fill="#5a4a30"/>
|
|
2097
|
+
<rect x="332" y="180" width="${P}" height="${P}" fill="#5a4a30"/>
|
|
2098
|
+
<rect x="328" y="176" width="${P}" height="${P}" fill="#4a5170" opacity="0.2"/>
|
|
2099
|
+
<rect x="332" y="172" width="${P}" height="${P}" fill="#4a5170" opacity="0.15"/>
|
|
2100
|
+
<rect x="328" y="168" width="${P}" height="${P}" fill="#4a5170" opacity="0.1"/>
|
|
2101
|
+
`;
|
|
2102
|
+
} else if (period === 'day') {
|
|
2103
|
+
// No fire, just logs (pixel)
|
|
2104
|
+
campfireHtml = `
|
|
2105
|
+
${stoneRing}
|
|
2106
|
+
<rect x="324" y="180" width="${P*3}" height="${P}" fill="#5a4a30"/>
|
|
2107
|
+
<rect x="328" y="176" width="${P*2}" height="${P}" fill="#6b4a28"/>
|
|
2108
|
+
`;
|
|
2109
|
+
} else {
|
|
2110
|
+
// Full fire with glow (all 4px pixel)
|
|
2111
|
+
campfireHtml = `
|
|
2112
|
+
${stoneRing}
|
|
2113
|
+
<!-- Glow (rect-based) -->
|
|
2114
|
+
<rect x="316" y="168" width="${P*7}" height="${P*4}" fill="#ff8c32" opacity="0.04"/>
|
|
2115
|
+
<rect x="320" y="172" width="${P*5}" height="${P*3}" fill="#ff6600" opacity="0.06"/>
|
|
2116
|
+
<!-- Logs -->
|
|
2117
|
+
<rect x="324" y="180" width="${P*3}" height="${P}" fill="#5a3a20"/>
|
|
2118
|
+
<rect x="326" y="180" width="${P*3}" height="${P}" fill="#6b4a28"/>
|
|
2119
|
+
<!-- Flames (pixel, animated with steps) -->
|
|
2120
|
+
<rect x="328" y="176" width="${P}" height="${P}" fill="#ff6600">
|
|
2121
|
+
<animate attributeName="opacity" values="1;0.6;1" dur="0.4s" steps="2" repeatCount="indefinite"/>
|
|
2122
|
+
</rect>
|
|
2123
|
+
<rect x="332" y="172" width="${P}" height="${P}" fill="#ffcc00">
|
|
2124
|
+
<animate attributeName="opacity" values="0.8;1;0.6" dur="0.5s" steps="2" repeatCount="indefinite"/>
|
|
2125
|
+
</rect>
|
|
2126
|
+
<rect x="332" y="176" width="${P}" height="${P}" fill="#ff8800"/>
|
|
2127
|
+
<rect x="328" y="172" width="${P}" height="${P}" fill="#ff9900" opacity="0.7">
|
|
2128
|
+
<animate attributeName="opacity" values="0.7;0.3;0.7" dur="0.35s" steps="2" repeatCount="indefinite"/>
|
|
2129
|
+
</rect>
|
|
2130
|
+
<rect x="336" y="176" width="${P}" height="${P}" fill="#ff6600" opacity="0.6"/>
|
|
2131
|
+
<rect x="330" y="168" width="${P}" height="${P}" fill="#ff6600" opacity="0.4">
|
|
2132
|
+
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="0.6s" steps="2" repeatCount="indefinite"/>
|
|
2133
|
+
</rect>
|
|
2134
|
+
<!-- Smoke (pixel) -->
|
|
2135
|
+
<rect x="332" y="164" width="${P}" height="${P}" fill="#4a5170" opacity="0.15">
|
|
2136
|
+
<animate attributeName="opacity" values="0.15;0.05;0.15" dur="2s" repeatCount="indefinite"/>
|
|
2137
|
+
</rect>
|
|
2138
|
+
<rect x="328" y="160" width="${P}" height="${P}" fill="#4a5170" opacity="0.08"/>
|
|
2139
|
+
`;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// --- Sherpa (period-specific) ---
|
|
2143
|
+
let sherpaHtml = '';
|
|
2144
|
+
if (period === 'dawn') {
|
|
2145
|
+
// Sleeping horizontally
|
|
2146
|
+
sherpaHtml = `
|
|
2147
|
+
<g transform="translate(345, 178)">
|
|
2148
|
+
<!-- Body lying flat -->
|
|
2149
|
+
<rect x="0" y="0" width="4" height="4" fill="#3498db"/>
|
|
2150
|
+
<rect x="4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2151
|
+
<rect x="8" y="0" width="4" height="4" fill="#3498db"/>
|
|
2152
|
+
<rect x="12" y="0" width="4" height="4" fill="#2c3e50"/>
|
|
2153
|
+
<rect x="16" y="0" width="4" height="4" fill="#2c3e50"/>
|
|
2154
|
+
<!-- Head -->
|
|
2155
|
+
<rect x="-4" y="-1" width="4" height="4" fill="#f5c6a0"/>
|
|
2156
|
+
<!-- Hat flat -->
|
|
2157
|
+
<rect x="-8" y="-2" width="4" height="4" fill="#e74c3c"/>
|
|
2158
|
+
<!-- Zzz (pixel) -->
|
|
2159
|
+
<rect x="8" y="-8" width="${P}" height="${P}" fill="#8888aa" opacity="0.5"/>
|
|
2160
|
+
<rect x="12" y="-12" width="${P}" height="${P}" fill="#8888aa" opacity="0.4"/>
|
|
2161
|
+
<rect x="14" y="-16" width="${P}" height="${P}" fill="#8888aa" opacity="0.3"/>
|
|
2162
|
+
<rect x="16" y="-20" width="${P}" height="${P}" fill="#8888aa" opacity="0.2"/>
|
|
2163
|
+
</g>
|
|
2164
|
+
`;
|
|
2165
|
+
} else if (period === 'morning') {
|
|
2166
|
+
// Stretching (arms raised)
|
|
2167
|
+
sherpaHtml = `
|
|
2168
|
+
<g transform="translate(345, 170)">
|
|
2169
|
+
<!-- Hat -->
|
|
2170
|
+
<rect x="0" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2171
|
+
<rect x="-4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2172
|
+
<rect x="4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2173
|
+
<!-- Face -->
|
|
2174
|
+
<rect x="-4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2175
|
+
<rect x="0" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2176
|
+
<rect x="4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2177
|
+
<!-- Body -->
|
|
2178
|
+
<rect x="-4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2179
|
+
<rect x="0" y="0" width="4" height="4" fill="#3498db"/>
|
|
2180
|
+
<rect x="4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2181
|
+
<!-- Arms raised -->
|
|
2182
|
+
<rect x="-8" y="-8" width="4" height="4" fill="#3498db"/>
|
|
2183
|
+
<rect x="8" y="-4" width="4" height="4" fill="#3498db"/>
|
|
2184
|
+
<!-- Lower body -->
|
|
2185
|
+
<rect x="-4" y="4" width="4" height="4" fill="#3498db"/>
|
|
2186
|
+
<rect x="0" y="4" width="4" height="4" fill="#3498db"/>
|
|
2187
|
+
<rect x="4" y="4" width="4" height="4" fill="#3498db"/>
|
|
2188
|
+
<!-- Legs -->
|
|
2189
|
+
<rect x="-4" y="8" width="4" height="4" fill="#2c3e50"/>
|
|
2190
|
+
<rect x="4" y="8" width="4" height="4" fill="#2c3e50"/>
|
|
2191
|
+
</g>
|
|
2192
|
+
`;
|
|
2193
|
+
} else if (period === 'day') {
|
|
2194
|
+
// Walking pose with backpack
|
|
2195
|
+
sherpaHtml = `
|
|
2196
|
+
<g transform="translate(355, 166)">
|
|
2197
|
+
<!-- Hat -->
|
|
2198
|
+
<rect x="0" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2199
|
+
<rect x="-4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2200
|
+
<rect x="4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2201
|
+
<!-- Face -->
|
|
2202
|
+
<rect x="-4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2203
|
+
<rect x="0" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2204
|
+
<rect x="4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2205
|
+
<!-- Body -->
|
|
2206
|
+
<rect x="-4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2207
|
+
<rect x="0" y="0" width="4" height="4" fill="#3498db"/>
|
|
2208
|
+
<rect x="4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2209
|
+
<rect x="-4" y="4" width="4" height="4" fill="#3498db"/>
|
|
2210
|
+
<rect x="0" y="4" width="4" height="4" fill="#3498db"/>
|
|
2211
|
+
<rect x="4" y="4" width="4" height="4" fill="#3498db"/>
|
|
2212
|
+
<!-- Backpack -->
|
|
2213
|
+
<rect x="8" y="0" width="4" height="4" fill="#8b6914"/>
|
|
2214
|
+
<rect x="8" y="4" width="4" height="4" fill="#8b6914"/>
|
|
2215
|
+
<!-- Legs (walking) -->
|
|
2216
|
+
<rect x="-4" y="8" width="4" height="4" fill="#2c3e50"/>
|
|
2217
|
+
<rect x="0" y="8" width="4" height="4" fill="#2c3e50"/>
|
|
2218
|
+
<rect x="4" y="12" width="4" height="4" fill="#2c3e50"/>
|
|
2219
|
+
<rect x="-4" y="12" width="4" height="4" fill="#2c3e50"/>
|
|
2220
|
+
</g>
|
|
2221
|
+
`;
|
|
2222
|
+
} else {
|
|
2223
|
+
// Sitting with mug
|
|
2224
|
+
sherpaHtml = `
|
|
2225
|
+
<g transform="translate(345, 170)">
|
|
2226
|
+
<!-- Hat -->
|
|
2227
|
+
<rect x="0" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2228
|
+
<rect x="-4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2229
|
+
<rect x="4" y="-8" width="4" height="4" fill="#e74c3c"/>
|
|
2230
|
+
<!-- Face -->
|
|
2231
|
+
<rect x="-4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2232
|
+
<rect x="0" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2233
|
+
<rect x="4" y="-4" width="4" height="4" fill="#f5c6a0"/>
|
|
2234
|
+
<!-- Body -->
|
|
2235
|
+
<rect x="-4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2236
|
+
<rect x="0" y="0" width="4" height="4" fill="#3498db"/>
|
|
2237
|
+
<rect x="4" y="0" width="4" height="4" fill="#3498db"/>
|
|
2238
|
+
<!-- Backpack -->
|
|
2239
|
+
<rect x="8" y="0" width="4" height="4" fill="#8b6914"/>
|
|
2240
|
+
<!-- Sitting legs -->
|
|
2241
|
+
<rect x="-4" y="4" width="4" height="4" fill="#2c3e50"/>
|
|
2242
|
+
<rect x="0" y="4" width="4" height="4" fill="#2c3e50"/>
|
|
2243
|
+
<!-- Mug (pixel) -->
|
|
2244
|
+
<rect x="-8" y="0" width="${P}" height="${P}" fill="#ddd"/>
|
|
2245
|
+
<rect x="-12" y="0" width="${P}" height="${P}" fill="#ddd" opacity="0.5"/>
|
|
2246
|
+
<!-- Steam (pixel) -->
|
|
2247
|
+
<rect x="-8" y="-4" width="${P}" height="${P}" fill="#4a5170" opacity="0.2">
|
|
2248
|
+
<animate attributeName="opacity" values="0.2;0.05;0.2" dur="2s" repeatCount="indefinite"/>
|
|
2249
|
+
</rect>
|
|
2250
|
+
</g>
|
|
2251
|
+
`;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// --- Distant climber (day and evening only) ---
|
|
2255
|
+
let distantClimber = '';
|
|
2256
|
+
if (period === 'day' || period === 'evening') {
|
|
2257
|
+
const climbOpacity = period === 'day' ? 0.6 : 0.5;
|
|
2258
|
+
distantClimber = `
|
|
2259
|
+
<g transform="translate(612, 148)" opacity="${climbOpacity}">
|
|
2260
|
+
<rect x="0" y="0" width="${P}" height="${P}" fill="#f5c6a0"/>
|
|
2261
|
+
<rect x="0" y="4" width="${P}" height="${P}" fill="#c84040"/>
|
|
2262
|
+
<rect x="0" y="8" width="${P}" height="${P}" fill="#2c3e50"/>
|
|
2263
|
+
<rect x="4" y="0" width="${P}" height="${P}" fill="#8090b0" opacity="0.5"/>
|
|
2264
|
+
<rect x="4" y="4" width="${P}" height="${P}" fill="#8090b0" opacity="0.4"/>
|
|
2265
|
+
</g>
|
|
2266
|
+
`;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// --- Tent glow (dawn only) ---
|
|
2270
|
+
let tentGlow = '';
|
|
2271
|
+
if (period === 'dawn') {
|
|
2272
|
+
tentGlow = `<rect x="88" y="180" width="4" height="4" fill="#ffcc44" opacity="0.3"/>`;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// --- Sunrise glow (morning only) ---
|
|
2276
|
+
let sunriseGlow = '';
|
|
2277
|
+
if (period === 'morning') {
|
|
2278
|
+
sunriseGlow = `<rect x="360" y="140" width="280" height="60" fill="#e8834a" opacity="0.06"/>
|
|
2279
|
+
<rect x="400" y="160" width="200" height="40" fill="#f5a060" opacity="0.05"/>`;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// --- Assemble SVG ---
|
|
2283
|
+
const svgString = `
|
|
2284
|
+
<svg viewBox="0 0 680 220" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" style="image-rendering:pixelated">
|
|
2285
|
+
<defs>
|
|
2286
|
+
<linearGradient id="bc-sky" x1="0" y1="0" x2="0" y2="1">
|
|
2287
|
+
${stops}
|
|
2288
|
+
</linearGradient>
|
|
2289
|
+
</defs>
|
|
2290
|
+
|
|
2291
|
+
<!-- Sky -->
|
|
2292
|
+
<rect width="680" height="220" fill="url(#bc-sky)"/>
|
|
2293
|
+
|
|
2294
|
+
${sunriseGlow}
|
|
2295
|
+
|
|
2296
|
+
<!-- Stars / clouds -->
|
|
2297
|
+
${starsHtml}
|
|
2298
|
+
|
|
2299
|
+
<!-- Far range mountains -->
|
|
2300
|
+
<polygon points="${farRangePoly}" fill="${theme.farRange}"/>
|
|
2301
|
+
|
|
2302
|
+
<!-- Snow caps -->
|
|
2303
|
+
${snowCaps}
|
|
2304
|
+
|
|
2305
|
+
<!-- Mid range mountains -->
|
|
2306
|
+
<polygon points="${midRangePoly}" fill="${theme.midRange}"/>
|
|
2307
|
+
|
|
2308
|
+
<!-- Ground -->
|
|
2309
|
+
${groundHtml}
|
|
2310
|
+
|
|
2311
|
+
<!-- Tent glow -->
|
|
2312
|
+
${tentGlow}
|
|
2313
|
+
|
|
2314
|
+
<!-- Tents -->
|
|
2315
|
+
${tents}
|
|
2316
|
+
|
|
2317
|
+
<!-- Prayer flags -->
|
|
2318
|
+
${prayerFlags1}
|
|
2319
|
+
${prayerFlags2}
|
|
2320
|
+
|
|
2321
|
+
<!-- Supplies & gear -->
|
|
2322
|
+
${supplies}
|
|
2323
|
+
|
|
2324
|
+
<!-- Campfire -->
|
|
2325
|
+
${campfireHtml}
|
|
2326
|
+
|
|
2327
|
+
<!-- Sherpa -->
|
|
2328
|
+
${sherpaHtml}
|
|
2329
|
+
|
|
2330
|
+
<!-- Distant climber -->
|
|
2331
|
+
${distantClimber}
|
|
2332
|
+
</svg>
|
|
2333
|
+
`;
|
|
2334
|
+
|
|
2335
|
+
container.innerHTML = svgString;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
function startSherpaQuotes() {
|
|
2339
|
+
const el = document.getElementById('sherpa-quote');
|
|
2340
|
+
if (!el) return;
|
|
2341
|
+
|
|
2342
|
+
const isFirstVisit = !localStorage.getItem(ONBOARDING_KEY);
|
|
2343
|
+
|
|
2344
|
+
if (isFirstVisit) {
|
|
2345
|
+
// First visit: start with tap prompt, then guide on click
|
|
2346
|
+
sherpaMode = 'intro';
|
|
2347
|
+
el.textContent = '나를 눌러보세유~';
|
|
2348
|
+
} else {
|
|
2349
|
+
setSherpaMode('grumpy');
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// Start rotation timer
|
|
2353
|
+
if (sherpaInterval) clearInterval(sherpaInterval);
|
|
2354
|
+
sherpaInterval = setInterval(() => {
|
|
2355
|
+
if (sherpaMode === 'intro') return; // Don't rotate during intro
|
|
2356
|
+
advanceSherpa();
|
|
2357
|
+
}, 8000);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
window.toggleSherpaMode = function() {
|
|
2361
|
+
const speech = document.getElementById('sherpa-speech');
|
|
2362
|
+
const el = document.getElementById('sherpa-quote');
|
|
2363
|
+
if (!el || !speech) return;
|
|
2364
|
+
|
|
2365
|
+
if (sherpaMode === 'intro') {
|
|
2366
|
+
// First click ever: enter guide mode
|
|
2367
|
+
setSherpaMode('guide');
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// Toggle between guide and grumpy
|
|
2372
|
+
const newMode = sherpaMode === 'guide' ? 'grumpy' : 'guide';
|
|
2373
|
+
|
|
2374
|
+
// Brief mode-switch message
|
|
2375
|
+
el.style.opacity = '0';
|
|
2376
|
+
setTimeout(() => {
|
|
2377
|
+
if (newMode === 'guide') {
|
|
2378
|
+
el.textContent = '가이드 모드여유~ 사용법 알려줄겨 📋';
|
|
2379
|
+
} else {
|
|
2380
|
+
el.textContent = '다시 푸념 모드여유... 😮💨';
|
|
1798
2381
|
}
|
|
2382
|
+
el.style.opacity = '1';
|
|
1799
2383
|
|
|
1800
|
-
|
|
2384
|
+
setTimeout(() => {
|
|
2385
|
+
setSherpaMode(newMode);
|
|
2386
|
+
}, 2000);
|
|
2387
|
+
}, 300);
|
|
2388
|
+
};
|
|
1801
2389
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2390
|
+
// ---------------------------------------------------------------------------
|
|
2391
|
+
// Activity Trail
|
|
2392
|
+
// ---------------------------------------------------------------------------
|
|
2393
|
+
|
|
2394
|
+
async function loadActivityTrail() {
|
|
2395
|
+
const container = document.getElementById('activity-trail');
|
|
2396
|
+
const section = document.getElementById('activity-trail-section');
|
|
2397
|
+
if (!container || !section) return;
|
|
2398
|
+
|
|
2399
|
+
try {
|
|
2400
|
+
const data = await api('GET', '/api/activity');
|
|
2401
|
+
if (!data || !data.daily || data.daily.length === 0) {
|
|
2402
|
+
section.style.display = 'none';
|
|
1806
2403
|
return;
|
|
1807
2404
|
}
|
|
1808
2405
|
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
tooltip.innerHTML = `
|
|
1827
|
-
<div class="onboarding-title">${s.title}</div>
|
|
1828
|
-
<div class="onboarding-text">${s.text}</div>
|
|
1829
|
-
<div class="onboarding-actions">
|
|
1830
|
-
<span class="onboarding-step">${step + 1}/${onboardingSteps.length}</span>
|
|
1831
|
-
<button class="btn btn-ghost btn-sm" onclick="skipOnboarding()">건너뛰기</button>
|
|
1832
|
-
<button class="btn btn-primary btn-sm" onclick="nextOnboardingStep()">${step === onboardingSteps.length - 1 ? '완료' : '다음'}</button>
|
|
1833
|
-
</div>`;
|
|
2406
|
+
section.style.display = '';
|
|
2407
|
+
const days = data.daily;
|
|
2408
|
+
const prs = data.mergedPrs || [];
|
|
2409
|
+
const streak = data.streak || 0;
|
|
2410
|
+
const totalCommits = days.reduce((s, d) => s + d.commits, 0);
|
|
2411
|
+
|
|
2412
|
+
// Calculate terrain heights
|
|
2413
|
+
const maxCommits = Math.max(...days.map(d => d.commits), 1);
|
|
2414
|
+
const svgW = 680, svgH = 180;
|
|
2415
|
+
const ground = 150, ceiling = 35;
|
|
2416
|
+
const dayW = (svgW - 40) / days.length; // 20px padding each side
|
|
2417
|
+
|
|
2418
|
+
// Map commits to Y coordinate (more commits = higher = lower Y)
|
|
2419
|
+
const heights = days.map(d => {
|
|
2420
|
+
if (d.commits === 0) return ground;
|
|
2421
|
+
return ground - ((d.commits / maxCommits) * (ground - ceiling));
|
|
2422
|
+
});
|
|
1834
2423
|
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2424
|
+
// Build stepped polyline points (pixel staircase)
|
|
2425
|
+
let terrainPoints = `20,${ground} `;
|
|
2426
|
+
heights.forEach((h, i) => {
|
|
2427
|
+
const x = 20 + i * dayW;
|
|
2428
|
+
const xEnd = 20 + (i + 1) * dayW;
|
|
2429
|
+
terrainPoints += `${x},${h} ${xEnd},${h} `;
|
|
2430
|
+
});
|
|
2431
|
+
terrainPoints += `${20 + days.length * dayW},${ground}`;
|
|
2432
|
+
|
|
2433
|
+
// PR merge dates set for lookup
|
|
2434
|
+
const prByDate = {};
|
|
2435
|
+
prs.forEach(pr => {
|
|
2436
|
+
const date = pr.mergedAt.split('T')[0];
|
|
2437
|
+
if (!prByDate[date]) prByDate[date] = [];
|
|
2438
|
+
prByDate[date].push(pr);
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
// Build pixel decorations
|
|
2442
|
+
let decorations = '';
|
|
2443
|
+
|
|
2444
|
+
// Stars
|
|
2445
|
+
const starPositions = [[45,8],[130,14],[220,6],[350,10],[480,5],[560,16],[640,8]];
|
|
2446
|
+
starPositions.forEach(([x,y]) => {
|
|
2447
|
+
decorations += `<rect x="${x}" y="${y}" width="2" height="2" fill="#fff" opacity="${0.2 + Math.random() * 0.3}"/>`;
|
|
2448
|
+
});
|
|
2449
|
+
|
|
2450
|
+
// Snow on high peaks (top 20%)
|
|
2451
|
+
const threshold = ceiling + (ground - ceiling) * 0.3;
|
|
2452
|
+
heights.forEach((h, i) => {
|
|
2453
|
+
if (h < threshold) {
|
|
2454
|
+
const x = 20 + i * dayW + dayW / 2 - 3;
|
|
2455
|
+
decorations += `<rect x="${x}" y="${h}" width="6" height="2" fill="rgba(255,255,255,0.2)"/>`;
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// Trees on low terrain
|
|
2460
|
+
heights.forEach((h, i) => {
|
|
2461
|
+
if (h > ground - 30 && h < ground && days[i].commits > 0 && Math.random() > 0.7) {
|
|
2462
|
+
const x = 20 + i * dayW + dayW / 2;
|
|
2463
|
+
decorations += `
|
|
2464
|
+
<rect x="${x}" y="${h - 8}" width="2" height="8" fill="#3d5a3d"/>
|
|
2465
|
+
<rect x="${x - 2}" y="${h - 12}" width="6" height="4" fill="#4a7a4a"/>
|
|
2466
|
+
<rect x="${x}" y="${h - 16}" width="2" height="4" fill="#5b8c5a"/>`;
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
|
|
2470
|
+
// Tents on rest days (0 commits, not first/last)
|
|
2471
|
+
heights.forEach((h, i) => {
|
|
2472
|
+
if (days[i].commits === 0 && i > 0 && i < days.length - 1 && Math.random() > 0.5) {
|
|
2473
|
+
const x = 20 + i * dayW + dayW / 2 - 4;
|
|
2474
|
+
decorations += `
|
|
2475
|
+
<rect x="${x + 3}" y="${ground - 8}" width="2" height="2" fill="#6b7394"/>
|
|
2476
|
+
<rect x="${x + 1}" y="${ground - 6}" width="6" height="2" fill="#6b7394"/>
|
|
2477
|
+
<rect x="${x}" y="${ground - 4}" width="8" height="2" fill="#4a5170"/>`;
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
// PR campfires + tooltip triggers
|
|
2482
|
+
let prMarkers = '';
|
|
2483
|
+
days.forEach((d, i) => {
|
|
2484
|
+
const datePrs = prByDate[d.date];
|
|
2485
|
+
if (datePrs && datePrs.length > 0) {
|
|
2486
|
+
const x = 20 + i * dayW + dayW / 2 - 4;
|
|
2487
|
+
const h = heights[i];
|
|
2488
|
+
const tooltipText = datePrs.map(p => `#${p.number} ${p.title}`).join('\n');
|
|
2489
|
+
prMarkers += `
|
|
2490
|
+
<g class="pr-marker" data-tooltip="${tooltipText.replace(/"/g, '"')}">
|
|
2491
|
+
<rect x="${x}" y="${h - 4}" width="2" height="2" fill="#8B4513"/>
|
|
2492
|
+
<rect x="${x + 4}" y="${h - 4}" width="2" height="2" fill="#8B4513"/>
|
|
2493
|
+
<rect x="${x + 2}" y="${h - 8}" width="2" height="4" fill="#ff6600"/>
|
|
2494
|
+
<rect x="${x}" y="${h - 10}" width="2" height="2" fill="#ffcc00"/>
|
|
2495
|
+
<rect x="${x + 4}" y="${h - 12}" width="2" height="4" fill="#ff9900"/>
|
|
2496
|
+
<rect x="${x + 2}" y="${h - 14}" width="2" height="4" fill="#ffcc00"/>
|
|
2497
|
+
<circle cx="${x + 3}" cy="${h - 8}" r="6" fill="#ff9900" opacity="0.06"/>
|
|
2498
|
+
</g>`;
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// Coins on top 3 peaks
|
|
2503
|
+
const peakIndices = heights
|
|
2504
|
+
.map((h, i) => ({ h, i }))
|
|
2505
|
+
.sort((a, b) => a.h - b.h)
|
|
2506
|
+
.slice(0, 3);
|
|
2507
|
+
peakIndices.forEach(({ h, i }) => {
|
|
2508
|
+
const x = 20 + i * dayW + dayW / 2 - 3;
|
|
2509
|
+
decorations += `
|
|
2510
|
+
<rect x="${x}" y="${h - 10}" width="6" height="6" fill="#f59e0b" opacity="0.7"/>
|
|
2511
|
+
<rect x="${x + 2}" y="${h - 8}" width="2" height="2" fill="#0a0c11"/>`;
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
// Flag on highest peak
|
|
2515
|
+
const highest = peakIndices[0];
|
|
2516
|
+
if (highest) {
|
|
2517
|
+
const x = 20 + highest.i * dayW + dayW / 2;
|
|
2518
|
+
decorations += `
|
|
2519
|
+
<rect x="${x}" y="${highest.h}" width="2" height="14" fill="#f59e0b"/>
|
|
2520
|
+
<polygon points="${x + 2},${highest.h} ${x + 8},${highest.h + 3} ${x + 2},${highest.h + 6}" fill="#f59e0b"/>`;
|
|
1846
2521
|
}
|
|
1847
2522
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2523
|
+
// Sherpa at today (last position)
|
|
2524
|
+
const lastX = 20 + (days.length - 1) * dayW + dayW / 2 - 4;
|
|
2525
|
+
const lastH = heights[heights.length - 1];
|
|
2526
|
+
const sherpaY = lastH - 16;
|
|
2527
|
+
const sherpa = `
|
|
2528
|
+
<g transform="translate(${lastX}, ${sherpaY})">
|
|
2529
|
+
<rect x="2" y="0" width="4" height="2" fill="#5b8c5a"/>
|
|
2530
|
+
<rect x="0" y="2" width="8" height="2" fill="#5b8c5a"/>
|
|
2531
|
+
<rect x="2" y="4" width="4" height="4" fill="#ffcc88"/>
|
|
2532
|
+
<rect x="2" y="8" width="4" height="4" fill="#e74c3c"/>
|
|
2533
|
+
<rect x="0" y="8" width="2" height="2" fill="#e74c3c"/>
|
|
2534
|
+
<rect x="6" y="8" width="2" height="4" fill="#8B6914"/>
|
|
2535
|
+
<rect x="2" y="12" width="2" height="2" fill="#5b4a3a"/>
|
|
2536
|
+
<rect x="4" y="12" width="2" height="2" fill="#5b4a3a"/>
|
|
2537
|
+
</g>`;
|
|
2538
|
+
|
|
2539
|
+
// Week labels
|
|
2540
|
+
let weekLabels = '';
|
|
2541
|
+
const weekSize = 7;
|
|
2542
|
+
for (let w = 0; w < Math.floor(days.length / weekSize); w++) {
|
|
2543
|
+
const x = 20 + (w * weekSize + 3) * dayW;
|
|
2544
|
+
const weeksAgo = Math.floor(days.length / weekSize) - w - 1;
|
|
2545
|
+
const label = weeksAgo === 0 ? '이번 주' : `${weeksAgo}주 전`;
|
|
2546
|
+
weekLabels += `<text x="${x}" y="170" font-size="8" fill="#4a5170" font-family="Outfit,sans-serif" text-anchor="middle">${label}</text>`;
|
|
2547
|
+
}
|
|
1851
2548
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2549
|
+
// Pixel clouds
|
|
2550
|
+
const clouds = `
|
|
2551
|
+
<g opacity="0.08">
|
|
2552
|
+
<rect x="80" y="20" width="4" height="4" fill="#fff"/>
|
|
2553
|
+
<rect x="84" y="18" width="8" height="4" fill="#fff"/>
|
|
2554
|
+
<rect x="88" y="16" width="4" height="4" fill="#fff"/>
|
|
2555
|
+
<rect x="92" y="18" width="4" height="4" fill="#fff"/>
|
|
2556
|
+
<rect x="76" y="22" width="24" height="4" fill="#fff"/>
|
|
2557
|
+
</g>
|
|
2558
|
+
<g opacity="0.06">
|
|
2559
|
+
<rect x="420" y="14" width="4" height="4" fill="#fff"/>
|
|
2560
|
+
<rect x="424" y="12" width="8" height="4" fill="#fff"/>
|
|
2561
|
+
<rect x="432" y="14" width="4" height="4" fill="#fff"/>
|
|
2562
|
+
<rect x="416" y="18" width="24" height="4" fill="#fff"/>
|
|
2563
|
+
</g>`;
|
|
2564
|
+
|
|
2565
|
+
// PR tooltip container (CSS positioned)
|
|
2566
|
+
const tooltip = `<div class="activity-tooltip" id="activity-tooltip" style="display:none;position:absolute;background:#1c2030;border:1px solid #2a2f42;border-radius:6px;padding:8px 12px;font-size:12px;color:#e4e8f0;pointer-events:none;white-space:nowrap;z-index:10;max-width:320px;box-shadow:0 4px 12px rgba(0,0,0,0.4);"></div>`;
|
|
2567
|
+
|
|
2568
|
+
const svg = `
|
|
2569
|
+
<svg viewBox="0 0 ${svgW} ${svgH}" style="display:block;width:100%;" xmlns="http://www.w3.org/2000/svg">
|
|
2570
|
+
<rect x="0" y="0" width="${svgW}" height="${svgH}" fill="#080a10"/>
|
|
2571
|
+
${clouds}
|
|
2572
|
+
${decorations}
|
|
2573
|
+
<defs>
|
|
2574
|
+
<linearGradient id="trailFill" x1="0" y1="0" x2="0" y2="1">
|
|
2575
|
+
<stop offset="0%" stop-color="#5b8c5a" stop-opacity="0.25"/>
|
|
2576
|
+
<stop offset="100%" stop-color="#5b8c5a" stop-opacity="0.03"/>
|
|
2577
|
+
</linearGradient>
|
|
2578
|
+
</defs>
|
|
2579
|
+
<polygon fill="url(#trailFill)" points="${terrainPoints}"/>
|
|
2580
|
+
<polyline fill="none" stroke="#5b8c5a" stroke-width="2" points="${terrainPoints.split(` ${20 + days.length * dayW},${ground}`)[0]}"/>
|
|
2581
|
+
<line x1="16" y1="${ground}" x2="${svgW - 16}" y2="${ground}" stroke="#1c2030" stroke-width="1"/>
|
|
2582
|
+
${prMarkers}
|
|
2583
|
+
${sherpa}
|
|
2584
|
+
${weekLabels}
|
|
2585
|
+
</svg>`;
|
|
2586
|
+
|
|
2587
|
+
const streakText = streak > 0 ? `🔥 연속 ${streak}일째 등반 중` : '⛺ 오늘은 쉬는 날';
|
|
2588
|
+
const periodText = `최근 4주 · 커밋 ${totalCommits}개 · PR ${prs.length}개`;
|
|
2589
|
+
|
|
2590
|
+
container.innerHTML = `
|
|
2591
|
+
<div style="position:relative;">
|
|
2592
|
+
${svg}
|
|
2593
|
+
${tooltip}
|
|
2594
|
+
</div>
|
|
2595
|
+
<div class="activity-info">
|
|
2596
|
+
<div class="activity-streak">${streakText}</div>
|
|
2597
|
+
<div class="activity-period">${periodText}</div>
|
|
2598
|
+
</div>`;
|
|
1856
2599
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2600
|
+
// Add PR tooltip hover handlers
|
|
2601
|
+
container.querySelectorAll('.pr-marker').forEach(marker => {
|
|
2602
|
+
marker.style.cursor = 'pointer';
|
|
2603
|
+
marker.addEventListener('mouseenter', (e) => {
|
|
2604
|
+
const tip = document.getElementById('activity-tooltip');
|
|
2605
|
+
if (!tip) return;
|
|
2606
|
+
tip.innerHTML = marker.getAttribute('data-tooltip').split('\n').map(l => escHtml(l)).join('<br>');
|
|
2607
|
+
tip.style.display = 'block';
|
|
2608
|
+
const rect = marker.getBoundingClientRect();
|
|
2609
|
+
const containerRect = container.getBoundingClientRect();
|
|
2610
|
+
let left = rect.left - containerRect.left;
|
|
2611
|
+
// Prevent overflow on right edge
|
|
2612
|
+
const tipWidth = tip.offsetWidth;
|
|
2613
|
+
if (left + tipWidth > containerRect.width) {
|
|
2614
|
+
left = containerRect.width - tipWidth - 8;
|
|
2615
|
+
}
|
|
2616
|
+
if (left < 8) left = 8;
|
|
2617
|
+
tip.style.left = left + 'px';
|
|
2618
|
+
tip.style.top = (rect.top - containerRect.top - 40) + 'px';
|
|
2619
|
+
});
|
|
2620
|
+
marker.addEventListener('mouseleave', () => {
|
|
2621
|
+
const tip = document.getElementById('activity-tooltip');
|
|
2622
|
+
if (tip) tip.style.display = 'none';
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
1861
2625
|
|
|
1862
|
-
|
|
1863
|
-
|
|
2626
|
+
} catch {
|
|
2627
|
+
if (section) section.style.display = 'none';
|
|
2628
|
+
}
|
|
1864
2629
|
}
|
|
1865
2630
|
|
|
1866
2631
|
// ---------------------------------------------------------------------------
|
|
@@ -1878,11 +2643,11 @@ async function init() {
|
|
|
1878
2643
|
}
|
|
1879
2644
|
renderAll();
|
|
1880
2645
|
loadPortal();
|
|
1881
|
-
|
|
2646
|
+
renderBasecampScene();
|
|
2647
|
+
startSherpaQuotes();
|
|
2648
|
+
loadActivityTrail();
|
|
1882
2649
|
connectWs();
|
|
1883
2650
|
|
|
1884
|
-
// Show onboarding for first-time users
|
|
1885
|
-
setTimeout(showOnboarding, 500);
|
|
1886
2651
|
}
|
|
1887
2652
|
|
|
1888
2653
|
document.addEventListener('DOMContentLoaded', init);
|