sanjang 0.3.3 → 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 CHANGED
@@ -1241,9 +1241,22 @@ function renderWorkspace(data) {
1241
1241
  previewEl.querySelector('.ws-preview-fallback').style.display = 'flex';
1242
1242
  });
1243
1243
  } else {
1244
- previewEl.innerHTML = `<span style="color:var(--text-muted);font-size:13px">
1245
- 서버가 실행 중이 아닙니다. 먼저 시작해주세요.
1246
- </span>`;
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>`;
1247
1260
  }
1248
1261
 
1249
1262
  // Terminal button label
@@ -1737,150 +1750,643 @@ window.autoFix = async function autoFix(name) {
1737
1750
  };
1738
1751
 
1739
1752
  // ---------------------------------------------------------------------------
1740
- // Onboarding Tutorial
1753
+ // Sherpa Guide Mode (replaces overlay onboarding)
1741
1754
  // ---------------------------------------------------------------------------
1742
1755
 
1743
1756
  const ONBOARDING_KEY = 'sanjang-onboarded';
1744
1757
 
1745
- const onboardingSteps = [
1746
- {
1747
- target: '#quickstart-input',
1748
- title: '캠프 만들기',
1749
- text: '하고 싶은 입력하면 AI가 캠프를 자동으로 만들어줘요.',
1750
- position: 'bottom',
1751
- },
1752
- {
1753
- target: '#ws-preview',
1754
- title: '프리뷰 확인',
1755
- text: '캠프에 들어가면 전체화면으로 프리뷰를 볼 수 있어요.',
1756
- position: 'center',
1757
- waitForWorkspace: true,
1758
- },
1759
- {
1760
- target: '#ws-save-btn',
1761
- title: '세이브하기',
1762
- text: '변경사항이 있으면 세이브 버튼으로 저장해요. 게임 세이브처럼요!',
1763
- position: 'left',
1764
- waitForWorkspace: true,
1765
- },
1758
+ const SHERPA_GUIDE = [
1759
+ "여기에 하고 싶은 거 적으면 되유. AI가 캠프 만들어줄겨.",
1760
+ "캠프 들어가면 프리뷰 전체화면으로 보여유. 편하쥬?",
1761
+ "세이브는 게임 세이브처럼 저장이여유. 💾 버튼 누르면 되유.",
1762
+ "팀에 보내기 누르면 PR 만들어주유. 셰르파가 해줄겨.",
1763
+ "그럼 이제 시작해봐유. 화이팅이여유~ 🏔️",
1766
1764
  ];
1767
1765
 
1768
- function showOnboarding() {
1769
- if (localStorage.getItem(ONBOARDING_KEY)) return;
1770
- let step = 0;
1766
+ // ---------------------------------------------------------------------------
1767
+ // Sherpa Mode System (guide ↔ grumpy toggle)
1768
+ // ---------------------------------------------------------------------------
1771
1769
 
1772
- function show() {
1773
- // Remove previous
1774
- document.querySelector('.onboarding-overlay')?.remove();
1770
+ let sherpaInterval = null;
1771
+ let sherpaMode = 'grumpy'; // 'guide' or 'grumpy'
1772
+ let sherpaQueue = [];
1773
+ let sherpaIdx = 0;
1775
1774
 
1776
- if (step >= onboardingSteps.length) {
1777
- localStorage.setItem(ONBOARDING_KEY, '1');
1778
- return;
1779
- }
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
+ }
1780
1783
 
1781
- const s = onboardingSteps[step];
1784
+ function setSherpaMode(mode) {
1785
+ sherpaMode = mode;
1786
+ sherpaIdx = 0;
1787
+ sherpaQueue = mode === 'guide' ? [...SHERPA_GUIDE] : shuffleArray(SHERPA_QUOTES);
1782
1788
 
1783
- // Skip workspace steps if not in workspace
1784
- if (s.waitForWorkspace && !currentWorkspace) {
1785
- step++;
1786
- show();
1787
- return;
1788
- }
1789
+ const el = document.getElementById('sherpa-quote');
1790
+ const speech = document.getElementById('sherpa-speech');
1791
+ if (!el || !speech) return;
1789
1792
 
1790
- const el = document.querySelector(s.target);
1791
- if (!el) { step++; show(); return; }
1792
-
1793
- const overlay = document.createElement('div');
1794
- overlay.className = 'onboarding-overlay';
1795
-
1796
- const rect = el.getBoundingClientRect();
1797
- const highlight = document.createElement('div');
1798
- highlight.className = 'onboarding-highlight';
1799
- highlight.style.top = `${rect.top - 4}px`;
1800
- highlight.style.left = `${rect.left - 4}px`;
1801
- highlight.style.width = `${rect.width + 8}px`;
1802
- highlight.style.height = `${rect.height + 8}px`;
1803
- overlay.appendChild(highlight);
1804
-
1805
- const tooltip = document.createElement('div');
1806
- tooltip.className = 'onboarding-tooltip';
1807
- tooltip.innerHTML = `
1808
- <div class="onboarding-title">${s.title}</div>
1809
- <div class="onboarding-text">${s.text}</div>
1810
- <div class="onboarding-actions">
1811
- <span class="onboarding-step">${step + 1}/${onboardingSteps.length}</span>
1812
- <button class="btn btn-ghost btn-sm" onclick="skipOnboarding()">건너뛰기</button>
1813
- <button class="btn btn-primary btn-sm" onclick="nextOnboardingStep()">${step === onboardingSteps.length - 1 ? '완료' : '다음'}</button>
1814
- </div>`;
1793
+ // Visual mode indicator
1794
+ speech.classList.toggle('guide-mode', mode === 'guide');
1815
1795
 
1816
- // Position tooltip near target
1817
- if (s.position === 'bottom') {
1818
- tooltip.style.top = `${rect.bottom + 12}px`;
1819
- tooltip.style.left = `${Math.max(12, rect.left)}px`;
1820
- } else if (s.position === 'left') {
1821
- tooltip.style.top = `${rect.top}px`;
1822
- tooltip.style.right = `${window.innerWidth - rect.left + 12}px`;
1823
- } else {
1824
- tooltip.style.top = '50%';
1825
- tooltip.style.left = '50%';
1826
- tooltip.style.transform = 'translate(-50%, -50%)';
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;
1827
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)',
1839
+ },
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)',
1847
+ },
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)',
1855
+ },
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
+ };
1865
+
1866
+ function renderBasecampScene() {
1867
+ const container = document.getElementById('bc-scene-container');
1868
+ if (!container) return;
1828
1869
 
1829
- overlay.appendChild(tooltip);
1830
- document.body.appendChild(overlay);
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('');
1831
1929
  }
1832
1930
 
1833
- window.nextOnboardingStep = function() {
1834
- step++;
1835
- show();
1836
- };
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
+ }
1837
2141
 
1838
- window.skipOnboarding = function() {
1839
- document.querySelector('.onboarding-overlay')?.remove();
1840
- localStorage.setItem(ONBOARDING_KEY, '1');
1841
- };
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
+ }
1842
2253
 
1843
- // Start first step (only if on portal/quickstart visible)
1844
- show();
1845
- }
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
+ }
1846
2268
 
1847
- // ---------------------------------------------------------------------------
1848
- // Sherpa Quote Rotation
1849
- // ---------------------------------------------------------------------------
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
+ }
1850
2337
 
1851
2338
  function startSherpaQuotes() {
1852
2339
  const el = document.getElementById('sherpa-quote');
1853
2340
  if (!el) return;
1854
2341
 
1855
- // Shuffle array (Fisher-Yates)
1856
- let quotes = [...SHERPA_QUOTES];
1857
- for (let i = quotes.length - 1; i > 0; i--) {
1858
- const j = Math.floor(Math.random() * (i + 1));
1859
- [quotes[i], quotes[j]] = [quotes[j], quotes[i]];
1860
- }
2342
+ const isFirstVisit = !localStorage.getItem(ONBOARDING_KEY);
1861
2343
 
1862
- let idx = 0;
1863
- // Set initial random quote
1864
- el.textContent = quotes[idx];
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
+ }
1865
2351
 
1866
- setInterval(() => {
1867
- el.style.opacity = '0';
1868
- setTimeout(() => {
1869
- idx++;
1870
- if (idx >= quotes.length) {
1871
- // Reshuffle
1872
- for (let i = quotes.length - 1; i > 0; i--) {
1873
- const j = Math.floor(Math.random() * (i + 1));
1874
- [quotes[i], quotes[j]] = [quotes[j], quotes[i]];
1875
- }
1876
- idx = 0;
1877
- }
1878
- el.textContent = quotes[idx];
1879
- el.style.opacity = '1';
1880
- }, 500);
2352
+ // Start rotation timer
2353
+ if (sherpaInterval) clearInterval(sherpaInterval);
2354
+ sherpaInterval = setInterval(() => {
2355
+ if (sherpaMode === 'intro') return; // Don't rotate during intro
2356
+ advanceSherpa();
1881
2357
  }, 8000);
1882
2358
  }
1883
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 = '다시 푸념 모드여유... 😮‍💨';
2381
+ }
2382
+ el.style.opacity = '1';
2383
+
2384
+ setTimeout(() => {
2385
+ setSherpaMode(newMode);
2386
+ }, 2000);
2387
+ }, 300);
2388
+ };
2389
+
1884
2390
  // ---------------------------------------------------------------------------
1885
2391
  // Activity Trail
1886
2392
  // ---------------------------------------------------------------------------
@@ -2137,12 +2643,11 @@ async function init() {
2137
2643
  }
2138
2644
  renderAll();
2139
2645
  loadPortal();
2646
+ renderBasecampScene();
2140
2647
  startSherpaQuotes();
2141
2648
  loadActivityTrail();
2142
2649
  connectWs();
2143
2650
 
2144
- // Show onboarding for first-time users
2145
- setTimeout(showOnboarding, 500);
2146
2651
  }
2147
2652
 
2148
2653
  document.addEventListener('DOMContentLoaded', init);
@@ -19,38 +19,14 @@
19
19
  <div id="portal">
20
20
  <!-- 베이스캠프 씬 -->
21
21
  <div class="portal-section" id="basecamp-scene">
22
- <div class="bc-scene">
23
- <!-- Stars -->
24
- <div class="bc-star twinkle" style="top:8%;left:12%"></div>
25
- <div class="bc-star twinkle-slow" style="top:15%;left:30%"></div>
26
- <div class="bc-star twinkle" style="top:5%;left:48%"></div>
27
- <div class="bc-star twinkle-slow" style="top:20%;left:65%"></div>
28
- <div class="bc-star twinkle" style="top:10%;left:80%"></div>
29
- <div class="bc-star twinkle-slow" style="top:25%;left:92%"></div>
30
- <div class="bc-star twinkle" style="top:18%;left:5%"></div>
31
- <div class="bc-star twinkle-slow" style="top:6%;left:72%"></div>
32
- <div class="bc-star twinkle" style="top:22%;left:42%"></div>
33
- <div class="bc-star twinkle-slow" style="top:12%;left:55%"></div>
34
-
35
- <!-- Mountains -->
36
- <div class="bc-mountain bc-mountain-back"></div>
37
- <div class="bc-mountain bc-mountain-front"></div>
38
-
39
- <!-- Snow caps -->
40
- <div class="bc-snow" style="bottom:64px;left:48%;width:12px"></div>
41
- <div class="bc-snow" style="bottom:60px;left:50%;width:8px"></div>
42
- <div class="bc-snow" style="bottom:56px;left:46%;width:4px"></div>
43
-
44
- <!-- Campfire glow + campfire -->
45
- <div class="bc-glow"></div>
46
- <div class="bc-campfire"></div>
47
-
48
- <!-- Sherpa -->
49
- <div class="bc-sherpa"></div>
50
-
51
- <!-- Speech bubble -->
22
+ <div class="bc-scene" id="bc-scene-container">
23
+ <!-- JS renders time-based SVG scene here -->
24
+ </div>
25
+ <!-- Speech bubble stays outside SVG for text rendering -->
26
+ <div class="bc-speech-wrap">
27
+ <div class="bc-sherpa-hitbox" onclick="toggleSherpaMode()" title="클릭해서 모드 변경"></div>
52
28
  <div class="bc-speech fade-in" id="sherpa-speech">
53
- <span id="sherpa-quote">요구사항 또 바뀌었댜... 뭐 그러려니 하쥬</span>
29
+ <span id="sherpa-quote"></span>
54
30
  </div>
55
31
  </div>
56
32
  </div>
@@ -2046,331 +2046,70 @@ header.hidden {
2046
2046
  Onboarding Tutorial
2047
2047
  ============================================================ */
2048
2048
 
2049
- .onboarding-overlay {
2050
- position: fixed;
2051
- top: 0; left: 0; right: 0; bottom: 0;
2052
- background: rgba(0, 0, 0, 0.6);
2053
- z-index: 10000;
2054
- }
2055
-
2056
- .onboarding-highlight {
2057
- position: fixed;
2058
- border: 2px solid var(--accent, #6366f1);
2059
- border-radius: 8px;
2060
- box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), 0 0 20px rgba(99, 102, 241, 0.4);
2061
- z-index: 10001;
2062
- pointer-events: none;
2063
- animation: onboard-pulse 1.5s ease-in-out infinite;
2064
- }
2065
-
2066
- @keyframes onboard-pulse {
2067
- 0%, 100% { box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), 0 0 20px rgba(99, 102, 241, 0.3); }
2068
- 50% { box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), 0 0 30px rgba(99, 102, 241, 0.6); }
2069
- }
2070
-
2071
- .onboarding-tooltip {
2072
- position: fixed;
2073
- background: var(--bg-elevated, #1e1e2e);
2074
- border: 1px solid var(--border, #333);
2075
- border-radius: 12px;
2076
- padding: 16px 20px;
2077
- max-width: 320px;
2078
- z-index: 10002;
2079
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
2080
- animation: onboard-fade-in 0.3s ease-out;
2081
- }
2082
-
2083
- @keyframes onboard-fade-in {
2084
- from { opacity: 0; transform: translateY(8px); }
2085
- to { opacity: 1; transform: translateY(0); }
2086
- }
2087
-
2088
- .onboarding-title {
2089
- font-size: 15px;
2090
- font-weight: 700;
2091
- color: var(--text-primary, #fff);
2092
- margin-bottom: 6px;
2093
- }
2094
-
2095
- .onboarding-text {
2096
- font-size: 13px;
2097
- color: var(--text-secondary, #aaa);
2098
- line-height: 1.5;
2099
- margin-bottom: 12px;
2100
- }
2101
-
2102
- .onboarding-actions {
2103
- display: flex;
2104
- align-items: center;
2105
- gap: 8px;
2106
- }
2107
-
2108
- .onboarding-step {
2109
- font-size: 11px;
2110
- color: var(--text-muted, #666);
2111
- flex: 1;
2112
- }
2113
2049
 
2114
2050
  /* ============================================================
2115
- Basecamp Scene
2051
+ Basecamp Scene — Himalaya
2116
2052
  ============================================================ */
2117
2053
 
2118
2054
  .bc-scene {
2119
2055
  position: relative;
2120
- height: 200px;
2121
- background: linear-gradient(180deg, #080a10 0%, #0c0e14 60%, #12151e 100%);
2056
+ height: 220px;
2122
2057
  border-radius: 12px;
2123
2058
  overflow: hidden;
2124
2059
  border: 1px solid #1c2030;
2125
2060
  }
2126
2061
 
2127
- /* Stars */
2128
- .bc-star {
2129
- position: absolute;
2130
- width: 4px;
2131
- height: 4px;
2132
- background: #fff;
2133
- image-rendering: pixelated;
2134
- }
2135
-
2136
- .bc-star.twinkle {
2137
- animation: twinkle 2s steps(2) infinite;
2138
- }
2139
-
2140
- .bc-star.twinkle-slow {
2141
- animation: twinkle 3.5s steps(2) infinite 0.8s;
2142
- }
2143
-
2144
- @keyframes twinkle {
2145
- 0%, 100% { opacity: 0.6; }
2146
- 50% { opacity: 0.1; }
2147
- }
2148
-
2149
- /* Mountains */
2150
- .bc-mountain {
2151
- position: absolute;
2152
- bottom: 0;
2153
- left: 0;
2154
- right: 0;
2155
- image-rendering: pixelated;
2156
- }
2157
-
2158
- .bc-mountain-back {
2159
- height: 100px;
2160
- background: #1a1d2a;
2161
- clip-path: polygon(
2162
- 0% 100%, 0% 70%, 5% 70%, 5% 60%, 10% 60%, 10% 50%, 15% 50%, 15% 40%,
2163
- 20% 40%, 20% 35%, 25% 35%, 25% 45%, 30% 45%, 30% 55%, 35% 55%, 35% 50%,
2164
- 40% 50%, 40% 35%, 45% 35%, 45% 25%, 50% 25%, 50% 20%, 55% 20%, 55% 30%,
2165
- 60% 30%, 60% 40%, 65% 40%, 65% 50%, 70% 50%, 70% 40%, 75% 40%, 75% 50%,
2166
- 80% 50%, 80% 60%, 85% 60%, 85% 70%, 90% 70%, 90% 80%, 95% 80%, 100% 80%,
2167
- 100% 100%
2168
- );
2169
- }
2170
-
2171
- .bc-mountain-front {
2172
- height: 80px;
2173
- background: #12151e;
2174
- clip-path: polygon(
2175
- 0% 100%, 0% 80%, 5% 80%, 5% 70%, 10% 70%, 10% 55%, 15% 55%, 15% 45%,
2176
- 20% 45%, 20% 40%, 25% 40%, 25% 50%, 30% 50%, 30% 60%, 35% 60%, 35% 55%,
2177
- 40% 55%, 40% 40%, 45% 40%, 45% 30%, 48% 30%, 48% 25%, 52% 25%, 52% 20%,
2178
- 55% 20%, 55% 30%, 58% 30%, 58% 40%, 62% 40%, 62% 50%, 65% 50%, 65% 45%,
2179
- 70% 45%, 70% 35%, 75% 35%, 75% 45%, 80% 45%, 80% 55%, 85% 55%, 85% 65%,
2180
- 90% 65%, 90% 75%, 95% 75%, 95% 85%, 100% 85%, 100% 100%
2181
- );
2182
- }
2183
-
2184
- /* Snow caps */
2185
- .bc-snow {
2186
- position: absolute;
2187
- height: 4px;
2188
- background: rgba(255, 255, 255, 0.25);
2189
- image-rendering: pixelated;
2062
+ .bc-scene svg {
2063
+ display: block;
2064
+ width: 100%;
2065
+ height: 100%;
2190
2066
  }
2191
2067
 
2192
- /* Campfire glow */
2193
- .bc-glow {
2068
+ /* Speech bubble positioning */
2069
+ .bc-speech-wrap {
2194
2070
  position: absolute;
2195
- bottom: 0;
2196
- left: 50%;
2197
- transform: translateX(-50%);
2198
- width: 80px;
2199
- height: 40px;
2200
- background: radial-gradient(ellipse at center bottom, rgba(255, 140, 50, 0.15) 0%, transparent 70%);
2201
- animation: glow-pulse 2s ease infinite;
2202
- }
2203
-
2204
- @keyframes glow-pulse {
2205
- 0%, 100% { opacity: 1; }
2206
- 50% { opacity: 0.6; }
2071
+ bottom: 56px;
2072
+ left: calc(50% + 16px);
2073
+ z-index: 5;
2207
2074
  }
2208
2075
 
2209
- /* Campfire pixel */
2210
- .bc-campfire {
2076
+ .bc-sherpa-hitbox {
2211
2077
  position: absolute;
2212
- bottom: 12px;
2213
- left: 50%;
2214
- transform: translateX(-50%);
2215
- width: 4px;
2216
- height: 4px;
2217
- background: #8b4513;
2218
- image-rendering: pixelated;
2219
- box-shadow:
2220
- -4px 0 0 #8b4513,
2221
- 4px 0 0 #8b4513,
2222
- 0 -4px 0 #ff6600,
2223
- -4px -4px 0 #ff8800,
2224
- 4px -4px 0 #ff8800,
2225
- 0 -8px 0 #ffaa00,
2226
- -4px -8px 0 #ff6600,
2227
- 4px -8px 0 #ff6600,
2228
- 0 -12px 0 #ffcc00;
2229
- animation: bc-fire 0.4s steps(1) infinite;
2230
- }
2231
-
2232
- @keyframes bc-fire {
2233
- 0% {
2234
- box-shadow:
2235
- -4px 0 0 #8b4513,
2236
- 4px 0 0 #8b4513,
2237
- 0 -4px 0 #ff6600,
2238
- -4px -4px 0 #ff8800,
2239
- 4px -4px 0 #ff8800,
2240
- 0 -8px 0 #ffaa00,
2241
- -4px -8px 0 #ff6600,
2242
- 4px -8px 0 #ff6600,
2243
- 0 -12px 0 #ffcc00;
2244
- }
2245
- 50% {
2246
- box-shadow:
2247
- -4px 0 0 #8b4513,
2248
- 4px 0 0 #8b4513,
2249
- 0 -4px 0 #ff8800,
2250
- -4px -4px 0 #ff6600,
2251
- 4px -4px 0 #ffaa00,
2252
- 0 -8px 0 #ff6600,
2253
- -4px -8px 0 #ffcc00,
2254
- 4px -8px 0 #ff8800,
2255
- 0 -12px 0 #ff6600;
2256
- }
2078
+ bottom: -44px;
2079
+ left: -8px;
2080
+ width: 44px;
2081
+ height: 44px;
2082
+ cursor: pointer;
2083
+ z-index: 6;
2257
2084
  }
2258
2085
 
2259
- /* Sherpa pixel character (idle pose) */
2260
- .bc-sherpa {
2261
- position: absolute;
2262
- bottom: 16px;
2263
- left: calc(50% + 24px);
2264
- width: 4px;
2265
- height: 4px;
2266
- background: transparent;
2267
- image-rendering: pixelated;
2268
- box-shadow:
2269
- /* Hat */
2270
- 0 -20px 0 #e74c3c,
2271
- -4px -20px 0 #e74c3c,
2272
- 4px -20px 0 #e74c3c,
2273
- -4px -24px 0 #e74c3c,
2274
- 0 -24px 0 #e74c3c,
2275
- 4px -24px 0 #e74c3c,
2276
- /* Face */
2277
- -4px -16px 0 #f5c6a0,
2278
- 0 -16px 0 #f5c6a0,
2279
- 4px -16px 0 #f5c6a0,
2280
- /* Body */
2281
- -4px -12px 0 #3498db,
2282
- 0 -12px 0 #3498db,
2283
- 4px -12px 0 #3498db,
2284
- -4px -8px 0 #3498db,
2285
- 0 -8px 0 #3498db,
2286
- 4px -8px 0 #3498db,
2287
- /* Backpack */
2288
- 8px -12px 0 #8b6914,
2289
- 8px -8px 0 #8b6914,
2290
- /* Legs */
2291
- -4px -4px 0 #2c3e50,
2292
- 0 -4px 0 #2c3e50,
2293
- -4px 0 0 #2c3e50,
2294
- 4px 0 0 #2c3e50;
2295
- animation: sherpa-idle 1s steps(1) infinite;
2296
- }
2297
-
2298
- @keyframes sherpa-idle {
2299
- 0%, 100% {
2300
- box-shadow:
2301
- 0 -20px 0 #e74c3c,
2302
- -4px -20px 0 #e74c3c,
2303
- 4px -20px 0 #e74c3c,
2304
- -4px -24px 0 #e74c3c,
2305
- 0 -24px 0 #e74c3c,
2306
- 4px -24px 0 #e74c3c,
2307
- -4px -16px 0 #f5c6a0,
2308
- 0 -16px 0 #f5c6a0,
2309
- 4px -16px 0 #f5c6a0,
2310
- -4px -12px 0 #3498db,
2311
- 0 -12px 0 #3498db,
2312
- 4px -12px 0 #3498db,
2313
- -4px -8px 0 #3498db,
2314
- 0 -8px 0 #3498db,
2315
- 4px -8px 0 #3498db,
2316
- 8px -12px 0 #8b6914,
2317
- 8px -8px 0 #8b6914,
2318
- -4px -4px 0 #2c3e50,
2319
- 0 -4px 0 #2c3e50,
2320
- -4px 0 0 #2c3e50,
2321
- 4px 0 0 #2c3e50;
2322
- }
2323
- 50% {
2324
- box-shadow:
2325
- 0 -20px 0 #e74c3c,
2326
- -4px -20px 0 #e74c3c,
2327
- 4px -20px 0 #e74c3c,
2328
- -4px -24px 0 #e74c3c,
2329
- 0 -24px 0 #e74c3c,
2330
- 4px -24px 0 #e74c3c,
2331
- -4px -16px 0 #f5c6a0,
2332
- 0 -16px 0 #f5c6a0,
2333
- 4px -16px 0 #f5c6a0,
2334
- -4px -12px 0 #3498db,
2335
- 0 -12px 0 #3498db,
2336
- 4px -12px 0 #3498db,
2337
- -4px -8px 0 #3498db,
2338
- 0 -8px 0 #3498db,
2339
- 4px -8px 0 #3498db,
2340
- 8px -12px 0 #8b6914,
2341
- 8px -8px 0 #8b6914,
2342
- -4px -4px 0 #2c3e50,
2343
- 4px -4px 0 #2c3e50,
2344
- -4px 0 0 #2c3e50,
2345
- 0 0 0 #2c3e50;
2346
- }
2086
+ #basecamp-scene {
2087
+ position: relative;
2347
2088
  }
2348
2089
 
2349
- /* Speech bubble */
2350
2090
  .bc-speech {
2351
- position: absolute;
2352
- bottom: 52px;
2353
- left: calc(50% + 8px);
2091
+ position: relative;
2354
2092
  background: #1c2030;
2355
2093
  border: 1px solid #2a2f42;
2356
2094
  border-radius: 8px;
2357
2095
  padding: 6px 10px;
2358
2096
  font-size: 11px;
2097
+ color: #e4e8f0;
2359
2098
  white-space: nowrap;
2360
- color: var(--text-secondary);
2361
2099
  animation: bubble-float 3s ease-in-out infinite;
2362
2100
  }
2363
2101
 
2364
2102
  .bc-speech::after {
2365
2103
  content: '';
2366
2104
  position: absolute;
2367
- bottom: -6px;
2105
+ bottom: -5px;
2368
2106
  left: 20px;
2369
- width: 0;
2370
- height: 0;
2371
- border-left: 6px solid transparent;
2372
- border-right: 6px solid transparent;
2373
- border-top: 6px solid #1c2030;
2107
+ width: 8px;
2108
+ height: 8px;
2109
+ background: #1c2030;
2110
+ border-right: 1px solid #2a2f42;
2111
+ border-bottom: 1px solid #2a2f42;
2112
+ transform: rotate(45deg);
2374
2113
  }
2375
2114
 
2376
2115
  @keyframes bubble-float {
@@ -2388,6 +2127,20 @@ header.hidden {
2388
2127
  transition: opacity 0.5s;
2389
2128
  }
2390
2129
 
2130
+ .bc-speech.guide-mode {
2131
+ border-color: rgba(99, 102, 241, 0.4);
2132
+ background: #1a1d2e;
2133
+ }
2134
+
2135
+ .bc-speech.guide-mode::after {
2136
+ background: #1a1d2e;
2137
+ border-color: rgba(99, 102, 241, 0.4);
2138
+ }
2139
+
2140
+ #sherpa-quote {
2141
+ transition: opacity 0.5s;
2142
+ }
2143
+
2391
2144
  /* ============================================================
2392
2145
  Activity Trail
2393
2146
  ============================================================ */
@@ -132,7 +132,49 @@ else if (command === "help" || command === "--help" || command === "-h") {
132
132
  `);
133
133
  }
134
134
  else {
135
- // Default: start server
135
+ // Default: start server — auto-init if no config exists
136
+ const configPath = resolve(projectRoot, "sanjang.config.js");
137
+ if (!existsSync(configPath)) {
138
+ console.log("⛰ 설정 파일이 없습니다. 프로젝트를 분석합니다...\n");
139
+ const { generateConfig, detectApps } = await import("../lib/config.js");
140
+ const apps = detectApps(projectRoot);
141
+ let appDir;
142
+ if (apps.length >= 2) {
143
+ console.log("⛰ 여러 앱이 감지되었습니다:");
144
+ for (let i = 0; i < apps.length; i++) {
145
+ console.log(` ${i + 1}) ${apps[i].dir}/\t(${apps[i].framework})`);
146
+ }
147
+ console.log("");
148
+ const { createInterface } = await import("node:readline");
149
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
150
+ const answer = await new Promise((r) => { rl.question(" 어떤 앱을 띄울까요? [번호]: ", r); });
151
+ rl.close();
152
+ const idx = parseInt(answer) - 1;
153
+ if (idx < 0 || idx >= apps.length || isNaN(idx)) {
154
+ console.error("⛰ 잘못된 선택입니다.");
155
+ process.exit(1);
156
+ }
157
+ appDir = apps[idx].dir;
158
+ console.log(` → ${appDir}/ (${apps[idx].framework}) 선택됨\n`);
159
+ }
160
+ else if (apps.length === 1) {
161
+ appDir = apps[0].dir;
162
+ }
163
+ const result = generateConfig(projectRoot, { appDir, force });
164
+ if (result.created) {
165
+ console.log(`⛰ ${result.message}`);
166
+ console.log(` 프레임워크: ${result.framework}\n`);
167
+ }
168
+ // Add .sanjang to .gitignore
169
+ const gitignorePath = resolve(projectRoot, ".gitignore");
170
+ if (existsSync(gitignorePath)) {
171
+ const { readFileSync, appendFileSync } = await import("node:fs");
172
+ const content = readFileSync(gitignorePath, "utf8");
173
+ if (!content.includes(".sanjang")) {
174
+ appendFileSync(gitignorePath, "\n# Sanjang local dev camps\n.sanjang/\n");
175
+ }
176
+ }
177
+ }
136
178
  const { startServer } = await import("../lib/server.js");
137
179
  await startServer(projectRoot, { port });
138
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanjang",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Local dev environment manager for vibe coders",
5
5
  "type": "module",
6
6
  "bin": {